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
quantalogic/agent.py
CHANGED
@@ -1,5 +1,6 @@
|
|
1
1
|
"""Enhanced QuantaLogic agent implementing the ReAct framework."""
|
2
2
|
|
3
|
+
import asyncio
|
3
4
|
from collections.abc import Callable
|
4
5
|
from datetime import datetime
|
5
6
|
from typing import Any
|
@@ -28,6 +29,9 @@ MAX_RESPONSE_LENGTH = 1024 * 32
|
|
28
29
|
DEFAULT_MAX_INPUT_TOKENS = 128 * 1024
|
29
30
|
DEFAULT_MAX_OUTPUT_TOKENS = 4096
|
30
31
|
|
32
|
+
# Maximum recursion depth for variable interpolation
|
33
|
+
MAX_INTERPOLATION_DEPTH = 10
|
34
|
+
|
31
35
|
|
32
36
|
class AgentConfig(BaseModel):
|
33
37
|
"""Configuration settings for the Agent."""
|
@@ -50,7 +54,12 @@ class ObserveResponseResult(BaseModel):
|
|
50
54
|
|
51
55
|
|
52
56
|
class Agent(BaseModel):
|
53
|
-
"""Enhanced QuantaLogic agent implementing ReAct framework.
|
57
|
+
"""Enhanced QuantaLogic agent implementing ReAct framework.
|
58
|
+
|
59
|
+
Supports both synchronous and asynchronous operations for task solving.
|
60
|
+
Use `solve_task` for synchronous contexts (e.g., CLI tools) and `async_solve_task`
|
61
|
+
for asynchronous contexts (e.g., web servers).
|
62
|
+
"""
|
54
63
|
|
55
64
|
model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True, extra="forbid")
|
56
65
|
|
@@ -87,13 +96,29 @@ class Agent(BaseModel):
|
|
87
96
|
get_environment: Callable[[], str] = get_environment,
|
88
97
|
compact_every_n_iterations: int | None = None,
|
89
98
|
max_tokens_working_memory: int | None = None,
|
99
|
+
event_emitter: EventEmitter | None = None,
|
90
100
|
):
|
91
|
-
"""Initialize the agent with model, memory, tools, and configurations.
|
101
|
+
"""Initialize the agent with model, memory, tools, and configurations.
|
102
|
+
|
103
|
+
Args:
|
104
|
+
model_name: Name of the model to use
|
105
|
+
memory: AgentMemory instance for storing conversation history
|
106
|
+
variable_store: VariableMemory instance for storing variables
|
107
|
+
tools: List of Tool instances
|
108
|
+
ask_for_user_validation: Function to ask for user validation
|
109
|
+
task_to_solve: Initial task to solve
|
110
|
+
specific_expertise: Description of the agent's expertise
|
111
|
+
get_environment: Function to get environment details
|
112
|
+
compact_every_n_iterations: How often to compact memory
|
113
|
+
max_tokens_working_memory: Maximum token count for working memory
|
114
|
+
event_emitter: EventEmitter instance for event handling
|
115
|
+
"""
|
92
116
|
try:
|
93
117
|
logger.debug("Initializing agent...")
|
94
118
|
|
95
|
-
# Create event emitter
|
96
|
-
event_emitter
|
119
|
+
# Create or use provided event emitter
|
120
|
+
if event_emitter is None:
|
121
|
+
event_emitter = EventEmitter()
|
97
122
|
|
98
123
|
# Add TaskCompleteTool to the tools list if not already present
|
99
124
|
if not any(isinstance(t, TaskCompleteTool) for t in tools):
|
@@ -155,173 +180,345 @@ class Agent(BaseModel):
|
|
155
180
|
|
156
181
|
@model_name.setter
|
157
182
|
def model_name(self, value: str) -> None:
|
158
|
-
"""Set the model name."""
|
183
|
+
"""Set the model name and update the model instance."""
|
159
184
|
self._model_name = value
|
160
185
|
# Update the model instance with the new name
|
161
186
|
self.model = GenerativeModel(model=value, event_emitter=self.event_emitter)
|
162
187
|
|
163
|
-
def clear_memory(self):
|
188
|
+
def clear_memory(self) -> None:
|
164
189
|
"""Clear the memory and reset the session."""
|
165
190
|
self._reset_session(clear_memory=True)
|
166
191
|
|
167
192
|
def solve_task(
|
168
193
|
self, task: str, max_iterations: int = 30, streaming: bool = False, clear_memory: bool = True
|
169
194
|
) -> str:
|
170
|
-
"""Solve the given task using the ReAct framework.
|
195
|
+
"""Solve the given task using the ReAct framework (synchronous version).
|
196
|
+
|
197
|
+
Ideal for synchronous applications. For asynchronous contexts, use `async_solve_task`.
|
171
198
|
|
172
199
|
Args:
|
173
|
-
task
|
174
|
-
max_iterations
|
175
|
-
|
176
|
-
|
177
|
-
clear_memory (bool, optional): Whether to clear the memory before solving the task.
|
200
|
+
task: The task description
|
201
|
+
max_iterations: Maximum number of iterations
|
202
|
+
streaming: Whether to use streaming mode
|
203
|
+
clear_memory: Whether to clear memory before solving
|
178
204
|
|
179
205
|
Returns:
|
180
|
-
|
206
|
+
The final response after task completion
|
181
207
|
"""
|
182
208
|
logger.debug(f"Solving task... {task}")
|
183
|
-
|
209
|
+
try:
|
210
|
+
loop = asyncio.get_event_loop()
|
211
|
+
except RuntimeError:
|
212
|
+
# Create a new event loop if one doesn't exist
|
213
|
+
loop = asyncio.new_event_loop()
|
214
|
+
asyncio.set_event_loop(loop)
|
184
215
|
|
185
|
-
|
186
|
-
|
216
|
+
return loop.run_until_complete(self.async_solve_task(task, max_iterations, streaming, clear_memory))
|
217
|
+
|
218
|
+
async def async_solve_task(
|
219
|
+
self, task: str, max_iterations: int = 30, streaming: bool = False, clear_memory: bool = True
|
220
|
+
) -> str:
|
221
|
+
"""Solve the given task using the ReAct framework (asynchronous version).
|
222
|
+
|
223
|
+
Ideal for asynchronous applications. For synchronous contexts, use `solve_task`.
|
224
|
+
|
225
|
+
Args:
|
226
|
+
task: The task description
|
227
|
+
max_iterations: Maximum number of iterations
|
228
|
+
streaming: Whether to use streaming mode
|
229
|
+
clear_memory: Whether to clear memory before solving
|
230
|
+
|
231
|
+
Returns:
|
232
|
+
The final response after task completion
|
233
|
+
"""
|
234
|
+
logger.debug(f"Solving task asynchronously... {task}")
|
235
|
+
self._reset_session(task_to_solve=task, max_iterations=max_iterations, clear_memory=clear_memory)
|
236
|
+
self.task_to_solve_summary = await self._async_generate_task_summary(task)
|
187
237
|
|
188
|
-
# Add system prompt to memory
|
189
|
-
# Check if system prompt is already in memory
|
190
|
-
# if not add it
|
191
|
-
# The system message is always the first message in memory
|
192
238
|
if not self.memory.memory or self.memory.memory[0].role != "system":
|
193
239
|
self.memory.add(Message(role="system", content=self.config.system_prompt))
|
194
240
|
|
195
|
-
self._emit_event(
|
196
|
-
"session_start",
|
197
|
-
{"system_prompt": self.config.system_prompt, "content": task},
|
198
|
-
)
|
241
|
+
self._emit_event("session_start", {"system_prompt": self.config.system_prompt, "content": task})
|
199
242
|
|
200
243
|
self.max_output_tokens = self.model.get_model_max_output_tokens() or DEFAULT_MAX_OUTPUT_TOKENS
|
201
244
|
self.max_input_tokens = self.model.get_model_max_input_tokens() or DEFAULT_MAX_INPUT_TOKENS
|
202
245
|
|
203
246
|
done = False
|
204
247
|
current_prompt = self._prepare_prompt_task(task)
|
205
|
-
|
206
248
|
self.current_iteration = 1
|
207
|
-
|
208
|
-
# Emit event: Task Solve Start
|
209
|
-
self._emit_event(
|
210
|
-
"task_solve_start",
|
211
|
-
{"initial_prompt": current_prompt, "task": task},
|
212
|
-
)
|
213
|
-
|
214
|
-
answer: str = ""
|
249
|
+
answer = ""
|
215
250
|
|
216
251
|
while not done:
|
217
252
|
try:
|
218
|
-
self._update_total_tokens(
|
219
|
-
|
220
|
-
# Emit event: Task Think Start after updating total tokens
|
253
|
+
self._update_total_tokens(self.memory.memory, current_prompt)
|
221
254
|
self._emit_event("task_think_start", {"prompt": current_prompt})
|
222
|
-
|
223
|
-
self._compact_memory_if_needed(current_prompt)
|
255
|
+
await self._async_compact_memory_if_needed(current_prompt)
|
224
256
|
|
225
257
|
if streaming:
|
226
|
-
# For streaming, collect the response chunks
|
227
258
|
content = ""
|
228
|
-
|
259
|
+
async_stream = await self.model.async_generate_with_history(
|
229
260
|
messages_history=self.memory.memory,
|
230
261
|
prompt=current_prompt,
|
231
262
|
streaming=True,
|
232
|
-
)
|
263
|
+
)
|
264
|
+
async for chunk in async_stream:
|
233
265
|
content += chunk
|
234
|
-
|
235
|
-
# Create a response object similar to non-streaming mode
|
236
266
|
result = ResponseStats(
|
237
267
|
response=content,
|
238
|
-
usage=TokenUsage(
|
239
|
-
prompt_tokens=0, # We don't have token counts in streaming mode
|
240
|
-
completion_tokens=0,
|
241
|
-
total_tokens=0,
|
242
|
-
),
|
268
|
+
usage=TokenUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
|
243
269
|
model=self.model.model,
|
244
270
|
finish_reason="stop",
|
245
271
|
)
|
246
272
|
else:
|
247
|
-
result = self.model.
|
248
|
-
messages_history=self.memory.memory,
|
249
|
-
|
273
|
+
result = await self.model.async_generate_with_history(
|
274
|
+
messages_history=self.memory.memory,
|
275
|
+
prompt=current_prompt,
|
276
|
+
streaming=False,
|
277
|
+
stop_words=["thinking"],
|
250
278
|
)
|
251
279
|
|
252
280
|
content = result.response
|
253
|
-
if not streaming:
|
281
|
+
if not streaming:
|
254
282
|
token_usage = result.usage
|
255
283
|
self.total_tokens = token_usage.total_tokens
|
256
284
|
|
257
|
-
|
258
|
-
self.
|
259
|
-
"task_think_end",
|
260
|
-
{
|
261
|
-
"response": content,
|
262
|
-
},
|
263
|
-
)
|
264
|
-
|
265
|
-
# Process the assistant's response
|
266
|
-
result = self._observe_response(content, iteration=self.current_iteration)
|
267
|
-
|
285
|
+
self._emit_event("task_think_end", {"response": content})
|
286
|
+
result = await self._async_observe_response(content, iteration=self.current_iteration)
|
268
287
|
current_prompt = result.next_prompt
|
269
288
|
|
270
289
|
if result.executed_tool == "task_complete":
|
271
|
-
self._emit_event(
|
272
|
-
|
273
|
-
{
|
274
|
-
"response": result.answer,
|
275
|
-
},
|
276
|
-
)
|
277
|
-
answer = result.answer
|
290
|
+
self._emit_event("task_complete", {"response": result.answer})
|
291
|
+
answer = result.answer or "" # Ensure answer is never None
|
278
292
|
done = True
|
279
293
|
|
280
294
|
self._update_session_memory(current_prompt, content)
|
281
|
-
|
282
295
|
self.current_iteration += 1
|
283
296
|
if self.current_iteration >= self.max_iterations:
|
284
297
|
done = True
|
285
298
|
self._emit_event("error_max_iterations_reached")
|
286
299
|
|
287
300
|
except Exception as e:
|
288
|
-
logger.error(f"Error during task solving: {str(e)}")
|
289
|
-
# Optionally, decide to continue or break based on exception type
|
301
|
+
logger.error(f"Error during async task solving: {str(e)}")
|
290
302
|
answer = f"Error: {str(e)}"
|
291
303
|
done = True
|
292
304
|
|
293
|
-
# Emit event: Task Solve End
|
294
305
|
self._emit_event("task_solve_end")
|
306
|
+
return answer
|
295
307
|
|
296
|
-
|
308
|
+
def _observe_response(self, content: str, iteration: int = 1) -> ObserveResponseResult:
|
309
|
+
"""Analyze the assistant's response and determine next steps (synchronous wrapper).
|
310
|
+
|
311
|
+
Args:
|
312
|
+
content: The response content to analyze
|
313
|
+
iteration: Current iteration number
|
314
|
+
|
315
|
+
Returns:
|
316
|
+
ObserveResponseResult with next steps information
|
317
|
+
"""
|
318
|
+
try:
|
319
|
+
loop = asyncio.get_event_loop()
|
320
|
+
except RuntimeError:
|
321
|
+
loop = asyncio.new_event_loop()
|
322
|
+
asyncio.set_event_loop(loop)
|
297
323
|
|
298
|
-
return
|
324
|
+
return loop.run_until_complete(self._async_observe_response(content, iteration))
|
299
325
|
|
300
|
-
def
|
301
|
-
"""
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
|
326
|
+
async def _async_observe_response(self, content: str, iteration: int = 1) -> ObserveResponseResult:
|
327
|
+
"""Analyze the assistant's response and determine next steps (asynchronous).
|
328
|
+
|
329
|
+
Args:
|
330
|
+
content: The response content to analyze
|
331
|
+
iteration: Current iteration number
|
332
|
+
|
333
|
+
Returns:
|
334
|
+
ObserveResponseResult with next steps information
|
335
|
+
"""
|
336
|
+
try:
|
337
|
+
parsed_content = self._parse_tool_usage(content)
|
338
|
+
if not parsed_content:
|
339
|
+
return self._handle_no_tool_usage()
|
313
340
|
|
314
|
-
|
315
|
-
|
341
|
+
for tool_name, tool_input in parsed_content.items():
|
342
|
+
tool = self.tools.get(tool_name)
|
343
|
+
if not tool:
|
344
|
+
return self._handle_tool_not_found(tool_name)
|
316
345
|
|
317
|
-
|
318
|
-
|
346
|
+
arguments_with_values = self._parse_tool_arguments(tool, tool_input)
|
347
|
+
is_repeated_call = self._is_repeated_tool_call(tool_name, arguments_with_values)
|
348
|
+
|
349
|
+
if is_repeated_call:
|
350
|
+
executed_tool, response = self._handle_repeated_tool_call(tool_name, arguments_with_values)
|
351
|
+
else:
|
352
|
+
executed_tool, response = await self._async_execute_tool(tool_name, tool, arguments_with_values)
|
353
|
+
|
354
|
+
if not executed_tool:
|
355
|
+
return self._handle_tool_execution_failure(response)
|
356
|
+
|
357
|
+
variable_name = self.variable_store.add(response)
|
358
|
+
new_prompt = self._format_observation_response(response, executed_tool, variable_name, iteration)
|
359
|
+
|
360
|
+
return ObserveResponseResult(
|
361
|
+
next_prompt=new_prompt,
|
362
|
+
executed_tool=executed_tool,
|
363
|
+
answer=response if executed_tool == "task_complete" else None,
|
364
|
+
)
|
365
|
+
except Exception as e:
|
366
|
+
return self._handle_error(e)
|
367
|
+
|
368
|
+
def _execute_tool(self, tool_name: str, tool: Tool, arguments_with_values: dict) -> tuple[str, Any]:
|
369
|
+
"""Execute a tool with validation if required (synchronous wrapper).
|
370
|
+
|
371
|
+
Args:
|
372
|
+
tool_name: Name of the tool to execute
|
373
|
+
tool: Tool instance
|
374
|
+
arguments_with_values: Tool arguments
|
375
|
+
|
376
|
+
Returns:
|
377
|
+
Tuple of (executed_tool_name, response)
|
378
|
+
"""
|
379
|
+
try:
|
380
|
+
loop = asyncio.get_event_loop()
|
381
|
+
except RuntimeError:
|
382
|
+
loop = asyncio.new_event_loop()
|
383
|
+
asyncio.set_event_loop(loop)
|
384
|
+
|
385
|
+
return loop.run_until_complete(self._async_execute_tool(tool_name, tool, arguments_with_values))
|
386
|
+
|
387
|
+
async def _async_execute_tool(self, tool_name: str, tool: Tool, arguments_with_values: dict) -> tuple[str, Any]:
|
388
|
+
"""Execute a tool with validation if required (asynchronous).
|
389
|
+
|
390
|
+
Args:
|
391
|
+
tool_name: Name of the tool to execute
|
392
|
+
tool: Tool instance
|
393
|
+
arguments_with_values: Tool arguments
|
394
|
+
|
395
|
+
Returns:
|
396
|
+
Tuple of (executed_tool_name, response)
|
397
|
+
"""
|
398
|
+
if tool.need_validation:
|
399
|
+
question_validation = (
|
400
|
+
"Do you permit the execution of this tool?\n"
|
401
|
+
f"Tool: {tool_name}\nArguments:\n"
|
402
|
+
"<arguments>\n"
|
403
|
+
+ "\n".join([f" <{key}>{value}</{key}>" for key, value in arguments_with_values.items()])
|
404
|
+
+ "\n</arguments>\nYes or No"
|
405
|
+
)
|
406
|
+
permission_granted = self.ask_for_user_validation(question_validation)
|
407
|
+
if not permission_granted:
|
408
|
+
return "", f"Error: execution of tool '{tool_name}' was denied by the user."
|
409
|
+
|
410
|
+
self._emit_event("tool_execution_start", {"tool_name": tool_name, "arguments": arguments_with_values})
|
411
|
+
|
412
|
+
try:
|
413
|
+
arguments_with_values_interpolated = {
|
414
|
+
key: await self._async_interpolate_variables(value) for key, value in arguments_with_values.items()
|
415
|
+
}
|
416
|
+
if tool.need_variables:
|
417
|
+
arguments_with_values_interpolated["variables"] = self.variable_store
|
418
|
+
if tool.need_caller_context_memory:
|
419
|
+
arguments_with_values_interpolated["caller_context_memory"] = self.memory.memory
|
420
|
+
|
421
|
+
converted_args = self.tools.validate_and_convert_arguments(tool_name, arguments_with_values_interpolated)
|
422
|
+
injectable_properties = tool.get_injectable_properties_in_execution()
|
423
|
+
for key, value in injectable_properties.items():
|
424
|
+
converted_args[key] = value
|
425
|
+
|
426
|
+
if hasattr(tool, "async_execute") and callable(tool.async_execute):
|
427
|
+
response = await tool.async_execute(**converted_args)
|
428
|
+
else:
|
429
|
+
# Fall back to synchronous execution if async is not available
|
430
|
+
response = tool.execute(**converted_args)
|
431
|
+
executed_tool = tool.name
|
432
|
+
except Exception as e:
|
433
|
+
response = f"Error executing tool: {tool_name}: {str(e)}\n"
|
434
|
+
executed_tool = ""
|
435
|
+
|
436
|
+
self._emit_event(
|
437
|
+
"tool_execution_end", {"tool_name": tool_name, "arguments": arguments_with_values, "response": response}
|
438
|
+
)
|
439
|
+
return executed_tool, response
|
440
|
+
|
441
|
+
async def _async_interpolate_variables(self, text: str, depth: int = 0) -> str:
|
442
|
+
"""Interpolate variables using $var$ syntax in the given text with recursion protection.
|
443
|
+
|
444
|
+
Args:
|
445
|
+
text: Text containing variable references
|
446
|
+
depth: Current recursion depth
|
447
|
+
|
448
|
+
Returns:
|
449
|
+
Text with variables interpolated
|
450
|
+
"""
|
451
|
+
if not isinstance(text, str):
|
452
|
+
return str(text)
|
453
|
+
|
454
|
+
if depth > MAX_INTERPOLATION_DEPTH:
|
455
|
+
logger.warning(f"Max interpolation depth ({MAX_INTERPOLATION_DEPTH}) reached, stopping recursion")
|
456
|
+
return text
|
457
|
+
|
458
|
+
try:
|
459
|
+
import re
|
460
|
+
|
461
|
+
# Process each variable in the store
|
462
|
+
for var in self.variable_store.keys():
|
463
|
+
# Properly escape the variable name for regex using re.escape
|
464
|
+
# but handle $ characters separately since they're part of our syntax
|
465
|
+
escaped_var = re.escape(var).replace('\\$', '$')
|
466
|
+
pattern = f"\\${escaped_var}\\$"
|
467
|
+
|
468
|
+
# Get variable value as string
|
469
|
+
replacement = str(self.variable_store[var])
|
470
|
+
|
471
|
+
# Replace all occurrences
|
472
|
+
text = re.sub(pattern, lambda m: replacement, text)
|
473
|
+
|
474
|
+
# Check if there are still variables to interpolate (for nested variables)
|
475
|
+
if '$' in text and depth < MAX_INTERPOLATION_DEPTH:
|
476
|
+
return await self._async_interpolate_variables(text, depth + 1)
|
477
|
+
|
478
|
+
return text
|
479
|
+
except Exception as e:
|
480
|
+
logger.error(f"Error in _async_interpolate_variables: {str(e)}")
|
481
|
+
return text
|
482
|
+
|
483
|
+
def _interpolate_variables(self, text: str) -> str:
|
484
|
+
"""Interpolate variables using $var$ syntax in the given text (synchronous wrapper).
|
485
|
+
|
486
|
+
Args:
|
487
|
+
text: Text containing variable references
|
488
|
+
|
489
|
+
Returns:
|
490
|
+
Text with variables interpolated
|
491
|
+
"""
|
492
|
+
try:
|
493
|
+
loop = asyncio.get_event_loop()
|
494
|
+
except RuntimeError:
|
495
|
+
loop = asyncio.new_event_loop()
|
496
|
+
asyncio.set_event_loop(loop)
|
497
|
+
|
498
|
+
return loop.run_until_complete(self._async_interpolate_variables(text))
|
499
|
+
|
500
|
+
def _compact_memory_if_needed(self, current_prompt: str = "") -> None:
|
501
|
+
"""Compacts the memory if it exceeds the maximum occupancy (synchronous wrapper).
|
502
|
+
|
503
|
+
Args:
|
504
|
+
current_prompt: Current prompt to calculate token usage
|
505
|
+
"""
|
506
|
+
try:
|
507
|
+
loop = asyncio.get_event_loop()
|
508
|
+
except RuntimeError:
|
509
|
+
loop = asyncio.new_event_loop()
|
510
|
+
asyncio.set_event_loop(loop)
|
511
|
+
|
512
|
+
return loop.run_until_complete(self._async_compact_memory_if_needed(current_prompt))
|
513
|
+
|
514
|
+
async def _async_compact_memory_if_needed(self, current_prompt: str = "") -> None:
|
515
|
+
"""Compacts the memory if it exceeds the maximum occupancy or token limit.
|
516
|
+
|
517
|
+
Args:
|
518
|
+
current_prompt: Current prompt to calculate token usage
|
519
|
+
"""
|
319
520
|
ratio_occupied = self._calculate_context_occupancy()
|
320
521
|
|
321
|
-
# Compact memory if any of these conditions are met:
|
322
|
-
# 1. Memory occupancy exceeds MAX_OCCUPANCY, or
|
323
|
-
# 2. Current iteration is a multiple of compact_every_n_iterations, or
|
324
|
-
# 3. Working memory exceeds max_tokens_working_memory (if set)
|
325
522
|
should_compact_by_occupancy = ratio_occupied >= MAX_OCCUPANCY
|
326
523
|
should_compact_by_iteration = (
|
327
524
|
self.compact_every_n_iterations is not None
|
@@ -329,7 +526,8 @@ class Agent(BaseModel):
|
|
329
526
|
and self.current_iteration % self.compact_every_n_iterations == 0
|
330
527
|
)
|
331
528
|
should_compact_by_token_limit = (
|
332
|
-
self.max_tokens_working_memory is not None
|
529
|
+
self.max_tokens_working_memory is not None
|
530
|
+
and self.total_tokens > self.max_tokens_working_memory
|
333
531
|
)
|
334
532
|
|
335
533
|
if should_compact_by_occupancy or should_compact_by_iteration or should_compact_by_token_limit:
|
@@ -341,83 +539,162 @@ class Agent(BaseModel):
|
|
341
539
|
f"Memory compaction triggered: Iteration {self.current_iteration} is a multiple of {self.compact_every_n_iterations}"
|
342
540
|
)
|
343
541
|
|
542
|
+
if should_compact_by_token_limit:
|
543
|
+
logger.debug(
|
544
|
+
f"Memory compaction triggered: Token count {self.total_tokens} exceeds limit {self.max_tokens_working_memory}"
|
545
|
+
)
|
546
|
+
|
344
547
|
self._emit_event("memory_full")
|
345
|
-
self.
|
548
|
+
await self._async_compact_memory()
|
346
549
|
self.total_tokens = self.model.token_counter_with_history(self.memory.memory, current_prompt)
|
347
550
|
self._emit_event("memory_compacted")
|
348
551
|
|
349
|
-
def
|
350
|
-
"""
|
351
|
-
|
552
|
+
async def _async_compact_memory(self) -> None:
|
553
|
+
"""Compact memory asynchronously."""
|
554
|
+
self.memory.compact()
|
352
555
|
|
353
|
-
|
354
|
-
|
556
|
+
async def _async_compact_memory_with_summary(self) -> str:
|
557
|
+
"""Generate a summary and compact memory asynchronously.
|
558
|
+
|
559
|
+
Returns:
|
560
|
+
Generated summary text
|
355
561
|
"""
|
356
|
-
|
357
|
-
|
358
|
-
"
|
359
|
-
"
|
360
|
-
"
|
361
|
-
"
|
362
|
-
"
|
363
|
-
|
562
|
+
prompt_summary = (
|
563
|
+
"Summarize the conversation concisely:\n"
|
564
|
+
"format in markdown:\n"
|
565
|
+
"<thinking>\n"
|
566
|
+
" - 1. **Completed Steps**: Briefly describe the steps.\n"
|
567
|
+
" - 2. **Variables Used**: List the variables.\n"
|
568
|
+
" - 3. **Progress Analysis**: Assess progress.\n"
|
569
|
+
"</thinking>\n"
|
570
|
+
"Keep the summary clear and actionable.\n"
|
571
|
+
)
|
364
572
|
|
365
|
-
|
366
|
-
if data:
|
367
|
-
event_data.update(data)
|
573
|
+
memory_copy = self.memory.memory.copy()
|
368
574
|
|
369
|
-
|
575
|
+
if len(memory_copy) < 3:
|
576
|
+
logger.warning("Not enough messages to compact memory with summary")
|
577
|
+
return "Memory compaction skipped: not enough messages"
|
370
578
|
|
371
|
-
|
372
|
-
|
579
|
+
user_message = memory_copy.pop()
|
580
|
+
assistant_message = memory_copy.pop()
|
581
|
+
summary = await self.model.async_generate_with_history(messages_history=memory_copy, prompt=prompt_summary)
|
582
|
+
|
583
|
+
# Remove last system message if present
|
584
|
+
if memory_copy and memory_copy[-1].role == "system":
|
585
|
+
memory_copy.pop()
|
586
|
+
|
587
|
+
memory_copy.append(Message(role="user", content=summary.response))
|
588
|
+
memory_copy.append(assistant_message)
|
589
|
+
memory_copy.append(user_message)
|
590
|
+
self.memory.memory = memory_copy
|
591
|
+
return summary.response
|
373
592
|
|
593
|
+
def _generate_task_summary(self, content: str) -> str:
|
594
|
+
"""Generate a concise task-focused summary (synchronous wrapper).
|
595
|
+
|
374
596
|
Args:
|
375
|
-
content
|
376
|
-
|
377
|
-
Helps track the progress and prevent infinite loops. Defaults to 1.
|
378
|
-
|
597
|
+
content: The content to summarize
|
598
|
+
|
379
599
|
Returns:
|
380
|
-
|
600
|
+
Generated task summary
|
381
601
|
"""
|
382
602
|
try:
|
383
|
-
|
384
|
-
|
385
|
-
|
603
|
+
loop = asyncio.get_event_loop()
|
604
|
+
except RuntimeError:
|
605
|
+
loop = asyncio.new_event_loop()
|
606
|
+
asyncio.set_event_loop(loop)
|
386
607
|
|
387
|
-
|
388
|
-
tool = self.tools.get(tool_name)
|
389
|
-
if not tool:
|
390
|
-
return self._handle_tool_not_found(tool_name)
|
608
|
+
return loop.run_until_complete(self._async_generate_task_summary(content))
|
391
609
|
|
392
|
-
|
393
|
-
|
610
|
+
async def _async_generate_task_summary(self, content: str) -> str:
|
611
|
+
"""Generate a concise task-focused summary using the generative model.
|
394
612
|
|
395
|
-
|
396
|
-
|
397
|
-
else:
|
398
|
-
executed_tool, response = self._execute_tool(tool_name, tool, arguments_with_values)
|
613
|
+
Args:
|
614
|
+
content: The content to summarize
|
399
615
|
|
400
|
-
|
401
|
-
|
616
|
+
Returns:
|
617
|
+
Generated task summary
|
618
|
+
"""
|
619
|
+
try:
|
620
|
+
if len(content) < 1024 * 4:
|
621
|
+
return content
|
622
|
+
|
623
|
+
prompt = (
|
624
|
+
"Create a task summary that captures ONLY: \n"
|
625
|
+
"1. Primary objective/purpose\n"
|
626
|
+
"2. Core actions/requirements\n"
|
627
|
+
"3. Desired end-state/outcome\n\n"
|
628
|
+
"Guidelines:\n"
|
629
|
+
"- Use imperative voice\n"
|
630
|
+
f"Input Task Description:\n{content}\n\n"
|
631
|
+
)
|
632
|
+
result = await self.model.async_generate(prompt=prompt)
|
633
|
+
logger.debug(f"Generated summary: {result.response}")
|
634
|
+
return result.response.strip() + "\n🚨 The FULL task is in <task> tag in the previous messages.\n"
|
635
|
+
except Exception as e:
|
636
|
+
logger.error(f"Error generating summary: {str(e)}")
|
637
|
+
return f"Summary generation failed: {str(e)}"
|
402
638
|
|
403
|
-
|
404
|
-
|
639
|
+
def _reset_session(self, task_to_solve: str = "", max_iterations: int = 30, clear_memory: bool = True) -> None:
|
640
|
+
"""Reset the agent's session.
|
641
|
+
|
642
|
+
Args:
|
643
|
+
task_to_solve: New task to solve
|
644
|
+
max_iterations: Maximum number of iterations
|
645
|
+
clear_memory: Whether to clear memory
|
646
|
+
"""
|
647
|
+
logger.debug("Resetting session...")
|
648
|
+
self.task_to_solve = task_to_solve
|
649
|
+
if clear_memory:
|
650
|
+
logger.debug("Clearing memory...")
|
651
|
+
self.memory.reset()
|
652
|
+
self.variable_store.reset()
|
653
|
+
self.total_tokens = 0
|
654
|
+
self.current_iteration = 0
|
655
|
+
self.max_output_tokens = self.model.get_model_max_output_tokens() or DEFAULT_MAX_OUTPUT_TOKENS
|
656
|
+
self.max_input_tokens = self.model.get_model_max_input_tokens() or DEFAULT_MAX_INPUT_TOKENS
|
657
|
+
self.max_iterations = max_iterations
|
405
658
|
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
410
|
-
|
659
|
+
def _update_total_tokens(self, message_history: list[Message], prompt: str) -> None:
|
660
|
+
"""Update the total tokens count based on message history and prompt.
|
661
|
+
|
662
|
+
Args:
|
663
|
+
message_history: List of messages
|
664
|
+
prompt: Current prompt
|
665
|
+
"""
|
666
|
+
self.total_tokens = self.model.token_counter_with_history(message_history, prompt)
|
411
667
|
|
412
|
-
|
413
|
-
|
668
|
+
def _emit_event(self, event_type: str, data: dict[str, Any] | None = None) -> None:
|
669
|
+
"""Emit an event with system context and optional additional data.
|
670
|
+
|
671
|
+
Args:
|
672
|
+
event_type: Type of event
|
673
|
+
data: Additional event data
|
674
|
+
"""
|
675
|
+
event_data = {
|
676
|
+
"iteration": self.current_iteration,
|
677
|
+
"total_tokens": self.total_tokens,
|
678
|
+
"context_occupancy": self._calculate_context_occupancy(),
|
679
|
+
"max_input_tokens": self.max_input_tokens,
|
680
|
+
"max_output_tokens": self.max_output_tokens,
|
681
|
+
}
|
682
|
+
if data:
|
683
|
+
event_data.update(data)
|
684
|
+
self.event_emitter.emit(event_type, event_data)
|
414
685
|
|
415
686
|
def _parse_tool_usage(self, content: str) -> dict:
|
416
|
-
"""Extract tool usage from the response content.
|
687
|
+
"""Extract tool usage from the response content.
|
688
|
+
|
689
|
+
Args:
|
690
|
+
content: Response content
|
691
|
+
|
692
|
+
Returns:
|
693
|
+
Dictionary mapping tool names to inputs
|
694
|
+
"""
|
417
695
|
if not content or not isinstance(content, str):
|
418
696
|
return {}
|
419
697
|
|
420
|
-
# Extract action
|
421
698
|
xml_parser = ToleranceXMLParser()
|
422
699
|
action = xml_parser.extract_elements(text=content, element_names=["action"])
|
423
700
|
|
@@ -426,17 +703,31 @@ class Agent(BaseModel):
|
|
426
703
|
if action:
|
427
704
|
return xml_parser.extract_elements(text=action["action"], element_names=tool_names)
|
428
705
|
else:
|
429
|
-
# Fallback to extracting tool usage directly
|
430
706
|
return xml_parser.extract_elements(text=content, element_names=tool_names)
|
431
707
|
|
432
|
-
|
433
|
-
|
434
|
-
|
708
|
+
def _parse_tool_arguments(self, tool: Tool, tool_input: str) -> dict:
|
709
|
+
"""Parse the tool arguments from the tool input.
|
710
|
+
|
711
|
+
Args:
|
712
|
+
tool: Tool instance
|
713
|
+
tool_input: Raw tool input text
|
714
|
+
|
715
|
+
Returns:
|
716
|
+
Dictionary of parsed arguments
|
717
|
+
"""
|
435
718
|
tool_parser = ToolParser(tool=tool)
|
436
719
|
return tool_parser.parse(tool_input)
|
437
720
|
|
438
721
|
def _is_repeated_tool_call(self, tool_name: str, arguments_with_values: dict) -> bool:
|
439
|
-
"""Check if the tool call is repeated.
|
722
|
+
"""Check if the tool call is repeated.
|
723
|
+
|
724
|
+
Args:
|
725
|
+
tool_name: Name of the tool
|
726
|
+
arguments_with_values: Tool arguments
|
727
|
+
|
728
|
+
Returns:
|
729
|
+
True if call is repeated, False otherwise
|
730
|
+
"""
|
440
731
|
current_call = {
|
441
732
|
"tool_name": tool_name,
|
442
733
|
"arguments": arguments_with_values,
|
@@ -455,16 +746,27 @@ class Agent(BaseModel):
|
|
455
746
|
current_call["count"] = 1
|
456
747
|
|
457
748
|
self.last_tool_call = current_call
|
458
|
-
return is_repeated_call and
|
749
|
+
return is_repeated_call and current_call.get("count", 0) >= 2
|
459
750
|
|
460
751
|
def _handle_no_tool_usage(self) -> ObserveResponseResult:
|
461
|
-
"""Handle the case where no tool usage is found in the response.
|
752
|
+
"""Handle the case where no tool usage is found in the response.
|
753
|
+
|
754
|
+
Returns:
|
755
|
+
ObserveResponseResult with error message
|
756
|
+
"""
|
462
757
|
return ObserveResponseResult(
|
463
758
|
next_prompt="Error: No tool usage found in response.", executed_tool=None, answer=None
|
464
759
|
)
|
465
760
|
|
466
761
|
def _handle_tool_not_found(self, tool_name: str) -> ObserveResponseResult:
|
467
|
-
"""Handle the case where the tool is not found.
|
762
|
+
"""Handle the case where the tool is not found.
|
763
|
+
|
764
|
+
Args:
|
765
|
+
tool_name: Name of the tool
|
766
|
+
|
767
|
+
Returns:
|
768
|
+
ObserveResponseResult with error message
|
769
|
+
"""
|
468
770
|
logger.warning(f"Tool '{tool_name}' not found in tool manager.")
|
469
771
|
return ObserveResponseResult(
|
470
772
|
next_prompt=f"Error: Tool '{tool_name}' not found in tool manager.",
|
@@ -472,8 +774,16 @@ class Agent(BaseModel):
|
|
472
774
|
answer=None,
|
473
775
|
)
|
474
776
|
|
475
|
-
def _handle_repeated_tool_call(self, tool_name: str, arguments_with_values: dict) ->
|
476
|
-
"""Handle the case where a tool call is repeated.
|
777
|
+
def _handle_repeated_tool_call(self, tool_name: str, arguments_with_values: dict) -> tuple[str, str]:
|
778
|
+
"""Handle the case where a tool call is repeated.
|
779
|
+
|
780
|
+
Args:
|
781
|
+
tool_name: Name of the tool
|
782
|
+
arguments_with_values: Tool arguments
|
783
|
+
|
784
|
+
Returns:
|
785
|
+
Tuple of (executed_tool_name, error_message)
|
786
|
+
"""
|
477
787
|
repeat_count = self.last_tool_call.get("count", 0)
|
478
788
|
error_message = (
|
479
789
|
"Error: Detected repeated identical tool call pattern.\n"
|
@@ -489,7 +799,14 @@ class Agent(BaseModel):
|
|
489
799
|
return tool_name, error_message
|
490
800
|
|
491
801
|
def _handle_tool_execution_failure(self, response: str) -> ObserveResponseResult:
|
492
|
-
"""Handle the case where tool execution fails.
|
802
|
+
"""Handle the case where tool execution fails.
|
803
|
+
|
804
|
+
Args:
|
805
|
+
response: Error response
|
806
|
+
|
807
|
+
Returns:
|
808
|
+
ObserveResponseResult with error message
|
809
|
+
"""
|
493
810
|
return ObserveResponseResult(
|
494
811
|
next_prompt=response,
|
495
812
|
executed_tool="",
|
@@ -497,7 +814,14 @@ class Agent(BaseModel):
|
|
497
814
|
)
|
498
815
|
|
499
816
|
def _handle_error(self, error: Exception) -> ObserveResponseResult:
|
500
|
-
"""Handle any exceptions that occur during response observation.
|
817
|
+
"""Handle any exceptions that occur during response observation.
|
818
|
+
|
819
|
+
Args:
|
820
|
+
error: Exception that occurred
|
821
|
+
|
822
|
+
Returns:
|
823
|
+
ObserveResponseResult with error message
|
824
|
+
"""
|
501
825
|
logger.error(f"Error in _observe_response: {str(error)}")
|
502
826
|
return ObserveResponseResult(
|
503
827
|
next_prompt=f"An error occurred while processing the response: {str(error)}",
|
@@ -506,9 +830,19 @@ class Agent(BaseModel):
|
|
506
830
|
)
|
507
831
|
|
508
832
|
def _format_observation_response(
|
509
|
-
self, response: str,
|
833
|
+
self, response: str, last_executed_tool: str, variable_name: str, iteration: int
|
510
834
|
) -> str:
|
511
|
-
"""Format the observation response with the given response, variable name, and iteration.
|
835
|
+
"""Format the observation response with the given response, variable name, and iteration.
|
836
|
+
|
837
|
+
Args:
|
838
|
+
response: Tool execution response
|
839
|
+
last_executed_tool: Name of last executed tool
|
840
|
+
variable_name: Name of variable storing response
|
841
|
+
iteration: Current iteration number
|
842
|
+
|
843
|
+
Returns:
|
844
|
+
Formatted observation response
|
845
|
+
"""
|
512
846
|
response_display = response
|
513
847
|
if len(response) > MAX_RESPONSE_LENGTH:
|
514
848
|
response_display = response[:MAX_RESPONSE_LENGTH]
|
@@ -516,8 +850,7 @@ class Agent(BaseModel):
|
|
516
850
|
f"... content was truncated full content available by interpolation in variable {variable_name}"
|
517
851
|
)
|
518
852
|
|
519
|
-
|
520
|
-
formatted_response = formatted_response = (
|
853
|
+
formatted_response = (
|
521
854
|
"# Analysis and Next Action Decision Point\n\n"
|
522
855
|
f"📊 Progress: Iteration {iteration}/{self.max_iterations}\n\n"
|
523
856
|
"## Global Task summary:\n"
|
@@ -535,7 +868,7 @@ class Agent(BaseModel):
|
|
535
868
|
"1. Your analysis of the progression resulting from the execution of the tool in <thinking> tags, don't include <context_analysis/>\n"
|
536
869
|
"2. Your tool execution plan in <tool_name> tags\n\n"
|
537
870
|
"## Last executed action result\n"
|
538
|
-
f"Last executed tool {
|
871
|
+
f"Last executed tool {last_executed_tool} Execution Result:\n"
|
539
872
|
f"\n<{variable_name}>\n{response_display}\n</{variable_name}>\n"
|
540
873
|
"## Response Format\n"
|
541
874
|
"```xml\n"
|
@@ -561,132 +894,14 @@ class Agent(BaseModel):
|
|
561
894
|
|
562
895
|
return formatted_response
|
563
896
|
|
564
|
-
def _execute_tool(self, tool_name: str, tool, arguments_with_values: dict) -> tuple[str, Any]:
|
565
|
-
"""Execute a tool with validation if required.
|
566
|
-
|
567
|
-
Args:
|
568
|
-
tool_name: Name of the tool to execute
|
569
|
-
tool: Tool instance
|
570
|
-
arguments_with_values: Dictionary of argument names and values
|
571
|
-
|
572
|
-
Returns:
|
573
|
-
tuple containing:
|
574
|
-
- executed_tool name (str)
|
575
|
-
- tool execution response (Any)
|
576
|
-
|
577
|
-
Note:
|
578
|
-
Predefined variable properties take precedence over dynamically provided values.
|
579
|
-
This ensures consistent behavior when tools have both predefined properties
|
580
|
-
and runtime-provided arguments. The precedence order is:
|
581
|
-
1. Predefined properties from tool.get_injectable_properties_in_execution()
|
582
|
-
2. Runtime-provided arguments from arguments_with_values
|
583
|
-
"""
|
584
|
-
# Handle tool validation if required
|
585
|
-
if tool.need_validation:
|
586
|
-
logger.debug(f"Tool '{tool_name}' requires validation.")
|
587
|
-
self._emit_event(
|
588
|
-
"tool_execute_validation_start",
|
589
|
-
{"tool_name": tool_name, "arguments": arguments_with_values},
|
590
|
-
)
|
591
|
-
|
592
|
-
question_validation: str = (
|
593
|
-
"Do you permit the execution of this tool?\n"
|
594
|
-
f"Tool: {tool_name}\n"
|
595
|
-
"Arguments:\n"
|
596
|
-
"<arguments>\n"
|
597
|
-
+ "\n".join([f" <{key}>{value}</{key}>" for key, value in arguments_with_values.items()])
|
598
|
-
+ "\n</arguments>\n"
|
599
|
-
"Yes or No"
|
600
|
-
)
|
601
|
-
permission_granted = self.ask_for_user_validation(question_validation)
|
602
|
-
|
603
|
-
self._emit_event(
|
604
|
-
"tool_execute_validation_end",
|
605
|
-
{"tool_name": tool_name, "arguments": arguments_with_values},
|
606
|
-
)
|
607
|
-
|
608
|
-
if not permission_granted:
|
609
|
-
logger.debug(f"Execution of tool '{tool_name}' was denied by the user.")
|
610
|
-
return "", f"Error: execution of tool '{tool_name}' was denied by the user."
|
611
|
-
|
612
|
-
# Emit event: Tool Execution Start
|
613
|
-
self._emit_event(
|
614
|
-
"tool_execution_start",
|
615
|
-
{"tool_name": tool_name, "arguments": arguments_with_values},
|
616
|
-
)
|
617
|
-
|
618
|
-
try:
|
619
|
-
# Execute the tool synchronously
|
620
|
-
arguments_with_values_interpolated = {
|
621
|
-
key: self._interpolate_variables(value) for key, value in arguments_with_values.items()
|
622
|
-
}
|
623
|
-
|
624
|
-
arguments_with_values_interpolated = arguments_with_values_interpolated
|
625
|
-
|
626
|
-
# test if tool need variables in context
|
627
|
-
if tool.need_variables:
|
628
|
-
# Inject variables into the tool if needed
|
629
|
-
arguments_with_values_interpolated["variables"] = self.variable_store
|
630
|
-
if tool.need_caller_context_memory:
|
631
|
-
# Inject caller context into the tool if needed
|
632
|
-
arguments_with_values_interpolated["caller_context_memory"] = self.memory.memory
|
633
|
-
|
634
|
-
try:
|
635
|
-
# Convert arguments to proper types
|
636
|
-
converted_args = self.tools.validate_and_convert_arguments(
|
637
|
-
tool_name, arguments_with_values_interpolated
|
638
|
-
)
|
639
|
-
except ValueError as e:
|
640
|
-
return "", f"Argument Error: {str(e)}"
|
641
|
-
|
642
|
-
# Add injectable variables
|
643
|
-
injectable_properties = tool.get_injectable_properties_in_execution()
|
644
|
-
for key, value in injectable_properties.items():
|
645
|
-
converted_args[key] = value
|
646
|
-
|
647
|
-
# Call tool execute with named arguments
|
648
|
-
response = tool.execute(**converted_args)
|
649
|
-
executed_tool = tool.name
|
650
|
-
except Exception as e:
|
651
|
-
response = f"Error executing tool: {tool_name}: {str(e)}\n"
|
652
|
-
executed_tool = ""
|
653
|
-
|
654
|
-
# Emit event: Tool Execution End
|
655
|
-
self._emit_event(
|
656
|
-
"tool_execution_end",
|
657
|
-
{
|
658
|
-
"tool_name": tool_name,
|
659
|
-
"arguments": arguments_with_values,
|
660
|
-
"response": response,
|
661
|
-
},
|
662
|
-
)
|
663
|
-
|
664
|
-
return executed_tool, response
|
665
|
-
|
666
|
-
def _interpolate_variables(self, text: str) -> str:
|
667
|
-
"""Interpolate variables using $var$ syntax in the given text."""
|
668
|
-
try:
|
669
|
-
import re
|
670
|
-
|
671
|
-
for var in self.variable_store.keys():
|
672
|
-
# Create safe pattern without double-escaping backslashes
|
673
|
-
safe_var = re.sub(r"([\\\.\^\$\*\+\?\{\}\[\]\|\(\)])", r"\\\1", var)
|
674
|
-
pattern = rf"\${safe_var}\$"
|
675
|
-
replacement = self.variable_store[var]
|
676
|
-
text = re.sub(pattern, replacement, text)
|
677
|
-
return text
|
678
|
-
except Exception as e:
|
679
|
-
logger.error(f"Error in _interpolate_variables: {str(e)}")
|
680
|
-
return text
|
681
|
-
|
682
897
|
def _prepare_prompt_task(self, task: str) -> str:
|
683
898
|
"""Prepare the initial prompt for the task.
|
684
899
|
|
685
900
|
Args:
|
686
|
-
task
|
901
|
+
task: The task description
|
687
902
|
|
688
903
|
Returns:
|
689
|
-
|
904
|
+
The formatted task prompt
|
690
905
|
"""
|
691
906
|
prompt_task: str = (
|
692
907
|
"## Your task to solve:\n"
|
@@ -702,7 +917,11 @@ class Agent(BaseModel):
|
|
702
917
|
return prompt_task
|
703
918
|
|
704
919
|
def _get_tools_names_prompt(self) -> str:
|
705
|
-
"""Construct a detailed prompt that lists the available tools for task execution.
|
920
|
+
"""Construct a detailed prompt that lists the available tools for task execution.
|
921
|
+
|
922
|
+
Returns:
|
923
|
+
Formatted tools prompt
|
924
|
+
"""
|
706
925
|
prompt_use_tools: str = (
|
707
926
|
"To accomplish this task, you have access to these tools:\n"
|
708
927
|
"\n"
|
@@ -712,15 +931,22 @@ class Agent(BaseModel):
|
|
712
931
|
"1. Select ONE tool per message\n"
|
713
932
|
"2. You will receive the tool's output in the next user response\n"
|
714
933
|
"3. Choose the most appropriate tool for each step\n"
|
715
|
-
"4.
|
934
|
+
"4. If it's not asked to write on files, don't use write_file tool\n"
|
935
|
+
"5. If files are written, then use tool to display the prepared download link\n"
|
936
|
+
"6. Give the final full answer using all the variables\n"
|
937
|
+
"7. Use task_complete tool to confirm task completion with the full content of the final answer\n"
|
716
938
|
)
|
717
939
|
return prompt_use_tools
|
718
940
|
|
719
941
|
def _get_variable_prompt(self) -> str:
|
720
|
-
"""Construct a prompt that explains how to use variables.
|
942
|
+
"""Construct a prompt that explains how to use variables.
|
943
|
+
|
944
|
+
Returns:
|
945
|
+
Formatted variables prompt
|
946
|
+
"""
|
721
947
|
prompt_use_variables: str = (
|
722
948
|
"To use a variable interpolation, use the format $variable_name$ in function arguments.\n"
|
723
|
-
"Example: <write_file><file_path>/path/to/file.txt</file_path><content>$var1$</write_file>\n"
|
949
|
+
"Example: <write_file><file_path>/path/to/file.txt</file_path><content>$var1$</content></write_file>\n"
|
724
950
|
"\n"
|
725
951
|
"Available variables:\n"
|
726
952
|
"\n"
|
@@ -731,96 +957,110 @@ class Agent(BaseModel):
|
|
731
957
|
return prompt_use_variables
|
732
958
|
|
733
959
|
def _calculate_context_occupancy(self) -> float:
|
734
|
-
"""Calculate the number of tokens in percentages for prompt and completion.
|
960
|
+
"""Calculate the number of tokens in percentages for prompt and completion.
|
961
|
+
|
962
|
+
Returns:
|
963
|
+
Percentage of context window occupied
|
964
|
+
"""
|
735
965
|
total_tokens = self.total_tokens
|
736
|
-
# Calculate token usage of prompt
|
737
966
|
max_tokens = self.model.get_model_max_input_tokens()
|
738
967
|
|
739
|
-
# Handle None value and prevent division by zero
|
740
968
|
if max_tokens is None or max_tokens <= 0:
|
741
969
|
logger.warning(f"Invalid max tokens value: {max_tokens}. Using default of {DEFAULT_MAX_INPUT_TOKENS}.")
|
742
970
|
max_tokens = DEFAULT_MAX_INPUT_TOKENS
|
743
971
|
|
744
972
|
return round((total_tokens / max_tokens) * 100, 2)
|
745
973
|
|
746
|
-
def _compact_memory_with_summary(self) -> str:
|
747
|
-
prompt_summary = (
|
748
|
-
"Summarize the conversation concisely:\n"
|
749
|
-
"format in markdown:\n"
|
750
|
-
"<thinking>\n"
|
751
|
-
" - 1. **Completed Steps**: Briefly describe the steps.\n"
|
752
|
-
" - 2. **Variables Used**: List the variables.\n"
|
753
|
-
" - 3. **Progress Analysis**: Assess progress.\n"
|
754
|
-
"</thinking>\n"
|
755
|
-
"Keep the summary clear and actionable.\n"
|
756
|
-
)
|
757
|
-
|
758
|
-
# Get all message system, except the last assistant / user message
|
759
|
-
memory_copy = self.memory.memory.copy()
|
760
|
-
|
761
|
-
# Remove the last assistant / user message
|
762
|
-
user_message = memory_copy.pop()
|
763
|
-
assistant_message = memory_copy.pop()
|
764
|
-
summary = self.model.generate_with_history(messages_history=memory_copy, prompt=prompt_summary)
|
765
|
-
# Remove user message
|
766
|
-
memory_copy.pop()
|
767
|
-
# Replace by summary
|
768
|
-
memory_copy.append(Message(role="user", content=summary.response))
|
769
|
-
memory_copy.append(assistant_message)
|
770
|
-
memory_copy.append(user_message)
|
771
|
-
self.memory.memory = memory_copy
|
772
|
-
return summary.response
|
773
|
-
|
774
|
-
def _generate_task_summary(self, content: str) -> str:
|
775
|
-
"""Generate a concise task-focused summary using the generative model.
|
776
|
-
|
777
|
-
Args:
|
778
|
-
content (str): The content to summarize
|
779
|
-
|
780
|
-
Returns:
|
781
|
-
str: Generated task summary
|
782
|
-
"""
|
783
|
-
try:
|
784
|
-
if len(content) < 1024*4:
|
785
|
-
return content
|
786
|
-
prompt = (
|
787
|
-
"Create a task summary that captures ONLY: \n"
|
788
|
-
"1. Primary objective/purpose\n"
|
789
|
-
"2. Core actions/requirements\n"
|
790
|
-
"3. Desired end-state/outcome\n\n"
|
791
|
-
"Guidelines:\n"
|
792
|
-
"- Use imperative voice\n"
|
793
|
-
f"Input Task Description:\n{content}\n\n"
|
794
|
-
)
|
795
|
-
result = self.model.generate(prompt=prompt)
|
796
|
-
logger.debug(f"Generated summary: {result.response}")
|
797
|
-
return result.response.strip() + "\n🚨 The FULL task is in <task> tag in the previous messages.\n"
|
798
|
-
except Exception as e:
|
799
|
-
logger.error(f"Error generating summary: {str(e)}")
|
800
|
-
return f"Summary generation failed: {str(e)}"
|
801
|
-
|
802
974
|
def _update_session_memory(self, user_content: str, assistant_content: str) -> None:
|
803
|
-
"""
|
804
|
-
Log session messages to memory and emit events.
|
975
|
+
"""Log session messages to memory and emit events.
|
805
976
|
|
806
977
|
Args:
|
807
|
-
user_content
|
808
|
-
assistant_content
|
978
|
+
user_content: The user's content
|
979
|
+
assistant_content: The assistant's content
|
809
980
|
"""
|
810
981
|
self.memory.add(Message(role="user", content=user_content))
|
811
|
-
self._emit_event(
|
812
|
-
"session_add_message",
|
813
|
-
{"role": "user", "content": user_content},
|
814
|
-
)
|
982
|
+
self._emit_event("session_add_message", {"role": "user", "content": user_content})
|
815
983
|
|
816
984
|
self.memory.add(Message(role="assistant", content=assistant_content))
|
817
|
-
|
818
|
-
self._emit_event(
|
819
|
-
"session_add_message",
|
820
|
-
{"role": "assistant", "content": assistant_content},
|
821
|
-
)
|
985
|
+
self._emit_event("session_add_message", {"role": "assistant", "content": assistant_content})
|
822
986
|
|
823
987
|
def update_model(self, new_model_name: str) -> None:
|
824
|
-
"""Update the model name and recreate the model instance.
|
988
|
+
"""Update the model name and recreate the model instance.
|
989
|
+
|
990
|
+
Args:
|
991
|
+
new_model_name: New model name to use
|
992
|
+
"""
|
825
993
|
self.model_name = new_model_name
|
826
994
|
self.model = GenerativeModel(model=new_model_name, event_emitter=self.event_emitter)
|
995
|
+
|
996
|
+
def add_tool(self, tool: Tool) -> None:
|
997
|
+
"""Add a new tool to the agent's tool manager.
|
998
|
+
|
999
|
+
Args:
|
1000
|
+
tool: The tool instance to add
|
1001
|
+
|
1002
|
+
Raises:
|
1003
|
+
ValueError: If a tool with the same name already exists
|
1004
|
+
"""
|
1005
|
+
if tool.name in self.tools.tool_names():
|
1006
|
+
raise ValueError(f"Tool with name '{tool.name}' already exists")
|
1007
|
+
|
1008
|
+
self.tools.add(tool)
|
1009
|
+
# Update tools markdown in config
|
1010
|
+
self.config = AgentConfig(
|
1011
|
+
environment_details=self.config.environment_details,
|
1012
|
+
tools_markdown=self.tools.to_markdown(),
|
1013
|
+
system_prompt=self.config.system_prompt,
|
1014
|
+
)
|
1015
|
+
logger.debug(f"Added tool: {tool.name}")
|
1016
|
+
|
1017
|
+
def remove_tool(self, tool_name: str) -> None:
|
1018
|
+
"""Remove a tool from the agent's tool manager.
|
1019
|
+
|
1020
|
+
Args:
|
1021
|
+
tool_name: Name of the tool to remove
|
1022
|
+
|
1023
|
+
Raises:
|
1024
|
+
ValueError: If tool doesn't exist or is TaskCompleteTool
|
1025
|
+
"""
|
1026
|
+
if tool_name not in self.tools.tool_names():
|
1027
|
+
raise ValueError(f"Tool '{tool_name}' does not exist")
|
1028
|
+
|
1029
|
+
tool = self.tools.get(tool_name)
|
1030
|
+
if isinstance(tool, TaskCompleteTool):
|
1031
|
+
raise ValueError("Cannot remove TaskCompleteTool as it is required")
|
1032
|
+
|
1033
|
+
self.tools.remove(tool_name)
|
1034
|
+
# Update tools markdown in config
|
1035
|
+
self.config = AgentConfig(
|
1036
|
+
environment_details=self.config.environment_details,
|
1037
|
+
tools_markdown=self.tools.to_markdown(),
|
1038
|
+
system_prompt=self.config.system_prompt,
|
1039
|
+
)
|
1040
|
+
logger.debug(f"Removed tool: {tool_name}")
|
1041
|
+
|
1042
|
+
def set_tools(self, tools: list[Tool]) -> None:
|
1043
|
+
"""Set/replace all tools for the agent.
|
1044
|
+
|
1045
|
+
Args:
|
1046
|
+
tools: List of tool instances to set
|
1047
|
+
|
1048
|
+
Note:
|
1049
|
+
TaskCompleteTool will be automatically added if not present
|
1050
|
+
"""
|
1051
|
+
# Ensure TaskCompleteTool is present
|
1052
|
+
if not any(isinstance(t, TaskCompleteTool) for t in tools):
|
1053
|
+
tools.append(TaskCompleteTool())
|
1054
|
+
|
1055
|
+
# Create new tool manager and add tools
|
1056
|
+
tool_manager = ToolManager()
|
1057
|
+
tool_manager.add_list(tools)
|
1058
|
+
self.tools = tool_manager
|
1059
|
+
|
1060
|
+
# Update config with new tools markdown
|
1061
|
+
self.config = AgentConfig(
|
1062
|
+
environment_details=self.config.environment_details,
|
1063
|
+
tools_markdown=self.tools.to_markdown(),
|
1064
|
+
system_prompt=self.config.system_prompt,
|
1065
|
+
)
|
1066
|
+
logger.debug(f"Set {len(tools)} tools")
|