Skip to content

Commit 944f8fd

Browse files
committed
fix(agno): fix streaming tool-call arguments and add structured output message attributes
1 parent dfb04bf commit 944f8fd

4 files changed

Lines changed: 255 additions & 43 deletions

File tree

python/instrumentation/openinference-instrumentation-agno/examples/streaming_tool_calls.py

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import json
2-
31
from agno.agent import Agent
42
from agno.models.anthropic import Claude
53
from agno.models.google import Gemini
@@ -21,20 +19,27 @@ def get_weather(city: str) -> str:
2119
"""Return current weather for a city."""
2220
return f"Sunny, 22°C in {city}."
2321

22+
2423
if __name__ == "__main__":
25-
for event in Agent(name="OpenAI Agent", model=OpenAIChat(id="gpt-4o-mini"), tools=[get_weather]).run(
26-
"What is the weather in Paris and New Delhi?", stream=True
27-
):
24+
for event in Agent(
25+
name="OpenAI Agent", model=OpenAIChat(id="gpt-4o-mini"), tools=[get_weather]
26+
).run("What is the weather in Paris and New Delhi?", stream=True):
2827
print(event)
2928

29+
# print(
30+
# Agent(name="OpenAI Agent", model=OpenAIChat(id="gpt-4o-mini"), tools=[get_weather]).run(
31+
# "What is the weather in Paris and New Delhi?", stream=False
32+
# )
33+
# )
34+
3035
print("\n=== Anthropic ===")
31-
for event in Agent(name="Anthropic Agent", model=Claude(id="claude-haiku-4-5-20251001"), tools=[get_weather]).run(
32-
"What is the weather in Paris and New Delhi?", stream=True
33-
):
36+
for event in Agent(
37+
name="Anthropic Agent", model=Claude(id="claude-haiku-4-5-20251001"), tools=[get_weather]
38+
).run("What is the weather in Paris and New Delhi?", stream=True):
3439
print(event)
3540

3641
print("\n=== Gemini ===")
37-
for event in Agent(name="Gemini Agent", model=Gemini(id="gemini-2.0-flash"), tools=[get_weather]).run(
38-
"What is the weather in Paris and New Delhi?", stream=True
39-
):
42+
for event in Agent(
43+
name="Gemini Agent", model=Gemini(id="gemini-2.0-flash"), tools=[get_weather]
44+
).run("What is the weather in Paris and New Delhi?", stream=True):
4045
print(event)

python/instrumentation/openinference-instrumentation-agno/pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,7 @@ testpaths = [
7878
[tool.mypy]
7979
strict = true
8080
explicit_package_bases = true
81+
warn_unused_ignores = false
8182
exclude = [
8283
"examples",
8384
"dist",
@@ -89,6 +90,7 @@ ignore_missing_imports = true
8990
module = [
9091
"agno",
9192
"wrapt",
93+
"wrapt.*",
9294
]
9395

9496
[tool.ruff]

python/instrumentation/openinference-instrumentation-agno/src/openinference/instrumentation/agno/_model_wrapper.py

Lines changed: 41 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,9 @@ def _bind_arguments(method: Callable[..., Any], *args: Any, **kwargs: Any) -> Di
7272
def _llm_input_messages(arguments: Mapping[str, Any]) -> Iterator[Tuple[str, Any]]:
7373
def process_message(idx: int, message: Any) -> Iterator[Tuple[str, Any]]:
7474
yield f"{LLM_INPUT_MESSAGES}.{idx}.{MESSAGE_ROLE}", message.role
75+
# tool_call_id links a role=tool result message back to the original tool call request
76+
if message.tool_call_id:
77+
yield f"{LLM_INPUT_MESSAGES}.{idx}.{MESSAGE_TOOL_CALL_ID}", message.tool_call_id
7578
if message.content:
7679
yield f"{LLM_INPUT_MESSAGES}.{idx}.{MESSAGE_CONTENT}", message.get_content_string()
7780
if message.tool_calls:
@@ -85,9 +88,10 @@ def process_message(idx: int, message: Any) -> Iterator[Tuple[str, Any]]:
8588
f"{LLM_INPUT_MESSAGES}.{idx}.{MESSAGE_TOOL_CALLS}.{tool_call_index}.{TOOL_CALL_FUNCTION_NAME}",
8689
_get_attr(function_obj, "name"),
8790
)
91+
arguments = _get_attr(function_obj, "arguments")
8892
yield (
8993
f"{LLM_INPUT_MESSAGES}.{idx}.{MESSAGE_TOOL_CALLS}.{tool_call_index}.{TOOL_CALL_FUNCTION_ARGUMENTS_JSON}",
90-
safe_json_dumps(_get_attr(function_obj, "arguments", {})),
94+
json.dumps(arguments) if isinstance(arguments, dict) else (arguments or "{}"),
9195
)
9296

9397
messages = arguments.get("messages", [])
@@ -199,9 +203,12 @@ def _output_value_and_mime_type(output: str) -> Iterator[Tuple[str, Any]]:
199203
f"{LLM_OUTPUT_MESSAGES}.{i}.{MESSAGE_TOOL_CALLS}.{tool_call_index}.{TOOL_CALL_FUNCTION_NAME}",
200204
_get_attr(function_obj, "name"),
201205
)
206+
arguments = _get_attr(function_obj, "arguments")
202207
yield (
203208
f"{LLM_OUTPUT_MESSAGES}.{i}.{MESSAGE_TOOL_CALLS}.{tool_call_index}.{TOOL_CALL_FUNCTION_ARGUMENTS_JSON}",
204-
safe_json_dumps(_get_attr(function_obj, "arguments", {})),
209+
json.dumps(arguments)
210+
if isinstance(arguments, dict)
211+
else (arguments or "{}"),
205212
)
206213

207214
yield OUTPUT_VALUE, safe_json_dumps(messages)
@@ -246,8 +253,8 @@ def _parse_model_output_stream(output: Any) -> Dict[str, Any]:
246253
non_indexed_tool_calls: list[Dict[str, Any]] = []
247254

248255
for chunk in output:
249-
if chunk.content:
250-
accumulated_content += chunk.content
256+
if chunk.content is not None and chunk.content != "":
257+
accumulated_content += str(chunk.content)
251258

252259
if chunk.tool_calls:
253260
for tool_call in chunk.tool_calls:
@@ -306,6 +313,33 @@ def _parse_model_output_stream(output: Any) -> Dict[str, Any]:
306313
return {"messages": messages}
307314

308315

316+
def _stream_output_messages(output: Dict[str, Any]) -> Iterator[Tuple[str, Any]]:
317+
for i, message in enumerate(output.get("messages", [])):
318+
if role := _get_attr(message, "role"):
319+
yield f"{LLM_OUTPUT_MESSAGES}.{i}.{MESSAGE_ROLE}", role
320+
if content := _get_attr(message, "content"):
321+
yield f"{LLM_OUTPUT_MESSAGES}.{i}.{MESSAGE_CONTENT}", content
322+
if tool_calls := _get_attr(message, "tool_calls"):
323+
for j, tool_call in enumerate(tool_calls):
324+
if tc_id := _get_attr(tool_call, "id"):
325+
yield (
326+
f"{LLM_OUTPUT_MESSAGES}.{i}.{MESSAGE_TOOL_CALLS}.{j}.{TOOL_CALL_ID}",
327+
tc_id,
328+
)
329+
function_obj = _get_attr(tool_call, "function", {})
330+
if fn_name := _get_attr(function_obj, "name"):
331+
yield (
332+
f"{LLM_OUTPUT_MESSAGES}.{i}.{MESSAGE_TOOL_CALLS}.{j}.{TOOL_CALL_FUNCTION_NAME}",
333+
fn_name,
334+
)
335+
fn_args = _get_attr(function_obj, "arguments")
336+
if fn_args is not None:
337+
yield (
338+
f"{LLM_OUTPUT_MESSAGES}.{i}.{MESSAGE_TOOL_CALLS}.{j}.{TOOL_CALL_FUNCTION_ARGUMENTS_JSON}",
339+
fn_args,
340+
)
341+
342+
309343
class _ModelWrapper:
310344
def __init__(self, tracer: trace_api.Tracer) -> None:
311345
self._tracer = tracer
@@ -407,6 +441,7 @@ def run_stream(
407441
output_message = json.dumps(output_message_dict)
408442
span.set_attribute(OUTPUT_MIME_TYPE, JSON)
409443
span.set_attribute(OUTPUT_VALUE, output_message)
444+
span.set_attributes(dict(_stream_output_messages(output_message_dict)))
410445

411446
# Find the final response with complete metrics (last one with response_usage)
412447
final_response_with_metrics = None
@@ -540,6 +575,7 @@ async def arun_stream(
540575
output_message = json.dumps(output_message_dict)
541576
span.set_attribute(OUTPUT_MIME_TYPE, JSON)
542577
span.set_attribute(OUTPUT_VALUE, output_message)
578+
span.set_attributes(dict(_stream_output_messages(output_message_dict)))
543579

544580
# Find the final response with complete metrics (last one with response_usage)
545581
final_response_with_metrics = None
@@ -597,6 +633,7 @@ async def arun_stream(
597633
MESSAGE_FUNCTION_CALL_NAME = MessageAttributes.MESSAGE_FUNCTION_CALL_NAME
598634
MESSAGE_NAME = MessageAttributes.MESSAGE_NAME
599635
MESSAGE_ROLE = MessageAttributes.MESSAGE_ROLE
636+
MESSAGE_TOOL_CALL_ID = MessageAttributes.MESSAGE_TOOL_CALL_ID
600637
MESSAGE_TOOL_CALLS = MessageAttributes.MESSAGE_TOOL_CALLS
601638

602639
# mime types

0 commit comments

Comments
 (0)