quantalogic 0.50.29__py3-none-any.whl → 0.51.0__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/flow.py +9 -7
- quantalogic/flow/flow_extractor.py +5 -2
- quantalogic/flow/flow_generator.py +5 -2
- quantalogic/flow/flow_manager.py +61 -15
- quantalogic/flow/flow_manager_schema.py +1 -1
- quantalogic/flow/flow_yaml.md +349 -262
- quantalogic-0.51.0.dist-info/METADATA +700 -0
- {quantalogic-0.50.29.dist-info → quantalogic-0.51.0.dist-info}/RECORD +11 -11
- quantalogic-0.50.29.dist-info/METADATA +0 -554
- {quantalogic-0.50.29.dist-info → quantalogic-0.51.0.dist-info}/LICENSE +0 -0
- {quantalogic-0.50.29.dist-info → quantalogic-0.51.0.dist-info}/WHEEL +0 -0
- {quantalogic-0.50.29.dist-info → quantalogic-0.51.0.dist-info}/entry_points.txt +0 -0
quantalogic/flow/flow.py
CHANGED
@@ -71,7 +71,7 @@ class WorkflowEngine:
|
|
71
71
|
def __init__(self, workflow, parent_engine: Optional["WorkflowEngine"] = None):
|
72
72
|
"""Initialize the WorkflowEngine with a workflow and optional parent for sub-workflows."""
|
73
73
|
self.workflow = workflow
|
74
|
-
self.context = {}
|
74
|
+
self.context: Dict[str, Any] = {}
|
75
75
|
self.observers: List[WorkflowObserver] = []
|
76
76
|
self.parent_engine = parent_engine # Link to parent engine for sub-workflow observer propagation
|
77
77
|
|
@@ -302,7 +302,7 @@ class Workflow:
|
|
302
302
|
|
303
303
|
|
304
304
|
class Nodes:
|
305
|
-
NODE_REGISTRY = {} # Registry to hold node functions and metadata
|
305
|
+
NODE_REGISTRY: Dict[str, Tuple[Callable, List[str], Optional[str]]] = {} # Registry to hold node functions and metadata
|
306
306
|
|
307
307
|
@classmethod
|
308
308
|
def define(cls, output: Optional[str] = None):
|
@@ -485,8 +485,8 @@ async def example_workflow():
|
|
485
485
|
# Define Pydantic model for structured output
|
486
486
|
class OrderDetails(BaseModel):
|
487
487
|
order_id: str
|
488
|
-
|
489
|
-
|
488
|
+
items_in_stock: List[str]
|
489
|
+
items_out_of_stock: List[str]
|
490
490
|
|
491
491
|
# Define an example observer for progress
|
492
492
|
async def progress_monitor(event: WorkflowEvent):
|
@@ -533,7 +533,9 @@ async def example_workflow():
|
|
533
533
|
output="inventory_status",
|
534
534
|
)
|
535
535
|
async def check_inventory(items: List[str]) -> OrderDetails:
|
536
|
-
|
536
|
+
# This is a placeholder function that would normally call an LLM
|
537
|
+
# The actual implementation is handled by the structured_llm_node decorator
|
538
|
+
return OrderDetails(order_id="123", items_in_stock=["item1"], items_out_of_stock=[])
|
537
539
|
|
538
540
|
@Nodes.define(output="payment_status")
|
539
541
|
async def process_payment(order: Dict[str, Any]) -> str:
|
@@ -572,11 +574,11 @@ async def example_workflow():
|
|
572
574
|
.sequence("validate_order", "check_inventory")
|
573
575
|
.then(
|
574
576
|
"payment_shipping",
|
575
|
-
condition=lambda ctx: ctx.get("inventory_status").
|
577
|
+
condition=lambda ctx: len(ctx.get("inventory_status").items_out_of_stock) == 0 if ctx.get("inventory_status") else False,
|
576
578
|
)
|
577
579
|
.then(
|
578
580
|
"notify_customer_out_of_stock",
|
579
|
-
condition=lambda ctx:
|
581
|
+
condition=lambda ctx: len(ctx.get("inventory_status").items_out_of_stock) > 0 if ctx.get("inventory_status") else True,
|
580
582
|
)
|
581
583
|
.parallel("update_order_status", "send_confirmation_email")
|
582
584
|
.node("update_order_status")
|
@@ -556,14 +556,17 @@ def generate_executable_script(workflow_def: WorkflowDefinition, global_vars: di
|
|
556
556
|
# Embed functions from workflow_def
|
557
557
|
for func_name, func_def in workflow_def.functions.items():
|
558
558
|
if func_def.type == "embedded":
|
559
|
-
|
559
|
+
if func_def.code is not None:
|
560
|
+
f.write(func_def.code + "\n\n")
|
561
|
+
else:
|
562
|
+
f.write("\n\n")
|
560
563
|
|
561
564
|
# Define workflow using chaining syntax
|
562
565
|
f.write("# Define the workflow using simplified syntax with automatic node registration\n")
|
563
566
|
f.write("workflow = (\n")
|
564
567
|
f.write(f' Workflow("{workflow_def.workflow.start}")\n')
|
565
568
|
for trans in workflow_def.workflow.transitions:
|
566
|
-
|
569
|
+
_from_node = trans.from_
|
567
570
|
to_node = trans.to
|
568
571
|
condition = trans.condition or "None"
|
569
572
|
if condition != "None":
|
@@ -50,14 +50,17 @@ def generate_executable_script(workflow_def: WorkflowDefinition, global_vars: di
|
|
50
50
|
# Embed functions from workflow_def
|
51
51
|
for func_name, func_def in workflow_def.functions.items():
|
52
52
|
if func_def.type == "embedded":
|
53
|
-
|
53
|
+
if func_def.code is not None:
|
54
|
+
f.write(func_def.code + "\n\n")
|
55
|
+
else:
|
56
|
+
f.write("\n\n")
|
54
57
|
|
55
58
|
# Define workflow using chaining syntax
|
56
59
|
f.write("# Define the workflow using simplified syntax with automatic node registration\n")
|
57
60
|
f.write("workflow = (\n")
|
58
61
|
f.write(f' Workflow("{workflow_def.workflow.start}")\n')
|
59
62
|
for trans in workflow_def.workflow.transitions:
|
60
|
-
|
63
|
+
_from_node = trans.from_
|
61
64
|
to_node = trans.to
|
62
65
|
condition = trans.condition or "None"
|
63
66
|
if condition != "None":
|
quantalogic/flow/flow_manager.py
CHANGED
@@ -8,7 +8,7 @@ import urllib
|
|
8
8
|
from pathlib import Path
|
9
9
|
from typing import Any, Callable, Dict, List, Optional, Type, Union
|
10
10
|
|
11
|
-
import yaml
|
11
|
+
import yaml # type: ignore
|
12
12
|
from loguru import logger
|
13
13
|
from pydantic import BaseModel, ValidationError
|
14
14
|
|
@@ -16,6 +16,7 @@ from pydantic import BaseModel, ValidationError
|
|
16
16
|
from quantalogic.flow.flow import Nodes, Workflow
|
17
17
|
from quantalogic.flow.flow_manager_schema import (
|
18
18
|
FunctionDefinition,
|
19
|
+
LLMConfig,
|
19
20
|
NodeDefinition,
|
20
21
|
TransitionDefinition,
|
21
22
|
WorkflowDefinition,
|
@@ -41,10 +42,13 @@ class WorkflowManager:
|
|
41
42
|
parallel: bool = False,
|
42
43
|
) -> None:
|
43
44
|
"""Add a new node to the workflow definition, supporting sub-workflows and LLM nodes."""
|
45
|
+
# Convert dict to LLMConfig if provided
|
46
|
+
llm_config_obj = LLMConfig(**llm_config) if llm_config is not None else None
|
47
|
+
|
44
48
|
node = NodeDefinition(
|
45
49
|
function=function,
|
46
50
|
sub_workflow=sub_workflow,
|
47
|
-
llm_config=
|
51
|
+
llm_config=llm_config_obj,
|
48
52
|
output=output or (f"{name}_result" if function or llm_config else None),
|
49
53
|
retries=retries,
|
50
54
|
delay=delay,
|
@@ -109,8 +113,13 @@ class WorkflowManager:
|
|
109
113
|
for t in to:
|
110
114
|
if t not in self.workflow.nodes:
|
111
115
|
raise ValueError(f"Target node '{t}' does not exist")
|
112
|
-
#
|
113
|
-
|
116
|
+
# Create TransitionDefinition with named parameters
|
117
|
+
# Create a TransitionDefinition with the correct field names
|
118
|
+
# The field is defined with alias="from" in the schema
|
119
|
+
transition_dict = {"from": from_, "to": to}
|
120
|
+
if condition is not None:
|
121
|
+
transition_dict["condition"] = condition
|
122
|
+
transition = TransitionDefinition.model_validate(transition_dict)
|
114
123
|
self.workflow.workflow.transitions.append(transition)
|
115
124
|
|
116
125
|
def set_start_node(self, name: str) -> None:
|
@@ -174,8 +183,12 @@ class WorkflowManager:
|
|
174
183
|
temp_path = temp_file.name
|
175
184
|
module_name = f"temp_module_{hash(temp_path)}"
|
176
185
|
spec = importlib.util.spec_from_file_location(module_name, temp_path)
|
186
|
+
if spec is None:
|
187
|
+
raise ValueError(f"Failed to create module spec from {temp_path}")
|
177
188
|
module = importlib.util.module_from_spec(spec)
|
178
189
|
sys.modules[module_name] = module
|
190
|
+
if spec.loader is None:
|
191
|
+
raise ValueError(f"Module spec has no loader for {temp_path}")
|
179
192
|
spec.loader.exec_module(module)
|
180
193
|
os.remove(temp_path)
|
181
194
|
return module
|
@@ -186,8 +199,12 @@ class WorkflowManager:
|
|
186
199
|
try:
|
187
200
|
module_name = f"local_module_{hash(source)}"
|
188
201
|
spec = importlib.util.spec_from_file_location(module_name, source)
|
202
|
+
if spec is None:
|
203
|
+
raise ValueError(f"Failed to create module spec from {source}")
|
189
204
|
module = importlib.util.module_from_spec(spec)
|
190
205
|
sys.modules[module_name] = module
|
206
|
+
if spec.loader is None:
|
207
|
+
raise ValueError(f"Module spec has no loader for {source}")
|
191
208
|
spec.loader.exec_module(module)
|
192
209
|
return module
|
193
210
|
except Exception as e:
|
@@ -209,21 +226,40 @@ class WorkflowManager:
|
|
209
226
|
functions: Dict[str, Callable] = {}
|
210
227
|
for func_name, func_def in self.workflow.functions.items():
|
211
228
|
if func_def.type == "embedded":
|
212
|
-
local_scope = {}
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
229
|
+
local_scope: Dict[str, Any] = {}
|
230
|
+
if func_def.code is not None:
|
231
|
+
exec(func_def.code, local_scope)
|
232
|
+
if func_name not in local_scope:
|
233
|
+
raise ValueError(f"Embedded function '{func_name}' not defined in code")
|
234
|
+
functions[func_name] = local_scope[func_name]
|
235
|
+
else:
|
236
|
+
raise ValueError(f"Embedded function '{func_name}' has no code")
|
217
237
|
elif func_def.type == "external":
|
218
238
|
try:
|
239
|
+
if func_def.module is None:
|
240
|
+
raise ValueError(f"External function '{func_name}' has no module specified")
|
219
241
|
module = self.import_module_from_source(func_def.module)
|
242
|
+
if func_def.function is None:
|
243
|
+
raise ValueError(f"External function '{func_name}' has no function name specified")
|
220
244
|
functions[func_name] = getattr(module, func_def.function)
|
221
245
|
except (ImportError, AttributeError) as e:
|
222
246
|
raise ValueError(f"Failed to import external function '{func_name}': {e}")
|
223
247
|
|
248
|
+
# Check if start node is set
|
224
249
|
if not self.workflow.workflow.start:
|
225
250
|
raise ValueError("Start node not set in workflow definition")
|
226
|
-
|
251
|
+
|
252
|
+
# We need to ensure we have a valid string for the start node
|
253
|
+
# First check if it's None and provide a fallback
|
254
|
+
if self.workflow.workflow.start is None:
|
255
|
+
logger.warning("Start node was None, using 'start' as default")
|
256
|
+
start_node_name = "start"
|
257
|
+
else:
|
258
|
+
# Otherwise convert to string
|
259
|
+
start_node_name = str(self.workflow.workflow.start)
|
260
|
+
|
261
|
+
# Create the workflow with a valid start node
|
262
|
+
wf = Workflow(start_node=start_node_name)
|
227
263
|
|
228
264
|
# Register observers
|
229
265
|
for observer_name in self.workflow.observers:
|
@@ -235,7 +271,13 @@ class WorkflowManager:
|
|
235
271
|
sub_workflows: Dict[str, Workflow] = {}
|
236
272
|
for node_name, node_def in self.workflow.nodes.items():
|
237
273
|
if node_def.sub_workflow:
|
238
|
-
|
274
|
+
# Ensure we have a valid start node for the sub-workflow
|
275
|
+
if node_def.sub_workflow.start is None:
|
276
|
+
logger.warning(f"Sub-workflow for node '{node_name}' has no start node, using '{node_name}_start' as default")
|
277
|
+
start_node = f"{node_name}_start"
|
278
|
+
else:
|
279
|
+
start_node = str(node_def.sub_workflow.start)
|
280
|
+
sub_wf = Workflow(start_node=start_node)
|
239
281
|
sub_workflows[node_name] = sub_wf
|
240
282
|
added_sub_nodes = set()
|
241
283
|
for trans in node_def.sub_workflow.transitions:
|
@@ -254,7 +296,9 @@ class WorkflowManager:
|
|
254
296
|
else:
|
255
297
|
sub_wf.then(to_nodes[0], condition=condition)
|
256
298
|
inputs = list(Nodes.NODE_REGISTRY[sub_wf.start_node][1])
|
257
|
-
|
299
|
+
# Ensure output is a string
|
300
|
+
output = node_def.output if node_def.output is not None else f"{node_name}_result"
|
301
|
+
wf.add_sub_workflow(node_name, sub_wf, inputs={k: k for k in inputs}, output=output)
|
258
302
|
elif node_def.function:
|
259
303
|
if node_def.function not in functions:
|
260
304
|
raise ValueError(f"Function '{node_def.function}' for node '{node_name}' not found")
|
@@ -265,13 +309,15 @@ class WorkflowManager:
|
|
265
309
|
elif node_def.llm_config:
|
266
310
|
llm_config = node_def.llm_config
|
267
311
|
# Extract inputs from prompt_template using regex
|
268
|
-
inputs
|
312
|
+
# Extract inputs from prompt_template using regex
|
313
|
+
input_vars = set(re.findall(r"{{\s*([^}]+?)\s*}}", llm_config.prompt_template))
|
269
314
|
cleaned_inputs = set()
|
270
|
-
for input_var in
|
315
|
+
for input_var in input_vars:
|
271
316
|
base_var = re.split(r"\s*[\+\-\*/]\s*", input_var.strip())[0].strip()
|
272
317
|
if base_var.isidentifier():
|
273
318
|
cleaned_inputs.add(base_var)
|
274
|
-
|
319
|
+
# Convert set to list for type compatibility
|
320
|
+
inputs_list: List[str] = list(cleaned_inputs)
|
275
321
|
|
276
322
|
# Define a dummy function to be decorated
|
277
323
|
async def dummy_func(**kwargs):
|
@@ -158,7 +158,7 @@ class WorkflowDefinition(BaseModel):
|
|
158
158
|
)
|
159
159
|
nodes: Dict[str, NodeDefinition] = Field(default_factory=dict, description="Dictionary of node definitions.")
|
160
160
|
workflow: WorkflowStructure = Field(
|
161
|
-
default_factory=WorkflowStructure, description="Main workflow structure with start node and transitions."
|
161
|
+
default_factory=lambda: WorkflowStructure(start=None), description="Main workflow structure with start node and transitions."
|
162
162
|
)
|
163
163
|
observers: List[str] = Field(
|
164
164
|
default_factory=list, description="List of observer function names to monitor workflow execution."
|