Claude commited on
Commit
11888fc
·
1 Parent(s): e82d9c9

fix: Address audit findings in orchestrators package

Browse files

CRITICAL 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 CHANGED
@@ -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 JudgeHandlerProtocol, SearchHandlerProtocol
 
 
 
 
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
- # Backwards compatibility aliases
57
- # TODO: Remove after migration period
58
- def get_magentic_orchestrator() -> type:
59
- """Deprecated: Use get_advanced_orchestrator() instead."""
 
 
 
 
 
 
 
 
 
 
 
 
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",
src/orchestrators/advanced.py CHANGED
@@ -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
- event_type = "judging"
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
- # Backwards compatibility alias
343
- # TODO: Remove after all imports are updated
344
- MagenticOrchestrator = AdvancedOrchestrator
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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()
src/orchestrators/base.py CHANGED
@@ -5,9 +5,10 @@ following the Interface Segregation Principle (ISP) and
5
  Dependency Inversion Principle (DIP).
6
  """
7
 
8
- from typing import Protocol
 
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
+ ...
src/orchestrators/factory.py CHANGED
@@ -9,22 +9,35 @@ Design Principles:
9
  - Single Responsibility: Only handles orchestrator creation logic
10
  """
11
 
12
- from typing import Any, Literal
13
 
14
  import structlog
15
 
16
- from src.orchestrators.base import JudgeHandlerProtocol, SearchHandlerProtocol
 
 
 
 
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() -> Any:
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. Please install it or use mode='simple'."
 
 
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
- ) -> Any:
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=config.max_iterations if config else 10,
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=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
 
src/orchestrators/hierarchical.py CHANGED
@@ -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__(self) -> None:
66
- """Initialize the hierarchical orchestrator."""
 
 
 
 
 
 
 
 
 
 
 
67
  self.team = ResearchTeam()
68
  self.judge = LLMSubIterationJudge()
69
- self.middleware = SubIterationMiddleware(self.team, self.judge, max_iterations=5)
 
 
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, using default state",
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
- result, assessment = await task_future
122
-
123
- assessment_text = assessment.reasoning if assessment else "None"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  yield AgentEvent(
125
  type="complete",
126
- message=(
127
- f"Research complete.\n\nResult:\n{result}\n\nAssessment:\n{assessment_text}"
128
- ),
129
- data={"assessment": assessment.model_dump() if assessment else None},
130
  )
 
131
  except Exception as e:
132
- logger.error("Orchestrator failed", error=str(e))
 
 
 
 
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}")
src/orchestrators/simple.py CHANGED
@@ -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: Any = None
66
- self._embeddings: Any = None
67
 
68
- def _get_analyzer(self) -> Any:
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
- from src.services.statistical_analyzer import get_statistical_analyzer
 
76
 
77
- self._analyzer = get_statistical_analyzer()
 
 
 
78
  return self._analyzer
79
 
80
- def _get_embeddings(self) -> Any:
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("Embeddings unavailable, using basic ranking", error=str(e))
 
 
 
 
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(