-
Notifications
You must be signed in to change notification settings - Fork 1.5k
Add Agent and agent run metadata and expose it on result objects and span attributes #3370
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
base: main
Are you sure you want to change the base?
Conversation
| def _run_span_end_attributes( | ||
| self, | ||
| settings: InstrumentationSettings, | ||
| usage: _usage.RunUsage, | ||
| message_history: list[_messages.ModelMessage], | ||
| new_message_index: int, | ||
| graph_ctx: GraphRunContext[_agent_graph.GraphAgentState, _agent_graph.GraphAgentDeps[AgentDepsT, OutputDataT]], | ||
| ): |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I reworked what attributes are passed to this because the existing ones are already available in the RunContext. I hope that's okay.
|
@sstanovnik Have you seen Baggage https://logfire.pydantic.dev/docs/reference/advanced/baggage/#basic-usage That could also be a solution here that doesn't require us to add any new fields. |
docs/logfire.md
Outdated
| from pydantic_ai.models.instrumented import InstrumentationSettings | ||
|
|
||
|
|
||
| def span_attribute_callback(ctx: RunContext[None]) -> dict[str, str]: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is useful, but not type safe unfortunately, as InstrumentationSettings is not generic in the deps type (and probably shouldn't be), meaning that you could create a span_attribute_callback that takes RunContext[Foo] and use it on an Agent[Bar] without any typing issues, which would then fail at runtime if you try to read attrs that exist on Foo but not Bar...
The obvious ways to solve that would be to make InstrumentationSettings generic or to make Agent and agent.run take the span_attributes directly, but I don't like either of those :) If baggage works for you, I'd be inclined to not add any new feature to Pydantic AI and just document that instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, I noticed that this couldn't be made type-safe without making the carrier generic, but that seems like a huge undertaking that also changes the interface in a way that, I would think, isn't desirable until v2.
Baggage would work for my immediate usecase, even though it applies to all child spans, which I don't really want it to - conceptually, I'm wanting to add data to the one specific span.
What about only accepting a dict for now, and leaving a callable taking RunContext[TDeps] for whenever InstrumentationSettings can be adapted to take a generic parameter?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sstanovnik Maybe instrumentation settings are not the best place for this:
In line with #3263, what do you think about arbitrary metadata: dict[str, Any] on the Agent, that's also set on the agent run span under a metadata attribute? That way you wouldn't be setting span attributes directly (assuming that's not an issue), but they would be queryable etc, and it'd feel more natural to add this on both Agent and agent.run directly, with support for RunContext-taking callables.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's also great - any way of adding information to the span would be great. What do you mean by
That way you wouldn't be setting span attributes directly (assuming that's not an issue), but they would be queryable [...]
I see that the linked PR does add the metadata into the span.
I'm up for closing this PR and opening another with the same approach using metadata, if you'd like :)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@sstanovnik I just mean that the metadata dict keys will not directly become span attributes, as they'd be nested under metadata, but if that's fine for your use case, feel free to update this PR (with force push possibly), or create a new one!
Metadata is attached to the logfire.agent.metadata span attribute. It can either be a string, dict, or a callable taking the RunContext and returning a string or a dict.
c57e1bb to
11687ce
Compare
sstanovnik
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Force-pushed this to retain the relevant conversation history of the previous implementation. I also added some comments on particulars I'm not certain about.
A major difference from your suggestion is that I did not add this to the Agent.run method. I felt the change was large enough as it is without having to figure out how to wire/override/merge that.
|
This PR is stale, and will be closed in 3 days if no reply is received. |
|
Apologies for the delay in responding to your review. Thank you for taking the time for this. I believe I've addressed all comments:
|
docs/agents.md
Outdated
| You can retrieve usage statistics (tokens, requests, etc.) at any time from the [`AgentRun`][pydantic_ai.agent.AgentRun] object via `agent_run.usage()`. This method returns a [`RunUsage`][pydantic_ai.usage.RunUsage] object containing the usage data. | ||
|
|
||
| Once the run finishes, `agent_run.result` becomes a [`AgentRunResult`][pydantic_ai.agent.AgentRunResult] object containing the final output (and related metadata). | ||
| You can inspect [`agent_run.metadata`][pydantic_ai.agent.AgentRun] or [`agent_run.result.metadata`][pydantic_ai.agent.AgentRunResult] after the run completes to read any metadata configured on the agent. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This deserves a separate section that also explains how to set it. I think much of what's currently in logfire.md can be moved here, and then those docs can link here.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I put most of the documentation here and only mentioned the relevant parts in logfire.md, linking here for details.
docs/logfire.md
Outdated
| 'openai:gpt-5', | ||
| instrument=True, | ||
| metadata=lambda ctx: {'deployment': 'staging', 'tenant': ctx.deps.tenant}, | ||
| ) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It'd be nice to expand the example to show running the agent with some deps.tenant, and then printing result.metadata at the end
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Example expanded - I included a complete combined example for the "Accessing usage and final output" section in the docs.
| usage: Optional usage to start with, useful for resuming a conversation or agents used in tools. | ||
| metadata: Optional metadata to attach to this run. Accepts a dictionary or a callable taking | ||
| [`RunContext`][pydantic_ai.tools.RunContext]. The resolved dictionary is shallow merged into the | ||
| agent's metadata (or any [`Agent.override`][pydantic_ai.agent.Agent.override]) with run-level keys |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Existing agent.overrides always cause the agent.run value to be ignored, fully overwriting the agent + agent run original. We should have the same behavior here
| try: | ||
| yield agent_run | ||
| finally: | ||
| resolve_run_metadata() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this need to be a function or could it be inlined?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
With the change that metadata is computed both at the start and end of the run, this now needs to be a function, but I placed it as a class method, not inline as it was before.
| base_config = self._metadata | ||
|
|
||
| base_metadata = self._resolve_metadata_config(base_config, ctx) | ||
| run_metadata = self._resolve_metadata_config(run_metadata_config, ctx) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
As mentioned above, overridden metadata fully overrides run metadata
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done, override now completely overrides both agent-constructor and run-level metadata.
| usage: Optional usage to start with, useful for resuming a conversation or agents used in tools. | ||
| metadata: Optional metadata to attach to this run. Accepts a dictionary or a callable taking | ||
| [`RunContext`][pydantic_ai.tools.RunContext]. The resolved dictionary is shallow merged into the | ||
| agent's metadata (or any [`Agent.override`][pydantic_ai.agent.Agent.override]) with run-level keys |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
See above, this behavior is inconsistent with existing expectations, so the behavior and docstring will need updating
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Docstrings updated to match the new override behaviour.
|
This PR is stale, and will be closed in 3 days if no reply is received. |
|
Thank you again bearing with my intermittent responses and for taking the time to review this code. I've addressed all comments:
|
| You can retrieve usage statistics (tokens, requests, etc.) at any time from the [`AgentRun`][pydantic_ai.agent.AgentRun] object via `agent_run.usage()`. This method returns a [`RunUsage`][pydantic_ai.usage.RunUsage] object containing the usage data. | ||
|
|
||
| Once the run finishes, `agent_run.result` becomes a [`AgentRunResult`][pydantic_ai.agent.AgentRunResult] object containing the final output (and related metadata). | ||
| You can inspect [`agent_run.metadata`][pydantic_ai.agent.AgentRun] or [`agent_run.result.metadata`][pydantic_ai.agent.AgentRunResult] after the run completes to read any metadata configured on the [`Agent`][pydantic_ai.agent.Agent]. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's make this a separate section that specifically documents the Metadata feature, and shows that it can be set on the agent as well as the run.
|
|
||
| See the [usage and metadata example in the agents guide](agents.md#accessing-usage-and-final-output) for an example that derives metadata from `deps` and accesses [`AgentRunResult.metadata`][pydantic_ai.agent.AgentRunResult]. | ||
|
|
||
| Resolved metadata is available after the run completes on |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we just need this on the main doc. If we link to there from here, this section only needs to mention that the metadata ends up on the metadata span attribute
| """Whether the output passed to an output validator is partial.""" | ||
| run_id: str | None = None | ||
| """"Unique identifier for the agent run.""" | ||
| metadata: dict[str, Any] | None = None |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Have a look at TemporalRunContext; we should add the field there to be included by default, and also update the docstring and Temporal docs to mention that.
Maybe the logic there can be changed to only list those fields that are excluded rather than the ones that are included, as new keys should typically be included unless the values are very large.
| model_settings: Optional settings to use for this model's request. | ||
| usage_limits: Optional limits on model request count or token usage. | ||
| usage: Optional usage to start with, useful for resuming a conversation or agents used in tools. | ||
| metadata: Optional metadata to attach to this run. Accepts a dictionary or a callable taking |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We should add it to the methods on UIAdapter as well
|
|
||
| final_result = agent_run.result | ||
| if instrumentation_settings and run_span.is_recording(): | ||
| if instrumentation_settings.include_content and final_result is not None: |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's combine these 2 ifs
| ) -> dict[str, Any] | None: | ||
| run_context = build_run_context(graph_run_ctx) | ||
| resolved_metadata = self._get_metadata(run_context, metadata) | ||
| run_context.metadata = resolved_metadata |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think this is needed as the run_context var is discarded
This allows users to add extra metadata to the agent's span, either as a direct string or dict, or a callable that takes the RunContext. The attributes are added/computed after the agent finishes. The metadata is under the
logfire.agent.metadataattribute.