quantalogic 0.35.0__py3-none-any.whl → 0.40.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/__init__.py +0 -4
- quantalogic/agent.py +603 -363
- quantalogic/agent_config.py +233 -46
- quantalogic/agent_factory.py +34 -22
- quantalogic/coding_agent.py +16 -14
- quantalogic/config.py +2 -1
- quantalogic/console_print_events.py +4 -8
- quantalogic/console_print_token.py +2 -2
- quantalogic/docs_cli.py +15 -10
- quantalogic/event_emitter.py +258 -83
- quantalogic/flow/__init__.py +23 -0
- quantalogic/flow/flow.py +595 -0
- quantalogic/flow/flow_extractor.py +672 -0
- quantalogic/flow/flow_generator.py +89 -0
- quantalogic/flow/flow_manager.py +407 -0
- quantalogic/flow/flow_manager_schema.py +169 -0
- quantalogic/flow/flow_yaml.md +419 -0
- quantalogic/generative_model.py +109 -77
- quantalogic/get_model_info.py +5 -5
- quantalogic/interactive_text_editor.py +100 -73
- quantalogic/main.py +17 -21
- quantalogic/model_info_list.py +3 -3
- quantalogic/model_info_litellm.py +14 -14
- quantalogic/prompts.py +2 -1
- quantalogic/{llm.py → quantlitellm.py} +29 -39
- quantalogic/search_agent.py +4 -4
- quantalogic/server/models.py +4 -1
- quantalogic/task_file_reader.py +5 -5
- quantalogic/task_runner.py +20 -20
- quantalogic/tool_manager.py +10 -21
- quantalogic/tools/__init__.py +98 -68
- quantalogic/tools/composio/composio.py +416 -0
- quantalogic/tools/{generate_database_report_tool.py → database/generate_database_report_tool.py} +4 -9
- quantalogic/tools/database/sql_query_tool_advanced.py +261 -0
- quantalogic/tools/document_tools/markdown_to_docx_tool.py +620 -0
- quantalogic/tools/document_tools/markdown_to_epub_tool.py +438 -0
- quantalogic/tools/document_tools/markdown_to_html_tool.py +362 -0
- quantalogic/tools/document_tools/markdown_to_ipynb_tool.py +319 -0
- quantalogic/tools/document_tools/markdown_to_latex_tool.py +420 -0
- quantalogic/tools/document_tools/markdown_to_pdf_tool.py +623 -0
- quantalogic/tools/document_tools/markdown_to_pptx_tool.py +319 -0
- quantalogic/tools/duckduckgo_search_tool.py +2 -4
- quantalogic/tools/finance/alpha_vantage_tool.py +440 -0
- quantalogic/tools/finance/ccxt_tool.py +373 -0
- quantalogic/tools/finance/finance_llm_tool.py +387 -0
- quantalogic/tools/finance/google_finance.py +192 -0
- quantalogic/tools/finance/market_intelligence_tool.py +520 -0
- quantalogic/tools/finance/technical_analysis_tool.py +491 -0
- quantalogic/tools/finance/tradingview_tool.py +336 -0
- quantalogic/tools/finance/yahoo_finance.py +236 -0
- quantalogic/tools/git/bitbucket_clone_repo_tool.py +181 -0
- quantalogic/tools/git/bitbucket_operations_tool.py +326 -0
- quantalogic/tools/git/clone_repo_tool.py +189 -0
- quantalogic/tools/git/git_operations_tool.py +532 -0
- quantalogic/tools/google_packages/google_news_tool.py +480 -0
- quantalogic/tools/grep_app_tool.py +123 -186
- quantalogic/tools/{dalle_e.py → image_generation/dalle_e.py} +37 -27
- quantalogic/tools/jinja_tool.py +6 -10
- quantalogic/tools/language_handlers/__init__.py +22 -9
- quantalogic/tools/list_directory_tool.py +131 -42
- quantalogic/tools/llm_tool.py +45 -15
- quantalogic/tools/llm_vision_tool.py +59 -7
- quantalogic/tools/markitdown_tool.py +17 -5
- quantalogic/tools/nasa_packages/models.py +47 -0
- quantalogic/tools/nasa_packages/nasa_apod_tool.py +232 -0
- quantalogic/tools/nasa_packages/nasa_neows_tool.py +147 -0
- quantalogic/tools/nasa_packages/services.py +82 -0
- quantalogic/tools/presentation_tools/presentation_llm_tool.py +396 -0
- quantalogic/tools/product_hunt/product_hunt_tool.py +258 -0
- quantalogic/tools/product_hunt/services.py +63 -0
- quantalogic/tools/rag_tool/__init__.py +48 -0
- quantalogic/tools/rag_tool/document_metadata.py +15 -0
- quantalogic/tools/rag_tool/query_response.py +20 -0
- quantalogic/tools/rag_tool/rag_tool.py +566 -0
- quantalogic/tools/rag_tool/rag_tool_beta.py +264 -0
- quantalogic/tools/read_html_tool.py +24 -38
- quantalogic/tools/replace_in_file_tool.py +10 -10
- quantalogic/tools/safe_python_interpreter_tool.py +10 -24
- quantalogic/tools/search_definition_names.py +2 -2
- quantalogic/tools/sequence_tool.py +14 -23
- quantalogic/tools/sql_query_tool.py +17 -19
- quantalogic/tools/tool.py +39 -15
- quantalogic/tools/unified_diff_tool.py +1 -1
- quantalogic/tools/utilities/csv_processor_tool.py +234 -0
- quantalogic/tools/utilities/download_file_tool.py +179 -0
- quantalogic/tools/utilities/mermaid_validator_tool.py +661 -0
- quantalogic/tools/utils/__init__.py +1 -4
- quantalogic/tools/utils/create_sample_database.py +24 -38
- quantalogic/tools/utils/generate_database_report.py +74 -82
- quantalogic/tools/wikipedia_search_tool.py +17 -21
- quantalogic/utils/ask_user_validation.py +1 -1
- quantalogic/utils/async_utils.py +35 -0
- quantalogic/utils/check_version.py +3 -5
- quantalogic/utils/get_all_models.py +2 -1
- quantalogic/utils/git_ls.py +21 -7
- quantalogic/utils/lm_studio_model_info.py +9 -7
- quantalogic/utils/python_interpreter.py +113 -43
- quantalogic/utils/xml_utility.py +178 -0
- quantalogic/version_check.py +1 -1
- quantalogic/welcome_message.py +7 -7
- quantalogic/xml_parser.py +0 -1
- {quantalogic-0.35.0.dist-info → quantalogic-0.40.0.dist-info}/METADATA +41 -1
- quantalogic-0.40.0.dist-info/RECORD +148 -0
- quantalogic-0.35.0.dist-info/RECORD +0 -102
- {quantalogic-0.35.0.dist-info → quantalogic-0.40.0.dist-info}/LICENSE +0 -0
- {quantalogic-0.35.0.dist-info → quantalogic-0.40.0.dist-info}/WHEEL +0 -0
- {quantalogic-0.35.0.dist-info → quantalogic-0.40.0.dist-info}/entry_points.txt +0 -0
@@ -23,7 +23,7 @@ class SQLQueryTool(Tool):
|
|
23
23
|
arg_type="string",
|
24
24
|
description="The SQL query to execute",
|
25
25
|
required=True,
|
26
|
-
example="SELECT * FROM customers WHERE country = 'France'"
|
26
|
+
example="SELECT * FROM customers WHERE country = 'France'",
|
27
27
|
),
|
28
28
|
ToolArgument(
|
29
29
|
name="start_row",
|
@@ -31,7 +31,7 @@ class SQLQueryTool(Tool):
|
|
31
31
|
description="1-based starting row number for results",
|
32
32
|
required=True,
|
33
33
|
example="1",
|
34
|
-
default="1"
|
34
|
+
default="1",
|
35
35
|
),
|
36
36
|
ToolArgument(
|
37
37
|
name="end_row",
|
@@ -39,27 +39,27 @@ class SQLQueryTool(Tool):
|
|
39
39
|
description="1-based ending row number for results",
|
40
40
|
required=True,
|
41
41
|
example="100",
|
42
|
-
default="100"
|
42
|
+
default="100",
|
43
43
|
),
|
44
44
|
]
|
45
45
|
connection_string: str = Field(
|
46
46
|
...,
|
47
47
|
description="SQLAlchemy-compatible database connection string",
|
48
|
-
example="postgresql://user:password@localhost/mydb"
|
48
|
+
example="postgresql://user:password@localhost/mydb",
|
49
49
|
)
|
50
50
|
|
51
51
|
def execute(self, query: str, start_row: Any, end_row: Any) -> str:
|
52
52
|
"""
|
53
53
|
Executes a SQL query and returns formatted results.
|
54
|
-
|
54
|
+
|
55
55
|
Args:
|
56
56
|
query: SQL query to execute
|
57
57
|
start_row: 1-based starting row number (supports various numeric types)
|
58
58
|
end_row: 1-based ending row number (supports various numeric types)
|
59
|
-
|
59
|
+
|
60
60
|
Returns:
|
61
61
|
str: Markdown-formatted results with pagination metadata
|
62
|
-
|
62
|
+
|
63
63
|
Raises:
|
64
64
|
ValueError: For invalid parameters or query errors
|
65
65
|
RuntimeError: For database connection issues
|
@@ -68,7 +68,7 @@ class SQLQueryTool(Tool):
|
|
68
68
|
# Convert and validate row numbers
|
69
69
|
start = self._convert_row_number(start_row, "start_row")
|
70
70
|
end = self._convert_row_number(end_row, "end_row")
|
71
|
-
|
71
|
+
|
72
72
|
if start > end:
|
73
73
|
raise ValueError(f"start_row ({start}) must be <= end_row ({end})")
|
74
74
|
|
@@ -83,23 +83,25 @@ class SQLQueryTool(Tool):
|
|
83
83
|
total_rows = len(all_rows)
|
84
84
|
actual_start = max(1, start)
|
85
85
|
actual_end = min(end, total_rows)
|
86
|
-
|
86
|
+
|
87
87
|
if actual_start > total_rows:
|
88
88
|
return f"No results found (total rows: {total_rows})"
|
89
89
|
|
90
90
|
# Slice results (convert to 0-based index)
|
91
|
-
displayed_rows = all_rows[actual_start-1:actual_end]
|
91
|
+
displayed_rows = all_rows[actual_start - 1 : actual_end]
|
92
92
|
|
93
93
|
# Format results
|
94
94
|
markdown = [
|
95
95
|
f"**Query Results:** `{actual_start}-{actual_end}` of `{total_rows}` rows",
|
96
|
-
self._format_table(columns, displayed_rows)
|
96
|
+
self._format_table(columns, displayed_rows),
|
97
97
|
]
|
98
98
|
|
99
99
|
# Add pagination notice
|
100
100
|
if actual_end < total_rows:
|
101
101
|
remaining = total_rows - actual_end
|
102
|
-
markdown.append(
|
102
|
+
markdown.append(
|
103
|
+
f"\n*Showing first {actual_end} rows - {remaining} more row{'s' if remaining > 1 else ''} available*"
|
104
|
+
)
|
103
105
|
|
104
106
|
return "\n".join(markdown)
|
105
107
|
|
@@ -125,10 +127,10 @@ class SQLQueryTool(Tool):
|
|
125
127
|
converted = int(num)
|
126
128
|
if converted != num: # Check if float had decimal part
|
127
129
|
raise ValueError("Decimal values are not allowed for row numbers")
|
128
|
-
|
130
|
+
|
129
131
|
if converted <= 0:
|
130
132
|
raise ValueError(f"{field_name} must be a positive integer")
|
131
|
-
|
133
|
+
|
132
134
|
return converted
|
133
135
|
except (ValueError, TypeError) as e:
|
134
136
|
raise ValueError(f"Invalid value for {field_name}: {repr(value)}") from e
|
@@ -141,7 +143,7 @@ class SQLQueryTool(Tool):
|
|
141
143
|
# Create header
|
142
144
|
header = "| " + " | ".join(columns) + " |"
|
143
145
|
separator = "| " + " | ".join(["---"] * len(columns)) + " |"
|
144
|
-
|
146
|
+
|
145
147
|
# Create rows with truncation
|
146
148
|
body = []
|
147
149
|
for row in rows:
|
@@ -155,7 +157,6 @@ class SQLQueryTool(Tool):
|
|
155
157
|
return "\n".join([header, separator] + body)
|
156
158
|
|
157
159
|
|
158
|
-
|
159
160
|
if __name__ == "__main__":
|
160
161
|
from quantalogic.tools.utils.create_sample_database import create_sample_database
|
161
162
|
|
@@ -164,6 +165,3 @@ if __name__ == "__main__":
|
|
164
165
|
tool = SQLQueryTool(connection_string="sqlite:///sample.db")
|
165
166
|
print(tool.execute("select * from customers", 1, 10))
|
166
167
|
print(tool.execute("select * from customers", 11, 20))
|
167
|
-
|
168
|
-
|
169
|
-
|
quantalogic/tools/tool.py
CHANGED
@@ -4,6 +4,7 @@ This module provides base classes and data models for creating configurable tool
|
|
4
4
|
with type-validated arguments and execution methods.
|
5
5
|
"""
|
6
6
|
|
7
|
+
import asyncio # Added for asynchronous support
|
7
8
|
from typing import Any, Literal
|
8
9
|
|
9
10
|
from pydantic import BaseModel, ConfigDict, Field, field_validator
|
@@ -129,9 +130,7 @@ class ToolDefinition(BaseModel):
|
|
129
130
|
value_info = f" (default: `{arg.default}`)"
|
130
131
|
|
131
132
|
parameters += (
|
132
|
-
f" - `{arg.name}`: "
|
133
|
-
f"({required_status}{value_info})\n"
|
134
|
-
f" {arg.description or ''}\n"
|
133
|
+
f" - `{arg.name}`: " f"({required_status}{value_info})\n" f" {arg.description or ''}\n"
|
135
134
|
)
|
136
135
|
if len(parameters) > 0:
|
137
136
|
markdown += parameters + "\n\n"
|
@@ -164,9 +163,17 @@ class ToolDefinition(BaseModel):
|
|
164
163
|
"""
|
165
164
|
properties_injectable = self.get_injectable_properties_in_execution()
|
166
165
|
|
167
|
-
return [
|
168
|
-
|
169
|
-
|
166
|
+
return [arg for arg in self.arguments if properties_injectable.get(arg.name) is None]
|
167
|
+
|
168
|
+
def get_injectable_properties_in_execution(self) -> dict[str, Any]:
|
169
|
+
"""Get injectable properties excluding tool arguments.
|
170
|
+
|
171
|
+
Returns:
|
172
|
+
A dictionary of property names and values, excluding tool arguments and None values.
|
173
|
+
"""
|
174
|
+
# This method is defined here in ToolDefinition and overridden in Tool
|
175
|
+
# For ToolDefinition, it returns an empty dict since it has no execution context yet
|
176
|
+
return {}
|
170
177
|
|
171
178
|
|
172
179
|
class Tool(ToolDefinition):
|
@@ -200,33 +207,50 @@ class Tool(ToolDefinition):
|
|
200
207
|
def execute(self, **kwargs) -> str:
|
201
208
|
"""Execute the tool with provided arguments.
|
202
209
|
|
210
|
+
If not implemented by a subclass, falls back to the asynchronous execute_async method.
|
211
|
+
|
203
212
|
Args:
|
204
213
|
**kwargs: Keyword arguments for tool execution.
|
205
214
|
|
206
|
-
|
207
|
-
|
215
|
+
Returns:
|
216
|
+
A string representing the result of tool execution.
|
217
|
+
"""
|
218
|
+
# Check if execute is implemented in the subclass
|
219
|
+
if self.__class__.execute is Tool.execute:
|
220
|
+
# If not implemented, run the async version synchronously
|
221
|
+
return asyncio.run(self.async_execute(**kwargs))
|
222
|
+
raise NotImplementedError("This method should be implemented by subclasses.")
|
223
|
+
|
224
|
+
async def async_execute(self, **kwargs) -> str:
|
225
|
+
"""Asynchronous version of execute.
|
226
|
+
|
227
|
+
By default, runs the synchronous execute method in a separate thread using asyncio.to_thread.
|
228
|
+
Subclasses can override this method to provide a native asynchronous implementation for
|
229
|
+
operations that benefit from async I/O (e.g., network requests).
|
230
|
+
|
231
|
+
Args:
|
232
|
+
**kwargs: Keyword arguments for tool execution.
|
208
233
|
|
209
234
|
Returns:
|
210
235
|
A string representing the result of tool execution.
|
211
236
|
"""
|
237
|
+
# Check if execute_async is implemented in the subclass
|
238
|
+
if self.__class__.async_execute is Tool.async_execute:
|
239
|
+
return await asyncio.to_thread(self.execute, **kwargs)
|
212
240
|
raise NotImplementedError("This method should be implemented by subclasses.")
|
213
241
|
|
214
242
|
def get_injectable_properties_in_execution(self) -> dict[str, Any]:
|
215
243
|
"""Get injectable properties excluding tool arguments.
|
216
|
-
|
244
|
+
|
217
245
|
Returns:
|
218
246
|
A dictionary of property names and values, excluding tool arguments and None values.
|
219
247
|
"""
|
220
248
|
# Get argument names from tool definition
|
221
249
|
argument_names = {arg.name for arg in self.arguments}
|
222
|
-
|
250
|
+
|
223
251
|
# Get properties excluding arguments and filter out None values
|
224
252
|
properties = self.get_properties(exclude=["arguments"])
|
225
|
-
return {
|
226
|
-
name: value
|
227
|
-
for name, value in properties.items()
|
228
|
-
if value is not None and name in argument_names
|
229
|
-
}
|
253
|
+
return {name: value for name, value in properties.items() if value is not None and name in argument_names}
|
230
254
|
|
231
255
|
|
232
256
|
if __name__ == "__main__":
|
@@ -304,7 +304,7 @@ class UnifiedDiffTool(Tool):
|
|
304
304
|
|
305
305
|
name: str = "unified_diff"
|
306
306
|
description: str = "Applies a unified diff patch to update a file."
|
307
|
-
need_validation: bool =
|
307
|
+
need_validation: bool = False
|
308
308
|
lenient: bool = True
|
309
309
|
tolerance: int = 5
|
310
310
|
arguments: list[ToolArgument] = [
|
@@ -0,0 +1,234 @@
|
|
1
|
+
"""Tool for processing CSV files with pandas."""
|
2
|
+
|
3
|
+
from pathlib import Path
|
4
|
+
from typing import Dict, List, Any
|
5
|
+
import json
|
6
|
+
import re
|
7
|
+
|
8
|
+
import pandas as pd
|
9
|
+
from loguru import logger
|
10
|
+
|
11
|
+
from quantalogic.tools.tool import Tool, ToolArgument
|
12
|
+
|
13
|
+
|
14
|
+
class CSVProcessorTool(Tool):
|
15
|
+
"""Tool for reading, processing and writing CSV files."""
|
16
|
+
|
17
|
+
name: str = "csv_processor_tool"
|
18
|
+
description: str = """
|
19
|
+
Process CSV files with operations like:
|
20
|
+
- read: Display CSV content and info
|
21
|
+
- add_column: Add a new column with values
|
22
|
+
- update_column: Update existing column values
|
23
|
+
- process_rows: Process rows with custom conditions
|
24
|
+
- filter_rows: Filter rows based on conditions
|
25
|
+
- describe: Get statistical description of the data
|
26
|
+
"""
|
27
|
+
need_validation: bool = False
|
28
|
+
arguments: list = [
|
29
|
+
ToolArgument(
|
30
|
+
name="input_path",
|
31
|
+
arg_type="string",
|
32
|
+
description="Path to the input CSV file",
|
33
|
+
required=True,
|
34
|
+
example="/path/to/input.csv",
|
35
|
+
),
|
36
|
+
ToolArgument(
|
37
|
+
name="operation",
|
38
|
+
arg_type="string",
|
39
|
+
description="""Operation to perform:
|
40
|
+
- read: Display CSV content
|
41
|
+
- add_column: Add new column
|
42
|
+
- update_column: Update column values
|
43
|
+
- process_rows: Process specific rows
|
44
|
+
- filter_rows: Filter rows by condition
|
45
|
+
- describe: Get data statistics
|
46
|
+
""",
|
47
|
+
required=True,
|
48
|
+
example="read",
|
49
|
+
),
|
50
|
+
ToolArgument(
|
51
|
+
name="column_name",
|
52
|
+
arg_type="string",
|
53
|
+
description="Name of the column to add/update",
|
54
|
+
required=False,
|
55
|
+
example="description",
|
56
|
+
default="",
|
57
|
+
),
|
58
|
+
ToolArgument(
|
59
|
+
name="column_value",
|
60
|
+
arg_type="string",
|
61
|
+
description="""Value for the column. Supports:
|
62
|
+
- Simple value: "some text"
|
63
|
+
- Template with variables: "User $name$ with email $email$"
|
64
|
+
- JSON format for complex operations: {"condition": "price > 100", "value": "premium"}
|
65
|
+
Variables in templates must be in $variable$ format and match column names.""",
|
66
|
+
required=False,
|
67
|
+
example="Hello $name$! Your email is $email$",
|
68
|
+
default="",
|
69
|
+
),
|
70
|
+
ToolArgument(
|
71
|
+
name="output_path",
|
72
|
+
arg_type="string",
|
73
|
+
description="Path to save the processed CSV file. If not provided, will overwrite input file",
|
74
|
+
required=False,
|
75
|
+
example="/path/to/output.csv",
|
76
|
+
default="",
|
77
|
+
),
|
78
|
+
]
|
79
|
+
|
80
|
+
def _read_csv(self, df: pd.DataFrame) -> str:
|
81
|
+
"""Display CSV content and information."""
|
82
|
+
info = {
|
83
|
+
"shape": df.shape,
|
84
|
+
"columns": df.columns.tolist(),
|
85
|
+
"data_types": df.dtypes.astype(str).to_dict(),
|
86
|
+
"preview": df.head().to_dict(orient="records"),
|
87
|
+
"total_rows": len(df),
|
88
|
+
}
|
89
|
+
return f"CSV Information:\n{json.dumps(info, indent=2)}"
|
90
|
+
|
91
|
+
def _parse_value(self, value: str) -> Dict[str, Any]:
|
92
|
+
"""Parse the column_value string into a dictionary."""
|
93
|
+
if not value:
|
94
|
+
return {"value": ""}
|
95
|
+
|
96
|
+
try:
|
97
|
+
# Try parsing as JSON first
|
98
|
+
return json.loads(value)
|
99
|
+
except json.JSONDecodeError:
|
100
|
+
# If not valid JSON, treat as template or simple value
|
101
|
+
return {"value": value, "is_template": bool(re.search(r'\$\w+\$', value))}
|
102
|
+
|
103
|
+
def _apply_template(self, template: str, row: pd.Series) -> str:
|
104
|
+
"""Apply template replacing $variable$ with values from row."""
|
105
|
+
result = template
|
106
|
+
for match in re.finditer(r'\$(\w+)\$', template):
|
107
|
+
var_name = match.group(1)
|
108
|
+
if var_name in row.index:
|
109
|
+
value = str(row[var_name])
|
110
|
+
result = result.replace(f"${var_name}$", value)
|
111
|
+
return result
|
112
|
+
|
113
|
+
def _process_rows(self, df: pd.DataFrame, condition: Dict[str, Any]) -> pd.DataFrame:
|
114
|
+
"""Process rows based on condition."""
|
115
|
+
try:
|
116
|
+
if "filter" in condition:
|
117
|
+
query = condition["filter"]
|
118
|
+
df = df.query(query)
|
119
|
+
|
120
|
+
if "update" in condition:
|
121
|
+
updates = condition["update"]
|
122
|
+
for col, value in updates.items():
|
123
|
+
if "filter" in condition:
|
124
|
+
df.loc[df.eval(condition["filter"]), col] = value
|
125
|
+
else:
|
126
|
+
df[col] = value
|
127
|
+
|
128
|
+
return df
|
129
|
+
except Exception as e:
|
130
|
+
logger.error(f"Error in _process_rows: {str(e)}")
|
131
|
+
raise
|
132
|
+
|
133
|
+
def execute(
|
134
|
+
self,
|
135
|
+
input_path: str,
|
136
|
+
operation: str,
|
137
|
+
column_name: str = "",
|
138
|
+
column_value: str = "",
|
139
|
+
output_path: str = "",
|
140
|
+
) -> str:
|
141
|
+
"""Process the CSV file according to specified operation."""
|
142
|
+
try:
|
143
|
+
input_path = Path(input_path)
|
144
|
+
if not input_path.exists():
|
145
|
+
return f"Error: Input file {input_path} does not exist"
|
146
|
+
|
147
|
+
logger.info(f"Reading CSV file: {input_path}")
|
148
|
+
df = pd.read_csv(input_path)
|
149
|
+
initial_shape = df.shape
|
150
|
+
|
151
|
+
if operation == "read":
|
152
|
+
return self._read_csv(df)
|
153
|
+
|
154
|
+
elif operation == "describe":
|
155
|
+
stats = df.describe().to_dict()
|
156
|
+
return f"Statistical Description:\n{json.dumps(stats, indent=2)}"
|
157
|
+
|
158
|
+
elif operation == "add_column":
|
159
|
+
if not column_name:
|
160
|
+
return "Error: column_name is required for add_column operation"
|
161
|
+
if column_name in df.columns:
|
162
|
+
return f"Error: Column {column_name} already exists"
|
163
|
+
|
164
|
+
value_dict = self._parse_value(column_value)
|
165
|
+
|
166
|
+
if value_dict.get("is_template", False):
|
167
|
+
# Handle template string with variables
|
168
|
+
df[column_name] = df.apply(
|
169
|
+
lambda row: self._apply_template(value_dict["value"], row),
|
170
|
+
axis=1
|
171
|
+
)
|
172
|
+
elif "formula" in value_dict:
|
173
|
+
# Handle formula-based column addition
|
174
|
+
df[column_name] = df.eval(value_dict["formula"])
|
175
|
+
else:
|
176
|
+
# Handle static value
|
177
|
+
df[column_name] = value_dict.get("value", "")
|
178
|
+
|
179
|
+
elif operation == "update_column":
|
180
|
+
if not column_name:
|
181
|
+
return "Error: column_name required for update_column"
|
182
|
+
if column_name not in df.columns:
|
183
|
+
return f"Error: Column {column_name} does not exist"
|
184
|
+
|
185
|
+
value_dict = self._parse_value(column_value)
|
186
|
+
|
187
|
+
if value_dict.get("is_template", False):
|
188
|
+
# Handle template string with variables
|
189
|
+
df[column_name] = df.apply(
|
190
|
+
lambda row: self._apply_template(value_dict["value"], row),
|
191
|
+
axis=1
|
192
|
+
)
|
193
|
+
elif "condition" in value_dict:
|
194
|
+
# Update based on condition
|
195
|
+
mask = df.eval(value_dict["condition"])
|
196
|
+
df.loc[mask, column_name] = value_dict.get("value", "")
|
197
|
+
else:
|
198
|
+
# Update all rows
|
199
|
+
df[column_name] = value_dict.get("value", "")
|
200
|
+
|
201
|
+
elif operation == "process_rows" or operation == "filter_rows":
|
202
|
+
if not column_value:
|
203
|
+
return f"Error: column_value with conditions required for {operation}"
|
204
|
+
conditions = self._parse_value(column_value)
|
205
|
+
df = self._process_rows(df, conditions)
|
206
|
+
|
207
|
+
else:
|
208
|
+
return f"Error: Unknown operation {operation}"
|
209
|
+
|
210
|
+
# Save if output path provided or if data was modified
|
211
|
+
if operation != "read" and operation != "describe":
|
212
|
+
save_path = output_path if output_path else input_path
|
213
|
+
save_path = Path(save_path)
|
214
|
+
logger.info(f"Saving processed CSV to: {save_path}")
|
215
|
+
df.to_csv(save_path, index=False)
|
216
|
+
|
217
|
+
return (
|
218
|
+
f"Successfully processed CSV:\n"
|
219
|
+
f"- Input shape: {initial_shape}\n"
|
220
|
+
f"- Output shape: {df.shape}\n"
|
221
|
+
f"- Operation: {operation}\n"
|
222
|
+
f"- Saved to: {save_path}"
|
223
|
+
)
|
224
|
+
|
225
|
+
return "Operation completed successfully"
|
226
|
+
|
227
|
+
except Exception as e:
|
228
|
+
logger.error(f"Error processing CSV: {str(e)}")
|
229
|
+
return f"Error processing CSV: {str(e)}"
|
230
|
+
|
231
|
+
|
232
|
+
if __name__ == "__main__":
|
233
|
+
tool = CSVProcessorTool()
|
234
|
+
print(tool.to_markdown())
|
@@ -0,0 +1,179 @@
|
|
1
|
+
"""Tool for preparing files or directories for download through the existing file server."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
import shutil
|
5
|
+
import zipfile
|
6
|
+
from datetime import datetime
|
7
|
+
from pathlib import Path
|
8
|
+
from typing import Optional
|
9
|
+
from loguru import logger
|
10
|
+
|
11
|
+
from quantalogic.tools.tool import Tool, ToolArgument
|
12
|
+
|
13
|
+
|
14
|
+
class PrepareDownloadTool(Tool):
|
15
|
+
"""Tool for preparing files or directories for download through the existing file server."""
|
16
|
+
|
17
|
+
name: str = "prepare_download_tool"
|
18
|
+
description: str = (
|
19
|
+
"Prepares a local file or directory for download. "
|
20
|
+
"If it's a directory, it will be zipped. "
|
21
|
+
"Returns an HTML link for downloading."
|
22
|
+
)
|
23
|
+
need_validation: bool = True
|
24
|
+
arguments: list = [
|
25
|
+
ToolArgument(
|
26
|
+
name="path",
|
27
|
+
arg_type="string",
|
28
|
+
description="The local path of the file or directory to prepare for download",
|
29
|
+
required=True,
|
30
|
+
example="/path/to/local/file.pdf",
|
31
|
+
),
|
32
|
+
ToolArgument(
|
33
|
+
name="filename",
|
34
|
+
arg_type="string",
|
35
|
+
description="Optional custom filename for download. If not provided, uses original name.",
|
36
|
+
required=False,
|
37
|
+
example="custom_name.pdf",
|
38
|
+
),
|
39
|
+
ToolArgument(
|
40
|
+
name="link_text",
|
41
|
+
arg_type="string",
|
42
|
+
description="Optional custom text for the download link. If not provided, uses a default.",
|
43
|
+
required=False,
|
44
|
+
example="Download Report",
|
45
|
+
),
|
46
|
+
]
|
47
|
+
|
48
|
+
def __init__(self, upload_dir: str = "/tmp/data", base_url: str = "http://localhost:8082"):
|
49
|
+
"""Initialize the tool with upload directory path.
|
50
|
+
|
51
|
+
Args:
|
52
|
+
upload_dir: Directory where files are served from. Defaults to /tmp/data.
|
53
|
+
base_url: Base URL of the server. Defaults to http://localhost:8082.
|
54
|
+
"""
|
55
|
+
super().__init__()
|
56
|
+
self.upload_dir = upload_dir
|
57
|
+
self.base_url = base_url.rstrip('/')
|
58
|
+
# Ensure upload directory exists
|
59
|
+
os.makedirs(upload_dir, exist_ok=True)
|
60
|
+
|
61
|
+
def _zip_directory(self, dir_path: str, zip_path: str) -> None:
|
62
|
+
"""Zip a directory.
|
63
|
+
|
64
|
+
Args:
|
65
|
+
dir_path: Path to the directory to zip
|
66
|
+
zip_path: Path where to save the zip file
|
67
|
+
"""
|
68
|
+
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zipf:
|
69
|
+
base_path = os.path.basename(dir_path)
|
70
|
+
for root, _, files in os.walk(dir_path):
|
71
|
+
for file in files:
|
72
|
+
file_path = os.path.join(root, file)
|
73
|
+
arcname = os.path.relpath(file_path, os.path.dirname(dir_path))
|
74
|
+
zipf.write(file_path, arcname)
|
75
|
+
logger.info(f"Directory zipped successfully: {zip_path}")
|
76
|
+
|
77
|
+
def _generate_html_link(self, url: str, text: str, filename: str) -> str:
|
78
|
+
"""Generate an HTML link with custom styling.
|
79
|
+
|
80
|
+
Args:
|
81
|
+
url: The download URL
|
82
|
+
text: Text to display in the link
|
83
|
+
filename: Name of the file (for logging)
|
84
|
+
|
85
|
+
Returns:
|
86
|
+
HTML formatted link
|
87
|
+
"""
|
88
|
+
# CSS styling for the download link
|
89
|
+
style = (
|
90
|
+
"color: #0066cc; "
|
91
|
+
"text-decoration: none; "
|
92
|
+
"padding: 8px 16px; "
|
93
|
+
"border: 1px solid #0066cc; "
|
94
|
+
"border-radius: 4px; "
|
95
|
+
"display: inline-block; "
|
96
|
+
"font-family: system-ui, -apple-system, sans-serif; "
|
97
|
+
"transition: all 0.2s;"
|
98
|
+
)
|
99
|
+
|
100
|
+
hover_style = (
|
101
|
+
"background-color: #0066cc; "
|
102
|
+
"color: white; "
|
103
|
+
"cursor: pointer;"
|
104
|
+
)
|
105
|
+
|
106
|
+
# Create the HTML link with embedded styles
|
107
|
+
html = f'''<a href="{url}"
|
108
|
+
style="{style}"
|
109
|
+
onmouseover="this.style.backgroundColor='#0066cc'; this.style.color='white';"
|
110
|
+
onmouseout="this.style.backgroundColor='transparent'; this.style.color='#0066cc';"
|
111
|
+
download="{filename}">{text}</a>'''
|
112
|
+
|
113
|
+
return html
|
114
|
+
|
115
|
+
def execute(self, path: str, filename: str = None, link_text: str = None) -> str:
|
116
|
+
"""Prepares a file or directory for download.
|
117
|
+
|
118
|
+
Args:
|
119
|
+
path: The local path of the file or directory to prepare.
|
120
|
+
filename: Optional custom filename for download.
|
121
|
+
link_text: Optional custom text for the download link.
|
122
|
+
|
123
|
+
Returns:
|
124
|
+
HTML link for downloading the file.
|
125
|
+
|
126
|
+
Raises:
|
127
|
+
Exception: If file operations fail.
|
128
|
+
"""
|
129
|
+
try:
|
130
|
+
path = os.path.abspath(path)
|
131
|
+
|
132
|
+
# Verify path exists
|
133
|
+
if not os.path.exists(path):
|
134
|
+
error_msg = f"Path not found: {path}"
|
135
|
+
logger.error(error_msg)
|
136
|
+
return error_msg
|
137
|
+
|
138
|
+
# Handle directory case - create zip file
|
139
|
+
if os.path.isdir(path):
|
140
|
+
source_name = os.path.basename(path)
|
141
|
+
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
142
|
+
zip_filename = filename or f"{source_name}_{timestamp}.zip"
|
143
|
+
if not zip_filename.endswith('.zip'):
|
144
|
+
zip_filename += '.zip'
|
145
|
+
|
146
|
+
zip_path = os.path.join(self.upload_dir, zip_filename)
|
147
|
+
self._zip_directory(path, zip_path)
|
148
|
+
target_path = zip_path
|
149
|
+
target_filename = zip_filename
|
150
|
+
else:
|
151
|
+
# Handle single file case
|
152
|
+
source_filename = os.path.basename(path)
|
153
|
+
target_filename = filename or source_filename
|
154
|
+
target_path = os.path.join(self.upload_dir, target_filename)
|
155
|
+
shutil.copy2(path, target_path)
|
156
|
+
logger.success(f"File copied to upload directory: {target_path}")
|
157
|
+
|
158
|
+
# Generate download URL
|
159
|
+
download_url = f"{self.base_url}/api/agent/download/{target_filename}"
|
160
|
+
|
161
|
+
# Generate link text
|
162
|
+
if not link_text:
|
163
|
+
if os.path.isdir(path):
|
164
|
+
link_text = f"Download {os.path.basename(path)} (ZIP)"
|
165
|
+
else:
|
166
|
+
link_text = f"Download {os.path.basename(path)}"
|
167
|
+
|
168
|
+
# Generate and return HTML link
|
169
|
+
return self._generate_html_link(download_url, link_text, target_filename)
|
170
|
+
|
171
|
+
except Exception as e:
|
172
|
+
error_msg = f"Error while preparing download: {str(e)}"
|
173
|
+
logger.error(error_msg)
|
174
|
+
return error_msg
|
175
|
+
|
176
|
+
|
177
|
+
if __name__ == "__main__":
|
178
|
+
tool = PrepareDownloadTool()
|
179
|
+
print(tool.to_markdown())
|