quantalogic 0.33.4__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.
Files changed (107) hide show
  1. quantalogic/__init__.py +0 -4
  2. quantalogic/agent.py +603 -362
  3. quantalogic/agent_config.py +260 -28
  4. quantalogic/agent_factory.py +43 -17
  5. quantalogic/coding_agent.py +20 -12
  6. quantalogic/config.py +7 -4
  7. quantalogic/console_print_events.py +4 -8
  8. quantalogic/console_print_token.py +2 -2
  9. quantalogic/docs_cli.py +15 -10
  10. quantalogic/event_emitter.py +258 -83
  11. quantalogic/flow/__init__.py +23 -0
  12. quantalogic/flow/flow.py +595 -0
  13. quantalogic/flow/flow_extractor.py +672 -0
  14. quantalogic/flow/flow_generator.py +89 -0
  15. quantalogic/flow/flow_manager.py +407 -0
  16. quantalogic/flow/flow_manager_schema.py +169 -0
  17. quantalogic/flow/flow_yaml.md +419 -0
  18. quantalogic/generative_model.py +109 -77
  19. quantalogic/get_model_info.py +6 -6
  20. quantalogic/interactive_text_editor.py +100 -73
  21. quantalogic/main.py +36 -23
  22. quantalogic/model_info_list.py +12 -0
  23. quantalogic/model_info_litellm.py +14 -14
  24. quantalogic/prompts.py +2 -1
  25. quantalogic/{llm.py → quantlitellm.py} +29 -39
  26. quantalogic/search_agent.py +4 -4
  27. quantalogic/server/models.py +4 -1
  28. quantalogic/task_file_reader.py +5 -5
  29. quantalogic/task_runner.py +21 -20
  30. quantalogic/tool_manager.py +10 -21
  31. quantalogic/tools/__init__.py +98 -68
  32. quantalogic/tools/composio/composio.py +416 -0
  33. quantalogic/tools/{generate_database_report_tool.py → database/generate_database_report_tool.py} +4 -9
  34. quantalogic/tools/database/sql_query_tool_advanced.py +261 -0
  35. quantalogic/tools/document_tools/markdown_to_docx_tool.py +620 -0
  36. quantalogic/tools/document_tools/markdown_to_epub_tool.py +438 -0
  37. quantalogic/tools/document_tools/markdown_to_html_tool.py +362 -0
  38. quantalogic/tools/document_tools/markdown_to_ipynb_tool.py +319 -0
  39. quantalogic/tools/document_tools/markdown_to_latex_tool.py +420 -0
  40. quantalogic/tools/document_tools/markdown_to_pdf_tool.py +623 -0
  41. quantalogic/tools/document_tools/markdown_to_pptx_tool.py +319 -0
  42. quantalogic/tools/duckduckgo_search_tool.py +2 -4
  43. quantalogic/tools/finance/alpha_vantage_tool.py +440 -0
  44. quantalogic/tools/finance/ccxt_tool.py +373 -0
  45. quantalogic/tools/finance/finance_llm_tool.py +387 -0
  46. quantalogic/tools/finance/google_finance.py +192 -0
  47. quantalogic/tools/finance/market_intelligence_tool.py +520 -0
  48. quantalogic/tools/finance/technical_analysis_tool.py +491 -0
  49. quantalogic/tools/finance/tradingview_tool.py +336 -0
  50. quantalogic/tools/finance/yahoo_finance.py +236 -0
  51. quantalogic/tools/git/bitbucket_clone_repo_tool.py +181 -0
  52. quantalogic/tools/git/bitbucket_operations_tool.py +326 -0
  53. quantalogic/tools/git/clone_repo_tool.py +189 -0
  54. quantalogic/tools/git/git_operations_tool.py +532 -0
  55. quantalogic/tools/google_packages/google_news_tool.py +480 -0
  56. quantalogic/tools/grep_app_tool.py +123 -186
  57. quantalogic/tools/{dalle_e.py → image_generation/dalle_e.py} +37 -27
  58. quantalogic/tools/jinja_tool.py +6 -10
  59. quantalogic/tools/language_handlers/__init__.py +22 -9
  60. quantalogic/tools/list_directory_tool.py +131 -42
  61. quantalogic/tools/llm_tool.py +45 -15
  62. quantalogic/tools/llm_vision_tool.py +59 -7
  63. quantalogic/tools/markitdown_tool.py +17 -5
  64. quantalogic/tools/nasa_packages/models.py +47 -0
  65. quantalogic/tools/nasa_packages/nasa_apod_tool.py +232 -0
  66. quantalogic/tools/nasa_packages/nasa_neows_tool.py +147 -0
  67. quantalogic/tools/nasa_packages/services.py +82 -0
  68. quantalogic/tools/presentation_tools/presentation_llm_tool.py +396 -0
  69. quantalogic/tools/product_hunt/product_hunt_tool.py +258 -0
  70. quantalogic/tools/product_hunt/services.py +63 -0
  71. quantalogic/tools/rag_tool/__init__.py +48 -0
  72. quantalogic/tools/rag_tool/document_metadata.py +15 -0
  73. quantalogic/tools/rag_tool/query_response.py +20 -0
  74. quantalogic/tools/rag_tool/rag_tool.py +566 -0
  75. quantalogic/tools/rag_tool/rag_tool_beta.py +264 -0
  76. quantalogic/tools/read_html_tool.py +24 -38
  77. quantalogic/tools/replace_in_file_tool.py +10 -10
  78. quantalogic/tools/safe_python_interpreter_tool.py +10 -24
  79. quantalogic/tools/search_definition_names.py +2 -2
  80. quantalogic/tools/sequence_tool.py +14 -23
  81. quantalogic/tools/sql_query_tool.py +17 -19
  82. quantalogic/tools/tool.py +39 -15
  83. quantalogic/tools/unified_diff_tool.py +1 -1
  84. quantalogic/tools/utilities/csv_processor_tool.py +234 -0
  85. quantalogic/tools/utilities/download_file_tool.py +179 -0
  86. quantalogic/tools/utilities/mermaid_validator_tool.py +661 -0
  87. quantalogic/tools/utils/__init__.py +1 -4
  88. quantalogic/tools/utils/create_sample_database.py +24 -38
  89. quantalogic/tools/utils/generate_database_report.py +74 -82
  90. quantalogic/tools/wikipedia_search_tool.py +17 -21
  91. quantalogic/utils/ask_user_validation.py +1 -1
  92. quantalogic/utils/async_utils.py +35 -0
  93. quantalogic/utils/check_version.py +3 -5
  94. quantalogic/utils/get_all_models.py +2 -1
  95. quantalogic/utils/git_ls.py +21 -7
  96. quantalogic/utils/lm_studio_model_info.py +9 -7
  97. quantalogic/utils/python_interpreter.py +113 -43
  98. quantalogic/utils/xml_utility.py +178 -0
  99. quantalogic/version_check.py +1 -1
  100. quantalogic/welcome_message.py +7 -7
  101. quantalogic/xml_parser.py +0 -1
  102. {quantalogic-0.33.4.dist-info → quantalogic-0.40.0.dist-info}/METADATA +44 -1
  103. quantalogic-0.40.0.dist-info/RECORD +148 -0
  104. quantalogic-0.33.4.dist-info/RECORD +0 -102
  105. {quantalogic-0.33.4.dist-info → quantalogic-0.40.0.dist-info}/LICENSE +0 -0
  106. {quantalogic-0.33.4.dist-info → quantalogic-0.40.0.dist-info}/WHEEL +0 -0
  107. {quantalogic-0.33.4.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(f"\n*Showing first {actual_end} rows - {remaining} more row{'s' if remaining > 1 else ''} available*")
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
- arg for arg in self.arguments if properties_injectable.get(arg.name) is None
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
- Raises:
207
- NotImplementedError: If the method is not implemented by a subclass.
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 = True
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())