janito 1.5.1__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.
Files changed (85) hide show
  1. janito/__init__.py +1 -1
  2. janito/__main__.py +0 -1
  3. janito/agent/config.py +11 -10
  4. janito/agent/config_defaults.py +3 -2
  5. janito/agent/conversation.py +93 -119
  6. janito/agent/conversation_api.py +98 -0
  7. janito/agent/conversation_exceptions.py +12 -0
  8. janito/agent/conversation_tool_calls.py +22 -0
  9. janito/agent/conversation_ui.py +17 -0
  10. janito/agent/message_handler.py +8 -9
  11. janito/agent/{agent.py → openai_client.py} +48 -16
  12. janito/agent/openai_schema_generator.py +53 -37
  13. janito/agent/profile_manager.py +172 -0
  14. janito/agent/queued_message_handler.py +13 -14
  15. janito/agent/rich_live.py +32 -0
  16. janito/agent/rich_message_handler.py +64 -0
  17. janito/agent/runtime_config.py +6 -1
  18. janito/agent/{tools/tool_base.py → tool_base.py} +15 -8
  19. janito/agent/tool_registry.py +118 -132
  20. janito/agent/tools/__init__.py +41 -2
  21. janito/agent/tools/ask_user.py +43 -33
  22. janito/agent/tools/create_directory.py +18 -16
  23. janito/agent/tools/create_file.py +31 -36
  24. janito/agent/tools/fetch_url.py +23 -19
  25. janito/agent/tools/find_files.py +40 -30
  26. janito/agent/tools/get_file_outline.py +100 -22
  27. janito/agent/tools/get_lines.py +40 -32
  28. janito/agent/tools/gitignore_utils.py +9 -6
  29. janito/agent/tools/move_file.py +22 -13
  30. janito/agent/tools/py_compile_file.py +40 -0
  31. janito/agent/tools/remove_directory.py +34 -24
  32. janito/agent/tools/remove_file.py +22 -20
  33. janito/agent/tools/replace_file.py +51 -0
  34. janito/agent/tools/replace_text_in_file.py +69 -42
  35. janito/agent/tools/rich_live.py +9 -2
  36. janito/agent/tools/run_bash_command.py +155 -107
  37. janito/agent/tools/run_python_command.py +139 -0
  38. janito/agent/tools/search_files.py +51 -25
  39. janito/agent/tools/tools_utils.py +4 -2
  40. janito/agent/tools/utils.py +6 -2
  41. janito/cli/_print_config.py +42 -16
  42. janito/cli/_utils.py +1 -0
  43. janito/cli/arg_parser.py +182 -29
  44. janito/cli/config_commands.py +54 -22
  45. janito/cli/logging_setup.py +9 -3
  46. janito/cli/main.py +11 -10
  47. janito/cli/runner/__init__.py +2 -0
  48. janito/cli/runner/cli_main.py +148 -0
  49. janito/cli/runner/config.py +33 -0
  50. janito/cli/runner/formatting.py +12 -0
  51. janito/cli/runner/scan.py +44 -0
  52. janito/cli_chat_shell/__init__.py +0 -1
  53. janito/cli_chat_shell/chat_loop.py +71 -92
  54. janito/cli_chat_shell/chat_state.py +38 -0
  55. janito/cli_chat_shell/chat_ui.py +43 -0
  56. janito/cli_chat_shell/commands/__init__.py +45 -0
  57. janito/cli_chat_shell/commands/config.py +22 -0
  58. janito/cli_chat_shell/commands/history_reset.py +29 -0
  59. janito/cli_chat_shell/commands/session.py +48 -0
  60. janito/cli_chat_shell/commands/session_control.py +12 -0
  61. janito/cli_chat_shell/commands/system.py +73 -0
  62. janito/cli_chat_shell/commands/utility.py +29 -0
  63. janito/cli_chat_shell/config_shell.py +39 -10
  64. janito/cli_chat_shell/load_prompt.py +5 -2
  65. janito/cli_chat_shell/session_manager.py +24 -27
  66. janito/cli_chat_shell/ui.py +75 -40
  67. janito/rich_utils.py +15 -2
  68. janito/web/__main__.py +10 -2
  69. janito/web/app.py +88 -52
  70. {janito-1.5.1.dist-info → janito-1.6.0.dist-info}/METADATA +76 -11
  71. janito-1.6.0.dist-info/RECORD +81 -0
  72. {janito-1.5.1.dist-info → janito-1.6.0.dist-info}/WHEEL +1 -1
  73. janito/agent/rich_tool_handler.py +0 -43
  74. janito/agent/templates/system_instructions.j2 +0 -38
  75. janito/agent/tool_auto_imports.py +0 -5
  76. janito/agent/tools/append_text_to_file.py +0 -41
  77. janito/agent/tools/py_compile.py +0 -39
  78. janito/agent/tools/python_exec.py +0 -83
  79. janito/cli/runner.py +0 -137
  80. janito/cli_chat_shell/commands.py +0 -204
  81. janito/render_prompt.py +0 -13
  82. janito-1.5.1.dist-info/RECORD +0 -66
  83. {janito-1.5.1.dist-info → janito-1.6.0.dist-info}/entry_points.txt +0 -0
  84. {janito-1.5.1.dist-info → janito-1.6.0.dist-info}/licenses/LICENSE +0 -0
  85. {janito-1.5.1.dist-info → janito-1.6.0.dist-info}/top_level.txt +0 -0
janito/__init__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.5.1"
1
+ __version__ = "1.6.0"
janito/__main__.py CHANGED
@@ -1,5 +1,4 @@
1
1
  from janito.cli.main import main
2
2
 
3
-
4
3
  if __name__ == "__main__":
5
4
  main()
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, 'r') as f:
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, 'w') as f:
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
- "system_prompt": "Override the entire Agent Profile prompt text",
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('.janito/config.json'))
131
- global_config = FileConfig(Path.home() / '.janito/config.json')
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")
@@ -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", # Default model
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
- "system_prompt": None, # None means auto-generate from Agent Profile role
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
  }
@@ -1,18 +1,17 @@
1
- from janito.agent.tool_registry import get_tool_schemas, handle_tool_call
2
- from janito.agent.runtime_config import runtime_config, unified_config
3
- from rich.console import Console
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(self, messages, max_rounds=50, message_handler=None, verbose_response=False, spinner=False, max_tokens=None):
24
- max_tools = runtime_config.get('max_tools', None)
25
- tool_calls_made = 0
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('max_tokens', 200000)
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(f"max_tokens must be an integer, got: {resolved_max_tokens!r}")
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 spinner:
42
- console = Console()
43
- # Calculate word count for all messages
44
- word_count = sum(len(str(m.get('content', '')).split()) for m in messages if 'content' in m)
45
- def format_count(n):
46
- if n >= 1_000_000:
47
- return f"{n/1_000_000:.1f}m"
48
- elif n >= 1_000:
49
- return f"{n/1_000:.1f}k"
50
- return str(n)
51
- # Count message types
52
- user_msgs = sum(1 for m in messages if m.get('role') == 'user')
53
- agent_msgs = sum(1 for m in messages if m.get('role') == 'assistant')
54
- tool_msgs = sum(1 for m in messages if m.get('role') == 'tool')
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
- if runtime_config.get('vanilla_mode', False):
83
- response = self.client.chat.completions.create(
84
- model=self.model,
85
- messages=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
- else:
89
- response = self.client.chat.completions.create(
90
- model=self.model,
91
- messages=messages,
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
- # Check for provider errors
102
- if hasattr(response, 'error') and response.error:
103
- error_msg = response.error.get('message', 'Unknown provider error')
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
- # Extract token usage info if available
113
- usage = getattr(response, 'usage', None)
114
- if usage:
115
- usage_info = {
116
- 'prompt_tokens': getattr(usage, 'prompt_tokens', None),
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
- else:
121
- usage_info = None
122
-
123
- # Route content through the unified message handler if provided
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(choice.message.content, msg_type="content")
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
- # Store usage info in usage_history, linked to the next agent message index
130
- agent_idx = len([m for m in messages if m.get('role') == 'agent'])
131
- self.usage_history.append({"agent_index": agent_idx, "usage": usage_info})
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
- # Sequential tool execution (default, only mode)
140
- for tool_call in choice.message.tool_calls:
141
- if max_tools is not None and tool_calls_made >= max_tools:
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({"role": "assistant", "content": choice.message.content, "tool_calls": [tc.to_dict() for tc in choice.message.tool_calls]})
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({"role": "tool", "tool_call_id": tr["tool_call_id"], "content": tr["content"]})
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,12 @@
1
+ class MaxRoundsExceededError(Exception):
2
+ pass
3
+
4
+
5
+ class EmptyResponseError(Exception):
6
+ pass
7
+
8
+
9
+ class ProviderError(Exception):
10
+ def __init__(self, message, error_data):
11
+ self.error_data = error_data
12
+ super().__init__(message)
@@ -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}")
@@ -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
- msg_type = msg.get('type', 'info')
14
- message = msg.get('message', '')
15
- else:
16
- message = msg
17
- msg_type = msg_type or 'info'
18
- self._queue.put(('message', message, msg_type))
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))