Skip to content

Commit e2f1333

Browse files
consolidate duplicated tool call extractions
1 parent 743389a commit e2f1333

3 files changed

Lines changed: 54 additions & 86 deletions

File tree

src/agentevals/converter.py

Lines changed: 14 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@
2020

2121
from .extraction import (
2222
extract_agent_response_from_attrs,
23+
extract_tool_call_from_span,
24+
extract_tool_result_from_span,
2325
extract_user_text_from_attrs,
2426
get_extractor,
2527
parse_json,
@@ -30,11 +32,7 @@
3032
ADK_LLM_REQUEST,
3133
ADK_LLM_RESPONSE,
3234
ADK_SCOPE_VALUE,
33-
ADK_TOOL_CALL_ARGS,
34-
ADK_TOOL_RESPONSE,
3535
OTEL_GENAI_AGENT_NAME,
36-
OTEL_GENAI_TOOL_CALL_ID,
37-
OTEL_GENAI_TOOL_NAME,
3836
OTEL_SCOPE,
3937
)
4038

@@ -223,37 +221,24 @@ def _extract_tool_trajectory(
223221
def _extract_from_tool_span(
224222
tool_span: Span,
225223
) -> tuple[genai_types.FunctionCall | None, genai_types.FunctionResponse | None]:
226-
tool_name = tool_span.get_tag(OTEL_GENAI_TOOL_NAME)
227-
tool_call_id = tool_span.get_tag(OTEL_GENAI_TOOL_CALL_ID)
228-
229-
if not tool_name:
230-
# Fallback: parse tool name from operationName "execute_tool <name>"
231-
op = tool_span.operation_name
232-
if op.startswith("execute_tool "):
233-
tool_name = op[len("execute_tool ") :]
234-
else:
235-
logger.warning("execute_tool span %s: no tool name found", tool_span.span_id)
236-
return None, None
237-
238-
args_raw = tool_span.get_tag(ADK_TOOL_CALL_ARGS, "{}")
239-
args = parse_json(args_raw)
224+
tool_call = extract_tool_call_from_span(tool_span)
225+
if tool_call is None:
226+
logger.warning("execute_tool span %s: no tool name found", tool_span.span_id)
227+
return None, None
240228

241229
fc = genai_types.FunctionCall(
242-
name=tool_name,
243-
args=args if args else {},
244-
id=tool_call_id,
230+
name=tool_call["name"],
231+
args=tool_call["args"],
232+
id=tool_call["id"],
245233
)
246234

247-
response_raw = tool_span.get_tag(ADK_TOOL_RESPONSE)
235+
tool_result = extract_tool_result_from_span(tool_span)
248236
fr = None
249-
if response_raw:
250-
response_data = parse_json(response_raw)
251-
# Response format varies: MCP uses {"content": [...], "isError": false},
252-
# other tools return flat dicts. We pass through as-is.
237+
if tool_result:
253238
fr = genai_types.FunctionResponse(
254-
name=tool_name,
255-
response=response_data if response_data else {},
256-
id=tool_call_id,
239+
name=tool_call["name"],
240+
response=tool_result["response"],
241+
id=tool_call["id"],
257242
)
258243

259244
return fc, fr

src/agentevals/extraction.py

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -235,6 +235,29 @@ def extract_tool_result_from_attrs(attrs: dict[str, Any]) -> dict[str, Any] | No
235235
return None
236236

237237

238+
# ---------------------------------------------------------------------------
239+
# Span-level convenience wrappers
240+
# ---------------------------------------------------------------------------
241+
242+
243+
def extract_tool_call_from_span(span: Span) -> dict[str, Any] | None:
244+
"""Extract tool call info from a Span object.
245+
246+
Delegates to extract_tool_call_from_attrs using the span's tags, operation
247+
name, and span ID. Returns {"id", "name", "args"} or None.
248+
"""
249+
return extract_tool_call_from_attrs(span.tags, span.operation_name, span.span_id)
250+
251+
252+
def extract_tool_result_from_span(span: Span) -> dict[str, Any] | None:
253+
"""Extract tool result from a Span object.
254+
255+
Delegates to extract_tool_result_from_attrs using the span's tags.
256+
Returns {"response": dict, "isError": bool} or None.
257+
"""
258+
return extract_tool_result_from_attrs(span.tags)
259+
260+
238261
# ---------------------------------------------------------------------------
239262
# Span classification helpers
240263
# ---------------------------------------------------------------------------

src/agentevals/genai_converter.py

Lines changed: 17 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -17,25 +17,22 @@
1717
from .extraction import (
1818
GenAIExtractor,
1919
extract_agent_response_from_attrs,
20+
extract_tool_call_from_span,
21+
extract_tool_result_from_span,
2022
extract_user_text_from_attrs,
2123
is_invocation_span,
2224
is_llm_span,
23-
parse_tool_response_content,
2425
)
2526
from .loader.base import Span, Trace
2627
from .trace_attrs import (
2728
OTEL_GENAI_INPUT_MESSAGES,
2829
OTEL_GENAI_OUTPUT_MESSAGES,
29-
OTEL_GENAI_TOOL_CALL_ARGUMENTS,
3030
OTEL_GENAI_TOOL_CALL_ID,
31-
OTEL_GENAI_TOOL_CALL_RESULT,
32-
OTEL_GENAI_TOOL_NAME,
3331
)
3432
from .utils.genai_messages import (
3533
ASSISTANT_ROLES,
3634
USER_ROLES,
3735
extract_text_from_message,
38-
extract_tool_call_args_from_messages,
3936
extract_tool_calls_from_message,
4037
parse_json_attr,
4138
)
@@ -385,68 +382,31 @@ def _extract_tool_calls(
385382
tool_responses: list[_ToolResponse] = []
386383

387384
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}")
391388
continue
392389

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)
394393

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
408397
else:
409398
tool_calls_no_id.append(tc)
410399

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]}")
415403
tool_responses.append(
416404
_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,
420408
)
421409
)
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
450410

451411
if llm_spans:
452412
for llm_span in llm_spans:

0 commit comments

Comments
 (0)