quantalogic 0.58.0__py3-none-any.whl → 0.59.1__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 +540 -123
- quantalogic/agent_factory.py +44 -40
- quantalogic/config.py +8 -7
- quantalogic/event_emitter.py +2 -2
- quantalogic/main.py +81 -5
- quantalogic/prompts/chat_system_prompt.j2 +54 -0
- quantalogic/task_runner.py +335 -178
- {quantalogic-0.58.0.dist-info → quantalogic-0.59.1.dist-info}/METADATA +124 -30
- {quantalogic-0.58.0.dist-info → quantalogic-0.59.1.dist-info}/RECORD +12 -11
- {quantalogic-0.58.0.dist-info → quantalogic-0.59.1.dist-info}/LICENSE +0 -0
- {quantalogic-0.58.0.dist-info → quantalogic-0.59.1.dist-info}/WHEEL +0 -0
- {quantalogic-0.58.0.dist-info → quantalogic-0.59.1.dist-info}/entry_points.txt +0 -0
quantalogic/agent.py
CHANGED
@@ -1,4 +1,4 @@
|
|
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
|
@@ -6,7 +6,7 @@ import uuid
|
|
6
6
|
from collections.abc import Awaitable, Callable
|
7
7
|
from datetime import datetime
|
8
8
|
from pathlib import Path
|
9
|
-
from typing import Any
|
9
|
+
from typing import Any, Optional
|
10
10
|
|
11
11
|
from jinja2 import Environment, FileSystemLoader
|
12
12
|
from loguru import logger
|
@@ -58,19 +58,21 @@ class ObserveResponseResult(BaseModel):
|
|
58
58
|
|
59
59
|
|
60
60
|
class Agent(BaseModel):
|
61
|
-
"""Enhanced QuantaLogic agent
|
61
|
+
"""Enhanced QuantaLogic agent supporting both ReAct goal-solving and conversational chat modes.
|
62
62
|
|
63
|
-
|
64
|
-
Use `
|
65
|
-
|
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.
|
66
68
|
"""
|
67
69
|
|
68
70
|
model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True, extra="forbid")
|
69
71
|
|
70
72
|
specific_expertise: str
|
71
73
|
model: GenerativeModel
|
72
|
-
memory: AgentMemory = AgentMemory() #
|
73
|
-
variable_store: VariableMemory = VariableMemory() #
|
74
|
+
memory: AgentMemory = AgentMemory() # List of User/Assistant Messages
|
75
|
+
variable_store: VariableMemory = VariableMemory() # Dictionary of variables
|
74
76
|
tools: ToolManager = ToolManager()
|
75
77
|
event_emitter: EventEmitter = EventEmitter()
|
76
78
|
config: AgentConfig
|
@@ -87,6 +89,8 @@ class Agent(BaseModel):
|
|
87
89
|
compact_every_n_iterations: int | None = None
|
88
90
|
max_tokens_working_memory: int | None = None
|
89
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
|
90
94
|
|
91
95
|
def __init__(
|
92
96
|
self,
|
@@ -101,30 +105,32 @@ class Agent(BaseModel):
|
|
101
105
|
compact_every_n_iterations: int | None = None,
|
102
106
|
max_tokens_working_memory: int | None = None,
|
103
107
|
event_emitter: EventEmitter | None = None,
|
108
|
+
chat_system_prompt: str | None = None,
|
109
|
+
tool_mode: Optional[str] = None,
|
104
110
|
):
|
105
111
|
"""Initialize the agent with model, memory, tools, and configurations.
|
106
|
-
|
112
|
+
|
107
113
|
Args:
|
108
114
|
model_name: Name of the model to use
|
109
115
|
memory: AgentMemory instance for storing conversation history
|
110
116
|
variable_store: VariableMemory instance for storing variables
|
111
|
-
tools: List of Tool instances
|
117
|
+
tools: List of Tool instances
|
112
118
|
ask_for_user_validation: Function to ask for user validation
|
113
|
-
task_to_solve: Initial task to solve
|
119
|
+
task_to_solve: Initial task to solve (for ReAct mode)
|
114
120
|
specific_expertise: Description of the agent's expertise
|
115
121
|
get_environment: Function to get environment details
|
116
122
|
compact_every_n_iterations: How often to compact memory
|
117
123
|
max_tokens_working_memory: Maximum token count for working memory
|
118
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
|
119
127
|
"""
|
120
128
|
try:
|
121
129
|
logger.debug("Initializing agent...")
|
122
130
|
|
123
|
-
# Create or use provided event emitter
|
124
131
|
if event_emitter is None:
|
125
132
|
event_emitter = EventEmitter()
|
126
133
|
|
127
|
-
# Add TaskCompleteTool to the tools list if not already present
|
128
134
|
if not any(isinstance(t, TaskCompleteTool) for t in tools):
|
129
135
|
tools.append(TaskCompleteTool())
|
130
136
|
|
@@ -145,7 +151,11 @@ class Agent(BaseModel):
|
|
145
151
|
system_prompt=system_prompt_text,
|
146
152
|
)
|
147
153
|
|
148
|
-
|
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
|
+
|
149
159
|
super().__init__(
|
150
160
|
specific_expertise=specific_expertise,
|
151
161
|
model=GenerativeModel(model=model_name, event_emitter=event_emitter),
|
@@ -166,12 +176,15 @@ class Agent(BaseModel):
|
|
166
176
|
system_prompt="",
|
167
177
|
compact_every_n_iterations=compact_every_n_iterations or 30,
|
168
178
|
max_tokens_working_memory=max_tokens_working_memory,
|
179
|
+
chat_system_prompt=chat_system_prompt,
|
180
|
+
tool_mode=tool_mode,
|
169
181
|
)
|
170
182
|
|
171
183
|
self._model_name = model_name
|
172
184
|
|
173
185
|
logger.debug(f"Memory will be compacted every {self.compact_every_n_iterations} iterations")
|
174
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}")
|
175
188
|
logger.debug("Agent initialized successfully.")
|
176
189
|
except Exception as e:
|
177
190
|
logger.error(f"Failed to initialize agent: {str(e)}")
|
@@ -186,7 +199,6 @@ class Agent(BaseModel):
|
|
186
199
|
def model_name(self, value: str) -> None:
|
187
200
|
"""Set the model name and update the model instance."""
|
188
201
|
self._model_name = value
|
189
|
-
# Update the model instance with the new name
|
190
202
|
self.model = GenerativeModel(model=value, event_emitter=self.event_emitter)
|
191
203
|
|
192
204
|
def clear_memory(self) -> None:
|
@@ -213,7 +225,6 @@ class Agent(BaseModel):
|
|
213
225
|
try:
|
214
226
|
loop = asyncio.get_event_loop()
|
215
227
|
except RuntimeError:
|
216
|
-
# Create a new event loop if one doesn't exist
|
217
228
|
loop = asyncio.new_event_loop()
|
218
229
|
asyncio.set_event_loop(loop)
|
219
230
|
|
@@ -278,7 +289,6 @@ class Agent(BaseModel):
|
|
278
289
|
messages_history=self.memory.memory,
|
279
290
|
prompt=current_prompt,
|
280
291
|
streaming=False,
|
281
|
-
# Removed stop_words parameter to allow complete responses
|
282
292
|
)
|
283
293
|
|
284
294
|
content = result.response
|
@@ -292,7 +302,7 @@ class Agent(BaseModel):
|
|
292
302
|
|
293
303
|
if result.executed_tool == "task_complete":
|
294
304
|
self._emit_event("task_complete", {"response": result.answer})
|
295
|
-
answer = result.answer or ""
|
305
|
+
answer = result.answer or ""
|
296
306
|
done = True
|
297
307
|
|
298
308
|
self._update_session_memory(current_prompt, content)
|
@@ -309,13 +319,166 @@ class Agent(BaseModel):
|
|
309
319
|
self._emit_event("task_solve_end")
|
310
320
|
return answer
|
311
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
|
+
|
312
475
|
def _observe_response(self, content: str, iteration: int = 1) -> ObserveResponseResult:
|
313
476
|
"""Analyze the assistant's response and determine next steps (synchronous wrapper).
|
314
|
-
|
477
|
+
|
315
478
|
Args:
|
316
479
|
content: The response content to analyze
|
317
480
|
iteration: Current iteration number
|
318
|
-
|
481
|
+
|
319
482
|
Returns:
|
320
483
|
ObserveResponseResult with next steps information
|
321
484
|
"""
|
@@ -329,20 +492,34 @@ class Agent(BaseModel):
|
|
329
492
|
|
330
493
|
async def _async_observe_response(self, content: str, iteration: int = 1) -> ObserveResponseResult:
|
331
494
|
"""Analyze the assistant's response and determine next steps (asynchronous).
|
332
|
-
|
495
|
+
|
333
496
|
Args:
|
334
497
|
content: The response content to analyze
|
335
498
|
iteration: Current iteration number
|
336
|
-
|
499
|
+
|
337
500
|
Returns:
|
338
501
|
ObserveResponseResult with next steps information
|
339
502
|
"""
|
340
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
|
341
512
|
parsed_content = self._parse_tool_usage(content)
|
342
513
|
if not parsed_content:
|
343
|
-
|
344
|
-
|
345
|
-
|
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]
|
346
523
|
tool = self.tools.get(tool_name)
|
347
524
|
if not tool:
|
348
525
|
return self._handle_tool_not_found(tool_name)
|
@@ -361,22 +538,28 @@ class Agent(BaseModel):
|
|
361
538
|
variable_name = self.variable_store.add(response)
|
362
539
|
new_prompt = self._format_observation_response(response, executed_tool, variable_name, iteration)
|
363
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
|
364
543
|
return ObserveResponseResult(
|
365
544
|
next_prompt=new_prompt,
|
366
545
|
executed_tool=executed_tool,
|
367
|
-
answer=response if
|
546
|
+
answer=response if is_task_complete_answer else None,
|
368
547
|
)
|
548
|
+
|
549
|
+
# If no tools were executed, return original content
|
550
|
+
return ObserveResponseResult(next_prompt=content, executed_tool=None, answer=None)
|
551
|
+
|
369
552
|
except Exception as e:
|
370
553
|
return self._handle_error(e)
|
371
554
|
|
372
555
|
def _execute_tool(self, tool_name: str, tool: Tool, arguments_with_values: dict) -> tuple[str, Any]:
|
373
556
|
"""Execute a tool with validation if required (synchronous wrapper).
|
374
|
-
|
557
|
+
|
375
558
|
Args:
|
376
559
|
tool_name: Name of the tool to execute
|
377
560
|
tool: Tool instance
|
378
561
|
arguments_with_values: Tool arguments
|
379
|
-
|
562
|
+
|
380
563
|
Returns:
|
381
564
|
Tuple of (executed_tool_name, response)
|
382
565
|
"""
|
@@ -390,12 +573,12 @@ class Agent(BaseModel):
|
|
390
573
|
|
391
574
|
async def _async_execute_tool(self, tool_name: str, tool: Tool, arguments_with_values: dict) -> tuple[str, Any]:
|
392
575
|
"""Execute a tool with validation if required (asynchronous).
|
393
|
-
|
576
|
+
|
394
577
|
Args:
|
395
578
|
tool_name: Name of the tool to execute
|
396
579
|
tool: Tool instance
|
397
580
|
arguments_with_values: Tool arguments
|
398
|
-
|
581
|
+
|
399
582
|
Returns:
|
400
583
|
Tuple of (executed_tool_name, response)
|
401
584
|
"""
|
@@ -403,12 +586,12 @@ class Agent(BaseModel):
|
|
403
586
|
logger.info(f"Tool '{tool_name}' requires validation.")
|
404
587
|
validation_id = str(uuid.uuid4())
|
405
588
|
logger.info(f"Validation ID: {validation_id}")
|
406
|
-
|
589
|
+
|
407
590
|
self._emit_event(
|
408
591
|
"tool_execute_validation_start",
|
409
592
|
{
|
410
593
|
"validation_id": validation_id,
|
411
|
-
"tool_name": tool_name,
|
594
|
+
"tool_name": tool_name,
|
412
595
|
"arguments": arguments_with_values
|
413
596
|
},
|
414
597
|
)
|
@@ -453,8 +636,11 @@ class Agent(BaseModel):
|
|
453
636
|
if hasattr(tool, "async_execute") and callable(tool.async_execute):
|
454
637
|
response = await tool.async_execute(**converted_args)
|
455
638
|
else:
|
456
|
-
# Fall back to synchronous execution if async is not available
|
457
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
|
+
|
458
644
|
executed_tool = tool.name
|
459
645
|
except Exception as e:
|
460
646
|
response = f"Error executing tool: {tool_name}: {str(e)}\n"
|
@@ -467,41 +653,33 @@ class Agent(BaseModel):
|
|
467
653
|
|
468
654
|
async def _async_interpolate_variables(self, text: str, depth: int = 0) -> str:
|
469
655
|
"""Interpolate variables using $var$ syntax in the given text with recursion protection.
|
470
|
-
|
656
|
+
|
471
657
|
Args:
|
472
658
|
text: Text containing variable references
|
473
659
|
depth: Current recursion depth
|
474
|
-
|
660
|
+
|
475
661
|
Returns:
|
476
662
|
Text with variables interpolated
|
477
663
|
"""
|
478
664
|
if not isinstance(text, str):
|
479
665
|
return str(text)
|
480
|
-
|
666
|
+
|
481
667
|
if depth > MAX_INTERPOLATION_DEPTH:
|
482
668
|
logger.warning(f"Max interpolation depth ({MAX_INTERPOLATION_DEPTH}) reached, stopping recursion")
|
483
669
|
return text
|
484
|
-
|
670
|
+
|
485
671
|
try:
|
486
672
|
import re
|
487
|
-
|
488
|
-
# Process each variable in the store
|
673
|
+
|
489
674
|
for var in self.variable_store.keys():
|
490
|
-
# Properly escape the variable name for regex using re.escape
|
491
|
-
# but handle $ characters separately since they're part of our syntax
|
492
675
|
escaped_var = re.escape(var).replace('\\$', '$')
|
493
676
|
pattern = f"\\${escaped_var}\\$"
|
494
|
-
|
495
|
-
# Get variable value as string
|
496
677
|
replacement = str(self.variable_store[var])
|
497
|
-
|
498
|
-
# Replace all occurrences
|
499
678
|
text = re.sub(pattern, lambda m: replacement, text)
|
500
|
-
|
501
|
-
# Check if there are still variables to interpolate (for nested variables)
|
679
|
+
|
502
680
|
if '$' in text and depth < MAX_INTERPOLATION_DEPTH:
|
503
681
|
return await self._async_interpolate_variables(text, depth + 1)
|
504
|
-
|
682
|
+
|
505
683
|
return text
|
506
684
|
except Exception as e:
|
507
685
|
logger.error(f"Error in _async_interpolate_variables: {str(e)}")
|
@@ -509,10 +687,10 @@ class Agent(BaseModel):
|
|
509
687
|
|
510
688
|
def _interpolate_variables(self, text: str) -> str:
|
511
689
|
"""Interpolate variables using $var$ syntax in the given text (synchronous wrapper).
|
512
|
-
|
690
|
+
|
513
691
|
Args:
|
514
692
|
text: Text containing variable references
|
515
|
-
|
693
|
+
|
516
694
|
Returns:
|
517
695
|
Text with variables interpolated
|
518
696
|
"""
|
@@ -526,7 +704,7 @@ class Agent(BaseModel):
|
|
526
704
|
|
527
705
|
def _compact_memory_if_needed(self, current_prompt: str = "") -> None:
|
528
706
|
"""Compacts the memory if it exceeds the maximum occupancy (synchronous wrapper).
|
529
|
-
|
707
|
+
|
530
708
|
Args:
|
531
709
|
current_prompt: Current prompt to calculate token usage
|
532
710
|
"""
|
@@ -540,7 +718,7 @@ class Agent(BaseModel):
|
|
540
718
|
|
541
719
|
async def _async_compact_memory_if_needed(self, current_prompt: str = "") -> None:
|
542
720
|
"""Compacts the memory if it exceeds the maximum occupancy or token limit.
|
543
|
-
|
721
|
+
|
544
722
|
Args:
|
545
723
|
current_prompt: Current prompt to calculate token usage
|
546
724
|
"""
|
@@ -553,7 +731,7 @@ class Agent(BaseModel):
|
|
553
731
|
and self.current_iteration % self.compact_every_n_iterations == 0
|
554
732
|
)
|
555
733
|
should_compact_by_token_limit = (
|
556
|
-
self.max_tokens_working_memory is not None
|
734
|
+
self.max_tokens_working_memory is not None
|
557
735
|
and self.total_tokens > self.max_tokens_working_memory
|
558
736
|
)
|
559
737
|
|
@@ -582,11 +760,10 @@ class Agent(BaseModel):
|
|
582
760
|
|
583
761
|
async def _async_compact_memory_with_summary(self) -> str:
|
584
762
|
"""Generate a summary and compact memory asynchronously.
|
585
|
-
|
763
|
+
|
586
764
|
Returns:
|
587
765
|
Generated summary text
|
588
766
|
"""
|
589
|
-
# Format conversation history for the template
|
590
767
|
memory_copy = self.memory.memory.copy()
|
591
768
|
|
592
769
|
if len(memory_copy) < 3:
|
@@ -595,20 +772,18 @@ class Agent(BaseModel):
|
|
595
772
|
|
596
773
|
user_message = memory_copy.pop()
|
597
774
|
assistant_message = memory_copy.pop()
|
598
|
-
|
599
|
-
|
600
|
-
prompt_summary = self._render_template('memory_compaction_prompt.j2',
|
775
|
+
|
776
|
+
prompt_summary = self._render_template('memory_compaction_prompt.j2',
|
601
777
|
conversation_history="\n\n".join(
|
602
|
-
f"[{msg.role.upper()}]: {msg.content}"
|
778
|
+
f"[{msg.role.upper()}]: {msg.content}"
|
603
779
|
for msg in memory_copy
|
604
780
|
))
|
605
|
-
|
781
|
+
|
606
782
|
summary = await self.model.async_generate_with_history(messages_history=memory_copy, prompt=prompt_summary)
|
607
|
-
|
608
|
-
# Remove last system message if present
|
783
|
+
|
609
784
|
if memory_copy and memory_copy[-1].role == "system":
|
610
785
|
memory_copy.pop()
|
611
|
-
|
786
|
+
|
612
787
|
memory_copy.append(Message(role="user", content=summary.response))
|
613
788
|
memory_copy.append(assistant_message)
|
614
789
|
memory_copy.append(user_message)
|
@@ -617,10 +792,10 @@ class Agent(BaseModel):
|
|
617
792
|
|
618
793
|
def _generate_task_summary(self, content: str) -> str:
|
619
794
|
"""Generate a concise task-focused summary (synchronous wrapper).
|
620
|
-
|
795
|
+
|
621
796
|
Args:
|
622
797
|
content: The content to summarize
|
623
|
-
|
798
|
+
|
624
799
|
Returns:
|
625
800
|
Generated task summary
|
626
801
|
"""
|
@@ -644,7 +819,7 @@ class Agent(BaseModel):
|
|
644
819
|
try:
|
645
820
|
if len(content) < 1024 * 4:
|
646
821
|
return content
|
647
|
-
|
822
|
+
|
648
823
|
prompt = self._render_template('task_summary_prompt.j2', content=content)
|
649
824
|
result = await self.model.async_generate(prompt=prompt)
|
650
825
|
logger.debug(f"Generated summary: {result.response}")
|
@@ -655,7 +830,7 @@ class Agent(BaseModel):
|
|
655
830
|
|
656
831
|
def _reset_session(self, task_to_solve: str = "", max_iterations: int = 30, clear_memory: bool = True) -> None:
|
657
832
|
"""Reset the agent's session.
|
658
|
-
|
833
|
+
|
659
834
|
Args:
|
660
835
|
task_to_solve: New task to solve
|
661
836
|
max_iterations: Maximum number of iterations
|
@@ -675,7 +850,7 @@ class Agent(BaseModel):
|
|
675
850
|
|
676
851
|
def _update_total_tokens(self, message_history: list[Message], prompt: str) -> None:
|
677
852
|
"""Update the total tokens count based on message history and prompt.
|
678
|
-
|
853
|
+
|
679
854
|
Args:
|
680
855
|
message_history: List of messages
|
681
856
|
prompt: Current prompt
|
@@ -684,7 +859,7 @@ class Agent(BaseModel):
|
|
684
859
|
|
685
860
|
def _emit_event(self, event_type: str, data: dict[str, Any] | None = None) -> None:
|
686
861
|
"""Emit an event with system context and optional additional data.
|
687
|
-
|
862
|
+
|
688
863
|
Args:
|
689
864
|
event_type: Type of event
|
690
865
|
data: Additional event data
|
@@ -702,10 +877,10 @@ class Agent(BaseModel):
|
|
702
877
|
|
703
878
|
def _parse_tool_usage(self, content: str) -> dict:
|
704
879
|
"""Extract tool usage from the response content.
|
705
|
-
|
880
|
+
|
706
881
|
Args:
|
707
882
|
content: Response content
|
708
|
-
|
883
|
+
|
709
884
|
Returns:
|
710
885
|
Dictionary mapping tool names to inputs
|
711
886
|
"""
|
@@ -718,30 +893,39 @@ class Agent(BaseModel):
|
|
718
893
|
tool_names = self.tools.tool_names()
|
719
894
|
|
720
895
|
if action:
|
721
|
-
|
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
|
722
904
|
else:
|
723
905
|
return xml_parser.extract_elements(text=content, element_names=tool_names)
|
724
906
|
|
725
|
-
def _parse_tool_arguments(self, tool: Tool, tool_input: str) -> dict:
|
907
|
+
def _parse_tool_arguments(self, tool: Tool, tool_input: str | dict) -> dict:
|
726
908
|
"""Parse the tool arguments from the tool input.
|
727
|
-
|
909
|
+
|
728
910
|
Args:
|
729
911
|
tool: Tool instance
|
730
|
-
tool_input: Raw tool input text
|
731
|
-
|
912
|
+
tool_input: Raw tool input text or pre-parsed dict
|
913
|
+
|
732
914
|
Returns:
|
733
915
|
Dictionary of parsed arguments
|
734
916
|
"""
|
917
|
+
if isinstance(tool_input, dict):
|
918
|
+
return tool_input # Already parsed from XML
|
735
919
|
tool_parser = ToolParser(tool=tool)
|
736
920
|
return tool_parser.parse(tool_input)
|
737
921
|
|
738
922
|
def _is_repeated_tool_call(self, tool_name: str, arguments_with_values: dict) -> bool:
|
739
923
|
"""Check if the tool call is repeated.
|
740
|
-
|
924
|
+
|
741
925
|
Args:
|
742
926
|
tool_name: Name of the tool
|
743
927
|
arguments_with_values: Tool arguments
|
744
|
-
|
928
|
+
|
745
929
|
Returns:
|
746
930
|
True if call is repeated, False otherwise
|
747
931
|
"""
|
@@ -767,7 +951,7 @@ class Agent(BaseModel):
|
|
767
951
|
|
768
952
|
def _handle_no_tool_usage(self) -> ObserveResponseResult:
|
769
953
|
"""Handle the case where no tool usage is found in the response.
|
770
|
-
|
954
|
+
|
771
955
|
Returns:
|
772
956
|
ObserveResponseResult with error message
|
773
957
|
"""
|
@@ -777,10 +961,10 @@ class Agent(BaseModel):
|
|
777
961
|
|
778
962
|
def _handle_tool_not_found(self, tool_name: str) -> ObserveResponseResult:
|
779
963
|
"""Handle the case where the tool is not found.
|
780
|
-
|
964
|
+
|
781
965
|
Args:
|
782
966
|
tool_name: Name of the tool
|
783
|
-
|
967
|
+
|
784
968
|
Returns:
|
785
969
|
ObserveResponseResult with error message
|
786
970
|
"""
|
@@ -793,11 +977,11 @@ class Agent(BaseModel):
|
|
793
977
|
|
794
978
|
def _handle_repeated_tool_call(self, tool_name: str, arguments_with_values: dict) -> tuple[str, str]:
|
795
979
|
"""Handle the case where a tool call is repeated.
|
796
|
-
|
980
|
+
|
797
981
|
Args:
|
798
982
|
tool_name: Name of the tool
|
799
983
|
arguments_with_values: Tool arguments
|
800
|
-
|
984
|
+
|
801
985
|
Returns:
|
802
986
|
Tuple of (executed_tool_name, error_message)
|
803
987
|
"""
|
@@ -812,10 +996,10 @@ class Agent(BaseModel):
|
|
812
996
|
|
813
997
|
def _handle_tool_execution_failure(self, response: str) -> ObserveResponseResult:
|
814
998
|
"""Handle the case where tool execution fails.
|
815
|
-
|
999
|
+
|
816
1000
|
Args:
|
817
1001
|
response: Error response
|
818
|
-
|
1002
|
+
|
819
1003
|
Returns:
|
820
1004
|
ObserveResponseResult with error message
|
821
1005
|
"""
|
@@ -827,10 +1011,10 @@ class Agent(BaseModel):
|
|
827
1011
|
|
828
1012
|
def _handle_error(self, error: Exception) -> ObserveResponseResult:
|
829
1013
|
"""Handle any exceptions that occur during response observation.
|
830
|
-
|
1014
|
+
|
831
1015
|
Args:
|
832
1016
|
error: Exception that occurred
|
833
|
-
|
1017
|
+
|
834
1018
|
Returns:
|
835
1019
|
ObserveResponseResult with error message
|
836
1020
|
"""
|
@@ -840,18 +1024,202 @@ class Agent(BaseModel):
|
|
840
1024
|
executed_tool=None,
|
841
1025
|
answer=None,
|
842
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
|
843
1211
|
|
844
1212
|
def _format_observation_response(
|
845
1213
|
self, response: str, last_executed_tool: str, variable_name: str, iteration: int
|
846
1214
|
) -> str:
|
847
1215
|
"""Format the observation response with the given response, variable name, and iteration.
|
848
|
-
|
1216
|
+
|
849
1217
|
Args:
|
850
1218
|
response: Tool execution response
|
851
1219
|
last_executed_tool: Name of last executed tool
|
852
1220
|
variable_name: Name of variable storing response
|
853
1221
|
iteration: Current iteration number
|
854
|
-
|
1222
|
+
|
855
1223
|
Returns:
|
856
1224
|
Formatted observation response
|
857
1225
|
"""
|
@@ -864,7 +1232,7 @@ class Agent(BaseModel):
|
|
864
1232
|
|
865
1233
|
tools_prompt = self._get_tools_names_prompt()
|
866
1234
|
variables_prompt = self._get_variable_prompt()
|
867
|
-
|
1235
|
+
|
868
1236
|
formatted_response = self._render_template(
|
869
1237
|
'observation_response_format.j2',
|
870
1238
|
iteration=iteration,
|
@@ -890,7 +1258,7 @@ class Agent(BaseModel):
|
|
890
1258
|
"""
|
891
1259
|
tools_prompt = self._get_tools_names_prompt()
|
892
1260
|
variables_prompt = self._get_variable_prompt()
|
893
|
-
|
1261
|
+
|
894
1262
|
prompt_task = self._render_template(
|
895
1263
|
'task_prompt.j2',
|
896
1264
|
task=task,
|
@@ -901,16 +1269,77 @@ class Agent(BaseModel):
|
|
901
1269
|
|
902
1270
|
def _get_tools_names_prompt(self) -> str:
|
903
1271
|
"""Construct a detailed prompt that lists the available tools for task execution.
|
904
|
-
|
1272
|
+
|
905
1273
|
Returns:
|
906
1274
|
Formatted tools prompt
|
907
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
|
908
1283
|
tool_names = ', '.join(self.tools.tool_names())
|
909
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
|
910
1339
|
|
911
1340
|
def _get_variable_prompt(self) -> str:
|
912
1341
|
"""Construct a prompt that explains how to use variables.
|
913
|
-
|
1342
|
+
|
914
1343
|
Returns:
|
915
1344
|
Formatted variables prompt
|
916
1345
|
"""
|
@@ -919,7 +1348,7 @@ class Agent(BaseModel):
|
|
919
1348
|
|
920
1349
|
def _calculate_context_occupancy(self) -> float:
|
921
1350
|
"""Calculate the number of tokens in percentages for prompt and completion.
|
922
|
-
|
1351
|
+
|
923
1352
|
Returns:
|
924
1353
|
Percentage of context window occupied
|
925
1354
|
"""
|
@@ -947,7 +1376,7 @@ class Agent(BaseModel):
|
|
947
1376
|
|
948
1377
|
def update_model(self, new_model_name: str) -> None:
|
949
1378
|
"""Update the model name and recreate the model instance.
|
950
|
-
|
1379
|
+
|
951
1380
|
Args:
|
952
1381
|
new_model_name: New model name to use
|
953
1382
|
"""
|
@@ -956,98 +1385,86 @@ class Agent(BaseModel):
|
|
956
1385
|
|
957
1386
|
def add_tool(self, tool: Tool) -> None:
|
958
1387
|
"""Add a new tool to the agent's tool manager.
|
959
|
-
|
1388
|
+
|
960
1389
|
Args:
|
961
1390
|
tool: The tool instance to add
|
962
|
-
|
1391
|
+
|
963
1392
|
Raises:
|
964
1393
|
ValueError: If a tool with the same name already exists
|
965
1394
|
"""
|
966
1395
|
if tool.name in self.tools.tool_names():
|
967
1396
|
raise ValueError(f"Tool with name '{tool.name}' already exists")
|
968
|
-
|
1397
|
+
|
969
1398
|
self.tools.add(tool)
|
970
|
-
# Update tools markdown in config
|
971
1399
|
self.config = AgentConfig(
|
972
1400
|
environment_details=self.config.environment_details,
|
973
1401
|
tools_markdown=self.tools.to_markdown(),
|
974
1402
|
system_prompt=self.config.system_prompt,
|
975
1403
|
)
|
976
1404
|
logger.debug(f"Added tool: {tool.name}")
|
977
|
-
|
1405
|
+
|
978
1406
|
def remove_tool(self, tool_name: str) -> None:
|
979
1407
|
"""Remove a tool from the agent's tool manager.
|
980
|
-
|
1408
|
+
|
981
1409
|
Args:
|
982
1410
|
tool_name: Name of the tool to remove
|
983
|
-
|
1411
|
+
|
984
1412
|
Raises:
|
985
1413
|
ValueError: If tool doesn't exist or is TaskCompleteTool
|
986
1414
|
"""
|
987
1415
|
if tool_name not in self.tools.tool_names():
|
988
1416
|
raise ValueError(f"Tool '{tool_name}' does not exist")
|
989
|
-
|
1417
|
+
|
990
1418
|
tool = self.tools.get(tool_name)
|
991
1419
|
if isinstance(tool, TaskCompleteTool):
|
992
1420
|
raise ValueError("Cannot remove TaskCompleteTool as it is required")
|
993
|
-
|
1421
|
+
|
994
1422
|
self.tools.remove(tool_name)
|
995
|
-
# Update tools markdown in config
|
996
1423
|
self.config = AgentConfig(
|
997
1424
|
environment_details=self.config.environment_details,
|
998
1425
|
tools_markdown=self.tools.to_markdown(),
|
999
1426
|
system_prompt=self.config.system_prompt,
|
1000
1427
|
)
|
1001
1428
|
logger.debug(f"Removed tool: {tool_name}")
|
1002
|
-
|
1429
|
+
|
1003
1430
|
def set_tools(self, tools: list[Tool]) -> None:
|
1004
1431
|
"""Set/replace all tools for the agent.
|
1005
|
-
|
1432
|
+
|
1006
1433
|
Args:
|
1007
1434
|
tools: List of tool instances to set
|
1008
|
-
|
1435
|
+
|
1009
1436
|
Note:
|
1010
1437
|
TaskCompleteTool will be automatically added if not present
|
1011
1438
|
"""
|
1012
|
-
# Ensure TaskCompleteTool is present
|
1013
1439
|
if not any(isinstance(t, TaskCompleteTool) for t in tools):
|
1014
1440
|
tools.append(TaskCompleteTool())
|
1015
|
-
|
1016
|
-
# Create new tool manager and add tools
|
1441
|
+
|
1017
1442
|
tool_manager = ToolManager()
|
1018
1443
|
tool_manager.add_list(tools)
|
1019
1444
|
self.tools = tool_manager
|
1020
|
-
|
1021
|
-
# Update config with new tools markdown
|
1445
|
+
|
1022
1446
|
self.config = AgentConfig(
|
1023
1447
|
environment_details=self.config.environment_details,
|
1024
1448
|
tools_markdown=self.tools.to_markdown(),
|
1025
1449
|
system_prompt=self.config.system_prompt,
|
1026
1450
|
)
|
1027
1451
|
logger.debug(f"Set {len(tools)} tools")
|
1028
|
-
|
1452
|
+
|
1029
1453
|
def _render_template(self, template_name: str, **kwargs) -> str:
|
1030
1454
|
"""Render a Jinja2 template with the provided variables.
|
1031
|
-
|
1455
|
+
|
1032
1456
|
Args:
|
1033
1457
|
template_name: Name of the template file (without directory path)
|
1034
1458
|
**kwargs: Variables to pass to the template
|
1035
|
-
|
1459
|
+
|
1036
1460
|
Returns:
|
1037
1461
|
str: The rendered template
|
1038
1462
|
"""
|
1039
1463
|
try:
|
1040
|
-
# Get the directory where this file is located
|
1041
1464
|
current_dir = Path(os.path.dirname(os.path.abspath(__file__)))
|
1042
|
-
|
1043
|
-
# Set up Jinja2 environment
|
1044
1465
|
template_dir = current_dir / 'prompts'
|
1045
1466
|
env = Environment(loader=FileSystemLoader(template_dir))
|
1046
|
-
|
1047
|
-
# Load the template
|
1048
1467
|
template = env.get_template(template_name)
|
1049
|
-
|
1050
|
-
# Render the template with the provided variables
|
1051
1468
|
return template.render(**kwargs)
|
1052
1469
|
except Exception as e:
|
1053
1470
|
logger.error(f"Error rendering template {template_name}: {str(e)}")
|