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.
- janito/__init__.py +1 -1
- janito/agent/api_exceptions.py +4 -0
- janito/agent/config.py +1 -1
- janito/agent/config_defaults.py +2 -3
- janito/agent/config_utils.py +0 -9
- janito/agent/conversation.py +177 -114
- janito/agent/conversation_api.py +179 -159
- janito/agent/conversation_tool_calls.py +11 -8
- janito/agent/llm_conversation_history.py +70 -0
- janito/agent/openai_client.py +44 -21
- janito/agent/openai_schema_generator.py +164 -128
- janito/agent/platform_discovery.py +134 -77
- janito/agent/profile_manager.py +5 -5
- janito/agent/rich_message_handler.py +80 -31
- janito/agent/templates/profiles/system_prompt_template_base.txt.j2 +9 -8
- janito/agent/test_openai_schema_generator.py +93 -0
- janito/agent/tool_base.py +7 -2
- janito/agent/tool_executor.py +63 -50
- janito/agent/tool_registry.py +5 -2
- janito/agent/tool_use_tracker.py +42 -5
- janito/agent/tools/__init__.py +13 -12
- janito/agent/tools/create_directory.py +9 -6
- janito/agent/tools/create_file.py +35 -54
- janito/agent/tools/delete_text_in_file.py +97 -0
- janito/agent/tools/fetch_url.py +50 -5
- janito/agent/tools/find_files.py +40 -26
- janito/agent/tools/get_file_outline/__init__.py +1 -0
- janito/agent/tools/{outline_file/__init__.py → get_file_outline/core.py} +14 -18
- janito/agent/tools/get_file_outline/python_outline.py +134 -0
- janito/agent/tools/{search_outline.py → get_file_outline/search_outline.py} +11 -0
- janito/agent/tools/get_lines.py +21 -12
- janito/agent/tools/move_file.py +13 -12
- janito/agent/tools/present_choices.py +3 -1
- janito/agent/tools/python_command_runner.py +150 -0
- janito/agent/tools/python_file_runner.py +148 -0
- janito/agent/tools/python_stdin_runner.py +154 -0
- janito/agent/tools/remove_directory.py +4 -2
- janito/agent/tools/remove_file.py +15 -13
- janito/agent/tools/replace_file.py +72 -0
- janito/agent/tools/replace_text_in_file.py +7 -5
- janito/agent/tools/run_bash_command.py +29 -72
- janito/agent/tools/run_powershell_command.py +142 -102
- janito/agent/tools/search_text.py +177 -131
- janito/agent/tools/validate_file_syntax/__init__.py +1 -0
- janito/agent/tools/validate_file_syntax/core.py +94 -0
- janito/agent/tools/validate_file_syntax/css_validator.py +35 -0
- janito/agent/tools/validate_file_syntax/html_validator.py +77 -0
- janito/agent/tools/validate_file_syntax/js_validator.py +27 -0
- janito/agent/tools/validate_file_syntax/json_validator.py +6 -0
- janito/agent/tools/validate_file_syntax/markdown_validator.py +66 -0
- janito/agent/tools/validate_file_syntax/ps1_validator.py +32 -0
- janito/agent/tools/validate_file_syntax/python_validator.py +5 -0
- janito/agent/tools/validate_file_syntax/xml_validator.py +11 -0
- janito/agent/tools/validate_file_syntax/yaml_validator.py +6 -0
- janito/agent/tools_utils/__init__.py +1 -0
- janito/agent/tools_utils/action_type.py +7 -0
- janito/agent/tools_utils/dir_walk_utils.py +24 -0
- janito/agent/tools_utils/formatting.py +49 -0
- janito/agent/tools_utils/gitignore_utils.py +69 -0
- janito/agent/tools_utils/test_gitignore_utils.py +46 -0
- janito/agent/tools_utils/utils.py +30 -0
- janito/cli/_livereload_log_utils.py +13 -0
- janito/cli/_print_config.py +63 -61
- janito/cli/arg_parser.py +57 -14
- janito/cli/cli_main.py +270 -0
- janito/cli/livereload_starter.py +60 -0
- janito/cli/main.py +166 -99
- janito/cli/one_shot.py +80 -0
- janito/cli/termweb_starter.py +2 -2
- janito/i18n/__init__.py +1 -1
- janito/livereload/app.py +25 -0
- janito/rich_utils.py +41 -25
- janito/{cli_chat_shell → shell}/commands/__init__.py +19 -14
- janito/{cli_chat_shell → shell}/commands/config.py +4 -4
- janito/shell/commands/conversation_restart.py +74 -0
- janito/shell/commands/edit.py +24 -0
- janito/shell/commands/history_view.py +18 -0
- janito/{cli_chat_shell → shell}/commands/lang.py +3 -0
- janito/shell/commands/livelogs.py +42 -0
- janito/{cli_chat_shell → shell}/commands/prompt.py +16 -6
- janito/shell/commands/session.py +35 -0
- janito/{cli_chat_shell → shell}/commands/session_control.py +3 -5
- janito/{cli_chat_shell → shell}/commands/termweb_log.py +18 -10
- janito/shell/commands/tools.py +26 -0
- janito/shell/commands/track.py +36 -0
- janito/shell/commands/utility.py +28 -0
- janito/{cli_chat_shell → shell}/commands/verbose.py +4 -5
- janito/shell/commands.py +40 -0
- janito/shell/input_history.py +62 -0
- janito/shell/main.py +257 -0
- janito/{cli_chat_shell/shell_command_completer.py → shell/prompt/completer.py} +1 -1
- janito/{cli_chat_shell/chat_ui.py → shell/prompt/session_setup.py} +19 -5
- janito/shell/session/manager.py +101 -0
- janito/{cli_chat_shell/ui.py → shell/ui/interactive.py} +23 -17
- janito/termweb/app.py +3 -3
- janito/termweb/static/editor.css +142 -0
- janito/termweb/static/editor.css.bak +27 -0
- janito/termweb/static/editor.html +15 -213
- janito/termweb/static/editor.html.bak +16 -215
- janito/termweb/static/editor.js +209 -0
- janito/termweb/static/editor.js.bak +227 -0
- janito/termweb/static/index.html +2 -3
- janito/termweb/static/index.html.bak +2 -3
- janito/termweb/static/termweb.css.bak +33 -84
- janito/termweb/static/termweb.js +15 -34
- janito/termweb/static/termweb.js.bak +18 -36
- janito/tests/test_rich_utils.py +44 -0
- janito/web/app.py +0 -75
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/METADATA +62 -42
- janito-1.10.0.dist-info/RECORD +158 -0
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/WHEEL +1 -1
- janito/agent/tools/dir_walk_utils.py +0 -16
- janito/agent/tools/gitignore_utils.py +0 -46
- janito/agent/tools/memory.py +0 -48
- janito/agent/tools/outline_file/formatting.py +0 -20
- janito/agent/tools/outline_file/python_outline.py +0 -71
- janito/agent/tools/present_choices_test.py +0 -18
- janito/agent/tools/rich_live.py +0 -44
- janito/agent/tools/run_python_command.py +0 -163
- janito/agent/tools/tools_utils.py +0 -56
- janito/agent/tools/utils.py +0 -33
- janito/agent/tools/validate_file_syntax.py +0 -163
- janito/cli/runner/cli_main.py +0 -180
- janito/cli_chat_shell/chat_loop.py +0 -163
- janito/cli_chat_shell/chat_state.py +0 -38
- janito/cli_chat_shell/commands/history_start.py +0 -37
- janito/cli_chat_shell/commands/session.py +0 -48
- janito/cli_chat_shell/commands/sum.py +0 -49
- janito/cli_chat_shell/commands/utility.py +0 -32
- janito/cli_chat_shell/session_manager.py +0 -72
- janito-1.8.1.dist-info/RECORD +0 -127
- /janito/agent/tools/{outline_file → get_file_outline}/markdown_outline.py +0 -0
- /janito/cli/{runner/_termweb_log_utils.py → _termweb_log_utils.py} +0 -0
- /janito/cli/{runner/config.py → config_runner.py} +0 -0
- /janito/cli/{runner/formatting.py → formatting_runner.py} +0 -0
- /janito/{cli/runner → shell}/__init__.py +0 -0
- /janito/{cli_chat_shell → shell/prompt}/load_prompt.py +0 -0
- /janito/{cli_chat_shell/config_shell.py → shell/session/config.py} +0 -0
- /janito/{cli_chat_shell/__init__.py → shell/session/history.py} +0 -0
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/entry_points.txt +0 -0
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/licenses/LICENSE +0 -0
- {janito-1.8.1.dist-info → janito-1.10.0.dist-info}/top_level.txt +0 -0
janito/agent/conversation_api.py
CHANGED
@@ -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
|
-
"""
|
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
|
-
|
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
|
-
|
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
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
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
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
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
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
87
|
-
|
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
|
-
|
146
|
+
return None
|
92
147
|
else:
|
93
|
-
print(
|
94
|
-
raise
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
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
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
status_code
|
119
|
-
|
120
|
-
|
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
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
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
|
-
|
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
|
-
|
207
|
+
history.add_message({"role": "user", "content": user_message_on_empty})
|
208
|
+
continue # Retry with updated history
|
196
209
|
else:
|
197
|
-
|
198
|
-
|
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
|
-
|
23
|
-
|
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":
|
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]
|
janito/agent/openai_client.py
CHANGED
@@ -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 =
|
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
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
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=
|
72
|
-
|
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:
|
79
|
-
message_handler: Optional handler for
|
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
|
-
|
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
|
-
|
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 {}
|