Skip to content

Commit 086d053

Browse files
committed
fix: backfill available tool using tool definitions if available
1 parent 6971865 commit 086d053

2 files changed

Lines changed: 343 additions & 371 deletions

File tree

src/strands_evals/mappers/generic_gen_ai_session_mapper.py

Lines changed: 55 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -337,20 +337,7 @@ def _convert_agent_invocation_span(self, span: dict, span_info: SpanInfo) -> Age
337337

338338
user_prompt = ""
339339
agent_response = ""
340-
available_tools: list[ToolConfig] = []
341-
342-
# Parse available tools from gen_ai.agent.tools or gen_ai.tool.definitions
343-
try:
344-
tools_json = attrs.get("gen_ai.agent.tools", "") or attrs.get("gen_ai.tool.definitions", "[]")
345-
tool_list = json.loads(str(tools_json)) if isinstance(tools_json, str) else tools_json
346-
if isinstance(tool_list, list):
347-
for item in tool_list:
348-
if isinstance(item, str):
349-
available_tools.append(ToolConfig(name=item))
350-
elif isinstance(item, dict):
351-
available_tools.append(ToolConfig(name=item.get("name", "")))
352-
except (json.JSONDecodeError, TypeError):
353-
pass
340+
available_tools = self._parse_tool_definitions(attrs)
354341

355342
# Extract from span_events (manual instrumentation format)
356343
for event in span.get("span_events", []):
@@ -444,6 +431,31 @@ def _convert_agent_invocation_span(self, span: dict, span_info: SpanInfo) -> Age
444431
# Content Processing Helpers
445432
# =========================================================================
446433

434+
@staticmethod
435+
def _parse_tool_definitions(attributes: dict) -> list[ToolConfig]:
436+
"""Parse available tools from a span's gen_ai.agent.tools or gen_ai.tool.definitions."""
437+
raw = attributes.get("gen_ai.agent.tools", "") or attributes.get("gen_ai.tool.definitions", "[]")
438+
try:
439+
tool_list = json.loads(raw) if isinstance(raw, str) else raw
440+
except (json.JSONDecodeError, TypeError):
441+
return []
442+
if not isinstance(tool_list, list):
443+
return []
444+
445+
configs: list[ToolConfig] = []
446+
for item in tool_list:
447+
if isinstance(item, str):
448+
if item:
449+
configs.append(ToolConfig(name=item))
450+
elif isinstance(item, dict):
451+
fn = item["function"] if isinstance(item.get("function"), dict) else item
452+
name = fn.get("name", "")
453+
if name:
454+
configs.append(
455+
ToolConfig(name=name, description=fn.get("description"), parameters=fn.get("parameters"))
456+
)
457+
return configs
458+
447459
def _convert_message_list_from_event(self, event_attrs: dict) -> list[UserMessage | AssistantMessage]:
448460
"""Parse operation.details event attrs and return typed messages via _convert_message_list."""
449461
messages: list[UserMessage | AssistantMessage] = []
@@ -636,23 +648,16 @@ def _enrich_openai_agent_spans(self, trace: Trace, raw_spans: list[dict]) -> Non
636648
637649
This enriches by:
638650
1. Re-parenting sub-agents using ``agent_handoff`` spans.
639-
2. Deriving each agent's ``available_tools`` from its owned tool spans.
651+
2. Backfilling ``user_prompt`` / ``agent_response`` from child chat spans.
652+
3. Deriving each agent's ``available_tools`` from child spans
640653
"""
641654
agent_spans = {
642655
s.span_info.span_id: s for s in trace.spans if isinstance(s, AgentInvocationSpan) and s.span_info.span_id
643656
}
644657

645658
self._reparent_by_handoff(agent_spans, raw_spans)
646-
647659
self._backfill_agent_prompts(trace, agent_spans)
648-
649-
for span in trace.spans:
650-
if isinstance(span, ToolExecutionSpan) and span.tool_call.name:
651-
owner_id = span.agent_span_id
652-
if owner_id and owner_id in agent_spans:
653-
agent = agent_spans[owner_id]
654-
if not any(t.name == span.tool_call.name for t in agent.available_tools):
655-
agent.available_tools.append(ToolConfig(name=span.tool_call.name))
660+
self._backfill_agent_tools(trace, agent_spans, raw_spans)
656661

657662
def _reparent_by_handoff(
658663
self,
@@ -689,6 +694,32 @@ def _reparent_by_handoff(
689694
if from_span and to_span and from_span.span_info.span_id:
690695
to_span.span_info.parent_span_id = from_span.span_info.span_id
691696

697+
def _backfill_agent_tools(
698+
self,
699+
trace: Trace,
700+
agent_spans: dict[str, AgentInvocationSpan],
701+
raw_spans: list[dict],
702+
) -> None:
703+
"""Populate available_tools on agent spans using their child spans"""
704+
# Fill agent spans' available tools from child chat spans' tool definitions.
705+
raw_attrs_by_id = {raw.get("span_id"): raw.get("attributes", {}) for raw in raw_spans}
706+
for span in trace.spans:
707+
if not isinstance(span, InferenceSpan):
708+
continue
709+
agent = agent_spans.get(span.span_info.parent_span_id or "")
710+
if agent is None or agent.available_tools:
711+
continue
712+
configs = self._parse_tool_definitions(raw_attrs_by_id.get(span.span_info.span_id, {}))
713+
if configs:
714+
agent.available_tools = list(configs)
715+
716+
# Fallback: get the names of tools the agents called.
717+
for span in trace.spans:
718+
if isinstance(span, ToolExecutionSpan) and span.tool_call.name:
719+
agent = agent_spans.get(span.agent_span_id or "")
720+
if agent and not any(t.name == span.tool_call.name for t in agent.available_tools):
721+
agent.available_tools.append(ToolConfig(name=span.tool_call.name))
722+
692723
def _backfill_agent_prompts(
693724
self,
694725
trace: Trace,

0 commit comments

Comments
 (0)