janito 1.5.2__py3-none-any.whl → 1.6.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/__main__.py +0 -1
- janito/agent/config.py +11 -10
- janito/agent/config_defaults.py +3 -2
- janito/agent/conversation.py +93 -119
- janito/agent/conversation_api.py +98 -0
- janito/agent/conversation_exceptions.py +12 -0
- janito/agent/conversation_tool_calls.py +22 -0
- janito/agent/conversation_ui.py +17 -0
- janito/agent/message_handler.py +8 -9
- janito/agent/{agent.py → openai_client.py} +48 -16
- janito/agent/openai_schema_generator.py +53 -37
- janito/agent/profile_manager.py +172 -0
- janito/agent/queued_message_handler.py +13 -14
- janito/agent/rich_live.py +32 -0
- janito/agent/rich_message_handler.py +64 -0
- janito/agent/runtime_config.py +6 -1
- janito/agent/{tools/tool_base.py → tool_base.py} +15 -8
- janito/agent/tool_registry.py +118 -132
- janito/agent/tools/__init__.py +41 -2
- janito/agent/tools/ask_user.py +43 -33
- janito/agent/tools/create_directory.py +18 -16
- janito/agent/tools/create_file.py +31 -36
- janito/agent/tools/fetch_url.py +23 -19
- janito/agent/tools/find_files.py +40 -36
- janito/agent/tools/get_file_outline.py +100 -22
- janito/agent/tools/get_lines.py +40 -32
- janito/agent/tools/gitignore_utils.py +9 -6
- janito/agent/tools/move_file.py +22 -13
- janito/agent/tools/py_compile_file.py +40 -0
- janito/agent/tools/remove_directory.py +34 -24
- janito/agent/tools/remove_file.py +22 -20
- janito/agent/tools/replace_file.py +51 -0
- janito/agent/tools/replace_text_in_file.py +69 -42
- janito/agent/tools/rich_live.py +9 -2
- janito/agent/tools/run_bash_command.py +155 -107
- janito/agent/tools/run_python_command.py +139 -0
- janito/agent/tools/search_files.py +51 -34
- janito/agent/tools/tools_utils.py +4 -2
- janito/agent/tools/utils.py +6 -2
- janito/cli/_print_config.py +42 -16
- janito/cli/_utils.py +1 -0
- janito/cli/arg_parser.py +182 -29
- janito/cli/config_commands.py +54 -22
- janito/cli/logging_setup.py +9 -3
- janito/cli/main.py +11 -10
- janito/cli/runner/__init__.py +2 -0
- janito/cli/runner/cli_main.py +148 -0
- janito/cli/runner/config.py +33 -0
- janito/cli/runner/formatting.py +12 -0
- janito/cli/runner/scan.py +44 -0
- janito/cli_chat_shell/__init__.py +0 -1
- janito/cli_chat_shell/chat_loop.py +71 -92
- janito/cli_chat_shell/chat_state.py +38 -0
- janito/cli_chat_shell/chat_ui.py +43 -0
- janito/cli_chat_shell/commands/__init__.py +45 -0
- janito/cli_chat_shell/commands/config.py +22 -0
- janito/cli_chat_shell/commands/history_reset.py +29 -0
- janito/cli_chat_shell/commands/session.py +48 -0
- janito/cli_chat_shell/commands/session_control.py +12 -0
- janito/cli_chat_shell/commands/system.py +73 -0
- janito/cli_chat_shell/commands/utility.py +29 -0
- janito/cli_chat_shell/config_shell.py +39 -10
- janito/cli_chat_shell/load_prompt.py +5 -2
- janito/cli_chat_shell/session_manager.py +24 -27
- janito/cli_chat_shell/ui.py +75 -40
- janito/rich_utils.py +15 -2
- janito/web/__main__.py +10 -2
- janito/web/app.py +88 -52
- {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/METADATA +76 -11
- janito-1.6.0.dist-info/RECORD +81 -0
- {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/WHEEL +1 -1
- janito/agent/rich_tool_handler.py +0 -43
- janito/agent/templates/system_instructions.j2 +0 -38
- janito/agent/tool_auto_imports.py +0 -5
- janito/agent/tools/append_text_to_file.py +0 -41
- janito/agent/tools/py_compile.py +0 -39
- janito/agent/tools/python_exec.py +0 -83
- janito/cli/runner.py +0 -137
- janito/cli_chat_shell/commands.py +0 -204
- janito/render_prompt.py +0 -13
- janito-1.5.2.dist-info/RECORD +0 -66
- {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/entry_points.txt +0 -0
- {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/licenses/LICENSE +0 -0
- {janito-1.5.2.dist-info → janito-1.6.0.dist-info}/top_level.txt +0 -0
janito/__init__.py
CHANGED
@@ -1 +1 @@
|
|
1
|
-
__version__ = "1.
|
1
|
+
__version__ = "1.6.0"
|
janito/__main__.py
CHANGED
janito/agent/config.py
CHANGED
@@ -30,8 +30,6 @@ class BaseConfig:
|
|
30
30
|
return self._data
|
31
31
|
|
32
32
|
|
33
|
-
|
34
|
-
|
35
33
|
class FileConfig(BaseConfig):
|
36
34
|
def __init__(self, path):
|
37
35
|
super().__init__()
|
@@ -40,36 +38,37 @@ class FileConfig(BaseConfig):
|
|
40
38
|
|
41
39
|
def load(self):
|
42
40
|
if self.path.exists():
|
43
|
-
with open(self.path,
|
41
|
+
with open(self.path, "r") as f:
|
44
42
|
self._data = json.load(f)
|
45
43
|
# Remove keys with value None (null in JSON)
|
46
44
|
self._data = {k: v for k, v in self._data.items() if v is not None}
|
47
45
|
|
48
|
-
|
49
46
|
else:
|
50
47
|
self._data = {}
|
51
48
|
|
52
49
|
def save(self):
|
53
50
|
self.path.parent.mkdir(parents=True, exist_ok=True)
|
54
|
-
with open(self.path,
|
51
|
+
with open(self.path, "w") as f:
|
55
52
|
json.dump(self._data, f, indent=2)
|
56
53
|
|
57
54
|
|
58
|
-
|
59
55
|
CONFIG_OPTIONS = {
|
60
56
|
"api_key": "API key for OpenAI-compatible service (required)",
|
57
|
+
"trust": "Trust mode: suppress all console output (bool, default: False)",
|
61
58
|
"model": "Model name to use (e.g., 'openai/gpt-4.1')",
|
62
59
|
"base_url": "API base URL (OpenAI-compatible endpoint)",
|
63
60
|
"role": "Role description for the Agent Profile (e.g., 'software engineer')",
|
64
|
-
"
|
61
|
+
"system_prompt_template": "Override the entire Agent Profile prompt text",
|
65
62
|
"temperature": "Sampling temperature (float, e.g., 0.0 - 2.0)",
|
66
63
|
"max_tokens": "Maximum tokens for model response (int)",
|
67
64
|
"use_azure_openai": "Whether to use Azure OpenAI client (default: False)",
|
68
65
|
# Accept template.* keys as valid config keys (for CLI validation, etc.)
|
69
66
|
"template": "Template context dictionary for Agent Profile prompt rendering (nested)",
|
67
|
+
"interaction_style": "Interaction style for the Agent Profile (e.g., 'default' or 'technical')",
|
70
68
|
# Note: template.* keys are validated dynamically, not statically here
|
71
69
|
}
|
72
70
|
|
71
|
+
|
73
72
|
class BaseConfig:
|
74
73
|
def __init__(self):
|
75
74
|
self._data = {}
|
@@ -83,7 +82,6 @@ class BaseConfig:
|
|
83
82
|
def all(self):
|
84
83
|
return self._data
|
85
84
|
|
86
|
-
|
87
85
|
"""
|
88
86
|
Returns a dictionary suitable for passing as Jinja2 template variables.
|
89
87
|
Merges the nested 'template' dict (if present) and all flat 'template.*' keys.
|
@@ -98,8 +96,10 @@ class BaseConfig:
|
|
98
96
|
|
99
97
|
# Import defaults for reference
|
100
98
|
|
99
|
+
|
101
100
|
class EffectiveConfig:
|
102
101
|
"""Read-only merged view of local and global configs"""
|
102
|
+
|
103
103
|
def __init__(self, local_cfg, global_cfg):
|
104
104
|
self.local_cfg = local_cfg
|
105
105
|
self.global_cfg = global_cfg
|
@@ -127,11 +127,12 @@ class EffectiveConfig:
|
|
127
127
|
|
128
128
|
# Singleton instances
|
129
129
|
|
130
|
-
local_config = FileConfig(Path(
|
131
|
-
global_config = FileConfig(Path.home() /
|
130
|
+
local_config = FileConfig(Path(".janito/config.json"))
|
131
|
+
global_config = FileConfig(Path.home() / ".janito/config.json")
|
132
132
|
|
133
133
|
effective_config = EffectiveConfig(local_config, global_config)
|
134
134
|
|
135
|
+
|
135
136
|
def get_api_key():
|
136
137
|
"""Retrieve API key from config files (local, then global)."""
|
137
138
|
api_key = effective_config.get("api_key")
|
janito/agent/config_defaults.py
CHANGED
@@ -1,11 +1,12 @@
|
|
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",
|
4
|
+
"model": "openai/gpt-4.1", # Default model
|
5
5
|
"base_url": "https://openrouter.ai/api/v1",
|
6
6
|
"role": "software engineer", # Part of the Agent Profile
|
7
|
-
"
|
7
|
+
"system_prompt_template": None, # None means auto-generate from Agent Profile role
|
8
8
|
"temperature": 0.2,
|
9
9
|
"max_tokens": 200000,
|
10
10
|
"use_azure_openai": False,
|
11
|
+
"interaction_style": "default",
|
11
12
|
}
|
janito/agent/conversation.py
CHANGED
@@ -1,18 +1,17 @@
|
|
1
|
-
from janito.agent.
|
2
|
-
|
3
|
-
|
1
|
+
from janito.agent.conversation_api import (
|
2
|
+
get_openai_response,
|
3
|
+
get_openai_stream_response,
|
4
|
+
retry_api_call,
|
5
|
+
)
|
6
|
+
from janito.agent.conversation_tool_calls import handle_tool_calls
|
7
|
+
from janito.agent.conversation_ui import show_spinner, print_verbose_event
|
8
|
+
from janito.agent.conversation_exceptions import (
|
9
|
+
MaxRoundsExceededError,
|
10
|
+
EmptyResponseError,
|
11
|
+
)
|
12
|
+
from janito.agent.runtime_config import unified_config
|
4
13
|
import pprint
|
5
14
|
|
6
|
-
class MaxRoundsExceededError(Exception):
|
7
|
-
pass
|
8
|
-
|
9
|
-
class EmptyResponseError(Exception):
|
10
|
-
pass
|
11
|
-
|
12
|
-
class ProviderError(Exception):
|
13
|
-
def __init__(self, message, error_data):
|
14
|
-
self.error_data = error_data
|
15
|
-
super().__init__(message)
|
16
15
|
|
17
16
|
class ConversationHandler:
|
18
17
|
def __init__(self, client, model):
|
@@ -20,136 +19,111 @@ class ConversationHandler:
|
|
20
19
|
self.model = model
|
21
20
|
self.usage_history = []
|
22
21
|
|
23
|
-
def handle_conversation(
|
24
|
-
|
25
|
-
|
22
|
+
def handle_conversation(
|
23
|
+
self,
|
24
|
+
messages,
|
25
|
+
max_rounds=50,
|
26
|
+
message_handler=None,
|
27
|
+
verbose_response=False,
|
28
|
+
spinner=False,
|
29
|
+
max_tokens=None,
|
30
|
+
verbose_events=False,
|
31
|
+
stream=False,
|
32
|
+
verbose_stream=False,
|
33
|
+
):
|
26
34
|
if not messages:
|
27
35
|
raise ValueError("No prompt provided in messages")
|
28
36
|
|
29
|
-
# Resolve max_tokens priority: runtime param > config > default
|
30
37
|
resolved_max_tokens = max_tokens
|
31
38
|
if resolved_max_tokens is None:
|
32
|
-
resolved_max_tokens = unified_config.get(
|
33
|
-
|
34
|
-
# Ensure max_tokens is always an int (handles config/CLI string values)
|
39
|
+
resolved_max_tokens = unified_config.get("max_tokens", 200000)
|
35
40
|
try:
|
36
41
|
resolved_max_tokens = int(resolved_max_tokens)
|
37
42
|
except (TypeError, ValueError):
|
38
|
-
raise ValueError(
|
43
|
+
raise ValueError(
|
44
|
+
f"max_tokens must be an integer, got: {resolved_max_tokens!r}"
|
45
|
+
)
|
39
46
|
|
40
47
|
for _ in range(max_rounds):
|
41
|
-
if
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
# Tool uses: count tool_calls in all agent messages
|
56
|
-
tool_uses = sum(len(m.get('tool_calls', [])) for m in messages if m.get('role') == 'assistant')
|
57
|
-
# Tool responses: tool_msgs
|
58
|
-
spinner_msg = (
|
59
|
-
f"[bold green]Waiting for AI response... ("
|
60
|
-
f"{format_count(word_count)} words, "
|
61
|
-
f"{user_msgs} user, {agent_msgs} agent, "
|
62
|
-
f"{tool_uses} tool uses, {tool_msgs} tool responses)"
|
63
|
-
)
|
64
|
-
with console.status(spinner_msg, spinner="dots") as status:
|
65
|
-
if runtime_config.get('vanilla_mode', False):
|
66
|
-
response = self.client.chat.completions.create(
|
67
|
-
model=self.model,
|
68
|
-
messages=messages,
|
69
|
-
max_tokens=resolved_max_tokens
|
70
|
-
)
|
71
|
-
else:
|
72
|
-
response = self.client.chat.completions.create(
|
73
|
-
model=self.model,
|
74
|
-
messages=messages,
|
75
|
-
tools=get_tool_schemas(),
|
76
|
-
tool_choice="auto",
|
77
|
-
temperature=0.2,
|
78
|
-
max_tokens=resolved_max_tokens
|
79
|
-
)
|
80
|
-
status.stop()
|
48
|
+
if stream:
|
49
|
+
# Streaming mode
|
50
|
+
def get_stream():
|
51
|
+
return get_openai_stream_response(
|
52
|
+
self.client,
|
53
|
+
self.model,
|
54
|
+
messages,
|
55
|
+
resolved_max_tokens,
|
56
|
+
verbose_stream=verbose_stream,
|
57
|
+
message_handler=message_handler,
|
58
|
+
)
|
59
|
+
|
60
|
+
retry_api_call(get_stream)
|
61
|
+
return None
|
81
62
|
else:
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
messages
|
86
|
-
max_tokens=resolved_max_tokens
|
63
|
+
# Non-streaming mode
|
64
|
+
def api_call():
|
65
|
+
return get_openai_response(
|
66
|
+
self.client, self.model, messages, resolved_max_tokens
|
87
67
|
)
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
tools=get_tool_schemas(),
|
93
|
-
tool_choice="auto",
|
94
|
-
temperature=0.2,
|
95
|
-
max_tokens=resolved_max_tokens
|
68
|
+
|
69
|
+
if spinner:
|
70
|
+
response = show_spinner(
|
71
|
+
"Waiting for AI response...", retry_api_call, api_call
|
96
72
|
)
|
73
|
+
else:
|
74
|
+
response = retry_api_call(api_call)
|
97
75
|
|
98
76
|
if verbose_response:
|
99
77
|
pprint.pprint(response)
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
error_code = response.error.get('code', 'unknown')
|
105
|
-
raise ProviderError(f"Provider error: {error_msg} (Code: {error_code})", response.error)
|
106
|
-
|
107
|
-
if not response.choices:
|
108
|
-
raise EmptyResponseError("The LLM API returned no choices in the response.")
|
109
|
-
|
78
|
+
if response is None or not getattr(response, "choices", None):
|
79
|
+
raise EmptyResponseError(
|
80
|
+
f"No choices in response; possible API or LLM error. Raw response: {response!r}"
|
81
|
+
)
|
110
82
|
choice = response.choices[0]
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
'completion_tokens': getattr(usage, 'completion_tokens', None),
|
118
|
-
'total_tokens': getattr(usage, 'total_tokens', None)
|
83
|
+
usage = getattr(response, "usage", None)
|
84
|
+
usage_info = (
|
85
|
+
{
|
86
|
+
"prompt_tokens": getattr(usage, "prompt_tokens", None),
|
87
|
+
"completion_tokens": getattr(usage, "completion_tokens", None),
|
88
|
+
"total_tokens": getattr(usage, "total_tokens", None),
|
119
89
|
}
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
90
|
+
if usage
|
91
|
+
else None
|
92
|
+
)
|
93
|
+
event = {"type": "content", "message": choice.message.content}
|
94
|
+
if verbose_events:
|
95
|
+
print_verbose_event(event)
|
124
96
|
if message_handler is not None and choice.message.content:
|
125
|
-
message_handler.handle_message(
|
126
|
-
|
127
|
-
# If no tool calls, return the agent's message and usage info
|
97
|
+
message_handler.handle_message(event)
|
128
98
|
if not choice.message.tool_calls:
|
129
|
-
|
130
|
-
|
131
|
-
|
99
|
+
agent_idx = len([m for m in messages if m.get("role") == "agent"])
|
100
|
+
self.usage_history.append(
|
101
|
+
{"agent_index": agent_idx, "usage": usage_info}
|
102
|
+
)
|
132
103
|
return {
|
133
104
|
"content": choice.message.content,
|
134
105
|
"usage": usage_info,
|
135
|
-
"usage_history": self.usage_history
|
106
|
+
"usage_history": self.usage_history,
|
136
107
|
}
|
137
|
-
|
138
|
-
tool_responses =
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
raise MaxRoundsExceededError(f"Maximum number of tool calls ({max_tools}) reached in this chat session.")
|
143
|
-
result = handle_tool_call(tool_call, message_handler=message_handler)
|
144
|
-
tool_responses.append({"tool_call_id": tool_call.id, "content": result})
|
145
|
-
tool_calls_made += 1
|
146
|
-
|
147
|
-
# Store usage info in usage_history, linked to the next agent message index
|
148
|
-
agent_idx = len([m for m in messages if m.get('role') == 'agent'])
|
108
|
+
# Tool calls
|
109
|
+
tool_responses = handle_tool_calls(
|
110
|
+
choice.message.tool_calls, message_handler=message_handler
|
111
|
+
)
|
112
|
+
agent_idx = len([m for m in messages if m.get("role") == "agent"])
|
149
113
|
self.usage_history.append({"agent_index": agent_idx, "usage": usage_info})
|
150
|
-
messages.append(
|
151
|
-
|
114
|
+
messages.append(
|
115
|
+
{
|
116
|
+
"role": "assistant",
|
117
|
+
"content": choice.message.content,
|
118
|
+
"tool_calls": [tc.to_dict() for tc in choice.message.tool_calls],
|
119
|
+
}
|
120
|
+
)
|
152
121
|
for tr in tool_responses:
|
153
|
-
messages.append(
|
154
|
-
|
122
|
+
messages.append(
|
123
|
+
{
|
124
|
+
"role": "tool",
|
125
|
+
"tool_call_id": tr["tool_call_id"],
|
126
|
+
"content": tr["content"],
|
127
|
+
}
|
128
|
+
)
|
155
129
|
raise MaxRoundsExceededError("Max conversation rounds exceeded")
|
@@ -0,0 +1,98 @@
|
|
1
|
+
"""
|
2
|
+
Handles OpenAI API calls and retry logic for conversation.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import time
|
6
|
+
import json
|
7
|
+
from janito.agent.runtime_config import runtime_config
|
8
|
+
from janito.agent.tool_registry import get_tool_schemas
|
9
|
+
|
10
|
+
|
11
|
+
def get_openai_response(
|
12
|
+
client, model, messages, max_tokens, tools=None, tool_choice=None, temperature=None
|
13
|
+
):
|
14
|
+
"""Non-streaming OpenAI API call."""
|
15
|
+
if runtime_config.get("vanilla_mode", False):
|
16
|
+
return client.chat.completions.create(
|
17
|
+
model=model,
|
18
|
+
messages=messages,
|
19
|
+
max_tokens=max_tokens,
|
20
|
+
)
|
21
|
+
else:
|
22
|
+
return client.chat.completions.create(
|
23
|
+
model=model,
|
24
|
+
messages=messages,
|
25
|
+
tools=tools or get_tool_schemas(),
|
26
|
+
tool_choice=tool_choice or "auto",
|
27
|
+
temperature=temperature if temperature is not None else 0.2,
|
28
|
+
max_tokens=max_tokens,
|
29
|
+
)
|
30
|
+
|
31
|
+
|
32
|
+
def get_openai_stream_response(
|
33
|
+
client,
|
34
|
+
model,
|
35
|
+
messages,
|
36
|
+
max_tokens,
|
37
|
+
tools=None,
|
38
|
+
tool_choice=None,
|
39
|
+
temperature=None,
|
40
|
+
verbose_stream=False,
|
41
|
+
message_handler=None,
|
42
|
+
):
|
43
|
+
"""Streaming OpenAI API call."""
|
44
|
+
openai_args = dict(
|
45
|
+
model=model,
|
46
|
+
messages=messages,
|
47
|
+
max_tokens=max_tokens,
|
48
|
+
stream=True,
|
49
|
+
)
|
50
|
+
if not runtime_config.get("vanilla_mode", False):
|
51
|
+
openai_args.update(
|
52
|
+
tools=tools or get_tool_schemas(),
|
53
|
+
tool_choice=tool_choice or "auto",
|
54
|
+
temperature=temperature if temperature is not None else 0.2,
|
55
|
+
)
|
56
|
+
response_stream = client.chat.completions.create(**openai_args)
|
57
|
+
content_accum = ""
|
58
|
+
for event in response_stream:
|
59
|
+
if verbose_stream or runtime_config.get("verbose_stream", False):
|
60
|
+
print(repr(event), flush=True)
|
61
|
+
delta = getattr(event.choices[0], "delta", None)
|
62
|
+
if delta and getattr(delta, "content", None):
|
63
|
+
chunk = delta.content
|
64
|
+
content_accum += chunk
|
65
|
+
if message_handler:
|
66
|
+
message_handler.handle_message({"type": "stream", "content": chunk})
|
67
|
+
if message_handler:
|
68
|
+
message_handler.handle_message({"type": "stream_end", "content": content_accum})
|
69
|
+
return None
|
70
|
+
|
71
|
+
|
72
|
+
def retry_api_call(api_func, max_retries=5, *args, **kwargs):
|
73
|
+
last_exception = None
|
74
|
+
for attempt in range(1, max_retries + 1):
|
75
|
+
try:
|
76
|
+
return api_func(*args, **kwargs)
|
77
|
+
except json.JSONDecodeError as e:
|
78
|
+
last_exception = e
|
79
|
+
if attempt < max_retries:
|
80
|
+
wait_time = 2**attempt
|
81
|
+
print(
|
82
|
+
f"Invalid/malformed response from OpenAI (attempt {attempt}/{max_retries}). Retrying in {wait_time} seconds..."
|
83
|
+
)
|
84
|
+
time.sleep(wait_time)
|
85
|
+
else:
|
86
|
+
print("Max retries for invalid response reached. Raising error.")
|
87
|
+
raise last_exception
|
88
|
+
except Exception as e:
|
89
|
+
last_exception = e
|
90
|
+
if attempt < max_retries:
|
91
|
+
wait_time = 2**attempt
|
92
|
+
print(
|
93
|
+
f"OpenAI API error (attempt {attempt}/{max_retries}): {e}. Retrying in {wait_time} seconds..."
|
94
|
+
)
|
95
|
+
time.sleep(wait_time)
|
96
|
+
else:
|
97
|
+
print("Max retries for OpenAI API error reached. Raising error.")
|
98
|
+
raise last_exception
|
@@ -0,0 +1,22 @@
|
|
1
|
+
"""
|
2
|
+
Helpers for handling tool calls in conversation.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from janito.agent.tool_registry import handle_tool_call
|
6
|
+
from .conversation_exceptions import MaxRoundsExceededError
|
7
|
+
from janito.agent.runtime_config import runtime_config
|
8
|
+
|
9
|
+
|
10
|
+
def handle_tool_calls(tool_calls, message_handler=None):
|
11
|
+
max_tools = runtime_config.get("max_tools", None)
|
12
|
+
tool_calls_made = 0
|
13
|
+
tool_responses = []
|
14
|
+
for tool_call in tool_calls:
|
15
|
+
if max_tools is not None and tool_calls_made >= max_tools:
|
16
|
+
raise MaxRoundsExceededError(
|
17
|
+
f"Maximum number of tool calls ({max_tools}) reached in this chat session."
|
18
|
+
)
|
19
|
+
result = handle_tool_call(tool_call, message_handler=message_handler)
|
20
|
+
tool_responses.append({"tool_call_id": tool_call.id, "content": result})
|
21
|
+
tool_calls_made += 1
|
22
|
+
return tool_responses
|
@@ -0,0 +1,17 @@
|
|
1
|
+
"""
|
2
|
+
UI helpers for conversation (spinner, verbose output).
|
3
|
+
"""
|
4
|
+
|
5
|
+
from rich.console import Console
|
6
|
+
|
7
|
+
|
8
|
+
def show_spinner(message, func, *args, **kwargs):
|
9
|
+
console = Console()
|
10
|
+
with console.status(message, spinner="dots") as status:
|
11
|
+
result = func(*args, **kwargs)
|
12
|
+
status.stop()
|
13
|
+
return result
|
14
|
+
|
15
|
+
|
16
|
+
def print_verbose_event(event):
|
17
|
+
print(f"[EVENT] {event}")
|
janito/agent/message_handler.py
CHANGED
@@ -1,5 +1,4 @@
|
|
1
|
-
|
2
|
-
class MessageHandler:
|
1
|
+
class QueueMessageHandler:
|
3
2
|
def __init__(self, queue, *args, **kwargs):
|
4
3
|
self._queue = queue
|
5
4
|
|
@@ -9,10 +8,10 @@ class MessageHandler:
|
|
9
8
|
|
10
9
|
def handle_message(self, msg, msg_type=None):
|
11
10
|
# Unified: send content (agent/LLM) messages to the frontend
|
12
|
-
if isinstance(msg, dict):
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
self._queue.put((
|
11
|
+
if not isinstance(msg, dict):
|
12
|
+
raise TypeError(
|
13
|
+
f"QueueMessageHandler.handle_message expects a dict with 'type' and 'message', got {type(msg)}: {msg!r}"
|
14
|
+
)
|
15
|
+
msg_type = msg.get("type", "info")
|
16
|
+
message = msg.get("message", "")
|
17
|
+
self._queue.put(("message", message, msg_type))
|