11import json
2+ import time
23import uuid
34from logging import getLogger
45from typing import Awaitable , Callable , Dict , List , Optional , Union
2930from chat_rag .llms import load_llm
3031from chat_rag .llms .types import Content , Message , ToolResult , ToolUse
3132
33+ from back .apps .health .models import Event
34+
3235logger = getLogger (__name__ )
3336
3437
@@ -198,6 +201,40 @@ async def resolve_references(reference_kis, retriever_config):
198201 }
199202
200203
204+ async def log_llm_event (
205+ event_type : str ,
206+ is_success : bool ,
207+ data : dict
208+ ):
209+ """
210+ Async function to log LLM-related events to the Event model.
211+
212+ Parameters:
213+ -----------
214+ event_type : str
215+ Type of event (e.g., 'llm_call_start', 'llm_call_complete')
216+ is_success : bool
217+ Whether the event represents a successful operation
218+ llm_call_id : str
219+ Unique identifier for the LLM call
220+ llm_config_name : str
221+ Name of the LLM configuration
222+ conversation_id : int
223+ ID of the conversation
224+ start_time : float
225+ Start time of the LLM call (used to calculate duration for 'complete' events)
226+ additional_data : dict, optional
227+ Any additional data to include in the event
228+ """
229+
230+ # Create the event asynchronously
231+ await database_sync_to_async (Event .objects .create )(
232+ event_type = event_type ,
233+ is_success = is_success ,
234+ data = data
235+ )
236+
237+
201238async def query_llm (
202239 llm_config_name : str ,
203240 conversation_id : int ,
@@ -221,18 +258,20 @@ async def query_llm(
221258 # if the llm config is mistral then return an error that mistral is not supported yet
222259 if llm_config .llm_type == LLMChoices .MISTRAL .value :
223260 await error_handler ({
224- "payload" : {
225- "errors" : "Error: Mistral is temporarily unavailable. We're working to add support for it soon. For now, please select a different model like OpenAI." ,
226- "request_info" : {"llm_config_name" : llm_config_name },
227- }
228- })
261+ "errors" : "Error: Mistral is temporarily unavailable. We're working to add support for it soon. For now, please select a different model provider like OpenAI." ,
262+ "llm_config_name" : llm_config_name ,
263+ "conversation_id" : conversation_id
264+ },
265+ event_type = "llm_config_not_found"
266+ )
229267 except LLMConfig .DoesNotExist :
230268 await error_handler ({
231- "payload" : {
232269 "errors" : f"LLM config with name: { llm_config_name } does not exist." ,
233- "request_info" : {"llm_config_name" : llm_config_name },
234- }
235- })
270+ "llm_config_name" : llm_config_name ,
271+ "conversation_id" : conversation_id
272+ },
273+ event_type = "llm_config_not_found"
274+ )
236275 return
237276
238277 conv = await database_sync_to_async (Conversation .objects .get )(pk = conversation_id )
@@ -252,25 +291,28 @@ async def query_llm(
252291 messages = messages [1 :]
253292 elif not prev_messages :
254293 await error_handler ({
255- "payload" : {
256- "errors" : "Error: No previous messages and no messages provided." ,
257- "request_info" : {"conversation_id" : conversation_id },
258- }
259- })
294+ "errors" : "Error: No previous messages and no messages provided." ,
295+ "conversation_id" : conversation_id ,
296+ },
297+ )
260298 return
261299 if messages :
262300 new_messages .extend (messages )
263301 else :
264302 new_messages = messages
265303 if new_messages is None :
266304 await error_handler ({
267- "payload" : {
268- "errors" : "Error: No messages provided." ,
269- "request_info" : {"conversation_id" : conversation_id },
270- }
271- })
305+ "errors" : "Error: No messages provided." ,
306+ "conversation_id" : conversation_id ,
307+ },
308+ )
272309 return
273310
311+
312+ # Generate a unique ID for this LLM call
313+ llm_call_id = str (uuid .uuid4 ())
314+ start_time = time .perf_counter ()
315+
274316 try :
275317 # Decrypt the API key from the LLMConfig if available.
276318 api_key = None
@@ -288,6 +330,25 @@ async def query_llm(
288330 api_key = api_key ,
289331 )
290332
333+ await log_llm_event (
334+ event_type = "llm_call_start" ,
335+ is_success = True ,
336+ data = {
337+ "llm_call_id" : llm_call_id ,
338+ "llm_config_name" : llm_config_name ,
339+ "conversation_id" : conversation_id ,
340+ "temperature" : temperature ,
341+ "max_tokens" : max_tokens ,
342+ "seed" : seed ,
343+ "tools" : tools ,
344+ "tool_choice" : tool_choice ,
345+ "messages" : new_messages ,
346+ "schema" : response_schema ,
347+ "cache_config" : cache_config ,
348+ "stream" : stream ,
349+ }
350+ )
351+
291352 if response_schema :
292353 response_message = await llm .aparse (
293354 messages = new_messages ,
@@ -356,14 +417,28 @@ async def query_llm(
356417 "last_chunk" : True ,
357418 }
358419
420+ await log_llm_event (
421+ event_type = "llm_call_complete" ,
422+ is_success = True ,
423+ data = {
424+ "llm_call_id" : llm_call_id ,
425+ "duration_seconds" : time .perf_counter () - start_time ,
426+ }
427+ )
428+
359429 except Exception as e :
360- logger .error ( "Error during LLM query" , exc_info = e )
361- await error_handler ({
362- "payload" : {
430+ logger .exception ( f "Error during llm call: { e } " )
431+ await error_handler (
432+ {
363433 "errors" : "There was an error generating the response. Please try again or contact the administrator." ,
364- "request_info" : {"conversation_id" : conversation_id },
365- }
366- })
434+ "error_message" : str (e ),
435+ "llm_config_name" : llm_config_name ,
436+ "conversation_id" : conversation_id ,
437+ "llm_call_id" : llm_call_id ,
438+ "duration_seconds" : time .perf_counter () - start_time ,
439+ },
440+ event_type = "llm_call_complete" ,
441+ )
367442 return
368443
369444
@@ -449,6 +524,7 @@ async def process_llm_request(self, data):
449524
450525 lm_msg_id = str (uuid .uuid4 ())
451526 data = serializer .validated_data
527+
452528 async for chunk in query_llm (
453529 data ["llm_config_name" ],
454530 data ["conversation_id" ],
@@ -463,7 +539,7 @@ async def process_llm_request(self, data):
463539 data .get ("cache_config" ),
464540 data .get ("response_schema" ),
465541 data .get ("stream" ),
466- error_handler = self .error_response ,
542+ error_handler = self .llm_error_response ,
467543 ):
468544 await self .send (
469545 json .dumps (
@@ -555,9 +631,24 @@ async def process_prompt_request(self, data):
555631 }
556632 )
557633
558-
559-
560634 async def error_response (self , data : dict ):
561635 data ["status" ] = WSStatusCodes .bad_request .value
562636 data ["type" ] = RPCMessageType .error .value
563637 await self .send (json .dumps (data ))
638+
639+ async def llm_error_response (self , data : dict , event_type : str = None ):
640+ if event_type :
641+ await log_llm_event (
642+ event_type = event_type ,
643+ is_success = False ,
644+ data = data
645+ )
646+ # This is info sent to the SDK, so don't send a detailed error message for now.
647+ return await self .error_response (
648+ {
649+ "payload" : {
650+ "errors" : data ["errors" ],
651+ "request_info" : {"conversation_id" : data ["conversation_id" ], "llm_config_name" : data ["llm_config_name" ]},
652+ }
653+ }
654+ )
0 commit comments