Skip to content

Commit 3ac94d8

Browse files
committed
fix(agno): remove pydantic runtime dependency
1 parent de65b32 commit 3ac94d8

4 files changed

Lines changed: 85 additions & 5 deletions

File tree

instrumentation-loongsuite/loongsuite-instrumentation-agno/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
77

88
## Unreleased
99

10+
### Removed
11+
12+
- Remove the direct `pydantic` runtime dependency from Agno instrumentation.
13+
1014
## Version 0.6.0 (2026-06-03)
1115

1216
### Removed

instrumentation-loongsuite/loongsuite-instrumentation-agno/pyproject.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,6 @@ dependencies = [
3030
"opentelemetry-instrumentation >= 0.58b0",
3131
"opentelemetry-semantic-conventions >= 0.58b0",
3232
"opentelemetry-util-genai",
33-
"pydantic",
3433
"wrapt >= 1.17.3, < 2.0.0",
3534
]
3635

instrumentation-loongsuite/loongsuite-instrumentation-agno/src/opentelemetry/instrumentation/agno/utils.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,6 @@
1818
from dataclasses import asdict, is_dataclass
1919
from typing import Any, Mapping, Sequence
2020

21-
from pydantic import BaseModel
22-
2321
from opentelemetry.util.genai.extended_semconv.gen_ai_extended_attributes import (
2422
GEN_AI_SESSION_ID,
2523
GEN_AI_USER_ID,
@@ -62,8 +60,17 @@ def _to_dict(value: Any) -> dict[str, Any] | None:
6260
return None
6361
if isinstance(value, Mapping):
6462
return dict(value)
65-
if isinstance(value, BaseModel):
66-
return value.model_dump(mode="json")
63+
model_dump = getattr(value, "model_dump", None)
64+
if callable(model_dump):
65+
try:
66+
return model_dump(mode="json")
67+
except TypeError:
68+
try:
69+
return model_dump()
70+
except Exception:
71+
return None
72+
except Exception:
73+
return None
6774
if is_dataclass(value):
6875
return asdict(value)
6976
to_dict = getattr(value, "to_dict", None)

instrumentation-loongsuite/loongsuite-instrumentation-agno/tests/test_agno.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -324,6 +324,43 @@ def run_once(index: int):
324324
assert len(model_spans) == 3
325325

326326

327+
@pytest.mark.parametrize("content_capture_mode", [None, "NO_CONTENT"])
328+
def test_content_capture_mode_does_not_gate_span_creation(
329+
monkeypatch,
330+
span_exporter: InMemorySpanExporter,
331+
tracer_provider: trace_api.TracerProvider,
332+
content_capture_mode: str | None,
333+
):
334+
AgnoInstrumentor().uninstrument()
335+
if hasattr(get_extended_telemetry_handler, "_default_handler"):
336+
delattr(get_extended_telemetry_handler, "_default_handler")
337+
span_exporter.clear()
338+
339+
env_var = "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT"
340+
if content_capture_mode is None:
341+
monkeypatch.delenv(env_var, raising=False)
342+
else:
343+
monkeypatch.setenv(env_var, content_capture_mode)
344+
345+
AgnoInstrumentor().instrument(tracer_provider=tracer_provider)
346+
347+
agent = Agent(name="NoContentAgent", model=EchoModel(), tools=[])
348+
response = agent.run("Say hello without content")
349+
350+
assert response.content == "hello"
351+
spans = _spans_by_name(span_exporter)
352+
assert "invoke_agent NoContentAgent" in spans
353+
assert "chat echo-model" in spans
354+
355+
agent_attrs = spans["invoke_agent NoContentAgent"].attributes
356+
model_attrs = spans["chat echo-model"].attributes
357+
assert agent_attrs["gen_ai.span.kind"] == "AGENT"
358+
assert model_attrs["gen_ai.span.kind"] == "LLM"
359+
assert "gen_ai.input.messages" not in agent_attrs
360+
assert "gen_ai.output.messages" not in agent_attrs
361+
assert "gen_ai.output.messages" not in model_attrs
362+
363+
327364
def test_async_function_call_emits_tool_span(
328365
span_exporter: InMemorySpanExporter,
329366
):
@@ -508,6 +545,39 @@ def test_tool_result_messages_do_not_duplicate_text_parts():
508545
assert parts[0].response == {"temperature": 21}
509546

510547

548+
def test_model_dump_objects_are_serialized_without_pydantic_base_class():
549+
class ModelDumpToolCall:
550+
def model_dump(self, mode="json"):
551+
assert mode == "json"
552+
return {
553+
"id": "call_1",
554+
"function": {
555+
"name": "get_weather",
556+
"arguments": '{"city":"Hangzhou"}',
557+
},
558+
}
559+
560+
messages = convert_agent_input(
561+
[
562+
SimpleNamespace(
563+
role="assistant",
564+
content=None,
565+
tool_calls=[ModelDumpToolCall()],
566+
)
567+
]
568+
)
569+
570+
parts = messages[0].parts
571+
tool_calls = [
572+
part for part in parts if getattr(part, "type", None) == "tool_call"
573+
]
574+
575+
assert len(tool_calls) == 1
576+
assert tool_calls[0].id == "call_1"
577+
assert tool_calls[0].name == "get_weather"
578+
assert tool_calls[0].arguments == {"city": "Hangzhou"}
579+
580+
511581
def test_missing_finish_reason_is_not_reported():
512582
agent = Agent(name="NoFinishReasonAgent", model=EchoModel(), tools=[])
513583
invocation = create_agent_invocation(agent, {"input": "hello"})

0 commit comments

Comments
 (0)