janito 1.14.3__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.3.dist-info/METADATA +0 -313
- janito-1.14.3.dist-info/RECORD +0 -162
- janito-1.14.3.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.3.dist-info → janito-2.0.0.dist-info}/WHEEL +0 -0
- {janito-1.14.3.dist-info → janito-2.0.0.dist-info}/entry_points.txt +0 -0
- {janito-1.14.3.dist-info → janito-2.0.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,54 @@
|
|
1
|
+
"""
|
2
|
+
Google Gemini LLM driver.
|
3
|
+
|
4
|
+
This driver handles interaction with the Google Gemini API, including support for tool/function calls and event publishing.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import json
|
8
|
+
import time
|
9
|
+
import uuid
|
10
|
+
import traceback
|
11
|
+
from typing import Optional, List, Dict, Any, Union
|
12
|
+
from janito.llm.driver import LLMDriver
|
13
|
+
from janito.drivers.google_genai.schema_generator import generate_tool_declarations
|
14
|
+
from janito.driver_events import (
|
15
|
+
GenerationStarted,
|
16
|
+
GenerationFinished,
|
17
|
+
RequestStarted,
|
18
|
+
RequestFinished,
|
19
|
+
ResponseReceived,
|
20
|
+
RequestStatus,
|
21
|
+
)
|
22
|
+
from janito.tools.adapters.local.adapter import LocalToolsAdapter
|
23
|
+
from janito.llm.message_parts import TextMessagePart, FunctionCallMessagePart
|
24
|
+
from janito.llm.driver_config import LLMDriverConfig
|
25
|
+
|
26
|
+
|
27
|
+
def extract_usage_metadata_native(usage_obj):
|
28
|
+
if usage_obj is None:
|
29
|
+
return {}
|
30
|
+
result = {}
|
31
|
+
for attr in dir(usage_obj):
|
32
|
+
if attr.startswith("_") or attr == "__class__":
|
33
|
+
continue
|
34
|
+
value = getattr(usage_obj, attr)
|
35
|
+
if isinstance(value, (str, int, float, bool, type(None))):
|
36
|
+
result[attr] = value
|
37
|
+
elif isinstance(value, list):
|
38
|
+
if all(isinstance(i, (str, int, float, bool, type(None))) for i in value):
|
39
|
+
result[attr] = value
|
40
|
+
return result
|
41
|
+
|
42
|
+
|
43
|
+
class GoogleGenaiModelDriver(LLMDriver):
|
44
|
+
available = False
|
45
|
+
unavailable_reason = "GoogleGenaiModelDriver is not implemented yet."
|
46
|
+
|
47
|
+
@classmethod
|
48
|
+
def is_available(cls):
|
49
|
+
return cls.available
|
50
|
+
|
51
|
+
name = "google_genai"
|
52
|
+
|
53
|
+
def __init__(self, tools_adapter=None):
|
54
|
+
raise ImportError(self.unavailable_reason)
|
@@ -0,0 +1,67 @@
|
|
1
|
+
import inspect
|
2
|
+
import typing
|
3
|
+
from janito.tools.tools_schema import ToolSchemaBase
|
4
|
+
|
5
|
+
try:
|
6
|
+
from google.genai import types as genai_types
|
7
|
+
except ImportError:
|
8
|
+
genai_types = None
|
9
|
+
|
10
|
+
|
11
|
+
class GeminiSchemaGenerator(ToolSchemaBase):
|
12
|
+
PYTHON_TYPE_TO_GENAI_TYPE = {
|
13
|
+
str: "STRING",
|
14
|
+
int: "INTEGER",
|
15
|
+
float: "NUMBER",
|
16
|
+
bool: "BOOLEAN",
|
17
|
+
list: "ARRAY",
|
18
|
+
dict: "OBJECT",
|
19
|
+
}
|
20
|
+
|
21
|
+
def type_to_genai_schema(self, annotation, description=None):
|
22
|
+
if hasattr(annotation, "__origin__"):
|
23
|
+
if annotation.__origin__ is list or annotation.__origin__ is typing.List:
|
24
|
+
return genai_types.Schema(
|
25
|
+
type="ARRAY",
|
26
|
+
items=self.type_to_genai_schema(annotation.__args__[0]),
|
27
|
+
description=description,
|
28
|
+
)
|
29
|
+
if annotation.__origin__ is dict or annotation.__origin__ is typing.Dict:
|
30
|
+
return genai_types.Schema(type="OBJECT", description=description)
|
31
|
+
return genai_types.Schema(
|
32
|
+
type=self.PYTHON_TYPE_TO_GENAI_TYPE.get(annotation, "STRING"),
|
33
|
+
description=description,
|
34
|
+
)
|
35
|
+
|
36
|
+
def generate_declaration(self, tool_class):
|
37
|
+
func, tool_name, sig, summary, param_descs, return_desc, description = (
|
38
|
+
self.validate_tool_class(tool_class)
|
39
|
+
)
|
40
|
+
properties = {}
|
41
|
+
required = []
|
42
|
+
# Removed tool_call_reason from properties and required
|
43
|
+
for name, param in sig.parameters.items():
|
44
|
+
if name == "self":
|
45
|
+
continue
|
46
|
+
annotation = param.annotation
|
47
|
+
pdesc = param_descs.get(name, "")
|
48
|
+
schema = self.type_to_genai_schema(annotation, description=pdesc)
|
49
|
+
properties[name] = schema
|
50
|
+
if param.default == inspect._empty:
|
51
|
+
required.append(name)
|
52
|
+
parameters_schema = genai_types.Schema(
|
53
|
+
type="OBJECT", properties=properties, required=required
|
54
|
+
)
|
55
|
+
return genai_types.FunctionDeclaration(
|
56
|
+
name=tool_name, description=description, parameters=parameters_schema
|
57
|
+
)
|
58
|
+
|
59
|
+
|
60
|
+
def generate_tool_declarations(tool_classes: list):
|
61
|
+
if genai_types is None:
|
62
|
+
raise ImportError("google-genai package is not installed.")
|
63
|
+
generator = GeminiSchemaGenerator()
|
64
|
+
function_declarations = [
|
65
|
+
generator.generate_declaration(tool_class) for tool_class in tool_classes
|
66
|
+
]
|
67
|
+
return [genai_types.Tool(function_declarations=function_declarations)]
|
@@ -0,0 +1,41 @@
|
|
1
|
+
import time
|
2
|
+
import uuid
|
3
|
+
import traceback
|
4
|
+
import json
|
5
|
+
from typing import Optional, List, Dict, Any, Union
|
6
|
+
from janito.llm.driver import LLMDriver
|
7
|
+
from janito.driver_events import (
|
8
|
+
GenerationStarted,
|
9
|
+
GenerationFinished,
|
10
|
+
RequestStarted,
|
11
|
+
RequestFinished,
|
12
|
+
ResponseReceived,
|
13
|
+
)
|
14
|
+
from janito.providers.openai.schema_generator import generate_tool_schemas
|
15
|
+
from janito.tools.adapters.local.adapter import LocalToolsAdapter
|
16
|
+
from janito.llm.message_parts import TextMessagePart, FunctionCallMessagePart
|
17
|
+
from janito.llm.driver_config import LLMDriverConfig
|
18
|
+
|
19
|
+
# Safe import of mistralai SDK
|
20
|
+
try:
|
21
|
+
from mistralai import Mistral
|
22
|
+
|
23
|
+
DRIVER_AVAILABLE = True
|
24
|
+
DRIVER_UNAVAILABLE_REASON = None
|
25
|
+
except ImportError:
|
26
|
+
DRIVER_AVAILABLE = False
|
27
|
+
DRIVER_UNAVAILABLE_REASON = "Missing dependency: mistralai (pip install mistralai)"
|
28
|
+
|
29
|
+
|
30
|
+
class MistralAIModelDriver(LLMDriver):
|
31
|
+
available = False
|
32
|
+
unavailable_reason = "MistralAIModelDriver is not implemented yet."
|
33
|
+
|
34
|
+
@classmethod
|
35
|
+
def is_available(cls):
|
36
|
+
return cls.available
|
37
|
+
|
38
|
+
name = "mistralai"
|
39
|
+
|
40
|
+
def __init__(self, tools_adapter=None):
|
41
|
+
raise ImportError(self.unavailable_reason)
|
@@ -0,0 +1,334 @@
|
|
1
|
+
import uuid
|
2
|
+
import traceback
|
3
|
+
from rich import pretty
|
4
|
+
from janito.llm.driver import LLMDriver
|
5
|
+
from janito.llm.driver_input import DriverInput
|
6
|
+
from janito.driver_events import RequestFinished, RequestStatus
|
7
|
+
|
8
|
+
# Safe import of openai SDK
|
9
|
+
try:
|
10
|
+
import openai
|
11
|
+
|
12
|
+
DRIVER_AVAILABLE = True
|
13
|
+
DRIVER_UNAVAILABLE_REASON = None
|
14
|
+
except ImportError:
|
15
|
+
DRIVER_AVAILABLE = False
|
16
|
+
DRIVER_UNAVAILABLE_REASON = "Missing dependency: openai (pip install openai)"
|
17
|
+
|
18
|
+
|
19
|
+
class OpenAIModelDriver(LLMDriver):
|
20
|
+
def _get_message_from_result(self, result):
|
21
|
+
"""Extract the message object from the provider result (OpenAI-specific)."""
|
22
|
+
if hasattr(result, "choices") and result.choices:
|
23
|
+
return result.choices[0].message
|
24
|
+
return None
|
25
|
+
|
26
|
+
"""
|
27
|
+
OpenAI LLM driver (threaded, queue-based, stateless). Uses input/output queues accessible via instance attributes.
|
28
|
+
"""
|
29
|
+
available = DRIVER_AVAILABLE
|
30
|
+
unavailable_reason = DRIVER_UNAVAILABLE_REASON
|
31
|
+
|
32
|
+
def __init__(self, tools_adapter=None, provider_name=None):
|
33
|
+
super().__init__(tools_adapter=tools_adapter, provider_name=provider_name)
|
34
|
+
|
35
|
+
def _prepare_api_kwargs(self, config, conversation):
|
36
|
+
"""
|
37
|
+
Prepares API kwargs for OpenAI, including tool schemas if tools_adapter is present,
|
38
|
+
and OpenAI-specific arguments (model, max_tokens, temperature, etc.).
|
39
|
+
"""
|
40
|
+
api_kwargs = {}
|
41
|
+
# Tool schemas (moved from base)
|
42
|
+
if self.tools_adapter:
|
43
|
+
try:
|
44
|
+
from janito.providers.openai.schema_generator import (
|
45
|
+
generate_tool_schemas,
|
46
|
+
)
|
47
|
+
|
48
|
+
tool_classes = self.tools_adapter.get_tool_classes()
|
49
|
+
tool_schemas = generate_tool_schemas(tool_classes)
|
50
|
+
api_kwargs["tools"] = tool_schemas
|
51
|
+
except Exception as e:
|
52
|
+
api_kwargs["tools"] = []
|
53
|
+
if hasattr(config, "verbose_api") and config.verbose_api:
|
54
|
+
print(f"[OpenAIModelDriver] Tool schema generation failed: {e}")
|
55
|
+
# OpenAI-specific parameters
|
56
|
+
if config.model:
|
57
|
+
api_kwargs["model"] = config.model
|
58
|
+
# Prefer max_completion_tokens if present, else fallback to max_tokens (for backward compatibility)
|
59
|
+
if (
|
60
|
+
hasattr(config, "max_completion_tokens")
|
61
|
+
and config.max_completion_tokens is not None
|
62
|
+
):
|
63
|
+
api_kwargs["max_completion_tokens"] = int(config.max_completion_tokens)
|
64
|
+
elif hasattr(config, "max_tokens") and config.max_tokens is not None:
|
65
|
+
# For models that do not support 'max_tokens', map to 'max_completion_tokens'
|
66
|
+
api_kwargs["max_completion_tokens"] = int(config.max_tokens)
|
67
|
+
for p in (
|
68
|
+
"temperature",
|
69
|
+
"top_p",
|
70
|
+
"presence_penalty",
|
71
|
+
"frequency_penalty",
|
72
|
+
"stop",
|
73
|
+
):
|
74
|
+
v = getattr(config, p, None)
|
75
|
+
if v is not None:
|
76
|
+
api_kwargs[p] = v
|
77
|
+
api_kwargs["messages"] = conversation
|
78
|
+
api_kwargs["stream"] = False
|
79
|
+
return api_kwargs
|
80
|
+
|
81
|
+
def _call_api(self, driver_input: DriverInput):
|
82
|
+
cancel_event = getattr(driver_input, "cancel_event", None)
|
83
|
+
config = driver_input.config
|
84
|
+
conversation = self.convert_history_to_api_messages(
|
85
|
+
driver_input.conversation_history
|
86
|
+
)
|
87
|
+
request_id = getattr(config, "request_id", None)
|
88
|
+
if config.verbose_api:
|
89
|
+
print(
|
90
|
+
f"[verbose-api] OpenAI API call about to be sent. Model: {config.model}, max_tokens: {config.max_tokens}, tools_adapter: {type(self.tools_adapter).__name__ if self.tools_adapter else None}",
|
91
|
+
flush=True,
|
92
|
+
)
|
93
|
+
try:
|
94
|
+
client = self._instantiate_openai_client(config)
|
95
|
+
api_kwargs = self._prepare_api_kwargs(config, conversation)
|
96
|
+
if config.verbose_api:
|
97
|
+
print(
|
98
|
+
f"[OpenAI] API CALL: chat.completions.create(**{api_kwargs})",
|
99
|
+
flush=True,
|
100
|
+
)
|
101
|
+
if self._check_cancel(cancel_event, request_id, before_call=True):
|
102
|
+
return None
|
103
|
+
result = client.chat.completions.create(**api_kwargs)
|
104
|
+
if self._check_cancel(cancel_event, request_id, before_call=False):
|
105
|
+
return None
|
106
|
+
self._print_verbose_result(config, result)
|
107
|
+
usage_dict = self._extract_usage(result)
|
108
|
+
if config.verbose_api:
|
109
|
+
print(
|
110
|
+
f"[OpenAI][DEBUG] Attaching usage info to RequestFinished: {usage_dict}",
|
111
|
+
flush=True,
|
112
|
+
)
|
113
|
+
self.output_queue.put(
|
114
|
+
RequestFinished(
|
115
|
+
driver_name=self.__class__.__name__,
|
116
|
+
request_id=request_id,
|
117
|
+
response=result,
|
118
|
+
status=RequestStatus.SUCCESS,
|
119
|
+
usage=usage_dict,
|
120
|
+
)
|
121
|
+
)
|
122
|
+
if config.verbose_api:
|
123
|
+
pretty.install()
|
124
|
+
print("[OpenAI] API RESPONSE:", flush=True)
|
125
|
+
pretty.pprint(result)
|
126
|
+
return result
|
127
|
+
except Exception as e:
|
128
|
+
print(f"[ERROR] Exception during OpenAI API call: {e}", flush=True)
|
129
|
+
print(f"[ERROR] config: {config}", flush=True)
|
130
|
+
print(
|
131
|
+
f"[ERROR] api_kwargs: {api_kwargs if 'api_kwargs' in locals() else 'N/A'}",
|
132
|
+
flush=True,
|
133
|
+
)
|
134
|
+
import traceback
|
135
|
+
|
136
|
+
print("[ERROR] Full stack trace:", flush=True)
|
137
|
+
print(traceback.format_exc(), flush=True)
|
138
|
+
raise
|
139
|
+
|
140
|
+
def _instantiate_openai_client(self, config):
|
141
|
+
try:
|
142
|
+
api_key_display = str(config.api_key)
|
143
|
+
if api_key_display and len(api_key_display) > 8:
|
144
|
+
api_key_display = api_key_display[:4] + "..." + api_key_display[-4:]
|
145
|
+
client_kwargs = {"api_key": config.api_key}
|
146
|
+
if getattr(config, "base_url", None):
|
147
|
+
client_kwargs["base_url"] = config.base_url
|
148
|
+
client = openai.OpenAI(**client_kwargs)
|
149
|
+
return client
|
150
|
+
except Exception as e:
|
151
|
+
print(
|
152
|
+
f"[ERROR] Exception during OpenAI client instantiation: {e}", flush=True
|
153
|
+
)
|
154
|
+
import traceback
|
155
|
+
|
156
|
+
print(traceback.format_exc(), flush=True)
|
157
|
+
raise
|
158
|
+
|
159
|
+
def _check_cancel(self, cancel_event, request_id, before_call=True):
|
160
|
+
if cancel_event is not None and cancel_event.is_set():
|
161
|
+
status = RequestStatus.CANCELLED
|
162
|
+
reason = (
|
163
|
+
"Cancelled before API call"
|
164
|
+
if before_call
|
165
|
+
else "Cancelled during API call"
|
166
|
+
)
|
167
|
+
self.output_queue.put(
|
168
|
+
RequestFinished(
|
169
|
+
driver_name=self.__class__.__name__,
|
170
|
+
request_id=request_id,
|
171
|
+
status=status,
|
172
|
+
reason=reason,
|
173
|
+
)
|
174
|
+
)
|
175
|
+
return True
|
176
|
+
return False
|
177
|
+
|
178
|
+
def _print_verbose_result(self, config, result):
|
179
|
+
if config.verbose_api:
|
180
|
+
print("[OpenAI] API RAW RESULT:", flush=True)
|
181
|
+
pretty.pprint(result)
|
182
|
+
if hasattr(result, "__dict__"):
|
183
|
+
print("[OpenAI] API RESULT __dict__:", flush=True)
|
184
|
+
pretty.pprint(result.__dict__)
|
185
|
+
try:
|
186
|
+
print("[OpenAI] API RESULT as dict:", dict(result), flush=True)
|
187
|
+
except Exception:
|
188
|
+
pass
|
189
|
+
print(
|
190
|
+
f"[OpenAI] API RESULT .usage: {getattr(result, 'usage', None)}",
|
191
|
+
flush=True,
|
192
|
+
)
|
193
|
+
try:
|
194
|
+
print(f"[OpenAI] API RESULT ['usage']: {result['usage']}", flush=True)
|
195
|
+
except Exception:
|
196
|
+
pass
|
197
|
+
if not hasattr(result, "usage") or getattr(result, "usage", None) is None:
|
198
|
+
print(
|
199
|
+
"[OpenAI][WARNING] No usage info found in API response.", flush=True
|
200
|
+
)
|
201
|
+
|
202
|
+
def _extract_usage(self, result):
|
203
|
+
usage = getattr(result, "usage", None)
|
204
|
+
if usage is not None:
|
205
|
+
usage_dict = self._usage_to_dict(usage)
|
206
|
+
if usage_dict is None:
|
207
|
+
print(
|
208
|
+
"[OpenAI][WARNING] Could not convert usage to dict, using string fallback.",
|
209
|
+
flush=True,
|
210
|
+
)
|
211
|
+
usage_dict = str(usage)
|
212
|
+
else:
|
213
|
+
usage_dict = self._extract_usage_from_result_dict(result)
|
214
|
+
return usage_dict
|
215
|
+
|
216
|
+
def _usage_to_dict(self, usage):
|
217
|
+
if hasattr(usage, "model_dump") and callable(getattr(usage, "model_dump")):
|
218
|
+
try:
|
219
|
+
return usage.model_dump()
|
220
|
+
except Exception:
|
221
|
+
pass
|
222
|
+
if hasattr(usage, "dict") and callable(getattr(usage, "dict")):
|
223
|
+
try:
|
224
|
+
return usage.dict()
|
225
|
+
except Exception:
|
226
|
+
pass
|
227
|
+
try:
|
228
|
+
return dict(usage)
|
229
|
+
except Exception:
|
230
|
+
try:
|
231
|
+
return vars(usage)
|
232
|
+
except Exception:
|
233
|
+
pass
|
234
|
+
return None
|
235
|
+
|
236
|
+
def _extract_usage_from_result_dict(self, result):
|
237
|
+
try:
|
238
|
+
return result["usage"]
|
239
|
+
except Exception:
|
240
|
+
return None
|
241
|
+
|
242
|
+
def convert_history_to_api_messages(self, conversation_history):
|
243
|
+
"""
|
244
|
+
Convert LLMConversationHistory to the list of dicts required by OpenAI's API.
|
245
|
+
Handles 'tool_results' and 'tool_calls' roles for compliance.
|
246
|
+
"""
|
247
|
+
import json
|
248
|
+
|
249
|
+
api_messages = []
|
250
|
+
for msg in conversation_history.get_history():
|
251
|
+
role = msg.get("role")
|
252
|
+
content = msg.get("content")
|
253
|
+
if role == "tool_results":
|
254
|
+
# Expect content to be a list of tool result dicts or a stringified list
|
255
|
+
try:
|
256
|
+
results = (
|
257
|
+
json.loads(content) if isinstance(content, str) else content
|
258
|
+
)
|
259
|
+
except Exception:
|
260
|
+
results = [content]
|
261
|
+
for result in results:
|
262
|
+
# result should be a dict with keys: name, content, tool_call_id
|
263
|
+
if isinstance(result, dict):
|
264
|
+
api_messages.append(
|
265
|
+
{
|
266
|
+
"role": "tool",
|
267
|
+
"content": result.get("content", ""),
|
268
|
+
"name": result.get("name", ""),
|
269
|
+
"tool_call_id": result.get("tool_call_id", ""),
|
270
|
+
}
|
271
|
+
)
|
272
|
+
else:
|
273
|
+
api_messages.append(
|
274
|
+
{
|
275
|
+
"role": "tool",
|
276
|
+
"content": str(result),
|
277
|
+
"name": "",
|
278
|
+
"tool_call_id": "",
|
279
|
+
}
|
280
|
+
)
|
281
|
+
elif role == "tool_calls":
|
282
|
+
# Convert to assistant message with tool_calls field
|
283
|
+
import json
|
284
|
+
|
285
|
+
try:
|
286
|
+
tool_calls = (
|
287
|
+
json.loads(content) if isinstance(content, str) else content
|
288
|
+
)
|
289
|
+
except Exception:
|
290
|
+
tool_calls = []
|
291
|
+
api_messages.append(
|
292
|
+
{"role": "assistant", "content": None, "tool_calls": tool_calls}
|
293
|
+
)
|
294
|
+
else:
|
295
|
+
# Special handling for 'function' role: extract 'name' from metadata if present
|
296
|
+
if role == "function":
|
297
|
+
name = ""
|
298
|
+
if isinstance(msg, dict):
|
299
|
+
metadata = msg.get("metadata", {})
|
300
|
+
name = (
|
301
|
+
metadata.get("name", "")
|
302
|
+
if isinstance(metadata, dict)
|
303
|
+
else ""
|
304
|
+
)
|
305
|
+
api_messages.append(
|
306
|
+
{"role": "tool", "content": content, "name": name}
|
307
|
+
)
|
308
|
+
else:
|
309
|
+
api_messages.append(msg)
|
310
|
+
return api_messages
|
311
|
+
|
312
|
+
def _convert_completion_message_to_parts(self, message):
|
313
|
+
"""
|
314
|
+
Convert an OpenAI completion message object to a list of MessagePart objects.
|
315
|
+
Handles text, tool calls, and can be extended for other types.
|
316
|
+
"""
|
317
|
+
from janito.llm.message_parts import TextMessagePart, FunctionCallMessagePart
|
318
|
+
|
319
|
+
parts = []
|
320
|
+
# Text content
|
321
|
+
content = getattr(message, "content", None)
|
322
|
+
if content:
|
323
|
+
parts.append(TextMessagePart(content=content))
|
324
|
+
# Tool calls
|
325
|
+
tool_calls = getattr(message, "tool_calls", None) or []
|
326
|
+
for tool_call in tool_calls:
|
327
|
+
parts.append(
|
328
|
+
FunctionCallMessagePart(
|
329
|
+
tool_call_id=getattr(tool_call, "id", ""),
|
330
|
+
function=getattr(tool_call, "function", None),
|
331
|
+
)
|
332
|
+
)
|
333
|
+
# Extend here for other message part types if needed
|
334
|
+
return parts
|
janito/event_bus/bus.py
ADDED
@@ -0,0 +1,68 @@
|
|
1
|
+
from collections import defaultdict
|
2
|
+
from datetime import datetime
|
3
|
+
from bisect import insort
|
4
|
+
import itertools
|
5
|
+
import threading
|
6
|
+
|
7
|
+
|
8
|
+
class EventBus:
|
9
|
+
"""
|
10
|
+
Generic event bus for publish/subscribe event-driven communication with handler priorities.
|
11
|
+
Automatically injects a timestamp (event.timestamp) into each event when published.
|
12
|
+
Handlers with lower priority numbers are called first (default priority=100).
|
13
|
+
Thread-safe for concurrent subscribe, unsubscribe, and publish operations.
|
14
|
+
"""
|
15
|
+
|
16
|
+
def __init__(self):
|
17
|
+
# _subscribers[event_type] = list of (priority, seq, callback)
|
18
|
+
self._subscribers = defaultdict(list)
|
19
|
+
self._seq_counter = itertools.count()
|
20
|
+
self._lock = threading.Lock()
|
21
|
+
|
22
|
+
def subscribe(self, event_type, callback, priority=100):
|
23
|
+
"""Subscribe a callback to a specific event type with a given priority (lower is higher priority)."""
|
24
|
+
with self._lock:
|
25
|
+
seq = next(self._seq_counter)
|
26
|
+
entry = (priority, seq, callback)
|
27
|
+
callbacks = self._subscribers[event_type]
|
28
|
+
# Prevent duplicate subscriptions of the same callback with the same priority
|
29
|
+
if not any(
|
30
|
+
cb == callback and prio == priority for prio, _, cb in callbacks
|
31
|
+
):
|
32
|
+
insort(callbacks, entry)
|
33
|
+
|
34
|
+
def unsubscribe(self, event_type, callback):
|
35
|
+
"""Unsubscribe a callback from a specific event type (all priorities)."""
|
36
|
+
with self._lock:
|
37
|
+
callbacks = self._subscribers[event_type]
|
38
|
+
self._subscribers[event_type] = [
|
39
|
+
entry for entry in callbacks if entry[2] != callback
|
40
|
+
]
|
41
|
+
|
42
|
+
def publish(self, event):
|
43
|
+
"""
|
44
|
+
Publish an event to all relevant subscribers in strict priority order.
|
45
|
+
Thread-safe: handlers are called outside the lock to avoid deadlocks.
|
46
|
+
"""
|
47
|
+
with self._lock:
|
48
|
+
# Collect all matching handlers (priority, seq, callback) for this event
|
49
|
+
matching_handlers = []
|
50
|
+
for event_type, callbacks in self._subscribers.items():
|
51
|
+
if isinstance(event, event_type):
|
52
|
+
matching_handlers.extend(callbacks)
|
53
|
+
# Remove duplicates (same callback for same event)
|
54
|
+
seen = set()
|
55
|
+
unique_handlers = []
|
56
|
+
for prio, seq, cb in matching_handlers:
|
57
|
+
if cb not in seen:
|
58
|
+
unique_handlers.append((prio, seq, cb))
|
59
|
+
seen.add(cb)
|
60
|
+
# Sort by priority, then sequence
|
61
|
+
unique_handlers.sort()
|
62
|
+
# Call handlers outside the lock to avoid deadlocks
|
63
|
+
for priority, seq, callback in unique_handlers:
|
64
|
+
callback(event)
|
65
|
+
|
66
|
+
|
67
|
+
# Singleton instance for global use
|
68
|
+
event_bus = EventBus()
|
@@ -0,0 +1,15 @@
|
|
1
|
+
import attr
|
2
|
+
from typing import ClassVar
|
3
|
+
from datetime import datetime
|
4
|
+
|
5
|
+
|
6
|
+
@attr.s(auto_attribs=True, kw_only=True)
|
7
|
+
class Event:
|
8
|
+
"""
|
9
|
+
Base class for all events in the system.
|
10
|
+
Represents a generic event with a category.
|
11
|
+
Automatically sets a timestamp at creation.
|
12
|
+
"""
|
13
|
+
|
14
|
+
category: ClassVar[str] = "generic"
|
15
|
+
timestamp: datetime = attr.ib(factory=datetime.utcnow)
|
@@ -0,0 +1,31 @@
|
|
1
|
+
import inspect
|
2
|
+
from .bus import event_bus
|
3
|
+
|
4
|
+
|
5
|
+
class EventHandlerBase:
|
6
|
+
"""
|
7
|
+
Base class for event handler classes.
|
8
|
+
Automatically subscribes methods named on_<EventClassName> to the event bus for the corresponding event type.
|
9
|
+
Pass one or more event modules (e.g., janito.report_events, janito.driver_events) to the constructor.
|
10
|
+
Raises an error if a handler method does not match any known event class.
|
11
|
+
"""
|
12
|
+
|
13
|
+
def __init__(self, *event_modules):
|
14
|
+
unknown_event_methods = []
|
15
|
+
for name, method in inspect.getmembers(self, predicate=inspect.ismethod):
|
16
|
+
if name.startswith("on_"):
|
17
|
+
event_class_name = name[3:]
|
18
|
+
event_class = None
|
19
|
+
for module in event_modules:
|
20
|
+
event_class = getattr(module, event_class_name, None)
|
21
|
+
if event_class:
|
22
|
+
break
|
23
|
+
if event_class:
|
24
|
+
event_bus.subscribe(event_class, method)
|
25
|
+
else:
|
26
|
+
unknown_event_methods.append(name)
|
27
|
+
if unknown_event_methods:
|
28
|
+
raise ValueError(
|
29
|
+
f"Unknown event handler methods found: {unknown_event_methods}. "
|
30
|
+
f"No matching event class found in provided event modules."
|
31
|
+
)
|
@@ -0,0 +1,57 @@
|
|
1
|
+
import threading
|
2
|
+
import queue
|
3
|
+
|
4
|
+
|
5
|
+
class QueueEventBusSentinel:
|
6
|
+
"""
|
7
|
+
Special event to signal the end of event publishing for QueueEventBus.
|
8
|
+
"""
|
9
|
+
|
10
|
+
pass
|
11
|
+
|
12
|
+
|
13
|
+
class QueueEventBus:
|
14
|
+
"""
|
15
|
+
Event bus using a single queue for event delivery, preserving event order.
|
16
|
+
API-compatible with EventBus for publish/subscribe, but all events go into one queue.
|
17
|
+
Thread-safe for concurrent publish operations.
|
18
|
+
"""
|
19
|
+
|
20
|
+
def __init__(self):
|
21
|
+
self._queue = queue.Queue()
|
22
|
+
self._lock = threading.Lock()
|
23
|
+
|
24
|
+
def subscribe(self, event_type=None, event_queue=None, priority=100):
|
25
|
+
"""
|
26
|
+
No-op for compatibility. Returns the single event queue.
|
27
|
+
"""
|
28
|
+
return self._queue
|
29
|
+
|
30
|
+
def unsubscribe(self, event_type=None, event_queue=None):
|
31
|
+
"""
|
32
|
+
No-op for compatibility.
|
33
|
+
"""
|
34
|
+
pass
|
35
|
+
|
36
|
+
def publish(self, event):
|
37
|
+
"""
|
38
|
+
Publish an event to the single queue.
|
39
|
+
"""
|
40
|
+
with self._lock:
|
41
|
+
self._queue.put(event)
|
42
|
+
|
43
|
+
def get_queue(self):
|
44
|
+
"""
|
45
|
+
Return the single event queue for consumers.
|
46
|
+
"""
|
47
|
+
return self._queue
|
48
|
+
|
49
|
+
def fetch_event(self, block=True, timeout=None):
|
50
|
+
"""
|
51
|
+
Fetch the next event from the queue. Blocks by default.
|
52
|
+
Returns None if a QueueEventBusSentinel is encountered.
|
53
|
+
"""
|
54
|
+
event = self._queue.get(block=block, timeout=timeout)
|
55
|
+
if isinstance(event, QueueEventBusSentinel):
|
56
|
+
return None
|
57
|
+
return event
|