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
@@ -7,21 +7,40 @@ from janito.i18n import tr
7
7
  import json
8
8
  from janito.agent.runtime_config import runtime_config
9
9
  from janito.agent.tool_registry import get_tool_schemas
10
- from janito.agent.conversation_exceptions import NoToolSupportError
10
+ from janito.agent.conversation_exceptions import NoToolSupportError, EmptyResponseError
11
+ from janito.agent.api_exceptions import ApiError
12
+
13
+
14
+ def _sanitize_utf8_surrogates(obj):
15
+ """
16
+ Recursively sanitize a dict/list/string by replacing surrogate codepoints with the unicode replacement character.
17
+ """
18
+ if isinstance(obj, str):
19
+ # Encode with surrogatepass, then decode with 'utf-8', replacing errors
20
+ return obj.encode("utf-8", "replace").decode("utf-8", "replace")
21
+ elif isinstance(obj, dict):
22
+ return {k: _sanitize_utf8_surrogates(v) for k, v in obj.items()}
23
+ elif isinstance(obj, list):
24
+ return [_sanitize_utf8_surrogates(x) for x in obj]
25
+ else:
26
+ return obj
11
27
 
12
28
 
13
29
  def get_openai_response(
14
30
  client, model, messages, max_tokens, tools=None, tool_choice=None, temperature=None
15
31
  ):
16
- """Non-streaming OpenAI API call."""
32
+ """OpenAI API call."""
33
+ messages = _sanitize_utf8_surrogates(messages)
34
+ from janito.agent.conversation_exceptions import ProviderError
35
+
17
36
  if runtime_config.get("vanilla_mode", False):
18
- return client.chat.completions.create(
37
+ response = client.chat.completions.create(
19
38
  model=model,
20
39
  messages=messages,
21
40
  max_tokens=max_tokens,
22
41
  )
23
42
  else:
24
- return client.chat.completions.create(
43
+ response = client.chat.completions.create(
25
44
  model=model,
26
45
  messages=messages,
27
46
  tools=tools or get_tool_schemas(),
@@ -29,170 +48,171 @@ def get_openai_response(
29
48
  temperature=temperature if temperature is not None else 0.2,
30
49
  max_tokens=max_tokens,
31
50
  )
51
+ # Explicitly check for missing or empty choices (API/LLM error)
52
+ if (
53
+ not hasattr(response, "choices")
54
+ or response.choices is None
55
+ or len(response.choices) == 0
56
+ ):
57
+ # Always check for error before raising ProviderError
58
+ error = getattr(response, "error", None)
59
+ if error:
60
+ print(f"ApiError: {error.get('message', error)}")
61
+ print(f"Full error object: {error}")
62
+ print(f"Raw response: {response}")
63
+ raise ApiError(error.get("message", str(error)))
64
+ raise ProviderError(
65
+ f"No choices in response; possible API or LLM error. Raw response: {response!r}",
66
+ {"code": 502, "raw_response": str(response)},
67
+ )
68
+ return response
32
69
 
33
70
 
34
- def get_openai_stream_response(
35
- client,
36
- model,
37
- messages,
38
- max_tokens,
39
- tools=None,
40
- tool_choice=None,
41
- temperature=None,
42
- verbose_stream=False,
43
- message_handler=None,
44
- ):
45
- """Streaming OpenAI API call."""
46
- openai_args = dict(
47
- model=model,
48
- messages=messages,
49
- max_tokens=max_tokens,
50
- stream=True,
71
+ def _extract_status_and_retry_after(e, error_message):
72
+ status_code = None
73
+ retry_after = None
74
+ if hasattr(e, "status_code"):
75
+ status_code = getattr(e, "status_code")
76
+ elif hasattr(e, "response") and hasattr(e.response, "status_code"):
77
+ status_code = getattr(e.response, "status_code")
78
+ if hasattr(e.response, "headers") and e.response.headers:
79
+ retry_after = e.response.headers.get("Retry-After")
80
+ else:
81
+ import re
82
+
83
+ match = re.search(r"[Ee]rror code: (\d{3})", error_message)
84
+ if match:
85
+ status_code = int(match.group(1))
86
+ retry_after_match = re.search(r"Retry-After\['\"]?:?\s*(\d+)", error_message)
87
+ if retry_after_match:
88
+ retry_after = retry_after_match.group(1)
89
+ return status_code, retry_after
90
+
91
+
92
+ def _calculate_wait_time(status_code, retry_after, attempt):
93
+ if status_code == 429 and retry_after is not None:
94
+ try:
95
+ return int(float(retry_after))
96
+ except Exception:
97
+ return 2**attempt
98
+ return 2**attempt
99
+
100
+
101
+ def _log_and_sleep(message, attempt, max_retries, e=None, wait_time=None):
102
+ print(
103
+ tr(
104
+ message,
105
+ attempt=attempt,
106
+ max_retries=max_retries,
107
+ e=e,
108
+ wait_time=wait_time,
109
+ )
51
110
  )
52
- if not runtime_config.get("vanilla_mode", False):
53
- openai_args.update(
54
- tools=tools or get_tool_schemas(),
55
- tool_choice=tool_choice or "auto",
56
- temperature=temperature if temperature is not None else 0.2,
111
+ time.sleep(wait_time)
112
+
113
+
114
+ def _handle_json_decode_error(e, attempt, max_retries):
115
+ if attempt < max_retries:
116
+ wait_time = 2**attempt
117
+ _log_and_sleep(
118
+ "Invalid/malformed response from OpenAI (attempt {attempt}/{max_retries}). Retrying in {wait_time} seconds...",
119
+ attempt,
120
+ max_retries,
121
+ wait_time=wait_time,
57
122
  )
58
- response_stream = client.chat.completions.create(**openai_args)
59
- content_accum = ""
60
- for event in response_stream:
61
- if verbose_stream or runtime_config.get("verbose_stream", False):
62
- print(repr(event), flush=True)
63
- delta = getattr(event.choices[0], "delta", None)
64
- if delta and getattr(delta, "content", None):
65
- chunk = delta.content
66
- content_accum += chunk
67
- if message_handler:
68
- message_handler.handle_message({"type": "stream", "content": chunk})
69
- if message_handler:
70
- message_handler.handle_message({"type": "stream_end", "content": content_accum})
71
- return None
72
-
73
-
74
- def retry_api_call(api_func, max_retries=5, *args, **kwargs):
75
- last_exception = None
76
- for attempt in range(1, max_retries + 1):
77
- try:
78
- return api_func(*args, **kwargs)
79
- except json.JSONDecodeError as e:
80
- last_exception = e
123
+ return None
124
+ else:
125
+ print(tr("Max retries for invalid response reached. Raising error."))
126
+ raise e
127
+
128
+
129
+ def _handle_general_exception(e, attempt, max_retries):
130
+ error_message = str(e)
131
+ if "No endpoints found that support tool use" in error_message:
132
+ print(tr("API does not support tool use."))
133
+ raise NoToolSupportError(error_message)
134
+ status_code, retry_after = _extract_status_and_retry_after(e, error_message)
135
+ if status_code is not None:
136
+ if status_code == 429:
137
+ wait_time = _calculate_wait_time(status_code, retry_after, attempt)
81
138
  if attempt < max_retries:
82
- wait_time = 2**attempt
83
- print(
84
- tr(
85
- "Invalid/malformed response from OpenAI (attempt {attempt}/{max_retries}). Retrying in {wait_time} seconds...",
86
- attempt=attempt,
87
- max_retries=max_retries,
88
- wait_time=wait_time,
89
- )
139
+ _log_and_sleep(
140
+ "OpenAI API rate limit (429) (attempt {attempt}/{max_retries}): {e}. Retrying in {wait_time} seconds...",
141
+ attempt,
142
+ max_retries,
143
+ e=e,
144
+ wait_time=wait_time,
90
145
  )
91
- time.sleep(wait_time)
146
+ return None
92
147
  else:
93
- print(tr("Max retries for invalid response reached. Raising error."))
94
- raise last_exception
95
- except Exception as e:
96
- last_exception = e
97
- status_code = None
98
- error_message = str(e)
99
- retry_after = None
100
- # Detect specific tool support error
101
- if "No endpoints found that support tool use" in error_message:
102
- print(tr("API does not support tool use."))
103
- raise NoToolSupportError(error_message)
104
- # Try to extract status code and Retry-After from known exception types or message
105
- if hasattr(e, "status_code"):
106
- status_code = getattr(e, "status_code")
107
- elif hasattr(e, "response") and hasattr(e.response, "status_code"):
108
- status_code = getattr(e.response, "status_code")
109
- # Check for Retry-After header
110
- if hasattr(e.response, "headers") and e.response.headers:
111
- retry_after = e.response.headers.get("Retry-After")
148
+ print("Max retries for OpenAI API rate limit reached. Raising error.")
149
+ raise e
150
+ elif 500 <= status_code < 600:
151
+ wait_time = 2**attempt
152
+ if attempt < max_retries:
153
+ _log_and_sleep(
154
+ "OpenAI API server error (attempt {attempt}/{max_retries}): {e}. Retrying in {wait_time} seconds...",
155
+ attempt,
156
+ max_retries,
157
+ e=e,
158
+ wait_time=wait_time,
159
+ )
160
+ return None
112
161
  else:
113
- # Try to parse from error message
114
- import re
115
-
116
- match = re.search(r"[Ee]rror code: (\d{3})", error_message)
117
- if match:
118
- status_code = int(match.group(1))
119
- # Try to find Retry-After in message
120
- retry_after_match = re.search(
121
- r"Retry-After['\"]?:?\s*(\d+)", error_message
162
+ print("Max retries for OpenAI API server error reached. Raising error.")
163
+ raise e
164
+ elif 400 <= status_code < 500:
165
+ print(
166
+ tr(
167
+ "OpenAI API client error {status_code}: {e}. Not retrying.",
168
+ status_code=status_code,
169
+ e=e,
122
170
  )
123
- if retry_after_match:
124
- retry_after = retry_after_match.group(1)
125
- # Decide retry logic based on status code
126
- if status_code is not None:
127
- if status_code == 429:
128
- # Use Retry-After if available, else exponential backoff
129
- if retry_after is not None:
130
- try:
131
- wait_time = int(float(retry_after))
132
- except Exception:
133
- wait_time = 2**attempt
134
- else:
135
- wait_time = 2**attempt
136
- if attempt < max_retries:
137
- print(
138
- tr(
139
- "OpenAI API rate limit (429) (attempt {attempt}/{max_retries}): {e}. Retrying in {wait_time} seconds...",
140
- attempt=attempt,
141
- max_retries=max_retries,
142
- e=e,
143
- wait_time=wait_time,
144
- )
145
- )
146
- time.sleep(wait_time)
147
- continue
148
- else:
149
- print(
150
- "Max retries for OpenAI API rate limit reached. Raising error."
151
- )
152
- raise last_exception
153
- elif 500 <= status_code < 600:
154
- # Retry on server errors
155
- if attempt < max_retries:
156
- wait_time = 2**attempt
157
- print(
158
- tr(
159
- "OpenAI API server error (attempt {attempt}/{max_retries}): {e}. Retrying in {wait_time} seconds...",
160
- attempt=attempt,
161
- max_retries=max_retries,
162
- e=e,
163
- wait_time=wait_time,
164
- )
165
- )
166
- time.sleep(wait_time)
167
- continue
168
- else:
169
- print(
170
- "Max retries for OpenAI API server error reached. Raising error."
171
- )
172
- raise last_exception
173
- elif 400 <= status_code < 500:
174
- # Do not retry on client errors (except 429)
175
- print(
176
- tr(
177
- "OpenAI API client error {status_code}: {e}. Not retrying.",
178
- status_code=status_code,
179
- e=e,
180
- )
181
- )
182
- raise last_exception
183
- # If status code not detected, fallback to previous behavior
184
- if attempt < max_retries:
185
- wait_time = 2**attempt
171
+ )
172
+ raise e
173
+ if attempt < max_retries:
174
+ wait_time = 2**attempt
175
+ _log_and_sleep(
176
+ "OpenAI API error (attempt {attempt}/{max_retries}): {e}. Retrying in {wait_time} seconds...",
177
+ attempt,
178
+ max_retries,
179
+ e=e,
180
+ wait_time=wait_time,
181
+ )
182
+ print(f"[DEBUG] Exception repr: {repr(e)}")
183
+ return None
184
+ else:
185
+ print(tr("Max retries for OpenAI API error reached. Raising error."))
186
+ raise e
187
+
188
+
189
+ def retry_api_call(
190
+ api_func, max_retries=5, *args, history=None, user_message_on_empty=None, **kwargs
191
+ ):
192
+ for attempt in range(1, max_retries + 1):
193
+ try:
194
+ response = api_func(*args, **kwargs)
195
+ error = getattr(response, "error", None)
196
+ if error:
197
+ print(f"ApiError: {error.get('message', error)}")
198
+ raise ApiError(error.get("message", str(error)))
199
+ return response
200
+ except ApiError:
201
+ raise
202
+ except EmptyResponseError:
203
+ if history is not None and user_message_on_empty is not None:
186
204
  print(
187
- tr(
188
- "OpenAI API error (attempt {attempt}/{max_retries}): {e}. Retrying in {wait_time} seconds...",
189
- attempt=attempt,
190
- max_retries=max_retries,
191
- e=e,
192
- wait_time=wait_time,
193
- )
205
+ f"[DEBUG] Adding user message to history: {user_message_on_empty}"
194
206
  )
195
- time.sleep(wait_time)
207
+ history.add_message({"role": "user", "content": user_message_on_empty})
208
+ continue # Retry with updated history
196
209
  else:
197
- print(tr("Max retries for OpenAI API error reached. Raising error."))
198
- raise last_exception
210
+ raise
211
+ except json.JSONDecodeError as e:
212
+ result = _handle_json_decode_error(e, attempt, max_retries)
213
+ if result is not None:
214
+ return result
215
+ except Exception as e:
216
+ result = _handle_general_exception(e, attempt, max_retries)
217
+ if result is not None:
218
+ return result
@@ -2,6 +2,7 @@
2
2
  Helpers for handling tool calls in conversation.
3
3
  """
4
4
 
5
+ import json
5
6
  from janito.agent.tool_executor import ToolExecutor
6
7
  from janito.agent import tool_registry
7
8
  from .conversation_exceptions import MaxRoundsExceededError
@@ -19,18 +20,20 @@ def handle_tool_calls(tool_calls, message_handler=None):
19
20
  )
20
21
  tool_entry = tool_registry._tool_registry[tool_call.function.name]
21
22
  try:
22
- result = ToolExecutor(message_handler=message_handler).execute(
23
- tool_entry, tool_call
24
- )
25
- tool_responses.append({"tool_call_id": tool_call.id, "content": result})
26
- except TypeError as e:
27
- # Return the error as a tool result, asking to retry with correct params
28
- error_msg = str(e)
23
+ arguments = json.loads(tool_call.function.arguments)
24
+ except (TypeError, AttributeError, json.JSONDecodeError) as e:
25
+ error_msg = f"Invalid/malformed function parameters: {e}. Please retry with valid JSON arguments."
29
26
  tool_responses.append(
30
27
  {
31
28
  "tool_call_id": tool_call.id,
32
- "content": f"Tool execution error: {error_msg}. Please retry with the correct parameters.",
29
+ "content": error_msg,
33
30
  }
34
31
  )
32
+ tool_calls_made += 1
33
+ continue
34
+ result = ToolExecutor(message_handler=message_handler).execute(
35
+ tool_entry, tool_call, arguments
36
+ )
37
+ tool_responses.append({"tool_call_id": tool_call.id, "content": result})
35
38
  tool_calls_made += 1
36
39
  return tool_responses
@@ -0,0 +1,70 @@
1
+ from typing import List, Dict, Optional
2
+ import json
3
+ import sys
4
+ import traceback
5
+
6
+
7
+ class LLMConversationHistory:
8
+ """
9
+ Manages the message history for a conversation, supporting OpenAI-style roles.
10
+ Intended to be used by ConversationHandler and chat loop for all history operations.
11
+ """
12
+
13
+ def __init__(self, messages: Optional[List[Dict]] = None):
14
+ self._messages = messages.copy() if messages else []
15
+
16
+ def add_message(self, message: Dict):
17
+ """Append a message dict to the history."""
18
+ content = message.get("content")
19
+ if isinstance(content, str) and any(
20
+ 0xD800 <= ord(ch) <= 0xDFFF for ch in content
21
+ ):
22
+ print(
23
+ f"Surrogate code point detected in message content: {content!r}\nStack trace:\n{''.join(traceback.format_stack())}",
24
+ file=sys.stderr,
25
+ )
26
+ self._messages.append(message)
27
+
28
+ def get_messages(self, role: Optional[str] = None) -> List[Dict]:
29
+ """
30
+ Return all messages, or only those matching a given role/type (e.g., 'assistant', 'user', 'tool').
31
+ If role is None, returns all messages.
32
+ """
33
+ if role is None:
34
+ return self._messages.copy()
35
+ return [msg for msg in self._messages if msg.get("role") == role]
36
+
37
+ def clear(self):
38
+ """Remove all messages from history."""
39
+ self._messages.clear()
40
+
41
+ def set_system_message(self, content: str):
42
+ """
43
+ Replace the first system prompt message, or insert if not present.
44
+ """
45
+ system_idx = next(
46
+ (i for i, m in enumerate(self._messages) if m.get("role") == "system"), None
47
+ )
48
+ system_msg = {"role": "system", "content": content}
49
+ if isinstance(content, str) and any(
50
+ 0xD800 <= ord(ch) <= 0xDFFF for ch in content
51
+ ):
52
+ print(
53
+ f"Surrogate code point detected in system message content: {content!r}\nStack trace:\n{''.join(traceback.format_stack())}",
54
+ file=sys.stderr,
55
+ )
56
+ if system_idx is not None:
57
+ self._messages[system_idx] = system_msg
58
+ else:
59
+ self._messages.insert(0, system_msg)
60
+
61
+ def to_json_file(self, path: str):
62
+ """Save the conversation history as a JSON file to the given path."""
63
+ with open(path, "w", encoding="utf-8") as f:
64
+ json.dump(self.get_messages(), f, indent=2, ensure_ascii=False)
65
+
66
+ def __len__(self):
67
+ return len(self._messages)
68
+
69
+ def __getitem__(self, idx):
70
+ return self._messages[idx]
@@ -4,6 +4,7 @@ import time
4
4
  from openai import OpenAI
5
5
  from janito.agent.conversation import ConversationHandler
6
6
  from janito.agent.conversation_exceptions import ProviderError
7
+ from janito.agent.llm_conversation_history import LLMConversationHistory
7
8
 
8
9
 
9
10
  class Agent:
@@ -18,7 +19,7 @@ class Agent:
18
19
  model: str = None,
19
20
  system_prompt_template: str | None = None,
20
21
  verbose_tools: bool = False,
21
- base_url: str = "https://openrouter.ai/api/v1",
22
+ base_url: str = None,
22
23
  azure_openai_api_version: str = "2023-05-15",
23
24
  use_azure_openai: bool = False,
24
25
  ):
@@ -41,17 +42,35 @@ class Agent:
41
42
  # Import inside conditional to avoid requiring AzureOpenAI unless needed
42
43
  from openai import AzureOpenAI
43
44
 
44
- self.client = AzureOpenAI(
45
- api_key=api_key,
46
- azure_endpoint=base_url,
47
- api_version=azure_openai_api_version,
48
- )
45
+ if base_url:
46
+ self.client = AzureOpenAI(
47
+ api_key=api_key,
48
+ azure_endpoint=base_url,
49
+ api_version=azure_openai_api_version,
50
+ )
51
+ else:
52
+ self.client = AzureOpenAI(
53
+ api_key=api_key,
54
+ api_version=azure_openai_api_version,
55
+ )
49
56
  else:
50
- self.client = OpenAI(
51
- base_url=base_url,
52
- api_key=api_key,
53
- default_headers={"HTTP-Referer": self.REFERER, "X-Title": self.TITLE},
54
- )
57
+ if base_url:
58
+ self.client = OpenAI(
59
+ base_url=base_url,
60
+ api_key=api_key,
61
+ default_headers={
62
+ "HTTP-Referer": self.REFERER,
63
+ "X-Title": self.TITLE,
64
+ },
65
+ )
66
+ else:
67
+ self.client = OpenAI(
68
+ api_key=api_key,
69
+ default_headers={
70
+ "HTTP-Referer": self.REFERER,
71
+ "X-Title": self.TITLE,
72
+ },
73
+ )
55
74
 
56
75
  self.conversation_handler = ConversationHandler(
57
76
  self.client,
@@ -64,29 +83,34 @@ class Agent:
64
83
 
65
84
  def chat(
66
85
  self,
67
- messages,
86
+ messages=None,
68
87
  message_handler=None,
69
88
  spinner=False,
70
89
  max_tokens=None,
71
- max_rounds=50,
72
- stream=False,
90
+ max_rounds=100,
91
+ tool_user=False,
73
92
  ):
74
93
  """
75
94
  Start a chat conversation with the agent.
76
95
 
77
96
  Args:
78
- messages: List of message dicts.
79
- message_handler: Optional handler for streaming or event messages.
97
+ messages: LLMConversationHistory instance or None.
98
+ message_handler: Optional handler for event messages.
80
99
  spinner: Show spinner during request.
81
100
  max_tokens: Max tokens for completion.
82
101
  max_rounds: Max conversation rounds.
83
- stream: If True, enable OpenAI streaming mode (yields tokens incrementally).
84
102
  Returns:
85
- If stream=False: dict with 'content', 'usage', and 'usage_history'.
86
- If stream=True: generator yielding content chunks or events.
103
+ dict with 'content', 'usage', and 'usage_history'.
87
104
  """
88
105
  from janito.agent.runtime_config import runtime_config
89
106
 
107
+ if messages is None:
108
+ messages = LLMConversationHistory()
109
+ elif not isinstance(messages, LLMConversationHistory):
110
+ raise TypeError(
111
+ "Agent.chat expects a LLMConversationHistory instance or None."
112
+ )
113
+
90
114
  max_retries = 5
91
115
  for attempt in range(1, max_retries + 1):
92
116
  try:
@@ -98,8 +122,7 @@ class Agent:
98
122
  spinner=spinner,
99
123
  max_tokens=max_tokens,
100
124
  verbose_events=runtime_config.get("verbose_events", False),
101
- stream=stream,
102
- verbose_stream=runtime_config.get("verbose_stream", False),
125
+ tool_user=tool_user,
103
126
  )
104
127
  except ProviderError as e:
105
128
  error_data = getattr(e, "error_data", {}) or {}