1414 APIStatusError as LLSApiStatusError ,
1515)
1616from openai ._exceptions import APIStatusError as OpenAIAPIStatusError
17+ from opentelemetry import trace
1718
1819from authentication import get_auth_dependency
1920from authentication .interface import AuthTuple
6566)
6667from utils .mcp_headers import McpHeaders , mcp_headers_dependency
6768from utils .mcp_oauth_probe import check_mcp_auth
69+ from utils .otel_tracing import (
70+ SpanAttributes ,
71+ SpanEvents ,
72+ add_span_event ,
73+ anonymize_value ,
74+ set_span_attributes ,
75+ )
6876from utils .query import (
6977 extract_provider_and_model_from_model_id ,
7078 handle_known_apistatus_errors ,
93101from utils .vector_search import build_rag_context
94102
95103logger = get_logger (__name__ )
104+ tracer = trace .get_tracer (__name__ )
96105router = APIRouter (tags = ["streaming_query" ])
97106
98107# Tracks background topic summary tasks for graceful shutdown.
@@ -158,11 +167,55 @@ async def streaming_query_endpoint_handler( # pylint: disable=too-many-locals
158167 - 500: Internal Server Error - Configuration not loaded or other server errors
159168 - 503: Service Unavailable - Unable to connect to OGX backend
160169 """
170+ root_span = tracer .start_span ("streaming_query.handle_request" )
171+ try :
172+ return await _handle_streaming_query_with_tracing (
173+ request , query_request , auth , mcp_headers , root_span
174+ )
175+ except Exception :
176+ root_span .end ()
177+ raise
178+
179+
180+ async def _handle_streaming_query_with_tracing ( # pylint: disable=too-many-locals
181+ request : Request ,
182+ query_request : QueryRequest ,
183+ auth : AuthTuple ,
184+ mcp_headers : McpHeaders ,
185+ root_span : trace .Span ,
186+ ) -> StreamingResponse :
187+ """Handle streaming query request with OTEL tracing instrumentation.
188+
189+ Parameters:
190+ request: The incoming HTTP request.
191+ query_request: Request payload containing query and optional parameters.
192+ auth: Authentication tuple (user_id, username, skip_check, token).
193+ mcp_headers: Headers to be passed to MCP servers.
194+ root_span: OpenTelemetry root span for this request.
195+
196+ Returns:
197+ StreamingResponse with SSE-formatted events.
198+
199+ Raises:
200+ HTTPException: On authentication, authorization, quota, or model errors.
201+ """
161202 check_configuration_loaded (configuration )
162203
163204 user_id , _user_name , _skip_userid_check , token = auth
164205 started_at = datetime .datetime .now (datetime .UTC ).strftime ("%Y-%m-%dT%H:%M:%SZ" )
165206
207+ # Set initial span attributes
208+ set_span_attributes (
209+ root_span ,
210+ {
211+ SpanAttributes .USER_ID : anonymize_value (user_id ),
212+ SpanAttributes .INPUT : anonymize_value (query_request .query ),
213+ SpanAttributes .REQUEST_ATTACHMENTS_COUNT : (
214+ len (query_request .attachments ) if query_request .attachments else 0
215+ ),
216+ },
217+ )
218+
166219 # Check MCP Auth
167220 await check_mcp_auth (configuration , mcp_headers , token , request .headers )
168221
@@ -181,6 +234,9 @@ async def streaming_query_endpoint_handler( # pylint: disable=too-many-locals
181234 if query_request .attachments :
182235 validate_attachments_metadata (query_request .attachments )
183236
237+ # Validation completed
238+ add_span_event (root_span , SpanEvents .VALIDATION_COMPLETED )
239+
184240 # Retrieve conversation if conversation_id is provided
185241 user_conversation = None
186242 if query_request .conversation_id :
@@ -291,6 +347,7 @@ async def streaming_query_endpoint_handler( # pylint: disable=too-many-locals
291347 responses_params = responses_params ,
292348 endpoint_path = endpoint_path ,
293349 image_attachments = image_attachments ,
350+ root_span = root_span ,
294351 ),
295352 media_type = response_media_type ,
296353 )
@@ -316,6 +373,7 @@ async def streaming_query_endpoint_handler( # pylint: disable=too-many-locals
316373 responses_params = responses_params ,
317374 turn_summary = turn_summary ,
318375 background_topic_summary_tasks = _background_topic_summary_tasks ,
376+ root_span = root_span ,
319377 ),
320378 media_type = response_media_type ,
321379 )
@@ -344,6 +402,7 @@ async def generate_response_with_compaction(
344402 responses_params : ResponsesApiParams ,
345403 endpoint_path : str ,
346404 image_attachments : Optional [list [Attachment ]] = None ,
405+ root_span : Optional [trace .Span ] = None ,
347406) -> AsyncIterator [str ]:
348407 """Stream a response for a conversation that requires compaction.
349408
@@ -359,79 +418,85 @@ async def generate_response_with_compaction(
359418 responses_params: The base Responses API parameters.
360419 endpoint_path: API endpoint path used for metric labeling.
361420 image_attachments: Image attachments for multimodal prompt construction.
421+ root_span: OpenTelemetry root span for this request.
362422
363423 Yields:
364424 SSE-formatted strings.
365425 """
366- media_type = context .query_request .media_type or MEDIA_TYPE_JSON
367- yield stream_start_event (
368- conversation_id = context .conversation_id ,
369- request_id = context .request_id ,
370- )
371-
372- compacted_original_input : Optional [ResponseInput ] = None
373426 try :
374- async for item in apply_compaction (
375- context .client ,
376- responses_params ,
377- configuration .inference ,
378- configuration .compaction ,
379- emit_events = True ,
380- cache = configured_conversation_cache (),
381- user_id = context .user_id ,
382- skip_user_id_check = context .skip_userid_check ,
383- ):
384- if isinstance (item , CompactionStartedEvent ):
385- yield stream_compaction_event (context .conversation_id )
386- elif isinstance (item , CompactionResult ):
387- responses_params = item .params
388- compacted_original_input = item .original_input
389-
390- generator , turn_summary = await retrieve_agent_response_generator (
391- responses_params = responses_params ,
392- context = context ,
393- endpoint_path = endpoint_path ,
394- image_attachments = image_attachments ,
395- )
396- except HTTPException as e :
397- yield http_exception_stream_event (e )
398- return
399- except RuntimeError as e : # library mode wraps 413 into runtime error
400- error_response = (
401- PromptTooLongResponse (model = responses_params .model )
402- if is_context_length_error (str (e ))
403- else InternalServerErrorResponse .generic ()
404- )
405- yield stream_http_error_event (error_response , media_type )
406- return
407- except APIConnectionError as e :
408- yield stream_http_error_event (
409- ServiceUnavailableResponse (backend_name = "OGX" , cause = str (e )),
410- media_type ,
411- )
412- return
413- except (LLSApiStatusError , OpenAIAPIStatusError ) as e :
414- yield stream_http_error_event (
415- handle_known_apistatus_errors (e , responses_params .model ), media_type
416- )
417- return
418-
419- # Combine inline RAG results (BYOK + Solr) with tool-based results
420- if context .moderation_result .decision == "passed" :
421- turn_summary .referenced_documents = deduplicate_referenced_documents (
422- context .inline_rag_context .referenced_documents
423- + turn_summary .referenced_documents
427+ media_type = context .query_request .media_type or MEDIA_TYPE_JSON
428+ yield stream_start_event (
429+ conversation_id = context .conversation_id ,
430+ request_id = context .request_id ,
424431 )
425432
426- # The start event was already emitted above; delegate the rest (re-yield,
427- # finalization, compacted-turn storage) to the shared generator.
428- async for event in generate_agent_response (
429- generator ,
430- context ,
431- responses_params ,
432- turn_summary ,
433- background_topic_summary_tasks = _background_topic_summary_tasks ,
434- emit_start = False ,
435- original_input = compacted_original_input ,
436- ):
437- yield event
433+ compacted_original_input : Optional [ResponseInput ] = None
434+ try :
435+ async for item in apply_compaction (
436+ context .client ,
437+ responses_params ,
438+ configuration .inference ,
439+ configuration .compaction ,
440+ emit_events = True ,
441+ cache = configured_conversation_cache (),
442+ user_id = context .user_id ,
443+ skip_user_id_check = context .skip_userid_check ,
444+ ):
445+ if isinstance (item , CompactionStartedEvent ):
446+ yield stream_compaction_event (context .conversation_id )
447+ elif isinstance (item , CompactionResult ):
448+ responses_params = item .params
449+ compacted_original_input = item .original_input
450+
451+ generator , turn_summary = await retrieve_agent_response_generator (
452+ responses_params = responses_params ,
453+ context = context ,
454+ endpoint_path = endpoint_path ,
455+ image_attachments = image_attachments ,
456+ )
457+ except HTTPException as e :
458+ yield http_exception_stream_event (e )
459+ return
460+ except RuntimeError as e : # library mode wraps 413 into runtime error
461+ error_response = (
462+ PromptTooLongResponse (model = responses_params .model )
463+ if is_context_length_error (str (e ))
464+ else InternalServerErrorResponse .generic ()
465+ )
466+ yield stream_http_error_event (error_response , media_type )
467+ return
468+ except APIConnectionError as e :
469+ yield stream_http_error_event (
470+ ServiceUnavailableResponse (backend_name = "OGX" , cause = str (e )),
471+ media_type ,
472+ )
473+ return
474+ except (LLSApiStatusError , OpenAIAPIStatusError ) as e :
475+ yield stream_http_error_event (
476+ handle_known_apistatus_errors (e , responses_params .model ), media_type
477+ )
478+ return
479+
480+ # Combine inline RAG results (BYOK + Solr) with tool-based results
481+ if context .moderation_result .decision == "passed" :
482+ turn_summary .referenced_documents = deduplicate_referenced_documents (
483+ context .inline_rag_context .referenced_documents
484+ + turn_summary .referenced_documents
485+ )
486+
487+ # The start event was already emitted above; delegate the rest (re-yield,
488+ # finalization, compacted-turn storage) to the shared generator.
489+ async for event in generate_agent_response (
490+ generator ,
491+ context ,
492+ responses_params ,
493+ turn_summary ,
494+ background_topic_summary_tasks = _background_topic_summary_tasks ,
495+ emit_start = False ,
496+ original_input = compacted_original_input ,
497+ root_span = root_span ,
498+ ):
499+ yield event
500+ finally :
501+ if root_span is not None :
502+ root_span .end ()
0 commit comments