VibecoderMcSwaggins commited on
Commit
3cb2e43
·
unverified ·
1 Parent(s): 04336d8

feat: SPEC_15 Advanced Mode Performance Optimization (#101)

Browse files

SPEC_15 Advanced Mode Performance Optimization + CodeRabbit fixes

examples/benchmark_advanced.py ADDED
@@ -0,0 +1,42 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Benchmark Advanced mode with different max_rounds settings."""
2
+
3
+ import asyncio
4
+ import time
5
+
6
+ from src.orchestrators.advanced import AdvancedOrchestrator
7
+
8
+
9
+ async def benchmark(max_rounds: int) -> float:
10
+ """Run benchmark with specified rounds, return elapsed time."""
11
+ # Pass max_rounds explicitly instead of mutating os.environ
12
+ orch = AdvancedOrchestrator(max_rounds=max_rounds)
13
+ start = time.time()
14
+
15
+ print(f"\nStarting benchmark with max_rounds={max_rounds}...")
16
+
17
+ try:
18
+ async for event in orch.run("sildenafil erectile dysfunction mechanism"):
19
+ if event.type == "progress":
20
+ print(f" Progress: {event.message}")
21
+ elif event.type == "complete":
22
+ print(" Complete!")
23
+ break
24
+ elif event.type == "error":
25
+ print(f" Error: {event.message}")
26
+ break
27
+ except Exception as e:
28
+ print(f" Exception: {e}")
29
+
30
+ return time.time() - start
31
+
32
+
33
+ async def main() -> None:
34
+ """Run benchmarks for different configurations."""
35
+ # Only run a quick test for 3 rounds to verify it works
36
+ rounds = 3
37
+ elapsed = await benchmark(rounds)
38
+ print(f"max_rounds={rounds}: {elapsed:.1f}s ({elapsed / 60:.1f}min)")
39
+
40
+
41
+ if __name__ == "__main__":
42
+ asyncio.run(main())
src/agents/magentic_agents.py CHANGED
@@ -89,6 +89,19 @@ When asked to evaluate:
89
  - Insufficient: Gaps in mechanism OR weak clinical evidence
90
  4. If insufficient, suggest specific search queries to fill gaps
91
 
 
 
 
 
 
 
 
 
 
 
 
 
 
92
  Be rigorous but fair. Look for:
93
  - Molecular targets and pathways
94
  - Animal model studies
 
89
  - Insufficient: Gaps in mechanism OR weak clinical evidence
90
  4. If insufficient, suggest specific search queries to fill gaps
91
 
92
+ ## CRITICAL OUTPUT FORMAT
93
+ To ensure the workflow terminates when appropriate, you MUST follow these rules:
94
+
95
+ IF evidence is SUFFICIENT (confidence >= 70%):
96
+ Start your response with a line like:
97
+ "✅ SUFFICIENT EVIDENCE (confidence: 72%). STOP SEARCHING. Delegate to ReportAgent NOW."
98
+ Use your actual numeric confidence instead of 72.
99
+ Then explain why.
100
+
101
+ IF evidence is INSUFFICIENT:
102
+ Start with "❌ INSUFFICIENT: <Reason>."
103
+ Then provide scores and next queries.
104
+
105
  Be rigorous but fair. Look for:
106
  - Molecular targets and pathways
107
  - Animal model studies
src/orchestrators/advanced.py CHANGED
@@ -15,6 +15,7 @@ Design Patterns:
15
  """
16
 
17
  import asyncio
 
18
  from collections.abc import AsyncGenerator
19
  from typing import TYPE_CHECKING, Any
20
 
@@ -65,10 +66,10 @@ class AdvancedOrchestrator(OrchestratorProtocol):
65
 
66
  def __init__(
67
  self,
68
- max_rounds: int = 10,
69
  chat_client: OpenAIChatClient | None = None,
70
  api_key: str | None = None,
71
- timeout_seconds: float = 600.0,
72
  domain: ResearchDomain | str | None = None,
73
  ) -> None:
74
  """Initialize orchestrator.
@@ -77,14 +78,33 @@ class AdvancedOrchestrator(OrchestratorProtocol):
77
  max_rounds: Maximum coordination rounds
78
  chat_client: Optional shared chat client for agents
79
  api_key: Optional OpenAI API key (for BYOK)
80
- timeout_seconds: Maximum workflow duration (default: 10 minutes)
81
  domain: Research domain for customization
82
  """
83
  # Validate requirements only if no key provided
84
  if not chat_client and not api_key:
85
  check_magentic_requirements()
86
 
87
- self._max_rounds = max_rounds
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
88
  self._timeout_seconds = timeout_seconds
89
  self.domain = domain
90
  self.domain_config = get_domain_config(domain)
@@ -163,7 +183,13 @@ class AdvancedOrchestrator(OrchestratorProtocol):
163
 
164
  task = f"""Research {self.domain_config.report_focus} for: {query}
165
 
166
- Workflow:
 
 
 
 
 
 
167
  1. SearchAgent: Find evidence from PubMed, ClinicalTrials.gov, and Europe PMC
168
  2. HypothesisAgent: Generate mechanistic hypotheses (Drug -> Target -> Pathway -> Effect)
169
  3. JudgeAgent: Evaluate if evidence is sufficient
@@ -182,8 +208,9 @@ The final output should be a structured research report."""
182
  yield AgentEvent(
183
  type="thinking",
184
  message=(
185
- "Multi-agent reasoning in progress... "
186
- "This may take 2-5 minutes for complex queries."
 
187
  ),
188
  iteration=0,
189
  )
@@ -198,10 +225,23 @@ The final output should be a structured research report."""
198
  if agent_event:
199
  if isinstance(event, MagenticAgentMessageEvent):
200
  iteration += 1
 
 
 
 
 
 
 
 
 
 
 
 
 
201
  # Yield progress update before the agent action
202
  yield AgentEvent(
203
  type="progress",
204
- message=f"Round {iteration}/{self._max_rounds}...",
205
  iteration=iteration,
206
  )
207
 
 
15
  """
16
 
17
  import asyncio
18
+ import os
19
  from collections.abc import AsyncGenerator
20
  from typing import TYPE_CHECKING, Any
21
 
 
66
 
67
  def __init__(
68
  self,
69
+ max_rounds: int | None = None,
70
  chat_client: OpenAIChatClient | None = None,
71
  api_key: str | None = None,
72
+ timeout_seconds: float = 300.0,
73
  domain: ResearchDomain | str | None = None,
74
  ) -> None:
75
  """Initialize orchestrator.
 
78
  max_rounds: Maximum coordination rounds
79
  chat_client: Optional shared chat client for agents
80
  api_key: Optional OpenAI API key (for BYOK)
81
+ timeout_seconds: Maximum workflow duration (default: 5 minutes)
82
  domain: Research domain for customization
83
  """
84
  # Validate requirements only if no key provided
85
  if not chat_client and not api_key:
86
  check_magentic_requirements()
87
 
88
+ # Environment-configurable rounds (default 5 for demos)
89
+ raw_rounds = os.getenv("ADVANCED_MAX_ROUNDS", "5")
90
+ try:
91
+ env_rounds = int(raw_rounds)
92
+ except ValueError:
93
+ logger.warning(
94
+ "Invalid ADVANCED_MAX_ROUNDS value %r, falling back to 5",
95
+ raw_rounds,
96
+ )
97
+ env_rounds = 5
98
+
99
+ if env_rounds < 1:
100
+ logger.warning(
101
+ "ADVANCED_MAX_ROUNDS must be >= 1, got %d; using 1 instead",
102
+ env_rounds,
103
+ )
104
+ env_rounds = 1
105
+
106
+ self._max_rounds = max_rounds if max_rounds is not None else env_rounds
107
+
108
  self._timeout_seconds = timeout_seconds
109
  self.domain = domain
110
  self.domain_config = get_domain_config(domain)
 
183
 
184
  task = f"""Research {self.domain_config.report_focus} for: {query}
185
 
186
+ ## CRITICAL RULE
187
+ When JudgeAgent says "SUFFICIENT EVIDENCE" or "STOP SEARCHING":
188
+ → IMMEDIATELY delegate to ReportAgent for synthesis
189
+ → Do NOT continue searching or gathering more evidence
190
+ → The Judge has determined evidence quality is adequate
191
+
192
+ ## Standard Workflow
193
  1. SearchAgent: Find evidence from PubMed, ClinicalTrials.gov, and Europe PMC
194
  2. HypothesisAgent: Generate mechanistic hypotheses (Drug -> Target -> Pathway -> Effect)
195
  3. JudgeAgent: Evaluate if evidence is sufficient
 
208
  yield AgentEvent(
209
  type="thinking",
210
  message=(
211
+ f"Multi-agent reasoning in progress ({self._max_rounds} rounds max)... "
212
+ f"Estimated time: {self._max_rounds * 45 // 60}-"
213
+ f"{self._max_rounds * 60 // 60} minutes."
214
  ),
215
  iteration=0,
216
  )
 
225
  if agent_event:
226
  if isinstance(event, MagenticAgentMessageEvent):
227
  iteration += 1
228
+
229
+ # Progress estimation (clamp to avoid negative values)
230
+ rounds_remaining = max(self._max_rounds - iteration, 0)
231
+ est_seconds = rounds_remaining * 45
232
+ if est_seconds >= 60:
233
+ est_display = f"{est_seconds // 60}m {est_seconds % 60}s"
234
+ else:
235
+ est_display = f"{est_seconds}s"
236
+
237
+ progress_msg = (
238
+ f"Round {iteration}/{self._max_rounds} (~{est_display} remaining)"
239
+ )
240
+
241
  # Yield progress update before the agent action
242
  yield AgentEvent(
243
  type="progress",
244
+ message=progress_msg,
245
  iteration=iteration,
246
  )
247
 
tests/unit/orchestrators/test_advanced_orchestrator.py ADDED
@@ -0,0 +1,73 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from unittest.mock import patch
3
+
4
+ import pytest
5
+
6
+ from src.orchestrators.advanced import AdvancedOrchestrator
7
+
8
+
9
+ @pytest.mark.unit
10
+ class TestAdvancedOrchestratorConfig:
11
+ """Tests for configuration options."""
12
+
13
+ def test_default_max_rounds_is_five(self) -> None:
14
+ """Default max_rounds should be 5 for faster demos."""
15
+ with (
16
+ patch.dict(os.environ, {}, clear=True),
17
+ patch("src.orchestrators.advanced.check_magentic_requirements"),
18
+ ):
19
+ # Clear any existing env var
20
+ os.environ.pop("ADVANCED_MAX_ROUNDS", None)
21
+ orch = AdvancedOrchestrator()
22
+ assert orch._max_rounds == 5
23
+
24
+ def test_max_rounds_from_env(self) -> None:
25
+ """max_rounds should be configurable via environment."""
26
+ with (
27
+ patch.dict(os.environ, {"ADVANCED_MAX_ROUNDS": "3"}),
28
+ patch("src.orchestrators.advanced.check_magentic_requirements"),
29
+ ):
30
+ orch = AdvancedOrchestrator()
31
+ assert orch._max_rounds == 3
32
+
33
+ def test_explicit_max_rounds_overrides_env(self) -> None:
34
+ """Explicit parameter should override environment."""
35
+ with (
36
+ patch.dict(os.environ, {"ADVANCED_MAX_ROUNDS": "3"}),
37
+ patch("src.orchestrators.advanced.check_magentic_requirements"),
38
+ ):
39
+ orch = AdvancedOrchestrator(max_rounds=7)
40
+ assert orch._max_rounds == 7
41
+
42
+ def test_timeout_default_is_five_minutes(self) -> None:
43
+ """Default timeout should be 300s (5 min) for faster failure."""
44
+ with patch("src.orchestrators.advanced.check_magentic_requirements"):
45
+ orch = AdvancedOrchestrator()
46
+ assert orch._timeout_seconds == 300.0
47
+
48
+ def test_invalid_env_rounds_falls_back_to_default(self) -> None:
49
+ """Invalid ADVANCED_MAX_ROUNDS should fall back to 5."""
50
+ with (
51
+ patch.dict(os.environ, {"ADVANCED_MAX_ROUNDS": "not_a_number"}),
52
+ patch("src.orchestrators.advanced.check_magentic_requirements"),
53
+ ):
54
+ orch = AdvancedOrchestrator()
55
+ assert orch._max_rounds == 5
56
+
57
+ def test_zero_env_rounds_clamps_to_one(self) -> None:
58
+ """ADVANCED_MAX_ROUNDS=0 should clamp to 1."""
59
+ with (
60
+ patch.dict(os.environ, {"ADVANCED_MAX_ROUNDS": "0"}),
61
+ patch("src.orchestrators.advanced.check_magentic_requirements"),
62
+ ):
63
+ orch = AdvancedOrchestrator()
64
+ assert orch._max_rounds == 1
65
+
66
+ def test_negative_env_rounds_clamps_to_one(self) -> None:
67
+ """Negative ADVANCED_MAX_ROUNDS should clamp to 1."""
68
+ with (
69
+ patch.dict(os.environ, {"ADVANCED_MAX_ROUNDS": "-5"}),
70
+ patch("src.orchestrators.advanced.check_magentic_requirements"),
71
+ ):
72
+ orch = AdvancedOrchestrator()
73
+ assert orch._max_rounds == 1