janito 1.14.2__py3-none-any.whl → 2.0.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.
- janito/__init__.py +6 -1
- janito/__main__.py +1 -1
- janito/agent/setup_agent.py +139 -0
- janito/agent/templates/profiles/{system_prompt_template_base.txt.j2 → system_prompt_template_main.txt.j2} +1 -1
- janito/cli/__init__.py +9 -0
- janito/cli/chat_mode/bindings.py +37 -0
- janito/cli/chat_mode/chat_entry.py +23 -0
- janito/cli/chat_mode/prompt_style.py +19 -0
- janito/cli/chat_mode/session.py +272 -0
- janito/{shell/prompt/completer.py → cli/chat_mode/shell/autocomplete.py} +7 -6
- janito/cli/chat_mode/shell/commands/__init__.py +55 -0
- janito/cli/chat_mode/shell/commands/base.py +9 -0
- janito/cli/chat_mode/shell/commands/clear.py +12 -0
- janito/{shell → cli/chat_mode/shell}/commands/conversation_restart.py +34 -30
- janito/cli/chat_mode/shell/commands/edit.py +25 -0
- janito/cli/chat_mode/shell/commands/help.py +16 -0
- janito/cli/chat_mode/shell/commands/history_view.py +93 -0
- janito/cli/chat_mode/shell/commands/lang.py +25 -0
- janito/cli/chat_mode/shell/commands/last.py +137 -0
- janito/cli/chat_mode/shell/commands/livelogs.py +49 -0
- janito/cli/chat_mode/shell/commands/multi.py +51 -0
- janito/cli/chat_mode/shell/commands/prompt.py +64 -0
- janito/cli/chat_mode/shell/commands/role.py +36 -0
- janito/cli/chat_mode/shell/commands/session.py +40 -0
- janito/{shell → cli/chat_mode/shell}/commands/session_control.py +2 -2
- janito/cli/chat_mode/shell/commands/termweb_log.py +92 -0
- janito/cli/chat_mode/shell/commands/tools.py +32 -0
- janito/{shell → cli/chat_mode/shell}/commands/utility.py +4 -7
- janito/{shell → cli/chat_mode/shell}/commands/verbose.py +5 -5
- janito/cli/chat_mode/shell/session/__init__.py +1 -0
- janito/{shell → cli/chat_mode/shell}/session/manager.py +9 -1
- janito/cli/chat_mode/toolbar.py +90 -0
- janito/cli/cli_commands/list_models.py +35 -0
- janito/cli/cli_commands/list_providers.py +9 -0
- janito/cli/cli_commands/list_tools.py +53 -0
- janito/cli/cli_commands/model_selection.py +50 -0
- janito/cli/cli_commands/model_utils.py +84 -0
- janito/cli/cli_commands/set_api_key.py +19 -0
- janito/cli/cli_commands/show_config.py +51 -0
- janito/cli/cli_commands/show_system_prompt.py +62 -0
- janito/cli/config.py +28 -0
- janito/cli/console.py +3 -0
- janito/cli/core/__init__.py +4 -0
- janito/cli/core/event_logger.py +59 -0
- janito/cli/core/getters.py +31 -0
- janito/cli/core/runner.py +141 -0
- janito/cli/core/setters.py +174 -0
- janito/cli/core/unsetters.py +54 -0
- janito/cli/main.py +8 -196
- janito/cli/main_cli.py +312 -0
- janito/cli/prompt_core.py +230 -0
- janito/cli/prompt_handler.py +6 -0
- janito/cli/rich_terminal_reporter.py +101 -0
- janito/cli/single_shot_mode/__init__.py +6 -0
- janito/cli/single_shot_mode/handler.py +137 -0
- janito/cli/termweb_starter.py +73 -24
- janito/cli/utils.py +25 -0
- janito/cli/verbose_output.py +196 -0
- janito/config.py +5 -0
- janito/config_manager.py +110 -0
- janito/conversation_history.py +30 -0
- janito/{agent/tools_utils/dir_walk_utils.py → dir_walk_utils.py} +3 -2
- janito/driver_events.py +98 -0
- janito/drivers/anthropic/driver.py +113 -0
- janito/drivers/azure_openai/driver.py +36 -0
- janito/drivers/driver_registry.py +33 -0
- janito/drivers/google_genai/driver.py +54 -0
- janito/drivers/google_genai/schema_generator.py +67 -0
- janito/drivers/mistralai/driver.py +41 -0
- janito/drivers/openai/driver.py +334 -0
- janito/event_bus/__init__.py +2 -0
- janito/event_bus/bus.py +68 -0
- janito/event_bus/event.py +15 -0
- janito/event_bus/handler.py +31 -0
- janito/event_bus/queue_bus.py +57 -0
- janito/exceptions.py +23 -0
- janito/formatting_token.py +54 -0
- janito/i18n/pt.py +1 -0
- janito/llm/__init__.py +5 -0
- janito/llm/agent.py +443 -0
- janito/llm/auth.py +62 -0
- janito/llm/driver.py +239 -0
- janito/llm/driver_config.py +34 -0
- janito/llm/driver_config_builder.py +34 -0
- janito/llm/driver_input.py +12 -0
- janito/llm/message_parts.py +60 -0
- janito/llm/model.py +38 -0
- janito/llm/provider.py +187 -0
- janito/perf_singleton.py +3 -0
- janito/performance_collector.py +167 -0
- janito/provider_config.py +98 -0
- janito/provider_registry.py +152 -0
- janito/providers/__init__.py +7 -0
- janito/providers/anthropic/model_info.py +22 -0
- janito/providers/anthropic/provider.py +65 -0
- janito/providers/azure_openai/model_info.py +15 -0
- janito/providers/azure_openai/provider.py +72 -0
- janito/providers/deepseek/__init__.py +1 -0
- janito/providers/deepseek/model_info.py +16 -0
- janito/providers/deepseek/provider.py +91 -0
- janito/providers/google/__init__.py +1 -0
- janito/providers/google/model_info.py +40 -0
- janito/providers/google/provider.py +69 -0
- janito/providers/mistralai/model_info.py +37 -0
- janito/providers/mistralai/provider.py +69 -0
- janito/providers/openai/__init__.py +1 -0
- janito/providers/openai/model_info.py +137 -0
- janito/providers/openai/provider.py +107 -0
- janito/providers/openai/schema_generator.py +63 -0
- janito/providers/provider_static_info.py +21 -0
- janito/providers/registry.py +26 -0
- janito/report_events.py +38 -0
- janito/termweb/app.py +1 -1
- janito/tools/__init__.py +16 -0
- janito/tools/adapters/__init__.py +1 -0
- janito/tools/adapters/local/__init__.py +54 -0
- janito/tools/adapters/local/adapter.py +92 -0
- janito/{agent/tools → tools/adapters/local}/ask_user.py +30 -13
- janito/tools/adapters/local/copy_file.py +84 -0
- janito/{agent/tools → tools/adapters/local}/create_directory.py +11 -10
- janito/tools/adapters/local/create_file.py +82 -0
- janito/tools/adapters/local/delete_text_in_file.py +136 -0
- janito/{agent/tools → tools/adapters/local}/fetch_url.py +18 -19
- janito/tools/adapters/local/find_files.py +140 -0
- janito/tools/adapters/local/get_file_outline/core.py +151 -0
- janito/{agent/tools → tools/adapters/local}/get_file_outline/python_outline.py +125 -0
- janito/tools/adapters/local/get_file_outline/python_outline_v2.py +156 -0
- janito/{agent/tools → tools/adapters/local}/get_file_outline/search_outline.py +12 -7
- janito/{agent/tools → tools/adapters/local}/move_file.py +13 -9
- janito/{agent/tools → tools/adapters/local}/open_url.py +7 -5
- janito/tools/adapters/local/python_code_run.py +165 -0
- janito/tools/adapters/local/python_command_run.py +163 -0
- janito/tools/adapters/local/python_file_run.py +162 -0
- janito/{agent/tools → tools/adapters/local}/remove_directory.py +15 -9
- janito/{agent/tools → tools/adapters/local}/remove_file.py +17 -14
- janito/{agent/tools → tools/adapters/local}/replace_text_in_file.py +27 -22
- janito/tools/adapters/local/run_bash_command.py +176 -0
- janito/tools/adapters/local/run_powershell_command.py +219 -0
- janito/{agent/tools → tools/adapters/local}/search_text/core.py +32 -12
- janito/{agent/tools → tools/adapters/local}/search_text/match_lines.py +13 -4
- janito/{agent/tools → tools/adapters/local}/search_text/pattern_utils.py +12 -4
- janito/{agent/tools → tools/adapters/local}/search_text/traverse_directory.py +15 -2
- janito/{agent/tools → tools/adapters/local}/validate_file_syntax/core.py +12 -11
- janito/{agent/tools → tools/adapters/local}/validate_file_syntax/css_validator.py +1 -1
- janito/{agent/tools → tools/adapters/local}/validate_file_syntax/html_validator.py +1 -1
- janito/{agent/tools → tools/adapters/local}/validate_file_syntax/js_validator.py +1 -1
- janito/{agent/tools → tools/adapters/local}/validate_file_syntax/json_validator.py +1 -1
- janito/{agent/tools → tools/adapters/local}/validate_file_syntax/markdown_validator.py +1 -1
- janito/{agent/tools → tools/adapters/local}/validate_file_syntax/ps1_validator.py +1 -1
- janito/{agent/tools → tools/adapters/local}/validate_file_syntax/python_validator.py +1 -1
- janito/{agent/tools → tools/adapters/local}/validate_file_syntax/xml_validator.py +1 -1
- janito/{agent/tools → tools/adapters/local}/validate_file_syntax/yaml_validator.py +1 -1
- janito/{agent/tools/get_lines.py → tools/adapters/local/view_file.py} +45 -27
- janito/tools/inspect_registry.py +17 -0
- janito/tools/tool_base.py +105 -0
- janito/tools/tool_events.py +58 -0
- janito/tools/tool_run_exception.py +12 -0
- janito/{agent → tools}/tool_use_tracker.py +2 -4
- janito/{agent/tools_utils/utils.py → tools/tool_utils.py} +18 -9
- janito/tools/tools_adapter.py +207 -0
- janito/tools/tools_schema.py +104 -0
- janito/utils.py +11 -0
- janito/version.py +4 -0
- janito-2.0.0.dist-info/METADATA +232 -0
- janito-2.0.0.dist-info/RECORD +180 -0
- janito/agent/__init__.py +0 -0
- janito/agent/api_exceptions.py +0 -4
- janito/agent/config.py +0 -147
- janito/agent/config_defaults.py +0 -12
- janito/agent/config_utils.py +0 -0
- janito/agent/content_handler.py +0 -0
- janito/agent/conversation.py +0 -238
- janito/agent/conversation_api.py +0 -306
- janito/agent/conversation_exceptions.py +0 -18
- janito/agent/conversation_tool_calls.py +0 -39
- janito/agent/conversation_ui.py +0 -17
- janito/agent/event.py +0 -24
- janito/agent/event_dispatcher.py +0 -24
- janito/agent/event_handler_protocol.py +0 -5
- janito/agent/event_system.py +0 -15
- janito/agent/llm_conversation_history.py +0 -82
- janito/agent/message_handler.py +0 -20
- janito/agent/message_handler_protocol.py +0 -5
- janito/agent/openai_client.py +0 -149
- janito/agent/openai_schema_generator.py +0 -187
- janito/agent/profile_manager.py +0 -96
- janito/agent/queued_message_handler.py +0 -50
- janito/agent/rich_live.py +0 -32
- janito/agent/rich_message_handler.py +0 -115
- janito/agent/runtime_config.py +0 -36
- janito/agent/test_handler_protocols.py +0 -47
- janito/agent/test_openai_schema_generator.py +0 -93
- janito/agent/tests/__init__.py +0 -1
- janito/agent/tool_base.py +0 -63
- janito/agent/tool_executor.py +0 -122
- janito/agent/tool_registry.py +0 -49
- janito/agent/tools/__init__.py +0 -47
- janito/agent/tools/create_file.py +0 -59
- janito/agent/tools/delete_text_in_file.py +0 -97
- janito/agent/tools/find_files.py +0 -106
- janito/agent/tools/get_file_outline/core.py +0 -81
- janito/agent/tools/present_choices.py +0 -64
- janito/agent/tools/python_command_runner.py +0 -201
- janito/agent/tools/python_file_runner.py +0 -199
- janito/agent/tools/python_stdin_runner.py +0 -208
- janito/agent/tools/replace_file.py +0 -72
- janito/agent/tools/run_bash_command.py +0 -218
- janito/agent/tools/run_powershell_command.py +0 -251
- janito/agent/tools_utils/__init__.py +0 -1
- janito/agent/tools_utils/action_type.py +0 -7
- janito/agent/tools_utils/test_gitignore_utils.py +0 -46
- janito/cli/_livereload_log_utils.py +0 -13
- janito/cli/_print_config.py +0 -96
- janito/cli/_termweb_log_utils.py +0 -17
- janito/cli/_utils.py +0 -9
- janito/cli/arg_parser.py +0 -272
- janito/cli/cli_main.py +0 -281
- janito/cli/config_commands.py +0 -211
- janito/cli/config_runner.py +0 -35
- janito/cli/formatting_runner.py +0 -12
- janito/cli/livereload_starter.py +0 -60
- janito/cli/logging_setup.py +0 -38
- janito/cli/one_shot.py +0 -80
- janito/livereload/app.py +0 -25
- janito/rich_utils.py +0 -59
- janito/shell/__init__.py +0 -0
- janito/shell/commands/__init__.py +0 -61
- janito/shell/commands/config.py +0 -22
- janito/shell/commands/edit.py +0 -24
- janito/shell/commands/history_view.py +0 -18
- janito/shell/commands/lang.py +0 -19
- janito/shell/commands/livelogs.py +0 -42
- janito/shell/commands/prompt.py +0 -62
- janito/shell/commands/termweb_log.py +0 -94
- janito/shell/commands/tools.py +0 -26
- janito/shell/commands/track.py +0 -36
- janito/shell/main.py +0 -326
- janito/shell/prompt/load_prompt.py +0 -57
- janito/shell/prompt/session_setup.py +0 -57
- janito/shell/session/config.py +0 -109
- janito/shell/session/history.py +0 -0
- janito/shell/ui/interactive.py +0 -226
- janito/termweb/static/editor.css +0 -158
- janito/termweb/static/editor.css.bak +0 -145
- janito/termweb/static/editor.html +0 -46
- janito/termweb/static/editor.html.bak +0 -46
- janito/termweb/static/editor.js +0 -265
- janito/termweb/static/editor.js.bak +0 -259
- janito/termweb/static/explorer.html.bak +0 -59
- janito/termweb/static/favicon.ico +0 -0
- janito/termweb/static/favicon.ico.bak +0 -0
- janito/termweb/static/index.html +0 -53
- janito/termweb/static/index.html.bak +0 -54
- janito/termweb/static/index.html.bak.bak +0 -175
- janito/termweb/static/landing.html.bak +0 -36
- janito/termweb/static/termicon.svg +0 -1
- janito/termweb/static/termweb.css +0 -214
- janito/termweb/static/termweb.css.bak +0 -237
- janito/termweb/static/termweb.js +0 -162
- janito/termweb/static/termweb.js.bak +0 -168
- janito/termweb/static/termweb.js.bak.bak +0 -157
- janito/termweb/static/termweb_quickopen.js +0 -135
- janito/termweb/static/termweb_quickopen.js.bak +0 -125
- janito/tests/test_rich_utils.py +0 -44
- janito/web/__init__.py +0 -0
- janito/web/__main__.py +0 -25
- janito/web/app.py +0 -145
- janito-1.14.2.dist-info/METADATA +0 -306
- janito-1.14.2.dist-info/RECORD +0 -162
- janito-1.14.2.dist-info/licenses/LICENSE +0 -21
- /janito/{shell → cli/chat_mode/shell}/input_history.py +0 -0
- /janito/{shell/commands/session.py → cli/chat_mode/shell/session/history.py} +0 -0
- /janito/{agent/tools_utils/formatting.py → formatting.py} +0 -0
- /janito/{agent/tools_utils/gitignore_utils.py → gitignore_utils.py} +0 -0
- /janito/{agent/platform_discovery.py → platform_discovery.py} +0 -0
- /janito/{agent/tools → tools/adapters/local}/get_file_outline/__init__.py +0 -0
- /janito/{agent/tools → tools/adapters/local}/get_file_outline/markdown_outline.py +0 -0
- /janito/{agent/tools → tools/adapters/local}/search_text/__init__.py +0 -0
- /janito/{agent/tools → tools/adapters/local}/validate_file_syntax/__init__.py +0 -0
- {janito-1.14.2.dist-info → janito-2.0.0.dist-info}/WHEEL +0 -0
- {janito-1.14.2.dist-info → janito-2.0.0.dist-info}/entry_points.txt +0 -0
- {janito-1.14.2.dist-info → janito-2.0.0.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
janito/llm/__init__.py
ADDED
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()
|