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,296 @@
|
|
1
|
+
"""Selenium Tool for web automation and testing.
|
2
|
+
|
3
|
+
This tool provides a high-level interface for web automation tasks using Selenium WebDriver.
|
4
|
+
It supports common web interactions like navigation, form filling, and element manipulation.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import asyncio
|
8
|
+
from typing import Optional, List, Dict, Any
|
9
|
+
from selenium import webdriver
|
10
|
+
from selenium.webdriver.common.by import By
|
11
|
+
from selenium.webdriver.support.ui import WebDriverWait
|
12
|
+
from selenium.webdriver.support import expected_conditions as EC
|
13
|
+
from selenium.webdriver.chrome.service import Service
|
14
|
+
from selenium.webdriver.chrome.options import Options
|
15
|
+
from webdriver_manager.chrome import ChromeDriverManager
|
16
|
+
from pydantic import Field, ConfigDict
|
17
|
+
from loguru import logger
|
18
|
+
|
19
|
+
from quantalogic.tools.tool import Tool, ToolArgument
|
20
|
+
|
21
|
+
class SeleniumTool(Tool):
|
22
|
+
"""Tool for web automation using Selenium WebDriver."""
|
23
|
+
|
24
|
+
model_config = ConfigDict(arbitrary_types_allowed=True)
|
25
|
+
|
26
|
+
name: str = Field(default="selenium_tool")
|
27
|
+
description: str = Field(
|
28
|
+
default=(
|
29
|
+
"Automates web browser interactions using Selenium WebDriver. "
|
30
|
+
"Supports navigation, form filling, clicking, and extracting content. "
|
31
|
+
"Uses Chrome browser in headless mode by default."
|
32
|
+
)
|
33
|
+
)
|
34
|
+
arguments: list = Field(
|
35
|
+
default=[
|
36
|
+
ToolArgument(
|
37
|
+
name="action",
|
38
|
+
arg_type="string",
|
39
|
+
description=(
|
40
|
+
"The automation action to perform. Available actions: "
|
41
|
+
"navigate, click, type, extract_text, extract_attribute, wait_for_element"
|
42
|
+
),
|
43
|
+
required=True,
|
44
|
+
example="navigate",
|
45
|
+
),
|
46
|
+
ToolArgument(
|
47
|
+
name="url",
|
48
|
+
arg_type="string",
|
49
|
+
description="URL to navigate to (required for 'navigate' action)",
|
50
|
+
required=False,
|
51
|
+
example="https://example.com",
|
52
|
+
),
|
53
|
+
ToolArgument(
|
54
|
+
name="selector",
|
55
|
+
arg_type="string",
|
56
|
+
description="CSS or XPath selector for target element",
|
57
|
+
required=False,
|
58
|
+
example="#login-button",
|
59
|
+
),
|
60
|
+
ToolArgument(
|
61
|
+
name="selector_type",
|
62
|
+
arg_type="string",
|
63
|
+
description="Type of selector (css, xpath, id, name, class_name)",
|
64
|
+
required=False,
|
65
|
+
default="css",
|
66
|
+
example="css",
|
67
|
+
),
|
68
|
+
ToolArgument(
|
69
|
+
name="value",
|
70
|
+
arg_type="string",
|
71
|
+
description="Value to type or attribute to extract",
|
72
|
+
required=False,
|
73
|
+
example="username123",
|
74
|
+
),
|
75
|
+
ToolArgument(
|
76
|
+
name="timeout",
|
77
|
+
arg_type="int",
|
78
|
+
description="Maximum time to wait for element (seconds)",
|
79
|
+
required=False,
|
80
|
+
default="10",
|
81
|
+
example="10",
|
82
|
+
),
|
83
|
+
]
|
84
|
+
)
|
85
|
+
|
86
|
+
driver: Optional[webdriver.Chrome] = Field(default=None, exclude=True)
|
87
|
+
headless: bool = Field(default=True, description="Run browser in headless mode")
|
88
|
+
custom_options: List[str] = Field(default_factory=list, description="Custom Chrome options")
|
89
|
+
|
90
|
+
def __init__(
|
91
|
+
self,
|
92
|
+
headless: bool = True,
|
93
|
+
custom_options: Optional[List[str]] = None,
|
94
|
+
name: str = "selenium_tool"
|
95
|
+
):
|
96
|
+
"""Initialize SeleniumTool with browser configuration.
|
97
|
+
|
98
|
+
Args:
|
99
|
+
headless (bool): Run browser in headless mode. Defaults to True.
|
100
|
+
custom_options (List[str], optional): Custom Chrome options.
|
101
|
+
name (str): Name of the tool instance.
|
102
|
+
"""
|
103
|
+
super().__init__(
|
104
|
+
**{
|
105
|
+
"headless": headless,
|
106
|
+
"custom_options": custom_options or [],
|
107
|
+
"name": name,
|
108
|
+
}
|
109
|
+
)
|
110
|
+
self._initialize_driver()
|
111
|
+
|
112
|
+
def _initialize_driver(self):
|
113
|
+
"""Initialize Chrome WebDriver with configured options."""
|
114
|
+
try:
|
115
|
+
chrome_options = Options()
|
116
|
+
if self.headless:
|
117
|
+
chrome_options.add_argument("--headless")
|
118
|
+
|
119
|
+
# Add common options for stability
|
120
|
+
chrome_options.add_argument("--no-sandbox")
|
121
|
+
chrome_options.add_argument("--disable-dev-shm-usage")
|
122
|
+
|
123
|
+
# Add custom options
|
124
|
+
for option in self.custom_options:
|
125
|
+
chrome_options.add_argument(option)
|
126
|
+
|
127
|
+
# Initialize the driver
|
128
|
+
service = Service(ChromeDriverManager().install())
|
129
|
+
self.driver = webdriver.Chrome(service=service, options=chrome_options)
|
130
|
+
self.driver.implicitly_wait(5)
|
131
|
+
logger.info("Successfully initialized Chrome WebDriver")
|
132
|
+
|
133
|
+
except Exception as e:
|
134
|
+
logger.error(f"Failed to initialize WebDriver: {str(e)}")
|
135
|
+
raise
|
136
|
+
|
137
|
+
def _get_by_method(self, selector_type: str) -> By:
|
138
|
+
"""Get the appropriate By method based on selector type.
|
139
|
+
|
140
|
+
Args:
|
141
|
+
selector_type: Type of selector (css, xpath, id, name, class_name)
|
142
|
+
|
143
|
+
Returns:
|
144
|
+
selenium.webdriver.common.by.By method
|
145
|
+
"""
|
146
|
+
selector_types = {
|
147
|
+
"css": By.CSS_SELECTOR,
|
148
|
+
"xpath": By.XPATH,
|
149
|
+
"id": By.ID,
|
150
|
+
"name": By.NAME,
|
151
|
+
"class_name": By.CLASS_NAME,
|
152
|
+
}
|
153
|
+
return selector_types.get(selector_type.lower(), By.CSS_SELECTOR)
|
154
|
+
|
155
|
+
async def async_execute(
|
156
|
+
self,
|
157
|
+
action: str,
|
158
|
+
url: Optional[str] = None,
|
159
|
+
selector: Optional[str] = None,
|
160
|
+
selector_type: str = "css",
|
161
|
+
value: Optional[str] = None,
|
162
|
+
timeout: int = 10,
|
163
|
+
) -> Dict[str, Any]:
|
164
|
+
"""Execute a Selenium automation action asynchronously.
|
165
|
+
|
166
|
+
Args:
|
167
|
+
action: The automation action to perform
|
168
|
+
url: URL to navigate to (for navigate action)
|
169
|
+
selector: Element selector
|
170
|
+
selector_type: Type of selector (css, xpath, id, name, class_name)
|
171
|
+
value: Value to type or attribute to extract
|
172
|
+
timeout: Maximum time to wait for element (seconds)
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
Dict containing action result and any extracted data
|
176
|
+
"""
|
177
|
+
try:
|
178
|
+
if not self.driver:
|
179
|
+
self._initialize_driver()
|
180
|
+
|
181
|
+
result = {"success": False, "message": "", "data": None}
|
182
|
+
|
183
|
+
# Handle different actions
|
184
|
+
if action == "navigate":
|
185
|
+
if not url:
|
186
|
+
raise ValueError("URL is required for navigate action")
|
187
|
+
self.driver.get(url)
|
188
|
+
result["success"] = True
|
189
|
+
result["message"] = f"Successfully navigated to {url}"
|
190
|
+
|
191
|
+
elif action in ["click", "type", "extract_text", "extract_attribute", "wait_for_element"]:
|
192
|
+
if not selector:
|
193
|
+
raise ValueError("Selector is required for element actions")
|
194
|
+
|
195
|
+
# Wait for element
|
196
|
+
by_method = self._get_by_method(selector_type)
|
197
|
+
element = WebDriverWait(self.driver, timeout).until(
|
198
|
+
EC.presence_of_element_located((by_method, selector))
|
199
|
+
)
|
200
|
+
|
201
|
+
if action == "click":
|
202
|
+
element.click()
|
203
|
+
result["success"] = True
|
204
|
+
result["message"] = f"Successfully clicked element: {selector}"
|
205
|
+
|
206
|
+
elif action == "type":
|
207
|
+
if not value:
|
208
|
+
raise ValueError("Value is required for type action")
|
209
|
+
element.clear()
|
210
|
+
element.send_keys(value)
|
211
|
+
result["success"] = True
|
212
|
+
result["message"] = f"Successfully typed '{value}' into element: {selector}"
|
213
|
+
|
214
|
+
elif action == "extract_text":
|
215
|
+
text = element.text
|
216
|
+
result["success"] = True
|
217
|
+
result["message"] = "Successfully extracted text"
|
218
|
+
result["data"] = text
|
219
|
+
|
220
|
+
elif action == "extract_attribute":
|
221
|
+
if not value:
|
222
|
+
raise ValueError("Attribute name is required for extract_attribute action")
|
223
|
+
attr_value = element.get_attribute(value)
|
224
|
+
result["success"] = True
|
225
|
+
result["message"] = f"Successfully extracted attribute: {value}"
|
226
|
+
result["data"] = attr_value
|
227
|
+
|
228
|
+
elif action == "wait_for_element":
|
229
|
+
result["success"] = True
|
230
|
+
result["message"] = f"Element found: {selector}"
|
231
|
+
|
232
|
+
else:
|
233
|
+
raise ValueError(f"Unknown action: {action}")
|
234
|
+
|
235
|
+
return result
|
236
|
+
|
237
|
+
except Exception as e:
|
238
|
+
logger.error(f"Error in SeleniumTool.async_execute: {str(e)}")
|
239
|
+
return {
|
240
|
+
"success": False,
|
241
|
+
"message": f"Error: {str(e)}",
|
242
|
+
"data": None
|
243
|
+
}
|
244
|
+
|
245
|
+
def execute(
|
246
|
+
self,
|
247
|
+
action: str,
|
248
|
+
url: Optional[str] = None,
|
249
|
+
selector: Optional[str] = None,
|
250
|
+
selector_type: str = "css",
|
251
|
+
value: Optional[str] = None,
|
252
|
+
timeout: int = 10,
|
253
|
+
) -> Dict[str, Any]:
|
254
|
+
"""Synchronous wrapper for async_execute."""
|
255
|
+
return asyncio.run(
|
256
|
+
self.async_execute(
|
257
|
+
action=action,
|
258
|
+
url=url,
|
259
|
+
selector=selector,
|
260
|
+
selector_type=selector_type,
|
261
|
+
value=value,
|
262
|
+
timeout=timeout,
|
263
|
+
)
|
264
|
+
)
|
265
|
+
|
266
|
+
def __del__(self):
|
267
|
+
"""Clean up WebDriver when the tool is destroyed."""
|
268
|
+
if self.driver:
|
269
|
+
try:
|
270
|
+
self.driver.quit()
|
271
|
+
logger.info("Successfully closed WebDriver")
|
272
|
+
except Exception as e:
|
273
|
+
logger.error(f"Error closing WebDriver: {str(e)}")
|
274
|
+
|
275
|
+
|
276
|
+
if __name__ == "__main__":
|
277
|
+
# Example usage
|
278
|
+
tool = SeleniumTool(headless=True)
|
279
|
+
|
280
|
+
# Navigate to a website
|
281
|
+
result = tool.execute(
|
282
|
+
action="navigate",
|
283
|
+
url="https://example.com"
|
284
|
+
)
|
285
|
+
print("Navigation result:", result)
|
286
|
+
|
287
|
+
# Extract text from an element
|
288
|
+
result = tool.execute(
|
289
|
+
action="extract_text",
|
290
|
+
selector="h1",
|
291
|
+
selector_type="css"
|
292
|
+
)
|
293
|
+
print("Extracted text:", result)
|
294
|
+
|
295
|
+
# Clean up
|
296
|
+
del tool
|
@@ -15,7 +15,7 @@ class VSCodeServerTool(Tool):
|
|
15
15
|
|
16
16
|
name: str = "vscode_tool"
|
17
17
|
description: str = "Launches a VS Code Server instance for remote development."
|
18
|
-
need_validation: bool =
|
18
|
+
need_validation: bool = False
|
19
19
|
arguments: list = [
|
20
20
|
ToolArgument(
|
21
21
|
name="workspace_path",
|
@@ -0,0 +1,145 @@
|
|
1
|
+
"""Web Navigation Tool using browser-use for automated web interactions."""
|
2
|
+
|
3
|
+
import asyncio
|
4
|
+
from typing import Callable, Optional
|
5
|
+
|
6
|
+
from loguru import logger
|
7
|
+
from pydantic import ConfigDict, Field
|
8
|
+
|
9
|
+
from browser_use import Agent
|
10
|
+
from langchain_openai import ChatOpenAI
|
11
|
+
from quantalogic.tools.tool import Tool, ToolArgument
|
12
|
+
|
13
|
+
|
14
|
+
class WebNavigationTool(Tool):
|
15
|
+
"""Tool for automated web navigation and interaction using browser-use."""
|
16
|
+
|
17
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, extra="allow")
|
18
|
+
|
19
|
+
name: str = Field(default="web_navigation")
|
20
|
+
description: str = Field(
|
21
|
+
default=(
|
22
|
+
"Navigate and interact with web pages using natural language instructions. "
|
23
|
+
"This tool can perform tasks like searching, comparing prices, filling forms, "
|
24
|
+
"and extracting information from websites."
|
25
|
+
)
|
26
|
+
)
|
27
|
+
arguments: list = Field(
|
28
|
+
default=[
|
29
|
+
ToolArgument(
|
30
|
+
name="task",
|
31
|
+
arg_type="string",
|
32
|
+
description="The web navigation task to perform in natural language",
|
33
|
+
required=True,
|
34
|
+
example="Search Python documentation for asyncio examples",
|
35
|
+
),
|
36
|
+
ToolArgument(
|
37
|
+
name="model_name",
|
38
|
+
arg_type="string",
|
39
|
+
description="The OpenAI model to use (e.g. gpt-3.5-turbo, gpt-4)",
|
40
|
+
required=True,
|
41
|
+
default="gpt-3.5-turbo",
|
42
|
+
example="gpt-3.5-turbo",
|
43
|
+
),
|
44
|
+
]
|
45
|
+
)
|
46
|
+
|
47
|
+
llm: Optional[ChatOpenAI] = Field(default=None, exclude=True)
|
48
|
+
agent: Optional[Agent] = Field(default=None, exclude=True)
|
49
|
+
|
50
|
+
def __init__(
|
51
|
+
self,
|
52
|
+
model_name: str = "gpt-3.5-turbo",
|
53
|
+
name: str = "web_navigation",
|
54
|
+
) -> None:
|
55
|
+
"""Initialize the WebNavigationTool.
|
56
|
+
|
57
|
+
Args:
|
58
|
+
model_name: OpenAI model to use. Defaults to "gpt-3.5-turbo".
|
59
|
+
name: Name of the tool instance. Defaults to "web_navigation".
|
60
|
+
"""
|
61
|
+
super().__init__(
|
62
|
+
**{
|
63
|
+
"name": name,
|
64
|
+
"model_name": model_name,
|
65
|
+
}
|
66
|
+
)
|
67
|
+
self.model_post_init(None)
|
68
|
+
|
69
|
+
def model_post_init(self, __context: None) -> None:
|
70
|
+
"""Initialize the LLM after model initialization.
|
71
|
+
|
72
|
+
Args:
|
73
|
+
__context: Unused context parameter.
|
74
|
+
|
75
|
+
Raises:
|
76
|
+
ValueError: If LLM initialization fails.
|
77
|
+
"""
|
78
|
+
try:
|
79
|
+
self.llm = ChatOpenAI(model=self.model_name)
|
80
|
+
logger.debug(f"Initialized WebNavigationTool with model: {self.model_name}")
|
81
|
+
except Exception as e:
|
82
|
+
logger.error(f"Error initializing LLM: {e}")
|
83
|
+
raise ValueError(f"Failed to initialize LLM: {e}") from e
|
84
|
+
|
85
|
+
async def async_execute(self, task: str, model_name: Optional[str] = None) -> str:
|
86
|
+
"""Execute the web navigation task asynchronously.
|
87
|
+
|
88
|
+
Args:
|
89
|
+
task: The web navigation task to perform.
|
90
|
+
model_name: Optional override for the LLM model.
|
91
|
+
|
92
|
+
Returns:
|
93
|
+
The result of the web navigation task.
|
94
|
+
|
95
|
+
Raises:
|
96
|
+
ValueError: If task is empty or LLM is not initialized.
|
97
|
+
RuntimeError: If web navigation fails.
|
98
|
+
"""
|
99
|
+
if not task:
|
100
|
+
raise ValueError("Task cannot be empty")
|
101
|
+
|
102
|
+
if not self.llm:
|
103
|
+
raise ValueError("LLM not initialized")
|
104
|
+
|
105
|
+
try:
|
106
|
+
# Create a new Agent instance for each task
|
107
|
+
agent = Agent(
|
108
|
+
task=task,
|
109
|
+
llm=self.llm,
|
110
|
+
)
|
111
|
+
|
112
|
+
# Run the agent
|
113
|
+
result = await agent.run()
|
114
|
+
logger.debug(f"Completed web navigation task: {task}")
|
115
|
+
return result
|
116
|
+
|
117
|
+
except Exception as e:
|
118
|
+
logger.error(f"Error during web navigation: {e}")
|
119
|
+
raise RuntimeError(f"Web navigation failed: {e}") from e
|
120
|
+
|
121
|
+
def execute(self, task: str, model_name: Optional[str] = None) -> str:
|
122
|
+
"""Execute the web navigation task synchronously.
|
123
|
+
|
124
|
+
Args:
|
125
|
+
task: The web navigation task to perform.
|
126
|
+
model_name: Optional override for the LLM model.
|
127
|
+
|
128
|
+
Returns:
|
129
|
+
The result of the web navigation task.
|
130
|
+
"""
|
131
|
+
return asyncio.run(self.async_execute(task=task, model_name=model_name))
|
132
|
+
|
133
|
+
|
134
|
+
if __name__ == "__main__":
|
135
|
+
# Example usage
|
136
|
+
tool = WebNavigationTool()
|
137
|
+
task = "Search Python documentation for asyncio examples"
|
138
|
+
|
139
|
+
try:
|
140
|
+
# Synchronous execution
|
141
|
+
result = tool.execute(task=task)
|
142
|
+
print("Navigation Result:")
|
143
|
+
print(result)
|
144
|
+
except Exception as e:
|
145
|
+
logger.error(f"Example failed: {e}")
|
@@ -1,23 +1,25 @@
|
|
1
1
|
"""Tool for writing a file and returning its content."""
|
2
2
|
|
3
3
|
import os
|
4
|
+
from pathlib import Path
|
4
5
|
|
6
|
+
from loguru import logger
|
5
7
|
from quantalogic.tools.tool import Tool, ToolArgument
|
6
8
|
|
7
9
|
|
8
10
|
class WriteFileTool(Tool):
|
9
|
-
"""Tool for writing a text file."""
|
11
|
+
"""Tool for writing a text file in /tmp directory."""
|
10
12
|
|
11
13
|
name: str = "write_file_tool"
|
12
|
-
description: str = "Writes a file with the given content. The tool will fail if the file already exists when not used in append mode."
|
14
|
+
description: str = "Writes a file with the given content in /tmp directory. The tool will fail if the file already exists when not used in append mode."
|
13
15
|
need_validation: bool = True
|
14
16
|
arguments: list = [
|
15
17
|
ToolArgument(
|
16
18
|
name="file_path",
|
17
19
|
arg_type="string",
|
18
|
-
description="The
|
20
|
+
description="The name of the file to write in /tmp directory. Can include subdirectories within /tmp.",
|
19
21
|
required=True,
|
20
|
-
example="/
|
22
|
+
example="/tmp/myfile.txt or myfile.txt",
|
21
23
|
),
|
22
24
|
ToolArgument(
|
23
25
|
name="content",
|
@@ -47,50 +49,84 @@ class WriteFileTool(Tool):
|
|
47
49
|
),
|
48
50
|
]
|
49
51
|
|
52
|
+
def _ensure_tmp_path(self, file_path: str) -> str:
|
53
|
+
"""Ensures the file path is within /tmp directory.
|
54
|
+
|
55
|
+
Args:
|
56
|
+
file_path (str): The original file path
|
57
|
+
|
58
|
+
Returns:
|
59
|
+
str: Normalized path within /tmp
|
60
|
+
|
61
|
+
Raises:
|
62
|
+
ValueError: If the path attempts to escape /tmp
|
63
|
+
"""
|
64
|
+
# Ensure /tmp exists and is writable
|
65
|
+
tmp_dir = Path("/tmp")
|
66
|
+
if not (tmp_dir.exists() and os.access(tmp_dir, os.W_OK)):
|
67
|
+
raise ValueError("Error: /tmp directory is not accessible")
|
68
|
+
|
69
|
+
# If path doesn't start with /tmp, prepend it
|
70
|
+
if not file_path.startswith("/tmp/"):
|
71
|
+
file_path = os.path.join("/tmp", file_path.lstrip("/"))
|
72
|
+
|
73
|
+
# Resolve the absolute path and check if it's really in /tmp
|
74
|
+
real_path = os.path.realpath(file_path)
|
75
|
+
if not real_path.startswith("/tmp/"):
|
76
|
+
raise ValueError("Error: Cannot write files outside of /tmp directory")
|
77
|
+
|
78
|
+
return real_path
|
79
|
+
|
50
80
|
def execute(self, file_path: str, content: str, append_mode: str = "False", overwrite: str = "False") -> str:
|
51
|
-
"""Writes a file with the given content.
|
81
|
+
"""Writes a file with the given content in /tmp directory.
|
52
82
|
|
53
83
|
Args:
|
54
|
-
file_path (str): The path to the file to write.
|
84
|
+
file_path (str): The path to the file to write (will be forced to /tmp).
|
55
85
|
content (str): The content to write to the file.
|
56
86
|
append_mode (str, optional): If true, append content to existing file. Defaults to "False".
|
57
87
|
overwrite (str, optional): If true, overwrite existing file. Defaults to "False".
|
58
88
|
|
59
89
|
Returns:
|
60
|
-
str:
|
90
|
+
str: Status message with file path and size.
|
61
91
|
|
62
92
|
Raises:
|
63
93
|
FileExistsError: If the file already exists and append_mode is False and overwrite is False.
|
94
|
+
ValueError: If attempting to write outside /tmp or if /tmp is not accessible.
|
64
95
|
"""
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
f.
|
92
|
-
|
93
|
-
|
96
|
+
try:
|
97
|
+
# Convert mode strings to booleans
|
98
|
+
append_mode_bool = append_mode.lower() in ["true", "1", "yes"]
|
99
|
+
overwrite_bool = overwrite.lower() in ["true", "1", "yes"]
|
100
|
+
|
101
|
+
# Ensure path is in /tmp and normalize it
|
102
|
+
file_path = self._ensure_tmp_path(file_path)
|
103
|
+
|
104
|
+
# Ensure parent directory exists (only within /tmp)
|
105
|
+
parent_dir = os.path.dirname(file_path)
|
106
|
+
if parent_dir.startswith("/tmp/"):
|
107
|
+
os.makedirs(parent_dir, exist_ok=True)
|
108
|
+
|
109
|
+
# Determine file write mode based on append_mode
|
110
|
+
mode = "a" if append_mode_bool else "w"
|
111
|
+
|
112
|
+
# Check if file already exists and not in append mode and not in overwrite mode
|
113
|
+
if os.path.exists(file_path) and not append_mode_bool and not overwrite_bool:
|
114
|
+
raise FileExistsError(
|
115
|
+
f"File {file_path} already exists. Set append_mode=True to append or overwrite=True to overwrite."
|
116
|
+
)
|
117
|
+
|
118
|
+
with open(file_path, mode, encoding="utf-8") as f:
|
119
|
+
f.write(content)
|
120
|
+
|
121
|
+
file_size = os.path.getsize(file_path)
|
122
|
+
return f"File {file_path} {'appended to' if append_mode_bool else 'written'} successfully. Size: {file_size} bytes."
|
123
|
+
|
124
|
+
except (ValueError, FileExistsError) as e:
|
125
|
+
logger.error(f"Write file error: {str(e)}")
|
126
|
+
raise
|
127
|
+
except Exception as e:
|
128
|
+
logger.error(f"Unexpected error writing file: {str(e)}")
|
129
|
+
raise ValueError(f"Failed to write file: {str(e)}")
|
94
130
|
|
95
131
|
|
96
132
|
if __name__ == "__main__":
|
quantalogic/utils/__init__.py
CHANGED
@@ -6,7 +6,6 @@ from .get_environment import get_environment
|
|
6
6
|
from .get_coding_environment import get_coding_environment
|
7
7
|
from .get_quantalogic_rules_content import get_quantalogic_rules_file_content
|
8
8
|
from .lm_studio_model_info import get_model_list
|
9
|
-
from .python_interpreter import interpret_ast
|
10
9
|
|
11
10
|
__all__ = [
|
12
11
|
"download_http_file",
|