quantalogic 0.57.0__py3-none-any.whl → 0.59.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/agent.py +566 -125
- quantalogic/agent_factory.py +44 -40
- quantalogic/config.py +8 -7
- quantalogic/create_custom_agent.py +146 -71
- quantalogic/event_emitter.py +2 -2
- quantalogic/main.py +81 -5
- quantalogic/prompts/chat_system_prompt.j2 +54 -0
- quantalogic/server/agent_server.py +19 -4
- quantalogic/task_runner.py +335 -178
- quantalogic/tools/google_packages/google_news_tool.py +26 -187
- quantalogic/tools/utilities/__init__.py +2 -0
- quantalogic/tools/utilities/download_file_tool.py +4 -2
- quantalogic/tools/utilities/vscode_tool.py +123 -0
- quantalogic/utils/ask_user_validation.py +26 -6
- {quantalogic-0.57.0.dist-info → quantalogic-0.59.0.dist-info}/METADATA +124 -30
- {quantalogic-0.57.0.dist-info → quantalogic-0.59.0.dist-info}/RECORD +19 -17
- {quantalogic-0.57.0.dist-info → quantalogic-0.59.0.dist-info}/LICENSE +0 -0
- {quantalogic-0.57.0.dist-info → quantalogic-0.59.0.dist-info}/WHEEL +0 -0
- {quantalogic-0.57.0.dist-info → quantalogic-0.59.0.dist-info}/entry_points.txt +0 -0
quantalogic/agent.py
CHANGED
@@ -1,11 +1,12 @@
|
|
1
|
-
"""Enhanced QuantaLogic agent implementing the ReAct framework."""
|
1
|
+
"""Enhanced QuantaLogic agent implementing the ReAct framework with optional chat mode."""
|
2
2
|
|
3
3
|
import asyncio
|
4
4
|
import os
|
5
|
-
|
5
|
+
import uuid
|
6
|
+
from collections.abc import Awaitable, Callable
|
6
7
|
from datetime import datetime
|
7
8
|
from pathlib import Path
|
8
|
-
from typing import Any
|
9
|
+
from typing import Any, Optional
|
9
10
|
|
10
11
|
from jinja2 import Environment, FileSystemLoader
|
11
12
|
from loguru import logger
|
@@ -57,25 +58,27 @@ class ObserveResponseResult(BaseModel):
|
|
57
58
|
|
58
59
|
|
59
60
|
class Agent(BaseModel):
|
60
|
-
"""Enhanced QuantaLogic agent
|
61
|
+
"""Enhanced QuantaLogic agent supporting both ReAct goal-solving and conversational chat modes.
|
61
62
|
|
62
|
-
|
63
|
-
Use `
|
64
|
-
|
63
|
+
Use `solve_task`/`async_solve_task` for goal-oriented ReAct mode (backward compatible).
|
64
|
+
Use `chat`/`async_chat` for conversational mode with a customizable persona.
|
65
|
+
|
66
|
+
Supports both synchronous and asynchronous operations. Use synchronous methods for CLI tools
|
67
|
+
and asynchronous methods for web servers or async contexts.
|
65
68
|
"""
|
66
69
|
|
67
70
|
model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True, extra="forbid")
|
68
71
|
|
69
72
|
specific_expertise: str
|
70
73
|
model: GenerativeModel
|
71
|
-
memory: AgentMemory = AgentMemory() #
|
72
|
-
variable_store: VariableMemory = VariableMemory() #
|
74
|
+
memory: AgentMemory = AgentMemory() # List of User/Assistant Messages
|
75
|
+
variable_store: VariableMemory = VariableMemory() # Dictionary of variables
|
73
76
|
tools: ToolManager = ToolManager()
|
74
77
|
event_emitter: EventEmitter = EventEmitter()
|
75
78
|
config: AgentConfig
|
76
79
|
task_to_solve: str
|
77
80
|
task_to_solve_summary: str = ""
|
78
|
-
ask_for_user_validation: Callable[[str], bool] = console_ask_for_user_validation
|
81
|
+
ask_for_user_validation: Callable[[str, str], Awaitable[bool]] = console_ask_for_user_validation
|
79
82
|
last_tool_call: dict[str, Any] = {} # Stores the last tool call information
|
80
83
|
total_tokens: int = 0 # Total tokens in the conversation
|
81
84
|
current_iteration: int = 0
|
@@ -86,6 +89,8 @@ class Agent(BaseModel):
|
|
86
89
|
compact_every_n_iterations: int | None = None
|
87
90
|
max_tokens_working_memory: int | None = None
|
88
91
|
_model_name: str = PrivateAttr(default="")
|
92
|
+
chat_system_prompt: str # Base persona prompt for chat mode
|
93
|
+
tool_mode: Optional[str] = None # Tool or toolset to prioritize in chat mode
|
89
94
|
|
90
95
|
def __init__(
|
91
96
|
self,
|
@@ -93,37 +98,39 @@ class Agent(BaseModel):
|
|
93
98
|
memory: AgentMemory = AgentMemory(),
|
94
99
|
variable_store: VariableMemory = VariableMemory(),
|
95
100
|
tools: list[Tool] = [TaskCompleteTool()],
|
96
|
-
ask_for_user_validation: Callable[[str], bool] = console_ask_for_user_validation,
|
101
|
+
ask_for_user_validation: Callable[[str, str], Awaitable[bool]] = console_ask_for_user_validation,
|
97
102
|
task_to_solve: str = "",
|
98
103
|
specific_expertise: str = "General AI assistant with coding and problem-solving capabilities",
|
99
104
|
get_environment: Callable[[], str] = get_environment,
|
100
105
|
compact_every_n_iterations: int | None = None,
|
101
106
|
max_tokens_working_memory: int | None = None,
|
102
107
|
event_emitter: EventEmitter | None = None,
|
108
|
+
chat_system_prompt: str | None = None,
|
109
|
+
tool_mode: Optional[str] = None,
|
103
110
|
):
|
104
111
|
"""Initialize the agent with model, memory, tools, and configurations.
|
105
|
-
|
112
|
+
|
106
113
|
Args:
|
107
114
|
model_name: Name of the model to use
|
108
115
|
memory: AgentMemory instance for storing conversation history
|
109
116
|
variable_store: VariableMemory instance for storing variables
|
110
|
-
tools: List of Tool instances
|
117
|
+
tools: List of Tool instances
|
111
118
|
ask_for_user_validation: Function to ask for user validation
|
112
|
-
task_to_solve: Initial task to solve
|
119
|
+
task_to_solve: Initial task to solve (for ReAct mode)
|
113
120
|
specific_expertise: Description of the agent's expertise
|
114
121
|
get_environment: Function to get environment details
|
115
122
|
compact_every_n_iterations: How often to compact memory
|
116
123
|
max_tokens_working_memory: Maximum token count for working memory
|
117
124
|
event_emitter: EventEmitter instance for event handling
|
125
|
+
chat_system_prompt: Optional base system prompt for chat mode persona
|
126
|
+
tool_mode: Optional tool or toolset to prioritize in chat mode
|
118
127
|
"""
|
119
128
|
try:
|
120
129
|
logger.debug("Initializing agent...")
|
121
130
|
|
122
|
-
# Create or use provided event emitter
|
123
131
|
if event_emitter is None:
|
124
132
|
event_emitter = EventEmitter()
|
125
133
|
|
126
|
-
# Add TaskCompleteTool to the tools list if not already present
|
127
134
|
if not any(isinstance(t, TaskCompleteTool) for t in tools):
|
128
135
|
tools.append(TaskCompleteTool())
|
129
136
|
|
@@ -144,7 +151,11 @@ class Agent(BaseModel):
|
|
144
151
|
system_prompt=system_prompt_text,
|
145
152
|
)
|
146
153
|
|
147
|
-
|
154
|
+
chat_system_prompt = chat_system_prompt or (
|
155
|
+
"You are a friendly, helpful AI assistant. Engage in natural conversation, "
|
156
|
+
"answer questions, and use tools when explicitly requested or when they enhance your response."
|
157
|
+
)
|
158
|
+
|
148
159
|
super().__init__(
|
149
160
|
specific_expertise=specific_expertise,
|
150
161
|
model=GenerativeModel(model=model_name, event_emitter=event_emitter),
|
@@ -165,12 +176,15 @@ class Agent(BaseModel):
|
|
165
176
|
system_prompt="",
|
166
177
|
compact_every_n_iterations=compact_every_n_iterations or 30,
|
167
178
|
max_tokens_working_memory=max_tokens_working_memory,
|
179
|
+
chat_system_prompt=chat_system_prompt,
|
180
|
+
tool_mode=tool_mode,
|
168
181
|
)
|
169
182
|
|
170
183
|
self._model_name = model_name
|
171
184
|
|
172
185
|
logger.debug(f"Memory will be compacted every {self.compact_every_n_iterations} iterations")
|
173
186
|
logger.debug(f"Max tokens for working memory set to: {self.max_tokens_working_memory}")
|
187
|
+
logger.debug(f"Tool mode set to: {self.tool_mode}")
|
174
188
|
logger.debug("Agent initialized successfully.")
|
175
189
|
except Exception as e:
|
176
190
|
logger.error(f"Failed to initialize agent: {str(e)}")
|
@@ -185,7 +199,6 @@ class Agent(BaseModel):
|
|
185
199
|
def model_name(self, value: str) -> None:
|
186
200
|
"""Set the model name and update the model instance."""
|
187
201
|
self._model_name = value
|
188
|
-
# Update the model instance with the new name
|
189
202
|
self.model = GenerativeModel(model=value, event_emitter=self.event_emitter)
|
190
203
|
|
191
204
|
def clear_memory(self) -> None:
|
@@ -212,7 +225,6 @@ class Agent(BaseModel):
|
|
212
225
|
try:
|
213
226
|
loop = asyncio.get_event_loop()
|
214
227
|
except RuntimeError:
|
215
|
-
# Create a new event loop if one doesn't exist
|
216
228
|
loop = asyncio.new_event_loop()
|
217
229
|
asyncio.set_event_loop(loop)
|
218
230
|
|
@@ -277,7 +289,6 @@ class Agent(BaseModel):
|
|
277
289
|
messages_history=self.memory.memory,
|
278
290
|
prompt=current_prompt,
|
279
291
|
streaming=False,
|
280
|
-
# Removed stop_words parameter to allow complete responses
|
281
292
|
)
|
282
293
|
|
283
294
|
content = result.response
|
@@ -291,7 +302,7 @@ class Agent(BaseModel):
|
|
291
302
|
|
292
303
|
if result.executed_tool == "task_complete":
|
293
304
|
self._emit_event("task_complete", {"response": result.answer})
|
294
|
-
answer = result.answer or ""
|
305
|
+
answer = result.answer or ""
|
295
306
|
done = True
|
296
307
|
|
297
308
|
self._update_session_memory(current_prompt, content)
|
@@ -308,13 +319,166 @@ class Agent(BaseModel):
|
|
308
319
|
self._emit_event("task_solve_end")
|
309
320
|
return answer
|
310
321
|
|
322
|
+
def chat(
|
323
|
+
self,
|
324
|
+
message: str,
|
325
|
+
streaming: bool = False,
|
326
|
+
clear_memory: bool = False,
|
327
|
+
auto_tool_call: bool = True,
|
328
|
+
) -> str:
|
329
|
+
"""Engage in a conversational chat with the user (synchronous version).
|
330
|
+
|
331
|
+
Ideal for synchronous applications. For asynchronous contexts, use `async_chat`.
|
332
|
+
|
333
|
+
Args:
|
334
|
+
message: The user's input message
|
335
|
+
streaming: Whether to stream the response
|
336
|
+
clear_memory: Whether to clear memory before starting
|
337
|
+
auto_tool_call: Whether to automatically execute detected tool calls and interpret results
|
338
|
+
|
339
|
+
Returns:
|
340
|
+
The assistant's response
|
341
|
+
"""
|
342
|
+
logger.debug(f"Chatting synchronously with message: {message}, auto_tool_call: {auto_tool_call}")
|
343
|
+
try:
|
344
|
+
loop = asyncio.get_event_loop()
|
345
|
+
except RuntimeError:
|
346
|
+
loop = asyncio.new_event_loop()
|
347
|
+
asyncio.set_event_loop(loop)
|
348
|
+
|
349
|
+
return loop.run_until_complete(self.async_chat(message, streaming, clear_memory, auto_tool_call))
|
350
|
+
|
351
|
+
async def async_chat(
|
352
|
+
self,
|
353
|
+
message: str,
|
354
|
+
streaming: bool = False,
|
355
|
+
clear_memory: bool = False,
|
356
|
+
auto_tool_call: bool = True,
|
357
|
+
) -> str:
|
358
|
+
"""Engage in a conversational chat with the user (asynchronous version).
|
359
|
+
|
360
|
+
Ideal for asynchronous applications. For synchronous contexts, use `chat`.
|
361
|
+
|
362
|
+
Args:
|
363
|
+
message: The user's input message
|
364
|
+
streaming: Whether to stream the response
|
365
|
+
clear_memory: Whether to clear memory before starting
|
366
|
+
auto_tool_call: Whether to automatically execute detected tool calls and interpret results
|
367
|
+
|
368
|
+
Returns:
|
369
|
+
The assistant's response
|
370
|
+
"""
|
371
|
+
logger.debug(f"Chatting asynchronously with message: {message}, auto_tool_call: {auto_tool_call}")
|
372
|
+
if clear_memory:
|
373
|
+
self.clear_memory()
|
374
|
+
|
375
|
+
# Prepare chat system prompt with tool information
|
376
|
+
tools_prompt = self._get_tools_names_prompt()
|
377
|
+
if self.tool_mode:
|
378
|
+
tools_prompt += f"\nPrioritized tool mode: {self.tool_mode}. Prefer tools related to {self.tool_mode} when applicable."
|
379
|
+
|
380
|
+
full_chat_prompt = self._render_template(
|
381
|
+
'chat_system_prompt.j2',
|
382
|
+
persona=self.chat_system_prompt,
|
383
|
+
tools_prompt=tools_prompt
|
384
|
+
)
|
385
|
+
|
386
|
+
if not self.memory.memory or self.memory.memory[0].role != "system":
|
387
|
+
self.memory.add(Message(role="system", content=full_chat_prompt))
|
388
|
+
|
389
|
+
self._emit_event("chat_start", {"message": message})
|
390
|
+
|
391
|
+
# Add user message to memory
|
392
|
+
self.memory.add(Message(role="user", content=message))
|
393
|
+
self._update_total_tokens(self.memory.memory, "")
|
394
|
+
|
395
|
+
# Iterative tool usage with auto-execution
|
396
|
+
current_prompt = message
|
397
|
+
response_content = ""
|
398
|
+
max_tool_iterations = 5 # Prevent infinite tool loops
|
399
|
+
tool_iteration = 0
|
400
|
+
|
401
|
+
while tool_iteration < max_tool_iterations:
|
402
|
+
try:
|
403
|
+
if streaming:
|
404
|
+
content = ""
|
405
|
+
# When streaming is enabled, the GenerativeModel._async_stream_response method
|
406
|
+
# already emits the stream_chunk events, so we don't need to emit them again here
|
407
|
+
async_stream = await self.model.async_generate_with_history(
|
408
|
+
messages_history=self.memory.memory,
|
409
|
+
prompt=current_prompt,
|
410
|
+
streaming=True,
|
411
|
+
)
|
412
|
+
# Just collect the chunks without re-emitting events
|
413
|
+
async for chunk in async_stream:
|
414
|
+
content += chunk
|
415
|
+
response = ResponseStats(
|
416
|
+
response=content,
|
417
|
+
usage=TokenUsage(prompt_tokens=0, completion_tokens=0, total_tokens=0),
|
418
|
+
model=self.model.model,
|
419
|
+
finish_reason="stop",
|
420
|
+
)
|
421
|
+
else:
|
422
|
+
response = await self.model.async_generate_with_history(
|
423
|
+
messages_history=self.memory.memory,
|
424
|
+
prompt=current_prompt,
|
425
|
+
streaming=False,
|
426
|
+
)
|
427
|
+
content = response.response
|
428
|
+
|
429
|
+
self.total_tokens = response.usage.total_tokens if not streaming else self.total_tokens
|
430
|
+
|
431
|
+
# Observe response for tool calls
|
432
|
+
observation = await self._async_observe_response(content)
|
433
|
+
if observation.executed_tool and auto_tool_call:
|
434
|
+
# Tool was executed; process result and continue
|
435
|
+
current_prompt = observation.next_prompt
|
436
|
+
|
437
|
+
# In chat mode, format the response with clear tool call visualization
|
438
|
+
if not self.task_to_solve.strip(): # We're in chat mode
|
439
|
+
# Format the response to clearly show the tool call and result
|
440
|
+
# Use a format that task_runner.py can parse and display nicely
|
441
|
+
|
442
|
+
# For a cleaner look, insert a special delimiter that task_runner.py can recognize
|
443
|
+
# to separate tool call from result
|
444
|
+
response_content = f"{content}\n\n__TOOL_RESULT_SEPARATOR__{observation.executed_tool}__\n{observation.next_prompt}"
|
445
|
+
else:
|
446
|
+
# In task mode, keep the original behavior
|
447
|
+
response_content = observation.next_prompt
|
448
|
+
|
449
|
+
tool_iteration += 1
|
450
|
+
self.memory.add(Message(role="assistant", content=content)) # Original tool call
|
451
|
+
self.memory.add(Message(role="user", content=observation.next_prompt)) # Tool result
|
452
|
+
logger.debug(f"Tool executed: {observation.executed_tool}, iteration: {tool_iteration}")
|
453
|
+
elif not observation.executed_tool and "<action>" in content and auto_tool_call:
|
454
|
+
# Detected malformed tool call attempt; provide feedback and exit loop
|
455
|
+
response_content = (
|
456
|
+
f"{content}\n\n⚠️ Error: Invalid tool call format detected. "
|
457
|
+
"Please use the exact XML structure as specified in the system prompt:\n"
|
458
|
+
"```xml\n<action>\n<tool_name>\n <parameter_name>value</parameter_name>\n</tool_name>\n</action>\n```"
|
459
|
+
)
|
460
|
+
break
|
461
|
+
else:
|
462
|
+
# No tool executed or auto_tool_call is False; final response
|
463
|
+
response_content = content
|
464
|
+
break
|
465
|
+
|
466
|
+
except Exception as e:
|
467
|
+
logger.error(f"Error during async chat: {str(e)}")
|
468
|
+
response_content = f"Error: {str(e)}"
|
469
|
+
break
|
470
|
+
|
471
|
+
self._update_session_memory(message, response_content)
|
472
|
+
self._emit_event("chat_response", {"response": response_content})
|
473
|
+
return response_content
|
474
|
+
|
311
475
|
def _observe_response(self, content: str, iteration: int = 1) -> ObserveResponseResult:
|
312
476
|
"""Analyze the assistant's response and determine next steps (synchronous wrapper).
|
313
|
-
|
477
|
+
|
314
478
|
Args:
|
315
479
|
content: The response content to analyze
|
316
480
|
iteration: Current iteration number
|
317
|
-
|
481
|
+
|
318
482
|
Returns:
|
319
483
|
ObserveResponseResult with next steps information
|
320
484
|
"""
|
@@ -328,20 +492,34 @@ class Agent(BaseModel):
|
|
328
492
|
|
329
493
|
async def _async_observe_response(self, content: str, iteration: int = 1) -> ObserveResponseResult:
|
330
494
|
"""Analyze the assistant's response and determine next steps (asynchronous).
|
331
|
-
|
495
|
+
|
332
496
|
Args:
|
333
497
|
content: The response content to analyze
|
334
498
|
iteration: Current iteration number
|
335
|
-
|
499
|
+
|
336
500
|
Returns:
|
337
501
|
ObserveResponseResult with next steps information
|
338
502
|
"""
|
339
503
|
try:
|
504
|
+
# Detect if we're in chat mode by checking if task_to_solve is empty
|
505
|
+
is_chat_mode = not self.task_to_solve.strip()
|
506
|
+
|
507
|
+
# Use specialized chat mode observation method if in chat mode
|
508
|
+
if is_chat_mode:
|
509
|
+
return await self._async_observe_response_chat(content, iteration)
|
510
|
+
|
511
|
+
# Parse content for tool usage
|
340
512
|
parsed_content = self._parse_tool_usage(content)
|
341
513
|
if not parsed_content:
|
342
|
-
|
343
|
-
|
344
|
-
|
514
|
+
logger.debug("No tool usage detected in response")
|
515
|
+
return ObserveResponseResult(next_prompt=content, executed_tool=None, answer=None)
|
516
|
+
|
517
|
+
# Process tools for regular ReAct mode
|
518
|
+
tool_names = list(parsed_content.keys())
|
519
|
+
for tool_name in tool_names:
|
520
|
+
if tool_name not in parsed_content:
|
521
|
+
continue
|
522
|
+
tool_input = parsed_content[tool_name]
|
345
523
|
tool = self.tools.get(tool_name)
|
346
524
|
if not tool:
|
347
525
|
return self._handle_tool_not_found(tool_name)
|
@@ -360,22 +538,28 @@ class Agent(BaseModel):
|
|
360
538
|
variable_name = self.variable_store.add(response)
|
361
539
|
new_prompt = self._format_observation_response(response, executed_tool, variable_name, iteration)
|
362
540
|
|
541
|
+
# In chat mode, don't set answer; in task mode, set answer only for task_complete
|
542
|
+
is_task_complete_answer = executed_tool == "task_complete" and not is_chat_mode
|
363
543
|
return ObserveResponseResult(
|
364
544
|
next_prompt=new_prompt,
|
365
545
|
executed_tool=executed_tool,
|
366
|
-
answer=response if
|
546
|
+
answer=response if is_task_complete_answer else None,
|
367
547
|
)
|
548
|
+
|
549
|
+
# If no tools were executed, return original content
|
550
|
+
return ObserveResponseResult(next_prompt=content, executed_tool=None, answer=None)
|
551
|
+
|
368
552
|
except Exception as e:
|
369
553
|
return self._handle_error(e)
|
370
554
|
|
371
555
|
def _execute_tool(self, tool_name: str, tool: Tool, arguments_with_values: dict) -> tuple[str, Any]:
|
372
556
|
"""Execute a tool with validation if required (synchronous wrapper).
|
373
|
-
|
557
|
+
|
374
558
|
Args:
|
375
559
|
tool_name: Name of the tool to execute
|
376
560
|
tool: Tool instance
|
377
561
|
arguments_with_values: Tool arguments
|
378
|
-
|
562
|
+
|
379
563
|
Returns:
|
380
564
|
Tuple of (executed_tool_name, response)
|
381
565
|
"""
|
@@ -389,16 +573,28 @@ class Agent(BaseModel):
|
|
389
573
|
|
390
574
|
async def _async_execute_tool(self, tool_name: str, tool: Tool, arguments_with_values: dict) -> tuple[str, Any]:
|
391
575
|
"""Execute a tool with validation if required (asynchronous).
|
392
|
-
|
576
|
+
|
393
577
|
Args:
|
394
578
|
tool_name: Name of the tool to execute
|
395
579
|
tool: Tool instance
|
396
580
|
arguments_with_values: Tool arguments
|
397
|
-
|
581
|
+
|
398
582
|
Returns:
|
399
583
|
Tuple of (executed_tool_name, response)
|
400
584
|
"""
|
401
585
|
if tool.need_validation:
|
586
|
+
logger.info(f"Tool '{tool_name}' requires validation.")
|
587
|
+
validation_id = str(uuid.uuid4())
|
588
|
+
logger.info(f"Validation ID: {validation_id}")
|
589
|
+
|
590
|
+
self._emit_event(
|
591
|
+
"tool_execute_validation_start",
|
592
|
+
{
|
593
|
+
"validation_id": validation_id,
|
594
|
+
"tool_name": tool_name,
|
595
|
+
"arguments": arguments_with_values
|
596
|
+
},
|
597
|
+
)
|
402
598
|
question_validation = (
|
403
599
|
"Do you permit the execution of this tool?\n"
|
404
600
|
f"Tool: {tool_name}\nArguments:\n"
|
@@ -406,7 +602,18 @@ class Agent(BaseModel):
|
|
406
602
|
+ "\n".join([f" <{key}>{value}</{key}>" for key, value in arguments_with_values.items()])
|
407
603
|
+ "\n</arguments>\nYes or No"
|
408
604
|
)
|
409
|
-
permission_granted = self.ask_for_user_validation(question_validation)
|
605
|
+
permission_granted = await self.ask_for_user_validation(validation_id=validation_id, question=question_validation)
|
606
|
+
|
607
|
+
self._emit_event(
|
608
|
+
"tool_execute_validation_end",
|
609
|
+
{
|
610
|
+
"validation_id": validation_id,
|
611
|
+
"tool_name": tool_name,
|
612
|
+
"arguments": arguments_with_values,
|
613
|
+
"granted": permission_granted
|
614
|
+
},
|
615
|
+
)
|
616
|
+
|
410
617
|
if not permission_granted:
|
411
618
|
return "", f"Error: execution of tool '{tool_name}' was denied by the user."
|
412
619
|
|
@@ -429,8 +636,11 @@ class Agent(BaseModel):
|
|
429
636
|
if hasattr(tool, "async_execute") and callable(tool.async_execute):
|
430
637
|
response = await tool.async_execute(**converted_args)
|
431
638
|
else:
|
432
|
-
# Fall back to synchronous execution if async is not available
|
433
639
|
response = tool.execute(**converted_args)
|
640
|
+
|
641
|
+
# Post-process tool response if needed
|
642
|
+
response = self._post_process_tool_response(tool_name, response)
|
643
|
+
|
434
644
|
executed_tool = tool.name
|
435
645
|
except Exception as e:
|
436
646
|
response = f"Error executing tool: {tool_name}: {str(e)}\n"
|
@@ -443,41 +653,33 @@ class Agent(BaseModel):
|
|
443
653
|
|
444
654
|
async def _async_interpolate_variables(self, text: str, depth: int = 0) -> str:
|
445
655
|
"""Interpolate variables using $var$ syntax in the given text with recursion protection.
|
446
|
-
|
656
|
+
|
447
657
|
Args:
|
448
658
|
text: Text containing variable references
|
449
659
|
depth: Current recursion depth
|
450
|
-
|
660
|
+
|
451
661
|
Returns:
|
452
662
|
Text with variables interpolated
|
453
663
|
"""
|
454
664
|
if not isinstance(text, str):
|
455
665
|
return str(text)
|
456
|
-
|
666
|
+
|
457
667
|
if depth > MAX_INTERPOLATION_DEPTH:
|
458
668
|
logger.warning(f"Max interpolation depth ({MAX_INTERPOLATION_DEPTH}) reached, stopping recursion")
|
459
669
|
return text
|
460
|
-
|
670
|
+
|
461
671
|
try:
|
462
672
|
import re
|
463
|
-
|
464
|
-
# Process each variable in the store
|
673
|
+
|
465
674
|
for var in self.variable_store.keys():
|
466
|
-
# Properly escape the variable name for regex using re.escape
|
467
|
-
# but handle $ characters separately since they're part of our syntax
|
468
675
|
escaped_var = re.escape(var).replace('\\$', '$')
|
469
676
|
pattern = f"\\${escaped_var}\\$"
|
470
|
-
|
471
|
-
# Get variable value as string
|
472
677
|
replacement = str(self.variable_store[var])
|
473
|
-
|
474
|
-
# Replace all occurrences
|
475
678
|
text = re.sub(pattern, lambda m: replacement, text)
|
476
|
-
|
477
|
-
# Check if there are still variables to interpolate (for nested variables)
|
679
|
+
|
478
680
|
if '$' in text and depth < MAX_INTERPOLATION_DEPTH:
|
479
681
|
return await self._async_interpolate_variables(text, depth + 1)
|
480
|
-
|
682
|
+
|
481
683
|
return text
|
482
684
|
except Exception as e:
|
483
685
|
logger.error(f"Error in _async_interpolate_variables: {str(e)}")
|
@@ -485,10 +687,10 @@ class Agent(BaseModel):
|
|
485
687
|
|
486
688
|
def _interpolate_variables(self, text: str) -> str:
|
487
689
|
"""Interpolate variables using $var$ syntax in the given text (synchronous wrapper).
|
488
|
-
|
690
|
+
|
489
691
|
Args:
|
490
692
|
text: Text containing variable references
|
491
|
-
|
693
|
+
|
492
694
|
Returns:
|
493
695
|
Text with variables interpolated
|
494
696
|
"""
|
@@ -502,7 +704,7 @@ class Agent(BaseModel):
|
|
502
704
|
|
503
705
|
def _compact_memory_if_needed(self, current_prompt: str = "") -> None:
|
504
706
|
"""Compacts the memory if it exceeds the maximum occupancy (synchronous wrapper).
|
505
|
-
|
707
|
+
|
506
708
|
Args:
|
507
709
|
current_prompt: Current prompt to calculate token usage
|
508
710
|
"""
|
@@ -516,7 +718,7 @@ class Agent(BaseModel):
|
|
516
718
|
|
517
719
|
async def _async_compact_memory_if_needed(self, current_prompt: str = "") -> None:
|
518
720
|
"""Compacts the memory if it exceeds the maximum occupancy or token limit.
|
519
|
-
|
721
|
+
|
520
722
|
Args:
|
521
723
|
current_prompt: Current prompt to calculate token usage
|
522
724
|
"""
|
@@ -529,7 +731,7 @@ class Agent(BaseModel):
|
|
529
731
|
and self.current_iteration % self.compact_every_n_iterations == 0
|
530
732
|
)
|
531
733
|
should_compact_by_token_limit = (
|
532
|
-
self.max_tokens_working_memory is not None
|
734
|
+
self.max_tokens_working_memory is not None
|
533
735
|
and self.total_tokens > self.max_tokens_working_memory
|
534
736
|
)
|
535
737
|
|
@@ -558,11 +760,10 @@ class Agent(BaseModel):
|
|
558
760
|
|
559
761
|
async def _async_compact_memory_with_summary(self) -> str:
|
560
762
|
"""Generate a summary and compact memory asynchronously.
|
561
|
-
|
763
|
+
|
562
764
|
Returns:
|
563
765
|
Generated summary text
|
564
766
|
"""
|
565
|
-
# Format conversation history for the template
|
566
767
|
memory_copy = self.memory.memory.copy()
|
567
768
|
|
568
769
|
if len(memory_copy) < 3:
|
@@ -571,20 +772,18 @@ class Agent(BaseModel):
|
|
571
772
|
|
572
773
|
user_message = memory_copy.pop()
|
573
774
|
assistant_message = memory_copy.pop()
|
574
|
-
|
575
|
-
|
576
|
-
prompt_summary = self._render_template('memory_compaction_prompt.j2',
|
775
|
+
|
776
|
+
prompt_summary = self._render_template('memory_compaction_prompt.j2',
|
577
777
|
conversation_history="\n\n".join(
|
578
|
-
f"[{msg.role.upper()}]: {msg.content}"
|
778
|
+
f"[{msg.role.upper()}]: {msg.content}"
|
579
779
|
for msg in memory_copy
|
580
780
|
))
|
581
|
-
|
781
|
+
|
582
782
|
summary = await self.model.async_generate_with_history(messages_history=memory_copy, prompt=prompt_summary)
|
583
|
-
|
584
|
-
# Remove last system message if present
|
783
|
+
|
585
784
|
if memory_copy and memory_copy[-1].role == "system":
|
586
785
|
memory_copy.pop()
|
587
|
-
|
786
|
+
|
588
787
|
memory_copy.append(Message(role="user", content=summary.response))
|
589
788
|
memory_copy.append(assistant_message)
|
590
789
|
memory_copy.append(user_message)
|
@@ -593,10 +792,10 @@ class Agent(BaseModel):
|
|
593
792
|
|
594
793
|
def _generate_task_summary(self, content: str) -> str:
|
595
794
|
"""Generate a concise task-focused summary (synchronous wrapper).
|
596
|
-
|
795
|
+
|
597
796
|
Args:
|
598
797
|
content: The content to summarize
|
599
|
-
|
798
|
+
|
600
799
|
Returns:
|
601
800
|
Generated task summary
|
602
801
|
"""
|
@@ -620,7 +819,7 @@ class Agent(BaseModel):
|
|
620
819
|
try:
|
621
820
|
if len(content) < 1024 * 4:
|
622
821
|
return content
|
623
|
-
|
822
|
+
|
624
823
|
prompt = self._render_template('task_summary_prompt.j2', content=content)
|
625
824
|
result = await self.model.async_generate(prompt=prompt)
|
626
825
|
logger.debug(f"Generated summary: {result.response}")
|
@@ -631,7 +830,7 @@ class Agent(BaseModel):
|
|
631
830
|
|
632
831
|
def _reset_session(self, task_to_solve: str = "", max_iterations: int = 30, clear_memory: bool = True) -> None:
|
633
832
|
"""Reset the agent's session.
|
634
|
-
|
833
|
+
|
635
834
|
Args:
|
636
835
|
task_to_solve: New task to solve
|
637
836
|
max_iterations: Maximum number of iterations
|
@@ -651,7 +850,7 @@ class Agent(BaseModel):
|
|
651
850
|
|
652
851
|
def _update_total_tokens(self, message_history: list[Message], prompt: str) -> None:
|
653
852
|
"""Update the total tokens count based on message history and prompt.
|
654
|
-
|
853
|
+
|
655
854
|
Args:
|
656
855
|
message_history: List of messages
|
657
856
|
prompt: Current prompt
|
@@ -660,7 +859,7 @@ class Agent(BaseModel):
|
|
660
859
|
|
661
860
|
def _emit_event(self, event_type: str, data: dict[str, Any] | None = None) -> None:
|
662
861
|
"""Emit an event with system context and optional additional data.
|
663
|
-
|
862
|
+
|
664
863
|
Args:
|
665
864
|
event_type: Type of event
|
666
865
|
data: Additional event data
|
@@ -678,10 +877,10 @@ class Agent(BaseModel):
|
|
678
877
|
|
679
878
|
def _parse_tool_usage(self, content: str) -> dict:
|
680
879
|
"""Extract tool usage from the response content.
|
681
|
-
|
880
|
+
|
682
881
|
Args:
|
683
882
|
content: Response content
|
684
|
-
|
883
|
+
|
685
884
|
Returns:
|
686
885
|
Dictionary mapping tool names to inputs
|
687
886
|
"""
|
@@ -694,30 +893,39 @@ class Agent(BaseModel):
|
|
694
893
|
tool_names = self.tools.tool_names()
|
695
894
|
|
696
895
|
if action:
|
697
|
-
|
896
|
+
tool_data = xml_parser.extract_elements(text=action["action"], element_names=tool_names)
|
897
|
+
# Handle nested parameters within action tags
|
898
|
+
for tool_name in tool_data:
|
899
|
+
if "<parameter_name>" in tool_data[tool_name]:
|
900
|
+
params = xml_parser.extract_elements(text=tool_data[tool_name], element_names=["parameter_name", "parameter_value"])
|
901
|
+
if "parameter_name" in params and "parameter_value" in params:
|
902
|
+
tool_data[tool_name] = {params["parameter_name"]: params["parameter_value"]}
|
903
|
+
return tool_data
|
698
904
|
else:
|
699
905
|
return xml_parser.extract_elements(text=content, element_names=tool_names)
|
700
906
|
|
701
|
-
def _parse_tool_arguments(self, tool: Tool, tool_input: str) -> dict:
|
907
|
+
def _parse_tool_arguments(self, tool: Tool, tool_input: str | dict) -> dict:
|
702
908
|
"""Parse the tool arguments from the tool input.
|
703
|
-
|
909
|
+
|
704
910
|
Args:
|
705
911
|
tool: Tool instance
|
706
|
-
tool_input: Raw tool input text
|
707
|
-
|
912
|
+
tool_input: Raw tool input text or pre-parsed dict
|
913
|
+
|
708
914
|
Returns:
|
709
915
|
Dictionary of parsed arguments
|
710
916
|
"""
|
917
|
+
if isinstance(tool_input, dict):
|
918
|
+
return tool_input # Already parsed from XML
|
711
919
|
tool_parser = ToolParser(tool=tool)
|
712
920
|
return tool_parser.parse(tool_input)
|
713
921
|
|
714
922
|
def _is_repeated_tool_call(self, tool_name: str, arguments_with_values: dict) -> bool:
|
715
923
|
"""Check if the tool call is repeated.
|
716
|
-
|
924
|
+
|
717
925
|
Args:
|
718
926
|
tool_name: Name of the tool
|
719
927
|
arguments_with_values: Tool arguments
|
720
|
-
|
928
|
+
|
721
929
|
Returns:
|
722
930
|
True if call is repeated, False otherwise
|
723
931
|
"""
|
@@ -743,7 +951,7 @@ class Agent(BaseModel):
|
|
743
951
|
|
744
952
|
def _handle_no_tool_usage(self) -> ObserveResponseResult:
|
745
953
|
"""Handle the case where no tool usage is found in the response.
|
746
|
-
|
954
|
+
|
747
955
|
Returns:
|
748
956
|
ObserveResponseResult with error message
|
749
957
|
"""
|
@@ -753,10 +961,10 @@ class Agent(BaseModel):
|
|
753
961
|
|
754
962
|
def _handle_tool_not_found(self, tool_name: str) -> ObserveResponseResult:
|
755
963
|
"""Handle the case where the tool is not found.
|
756
|
-
|
964
|
+
|
757
965
|
Args:
|
758
966
|
tool_name: Name of the tool
|
759
|
-
|
967
|
+
|
760
968
|
Returns:
|
761
969
|
ObserveResponseResult with error message
|
762
970
|
"""
|
@@ -769,11 +977,11 @@ class Agent(BaseModel):
|
|
769
977
|
|
770
978
|
def _handle_repeated_tool_call(self, tool_name: str, arguments_with_values: dict) -> tuple[str, str]:
|
771
979
|
"""Handle the case where a tool call is repeated.
|
772
|
-
|
980
|
+
|
773
981
|
Args:
|
774
982
|
tool_name: Name of the tool
|
775
983
|
arguments_with_values: Tool arguments
|
776
|
-
|
984
|
+
|
777
985
|
Returns:
|
778
986
|
Tuple of (executed_tool_name, error_message)
|
779
987
|
"""
|
@@ -788,10 +996,10 @@ class Agent(BaseModel):
|
|
788
996
|
|
789
997
|
def _handle_tool_execution_failure(self, response: str) -> ObserveResponseResult:
|
790
998
|
"""Handle the case where tool execution fails.
|
791
|
-
|
999
|
+
|
792
1000
|
Args:
|
793
1001
|
response: Error response
|
794
|
-
|
1002
|
+
|
795
1003
|
Returns:
|
796
1004
|
ObserveResponseResult with error message
|
797
1005
|
"""
|
@@ -803,10 +1011,10 @@ class Agent(BaseModel):
|
|
803
1011
|
|
804
1012
|
def _handle_error(self, error: Exception) -> ObserveResponseResult:
|
805
1013
|
"""Handle any exceptions that occur during response observation.
|
806
|
-
|
1014
|
+
|
807
1015
|
Args:
|
808
1016
|
error: Exception that occurred
|
809
|
-
|
1017
|
+
|
810
1018
|
Returns:
|
811
1019
|
ObserveResponseResult with error message
|
812
1020
|
"""
|
@@ -816,18 +1024,202 @@ class Agent(BaseModel):
|
|
816
1024
|
executed_tool=None,
|
817
1025
|
answer=None,
|
818
1026
|
)
|
1027
|
+
|
1028
|
+
async def _async_observe_response_chat(self, content: str, iteration: int = 1) -> ObserveResponseResult:
|
1029
|
+
"""Specialized observation method for chat mode with tool handling.
|
1030
|
+
|
1031
|
+
This method processes responses in chat mode, identifying and executing tool calls
|
1032
|
+
while providing appropriate default parameters when needed. Prevents task_complete usage.
|
1033
|
+
|
1034
|
+
Args:
|
1035
|
+
content: The response content to analyze
|
1036
|
+
iteration: Current iteration number
|
1037
|
+
|
1038
|
+
Returns:
|
1039
|
+
ObserveResponseResult with next steps information
|
1040
|
+
"""
|
1041
|
+
try:
|
1042
|
+
# Check for tool call patterns in the content
|
1043
|
+
if "<action>" not in content:
|
1044
|
+
logger.debug("No tool usage detected in chat response")
|
1045
|
+
return ObserveResponseResult(next_prompt=content, executed_tool=None, answer=None)
|
1046
|
+
|
1047
|
+
# Parse content for tool usage
|
1048
|
+
parsed_content = self._parse_tool_usage(content)
|
1049
|
+
if not parsed_content:
|
1050
|
+
# Malformed tool call in chat mode; return feedback
|
1051
|
+
error_prompt = (
|
1052
|
+
"⚠️ Error: Invalid tool call format detected. "
|
1053
|
+
"Please use the exact XML structure:\n"
|
1054
|
+
"```xml\n<action>\n<tool_name>\n <parameter_name>value</parameter_name>\n</tool_name>\n</action>\n```"
|
1055
|
+
)
|
1056
|
+
return ObserveResponseResult(next_prompt=error_prompt, executed_tool=None, answer=None)
|
1057
|
+
|
1058
|
+
# Check for task_complete attempt and block it with feedback
|
1059
|
+
if "task_complete" in parsed_content:
|
1060
|
+
feedback = (
|
1061
|
+
"⚠️ Note: The 'task_complete' tool is not available in chat mode. "
|
1062
|
+
"This is a conversational mode; tasks are not completed here. "
|
1063
|
+
"Please use other tools or continue the conversation."
|
1064
|
+
)
|
1065
|
+
return ObserveResponseResult(next_prompt=feedback, executed_tool=None, answer=None)
|
1066
|
+
|
1067
|
+
# Process tools with prioritization based on tool_mode
|
1068
|
+
tool_names = list(parsed_content.keys())
|
1069
|
+
# Prioritize specific tools if tool_mode is set and the tool is available
|
1070
|
+
if self.tool_mode and self.tool_mode in self.tools.tool_names() and self.tool_mode in tool_names:
|
1071
|
+
tool_names = [self.tool_mode] + [t for t in tool_names if t != self.tool_mode]
|
1072
|
+
|
1073
|
+
for tool_name in tool_names:
|
1074
|
+
if tool_name not in parsed_content:
|
1075
|
+
continue
|
1076
|
+
|
1077
|
+
tool_input = parsed_content[tool_name]
|
1078
|
+
tool = self.tools.get(tool_name)
|
1079
|
+
if not tool:
|
1080
|
+
return self._handle_tool_not_found(tool_name)
|
1081
|
+
|
1082
|
+
# Parse tool arguments from the input
|
1083
|
+
arguments_with_values = self._parse_tool_arguments(tool, tool_input)
|
1084
|
+
|
1085
|
+
# Apply default parameters based on tool schema if missing
|
1086
|
+
self._apply_default_parameters(tool, arguments_with_values)
|
1087
|
+
|
1088
|
+
# Check for repeated calls
|
1089
|
+
is_repeated_call = self._is_repeated_tool_call(tool_name, arguments_with_values)
|
1090
|
+
if is_repeated_call:
|
1091
|
+
executed_tool, response = self._handle_repeated_tool_call(tool_name, arguments_with_values)
|
1092
|
+
else:
|
1093
|
+
executed_tool, response = await self._async_execute_tool(tool_name, tool, arguments_with_values)
|
1094
|
+
|
1095
|
+
if not executed_tool:
|
1096
|
+
# Tool execution failed
|
1097
|
+
return self._handle_tool_execution_failure(response)
|
1098
|
+
|
1099
|
+
# Store result in variable memory for potential future reference
|
1100
|
+
variable_name = f"result_{executed_tool}_{iteration}"
|
1101
|
+
self.variable_store[variable_name] = response
|
1102
|
+
|
1103
|
+
# Truncate response if too long for display
|
1104
|
+
response_display = response
|
1105
|
+
if len(response) > MAX_RESPONSE_LENGTH:
|
1106
|
+
response_display = response[:MAX_RESPONSE_LENGTH]
|
1107
|
+
response_display += f"... (truncated, full content available in ${variable_name})"
|
1108
|
+
|
1109
|
+
# Format result in a user-friendly way
|
1110
|
+
return ObserveResponseResult(
|
1111
|
+
next_prompt=response_display,
|
1112
|
+
executed_tool=executed_tool,
|
1113
|
+
answer=None
|
1114
|
+
)
|
1115
|
+
|
1116
|
+
# If we get here, no tool was successfully executed
|
1117
|
+
return ObserveResponseResult(
|
1118
|
+
next_prompt="I tried to use a tool, but encountered an issue. Please try again with a different request.",
|
1119
|
+
executed_tool=None,
|
1120
|
+
answer=None
|
1121
|
+
)
|
1122
|
+
|
1123
|
+
except Exception as e:
|
1124
|
+
return self._handle_error(e)
|
1125
|
+
|
1126
|
+
def _apply_default_parameters(self, tool: Tool, arguments_with_values: dict) -> None:
|
1127
|
+
"""Apply default parameters to tool arguments based on tool schema.
|
1128
|
+
|
1129
|
+
This method examines the tool's schema and fills in any missing required parameters
|
1130
|
+
with sensible defaults based on the tool type.
|
1131
|
+
|
1132
|
+
Args:
|
1133
|
+
tool: The tool instance
|
1134
|
+
arguments_with_values: Dictionary of current arguments
|
1135
|
+
"""
|
1136
|
+
try:
|
1137
|
+
# Add defaults for common search tools
|
1138
|
+
if tool.name == "duckduckgo_tool" and "max_results" not in arguments_with_values:
|
1139
|
+
logger.debug(f"Adding default max_results=5 for {tool.name}")
|
1140
|
+
arguments_with_values["max_results"] = "5"
|
1141
|
+
|
1142
|
+
# Check tool schema for required parameters
|
1143
|
+
if hasattr(tool, "schema") and hasattr(tool.schema, "parameters"):
|
1144
|
+
for param_name, param_info in tool.schema.parameters.items():
|
1145
|
+
# If required parameter is missing, try to add a default
|
1146
|
+
if param_info.get("required", False) and param_name not in arguments_with_values:
|
1147
|
+
if "default" in param_info:
|
1148
|
+
logger.debug(f"Adding default value for {param_name} in {tool.name}")
|
1149
|
+
arguments_with_values[param_name] = param_info["default"]
|
1150
|
+
except Exception as e:
|
1151
|
+
logger.debug(f"Error applying default parameters: {str(e)}")
|
1152
|
+
# Continue without defaults rather than failing the whole operation
|
1153
|
+
|
1154
|
+
def _post_process_tool_response(self, tool_name: str, response: Any) -> str:
|
1155
|
+
"""Process tool response for better presentation to the user.
|
1156
|
+
|
1157
|
+
This generic method handles common tool response formats:
|
1158
|
+
- Parses JSON strings into structured data
|
1159
|
+
- Formats search results into readable text
|
1160
|
+
- Handles different response types appropriately
|
1161
|
+
|
1162
|
+
Args:
|
1163
|
+
tool_name: Name of the tool that produced the response
|
1164
|
+
response: Raw tool response
|
1165
|
+
|
1166
|
+
Returns:
|
1167
|
+
Processed response as a string
|
1168
|
+
"""
|
1169
|
+
# Immediately return if response is not a string
|
1170
|
+
if not isinstance(response, str):
|
1171
|
+
return response
|
1172
|
+
|
1173
|
+
# Try to parse as JSON if it looks like JSON
|
1174
|
+
if response.strip().startswith(("{" , "[")) and response.strip().endswith(("}", "]")):
|
1175
|
+
try:
|
1176
|
+
# Use lazy import for json to maintain dependency structure
|
1177
|
+
import json
|
1178
|
+
parsed = json.loads(response)
|
1179
|
+
|
1180
|
+
# Handle list-type responses (common for search tools)
|
1181
|
+
if isinstance(parsed, list) and parsed:
|
1182
|
+
# Detect if this is a search result by checking for common fields
|
1183
|
+
search_result_fields = ['title', 'href', 'url', 'body', 'content', 'snippet']
|
1184
|
+
if isinstance(parsed[0], dict) and any(field in parsed[0] for field in search_result_fields):
|
1185
|
+
# Format as search results
|
1186
|
+
formatted_results = []
|
1187
|
+
for idx, result in enumerate(parsed, 1):
|
1188
|
+
if not isinstance(result, dict):
|
1189
|
+
continue
|
1190
|
+
|
1191
|
+
# Extract common fields with fallbacks
|
1192
|
+
title = result.get('title', 'No title')
|
1193
|
+
url = result.get('href', result.get('url', 'No link'))
|
1194
|
+
description = result.get('body', result.get('content',
|
1195
|
+
result.get('snippet', result.get('description', 'No description'))))
|
1196
|
+
|
1197
|
+
formatted_results.append(f"{idx}. {title}\n URL: {url}\n {description}\n")
|
1198
|
+
|
1199
|
+
if formatted_results:
|
1200
|
+
return "\n".join(formatted_results)
|
1201
|
+
|
1202
|
+
# If not handled as a special case, just pretty-print
|
1203
|
+
return json.dumps(parsed, indent=2, ensure_ascii=False)
|
1204
|
+
|
1205
|
+
except json.JSONDecodeError:
|
1206
|
+
# Not valid JSON after all
|
1207
|
+
pass
|
1208
|
+
|
1209
|
+
# Return original response if no special handling applies
|
1210
|
+
return response
|
819
1211
|
|
820
1212
|
def _format_observation_response(
|
821
1213
|
self, response: str, last_executed_tool: str, variable_name: str, iteration: int
|
822
1214
|
) -> str:
|
823
1215
|
"""Format the observation response with the given response, variable name, and iteration.
|
824
|
-
|
1216
|
+
|
825
1217
|
Args:
|
826
1218
|
response: Tool execution response
|
827
1219
|
last_executed_tool: Name of last executed tool
|
828
1220
|
variable_name: Name of variable storing response
|
829
1221
|
iteration: Current iteration number
|
830
|
-
|
1222
|
+
|
831
1223
|
Returns:
|
832
1224
|
Formatted observation response
|
833
1225
|
"""
|
@@ -840,7 +1232,7 @@ class Agent(BaseModel):
|
|
840
1232
|
|
841
1233
|
tools_prompt = self._get_tools_names_prompt()
|
842
1234
|
variables_prompt = self._get_variable_prompt()
|
843
|
-
|
1235
|
+
|
844
1236
|
formatted_response = self._render_template(
|
845
1237
|
'observation_response_format.j2',
|
846
1238
|
iteration=iteration,
|
@@ -866,7 +1258,7 @@ class Agent(BaseModel):
|
|
866
1258
|
"""
|
867
1259
|
tools_prompt = self._get_tools_names_prompt()
|
868
1260
|
variables_prompt = self._get_variable_prompt()
|
869
|
-
|
1261
|
+
|
870
1262
|
prompt_task = self._render_template(
|
871
1263
|
'task_prompt.j2',
|
872
1264
|
task=task,
|
@@ -877,16 +1269,77 @@ class Agent(BaseModel):
|
|
877
1269
|
|
878
1270
|
def _get_tools_names_prompt(self) -> str:
|
879
1271
|
"""Construct a detailed prompt that lists the available tools for task execution.
|
880
|
-
|
1272
|
+
|
881
1273
|
Returns:
|
882
1274
|
Formatted tools prompt
|
883
1275
|
"""
|
1276
|
+
# Check if we're in chat mode
|
1277
|
+
is_chat_mode = not self.task_to_solve.strip()
|
1278
|
+
|
1279
|
+
if is_chat_mode:
|
1280
|
+
return self._get_tools_names_prompt_for_chat()
|
1281
|
+
|
1282
|
+
# Default task mode behavior
|
884
1283
|
tool_names = ', '.join(self.tools.tool_names())
|
885
1284
|
return self._render_template('tools_prompt.j2', tool_names=tool_names)
|
1285
|
+
|
1286
|
+
def _get_tools_names_prompt_for_chat(self) -> str:
|
1287
|
+
"""Construct a detailed prompt for chat mode that includes tool parameters, excluding task_complete.
|
1288
|
+
|
1289
|
+
Returns:
|
1290
|
+
Formatted tools prompt with parameter details
|
1291
|
+
"""
|
1292
|
+
tool_descriptions = []
|
1293
|
+
|
1294
|
+
try:
|
1295
|
+
for tool_name in self.tools.tool_names():
|
1296
|
+
if tool_name == "task_complete":
|
1297
|
+
continue # Explicitly exclude task_complete in chat mode
|
1298
|
+
|
1299
|
+
try:
|
1300
|
+
tool = self.tools.get(tool_name)
|
1301
|
+
params = []
|
1302
|
+
|
1303
|
+
# Get parameter details if available
|
1304
|
+
try:
|
1305
|
+
if hasattr(tool, "schema") and hasattr(tool.schema, "parameters"):
|
1306
|
+
schema_params = getattr(tool.schema, "parameters", {})
|
1307
|
+
if isinstance(schema_params, dict):
|
1308
|
+
for param_name, param_info in schema_params.items():
|
1309
|
+
if not isinstance(param_info, dict):
|
1310
|
+
continue
|
1311
|
+
|
1312
|
+
required = "(required)" if param_info.get("required", False) else "(optional)"
|
1313
|
+
default = f" default: {param_info['default']}" if "default" in param_info else ""
|
1314
|
+
param_desc = f"{param_name} {required}{default}"
|
1315
|
+
params.append(param_desc)
|
1316
|
+
except Exception as e:
|
1317
|
+
logger.debug(f"Error parsing schema for {tool_name}: {str(e)}")
|
1318
|
+
|
1319
|
+
# Special case for duckduckgo_tool
|
1320
|
+
if tool_name == "duckduckgo_tool" and not any(p.startswith("max_results ") for p in params):
|
1321
|
+
params.append("max_results (required) default: 5")
|
1322
|
+
|
1323
|
+
# Special case for other search tools that might need max_results
|
1324
|
+
if "search" in tool_name.lower() and not any(p.startswith("max_results ") for p in params):
|
1325
|
+
params.append("max_results (optional) default: 5")
|
1326
|
+
|
1327
|
+
param_str = ", ".join(params) if params else "No parameters required"
|
1328
|
+
tool_descriptions.append(f"{tool_name}: {param_str}")
|
1329
|
+
except Exception as e:
|
1330
|
+
logger.debug(f"Error processing tool {tool_name}: {str(e)}")
|
1331
|
+
# Still include the tool in the list, but with minimal info
|
1332
|
+
tool_descriptions.append(f"{tool_name}: Error retrieving parameters")
|
1333
|
+
except Exception as e:
|
1334
|
+
logger.debug(f"Error generating tool descriptions: {str(e)}")
|
1335
|
+
return "Error retrieving tool information"
|
1336
|
+
|
1337
|
+
formatted_tools = "\n".join(tool_descriptions) if tool_descriptions else "No tools available."
|
1338
|
+
return formatted_tools
|
886
1339
|
|
887
1340
|
def _get_variable_prompt(self) -> str:
|
888
1341
|
"""Construct a prompt that explains how to use variables.
|
889
|
-
|
1342
|
+
|
890
1343
|
Returns:
|
891
1344
|
Formatted variables prompt
|
892
1345
|
"""
|
@@ -895,7 +1348,7 @@ class Agent(BaseModel):
|
|
895
1348
|
|
896
1349
|
def _calculate_context_occupancy(self) -> float:
|
897
1350
|
"""Calculate the number of tokens in percentages for prompt and completion.
|
898
|
-
|
1351
|
+
|
899
1352
|
Returns:
|
900
1353
|
Percentage of context window occupied
|
901
1354
|
"""
|
@@ -923,7 +1376,7 @@ class Agent(BaseModel):
|
|
923
1376
|
|
924
1377
|
def update_model(self, new_model_name: str) -> None:
|
925
1378
|
"""Update the model name and recreate the model instance.
|
926
|
-
|
1379
|
+
|
927
1380
|
Args:
|
928
1381
|
new_model_name: New model name to use
|
929
1382
|
"""
|
@@ -932,98 +1385,86 @@ class Agent(BaseModel):
|
|
932
1385
|
|
933
1386
|
def add_tool(self, tool: Tool) -> None:
|
934
1387
|
"""Add a new tool to the agent's tool manager.
|
935
|
-
|
1388
|
+
|
936
1389
|
Args:
|
937
1390
|
tool: The tool instance to add
|
938
|
-
|
1391
|
+
|
939
1392
|
Raises:
|
940
1393
|
ValueError: If a tool with the same name already exists
|
941
1394
|
"""
|
942
1395
|
if tool.name in self.tools.tool_names():
|
943
1396
|
raise ValueError(f"Tool with name '{tool.name}' already exists")
|
944
|
-
|
1397
|
+
|
945
1398
|
self.tools.add(tool)
|
946
|
-
# Update tools markdown in config
|
947
1399
|
self.config = AgentConfig(
|
948
1400
|
environment_details=self.config.environment_details,
|
949
1401
|
tools_markdown=self.tools.to_markdown(),
|
950
1402
|
system_prompt=self.config.system_prompt,
|
951
1403
|
)
|
952
1404
|
logger.debug(f"Added tool: {tool.name}")
|
953
|
-
|
1405
|
+
|
954
1406
|
def remove_tool(self, tool_name: str) -> None:
|
955
1407
|
"""Remove a tool from the agent's tool manager.
|
956
|
-
|
1408
|
+
|
957
1409
|
Args:
|
958
1410
|
tool_name: Name of the tool to remove
|
959
|
-
|
1411
|
+
|
960
1412
|
Raises:
|
961
1413
|
ValueError: If tool doesn't exist or is TaskCompleteTool
|
962
1414
|
"""
|
963
1415
|
if tool_name not in self.tools.tool_names():
|
964
1416
|
raise ValueError(f"Tool '{tool_name}' does not exist")
|
965
|
-
|
1417
|
+
|
966
1418
|
tool = self.tools.get(tool_name)
|
967
1419
|
if isinstance(tool, TaskCompleteTool):
|
968
1420
|
raise ValueError("Cannot remove TaskCompleteTool as it is required")
|
969
|
-
|
1421
|
+
|
970
1422
|
self.tools.remove(tool_name)
|
971
|
-
# Update tools markdown in config
|
972
1423
|
self.config = AgentConfig(
|
973
1424
|
environment_details=self.config.environment_details,
|
974
1425
|
tools_markdown=self.tools.to_markdown(),
|
975
1426
|
system_prompt=self.config.system_prompt,
|
976
1427
|
)
|
977
1428
|
logger.debug(f"Removed tool: {tool_name}")
|
978
|
-
|
1429
|
+
|
979
1430
|
def set_tools(self, tools: list[Tool]) -> None:
|
980
1431
|
"""Set/replace all tools for the agent.
|
981
|
-
|
1432
|
+
|
982
1433
|
Args:
|
983
1434
|
tools: List of tool instances to set
|
984
|
-
|
1435
|
+
|
985
1436
|
Note:
|
986
1437
|
TaskCompleteTool will be automatically added if not present
|
987
1438
|
"""
|
988
|
-
# Ensure TaskCompleteTool is present
|
989
1439
|
if not any(isinstance(t, TaskCompleteTool) for t in tools):
|
990
1440
|
tools.append(TaskCompleteTool())
|
991
|
-
|
992
|
-
# Create new tool manager and add tools
|
1441
|
+
|
993
1442
|
tool_manager = ToolManager()
|
994
1443
|
tool_manager.add_list(tools)
|
995
1444
|
self.tools = tool_manager
|
996
|
-
|
997
|
-
# Update config with new tools markdown
|
1445
|
+
|
998
1446
|
self.config = AgentConfig(
|
999
1447
|
environment_details=self.config.environment_details,
|
1000
1448
|
tools_markdown=self.tools.to_markdown(),
|
1001
1449
|
system_prompt=self.config.system_prompt,
|
1002
1450
|
)
|
1003
1451
|
logger.debug(f"Set {len(tools)} tools")
|
1004
|
-
|
1452
|
+
|
1005
1453
|
def _render_template(self, template_name: str, **kwargs) -> str:
|
1006
1454
|
"""Render a Jinja2 template with the provided variables.
|
1007
|
-
|
1455
|
+
|
1008
1456
|
Args:
|
1009
1457
|
template_name: Name of the template file (without directory path)
|
1010
1458
|
**kwargs: Variables to pass to the template
|
1011
|
-
|
1459
|
+
|
1012
1460
|
Returns:
|
1013
1461
|
str: The rendered template
|
1014
1462
|
"""
|
1015
1463
|
try:
|
1016
|
-
# Get the directory where this file is located
|
1017
1464
|
current_dir = Path(os.path.dirname(os.path.abspath(__file__)))
|
1018
|
-
|
1019
|
-
# Set up Jinja2 environment
|
1020
1465
|
template_dir = current_dir / 'prompts'
|
1021
1466
|
env = Environment(loader=FileSystemLoader(template_dir))
|
1022
|
-
|
1023
|
-
# Load the template
|
1024
1467
|
template = env.get_template(template_name)
|
1025
|
-
|
1026
|
-
# Render the template with the provided variables
|
1027
1468
|
return template.render(**kwargs)
|
1028
1469
|
except Exception as e:
|
1029
1470
|
logger.error(f"Error rendering template {template_name}: {str(e)}")
|