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/llm/driver.py
ADDED
@@ -0,0 +1,239 @@
|
|
1
|
+
import threading
|
2
|
+
from abc import ABC, abstractmethod
|
3
|
+
from queue import Queue
|
4
|
+
from janito.llm.driver_input import DriverInput
|
5
|
+
from janito.driver_events import (
|
6
|
+
RequestStarted,
|
7
|
+
RequestFinished,
|
8
|
+
ResponseReceived,
|
9
|
+
RequestStatus,
|
10
|
+
)
|
11
|
+
|
12
|
+
|
13
|
+
class LLMDriver(ABC):
|
14
|
+
def clear_output_queue(self):
|
15
|
+
"""Remove all items from the output queue."""
|
16
|
+
try:
|
17
|
+
while True:
|
18
|
+
self.output_queue.get_nowait()
|
19
|
+
except Exception:
|
20
|
+
pass
|
21
|
+
|
22
|
+
"""
|
23
|
+
Abstract base class for LLM drivers (threaded, queue-based).
|
24
|
+
Subclasses must implement:
|
25
|
+
- _call_api: Call provider API with DriverInput.
|
26
|
+
- _convert_completion_message_to_parts: Convert provider message to MessagePart objects.
|
27
|
+
- convert_history_to_api_messages: Convert LLMConversationHistory to provider-specific messages format for API calls.
|
28
|
+
Workflow:
|
29
|
+
- Accept DriverInput via input_queue.
|
30
|
+
- Put DriverEvents on output_queue.
|
31
|
+
- Use start() to launch worker loop in a thread.
|
32
|
+
The driver automatically creates its own input/output queues, accessible via .input_queue and .output_queue.
|
33
|
+
"""
|
34
|
+
|
35
|
+
available = True
|
36
|
+
unavailable_reason = None
|
37
|
+
|
38
|
+
def __init__(self, tools_adapter=None, provider_name=None):
|
39
|
+
self.input_queue = Queue()
|
40
|
+
self.output_queue = Queue()
|
41
|
+
self._thread = None
|
42
|
+
self.tools_adapter = tools_adapter
|
43
|
+
self.provider_name = provider_name
|
44
|
+
|
45
|
+
def start(self):
|
46
|
+
"""Validate tool schemas (if any) and launch the driver's background thread to process DriverInput objects."""
|
47
|
+
# Validate all tool schemas before starting the thread
|
48
|
+
if self.tools_adapter is not None:
|
49
|
+
from janito.tools.tools_schema import ToolSchemaBase
|
50
|
+
validator = ToolSchemaBase()
|
51
|
+
for tool in self.tools_adapter.get_tools():
|
52
|
+
# Validate the tool's class (not instance)
|
53
|
+
validator.validate_tool_class(tool.__class__)
|
54
|
+
self._thread = threading.Thread(target=self._run, daemon=True)
|
55
|
+
self._thread.start()
|
56
|
+
|
57
|
+
def _run(self):
|
58
|
+
while True:
|
59
|
+
driver_input = self.input_queue.get()
|
60
|
+
if driver_input is None:
|
61
|
+
break # Sentinel received, exit thread
|
62
|
+
try:
|
63
|
+
# Only process if driver_input is a DriverInput instance
|
64
|
+
if isinstance(driver_input, DriverInput):
|
65
|
+
self.process_driver_input(driver_input)
|
66
|
+
else:
|
67
|
+
# Optionally log or handle unexpected input types
|
68
|
+
pass
|
69
|
+
except Exception as e:
|
70
|
+
import traceback
|
71
|
+
|
72
|
+
self.output_queue.put(
|
73
|
+
RequestFinished(
|
74
|
+
driver_name=self.__class__.__name__,
|
75
|
+
request_id=getattr(driver_input.config, "request_id", None),
|
76
|
+
status=RequestStatus.ERROR,
|
77
|
+
error=str(e),
|
78
|
+
exception=e,
|
79
|
+
traceback=traceback.format_exc(),
|
80
|
+
)
|
81
|
+
)
|
82
|
+
|
83
|
+
def handle_driver_unavailable(self, request_id):
|
84
|
+
self.output_queue.put(
|
85
|
+
RequestFinished(
|
86
|
+
driver_name=self.__class__.__name__,
|
87
|
+
request_id=request_id,
|
88
|
+
status=RequestStatus.ERROR,
|
89
|
+
error=self.unavailable_reason,
|
90
|
+
exception=ImportError(self.unavailable_reason),
|
91
|
+
traceback=None,
|
92
|
+
)
|
93
|
+
)
|
94
|
+
|
95
|
+
def emit_response_received(
|
96
|
+
self, driver_name, request_id, result, parts, timestamp=None, metadata=None
|
97
|
+
):
|
98
|
+
self.output_queue.put(
|
99
|
+
ResponseReceived(
|
100
|
+
driver_name=driver_name,
|
101
|
+
request_id=request_id,
|
102
|
+
parts=parts,
|
103
|
+
tool_results=[],
|
104
|
+
timestamp=timestamp,
|
105
|
+
metadata=metadata or {},
|
106
|
+
)
|
107
|
+
)
|
108
|
+
# Debug: print summary of parts by type
|
109
|
+
if hasattr(self, "config") and getattr(self.config, "verbose_api", False):
|
110
|
+
from collections import Counter
|
111
|
+
|
112
|
+
type_counts = Counter(type(p).__name__ for p in parts)
|
113
|
+
print(
|
114
|
+
f"[verbose-api] Emitting ResponseReceived with parts: {dict(type_counts)}",
|
115
|
+
flush=True,
|
116
|
+
)
|
117
|
+
|
118
|
+
def process_driver_input(self, driver_input: DriverInput):
|
119
|
+
|
120
|
+
config = driver_input.config
|
121
|
+
request_id = getattr(config, "request_id", None)
|
122
|
+
if not self.available:
|
123
|
+
self.handle_driver_unavailable(request_id)
|
124
|
+
return
|
125
|
+
self.output_queue.put(
|
126
|
+
RequestStarted(
|
127
|
+
driver_name=self.__class__.__name__,
|
128
|
+
request_id=request_id,
|
129
|
+
payload={"provider_name": self.provider_name},
|
130
|
+
)
|
131
|
+
)
|
132
|
+
# Check for cancel_event before starting
|
133
|
+
if (
|
134
|
+
hasattr(driver_input, "cancel_event")
|
135
|
+
and driver_input.cancel_event is not None
|
136
|
+
and driver_input.cancel_event.is_set()
|
137
|
+
):
|
138
|
+
self.output_queue.put(
|
139
|
+
RequestFinished(
|
140
|
+
driver_name=self.__class__.__name__,
|
141
|
+
request_id=request_id,
|
142
|
+
status=RequestStatus.CANCELLED,
|
143
|
+
reason="Canceled before start",
|
144
|
+
)
|
145
|
+
)
|
146
|
+
return
|
147
|
+
try:
|
148
|
+
result = self._call_api(driver_input)
|
149
|
+
# If result is None and cancel_event is set, treat as cancelled
|
150
|
+
if (
|
151
|
+
hasattr(driver_input, "cancel_event")
|
152
|
+
and driver_input.cancel_event is not None
|
153
|
+
and driver_input.cancel_event.is_set()
|
154
|
+
):
|
155
|
+
self.output_queue.put(
|
156
|
+
RequestFinished(
|
157
|
+
driver_name=self.__class__.__name__,
|
158
|
+
request_id=request_id,
|
159
|
+
status=RequestStatus.CANCELLED,
|
160
|
+
reason="Cancelled during processing (post-API)",
|
161
|
+
)
|
162
|
+
)
|
163
|
+
return
|
164
|
+
if (
|
165
|
+
result is None
|
166
|
+
and hasattr(driver_input, "cancel_event")
|
167
|
+
and driver_input.cancel_event is not None
|
168
|
+
and driver_input.cancel_event.is_set()
|
169
|
+
):
|
170
|
+
# Already handled by driver
|
171
|
+
return
|
172
|
+
# Check for cancel_event after API call (subclasses should also check during long calls)
|
173
|
+
if (
|
174
|
+
hasattr(driver_input, "cancel_event")
|
175
|
+
and driver_input.cancel_event is not None
|
176
|
+
and driver_input.cancel_event.is_set()
|
177
|
+
):
|
178
|
+
self.output_queue.put(
|
179
|
+
RequestFinished(
|
180
|
+
driver_name=self.__class__.__name__,
|
181
|
+
request_id=request_id,
|
182
|
+
status=RequestStatus.CANCELLED,
|
183
|
+
reason="Canceled during processing",
|
184
|
+
)
|
185
|
+
)
|
186
|
+
return
|
187
|
+
message = self._get_message_from_result(result)
|
188
|
+
parts = (
|
189
|
+
self._convert_completion_message_to_parts(message) if message else []
|
190
|
+
)
|
191
|
+
timestamp = getattr(result, "created", None)
|
192
|
+
metadata = {"usage": getattr(result, "usage", None), "raw_response": result}
|
193
|
+
self.emit_response_received(
|
194
|
+
self.__class__.__name__, request_id, result, parts, timestamp, metadata
|
195
|
+
)
|
196
|
+
except Exception as ex:
|
197
|
+
import traceback
|
198
|
+
|
199
|
+
self.output_queue.put(
|
200
|
+
RequestFinished(
|
201
|
+
driver_name=self.__class__.__name__,
|
202
|
+
request_id=request_id,
|
203
|
+
status=RequestStatus.ERROR,
|
204
|
+
error=str(ex),
|
205
|
+
exception=ex,
|
206
|
+
traceback=traceback.format_exc(),
|
207
|
+
)
|
208
|
+
)
|
209
|
+
|
210
|
+
@abstractmethod
|
211
|
+
def _prepare_api_kwargs(self, config, conversation):
|
212
|
+
"""
|
213
|
+
Subclasses must implement: Prepare API kwargs for the provider, including any tool schemas if needed.
|
214
|
+
"""
|
215
|
+
pass
|
216
|
+
|
217
|
+
@abstractmethod
|
218
|
+
def _call_api(self, driver_input: DriverInput):
|
219
|
+
"""Subclasses implement: Use driver_input to call provider and return result object."""
|
220
|
+
pass
|
221
|
+
|
222
|
+
@abstractmethod
|
223
|
+
def _convert_completion_message_to_parts(self, message):
|
224
|
+
"""Subclasses implement: Convert provider message to list of MessagePart objects."""
|
225
|
+
pass
|
226
|
+
|
227
|
+
@abstractmethod
|
228
|
+
def convert_history_to_api_messages(self, conversation_history):
|
229
|
+
"""
|
230
|
+
Subclasses implement: Convert LLMConversationHistory to the messages object required by their provider API.
|
231
|
+
:param conversation_history: LLMConversationHistory instance
|
232
|
+
:return: Provider-specific messages object (e.g., list of dicts for OpenAI)
|
233
|
+
"""
|
234
|
+
pass
|
235
|
+
|
236
|
+
@abstractmethod
|
237
|
+
def _get_message_from_result(self, result):
|
238
|
+
"""Extract the message object from the provider result. Subclasses must implement this."""
|
239
|
+
raise NotImplementedError("Subclasses must implement _get_message_from_result.")
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from dataclasses import dataclass, field
|
2
|
+
from typing import Any, Optional
|
3
|
+
|
4
|
+
|
5
|
+
@dataclass
|
6
|
+
class LLMDriverConfig:
|
7
|
+
# For OpenAI and similar providers that distinguish between completion and response tokens
|
8
|
+
max_completion_tokens: Optional[int] = None
|
9
|
+
verbose_api: Optional[bool] = None
|
10
|
+
"""
|
11
|
+
Common configuration container for LLM drivers.
|
12
|
+
- verbose_api: Print API trace info if set
|
13
|
+
|
14
|
+
Holds standard attributes that most LLM drivers require (used as a config or schema reference object).
|
15
|
+
Inspired by the OpenAI driver, but fields are generic for most LLM backends.
|
16
|
+
"""
|
17
|
+
model: str = None # Model is required but can be set from CLI
|
18
|
+
api_key: Optional[str] = None
|
19
|
+
base_url: Optional[str] = None
|
20
|
+
max_tokens: Optional[int] = None
|
21
|
+
temperature: Optional[float] = None
|
22
|
+
top_p: Optional[float] = None
|
23
|
+
presence_penalty: Optional[float] = None
|
24
|
+
frequency_penalty: Optional[float] = None
|
25
|
+
stop: Optional[Any] = None # list or string, depending on backend
|
26
|
+
extra: dict = field(
|
27
|
+
default_factory=dict
|
28
|
+
) # for provider-specific miscellaneous config fields
|
29
|
+
|
30
|
+
def to_dict(self) -> dict:
|
31
|
+
d = self.__dict__.copy()
|
32
|
+
d.update(d.pop("extra", {}))
|
33
|
+
# Remove Nones (for compatibility)
|
34
|
+
return {k: v for k, v in d.items() if v is not None}
|
@@ -0,0 +1,34 @@
|
|
1
|
+
from typing import Type, Dict, Any
|
2
|
+
from janito.llm.driver_config import LLMDriverConfig
|
3
|
+
|
4
|
+
|
5
|
+
def build_llm_driver_config(
|
6
|
+
config: Dict[str, Any], driver_class: Type
|
7
|
+
) -> LLMDriverConfig:
|
8
|
+
"""
|
9
|
+
Build an LLMDriverConfig instance for the given driver class based on its declared driver_fields.
|
10
|
+
Only fills fields missing from given config; does not overwrite fields already provided.
|
11
|
+
Any config fields not in driver_fields or LLMDriverConfig fields go into .extra.
|
12
|
+
"""
|
13
|
+
driver_fields = getattr(driver_class, "driver_fields", None)
|
14
|
+
if driver_fields is None:
|
15
|
+
driver_fields = set(LLMDriverConfig.__dataclass_fields__.keys()) - {
|
16
|
+
"model",
|
17
|
+
"extra",
|
18
|
+
}
|
19
|
+
base_info = {}
|
20
|
+
extra = {}
|
21
|
+
for k, v in (config or {}).items():
|
22
|
+
if k in driver_fields and k in LLMDriverConfig.__dataclass_fields__:
|
23
|
+
base_info[k] = v
|
24
|
+
else:
|
25
|
+
extra[k] = v
|
26
|
+
# Only set missing fields, do NOT overwrite those from CLI/user
|
27
|
+
for field in driver_fields:
|
28
|
+
if field not in base_info and field in LLMDriverConfig.__dataclass_fields__:
|
29
|
+
base_info[field] = (
|
30
|
+
None # Optional: replace None with provider/driver default if wanted
|
31
|
+
)
|
32
|
+
return LLMDriverConfig(
|
33
|
+
model=config.get("model") or config.get("model_name"), extra=extra, **base_info
|
34
|
+
)
|
@@ -0,0 +1,12 @@
|
|
1
|
+
from dataclasses import dataclass, field
|
2
|
+
from typing import Optional
|
3
|
+
import threading
|
4
|
+
from janito.llm.driver_config import LLMDriverConfig
|
5
|
+
from janito.conversation_history import LLMConversationHistory
|
6
|
+
|
7
|
+
|
8
|
+
@dataclass
|
9
|
+
class DriverInput:
|
10
|
+
config: LLMDriverConfig
|
11
|
+
conversation_history: LLMConversationHistory
|
12
|
+
cancel_event: Optional[threading.Event] = field(default=None)
|
@@ -0,0 +1,60 @@
|
|
1
|
+
import attr
|
2
|
+
|
3
|
+
|
4
|
+
class MessagePart:
|
5
|
+
"""
|
6
|
+
Base class for all driver message parts.
|
7
|
+
"""
|
8
|
+
|
9
|
+
type: str
|
10
|
+
|
11
|
+
|
12
|
+
@attr.s(auto_attribs=True, kw_only=True)
|
13
|
+
class TextMessagePart(MessagePart):
|
14
|
+
content: str = ""
|
15
|
+
|
16
|
+
|
17
|
+
@attr.s(auto_attribs=True, kw_only=True)
|
18
|
+
class InlineDataMessagePart(MessagePart):
|
19
|
+
content: bytes = b""
|
20
|
+
|
21
|
+
|
22
|
+
@attr.s(auto_attribs=True, kw_only=True)
|
23
|
+
class FileDataMessagePart(MessagePart):
|
24
|
+
content: str = ""
|
25
|
+
|
26
|
+
|
27
|
+
@attr.s(auto_attribs=True, kw_only=True)
|
28
|
+
class VideoMetadataMessagePart(MessagePart):
|
29
|
+
content: dict = attr.Factory(dict)
|
30
|
+
|
31
|
+
|
32
|
+
@attr.s(auto_attribs=True, kw_only=True)
|
33
|
+
class CodeExecutionResultMessagePart(MessagePart):
|
34
|
+
content: str = ""
|
35
|
+
stdout: str = ""
|
36
|
+
stderr: str = ""
|
37
|
+
|
38
|
+
|
39
|
+
@attr.s(auto_attribs=True, kw_only=True)
|
40
|
+
class ExecutableCodeMessagePart(MessagePart):
|
41
|
+
content: str = ""
|
42
|
+
|
43
|
+
|
44
|
+
@attr.s(auto_attribs=True, kw_only=True)
|
45
|
+
class FunctionCallMessagePart(MessagePart):
|
46
|
+
tool_call_id: str = ""
|
47
|
+
function: object = (
|
48
|
+
None # Should match OpenAI SDK structure (with arguments as JSON string)
|
49
|
+
)
|
50
|
+
|
51
|
+
|
52
|
+
@attr.s(auto_attribs=True, kw_only=True)
|
53
|
+
class FunctionResponseMessagePart(MessagePart):
|
54
|
+
name: str = ""
|
55
|
+
content: dict = attr.Factory(dict)
|
56
|
+
|
57
|
+
|
58
|
+
@attr.s(auto_attribs=True, kw_only=True)
|
59
|
+
class ThoughtMessagePart(MessagePart):
|
60
|
+
content: bool = False
|
janito/llm/model.py
ADDED
@@ -0,0 +1,38 @@
|
|
1
|
+
from dataclasses import dataclass, field
|
2
|
+
from typing import Any, Optional
|
3
|
+
|
4
|
+
|
5
|
+
@dataclass
|
6
|
+
class LLMModelInfo:
|
7
|
+
name: str
|
8
|
+
context: Any = "N/A"
|
9
|
+
max_input: Any = "N/A"
|
10
|
+
max_cot: Any = "N/A"
|
11
|
+
max_response: Any = "N/A"
|
12
|
+
thinking_supported: Any = "N/A"
|
13
|
+
default_temp: float = 0.2
|
14
|
+
open: Optional[Any] = None
|
15
|
+
category: Optional[str] = None
|
16
|
+
driver: Optional[str] = None
|
17
|
+
# This enables arbitrary provider-specific metadata
|
18
|
+
other: dict = field(default_factory=dict)
|
19
|
+
|
20
|
+
def to_dict(self) -> dict:
|
21
|
+
d = self.__dict__.copy()
|
22
|
+
if not self.open:
|
23
|
+
d.pop("open")
|
24
|
+
if not self.category:
|
25
|
+
d.pop("category")
|
26
|
+
if not self.driver:
|
27
|
+
d.pop("driver")
|
28
|
+
if not self.other:
|
29
|
+
d.pop("other")
|
30
|
+
return d
|
31
|
+
|
32
|
+
@staticmethod
|
33
|
+
def get_model_info(model_specs):
|
34
|
+
"""
|
35
|
+
Standard get_model_info implementation for all providers:
|
36
|
+
returns a list of model info dicts, one per model in the given MODEL_SPECS dict.
|
37
|
+
"""
|
38
|
+
return [m.to_dict() for m in model_specs.values()]
|
janito/llm/provider.py
ADDED
@@ -0,0 +1,187 @@
|
|
1
|
+
from abc import ABC, abstractmethod
|
2
|
+
import importlib
|
3
|
+
from janito.llm.driver import LLMDriver
|
4
|
+
|
5
|
+
|
6
|
+
class LLMProvider(ABC):
|
7
|
+
def create_driver(self):
|
8
|
+
"""
|
9
|
+
Returns a new instance of the configured driver for this provider.
|
10
|
+
Subclasses must implement this method.
|
11
|
+
"""
|
12
|
+
raise NotImplementedError(
|
13
|
+
"LLMProvider subclasses must implement create_driver()."
|
14
|
+
)
|
15
|
+
|
16
|
+
"""
|
17
|
+
Abstract base class for Large Language Model (LLM) providers.
|
18
|
+
|
19
|
+
Provider Usage and Driver Communication Flow:
|
20
|
+
1. Provider class is selected (e.g., OpenAIProvider, MistralProvider).
|
21
|
+
2. An instance of the provider is created. This instance is bound to a specific configuration (LLMDriverConfig) containing model, credentials, etc.
|
22
|
+
3. All drivers created by that provider instance are associated with the bound config.
|
23
|
+
4. To communicate with an LLM, call create_driver() on the provider instance, which yields a driver configured for the attached config. Every driver created via this method inherits the provider's configuration.
|
24
|
+
|
25
|
+
Key: You do not create/configure a driver directly—always go through the provider to ensure correct configuration binding to the provider instance.
|
26
|
+
|
27
|
+
Subclasses must implement the core interface for interacting with LLM APIs and define `provider_name` as a class attribute.
|
28
|
+
"""
|
29
|
+
|
30
|
+
name: str = None # Must be set on subclasses
|
31
|
+
DEFAULT_MODEL: str = None # Should be set by subclasses
|
32
|
+
|
33
|
+
def __init_subclass__(cls, **kwargs):
|
34
|
+
super().__init_subclass__(**kwargs)
|
35
|
+
if (
|
36
|
+
not hasattr(cls, "name")
|
37
|
+
or not isinstance(getattr(cls, "name"), str)
|
38
|
+
or not cls.name
|
39
|
+
):
|
40
|
+
raise TypeError(
|
41
|
+
f"Class {cls.__name__} must define a class attribute 'name' (non-empty str)"
|
42
|
+
)
|
43
|
+
if (
|
44
|
+
not hasattr(cls, "DEFAULT_MODEL")
|
45
|
+
or getattr(cls, "DEFAULT_MODEL", None) is None
|
46
|
+
):
|
47
|
+
raise TypeError(
|
48
|
+
f"Class {cls.__name__} must define a class attribute 'DEFAULT_MODEL' (non-empty str)"
|
49
|
+
)
|
50
|
+
|
51
|
+
def fill_missing_device_info(self, config):
|
52
|
+
"""
|
53
|
+
Fill missing LLMDriverConfig fields (max_tokens, temperature, etc) from MODEL_SPECS for the chosen model.
|
54
|
+
Mutates the config in place.
|
55
|
+
"""
|
56
|
+
if not hasattr(self, "MODEL_SPECS"):
|
57
|
+
return
|
58
|
+
model_name = getattr(config, "model", None) or getattr(
|
59
|
+
self, "DEFAULT_MODEL", None
|
60
|
+
)
|
61
|
+
model_info = self.MODEL_SPECS.get(model_name)
|
62
|
+
if not model_info:
|
63
|
+
return
|
64
|
+
# Handle common fields from model_info
|
65
|
+
spec_dict = (
|
66
|
+
model_info.to_dict() if hasattr(model_info, "to_dict") else dict(model_info)
|
67
|
+
)
|
68
|
+
if (
|
69
|
+
hasattr(config, "max_tokens")
|
70
|
+
and getattr(config, "max_tokens", None) is None
|
71
|
+
):
|
72
|
+
val = spec_dict.get("max_tokens") or spec_dict.get("max_response")
|
73
|
+
if val is not None:
|
74
|
+
try:
|
75
|
+
config.max_tokens = int(val)
|
76
|
+
except Exception:
|
77
|
+
pass
|
78
|
+
if (
|
79
|
+
hasattr(config, "temperature")
|
80
|
+
and getattr(config, "temperature", None) is None
|
81
|
+
):
|
82
|
+
val = spec_dict.get("temperature")
|
83
|
+
if val is None:
|
84
|
+
val = spec_dict.get("default_temp")
|
85
|
+
if val is not None:
|
86
|
+
try:
|
87
|
+
config.temperature = float(val)
|
88
|
+
except Exception:
|
89
|
+
pass
|
90
|
+
|
91
|
+
@property
|
92
|
+
@abstractmethod
|
93
|
+
def driver(self) -> LLMDriver:
|
94
|
+
pass
|
95
|
+
|
96
|
+
def get_model_info(self, model_name=None):
|
97
|
+
"""
|
98
|
+
Return the info dict for a given model (driver, params, etc). If model_name is None, return all model info dicts.
|
99
|
+
MODEL_SPECS must be dict[str, LLMModelInfo].
|
100
|
+
"""
|
101
|
+
if not hasattr(self, "MODEL_SPECS"):
|
102
|
+
raise NotImplementedError(
|
103
|
+
"This provider does not have a MODEL_SPECS attribute."
|
104
|
+
)
|
105
|
+
if model_name is None:
|
106
|
+
return {
|
107
|
+
name: model_info.to_dict()
|
108
|
+
for name, model_info in self.MODEL_SPECS.items()
|
109
|
+
}
|
110
|
+
if model_name in self.MODEL_SPECS:
|
111
|
+
return self.MODEL_SPECS[model_name].to_dict()
|
112
|
+
return None
|
113
|
+
|
114
|
+
def _validate_model_specs(self):
|
115
|
+
if not hasattr(self, "MODEL_SPECS"):
|
116
|
+
raise NotImplementedError(
|
117
|
+
"This provider does not have a MODEL_SPECS attribute."
|
118
|
+
)
|
119
|
+
|
120
|
+
def _get_model_name_from_config(self, config):
|
121
|
+
return (config or {}).get("model_name", getattr(self, "DEFAULT_MODEL", None))
|
122
|
+
|
123
|
+
def _get_model_spec_entry(self, model_name):
|
124
|
+
spec = self.MODEL_SPECS.get(model_name, None)
|
125
|
+
if spec is None:
|
126
|
+
raise ValueError(f"Model '{model_name}' not found in MODEL_SPECS.")
|
127
|
+
return spec
|
128
|
+
|
129
|
+
def _get_driver_name_from_spec(self, spec):
|
130
|
+
driver_name = None
|
131
|
+
if hasattr(spec, "driver") and spec.driver:
|
132
|
+
driver_name = spec.driver
|
133
|
+
elif hasattr(spec, "other") and isinstance(spec.other, dict):
|
134
|
+
driver_name = spec.other.get("driver", None)
|
135
|
+
return driver_name
|
136
|
+
|
137
|
+
def _resolve_driver_class(self, driver_name):
|
138
|
+
if not driver_name:
|
139
|
+
raise NotImplementedError(
|
140
|
+
"No driver class found or specified for this MODEL_SPECS entry."
|
141
|
+
)
|
142
|
+
module_root = "janito.drivers"
|
143
|
+
probable_path = None
|
144
|
+
mapping = {
|
145
|
+
"OpenAIResponsesModelDriver": "openai_responses.driver",
|
146
|
+
"OpenAIModelDriver": "openai.driver",
|
147
|
+
"AzureOpenAIModelDriver": "azure_openai.driver",
|
148
|
+
"GoogleGenaiModelDriver": "google_genai.driver",
|
149
|
+
}
|
150
|
+
if driver_name in mapping:
|
151
|
+
probable_path = mapping[driver_name]
|
152
|
+
module_path = f"{module_root}.{probable_path}"
|
153
|
+
mod = importlib.import_module(module_path)
|
154
|
+
return getattr(mod, driver_name)
|
155
|
+
# Attempt dynamic fallback based on convention
|
156
|
+
if driver_name.endswith("ModelDriver"):
|
157
|
+
base = driver_name[: -len("ModelDriver")]
|
158
|
+
mod_name = base.replace("_", "").lower()
|
159
|
+
module_path = f"{module_root}.{mod_name}.driver"
|
160
|
+
try:
|
161
|
+
mod = importlib.import_module(module_path)
|
162
|
+
return getattr(mod, driver_name)
|
163
|
+
except Exception:
|
164
|
+
pass
|
165
|
+
raise NotImplementedError(
|
166
|
+
"No driver class found for driver_name: {}".format(driver_name)
|
167
|
+
)
|
168
|
+
|
169
|
+
def _validate_required_config(self, driver_class, config, driver_name):
|
170
|
+
required = getattr(driver_class, "required_config", None)
|
171
|
+
if required:
|
172
|
+
missing = [
|
173
|
+
k
|
174
|
+
for k in required
|
175
|
+
if not config or k not in config or config.get(k) in (None, "")
|
176
|
+
]
|
177
|
+
if missing:
|
178
|
+
raise ValueError(
|
179
|
+
f"Missing required config for {driver_name}: {', '.join(missing)}"
|
180
|
+
)
|
181
|
+
|
182
|
+
def create_agent(self, tools_adapter=None, agent_name: str = None, **kwargs):
|
183
|
+
from janito.llm.agent import LLMAgent
|
184
|
+
|
185
|
+
# Dynamically create driver if supported, else fallback to existing.
|
186
|
+
driver = self.driver
|
187
|
+
return LLMAgent(self, tools_adapter, agent_name=agent_name, **kwargs)
|
janito/perf_singleton.py
ADDED