quantalogic 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- quantalogic/__init__.py +20 -0
- quantalogic/agent.py +638 -0
- quantalogic/agent_config.py +138 -0
- quantalogic/coding_agent.py +83 -0
- quantalogic/event_emitter.py +223 -0
- quantalogic/generative_model.py +226 -0
- quantalogic/interactive_text_editor.py +190 -0
- quantalogic/main.py +185 -0
- quantalogic/memory.py +217 -0
- quantalogic/model_names.py +19 -0
- quantalogic/print_event.py +66 -0
- quantalogic/prompts.py +99 -0
- quantalogic/server/__init__.py +3 -0
- quantalogic/server/agent_server.py +633 -0
- quantalogic/server/models.py +60 -0
- quantalogic/server/routes.py +117 -0
- quantalogic/server/state.py +199 -0
- quantalogic/server/static/js/event_visualizer.js +430 -0
- quantalogic/server/static/js/quantalogic.js +571 -0
- quantalogic/server/templates/index.html +134 -0
- quantalogic/tool_manager.py +68 -0
- quantalogic/tools/__init__.py +46 -0
- quantalogic/tools/agent_tool.py +88 -0
- quantalogic/tools/download_http_file_tool.py +64 -0
- quantalogic/tools/edit_whole_content_tool.py +70 -0
- quantalogic/tools/elixir_tool.py +240 -0
- quantalogic/tools/execute_bash_command_tool.py +116 -0
- quantalogic/tools/input_question_tool.py +57 -0
- quantalogic/tools/language_handlers/__init__.py +21 -0
- quantalogic/tools/language_handlers/c_handler.py +33 -0
- quantalogic/tools/language_handlers/cpp_handler.py +33 -0
- quantalogic/tools/language_handlers/go_handler.py +33 -0
- quantalogic/tools/language_handlers/java_handler.py +37 -0
- quantalogic/tools/language_handlers/javascript_handler.py +42 -0
- quantalogic/tools/language_handlers/python_handler.py +29 -0
- quantalogic/tools/language_handlers/rust_handler.py +33 -0
- quantalogic/tools/language_handlers/scala_handler.py +33 -0
- quantalogic/tools/language_handlers/typescript_handler.py +42 -0
- quantalogic/tools/list_directory_tool.py +123 -0
- quantalogic/tools/llm_tool.py +119 -0
- quantalogic/tools/markitdown_tool.py +105 -0
- quantalogic/tools/nodejs_tool.py +515 -0
- quantalogic/tools/python_tool.py +469 -0
- quantalogic/tools/read_file_block_tool.py +140 -0
- quantalogic/tools/read_file_tool.py +79 -0
- quantalogic/tools/replace_in_file_tool.py +300 -0
- quantalogic/tools/ripgrep_tool.py +353 -0
- quantalogic/tools/search_definition_names.py +419 -0
- quantalogic/tools/task_complete_tool.py +35 -0
- quantalogic/tools/tool.py +146 -0
- quantalogic/tools/unified_diff_tool.py +387 -0
- quantalogic/tools/write_file_tool.py +97 -0
- quantalogic/utils/__init__.py +17 -0
- quantalogic/utils/ask_user_validation.py +12 -0
- quantalogic/utils/download_http_file.py +77 -0
- quantalogic/utils/get_coding_environment.py +15 -0
- quantalogic/utils/get_environment.py +26 -0
- quantalogic/utils/get_quantalogic_rules_content.py +19 -0
- quantalogic/utils/git_ls.py +121 -0
- quantalogic/utils/read_file.py +54 -0
- quantalogic/utils/read_http_text_content.py +101 -0
- quantalogic/xml_parser.py +242 -0
- quantalogic/xml_tool_parser.py +99 -0
- quantalogic-0.2.0.dist-info/LICENSE +201 -0
- quantalogic-0.2.0.dist-info/METADATA +1034 -0
- quantalogic-0.2.0.dist-info/RECORD +68 -0
- quantalogic-0.2.0.dist-info/WHEEL +4 -0
- quantalogic-0.2.0.dist-info/entry_points.txt +3 -0
quantalogic/__init__.py
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
# QuantaLogic package initialization
|
2
|
+
import warnings
|
3
|
+
|
4
|
+
# Suppress specific warnings related to Pydantic's V2 configuration changes
|
5
|
+
warnings.filterwarnings(
|
6
|
+
"ignore",
|
7
|
+
category=UserWarning,
|
8
|
+
module="pydantic.*",
|
9
|
+
message=".*config keys have changed in V2:.*|.*'fields' config key is removed in V2.*",
|
10
|
+
)
|
11
|
+
|
12
|
+
|
13
|
+
from .agent import Agent # noqa: E402
|
14
|
+
from .event_emitter import EventEmitter # noqa: E402
|
15
|
+
from .memory import AgentMemory, VariableMemory # noqa: E402
|
16
|
+
from .print_event import console_print_events # noqa: E402
|
17
|
+
|
18
|
+
"""QuantaLogic package for AI-powered generative models."""
|
19
|
+
|
20
|
+
__all__ = ["Agent", "EventEmitter", "AgentMemory", "VariableMemory", "console_print_events"]
|
quantalogic/agent.py
ADDED
@@ -0,0 +1,638 @@
|
|
1
|
+
"""Enhanced QuantaLogic agent implementing the ReAct framework."""
|
2
|
+
|
3
|
+
import os
|
4
|
+
import sys
|
5
|
+
from collections.abc import Callable
|
6
|
+
from datetime import datetime
|
7
|
+
from typing import Any
|
8
|
+
|
9
|
+
from loguru import logger
|
10
|
+
from pydantic import BaseModel, ConfigDict
|
11
|
+
|
12
|
+
from quantalogic.event_emitter import EventEmitter
|
13
|
+
from quantalogic.generative_model import GenerativeModel
|
14
|
+
from quantalogic.memory import AgentMemory, Message, VariableMemory
|
15
|
+
from quantalogic.prompts import system_prompt
|
16
|
+
from quantalogic.tool_manager import ToolManager
|
17
|
+
from quantalogic.tools.task_complete_tool import TaskCompleteTool
|
18
|
+
from quantalogic.tools.tool import Tool
|
19
|
+
from quantalogic.utils import get_environment
|
20
|
+
from quantalogic.utils.ask_user_validation import console_ask_for_user_validation
|
21
|
+
from quantalogic.xml_parser import ToleranceXMLParser
|
22
|
+
from quantalogic.xml_tool_parser import ToolParser
|
23
|
+
|
24
|
+
# Configure logger based on environment variable
|
25
|
+
log_level = os.getenv("LOG_LEVEL", "ERROR")
|
26
|
+
logger.remove()
|
27
|
+
logger.add(sys.stderr, level=log_level)
|
28
|
+
|
29
|
+
|
30
|
+
# Maximum ratio occupancy of the occupied memory
|
31
|
+
MAX_OCCUPANCY = 90.0
|
32
|
+
|
33
|
+
# Maximum response length in characters
|
34
|
+
MAX_RESPONSE_LENGTH = 1024 * 32
|
35
|
+
|
36
|
+
DEFAULT_MAX_INPUT_TOKENS = 128 * 1024
|
37
|
+
DEFAULT_MAX_OUTPUT_TOKENS = 4096
|
38
|
+
|
39
|
+
|
40
|
+
class AgentConfig(BaseModel):
|
41
|
+
"""Configuration settings for the Agent."""
|
42
|
+
|
43
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
44
|
+
|
45
|
+
environment_details: str
|
46
|
+
tools_markdown: str
|
47
|
+
system_prompt: str
|
48
|
+
|
49
|
+
|
50
|
+
class ObserveResponseResult(BaseModel):
|
51
|
+
"""Represents the result of observing the assistant's response."""
|
52
|
+
|
53
|
+
model_config = ConfigDict(extra="forbid", frozen=True)
|
54
|
+
|
55
|
+
next_prompt: str
|
56
|
+
executed_tool: str | None = None
|
57
|
+
answer: str | None = None
|
58
|
+
|
59
|
+
|
60
|
+
class Agent(BaseModel):
|
61
|
+
"""Enhanced QuantaLogic agent implementing ReAct framework."""
|
62
|
+
|
63
|
+
model_config = ConfigDict(arbitrary_types_allowed=True, validate_assignment=True, extra="forbid")
|
64
|
+
|
65
|
+
specific_expertise: str
|
66
|
+
model: GenerativeModel
|
67
|
+
memory: AgentMemory = AgentMemory()
|
68
|
+
variable_store: VariableMemory = VariableMemory()
|
69
|
+
tools: ToolManager = ToolManager()
|
70
|
+
event_emitter: EventEmitter = EventEmitter()
|
71
|
+
config: AgentConfig
|
72
|
+
task_to_solve: str
|
73
|
+
ask_for_user_validation: Callable[[str], bool] = console_ask_for_user_validation
|
74
|
+
last_tool_call: dict[str, Any] = {} # Stores the last tool call information
|
75
|
+
total_tokens: int = 0 # Total tokens in the conversation
|
76
|
+
current_iteration: int = 0
|
77
|
+
max_input_tokens: int = DEFAULT_MAX_INPUT_TOKENS
|
78
|
+
max_output_tokens: int = DEFAULT_MAX_OUTPUT_TOKENS
|
79
|
+
max_iterations: int = 30
|
80
|
+
system_prompt: str = ""
|
81
|
+
|
82
|
+
def __init__(
|
83
|
+
self,
|
84
|
+
model_name: str = "ollama/qwen2.5-coder:14b",
|
85
|
+
memory: AgentMemory = AgentMemory(),
|
86
|
+
tools: list[Tool] = [TaskCompleteTool()],
|
87
|
+
ask_for_user_validation: Callable[[str], bool] = console_ask_for_user_validation,
|
88
|
+
task_to_solve: str = "",
|
89
|
+
specific_expertise: str = "General AI assistant with coding and problem-solving capabilities",
|
90
|
+
get_environment: Callable[[], str] = get_environment,
|
91
|
+
):
|
92
|
+
"""Initialize the agent with model, memory, tools, and configurations."""
|
93
|
+
try:
|
94
|
+
# Add TaskCompleteTool to the tools list if not already present
|
95
|
+
if TaskCompleteTool() not in tools:
|
96
|
+
tools.append(TaskCompleteTool())
|
97
|
+
|
98
|
+
tool_manager = ToolManager(tools={tool.name: tool for tool in tools})
|
99
|
+
environment = get_environment()
|
100
|
+
tools_markdown = tool_manager.to_markdown()
|
101
|
+
|
102
|
+
system_prompt_text = system_prompt(
|
103
|
+
tools=tools_markdown, environment=environment, expertise=specific_expertise
|
104
|
+
)
|
105
|
+
|
106
|
+
config = AgentConfig(
|
107
|
+
environment_details=environment,
|
108
|
+
tools_markdown=tools_markdown,
|
109
|
+
system_prompt=system_prompt_text,
|
110
|
+
)
|
111
|
+
|
112
|
+
super().__init__(
|
113
|
+
model=GenerativeModel(model=model_name),
|
114
|
+
memory=memory,
|
115
|
+
variable_store=VariableMemory(),
|
116
|
+
tools=tool_manager,
|
117
|
+
config=config,
|
118
|
+
ask_for_user_validation=ask_for_user_validation,
|
119
|
+
task_to_solve=task_to_solve,
|
120
|
+
specific_expertise=specific_expertise,
|
121
|
+
)
|
122
|
+
logger.info("Agent initialized successfully.")
|
123
|
+
except Exception as e:
|
124
|
+
logger.error(f"Failed to initialize agent: {str(e)}")
|
125
|
+
raise
|
126
|
+
|
127
|
+
def solve_task(self, task: str, max_iterations: int = 30) -> str:
|
128
|
+
"""Solve the given task using the ReAct framework.
|
129
|
+
|
130
|
+
Args:
|
131
|
+
task (str): The task description.
|
132
|
+
max_iterations (int, optional): Maximum number of iterations to attempt solving the task.
|
133
|
+
Defaults to 30 to prevent infinite loops and ensure timely task completion.
|
134
|
+
|
135
|
+
Returns:
|
136
|
+
str: The final response after task completion.
|
137
|
+
"""
|
138
|
+
self._reset_session(task_to_solve=task, max_iterations=max_iterations)
|
139
|
+
|
140
|
+
# Add system prompt to memory
|
141
|
+
self.memory.add(Message(role="system", content=self.config.system_prompt))
|
142
|
+
|
143
|
+
self._emit_event(
|
144
|
+
"session_start",
|
145
|
+
{"system_prompt": self.config.system_prompt, "content": task},
|
146
|
+
)
|
147
|
+
|
148
|
+
self.max_output_tokens = self.model.get_model_max_output_tokens() or DEFAULT_MAX_OUTPUT_TOKENS
|
149
|
+
self.max_input_tokens = self.model.get_model_max_input_tokens() or DEFAULT_MAX_INPUT_TOKENS
|
150
|
+
|
151
|
+
done = False
|
152
|
+
current_prompt = self._prepare_prompt_task(task)
|
153
|
+
|
154
|
+
self.current_iteration = 1
|
155
|
+
|
156
|
+
# Emit event: Task Solve Start
|
157
|
+
self._emit_event(
|
158
|
+
"task_solve_start",
|
159
|
+
{"initial_prompt": current_prompt, "task": task},
|
160
|
+
)
|
161
|
+
|
162
|
+
answer: str = ""
|
163
|
+
|
164
|
+
while not done:
|
165
|
+
try:
|
166
|
+
self._update_total_tokens(message_history=self.memory.memory, prompt=current_prompt)
|
167
|
+
|
168
|
+
# Emit event: Task Think Start after updating total tokens
|
169
|
+
self._emit_event("task_think_start")
|
170
|
+
|
171
|
+
self._compact_memory_if_needed(current_prompt)
|
172
|
+
|
173
|
+
result = self.model.generate_with_history(messages_history=self.memory.memory, prompt=current_prompt)
|
174
|
+
|
175
|
+
content = result.response
|
176
|
+
token_usage = result.usage
|
177
|
+
self.total_tokens = token_usage.total_tokens
|
178
|
+
|
179
|
+
# Emit event: Task Think End
|
180
|
+
self._emit_event(
|
181
|
+
"task_think_end",
|
182
|
+
{
|
183
|
+
"response": content,
|
184
|
+
},
|
185
|
+
)
|
186
|
+
|
187
|
+
# Process the assistant's response
|
188
|
+
result = self._observe_response(result.response, iteration=self.current_iteration)
|
189
|
+
|
190
|
+
current_prompt = result.next_prompt
|
191
|
+
|
192
|
+
if result.executed_tool == "task_complete":
|
193
|
+
self._emit_event(
|
194
|
+
"task_complete",
|
195
|
+
{
|
196
|
+
"response": result.answer,
|
197
|
+
},
|
198
|
+
)
|
199
|
+
answer = result.answer
|
200
|
+
done = True
|
201
|
+
|
202
|
+
self._update_session_memory(current_prompt, content)
|
203
|
+
|
204
|
+
self.current_iteration += 1
|
205
|
+
if self.current_iteration >= self.max_iterations:
|
206
|
+
done = True
|
207
|
+
self._emit_event("error_max_iterations_reached")
|
208
|
+
|
209
|
+
except Exception as e:
|
210
|
+
logger.error(f"Error during task solving: {str(e)}")
|
211
|
+
# Optionally, decide to continue or break based on exception type
|
212
|
+
answer = f"Error: {str(e)}"
|
213
|
+
done = True
|
214
|
+
|
215
|
+
# Emit event: Task Solve End
|
216
|
+
self._emit_event("task_solve_end")
|
217
|
+
|
218
|
+
return answer
|
219
|
+
|
220
|
+
def _reset_session(self, task_to_solve: str = "", max_iterations: int = 30):
|
221
|
+
"""Reset the agent's session."""
|
222
|
+
self.task_to_solve = task_to_solve
|
223
|
+
self.memory.reset()
|
224
|
+
self.variable_store.reset()
|
225
|
+
self.total_tokens = 0
|
226
|
+
self.current_iteration = 0
|
227
|
+
self.max_output_tokens = self.model.get_model_max_output_tokens() or DEFAULT_MAX_OUTPUT_TOKENS
|
228
|
+
self.max_input_tokens = self.model.get_model_max_input_tokens() or DEFAULT_MAX_INPUT_TOKENS
|
229
|
+
self.max_iterations = max_iterations
|
230
|
+
|
231
|
+
def _update_total_tokens(self, message_history: list[Message], prompt: str) -> None:
|
232
|
+
self.total_tokens = self.model.token_counter_with_history(message_history, prompt)
|
233
|
+
|
234
|
+
def _compact_memory_if_needed(self, current_prompt: str = ""):
|
235
|
+
"""Compacts the memory if it exceeds the maximum occupancy."""
|
236
|
+
ratio_occupied = self._calculate_context_occupancy()
|
237
|
+
if ratio_occupied >= MAX_OCCUPANCY:
|
238
|
+
self._emit_event("memory_full")
|
239
|
+
self.memory.compact()
|
240
|
+
self.total_tokens = self.model.token_counter_with_history(self.memory.memory, current_prompt)
|
241
|
+
self._emit_event("memory_compacted")
|
242
|
+
|
243
|
+
def _emit_event(self, event_type: str, data: dict[str, Any] | None = None) -> None:
|
244
|
+
"""
|
245
|
+
Emit an event with system context and optional additional data.
|
246
|
+
|
247
|
+
Why: Provides a standardized way to track and log system events
|
248
|
+
with consistent contextual information.
|
249
|
+
"""
|
250
|
+
# Use empty dict as default to avoid mutable default argument
|
251
|
+
event_data = {
|
252
|
+
"iteration": self.current_iteration,
|
253
|
+
"total_tokens": self.total_tokens,
|
254
|
+
"context_occupancy": self._calculate_context_occupancy(),
|
255
|
+
"max_input_tokens": self.max_input_tokens,
|
256
|
+
"max_output_tokens": self.max_output_tokens,
|
257
|
+
}
|
258
|
+
|
259
|
+
# Merge additional data if provided
|
260
|
+
if data:
|
261
|
+
event_data.update(data)
|
262
|
+
|
263
|
+
self.event_emitter.emit(event_type, event_data)
|
264
|
+
|
265
|
+
def _observe_response(self, content: str, iteration: int = 1) -> ObserveResponseResult:
|
266
|
+
"""Analyze the assistant's response and determine next steps.
|
267
|
+
|
268
|
+
Args:
|
269
|
+
content (str): The assistant's response content.
|
270
|
+
iteration (int, optional): The current iteration number of task solving.
|
271
|
+
Helps track the progress and prevent infinite loops. Defaults to 1.
|
272
|
+
|
273
|
+
Returns:
|
274
|
+
ObserveResponseResult: A result indicating if the task is done and the next prompt.
|
275
|
+
"""
|
276
|
+
try:
|
277
|
+
parsed_content = self._parse_tool_usage(content)
|
278
|
+
if not parsed_content:
|
279
|
+
return self._handle_no_tool_usage()
|
280
|
+
|
281
|
+
for tool_name, tool_input in parsed_content.items():
|
282
|
+
tool = self.tools.get(tool_name)
|
283
|
+
if not tool:
|
284
|
+
return self._handle_tool_not_found(tool_name)
|
285
|
+
|
286
|
+
arguments_with_values = self._parse_tool_arguments(tool, tool_input)
|
287
|
+
is_repeated_call = self._is_repeated_tool_call(tool_name, arguments_with_values)
|
288
|
+
|
289
|
+
if is_repeated_call:
|
290
|
+
return self._handle_repeated_tool_call(tool_name, arguments_with_values)
|
291
|
+
|
292
|
+
executed_tool, response = self._execute_tool(tool_name, tool, arguments_with_values)
|
293
|
+
if not executed_tool:
|
294
|
+
return self._handle_tool_execution_failure(response)
|
295
|
+
|
296
|
+
variable_name = self.variable_store.add(response)
|
297
|
+
new_prompt = self._format_observation_response(response, variable_name, iteration)
|
298
|
+
|
299
|
+
return ObserveResponseResult(
|
300
|
+
next_prompt=new_prompt,
|
301
|
+
executed_tool=executed_tool,
|
302
|
+
answer=response if executed_tool == "task_complete" else None,
|
303
|
+
)
|
304
|
+
|
305
|
+
except Exception as e:
|
306
|
+
return self._handle_error(e)
|
307
|
+
|
308
|
+
def _parse_tool_usage(self, content: str) -> dict:
|
309
|
+
"""Extract tool usage from the response content."""
|
310
|
+
xml_parser = ToleranceXMLParser()
|
311
|
+
tool_names = self.tools.tool_names()
|
312
|
+
return xml_parser.extract_elements(text=content, element_names=tool_names)
|
313
|
+
|
314
|
+
def _parse_tool_arguments(self, tool, tool_input: str) -> dict:
|
315
|
+
"""Parse the tool arguments from the tool input."""
|
316
|
+
tool_parser = ToolParser(tool=tool)
|
317
|
+
return tool_parser.parse(tool_input)
|
318
|
+
|
319
|
+
def _is_repeated_tool_call(self, tool_name: str, arguments_with_values: dict) -> bool:
|
320
|
+
"""Check if the tool call is repeated."""
|
321
|
+
current_call = {
|
322
|
+
"tool_name": tool_name,
|
323
|
+
"arguments": arguments_with_values,
|
324
|
+
"timestamp": datetime.now().isoformat(),
|
325
|
+
}
|
326
|
+
|
327
|
+
is_repeated_call = (
|
328
|
+
self.last_tool_call.get("tool_name") == current_call["tool_name"]
|
329
|
+
and self.last_tool_call.get("arguments") == current_call["arguments"]
|
330
|
+
)
|
331
|
+
|
332
|
+
if is_repeated_call:
|
333
|
+
repeat_count = self.last_tool_call.get("count", 0) + 1
|
334
|
+
current_call["count"] = repeat_count
|
335
|
+
else:
|
336
|
+
current_call["count"] = 1
|
337
|
+
|
338
|
+
self.last_tool_call = current_call
|
339
|
+
return is_repeated_call and repeat_count >= 2
|
340
|
+
|
341
|
+
def _handle_no_tool_usage(self) -> ObserveResponseResult:
|
342
|
+
"""Handle the case where no tool usage is found in the response."""
|
343
|
+
return ObserveResponseResult(
|
344
|
+
next_prompt="Error: No tool usage found in response.", executed_tool=None, answer=None
|
345
|
+
)
|
346
|
+
|
347
|
+
def _handle_tool_not_found(self, tool_name: str) -> ObserveResponseResult:
|
348
|
+
"""Handle the case where the tool is not found."""
|
349
|
+
logger.warning(f"Tool '{tool_name}' not found in tool manager.")
|
350
|
+
return ObserveResponseResult(
|
351
|
+
next_prompt=f"Error: Tool '{tool_name}' not found in tool manager.",
|
352
|
+
executed_tool="",
|
353
|
+
answer=None,
|
354
|
+
)
|
355
|
+
|
356
|
+
def _handle_repeated_tool_call(self, tool_name: str, arguments_with_values: dict) -> ObserveResponseResult:
|
357
|
+
"""Handle the case where a tool call is repeated."""
|
358
|
+
repeat_count = self.last_tool_call.get("count", 0)
|
359
|
+
error_message = (
|
360
|
+
"Error: Detected repeated identical tool call pattern.\n"
|
361
|
+
f"Tool: {tool_name}\n"
|
362
|
+
f"Arguments: {arguments_with_values}\n"
|
363
|
+
f"Repeated {repeat_count} times\n\n"
|
364
|
+
"PLEASE:\n"
|
365
|
+
"1. Review your previous steps\n"
|
366
|
+
"2. Consider a different approach\n"
|
367
|
+
"3. Use a different tool or modify the arguments\n"
|
368
|
+
"4. Ensure you're making progress towards the goal"
|
369
|
+
)
|
370
|
+
return ObserveResponseResult(
|
371
|
+
next_prompt=error_message,
|
372
|
+
executed_tool="",
|
373
|
+
answer=None,
|
374
|
+
)
|
375
|
+
|
376
|
+
def _handle_tool_execution_failure(self, response: str) -> ObserveResponseResult:
|
377
|
+
"""Handle the case where tool execution fails."""
|
378
|
+
return ObserveResponseResult(
|
379
|
+
next_prompt=response,
|
380
|
+
executed_tool="",
|
381
|
+
answer=None,
|
382
|
+
)
|
383
|
+
|
384
|
+
def _handle_error(self, error: Exception) -> ObserveResponseResult:
|
385
|
+
"""Handle any exceptions that occur during response observation."""
|
386
|
+
logger.error(f"Error in _observe_response: {str(error)}")
|
387
|
+
return ObserveResponseResult(
|
388
|
+
next_prompt=f"An error occurred while processing the response: {str(error)}",
|
389
|
+
executed_tool=None,
|
390
|
+
answer=None,
|
391
|
+
)
|
392
|
+
|
393
|
+
def _format_observation_response(self, response: str, variable_name: str, iteration: int) -> str:
|
394
|
+
"""Format the observation response with the given response, variable name, and iteration."""
|
395
|
+
response_display = response
|
396
|
+
if len(response) > MAX_RESPONSE_LENGTH:
|
397
|
+
response_display = response[:MAX_RESPONSE_LENGTH]
|
398
|
+
response_display += (
|
399
|
+
f"... content was truncated. Full content available by interpolation in variable {variable_name}"
|
400
|
+
)
|
401
|
+
|
402
|
+
formatted_response = (
|
403
|
+
"\n"
|
404
|
+
f"--- Observations for iteration {iteration} ---\n"
|
405
|
+
"\n"
|
406
|
+
f"\n --- Tool execution result stored in variable ${variable_name}$ --- \n"
|
407
|
+
"\n"
|
408
|
+
f"<{variable_name}>\n{response_display}\n</{variable_name}>\n" + "\n"
|
409
|
+
"\n"
|
410
|
+
"--- Tools --- \n"
|
411
|
+
)
|
412
|
+
return formatted_response
|
413
|
+
|
414
|
+
def _format_observation_response(self, response: str, variable_name: str, iteration: int) -> str:
|
415
|
+
"""Format the observation response with the given response, variable name, and iteration."""
|
416
|
+
response_display = response
|
417
|
+
if len(response) > MAX_RESPONSE_LENGTH:
|
418
|
+
response_display = response[:MAX_RESPONSE_LENGTH]
|
419
|
+
response_display += (
|
420
|
+
f"... content was trunctated full content available by interpolation in variable {variable_name}"
|
421
|
+
)
|
422
|
+
|
423
|
+
# Format the response message
|
424
|
+
formatted_response = (
|
425
|
+
"\n"
|
426
|
+
f"--- Observations for iteration {iteration} ---\n"
|
427
|
+
"\n"
|
428
|
+
f"\n --- Tool execution result stored in variable ${variable_name}$ --- \n"
|
429
|
+
"\n"
|
430
|
+
f"<{variable_name}>\n{response_display}\n</{variable_name}>\n" + "\n"
|
431
|
+
"\n"
|
432
|
+
f"--- Tools --- \n"
|
433
|
+
"\n"
|
434
|
+
f"{self._get_tools_names_prompt()}"
|
435
|
+
"\n"
|
436
|
+
f"--- Variables --- \n"
|
437
|
+
"\n"
|
438
|
+
f"{self._get_variable_prompt()}"
|
439
|
+
"\n"
|
440
|
+
"You must analyze this answer and evaluate what to do next to solve the task.\n"
|
441
|
+
"If the step failed, take a step back and rethink your approach.\n"
|
442
|
+
"\n"
|
443
|
+
"--- Format ---\n"
|
444
|
+
"\n"
|
445
|
+
"You MUST respond with exactly two XML blocks formatted in markdown:\n"
|
446
|
+
"\n"
|
447
|
+
" - One <thinking> block detailing your analysis,\n"
|
448
|
+
" - One <tool_name> block specifying the chosen tool and its arguments, as outlined in the system prompt.\n"
|
449
|
+
)
|
450
|
+
|
451
|
+
return formatted_response
|
452
|
+
|
453
|
+
def _execute_tool(self, tool_name: str, tool, arguments_with_values: dict) -> tuple[str, Any]:
|
454
|
+
"""Execute a tool with validation if required.
|
455
|
+
|
456
|
+
Args:
|
457
|
+
tool_name: Name of the tool to execute
|
458
|
+
tool: Tool instance
|
459
|
+
arguments_with_values: Dictionary of argument names and values
|
460
|
+
|
461
|
+
Returns:
|
462
|
+
tuple containing:
|
463
|
+
- executed_tool name (str)
|
464
|
+
- tool execution response (Any)
|
465
|
+
"""
|
466
|
+
# Handle tool validation if required
|
467
|
+
if tool.need_validation:
|
468
|
+
logger.debug(f"Tool '{tool_name}' requires validation.")
|
469
|
+
self._emit_event(
|
470
|
+
"tool_execute_validation_start",
|
471
|
+
{"tool_name": tool_name, "arguments": arguments_with_values},
|
472
|
+
)
|
473
|
+
|
474
|
+
question_validation: str = (
|
475
|
+
"Do you permit the execution of this tool?"
|
476
|
+
f"Tool: {tool_name}"
|
477
|
+
f"Arguments: {arguments_with_values}"
|
478
|
+
"Yes or No"
|
479
|
+
).join("\n")
|
480
|
+
permission_granted = self.ask_for_user_validation(question_validation)
|
481
|
+
|
482
|
+
self._emit_event(
|
483
|
+
"tool_execute_validation_end",
|
484
|
+
{"tool_name": tool_name, "arguments": arguments_with_values},
|
485
|
+
)
|
486
|
+
|
487
|
+
if not permission_granted:
|
488
|
+
logger.debug(f"Execution of tool '{tool_name}' was denied by the user.")
|
489
|
+
return "", f"Error: execution of tool '{tool_name}' was denied by the user."
|
490
|
+
|
491
|
+
# Emit event: Tool Execution Start
|
492
|
+
self._emit_event(
|
493
|
+
"tool_execution_start",
|
494
|
+
{"tool_name": tool_name, "arguments": arguments_with_values},
|
495
|
+
)
|
496
|
+
|
497
|
+
try:
|
498
|
+
# Execute the tool synchronously
|
499
|
+
arguments_with_values_interpolated = {
|
500
|
+
key: self._interpolate_variables(value) for key, value in arguments_with_values.items()
|
501
|
+
}
|
502
|
+
# Call tool execute with named arguments
|
503
|
+
response = tool.execute(**arguments_with_values_interpolated)
|
504
|
+
executed_tool = tool.name
|
505
|
+
except Exception as e:
|
506
|
+
response = f"Error executing tool: {tool_name}: {str(e)}\n"
|
507
|
+
executed_tool = ""
|
508
|
+
|
509
|
+
# Emit event: Tool Execution End
|
510
|
+
self._emit_event(
|
511
|
+
"tool_execution_end",
|
512
|
+
{
|
513
|
+
"tool_name": tool_name,
|
514
|
+
"arguments": arguments_with_values,
|
515
|
+
"response": response,
|
516
|
+
},
|
517
|
+
)
|
518
|
+
|
519
|
+
return executed_tool, response
|
520
|
+
|
521
|
+
def _interpolate_variables(self, text: str) -> str:
|
522
|
+
"""Interpolate variables using $var1$ syntax in the given text."""
|
523
|
+
try:
|
524
|
+
for var in self.variable_store.keys():
|
525
|
+
text = text.replace(f"${var}$", self.variable_store[var])
|
526
|
+
return text
|
527
|
+
except Exception as e:
|
528
|
+
logger.error(f"Error in _interpolate_variables: {str(e)}")
|
529
|
+
return text
|
530
|
+
|
531
|
+
def _prepare_prompt_task(self, task: str) -> str:
|
532
|
+
"""Prepare the initial prompt for the task.
|
533
|
+
|
534
|
+
Args:
|
535
|
+
task (str): The task description.
|
536
|
+
|
537
|
+
Returns:
|
538
|
+
str: The formatted task prompt.
|
539
|
+
"""
|
540
|
+
prompt_task: str = (
|
541
|
+
"## Your task to solve:\n"
|
542
|
+
f"<task>\n{task}\n</task>\n"
|
543
|
+
"\n### Tools:\n"
|
544
|
+
"-----------------------------\n"
|
545
|
+
f"{self._get_tools_names_prompt()}\n"
|
546
|
+
"### Variables:\n"
|
547
|
+
"-----------------------------\n"
|
548
|
+
f"{self._get_variable_prompt()}\n"
|
549
|
+
)
|
550
|
+
return prompt_task
|
551
|
+
|
552
|
+
def _get_tools_names_prompt(self) -> str:
|
553
|
+
"""Construct a detailed prompt that lists the available tools for task execution."""
|
554
|
+
prompt_use_tools: str = (
|
555
|
+
"To accomplish this task, you have access to these tools:\n"
|
556
|
+
"\n"
|
557
|
+
f"{', '.join(self.tools.tool_names())}\n\n"
|
558
|
+
"Instructions:\n"
|
559
|
+
"\n"
|
560
|
+
"1. Select ONE tool per message\n"
|
561
|
+
"2. You will receive the tool's output in the next user response\n"
|
562
|
+
"3. Choose the most appropriate tool for each step\n"
|
563
|
+
)
|
564
|
+
return prompt_use_tools
|
565
|
+
|
566
|
+
def _get_variable_prompt(self) -> str:
|
567
|
+
"""Construct a prompt that explains how to use variables."""
|
568
|
+
prompt_use_variables: str = (
|
569
|
+
"To use a variable interpolation, use the format $variable_name$ in function arguments.\n"
|
570
|
+
"Example: <write_file><file_path>/path/to/file.txt</file_path><content>$var1$</write_file>\n"
|
571
|
+
"\n"
|
572
|
+
"Available variables:\n"
|
573
|
+
"\n"
|
574
|
+
f"{', '.join(self.variable_store.keys())}\n"
|
575
|
+
)
|
576
|
+
return prompt_use_variables
|
577
|
+
|
578
|
+
def _calculate_context_occupancy(self) -> float:
|
579
|
+
"""Calculate the number of tokens in percentages for prompt and completion."""
|
580
|
+
total_tokens = self.total_tokens
|
581
|
+
# Calculate token usage of prompt
|
582
|
+
max_tokens = self.model.get_model_max_input_tokens()
|
583
|
+
|
584
|
+
# Handle None value and prevent division by zero
|
585
|
+
if max_tokens is None or max_tokens <= 0:
|
586
|
+
logger.warning(f"Invalid max tokens value: {max_tokens}. Using default of {DEFAULT_MAX_INPUT_TOKENS}.")
|
587
|
+
max_tokens = DEFAULT_MAX_INPUT_TOKENS
|
588
|
+
|
589
|
+
return round((total_tokens / max_tokens) * 100, 2)
|
590
|
+
|
591
|
+
def _compact_memory_with_summary(self) -> str:
|
592
|
+
prompt_summary = (
|
593
|
+
"Summarize the conversation concisely:\n"
|
594
|
+
"format in markdown:\n"
|
595
|
+
"<thinking>\n"
|
596
|
+
" - 1. **Completed Steps**: Briefly describe the steps.\n"
|
597
|
+
" - 2. **Variables Used**: List the variables.\n"
|
598
|
+
" - 3. **Progress Analysis**: Assess progress.\n"
|
599
|
+
"</thinking>\n"
|
600
|
+
"Keep the summary clear and actionable.\n"
|
601
|
+
)
|
602
|
+
|
603
|
+
# Get all message system, except the last assistant / user message
|
604
|
+
memory_copy = self.memory.memory.copy()
|
605
|
+
|
606
|
+
# Remove the last assistant / user message
|
607
|
+
user_message = memory_copy.pop()
|
608
|
+
assistant_message = memory_copy.pop()
|
609
|
+
summary = self.model.generate_with_history(messages_history=memory_copy, prompt=prompt_summary)
|
610
|
+
# Remove user message
|
611
|
+
memory_copy.pop()
|
612
|
+
# Replace by summary
|
613
|
+
memory_copy.append(Message(role="user", content=summary.response))
|
614
|
+
memory_copy.append(assistant_message)
|
615
|
+
memory_copy.append(user_message)
|
616
|
+
self.memory.memory = memory_copy
|
617
|
+
return summary.response
|
618
|
+
|
619
|
+
def _update_session_memory(self, user_content: str, assistant_content: str) -> None:
|
620
|
+
"""
|
621
|
+
Log session messages to memory and emit events.
|
622
|
+
|
623
|
+
Args:
|
624
|
+
user_content (str): The user's content.
|
625
|
+
assistant_content (str): The assistant's content.
|
626
|
+
"""
|
627
|
+
self.memory.add(Message(role="user", content=user_content))
|
628
|
+
self._emit_event(
|
629
|
+
"session_add_message",
|
630
|
+
{"role": "user", "content": user_content},
|
631
|
+
)
|
632
|
+
|
633
|
+
self.memory.add(Message(role="assistant", content=assistant_content))
|
634
|
+
|
635
|
+
self._emit_event(
|
636
|
+
"session_add_message",
|
637
|
+
{"role": "assistant", "content": assistant_content},
|
638
|
+
)
|