quantalogic 0.53.0__py3-none-any.whl → 0.56.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/__init__.py +7 -0
- quantalogic/flow/flow.py +267 -80
- quantalogic/flow/flow_extractor.py +216 -87
- quantalogic/flow/flow_generator.py +157 -88
- quantalogic/flow/flow_manager.py +252 -125
- quantalogic/flow/flow_manager_schema.py +62 -43
- quantalogic/flow/flow_mermaid.py +151 -68
- quantalogic/flow/flow_validator.py +204 -77
- quantalogic/flow/flow_yaml.md +341 -156
- quantalogic/tools/safe_python_interpreter_tool.py +6 -1
- quantalogic/xml_parser.py +5 -1
- quantalogic/xml_tool_parser.py +4 -1
- {quantalogic-0.53.0.dist-info → quantalogic-0.56.0.dist-info}/METADATA +16 -6
- {quantalogic-0.53.0.dist-info → quantalogic-0.56.0.dist-info}/RECORD +17 -17
- {quantalogic-0.53.0.dist-info → quantalogic-0.56.0.dist-info}/LICENSE +0 -0
- {quantalogic-0.53.0.dist-info → quantalogic-0.56.0.dist-info}/WHEEL +0 -0
- {quantalogic-0.53.0.dist-info → quantalogic-0.56.0.dist-info}/entry_points.txt +0 -0
@@ -4,7 +4,7 @@ import re
|
|
4
4
|
from typing import Dict, Optional
|
5
5
|
|
6
6
|
from quantalogic.flow.flow import Nodes # Import Nodes to access NODE_REGISTRY
|
7
|
-
from quantalogic.flow.flow_manager_schema import WorkflowDefinition
|
7
|
+
from quantalogic.flow.flow_manager_schema import BranchCondition, WorkflowDefinition # Added BranchCondition import
|
8
8
|
|
9
9
|
|
10
10
|
def generate_executable_script(
|
@@ -14,20 +14,21 @@ def generate_executable_script(
|
|
14
14
|
initial_context: Optional[Dict[str, object]] = None,
|
15
15
|
) -> None:
|
16
16
|
"""
|
17
|
-
Generate an executable Python script from a WorkflowDefinition with global variables.
|
17
|
+
Generate an executable Python script from a WorkflowDefinition with global variables using decorators.
|
18
18
|
|
19
19
|
Args:
|
20
20
|
workflow_def: The WorkflowDefinition object containing the workflow details.
|
21
21
|
global_vars: Dictionary of global variables extracted from the source file.
|
22
22
|
output_file: The path where the executable script will be written.
|
23
|
-
initial_context: Optional initial context; if None, inferred from the workflow.
|
23
|
+
initial_context: Optional initial context; if None, inferred from the workflow with default values.
|
24
24
|
|
25
25
|
The generated script includes:
|
26
26
|
- A shebang using `uv run` for environment management.
|
27
27
|
- Metadata specifying the required Python version and dependencies.
|
28
28
|
- Global variables from the original script.
|
29
|
-
-
|
30
|
-
- Workflow instantiation using direct chaining syntax.
|
29
|
+
- Functions defined with appropriate Nodes decorators (e.g., @Nodes.define, @Nodes.llm_node).
|
30
|
+
- Workflow instantiation using direct chaining syntax with function names, including branch and converge.
|
31
|
+
- Support for input mappings and template nodes via workflow configuration and decorators.
|
31
32
|
- A default initial_context inferred from the workflow with customization guidance.
|
32
33
|
"""
|
33
34
|
# Infer initial context if not provided
|
@@ -37,25 +38,21 @@ def generate_executable_script(
|
|
37
38
|
if start_node and start_node in workflow_def.nodes:
|
38
39
|
node_def = workflow_def.nodes[start_node]
|
39
40
|
if node_def.function:
|
40
|
-
|
41
|
-
if start_node in Nodes.NODE_REGISTRY:
|
42
|
-
inputs = Nodes.NODE_REGISTRY[start_node][1]
|
43
|
-
initial_context = {input_name: None for input_name in inputs}
|
44
|
-
# Fallback: Parse embedded function code
|
45
|
-
elif node_def.function in workflow_def.functions:
|
41
|
+
if node_def.function in workflow_def.functions:
|
46
42
|
func_def = workflow_def.functions[node_def.function]
|
47
43
|
if func_def.type == "embedded" and func_def.code:
|
48
44
|
try:
|
49
45
|
tree = ast.parse(func_def.code)
|
50
46
|
for node in ast.walk(tree):
|
51
|
-
if isinstance(node, ast.
|
47
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
52
48
|
inputs = [param.arg for param in node.args.args]
|
53
|
-
|
49
|
+
# Assign default values: empty string for strings, 0 for numbers, etc.
|
50
|
+
for input_name in inputs:
|
51
|
+
initial_context[input_name] = "" # Default to empty string for simplicity
|
54
52
|
break
|
55
53
|
except SyntaxError:
|
56
|
-
pass
|
54
|
+
pass
|
57
55
|
elif node_def.llm_config:
|
58
|
-
# LLM node: Parse prompt template for variables
|
59
56
|
prompt = node_def.llm_config.prompt_template or ""
|
60
57
|
input_vars = set(re.findall(r"{{\s*([^}]+?)\s*}}", prompt))
|
61
58
|
cleaned_inputs = {
|
@@ -63,15 +60,20 @@ def generate_executable_script(
|
|
63
60
|
for var in input_vars
|
64
61
|
if var.strip().isidentifier()
|
65
62
|
}
|
66
|
-
|
63
|
+
for var in cleaned_inputs:
|
64
|
+
initial_context[var] = ""
|
65
|
+
elif node_def.template_config:
|
66
|
+
template = node_def.template_config.template or ""
|
67
|
+
input_vars = set(re.findall(r"{{\s*([^}]+?)\s*}}", template))
|
68
|
+
cleaned_inputs = {
|
69
|
+
re.split(r"\s*[\+\-\*/]\s*", var.strip())[0].strip()
|
70
|
+
for var in input_vars
|
71
|
+
if var.strip().isidentifier()
|
72
|
+
}
|
73
|
+
initial_context = {"rendered_content": "", **{var: "" for var in cleaned_inputs}}
|
67
74
|
elif node_def.sub_workflow:
|
68
|
-
# Sub-workflow: Infer from sub-workflow's start node
|
69
75
|
sub_start = node_def.sub_workflow.start or f"{start_node}_start"
|
70
|
-
if sub_start in
|
71
|
-
inputs = Nodes.NODE_REGISTRY[sub_start][1]
|
72
|
-
initial_context = {input_name: None for input_name in inputs}
|
73
|
-
# Fallback: Check sub-workflow's start node function
|
74
|
-
elif sub_start in workflow_def.nodes:
|
76
|
+
if sub_start in workflow_def.nodes:
|
75
77
|
sub_node_def = workflow_def.nodes[sub_start]
|
76
78
|
if sub_node_def.function in workflow_def.functions:
|
77
79
|
func_def = workflow_def.functions[sub_node_def.function]
|
@@ -79,15 +81,21 @@ def generate_executable_script(
|
|
79
81
|
try:
|
80
82
|
tree = ast.parse(func_def.code)
|
81
83
|
for node in ast.walk(tree):
|
82
|
-
if isinstance(node, ast.
|
84
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
83
85
|
inputs = [param.arg for param in node.args.args]
|
84
|
-
|
86
|
+
for input_name in inputs:
|
87
|
+
initial_context[input_name] = ""
|
85
88
|
break
|
86
89
|
except SyntaxError:
|
87
90
|
pass
|
91
|
+
# Apply inputs_mapping if present
|
92
|
+
if node_def.inputs_mapping:
|
93
|
+
for key, value in node_def.inputs_mapping.items():
|
94
|
+
if not value.startswith("lambda ctx:"): # Only static mappings contribute to context
|
95
|
+
initial_context[value] = ""
|
88
96
|
|
89
97
|
with open(output_file, "w") as f:
|
90
|
-
#
|
98
|
+
# Shebang and metadata
|
91
99
|
f.write("#!/usr/bin/env -S uv run\n")
|
92
100
|
f.write("# /// script\n")
|
93
101
|
f.write('# requires-python = ">=3.12"\n')
|
@@ -102,128 +110,189 @@ def generate_executable_script(
|
|
102
110
|
f.write("# ]\n")
|
103
111
|
f.write("# ///\n\n")
|
104
112
|
|
105
|
-
#
|
113
|
+
# Imports
|
106
114
|
f.write("import anyio\n")
|
107
115
|
f.write("from typing import List\n")
|
108
116
|
f.write("from loguru import logger\n")
|
109
117
|
f.write("from quantalogic.flow import Nodes, Workflow\n\n")
|
110
118
|
|
111
|
-
#
|
119
|
+
# Global variables
|
112
120
|
for var_name, value in global_vars.items():
|
113
121
|
f.write(f"{var_name} = {repr(value)}\n")
|
114
122
|
f.write("\n")
|
115
123
|
|
116
|
-
#
|
117
|
-
for func_name, func_def in workflow_def.functions.items():
|
118
|
-
if func_def.type == "embedded" and func_def.code:
|
119
|
-
f.write(func_def.code + "\n\n")
|
120
|
-
|
121
|
-
# Register nodes explicitly with their intended names
|
122
|
-
f.write("# Register nodes with their workflow names\n")
|
124
|
+
# Define functions with decorators
|
123
125
|
for node_name, node_def in workflow_def.nodes.items():
|
124
126
|
if node_def.function and node_def.function in workflow_def.functions:
|
125
|
-
output = node_def.output or f"{node_name}_result"
|
126
|
-
f.write(f"Nodes.NODE_REGISTRY['{node_name}'] = (greet if '{node_name}' == 'start' else end, ")
|
127
|
-
# Extract inputs using ast parsing
|
128
127
|
func_def = workflow_def.functions[node_def.function]
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
128
|
+
if func_def.type == "embedded" and func_def.code:
|
129
|
+
# Strip original decorator and apply new one
|
130
|
+
code_lines = func_def.code.split('\n')
|
131
|
+
func_body = ""
|
132
|
+
for line in code_lines:
|
133
|
+
if line.strip().startswith('@Nodes.'):
|
134
|
+
continue # Skip original decorator
|
135
|
+
func_body += line + "\n"
|
136
|
+
func_body = func_body.rstrip("\n")
|
137
|
+
|
138
|
+
# Generate new decorator based on node type
|
139
|
+
decorator = ""
|
140
|
+
if node_def.llm_config:
|
141
|
+
params = [f"model={repr(node_def.llm_config.model)}"]
|
142
|
+
if node_def.llm_config.system_prompt:
|
143
|
+
params.append(f"system_prompt={repr(node_def.llm_config.system_prompt)}")
|
144
|
+
if node_def.llm_config.prompt_template:
|
145
|
+
params.append(f"prompt_template={repr(node_def.llm_config.prompt_template)}")
|
146
|
+
if node_def.llm_config.prompt_file:
|
147
|
+
params.append(f"prompt_file={repr(node_def.llm_config.prompt_file)}")
|
148
|
+
params.append(f"output={repr(node_def.output or f'{node_name}_result')}")
|
149
|
+
for param in ["temperature", "max_tokens", "top_p", "presence_penalty", "frequency_penalty"]:
|
150
|
+
value = getattr(node_def.llm_config, param, None)
|
151
|
+
if value is not None:
|
152
|
+
params.append(f"{param}={repr(value)}")
|
153
|
+
decorator = f"@Nodes.llm_node({', '.join(params)})\n"
|
154
|
+
elif node_def.template_config:
|
155
|
+
params = [f"output={repr(node_def.output or f'{node_name}_result')}"]
|
156
|
+
if node_def.template_config.template:
|
157
|
+
params.append(f"template={repr(node_def.template_config.template)}")
|
158
|
+
if node_def.template_config.template_file:
|
159
|
+
params.append(f"template_file={repr(node_def.template_config.template_file)}")
|
160
|
+
decorator = f"@Nodes.template_node({', '.join(params)})\n"
|
161
|
+
else:
|
162
|
+
decorator = f"@Nodes.define(output={repr(node_def.output or f'{node_name}_result')})\n"
|
163
|
+
# Write function with new decorator
|
164
|
+
f.write(f"{decorator}{func_body}\n\n")
|
165
|
+
|
166
|
+
# Define workflow using function names
|
167
|
+
f.write("# Define the workflow with branch and converge support\n")
|
143
168
|
f.write("workflow = (\n")
|
144
|
-
|
145
|
-
|
169
|
+
start_node = workflow_def.workflow.start
|
170
|
+
start_func = workflow_def.nodes[start_node].function if start_node in workflow_def.nodes and workflow_def.nodes[start_node].function else start_node
|
171
|
+
f.write(f' Workflow("{start_func}")\n')
|
172
|
+
|
146
173
|
for node_name, node_def in workflow_def.nodes.items():
|
174
|
+
func_name = node_def.function if node_def.function else node_name
|
147
175
|
if node_def.sub_workflow:
|
148
176
|
sub_start = node_def.sub_workflow.start or f"{node_name}_start"
|
149
|
-
|
150
|
-
|
151
|
-
|
177
|
+
sub_start_func = workflow_def.nodes[sub_start].function if sub_start in workflow_def.nodes and workflow_def.nodes[sub_start].function else sub_start
|
178
|
+
f.write(f' .add_sub_workflow("{node_name}", Workflow("{sub_start_func}"), ')
|
179
|
+
if node_def.inputs_mapping:
|
180
|
+
inputs_mapping_str = "{"
|
181
|
+
for k, v in node_def.inputs_mapping.items():
|
182
|
+
if v.startswith("lambda ctx:"):
|
183
|
+
inputs_mapping_str += f"{repr(k)}: {v}, "
|
184
|
+
else:
|
185
|
+
inputs_mapping_str += f"{repr(k)}: {repr(v)}, "
|
186
|
+
inputs_mapping_str = inputs_mapping_str.rstrip(", ") + "}"
|
187
|
+
f.write(f"inputs={inputs_mapping_str}, ")
|
188
|
+
else:
|
189
|
+
inputs = []
|
190
|
+
if sub_start in workflow_def.nodes and workflow_def.nodes[sub_start].function in workflow_def.functions:
|
191
|
+
func_def = workflow_def.functions[workflow_def.nodes[sub_start].function]
|
192
|
+
if func_def.code:
|
193
|
+
try:
|
194
|
+
tree = ast.parse(func_def.code)
|
195
|
+
for node in ast.walk(tree):
|
196
|
+
if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
|
197
|
+
inputs = [param.arg for param in node.args.args]
|
198
|
+
break
|
199
|
+
except SyntaxError:
|
200
|
+
pass
|
201
|
+
f.write(f'inputs={{{", ".join(f"{k!r}: {k!r}" for k in inputs)}}}, ')
|
152
202
|
f.write(f'output="{node_def.output or f"{node_name}_result"}")\n')
|
153
203
|
else:
|
154
|
-
|
155
|
-
|
204
|
+
if node_def.inputs_mapping:
|
205
|
+
inputs_mapping_str = "{"
|
206
|
+
for k, v in node_def.inputs_mapping.items():
|
207
|
+
if v.startswith("lambda ctx:"):
|
208
|
+
inputs_mapping_str += f"{repr(k)}: {v}, "
|
209
|
+
else:
|
210
|
+
inputs_mapping_str += f"{repr(k)}: {repr(v)}, "
|
211
|
+
inputs_mapping_str = inputs_mapping_str.rstrip(", ") + "}"
|
212
|
+
f.write(f' .node("{func_name}", inputs_mapping={inputs_mapping_str})\n')
|
213
|
+
else:
|
214
|
+
f.write(f' .node("{func_name}")\n')
|
215
|
+
|
156
216
|
for trans in workflow_def.workflow.transitions:
|
157
|
-
|
217
|
+
from_node = trans.from_node
|
218
|
+
from_func = workflow_def.nodes[from_node].function if from_node in workflow_def.nodes and workflow_def.nodes[from_node].function else from_node
|
158
219
|
to_node = trans.to_node
|
159
|
-
condition = trans.condition or "None"
|
160
|
-
if condition != "None" and not condition.startswith("lambda ctx:"):
|
161
|
-
condition = f"lambda ctx: {condition}"
|
162
220
|
if isinstance(to_node, str):
|
163
|
-
|
164
|
-
|
165
|
-
f.write(f' .
|
166
|
-
|
221
|
+
to_func = workflow_def.nodes[to_node].function if to_node in workflow_def.nodes and workflow_def.nodes[to_node].function else to_node
|
222
|
+
condition = f"lambda ctx: {trans.condition}" if trans.condition else "None"
|
223
|
+
f.write(f' .then("{to_func}", condition={condition})\n')
|
224
|
+
elif all(isinstance(tn, str) for tn in to_node):
|
225
|
+
to_funcs = [workflow_def.nodes[tn].function if tn in workflow_def.nodes and workflow_def.nodes[tn].function else tn for tn in to_node]
|
226
|
+
f.write(f' .parallel({", ".join(f"{n!r}" for n in to_funcs)})\n')
|
227
|
+
else: # BranchCondition list
|
228
|
+
branches = []
|
229
|
+
for branch in to_node:
|
230
|
+
branch_func = workflow_def.nodes[branch.to_node].function if branch.to_node in workflow_def.nodes and workflow_def.nodes[branch.to_node].function else branch.to_node
|
231
|
+
cond = f"lambda ctx: {branch.condition}" if branch.condition else "None"
|
232
|
+
branches.append(f'("{branch_func}", {cond})')
|
233
|
+
f.write(f' .branch([{", ".join(branches)}])\n')
|
234
|
+
|
235
|
+
for conv_node in workflow_def.workflow.convergence_nodes:
|
236
|
+
conv_func = workflow_def.nodes[conv_node].function if conv_node in workflow_def.nodes and workflow_def.nodes[conv_node].function else conv_node
|
237
|
+
f.write(f' .converge("{conv_func}")\n')
|
238
|
+
|
167
239
|
if hasattr(workflow_def, 'observers'):
|
168
240
|
for observer in workflow_def.observers:
|
169
241
|
f.write(f" .add_observer({observer})\n")
|
170
242
|
f.write(")\n\n")
|
171
243
|
|
172
|
-
# Main
|
244
|
+
# Main function
|
173
245
|
f.write("async def main():\n")
|
174
|
-
f.write(' """Main function to run the
|
175
|
-
f.write(" # Customize initial_context as needed
|
246
|
+
f.write(' """Main function to run the workflow."""\n')
|
247
|
+
f.write(" # Customize initial_context as needed\n")
|
176
248
|
f.write(" # Inferred required inputs:\n")
|
177
249
|
inferred_inputs = list(initial_context.keys())
|
178
250
|
f.write(f" # {', '.join(inferred_inputs) if inferred_inputs else 'None detected'}\n")
|
179
251
|
f.write(" initial_context = {\n")
|
180
252
|
for key, value in initial_context.items():
|
181
253
|
f.write(f" {repr(key)}: {repr(value)},\n")
|
182
|
-
f.write(" }
|
254
|
+
f.write(" }\n")
|
183
255
|
f.write(" engine = workflow.build()\n")
|
184
256
|
f.write(" result = await engine.run(initial_context)\n")
|
185
257
|
f.write(' logger.info(f"Workflow result: {result}")\n\n')
|
186
258
|
|
187
|
-
# Entry point
|
259
|
+
# Entry point
|
188
260
|
f.write('if __name__ == "__main__":\n')
|
189
261
|
f.write(" anyio.run(main)\n")
|
190
262
|
|
191
|
-
# Set executable permissions (original feature)
|
192
263
|
os.chmod(output_file, 0o755)
|
193
264
|
|
194
265
|
|
195
|
-
# Example usage (consistent with original structure)
|
196
266
|
if __name__ == "__main__":
|
197
267
|
from quantalogic.flow.flow_manager import WorkflowManager
|
198
268
|
|
199
|
-
# Create the workflow manager
|
200
269
|
manager = WorkflowManager()
|
201
|
-
|
202
|
-
# Define and add functions
|
203
270
|
manager.add_function(
|
204
271
|
name="greet",
|
205
272
|
type_="embedded",
|
206
273
|
code="async def greet(name): return f'Hello, {name}!'",
|
207
274
|
)
|
275
|
+
manager.add_function(
|
276
|
+
name="check",
|
277
|
+
type_="embedded",
|
278
|
+
code="async def check(name): return len(name) > 3",
|
279
|
+
)
|
208
280
|
manager.add_function(
|
209
281
|
name="end",
|
210
282
|
type_="embedded",
|
211
283
|
code="async def end(greeting): return f'{greeting} Goodbye!'",
|
212
284
|
)
|
213
|
-
|
214
|
-
|
215
|
-
manager.add_node(name="start", function="greet", output="greeting")
|
285
|
+
manager.add_node(name="start", function="greet", output="greeting", inputs_mapping={"name": "user_name"})
|
286
|
+
manager.add_node(name="check", function="check", output="condition")
|
216
287
|
manager.add_node(name="end", function="end", output="farewell")
|
217
|
-
|
218
|
-
# Set start node and transitions
|
219
288
|
manager.set_start_node("start")
|
220
|
-
manager.add_transition(
|
221
|
-
|
222
|
-
|
289
|
+
manager.add_transition(
|
290
|
+
from_node="start",
|
291
|
+
to_node=[
|
292
|
+
BranchCondition(to_node="check", condition="ctx['name'] == 'Alice'")
|
293
|
+
]
|
294
|
+
)
|
295
|
+
manager.add_convergence_node("end")
|
223
296
|
wf_def = manager.workflow
|
224
|
-
|
225
|
-
# Define global variables
|
226
297
|
global_vars = {"MY_CONSTANT": 42}
|
227
|
-
|
228
|
-
# Generate the script with inferred context
|
229
298
|
generate_executable_script(wf_def, global_vars, "workflow_script.py")
|