Spaces:
Running
fix: Address audit findings in orchestrators package
Browse filesCRITICAL fixes:
- HierarchicalOrchestrator now accepts config parameter and uses max_iterations
- Added timeout protection (DEFAULT_TIMEOUT_SECONDS = 300.0)
HIGH priority fixes:
- Added OrchestratorProtocol to base.py for type safety
- Factory returns OrchestratorProtocol instead of Any
MEDIUM priority fixes:
- Added deprecation warnings for MagenticOrchestrator class alias
- Added deprecation warning for get_magentic_orchestrator() function
- Improved error handling with separate ImportError/Exception blocks
- Added null check before calling analyzer.analyze() in simple.py
Type safety improvements:
- simple.py uses proper StatisticalAnalyzer/EmbeddingService types via TYPE_CHECKING
- All public API functions have proper return type annotations
- Sorted __all__ alphabetically per ruff RUF022
All 147 tests pass, linting and mypy clean.
- src/orchestrators/__init__.py +50 -10
- src/orchestrators/advanced.py +47 -13
- src/orchestrators/base.py +23 -2
- src/orchestrators/factory.py +26 -10
- src/orchestrators/hierarchical.py +69 -34
- src/orchestrators/simple.py +35 -9
|
@@ -17,7 +17,7 @@ Usage:
|
|
| 17 |
orchestrator = create_orchestrator(mode="advanced", api_key="sk-...")
|
| 18 |
|
| 19 |
Protocols:
|
| 20 |
-
from src.orchestrators import SearchHandlerProtocol, JudgeHandlerProtocol
|
| 21 |
|
| 22 |
Design Patterns Applied:
|
| 23 |
- Factory Pattern: create_orchestrator() creates appropriate orchestrator
|
|
@@ -25,8 +25,17 @@ Design Patterns Applied:
|
|
| 25 |
- Facade Pattern: This __init__.py provides a clean public API
|
| 26 |
"""
|
| 27 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
# Protocols (Interface Segregation Principle)
|
| 29 |
-
from src.orchestrators.base import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
|
| 31 |
# Factory (creational pattern)
|
| 32 |
from src.orchestrators.factory import create_orchestrator
|
|
@@ -34,35 +43,66 @@ from src.orchestrators.factory import create_orchestrator
|
|
| 34 |
# Orchestrators (Strategy Pattern implementations)
|
| 35 |
from src.orchestrators.simple import Orchestrator
|
| 36 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 37 |
# Lazy imports for optional dependencies
|
| 38 |
# These are not imported at module level to avoid breaking simple mode
|
| 39 |
# when agent-framework-core is not installed
|
| 40 |
|
| 41 |
|
| 42 |
-
def get_advanced_orchestrator() -> type:
|
| 43 |
-
"""Get the AdvancedOrchestrator class (requires agent-framework-core).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 44 |
from src.orchestrators.advanced import AdvancedOrchestrator
|
| 45 |
|
| 46 |
return AdvancedOrchestrator
|
| 47 |
|
| 48 |
|
| 49 |
-
def get_hierarchical_orchestrator() -> type:
|
| 50 |
-
"""Get the HierarchicalOrchestrator class (requires agent-framework-core).
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
from src.orchestrators.hierarchical import HierarchicalOrchestrator
|
| 52 |
|
| 53 |
return HierarchicalOrchestrator
|
| 54 |
|
| 55 |
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 60 |
return get_advanced_orchestrator()
|
| 61 |
|
| 62 |
|
| 63 |
__all__ = [
|
| 64 |
"JudgeHandlerProtocol",
|
| 65 |
"Orchestrator",
|
|
|
|
| 66 |
"SearchHandlerProtocol",
|
| 67 |
"create_orchestrator",
|
| 68 |
"get_advanced_orchestrator",
|
|
|
|
| 17 |
orchestrator = create_orchestrator(mode="advanced", api_key="sk-...")
|
| 18 |
|
| 19 |
Protocols:
|
| 20 |
+
from src.orchestrators import SearchHandlerProtocol, JudgeHandlerProtocol, OrchestratorProtocol
|
| 21 |
|
| 22 |
Design Patterns Applied:
|
| 23 |
- Factory Pattern: create_orchestrator() creates appropriate orchestrator
|
|
|
|
| 25 |
- Facade Pattern: This __init__.py provides a clean public API
|
| 26 |
"""
|
| 27 |
|
| 28 |
+
from __future__ import annotations
|
| 29 |
+
|
| 30 |
+
import warnings
|
| 31 |
+
from typing import TYPE_CHECKING
|
| 32 |
+
|
| 33 |
# Protocols (Interface Segregation Principle)
|
| 34 |
+
from src.orchestrators.base import (
|
| 35 |
+
JudgeHandlerProtocol,
|
| 36 |
+
OrchestratorProtocol,
|
| 37 |
+
SearchHandlerProtocol,
|
| 38 |
+
)
|
| 39 |
|
| 40 |
# Factory (creational pattern)
|
| 41 |
from src.orchestrators.factory import create_orchestrator
|
|
|
|
| 43 |
# Orchestrators (Strategy Pattern implementations)
|
| 44 |
from src.orchestrators.simple import Orchestrator
|
| 45 |
|
| 46 |
+
if TYPE_CHECKING:
|
| 47 |
+
from src.orchestrators.advanced import AdvancedOrchestrator
|
| 48 |
+
from src.orchestrators.hierarchical import HierarchicalOrchestrator
|
| 49 |
+
|
| 50 |
# Lazy imports for optional dependencies
|
| 51 |
# These are not imported at module level to avoid breaking simple mode
|
| 52 |
# when agent-framework-core is not installed
|
| 53 |
|
| 54 |
|
| 55 |
+
def get_advanced_orchestrator() -> type[AdvancedOrchestrator]:
|
| 56 |
+
"""Get the AdvancedOrchestrator class (requires agent-framework-core).
|
| 57 |
+
|
| 58 |
+
Returns:
|
| 59 |
+
The AdvancedOrchestrator class
|
| 60 |
+
|
| 61 |
+
Raises:
|
| 62 |
+
ImportError: If agent-framework-core is not installed
|
| 63 |
+
"""
|
| 64 |
from src.orchestrators.advanced import AdvancedOrchestrator
|
| 65 |
|
| 66 |
return AdvancedOrchestrator
|
| 67 |
|
| 68 |
|
| 69 |
+
def get_hierarchical_orchestrator() -> type[HierarchicalOrchestrator]:
|
| 70 |
+
"""Get the HierarchicalOrchestrator class (requires agent-framework-core).
|
| 71 |
+
|
| 72 |
+
Returns:
|
| 73 |
+
The HierarchicalOrchestrator class
|
| 74 |
+
|
| 75 |
+
Raises:
|
| 76 |
+
ImportError: If agent-framework-core is not installed
|
| 77 |
+
"""
|
| 78 |
from src.orchestrators.hierarchical import HierarchicalOrchestrator
|
| 79 |
|
| 80 |
return HierarchicalOrchestrator
|
| 81 |
|
| 82 |
|
| 83 |
+
def get_magentic_orchestrator() -> type[AdvancedOrchestrator]:
|
| 84 |
+
"""Get the AdvancedOrchestrator class.
|
| 85 |
+
|
| 86 |
+
.. deprecated:: 0.1.0
|
| 87 |
+
Use :func:`get_advanced_orchestrator` instead.
|
| 88 |
+
The name 'magentic' was confusing with the 'magentic' PyPI package.
|
| 89 |
+
|
| 90 |
+
Returns:
|
| 91 |
+
The AdvancedOrchestrator class
|
| 92 |
+
"""
|
| 93 |
+
warnings.warn(
|
| 94 |
+
"get_magentic_orchestrator() is deprecated, use get_advanced_orchestrator() instead. "
|
| 95 |
+
"The name 'magentic' was confusing with the 'magentic' PyPI package.",
|
| 96 |
+
DeprecationWarning,
|
| 97 |
+
stacklevel=2,
|
| 98 |
+
)
|
| 99 |
return get_advanced_orchestrator()
|
| 100 |
|
| 101 |
|
| 102 |
__all__ = [
|
| 103 |
"JudgeHandlerProtocol",
|
| 104 |
"Orchestrator",
|
| 105 |
+
"OrchestratorProtocol",
|
| 106 |
"SearchHandlerProtocol",
|
| 107 |
"create_orchestrator",
|
| 108 |
"get_advanced_orchestrator",
|
|
@@ -279,6 +279,26 @@ The final output should be a structured research report."""
|
|
| 279 |
# taking care to avoid infinite recursion if str() calls .text
|
| 280 |
return str(message)
|
| 281 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 282 |
def _process_event(self, event: Any, iteration: int) -> AgentEvent | None:
|
| 283 |
"""Process workflow event into AgentEvent."""
|
| 284 |
if isinstance(event, MagenticOrchestratorMessageEvent):
|
|
@@ -293,17 +313,9 @@ The final output should be a structured research report."""
|
|
| 293 |
elif isinstance(event, MagenticAgentMessageEvent):
|
| 294 |
agent_name = event.agent_id or "unknown"
|
| 295 |
text = self._extract_text(event.message)
|
|
|
|
| 296 |
|
| 297 |
-
|
| 298 |
-
if "search" in agent_name.lower():
|
| 299 |
-
event_type = "search_complete"
|
| 300 |
-
elif "judge" in agent_name.lower():
|
| 301 |
-
event_type = "judge_complete"
|
| 302 |
-
elif "hypothes" in agent_name.lower():
|
| 303 |
-
event_type = "hypothesizing"
|
| 304 |
-
elif "report" in agent_name.lower():
|
| 305 |
-
event_type = "synthesizing"
|
| 306 |
-
|
| 307 |
return AgentEvent(
|
| 308 |
type=event_type, # type: ignore[arg-type]
|
| 309 |
message=f"{agent_name}: {text[:200]}...",
|
|
@@ -339,6 +351,28 @@ The final output should be a structured research report."""
|
|
| 339 |
return None
|
| 340 |
|
| 341 |
|
| 342 |
-
|
| 343 |
-
|
| 344 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 279 |
# taking care to avoid infinite recursion if str() calls .text
|
| 280 |
return str(message)
|
| 281 |
|
| 282 |
+
def _get_event_type_for_agent(self, agent_name: str) -> str:
|
| 283 |
+
"""Map agent name to appropriate event type.
|
| 284 |
+
|
| 285 |
+
Args:
|
| 286 |
+
agent_name: The agent ID from the workflow event
|
| 287 |
+
|
| 288 |
+
Returns:
|
| 289 |
+
Event type string matching AgentEvent.type Literal
|
| 290 |
+
"""
|
| 291 |
+
agent_lower = agent_name.lower()
|
| 292 |
+
if "search" in agent_lower:
|
| 293 |
+
return "search_complete"
|
| 294 |
+
if "judge" in agent_lower:
|
| 295 |
+
return "judge_complete"
|
| 296 |
+
if "hypothes" in agent_lower:
|
| 297 |
+
return "hypothesizing"
|
| 298 |
+
if "report" in agent_lower:
|
| 299 |
+
return "synthesizing"
|
| 300 |
+
return "judging" # Default for unknown agents
|
| 301 |
+
|
| 302 |
def _process_event(self, event: Any, iteration: int) -> AgentEvent | None:
|
| 303 |
"""Process workflow event into AgentEvent."""
|
| 304 |
if isinstance(event, MagenticOrchestratorMessageEvent):
|
|
|
|
| 313 |
elif isinstance(event, MagenticAgentMessageEvent):
|
| 314 |
agent_name = event.agent_id or "unknown"
|
| 315 |
text = self._extract_text(event.message)
|
| 316 |
+
event_type = self._get_event_type_for_agent(agent_name)
|
| 317 |
|
| 318 |
+
# All returned types are valid AgentEvent.type literals
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 319 |
return AgentEvent(
|
| 320 |
type=event_type, # type: ignore[arg-type]
|
| 321 |
message=f"{agent_name}: {text[:200]}...",
|
|
|
|
| 351 |
return None
|
| 352 |
|
| 353 |
|
| 354 |
+
def _create_deprecated_alias() -> type["AdvancedOrchestrator"]:
|
| 355 |
+
"""Create a deprecated alias that warns on use."""
|
| 356 |
+
import warnings
|
| 357 |
+
|
| 358 |
+
class MagenticOrchestrator(AdvancedOrchestrator):
|
| 359 |
+
"""Deprecated alias for AdvancedOrchestrator.
|
| 360 |
+
|
| 361 |
+
.. deprecated:: 0.1.0
|
| 362 |
+
Use :class:`AdvancedOrchestrator` instead.
|
| 363 |
+
"""
|
| 364 |
+
|
| 365 |
+
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
| 366 |
+
warnings.warn(
|
| 367 |
+
"MagenticOrchestrator is deprecated, use AdvancedOrchestrator instead. "
|
| 368 |
+
"The name 'magentic' was confusing with the 'magentic' PyPI package.",
|
| 369 |
+
DeprecationWarning,
|
| 370 |
+
stacklevel=2,
|
| 371 |
+
)
|
| 372 |
+
super().__init__(*args, **kwargs)
|
| 373 |
+
|
| 374 |
+
return MagenticOrchestrator
|
| 375 |
+
|
| 376 |
+
|
| 377 |
+
# Backwards compatibility alias with deprecation warning
|
| 378 |
+
MagenticOrchestrator = _create_deprecated_alias()
|
|
@@ -5,9 +5,10 @@ following the Interface Segregation Principle (ISP) and
|
|
| 5 |
Dependency Inversion Principle (DIP).
|
| 6 |
"""
|
| 7 |
|
| 8 |
-
from
|
|
|
|
| 9 |
|
| 10 |
-
from src.utils.models import Evidence, JudgeAssessment, SearchResult
|
| 11 |
|
| 12 |
|
| 13 |
class SearchHandlerProtocol(Protocol):
|
|
@@ -50,3 +51,23 @@ class JudgeHandlerProtocol(Protocol):
|
|
| 50 |
JudgeAssessment with sufficiency determination and next steps
|
| 51 |
"""
|
| 52 |
...
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
Dependency Inversion Principle (DIP).
|
| 6 |
"""
|
| 7 |
|
| 8 |
+
from collections.abc import AsyncGenerator
|
| 9 |
+
from typing import Protocol, runtime_checkable
|
| 10 |
|
| 11 |
+
from src.utils.models import AgentEvent, Evidence, JudgeAssessment, SearchResult
|
| 12 |
|
| 13 |
|
| 14 |
class SearchHandlerProtocol(Protocol):
|
|
|
|
| 51 |
JudgeAssessment with sufficiency determination and next steps
|
| 52 |
"""
|
| 53 |
...
|
| 54 |
+
|
| 55 |
+
|
| 56 |
+
@runtime_checkable
|
| 57 |
+
class OrchestratorProtocol(Protocol):
|
| 58 |
+
"""Protocol for orchestrators.
|
| 59 |
+
|
| 60 |
+
All orchestrators (Simple, Advanced, Hierarchical) implement this interface,
|
| 61 |
+
allowing them to be used interchangeably by the factory and UI.
|
| 62 |
+
"""
|
| 63 |
+
|
| 64 |
+
def run(self, query: str) -> AsyncGenerator[AgentEvent, None]:
|
| 65 |
+
"""Run the orchestrator workflow.
|
| 66 |
+
|
| 67 |
+
Args:
|
| 68 |
+
query: User's research question
|
| 69 |
+
|
| 70 |
+
Yields:
|
| 71 |
+
AgentEvent objects for real-time UI updates
|
| 72 |
+
"""
|
| 73 |
+
...
|
|
@@ -9,22 +9,35 @@ Design Principles:
|
|
| 9 |
- Single Responsibility: Only handles orchestrator creation logic
|
| 10 |
"""
|
| 11 |
|
| 12 |
-
from typing import
|
| 13 |
|
| 14 |
import structlog
|
| 15 |
|
| 16 |
-
from src.orchestrators.base import
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
from src.orchestrators.simple import Orchestrator
|
| 18 |
from src.utils.config import settings
|
| 19 |
from src.utils.models import OrchestratorConfig
|
| 20 |
|
|
|
|
|
|
|
|
|
|
| 21 |
logger = structlog.get_logger()
|
| 22 |
|
| 23 |
|
| 24 |
-
def _get_advanced_orchestrator_class() ->
|
| 25 |
"""Import AdvancedOrchestrator lazily to avoid hard dependency.
|
| 26 |
|
| 27 |
This allows the simple mode to work without agent-framework-core installed.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
"""
|
| 29 |
try:
|
| 30 |
from src.orchestrators.advanced import AdvancedOrchestrator
|
|
@@ -33,7 +46,9 @@ def _get_advanced_orchestrator_class() -> Any:
|
|
| 33 |
except ImportError as e:
|
| 34 |
logger.error("Failed to import AdvancedOrchestrator", error=str(e))
|
| 35 |
raise ValueError(
|
| 36 |
-
"Advanced mode requires agent-framework-core.
|
|
|
|
|
|
|
| 37 |
) from e
|
| 38 |
|
| 39 |
|
|
@@ -43,7 +58,7 @@ def create_orchestrator(
|
|
| 43 |
config: OrchestratorConfig | None = None,
|
| 44 |
mode: Literal["simple", "magentic", "advanced", "hierarchical"] | None = None,
|
| 45 |
api_key: str | None = None,
|
| 46 |
-
) ->
|
| 47 |
"""
|
| 48 |
Create an orchestrator instance.
|
| 49 |
|
|
@@ -54,32 +69,33 @@ def create_orchestrator(
|
|
| 54 |
Args:
|
| 55 |
search_handler: The search handler (required for simple mode)
|
| 56 |
judge_handler: The judge handler (required for simple mode)
|
| 57 |
-
config: Optional configuration
|
| 58 |
mode: "simple", "magentic", "advanced", "hierarchical" or None (auto-detect)
|
| 59 |
Note: "magentic" is an alias for "advanced" (kept for backwards compatibility)
|
| 60 |
api_key: Optional API key for advanced mode (OpenAI)
|
| 61 |
|
| 62 |
Returns:
|
| 63 |
-
Orchestrator instance
|
| 64 |
|
| 65 |
Raises:
|
| 66 |
ValueError: If required handlers are missing for simple mode
|
| 67 |
ValueError: If advanced mode is requested but dependencies are missing
|
| 68 |
"""
|
|
|
|
| 69 |
effective_mode = _determine_mode(mode, api_key)
|
| 70 |
logger.info("Creating orchestrator", mode=effective_mode)
|
| 71 |
|
| 72 |
if effective_mode == "advanced":
|
| 73 |
orchestrator_cls = _get_advanced_orchestrator_class()
|
| 74 |
return orchestrator_cls(
|
| 75 |
-
max_rounds=
|
| 76 |
api_key=api_key,
|
| 77 |
)
|
| 78 |
|
| 79 |
if effective_mode == "hierarchical":
|
| 80 |
from src.orchestrators.hierarchical import HierarchicalOrchestrator
|
| 81 |
|
| 82 |
-
return HierarchicalOrchestrator()
|
| 83 |
|
| 84 |
# Simple mode requires handlers
|
| 85 |
if search_handler is None or judge_handler is None:
|
|
@@ -88,7 +104,7 @@ def create_orchestrator(
|
|
| 88 |
return Orchestrator(
|
| 89 |
search_handler=search_handler,
|
| 90 |
judge_handler=judge_handler,
|
| 91 |
-
config=
|
| 92 |
)
|
| 93 |
|
| 94 |
|
|
|
|
| 9 |
- Single Responsibility: Only handles orchestrator creation logic
|
| 10 |
"""
|
| 11 |
|
| 12 |
+
from typing import TYPE_CHECKING, Literal
|
| 13 |
|
| 14 |
import structlog
|
| 15 |
|
| 16 |
+
from src.orchestrators.base import (
|
| 17 |
+
JudgeHandlerProtocol,
|
| 18 |
+
OrchestratorProtocol,
|
| 19 |
+
SearchHandlerProtocol,
|
| 20 |
+
)
|
| 21 |
from src.orchestrators.simple import Orchestrator
|
| 22 |
from src.utils.config import settings
|
| 23 |
from src.utils.models import OrchestratorConfig
|
| 24 |
|
| 25 |
+
if TYPE_CHECKING:
|
| 26 |
+
from src.orchestrators.advanced import AdvancedOrchestrator
|
| 27 |
+
|
| 28 |
logger = structlog.get_logger()
|
| 29 |
|
| 30 |
|
| 31 |
+
def _get_advanced_orchestrator_class() -> type["AdvancedOrchestrator"]:
|
| 32 |
"""Import AdvancedOrchestrator lazily to avoid hard dependency.
|
| 33 |
|
| 34 |
This allows the simple mode to work without agent-framework-core installed.
|
| 35 |
+
|
| 36 |
+
Returns:
|
| 37 |
+
The AdvancedOrchestrator class
|
| 38 |
+
|
| 39 |
+
Raises:
|
| 40 |
+
ValueError: If agent-framework-core is not installed
|
| 41 |
"""
|
| 42 |
try:
|
| 43 |
from src.orchestrators.advanced import AdvancedOrchestrator
|
|
|
|
| 46 |
except ImportError as e:
|
| 47 |
logger.error("Failed to import AdvancedOrchestrator", error=str(e))
|
| 48 |
raise ValueError(
|
| 49 |
+
"Advanced mode requires agent-framework-core. "
|
| 50 |
+
"Install with: pip install agent-framework-core. "
|
| 51 |
+
"Or use mode='simple' instead."
|
| 52 |
) from e
|
| 53 |
|
| 54 |
|
|
|
|
| 58 |
config: OrchestratorConfig | None = None,
|
| 59 |
mode: Literal["simple", "magentic", "advanced", "hierarchical"] | None = None,
|
| 60 |
api_key: str | None = None,
|
| 61 |
+
) -> OrchestratorProtocol:
|
| 62 |
"""
|
| 63 |
Create an orchestrator instance.
|
| 64 |
|
|
|
|
| 69 |
Args:
|
| 70 |
search_handler: The search handler (required for simple mode)
|
| 71 |
judge_handler: The judge handler (required for simple mode)
|
| 72 |
+
config: Optional configuration (max_iterations, timeouts, etc.)
|
| 73 |
mode: "simple", "magentic", "advanced", "hierarchical" or None (auto-detect)
|
| 74 |
Note: "magentic" is an alias for "advanced" (kept for backwards compatibility)
|
| 75 |
api_key: Optional API key for advanced mode (OpenAI)
|
| 76 |
|
| 77 |
Returns:
|
| 78 |
+
Orchestrator instance implementing OrchestratorProtocol
|
| 79 |
|
| 80 |
Raises:
|
| 81 |
ValueError: If required handlers are missing for simple mode
|
| 82 |
ValueError: If advanced mode is requested but dependencies are missing
|
| 83 |
"""
|
| 84 |
+
effective_config = config or OrchestratorConfig()
|
| 85 |
effective_mode = _determine_mode(mode, api_key)
|
| 86 |
logger.info("Creating orchestrator", mode=effective_mode)
|
| 87 |
|
| 88 |
if effective_mode == "advanced":
|
| 89 |
orchestrator_cls = _get_advanced_orchestrator_class()
|
| 90 |
return orchestrator_cls(
|
| 91 |
+
max_rounds=effective_config.max_iterations,
|
| 92 |
api_key=api_key,
|
| 93 |
)
|
| 94 |
|
| 95 |
if effective_mode == "hierarchical":
|
| 96 |
from src.orchestrators.hierarchical import HierarchicalOrchestrator
|
| 97 |
|
| 98 |
+
return HierarchicalOrchestrator(config=effective_config)
|
| 99 |
|
| 100 |
# Simple mode requires handlers
|
| 101 |
if search_handler is None or judge_handler is None:
|
|
|
|
| 104 |
return Orchestrator(
|
| 105 |
search_handler=search_handler,
|
| 106 |
judge_handler=judge_handler,
|
| 107 |
+
config=effective_config,
|
| 108 |
)
|
| 109 |
|
| 110 |
|
|
@@ -21,10 +21,13 @@ from src.agents.magentic_agents import create_search_agent
|
|
| 21 |
from src.middleware.sub_iteration import SubIterationMiddleware, SubIterationTeam
|
| 22 |
from src.services.embeddings import get_embedding_service
|
| 23 |
from src.state import init_magentic_state
|
| 24 |
-
from src.utils.models import AgentEvent
|
| 25 |
|
| 26 |
logger = structlog.get_logger()
|
| 27 |
|
|
|
|
|
|
|
|
|
|
| 28 |
|
| 29 |
class ResearchTeam(SubIterationTeam):
|
| 30 |
"""Adapts ChatAgent to SubIterationTeam protocol.
|
|
@@ -60,13 +63,27 @@ class HierarchicalOrchestrator:
|
|
| 60 |
- Sub-iteration middleware for fine-grained control
|
| 61 |
- LLM-based judge for sub-iteration decisions
|
| 62 |
- Event-driven architecture for UI updates
|
|
|
|
| 63 |
"""
|
| 64 |
|
| 65 |
-
def __init__(
|
| 66 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 67 |
self.team = ResearchTeam()
|
| 68 |
self.judge = LLMSubIterationJudge()
|
| 69 |
-
self.middleware = SubIterationMiddleware(
|
|
|
|
|
|
|
| 70 |
|
| 71 |
async def run(self, query: str) -> AsyncGenerator[AgentEvent, None]:
|
| 72 |
"""Run the hierarchical workflow.
|
|
@@ -82,10 +99,14 @@ class HierarchicalOrchestrator:
|
|
| 82 |
try:
|
| 83 |
service = get_embedding_service()
|
| 84 |
init_magentic_state(service)
|
|
|
|
|
|
|
|
|
|
| 85 |
except Exception as e:
|
| 86 |
logger.warning(
|
| 87 |
-
"Embedding service initialization failed
|
| 88 |
error=str(e),
|
|
|
|
| 89 |
)
|
| 90 |
init_magentic_state()
|
| 91 |
|
|
@@ -96,38 +117,52 @@ class HierarchicalOrchestrator:
|
|
| 96 |
async def event_callback(event: AgentEvent) -> None:
|
| 97 |
await queue.put(event)
|
| 98 |
|
| 99 |
-
task_future = asyncio.create_task(self.middleware.run(query, event_callback))
|
| 100 |
-
|
| 101 |
-
while not task_future.done():
|
| 102 |
-
get_event = asyncio.create_task(queue.get())
|
| 103 |
-
done, _ = await asyncio.wait(
|
| 104 |
-
{task_future, get_event}, return_when=asyncio.FIRST_COMPLETED
|
| 105 |
-
)
|
| 106 |
-
|
| 107 |
-
if get_event in done:
|
| 108 |
-
event = get_event.result()
|
| 109 |
-
if event:
|
| 110 |
-
yield event
|
| 111 |
-
else:
|
| 112 |
-
get_event.cancel()
|
| 113 |
-
|
| 114 |
-
# Process remaining events
|
| 115 |
-
while not queue.empty():
|
| 116 |
-
ev = queue.get_nowait()
|
| 117 |
-
if ev:
|
| 118 |
-
yield ev
|
| 119 |
-
|
| 120 |
try:
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 124 |
yield AgentEvent(
|
| 125 |
type="complete",
|
| 126 |
-
message=
|
| 127 |
-
|
| 128 |
-
),
|
| 129 |
-
data={"assessment": assessment.model_dump() if assessment else None},
|
| 130 |
)
|
|
|
|
| 131 |
except Exception as e:
|
| 132 |
-
logger.error(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 133 |
yield AgentEvent(type="error", message=f"Orchestrator failed: {e}")
|
|
|
|
| 21 |
from src.middleware.sub_iteration import SubIterationMiddleware, SubIterationTeam
|
| 22 |
from src.services.embeddings import get_embedding_service
|
| 23 |
from src.state import init_magentic_state
|
| 24 |
+
from src.utils.models import AgentEvent, OrchestratorConfig
|
| 25 |
|
| 26 |
logger = structlog.get_logger()
|
| 27 |
|
| 28 |
+
# Default timeout for hierarchical orchestrator (5 minutes)
|
| 29 |
+
DEFAULT_TIMEOUT_SECONDS = 300.0
|
| 30 |
+
|
| 31 |
|
| 32 |
class ResearchTeam(SubIterationTeam):
|
| 33 |
"""Adapts ChatAgent to SubIterationTeam protocol.
|
|
|
|
| 63 |
- Sub-iteration middleware for fine-grained control
|
| 64 |
- LLM-based judge for sub-iteration decisions
|
| 65 |
- Event-driven architecture for UI updates
|
| 66 |
+
- Configurable iterations and timeout
|
| 67 |
"""
|
| 68 |
|
| 69 |
+
def __init__(
|
| 70 |
+
self,
|
| 71 |
+
config: OrchestratorConfig | None = None,
|
| 72 |
+
timeout_seconds: float = DEFAULT_TIMEOUT_SECONDS,
|
| 73 |
+
) -> None:
|
| 74 |
+
"""Initialize the hierarchical orchestrator.
|
| 75 |
+
|
| 76 |
+
Args:
|
| 77 |
+
config: Optional configuration (uses defaults if not provided)
|
| 78 |
+
timeout_seconds: Maximum workflow duration (default: 5 minutes)
|
| 79 |
+
"""
|
| 80 |
+
self.config = config or OrchestratorConfig()
|
| 81 |
+
self._timeout_seconds = timeout_seconds
|
| 82 |
self.team = ResearchTeam()
|
| 83 |
self.judge = LLMSubIterationJudge()
|
| 84 |
+
self.middleware = SubIterationMiddleware(
|
| 85 |
+
self.team, self.judge, max_iterations=self.config.max_iterations
|
| 86 |
+
)
|
| 87 |
|
| 88 |
async def run(self, query: str) -> AsyncGenerator[AgentEvent, None]:
|
| 89 |
"""Run the hierarchical workflow.
|
|
|
|
| 99 |
try:
|
| 100 |
service = get_embedding_service()
|
| 101 |
init_magentic_state(service)
|
| 102 |
+
except ImportError:
|
| 103 |
+
logger.info("Embedding service not available (dependencies missing)")
|
| 104 |
+
init_magentic_state()
|
| 105 |
except Exception as e:
|
| 106 |
logger.warning(
|
| 107 |
+
"Embedding service initialization failed",
|
| 108 |
error=str(e),
|
| 109 |
+
error_type=type(e).__name__,
|
| 110 |
)
|
| 111 |
init_magentic_state()
|
| 112 |
|
|
|
|
| 117 |
async def event_callback(event: AgentEvent) -> None:
|
| 118 |
await queue.put(event)
|
| 119 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 120 |
try:
|
| 121 |
+
async with asyncio.timeout(self._timeout_seconds):
|
| 122 |
+
task_future = asyncio.create_task(self.middleware.run(query, event_callback))
|
| 123 |
+
|
| 124 |
+
while not task_future.done():
|
| 125 |
+
get_event = asyncio.create_task(queue.get())
|
| 126 |
+
done, _ = await asyncio.wait(
|
| 127 |
+
{task_future, get_event}, return_when=asyncio.FIRST_COMPLETED
|
| 128 |
+
)
|
| 129 |
+
|
| 130 |
+
if get_event in done:
|
| 131 |
+
event = get_event.result()
|
| 132 |
+
if event:
|
| 133 |
+
yield event
|
| 134 |
+
else:
|
| 135 |
+
get_event.cancel()
|
| 136 |
+
|
| 137 |
+
# Process remaining events
|
| 138 |
+
while not queue.empty():
|
| 139 |
+
ev = queue.get_nowait()
|
| 140 |
+
if ev:
|
| 141 |
+
yield ev
|
| 142 |
+
|
| 143 |
+
result, assessment = await task_future
|
| 144 |
+
|
| 145 |
+
assessment_text = assessment.reasoning if assessment else "None"
|
| 146 |
+
yield AgentEvent(
|
| 147 |
+
type="complete",
|
| 148 |
+
message=(
|
| 149 |
+
f"Research complete.\n\nResult:\n{result}\n\nAssessment:\n{assessment_text}"
|
| 150 |
+
),
|
| 151 |
+
data={"assessment": assessment.model_dump() if assessment else None},
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
except TimeoutError:
|
| 155 |
+
logger.warning("Hierarchical workflow timed out", query=query)
|
| 156 |
yield AgentEvent(
|
| 157 |
type="complete",
|
| 158 |
+
message="Research timed out. Results may be incomplete.",
|
| 159 |
+
data={"reason": "timeout"},
|
|
|
|
|
|
|
| 160 |
)
|
| 161 |
+
|
| 162 |
except Exception as e:
|
| 163 |
+
logger.error(
|
| 164 |
+
"Orchestrator failed",
|
| 165 |
+
error=str(e),
|
| 166 |
+
error_type=type(e).__name__,
|
| 167 |
+
)
|
| 168 |
yield AgentEvent(type="error", message=f"Orchestrator failed: {e}")
|
|
@@ -8,9 +8,11 @@ Design Pattern: Template Method - defines the skeleton of the search-judge loop
|
|
| 8 |
while allowing handlers to implement specific behaviors.
|
| 9 |
"""
|
| 10 |
|
|
|
|
|
|
|
| 11 |
import asyncio
|
| 12 |
from collections.abc import AsyncGenerator
|
| 13 |
-
from typing import Any
|
| 14 |
|
| 15 |
import structlog
|
| 16 |
|
|
@@ -24,6 +26,10 @@ from src.utils.models import (
|
|
| 24 |
SearchResult,
|
| 25 |
)
|
| 26 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
logger = structlog.get_logger()
|
| 28 |
|
| 29 |
|
|
@@ -61,26 +67,36 @@ class Orchestrator:
|
|
| 61 |
self._enable_analysis = enable_analysis and settings.modal_available
|
| 62 |
self._enable_embeddings = enable_embeddings
|
| 63 |
|
| 64 |
-
# Lazy-load services
|
| 65 |
-
self._analyzer:
|
| 66 |
-
self._embeddings:
|
| 67 |
|
| 68 |
-
def _get_analyzer(self) ->
|
| 69 |
"""Lazy initialization of StatisticalAnalyzer.
|
| 70 |
|
| 71 |
Note: This imports from src.services, NOT src.agents,
|
| 72 |
so it works without the magentic optional dependency.
|
|
|
|
|
|
|
|
|
|
| 73 |
"""
|
| 74 |
if self._analyzer is None:
|
| 75 |
-
|
|
|
|
| 76 |
|
| 77 |
-
|
|
|
|
|
|
|
|
|
|
| 78 |
return self._analyzer
|
| 79 |
|
| 80 |
-
def _get_embeddings(self) ->
|
| 81 |
"""Lazy initialization of EmbeddingService.
|
| 82 |
|
| 83 |
Uses local sentence-transformers - NO API key required.
|
|
|
|
|
|
|
|
|
|
| 84 |
"""
|
| 85 |
if self._embeddings is None and self._enable_embeddings:
|
| 86 |
try:
|
|
@@ -88,8 +104,15 @@ class Orchestrator:
|
|
| 88 |
|
| 89 |
self._embeddings = get_embedding_service()
|
| 90 |
logger.info("Embedding service enabled for semantic ranking")
|
|
|
|
|
|
|
|
|
|
| 91 |
except Exception as e:
|
| 92 |
-
logger.warning(
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
self._enable_embeddings = False
|
| 94 |
return self._embeddings
|
| 95 |
|
|
@@ -128,6 +151,9 @@ class Orchestrator:
|
|
| 128 |
|
| 129 |
try:
|
| 130 |
analyzer = self._get_analyzer()
|
|
|
|
|
|
|
|
|
|
| 131 |
|
| 132 |
# Run Modal analysis (no agent_framework needed!)
|
| 133 |
analysis_result = await analyzer.analyze(
|
|
|
|
| 8 |
while allowing handlers to implement specific behaviors.
|
| 9 |
"""
|
| 10 |
|
| 11 |
+
from __future__ import annotations
|
| 12 |
+
|
| 13 |
import asyncio
|
| 14 |
from collections.abc import AsyncGenerator
|
| 15 |
+
from typing import TYPE_CHECKING, Any
|
| 16 |
|
| 17 |
import structlog
|
| 18 |
|
|
|
|
| 26 |
SearchResult,
|
| 27 |
)
|
| 28 |
|
| 29 |
+
if TYPE_CHECKING:
|
| 30 |
+
from src.services.embeddings import EmbeddingService
|
| 31 |
+
from src.services.statistical_analyzer import StatisticalAnalyzer
|
| 32 |
+
|
| 33 |
logger = structlog.get_logger()
|
| 34 |
|
| 35 |
|
|
|
|
| 67 |
self._enable_analysis = enable_analysis and settings.modal_available
|
| 68 |
self._enable_embeddings = enable_embeddings
|
| 69 |
|
| 70 |
+
# Lazy-load services (typed for IDE support)
|
| 71 |
+
self._analyzer: StatisticalAnalyzer | None = None
|
| 72 |
+
self._embeddings: EmbeddingService | None = None
|
| 73 |
|
| 74 |
+
def _get_analyzer(self) -> StatisticalAnalyzer | None:
|
| 75 |
"""Lazy initialization of StatisticalAnalyzer.
|
| 76 |
|
| 77 |
Note: This imports from src.services, NOT src.agents,
|
| 78 |
so it works without the magentic optional dependency.
|
| 79 |
+
|
| 80 |
+
Returns:
|
| 81 |
+
StatisticalAnalyzer instance, or None if Modal is unavailable
|
| 82 |
"""
|
| 83 |
if self._analyzer is None:
|
| 84 |
+
try:
|
| 85 |
+
from src.services.statistical_analyzer import get_statistical_analyzer
|
| 86 |
|
| 87 |
+
self._analyzer = get_statistical_analyzer()
|
| 88 |
+
except ImportError:
|
| 89 |
+
logger.info("StatisticalAnalyzer not available (Modal dependencies missing)")
|
| 90 |
+
self._enable_analysis = False
|
| 91 |
return self._analyzer
|
| 92 |
|
| 93 |
+
def _get_embeddings(self) -> EmbeddingService | None:
|
| 94 |
"""Lazy initialization of EmbeddingService.
|
| 95 |
|
| 96 |
Uses local sentence-transformers - NO API key required.
|
| 97 |
+
|
| 98 |
+
Returns:
|
| 99 |
+
EmbeddingService instance, or None if unavailable
|
| 100 |
"""
|
| 101 |
if self._embeddings is None and self._enable_embeddings:
|
| 102 |
try:
|
|
|
|
| 104 |
|
| 105 |
self._embeddings = get_embedding_service()
|
| 106 |
logger.info("Embedding service enabled for semantic ranking")
|
| 107 |
+
except ImportError:
|
| 108 |
+
logger.info("Embedding service not available (dependencies missing)")
|
| 109 |
+
self._enable_embeddings = False
|
| 110 |
except Exception as e:
|
| 111 |
+
logger.warning(
|
| 112 |
+
"Embedding service initialization failed",
|
| 113 |
+
error=str(e),
|
| 114 |
+
error_type=type(e).__name__,
|
| 115 |
+
)
|
| 116 |
self._enable_embeddings = False
|
| 117 |
return self._embeddings
|
| 118 |
|
|
|
|
| 151 |
|
| 152 |
try:
|
| 153 |
analyzer = self._get_analyzer()
|
| 154 |
+
if analyzer is None:
|
| 155 |
+
logger.info("StatisticalAnalyzer not available, skipping analysis phase")
|
| 156 |
+
return
|
| 157 |
|
| 158 |
# Run Modal analysis (no agent_framework needed!)
|
| 159 |
analysis_result = await analyzer.analyze(
|