quantalogic 0.59.3__py3-none-any.whl → 0.61.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 +268 -24
- quantalogic/agent_config.py +5 -5
- quantalogic/agent_factory.py +2 -2
- quantalogic/codeact/__init__.py +0 -0
- quantalogic/codeact/agent.py +499 -0
- quantalogic/codeact/cli.py +232 -0
- quantalogic/codeact/constants.py +9 -0
- quantalogic/codeact/events.py +78 -0
- quantalogic/codeact/llm_util.py +76 -0
- quantalogic/codeact/prompts/error_format.j2 +11 -0
- quantalogic/codeact/prompts/generate_action.j2 +26 -0
- quantalogic/codeact/prompts/generate_program.j2 +39 -0
- quantalogic/codeact/prompts/response_format.j2 +11 -0
- quantalogic/codeact/tools_manager.py +135 -0
- quantalogic/codeact/utils.py +135 -0
- quantalogic/coding_agent.py +2 -2
- quantalogic/create_custom_agent.py +26 -78
- quantalogic/prompts/chat_system_prompt.j2 +10 -7
- quantalogic/prompts/code_2_system_prompt.j2 +190 -0
- quantalogic/prompts/code_system_prompt.j2 +142 -0
- quantalogic/prompts/doc_system_prompt.j2 +178 -0
- quantalogic/prompts/legal_2_system_prompt.j2 +218 -0
- quantalogic/prompts/legal_system_prompt.j2 +140 -0
- quantalogic/prompts/system_prompt.j2 +6 -2
- quantalogic/prompts/tools_prompt.j2 +2 -4
- quantalogic/prompts.py +23 -4
- quantalogic/python_interpreter/__init__.py +23 -0
- quantalogic/python_interpreter/assignment_visitors.py +63 -0
- quantalogic/python_interpreter/base_visitors.py +20 -0
- quantalogic/python_interpreter/class_visitors.py +22 -0
- quantalogic/python_interpreter/comprehension_visitors.py +172 -0
- quantalogic/python_interpreter/context_visitors.py +59 -0
- quantalogic/python_interpreter/control_flow_visitors.py +88 -0
- quantalogic/python_interpreter/exception_visitors.py +109 -0
- quantalogic/python_interpreter/exceptions.py +39 -0
- quantalogic/python_interpreter/execution.py +202 -0
- quantalogic/python_interpreter/function_utils.py +386 -0
- quantalogic/python_interpreter/function_visitors.py +209 -0
- quantalogic/python_interpreter/import_visitors.py +28 -0
- quantalogic/python_interpreter/interpreter_core.py +358 -0
- quantalogic/python_interpreter/literal_visitors.py +74 -0
- quantalogic/python_interpreter/misc_visitors.py +148 -0
- quantalogic/python_interpreter/operator_visitors.py +108 -0
- quantalogic/python_interpreter/scope.py +10 -0
- quantalogic/python_interpreter/visit_handlers.py +110 -0
- quantalogic/server/agent_server.py +1 -1
- quantalogic/tools/__init__.py +6 -3
- quantalogic/tools/action_gen.py +366 -0
- quantalogic/tools/duckduckgo_search_tool.py +1 -0
- quantalogic/tools/execute_bash_command_tool.py +114 -57
- quantalogic/tools/file_tracker_tool.py +49 -0
- quantalogic/tools/google_packages/google_news_tool.py +3 -0
- quantalogic/tools/image_generation/dalle_e.py +89 -137
- quantalogic/tools/python_tool.py +13 -0
- quantalogic/tools/rag_tool/__init__.py +2 -9
- quantalogic/tools/rag_tool/document_rag_sources_.py +728 -0
- quantalogic/tools/rag_tool/ocr_pdf_markdown.py +144 -0
- quantalogic/tools/replace_in_file_tool.py +1 -1
- quantalogic/tools/{search_definition_names.py → search_definition_names_tool.py} +2 -2
- quantalogic/tools/terminal_capture_tool.py +293 -0
- quantalogic/tools/tool.py +120 -22
- quantalogic/tools/utilities/__init__.py +2 -0
- quantalogic/tools/utilities/download_file_tool.py +3 -5
- quantalogic/tools/utilities/llm_tool.py +283 -0
- quantalogic/tools/utilities/selenium_tool.py +296 -0
- quantalogic/tools/utilities/vscode_tool.py +1 -1
- quantalogic/tools/web_navigation/__init__.py +5 -0
- quantalogic/tools/web_navigation/web_tool.py +145 -0
- quantalogic/tools/write_file_tool.py +72 -36
- quantalogic/utils/__init__.py +0 -1
- quantalogic/utils/test_python_interpreter.py +119 -0
- {quantalogic-0.59.3.dist-info → quantalogic-0.61.0.dist-info}/METADATA +7 -2
- {quantalogic-0.59.3.dist-info → quantalogic-0.61.0.dist-info}/RECORD +76 -35
- quantalogic/tools/rag_tool/document_metadata.py +0 -15
- quantalogic/tools/rag_tool/query_response.py +0 -20
- quantalogic/tools/rag_tool/rag_tool.py +0 -566
- quantalogic/tools/rag_tool/rag_tool_beta.py +0 -264
- quantalogic/utils/python_interpreter.py +0 -905
- {quantalogic-0.59.3.dist-info → quantalogic-0.61.0.dist-info}/LICENSE +0 -0
- {quantalogic-0.59.3.dist-info → quantalogic-0.61.0.dist-info}/WHEEL +0 -0
- {quantalogic-0.59.3.dist-info → quantalogic-0.61.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,366 @@
|
|
1
|
+
import ast
|
2
|
+
import asyncio
|
3
|
+
from asyncio import TimeoutError
|
4
|
+
from contextlib import AsyncExitStack
|
5
|
+
from functools import partial
|
6
|
+
from typing import Callable, Dict, List
|
7
|
+
|
8
|
+
import litellm
|
9
|
+
import typer
|
10
|
+
from loguru import logger
|
11
|
+
|
12
|
+
from quantalogic.python_interpreter import execute_async
|
13
|
+
from quantalogic.tools.tool import Tool, ToolArgument
|
14
|
+
|
15
|
+
# Configure loguru to log to a file with rotation, matching original
|
16
|
+
logger.add("action_gen.log", rotation="10 MB", level="DEBUG")
|
17
|
+
|
18
|
+
# Initialize Typer app, unchanged
|
19
|
+
app = typer.Typer()
|
20
|
+
|
21
|
+
# Define tool classes with logging in async_execute, preserving original structure
|
22
|
+
class AddTool(Tool):
|
23
|
+
def __init__(self):
|
24
|
+
super().__init__(
|
25
|
+
name="add_tool",
|
26
|
+
description="Adds two numbers and returns the sum.",
|
27
|
+
arguments=[
|
28
|
+
ToolArgument(name="a", arg_type="int", description="First number", required=True),
|
29
|
+
ToolArgument(name="b", arg_type="int", description="Second number", required=True)
|
30
|
+
],
|
31
|
+
return_type="int"
|
32
|
+
)
|
33
|
+
|
34
|
+
async def async_execute(self, **kwargs) -> str:
|
35
|
+
logger.info(f"Starting tool execution: {self.name}")
|
36
|
+
logger.info(f"Adding {kwargs['a']} and {kwargs['b']}")
|
37
|
+
result = str(int(kwargs["a"]) + int(kwargs["b"]))
|
38
|
+
logger.info(f"Finished tool execution: {self.name}")
|
39
|
+
return result
|
40
|
+
|
41
|
+
class MultiplyTool(Tool):
|
42
|
+
def __init__(self):
|
43
|
+
super().__init__(
|
44
|
+
name="multiply_tool",
|
45
|
+
description="Multiplies two numbers and returns the product.",
|
46
|
+
arguments=[
|
47
|
+
ToolArgument(name="x", arg_type="int", description="First number", required=True),
|
48
|
+
ToolArgument(name="y", arg_type="int", description="Second number", required=True)
|
49
|
+
],
|
50
|
+
return_type="int"
|
51
|
+
)
|
52
|
+
|
53
|
+
async def async_execute(self, **kwargs) -> str:
|
54
|
+
logger.info(f"Starting tool execution: {self.name}")
|
55
|
+
logger.info(f"Multiplying {kwargs['x']} and {kwargs['y']}")
|
56
|
+
result = str(int(kwargs["x"]) * int(kwargs["y"]))
|
57
|
+
logger.info(f"Finished tool execution: {self.name}")
|
58
|
+
return result
|
59
|
+
|
60
|
+
class ConcatTool(Tool):
|
61
|
+
def __init__(self):
|
62
|
+
super().__init__(
|
63
|
+
name="concat_tool",
|
64
|
+
description="Concatenates two strings.",
|
65
|
+
arguments=[
|
66
|
+
ToolArgument(name="s1", arg_type="string", description="First string", required=True),
|
67
|
+
ToolArgument(name="s2", arg_type="string", description="Second string", required=True)
|
68
|
+
],
|
69
|
+
return_type="string"
|
70
|
+
)
|
71
|
+
|
72
|
+
async def async_execute(self, **kwargs) -> str:
|
73
|
+
logger.info(f"Starting tool execution: {self.name}")
|
74
|
+
logger.info(f"Concatenating '{kwargs['s1']}' and '{kwargs['s2']}'")
|
75
|
+
result = kwargs["s1"] + kwargs["s2"]
|
76
|
+
logger.info(f"Finished tool execution: {self.name}")
|
77
|
+
return result
|
78
|
+
|
79
|
+
class AgentTool(Tool):
|
80
|
+
def __init__(self, model: str = "gemini/gemini-2.0-flash"):
|
81
|
+
super().__init__(
|
82
|
+
name="agent_tool",
|
83
|
+
description="Generates text using a language model based on a system prompt and user prompt.",
|
84
|
+
arguments=[
|
85
|
+
ToolArgument(name="system_prompt", arg_type="string", description="System prompt to guide the model's behavior", required=True),
|
86
|
+
ToolArgument(name="prompt", arg_type="string", description="User prompt to generate a response for", required=True),
|
87
|
+
ToolArgument(name="temperature", arg_type="float", description="Temperature for generation (0 to 1)", required=True)
|
88
|
+
],
|
89
|
+
return_type="string"
|
90
|
+
)
|
91
|
+
self.model = model
|
92
|
+
|
93
|
+
async def async_execute(self, **kwargs) -> str:
|
94
|
+
logger.info(f"Starting tool execution: {self.name}")
|
95
|
+
system_prompt = kwargs["system_prompt"]
|
96
|
+
prompt = kwargs["prompt"]
|
97
|
+
temperature = float(kwargs["temperature"])
|
98
|
+
|
99
|
+
# Validate temperature, unchanged
|
100
|
+
if not 0 <= temperature <= 1:
|
101
|
+
logger.error(f"Temperature {temperature} is out of range (0-1)")
|
102
|
+
raise ValueError("Temperature must be between 0 and 1")
|
103
|
+
|
104
|
+
logger.info(f"Generating text with model {self.model}, temperature {temperature}")
|
105
|
+
try:
|
106
|
+
async with AsyncExitStack() as stack:
|
107
|
+
timeout_cm = asyncio.timeout(30)
|
108
|
+
await stack.enter_async_context(timeout_cm)
|
109
|
+
|
110
|
+
logger.debug(f"Making API call to {self.model}")
|
111
|
+
response = await litellm.acompletion(
|
112
|
+
model=self.model,
|
113
|
+
messages=[
|
114
|
+
{"role": "system", "content": system_prompt},
|
115
|
+
{"role": "user", "content": prompt}
|
116
|
+
],
|
117
|
+
temperature=temperature,
|
118
|
+
max_tokens=1000 # Original default
|
119
|
+
)
|
120
|
+
generated_text = response.choices[0].message.content.strip()
|
121
|
+
logger.debug(f"Generated text: {generated_text}")
|
122
|
+
result = generated_text
|
123
|
+
logger.info(f"Finished tool execution: {self.name}")
|
124
|
+
return result
|
125
|
+
except TimeoutError as e:
|
126
|
+
error_msg = f"API call to {self.model} timed out after 30 seconds"
|
127
|
+
logger.error(error_msg)
|
128
|
+
raise TimeoutError(error_msg) from e
|
129
|
+
except Exception as e:
|
130
|
+
logger.error(f"Failed to generate text with {self.model}: {str(e)}")
|
131
|
+
raise RuntimeError(f"Text generation failed: {str(e)}")
|
132
|
+
|
133
|
+
# Asynchronous function to generate the program, matching original behavior with updated prompt
|
134
|
+
async def generate_program(task_description: str, tools: List[Tool], model: str, max_tokens: int) -> str:
|
135
|
+
"""
|
136
|
+
Asynchronously generate a Python program that solves a given task using a list of tools.
|
137
|
+
|
138
|
+
Args:
|
139
|
+
task_description (str): A description of the task to be solved.
|
140
|
+
tools (List[Tool]): A list of Tool objects available for use.
|
141
|
+
model (str): The litellm model to use for code generation.
|
142
|
+
max_tokens (int): Maximum number of tokens for the generated response.
|
143
|
+
|
144
|
+
Returns:
|
145
|
+
str: A string containing a complete Python program.
|
146
|
+
"""
|
147
|
+
logger.debug(f"Generating program for task: {task_description}")
|
148
|
+
tool_docstrings = "\n\n".join([tool.to_docstring() for tool in tools])
|
149
|
+
|
150
|
+
# Updated prompt with reinforced instruction to exclude __main__ block
|
151
|
+
prompt = f"""
|
152
|
+
You are a Python code generator. Your task is to create a Python program that solves the following task:
|
153
|
+
"{task_description}"
|
154
|
+
|
155
|
+
You have access to the following pre-defined async tool functions, as defined with their signatures and descriptions:
|
156
|
+
|
157
|
+
{tool_docstrings}
|
158
|
+
|
159
|
+
Instructions:
|
160
|
+
1. Generate a Python program as a single string.
|
161
|
+
2. Include only the import for asyncio (import asyncio).
|
162
|
+
3. Define an async function named main() that solves the task.
|
163
|
+
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.
|
164
|
+
5. Do not redefine the tool functions within the program; assume they are already available in the namespace.
|
165
|
+
6. Return the program as markdown code block.
|
166
|
+
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().
|
167
|
+
8. Do not include explanatory text outside the program string.
|
168
|
+
9. Express all string variables as multiline strings
|
169
|
+
string, always start a string at the beginning of a line.
|
170
|
+
10. Always print the result at the end of the program.
|
171
|
+
|
172
|
+
Example task: "Add 5 and 7 and print the result"
|
173
|
+
Example output:
|
174
|
+
```python
|
175
|
+
import asyncio
|
176
|
+
|
177
|
+
async def main():
|
178
|
+
result = await add_tool(a=5, b=7)
|
179
|
+
print(result)
|
180
|
+
```
|
181
|
+
"""
|
182
|
+
|
183
|
+
logger.debug(f"Prompt sent to litellm:\n{prompt}")
|
184
|
+
|
185
|
+
try:
|
186
|
+
logger.debug(f"Calling litellm with model {model}")
|
187
|
+
response = await litellm.acompletion(
|
188
|
+
model=model,
|
189
|
+
messages=[
|
190
|
+
{"role": "system", "content": "You are a Python code generator."},
|
191
|
+
{"role": "user", "content": prompt}
|
192
|
+
],
|
193
|
+
max_tokens=max_tokens,
|
194
|
+
temperature=0.3
|
195
|
+
)
|
196
|
+
generated_code = response.choices[0].message.content.strip()
|
197
|
+
logger.debug("Code generation successful")
|
198
|
+
except Exception as e:
|
199
|
+
logger.error(f"Failed to generate code: {str(e)}")
|
200
|
+
raise typer.BadParameter(f"Failed to generate code with model '{model}': {str(e)}")
|
201
|
+
|
202
|
+
# Clean up output, preserving original logic
|
203
|
+
if generated_code.startswith('"""') and generated_code.endswith('"""'):
|
204
|
+
generated_code = generated_code[3:-3]
|
205
|
+
elif generated_code.startswith("```python") and generated_code.endswith("```"):
|
206
|
+
generated_code = generated_code[9:-3].strip()
|
207
|
+
|
208
|
+
# Post-processing to remove any __main__ block if generated despite instructions
|
209
|
+
if "if __name__ == \"__main__\":" in generated_code:
|
210
|
+
lines = generated_code.splitlines()
|
211
|
+
main_end_idx = next(
|
212
|
+
(i for i in range(len(lines)) if "if __name__" in lines[i]),
|
213
|
+
len(lines)
|
214
|
+
)
|
215
|
+
generated_code = "\n".join(lines[:main_end_idx]).strip()
|
216
|
+
logger.warning("Removed unexpected __main__ block from generated code")
|
217
|
+
|
218
|
+
return generated_code
|
219
|
+
|
220
|
+
# Updated async core logic with improved interpreter usage
|
221
|
+
async def generate_core(task: str, model: str, max_tokens: int) -> None:
|
222
|
+
"""
|
223
|
+
Core logic to generate and execute a Python program based on a task description.
|
224
|
+
|
225
|
+
Args:
|
226
|
+
task (str): The task description to generate a program for.
|
227
|
+
model (str): The litellm model to use for generation.
|
228
|
+
max_tokens (int): Maximum number of tokens for the generated response.
|
229
|
+
"""
|
230
|
+
logger.info(f"Starting generate command for task: {task}")
|
231
|
+
# Input validation, unchanged
|
232
|
+
if not task.strip():
|
233
|
+
logger.error("Task description is empty")
|
234
|
+
raise typer.BadParameter("Task description cannot be empty")
|
235
|
+
if max_tokens <= 0:
|
236
|
+
logger.error("max-tokens must be positive")
|
237
|
+
raise typer.BadParameter("max-tokens must be a positive integer")
|
238
|
+
|
239
|
+
# Initialize tools, unchanged
|
240
|
+
tools = [
|
241
|
+
AddTool(),
|
242
|
+
MultiplyTool(),
|
243
|
+
ConcatTool(),
|
244
|
+
AgentTool(model=model)
|
245
|
+
]
|
246
|
+
|
247
|
+
# Generate the program
|
248
|
+
try:
|
249
|
+
program = await generate_program(task, tools, model, max_tokens)
|
250
|
+
except Exception as e:
|
251
|
+
logger.error(f"Failed to generate program: {str(e)}")
|
252
|
+
typer.echo(typer.style(f"Error: {str(e)}", fg=typer.colors.RED))
|
253
|
+
raise typer.Exit(code=1)
|
254
|
+
|
255
|
+
logger.debug(f"Generated program:\n{program}")
|
256
|
+
# Output the generated program with original style
|
257
|
+
typer.echo(typer.style("Generated Python Program:", fg=typer.colors.GREEN, bold=True))
|
258
|
+
typer.echo(program)
|
259
|
+
|
260
|
+
# Validate program structure
|
261
|
+
try:
|
262
|
+
ast_tree = ast.parse(program)
|
263
|
+
has_async_main = any(
|
264
|
+
isinstance(node, ast.AsyncFunctionDef) and node.name == "main"
|
265
|
+
for node in ast.walk(ast_tree)
|
266
|
+
)
|
267
|
+
if not has_async_main:
|
268
|
+
logger.warning("Generated code lacks an async main() function")
|
269
|
+
typer.echo(typer.style("Warning: Generated code lacks an async main() function", fg=typer.colors.YELLOW))
|
270
|
+
return
|
271
|
+
except SyntaxError as e:
|
272
|
+
logger.error(f"Syntax error in generated code: {str(e)}")
|
273
|
+
typer.echo(typer.style(f"Syntax error in generated code: {str(e)}", fg=typer.colors.RED))
|
274
|
+
return
|
275
|
+
|
276
|
+
# Prepare namespace with tool instances
|
277
|
+
namespace: Dict[str, Callable] = {
|
278
|
+
"asyncio": asyncio,
|
279
|
+
"add_tool": partial(AddTool().async_execute),
|
280
|
+
"multiply_tool": partial(MultiplyTool().async_execute),
|
281
|
+
"concat_tool": partial(ConcatTool().async_execute),
|
282
|
+
"agent_tool": partial(AgentTool(model=model).async_execute),
|
283
|
+
}
|
284
|
+
|
285
|
+
# Check for namespace collisions
|
286
|
+
reserved_names = set(vars(__builtins__))
|
287
|
+
for name in namespace:
|
288
|
+
if name in reserved_names and name != "asyncio":
|
289
|
+
logger.warning(f"Namespace collision detected: '{name}' shadows a builtin")
|
290
|
+
typer.echo(typer.style(f"Warning: Tool name '{name}' shadows a builtin", fg=typer.colors.YELLOW))
|
291
|
+
|
292
|
+
# Execute the program
|
293
|
+
typer.echo("\n" + typer.style("Executing the program:", fg=typer.colors.GREEN, bold=True))
|
294
|
+
try:
|
295
|
+
logger.debug("Executing generated code with execute_async")
|
296
|
+
execution_result = await execute_async(
|
297
|
+
code=program,
|
298
|
+
timeout=30,
|
299
|
+
entry_point="main",
|
300
|
+
allowed_modules=["asyncio"],
|
301
|
+
namespace=namespace,
|
302
|
+
)
|
303
|
+
|
304
|
+
# Detailed error handling
|
305
|
+
if execution_result.error:
|
306
|
+
if "SyntaxError" in execution_result.error:
|
307
|
+
logger.error(f"Syntax error: {execution_result.error}")
|
308
|
+
typer.echo(typer.style(f"Syntax error: {execution_result.error}", fg=typer.colors.RED))
|
309
|
+
elif "TimeoutError" in execution_result.error:
|
310
|
+
logger.error(f"Timeout: {execution_result.error}")
|
311
|
+
typer.echo(typer.style(f"Timeout: {execution_result.error}", fg=typer.colors.RED))
|
312
|
+
else:
|
313
|
+
logger.error(f"Runtime error: {execution_result.error}")
|
314
|
+
typer.echo(typer.style(f"Runtime error: {execution_result.error}", fg=typer.colors.RED))
|
315
|
+
else:
|
316
|
+
logger.info(f"Execution completed in {execution_result.execution_time:.2f} seconds")
|
317
|
+
typer.echo(typer.style(f"Execution completed in {execution_result.execution_time:.2f} seconds", fg=typer.colors.GREEN))
|
318
|
+
|
319
|
+
# Display the result if it's not None
|
320
|
+
if execution_result.result is not None:
|
321
|
+
typer.echo("\n" + typer.style("Result:", fg=typer.colors.BLUE, bold=True))
|
322
|
+
typer.echo(str(execution_result.result))
|
323
|
+
except ValueError as e:
|
324
|
+
logger.error(f"Invalid code generated: {str(e)}")
|
325
|
+
typer.echo(typer.style(f"Invalid code: {str(e)}", fg=typer.colors.RED))
|
326
|
+
except Exception as e:
|
327
|
+
logger.error(f"Unexpected execution error: {str(e)}")
|
328
|
+
typer.echo(typer.style(f"Unexpected error during execution: {str(e)}", fg=typer.colors.RED))
|
329
|
+
else:
|
330
|
+
logger.info("Program executed successfully")
|
331
|
+
|
332
|
+
@app.command()
|
333
|
+
def generate(
|
334
|
+
task: str = typer.Argument(
|
335
|
+
...,
|
336
|
+
help="The task description to generate a program for (e.g., 'Add 5 and 7 and print the result')"
|
337
|
+
),
|
338
|
+
model: str = typer.Option(
|
339
|
+
"gemini/gemini-2.0-flash",
|
340
|
+
"--model",
|
341
|
+
"-m",
|
342
|
+
help="The litellm model to use for generation (e.g., 'gpt-3.5-turbo', 'gpt-4')"
|
343
|
+
),
|
344
|
+
max_tokens: int = typer.Option(
|
345
|
+
4000,
|
346
|
+
"--max-tokens",
|
347
|
+
"-t",
|
348
|
+
help="Maximum number of tokens for the generated response (default: 4000)"
|
349
|
+
)
|
350
|
+
) -> None:
|
351
|
+
"""Generate and execute a Python program based on a task description"""
|
352
|
+
try:
|
353
|
+
# Run async core logic, preserving original execution style
|
354
|
+
asyncio.run(generate_core(task, model, max_tokens))
|
355
|
+
except Exception as e:
|
356
|
+
logger.error(f"Command failed: {str(e)}")
|
357
|
+
typer.echo(typer.style(f"Error: {str(e)}", fg=typer.colors.RED))
|
358
|
+
raise typer.Exit(code=1)
|
359
|
+
|
360
|
+
# Entry point, unchanged
|
361
|
+
def main() -> None:
|
362
|
+
logger.debug("Starting script execution")
|
363
|
+
app()
|
364
|
+
|
365
|
+
if __name__ == "__main__":
|
366
|
+
main()
|
@@ -33,6 +33,7 @@ class DuckDuckGoSearchTool(Tool):
|
|
33
33
|
"""
|
34
34
|
|
35
35
|
name: str = "duckduckgo_tool"
|
36
|
+
need_post_process: bool = False
|
36
37
|
description: str = "Retrieves search results from DuckDuckGo. " "Provides structured output of search results."
|
37
38
|
arguments: list = [
|
38
39
|
ToolArgument(
|
@@ -6,6 +6,10 @@ import signal
|
|
6
6
|
import subprocess
|
7
7
|
import sys
|
8
8
|
from typing import Dict, Optional, Union
|
9
|
+
from pathlib import Path
|
10
|
+
from datetime import datetime
|
11
|
+
import shutil
|
12
|
+
import re
|
9
13
|
|
10
14
|
from loguru import logger
|
11
15
|
|
@@ -24,7 +28,7 @@ class ExecuteBashCommandTool(Tool):
|
|
24
28
|
"""Tool for executing bash commands with real-time I/O handling."""
|
25
29
|
|
26
30
|
name: str = "execute_bash_tool"
|
27
|
-
description: str = "Executes a bash command and returns its output."
|
31
|
+
description: str = "Executes a bash command and returns its output. All commands are executed in /tmp for security."
|
28
32
|
need_validation: bool = True
|
29
33
|
arguments: list = [
|
30
34
|
ToolArgument(
|
@@ -34,14 +38,6 @@ class ExecuteBashCommandTool(Tool):
|
|
34
38
|
required=True,
|
35
39
|
example="ls -la",
|
36
40
|
),
|
37
|
-
ToolArgument(
|
38
|
-
name="working_dir",
|
39
|
-
arg_type="string",
|
40
|
-
description="The working directory where the command will be executed. Defaults to the current directory.",
|
41
|
-
required=False,
|
42
|
-
example="/path/to/directory",
|
43
|
-
default=os.getcwd(),
|
44
|
-
),
|
45
41
|
ToolArgument(
|
46
42
|
name="timeout",
|
47
43
|
arg_type="int",
|
@@ -52,15 +48,67 @@ class ExecuteBashCommandTool(Tool):
|
|
52
48
|
),
|
53
49
|
]
|
54
50
|
|
51
|
+
def _validate_command(self, command: str) -> None:
|
52
|
+
"""Validate the command for potential security risks."""
|
53
|
+
forbidden_commands = ["rm -rf /", "mkfs", "dd", ":(){ :|:& };:"]
|
54
|
+
for cmd in forbidden_commands:
|
55
|
+
if cmd in command.lower():
|
56
|
+
raise ValueError(f"Command '{command}' contains forbidden operation")
|
57
|
+
|
58
|
+
def _handle_mkdir_command(self, command: str) -> str:
|
59
|
+
"""Handle mkdir command with automatic cleanup and date suffix.
|
60
|
+
|
61
|
+
Args:
|
62
|
+
command: The original mkdir command
|
63
|
+
|
64
|
+
Returns:
|
65
|
+
Modified command that handles existing directories and adds date suffix
|
66
|
+
"""
|
67
|
+
try:
|
68
|
+
# Extract directory name from mkdir command
|
69
|
+
mkdir_pattern = r'mkdir\s+(?:-p\s+)?["\']?([^"\'>\n]+)["\']?'
|
70
|
+
match = re.search(mkdir_pattern, command)
|
71
|
+
if not match:
|
72
|
+
return command
|
73
|
+
|
74
|
+
dir_name = match.group(1)
|
75
|
+
# Add timestamp suffix
|
76
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
77
|
+
new_dir_name = f"{dir_name}_{timestamp}"
|
78
|
+
|
79
|
+
# Create cleanup and mkdir commands
|
80
|
+
cleanup_cmd = f'rm -rf "{dir_name}" 2>/dev/null; '
|
81
|
+
mkdir_cmd = command.replace(dir_name, new_dir_name)
|
82
|
+
|
83
|
+
return cleanup_cmd + mkdir_cmd
|
84
|
+
|
85
|
+
except Exception as e:
|
86
|
+
logger.warning(f"Error processing mkdir command: {e}")
|
87
|
+
return command
|
88
|
+
|
89
|
+
def _format_output(self, stdout: str, return_code: int, error: Optional[str] = None) -> str:
|
90
|
+
"""Format command output with stdout, return code, and optional error."""
|
91
|
+
formatted_result = "<command_output>\n"
|
92
|
+
formatted_result += f" <stdout>{stdout.strip()}</stdout>\n"
|
93
|
+
formatted_result += f" <returncode>{return_code}</returncode>\n"
|
94
|
+
if error:
|
95
|
+
formatted_result += f" <error>{error}</error>\n"
|
96
|
+
formatted_result += "</command_output>"
|
97
|
+
return formatted_result
|
98
|
+
|
55
99
|
def _execute_windows(
|
56
100
|
self,
|
57
101
|
command: str,
|
58
|
-
cwd: str,
|
59
102
|
timeout_seconds: int,
|
60
103
|
env_vars: Dict[str, str],
|
104
|
+
cwd: Optional[str] = None,
|
61
105
|
) -> str:
|
62
106
|
"""Execute command on Windows platform."""
|
63
107
|
try:
|
108
|
+
# Handle mkdir commands
|
109
|
+
if "mkdir" in command:
|
110
|
+
command = self._handle_mkdir_command(command)
|
111
|
+
|
64
112
|
# On Windows, use subprocess with pipes
|
65
113
|
process = subprocess.Popen(
|
66
114
|
command,
|
@@ -68,7 +116,7 @@ class ExecuteBashCommandTool(Tool):
|
|
68
116
|
stdin=subprocess.PIPE,
|
69
117
|
stdout=subprocess.PIPE,
|
70
118
|
stderr=subprocess.PIPE,
|
71
|
-
cwd=cwd,
|
119
|
+
cwd="/tmp", # cwd, Force /tmp directory
|
72
120
|
env=env_vars,
|
73
121
|
text=True,
|
74
122
|
encoding="utf-8",
|
@@ -77,34 +125,27 @@ class ExecuteBashCommandTool(Tool):
|
|
77
125
|
try:
|
78
126
|
stdout, stderr = process.communicate(timeout=timeout_seconds)
|
79
127
|
return_code = process.returncode
|
80
|
-
|
81
|
-
if return_code != 0 and stderr:
|
82
|
-
logger.warning(f"Command failed with error: {stderr}")
|
83
|
-
|
84
|
-
formatted_result = (
|
85
|
-
"<command_output>"
|
86
|
-
f" <stdout>{stdout.strip()}</stdout>"
|
87
|
-
f" <returncode>{return_code}</returncode>"
|
88
|
-
f"</command_output>"
|
89
|
-
)
|
90
|
-
return formatted_result
|
128
|
+
return self._format_output(stdout, return_code, stderr if stderr else None)
|
91
129
|
|
92
130
|
except subprocess.TimeoutExpired:
|
93
131
|
process.kill()
|
94
|
-
return f"Command timed out after {timeout_seconds} seconds
|
132
|
+
return self._format_output("", 1, f"Command timed out after {timeout_seconds} seconds")
|
95
133
|
|
96
134
|
except Exception as e:
|
97
|
-
return f"Unexpected error executing command: {str(e)}"
|
135
|
+
return self._format_output("", 1, f"Unexpected error executing command: {str(e)}")
|
98
136
|
|
99
137
|
def _execute_unix(
|
100
138
|
self,
|
101
139
|
command: str,
|
102
|
-
cwd: str,
|
103
140
|
timeout_seconds: int,
|
104
141
|
env_vars: Dict[str, str],
|
105
142
|
) -> str:
|
106
143
|
"""Execute command on Unix platform."""
|
107
144
|
try:
|
145
|
+
# Handle mkdir commands
|
146
|
+
if "mkdir" in command:
|
147
|
+
command = self._handle_mkdir_command(command)
|
148
|
+
|
108
149
|
master, slave = pty.openpty()
|
109
150
|
proc = subprocess.Popen(
|
110
151
|
command,
|
@@ -112,7 +153,7 @@ class ExecuteBashCommandTool(Tool):
|
|
112
153
|
stdin=slave,
|
113
154
|
stdout=slave,
|
114
155
|
stderr=subprocess.STDOUT,
|
115
|
-
cwd=cwd,
|
156
|
+
cwd="/tmp", # cwd=cwd, # Force /tmp directory
|
116
157
|
env=env_vars,
|
117
158
|
preexec_fn=os.setsid,
|
118
159
|
close_fds=True,
|
@@ -127,74 +168,90 @@ class ExecuteBashCommandTool(Tool):
|
|
127
168
|
rlist, _, _ = select.select([master, sys.stdin], [], [], timeout_seconds)
|
128
169
|
if not rlist:
|
129
170
|
if proc.poll() is not None:
|
130
|
-
break
|
171
|
+
break
|
131
172
|
raise subprocess.TimeoutExpired(command, timeout_seconds)
|
132
173
|
|
133
174
|
for fd in rlist:
|
134
175
|
if fd == master:
|
135
|
-
|
136
|
-
|
176
|
+
try:
|
177
|
+
data = os.read(master, 1024).decode()
|
178
|
+
if not data:
|
179
|
+
break_loop = True
|
180
|
+
break
|
181
|
+
stdout_buffer.append(data)
|
182
|
+
sys.stdout.write(data)
|
183
|
+
sys.stdout.flush()
|
184
|
+
except (OSError, UnicodeDecodeError) as e:
|
185
|
+
logger.warning(f"Error reading output: {e}")
|
137
186
|
break_loop = True
|
138
187
|
break
|
139
|
-
stdout_buffer.append(data)
|
140
|
-
sys.stdout.write(data)
|
141
|
-
sys.stdout.flush()
|
142
188
|
elif fd == sys.stdin:
|
143
|
-
|
144
|
-
|
189
|
+
try:
|
190
|
+
user_input = os.read(sys.stdin.fileno(), 1024)
|
191
|
+
os.write(master, user_input)
|
192
|
+
except OSError as e:
|
193
|
+
logger.warning(f"Error handling input: {e}")
|
145
194
|
|
146
195
|
if break_loop or proc.poll() is not None:
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
196
|
+
try:
|
197
|
+
while True:
|
198
|
+
data = os.read(master, 1024).decode()
|
199
|
+
if not data:
|
200
|
+
break
|
201
|
+
stdout_buffer.append(data)
|
202
|
+
sys.stdout.write(data)
|
203
|
+
sys.stdout.flush()
|
204
|
+
except (OSError, UnicodeDecodeError):
|
205
|
+
pass
|
154
206
|
break
|
155
207
|
|
156
208
|
except subprocess.TimeoutExpired:
|
157
209
|
os.killpg(os.getpgid(proc.pid), signal.SIGTERM)
|
158
|
-
return f"Command timed out after {timeout_seconds} seconds
|
210
|
+
return self._format_output("", 1, f"Command timed out after {timeout_seconds} seconds")
|
159
211
|
except EOFError:
|
160
|
-
pass
|
212
|
+
pass
|
161
213
|
finally:
|
162
214
|
os.close(master)
|
163
215
|
proc.wait()
|
164
216
|
|
165
217
|
stdout_content = "".join(stdout_buffer)
|
166
218
|
return_code = proc.returncode
|
167
|
-
|
168
|
-
|
169
|
-
f" <stdout>{stdout_content.strip()}</stdout>"
|
170
|
-
f" <returncode>{return_code}</returncode>"
|
171
|
-
f"</command_output>"
|
172
|
-
)
|
173
|
-
return formatted_result
|
219
|
+
return self._format_output(stdout_content, return_code)
|
220
|
+
|
174
221
|
except Exception as e:
|
175
|
-
return f"Unexpected error executing command: {str(e)}"
|
222
|
+
return self._format_output("", 1, f"Unexpected error executing command: {str(e)}")
|
176
223
|
|
177
224
|
def execute(
|
178
225
|
self,
|
179
226
|
command: str,
|
180
|
-
working_dir: Optional[str] = None,
|
227
|
+
working_dir: Optional[str] = None, # Kept for backward compatibility but ignored
|
181
228
|
timeout: Union[int, str, None] = 60,
|
182
229
|
env: Optional[Dict[str, str]] = None,
|
183
230
|
) -> str:
|
184
|
-
"""Executes a bash command with interactive input handling."""
|
231
|
+
"""Executes a bash command with interactive input handling in /tmp directory."""
|
232
|
+
# Ensure /tmp exists and is writable
|
233
|
+
tmp_dir = Path("/tmp")
|
234
|
+
if not (tmp_dir.exists() and os.access(tmp_dir, os.W_OK)):
|
235
|
+
return self._format_output("", 1, "Error: /tmp directory is not accessible")
|
236
|
+
|
237
|
+
# Validate command
|
238
|
+
try:
|
239
|
+
self._validate_command(command)
|
240
|
+
except ValueError as e:
|
241
|
+
return self._format_output("", 1, str(e))
|
242
|
+
|
185
243
|
timeout_seconds = int(timeout) if timeout else 60
|
186
|
-
cwd = working_dir or os.getcwd()
|
187
244
|
env_vars = os.environ.copy()
|
188
245
|
if env:
|
189
246
|
env_vars.update(env)
|
190
247
|
|
191
248
|
if sys.platform == "win32":
|
192
|
-
return self._execute_windows(command,
|
249
|
+
return self._execute_windows(command, timeout_seconds, env_vars)
|
193
250
|
else:
|
194
251
|
if not pty:
|
195
252
|
logger.warning("PTY module not available, falling back to Windows-style execution")
|
196
|
-
return self._execute_windows(command,
|
197
|
-
return self._execute_unix(command,
|
253
|
+
return self._execute_windows(command, timeout_seconds, env_vars)
|
254
|
+
return self._execute_unix(command, timeout_seconds, env_vars)
|
198
255
|
|
199
256
|
|
200
257
|
if __name__ == "__main__":
|