autobyteus 1.1.2__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/__init__.py +2 -0
- autobyteus/agent/bootstrap_steps/agent_bootstrapper.py +2 -0
- autobyteus/agent/bootstrap_steps/mcp_server_prewarming_step.py +71 -0
- 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/llm_response_processor/provider_aware_tool_usage_processor.py +41 -12
- 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 +25 -34
- autobyteus/agent/shutdown_steps/__init__.py +17 -0
- autobyteus/agent/shutdown_steps/agent_shutdown_orchestrator.py +63 -0
- autobyteus/agent/shutdown_steps/base_shutdown_step.py +33 -0
- autobyteus/agent/shutdown_steps/llm_instance_cleanup_step.py +45 -0
- autobyteus/agent/shutdown_steps/mcp_server_cleanup_step.py +32 -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/base_tool.py +2 -0
- 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/__init__.py +10 -7
- autobyteus/tools/mcp/call_handlers/__init__.py +0 -2
- autobyteus/tools/mcp/config_service.py +1 -6
- autobyteus/tools/mcp/factory.py +12 -26
- autobyteus/tools/mcp/server/__init__.py +16 -0
- autobyteus/tools/mcp/server/base_managed_mcp_server.py +139 -0
- autobyteus/tools/mcp/server/http_managed_mcp_server.py +29 -0
- autobyteus/tools/mcp/server/proxy.py +36 -0
- autobyteus/tools/mcp/server/stdio_managed_mcp_server.py +33 -0
- autobyteus/tools/mcp/server_instance_manager.py +93 -0
- autobyteus/tools/mcp/tool.py +28 -46
- autobyteus/tools/mcp/tool_registrar.py +179 -0
- autobyteus/tools/mcp/types.py +10 -21
- autobyteus/tools/pdf_downloader.py +2 -1
- autobyteus/tools/registry/tool_definition.py +20 -7
- autobyteus/tools/registry/tool_registry.py +75 -28
- 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.2.dist-info → autobyteus-1.1.4.dist-info}/METADATA +16 -13
- {autobyteus-1.1.2.dist-info → autobyteus-1.1.4.dist-info}/RECORD +156 -75
- {autobyteus-1.1.2.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/call_handlers/sse_handler.py +0 -22
- autobyteus/tools/mcp/registrar.py +0 -323
- autobyteus/workflow/simple_task.py +0 -98
- autobyteus/workflow/task.py +0 -147
- autobyteus/workflow/workflow.py +0 -49
- {autobyteus-1.1.2.dist-info → autobyteus-1.1.4.dist-info}/WHEEL +0 -0
- {autobyteus-1.1.2.dist-info → autobyteus-1.1.4.dist-info}/licenses/LICENSE +0 -0
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# file: autobyteus/examples/workflow/run_workflow_with_tui.py
|
|
2
|
+
"""
|
|
3
|
+
This example script demonstrates how to run an AgenticWorkflow with the
|
|
4
|
+
new Textual-based user interface.
|
|
5
|
+
"""
|
|
6
|
+
import asyncio
|
|
7
|
+
import logging
|
|
8
|
+
import argparse
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
import sys
|
|
11
|
+
import os
|
|
12
|
+
|
|
13
|
+
# --- Boilerplate to make the script runnable from the project root ---
|
|
14
|
+
SCRIPT_DIR = Path(__file__).resolve().parent.parent
|
|
15
|
+
PACKAGE_ROOT = SCRIPT_DIR.parent
|
|
16
|
+
if str(PACKAGE_ROOT) not in sys.path:
|
|
17
|
+
sys.path.insert(0, str(PACKAGE_ROOT))
|
|
18
|
+
|
|
19
|
+
# Load environment variables from .env file
|
|
20
|
+
try:
|
|
21
|
+
from dotenv import load_dotenv
|
|
22
|
+
load_dotenv(PACKAGE_ROOT / ".env")
|
|
23
|
+
except ImportError:
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
# --- Imports for the Workflow TUI Example ---
|
|
27
|
+
try:
|
|
28
|
+
from autobyteus.agent.context import AgentConfig
|
|
29
|
+
from autobyteus.llm.models import LLMModel
|
|
30
|
+
from autobyteus.llm.llm_factory import default_llm_factory, LLMFactory
|
|
31
|
+
from autobyteus.workflow.workflow_builder import WorkflowBuilder
|
|
32
|
+
from autobyteus.cli.workflow_tui.app import WorkflowApp
|
|
33
|
+
except ImportError as e:
|
|
34
|
+
print(f"Error importing autobyteus components: {e}", file=sys.stderr)
|
|
35
|
+
sys.exit(1)
|
|
36
|
+
|
|
37
|
+
# --- Logging Setup ---
|
|
38
|
+
# It's crucial to log to a file so that stdout/stderr are free for Textual.
|
|
39
|
+
def setup_file_logging() -> Path:
|
|
40
|
+
"""
|
|
41
|
+
Sets up file-based logging and returns the path to the log file.
|
|
42
|
+
"""
|
|
43
|
+
log_dir = PACKAGE_ROOT / "logs"
|
|
44
|
+
log_dir.mkdir(exist_ok=True)
|
|
45
|
+
log_file_path = log_dir / "workflow_tui_app.log"
|
|
46
|
+
|
|
47
|
+
logging.basicConfig(
|
|
48
|
+
level=logging.INFO,
|
|
49
|
+
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
|
50
|
+
filename=log_file_path,
|
|
51
|
+
filemode="w",
|
|
52
|
+
)
|
|
53
|
+
# Silence the noisy asyncio logger in the file log
|
|
54
|
+
logging.getLogger("asyncio").setLevel(logging.WARNING)
|
|
55
|
+
logging.getLogger("textual").setLevel(logging.WARNING)
|
|
56
|
+
|
|
57
|
+
return log_file_path
|
|
58
|
+
|
|
59
|
+
def create_demo_workflow(model_name: str):
|
|
60
|
+
"""Creates a simple two-agent workflow for the TUI demonstration."""
|
|
61
|
+
# The factory will handle API key checks based on the selected model's provider.
|
|
62
|
+
|
|
63
|
+
# Validate model
|
|
64
|
+
try:
|
|
65
|
+
_ = LLMModel[model_name]
|
|
66
|
+
except KeyError:
|
|
67
|
+
logging.critical(f"LLM Model '{model_name}' is not valid. Use --help-models to see available models.")
|
|
68
|
+
print(f"\nCRITICAL ERROR: LLM Model '{model_name}' is not valid. Use --help-models to see available models.\nCheck log file for details.")
|
|
69
|
+
sys.exit(1)
|
|
70
|
+
|
|
71
|
+
# Coordinator Agent Config - Gets its own LLM instance
|
|
72
|
+
coordinator_config = AgentConfig(
|
|
73
|
+
name="Coordinator",
|
|
74
|
+
role="Project Manager",
|
|
75
|
+
description="Delegates tasks to the team to fulfill the user's request.",
|
|
76
|
+
llm_instance=default_llm_factory.create_llm(model_identifier=model_name),
|
|
77
|
+
system_prompt=(
|
|
78
|
+
"You are a project manager. Your job is to understand the user's request and delegate tasks to your team. "
|
|
79
|
+
"The workflow will provide you with a team manifest. Use your tools to communicate with your team.\n\n"
|
|
80
|
+
"Here are your available tools:\n"
|
|
81
|
+
"{{tools}}"
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
# Specialist Agent Config (FactChecker) - Gets its own LLM instance
|
|
86
|
+
fact_checker_config = AgentConfig(
|
|
87
|
+
name="FactChecker",
|
|
88
|
+
role="Specialist",
|
|
89
|
+
description="An agent with a limited, internal knowledge base for answering direct factual questions.",
|
|
90
|
+
llm_instance=default_llm_factory.create_llm(model_identifier=model_name),
|
|
91
|
+
system_prompt=(
|
|
92
|
+
"You are a fact-checking bot. You have the following knowledge:\n"
|
|
93
|
+
"- The capital of France is Paris.\n"
|
|
94
|
+
"- The tallest mountain on Earth is Mount Everest.\n"
|
|
95
|
+
"If asked something you don't know, say 'I do not have information on that topic.'\n\n"
|
|
96
|
+
"Here is the manifest of tools available to you:\n"
|
|
97
|
+
"{{tools}}"
|
|
98
|
+
)
|
|
99
|
+
)
|
|
100
|
+
|
|
101
|
+
# Build the workflow
|
|
102
|
+
workflow = (
|
|
103
|
+
WorkflowBuilder(
|
|
104
|
+
name="TUIDemoWorkflow",
|
|
105
|
+
description="A simple two-agent workflow for demonstrating the TUI."
|
|
106
|
+
)
|
|
107
|
+
.set_coordinator(coordinator_config)
|
|
108
|
+
.add_agent_node(fact_checker_config, dependencies=[])
|
|
109
|
+
.build()
|
|
110
|
+
)
|
|
111
|
+
return workflow
|
|
112
|
+
|
|
113
|
+
async def main(args: argparse.Namespace, log_file: Path):
|
|
114
|
+
"""Main async function to create the workflow and run the TUI app."""
|
|
115
|
+
print("Setting up workflow...")
|
|
116
|
+
print(f"--> Logs will be written to: {log_file.resolve()}")
|
|
117
|
+
try:
|
|
118
|
+
workflow = create_demo_workflow(model_name=args.llm_model)
|
|
119
|
+
app = WorkflowApp(workflow=workflow)
|
|
120
|
+
await app.run_async()
|
|
121
|
+
except Exception as e:
|
|
122
|
+
logging.critical(f"Failed to create or run workflow TUI: {e}", exc_info=True)
|
|
123
|
+
print(f"\nCRITICAL ERROR: {e}\nCheck log file for details: {log_file.resolve()}")
|
|
124
|
+
|
|
125
|
+
if __name__ == "__main__":
|
|
126
|
+
parser = argparse.ArgumentParser(description="Run an AgenticWorkflow with a Textual TUI.")
|
|
127
|
+
parser.add_argument("--llm-model", type=str, default="kimi-latest", help="The LLM model to use for the agents.")
|
|
128
|
+
parser.add_argument("--help-models", action="store_true", help="Display available LLM models and exit.")
|
|
129
|
+
|
|
130
|
+
if "--help-models" in sys.argv:
|
|
131
|
+
try:
|
|
132
|
+
LLMFactory.ensure_initialized()
|
|
133
|
+
print("Available LLM Models (you can use either name or value with --llm-model):")
|
|
134
|
+
all_models = sorted(list(LLMModel), key=lambda m: m.name)
|
|
135
|
+
if not all_models:
|
|
136
|
+
print(" No models found.")
|
|
137
|
+
for model in all_models:
|
|
138
|
+
print(f" - Name: {model.name:<35} Value: {model.value}")
|
|
139
|
+
except Exception as e:
|
|
140
|
+
print(f"Error listing models: {e}")
|
|
141
|
+
sys.exit(0)
|
|
142
|
+
|
|
143
|
+
parsed_args = parser.parse_args()
|
|
144
|
+
|
|
145
|
+
log_file_path = setup_file_logging()
|
|
146
|
+
try:
|
|
147
|
+
asyncio.run(main(parsed_args, log_file_path))
|
|
148
|
+
except KeyboardInterrupt:
|
|
149
|
+
print("\nExiting application.")
|
|
150
|
+
except Exception as e:
|
|
151
|
+
# This catches errors during asyncio.run, which might not be logged otherwise
|
|
152
|
+
logging.critical(f"Top-level application error: {e}", exc_info=True)
|
|
153
|
+
print(f"\nUNHANDLED ERROR: {e}\nCheck log file for details: {log_file_path.resolve()}")
|
|
@@ -1,264 +0,0 @@
|
|
|
1
|
-
# file: autobyteus/autobyteus/agent/context/agent_phase_manager.py
|
|
2
|
-
import asyncio
|
|
3
|
-
import logging
|
|
4
|
-
from typing import TYPE_CHECKING, Optional, Dict, Any
|
|
5
|
-
|
|
6
|
-
from autobyteus.agent.phases import AgentOperationalPhase, phase_transition
|
|
7
|
-
|
|
8
|
-
if TYPE_CHECKING:
|
|
9
|
-
from autobyteus.agent.context.agent_context import AgentContext
|
|
10
|
-
from autobyteus.agent.tool_invocation import ToolInvocation
|
|
11
|
-
from autobyteus.agent.events.notifiers import AgentExternalEventNotifier
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
logger = logging.getLogger(__name__)
|
|
15
|
-
|
|
16
|
-
class AgentPhaseManager:
|
|
17
|
-
"""
|
|
18
|
-
Manages the operational phase of an agent, uses an AgentExternalEventNotifier
|
|
19
|
-
to signal phase changes externally, and executes phase transition hooks.
|
|
20
|
-
"""
|
|
21
|
-
def __init__(self, context: 'AgentContext', notifier: 'AgentExternalEventNotifier'):
|
|
22
|
-
self.context: 'AgentContext' = context
|
|
23
|
-
self.notifier: 'AgentExternalEventNotifier' = notifier
|
|
24
|
-
|
|
25
|
-
self.context.current_phase = AgentOperationalPhase.UNINITIALIZED
|
|
26
|
-
|
|
27
|
-
logger.debug(f"AgentPhaseManager initialized for agent_id '{self.context.agent_id}'. "
|
|
28
|
-
f"Initial phase: {self.context.current_phase.value}. Uses provided notifier.")
|
|
29
|
-
|
|
30
|
-
async def _execute_hooks(self, old_phase: AgentOperationalPhase, new_phase: AgentOperationalPhase):
|
|
31
|
-
"""Asynchronously executes hooks that match the given phase transition."""
|
|
32
|
-
hooks_to_run = [
|
|
33
|
-
hook for hook in self.context.config.phase_hooks
|
|
34
|
-
if hook.source_phase == old_phase and hook.target_phase == new_phase
|
|
35
|
-
]
|
|
36
|
-
|
|
37
|
-
if not hooks_to_run:
|
|
38
|
-
return
|
|
39
|
-
|
|
40
|
-
hook_names = [hook.__class__.__name__ for hook in hooks_to_run]
|
|
41
|
-
logger.info(f"Agent '{self.context.agent_id}': Executing {len(hooks_to_run)} hooks for transition "
|
|
42
|
-
f"'{old_phase.value}' -> '{new_phase.value}': {hook_names}")
|
|
43
|
-
|
|
44
|
-
for hook in hooks_to_run:
|
|
45
|
-
try:
|
|
46
|
-
await hook.execute(self.context)
|
|
47
|
-
logger.debug(f"Agent '{self.context.agent_id}': Hook '{hook.__class__.__name__}' executed successfully.")
|
|
48
|
-
except Exception as e:
|
|
49
|
-
logger.error(f"Agent '{self.context.agent_id}': Error executing phase transition hook "
|
|
50
|
-
f"'{hook.__class__.__name__}' for '{old_phase.value}' -> '{new_phase.value}': {e}",
|
|
51
|
-
exc_info=True)
|
|
52
|
-
# We log the error but do not halt the agent's phase transition.
|
|
53
|
-
|
|
54
|
-
async def _transition_phase(self, new_phase: AgentOperationalPhase,
|
|
55
|
-
notify_method_name: str,
|
|
56
|
-
additional_data: Optional[Dict[str, Any]] = None):
|
|
57
|
-
"""
|
|
58
|
-
Private async helper to change the agent's phase, execute hooks, and then
|
|
59
|
-
call the appropriate notifier method. Hooks are now awaited.
|
|
60
|
-
"""
|
|
61
|
-
if not isinstance(new_phase, AgentOperationalPhase):
|
|
62
|
-
logger.error(f"AgentPhaseManager for '{self.context.agent_id}' received invalid type for new_phase: {type(new_phase)}. Must be AgentOperationalPhase.")
|
|
63
|
-
return
|
|
64
|
-
|
|
65
|
-
old_phase = self.context.current_phase
|
|
66
|
-
|
|
67
|
-
if old_phase == new_phase:
|
|
68
|
-
logger.debug(f"AgentPhaseManager for '{self.context.agent_id}': already in phase {new_phase.value}. No transition.")
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
logger.info(f"Agent '{self.context.agent_id}' phase transitioning from {old_phase.value} to {new_phase.value}.")
|
|
72
|
-
self.context.current_phase = new_phase
|
|
73
|
-
|
|
74
|
-
# Execute and wait for hooks to complete *before* notifying externally.
|
|
75
|
-
await self._execute_hooks(old_phase, new_phase)
|
|
76
|
-
|
|
77
|
-
notifier_method = getattr(self.notifier, notify_method_name, None)
|
|
78
|
-
if notifier_method and callable(notifier_method):
|
|
79
|
-
notify_args = {"old_phase": old_phase}
|
|
80
|
-
if additional_data:
|
|
81
|
-
notify_args.update(additional_data)
|
|
82
|
-
|
|
83
|
-
notifier_method(**notify_args)
|
|
84
|
-
else:
|
|
85
|
-
logger.error(f"AgentPhaseManager for '{self.context.agent_id}': Notifier method '{notify_method_name}' not found or not callable on {type(self.notifier).__name__}.")
|
|
86
|
-
|
|
87
|
-
@phase_transition(
|
|
88
|
-
source_phases=[AgentOperationalPhase.SHUTDOWN_COMPLETE, AgentOperationalPhase.ERROR],
|
|
89
|
-
target_phase=AgentOperationalPhase.UNINITIALIZED,
|
|
90
|
-
description="Triggered when the agent runtime is started or restarted after being in a terminal state."
|
|
91
|
-
)
|
|
92
|
-
async def notify_runtime_starting_and_uninitialized(self) -> None:
|
|
93
|
-
if self.context.current_phase == AgentOperationalPhase.UNINITIALIZED:
|
|
94
|
-
await self._transition_phase(AgentOperationalPhase.UNINITIALIZED, "notify_phase_uninitialized_entered")
|
|
95
|
-
elif self.context.current_phase.is_terminal():
|
|
96
|
-
await self._transition_phase(AgentOperationalPhase.UNINITIALIZED, "notify_phase_uninitialized_entered")
|
|
97
|
-
else:
|
|
98
|
-
logger.warning(f"Agent '{self.context.agent_id}' notify_runtime_starting_and_uninitialized called in unexpected phase: {self.context.current_phase.value}")
|
|
99
|
-
|
|
100
|
-
@phase_transition(
|
|
101
|
-
source_phases=[AgentOperationalPhase.UNINITIALIZED],
|
|
102
|
-
target_phase=AgentOperationalPhase.BOOTSTRAPPING,
|
|
103
|
-
description="Occurs when the agent's internal bootstrapping process begins."
|
|
104
|
-
)
|
|
105
|
-
async def notify_bootstrapping_started(self) -> None:
|
|
106
|
-
await self._transition_phase(AgentOperationalPhase.BOOTSTRAPPING, "notify_phase_bootstrapping_started")
|
|
107
|
-
|
|
108
|
-
@phase_transition(
|
|
109
|
-
source_phases=[AgentOperationalPhase.BOOTSTRAPPING],
|
|
110
|
-
target_phase=AgentOperationalPhase.IDLE,
|
|
111
|
-
description="Occurs when the agent successfully completes bootstrapping and is ready for input."
|
|
112
|
-
)
|
|
113
|
-
async def notify_initialization_complete(self) -> None:
|
|
114
|
-
if self.context.current_phase.is_initializing() or self.context.current_phase == AgentOperationalPhase.UNINITIALIZED:
|
|
115
|
-
# This will now be a BOOTSTRAPPING -> IDLE transition
|
|
116
|
-
await self._transition_phase(AgentOperationalPhase.IDLE, "notify_phase_idle_entered")
|
|
117
|
-
else:
|
|
118
|
-
logger.warning(f"Agent '{self.context.agent_id}' notify_initialization_complete called in unexpected phase: {self.context.current_phase.value}")
|
|
119
|
-
|
|
120
|
-
@phase_transition(
|
|
121
|
-
source_phases=[
|
|
122
|
-
AgentOperationalPhase.IDLE, AgentOperationalPhase.ANALYZING_LLM_RESPONSE,
|
|
123
|
-
AgentOperationalPhase.PROCESSING_TOOL_RESULT, AgentOperationalPhase.EXECUTING_TOOL,
|
|
124
|
-
AgentOperationalPhase.TOOL_DENIED
|
|
125
|
-
],
|
|
126
|
-
target_phase=AgentOperationalPhase.PROCESSING_USER_INPUT,
|
|
127
|
-
description="Fires when the agent begins processing a new user message or inter-agent message."
|
|
128
|
-
)
|
|
129
|
-
async def notify_processing_input_started(self, trigger_info: Optional[str] = None) -> None:
|
|
130
|
-
if self.context.current_phase in [AgentOperationalPhase.IDLE, AgentOperationalPhase.ANALYZING_LLM_RESPONSE, AgentOperationalPhase.PROCESSING_TOOL_RESULT, AgentOperationalPhase.EXECUTING_TOOL, AgentOperationalPhase.TOOL_DENIED]:
|
|
131
|
-
data = {"trigger_info": trigger_info} if trigger_info else {}
|
|
132
|
-
await self._transition_phase(AgentOperationalPhase.PROCESSING_USER_INPUT, "notify_phase_processing_user_input_started", additional_data=data)
|
|
133
|
-
elif self.context.current_phase == AgentOperationalPhase.PROCESSING_USER_INPUT:
|
|
134
|
-
logger.debug(f"Agent '{self.context.agent_id}' already in PROCESSING_USER_INPUT phase.")
|
|
135
|
-
else:
|
|
136
|
-
logger.warning(f"Agent '{self.context.agent_id}' notify_processing_input_started called in unexpected phase: {self.context.current_phase.value}")
|
|
137
|
-
|
|
138
|
-
@phase_transition(
|
|
139
|
-
source_phases=[AgentOperationalPhase.PROCESSING_USER_INPUT, AgentOperationalPhase.PROCESSING_TOOL_RESULT],
|
|
140
|
-
target_phase=AgentOperationalPhase.AWAITING_LLM_RESPONSE,
|
|
141
|
-
description="Occurs just before the agent makes a call to the LLM."
|
|
142
|
-
)
|
|
143
|
-
async def notify_awaiting_llm_response(self) -> None:
|
|
144
|
-
await self._transition_phase(AgentOperationalPhase.AWAITING_LLM_RESPONSE, "notify_phase_awaiting_llm_response_started")
|
|
145
|
-
|
|
146
|
-
@phase_transition(
|
|
147
|
-
source_phases=[AgentOperationalPhase.AWAITING_LLM_RESPONSE],
|
|
148
|
-
target_phase=AgentOperationalPhase.ANALYZING_LLM_RESPONSE,
|
|
149
|
-
description="Occurs after the agent has received a complete response from the LLM and begins to analyze it."
|
|
150
|
-
)
|
|
151
|
-
async def notify_analyzing_llm_response(self) -> None:
|
|
152
|
-
await self._transition_phase(AgentOperationalPhase.ANALYZING_LLM_RESPONSE, "notify_phase_analyzing_llm_response_started")
|
|
153
|
-
|
|
154
|
-
@phase_transition(
|
|
155
|
-
source_phases=[AgentOperationalPhase.ANALYZING_LLM_RESPONSE],
|
|
156
|
-
target_phase=AgentOperationalPhase.AWAITING_TOOL_APPROVAL,
|
|
157
|
-
description="Occurs if the agent proposes a tool use that requires manual user approval."
|
|
158
|
-
)
|
|
159
|
-
async def notify_tool_execution_pending_approval(self, tool_invocation: 'ToolInvocation') -> None:
|
|
160
|
-
await self._transition_phase(AgentOperationalPhase.AWAITING_TOOL_APPROVAL, "notify_phase_awaiting_tool_approval_started")
|
|
161
|
-
|
|
162
|
-
@phase_transition(
|
|
163
|
-
source_phases=[AgentOperationalPhase.AWAITING_TOOL_APPROVAL],
|
|
164
|
-
target_phase=AgentOperationalPhase.EXECUTING_TOOL,
|
|
165
|
-
description="Occurs after a pending tool use has been approved and is about to be executed."
|
|
166
|
-
)
|
|
167
|
-
async def notify_tool_execution_resumed_after_approval(self, approved: bool, tool_name: Optional[str]) -> None:
|
|
168
|
-
if approved and tool_name:
|
|
169
|
-
await self._transition_phase(AgentOperationalPhase.EXECUTING_TOOL, "notify_phase_executing_tool_started", additional_data={"tool_name": tool_name})
|
|
170
|
-
else:
|
|
171
|
-
logger.info(f"Agent '{self.context.agent_id}' tool execution denied for '{tool_name}'. Transitioning to allow LLM to process denial.")
|
|
172
|
-
await self.notify_tool_denied(tool_name)
|
|
173
|
-
|
|
174
|
-
@phase_transition(
|
|
175
|
-
source_phases=[AgentOperationalPhase.AWAITING_TOOL_APPROVAL],
|
|
176
|
-
target_phase=AgentOperationalPhase.TOOL_DENIED,
|
|
177
|
-
description="Occurs after a pending tool use has been denied by the user."
|
|
178
|
-
)
|
|
179
|
-
async def notify_tool_denied(self, tool_name: Optional[str]) -> None:
|
|
180
|
-
"""Notifies that a tool execution has been denied."""
|
|
181
|
-
await self._transition_phase(
|
|
182
|
-
AgentOperationalPhase.TOOL_DENIED,
|
|
183
|
-
"notify_phase_tool_denied_started",
|
|
184
|
-
additional_data={"tool_name": tool_name, "denial_for_tool": tool_name}
|
|
185
|
-
)
|
|
186
|
-
|
|
187
|
-
@phase_transition(
|
|
188
|
-
source_phases=[AgentOperationalPhase.ANALYZING_LLM_RESPONSE],
|
|
189
|
-
target_phase=AgentOperationalPhase.EXECUTING_TOOL,
|
|
190
|
-
description="Occurs when an agent with auto-approval executes a tool."
|
|
191
|
-
)
|
|
192
|
-
async def notify_tool_execution_started(self, tool_name: str) -> None:
|
|
193
|
-
await self._transition_phase(AgentOperationalPhase.EXECUTING_TOOL, "notify_phase_executing_tool_started", additional_data={"tool_name": tool_name})
|
|
194
|
-
|
|
195
|
-
@phase_transition(
|
|
196
|
-
source_phases=[AgentOperationalPhase.EXECUTING_TOOL],
|
|
197
|
-
target_phase=AgentOperationalPhase.PROCESSING_TOOL_RESULT,
|
|
198
|
-
description="Fires after a tool has finished executing and the agent begins processing its result."
|
|
199
|
-
)
|
|
200
|
-
async def notify_processing_tool_result(self, tool_name: str) -> None:
|
|
201
|
-
await self._transition_phase(AgentOperationalPhase.PROCESSING_TOOL_RESULT, "notify_phase_processing_tool_result_started", additional_data={"tool_name": tool_name})
|
|
202
|
-
|
|
203
|
-
@phase_transition(
|
|
204
|
-
source_phases=[
|
|
205
|
-
AgentOperationalPhase.PROCESSING_USER_INPUT, AgentOperationalPhase.ANALYZING_LLM_RESPONSE,
|
|
206
|
-
AgentOperationalPhase.PROCESSING_TOOL_RESULT
|
|
207
|
-
],
|
|
208
|
-
target_phase=AgentOperationalPhase.IDLE,
|
|
209
|
-
description="Occurs when an agent completes a processing cycle and is waiting for new input."
|
|
210
|
-
)
|
|
211
|
-
async def notify_processing_complete_and_idle(self) -> None:
|
|
212
|
-
if not self.context.current_phase.is_terminal() and self.context.current_phase != AgentOperationalPhase.IDLE:
|
|
213
|
-
await self._transition_phase(AgentOperationalPhase.IDLE, "notify_phase_idle_entered")
|
|
214
|
-
elif self.context.current_phase == AgentOperationalPhase.IDLE:
|
|
215
|
-
logger.debug(f"Agent '{self.context.agent_id}' processing complete, already IDLE.")
|
|
216
|
-
else:
|
|
217
|
-
logger.warning(f"Agent '{self.context.agent_id}' notify_processing_complete_and_idle called in unexpected phase: {self.context.current_phase.value}")
|
|
218
|
-
|
|
219
|
-
@phase_transition(
|
|
220
|
-
source_phases=[
|
|
221
|
-
AgentOperationalPhase.UNINITIALIZED, AgentOperationalPhase.BOOTSTRAPPING, AgentOperationalPhase.IDLE,
|
|
222
|
-
AgentOperationalPhase.PROCESSING_USER_INPUT, AgentOperationalPhase.AWAITING_LLM_RESPONSE,
|
|
223
|
-
AgentOperationalPhase.ANALYZING_LLM_RESPONSE, AgentOperationalPhase.AWAITING_TOOL_APPROVAL,
|
|
224
|
-
AgentOperationalPhase.TOOL_DENIED, AgentOperationalPhase.EXECUTING_TOOL,
|
|
225
|
-
AgentOperationalPhase.PROCESSING_TOOL_RESULT, AgentOperationalPhase.SHUTTING_DOWN
|
|
226
|
-
],
|
|
227
|
-
target_phase=AgentOperationalPhase.ERROR,
|
|
228
|
-
description="A catch-all transition that can occur from any non-terminal state if an unrecoverable error happens."
|
|
229
|
-
)
|
|
230
|
-
async def notify_error_occurred(self, error_message: str, error_details: Optional[str] = None) -> None:
|
|
231
|
-
if self.context.current_phase != AgentOperationalPhase.ERROR:
|
|
232
|
-
data = {"error_message": error_message, "error_details": error_details}
|
|
233
|
-
await self._transition_phase(AgentOperationalPhase.ERROR, "notify_phase_error_entered", additional_data=data)
|
|
234
|
-
else:
|
|
235
|
-
logger.debug(f"Agent '{self.context.agent_id}' already in ERROR phase when another error notified: {error_message}")
|
|
236
|
-
|
|
237
|
-
@phase_transition(
|
|
238
|
-
source_phases=[
|
|
239
|
-
AgentOperationalPhase.UNINITIALIZED, AgentOperationalPhase.BOOTSTRAPPING, AgentOperationalPhase.IDLE,
|
|
240
|
-
AgentOperationalPhase.PROCESSING_USER_INPUT, AgentOperationalPhase.AWAITING_LLM_RESPONSE,
|
|
241
|
-
AgentOperationalPhase.ANALYZING_LLM_RESPONSE, AgentOperationalPhase.AWAITING_TOOL_APPROVAL,
|
|
242
|
-
AgentOperationalPhase.TOOL_DENIED, AgentOperationalPhase.EXECUTING_TOOL,
|
|
243
|
-
AgentOperationalPhase.PROCESSING_TOOL_RESULT
|
|
244
|
-
],
|
|
245
|
-
target_phase=AgentOperationalPhase.SHUTTING_DOWN,
|
|
246
|
-
description="Fires when the agent begins its graceful shutdown sequence."
|
|
247
|
-
)
|
|
248
|
-
async def notify_shutdown_initiated(self) -> None:
|
|
249
|
-
if not self.context.current_phase.is_terminal():
|
|
250
|
-
await self._transition_phase(AgentOperationalPhase.SHUTTING_DOWN, "notify_phase_shutting_down_started")
|
|
251
|
-
else:
|
|
252
|
-
logger.debug(f"Agent '{self.context.agent_id}' shutdown initiated but already in a terminal phase: {self.context.current_phase.value}")
|
|
253
|
-
|
|
254
|
-
@phase_transition(
|
|
255
|
-
source_phases=[AgentOperationalPhase.SHUTTING_DOWN],
|
|
256
|
-
target_phase=AgentOperationalPhase.SHUTDOWN_COMPLETE,
|
|
257
|
-
description="The final transition when the agent has successfully shut down and released its resources."
|
|
258
|
-
)
|
|
259
|
-
async def notify_final_shutdown_complete(self) -> None:
|
|
260
|
-
final_phase = AgentOperationalPhase.ERROR if self.context.current_phase == AgentOperationalPhase.ERROR else AgentOperationalPhase.SHUTDOWN_COMPLETE
|
|
261
|
-
if final_phase == AgentOperationalPhase.ERROR:
|
|
262
|
-
await self._transition_phase(AgentOperationalPhase.ERROR, "notify_phase_error_entered", additional_data={"error_message": "Shutdown completed with agent in error state."})
|
|
263
|
-
else:
|
|
264
|
-
await self._transition_phase(AgentOperationalPhase.SHUTDOWN_COMPLETE, "notify_phase_shutdown_completed")
|
|
@@ -1,49 +0,0 @@
|
|
|
1
|
-
# file: autobyteus/autobyteus/agent/context/phases.py
|
|
2
|
-
from enum import Enum
|
|
3
|
-
|
|
4
|
-
class AgentOperationalPhase(str, Enum):
|
|
5
|
-
"""
|
|
6
|
-
Defines the fine-grained operational phases of an agent.
|
|
7
|
-
This is the single source of truth for an agent's current state of operation.
|
|
8
|
-
"""
|
|
9
|
-
UNINITIALIZED = "uninitialized" # Agent object created, but runtime not started or fully set up.
|
|
10
|
-
BOOTSTRAPPING = "bootstrapping" # Agent is running its internal initialization/bootstrap sequence.
|
|
11
|
-
IDLE = "idle" # Fully initialized and ready for new input.
|
|
12
|
-
|
|
13
|
-
PROCESSING_USER_INPUT = "processing_user_input" # Actively processing a user message, typically preparing for an LLM call.
|
|
14
|
-
AWAITING_LLM_RESPONSE = "awaiting_llm_response" # Sent a request to LLM, waiting for the full response or stream.
|
|
15
|
-
ANALYZING_LLM_RESPONSE = "analyzing_llm_response" # Received LLM response, analyzing it for next actions (e.g., tool use, direct reply).
|
|
16
|
-
|
|
17
|
-
AWAITING_TOOL_APPROVAL = "awaiting_tool_approval" # Paused, needs external (user) approval for a tool invocation.
|
|
18
|
-
TOOL_DENIED = "tool_denied" # A proposed tool execution was denied by the user. Agent is processing the denial.
|
|
19
|
-
EXECUTING_TOOL = "executing_tool" # Tool has been approved (or auto-approved) and is currently running.
|
|
20
|
-
PROCESSING_TOOL_RESULT = "processing_tool_result" # Received a tool's result, actively processing it (often leading to another LLM call).
|
|
21
|
-
|
|
22
|
-
SHUTTING_DOWN = "shutting_down" # Shutdown sequence has been initiated.
|
|
23
|
-
SHUTDOWN_COMPLETE = "shutdown_complete" # Agent has fully stopped and released resources.
|
|
24
|
-
ERROR = "error" # An unrecoverable error has occurred. Agent might be non-operational.
|
|
25
|
-
|
|
26
|
-
def __str__(self) -> str:
|
|
27
|
-
return self.value
|
|
28
|
-
|
|
29
|
-
def is_initializing(self) -> bool:
|
|
30
|
-
"""Checks if the agent is in any of the initializing phases."""
|
|
31
|
-
return self in [
|
|
32
|
-
AgentOperationalPhase.BOOTSTRAPPING,
|
|
33
|
-
]
|
|
34
|
-
|
|
35
|
-
def is_processing(self) -> bool:
|
|
36
|
-
"""Checks if the agent is in any active processing phase (post-initialization, pre-shutdown)."""
|
|
37
|
-
return self in [
|
|
38
|
-
AgentOperationalPhase.PROCESSING_USER_INPUT,
|
|
39
|
-
AgentOperationalPhase.AWAITING_LLM_RESPONSE,
|
|
40
|
-
AgentOperationalPhase.ANALYZING_LLM_RESPONSE,
|
|
41
|
-
AgentOperationalPhase.AWAITING_TOOL_APPROVAL,
|
|
42
|
-
AgentOperationalPhase.TOOL_DENIED,
|
|
43
|
-
AgentOperationalPhase.EXECUTING_TOOL,
|
|
44
|
-
AgentOperationalPhase.PROCESSING_TOOL_RESULT,
|
|
45
|
-
]
|
|
46
|
-
|
|
47
|
-
def is_terminal(self) -> bool:
|
|
48
|
-
"""Checks if the phase is a terminal state (shutdown or error)."""
|
|
49
|
-
return self in [AgentOperationalPhase.SHUTDOWN_COMPLETE, AgentOperationalPhase.ERROR]
|
|
File without changes
|
|
@@ -1,164 +0,0 @@
|
|
|
1
|
-
# file: autobyteus/autobyteus/agent/group/agent_group.py
|
|
2
|
-
import asyncio
|
|
3
|
-
import logging
|
|
4
|
-
import uuid
|
|
5
|
-
from typing import List, Dict, Optional, Any
|
|
6
|
-
|
|
7
|
-
from autobyteus.agent.context.agent_config import AgentConfig
|
|
8
|
-
from autobyteus.agent.factory import AgentFactory
|
|
9
|
-
from autobyteus.agent.agent import Agent
|
|
10
|
-
from autobyteus.agent.group.agent_group_context import AgentGroupContext
|
|
11
|
-
from autobyteus.agent.message.send_message_to import SendMessageTo
|
|
12
|
-
from autobyteus.agent.message.agent_input_user_message import AgentInputUserMessage
|
|
13
|
-
from autobyteus.agent.streaming.agent_event_stream import AgentEventStream
|
|
14
|
-
from autobyteus.llm.utils.response_types import CompleteResponse
|
|
15
|
-
|
|
16
|
-
logger = logging.getLogger(__name__)
|
|
17
|
-
|
|
18
|
-
class AgentGroup:
|
|
19
|
-
def __init__(self,
|
|
20
|
-
agent_configs: List[AgentConfig],
|
|
21
|
-
coordinator_config_name: str,
|
|
22
|
-
group_id: Optional[str] = None):
|
|
23
|
-
if not agent_configs or not all(isinstance(c, AgentConfig) for c in agent_configs):
|
|
24
|
-
raise TypeError("agent_configs must be a non-empty list of AgentConfig instances.")
|
|
25
|
-
if not coordinator_config_name or not isinstance(coordinator_config_name, str):
|
|
26
|
-
raise TypeError("coordinator_config_name must be a non-empty string.")
|
|
27
|
-
|
|
28
|
-
self.group_id: str = group_id or f"group_{uuid.uuid4()}"
|
|
29
|
-
self.agent_factory = AgentFactory() # Get singleton instance
|
|
30
|
-
self._agent_configs_map: Dict[str, AgentConfig] = {
|
|
31
|
-
config.name: config for config in agent_configs
|
|
32
|
-
}
|
|
33
|
-
self.coordinator_config_name: str = coordinator_config_name
|
|
34
|
-
self.agents: List[Agent] = []
|
|
35
|
-
self.coordinator_agent: Optional[Agent] = None
|
|
36
|
-
self.group_context: Optional[AgentGroupContext] = None
|
|
37
|
-
self._is_initialized: bool = False
|
|
38
|
-
self._is_running: bool = False
|
|
39
|
-
|
|
40
|
-
if self.coordinator_config_name not in self._agent_configs_map:
|
|
41
|
-
raise ValueError(f"Coordinator config name '{self.coordinator_config_name}' "
|
|
42
|
-
f"not found in provided agent_configs. Available: {list(self._agent_configs_map.keys())}")
|
|
43
|
-
logger.info(f"AgentGroup '{self.group_id}' created with {len(agent_configs)} configurations. "
|
|
44
|
-
f"Coordinator: '{self.coordinator_config_name}'.")
|
|
45
|
-
self._initialize_agents()
|
|
46
|
-
|
|
47
|
-
def _initialize_agents(self):
|
|
48
|
-
if self._is_initialized:
|
|
49
|
-
logger.warning(f"AgentGroup '{self.group_id}' agents already initialized. Skipping.")
|
|
50
|
-
return
|
|
51
|
-
|
|
52
|
-
temp_agents_list: List[Agent] = []
|
|
53
|
-
temp_coordinator_agent: Optional[Agent] = None
|
|
54
|
-
for config_name, original_config in self._agent_configs_map.items():
|
|
55
|
-
|
|
56
|
-
modified_tools = list(original_config.tools)
|
|
57
|
-
is_send_message_present = any(isinstance(tool, SendMessageTo) for tool in modified_tools)
|
|
58
|
-
if not is_send_message_present:
|
|
59
|
-
modified_tools.append(SendMessageTo())
|
|
60
|
-
|
|
61
|
-
# This logic correctly re-uses the user-provided LLM instance and other properties
|
|
62
|
-
# when creating the effective config for the agent factory.
|
|
63
|
-
effective_config = AgentConfig(
|
|
64
|
-
name=original_config.name,
|
|
65
|
-
role=original_config.role,
|
|
66
|
-
description=original_config.description,
|
|
67
|
-
llm_instance=original_config.llm_instance,
|
|
68
|
-
system_prompt=original_config.system_prompt,
|
|
69
|
-
tools=modified_tools,
|
|
70
|
-
auto_execute_tools=original_config.auto_execute_tools,
|
|
71
|
-
use_xml_tool_format=original_config.use_xml_tool_format,
|
|
72
|
-
input_processors=original_config.input_processors,
|
|
73
|
-
llm_response_processors=original_config.llm_response_processors,
|
|
74
|
-
system_prompt_processors=original_config.system_prompt_processors,
|
|
75
|
-
workspace=original_config.workspace,
|
|
76
|
-
phase_hooks=original_config.phase_hooks,
|
|
77
|
-
initial_custom_data=original_config.initial_custom_data
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
try:
|
|
81
|
-
agent_instance = self.agent_factory.create_agent(config=effective_config)
|
|
82
|
-
temp_agents_list.append(agent_instance)
|
|
83
|
-
|
|
84
|
-
if config_name == self.coordinator_config_name:
|
|
85
|
-
temp_coordinator_agent = agent_instance
|
|
86
|
-
logger.debug(f"Agent '{agent_instance.agent_id}' (Role: {original_config.role}) created for group '{self.group_id}'.")
|
|
87
|
-
except Exception as e:
|
|
88
|
-
logger.error(f"Failed to create agent for config '{config_name}' for group '{self.group_id}': {e}", exc_info=True)
|
|
89
|
-
raise RuntimeError(f"Failed to initialize agent for config '{config_name}' in group '{self.group_id}'.") from e
|
|
90
|
-
|
|
91
|
-
if not temp_coordinator_agent:
|
|
92
|
-
raise RuntimeError(f"Coordinator agent '{self.coordinator_config_name}' could not be instantiated.")
|
|
93
|
-
|
|
94
|
-
self.agents = temp_agents_list
|
|
95
|
-
self.coordinator_agent = temp_coordinator_agent
|
|
96
|
-
self.group_context = AgentGroupContext(group_id=self.group_id, agents=self.agents, coordinator_agent_id=self.coordinator_agent.agent_id)
|
|
97
|
-
for agent in self.agents:
|
|
98
|
-
agent.context.custom_data['agent_group_context'] = self.group_context
|
|
99
|
-
self._is_initialized = True
|
|
100
|
-
logger.info(f"AgentGroup '{self.group_id}' all {len(self.agents)} agents initialized successfully.")
|
|
101
|
-
|
|
102
|
-
async def start(self):
|
|
103
|
-
if not self._is_initialized: raise RuntimeError(f"AgentGroup '{self.group_id}' must be initialized before starting.")
|
|
104
|
-
if self._is_running: logger.warning(f"AgentGroup '{self.group_id}' is already running."); return
|
|
105
|
-
logger.info(f"Starting all agents in AgentGroup '{self.group_id}'..."); self._is_running = True
|
|
106
|
-
try:
|
|
107
|
-
for agent in self.agents:
|
|
108
|
-
if not agent.is_running:
|
|
109
|
-
agent.start()
|
|
110
|
-
# Give loops a chance to start
|
|
111
|
-
await asyncio.sleep(0.01)
|
|
112
|
-
logger.info(f"All agents in AgentGroup '{self.group_id}' have been requested to start.")
|
|
113
|
-
except Exception as e:
|
|
114
|
-
self._is_running = False; logger.error(f"Error starting agents in AgentGroup '{self.group_id}': {e}", exc_info=True)
|
|
115
|
-
await self.stop(timeout=2.0); raise
|
|
116
|
-
|
|
117
|
-
async def stop(self, timeout: float = 10.0):
|
|
118
|
-
if not self._is_running and not any(a.is_running for a in self.agents):
|
|
119
|
-
logger.info(f"AgentGroup '{self.group_id}' is already stopped or was never started."); self._is_running = False; return
|
|
120
|
-
logger.info(f"Stopping all agents in AgentGroup '{self.group_id}' with timeout {timeout}s...")
|
|
121
|
-
stop_tasks = [agent.stop(timeout=timeout) for agent in self.agents]
|
|
122
|
-
results = await asyncio.gather(*stop_tasks, return_exceptions=True)
|
|
123
|
-
for agent, result in zip(self.agents, results):
|
|
124
|
-
if isinstance(result, Exception): logger.error(f"Error stopping agent '{agent.agent_id}': {result}", exc_info=result)
|
|
125
|
-
self._is_running = False; logger.info(f"All agents in AgentGroup '{self.group_id}' have been requested to stop.")
|
|
126
|
-
|
|
127
|
-
async def process_task_for_coordinator(self, initial_input_content: str, user_id: Optional[str] = None) -> Any:
|
|
128
|
-
if not self.coordinator_agent: raise RuntimeError(f"Coordinator agent not set in group '{self.group_id}'.")
|
|
129
|
-
await self.start()
|
|
130
|
-
final_response_aggregator = ""
|
|
131
|
-
output_stream_listener_task = None
|
|
132
|
-
streamer = None
|
|
133
|
-
try:
|
|
134
|
-
streamer = AgentEventStream(self.coordinator_agent)
|
|
135
|
-
async def listen_for_final_output():
|
|
136
|
-
nonlocal final_response_aggregator
|
|
137
|
-
try:
|
|
138
|
-
async for complete_response_data in streamer.stream_assistant_final_response():
|
|
139
|
-
final_response_aggregator += complete_response_data.content
|
|
140
|
-
except Exception as e_stream:
|
|
141
|
-
logger.error(f"Error streaming final output from coordinator: {e_stream}", exc_info=True)
|
|
142
|
-
output_stream_listener_task = asyncio.create_task(listen_for_final_output())
|
|
143
|
-
input_message = AgentInputUserMessage(content=initial_input_content, metadata={"user_id": user_id} if user_id else {})
|
|
144
|
-
await self.coordinator_agent.post_user_message(input_message)
|
|
145
|
-
|
|
146
|
-
# Wait for the listener to finish, which happens after the agent is done and the stream closes.
|
|
147
|
-
if output_stream_listener_task:
|
|
148
|
-
await output_stream_listener_task
|
|
149
|
-
|
|
150
|
-
return final_response_aggregator
|
|
151
|
-
finally:
|
|
152
|
-
if output_stream_listener_task and not output_stream_listener_task.done():
|
|
153
|
-
output_stream_listener_task.cancel()
|
|
154
|
-
if streamer: await streamer.close()
|
|
155
|
-
|
|
156
|
-
def get_agent_by_id(self, agent_id: str) -> Optional[Agent]:
|
|
157
|
-
return next((agent for agent in self.agents if agent.agent_id == agent_id), None)
|
|
158
|
-
|
|
159
|
-
def get_agents_by_role(self, role_name: str) -> List[Agent]:
|
|
160
|
-
return [agent for agent in self.agents if agent.context.config.role == role_name]
|
|
161
|
-
|
|
162
|
-
@property
|
|
163
|
-
def is_running(self) -> bool:
|
|
164
|
-
return self._is_running and any(a.is_running for a in self.agents)
|