quantalogic 0.2.14__tar.gz → 0.2.16__tar.gz

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 (74) hide show
  1. {quantalogic-0.2.14 → quantalogic-0.2.16}/PKG-INFO +5 -2
  2. {quantalogic-0.2.14 → quantalogic-0.2.16}/README.md +3 -1
  3. {quantalogic-0.2.14 → quantalogic-0.2.16}/pyproject.toml +2 -1
  4. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/agent.py +3 -3
  5. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/agent_config.py +4 -0
  6. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/coding_agent.py +3 -1
  7. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/generative_model.py +43 -15
  8. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/main.py +43 -16
  9. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/prompts.py +1 -0
  10. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/search_agent.py +24 -5
  11. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/__init__.py +2 -0
  12. quantalogic-0.2.16/quantalogic/tools/duckduckgo_search_tool.py +214 -0
  13. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/markitdown_tool.py +36 -33
  14. {quantalogic-0.2.14 → quantalogic-0.2.16}/LICENSE +0 -0
  15. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/__init__.py +0 -0
  16. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/event_emitter.py +0 -0
  17. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/interactive_text_editor.py +0 -0
  18. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/memory.py +0 -0
  19. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/model_names.py +0 -0
  20. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/print_event.py +0 -0
  21. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/server/__init__.py +0 -0
  22. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/server/agent_server.py +0 -0
  23. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/server/models.py +0 -0
  24. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/server/routes.py +0 -0
  25. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/server/state.py +0 -0
  26. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/server/static/js/event_visualizer.js +0 -0
  27. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/server/static/js/quantalogic.js +0 -0
  28. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/server/templates/index.html +0 -0
  29. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tool_manager.py +0 -0
  30. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/agent_tool.py +0 -0
  31. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/download_http_file_tool.py +0 -0
  32. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/edit_whole_content_tool.py +0 -0
  33. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/elixir_tool.py +0 -0
  34. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/execute_bash_command_tool.py +0 -0
  35. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/input_question_tool.py +0 -0
  36. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/language_handlers/__init__.py +0 -0
  37. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/language_handlers/c_handler.py +0 -0
  38. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/language_handlers/cpp_handler.py +0 -0
  39. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/language_handlers/go_handler.py +0 -0
  40. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/language_handlers/java_handler.py +0 -0
  41. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/language_handlers/javascript_handler.py +0 -0
  42. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/language_handlers/python_handler.py +0 -0
  43. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/language_handlers/rust_handler.py +0 -0
  44. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/language_handlers/scala_handler.py +0 -0
  45. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/language_handlers/typescript_handler.py +0 -0
  46. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/list_directory_tool.py +0 -0
  47. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/llm_tool.py +0 -0
  48. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/llm_vision_tool.py +0 -0
  49. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/nodejs_tool.py +0 -0
  50. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/python_tool.py +0 -0
  51. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/read_file_block_tool.py +0 -0
  52. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/read_file_tool.py +0 -0
  53. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/replace_in_file_tool.py +0 -0
  54. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/ripgrep_tool.py +0 -0
  55. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/search_definition_names.py +0 -0
  56. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/serpapi_search_tool.py +0 -0
  57. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/task_complete_tool.py +0 -0
  58. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/tool.py +0 -0
  59. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/unified_diff_tool.py +0 -0
  60. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/wikipedia_search_tool.py +0 -0
  61. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/tools/write_file_tool.py +0 -0
  62. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/utils/__init__.py +0 -0
  63. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/utils/ask_user_validation.py +0 -0
  64. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/utils/check_version.py +0 -0
  65. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/utils/download_http_file.py +0 -0
  66. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/utils/get_coding_environment.py +0 -0
  67. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/utils/get_environment.py +0 -0
  68. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/utils/get_quantalogic_rules_content.py +0 -0
  69. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/utils/git_ls.py +0 -0
  70. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/utils/read_file.py +0 -0
  71. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/utils/read_http_text_content.py +0 -0
  72. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/version.py +0 -0
  73. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/xml_parser.py +0 -0
  74. {quantalogic-0.2.14 → quantalogic-0.2.16}/quantalogic/xml_tool_parser.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: quantalogic
3
- Version: 0.2.14
3
+ Version: 0.2.16
4
4
  Summary: QuantaLogic ReAct Agents
5
5
  Author: Raphaël MANSUY
6
6
  Author-email: raphael.mansuy@gmail.com
@@ -10,6 +10,7 @@ Classifier: Programming Language :: Python :: 3.12
10
10
  Classifier: Programming Language :: Python :: 3.13
11
11
  Requires-Dist: boto3 (>=1.35.86,<2.0.0)
12
12
  Requires-Dist: click (>=8.1.8,<9.0.0)
13
+ Requires-Dist: duckduckgo-search (>=7.2.1,<8.0.0)
13
14
  Requires-Dist: fastapi (>=0.115.6,<0.116.0)
14
15
  Requires-Dist: google-auth (>=2.20.0,<3.0.0)
15
16
  Requires-Dist: google-search-results (>=2.4.2,<3.0.0)
@@ -132,7 +133,8 @@ Options:
132
133
  e.g. "openrouter/A/gpt-4o-mini").
133
134
  --log [info|debug|warning] Set logging level (info/debug/warning).
134
135
  --verbose Enable verbose output.
135
- --mode [code|basic|interpreter|full|code-basic|search]
136
+ --max-iterations INTEGER Maximum iterations for task solving (default: 30).
137
+ --mode [code|basic|interpreter|full|code-basic|search|search-full]
136
138
  Agent mode (code/search/full).
137
139
  --help Show this message and exit.
138
140
 
@@ -153,6 +155,7 @@ task Execute a task with the QuantaLogic AI Assistant
153
155
  - interpreter: Interactive code execution agent
154
156
  - full: Full-featured agent with all capabilities
155
157
  - code-basic: Coding agent with basic reasoning
158
+ - search: Web search agent with Wikipedia, DuckDuckGo and SERPApi integration
156
159
 
157
160
  #### Task Execution
158
161
 
@@ -92,7 +92,8 @@ Options:
92
92
  e.g. "openrouter/A/gpt-4o-mini").
93
93
  --log [info|debug|warning] Set logging level (info/debug/warning).
94
94
  --verbose Enable verbose output.
95
- --mode [code|basic|interpreter|full|code-basic|search]
95
+ --max-iterations INTEGER Maximum iterations for task solving (default: 30).
96
+ --mode [code|basic|interpreter|full|code-basic|search|search-full]
96
97
  Agent mode (code/search/full).
97
98
  --help Show this message and exit.
98
99
 
@@ -113,6 +114,7 @@ task Execute a task with the QuantaLogic AI Assistant
113
114
  - interpreter: Interactive code execution agent
114
115
  - full: Full-featured agent with all capabilities
115
116
  - code-basic: Coding agent with basic reasoning
117
+ - search: Web search agent with Wikipedia, DuckDuckGo and SERPApi integration
116
118
 
117
119
  #### Task Execution
118
120
 
@@ -1,6 +1,6 @@
1
1
  [tool.poetry]
2
2
  name = "quantalogic"
3
- version = "0.2.14"
3
+ version = "0.2.16"
4
4
  description = "QuantaLogic ReAct Agents"
5
5
  authors = ["Raphaël MANSUY <raphael.mansuy@gmail.com>"]
6
6
  readme = "README.md"
@@ -36,6 +36,7 @@ toml = "^0.10.2"
36
36
  types-requests = "^2.32.0.20241016"
37
37
  google-search-results = "^2.4.2"
38
38
  serpapi = "^0.1.5"
39
+ duckduckgo-search = "^7.2.1"
39
40
 
40
41
  [tool.poetry.scripts]
41
42
  quantalogic = "quantalogic.main:cli"
@@ -406,7 +406,7 @@ class Agent(BaseModel):
406
406
 
407
407
  formatted_response = (
408
408
  "\n"
409
- f"--- Observations for iteration {iteration} ---\n"
409
+ f"--- Observations for iteration {iteration} / max {self.max_iterations} ---\n"
410
410
  "\n"
411
411
  f"\n --- Tool execution result stored in variable ${variable_name}$ --- \n"
412
412
  "\n"
@@ -428,7 +428,7 @@ class Agent(BaseModel):
428
428
  # Format the response message
429
429
  formatted_response = (
430
430
  "\n"
431
- f"--- Observations for iteration {iteration} ---\n"
431
+ f"--- Observations for iteration {iteration} / max {self.max_iterations} ---\n"
432
432
  "\n"
433
433
  f"\n --- Tool execution result stored in variable ${variable_name}$ --- \n"
434
434
  "\n"
@@ -440,7 +440,7 @@ class Agent(BaseModel):
440
440
  "\n"
441
441
  f"--- Variables --- \n"
442
442
  "\n"
443
- f"{self._get_variable_prompt()}"
443
+ f"{self._get_variable_prompt()}\n"
444
444
  "\n"
445
445
  "You must analyze this answer and evaluate what to do next to solve the task.\n"
446
446
  "If the step failed, take a step back and rethink your approach.\n"
@@ -24,6 +24,8 @@ from quantalogic.tools import (
24
24
  SearchDefinitionNames,
25
25
  TaskCompleteTool,
26
26
  WriteFileTool,
27
+ DuckDuckGoSearchTool,
28
+ WikipediaSearchTool,
27
29
  )
28
30
 
29
31
  MODEL_NAME = "deepseek/deepseek-chat"
@@ -124,6 +126,8 @@ def create_full_agent(model_name: str, vision_model_name: str | None) -> Agent:
124
126
  MarkitdownTool(),
125
127
  LLMTool(model_name=model_name),
126
128
  DownloadHttpFileTool(),
129
+ WikipediaSearchTool(),
130
+ DuckDuckGoSearchTool(),
127
131
  ]
128
132
 
129
133
  if vision_model_name:
@@ -1,5 +1,6 @@
1
1
  from quantalogic.agent import Agent
2
2
  from quantalogic.tools import (
3
+ DuckDuckGoSearchTool,
3
4
  EditWholeContentTool,
4
5
  ExecuteBashCommandTool,
5
6
  InputQuestionTool,
@@ -59,6 +60,7 @@ def create_coding_agent(model_name: str, vision_model_name: str | None = None, b
59
60
  ReadFileTool(),
60
61
  ExecuteBashCommandTool(),
61
62
  InputQuestionTool(),
63
+ DuckDuckGoSearchTool(),
62
64
  ]
63
65
 
64
66
  if vision_model_name:
@@ -69,7 +71,7 @@ def create_coding_agent(model_name: str, vision_model_name: str | None = None, b
69
71
  LLMTool(
70
72
  model_name=model_name,
71
73
  system_prompt="You are a software expert, your role is to answer coding questions.",
72
- name="coding_consultant", # Handles implementation-level coding questions
74
+ name="coding_consultant", # Handles implementation-level coding questions
73
75
  )
74
76
  )
75
77
  tools.append(
@@ -1,5 +1,7 @@
1
1
  """Generative model module for AI-powered text generation."""
2
2
 
3
+ import functools
4
+
3
5
  import openai
4
6
  from litellm import completion, exceptions, get_max_tokens, get_model_info, token_counter
5
7
  from loguru import logger
@@ -83,6 +85,7 @@ class GenerativeModel:
83
85
  logger.debug(f"Initializing GenerativeModel with model={model}, temperature={temperature}")
84
86
  self.model = model
85
87
  self.temperature = temperature
88
+ self._get_model_info_cached = functools.lru_cache(maxsize=32)(self._get_model_info_impl)
86
89
 
87
90
  # Define retriable exceptions based on LiteLLM's exception mapping
88
91
  RETRIABLE_EXCEPTIONS = (
@@ -235,21 +238,46 @@ class GenerativeModel:
235
238
  litellm_messages.append({"role": "user", "content": str(prompt)})
236
239
  return token_counter(model=self.model, messages=litellm_messages)
237
240
 
238
- def get_model_info(self) -> dict | None:
239
- """Get information about the model."""
240
- logger.debug(f"Retrieving model info for {self.model}")
241
- model_info = get_model_info(self.model)
242
-
243
- if not model_info:
244
- logger.debug("Model info not found, trying without openrouter/ prefix")
245
- model_info = get_model_info(self.model.replace("openrouter/", ""))
246
-
247
- if model_info:
248
- logger.debug(f"Model info retrieved: {model_info.keys()}")
249
- else:
250
- logger.debug("No model info available")
251
-
252
- return model_info
241
+ def _get_model_info_impl(self, model_name: str) -> dict:
242
+ """Get information about the model with prefix fallback logic.
243
+
244
+ Attempts to find model info by progressively removing provider prefixes.
245
+ Raises ValueError if no valid model configuration is found.
246
+ Results are cached to improve performance.
247
+
248
+ Example:
249
+ openrouter/openai/gpt-4o-mini → openai/gpt-4o-mini → gpt-4o-mini
250
+ """
251
+ original_model = model_name
252
+
253
+ while True:
254
+ try:
255
+ logger.debug(f"Attempting to retrieve model info for: {model_name}")
256
+ model_info = get_model_info(model_name)
257
+ if model_info:
258
+ logger.debug(f"Found model info for {model_name}: {model_info}")
259
+ return model_info
260
+ except Exception:
261
+ pass
262
+
263
+ # Try removing one prefix level
264
+ parts = model_name.split('/')
265
+ if len(parts) <= 1:
266
+ break
267
+ model_name = '/'.join(parts[1:])
268
+
269
+ error_msg = f"Could not find model info for {original_model} after trying: {self.model} → {model_name}"
270
+ logger.error(error_msg)
271
+ raise ValueError(error_msg)
272
+
273
+ def get_model_info(self, model_name: str = None) -> dict:
274
+ """Get cached information about the model.
275
+
276
+ If no model name is provided, uses the current model.
277
+ """
278
+ if model_name is None:
279
+ model_name = self.model
280
+ return self._get_model_info_cached(model_name)
253
281
 
254
282
  def get_model_max_input_tokens(self) -> int:
255
283
  """Get the maximum number of input tokens for the model."""
@@ -32,9 +32,9 @@ from quantalogic.agent_config import ( # noqa: E402
32
32
  )
33
33
  from quantalogic.interactive_text_editor import get_multiline_input # noqa: E402
34
34
  from quantalogic.print_event import console_print_events # noqa: E402
35
- from quantalogic.search_agent import create_search_agent
35
+ from quantalogic.search_agent import create_search_agent # noqa: E402
36
36
 
37
- AGENT_MODES = ["code", "basic", "interpreter", "full", "code-basic","search"]
37
+ AGENT_MODES = ["code", "basic", "interpreter", "full", "code-basic", "search", "search-full"]
38
38
 
39
39
 
40
40
  def create_agent_for_mode(mode: str, model_name: str, vision_model_name: str | None) -> Agent:
@@ -53,9 +53,12 @@ def create_agent_for_mode(mode: str, model_name: str, vision_model_name: str | N
53
53
  return create_interpreter_agent(model_name, vision_model_name)
54
54
  elif mode == "search":
55
55
  return create_search_agent(model_name)
56
+ if mode == "search-full":
57
+ return create_search_agent(model_name, mode_full=True)
56
58
  else:
57
59
  raise ValueError(f"Unknown agent mode: {mode}")
58
60
 
61
+
59
62
  def check_new_version():
60
63
  # Randomly check for updates (1 in 10 chance)
61
64
  if random.randint(1, 10) == 1:
@@ -81,6 +84,7 @@ def check_new_version():
81
84
  except Exception:
82
85
  return
83
86
 
87
+
84
88
  def configure_logger(log_level: str) -> None:
85
89
  """Configure the logger with the specified log level and format."""
86
90
  logger.remove()
@@ -122,7 +126,9 @@ def get_task_from_file(file_path: str) -> str:
122
126
  raise Exception(f"Unexpected error reading file: {e}")
123
127
 
124
128
 
125
- def display_welcome_message(console: Console, model_name: str, vision_model_name: str | None) -> None:
129
+ def display_welcome_message(
130
+ console: Console, model_name: str, vision_model_name: str | None, max_iterations: int = 50
131
+ ) -> None:
126
132
  """Display the welcome message and instructions."""
127
133
  version = get_version()
128
134
  console.print(
@@ -135,7 +141,8 @@ def display_welcome_message(console: Console, model_name: str, vision_model_name
135
141
  f"[yellow] 🤖 System Info:[/yellow]\n\n"
136
142
  "\n"
137
143
  f"- Model: {model_name}\n"
138
- f"- Vision Model: {vision_model_name}\n\n"
144
+ f"- Vision Model: {vision_model_name}\n"
145
+ f"- Max Iterations: {max_iterations}\n\n"
139
146
  "[bold magenta]💡 Pro Tips:[/bold magenta]\n\n"
140
147
  "- Be as specific as possible in your task description to get the best results!\n"
141
148
  "- Use clear and concise language when describing your task\n"
@@ -167,6 +174,12 @@ def display_welcome_message(console: Console, model_name: str, vision_model_name
167
174
  default=None,
168
175
  help='Specify the vision model to use (litellm format, e.g. "openrouter/A/gpt-4o-mini").',
169
176
  )
177
+ @click.option(
178
+ "--max-iterations",
179
+ type=int,
180
+ default=30,
181
+ help="Maximum number of iterations for task solving (default: 30).",
182
+ )
170
183
  @click.pass_context
171
184
  def cli(
172
185
  ctx: click.Context,
@@ -176,6 +189,7 @@ def cli(
176
189
  mode: str,
177
190
  log: str,
178
191
  vision_model_name: str | None,
192
+ max_iterations: int,
179
193
  ) -> None:
180
194
  """QuantaLogic AI Assistant - A powerful AI tool for various tasks."""
181
195
  if version:
@@ -184,7 +198,13 @@ def cli(
184
198
  sys.exit(0)
185
199
  if ctx.invoked_subcommand is None:
186
200
  ctx.invoke(
187
- task, model_name=model_name, verbose=verbose, mode=mode, log=log, vision_model_name=vision_model_name
201
+ task,
202
+ model_name=model_name,
203
+ verbose=verbose,
204
+ mode=mode,
205
+ log=log,
206
+ vision_model_name=vision_model_name,
207
+ max_iterations=max_iterations,
188
208
  )
189
209
 
190
210
 
@@ -208,6 +228,12 @@ def cli(
208
228
  default=None,
209
229
  help='Specify the vision model to use (litellm format, e.g. "openrouter/openai/gpt-4o-mini").',
210
230
  )
231
+ @click.option(
232
+ "--max-iterations",
233
+ type=int,
234
+ default=30,
235
+ help="Maximum number of iterations for task solving (default: 30).",
236
+ )
211
237
  @click.argument("task", required=False)
212
238
  def task(
213
239
  file: Optional[str],
@@ -217,12 +243,12 @@ def task(
217
243
  log: str,
218
244
  vision_model_name: str | None,
219
245
  task: Optional[str],
246
+ max_iterations: int,
220
247
  ) -> None:
221
248
  """Execute a task with the QuantaLogic AI Assistant."""
222
249
  console = Console()
223
250
  switch_verbose(verbose, log)
224
251
 
225
-
226
252
  try:
227
253
  if file:
228
254
  task_content = get_task_from_file(file)
@@ -231,7 +257,7 @@ def task(
231
257
  check_new_version()
232
258
  task_content = task
233
259
  else:
234
- display_welcome_message(console, model_name, vision_model_name)
260
+ display_welcome_message(console, model_name, vision_model_name, max_iterations=max_iterations)
235
261
  check_new_version()
236
262
  logger.debug("Waiting for user input...")
237
263
  task_content = get_multiline_input(console).strip()
@@ -241,14 +267,13 @@ def task(
241
267
  console.print("[yellow]No task provided. Exiting...[/yellow]")
242
268
  sys.exit(2)
243
269
 
244
- if model_name != MODEL_NAME:
245
- console.print(
246
- Panel.fit(
247
- f"[bold]Task to be submitted:[/bold]\n{task_content}",
248
- title="[bold]Task Preview[/bold]",
249
- border_style="blue",
250
- )
270
+ console.print(
271
+ Panel.fit(
272
+ f"[bold]Task to be submitted:[/bold]\n{task_content}",
273
+ title="[bold]Task Preview[/bold]",
274
+ border_style="blue",
251
275
  )
276
+ )
252
277
  if not Confirm.ask("[bold]Are you sure you want to submit this task?[/bold]"):
253
278
  console.print("[yellow]Task submission cancelled. Exiting...[/yellow]")
254
279
  sys.exit(0)
@@ -284,8 +309,10 @@ def task(
284
309
  logger.debug("Registered event handlers for agent events with events: {events}")
285
310
 
286
311
  logger.debug(f"Solving task with agent: {task_content}")
287
- result = agent.solve_task(task=task_content, max_iterations=300)
288
- logger.debug(f"Task solved with result: {result}")
312
+ if max_iterations < 1:
313
+ raise ValueError("max_iterations must be greater than 0")
314
+ result = agent.solve_task(task=task_content, max_iterations=max_iterations)
315
+ logger.debug(f"Task solved with result: {result} using {max_iterations} iterations")
289
316
 
290
317
  console.print(
291
318
  Panel.fit(
@@ -90,4 +90,5 @@ Every response must contain exactly two XML blocks:
90
90
 
91
91
  ### Environment Details
92
92
  {environment}
93
+
93
94
  """
@@ -1,16 +1,27 @@
1
1
  from quantalogic.agent import Agent
2
- from quantalogic.tools import InputQuestionTool, SerpApiSearchTool, TaskCompleteTool, WikipediaSearchTool, ReadFileBlockTool,ReadFileTool, MarkitdownTool, RipgrepTool
2
+ from quantalogic.tools import (
3
+ DuckDuckGoSearchTool,
4
+ InputQuestionTool,
5
+ MarkitdownTool,
6
+ ReadFileBlockTool,
7
+ ReadFileTool,
8
+ RipgrepTool,
9
+ SerpApiSearchTool,
10
+ TaskCompleteTool,
11
+ WikipediaSearchTool,
12
+ )
3
13
 
4
14
 
5
- def create_search_agent(model_name: str) -> Agent:
6
- """Creates and configures a search agent with web and knowledge search tools.
15
+ def create_search_agent(model_name: str, mode_full: bool = False) -> Agent:
16
+ """Creates and configures a search agent with web, knowledge, and privacy-focused search tools.
7
17
 
8
18
  Args:
9
19
  model_name (str): Name of the language model to use for the agent's core capabilities
20
+ mode_full (bool, optional): If True, the agent will be configured with a full set of tools.
10
21
 
11
22
  Returns:
12
23
  Agent: A fully configured search agent instance with:
13
- - Web search capabilities (SerpAPI)
24
+ - Web search capabilities (SerpAPI, DuckDuckGo)
14
25
  - Knowledge search capabilities (Wikipedia)
15
26
  - Basic interaction tools
16
27
  """
@@ -21,7 +32,7 @@ def create_search_agent(model_name: str) -> Agent:
21
32
 
22
33
  tools = [
23
34
  # Search tools
24
- SerpApiSearchTool(), # Web search capabilities
35
+ DuckDuckGoSearchTool(), # Privacy-focused web search
25
36
  WikipediaSearchTool(), # Knowledge search capabilities
26
37
  # Basic interaction tools
27
38
  TaskCompleteTool(), # Marks task completion
@@ -34,6 +45,14 @@ def create_search_agent(model_name: str) -> Agent:
34
45
  RipgrepTool(), # Code search capabilities
35
46
  ]
36
47
 
48
+ if mode_full:
49
+ tools.extend(
50
+ [
51
+ # Search tools
52
+ SerpApiSearchTool(), # Web search capabilities
53
+ ]
54
+ )
55
+
37
56
  return Agent(
38
57
  model_name=model_name,
39
58
  tools=tools,
@@ -18,6 +18,7 @@ from .replace_in_file_tool import ReplaceInFileTool
18
18
  from .ripgrep_tool import RipgrepTool
19
19
  from .search_definition_names import SearchDefinitionNames
20
20
  from .serpapi_search_tool import SerpApiSearchTool
21
+ from .duckduckgo_search_tool import DuckDuckGoSearchTool
21
22
  from .task_complete_tool import TaskCompleteTool
22
23
  from .tool import Tool, ToolArgument
23
24
  from .unified_diff_tool import UnifiedDiffTool
@@ -27,6 +28,7 @@ from .write_file_tool import WriteFileTool
27
28
  __all__ = [
28
29
  "WikipediaSearchTool",
29
30
  "SerpApiSearchTool",
31
+ "DuckDuckGoSearchTool",
30
32
  "Tool",
31
33
  "ToolArgument",
32
34
  "TaskCompleteTool",
@@ -0,0 +1,214 @@
1
+ """Tool for interacting with DuckDuckGo for search results."""
2
+
3
+ from duckduckgo_search import DDGS
4
+
5
+ from quantalogic.tools.tool import Tool, ToolArgument
6
+
7
+
8
+ class DuckDuckGoSearchTool(Tool):
9
+ """Tool for retrieving search results from DuckDuckGo.
10
+
11
+ This tool provides a convenient interface to DuckDuckGo's search capabilities,
12
+ supporting multiple search types and structured JSON output.
13
+
14
+ Example usage:
15
+ ```python
16
+ tool = DuckDuckGoSearchTool()
17
+ results = tool.execute(
18
+ query="machine learning",
19
+ search_type="text",
20
+ max_results=10,
21
+ region="us-en",
22
+ safesearch="moderate"
23
+ )
24
+ print(results)
25
+ ```
26
+
27
+ The tool handles:
28
+ - Query validation
29
+ - API error handling
30
+ - Multiple search types (text, images, videos, news)
31
+ - Scope filtering (region, safesearch, timelimit)
32
+ - JSON result formatting
33
+ """
34
+
35
+ name: str = "duckduckgo_tool"
36
+ description: str = (
37
+ "Retrieves search results from DuckDuckGo. "
38
+ "Provides structured output of search results."
39
+ )
40
+ arguments: list = [
41
+ ToolArgument(
42
+ name="query",
43
+ arg_type="string",
44
+ description="The search query to execute",
45
+ required=True,
46
+ example="machine learning",
47
+ ),
48
+ ToolArgument(
49
+ name="max_results",
50
+ arg_type="int",
51
+ description="Maximum number of results to retrieve (1-50)",
52
+ required=True,
53
+ default="10",
54
+ example="20",
55
+ ),
56
+ ToolArgument(
57
+ name="search_type",
58
+ arg_type="string",
59
+ description="Type of search to perform (text, images, videos, news)",
60
+ required=False,
61
+ default="text",
62
+ example="images",
63
+ ),
64
+ ToolArgument(
65
+ name="region",
66
+ arg_type="string",
67
+ description="Region for search results (e.g., 'wt-wt', 'us-en')",
68
+ required=False,
69
+ default="wt-wt",
70
+ example="us-en",
71
+ ),
72
+ ToolArgument(
73
+ name="safesearch",
74
+ arg_type="string",
75
+ description="Safesearch level ('on', 'moderate', 'off')",
76
+ required=False,
77
+ default="moderate",
78
+ example="moderate",
79
+ ),
80
+ ToolArgument(
81
+ name="timelimit",
82
+ arg_type="string",
83
+ description="Time limit for results (e.g., 'd' for day, 'w' for week)",
84
+ required=False,
85
+ default=None,
86
+ example="d",
87
+ ),
88
+ ]
89
+
90
+ def execute(
91
+ self,
92
+ query: str,
93
+ max_results: int = 10,
94
+ search_type: str = "text",
95
+ region: str = "wt-wt",
96
+ safesearch: str = "moderate",
97
+ timelimit: str = None,
98
+ ) -> str:
99
+ """Execute a search query using DuckDuckGo and return results.
100
+
101
+ Args:
102
+ query: The search query to execute
103
+ max_results: Maximum number of results to retrieve (1-50)
104
+ search_type: Type of search to perform (text, images, videos, news)
105
+ region: Region for search results (e.g., "wt-wt", "us-en")
106
+ safesearch: Safesearch level ("on", "moderate", "off")
107
+ timelimit: Time limit for results (e.g., "d" for day, "w" for week)
108
+
109
+ Returns:
110
+ Pretty-printed JSON string of search results.
111
+
112
+ Raises:
113
+ ValueError: If any parameter is invalid
114
+ RuntimeError: If search fails
115
+ """
116
+ # Handle empty string parameters by setting to defaults
117
+ query = str(query) if query else query
118
+ search_type = search_type if search_type else "text"
119
+ region = region if region else "wt-wt"
120
+ safesearch = safesearch if safesearch else "moderate"
121
+ timelimit = timelimit if timelimit else None
122
+
123
+ # Validate and convert query
124
+ if not query:
125
+ raise ValueError("Query must be a non-empty string")
126
+ try:
127
+ query = str(query)
128
+ except (TypeError, ValueError) as e:
129
+ raise ValueError(f"Query must be convertible to string: {str(e)}")
130
+
131
+ # Validate and convert max_results
132
+ try:
133
+ max_results = int(max_results)
134
+ if max_results < 1 or max_results > 50:
135
+ raise ValueError("Number of results must be between 1 and 50")
136
+ except (TypeError, ValueError) as e:
137
+ raise ValueError(f"Invalid number of results: {str(e)}")
138
+
139
+ # Validate search_type
140
+ if search_type not in ["text", "images", "videos", "news"]:
141
+ raise ValueError("search_type must be one of: text, images, videos, news")
142
+
143
+ # Validate safesearch
144
+ if safesearch not in ["on", "moderate", "off"]:
145
+ raise ValueError("safesearch must be one of: on, moderate, off")
146
+
147
+ try:
148
+ ddgs = DDGS()
149
+
150
+ # Perform the appropriate search based on search_type
151
+ if search_type == "text":
152
+ results = ddgs.text(
153
+ keywords=query,
154
+ region=region,
155
+ safesearch=safesearch,
156
+ timelimit=timelimit,
157
+ max_results=max_results,
158
+ )
159
+ elif search_type == "images":
160
+ results = ddgs.images(
161
+ keywords=query,
162
+ region=region,
163
+ safesearch=safesearch,
164
+ timelimit=timelimit,
165
+ max_results=max_results,
166
+ )
167
+ elif search_type == "videos":
168
+ results = ddgs.videos(
169
+ keywords=query,
170
+ region=region,
171
+ safesearch=safesearch,
172
+ timelimit=timelimit,
173
+ max_results=max_results,
174
+ )
175
+ elif search_type == "news":
176
+ results = ddgs.news(
177
+ keywords=query,
178
+ region=region,
179
+ safesearch=safesearch,
180
+ timelimit=timelimit,
181
+ max_results=max_results,
182
+ )
183
+
184
+ # Return pretty-printed JSON
185
+ import json
186
+ return json.dumps(results, indent=4, ensure_ascii=False)
187
+
188
+ except Exception as e:
189
+ raise RuntimeError(f"Search failed: {str(e)}")
190
+
191
+
192
+ def main():
193
+ """Demonstrate DuckDuckGoSearchTool functionality."""
194
+ try:
195
+ tool = DuckDuckGoSearchTool()
196
+
197
+ # Test basic search functionality
198
+ print("Testing DuckDuckGoSearchTool with sample query...")
199
+ results = tool.execute(query="Python programming", max_results=3)
200
+ print(results)
201
+
202
+ # Test error handling
203
+ print("\nTesting error handling with invalid query...")
204
+ try:
205
+ tool.execute(query="")
206
+ except ValueError as e:
207
+ print(f"Caught expected ValueError: {e}")
208
+
209
+ except Exception as e:
210
+ print(f"Error in main: {e}")
211
+
212
+
213
+ if __name__ == "__main__":
214
+ main()
@@ -47,50 +47,53 @@ class MarkitdownTool(Tool):
47
47
  Returns:
48
48
  str: The Markdown content or a success message.
49
49
  """
50
- # Handle tilde expansion for local paths
51
- if file_path.startswith("~"):
52
- file_path = os.path.expanduser(file_path)
53
-
54
- # Handle URL paths
55
- if file_path.startswith(("http://", "https://")):
56
- try:
57
- # Create a temporary file
58
- with tempfile.NamedTemporaryFile(delete=False) as temp_file:
59
- temp_path = temp_file.name
60
- # Download the file from URL
61
- download_http_file(file_path, temp_path)
62
- # Use the temporary file path for conversion
63
- file_path = temp_path
64
- is_temp_file = True
65
- except Exception as e:
66
- return f"Error downloading file from URL: {str(e)}"
67
- else:
68
- is_temp_file = False
69
-
70
50
  try:
71
- from markitdown import MarkItDown
51
+ # Handle URL paths first
52
+ if file_path.startswith(("http://", "https://")):
53
+ try:
54
+ with tempfile.NamedTemporaryFile(delete=False) as temp_file:
55
+ temp_path = temp_file.name
56
+ download_http_file(file_path, temp_path)
57
+ file_path = temp_path
58
+ is_temp_file = True
59
+ except Exception as e:
60
+ return f"Error downloading file from URL: {str(e)}"
61
+ else:
62
+ is_temp_file = False
63
+ # Handle local paths
64
+ if file_path.startswith("~"):
65
+ file_path = os.path.expanduser(file_path)
66
+ if not os.path.isabs(file_path):
67
+ file_path = os.path.abspath(file_path)
68
+
69
+ # Verify file exists
70
+ if not os.path.exists(file_path):
71
+ return f"Error: File not found at path: {file_path}"
72
72
 
73
+ from markitdown import MarkItDown
73
74
  md = MarkItDown()
74
75
  result = md.convert(file_path)
75
76
 
76
77
  if output_file_path:
78
+ # Ensure output directory exists
79
+ output_dir = os.path.dirname(output_file_path)
80
+ if output_dir and not os.path.exists(output_dir):
81
+ os.makedirs(output_dir)
82
+
77
83
  with open(output_file_path, "w", encoding="utf-8") as f:
78
84
  f.write(result.text_content)
79
- output_message = f"Markdown content successfully written to {output_file_path}"
80
- else:
81
- # Truncate content if it exceeds MAX_LINES
82
- lines = result.text_content.splitlines()
83
- if len(lines) > MAX_LINES:
84
- truncated_content = "\n".join(lines[:MAX_LINES])
85
- output_message = f"Markdown content truncated to {MAX_LINES} lines:\n{truncated_content}"
86
- else:
87
- output_message = result.text_content
88
-
89
- return output_message
85
+ return f"Markdown content successfully written to {output_file_path}"
86
+
87
+ # Handle content truncation
88
+ lines = result.text_content.splitlines()
89
+ if len(lines) > MAX_LINES:
90
+ truncated_content = "\n".join(lines[:MAX_LINES])
91
+ return f"Markdown content truncated to {MAX_LINES} lines:\n{truncated_content}"
92
+ return result.text_content
93
+
90
94
  except Exception as e:
91
95
  return f"Error converting file to Markdown: {str(e)}"
92
96
  finally:
93
- # Clean up temporary file if it was created
94
97
  if is_temp_file and os.path.exists(file_path):
95
98
  os.remove(file_path)
96
99
 
File without changes