janito 1.8.1__py3-none-any.whl → 1.10.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 (142) hide show
  1. janito/__init__.py +1 -1
  2. janito/agent/api_exceptions.py +4 -0
  3. janito/agent/config.py +1 -1
  4. janito/agent/config_defaults.py +2 -3
  5. janito/agent/config_utils.py +0 -9
  6. janito/agent/conversation.py +177 -114
  7. janito/agent/conversation_api.py +179 -159
  8. janito/agent/conversation_tool_calls.py +11 -8
  9. janito/agent/llm_conversation_history.py +70 -0
  10. janito/agent/openai_client.py +44 -21
  11. janito/agent/openai_schema_generator.py +164 -128
  12. janito/agent/platform_discovery.py +134 -77
  13. janito/agent/profile_manager.py +5 -5
  14. janito/agent/rich_message_handler.py +80 -31
  15. janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +9 -8
  16. janito/agent/test_openai_schema_generator.py +93 -0
  17. janito/agent/tool_base.py +7 -2
  18. janito/agent/tool_executor.py +63 -50
  19. janito/agent/tool_registry.py +5 -2
  20. janito/agent/tool_use_tracker.py +42 -5
  21. janito/agent/tools/__init__.py +13 -12
  22. janito/agent/tools/create_directory.py +9 -6
  23. janito/agent/tools/create_file.py +35 -54
  24. janito/agent/tools/delete_text_in_file.py +97 -0
  25. janito/agent/tools/fetch_url.py +50 -5
  26. janito/agent/tools/find_files.py +40 -26
  27. janito/agent/tools/get_file_outline/__init__.py +1 -0
  28. janito/agent/tools/{outline_file/__init__.py → get_file_outline/core.py} +14 -18
  29. janito/agent/tools/get_file_outline/python_outline.py +134 -0
  30. janito/agent/tools/{search_outline.py → get_file_outline/search_outline.py} +11 -0
  31. janito/agent/tools/get_lines.py +21 -12
  32. janito/agent/tools/move_file.py +13 -12
  33. janito/agent/tools/present_choices.py +3 -1
  34. janito/agent/tools/python_command_runner.py +150 -0
  35. janito/agent/tools/python_file_runner.py +148 -0
  36. janito/agent/tools/python_stdin_runner.py +154 -0
  37. janito/agent/tools/remove_directory.py +4 -2
  38. janito/agent/tools/remove_file.py +15 -13
  39. janito/agent/tools/replace_file.py +72 -0
  40. janito/agent/tools/replace_text_in_file.py +7 -5
  41. janito/agent/tools/run_bash_command.py +29 -72
  42. janito/agent/tools/run_powershell_command.py +142 -102
  43. janito/agent/tools/search_text.py +177 -131
  44. janito/agent/tools/validate_file_syntax/__init__.py +1 -0
  45. janito/agent/tools/validate_file_syntax/core.py +94 -0
  46. janito/agent/tools/validate_file_syntax/css_validator.py +35 -0
  47. janito/agent/tools/validate_file_syntax/html_validator.py +77 -0
  48. janito/agent/tools/validate_file_syntax/js_validator.py +27 -0
  49. janito/agent/tools/validate_file_syntax/json_validator.py +6 -0
  50. janito/agent/tools/validate_file_syntax/markdown_validator.py +66 -0
  51. janito/agent/tools/validate_file_syntax/ps1_validator.py +32 -0
  52. janito/agent/tools/validate_file_syntax/python_validator.py +5 -0
  53. janito/agent/tools/validate_file_syntax/xml_validator.py +11 -0
  54. janito/agent/tools/validate_file_syntax/yaml_validator.py +6 -0
  55. janito/agent/tools_utils/__init__.py +1 -0
  56. janito/agent/tools_utils/action_type.py +7 -0
  57. janito/agent/tools_utils/dir_walk_utils.py +24 -0
  58. janito/agent/tools_utils/formatting.py +49 -0
  59. janito/agent/tools_utils/gitignore_utils.py +69 -0
  60. janito/agent/tools_utils/test_gitignore_utils.py +46 -0
  61. janito/agent/tools_utils/utils.py +30 -0
  62. janito/cli/_livereload_log_utils.py +13 -0
  63. janito/cli/_print_config.py +63 -61
  64. janito/cli/arg_parser.py +57 -14
  65. janito/cli/cli_main.py +270 -0
  66. janito/cli/livereload_starter.py +60 -0
  67. janito/cli/main.py +166 -99
  68. janito/cli/one_shot.py +80 -0
  69. janito/cli/termweb_starter.py +2 -2
  70. janito/i18n/__init__.py +1 -1
  71. janito/livereload/app.py +25 -0
  72. janito/rich_utils.py +41 -25
  73. janito/{cli_chat_shell → shell}/commands/__init__.py +19 -14
  74. janito/{cli_chat_shell → shell}/commands/config.py +4 -4
  75. janito/shell/commands/conversation_restart.py +74 -0
  76. janito/shell/commands/edit.py +24 -0
  77. janito/shell/commands/history_view.py +18 -0
  78. janito/{cli_chat_shell → shell}/commands/lang.py +3 -0
  79. janito/shell/commands/livelogs.py +42 -0
  80. janito/{cli_chat_shell → shell}/commands/prompt.py +16 -6
  81. janito/shell/commands/session.py +35 -0
  82. janito/{cli_chat_shell → shell}/commands/session_control.py +3 -5
  83. janito/{cli_chat_shell → shell}/commands/termweb_log.py +18 -10
  84. janito/shell/commands/tools.py +26 -0
  85. janito/shell/commands/track.py +36 -0
  86. janito/shell/commands/utility.py +28 -0
  87. janito/{cli_chat_shell → shell}/commands/verbose.py +4 -5
  88. janito/shell/commands.py +40 -0
  89. janito/shell/input_history.py +62 -0
  90. janito/shell/main.py +257 -0
  91. janito/{cli_chat_shell/shell_command_completer.py → shell/prompt/completer.py} +1 -1
  92. janito/{cli_chat_shell/chat_ui.py → shell/prompt/session_setup.py} +19 -5
  93. janito/shell/session/manager.py +101 -0
  94. janito/{cli_chat_shell/ui.py → shell/ui/interactive.py} +23 -17
  95. janito/termweb/app.py +3 -3
  96. janito/termweb/static/editor.css +142 -0
  97. janito/termweb/static/editor.css.bak +27 -0
  98. janito/termweb/static/editor.html +15 -213
  99. janito/termweb/static/editor.html.bak +16 -215
  100. janito/termweb/static/editor.js +209 -0
  101. janito/termweb/static/editor.js.bak +227 -0
  102. janito/termweb/static/index.html +2 -3
  103. janito/termweb/static/index.html.bak +2 -3
  104. janito/termweb/static/termweb.css.bak +33 -84
  105. janito/termweb/static/termweb.js +15 -34
  106. janito/termweb/static/termweb.js.bak +18 -36
  107. janito/tests/test_rich_utils.py +44 -0
  108. janito/web/app.py +0 -75
  109. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/METADATA +62 -42
  110. janito-1.10.0.dist-info/RECORD +158 -0
  111. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/WHEEL +1 -1
  112. janito/agent/tools/dir_walk_utils.py +0 -16
  113. janito/agent/tools/gitignore_utils.py +0 -46
  114. janito/agent/tools/memory.py +0 -48
  115. janito/agent/tools/outline_file/formatting.py +0 -20
  116. janito/agent/tools/outline_file/python_outline.py +0 -71
  117. janito/agent/tools/present_choices_test.py +0 -18
  118. janito/agent/tools/rich_live.py +0 -44
  119. janito/agent/tools/run_python_command.py +0 -163
  120. janito/agent/tools/tools_utils.py +0 -56
  121. janito/agent/tools/utils.py +0 -33
  122. janito/agent/tools/validate_file_syntax.py +0 -163
  123. janito/cli/runner/cli_main.py +0 -180
  124. janito/cli_chat_shell/chat_loop.py +0 -163
  125. janito/cli_chat_shell/chat_state.py +0 -38
  126. janito/cli_chat_shell/commands/history_start.py +0 -37
  127. janito/cli_chat_shell/commands/session.py +0 -48
  128. janito/cli_chat_shell/commands/sum.py +0 -49
  129. janito/cli_chat_shell/commands/utility.py +0 -32
  130. janito/cli_chat_shell/session_manager.py +0 -72
  131. janito-1.8.1.dist-info/RECORD +0 -127
  132. /janito/agent/tools/{outline_file → get_file_outline}/markdown_outline.py +0 -0
  133. /janito/cli/{runner/_termweb_log_utils.py → _termweb_log_utils.py} +0 -0
  134. /janito/cli/{runner/config.py → config_runner.py} +0 -0
  135. /janito/cli/{runner/formatting.py → formatting_runner.py} +0 -0
  136. /janito/{cli/runner → shell}/__init__.py +0 -0
  137. /janito/{cli_chat_shell → shell/prompt}/load_prompt.py +0 -0
  138. /janito/{cli_chat_shell/config_shell.py → shell/session/config.py} +0 -0
  139. /janito/{cli_chat_shell/__init__.py → shell/session/history.py} +0 -0
  140. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/entry_points.txt +0 -0
  141. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/licenses/LICENSE +0 -0
  142. {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/top_level.txt +0 -0
janito/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.8.1"
1
+ __version__ = "1.10.0-dev"
@@ -0,0 +1,4 @@
1
+ class ApiError(Exception):
2
+ """Custom exception for API errors."""
3
+
4
+ pass
janito/agent/config.py CHANGED
@@ -55,7 +55,7 @@ class FileConfig(BaseConfig):
55
55
  CONFIG_OPTIONS = {
56
56
  "api_key": "API key for OpenAI-compatible service (required)",
57
57
  "trust": "Trust mode: suppress all console output (bool, default: False)",
58
- "model": "Model name to use (e.g., 'openai/gpt-4.1')",
58
+ "model": "Model name to use (e.g., 'gpt-4.1')",
59
59
  "base_url": "API base URL (OpenAI-compatible endpoint)",
60
60
  "role": "Role description for the Agent Profile (e.g., 'software engineer')",
61
61
  "system_prompt_template": "Override the entire Agent Profile prompt text",
@@ -1,12 +1,11 @@
1
1
  # Centralized config defaults for Janito
2
2
  CONFIG_DEFAULTS = {
3
3
  "api_key": None, # Must be set by user
4
- "model": "openai/gpt-4.1", # Default model
5
- "base_url": "https://openrouter.ai/api/v1",
4
+ "model": "gpt-4.1", # Default model
6
5
  "role": "software developer", # Part of the Agent Profile
7
6
  "system_prompt_template": None, # None means auto-generate from Agent Profile role
8
7
  "temperature": 0.2,
9
- "max_tokens": 200000,
8
+ "max_tokens": 32000,
10
9
  "use_azure_openai": False,
11
10
  "azure_openai_api_version": "2023-05-15",
12
11
  "profile": "base",
@@ -1,9 +0,0 @@
1
- def merge_configs(*configs):
2
- """
3
- Merge multiple config-like objects (with .all()) into one dict.
4
- Later configs override earlier ones.
5
- """
6
- merged = {}
7
- for cfg in configs:
8
- merged.update(cfg.all())
9
- return merged
@@ -1,6 +1,5 @@
1
1
  from janito.agent.conversation_api import (
2
2
  get_openai_response,
3
- get_openai_stream_response,
4
3
  retry_api_call,
5
4
  )
6
5
  from janito.agent.conversation_tool_calls import handle_tool_calls
@@ -11,7 +10,32 @@ from janito.agent.conversation_exceptions import (
11
10
  NoToolSupportError,
12
11
  )
13
12
  from janito.agent.runtime_config import unified_config, runtime_config
13
+ from janito.agent.api_exceptions import ApiError
14
14
  import pprint
15
+ from janito.agent.llm_conversation_history import LLMConversationHistory
16
+
17
+
18
+ def get_openai_response_with_content_check(client, model, messages, max_tokens):
19
+ response = get_openai_response(client, model, messages, max_tokens)
20
+ # Check for empty assistant message content, but allow tool/function calls
21
+ if not hasattr(response, "choices") or not response.choices:
22
+ return response # Let normal error handling occur
23
+ choice = response.choices[0]
24
+ content = getattr(choice.message, "content", None)
25
+ # Check for function_call (legacy OpenAI) or tool_calls (OpenAI v2 and others)
26
+ has_function_call = (
27
+ hasattr(choice.message, "function_call") and choice.message.function_call
28
+ )
29
+ has_tool_calls = hasattr(choice.message, "tool_calls") and choice.message.tool_calls
30
+ if (content is None or str(content).strip() == "") and not (
31
+ has_function_call or has_tool_calls
32
+ ):
33
+ print(
34
+ "[DEBUG] Empty assistant message detected with no tool/function call. Will retry. Raw response:"
35
+ )
36
+ print(repr(response))
37
+ raise EmptyResponseError("Empty assistant message content.")
38
+ return response
15
39
 
16
40
 
17
41
  class ConversationHandler:
@@ -27,24 +51,10 @@ class ConversationHandler:
27
51
  """
28
52
  return [msg for msg in messages if msg.get("role") != "system"]
29
53
 
30
- def handle_conversation(
31
- self,
32
- messages,
33
- max_rounds=50,
34
- message_handler=None,
35
- verbose_response=False,
36
- spinner=False,
37
- max_tokens=None,
38
- verbose_events=False,
39
- stream=False,
40
- verbose_stream=False,
41
- ):
42
- if not messages:
43
- raise ValueError("No prompt provided in messages")
44
-
54
+ def _resolve_max_tokens(self, max_tokens):
45
55
  resolved_max_tokens = max_tokens
46
56
  if resolved_max_tokens is None:
47
- resolved_max_tokens = unified_config.get("max_tokens", 200000)
57
+ resolved_max_tokens = unified_config.get("max_tokens", 32000)
48
58
  try:
49
59
  resolved_max_tokens = int(resolved_max_tokens)
50
60
  except (TypeError, ValueError):
@@ -53,123 +63,176 @@ class ConversationHandler:
53
63
  resolved_max_tokens=resolved_max_tokens
54
64
  )
55
65
  )
56
-
57
- # If vanilla mode is set and max_tokens was not provided, default to 8000
58
66
  if runtime_config.get("vanilla_mode", False) and max_tokens is None:
59
67
  resolved_max_tokens = 8000
68
+ return resolved_max_tokens
69
+
70
+ def _call_openai_api(self, history, resolved_max_tokens, spinner):
71
+ def api_call():
72
+ return get_openai_response_with_content_check(
73
+ self.client,
74
+ self.model,
75
+ history.get_messages(),
76
+ resolved_max_tokens,
77
+ )
78
+
79
+ user_message_on_empty = "Received an empty message from you. Please try again."
80
+ if spinner:
81
+ response = show_spinner(
82
+ "Waiting for AI response...",
83
+ retry_api_call,
84
+ api_call,
85
+ history=history,
86
+ user_message_on_empty=user_message_on_empty,
87
+ )
88
+ else:
89
+ response = retry_api_call(
90
+ api_call, history=history, user_message_on_empty=user_message_on_empty
91
+ )
92
+ return response
93
+
94
+ def _handle_no_tool_support(self, messages, max_tokens, spinner):
95
+ print(
96
+ "\u26a0\ufe0f Endpoint does not support tool use. Proceeding in vanilla mode (tools disabled)."
97
+ )
98
+ runtime_config.set("vanilla_mode", True)
99
+ resolved_max_tokens = 8000
100
+ if max_tokens is None:
101
+ runtime_config.set("max_tokens", 8000)
102
+
103
+ def api_call_vanilla():
104
+ return get_openai_response_with_content_check(
105
+ self.client, self.model, messages, resolved_max_tokens
106
+ )
107
+
108
+ user_message_on_empty = "Received an empty message from you. Please try again."
109
+ if spinner:
110
+ response = show_spinner(
111
+ "Waiting for AI response (tools disabled)...",
112
+ retry_api_call,
113
+ api_call_vanilla,
114
+ history=None,
115
+ user_message_on_empty=user_message_on_empty,
116
+ )
117
+ else:
118
+ response = retry_api_call(
119
+ api_call_vanilla,
120
+ history=None,
121
+ user_message_on_empty=user_message_on_empty,
122
+ )
123
+ print(
124
+ "[DEBUG] OpenAI API raw response (tools disabled):",
125
+ repr(response),
126
+ )
127
+ return response, resolved_max_tokens
128
+
129
+ def _process_response(self, response):
130
+ if runtime_config.get("verbose_response", False):
131
+ pprint.pprint(response)
132
+ if response is None or not getattr(response, "choices", None):
133
+ error = getattr(response, "error", None)
134
+ if error:
135
+ print(f"ApiError: {error.get('message', error)}")
136
+ raise ApiError(error.get("message", str(error)))
137
+ raise EmptyResponseError(
138
+ f"No choices in response; possible API or LLM error. Raw response: {response!r}"
139
+ )
140
+ choice = response.choices[0]
141
+ usage = getattr(response, "usage", None)
142
+ usage_info = (
143
+ {
144
+ "_debug_raw_usage": getattr(response, "usage", None),
145
+ "prompt_tokens": getattr(usage, "prompt_tokens", None),
146
+ "completion_tokens": getattr(usage, "completion_tokens", None),
147
+ "total_tokens": getattr(usage, "total_tokens", None),
148
+ }
149
+ if usage
150
+ else None
151
+ )
152
+ return choice, usage_info
153
+
154
+ def _handle_tool_calls(
155
+ self, choice, history, message_handler, usage_info, tool_user=False
156
+ ):
157
+ tool_responses = handle_tool_calls(
158
+ choice.message.tool_calls, message_handler=message_handler
159
+ )
160
+ agent_idx = len([m for m in history.get_messages() if m.get("role") == "agent"])
161
+ self.usage_history.append({"agent_index": agent_idx, "usage": usage_info})
162
+ history.add_message(
163
+ {
164
+ "role": "assistant",
165
+ "content": choice.message.content,
166
+ "tool_calls": [tc.to_dict() for tc in choice.message.tool_calls],
167
+ }
168
+ )
169
+ for tool_response in tool_responses:
170
+ history.add_message(
171
+ {
172
+ "role": "user" if tool_user else "tool",
173
+ "tool_call_id": tool_response["tool_call_id"],
174
+ "content": tool_response["content"],
175
+ }
176
+ )
177
+
178
+ def handle_conversation(
179
+ self,
180
+ messages,
181
+ max_rounds=100,
182
+ message_handler=None,
183
+ verbose_response=False,
184
+ spinner=False,
185
+ max_tokens=None,
186
+ verbose_events=False,
187
+ tool_user=False,
188
+ ):
189
+
190
+ if isinstance(messages, LLMConversationHistory):
191
+ history = messages
192
+ else:
193
+ history = LLMConversationHistory(messages)
194
+
195
+ if len(history) == 0:
196
+ raise ValueError("No prompt provided in messages")
197
+
198
+ resolved_max_tokens = self._resolve_max_tokens(max_tokens)
60
199
 
61
200
  for _ in range(max_rounds):
62
201
  try:
63
- if stream:
64
- # Streaming mode
65
- def get_stream():
66
- return get_openai_stream_response(
67
- self.client,
68
- self.model,
69
- messages,
70
- resolved_max_tokens,
71
- verbose_stream=runtime_config.get("verbose_stream", False),
72
- message_handler=message_handler,
73
- )
74
-
75
- retry_api_call(get_stream)
76
- return None
77
- else:
78
- # Non-streaming mode
79
- def api_call():
80
- return get_openai_response(
81
- self.client, self.model, messages, resolved_max_tokens
82
- )
83
-
84
- if spinner:
85
- response = show_spinner(
86
- "Waiting for AI response...", retry_api_call, api_call
87
- )
88
- else:
89
- response = retry_api_call(api_call)
90
- print("[DEBUG] OpenAI API raw response:", repr(response))
202
+ response = self._call_openai_api(history, resolved_max_tokens, spinner)
203
+ error = getattr(response, "error", None)
204
+ if error:
205
+ print(f"ApiError: {error.get('message', error)}")
206
+ raise ApiError(error.get("message", str(error)))
91
207
  except NoToolSupportError:
92
- print(
93
- "⚠️ Endpoint does not support tool use. Proceeding in vanilla mode (tools disabled)."
208
+ response, resolved_max_tokens = self._handle_no_tool_support(
209
+ messages, max_tokens, spinner
94
210
  )
95
- runtime_config.set("vanilla_mode", True)
96
- if max_tokens is None:
97
- runtime_config.set("max_tokens", 8000)
98
- resolved_max_tokens = 8000
99
-
100
- # Remove system prompt for vanilla mode if needed (call this externally when appropriate)
101
- # messages = ConversationHandler.remove_system_prompt(messages)
102
- # Retry once with tools disabled
103
- def api_call_vanilla():
104
- return get_openai_response(
105
- self.client, self.model, messages, resolved_max_tokens
106
- )
107
-
108
- if spinner:
109
- response = show_spinner(
110
- "Waiting for AI response (tools disabled)...",
111
- retry_api_call,
112
- api_call_vanilla,
113
- )
114
- else:
115
- response = retry_api_call(api_call_vanilla)
116
- print(
117
- "[DEBUG] OpenAI API raw response (tools disabled):",
118
- repr(response),
119
- )
120
- if runtime_config.get("verbose_response", False):
121
- pprint.pprint(response)
122
- if response is None or not getattr(response, "choices", None):
123
- raise EmptyResponseError(
124
- f"No choices in response; possible API or LLM error. Raw response: {response!r}"
125
- )
126
- choice = response.choices[0]
127
- usage = getattr(response, "usage", None)
128
- usage_info = (
129
- {
130
- # DEBUG: Show usage extraction
131
- "_debug_raw_usage": getattr(response, "usage", None),
132
- "prompt_tokens": getattr(usage, "prompt_tokens", None),
133
- "completion_tokens": getattr(usage, "completion_tokens", None),
134
- "total_tokens": getattr(usage, "total_tokens", None),
135
- }
136
- if usage
137
- else None
138
- )
211
+ choice, usage_info = self._process_response(response)
139
212
  event = {"type": "content", "message": choice.message.content}
140
213
  if runtime_config.get("verbose_events", False):
141
214
  print_verbose_event(event)
142
215
  if message_handler is not None and choice.message.content:
143
216
  message_handler.handle_message(event)
144
217
  if not choice.message.tool_calls:
145
- agent_idx = len([m for m in messages if m.get("role") == "agent"])
218
+ agent_idx = len(
219
+ [m for m in history.get_messages() if m.get("role") == "agent"]
220
+ )
146
221
  self.usage_history.append(
147
222
  {"agent_index": agent_idx, "usage": usage_info}
148
223
  )
224
+ history.add_message(
225
+ {
226
+ "role": "assistant",
227
+ "content": choice.message.content,
228
+ }
229
+ )
149
230
  return {
150
231
  "content": choice.message.content,
151
232
  "usage": usage_info,
152
233
  "usage_history": self.usage_history,
153
234
  }
154
- # Tool calls
155
- tool_responses = handle_tool_calls(
156
- choice.message.tool_calls, message_handler=message_handler
235
+ self._handle_tool_calls(
236
+ choice, history, message_handler, usage_info, tool_user=tool_user
157
237
  )
158
- agent_idx = len([m for m in messages if m.get("role") == "agent"])
159
- self.usage_history.append({"agent_index": agent_idx, "usage": usage_info})
160
- messages.append(
161
- {
162
- "role": "assistant",
163
- "content": choice.message.content,
164
- "tool_calls": [tc.to_dict() for tc in choice.message.tool_calls],
165
- }
166
- )
167
- for tool_response in tool_responses:
168
- messages.append(
169
- {
170
- "role": "tool",
171
- "tool_call_id": tool_response["tool_call_id"],
172
- "content": tool_response["content"],
173
- }
174
- )
175
238
  raise MaxRoundsExceededError(f"Max conversation rounds exceeded ({max_rounds})")