|
7 | 7 |
|
8 | 8 | from fastapi import HTTPException |
9 | 9 | from ogx_client import APIConnectionError, APIStatusError, AsyncOgxClient |
| 10 | +from opentelemetry import trace |
10 | 11 | from pydantic_ai.exceptions import ( |
11 | 12 | AgentRunError, |
12 | 13 | ) |
|
36 | 37 | process_native_tool_result, |
37 | 38 | ) |
38 | 39 | from utils.conversations import append_turn_items_to_conversation |
| 40 | +from utils.otel_tracing import ( |
| 41 | + SpanAttributes, |
| 42 | + SpanEvents, |
| 43 | + add_span_event, |
| 44 | + set_span_attributes, |
| 45 | +) |
39 | 46 | from utils.pydantic_ai_helpers import build_agent |
40 | 47 | from utils.query import ( |
41 | 48 | build_multimodal_input, |
|
45 | 52 | from utils.token_counter import TokenCounter |
46 | 53 |
|
47 | 54 | logger = get_logger(__name__) |
| 55 | +tracer = trace.get_tracer(__name__) |
48 | 56 |
|
49 | 57 | AgentInferenceError: TypeAlias = ( |
50 | 58 | AgentRunError | APIStatusError | APIConnectionError | RuntimeError |
@@ -180,20 +188,41 @@ def build_turn_summary_from_agent_run( |
180 | 188 | turn_summary=TurnSummary(), |
181 | 189 | ) |
182 | 190 |
|
| 191 | + # Track tool calls for OTEL instrumentation |
| 192 | + tool_call_names: list[str] = [] |
| 193 | + |
183 | 194 | for message in run_result.new_messages(): |
184 | 195 | if isinstance(message, ModelResponse): |
185 | 196 | if message.text: |
186 | 197 | state.turn_summary.llm_response = message.text |
187 | 198 | for tool_call_part in message.tool_calls: |
188 | 199 | process_function_tool_call(state, tool_call_part) |
| 200 | + tool_call_names.append(tool_call_part.tool_name) |
189 | 201 | for call_part, return_part in message.native_tool_calls: |
190 | 202 | process_native_tool_call(state, call_part) |
191 | 203 | process_native_tool_result(state, return_part) |
| 204 | + tool_call_names.append(call_part.tool_name) |
192 | 205 | elif isinstance(message, ModelRequest): |
193 | 206 | for request_part in message.parts: |
194 | 207 | if isinstance(request_part, ToolReturnPart): |
195 | 208 | process_function_tool_result(state, request_part) |
196 | 209 |
|
| 210 | + # Add tool execution attributes to current span (parent llm.inference span) |
| 211 | + current_span = trace.get_current_span() |
| 212 | + if current_span.is_recording() and tool_call_names: |
| 213 | + set_span_attributes( |
| 214 | + current_span, |
| 215 | + { |
| 216 | + SpanAttributes.TOOL_CALLS_COUNT: len(tool_call_names), |
| 217 | + SpanAttributes.TOOL_CALLS_NAMES: tool_call_names, |
| 218 | + }, |
| 219 | + ) |
| 220 | + add_span_event( |
| 221 | + current_span, |
| 222 | + SpanEvents.TOOL_EXECUTION_COMPLETED, |
| 223 | + {"tool.calls": ", ".join(tool_call_names)}, |
| 224 | + ) |
| 225 | + |
197 | 226 | state.turn_summary.id = run_result.response.provider_response_id or "" |
198 | 227 | state.turn_summary.token_usage = extract_agent_token_usage( |
199 | 228 | run_result.usage, |
@@ -231,44 +260,83 @@ async def retrieve_agent_response( |
231 | 260 | Raises: |
232 | 261 | HTTPException: On moderation is not applicable; on agent or provider failure. |
233 | 262 | """ |
234 | | - if moderation_result.decision == "blocked": |
235 | | - await append_turn_items_to_conversation( |
236 | | - client, |
237 | | - responses_params.conversation, |
238 | | - responses_params.input, |
239 | | - [moderation_result.refusal_response], |
| 263 | + with tracer.start_as_current_span("llm.inference") as span: |
| 264 | + # Extract provider and model from model_id |
| 265 | + provider_id, model_id = extract_provider_and_model_from_model_id( |
| 266 | + responses_params.model |
240 | 267 | ) |
241 | | - return TurnSummary( |
242 | | - id=moderation_result.moderation_id, |
243 | | - llm_response=moderation_result.message, |
244 | | - ) |
245 | | - try: |
246 | | - agent = build_agent( |
247 | | - client, |
248 | | - responses_params, |
249 | | - configuration, |
250 | | - shields=shield_ids, |
251 | | - no_tools=no_tools, |
| 268 | + |
| 269 | + # Set LLM attributes |
| 270 | + set_span_attributes( |
| 271 | + span, |
| 272 | + { |
| 273 | + SpanAttributes.LLM_MODEL_ID: model_id, |
| 274 | + SpanAttributes.LLM_PROVIDER_ID: provider_id, |
| 275 | + }, |
252 | 276 | ) |
253 | | - logger.debug("Starting agent non-streaming response processing") |
254 | | - if image_attachments: |
255 | | - prompt = build_multimodal_input( |
256 | | - cast(str, responses_params.input), |
257 | | - image_attachments, |
| 277 | + |
| 278 | + if moderation_result.decision == "blocked": |
| 279 | + await append_turn_items_to_conversation( |
| 280 | + client, |
| 281 | + responses_params.conversation, |
| 282 | + responses_params.input, |
| 283 | + [moderation_result.refusal_response], |
258 | 284 | ) |
259 | | - else: |
260 | | - prompt = cast(str, responses_params.input) |
261 | | - run_result = await agent.run(prompt) |
262 | | - except (AgentRunError, APIStatusError, APIConnectionError, RuntimeError) as exc: |
263 | | - response = map_agent_inference_error(exc, responses_params.model) |
264 | | - raise HTTPException(**response.model_dump()) from exc |
265 | | - |
266 | | - vector_store_ids = extract_vector_store_ids_from_tools(responses_params.tools) |
267 | | - rag_id_mapping = configuration.rag_id_mapping |
268 | | - return build_turn_summary_from_agent_run( |
269 | | - run_result, |
270 | | - model_id=responses_params.model, |
271 | | - endpoint_path=endpoint_path, |
272 | | - vector_store_ids=vector_store_ids, |
273 | | - rag_id_mapping=rag_id_mapping, |
274 | | - ) |
| 285 | + return TurnSummary( |
| 286 | + id=moderation_result.moderation_id, |
| 287 | + llm_response=moderation_result.message, |
| 288 | + ) |
| 289 | + |
| 290 | + # Emit inference started event |
| 291 | + add_span_event(span, SpanEvents.LLM_INFERENCE_STARTED) |
| 292 | + |
| 293 | + try: |
| 294 | + agent = build_agent( |
| 295 | + client, |
| 296 | + responses_params, |
| 297 | + configuration, |
| 298 | + shields=shield_ids, |
| 299 | + no_tools=no_tools, |
| 300 | + ) |
| 301 | + logger.debug("Starting agent non-streaming response processing") |
| 302 | + if image_attachments: |
| 303 | + prompt = build_multimodal_input( |
| 304 | + cast(str, responses_params.input), |
| 305 | + image_attachments, |
| 306 | + ) |
| 307 | + else: |
| 308 | + prompt = cast(str, responses_params.input) |
| 309 | + run_result = await agent.run(prompt) |
| 310 | + except ( |
| 311 | + AgentRunError, |
| 312 | + APIStatusError, |
| 313 | + APIConnectionError, |
| 314 | + RuntimeError, |
| 315 | + ) as exc: |
| 316 | + response = map_agent_inference_error(exc, responses_params.model) |
| 317 | + raise HTTPException(**response.model_dump()) from exc |
| 318 | + |
| 319 | + # Set token usage attributes |
| 320 | + if run_result.usage: |
| 321 | + set_span_attributes( |
| 322 | + span, |
| 323 | + { |
| 324 | + SpanAttributes.LLM_USAGE_INPUT_TOKENS: run_result.usage.input_tokens, |
| 325 | + SpanAttributes.LLM_USAGE_OUTPUT_TOKENS: run_result.usage.output_tokens, |
| 326 | + }, |
| 327 | + ) |
| 328 | + |
| 329 | + vector_store_ids = extract_vector_store_ids_from_tools(responses_params.tools) |
| 330 | + rag_id_mapping = configuration.rag_id_mapping |
| 331 | + turn_summary = build_turn_summary_from_agent_run( |
| 332 | + run_result, |
| 333 | + model_id=responses_params.model, |
| 334 | + endpoint_path=endpoint_path, |
| 335 | + vector_store_ids=vector_store_ids, |
| 336 | + rag_id_mapping=rag_id_mapping, |
| 337 | + ) |
| 338 | + |
| 339 | + # Emit inference completed event after successful summary build |
| 340 | + add_span_event(span, SpanEvents.LLM_INFERENCE_COMPLETED) |
| 341 | + |
| 342 | + return turn_summary |
0 commit comments