From 3ac94d87d151eff88ba626b3dea8a3914cab3753 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=B5=81=E5=B1=BF?= Date: Tue, 16 Jun 2026 15:26:16 +0800 Subject: [PATCH] fix(agno): remove pydantic runtime dependency --- .../CHANGELOG.md | 4 ++ .../pyproject.toml | 1 - .../instrumentation/agno/utils.py | 15 ++-- .../tests/test_agno.py | 70 +++++++++++++++++++ 4 files changed, 85 insertions(+), 5 deletions(-) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agno/CHANGELOG.md b/instrumentation-loongsuite/loongsuite-instrumentation-agno/CHANGELOG.md index fe057de8d..e200cfdc7 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agno/CHANGELOG.md +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agno/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## Unreleased +### Removed + +- Remove the direct `pydantic` runtime dependency from Agno instrumentation. + ## Version 0.6.0 (2026-06-03) ### Removed diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agno/pyproject.toml b/instrumentation-loongsuite/loongsuite-instrumentation-agno/pyproject.toml index 76f626cd6..97b2ea8c3 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agno/pyproject.toml +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agno/pyproject.toml @@ -30,7 +30,6 @@ dependencies = [ "opentelemetry-instrumentation >= 0.58b0", "opentelemetry-semantic-conventions >= 0.58b0", "opentelemetry-util-genai", - "pydantic", "wrapt >= 1.17.3, < 2.0.0", ] diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agno/src/opentelemetry/instrumentation/agno/utils.py b/instrumentation-loongsuite/loongsuite-instrumentation-agno/src/opentelemetry/instrumentation/agno/utils.py index a7779316a..0c50efdb7 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agno/src/opentelemetry/instrumentation/agno/utils.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agno/src/opentelemetry/instrumentation/agno/utils.py @@ -18,8 +18,6 @@ from dataclasses import asdict, is_dataclass from typing import Any, Mapping, Sequence -from pydantic import BaseModel - from opentelemetry.util.genai.extended_semconv.gen_ai_extended_attributes import ( GEN_AI_SESSION_ID, GEN_AI_USER_ID, @@ -62,8 +60,17 @@ def _to_dict(value: Any) -> dict[str, Any] | None: return None if isinstance(value, Mapping): return dict(value) - if isinstance(value, BaseModel): - return value.model_dump(mode="json") + model_dump = getattr(value, "model_dump", None) + if callable(model_dump): + try: + return model_dump(mode="json") + except TypeError: + try: + return model_dump() + except Exception: + return None + except Exception: + return None if is_dataclass(value): return asdict(value) to_dict = getattr(value, "to_dict", None) diff --git a/instrumentation-loongsuite/loongsuite-instrumentation-agno/tests/test_agno.py b/instrumentation-loongsuite/loongsuite-instrumentation-agno/tests/test_agno.py index 0bbdb4d06..830f5369f 100644 --- a/instrumentation-loongsuite/loongsuite-instrumentation-agno/tests/test_agno.py +++ b/instrumentation-loongsuite/loongsuite-instrumentation-agno/tests/test_agno.py @@ -324,6 +324,43 @@ def run_once(index: int): assert len(model_spans) == 3 +@pytest.mark.parametrize("content_capture_mode", [None, "NO_CONTENT"]) +def test_content_capture_mode_does_not_gate_span_creation( + monkeypatch, + span_exporter: InMemorySpanExporter, + tracer_provider: trace_api.TracerProvider, + content_capture_mode: str | None, +): + AgnoInstrumentor().uninstrument() + if hasattr(get_extended_telemetry_handler, "_default_handler"): + delattr(get_extended_telemetry_handler, "_default_handler") + span_exporter.clear() + + env_var = "OTEL_INSTRUMENTATION_GENAI_CAPTURE_MESSAGE_CONTENT" + if content_capture_mode is None: + monkeypatch.delenv(env_var, raising=False) + else: + monkeypatch.setenv(env_var, content_capture_mode) + + AgnoInstrumentor().instrument(tracer_provider=tracer_provider) + + agent = Agent(name="NoContentAgent", model=EchoModel(), tools=[]) + response = agent.run("Say hello without content") + + assert response.content == "hello" + spans = _spans_by_name(span_exporter) + assert "invoke_agent NoContentAgent" in spans + assert "chat echo-model" in spans + + agent_attrs = spans["invoke_agent NoContentAgent"].attributes + model_attrs = spans["chat echo-model"].attributes + assert agent_attrs["gen_ai.span.kind"] == "AGENT" + assert model_attrs["gen_ai.span.kind"] == "LLM" + assert "gen_ai.input.messages" not in agent_attrs + assert "gen_ai.output.messages" not in agent_attrs + assert "gen_ai.output.messages" not in model_attrs + + def test_async_function_call_emits_tool_span( span_exporter: InMemorySpanExporter, ): @@ -508,6 +545,39 @@ def test_tool_result_messages_do_not_duplicate_text_parts(): assert parts[0].response == {"temperature": 21} +def test_model_dump_objects_are_serialized_without_pydantic_base_class(): + class ModelDumpToolCall: + def model_dump(self, mode="json"): + assert mode == "json" + return { + "id": "call_1", + "function": { + "name": "get_weather", + "arguments": '{"city":"Hangzhou"}', + }, + } + + messages = convert_agent_input( + [ + SimpleNamespace( + role="assistant", + content=None, + tool_calls=[ModelDumpToolCall()], + ) + ] + ) + + parts = messages[0].parts + tool_calls = [ + part for part in parts if getattr(part, "type", None) == "tool_call" + ] + + assert len(tool_calls) == 1 + assert tool_calls[0].id == "call_1" + assert tool_calls[0].name == "get_weather" + assert tool_calls[0].arguments == {"city": "Hangzhou"} + + def test_missing_finish_reason_is_not_reported(): agent = Agent(name="NoFinishReasonAgent", model=EchoModel(), tools=[]) invocation = create_agent_invocation(agent, {"input": "hello"})