quantalogic 0.80__py3-none-any.whl → 0.93__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.
- quantalogic/flow/__init__.py +16 -34
 - quantalogic/main.py +11 -6
 - quantalogic/tools/tool.py +8 -922
 - quantalogic-0.93.dist-info/METADATA +475 -0
 - {quantalogic-0.80.dist-info → quantalogic-0.93.dist-info}/RECORD +8 -54
 - quantalogic/codeact/TODO.md +0 -14
 - quantalogic/codeact/__init__.py +0 -0
 - quantalogic/codeact/agent.py +0 -478
 - quantalogic/codeact/cli.py +0 -50
 - quantalogic/codeact/cli_commands/__init__.py +0 -0
 - quantalogic/codeact/cli_commands/create_toolbox.py +0 -45
 - quantalogic/codeact/cli_commands/install_toolbox.py +0 -20
 - quantalogic/codeact/cli_commands/list_executor.py +0 -15
 - quantalogic/codeact/cli_commands/list_reasoners.py +0 -15
 - quantalogic/codeact/cli_commands/list_toolboxes.py +0 -47
 - quantalogic/codeact/cli_commands/task.py +0 -215
 - quantalogic/codeact/cli_commands/tool_info.py +0 -24
 - quantalogic/codeact/cli_commands/uninstall_toolbox.py +0 -43
 - quantalogic/codeact/config.yaml +0 -21
 - quantalogic/codeact/constants.py +0 -9
 - quantalogic/codeact/events.py +0 -85
 - quantalogic/codeact/examples/README.md +0 -342
 - quantalogic/codeact/examples/agent_sample.yaml +0 -29
 - quantalogic/codeact/executor.py +0 -186
 - quantalogic/codeact/history_manager.py +0 -94
 - quantalogic/codeact/llm_util.py +0 -57
 - quantalogic/codeact/plugin_manager.py +0 -92
 - quantalogic/codeact/prompts/error_format.j2 +0 -11
 - quantalogic/codeact/prompts/generate_action.j2 +0 -77
 - quantalogic/codeact/prompts/generate_program.j2 +0 -52
 - quantalogic/codeact/prompts/response_format.j2 +0 -11
 - quantalogic/codeact/react_agent.py +0 -318
 - quantalogic/codeact/reasoner.py +0 -185
 - quantalogic/codeact/templates/toolbox/README.md.j2 +0 -10
 - quantalogic/codeact/templates/toolbox/pyproject.toml.j2 +0 -16
 - quantalogic/codeact/templates/toolbox/tools.py.j2 +0 -6
 - quantalogic/codeact/templates.py +0 -7
 - quantalogic/codeact/tools_manager.py +0 -258
 - quantalogic/codeact/utils.py +0 -62
 - quantalogic/codeact/xml_utils.py +0 -126
 - quantalogic/flow/flow.py +0 -1070
 - quantalogic/flow/flow_extractor.py +0 -783
 - quantalogic/flow/flow_generator.py +0 -322
 - quantalogic/flow/flow_manager.py +0 -676
 - quantalogic/flow/flow_manager_schema.py +0 -287
 - quantalogic/flow/flow_mermaid.py +0 -365
 - quantalogic/flow/flow_validator.py +0 -479
 - quantalogic/flow/flow_yaml.linkedin.md +0 -31
 - quantalogic/flow/flow_yaml.md +0 -767
 - quantalogic/flow/templates/prompt_check_inventory.j2 +0 -1
 - quantalogic/flow/templates/system_check_inventory.j2 +0 -1
 - quantalogic-0.80.dist-info/METADATA +0 -900
 - {quantalogic-0.80.dist-info → quantalogic-0.93.dist-info}/LICENSE +0 -0
 - {quantalogic-0.80.dist-info → quantalogic-0.93.dist-info}/WHEEL +0 -0
 - {quantalogic-0.80.dist-info → quantalogic-0.93.dist-info}/entry_points.txt +0 -0
 
    
        quantalogic/flow/flow.py
    DELETED
    
    | 
         @@ -1,1070 +0,0 @@ 
     | 
|
| 
       1 
     | 
    
         
            -
            #!/usr/bin/env -S uv run
         
     | 
| 
       2 
     | 
    
         
            -
            # /// script
         
     | 
| 
       3 
     | 
    
         
            -
            # requires-python = ">=3.12"
         
     | 
| 
       4 
     | 
    
         
            -
            # dependencies = [
         
     | 
| 
       5 
     | 
    
         
            -
            #     "loguru>=0.7.2",              # Logging utility
         
     | 
| 
       6 
     | 
    
         
            -
            #     "litellm>=1.0.0",             # LLM integration
         
     | 
| 
       7 
     | 
    
         
            -
            #     "pydantic>=2.0.0",            # Data validation and settings
         
     | 
| 
       8 
     | 
    
         
            -
            #     "anyio>=4.0.0",               # Async utilities
         
     | 
| 
       9 
     | 
    
         
            -
            #     "jinja2>=3.1.0",              # Templating engine
         
     | 
| 
       10 
     | 
    
         
            -
            #     "instructor"  # Structured LLM output with litellm integration
         
     | 
| 
       11 
     | 
    
         
            -
            # ]
         
     | 
| 
       12 
     | 
    
         
            -
            # ///
         
     | 
| 
       13 
     | 
    
         
            -
             
     | 
| 
       14 
     | 
    
         
            -
            import asyncio
         
     | 
| 
       15 
     | 
    
         
            -
            import inspect
         
     | 
| 
       16 
     | 
    
         
            -
            import os
         
     | 
| 
       17 
     | 
    
         
            -
            from dataclasses import dataclass
         
     | 
| 
       18 
     | 
    
         
            -
            from enum import Enum
         
     | 
| 
       19 
     | 
    
         
            -
            from pathlib import Path
         
     | 
| 
       20 
     | 
    
         
            -
            from typing import Any, Callable, Dict, List, Optional, Tuple, Type, Union
         
     | 
| 
       21 
     | 
    
         
            -
             
     | 
| 
       22 
     | 
    
         
            -
            import instructor
         
     | 
| 
       23 
     | 
    
         
            -
            from jinja2 import Environment, FileSystemLoader, Template, TemplateNotFound
         
     | 
| 
       24 
     | 
    
         
            -
            from litellm import acompletion
         
     | 
| 
       25 
     | 
    
         
            -
            from loguru import logger
         
     | 
| 
       26 
     | 
    
         
            -
            from pydantic import BaseModel, ValidationError
         
     | 
| 
       27 
     | 
    
         
            -
             
     | 
| 
       28 
     | 
    
         
            -
             
     | 
| 
       29 
     | 
    
         
            -
            # Define event types and structure for observer system
         
     | 
| 
       30 
     | 
    
         
            -
            class WorkflowEventType(Enum):
         
     | 
| 
       31 
     | 
    
         
            -
                NODE_STARTED = "node_started"
         
     | 
| 
       32 
     | 
    
         
            -
                NODE_COMPLETED = "node_completed"
         
     | 
| 
       33 
     | 
    
         
            -
                NODE_FAILED = "node_failed"
         
     | 
| 
       34 
     | 
    
         
            -
                TRANSITION_EVALUATED = "transition_evaluated"
         
     | 
| 
       35 
     | 
    
         
            -
                WORKFLOW_STARTED = "workflow_started"
         
     | 
| 
       36 
     | 
    
         
            -
                WORKFLOW_COMPLETED = "workflow_completed"
         
     | 
| 
       37 
     | 
    
         
            -
                SUB_WORKFLOW_ENTERED = "sub_workflow_entered"
         
     | 
| 
       38 
     | 
    
         
            -
                SUB_WORKFLOW_EXITED = "sub_workflow_exited"
         
     | 
| 
       39 
     | 
    
         
            -
             
     | 
| 
       40 
     | 
    
         
            -
             
     | 
| 
       41 
     | 
    
         
            -
            @dataclass
         
     | 
| 
       42 
     | 
    
         
            -
            class WorkflowEvent:
         
     | 
| 
       43 
     | 
    
         
            -
                event_type: WorkflowEventType
         
     | 
| 
       44 
     | 
    
         
            -
                node_name: Optional[str]
         
     | 
| 
       45 
     | 
    
         
            -
                context: Dict[str, Any]
         
     | 
| 
       46 
     | 
    
         
            -
                result: Optional[Any] = None
         
     | 
| 
       47 
     | 
    
         
            -
                exception: Optional[Exception] = None
         
     | 
| 
       48 
     | 
    
         
            -
                transition_from: Optional[str] = None
         
     | 
| 
       49 
     | 
    
         
            -
                transition_to: Optional[str] = None
         
     | 
| 
       50 
     | 
    
         
            -
                sub_workflow_name: Optional[str] = None
         
     | 
| 
       51 
     | 
    
         
            -
                usage: Optional[Dict[str, Any]] = None
         
     | 
| 
       52 
     | 
    
         
            -
             
     | 
| 
       53 
     | 
    
         
            -
             
     | 
| 
       54 
     | 
    
         
            -
            WorkflowObserver = Callable[[WorkflowEvent], None]
         
     | 
| 
       55 
     | 
    
         
            -
             
     | 
| 
       56 
     | 
    
         
            -
             
     | 
| 
       57 
     | 
    
         
            -
            class SubWorkflowNode:
         
     | 
| 
       58 
     | 
    
         
            -
                def __init__(self, sub_workflow: "Workflow", inputs: Dict[str, Any], output: str):
         
     | 
| 
       59 
     | 
    
         
            -
                    """Initialize a sub-workflow node with flexible inputs mapping."""
         
     | 
| 
       60 
     | 
    
         
            -
                    self.sub_workflow = sub_workflow
         
     | 
| 
       61 
     | 
    
         
            -
                    self.inputs = inputs
         
     | 
| 
       62 
     | 
    
         
            -
                    self.output = output
         
     | 
| 
       63 
     | 
    
         
            -
             
     | 
| 
       64 
     | 
    
         
            -
                async def __call__(self, engine: "WorkflowEngine"):
         
     | 
| 
       65 
     | 
    
         
            -
                    """Execute the sub-workflow with the engine's context using inputs mapping."""
         
     | 
| 
       66 
     | 
    
         
            -
                    sub_context = {}
         
     | 
| 
       67 
     | 
    
         
            -
                    for sub_key, mapping in self.inputs.items():
         
     | 
| 
       68 
     | 
    
         
            -
                        if callable(mapping):
         
     | 
| 
       69 
     | 
    
         
            -
                            sub_context[sub_key] = mapping(engine.context)
         
     | 
| 
       70 
     | 
    
         
            -
                        elif isinstance(mapping, str):
         
     | 
| 
       71 
     | 
    
         
            -
                            sub_context[sub_key] = engine.context.get(mapping)
         
     | 
| 
       72 
     | 
    
         
            -
                        else:
         
     | 
| 
       73 
     | 
    
         
            -
                            sub_context[sub_key] = mapping
         
     | 
| 
       74 
     | 
    
         
            -
                    sub_engine = self.sub_workflow.build(parent_engine=engine)
         
     | 
| 
       75 
     | 
    
         
            -
                    result = await sub_engine.run(sub_context)
         
     | 
| 
       76 
     | 
    
         
            -
                    return result.get(self.output)
         
     | 
| 
       77 
     | 
    
         
            -
             
     | 
| 
       78 
     | 
    
         
            -
             
     | 
| 
       79 
     | 
    
         
            -
            class WorkflowEngine:
         
     | 
| 
       80 
     | 
    
         
            -
                def __init__(self, workflow, parent_engine: Optional["WorkflowEngine"] = None):
         
     | 
| 
       81 
     | 
    
         
            -
                    """Initialize the WorkflowEngine with a workflow and optional parent for sub-workflows."""
         
     | 
| 
       82 
     | 
    
         
            -
                    self.workflow = workflow
         
     | 
| 
       83 
     | 
    
         
            -
                    self.context: Dict[str, Any] = {}
         
     | 
| 
       84 
     | 
    
         
            -
                    self.observers: List[WorkflowObserver] = []
         
     | 
| 
       85 
     | 
    
         
            -
                    self.parent_engine = parent_engine
         
     | 
| 
       86 
     | 
    
         
            -
             
     | 
| 
       87 
     | 
    
         
            -
                def add_observer(self, observer: WorkflowObserver) -> None:
         
     | 
| 
       88 
     | 
    
         
            -
                    """Register an event observer callback."""
         
     | 
| 
       89 
     | 
    
         
            -
                    if observer not in self.observers:
         
     | 
| 
       90 
     | 
    
         
            -
                        self.observers.append(observer)
         
     | 
| 
       91 
     | 
    
         
            -
                        logger.debug(f"Added observer: {observer}")
         
     | 
| 
       92 
     | 
    
         
            -
                    if self.parent_engine:
         
     | 
| 
       93 
     | 
    
         
            -
                        self.parent_engine.add_observer(observer)
         
     | 
| 
       94 
     | 
    
         
            -
             
     | 
| 
       95 
     | 
    
         
            -
                def remove_observer(self, observer: WorkflowObserver) -> None:
         
     | 
| 
       96 
     | 
    
         
            -
                    """Remove an event observer callback."""
         
     | 
| 
       97 
     | 
    
         
            -
                    if observer in self.observers:
         
     | 
| 
       98 
     | 
    
         
            -
                        self.observers.remove(observer)
         
     | 
| 
       99 
     | 
    
         
            -
                        logger.debug(f"Removed observer: {observer}")
         
     | 
| 
       100 
     | 
    
         
            -
             
     | 
| 
       101 
     | 
    
         
            -
                async def _notify_observers(self, event: WorkflowEvent) -> None:
         
     | 
| 
       102 
     | 
    
         
            -
                    """Asynchronously notify all observers of an event."""
         
     | 
| 
       103 
     | 
    
         
            -
                    tasks = []
         
     | 
| 
       104 
     | 
    
         
            -
                    for observer in self.observers:
         
     | 
| 
       105 
     | 
    
         
            -
                        try:
         
     | 
| 
       106 
     | 
    
         
            -
                            if asyncio.iscoroutinefunction(observer):
         
     | 
| 
       107 
     | 
    
         
            -
                                tasks.append(observer(event))
         
     | 
| 
       108 
     | 
    
         
            -
                            else:
         
     | 
| 
       109 
     | 
    
         
            -
                                observer(event)
         
     | 
| 
       110 
     | 
    
         
            -
                        except Exception as e:
         
     | 
| 
       111 
     | 
    
         
            -
                            logger.error(f"Observer {observer} failed for {event.event_type.value}: {e}")
         
     | 
| 
       112 
     | 
    
         
            -
                    if tasks:
         
     | 
| 
       113 
     | 
    
         
            -
                        await asyncio.gather(*tasks)
         
     | 
| 
       114 
     | 
    
         
            -
             
     | 
| 
       115 
     | 
    
         
            -
                async def run(self, initial_context: Dict[str, Any]) -> Dict[str, Any]:
         
     | 
| 
       116 
     | 
    
         
            -
                    """Execute the workflow starting from the entry node with event notifications."""
         
     | 
| 
       117 
     | 
    
         
            -
                    self.context = initial_context.copy()
         
     | 
| 
       118 
     | 
    
         
            -
                    await self._notify_observers(
         
     | 
| 
       119 
     | 
    
         
            -
                        WorkflowEvent(event_type=WorkflowEventType.WORKFLOW_STARTED, node_name=None, context=self.context)
         
     | 
| 
       120 
     | 
    
         
            -
                    )
         
     | 
| 
       121 
     | 
    
         
            -
             
     | 
| 
       122 
     | 
    
         
            -
                    current_node = self.workflow.start_node
         
     | 
| 
       123 
     | 
    
         
            -
                    while current_node:
         
     | 
| 
       124 
     | 
    
         
            -
                        logger.info(f"Executing node: {current_node}")
         
     | 
| 
       125 
     | 
    
         
            -
                        await self._notify_observers(
         
     | 
| 
       126 
     | 
    
         
            -
                            WorkflowEvent(event_type=WorkflowEventType.NODE_STARTED, node_name=current_node, context=self.context)
         
     | 
| 
       127 
     | 
    
         
            -
                        )
         
     | 
| 
       128 
     | 
    
         
            -
             
     | 
| 
       129 
     | 
    
         
            -
                        node_func = self.workflow.nodes.get(current_node)
         
     | 
| 
       130 
     | 
    
         
            -
                        if not node_func:
         
     | 
| 
       131 
     | 
    
         
            -
                            logger.error(f"Node {current_node} not found")
         
     | 
| 
       132 
     | 
    
         
            -
                            exc = ValueError(f"Node {current_node} not found")
         
     | 
| 
       133 
     | 
    
         
            -
                            await self._notify_observers(
         
     | 
| 
       134 
     | 
    
         
            -
                                WorkflowEvent(
         
     | 
| 
       135 
     | 
    
         
            -
                                    event_type=WorkflowEventType.NODE_FAILED,
         
     | 
| 
       136 
     | 
    
         
            -
                                    node_name=current_node,
         
     | 
| 
       137 
     | 
    
         
            -
                                    context=self.context,
         
     | 
| 
       138 
     | 
    
         
            -
                                    exception=exc,
         
     | 
| 
       139 
     | 
    
         
            -
                                )
         
     | 
| 
       140 
     | 
    
         
            -
                            )
         
     | 
| 
       141 
     | 
    
         
            -
                            break
         
     | 
| 
       142 
     | 
    
         
            -
             
     | 
| 
       143 
     | 
    
         
            -
                        input_mappings = self.workflow.node_input_mappings.get(current_node, {})
         
     | 
| 
       144 
     | 
    
         
            -
                        inputs = {}
         
     | 
| 
       145 
     | 
    
         
            -
                        for key, mapping in input_mappings.items():
         
     | 
| 
       146 
     | 
    
         
            -
                            if callable(mapping):
         
     | 
| 
       147 
     | 
    
         
            -
                                inputs[key] = mapping(self.context)
         
     | 
| 
       148 
     | 
    
         
            -
                            elif isinstance(mapping, str):
         
     | 
| 
       149 
     | 
    
         
            -
                                inputs[key] = self.context.get(mapping)
         
     | 
| 
       150 
     | 
    
         
            -
                            else:
         
     | 
| 
       151 
     | 
    
         
            -
                                inputs[key] = mapping
         
     | 
| 
       152 
     | 
    
         
            -
                        for param in self.workflow.node_inputs[current_node]:
         
     | 
| 
       153 
     | 
    
         
            -
                            if param not in inputs:
         
     | 
| 
       154 
     | 
    
         
            -
                                inputs[param] = self.context.get(param)
         
     | 
| 
       155 
     | 
    
         
            -
             
     | 
| 
       156 
     | 
    
         
            -
                        result = None
         
     | 
| 
       157 
     | 
    
         
            -
                        exception = None
         
     | 
| 
       158 
     | 
    
         
            -
             
     | 
| 
       159 
     | 
    
         
            -
                        if isinstance(node_func, SubWorkflowNode):
         
     | 
| 
       160 
     | 
    
         
            -
                            await self._notify_observers(
         
     | 
| 
       161 
     | 
    
         
            -
                                WorkflowEvent(
         
     | 
| 
       162 
     | 
    
         
            -
                                    event_type=WorkflowEventType.SUB_WORKFLOW_ENTERED,
         
     | 
| 
       163 
     | 
    
         
            -
                                    node_name=current_node,
         
     | 
| 
       164 
     | 
    
         
            -
                                    context=self.context,
         
     | 
| 
       165 
     | 
    
         
            -
                                    sub_workflow_name=current_node,
         
     | 
| 
       166 
     | 
    
         
            -
                                )
         
     | 
| 
       167 
     | 
    
         
            -
                            )
         
     | 
| 
       168 
     | 
    
         
            -
             
     | 
| 
       169 
     | 
    
         
            -
                        try:
         
     | 
| 
       170 
     | 
    
         
            -
                            if isinstance(node_func, SubWorkflowNode):
         
     | 
| 
       171 
     | 
    
         
            -
                                result = await node_func(self)
         
     | 
| 
       172 
     | 
    
         
            -
                                usage = None
         
     | 
| 
       173 
     | 
    
         
            -
                            else:
         
     | 
| 
       174 
     | 
    
         
            -
                                result = await node_func(**inputs)
         
     | 
| 
       175 
     | 
    
         
            -
                                usage = getattr(node_func, "usage", None)
         
     | 
| 
       176 
     | 
    
         
            -
                            output_key = self.workflow.node_outputs[current_node]
         
     | 
| 
       177 
     | 
    
         
            -
                            if output_key:
         
     | 
| 
       178 
     | 
    
         
            -
                                self.context[output_key] = result
         
     | 
| 
       179 
     | 
    
         
            -
                            elif isinstance(result, dict):
         
     | 
| 
       180 
     | 
    
         
            -
                                self.context.update(result)
         
     | 
| 
       181 
     | 
    
         
            -
                                logger.debug(f"Updated context with {result} from node {current_node}")
         
     | 
| 
       182 
     | 
    
         
            -
                            await self._notify_observers(
         
     | 
| 
       183 
     | 
    
         
            -
                                WorkflowEvent(
         
     | 
| 
       184 
     | 
    
         
            -
                                    event_type=WorkflowEventType.NODE_COMPLETED,
         
     | 
| 
       185 
     | 
    
         
            -
                                    node_name=current_node,
         
     | 
| 
       186 
     | 
    
         
            -
                                    context=self.context,
         
     | 
| 
       187 
     | 
    
         
            -
                                    result=result,
         
     | 
| 
       188 
     | 
    
         
            -
                                    usage=usage,
         
     | 
| 
       189 
     | 
    
         
            -
                                )
         
     | 
| 
       190 
     | 
    
         
            -
                            )
         
     | 
| 
       191 
     | 
    
         
            -
                        except Exception as e:
         
     | 
| 
       192 
     | 
    
         
            -
                            logger.error(f"Error executing node {current_node}: {e}")
         
     | 
| 
       193 
     | 
    
         
            -
                            exception = e
         
     | 
| 
       194 
     | 
    
         
            -
                            await self._notify_observers(
         
     | 
| 
       195 
     | 
    
         
            -
                                WorkflowEvent(
         
     | 
| 
       196 
     | 
    
         
            -
                                    event_type=WorkflowEventType.NODE_FAILED,
         
     | 
| 
       197 
     | 
    
         
            -
                                    node_name=current_node,
         
     | 
| 
       198 
     | 
    
         
            -
                                    context=self.context,
         
     | 
| 
       199 
     | 
    
         
            -
                                    exception=e,
         
     | 
| 
       200 
     | 
    
         
            -
                                )
         
     | 
| 
       201 
     | 
    
         
            -
                            )
         
     | 
| 
       202 
     | 
    
         
            -
                            raise
         
     | 
| 
       203 
     | 
    
         
            -
                        finally:
         
     | 
| 
       204 
     | 
    
         
            -
                            if isinstance(node_func, SubWorkflowNode):
         
     | 
| 
       205 
     | 
    
         
            -
                                await self._notify_observers(
         
     | 
| 
       206 
     | 
    
         
            -
                                    WorkflowEvent(
         
     | 
| 
       207 
     | 
    
         
            -
                                        event_type=WorkflowEventType.SUB_WORKFLOW_EXITED,
         
     | 
| 
       208 
     | 
    
         
            -
                                        node_name=current_node,
         
     | 
| 
       209 
     | 
    
         
            -
                                        context=self.context,
         
     | 
| 
       210 
     | 
    
         
            -
                                        sub_workflow_name=current_node,
         
     | 
| 
       211 
     | 
    
         
            -
                                        result=result,
         
     | 
| 
       212 
     | 
    
         
            -
                                        exception=exception,
         
     | 
| 
       213 
     | 
    
         
            -
                                    )
         
     | 
| 
       214 
     | 
    
         
            -
                                )
         
     | 
| 
       215 
     | 
    
         
            -
             
     | 
| 
       216 
     | 
    
         
            -
                        next_nodes = self.workflow.transitions.get(current_node, [])
         
     | 
| 
       217 
     | 
    
         
            -
                        current_node = None
         
     | 
| 
       218 
     | 
    
         
            -
                        for next_node, condition in next_nodes:
         
     | 
| 
       219 
     | 
    
         
            -
                            await self._notify_observers(
         
     | 
| 
       220 
     | 
    
         
            -
                                WorkflowEvent(
         
     | 
| 
       221 
     | 
    
         
            -
                                    event_type=WorkflowEventType.TRANSITION_EVALUATED,
         
     | 
| 
       222 
     | 
    
         
            -
                                    node_name=None,
         
     | 
| 
       223 
     | 
    
         
            -
                                    context=self.context,
         
     | 
| 
       224 
     | 
    
         
            -
                                    transition_from=current_node,
         
     | 
| 
       225 
     | 
    
         
            -
                                    transition_to=next_node,
         
     | 
| 
       226 
     | 
    
         
            -
                                )
         
     | 
| 
       227 
     | 
    
         
            -
                            )
         
     | 
| 
       228 
     | 
    
         
            -
                            if condition is None or condition(self.context):
         
     | 
| 
       229 
     | 
    
         
            -
                                current_node = next_node
         
     | 
| 
       230 
     | 
    
         
            -
                                break
         
     | 
| 
       231 
     | 
    
         
            -
             
     | 
| 
       232 
     | 
    
         
            -
                    logger.info("Workflow execution completed")
         
     | 
| 
       233 
     | 
    
         
            -
                    await self._notify_observers(
         
     | 
| 
       234 
     | 
    
         
            -
                        WorkflowEvent(event_type=WorkflowEventType.WORKFLOW_COMPLETED, node_name=None, context=self.context)
         
     | 
| 
       235 
     | 
    
         
            -
                    )
         
     | 
| 
       236 
     | 
    
         
            -
                    return self.context
         
     | 
| 
       237 
     | 
    
         
            -
             
     | 
| 
       238 
     | 
    
         
            -
             
     | 
| 
       239 
     | 
    
         
            -
            class Workflow:
         
     | 
| 
       240 
     | 
    
         
            -
                def __init__(self, start_node: str):
         
     | 
| 
       241 
     | 
    
         
            -
                    """Initialize a workflow with a starting node.
         
     | 
| 
       242 
     | 
    
         
            -
             
     | 
| 
       243 
     | 
    
         
            -
                    Args:
         
     | 
| 
       244 
     | 
    
         
            -
                        start_node: The name of the initial node in the workflow.
         
     | 
| 
       245 
     | 
    
         
            -
                    """
         
     | 
| 
       246 
     | 
    
         
            -
                    self.start_node = start_node
         
     | 
| 
       247 
     | 
    
         
            -
                    self.nodes: Dict[str, Callable] = {}
         
     | 
| 
       248 
     | 
    
         
            -
                    self.node_inputs: Dict[str, List[str]] = {}
         
     | 
| 
       249 
     | 
    
         
            -
                    self.node_outputs: Dict[str, Optional[str]] = {}
         
     | 
| 
       250 
     | 
    
         
            -
                    self.transitions: Dict[str, List[Tuple[str, Optional[Callable]]]] = {}
         
     | 
| 
       251 
     | 
    
         
            -
                    self.node_input_mappings: Dict[str, Dict[str, Any]] = {}
         
     | 
| 
       252 
     | 
    
         
            -
                    self.current_node = None
         
     | 
| 
       253 
     | 
    
         
            -
                    self._observers: List[WorkflowObserver] = []
         
     | 
| 
       254 
     | 
    
         
            -
                    self._register_node(start_node)
         
     | 
| 
       255 
     | 
    
         
            -
                    self.current_node = start_node
         
     | 
| 
       256 
     | 
    
         
            -
                    # Loop-specific attributes
         
     | 
| 
       257 
     | 
    
         
            -
                    self.in_loop = False
         
     | 
| 
       258 
     | 
    
         
            -
                    self.loop_nodes = []
         
     | 
| 
       259 
     | 
    
         
            -
                    self.loop_entry_node = None
         
     | 
| 
       260 
     | 
    
         
            -
             
     | 
| 
       261 
     | 
    
         
            -
                def _register_node(self, name: str):
         
     | 
| 
       262 
     | 
    
         
            -
                    """Register a node without modifying the current node."""
         
     | 
| 
       263 
     | 
    
         
            -
                    if name not in Nodes.NODE_REGISTRY:
         
     | 
| 
       264 
     | 
    
         
            -
                        raise ValueError(f"Node {name} not registered")
         
     | 
| 
       265 
     | 
    
         
            -
                    func, inputs, output = Nodes.NODE_REGISTRY[name]
         
     | 
| 
       266 
     | 
    
         
            -
                    self.nodes[name] = func
         
     | 
| 
       267 
     | 
    
         
            -
                    self.node_inputs[name] = inputs
         
     | 
| 
       268 
     | 
    
         
            -
                    self.node_outputs[name] = output
         
     | 
| 
       269 
     | 
    
         
            -
             
     | 
| 
       270 
     | 
    
         
            -
                def node(self, name: str, inputs_mapping: Optional[Dict[str, Any]] = None):
         
     | 
| 
       271 
     | 
    
         
            -
                    """Add a node to the workflow chain with an optional inputs mapping.
         
     | 
| 
       272 
     | 
    
         
            -
             
     | 
| 
       273 
     | 
    
         
            -
                    Args:
         
     | 
| 
       274 
     | 
    
         
            -
                        name: The name of the node to add.
         
     | 
| 
       275 
     | 
    
         
            -
                        inputs_mapping: Optional dictionary mapping node inputs to context keys or callables.
         
     | 
| 
       276 
     | 
    
         
            -
             
     | 
| 
       277 
     | 
    
         
            -
                    Returns:
         
     | 
| 
       278 
     | 
    
         
            -
                        Self for method chaining.
         
     | 
| 
       279 
     | 
    
         
            -
                    """
         
     | 
| 
       280 
     | 
    
         
            -
                    self._register_node(name)
         
     | 
| 
       281 
     | 
    
         
            -
                    if self.in_loop:
         
     | 
| 
       282 
     | 
    
         
            -
                        self.loop_nodes.append(name)
         
     | 
| 
       283 
     | 
    
         
            -
                    if inputs_mapping:
         
     | 
| 
       284 
     | 
    
         
            -
                        self.node_input_mappings[name] = inputs_mapping
         
     | 
| 
       285 
     | 
    
         
            -
                        logger.debug(f"Added inputs mapping for node {name}: {inputs_mapping}")
         
     | 
| 
       286 
     | 
    
         
            -
                    self.current_node = name
         
     | 
| 
       287 
     | 
    
         
            -
                    return self
         
     | 
| 
       288 
     | 
    
         
            -
             
     | 
| 
       289 
     | 
    
         
            -
                def sequence(self, *nodes: str):
         
     | 
| 
       290 
     | 
    
         
            -
                    """Add a sequence of nodes to execute in order.
         
     | 
| 
       291 
     | 
    
         
            -
             
     | 
| 
       292 
     | 
    
         
            -
                    Args:
         
     | 
| 
       293 
     | 
    
         
            -
                        *nodes: Variable number of node names to execute sequentially.
         
     | 
| 
       294 
     | 
    
         
            -
             
     | 
| 
       295 
     | 
    
         
            -
                    Returns:
         
     | 
| 
       296 
     | 
    
         
            -
                        Self for method chaining.
         
     | 
| 
       297 
     | 
    
         
            -
                    """
         
     | 
| 
       298 
     | 
    
         
            -
                    if not nodes:
         
     | 
| 
       299 
     | 
    
         
            -
                        return self
         
     | 
| 
       300 
     | 
    
         
            -
                    for node in nodes:
         
     | 
| 
       301 
     | 
    
         
            -
                        if node not in Nodes.NODE_REGISTRY:
         
     | 
| 
       302 
     | 
    
         
            -
                            raise ValueError(f"Node {node} not registered")
         
     | 
| 
       303 
     | 
    
         
            -
                        func, inputs, output = Nodes.NODE_REGISTRY[node]
         
     | 
| 
       304 
     | 
    
         
            -
                        self.nodes[node] = func
         
     | 
| 
       305 
     | 
    
         
            -
                        self.node_inputs[node] = inputs
         
     | 
| 
       306 
     | 
    
         
            -
                        self.node_outputs[node] = output
         
     | 
| 
       307 
     | 
    
         
            -
                    for i in range(len(nodes) - 1):
         
     | 
| 
       308 
     | 
    
         
            -
                        self.transitions.setdefault(nodes[i], []).append((nodes[i + 1], None))
         
     | 
| 
       309 
     | 
    
         
            -
                    self.current_node = nodes[-1]
         
     | 
| 
       310 
     | 
    
         
            -
                    return self
         
     | 
| 
       311 
     | 
    
         
            -
             
     | 
| 
       312 
     | 
    
         
            -
                def then(self, next_node: str, condition: Optional[Callable] = None):
         
     | 
| 
       313 
     | 
    
         
            -
                    """Add a transition to the next node with an optional condition.
         
     | 
| 
       314 
     | 
    
         
            -
             
     | 
| 
       315 
     | 
    
         
            -
                    Args:
         
     | 
| 
       316 
     | 
    
         
            -
                        next_node: Name of the node to transition to.
         
     | 
| 
       317 
     | 
    
         
            -
                        condition: Optional callable taking context and returning a boolean.
         
     | 
| 
       318 
     | 
    
         
            -
             
     | 
| 
       319 
     | 
    
         
            -
                    Returns:
         
     | 
| 
       320 
     | 
    
         
            -
                        Self for method chaining.
         
     | 
| 
       321 
     | 
    
         
            -
                    """
         
     | 
| 
       322 
     | 
    
         
            -
                    if next_node not in self.nodes:
         
     | 
| 
       323 
     | 
    
         
            -
                        self._register_node(next_node)
         
     | 
| 
       324 
     | 
    
         
            -
                    if self.current_node:
         
     | 
| 
       325 
     | 
    
         
            -
                        self.transitions.setdefault(self.current_node, []).append((next_node, condition))
         
     | 
| 
       326 
     | 
    
         
            -
                        logger.debug(f"Added transition from {self.current_node} to {next_node} with condition {condition}")
         
     | 
| 
       327 
     | 
    
         
            -
                    else:
         
     | 
| 
       328 
     | 
    
         
            -
                        logger.warning("No current node set for transition")
         
     | 
| 
       329 
     | 
    
         
            -
                    self.current_node = next_node
         
     | 
| 
       330 
     | 
    
         
            -
                    return self
         
     | 
| 
       331 
     | 
    
         
            -
             
     | 
| 
       332 
     | 
    
         
            -
                def branch(
         
     | 
| 
       333 
     | 
    
         
            -
                    self,
         
     | 
| 
       334 
     | 
    
         
            -
                    branches: List[Tuple[str, Optional[Callable]]],
         
     | 
| 
       335 
     | 
    
         
            -
                    default: Optional[str] = None,
         
     | 
| 
       336 
     | 
    
         
            -
                    next_node: Optional[str] = None,
         
     | 
| 
       337 
     | 
    
         
            -
                ) -> "Workflow":
         
     | 
| 
       338 
     | 
    
         
            -
                    """Add multiple conditional branches from the current node with an optional default and next node.
         
     | 
| 
       339 
     | 
    
         
            -
             
     | 
| 
       340 
     | 
    
         
            -
                    Args:
         
     | 
| 
       341 
     | 
    
         
            -
                        branches: List of tuples (next_node, condition), where condition takes context and returns a boolean.
         
     | 
| 
       342 
     | 
    
         
            -
                        default: Optional node to transition to if no branch conditions are met.
         
     | 
| 
       343 
     | 
    
         
            -
                        next_node: Optional node to set as current_node after branching (e.g., for convergence).
         
     | 
| 
       344 
     | 
    
         
            -
             
     | 
| 
       345 
     | 
    
         
            -
                    Returns:
         
     | 
| 
       346 
     | 
    
         
            -
                        Self for method chaining.
         
     | 
| 
       347 
     | 
    
         
            -
                    """
         
     | 
| 
       348 
     | 
    
         
            -
                    if not self.current_node:
         
     | 
| 
       349 
     | 
    
         
            -
                        logger.warning("No current node set for branching")
         
     | 
| 
       350 
     | 
    
         
            -
                        return self
         
     | 
| 
       351 
     | 
    
         
            -
                    for next_node_name, condition in branches:
         
     | 
| 
       352 
     | 
    
         
            -
                        if next_node_name not in self.nodes:
         
     | 
| 
       353 
     | 
    
         
            -
                            self._register_node(next_node_name)
         
     | 
| 
       354 
     | 
    
         
            -
                        self.transitions.setdefault(self.current_node, []).append((next_node_name, condition))
         
     | 
| 
       355 
     | 
    
         
            -
                        logger.debug(f"Added branch from {self.current_node} to {next_node_name} with condition {condition}")
         
     | 
| 
       356 
     | 
    
         
            -
                    if default:
         
     | 
| 
       357 
     | 
    
         
            -
                        if default not in self.nodes:
         
     | 
| 
       358 
     | 
    
         
            -
                            self._register_node(default)
         
     | 
| 
       359 
     | 
    
         
            -
                        self.transitions.setdefault(self.current_node, []).append((default, None))
         
     | 
| 
       360 
     | 
    
         
            -
                        logger.debug(f"Added default transition from {self.current_node} to {default}")
         
     | 
| 
       361 
     | 
    
         
            -
                    self.current_node = next_node  # Explicitly set next_node if provided
         
     | 
| 
       362 
     | 
    
         
            -
                    return self
         
     | 
| 
       363 
     | 
    
         
            -
             
     | 
| 
       364 
     | 
    
         
            -
                def converge(self, convergence_node: str) -> "Workflow":
         
     | 
| 
       365 
     | 
    
         
            -
                    """Set a convergence point for all previous branches.
         
     | 
| 
       366 
     | 
    
         
            -
             
     | 
| 
       367 
     | 
    
         
            -
                    Args:
         
     | 
| 
       368 
     | 
    
         
            -
                        convergence_node: Name of the node where branches converge.
         
     | 
| 
       369 
     | 
    
         
            -
             
     | 
| 
       370 
     | 
    
         
            -
                    Returns:
         
     | 
| 
       371 
     | 
    
         
            -
                        Self for method chaining.
         
     | 
| 
       372 
     | 
    
         
            -
                    """
         
     | 
| 
       373 
     | 
    
         
            -
                    if convergence_node not in self.nodes:
         
     | 
| 
       374 
     | 
    
         
            -
                        self._register_node(convergence_node)
         
     | 
| 
       375 
     | 
    
         
            -
                    for node in self.nodes:
         
     | 
| 
       376 
     | 
    
         
            -
                        if (node not in self.transitions or not self.transitions[node]) and node != convergence_node:
         
     | 
| 
       377 
     | 
    
         
            -
                            self.transitions.setdefault(node, []).append((convergence_node, None))
         
     | 
| 
       378 
     | 
    
         
            -
                            logger.debug(f"Added convergence from {node} to {convergence_node}")
         
     | 
| 
       379 
     | 
    
         
            -
                    self.current_node = convergence_node
         
     | 
| 
       380 
     | 
    
         
            -
                    return self
         
     | 
| 
       381 
     | 
    
         
            -
             
     | 
| 
       382 
     | 
    
         
            -
                def parallel(self, *nodes: str):
         
     | 
| 
       383 
     | 
    
         
            -
                    """Add parallel nodes to execute concurrently.
         
     | 
| 
       384 
     | 
    
         
            -
             
     | 
| 
       385 
     | 
    
         
            -
                    Args:
         
     | 
| 
       386 
     | 
    
         
            -
                        *nodes: Variable number of node names to execute in parallel.
         
     | 
| 
       387 
     | 
    
         
            -
             
     | 
| 
       388 
     | 
    
         
            -
                    Returns:
         
     | 
| 
       389 
     | 
    
         
            -
                        Self for method chaining.
         
     | 
| 
       390 
     | 
    
         
            -
                    """
         
     | 
| 
       391 
     | 
    
         
            -
                    if self.current_node:
         
     | 
| 
       392 
     | 
    
         
            -
                        for node in nodes:
         
     | 
| 
       393 
     | 
    
         
            -
                            self.transitions.setdefault(self.current_node, []).append((node, None))
         
     | 
| 
       394 
     | 
    
         
            -
                    self.current_node = None
         
     | 
| 
       395 
     | 
    
         
            -
                    return self
         
     | 
| 
       396 
     | 
    
         
            -
             
     | 
| 
       397 
     | 
    
         
            -
                def add_observer(self, observer: WorkflowObserver) -> "Workflow":
         
     | 
| 
       398 
     | 
    
         
            -
                    """Add an event observer callback to the workflow.
         
     | 
| 
       399 
     | 
    
         
            -
             
     | 
| 
       400 
     | 
    
         
            -
                    Args:
         
     | 
| 
       401 
     | 
    
         
            -
                        observer: Callable to handle workflow events.
         
     | 
| 
       402 
     | 
    
         
            -
             
     | 
| 
       403 
     | 
    
         
            -
                    Returns:
         
     | 
| 
       404 
     | 
    
         
            -
                        Self for method chaining.
         
     | 
| 
       405 
     | 
    
         
            -
                    """
         
     | 
| 
       406 
     | 
    
         
            -
                    if observer not in self._observers:
         
     | 
| 
       407 
     | 
    
         
            -
                        self._observers.append(observer)
         
     | 
| 
       408 
     | 
    
         
            -
                        logger.debug(f"Added observer to workflow: {observer}")
         
     | 
| 
       409 
     | 
    
         
            -
                    return self
         
     | 
| 
       410 
     | 
    
         
            -
             
     | 
| 
       411 
     | 
    
         
            -
                def add_sub_workflow(self, name: str, sub_workflow: "Workflow", inputs: Dict[str, Any], output: str):
         
     | 
| 
       412 
     | 
    
         
            -
                    """Add a sub-workflow as a node with flexible inputs mapping.
         
     | 
| 
       413 
     | 
    
         
            -
             
     | 
| 
       414 
     | 
    
         
            -
                    Args:
         
     | 
| 
       415 
     | 
    
         
            -
                        name: Name of the sub-workflow node.
         
     | 
| 
       416 
     | 
    
         
            -
                        sub_workflow: The Workflow instance to embed.
         
     | 
| 
       417 
     | 
    
         
            -
                        inputs: Dictionary mapping sub-workflow inputs to context keys or callables.
         
     | 
| 
       418 
     | 
    
         
            -
                        output: Context key for the sub-workflow's result.
         
     | 
| 
       419 
     | 
    
         
            -
             
     | 
| 
       420 
     | 
    
         
            -
                    Returns:
         
     | 
| 
       421 
     | 
    
         
            -
                        Self for method chaining.
         
     | 
| 
       422 
     | 
    
         
            -
                    """
         
     | 
| 
       423 
     | 
    
         
            -
                    sub_node = SubWorkflowNode(sub_workflow, inputs, output)
         
     | 
| 
       424 
     | 
    
         
            -
                    self.nodes[name] = sub_node
         
     | 
| 
       425 
     | 
    
         
            -
                    self.node_inputs[name] = []
         
     | 
| 
       426 
     | 
    
         
            -
                    self.node_outputs[name] = output
         
     | 
| 
       427 
     | 
    
         
            -
                    self.current_node = name
         
     | 
| 
       428 
     | 
    
         
            -
                    logger.debug(f"Added sub-workflow {name} with inputs {inputs} and output {output}")
         
     | 
| 
       429 
     | 
    
         
            -
                    return self
         
     | 
| 
       430 
     | 
    
         
            -
             
     | 
| 
       431 
     | 
    
         
            -
                def start_loop(self):
         
     | 
| 
       432 
     | 
    
         
            -
                    """Begin defining a loop in the workflow.
         
     | 
| 
       433 
     | 
    
         
            -
             
     | 
| 
       434 
     | 
    
         
            -
                    Raises:
         
     | 
| 
       435 
     | 
    
         
            -
                        ValueError: If called without a current node.
         
     | 
| 
       436 
     | 
    
         
            -
             
     | 
| 
       437 
     | 
    
         
            -
                    Returns:
         
     | 
| 
       438 
     | 
    
         
            -
                        Self for method chaining.
         
     | 
| 
       439 
     | 
    
         
            -
                    """
         
     | 
| 
       440 
     | 
    
         
            -
                    if self.current_node is None:
         
     | 
| 
       441 
     | 
    
         
            -
                        raise ValueError("Cannot start loop without a current node")
         
     | 
| 
       442 
     | 
    
         
            -
                    self.loop_entry_node = self.current_node
         
     | 
| 
       443 
     | 
    
         
            -
                    self.in_loop = True
         
     | 
| 
       444 
     | 
    
         
            -
                    self.loop_nodes = []
         
     | 
| 
       445 
     | 
    
         
            -
                    return self
         
     | 
| 
       446 
     | 
    
         
            -
             
     | 
| 
       447 
     | 
    
         
            -
                def end_loop(self, condition: Callable[[Dict[str, Any]], bool], next_node: str):
         
     | 
| 
       448 
     | 
    
         
            -
                    """End the loop, setting up transitions based on the condition.
         
     | 
| 
       449 
     | 
    
         
            -
             
     | 
| 
       450 
     | 
    
         
            -
                    Args:
         
     | 
| 
       451 
     | 
    
         
            -
                        condition: Callable taking context and returning True when the loop should exit.
         
     | 
| 
       452 
     | 
    
         
            -
                        next_node: Name of the node to transition to after the loop exits.
         
     | 
| 
       453 
     | 
    
         
            -
             
     | 
| 
       454 
     | 
    
         
            -
                    Raises:
         
     | 
| 
       455 
     | 
    
         
            -
                        ValueError: If no loop nodes are defined.
         
     | 
| 
       456 
     | 
    
         
            -
             
     | 
| 
       457 
     | 
    
         
            -
                    Returns:
         
     | 
| 
       458 
     | 
    
         
            -
                        Self for method chaining.
         
     | 
| 
       459 
     | 
    
         
            -
                    """
         
     | 
| 
       460 
     | 
    
         
            -
                    if not self.in_loop or not self.loop_nodes:
         
     | 
| 
       461 
     | 
    
         
            -
                        raise ValueError("No loop nodes defined")
         
     | 
| 
       462 
     | 
    
         
            -
                    
         
     | 
| 
       463 
     | 
    
         
            -
                    first_node = self.loop_nodes[0]
         
     | 
| 
       464 
     | 
    
         
            -
                    last_node = self.loop_nodes[-1]
         
     | 
| 
       465 
     | 
    
         
            -
                    
         
     | 
| 
       466 
     | 
    
         
            -
                    # Transition from the node before the loop to the first loop node
         
     | 
| 
       467 
     | 
    
         
            -
                    self.transitions.setdefault(self.loop_entry_node, []).append((first_node, None))
         
     | 
| 
       468 
     | 
    
         
            -
                    
         
     | 
| 
       469 
     | 
    
         
            -
                    # Transitions within the loop
         
     | 
| 
       470 
     | 
    
         
            -
                    for i in range(len(self.loop_nodes) - 1):
         
     | 
| 
       471 
     | 
    
         
            -
                        self.transitions.setdefault(self.loop_nodes[i], []).append((self.loop_nodes[i + 1], None))
         
     | 
| 
       472 
     | 
    
         
            -
                    
         
     | 
| 
       473 
     | 
    
         
            -
                    # Conditional transitions from the last loop node
         
     | 
| 
       474 
     | 
    
         
            -
                    # If condition is False, loop back to the first node
         
     | 
| 
       475 
     | 
    
         
            -
                    self.transitions.setdefault(last_node, []).append((first_node, lambda ctx: not condition(ctx)))
         
     | 
| 
       476 
     | 
    
         
            -
                    # If condition is True, exit to the next node
         
     | 
| 
       477 
     | 
    
         
            -
                    self.transitions.setdefault(last_node, []).append((next_node, condition))
         
     | 
| 
       478 
     | 
    
         
            -
                    
         
     | 
| 
       479 
     | 
    
         
            -
                    # Register the next_node if not already present
         
     | 
| 
       480 
     | 
    
         
            -
                    if next_node not in self.nodes:
         
     | 
| 
       481 
     | 
    
         
            -
                        self._register_node(next_node)
         
     | 
| 
       482 
     | 
    
         
            -
                    
         
     | 
| 
       483 
     | 
    
         
            -
                    # Update state
         
     | 
| 
       484 
     | 
    
         
            -
                    self.current_node = next_node
         
     | 
| 
       485 
     | 
    
         
            -
                    self.in_loop = False
         
     | 
| 
       486 
     | 
    
         
            -
                    self.loop_nodes = []
         
     | 
| 
       487 
     | 
    
         
            -
                    self.loop_entry_node = None
         
     | 
| 
       488 
     | 
    
         
            -
                    
         
     | 
| 
       489 
     | 
    
         
            -
                    return self
         
     | 
| 
       490 
     | 
    
         
            -
             
     | 
| 
       491 
     | 
    
         
            -
                def build(self, parent_engine: Optional["WorkflowEngine"] = None) -> WorkflowEngine:
         
     | 
| 
       492 
     | 
    
         
            -
                    """Build and return a WorkflowEngine instance with registered observers.
         
     | 
| 
       493 
     | 
    
         
            -
             
     | 
| 
       494 
     | 
    
         
            -
                    Args:
         
     | 
| 
       495 
     | 
    
         
            -
                        parent_engine: Optional parent WorkflowEngine for sub-workflows.
         
     | 
| 
       496 
     | 
    
         
            -
             
     | 
| 
       497 
     | 
    
         
            -
                    Returns:
         
     | 
| 
       498 
     | 
    
         
            -
                        Configured WorkflowEngine instance.
         
     | 
| 
       499 
     | 
    
         
            -
                    """
         
     | 
| 
       500 
     | 
    
         
            -
                    engine = WorkflowEngine(self, parent_engine=parent_engine)
         
     | 
| 
       501 
     | 
    
         
            -
                    for observer in self._observers:
         
     | 
| 
       502 
     | 
    
         
            -
                        engine.add_observer(observer)
         
     | 
| 
       503 
     | 
    
         
            -
                    return engine
         
     | 
| 
       504 
     | 
    
         
            -
             
     | 
| 
       505 
     | 
    
         
            -
             
     | 
| 
       506 
     | 
    
         
            -
            class Nodes:
         
     | 
| 
       507 
     | 
    
         
            -
                NODE_REGISTRY: Dict[str, Tuple[Callable, List[str], Optional[str]]] = {}
         
     | 
| 
       508 
     | 
    
         
            -
             
     | 
| 
       509 
     | 
    
         
            -
                @classmethod
         
     | 
| 
       510 
     | 
    
         
            -
                def define(cls, output: Optional[str] = None):
         
     | 
| 
       511 
     | 
    
         
            -
                    """Decorator for defining simple workflow nodes.
         
     | 
| 
       512 
     | 
    
         
            -
             
     | 
| 
       513 
     | 
    
         
            -
                    Args:
         
     | 
| 
       514 
     | 
    
         
            -
                        output: Optional context key for the node's result.
         
     | 
| 
       515 
     | 
    
         
            -
             
     | 
| 
       516 
     | 
    
         
            -
                    Returns:
         
     | 
| 
       517 
     | 
    
         
            -
                        Decorator function wrapping the node logic.
         
     | 
| 
       518 
     | 
    
         
            -
                    """
         
     | 
| 
       519 
     | 
    
         
            -
                    def decorator(func: Callable) -> Callable:
         
     | 
| 
       520 
     | 
    
         
            -
                        async def wrapped_func(**kwargs):
         
     | 
| 
       521 
     | 
    
         
            -
                            try:
         
     | 
| 
       522 
     | 
    
         
            -
                                if asyncio.iscoroutinefunction(func):
         
     | 
| 
       523 
     | 
    
         
            -
                                    result = await func(**kwargs)
         
     | 
| 
       524 
     | 
    
         
            -
                                else:
         
     | 
| 
       525 
     | 
    
         
            -
                                    result = func(**kwargs)
         
     | 
| 
       526 
     | 
    
         
            -
                                logger.debug(f"Node {func.__name__} executed with result: {result}")
         
     | 
| 
       527 
     | 
    
         
            -
                                return result
         
     | 
| 
       528 
     | 
    
         
            -
                            except Exception as e:
         
     | 
| 
       529 
     | 
    
         
            -
                                logger.error(f"Error in node {func.__name__}: {e}")
         
     | 
| 
       530 
     | 
    
         
            -
                                raise
         
     | 
| 
       531 
     | 
    
         
            -
                        sig = inspect.signature(func)
         
     | 
| 
       532 
     | 
    
         
            -
                        inputs = [param.name for param in sig.parameters.values()]
         
     | 
| 
       533 
     | 
    
         
            -
                        logger.debug(f"Registering node {func.__name__} with inputs {inputs} and output {output}")
         
     | 
| 
       534 
     | 
    
         
            -
                        cls.NODE_REGISTRY[func.__name__] = (wrapped_func, inputs, output)
         
     | 
| 
       535 
     | 
    
         
            -
                        return wrapped_func
         
     | 
| 
       536 
     | 
    
         
            -
                    return decorator
         
     | 
| 
       537 
     | 
    
         
            -
             
     | 
| 
       538 
     | 
    
         
            -
                @classmethod
         
     | 
| 
       539 
     | 
    
         
            -
                def validate_node(cls, output: str):
         
     | 
| 
       540 
     | 
    
         
            -
                    """Decorator for nodes that validate inputs and return a string.
         
     | 
| 
       541 
     | 
    
         
            -
             
     | 
| 
       542 
     | 
    
         
            -
                    Args:
         
     | 
| 
       543 
     | 
    
         
            -
                        output: Context key for the validation result.
         
     | 
| 
       544 
     | 
    
         
            -
             
     | 
| 
       545 
     | 
    
         
            -
                    Returns:
         
     | 
| 
       546 
     | 
    
         
            -
                        Decorator function wrapping the validation logic.
         
     | 
| 
       547 
     | 
    
         
            -
                    """
         
     | 
| 
       548 
     | 
    
         
            -
                    def decorator(func: Callable) -> Callable:
         
     | 
| 
       549 
     | 
    
         
            -
                        async def wrapped_func(**kwargs):
         
     | 
| 
       550 
     | 
    
         
            -
                            try:
         
     | 
| 
       551 
     | 
    
         
            -
                                if asyncio.iscoroutinefunction(func):
         
     | 
| 
       552 
     | 
    
         
            -
                                    result = await func(**kwargs)
         
     | 
| 
       553 
     | 
    
         
            -
                                else:
         
     | 
| 
       554 
     | 
    
         
            -
                                    result = func(**kwargs)
         
     | 
| 
       555 
     | 
    
         
            -
                                if not isinstance(result, str):
         
     | 
| 
       556 
     | 
    
         
            -
                                    raise ValueError(f"Validation node {func.__name__} must return a string")
         
     | 
| 
       557 
     | 
    
         
            -
                                logger.info(f"Validation result from {func.__name__}: {result}")
         
     | 
| 
       558 
     | 
    
         
            -
                                return result
         
     | 
| 
       559 
     | 
    
         
            -
                            except Exception as e:
         
     | 
| 
       560 
     | 
    
         
            -
                                logger.error(f"Validation error in {func.__name__}: {e}")
         
     | 
| 
       561 
     | 
    
         
            -
                                raise
         
     | 
| 
       562 
     | 
    
         
            -
                        sig = inspect.signature(func)
         
     | 
| 
       563 
     | 
    
         
            -
                        inputs = [param.name for param in sig.parameters.values()]
         
     | 
| 
       564 
     | 
    
         
            -
                        logger.debug(f"Registering node {func.__name__} with inputs {inputs} and output {output}")
         
     | 
| 
       565 
     | 
    
         
            -
                        cls.NODE_REGISTRY[func.__name__] = (wrapped_func, inputs, output)
         
     | 
| 
       566 
     | 
    
         
            -
                        return wrapped_func
         
     | 
| 
       567 
     | 
    
         
            -
                    return decorator
         
     | 
| 
       568 
     | 
    
         
            -
             
     | 
| 
       569 
     | 
    
         
            -
                @classmethod
         
     | 
| 
       570 
     | 
    
         
            -
                def transform_node(cls, output: str, transformer: Callable[[Any], Any]):
         
     | 
| 
       571 
     | 
    
         
            -
                    """Decorator for nodes that transform their inputs.
         
     | 
| 
       572 
     | 
    
         
            -
             
     | 
| 
       573 
     | 
    
         
            -
                    Args:
         
     | 
| 
       574 
     | 
    
         
            -
                        output: Context key for the transformed result.
         
     | 
| 
       575 
     | 
    
         
            -
                        transformer: Callable to transform the input.
         
     | 
| 
       576 
     | 
    
         
            -
             
     | 
| 
       577 
     | 
    
         
            -
                    Returns:
         
     | 
| 
       578 
     | 
    
         
            -
                        Decorator function wrapping the transformation logic.
         
     | 
| 
       579 
     | 
    
         
            -
                    """
         
     | 
| 
       580 
     | 
    
         
            -
                    def decorator(func: Callable) -> Callable:
         
     | 
| 
       581 
     | 
    
         
            -
                        async def wrapped_func(**kwargs):
         
     | 
| 
       582 
     | 
    
         
            -
                            try:
         
     | 
| 
       583 
     | 
    
         
            -
                                input_key = list(kwargs.keys())[0] if kwargs else None
         
     | 
| 
       584 
     | 
    
         
            -
                                if input_key:
         
     | 
| 
       585 
     | 
    
         
            -
                                    transformed_input = transformer(kwargs[input_key])
         
     | 
| 
       586 
     | 
    
         
            -
                                    kwargs[input_key] = transformed_input
         
     | 
| 
       587 
     | 
    
         
            -
                                if asyncio.iscoroutinefunction(func):
         
     | 
| 
       588 
     | 
    
         
            -
                                    result = await func(**kwargs)
         
     | 
| 
       589 
     | 
    
         
            -
                                else:
         
     | 
| 
       590 
     | 
    
         
            -
                                    result = func(**kwargs)
         
     | 
| 
       591 
     | 
    
         
            -
                                logger.debug(f"Transformed node {func.__name__} executed with result: {result}")
         
     | 
| 
       592 
     | 
    
         
            -
                                return result
         
     | 
| 
       593 
     | 
    
         
            -
                            except Exception as e:
         
     | 
| 
       594 
     | 
    
         
            -
                                logger.error(f"Error in transform node {func.__name__}: {e}")
         
     | 
| 
       595 
     | 
    
         
            -
                                raise
         
     | 
| 
       596 
     | 
    
         
            -
                        sig = inspect.signature(func)
         
     | 
| 
       597 
     | 
    
         
            -
                        inputs = [param.name for param in sig.parameters.values()]
         
     | 
| 
       598 
     | 
    
         
            -
                        logger.debug(f"Registering node {func.__name__} with inputs {inputs} and output {output}")
         
     | 
| 
       599 
     | 
    
         
            -
                        cls.NODE_REGISTRY[func.__name__] = (wrapped_func, inputs, output)
         
     | 
| 
       600 
     | 
    
         
            -
                        return wrapped_func
         
     | 
| 
       601 
     | 
    
         
            -
                    return decorator
         
     | 
| 
       602 
     | 
    
         
            -
             
     | 
| 
       603 
     | 
    
         
            -
                @staticmethod
         
     | 
| 
       604 
     | 
    
         
            -
                def _load_prompt_from_file(prompt_file: str, context: Dict[str, Any]) -> str:
         
     | 
| 
       605 
     | 
    
         
            -
                    """Load and render a Jinja2 template from an external file."""
         
     | 
| 
       606 
     | 
    
         
            -
                    try:
         
     | 
| 
       607 
     | 
    
         
            -
                        file_path = Path(prompt_file).resolve()
         
     | 
| 
       608 
     | 
    
         
            -
                        directory = file_path.parent
         
     | 
| 
       609 
     | 
    
         
            -
                        filename = file_path.name
         
     | 
| 
       610 
     | 
    
         
            -
                        env = Environment(loader=FileSystemLoader(directory))
         
     | 
| 
       611 
     | 
    
         
            -
                        template = env.get_template(filename)
         
     | 
| 
       612 
     | 
    
         
            -
                        return template.render(**context)
         
     | 
| 
       613 
     | 
    
         
            -
                    except TemplateNotFound as e:
         
     | 
| 
       614 
     | 
    
         
            -
                        logger.error(f"Jinja2 template file '{prompt_file}' not found: {e}")
         
     | 
| 
       615 
     | 
    
         
            -
                        raise ValueError(f"Prompt file '{prompt_file}' not found")
         
     | 
| 
       616 
     | 
    
         
            -
                    except Exception as e:
         
     | 
| 
       617 
     | 
    
         
            -
                        logger.error(f"Error loading or rendering prompt file '{prompt_file}': {e}")
         
     | 
| 
       618 
     | 
    
         
            -
                        raise
         
     | 
| 
       619 
     | 
    
         
            -
             
     | 
| 
       620 
     | 
    
         
            -
                @staticmethod
         
     | 
| 
       621 
     | 
    
         
            -
                def _render_template(template: str, template_file: Optional[str], context: Dict[str, Any]) -> str:
         
     | 
| 
       622 
     | 
    
         
            -
                    """Render a Jinja2 template from either a string or an external file."""
         
     | 
| 
       623 
     | 
    
         
            -
                    if template_file:
         
     | 
| 
       624 
     | 
    
         
            -
                        return Nodes._load_prompt_from_file(template_file, context)
         
     | 
| 
       625 
     | 
    
         
            -
                    try:
         
     | 
| 
       626 
     | 
    
         
            -
                        return Template(template).render(**context)
         
     | 
| 
       627 
     | 
    
         
            -
                    except Exception as e:
         
     | 
| 
       628 
     | 
    
         
            -
                        logger.error(f"Error rendering template: {e}")
         
     | 
| 
       629 
     | 
    
         
            -
                        raise
         
     | 
| 
       630 
     | 
    
         
            -
             
     | 
| 
       631 
     | 
    
         
            -
                @classmethod
         
     | 
| 
       632 
     | 
    
         
            -
                def llm_node(
         
     | 
| 
       633 
     | 
    
         
            -
                    cls,
         
     | 
| 
       634 
     | 
    
         
            -
                    system_prompt: str = "",
         
     | 
| 
       635 
     | 
    
         
            -
                    system_prompt_file: Optional[str] = None,
         
     | 
| 
       636 
     | 
    
         
            -
                    output: str = "",
         
     | 
| 
       637 
     | 
    
         
            -
                    prompt_template: str = "",
         
     | 
| 
       638 
     | 
    
         
            -
                    prompt_file: Optional[str] = None,
         
     | 
| 
       639 
     | 
    
         
            -
                    temperature: float = 0.7,
         
     | 
| 
       640 
     | 
    
         
            -
                    max_tokens: int = 2000,
         
     | 
| 
       641 
     | 
    
         
            -
                    top_p: float = 1.0,
         
     | 
| 
       642 
     | 
    
         
            -
                    presence_penalty: float = 0.0,
         
     | 
| 
       643 
     | 
    
         
            -
                    frequency_penalty: float = 0.0,
         
     | 
| 
       644 
     | 
    
         
            -
                    model: Union[Callable[[Dict[str, Any]], str], str] = lambda ctx: "gpt-3.5-turbo",
         
     | 
| 
       645 
     | 
    
         
            -
                    **kwargs,
         
     | 
| 
       646 
     | 
    
         
            -
                ):
         
     | 
| 
       647 
     | 
    
         
            -
                    """Decorator for creating LLM nodes with plain text output, supporting dynamic parameters.
         
     | 
| 
       648 
     | 
    
         
            -
             
     | 
| 
       649 
     | 
    
         
            -
                    Args:
         
     | 
| 
       650 
     | 
    
         
            -
                        system_prompt: Inline system prompt defining LLM behavior.
         
     | 
| 
       651 
     | 
    
         
            -
                        system_prompt_file: Path to a system prompt template file (overrides system_prompt).
         
     | 
| 
       652 
     | 
    
         
            -
                        output: Context key for the LLM's result.
         
     | 
| 
       653 
     | 
    
         
            -
                        prompt_template: Inline Jinja2 template for the user prompt.
         
     | 
| 
       654 
     | 
    
         
            -
                        prompt_file: Path to a user prompt template file (overrides prompt_template).
         
     | 
| 
       655 
     | 
    
         
            -
                        temperature: Randomness control (0.0 to 1.0).
         
     | 
| 
       656 
     | 
    
         
            -
                        max_tokens: Maximum response length.
         
     | 
| 
       657 
     | 
    
         
            -
                        top_p: Nucleus sampling parameter (0.0 to 1.0).
         
     | 
| 
       658 
     | 
    
         
            -
                        presence_penalty: Penalty for repetition (-2.0 to 2.0).
         
     | 
| 
       659 
     | 
    
         
            -
                        frequency_penalty: Penalty for frequent words (-2.0 to 2.0).
         
     | 
| 
       660 
     | 
    
         
            -
                        model: Callable or string to determine the LLM model dynamically from context.
         
     | 
| 
       661 
     | 
    
         
            -
                        **kwargs: Additional parameters for the LLM call.
         
     | 
| 
       662 
     | 
    
         
            -
             
     | 
| 
       663 
     | 
    
         
            -
                    Returns:
         
     | 
| 
       664 
     | 
    
         
            -
                        Decorator function wrapping the LLM logic.
         
     | 
| 
       665 
     | 
    
         
            -
                    """
         
     | 
| 
       666 
     | 
    
         
            -
                    def decorator(func: Callable) -> Callable:
         
     | 
| 
       667 
     | 
    
         
            -
                        # Store all decorator parameters in a config dictionary
         
     | 
| 
       668 
     | 
    
         
            -
                        config = {
         
     | 
| 
       669 
     | 
    
         
            -
                            "system_prompt": system_prompt,
         
     | 
| 
       670 
     | 
    
         
            -
                            "system_prompt_file": system_prompt_file,
         
     | 
| 
       671 
     | 
    
         
            -
                            "prompt_template": prompt_template,
         
     | 
| 
       672 
     | 
    
         
            -
                            "prompt_file": prompt_file,
         
     | 
| 
       673 
     | 
    
         
            -
                            "temperature": temperature,
         
     | 
| 
       674 
     | 
    
         
            -
                            "max_tokens": max_tokens,
         
     | 
| 
       675 
     | 
    
         
            -
                            "top_p": top_p,
         
     | 
| 
       676 
     | 
    
         
            -
                            "presence_penalty": presence_penalty,
         
     | 
| 
       677 
     | 
    
         
            -
                            "frequency_penalty": frequency_penalty,
         
     | 
| 
       678 
     | 
    
         
            -
                            "model": model,
         
     | 
| 
       679 
     | 
    
         
            -
                            **kwargs,
         
     | 
| 
       680 
     | 
    
         
            -
                        }
         
     | 
| 
       681 
     | 
    
         
            -
             
     | 
| 
       682 
     | 
    
         
            -
                        async def wrapped_func(**func_kwargs):
         
     | 
| 
       683 
     | 
    
         
            -
                            # Use func_kwargs to override config values if provided, otherwise use config defaults
         
     | 
| 
       684 
     | 
    
         
            -
                            system_prompt_to_use = func_kwargs.pop("system_prompt", config["system_prompt"])
         
     | 
| 
       685 
     | 
    
         
            -
                            system_prompt_file_to_use = func_kwargs.pop("system_prompt_file", config["system_prompt_file"])
         
     | 
| 
       686 
     | 
    
         
            -
                            prompt_template_to_use = func_kwargs.pop("prompt_template", config["prompt_template"])
         
     | 
| 
       687 
     | 
    
         
            -
                            prompt_file_to_use = func_kwargs.pop("prompt_file", config["prompt_file"])
         
     | 
| 
       688 
     | 
    
         
            -
                            temperature_to_use = func_kwargs.pop("temperature", config["temperature"])
         
     | 
| 
       689 
     | 
    
         
            -
                            max_tokens_to_use = func_kwargs.pop("max_tokens", config["max_tokens"])
         
     | 
| 
       690 
     | 
    
         
            -
                            top_p_to_use = func_kwargs.pop("top_p", config["top_p"])
         
     | 
| 
       691 
     | 
    
         
            -
                            presence_penalty_to_use = func_kwargs.pop("presence_penalty", config["presence_penalty"])
         
     | 
| 
       692 
     | 
    
         
            -
                            frequency_penalty_to_use = func_kwargs.pop("frequency_penalty", config["frequency_penalty"])
         
     | 
| 
       693 
     | 
    
         
            -
                            model_to_use = func_kwargs.pop("model", config["model"])
         
     | 
| 
       694 
     | 
    
         
            -
             
     | 
| 
       695 
     | 
    
         
            -
                            # Handle callable model parameter
         
     | 
| 
       696 
     | 
    
         
            -
                            if callable(model_to_use):
         
     | 
| 
       697 
     | 
    
         
            -
                                model_to_use = model_to_use(func_kwargs)
         
     | 
| 
       698 
     | 
    
         
            -
             
     | 
| 
       699 
     | 
    
         
            -
                            # Load system prompt from file if specified
         
     | 
| 
       700 
     | 
    
         
            -
                            if system_prompt_file_to_use:
         
     | 
| 
       701 
     | 
    
         
            -
                                system_content = cls._load_prompt_from_file(system_prompt_file_to_use, func_kwargs)
         
     | 
| 
       702 
     | 
    
         
            -
                            else:
         
     | 
| 
       703 
     | 
    
         
            -
                                system_content = system_prompt_to_use
         
     | 
| 
       704 
     | 
    
         
            -
             
     | 
| 
       705 
     | 
    
         
            -
                            # Prepare template variables and render prompt
         
     | 
| 
       706 
     | 
    
         
            -
                            sig = inspect.signature(func)
         
     | 
| 
       707 
     | 
    
         
            -
                            template_vars = {k: v for k, v in func_kwargs.items() if k in sig.parameters}
         
     | 
| 
       708 
     | 
    
         
            -
                            prompt = cls._render_template(prompt_template_to_use, prompt_file_to_use, template_vars)
         
     | 
| 
       709 
     | 
    
         
            -
                            messages = [
         
     | 
| 
       710 
     | 
    
         
            -
                                {"role": "system", "content": system_content},
         
     | 
| 
       711 
     | 
    
         
            -
                                {"role": "user", "content": prompt},
         
     | 
| 
       712 
     | 
    
         
            -
                            ]
         
     | 
| 
       713 
     | 
    
         
            -
             
     | 
| 
       714 
     | 
    
         
            -
                            # Logging for debugging
         
     | 
| 
       715 
     | 
    
         
            -
                            truncated_prompt = prompt[:200] + "..." if len(prompt) > 200 else prompt
         
     | 
| 
       716 
     | 
    
         
            -
                            logger.info(f"LLM node {func.__name__} using model: {model_to_use}")
         
     | 
| 
       717 
     | 
    
         
            -
                            logger.debug(f"System prompt: {system_content[:100]}...")
         
     | 
| 
       718 
     | 
    
         
            -
                            logger.debug(f"User prompt preview: {truncated_prompt}")
         
     | 
| 
       719 
     | 
    
         
            -
             
     | 
| 
       720 
     | 
    
         
            -
                            # Call the acompletion function with the resolved model
         
     | 
| 
       721 
     | 
    
         
            -
                            try:
         
     | 
| 
       722 
     | 
    
         
            -
                                response = await acompletion(
         
     | 
| 
       723 
     | 
    
         
            -
                                    model=model_to_use,
         
     | 
| 
       724 
     | 
    
         
            -
                                    messages=messages,
         
     | 
| 
       725 
     | 
    
         
            -
                                    temperature=temperature_to_use,
         
     | 
| 
       726 
     | 
    
         
            -
                                    max_tokens=max_tokens_to_use,
         
     | 
| 
       727 
     | 
    
         
            -
                                    top_p=top_p_to_use,
         
     | 
| 
       728 
     | 
    
         
            -
                                    presence_penalty=presence_penalty_to_use,
         
     | 
| 
       729 
     | 
    
         
            -
                                    frequency_penalty=frequency_penalty_to_use,
         
     | 
| 
       730 
     | 
    
         
            -
                                    drop_params=True,
         
     | 
| 
       731 
     | 
    
         
            -
                                    **kwargs,
         
     | 
| 
       732 
     | 
    
         
            -
                                )
         
     | 
| 
       733 
     | 
    
         
            -
                                content = response.choices[0].message.content.strip()
         
     | 
| 
       734 
     | 
    
         
            -
                                wrapped_func.usage = {
         
     | 
| 
       735 
     | 
    
         
            -
                                    "prompt_tokens": response.usage.prompt_tokens,
         
     | 
| 
       736 
     | 
    
         
            -
                                    "completion_tokens": response.usage.completion_tokens,
         
     | 
| 
       737 
     | 
    
         
            -
                                    "total_tokens": response.usage.total_tokens,
         
     | 
| 
       738 
     | 
    
         
            -
                                    "cost": getattr(response, "cost", None),
         
     | 
| 
       739 
     | 
    
         
            -
                                }
         
     | 
| 
       740 
     | 
    
         
            -
                                logger.debug(f"LLM output from {func.__name__}: {content[:50]}...")
         
     | 
| 
       741 
     | 
    
         
            -
                                return content
         
     | 
| 
       742 
     | 
    
         
            -
                            except Exception as e:
         
     | 
| 
       743 
     | 
    
         
            -
                                logger.error(f"Error in LLM node {func.__name__}: {e}")
         
     | 
| 
       744 
     | 
    
         
            -
                                raise
         
     | 
| 
       745 
     | 
    
         
            -
             
     | 
| 
       746 
     | 
    
         
            -
                        # Register the node with its inputs and output
         
     | 
| 
       747 
     | 
    
         
            -
                        sig = inspect.signature(func)
         
     | 
| 
       748 
     | 
    
         
            -
                        inputs = [param.name for param in sig.parameters.values()]
         
     | 
| 
       749 
     | 
    
         
            -
                        logger.debug(f"Registering node {func.__name__} with inputs {inputs} and output {output}")
         
     | 
| 
       750 
     | 
    
         
            -
                        cls.NODE_REGISTRY[func.__name__] = (wrapped_func, inputs, output)
         
     | 
| 
       751 
     | 
    
         
            -
                        return wrapped_func
         
     | 
| 
       752 
     | 
    
         
            -
                    return decorator
         
     | 
| 
       753 
     | 
    
         
            -
             
     | 
| 
       754 
     | 
    
         
            -
                @classmethod
         
     | 
| 
       755 
     | 
    
         
            -
                def structured_llm_node(
         
     | 
| 
       756 
     | 
    
         
            -
                    cls,
         
     | 
| 
       757 
     | 
    
         
            -
                    system_prompt: str = "",
         
     | 
| 
       758 
     | 
    
         
            -
                    system_prompt_file: Optional[str] = None,
         
     | 
| 
       759 
     | 
    
         
            -
                    output: str = "",
         
     | 
| 
       760 
     | 
    
         
            -
                    response_model: Type[BaseModel] = None,
         
     | 
| 
       761 
     | 
    
         
            -
                    prompt_template: str = "",
         
     | 
| 
       762 
     | 
    
         
            -
                    prompt_file: Optional[str] = None,
         
     | 
| 
       763 
     | 
    
         
            -
                    temperature: float = 0.7,
         
     | 
| 
       764 
     | 
    
         
            -
                    max_tokens: int = 2000,
         
     | 
| 
       765 
     | 
    
         
            -
                    top_p: float = 1.0,
         
     | 
| 
       766 
     | 
    
         
            -
                    presence_penalty: float = 0.0,
         
     | 
| 
       767 
     | 
    
         
            -
                    frequency_penalty: float = 0.0,
         
     | 
| 
       768 
     | 
    
         
            -
                    model: Union[Callable[[Dict[str, Any]], str], str] = lambda ctx: "gpt-3.5-turbo",
         
     | 
| 
       769 
     | 
    
         
            -
                    **kwargs,
         
     | 
| 
       770 
     | 
    
         
            -
                ):
         
     | 
| 
       771 
     | 
    
         
            -
                    """Decorator for creating LLM nodes with structured output, supporting dynamic parameters.
         
     | 
| 
       772 
     | 
    
         
            -
             
     | 
| 
       773 
     | 
    
         
            -
                    Args:
         
     | 
| 
       774 
     | 
    
         
            -
                        system_prompt: Inline system prompt defining LLM behavior.
         
     | 
| 
       775 
     | 
    
         
            -
                        system_prompt_file: Path to a system prompt template file (overrides system_prompt).
         
     | 
| 
       776 
     | 
    
         
            -
                        output: Context key for the LLM's structured result.
         
     | 
| 
       777 
     | 
    
         
            -
                        response_model: Pydantic model class for structured output.
         
     | 
| 
       778 
     | 
    
         
            -
                        prompt_template: Inline Jinja2 template for the user prompt.
         
     | 
| 
       779 
     | 
    
         
            -
                        prompt_file: Path to a user prompt template file (overrides prompt_template).
         
     | 
| 
       780 
     | 
    
         
            -
                        temperature: Randomness control (0.0 to 1.0).
         
     | 
| 
       781 
     | 
    
         
            -
                        max_tokens: Maximum response length.
         
     | 
| 
       782 
     | 
    
         
            -
                        top_p: Nucleus sampling parameter (0.0 to 1.0).
         
     | 
| 
       783 
     | 
    
         
            -
                        presence_penalty: Penalty for repetition (-2.0 to 2.0).
         
     | 
| 
       784 
     | 
    
         
            -
                        frequency_penalty: Penalty for frequent words (-2.0 to 2.0).
         
     | 
| 
       785 
     | 
    
         
            -
                        model: Callable or string to determine the LLM model dynamically from context.
         
     | 
| 
       786 
     | 
    
         
            -
                        **kwargs: Additional parameters for the LLM call.
         
     | 
| 
       787 
     | 
    
         
            -
             
     | 
| 
       788 
     | 
    
         
            -
                    Returns:
         
     | 
| 
       789 
     | 
    
         
            -
                        Decorator function wrapping the structured LLM logic.
         
     | 
| 
       790 
     | 
    
         
            -
                    """
         
     | 
| 
       791 
     | 
    
         
            -
                    try:
         
     | 
| 
       792 
     | 
    
         
            -
                        client = instructor.from_litellm(acompletion)
         
     | 
| 
       793 
     | 
    
         
            -
                    except ImportError:
         
     | 
| 
       794 
     | 
    
         
            -
                        logger.error("Instructor not installed. Install with 'pip install instructor[litellm]'")
         
     | 
| 
       795 
     | 
    
         
            -
                        raise ImportError("Instructor is required for structured_llm_node")
         
     | 
| 
       796 
     | 
    
         
            -
             
     | 
| 
       797 
     | 
    
         
            -
                    def decorator(func: Callable) -> Callable:
         
     | 
| 
       798 
     | 
    
         
            -
                        # Store all decorator parameters in a config dictionary
         
     | 
| 
       799 
     | 
    
         
            -
                        config = {
         
     | 
| 
       800 
     | 
    
         
            -
                            "system_prompt": system_prompt,
         
     | 
| 
       801 
     | 
    
         
            -
                            "system_prompt_file": system_prompt_file,
         
     | 
| 
       802 
     | 
    
         
            -
                            "prompt_template": prompt_template,
         
     | 
| 
       803 
     | 
    
         
            -
                            "prompt_file": prompt_file,
         
     | 
| 
       804 
     | 
    
         
            -
                            "temperature": temperature,
         
     | 
| 
       805 
     | 
    
         
            -
                            "max_tokens": max_tokens,
         
     | 
| 
       806 
     | 
    
         
            -
                            "top_p": top_p,
         
     | 
| 
       807 
     | 
    
         
            -
                            "presence_penalty": presence_penalty,
         
     | 
| 
       808 
     | 
    
         
            -
                            "frequency_penalty": frequency_penalty,
         
     | 
| 
       809 
     | 
    
         
            -
                            "model": model,
         
     | 
| 
       810 
     | 
    
         
            -
                            **kwargs,
         
     | 
| 
       811 
     | 
    
         
            -
                        }
         
     | 
| 
       812 
     | 
    
         
            -
             
     | 
| 
       813 
     | 
    
         
            -
                        async def wrapped_func(**func_kwargs):
         
     | 
| 
       814 
     | 
    
         
            -
                            # Resolve parameters, prioritizing func_kwargs over config defaults
         
     | 
| 
       815 
     | 
    
         
            -
                            system_prompt_to_use = func_kwargs.pop("system_prompt", config["system_prompt"])
         
     | 
| 
       816 
     | 
    
         
            -
                            system_prompt_file_to_use = func_kwargs.pop("system_prompt_file", config["system_prompt_file"])
         
     | 
| 
       817 
     | 
    
         
            -
                            prompt_template_to_use = func_kwargs.pop("prompt_template", config["prompt_template"])
         
     | 
| 
       818 
     | 
    
         
            -
                            prompt_file_to_use = func_kwargs.pop("prompt_file", config["prompt_file"])
         
     | 
| 
       819 
     | 
    
         
            -
                            temperature_to_use = func_kwargs.pop("temperature", config["temperature"])
         
     | 
| 
       820 
     | 
    
         
            -
                            max_tokens_to_use = func_kwargs.pop("max_tokens", config["max_tokens"])
         
     | 
| 
       821 
     | 
    
         
            -
                            top_p_to_use = func_kwargs.pop("top_p", config["top_p"])
         
     | 
| 
       822 
     | 
    
         
            -
                            presence_penalty_to_use = func_kwargs.pop("presence_penalty", config["presence_penalty"])
         
     | 
| 
       823 
     | 
    
         
            -
                            frequency_penalty_to_use = func_kwargs.pop("frequency_penalty", config["frequency_penalty"])
         
     | 
| 
       824 
     | 
    
         
            -
                            model_to_use = func_kwargs.pop("model", config["model"])
         
     | 
| 
       825 
     | 
    
         
            -
             
     | 
| 
       826 
     | 
    
         
            -
                            # Handle callable model parameter
         
     | 
| 
       827 
     | 
    
         
            -
                            if callable(model_to_use):
         
     | 
| 
       828 
     | 
    
         
            -
                                model_to_use = model_to_use(func_kwargs)
         
     | 
| 
       829 
     | 
    
         
            -
             
     | 
| 
       830 
     | 
    
         
            -
                            # Load system prompt from file if specified
         
     | 
| 
       831 
     | 
    
         
            -
                            if system_prompt_file_to_use:
         
     | 
| 
       832 
     | 
    
         
            -
                                system_content = cls._load_prompt_from_file(system_prompt_file_to_use, func_kwargs)
         
     | 
| 
       833 
     | 
    
         
            -
                            else:
         
     | 
| 
       834 
     | 
    
         
            -
                                system_content = system_prompt_to_use
         
     | 
| 
       835 
     | 
    
         
            -
             
     | 
| 
       836 
     | 
    
         
            -
                            # Render prompt using template variables
         
     | 
| 
       837 
     | 
    
         
            -
                            sig = inspect.signature(func)
         
     | 
| 
       838 
     | 
    
         
            -
                            template_vars = {k: v for k, v in func_kwargs.items() if k in sig.parameters}
         
     | 
| 
       839 
     | 
    
         
            -
                            prompt = cls._render_template(prompt_template_to_use, prompt_file_to_use, template_vars)
         
     | 
| 
       840 
     | 
    
         
            -
                            messages = [
         
     | 
| 
       841 
     | 
    
         
            -
                                {"role": "system", "content": system_content},
         
     | 
| 
       842 
     | 
    
         
            -
                                {"role": "user", "content": prompt},
         
     | 
| 
       843 
     | 
    
         
            -
                            ]
         
     | 
| 
       844 
     | 
    
         
            -
             
     | 
| 
       845 
     | 
    
         
            -
                            # Logging for debugging
         
     | 
| 
       846 
     | 
    
         
            -
                            truncated_prompt = prompt[:200] + "..." if len(prompt) > 200 else prompt
         
     | 
| 
       847 
     | 
    
         
            -
                            logger.info(f"Structured LLM node {func.__name__} using model: {model_to_use}")
         
     | 
| 
       848 
     | 
    
         
            -
                            logger.debug(f"System prompt: {system_content[:100]}...")
         
     | 
| 
       849 
     | 
    
         
            -
                            logger.debug(f"User prompt preview: {truncated_prompt}")
         
     | 
| 
       850 
     | 
    
         
            -
                            logger.debug(f"Expected response model: {response_model.__name__}")
         
     | 
| 
       851 
     | 
    
         
            -
             
     | 
| 
       852 
     | 
    
         
            -
                            # Generate structured response
         
     | 
| 
       853 
     | 
    
         
            -
                            try:
         
     | 
| 
       854 
     | 
    
         
            -
                                structured_response, raw_response = await client.chat.completions.create_with_completion(
         
     | 
| 
       855 
     | 
    
         
            -
                                    model=model_to_use,
         
     | 
| 
       856 
     | 
    
         
            -
                                    messages=messages,
         
     | 
| 
       857 
     | 
    
         
            -
                                    response_model=response_model,
         
     | 
| 
       858 
     | 
    
         
            -
                                    temperature=temperature_to_use,
         
     | 
| 
       859 
     | 
    
         
            -
                                    max_tokens=max_tokens_to_use,
         
     | 
| 
       860 
     | 
    
         
            -
                                    top_p=top_p_to_use,
         
     | 
| 
       861 
     | 
    
         
            -
                                    presence_penalty=presence_penalty_to_use,
         
     | 
| 
       862 
     | 
    
         
            -
                                    frequency_penalty=frequency_penalty_to_use,
         
     | 
| 
       863 
     | 
    
         
            -
                                    drop_params=True,
         
     | 
| 
       864 
     | 
    
         
            -
                                    **kwargs,
         
     | 
| 
       865 
     | 
    
         
            -
                                )
         
     | 
| 
       866 
     | 
    
         
            -
                                wrapped_func.usage = {
         
     | 
| 
       867 
     | 
    
         
            -
                                    "prompt_tokens": raw_response.usage.prompt_tokens,
         
     | 
| 
       868 
     | 
    
         
            -
                                    "completion_tokens": raw_response.usage.completion_tokens,
         
     | 
| 
       869 
     | 
    
         
            -
                                    "total_tokens": raw_response.usage.total_tokens,
         
     | 
| 
       870 
     | 
    
         
            -
                                    "cost": getattr(raw_response, "cost", None),
         
     | 
| 
       871 
     | 
    
         
            -
                                }
         
     | 
| 
       872 
     | 
    
         
            -
                                logger.debug(f"Structured output from {func.__name__}: {structured_response}")
         
     | 
| 
       873 
     | 
    
         
            -
                                return structured_response
         
     | 
| 
       874 
     | 
    
         
            -
                            except ValidationError as e:
         
     | 
| 
       875 
     | 
    
         
            -
                                logger.error(f"Validation error in {func.__name__}: {e}")
         
     | 
| 
       876 
     | 
    
         
            -
                                raise
         
     | 
| 
       877 
     | 
    
         
            -
                            except Exception as e:
         
     | 
| 
       878 
     | 
    
         
            -
                                logger.error(f"Error in structured LLM node {func.__name__}: {e}")
         
     | 
| 
       879 
     | 
    
         
            -
                                raise
         
     | 
| 
       880 
     | 
    
         
            -
             
     | 
| 
       881 
     | 
    
         
            -
                        # Register the node
         
     | 
| 
       882 
     | 
    
         
            -
                        sig = inspect.signature(func)
         
     | 
| 
       883 
     | 
    
         
            -
                        inputs = [param.name for param in sig.parameters.values()]
         
     | 
| 
       884 
     | 
    
         
            -
                        logger.debug(f"Registering node {func.__name__} with inputs {inputs} and output {output}")
         
     | 
| 
       885 
     | 
    
         
            -
                        cls.NODE_REGISTRY[func.__name__] = (wrapped_func, inputs, output)
         
     | 
| 
       886 
     | 
    
         
            -
                        return wrapped_func
         
     | 
| 
       887 
     | 
    
         
            -
                    return decorator
         
     | 
| 
       888 
     | 
    
         
            -
             
     | 
| 
       889 
     | 
    
         
            -
                @classmethod
         
     | 
| 
       890 
     | 
    
         
            -
                def template_node(
         
     | 
| 
       891 
     | 
    
         
            -
                    cls,
         
     | 
| 
       892 
     | 
    
         
            -
                    output: str,
         
     | 
| 
       893 
     | 
    
         
            -
                    template: str = "",
         
     | 
| 
       894 
     | 
    
         
            -
                    template_file: Optional[str] = None,
         
     | 
| 
       895 
     | 
    
         
            -
                ):
         
     | 
| 
       896 
     | 
    
         
            -
                    """Decorator for creating nodes that apply a Jinja2 template to inputs.
         
     | 
| 
       897 
     | 
    
         
            -
             
     | 
| 
       898 
     | 
    
         
            -
                    Args:
         
     | 
| 
       899 
     | 
    
         
            -
                        output: Context key for the rendered result.
         
     | 
| 
       900 
     | 
    
         
            -
                        template: Inline Jinja2 template string.
         
     | 
| 
       901 
     | 
    
         
            -
                        template_file: Path to a template file (overrides template).
         
     | 
| 
       902 
     | 
    
         
            -
             
     | 
| 
       903 
     | 
    
         
            -
                    Returns:
         
     | 
| 
       904 
     | 
    
         
            -
                        Decorator function wrapping the template logic.
         
     | 
| 
       905 
     | 
    
         
            -
                    """
         
     | 
| 
       906 
     | 
    
         
            -
                    def decorator(func: Callable) -> Callable:
         
     | 
| 
       907 
     | 
    
         
            -
                        async def wrapped_func(**func_kwargs):
         
     | 
| 
       908 
     | 
    
         
            -
                            template_to_use = func_kwargs.pop("template", template)
         
     | 
| 
       909 
     | 
    
         
            -
                            template_file_to_use = func_kwargs.pop("template_file", template_file)
         
     | 
| 
       910 
     | 
    
         
            -
             
     | 
| 
       911 
     | 
    
         
            -
                            sig = inspect.signature(func)
         
     | 
| 
       912 
     | 
    
         
            -
                            expected_params = [p.name for p in sig.parameters.values() if p.name != 'rendered_content']
         
     | 
| 
       913 
     | 
    
         
            -
                            template_vars = {k: v for k, v in func_kwargs.items() if k in expected_params}
         
     | 
| 
       914 
     | 
    
         
            -
                            rendered_content = cls._render_template(template_to_use, template_file_to_use, template_vars)
         
     | 
| 
       915 
     | 
    
         
            -
             
     | 
| 
       916 
     | 
    
         
            -
                            filtered_kwargs = {k: v for k, v in func_kwargs.items() if k in expected_params}
         
     | 
| 
       917 
     | 
    
         
            -
             
     | 
| 
       918 
     | 
    
         
            -
                            try:
         
     | 
| 
       919 
     | 
    
         
            -
                                if asyncio.iscoroutinefunction(func):
         
     | 
| 
       920 
     | 
    
         
            -
                                    result = await func(rendered_content=rendered_content, **filtered_kwargs)
         
     | 
| 
       921 
     | 
    
         
            -
                                else:
         
     | 
| 
       922 
     | 
    
         
            -
                                    result = func(rendered_content=rendered_content, **filtered_kwargs)
         
     | 
| 
       923 
     | 
    
         
            -
                                logger.debug(f"Template node {func.__name__} rendered: {rendered_content[:50]}...")
         
     | 
| 
       924 
     | 
    
         
            -
                                return result
         
     | 
| 
       925 
     | 
    
         
            -
                            except Exception as e:
         
     | 
| 
       926 
     | 
    
         
            -
                                logger.error(f"Error in template node {func.__name__}: {e}")
         
     | 
| 
       927 
     | 
    
         
            -
                                raise
         
     | 
| 
       928 
     | 
    
         
            -
                        sig = inspect.signature(func)
         
     | 
| 
       929 
     | 
    
         
            -
                        inputs = [param.name for param in sig.parameters.values()]
         
     | 
| 
       930 
     | 
    
         
            -
                        if 'rendered_content' not in inputs:
         
     | 
| 
       931 
     | 
    
         
            -
                            inputs.insert(0, 'rendered_content')
         
     | 
| 
       932 
     | 
    
         
            -
                        logger.debug(f"Registering node {func.__name__} with inputs {inputs} and output {output}")
         
     | 
| 
       933 
     | 
    
         
            -
                        cls.NODE_REGISTRY[func.__name__] = (wrapped_func, inputs, output)
         
     | 
| 
       934 
     | 
    
         
            -
                        return wrapped_func
         
     | 
| 
       935 
     | 
    
         
            -
                    return decorator
         
     | 
| 
       936 
     | 
    
         
            -
             
     | 
| 
       937 
     | 
    
         
            -
             
     | 
| 
       938 
     | 
    
         
            -
            # Add a templates directory path at the module level
         
     | 
| 
       939 
     | 
    
         
            -
            TEMPLATES_DIR = os.path.join(os.path.dirname(os.path.abspath(__file__)), "templates")
         
     | 
| 
       940 
     | 
    
         
            -
             
     | 
| 
       941 
     | 
    
         
            -
            # Helper function to get template paths
         
     | 
| 
       942 
     | 
    
         
            -
            def get_template_path(template_name):
         
     | 
| 
       943 
     | 
    
         
            -
                return os.path.join(TEMPLATES_DIR, template_name)
         
     | 
| 
       944 
     | 
    
         
            -
             
     | 
| 
       945 
     | 
    
         
            -
             
     | 
| 
       946 
     | 
    
         
            -
            async def example_workflow():
         
     | 
| 
       947 
     | 
    
         
            -
                class OrderDetails(BaseModel):
         
     | 
| 
       948 
     | 
    
         
            -
                    order_id: str
         
     | 
| 
       949 
     | 
    
         
            -
                    items_in_stock: List[str]
         
     | 
| 
       950 
     | 
    
         
            -
                    items_out_of_stock: List[str]
         
     | 
| 
       951 
     | 
    
         
            -
             
     | 
| 
       952 
     | 
    
         
            -
                async def progress_monitor(event: WorkflowEvent):
         
     | 
| 
       953 
     | 
    
         
            -
                    print(f"[{event.event_type.value}] {event.node_name or 'Workflow'}")
         
     | 
| 
       954 
     | 
    
         
            -
                    if event.result is not None:
         
     | 
| 
       955 
     | 
    
         
            -
                        print(f"Result: {event.result}")
         
     | 
| 
       956 
     | 
    
         
            -
                    if event.exception is not None:
         
     | 
| 
       957 
     | 
    
         
            -
                        print(f"Exception: {event.exception}")
         
     | 
| 
       958 
     | 
    
         
            -
             
     | 
| 
       959 
     | 
    
         
            -
                class TokenUsageObserver:
         
     | 
| 
       960 
     | 
    
         
            -
                    def __init__(self):
         
     | 
| 
       961 
     | 
    
         
            -
                        self.total_prompt_tokens = 0
         
     | 
| 
       962 
     | 
    
         
            -
                        self.total_completion_tokens = 0
         
     | 
| 
       963 
     | 
    
         
            -
                        self.total_cost = 0.0
         
     | 
| 
       964 
     | 
    
         
            -
                        self.node_usages = {}
         
     | 
| 
       965 
     | 
    
         
            -
             
     | 
| 
       966 
     | 
    
         
            -
                    def __call__(self, event: WorkflowEvent):
         
     | 
| 
       967 
     | 
    
         
            -
                        if event.event_type == WorkflowEventType.NODE_COMPLETED and event.usage:
         
     | 
| 
       968 
     | 
    
         
            -
                            usage = event.usage
         
     | 
| 
       969 
     | 
    
         
            -
                            self.total_prompt_tokens += usage.get("prompt_tokens", 0)
         
     | 
| 
       970 
     | 
    
         
            -
                            self.total_completion_tokens += usage.get("completion_tokens", 0)
         
     | 
| 
       971 
     | 
    
         
            -
                            if usage.get("cost") is not None:
         
     | 
| 
       972 
     | 
    
         
            -
                                self.total_cost += usage["cost"]
         
     | 
| 
       973 
     | 
    
         
            -
                            self.node_usages[event.node_name] = usage
         
     | 
| 
       974 
     | 
    
         
            -
                        if event.event_type == WorkflowEventType.WORKFLOW_COMPLETED:
         
     | 
| 
       975 
     | 
    
         
            -
                            print(f"Total prompt tokens: {self.total_prompt_tokens}")
         
     | 
| 
       976 
     | 
    
         
            -
                            print(f"Total completion tokens: {self.total_completion_tokens}")
         
     | 
| 
       977 
     | 
    
         
            -
                            print(f"Total cost: {self.total_cost}")
         
     | 
| 
       978 
     | 
    
         
            -
                            for node, usage in self.node_usages.items():
         
     | 
| 
       979 
     | 
    
         
            -
                                print(f"Node {node}: {usage}")
         
     | 
| 
       980 
     | 
    
         
            -
             
     | 
| 
       981 
     | 
    
         
            -
                @Nodes.validate_node(output="validation_result")
         
     | 
| 
       982 
     | 
    
         
            -
                async def validate_order(order: Dict[str, Any]) -> str:
         
     | 
| 
       983 
     | 
    
         
            -
                    return "Order validated" if order.get("items") else "Invalid order"
         
     | 
| 
       984 
     | 
    
         
            -
             
     | 
| 
       985 
     | 
    
         
            -
                @Nodes.structured_llm_node(
         
     | 
| 
       986 
     | 
    
         
            -
                    system_prompt_file=get_template_path("system_check_inventory.j2"),
         
     | 
| 
       987 
     | 
    
         
            -
                    output="inventory_status",
         
     | 
| 
       988 
     | 
    
         
            -
                    response_model=OrderDetails,
         
     | 
| 
       989 
     | 
    
         
            -
                    prompt_file=get_template_path("prompt_check_inventory.j2"),
         
     | 
| 
       990 
     | 
    
         
            -
                )
         
     | 
| 
       991 
     | 
    
         
            -
                async def check_inventory(items: List[str]) -> OrderDetails:
         
     | 
| 
       992 
     | 
    
         
            -
                    return OrderDetails(order_id="123", items_in_stock=["item1"], items_out_of_stock=[])
         
     | 
| 
       993 
     | 
    
         
            -
             
     | 
| 
       994 
     | 
    
         
            -
                @Nodes.define(output="payment_status")
         
     | 
| 
       995 
     | 
    
         
            -
                async def process_payment(order: Dict[str, Any]) -> str:
         
     | 
| 
       996 
     | 
    
         
            -
                    return "Payment processed"
         
     | 
| 
       997 
     | 
    
         
            -
             
     | 
| 
       998 
     | 
    
         
            -
                @Nodes.define(output="shipping_confirmation")
         
     | 
| 
       999 
     | 
    
         
            -
                async def arrange_shipping(order: Dict[str, Any]) -> str:
         
     | 
| 
       1000 
     | 
    
         
            -
                    return "Shipping arranged"
         
     | 
| 
       1001 
     | 
    
         
            -
             
     | 
| 
       1002 
     | 
    
         
            -
                @Nodes.define(output="order_status")
         
     | 
| 
       1003 
     | 
    
         
            -
                async def update_order_status(shipping_confirmation: str) -> str:
         
     | 
| 
       1004 
     | 
    
         
            -
                    return "Order updated"
         
     | 
| 
       1005 
     | 
    
         
            -
             
     | 
| 
       1006 
     | 
    
         
            -
                @Nodes.define(output="email_status")
         
     | 
| 
       1007 
     | 
    
         
            -
                async def send_confirmation_email(shipping_confirmation: str) -> str:
         
     | 
| 
       1008 
     | 
    
         
            -
                    return "Email sent"
         
     | 
| 
       1009 
     | 
    
         
            -
             
     | 
| 
       1010 
     | 
    
         
            -
                @Nodes.define(output="notification_status")
         
     | 
| 
       1011 
     | 
    
         
            -
                async def notify_customer_out_of_stock(inventory_status: OrderDetails) -> str:
         
     | 
| 
       1012 
     | 
    
         
            -
                    return "Customer notified of out-of-stock"
         
     | 
| 
       1013 
     | 
    
         
            -
             
     | 
| 
       1014 
     | 
    
         
            -
                @Nodes.transform_node(output="transformed_items", transformer=lambda x: [item.upper() for item in x])
         
     | 
| 
       1015 
     | 
    
         
            -
                async def transform_items(items: List[str]) -> List[str]:
         
     | 
| 
       1016 
     | 
    
         
            -
                    return items
         
     | 
| 
       1017 
     | 
    
         
            -
             
     | 
| 
       1018 
     | 
    
         
            -
                @Nodes.template_node(
         
     | 
| 
       1019 
     | 
    
         
            -
                    output="formatted_message",
         
     | 
| 
       1020 
     | 
    
         
            -
                    template="Order contains: {{ items | join(', ') }}",
         
     | 
| 
       1021 
     | 
    
         
            -
                )
         
     | 
| 
       1022 
     | 
    
         
            -
                async def format_order_message(rendered_content: str, items: List[str]) -> str:
         
     | 
| 
       1023 
     | 
    
         
            -
                    return rendered_content
         
     | 
| 
       1024 
     | 
    
         
            -
             
     | 
| 
       1025 
     | 
    
         
            -
                payment_shipping_sub_wf = Workflow("process_payment").sequence("process_payment", "arrange_shipping")
         
     | 
| 
       1026 
     | 
    
         
            -
             
     | 
| 
       1027 
     | 
    
         
            -
                token_observer = TokenUsageObserver()
         
     | 
| 
       1028 
     | 
    
         
            -
             
     | 
| 
       1029 
     | 
    
         
            -
                workflow = (
         
     | 
| 
       1030 
     | 
    
         
            -
                    Workflow("validate_order")
         
     | 
| 
       1031 
     | 
    
         
            -
                    .add_observer(progress_monitor)
         
     | 
| 
       1032 
     | 
    
         
            -
                    .add_observer(token_observer)
         
     | 
| 
       1033 
     | 
    
         
            -
                    .node("validate_order", inputs_mapping={"order": "customer_order"})
         
     | 
| 
       1034 
     | 
    
         
            -
                    .node("transform_items")
         
     | 
| 
       1035 
     | 
    
         
            -
                    .node("format_order_message", inputs_mapping={
         
     | 
| 
       1036 
     | 
    
         
            -
                        "items": "items",
         
     | 
| 
       1037 
     | 
    
         
            -
                        "template": "Custom order: {{ items | join(', ') }}"
         
     | 
| 
       1038 
     | 
    
         
            -
                    })
         
     | 
| 
       1039 
     | 
    
         
            -
                    .node("check_inventory", inputs_mapping={
         
     | 
| 
       1040 
     | 
    
         
            -
                        "model": lambda ctx: "gemini/gemini-2.0-flash",
         
     | 
| 
       1041 
     | 
    
         
            -
                        "items": "transformed_items",
         
     | 
| 
       1042 
     | 
    
         
            -
                        "temperature": 0.5,
         
     | 
| 
       1043 
     | 
    
         
            -
                        "max_tokens": 1000
         
     | 
| 
       1044 
     | 
    
         
            -
                    })
         
     | 
| 
       1045 
     | 
    
         
            -
                    .add_sub_workflow(
         
     | 
| 
       1046 
     | 
    
         
            -
                        "payment_shipping",
         
     | 
| 
       1047 
     | 
    
         
            -
                        payment_shipping_sub_wf,
         
     | 
| 
       1048 
     | 
    
         
            -
                        inputs={"order": lambda ctx: {"items": ctx["items"]}},
         
     | 
| 
       1049 
     | 
    
         
            -
                        output="shipping_confirmation"
         
     | 
| 
       1050 
     | 
    
         
            -
                    )
         
     | 
| 
       1051 
     | 
    
         
            -
                    .branch(
         
     | 
| 
       1052 
     | 
    
         
            -
                        [
         
     | 
| 
       1053 
     | 
    
         
            -
                            ("payment_shipping", lambda ctx: len(ctx.get("inventory_status").items_out_of_stock) == 0 if ctx.get("inventory_status") else False),
         
     | 
| 
       1054 
     | 
    
         
            -
                            ("notify_customer_out_of_stock", lambda ctx: len(ctx.get("inventory_status").items_out_of_stock) > 0 if ctx.get("inventory_status") else True)
         
     | 
| 
       1055 
     | 
    
         
            -
                        ],
         
     | 
| 
       1056 
     | 
    
         
            -
                        next_node="update_order_status"
         
     | 
| 
       1057 
     | 
    
         
            -
                    )
         
     | 
| 
       1058 
     | 
    
         
            -
                    .converge("update_order_status")
         
     | 
| 
       1059 
     | 
    
         
            -
                    .sequence("update_order_status", "send_confirmation_email")
         
     | 
| 
       1060 
     | 
    
         
            -
                )
         
     | 
| 
       1061 
     | 
    
         
            -
             
     | 
| 
       1062 
     | 
    
         
            -
                initial_context = {"customer_order": {"items": ["item1", "item2"]}, "items": ["item1", "item2"]}
         
     | 
| 
       1063 
     | 
    
         
            -
                engine = workflow.build()
         
     | 
| 
       1064 
     | 
    
         
            -
                result = await engine.run(initial_context)
         
     | 
| 
       1065 
     | 
    
         
            -
                logger.info(f"Workflow result: {result}")
         
     | 
| 
       1066 
     | 
    
         
            -
             
     | 
| 
       1067 
     | 
    
         
            -
             
     | 
| 
       1068 
     | 
    
         
            -
            if __name__ == "__main__":
         
     | 
| 
       1069 
     | 
    
         
            -
                logger.info("Initializing Quantalogic Flow Package")
         
     | 
| 
       1070 
     | 
    
         
            -
                asyncio.run(example_workflow())
         
     |