quantalogic 0.57.0__py3-none-any.whl → 0.59.0__py3-none-any.whl

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