Skip to content

Commit 2a08277

Browse files
committed
Fix auto instrumentation
1 parent 467b38a commit 2a08277

File tree

4 files changed

+58
-3
lines changed

4 files changed

+58
-3
lines changed

examples/pydantic_ai_examples/temporal_graph.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import os
22

33
os.environ['PYDANTIC_DISABLE_PLUGINS'] = 'true'
4+
5+
46
import asyncio
57
import random
68
from collections.abc import Iterable
79
from dataclasses import dataclass
810
from datetime import timedelta
911
from typing import Annotated, Any, Generic, Literal
1012

13+
import logfire
1114
from temporalio import activity, workflow
1215
from temporalio.client import Client
1316
from temporalio.contrib.pydantic import pydantic_data_converter
@@ -24,6 +27,8 @@
2427
TypeExpression,
2528
)
2629

30+
logfire.configure()
31+
2732
T = TypeVar('T', infer_variance=True)
2833

2934

pydantic_ai_slim/pydantic_ai/_agent_graph.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1044,6 +1044,7 @@ def build_agent_graph(
10441044
]:
10451045
"""Build the execution [Graph][pydantic_graph.Graph] for a given agent."""
10461046
g = GraphBuilder(
1047+
name=name or 'Agent',
10471048
state_type=GraphAgentState,
10481049
deps_type=GraphAgentDeps[DepsT, OutputT],
10491050
input_type=UserPromptNode[DepsT, OutputT],

pydantic_graph/pydantic_graph/v2/graph.py

Lines changed: 44 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
from __future__ import annotations as _annotations
99

1010
import asyncio
11+
import inspect
12+
import types
1113
import uuid
1214
from collections.abc import AsyncGenerator, AsyncIterator, Iterable, Sequence
1315
from contextlib import AbstractContextManager, ExitStack, asynccontextmanager
@@ -115,6 +117,9 @@ class Graph(Generic[StateT, DepsT, InputT, OutputT]):
115117
```
116118
"""
117119

120+
name: str | None
121+
"""Optional name for the graph, if not provided the name will be inferred from the calling frame on the first call to a graph method."""
122+
118123
state_type: type[StateT]
119124
"""The type of the graph state."""
120125

@@ -163,6 +168,7 @@ async def run(
163168
deps: DepsT = None,
164169
inputs: InputT = None,
165170
span: AbstractContextManager[AbstractSpan] | None = None,
171+
infer_name: bool = True,
166172
) -> OutputT:
167173
"""Execute the graph and return the final output.
168174
@@ -174,11 +180,15 @@ async def run(
174180
deps: The dependencies instance
175181
inputs: The input data for the graph
176182
span: Optional span for tracing/instrumentation
183+
infer_name: Whether to infer the graph name from the calling frame.
177184
178185
Returns:
179186
The final output from the graph execution
180187
"""
181-
async with self.iter(state=state, deps=deps, inputs=inputs, span=span) as graph_run:
188+
if infer_name and self.name is None:
189+
self._infer_name(inspect.currentframe())
190+
191+
async with self.iter(state=state, deps=deps, inputs=inputs, span=span, infer_name=False) as graph_run:
182192
# Note: This would probably be better using `async for _ in graph_run`, but this tests the `next` method,
183193
# which I'm less confident will be implemented correctly if not used on the critical path. We can change it
184194
# once we have tests, etc.
@@ -198,6 +208,7 @@ async def iter(
198208
deps: DepsT = None,
199209
inputs: InputT = None,
200210
span: AbstractContextManager[AbstractSpan] | None = None,
211+
infer_name: bool = True,
201212
) -> AsyncIterator[GraphRun[StateT, DepsT, OutputT]]:
202213
"""Create an iterator for step-by-step graph execution.
203214
@@ -209,10 +220,16 @@ async def iter(
209220
deps: The dependencies instance
210221
inputs: The input data for the graph
211222
span: Optional span for tracing/instrumentation
223+
infer_name: Whether to infer the graph name from the calling frame.
212224
213225
Yields:
214226
A GraphRun instance that can be iterated for step-by-step execution
215227
"""
228+
if infer_name and self.name is None:
229+
# f_back because `asynccontextmanager` adds one frame
230+
if frame := inspect.currentframe(): # pragma: no branch
231+
self._infer_name(frame.f_back)
232+
216233
with ExitStack() as stack:
217234
entered_span: AbstractSpan | None = None
218235
if span is None:
@@ -251,6 +268,26 @@ def __repr__(self):
251268
"""
252269
return self.render()
253270

271+
def _infer_name(self, function_frame: types.FrameType | None) -> None:
272+
"""Infer the agent name from the call frame.
273+
274+
Usage should be `self._infer_name(inspect.currentframe())`.
275+
276+
Copied from `Agent`.
277+
"""
278+
assert self.name is None, 'Name already set'
279+
if function_frame is not None and (parent_frame := function_frame.f_back): # pragma: no branch
280+
for name, item in parent_frame.f_locals.items():
281+
if item is self:
282+
self.name = name
283+
return
284+
if parent_frame.f_locals != parent_frame.f_globals: # pragma: no branch
285+
# if we couldn't find the agent in locals and globals are a different dict, try globals
286+
for name, item in parent_frame.f_globals.items(): # pragma: no branch
287+
if item is self:
288+
self.name = name
289+
return
290+
254291

255292
@dataclass
256293
class GraphTask:
@@ -497,8 +534,12 @@ async def _handle_task(
497534
if isinstance(node, StartNode | Fork):
498535
return self._handle_edges(node, inputs, fork_stack)
499536
elif isinstance(node, Step):
500-
step_context = StepContext[StateT, DepsT, Any](state, deps, inputs)
501-
output = await node.call(step_context)
537+
with ExitStack() as stack:
538+
if self.graph.auto_instrument:
539+
stack.enter_context(logfire_span('run node {node_id}', node_id=node.id, node=node))
540+
541+
step_context = StepContext[StateT, DepsT, Any](state, deps, inputs)
542+
output = await node.call(step_context)
502543
if isinstance(node, NodeStep):
503544
return self._handle_node(node, output, fork_stack)
504545
else:

pydantic_graph/pydantic_graph/v2/graph_builder.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -143,6 +143,9 @@ async def process_data(ctx: StepContext[MyState, MyDeps, str]) -> int:
143143
```
144144
"""
145145

146+
name: str | None
147+
"""Optional name for the graph, if not provided the name will be inferred from the calling frame on the first call to a graph method."""
148+
146149
state_type: TypeOrTypeExpression[StateT]
147150
"""The type of the graph state."""
148151

@@ -173,6 +176,7 @@ async def process_data(ctx: StepContext[MyState, MyDeps, str]) -> int:
173176
def __init__(
174177
self,
175178
*,
179+
name: str | None = None,
176180
state_type: TypeOrTypeExpression[StateT] = NoneType,
177181
deps_type: TypeOrTypeExpression[DepsT] = NoneType,
178182
input_type: TypeOrTypeExpression[GraphInputT] = NoneType,
@@ -182,12 +186,15 @@ def __init__(
182186
"""Initialize a graph builder.
183187
184188
Args:
189+
name: Optional name for the graph, if not provided the name will be inferred from the calling frame on the first call to a graph method.
185190
state_type: The type of the graph state
186191
deps_type: The type of the dependencies
187192
input_type: The type of the graph input data
188193
output_type: The type of the graph output data
189194
auto_instrument: Whether to automatically create instrumentation spans
190195
"""
196+
self.name = name
197+
191198
self.state_type = state_type
192199
self.deps_type = deps_type
193200
self.input_type = input_type
@@ -726,6 +733,7 @@ def build(self) -> Graph[StateT, DepsT, GraphInputT, GraphOutputT]:
726733
parent_forks = _collect_dominating_forks(nodes, edges_by_source)
727734

728735
return Graph[StateT, DepsT, GraphInputT, GraphOutputT](
736+
name=self.name,
729737
state_type=unpack_type_expression(self.state_type),
730738
deps_type=unpack_type_expression(self.deps_type),
731739
input_type=unpack_type_expression(self.input_type),

0 commit comments

Comments
 (0)