8
8
from __future__ import annotations as _annotations
9
9
10
10
import asyncio
11
+ import inspect
12
+ import types
11
13
import uuid
12
14
from collections .abc import AsyncGenerator , AsyncIterator , Iterable , Sequence
13
15
from contextlib import AbstractContextManager , ExitStack , asynccontextmanager
@@ -115,6 +117,9 @@ class Graph(Generic[StateT, DepsT, InputT, OutputT]):
115
117
```
116
118
"""
117
119
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
+
118
123
state_type : type [StateT ]
119
124
"""The type of the graph state."""
120
125
@@ -163,6 +168,7 @@ async def run(
163
168
deps : DepsT = None ,
164
169
inputs : InputT = None ,
165
170
span : AbstractContextManager [AbstractSpan ] | None = None ,
171
+ infer_name : bool = True ,
166
172
) -> OutputT :
167
173
"""Execute the graph and return the final output.
168
174
@@ -174,11 +180,15 @@ async def run(
174
180
deps: The dependencies instance
175
181
inputs: The input data for the graph
176
182
span: Optional span for tracing/instrumentation
183
+ infer_name: Whether to infer the graph name from the calling frame.
177
184
178
185
Returns:
179
186
The final output from the graph execution
180
187
"""
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 :
182
192
# Note: This would probably be better using `async for _ in graph_run`, but this tests the `next` method,
183
193
# which I'm less confident will be implemented correctly if not used on the critical path. We can change it
184
194
# once we have tests, etc.
@@ -198,6 +208,7 @@ async def iter(
198
208
deps : DepsT = None ,
199
209
inputs : InputT = None ,
200
210
span : AbstractContextManager [AbstractSpan ] | None = None ,
211
+ infer_name : bool = True ,
201
212
) -> AsyncIterator [GraphRun [StateT , DepsT , OutputT ]]:
202
213
"""Create an iterator for step-by-step graph execution.
203
214
@@ -209,10 +220,16 @@ async def iter(
209
220
deps: The dependencies instance
210
221
inputs: The input data for the graph
211
222
span: Optional span for tracing/instrumentation
223
+ infer_name: Whether to infer the graph name from the calling frame.
212
224
213
225
Yields:
214
226
A GraphRun instance that can be iterated for step-by-step execution
215
227
"""
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
+
216
233
with ExitStack () as stack :
217
234
entered_span : AbstractSpan | None = None
218
235
if span is None :
@@ -251,6 +268,26 @@ def __repr__(self):
251
268
"""
252
269
return self .render ()
253
270
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
+
254
291
255
292
@dataclass
256
293
class GraphTask :
@@ -497,8 +534,12 @@ async def _handle_task(
497
534
if isinstance (node , StartNode | Fork ):
498
535
return self ._handle_edges (node , inputs , fork_stack )
499
536
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 )
502
543
if isinstance (node , NodeStep ):
503
544
return self ._handle_node (node , output , fork_stack )
504
545
else :
0 commit comments