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.
- hammad/__init__.py +269 -16
- hammad/_internal.py +19 -1
- hammad/cli/plugins.py +3 -1
- hammad/data/__init__.py +10 -0
- hammad/data/collections/__init__.py +5 -1
- hammad/data/sql/__init__.py +2 -1
- hammad/genai/__init__.py +57 -0
- hammad/genai/agents/__init__.py +11 -1
- hammad/genai/agents/agent.py +719 -213
- hammad/genai/agents/run.py +50 -12
- hammad/genai/agents/types/agent_response.py +2 -1
- hammad/genai/graphs/__init__.py +113 -0
- hammad/genai/graphs/base.py +1103 -0
- hammad/genai/graphs/plugins.py +316 -0
- hammad/genai/graphs/types.py +638 -0
- hammad/genai/models/embeddings/__init__.py +5 -1
- hammad/genai/models/embeddings/model.py +31 -2
- hammad/genai/models/language/__init__.py +5 -1
- hammad/genai/models/language/model.py +70 -0
- hammad/genai/models/language/run.py +29 -12
- hammad/genai/models/language/types/language_model_response.py +1 -1
- hammad/genai/types/tools.py +1 -1
- hammad/logging/logger.py +10 -0
- {hammad_python-0.0.22.dist-info → hammad_python-0.0.24.dist-info}/METADATA +5 -1
- {hammad_python-0.0.22.dist-info → hammad_python-0.0.24.dist-info}/RECORD +27 -23
- {hammad_python-0.0.22.dist-info → hammad_python-0.0.24.dist-info}/WHEEL +0 -0
- {hammad_python-0.0.22.dist-info → hammad_python-0.0.24.dist-info}/licenses/LICENSE +0 -0
@@ -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)
|