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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (283) hide show
  1. janito/__init__.py +6 -1
  2. janito/__main__.py +1 -1
  3. janito/agent/setup_agent.py +139 -0
  4. janito/agent/templates/profiles/{system_prompt_template_base.txt.j2 → system_prompt_template_main.txt.j2} +1 -1
  5. janito/cli/__init__.py +9 -0
  6. janito/cli/chat_mode/bindings.py +37 -0
  7. janito/cli/chat_mode/chat_entry.py +23 -0
  8. janito/cli/chat_mode/prompt_style.py +19 -0
  9. janito/cli/chat_mode/session.py +272 -0
  10. janito/{shell/prompt/completer.py → cli/chat_mode/shell/autocomplete.py} +7 -6
  11. janito/cli/chat_mode/shell/commands/__init__.py +55 -0
  12. janito/cli/chat_mode/shell/commands/base.py +9 -0
  13. janito/cli/chat_mode/shell/commands/clear.py +12 -0
  14. janito/{shell → cli/chat_mode/shell}/commands/conversation_restart.py +34 -30
  15. janito/cli/chat_mode/shell/commands/edit.py +25 -0
  16. janito/cli/chat_mode/shell/commands/help.py +16 -0
  17. janito/cli/chat_mode/shell/commands/history_view.py +93 -0
  18. janito/cli/chat_mode/shell/commands/lang.py +25 -0
  19. janito/cli/chat_mode/shell/commands/last.py +137 -0
  20. janito/cli/chat_mode/shell/commands/livelogs.py +49 -0
  21. janito/cli/chat_mode/shell/commands/multi.py +51 -0
  22. janito/cli/chat_mode/shell/commands/prompt.py +64 -0
  23. janito/cli/chat_mode/shell/commands/role.py +36 -0
  24. janito/cli/chat_mode/shell/commands/session.py +40 -0
  25. janito/{shell → cli/chat_mode/shell}/commands/session_control.py +2 -2
  26. janito/cli/chat_mode/shell/commands/termweb_log.py +92 -0
  27. janito/cli/chat_mode/shell/commands/tools.py +32 -0
  28. janito/{shell → cli/chat_mode/shell}/commands/utility.py +4 -7
  29. janito/{shell → cli/chat_mode/shell}/commands/verbose.py +5 -5
  30. janito/cli/chat_mode/shell/session/__init__.py +1 -0
  31. janito/{shell → cli/chat_mode/shell}/session/manager.py +9 -1
  32. janito/cli/chat_mode/toolbar.py +90 -0
  33. janito/cli/cli_commands/list_models.py +35 -0
  34. janito/cli/cli_commands/list_providers.py +9 -0
  35. janito/cli/cli_commands/list_tools.py +53 -0
  36. janito/cli/cli_commands/model_selection.py +50 -0
  37. janito/cli/cli_commands/model_utils.py +84 -0
  38. janito/cli/cli_commands/set_api_key.py +19 -0
  39. janito/cli/cli_commands/show_config.py +51 -0
  40. janito/cli/cli_commands/show_system_prompt.py +62 -0
  41. janito/cli/config.py +28 -0
  42. janito/cli/console.py +3 -0
  43. janito/cli/core/__init__.py +4 -0
  44. janito/cli/core/event_logger.py +59 -0
  45. janito/cli/core/getters.py +31 -0
  46. janito/cli/core/runner.py +141 -0
  47. janito/cli/core/setters.py +174 -0
  48. janito/cli/core/unsetters.py +54 -0
  49. janito/cli/main.py +8 -196
  50. janito/cli/main_cli.py +312 -0
  51. janito/cli/prompt_core.py +230 -0
  52. janito/cli/prompt_handler.py +6 -0
  53. janito/cli/rich_terminal_reporter.py +101 -0
  54. janito/cli/single_shot_mode/__init__.py +6 -0
  55. janito/cli/single_shot_mode/handler.py +137 -0
  56. janito/cli/termweb_starter.py +73 -24
  57. janito/cli/utils.py +25 -0
  58. janito/cli/verbose_output.py +196 -0
  59. janito/config.py +5 -0
  60. janito/config_manager.py +110 -0
  61. janito/conversation_history.py +30 -0
  62. janito/{agent/tools_utils/dir_walk_utils.py → dir_walk_utils.py} +3 -2
  63. janito/driver_events.py +98 -0
  64. janito/drivers/anthropic/driver.py +113 -0
  65. janito/drivers/azure_openai/driver.py +36 -0
  66. janito/drivers/driver_registry.py +33 -0
  67. janito/drivers/google_genai/driver.py +54 -0
  68. janito/drivers/google_genai/schema_generator.py +67 -0
  69. janito/drivers/mistralai/driver.py +41 -0
  70. janito/drivers/openai/driver.py +334 -0
  71. janito/event_bus/__init__.py +2 -0
  72. janito/event_bus/bus.py +68 -0
  73. janito/event_bus/event.py +15 -0
  74. janito/event_bus/handler.py +31 -0
  75. janito/event_bus/queue_bus.py +57 -0
  76. janito/exceptions.py +23 -0
  77. janito/formatting_token.py +54 -0
  78. janito/i18n/pt.py +1 -0
  79. janito/llm/__init__.py +5 -0
  80. janito/llm/agent.py +443 -0
  81. janito/llm/auth.py +62 -0
  82. janito/llm/driver.py +239 -0
  83. janito/llm/driver_config.py +34 -0
  84. janito/llm/driver_config_builder.py +34 -0
  85. janito/llm/driver_input.py +12 -0
  86. janito/llm/message_parts.py +60 -0
  87. janito/llm/model.py +38 -0
  88. janito/llm/provider.py +187 -0
  89. janito/perf_singleton.py +3 -0
  90. janito/performance_collector.py +167 -0
  91. janito/provider_config.py +98 -0
  92. janito/provider_registry.py +152 -0
  93. janito/providers/__init__.py +7 -0
  94. janito/providers/anthropic/model_info.py +22 -0
  95. janito/providers/anthropic/provider.py +65 -0
  96. janito/providers/azure_openai/model_info.py +15 -0
  97. janito/providers/azure_openai/provider.py +72 -0
  98. janito/providers/deepseek/__init__.py +1 -0
  99. janito/providers/deepseek/model_info.py +16 -0
  100. janito/providers/deepseek/provider.py +91 -0
  101. janito/providers/google/__init__.py +1 -0
  102. janito/providers/google/model_info.py +40 -0
  103. janito/providers/google/provider.py +69 -0
  104. janito/providers/mistralai/model_info.py +37 -0
  105. janito/providers/mistralai/provider.py +69 -0
  106. janito/providers/openai/__init__.py +1 -0
  107. janito/providers/openai/model_info.py +137 -0
  108. janito/providers/openai/provider.py +107 -0
  109. janito/providers/openai/schema_generator.py +63 -0
  110. janito/providers/provider_static_info.py +21 -0
  111. janito/providers/registry.py +26 -0
  112. janito/report_events.py +38 -0
  113. janito/termweb/app.py +1 -1
  114. janito/tools/__init__.py +16 -0
  115. janito/tools/adapters/__init__.py +1 -0
  116. janito/tools/adapters/local/__init__.py +54 -0
  117. janito/tools/adapters/local/adapter.py +92 -0
  118. janito/{agent/tools → tools/adapters/local}/ask_user.py +30 -13
  119. janito/tools/adapters/local/copy_file.py +84 -0
  120. janito/{agent/tools → tools/adapters/local}/create_directory.py +11 -10
  121. janito/tools/adapters/local/create_file.py +82 -0
  122. janito/tools/adapters/local/delete_text_in_file.py +136 -0
  123. janito/{agent/tools → tools/adapters/local}/fetch_url.py +18 -19
  124. janito/tools/adapters/local/find_files.py +140 -0
  125. janito/tools/adapters/local/get_file_outline/core.py +151 -0
  126. janito/{agent/tools → tools/adapters/local}/get_file_outline/python_outline.py +125 -0
  127. janito/tools/adapters/local/get_file_outline/python_outline_v2.py +156 -0
  128. janito/{agent/tools → tools/adapters/local}/get_file_outline/search_outline.py +12 -7
  129. janito/{agent/tools → tools/adapters/local}/move_file.py +13 -9
  130. janito/tools/adapters/local/open_html_in_browser.py +34 -0
  131. janito/{agent/tools → tools/adapters/local}/open_url.py +7 -5
  132. janito/tools/adapters/local/python_code_run.py +165 -0
  133. janito/tools/adapters/local/python_command_run.py +163 -0
  134. janito/tools/adapters/local/python_file_run.py +162 -0
  135. janito/{agent/tools → tools/adapters/local}/remove_directory.py +15 -9
  136. janito/{agent/tools → tools/adapters/local}/remove_file.py +17 -14
  137. janito/{agent/tools → tools/adapters/local}/replace_text_in_file.py +27 -22
  138. janito/tools/adapters/local/run_bash_command.py +176 -0
  139. janito/tools/adapters/local/run_powershell_command.py +219 -0
  140. janito/{agent/tools → tools/adapters/local}/search_text/core.py +32 -12
  141. janito/{agent/tools → tools/adapters/local}/search_text/match_lines.py +13 -4
  142. janito/{agent/tools → tools/adapters/local}/search_text/pattern_utils.py +12 -4
  143. janito/{agent/tools → tools/adapters/local}/search_text/traverse_directory.py +15 -2
  144. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/core.py +12 -11
  145. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/css_validator.py +1 -1
  146. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/html_validator.py +1 -1
  147. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/js_validator.py +1 -1
  148. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/json_validator.py +1 -1
  149. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/markdown_validator.py +1 -1
  150. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/ps1_validator.py +1 -1
  151. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/python_validator.py +1 -1
  152. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/xml_validator.py +1 -1
  153. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/yaml_validator.py +1 -1
  154. janito/{agent/tools/get_lines.py → tools/adapters/local/view_file.py} +45 -27
  155. janito/tools/inspect_registry.py +17 -0
  156. janito/tools/tool_base.py +105 -0
  157. janito/tools/tool_events.py +58 -0
  158. janito/tools/tool_run_exception.py +12 -0
  159. janito/{agent → tools}/tool_use_tracker.py +2 -4
  160. janito/{agent/tools_utils/utils.py → tools/tool_utils.py} +18 -9
  161. janito/tools/tools_adapter.py +207 -0
  162. janito/tools/tools_schema.py +104 -0
  163. janito/utils.py +11 -0
  164. janito/version.py +4 -0
  165. janito-2.0.1.dist-info/METADATA +232 -0
  166. janito-2.0.1.dist-info/RECORD +181 -0
  167. janito/agent/__init__.py +0 -0
  168. janito/agent/api_exceptions.py +0 -4
  169. janito/agent/config.py +0 -147
  170. janito/agent/config_defaults.py +0 -12
  171. janito/agent/config_utils.py +0 -0
  172. janito/agent/content_handler.py +0 -0
  173. janito/agent/conversation.py +0 -238
  174. janito/agent/conversation_api.py +0 -306
  175. janito/agent/conversation_exceptions.py +0 -18
  176. janito/agent/conversation_tool_calls.py +0 -39
  177. janito/agent/conversation_ui.py +0 -17
  178. janito/agent/event.py +0 -24
  179. janito/agent/event_dispatcher.py +0 -24
  180. janito/agent/event_handler_protocol.py +0 -5
  181. janito/agent/event_system.py +0 -15
  182. janito/agent/llm_conversation_history.py +0 -82
  183. janito/agent/message_handler.py +0 -20
  184. janito/agent/message_handler_protocol.py +0 -5
  185. janito/agent/openai_client.py +0 -149
  186. janito/agent/openai_schema_generator.py +0 -187
  187. janito/agent/profile_manager.py +0 -96
  188. janito/agent/queued_message_handler.py +0 -50
  189. janito/agent/rich_live.py +0 -32
  190. janito/agent/rich_message_handler.py +0 -115
  191. janito/agent/runtime_config.py +0 -36
  192. janito/agent/test_handler_protocols.py +0 -47
  193. janito/agent/test_openai_schema_generator.py +0 -93
  194. janito/agent/tests/__init__.py +0 -1
  195. janito/agent/tool_base.py +0 -63
  196. janito/agent/tool_executor.py +0 -122
  197. janito/agent/tool_registry.py +0 -49
  198. janito/agent/tools/__init__.py +0 -47
  199. janito/agent/tools/create_file.py +0 -59
  200. janito/agent/tools/delete_text_in_file.py +0 -97
  201. janito/agent/tools/find_files.py +0 -106
  202. janito/agent/tools/get_file_outline/core.py +0 -81
  203. janito/agent/tools/present_choices.py +0 -64
  204. janito/agent/tools/python_command_runner.py +0 -201
  205. janito/agent/tools/python_file_runner.py +0 -199
  206. janito/agent/tools/python_stdin_runner.py +0 -208
  207. janito/agent/tools/replace_file.py +0 -72
  208. janito/agent/tools/run_bash_command.py +0 -218
  209. janito/agent/tools/run_powershell_command.py +0 -251
  210. janito/agent/tools_utils/__init__.py +0 -1
  211. janito/agent/tools_utils/action_type.py +0 -7
  212. janito/agent/tools_utils/test_gitignore_utils.py +0 -46
  213. janito/cli/_livereload_log_utils.py +0 -13
  214. janito/cli/_print_config.py +0 -96
  215. janito/cli/_termweb_log_utils.py +0 -17
  216. janito/cli/_utils.py +0 -9
  217. janito/cli/arg_parser.py +0 -272
  218. janito/cli/cli_main.py +0 -281
  219. janito/cli/config_commands.py +0 -211
  220. janito/cli/config_runner.py +0 -35
  221. janito/cli/formatting_runner.py +0 -12
  222. janito/cli/livereload_starter.py +0 -60
  223. janito/cli/logging_setup.py +0 -38
  224. janito/cli/one_shot.py +0 -80
  225. janito/livereload/app.py +0 -25
  226. janito/rich_utils.py +0 -59
  227. janito/shell/__init__.py +0 -0
  228. janito/shell/commands/__init__.py +0 -61
  229. janito/shell/commands/config.py +0 -22
  230. janito/shell/commands/edit.py +0 -24
  231. janito/shell/commands/history_view.py +0 -18
  232. janito/shell/commands/lang.py +0 -19
  233. janito/shell/commands/livelogs.py +0 -42
  234. janito/shell/commands/prompt.py +0 -62
  235. janito/shell/commands/termweb_log.py +0 -94
  236. janito/shell/commands/tools.py +0 -26
  237. janito/shell/commands/track.py +0 -36
  238. janito/shell/main.py +0 -326
  239. janito/shell/prompt/load_prompt.py +0 -57
  240. janito/shell/prompt/session_setup.py +0 -57
  241. janito/shell/session/config.py +0 -109
  242. janito/shell/session/history.py +0 -0
  243. janito/shell/ui/interactive.py +0 -226
  244. janito/termweb/static/editor.css +0 -158
  245. janito/termweb/static/editor.css.bak +0 -145
  246. janito/termweb/static/editor.html +0 -46
  247. janito/termweb/static/editor.html.bak +0 -46
  248. janito/termweb/static/editor.js +0 -265
  249. janito/termweb/static/editor.js.bak +0 -259
  250. janito/termweb/static/explorer.html.bak +0 -59
  251. janito/termweb/static/favicon.ico +0 -0
  252. janito/termweb/static/favicon.ico.bak +0 -0
  253. janito/termweb/static/index.html +0 -53
  254. janito/termweb/static/index.html.bak +0 -54
  255. janito/termweb/static/index.html.bak.bak +0 -175
  256. janito/termweb/static/landing.html.bak +0 -36
  257. janito/termweb/static/termicon.svg +0 -1
  258. janito/termweb/static/termweb.css +0 -214
  259. janito/termweb/static/termweb.css.bak +0 -237
  260. janito/termweb/static/termweb.js +0 -162
  261. janito/termweb/static/termweb.js.bak +0 -168
  262. janito/termweb/static/termweb.js.bak.bak +0 -157
  263. janito/termweb/static/termweb_quickopen.js +0 -135
  264. janito/termweb/static/termweb_quickopen.js.bak +0 -125
  265. janito/tests/test_rich_utils.py +0 -44
  266. janito/web/__init__.py +0 -0
  267. janito/web/__main__.py +0 -25
  268. janito/web/app.py +0 -145
  269. janito-1.14.3.dist-info/METADATA +0 -313
  270. janito-1.14.3.dist-info/RECORD +0 -162
  271. janito-1.14.3.dist-info/licenses/LICENSE +0 -21
  272. /janito/{shell → cli/chat_mode/shell}/input_history.py +0 -0
  273. /janito/{shell/commands/session.py → cli/chat_mode/shell/session/history.py} +0 -0
  274. /janito/{agent/tools_utils/formatting.py → formatting.py} +0 -0
  275. /janito/{agent/tools_utils/gitignore_utils.py → gitignore_utils.py} +0 -0
  276. /janito/{agent/platform_discovery.py → platform_discovery.py} +0 -0
  277. /janito/{agent/tools → tools/adapters/local}/get_file_outline/__init__.py +0 -0
  278. /janito/{agent/tools → tools/adapters/local}/get_file_outline/markdown_outline.py +0 -0
  279. /janito/{agent/tools → tools/adapters/local}/search_text/__init__.py +0 -0
  280. /janito/{agent/tools → tools/adapters/local}/validate_file_syntax/__init__.py +0 -0
  281. {janito-1.14.3.dist-info → janito-2.0.1.dist-info}/WHEEL +0 -0
  282. {janito-1.14.3.dist-info → janito-2.0.1.dist-info}/entry_points.txt +0 -0
  283. {janito-1.14.3.dist-info → janito-2.0.1.dist-info}/top_level.txt +0 -0
@@ -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
@@ -0,0 +1,2 @@
1
+ from .bus import EventBus, event_bus
2
+ from .handler import EventHandlerBase
@@ -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