quantalogic 0.61.3__py3-none-any.whl → 0.92__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.
Files changed (35) hide show
  1. quantalogic/agent.py +0 -1
  2. quantalogic/flow/__init__.py +16 -34
  3. quantalogic/main.py +11 -6
  4. quantalogic/tools/action_gen.py +1 -1
  5. quantalogic/tools/tool.py +8 -500
  6. quantalogic-0.92.dist-info/METADATA +448 -0
  7. {quantalogic-0.61.3.dist-info → quantalogic-0.92.dist-info}/RECORD +10 -33
  8. {quantalogic-0.61.3.dist-info → quantalogic-0.92.dist-info}/WHEEL +1 -1
  9. quantalogic-0.92.dist-info/entry_points.txt +3 -0
  10. quantalogic/codeact/__init__.py +0 -0
  11. quantalogic/codeact/agent.py +0 -499
  12. quantalogic/codeact/cli.py +0 -232
  13. quantalogic/codeact/constants.py +0 -9
  14. quantalogic/codeact/events.py +0 -78
  15. quantalogic/codeact/llm_util.py +0 -76
  16. quantalogic/codeact/prompts/error_format.j2 +0 -11
  17. quantalogic/codeact/prompts/generate_action.j2 +0 -26
  18. quantalogic/codeact/prompts/generate_program.j2 +0 -39
  19. quantalogic/codeact/prompts/response_format.j2 +0 -11
  20. quantalogic/codeact/tools_manager.py +0 -135
  21. quantalogic/codeact/utils.py +0 -135
  22. quantalogic/flow/flow.py +0 -960
  23. quantalogic/flow/flow_extractor.py +0 -723
  24. quantalogic/flow/flow_generator.py +0 -294
  25. quantalogic/flow/flow_manager.py +0 -637
  26. quantalogic/flow/flow_manager_schema.py +0 -255
  27. quantalogic/flow/flow_mermaid.py +0 -365
  28. quantalogic/flow/flow_validator.py +0 -479
  29. quantalogic/flow/flow_yaml.linkedin.md +0 -31
  30. quantalogic/flow/flow_yaml.md +0 -767
  31. quantalogic/flow/templates/prompt_check_inventory.j2 +0 -1
  32. quantalogic/flow/templates/system_check_inventory.j2 +0 -1
  33. quantalogic-0.61.3.dist-info/METADATA +0 -900
  34. quantalogic-0.61.3.dist-info/entry_points.txt +0 -6
  35. {quantalogic-0.61.3.dist-info → quantalogic-0.92.dist-info}/LICENSE +0 -0
@@ -1,637 +0,0 @@
1
- import asyncio
2
- import importlib
3
- import importlib.util
4
- import os
5
- import re
6
- import subprocess
7
- import sys
8
- import tempfile
9
- import urllib
10
- from pathlib import Path
11
- from typing import Any, Callable, Dict, List, Optional, Type, Union
12
-
13
- import yaml # type: ignore
14
- from loguru import logger
15
- from pydantic import BaseModel, ValidationError
16
-
17
- from quantalogic.flow.flow import Nodes, Workflow
18
- from quantalogic.flow.flow_manager_schema import (
19
- BranchCondition,
20
- FunctionDefinition,
21
- LLMConfig,
22
- NodeDefinition,
23
- TemplateConfig,
24
- TransitionDefinition,
25
- WorkflowDefinition,
26
- WorkflowStructure,
27
- )
28
-
29
-
30
- class WorkflowManager:
31
- def __init__(self, workflow: Optional[WorkflowDefinition] = None):
32
- """Initialize the WorkflowManager with an optional workflow definition."""
33
- self.workflow = workflow or WorkflowDefinition()
34
- self._ensure_dependencies()
35
-
36
- def _ensure_dependencies(self) -> None:
37
- """Ensure all specified dependencies are installed or available."""
38
- if not self.workflow.dependencies:
39
- return
40
-
41
- for dep in self.workflow.dependencies:
42
- if dep.startswith("http://") or dep.startswith("https://"):
43
- logger.debug(f"Dependency '{dep}' is a remote URL, will be fetched during instantiation")
44
- elif os.path.isfile(dep):
45
- logger.debug(f"Dependency '{dep}' is a local file, will be loaded during instantiation")
46
- else:
47
- try:
48
- module_name = dep.split(">")[0].split("<")[0].split("=")[0].strip()
49
- importlib.import_module(module_name)
50
- logger.debug(f"Dependency '{dep}' is already installed")
51
- except ImportError:
52
- logger.info(f"Installing dependency '{dep}' via pip")
53
- try:
54
- subprocess.check_call([sys.executable, "-m", "pip", "install", dep])
55
- logger.debug(f"Successfully installed '{dep}'")
56
- except subprocess.CalledProcessError as e:
57
- raise ValueError(f"Failed to install dependency '{dep}': {e}")
58
-
59
- def add_node(
60
- self,
61
- name: str,
62
- function: Optional[str] = None,
63
- sub_workflow: Optional[WorkflowStructure] = None,
64
- llm_config: Optional[Dict[str, Any]] = None,
65
- template_config: Optional[Dict[str, Any]] = None,
66
- inputs_mapping: Optional[Dict[str, Union[str, Callable]]] = None,
67
- output: Optional[str] = None,
68
- retries: int = 3,
69
- delay: float = 1.0,
70
- timeout: Optional[float] = None,
71
- parallel: bool = False,
72
- ) -> None:
73
- """Add a new node to the workflow definition with support for template nodes and inputs mapping."""
74
- llm_config_obj = LLMConfig(**llm_config) if llm_config is not None else None
75
- template_config_obj = TemplateConfig(**template_config) if template_config is not None else None
76
-
77
- serializable_inputs_mapping = {}
78
- if inputs_mapping:
79
- for key, value in inputs_mapping.items():
80
- if callable(value):
81
- if hasattr(value, '__name__') and value.__name__ == '<lambda>':
82
- import inspect
83
- try:
84
- source = inspect.getsource(value).strip()
85
- serializable_inputs_mapping[key] = f"lambda ctx: {source.split(':')[-1].strip()}"
86
- except Exception:
87
- serializable_inputs_mapping[key] = str(value)
88
- else:
89
- serializable_inputs_mapping[key] = value.__name__
90
- else:
91
- serializable_inputs_mapping[key] = value
92
-
93
- node = NodeDefinition(
94
- function=function,
95
- sub_workflow=sub_workflow,
96
- llm_config=llm_config_obj,
97
- template_config=template_config_obj,
98
- inputs_mapping=serializable_inputs_mapping,
99
- output=output or (f"{name}_result" if function or llm_config or template_config else None),
100
- retries=retries,
101
- delay=delay,
102
- timeout=timeout,
103
- parallel=parallel,
104
- )
105
- self.workflow.nodes[name] = node
106
-
107
- def remove_node(self, name: str) -> None:
108
- """Remove a node and clean up related transitions and start node."""
109
- if name not in self.workflow.nodes:
110
- raise ValueError(f"Node '{name}' does not exist")
111
- del self.workflow.nodes[name]
112
- self.workflow.workflow.transitions = [
113
- t
114
- for t in self.workflow.workflow.transitions
115
- if t.from_node != name and (isinstance(t.to_node, str) or all(
116
- isinstance(tn, str) and tn != name or isinstance(tn, BranchCondition) and tn.to_node != name
117
- for tn in t.to_node
118
- ))
119
- ]
120
- if self.workflow.workflow.start == name:
121
- self.workflow.workflow.start = None
122
- if name in self.workflow.workflow.convergence_nodes:
123
- self.workflow.workflow.convergence_nodes.remove(name)
124
-
125
- def update_node(
126
- self,
127
- name: str,
128
- function: Optional[str] = None,
129
- template_config: Optional[Dict[str, Any]] = None,
130
- inputs_mapping: Optional[Dict[str, Union[str, Callable]]] = None,
131
- output: Optional[str] = None,
132
- retries: Optional[int] = None,
133
- delay: Optional[float] = None,
134
- timeout: Optional[Union[float, None]] = None,
135
- parallel: Optional[bool] = None,
136
- ) -> None:
137
- """Update specific fields of an existing node with template and mapping support."""
138
- if name not in self.workflow.nodes:
139
- raise ValueError(f"Node '{name}' does not exist")
140
- node = self.workflow.nodes[name]
141
- if function is not None:
142
- node.function = function
143
- if template_config is not None:
144
- node.template_config = TemplateConfig(**template_config)
145
- if inputs_mapping is not None:
146
- serializable_inputs_mapping = {}
147
- for key, value in inputs_mapping.items():
148
- if callable(value):
149
- if hasattr(value, '__name__') and value.__name__ == '<lambda>':
150
- import inspect
151
- try:
152
- source = inspect.getsource(value).strip()
153
- serializable_inputs_mapping[key] = f"lambda ctx: {source.split(':')[-1].strip()}"
154
- except Exception:
155
- serializable_inputs_mapping[key] = str(value)
156
- else:
157
- serializable_inputs_mapping[key] = value.__name__
158
- else:
159
- serializable_inputs_mapping[key] = value
160
- node.inputs_mapping = serializable_inputs_mapping
161
- if output is not None:
162
- node.output = output
163
- if retries is not None:
164
- node.retries = retries
165
- if delay is not None:
166
- node.delay = delay
167
- if timeout is not None:
168
- node.timeout = timeout
169
- if parallel is not None:
170
- node.parallel = parallel
171
-
172
- def add_transition(
173
- self,
174
- from_node: str,
175
- to_node: Union[str, List[Union[str, BranchCondition]]],
176
- condition: Optional[str] = None,
177
- strict: bool = True,
178
- ) -> None:
179
- """Add a transition between nodes, supporting branching."""
180
- if strict:
181
- if from_node not in self.workflow.nodes:
182
- raise ValueError(f"Source node '{from_node}' does not exist")
183
- if isinstance(to_node, str):
184
- if to_node not in self.workflow.nodes:
185
- raise ValueError(f"Target node '{to_node}' does not exist")
186
- else:
187
- for t in to_node:
188
- target = t if isinstance(t, str) else t.to_node
189
- if target not in self.workflow.nodes:
190
- raise ValueError(f"Target node '{target}' does not exist")
191
- transition = TransitionDefinition(
192
- from_node=from_node,
193
- to_node=to_node,
194
- condition=condition
195
- )
196
- self.workflow.workflow.transitions.append(transition)
197
-
198
- def set_start_node(self, name: str) -> None:
199
- """Set the start node of the workflow."""
200
- if name not in self.workflow.nodes:
201
- raise ValueError(f"Node '{name}' does not exist")
202
- self.workflow.workflow.start = name
203
-
204
- def add_convergence_node(self, name: str) -> None:
205
- """Add a convergence node to the workflow."""
206
- if name not in self.workflow.nodes:
207
- raise ValueError(f"Node '{name}' does not exist")
208
- if name not in self.workflow.workflow.convergence_nodes:
209
- self.workflow.workflow.convergence_nodes.append(name)
210
- logger.debug(f"Added convergence node '{name}'")
211
-
212
- def add_function(
213
- self,
214
- name: str,
215
- type_: str,
216
- code: Optional[str] = None,
217
- module: Optional[str] = None,
218
- function: Optional[str] = None,
219
- ) -> None:
220
- """Add a function definition to the workflow."""
221
- func_def = FunctionDefinition(type=type_, code=code, module=module, function=function)
222
- self.workflow.functions[name] = func_def
223
-
224
- def add_observer(self, observer_name: str) -> None:
225
- """Add an observer function name to the workflow."""
226
- if observer_name not in self.workflow.functions:
227
- raise ValueError(f"Observer function '{observer_name}' not defined in functions")
228
- if observer_name not in self.workflow.observers:
229
- self.workflow.observers.append(observer_name)
230
- logger.debug(f"Added observer '{observer_name}' to workflow")
231
-
232
- def _resolve_model(self, model_str: str) -> Type[BaseModel]:
233
- """Resolve a string to a Pydantic model class for structured_llm_node."""
234
- try:
235
- module_name, class_name = model_str.split(":")
236
- module = importlib.import_module(module_name)
237
- model_class = getattr(module, class_name)
238
- if not issubclass(model_class, BaseModel):
239
- raise ValueError(f"{model_str} is not a Pydantic model")
240
- return model_class
241
- except (ValueError, ImportError, AttributeError) as e:
242
- raise ValueError(f"Failed to resolve response_model '{model_str}': {e}")
243
-
244
- def import_module_from_source(self, source: str) -> Any:
245
- """Import a module from various sources."""
246
- if source.startswith("http://") or source.startswith("https://"):
247
- try:
248
- with urllib.request.urlopen(source) as response:
249
- code = response.read().decode("utf-8")
250
- with tempfile.NamedTemporaryFile(delete=False, suffix=".py") as temp_file:
251
- temp_file.write(code.encode("utf-8"))
252
- temp_path = temp_file.name
253
- module_name = f"temp_module_{hash(temp_path)}"
254
- spec = importlib.util.spec_from_file_location(module_name, temp_path)
255
- if spec is None:
256
- raise ValueError(f"Failed to create module spec from {temp_path}")
257
- module = importlib.util.module_from_spec(spec)
258
- sys.modules[module_name] = module
259
- if spec.loader is None:
260
- raise ValueError(f"Module spec has no loader for {temp_path}")
261
- spec.loader.exec_module(module)
262
- os.remove(temp_path)
263
- return module
264
- except Exception as e:
265
- raise ValueError(f"Failed to import module from URL '{source}': {e}")
266
- elif os.path.isfile(source):
267
- try:
268
- module_name = f"local_module_{hash(source)}"
269
- spec = importlib.util.spec_from_file_location(module_name, source)
270
- if spec is None:
271
- raise ValueError(f"Failed to create module spec from {source}")
272
- module = importlib.util.module_from_spec(spec)
273
- sys.modules[module_name] = module
274
- if spec.loader is None:
275
- raise ValueError(f"Module spec has no loader for {source}")
276
- spec.loader.exec_module(module)
277
- return module
278
- except Exception as e:
279
- raise ValueError(f"Failed to import module from file '{source}': {e}")
280
- else:
281
- try:
282
- return importlib.import_module(source)
283
- except ImportError as e:
284
- logger.error(f"Module '{source}' not found: {e}")
285
- raise ValueError(
286
- f"Failed to import module '{source}': {e}. "
287
- f"Ensure it is installed using 'pip install {source}' or check the module name."
288
- )
289
-
290
- def instantiate_workflow(self) -> Workflow:
291
- """Instantiate a Workflow object with full support for template_node and inputs_mapping."""
292
- self._ensure_dependencies()
293
-
294
- functions: Dict[str, Callable] = {}
295
- for func_name, func_def in self.workflow.functions.items():
296
- if func_def.type == "embedded":
297
- local_scope: Dict[str, Any] = {}
298
- if func_def.code is not None:
299
- exec(func_def.code, local_scope)
300
- if func_name not in local_scope:
301
- raise ValueError(f"Embedded function '{func_name}' not defined in code")
302
- functions[func_name] = local_scope[func_name]
303
- else:
304
- raise ValueError(f"Embedded function '{func_name}' has no code")
305
- elif func_def.type == "external":
306
- try:
307
- if func_def.module is None:
308
- raise ValueError(f"External function '{func_name}' has no module specified")
309
- module = self.import_module_from_source(func_def.module)
310
- if func_def.function is None:
311
- raise ValueError(f"External function '{func_name}' has no function name specified")
312
- functions[func_name] = getattr(module, func_def.function)
313
- except (ImportError, AttributeError) as e:
314
- raise ValueError(f"Failed to import external function '{func_name}': {e}")
315
-
316
- if not self.workflow.workflow.start:
317
- raise ValueError("Start node not set in workflow definition")
318
-
319
- start_node_name = str(self.workflow.workflow.start) if self.workflow.workflow.start else "start"
320
- if self.workflow.workflow.start is None:
321
- logger.warning("Start node was None, using 'start' as default")
322
-
323
- # Register all nodes with their node names
324
- for node_name, node_def in self.workflow.nodes.items():
325
- if node_def.function:
326
- if node_def.function not in functions:
327
- raise ValueError(f"Function '{node_def.function}' for node '{node_name}' not found")
328
- func = functions[node_def.function]
329
- Nodes.NODE_REGISTRY[node_name] = (
330
- Nodes.define(output=node_def.output)(func),
331
- ["user_name"], # Explicitly define inputs based on function signature
332
- node_def.output
333
- )
334
- elif node_def.llm_config:
335
- llm_config = node_def.llm_config
336
- input_vars = set(re.findall(r"{{\s*([^}]+?)\s*}}", llm_config.prompt_template)) if not llm_config.prompt_file else set()
337
- cleaned_inputs = set()
338
- for input_var in input_vars:
339
- base_var = re.split(r"\s*[\+\-\*/]\s*", input_var.strip())[0].strip()
340
- if base_var.isidentifier():
341
- cleaned_inputs.add(base_var)
342
- inputs_list: List[str] = list(cleaned_inputs)
343
-
344
- async def dummy_func(**kwargs):
345
- pass
346
-
347
- # Handle callable model if specified in inputs_mapping, else use default
348
- def model_callable(ctx):
349
- return llm_config.model # Default to string from schema
350
- if node_def.inputs_mapping and "model" in node_def.inputs_mapping:
351
- model_value = node_def.inputs_mapping["model"]
352
- if isinstance(model_value, str) and model_value.startswith("lambda ctx:"):
353
- try:
354
- model_callable = eval(model_value)
355
- except Exception as e:
356
- logger.warning(f"Failed to evaluate model lambda for {node_name}: {e}")
357
- def model_callable(ctx):
358
- return model_value
359
-
360
- if llm_config.response_model:
361
- response_model = self._resolve_model(llm_config.response_model)
362
- decorated_func = Nodes.structured_llm_node(
363
- model=model_callable,
364
- system_prompt=llm_config.system_prompt or "",
365
- system_prompt_file=llm_config.system_prompt_file,
366
- prompt_template=llm_config.prompt_template,
367
- prompt_file=llm_config.prompt_file,
368
- response_model=response_model,
369
- output=node_def.output or f"{node_name}_result",
370
- temperature=llm_config.temperature,
371
- max_tokens=llm_config.max_tokens or 2000,
372
- top_p=llm_config.top_p,
373
- presence_penalty=llm_config.presence_penalty,
374
- frequency_penalty=llm_config.frequency_penalty,
375
- api_key=llm_config.api_key,
376
- )(dummy_func)
377
- else:
378
- decorated_func = Nodes.llm_node(
379
- model=model_callable,
380
- system_prompt=llm_config.system_prompt or "",
381
- system_prompt_file=llm_config.system_prompt_file,
382
- prompt_template=llm_config.prompt_template,
383
- prompt_file=llm_config.prompt_file,
384
- output=node_def.output or f"{node_name}_result",
385
- temperature=llm_config.temperature,
386
- max_tokens=llm_config.max_tokens or 2000,
387
- top_p=llm_config.top_p,
388
- presence_penalty=llm_config.presence_penalty,
389
- frequency_penalty=llm_config.frequency_penalty,
390
- api_key=llm_config.api_key,
391
- )(dummy_func)
392
-
393
- Nodes.NODE_REGISTRY[node_name] = (decorated_func, inputs_list, node_def.output or f"{node_name}_result")
394
- elif node_def.template_config:
395
- template_config = node_def.template_config
396
- input_vars = set(re.findall(r"{{\s*([^}]+?)\s*}}", template_config.template)) if not template_config.template_file else set()
397
- cleaned_inputs = {var.strip() for var in input_vars if var.strip().isidentifier()}
398
- inputs_list = list(cleaned_inputs)
399
-
400
- async def dummy_template_func(rendered_content: str, **kwargs):
401
- return rendered_content
402
-
403
- decorated_func = Nodes.template_node(
404
- output=node_def.output or f"{node_name}_result",
405
- template=template_config.template,
406
- template_file=template_config.template_file,
407
- )(dummy_template_func)
408
-
409
- Nodes.NODE_REGISTRY[node_name] = (decorated_func, ["rendered_content"] + inputs_list, node_def.output or f"{node_name}_result")
410
-
411
- # Create the Workflow instance after all nodes are registered
412
- wf = Workflow(start_node=start_node_name)
413
-
414
- for observer_name in self.workflow.observers:
415
- if observer_name not in functions:
416
- raise ValueError(f"Observer '{observer_name}' not found in functions")
417
- wf.add_observer(functions[observer_name])
418
- logger.debug(f"Registered observer '{observer_name}' in workflow")
419
-
420
- sub_workflows: Dict[str, Workflow] = {}
421
- for node_name, node_def in self.workflow.nodes.items():
422
- inputs_mapping = {}
423
- if node_def.inputs_mapping:
424
- for key, value in node_def.inputs_mapping.items():
425
- if isinstance(value, str) and value.startswith("lambda ctx:"):
426
- try:
427
- inputs_mapping[key] = eval(value)
428
- except Exception as e:
429
- logger.warning(f"Failed to evaluate lambda for {key} in {node_name}: {e}")
430
- inputs_mapping[key] = value
431
- else:
432
- inputs_mapping[key] = value
433
-
434
- if node_def.sub_workflow:
435
- start_node = str(node_def.sub_workflow.start) if node_def.sub_workflow.start else f"{node_name}_start"
436
- if node_def.sub_workflow.start is None:
437
- logger.warning(f"Sub-workflow for node '{node_name}' has no start node, using '{start_node}'")
438
- sub_wf = Workflow(start_node=start_node)
439
- sub_workflows[node_name] = sub_wf
440
- added_sub_nodes = set()
441
- for trans in node_def.sub_workflow.transitions:
442
- from_node = trans.from_node
443
- if from_node not in added_sub_nodes:
444
- sub_wf.node(from_node)
445
- added_sub_nodes.add(from_node)
446
- if isinstance(trans.to_node, str):
447
- to_nodes = [trans.to_node]
448
- condition = eval(f"lambda ctx: {trans.condition}") if trans.condition else None
449
- if to_nodes[0] not in added_sub_nodes:
450
- sub_wf.node(to_nodes[0])
451
- added_sub_nodes.add(to_nodes[0])
452
- sub_wf.then(to_nodes[0], condition=condition)
453
- elif all(isinstance(tn, str) for tn in trans.to_node):
454
- to_nodes = trans.to_node
455
- for to_node in to_nodes:
456
- if to_node not in added_sub_nodes:
457
- sub_wf.node(to_node)
458
- added_sub_nodes.add(to_node)
459
- sub_wf.parallel(*to_nodes)
460
- else:
461
- branches = [(tn.to_node, eval(f"lambda ctx: {tn.condition}") if tn.condition else None)
462
- for tn in trans.to_node]
463
- for to_node, _ in branches:
464
- if to_node not in added_sub_nodes:
465
- sub_wf.node(to_node)
466
- added_sub_nodes.add(to_node)
467
- sub_wf.branch(branches)
468
- inputs = list(Nodes.NODE_REGISTRY[sub_wf.start_node][1])
469
- output = node_def.output if node_def.output is not None else f"{node_name}_result"
470
- wf.add_sub_workflow(node_name, sub_wf, inputs={k: k for k in inputs}, output=output)
471
- else:
472
- wf.node(node_name, inputs_mapping=inputs_mapping if inputs_mapping else None)
473
-
474
- added_nodes = set()
475
- for trans in self.workflow.workflow.transitions:
476
- from_node = trans.from_node
477
- if from_node not in added_nodes and from_node not in sub_workflows:
478
- wf.node(from_node)
479
- added_nodes.add(from_node)
480
- if isinstance(trans.to_node, str):
481
- to_nodes = [trans.to_node]
482
- condition = eval(f"lambda ctx: {trans.condition}") if trans.condition else None
483
- if to_nodes[0] not in added_nodes and to_nodes[0] not in sub_workflows:
484
- wf.node(to_nodes[0])
485
- added_nodes.add(to_nodes[0])
486
- wf.then(to_nodes[0], condition=condition)
487
- elif all(isinstance(tn, str) for tn in trans.to_node):
488
- to_nodes = trans.to_node
489
- for to_node in to_nodes:
490
- if to_node not in added_nodes and to_node not in sub_workflows:
491
- wf.node(to_node)
492
- added_nodes.add(to_node)
493
- wf.parallel(*to_nodes)
494
- else:
495
- branches = [(tn.to_node, eval(f"lambda ctx: {tn.condition}") if tn.condition else None)
496
- for tn in trans.to_node]
497
- for to_node, _ in branches:
498
- if to_node not in added_nodes and to_node not in sub_workflows:
499
- wf.node(to_node)
500
- added_nodes.add(to_node)
501
- wf.branch(branches)
502
-
503
- for conv_node in self.workflow.workflow.convergence_nodes:
504
- if conv_node not in added_nodes and conv_node not in sub_workflows:
505
- wf.node(conv_node)
506
- added_nodes.add(conv_node)
507
- wf.converge(conv_node)
508
-
509
- return wf
510
-
511
- def load_from_yaml(self, file_path: Union[str, Path]) -> None:
512
- """Load a workflow from a YAML file with validation."""
513
- file_path = Path(file_path)
514
- if not file_path.exists():
515
- raise FileNotFoundError(f"YAML file '{file_path}' not found")
516
- with file_path.open("r") as f:
517
- data = yaml.safe_load(f)
518
- try:
519
- self.workflow = WorkflowDefinition.model_validate(data)
520
- self._ensure_dependencies()
521
- except ValidationError as e:
522
- raise ValueError(f"Invalid workflow YAML: {e}")
523
-
524
- def save_to_yaml(self, file_path: Union[str, Path]) -> None:
525
- """Save the workflow to a YAML file using aliases and multi-line block scalars for code."""
526
- file_path = Path(file_path)
527
-
528
- def str_representer(dumper, data):
529
- if "\n" in data:
530
- return dumper.represent_scalar("tag:yaml.org,2002:str", data, style="|")
531
- return dumper.represent_scalar("tag:yaml.org,2002:str", data)
532
-
533
- yaml.add_representer(str, str_representer, Dumper=yaml.SafeDumper)
534
-
535
- with file_path.open("w") as f:
536
- yaml.safe_dump(
537
- self.workflow.model_dump(by_alias=True),
538
- f,
539
- default_flow_style=False,
540
- sort_keys=False,
541
- allow_unicode=True,
542
- width=120,
543
- )
544
-
545
-
546
- async def test_workflow():
547
- """Test the workflow execution."""
548
- manager = WorkflowManager()
549
- manager.workflow.dependencies = ["requests>=2.28.0"]
550
- manager.add_function(
551
- name="greet",
552
- type_="embedded",
553
- code="def greet(user_name): return f'Hello, {user_name}!'",
554
- )
555
- manager.add_function(
556
- name="check_condition",
557
- type_="embedded",
558
- code="def check_condition(user_name): return len(user_name) > 3",
559
- )
560
- manager.add_function(
561
- name="farewell",
562
- type_="embedded",
563
- code="def farewell(user_name): return f'Goodbye, {user_name}!'",
564
- )
565
- manager.add_function(
566
- name="monitor",
567
- type_="embedded",
568
- code="""async def monitor(event):
569
- print(f'[EVENT] {event.event_type.value} @ {event.node_name or "workflow"}')
570
- if event.result:
571
- print(f'Result: {event.result}')
572
- if event.exception:
573
- print(f'Error: {event.exception}')""",
574
- )
575
- manager.add_node(
576
- name="start",
577
- function="greet",
578
- inputs_mapping={"user_name": "name_input"},
579
- )
580
- manager.add_node(
581
- name="format_greeting",
582
- template_config={"template": "User: {{ user_name }} greeted on {{ date }}"},
583
- inputs_mapping={"user_name": "name_input", "date": "lambda ctx: '2025-03-06'"},
584
- )
585
- manager.add_node(
586
- name="branch_true",
587
- function="check_condition",
588
- inputs_mapping={"user_name": "name_input"},
589
- )
590
- manager.add_node(
591
- name="branch_false",
592
- function="check_condition",
593
- inputs_mapping={"user_name": "name_input"},
594
- )
595
- manager.add_node(
596
- name="end",
597
- function="farewell",
598
- inputs_mapping={"user_name": "name_input"},
599
- )
600
- manager.set_start_node("start")
601
- manager.add_transition(
602
- from_node="start",
603
- to_node="format_greeting"
604
- )
605
- manager.add_transition(
606
- from_node="format_greeting",
607
- to_node=[
608
- BranchCondition(to_node="branch_true", condition="ctx.get('user_name') == 'Alice'"),
609
- BranchCondition(to_node="branch_false", condition="ctx.get('user_name') != 'Alice'")
610
- ]
611
- )
612
- manager.add_convergence_node("end")
613
- manager.add_observer("monitor")
614
- manager.save_to_yaml("workflow.yaml")
615
-
616
- # Load and instantiate
617
- new_manager = WorkflowManager()
618
- new_manager.load_from_yaml("workflow.yaml")
619
- print("Workflow structure:")
620
- print(new_manager.workflow.model_dump())
621
-
622
- # Execute the workflow
623
- workflow = new_manager.instantiate_workflow()
624
- engine = workflow.build()
625
- initial_context = {"name_input": "Alice"}
626
- result = await engine.run(initial_context)
627
- print("\nExecution result:")
628
- print(result)
629
-
630
-
631
- def main():
632
- """Run the workflow test."""
633
- asyncio.run(test_workflow())
634
-
635
-
636
- if __name__ == "__main__":
637
- main()