quantalogic 0.61.1__py3-none-any.whl → 0.61.3__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_config.py +4 -4
- quantalogic/codeact/agent.py +1 -1
- quantalogic/codeact/cli.py +5 -5
- quantalogic/codeact/tools_manager.py +1 -1
- quantalogic/coding_agent.py +1 -1
- quantalogic/tools/action_gen_safe.py +340 -0
- quantalogic/tools/write_file_tool.py +49 -43
- {quantalogic-0.61.1.dist-info → quantalogic-0.61.3.dist-info}/METADATA +4 -3
- {quantalogic-0.61.1.dist-info → quantalogic-0.61.3.dist-info}/RECORD +12 -30
- quantalogic/python_interpreter/__init__.py +0 -23
- quantalogic/python_interpreter/assignment_visitors.py +0 -63
- quantalogic/python_interpreter/base_visitors.py +0 -20
- quantalogic/python_interpreter/class_visitors.py +0 -22
- quantalogic/python_interpreter/comprehension_visitors.py +0 -172
- quantalogic/python_interpreter/context_visitors.py +0 -59
- quantalogic/python_interpreter/control_flow_visitors.py +0 -88
- quantalogic/python_interpreter/exception_visitors.py +0 -109
- quantalogic/python_interpreter/exceptions.py +0 -39
- quantalogic/python_interpreter/execution.py +0 -202
- quantalogic/python_interpreter/function_utils.py +0 -386
- quantalogic/python_interpreter/function_visitors.py +0 -209
- quantalogic/python_interpreter/import_visitors.py +0 -28
- quantalogic/python_interpreter/interpreter_core.py +0 -358
- quantalogic/python_interpreter/literal_visitors.py +0 -74
- quantalogic/python_interpreter/misc_visitors.py +0 -148
- quantalogic/python_interpreter/operator_visitors.py +0 -108
- quantalogic/python_interpreter/scope.py +0 -10
- quantalogic/python_interpreter/visit_handlers.py +0 -110
- {quantalogic-0.61.1.dist-info → quantalogic-0.61.3.dist-info}/LICENSE +0 -0
- {quantalogic-0.61.1.dist-info → quantalogic-0.61.3.dist-info}/WHEEL +0 -0
- {quantalogic-0.61.1.dist-info → quantalogic-0.61.3.dist-info}/entry_points.txt +0 -0
quantalogic/agent_config.py
CHANGED
@@ -75,7 +75,7 @@ def create_agent(
|
|
75
75
|
TaskCompleteTool(),
|
76
76
|
ReadFileTool(),
|
77
77
|
ReadFileBlockTool(),
|
78
|
-
WriteFileTool(),
|
78
|
+
WriteFileTool(disable_ensure_tmp_path=True),
|
79
79
|
EditWholeContentTool(),
|
80
80
|
InputQuestionTool(),
|
81
81
|
ListDirectoryTool(),
|
@@ -129,7 +129,7 @@ def create_interpreter_agent(
|
|
129
129
|
TaskCompleteTool(),
|
130
130
|
ReadFileTool(),
|
131
131
|
ReadFileBlockTool(),
|
132
|
-
WriteFileTool(),
|
132
|
+
WriteFileTool(disable_ensure_tmp_path=True),
|
133
133
|
EditWholeContentTool(),
|
134
134
|
InputQuestionTool(),
|
135
135
|
ListDirectoryTool(),
|
@@ -176,7 +176,7 @@ def create_full_agent(
|
|
176
176
|
TaskCompleteTool(),
|
177
177
|
ReadFileTool(),
|
178
178
|
ReadFileBlockTool(),
|
179
|
-
WriteFileTool(),
|
179
|
+
WriteFileTool(disable_ensure_tmp_path=True),
|
180
180
|
EditWholeContentTool(),
|
181
181
|
InputQuestionTool(),
|
182
182
|
ListDirectoryTool(),
|
@@ -237,7 +237,7 @@ def create_basic_agent(
|
|
237
237
|
SearchDefinitionNamesTool(),
|
238
238
|
ReadFileTool(),
|
239
239
|
ReplaceInFileTool(),
|
240
|
-
WriteFileTool(),
|
240
|
+
WriteFileTool(disable_ensure_tmp_path=True),
|
241
241
|
EditWholeContentTool(),
|
242
242
|
ReplaceInFileTool(),
|
243
243
|
InputQuestionTool(),
|
quantalogic/codeact/agent.py
CHANGED
@@ -5,8 +5,8 @@ from typing import Callable, Dict, List, Optional, Tuple
|
|
5
5
|
from jinja2 import Environment, FileSystemLoader
|
6
6
|
from loguru import logger
|
7
7
|
from lxml import etree
|
8
|
+
from quantalogic_pythonbox import execute_async
|
8
9
|
|
9
|
-
from quantalogic.python_interpreter import execute_async
|
10
10
|
from quantalogic.tools import Tool
|
11
11
|
|
12
12
|
from .constants import MAX_GENERATE_PROGRAM_TOKENS, MAX_HISTORY_TOKENS, MAX_TOKENS, TEMPLATE_DIR
|
quantalogic/codeact/cli.py
CHANGED
@@ -4,11 +4,9 @@ from typing import Callable, List, Optional, Union
|
|
4
4
|
import typer
|
5
5
|
from loguru import logger
|
6
6
|
|
7
|
-
from quantalogic.
|
8
|
-
|
9
|
-
from .
|
10
|
-
from .constants import DEFAULT_MODEL, LOG_FILE
|
11
|
-
from .events import ( # Import events from events.py
|
7
|
+
from quantalogic.codeact.agent import Agent # Import only Agent from agent.py
|
8
|
+
from quantalogic.codeact.constants import DEFAULT_MODEL, LOG_FILE
|
9
|
+
from quantalogic.codeact.events import ( # Import events from events.py
|
12
10
|
ActionExecutedEvent,
|
13
11
|
ActionGeneratedEvent,
|
14
12
|
ErrorOccurredEvent,
|
@@ -22,6 +20,8 @@ from .events import ( # Import events from events.py
|
|
22
20
|
ToolExecutionErrorEvent,
|
23
21
|
ToolExecutionStartedEvent,
|
24
22
|
)
|
23
|
+
from quantalogic.tools import create_tool
|
24
|
+
|
25
25
|
from .tools_manager import Tool, get_default_tools
|
26
26
|
from .utils import XMLResultHandler
|
27
27
|
|
quantalogic/coding_agent.py
CHANGED
@@ -65,7 +65,7 @@ def create_coding_agent(
|
|
65
65
|
# Core file manipulation tools
|
66
66
|
TaskCompleteTool(), # Marks task completion
|
67
67
|
ReadFileBlockTool(), # Reads specific file sections
|
68
|
-
WriteFileTool(), # Creates new files
|
68
|
+
WriteFileTool(disable_ensure_tmp_path=True), # Creates new files
|
69
69
|
ReplaceInFileTool(), # Updates file sections
|
70
70
|
EditWholeContentTool(), # Modifies entire files
|
71
71
|
# Code navigation and search tools
|
@@ -0,0 +1,340 @@
|
|
1
|
+
import ast
|
2
|
+
import asyncio
|
3
|
+
from functools import partial
|
4
|
+
from typing import Callable, Dict, List
|
5
|
+
|
6
|
+
import litellm
|
7
|
+
import typer
|
8
|
+
from loguru import logger
|
9
|
+
|
10
|
+
from quantalogic.tools.tool import Tool, ToolArgument
|
11
|
+
from quantalogic.utils.python_interpreter import ASTInterpreter
|
12
|
+
|
13
|
+
# Configure loguru to log to a file with rotation
|
14
|
+
logger.add("action_gen_safe.log", rotation="10 MB", level="DEBUG")
|
15
|
+
|
16
|
+
# Initialize Typer app
|
17
|
+
app = typer.Typer()
|
18
|
+
|
19
|
+
# Define tool classes with logging in async_execute
|
20
|
+
class AddTool(Tool):
|
21
|
+
def __init__(self):
|
22
|
+
super().__init__(
|
23
|
+
name="add_tool",
|
24
|
+
description="Adds two numbers and returns the sum.",
|
25
|
+
arguments=[
|
26
|
+
ToolArgument(name="a", arg_type="int", description="First number", required=True),
|
27
|
+
ToolArgument(name="b", arg_type="int", description="Second number", required=True)
|
28
|
+
],
|
29
|
+
return_type="int"
|
30
|
+
)
|
31
|
+
|
32
|
+
async def async_execute(self, **kwargs) -> str:
|
33
|
+
logger.info(f"Adding {kwargs['a']} and {kwargs['b']}")
|
34
|
+
return str(int(kwargs["a"]) + int(kwargs["b"]))
|
35
|
+
|
36
|
+
class MultiplyTool(Tool):
|
37
|
+
def __init__(self):
|
38
|
+
super().__init__(
|
39
|
+
name="multiply_tool",
|
40
|
+
description="Multiplies two numbers and returns the product.",
|
41
|
+
arguments=[
|
42
|
+
ToolArgument(name="x", arg_type="int", description="First number", required=True),
|
43
|
+
ToolArgument(name="y", arg_type="int", description="Second number", required=True)
|
44
|
+
],
|
45
|
+
return_type="int"
|
46
|
+
)
|
47
|
+
|
48
|
+
async def async_execute(self, **kwargs) -> str:
|
49
|
+
logger.info(f"Multiplying {kwargs['x']} and {kwargs['y']}")
|
50
|
+
return str(int(kwargs["x"]) * int(kwargs["y"]))
|
51
|
+
|
52
|
+
class ConcatTool(Tool):
|
53
|
+
def __init__(self):
|
54
|
+
super().__init__(
|
55
|
+
name="concat_tool",
|
56
|
+
description="Concatenates two strings.",
|
57
|
+
arguments=[
|
58
|
+
ToolArgument(name="s1", arg_type="string", description="First string", required=True),
|
59
|
+
ToolArgument(name="s2", arg_type="string", description="Second string", required=True)
|
60
|
+
],
|
61
|
+
return_type="string"
|
62
|
+
)
|
63
|
+
|
64
|
+
async def async_execute(self, **kwargs) -> str:
|
65
|
+
logger.info(f"Concatenating '{kwargs['s1']}' and '{kwargs['s2']}'")
|
66
|
+
return kwargs["s1"] + kwargs["s2"]
|
67
|
+
|
68
|
+
class AgentTool(Tool):
|
69
|
+
def __init__(self, model: str = "gemini/gemini-2.0-flash"):
|
70
|
+
super().__init__(
|
71
|
+
name="agent_tool",
|
72
|
+
description="Generates text using a language model based on a system prompt and user prompt.",
|
73
|
+
arguments=[
|
74
|
+
ToolArgument(name="system_prompt", arg_type="string", description="System prompt to guide the model's behavior", required=True),
|
75
|
+
ToolArgument(name="prompt", arg_type="string", description="User prompt to generate a response for", required=True),
|
76
|
+
ToolArgument(name="temperature", arg_type="float", description="Temperature for generation (0 to 1)", required=True)
|
77
|
+
],
|
78
|
+
return_type="string"
|
79
|
+
)
|
80
|
+
self.model = model
|
81
|
+
|
82
|
+
async def async_execute(self, **kwargs) -> str:
|
83
|
+
system_prompt = kwargs["system_prompt"]
|
84
|
+
prompt = kwargs["prompt"]
|
85
|
+
temperature = float(kwargs["temperature"])
|
86
|
+
|
87
|
+
# Validate temperature
|
88
|
+
if not 0 <= temperature <= 1:
|
89
|
+
logger.error(f"Temperature {temperature} is out of range (0-1)")
|
90
|
+
raise ValueError("Temperature must be between 0 and 1")
|
91
|
+
|
92
|
+
logger.info(f"Generating text with model {self.model}, temperature {temperature}")
|
93
|
+
try:
|
94
|
+
response = await litellm.acompletion(
|
95
|
+
model=self.model,
|
96
|
+
messages=[
|
97
|
+
{"role": "system", "content": system_prompt},
|
98
|
+
{"role": "user", "content": prompt}
|
99
|
+
],
|
100
|
+
temperature=temperature,
|
101
|
+
max_tokens=1000 # Reasonable default for text generation
|
102
|
+
)
|
103
|
+
generated_text = response.choices[0].message.content.strip()
|
104
|
+
logger.debug(f"Generated text: {generated_text}")
|
105
|
+
return generated_text
|
106
|
+
except Exception as e:
|
107
|
+
logger.error(f"Failed to generate text with {self.model}: {str(e)}")
|
108
|
+
raise RuntimeError(f"Text generation failed: {str(e)}")
|
109
|
+
|
110
|
+
# Asynchronous function to generate the program
|
111
|
+
async def generate_program(task_description: str, tools: List[Tool], model: str, max_tokens: int) -> str:
|
112
|
+
"""
|
113
|
+
Asynchronously generate a Python program that solves a given task using a list of tools.
|
114
|
+
|
115
|
+
Args:
|
116
|
+
task_description (str): A description of the task to be solved.
|
117
|
+
tools (List[Tool]): A list of Tool objects available for use.
|
118
|
+
model (str): The litellm model to use for code generation.
|
119
|
+
max_tokens (int): Maximum number of tokens for the generated response.
|
120
|
+
|
121
|
+
Returns:
|
122
|
+
str: A string containing a complete Python program.
|
123
|
+
"""
|
124
|
+
logger.debug(f"Generating program for task: {task_description}")
|
125
|
+
# Collect tool docstrings
|
126
|
+
tool_docstrings = "\n\n".join([tool.to_docstring() for tool in tools])
|
127
|
+
|
128
|
+
# Construct the prompt for litellm
|
129
|
+
prompt = f"""
|
130
|
+
You are a Python code generator. Your task is to create a Python program that solves the following task:
|
131
|
+
"{task_description}"
|
132
|
+
|
133
|
+
You have access to the following pre-defined async tool functions, as defined with their signatures and descriptions:
|
134
|
+
|
135
|
+
{tool_docstrings}
|
136
|
+
|
137
|
+
Instructions:
|
138
|
+
1. Generate a Python program as a single string.
|
139
|
+
2. Include only the import for asyncio (import asyncio).
|
140
|
+
3. Define an async function named main() that solves the task.
|
141
|
+
4. Use the pre-defined tool functions (e.g., add_tool, multiply_tool, concat_tool) directly by calling them with await and the appropriate arguments as specified in their descriptions.
|
142
|
+
5. Do not redefine the tool functions within the program; assume they are already available in the namespace.
|
143
|
+
6. Return the program as a string enclosed in triple quotes (\"\"\"program\"\"\")).
|
144
|
+
7. Do not include asyncio.run(main()) or any code outside the main() function definition.
|
145
|
+
8. Do not include explanatory text outside the program string.
|
146
|
+
9. Express all string variables as multiline strings (\"\"\"\nstring\n\"\"\"), always start a string at the beginning of a line.
|
147
|
+
10. Always print the result at the end of the program.
|
148
|
+
11. Never call the main() function.
|
149
|
+
12. Never use not defined variables, modules, or functions.
|
150
|
+
|
151
|
+
Example task: "Add 5 and 7 and print the result"
|
152
|
+
Example output:
|
153
|
+
\"\"\"import asyncio
|
154
|
+
async def main():
|
155
|
+
result = await add_tool(a=5, b=7)
|
156
|
+
print(result)
|
157
|
+
\"\"\"
|
158
|
+
"""
|
159
|
+
|
160
|
+
logger.debug(f"Prompt sent to litellm:\n{prompt}")
|
161
|
+
|
162
|
+
try:
|
163
|
+
# Call litellm asynchronously to generate the program
|
164
|
+
logger.debug(f"Calling litellm with model {model}")
|
165
|
+
response = await litellm.acompletion(
|
166
|
+
model=model,
|
167
|
+
messages=[
|
168
|
+
{"role": "system", "content": "You are a Python code generator."},
|
169
|
+
{"role": "user", "content": prompt}
|
170
|
+
],
|
171
|
+
max_tokens=max_tokens,
|
172
|
+
temperature=0.3
|
173
|
+
)
|
174
|
+
generated_code = response.choices[0].message.content.strip()
|
175
|
+
logger.debug("Code generation successful")
|
176
|
+
except Exception as e:
|
177
|
+
logger.error(f"Failed to generate code: {str(e)}")
|
178
|
+
raise typer.BadParameter(f"Failed to generate code with model '{model}': {str(e)}")
|
179
|
+
|
180
|
+
# Clean up the output
|
181
|
+
if generated_code.startswith('"""') and generated_code.endswith('"""'):
|
182
|
+
generated_code = generated_code[3:-3]
|
183
|
+
elif generated_code.startswith("```python") and generated_code.endswith("```"):
|
184
|
+
generated_code = generated_code[9:-3].strip()
|
185
|
+
|
186
|
+
return generated_code
|
187
|
+
|
188
|
+
# Async core logic for generate
|
189
|
+
async def generate_core(task: str, model: str, max_tokens: int):
|
190
|
+
logger.info(f"Starting generate command for task: {task}")
|
191
|
+
# Input validation
|
192
|
+
if not task.strip():
|
193
|
+
logger.error("Task description is empty")
|
194
|
+
raise typer.BadParameter("Task description cannot be empty")
|
195
|
+
if max_tokens <= 0:
|
196
|
+
logger.error("max-tokens must be positive")
|
197
|
+
raise typer.BadParameter("max-tokens must be a positive integer")
|
198
|
+
|
199
|
+
# Initialize tools
|
200
|
+
tools = [
|
201
|
+
AddTool(),
|
202
|
+
MultiplyTool(),
|
203
|
+
ConcatTool(),
|
204
|
+
AgentTool(model=model) # Include AgentTool with the specified model
|
205
|
+
]
|
206
|
+
|
207
|
+
# Generate the program
|
208
|
+
try:
|
209
|
+
program = await generate_program(task, tools, model, max_tokens)
|
210
|
+
except Exception as e:
|
211
|
+
logger.error(f"Failed to generate program: {str(e)}")
|
212
|
+
typer.echo(typer.style(f"Error: {str(e)}", fg=typer.colors.RED))
|
213
|
+
raise typer.Exit(code=1)
|
214
|
+
|
215
|
+
logger.debug(f"Generated program:\n{program}")
|
216
|
+
# Output the generated program
|
217
|
+
typer.echo(typer.style("Generated Python Program:", fg=typer.colors.GREEN, bold=True))
|
218
|
+
typer.echo(program)
|
219
|
+
|
220
|
+
# Attempt to execute the program using the safe interpreter
|
221
|
+
typer.echo("\n" + typer.style("Executing the program safely:", fg=typer.colors.GREEN, bold=True))
|
222
|
+
try:
|
223
|
+
# Create instances of tools
|
224
|
+
add_tool_instance = AddTool()
|
225
|
+
multiply_tool_instance = MultiplyTool()
|
226
|
+
concat_tool_instance = ConcatTool()
|
227
|
+
agent_tool_instance = AgentTool(model=model)
|
228
|
+
|
229
|
+
# Define the allowed modules
|
230
|
+
allowed_modules = ["asyncio"]
|
231
|
+
|
232
|
+
# Create a namespace with our tools
|
233
|
+
namespace = {
|
234
|
+
"add_tool": add_tool_instance.async_execute,
|
235
|
+
"multiply_tool": multiply_tool_instance.async_execute,
|
236
|
+
"concat_tool": concat_tool_instance.async_execute,
|
237
|
+
"agent_tool": agent_tool_instance.async_execute,
|
238
|
+
}
|
239
|
+
|
240
|
+
# Parse the program to AST
|
241
|
+
program_ast = ast.parse(program)
|
242
|
+
|
243
|
+
# Extract imports first to ensure they're allowed
|
244
|
+
for node in program_ast.body:
|
245
|
+
if isinstance(node, ast.Import):
|
246
|
+
for name in node.names:
|
247
|
+
if name.name not in allowed_modules:
|
248
|
+
raise ValueError(f"Import not allowed: {name.name}")
|
249
|
+
elif isinstance(node, ast.ImportFrom):
|
250
|
+
if node.module not in allowed_modules:
|
251
|
+
raise ValueError(f"Import from not allowed: {node.module}")
|
252
|
+
|
253
|
+
# Find the main function in the AST
|
254
|
+
main_func_node = None
|
255
|
+
for node in program_ast.body:
|
256
|
+
if isinstance(node, ast.AsyncFunctionDef) and node.name == "main":
|
257
|
+
main_func_node = node
|
258
|
+
break
|
259
|
+
|
260
|
+
if not main_func_node:
|
261
|
+
logger.warning("No async main() function found in generated code")
|
262
|
+
typer.echo(typer.style("Warning: No async main() function found", fg=typer.colors.YELLOW))
|
263
|
+
return
|
264
|
+
|
265
|
+
# Execute the code safely to define functions in the namespace
|
266
|
+
# Use a custom ASTInterpreter directly instead of calling interpret_code
|
267
|
+
interpreter = ASTInterpreter(allowed_modules=allowed_modules, source=program)
|
268
|
+
|
269
|
+
# Add tools to the interpreter's environment
|
270
|
+
interpreter.env_stack[0].update({
|
271
|
+
"add_tool": add_tool_instance.async_execute,
|
272
|
+
"multiply_tool": multiply_tool_instance.async_execute,
|
273
|
+
"concat_tool": concat_tool_instance.async_execute,
|
274
|
+
"agent_tool": agent_tool_instance.async_execute,
|
275
|
+
})
|
276
|
+
|
277
|
+
# First, execute all non-main code (imports and other top-level code)
|
278
|
+
for node in program_ast.body:
|
279
|
+
if not isinstance(node, ast.AsyncFunctionDef) or node.name != "main":
|
280
|
+
await interpreter.visit(node, wrap_exceptions=True)
|
281
|
+
|
282
|
+
# Then execute the main function definition
|
283
|
+
for node in program_ast.body:
|
284
|
+
if isinstance(node, ast.AsyncFunctionDef) and node.name == "main":
|
285
|
+
await interpreter.visit(node, wrap_exceptions=True)
|
286
|
+
|
287
|
+
# Get the main function from the environment
|
288
|
+
main_func = interpreter.get_variable("main")
|
289
|
+
|
290
|
+
# Execute the main function in the current event loop
|
291
|
+
logger.debug("Executing main() function")
|
292
|
+
await main_func()
|
293
|
+
|
294
|
+
logger.info("Program executed successfully")
|
295
|
+
typer.echo(typer.style("Execution completed successfully", fg=typer.colors.GREEN))
|
296
|
+
|
297
|
+
except SyntaxError as e:
|
298
|
+
logger.error(f"Syntax error in generated code: {e}")
|
299
|
+
typer.echo(typer.style(f"Syntax error: {e}", fg=typer.colors.RED))
|
300
|
+
except Exception as e:
|
301
|
+
logger.error(f"Execution error: {e}")
|
302
|
+
typer.echo(typer.style(f"Execution failed: {e}", fg=typer.colors.RED))
|
303
|
+
|
304
|
+
# Synchronous callback to invoke async generate_core
|
305
|
+
@app.callback(invoke_without_command=True)
|
306
|
+
def generate(
|
307
|
+
task: str = typer.Argument(
|
308
|
+
...,
|
309
|
+
help="The task description to generate a program for (e.g., 'Add 5 and 7 and print the result')"
|
310
|
+
),
|
311
|
+
model: str = typer.Option(
|
312
|
+
"gemini/gemini-2.0-flash",
|
313
|
+
"--model",
|
314
|
+
"-m",
|
315
|
+
help="The litellm model to use for generation (e.g., 'gpt-3.5-turbo', 'gpt-4')"
|
316
|
+
),
|
317
|
+
max_tokens: int = typer.Option(
|
318
|
+
4000,
|
319
|
+
"--max-tokens",
|
320
|
+
"-t",
|
321
|
+
help="Maximum number of tokens for the generated response (default: 4000)"
|
322
|
+
)
|
323
|
+
):
|
324
|
+
"""
|
325
|
+
Asynchronously generate a Python program based on a task description using specified tools and model.
|
326
|
+
Executes the program in a controlled, safe environment.
|
327
|
+
|
328
|
+
Examples:
|
329
|
+
$ python action_gen_safe.py "Add 5 and 7 and print the result"
|
330
|
+
$ python action_gen_safe.py "Concatenate 'Hello' and 'World' and print it" --model gpt-4 --max-tokens 5000
|
331
|
+
"""
|
332
|
+
asyncio.run(generate_core(task, model, max_tokens))
|
333
|
+
|
334
|
+
# Entry point to start the app
|
335
|
+
def main():
|
336
|
+
logger.debug("Starting script execution")
|
337
|
+
app()
|
338
|
+
|
339
|
+
if __name__ == "__main__":
|
340
|
+
main()
|
@@ -4,50 +4,56 @@ import os
|
|
4
4
|
from pathlib import Path
|
5
5
|
|
6
6
|
from loguru import logger
|
7
|
+
from pydantic import Field
|
8
|
+
|
7
9
|
from quantalogic.tools.tool import Tool, ToolArgument
|
8
10
|
|
9
11
|
|
10
12
|
class WriteFileTool(Tool):
|
11
|
-
"""Tool for writing a text file
|
13
|
+
"""Tool for writing a text file to a specified path."""
|
12
14
|
|
13
15
|
name: str = "write_file_tool"
|
14
|
-
description: str = "Writes a file
|
16
|
+
description: str = "Writes a file to the specified path. The tool will fail if the file already exists when not used in append mode."
|
15
17
|
need_validation: bool = True
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
18
|
+
|
19
|
+
disable_ensure_tmp_path: bool = Field(default=False)
|
20
|
+
|
21
|
+
arguments: list = Field(
|
22
|
+
default=[
|
23
|
+
ToolArgument(
|
24
|
+
name="file_path",
|
25
|
+
arg_type="string",
|
26
|
+
description="The path to the file to write. By default, paths will be forced to /tmp directory unless disable_ensure_tmp_path is enabled. Can include subdirectories.",
|
27
|
+
required=True,
|
28
|
+
example="/tmp/myfile.txt or ./myfile.txt",
|
29
|
+
),
|
30
|
+
ToolArgument(
|
31
|
+
name="content",
|
32
|
+
arg_type="string",
|
33
|
+
description="""The content to write to the file. Use CDATA to escape special characters.
|
34
|
+
Don't add newlines at the beginning or end of the content.
|
35
|
+
""",
|
36
|
+
required=True,
|
37
|
+
example="Hello, world!",
|
38
|
+
),
|
39
|
+
ToolArgument(
|
40
|
+
name="append_mode",
|
41
|
+
arg_type="string",
|
42
|
+
description="""Append mode. If true, the content will be appended to the end of the file.
|
43
|
+
""",
|
44
|
+
required=False,
|
45
|
+
example="False",
|
46
|
+
),
|
47
|
+
ToolArgument(
|
48
|
+
name="overwrite",
|
49
|
+
arg_type="string",
|
50
|
+
description="Overwrite mode. If true, existing files can be overwritten. Defaults to False.",
|
51
|
+
required=False,
|
52
|
+
example="False",
|
53
|
+
default="False",
|
54
|
+
),
|
55
|
+
],
|
56
|
+
)
|
51
57
|
|
52
58
|
def _ensure_tmp_path(self, file_path: str) -> str:
|
53
59
|
"""Ensures the file path is within /tmp directory.
|
@@ -99,12 +105,10 @@ class WriteFileTool(Tool):
|
|
99
105
|
overwrite_bool = overwrite.lower() in ["true", "1", "yes"]
|
100
106
|
|
101
107
|
# Ensure path is in /tmp and normalize it
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
if parent_dir.startswith("/tmp/"):
|
107
|
-
os.makedirs(parent_dir, exist_ok=True)
|
108
|
+
if not self.disable_ensure_tmp_path:
|
109
|
+
file_path = self._ensure_tmp_path(file_path)
|
110
|
+
if not file_path.startswith('/tmp/'):
|
111
|
+
raise ValueError('File path must be under /tmp when disable_ensure_tmp_path is False')
|
108
112
|
|
109
113
|
# Determine file write mode based on append_mode
|
110
114
|
mode = "a" if append_mode_bool else "w"
|
@@ -115,6 +119,8 @@ class WriteFileTool(Tool):
|
|
115
119
|
f"File {file_path} already exists. Set append_mode=True to append or overwrite=True to overwrite."
|
116
120
|
)
|
117
121
|
|
122
|
+
os.makedirs(os.path.dirname(file_path), exist_ok=True)
|
123
|
+
|
118
124
|
with open(file_path, mode, encoding="utf-8") as f:
|
119
125
|
f.write(content)
|
120
126
|
|
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.1
|
2
2
|
Name: quantalogic
|
3
|
-
Version: 0.61.
|
3
|
+
Version: 0.61.3
|
4
4
|
Summary: QuantaLogic ReAct Agents
|
5
5
|
Author: Raphaël MANSUY
|
6
6
|
Author-email: raphael.mansuy@gmail.com
|
@@ -22,7 +22,7 @@ Requires-Dist: google-search-results (>=2.4.2,<3.0.0)
|
|
22
22
|
Requires-Dist: html2text (>=2024.2.26,<2025.0.0)
|
23
23
|
Requires-Dist: instructor (>=1.7.2,<2.0.0)
|
24
24
|
Requires-Dist: jinja2 (>=3.1.5,<4.0.0)
|
25
|
-
Requires-Dist: litellm (>=1.
|
25
|
+
Requires-Dist: litellm (>=1.63.14,<2.0.0)
|
26
26
|
Requires-Dist: loguru (>=0.7.3,<0.8.0)
|
27
27
|
Requires-Dist: markdownify (>=0.14.1,<0.15.0)
|
28
28
|
Requires-Dist: markitdown (>=0.0.1a3,<0.0.2)
|
@@ -34,6 +34,7 @@ Requires-Dist: psutil (>=7.0.0,<8.0.0)
|
|
34
34
|
Requires-Dist: pydantic (>=2.10.4,<3.0.0)
|
35
35
|
Requires-Dist: pytest-asyncio (>=0.25.3,<0.26.0)
|
36
36
|
Requires-Dist: python-dotenv (>=1.0.1,<2.0.0)
|
37
|
+
Requires-Dist: quantalogic-pythonbox (>=0.8.3,<0.9.0)
|
37
38
|
Requires-Dist: requests (>=2.32.3,<3.0.0)
|
38
39
|
Requires-Dist: rich (>=13.9.4,<14.0.0)
|
39
40
|
Requires-Dist: serpapi (>=0.1.5,<0.2.0)
|
@@ -388,7 +389,7 @@ The **Flow module** is your architect—building workflows that hum with precisi
|
|
388
389
|
|
389
390
|
🔍 **Want to dive deeper?** Check out our comprehensive [Workflow YAML DSL Specification](./quantalogic/flow/flow_yaml.md), a detailed guide that walks you through defining powerful, structured workflows. From basic node configurations to complex transition logic, this documentation is your roadmap to mastering workflow design with QuantaLogic.
|
390
391
|
|
391
|
-
📚 **For a deeper understanding of Flow YAML and its applications, please refer to the official [Flow
|
392
|
+
📚 **For a deeper understanding of Flow YAML and its applications, please refer to the official [Flow YAMAL Documentation](./quantalogic/flow/flow_yaml.md).**
|
392
393
|
|
393
394
|
The Flow YAML documentation provides a comprehensive overview of the Flow YAML language, including its syntax, features, and best practices. It's a valuable resource for anyone looking to create complex workflows with QuantaLogic.
|
394
395
|
|