diff --git a/.riot/requirements/1205fcd.txt b/.riot/requirements/1205fcd.txt new file mode 100644 index 00000000000..83d0395a827 --- /dev/null +++ b/.riot/requirements/1205fcd.txt @@ -0,0 +1,105 @@ +# +# This file is autogenerated by pip-compile with Python 3.11 +# by the following command: +# +# pip-compile --allow-unsafe --no-annotate .riot/requirements/1205fcd.in +# +ag-ui-protocol==0.1.8 +aiohappyeyeballs==2.6.1 +aiohttp==3.12.15 +aiosignal==1.4.0 +annotated-types==0.7.0 +anthropic==0.60.0 +anyio==4.9.0 +argcomplete==3.6.2 +attrs==25.3.0 +boto3==1.39.16 +botocore==1.39.16 +cachetools==5.5.2 +certifi==2025.7.14 +charset-normalizer==3.4.2 +click==8.2.1 +cohere==5.16.1 +colorama==0.4.6 +coverage[toml]==7.10.1 +distro==1.9.0 +eval-type-backport==0.2.2 +fastavro==1.11.1 +filelock==3.18.0 +frozenlist==1.7.0 +fsspec==2025.7.0 +google-auth==2.40.3 +google-genai==1.27.0 +griffe==1.9.0 +groq==0.30.0 +h11==0.16.0 +hf-xet==1.1.5 +httpcore==1.0.9 +httpx==0.28.1 +httpx-sse==0.4.0 +huggingface-hub[inference]==0.34.3 +hypothesis==6.45.0 +idna==3.10 +importlib-metadata==8.7.0 +iniconfig==2.1.0 +jiter==0.10.0 +jmespath==1.0.1 +jsonschema==4.25.0 +jsonschema-specifications==2025.4.1 +logfire-api==4.0.0 +markdown-it-py==3.0.0 +mcp==1.12.2 +mdurl==0.1.2 +mistralai==1.9.3 +mock==5.2.0 +multidict==6.6.3 +openai==1.98.0 +opentelemetry-api==1.36.0 +opentracing==2.4.0 +packaging==25.0 +pluggy==1.6.0 +prompt-toolkit==3.0.51 +propcache==0.3.2 +pyasn1==0.6.1 +pyasn1-modules==0.4.2 +pydantic==2.11.7 +pydantic-ai==0.4.4 +pydantic-ai-slim[ag-ui,anthropic,bedrock,cli,cohere,evals,google,groq,huggingface,mcp,mistral,openai,vertexai]==0.4.4 +pydantic-core==2.33.2 +pydantic-evals==0.4.4 +pydantic-graph==0.4.4 +pydantic-settings==2.10.1 +pygments==2.19.2 +pytest==8.4.1 +pytest-asyncio==1.1.0 +pytest-cov==6.2.1 +pytest-mock==3.14.1 +python-dateutil==2.9.0.post0 +python-dotenv==1.1.1 +python-multipart==0.0.20 +pyyaml==6.0.2 +referencing==0.36.2 +requests==2.32.4 +rich==14.1.0 +rpds-py==0.26.0 +rsa==4.9.1 +s3transfer==0.13.1 +six==1.17.0 +sniffio==1.3.1 +sortedcontainers==2.4.0 +sse-starlette==3.0.2 +starlette==0.47.2 +tenacity==8.5.0 +tokenizers==0.21.4 +tqdm==4.67.1 +types-requests==2.32.4.20250611 +typing-extensions==4.14.1 +typing-inspection==0.4.1 +urllib3==2.5.0 +uvicorn==0.35.0 +vcrpy==7.0.0 +wcwidth==0.2.13 +websockets==15.0.1 +wrapt==1.17.2 +yarl==1.20.1 +zipp==3.23.0 diff --git a/.riot/requirements/17ed285.txt b/.riot/requirements/17ed285.txt new file mode 100644 index 00000000000..a962c676dfd --- /dev/null +++ b/.riot/requirements/17ed285.txt @@ -0,0 +1,105 @@ +# +# This file is autogenerated by pip-compile with Python 3.12 +# by the following command: +# +# pip-compile --allow-unsafe --no-annotate .riot/requirements/17ed285.in +# +ag-ui-protocol==0.1.8 +aiohappyeyeballs==2.6.1 +aiohttp==3.12.15 +aiosignal==1.4.0 +annotated-types==0.7.0 +anthropic==0.60.0 +anyio==4.9.0 +argcomplete==3.6.2 +attrs==25.3.0 +boto3==1.39.16 +botocore==1.39.16 +cachetools==5.5.2 +certifi==2025.7.14 +charset-normalizer==3.4.2 +click==8.2.1 +cohere==5.16.1 +colorama==0.4.6 +coverage[toml]==7.10.1 +distro==1.9.0 +eval-type-backport==0.2.2 +fastavro==1.11.1 +filelock==3.18.0 +frozenlist==1.7.0 +fsspec==2025.7.0 +google-auth==2.40.3 +google-genai==1.27.0 +griffe==1.9.0 +groq==0.30.0 +h11==0.16.0 +hf-xet==1.1.5 +httpcore==1.0.9 +httpx==0.28.1 +httpx-sse==0.4.0 +huggingface-hub[inference]==0.34.3 +hypothesis==6.45.0 +idna==3.10 +importlib-metadata==8.7.0 +iniconfig==2.1.0 +jiter==0.10.0 +jmespath==1.0.1 +jsonschema==4.25.0 +jsonschema-specifications==2025.4.1 +logfire-api==4.0.0 +markdown-it-py==3.0.0 +mcp==1.12.2 +mdurl==0.1.2 +mistralai==1.9.3 +mock==5.2.0 +multidict==6.6.3 +openai==1.98.0 +opentelemetry-api==1.36.0 +opentracing==2.4.0 +packaging==25.0 +pluggy==1.6.0 +prompt-toolkit==3.0.51 +propcache==0.3.2 +pyasn1==0.6.1 +pyasn1-modules==0.4.2 +pydantic==2.11.7 +pydantic-ai==0.4.4 +pydantic-ai-slim[ag-ui,anthropic,bedrock,cli,cohere,evals,google,groq,huggingface,mcp,mistral,openai,vertexai]==0.4.4 +pydantic-core==2.33.2 +pydantic-evals==0.4.4 +pydantic-graph==0.4.4 +pydantic-settings==2.10.1 +pygments==2.19.2 +pytest==8.4.1 +pytest-asyncio==1.1.0 +pytest-cov==6.2.1 +pytest-mock==3.14.1 +python-dateutil==2.9.0.post0 +python-dotenv==1.1.1 +python-multipart==0.0.20 +pyyaml==6.0.2 +referencing==0.36.2 +requests==2.32.4 +rich==14.1.0 +rpds-py==0.26.0 +rsa==4.9.1 +s3transfer==0.13.1 +six==1.17.0 +sniffio==1.3.1 +sortedcontainers==2.4.0 +sse-starlette==3.0.2 +starlette==0.47.2 +tenacity==8.5.0 +tokenizers==0.21.4 +tqdm==4.67.1 +types-requests==2.32.4.20250611 +typing-extensions==4.14.1 +typing-inspection==0.4.1 +urllib3==2.5.0 +uvicorn==0.35.0 +vcrpy==7.0.0 +wcwidth==0.2.13 +websockets==15.0.1 +wrapt==1.17.2 +yarl==1.20.1 +zipp==3.23.0 diff --git a/.riot/requirements/1894006.txt b/.riot/requirements/1894006.txt new file mode 100644 index 00000000000..129e7c60038 --- /dev/null +++ b/.riot/requirements/1894006.txt @@ -0,0 +1,109 @@ +# +# This file is autogenerated by pip-compile with Python 3.10 +# by the following command: +# +# pip-compile --allow-unsafe --no-annotate .riot/requirements/1894006.in +# +ag-ui-protocol==0.1.8 +aiohappyeyeballs==2.6.1 +aiohttp==3.12.15 +aiosignal==1.4.0 +annotated-types==0.7.0 +anthropic==0.60.0 +anyio==4.9.0 +argcomplete==3.6.2 +async-timeout==5.0.1 +attrs==25.3.0 +backports-asyncio-runner==1.2.0 +boto3==1.39.16 +botocore==1.39.16 +cachetools==5.5.2 +certifi==2025.7.14 +charset-normalizer==3.4.2 +click==8.2.1 +cohere==5.16.1 +colorama==0.4.6 +coverage[toml]==7.10.1 +distro==1.9.0 +eval-type-backport==0.2.2 +exceptiongroup==1.3.0 +fastavro==1.11.1 +filelock==3.18.0 +frozenlist==1.7.0 +fsspec==2025.7.0 +google-auth==2.40.3 +google-genai==1.27.0 +griffe==1.9.0 +groq==0.30.0 +h11==0.16.0 +hf-xet==1.1.5 +httpcore==1.0.9 +httpx==0.28.1 +httpx-sse==0.4.0 +huggingface-hub[inference]==0.34.3 +hypothesis==6.45.0 +idna==3.10 +importlib-metadata==8.7.0 +iniconfig==2.1.0 +jiter==0.10.0 +jmespath==1.0.1 +jsonschema==4.25.0 +jsonschema-specifications==2025.4.1 +logfire-api==4.0.0 +markdown-it-py==3.0.0 +mcp==1.12.2 +mdurl==0.1.2 +mistralai==1.9.3 +mock==5.2.0 +multidict==6.6.3 +openai==1.98.0 +opentelemetry-api==1.36.0 +opentracing==2.4.0 +packaging==25.0 +pluggy==1.6.0 +prompt-toolkit==3.0.51 +propcache==0.3.2 +pyasn1==0.6.1 +pyasn1-modules==0.4.2 +pydantic==2.11.7 +pydantic-ai==0.4.4 +pydantic-ai-slim[ag-ui,anthropic,bedrock,cli,cohere,evals,google,groq,huggingface,mcp,mistral,openai,vertexai]==0.4.4 +pydantic-core==2.33.2 +pydantic-evals==0.4.4 +pydantic-graph==0.4.4 +pydantic-settings==2.10.1 +pygments==2.19.2 +pytest==8.4.1 +pytest-asyncio==1.1.0 +pytest-cov==6.2.1 +pytest-mock==3.14.1 +python-dateutil==2.9.0.post0 +python-dotenv==1.1.1 +python-multipart==0.0.20 +pyyaml==6.0.2 +referencing==0.36.2 +requests==2.32.4 +rich==14.1.0 +rpds-py==0.26.0 +rsa==4.9.1 +s3transfer==0.13.1 +six==1.17.0 +sniffio==1.3.1 +sortedcontainers==2.4.0 +sse-starlette==3.0.2 +starlette==0.47.2 +tenacity==8.5.0 +tokenizers==0.21.4 +tomli==2.2.1 +tqdm==4.67.1 +types-requests==2.32.4.20250611 +typing-extensions==4.14.1 +typing-inspection==0.4.1 +urllib3==2.5.0 +uvicorn==0.35.0 +vcrpy==7.0.0 +wcwidth==0.2.13 +websockets==15.0.1 +wrapt==1.17.2 +yarl==1.20.1 +zipp==3.23.0 diff --git a/.riot/requirements/192fc05.txt b/.riot/requirements/192fc05.txt new file mode 100644 index 00000000000..9c9827cbefe --- /dev/null +++ b/.riot/requirements/192fc05.txt @@ -0,0 +1,105 @@ +# +# This file is autogenerated by pip-compile with Python 3.13 +# by the following command: +# +# pip-compile --allow-unsafe --no-annotate .riot/requirements/192fc05.in +# +ag-ui-protocol==0.1.8 +aiohappyeyeballs==2.6.1 +aiohttp==3.12.15 +aiosignal==1.4.0 +annotated-types==0.7.0 +anthropic==0.60.0 +anyio==4.9.0 +argcomplete==3.6.2 +attrs==25.3.0 +boto3==1.39.16 +botocore==1.39.16 +cachetools==5.5.2 +certifi==2025.7.14 +charset-normalizer==3.4.2 +click==8.2.1 +cohere==5.16.1 +colorama==0.4.6 +coverage[toml]==7.10.1 +distro==1.9.0 +eval-type-backport==0.2.2 +fastavro==1.11.1 +filelock==3.18.0 +frozenlist==1.7.0 +fsspec==2025.7.0 +google-auth==2.40.3 +google-genai==1.27.0 +griffe==1.9.0 +groq==0.30.0 +h11==0.16.0 +hf-xet==1.1.5 +httpcore==1.0.9 +httpx==0.28.1 +httpx-sse==0.4.0 +huggingface-hub[inference]==0.34.3 +hypothesis==6.45.0 +idna==3.10 +importlib-metadata==8.7.0 +iniconfig==2.1.0 +jiter==0.10.0 +jmespath==1.0.1 +jsonschema==4.25.0 +jsonschema-specifications==2025.4.1 +logfire-api==4.0.0 +markdown-it-py==3.0.0 +mcp==1.12.2 +mdurl==0.1.2 +mistralai==1.9.3 +mock==5.2.0 +multidict==6.6.3 +openai==1.98.0 +opentelemetry-api==1.36.0 +opentracing==2.4.0 +packaging==25.0 +pluggy==1.6.0 +prompt-toolkit==3.0.51 +propcache==0.3.2 +pyasn1==0.6.1 +pyasn1-modules==0.4.2 +pydantic==2.11.7 +pydantic-ai==0.4.4 +pydantic-ai-slim[ag-ui,anthropic,bedrock,cli,cohere,evals,google,groq,huggingface,mcp,mistral,openai,vertexai]==0.4.4 +pydantic-core==2.33.2 +pydantic-evals==0.4.4 +pydantic-graph==0.4.4 +pydantic-settings==2.10.1 +pygments==2.19.2 +pytest==8.4.1 +pytest-asyncio==1.1.0 +pytest-cov==6.2.1 +pytest-mock==3.14.1 +python-dateutil==2.9.0.post0 +python-dotenv==1.1.1 +python-multipart==0.0.20 +pyyaml==6.0.2 +referencing==0.36.2 +requests==2.32.4 +rich==14.1.0 +rpds-py==0.26.0 +rsa==4.9.1 +s3transfer==0.13.1 +six==1.17.0 +sniffio==1.3.1 +sortedcontainers==2.4.0 +sse-starlette==3.0.2 +starlette==0.47.2 +tenacity==8.5.0 +tokenizers==0.21.4 +tqdm==4.67.1 +types-requests==2.32.4.20250611 +typing-extensions==4.14.1 +typing-inspection==0.4.1 +urllib3==2.5.0 +uvicorn==0.35.0 +vcrpy==7.0.0 +wcwidth==0.2.13 +websockets==15.0.1 +wrapt==1.17.2 +yarl==1.20.1 +zipp==3.23.0 diff --git a/.riot/requirements/aed1753.txt b/.riot/requirements/aed1753.txt new file mode 100644 index 00000000000..8ff65efe2e0 --- /dev/null +++ b/.riot/requirements/aed1753.txt @@ -0,0 +1,99 @@ +# +# This file is autogenerated by pip-compile with Python 3.9 +# by the following command: +# +# pip-compile --allow-unsafe --no-annotate .riot/requirements/aed1753.in +# +ag-ui-protocol==0.1.8 +aiohappyeyeballs==2.6.1 +aiohttp==3.12.15 +aiosignal==1.4.0 +annotated-types==0.7.0 +anthropic==0.60.0 +anyio==4.9.0 +argcomplete==3.6.2 +async-timeout==5.0.1 +attrs==25.3.0 +backports-asyncio-runner==1.2.0 +boto3==1.39.16 +botocore==1.39.16 +cachetools==5.5.2 +certifi==2025.7.14 +charset-normalizer==3.4.2 +cohere==5.16.1 +colorama==0.4.6 +coverage[toml]==7.10.1 +distro==1.9.0 +eval-type-backport==0.2.2 +exceptiongroup==1.3.0 +fastavro==1.11.1 +filelock==3.18.0 +frozenlist==1.7.0 +fsspec==2025.7.0 +google-auth==2.40.3 +google-genai==1.27.0 +griffe==1.9.0 +groq==0.30.0 +h11==0.16.0 +hf-xet==1.1.5 +httpcore==1.0.9 +httpx==0.28.1 +httpx-sse==0.4.0 +huggingface-hub[inference]==0.34.3 +hypothesis==6.45.0 +idna==3.10 +importlib-metadata==8.7.0 +iniconfig==2.1.0 +jiter==0.10.0 +jmespath==1.0.1 +logfire-api==4.0.0 +markdown-it-py==3.0.0 +mdurl==0.1.2 +mistralai==1.9.3 +mock==5.2.0 +multidict==6.6.3 +openai==1.98.0 +opentelemetry-api==1.36.0 +opentracing==2.4.0 +packaging==25.0 +pluggy==1.6.0 +prompt-toolkit==3.0.51 +propcache==0.3.2 +pyasn1==0.6.1 +pyasn1-modules==0.4.2 +pydantic==2.11.7 +pydantic-ai==0.4.4 +pydantic-ai-slim[ag-ui,anthropic,bedrock,cli,cohere,evals,google,groq,huggingface,mcp,mistral,openai,vertexai]==0.4.4 +pydantic-core==2.33.2 +pydantic-evals==0.4.4 +pydantic-graph==0.4.4 +pygments==2.19.2 +pytest==8.4.1 +pytest-asyncio==1.1.0 +pytest-cov==6.2.1 +pytest-mock==3.14.1 +python-dateutil==2.9.0.post0 +pyyaml==6.0.2 +requests==2.32.4 +rich==14.1.0 +rsa==4.9.1 +s3transfer==0.13.1 +six==1.17.0 +sniffio==1.3.1 +sortedcontainers==2.4.0 +starlette==0.47.2 +tenacity==8.5.0 +tokenizers==0.21.4 +tomli==2.2.1 +tqdm==4.67.1 +types-requests==2.31.0.6 +types-urllib3==1.26.25.14 +typing-extensions==4.14.1 +typing-inspection==0.4.1 +urllib3==1.26.20 +vcrpy==7.0.0 +wcwidth==0.2.13 +websockets==15.0.1 +wrapt==1.17.2 +yarl==1.20.1 +zipp==3.23.0 diff --git a/ddtrace/contrib/integration_registry/registry.yaml b/ddtrace/contrib/integration_registry/registry.yaml index 06f3c62a5d7..d390d919869 100644 --- a/ddtrace/contrib/integration_registry/registry.yaml +++ b/ddtrace/contrib/integration_registry/registry.yaml @@ -393,16 +393,6 @@ integrations: min: 20.12.1 max: 24.11.1 -- integration_name: gunicorn - is_external_package: true - is_tested: true - dependency_names: - - gunicorn - tested_versions_by_dependency: - gunicorn: - min: 20.0.4 - max: 23.0.0 - - integration_name: google_genai is_external_package: true is_tested: true @@ -443,6 +433,16 @@ integrations: min: 1.34.1 max: 1.68.1 +- integration_name: gunicorn + is_external_package: true + is_tested: true + dependency_names: + - gunicorn + tested_versions_by_dependency: + gunicorn: + min: 20.0.4 + max: 23.0.0 + - integration_name: httplib is_external_package: false is_tested: true @@ -572,7 +572,7 @@ integrations: - mcp tested_versions_by_dependency: mcp: - min: 1.10.0 + min: 1.10.1 max: 1.11.0 - integration_name: molten @@ -667,7 +667,7 @@ integrations: tested_versions_by_dependency: pydantic-ai-slim: min: 0.3.0 - max: 0.3.5 + max: 0.4.4 - integration_name: pylibmc is_external_package: true diff --git a/ddtrace/contrib/internal/pydantic_ai/patch.py b/ddtrace/contrib/internal/pydantic_ai/patch.py index d490088531f..d867caa5188 100644 --- a/ddtrace/contrib/internal/pydantic_ai/patch.py +++ b/ddtrace/contrib/internal/pydantic_ai/patch.py @@ -7,6 +7,8 @@ from ddtrace.contrib.internal.trace_utils import unwrap from ddtrace.contrib.internal.trace_utils import wrap from ddtrace.contrib.trace_utils import with_traced_module +from ddtrace.internal.utils import get_argument_value +from ddtrace.internal.utils.version import parse_version from ddtrace.llmobs._integrations.pydantic_ai import PydanticAIIntegration from ddtrace.trace import Pin @@ -24,6 +26,9 @@ def _supported_versions() -> Dict[str, str]: return {"pydantic_ai": "*"} +PYDANTIC_AI_VERSION = parse_version(get_version()) + + @with_traced_module def traced_agent_run_stream(pydantic_ai, pin, func, instance, args, kwargs): integration = pydantic_ai._datadog_integration @@ -56,12 +61,26 @@ def traced_agent_iter(pydantic_ai, pin, func, instance, args, kwargs): @with_traced_module -async def traced_tool_run(pydantic_ai, pin, func, instance, args, kwargs): +async def traced_tool_manager_call(pydantic_ai, pin, func, instance, args, kwargs): + tool_call = get_argument_value(args, kwargs, 0, "tool_call", True) + tool_name = getattr(tool_call, "tool_name", None) or "Pydantic Tool" + tool_manager_tools = getattr(instance, "tools", {}) or {} + tool_instance = tool_manager_tools.get(tool_name) or None + return await traced_tool_run(pydantic_ai, pin, func, tool_instance, args, kwargs, tool_name) + + +@with_traced_module +async def traced_tool_call(pydantic_ai, pin, func, instance, args, kwargs): + tool_name = getattr(instance, "name", None) or "Pydantic Tool" + return await traced_tool_run(pydantic_ai, pin, func, instance, args, kwargs, tool_name) + + +async def traced_tool_run(pydantic_ai, pin, func, instance, args, kwargs, tool_name): integration = pydantic_ai._datadog_integration resp = None try: span = integration.trace(pin, "Pydantic Tool", submit_to_llmobs=True, kind="tool") - span.name = getattr(instance, "name", None) or "Pydantic Tool" + span.name = tool_name resp = await func(*args, **kwargs) return resp except Exception: @@ -85,8 +104,11 @@ def patch(): pydantic_ai._datadog_integration = PydanticAIIntegration(integration_config=config.pydantic_ai) wrap(pydantic_ai, "agent.Agent.iter", traced_agent_iter(pydantic_ai)) - wrap(pydantic_ai, "tools.Tool.run", traced_tool_run(pydantic_ai)) wrap(pydantic_ai, "agent.Agent.run_stream", traced_agent_run_stream(pydantic_ai)) + if PYDANTIC_AI_VERSION >= (0, 4, 4): + wrap(pydantic_ai, "agent.ToolManager.handle_call", traced_tool_manager_call(pydantic_ai)) + else: + wrap(pydantic_ai, "tools.Tool.run", traced_tool_call(pydantic_ai)) def unpatch(): @@ -98,8 +120,11 @@ def unpatch(): pydantic_ai._datadog_patch = False unwrap(pydantic_ai.agent.Agent, "iter") - unwrap(pydantic_ai.tools.Tool, "run") unwrap(pydantic_ai.agent.Agent, "run_stream") + if PYDANTIC_AI_VERSION >= (0, 4, 4): + unwrap(pydantic_ai.agent.ToolManager, "handle_call") + else: + unwrap(pydantic_ai.tools.Tool, "run") delattr(pydantic_ai, "_datadog_integration") Pin().remove_from(pydantic_ai) diff --git a/ddtrace/llmobs/_integrations/pydantic_ai.py b/ddtrace/llmobs/_integrations/pydantic_ai.py index d783754288d..4f1985d725b 100644 --- a/ddtrace/llmobs/_integrations/pydantic_ai.py +++ b/ddtrace/llmobs/_integrations/pydantic_ai.py @@ -2,6 +2,7 @@ from typing import Dict from typing import List from typing import Optional +from typing import Sequence from typing import Tuple from ddtrace.internal.utils import get_argument_value @@ -126,15 +127,21 @@ def _llmobs_set_tags_tool( if tool_call: tool_name = getattr(tool_call, "tool_name", "") tool_input = getattr(tool_call, "args", {}) + tool_def = getattr(tool_instance, "tool_def", None) + tool_description = ( + getattr(tool_def, "description", "") if tool_def else getattr(tool_instance, "description", "") + ) span._set_ctx_items( { NAME: tool_name, - METADATA: {"description": getattr(tool_instance, "description", "")}, + METADATA: {"description": tool_description}, INPUT_VALUE: tool_input, } ) if not span.error: - span._set_ctx_item(OUTPUT_VALUE, getattr(response, "content", "")) + # depending on the version, the output may be a ToolReturnPart or the raw response + output_content = getattr(response, "content", "") or response + span._set_ctx_item(OUTPUT_VALUE, output_content) def _tag_agent_manifest(self, span: Span, kwargs: Dict[str, Any], agent: Any) -> None: if not agent: @@ -154,29 +161,46 @@ def _tag_agent_manifest(self, span: Span, kwargs: Dict[str, Any], agent: Any) -> manifest["instructions"] = agent._instructions if hasattr(agent, "_system_prompts"): manifest["system_prompts"] = agent._system_prompts - if hasattr(agent, "_function_tools"): - manifest["tools"] = self._get_agent_tools(agent._function_tools) if kwargs.get("deps", None): agent_dependencies = kwargs.get("deps", None) manifest["dependencies"] = getattr(agent_dependencies, "__dict__", agent_dependencies) + manifest["tools"] = self._get_agent_tools(agent) span._set_ctx_item(AGENT_MANIFEST, manifest) - def _get_agent_tools(self, tools: Any) -> List[Dict[str, Any]]: + def _get_agent_tools(self, agent: Any) -> List[Dict[str, Any]]: + """ + Extract tools from the agent and format them to be used in the agent manifest. + + For pydantic-ai < 0.4.4, tools are stored in the agent's _function_tools attribute. + For pydantic-ai >= 0.4.4, tools are stored in the agent's _function_toolset (tools) and + _user_toolsets (user-defined toolsets) attributes. + """ + tools: Dict[str, Any] = {} + if hasattr(agent, "_function_tools"): + tools = getattr(agent, "_function_tools", {}) or {} + elif hasattr(agent, "_user_toolsets") or hasattr(agent, "_function_toolset"): + user_toolsets: Sequence[Any] = getattr(agent, "_user_toolsets", []) or [] + function_toolset = getattr(agent, "_function_toolset", None) + combined_toolsets = list(user_toolsets) + [function_toolset] if function_toolset else user_toolsets + for toolset in combined_toolsets: + tools.update(getattr(toolset, "tools", {}) or {}) + if not tools: return [] + formatted_tools = [] for tool_name, tool_instance in tools.items(): - tool_dict = {} + tool_dict: Dict[str, Any] = {} tool_dict["name"] = tool_name if hasattr(tool_instance, "description"): tool_dict["description"] = tool_instance.description function_schema = getattr(tool_instance, "function_schema", {}) json_schema = getattr(function_schema, "json_schema", {}) required_params = {param: True for param in json_schema.get("required", [])} - parameters = {} + parameters: Dict[str, Dict[str, Any]] = {} for param, schema in json_schema.get("properties", {}).items(): - param_dict = {} + param_dict: Dict[str, Any] = {} if "type" in schema: param_dict["type"] = schema["type"] if param in required_params: diff --git a/releasenotes/notes/fix-pydantic-tool-tracing-16708c11c27f8a92.yaml b/releasenotes/notes/fix-pydantic-tool-tracing-16708c11c27f8a92.yaml new file mode 100644 index 00000000000..9e0df037649 --- /dev/null +++ b/releasenotes/notes/fix-pydantic-tool-tracing-16708c11c27f8a92.yaml @@ -0,0 +1,4 @@ +fixes: + - | + pydantic_ai: This fix resolves an issue where enabling the Pydantic AI for pydantic-ai-slim >= 0.4.4 would + fail. See ``this issue ``_ for more details. \ No newline at end of file diff --git a/riotfile.py b/riotfile.py index c9ab4a7ebde..2005bacda40 100644 --- a/riotfile.py +++ b/riotfile.py @@ -2976,7 +2976,7 @@ def select_pys(min_version: str = MIN_PYTHON_VERSION, max_version: str = MAX_PYT pys=select_pys(min_version="3.9"), pkgs={ "pytest-asyncio": latest, - "pydantic-ai": ["==0.3.0", latest], + "pydantic-ai": ["==0.3.0", "==0.4.4", latest], "vcrpy": "==7.0.0", }, ), diff --git a/supported_versions_output.json b/supported_versions_output.json index 02f5611cfa0..b7fffc0ec3a 100644 --- a/supported_versions_output.json +++ b/supported_versions_output.json @@ -307,7 +307,7 @@ "minimum_tracer_supported": "1.3.1", "max_tracer_supported": "1.5.2", "pinned": "true", - "auto-instrumented": true + "auto-instrumented": false }, { "dependency": "gevent", @@ -432,7 +432,7 @@ { "dependency": "mcp", "integration": "mcp", - "minimum_tracer_supported": "1.10.0", + "minimum_tracer_supported": "1.10.1", "max_tracer_supported": "1.11.0", "auto-instrumented": true }, @@ -503,7 +503,7 @@ "dependency": "pydantic-ai-slim", "integration": "pydantic_ai", "minimum_tracer_supported": "0.3.0", - "max_tracer_supported": "0.3.5", + "max_tracer_supported": "0.4.4", "pinned": "true", "auto-instrumented": true }, diff --git a/supported_versions_table.csv b/supported_versions_table.csv index edf6d76a144..d4c86d80ccb 100644 --- a/supported_versions_table.csv +++ b/supported_versions_table.csv @@ -41,7 +41,7 @@ fastapi,fastapi,0.64.0,0.115.12,True flask,flask,1.1.4,3.1.0,True flask-cache,flask_cache,0.13.1,0.13.1,False flask-caching,flask_cache,1.10.1,2.3.0,False -freezegun,freezegun *,1.3.1,1.5.2,True +freezegun,freezegun *,1.3.1,1.5.2,False gevent,gevent,20.12.1,24.11.1,True google-genai,google_genai,1.21.1,1.21.1,True google-generativeai,google_generativeai,0.7.2,0.8.3,True @@ -59,7 +59,7 @@ logbook,logbook,1.0.0,1.8.1,True loguru,loguru,0.4.1,0.7.2,True mako,mako,1.0.14,1.3.8,True mariadb,mariadb,1.0.11,1.1.12,True -mcp,mcp,1.10.0,1.11.0,True +mcp,mcp,1.10.1,1.11.0,True molten,molten,1.0.2,1.0.2,True mongoengine,mongoengine,0.23.1,0.29.1,True mysql-connector-python,mysql,8.0.5,9.0.0,True @@ -69,7 +69,7 @@ openai-agents,openai_agents,0.0.8,0.0.16,True protobuf,protobuf,5.29.3,6.30.1,False psycopg,psycopg,3.0.18,3.2.9,True psycopg2-binary,psycopg,2.8.6,2.9.10,True -pydantic-ai-slim,pydantic_ai *,0.3.0,0.3.5,True +pydantic-ai-slim,pydantic_ai *,0.3.0,0.4.4,True pylibmc,pylibmc,1.6.3,1.6.3,True pymemcache,pymemcache,3.4.4,4.0.0,True pymongo,pymongo,3.8.0,4.13.2,True diff --git a/tests/contrib/pydantic_ai/cassettes/agent_run_stream_with_toolset.yaml b/tests/contrib/pydantic_ai/cassettes/agent_run_stream_with_toolset.yaml new file mode 100644 index 00000000000..c94c39d6174 --- /dev/null +++ b/tests/contrib/pydantic_ai/cassettes/agent_run_stream_with_toolset.yaml @@ -0,0 +1,112 @@ +interactions: +- request: + body: '{"messages":[{"role":"user","content":"Hello, world!"}],"model":"gpt-4o","stream":false,"tool_choice":"auto","tools":[{"type":"function","function":{"name":"foo_tool","description":"Return + foo string","parameters":{"additionalProperties":false,"properties":{},"type":"object"}}},{"type":"function","function":{"name":"calculate_square_tool","description":"Calculates + the square of a number","parameters":{"additionalProperties":false,"properties":{"x":{"type":"integer"}},"required":["x"],"type":"object"},"strict":true}}]}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate + connection: + - keep-alive + content-length: + - '523' + content-type: + - application/json + cookie: + - _cfuvid=7yprwreREaQe1ApoE3CUHN.9GZrN53KIrD0S10.M5g0-1750870032909-0.0.1.1-604800000 + host: + - api.openai.com + user-agent: + - pydantic-ai/0.4.4 + x-stainless-arch: + - arm64 + x-stainless-async: + - async:asyncio + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 1.98.0 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.10.15 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: !!binary | + H4sIAAAAAAAAA4xSy27bMBC86yvYPVuFrNiW4ksAJwfnHBQIUAQCTa5kthSXIammbuB/LyjZltwH + kIsOOzujmeG+J4yBkrBmIPY8iNbqdPNrOee71+Xy7bBdbVY39PR8Lx++PL+6TSFgFhm0+4YinFmf + BbVWY1BkBlg45AGj6rxY3pS3RZbnPdCSRB1pjQ3pgtI8yxdpVqbZ6kTckxLoYc2+Jowx9t5/o0Uj + 8SesWTY7T1r0njcI68sSY+BIxwlw75UP3ASYjaAgE9D0rreoNX1iW3pjghv2yAYCO1DHAkl+uJsS + Hdad59G36bSeANwYCjzm7i2/nJDjxaSmxjra+T+oUCuj/L5yyD2ZaMgHstCjx4Sxl76M7iofWEet + DVWg79j/bjUf5GBsfwTnp6IgUOB6nBdn0pVaJTFwpf2kTBBc7FGOzLF53klFEyCZZP7bzL+0h9zK + NB+RHwEh0AaUlXUolbgOPK45jLf5v7VLx71h8Oh+KIFVUOjiO0iseaeHswF/8AHbqlamQWedGm6n + thXPy/K2LHaLBSTH5DcAAAD//wMApte0ukQDAAA= + headers: + CF-RAY: + - 96769f27bc9e874c-IAD + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Wed, 30 Jul 2025 17:37:03 GMT + Server: + - cloudflare + Set-Cookie: + - __cf_bm=T0zb7FLDJ8ygDf3MzBLdIqTyYQ.CrAlR7bsro8ejYBo-1753897023-1.0.1.1-C6lZJmZHFShaYZLFZ0EE42dpP4SXsThutCJFmSigDxuaU7_I_pf5s.DDRcvoMbqIYg1._otssyRclL7qOE.PrQ9Plz0OzA608S.ZkULmei0; + path=/; expires=Wed, 30-Jul-25 18:07:03 GMT; domain=.api.openai.com; HttpOnly; + Secure; SameSite=None + - _cfuvid=xRE5bdx5qd8AFyvYenRWWh4YjuD1HEAj_VRUi0LANJk-1753897023184-0.0.1.1-604800000; + path=/; domain=.api.openai.com; HttpOnly; Secure; SameSite=None + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-organization: + - datadog-4 + openai-processing-ms: + - '363' + openai-project: + - proj_6cMiry5CHgK3zKotG0LtMb9H + openai-version: + - '2020-10-01' + strict-transport-security: + - max-age=31536000; includeSubDomains; preload + x-envoy-upstream-service-time: + - '371' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '30000000' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '29999994' + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 0s + x-request-id: + - req_c5241741af5a197ac91878287bee3aed + status: + code: 200 + message: OK +version: 1 diff --git a/tests/contrib/pydantic_ai/test_pydantic_ai_llmobs.py b/tests/contrib/pydantic_ai/test_pydantic_ai_llmobs.py index eba7344218f..525cad5a5a7 100644 --- a/tests/contrib/pydantic_ai/test_pydantic_ai_llmobs.py +++ b/tests/contrib/pydantic_ai/test_pydantic_ai_llmobs.py @@ -1,17 +1,24 @@ import mock +import pydantic_ai import pytest from typing_extensions import TypedDict +from ddtrace.internal.utils.version import parse_version from ddtrace.llmobs._utils import safe_json from tests.contrib.pydantic_ai.utils import calculate_square_tool from tests.contrib.pydantic_ai.utils import expected_agent_metadata from tests.contrib.pydantic_ai.utils import expected_calculate_square_tool +from tests.contrib.pydantic_ai.utils import expected_foo_tool from tests.contrib.pydantic_ai.utils import expected_run_agent_span_event from tests.contrib.pydantic_ai.utils import expected_run_tool_span_event +from tests.contrib.pydantic_ai.utils import foo_tool from tests.contrib.pydantic_ai.utils import get_usage from tests.llmobs._utils import _expected_llmobs_non_llm_span_event +PYDANTIC_AI_VERSION = parse_version(pydantic_ai.__version__) + + @pytest.mark.parametrize( "ddtrace_global_config", [dict(_llmobs_enabled=True, _llmobs_ml_app="", _llmobs_agentless_enabled=True)], @@ -215,6 +222,26 @@ async def test_agent_iter_error(self, pydantic_ai, request_vcr, llmobs_events): assert llmobs_events[0]["status"] == "error" assert llmobs_events[0]["meta"]["error.message"] == "test error" + @pytest.mark.skipif(PYDANTIC_AI_VERSION < (0, 4, 4), reason="pydantic-ai < 0.4.4 does not support toolsets") + async def test_agent_run_with_toolset(self, pydantic_ai, request_vcr, llmobs_events, mock_tracer): + """Test that the agent manifest includes tools from both the function toolset and the user-defined toolsets""" + from pydantic_ai.toolsets import FunctionToolset + + with request_vcr.use_cassette("agent_run_stream_with_toolset.yaml"): + agent = pydantic_ai.Agent( + model="gpt-4o", + name="test_agent", + toolsets=[FunctionToolset(tools=[calculate_square_tool])], + tools=[foo_tool], + ) + result = await agent.run("Hello, world!") + token_metrics = get_usage(result) + span = mock_tracer.pop_traces()[0][0] + assert len(llmobs_events) == 1 + assert llmobs_events[0] == expected_run_agent_span_event( + span, result.output, token_metrics, tools=expected_calculate_square_tool() + expected_foo_tool() + ) + class TestLLMObsPydanticAISpanLinks: async def test_agent_calls_tool(self, pydantic_ai, request_vcr, llmobs_events): diff --git a/tests/contrib/pydantic_ai/test_pydantic_ai_patch.py b/tests/contrib/pydantic_ai/test_pydantic_ai_patch.py index 210dc5d6a0e..d5b25907ad4 100644 --- a/tests/contrib/pydantic_ai/test_pydantic_ai_patch.py +++ b/tests/contrib/pydantic_ai/test_pydantic_ai_patch.py @@ -1,9 +1,13 @@ from ddtrace.contrib.internal.pydantic_ai.patch import get_version from ddtrace.contrib.internal.pydantic_ai.patch import patch from ddtrace.contrib.internal.pydantic_ai.patch import unpatch +from ddtrace.internal.utils.version import parse_version from tests.contrib.patch import PatchTestCase +PYDANTIC_AI_VERSION = parse_version(get_version()) + + class TestPydanticAIPatch(PatchTestCase.Base): __integration_name__ = "pydantic_ai" __module_name__ = "pydantic_ai" @@ -13,12 +17,24 @@ class TestPydanticAIPatch(PatchTestCase.Base): def assert_module_patched(self, pydantic_ai): self.assert_wrapped(pydantic_ai.agent.Agent.iter) - self.assert_wrapped(pydantic_ai.tools.Tool.run) + self.assert_wrapped(pydantic_ai.agent.Agent.run_stream) + if PYDANTIC_AI_VERSION >= (0, 4, 4): + self.assert_wrapped(pydantic_ai.agent.ToolManager.handle_call) + else: + self.assert_wrapped(pydantic_ai.tools.Tool.run) def assert_not_module_patched(self, pydantic_ai): self.assert_not_wrapped(pydantic_ai.agent.Agent.iter) - self.assert_not_wrapped(pydantic_ai.tools.Tool.run) + self.assert_not_wrapped(pydantic_ai.agent.Agent.run_stream) + if PYDANTIC_AI_VERSION >= (0, 4, 4): + self.assert_not_wrapped(pydantic_ai.agent.ToolManager.handle_call) + else: + self.assert_not_wrapped(pydantic_ai.tools.Tool.run) def assert_not_module_double_patched(self, pydantic_ai): self.assert_not_double_wrapped(pydantic_ai.agent.Agent.iter) - self.assert_not_double_wrapped(pydantic_ai.tools.Tool.run) + self.assert_not_double_wrapped(pydantic_ai.agent.Agent.run_stream) + if PYDANTIC_AI_VERSION >= (0, 4, 4): + self.assert_not_double_wrapped(pydantic_ai.agent.ToolManager.handle_call) + else: + self.assert_not_double_wrapped(pydantic_ai.tools.Tool.run) diff --git a/tests/contrib/pydantic_ai/utils.py b/tests/contrib/pydantic_ai/utils.py index 3412097067e..64aded0fbca 100644 --- a/tests/contrib/pydantic_ai/utils.py +++ b/tests/contrib/pydantic_ai/utils.py @@ -13,6 +13,16 @@ def expected_calculate_square_tool(): ] +def expected_foo_tool(): + return [ + { + "name": "foo_tool", + "description": "Return foo string", + "parameters": {}, + } + ] + + def expected_agent_metadata(instructions=None, system_prompt=None, model_settings=None, tools=None) -> Dict: metadata = { "agent_manifest": { @@ -76,3 +86,8 @@ def get_usage(result): def calculate_square_tool(x: int) -> int: """Calculates the square of a number""" return x * x + + +def foo_tool() -> str: + """Return foo string""" + return "foo"