|
17 | 17 | from .extraction import ( |
18 | 18 | GenAIExtractor, |
19 | 19 | extract_agent_response_from_attrs, |
| 20 | + extract_tool_call_from_span, |
| 21 | + extract_tool_result_from_span, |
20 | 22 | extract_user_text_from_attrs, |
21 | 23 | is_invocation_span, |
22 | 24 | is_llm_span, |
23 | | - parse_tool_response_content, |
24 | 25 | ) |
25 | 26 | from .loader.base import Span, Trace |
26 | 27 | from .trace_attrs import ( |
27 | 28 | OTEL_GENAI_INPUT_MESSAGES, |
28 | 29 | OTEL_GENAI_OUTPUT_MESSAGES, |
29 | | - OTEL_GENAI_TOOL_CALL_ARGUMENTS, |
30 | 30 | OTEL_GENAI_TOOL_CALL_ID, |
31 | | - OTEL_GENAI_TOOL_CALL_RESULT, |
32 | | - OTEL_GENAI_TOOL_NAME, |
33 | 31 | ) |
34 | 32 | from .utils.genai_messages import ( |
35 | 33 | ASSISTANT_ROLES, |
36 | 34 | USER_ROLES, |
37 | 35 | extract_text_from_message, |
38 | | - extract_tool_call_args_from_messages, |
39 | 36 | extract_tool_calls_from_message, |
40 | 37 | parse_json_attr, |
41 | 38 | ) |
@@ -385,68 +382,31 @@ def _extract_tool_calls( |
385 | 382 | tool_responses: list[_ToolResponse] = [] |
386 | 383 |
|
387 | 384 | for tool_span in tool_spans: |
388 | | - tool_name = tool_span.get_tag(OTEL_GENAI_TOOL_NAME) |
389 | | - if not tool_name: |
390 | | - logger.warning(f"Tool span missing gen_ai.tool.name: {tool_span.operation_name}") |
| 385 | + tool_call = extract_tool_call_from_span(tool_span) |
| 386 | + if tool_call is None: |
| 387 | + logger.warning(f"Tool span missing tool name: {tool_span.operation_name}") |
391 | 388 | continue |
392 | 389 |
|
393 | | - tool_call_id = tool_span.get_tag(OTEL_GENAI_TOOL_CALL_ID) |
| 390 | + # Use the real semconv tool_call_id for dedup (None when not present on |
| 391 | + # the span), rather than the shared function's span_id/"unknown" fallback. |
| 392 | + real_tool_call_id = tool_span.get_tag(OTEL_GENAI_TOOL_CALL_ID) |
394 | 393 |
|
395 | | - args_raw = tool_span.get_tag(OTEL_GENAI_TOOL_CALL_ARGUMENTS, "{}") |
396 | | - args = parse_json_attr(args_raw, "gen_ai.tool.call.arguments") |
397 | | - if not isinstance(args, dict): |
398 | | - args = {} |
399 | | - |
400 | | - if not args: |
401 | | - input_msgs_raw = tool_span.get_tag(OTEL_GENAI_INPUT_MESSAGES) |
402 | | - if input_msgs_raw: |
403 | | - args, _ = extract_tool_call_args_from_messages(input_msgs_raw, tool_name) |
404 | | - |
405 | | - tc = _ToolCall(name=tool_name, args=args, id=tool_call_id) |
406 | | - if tool_call_id: |
407 | | - tool_calls_by_id[tool_call_id] = tc |
| 394 | + tc = _ToolCall(name=tool_call["name"], args=tool_call["args"], id=real_tool_call_id) |
| 395 | + if real_tool_call_id: |
| 396 | + tool_calls_by_id[real_tool_call_id] = tc |
408 | 397 | else: |
409 | 398 | tool_calls_no_id.append(tc) |
410 | 399 |
|
411 | | - result_raw = tool_span.get_tag(OTEL_GENAI_TOOL_CALL_RESULT) |
412 | | - if result_raw: |
413 | | - result_data = parse_tool_response_content(result_raw) |
414 | | - logger.debug(f"Tool {tool_name} result: {str(result_data)[:100]}") |
| 400 | + tool_result = extract_tool_result_from_span(tool_span) |
| 401 | + if tool_result: |
| 402 | + logger.debug(f"Tool {tool_call['name']} result: {str(tool_result['response'])[:100]}") |
415 | 403 | tool_responses.append( |
416 | 404 | _ToolResponse( |
417 | | - name=tool_name, |
418 | | - response=result_data, |
419 | | - id=tool_call_id, |
| 405 | + name=tool_call["name"], |
| 406 | + response=tool_result["response"], |
| 407 | + id=real_tool_call_id, |
420 | 408 | ) |
421 | 409 | ) |
422 | | - else: |
423 | | - output_msgs_raw = tool_span.get_tag(OTEL_GENAI_OUTPUT_MESSAGES) |
424 | | - if output_msgs_raw: |
425 | | - output_msgs = parse_json_attr(output_msgs_raw, "gen_ai.output.messages") |
426 | | - if isinstance(output_msgs, list): |
427 | | - for msg in output_msgs: |
428 | | - if not isinstance(msg, dict): |
429 | | - continue |
430 | | - for part in msg.get("parts", []): |
431 | | - if not isinstance(part, dict): |
432 | | - continue |
433 | | - if part.get("type") == "tool_call_response" and "response" in part: |
434 | | - resp = part["response"] |
435 | | - if isinstance(resp, list): |
436 | | - texts = [t.get("text", "") for t in resp if isinstance(t, dict) and "text" in t] |
437 | | - result_data = parse_tool_response_content(" ".join(texts)) |
438 | | - elif isinstance(resp, dict): |
439 | | - result_data = resp |
440 | | - else: |
441 | | - result_data = {"result": str(resp)} |
442 | | - tool_responses.append( |
443 | | - _ToolResponse( |
444 | | - name=tool_name, |
445 | | - response=result_data, |
446 | | - id=tool_call_id, |
447 | | - ) |
448 | | - ) |
449 | | - break |
450 | 410 |
|
451 | 411 | if llm_spans: |
452 | 412 | for llm_span in llm_spans: |
|
0 commit comments