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.
Files changed (81) hide show
  1. quantalogic/agent.py +268 -24
  2. quantalogic/agent_config.py +5 -5
  3. quantalogic/agent_factory.py +2 -2
  4. quantalogic/codeact/__init__.py +0 -0
  5. quantalogic/codeact/agent.py +499 -0
  6. quantalogic/codeact/cli.py +232 -0
  7. quantalogic/codeact/constants.py +9 -0
  8. quantalogic/codeact/events.py +78 -0
  9. quantalogic/codeact/llm_util.py +76 -0
  10. quantalogic/codeact/prompts/error_format.j2 +11 -0
  11. quantalogic/codeact/prompts/generate_action.j2 +26 -0
  12. quantalogic/codeact/prompts/generate_program.j2 +39 -0
  13. quantalogic/codeact/prompts/response_format.j2 +11 -0
  14. quantalogic/codeact/tools_manager.py +135 -0
  15. quantalogic/codeact/utils.py +135 -0
  16. quantalogic/coding_agent.py +2 -2
  17. quantalogic/create_custom_agent.py +26 -78
  18. quantalogic/prompts/chat_system_prompt.j2 +10 -7
  19. quantalogic/prompts/code_2_system_prompt.j2 +190 -0
  20. quantalogic/prompts/code_system_prompt.j2 +142 -0
  21. quantalogic/prompts/doc_system_prompt.j2 +178 -0
  22. quantalogic/prompts/legal_2_system_prompt.j2 +218 -0
  23. quantalogic/prompts/legal_system_prompt.j2 +140 -0
  24. quantalogic/prompts/system_prompt.j2 +6 -2
  25. quantalogic/prompts/tools_prompt.j2 +2 -4
  26. quantalogic/prompts.py +23 -4
  27. quantalogic/python_interpreter/__init__.py +23 -0
  28. quantalogic/python_interpreter/assignment_visitors.py +63 -0
  29. quantalogic/python_interpreter/base_visitors.py +20 -0
  30. quantalogic/python_interpreter/class_visitors.py +22 -0
  31. quantalogic/python_interpreter/comprehension_visitors.py +172 -0
  32. quantalogic/python_interpreter/context_visitors.py +59 -0
  33. quantalogic/python_interpreter/control_flow_visitors.py +88 -0
  34. quantalogic/python_interpreter/exception_visitors.py +109 -0
  35. quantalogic/python_interpreter/exceptions.py +39 -0
  36. quantalogic/python_interpreter/execution.py +202 -0
  37. quantalogic/python_interpreter/function_utils.py +386 -0
  38. quantalogic/python_interpreter/function_visitors.py +209 -0
  39. quantalogic/python_interpreter/import_visitors.py +28 -0
  40. quantalogic/python_interpreter/interpreter_core.py +358 -0
  41. quantalogic/python_interpreter/literal_visitors.py +74 -0
  42. quantalogic/python_interpreter/misc_visitors.py +148 -0
  43. quantalogic/python_interpreter/operator_visitors.py +108 -0
  44. quantalogic/python_interpreter/scope.py +10 -0
  45. quantalogic/python_interpreter/visit_handlers.py +110 -0
  46. quantalogic/server/agent_server.py +1 -1
  47. quantalogic/tools/__init__.py +6 -3
  48. quantalogic/tools/action_gen.py +366 -0
  49. quantalogic/tools/duckduckgo_search_tool.py +1 -0
  50. quantalogic/tools/execute_bash_command_tool.py +114 -57
  51. quantalogic/tools/file_tracker_tool.py +49 -0
  52. quantalogic/tools/google_packages/google_news_tool.py +3 -0
  53. quantalogic/tools/image_generation/dalle_e.py +89 -137
  54. quantalogic/tools/python_tool.py +13 -0
  55. quantalogic/tools/rag_tool/__init__.py +2 -9
  56. quantalogic/tools/rag_tool/document_rag_sources_.py +728 -0
  57. quantalogic/tools/rag_tool/ocr_pdf_markdown.py +144 -0
  58. quantalogic/tools/replace_in_file_tool.py +1 -1
  59. quantalogic/tools/{search_definition_names.py → search_definition_names_tool.py} +2 -2
  60. quantalogic/tools/terminal_capture_tool.py +293 -0
  61. quantalogic/tools/tool.py +120 -22
  62. quantalogic/tools/utilities/__init__.py +2 -0
  63. quantalogic/tools/utilities/download_file_tool.py +3 -5
  64. quantalogic/tools/utilities/llm_tool.py +283 -0
  65. quantalogic/tools/utilities/selenium_tool.py +296 -0
  66. quantalogic/tools/utilities/vscode_tool.py +1 -1
  67. quantalogic/tools/web_navigation/__init__.py +5 -0
  68. quantalogic/tools/web_navigation/web_tool.py +145 -0
  69. quantalogic/tools/write_file_tool.py +72 -36
  70. quantalogic/utils/__init__.py +0 -1
  71. quantalogic/utils/test_python_interpreter.py +119 -0
  72. {quantalogic-0.59.3.dist-info → quantalogic-0.61.0.dist-info}/METADATA +7 -2
  73. {quantalogic-0.59.3.dist-info → quantalogic-0.61.0.dist-info}/RECORD +76 -35
  74. quantalogic/tools/rag_tool/document_metadata.py +0 -15
  75. quantalogic/tools/rag_tool/query_response.py +0 -20
  76. quantalogic/tools/rag_tool/rag_tool.py +0 -566
  77. quantalogic/tools/rag_tool/rag_tool_beta.py +0 -264
  78. quantalogic/utils/python_interpreter.py +0 -905
  79. {quantalogic-0.59.3.dist-info → quantalogic-0.61.0.dist-info}/LICENSE +0 -0
  80. {quantalogic-0.59.3.dist-info → quantalogic-0.61.0.dist-info}/WHEEL +0 -0
  81. {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 # Process completed but select timed out
171
+ break
131
172
  raise subprocess.TimeoutExpired(command, timeout_seconds)
132
173
 
133
174
  for fd in rlist:
134
175
  if fd == master:
135
- data = os.read(master, 1024).decode()
136
- if not data:
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
- user_input = os.read(sys.stdin.fileno(), 1024)
144
- os.write(master, user_input)
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
- while True:
148
- data = os.read(master, 1024).decode()
149
- if not data:
150
- break
151
- stdout_buffer.append(data)
152
- sys.stdout.write(data)
153
- sys.stdout.flush()
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 # Process exited normally
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
- formatted_result = (
168
- "<command_output>"
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, cwd, timeout_seconds, env_vars)
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, cwd, timeout_seconds, env_vars)
197
- return self._execute_unix(command, cwd, timeout_seconds, env_vars)
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__":