quantalogic 0.50.28__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/agent.py CHANGED
@@ -562,17 +562,7 @@ class Agent(BaseModel):
562
562
  Returns:
563
563
  Generated summary text
564
564
  """
565
- prompt_summary = (
566
- "Summarize the conversation concisely:\n"
567
- "format in markdown:\n"
568
- "<thinking>\n"
569
- " - 1. **Completed Steps**: Briefly describe the steps.\n"
570
- " - 2. **Variables Used**: List the variables.\n"
571
- " - 3. **Progress Analysis**: Assess progress.\n"
572
- "</thinking>\n"
573
- "Keep the summary clear and actionable.\n"
574
- )
575
-
565
+ # Format conversation history for the template
576
566
  memory_copy = self.memory.memory.copy()
577
567
 
578
568
  if len(memory_copy) < 3:
@@ -581,6 +571,14 @@ class Agent(BaseModel):
581
571
 
582
572
  user_message = memory_copy.pop()
583
573
  assistant_message = memory_copy.pop()
574
+
575
+ # Create summarization prompt using template
576
+ prompt_summary = self._render_template('memory_compaction_prompt.j2',
577
+ conversation_history="\n\n".join(
578
+ f"[{msg.role.upper()}]: {msg.content}"
579
+ for msg in memory_copy
580
+ ))
581
+
584
582
  summary = await self.model.async_generate_with_history(messages_history=memory_copy, prompt=prompt_summary)
585
583
 
586
584
  # Remove last system message if present
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
- items: List[str]
489
- in_stock: bool
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
- pass
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").in_stock if ctx.get("inventory_status") else False,
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: not ctx.get("inventory_status").in_stock if ctx.get("inventory_status") else True,
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
- f.write(func_def.code + "\n\n")
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
- from_node = trans.from_
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
- f.write(func_def.code + "\n\n")
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
- from_node = trans.from_
63
+ _from_node = trans.from_
61
64
  to_node = trans.to
62
65
  condition = trans.condition or "None"
63
66
  if condition != "None":
@@ -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=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
- # Use 'from' field name instead of the alias 'from_'
113
- transition = TransitionDefinition(**{"from": from_, "to": to, "condition": condition})
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
- exec(func_def.code, local_scope)
214
- if func_name not in local_scope:
215
- raise ValueError(f"Embedded function '{func_name}' not defined in code")
216
- functions[func_name] = local_scope[func_name]
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
- wf = Workflow(start_node=self.workflow.workflow.start)
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
- sub_wf = Workflow(node_def.sub_workflow.start)
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
- wf.add_sub_workflow(node_name, sub_wf, inputs={k: k for k in inputs}, output=node_def.output)
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 = set(re.findall(r"{{\s*([^}]+?)\s*}}", llm_config.prompt_template))
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 inputs:
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
- inputs_list = list(cleaned_inputs)
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."