quantalogic 0.59.2__py3-none-any.whl → 0.60.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 (42) hide show
  1. quantalogic/agent.py +268 -24
  2. quantalogic/create_custom_agent.py +26 -78
  3. quantalogic/prompts/chat_system_prompt.j2 +10 -7
  4. quantalogic/prompts/code_2_system_prompt.j2 +190 -0
  5. quantalogic/prompts/code_system_prompt.j2 +142 -0
  6. quantalogic/prompts/doc_system_prompt.j2 +178 -0
  7. quantalogic/prompts/legal_2_system_prompt.j2 +218 -0
  8. quantalogic/prompts/legal_system_prompt.j2 +140 -0
  9. quantalogic/prompts/system_prompt.j2 +6 -2
  10. quantalogic/prompts/task_prompt.j2 +1 -1
  11. quantalogic/prompts/tools_prompt.j2 +2 -4
  12. quantalogic/prompts.py +23 -4
  13. quantalogic/server/agent_server.py +1 -1
  14. quantalogic/tools/__init__.py +2 -0
  15. quantalogic/tools/duckduckgo_search_tool.py +1 -0
  16. quantalogic/tools/execute_bash_command_tool.py +114 -57
  17. quantalogic/tools/file_tracker_tool.py +49 -0
  18. quantalogic/tools/google_packages/google_news_tool.py +3 -0
  19. quantalogic/tools/image_generation/dalle_e.py +89 -137
  20. quantalogic/tools/rag_tool/__init__.py +2 -9
  21. quantalogic/tools/rag_tool/document_rag_sources_.py +728 -0
  22. quantalogic/tools/rag_tool/ocr_pdf_markdown.py +144 -0
  23. quantalogic/tools/replace_in_file_tool.py +1 -1
  24. quantalogic/tools/terminal_capture_tool.py +293 -0
  25. quantalogic/tools/tool.py +4 -0
  26. quantalogic/tools/utilities/__init__.py +2 -0
  27. quantalogic/tools/utilities/download_file_tool.py +3 -5
  28. quantalogic/tools/utilities/llm_tool.py +283 -0
  29. quantalogic/tools/utilities/selenium_tool.py +296 -0
  30. quantalogic/tools/utilities/vscode_tool.py +1 -1
  31. quantalogic/tools/web_navigation/__init__.py +5 -0
  32. quantalogic/tools/web_navigation/web_tool.py +145 -0
  33. quantalogic/tools/write_file_tool.py +72 -36
  34. {quantalogic-0.59.2.dist-info → quantalogic-0.60.0.dist-info}/METADATA +2 -2
  35. {quantalogic-0.59.2.dist-info → quantalogic-0.60.0.dist-info}/RECORD +38 -29
  36. quantalogic/tools/rag_tool/document_metadata.py +0 -15
  37. quantalogic/tools/rag_tool/query_response.py +0 -20
  38. quantalogic/tools/rag_tool/rag_tool.py +0 -566
  39. quantalogic/tools/rag_tool/rag_tool_beta.py +0 -264
  40. {quantalogic-0.59.2.dist-info → quantalogic-0.60.0.dist-info}/LICENSE +0 -0
  41. {quantalogic-0.59.2.dist-info → quantalogic-0.60.0.dist-info}/WHEEL +0 -0
  42. {quantalogic-0.59.2.dist-info → quantalogic-0.60.0.dist-info}/entry_points.txt +0 -0
@@ -0,0 +1,144 @@
1
+
2
+ import asyncio
3
+ import os
4
+ import tempfile
5
+ from pathlib import Path
6
+ from typing import Optional, Union, List
7
+
8
+ import typer
9
+ from loguru import logger
10
+ from pyzerox import zerox
11
+
12
+ # Import the flow API (assumes quantalogic/flow/flow.py is in your project structure)
13
+ from quantalogic.flow.flow import Nodes, Workflow
14
+
15
+ class PDFToMarkdownConverter:
16
+ """A class to handle PDF to Markdown conversion using vision models."""
17
+
18
+ def __init__(
19
+ self,
20
+ model: str = "gemini/gemini-2.0-flash",
21
+ custom_system_prompt: Optional[str] = None,
22
+ output_dir: Optional[str] = None
23
+ ):
24
+ self.model = model
25
+ self.custom_system_prompt = custom_system_prompt or (
26
+ "Convert the PDF page to a clean, well-formatted Markdown document. "
27
+ "Preserve structure, headings, and any code or mathematical notation. "
28
+ "For the images and chart, create a literal description what is visible. "
29
+ "Return only pure Markdown content, excluding any metadata or non-Markdown elements."
30
+ )
31
+ self.output_dir = output_dir
32
+
33
+ @staticmethod
34
+ def validate_pdf_path(pdf_path: str) -> bool:
35
+ """Validate the PDF file path."""
36
+ if not pdf_path:
37
+ logger.error("PDF path is required")
38
+ return False
39
+ if not os.path.exists(pdf_path):
40
+ logger.error(f"PDF file not found: {pdf_path}")
41
+ return False
42
+ if not pdf_path.lower().endswith(".pdf"):
43
+ logger.error(f"File must be a PDF: {pdf_path}")
44
+ return False
45
+ return True
46
+
47
+ async def convert_pdf(
48
+ self,
49
+ pdf_path: str,
50
+ select_pages: Optional[Union[int, List[int]]] = None
51
+ ) -> str:
52
+ """Convert a PDF to Markdown using a vision model."""
53
+ if not self.validate_pdf_path(pdf_path):
54
+ raise ValueError("Invalid PDF path")
55
+
56
+ try:
57
+ logger.info(f"Calling zerox with model: {self.model}, file: {pdf_path}")
58
+ zerox_result = await zerox(
59
+ file_path=pdf_path,
60
+ model=self.model,
61
+ system_prompt=self.custom_system_prompt,
62
+ output_dir=self.output_dir,
63
+ select_pages=select_pages
64
+ )
65
+
66
+ markdown_content = ""
67
+ if hasattr(zerox_result, 'pages') and zerox_result.pages:
68
+ markdown_content = "\n\n".join(
69
+ page.content for page in zerox_result.pages
70
+ if hasattr(page, 'content') and page.content
71
+ )
72
+ elif isinstance(zerox_result, str):
73
+ markdown_content = zerox_result
74
+ elif hasattr(zerox_result, 'markdown'):
75
+ markdown_content = zerox_result.markdown
76
+ elif hasattr(zerox_result, 'text'):
77
+ markdown_content = zerox_result.text
78
+ else:
79
+ markdown_content = str(zerox_result)
80
+ logger.warning("Unexpected zerox_result type; converted to string.")
81
+
82
+ if not markdown_content.strip():
83
+ logger.warning("Generated Markdown content is empty.")
84
+ return ""
85
+
86
+ logger.info(f"Extracted Markdown content length: {len(markdown_content)} characters")
87
+ return markdown_content
88
+
89
+ except Exception as e:
90
+ logger.error(f"Error converting PDF to Markdown: {e}")
91
+ raise
92
+
93
+ async def convert_and_save(
94
+ self,
95
+ pdf_path: str,
96
+ output_md: Optional[str] = None,
97
+ select_pages: Optional[Union[int, List[int]]] = None
98
+ ) -> str:
99
+ """Convert PDF to Markdown and optionally save to file."""
100
+ markdown_content = await self.convert_pdf(pdf_path, select_pages)
101
+
102
+ if output_md:
103
+ output_path = Path(output_md)
104
+ output_path.parent.mkdir(parents=True, exist_ok=True)
105
+ with output_path.open("w", encoding="utf-8") as f:
106
+ f.write(markdown_content)
107
+ logger.info(f"Saved Markdown to: {output_path}")
108
+ return str(output_path)
109
+
110
+ return markdown_content
111
+
112
+ # Typer CLI app
113
+ app = typer.Typer()
114
+
115
+ @app.command()
116
+ def convert(
117
+ input_pdf: str = typer.Argument(..., help="Path to the input PDF file"),
118
+ output_md: Optional[str] = typer.Argument(None, help="Path to save the output Markdown file (defaults to input_pdf_name.md)"),
119
+ model: str = typer.Option("gemini/gemini-2.0-flash", help="LiteLLM-compatible model name"),
120
+ system_prompt: Optional[str] = typer.Option(None, help="Custom system prompt for the vision model")
121
+ ):
122
+ """Convert a PDF file to Markdown using vision models."""
123
+ if not PDFToMarkdownConverter.validate_pdf_path(input_pdf):
124
+ typer.echo(f"Error: Invalid PDF path: {input_pdf}", err=True)
125
+ raise typer.Exit(code=1)
126
+
127
+ if output_md is None:
128
+ output_md = str(Path(input_pdf).with_suffix(".md"))
129
+
130
+ with tempfile.TemporaryDirectory() as temp_dir:
131
+ try:
132
+ converter = PDFToMarkdownConverter(
133
+ model=model,
134
+ custom_system_prompt=system_prompt,
135
+ output_dir=temp_dir
136
+ )
137
+ output_path = asyncio.run(converter.convert_and_save(input_pdf, output_md))
138
+ typer.echo(f"PDF converted to Markdown: {output_path}")
139
+ except Exception as e:
140
+ typer.echo(f"Error during conversion: {e}", err=True)
141
+ raise typer.Exit(code=1)
142
+
143
+ if __name__ == "__main__":
144
+ app()
@@ -64,7 +64,7 @@ class ReplaceInFileTool(Tool):
64
64
  "Returns the updated content or an error."
65
65
  "⚠️ THIS TOOL MUST BE USED IN PRIORITY TO UPDATE AN EXISTING FILE."
66
66
  )
67
- need_validation: bool = False
67
+ need_validation: bool = True
68
68
 
69
69
  SIMILARITY_THRESHOLD: float = 0.85
70
70
 
@@ -0,0 +1,293 @@
1
+ """Tool for capturing terminal recordings and screenshots."""
2
+
3
+ import os
4
+ import shlex
5
+ import subprocess
6
+ from datetime import datetime
7
+ from enum import Enum
8
+ from pathlib import Path
9
+ from typing import Optional
10
+
11
+ from loguru import logger
12
+
13
+ from quantalogic.tools.execute_bash_command_tool import ExecuteBashCommandTool
14
+ from quantalogic.tools.tool import Tool, ToolArgument
15
+
16
+
17
+ class CaptureType(str, Enum):
18
+ """Type of terminal capture."""
19
+ RECORD = "record"
20
+ SCREENSHOT = "screenshot"
21
+
22
+
23
+ class TerminalCaptureTool(Tool):
24
+ """Tool for capturing terminal output as recordings or screenshots."""
25
+
26
+ name: str = "terminal_capture_tool"
27
+ description: str = "Captures terminal output as recordings or screenshots."
28
+ need_validation: bool = False
29
+ arguments: list = [
30
+ ToolArgument(
31
+ name="capture_type",
32
+ arg_type="string",
33
+ description="Type of capture ('record' or 'screenshot')",
34
+ required=True,
35
+ example="screenshot",
36
+ ),
37
+ ToolArgument(
38
+ name="output_path",
39
+ arg_type="string",
40
+ description="Path where to save the capture (use .cast for recordings, .png for screenshots)",
41
+ required=True,
42
+ example="/path/to/output.png",
43
+ ),
44
+ ToolArgument(
45
+ name="command",
46
+ arg_type="string",
47
+ description="Command to execute and capture (only for recording)",
48
+ required=False,
49
+ example="ls -la",
50
+ ),
51
+ ToolArgument(
52
+ name="duration",
53
+ arg_type="int",
54
+ description="Duration in seconds for recording (only for recording)",
55
+ required=False,
56
+ default="30",
57
+ ),
58
+ ToolArgument(
59
+ name="overwrite",
60
+ arg_type="boolean",
61
+ description="Whether to overwrite existing file",
62
+ required=False,
63
+ default="true",
64
+ ),
65
+ ]
66
+
67
+ def __init__(self):
68
+ """Initialize with ExecuteBashCommandTool for command execution."""
69
+ super().__init__()
70
+ self.bash_tool = ExecuteBashCommandTool()
71
+
72
+ def _ensure_dependencies(self, capture_type: CaptureType) -> bool:
73
+ """Check if required dependencies are installed.
74
+
75
+ Args:
76
+ capture_type: Type of capture to check dependencies for
77
+ """
78
+ if capture_type == CaptureType.RECORD:
79
+ try:
80
+ # Check for asciinema using poetry run
81
+ result = subprocess.run(
82
+ ["poetry", "run", "which", "asciinema"],
83
+ capture_output=True,
84
+ text=True,
85
+ check=False
86
+ )
87
+
88
+ if result.returncode == 0 and result.stdout:
89
+ logger.info(f"Found asciinema at: {result.stdout.strip()}")
90
+ return True
91
+
92
+ # If not found, try installing it
93
+ logger.info("Installing asciinema...")
94
+ install_result = subprocess.run(
95
+ ["poetry", "add", "asciinema"],
96
+ capture_output=True,
97
+ text=True,
98
+ check=False
99
+ )
100
+
101
+ if install_result.returncode == 0:
102
+ logger.info("Successfully installed asciinema")
103
+ return True
104
+
105
+ logger.error(f"Failed to install asciinema: {install_result.stderr}")
106
+ return False
107
+
108
+ except Exception as e:
109
+ logger.error(f"Error checking/installing asciinema: {e}")
110
+ return False
111
+
112
+ elif capture_type == CaptureType.SCREENSHOT:
113
+ try:
114
+ # Try gnome-screenshot first
115
+ result = subprocess.run(
116
+ ["which", "gnome-screenshot"],
117
+ capture_output=True,
118
+ text=True,
119
+ check=False
120
+ )
121
+
122
+ if result.returncode == 0:
123
+ logger.info("Found gnome-screenshot")
124
+ return True
125
+
126
+ # Try import from ImageMagick as fallback
127
+ result = subprocess.run(
128
+ ["which", "import"],
129
+ capture_output=True,
130
+ text=True,
131
+ check=False
132
+ )
133
+
134
+ if result.returncode == 0:
135
+ logger.info("Found ImageMagick import")
136
+ return True
137
+
138
+ logger.error("Neither gnome-screenshot nor ImageMagick found")
139
+ return False
140
+
141
+ except Exception as e:
142
+ logger.error(f"Error checking screenshot dependencies: {e}")
143
+ return False
144
+
145
+ def _capture_screenshot(
146
+ self,
147
+ output_path: str,
148
+ overwrite: bool = True
149
+ ) -> str:
150
+ """Capture terminal screenshot.
151
+
152
+ Args:
153
+ output_path: Path to save screenshot
154
+ overwrite: Whether to overwrite existing file
155
+ """
156
+ try:
157
+ # Ensure output directory exists
158
+ output_path = Path(output_path)
159
+ output_path.parent.mkdir(parents=True, exist_ok=True)
160
+
161
+ # Check if file exists and handle overwrite
162
+ if output_path.exists():
163
+ if not overwrite:
164
+ return f"File {output_path} already exists and overwrite=False"
165
+ try:
166
+ output_path.unlink()
167
+ except OSError as e:
168
+ return f"Failed to remove existing file: {str(e)}"
169
+
170
+ # Get active window ID
171
+ window_id = subprocess.run(
172
+ ["xdotool", "getactivewindow"],
173
+ capture_output=True,
174
+ text=True,
175
+ check=False
176
+ )
177
+
178
+ if window_id.returncode != 0:
179
+ return "Failed to get active window ID"
180
+
181
+ # Try gnome-screenshot first
182
+ if subprocess.run(["which", "gnome-screenshot"], capture_output=True).returncode == 0:
183
+ cmd = f"gnome-screenshot --window --file={shlex.quote(str(output_path))}"
184
+ else:
185
+ # Fallback to ImageMagick's import
186
+ cmd = f"import -window {window_id.stdout.strip()} {shlex.quote(str(output_path))}"
187
+
188
+ logger.debug(f"Running command: {cmd}")
189
+ result = self.bash_tool.execute(command=cmd)
190
+
191
+ if output_path.exists():
192
+ return f"Screenshot saved to {output_path}"
193
+ else:
194
+ return f"Failed to capture screenshot: {result}"
195
+
196
+ except Exception as e:
197
+ return f"Failed to capture screenshot: {str(e)}"
198
+
199
+ def _record_terminal(
200
+ self,
201
+ output_path: str,
202
+ command: str,
203
+ duration: int = 30,
204
+ overwrite: bool = True
205
+ ) -> str:
206
+ """Record terminal session using asciinema.
207
+
208
+ Args:
209
+ output_path: Path to save recording (.cast file)
210
+ command: Command to execute
211
+ duration: Recording duration in seconds
212
+ overwrite: Whether to overwrite existing file
213
+ """
214
+ try:
215
+ # Ensure output directory exists
216
+ output_path = Path(output_path)
217
+ output_path.parent.mkdir(parents=True, exist_ok=True)
218
+
219
+ # Check if file exists and handle overwrite
220
+ if output_path.exists():
221
+ if not overwrite:
222
+ return f"File {output_path} already exists and overwrite=False"
223
+ try:
224
+ output_path.unlink()
225
+ except OSError as e:
226
+ return f"Failed to remove existing file: {str(e)}"
227
+
228
+ # Properly escape the command for shell execution
229
+ escaped_command = shlex.quote(command)
230
+
231
+ # Record terminal session using poetry run to ensure correct virtualenv
232
+ record_cmd = (
233
+ f"poetry run asciinema rec -c {escaped_command} "
234
+ f"-t 'Terminal Recording {datetime.now()}' "
235
+ f"{shlex.quote(str(output_path))}"
236
+ )
237
+ logger.debug(f"Running command: {record_cmd}")
238
+
239
+ result = self.bash_tool.execute(
240
+ command=record_cmd,
241
+ timeout=duration
242
+ )
243
+
244
+ if output_path.exists():
245
+ return f"Recording saved to {output_path}"
246
+ else:
247
+ return f"Failed to record: {result}"
248
+
249
+ except Exception as e:
250
+ return f"Failed to record terminal: {str(e)}"
251
+
252
+ def execute(
253
+ self,
254
+ capture_type: str,
255
+ output_path: str,
256
+ command: Optional[str] = None,
257
+ duration: Optional[int] = 30,
258
+ overwrite: Optional[bool] = True,
259
+ ) -> str:
260
+ """Execute the terminal capture.
261
+
262
+ Args:
263
+ capture_type: Type of capture ('record' or 'screenshot')
264
+ output_path: Path where to save the capture
265
+ command: Command to execute and capture (only for recording)
266
+ duration: Duration in seconds for recording (only for recording)
267
+ overwrite: Whether to overwrite existing file
268
+
269
+ Returns:
270
+ A string indicating success or failure
271
+ """
272
+ try:
273
+ capture_type_enum = CaptureType(capture_type.lower())
274
+ except ValueError:
275
+ return f"Invalid capture type: {capture_type}. Must be 'record' or 'screenshot'"
276
+
277
+ if not self._ensure_dependencies(capture_type_enum):
278
+ if capture_type_enum == CaptureType.RECORD:
279
+ return "asciinema not found and failed to install it. Please install manually: poetry add asciinema"
280
+ else:
281
+ return "Screenshot tools not found. Please install: sudo apt install gnome-screenshot"
282
+
283
+ if capture_type_enum == CaptureType.SCREENSHOT:
284
+ return self._capture_screenshot(output_path, overwrite)
285
+ else:
286
+ if not command:
287
+ return "Command is required for recording"
288
+ return self._record_terminal(output_path, command, duration, overwrite)
289
+
290
+
291
+ if __name__ == "__main__":
292
+ tool = TerminalCaptureTool()
293
+ print(tool.to_markdown())
quantalogic/tools/tool.py CHANGED
@@ -58,6 +58,10 @@ class ToolDefinition(BaseModel):
58
58
  default=False,
59
59
  description="When True, requires user confirmation before execution. Useful for tools that perform potentially destructive operations.",
60
60
  )
61
+ need_post_process: bool = Field(
62
+ default=True,
63
+ description="When True, requires user confirmation before execution. Useful for tools that perform potentially destructive operations.",
64
+ )
61
65
  need_variables: bool = Field(
62
66
  default=False,
63
67
  description="When True, provides access to the agent's variable store. Required for tools that need to interpolate variables (e.g., Jinja templates).",
@@ -11,6 +11,7 @@ from .csv_processor_tool import CSVProcessorTool
11
11
  from .download_file_tool import PrepareDownloadTool
12
12
  from .mermaid_validator_tool import MermaidValidatorTool
13
13
  from .vscode_tool import VSCodeServerTool
14
+ from .llm_tool import OrientedLLMTool
14
15
 
15
16
  # Define __all__ to control what is imported with `from ... import *`
16
17
  __all__ = [
@@ -18,6 +19,7 @@ __all__ = [
18
19
  'PrepareDownloadTool',
19
20
  'MermaidValidatorTool',
20
21
  'VSCodeServerTool',
22
+ 'OrientedLLMTool',
21
23
  ]
22
24
 
23
25
  # Optional: Add logging for import confirmation
@@ -20,7 +20,7 @@ class PrepareDownloadTool(Tool):
20
20
  "If it's a directory, it will be zipped. "
21
21
  "Returns an HTML link for downloading."
22
22
  )
23
- need_validation: bool = True
23
+ need_validation: bool = False
24
24
  arguments: list = [
25
25
  ToolArgument(
26
26
  name="path",
@@ -106,10 +106,8 @@ class PrepareDownloadTool(Tool):
106
106
  )
107
107
 
108
108
  # Create the HTML link with embedded styles
109
- html = f'''<a href="{url}"
110
- style="{style}"
111
- onmouseover="this.style.backgroundColor='#0066cc'; this.style.color='white';"
112
- onmouseout="this.style.backgroundColor='transparent'; this.style.color='#0066cc';"
109
+ html = f'''Using this download functionnal download link : <a href="{url}"
110
+ style="{style}"
113
111
  download="{filename}">{text}</a>'''
114
112
 
115
113
  return html