autobyteus 1.0.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.
- autobyteus/__init__.py +5 -0
- autobyteus/agent/__init__.py +0 -0
- autobyteus/agent/agent.py +231 -0
- autobyteus/agent/async_agent.py +175 -0
- autobyteus/agent/async_group_aware_agent.py +136 -0
- autobyteus/agent/exceptions.py +6 -0
- autobyteus/agent/factory/__init__.py +0 -0
- autobyteus/agent/group/__init__.py +0 -0
- autobyteus/agent/group/async_group_aware_agent.py +122 -0
- autobyteus/agent/group/coordinator_agent.py +36 -0
- autobyteus/agent/group/group_aware_agent.py +121 -0
- autobyteus/agent/message/__init__.py +0 -0
- autobyteus/agent/message/message.py +41 -0
- autobyteus/agent/message/message_types.py +29 -0
- autobyteus/agent/message/send_message_to.py +47 -0
- autobyteus/agent/orchestrator/__init__.py +0 -0
- autobyteus/agent/orchestrator/base_agent_orchestrator.py +82 -0
- autobyteus/agent/orchestrator/multi_replica_agent_orchestrator.py +72 -0
- autobyteus/agent/orchestrator/single_replica_agent_orchestrator.py +43 -0
- autobyteus/agent/response_parser/__init__.py +0 -0
- autobyteus/agent/response_parser/tool_usage_command_parser.py +100 -0
- autobyteus/agent/status.py +12 -0
- autobyteus/agent/tool_invocation.py +7 -0
- autobyteus/check_requirements.py +33 -0
- autobyteus/conversation/__init__.py +0 -0
- autobyteus/conversation/conversation.py +54 -0
- autobyteus/conversation/user_message.py +59 -0
- autobyteus/events/__init__.py +0 -0
- autobyteus/events/decorators.py +29 -0
- autobyteus/events/event_emitter.py +74 -0
- autobyteus/events/event_manager.py +73 -0
- autobyteus/events/event_types.py +17 -0
- autobyteus/llm/__init__.py +0 -0
- autobyteus/llm/api/__init__.py +0 -0
- autobyteus/llm/api/autobyteus_llm.py +136 -0
- autobyteus/llm/api/bedrock_llm.py +78 -0
- autobyteus/llm/api/claude_llm.py +138 -0
- autobyteus/llm/api/deepseek_llm.py +226 -0
- autobyteus/llm/api/gemini_llm.py +79 -0
- autobyteus/llm/api/grok_llm.py +211 -0
- autobyteus/llm/api/groq_llm.py +88 -0
- autobyteus/llm/api/mistral_llm.py +120 -0
- autobyteus/llm/api/nvidia_llm.py +102 -0
- autobyteus/llm/api/ollama_llm.py +130 -0
- autobyteus/llm/api/openai_llm.py +148 -0
- autobyteus/llm/autobyteus_provider.py +172 -0
- autobyteus/llm/base_llm.py +222 -0
- autobyteus/llm/extensions/__init__.py +0 -0
- autobyteus/llm/extensions/base_extension.py +45 -0
- autobyteus/llm/extensions/extension_registry.py +37 -0
- autobyteus/llm/extensions/token_usage_tracking_extension.py +79 -0
- autobyteus/llm/llm_factory.py +292 -0
- autobyteus/llm/models.py +73 -0
- autobyteus/llm/ollama_provider.py +98 -0
- autobyteus/llm/providers.py +14 -0
- autobyteus/llm/token_counter/__init__.py +0 -0
- autobyteus/llm/token_counter/base_token_counter.py +69 -0
- autobyteus/llm/token_counter/claude_token_counter.py +77 -0
- autobyteus/llm/token_counter/deepseek_token_counter.py +24 -0
- autobyteus/llm/token_counter/mistral_token_counter.py +115 -0
- autobyteus/llm/token_counter/openai_token_counter.py +84 -0
- autobyteus/llm/token_counter/token_counter_factory.py +40 -0
- autobyteus/llm/utils/__init__.py +0 -0
- autobyteus/llm/utils/image_payload_formatter.py +89 -0
- autobyteus/llm/utils/llm_config.py +90 -0
- autobyteus/llm/utils/messages.py +40 -0
- autobyteus/llm/utils/rate_limiter.py +41 -0
- autobyteus/llm/utils/response_types.py +19 -0
- autobyteus/llm/utils/token_pricing_config.py +87 -0
- autobyteus/llm/utils/token_usage.py +13 -0
- autobyteus/llm/utils/token_usage_tracker.py +98 -0
- autobyteus/person/__init__.py +0 -0
- autobyteus/person/examples/__init__.py +0 -0
- autobyteus/person/examples/sample_persons.py +14 -0
- autobyteus/person/examples/sample_roles.py +14 -0
- autobyteus/person/person.py +29 -0
- autobyteus/person/role.py +14 -0
- autobyteus/prompt/__init__.py +0 -0
- autobyteus/prompt/prompt_builder.py +64 -0
- autobyteus/prompt/prompt_template.py +44 -0
- autobyteus/prompt/prompt_version_manager.py +58 -0
- autobyteus/prompt/storage/__init__.py +0 -0
- autobyteus/prompt/storage/prompt_version_model.py +29 -0
- autobyteus/prompt/storage/prompt_version_repository.py +83 -0
- autobyteus/tools/__init__.py +0 -0
- autobyteus/tools/ask_user_input.py +93 -0
- autobyteus/tools/base_tool.py +49 -0
- autobyteus/tools/bash/__init__.py +0 -0
- autobyteus/tools/bash/bash_executor.py +103 -0
- autobyteus/tools/bash/factory/__init__.py +0 -0
- autobyteus/tools/bash/factory/bash_executor_factory.py +6 -0
- autobyteus/tools/browser/__init__.py +0 -0
- autobyteus/tools/browser/session_aware/__init__.py +0 -0
- autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +61 -0
- autobyteus/tools/browser/session_aware/browser_session_aware_tool.py +27 -0
- autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +202 -0
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +29 -0
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +34 -0
- autobyteus/tools/browser/session_aware/factory/__init__.py +0 -0
- autobyteus/tools/browser/session_aware/factory/browser_session_aware_web_element_trigger_factory.py +6 -0
- autobyteus/tools/browser/session_aware/factory/browser_session_aware_webpage_reader_factory.py +10 -0
- autobyteus/tools/browser/session_aware/factory/browser_session_aware_webpage_screenshot_taker_factory.py +6 -0
- autobyteus/tools/browser/session_aware/shared_browser_session.py +11 -0
- autobyteus/tools/browser/session_aware/shared_browser_session_manager.py +24 -0
- autobyteus/tools/browser/session_aware/web_element_action.py +20 -0
- autobyteus/tools/browser/standalone/__init__.py +0 -0
- autobyteus/tools/browser/standalone/factory/__init__.py +0 -0
- autobyteus/tools/browser/standalone/factory/google_search_factory.py +10 -0
- autobyteus/tools/browser/standalone/factory/webpage_reader_factory.py +10 -0
- autobyteus/tools/browser/standalone/factory/webpage_screenshot_taker_factory.py +6 -0
- autobyteus/tools/browser/standalone/google_search_ui.py +113 -0
- autobyteus/tools/browser/standalone/navigate_to.py +62 -0
- autobyteus/tools/browser/standalone/webpage_image_downloader.py +128 -0
- autobyteus/tools/browser/standalone/webpage_reader.py +78 -0
- autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +49 -0
- autobyteus/tools/factory/__init__.py +0 -0
- autobyteus/tools/factory/ask_user_input_factory.py +6 -0
- autobyteus/tools/factory/image_downloader_factory.py +9 -0
- autobyteus/tools/factory/pdf_downloader_factory.py +9 -0
- autobyteus/tools/factory/tool_factory.py +8 -0
- autobyteus/tools/factory/webpage_image_downloader_factory.py +6 -0
- autobyteus/tools/file/__init__.py +0 -0
- autobyteus/tools/file/factory/__init__.py +0 -0
- autobyteus/tools/file/factory/file_reader_factory.py +6 -0
- autobyteus/tools/file/factory/file_writer_factory.py +6 -0
- autobyteus/tools/file/file_reader.py +60 -0
- autobyteus/tools/file/file_writer.py +65 -0
- autobyteus/tools/handlers/__init__.py +0 -0
- autobyteus/tools/handlers/shell_handler.py +32 -0
- autobyteus/tools/image_downloader.py +142 -0
- autobyteus/tools/operation/__init__.py +0 -0
- autobyteus/tools/operation/file_operation.py +73 -0
- autobyteus/tools/operation/file_rename_operation.py +52 -0
- autobyteus/tools/operation/operation.py +64 -0
- autobyteus/tools/operation/shell_operation.py +65 -0
- autobyteus/tools/pdf_downloader.py +94 -0
- autobyteus/tools/timer.py +123 -0
- autobyteus/tools/utils.py +17 -0
- autobyteus/tools/web_page_pdf_generator.py +86 -0
- autobyteus/utils/__init__.py +0 -0
- autobyteus/utils/dynamic_enum.py +35 -0
- autobyteus/utils/file_utils.py +15 -0
- autobyteus/utils/html_cleaner.py +240 -0
- autobyteus/utils/singleton.py +26 -0
- autobyteus/workflow/__init__.py +0 -0
- autobyteus/workflow/simple_task.py +98 -0
- autobyteus/workflow/task.py +147 -0
- autobyteus/workflow/workflow.py +49 -0
- autobyteus-1.0.0.dist-info/LICENSE +14 -0
- autobyteus-1.0.0.dist-info/METADATA +138 -0
- autobyteus-1.0.0.dist-info/RECORD +153 -0
- autobyteus-1.0.0.dist-info/WHEEL +5 -0
- autobyteus-1.0.0.dist-info/top_level.txt +1 -0
autobyteus/__init__.py
ADDED
|
File without changes
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from typing import List, Optional
|
|
4
|
+
from autobyteus.agent.response_parser.tool_usage_command_parser import ToolUsageCommandParser
|
|
5
|
+
from autobyteus.events.event_emitter import EventEmitter
|
|
6
|
+
from autobyteus.llm.base_llm import BaseLLM
|
|
7
|
+
from autobyteus.tools.base_tool import BaseTool
|
|
8
|
+
from autobyteus.prompt.prompt_builder import PromptBuilder
|
|
9
|
+
from autobyteus.events.event_types import EventType
|
|
10
|
+
from autobyteus.agent.status import AgentStatus
|
|
11
|
+
from autobyteus.conversation.user_message import UserMessage
|
|
12
|
+
from autobyteus.conversation.conversation import Conversation
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
class Agent(EventEmitter):
|
|
17
|
+
def __init__(self, role: str, llm: BaseLLM, tools: Optional[List[BaseTool]] = None,
|
|
18
|
+
agent_id=None,
|
|
19
|
+
prompt_builder: Optional[PromptBuilder] = None,
|
|
20
|
+
initial_user_message: Optional[UserMessage] = None):
|
|
21
|
+
super().__init__()
|
|
22
|
+
self.role = role
|
|
23
|
+
self.llm = llm
|
|
24
|
+
self.tools = tools or []
|
|
25
|
+
self.tool_usage_response_parser = ToolUsageCommandParser() if self.tools else None
|
|
26
|
+
self.conversation = None
|
|
27
|
+
self.agent_id = agent_id or f"{self.role}-001"
|
|
28
|
+
self.status = AgentStatus.NOT_STARTED
|
|
29
|
+
self._run_task = None
|
|
30
|
+
self._queues_initialized = False
|
|
31
|
+
self.task_completed = None
|
|
32
|
+
self.prompt_builder = prompt_builder
|
|
33
|
+
self.initial_user_message = initial_user_message
|
|
34
|
+
|
|
35
|
+
if not self.prompt_builder and not self.initial_user_message:
|
|
36
|
+
raise ValueError("Either prompt_builder or initial_user_message must be provided")
|
|
37
|
+
|
|
38
|
+
if self.tools:
|
|
39
|
+
self.set_agent_id_on_tools()
|
|
40
|
+
|
|
41
|
+
logger.info(f"StandaloneAgent initialized with role: {self.role}, agent_id: {self.agent_id}")
|
|
42
|
+
|
|
43
|
+
def _initialize_queues(self):
|
|
44
|
+
if not self._queues_initialized:
|
|
45
|
+
self.tool_result_messages = asyncio.Queue()
|
|
46
|
+
self.user_messages = asyncio.Queue()
|
|
47
|
+
self._queues_initialized = True
|
|
48
|
+
logger.info(f"Queues initialized for agent {self.role}")
|
|
49
|
+
|
|
50
|
+
def _initialize_task_completed(self):
|
|
51
|
+
if self.task_completed is None:
|
|
52
|
+
self.task_completed = asyncio.Event()
|
|
53
|
+
logger.info(f"task_completed Event initialized for agent {self.role}")
|
|
54
|
+
|
|
55
|
+
def get_task_completed(self):
|
|
56
|
+
if self.task_completed is None:
|
|
57
|
+
raise RuntimeError("task_completed Event accessed before initialization")
|
|
58
|
+
return self.task_completed
|
|
59
|
+
|
|
60
|
+
async def run(self):
|
|
61
|
+
try:
|
|
62
|
+
logger.info(f"Starting execution for agent: {self.role}")
|
|
63
|
+
self._initialize_queues()
|
|
64
|
+
self._initialize_task_completed()
|
|
65
|
+
await self.initialize_conversation()
|
|
66
|
+
|
|
67
|
+
user_message_handler = asyncio.create_task(self.handle_user_messages())
|
|
68
|
+
tool_result_handler = asyncio.create_task(self.handle_tool_result_messages())
|
|
69
|
+
|
|
70
|
+
# Once everything is ready, set the status to RUNNING
|
|
71
|
+
self.status = AgentStatus.RUNNING
|
|
72
|
+
|
|
73
|
+
await asyncio.gather(user_message_handler, tool_result_handler)
|
|
74
|
+
|
|
75
|
+
except Exception as e:
|
|
76
|
+
logger.error(f"Error in agent {self.role} execution: {str(e)}")
|
|
77
|
+
self.status = AgentStatus.ERROR
|
|
78
|
+
finally:
|
|
79
|
+
self.status = AgentStatus.ENDED
|
|
80
|
+
await self.cleanup()
|
|
81
|
+
|
|
82
|
+
async def handle_user_messages(self):
|
|
83
|
+
logger.info(f"Agent {self.role} started handling user messages")
|
|
84
|
+
while not self.task_completed.is_set() and self.status == AgentStatus.RUNNING:
|
|
85
|
+
try:
|
|
86
|
+
user_message: UserMessage = await asyncio.wait_for(self.user_messages.get(), timeout=1.0)
|
|
87
|
+
logger.info(f"Agent {self.role} handling user message")
|
|
88
|
+
response = await self.conversation.send_user_message(user_message.content, user_message.file_paths)
|
|
89
|
+
await self.process_llm_response(response)
|
|
90
|
+
except asyncio.TimeoutError:
|
|
91
|
+
continue
|
|
92
|
+
except asyncio.CancelledError:
|
|
93
|
+
logger.info(f"User message handler for agent {self.role} cancelled")
|
|
94
|
+
break
|
|
95
|
+
except Exception as e:
|
|
96
|
+
logger.error(f"Error handling user message for agent {self.role}: {str(e)}")
|
|
97
|
+
|
|
98
|
+
async def receive_user_message(self, message: UserMessage):
|
|
99
|
+
"""
|
|
100
|
+
This method gracefully waits for the agent to become RUNNING
|
|
101
|
+
if it's in the process of starting up, ensuring the queues are
|
|
102
|
+
initialized before we put a message into them.
|
|
103
|
+
"""
|
|
104
|
+
logger.info(f"Agent {self.agent_id} received user message")
|
|
105
|
+
|
|
106
|
+
# If the agent is not started (or ended), begin the start process
|
|
107
|
+
if self.status in [AgentStatus.NOT_STARTED, AgentStatus.ENDED]:
|
|
108
|
+
self.start()
|
|
109
|
+
|
|
110
|
+
# If the agent is still starting, wait until it transitions to RUNNING
|
|
111
|
+
while self.status == AgentStatus.STARTING:
|
|
112
|
+
await asyncio.sleep(0.1)
|
|
113
|
+
|
|
114
|
+
if self.status != AgentStatus.RUNNING:
|
|
115
|
+
logger.error(f"Agent is not in a running state: {self.status}")
|
|
116
|
+
return
|
|
117
|
+
|
|
118
|
+
# Now that we are running, safely place the message in the queue
|
|
119
|
+
await self.user_messages.put(message)
|
|
120
|
+
|
|
121
|
+
async def handle_tool_result_messages(self):
|
|
122
|
+
logger.info(f"Agent {self.role} started handling tool result messages")
|
|
123
|
+
while not self.task_completed.is_set() and self.status == AgentStatus.RUNNING:
|
|
124
|
+
try:
|
|
125
|
+
message = await asyncio.wait_for(self.tool_result_messages.get(), timeout=1.0)
|
|
126
|
+
logger.info(f"Agent {self.role} handling tool result message: {message}")
|
|
127
|
+
response = await self.conversation.send_user_message(f"Tool execution result: {message}")
|
|
128
|
+
await self.process_llm_response(response)
|
|
129
|
+
except asyncio.TimeoutError:
|
|
130
|
+
continue
|
|
131
|
+
except asyncio.CancelledError:
|
|
132
|
+
logger.info(f"Tool result handler for agent {self.role} cancelled")
|
|
133
|
+
break
|
|
134
|
+
except Exception as e:
|
|
135
|
+
logger.error(f"Error handling tool result for agent {self.role}: {str(e)}")
|
|
136
|
+
|
|
137
|
+
async def initialize_conversation(self):
|
|
138
|
+
logger.info(f"Initializing conversation for agent: {self.role}")
|
|
139
|
+
self.conversation = Conversation(self.llm)
|
|
140
|
+
|
|
141
|
+
if self.initial_user_message:
|
|
142
|
+
initial_message = self.initial_user_message
|
|
143
|
+
else:
|
|
144
|
+
prompt_content = self.prompt_builder.set_variable_value(
|
|
145
|
+
"external_tools",
|
|
146
|
+
self._get_external_tools_section()
|
|
147
|
+
).build()
|
|
148
|
+
initial_message = UserMessage(content=prompt_content)
|
|
149
|
+
|
|
150
|
+
logger.debug(f"Initial user message for agent {self.role}: {initial_message}")
|
|
151
|
+
initial_llm_response = await self.conversation.send_user_message(
|
|
152
|
+
initial_message.content,
|
|
153
|
+
initial_message.file_paths
|
|
154
|
+
)
|
|
155
|
+
await self.process_llm_response(initial_llm_response)
|
|
156
|
+
|
|
157
|
+
async def process_llm_response(self, response: str) -> None:
|
|
158
|
+
self.emit(EventType.ASSISTANT_RESPONSE, response=response)
|
|
159
|
+
|
|
160
|
+
if self.tools and self.tool_usage_response_parser:
|
|
161
|
+
tool_invocation = self.tool_usage_response_parser.parse_response(response)
|
|
162
|
+
if tool_invocation.is_valid():
|
|
163
|
+
await self.execute_tool(tool_invocation)
|
|
164
|
+
return
|
|
165
|
+
|
|
166
|
+
logger.info(f"Assistant response for agent {self.role}: {response}")
|
|
167
|
+
|
|
168
|
+
async def execute_tool(self, tool_invocation):
|
|
169
|
+
name = tool_invocation.name
|
|
170
|
+
arguments = tool_invocation.arguments
|
|
171
|
+
logger.info(f"Agent {self.role} attempting to execute tool: {name}")
|
|
172
|
+
|
|
173
|
+
tool = next((t for t in self.tools if t.get_name() == name), None)
|
|
174
|
+
if tool:
|
|
175
|
+
try:
|
|
176
|
+
result = await tool.execute(**arguments)
|
|
177
|
+
logger.info(f"Tool '{name}' executed successfully by agent {self.role}. Result: {result}")
|
|
178
|
+
await self.tool_result_messages.put(result)
|
|
179
|
+
except Exception as e:
|
|
180
|
+
error_message = str(e)
|
|
181
|
+
logger.error(f"Error executing tool '{name}' by agent {self.role}: {error_message}")
|
|
182
|
+
await self.tool_result_messages.put(f"Error: {error_message}")
|
|
183
|
+
else:
|
|
184
|
+
logger.warning(f"Tool '{name}' not found for agent {self.role}.")
|
|
185
|
+
|
|
186
|
+
def start(self):
|
|
187
|
+
"""
|
|
188
|
+
Starts the agent by creating a task that runs the main loop (run).
|
|
189
|
+
Sets the AgentStatus to STARTING to prevent message enqueuing before
|
|
190
|
+
the system is fully initialized.
|
|
191
|
+
"""
|
|
192
|
+
if self.status in [AgentStatus.NOT_STARTED, AgentStatus.ENDED]:
|
|
193
|
+
logger.info(f"Starting agent {self.role}")
|
|
194
|
+
self.status = AgentStatus.STARTING
|
|
195
|
+
self._run_task = asyncio.create_task(self.run())
|
|
196
|
+
elif self.status == AgentStatus.STARTING:
|
|
197
|
+
logger.info(f"Agent {self.role} is already in STARTING state.")
|
|
198
|
+
elif self.status == AgentStatus.RUNNING:
|
|
199
|
+
logger.info(f"Agent {self.role} is already running.")
|
|
200
|
+
else:
|
|
201
|
+
logger.warning(f"Agent {self.role} is in an unexpected state: {self.status}")
|
|
202
|
+
|
|
203
|
+
def stop(self):
|
|
204
|
+
if self._run_task and not self._run_task.done():
|
|
205
|
+
self._run_task.cancel()
|
|
206
|
+
|
|
207
|
+
async def cleanup(self):
|
|
208
|
+
while not self.tool_result_messages.empty():
|
|
209
|
+
self.tool_result_messages.get_nowait()
|
|
210
|
+
while not self.user_messages.empty():
|
|
211
|
+
self.user_messages.get_nowait()
|
|
212
|
+
await self.llm.cleanup()
|
|
213
|
+
logger.info(f"Cleanup completed for agent: {self.role}")
|
|
214
|
+
|
|
215
|
+
def set_agent_id_on_tools(self):
|
|
216
|
+
if self.tools:
|
|
217
|
+
for tool in self.tools:
|
|
218
|
+
tool.set_agent_id(self.agent_id)
|
|
219
|
+
|
|
220
|
+
def _get_external_tools_section(self) -> str:
|
|
221
|
+
if not self.tools:
|
|
222
|
+
return ""
|
|
223
|
+
|
|
224
|
+
external_tools_section = ""
|
|
225
|
+
for i, tool in enumerate(self.tools):
|
|
226
|
+
external_tools_section += f" {i + 1} {tool.tool_usage_xml()}\n\n"
|
|
227
|
+
return external_tools_section.strip()
|
|
228
|
+
|
|
229
|
+
def on_task_completed(self, *args, **kwargs):
|
|
230
|
+
logger.info(f"Task completed event received for agent: {self.role}")
|
|
231
|
+
self.task_completed.set()
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from typing import (
|
|
4
|
+
List,
|
|
5
|
+
Optional,
|
|
6
|
+
AsyncGenerator,
|
|
7
|
+
Any,
|
|
8
|
+
NoReturn,
|
|
9
|
+
Union,
|
|
10
|
+
AsyncIterator
|
|
11
|
+
)
|
|
12
|
+
from autobyteus.agent.agent import Agent
|
|
13
|
+
from autobyteus.llm.base_llm import BaseLLM
|
|
14
|
+
from autobyteus.tools.base_tool import BaseTool
|
|
15
|
+
from autobyteus.prompt.prompt_builder import PromptBuilder
|
|
16
|
+
from autobyteus.events.event_types import EventType
|
|
17
|
+
from autobyteus.agent.status import AgentStatus
|
|
18
|
+
from autobyteus.conversation.user_message import UserMessage
|
|
19
|
+
from autobyteus.conversation.conversation import Conversation
|
|
20
|
+
from autobyteus.agent.tool_invocation import ToolInvocation
|
|
21
|
+
|
|
22
|
+
logger = logging.getLogger(__name__)
|
|
23
|
+
|
|
24
|
+
class AsyncAgent(Agent):
|
|
25
|
+
"""
|
|
26
|
+
An asynchronous agent that supports streaming LLM responses while maintaining
|
|
27
|
+
compatibility with the base agent functionality.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(
|
|
31
|
+
self,
|
|
32
|
+
role: str,
|
|
33
|
+
llm: BaseLLM,
|
|
34
|
+
tools: Optional[List[BaseTool]] = None,
|
|
35
|
+
agent_id: Optional[str] = None,
|
|
36
|
+
prompt_builder: Optional[PromptBuilder] = None,
|
|
37
|
+
initial_user_message: Optional[UserMessage] = None
|
|
38
|
+
) -> None:
|
|
39
|
+
"""
|
|
40
|
+
Initialize the AsyncAgent with the given parameters.
|
|
41
|
+
|
|
42
|
+
Args:
|
|
43
|
+
role: The role of the agent
|
|
44
|
+
llm: The language model instance
|
|
45
|
+
tools: List of available tools
|
|
46
|
+
use_xml_parser: Whether to use XML parser for responses
|
|
47
|
+
agent_id: Optional unique identifier for the agent
|
|
48
|
+
prompt_builder: Optional prompt builder instance
|
|
49
|
+
initial_user_message: Optional initial message to start the conversation
|
|
50
|
+
"""
|
|
51
|
+
super().__init__(
|
|
52
|
+
role,
|
|
53
|
+
llm,
|
|
54
|
+
tools,
|
|
55
|
+
agent_id,
|
|
56
|
+
prompt_builder,
|
|
57
|
+
initial_user_message
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
async def initialize_conversation(self) -> None:
|
|
61
|
+
"""Initialize the conversation with initial message or prompt."""
|
|
62
|
+
logger.info(f"Initializing conversation for agent: {self.role}")
|
|
63
|
+
self.conversation = Conversation(self.llm)
|
|
64
|
+
|
|
65
|
+
if self.initial_user_message:
|
|
66
|
+
initial_message = self.initial_user_message
|
|
67
|
+
else:
|
|
68
|
+
prompt_content = self.prompt_builder.set_variable_value(
|
|
69
|
+
"external_tools",
|
|
70
|
+
self._get_external_tools_section()
|
|
71
|
+
).build()
|
|
72
|
+
initial_message = UserMessage(content=prompt_content)
|
|
73
|
+
|
|
74
|
+
logger.debug(f"Initial user message for agent {self.role}: {initial_message}")
|
|
75
|
+
await self.process_streaming_response(
|
|
76
|
+
self.conversation.stream_user_message(
|
|
77
|
+
initial_message.content,
|
|
78
|
+
initial_message.file_paths
|
|
79
|
+
)
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
async def handle_user_messages(self) -> NoReturn:
|
|
83
|
+
"""
|
|
84
|
+
Handle incoming user messages continuously.
|
|
85
|
+
Processes messages using streaming responses.
|
|
86
|
+
"""
|
|
87
|
+
logger.info(f"Agent {self.role} started handling user messages")
|
|
88
|
+
while not self.task_completed.is_set() and self.status == AgentStatus.RUNNING:
|
|
89
|
+
try:
|
|
90
|
+
user_message: UserMessage = await asyncio.wait_for(
|
|
91
|
+
self.user_messages.get(),
|
|
92
|
+
timeout=1.0
|
|
93
|
+
)
|
|
94
|
+
logger.info(f"Agent {self.role} handling user message")
|
|
95
|
+
await self.process_streaming_response(
|
|
96
|
+
self.conversation.stream_user_message(
|
|
97
|
+
user_message.content,
|
|
98
|
+
user_message.file_paths
|
|
99
|
+
)
|
|
100
|
+
)
|
|
101
|
+
except asyncio.TimeoutError:
|
|
102
|
+
continue
|
|
103
|
+
except asyncio.CancelledError:
|
|
104
|
+
logger.info(f"User message handler for agent {self.role} cancelled")
|
|
105
|
+
break
|
|
106
|
+
except Exception as e:
|
|
107
|
+
logger.error(f"Error handling user message for agent {self.role}: {str(e)}")
|
|
108
|
+
|
|
109
|
+
async def handle_tool_result_messages(self) -> NoReturn:
|
|
110
|
+
"""
|
|
111
|
+
Handle tool execution result messages continuously.
|
|
112
|
+
Processes messages using streaming responses.
|
|
113
|
+
"""
|
|
114
|
+
logger.info(f"Agent {self.role} started handling tool result messages")
|
|
115
|
+
while not self.task_completed.is_set() and self.status == AgentStatus.RUNNING:
|
|
116
|
+
try:
|
|
117
|
+
message: str = await asyncio.wait_for(
|
|
118
|
+
self.tool_result_messages.get(),
|
|
119
|
+
timeout=1.0
|
|
120
|
+
)
|
|
121
|
+
logger.info(f"Agent {self.role} handling tool result message: {message}")
|
|
122
|
+
await self.process_streaming_response(
|
|
123
|
+
self.conversation.stream_user_message(
|
|
124
|
+
f"Tool execution result: {message}"
|
|
125
|
+
)
|
|
126
|
+
)
|
|
127
|
+
except asyncio.TimeoutError:
|
|
128
|
+
continue
|
|
129
|
+
except asyncio.CancelledError:
|
|
130
|
+
logger.info(f"Tool result handler for agent {self.role} cancelled")
|
|
131
|
+
break
|
|
132
|
+
except Exception as e:
|
|
133
|
+
logger.error(f"Error handling tool result for agent {self.role}: {str(e)}")
|
|
134
|
+
|
|
135
|
+
async def process_streaming_response(
|
|
136
|
+
self,
|
|
137
|
+
response_stream: AsyncIterator[str]
|
|
138
|
+
) -> None:
|
|
139
|
+
"""
|
|
140
|
+
Process streaming responses from the LLM, emitting each chunk and handling
|
|
141
|
+
tool invocations after receiving the complete response.
|
|
142
|
+
|
|
143
|
+
Args:
|
|
144
|
+
response_stream: AsyncIterator yielding response tokens
|
|
145
|
+
"""
|
|
146
|
+
complete_response: str = ""
|
|
147
|
+
try:
|
|
148
|
+
async for chunk in response_stream:
|
|
149
|
+
# Emit each chunk as it arrives
|
|
150
|
+
self.emit(
|
|
151
|
+
EventType.ASSISTANT_RESPONSE,
|
|
152
|
+
response=chunk,
|
|
153
|
+
is_complete=False
|
|
154
|
+
)
|
|
155
|
+
complete_response += chunk
|
|
156
|
+
# Emit the complete response
|
|
157
|
+
self.emit(
|
|
158
|
+
EventType.ASSISTANT_RESPONSE,
|
|
159
|
+
response=complete_response,
|
|
160
|
+
is_complete=True
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
if self.tools and self.tool_usage_response_parser:
|
|
164
|
+
tool_invocation: ToolInvocation = self.tool_usage_response_parser.parse_response(complete_response)
|
|
165
|
+
if tool_invocation.is_valid():
|
|
166
|
+
await self.execute_tool(tool_invocation)
|
|
167
|
+
return
|
|
168
|
+
|
|
169
|
+
logger.info(f"Assistant response for agent {self.role}: {complete_response}")
|
|
170
|
+
|
|
171
|
+
except Exception as e:
|
|
172
|
+
logger.error(f"Error processing streaming response for agent {self.role}: {str(e)}")
|
|
173
|
+
self.emit(
|
|
174
|
+
EventType.ERROR,
|
|
175
|
+
error=str(e))
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
|
|
2
|
+
import asyncio
|
|
3
|
+
import logging
|
|
4
|
+
from typing import TYPE_CHECKING, Optional, AsyncIterator
|
|
5
|
+
from autobyteus.agent.async_agent import AsyncAgent
|
|
6
|
+
from autobyteus.agent.message.message import Message
|
|
7
|
+
from autobyteus.agent.message.message_types import MessageType
|
|
8
|
+
from autobyteus.agent.message.send_message_to import SendMessageTo
|
|
9
|
+
from autobyteus.agent.status import AgentStatus
|
|
10
|
+
from autobyteus.events.event_types import EventType
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from autobyteus.agent.orchestrator.base_agent_orchestrator import BaseAgentOrchestrator
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
class AsyncGroupAwareAgent(AsyncAgent):
|
|
18
|
+
def __init__(self, *args, **kwargs):
|
|
19
|
+
super().__init__(*args, **kwargs)
|
|
20
|
+
self.agent_orchestrator: Optional['BaseAgentOrchestrator'] = None
|
|
21
|
+
self.incoming_agent_messages: Optional[asyncio.Queue] = None
|
|
22
|
+
logger.info(f"AsyncGroupAwareAgent initialized with role: {self.role}")
|
|
23
|
+
|
|
24
|
+
def _initialize_queues(self):
|
|
25
|
+
if not self._queues_initialized:
|
|
26
|
+
super()._initialize_queues()
|
|
27
|
+
self.incoming_agent_messages = asyncio.Queue()
|
|
28
|
+
logger.info(f"Queues initialized for agent {self.role}")
|
|
29
|
+
|
|
30
|
+
def set_agent_orchestrator(self, agent_orchestrator: 'BaseAgentOrchestrator'):
|
|
31
|
+
self.agent_orchestrator = agent_orchestrator
|
|
32
|
+
if not any(isinstance(tool, SendMessageTo) for tool in self.tools):
|
|
33
|
+
self.tools.append(SendMessageTo(agent_orchestrator))
|
|
34
|
+
self.register_task_completion_listener()
|
|
35
|
+
logger.info(f"Agent orchestrator set for agent {self.role}")
|
|
36
|
+
|
|
37
|
+
def register_task_completion_listener(self):
|
|
38
|
+
"""Register to listen for task completion events from the orchestrator specifically"""
|
|
39
|
+
logger.info(f"Registering task completion listener for agent: {self.role}")
|
|
40
|
+
if self.agent_orchestrator:
|
|
41
|
+
self.subscribe_from(
|
|
42
|
+
self.agent_orchestrator,
|
|
43
|
+
EventType.TASK_COMPLETED,
|
|
44
|
+
self.on_task_completed
|
|
45
|
+
)
|
|
46
|
+
else:
|
|
47
|
+
logger.warning(f"Cannot register task completion listener for agent {self.role}: orchestrator not set")
|
|
48
|
+
|
|
49
|
+
async def receive_agent_message(self, message: Message):
|
|
50
|
+
logger.info(f"Agent {self.agent_id} received message from {message.sender_agent_id}")
|
|
51
|
+
if not self._queues_initialized:
|
|
52
|
+
self._initialize_queues()
|
|
53
|
+
await self.incoming_agent_messages.put(message)
|
|
54
|
+
if self.status != AgentStatus.RUNNING:
|
|
55
|
+
self.start()
|
|
56
|
+
|
|
57
|
+
async def run(self):
|
|
58
|
+
try:
|
|
59
|
+
logger.info(f"Agent {self.role} entering running state")
|
|
60
|
+
self._initialize_queues()
|
|
61
|
+
self._initialize_task_completed()
|
|
62
|
+
await self.initialize_conversation()
|
|
63
|
+
|
|
64
|
+
self.status = AgentStatus.RUNNING
|
|
65
|
+
|
|
66
|
+
user_message_handler = asyncio.create_task(self.handle_user_messages())
|
|
67
|
+
tool_result_handler = asyncio.create_task(self.handle_tool_result_messages())
|
|
68
|
+
agent_message_handler = asyncio.create_task(self.handle_agent_messages())
|
|
69
|
+
|
|
70
|
+
done, pending = await asyncio.wait(
|
|
71
|
+
[agent_message_handler, tool_result_handler, user_message_handler, self.task_completed.wait()],
|
|
72
|
+
return_when=asyncio.FIRST_COMPLETED
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
for task in pending:
|
|
76
|
+
task.cancel()
|
|
77
|
+
|
|
78
|
+
await asyncio.gather(*pending, return_exceptions=True)
|
|
79
|
+
|
|
80
|
+
except Exception as e:
|
|
81
|
+
logger.error(f"Error in agent {self.role} execution: {str(e)}")
|
|
82
|
+
self.status = AgentStatus.ERROR
|
|
83
|
+
finally:
|
|
84
|
+
self.status = AgentStatus.ENDED
|
|
85
|
+
await self.cleanup()
|
|
86
|
+
|
|
87
|
+
async def handle_agent_messages(self):
|
|
88
|
+
logger.info(f"Agent {self.role} started handling agent messages")
|
|
89
|
+
while not self.task_completed.is_set() and self.status == AgentStatus.RUNNING:
|
|
90
|
+
try:
|
|
91
|
+
message = await asyncio.wait_for(self.incoming_agent_messages.get(), timeout=1.0)
|
|
92
|
+
logger.info(f"{self.role} processing message from {message.sender_agent_id}")
|
|
93
|
+
|
|
94
|
+
if message.message_type == MessageType.TASK_RESULT:
|
|
95
|
+
self.agent_orchestrator.handle_task_completed(message.sender_agent_id)
|
|
96
|
+
|
|
97
|
+
await self.process_streaming_response(
|
|
98
|
+
self.conversation.stream_user_message(
|
|
99
|
+
f"Message from sender_agent_id {message.sender_agent_id}, content {message.content}"
|
|
100
|
+
)
|
|
101
|
+
)
|
|
102
|
+
except asyncio.TimeoutError:
|
|
103
|
+
continue
|
|
104
|
+
except asyncio.CancelledError:
|
|
105
|
+
logger.info(f"Agent message handler for agent {self.role} cancelled")
|
|
106
|
+
break
|
|
107
|
+
except Exception as e:
|
|
108
|
+
logger.error(f"Error handling agent message for agent {self.role}: {str(e)}")
|
|
109
|
+
|
|
110
|
+
async def execute_tool(self, tool_invocation):
|
|
111
|
+
name = tool_invocation.name
|
|
112
|
+
arguments = tool_invocation.arguments
|
|
113
|
+
logger.info(f"Agent {self.role} attempting to execute tool: {name}")
|
|
114
|
+
|
|
115
|
+
tool = next((t for t in self.tools if t.get_name() == name), None)
|
|
116
|
+
if tool:
|
|
117
|
+
try:
|
|
118
|
+
result = await tool.execute(**arguments)
|
|
119
|
+
logger.info(f"Tool '{name}' executed successfully by agent {self.role}. Result: {result}")
|
|
120
|
+
if not isinstance(tool, SendMessageTo):
|
|
121
|
+
await self.tool_result_messages.put(result)
|
|
122
|
+
else:
|
|
123
|
+
logger.info(f"SendMessageTo tool executed by agent {self.role}: {result}")
|
|
124
|
+
except Exception as e:
|
|
125
|
+
error_message = str(e)
|
|
126
|
+
logger.error(f"Error executing tool '{name}' by agent {self.role}: {error_message}")
|
|
127
|
+
if not isinstance(tool, SendMessageTo):
|
|
128
|
+
await self.tool_result_messages.put(f"Error: {error_message}")
|
|
129
|
+
else:
|
|
130
|
+
logger.warning(f"Tool '{name}' not found for agent {self.role}.")
|
|
131
|
+
|
|
132
|
+
async def cleanup(self):
|
|
133
|
+
await super().cleanup()
|
|
134
|
+
while not self.incoming_agent_messages.empty():
|
|
135
|
+
self.incoming_agent_messages.get_nowait()
|
|
136
|
+
logger.info(f"Cleanup completed for async group-aware agent: {self.role}")
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import asyncio
|
|
2
|
+
import logging
|
|
3
|
+
from typing import TYPE_CHECKING, Optional, AsyncIterator
|
|
4
|
+
from autobyteus.agent.async_agent import AsyncAgent
|
|
5
|
+
from autobyteus.agent.message.message import Message
|
|
6
|
+
from autobyteus.agent.message.message_types import MessageType
|
|
7
|
+
from autobyteus.agent.message.send_message_to import SendMessageTo
|
|
8
|
+
from autobyteus.agent.status import AgentStatus
|
|
9
|
+
from autobyteus.events.event_types import EventType
|
|
10
|
+
|
|
11
|
+
if TYPE_CHECKING:
|
|
12
|
+
from autobyteus.agent.orchestrator.base_agent_orchestrator import BaseAgentOrchestrator
|
|
13
|
+
|
|
14
|
+
logger = logging.getLogger(__name__)
|
|
15
|
+
|
|
16
|
+
class AsyncGroupAwareAgent(AsyncAgent):
|
|
17
|
+
def __init__(self, *args, **kwargs):
|
|
18
|
+
super().__init__(*args, **kwargs)
|
|
19
|
+
self.agent_orchestrator: Optional['BaseAgentOrchestrator'] = None
|
|
20
|
+
self.incoming_agent_messages: Optional[asyncio.Queue] = None
|
|
21
|
+
logger.info(f"AsyncGroupAwareAgent initialized with role: {self.role}")
|
|
22
|
+
|
|
23
|
+
def _initialize_queues(self):
|
|
24
|
+
if not self._queues_initialized:
|
|
25
|
+
super()._initialize_queues()
|
|
26
|
+
self.incoming_agent_messages = asyncio.Queue()
|
|
27
|
+
logger.info(f"Queues initialized for agent {self.role}")
|
|
28
|
+
|
|
29
|
+
def set_agent_orchestrator(self, agent_orchestrator: 'BaseAgentOrchestrator'):
|
|
30
|
+
self.agent_orchestrator = agent_orchestrator
|
|
31
|
+
if not any(isinstance(tool, SendMessageTo) for tool in self.tools):
|
|
32
|
+
self.tools.append(SendMessageTo(agent_orchestrator))
|
|
33
|
+
logger.info(f"Agent orchestrator set for agent {self.role}")
|
|
34
|
+
|
|
35
|
+
async def receive_agent_message(self, message: Message):
|
|
36
|
+
logger.info(f"Agent {self.agent_id} received message from {message.sender_agent_id}")
|
|
37
|
+
if not self._queues_initialized:
|
|
38
|
+
self._initialize_queues()
|
|
39
|
+
await self.incoming_agent_messages.put(message)
|
|
40
|
+
if self.status != AgentStatus.RUNNING:
|
|
41
|
+
self.start()
|
|
42
|
+
|
|
43
|
+
async def run(self):
|
|
44
|
+
try:
|
|
45
|
+
logger.info(f"Agent {self.role} entering running state")
|
|
46
|
+
self._initialize_queues()
|
|
47
|
+
self._initialize_task_completed()
|
|
48
|
+
await self.initialize_conversation()
|
|
49
|
+
|
|
50
|
+
self.status = AgentStatus.RUNNING
|
|
51
|
+
|
|
52
|
+
user_message_handler = asyncio.create_task(self.handle_user_messages())
|
|
53
|
+
tool_result_handler = asyncio.create_task(self.handle_tool_result_messages())
|
|
54
|
+
agent_message_handler = asyncio.create_task(self.handle_agent_messages())
|
|
55
|
+
|
|
56
|
+
done, pending = await asyncio.wait(
|
|
57
|
+
[agent_message_handler, tool_result_handler, user_message_handler, self.task_completed.wait()],
|
|
58
|
+
return_when=asyncio.FIRST_COMPLETED
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
for task in pending:
|
|
62
|
+
task.cancel()
|
|
63
|
+
|
|
64
|
+
await asyncio.gather(*pending, return_exceptions=True)
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
logger.error(f"Error in agent {self.role} execution: {str(e)}")
|
|
68
|
+
self.status = AgentStatus.ERROR
|
|
69
|
+
finally:
|
|
70
|
+
self.status = AgentStatus.ENDED
|
|
71
|
+
await self.cleanup()
|
|
72
|
+
|
|
73
|
+
async def handle_agent_messages(self):
|
|
74
|
+
logger.info(f"Agent {self.role} started handling agent messages")
|
|
75
|
+
while not self.task_completed.is_set() and self.status == AgentStatus.RUNNING:
|
|
76
|
+
try:
|
|
77
|
+
message = await asyncio.wait_for(self.incoming_agent_messages.get(), timeout=1.0)
|
|
78
|
+
logger.info(f"{self.role} processing message from {message.sender_agent_id}")
|
|
79
|
+
|
|
80
|
+
if message.message_type == MessageType.TASK_RESULT:
|
|
81
|
+
self.agent_orchestrator.handle_task_completed(message.sender_agent_id)
|
|
82
|
+
|
|
83
|
+
await self.process_streaming_response(
|
|
84
|
+
self.conversation.stream_user_message(
|
|
85
|
+
f"Message from sender_agent_id {message.sender_agent_id}, content {message.content}"
|
|
86
|
+
)
|
|
87
|
+
)
|
|
88
|
+
except asyncio.TimeoutError:
|
|
89
|
+
continue
|
|
90
|
+
except asyncio.CancelledError:
|
|
91
|
+
logger.info(f"Agent message handler for agent {self.role} cancelled")
|
|
92
|
+
break
|
|
93
|
+
except Exception as e:
|
|
94
|
+
logger.error(f"Error handling agent message for agent {self.role}: {str(e)}")
|
|
95
|
+
|
|
96
|
+
async def execute_tool(self, tool_invocation):
|
|
97
|
+
name = tool_invocation.name
|
|
98
|
+
arguments = tool_invocation.arguments
|
|
99
|
+
logger.info(f"Agent {self.role} attempting to execute tool: {name}")
|
|
100
|
+
|
|
101
|
+
tool = next((t for t in self.tools if t.get_name() == name), None)
|
|
102
|
+
if tool:
|
|
103
|
+
try:
|
|
104
|
+
result = await tool.execute(**arguments)
|
|
105
|
+
logger.info(f"Tool '{name}' executed successfully by agent {self.role}. Result: {result}")
|
|
106
|
+
if not isinstance(tool, SendMessageTo):
|
|
107
|
+
await self.tool_result_messages.put(result)
|
|
108
|
+
else:
|
|
109
|
+
logger.info(f"SendMessageTo tool executed by agent {self.role}: {result}")
|
|
110
|
+
except Exception as e:
|
|
111
|
+
error_message = str(e)
|
|
112
|
+
logger.error(f"Error executing tool '{name}' by agent {self.role}: {error_message}")
|
|
113
|
+
if not isinstance(tool, SendMessageTo):
|
|
114
|
+
await self.tool_result_messages.put(f"Error: {error_message}")
|
|
115
|
+
else:
|
|
116
|
+
logger.warning(f"Tool '{name}' not found for agent {self.role}.")
|
|
117
|
+
|
|
118
|
+
async def cleanup(self):
|
|
119
|
+
await super().cleanup()
|
|
120
|
+
while not self.incoming_agent_messages.empty():
|
|
121
|
+
self.incoming_agent_messages.get_nowait()
|
|
122
|
+
logger.info(f"Cleanup completed for async group-aware agent: {self.role}")
|