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.
Files changed (171) hide show
  1. autobyteus/agent/agent.py +1 -1
  2. autobyteus/agent/bootstrap_steps/__init__.py +2 -0
  3. autobyteus/agent/bootstrap_steps/agent_bootstrapper.py +2 -0
  4. autobyteus/agent/bootstrap_steps/mcp_server_prewarming_step.py +71 -0
  5. autobyteus/agent/bootstrap_steps/system_prompt_processing_step.py +4 -2
  6. autobyteus/agent/context/agent_config.py +36 -5
  7. autobyteus/agent/events/worker_event_dispatcher.py +1 -2
  8. autobyteus/agent/handlers/inter_agent_message_event_handler.py +1 -1
  9. autobyteus/agent/handlers/llm_user_message_ready_event_handler.py +2 -2
  10. autobyteus/agent/handlers/tool_result_event_handler.py +48 -20
  11. autobyteus/agent/handlers/user_input_message_event_handler.py +1 -1
  12. autobyteus/agent/input_processor/__init__.py +1 -7
  13. autobyteus/agent/llm_response_processor/provider_aware_tool_usage_processor.py +41 -12
  14. autobyteus/agent/message/context_file_type.py +6 -0
  15. autobyteus/agent/message/send_message_to.py +68 -99
  16. autobyteus/agent/phases/discover.py +2 -1
  17. autobyteus/agent/runtime/agent_worker.py +25 -34
  18. autobyteus/agent/shutdown_steps/__init__.py +17 -0
  19. autobyteus/agent/shutdown_steps/agent_shutdown_orchestrator.py +63 -0
  20. autobyteus/agent/shutdown_steps/base_shutdown_step.py +33 -0
  21. autobyteus/agent/shutdown_steps/llm_instance_cleanup_step.py +45 -0
  22. autobyteus/agent/shutdown_steps/mcp_server_cleanup_step.py +32 -0
  23. autobyteus/agent/tool_execution_result_processor/__init__.py +9 -0
  24. autobyteus/agent/tool_execution_result_processor/base_processor.py +46 -0
  25. autobyteus/agent/tool_execution_result_processor/processor_definition.py +36 -0
  26. autobyteus/agent/tool_execution_result_processor/processor_meta.py +36 -0
  27. autobyteus/agent/tool_execution_result_processor/processor_registry.py +70 -0
  28. autobyteus/agent/workspace/base_workspace.py +17 -2
  29. autobyteus/cli/__init__.py +1 -1
  30. autobyteus/cli/cli_display.py +1 -1
  31. autobyteus/cli/workflow_tui/__init__.py +4 -0
  32. autobyteus/cli/workflow_tui/app.py +210 -0
  33. autobyteus/cli/workflow_tui/state.py +189 -0
  34. autobyteus/cli/workflow_tui/widgets/__init__.py +6 -0
  35. autobyteus/cli/workflow_tui/widgets/agent_list_sidebar.py +149 -0
  36. autobyteus/cli/workflow_tui/widgets/focus_pane.py +335 -0
  37. autobyteus/cli/workflow_tui/widgets/logo.py +27 -0
  38. autobyteus/cli/workflow_tui/widgets/renderables.py +70 -0
  39. autobyteus/cli/workflow_tui/widgets/shared.py +51 -0
  40. autobyteus/cli/workflow_tui/widgets/status_bar.py +14 -0
  41. autobyteus/events/event_types.py +3 -0
  42. autobyteus/llm/api/lmstudio_llm.py +37 -0
  43. autobyteus/llm/api/openai_compatible_llm.py +20 -3
  44. autobyteus/llm/llm_factory.py +2 -0
  45. autobyteus/llm/lmstudio_provider.py +89 -0
  46. autobyteus/llm/providers.py +1 -0
  47. autobyteus/llm/token_counter/token_counter_factory.py +2 -0
  48. autobyteus/tools/__init__.py +2 -0
  49. autobyteus/tools/ask_user_input.py +2 -1
  50. autobyteus/tools/base_tool.py +2 -0
  51. autobyteus/tools/bash/bash_executor.py +2 -1
  52. autobyteus/tools/browser/session_aware/browser_session_aware_navigate_to.py +2 -0
  53. autobyteus/tools/browser/session_aware/browser_session_aware_web_element_trigger.py +3 -0
  54. autobyteus/tools/browser/session_aware/browser_session_aware_webpage_reader.py +3 -0
  55. autobyteus/tools/browser/session_aware/browser_session_aware_webpage_screenshot_taker.py +3 -0
  56. autobyteus/tools/browser/standalone/google_search_ui.py +2 -0
  57. autobyteus/tools/browser/standalone/navigate_to.py +2 -0
  58. autobyteus/tools/browser/standalone/web_page_pdf_generator.py +3 -0
  59. autobyteus/tools/browser/standalone/webpage_image_downloader.py +3 -0
  60. autobyteus/tools/browser/standalone/webpage_reader.py +2 -0
  61. autobyteus/tools/browser/standalone/webpage_screenshot_taker.py +3 -0
  62. autobyteus/tools/file/file_reader.py +36 -9
  63. autobyteus/tools/file/file_writer.py +37 -9
  64. autobyteus/tools/functional_tool.py +5 -4
  65. autobyteus/tools/image_downloader.py +2 -0
  66. autobyteus/tools/mcp/__init__.py +10 -7
  67. autobyteus/tools/mcp/call_handlers/__init__.py +0 -2
  68. autobyteus/tools/mcp/config_service.py +1 -6
  69. autobyteus/tools/mcp/factory.py +12 -26
  70. autobyteus/tools/mcp/server/__init__.py +16 -0
  71. autobyteus/tools/mcp/server/base_managed_mcp_server.py +139 -0
  72. autobyteus/tools/mcp/server/http_managed_mcp_server.py +29 -0
  73. autobyteus/tools/mcp/server/proxy.py +36 -0
  74. autobyteus/tools/mcp/server/stdio_managed_mcp_server.py +33 -0
  75. autobyteus/tools/mcp/server_instance_manager.py +93 -0
  76. autobyteus/tools/mcp/tool.py +28 -46
  77. autobyteus/tools/mcp/tool_registrar.py +179 -0
  78. autobyteus/tools/mcp/types.py +10 -21
  79. autobyteus/tools/pdf_downloader.py +2 -1
  80. autobyteus/tools/registry/tool_definition.py +20 -7
  81. autobyteus/tools/registry/tool_registry.py +75 -28
  82. autobyteus/tools/timer.py +2 -0
  83. autobyteus/tools/tool_category.py +14 -4
  84. autobyteus/tools/tool_meta.py +6 -1
  85. autobyteus/tools/tool_origin.py +10 -0
  86. autobyteus/workflow/agentic_workflow.py +93 -0
  87. autobyteus/{agent/workflow → workflow}/base_agentic_workflow.py +19 -27
  88. autobyteus/workflow/bootstrap_steps/__init__.py +20 -0
  89. autobyteus/workflow/bootstrap_steps/agent_tool_injection_step.py +34 -0
  90. autobyteus/workflow/bootstrap_steps/base_workflow_bootstrap_step.py +23 -0
  91. autobyteus/workflow/bootstrap_steps/coordinator_initialization_step.py +41 -0
  92. autobyteus/workflow/bootstrap_steps/coordinator_prompt_preparation_step.py +108 -0
  93. autobyteus/workflow/bootstrap_steps/workflow_bootstrapper.py +50 -0
  94. autobyteus/workflow/bootstrap_steps/workflow_runtime_queue_initialization_step.py +25 -0
  95. autobyteus/workflow/context/__init__.py +17 -0
  96. autobyteus/workflow/context/team_manager.py +147 -0
  97. autobyteus/workflow/context/workflow_config.py +30 -0
  98. autobyteus/workflow/context/workflow_context.py +61 -0
  99. autobyteus/workflow/context/workflow_node_config.py +76 -0
  100. autobyteus/workflow/context/workflow_runtime_state.py +53 -0
  101. autobyteus/workflow/events/__init__.py +29 -0
  102. autobyteus/workflow/events/workflow_event_dispatcher.py +39 -0
  103. autobyteus/workflow/events/workflow_events.py +53 -0
  104. autobyteus/workflow/events/workflow_input_event_queue_manager.py +21 -0
  105. autobyteus/workflow/exceptions.py +8 -0
  106. autobyteus/workflow/factory/__init__.py +9 -0
  107. autobyteus/workflow/factory/workflow_factory.py +99 -0
  108. autobyteus/workflow/handlers/__init__.py +19 -0
  109. autobyteus/workflow/handlers/base_workflow_event_handler.py +16 -0
  110. autobyteus/workflow/handlers/inter_agent_message_request_event_handler.py +61 -0
  111. autobyteus/workflow/handlers/lifecycle_workflow_event_handler.py +27 -0
  112. autobyteus/workflow/handlers/process_user_message_event_handler.py +46 -0
  113. autobyteus/workflow/handlers/tool_approval_workflow_event_handler.py +39 -0
  114. autobyteus/workflow/handlers/workflow_event_handler_registry.py +23 -0
  115. autobyteus/workflow/phases/__init__.py +11 -0
  116. autobyteus/workflow/phases/workflow_operational_phase.py +19 -0
  117. autobyteus/workflow/phases/workflow_phase_manager.py +48 -0
  118. autobyteus/workflow/runtime/__init__.py +13 -0
  119. autobyteus/workflow/runtime/workflow_runtime.py +82 -0
  120. autobyteus/workflow/runtime/workflow_worker.py +117 -0
  121. autobyteus/workflow/shutdown_steps/__init__.py +17 -0
  122. autobyteus/workflow/shutdown_steps/agent_team_shutdown_step.py +42 -0
  123. autobyteus/workflow/shutdown_steps/base_workflow_shutdown_step.py +16 -0
  124. autobyteus/workflow/shutdown_steps/bridge_cleanup_step.py +28 -0
  125. autobyteus/workflow/shutdown_steps/sub_workflow_shutdown_step.py +41 -0
  126. autobyteus/workflow/shutdown_steps/workflow_shutdown_orchestrator.py +35 -0
  127. autobyteus/workflow/streaming/__init__.py +26 -0
  128. autobyteus/workflow/streaming/agent_event_bridge.py +48 -0
  129. autobyteus/workflow/streaming/agent_event_multiplexer.py +70 -0
  130. autobyteus/workflow/streaming/workflow_event_bridge.py +50 -0
  131. autobyteus/workflow/streaming/workflow_event_notifier.py +83 -0
  132. autobyteus/workflow/streaming/workflow_event_stream.py +33 -0
  133. autobyteus/workflow/streaming/workflow_stream_event_payloads.py +28 -0
  134. autobyteus/workflow/streaming/workflow_stream_events.py +45 -0
  135. autobyteus/workflow/utils/__init__.py +9 -0
  136. autobyteus/workflow/utils/wait_for_idle.py +46 -0
  137. autobyteus/workflow/workflow_builder.py +151 -0
  138. {autobyteus-1.1.2.dist-info → autobyteus-1.1.4.dist-info}/METADATA +16 -13
  139. {autobyteus-1.1.2.dist-info → autobyteus-1.1.4.dist-info}/RECORD +156 -75
  140. {autobyteus-1.1.2.dist-info → autobyteus-1.1.4.dist-info}/top_level.txt +1 -0
  141. examples/__init__.py +1 -0
  142. examples/discover_phase_transitions.py +104 -0
  143. examples/run_browser_agent.py +260 -0
  144. examples/run_google_slides_agent.py +286 -0
  145. examples/run_mcp_browser_client.py +174 -0
  146. examples/run_mcp_google_slides_client.py +270 -0
  147. examples/run_mcp_list_tools.py +189 -0
  148. examples/run_poem_writer.py +274 -0
  149. examples/run_sqlite_agent.py +293 -0
  150. examples/workflow/__init__.py +1 -0
  151. examples/workflow/run_basic_research_workflow.py +189 -0
  152. examples/workflow/run_code_review_workflow.py +269 -0
  153. examples/workflow/run_debate_workflow.py +212 -0
  154. examples/workflow/run_workflow_with_tui.py +153 -0
  155. autobyteus/agent/context/agent_phase_manager.py +0 -264
  156. autobyteus/agent/context/phases.py +0 -49
  157. autobyteus/agent/group/__init__.py +0 -0
  158. autobyteus/agent/group/agent_group.py +0 -164
  159. autobyteus/agent/group/agent_group_context.py +0 -81
  160. autobyteus/agent/input_processor/content_prefixing_input_processor.py +0 -41
  161. autobyteus/agent/input_processor/metadata_appending_input_processor.py +0 -34
  162. autobyteus/agent/input_processor/passthrough_input_processor.py +0 -33
  163. autobyteus/agent/workflow/__init__.py +0 -11
  164. autobyteus/agent/workflow/agentic_workflow.py +0 -89
  165. autobyteus/tools/mcp/call_handlers/sse_handler.py +0 -22
  166. autobyteus/tools/mcp/registrar.py +0 -323
  167. autobyteus/workflow/simple_task.py +0 -98
  168. autobyteus/workflow/task.py +0 -147
  169. autobyteus/workflow/workflow.py +0 -49
  170. {autobyteus-1.1.2.dist-info → autobyteus-1.1.4.dist-info}/WHEEL +0 -0
  171. {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)