Skip to content

feat(llmobs): [MLOB-2662] [MLOB-3100] add agent manifest #13311

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 69 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
69 commits
Select commit Hold shift + click to select a range
4cc8e94
add more fields to agent manifest
ncybul May 1, 2025
c89efd0
support hosted tools
ncybul May 2, 2025
47275fa
move agent manifest to metadata field
ncybul May 7, 2025
59f4693
remove open ai agents metadata for agent spans
ncybul May 7, 2025
c05a832
fix tests
ncybul May 7, 2025
43151aa
add release note
ncybul May 7, 2025
b663a6a
update handoff field for agent manifest
ncybul May 13, 2025
6c15ea8
feat(llmobs): add pydantic_ai instrumentation
Kyle-Verhoog Feb 27, 2025
62d1d5e
fix existing apm tests
ncybul Jun 24, 2025
c01df4f
remove llmobs tagging for now and add model and provider tags to apm …
ncybul Jun 24, 2025
6ef188d
trace tool runs
ncybul Jun 25, 2025
b4b1009
trace agent.iter instead of agent run methods and add corresponding t…
ncybul Jun 25, 2025
aa95f54
add tests for tool usage
ncybul Jun 25, 2025
b59254f
Merge branch 'main' into kylev/pydantic
ncybul Jun 25, 2025
5bf858c
move setting base tags into the pydantic integration
ncybul Jun 25, 2025
1155207
remove unused imports and fixtures
ncybul Jun 25, 2025
b3d1e05
fix double patching issue and begin setting llmobs tags
ncybul Jun 25, 2025
3281376
add double patching checks
ncybul Jun 25, 2025
7d47b69
add patch tests
ncybul Jun 25, 2025
5ebcccc
run black
ncybul Jun 25, 2025
53a581c
ran ruff
ncybul Jun 25, 2025
9300a5d
type fixes
ncybul Jun 25, 2025
b8137a6
update suitespec
ncybul Jun 25, 2025
cf35d57
add pydantic ai section in suitespec
ncybul Jun 26, 2025
d232be0
remove unused argument
ncybul Jun 26, 2025
038061c
remove unneeded cassettes and reuse existing one
ncybul Jun 26, 2025
0a36d5d
use standard system property to get model provider and parse it if ne…
ncybul Jun 26, 2025
2fb2b8d
run black
ncybul Jun 26, 2025
2439bd1
ruff fix
ncybul Jun 26, 2025
611279a
add error handling and test with error
ncybul Jun 26, 2025
07baefe
add release note for pydantic ai integration
ncybul Jun 26, 2025
408f0a4
run black
ncybul Jun 26, 2025
0cf8428
add docs file
ncybul Jun 26, 2025
8acbe43
remove erroneous change to openai test
ncybul Jun 26, 2025
ebe09ca
ignore error stack for error test
ncybul Jun 26, 2025
f50ab44
run black
ncybul Jun 26, 2025
2a19f0f
update registry
ncybul Jun 27, 2025
0b8ddb6
Merge branch 'main' into kylev/pydantic
ncybul Jun 27, 2025
44f1eed
Merge branch 'kylev/pydantic' into nicole-cybul/pydantic-llmobs-tracing
ncybul Jun 27, 2025
c2a10e4
add preliminary span links
ncybul Jun 30, 2025
2cdaa69
create helper trace method
ncybul Jun 30, 2025
c57d55e
add agent span tagging
ncybul Jul 1, 2025
2ba1394
implement tool span tagging
ncybul Jul 1, 2025
8d20fb7
Merge branch 'main' into nicole-cybul/pydantic-llmobs-tracing
ncybul Jul 1, 2025
b0660eb
trace run_stream separately and add tests
ncybul Jul 7, 2025
e5cfe9d
add agent iter error test
ncybul Jul 7, 2025
59c6895
add tests for structured tool and stream calls
ncybul Jul 7, 2025
efc65ec
add test for run stream error
ncybul Jul 7, 2025
aa20370
add test for stream text where delta is true
ncybul Jul 8, 2025
ff676ee
consolidate stream text tests
ncybul Jul 8, 2025
e9593f1
add span link test
ncybul Jul 8, 2025
6096966
move calculate square tool to utils file
ncybul Jul 8, 2025
1c7c70e
add assertions on tool span events
ncybul Jul 8, 2025
9d5fbeb
add assertions for agent and tool metadata
ncybul Jul 8, 2025
b468cff
perform llmobs tagging for mid streaming errors
ncybul Jul 8, 2025
66b94c8
run black
ncybul Jul 8, 2025
6b17c96
ruff fixes
ncybul Jul 8, 2025
28323e8
add release note
ncybul Jul 8, 2025
3075c7a
type fixes
ncybul Jul 8, 2025
6679b87
fix type annotation
ncybul Jul 8, 2025
5a212c1
more type fixes
ncybul Jul 8, 2025
fbe9b1b
Merge branch 'main' into nicole-cybul/agent-manifest-instrumentation
ncybul Jul 9, 2025
43fff38
fix patching for run single turn
ncybul Jul 9, 2025
5ae2c26
Merge branch 'nicole-cybul/pydantic-llmobs-tracing' into nicole-cybul…
ncybul Jul 9, 2025
dd9f53f
add agent manifest tagging for pydantic ai
ncybul Jul 9, 2025
f7f6aae
remove metadata tagging for pydantic ai in favor of agent manifest
ncybul Jul 9, 2025
c378c4e
add agent manifest for crewai
ncybul Jul 10, 2025
f4fbda9
remove memory from crewai manifest
ncybul Jul 10, 2025
c8daba4
tag what is available for langgraph agent manifest
ncybul Jul 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 22 additions & 1 deletion ddtrace/contrib/internal/openai_agents/patch.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,12 @@
from ddtrace.contrib.internal.openai_agents.processor import LLMObsTraceProcessor
from ddtrace.llmobs._integrations.openai_agents import OpenAIAgentsIntegration
from ddtrace.trace import Pin
from ddtrace.contrib.trace_utils import with_traced_module_async
from ddtrace.contrib.trace_utils import wrap
from ddtrace.contrib.trace_utils import unwrap
from ddtrace.internal.utils import get_argument_value
from ddtrace.contrib.internal.openai_agents.utils import create_agent_manifest
from ddtrace.llmobs._constants import AGENT_MANIFEST


config._add("openai_agents", {})
Expand All @@ -17,11 +23,22 @@ def get_version() -> str:

return getattr(version, "__version__", "")


def _supported_versions() -> Dict[str, str]:
return {"agents": ">=0.0.2"}


@with_traced_module_async
async def patched_run_single_turn(agents, pin, func, instance, args, kwargs):
from ddtrace import tracer
s = tracer.current_span()
r = await func(*args, **kwargs)

agent = get_argument_value(args, kwargs, 0, "agent", None)
agent_manifest = create_agent_manifest(agent) if agent else None

s._set_ctx_item(AGENT_MANIFEST, agent_manifest)
return r

def patch():
"""
Patch the instrumented methods
Expand All @@ -34,6 +51,8 @@ def patch():
Pin().onto(agents)

add_trace_processor(LLMObsTraceProcessor(OpenAIAgentsIntegration(integration_config=config.openai_agents)))

wrap(agents.Runner, "_run_single_turn", patched_run_single_turn(agents))
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe this method may have been moved onto the agents.run.AgentRunner class. We may need to condition this patching based on the library version.



def unpatch():
Expand All @@ -44,3 +63,5 @@ def unpatch():
return

agents._datadog_patch = False

unwrap(agents.Runner, "_run_single_turn")
145 changes: 145 additions & 0 deletions ddtrace/contrib/internal/openai_agents/utils.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
from agents import Agent
from agents import Handoff
from agents import (
WebSearchTool,
FileSearchTool,
ComputerTool,
)

def create_agent_manifest(agent):
manifest = {}
manifest["framework"] = "OpenAI"
manifest["model_provider"] = "openai"

if hasattr(agent, "name"):
manifest["name"] = agent.name
if hasattr(agent, "instructions"):
manifest["instructions"] = agent.instructions
if hasattr(agent, "handoff_description"):
manifest["handoff_description"] = agent.handoff_description
if hasattr(agent, "model"):
manifest["model"] = agent.model

model_settings = extract_model_settings_from_agent(agent)
if model_settings:
manifest["model_settings"] = model_settings

tools = extract_tools_from_agent(agent)
if tools:
manifest["tools"] = tools

handoffs = extract_handoffs_from_agent(agent)
if handoffs:
manifest["handoffs"] = handoffs

guardrails = extract_guardrails_from_agent(agent)
if guardrails:
manifest["guardrails"] = guardrails

return manifest

def extract_model_settings_from_agent(agent):
if not hasattr(agent, "model_settings"):
return None

# convert model_settings to dict if it's not already
model_settings = agent.model_settings
if type(model_settings) != dict:
if hasattr(model_settings, "__dict__"):
model_settings = model_settings.__dict__
else:
return None

return make_json_compatible(model_settings)

def extract_tools_from_agent(agent):
if not hasattr(agent, "tools"):
return None

tools = []
for tool in agent.tools:
tool_dict = {}
if isinstance(tool, WebSearchTool):
if hasattr(tool, "user_location"):
tool_dict["user_location"] = tool.user_location
if hasattr(tool, "search_context_size"):
tool_dict["search_context_size"] = tool.search_context_size
elif isinstance(tool, FileSearchTool):
if hasattr(tool, "vector_store_ids"):
tool_dict["vector_store_ids"] = tool.vector_store_ids
if hasattr(tool, "max_num_results"):
tool_dict["max_num_results"] = tool.max_num_results
if hasattr(tool, "include_search_results"):
tool_dict["include_search_results"] = tool.include_search_results
elif isinstance(tool, ComputerTool):
if hasattr(tool, "name"):
tool_dict["name"] = tool.name
else:
if hasattr(tool, "name"):
tool_dict["name"] = tool.name
if hasattr(tool, "description"):
tool_dict["description"] = tool.description
if hasattr(tool, "strict_json_schema"):
tool_dict["strict_json_schema"] = tool.strict_json_schema
if hasattr(tool, "params_json_schema"):
parameter_schema = tool.params_json_schema
required_params = get_required_param_dict(parameter_schema.get("required", []))
parameters = {}
for param, schema in parameter_schema.get("properties", {}).items():
param_dict = {}
if "type" in schema:
param_dict["type"] = schema["type"]
if "title" in schema:
param_dict["title"] = schema["title"]
if param in required_params:
param_dict["required"] = True
parameters[param] = param_dict
tool_dict["parameters"] = parameters
Comment on lines +78 to +97
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PydanticAI has a very similar schema for tool definitions. It may be helpful to pull this out into a shared helper function.

tools.append(tool_dict)

return tools

def extract_handoffs_from_agent(agent):
if not hasattr(agent, "handoffs"):
return None

handoffs = []
for handoff in agent.handoffs:
handoff_dict = {}
if isinstance(handoff, Agent):
if hasattr(handoff, "handoff_description"):
handoff_dict["handoff_description"] = handoff.handoff_description
if hasattr(handoff, "name"):
handoff_dict["agent_name"] = handoff.name
elif isinstance(handoff, Handoff):
if hasattr(handoff, "tool_name"):
handoff_dict["tool_name"] = handoff.tool_name
if hasattr(handoff, "tool_description"):
handoff_dict["handoff_description"] = handoff.tool_description
if hasattr(handoff, "agent_name"):
handoff_dict["agent_name"] = handoff.agent_name
if handoff_dict:
handoffs.append(handoff_dict)

return handoffs

def extract_guardrails_from_agent(agent):
guardrails = []
if hasattr(agent, "input_guardrails"):
guardrails.extend([getattr(guardrail, "name", "") for guardrail in agent.input_guardrails])
if hasattr(agent, "output_guardrails"):
guardrails.extend([getattr(guardrail, "name", "") for guardrail in agent.output_guardrails])
return guardrails

def get_required_param_dict(required_params):
return {param: True for param in required_params}

def make_json_compatible(obj):
if isinstance(obj, dict):
return {str(k): make_json_compatible(v) for k, v in obj.items()}
elif isinstance(obj, (list, tuple, set)):
return [make_json_compatible(v) for v in obj]
elif isinstance(obj, (int, float, str, bool)) or obj is None:
return obj
else:
return str(obj)
43 changes: 39 additions & 4 deletions ddtrace/contrib/internal/pydantic_ai/patch.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import sys
from typing import Dict

from ddtrace import config
from ddtrace.contrib.internal.pydantic_ai.utils import TracedPydanticAsyncContextManager
from ddtrace.contrib.internal.pydantic_ai.utils import TracedPydanticRunStream
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
Expand All @@ -22,22 +24,53 @@ def _supported_versions() -> Dict[str, str]:
return {"pydantic_ai": "*"}


@with_traced_module
def traced_agent_run_stream(pydantic_ai, pin, func, instance, args, kwargs):
integration = pydantic_ai._datadog_integration
integration._run_stream_active = True
span = integration.trace(
pin, "Pydantic Agent", submit_to_llmobs=True, model=getattr(instance, "model", None), kind="agent"
)
span.name = getattr(instance, "name", None) or "Pydantic Agent"

result = func(*args, **kwargs)
kwargs["instance"] = instance
return TracedPydanticRunStream(result, span, integration, args, kwargs)


@with_traced_module
def traced_agent_iter(pydantic_ai, pin, func, instance, args, kwargs):
integration = pydantic_ai._datadog_integration
span = integration.trace(pin, "Pydantic Agent", submit_to_llmobs=False, model=getattr(instance, "model", None))
# avoid double tracing if run_stream has already been called
if integration._run_stream_active:
integration._run_stream_active = False
return func(*args, **kwargs)
span = integration.trace(
pin, "Pydantic Agent", submit_to_llmobs=True, model=getattr(instance, "model", None), kind="agent"
)
span.name = getattr(instance, "name", None) or "Pydantic Agent"

result = func(*args, **kwargs)
return TracedPydanticAsyncContextManager(result, span)
kwargs["instance"] = instance
return TracedPydanticAsyncContextManager(result, span, instance, integration, args, kwargs)


@with_traced_module
async def traced_tool_run(pydantic_ai, pin, func, instance, args, kwargs):
integration = pydantic_ai._datadog_integration
with integration.trace(pin, "Pydantic Tool", submit_to_llmobs=False) as span:
resp = None
try:
span = integration.trace(pin, "Pydantic Tool", submit_to_llmobs=True, kind="tool")
span.name = getattr(instance, "name", None) or "Pydantic Tool"
return await func(*args, **kwargs)
resp = await func(*args, **kwargs)
return resp
except Exception:
span.set_exc_info(*sys.exc_info())
raise
finally:
kwargs["instance"] = instance
integration.llmobs_set_tags(span, args=args, kwargs=kwargs, response=resp)
span.finish()


def patch():
Expand All @@ -53,6 +86,7 @@ def patch():

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))


def unpatch():
Expand All @@ -65,6 +99,7 @@ def unpatch():

unwrap(pydantic_ai.agent.Agent, "iter")
unwrap(pydantic_ai.tools.Tool, "run")
unwrap(pydantic_ai.agent.Agent, "run_stream")

delattr(pydantic_ai, "_datadog_integration")
Pin().remove_from(pydantic_ai)
Loading
Loading