hammad-python 0.0.22__py3-none-any.whl → 0.0.24__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1103 @@
1
+ """hammad.genai.graphs.base - Graph implementation using pydantic-graph with Agent/LanguageModel integration"""
2
+
3
+ from typing import (
4
+ Any,
5
+ Dict,
6
+ List,
7
+ Optional,
8
+ Type,
9
+ TypeVar,
10
+ Generic,
11
+ Union,
12
+ Callable,
13
+ get_type_hints,
14
+ ParamSpec,
15
+ Awaitable,
16
+ )
17
+ from typing_extensions import Literal
18
+ from dataclasses import dataclass, field
19
+ import inspect
20
+ from functools import wraps
21
+ import asyncio
22
+
23
+ from pydantic_graph import BaseNode, End, Graph as PydanticGraph, GraphRunContext
24
+ from pydantic import BaseModel
25
+ from ..models.language.utils import (
26
+ LanguageModelRequestBuilder,
27
+ parse_messages_input,
28
+ consolidate_system_messages,
29
+ )
30
+
31
+ from ..agents.agent import Agent
32
+ from ..agents.types.agent_response import AgentResponse
33
+ from ..agents.types.agent_messages import AgentMessages
34
+ from ..models.language.model import LanguageModel
35
+ from ..models.language.types.language_model_name import LanguageModelName
36
+ from .types import (
37
+ GraphContext,
38
+ GraphResponse,
39
+ GraphStream,
40
+ GraphResponseChunk,
41
+ GraphState,
42
+ BasePlugin,
43
+ ActionSettings,
44
+ GraphHistoryEntry,
45
+ )
46
+
47
+ __all__ = [
48
+ "BaseGraph",
49
+ "action",
50
+ "ActionNode",
51
+ "GraphBuilder",
52
+ "GraphStream",
53
+ "GraphResponseChunk",
54
+ ]
55
+
56
+ T = TypeVar("T")
57
+ StateT = TypeVar("StateT")
58
+ P = ParamSpec("P")
59
+
60
+
61
+ class ActionNode(BaseNode[StateT, None, Any]):
62
+ """A pydantic-graph node that wraps a user-defined action function."""
63
+
64
+ def __init__(
65
+ self,
66
+ action_name: str,
67
+ action_func: Callable,
68
+ settings: ActionSettings,
69
+ **action_params: Any,
70
+ ):
71
+ """Initialize the action node with parameters."""
72
+ self.action_name = action_name
73
+ self.action_func = action_func
74
+ self.settings = settings
75
+
76
+ # Store action parameters as instance attributes for pydantic-graph
77
+ for param_name, param_value in action_params.items():
78
+ setattr(self, param_name, param_value)
79
+
80
+ async def run(self, ctx: GraphRunContext[StateT]) -> Union[BaseNode, End]:
81
+ """Execute the action function using Agent/LanguageModel infrastructure."""
82
+
83
+ # Create enhanced context that wraps pydantic-graph context
84
+ enhanced_ctx = GraphContext(
85
+ pydantic_context=ctx,
86
+ plugins=[], # Will be populated by BaseGraph
87
+ history=[],
88
+ metadata={},
89
+ )
90
+
91
+ # Extract action parameters from self
92
+ action_params = {}
93
+ sig = inspect.signature(self.action_func)
94
+ for param_name in sig.parameters:
95
+ if param_name not in ("self", "ctx", "context", "agent", "language_model"):
96
+ if hasattr(self, param_name):
97
+ action_params[param_name] = getattr(self, param_name)
98
+
99
+ # Get the docstring from the action function to use as field-level instructions
100
+ field_instructions = self.action_func.__doc__ or ""
101
+
102
+ # Get the global system prompt from the graph class docstring
103
+ global_system_prompt = ""
104
+ if hasattr(self, "_graph_docstring"):
105
+ global_system_prompt = self._graph_docstring
106
+
107
+ # Add well-defined step execution context
108
+ step_context = f"""
109
+ You are executing step '{self.action_name}' in a multi-step graph workflow.
110
+
111
+ Step Purpose: {field_instructions or "Execute the requested action"}
112
+
113
+ Execution Guidelines:
114
+ - Focus on completing this specific step's objective
115
+ - Provide clear, actionable output that can be used by subsequent steps
116
+ - If this step involves decision-making, be explicit about your reasoning
117
+ - Maintain consistency with the overall workflow context
118
+ """
119
+
120
+ # Check if the action function expects to handle the language model itself
121
+ expects_language_model = (
122
+ "language_model" in sig.parameters or "agent" in sig.parameters
123
+ )
124
+
125
+ if expects_language_model:
126
+ # Legacy mode: action function expects to handle language model
127
+ # Combine global system prompt with field-level instructions and step context
128
+ combined_instructions = global_system_prompt
129
+ if step_context:
130
+ combined_instructions += f"\n\n{step_context}"
131
+ if field_instructions and field_instructions not in combined_instructions:
132
+ combined_instructions += (
133
+ f"\n\nAdditional Instructions: {field_instructions}"
134
+ )
135
+
136
+ # Get verbose/debug flags and language model kwargs from the node
137
+ verbose = getattr(self, "_verbose", self.settings.verbose)
138
+ debug = getattr(self, "_debug", self.settings.debug)
139
+ language_model_kwargs = getattr(self, "_language_model_kwargs", {})
140
+
141
+ # Get end strategy parameters from node or settings
142
+ max_steps = getattr(self, "_max_steps", self.settings.max_steps)
143
+ end_strategy = getattr(self, "_end_strategy", self.settings.end_strategy)
144
+ end_tool = getattr(self, "_end_tool", self.settings.end_tool)
145
+
146
+ if self.settings.tools or self.settings.instructions:
147
+ agent = Agent(
148
+ name=self.settings.name or self.action_name,
149
+ instructions=self.settings.instructions or combined_instructions,
150
+ model=self.settings.model or "openai/gpt-4o-mini",
151
+ tools=self.settings.tools,
152
+ max_steps=max_steps,
153
+ end_strategy=end_strategy,
154
+ end_tool=end_tool,
155
+ verbose=verbose,
156
+ debug=debug,
157
+ **language_model_kwargs,
158
+ )
159
+ # Pass history to context if available
160
+ history = getattr(self, "_history", None)
161
+ if history:
162
+ enhanced_ctx.metadata["history"] = history
163
+
164
+ if asyncio.iscoroutinefunction(self.action_func):
165
+ result = await self.action_func(
166
+ enhanced_ctx, agent, **action_params
167
+ )
168
+ else:
169
+ result = self.action_func(enhanced_ctx, agent, **action_params)
170
+ else:
171
+ language_model = LanguageModel(
172
+ model=self.settings.model or "openai/gpt-4o-mini",
173
+ verbose=verbose,
174
+ debug=debug,
175
+ **language_model_kwargs,
176
+ )
177
+ # Pass history to context if available
178
+ history = getattr(self, "_history", None)
179
+ if history:
180
+ enhanced_ctx.metadata["history"] = history
181
+
182
+ if asyncio.iscoroutinefunction(self.action_func):
183
+ result = await self.action_func(
184
+ enhanced_ctx, language_model, **action_params
185
+ )
186
+ else:
187
+ result = self.action_func(
188
+ enhanced_ctx, language_model, **action_params
189
+ )
190
+ else:
191
+ # New mode: framework handles language model internally
192
+ # Build the user message from the action parameters with clear context
193
+ user_message = ""
194
+ if action_params:
195
+ if len(action_params) == 1:
196
+ # Single parameter - use its value directly with context
197
+ param_value = list(action_params.values())[0]
198
+ user_message = f"Process the following input for step '{self.action_name}':\n\n{param_value}"
199
+ else:
200
+ # Multiple parameters - format them clearly
201
+ param_list = "\n".join(
202
+ f"- {k}: {v}" for k, v in action_params.items()
203
+ )
204
+ user_message = f"Execute step '{self.action_name}' with the following parameters:\n\n{param_list}"
205
+ else:
206
+ # No parameters - provide clear step instruction
207
+ user_message = f"Execute the '{self.action_name}' step of the workflow."
208
+
209
+ # Combine global system prompt with step context and field-level instructions
210
+ combined_instructions = global_system_prompt
211
+ if step_context:
212
+ combined_instructions += f"\n\n{step_context}"
213
+ if field_instructions and field_instructions not in combined_instructions:
214
+ combined_instructions += (
215
+ f"\n\nAdditional Instructions: {field_instructions}"
216
+ )
217
+
218
+ # Add execution guidelines for framework mode
219
+ execution_guidelines = """
220
+
221
+ Execution Guidelines:
222
+ - Provide a clear, direct response that addresses the step's objective
223
+ - Your output will be used as input for subsequent workflow steps
224
+ - Be concise but comprehensive in your response
225
+ - If making decisions or analysis, show your reasoning process
226
+ """
227
+ combined_instructions += execution_guidelines
228
+
229
+ # Get verbose/debug flags and language model kwargs from the node
230
+ verbose = getattr(self, "_verbose", self.settings.verbose)
231
+ debug = getattr(self, "_debug", self.settings.debug)
232
+ language_model_kwargs = getattr(self, "_language_model_kwargs", {})
233
+
234
+ # Get end strategy parameters from node or settings
235
+ max_steps = getattr(self, "_max_steps", self.settings.max_steps)
236
+ end_strategy = getattr(self, "_end_strategy", self.settings.end_strategy)
237
+ end_tool = getattr(self, "_end_tool", self.settings.end_tool)
238
+
239
+ # Determine if we need to use Agent or LanguageModel
240
+ if self.settings.tools or self.settings.instructions:
241
+ # Use Agent for complex operations with tools/instructions
242
+ agent = Agent(
243
+ name=self.settings.name or self.action_name,
244
+ instructions=self.settings.instructions or combined_instructions,
245
+ model=self.settings.model or "openai/gpt-4o-mini",
246
+ tools=self.settings.tools,
247
+ max_steps=max_steps,
248
+ end_strategy=end_strategy,
249
+ end_tool=end_tool,
250
+ verbose=verbose,
251
+ debug=debug,
252
+ **language_model_kwargs,
253
+ )
254
+
255
+ # Get history if available
256
+ history = getattr(self, "_history", None)
257
+
258
+ # Run the agent with the user message and history
259
+ if history:
260
+ # If history is provided, we need to combine it with the user message
261
+ # The history should be the conversation context, and user_message is the new input
262
+ combined_messages = parse_messages_input(history)
263
+ combined_messages.append({"role": "user", "content": user_message})
264
+ agent_result = await agent.async_run(combined_messages)
265
+ else:
266
+ agent_result = await agent.async_run(user_message)
267
+ result = agent_result.output
268
+ else:
269
+ # Use LanguageModel for simple operations
270
+ language_model = LanguageModel(
271
+ model=self.settings.model or "openai/gpt-4o-mini",
272
+ verbose=verbose,
273
+ debug=debug,
274
+ **language_model_kwargs,
275
+ )
276
+
277
+ # Get history if available
278
+ history = getattr(self, "_history", None)
279
+
280
+ # Create messages using the language model utils
281
+ if history:
282
+ # If history is provided, use it as the base messages
283
+ messages = parse_messages_input(
284
+ history, instructions=combined_instructions
285
+ )
286
+ # Then add the user message from action parameters
287
+ messages.append({"role": "user", "content": user_message})
288
+ else:
289
+ # Otherwise, use the user message
290
+ messages = parse_messages_input(
291
+ user_message, instructions=combined_instructions
292
+ )
293
+ messages = consolidate_system_messages(messages)
294
+
295
+ # Run the language model with the consolidated messages
296
+ lm_result = await language_model.async_run(messages)
297
+ result = lm_result.output
298
+
299
+ # Get the return type annotation to determine expected output type
300
+ return_type = sig.return_annotation
301
+ if return_type != inspect.Parameter.empty and return_type != str:
302
+ # If the action expects a specific return type, try to parse it
303
+ # For now, we'll just return the string result
304
+ # In a full implementation, we'd use structured output parsing
305
+ pass
306
+
307
+ # Handle the result based on settings
308
+ if isinstance(result, (BaseNode, End)):
309
+ return result
310
+ elif self.settings.terminates:
311
+ return End(result)
312
+ else:
313
+ # For non-terminating actions that don't return a node, continue to next
314
+ # This would be more sophisticated in a real implementation with routing
315
+ return End(result)
316
+
317
+
318
+ class ActionDecorator:
319
+ """Decorator for creating actions that become nodes in the graph."""
320
+
321
+ def __init__(self):
322
+ self._actions: Dict[str, Type[ActionNode]] = {}
323
+ self._start_action: Optional[str] = None
324
+
325
+ def __call__(
326
+ self,
327
+ func: Optional[Callable] = None,
328
+ *,
329
+ model: Optional[LanguageModelName | str] = None,
330
+ temperature: Optional[float] = None,
331
+ max_tokens: Optional[int] = None,
332
+ tools: Optional[List[Callable]] = None,
333
+ start: bool = False,
334
+ terminates: bool = False,
335
+ xml: Optional[str] = None,
336
+ next: Optional[Union[str, List[str]]] = None,
337
+ read_history: bool = False,
338
+ persist_history: bool = False,
339
+ condition: Optional[str] = None,
340
+ name: Optional[str] = None,
341
+ instructions: Optional[str] = None,
342
+ verbose: bool = False,
343
+ debug: bool = False,
344
+ # Agent end strategy parameters
345
+ max_steps: Optional[int] = None,
346
+ end_strategy: Optional[Literal["tool"]] = None,
347
+ end_tool: Optional[Callable] = None,
348
+ **kwargs: Any,
349
+ ) -> Union[Callable, Type[ActionNode]]:
350
+ """Main action decorator."""
351
+
352
+ settings = ActionSettings(
353
+ model=model,
354
+ temperature=temperature,
355
+ max_tokens=max_tokens,
356
+ tools=tools or [],
357
+ start=start,
358
+ terminates=terminates,
359
+ xml=xml,
360
+ next=next,
361
+ read_history=read_history,
362
+ persist_history=persist_history,
363
+ condition=condition,
364
+ name=name,
365
+ instructions=instructions,
366
+ verbose=verbose,
367
+ debug=debug,
368
+ max_steps=max_steps,
369
+ end_strategy=end_strategy,
370
+ end_tool=end_tool,
371
+ kwargs=kwargs,
372
+ )
373
+
374
+ def decorator(f: Callable) -> Callable:
375
+ action_name = name or f.__name__
376
+
377
+ # Create a dynamic ActionNode class for this specific action
378
+ class DynamicActionNode(ActionNode[StateT]):
379
+ def __init__(self, **action_params):
380
+ super().__init__(
381
+ action_name=action_name,
382
+ action_func=f,
383
+ settings=settings,
384
+ **action_params,
385
+ )
386
+
387
+ # Store the action
388
+ self._actions[action_name] = DynamicActionNode
389
+ if start:
390
+ if self._start_action is not None:
391
+ raise ValueError(
392
+ f"Multiple start actions: {self._start_action} and {action_name}"
393
+ )
394
+ self._start_action = action_name
395
+
396
+ # Return the original function with metadata attached
397
+ f._action_name = action_name
398
+ f._action_settings = settings
399
+ f._action_node_class = DynamicActionNode
400
+ f._is_start = start
401
+
402
+ return f
403
+
404
+ if func is None:
405
+ return decorator
406
+ else:
407
+ return decorator(func)
408
+
409
+ def start(
410
+ self, func: Optional[Callable] = None, **kwargs
411
+ ) -> Union[Callable, Type[ActionNode]]:
412
+ """Decorator for start actions."""
413
+ return self.__call__(func, start=True, **kwargs)
414
+
415
+ def end(
416
+ self, func: Optional[Callable] = None, **kwargs
417
+ ) -> Union[Callable, Type[ActionNode]]:
418
+ """Decorator for end actions."""
419
+ return self.__call__(func, terminates=True, **kwargs)
420
+
421
+
422
+ # Global action decorator
423
+ action = ActionDecorator()
424
+
425
+
426
+ class GraphBuilder(Generic[StateT, T]):
427
+ """Builder for creating graphs with plugins and configuration."""
428
+
429
+ def __init__(self, graph_class: Type["BaseGraph[StateT, T]"]):
430
+ self.graph_class = graph_class
431
+ self.plugins: List[BasePlugin] = []
432
+ self.global_model: Optional[LanguageModelName] = None
433
+ self.global_settings: Dict[str, Any] = {}
434
+
435
+ def with_plugin(self, plugin: BasePlugin) -> "GraphBuilder[StateT, T]":
436
+ """Add a plugin to the graph."""
437
+ self.plugins.append(plugin)
438
+ return self
439
+
440
+ def with_model(self, model: LanguageModelName) -> "GraphBuilder[StateT, T]":
441
+ """Set the global model for the graph."""
442
+ self.global_model = model
443
+ return self
444
+
445
+ def with_settings(self, **settings: Any) -> "GraphBuilder[StateT, T]":
446
+ """Set global settings for the graph."""
447
+ self.global_settings.update(settings)
448
+ return self
449
+
450
+ def build(self) -> "BaseGraph[StateT, T]":
451
+ """Build the graph instance."""
452
+ instance = self.graph_class()
453
+ instance._plugins = self.plugins
454
+ instance._global_model = self.global_model
455
+ instance._global_settings = self.global_settings
456
+ instance._initialize()
457
+ return instance
458
+
459
+
460
+ class BaseGraph(Generic[StateT, T]):
461
+ """Base class for graphs that provides action decorator support on top of pydantic-graph."""
462
+
463
+ def __init__(self, state: Optional[StateT] = None):
464
+ self._plugins: List[BasePlugin] = []
465
+ self._global_model: Optional[LanguageModelName] = None
466
+ self._global_settings: Dict[str, Any] = {}
467
+ self._pydantic_graph: Optional[PydanticGraph] = None
468
+ self._action_nodes: Dict[str, Type[ActionNode]] = {}
469
+ self._start_action_name: Optional[str] = None
470
+ self._start_action_func: Optional[Callable] = None
471
+ self._state: Optional[StateT] = state
472
+ self._state_class: Optional[Type[StateT]] = None
473
+ # Initialize the graph automatically
474
+ self._initialize()
475
+
476
+ def _initialize(self) -> None:
477
+ """Initialize the graph by collecting actions and creating the pydantic graph."""
478
+ self._collect_state_class()
479
+ self._collect_actions()
480
+ self._create_pydantic_graph()
481
+
482
+ def _collect_state_class(self) -> None:
483
+ """Collect the State class if defined in the graph."""
484
+ # Look for a State class defined in the graph
485
+ for attr_name in dir(self.__class__):
486
+ attr = getattr(self.__class__, attr_name)
487
+ if (
488
+ isinstance(attr, type)
489
+ and attr_name == "State"
490
+ and attr != self.__class__
491
+ ):
492
+ self._state_class = attr
493
+ # If no state was provided in constructor, try to create default instance
494
+ if self._state is None:
495
+ try:
496
+ if hasattr(attr, "__call__"):
497
+ self._state = attr()
498
+ except Exception:
499
+ # If we can't create a default instance, leave it as None
500
+ pass
501
+ break
502
+
503
+ def _collect_actions(self) -> None:
504
+ """Collect all actions defined in the graph class."""
505
+ actions_found = []
506
+
507
+ # Get the graph class docstring for global system prompt
508
+ graph_docstring = self.__class__.__doc__ or ""
509
+
510
+ for attr_name in dir(self):
511
+ attr = getattr(self, attr_name)
512
+ if hasattr(attr, "_action_name"):
513
+ action_name = attr._action_name
514
+ action_node_class = attr._action_node_class
515
+
516
+ self._action_nodes[action_name] = action_node_class
517
+ actions_found.append((action_name, attr))
518
+
519
+ if hasattr(attr, "_is_start") and attr._is_start:
520
+ if self._start_action_name is not None:
521
+ raise ValueError(
522
+ f"Multiple start actions: {self._start_action_name} and {action_name}"
523
+ )
524
+ self._start_action_name = action_name
525
+ self._start_action_func = attr
526
+
527
+ # If no explicit start action was defined and we have exactly one action,
528
+ # automatically make it the start action
529
+ if self._start_action_name is None and len(actions_found) == 1:
530
+ action_name, action_func = actions_found[0]
531
+ self._start_action_name = action_name
532
+ self._start_action_func = action_func
533
+
534
+ # Store the graph docstring in all action nodes for access during execution
535
+ for action_node_class in self._action_nodes.values():
536
+ # We'll add this to the action node instances when they're created
537
+ action_node_class._graph_docstring = graph_docstring
538
+
539
+ def _create_pydantic_graph(self) -> None:
540
+ """Create the underlying pydantic graph from collected actions."""
541
+ if not self._action_nodes:
542
+ raise ValueError("No actions defined in graph")
543
+
544
+ # Create the pydantic graph with the node classes
545
+ node_classes = list(self._action_nodes.values())
546
+ self._pydantic_graph = PydanticGraph(nodes=node_classes)
547
+
548
+ def _get_start_action_signature(self) -> inspect.Signature:
549
+ """Get the signature of the start action for type-safe run methods."""
550
+ if self._start_action_func is None:
551
+ return inspect.Signature([])
552
+
553
+ sig = inspect.signature(self._start_action_func)
554
+ # Filter out 'self', 'ctx'/'context', 'agent', 'language_model' parameters
555
+ params = []
556
+ for param_name, param in sig.parameters.items():
557
+ if param_name not in ("self", "ctx", "context", "agent", "language_model"):
558
+ params.append(param)
559
+
560
+ return inspect.Signature(params)
561
+
562
+ def run(
563
+ self,
564
+ *args,
565
+ state: Optional[StateT] = None,
566
+ history: Optional[AgentMessages] = None,
567
+ verbose: bool = False,
568
+ debug: bool = False,
569
+ **kwargs,
570
+ ) -> GraphResponse[T, StateT]:
571
+ """
572
+ Run the graph with the given parameters.
573
+ The signature is dynamically determined by the start action.
574
+
575
+ Args:
576
+ *args: Arguments for the start action
577
+ state: Optional state object to use for the execution
578
+ history: Optional chat history in various formats (str, messages list, History object)
579
+ verbose: Enable verbose logging
580
+ debug: Enable debug logging
581
+ **kwargs: Additional keyword arguments for the start action and language model
582
+
583
+ Returns:
584
+ GraphResponse containing the execution result and metadata
585
+ """
586
+
587
+ if self._start_action_name is None:
588
+ raise ValueError("No start action defined")
589
+
590
+ # Get the start action node class
591
+ start_node_class = self._action_nodes[self._start_action_name]
592
+
593
+ # Create the start node instance with the provided arguments
594
+ start_sig = self._get_start_action_signature()
595
+
596
+ # Separate language model kwargs from start action kwargs
597
+ language_model_kwargs = {}
598
+ start_action_kwargs = {}
599
+
600
+ # Language model specific parameters
601
+ lm_params = {
602
+ "temperature",
603
+ "max_tokens",
604
+ "top_p",
605
+ "frequency_penalty",
606
+ "presence_penalty",
607
+ "stop",
608
+ "stream",
609
+ "response_format",
610
+ "seed",
611
+ "tools",
612
+ "tool_choice",
613
+ "parallel_tool_calls",
614
+ "functions",
615
+ "function_call",
616
+ "user",
617
+ "system",
618
+ "n",
619
+ "echo",
620
+ "logprobs",
621
+ "top_logprobs",
622
+ "suffix",
623
+ "max_retries",
624
+ "timeout",
625
+ "model",
626
+ "type",
627
+ "instructor_mode",
628
+ "max_steps",
629
+ "end_strategy",
630
+ "end_tool",
631
+ }
632
+
633
+ for key, value in kwargs.items():
634
+ if key in lm_params:
635
+ language_model_kwargs[key] = value
636
+ else:
637
+ start_action_kwargs[key] = value
638
+
639
+ # Bind arguments to start action parameters
640
+ try:
641
+ bound_args = start_sig.bind(*args, **start_action_kwargs)
642
+ bound_args.apply_defaults()
643
+ except TypeError as e:
644
+ raise ValueError(
645
+ f"Invalid arguments for start action '{self._start_action_name}': {e}"
646
+ )
647
+
648
+ start_node = start_node_class(**bound_args.arguments)
649
+ # Pass the graph docstring to the node for global system prompt
650
+ start_node._graph_docstring = self.__class__.__doc__ or ""
651
+ # Pass verbose/debug flags and language model kwargs
652
+ start_node._verbose = verbose
653
+ start_node._debug = debug
654
+ start_node._language_model_kwargs = language_model_kwargs
655
+ # Pass history if provided
656
+ start_node._history = history
657
+
658
+ # Pass end strategy parameters if provided
659
+ if "max_steps" in language_model_kwargs:
660
+ start_node._max_steps = language_model_kwargs["max_steps"]
661
+ if "end_strategy" in language_model_kwargs:
662
+ start_node._end_strategy = language_model_kwargs["end_strategy"]
663
+ if "end_tool" in language_model_kwargs:
664
+ start_node._end_tool = language_model_kwargs["end_tool"]
665
+
666
+ # Run the pydantic graph
667
+ if not self._pydantic_graph:
668
+ raise ValueError("Graph not initialized")
669
+
670
+ # Use the provided state or the graph's state
671
+ execution_state = state if state is not None else self._state
672
+
673
+ # Execute the graph using pydantic-graph
674
+ try:
675
+ # For now, use sync execution - would implement proper async support
676
+ result = self._pydantic_graph.run_sync(start_node, state=execution_state)
677
+
678
+ # Extract the actual output from pydantic-graph result
679
+ if hasattr(result, "data"):
680
+ output = result.data
681
+ elif hasattr(result, "output"):
682
+ output = result.output
683
+ else:
684
+ output = str(result)
685
+
686
+ # Create our response object
687
+ return GraphResponse(
688
+ type="graph",
689
+ model=self._global_model or "openai/gpt-4o-mini",
690
+ output=output,
691
+ content=str(output),
692
+ completion=None,
693
+ state=execution_state,
694
+ history=[], # Would be populated from pydantic-graph execution
695
+ start_node=self._start_action_name,
696
+ nodes_executed=[self._start_action_name], # Would track from execution
697
+ metadata={},
698
+ )
699
+
700
+ except Exception as e:
701
+ raise RuntimeError(f"Graph execution failed: {e}") from e
702
+
703
+ def iter(
704
+ self,
705
+ *args,
706
+ state: Optional[StateT] = None,
707
+ history: Optional[AgentMessages] = None,
708
+ verbose: bool = False,
709
+ debug: bool = False,
710
+ max_steps: Optional[int] = None,
711
+ end_strategy: Optional[Literal["tool"]] = None,
712
+ end_tool: Optional[Callable] = None,
713
+ **kwargs,
714
+ ) -> GraphStream[T, StateT]:
715
+ """
716
+ Create an iterator for the graph execution.
717
+ The signature is dynamically determined by the start action.
718
+
719
+ Args:
720
+ *args: Arguments for the start action
721
+ state: Optional state object to use for the execution
722
+ history: Optional chat history in various formats (str, messages list, History object)
723
+ verbose: Enable verbose logging
724
+ debug: Enable debug logging
725
+ max_steps: Maximum number of steps to execute
726
+ end_strategy: Strategy for ending execution
727
+ end_tool: Tool to use for ending execution
728
+ **kwargs: Additional keyword arguments for the start action and language model
729
+
730
+ Returns:
731
+ GraphStream that can be iterated over to get each execution step
732
+ """
733
+
734
+ if self._start_action_name is None:
735
+ raise ValueError("No start action defined")
736
+
737
+ # Get the start action node class
738
+ start_node_class = self._action_nodes[self._start_action_name]
739
+
740
+ # Create the start node instance with the provided arguments
741
+ start_sig = self._get_start_action_signature()
742
+
743
+ # Separate language model kwargs from start action kwargs
744
+ language_model_kwargs = {}
745
+ start_action_kwargs = {}
746
+
747
+ # Language model specific parameters
748
+ lm_params = {
749
+ "temperature",
750
+ "max_tokens",
751
+ "top_p",
752
+ "frequency_penalty",
753
+ "presence_penalty",
754
+ "stop",
755
+ "stream",
756
+ "response_format",
757
+ "seed",
758
+ "tools",
759
+ "tool_choice",
760
+ "parallel_tool_calls",
761
+ "functions",
762
+ "function_call",
763
+ "user",
764
+ "system",
765
+ "n",
766
+ "echo",
767
+ "logprobs",
768
+ "top_logprobs",
769
+ "suffix",
770
+ "max_retries",
771
+ "timeout",
772
+ "model",
773
+ "type",
774
+ "instructor_mode",
775
+ "max_steps",
776
+ "end_strategy",
777
+ "end_tool",
778
+ }
779
+
780
+ for key, value in kwargs.items():
781
+ if key in lm_params:
782
+ language_model_kwargs[key] = value
783
+ else:
784
+ start_action_kwargs[key] = value
785
+
786
+ try:
787
+ bound_args = start_sig.bind(*args, **start_action_kwargs)
788
+ bound_args.apply_defaults()
789
+ except TypeError as e:
790
+ raise ValueError(
791
+ f"Invalid arguments for start action '{self._start_action_name}': {e}"
792
+ )
793
+
794
+ start_node = start_node_class(**bound_args.arguments)
795
+ # Pass the graph docstring to the node for global system prompt
796
+ start_node._graph_docstring = self.__class__.__doc__ or ""
797
+ # Pass verbose/debug flags and language model kwargs
798
+ start_node._verbose = verbose
799
+ start_node._debug = debug
800
+ start_node._language_model_kwargs = language_model_kwargs
801
+ # Pass history if provided
802
+ start_node._history = history
803
+
804
+ # Pass end strategy parameters if provided
805
+ if max_steps is not None:
806
+ start_node._max_steps = max_steps
807
+ if end_strategy is not None:
808
+ start_node._end_strategy = end_strategy
809
+ if end_tool is not None:
810
+ start_node._end_tool = end_tool
811
+
812
+ # Use the provided state or the graph's state
813
+ execution_state = state if state is not None else self._state
814
+
815
+ # Create and return GraphStream
816
+ return GraphStream(
817
+ graph=self,
818
+ start_node=start_node,
819
+ state=execution_state,
820
+ verbose=verbose,
821
+ debug=debug,
822
+ max_steps=max_steps,
823
+ end_strategy=end_strategy,
824
+ end_tool=end_tool,
825
+ **language_model_kwargs,
826
+ )
827
+
828
+ async def async_run(
829
+ self,
830
+ *args,
831
+ state: Optional[StateT] = None,
832
+ history: Optional[AgentMessages] = None,
833
+ verbose: bool = False,
834
+ debug: bool = False,
835
+ max_steps: Optional[int] = None,
836
+ end_strategy: Optional[Literal["tool"]] = None,
837
+ end_tool: Optional[Callable] = None,
838
+ **kwargs,
839
+ ) -> GraphResponse[T, StateT]:
840
+ """Async version of run.
841
+
842
+ Args:
843
+ *args: Arguments for the start action
844
+ state: Optional state object to use for the execution
845
+ history: Optional chat history in various formats (str, messages list, History object)
846
+ verbose: Enable verbose logging
847
+ debug: Enable debug logging
848
+ **kwargs: Additional keyword arguments for the start action and language model
849
+
850
+ Returns:
851
+ GraphResponse containing the execution result and metadata
852
+ """
853
+
854
+ if self._start_action_name is None:
855
+ raise ValueError("No start action defined")
856
+
857
+ # Get the start action node class
858
+ start_node_class = self._action_nodes[self._start_action_name]
859
+
860
+ # Create the start node instance with the provided arguments
861
+ start_sig = self._get_start_action_signature()
862
+
863
+ # Separate language model kwargs from start action kwargs
864
+ language_model_kwargs = {}
865
+ start_action_kwargs = {}
866
+
867
+ # Language model specific parameters
868
+ lm_params = {
869
+ "temperature",
870
+ "max_tokens",
871
+ "top_p",
872
+ "frequency_penalty",
873
+ "presence_penalty",
874
+ "stop",
875
+ "stream",
876
+ "response_format",
877
+ "seed",
878
+ "tools",
879
+ "tool_choice",
880
+ "parallel_tool_calls",
881
+ "functions",
882
+ "function_call",
883
+ "user",
884
+ "system",
885
+ "n",
886
+ "echo",
887
+ "logprobs",
888
+ "top_logprobs",
889
+ "suffix",
890
+ "max_retries",
891
+ "timeout",
892
+ "model",
893
+ "type",
894
+ "instructor_mode",
895
+ "max_steps",
896
+ "end_strategy",
897
+ "end_tool",
898
+ }
899
+
900
+ for key, value in kwargs.items():
901
+ if key in lm_params:
902
+ language_model_kwargs[key] = value
903
+ else:
904
+ start_action_kwargs[key] = value
905
+
906
+ try:
907
+ bound_args = start_sig.bind(*args, **start_action_kwargs)
908
+ bound_args.apply_defaults()
909
+ except TypeError as e:
910
+ raise ValueError(
911
+ f"Invalid arguments for start action '{self._start_action_name}': {e}"
912
+ )
913
+
914
+ start_node = start_node_class(**bound_args.arguments)
915
+ # Pass the graph docstring to the node for global system prompt
916
+ start_node._graph_docstring = self.__class__.__doc__ or ""
917
+ # Pass verbose/debug flags and language model kwargs
918
+ start_node._verbose = verbose
919
+ start_node._debug = debug
920
+ start_node._language_model_kwargs = language_model_kwargs
921
+ # Pass history if provided
922
+ start_node._history = history
923
+
924
+ # Pass end strategy parameters if provided
925
+ if max_steps is not None:
926
+ start_node._max_steps = max_steps
927
+ if end_strategy is not None:
928
+ start_node._end_strategy = end_strategy
929
+ if end_tool is not None:
930
+ start_node._end_tool = end_tool
931
+
932
+ # Run the pydantic graph asynchronously
933
+ if not self._pydantic_graph:
934
+ raise ValueError("Graph not initialized")
935
+
936
+ # Use the provided state or the graph's state
937
+ execution_state = state if state is not None else self._state
938
+
939
+ try:
940
+ # Execute the graph using pydantic-graph async
941
+ result = await self._pydantic_graph.run(start_node, state=execution_state)
942
+
943
+ # Extract the actual output from pydantic-graph result
944
+ if hasattr(result, "data"):
945
+ output = result.data
946
+ elif hasattr(result, "output"):
947
+ output = result.output
948
+ else:
949
+ output = str(result)
950
+
951
+ # Create our response object
952
+ return GraphResponse(
953
+ type="graph",
954
+ model=self._global_model or "openai/gpt-4o-mini",
955
+ output=output,
956
+ content=str(output),
957
+ completion=None,
958
+ state=execution_state,
959
+ history=[], # Would be populated from pydantic-graph execution
960
+ start_node=self._start_action_name,
961
+ nodes_executed=[self._start_action_name], # Would track from execution
962
+ metadata={},
963
+ )
964
+
965
+ except Exception as e:
966
+ raise RuntimeError(f"Async graph execution failed: {e}") from e
967
+
968
+ async def async_iter(
969
+ self,
970
+ *args,
971
+ state: Optional[StateT] = None,
972
+ history: Optional[AgentMessages] = None,
973
+ verbose: bool = False,
974
+ debug: bool = False,
975
+ max_steps: Optional[int] = None,
976
+ end_strategy: Optional[Literal["tool"]] = None,
977
+ end_tool: Optional[Callable] = None,
978
+ **kwargs,
979
+ ) -> GraphStream[T, StateT]:
980
+ """Async version of iter.
981
+
982
+ Args:
983
+ *args: Arguments for the start action
984
+ state: Optional state object to use for the execution
985
+ history: Optional chat history in various formats (str, messages list, History object)
986
+ verbose: Enable verbose logging
987
+ debug: Enable debug logging
988
+ max_steps: Maximum number of steps to execute
989
+ end_strategy: Strategy for ending execution
990
+ end_tool: Tool to use for ending execution
991
+ **kwargs: Additional keyword arguments for the start action and language model
992
+
993
+ Returns:
994
+ GraphStream that can be iterated over asynchronously
995
+ """
996
+
997
+ if self._start_action_name is None:
998
+ raise ValueError("No start action defined")
999
+
1000
+ start_node_class = self._action_nodes[self._start_action_name]
1001
+ start_sig = self._get_start_action_signature()
1002
+
1003
+ # Separate language model kwargs from start action kwargs
1004
+ language_model_kwargs = {}
1005
+ start_action_kwargs = {}
1006
+
1007
+ # Language model specific parameters
1008
+ lm_params = {
1009
+ "temperature",
1010
+ "max_tokens",
1011
+ "top_p",
1012
+ "frequency_penalty",
1013
+ "presence_penalty",
1014
+ "stop",
1015
+ "stream",
1016
+ "response_format",
1017
+ "seed",
1018
+ "tools",
1019
+ "tool_choice",
1020
+ "parallel_tool_calls",
1021
+ "functions",
1022
+ "function_call",
1023
+ "user",
1024
+ "system",
1025
+ "n",
1026
+ "echo",
1027
+ "logprobs",
1028
+ "top_logprobs",
1029
+ "suffix",
1030
+ "max_retries",
1031
+ "timeout",
1032
+ "model",
1033
+ "type",
1034
+ "instructor_mode",
1035
+ "max_steps",
1036
+ "end_strategy",
1037
+ "end_tool",
1038
+ }
1039
+
1040
+ for key, value in kwargs.items():
1041
+ if key in lm_params:
1042
+ language_model_kwargs[key] = value
1043
+ else:
1044
+ start_action_kwargs[key] = value
1045
+
1046
+ try:
1047
+ bound_args = start_sig.bind(*args, **start_action_kwargs)
1048
+ bound_args.apply_defaults()
1049
+ except TypeError as e:
1050
+ raise ValueError(
1051
+ f"Invalid arguments for start action '{self._start_action_name}': {e}"
1052
+ )
1053
+
1054
+ start_node = start_node_class(**bound_args.arguments)
1055
+ # Pass the graph docstring to the node for global system prompt
1056
+ start_node._graph_docstring = self.__class__.__doc__ or ""
1057
+ # Pass verbose/debug flags and language model kwargs
1058
+ start_node._verbose = verbose
1059
+ start_node._debug = debug
1060
+ start_node._language_model_kwargs = language_model_kwargs
1061
+ # Pass history if provided
1062
+ start_node._history = history
1063
+
1064
+ # Pass end strategy parameters if provided
1065
+ if max_steps is not None:
1066
+ start_node._max_steps = max_steps
1067
+ if end_strategy is not None:
1068
+ start_node._end_strategy = end_strategy
1069
+ if end_tool is not None:
1070
+ start_node._end_tool = end_tool
1071
+
1072
+ # Use the provided state or the graph's state
1073
+ execution_state = state if state is not None else self._state
1074
+
1075
+ # Create and return GraphStream
1076
+ return GraphStream(
1077
+ graph=self,
1078
+ start_node=start_node,
1079
+ state=execution_state,
1080
+ verbose=verbose,
1081
+ debug=debug,
1082
+ max_steps=max_steps,
1083
+ end_strategy=end_strategy,
1084
+ end_tool=end_tool,
1085
+ **language_model_kwargs,
1086
+ )
1087
+
1088
+ def visualize(self, filename: str) -> None:
1089
+ """Generate a visualization of the graph using pydantic-graph's mermaid support."""
1090
+ if self._pydantic_graph and self._start_action_name:
1091
+ start_node_class = self._action_nodes.get(self._start_action_name)
1092
+ if start_node_class:
1093
+ # Use pydantic-graph's built-in mermaid generation
1094
+ mermaid_code = self._pydantic_graph.mermaid_code(
1095
+ start_node=start_node_class
1096
+ )
1097
+ with open(filename, "w") as f:
1098
+ f.write(mermaid_code)
1099
+
1100
+ @classmethod
1101
+ def builder(cls) -> GraphBuilder[StateT, T]:
1102
+ """Create a builder for this graph."""
1103
+ return GraphBuilder(cls)