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.
Files changed (68) hide show
  1. quantalogic/__init__.py +20 -0
  2. quantalogic/agent.py +638 -0
  3. quantalogic/agent_config.py +138 -0
  4. quantalogic/coding_agent.py +83 -0
  5. quantalogic/event_emitter.py +223 -0
  6. quantalogic/generative_model.py +226 -0
  7. quantalogic/interactive_text_editor.py +190 -0
  8. quantalogic/main.py +185 -0
  9. quantalogic/memory.py +217 -0
  10. quantalogic/model_names.py +19 -0
  11. quantalogic/print_event.py +66 -0
  12. quantalogic/prompts.py +99 -0
  13. quantalogic/server/__init__.py +3 -0
  14. quantalogic/server/agent_server.py +633 -0
  15. quantalogic/server/models.py +60 -0
  16. quantalogic/server/routes.py +117 -0
  17. quantalogic/server/state.py +199 -0
  18. quantalogic/server/static/js/event_visualizer.js +430 -0
  19. quantalogic/server/static/js/quantalogic.js +571 -0
  20. quantalogic/server/templates/index.html +134 -0
  21. quantalogic/tool_manager.py +68 -0
  22. quantalogic/tools/__init__.py +46 -0
  23. quantalogic/tools/agent_tool.py +88 -0
  24. quantalogic/tools/download_http_file_tool.py +64 -0
  25. quantalogic/tools/edit_whole_content_tool.py +70 -0
  26. quantalogic/tools/elixir_tool.py +240 -0
  27. quantalogic/tools/execute_bash_command_tool.py +116 -0
  28. quantalogic/tools/input_question_tool.py +57 -0
  29. quantalogic/tools/language_handlers/__init__.py +21 -0
  30. quantalogic/tools/language_handlers/c_handler.py +33 -0
  31. quantalogic/tools/language_handlers/cpp_handler.py +33 -0
  32. quantalogic/tools/language_handlers/go_handler.py +33 -0
  33. quantalogic/tools/language_handlers/java_handler.py +37 -0
  34. quantalogic/tools/language_handlers/javascript_handler.py +42 -0
  35. quantalogic/tools/language_handlers/python_handler.py +29 -0
  36. quantalogic/tools/language_handlers/rust_handler.py +33 -0
  37. quantalogic/tools/language_handlers/scala_handler.py +33 -0
  38. quantalogic/tools/language_handlers/typescript_handler.py +42 -0
  39. quantalogic/tools/list_directory_tool.py +123 -0
  40. quantalogic/tools/llm_tool.py +119 -0
  41. quantalogic/tools/markitdown_tool.py +105 -0
  42. quantalogic/tools/nodejs_tool.py +515 -0
  43. quantalogic/tools/python_tool.py +469 -0
  44. quantalogic/tools/read_file_block_tool.py +140 -0
  45. quantalogic/tools/read_file_tool.py +79 -0
  46. quantalogic/tools/replace_in_file_tool.py +300 -0
  47. quantalogic/tools/ripgrep_tool.py +353 -0
  48. quantalogic/tools/search_definition_names.py +419 -0
  49. quantalogic/tools/task_complete_tool.py +35 -0
  50. quantalogic/tools/tool.py +146 -0
  51. quantalogic/tools/unified_diff_tool.py +387 -0
  52. quantalogic/tools/write_file_tool.py +97 -0
  53. quantalogic/utils/__init__.py +17 -0
  54. quantalogic/utils/ask_user_validation.py +12 -0
  55. quantalogic/utils/download_http_file.py +77 -0
  56. quantalogic/utils/get_coding_environment.py +15 -0
  57. quantalogic/utils/get_environment.py +26 -0
  58. quantalogic/utils/get_quantalogic_rules_content.py +19 -0
  59. quantalogic/utils/git_ls.py +121 -0
  60. quantalogic/utils/read_file.py +54 -0
  61. quantalogic/utils/read_http_text_content.py +101 -0
  62. quantalogic/xml_parser.py +242 -0
  63. quantalogic/xml_tool_parser.py +99 -0
  64. quantalogic-0.2.0.dist-info/LICENSE +201 -0
  65. quantalogic-0.2.0.dist-info/METADATA +1034 -0
  66. quantalogic-0.2.0.dist-info/RECORD +68 -0
  67. quantalogic-0.2.0.dist-info/WHEEL +4 -0
  68. quantalogic-0.2.0.dist-info/entry_points.txt +3 -0
@@ -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
+ )