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.
@@ -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
- - Embedded functions included directly in the script with node registration.
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
- # Function node: Try NODE_REGISTRY first
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.AsyncFunctionDef) or isinstance(node, ast.FunctionDef):
47
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
52
48
  inputs = [param.arg for param in node.args.args]
53
- initial_context = {input_name: None for input_name in inputs}
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 # If parsing fails, leave context empty
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
- initial_context = {var: None for var in cleaned_inputs}
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 Nodes.NODE_REGISTRY:
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.AsyncFunctionDef) or isinstance(node, ast.FunctionDef):
84
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
83
85
  inputs = [param.arg for param in node.args.args]
84
- initial_context = {input_name: None for input_name in inputs}
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
- # Write the shebang and metadata (exact original style)
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
- # Write necessary imports (matching original)
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
- # Write global variables (preserving original feature)
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
- # Embed functions from workflow_def without decorators
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
- inputs = []
130
- if func_def.code:
131
- try:
132
- tree = ast.parse(func_def.code)
133
- for node in ast.walk(tree):
134
- if isinstance(node, ast.AsyncFunctionDef) or isinstance(node, ast.FunctionDef):
135
- inputs = [param.arg for param in node.args.args]
136
- break
137
- except SyntaxError:
138
- pass
139
- f.write(f"{repr(inputs)}, {repr(output)})\n")
140
-
141
- # Define workflow using chaining syntax (original style with enhancements)
142
- f.write("\n# Define the workflow using simplified syntax with automatic node registration\n")
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
- f.write(f' Workflow("{workflow_def.workflow.start}")\n')
145
- # Add all nodes explicitly
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
- f.write(f' .add_sub_workflow("{node_name}", Workflow("{sub_start}"), ')
150
- inputs = Nodes.NODE_REGISTRY.get(sub_start, ([], None))[0] if sub_start in Nodes.NODE_REGISTRY else []
151
- f.write(f'inputs={{{", ".join(f"{k!r}: {k!r}" for k in inputs)}}}, ')
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
- f.write(f' .node("{node_name}")\n')
155
- # Add transitions (original style preserved)
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
- _from_node = trans.from_node # Original used `_from_node`
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
- f.write(f' .then("{to_node}", condition={condition})\n')
164
- else:
165
- f.write(f' .parallel({", ".join(f"{n!r}" for n in to_node)})\n')
166
- # Add observers (original feature)
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 asynchronous function (updated with inferred context)
244
+ # Main function
173
245
  f.write("async def main():\n")
174
- f.write(' """Main function to run the story generation workflow."""\n')
175
- f.write(" # Customize initial_context as needed based on the workflow's nodes\n")
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(" } # Customize initial_context as needed\n")
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 (original style)
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
- # Add nodes to the workflow
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("start", "end")
221
-
222
- # Get the WorkflowDefinition
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")