VibecoderMcSwaggins commited on
Commit
1922dbd
·
1 Parent(s): 1f96735

fix: linting and types for phase 5

Browse files
examples/orchestrator_demo/run_magentic.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Demo: Magentic-One Orchestrator for DeepCritical.
4
+
5
+ This script demonstrates Phase 5 functionality:
6
+ - Multi-Agent Coordination (Searcher + Judge + Manager)
7
+ - Magentic-One Workflow
8
+
9
+ Usage:
10
+ export OPENAI_API_KEY=...
11
+ uv run python examples/orchestrator_demo/run_magentic.py "metformin cancer"
12
+ """
13
+
14
+ import argparse
15
+ import asyncio
16
+ import os
17
+ import sys
18
+
19
+ from src.agent_factory.judges import JudgeHandler
20
+ from src.orchestrator_factory import create_orchestrator
21
+ from src.tools.pubmed import PubMedTool
22
+ from src.tools.search_handler import SearchHandler
23
+ from src.tools.websearch import WebTool
24
+ from src.utils.models import OrchestratorConfig
25
+
26
+
27
+ async def main() -> None:
28
+ """Run the magentic agent demo."""
29
+ parser = argparse.ArgumentParser(description="Run DeepCritical Magentic Agent")
30
+ parser.add_argument("query", help="Research query (e.g., 'metformin cancer')")
31
+ parser.add_argument("--iterations", type=int, default=10, help="Max rounds")
32
+ args = parser.parse_args()
33
+
34
+ # Check for keys
35
+ if not (os.getenv("OPENAI_API_KEY") or os.getenv("ANTHROPIC_API_KEY")):
36
+ print("Error: No API key found. Magentic requires real LLM.")
37
+ sys.exit(1)
38
+
39
+ print(f"\n{'='*60}")
40
+ print("DeepCritical Magentic Agent Demo")
41
+ print(f"Query: {args.query}")
42
+ print("Mode: MAGENTIC (Multi-Agent)")
43
+ print(f"{ '='*60}\n")
44
+
45
+ # 1. Setup Search Tools
46
+ search_handler = SearchHandler(tools=[PubMedTool(), WebTool()], timeout=30.0)
47
+
48
+ # 2. Setup Judge
49
+ judge_handler = JudgeHandler()
50
+
51
+ # 3. Setup Orchestrator via Factory
52
+ config = OrchestratorConfig(max_iterations=args.iterations)
53
+ orchestrator = create_orchestrator(
54
+ search_handler=search_handler,
55
+ judge_handler=judge_handler,
56
+ config=config,
57
+ mode="magentic",
58
+ )
59
+
60
+ if not orchestrator:
61
+ print("Failed to create Magentic orchestrator. Is agent-framework installed?")
62
+ sys.exit(1)
63
+
64
+ # 4. Run Loop
65
+ try:
66
+ async for event in orchestrator.run(args.query):
67
+ # Print event with icon
68
+ # Clean up markdown for CLI
69
+ msg_obj = event.message
70
+ msg_text = ""
71
+ if hasattr(msg_obj, "text"):
72
+ msg_text = msg_obj.text
73
+ else:
74
+ msg_text = str(msg_obj)
75
+
76
+ msg = msg_text.replace("\n", " ").replace("**", "")[:150]
77
+ print(f"[{event.type.upper()}] {msg}...")
78
+
79
+ if event.type == "complete":
80
+ print("\n--- FINAL OUTPUT ---\n")
81
+ print(msg_text)
82
+
83
+ except Exception as e:
84
+ print(f"\n❌ Error: {e}")
85
+ import traceback
86
+
87
+ traceback.print_exc()
88
+
89
+
90
+ if __name__ == "__main__":
91
+ asyncio.run(main())
pyproject.toml CHANGED
@@ -46,6 +46,9 @@ dev = [
46
  "mypy>=1.10",
47
  "pre-commit>=3.7",
48
  ]
 
 
 
49
 
50
  [build-system]
51
  requires = ["hatchling"]
@@ -112,4 +115,7 @@ exclude_lines = [
112
  "pragma: no cover",
113
  "if TYPE_CHECKING:",
114
  "raise NotImplementedError",
115
- ]
 
 
 
 
46
  "mypy>=1.10",
47
  "pre-commit>=3.7",
48
  ]
49
+ magentic = [
50
+ "agent-framework-core",
51
+ ]
52
 
53
  [build-system]
54
  requires = ["hatchling"]
 
115
  "pragma: no cover",
116
  "if TYPE_CHECKING:",
117
  "raise NotImplementedError",
118
+ ]
119
+
120
+ [tool.uv.sources]
121
+ agent-framework-core = { path = "reference_repos/agent-framework/python/packages/core", editable = true }
src/agents/__init__.py ADDED
File without changes
src/agents/judge_agent.py ADDED
@@ -0,0 +1,96 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Judge agent wrapper for Magentic integration."""
2
+
3
+ from collections.abc import AsyncIterable
4
+ from typing import Any
5
+
6
+ from agent_framework import (
7
+ AgentRunResponse,
8
+ AgentRunResponseUpdate,
9
+ AgentThread,
10
+ BaseAgent,
11
+ ChatMessage,
12
+ Role,
13
+ )
14
+
15
+ from src.orchestrator import JudgeHandlerProtocol
16
+ from src.utils.models import Evidence, JudgeAssessment
17
+
18
+
19
+ class JudgeAgent(BaseAgent): # type: ignore[misc]
20
+ """Wraps JudgeHandler as an AgentProtocol for Magentic."""
21
+
22
+ def __init__(
23
+ self,
24
+ judge_handler: JudgeHandlerProtocol,
25
+ evidence_store: dict[str, list[Evidence]],
26
+ ) -> None:
27
+ super().__init__(
28
+ name="JudgeAgent",
29
+ description="Evaluates evidence quality and determines if sufficient for synthesis",
30
+ )
31
+ self._handler = judge_handler
32
+ self._evidence_store = evidence_store # Shared state for evidence
33
+
34
+ async def run(
35
+ self,
36
+ messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
37
+ *,
38
+ thread: AgentThread | None = None,
39
+ **kwargs: Any,
40
+ ) -> AgentRunResponse:
41
+ """Assess evidence quality."""
42
+ # Extract original question from messages
43
+ question = ""
44
+ if isinstance(messages, list):
45
+ for msg in reversed(messages):
46
+ if isinstance(msg, ChatMessage) and msg.role == Role.USER and msg.text:
47
+ question = msg.text
48
+ break
49
+ elif isinstance(msg, str):
50
+ question = msg
51
+ break
52
+ elif isinstance(messages, str):
53
+ question = messages
54
+
55
+ # Get evidence from shared store
56
+ evidence = self._evidence_store.get("current", [])
57
+
58
+ # Assess
59
+ assessment: JudgeAssessment = await self._handler.assess(question, evidence)
60
+
61
+ # Format response
62
+ response_text = f"""## Assessment
63
+
64
+ **Sufficient**: {assessment.sufficient}
65
+ **Confidence**: {assessment.confidence:.0%}
66
+ **Recommendation**: {assessment.recommendation}
67
+
68
+ ### Scores
69
+ - Mechanism: {assessment.details.mechanism_score}/10
70
+ - Clinical: {assessment.details.clinical_evidence_score}/10
71
+
72
+ ### Reasoning
73
+ {assessment.reasoning}
74
+ """
75
+
76
+ if assessment.next_search_queries:
77
+ response_text += "\n### Next Queries\n" + "\n".join(
78
+ f"- {q}" for q in assessment.next_search_queries
79
+ )
80
+
81
+ return AgentRunResponse(
82
+ messages=[ChatMessage(role=Role.ASSISTANT, text=response_text)],
83
+ response_id=f"judge-{assessment.recommendation}",
84
+ additional_properties={"assessment": assessment.model_dump()},
85
+ )
86
+
87
+ async def run_stream(
88
+ self,
89
+ messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
90
+ *,
91
+ thread: AgentThread | None = None,
92
+ **kwargs: Any,
93
+ ) -> AsyncIterable[AgentRunResponseUpdate]:
94
+ """Streaming wrapper for judge."""
95
+ result = await self.run(messages, thread=thread, **kwargs)
96
+ yield AgentRunResponseUpdate(messages=result.messages, response_id=result.response_id)
src/agents/search_agent.py ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from collections.abc import AsyncIterable
2
+ from typing import Any
3
+
4
+ from agent_framework import (
5
+ AgentRunResponse,
6
+ AgentRunResponseUpdate,
7
+ AgentThread,
8
+ BaseAgent,
9
+ ChatMessage,
10
+ Role,
11
+ )
12
+
13
+ from src.orchestrator import SearchHandlerProtocol
14
+ from src.utils.models import Evidence, SearchResult
15
+
16
+
17
+ class SearchAgent(BaseAgent): # type: ignore[misc]
18
+ """Wraps SearchHandler as an AgentProtocol for Magentic."""
19
+
20
+ def __init__(
21
+ self,
22
+ search_handler: SearchHandlerProtocol,
23
+ evidence_store: dict[str, list[Evidence]],
24
+ ) -> None:
25
+ super().__init__(
26
+ name="SearchAgent",
27
+ description="Searches PubMed and web for drug repurposing evidence",
28
+ )
29
+ self._handler = search_handler
30
+ self._evidence_store = evidence_store
31
+
32
+ async def run(
33
+ self,
34
+ messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
35
+ *,
36
+ thread: AgentThread | None = None,
37
+ **kwargs: Any,
38
+ ) -> AgentRunResponse:
39
+ """Execute search based on the last user message."""
40
+ # Extract query from messages
41
+ query = ""
42
+ if isinstance(messages, list):
43
+ for msg in reversed(messages):
44
+ if isinstance(msg, ChatMessage) and msg.role == Role.USER and msg.text:
45
+ query = msg.text
46
+ break
47
+ elif isinstance(msg, str):
48
+ query = msg
49
+ break
50
+ elif isinstance(messages, str):
51
+ query = messages
52
+
53
+ if not query:
54
+ return AgentRunResponse(
55
+ messages=[ChatMessage(role=Role.ASSISTANT, text="No query provided")],
56
+ response_id="search-no-query",
57
+ )
58
+
59
+ # Execute search
60
+ result: SearchResult = await self._handler.execute(query, max_results_per_tool=10)
61
+
62
+ # Update shared evidence store
63
+ # We append new evidence, deduplicating by URL is handled in Orchestrator usually,
64
+ # but here we should probably add to the list.
65
+ # For simplicity in this MVP phase, we just extend the list.
66
+ # Ideally, we should dedupe.
67
+ existing_urls = {e.citation.url for e in self._evidence_store["current"]}
68
+ new_unique = [e for e in result.evidence if e.citation.url not in existing_urls]
69
+ self._evidence_store["current"].extend(new_unique)
70
+
71
+ # Format response
72
+ evidence_text = "\n".join(
73
+ [
74
+ f"- [{e.citation.title}]({e.citation.url}): {e.content[:200]}..."
75
+ for e in result.evidence[:5]
76
+ ]
77
+ )
78
+
79
+ response_text = (
80
+ f"Found {result.total_found} sources ({len(new_unique)} new):\n\n{evidence_text}"
81
+ )
82
+
83
+ return AgentRunResponse(
84
+ messages=[ChatMessage(role=Role.ASSISTANT, text=response_text)],
85
+ response_id=f"search-{result.total_found}",
86
+ additional_properties={"evidence": [e.model_dump() for e in result.evidence]},
87
+ )
88
+
89
+ async def run_stream(
90
+ self,
91
+ messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
92
+ *,
93
+ thread: AgentThread | None = None,
94
+ **kwargs: Any,
95
+ ) -> AsyncIterable[AgentRunResponseUpdate]:
96
+ """Streaming wrapper for search (search itself isn't streaming)."""
97
+ result = await self.run(messages, thread=thread, **kwargs)
98
+ # Yield single update with full result
99
+ yield AgentRunResponseUpdate(messages=result.messages, response_id=result.response_id)
src/app.py CHANGED
@@ -7,19 +7,20 @@ from typing import Any
7
  import gradio as gr
8
 
9
  from src.agent_factory.judges import JudgeHandler, MockJudgeHandler
10
- from src.orchestrator import Orchestrator
11
  from src.tools.pubmed import PubMedTool
12
  from src.tools.search_handler import SearchHandler
13
  from src.tools.websearch import WebTool
14
  from src.utils.models import OrchestratorConfig
15
 
16
 
17
- def create_orchestrator(use_mock: bool = False) -> Orchestrator:
18
  """
19
  Create an orchestrator instance.
20
 
21
  Args:
22
  use_mock: If True, use MockJudgeHandler (no API key needed)
 
23
 
24
  Returns:
25
  Configured Orchestrator instance
@@ -43,16 +44,18 @@ def create_orchestrator(use_mock: bool = False) -> Orchestrator:
43
  else:
44
  judge_handler = JudgeHandler()
45
 
46
- return Orchestrator(
47
  search_handler=search_handler,
48
  judge_handler=judge_handler,
49
  config=config,
 
50
  )
51
 
52
 
53
  async def research_agent(
54
  message: str,
55
  history: list[dict[str, Any]],
 
56
  ) -> AsyncGenerator[str, None]:
57
  """
58
  Gradio chat function that runs the research agent.
@@ -60,6 +63,7 @@ async def research_agent(
60
  Args:
61
  message: User's research question
62
  history: Chat history (Gradio format)
 
63
 
64
  Yields:
65
  Markdown-formatted responses for streaming
@@ -70,7 +74,16 @@ async def research_agent(
70
 
71
  # Create orchestrator (use mock if no API key)
72
  use_mock = not (os.getenv("OPENAI_API_KEY") or os.getenv("ANTHROPIC_API_KEY"))
73
- orchestrator = create_orchestrator(use_mock=use_mock)
 
 
 
 
 
 
 
 
 
74
 
75
  # Run the agent and stream events
76
  response_parts = []
@@ -126,6 +139,14 @@ def create_demo() -> Any:
126
  "What medications show promise for Long COVID treatment?",
127
  "Can statins be repurposed for neurological conditions?",
128
  ],
 
 
 
 
 
 
 
 
129
  )
130
 
131
  gr.Markdown("""
 
7
  import gradio as gr
8
 
9
  from src.agent_factory.judges import JudgeHandler, MockJudgeHandler
10
+ from src.orchestrator_factory import create_orchestrator
11
  from src.tools.pubmed import PubMedTool
12
  from src.tools.search_handler import SearchHandler
13
  from src.tools.websearch import WebTool
14
  from src.utils.models import OrchestratorConfig
15
 
16
 
17
+ def configure_orchestrator(use_mock: bool = False, mode: str = "simple") -> Any:
18
  """
19
  Create an orchestrator instance.
20
 
21
  Args:
22
  use_mock: If True, use MockJudgeHandler (no API key needed)
23
+ mode: Orchestrator mode ("simple" or "magentic")
24
 
25
  Returns:
26
  Configured Orchestrator instance
 
44
  else:
45
  judge_handler = JudgeHandler()
46
 
47
+ return create_orchestrator(
48
  search_handler=search_handler,
49
  judge_handler=judge_handler,
50
  config=config,
51
+ mode=mode, # type: ignore
52
  )
53
 
54
 
55
  async def research_agent(
56
  message: str,
57
  history: list[dict[str, Any]],
58
+ mode: str = "simple",
59
  ) -> AsyncGenerator[str, None]:
60
  """
61
  Gradio chat function that runs the research agent.
 
63
  Args:
64
  message: User's research question
65
  history: Chat history (Gradio format)
66
+ mode: Orchestrator mode ("simple" or "magentic")
67
 
68
  Yields:
69
  Markdown-formatted responses for streaming
 
74
 
75
  # Create orchestrator (use mock if no API key)
76
  use_mock = not (os.getenv("OPENAI_API_KEY") or os.getenv("ANTHROPIC_API_KEY"))
77
+
78
+ # If magentic mode requested but no keys, fallback/warn
79
+ if mode == "magentic" and use_mock:
80
+ yield (
81
+ "⚠️ **Warning**: Magentic mode requires valid API keys. "
82
+ "Falling back to Mock Simple mode."
83
+ )
84
+ mode = "simple"
85
+
86
+ orchestrator = configure_orchestrator(use_mock=use_mock, mode=mode)
87
 
88
  # Run the agent and stream events
89
  response_parts = []
 
139
  "What medications show promise for Long COVID treatment?",
140
  "Can statins be repurposed for neurological conditions?",
141
  ],
142
+ additional_inputs=[
143
+ gr.Radio(
144
+ choices=["simple", "magentic"],
145
+ value="simple",
146
+ label="Orchestrator Mode",
147
+ info="Simple: Linear loop | Magentic: Multi-Agent (Requires OpenAI Key)",
148
+ )
149
+ ],
150
  )
151
 
152
  gr.Markdown("""
src/orchestrator_factory.py ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Factory for creating orchestrators."""
2
+
3
+ from typing import Any, Literal
4
+
5
+ from src.orchestrator import JudgeHandlerProtocol, Orchestrator, SearchHandlerProtocol
6
+ from src.utils.models import OrchestratorConfig
7
+
8
+ # Define protocols again or import if they were in a shared place.
9
+
10
+ # Since they are in src/orchestrator.py, we can import them?
11
+
12
+ # But SearchHandler and JudgeHandler in arguments are concrete classes in the type hint,
13
+
14
+ # which satisfy the protocol.
15
+
16
+
17
+ def create_orchestrator(
18
+ search_handler: SearchHandlerProtocol,
19
+ judge_handler: JudgeHandlerProtocol,
20
+ config: OrchestratorConfig | None = None,
21
+ mode: Literal["simple", "magentic"] = "simple",
22
+ ) -> Any:
23
+ """
24
+ Create an orchestrator instance.
25
+
26
+ Args:
27
+ search_handler: The search handler
28
+ judge_handler: The judge handler
29
+ config: Optional configuration
30
+ mode: "simple" for Phase 4 loop, "magentic" for Phase 5 multi-agent
31
+
32
+ Returns:
33
+ Orchestrator instance (same interface regardless of mode)
34
+ """
35
+ if mode == "magentic":
36
+ try:
37
+ from src.orchestrator_magentic import MagenticOrchestrator
38
+
39
+ return MagenticOrchestrator(
40
+ search_handler=search_handler,
41
+ judge_handler=judge_handler,
42
+ max_rounds=config.max_iterations if config else 10,
43
+ )
44
+ except ImportError:
45
+ # Fallback to simple if agent-framework not installed
46
+ pass
47
+
48
+ return Orchestrator(
49
+ search_handler=search_handler,
50
+ judge_handler=judge_handler,
51
+ config=config,
52
+ )
src/orchestrator_magentic.py ADDED
@@ -0,0 +1,185 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Magentic-based orchestrator for DeepCritical."""
2
+
3
+ from collections.abc import AsyncGenerator
4
+
5
+ import structlog
6
+ from agent_framework import (
7
+ MagenticAgentDeltaEvent,
8
+ MagenticAgentMessageEvent,
9
+ MagenticBuilder,
10
+ MagenticFinalResultEvent,
11
+ MagenticOrchestratorMessageEvent,
12
+ WorkflowOutputEvent,
13
+ )
14
+ from agent_framework.openai import OpenAIChatClient
15
+
16
+ from src.agents.judge_agent import JudgeAgent
17
+ from src.agents.search_agent import SearchAgent
18
+ from src.orchestrator import JudgeHandlerProtocol, SearchHandlerProtocol
19
+ from src.utils.config import settings
20
+ from src.utils.models import AgentEvent, Evidence
21
+
22
+ logger = structlog.get_logger()
23
+
24
+
25
+ class MagenticOrchestrator:
26
+ """
27
+ Magentic-based orchestrator - same API as Orchestrator.
28
+
29
+ Uses Microsoft Agent Framework's MagenticBuilder for multi-agent coordination.
30
+ """
31
+
32
+ def __init__(
33
+ self,
34
+ search_handler: SearchHandlerProtocol,
35
+ judge_handler: JudgeHandlerProtocol,
36
+ max_rounds: int = 10,
37
+ ) -> None:
38
+ self._search_handler = search_handler
39
+ self._judge_handler = judge_handler
40
+ self._max_rounds = max_rounds
41
+ self._evidence_store: dict[str, list[Evidence]] = {"current": []}
42
+
43
+ async def run(self, query: str) -> AsyncGenerator[AgentEvent, None]: # noqa: PLR0912
44
+ """
45
+ Run the Magentic workflow - same API as simple Orchestrator.
46
+
47
+ Yields AgentEvent objects for real-time UI updates.
48
+ """
49
+ logger.info("Starting Magentic orchestrator", query=query)
50
+
51
+ yield AgentEvent(
52
+ type="started",
53
+ message=f"Starting research (Magentic mode): {query}",
54
+ iteration=0,
55
+ )
56
+
57
+ # Create agent wrappers
58
+ search_agent = SearchAgent(self._search_handler, self._evidence_store)
59
+ judge_agent = JudgeAgent(self._judge_handler, self._evidence_store)
60
+
61
+ # Build Magentic workflow
62
+ # Note: MagenticBuilder.participants takes named arguments for agent instances
63
+ workflow = (
64
+ MagenticBuilder()
65
+ .participants(
66
+ searcher=search_agent,
67
+ judge=judge_agent,
68
+ )
69
+ .with_standard_manager(
70
+ chat_client=OpenAIChatClient(
71
+ model_id=settings.openai_model, api_key=settings.openai_api_key
72
+ ),
73
+ max_round_count=self._max_rounds,
74
+ max_stall_count=3,
75
+ max_reset_count=2,
76
+ )
77
+ .build()
78
+ )
79
+
80
+ # Task instruction for the manager
81
+ task = f"""Research drug repurposing opportunities for: {query}
82
+
83
+ Instructions:
84
+ 1. Use SearcherAgent to find evidence. SEND ONLY A SIMPLE KEYWORD QUERY (e.g. "metformin aging")
85
+ as the instruction. Complex queries fail.
86
+ 2. Use JudgeAgent to evaluate if evidence is sufficient.
87
+ 3. If JudgeAgent says "continue", search with refined queries.
88
+ 4. If JudgeAgent says "synthesize", provide final synthesis
89
+ 5. Stop when synthesis is ready or max rounds reached
90
+
91
+ Focus on finding:
92
+ - Mechanism of action evidence
93
+ - Clinical/preclinical studies
94
+ - Specific drug candidates
95
+ """
96
+
97
+ iteration = 0
98
+ try:
99
+ # workflow.run_stream returns an async generator of workflow events
100
+ # We use 'await' in the for loop for async generator
101
+ async for event in workflow.run_stream(task):
102
+ if isinstance(event, MagenticOrchestratorMessageEvent):
103
+ # Manager events (planning, instruction, ledger)
104
+ # The 'message' attribute might be None if it's just a state change,
105
+ # check message presence
106
+ message_text = (
107
+ event.message.text
108
+ if event.message and hasattr(event.message, "text")
109
+ else ""
110
+ )
111
+ # kind might be 'plan', 'instruction', etc.
112
+ kind = getattr(event, "kind", "manager")
113
+
114
+ if message_text:
115
+ yield AgentEvent(
116
+ type="judging",
117
+ message=f"Manager ({kind}): {message_text[:100]}...",
118
+ iteration=iteration,
119
+ )
120
+
121
+ elif isinstance(event, MagenticAgentMessageEvent):
122
+ # Complete agent response
123
+ iteration += 1
124
+ agent_name = event.agent_id or "unknown"
125
+ msg_text = (
126
+ event.message.text
127
+ if event.message and hasattr(event.message, "text")
128
+ else ""
129
+ )
130
+
131
+ if "search" in agent_name.lower():
132
+ # Check if we found evidence (based on SearchAgent logic)
133
+ yield AgentEvent(
134
+ type="search_complete",
135
+ message=f"Search agent: {msg_text[:100]}...",
136
+ iteration=iteration,
137
+ )
138
+ elif "judge" in agent_name.lower():
139
+ yield AgentEvent(
140
+ type="judge_complete",
141
+ message=f"Judge agent: {msg_text[:100]}...",
142
+ iteration=iteration,
143
+ )
144
+
145
+ elif isinstance(event, MagenticFinalResultEvent):
146
+ # Final workflow result
147
+ final_text = (
148
+ event.message.text
149
+ if event.message and hasattr(event.message, "text")
150
+ else "No result"
151
+ )
152
+ yield AgentEvent(
153
+ type="complete",
154
+ message=final_text,
155
+ data={"iterations": iteration},
156
+ iteration=iteration,
157
+ )
158
+
159
+ elif isinstance(event, MagenticAgentDeltaEvent):
160
+ # Streaming token chunks from agents (optional "typing" effect)
161
+ # Only emit if we have actual text content
162
+ if event.text:
163
+ yield AgentEvent(
164
+ type="streaming",
165
+ message=event.text,
166
+ data={"agent_id": event.agent_id},
167
+ iteration=iteration,
168
+ )
169
+
170
+ elif isinstance(event, WorkflowOutputEvent):
171
+ # Alternative final output event
172
+ if event.data:
173
+ yield AgentEvent(
174
+ type="complete",
175
+ message=str(event.data),
176
+ iteration=iteration,
177
+ )
178
+
179
+ except Exception as e:
180
+ logger.error("Magentic workflow failed", error=str(e))
181
+ yield AgentEvent(
182
+ type="error",
183
+ message=f"Workflow error: {e!s}",
184
+ iteration=iteration,
185
+ )
src/utils/models.py CHANGED
@@ -106,6 +106,7 @@ class AgentEvent(BaseModel):
106
  "synthesizing",
107
  "complete",
108
  "error",
 
109
  ]
110
  message: str
111
  data: Any = None
 
106
  "synthesizing",
107
  "complete",
108
  "error",
109
+ "streaming",
110
  ]
111
  message: str
112
  data: Any = None
uv.lock CHANGED
@@ -19,6 +19,54 @@ wheels = [
19
  { url = "https://files.pythonhosted.org/packages/8f/78/eb55fabaab41abc53f52c0918a9a8c0f747807e5306273f51120fd695957/ag_ui_protocol-0.1.10-py3-none-any.whl", hash = "sha256:c81e6981f30aabdf97a7ee312bfd4df0cd38e718d9fc10019c7d438128b93ab5", size = 7889 },
20
  ]
21
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  [[package]]
23
  name = "aiofiles"
24
  version = "24.1.0"
@@ -165,6 +213,35 @@ wheels = [
165
  { url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608 },
166
  ]
167
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
  [[package]]
169
  name = "backports-tarfile"
170
  version = "1.2.0"
@@ -684,9 +761,13 @@ dev = [
684
  { name = "respx" },
685
  { name = "ruff" },
686
  ]
 
 
 
687
 
688
  [package.metadata]
689
  requires-dist = [
 
690
  { name = "anthropic", specifier = ">=0.18.0" },
691
  { name = "beautifulsoup4", specifier = ">=4.12" },
692
  { name = "duckduckgo-search", specifier = ">=6.0" },
@@ -710,7 +791,7 @@ requires-dist = [
710
  { name = "tenacity", specifier = ">=8.2" },
711
  { name = "xmltodict", specifier = ">=0.13" },
712
  ]
713
- provides-extras = ["dev"]
714
 
715
  [[package]]
716
  name = "diskcache"
@@ -1083,6 +1164,57 @@ wheels = [
1083
  { url = "https://files.pythonhosted.org/packages/27/79/28f295d5064750674014f50e3c2daf8dc233964c904d357ac2bd0e33fc31/groq-0.36.0-py3-none-any.whl", hash = "sha256:ac7eeae31a5c2e76d30ea678f0b1a9168ff906c4440f5ec3a42ac74d5b4fdb3c", size = 137279 },
1084
  ]
1085
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1086
  [[package]]
1087
  name = "h11"
1088
  version = "0.16.0"
@@ -1683,6 +1815,11 @@ wheels = [
1683
  { url = "https://files.pythonhosted.org/packages/a9/bb/711099f9c6bb52770f56e56401cdfb10da5b67029f701e0df29362df4c8e/mcp-1.22.0-py3-none-any.whl", hash = "sha256:bed758e24df1ed6846989c909ba4e3df339a27b4f30f1b8b627862a4bade4e98", size = 175489 },
1684
  ]
1685
 
 
 
 
 
 
1686
  [[package]]
1687
  name = "mdurl"
1688
  version = "0.1.2"
@@ -1719,6 +1856,32 @@ wheels = [
1719
  { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667 },
1720
  ]
1721
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1722
  [[package]]
1723
  name = "mypy"
1724
  version = "1.18.2"
@@ -1924,6 +2087,24 @@ wheels = [
1924
  { url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359 },
1925
  ]
1926
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1927
  [[package]]
1928
  name = "opentelemetry-exporter-otlp-proto-http"
1929
  version = "1.38.0"
@@ -2012,6 +2193,15 @@ wheels = [
2012
  { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954 },
2013
  ]
2014
 
 
 
 
 
 
 
 
 
 
2015
  [[package]]
2016
  name = "opentelemetry-util-http"
2017
  version = "0.59b0"
 
19
  { url = "https://files.pythonhosted.org/packages/8f/78/eb55fabaab41abc53f52c0918a9a8c0f747807e5306273f51120fd695957/ag_ui_protocol-0.1.10-py3-none-any.whl", hash = "sha256:c81e6981f30aabdf97a7ee312bfd4df0cd38e718d9fc10019c7d438128b93ab5", size = 7889 },
20
  ]
21
 
22
+ [[package]]
23
+ name = "agent-framework-core"
24
+ version = "1.0.0b251120"
25
+ source = { editable = "reference_repos/agent-framework/python/packages/core" }
26
+ dependencies = [
27
+ { name = "azure-identity" },
28
+ { name = "mcp", extra = ["ws"] },
29
+ { name = "openai" },
30
+ { name = "opentelemetry-api" },
31
+ { name = "opentelemetry-exporter-otlp-proto-grpc" },
32
+ { name = "opentelemetry-sdk" },
33
+ { name = "opentelemetry-semantic-conventions-ai" },
34
+ { name = "packaging" },
35
+ { name = "pydantic" },
36
+ { name = "pydantic-settings" },
37
+ { name = "typing-extensions" },
38
+ ]
39
+
40
+ [package.metadata]
41
+ requires-dist = [
42
+ { name = "agent-framework-a2a", marker = "extra == 'all'", editable = "reference_repos/agent-framework/python/packages/a2a" },
43
+ { name = "agent-framework-ag-ui", marker = "extra == 'all'", editable = "reference_repos/agent-framework/python/packages/ag-ui" },
44
+ { name = "agent-framework-anthropic", marker = "extra == 'all'", editable = "reference_repos/agent-framework/python/packages/anthropic" },
45
+ { name = "agent-framework-azure-ai", marker = "extra == 'all'", editable = "reference_repos/agent-framework/python/packages/azure-ai" },
46
+ { name = "agent-framework-azure-ai-search", marker = "extra == 'all'", editable = "reference_repos/agent-framework/python/packages/azure-ai-search" },
47
+ { name = "agent-framework-azurefunctions", marker = "extra == 'all'", editable = "reference_repos/agent-framework/python/packages/azurefunctions" },
48
+ { name = "agent-framework-chatkit", marker = "extra == 'all'", editable = "reference_repos/agent-framework/python/packages/chatkit" },
49
+ { name = "agent-framework-copilotstudio", marker = "extra == 'all'", editable = "reference_repos/agent-framework/python/packages/copilotstudio" },
50
+ { name = "agent-framework-declarative", marker = "extra == 'all'", editable = "reference_repos/agent-framework/python/packages/declarative" },
51
+ { name = "agent-framework-devui", marker = "extra == 'all'", editable = "reference_repos/agent-framework/python/packages/devui" },
52
+ { name = "agent-framework-lab", marker = "extra == 'all'", editable = "reference_repos/agent-framework/python/packages/lab" },
53
+ { name = "agent-framework-mem0", marker = "extra == 'all'", editable = "reference_repos/agent-framework/python/packages/mem0" },
54
+ { name = "agent-framework-purview", marker = "extra == 'all'", editable = "reference_repos/agent-framework/python/packages/purview" },
55
+ { name = "agent-framework-redis", marker = "extra == 'all'", editable = "reference_repos/agent-framework/python/packages/redis" },
56
+ { name = "azure-identity", specifier = ">=1,<2" },
57
+ { name = "mcp", extras = ["ws"], specifier = ">=1.13" },
58
+ { name = "openai", specifier = ">=1.99.0" },
59
+ { name = "opentelemetry-api", specifier = ">=1.24" },
60
+ { name = "opentelemetry-exporter-otlp-proto-grpc", specifier = ">=1.36.0" },
61
+ { name = "opentelemetry-sdk", specifier = ">=1.24" },
62
+ { name = "opentelemetry-semantic-conventions-ai", specifier = ">=0.4.13" },
63
+ { name = "packaging", specifier = ">=24.1" },
64
+ { name = "pydantic", specifier = ">=2,<3" },
65
+ { name = "pydantic-settings", specifier = ">=2,<3" },
66
+ { name = "typing-extensions" },
67
+ ]
68
+ provides-extras = ["all"]
69
+
70
  [[package]]
71
  name = "aiofiles"
72
  version = "24.1.0"
 
213
  { url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608 },
214
  ]
215
 
216
+ [[package]]
217
+ name = "azure-core"
218
+ version = "1.36.0"
219
+ source = { registry = "https://pypi.org/simple" }
220
+ dependencies = [
221
+ { name = "requests" },
222
+ { name = "typing-extensions" },
223
+ ]
224
+ sdist = { url = "https://files.pythonhosted.org/packages/0a/c4/d4ff3bc3ddf155156460bff340bbe9533f99fac54ddea165f35a8619f162/azure_core-1.36.0.tar.gz", hash = "sha256:22e5605e6d0bf1d229726af56d9e92bc37b6e726b141a18be0b4d424131741b7", size = 351139 }
225
+ wheels = [
226
+ { url = "https://files.pythonhosted.org/packages/b1/3c/b90d5afc2e47c4a45f4bba00f9c3193b0417fad5ad3bb07869f9d12832aa/azure_core-1.36.0-py3-none-any.whl", hash = "sha256:fee9923a3a753e94a259563429f3644aaf05c486d45b1215d098115102d91d3b", size = 213302 },
227
+ ]
228
+
229
+ [[package]]
230
+ name = "azure-identity"
231
+ version = "1.25.1"
232
+ source = { registry = "https://pypi.org/simple" }
233
+ dependencies = [
234
+ { name = "azure-core" },
235
+ { name = "cryptography" },
236
+ { name = "msal" },
237
+ { name = "msal-extensions" },
238
+ { name = "typing-extensions" },
239
+ ]
240
+ sdist = { url = "https://files.pythonhosted.org/packages/06/8d/1a6c41c28a37eab26dc85ab6c86992c700cd3f4a597d9ed174b0e9c69489/azure_identity-1.25.1.tar.gz", hash = "sha256:87ca8328883de6036443e1c37b40e8dc8fb74898240f61071e09d2e369361456", size = 279826 }
241
+ wheels = [
242
+ { url = "https://files.pythonhosted.org/packages/83/7b/5652771e24fff12da9dde4c20ecf4682e606b104f26419d139758cc935a6/azure_identity-1.25.1-py3-none-any.whl", hash = "sha256:e9edd720af03dff020223cd269fa3a61e8f345ea75443858273bcb44844ab651", size = 191317 },
243
+ ]
244
+
245
  [[package]]
246
  name = "backports-tarfile"
247
  version = "1.2.0"
 
761
  { name = "respx" },
762
  { name = "ruff" },
763
  ]
764
+ magentic = [
765
+ { name = "agent-framework-core" },
766
+ ]
767
 
768
  [package.metadata]
769
  requires-dist = [
770
+ { name = "agent-framework-core", marker = "extra == 'magentic'", editable = "reference_repos/agent-framework/python/packages/core" },
771
  { name = "anthropic", specifier = ">=0.18.0" },
772
  { name = "beautifulsoup4", specifier = ">=4.12" },
773
  { name = "duckduckgo-search", specifier = ">=6.0" },
 
791
  { name = "tenacity", specifier = ">=8.2" },
792
  { name = "xmltodict", specifier = ">=0.13" },
793
  ]
794
+ provides-extras = ["dev", "magentic"]
795
 
796
  [[package]]
797
  name = "diskcache"
 
1164
  { url = "https://files.pythonhosted.org/packages/27/79/28f295d5064750674014f50e3c2daf8dc233964c904d357ac2bd0e33fc31/groq-0.36.0-py3-none-any.whl", hash = "sha256:ac7eeae31a5c2e76d30ea678f0b1a9168ff906c4440f5ec3a42ac74d5b4fdb3c", size = 137279 },
1165
  ]
1166
 
1167
+ [[package]]
1168
+ name = "grpcio"
1169
+ version = "1.76.0"
1170
+ source = { registry = "https://pypi.org/simple" }
1171
+ dependencies = [
1172
+ { name = "typing-extensions" },
1173
+ ]
1174
+ sdist = { url = "https://files.pythonhosted.org/packages/b6/e0/318c1ce3ae5a17894d5791e87aea147587c9e702f24122cc7a5c8bbaeeb1/grpcio-1.76.0.tar.gz", hash = "sha256:7be78388d6da1a25c0d5ec506523db58b18be22d9c37d8d3a32c08be4987bd73", size = 12785182 }
1175
+ wheels = [
1176
+ { url = "https://files.pythonhosted.org/packages/a0/00/8163a1beeb6971f66b4bbe6ac9457b97948beba8dd2fc8e1281dce7f79ec/grpcio-1.76.0-cp311-cp311-linux_armv7l.whl", hash = "sha256:2e1743fbd7f5fa713a1b0a8ac8ebabf0ec980b5d8809ec358d488e273b9cf02a", size = 5843567 },
1177
+ { url = "https://files.pythonhosted.org/packages/10/c1/934202f5cf335e6d852530ce14ddb0fef21be612ba9ecbbcbd4d748ca32d/grpcio-1.76.0-cp311-cp311-macosx_11_0_universal2.whl", hash = "sha256:a8c2cf1209497cf659a667d7dea88985e834c24b7c3b605e6254cbb5076d985c", size = 11848017 },
1178
+ { url = "https://files.pythonhosted.org/packages/11/0b/8dec16b1863d74af6eb3543928600ec2195af49ca58b16334972f6775663/grpcio-1.76.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:08caea849a9d3c71a542827d6df9d5a69067b0a1efbea8a855633ff5d9571465", size = 6412027 },
1179
+ { url = "https://files.pythonhosted.org/packages/d7/64/7b9e6e7ab910bea9d46f2c090380bab274a0b91fb0a2fe9b0cd399fffa12/grpcio-1.76.0-cp311-cp311-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:f0e34c2079d47ae9f6188211db9e777c619a21d4faba6977774e8fa43b085e48", size = 7075913 },
1180
+ { url = "https://files.pythonhosted.org/packages/68/86/093c46e9546073cefa789bd76d44c5cb2abc824ca62af0c18be590ff13ba/grpcio-1.76.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8843114c0cfce61b40ad48df65abcfc00d4dba82eae8718fab5352390848c5da", size = 6615417 },
1181
+ { url = "https://files.pythonhosted.org/packages/f7/b6/5709a3a68500a9c03da6fb71740dcdd5ef245e39266461a03f31a57036d8/grpcio-1.76.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8eddfb4d203a237da6f3cc8a540dad0517d274b5a1e9e636fd8d2c79b5c1d397", size = 7199683 },
1182
+ { url = "https://files.pythonhosted.org/packages/91/d3/4b1f2bf16ed52ce0b508161df3a2d186e4935379a159a834cb4a7d687429/grpcio-1.76.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:32483fe2aab2c3794101c2a159070584e5db11d0aa091b2c0ea9c4fc43d0d749", size = 8163109 },
1183
+ { url = "https://files.pythonhosted.org/packages/5c/61/d9043f95f5f4cf085ac5dd6137b469d41befb04bd80280952ffa2a4c3f12/grpcio-1.76.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:dcfe41187da8992c5f40aa8c5ec086fa3672834d2be57a32384c08d5a05b4c00", size = 7626676 },
1184
+ { url = "https://files.pythonhosted.org/packages/36/95/fd9a5152ca02d8881e4dd419cdd790e11805979f499a2e5b96488b85cf27/grpcio-1.76.0-cp311-cp311-win32.whl", hash = "sha256:2107b0c024d1b35f4083f11245c0e23846ae64d02f40b2b226684840260ed054", size = 3997688 },
1185
+ { url = "https://files.pythonhosted.org/packages/60/9c/5c359c8d4c9176cfa3c61ecd4efe5affe1f38d9bae81e81ac7186b4c9cc8/grpcio-1.76.0-cp311-cp311-win_amd64.whl", hash = "sha256:522175aba7af9113c48ec10cc471b9b9bd4f6ceb36aeb4544a8e2c80ed9d252d", size = 4709315 },
1186
+ { url = "https://files.pythonhosted.org/packages/bf/05/8e29121994b8d959ffa0afd28996d452f291b48cfc0875619de0bde2c50c/grpcio-1.76.0-cp312-cp312-linux_armv7l.whl", hash = "sha256:81fd9652b37b36f16138611c7e884eb82e0cec137c40d3ef7c3f9b3ed00f6ed8", size = 5799718 },
1187
+ { url = "https://files.pythonhosted.org/packages/d9/75/11d0e66b3cdf998c996489581bdad8900db79ebd83513e45c19548f1cba4/grpcio-1.76.0-cp312-cp312-macosx_11_0_universal2.whl", hash = "sha256:04bbe1bfe3a68bbfd4e52402ab7d4eb59d72d02647ae2042204326cf4bbad280", size = 11825627 },
1188
+ { url = "https://files.pythonhosted.org/packages/28/50/2f0aa0498bc188048f5d9504dcc5c2c24f2eb1a9337cd0fa09a61a2e75f0/grpcio-1.76.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d388087771c837cdb6515539f43b9d4bf0b0f23593a24054ac16f7a960be16f4", size = 6359167 },
1189
+ { url = "https://files.pythonhosted.org/packages/66/e5/bbf0bb97d29ede1d59d6588af40018cfc345b17ce979b7b45424628dc8bb/grpcio-1.76.0-cp312-cp312-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:9f8f757bebaaea112c00dba718fc0d3260052ce714e25804a03f93f5d1c6cc11", size = 7044267 },
1190
+ { url = "https://files.pythonhosted.org/packages/f5/86/f6ec2164f743d9609691115ae8ece098c76b894ebe4f7c94a655c6b03e98/grpcio-1.76.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:980a846182ce88c4f2f7e2c22c56aefd515daeb36149d1c897f83cf57999e0b6", size = 6573963 },
1191
+ { url = "https://files.pythonhosted.org/packages/60/bc/8d9d0d8505feccfdf38a766d262c71e73639c165b311c9457208b56d92ae/grpcio-1.76.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f92f88e6c033db65a5ae3d97905c8fea9c725b63e28d5a75cb73b49bda5024d8", size = 7164484 },
1192
+ { url = "https://files.pythonhosted.org/packages/67/e6/5d6c2fc10b95edf6df9b8f19cf10a34263b7fd48493936fffd5085521292/grpcio-1.76.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:4baf3cbe2f0be3289eb68ac8ae771156971848bb8aaff60bad42005539431980", size = 8127777 },
1193
+ { url = "https://files.pythonhosted.org/packages/3f/c8/dce8ff21c86abe025efe304d9e31fdb0deaaa3b502b6a78141080f206da0/grpcio-1.76.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:615ba64c208aaceb5ec83bfdce7728b80bfeb8be97562944836a7a0a9647d882", size = 7594014 },
1194
+ { url = "https://files.pythonhosted.org/packages/e0/42/ad28191ebf983a5d0ecef90bab66baa5a6b18f2bfdef9d0a63b1973d9f75/grpcio-1.76.0-cp312-cp312-win32.whl", hash = "sha256:45d59a649a82df5718fd9527ce775fd66d1af35e6d31abdcdc906a49c6822958", size = 3984750 },
1195
+ { url = "https://files.pythonhosted.org/packages/9e/00/7bd478cbb851c04a48baccaa49b75abaa8e4122f7d86da797500cccdd771/grpcio-1.76.0-cp312-cp312-win_amd64.whl", hash = "sha256:c088e7a90b6017307f423efbb9d1ba97a22aa2170876223f9709e9d1de0b5347", size = 4704003 },
1196
+ { url = "https://files.pythonhosted.org/packages/fc/ed/71467ab770effc9e8cef5f2e7388beb2be26ed642d567697bb103a790c72/grpcio-1.76.0-cp313-cp313-linux_armv7l.whl", hash = "sha256:26ef06c73eb53267c2b319f43e6634c7556ea37672029241a056629af27c10e2", size = 5807716 },
1197
+ { url = "https://files.pythonhosted.org/packages/2c/85/c6ed56f9817fab03fa8a111ca91469941fb514e3e3ce6d793cb8f1e1347b/grpcio-1.76.0-cp313-cp313-macosx_11_0_universal2.whl", hash = "sha256:45e0111e73f43f735d70786557dc38141185072d7ff8dc1829d6a77ac1471468", size = 11821522 },
1198
+ { url = "https://files.pythonhosted.org/packages/ac/31/2b8a235ab40c39cbc141ef647f8a6eb7b0028f023015a4842933bc0d6831/grpcio-1.76.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:83d57312a58dcfe2a3a0f9d1389b299438909a02db60e2f2ea2ae2d8034909d3", size = 6362558 },
1199
+ { url = "https://files.pythonhosted.org/packages/bd/64/9784eab483358e08847498ee56faf8ff6ea8e0a4592568d9f68edc97e9e9/grpcio-1.76.0-cp313-cp313-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:3e2a27c89eb9ac3d81ec8835e12414d73536c6e620355d65102503064a4ed6eb", size = 7049990 },
1200
+ { url = "https://files.pythonhosted.org/packages/2b/94/8c12319a6369434e7a184b987e8e9f3b49a114c489b8315f029e24de4837/grpcio-1.76.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:61f69297cba3950a524f61c7c8ee12e55c486cb5f7db47ff9dcee33da6f0d3ae", size = 6575387 },
1201
+ { url = "https://files.pythonhosted.org/packages/15/0f/f12c32b03f731f4a6242f771f63039df182c8b8e2cf8075b245b409259d4/grpcio-1.76.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:6a15c17af8839b6801d554263c546c69c4d7718ad4321e3166175b37eaacca77", size = 7166668 },
1202
+ { url = "https://files.pythonhosted.org/packages/ff/2d/3ec9ce0c2b1d92dd59d1c3264aaec9f0f7c817d6e8ac683b97198a36ed5a/grpcio-1.76.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:25a18e9810fbc7e7f03ec2516addc116a957f8cbb8cbc95ccc80faa072743d03", size = 8124928 },
1203
+ { url = "https://files.pythonhosted.org/packages/1a/74/fd3317be5672f4856bcdd1a9e7b5e17554692d3db9a3b273879dc02d657d/grpcio-1.76.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:931091142fd8cc14edccc0845a79248bc155425eee9a98b2db2ea4f00a235a42", size = 7589983 },
1204
+ { url = "https://files.pythonhosted.org/packages/45/bb/ca038cf420f405971f19821c8c15bcbc875505f6ffadafe9ffd77871dc4c/grpcio-1.76.0-cp313-cp313-win32.whl", hash = "sha256:5e8571632780e08526f118f74170ad8d50fb0a48c23a746bef2a6ebade3abd6f", size = 3984727 },
1205
+ { url = "https://files.pythonhosted.org/packages/41/80/84087dc56437ced7cdd4b13d7875e7439a52a261e3ab4e06488ba6173b0a/grpcio-1.76.0-cp313-cp313-win_amd64.whl", hash = "sha256:f9f7bd5faab55f47231ad8dba7787866b69f5e93bc306e3915606779bbfb4ba8", size = 4702799 },
1206
+ { url = "https://files.pythonhosted.org/packages/b4/46/39adac80de49d678e6e073b70204091e76631e03e94928b9ea4ecf0f6e0e/grpcio-1.76.0-cp314-cp314-linux_armv7l.whl", hash = "sha256:ff8a59ea85a1f2191a0ffcc61298c571bc566332f82e5f5be1b83c9d8e668a62", size = 5808417 },
1207
+ { url = "https://files.pythonhosted.org/packages/9c/f5/a4531f7fb8b4e2a60b94e39d5d924469b7a6988176b3422487be61fe2998/grpcio-1.76.0-cp314-cp314-macosx_11_0_universal2.whl", hash = "sha256:06c3d6b076e7b593905d04fdba6a0525711b3466f43b3400266f04ff735de0cd", size = 11828219 },
1208
+ { url = "https://files.pythonhosted.org/packages/4b/1c/de55d868ed7a8bd6acc6b1d6ddc4aa36d07a9f31d33c912c804adb1b971b/grpcio-1.76.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fd5ef5932f6475c436c4a55e4336ebbe47bd3272be04964a03d316bbf4afbcbc", size = 6367826 },
1209
+ { url = "https://files.pythonhosted.org/packages/59/64/99e44c02b5adb0ad13ab3adc89cb33cb54bfa90c74770f2607eea629b86f/grpcio-1.76.0-cp314-cp314-manylinux2014_i686.manylinux_2_17_i686.whl", hash = "sha256:b331680e46239e090f5b3cead313cc772f6caa7d0fc8de349337563125361a4a", size = 7049550 },
1210
+ { url = "https://files.pythonhosted.org/packages/43/28/40a5be3f9a86949b83e7d6a2ad6011d993cbe9b6bd27bea881f61c7788b6/grpcio-1.76.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2229ae655ec4e8999599469559e97630185fdd53ae1e8997d147b7c9b2b72cba", size = 6575564 },
1211
+ { url = "https://files.pythonhosted.org/packages/4b/a9/1be18e6055b64467440208a8559afac243c66a8b904213af6f392dc2212f/grpcio-1.76.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:490fa6d203992c47c7b9e4a9d39003a0c2bcc1c9aa3c058730884bbbb0ee9f09", size = 7176236 },
1212
+ { url = "https://files.pythonhosted.org/packages/0f/55/dba05d3fcc151ce6e81327541d2cc8394f442f6b350fead67401661bf041/grpcio-1.76.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:479496325ce554792dba6548fae3df31a72cef7bad71ca2e12b0e58f9b336bfc", size = 8125795 },
1213
+ { url = "https://files.pythonhosted.org/packages/4a/45/122df922d05655f63930cf42c9e3f72ba20aadb26c100ee105cad4ce4257/grpcio-1.76.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:1c9b93f79f48b03ada57ea24725d83a30284a012ec27eab2cf7e50a550cbbbcc", size = 7592214 },
1214
+ { url = "https://files.pythonhosted.org/packages/4a/6e/0b899b7f6b66e5af39e377055fb4a6675c9ee28431df5708139df2e93233/grpcio-1.76.0-cp314-cp314-win32.whl", hash = "sha256:747fa73efa9b8b1488a95d0ba1039c8e2dca0f741612d80415b1e1c560febf4e", size = 4062961 },
1215
+ { url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462 },
1216
+ ]
1217
+
1218
  [[package]]
1219
  name = "h11"
1220
  version = "0.16.0"
 
1815
  { url = "https://files.pythonhosted.org/packages/a9/bb/711099f9c6bb52770f56e56401cdfb10da5b67029f701e0df29362df4c8e/mcp-1.22.0-py3-none-any.whl", hash = "sha256:bed758e24df1ed6846989c909ba4e3df339a27b4f30f1b8b627862a4bade4e98", size = 175489 },
1816
  ]
1817
 
1818
+ [package.optional-dependencies]
1819
+ ws = [
1820
+ { name = "websockets" },
1821
+ ]
1822
+
1823
  [[package]]
1824
  name = "mdurl"
1825
  version = "0.1.2"
 
1856
  { url = "https://files.pythonhosted.org/packages/a4/8e/469e5a4a2f5855992e425f3cb33804cc07bf18d48f2db061aec61ce50270/more_itertools-10.8.0-py3-none-any.whl", hash = "sha256:52d4362373dcf7c52546bc4af9a86ee7c4579df9a8dc268be0a2f949d376cc9b", size = 69667 },
1857
  ]
1858
 
1859
+ [[package]]
1860
+ name = "msal"
1861
+ version = "1.34.0"
1862
+ source = { registry = "https://pypi.org/simple" }
1863
+ dependencies = [
1864
+ { name = "cryptography" },
1865
+ { name = "pyjwt", extra = ["crypto"] },
1866
+ { name = "requests" },
1867
+ ]
1868
+ sdist = { url = "https://files.pythonhosted.org/packages/cf/0e/c857c46d653e104019a84f22d4494f2119b4fe9f896c92b4b864b3b045cc/msal-1.34.0.tar.gz", hash = "sha256:76ba83b716ea5a6d75b0279c0ac353a0e05b820ca1f6682c0eb7f45190c43c2f", size = 153961 }
1869
+ wheels = [
1870
+ { url = "https://files.pythonhosted.org/packages/c2/dc/18d48843499e278538890dc709e9ee3dea8375f8be8e82682851df1b48b5/msal-1.34.0-py3-none-any.whl", hash = "sha256:f669b1644e4950115da7a176441b0e13ec2975c29528d8b9e81316023676d6e1", size = 116987 },
1871
+ ]
1872
+
1873
+ [[package]]
1874
+ name = "msal-extensions"
1875
+ version = "1.3.1"
1876
+ source = { registry = "https://pypi.org/simple" }
1877
+ dependencies = [
1878
+ { name = "msal" },
1879
+ ]
1880
+ sdist = { url = "https://files.pythonhosted.org/packages/01/99/5d239b6156eddf761a636bded1118414d161bd6b7b37a9335549ed159396/msal_extensions-1.3.1.tar.gz", hash = "sha256:c5b0fd10f65ef62b5f1d62f4251d51cbcaf003fcedae8c91b040a488614be1a4", size = 23315 }
1881
+ wheels = [
1882
+ { url = "https://files.pythonhosted.org/packages/5e/75/bd9b7bb966668920f06b200e84454c8f3566b102183bc55c5473d96cb2b9/msal_extensions-1.3.1-py3-none-any.whl", hash = "sha256:96d3de4d034504e969ac5e85bae8106c8373b5c6568e4c8fa7af2eca9dbe6bca", size = 20583 },
1883
+ ]
1884
+
1885
  [[package]]
1886
  name = "mypy"
1887
  version = "1.18.2"
 
2087
  { url = "https://files.pythonhosted.org/packages/a7/9e/55a41c9601191e8cd8eb626b54ee6827b9c9d4a46d736f32abc80d8039fc/opentelemetry_exporter_otlp_proto_common-1.38.0-py3-none-any.whl", hash = "sha256:03cb76ab213300fe4f4c62b7d8f17d97fcfd21b89f0b5ce38ea156327ddda74a", size = 18359 },
2088
  ]
2089
 
2090
+ [[package]]
2091
+ name = "opentelemetry-exporter-otlp-proto-grpc"
2092
+ version = "1.38.0"
2093
+ source = { registry = "https://pypi.org/simple" }
2094
+ dependencies = [
2095
+ { name = "googleapis-common-protos" },
2096
+ { name = "grpcio" },
2097
+ { name = "opentelemetry-api" },
2098
+ { name = "opentelemetry-exporter-otlp-proto-common" },
2099
+ { name = "opentelemetry-proto" },
2100
+ { name = "opentelemetry-sdk" },
2101
+ { name = "typing-extensions" },
2102
+ ]
2103
+ sdist = { url = "https://files.pythonhosted.org/packages/a2/c0/43222f5b97dc10812bc4f0abc5dc7cd0a2525a91b5151d26c9e2e958f52e/opentelemetry_exporter_otlp_proto_grpc-1.38.0.tar.gz", hash = "sha256:2473935e9eac71f401de6101d37d6f3f0f1831db92b953c7dcc912536158ebd6", size = 24676 }
2104
+ wheels = [
2105
+ { url = "https://files.pythonhosted.org/packages/28/f0/bd831afbdba74ca2ce3982142a2fad707f8c487e8a3b6fef01f1d5945d1b/opentelemetry_exporter_otlp_proto_grpc-1.38.0-py3-none-any.whl", hash = "sha256:7c49fd9b4bd0dbe9ba13d91f764c2d20b0025649a6e4ac35792fb8d84d764bc7", size = 19695 },
2106
+ ]
2107
+
2108
  [[package]]
2109
  name = "opentelemetry-exporter-otlp-proto-http"
2110
  version = "1.38.0"
 
2193
  { url = "https://files.pythonhosted.org/packages/24/7d/c88d7b15ba8fe5c6b8f93be50fc11795e9fc05386c44afaf6b76fe191f9b/opentelemetry_semantic_conventions-0.59b0-py3-none-any.whl", hash = "sha256:35d3b8833ef97d614136e253c1da9342b4c3c083bbaf29ce31d572a1c3825eed", size = 207954 },
2194
  ]
2195
 
2196
+ [[package]]
2197
+ name = "opentelemetry-semantic-conventions-ai"
2198
+ version = "0.4.13"
2199
+ source = { registry = "https://pypi.org/simple" }
2200
+ sdist = { url = "https://files.pythonhosted.org/packages/ba/e6/40b59eda51ac47009fb47afcdf37c6938594a0bd7f3b9fadcbc6058248e3/opentelemetry_semantic_conventions_ai-0.4.13.tar.gz", hash = "sha256:94efa9fb4ffac18c45f54a3a338ffeb7eedb7e1bb4d147786e77202e159f0036", size = 5368 }
2201
+ wheels = [
2202
+ { url = "https://files.pythonhosted.org/packages/35/b5/cf25da2218910f0d6cdf7f876a06bed118c4969eacaf60a887cbaef44f44/opentelemetry_semantic_conventions_ai-0.4.13-py3-none-any.whl", hash = "sha256:883a30a6bb5deaec0d646912b5f9f6dcbb9f6f72557b73d0f2560bf25d13e2d5", size = 6080 },
2203
+ ]
2204
+
2205
  [[package]]
2206
  name = "opentelemetry-util-http"
2207
  version = "0.59b0"