janito 1.14.3__py3-none-any.whl → 2.0.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. janito/__init__.py +6 -1
  2. janito/__main__.py +1 -1
  3. janito/agent/setup_agent.py +139 -0
  4. janito/agent/templates/profiles/{system_prompt_template_base.txt.j2 → system_prompt_template_main.txt.j2} +1 -1
  5. janito/cli/__init__.py +9 -0
  6. janito/cli/chat_mode/bindings.py +37 -0
  7. janito/cli/chat_mode/chat_entry.py +23 -0
  8. janito/cli/chat_mode/prompt_style.py +19 -0
  9. janito/cli/chat_mode/session.py +272 -0
  10. janito/{shell/prompt/completer.py → cli/chat_mode/shell/autocomplete.py} +7 -6
  11. janito/cli/chat_mode/shell/commands/__init__.py +55 -0
  12. janito/cli/chat_mode/shell/commands/base.py +9 -0
  13. janito/cli/chat_mode/shell/commands/clear.py +12 -0
  14. janito/{shell → cli/chat_mode/shell}/commands/conversation_restart.py +34 -30
  15. janito/cli/chat_mode/shell/commands/edit.py +25 -0
  16. janito/cli/chat_mode/shell/commands/help.py +16 -0
  17. janito/cli/chat_mode/shell/commands/history_view.py +93 -0
  18. janito/cli/chat_mode/shell/commands/lang.py +25 -0
  19. janito/cli/chat_mode/shell/commands/last.py +137 -0
  20. janito/cli/chat_mode/shell/commands/livelogs.py +49 -0
  21. janito/cli/chat_mode/shell/commands/multi.py +51 -0
  22. janito/cli/chat_mode/shell/commands/prompt.py +64 -0
  23. janito/cli/chat_mode/shell/commands/role.py +36 -0
  24. janito/cli/chat_mode/shell/commands/session.py +40 -0
  25. janito/{shell → cli/chat_mode/shell}/commands/session_control.py +2 -2
  26. janito/cli/chat_mode/shell/commands/termweb_log.py +92 -0
  27. janito/cli/chat_mode/shell/commands/tools.py +32 -0
  28. janito/{shell → cli/chat_mode/shell}/commands/utility.py +4 -7
  29. janito/{shell → cli/chat_mode/shell}/commands/verbose.py +5 -5
  30. janito/cli/chat_mode/shell/session/__init__.py +1 -0
  31. janito/{shell → cli/chat_mode/shell}/session/manager.py +9 -1
  32. janito/cli/chat_mode/toolbar.py +90 -0
  33. janito/cli/cli_commands/list_models.py +35 -0
  34. janito/cli/cli_commands/list_providers.py +9 -0
  35. janito/cli/cli_commands/list_tools.py +53 -0
  36. janito/cli/cli_commands/model_selection.py +50 -0
  37. janito/cli/cli_commands/model_utils.py +84 -0
  38. janito/cli/cli_commands/set_api_key.py +19 -0
  39. janito/cli/cli_commands/show_config.py +51 -0
  40. janito/cli/cli_commands/show_system_prompt.py +62 -0
  41. janito/cli/config.py +28 -0
  42. janito/cli/console.py +3 -0
  43. janito/cli/core/__init__.py +4 -0
  44. janito/cli/core/event_logger.py +59 -0
  45. janito/cli/core/getters.py +31 -0
  46. janito/cli/core/runner.py +141 -0
  47. janito/cli/core/setters.py +174 -0
  48. janito/cli/core/unsetters.py +54 -0
  49. janito/cli/main.py +8 -196
  50. janito/cli/main_cli.py +312 -0
  51. janito/cli/prompt_core.py +230 -0
  52. janito/cli/prompt_handler.py +6 -0
  53. janito/cli/rich_terminal_reporter.py +101 -0
  54. janito/cli/single_shot_mode/__init__.py +6 -0
  55. janito/cli/single_shot_mode/handler.py +137 -0
  56. janito/cli/termweb_starter.py +73 -24
  57. janito/cli/utils.py +25 -0
  58. janito/cli/verbose_output.py +196 -0
  59. janito/config.py +5 -0
  60. janito/config_manager.py +110 -0
  61. janito/conversation_history.py +30 -0
  62. janito/{agent/tools_utils/dir_walk_utils.py → dir_walk_utils.py} +3 -2
  63. janito/driver_events.py +98 -0
  64. janito/drivers/anthropic/driver.py +113 -0
  65. janito/drivers/azure_openai/driver.py +36 -0
  66. janito/drivers/driver_registry.py +33 -0
  67. janito/drivers/google_genai/driver.py +54 -0
  68. janito/drivers/google_genai/schema_generator.py +67 -0
  69. janito/drivers/mistralai/driver.py +41 -0
  70. janito/drivers/openai/driver.py +334 -0
  71. janito/event_bus/__init__.py +2 -0
  72. janito/event_bus/bus.py +68 -0
  73. janito/event_bus/event.py +15 -0
  74. janito/event_bus/handler.py +31 -0
  75. janito/event_bus/queue_bus.py +57 -0
  76. janito/exceptions.py +23 -0
  77. janito/formatting_token.py +54 -0
  78. janito/i18n/pt.py +1 -0
  79. janito/llm/__init__.py +5 -0
  80. janito/llm/agent.py +443 -0
  81. janito/llm/auth.py +62 -0
  82. janito/llm/driver.py +239 -0
  83. janito/llm/driver_config.py +34 -0
  84. janito/llm/driver_config_builder.py +34 -0
  85. janito/llm/driver_input.py +12 -0
  86. janito/llm/message_parts.py +60 -0
  87. janito/llm/model.py +38 -0
  88. janito/llm/provider.py +187 -0
  89. janito/perf_singleton.py +3 -0
  90. janito/performance_collector.py +167 -0
  91. janito/provider_config.py +98 -0
  92. janito/provider_registry.py +152 -0
  93. janito/providers/__init__.py +7 -0
  94. janito/providers/anthropic/model_info.py +22 -0
  95. janito/providers/anthropic/provider.py +65 -0
  96. janito/providers/azure_openai/model_info.py +15 -0
  97. janito/providers/azure_openai/provider.py +72 -0
  98. janito/providers/deepseek/__init__.py +1 -0
  99. janito/providers/deepseek/model_info.py +16 -0
  100. janito/providers/deepseek/provider.py +91 -0
  101. janito/providers/google/__init__.py +1 -0
  102. janito/providers/google/model_info.py +40 -0
  103. janito/providers/google/provider.py +69 -0
  104. janito/providers/mistralai/model_info.py +37 -0
  105. janito/providers/mistralai/provider.py +69 -0
  106. janito/providers/openai/__init__.py +1 -0
  107. janito/providers/openai/model_info.py +137 -0
  108. janito/providers/openai/provider.py +107 -0
  109. janito/providers/openai/schema_generator.py +63 -0
  110. janito/providers/provider_static_info.py +21 -0
  111. janito/providers/registry.py +26 -0
  112. janito/report_events.py +38 -0
  113. janito/termweb/app.py +1 -1
  114. janito/tools/__init__.py +16 -0
  115. janito/tools/adapters/__init__.py +1 -0
  116. janito/tools/adapters/local/__init__.py +54 -0
  117. janito/tools/adapters/local/adapter.py +92 -0
  118. janito/{agent/tools → tools/adapters/local}/ask_user.py +30 -13
  119. janito/tools/adapters/local/copy_file.py +84 -0
  120. janito/{agent/tools → tools/adapters/local}/create_directory.py +11 -10
  121. janito/tools/adapters/local/create_file.py +82 -0
  122. janito/tools/adapters/local/delete_text_in_file.py +136 -0
  123. janito/{agent/tools → tools/adapters/local}/fetch_url.py +18 -19
  124. janito/tools/adapters/local/find_files.py +140 -0
  125. janito/tools/adapters/local/get_file_outline/core.py +151 -0
  126. janito/{agent/tools → tools/adapters/local}/get_file_outline/python_outline.py +125 -0
  127. janito/tools/adapters/local/get_file_outline/python_outline_v2.py +156 -0
  128. janito/{agent/tools → tools/adapters/local}/get_file_outline/search_outline.py +12 -7
  129. janito/{agent/tools → tools/adapters/local}/move_file.py +13 -9
  130. janito/tools/adapters/local/open_html_in_browser.py +34 -0
  131. janito/{agent/tools → tools/adapters/local}/open_url.py +7 -5
  132. janito/tools/adapters/local/python_code_run.py +165 -0
  133. janito/tools/adapters/local/python_command_run.py +163 -0
  134. janito/tools/adapters/local/python_file_run.py +162 -0
  135. janito/{agent/tools → tools/adapters/local}/remove_directory.py +15 -9
  136. janito/{agent/tools → tools/adapters/local}/remove_file.py +17 -14
  137. janito/{agent/tools → tools/adapters/local}/replace_text_in_file.py +27 -22
  138. janito/tools/adapters/local/run_bash_command.py +176 -0
  139. janito/tools/adapters/local/run_powershell_command.py +219 -0
  140. janito/{agent/tools → tools/adapters/local}/search_text/core.py +32 -12
  141. janito/{agent/tools → tools/adapters/local}/search_text/match_lines.py +13 -4
  142. janito/{agent/tools → tools/adapters/local}/search_text/pattern_utils.py +12 -4
  143. janito/{agent/tools → tools/adapters/local}/search_text/traverse_directory.py +15 -2
  144. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/core.py +12 -11
  145. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/css_validator.py +1 -1
  146. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/html_validator.py +1 -1
  147. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/js_validator.py +1 -1
  148. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/json_validator.py +1 -1
  149. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/markdown_validator.py +1 -1
  150. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/ps1_validator.py +1 -1
  151. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/python_validator.py +1 -1
  152. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/xml_validator.py +1 -1
  153. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/yaml_validator.py +1 -1
  154. janito/{agent/tools/get_lines.py → tools/adapters/local/view_file.py} +45 -27
  155. janito/tools/inspect_registry.py +17 -0
  156. janito/tools/tool_base.py +105 -0
  157. janito/tools/tool_events.py +58 -0
  158. janito/tools/tool_run_exception.py +12 -0
  159. janito/{agent → tools}/tool_use_tracker.py +2 -4
  160. janito/{agent/tools_utils/utils.py → tools/tool_utils.py} +18 -9
  161. janito/tools/tools_adapter.py +207 -0
  162. janito/tools/tools_schema.py +104 -0
  163. janito/utils.py +11 -0
  164. janito/version.py +4 -0
  165. janito-2.0.1.dist-info/METADATA +232 -0
  166. janito-2.0.1.dist-info/RECORD +181 -0
  167. janito/agent/__init__.py +0 -0
  168. janito/agent/api_exceptions.py +0 -4
  169. janito/agent/config.py +0 -147
  170. janito/agent/config_defaults.py +0 -12
  171. janito/agent/config_utils.py +0 -0
  172. janito/agent/content_handler.py +0 -0
  173. janito/agent/conversation.py +0 -238
  174. janito/agent/conversation_api.py +0 -306
  175. janito/agent/conversation_exceptions.py +0 -18
  176. janito/agent/conversation_tool_calls.py +0 -39
  177. janito/agent/conversation_ui.py +0 -17
  178. janito/agent/event.py +0 -24
  179. janito/agent/event_dispatcher.py +0 -24
  180. janito/agent/event_handler_protocol.py +0 -5
  181. janito/agent/event_system.py +0 -15
  182. janito/agent/llm_conversation_history.py +0 -82
  183. janito/agent/message_handler.py +0 -20
  184. janito/agent/message_handler_protocol.py +0 -5
  185. janito/agent/openai_client.py +0 -149
  186. janito/agent/openai_schema_generator.py +0 -187
  187. janito/agent/profile_manager.py +0 -96
  188. janito/agent/queued_message_handler.py +0 -50
  189. janito/agent/rich_live.py +0 -32
  190. janito/agent/rich_message_handler.py +0 -115
  191. janito/agent/runtime_config.py +0 -36
  192. janito/agent/test_handler_protocols.py +0 -47
  193. janito/agent/test_openai_schema_generator.py +0 -93
  194. janito/agent/tests/__init__.py +0 -1
  195. janito/agent/tool_base.py +0 -63
  196. janito/agent/tool_executor.py +0 -122
  197. janito/agent/tool_registry.py +0 -49
  198. janito/agent/tools/__init__.py +0 -47
  199. janito/agent/tools/create_file.py +0 -59
  200. janito/agent/tools/delete_text_in_file.py +0 -97
  201. janito/agent/tools/find_files.py +0 -106
  202. janito/agent/tools/get_file_outline/core.py +0 -81
  203. janito/agent/tools/present_choices.py +0 -64
  204. janito/agent/tools/python_command_runner.py +0 -201
  205. janito/agent/tools/python_file_runner.py +0 -199
  206. janito/agent/tools/python_stdin_runner.py +0 -208
  207. janito/agent/tools/replace_file.py +0 -72
  208. janito/agent/tools/run_bash_command.py +0 -218
  209. janito/agent/tools/run_powershell_command.py +0 -251
  210. janito/agent/tools_utils/__init__.py +0 -1
  211. janito/agent/tools_utils/action_type.py +0 -7
  212. janito/agent/tools_utils/test_gitignore_utils.py +0 -46
  213. janito/cli/_livereload_log_utils.py +0 -13
  214. janito/cli/_print_config.py +0 -96
  215. janito/cli/_termweb_log_utils.py +0 -17
  216. janito/cli/_utils.py +0 -9
  217. janito/cli/arg_parser.py +0 -272
  218. janito/cli/cli_main.py +0 -281
  219. janito/cli/config_commands.py +0 -211
  220. janito/cli/config_runner.py +0 -35
  221. janito/cli/formatting_runner.py +0 -12
  222. janito/cli/livereload_starter.py +0 -60
  223. janito/cli/logging_setup.py +0 -38
  224. janito/cli/one_shot.py +0 -80
  225. janito/livereload/app.py +0 -25
  226. janito/rich_utils.py +0 -59
  227. janito/shell/__init__.py +0 -0
  228. janito/shell/commands/__init__.py +0 -61
  229. janito/shell/commands/config.py +0 -22
  230. janito/shell/commands/edit.py +0 -24
  231. janito/shell/commands/history_view.py +0 -18
  232. janito/shell/commands/lang.py +0 -19
  233. janito/shell/commands/livelogs.py +0 -42
  234. janito/shell/commands/prompt.py +0 -62
  235. janito/shell/commands/termweb_log.py +0 -94
  236. janito/shell/commands/tools.py +0 -26
  237. janito/shell/commands/track.py +0 -36
  238. janito/shell/main.py +0 -326
  239. janito/shell/prompt/load_prompt.py +0 -57
  240. janito/shell/prompt/session_setup.py +0 -57
  241. janito/shell/session/config.py +0 -109
  242. janito/shell/session/history.py +0 -0
  243. janito/shell/ui/interactive.py +0 -226
  244. janito/termweb/static/editor.css +0 -158
  245. janito/termweb/static/editor.css.bak +0 -145
  246. janito/termweb/static/editor.html +0 -46
  247. janito/termweb/static/editor.html.bak +0 -46
  248. janito/termweb/static/editor.js +0 -265
  249. janito/termweb/static/editor.js.bak +0 -259
  250. janito/termweb/static/explorer.html.bak +0 -59
  251. janito/termweb/static/favicon.ico +0 -0
  252. janito/termweb/static/favicon.ico.bak +0 -0
  253. janito/termweb/static/index.html +0 -53
  254. janito/termweb/static/index.html.bak +0 -54
  255. janito/termweb/static/index.html.bak.bak +0 -175
  256. janito/termweb/static/landing.html.bak +0 -36
  257. janito/termweb/static/termicon.svg +0 -1
  258. janito/termweb/static/termweb.css +0 -214
  259. janito/termweb/static/termweb.css.bak +0 -237
  260. janito/termweb/static/termweb.js +0 -162
  261. janito/termweb/static/termweb.js.bak +0 -168
  262. janito/termweb/static/termweb.js.bak.bak +0 -157
  263. janito/termweb/static/termweb_quickopen.js +0 -135
  264. janito/termweb/static/termweb_quickopen.js.bak +0 -125
  265. janito/tests/test_rich_utils.py +0 -44
  266. janito/web/__init__.py +0 -0
  267. janito/web/__main__.py +0 -25
  268. janito/web/app.py +0 -145
  269. janito-1.14.3.dist-info/METADATA +0 -313
  270. janito-1.14.3.dist-info/RECORD +0 -162
  271. janito-1.14.3.dist-info/licenses/LICENSE +0 -21
  272. /janito/{shell → cli/chat_mode/shell}/input_history.py +0 -0
  273. /janito/{shell/commands/session.py → cli/chat_mode/shell/session/history.py} +0 -0
  274. /janito/{agent/tools_utils/formatting.py → formatting.py} +0 -0
  275. /janito/{agent/tools_utils/gitignore_utils.py → gitignore_utils.py} +0 -0
  276. /janito/{agent/platform_discovery.py → platform_discovery.py} +0 -0
  277. /janito/{agent/tools → tools/adapters/local}/get_file_outline/__init__.py +0 -0
  278. /janito/{agent/tools → tools/adapters/local}/get_file_outline/markdown_outline.py +0 -0
  279. /janito/{agent/tools → tools/adapters/local}/search_text/__init__.py +0 -0
  280. /janito/{agent/tools → tools/adapters/local}/validate_file_syntax/__init__.py +0 -0
  281. {janito-1.14.3.dist-info → janito-2.0.1.dist-info}/WHEEL +0 -0
  282. {janito-1.14.3.dist-info → janito-2.0.1.dist-info}/entry_points.txt +0 -0
  283. {janito-1.14.3.dist-info → janito-2.0.1.dist-info}/top_level.txt +0 -0
janito/exceptions.py ADDED
@@ -0,0 +1,23 @@
1
+ class ToolCallException(Exception):
2
+ """
3
+ Exception raised when a tool call fails (e.g., not found, invalid arguments, invocation failure).
4
+ This is distinct from ToolCallError event, which is for event bus notification.
5
+ """
6
+
7
+ def __init__(self, tool_name, error, arguments=None, exception=None):
8
+ self.tool_name = tool_name
9
+ self.error = error
10
+ self.arguments = arguments
11
+ self.original_exception = exception
12
+ super().__init__(f"ToolCallException: {tool_name}: {error}")
13
+
14
+
15
+ class MissingProviderSelectionException(Exception):
16
+ """
17
+ Raised when no provider is specified and no default provider is set.
18
+ """
19
+
20
+ def __init__(self, configured=None, supported=None):
21
+ self.configured = configured or []
22
+ self.supported = supported or []
23
+ super().__init__("No provider specified and no default provider is set.")
@@ -0,0 +1,54 @@
1
+ """
2
+ Token summary formatter for rich and pt markup.
3
+ - Used to display token/message counters after completions.
4
+ """
5
+
6
+ from janito.perf_singleton import performance_collector
7
+
8
+ from rich.rule import Rule
9
+
10
+
11
+ def format_tokens(n, tag=None, use_rich=False):
12
+ if n is None:
13
+ return "?"
14
+ if n < 1000:
15
+ val = str(n)
16
+ elif n < 1000000:
17
+ val = f"{n/1000:.1f}k"
18
+ else:
19
+ val = f"{n/1000000:.1f}M"
20
+ if tag:
21
+ if use_rich:
22
+ return f"[{tag}]{val}[/{tag}]"
23
+ else:
24
+ return f"<{tag}>{val}</{tag}>"
25
+ return val
26
+
27
+
28
+ def format_token_message_summary(msg_count, usage, width=96, use_rich=False):
29
+ """
30
+ Returns a string (rich or pt markup) summarizing message count and last token usage.
31
+ """
32
+ left = f" Messages: {'[' if use_rich else '<'}msg_count{']' if use_rich else '>'}{msg_count}{'[/msg_count]' if use_rich else '</msg_count>'}"
33
+ tokens_part = ""
34
+ if usage:
35
+ prompt_tokens = usage.get("prompt_tokens")
36
+ completion_tokens = usage.get("completion_tokens")
37
+ total_tokens = usage.get("total_tokens")
38
+ tokens_part = (
39
+ f" | Tokens - Prompt: {format_tokens(prompt_tokens, 'tokens_in', use_rich)}, "
40
+ f"Completion: {format_tokens(completion_tokens, 'tokens_out', use_rich)}, "
41
+ f"Total: {format_tokens(total_tokens, 'tokens_total', use_rich)}"
42
+ )
43
+ return f"{left}{tokens_part}"
44
+
45
+
46
+ def print_token_message_summary(console, msg_count=None, usage=None, width=96):
47
+ """Prints the summary using rich markup, using defaults from perf_singleton if not given."""
48
+ if usage is None:
49
+ usage = performance_collector.get_last_request_usage()
50
+ if msg_count is None:
51
+ msg_count = performance_collector.get_total_turns() or 0
52
+ line = format_token_message_summary(msg_count, usage, width, use_rich=True)
53
+ if line.strip():
54
+ console.print(Rule(line))
janito/i18n/pt.py CHANGED
@@ -1,3 +1,4 @@
1
+ # pragma: allowlist secret
1
2
  translations = {
2
3
  "36107ed78ab25f6fb12ad8ce13018cd1ce6735d1": "Iniciando servidor web...",
3
4
  "70a0d194687568a47aa617fd85036ace1e69a982": "Deseja realmente sair? (s/n): ",
janito/llm/__init__.py ADDED
@@ -0,0 +1,5 @@
1
+ # janito.llm package
2
+
3
+ from .driver import LLMDriver
4
+ from .provider import LLMProvider
5
+ from .driver_config import LLMDriverConfig
janito/llm/agent.py ADDED
@@ -0,0 +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
janito/llm/auth.py ADDED
@@ -0,0 +1,62 @@
1
+ """
2
+ LLMAuthManager: Handles authentication credentials for LLM providers, persisted in ~/.janito/auth.json or a custom path.
3
+ """
4
+
5
+ import os
6
+ import json
7
+ from typing import Dict, Optional
8
+
9
+
10
+ class LLMAuthManager:
11
+ """
12
+ Manages authentication tokens, API keys, or credentials for LLM providers.
13
+ Persists credentials in ~/.janito/auth.json or a custom path.
14
+ """
15
+
16
+ def __init__(self, auth_file: Optional[str] = None):
17
+ if auth_file is not None:
18
+ self._auth_file = os.path.expanduser(auth_file)
19
+ else:
20
+ self._auth_file = os.path.expanduser("~/.janito/auth.json")
21
+ self._credentials: Dict[str, str] = {}
22
+ self._load_credentials()
23
+
24
+ def _load_credentials(self):
25
+ if os.path.exists(self._auth_file):
26
+ try:
27
+ with open(self._auth_file, "r") as f:
28
+ self._credentials = json.load(f)
29
+ except Exception:
30
+ self._credentials = {}
31
+ else:
32
+ self._credentials = {}
33
+
34
+ def _save_credentials(self):
35
+ os.makedirs(os.path.dirname(self._auth_file), exist_ok=True)
36
+ with open(self._auth_file, "w") as f:
37
+ json.dump(self._credentials, f, indent=2)
38
+
39
+ def set_credentials(self, provider_name: str, credentials: str) -> None:
40
+ """
41
+ Store credentials for a given provider and persist to disk. Raises ValueError if provider is unknown.
42
+ """
43
+ from janito.providers.registry import LLMProviderRegistry
44
+
45
+ if provider_name not in LLMProviderRegistry.list_providers():
46
+ raise ValueError(f"Unknown provider: {provider_name}")
47
+ self._credentials[provider_name] = credentials
48
+ self._save_credentials()
49
+
50
+ def get_credentials(self, provider_name: str) -> Optional[str]:
51
+ """
52
+ Retrieve credentials for a given provider.
53
+ """
54
+ return self._credentials.get(provider_name)
55
+
56
+ def remove_credentials(self, provider_name: str) -> None:
57
+ """
58
+ Remove credentials for a given provider and update disk.
59
+ """
60
+ if provider_name in self._credentials:
61
+ del self._credentials[provider_name]
62
+ self._save_credentials()