autobyteus 1.1.3__py3-none-any.whl → 1.1.4__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/agent.py +1 -1
- autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +4 -2
- autobyteus/agent/context/agent_config.py +36 -5
- autobyteus/agent/events/worker_event_dispatcher.py +1 -2
- autobyteus/agent/handlers/inter_agent_message_event_handler.py +1 -1
- autobyteus/agent/handlers/llm_user_message_ready_event_handler.py +2 -2
- autobyteus/agent/handlers/tool_result_event_handler.py +48 -20
- autobyteus/agent/handlers/user_input_message_event_handler.py +1 -1
- autobyteus/agent/input_processor/__init__.py +1 -7
- autobyteus/agent/message/context_file_type.py +6 -0
- autobyteus/agent/message/send_message_to.py +68 -99
- autobyteus/agent/phases/discover.py +2 -1
- autobyteus/agent/runtime/agent_worker.py +1 -0
- autobyteus/agent/tool_execution_result_processor/__init__.py +9 -0
- autobyteus/agent/tool_execution_result_processor/base_processor.py +46 -0
- autobyteus/agent/tool_execution_result_processor/processor_definition.py +36 -0
- autobyteus/agent/tool_execution_result_processor/processor_meta.py +36 -0
- autobyteus/agent/tool_execution_result_processor/processor_registry.py +70 -0
- autobyteus/agent/workspace/base_workspace.py +17 -2
- autobyteus/cli/__init__.py +1 -1
- autobyteus/cli/cli_display.py +1 -1
- autobyteus/cli/workflow_tui/__init__.py +4 -0
- autobyteus/cli/workflow_tui/app.py +210 -0
- autobyteus/cli/workflow_tui/state.py +189 -0
- autobyteus/cli/workflow_tui/widgets/__init__.py +6 -0
- autobyteus/cli/workflow_tui/widgets/agent_list_sidebar.py +149 -0
- autobyteus/cli/workflow_tui/widgets/focus_pane.py +335 -0
- autobyteus/cli/workflow_tui/widgets/logo.py +27 -0
- autobyteus/cli/workflow_tui/widgets/renderables.py +70 -0
- autobyteus/cli/workflow_tui/widgets/shared.py +51 -0
- autobyteus/cli/workflow_tui/widgets/status_bar.py +14 -0
- autobyteus/events/event_types.py +3 -0
- autobyteus/llm/api/lmstudio_llm.py +37 -0
- autobyteus/llm/api/openai_compatible_llm.py +20 -3
- autobyteus/llm/llm_factory.py +2 -0
- autobyteus/llm/lmstudio_provider.py +89 -0
- autobyteus/llm/providers.py +1 -0
- autobyteus/llm/token_counter/token_counter_factory.py +2 -0
- autobyteus/tools/__init__.py +2 -0
- autobyteus/tools/ask_user_input.py +2 -1
- autobyteus/tools/bash/bash_executor.py +2 -1
- autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +2 -0
- autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +3 -0
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +3 -0
- autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +3 -0
- autobyteus/tools/browser/standalone/google_search_ui.py +2 -0
- autobyteus/tools/browser/standalone/navigate_to.py +2 -0
- autobyteus/tools/browser/standalone/web_page_pdf_generator.py +3 -0
- autobyteus/tools/browser/standalone/webpage_image_downloader.py +3 -0
- autobyteus/tools/browser/standalone/webpage_reader.py +2 -0
- autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +3 -0
- autobyteus/tools/file/file_reader.py +36 -9
- autobyteus/tools/file/file_writer.py +37 -9
- autobyteus/tools/functional_tool.py +5 -4
- autobyteus/tools/image_downloader.py +2 -0
- autobyteus/tools/mcp/tool_registrar.py +3 -1
- autobyteus/tools/pdf_downloader.py +2 -1
- autobyteus/tools/registry/tool_definition.py +12 -8
- autobyteus/tools/registry/tool_registry.py +50 -2
- autobyteus/tools/timer.py +2 -0
- autobyteus/tools/tool_category.py +14 -4
- autobyteus/tools/tool_meta.py +6 -1
- autobyteus/tools/tool_origin.py +10 -0
- autobyteus/workflow/agentic_workflow.py +93 -0
- autobyteus/{agent/workflow → workflow}/base_agentic_workflow.py +19 -27
- autobyteus/workflow/bootstrap_steps/__init__.py +20 -0
- autobyteus/workflow/bootstrap_steps/agent_tool_injection_step.py +34 -0
- autobyteus/workflow/bootstrap_steps/base_workflow_bootstrap_step.py +23 -0
- autobyteus/workflow/bootstrap_steps/coordinator_initialization_step.py +41 -0
- autobyteus/workflow/bootstrap_steps/coordinator_prompt_preparation_step.py +108 -0
- autobyteus/workflow/bootstrap_steps/workflow_bootstrapper.py +50 -0
- autobyteus/workflow/bootstrap_steps/workflow_runtime_queue_initialization_step.py +25 -0
- autobyteus/workflow/context/__init__.py +17 -0
- autobyteus/workflow/context/team_manager.py +147 -0
- autobyteus/workflow/context/workflow_config.py +30 -0
- autobyteus/workflow/context/workflow_context.py +61 -0
- autobyteus/workflow/context/workflow_node_config.py +76 -0
- autobyteus/workflow/context/workflow_runtime_state.py +53 -0
- autobyteus/workflow/events/__init__.py +29 -0
- autobyteus/workflow/events/workflow_event_dispatcher.py +39 -0
- autobyteus/workflow/events/workflow_events.py +53 -0
- autobyteus/workflow/events/workflow_input_event_queue_manager.py +21 -0
- autobyteus/workflow/exceptions.py +8 -0
- autobyteus/workflow/factory/__init__.py +9 -0
- autobyteus/workflow/factory/workflow_factory.py +99 -0
- autobyteus/workflow/handlers/__init__.py +19 -0
- autobyteus/workflow/handlers/base_workflow_event_handler.py +16 -0
- autobyteus/workflow/handlers/inter_agent_message_request_event_handler.py +61 -0
- autobyteus/workflow/handlers/lifecycle_workflow_event_handler.py +27 -0
- autobyteus/workflow/handlers/process_user_message_event_handler.py +46 -0
- autobyteus/workflow/handlers/tool_approval_workflow_event_handler.py +39 -0
- autobyteus/workflow/handlers/workflow_event_handler_registry.py +23 -0
- autobyteus/workflow/phases/__init__.py +11 -0
- autobyteus/workflow/phases/workflow_operational_phase.py +19 -0
- autobyteus/workflow/phases/workflow_phase_manager.py +48 -0
- autobyteus/workflow/runtime/__init__.py +13 -0
- autobyteus/workflow/runtime/workflow_runtime.py +82 -0
- autobyteus/workflow/runtime/workflow_worker.py +117 -0
- autobyteus/workflow/shutdown_steps/__init__.py +17 -0
- autobyteus/workflow/shutdown_steps/agent_team_shutdown_step.py +42 -0
- autobyteus/workflow/shutdown_steps/base_workflow_shutdown_step.py +16 -0
- autobyteus/workflow/shutdown_steps/bridge_cleanup_step.py +28 -0
- autobyteus/workflow/shutdown_steps/sub_workflow_shutdown_step.py +41 -0
- autobyteus/workflow/shutdown_steps/workflow_shutdown_orchestrator.py +35 -0
- autobyteus/workflow/streaming/__init__.py +26 -0
- autobyteus/workflow/streaming/agent_event_bridge.py +48 -0
- autobyteus/workflow/streaming/agent_event_multiplexer.py +70 -0
- autobyteus/workflow/streaming/workflow_event_bridge.py +50 -0
- autobyteus/workflow/streaming/workflow_event_notifier.py +83 -0
- autobyteus/workflow/streaming/workflow_event_stream.py +33 -0
- autobyteus/workflow/streaming/workflow_stream_event_payloads.py +28 -0
- autobyteus/workflow/streaming/workflow_stream_events.py +45 -0
- autobyteus/workflow/utils/__init__.py +9 -0
- autobyteus/workflow/utils/wait_for_idle.py +46 -0
- autobyteus/workflow/workflow_builder.py +151 -0
- {autobyteus-1.1.3.dist-info → autobyteus-1.1.4.dist-info}/METADATA +16 -14
- {autobyteus-1.1.3.dist-info → autobyteus-1.1.4.dist-info}/RECORD +134 -65
- {autobyteus-1.1.3.dist-info → autobyteus-1.1.4.dist-info}/top_level.txt +1 -0
- examples/__init__.py +1 -0
- examples/discover_phase_transitions.py +104 -0
- examples/run_browser_agent.py +260 -0
- examples/run_google_slides_agent.py +286 -0
- examples/run_mcp_browser_client.py +174 -0
- examples/run_mcp_google_slides_client.py +270 -0
- examples/run_mcp_list_tools.py +189 -0
- examples/run_poem_writer.py +274 -0
- examples/run_sqlite_agent.py +293 -0
- examples/workflow/__init__.py +1 -0
- examples/workflow/run_basic_research_workflow.py +189 -0
- examples/workflow/run_code_review_workflow.py +269 -0
- examples/workflow/run_debate_workflow.py +212 -0
- examples/workflow/run_workflow_with_tui.py +153 -0
- autobyteus/agent/context/agent_phase_manager.py +0 -264
- autobyteus/agent/context/phases.py +0 -49
- autobyteus/agent/group/__init__.py +0 -0
- autobyteus/agent/group/agent_group.py +0 -164
- autobyteus/agent/group/agent_group_context.py +0 -81
- autobyteus/agent/input_processor/content_prefixing_input_processor.py +0 -41
- autobyteus/agent/input_processor/metadata_appending_input_processor.py +0 -34
- autobyteus/agent/input_processor/passthrough_input_processor.py +0 -33
- autobyteus/agent/workflow/__init__.py +0 -11
- autobyteus/agent/workflow/agentic_workflow.py +0 -89
- autobyteus/tools/mcp/registrar.py +0 -202
- autobyteus/workflow/simple_task.py +0 -98
- autobyteus/workflow/task.py +0 -147
- autobyteus/workflow/workflow.py +0 -49
- {autobyteus-1.1.3.dist-info → autobyteus-1.1.4.dist-info}/WHEEL +0 -0
- {autobyteus-1.1.3.dist-info → autobyteus-1.1.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/agent/tool_execution_result_processor/processor_definition.py
|
|
2
|
+
import logging
|
|
3
|
+
from typing import Type, TYPE_CHECKING
|
|
4
|
+
|
|
5
|
+
if TYPE_CHECKING:
|
|
6
|
+
from .base_processor import BaseToolExecutionResultProcessor
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
class ToolExecutionResultProcessorDefinition:
|
|
11
|
+
"""
|
|
12
|
+
Represents the definition of a tool execution result processor.
|
|
13
|
+
Contains its registered name and the class itself.
|
|
14
|
+
"""
|
|
15
|
+
def __init__(self, name: str, processor_class: Type['BaseToolExecutionResultProcessor']):
|
|
16
|
+
"""
|
|
17
|
+
Initializes the ToolExecutionResultProcessorDefinition.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
name: The unique registered name of the processor.
|
|
21
|
+
processor_class: The class of the tool execution result processor.
|
|
22
|
+
|
|
23
|
+
Raises:
|
|
24
|
+
ValueError: If name is empty or processor_class is not a type.
|
|
25
|
+
"""
|
|
26
|
+
if not name or not isinstance(name, str):
|
|
27
|
+
raise ValueError("Tool Execution Result Processor name must be a non-empty string.")
|
|
28
|
+
if not isinstance(processor_class, type):
|
|
29
|
+
raise ValueError("processor_class must be a class type.")
|
|
30
|
+
|
|
31
|
+
self.name: str = name
|
|
32
|
+
self.processor_class: Type['BaseToolExecutionResultProcessor'] = processor_class
|
|
33
|
+
logger.debug(f"ToolExecutionResultProcessorDefinition created: name='{name}', class='{processor_class.__name__}'.")
|
|
34
|
+
|
|
35
|
+
def __repr__(self) -> str:
|
|
36
|
+
return f"<ToolExecutionResultProcessorDefinition name='{self.name}', class='{self.processor_class.__name__}'>"
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/agent/tool_execution_result_processor/processor_meta.py
|
|
2
|
+
import logging
|
|
3
|
+
from abc import ABCMeta
|
|
4
|
+
|
|
5
|
+
from .processor_registry import default_tool_execution_result_processor_registry
|
|
6
|
+
from .processor_definition import ToolExecutionResultProcessorDefinition
|
|
7
|
+
|
|
8
|
+
logger = logging.getLogger(__name__)
|
|
9
|
+
|
|
10
|
+
class ToolExecutionResultProcessorMeta(ABCMeta):
|
|
11
|
+
"""
|
|
12
|
+
Metaclass for BaseToolExecutionResultProcessor that automatically registers
|
|
13
|
+
concrete processor subclasses with the default registry.
|
|
14
|
+
"""
|
|
15
|
+
def __init__(cls, name, bases, dct):
|
|
16
|
+
super().__init__(name, bases, dct)
|
|
17
|
+
|
|
18
|
+
if name == 'BaseToolExecutionResultProcessor' or getattr(cls, "__abstractmethods__", None):
|
|
19
|
+
logger.debug(f"Skipping registration for abstract tool execution result processor class: {name}")
|
|
20
|
+
return
|
|
21
|
+
|
|
22
|
+
try:
|
|
23
|
+
processor_name = cls.get_name()
|
|
24
|
+
|
|
25
|
+
if not processor_name or not isinstance(processor_name, str):
|
|
26
|
+
logger.error(f"Tool execution result processor class {name} must return a valid string from get_name(). Skipping.")
|
|
27
|
+
return
|
|
28
|
+
|
|
29
|
+
definition = ToolExecutionResultProcessorDefinition(name=processor_name, processor_class=cls)
|
|
30
|
+
default_tool_execution_result_processor_registry.register_processor(definition)
|
|
31
|
+
logger.info(f"Auto-registered tool execution result processor: '{processor_name}' from class {name}.")
|
|
32
|
+
|
|
33
|
+
except AttributeError as e:
|
|
34
|
+
logger.error(f"Tool execution result processor class {name} is missing 'get_name' method ({e}). Skipping registration.")
|
|
35
|
+
except Exception as e:
|
|
36
|
+
logger.error(f"Failed to auto-register tool execution result processor class {name}: {e}", exc_info=True)
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/agent/tool_execution_result_processor/processor_registry.py
|
|
2
|
+
import logging
|
|
3
|
+
from typing import TYPE_CHECKING, Dict, List, Optional
|
|
4
|
+
|
|
5
|
+
from autobyteus.utils.singleton import SingletonMeta
|
|
6
|
+
from .processor_definition import ToolExecutionResultProcessorDefinition
|
|
7
|
+
|
|
8
|
+
if TYPE_CHECKING:
|
|
9
|
+
from .base_processor import BaseToolExecutionResultProcessor
|
|
10
|
+
|
|
11
|
+
logger = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
class ToolExecutionResultProcessorRegistry(metaclass=SingletonMeta):
|
|
14
|
+
"""
|
|
15
|
+
A singleton registry for ToolExecutionResultProcessorDefinition objects.
|
|
16
|
+
Processors are typically auto-registered via ToolExecutionResultProcessorMeta.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
def __init__(self):
|
|
20
|
+
"""Initializes the registry with an empty store."""
|
|
21
|
+
self._definitions: Dict[str, ToolExecutionResultProcessorDefinition] = {}
|
|
22
|
+
logger.info("ToolExecutionResultProcessorRegistry initialized.")
|
|
23
|
+
|
|
24
|
+
def register_processor(self, definition: ToolExecutionResultProcessorDefinition) -> None:
|
|
25
|
+
"""
|
|
26
|
+
Registers a tool execution result processor definition.
|
|
27
|
+
"""
|
|
28
|
+
if not isinstance(definition, ToolExecutionResultProcessorDefinition):
|
|
29
|
+
raise TypeError(f"Expected ToolExecutionResultProcessorDefinition instance, got {type(definition).__name__}.")
|
|
30
|
+
|
|
31
|
+
processor_name = definition.name
|
|
32
|
+
if processor_name in self._definitions:
|
|
33
|
+
logger.warning(f"Overwriting existing tool execution result processor definition for name: '{processor_name}'.")
|
|
34
|
+
|
|
35
|
+
self._definitions[processor_name] = definition
|
|
36
|
+
logger.info(f"Tool execution result processor definition '{processor_name}' registered successfully.")
|
|
37
|
+
|
|
38
|
+
def get_processor_definition(self, name: str) -> Optional[ToolExecutionResultProcessorDefinition]:
|
|
39
|
+
"""
|
|
40
|
+
Retrieves a processor definition by its name.
|
|
41
|
+
"""
|
|
42
|
+
return self._definitions.get(name)
|
|
43
|
+
|
|
44
|
+
def get_processor(self, name: str) -> Optional['BaseToolExecutionResultProcessor']:
|
|
45
|
+
"""
|
|
46
|
+
Retrieves an instance of a processor by its name.
|
|
47
|
+
"""
|
|
48
|
+
definition = self.get_processor_definition(name)
|
|
49
|
+
if definition:
|
|
50
|
+
try:
|
|
51
|
+
return definition.processor_class()
|
|
52
|
+
except Exception as e:
|
|
53
|
+
logger.error(f"Failed to instantiate tool execution result processor '{name}': {e}", exc_info=True)
|
|
54
|
+
return None
|
|
55
|
+
return None
|
|
56
|
+
|
|
57
|
+
def list_processor_names(self) -> List[str]:
|
|
58
|
+
"""
|
|
59
|
+
Returns a list of names of all registered processor definitions.
|
|
60
|
+
"""
|
|
61
|
+
return list(self._definitions.keys())
|
|
62
|
+
|
|
63
|
+
def get_all_definitions(self) -> Dict[str, ToolExecutionResultProcessorDefinition]:
|
|
64
|
+
"""
|
|
65
|
+
Returns a dictionary of all registered processor definitions.
|
|
66
|
+
"""
|
|
67
|
+
return dict(self._definitions)
|
|
68
|
+
|
|
69
|
+
# Default instance of the registry
|
|
70
|
+
default_tool_execution_result_processor_registry = ToolExecutionResultProcessorRegistry()
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# file: autobyteus/autobyteus/agent/workspace/base_workspace.py
|
|
2
2
|
import logging
|
|
3
|
+
import uuid
|
|
3
4
|
from abc import ABC, abstractmethod
|
|
4
5
|
from typing import Optional, Any, Dict, TYPE_CHECKING
|
|
5
6
|
from autobyteus.tools.parameter_schema import ParameterSchema
|
|
@@ -29,7 +30,8 @@ class BaseAgentWorkspace(ABC, metaclass=WorkspaceMeta):
|
|
|
29
30
|
"""
|
|
30
31
|
self._config: WorkspaceConfig = config or WorkspaceConfig()
|
|
31
32
|
self.context: Optional['AgentContext'] = None
|
|
32
|
-
|
|
33
|
+
self.workspace_id: str = str(uuid.uuid4())
|
|
34
|
+
logger.debug(f"{self.__class__.__name__} instance initialized with ID {self.workspace_id}. Context pending injection.")
|
|
33
35
|
|
|
34
36
|
def set_context(self, context: 'AgentContext'):
|
|
35
37
|
"""
|
|
@@ -53,6 +55,19 @@ class BaseAgentWorkspace(ABC, metaclass=WorkspaceMeta):
|
|
|
53
55
|
"""Configuration for the workspace. Implementations can use this as needed."""
|
|
54
56
|
return self._config
|
|
55
57
|
|
|
58
|
+
@abstractmethod
|
|
59
|
+
def get_base_path(self) -> str:
|
|
60
|
+
"""Returns the base path for the workspace, which can be used to resolve relative paths."""
|
|
61
|
+
pass
|
|
62
|
+
|
|
63
|
+
def get_name(self) -> str:
|
|
64
|
+
"""
|
|
65
|
+
Returns a user-friendly name for this workspace instance.
|
|
66
|
+
By default, it returns the unique workspace ID.
|
|
67
|
+
Subclasses can override this to provide a more descriptive name (e.g., a directory name).
|
|
68
|
+
"""
|
|
69
|
+
return self.workspace_id
|
|
70
|
+
|
|
56
71
|
# --- Methods for self-description ---
|
|
57
72
|
|
|
58
73
|
@classmethod
|
|
@@ -74,4 +89,4 @@ class BaseAgentWorkspace(ABC, metaclass=WorkspaceMeta):
|
|
|
74
89
|
pass
|
|
75
90
|
|
|
76
91
|
def __repr__(self) -> str:
|
|
77
|
-
return f"<{self.__class__.__name__} agent_id='{self.agent_id or 'N/A'}>"
|
|
92
|
+
return f"<{self.__class__.__name__} workspace_id='{self.workspace_id}' agent_id='{self.agent_id or 'N/A'}>"
|
autobyteus/cli/__init__.py
CHANGED
autobyteus/cli/cli_display.py
CHANGED
|
@@ -4,7 +4,7 @@ import sys
|
|
|
4
4
|
from typing import Optional, List, Dict, Any
|
|
5
5
|
import json
|
|
6
6
|
|
|
7
|
-
from autobyteus.agent.
|
|
7
|
+
from autobyteus.agent.phases.phase_enum import AgentOperationalPhase
|
|
8
8
|
from autobyteus.agent.streaming.stream_events import StreamEvent, StreamEventType
|
|
9
9
|
from autobyteus.agent.streaming.stream_event_payloads import (
|
|
10
10
|
AssistantChunkData,
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/cli/workflow_tui/app.py
|
|
2
|
+
"""
|
|
3
|
+
The main Textual application class for the workflow TUI. This class orchestrates
|
|
4
|
+
the UI by reacting to changes in a central state store.
|
|
5
|
+
"""
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
from typing import Dict, Optional, Any
|
|
9
|
+
|
|
10
|
+
from textual.app import App, ComposeResult
|
|
11
|
+
from textual.containers import Horizontal
|
|
12
|
+
from textual.widgets import Header, Static
|
|
13
|
+
from textual.reactive import reactive
|
|
14
|
+
|
|
15
|
+
from autobyteus.workflow.agentic_workflow import AgenticWorkflow
|
|
16
|
+
from autobyteus.workflow.streaming.workflow_event_stream import WorkflowEventStream
|
|
17
|
+
from autobyteus.agent.message.agent_input_user_message import AgentInputUserMessage
|
|
18
|
+
from autobyteus.agent.streaming.stream_events import StreamEventType as AgentStreamEventType
|
|
19
|
+
from autobyteus.agent.streaming.stream_event_payloads import AssistantChunkData
|
|
20
|
+
from autobyteus.workflow.streaming.workflow_stream_event_payloads import AgentEventRebroadcastPayload, WorkflowPhaseTransitionData
|
|
21
|
+
|
|
22
|
+
from .state import TUIStateStore
|
|
23
|
+
from .widgets.agent_list_sidebar import AgentListSidebar
|
|
24
|
+
from .widgets.focus_pane import FocusPane
|
|
25
|
+
from .widgets.status_bar import StatusBar
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
class WorkflowApp(App):
|
|
30
|
+
"""A Textual TUI for interacting with an agentic workflow, built around a central state store."""
|
|
31
|
+
|
|
32
|
+
TITLE = "AutoByteus"
|
|
33
|
+
CSS_PATH = "app.css"
|
|
34
|
+
BINDINGS = [
|
|
35
|
+
("d", "toggle_dark", "Toggle Dark Mode"),
|
|
36
|
+
("q", "quit", "Quit"),
|
|
37
|
+
]
|
|
38
|
+
|
|
39
|
+
focused_node_data: reactive[Optional[Dict[str, Any]]] = reactive(None)
|
|
40
|
+
# The store_version property will trigger UI updates for the sidebar.
|
|
41
|
+
store_version: reactive[int] = reactive(0)
|
|
42
|
+
|
|
43
|
+
def __init__(self, workflow: AgenticWorkflow, **kwargs):
|
|
44
|
+
super().__init__(**kwargs)
|
|
45
|
+
self.workflow = workflow
|
|
46
|
+
self.store = TUIStateStore(workflow=self.workflow)
|
|
47
|
+
self.workflow_stream: Optional[WorkflowEventStream] = None
|
|
48
|
+
# Flag to indicate that the UI needs an update, used for throttling.
|
|
49
|
+
self._ui_update_pending = False
|
|
50
|
+
|
|
51
|
+
def compose(self) -> ComposeResult:
|
|
52
|
+
yield Header(id="app-header", name="AutoByteus Mission Control")
|
|
53
|
+
with Horizontal(id="main-container"):
|
|
54
|
+
yield AgentListSidebar(id="sidebar")
|
|
55
|
+
yield FocusPane(id="focus-pane")
|
|
56
|
+
yield StatusBar()
|
|
57
|
+
|
|
58
|
+
async def on_mount(self) -> None:
|
|
59
|
+
"""Start background tasks when the app is mounted."""
|
|
60
|
+
self.workflow.start()
|
|
61
|
+
self.workflow_stream = WorkflowEventStream(self.workflow)
|
|
62
|
+
|
|
63
|
+
# Initialize the UI with the starting state
|
|
64
|
+
initial_tree = self.store.get_tree_data()
|
|
65
|
+
initial_focus_node = initial_tree.get(self.workflow.name)
|
|
66
|
+
|
|
67
|
+
self.store.set_focused_node(initial_focus_node)
|
|
68
|
+
self.focused_node_data = initial_focus_node
|
|
69
|
+
self.store_version = self.store.version # Trigger initial render
|
|
70
|
+
|
|
71
|
+
self.run_worker(self._listen_for_workflow_events(), name="workflow_listener")
|
|
72
|
+
|
|
73
|
+
# Set up a timer to run the throttled UI updater at ~15 FPS.
|
|
74
|
+
self.set_interval(1 / 15, self._throttled_ui_updater, name="ui_updater")
|
|
75
|
+
logger.info("Workflow TUI mounted, workflow listener and throttled UI updater started.")
|
|
76
|
+
|
|
77
|
+
async def on_unmount(self) -> None:
|
|
78
|
+
if self.workflow and self.workflow.is_running:
|
|
79
|
+
await self.workflow.stop()
|
|
80
|
+
|
|
81
|
+
def _throttled_ui_updater(self) -> None:
|
|
82
|
+
"""
|
|
83
|
+
Periodically checks if the UI state is dirty and, if so, triggers
|
|
84
|
+
reactive updates. It also flushes streaming buffers from the focus pane.
|
|
85
|
+
"""
|
|
86
|
+
focus_pane = self.query_one(FocusPane)
|
|
87
|
+
if self._ui_update_pending:
|
|
88
|
+
self._ui_update_pending = False
|
|
89
|
+
# This is the throttled trigger for the async watcher.
|
|
90
|
+
self.store_version = self.store.version
|
|
91
|
+
|
|
92
|
+
# Always flush the focus pane's streaming buffer for smooth text rendering.
|
|
93
|
+
focus_pane.flush_stream_buffers()
|
|
94
|
+
|
|
95
|
+
async def _listen_for_workflow_events(self) -> None:
|
|
96
|
+
"""A background worker that forwards workflow events to the state store and updates the UI."""
|
|
97
|
+
if not self.workflow_stream: return
|
|
98
|
+
try:
|
|
99
|
+
async for event in self.workflow_stream.all_events():
|
|
100
|
+
# 1. Always update the central state store immediately.
|
|
101
|
+
self.store.process_event(event)
|
|
102
|
+
|
|
103
|
+
# 2. Mark the UI as needing an update for the throttled components.
|
|
104
|
+
self._ui_update_pending = True
|
|
105
|
+
|
|
106
|
+
# 3. Handle real-time, incremental updates directly.
|
|
107
|
+
# This is for components like the FocusPane's text stream, which needs
|
|
108
|
+
# to be as low-latency as possible. The actual UI update is buffered.
|
|
109
|
+
if isinstance(event.data, AgentEventRebroadcastPayload):
|
|
110
|
+
payload = event.data
|
|
111
|
+
agent_name = payload.agent_name
|
|
112
|
+
agent_event = payload.agent_event
|
|
113
|
+
focus_pane = self.query_one(FocusPane)
|
|
114
|
+
|
|
115
|
+
is_currently_focused = (focus_pane._focused_node_data and focus_pane._focused_node_data.get('name') == agent_name)
|
|
116
|
+
|
|
117
|
+
# If the event is for the currently focused agent, send the event
|
|
118
|
+
# to be buffered and eventually rendered.
|
|
119
|
+
if is_currently_focused:
|
|
120
|
+
await focus_pane.add_agent_event(agent_event)
|
|
121
|
+
|
|
122
|
+
except asyncio.CancelledError:
|
|
123
|
+
logger.info("Workflow event listener task was cancelled.")
|
|
124
|
+
except Exception:
|
|
125
|
+
logger.error("Critical error in workflow TUI event listener", exc_info=True)
|
|
126
|
+
finally:
|
|
127
|
+
if self.workflow_stream: await self.workflow_stream.close()
|
|
128
|
+
|
|
129
|
+
# --- Reactive Watchers ---
|
|
130
|
+
|
|
131
|
+
async def watch_store_version(self, new_version: int):
|
|
132
|
+
"""
|
|
133
|
+
Reacts to changes in the store version. This is now called by the throttled
|
|
134
|
+
updater, not on every event. Its main job is to update less-frequently
|
|
135
|
+
changing components like the sidebar tree and workflow dashboards.
|
|
136
|
+
"""
|
|
137
|
+
sidebar = self.query_one(AgentListSidebar)
|
|
138
|
+
focus_pane = self.query_one(FocusPane)
|
|
139
|
+
|
|
140
|
+
# Fetch fresh data from the store for the update
|
|
141
|
+
tree_data = self.store.get_tree_data()
|
|
142
|
+
agent_phases = self.store._agent_phases
|
|
143
|
+
workflow_phases = self.store._workflow_phases
|
|
144
|
+
speaking_agents = self.store._speaking_agents
|
|
145
|
+
|
|
146
|
+
# Update sidebar
|
|
147
|
+
sidebar.update_tree(tree_data, agent_phases, workflow_phases, speaking_agents)
|
|
148
|
+
|
|
149
|
+
# Intelligently update the focus pane
|
|
150
|
+
focused_data = self.focused_node_data
|
|
151
|
+
if focused_data and focused_data.get("type") in ['workflow', 'subworkflow']:
|
|
152
|
+
# If a workflow/subworkflow is focused, its dashboard might be out of date.
|
|
153
|
+
# A full re-render is cheap and ensures consistency for its title and panels.
|
|
154
|
+
history = self.store.get_history_for_node(focused_data['name'], focused_data['type'])
|
|
155
|
+
await focus_pane.update_content(
|
|
156
|
+
node_data=focused_data,
|
|
157
|
+
history=history,
|
|
158
|
+
pending_approval=None,
|
|
159
|
+
all_agent_phases=agent_phases,
|
|
160
|
+
all_workflow_phases=workflow_phases
|
|
161
|
+
)
|
|
162
|
+
elif focused_data and focused_data.get("type") == 'agent':
|
|
163
|
+
# For agents, we only need to update the title status, not the whole log.
|
|
164
|
+
focus_pane.update_current_node_status(agent_phases, workflow_phases)
|
|
165
|
+
|
|
166
|
+
|
|
167
|
+
async def watch_focused_node_data(self, new_node_data: Optional[Dict[str, Any]]):
|
|
168
|
+
"""Reacts to changes in which node is focused. Primarily used for full pane reloads on user click."""
|
|
169
|
+
if not new_node_data: return
|
|
170
|
+
|
|
171
|
+
node_name = new_node_data['name']
|
|
172
|
+
node_type = new_node_data['type']
|
|
173
|
+
|
|
174
|
+
history = self.store.get_history_for_node(node_name, node_type)
|
|
175
|
+
pending_approval = self.store.get_pending_approval_for_agent(node_name) if node_type == 'agent' else None
|
|
176
|
+
|
|
177
|
+
sidebar = self.query_one(AgentListSidebar)
|
|
178
|
+
focus_pane = self.query_one(FocusPane)
|
|
179
|
+
|
|
180
|
+
await focus_pane.update_content(
|
|
181
|
+
node_data=new_node_data,
|
|
182
|
+
history=history,
|
|
183
|
+
pending_approval=pending_approval,
|
|
184
|
+
all_agent_phases=self.store._agent_phases,
|
|
185
|
+
all_workflow_phases=self.store._workflow_phases
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
sidebar.update_selection(node_name)
|
|
189
|
+
|
|
190
|
+
# --- Event Handlers (Actions) ---
|
|
191
|
+
|
|
192
|
+
def on_agent_list_sidebar_node_selected(self, message: AgentListSidebar.NodeSelected):
|
|
193
|
+
"""Handles a node being selected by updating the store and the app's reactive state."""
|
|
194
|
+
self.store.set_focused_node(message.node_data)
|
|
195
|
+
self.focused_node_data = message.node_data
|
|
196
|
+
|
|
197
|
+
async def on_focus_pane_message_submitted(self, message: FocusPane.MessageSubmitted):
|
|
198
|
+
"""Dispatches a user message to the backend model."""
|
|
199
|
+
user_message = AgentInputUserMessage(content=message.text)
|
|
200
|
+
await self.workflow.post_message(message=user_message, target_agent_name=message.agent_name)
|
|
201
|
+
|
|
202
|
+
async def on_focus_pane_approval_submitted(self, message: FocusPane.ApprovalSubmitted):
|
|
203
|
+
"""Dispatches a tool approval to the backend model."""
|
|
204
|
+
self.store.clear_pending_approval(message.agent_name)
|
|
205
|
+
await self.workflow.post_tool_execution_approval(
|
|
206
|
+
agent_name=message.agent_name,
|
|
207
|
+
tool_invocation_id=message.invocation_id,
|
|
208
|
+
is_approved=message.is_approved,
|
|
209
|
+
reason=message.reason,
|
|
210
|
+
)
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# file: autobyteus/autobyteus/cli/workflow_tui/state.py
|
|
2
|
+
"""
|
|
3
|
+
Defines a centralized state store for the TUI application, following state management best practices.
|
|
4
|
+
"""
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Dict, List, Optional, Any
|
|
7
|
+
import copy
|
|
8
|
+
|
|
9
|
+
from autobyteus.agent.context import AgentConfig
|
|
10
|
+
from autobyteus.workflow.agentic_workflow import AgenticWorkflow
|
|
11
|
+
from autobyteus.agent.phases import AgentOperationalPhase
|
|
12
|
+
from autobyteus.workflow.phases import WorkflowOperationalPhase
|
|
13
|
+
from autobyteus.agent.streaming.stream_events import StreamEvent as AgentStreamEvent, StreamEventType as AgentStreamEventType
|
|
14
|
+
from autobyteus.agent.streaming.stream_event_payloads import (
|
|
15
|
+
AgentOperationalPhaseTransitionData, ToolInvocationApprovalRequestedData,
|
|
16
|
+
AssistantChunkData, AssistantCompleteResponseData
|
|
17
|
+
)
|
|
18
|
+
from autobyteus.workflow.streaming.workflow_stream_events import WorkflowStreamEvent
|
|
19
|
+
from autobyteus.workflow.streaming.workflow_stream_event_payloads import AgentEventRebroadcastPayload, SubWorkflowEventRebroadcastPayload, WorkflowPhaseTransitionData
|
|
20
|
+
|
|
21
|
+
logger = logging.getLogger(__name__)
|
|
22
|
+
|
|
23
|
+
class TUIStateStore:
|
|
24
|
+
"""
|
|
25
|
+
A centralized store for all TUI-related state.
|
|
26
|
+
|
|
27
|
+
This class acts as the single source of truth for the UI. It processes events
|
|
28
|
+
from the backend and updates its state. The main App class can then react to
|
|
29
|
+
these state changes to update the UI components declaratively. This is a plain
|
|
30
|
+
Python class and does not use Textual reactive properties.
|
|
31
|
+
"""
|
|
32
|
+
|
|
33
|
+
def __init__(self, workflow: AgenticWorkflow):
|
|
34
|
+
self.workflow_name = workflow.name
|
|
35
|
+
self.workflow_role = workflow.role
|
|
36
|
+
|
|
37
|
+
self.focused_node_data: Optional[Dict[str, Any]] = None
|
|
38
|
+
|
|
39
|
+
self._node_roles: Dict[str, str] = self._extract_node_roles(workflow)
|
|
40
|
+
self._nodes: Dict[str, Any] = self._initialize_root_node()
|
|
41
|
+
self._agent_phases: Dict[str, AgentOperationalPhase] = {}
|
|
42
|
+
self._workflow_phases: Dict[str, WorkflowOperationalPhase] = {self.workflow_name: WorkflowOperationalPhase.UNINITIALIZED}
|
|
43
|
+
self._agent_event_history: Dict[str, List[AgentStreamEvent]] = {}
|
|
44
|
+
self._workflow_event_history: Dict[str, List[WorkflowStreamEvent]] = {self.workflow_name: []}
|
|
45
|
+
self._pending_approvals: Dict[str, ToolInvocationApprovalRequestedData] = {}
|
|
46
|
+
self._speaking_agents: Dict[str, bool] = {}
|
|
47
|
+
|
|
48
|
+
# REMOVED: The complex stream aggregator is the source of the bug.
|
|
49
|
+
# self._agent_stream_aggregators: Dict[str, Dict[str, str]] = {}
|
|
50
|
+
|
|
51
|
+
# Version counter to signal state changes to the UI
|
|
52
|
+
self.version = 0
|
|
53
|
+
|
|
54
|
+
def _extract_node_roles(self, workflow: AgenticWorkflow) -> Dict[str, str]:
|
|
55
|
+
"""Builds a map of node names to their defined roles from the config."""
|
|
56
|
+
roles = {}
|
|
57
|
+
if workflow._runtime and workflow._runtime.context and workflow._runtime.context.config:
|
|
58
|
+
for node_config in workflow._runtime.context.config.nodes:
|
|
59
|
+
role = getattr(node_config.node_definition, 'role', None)
|
|
60
|
+
if role:
|
|
61
|
+
roles[node_config.name] = role
|
|
62
|
+
return roles
|
|
63
|
+
|
|
64
|
+
def _initialize_root_node(self) -> Dict[str, Any]:
|
|
65
|
+
"""Creates the initial root node for the state tree."""
|
|
66
|
+
return {
|
|
67
|
+
self.workflow_name: {
|
|
68
|
+
"type": "workflow",
|
|
69
|
+
"name": self.workflow_name,
|
|
70
|
+
"role": self.workflow_role,
|
|
71
|
+
"children": {}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
def process_event(self, event: WorkflowStreamEvent):
|
|
76
|
+
"""
|
|
77
|
+
The main entry point for processing events from the backend.
|
|
78
|
+
This method acts as a reducer, updating the state based on the event.
|
|
79
|
+
"""
|
|
80
|
+
if event.event_source_type == "WORKFLOW" and isinstance(event.data, WorkflowPhaseTransitionData):
|
|
81
|
+
self._workflow_phases[self.workflow_name] = event.data.new_phase
|
|
82
|
+
|
|
83
|
+
self._process_event_recursively(event, self.workflow_name)
|
|
84
|
+
|
|
85
|
+
# Increment version to signal that the state has changed.
|
|
86
|
+
self.version += 1
|
|
87
|
+
|
|
88
|
+
# REMOVED: The flush aggregator logic is no longer needed.
|
|
89
|
+
# def _flush_aggregator_for_agent(self, agent_name: str): ...
|
|
90
|
+
|
|
91
|
+
def _process_event_recursively(self, event: WorkflowStreamEvent, parent_name: str):
|
|
92
|
+
"""Recursively processes events to build up the state tree."""
|
|
93
|
+
if parent_name not in self._workflow_event_history:
|
|
94
|
+
self._workflow_event_history[parent_name] = []
|
|
95
|
+
self._workflow_event_history[parent_name].append(event)
|
|
96
|
+
|
|
97
|
+
# AGENT EVENT (LEAF NODE)
|
|
98
|
+
if isinstance(event.data, AgentEventRebroadcastPayload):
|
|
99
|
+
payload = event.data
|
|
100
|
+
agent_name = payload.agent_name
|
|
101
|
+
agent_event = payload.agent_event
|
|
102
|
+
|
|
103
|
+
if agent_name not in self._agent_event_history:
|
|
104
|
+
self._agent_event_history[agent_name] = []
|
|
105
|
+
if self._find_node(parent_name):
|
|
106
|
+
agent_role = self._node_roles.get(agent_name, "Agent")
|
|
107
|
+
self._add_node(agent_name, {"type": "agent", "name": agent_name, "role": agent_role, "children": {}}, parent_name)
|
|
108
|
+
else:
|
|
109
|
+
logger.error(f"Cannot add agent node '{agent_name}': parent '{parent_name}' not found in state tree.")
|
|
110
|
+
|
|
111
|
+
# SIMPLIFIED LOGIC: Always append the event to the history, regardless of focus.
|
|
112
|
+
# This ensures the history is always a complete and accurate log of what happened.
|
|
113
|
+
self._agent_event_history[agent_name].append(agent_event)
|
|
114
|
+
|
|
115
|
+
# --- State update logic for specific events (applies to both focused and non-focused) ---
|
|
116
|
+
if agent_event.event_type == AgentStreamEventType.AGENT_OPERATIONAL_PHASE_TRANSITION:
|
|
117
|
+
phase_data: AgentOperationalPhaseTransitionData = agent_event.data
|
|
118
|
+
self._agent_phases[agent_name] = phase_data.new_phase
|
|
119
|
+
if agent_name in self._pending_approvals:
|
|
120
|
+
del self._pending_approvals[agent_name]
|
|
121
|
+
elif agent_event.event_type == AgentStreamEventType.AGENT_IDLE:
|
|
122
|
+
self._agent_phases[agent_name] = AgentOperationalPhase.IDLE
|
|
123
|
+
elif agent_event.event_type == AgentStreamEventType.TOOL_INVOCATION_APPROVAL_REQUESTED:
|
|
124
|
+
self._pending_approvals[agent_name] = agent_event.data
|
|
125
|
+
|
|
126
|
+
# SUB-WORKFLOW EVENT (BRANCH NODE)
|
|
127
|
+
elif isinstance(event.data, SubWorkflowEventRebroadcastPayload):
|
|
128
|
+
payload = event.data
|
|
129
|
+
sub_workflow_name = payload.sub_workflow_node_name
|
|
130
|
+
sub_workflow_event = payload.sub_workflow_event
|
|
131
|
+
|
|
132
|
+
sub_workflow_node = self._find_node(sub_workflow_name)
|
|
133
|
+
if not sub_workflow_node:
|
|
134
|
+
role = self._node_roles.get(sub_workflow_name, "Sub-Workflow")
|
|
135
|
+
self._add_node(sub_workflow_name, {"type": "subworkflow", "name": sub_workflow_name, "role": role, "children": {}}, parent_name)
|
|
136
|
+
|
|
137
|
+
if sub_workflow_event.event_source_type == "WORKFLOW" and isinstance(sub_workflow_event.data, WorkflowPhaseTransitionData):
|
|
138
|
+
self._workflow_phases[sub_workflow_name] = sub_workflow_event.data.new_phase
|
|
139
|
+
|
|
140
|
+
self._process_event_recursively(sub_workflow_event, parent_name=sub_workflow_name)
|
|
141
|
+
|
|
142
|
+
def _add_node(self, node_name: str, node_data: Dict, parent_name: str):
|
|
143
|
+
"""Adds a node to the state tree under a specific parent."""
|
|
144
|
+
parent = self._find_node(parent_name)
|
|
145
|
+
if parent:
|
|
146
|
+
parent["children"][node_name] = node_data
|
|
147
|
+
else:
|
|
148
|
+
logger.error(f"Could not find parent node '{parent_name}' to add child '{node_name}'.")
|
|
149
|
+
|
|
150
|
+
def _find_node(self, node_name: str, tree: Optional[Dict] = None) -> Optional[Dict]:
|
|
151
|
+
"""Recursively finds a node by name in the state tree."""
|
|
152
|
+
if tree is None:
|
|
153
|
+
tree = self._nodes
|
|
154
|
+
|
|
155
|
+
for name, node_data in tree.items():
|
|
156
|
+
if name == node_name:
|
|
157
|
+
return node_data
|
|
158
|
+
if node_data.get("children"):
|
|
159
|
+
found = self._find_node(node_name, node_data.get("children"))
|
|
160
|
+
if found:
|
|
161
|
+
return found
|
|
162
|
+
return None
|
|
163
|
+
|
|
164
|
+
def get_tree_data(self) -> Dict:
|
|
165
|
+
"""Constructs a serializable representation of the tree for the sidebar."""
|
|
166
|
+
return copy.deepcopy(self._nodes)
|
|
167
|
+
|
|
168
|
+
def get_history_for_node(self, node_name: str, node_type: str) -> List:
|
|
169
|
+
"""Retrieves the event history for a given node."""
|
|
170
|
+
if node_type == 'agent':
|
|
171
|
+
# REMOVED: Flushing is no longer necessary as the history is always complete.
|
|
172
|
+
return self._agent_event_history.get(node_name, [])
|
|
173
|
+
elif node_type in ['workflow', 'subworkflow']:
|
|
174
|
+
return []
|
|
175
|
+
return []
|
|
176
|
+
|
|
177
|
+
def get_pending_approval_for_agent(self, agent_name: str) -> Optional[ToolInvocationApprovalRequestedData]:
|
|
178
|
+
"""Gets pending approval data for a specific agent."""
|
|
179
|
+
return self._pending_approvals.get(agent_name)
|
|
180
|
+
|
|
181
|
+
def clear_pending_approval(self, agent_name: str):
|
|
182
|
+
"""Clears a pending approval after it's been handled."""
|
|
183
|
+
if agent_name in self._pending_approvals:
|
|
184
|
+
del self._pending_approvals[agent_name]
|
|
185
|
+
|
|
186
|
+
def set_focused_node(self, node_data: Optional[Dict[str, Any]]):
|
|
187
|
+
"""Sets the currently focused node in the state."""
|
|
188
|
+
# REMOVED: Flushing logic is no longer needed here.
|
|
189
|
+
self.focused_node_data = node_data
|