autobyteus 1.1.0__py3-none-any.whl → 1.1.2__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.
- autobyteus/agent/bootstrap_steps/agent_bootstrapper.py +1 -1
- autobyteus/agent/bootstrap_steps/agent_runtime_queue_initialization_step.py +1 -1
- autobyteus/agent/bootstrap_steps/base_bootstrap_step.py +1 -1
- autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +1 -1
- autobyteus/agent/bootstrap_steps/workspace_context_initialization_step.py +1 -1
- autobyteus/agent/context/__init__.py +0 -5
- autobyteus/agent/context/agent_config.py +6 -2
- autobyteus/agent/context/agent_context.py +2 -5
- autobyteus/agent/context/agent_phase_manager.py +105 -5
- autobyteus/agent/context/agent_runtime_state.py +2 -2
- autobyteus/agent/context/phases.py +2 -0
- autobyteus/agent/events/__init__.py +0 -11
- autobyteus/agent/events/agent_events.py +0 -37
- autobyteus/agent/events/notifiers.py +25 -7
- autobyteus/agent/events/worker_event_dispatcher.py +1 -1
- autobyteus/agent/factory/agent_factory.py +6 -2
- autobyteus/agent/group/agent_group.py +16 -7
- autobyteus/agent/handlers/approved_tool_invocation_event_handler.py +28 -14
- autobyteus/agent/handlers/lifecycle_event_logger.py +1 -1
- autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +4 -2
- autobyteus/agent/handlers/tool_invocation_request_event_handler.py +40 -15
- autobyteus/agent/handlers/tool_result_event_handler.py +12 -7
- autobyteus/agent/hooks/__init__.py +7 -0
- autobyteus/agent/hooks/base_phase_hook.py +11 -2
- autobyteus/agent/hooks/hook_definition.py +36 -0
- autobyteus/agent/hooks/hook_meta.py +37 -0
- autobyteus/agent/hooks/hook_registry.py +118 -0
- autobyteus/agent/input_processor/base_user_input_processor.py +6 -3
- autobyteus/agent/input_processor/passthrough_input_processor.py +2 -1
- autobyteus/agent/input_processor/processor_meta.py +1 -1
- autobyteus/agent/input_processor/processor_registry.py +19 -0
- autobyteus/agent/llm_response_processor/base_processor.py +6 -3
- autobyteus/agent/llm_response_processor/processor_meta.py +1 -1
- autobyteus/agent/llm_response_processor/processor_registry.py +19 -0
- autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +2 -1
- autobyteus/agent/message/context_file_type.py +2 -3
- autobyteus/agent/phases/__init__.py +18 -0
- autobyteus/agent/phases/discover.py +52 -0
- autobyteus/agent/phases/manager.py +265 -0
- autobyteus/agent/phases/phase_enum.py +49 -0
- autobyteus/agent/phases/transition_decorator.py +40 -0
- autobyteus/agent/phases/transition_info.py +33 -0
- autobyteus/agent/remote_agent.py +1 -1
- autobyteus/agent/runtime/agent_runtime.py +5 -10
- autobyteus/agent/runtime/agent_worker.py +62 -19
- autobyteus/agent/streaming/agent_event_stream.py +58 -5
- autobyteus/agent/streaming/stream_event_payloads.py +24 -13
- autobyteus/agent/streaming/stream_events.py +14 -11
- autobyteus/agent/system_prompt_processor/base_processor.py +6 -3
- autobyteus/agent/system_prompt_processor/processor_meta.py +1 -1
- autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +45 -31
- autobyteus/agent/tool_invocation.py +29 -3
- autobyteus/agent/utils/wait_for_idle.py +1 -1
- autobyteus/agent/workspace/__init__.py +2 -0
- autobyteus/agent/workspace/base_workspace.py +33 -11
- autobyteus/agent/workspace/workspace_config.py +160 -0
- autobyteus/agent/workspace/workspace_definition.py +36 -0
- autobyteus/agent/workspace/workspace_meta.py +37 -0
- autobyteus/agent/workspace/workspace_registry.py +72 -0
- autobyteus/cli/__init__.py +4 -3
- autobyteus/cli/agent_cli.py +25 -207
- autobyteus/cli/cli_display.py +205 -0
- autobyteus/events/event_manager.py +2 -1
- autobyteus/events/event_types.py +3 -1
- autobyteus/llm/api/autobyteus_llm.py +2 -12
- autobyteus/llm/api/deepseek_llm.py +11 -173
- autobyteus/llm/api/grok_llm.py +11 -172
- autobyteus/llm/api/kimi_llm.py +24 -0
- autobyteus/llm/api/mistral_llm.py +4 -4
- autobyteus/llm/api/ollama_llm.py +2 -2
- autobyteus/llm/api/openai_compatible_llm.py +193 -0
- autobyteus/llm/api/openai_llm.py +11 -139
- autobyteus/llm/extensions/token_usage_tracking_extension.py +11 -1
- autobyteus/llm/llm_factory.py +168 -42
- autobyteus/llm/models.py +25 -29
- autobyteus/llm/ollama_provider.py +6 -2
- autobyteus/llm/ollama_provider_resolver.py +44 -0
- autobyteus/llm/providers.py +1 -0
- autobyteus/llm/token_counter/kimi_token_counter.py +24 -0
- autobyteus/llm/token_counter/token_counter_factory.py +3 -0
- autobyteus/llm/utils/messages.py +3 -3
- autobyteus/tools/__init__.py +2 -0
- autobyteus/tools/base_tool.py +7 -1
- autobyteus/tools/functional_tool.py +20 -5
- autobyteus/tools/mcp/call_handlers/stdio_handler.py +15 -1
- autobyteus/tools/mcp/config_service.py +106 -127
- autobyteus/tools/mcp/registrar.py +247 -59
- autobyteus/tools/mcp/types.py +5 -3
- autobyteus/tools/registry/tool_definition.py +8 -1
- autobyteus/tools/registry/tool_registry.py +18 -0
- autobyteus/tools/tool_category.py +11 -0
- autobyteus/tools/tool_meta.py +3 -1
- autobyteus/tools/tool_state.py +20 -0
- autobyteus/tools/usage/parsers/_json_extractor.py +99 -0
- autobyteus/tools/usage/parsers/default_json_tool_usage_parser.py +46 -77
- autobyteus/tools/usage/parsers/default_xml_tool_usage_parser.py +87 -96
- autobyteus/tools/usage/parsers/gemini_json_tool_usage_parser.py +37 -47
- autobyteus/tools/usage/parsers/openai_json_tool_usage_parser.py +112 -113
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/METADATA +13 -12
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/RECORD +103 -82
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/WHEEL +0 -0
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/licenses/LICENSE +0 -0
- {autobyteus-1.1.0.dist-info → autobyteus-1.1.2.dist-info}/top_level.txt +0 -0
autobyteus/cli/agent_cli.py
CHANGED
|
@@ -1,212 +1,41 @@
|
|
|
1
1
|
import asyncio
|
|
2
2
|
import logging
|
|
3
3
|
import sys
|
|
4
|
-
from typing import Optional
|
|
5
|
-
import json
|
|
4
|
+
from typing import Optional
|
|
6
5
|
|
|
7
6
|
from autobyteus.agent.agent import Agent
|
|
8
|
-
from autobyteus.agent.context.phases import AgentOperationalPhase
|
|
9
7
|
from autobyteus.agent.message.agent_input_user_message import AgentInputUserMessage
|
|
10
8
|
from autobyteus.agent.streaming.agent_event_stream import AgentEventStream
|
|
11
|
-
from
|
|
12
|
-
from autobyteus.agent.streaming.stream_event_payloads import (
|
|
13
|
-
AssistantChunkData,
|
|
14
|
-
AssistantCompleteResponseData,
|
|
15
|
-
ToolInvocationApprovalRequestedData,
|
|
16
|
-
ToolInteractionLogEntryData,
|
|
17
|
-
AgentOperationalPhaseTransitionData,
|
|
18
|
-
ErrorEventData,
|
|
19
|
-
)
|
|
9
|
+
from .cli_display import InteractiveCLIDisplay
|
|
20
10
|
|
|
21
11
|
logger = logging.getLogger(__name__)
|
|
22
12
|
|
|
23
|
-
|
|
24
|
-
"""
|
|
25
|
-
Manages the state and rendering logic for the interactive CLI session.
|
|
26
|
-
Input reading is handled by the main `run` loop. This class only handles output.
|
|
13
|
+
async def run(agent: Agent, show_tool_logs: bool = True, show_token_usage: bool = False, initial_prompt: Optional[str] = None):
|
|
27
14
|
"""
|
|
28
|
-
|
|
29
|
-
self.agent_turn_complete_event = agent_turn_complete_event
|
|
30
|
-
self.show_tool_logs = show_tool_logs
|
|
31
|
-
self.show_token_usage = show_token_usage
|
|
32
|
-
self.current_line_empty = True
|
|
33
|
-
self.agent_has_spoken_this_turn = False
|
|
34
|
-
self.pending_approval_data: Optional[ToolInvocationApprovalRequestedData] = None
|
|
35
|
-
self.is_thinking = False
|
|
36
|
-
self.is_in_content_block = False
|
|
37
|
-
|
|
38
|
-
def reset_turn_state(self):
|
|
39
|
-
"""Resets flags that are tracked on a per-turn basis."""
|
|
40
|
-
self._end_thinking_block()
|
|
41
|
-
self.agent_has_spoken_this_turn = False
|
|
42
|
-
self.is_in_content_block = False
|
|
43
|
-
|
|
44
|
-
def _ensure_new_line(self):
|
|
45
|
-
"""Ensures the cursor is on a new line if the current one isn't empty."""
|
|
46
|
-
if not self.current_line_empty:
|
|
47
|
-
sys.stdout.write("\n")
|
|
48
|
-
sys.stdout.flush()
|
|
49
|
-
self.current_line_empty = True
|
|
50
|
-
|
|
51
|
-
def _end_thinking_block(self):
|
|
52
|
-
"""Closes the <Thinking> block if it was active."""
|
|
53
|
-
if self.is_thinking:
|
|
54
|
-
sys.stdout.write("\n</Thinking>")
|
|
55
|
-
sys.stdout.flush()
|
|
56
|
-
self.is_thinking = False
|
|
57
|
-
self.current_line_empty = False
|
|
58
|
-
|
|
59
|
-
def _display_tool_approval_prompt(self):
|
|
60
|
-
"""Displays the tool approval prompt using stored pending data."""
|
|
61
|
-
if not self.pending_approval_data:
|
|
62
|
-
return
|
|
63
|
-
|
|
64
|
-
try:
|
|
65
|
-
args_str = json.dumps(self.pending_approval_data.arguments, indent=2)
|
|
66
|
-
except TypeError:
|
|
67
|
-
args_str = str(self.pending_approval_data.arguments)
|
|
68
|
-
|
|
69
|
-
self._ensure_new_line()
|
|
70
|
-
prompt_message = (
|
|
71
|
-
f"Tool Call: '{self.pending_approval_data.tool_name}' requests permission to run with arguments:\n"
|
|
72
|
-
f"{args_str}\nApprove? (y/n): "
|
|
73
|
-
)
|
|
74
|
-
sys.stdout.write(prompt_message)
|
|
75
|
-
sys.stdout.flush()
|
|
76
|
-
self.current_line_empty = False
|
|
77
|
-
|
|
78
|
-
async def handle_stream_event(self, event: StreamEvent):
|
|
79
|
-
"""Processes a single StreamEvent and updates the CLI display."""
|
|
80
|
-
# A block of thinking ends if any event other than a reasoning chunk arrives.
|
|
81
|
-
is_reasoning_only_chunk = (
|
|
82
|
-
event.event_type == StreamEventType.ASSISTANT_CHUNK and
|
|
83
|
-
isinstance(event.data, AssistantChunkData) and
|
|
84
|
-
bool(event.data.reasoning) and not bool(event.data.content)
|
|
85
|
-
)
|
|
86
|
-
if not is_reasoning_only_chunk:
|
|
87
|
-
self._end_thinking_block()
|
|
88
|
-
|
|
89
|
-
# Most events should start on a new line.
|
|
90
|
-
if event.event_type != StreamEventType.ASSISTANT_CHUNK:
|
|
91
|
-
self._ensure_new_line()
|
|
92
|
-
|
|
93
|
-
if event.event_type in [
|
|
94
|
-
StreamEventType.AGENT_IDLE,
|
|
95
|
-
StreamEventType.ERROR_EVENT,
|
|
96
|
-
StreamEventType.TOOL_INVOCATION_APPROVAL_REQUESTED,
|
|
97
|
-
]:
|
|
98
|
-
self.agent_turn_complete_event.set()
|
|
99
|
-
|
|
100
|
-
if event.event_type == StreamEventType.ASSISTANT_CHUNK and isinstance(event.data, AssistantChunkData):
|
|
101
|
-
# If this is the first output from the agent this turn, print the "Agent: " prefix.
|
|
102
|
-
if not self.agent_has_spoken_this_turn:
|
|
103
|
-
self._ensure_new_line()
|
|
104
|
-
sys.stdout.write("Agent:\n")
|
|
105
|
-
sys.stdout.flush()
|
|
106
|
-
self.agent_has_spoken_this_turn = True
|
|
107
|
-
self.current_line_empty = True
|
|
108
|
-
|
|
109
|
-
# Stream reasoning to stdout without logger formatting.
|
|
110
|
-
if event.data.reasoning:
|
|
111
|
-
if not self.is_thinking:
|
|
112
|
-
sys.stdout.write("<Thinking>\n")
|
|
113
|
-
sys.stdout.flush()
|
|
114
|
-
self.is_thinking = True
|
|
115
|
-
self.current_line_empty = True # We just printed a newline
|
|
116
|
-
|
|
117
|
-
sys.stdout.write(event.data.reasoning)
|
|
118
|
-
sys.stdout.flush()
|
|
119
|
-
self.current_line_empty = event.data.reasoning.endswith('\n')
|
|
15
|
+
Runs an interactive command-line interface for a single agent.
|
|
120
16
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
if not self.is_in_content_block:
|
|
124
|
-
self._ensure_new_line() # Ensures content starts on a new line after </Thinking>
|
|
125
|
-
self.is_in_content_block = True
|
|
126
|
-
sys.stdout.write(event.data.content)
|
|
127
|
-
sys.stdout.flush()
|
|
128
|
-
self.current_line_empty = event.data.content.endswith('\n')
|
|
129
|
-
|
|
130
|
-
if self.show_token_usage and event.data.is_complete and event.data.usage:
|
|
131
|
-
self._ensure_new_line()
|
|
132
|
-
usage = event.data.usage
|
|
133
|
-
logger.info(
|
|
134
|
-
f"[Token Usage: Prompt={usage.prompt_tokens}, "
|
|
135
|
-
f"Completion={usage.completion_tokens}, Total={usage.total_tokens}]"
|
|
136
|
-
)
|
|
137
|
-
|
|
138
|
-
elif event.event_type == StreamEventType.ASSISTANT_COMPLETE_RESPONSE and isinstance(event.data, AssistantCompleteResponseData):
|
|
139
|
-
# The reasoning has already been streamed. Do not log it again.
|
|
140
|
-
|
|
141
|
-
if not self.agent_has_spoken_this_turn:
|
|
142
|
-
# This case handles responses that might not have streamed any content chunks (e.g., only a tool call).
|
|
143
|
-
# We still need to ensure the agent's turn is visibly terminated with a newline.
|
|
144
|
-
self._ensure_new_line()
|
|
145
|
-
|
|
146
|
-
# If there's final content that wasn't in a chunk, print it.
|
|
147
|
-
if event.data.content:
|
|
148
|
-
sys.stdout.write(f"Agent: {event.data.content}\n")
|
|
149
|
-
sys.stdout.flush()
|
|
150
|
-
|
|
151
|
-
if self.show_token_usage and event.data.usage:
|
|
152
|
-
self._ensure_new_line()
|
|
153
|
-
usage = event.data.usage
|
|
154
|
-
logger.info(
|
|
155
|
-
f"[Token Usage: Prompt={usage.prompt_tokens}, "
|
|
156
|
-
f"Completion={usage.completion_tokens}, Total={usage.total_tokens}]"
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
self.current_line_empty = True
|
|
160
|
-
self.reset_turn_state() # Reset for next turn
|
|
17
|
+
This function orchestrates the agent's lifecycle, user input, and event streaming
|
|
18
|
+
for an interactive session.
|
|
161
19
|
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
logger.info(f"[Tool Log: {event.data.log_entry}]")
|
|
169
|
-
|
|
170
|
-
elif event.event_type == StreamEventType.AGENT_OPERATIONAL_PHASE_TRANSITION and isinstance(event.data, AgentOperationalPhaseTransitionData):
|
|
171
|
-
if event.data.new_phase == AgentOperationalPhase.EXECUTING_TOOL:
|
|
172
|
-
tool_name = event.data.tool_name or "a tool"
|
|
173
|
-
sys.stdout.write(f"Agent: Waiting for tool '{tool_name}' to complete...\n")
|
|
174
|
-
sys.stdout.flush()
|
|
175
|
-
self.current_line_empty = True
|
|
176
|
-
self.agent_has_spoken_this_turn = True
|
|
177
|
-
elif event.data.new_phase == AgentOperationalPhase.BOOTSTRAPPING:
|
|
178
|
-
logger.info("[Agent is initializing...]")
|
|
179
|
-
else:
|
|
180
|
-
phase_msg = f"[Agent Status: {event.data.new_phase.value}"
|
|
181
|
-
if event.data.tool_name:
|
|
182
|
-
phase_msg += f" ({event.data.tool_name})"
|
|
183
|
-
phase_msg += "]"
|
|
184
|
-
logger.info(phase_msg)
|
|
185
|
-
|
|
186
|
-
elif event.event_type == StreamEventType.ERROR_EVENT and isinstance(event.data, ErrorEventData):
|
|
187
|
-
logger.error(f"[Error: {event.data.message} (Source: {event.data.source})]")
|
|
188
|
-
|
|
189
|
-
elif event.event_type == StreamEventType.AGENT_IDLE:
|
|
190
|
-
logger.info("[Agent is now idle.]")
|
|
191
|
-
|
|
192
|
-
else:
|
|
193
|
-
# Add logging for unhandled events for better debugging
|
|
194
|
-
logger.debug(f"CLI Manager: Unhandled StreamEvent type: {event.event_type.value}")
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
async def run(agent: Agent, show_tool_logs: bool = True, show_token_usage: bool = False, initial_prompt: Optional[str] = None):
|
|
20
|
+
Args:
|
|
21
|
+
agent: The agent instance to run.
|
|
22
|
+
show_tool_logs: If True, displays detailed logs from tool interactions.
|
|
23
|
+
show_token_usage: If True, displays token usage information after each LLM call.
|
|
24
|
+
initial_prompt: An optional initial prompt to send to the agent automatically.
|
|
25
|
+
"""
|
|
198
26
|
if not isinstance(agent, Agent):
|
|
199
27
|
raise TypeError(f"Expected an Agent instance, got {type(agent).__name__}")
|
|
200
28
|
|
|
201
29
|
logger.info(f"Starting interactive CLI session for agent '{agent.agent_id}'.")
|
|
202
30
|
agent_turn_complete_event = asyncio.Event()
|
|
203
|
-
|
|
31
|
+
cli_display = InteractiveCLIDisplay(agent_turn_complete_event, show_tool_logs, show_token_usage)
|
|
204
32
|
streamer = AgentEventStream(agent)
|
|
205
33
|
|
|
206
34
|
async def process_agent_events():
|
|
35
|
+
"""Task to continuously process and display events from the agent."""
|
|
207
36
|
try:
|
|
208
37
|
async for event in streamer.all_events():
|
|
209
|
-
await
|
|
38
|
+
await cli_display.handle_stream_event(event)
|
|
210
39
|
except asyncio.CancelledError:
|
|
211
40
|
logger.info("CLI event processing task cancelled.")
|
|
212
41
|
except Exception as e:
|
|
@@ -226,35 +55,29 @@ async def run(agent: Agent, show_tool_logs: bool = True, show_token_usage: bool
|
|
|
226
55
|
await asyncio.wait_for(agent_turn_complete_event.wait(), timeout=30.0)
|
|
227
56
|
except asyncio.TimeoutError:
|
|
228
57
|
logger.error(f"Agent did not become idle within 30 seconds. Exiting.")
|
|
229
|
-
# Gracefully exit if agent fails to start
|
|
230
58
|
return
|
|
231
59
|
|
|
232
60
|
if initial_prompt:
|
|
233
61
|
logger.info(f"Initial prompt provided: '{initial_prompt}'")
|
|
234
|
-
print(f"You: {initial_prompt}")
|
|
62
|
+
print(f"You: {initial_prompt}")
|
|
235
63
|
agent_turn_complete_event.clear()
|
|
236
|
-
|
|
64
|
+
cli_display.reset_turn_state()
|
|
237
65
|
await agent.post_user_message(AgentInputUserMessage(content=initial_prompt))
|
|
238
66
|
await agent_turn_complete_event.wait()
|
|
239
67
|
|
|
68
|
+
# Main input loop
|
|
240
69
|
while True:
|
|
241
70
|
agent_turn_complete_event.clear()
|
|
242
71
|
|
|
243
|
-
if
|
|
244
|
-
logger.debug("Waiting for tool approval from user...")
|
|
72
|
+
if cli_display.pending_approval_data:
|
|
245
73
|
approval_input = await asyncio.get_event_loop().run_in_executor(None, sys.stdin.readline)
|
|
246
74
|
approval_input = approval_input.strip().lower()
|
|
247
75
|
|
|
248
|
-
approval_data =
|
|
249
|
-
|
|
76
|
+
approval_data = cli_display.pending_approval_data
|
|
77
|
+
cli_display.pending_approval_data = None
|
|
250
78
|
|
|
251
79
|
is_approved = approval_input in ["y", "yes"]
|
|
252
80
|
reason = "User approved via CLI" if is_approved else "User denied via CLI"
|
|
253
|
-
|
|
254
|
-
if is_approved:
|
|
255
|
-
logger.info(f"User approved tool invocation '{approval_data.invocation_id}'.")
|
|
256
|
-
else:
|
|
257
|
-
logger.info(f"User denied tool invocation '{approval_data.invocation_id}'.")
|
|
258
81
|
|
|
259
82
|
await agent.post_tool_execution_approval(approval_data.invocation_id, is_approved, reason)
|
|
260
83
|
|
|
@@ -265,35 +88,30 @@ async def run(agent: Agent, show_tool_logs: bool = True, show_token_usage: bool
|
|
|
265
88
|
user_input = user_input.rstrip('\n')
|
|
266
89
|
|
|
267
90
|
if user_input.lower().strip() in ["/quit", "/exit"]:
|
|
268
|
-
logger.info("Exit command received.")
|
|
269
91
|
break
|
|
270
92
|
if not user_input.strip():
|
|
271
93
|
continue
|
|
272
94
|
|
|
273
|
-
|
|
274
|
-
cli_manager.reset_turn_state()
|
|
95
|
+
cli_display.reset_turn_state()
|
|
275
96
|
await agent.post_user_message(AgentInputUserMessage(content=user_input))
|
|
276
97
|
|
|
277
98
|
await agent_turn_complete_event.wait()
|
|
278
|
-
logger.debug("Agent turn complete, looping.")
|
|
279
99
|
|
|
280
100
|
except (KeyboardInterrupt, EOFError):
|
|
281
|
-
logger.info("Exit signal received.
|
|
101
|
+
logger.info("Exit signal received.")
|
|
282
102
|
except Exception as e:
|
|
283
103
|
logger.error(f"An unexpected error occurred in the CLI main loop: {e}", exc_info=True)
|
|
284
104
|
finally:
|
|
285
|
-
logger.info("
|
|
105
|
+
logger.info("Shutting down interactive session...")
|
|
286
106
|
if not event_task.done():
|
|
287
107
|
event_task.cancel()
|
|
288
108
|
try:
|
|
289
109
|
await event_task
|
|
290
110
|
except asyncio.CancelledError:
|
|
291
|
-
pass
|
|
111
|
+
pass
|
|
292
112
|
|
|
293
113
|
if agent.is_running:
|
|
294
|
-
logger.info("Stopping agent...")
|
|
295
114
|
await agent.stop()
|
|
296
115
|
|
|
297
|
-
logger.info("Closing event stream...")
|
|
298
116
|
await streamer.close()
|
|
299
117
|
logger.info("Interactive session finished.")
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
import sys
|
|
4
|
+
from typing import Optional, List, Dict, Any
|
|
5
|
+
import json
|
|
6
|
+
|
|
7
|
+
from autobyteus.agent.context.phases import AgentOperationalPhase
|
|
8
|
+
from autobyteus.agent.streaming.stream_events import StreamEvent, StreamEventType
|
|
9
|
+
from autobyteus.agent.streaming.stream_event_payloads import (
|
|
10
|
+
AssistantChunkData,
|
|
11
|
+
AssistantCompleteResponseData,
|
|
12
|
+
ToolInvocationApprovalRequestedData,
|
|
13
|
+
ToolInteractionLogEntryData,
|
|
14
|
+
AgentOperationalPhaseTransitionData,
|
|
15
|
+
ErrorEventData,
|
|
16
|
+
ToolInvocationAutoExecutingData,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
class InteractiveCLIDisplay:
|
|
22
|
+
"""
|
|
23
|
+
Manages the state and rendering logic for the interactive CLI session's display.
|
|
24
|
+
Input reading is handled by the main `run` loop. This class only handles output.
|
|
25
|
+
"""
|
|
26
|
+
def __init__(self, agent_turn_complete_event: asyncio.Event, show_tool_logs: bool, show_token_usage: bool):
|
|
27
|
+
self.agent_turn_complete_event = agent_turn_complete_event
|
|
28
|
+
self.show_tool_logs = show_tool_logs
|
|
29
|
+
self.show_token_usage = show_token_usage
|
|
30
|
+
self.current_line_empty = True
|
|
31
|
+
self.agent_has_spoken_this_turn = False
|
|
32
|
+
self.pending_approval_data: Optional[ToolInvocationApprovalRequestedData] = None
|
|
33
|
+
self.is_thinking = False
|
|
34
|
+
self.is_in_content_block = False
|
|
35
|
+
|
|
36
|
+
def reset_turn_state(self):
|
|
37
|
+
"""Resets flags that are tracked on a per-turn basis."""
|
|
38
|
+
self._end_thinking_block()
|
|
39
|
+
self.agent_has_spoken_this_turn = False
|
|
40
|
+
self.is_in_content_block = False
|
|
41
|
+
|
|
42
|
+
def _ensure_new_line(self):
|
|
43
|
+
"""Ensures the cursor is on a new line if the current one isn't empty."""
|
|
44
|
+
if not self.current_line_empty:
|
|
45
|
+
sys.stdout.write("\n")
|
|
46
|
+
sys.stdout.flush()
|
|
47
|
+
self.current_line_empty = True
|
|
48
|
+
|
|
49
|
+
def _end_thinking_block(self):
|
|
50
|
+
"""Closes the <Thinking> block if it was active."""
|
|
51
|
+
if self.is_thinking:
|
|
52
|
+
sys.stdout.write("\n</Thinking>")
|
|
53
|
+
sys.stdout.flush()
|
|
54
|
+
self.is_thinking = False
|
|
55
|
+
self.current_line_empty = False
|
|
56
|
+
|
|
57
|
+
def _display_tool_approval_prompt(self):
|
|
58
|
+
"""Displays the tool approval prompt using stored pending data."""
|
|
59
|
+
if not self.pending_approval_data:
|
|
60
|
+
return
|
|
61
|
+
|
|
62
|
+
try:
|
|
63
|
+
args_str = json.dumps(self.pending_approval_data.arguments, indent=2)
|
|
64
|
+
except TypeError:
|
|
65
|
+
args_str = str(self.pending_approval_data.arguments)
|
|
66
|
+
|
|
67
|
+
self._ensure_new_line()
|
|
68
|
+
prompt_message = (
|
|
69
|
+
f"Tool Call: '{self.pending_approval_data.tool_name}' requests permission to run with arguments:\n"
|
|
70
|
+
f"{args_str}\nApprove? (y/n): "
|
|
71
|
+
)
|
|
72
|
+
sys.stdout.write(prompt_message)
|
|
73
|
+
sys.stdout.flush()
|
|
74
|
+
self.current_line_empty = False
|
|
75
|
+
|
|
76
|
+
async def handle_stream_event(self, event: StreamEvent):
|
|
77
|
+
"""Processes a single StreamEvent and updates the CLI display."""
|
|
78
|
+
# A block of thinking ends if any event other than a reasoning chunk arrives.
|
|
79
|
+
is_reasoning_only_chunk = (
|
|
80
|
+
event.event_type == StreamEventType.ASSISTANT_CHUNK and
|
|
81
|
+
isinstance(event.data, AssistantChunkData) and
|
|
82
|
+
bool(event.data.reasoning) and not bool(event.data.content)
|
|
83
|
+
)
|
|
84
|
+
if not is_reasoning_only_chunk:
|
|
85
|
+
self._end_thinking_block()
|
|
86
|
+
|
|
87
|
+
# Most events should start on a new line.
|
|
88
|
+
if event.event_type != StreamEventType.ASSISTANT_CHUNK:
|
|
89
|
+
self._ensure_new_line()
|
|
90
|
+
|
|
91
|
+
if event.event_type in [
|
|
92
|
+
StreamEventType.AGENT_IDLE,
|
|
93
|
+
StreamEventType.ERROR_EVENT,
|
|
94
|
+
StreamEventType.TOOL_INVOCATION_APPROVAL_REQUESTED,
|
|
95
|
+
]:
|
|
96
|
+
self.agent_turn_complete_event.set()
|
|
97
|
+
|
|
98
|
+
if event.event_type == StreamEventType.ASSISTANT_CHUNK and isinstance(event.data, AssistantChunkData):
|
|
99
|
+
# If this is the first output from the agent this turn, print the "Agent: " prefix.
|
|
100
|
+
if not self.agent_has_spoken_this_turn:
|
|
101
|
+
self._ensure_new_line()
|
|
102
|
+
sys.stdout.write("Agent:\n")
|
|
103
|
+
sys.stdout.flush()
|
|
104
|
+
self.agent_has_spoken_this_turn = True
|
|
105
|
+
self.current_line_empty = True
|
|
106
|
+
|
|
107
|
+
# Stream reasoning to stdout without logger formatting.
|
|
108
|
+
if event.data.reasoning:
|
|
109
|
+
if not self.is_thinking:
|
|
110
|
+
sys.stdout.write("<Thinking>\n")
|
|
111
|
+
sys.stdout.flush()
|
|
112
|
+
self.is_thinking = True
|
|
113
|
+
self.current_line_empty = True # We just printed a newline
|
|
114
|
+
|
|
115
|
+
sys.stdout.write(event.data.reasoning)
|
|
116
|
+
sys.stdout.flush()
|
|
117
|
+
self.current_line_empty = event.data.reasoning.endswith('\n')
|
|
118
|
+
|
|
119
|
+
# Stream content to stdout.
|
|
120
|
+
if event.data.content:
|
|
121
|
+
if not self.is_in_content_block:
|
|
122
|
+
self._ensure_new_line() # Ensures content starts on a new line after </Thinking>
|
|
123
|
+
self.is_in_content_block = True
|
|
124
|
+
sys.stdout.write(event.data.content)
|
|
125
|
+
sys.stdout.flush()
|
|
126
|
+
self.current_line_empty = event.data.content.endswith('\n')
|
|
127
|
+
|
|
128
|
+
if self.show_token_usage and event.data.is_complete and event.data.usage:
|
|
129
|
+
self._ensure_new_line()
|
|
130
|
+
usage = event.data.usage
|
|
131
|
+
logger.info(
|
|
132
|
+
f"[Token Usage: Prompt={usage.prompt_tokens}, "
|
|
133
|
+
f"Completion={usage.completion_tokens}, Total={usage.total_tokens}]"
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
elif event.event_type == StreamEventType.ASSISTANT_COMPLETE_RESPONSE and isinstance(event.data, AssistantCompleteResponseData):
|
|
137
|
+
# The reasoning has already been streamed. Do not log it again.
|
|
138
|
+
|
|
139
|
+
if not self.agent_has_spoken_this_turn:
|
|
140
|
+
# This case handles responses that might not have streamed any content chunks (e.g., only a tool call).
|
|
141
|
+
# We still need to ensure the agent's turn is visibly terminated with a newline.
|
|
142
|
+
self._ensure_new_line()
|
|
143
|
+
|
|
144
|
+
# If there's final content that wasn't in a chunk, print it.
|
|
145
|
+
if event.data.content:
|
|
146
|
+
sys.stdout.write(f"Agent: {event.data.content}\n")
|
|
147
|
+
sys.stdout.flush()
|
|
148
|
+
|
|
149
|
+
if self.show_token_usage and event.data.usage:
|
|
150
|
+
self._ensure_new_line()
|
|
151
|
+
usage = event.data.usage
|
|
152
|
+
logger.info(
|
|
153
|
+
f"[Token Usage: Prompt={usage.prompt_tokens}, "
|
|
154
|
+
f"Completion={usage.completion_tokens}, Total={usage.total_tokens}]"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
self.current_line_empty = True
|
|
158
|
+
self.reset_turn_state() # Reset for next turn
|
|
159
|
+
|
|
160
|
+
elif event.event_type == StreamEventType.TOOL_INVOCATION_APPROVAL_REQUESTED and isinstance(event.data, ToolInvocationApprovalRequestedData):
|
|
161
|
+
self.pending_approval_data = event.data
|
|
162
|
+
self._display_tool_approval_prompt()
|
|
163
|
+
|
|
164
|
+
elif event.event_type == StreamEventType.TOOL_INVOCATION_AUTO_EXECUTING and isinstance(event.data, ToolInvocationAutoExecutingData):
|
|
165
|
+
tool_name = event.data.tool_name
|
|
166
|
+
self._ensure_new_line()
|
|
167
|
+
sys.stdout.write(f"Agent: Automatically executing tool '{tool_name}'...\n")
|
|
168
|
+
sys.stdout.flush()
|
|
169
|
+
self.current_line_empty = True
|
|
170
|
+
self.agent_has_spoken_this_turn = True
|
|
171
|
+
|
|
172
|
+
elif event.event_type == StreamEventType.TOOL_INTERACTION_LOG_ENTRY and isinstance(event.data, ToolInteractionLogEntryData):
|
|
173
|
+
if self.show_tool_logs:
|
|
174
|
+
logger.info(
|
|
175
|
+
f"[Tool Log ({event.data.tool_name} | {event.data.tool_invocation_id})]: {event.data.log_entry}"
|
|
176
|
+
)
|
|
177
|
+
|
|
178
|
+
elif event.event_type == StreamEventType.AGENT_OPERATIONAL_PHASE_TRANSITION and isinstance(event.data, AgentOperationalPhaseTransitionData):
|
|
179
|
+
if event.data.new_phase == AgentOperationalPhase.EXECUTING_TOOL:
|
|
180
|
+
tool_name = event.data.tool_name or "a tool"
|
|
181
|
+
sys.stdout.write(f"Agent: Waiting for tool '{tool_name}' to complete...\n")
|
|
182
|
+
sys.stdout.flush()
|
|
183
|
+
self.current_line_empty = True
|
|
184
|
+
self.agent_has_spoken_this_turn = True
|
|
185
|
+
elif event.data.new_phase == AgentOperationalPhase.BOOTSTRAPPING:
|
|
186
|
+
logger.info("[Agent is initializing...]")
|
|
187
|
+
elif event.data.new_phase == AgentOperationalPhase.TOOL_DENIED:
|
|
188
|
+
tool_name = event.data.tool_name or "a tool"
|
|
189
|
+
logger.info(f"[Tool '{tool_name}' was denied by user. Agent is reconsidering.]")
|
|
190
|
+
else:
|
|
191
|
+
phase_msg = f"[Agent Status: {event.data.new_phase.value}"
|
|
192
|
+
if event.data.tool_name:
|
|
193
|
+
phase_msg += f" ({event.data.tool_name})"
|
|
194
|
+
phase_msg += "]"
|
|
195
|
+
logger.info(phase_msg)
|
|
196
|
+
|
|
197
|
+
elif event.event_type == StreamEventType.ERROR_EVENT and isinstance(event.data, ErrorEventData):
|
|
198
|
+
logger.error(f"[Error: {event.data.message} (Source: {event.data.source})]")
|
|
199
|
+
|
|
200
|
+
elif event.event_type == StreamEventType.AGENT_IDLE:
|
|
201
|
+
logger.info("[Agent is now idle.]")
|
|
202
|
+
|
|
203
|
+
else:
|
|
204
|
+
# Add logging for unhandled events for better debugging
|
|
205
|
+
logger.debug(f"CLI Display: Unhandled StreamEvent type: {event.event_type.value}")
|
|
@@ -122,7 +122,8 @@ class EventManager(metaclass=SingletonMeta):
|
|
|
122
122
|
listener(**final_args_to_pass)
|
|
123
123
|
|
|
124
124
|
def emit(self, event_type: EventType, origin_object_id: Optional[str] = None, **kwargs: Any):
|
|
125
|
-
|
|
125
|
+
# FIX: Added 'event_type' to the dictionary passed to listeners.
|
|
126
|
+
available_kwargs_for_listeners = {"event_type": event_type, "object_id": origin_object_id, **kwargs}
|
|
126
127
|
|
|
127
128
|
targeted_topic = Topic(event_type, origin_object_id)
|
|
128
129
|
global_topic = Topic(event_type, None)
|
autobyteus/events/event_types.py
CHANGED
|
@@ -21,6 +21,7 @@ class EventType(Enum):
|
|
|
21
21
|
AGENT_PHASE_AWAITING_LLM_RESPONSE_STARTED = "agent_phase_awaiting_llm_response_started"
|
|
22
22
|
AGENT_PHASE_ANALYZING_LLM_RESPONSE_STARTED = "agent_phase_analyzing_llm_response_started"
|
|
23
23
|
AGENT_PHASE_AWAITING_TOOL_APPROVAL_STARTED = "agent_phase_awaiting_tool_approval_started"
|
|
24
|
+
AGENT_PHASE_TOOL_DENIED_STARTED = "agent_phase_tool_denied_started"
|
|
24
25
|
AGENT_PHASE_EXECUTING_TOOL_STARTED = "agent_phase_executing_tool_started"
|
|
25
26
|
AGENT_PHASE_PROCESSING_TOOL_RESULT_STARTED = "agent_phase_processing_tool_result_started"
|
|
26
27
|
AGENT_PHASE_SHUTTING_DOWN_STARTED = "agent_phase_shutting_down_started"
|
|
@@ -30,12 +31,13 @@ class EventType(Enum):
|
|
|
30
31
|
# --- Agent Data Outputs ---
|
|
31
32
|
AGENT_DATA_ASSISTANT_CHUNK = "agent_data_assistant_chunk"
|
|
32
33
|
AGENT_DATA_ASSISTANT_CHUNK_STREAM_END = "agent_data_assistant_chunk_stream_end"
|
|
33
|
-
AGENT_DATA_ASSISTANT_COMPLETE_RESPONSE = "agent_data_assistant_complete_response"
|
|
34
|
+
AGENT_DATA_ASSISTANT_COMPLETE_RESPONSE = "agent_data_assistant_complete_response"
|
|
34
35
|
AGENT_DATA_TOOL_LOG = "agent_data_tool_log"
|
|
35
36
|
AGENT_DATA_TOOL_LOG_STREAM_END = "agent_data_tool_log_stream_end"
|
|
36
37
|
|
|
37
38
|
# --- Agent Requests for External Interaction ---
|
|
38
39
|
AGENT_REQUEST_TOOL_INVOCATION_APPROVAL = "agent_request_tool_invocation_approval"
|
|
40
|
+
AGENT_TOOL_INVOCATION_AUTO_EXECUTING = "agent_tool_invocation_auto_executing"
|
|
39
41
|
|
|
40
42
|
# --- Agent Errors (not necessarily phase changes, e.g., error during output generation) ---
|
|
41
43
|
AGENT_ERROR_OUTPUT_GENERATION = "agent_error_output_generation"
|
|
@@ -30,18 +30,13 @@ class AutobyteusLLM(BaseLLM):
|
|
|
30
30
|
image_urls: Optional[List[str]] = None,
|
|
31
31
|
**kwargs
|
|
32
32
|
) -> CompleteResponse:
|
|
33
|
-
user_message_index = kwargs.get("user_message_index")
|
|
34
|
-
if user_message_index is None:
|
|
35
|
-
raise ValueError("user_message_index is required in kwargs")
|
|
36
|
-
|
|
37
33
|
self.add_user_message(user_message)
|
|
38
34
|
try:
|
|
39
35
|
response = await self.client.send_message(
|
|
40
36
|
conversation_id=self.conversation_id,
|
|
41
37
|
model_name=self.model.name,
|
|
42
38
|
user_message=user_message,
|
|
43
|
-
|
|
44
|
-
user_message_index=user_message_index
|
|
39
|
+
image_urls=image_urls
|
|
45
40
|
)
|
|
46
41
|
|
|
47
42
|
assistant_message = response['response']
|
|
@@ -69,10 +64,6 @@ class AutobyteusLLM(BaseLLM):
|
|
|
69
64
|
image_urls: Optional[List[str]] = None,
|
|
70
65
|
**kwargs
|
|
71
66
|
) -> AsyncGenerator[ChunkResponse, None]:
|
|
72
|
-
user_message_index = kwargs.get("user_message_index")
|
|
73
|
-
if user_message_index is None:
|
|
74
|
-
raise ValueError("user_message_index is required in kwargs")
|
|
75
|
-
|
|
76
67
|
self.add_user_message(user_message)
|
|
77
68
|
complete_response = ""
|
|
78
69
|
|
|
@@ -81,8 +72,7 @@ class AutobyteusLLM(BaseLLM):
|
|
|
81
72
|
conversation_id=self.conversation_id,
|
|
82
73
|
model_name=self.model.name,
|
|
83
74
|
user_message=user_message,
|
|
84
|
-
|
|
85
|
-
user_message_index=user_message_index
|
|
75
|
+
image_urls=image_urls
|
|
86
76
|
):
|
|
87
77
|
if 'error' in chunk:
|
|
88
78
|
raise RuntimeError(chunk['error'])
|