autobyteus 1.1.7__py3-none-any.whl → 1.1.9__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/system_prompt_processing_step.py +6 -2
- autobyteus/agent/handlers/inter_agent_message_event_handler.py +17 -19
- autobyteus/agent/handlers/llm_complete_response_received_event_handler.py +6 -3
- autobyteus/agent/handlers/tool_result_event_handler.py +86 -23
- autobyteus/agent/handlers/user_input_message_event_handler.py +19 -10
- autobyteus/agent/hooks/base_phase_hook.py +17 -0
- autobyteus/agent/hooks/hook_registry.py +15 -27
- autobyteus/agent/input_processor/base_user_input_processor.py +17 -1
- autobyteus/agent/input_processor/processor_registry.py +15 -27
- autobyteus/agent/llm_response_processor/base_processor.py +17 -1
- autobyteus/agent/llm_response_processor/processor_registry.py +15 -24
- autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +14 -0
- autobyteus/agent/message/agent_input_user_message.py +15 -2
- autobyteus/agent/message/send_message_to.py +1 -1
- autobyteus/agent/processor_option.py +17 -0
- autobyteus/agent/sender_type.py +1 -0
- autobyteus/agent/system_prompt_processor/base_processor.py +17 -1
- autobyteus/agent/system_prompt_processor/processor_registry.py +15 -27
- autobyteus/agent/system_prompt_processor/tool_manifest_injector_processor.py +10 -0
- autobyteus/agent/tool_execution_result_processor/base_processor.py +17 -1
- autobyteus/agent/tool_execution_result_processor/processor_registry.py +15 -1
- autobyteus/agent/workspace/base_workspace.py +1 -1
- autobyteus/agent/workspace/workspace_definition.py +1 -1
- autobyteus/agent_team/bootstrap_steps/team_context_initialization_step.py +1 -1
- autobyteus/agent_team/streaming/agent_team_stream_event_payloads.py +2 -2
- autobyteus/agent_team/task_notification/__init__.py +4 -0
- autobyteus/agent_team/task_notification/activation_policy.py +70 -0
- autobyteus/agent_team/task_notification/system_event_driven_agent_task_notifier.py +56 -122
- autobyteus/agent_team/task_notification/task_activator.py +66 -0
- autobyteus/cli/agent_team_tui/state.py +17 -20
- autobyteus/cli/agent_team_tui/widgets/focus_pane.py +1 -1
- autobyteus/cli/agent_team_tui/widgets/task_board_panel.py +1 -1
- autobyteus/events/event_types.py +2 -2
- autobyteus/llm/api/gemini_llm.py +45 -54
- autobyteus/llm/api/qwen_llm.py +25 -0
- autobyteus/llm/autobyteus_provider.py +8 -2
- autobyteus/llm/llm_factory.py +16 -0
- autobyteus/multimedia/audio/api/autobyteus_audio_client.py +4 -1
- autobyteus/multimedia/audio/api/gemini_audio_client.py +84 -153
- autobyteus/multimedia/audio/audio_client_factory.py +47 -22
- autobyteus/multimedia/audio/audio_model.py +13 -6
- autobyteus/multimedia/audio/autobyteus_audio_provider.py +8 -2
- autobyteus/multimedia/audio/base_audio_client.py +3 -1
- autobyteus/multimedia/image/api/autobyteus_image_client.py +12 -5
- autobyteus/multimedia/image/api/gemini_image_client.py +72 -130
- autobyteus/multimedia/image/api/openai_image_client.py +4 -2
- autobyteus/multimedia/image/autobyteus_image_provider.py +8 -2
- autobyteus/multimedia/image/base_image_client.py +6 -2
- autobyteus/multimedia/image/image_client_factory.py +20 -19
- autobyteus/multimedia/image/image_model.py +13 -6
- autobyteus/multimedia/providers.py +1 -0
- autobyteus/task_management/__init__.py +9 -10
- autobyteus/task_management/base_task_board.py +14 -6
- autobyteus/task_management/converters/__init__.py +0 -2
- autobyteus/task_management/converters/task_board_converter.py +7 -16
- autobyteus/task_management/events.py +6 -6
- autobyteus/task_management/in_memory_task_board.py +48 -38
- autobyteus/task_management/schemas/__init__.py +2 -2
- autobyteus/task_management/schemas/{plan_definition.py → task_definition.py} +5 -6
- autobyteus/task_management/schemas/task_status_report.py +0 -1
- autobyteus/task_management/task.py +60 -0
- autobyteus/task_management/tools/__init__.py +4 -2
- autobyteus/task_management/tools/get_my_tasks.py +80 -0
- autobyteus/task_management/tools/get_task_board_status.py +3 -3
- autobyteus/task_management/tools/publish_task.py +77 -0
- autobyteus/task_management/tools/publish_tasks.py +74 -0
- autobyteus/task_management/tools/update_task_status.py +5 -5
- autobyteus/tools/__init__.py +3 -1
- autobyteus/tools/base_tool.py +4 -4
- autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +1 -1
- autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +1 -1
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +1 -1
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +1 -1
- autobyteus/tools/browser/standalone/navigate_to.py +1 -1
- autobyteus/tools/browser/standalone/web_page_pdf_generator.py +1 -1
- autobyteus/tools/browser/standalone/webpage_image_downloader.py +1 -1
- autobyteus/tools/browser/standalone/webpage_reader.py +1 -1
- autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +1 -1
- autobyteus/tools/functional_tool.py +1 -1
- autobyteus/tools/google_search.py +1 -1
- autobyteus/tools/image_downloader.py +1 -1
- autobyteus/tools/mcp/factory.py +1 -1
- autobyteus/tools/mcp/schema_mapper.py +1 -1
- autobyteus/tools/mcp/tool.py +1 -1
- autobyteus/tools/multimedia/__init__.py +2 -0
- autobyteus/tools/multimedia/audio_tools.py +10 -20
- autobyteus/tools/multimedia/image_tools.py +21 -22
- autobyteus/tools/multimedia/media_reader_tool.py +117 -0
- autobyteus/tools/pydantic_schema_converter.py +1 -1
- autobyteus/tools/registry/tool_definition.py +1 -1
- autobyteus/tools/timer.py +1 -1
- autobyteus/tools/tool_meta.py +1 -1
- autobyteus/tools/usage/formatters/default_json_example_formatter.py +1 -1
- autobyteus/tools/usage/formatters/default_xml_example_formatter.py +1 -1
- autobyteus/tools/usage/formatters/default_xml_schema_formatter.py +59 -3
- autobyteus/tools/usage/formatters/gemini_json_example_formatter.py +1 -1
- autobyteus/tools/usage/formatters/google_json_example_formatter.py +1 -1
- autobyteus/tools/usage/formatters/openai_json_example_formatter.py +1 -1
- autobyteus/{tools → utils}/parameter_schema.py +1 -1
- {autobyteus-1.1.7.dist-info → autobyteus-1.1.9.dist-info}/METADATA +2 -2
- {autobyteus-1.1.7.dist-info → autobyteus-1.1.9.dist-info}/RECORD +105 -99
- examples/run_poem_writer.py +1 -1
- autobyteus/task_management/converters/task_plan_converter.py +0 -48
- autobyteus/task_management/task_plan.py +0 -110
- autobyteus/task_management/tools/publish_task_plan.py +0 -101
- {autobyteus-1.1.7.dist-info → autobyteus-1.1.9.dist-info}/WHEEL +0 -0
- {autobyteus-1.1.7.dist-info → autobyteus-1.1.9.dist-info}/licenses/LICENSE +0 -0
- {autobyteus-1.1.7.dist-info → autobyteus-1.1.9.dist-info}/top_level.txt +0 -0
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# file: autobyteus/autobyteus/agent_team/task_notification/system_event_driven_agent_task_notifier.py
|
|
2
|
-
import asyncio
|
|
3
2
|
import logging
|
|
4
|
-
from typing import
|
|
3
|
+
from typing import Union, TYPE_CHECKING
|
|
5
4
|
|
|
6
5
|
from autobyteus.events.event_types import EventType
|
|
7
|
-
from autobyteus.
|
|
8
|
-
from autobyteus.agent.message import AgentInputUserMessage
|
|
9
|
-
from autobyteus.task_management.events import TaskPlanPublishedEvent, TaskStatusUpdatedEvent
|
|
6
|
+
from autobyteus.task_management.events import TasksAddedEvent, TaskStatusUpdatedEvent
|
|
10
7
|
from autobyteus.task_management.base_task_board import TaskStatus
|
|
11
|
-
|
|
8
|
+
|
|
9
|
+
# Import the new, separated components
|
|
10
|
+
from .activation_policy import ActivationPolicy
|
|
11
|
+
from .task_activator import TaskActivator
|
|
12
12
|
|
|
13
13
|
if TYPE_CHECKING:
|
|
14
14
|
from autobyteus.task_management.base_task_board import BaseTaskBoard
|
|
@@ -18,8 +18,11 @@ logger = logging.getLogger(__name__)
|
|
|
18
18
|
|
|
19
19
|
class SystemEventDrivenAgentTaskNotifier:
|
|
20
20
|
"""
|
|
21
|
-
An internal component that monitors a TaskBoard and
|
|
22
|
-
|
|
21
|
+
An internal component that monitors a TaskBoard and orchestrates agent
|
|
22
|
+
activation based on task runnability.
|
|
23
|
+
|
|
24
|
+
This class acts as a conductor, delegating the logic for *when* to activate
|
|
25
|
+
to an ActivationPolicy and the action of *how* to activate to a TaskActivator.
|
|
23
26
|
"""
|
|
24
27
|
def __init__(self, task_board: 'BaseTaskBoard', team_manager: 'TeamManager'):
|
|
25
28
|
"""
|
|
@@ -27,138 +30,69 @@ class SystemEventDrivenAgentTaskNotifier:
|
|
|
27
30
|
|
|
28
31
|
Args:
|
|
29
32
|
task_board: The team's shared task board instance.
|
|
30
|
-
team_manager: The team's manager for
|
|
33
|
+
team_manager: The team's manager for activating agents.
|
|
31
34
|
"""
|
|
32
35
|
if not task_board or not team_manager:
|
|
33
36
|
raise ValueError("TaskBoard and TeamManager are required for the notifier.")
|
|
34
37
|
|
|
35
38
|
self._task_board = task_board
|
|
36
39
|
self._team_manager = team_manager
|
|
37
|
-
|
|
38
|
-
|
|
40
|
+
|
|
41
|
+
# Instantiate the components that hold the actual logic and action
|
|
42
|
+
self._policy = ActivationPolicy(team_id=self._team_manager.team_id)
|
|
43
|
+
self._activator = TaskActivator(team_manager=self._team_manager)
|
|
44
|
+
|
|
45
|
+
logger.info(f"SystemEventDrivenAgentTaskNotifier orchestrator initialized for team '{self._team_manager.team_id}'.")
|
|
39
46
|
|
|
40
47
|
def start_monitoring(self):
|
|
41
48
|
"""
|
|
42
49
|
Subscribes to task board events to begin monitoring for runnable tasks.
|
|
43
|
-
This should be called once during the agent team's bootstrap process.
|
|
44
50
|
"""
|
|
45
|
-
self._task_board.subscribe(
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
EventType.TASK_BOARD_STATUS_UPDATED,
|
|
51
|
-
self._handle_task_board_update
|
|
52
|
-
)
|
|
53
|
-
logger.info(f"Team '{self._team_manager.team_id}': Task notifier is now monitoring TaskBoard events.")
|
|
54
|
-
|
|
55
|
-
async def _handle_task_board_update(self, payload: Union[TaskPlanPublishedEvent, TaskStatusUpdatedEvent], **kwargs):
|
|
51
|
+
self._task_board.subscribe(EventType.TASK_BOARD_TASKS_ADDED, self._handle_tasks_changed)
|
|
52
|
+
self._task_board.subscribe(EventType.TASK_BOARD_STATUS_UPDATED, self._handle_tasks_changed)
|
|
53
|
+
logger.info(f"Team '{self._team_manager.team_id}': Task notifier orchestrator is now monitoring TaskBoard events.")
|
|
54
|
+
|
|
55
|
+
async def _handle_tasks_changed(self, payload: Union[TasksAddedEvent, TaskStatusUpdatedEvent], **kwargs):
|
|
56
56
|
"""
|
|
57
|
-
|
|
58
|
-
payload to decide when to check for and notify agents of newly runnable tasks.
|
|
57
|
+
Orchestrates the agent activation workflow upon any change to the task board.
|
|
59
58
|
"""
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
self._dispatched_task_ids.clear()
|
|
63
|
-
await self._scan_and_notify_all_runnable_tasks()
|
|
64
|
-
|
|
65
|
-
elif isinstance(payload, TaskStatusUpdatedEvent):
|
|
66
|
-
# Only trigger a check for dependent tasks if a task has been completed,
|
|
67
|
-
# as this is the only status change that can unblock dependent tasks.
|
|
68
|
-
if payload.new_status == TaskStatus.COMPLETED:
|
|
69
|
-
logger.info(f"Team '{self._team_manager.team_id}': Task '{payload.task_id}' completed. Checking for newly unblocked dependent tasks.")
|
|
70
|
-
await self._check_and_notify_dependent_tasks(payload.task_id)
|
|
71
|
-
else:
|
|
72
|
-
logger.debug(f"Team '{self._team_manager.team_id}': Task '{payload.task_id}' status updated to '{payload.new_status.value}'. No dependent task check needed.")
|
|
73
|
-
else:
|
|
74
|
-
# This case should ideally not be hit with the new strong typing, but is kept as a safeguard.
|
|
75
|
-
logger.warning(f"Team '{self._team_manager.team_id}': Task notifier received an unhandled payload type: {type(payload)}")
|
|
59
|
+
team_id = self._team_manager.team_id
|
|
60
|
+
logger.info(f"Team '{team_id}': Task board changed ({type(payload).__name__}). Orchestrating activation check.")
|
|
76
61
|
|
|
62
|
+
# If a new batch of tasks was added, it's a new "wave" of work.
|
|
63
|
+
# We must reset the policy's memory of who has been activated.
|
|
64
|
+
if isinstance(payload, TasksAddedEvent):
|
|
65
|
+
logger.info(f"Team '{team_id}': New tasks added. Resetting activation policy.")
|
|
66
|
+
self._policy.reset()
|
|
77
67
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
if
|
|
82
|
-
|
|
83
|
-
if not getattr(self._task_board, 'current_plan', None):
|
|
68
|
+
# 1. DATA FETCHING: Get the current state of runnable tasks from the board.
|
|
69
|
+
runnable_tasks = self._task_board.get_next_runnable_tasks()
|
|
70
|
+
|
|
71
|
+
if not runnable_tasks:
|
|
72
|
+
logger.debug(f"Team '{team_id}': No runnable tasks found after change. No action needed.")
|
|
84
73
|
return
|
|
85
74
|
|
|
86
|
-
|
|
87
|
-
|
|
75
|
+
# 2. POLICY DECISION: Ask the policy to decide which agents to activate.
|
|
76
|
+
# The policy contains all the complex state and logic.
|
|
77
|
+
agents_to_activate = self._policy.determine_activations(runnable_tasks)
|
|
88
78
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
# Now, check if this child task is fully runnable (all its parents are done)
|
|
93
|
-
all_deps_met = all(
|
|
94
|
-
task_statuses.get(dep_id) == TaskStatus.COMPLETED for dep_id in child_task.dependencies
|
|
95
|
-
)
|
|
96
|
-
|
|
97
|
-
if all_deps_met and child_task.task_id not in self._dispatched_task_ids:
|
|
98
|
-
await self._dispatch_notification_for_task(child_task)
|
|
79
|
+
if not agents_to_activate:
|
|
80
|
+
logger.info(f"Team '{team_id}': Runnable tasks exist, but policy determined no new agents need activation.")
|
|
81
|
+
return
|
|
99
82
|
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if task.task_id
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
async def _dispatch_notification_for_task(self, task: Task):
|
|
113
|
-
"""
|
|
114
|
-
Constructs and sends a context-rich notification for a single runnable task
|
|
115
|
-
by treating it as a user message to trigger the full processing pipeline.
|
|
116
|
-
It tags the message with metadata to indicate its system origin.
|
|
117
|
-
"""
|
|
118
|
-
try:
|
|
119
|
-
team_id = self._team_manager.team_id
|
|
120
|
-
logger.info(f"Team '{team_id}': Dispatching notification for runnable task '{task.task_name}' to assignee '{task.assignee_name}'.")
|
|
121
|
-
|
|
122
|
-
context_from_parents = []
|
|
123
|
-
if task.dependencies:
|
|
124
|
-
parent_task_deliverables_info = []
|
|
125
|
-
for dep_id in task.dependencies:
|
|
126
|
-
parent_task = getattr(self._task_board, '_task_map', {}).get(dep_id)
|
|
127
|
-
if parent_task and parent_task.file_deliverables:
|
|
128
|
-
deliverables_str = "\n".join(
|
|
129
|
-
[f" - File: {d.file_path}, Summary: {d.summary}" for d in parent_task.file_deliverables]
|
|
130
|
-
)
|
|
131
|
-
parent_task_deliverables_info.append(
|
|
132
|
-
f"The parent task '{parent_task.task_name}' produced the following deliverables:\n{deliverables_str}"
|
|
133
|
-
)
|
|
134
|
-
|
|
135
|
-
if parent_task_deliverables_info:
|
|
136
|
-
context_from_parents.append(
|
|
137
|
-
"Your task is now unblocked. Here is the context from the completed parent task(s):\n" +
|
|
138
|
-
"\n\n".join(parent_task_deliverables_info)
|
|
83
|
+
# 3. ACTION EXECUTION: Tell the activator to perform the activations.
|
|
84
|
+
for agent_name in agents_to_activate:
|
|
85
|
+
# First, update the status for all of that agent's runnable tasks to QUEUED.
|
|
86
|
+
# This is a state change action, which the orchestrator is responsible for.
|
|
87
|
+
agent_runnable_tasks = [t for t in runnable_tasks if t.assignee_name == agent_name]
|
|
88
|
+
for task in agent_runnable_tasks:
|
|
89
|
+
# We only need to queue tasks that are NOT_STARTED.
|
|
90
|
+
if self._task_board.task_statuses.get(task.task_id) == TaskStatus.NOT_STARTED:
|
|
91
|
+
self._task_board.update_task_status(
|
|
92
|
+
task_id=task.task_id,
|
|
93
|
+
status=TaskStatus.QUEUED,
|
|
94
|
+
agent_name="SystemTaskNotifier"
|
|
139
95
|
)
|
|
140
|
-
|
|
141
|
-
message_parts: List[str] = [f"Your task '{task.task_name}' is now ready to start."]
|
|
142
|
-
if context_from_parents:
|
|
143
|
-
message_parts.extend(context_from_parents)
|
|
144
|
-
|
|
145
|
-
message_parts.append(f"\nYour task description:\n{task.description}")
|
|
146
|
-
|
|
147
|
-
content = "\n\n".join(message_parts)
|
|
148
96
|
|
|
149
|
-
#
|
|
150
|
-
|
|
151
|
-
content=content,
|
|
152
|
-
metadata={'source': 'system_task_notifier'}
|
|
153
|
-
)
|
|
154
|
-
event = ProcessUserMessageEvent(
|
|
155
|
-
user_message=user_message,
|
|
156
|
-
target_agent_name=task.assignee_name
|
|
157
|
-
)
|
|
158
|
-
|
|
159
|
-
# Use the existing method for dispatching user messages.
|
|
160
|
-
await self._team_manager.dispatch_user_message_to_agent(event)
|
|
161
|
-
self._dispatched_task_ids.add(task.task_id)
|
|
162
|
-
|
|
163
|
-
except Exception as e:
|
|
164
|
-
logger.error(f"Team '{self._team_manager.team_id}': Failed to dispatch notification for task '{task.task_id}': {e}", exc_info=True)
|
|
97
|
+
# Now, trigger the single activation notification.
|
|
98
|
+
await self._activator.activate_agent(agent_name)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/agent_team/task_notification/task_activator.py
|
|
2
|
+
"""
|
|
3
|
+
Defines the component responsible for the action of activating an agent.
|
|
4
|
+
"""
|
|
5
|
+
import logging
|
|
6
|
+
from typing import TYPE_CHECKING
|
|
7
|
+
|
|
8
|
+
from autobyteus.agent.message import AgentInputUserMessage
|
|
9
|
+
from autobyteus.agent_team.events import ProcessUserMessageEvent
|
|
10
|
+
from autobyteus.agent.sender_type import SenderType, TASK_NOTIFIER_SENDER_ID
|
|
11
|
+
|
|
12
|
+
if TYPE_CHECKING:
|
|
13
|
+
from autobyteus.agent_team.context.team_manager import TeamManager
|
|
14
|
+
|
|
15
|
+
logger = logging.getLogger(__name__)
|
|
16
|
+
|
|
17
|
+
class TaskActivator:
|
|
18
|
+
"""
|
|
19
|
+
A component with the single responsibility of activating an agent.
|
|
20
|
+
|
|
21
|
+
Activation involves ensuring the agent is running and sending it a
|
|
22
|
+
standardized "start work" message.
|
|
23
|
+
"""
|
|
24
|
+
def __init__(self, team_manager: 'TeamManager'):
|
|
25
|
+
"""
|
|
26
|
+
Initializes the TaskActivator.
|
|
27
|
+
|
|
28
|
+
Args:
|
|
29
|
+
team_manager: The team's manager, used to start agents and dispatch messages.
|
|
30
|
+
"""
|
|
31
|
+
if not team_manager:
|
|
32
|
+
raise ValueError("TaskActivator requires a valid TeamManager instance.")
|
|
33
|
+
self._team_manager = team_manager
|
|
34
|
+
logger.debug(f"TaskActivator initialized for team '{self._team_manager.team_id}'.")
|
|
35
|
+
|
|
36
|
+
async def activate_agent(self, agent_name: str):
|
|
37
|
+
"""
|
|
38
|
+
Activates a specific agent by ensuring it is ready and sending it a
|
|
39
|
+
generic "start work" notification.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
agent_name: The unique name of the agent to activate.
|
|
43
|
+
"""
|
|
44
|
+
team_id = self._team_manager.team_id
|
|
45
|
+
try:
|
|
46
|
+
logger.info(f"Team '{team_id}': TaskActivator is activating agent '{agent_name}'.")
|
|
47
|
+
|
|
48
|
+
# This ensures the agent is started and ready to receive the message.
|
|
49
|
+
await self._team_manager.ensure_node_is_ready(agent_name)
|
|
50
|
+
|
|
51
|
+
notification_message = AgentInputUserMessage(
|
|
52
|
+
content="You have new tasks in your queue. Please review your task list using your tools and begin your work.",
|
|
53
|
+
sender_type=SenderType.SYSTEM,
|
|
54
|
+
metadata={'sender_id': TASK_NOTIFIER_SENDER_ID}
|
|
55
|
+
)
|
|
56
|
+
event = ProcessUserMessageEvent(
|
|
57
|
+
user_message=notification_message,
|
|
58
|
+
target_agent_name=agent_name
|
|
59
|
+
)
|
|
60
|
+
await self._team_manager.dispatch_user_message_to_agent(event)
|
|
61
|
+
|
|
62
|
+
logger.info(f"Team '{team_id}': Successfully sent activation notification to '{agent_name}'.")
|
|
63
|
+
|
|
64
|
+
except Exception as e:
|
|
65
|
+
# FIXED: Removed "TaskActivator" from the log message to align with the unit test assertion.
|
|
66
|
+
logger.error(f"Team '{team_id}': Failed to activate agent '{agent_name}': {e}", exc_info=True)
|
|
@@ -16,8 +16,8 @@ from autobyteus.agent.streaming.stream_event_payloads import (
|
|
|
16
16
|
)
|
|
17
17
|
from autobyteus.agent_team.streaming.agent_team_stream_events import AgentTeamStreamEvent
|
|
18
18
|
from autobyteus.agent_team.streaming.agent_team_stream_event_payloads import AgentEventRebroadcastPayload, SubTeamEventRebroadcastPayload, AgentTeamPhaseTransitionData
|
|
19
|
-
from autobyteus.task_management.
|
|
20
|
-
from autobyteus.task_management.events import
|
|
19
|
+
from autobyteus.task_management.task import Task
|
|
20
|
+
from autobyteus.task_management.events import TasksAddedEvent, TaskStatusUpdatedEvent
|
|
21
21
|
from autobyteus.task_management.base_task_board import TaskStatus
|
|
22
22
|
|
|
23
23
|
logger = logging.getLogger(__name__)
|
|
@@ -86,29 +86,26 @@ class TUIStateStore:
|
|
|
86
86
|
self._team_event_history[parent_name].append(event)
|
|
87
87
|
|
|
88
88
|
if event.event_source_type == "TASK_BOARD":
|
|
89
|
-
# The 'parent_name' argument holds the friendly name of the team (or sub-team)
|
|
90
|
-
# that is the context for this event. This is the key we use for UI state.
|
|
91
89
|
team_name_key = parent_name
|
|
92
|
-
if isinstance(event.data,
|
|
93
|
-
self._task_plans[team_name_key] =
|
|
94
|
-
|
|
95
|
-
self.
|
|
96
|
-
|
|
90
|
+
if isinstance(event.data, TasksAddedEvent):
|
|
91
|
+
if team_name_key not in self._task_plans: self._task_plans[team_name_key] = []
|
|
92
|
+
if team_name_key not in self._task_statuses: self._task_statuses[team_name_key] = {}
|
|
93
|
+
self._task_plans[team_name_key].extend(event.data.tasks)
|
|
94
|
+
for task in event.data.tasks:
|
|
95
|
+
self._task_statuses[team_name_key][task.task_id] = TaskStatus.NOT_STARTED
|
|
96
|
+
logger.debug(f"TUI State: Added {len(event.data.tasks)} tasks to board for '{team_name_key}'.")
|
|
97
|
+
|
|
97
98
|
elif isinstance(event.data, TaskStatusUpdatedEvent):
|
|
98
|
-
|
|
99
|
-
if team_name_key not in self._task_statuses:
|
|
100
|
-
self._task_statuses[team_name_key] = {}
|
|
99
|
+
if team_name_key not in self._task_statuses: self._task_statuses[team_name_key] = {}
|
|
101
100
|
self._task_statuses[team_name_key][event.data.task_id] = event.data.new_status
|
|
102
101
|
logger.debug(f"TUI State: Updated status for task '{event.data.task_id}' in team '{team_name_key}' to {event.data.new_status}.")
|
|
103
102
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
logger.debug(f"TUI State: Synced deliverables for task '{event.data.task_id}' in team '{team_name_key}'.")
|
|
111
|
-
break
|
|
103
|
+
if event.data.deliverables is not None and team_name_key in self._task_plans:
|
|
104
|
+
for task in self._task_plans[team_name_key]:
|
|
105
|
+
if task.task_id == event.data.task_id:
|
|
106
|
+
task.file_deliverables = event.data.deliverables
|
|
107
|
+
logger.debug(f"TUI State: Synced deliverables for task '{event.data.task_id}' in team '{team_name_key}'.")
|
|
108
|
+
break
|
|
112
109
|
return
|
|
113
110
|
|
|
114
111
|
if isinstance(event.data, AgentEventRebroadcastPayload):
|
|
@@ -15,7 +15,7 @@ from textual.containers import VerticalScroll, Horizontal
|
|
|
15
15
|
from autobyteus.agent.phases import AgentOperationalPhase
|
|
16
16
|
from autobyteus.agent_team.phases import AgentTeamOperationalPhase
|
|
17
17
|
from autobyteus.task_management.base_task_board import TaskStatus
|
|
18
|
-
from autobyteus.task_management.
|
|
18
|
+
from autobyteus.task_management.task import Task
|
|
19
19
|
from autobyteus.agent.streaming.stream_events import StreamEvent as AgentStreamEvent, StreamEventType as AgentStreamEventType
|
|
20
20
|
from autobyteus.agent.streaming.stream_event_payloads import (
|
|
21
21
|
AgentOperationalPhaseTransitionData, AssistantChunkData, AssistantCompleteResponseData,
|
|
@@ -6,7 +6,7 @@ from rich.panel import Panel
|
|
|
6
6
|
from rich.text import Text
|
|
7
7
|
from textual.widgets import Static
|
|
8
8
|
|
|
9
|
-
from autobyteus.task_management.
|
|
9
|
+
from autobyteus.task_management.task import Task
|
|
10
10
|
from autobyteus.task_management.base_task_board import TaskStatus
|
|
11
11
|
from .shared import TASK_STATUS_ICONS, LOG_ICON
|
|
12
12
|
|
autobyteus/events/event_types.py
CHANGED
|
@@ -47,8 +47,8 @@ class EventType(Enum):
|
|
|
47
47
|
TEAM_STREAM_EVENT = "team_stream_event" # For unified agent team event stream
|
|
48
48
|
|
|
49
49
|
# --- Task Board Events ---
|
|
50
|
-
|
|
51
|
-
TASK_BOARD_STATUS_UPDATED = "
|
|
50
|
+
TASK_BOARD_TASKS_ADDED = "task_board.tasks.added"
|
|
51
|
+
TASK_BOARD_STATUS_UPDATED = "task_board.status.updated"
|
|
52
52
|
|
|
53
53
|
def __str__(self):
|
|
54
54
|
return self.value
|
autobyteus/llm/api/gemini_llm.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import logging
|
|
2
|
-
from typing import Dict, List, AsyncGenerator, Any
|
|
3
|
-
|
|
2
|
+
from typing import Dict, Optional, List, AsyncGenerator, Any
|
|
3
|
+
from google import genai
|
|
4
|
+
from google.genai import types as genai_types
|
|
4
5
|
import os
|
|
5
6
|
from autobyteus.llm.models import LLMModel
|
|
6
7
|
from autobyteus.llm.base_llm import BaseLLM
|
|
@@ -13,66 +14,60 @@ from autobyteus.llm.user_message import LLMUserMessage
|
|
|
13
14
|
logger = logging.getLogger(__name__)
|
|
14
15
|
|
|
15
16
|
def _format_gemini_history(messages: List[Message]) -> List[Dict[str, Any]]:
|
|
16
|
-
"""
|
|
17
|
-
Formats internal message history for the Gemini API.
|
|
18
|
-
This function remains compatible with the older library.
|
|
19
|
-
"""
|
|
17
|
+
"""Formats internal message history for the Gemini API."""
|
|
20
18
|
history = []
|
|
21
|
-
# System message is handled separately in the
|
|
19
|
+
# System message is handled separately in the new API
|
|
22
20
|
for msg in messages:
|
|
23
21
|
if msg.role in [MessageRole.USER, MessageRole.ASSISTANT]:
|
|
22
|
+
# NOTE: This history conversion will need to be updated for multimodal messages
|
|
24
23
|
role = 'model' if msg.role == MessageRole.ASSISTANT else 'user'
|
|
24
|
+
# The `parts` must be a list of dictionaries (Part objects), not a list of strings.
|
|
25
25
|
history.append({"role": role, "parts": [{"text": msg.content}]})
|
|
26
26
|
return history
|
|
27
27
|
|
|
28
28
|
class GeminiLLM(BaseLLM):
|
|
29
29
|
def __init__(self, model: LLMModel = None, llm_config: LLMConfig = None):
|
|
30
|
+
self.generation_config_dict = {
|
|
31
|
+
"response_mime_type": "text/plain",
|
|
32
|
+
}
|
|
33
|
+
|
|
30
34
|
if model is None:
|
|
31
|
-
model = LLMModel['gemini-2.5-flash']
|
|
35
|
+
model = LLMModel['gemini-2.5-flash']
|
|
32
36
|
if llm_config is None:
|
|
33
37
|
llm_config = LLMConfig()
|
|
34
|
-
|
|
38
|
+
|
|
35
39
|
super().__init__(model=model, llm_config=llm_config)
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
self.initialize()
|
|
39
|
-
|
|
40
|
-
system_instruction = self.system_message if self.system_message else None
|
|
41
|
-
|
|
42
|
-
self.model = genai.GenerativeModel(
|
|
43
|
-
model_name=self.model.value,
|
|
44
|
-
system_instruction=system_instruction
|
|
45
|
-
)
|
|
40
|
+
self.client = self.initialize()
|
|
41
|
+
self.async_client = self.client.aio
|
|
46
42
|
|
|
47
|
-
@
|
|
48
|
-
def initialize():
|
|
49
|
-
"""
|
|
50
|
-
CHANGED: This method now configures the genai library with the API key
|
|
51
|
-
instead of creating a client instance.
|
|
52
|
-
"""
|
|
43
|
+
@classmethod
|
|
44
|
+
def initialize(cls) -> genai.client.Client:
|
|
53
45
|
api_key = os.environ.get("GEMINI_API_KEY")
|
|
54
46
|
if not api_key:
|
|
55
47
|
logger.error("GEMINI_API_KEY environment variable is not set.")
|
|
56
48
|
raise ValueError("GEMINI_API_KEY environment variable is not set.")
|
|
57
49
|
try:
|
|
58
|
-
genai.
|
|
50
|
+
return genai.Client()
|
|
59
51
|
except Exception as e:
|
|
60
|
-
logger.error(f"Failed to
|
|
61
|
-
raise ValueError(f"Failed to
|
|
52
|
+
logger.error(f"Failed to initialize Gemini client: {str(e)}")
|
|
53
|
+
raise ValueError(f"Failed to initialize Gemini client: {str(e)}")
|
|
62
54
|
|
|
63
|
-
def _get_generation_config(self) ->
|
|
64
|
-
"""
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
""
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
55
|
+
def _get_generation_config(self) -> genai_types.GenerateContentConfig:
|
|
56
|
+
"""Builds the generation config, handling special cases like 'thinking'."""
|
|
57
|
+
config = self.generation_config_dict.copy()
|
|
58
|
+
|
|
59
|
+
thinking_config = None
|
|
60
|
+
if "flash" in self.model.value:
|
|
61
|
+
thinking_config = genai_types.ThinkingConfig(thinking_budget=0)
|
|
62
|
+
|
|
63
|
+
# System instruction is now part of the config
|
|
64
|
+
system_instruction = self.system_message if self.system_message else None
|
|
65
|
+
|
|
66
|
+
return genai_types.GenerateContentConfig(
|
|
67
|
+
**config,
|
|
68
|
+
thinking_config=thinking_config,
|
|
69
|
+
system_instruction=system_instruction
|
|
70
|
+
)
|
|
76
71
|
|
|
77
72
|
async def _send_user_message_to_llm(self, user_message: LLMUserMessage, **kwargs) -> CompleteResponse:
|
|
78
73
|
self.add_user_message(user_message)
|
|
@@ -81,20 +76,19 @@ class GeminiLLM(BaseLLM):
|
|
|
81
76
|
history = _format_gemini_history(self.messages)
|
|
82
77
|
generation_config = self._get_generation_config()
|
|
83
78
|
|
|
84
|
-
|
|
85
|
-
|
|
79
|
+
response = await self.async_client.models.generate_content(
|
|
80
|
+
model=f"models/{self.model.value}",
|
|
86
81
|
contents=history,
|
|
87
|
-
|
|
82
|
+
config=generation_config,
|
|
88
83
|
)
|
|
89
84
|
|
|
90
85
|
assistant_message = response.text
|
|
91
86
|
self.add_assistant_message(assistant_message)
|
|
92
87
|
|
|
93
|
-
# CHANGED: Token usage is extracted from 'usage_metadata'.
|
|
94
88
|
token_usage = TokenUsage(
|
|
95
|
-
prompt_tokens=
|
|
96
|
-
completion_tokens=
|
|
97
|
-
total_tokens=
|
|
89
|
+
prompt_tokens=0,
|
|
90
|
+
completion_tokens=0,
|
|
91
|
+
total_tokens=0
|
|
98
92
|
)
|
|
99
93
|
|
|
100
94
|
return CompleteResponse(
|
|
@@ -113,11 +107,10 @@ class GeminiLLM(BaseLLM):
|
|
|
113
107
|
history = _format_gemini_history(self.messages)
|
|
114
108
|
generation_config = self._get_generation_config()
|
|
115
109
|
|
|
116
|
-
|
|
117
|
-
|
|
110
|
+
response_stream = await self.async_client.models.generate_content_stream(
|
|
111
|
+
model=f"models/{self.model.value}",
|
|
118
112
|
contents=history,
|
|
119
|
-
|
|
120
|
-
stream=True
|
|
113
|
+
config=generation_config,
|
|
121
114
|
)
|
|
122
115
|
|
|
123
116
|
async for chunk in response_stream:
|
|
@@ -130,8 +123,6 @@ class GeminiLLM(BaseLLM):
|
|
|
130
123
|
|
|
131
124
|
self.add_assistant_message(complete_response)
|
|
132
125
|
|
|
133
|
-
# NOTE: The old library's async stream does not easily expose token usage.
|
|
134
|
-
# Keeping it at 0, consistent with your original implementation.
|
|
135
126
|
token_usage = TokenUsage(
|
|
136
127
|
prompt_tokens=0,
|
|
137
128
|
completion_tokens=0,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
from typing import Optional
|
|
3
|
+
from autobyteus.llm.models import LLMModel
|
|
4
|
+
from autobyteus.llm.utils.llm_config import LLMConfig
|
|
5
|
+
from autobyteus.llm.api.openai_compatible_llm import OpenAICompatibleLLM
|
|
6
|
+
|
|
7
|
+
logger = logging.getLogger(__name__)
|
|
8
|
+
|
|
9
|
+
class QwenLLM(OpenAICompatibleLLM):
|
|
10
|
+
def __init__(self, model: LLMModel = None, llm_config: LLMConfig = None):
|
|
11
|
+
if model is None:
|
|
12
|
+
model = LLMModel['qwen3-max-preview']
|
|
13
|
+
if llm_config is None:
|
|
14
|
+
llm_config = LLMConfig()
|
|
15
|
+
|
|
16
|
+
super().__init__(
|
|
17
|
+
model=model,
|
|
18
|
+
llm_config=llm_config,
|
|
19
|
+
api_key_env_var="DASHSCOPE_API_KEY",
|
|
20
|
+
base_url="https://dashscope-intl.aliyuncs.com/compatible-mode/v1"
|
|
21
|
+
)
|
|
22
|
+
logger.info(f"QwenLLM initialized with model: {self.model}")
|
|
23
|
+
|
|
24
|
+
async def cleanup(self):
|
|
25
|
+
await super().cleanup()
|
|
@@ -19,7 +19,9 @@ class AutobyteusModelProvider:
|
|
|
19
19
|
|
|
20
20
|
@staticmethod
|
|
21
21
|
def _get_hosts() -> List[str]:
|
|
22
|
-
"""
|
|
22
|
+
"""
|
|
23
|
+
Gets Autobyteus LLM server hosts from env vars. Skips discovery if no host is configured.
|
|
24
|
+
"""
|
|
23
25
|
hosts_str = os.getenv('AUTOBYTEUS_LLM_SERVER_HOSTS')
|
|
24
26
|
if hosts_str:
|
|
25
27
|
return [host.strip() for host in hosts_str.split(',')]
|
|
@@ -28,7 +30,7 @@ class AutobyteusModelProvider:
|
|
|
28
30
|
if legacy_host:
|
|
29
31
|
return [legacy_host]
|
|
30
32
|
|
|
31
|
-
return [
|
|
33
|
+
return []
|
|
32
34
|
|
|
33
35
|
@staticmethod
|
|
34
36
|
def discover_and_register():
|
|
@@ -37,6 +39,10 @@ class AutobyteusModelProvider:
|
|
|
37
39
|
from autobyteus.llm.llm_factory import LLMFactory
|
|
38
40
|
|
|
39
41
|
hosts = AutobyteusModelProvider._get_hosts()
|
|
42
|
+
if not hosts:
|
|
43
|
+
logger.info("No Autobyteus LLM server hosts configured. Skipping Autobyteus LLM model discovery.")
|
|
44
|
+
return
|
|
45
|
+
|
|
40
46
|
total_registered_count = 0
|
|
41
47
|
|
|
42
48
|
for host_url in hosts:
|
autobyteus/llm/llm_factory.py
CHANGED
|
@@ -17,6 +17,7 @@ from autobyteus.llm.api.deepseek_llm import DeepSeekLLM
|
|
|
17
17
|
from autobyteus.llm.api.gemini_llm import GeminiLLM
|
|
18
18
|
from autobyteus.llm.api.grok_llm import GrokLLM
|
|
19
19
|
from autobyteus.llm.api.kimi_llm import KimiLLM
|
|
20
|
+
from autobyteus.llm.api.qwen_llm import QwenLLM
|
|
20
21
|
from autobyteus.llm.ollama_provider import OllamaModelProvider
|
|
21
22
|
from autobyteus.llm.lmstudio_provider import LMStudioModelProvider
|
|
22
23
|
from autobyteus.utils.singleton import SingletonMeta
|
|
@@ -344,6 +345,21 @@ class LLMFactory(metaclass=SingletonMeta):
|
|
|
344
345
|
pricing_config=TokenPricingConfig(27.59, 27.59)
|
|
345
346
|
)
|
|
346
347
|
),
|
|
348
|
+
# QWEN Provider Models
|
|
349
|
+
LLMModel(
|
|
350
|
+
name="qwen3-max",
|
|
351
|
+
value="qwen-max",
|
|
352
|
+
provider=LLMProvider.QWEN,
|
|
353
|
+
llm_class=QwenLLM,
|
|
354
|
+
canonical_name="qwen3-max",
|
|
355
|
+
default_config=LLMConfig(
|
|
356
|
+
token_limit=262144,
|
|
357
|
+
pricing_config=TokenPricingConfig(
|
|
358
|
+
input_token_pricing=2.4,
|
|
359
|
+
output_token_pricing=12.0
|
|
360
|
+
)
|
|
361
|
+
)
|
|
362
|
+
),
|
|
347
363
|
]
|
|
348
364
|
for model in supported_models:
|
|
349
365
|
LLMFactory.register_model(model)
|
|
@@ -26,7 +26,8 @@ class AutobyteusAudioClient(BaseAudioClient):
|
|
|
26
26
|
async def generate_speech(
|
|
27
27
|
self,
|
|
28
28
|
prompt: str,
|
|
29
|
-
generation_config: Optional[Dict[str, Any]] = None
|
|
29
|
+
generation_config: Optional[Dict[str, Any]] = None,
|
|
30
|
+
**kwargs
|
|
30
31
|
) -> SpeechGenerationResponse:
|
|
31
32
|
"""
|
|
32
33
|
Generates speech by calling the generate_speech endpoint on the remote Autobyteus server.
|
|
@@ -36,6 +37,8 @@ class AutobyteusAudioClient(BaseAudioClient):
|
|
|
36
37
|
|
|
37
38
|
model_name_for_server = self.model.name
|
|
38
39
|
|
|
40
|
+
# Note: The underlying autobyteus_client.generate_speech does not currently accept **kwargs.
|
|
41
|
+
# They are accepted here for interface consistency and future-proofing.
|
|
39
42
|
response_data = await self.autobyteus_client.generate_speech(
|
|
40
43
|
model_name=model_name_for_server,
|
|
41
44
|
prompt=prompt,
|