janito 2.1.1__py3-none-any.whl → 2.3.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 (137) hide show
  1. janito/__init__.py +6 -6
  2. janito/agent/setup_agent.py +14 -5
  3. janito/agent/templates/profiles/system_prompt_template_main.txt.j2 +3 -1
  4. janito/cli/chat_mode/bindings.py +6 -0
  5. janito/cli/chat_mode/session.py +16 -0
  6. janito/cli/chat_mode/shell/autocomplete.py +21 -21
  7. janito/cli/chat_mode/shell/commands/__init__.py +3 -2
  8. janito/cli/chat_mode/shell/commands/clear.py +12 -12
  9. janito/cli/chat_mode/shell/commands/exec.py +27 -0
  10. janito/cli/chat_mode/shell/commands/multi.py +51 -51
  11. janito/cli/chat_mode/shell/commands/tools.py +17 -6
  12. janito/cli/chat_mode/shell/input_history.py +62 -62
  13. janito/cli/chat_mode/shell/session/manager.py +1 -0
  14. janito/cli/chat_mode/toolbar.py +3 -1
  15. janito/cli/cli_commands/list_models.py +35 -35
  16. janito/cli/cli_commands/list_providers.py +9 -9
  17. janito/cli/cli_commands/list_tools.py +53 -53
  18. janito/cli/cli_commands/model_selection.py +50 -50
  19. janito/cli/cli_commands/model_utils.py +13 -2
  20. janito/cli/cli_commands/set_api_key.py +19 -19
  21. janito/cli/cli_commands/show_config.py +51 -51
  22. janito/cli/cli_commands/show_system_prompt.py +62 -62
  23. janito/cli/config.py +2 -1
  24. janito/cli/core/__init__.py +4 -4
  25. janito/cli/core/event_logger.py +59 -59
  26. janito/cli/core/getters.py +3 -1
  27. janito/cli/core/runner.py +27 -6
  28. janito/cli/core/setters.py +5 -1
  29. janito/cli/core/unsetters.py +54 -54
  30. janito/cli/main_cli.py +12 -1
  31. janito/cli/prompt_core.py +5 -2
  32. janito/cli/rich_terminal_reporter.py +22 -3
  33. janito/cli/single_shot_mode/__init__.py +6 -6
  34. janito/cli/single_shot_mode/handler.py +11 -1
  35. janito/cli/verbose_output.py +1 -1
  36. janito/config.py +5 -5
  37. janito/config_manager.py +2 -0
  38. janito/driver_events.py +14 -0
  39. janito/drivers/anthropic/driver.py +113 -113
  40. janito/drivers/azure_openai/driver.py +38 -3
  41. janito/drivers/driver_registry.py +0 -2
  42. janito/drivers/openai/driver.py +196 -36
  43. janito/formatting_token.py +54 -54
  44. janito/i18n/__init__.py +35 -35
  45. janito/i18n/messages.py +23 -23
  46. janito/i18n/pt.py +47 -47
  47. janito/llm/__init__.py +5 -5
  48. janito/llm/agent.py +443 -443
  49. janito/llm/auth.py +1 -0
  50. janito/llm/driver.py +7 -1
  51. janito/llm/driver_config.py +1 -0
  52. janito/llm/driver_config_builder.py +34 -34
  53. janito/llm/driver_input.py +12 -12
  54. janito/llm/message_parts.py +60 -60
  55. janito/llm/model.py +38 -38
  56. janito/llm/provider.py +196 -196
  57. janito/provider_config.py +7 -3
  58. janito/provider_registry.py +29 -5
  59. janito/providers/__init__.py +1 -0
  60. janito/providers/anthropic/model_info.py +22 -22
  61. janito/providers/anthropic/provider.py +2 -2
  62. janito/providers/azure_openai/model_info.py +7 -6
  63. janito/providers/azure_openai/provider.py +44 -2
  64. janito/providers/deepseek/__init__.py +1 -1
  65. janito/providers/deepseek/model_info.py +16 -16
  66. janito/providers/deepseek/provider.py +91 -91
  67. janito/providers/google/model_info.py +21 -29
  68. janito/providers/google/provider.py +49 -38
  69. janito/providers/mistralai/provider.py +2 -2
  70. janito/providers/openai/model_info.py +0 -11
  71. janito/providers/openai/provider.py +1 -1
  72. janito/providers/provider_static_info.py +2 -3
  73. janito/providers/registry.py +26 -26
  74. janito/tools/adapters/__init__.py +1 -1
  75. janito/tools/adapters/local/__init__.py +62 -62
  76. janito/tools/adapters/local/adapter.py +33 -11
  77. janito/tools/adapters/local/ask_user.py +102 -102
  78. janito/tools/adapters/local/copy_file.py +84 -84
  79. janito/tools/adapters/local/create_directory.py +69 -69
  80. janito/tools/adapters/local/create_file.py +82 -82
  81. janito/tools/adapters/local/delete_text_in_file.py +4 -7
  82. janito/tools/adapters/local/fetch_url.py +97 -97
  83. janito/tools/adapters/local/find_files.py +138 -140
  84. janito/tools/adapters/local/get_file_outline/__init__.py +1 -1
  85. janito/tools/adapters/local/get_file_outline/core.py +117 -151
  86. janito/tools/adapters/local/get_file_outline/java_outline.py +40 -0
  87. janito/tools/adapters/local/get_file_outline/markdown_outline.py +14 -14
  88. janito/tools/adapters/local/get_file_outline/python_outline.py +303 -303
  89. janito/tools/adapters/local/get_file_outline/python_outline_v2.py +156 -156
  90. janito/tools/adapters/local/get_file_outline/search_outline.py +33 -33
  91. janito/tools/adapters/local/move_file.py +3 -13
  92. janito/tools/adapters/local/open_html_in_browser.py +24 -29
  93. janito/tools/adapters/local/open_url.py +3 -2
  94. janito/tools/adapters/local/python_code_run.py +166 -166
  95. janito/tools/adapters/local/python_command_run.py +164 -164
  96. janito/tools/adapters/local/python_file_run.py +163 -163
  97. janito/tools/adapters/local/remove_directory.py +6 -17
  98. janito/tools/adapters/local/remove_file.py +9 -15
  99. janito/tools/adapters/local/replace_text_in_file.py +6 -9
  100. janito/tools/adapters/local/run_bash_command.py +176 -176
  101. janito/tools/adapters/local/run_powershell_command.py +219 -219
  102. janito/tools/adapters/local/search_text/__init__.py +1 -1
  103. janito/tools/adapters/local/search_text/core.py +201 -201
  104. janito/tools/adapters/local/search_text/match_lines.py +1 -1
  105. janito/tools/adapters/local/search_text/pattern_utils.py +73 -73
  106. janito/tools/adapters/local/search_text/traverse_directory.py +145 -145
  107. janito/tools/adapters/local/validate_file_syntax/__init__.py +1 -1
  108. janito/tools/adapters/local/validate_file_syntax/core.py +106 -106
  109. janito/tools/adapters/local/validate_file_syntax/css_validator.py +35 -35
  110. janito/tools/adapters/local/validate_file_syntax/html_validator.py +93 -93
  111. janito/tools/adapters/local/validate_file_syntax/js_validator.py +27 -27
  112. janito/tools/adapters/local/validate_file_syntax/json_validator.py +6 -6
  113. janito/tools/adapters/local/validate_file_syntax/markdown_validator.py +109 -109
  114. janito/tools/adapters/local/validate_file_syntax/ps1_validator.py +32 -32
  115. janito/tools/adapters/local/validate_file_syntax/python_validator.py +5 -5
  116. janito/tools/adapters/local/validate_file_syntax/xml_validator.py +11 -11
  117. janito/tools/adapters/local/validate_file_syntax/yaml_validator.py +6 -6
  118. janito/tools/adapters/local/view_file.py +167 -167
  119. janito/tools/inspect_registry.py +17 -17
  120. janito/tools/tool_base.py +105 -105
  121. janito/tools/tool_events.py +58 -58
  122. janito/tools/tool_run_exception.py +12 -12
  123. janito/tools/tool_use_tracker.py +81 -81
  124. janito/tools/tool_utils.py +45 -45
  125. janito/tools/tools_adapter.py +78 -6
  126. janito/tools/tools_schema.py +104 -104
  127. janito/version.py +4 -4
  128. {janito-2.1.1.dist-info → janito-2.3.0.dist-info}/METADATA +388 -232
  129. janito-2.3.0.dist-info/RECORD +181 -0
  130. janito-2.3.0.dist-info/licenses/LICENSE +21 -0
  131. janito/cli/chat_mode/shell/commands/last.py +0 -137
  132. janito/drivers/google_genai/driver.py +0 -54
  133. janito/drivers/google_genai/schema_generator.py +0 -67
  134. janito-2.1.1.dist-info/RECORD +0 -181
  135. {janito-2.1.1.dist-info → janito-2.3.0.dist-info}/WHEEL +0 -0
  136. {janito-2.1.1.dist-info → janito-2.3.0.dist-info}/entry_points.txt +0 -0
  137. {janito-2.1.1.dist-info → janito-2.3.0.dist-info}/top_level.txt +0 -0
janito/llm/agent.py CHANGED
@@ -1,443 +1,443 @@
1
- from janito.llm.driver_input import DriverInput
2
- from janito.llm.driver_config import LLMDriverConfig
3
- from janito.conversation_history import LLMConversationHistory
4
- from janito.tools.tools_adapter import ToolsAdapterBase
5
- from queue import Queue, Empty
6
- from janito.driver_events import RequestStatus
7
- from typing import Any, Optional, List, Iterator, Union
8
- import threading
9
- import logging
10
- from jinja2 import Environment, FileSystemLoader, select_autoescape
11
- from pathlib import Path
12
- import time
13
- from janito.event_bus.bus import event_bus
14
-
15
-
16
- class LLMAgent:
17
- _event_lock: threading.Lock
18
- _latest_event: Optional[str]
19
-
20
- @property
21
- def template_vars(self):
22
- if not hasattr(self, "_template_vars"):
23
- self._template_vars = {}
24
- return self._template_vars
25
-
26
- """
27
- Represents an agent that interacts with an LLM driver to generate responses.
28
- Maintains conversation history as required by the new driver interface.
29
- """
30
-
31
- def __init__(
32
- self,
33
- llm_provider,
34
- tools_adapter: ToolsAdapterBase,
35
- agent_name: Optional[str] = None,
36
- system_prompt: Optional[str] = None,
37
- temperature: Optional[float] = None,
38
- conversation_history: Optional[LLMConversationHistory] = None,
39
- input_queue: Queue = None,
40
- output_queue: Queue = None,
41
- verbose_agent: bool = False,
42
- **kwargs: Any,
43
- ):
44
- self.llm_provider = llm_provider
45
- self.tools_adapter = tools_adapter
46
- self.agent_name = agent_name
47
- self.system_prompt = system_prompt
48
- self.temperature = temperature
49
- self.conversation_history = conversation_history or LLMConversationHistory()
50
- self.input_queue = input_queue if input_queue is not None else Queue()
51
- self.output_queue = output_queue if output_queue is not None else Queue()
52
- self._event_lock = threading.Lock()
53
- self._latest_event = None
54
- self.verbose_agent = verbose_agent
55
- self.driver = None # Will be set by setup_agent if available
56
-
57
- def get_provider_name(self):
58
- # Try to get provider name from driver, fallback to llm_provider, else '?'
59
- if self.driver and hasattr(self.driver, "name"):
60
- return self.driver.name
61
- elif hasattr(self.llm_provider, "name"):
62
- return self.llm_provider.name
63
- return "?"
64
-
65
- def get_model_name(self):
66
- # Try to get model name from driver, fallback to llm_provider, else '?'
67
- if self.driver and hasattr(self.driver, "model_name"):
68
- return self.driver.model_name
69
- elif hasattr(self.llm_provider, "model_name"):
70
- return self.llm_provider.model_name
71
- return "?"
72
-
73
- def set_template_var(self, key: str, value: str) -> None:
74
- """Set a variable for system prompt templating."""
75
- if not hasattr(self, "_template_vars"):
76
- self._template_vars = {}
77
- self._template_vars[key] = value
78
-
79
- def set_system_prompt(self, prompt: str) -> None:
80
- self.system_prompt = prompt
81
-
82
- def set_system_using_template(self, template_path: str, **kwargs) -> None:
83
- env = Environment(
84
- loader=FileSystemLoader(Path(template_path).parent),
85
- autoescape=select_autoescape(),
86
- )
87
- template = env.get_template(Path(template_path).name)
88
- self.system_prompt = template.render(**kwargs)
89
-
90
- def _refresh_system_prompt_from_template(self):
91
- if hasattr(self, "_template_vars") and hasattr(self, "system_prompt_template"):
92
- env = Environment(
93
- loader=FileSystemLoader(Path(self.system_prompt_template).parent),
94
- autoescape=select_autoescape(),
95
- )
96
- template = env.get_template(Path(self.system_prompt_template).name)
97
- self.system_prompt = template.render(**self._template_vars)
98
-
99
- def get_system_prompt(self) -> str:
100
- return self.system_prompt
101
-
102
- def _add_prompt_to_history(self, prompt_or_messages, role):
103
- if isinstance(prompt_or_messages, str):
104
- self.conversation_history.add_message(role, prompt_or_messages)
105
- elif isinstance(prompt_or_messages, list):
106
- for msg in prompt_or_messages:
107
- self.conversation_history.add_message(
108
- msg.get("role", role), msg.get("content", "")
109
- )
110
-
111
- def _ensure_system_prompt(self):
112
- if self.system_prompt and (
113
- not self.conversation_history._history
114
- or self.conversation_history._history[0]["role"] != "system"
115
- ):
116
- self.conversation_history._history.insert(
117
- 0, {"role": "system", "content": self.system_prompt}
118
- )
119
-
120
- def _validate_and_update_history(
121
- self,
122
- prompt: str = None,
123
- messages: Optional[List[dict]] = None,
124
- role: str = "user",
125
- ):
126
- if prompt is None and not messages:
127
- raise ValueError(
128
- "Either prompt or messages must be provided to Agent.chat."
129
- )
130
- if prompt is not None:
131
- self._add_prompt_to_history(prompt, role)
132
- elif messages:
133
- self._add_prompt_to_history(messages, role)
134
-
135
- def _log_event_verbose(self, event):
136
- if getattr(self, "verbose_agent", False):
137
- if hasattr(event, "parts"):
138
- for i, part in enumerate(getattr(event, "parts", [])):
139
- pass # Add detailed logging here if needed
140
- else:
141
- pass # Add detailed logging here if needed
142
-
143
- def _handle_event_type(self, event):
144
- event_class = getattr(event, "__class__", None)
145
- if event_class is not None and event_class.__name__ == "ResponseReceived":
146
- added_tool_results = self._handle_response_received(event)
147
- return event, added_tool_results
148
- # For all other events (including RequestFinished with status='error', RequestStarted), do not exit loop
149
- return None, False
150
-
151
- def _prepare_driver_input(self, config, cancel_event=None):
152
- return DriverInput(
153
- config=config,
154
- conversation_history=self.conversation_history,
155
- cancel_event=cancel_event,
156
- )
157
-
158
- def _process_next_response(
159
- self, poll_timeout: float = 1.0, max_wait_time: float = 300.0
160
- ):
161
- """
162
- Wait for a single event from the output queue (with timeout), process it, and return the result.
163
- This function is intended to be called from the main agent loop, which controls the overall flow.
164
- """
165
- if getattr(self, "verbose_agent", False):
166
- print("[agent] [DEBUG] Entered _process_next_response")
167
- elapsed = 0.0
168
- try:
169
- if getattr(self, "verbose_agent", False):
170
- print("[agent] [DEBUG] Waiting for event from output_queue...")
171
- return self._poll_for_event(poll_timeout, max_wait_time)
172
- except KeyboardInterrupt:
173
- self._handle_keyboard_interrupt()
174
- return None, False
175
-
176
- def _poll_for_event(self, poll_timeout, max_wait_time):
177
- elapsed = 0.0
178
- while True:
179
- event = self._get_event_from_output_queue(poll_timeout)
180
- if event is None:
181
- elapsed += poll_timeout
182
- if elapsed >= max_wait_time:
183
- error_msg = f"[ERROR] No output from driver in agent.chat() after {max_wait_time} seconds (timeout exit)"
184
- print(error_msg)
185
- print("[DEBUG] Exiting _process_next_response due to timeout")
186
- return None, False
187
- continue
188
- if getattr(self, "verbose_agent", False):
189
- print(f"[agent] [DEBUG] Received event from output_queue: {event}")
190
- event_bus.publish(event)
191
- self._log_event_verbose(event)
192
- event_class = getattr(event, "__class__", None)
193
- event_name = event_class.__name__ if event_class else None
194
- if event_name == "ResponseReceived":
195
- result = self._handle_event_type(event)
196
- return result
197
- elif event_name == "RequestFinished" and getattr(event, "status", None) in [
198
- RequestStatus.ERROR,
199
- RequestStatus.EMPTY_RESPONSE,
200
- RequestStatus.TIMEOUT,
201
- ]:
202
- return (event, False)
203
-
204
- def _handle_keyboard_interrupt(self):
205
- if hasattr(self, "input_queue") and self.input_queue is not None:
206
- from janito.driver_events import RequestFinished
207
-
208
- cancel_event = RequestFinished(
209
- status=RequestStatus.CANCELLED,
210
- reason="User interrupted (KeyboardInterrupt)",
211
- )
212
- self.input_queue.put(cancel_event)
213
-
214
- def _get_event_from_output_queue(self, poll_timeout):
215
- try:
216
- return self.output_queue.get(timeout=poll_timeout)
217
- except Empty:
218
- return None
219
-
220
- def _handle_response_received(self, event) -> bool:
221
- """
222
- Handle a ResponseReceived event: execute tool calls if present, update history.
223
- Returns True if the agent loop should continue (tool calls found), False otherwise.
224
- """
225
- if getattr(self, "verbose_agent", False):
226
- print("[agent] [INFO] Handling ResponseReceived event.")
227
- from janito.llm.message_parts import FunctionCallMessagePart
228
-
229
- tool_calls = []
230
- tool_results = []
231
- for part in event.parts:
232
- if isinstance(part, FunctionCallMessagePart):
233
- if getattr(self, "verbose_agent", False):
234
- print(
235
- f"[agent] [DEBUG] Tool call detected: {getattr(part, 'name', repr(part))} with arguments: {getattr(part, 'arguments', None)}"
236
- )
237
- tool_calls.append(part)
238
- result = self.tools_adapter.execute_function_call_message_part(part)
239
- tool_results.append(result)
240
- if tool_calls:
241
- # Prepare tool_calls message for assistant
242
- tool_calls_list = []
243
- tool_results_list = []
244
- for call, result in zip(tool_calls, tool_results):
245
- function_name = (
246
- getattr(call, "name", None)
247
- or (
248
- getattr(call, "function", None)
249
- and getattr(call.function, "name", None)
250
- )
251
- or "function"
252
- )
253
- arguments = getattr(call, "function", None) and getattr(
254
- call.function, "arguments", None
255
- )
256
- tool_call_id = getattr(call, "tool_call_id", None)
257
- tool_calls_list.append(
258
- {
259
- "id": tool_call_id,
260
- "type": "function",
261
- "function": {
262
- "name": function_name,
263
- "arguments": (
264
- arguments
265
- if isinstance(arguments, str)
266
- else str(arguments) if arguments else ""
267
- ),
268
- },
269
- }
270
- )
271
- tool_results_list.append(
272
- {
273
- "name": function_name,
274
- "content": str(result),
275
- "tool_call_id": tool_call_id,
276
- }
277
- )
278
- # Add assistant tool_calls message
279
- import json
280
-
281
- self.conversation_history.add_message(
282
- "tool_calls", json.dumps(tool_calls_list)
283
- )
284
- # Add tool_results message
285
- self.conversation_history.add_message(
286
- "tool_results", json.dumps(tool_results_list)
287
- )
288
- return True # Continue the loop
289
- else:
290
- return False # No tool calls, return event
291
-
292
- def chat(
293
- self,
294
- prompt: str = None,
295
- messages: Optional[List[dict]] = None,
296
- role: str = "user",
297
- config=None,
298
- ):
299
- if (
300
- hasattr(self, "driver")
301
- and self.driver
302
- and hasattr(self.driver, "clear_output_queue")
303
- ):
304
- self.driver.clear_output_queue()
305
- """
306
- Main agent conversation loop supporting function/tool calls and conversation history extension, now as a blocking event-driven loop with event publishing.
307
-
308
- Args:
309
- prompt: The user prompt as a string (optional if messages is provided).
310
- messages: A list of message dicts (optional if prompt is provided).
311
- role: The role for the prompt (default: 'user').
312
- config: Optional driver config (defaults to provider config).
313
-
314
- Returns:
315
- The final ResponseReceived event (or error event) when the conversation is complete.
316
- """
317
- self._validate_and_update_history(prompt, messages, role)
318
- self._ensure_system_prompt()
319
- if config is None:
320
- config = self.llm_provider.driver_config
321
- loop_count = 1
322
- import threading
323
-
324
- cancel_event = threading.Event()
325
- while True:
326
- self._print_verbose_chat_loop(loop_count)
327
- driver_input = self._prepare_driver_input(config, cancel_event=cancel_event)
328
- self.input_queue.put(driver_input)
329
- try:
330
- result, added_tool_results = self._process_next_response()
331
- except KeyboardInterrupt:
332
- cancel_event.set()
333
- raise
334
- if getattr(self, "verbose_agent", False):
335
- print(
336
- f"[agent] [DEBUG] Returned from _process_next_response: result={result}, added_tool_results={added_tool_results}"
337
- )
338
- if result is None:
339
- if getattr(self, "verbose_agent", False):
340
- print(
341
- f"[agent] [INFO] Exiting chat loop: _process_next_response returned None result (likely timeout or error). Returning (None, False)."
342
- )
343
- return None, False
344
- if not added_tool_results:
345
- if getattr(self, "verbose_agent", False):
346
- print(
347
- f"[agent] [INFO] Exiting chat loop: _process_next_response returned added_tool_results=False (final response or no more tool calls). Returning result: {result}"
348
- )
349
- return result
350
- loop_count += 1
351
-
352
- def _print_verbose_chat_loop(self, loop_count):
353
- if getattr(self, "verbose_agent", False):
354
- print(
355
- f"[agent] [DEBUG] Preparing new driver_input (loop_count={loop_count}) with updated conversation history:"
356
- )
357
- for msg in self.conversation_history.get_history():
358
- print(" ", msg)
359
-
360
- def set_latest_event(self, event: str) -> None:
361
- with self._event_lock:
362
- self._latest_event = event
363
-
364
- def get_latest_event(self) -> Optional[str]:
365
- with self._event_lock:
366
- return self._latest_event
367
-
368
- def get_history(self) -> LLMConversationHistory:
369
- """Get the agent's interaction history."""
370
- return self.conversation_history
371
-
372
- def reset_conversation_history(self) -> None:
373
- """Reset/clear the interaction history."""
374
- self.conversation_history = LLMConversationHistory()
375
-
376
- def get_provider_name(self) -> str:
377
- """Return the provider name, if available."""
378
- if hasattr(self.llm_provider, "name"):
379
- return getattr(self.llm_provider, "name", "?")
380
- if self.driver and hasattr(self.driver, "name"):
381
- return getattr(self.driver, "name", "?")
382
- return "?"
383
-
384
- def get_model_name(self) -> str:
385
- """Return the model name, if available."""
386
- if self.driver and hasattr(self.driver, "model_name"):
387
- return getattr(self.driver, "model_name", "?")
388
- return "?"
389
-
390
- def get_name(self) -> Optional[str]:
391
- return self.agent_name
392
-
393
- def get_provider_name(self) -> str:
394
- """
395
- Return the provider name for this agent, if available.
396
- """
397
- if hasattr(self, "llm_provider") and hasattr(self.llm_provider, "name"):
398
- return self.llm_provider.name
399
- if (
400
- hasattr(self, "driver")
401
- and self.driver
402
- and hasattr(self.driver, "provider_name")
403
- ):
404
- return self.driver.provider_name
405
- if hasattr(self, "driver") and self.driver and hasattr(self.driver, "name"):
406
- return self.driver.name
407
- return "?"
408
-
409
- def get_model_name(self) -> str:
410
- """
411
- Return the model name for this agent, if available.
412
- """
413
- if (
414
- hasattr(self, "driver")
415
- and self.driver
416
- and hasattr(self.driver, "model_name")
417
- ):
418
- return self.driver.model_name
419
- if hasattr(self, "llm_provider") and hasattr(self.llm_provider, "model_name"):
420
- return self.llm_provider.model_name
421
- return "?"
422
-
423
- def join_driver(self, timeout=None):
424
- """
425
- Wait for the driver's background thread to finish. Call this before exiting to avoid daemon thread shutdown errors.
426
- :param timeout: Optional timeout in seconds.
427
- Handles KeyboardInterrupt gracefully.
428
- """
429
- if (
430
- hasattr(self, "driver")
431
- and self.driver
432
- and hasattr(self.driver, "_thread")
433
- and self.driver._thread
434
- ):
435
- try:
436
- self.driver._thread.join(timeout)
437
- except KeyboardInterrupt:
438
- print(
439
- "\n[INFO] Interrupted by user during driver shutdown. Cleaning up..."
440
- )
441
- # Optionally, perform additional cleanup here
442
- # Do not re-raise to suppress traceback and exit gracefully
443
- return
1
+ from janito.llm.driver_input import DriverInput
2
+ from janito.llm.driver_config import LLMDriverConfig
3
+ from janito.conversation_history import LLMConversationHistory
4
+ from janito.tools.tools_adapter import ToolsAdapterBase
5
+ from queue import Queue, Empty
6
+ from janito.driver_events import RequestStatus
7
+ from typing import Any, Optional, List, Iterator, Union
8
+ import threading
9
+ import logging
10
+ from jinja2 import Environment, FileSystemLoader, select_autoescape
11
+ from pathlib import Path
12
+ import time
13
+ from janito.event_bus.bus import event_bus
14
+
15
+
16
+ class LLMAgent:
17
+ _event_lock: threading.Lock
18
+ _latest_event: Optional[str]
19
+
20
+ @property
21
+ def template_vars(self):
22
+ if not hasattr(self, "_template_vars"):
23
+ self._template_vars = {}
24
+ return self._template_vars
25
+
26
+ """
27
+ Represents an agent that interacts with an LLM driver to generate responses.
28
+ Maintains conversation history as required by the new driver interface.
29
+ """
30
+
31
+ def __init__(
32
+ self,
33
+ llm_provider,
34
+ tools_adapter: ToolsAdapterBase,
35
+ agent_name: Optional[str] = None,
36
+ system_prompt: Optional[str] = None,
37
+ temperature: Optional[float] = None,
38
+ conversation_history: Optional[LLMConversationHistory] = None,
39
+ input_queue: Queue = None,
40
+ output_queue: Queue = None,
41
+ verbose_agent: bool = False,
42
+ **kwargs: Any,
43
+ ):
44
+ self.llm_provider = llm_provider
45
+ self.tools_adapter = tools_adapter
46
+ self.agent_name = agent_name
47
+ self.system_prompt = system_prompt
48
+ self.temperature = temperature
49
+ self.conversation_history = conversation_history or LLMConversationHistory()
50
+ self.input_queue = input_queue if input_queue is not None else Queue()
51
+ self.output_queue = output_queue if output_queue is not None else Queue()
52
+ self._event_lock = threading.Lock()
53
+ self._latest_event = None
54
+ self.verbose_agent = verbose_agent
55
+ self.driver = None # Will be set by setup_agent if available
56
+
57
+ def get_provider_name(self):
58
+ # Try to get provider name from driver, fallback to llm_provider, else '?'
59
+ if self.driver and hasattr(self.driver, "name"):
60
+ return self.driver.name
61
+ elif hasattr(self.llm_provider, "name"):
62
+ return self.llm_provider.name
63
+ return "?"
64
+
65
+ def get_model_name(self):
66
+ # Try to get model name from driver, fallback to llm_provider, else '?'
67
+ if self.driver and hasattr(self.driver, "model_name"):
68
+ return self.driver.model_name
69
+ elif hasattr(self.llm_provider, "model_name"):
70
+ return self.llm_provider.model_name
71
+ return "?"
72
+
73
+ def set_template_var(self, key: str, value: str) -> None:
74
+ """Set a variable for system prompt templating."""
75
+ if not hasattr(self, "_template_vars"):
76
+ self._template_vars = {}
77
+ self._template_vars[key] = value
78
+
79
+ def set_system_prompt(self, prompt: str) -> None:
80
+ self.system_prompt = prompt
81
+
82
+ def set_system_using_template(self, template_path: str, **kwargs) -> None:
83
+ env = Environment(
84
+ loader=FileSystemLoader(Path(template_path).parent),
85
+ autoescape=select_autoescape(),
86
+ )
87
+ template = env.get_template(Path(template_path).name)
88
+ self.system_prompt = template.render(**kwargs)
89
+
90
+ def _refresh_system_prompt_from_template(self):
91
+ if hasattr(self, "_template_vars") and hasattr(self, "system_prompt_template"):
92
+ env = Environment(
93
+ loader=FileSystemLoader(Path(self.system_prompt_template).parent),
94
+ autoescape=select_autoescape(),
95
+ )
96
+ template = env.get_template(Path(self.system_prompt_template).name)
97
+ self.system_prompt = template.render(**self._template_vars)
98
+
99
+ def get_system_prompt(self) -> str:
100
+ return self.system_prompt
101
+
102
+ def _add_prompt_to_history(self, prompt_or_messages, role):
103
+ if isinstance(prompt_or_messages, str):
104
+ self.conversation_history.add_message(role, prompt_or_messages)
105
+ elif isinstance(prompt_or_messages, list):
106
+ for msg in prompt_or_messages:
107
+ self.conversation_history.add_message(
108
+ msg.get("role", role), msg.get("content", "")
109
+ )
110
+
111
+ def _ensure_system_prompt(self):
112
+ if self.system_prompt and (
113
+ not self.conversation_history._history
114
+ or self.conversation_history._history[0]["role"] != "system"
115
+ ):
116
+ self.conversation_history._history.insert(
117
+ 0, {"role": "system", "content": self.system_prompt}
118
+ )
119
+
120
+ def _validate_and_update_history(
121
+ self,
122
+ prompt: str = None,
123
+ messages: Optional[List[dict]] = None,
124
+ role: str = "user",
125
+ ):
126
+ if prompt is None and not messages:
127
+ raise ValueError(
128
+ "Either prompt or messages must be provided to Agent.chat."
129
+ )
130
+ if prompt is not None:
131
+ self._add_prompt_to_history(prompt, role)
132
+ elif messages:
133
+ self._add_prompt_to_history(messages, role)
134
+
135
+ def _log_event_verbose(self, event):
136
+ if getattr(self, "verbose_agent", False):
137
+ if hasattr(event, "parts"):
138
+ for i, part in enumerate(getattr(event, "parts", [])):
139
+ pass # Add detailed logging here if needed
140
+ else:
141
+ pass # Add detailed logging here if needed
142
+
143
+ def _handle_event_type(self, event):
144
+ event_class = getattr(event, "__class__", None)
145
+ if event_class is not None and event_class.__name__ == "ResponseReceived":
146
+ added_tool_results = self._handle_response_received(event)
147
+ return event, added_tool_results
148
+ # For all other events (including RequestFinished with status='error', RequestStarted), do not exit loop
149
+ return None, False
150
+
151
+ def _prepare_driver_input(self, config, cancel_event=None):
152
+ return DriverInput(
153
+ config=config,
154
+ conversation_history=self.conversation_history,
155
+ cancel_event=cancel_event,
156
+ )
157
+
158
+ def _process_next_response(
159
+ self, poll_timeout: float = 1.0, max_wait_time: float = 300.0
160
+ ):
161
+ """
162
+ Wait for a single event from the output queue (with timeout), process it, and return the result.
163
+ This function is intended to be called from the main agent loop, which controls the overall flow.
164
+ """
165
+ if getattr(self, "verbose_agent", False):
166
+ print("[agent] [DEBUG] Entered _process_next_response")
167
+ elapsed = 0.0
168
+ try:
169
+ if getattr(self, "verbose_agent", False):
170
+ print("[agent] [DEBUG] Waiting for event from output_queue...")
171
+ return self._poll_for_event(poll_timeout, max_wait_time)
172
+ except KeyboardInterrupt:
173
+ self._handle_keyboard_interrupt()
174
+ return None, False
175
+
176
+ def _poll_for_event(self, poll_timeout, max_wait_time):
177
+ elapsed = 0.0
178
+ while True:
179
+ event = self._get_event_from_output_queue(poll_timeout)
180
+ if event is None:
181
+ elapsed += poll_timeout
182
+ if elapsed >= max_wait_time:
183
+ error_msg = f"[ERROR] No output from driver in agent.chat() after {max_wait_time} seconds (timeout exit)"
184
+ print(error_msg)
185
+ print("[DEBUG] Exiting _process_next_response due to timeout")
186
+ return None, False
187
+ continue
188
+ if getattr(self, "verbose_agent", False):
189
+ print(f"[agent] [DEBUG] Received event from output_queue: {event}")
190
+ event_bus.publish(event)
191
+ self._log_event_verbose(event)
192
+ event_class = getattr(event, "__class__", None)
193
+ event_name = event_class.__name__ if event_class else None
194
+ if event_name == "ResponseReceived":
195
+ result = self._handle_event_type(event)
196
+ return result
197
+ elif event_name == "RequestFinished" and getattr(event, "status", None) in [
198
+ RequestStatus.ERROR,
199
+ RequestStatus.EMPTY_RESPONSE,
200
+ RequestStatus.TIMEOUT,
201
+ ]:
202
+ return (event, False)
203
+
204
+ def _handle_keyboard_interrupt(self):
205
+ if hasattr(self, "input_queue") and self.input_queue is not None:
206
+ from janito.driver_events import RequestFinished
207
+
208
+ cancel_event = RequestFinished(
209
+ status=RequestStatus.CANCELLED,
210
+ reason="User interrupted (KeyboardInterrupt)",
211
+ )
212
+ self.input_queue.put(cancel_event)
213
+
214
+ def _get_event_from_output_queue(self, poll_timeout):
215
+ try:
216
+ return self.output_queue.get(timeout=poll_timeout)
217
+ except Empty:
218
+ return None
219
+
220
+ def _handle_response_received(self, event) -> bool:
221
+ """
222
+ Handle a ResponseReceived event: execute tool calls if present, update history.
223
+ Returns True if the agent loop should continue (tool calls found), False otherwise.
224
+ """
225
+ if getattr(self, "verbose_agent", False):
226
+ print("[agent] [INFO] Handling ResponseReceived event.")
227
+ from janito.llm.message_parts import FunctionCallMessagePart
228
+
229
+ tool_calls = []
230
+ tool_results = []
231
+ for part in event.parts:
232
+ if isinstance(part, FunctionCallMessagePart):
233
+ if getattr(self, "verbose_agent", False):
234
+ print(
235
+ f"[agent] [DEBUG] Tool call detected: {getattr(part, 'name', repr(part))} with arguments: {getattr(part, 'arguments', None)}"
236
+ )
237
+ tool_calls.append(part)
238
+ result = self.tools_adapter.execute_function_call_message_part(part)
239
+ tool_results.append(result)
240
+ if tool_calls:
241
+ # Prepare tool_calls message for assistant
242
+ tool_calls_list = []
243
+ tool_results_list = []
244
+ for call, result in zip(tool_calls, tool_results):
245
+ function_name = (
246
+ getattr(call, "name", None)
247
+ or (
248
+ getattr(call, "function", None)
249
+ and getattr(call.function, "name", None)
250
+ )
251
+ or "function"
252
+ )
253
+ arguments = getattr(call, "function", None) and getattr(
254
+ call.function, "arguments", None
255
+ )
256
+ tool_call_id = getattr(call, "tool_call_id", None)
257
+ tool_calls_list.append(
258
+ {
259
+ "id": tool_call_id,
260
+ "type": "function",
261
+ "function": {
262
+ "name": function_name,
263
+ "arguments": (
264
+ arguments
265
+ if isinstance(arguments, str)
266
+ else str(arguments) if arguments else ""
267
+ ),
268
+ },
269
+ }
270
+ )
271
+ tool_results_list.append(
272
+ {
273
+ "name": function_name,
274
+ "content": str(result),
275
+ "tool_call_id": tool_call_id,
276
+ }
277
+ )
278
+ # Add assistant tool_calls message
279
+ import json
280
+
281
+ self.conversation_history.add_message(
282
+ "tool_calls", json.dumps(tool_calls_list)
283
+ )
284
+ # Add tool_results message
285
+ self.conversation_history.add_message(
286
+ "tool_results", json.dumps(tool_results_list)
287
+ )
288
+ return True # Continue the loop
289
+ else:
290
+ return False # No tool calls, return event
291
+
292
+ def chat(
293
+ self,
294
+ prompt: str = None,
295
+ messages: Optional[List[dict]] = None,
296
+ role: str = "user",
297
+ config=None,
298
+ ):
299
+ if (
300
+ hasattr(self, "driver")
301
+ and self.driver
302
+ and hasattr(self.driver, "clear_output_queue")
303
+ ):
304
+ self.driver.clear_output_queue()
305
+ """
306
+ Main agent conversation loop supporting function/tool calls and conversation history extension, now as a blocking event-driven loop with event publishing.
307
+
308
+ Args:
309
+ prompt: The user prompt as a string (optional if messages is provided).
310
+ messages: A list of message dicts (optional if prompt is provided).
311
+ role: The role for the prompt (default: 'user').
312
+ config: Optional driver config (defaults to provider config).
313
+
314
+ Returns:
315
+ The final ResponseReceived event (or error event) when the conversation is complete.
316
+ """
317
+ self._validate_and_update_history(prompt, messages, role)
318
+ self._ensure_system_prompt()
319
+ if config is None:
320
+ config = self.llm_provider.driver_config
321
+ loop_count = 1
322
+ import threading
323
+
324
+ cancel_event = threading.Event()
325
+ while True:
326
+ self._print_verbose_chat_loop(loop_count)
327
+ driver_input = self._prepare_driver_input(config, cancel_event=cancel_event)
328
+ self.input_queue.put(driver_input)
329
+ try:
330
+ result, added_tool_results = self._process_next_response()
331
+ except KeyboardInterrupt:
332
+ cancel_event.set()
333
+ raise
334
+ if getattr(self, "verbose_agent", False):
335
+ print(
336
+ f"[agent] [DEBUG] Returned from _process_next_response: result={result}, added_tool_results={added_tool_results}"
337
+ )
338
+ if result is None:
339
+ if getattr(self, "verbose_agent", False):
340
+ print(
341
+ f"[agent] [INFO] Exiting chat loop: _process_next_response returned None result (likely timeout or error). Returning (None, False)."
342
+ )
343
+ return None, False
344
+ if not added_tool_results:
345
+ if getattr(self, "verbose_agent", False):
346
+ print(
347
+ f"[agent] [INFO] Exiting chat loop: _process_next_response returned added_tool_results=False (final response or no more tool calls). Returning result: {result}"
348
+ )
349
+ return result
350
+ loop_count += 1
351
+
352
+ def _print_verbose_chat_loop(self, loop_count):
353
+ if getattr(self, "verbose_agent", False):
354
+ print(
355
+ f"[agent] [DEBUG] Preparing new driver_input (loop_count={loop_count}) with updated conversation history:"
356
+ )
357
+ for msg in self.conversation_history.get_history():
358
+ print(" ", msg)
359
+
360
+ def set_latest_event(self, event: str) -> None:
361
+ with self._event_lock:
362
+ self._latest_event = event
363
+
364
+ def get_latest_event(self) -> Optional[str]:
365
+ with self._event_lock:
366
+ return self._latest_event
367
+
368
+ def get_history(self) -> LLMConversationHistory:
369
+ """Get the agent's interaction history."""
370
+ return self.conversation_history
371
+
372
+ def reset_conversation_history(self) -> None:
373
+ """Reset/clear the interaction history."""
374
+ self.conversation_history = LLMConversationHistory()
375
+
376
+ def get_provider_name(self) -> str:
377
+ """Return the provider name, if available."""
378
+ if hasattr(self.llm_provider, "name"):
379
+ return getattr(self.llm_provider, "name", "?")
380
+ if self.driver and hasattr(self.driver, "name"):
381
+ return getattr(self.driver, "name", "?")
382
+ return "?"
383
+
384
+ def get_model_name(self) -> str:
385
+ """Return the model name, if available."""
386
+ if self.driver and hasattr(self.driver, "model_name"):
387
+ return getattr(self.driver, "model_name", "?")
388
+ return "?"
389
+
390
+ def get_name(self) -> Optional[str]:
391
+ return self.agent_name
392
+
393
+ def get_provider_name(self) -> str:
394
+ """
395
+ Return the provider name for this agent, if available.
396
+ """
397
+ if hasattr(self, "llm_provider") and hasattr(self.llm_provider, "name"):
398
+ return self.llm_provider.name
399
+ if (
400
+ hasattr(self, "driver")
401
+ and self.driver
402
+ and hasattr(self.driver, "provider_name")
403
+ ):
404
+ return self.driver.provider_name
405
+ if hasattr(self, "driver") and self.driver and hasattr(self.driver, "name"):
406
+ return self.driver.name
407
+ return "?"
408
+
409
+ def get_model_name(self) -> str:
410
+ """
411
+ Return the model name for this agent, if available.
412
+ """
413
+ if (
414
+ hasattr(self, "driver")
415
+ and self.driver
416
+ and hasattr(self.driver, "model_name")
417
+ ):
418
+ return self.driver.model_name
419
+ if hasattr(self, "llm_provider") and hasattr(self.llm_provider, "model_name"):
420
+ return self.llm_provider.model_name
421
+ return "?"
422
+
423
+ def join_driver(self, timeout=None):
424
+ """
425
+ Wait for the driver's background thread to finish. Call this before exiting to avoid daemon thread shutdown errors.
426
+ :param timeout: Optional timeout in seconds.
427
+ Handles KeyboardInterrupt gracefully.
428
+ """
429
+ if (
430
+ hasattr(self, "driver")
431
+ and self.driver
432
+ and hasattr(self.driver, "_thread")
433
+ and self.driver._thread
434
+ ):
435
+ try:
436
+ self.driver._thread.join(timeout)
437
+ except KeyboardInterrupt:
438
+ print(
439
+ "\n[INFO] Interrupted by user during driver shutdown. Cleaning up..."
440
+ )
441
+ # Optionally, perform additional cleanup here
442
+ # Do not re-raise to suppress traceback and exit gracefully
443
+ return