quantalogic 0.61.2__py3-none-any.whl → 0.80__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.
Files changed (67) hide show
  1. quantalogic/agent.py +0 -1
  2. quantalogic/codeact/TODO.md +14 -0
  3. quantalogic/codeact/agent.py +400 -421
  4. quantalogic/codeact/cli.py +42 -224
  5. quantalogic/codeact/cli_commands/__init__.py +0 -0
  6. quantalogic/codeact/cli_commands/create_toolbox.py +45 -0
  7. quantalogic/codeact/cli_commands/install_toolbox.py +20 -0
  8. quantalogic/codeact/cli_commands/list_executor.py +15 -0
  9. quantalogic/codeact/cli_commands/list_reasoners.py +15 -0
  10. quantalogic/codeact/cli_commands/list_toolboxes.py +47 -0
  11. quantalogic/codeact/cli_commands/task.py +215 -0
  12. quantalogic/codeact/cli_commands/tool_info.py +24 -0
  13. quantalogic/codeact/cli_commands/uninstall_toolbox.py +43 -0
  14. quantalogic/codeact/config.yaml +21 -0
  15. quantalogic/codeact/constants.py +1 -1
  16. quantalogic/codeact/events.py +12 -5
  17. quantalogic/codeact/examples/README.md +342 -0
  18. quantalogic/codeact/examples/agent_sample.yaml +29 -0
  19. quantalogic/codeact/executor.py +186 -0
  20. quantalogic/codeact/history_manager.py +94 -0
  21. quantalogic/codeact/llm_util.py +3 -22
  22. quantalogic/codeact/plugin_manager.py +92 -0
  23. quantalogic/codeact/prompts/generate_action.j2 +65 -14
  24. quantalogic/codeact/prompts/generate_program.j2 +32 -19
  25. quantalogic/codeact/react_agent.py +318 -0
  26. quantalogic/codeact/reasoner.py +185 -0
  27. quantalogic/codeact/templates/toolbox/README.md.j2 +10 -0
  28. quantalogic/codeact/templates/toolbox/pyproject.toml.j2 +16 -0
  29. quantalogic/codeact/templates/toolbox/tools.py.j2 +6 -0
  30. quantalogic/codeact/templates.py +7 -0
  31. quantalogic/codeact/tools_manager.py +242 -119
  32. quantalogic/codeact/utils.py +16 -89
  33. quantalogic/codeact/xml_utils.py +126 -0
  34. quantalogic/flow/flow.py +151 -41
  35. quantalogic/flow/flow_extractor.py +61 -1
  36. quantalogic/flow/flow_generator.py +34 -6
  37. quantalogic/flow/flow_manager.py +64 -25
  38. quantalogic/flow/flow_manager_schema.py +32 -0
  39. quantalogic/tools/action_gen.py +1 -1
  40. quantalogic/tools/action_gen_safe.py +340 -0
  41. quantalogic/tools/tool.py +531 -109
  42. quantalogic/tools/write_file_tool.py +7 -8
  43. {quantalogic-0.61.2.dist-info → quantalogic-0.80.dist-info}/METADATA +3 -2
  44. {quantalogic-0.61.2.dist-info → quantalogic-0.80.dist-info}/RECORD +47 -42
  45. {quantalogic-0.61.2.dist-info → quantalogic-0.80.dist-info}/WHEEL +1 -1
  46. quantalogic-0.80.dist-info/entry_points.txt +3 -0
  47. quantalogic/python_interpreter/__init__.py +0 -23
  48. quantalogic/python_interpreter/assignment_visitors.py +0 -63
  49. quantalogic/python_interpreter/base_visitors.py +0 -20
  50. quantalogic/python_interpreter/class_visitors.py +0 -22
  51. quantalogic/python_interpreter/comprehension_visitors.py +0 -172
  52. quantalogic/python_interpreter/context_visitors.py +0 -59
  53. quantalogic/python_interpreter/control_flow_visitors.py +0 -88
  54. quantalogic/python_interpreter/exception_visitors.py +0 -109
  55. quantalogic/python_interpreter/exceptions.py +0 -39
  56. quantalogic/python_interpreter/execution.py +0 -202
  57. quantalogic/python_interpreter/function_utils.py +0 -386
  58. quantalogic/python_interpreter/function_visitors.py +0 -209
  59. quantalogic/python_interpreter/import_visitors.py +0 -28
  60. quantalogic/python_interpreter/interpreter_core.py +0 -358
  61. quantalogic/python_interpreter/literal_visitors.py +0 -74
  62. quantalogic/python_interpreter/misc_visitors.py +0 -148
  63. quantalogic/python_interpreter/operator_visitors.py +0 -108
  64. quantalogic/python_interpreter/scope.py +0 -10
  65. quantalogic/python_interpreter/visit_handlers.py +0 -110
  66. quantalogic-0.61.2.dist-info/entry_points.txt +0 -6
  67. {quantalogic-0.61.2.dist-info → quantalogic-0.80.dist-info}/LICENSE +0 -0
@@ -0,0 +1,94 @@
1
+ """History management module for storing and formatting agent steps."""
2
+
3
+ from typing import Dict, List
4
+
5
+ from loguru import logger
6
+ from lxml import etree
7
+
8
+ from .xml_utils import XMLResultHandler
9
+
10
+
11
+ class HistoryManager:
12
+ """Manages the storage and formatting of agent step history with persistent context."""
13
+
14
+ def __init__(self, max_tokens: int = 64*1024, system_prompt: str = "", task_description: str = ""):
15
+ """
16
+ Initialize the HistoryManager with a token limit and persistent context.
17
+
18
+ Args:
19
+ max_tokens (int): Maximum number of tokens for history formatting (default: 16000).
20
+ system_prompt (str): Persistent system-level instructions for the agent (default: "").
21
+ task_description (str): Persistent description of the current task (default: "").
22
+ """
23
+ self.max_tokens: int = max_tokens
24
+ self.system_prompt: str = system_prompt
25
+ self.task_description: str = task_description
26
+ self.store: List[Dict] = []
27
+ logger.debug(f"Initialized HistoryManager with system_prompt: '{system_prompt}', task_description: '{task_description}'")
28
+
29
+ def add_step(self, step_data: Dict) -> None:
30
+ """
31
+ Add a step to the history store.
32
+
33
+ Args:
34
+ step_data (Dict): Dictionary containing step details (step_number, thought, action, result).
35
+ """
36
+ self.store.append(step_data)
37
+ logger.debug(f"Added step {step_data['step_number']} to history")
38
+
39
+ def format_history(self, max_iterations: int) -> str:
40
+ """
41
+ Format the history with available variables, truncating to fit within max_tokens.
42
+
43
+ Args:
44
+ max_iterations (int): Maximum allowed iterations for context.
45
+
46
+ Returns:
47
+ str: Formatted string of previous steps, or "No previous steps" if empty.
48
+ """
49
+ included_steps: List[str] = []
50
+ total_tokens: int = 0
51
+ for step in reversed(self.store):
52
+ try:
53
+ root = etree.fromstring(step['result'])
54
+ vars_elem = root.find("Variables")
55
+ available_vars = (
56
+ [var.get('name') for var in vars_elem.findall("Variable")]
57
+ if vars_elem is not None else []
58
+ )
59
+ except etree.XMLSyntaxError:
60
+ available_vars = []
61
+
62
+ step_str: str = (
63
+ f"===== Step {step['step_number']} of {max_iterations} max =====\n"
64
+ f"Thought:\n{step['thought']}\n\n"
65
+ f"Action:\n{step['action']}\n\n"
66
+ f"Result:\n{XMLResultHandler.format_result_summary(step['result']) if step.get('result') else 'No result available'}\n"
67
+ f"Available variables: {', '.join(available_vars) or 'None'}"
68
+ )
69
+ step_tokens: int = len(step_str.split())
70
+ if total_tokens + step_tokens > self.max_tokens:
71
+ break
72
+ included_steps.append(step_str)
73
+ total_tokens += step_tokens
74
+ return "\n".join(reversed(included_steps)) or "No previous steps"
75
+
76
+ def get_full_context(self, max_iterations: int) -> str:
77
+ """
78
+ Return the full context including system prompt, task description, and formatted history.
79
+
80
+ Args:
81
+ max_iterations (int): Maximum allowed iterations for history formatting.
82
+
83
+ Returns:
84
+ str: Combined string of system prompt, task description, and history.
85
+ """
86
+ context_parts = []
87
+ if self.system_prompt:
88
+ context_parts.append(f"System Prompt:\n{self.system_prompt}")
89
+ if self.task_description:
90
+ context_parts.append(f"Task Description:\n{self.task_description}")
91
+ history_str = self.format_history(max_iterations)
92
+ if history_str != "No previous steps":
93
+ context_parts.append(f"History:\n{history_str}")
94
+ return "\n\n".join(context_parts)
@@ -13,27 +13,8 @@ async def litellm_completion(
13
13
  notify_event: Optional[Callable] = None,
14
14
  **kwargs
15
15
  ) -> str:
16
- """
17
- A wrapper for litellm.acompletion that supports streaming and non-streaming modes.
18
-
19
- Args:
20
- model (str): The model to use (e.g., "gemini/gemini-2.0-flash").
21
- messages (List[dict]): The conversation history as a list of message dictionaries.
22
- max_tokens (int): Maximum number of tokens to generate.
23
- temperature (float): Sampling temperature for the model.
24
- stream (bool): If True, stream tokens; if False, return the full response.
25
- step (Optional[int]): Step number for event tracking (used in streaming mode).
26
- notify_event (Optional[Callable]): Callback to trigger events during streaming.
27
- **kwargs: Additional arguments to pass to litellm.acompletion.
28
-
29
- Returns:
30
- str: The generated response (full text in both modes).
31
-
32
- Raises:
33
- ValueError: If notify_event is missing when stream=True.
34
- Exception: If the completion request fails.
35
- """
36
- from .events import StreamTokenEvent # Local import to avoid circular dependency
16
+ """A wrapper for litellm.acompletion that supports streaming and non-streaming modes."""
17
+ from .events import StreamTokenEvent
37
18
 
38
19
  if stream:
39
20
  if notify_event is None:
@@ -73,4 +54,4 @@ async def litellm_completion(
73
54
  )
74
55
  return response.choices[0].message.content
75
56
  except Exception as e:
76
- raise Exception(f"Completion failed: {e}")
57
+ raise Exception(f"Completion failed: {e}")
@@ -0,0 +1,92 @@
1
+ """Plugin management module for dynamically loading components."""
2
+
3
+ from importlib.metadata import entry_points
4
+ from typing import List
5
+
6
+ from loguru import logger
7
+
8
+ from quantalogic.tools import Tool
9
+
10
+ from .executor import Executor
11
+ from .reasoner import Reasoner
12
+ from .tools_manager import ToolRegistry
13
+
14
+
15
+ class PluginManager:
16
+ """Manages dynamic loading of plugins for tools, reasoners, executors, and CLI commands."""
17
+ _instance = None
18
+
19
+ def __new__(cls):
20
+ if cls._instance is None:
21
+ cls._instance = super(PluginManager, cls).__new__(cls)
22
+ cls._instance.tools = ToolRegistry()
23
+ cls._instance.reasoners = {"default": Reasoner}
24
+ cls._instance.executors = {"default": Executor}
25
+ cls._instance.cli_commands = {}
26
+ cls._instance._plugins_loaded = False
27
+ return cls._instance
28
+
29
+ def load_plugins(self, force: bool = False) -> None:
30
+ """Load all plugins from registered entry points, handling duplicates gracefully.
31
+
32
+ Args:
33
+ force: If True, reload plugins even if they were already loaded
34
+ """
35
+ if self._plugins_loaded and not force:
36
+ logger.debug("Plugins already loaded, skipping entire load process")
37
+ return
38
+
39
+ # Clear existing plugins only if forcing reload
40
+ if force:
41
+ logger.info("Forcing plugin reload, clearing existing registrations")
42
+ self.tools = ToolRegistry()
43
+ self.reasoners = {"default": Reasoner}
44
+ self.executors = {"default": Executor}
45
+ self.cli_commands = {}
46
+ self._plugins_loaded = False
47
+
48
+ logger.debug("Loading plugins")
49
+ for group, store in [
50
+ ("quantalogic.tools", self.tools.load_toolboxes),
51
+ ("quantalogic.reasoners", self.reasoners),
52
+ ("quantalogic.executors", self.executors),
53
+ ("quantalogic.cli", self.cli_commands),
54
+ ]:
55
+ try:
56
+ eps = entry_points(group=group)
57
+ logger.debug(f"Found {len(eps)} entry points for group {group}")
58
+ for ep in eps:
59
+ try:
60
+ loaded = ep.load()
61
+ if group == "quantalogic.tools":
62
+ # Load static tools first
63
+ store()
64
+ # Then initialize dynamic MCP tools if applicable
65
+ if ep.name == "quantalogic_toolbox_mcp":
66
+ import asyncio
67
+
68
+ from quantalogic_toolbox_mcp.tools import initialize_toolbox
69
+ asyncio.run(initialize_toolbox(self.tools))
70
+ else:
71
+ store[ep.name] = loaded
72
+ logger.info(f"Loaded plugin {ep.name} for {group}")
73
+ except Exception as e:
74
+ logger.warning(f"Skipping plugin {ep.name} for {group} due to error: {e}")
75
+ except Exception as e:
76
+ logger.error(f"Failed to retrieve entry points for {group}: {e}")
77
+ self._plugins_loaded = True
78
+ logger.info("Plugin loading completed")
79
+
80
+ def get_tools(self, force_reload: bool = False) -> List[Tool]:
81
+ """Return all registered tools.
82
+
83
+ Args:
84
+ force_reload: If True, reload plugins before getting tools
85
+ """
86
+ if force_reload:
87
+ self.load_plugins(force=True)
88
+ else:
89
+ # Ensure plugins are loaded at least once, but don’t reload
90
+ if not self._plugins_loaded:
91
+ self.load_plugins()
92
+ return self.tools.get_tools()
@@ -1,26 +1,77 @@
1
- Solve the following task: '{{ task }}'
1
+ Before generating the code, follow these steps:
2
+
3
+ 1. **Review the History**: Examine the <History> section below to understand previous thoughts, actions, and results.
4
+ 2. **Identify Available Variables**: Check the 'Currently available variables' list and 'Available variables' in the history. These are stored in the `context_vars` dictionary and can be accessed using `context_vars.get('variable_name', default_value)`.
5
+ 3. **Understand Variable Types**: Variables in `context_vars` are objects returned by tools, not just their string representations. Refer to tool documentation (e.g., `create_project_plan` returns `PlanResult` with `task_id`, `task_description`, `subtasks`) to access attributes directly (e.g., `plan.task_id`) rather than parsing strings.
6
+ 4. **Use Previous Results**: Incorporate relevant variables from prior steps to avoid redundant calculations and ensure continuity. For example, if `step1_plan` is a `PlanResult`, retrieve it with `plan = context_vars.get('step1_plan')` and use `plan.task_id`.
7
+ 5. **Plan Your Approach**: Based on the history and variables, determine the next logical step toward solving the task.
8
+
9
+ Currently available variables from previous steps: {{ available_vars | join(', ') }}
10
+
11
+ Solve the following task:
12
+
13
+ <Task>
14
+ {{ task }}
15
+ </Task>
16
+
2
17
  This is step {{ current_step }} out of {{ max_iterations }} allowed steps.
18
+
3
19
  Previous steps:
20
+
21
+ <History>
4
22
  {{ history_str }}
23
+ </History>
24
+
25
+ Task:
5
26
 
6
27
  Generate a Python program with an async main() function that uses the available tools to take the next step toward solving the task.
7
28
 
8
29
  Essential Requirements:
9
- - Check context_vars for previous results: context_vars.get("variable_name")
10
- - Build upon previous steps when applicable, using variables listed in 'Available variables' from the history
11
- - Return "Task completed: [final answer]" if solved
12
- - Return intermediate results otherwise
13
- - Handle potential errors in tool arguments
14
- - Maintain proper async/await patterns
15
- - Important: When defining variables in the code, prefix them with 'step{{ current_step }}_' to ensure uniqueness across steps. For example, use 'step{{ current_step }}_intermediate_value'.
30
+
31
+ - **Always check the 'Available variables' from the history** before proceeding. These are listed in the <History> section and stored in the `context_vars` dictionary as objects. Access them using `context_vars.get("variable_name", default_value)` to retrieve results from previous steps.
32
+ - **Handle Variable Types Correctly**: Variables in `context_vars` are the actual objects returned by tools (e.g., `PlanResult` from `create_project_plan`). Use attribute access (e.g., `plan.task_id`) instead of string methods like `split()` unless the variable is explicitly a string. Check tool documentation for return types.
33
+ - **Build upon previous steps** by using these variables when they are relevant. This avoids redundant work and ensures continuity. For example, if a previous step stored a `PlanResult` as `step1_plan`, retrieve it with `plan = context_vars.get('step1_plan')` and use `plan.task_id`.
34
+ - When defining new variables, **prefix them with 'step{{ current_step }}_'** to ensure uniqueness (e.g., `step{{ current_step }}_result`). This prevents overwriting variables from earlier steps.
35
+ - Return `"Task completed: [final answer]"` if the task is fully solved in this step.
36
+ - Otherwise, return intermediate results as a string to be stored for future steps.
37
+ - Handle potential errors in tool arguments gracefully, using try/except blocks if needed.
38
+ - Use proper async/await syntax for all asynchronous operations.
39
+
40
+ Variable Naming Convention:
41
+ - Prefix all new variables with 'step{{ current_step }}_' (e.g., `step{{ current_step }}_result = ...`).
42
+ - This ensures variables are unique across steps and traceable to their origin.
43
+
44
+ Example of using variables from previous steps:
45
+
46
+ Task: "Retrieve the plan from the previous step"
47
+ History:
48
+ "===== Step 1 of 5 max =====
49
+ Thought: Create a project plan
50
+ Action: <code>import asyncio\nasync def main():\n step1_plan = await quantalogic_planning_toolbox.create_project_plan(task_description='Write an article')\n return f'{step1_plan}'</code>
51
+ Result: task_id='abc123' task_description='Write an article' subtasks=[...]
52
+ Available variables: step1_plan"
53
+
54
+ Code:
55
+ import asyncio
56
+
57
+ async def main():
58
+ # Retrieve previous plan as an object, not a string
59
+ step1_plan = context_vars.get('step1_plan')
60
+ if step1_plan:
61
+ step2_task_id: str = step1_plan.task_id # Access attribute directly
62
+ step2_retrieved_plan = await quantalogic_planning_toolbox.retrieve_project_plan(task_id=step2_task_id)
63
+ return f"Retrieved plan: {step2_retrieved_plan}"
64
+ return "Error: No plan found from previous step"
65
+
66
+ Error Reflection:
67
+ - If there were errors in previous steps, review the error messages in the <History> section and adjust your code to avoid similar issues.
68
+ - For example, if a variable was missing, check `context_vars` with a default value.
69
+ - If a tool call failed, verify the arguments and ensure they match the tool's requirements.
70
+ - If an error like "'PlanResult' object has no attribute 'split'" occurred, it means you tried to treat an object as a string. Use attribute access instead (e.g., `.task_id`).
16
71
 
17
72
  The program must:
73
+
18
74
  1. Use only the provided async tools
19
75
  2. Follow a subset of Python 3.10+ syntax, doesn't allow unsafe operations such as eval() or exec()
20
76
  3. Include type annotations for variables, give very explicit names to variables
21
- 4. Output progress information
22
-
23
- Example Context Usage:
24
- previous_value = context_vars.get('step1_intermediate_result')
25
- if previous_value:
26
- next_step = await process_tool(input=previous_value)
77
+ 4. Output progress information
@@ -2,38 +2,51 @@ You are a Python code generator. Your task is to create a Python program that so
2
2
 
3
3
  "{{ task_description }}"
4
4
 
5
- You have access to the following pre-defined async tool functions, as defined with their signatures and descriptions:
5
+ You have access to the following pre-defined async tool functions, grouped by toolbox:
6
6
 
7
- {{ tool_docstrings }}
7
+ {% for toolbox_name, docstrings in tools_by_toolbox.items() %}
8
+ ### {{ toolbox_name }}
9
+ {% for docstring in docstrings %}
10
+ {{ docstring }}
8
11
 
9
- If applicable use tools to assess the situation and generate a Python program that solves the task step by step.
12
+ {% endfor %}
13
+ {% endfor %}
10
14
 
11
- If applicable use tools to verify the task is completed.
15
+ If applicable, use tools to assess the situation and generate a Python program that solves the task step by step.
16
+
17
+ If applicable, use tools to verify the task is completed.
12
18
 
13
19
  Instructions:
14
- 1. Generate a simple vanilla Python program, avoid complex logic, return the program as a single string.
20
+ 1. Generate a very simple Python program, avoid complex logic, return the program as a single string. No more than 3 functions called.
15
21
  2. Include only the import for asyncio (import asyncio).
16
22
  3. Define an async function named main() that solves the task.
17
- 4. Use the pre-defined tool functions directly by calling them with await and the appropriate arguments as specified in their descriptions.
18
- 5. Do not redefine the tool functions within the program; assume they are already available in the namespace.
23
+ 4. Use the pre-defined tool functions by calling them with await and prefixing them with their toolbox name (e.g., `await {{ toolbox_name }}.tool_name(arg1, arg2)`). For core tools, use `default.tool_name` (e.g., `await default.agent_tool(...)`).
24
+ 5. Do not redefine the tool functions within the program; assume they are already available in the namespace under their toolbox names (e.g., `default.agent_tool` for core tools).
19
25
  6. Return the program as a plain string (no markdown or extra text).
20
26
  7. Strictly exclude asyncio.run(main()) or any code outside the main() function definition, including any 'if __name__ == "__main__":' block, as the runtime will handle execution of main().
21
- 8. Express all string variables as multiline strings string, always start a string at the beginning of a line.
27
+ 8. Express all string variables as multiline strings, always start a string at the beginning of a line.
22
28
  9. Always return a string from main(); use "Task completed: [result]" if the task is solved, otherwise return intermediate results.
23
- 10. Access variables from previous steps using the 'context_vars' dictionary (e.g., previous_value = context_vars.get("var_name", default_value))
29
+ 10. Access variables from previous steps using the `context_vars` dictionary:
30
+ - Use `context_vars.get("variable_name", default_value)` to safely retrieve variables (e.g., `previous_sum = context_vars.get("step1_sum", 0)`).
31
+ - Always specify a default value to handle cases where the variable might not exist.
32
+ - Check the history for 'Available variables' to identify relevant previous results.
33
+ - Use these variables to build on prior work rather than starting from scratch.
24
34
  11. Be careful to avoid programs that cannot terminate.
35
+ 12. When defining new variables, prefix them with 'step<current_step>_' (e.g., `step1_result`) to ensure uniqueness across steps.
36
+ 13. Never use dangerous functions like eval, or any other unsafe operations.
37
+ 14. If a return result is tool complex use an intermediate result to store it.
38
+ 15. VERY IMPORTANT: If the return type of a function is Any or not specified don't call another function after this just return the result, the result will be handled by the runtime.
25
39
 
26
-
27
- Example task: "Calculate running total by adding 5 to previous sum stored in context"
40
+ Example task: "Translate the poem 'The sun rises' into Spanish using the agent_tool"
28
41
  Example output:
29
42
  import asyncio
30
43
 
31
44
  async def main():
32
- # Retrieve previous total or default to 0
33
- current_total = int(context_vars.get('running_total', 0))
34
-
35
- # Execute tool operation
36
- new_total = await add_tool(a=current_total, b=5)
37
-
38
- # Return formatted result
39
- return f"Task completed: {new_total}"
45
+ step1_poem: str = "The sun rises"
46
+ step1_system_prompt: str = "You are a translation expert."
47
+ step1_translation: str = await default.agent_tool(
48
+ system_prompt=step1_system_prompt,
49
+ prompt=f"Translate '{step1_poem}' into Spanish",
50
+ temperature=0.7
51
+ )
52
+ return f"Task completed: {step1_translation}"