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.
Files changed (282) 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/{agent/tools → tools/adapters/local}/open_url.py +7 -5
  131. janito/tools/adapters/local/python_code_run.py +165 -0
  132. janito/tools/adapters/local/python_command_run.py +163 -0
  133. janito/tools/adapters/local/python_file_run.py +162 -0
  134. janito/{agent/tools → tools/adapters/local}/remove_directory.py +15 -9
  135. janito/{agent/tools → tools/adapters/local}/remove_file.py +17 -14
  136. janito/{agent/tools → tools/adapters/local}/replace_text_in_file.py +27 -22
  137. janito/tools/adapters/local/run_bash_command.py +176 -0
  138. janito/tools/adapters/local/run_powershell_command.py +219 -0
  139. janito/{agent/tools → tools/adapters/local}/search_text/core.py +32 -12
  140. janito/{agent/tools → tools/adapters/local}/search_text/match_lines.py +13 -4
  141. janito/{agent/tools → tools/adapters/local}/search_text/pattern_utils.py +12 -4
  142. janito/{agent/tools → tools/adapters/local}/search_text/traverse_directory.py +15 -2
  143. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/core.py +12 -11
  144. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/css_validator.py +1 -1
  145. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/html_validator.py +1 -1
  146. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/js_validator.py +1 -1
  147. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/json_validator.py +1 -1
  148. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/markdown_validator.py +1 -1
  149. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/ps1_validator.py +1 -1
  150. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/python_validator.py +1 -1
  151. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/xml_validator.py +1 -1
  152. janito/{agent/tools → tools/adapters/local}/validate_file_syntax/yaml_validator.py +1 -1
  153. janito/{agent/tools/get_lines.py → tools/adapters/local/view_file.py} +45 -27
  154. janito/tools/inspect_registry.py +17 -0
  155. janito/tools/tool_base.py +105 -0
  156. janito/tools/tool_events.py +58 -0
  157. janito/tools/tool_run_exception.py +12 -0
  158. janito/{agent → tools}/tool_use_tracker.py +2 -4
  159. janito/{agent/tools_utils/utils.py → tools/tool_utils.py} +18 -9
  160. janito/tools/tools_adapter.py +207 -0
  161. janito/tools/tools_schema.py +104 -0
  162. janito/utils.py +11 -0
  163. janito/version.py +4 -0
  164. janito-2.0.0.dist-info/METADATA +232 -0
  165. janito-2.0.0.dist-info/RECORD +180 -0
  166. janito/agent/__init__.py +0 -0
  167. janito/agent/api_exceptions.py +0 -4
  168. janito/agent/config.py +0 -147
  169. janito/agent/config_defaults.py +0 -12
  170. janito/agent/config_utils.py +0 -0
  171. janito/agent/content_handler.py +0 -0
  172. janito/agent/conversation.py +0 -238
  173. janito/agent/conversation_api.py +0 -306
  174. janito/agent/conversation_exceptions.py +0 -18
  175. janito/agent/conversation_tool_calls.py +0 -39
  176. janito/agent/conversation_ui.py +0 -17
  177. janito/agent/event.py +0 -24
  178. janito/agent/event_dispatcher.py +0 -24
  179. janito/agent/event_handler_protocol.py +0 -5
  180. janito/agent/event_system.py +0 -15
  181. janito/agent/llm_conversation_history.py +0 -82
  182. janito/agent/message_handler.py +0 -20
  183. janito/agent/message_handler_protocol.py +0 -5
  184. janito/agent/openai_client.py +0 -149
  185. janito/agent/openai_schema_generator.py +0 -187
  186. janito/agent/profile_manager.py +0 -96
  187. janito/agent/queued_message_handler.py +0 -50
  188. janito/agent/rich_live.py +0 -32
  189. janito/agent/rich_message_handler.py +0 -115
  190. janito/agent/runtime_config.py +0 -36
  191. janito/agent/test_handler_protocols.py +0 -47
  192. janito/agent/test_openai_schema_generator.py +0 -93
  193. janito/agent/tests/__init__.py +0 -1
  194. janito/agent/tool_base.py +0 -63
  195. janito/agent/tool_executor.py +0 -122
  196. janito/agent/tool_registry.py +0 -49
  197. janito/agent/tools/__init__.py +0 -47
  198. janito/agent/tools/create_file.py +0 -59
  199. janito/agent/tools/delete_text_in_file.py +0 -97
  200. janito/agent/tools/find_files.py +0 -106
  201. janito/agent/tools/get_file_outline/core.py +0 -81
  202. janito/agent/tools/present_choices.py +0 -64
  203. janito/agent/tools/python_command_runner.py +0 -201
  204. janito/agent/tools/python_file_runner.py +0 -199
  205. janito/agent/tools/python_stdin_runner.py +0 -208
  206. janito/agent/tools/replace_file.py +0 -72
  207. janito/agent/tools/run_bash_command.py +0 -218
  208. janito/agent/tools/run_powershell_command.py +0 -251
  209. janito/agent/tools_utils/__init__.py +0 -1
  210. janito/agent/tools_utils/action_type.py +0 -7
  211. janito/agent/tools_utils/test_gitignore_utils.py +0 -46
  212. janito/cli/_livereload_log_utils.py +0 -13
  213. janito/cli/_print_config.py +0 -96
  214. janito/cli/_termweb_log_utils.py +0 -17
  215. janito/cli/_utils.py +0 -9
  216. janito/cli/arg_parser.py +0 -272
  217. janito/cli/cli_main.py +0 -281
  218. janito/cli/config_commands.py +0 -211
  219. janito/cli/config_runner.py +0 -35
  220. janito/cli/formatting_runner.py +0 -12
  221. janito/cli/livereload_starter.py +0 -60
  222. janito/cli/logging_setup.py +0 -38
  223. janito/cli/one_shot.py +0 -80
  224. janito/livereload/app.py +0 -25
  225. janito/rich_utils.py +0 -59
  226. janito/shell/__init__.py +0 -0
  227. janito/shell/commands/__init__.py +0 -61
  228. janito/shell/commands/config.py +0 -22
  229. janito/shell/commands/edit.py +0 -24
  230. janito/shell/commands/history_view.py +0 -18
  231. janito/shell/commands/lang.py +0 -19
  232. janito/shell/commands/livelogs.py +0 -42
  233. janito/shell/commands/prompt.py +0 -62
  234. janito/shell/commands/termweb_log.py +0 -94
  235. janito/shell/commands/tools.py +0 -26
  236. janito/shell/commands/track.py +0 -36
  237. janito/shell/main.py +0 -326
  238. janito/shell/prompt/load_prompt.py +0 -57
  239. janito/shell/prompt/session_setup.py +0 -57
  240. janito/shell/session/config.py +0 -109
  241. janito/shell/session/history.py +0 -0
  242. janito/shell/ui/interactive.py +0 -226
  243. janito/termweb/static/editor.css +0 -158
  244. janito/termweb/static/editor.css.bak +0 -145
  245. janito/termweb/static/editor.html +0 -46
  246. janito/termweb/static/editor.html.bak +0 -46
  247. janito/termweb/static/editor.js +0 -265
  248. janito/termweb/static/editor.js.bak +0 -259
  249. janito/termweb/static/explorer.html.bak +0 -59
  250. janito/termweb/static/favicon.ico +0 -0
  251. janito/termweb/static/favicon.ico.bak +0 -0
  252. janito/termweb/static/index.html +0 -53
  253. janito/termweb/static/index.html.bak +0 -54
  254. janito/termweb/static/index.html.bak.bak +0 -175
  255. janito/termweb/static/landing.html.bak +0 -36
  256. janito/termweb/static/termicon.svg +0 -1
  257. janito/termweb/static/termweb.css +0 -214
  258. janito/termweb/static/termweb.css.bak +0 -237
  259. janito/termweb/static/termweb.js +0 -162
  260. janito/termweb/static/termweb.js.bak +0 -168
  261. janito/termweb/static/termweb.js.bak.bak +0 -157
  262. janito/termweb/static/termweb_quickopen.js +0 -135
  263. janito/termweb/static/termweb_quickopen.js.bak +0 -125
  264. janito/tests/test_rich_utils.py +0 -44
  265. janito/web/__init__.py +0 -0
  266. janito/web/__main__.py +0 -25
  267. janito/web/app.py +0 -145
  268. janito-1.14.3.dist-info/METADATA +0 -313
  269. janito-1.14.3.dist-info/RECORD +0 -162
  270. janito-1.14.3.dist-info/licenses/LICENSE +0 -21
  271. /janito/{shell → cli/chat_mode/shell}/input_history.py +0 -0
  272. /janito/{shell/commands/session.py → cli/chat_mode/shell/session/history.py} +0 -0
  273. /janito/{agent/tools_utils/formatting.py → formatting.py} +0 -0
  274. /janito/{agent/tools_utils/gitignore_utils.py → gitignore_utils.py} +0 -0
  275. /janito/{agent/platform_discovery.py → platform_discovery.py} +0 -0
  276. /janito/{agent/tools → tools/adapters/local}/get_file_outline/__init__.py +0 -0
  277. /janito/{agent/tools → tools/adapters/local}/get_file_outline/markdown_outline.py +0 -0
  278. /janito/{agent/tools → tools/adapters/local}/search_text/__init__.py +0 -0
  279. /janito/{agent/tools → tools/adapters/local}/validate_file_syntax/__init__.py +0 -0
  280. {janito-1.14.3.dist-info → janito-2.0.0.dist-info}/WHEEL +0 -0
  281. {janito-1.14.3.dist-info → janito-2.0.0.dist-info}/entry_points.txt +0 -0
  282. {janito-1.14.3.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)
@@ -0,0 +1,3 @@
1
+ from janito.performance_collector import PerformanceCollector
2
+
3
+ performance_collector = PerformanceCollector()