refactor(cleanup): Remove Anthropic + Modal partial wiring (P3 Tech Debt) (#130)
Browse files## P3 Tech Debt Cleanup Complete
### Anthropic Removal
- Removed partial wiring that never worked end-to-end
- Cleaned config, factory, app, agent_factory
### Modal Removal
- Deleted code_execution, statistical_analyzer, analysis agents
- Net reduction: ~1400 lines of dead code
### CodeRabbit Fixes
- Removed duplicate chromadb/sentence-transformers from extras
- Kept them in core deps (needed for evidence deduplication)
- Pinned requires-python to >=3.11,<4.0
- Removed Modal deps from requirements.txt
✅ 302 tests pass
- AGENTS.md +2 -7
- CLAUDE.md +2 -7
- GEMINI.md +2 -8
- docs/future-roadmap/P3_MODAL_INTEGRATION_REMOVAL.md +1 -1
- docs/future-roadmap/P3_REMOVE_ANTHROPIC_PARTIAL_WIRING.md +1 -1
- examples/modal_demo/run_analysis.py +0 -65
- examples/modal_demo/test_code_execution.py +0 -169
- examples/modal_demo/verify_sandbox.py +0 -101
- pyproject.toml +5 -10
- requirements.txt +7 -7
- src/agent_factory/judges.py +0 -13
- src/agents/analysis_agent.py +0 -145
- src/agents/code_executor_agent.py +0 -66
- src/app.py +1 -1
- src/mcp_tools.py +0 -69
- src/services/llamaindex_rag.py +2 -8
- src/services/statistical_analyzer.py +0 -259
- src/tools/code_execution.py +0 -260
- src/utils/config.py +1 -8
- src/utils/exceptions.py +0 -6
- src/utils/llm_factory.py +0 -64
- src/utils/service_loader.py +2 -33
- tests/integration/test_modal.py +0 -67
- tests/unit/agent_factory/test_get_model_auto_detect.py +1 -16
- tests/unit/agent_factory/test_judges_factory.py +1 -15
- tests/unit/orchestrators/test_advanced_p2_dead_zones.py +0 -1
- tests/unit/services/test_statistical_analyzer.py +0 -104
- tests/unit/test_app_smoke.py +0 -2
- tests/unit/utils/test_service_loader.py +116 -51
- uv.lock +11 -159
AGENTS.md
CHANGED
|
@@ -60,13 +60,11 @@ Research Report with Citations
|
|
| 60 |
- `src/tools/pubmed.py` - PubMed E-utilities search
|
| 61 |
- `src/tools/clinicaltrials.py` - ClinicalTrials.gov API
|
| 62 |
- `src/tools/europepmc.py` - Europe PMC search
|
| 63 |
-
- `src/tools/code_execution.py` - Modal sandbox execution
|
| 64 |
- `src/tools/search_handler.py` - Scatter-gather orchestration
|
| 65 |
- `src/services/embeddings.py` - Local embeddings (sentence-transformers, in-memory)
|
| 66 |
- `src/services/llamaindex_rag.py` - Premium embeddings (OpenAI, persistent ChromaDB)
|
| 67 |
- `src/services/embedding_protocol.py` - Protocol interface for embedding services
|
| 68 |
- `src/services/research_memory.py` - Shared memory layer for research state
|
| 69 |
-
- `src/services/statistical_analyzer.py` - Statistical analysis via Modal
|
| 70 |
- `src/utils/service_loader.py` - Tiered service selection (free vs premium)
|
| 71 |
- `src/agent_factory/judges.py` - LLM-based evidence assessment
|
| 72 |
- `src/agents/` - Magentic multi-agent mode (SearchAgent, JudgeAgent, etc.)
|
|
@@ -82,10 +80,9 @@ Research Report with Citations
|
|
| 82 |
|
| 83 |
Settings via pydantic-settings from `.env`:
|
| 84 |
|
| 85 |
-
- `LLM_PROVIDER`: "openai" or "
|
| 86 |
-
- `OPENAI_API_KEY
|
| 87 |
- `NCBI_API_KEY`: Optional, for higher PubMed rate limits
|
| 88 |
-
- `MODAL_TOKEN_ID` / `MODAL_TOKEN_SECRET`: For Modal sandbox (optional)
|
| 89 |
- `MAX_ITERATIONS`: 1-50, default 10
|
| 90 |
- `LOG_LEVEL`: DEBUG, INFO, WARNING, ERROR
|
| 91 |
|
|
@@ -107,8 +104,6 @@ Default models in `src/utils/config.py`:
|
|
| 107 |
- **OpenAI:** `gpt-5` - Flagship model
|
| 108 |
- **HuggingFace (Free Tier):** `Qwen/Qwen2.5-7B-Instruct` - See critical note below
|
| 109 |
|
| 110 |
-
**NOTE:** Anthropic is NOT supported (no embeddings API). See `P3_REMOVE_ANTHROPIC_PARTIAL_WIRING.md`.
|
| 111 |
-
|
| 112 |
---
|
| 113 |
|
| 114 |
## ⚠️ OpenAI API Keys
|
|
|
|
| 60 |
- `src/tools/pubmed.py` - PubMed E-utilities search
|
| 61 |
- `src/tools/clinicaltrials.py` - ClinicalTrials.gov API
|
| 62 |
- `src/tools/europepmc.py` - Europe PMC search
|
|
|
|
| 63 |
- `src/tools/search_handler.py` - Scatter-gather orchestration
|
| 64 |
- `src/services/embeddings.py` - Local embeddings (sentence-transformers, in-memory)
|
| 65 |
- `src/services/llamaindex_rag.py` - Premium embeddings (OpenAI, persistent ChromaDB)
|
| 66 |
- `src/services/embedding_protocol.py` - Protocol interface for embedding services
|
| 67 |
- `src/services/research_memory.py` - Shared memory layer for research state
|
|
|
|
| 68 |
- `src/utils/service_loader.py` - Tiered service selection (free vs premium)
|
| 69 |
- `src/agent_factory/judges.py` - LLM-based evidence assessment
|
| 70 |
- `src/agents/` - Magentic multi-agent mode (SearchAgent, JudgeAgent, etc.)
|
|
|
|
| 80 |
|
| 81 |
Settings via pydantic-settings from `.env`:
|
| 82 |
|
| 83 |
+
- `LLM_PROVIDER`: "openai" or "huggingface"
|
| 84 |
+
- `OPENAI_API_KEY`: LLM keys
|
| 85 |
- `NCBI_API_KEY`: Optional, for higher PubMed rate limits
|
|
|
|
| 86 |
- `MAX_ITERATIONS`: 1-50, default 10
|
| 87 |
- `LOG_LEVEL`: DEBUG, INFO, WARNING, ERROR
|
| 88 |
|
|
|
|
| 104 |
- **OpenAI:** `gpt-5` - Flagship model
|
| 105 |
- **HuggingFace (Free Tier):** `Qwen/Qwen2.5-7B-Instruct` - See critical note below
|
| 106 |
|
|
|
|
|
|
|
| 107 |
---
|
| 108 |
|
| 109 |
## ⚠️ OpenAI API Keys
|
CLAUDE.md
CHANGED
|
@@ -60,13 +60,11 @@ Research Report with Citations
|
|
| 60 |
- `src/tools/pubmed.py` - PubMed E-utilities search
|
| 61 |
- `src/tools/clinicaltrials.py` - ClinicalTrials.gov API
|
| 62 |
- `src/tools/europepmc.py` - Europe PMC search
|
| 63 |
-
- `src/tools/code_execution.py` - Modal sandbox execution
|
| 64 |
- `src/tools/search_handler.py` - Scatter-gather orchestration
|
| 65 |
- `src/services/embeddings.py` - Local embeddings (sentence-transformers, in-memory)
|
| 66 |
- `src/services/llamaindex_rag.py` - Premium embeddings (OpenAI, persistent ChromaDB)
|
| 67 |
- `src/services/embedding_protocol.py` - Protocol interface for embedding services
|
| 68 |
- `src/services/research_memory.py` - Shared memory layer for research state
|
| 69 |
-
- `src/services/statistical_analyzer.py` - Statistical analysis via Modal
|
| 70 |
- `src/utils/service_loader.py` - Tiered service selection (free vs premium)
|
| 71 |
- `src/agent_factory/judges.py` - LLM-based evidence assessment
|
| 72 |
- `src/agents/` - Magentic multi-agent mode (SearchAgent, JudgeAgent, etc.)
|
|
@@ -82,10 +80,9 @@ Research Report with Citations
|
|
| 82 |
|
| 83 |
Settings via pydantic-settings from `.env`:
|
| 84 |
|
| 85 |
-
- `LLM_PROVIDER`: "openai" or "
|
| 86 |
-
- `OPENAI_API_KEY
|
| 87 |
- `NCBI_API_KEY`: Optional, for higher PubMed rate limits
|
| 88 |
-
- `MODAL_TOKEN_ID` / `MODAL_TOKEN_SECRET`: For Modal sandbox (optional)
|
| 89 |
- `MAX_ITERATIONS`: 1-50, default 10
|
| 90 |
- `LOG_LEVEL`: DEBUG, INFO, WARNING, ERROR
|
| 91 |
|
|
@@ -114,8 +111,6 @@ Default models in `src/utils/config.py`:
|
|
| 114 |
- **OpenAI:** `gpt-5` - Flagship model
|
| 115 |
- **HuggingFace (Free Tier):** `Qwen/Qwen2.5-7B-Instruct` - See critical note below
|
| 116 |
|
| 117 |
-
**NOTE:** Anthropic is NOT supported (no embeddings API). See `P3_REMOVE_ANTHROPIC_PARTIAL_WIRING.md`.
|
| 118 |
-
|
| 119 |
---
|
| 120 |
|
| 121 |
## ⚠️ OpenAI API Keys
|
|
|
|
| 60 |
- `src/tools/pubmed.py` - PubMed E-utilities search
|
| 61 |
- `src/tools/clinicaltrials.py` - ClinicalTrials.gov API
|
| 62 |
- `src/tools/europepmc.py` - Europe PMC search
|
|
|
|
| 63 |
- `src/tools/search_handler.py` - Scatter-gather orchestration
|
| 64 |
- `src/services/embeddings.py` - Local embeddings (sentence-transformers, in-memory)
|
| 65 |
- `src/services/llamaindex_rag.py` - Premium embeddings (OpenAI, persistent ChromaDB)
|
| 66 |
- `src/services/embedding_protocol.py` - Protocol interface for embedding services
|
| 67 |
- `src/services/research_memory.py` - Shared memory layer for research state
|
|
|
|
| 68 |
- `src/utils/service_loader.py` - Tiered service selection (free vs premium)
|
| 69 |
- `src/agent_factory/judges.py` - LLM-based evidence assessment
|
| 70 |
- `src/agents/` - Magentic multi-agent mode (SearchAgent, JudgeAgent, etc.)
|
|
|
|
| 80 |
|
| 81 |
Settings via pydantic-settings from `.env`:
|
| 82 |
|
| 83 |
+
- `LLM_PROVIDER`: "openai" or "huggingface"
|
| 84 |
+
- `OPENAI_API_KEY`: LLM keys
|
| 85 |
- `NCBI_API_KEY`: Optional, for higher PubMed rate limits
|
|
|
|
| 86 |
- `MAX_ITERATIONS`: 1-50, default 10
|
| 87 |
- `LOG_LEVEL`: DEBUG, INFO, WARNING, ERROR
|
| 88 |
|
|
|
|
| 111 |
- **OpenAI:** `gpt-5` - Flagship model
|
| 112 |
- **HuggingFace (Free Tier):** `Qwen/Qwen2.5-7B-Instruct` - See critical note below
|
| 113 |
|
|
|
|
|
|
|
| 114 |
---
|
| 115 |
|
| 116 |
## ⚠️ OpenAI API Keys
|
GEMINI.md
CHANGED
|
@@ -16,7 +16,6 @@ The project follows a **Vertical Slice Architecture** (Search -> Judge -> Orches
|
|
| 16 |
- **Package Manager:** `uv` (Rust-based, extremely fast)
|
| 17 |
- **Frameworks:** `pydantic`, `pydantic-ai`, `httpx`, `gradio[mcp]`
|
| 18 |
- **Vector DB:** `chromadb` with `sentence-transformers` for semantic search
|
| 19 |
-
- **Code Execution:** `modal` for secure sandboxed Python execution
|
| 20 |
- **Testing:** `pytest`, `pytest-asyncio`, `respx` (for mocking)
|
| 21 |
- **Quality:** `ruff` (linting/formatting), `mypy` (strict type checking), `pre-commit`
|
| 22 |
|
|
@@ -60,13 +59,11 @@ The project follows a **Vertical Slice Architecture** (Search -> Judge -> Orches
|
|
| 60 |
- `src/tools/pubmed.py` - PubMed E-utilities search
|
| 61 |
- `src/tools/clinicaltrials.py` - ClinicalTrials.gov API
|
| 62 |
- `src/tools/europepmc.py` - Europe PMC search
|
| 63 |
-
- `src/tools/code_execution.py` - Modal sandbox execution
|
| 64 |
- `src/tools/search_handler.py` - Scatter-gather orchestration
|
| 65 |
- `src/services/embeddings.py` - Local embeddings (sentence-transformers, in-memory)
|
| 66 |
- `src/services/llamaindex_rag.py` - Premium embeddings (OpenAI, persistent ChromaDB)
|
| 67 |
- `src/services/embedding_protocol.py` - Protocol interface for embedding services
|
| 68 |
- `src/services/research_memory.py` - Shared memory layer for research state
|
| 69 |
-
- `src/services/statistical_analyzer.py` - Statistical analysis via Modal
|
| 70 |
- `src/utils/service_loader.py` - Tiered service selection (free vs premium)
|
| 71 |
- `src/mcp_tools.py` - MCP tool wrappers
|
| 72 |
- `src/app.py` - Gradio UI (HuggingFace Spaces) with MCP server
|
|
@@ -75,10 +72,9 @@ The project follows a **Vertical Slice Architecture** (Search -> Judge -> Orches
|
|
| 75 |
|
| 76 |
Settings via pydantic-settings from `.env`:
|
| 77 |
|
| 78 |
-
- `LLM_PROVIDER`: "openai" or "
|
| 79 |
-
- `OPENAI_API_KEY
|
| 80 |
- `NCBI_API_KEY`: Optional, for higher PubMed rate limits
|
| 81 |
-
- `MODAL_TOKEN_ID` / `MODAL_TOKEN_SECRET`: For Modal sandbox (optional)
|
| 82 |
- `MAX_ITERATIONS`: 1-50, default 10
|
| 83 |
- `LOG_LEVEL`: DEBUG, INFO, WARNING, ERROR
|
| 84 |
|
|
@@ -89,8 +85,6 @@ Default models in `src/utils/config.py`:
|
|
| 89 |
- **OpenAI:** `gpt-5` - Flagship model
|
| 90 |
- **HuggingFace (Free Tier):** `Qwen/Qwen2.5-7B-Instruct` - See critical note below
|
| 91 |
|
| 92 |
-
**NOTE:** Anthropic is NOT supported (no embeddings API). See `P3_REMOVE_ANTHROPIC_PARTIAL_WIRING.md`.
|
| 93 |
-
|
| 94 |
---
|
| 95 |
|
| 96 |
## ⚠️ OpenAI API Keys
|
|
|
|
| 16 |
- **Package Manager:** `uv` (Rust-based, extremely fast)
|
| 17 |
- **Frameworks:** `pydantic`, `pydantic-ai`, `httpx`, `gradio[mcp]`
|
| 18 |
- **Vector DB:** `chromadb` with `sentence-transformers` for semantic search
|
|
|
|
| 19 |
- **Testing:** `pytest`, `pytest-asyncio`, `respx` (for mocking)
|
| 20 |
- **Quality:** `ruff` (linting/formatting), `mypy` (strict type checking), `pre-commit`
|
| 21 |
|
|
|
|
| 59 |
- `src/tools/pubmed.py` - PubMed E-utilities search
|
| 60 |
- `src/tools/clinicaltrials.py` - ClinicalTrials.gov API
|
| 61 |
- `src/tools/europepmc.py` - Europe PMC search
|
|
|
|
| 62 |
- `src/tools/search_handler.py` - Scatter-gather orchestration
|
| 63 |
- `src/services/embeddings.py` - Local embeddings (sentence-transformers, in-memory)
|
| 64 |
- `src/services/llamaindex_rag.py` - Premium embeddings (OpenAI, persistent ChromaDB)
|
| 65 |
- `src/services/embedding_protocol.py` - Protocol interface for embedding services
|
| 66 |
- `src/services/research_memory.py` - Shared memory layer for research state
|
|
|
|
| 67 |
- `src/utils/service_loader.py` - Tiered service selection (free vs premium)
|
| 68 |
- `src/mcp_tools.py` - MCP tool wrappers
|
| 69 |
- `src/app.py` - Gradio UI (HuggingFace Spaces) with MCP server
|
|
|
|
| 72 |
|
| 73 |
Settings via pydantic-settings from `.env`:
|
| 74 |
|
| 75 |
+
- `LLM_PROVIDER`: "openai" or "huggingface"
|
| 76 |
+
- `OPENAI_API_KEY`: LLM keys
|
| 77 |
- `NCBI_API_KEY`: Optional, for higher PubMed rate limits
|
|
|
|
| 78 |
- `MAX_ITERATIONS`: 1-50, default 10
|
| 79 |
- `LOG_LEVEL`: DEBUG, INFO, WARNING, ERROR
|
| 80 |
|
|
|
|
| 85 |
- **OpenAI:** `gpt-5` - Flagship model
|
| 86 |
- **HuggingFace (Free Tier):** `Qwen/Qwen2.5-7B-Instruct` - See critical note below
|
| 87 |
|
|
|
|
|
|
|
| 88 |
---
|
| 89 |
|
| 90 |
## ⚠️ OpenAI API Keys
|
docs/future-roadmap/P3_MODAL_INTEGRATION_REMOVAL.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
# P3 Tech Debt: Modal Integration Removal
|
| 2 |
|
| 3 |
**Date**: 2025-12-04
|
| 4 |
-
**Status**:
|
| 5 |
**Severity**: P3 (Tech Debt - Not blocking functionality)
|
| 6 |
**Component**: Multiple files
|
| 7 |
|
|
|
|
| 1 |
# P3 Tech Debt: Modal Integration Removal
|
| 2 |
|
| 3 |
**Date**: 2025-12-04
|
| 4 |
+
**Status**: DONE
|
| 5 |
**Severity**: P3 (Tech Debt - Not blocking functionality)
|
| 6 |
**Component**: Multiple files
|
| 7 |
|
docs/future-roadmap/P3_REMOVE_ANTHROPIC_PARTIAL_WIRING.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
| 1 |
# P3 Tech Debt: Remove Anthropic Partial Wiring
|
| 2 |
|
| 3 |
**Date**: 2025-12-03
|
| 4 |
-
**Status**:
|
| 5 |
**Severity**: P3 (Tech Debt / Simplification)
|
| 6 |
**Component**: Architecture / Provider Integration
|
| 7 |
|
|
|
|
| 1 |
# P3 Tech Debt: Remove Anthropic Partial Wiring
|
| 2 |
|
| 3 |
**Date**: 2025-12-03
|
| 4 |
+
**Status**: DONE
|
| 5 |
**Severity**: P3 (Tech Debt / Simplification)
|
| 6 |
**Component**: Architecture / Provider Integration
|
| 7 |
|
examples/modal_demo/run_analysis.py
DELETED
|
@@ -1,65 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Demo: Modal-powered statistical analysis.
|
| 3 |
-
|
| 4 |
-
This script uses StatisticalAnalyzer directly (NO agent_framework dependency).
|
| 5 |
-
|
| 6 |
-
# Usage:
|
| 7 |
-
# source .env
|
| 8 |
-
# uv run python examples/modal_demo/run_analysis.py "testosterone libido"
|
| 9 |
-
"""
|
| 10 |
-
|
| 11 |
-
import argparse
|
| 12 |
-
import asyncio
|
| 13 |
-
import os
|
| 14 |
-
import sys
|
| 15 |
-
|
| 16 |
-
from src.services.statistical_analyzer import get_statistical_analyzer
|
| 17 |
-
from src.tools.pubmed import PubMedTool
|
| 18 |
-
from src.utils.config import settings
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
async def main() -> None:
|
| 22 |
-
"""Run the Modal analysis demo."""
|
| 23 |
-
parser = argparse.ArgumentParser(description="Modal Analysis Demo")
|
| 24 |
-
parser.add_argument("query", help="Research query")
|
| 25 |
-
args = parser.parse_args()
|
| 26 |
-
|
| 27 |
-
if not settings.modal_available:
|
| 28 |
-
print("Error: Modal credentials not configured.")
|
| 29 |
-
sys.exit(1)
|
| 30 |
-
|
| 31 |
-
if not (os.getenv("OPENAI_API_KEY") or os.getenv("ANTHROPIC_API_KEY")):
|
| 32 |
-
print("Error: No LLM API key found.")
|
| 33 |
-
sys.exit(1)
|
| 34 |
-
|
| 35 |
-
print(f"\n{'=' * 60}")
|
| 36 |
-
print("DeepBoner Modal Analysis Demo")
|
| 37 |
-
print(f"Query: {args.query}")
|
| 38 |
-
print(f"{'=' * 60}\n")
|
| 39 |
-
|
| 40 |
-
# Step 1: Gather Evidence
|
| 41 |
-
print("Step 1: Gathering evidence from PubMed...")
|
| 42 |
-
pubmed = PubMedTool()
|
| 43 |
-
evidence = await pubmed.search(args.query, max_results=5)
|
| 44 |
-
print(f" Found {len(evidence)} papers\n")
|
| 45 |
-
|
| 46 |
-
# Step 2: Run Modal Analysis
|
| 47 |
-
print("Step 2: Running statistical analysis in Modal sandbox...")
|
| 48 |
-
analyzer = get_statistical_analyzer()
|
| 49 |
-
result = await analyzer.analyze(query=args.query, evidence=evidence)
|
| 50 |
-
|
| 51 |
-
# Step 3: Display Results
|
| 52 |
-
print("\n" + "=" * 60)
|
| 53 |
-
print("ANALYSIS RESULTS")
|
| 54 |
-
print("=" * 60)
|
| 55 |
-
print(f"\nVerdict: {result.verdict}")
|
| 56 |
-
print(f"Confidence: {result.confidence:.0%}")
|
| 57 |
-
print("\nKey Findings:")
|
| 58 |
-
for finding in result.key_findings:
|
| 59 |
-
print(f" - {finding}")
|
| 60 |
-
|
| 61 |
-
print("\n[Demo Complete - Code executed in Modal, not locally]")
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
if __name__ == "__main__":
|
| 65 |
-
asyncio.run(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/modal_demo/test_code_execution.py
DELETED
|
@@ -1,169 +0,0 @@
|
|
| 1 |
-
"""Demo script to test Modal code execution integration.
|
| 2 |
-
|
| 3 |
-
Run with: uv run python examples/modal_demo/test_code_execution.py
|
| 4 |
-
"""
|
| 5 |
-
|
| 6 |
-
import sys
|
| 7 |
-
from pathlib import Path
|
| 8 |
-
|
| 9 |
-
# Add src to path
|
| 10 |
-
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
|
| 11 |
-
|
| 12 |
-
from src.tools.code_execution import CodeExecutionError, get_code_executor
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
def test_basic_execution():
|
| 16 |
-
"""Test basic code execution."""
|
| 17 |
-
print("\n=== Test 1: Basic Execution ===")
|
| 18 |
-
executor = get_code_executor()
|
| 19 |
-
|
| 20 |
-
code = """
|
| 21 |
-
print("Hello from Modal sandbox!")
|
| 22 |
-
result = 2 + 2
|
| 23 |
-
print(f"2 + 2 = {result}")
|
| 24 |
-
"""
|
| 25 |
-
|
| 26 |
-
result = executor.execute(code)
|
| 27 |
-
print(f"Success: {result['success']}")
|
| 28 |
-
print(f"Stdout:\n{result['stdout']}")
|
| 29 |
-
if result["stderr"]:
|
| 30 |
-
print(f"Stderr:\n{result['stderr']}")
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def test_scientific_computing():
|
| 34 |
-
"""Test scientific computing libraries."""
|
| 35 |
-
print("\n=== Test 2: Scientific Computing ===")
|
| 36 |
-
executor = get_code_executor()
|
| 37 |
-
|
| 38 |
-
code = """
|
| 39 |
-
import pandas as pd
|
| 40 |
-
import numpy as np
|
| 41 |
-
|
| 42 |
-
# Create sample data
|
| 43 |
-
data = {
|
| 44 |
-
'drug': ['DrugA', 'DrugB', 'DrugC'],
|
| 45 |
-
'efficacy': [0.75, 0.82, 0.68],
|
| 46 |
-
'sample_size': [100, 150, 120]
|
| 47 |
-
}
|
| 48 |
-
|
| 49 |
-
df = pd.DataFrame(data)
|
| 50 |
-
|
| 51 |
-
# Calculate weighted average
|
| 52 |
-
weighted_avg = np.average(df['efficacy'], weights=df['sample_size'])
|
| 53 |
-
|
| 54 |
-
print(f"Drugs tested: {len(df)}")
|
| 55 |
-
print(f"Weighted average efficacy: {weighted_avg:.3f}")
|
| 56 |
-
print("\\nDataFrame:")
|
| 57 |
-
print(df.to_string())
|
| 58 |
-
"""
|
| 59 |
-
|
| 60 |
-
result = executor.execute(code)
|
| 61 |
-
print(f"Success: {result['success']}")
|
| 62 |
-
print(f"Output:\n{result['stdout']}")
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
def test_statistical_analysis():
|
| 66 |
-
"""Test statistical analysis."""
|
| 67 |
-
print("\n=== Test 3: Statistical Analysis ===")
|
| 68 |
-
executor = get_code_executor()
|
| 69 |
-
|
| 70 |
-
code = """
|
| 71 |
-
import numpy as np
|
| 72 |
-
from scipy import stats
|
| 73 |
-
|
| 74 |
-
# Simulate two treatment groups
|
| 75 |
-
np.random.seed(42)
|
| 76 |
-
control_group = np.random.normal(100, 15, 50)
|
| 77 |
-
treatment_group = np.random.normal(110, 15, 50)
|
| 78 |
-
|
| 79 |
-
# Perform t-test
|
| 80 |
-
t_stat, p_value = stats.ttest_ind(treatment_group, control_group)
|
| 81 |
-
|
| 82 |
-
print(f"Control mean: {np.mean(control_group):.2f}")
|
| 83 |
-
print(f"Treatment mean: {np.mean(treatment_group):.2f}")
|
| 84 |
-
print(f"T-statistic: {t_stat:.3f}")
|
| 85 |
-
print(f"P-value: {p_value:.4f}")
|
| 86 |
-
|
| 87 |
-
if p_value < 0.05:
|
| 88 |
-
print("Result: Statistically significant difference")
|
| 89 |
-
else:
|
| 90 |
-
print("Result: No significant difference")
|
| 91 |
-
"""
|
| 92 |
-
|
| 93 |
-
result = executor.execute(code)
|
| 94 |
-
print(f"Success: {result['success']}")
|
| 95 |
-
print(f"Output:\n{result['stdout']}")
|
| 96 |
-
|
| 97 |
-
|
| 98 |
-
def test_with_return_value():
|
| 99 |
-
"""Test execute_with_return method."""
|
| 100 |
-
print("\n=== Test 4: Return Value ===")
|
| 101 |
-
executor = get_code_executor()
|
| 102 |
-
|
| 103 |
-
code = """
|
| 104 |
-
import numpy as np
|
| 105 |
-
|
| 106 |
-
# Calculate something
|
| 107 |
-
data = np.array([1, 2, 3, 4, 5])
|
| 108 |
-
result = {
|
| 109 |
-
'mean': float(np.mean(data)),
|
| 110 |
-
'std': float(np.std(data)),
|
| 111 |
-
'sum': int(np.sum(data))
|
| 112 |
-
}
|
| 113 |
-
"""
|
| 114 |
-
|
| 115 |
-
try:
|
| 116 |
-
result = executor.execute_with_return(code)
|
| 117 |
-
print(f"Returned result: {result}")
|
| 118 |
-
print(f"Mean: {result['mean']}")
|
| 119 |
-
print(f"Std: {result['std']}")
|
| 120 |
-
print(f"Sum: {result['sum']}")
|
| 121 |
-
except CodeExecutionError as e:
|
| 122 |
-
print(f"Error: {e}")
|
| 123 |
-
|
| 124 |
-
|
| 125 |
-
def test_error_handling():
|
| 126 |
-
"""Test error handling."""
|
| 127 |
-
print("\n=== Test 5: Error Handling ===")
|
| 128 |
-
executor = get_code_executor()
|
| 129 |
-
|
| 130 |
-
code = """
|
| 131 |
-
# This will fail
|
| 132 |
-
x = 1 / 0
|
| 133 |
-
"""
|
| 134 |
-
|
| 135 |
-
result = executor.execute(code)
|
| 136 |
-
print(f"Success: {result['success']}")
|
| 137 |
-
print(f"Error: {result['error']}")
|
| 138 |
-
|
| 139 |
-
|
| 140 |
-
def main():
|
| 141 |
-
"""Run all tests."""
|
| 142 |
-
print("=" * 60)
|
| 143 |
-
print("Modal Code Execution Demo")
|
| 144 |
-
print("=" * 60)
|
| 145 |
-
|
| 146 |
-
tests = [
|
| 147 |
-
test_basic_execution,
|
| 148 |
-
test_scientific_computing,
|
| 149 |
-
test_statistical_analysis,
|
| 150 |
-
test_with_return_value,
|
| 151 |
-
test_error_handling,
|
| 152 |
-
]
|
| 153 |
-
|
| 154 |
-
for test in tests:
|
| 155 |
-
try:
|
| 156 |
-
test()
|
| 157 |
-
except Exception as e:
|
| 158 |
-
print(f"\n❌ Test failed: {e}")
|
| 159 |
-
import traceback
|
| 160 |
-
|
| 161 |
-
traceback.print_exc()
|
| 162 |
-
|
| 163 |
-
print("\n" + "=" * 60)
|
| 164 |
-
print("Demo completed!")
|
| 165 |
-
print("=" * 60)
|
| 166 |
-
|
| 167 |
-
|
| 168 |
-
if __name__ == "__main__":
|
| 169 |
-
main()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
examples/modal_demo/verify_sandbox.py
DELETED
|
@@ -1,101 +0,0 @@
|
|
| 1 |
-
#!/usr/bin/env python3
|
| 2 |
-
"""Verify that Modal sandbox is properly isolated.
|
| 3 |
-
|
| 4 |
-
This script proves to judges that code runs in Modal, not locally.
|
| 5 |
-
NO agent_framework dependency - uses only src.tools.code_execution.
|
| 6 |
-
|
| 7 |
-
Usage:
|
| 8 |
-
uv run python examples/modal_demo/verify_sandbox.py
|
| 9 |
-
"""
|
| 10 |
-
|
| 11 |
-
import asyncio
|
| 12 |
-
from functools import partial
|
| 13 |
-
|
| 14 |
-
from src.tools.code_execution import CodeExecutionError, get_code_executor
|
| 15 |
-
from src.utils.config import settings
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
def print_result(result: dict) -> None:
|
| 19 |
-
"""Print execution result, surfacing errors when they occur."""
|
| 20 |
-
if result.get("success"):
|
| 21 |
-
print(f" {result['stdout'].strip()}\n")
|
| 22 |
-
else:
|
| 23 |
-
error = result.get("error") or result.get("stderr", "").strip() or "Unknown error"
|
| 24 |
-
print(f" ERROR: {error}\n")
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
async def main() -> None:
|
| 28 |
-
"""Verify Modal sandbox isolation."""
|
| 29 |
-
if not settings.modal_available:
|
| 30 |
-
print("Error: Modal credentials not configured.")
|
| 31 |
-
print("Set MODAL_TOKEN_ID and MODAL_TOKEN_SECRET in .env")
|
| 32 |
-
return
|
| 33 |
-
|
| 34 |
-
try:
|
| 35 |
-
executor = get_code_executor()
|
| 36 |
-
loop = asyncio.get_running_loop()
|
| 37 |
-
|
| 38 |
-
print("=" * 60)
|
| 39 |
-
print("Modal Sandbox Isolation Verification")
|
| 40 |
-
print("=" * 60 + "\n")
|
| 41 |
-
|
| 42 |
-
# Test 1: Hostname
|
| 43 |
-
print("Test 1: Check hostname (should NOT be your machine)")
|
| 44 |
-
code1 = "import socket; print(f'Hostname: {socket.gethostname()}')"
|
| 45 |
-
result1 = await loop.run_in_executor(None, partial(executor.execute, code1))
|
| 46 |
-
print_result(result1)
|
| 47 |
-
|
| 48 |
-
# Test 2: Scientific libraries
|
| 49 |
-
print("Test 2: Verify scientific libraries")
|
| 50 |
-
code2 = """
|
| 51 |
-
import pandas as pd
|
| 52 |
-
import numpy as np
|
| 53 |
-
import scipy
|
| 54 |
-
print(f"pandas: {pd.__version__}")
|
| 55 |
-
print(f"numpy: {np.__version__}")
|
| 56 |
-
print(f"scipy: {scipy.__version__}")
|
| 57 |
-
"""
|
| 58 |
-
result2 = await loop.run_in_executor(None, partial(executor.execute, code2))
|
| 59 |
-
print_result(result2)
|
| 60 |
-
|
| 61 |
-
# Test 3: Network blocked
|
| 62 |
-
print("Test 3: Verify network isolation")
|
| 63 |
-
code3 = """
|
| 64 |
-
import urllib.request
|
| 65 |
-
try:
|
| 66 |
-
urllib.request.urlopen("https://google.com", timeout=2)
|
| 67 |
-
print("Network: ALLOWED (unexpected!)")
|
| 68 |
-
except Exception:
|
| 69 |
-
print("Network: BLOCKED (as expected)")
|
| 70 |
-
"""
|
| 71 |
-
result3 = await loop.run_in_executor(None, partial(executor.execute, code3))
|
| 72 |
-
print_result(result3)
|
| 73 |
-
|
| 74 |
-
# Test 4: Real statistics
|
| 75 |
-
print("Test 4: Execute statistical analysis")
|
| 76 |
-
code4 = """
|
| 77 |
-
import pandas as pd
|
| 78 |
-
import scipy.stats as stats
|
| 79 |
-
|
| 80 |
-
data = pd.DataFrame({'effect': [0.42, 0.38, 0.51]})
|
| 81 |
-
mean = data['effect'].mean()
|
| 82 |
-
t_stat, p_val = stats.ttest_1samp(data['effect'], 0)
|
| 83 |
-
|
| 84 |
-
print(f"Mean Effect: {mean:.3f}")
|
| 85 |
-
print(f"P-value: {p_val:.4f}")
|
| 86 |
-
print(f"Verdict: {'SUPPORTED' if p_val < 0.05 else 'INCONCLUSIVE'}")
|
| 87 |
-
"""
|
| 88 |
-
result4 = await loop.run_in_executor(None, partial(executor.execute, code4))
|
| 89 |
-
print_result(result4)
|
| 90 |
-
|
| 91 |
-
print("=" * 60)
|
| 92 |
-
print("All tests complete - Modal sandbox verified!")
|
| 93 |
-
print("=" * 60)
|
| 94 |
-
|
| 95 |
-
except CodeExecutionError as e:
|
| 96 |
-
print(f"Error: Modal code execution failed: {e}")
|
| 97 |
-
print("Hint: Ensure Modal SDK is installed and credentials are valid.")
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
if __name__ == "__main__":
|
| 101 |
-
asyncio.run(main())
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
pyproject.toml
CHANGED
|
@@ -4,7 +4,7 @@ version = "0.1.0"
|
|
| 4 |
description = "AI-Native Sexual Health Research Agent"
|
| 5 |
readme = "README.md"
|
| 6 |
license = "Apache-2.0"
|
| 7 |
-
requires-python = ">=3.11"
|
| 8 |
dependencies = [
|
| 9 |
# Core
|
| 10 |
"pydantic>=2.7",
|
|
@@ -12,7 +12,8 @@ dependencies = [
|
|
| 12 |
"pydantic-ai>=0.0.16", # Agent framework
|
| 13 |
# AI Providers
|
| 14 |
"openai>=1.0.0",
|
| 15 |
-
|
|
|
|
| 16 |
# HTTP & Parsing
|
| 17 |
"httpx>=0.27", # Async HTTP client (PubMed)
|
| 18 |
"beautifulsoup4>=4.12", # HTML parsing
|
|
@@ -62,18 +63,12 @@ dev = [
|
|
| 62 |
magentic = [
|
| 63 |
"agent-framework-core>=1.0.0b251120,<2.0.0", # Microsoft Agent Framework (PyPI)
|
| 64 |
]
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
"sentence-transformers>=2.2.0",
|
| 68 |
-
]
|
| 69 |
-
modal = [
|
| 70 |
-
# Mario's Modal code execution + LlamaIndex RAG
|
| 71 |
-
"modal>=0.63.0",
|
| 72 |
"llama-index>=0.11.0",
|
| 73 |
"llama-index-llms-openai",
|
| 74 |
"llama-index-embeddings-openai",
|
| 75 |
"llama-index-vector-stores-chroma",
|
| 76 |
-
"chromadb>=0.4.0",
|
| 77 |
]
|
| 78 |
|
| 79 |
[build-system]
|
|
|
|
| 4 |
description = "AI-Native Sexual Health Research Agent"
|
| 5 |
readme = "README.md"
|
| 6 |
license = "Apache-2.0"
|
| 7 |
+
requires-python = ">=3.11,<4.0"
|
| 8 |
dependencies = [
|
| 9 |
# Core
|
| 10 |
"pydantic>=2.7",
|
|
|
|
| 12 |
"pydantic-ai>=0.0.16", # Agent framework
|
| 13 |
# AI Providers
|
| 14 |
"openai>=1.0.0",
|
| 15 |
+
"chromadb>=0.4.22",
|
| 16 |
+
"sentence-transformers>=2.2.2",
|
| 17 |
# HTTP & Parsing
|
| 18 |
"httpx>=0.27", # Async HTTP client (PubMed)
|
| 19 |
"beautifulsoup4>=4.12", # HTML parsing
|
|
|
|
| 63 |
magentic = [
|
| 64 |
"agent-framework-core>=1.0.0b251120,<2.0.0", # Microsoft Agent Framework (PyPI)
|
| 65 |
]
|
| 66 |
+
rag = [
|
| 67 |
+
# LlamaIndex RAG support (chromadb already in core deps)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
"llama-index>=0.11.0",
|
| 69 |
"llama-index-llms-openai",
|
| 70 |
"llama-index-embeddings-openai",
|
| 71 |
"llama-index-vector-stores-chroma",
|
|
|
|
| 72 |
]
|
| 73 |
|
| 74 |
[build-system]
|
requirements.txt
CHANGED
|
@@ -5,7 +5,8 @@ pydantic-ai>=0.0.16
|
|
| 5 |
|
| 6 |
# AI Providers
|
| 7 |
openai>=1.0.0
|
| 8 |
-
|
|
|
|
| 9 |
huggingface-hub>=0.20.0
|
| 10 |
|
| 11 |
# Multi-agent orchestration (Advanced mode)
|
|
@@ -37,9 +38,8 @@ requests>=2.32.5
|
|
| 37 |
limits>=3.0
|
| 38 |
urllib3>=2.5.0 # Security fix for GHSA-48p4-8xcf-vxj5
|
| 39 |
|
| 40 |
-
# Optional:
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
sentence-transformers>=2.2.0
|
|
|
|
| 5 |
|
| 6 |
# AI Providers
|
| 7 |
openai>=1.0.0
|
| 8 |
+
chromadb>=0.4.22
|
| 9 |
+
sentence-transformers>=2.2.2
|
| 10 |
huggingface-hub>=0.20.0
|
| 11 |
|
| 12 |
# Multi-agent orchestration (Advanced mode)
|
|
|
|
| 38 |
limits>=3.0
|
| 39 |
urllib3>=2.5.0 # Security fix for GHSA-48p4-8xcf-vxj5
|
| 40 |
|
| 41 |
+
# Optional: LlamaIndex RAG (chromadb/sentence-transformers already in core above)
|
| 42 |
+
llama-index>=0.11.0
|
| 43 |
+
llama-index-llms-openai
|
| 44 |
+
llama-index-embeddings-openai
|
| 45 |
+
llama-index-vector-stores-chroma
|
|
|
src/agent_factory/judges.py
CHANGED
|
@@ -63,24 +63,11 @@ def get_model(api_key: str | None = None) -> Any:
|
|
| 63 |
|
| 64 |
Args:
|
| 65 |
api_key: Optional BYOK key. Auto-detects provider from prefix:
|
| 66 |
-
- "sk-ant-..." → Anthropic (NOT SUPPORTED - raises error)
|
| 67 |
- "sk-..." → OpenAI
|
| 68 |
- Other → Falls through to env vars
|
| 69 |
-
|
| 70 |
-
Raises:
|
| 71 |
-
NotImplementedError: If Anthropic key detected (no embeddings support).
|
| 72 |
-
|
| 73 |
-
Note: Anthropic is NOT supported because it lacks embeddings API.
|
| 74 |
-
See P3_REMOVE_ANTHROPIC_PARTIAL_WIRING.md.
|
| 75 |
"""
|
| 76 |
# Priority 1: BYOK - Auto-detect provider from key prefix
|
| 77 |
if api_key:
|
| 78 |
-
if api_key.startswith("sk-ant-"):
|
| 79 |
-
# Anthropic not supported - no embeddings API
|
| 80 |
-
raise NotImplementedError(
|
| 81 |
-
"Anthropic is not supported (no embeddings API). "
|
| 82 |
-
"Use OpenAI key (sk-...) or leave empty for free HuggingFace tier."
|
| 83 |
-
)
|
| 84 |
if api_key.startswith("sk-"):
|
| 85 |
# OpenAI BYOK
|
| 86 |
openai_provider = OpenAIProvider(api_key=api_key)
|
|
|
|
| 63 |
|
| 64 |
Args:
|
| 65 |
api_key: Optional BYOK key. Auto-detects provider from prefix:
|
|
|
|
| 66 |
- "sk-..." → OpenAI
|
| 67 |
- Other → Falls through to env vars
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 68 |
"""
|
| 69 |
# Priority 1: BYOK - Auto-detect provider from key prefix
|
| 70 |
if api_key:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
if api_key.startswith("sk-"):
|
| 72 |
# OpenAI BYOK
|
| 73 |
openai_provider = OpenAIProvider(api_key=api_key)
|
src/agents/analysis_agent.py
DELETED
|
@@ -1,145 +0,0 @@
|
|
| 1 |
-
"""Analysis agent for statistical analysis using Modal code execution.
|
| 2 |
-
|
| 3 |
-
This agent wraps StatisticalAnalyzer for use in magentic multi-agent mode.
|
| 4 |
-
The core logic is in src/services/statistical_analyzer.py to avoid
|
| 5 |
-
coupling agent_framework to the simple orchestrator.
|
| 6 |
-
"""
|
| 7 |
-
|
| 8 |
-
from collections.abc import AsyncIterable
|
| 9 |
-
from typing import TYPE_CHECKING, Any
|
| 10 |
-
|
| 11 |
-
from agent_framework import (
|
| 12 |
-
AgentRunResponse,
|
| 13 |
-
AgentRunResponseUpdate,
|
| 14 |
-
AgentThread,
|
| 15 |
-
BaseAgent,
|
| 16 |
-
ChatMessage,
|
| 17 |
-
Role,
|
| 18 |
-
)
|
| 19 |
-
|
| 20 |
-
from src.services.statistical_analyzer import (
|
| 21 |
-
AnalysisResult,
|
| 22 |
-
get_statistical_analyzer,
|
| 23 |
-
)
|
| 24 |
-
|
| 25 |
-
if TYPE_CHECKING:
|
| 26 |
-
from src.services.embeddings import EmbeddingService
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
class AnalysisAgent(BaseAgent): # type: ignore[misc]
|
| 30 |
-
"""Wraps StatisticalAnalyzer for magentic multi-agent mode."""
|
| 31 |
-
|
| 32 |
-
def __init__(
|
| 33 |
-
self,
|
| 34 |
-
evidence_store: dict[str, Any],
|
| 35 |
-
embedding_service: "EmbeddingService | None" = None,
|
| 36 |
-
) -> None:
|
| 37 |
-
super().__init__(
|
| 38 |
-
name="AnalysisAgent",
|
| 39 |
-
description="Performs statistical analysis using Modal sandbox",
|
| 40 |
-
)
|
| 41 |
-
self._evidence_store = evidence_store
|
| 42 |
-
self._embeddings = embedding_service
|
| 43 |
-
self._analyzer = get_statistical_analyzer()
|
| 44 |
-
|
| 45 |
-
async def run(
|
| 46 |
-
self,
|
| 47 |
-
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
|
| 48 |
-
*,
|
| 49 |
-
thread: AgentThread | None = None,
|
| 50 |
-
**kwargs: Any,
|
| 51 |
-
) -> AgentRunResponse:
|
| 52 |
-
"""Analyze evidence and return verdict."""
|
| 53 |
-
query = self._extract_query(messages)
|
| 54 |
-
hypotheses = self._evidence_store.get("hypotheses", [])
|
| 55 |
-
evidence = self._evidence_store.get("current", [])
|
| 56 |
-
|
| 57 |
-
if not evidence:
|
| 58 |
-
return self._error_response("No evidence available.")
|
| 59 |
-
|
| 60 |
-
# Get primary hypothesis if available
|
| 61 |
-
hypothesis_dict = None
|
| 62 |
-
if hypotheses:
|
| 63 |
-
h = hypotheses[0]
|
| 64 |
-
hypothesis_dict = {
|
| 65 |
-
"drug": getattr(h, "drug", "Unknown"),
|
| 66 |
-
"target": getattr(h, "target", "?"),
|
| 67 |
-
"pathway": getattr(h, "pathway", "?"),
|
| 68 |
-
"effect": getattr(h, "effect", "?"),
|
| 69 |
-
"confidence": getattr(h, "confidence", 0.5),
|
| 70 |
-
}
|
| 71 |
-
|
| 72 |
-
# Delegate to StatisticalAnalyzer
|
| 73 |
-
result = await self._analyzer.analyze(
|
| 74 |
-
query=query,
|
| 75 |
-
evidence=evidence,
|
| 76 |
-
hypothesis=hypothesis_dict,
|
| 77 |
-
)
|
| 78 |
-
|
| 79 |
-
# Store in shared context
|
| 80 |
-
self._evidence_store["analysis"] = result.model_dump()
|
| 81 |
-
|
| 82 |
-
# Format response
|
| 83 |
-
response_text = self._format_response(result)
|
| 84 |
-
|
| 85 |
-
return AgentRunResponse(
|
| 86 |
-
messages=[ChatMessage(role=Role.ASSISTANT, text=response_text)],
|
| 87 |
-
response_id=f"analysis-{result.verdict.lower()}",
|
| 88 |
-
additional_properties={"analysis": result.model_dump()},
|
| 89 |
-
)
|
| 90 |
-
|
| 91 |
-
def _format_response(self, result: AnalysisResult) -> str:
|
| 92 |
-
"""Format analysis result as markdown."""
|
| 93 |
-
lines = [
|
| 94 |
-
"## Statistical Analysis Complete\n",
|
| 95 |
-
f"### Verdict: **{result.verdict}**",
|
| 96 |
-
f"**Confidence**: {result.confidence:.0%}\n",
|
| 97 |
-
"### Key Findings",
|
| 98 |
-
]
|
| 99 |
-
for finding in result.key_findings:
|
| 100 |
-
lines.append(f"- {finding}")
|
| 101 |
-
|
| 102 |
-
lines.extend(
|
| 103 |
-
[
|
| 104 |
-
"\n### Statistical Evidence",
|
| 105 |
-
"```",
|
| 106 |
-
result.statistical_evidence,
|
| 107 |
-
"```",
|
| 108 |
-
]
|
| 109 |
-
)
|
| 110 |
-
return "\n".join(lines)
|
| 111 |
-
|
| 112 |
-
def _error_response(self, message: str) -> AgentRunResponse:
|
| 113 |
-
"""Create error response."""
|
| 114 |
-
return AgentRunResponse(
|
| 115 |
-
messages=[ChatMessage(role=Role.ASSISTANT, text=f"**Error**: {message}")],
|
| 116 |
-
response_id="analysis-error",
|
| 117 |
-
)
|
| 118 |
-
|
| 119 |
-
def _extract_query(
|
| 120 |
-
self,
|
| 121 |
-
messages: str | ChatMessage | list[str] | list[ChatMessage] | None,
|
| 122 |
-
) -> str:
|
| 123 |
-
"""Extract query from messages."""
|
| 124 |
-
if isinstance(messages, str):
|
| 125 |
-
return messages
|
| 126 |
-
elif isinstance(messages, ChatMessage):
|
| 127 |
-
return messages.text or ""
|
| 128 |
-
elif isinstance(messages, list):
|
| 129 |
-
for msg in reversed(messages):
|
| 130 |
-
if isinstance(msg, ChatMessage) and msg.role == Role.USER:
|
| 131 |
-
return msg.text or ""
|
| 132 |
-
elif isinstance(msg, str):
|
| 133 |
-
return msg
|
| 134 |
-
return ""
|
| 135 |
-
|
| 136 |
-
async def run_stream(
|
| 137 |
-
self,
|
| 138 |
-
messages: str | ChatMessage | list[str] | list[ChatMessage] | None = None,
|
| 139 |
-
*,
|
| 140 |
-
thread: AgentThread | None = None,
|
| 141 |
-
**kwargs: Any,
|
| 142 |
-
) -> AsyncIterable[AgentRunResponseUpdate]:
|
| 143 |
-
"""Streaming wrapper."""
|
| 144 |
-
result = await self.run(messages, thread=thread, **kwargs)
|
| 145 |
-
yield AgentRunResponseUpdate(messages=result.messages, response_id=result.response_id)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/agents/code_executor_agent.py
DELETED
|
@@ -1,66 +0,0 @@
|
|
| 1 |
-
"""Code execution agent using Modal."""
|
| 2 |
-
|
| 3 |
-
import asyncio
|
| 4 |
-
|
| 5 |
-
import structlog
|
| 6 |
-
from agent_framework import ChatAgent, ai_function
|
| 7 |
-
|
| 8 |
-
from src.clients.base import BaseChatClient
|
| 9 |
-
from src.clients.factory import get_chat_client
|
| 10 |
-
from src.tools.code_execution import get_code_executor
|
| 11 |
-
|
| 12 |
-
logger = structlog.get_logger()
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
@ai_function # type: ignore[arg-type, misc]
|
| 16 |
-
async def execute_python_code(code: str) -> str:
|
| 17 |
-
"""Execute Python code in a secure sandbox.
|
| 18 |
-
|
| 19 |
-
Args:
|
| 20 |
-
code: The Python code to execute.
|
| 21 |
-
|
| 22 |
-
Returns:
|
| 23 |
-
The standard output and standard error of the execution.
|
| 24 |
-
"""
|
| 25 |
-
logger.info("Code execution starting", code_length=len(code))
|
| 26 |
-
executor = get_code_executor()
|
| 27 |
-
loop = asyncio.get_running_loop()
|
| 28 |
-
|
| 29 |
-
# Run in executor to avoid blocking
|
| 30 |
-
try:
|
| 31 |
-
result = await loop.run_in_executor(None, lambda: executor.execute(code))
|
| 32 |
-
if result["success"]:
|
| 33 |
-
logger.info("Code execution succeeded")
|
| 34 |
-
return f"Stdout:\n{result['stdout']}"
|
| 35 |
-
else:
|
| 36 |
-
logger.warning("Code execution failed", error=result.get("error"))
|
| 37 |
-
return f"Error:\n{result['error']}\nStderr:\n{result['stderr']}"
|
| 38 |
-
except Exception as e:
|
| 39 |
-
logger.error("Code execution exception", error=str(e))
|
| 40 |
-
return f"Execution failed: {e}"
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
def create_code_executor_agent(chat_client: BaseChatClient | None = None) -> ChatAgent:
|
| 44 |
-
"""Create a code executor agent.
|
| 45 |
-
|
| 46 |
-
Args:
|
| 47 |
-
chat_client: Optional custom chat client.
|
| 48 |
-
|
| 49 |
-
Returns:
|
| 50 |
-
ChatAgent configured for code execution.
|
| 51 |
-
"""
|
| 52 |
-
client = chat_client or get_chat_client()
|
| 53 |
-
|
| 54 |
-
return ChatAgent(
|
| 55 |
-
name="CodeExecutorAgent",
|
| 56 |
-
description="Executes Python code for data analysis, calculation, and simulation.",
|
| 57 |
-
instructions="""You are a code execution expert.
|
| 58 |
-
When asked to analyze data or perform calculations, write Python code and execute it.
|
| 59 |
-
Use libraries like pandas, numpy, scipy, matplotlib.
|
| 60 |
-
|
| 61 |
-
Always output the code you want to execute using the `execute_python_code` tool.
|
| 62 |
-
Check the output and interpret the results.""",
|
| 63 |
-
chat_client=client,
|
| 64 |
-
tools=[execute_python_code],
|
| 65 |
-
temperature=0.0, # Strict code generation
|
| 66 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/app.py
CHANGED
|
@@ -161,7 +161,7 @@ async def research_agent(
|
|
| 161 |
if not has_paid_key:
|
| 162 |
yield (
|
| 163 |
"🤗 **Free Tier**: Using HuggingFace Inference (Llama 3.1 / Mistral) for AI analysis.\n"
|
| 164 |
-
"For premium models, enter an OpenAI
|
| 165 |
)
|
| 166 |
|
| 167 |
# Run the agent and stream events
|
|
|
|
| 161 |
if not has_paid_key:
|
| 162 |
yield (
|
| 163 |
"🤗 **Free Tier**: Using HuggingFace Inference (Llama 3.1 / Mistral) for AI analysis.\n"
|
| 164 |
+
"For premium models, enter an OpenAI API key below.\n\n"
|
| 165 |
)
|
| 166 |
|
| 167 |
# Run the agent and stream events
|
src/mcp_tools.py
CHANGED
|
@@ -161,72 +161,3 @@ async def search_all_sources(
|
|
| 161 |
formatted.append(f"## Europe PMC\n*Error: {europepmc_results}*\n")
|
| 162 |
|
| 163 |
return "\n---\n".join(formatted)
|
| 164 |
-
|
| 165 |
-
|
| 166 |
-
async def analyze_hypothesis(
|
| 167 |
-
drug: str,
|
| 168 |
-
condition: str,
|
| 169 |
-
evidence_summary: str,
|
| 170 |
-
) -> str:
|
| 171 |
-
"""Perform statistical analysis of research hypothesis using Modal.
|
| 172 |
-
|
| 173 |
-
Executes AI-generated Python code in a secure Modal sandbox to analyze
|
| 174 |
-
the statistical evidence for a research hypothesis.
|
| 175 |
-
|
| 176 |
-
Args:
|
| 177 |
-
drug: The drug being evaluated (e.g., "sildenafil")
|
| 178 |
-
condition: The target condition (e.g., "erectile dysfunction")
|
| 179 |
-
evidence_summary: Summary of evidence to analyze
|
| 180 |
-
|
| 181 |
-
Returns:
|
| 182 |
-
Analysis result with verdict (SUPPORTED/REFUTED/INCONCLUSIVE) and statistics
|
| 183 |
-
"""
|
| 184 |
-
from src.services.statistical_analyzer import get_statistical_analyzer
|
| 185 |
-
from src.utils.config import settings
|
| 186 |
-
from src.utils.models import Citation, Evidence
|
| 187 |
-
|
| 188 |
-
if not settings.modal_available:
|
| 189 |
-
return "Error: Modal credentials not configured. Set MODAL_TOKEN_ID and MODAL_TOKEN_SECRET."
|
| 190 |
-
|
| 191 |
-
# Create evidence from summary
|
| 192 |
-
evidence = [
|
| 193 |
-
Evidence(
|
| 194 |
-
content=evidence_summary,
|
| 195 |
-
citation=Citation(
|
| 196 |
-
source="pubmed",
|
| 197 |
-
title=f"Evidence for {drug} in {condition}",
|
| 198 |
-
url="https://example.com",
|
| 199 |
-
date="2024-01-01",
|
| 200 |
-
authors=["User Provided"],
|
| 201 |
-
),
|
| 202 |
-
relevance=0.9,
|
| 203 |
-
)
|
| 204 |
-
]
|
| 205 |
-
|
| 206 |
-
analyzer = get_statistical_analyzer()
|
| 207 |
-
result = await analyzer.analyze(
|
| 208 |
-
query=f"Can {drug} treat {condition}?",
|
| 209 |
-
evidence=evidence,
|
| 210 |
-
hypothesis={"drug": drug, "target": "unknown", "pathway": "unknown", "effect": condition},
|
| 211 |
-
)
|
| 212 |
-
|
| 213 |
-
return f"""## Statistical Analysis: {drug} for {condition}
|
| 214 |
-
|
| 215 |
-
### Verdict: **{result.verdict}**
|
| 216 |
-
**Confidence**: {result.confidence:.0%}
|
| 217 |
-
|
| 218 |
-
### Key Findings
|
| 219 |
-
{chr(10).join(f"- {f}" for f in result.key_findings) or "- No specific findings extracted"}
|
| 220 |
-
|
| 221 |
-
### Execution Output
|
| 222 |
-
```
|
| 223 |
-
{result.execution_output}
|
| 224 |
-
```
|
| 225 |
-
|
| 226 |
-
### Generated Code
|
| 227 |
-
```python
|
| 228 |
-
{result.code_generated}
|
| 229 |
-
```
|
| 230 |
-
|
| 231 |
-
**Executed in Modal Sandbox** - Isolated, secure, reproducible.
|
| 232 |
-
"""
|
|
|
|
| 161 |
formatted.append(f"## Europe PMC\n*Error: {europepmc_results}*\n")
|
| 162 |
|
| 163 |
return "\n---\n".join(formatted)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/services/llamaindex_rag.py
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
"""LlamaIndex RAG service for evidence retrieval and indexing.
|
| 2 |
|
| 3 |
-
Requires optional dependencies: uv sync --extra
|
| 4 |
|
| 5 |
Migration Note (v1.0 rebrand):
|
| 6 |
Default collection_name changed from "deepcritical_evidence" to "deepboner_evidence".
|
|
@@ -64,7 +64,7 @@ class LlamaIndexRAGService:
|
|
| 64 |
from llama_index.vector_stores.chroma import ChromaVectorStore
|
| 65 |
except ImportError as e:
|
| 66 |
raise ImportError(
|
| 67 |
-
"LlamaIndex dependencies not installed. Run: uv sync --extra
|
| 68 |
) from e
|
| 69 |
|
| 70 |
# Store references for use in other methods
|
|
@@ -91,12 +91,6 @@ class LlamaIndexRAGService:
|
|
| 91 |
raise ConfigurationError("OPENAI_API_KEY required for LlamaIndex RAG service")
|
| 92 |
|
| 93 |
# Defense-in-depth: Validate key prefix to prevent cryptic auth errors
|
| 94 |
-
# Note: Anthropic keys start with sk-ant-, which would pass startswith("sk-")
|
| 95 |
-
if self.api_key.startswith("sk-ant-"):
|
| 96 |
-
raise ConfigurationError(
|
| 97 |
-
"Anthropic keys (sk-ant-...) are not supported for embeddings. "
|
| 98 |
-
"LlamaIndex RAG requires an OpenAI API key (sk-...)."
|
| 99 |
-
)
|
| 100 |
if not self.api_key.startswith("sk-"):
|
| 101 |
raise ConfigurationError(
|
| 102 |
f"Invalid API key format. Expected OpenAI key starting with 'sk-', "
|
|
|
|
| 1 |
"""LlamaIndex RAG service for evidence retrieval and indexing.
|
| 2 |
|
| 3 |
+
Requires optional dependencies: uv sync --extra rag
|
| 4 |
|
| 5 |
Migration Note (v1.0 rebrand):
|
| 6 |
Default collection_name changed from "deepcritical_evidence" to "deepboner_evidence".
|
|
|
|
| 64 |
from llama_index.vector_stores.chroma import ChromaVectorStore
|
| 65 |
except ImportError as e:
|
| 66 |
raise ImportError(
|
| 67 |
+
"LlamaIndex dependencies not installed. Run: uv sync --extra rag"
|
| 68 |
) from e
|
| 69 |
|
| 70 |
# Store references for use in other methods
|
|
|
|
| 91 |
raise ConfigurationError("OPENAI_API_KEY required for LlamaIndex RAG service")
|
| 92 |
|
| 93 |
# Defense-in-depth: Validate key prefix to prevent cryptic auth errors
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 94 |
if not self.api_key.startswith("sk-"):
|
| 95 |
raise ConfigurationError(
|
| 96 |
f"Invalid API key format. Expected OpenAI key starting with 'sk-', "
|
src/services/statistical_analyzer.py
DELETED
|
@@ -1,259 +0,0 @@
|
|
| 1 |
-
"""Statistical analysis service using Modal code execution.
|
| 2 |
-
|
| 3 |
-
This module provides Modal-based statistical analysis WITHOUT depending on
|
| 4 |
-
agent_framework. This allows it to be used in the simple orchestrator mode
|
| 5 |
-
without requiring the magentic optional dependency.
|
| 6 |
-
|
| 7 |
-
The AnalysisAgent (in src/agents/) wraps this service for magentic mode.
|
| 8 |
-
"""
|
| 9 |
-
|
| 10 |
-
import asyncio
|
| 11 |
-
import re
|
| 12 |
-
from functools import lru_cache, partial
|
| 13 |
-
from typing import Any, Literal
|
| 14 |
-
|
| 15 |
-
import structlog
|
| 16 |
-
|
| 17 |
-
# Type alias for verdict values
|
| 18 |
-
VerdictType = Literal["SUPPORTED", "REFUTED", "INCONCLUSIVE"]
|
| 19 |
-
|
| 20 |
-
from pydantic import BaseModel, Field
|
| 21 |
-
from pydantic_ai import Agent
|
| 22 |
-
|
| 23 |
-
from src.agent_factory.judges import get_model
|
| 24 |
-
from src.tools.code_execution import (
|
| 25 |
-
CodeExecutionError,
|
| 26 |
-
get_code_executor,
|
| 27 |
-
get_sandbox_library_prompt,
|
| 28 |
-
)
|
| 29 |
-
from src.utils.models import Evidence
|
| 30 |
-
|
| 31 |
-
logger = structlog.get_logger()
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
class AnalysisResult(BaseModel):
|
| 35 |
-
"""Result of statistical analysis."""
|
| 36 |
-
|
| 37 |
-
verdict: VerdictType = Field(
|
| 38 |
-
description="SUPPORTED, REFUTED, or INCONCLUSIVE",
|
| 39 |
-
)
|
| 40 |
-
confidence: float = Field(ge=0.0, le=1.0, description="Confidence in verdict (0-1)")
|
| 41 |
-
statistical_evidence: str = Field(
|
| 42 |
-
description="Summary of statistical findings from code execution"
|
| 43 |
-
)
|
| 44 |
-
code_generated: str = Field(description="Python code that was executed")
|
| 45 |
-
execution_output: str = Field(description="Output from code execution")
|
| 46 |
-
key_findings: list[str] = Field(default_factory=list, description="Key takeaways")
|
| 47 |
-
limitations: list[str] = Field(default_factory=list, description="Limitations")
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
class StatisticalAnalyzer:
|
| 51 |
-
"""Performs statistical analysis using Modal code execution.
|
| 52 |
-
|
| 53 |
-
This service:
|
| 54 |
-
1. Generates Python code for statistical analysis using LLM
|
| 55 |
-
2. Executes code in Modal sandbox
|
| 56 |
-
3. Interprets results
|
| 57 |
-
4. Returns verdict (SUPPORTED/REFUTED/INCONCLUSIVE)
|
| 58 |
-
|
| 59 |
-
Note: This class has NO agent_framework dependency, making it safe
|
| 60 |
-
to use in the simple orchestrator without the magentic extra.
|
| 61 |
-
"""
|
| 62 |
-
|
| 63 |
-
def __init__(self) -> None:
|
| 64 |
-
"""Initialize the analyzer."""
|
| 65 |
-
self._code_executor: Any = None
|
| 66 |
-
self._agent: Agent[None, str] | None = None
|
| 67 |
-
|
| 68 |
-
def _get_code_executor(self) -> Any:
|
| 69 |
-
"""Lazy initialization of code executor."""
|
| 70 |
-
if self._code_executor is None:
|
| 71 |
-
self._code_executor = get_code_executor()
|
| 72 |
-
return self._code_executor
|
| 73 |
-
|
| 74 |
-
def _get_agent(self) -> Agent[None, str]:
|
| 75 |
-
"""Lazy initialization of LLM agent for code generation."""
|
| 76 |
-
if self._agent is None:
|
| 77 |
-
library_versions = get_sandbox_library_prompt()
|
| 78 |
-
self._agent = Agent(
|
| 79 |
-
model=get_model(),
|
| 80 |
-
output_type=str,
|
| 81 |
-
system_prompt=f"""You are a biomedical data scientist.
|
| 82 |
-
|
| 83 |
-
Generate Python code to analyze research evidence and test hypotheses.
|
| 84 |
-
|
| 85 |
-
Guidelines:
|
| 86 |
-
1. Use pandas, numpy, scipy.stats for analysis
|
| 87 |
-
2. Print clear, interpretable results
|
| 88 |
-
3. Include statistical tests (t-tests, chi-square, etc.)
|
| 89 |
-
4. Calculate effect sizes and confidence intervals
|
| 90 |
-
5. Keep code concise (<50 lines)
|
| 91 |
-
6. Set 'result' variable to SUPPORTED, REFUTED, or INCONCLUSIVE
|
| 92 |
-
|
| 93 |
-
Available libraries:
|
| 94 |
-
{library_versions}
|
| 95 |
-
|
| 96 |
-
Output format: Return ONLY executable Python code, no explanations.""",
|
| 97 |
-
)
|
| 98 |
-
return self._agent
|
| 99 |
-
|
| 100 |
-
async def analyze(
|
| 101 |
-
self,
|
| 102 |
-
query: str,
|
| 103 |
-
evidence: list[Evidence],
|
| 104 |
-
hypothesis: dict[str, Any] | None = None,
|
| 105 |
-
) -> AnalysisResult:
|
| 106 |
-
"""Run statistical analysis on evidence.
|
| 107 |
-
|
| 108 |
-
Args:
|
| 109 |
-
query: The research question
|
| 110 |
-
evidence: List of Evidence objects to analyze
|
| 111 |
-
hypothesis: Optional hypothesis dict with drug, target, pathway, effect
|
| 112 |
-
|
| 113 |
-
Returns:
|
| 114 |
-
AnalysisResult with verdict and statistics
|
| 115 |
-
"""
|
| 116 |
-
# Build analysis prompt (method handles slicing internally)
|
| 117 |
-
evidence_summary = self._summarize_evidence(evidence)
|
| 118 |
-
hypothesis_text = ""
|
| 119 |
-
if hypothesis:
|
| 120 |
-
hypothesis_text = (
|
| 121 |
-
f"\nHypothesis: {hypothesis.get('drug', 'Unknown')} → "
|
| 122 |
-
f"{hypothesis.get('target', '?')} → "
|
| 123 |
-
f"{hypothesis.get('pathway', '?')} → "
|
| 124 |
-
f"{hypothesis.get('effect', '?')}\n"
|
| 125 |
-
f"Confidence: {hypothesis.get('confidence', 0.5):.0%}\n"
|
| 126 |
-
)
|
| 127 |
-
|
| 128 |
-
prompt = f"""Generate Python code to statistically analyze:
|
| 129 |
-
|
| 130 |
-
**Research Question**: {query}
|
| 131 |
-
{hypothesis_text}
|
| 132 |
-
|
| 133 |
-
**Evidence Summary**:
|
| 134 |
-
{evidence_summary}
|
| 135 |
-
|
| 136 |
-
Generate executable Python code to analyze this evidence."""
|
| 137 |
-
|
| 138 |
-
try:
|
| 139 |
-
# Generate code
|
| 140 |
-
agent = self._get_agent()
|
| 141 |
-
code_result = await agent.run(prompt)
|
| 142 |
-
generated_code = code_result.output
|
| 143 |
-
|
| 144 |
-
# Execute in Modal sandbox
|
| 145 |
-
loop = asyncio.get_running_loop()
|
| 146 |
-
executor = self._get_code_executor()
|
| 147 |
-
execution = await loop.run_in_executor(
|
| 148 |
-
None, partial(executor.execute, generated_code, timeout=120)
|
| 149 |
-
)
|
| 150 |
-
|
| 151 |
-
if not execution["success"]:
|
| 152 |
-
return AnalysisResult(
|
| 153 |
-
verdict="INCONCLUSIVE",
|
| 154 |
-
confidence=0.0,
|
| 155 |
-
statistical_evidence=(
|
| 156 |
-
f"Execution failed: {execution.get('error', 'Unknown error')}"
|
| 157 |
-
),
|
| 158 |
-
code_generated=generated_code,
|
| 159 |
-
execution_output=execution.get("stderr", ""),
|
| 160 |
-
key_findings=[],
|
| 161 |
-
limitations=["Code execution failed"],
|
| 162 |
-
)
|
| 163 |
-
|
| 164 |
-
# Interpret results
|
| 165 |
-
return self._interpret_results(generated_code, execution)
|
| 166 |
-
|
| 167 |
-
except CodeExecutionError as e:
|
| 168 |
-
return AnalysisResult(
|
| 169 |
-
verdict="INCONCLUSIVE",
|
| 170 |
-
confidence=0.0,
|
| 171 |
-
statistical_evidence=str(e),
|
| 172 |
-
code_generated="",
|
| 173 |
-
execution_output="",
|
| 174 |
-
key_findings=[],
|
| 175 |
-
limitations=[f"Analysis error: {e}"],
|
| 176 |
-
)
|
| 177 |
-
|
| 178 |
-
def _summarize_evidence(self, evidence: list[Evidence]) -> str:
|
| 179 |
-
"""Summarize evidence for code generation prompt."""
|
| 180 |
-
if not evidence:
|
| 181 |
-
return "No evidence available."
|
| 182 |
-
|
| 183 |
-
lines = []
|
| 184 |
-
for i, ev in enumerate(evidence[:5], 1):
|
| 185 |
-
content = ev.content
|
| 186 |
-
truncated = content[:200] + ("..." if len(content) > 200 else "")
|
| 187 |
-
lines.append(f"{i}. {truncated}")
|
| 188 |
-
lines.append(f" Source: {ev.citation.title}")
|
| 189 |
-
lines.append(f" Relevance: {ev.relevance:.0%}\n")
|
| 190 |
-
|
| 191 |
-
return "\n".join(lines)
|
| 192 |
-
|
| 193 |
-
def _interpret_results(
|
| 194 |
-
self,
|
| 195 |
-
code: str,
|
| 196 |
-
execution: dict[str, Any],
|
| 197 |
-
) -> AnalysisResult:
|
| 198 |
-
"""Interpret code execution results."""
|
| 199 |
-
stdout = execution["stdout"]
|
| 200 |
-
stdout_upper = stdout.upper()
|
| 201 |
-
|
| 202 |
-
# Extract verdict with robust word-boundary matching
|
| 203 |
-
verdict: VerdictType = "INCONCLUSIVE"
|
| 204 |
-
if re.search(r"\bSUPPORTED\b", stdout_upper) and not re.search(
|
| 205 |
-
r"\b(?:NOT|UN)SUPPORTED\b", stdout_upper
|
| 206 |
-
):
|
| 207 |
-
verdict = "SUPPORTED"
|
| 208 |
-
elif re.search(r"\bREFUTED\b", stdout_upper):
|
| 209 |
-
verdict = "REFUTED"
|
| 210 |
-
|
| 211 |
-
# Extract key findings
|
| 212 |
-
key_findings = []
|
| 213 |
-
for line in stdout.split("\n"):
|
| 214 |
-
line_lower = line.lower()
|
| 215 |
-
if any(kw in line_lower for kw in ["p-value", "significant", "effect", "mean"]):
|
| 216 |
-
key_findings.append(line.strip())
|
| 217 |
-
|
| 218 |
-
# Calculate confidence from p-values
|
| 219 |
-
confidence = self._calculate_confidence(stdout)
|
| 220 |
-
|
| 221 |
-
return AnalysisResult(
|
| 222 |
-
verdict=verdict,
|
| 223 |
-
confidence=confidence,
|
| 224 |
-
statistical_evidence=stdout.strip(),
|
| 225 |
-
code_generated=code,
|
| 226 |
-
execution_output=stdout,
|
| 227 |
-
key_findings=key_findings[:5],
|
| 228 |
-
limitations=[
|
| 229 |
-
"Analysis based on summary data only",
|
| 230 |
-
"Limited to available evidence",
|
| 231 |
-
"Statistical tests assume data independence",
|
| 232 |
-
],
|
| 233 |
-
)
|
| 234 |
-
|
| 235 |
-
def _calculate_confidence(self, output: str) -> float:
|
| 236 |
-
"""Calculate confidence based on statistical results."""
|
| 237 |
-
p_values = re.findall(r"p[-\s]?value[:\s]+(\d+\.?\d*)", output.lower())
|
| 238 |
-
|
| 239 |
-
if p_values:
|
| 240 |
-
try:
|
| 241 |
-
min_p = min(float(p) for p in p_values)
|
| 242 |
-
if min_p < 0.001:
|
| 243 |
-
return 0.95
|
| 244 |
-
elif min_p < 0.01:
|
| 245 |
-
return 0.90
|
| 246 |
-
elif min_p < 0.05:
|
| 247 |
-
return 0.80
|
| 248 |
-
else:
|
| 249 |
-
return 0.60
|
| 250 |
-
except ValueError:
|
| 251 |
-
logger.debug("Failed to parse p-values", p_values=p_values)
|
| 252 |
-
|
| 253 |
-
return 0.70 # Default
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
@lru_cache(maxsize=1)
|
| 257 |
-
def get_statistical_analyzer() -> StatisticalAnalyzer:
|
| 258 |
-
"""Get or create singleton StatisticalAnalyzer instance (thread-safe via lru_cache)."""
|
| 259 |
-
return StatisticalAnalyzer()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/tools/code_execution.py
DELETED
|
@@ -1,260 +0,0 @@
|
|
| 1 |
-
"""Modal-based secure code execution tool for statistical analysis.
|
| 2 |
-
|
| 3 |
-
This module provides sandboxed Python code execution using Modal's serverless infrastructure.
|
| 4 |
-
It's designed for running LLM-generated statistical analysis code safely.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
from functools import lru_cache
|
| 8 |
-
from typing import Any
|
| 9 |
-
|
| 10 |
-
import structlog
|
| 11 |
-
|
| 12 |
-
from src.utils.config import settings
|
| 13 |
-
|
| 14 |
-
logger = structlog.get_logger(__name__)
|
| 15 |
-
|
| 16 |
-
# Shared library versions for Modal sandbox - used by both executor and LLM prompts
|
| 17 |
-
# Keep these in sync to avoid version mismatch between generated code and execution
|
| 18 |
-
SANDBOX_LIBRARIES: dict[str, str] = {
|
| 19 |
-
"pandas": "2.2.0",
|
| 20 |
-
"numpy": "1.26.4",
|
| 21 |
-
"scipy": "1.11.4",
|
| 22 |
-
"matplotlib": "3.8.2",
|
| 23 |
-
"scikit-learn": "1.4.0",
|
| 24 |
-
"statsmodels": "0.14.1",
|
| 25 |
-
}
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
def get_sandbox_library_list() -> list[str]:
|
| 29 |
-
"""Get list of library==version strings for Modal image."""
|
| 30 |
-
return [f"{lib}=={ver}" for lib, ver in SANDBOX_LIBRARIES.items()]
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
def get_sandbox_library_prompt() -> str:
|
| 34 |
-
"""Get formatted library versions for LLM prompts."""
|
| 35 |
-
return "\n".join(f"- {lib}=={ver}" for lib, ver in SANDBOX_LIBRARIES.items())
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
class CodeExecutionError(Exception):
|
| 39 |
-
"""Raised when code execution fails."""
|
| 40 |
-
|
| 41 |
-
pass
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
class ModalCodeExecutor:
|
| 45 |
-
"""Execute Python code securely using Modal sandboxes.
|
| 46 |
-
|
| 47 |
-
This class provides a safe environment for executing LLM-generated code,
|
| 48 |
-
particularly for scientific computing and statistical analysis tasks.
|
| 49 |
-
|
| 50 |
-
Features:
|
| 51 |
-
- Sandboxed execution (isolated from host system)
|
| 52 |
-
- Pre-installed scientific libraries (numpy, scipy, pandas, matplotlib)
|
| 53 |
-
- Network isolation for security
|
| 54 |
-
- Timeout protection
|
| 55 |
-
- Stdout/stderr capture
|
| 56 |
-
|
| 57 |
-
Example:
|
| 58 |
-
>>> executor = ModalCodeExecutor()
|
| 59 |
-
>>> result = executor.execute('''
|
| 60 |
-
... import pandas as pd
|
| 61 |
-
... df = pd.DataFrame({'a': [1, 2, 3]})
|
| 62 |
-
... result = df['a'].sum()
|
| 63 |
-
... ''')
|
| 64 |
-
>>> print(result['stdout'])
|
| 65 |
-
6
|
| 66 |
-
"""
|
| 67 |
-
|
| 68 |
-
def __init__(self) -> None:
|
| 69 |
-
"""Initialize Modal code executor.
|
| 70 |
-
|
| 71 |
-
Note:
|
| 72 |
-
Logs a warning if Modal credentials are not configured.
|
| 73 |
-
Execution will fail at runtime without valid credentials.
|
| 74 |
-
"""
|
| 75 |
-
# Check for Modal credentials
|
| 76 |
-
self.modal_token_id = settings.modal_token_id
|
| 77 |
-
self.modal_token_secret = settings.modal_token_secret
|
| 78 |
-
|
| 79 |
-
if not self.modal_token_id or not self.modal_token_secret:
|
| 80 |
-
logger.warning(
|
| 81 |
-
"Modal credentials not found. Code execution will fail unless modal setup is run."
|
| 82 |
-
)
|
| 83 |
-
|
| 84 |
-
def execute(self, code: str, timeout: int = 60, allow_network: bool = False) -> dict[str, Any]:
|
| 85 |
-
"""Execute Python code in a Modal sandbox.
|
| 86 |
-
|
| 87 |
-
Args:
|
| 88 |
-
code: Python code to execute
|
| 89 |
-
timeout: Maximum execution time in seconds (default: 60)
|
| 90 |
-
allow_network: Whether to allow network access (default: False for security)
|
| 91 |
-
|
| 92 |
-
Returns:
|
| 93 |
-
Dictionary containing:
|
| 94 |
-
- stdout: Standard output from code execution
|
| 95 |
-
- stderr: Standard error from code execution
|
| 96 |
-
- success: Boolean indicating if execution succeeded
|
| 97 |
-
- error: Error message if execution failed
|
| 98 |
-
|
| 99 |
-
Raises:
|
| 100 |
-
CodeExecutionError: If execution fails or times out
|
| 101 |
-
"""
|
| 102 |
-
try:
|
| 103 |
-
import modal
|
| 104 |
-
except ImportError as e:
|
| 105 |
-
raise CodeExecutionError(
|
| 106 |
-
"Modal SDK not installed. Run: uv sync or pip install modal>=0.63.0"
|
| 107 |
-
) from e
|
| 108 |
-
|
| 109 |
-
logger.info("executing_code", code_length=len(code), timeout=timeout)
|
| 110 |
-
|
| 111 |
-
try:
|
| 112 |
-
# Create or lookup Modal app
|
| 113 |
-
app = modal.App.lookup("deepboner-code-execution", create_if_missing=True)
|
| 114 |
-
|
| 115 |
-
# Define scientific computing image with common libraries
|
| 116 |
-
scientific_image = modal.Image.debian_slim(python_version="3.11").pip_install(
|
| 117 |
-
*get_sandbox_library_list()
|
| 118 |
-
)
|
| 119 |
-
|
| 120 |
-
# Create sandbox with security restrictions
|
| 121 |
-
sandbox = modal.Sandbox.create(
|
| 122 |
-
app=app,
|
| 123 |
-
image=scientific_image,
|
| 124 |
-
timeout=timeout,
|
| 125 |
-
block_network=not allow_network, # Wire the network control
|
| 126 |
-
)
|
| 127 |
-
|
| 128 |
-
try:
|
| 129 |
-
# Execute the code
|
| 130 |
-
# Wrap code to capture result
|
| 131 |
-
wrapped_code = f"""
|
| 132 |
-
import sys
|
| 133 |
-
import io
|
| 134 |
-
from contextlib import redirect_stdout, redirect_stderr
|
| 135 |
-
|
| 136 |
-
stdout_io = io.StringIO()
|
| 137 |
-
stderr_io = io.StringIO()
|
| 138 |
-
|
| 139 |
-
try:
|
| 140 |
-
with redirect_stdout(stdout_io), redirect_stderr(stderr_io):
|
| 141 |
-
{self._indent_code(code, 8)}
|
| 142 |
-
print("__EXECUTION_SUCCESS__")
|
| 143 |
-
except Exception as e:
|
| 144 |
-
print(f"__EXECUTION_ERROR__: {{type(e).__name__}}: {{e}}", file=sys.stderr)
|
| 145 |
-
|
| 146 |
-
print("__STDOUT_START__")
|
| 147 |
-
print(stdout_io.getvalue())
|
| 148 |
-
print("__STDOUT_END__")
|
| 149 |
-
print("__STDERR_START__")
|
| 150 |
-
print(stderr_io.getvalue(), file=sys.stderr)
|
| 151 |
-
print("__STDERR_END__", file=sys.stderr)
|
| 152 |
-
"""
|
| 153 |
-
|
| 154 |
-
# Run the wrapped code
|
| 155 |
-
process = sandbox.exec("python", "-c", wrapped_code, timeout=timeout)
|
| 156 |
-
|
| 157 |
-
# Read output
|
| 158 |
-
stdout_raw = process.stdout.read()
|
| 159 |
-
stderr_raw = process.stderr.read()
|
| 160 |
-
finally:
|
| 161 |
-
# Always clean up sandbox to prevent resource leaks
|
| 162 |
-
sandbox.terminate()
|
| 163 |
-
|
| 164 |
-
# Parse output
|
| 165 |
-
success = "__EXECUTION_SUCCESS__" in stdout_raw
|
| 166 |
-
|
| 167 |
-
# Extract actual stdout/stderr
|
| 168 |
-
stdout = self._extract_output(stdout_raw, "__STDOUT_START__", "__STDOUT_END__")
|
| 169 |
-
stderr = self._extract_output(stderr_raw, "__STDERR_START__", "__STDERR_END__")
|
| 170 |
-
|
| 171 |
-
result = {
|
| 172 |
-
"stdout": stdout,
|
| 173 |
-
"stderr": stderr,
|
| 174 |
-
"success": success,
|
| 175 |
-
"error": stderr if not success else None,
|
| 176 |
-
}
|
| 177 |
-
|
| 178 |
-
logger.info(
|
| 179 |
-
"code_execution_completed",
|
| 180 |
-
success=success,
|
| 181 |
-
stdout_length=len(stdout),
|
| 182 |
-
stderr_length=len(stderr),
|
| 183 |
-
)
|
| 184 |
-
|
| 185 |
-
return result
|
| 186 |
-
|
| 187 |
-
except Exception as e:
|
| 188 |
-
logger.error("code_execution_failed", error=str(e), error_type=type(e).__name__)
|
| 189 |
-
raise CodeExecutionError(f"Code execution failed: {e}") from e
|
| 190 |
-
|
| 191 |
-
def execute_with_return(self, code: str, timeout: int = 60) -> Any:
|
| 192 |
-
"""Execute code and return the value of the 'result' variable.
|
| 193 |
-
|
| 194 |
-
Convenience method that executes code and extracts a return value.
|
| 195 |
-
The code should assign its final result to a variable named 'result'.
|
| 196 |
-
|
| 197 |
-
Args:
|
| 198 |
-
code: Python code to execute (must set 'result' variable)
|
| 199 |
-
timeout: Maximum execution time in seconds
|
| 200 |
-
|
| 201 |
-
Returns:
|
| 202 |
-
The value of the 'result' variable from the executed code
|
| 203 |
-
|
| 204 |
-
Example:
|
| 205 |
-
>>> executor.execute_with_return("result = 2 + 2")
|
| 206 |
-
4
|
| 207 |
-
"""
|
| 208 |
-
# Modify code to print result as JSON
|
| 209 |
-
wrapped = f"""
|
| 210 |
-
import json
|
| 211 |
-
{code}
|
| 212 |
-
print(json.dumps({{"__RESULT__": result}}))
|
| 213 |
-
"""
|
| 214 |
-
|
| 215 |
-
execution_result = self.execute(wrapped, timeout=timeout)
|
| 216 |
-
|
| 217 |
-
if not execution_result["success"]:
|
| 218 |
-
raise CodeExecutionError(f"Execution failed: {execution_result['error']}")
|
| 219 |
-
|
| 220 |
-
# Parse result from stdout
|
| 221 |
-
import json
|
| 222 |
-
|
| 223 |
-
try:
|
| 224 |
-
output = execution_result["stdout"].strip()
|
| 225 |
-
if "__RESULT__" in output:
|
| 226 |
-
# Extract JSON line
|
| 227 |
-
for line in output.split("\n"):
|
| 228 |
-
if "__RESULT__" in line:
|
| 229 |
-
data = json.loads(line)
|
| 230 |
-
return data["__RESULT__"]
|
| 231 |
-
raise ValueError("Result not found in output")
|
| 232 |
-
except (json.JSONDecodeError, ValueError) as e:
|
| 233 |
-
logger.warning(
|
| 234 |
-
"failed_to_parse_result", error=str(e), stdout=execution_result["stdout"]
|
| 235 |
-
)
|
| 236 |
-
return execution_result["stdout"]
|
| 237 |
-
|
| 238 |
-
def _indent_code(self, code: str, spaces: int) -> str:
|
| 239 |
-
"""Indent code by specified number of spaces."""
|
| 240 |
-
indent = " " * spaces
|
| 241 |
-
return "\n".join(indent + line if line.strip() else line for line in code.split("\n"))
|
| 242 |
-
|
| 243 |
-
def _extract_output(self, text: str, start_marker: str, end_marker: str) -> str:
|
| 244 |
-
"""Extract content between markers."""
|
| 245 |
-
start_idx = text.find(start_marker)
|
| 246 |
-
if start_idx == -1:
|
| 247 |
-
return text.strip()
|
| 248 |
-
start_idx += len(start_marker)
|
| 249 |
-
|
| 250 |
-
end_idx = text.find(end_marker, start_idx)
|
| 251 |
-
if end_idx == -1:
|
| 252 |
-
return text.strip()
|
| 253 |
-
|
| 254 |
-
return text[start_idx:end_idx].strip()
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
@lru_cache(maxsize=1)
|
| 258 |
-
def get_code_executor() -> ModalCodeExecutor:
|
| 259 |
-
"""Get or create singleton code executor instance (thread-safe via lru_cache)."""
|
| 260 |
-
return ModalCodeExecutor()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/utils/config.py
CHANGED
|
@@ -42,7 +42,7 @@ class Settings(BaseSettings):
|
|
| 42 |
)
|
| 43 |
|
| 44 |
# Embedding Configuration
|
| 45 |
-
# Note: OpenAI embeddings require OPENAI_API_KEY
|
| 46 |
openai_embedding_model: str = Field(
|
| 47 |
default="text-embedding-3-small",
|
| 48 |
description="OpenAI embedding model (used by LlamaIndex RAG)",
|
|
@@ -77,15 +77,8 @@ class Settings(BaseSettings):
|
|
| 77 |
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
|
| 78 |
|
| 79 |
# External Services
|
| 80 |
-
modal_token_id: str | None = Field(default=None, description="Modal token ID")
|
| 81 |
-
modal_token_secret: str | None = Field(default=None, description="Modal token secret")
|
| 82 |
chroma_db_path: str = Field(default="./chroma_db", description="ChromaDB storage path")
|
| 83 |
|
| 84 |
-
@property
|
| 85 |
-
def modal_available(self) -> bool:
|
| 86 |
-
"""Check if Modal credentials are configured."""
|
| 87 |
-
return bool(self.modal_token_id and self.modal_token_secret)
|
| 88 |
-
|
| 89 |
def get_api_key(self) -> str:
|
| 90 |
"""Get the API key for the configured provider."""
|
| 91 |
# Normalize provider for case-insensitive matching
|
|
|
|
| 42 |
)
|
| 43 |
|
| 44 |
# Embedding Configuration
|
| 45 |
+
# Note: OpenAI embeddings require OPENAI_API_KEY
|
| 46 |
openai_embedding_model: str = Field(
|
| 47 |
default="text-embedding-3-small",
|
| 48 |
description="OpenAI embedding model (used by LlamaIndex RAG)",
|
|
|
|
| 77 |
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
|
| 78 |
|
| 79 |
# External Services
|
|
|
|
|
|
|
| 80 |
chroma_db_path: str = Field(default="./chroma_db", description="ChromaDB storage path")
|
| 81 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
def get_api_key(self) -> str:
|
| 83 |
"""Get the API key for the configured provider."""
|
| 84 |
# Normalize provider for case-insensitive matching
|
src/utils/exceptions.py
CHANGED
|
@@ -49,12 +49,6 @@ class QuotaExceededError(LLMError):
|
|
| 49 |
pass
|
| 50 |
|
| 51 |
|
| 52 |
-
class ModalError(DeepBonerError):
|
| 53 |
-
"""Raised when Modal sandbox operations fail."""
|
| 54 |
-
|
| 55 |
-
pass
|
| 56 |
-
|
| 57 |
-
|
| 58 |
class SynthesisError(DeepBonerError):
|
| 59 |
"""Raised when report synthesis fails after trying all available models.
|
| 60 |
|
|
|
|
| 49 |
pass
|
| 50 |
|
| 51 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
class SynthesisError(DeepBonerError):
|
| 53 |
"""Raised when report synthesis fails after trying all available models.
|
| 54 |
|
src/utils/llm_factory.py
DELETED
|
@@ -1,64 +0,0 @@
|
|
| 1 |
-
"""Centralized LLM client factory.
|
| 2 |
-
|
| 3 |
-
This module provides factory functions for creating LLM clients.
|
| 4 |
-
DEPRECATED: Prefer src.clients.factory.get_chat_client() directly.
|
| 5 |
-
"""
|
| 6 |
-
|
| 7 |
-
from typing import Any
|
| 8 |
-
|
| 9 |
-
from src.clients.base import BaseChatClient
|
| 10 |
-
from src.clients.factory import get_chat_client
|
| 11 |
-
from src.utils.config import settings
|
| 12 |
-
from src.utils.exceptions import ConfigurationError
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
def get_magentic_client() -> BaseChatClient:
|
| 16 |
-
"""
|
| 17 |
-
Get the chat client for Magentic agents.
|
| 18 |
-
|
| 19 |
-
Now unified to support OpenAI, Gemini, and HuggingFace.
|
| 20 |
-
"""
|
| 21 |
-
return get_chat_client()
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
def get_pydantic_ai_model() -> Any:
|
| 25 |
-
"""
|
| 26 |
-
Get the appropriate model for pydantic-ai based on configuration.
|
| 27 |
-
Used by legacy Simple Mode components.
|
| 28 |
-
"""
|
| 29 |
-
from pydantic_ai.models.openai import OpenAIChatModel
|
| 30 |
-
from pydantic_ai.providers.openai import OpenAIProvider
|
| 31 |
-
|
| 32 |
-
# Normalize provider for case-insensitive matching
|
| 33 |
-
provider_lower = settings.llm_provider.lower() if settings.llm_provider else ""
|
| 34 |
-
|
| 35 |
-
if provider_lower == "openai":
|
| 36 |
-
if not settings.openai_api_key:
|
| 37 |
-
raise ConfigurationError("OPENAI_API_KEY not set for pydantic-ai")
|
| 38 |
-
provider = OpenAIProvider(api_key=settings.openai_api_key)
|
| 39 |
-
return OpenAIChatModel(settings.openai_model, provider=provider)
|
| 40 |
-
|
| 41 |
-
if provider_lower == "anthropic":
|
| 42 |
-
raise ConfigurationError("Anthropic is not supported (no embeddings API). See P3 doc.")
|
| 43 |
-
|
| 44 |
-
raise ConfigurationError(f"Unknown LLM provider for simple mode: {settings.llm_provider}")
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
def check_magentic_requirements() -> None:
|
| 48 |
-
"""
|
| 49 |
-
Check if Magentic mode requirements are met.
|
| 50 |
-
Now supports multiple providers via ChatClientFactory.
|
| 51 |
-
"""
|
| 52 |
-
# Advanced/Magentic mode now works with ANY provider (including free HF)
|
| 53 |
-
pass
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
def check_simple_mode_requirements() -> None:
|
| 57 |
-
"""
|
| 58 |
-
Check if simple mode requirements are met.
|
| 59 |
-
"""
|
| 60 |
-
if not settings.has_any_llm_key:
|
| 61 |
-
# Simple mode still requires explicit keys?
|
| 62 |
-
# Actually, simple mode also had HF support but it was brittle.
|
| 63 |
-
# We are deleting simple mode later, so let's leave this as is for now.
|
| 64 |
-
pass
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/utils/service_loader.py
CHANGED
|
@@ -18,7 +18,6 @@ from src.utils.config import settings
|
|
| 18 |
|
| 19 |
if TYPE_CHECKING:
|
| 20 |
from src.services.embedding_protocol import EmbeddingServiceProtocol
|
| 21 |
-
from src.services.statistical_analyzer import StatisticalAnalyzer
|
| 22 |
|
| 23 |
logger = structlog.get_logger()
|
| 24 |
|
|
@@ -66,13 +65,9 @@ def get_embedding_service(api_key: str | None = None) -> "EmbeddingServiceProtoc
|
|
| 66 |
ImportError: If no embedding service dependencies are available
|
| 67 |
"""
|
| 68 |
# Determine if we have a valid OpenAI key (BYOK or Env)
|
| 69 |
-
# Note: Must check sk-ant- BEFORE sk- since Anthropic keys start with sk-ant-
|
| 70 |
has_openai = False
|
| 71 |
if api_key:
|
| 72 |
-
if api_key.startswith("sk-
|
| 73 |
-
# Anthropic key - not supported for embeddings
|
| 74 |
-
logger.warning("Anthropic keys don't support embeddings, falling back to free tier")
|
| 75 |
-
elif api_key.startswith("sk-"):
|
| 76 |
# OpenAI BYOK
|
| 77 |
has_openai = True
|
| 78 |
elif settings.has_openai_key:
|
|
@@ -125,7 +120,7 @@ def get_embedding_service(api_key: str | None = None) -> "EmbeddingServiceProtoc
|
|
| 125 |
raise ImportError(
|
| 126 |
"No embedding service available. Install either:\n"
|
| 127 |
" - uv sync --extra embeddings (for local embeddings)\n"
|
| 128 |
-
" - uv sync --extra
|
| 129 |
) from e
|
| 130 |
|
| 131 |
|
|
@@ -157,29 +152,3 @@ def get_embedding_service_if_available(
|
|
| 157 |
error_type=type(e).__name__,
|
| 158 |
)
|
| 159 |
return None
|
| 160 |
-
|
| 161 |
-
|
| 162 |
-
def get_analyzer_if_available() -> "StatisticalAnalyzer | None":
|
| 163 |
-
"""Safely attempt to load and initialize the StatisticalAnalyzer.
|
| 164 |
-
|
| 165 |
-
Returns:
|
| 166 |
-
StatisticalAnalyzer instance if Modal is available, else None.
|
| 167 |
-
"""
|
| 168 |
-
try:
|
| 169 |
-
from src.services.statistical_analyzer import get_statistical_analyzer
|
| 170 |
-
|
| 171 |
-
analyzer = get_statistical_analyzer()
|
| 172 |
-
logger.info("StatisticalAnalyzer initialized successfully")
|
| 173 |
-
return analyzer
|
| 174 |
-
except ImportError as e:
|
| 175 |
-
logger.info(
|
| 176 |
-
"StatisticalAnalyzer not available (Modal dependencies missing)",
|
| 177 |
-
missing_dependency=str(e),
|
| 178 |
-
)
|
| 179 |
-
except Exception as e:
|
| 180 |
-
logger.warning(
|
| 181 |
-
"StatisticalAnalyzer initialization failed unexpectedly",
|
| 182 |
-
error=str(e),
|
| 183 |
-
error_type=type(e).__name__,
|
| 184 |
-
)
|
| 185 |
-
return None
|
|
|
|
| 18 |
|
| 19 |
if TYPE_CHECKING:
|
| 20 |
from src.services.embedding_protocol import EmbeddingServiceProtocol
|
|
|
|
| 21 |
|
| 22 |
logger = structlog.get_logger()
|
| 23 |
|
|
|
|
| 65 |
ImportError: If no embedding service dependencies are available
|
| 66 |
"""
|
| 67 |
# Determine if we have a valid OpenAI key (BYOK or Env)
|
|
|
|
| 68 |
has_openai = False
|
| 69 |
if api_key:
|
| 70 |
+
if api_key.startswith("sk-"):
|
|
|
|
|
|
|
|
|
|
| 71 |
# OpenAI BYOK
|
| 72 |
has_openai = True
|
| 73 |
elif settings.has_openai_key:
|
|
|
|
| 120 |
raise ImportError(
|
| 121 |
"No embedding service available. Install either:\n"
|
| 122 |
" - uv sync --extra embeddings (for local embeddings)\n"
|
| 123 |
+
" - uv sync --extra rag (for LlamaIndex with OpenAI)"
|
| 124 |
) from e
|
| 125 |
|
| 126 |
|
|
|
|
| 152 |
error_type=type(e).__name__,
|
| 153 |
)
|
| 154 |
return None
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/integration/test_modal.py
DELETED
|
@@ -1,67 +0,0 @@
|
|
| 1 |
-
"""Integration tests for Modal (requires credentials and modal package)."""
|
| 2 |
-
|
| 3 |
-
import pytest
|
| 4 |
-
|
| 5 |
-
from src.utils.config import settings
|
| 6 |
-
|
| 7 |
-
# Check if any LLM API key is available
|
| 8 |
-
_llm_available = bool(settings.openai_api_key or settings.anthropic_api_key)
|
| 9 |
-
|
| 10 |
-
# Check if modal package is installed
|
| 11 |
-
try:
|
| 12 |
-
import modal # noqa: F401
|
| 13 |
-
|
| 14 |
-
_modal_installed = True
|
| 15 |
-
except ImportError:
|
| 16 |
-
_modal_installed = False
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
@pytest.mark.integration
|
| 20 |
-
@pytest.mark.skipif(not _modal_installed, reason="Modal package not installed")
|
| 21 |
-
@pytest.mark.skipif(not settings.modal_available, reason="Modal credentials not configured")
|
| 22 |
-
class TestModalIntegration:
|
| 23 |
-
"""Integration tests requiring Modal credentials."""
|
| 24 |
-
|
| 25 |
-
@pytest.mark.asyncio
|
| 26 |
-
async def test_sandbox_executes_code(self) -> None:
|
| 27 |
-
"""Modal sandbox should execute Python code."""
|
| 28 |
-
import asyncio
|
| 29 |
-
from functools import partial
|
| 30 |
-
|
| 31 |
-
from src.tools.code_execution import get_code_executor
|
| 32 |
-
|
| 33 |
-
executor = get_code_executor()
|
| 34 |
-
code = "import pandas as pd; print(pd.DataFrame({'a': [1,2,3]})['a'].sum())"
|
| 35 |
-
|
| 36 |
-
loop = asyncio.get_running_loop()
|
| 37 |
-
result = await loop.run_in_executor(None, partial(executor.execute, code, timeout=30))
|
| 38 |
-
|
| 39 |
-
assert result["success"]
|
| 40 |
-
assert "6" in result["stdout"]
|
| 41 |
-
|
| 42 |
-
@pytest.mark.asyncio
|
| 43 |
-
@pytest.mark.skipif(not _llm_available, reason="LLM API key not configured")
|
| 44 |
-
async def test_statistical_analyzer_works(self) -> None:
|
| 45 |
-
"""StatisticalAnalyzer should work end-to-end (requires Modal + LLM)."""
|
| 46 |
-
from src.services.statistical_analyzer import get_statistical_analyzer
|
| 47 |
-
from src.utils.models import Citation, Evidence
|
| 48 |
-
|
| 49 |
-
evidence = [
|
| 50 |
-
Evidence(
|
| 51 |
-
content="Drug shows 40% improvement in trial.",
|
| 52 |
-
citation=Citation(
|
| 53 |
-
source="pubmed",
|
| 54 |
-
title="Test",
|
| 55 |
-
url="https://test.com",
|
| 56 |
-
date="2024-01-01",
|
| 57 |
-
authors=["Test"],
|
| 58 |
-
),
|
| 59 |
-
relevance=0.9,
|
| 60 |
-
)
|
| 61 |
-
]
|
| 62 |
-
|
| 63 |
-
analyzer = get_statistical_analyzer()
|
| 64 |
-
result = await analyzer.analyze("test drug efficacy", evidence)
|
| 65 |
-
|
| 66 |
-
assert result.verdict in ["SUPPORTED", "REFUTED", "INCONCLUSIVE"]
|
| 67 |
-
assert 0.0 <= result.confidence <= 1.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/unit/agent_factory/test_get_model_auto_detect.py
CHANGED
|
@@ -7,11 +7,7 @@ from src.utils.config import settings
|
|
| 7 |
|
| 8 |
|
| 9 |
class TestGetModelAutoDetect:
|
| 10 |
-
"""Test that get_model() auto-detects available providers.
|
| 11 |
-
|
| 12 |
-
NOTE: Anthropic is NOT supported (no embeddings API).
|
| 13 |
-
See P3_REMOVE_ANTHROPIC_PARTIAL_WIRING.md.
|
| 14 |
-
"""
|
| 15 |
|
| 16 |
def test_returns_openai_when_key_present(self, monkeypatch):
|
| 17 |
"""OpenAI key present → OpenAI model."""
|
|
@@ -30,16 +26,6 @@ class TestGetModelAutoDetect:
|
|
| 30 |
model = get_model(api_key="sk-byok-test-key")
|
| 31 |
assert isinstance(model, OpenAIChatModel)
|
| 32 |
|
| 33 |
-
def test_byok_anthropic_key_raises_not_implemented(self, monkeypatch):
|
| 34 |
-
"""BYOK: api_key='sk-ant-...' → NotImplementedError (Anthropic not supported)."""
|
| 35 |
-
monkeypatch.setattr(settings, "openai_api_key", None)
|
| 36 |
-
monkeypatch.setattr(settings, "hf_token", None)
|
| 37 |
-
|
| 38 |
-
with pytest.raises(NotImplementedError) as exc_info:
|
| 39 |
-
get_model(api_key="sk-ant-test-key")
|
| 40 |
-
|
| 41 |
-
assert "Anthropic is not supported" in str(exc_info.value)
|
| 42 |
-
|
| 43 |
def test_returns_huggingface_when_hf_token_present(self, monkeypatch):
|
| 44 |
"""HF_TOKEN present (no paid keys) → HuggingFace model."""
|
| 45 |
monkeypatch.setattr(settings, "openai_api_key", None)
|
|
@@ -57,7 +43,6 @@ class TestGetModelAutoDetect:
|
|
| 57 |
monkeypatch.delenv("HF_TOKEN", raising=False)
|
| 58 |
|
| 59 |
# Should raise clear error when no tokens available
|
| 60 |
-
import pytest
|
| 61 |
|
| 62 |
with pytest.raises(RuntimeError) as exc_info:
|
| 63 |
get_model()
|
|
|
|
| 7 |
|
| 8 |
|
| 9 |
class TestGetModelAutoDetect:
|
| 10 |
+
"""Test that get_model() auto-detects available providers."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 11 |
|
| 12 |
def test_returns_openai_when_key_present(self, monkeypatch):
|
| 13 |
"""OpenAI key present → OpenAI model."""
|
|
|
|
| 26 |
model = get_model(api_key="sk-byok-test-key")
|
| 27 |
assert isinstance(model, OpenAIChatModel)
|
| 28 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 29 |
def test_returns_huggingface_when_hf_token_present(self, monkeypatch):
|
| 30 |
"""HF_TOKEN present (no paid keys) → HuggingFace model."""
|
| 31 |
monkeypatch.setattr(settings, "openai_api_key", None)
|
|
|
|
| 43 |
monkeypatch.delenv("HF_TOKEN", raising=False)
|
| 44 |
|
| 45 |
# Should raise clear error when no tokens available
|
|
|
|
| 46 |
|
| 47 |
with pytest.raises(RuntimeError) as exc_info:
|
| 48 |
get_model()
|
tests/unit/agent_factory/test_judges_factory.py
CHANGED
|
@@ -1,8 +1,4 @@
|
|
| 1 |
-
"""Unit tests for Judge Factory and Model Selection.
|
| 2 |
-
|
| 3 |
-
NOTE: Anthropic is NOT supported (no embeddings API).
|
| 4 |
-
See P3_REMOVE_ANTHROPIC_PARTIAL_WIRING.md.
|
| 5 |
-
"""
|
| 6 |
|
| 7 |
from unittest.mock import patch
|
| 8 |
|
|
@@ -42,16 +38,6 @@ def test_get_model_byok_openai(mock_settings):
|
|
| 42 |
assert isinstance(model, OpenAIChatModel)
|
| 43 |
|
| 44 |
|
| 45 |
-
def test_get_model_byok_anthropic_raises(mock_settings):
|
| 46 |
-
"""Test that BYOK Anthropic key raises NotImplementedError."""
|
| 47 |
-
mock_settings.has_openai_key = False
|
| 48 |
-
|
| 49 |
-
with pytest.raises(NotImplementedError) as exc_info:
|
| 50 |
-
get_model(api_key="sk-ant-test")
|
| 51 |
-
|
| 52 |
-
assert "Anthropic is not supported" in str(exc_info.value)
|
| 53 |
-
|
| 54 |
-
|
| 55 |
def test_get_model_huggingface(mock_settings):
|
| 56 |
"""Test that HuggingFace model is returned when no paid keys."""
|
| 57 |
mock_settings.has_openai_key = False
|
|
|
|
| 1 |
+
"""Unit tests for Judge Factory and Model Selection."""
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
|
| 3 |
from unittest.mock import patch
|
| 4 |
|
|
|
|
| 38 |
assert isinstance(model, OpenAIChatModel)
|
| 39 |
|
| 40 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
def test_get_model_huggingface(mock_settings):
|
| 42 |
"""Test that HuggingFace model is returned when no paid keys."""
|
| 43 |
mock_settings.has_openai_key = False
|
tests/unit/orchestrators/test_advanced_p2_dead_zones.py
CHANGED
|
@@ -14,7 +14,6 @@ async def test_advanced_initialization_events():
|
|
| 14 |
patch("src.orchestrators.advanced.AdvancedOrchestrator._init_embedding_service"),
|
| 15 |
patch("src.orchestrators.advanced.init_magentic_state"),
|
| 16 |
patch("src.orchestrators.advanced.AdvancedOrchestrator._build_workflow") as mock_build,
|
| 17 |
-
patch("src.utils.llm_factory.check_magentic_requirements"),
|
| 18 |
): # Bypass check
|
| 19 |
# Setup mocks
|
| 20 |
mock_workflow = MagicMock()
|
|
|
|
| 14 |
patch("src.orchestrators.advanced.AdvancedOrchestrator._init_embedding_service"),
|
| 15 |
patch("src.orchestrators.advanced.init_magentic_state"),
|
| 16 |
patch("src.orchestrators.advanced.AdvancedOrchestrator._build_workflow") as mock_build,
|
|
|
|
| 17 |
): # Bypass check
|
| 18 |
# Setup mocks
|
| 19 |
mock_workflow = MagicMock()
|
tests/unit/services/test_statistical_analyzer.py
DELETED
|
@@ -1,104 +0,0 @@
|
|
| 1 |
-
"Unit tests for StatisticalAnalyzer service."
|
| 2 |
-
|
| 3 |
-
from unittest.mock import AsyncMock, MagicMock, patch
|
| 4 |
-
|
| 5 |
-
import pytest
|
| 6 |
-
|
| 7 |
-
from src.services.statistical_analyzer import (
|
| 8 |
-
AnalysisResult,
|
| 9 |
-
StatisticalAnalyzer,
|
| 10 |
-
get_statistical_analyzer,
|
| 11 |
-
)
|
| 12 |
-
from src.utils.models import Citation, Evidence
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
@pytest.fixture
|
| 16 |
-
def sample_evidence() -> list[Evidence]:
|
| 17 |
-
"""Sample evidence for testing."""
|
| 18 |
-
return [
|
| 19 |
-
Evidence(
|
| 20 |
-
content="Testosterone therapy shows effect size of 0.45.",
|
| 21 |
-
citation=Citation(
|
| 22 |
-
source="pubmed",
|
| 23 |
-
title="Testosterone HSDD Study",
|
| 24 |
-
url="https://pubmed.ncbi.nlm.nih.gov/12345/",
|
| 25 |
-
date="2024-01-15",
|
| 26 |
-
authors=["Smith J"],
|
| 27 |
-
),
|
| 28 |
-
relevance=0.9,
|
| 29 |
-
)
|
| 30 |
-
]
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
class TestStatisticalAnalyzer:
|
| 34 |
-
"""Tests for StatisticalAnalyzer (no agent_framework dependency)."""
|
| 35 |
-
|
| 36 |
-
def test_no_agent_framework_import(self) -> None:
|
| 37 |
-
"""StatisticalAnalyzer must NOT import agent_framework."""
|
| 38 |
-
import src.services.statistical_analyzer as module
|
| 39 |
-
|
| 40 |
-
# Check module doesn't import agent_framework
|
| 41 |
-
with open(module.__file__) as f:
|
| 42 |
-
source = f.read()
|
| 43 |
-
assert "from agent_framework" not in source
|
| 44 |
-
assert "import agent_framework" not in source
|
| 45 |
-
assert "BaseAgent" not in source
|
| 46 |
-
|
| 47 |
-
@pytest.mark.asyncio
|
| 48 |
-
async def test_analyze_returns_result(self, sample_evidence: list[Evidence]) -> None:
|
| 49 |
-
"""analyze() should return AnalysisResult."""
|
| 50 |
-
analyzer = StatisticalAnalyzer()
|
| 51 |
-
|
| 52 |
-
with (
|
| 53 |
-
patch.object(analyzer, "_get_agent") as mock_agent,
|
| 54 |
-
patch.object(analyzer, "_get_code_executor") as mock_executor,
|
| 55 |
-
):
|
| 56 |
-
# Mock LLM
|
| 57 |
-
mock_agent.return_value.run = AsyncMock(
|
| 58 |
-
return_value=MagicMock(output="print('SUPPORTED')")
|
| 59 |
-
)
|
| 60 |
-
|
| 61 |
-
# Mock Modal
|
| 62 |
-
mock_executor.return_value.execute.return_value = {
|
| 63 |
-
"stdout": "SUPPORTED\np-value: 0.01",
|
| 64 |
-
"stderr": "",
|
| 65 |
-
"success": True,
|
| 66 |
-
}
|
| 67 |
-
|
| 68 |
-
result = await analyzer.analyze("test query", sample_evidence)
|
| 69 |
-
|
| 70 |
-
assert isinstance(result, AnalysisResult)
|
| 71 |
-
assert result.verdict == "SUPPORTED"
|
| 72 |
-
|
| 73 |
-
def test_singleton(self) -> None:
|
| 74 |
-
"""get_statistical_analyzer should return singleton."""
|
| 75 |
-
a1 = get_statistical_analyzer()
|
| 76 |
-
a2 = get_statistical_analyzer()
|
| 77 |
-
assert a1 is a2
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
class TestAnalysisResult:
|
| 81 |
-
"""Tests for AnalysisResult model."""
|
| 82 |
-
|
| 83 |
-
def test_verdict_values(self) -> None:
|
| 84 |
-
"""Verdict should be one of the expected values."""
|
| 85 |
-
for verdict in ["SUPPORTED", "REFUTED", "INCONCLUSIVE"]:
|
| 86 |
-
result = AnalysisResult(
|
| 87 |
-
verdict=verdict, # type: ignore
|
| 88 |
-
confidence=0.8,
|
| 89 |
-
statistical_evidence="test",
|
| 90 |
-
code_generated="print('test')",
|
| 91 |
-
execution_output="test",
|
| 92 |
-
)
|
| 93 |
-
assert result.verdict == verdict
|
| 94 |
-
|
| 95 |
-
def test_confidence_bounds(self) -> None:
|
| 96 |
-
"""Confidence must be 0.0-1.0."""
|
| 97 |
-
with pytest.raises(ValueError):
|
| 98 |
-
AnalysisResult(
|
| 99 |
-
verdict="SUPPORTED",
|
| 100 |
-
confidence=1.5, # Invalid
|
| 101 |
-
statistical_evidence="test",
|
| 102 |
-
code_generated="test",
|
| 103 |
-
execution_output="test",
|
| 104 |
-
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
tests/unit/test_app_smoke.py
CHANGED
|
@@ -35,7 +35,6 @@ class TestAppSmoke:
|
|
| 35 |
Ensures the MCP server can expose these tools.
|
| 36 |
"""
|
| 37 |
from src.mcp_tools import (
|
| 38 |
-
analyze_hypothesis,
|
| 39 |
search_all_sources,
|
| 40 |
search_clinical_trials,
|
| 41 |
search_europepmc,
|
|
@@ -47,4 +46,3 @@ class TestAppSmoke:
|
|
| 47 |
assert callable(search_clinical_trials)
|
| 48 |
assert callable(search_europepmc)
|
| 49 |
assert callable(search_all_sources)
|
| 50 |
-
assert callable(analyze_hypothesis)
|
|
|
|
| 35 |
Ensures the MCP server can expose these tools.
|
| 36 |
"""
|
| 37 |
from src.mcp_tools import (
|
|
|
|
| 38 |
search_all_sources,
|
| 39 |
search_clinical_trials,
|
| 40 |
search_europepmc,
|
|
|
|
| 46 |
assert callable(search_clinical_trials)
|
| 47 |
assert callable(search_europepmc)
|
| 48 |
assert callable(search_all_sources)
|
|
|
tests/unit/utils/test_service_loader.py
CHANGED
|
@@ -1,36 +1,37 @@
|
|
| 1 |
from unittest.mock import MagicMock, patch
|
| 2 |
|
| 3 |
from src.utils.service_loader import (
|
| 4 |
-
get_analyzer_if_available,
|
| 5 |
get_embedding_service_if_available,
|
| 6 |
)
|
| 7 |
|
| 8 |
|
| 9 |
-
|
| 10 |
-
"""Test
|
| 11 |
-
mock_service = MagicMock()
|
| 12 |
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
|
|
|
|
|
|
|
|
|
|
| 21 |
|
| 22 |
-
def
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
|
| 35 |
|
| 36 |
def test_get_embedding_service_generic_error():
|
|
@@ -47,33 +48,97 @@ def test_get_embedding_service_generic_error():
|
|
| 47 |
assert service is None
|
| 48 |
|
| 49 |
|
| 50 |
-
|
| 51 |
-
"""Test
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
| 77 |
-
):
|
| 78 |
-
|
| 79 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
from unittest.mock import MagicMock, patch
|
| 2 |
|
| 3 |
from src.utils.service_loader import (
|
|
|
|
| 4 |
get_embedding_service_if_available,
|
| 5 |
)
|
| 6 |
|
| 7 |
|
| 8 |
+
class TestGetEmbeddingServiceIfAvailable:
|
| 9 |
+
"""Test get_embedding_service_if_available() safety wrapper."""
|
|
|
|
| 10 |
|
| 11 |
+
def test_returns_service_when_available(self):
|
| 12 |
+
"""Test successful loading of embedding service (free tier fallback)."""
|
| 13 |
+
mock_service = MagicMock()
|
| 14 |
|
| 15 |
+
# Patch settings to disable premium tier, then patch the local service
|
| 16 |
+
with patch("src.utils.service_loader.settings") as mock_settings:
|
| 17 |
+
mock_settings.has_openai_key = False
|
| 18 |
|
| 19 |
+
with patch("src.services.embeddings.get_embedding_service", return_value=mock_service):
|
| 20 |
+
service = get_embedding_service_if_available()
|
| 21 |
+
assert service is mock_service
|
| 22 |
|
| 23 |
+
def test_returns_none_when_no_service_available(self):
|
| 24 |
+
"""Test handling of ImportError when loading embedding service."""
|
| 25 |
+
# Disable premium tier, then make local service fail
|
| 26 |
+
with patch("src.utils.service_loader.settings") as mock_settings:
|
| 27 |
+
mock_settings.has_openai_key = False
|
| 28 |
|
| 29 |
+
with patch(
|
| 30 |
+
"src.services.embeddings.get_embedding_service",
|
| 31 |
+
side_effect=ImportError("Missing deps"),
|
| 32 |
+
):
|
| 33 |
+
service = get_embedding_service_if_available()
|
| 34 |
+
assert service is None
|
| 35 |
|
| 36 |
|
| 37 |
def test_get_embedding_service_generic_error():
|
|
|
|
| 48 |
assert service is None
|
| 49 |
|
| 50 |
|
| 51 |
+
class TestGetEmbeddingService:
|
| 52 |
+
"""Test get_embedding_service() logic."""
|
| 53 |
+
|
| 54 |
+
def test_uses_llamaindex_when_openai_key_present(self):
|
| 55 |
+
"""OpenAI key (env) → LlamaIndex."""
|
| 56 |
+
with patch("src.utils.service_loader.settings") as mock_settings:
|
| 57 |
+
mock_settings.has_openai_key = True
|
| 58 |
+
mock_settings.openai_api_key = "sk-env"
|
| 59 |
+
|
| 60 |
+
# Mock LlamaIndex dependencies and factory
|
| 61 |
+
with patch.dict(
|
| 62 |
+
"sys.modules",
|
| 63 |
+
{
|
| 64 |
+
"src.services.llamaindex_rag": MagicMock(),
|
| 65 |
+
"chromadb": MagicMock(),
|
| 66 |
+
"llama_index": MagicMock(),
|
| 67 |
+
},
|
| 68 |
+
):
|
| 69 |
+
mock_rag_service = MagicMock()
|
| 70 |
+
with patch(
|
| 71 |
+
"src.services.llamaindex_rag.get_rag_service", return_value=mock_rag_service
|
| 72 |
+
):
|
| 73 |
+
from src.utils.service_loader import get_embedding_service
|
| 74 |
+
|
| 75 |
+
service = get_embedding_service()
|
| 76 |
+
assert service is mock_rag_service
|
| 77 |
+
|
| 78 |
+
def test_uses_llamaindex_when_byok_key_present(self):
|
| 79 |
+
"""BYOK key → LlamaIndex."""
|
| 80 |
+
with patch("src.utils.service_loader.settings") as mock_settings:
|
| 81 |
+
mock_settings.has_openai_key = False
|
| 82 |
+
|
| 83 |
+
with patch.dict(
|
| 84 |
+
"sys.modules",
|
| 85 |
+
{
|
| 86 |
+
"src.services.llamaindex_rag": MagicMock(),
|
| 87 |
+
},
|
| 88 |
+
):
|
| 89 |
+
mock_rag_service = MagicMock()
|
| 90 |
+
with patch(
|
| 91 |
+
"src.services.llamaindex_rag.get_rag_service", return_value=mock_rag_service
|
| 92 |
+
):
|
| 93 |
+
from src.utils.service_loader import get_embedding_service
|
| 94 |
+
|
| 95 |
+
service = get_embedding_service(api_key="sk-test")
|
| 96 |
+
assert service is mock_rag_service
|
| 97 |
+
|
| 98 |
+
def test_falls_back_to_local_when_no_openai_key(self):
|
| 99 |
+
"""No OpenAI key → Local embeddings."""
|
| 100 |
+
with patch("src.utils.service_loader.settings") as mock_settings:
|
| 101 |
+
mock_settings.has_openai_key = False
|
| 102 |
+
|
| 103 |
+
mock_local_service = MagicMock()
|
| 104 |
+
with patch(
|
| 105 |
+
"src.services.embeddings.get_embedding_service", return_value=mock_local_service
|
| 106 |
+
):
|
| 107 |
+
from src.utils.service_loader import get_embedding_service
|
| 108 |
+
|
| 109 |
+
service = get_embedding_service()
|
| 110 |
+
assert service is mock_local_service
|
| 111 |
+
|
| 112 |
+
def test_falls_back_when_llamaindex_import_fails(self):
|
| 113 |
+
"""LlamaIndex fails import → Local embeddings."""
|
| 114 |
+
with patch("src.utils.service_loader.settings") as mock_settings:
|
| 115 |
+
mock_settings.has_openai_key = True
|
| 116 |
+
|
| 117 |
+
# Mock ImportError for LlamaIndex
|
| 118 |
+
with patch(
|
| 119 |
+
"src.services.llamaindex_rag.get_rag_service", side_effect=ImportError("No deps")
|
| 120 |
+
):
|
| 121 |
+
mock_local_service = MagicMock()
|
| 122 |
+
with patch(
|
| 123 |
+
"src.services.embeddings.get_embedding_service", return_value=mock_local_service
|
| 124 |
+
):
|
| 125 |
+
from src.utils.service_loader import get_embedding_service
|
| 126 |
+
|
| 127 |
+
service = get_embedding_service()
|
| 128 |
+
assert service is mock_local_service
|
| 129 |
+
|
| 130 |
+
def test_raises_when_no_embedding_service_available(self):
|
| 131 |
+
"""All services fail → ImportError."""
|
| 132 |
+
with patch("src.utils.service_loader.settings") as mock_settings:
|
| 133 |
+
mock_settings.has_openai_key = False
|
| 134 |
+
|
| 135 |
+
with patch(
|
| 136 |
+
"src.services.embeddings.get_embedding_service", side_effect=ImportError("No deps")
|
| 137 |
+
):
|
| 138 |
+
import pytest
|
| 139 |
+
|
| 140 |
+
from src.utils.service_loader import get_embedding_service
|
| 141 |
+
|
| 142 |
+
with pytest.raises(ImportError) as exc:
|
| 143 |
+
get_embedding_service()
|
| 144 |
+
assert "No embedding service available" in str(exc.value)
|
uv.lock
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
version = 1
|
| 2 |
revision = 1
|
| 3 |
-
requires-python = ">=3.11"
|
| 4 |
resolution-markers = [
|
| 5 |
"python_full_version >= '3.13'",
|
| 6 |
"python_full_version == '3.12.*'",
|
|
@@ -619,47 +619,6 @@ wheels = [
|
|
| 619 |
{ url = "https://files.pythonhosted.org/packages/e6/46/eb6eca305c77a4489affe1c5d8f4cae82f285d9addd8de4ec084a7184221/cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace", size = 11503 },
|
| 620 |
]
|
| 621 |
|
| 622 |
-
[[package]]
|
| 623 |
-
name = "cbor2"
|
| 624 |
-
version = "5.7.1"
|
| 625 |
-
source = { registry = "https://pypi.org/simple" }
|
| 626 |
-
sdist = { url = "https://files.pythonhosted.org/packages/a2/b8/c0f6a7d46f816cb18b1fda61a2fe648abe16039f1ff93ea720a6e9fb3cee/cbor2-5.7.1.tar.gz", hash = "sha256:7a405a1d7c8230ee9acf240aad48ae947ef584e8af05f169f3c1bde8f01f8b71", size = 102467 }
|
| 627 |
-
wheels = [
|
| 628 |
-
{ url = "https://files.pythonhosted.org/packages/52/67/319baac9c51de0053f58fa74a9548f93f3629aa3adeebd7d2c99d1379370/cbor2-5.7.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:2b1efbe6e82721be44b9faf47d0fd97b0150213eb6a4ba554f4947442bc4e13f", size = 67894 },
|
| 629 |
-
{ url = "https://files.pythonhosted.org/packages/2c/53/d23d0a234a4a098b019ac1cadd33631c973142fc947a68c4a38ca47aa5dc/cbor2-5.7.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:fb94bab27e00283bdd8f160e125e17dbabec4c9e6ffc8da91c36547ec1eb707f", size = 68444 },
|
| 630 |
-
{ url = "https://files.pythonhosted.org/packages/3a/a2/a6fa59e1c23b0bc77628d64153eb9fc69ac8dde5f8ed41a7d5316fcd0bcd/cbor2-5.7.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29f22266b5e08e0e4152e87ba185e04d3a84a4fd545b99ae3ebe42c658c66a53", size = 261600 },
|
| 631 |
-
{ url = "https://files.pythonhosted.org/packages/3d/cb/e0fa066aa7a09b15b8f56bafef6b2be19d9db31310310b0a5601af5c0128/cbor2-5.7.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:25d4c7554d6627da781c9bd1d0dd0709456eecb71f605829f98961bb98487dda", size = 254904 },
|
| 632 |
-
{ url = "https://files.pythonhosted.org/packages/2c/d5/b1fb4a3828c440e100a4b2658dd2e8f422faf08f4fcc8e2c92b240656b44/cbor2-5.7.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f1e15c3a08008cf13ce1dfc64d17c960df5d66d935788d28ec7df54bf0ffb0ef", size = 257388 },
|
| 633 |
-
{ url = "https://files.pythonhosted.org/packages/34/d5/252657bc5af964fc5f19c0e0e82031b4c32eba5d3ed4098e963e0e8c47a6/cbor2-5.7.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9f6cdf7eb604ea0e7ef34e3f0b5447da0029ecd3ab7b2dc70e43fa5f7bcfca89", size = 251494 },
|
| 634 |
-
{ url = "https://files.pythonhosted.org/packages/8a/3a/503ea4c2977411858ca287808d077fdb4bb1fafdb4b39177b8ce3d5619ac/cbor2-5.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:dd25cbef8e8e6dbf69f0de95311aecaca7217230cda83ae99fdc37cd20d99250", size = 68147 },
|
| 635 |
-
{ url = "https://files.pythonhosted.org/packages/49/9e/fe4c9703fd444da193f892787110c5da2a85c16d26917fcb2584f5d00077/cbor2-5.7.1-cp311-cp311-win_arm64.whl", hash = "sha256:40cc9c67242a7abac5a4e062bc4d1d2376979878c0565a4b2f08fd9ed9212945", size = 64126 },
|
| 636 |
-
{ url = "https://files.pythonhosted.org/packages/56/54/48426472f0c051982c647331441aed09b271a0500356ae0b7054c813d174/cbor2-5.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:bd5ca44891c06f6b85d440836c967187dc1d30b15f86f315d55c675d3a841078", size = 69031 },
|
| 637 |
-
{ url = "https://files.pythonhosted.org/packages/d3/68/1dd58c7706e9752188358223db58c83f3c48e07f728aa84221ffd244652f/cbor2-5.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:537d73ef930ccc1a7b6a2e8d2cbf81407d270deb18e40cda5eb511bd70f71078", size = 68825 },
|
| 638 |
-
{ url = "https://files.pythonhosted.org/packages/09/4e/380562fe9f9995a1875fb5ec26fd041e19d61f4630cb690a98c5195945fc/cbor2-5.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:edbf814dd7763b6eda27a5770199f6ccd55bd78be8f4367092460261bfbf19d0", size = 286222 },
|
| 639 |
-
{ url = "https://files.pythonhosted.org/packages/7c/bb/9eccdc1ea3c4d5c7cdb2e49b9de49534039616be5455ce69bd64c0b2efe2/cbor2-5.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fc81da8c0e09beb42923e455e477b36ff14a03b9ca18a8a2e9b462de9a953e8", size = 285688 },
|
| 640 |
-
{ url = "https://files.pythonhosted.org/packages/59/8c/4696d82f5bd04b3d45d9a64ec037fa242630c134e3218d6c252b4f59b909/cbor2-5.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e4a7d660d428911a3aadb7105e94438d7671ab977356fdf647a91aab751033bd", size = 277063 },
|
| 641 |
-
{ url = "https://files.pythonhosted.org/packages/95/50/6538e44ca970caaad2fa376b81701d073d84bf597aac07a59d0a253b1a7f/cbor2-5.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:228e0af9c0a9ddf6375b6ae010eaa1942a1901d403f134ac9ee6a76a322483f9", size = 278334 },
|
| 642 |
-
{ url = "https://files.pythonhosted.org/packages/64/a9/156ccd2207fb26b5b61d23728b4dbdc595d1600125aa79683a4a8ddc9313/cbor2-5.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:2d08a6c0d9ed778448e185508d870f4160ba74f59bb17a966abd0d14d0ff4dd3", size = 68404 },
|
| 643 |
-
{ url = "https://files.pythonhosted.org/packages/4f/49/adc53615e9dd32c4421f6935dfa2235013532c6e6b28ee515bbdd92618be/cbor2-5.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:752506cfe72da0f4014b468b30191470ee8919a64a0772bd3b36a4fccf5fcefc", size = 64047 },
|
| 644 |
-
{ url = "https://files.pythonhosted.org/packages/16/b1/51fb868fe38d893c570bb90b38d365ff0f00421402c1ae8f63b31b25d665/cbor2-5.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:59d5da59fffe89692d5bd1530eef4d26e4eb7aa794aaa1f4e192614786409009", size = 69068 },
|
| 645 |
-
{ url = "https://files.pythonhosted.org/packages/b9/db/5abc62ec456f552f617aac3359a5d7114b23be9c4d886169592cd5f074b9/cbor2-5.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:533117918d518e01348f8cd0331271c207e7224b9a1ed492a0ff00847f28edc8", size = 68927 },
|
| 646 |
-
{ url = "https://files.pythonhosted.org/packages/9a/c2/58d787395c99874d2a2395b3a22c9d48a3cfc5a7dcd5817bf74764998b75/cbor2-5.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8d6d9436ff3c3323ea5863ecf7ae1139590991685b44b9eb6b7bb1734a594af6", size = 285185 },
|
| 647 |
-
{ url = "https://files.pythonhosted.org/packages/d0/9c/b680b264a8f4b9aa59c95e166c816275a13138cbee92dd2917f58bca47b9/cbor2-5.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:661b871ca754a619fcd98c13a38b4696b2b57dab8b24235c00b0ba322c040d24", size = 284440 },
|
| 648 |
-
{ url = "https://files.pythonhosted.org/packages/1f/59/68183c655d6226d0eee10027f52516882837802a8d5746317a88362ed686/cbor2-5.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d8065aa90d715fd9bb28727b2d774ee16e695a0e1627ae76e54bf19f9d99d63f", size = 276876 },
|
| 649 |
-
{ url = "https://files.pythonhosted.org/packages/ee/a2/1964e0a569d2b81e8f4862753fee7701ae5773c22e45492a26f92f62e75a/cbor2-5.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cb1b7047d73590cfe8e373e2c804fa99be47e55b1b6186602d0f86f384cecec1", size = 278216 },
|
| 650 |
-
{ url = "https://files.pythonhosted.org/packages/00/78/9b566d68cb88bb1ecebe354765625161c9d6060a16e55008006d6359f776/cbor2-5.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:31d511df7ebd6624fdb4cecdafb4ffb9a205f9ff8c8d98edd1bef0d27f944d74", size = 68451 },
|
| 651 |
-
{ url = "https://files.pythonhosted.org/packages/db/85/7a6a922d147d027fd5d8fd5224b39e8eaf152a42e8cf16351458096d3d62/cbor2-5.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:f5d37f7b0f84394d2995bd8722cb01c86a885c4821a864a34b7b4d9950c5e26e", size = 64111 },
|
| 652 |
-
{ url = "https://files.pythonhosted.org/packages/5f/f0/f220222a57371e33434ba7bdc25de31d611cbc0ade2a868e03c3553305e7/cbor2-5.7.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:e5826e4fa4c33661960073f99cf67c82783895524fb66f3ebdd635c19b5a7d68", size = 69002 },
|
| 653 |
-
{ url = "https://files.pythonhosted.org/packages/c7/3c/34b62ba5173541659f248f005d13373530f02fb997b78fde00bf01ede4f4/cbor2-5.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:f19a00d6ac9a77cb611073250b06bf4494b41ba78a1716704f7008e0927d9366", size = 69177 },
|
| 654 |
-
{ url = "https://files.pythonhosted.org/packages/77/fd/2400d820d9733df00a5c18aa74201e51d710fb91588687eb594f4a7688ea/cbor2-5.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d2113aea044cd172f199da3520bc4401af69eae96c5180ca7eb660941928cb89", size = 284259 },
|
| 655 |
-
{ url = "https://files.pythonhosted.org/packages/42/65/280488ef196c1d71ba123cd406ea47727bb3a0e057767a733d9793fcc428/cbor2-5.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f17eacea2d28fecf28ac413c1d7927cde0a11957487d2630655d6b5c9c46a0b", size = 281958 },
|
| 656 |
-
{ url = "https://files.pythonhosted.org/packages/42/82/bcdd3fdc73bd5f4194fdb08c808112010add9530bae1dcfdb1e2b2ceae19/cbor2-5.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d65deea39cae533a629561e7da672402c46731122b6129ed7c8eaa1efe04efce", size = 276025 },
|
| 657 |
-
{ url = "https://files.pythonhosted.org/packages/ae/a8/a6065dd6a157b877d7d8f3fe96f410fb191a2db1e6588f4d20b5f9a507c2/cbor2-5.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:57d8cc29ec1fd20500748e0e767ff88c13afcee839081ba4478c41fcda6ee18b", size = 275978 },
|
| 658 |
-
{ url = "https://files.pythonhosted.org/packages/62/f4/37934045174af9e4253a340b43f07197af54002070cb80fae82d878f1f14/cbor2-5.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:94fb939d0946f80c49ba45105ca3a3e13e598fc9abd63efc6661b02d4b4d2c50", size = 70269 },
|
| 659 |
-
{ url = "https://files.pythonhosted.org/packages/0b/fd/933416643e7f5540ae818691fb23fa4189010c6efa39a12c4f59d825da28/cbor2-5.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4fd7225ac820bbb9f03bd16bc1a7efb6c4d1c451f22c0a153ff4ec46495c59c5", size = 66182 },
|
| 660 |
-
{ url = "https://files.pythonhosted.org/packages/d5/7d/383bafeabb54c17fe5b6d5aca4e863e6b7df10bcc833b34aa169e9dfce1a/cbor2-5.7.1-py3-none-any.whl", hash = "sha256:68834e4eff2f56629ce6422b0634bc3f74c5a4269de5363f5265fe452c706ba7", size = 23829 },
|
| 661 |
-
]
|
| 662 |
-
|
| 663 |
[[package]]
|
| 664 |
name = "certifi"
|
| 665 |
version = "2025.11.12"
|
|
@@ -1118,8 +1077,8 @@ name = "deepboner"
|
|
| 1118 |
version = "0.1.0"
|
| 1119 |
source = { editable = "." }
|
| 1120 |
dependencies = [
|
| 1121 |
-
{ name = "anthropic" },
|
| 1122 |
{ name = "beautifulsoup4" },
|
|
|
|
| 1123 |
{ name = "duckduckgo-search" },
|
| 1124 |
{ name = "gradio", extra = ["mcp"] },
|
| 1125 |
{ name = "httpx" },
|
|
@@ -1137,6 +1096,7 @@ dependencies = [
|
|
| 1137 |
{ name = "pydantic-settings" },
|
| 1138 |
{ name = "python-dotenv" },
|
| 1139 |
{ name = "requests" },
|
|
|
|
| 1140 |
{ name = "structlog" },
|
| 1141 |
{ name = "tenacity" },
|
| 1142 |
{ name = "urllib3" },
|
|
@@ -1158,30 +1118,22 @@ dev = [
|
|
| 1158 |
{ name = "ruff" },
|
| 1159 |
{ name = "typer" },
|
| 1160 |
]
|
| 1161 |
-
embeddings = [
|
| 1162 |
-
{ name = "chromadb" },
|
| 1163 |
-
{ name = "sentence-transformers" },
|
| 1164 |
-
]
|
| 1165 |
magentic = [
|
| 1166 |
{ name = "agent-framework-core" },
|
| 1167 |
]
|
| 1168 |
-
|
| 1169 |
-
{ name = "chromadb" },
|
| 1170 |
{ name = "llama-index" },
|
| 1171 |
{ name = "llama-index-embeddings-openai" },
|
| 1172 |
{ name = "llama-index-llms-openai" },
|
| 1173 |
{ name = "llama-index-vector-stores-chroma" },
|
| 1174 |
-
{ name = "modal" },
|
| 1175 |
]
|
| 1176 |
|
| 1177 |
[package.metadata]
|
| 1178 |
requires-dist = [
|
| 1179 |
{ name = "agent-framework-core", marker = "extra == 'magentic'", specifier = ">=1.0.0b251120,<2.0.0" },
|
| 1180 |
-
{ name = "anthropic", specifier = ">=0.18.0" },
|
| 1181 |
{ name = "bandit", marker = "extra == 'dev'", specifier = ">=1.7.0" },
|
| 1182 |
{ name = "beautifulsoup4", specifier = ">=4.12" },
|
| 1183 |
-
{ name = "chromadb",
|
| 1184 |
-
{ name = "chromadb", marker = "extra == 'modal'", specifier = ">=0.4.0" },
|
| 1185 |
{ name = "duckduckgo-search", specifier = ">=5.0" },
|
| 1186 |
{ name = "gradio", extras = ["mcp"], specifier = ">=6.0.0" },
|
| 1187 |
{ name = "httpx", specifier = ">=0.27" },
|
|
@@ -1192,12 +1144,11 @@ requires-dist = [
|
|
| 1192 |
{ name = "langgraph", specifier = ">=0.2.50,<1.0" },
|
| 1193 |
{ name = "langgraph-checkpoint-sqlite", specifier = ">=3.0.0,<4.0" },
|
| 1194 |
{ name = "limits", specifier = ">=3.0" },
|
| 1195 |
-
{ name = "llama-index", marker = "extra == '
|
| 1196 |
-
{ name = "llama-index-embeddings-openai", marker = "extra == '
|
| 1197 |
-
{ name = "llama-index-llms-openai", marker = "extra == '
|
| 1198 |
-
{ name = "llama-index-vector-stores-chroma", marker = "extra == '
|
| 1199 |
{ name = "mcp", specifier = ">=1.23.0" },
|
| 1200 |
-
{ name = "modal", marker = "extra == 'modal'", specifier = ">=0.63.0" },
|
| 1201 |
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10" },
|
| 1202 |
{ name = "openai", specifier = ">=1.0.0" },
|
| 1203 |
{ name = "pip-audit", marker = "extra == 'dev'", specifier = ">=2.7.0" },
|
|
@@ -1214,14 +1165,14 @@ requires-dist = [
|
|
| 1214 |
{ name = "requests", specifier = ">=2.32.5" },
|
| 1215 |
{ name = "respx", marker = "extra == 'dev'", specifier = ">=0.21" },
|
| 1216 |
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4.0" },
|
| 1217 |
-
{ name = "sentence-transformers",
|
| 1218 |
{ name = "structlog", specifier = ">=24.1" },
|
| 1219 |
{ name = "tenacity", specifier = ">=8.2" },
|
| 1220 |
{ name = "typer", marker = "extra == 'dev'", specifier = ">=0.9.0" },
|
| 1221 |
{ name = "urllib3", specifier = ">=2.5.0" },
|
| 1222 |
{ name = "xmltodict", specifier = ">=0.13" },
|
| 1223 |
]
|
| 1224 |
-
provides-extras = ["dev", "magentic", "
|
| 1225 |
|
| 1226 |
[[package]]
|
| 1227 |
name = "defusedxml"
|
|
@@ -1863,19 +1814,6 @@ wheels = [
|
|
| 1863 |
{ url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462 },
|
| 1864 |
]
|
| 1865 |
|
| 1866 |
-
[[package]]
|
| 1867 |
-
name = "grpclib"
|
| 1868 |
-
version = "0.4.8"
|
| 1869 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1870 |
-
dependencies = [
|
| 1871 |
-
{ name = "h2" },
|
| 1872 |
-
{ name = "multidict" },
|
| 1873 |
-
]
|
| 1874 |
-
sdist = { url = "https://files.pythonhosted.org/packages/19/75/0f0d3524b38b35e5cd07334b754aa9bd0570140ad982131b04ebfa3b0374/grpclib-0.4.8.tar.gz", hash = "sha256:d8823763780ef94fed8b2c562f7485cf0bbee15fc7d065a640673667f7719c9a", size = 62793 }
|
| 1875 |
-
wheels = [
|
| 1876 |
-
{ url = "https://files.pythonhosted.org/packages/03/8b/ad381ec1b8195fa4a9a693cb8087e031b99530c0d6b8ad036dcb99e144c4/grpclib-0.4.8-py3-none-any.whl", hash = "sha256:a5047733a7acc1c1cee6abf3c841c7c6fab67d2844a45a853b113fa2e6cd2654", size = 76311 },
|
| 1877 |
-
]
|
| 1878 |
-
|
| 1879 |
[[package]]
|
| 1880 |
name = "h11"
|
| 1881 |
version = "0.16.0"
|
|
@@ -1885,19 +1823,6 @@ wheels = [
|
|
| 1885 |
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
|
| 1886 |
]
|
| 1887 |
|
| 1888 |
-
[[package]]
|
| 1889 |
-
name = "h2"
|
| 1890 |
-
version = "4.3.0"
|
| 1891 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1892 |
-
dependencies = [
|
| 1893 |
-
{ name = "hpack" },
|
| 1894 |
-
{ name = "hyperframe" },
|
| 1895 |
-
]
|
| 1896 |
-
sdist = { url = "https://files.pythonhosted.org/packages/1d/17/afa56379f94ad0fe8defd37d6eb3f89a25404ffc71d4d848893d270325fc/h2-4.3.0.tar.gz", hash = "sha256:6c59efe4323fa18b47a632221a1888bd7fde6249819beda254aeca909f221bf1", size = 2152026 }
|
| 1897 |
-
wheels = [
|
| 1898 |
-
{ url = "https://files.pythonhosted.org/packages/69/b2/119f6e6dcbd96f9069ce9a2665e0146588dc9f88f29549711853645e736a/h2-4.3.0-py3-none-any.whl", hash = "sha256:c438f029a25f7945c69e0ccf0fb951dc3f73a5f6412981daee861431b70e2bdd", size = 61779 },
|
| 1899 |
-
]
|
| 1900 |
-
|
| 1901 |
[[package]]
|
| 1902 |
name = "hf-xet"
|
| 1903 |
version = "1.2.0"
|
|
@@ -1927,15 +1852,6 @@ wheels = [
|
|
| 1927 |
{ url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735 },
|
| 1928 |
]
|
| 1929 |
|
| 1930 |
-
[[package]]
|
| 1931 |
-
name = "hpack"
|
| 1932 |
-
version = "4.1.0"
|
| 1933 |
-
source = { registry = "https://pypi.org/simple" }
|
| 1934 |
-
sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276 }
|
| 1935 |
-
wheels = [
|
| 1936 |
-
{ url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357 },
|
| 1937 |
-
]
|
| 1938 |
-
|
| 1939 |
[[package]]
|
| 1940 |
name = "httpcore"
|
| 1941 |
version = "1.0.9"
|
|
@@ -2045,15 +1961,6 @@ wheels = [
|
|
| 2045 |
{ url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794 },
|
| 2046 |
]
|
| 2047 |
|
| 2048 |
-
[[package]]
|
| 2049 |
-
name = "hyperframe"
|
| 2050 |
-
version = "6.1.0"
|
| 2051 |
-
source = { registry = "https://pypi.org/simple" }
|
| 2052 |
-
sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566 }
|
| 2053 |
-
wheels = [
|
| 2054 |
-
{ url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007 },
|
| 2055 |
-
]
|
| 2056 |
-
|
| 2057 |
[[package]]
|
| 2058 |
name = "identify"
|
| 2059 |
version = "2.6.15"
|
|
@@ -3160,31 +3067,6 @@ wheels = [
|
|
| 3160 |
{ url = "https://files.pythonhosted.org/packages/6a/fc/0e61d9a4e29c8679356795a40e48f647b4aad58d71bfc969f0f8f56fb912/mmh3-5.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9", size = 40455 },
|
| 3161 |
]
|
| 3162 |
|
| 3163 |
-
[[package]]
|
| 3164 |
-
name = "modal"
|
| 3165 |
-
version = "1.2.4"
|
| 3166 |
-
source = { registry = "https://pypi.org/simple" }
|
| 3167 |
-
dependencies = [
|
| 3168 |
-
{ name = "aiohttp" },
|
| 3169 |
-
{ name = "cbor2" },
|
| 3170 |
-
{ name = "certifi" },
|
| 3171 |
-
{ name = "click" },
|
| 3172 |
-
{ name = "grpclib" },
|
| 3173 |
-
{ name = "protobuf" },
|
| 3174 |
-
{ name = "rich" },
|
| 3175 |
-
{ name = "synchronicity" },
|
| 3176 |
-
{ name = "toml" },
|
| 3177 |
-
{ name = "typer" },
|
| 3178 |
-
{ name = "types-certifi" },
|
| 3179 |
-
{ name = "types-toml" },
|
| 3180 |
-
{ name = "typing-extensions" },
|
| 3181 |
-
{ name = "watchfiles" },
|
| 3182 |
-
]
|
| 3183 |
-
sdist = { url = "https://files.pythonhosted.org/packages/91/b1/7bd589a3e1cc1ffc3fc2c05d1fab4b02459552d1ed416e00f19969e54f32/modal-1.2.4.tar.gz", hash = "sha256:5acb4a57a4bc857944579a3cf36e93f38d39499837628e9acf591d45d0c88c89", size = 645018 }
|
| 3184 |
-
wheels = [
|
| 3185 |
-
{ url = "https://files.pythonhosted.org/packages/ad/5a/a6bb9d01111109398bad8405587dde4f65088604b958c4f5e8cc5b212460/modal-1.2.4-py3-none-any.whl", hash = "sha256:cf4f01081bd9e5e1ec844d87a2c6a5805fd7c8f4deff5671c20d3b1505899aa8", size = 742291 },
|
| 3186 |
-
]
|
| 3187 |
-
|
| 3188 |
[[package]]
|
| 3189 |
name = "more-itertools"
|
| 3190 |
version = "10.8.0"
|
|
@@ -5919,18 +5801,6 @@ wheels = [
|
|
| 5919 |
{ url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353 },
|
| 5920 |
]
|
| 5921 |
|
| 5922 |
-
[[package]]
|
| 5923 |
-
name = "synchronicity"
|
| 5924 |
-
version = "0.10.4"
|
| 5925 |
-
source = { registry = "https://pypi.org/simple" }
|
| 5926 |
-
dependencies = [
|
| 5927 |
-
{ name = "typing-extensions" },
|
| 5928 |
-
]
|
| 5929 |
-
sdist = { url = "https://files.pythonhosted.org/packages/9e/92/2abaf9f4d846c2b7c240e9ce3c9198abf6660265bc1031640cbca5365351/synchronicity-0.10.4.tar.gz", hash = "sha256:3a9ac19f9a58cad64fcb3729812b828b77e54e0a90ced4439e09d3d9c19a90f0", size = 66903 }
|
| 5930 |
-
wheels = [
|
| 5931 |
-
{ url = "https://files.pythonhosted.org/packages/01/c6/a3631d119c9979816c0ed0354aa9fb829a14f53a43337f263dc3329b3a6e/synchronicity-0.10.4-py3-none-any.whl", hash = "sha256:0e3f00b2123cf2a77a8bb3b65fbeccad04adea682bfbd50c01637b75a168c73b", size = 39652 },
|
| 5932 |
-
]
|
| 5933 |
-
|
| 5934 |
[[package]]
|
| 5935 |
name = "temporalio"
|
| 5936 |
version = "1.19.0"
|
|
@@ -6239,15 +6109,6 @@ wheels = [
|
|
| 6239 |
{ url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028 },
|
| 6240 |
]
|
| 6241 |
|
| 6242 |
-
[[package]]
|
| 6243 |
-
name = "types-certifi"
|
| 6244 |
-
version = "2021.10.8.3"
|
| 6245 |
-
source = { registry = "https://pypi.org/simple" }
|
| 6246 |
-
sdist = { url = "https://files.pythonhosted.org/packages/52/68/943c3aeaf14624712a0357c4a67814dba5cea36d194f5c764dad7959a00c/types-certifi-2021.10.8.3.tar.gz", hash = "sha256:72cf7798d165bc0b76e1c10dd1ea3097c7063c42c21d664523b928e88b554a4f", size = 2095 }
|
| 6247 |
-
wheels = [
|
| 6248 |
-
{ url = "https://files.pythonhosted.org/packages/b5/63/2463d89481e811f007b0e1cd0a91e52e141b47f9de724d20db7b861dcfec/types_certifi-2021.10.8.3-py3-none-any.whl", hash = "sha256:b2d1e325e69f71f7c78e5943d410e650b4707bb0ef32e4ddf3da37f54176e88a", size = 2136 },
|
| 6249 |
-
]
|
| 6250 |
-
|
| 6251 |
[[package]]
|
| 6252 |
name = "types-protobuf"
|
| 6253 |
version = "6.32.1.20251105"
|
|
@@ -6269,15 +6130,6 @@ wheels = [
|
|
| 6269 |
{ url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658 },
|
| 6270 |
]
|
| 6271 |
|
| 6272 |
-
[[package]]
|
| 6273 |
-
name = "types-toml"
|
| 6274 |
-
version = "0.10.8.20240310"
|
| 6275 |
-
source = { registry = "https://pypi.org/simple" }
|
| 6276 |
-
sdist = { url = "https://files.pythonhosted.org/packages/86/47/3e4c75042792bff8e90d7991aa5c51812cc668828cc6cce711e97f63a607/types-toml-0.10.8.20240310.tar.gz", hash = "sha256:3d41501302972436a6b8b239c850b26689657e25281b48ff0ec06345b8830331", size = 4392 }
|
| 6277 |
-
wheels = [
|
| 6278 |
-
{ url = "https://files.pythonhosted.org/packages/da/a2/d32ab58c0b216912638b140ab2170ee4b8644067c293b170e19fba340ccc/types_toml-0.10.8.20240310-py3-none-any.whl", hash = "sha256:627b47775d25fa29977d9c70dc0cbab3f314f32c8d8d0c012f2ef5de7aaec05d", size = 4777 },
|
| 6279 |
-
]
|
| 6280 |
-
|
| 6281 |
[[package]]
|
| 6282 |
name = "typing-extensions"
|
| 6283 |
version = "4.15.0"
|
|
|
|
| 1 |
version = 1
|
| 2 |
revision = 1
|
| 3 |
+
requires-python = ">=3.11, <4.0"
|
| 4 |
resolution-markers = [
|
| 5 |
"python_full_version >= '3.13'",
|
| 6 |
"python_full_version == '3.12.*'",
|
|
|
|
| 619 |
{ url = "https://files.pythonhosted.org/packages/e6/46/eb6eca305c77a4489affe1c5d8f4cae82f285d9addd8de4ec084a7184221/cachetools-6.2.2-py3-none-any.whl", hash = "sha256:6c09c98183bf58560c97b2abfcedcbaf6a896a490f534b031b661d3723b45ace", size = 11503 },
|
| 620 |
]
|
| 621 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 622 |
[[package]]
|
| 623 |
name = "certifi"
|
| 624 |
version = "2025.11.12"
|
|
|
|
| 1077 |
version = "0.1.0"
|
| 1078 |
source = { editable = "." }
|
| 1079 |
dependencies = [
|
|
|
|
| 1080 |
{ name = "beautifulsoup4" },
|
| 1081 |
+
{ name = "chromadb" },
|
| 1082 |
{ name = "duckduckgo-search" },
|
| 1083 |
{ name = "gradio", extra = ["mcp"] },
|
| 1084 |
{ name = "httpx" },
|
|
|
|
| 1096 |
{ name = "pydantic-settings" },
|
| 1097 |
{ name = "python-dotenv" },
|
| 1098 |
{ name = "requests" },
|
| 1099 |
+
{ name = "sentence-transformers" },
|
| 1100 |
{ name = "structlog" },
|
| 1101 |
{ name = "tenacity" },
|
| 1102 |
{ name = "urllib3" },
|
|
|
|
| 1118 |
{ name = "ruff" },
|
| 1119 |
{ name = "typer" },
|
| 1120 |
]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1121 |
magentic = [
|
| 1122 |
{ name = "agent-framework-core" },
|
| 1123 |
]
|
| 1124 |
+
rag = [
|
|
|
|
| 1125 |
{ name = "llama-index" },
|
| 1126 |
{ name = "llama-index-embeddings-openai" },
|
| 1127 |
{ name = "llama-index-llms-openai" },
|
| 1128 |
{ name = "llama-index-vector-stores-chroma" },
|
|
|
|
| 1129 |
]
|
| 1130 |
|
| 1131 |
[package.metadata]
|
| 1132 |
requires-dist = [
|
| 1133 |
{ name = "agent-framework-core", marker = "extra == 'magentic'", specifier = ">=1.0.0b251120,<2.0.0" },
|
|
|
|
| 1134 |
{ name = "bandit", marker = "extra == 'dev'", specifier = ">=1.7.0" },
|
| 1135 |
{ name = "beautifulsoup4", specifier = ">=4.12" },
|
| 1136 |
+
{ name = "chromadb", specifier = ">=0.4.22" },
|
|
|
|
| 1137 |
{ name = "duckduckgo-search", specifier = ">=5.0" },
|
| 1138 |
{ name = "gradio", extras = ["mcp"], specifier = ">=6.0.0" },
|
| 1139 |
{ name = "httpx", specifier = ">=0.27" },
|
|
|
|
| 1144 |
{ name = "langgraph", specifier = ">=0.2.50,<1.0" },
|
| 1145 |
{ name = "langgraph-checkpoint-sqlite", specifier = ">=3.0.0,<4.0" },
|
| 1146 |
{ name = "limits", specifier = ">=3.0" },
|
| 1147 |
+
{ name = "llama-index", marker = "extra == 'rag'", specifier = ">=0.11.0" },
|
| 1148 |
+
{ name = "llama-index-embeddings-openai", marker = "extra == 'rag'" },
|
| 1149 |
+
{ name = "llama-index-llms-openai", marker = "extra == 'rag'" },
|
| 1150 |
+
{ name = "llama-index-vector-stores-chroma", marker = "extra == 'rag'" },
|
| 1151 |
{ name = "mcp", specifier = ">=1.23.0" },
|
|
|
|
| 1152 |
{ name = "mypy", marker = "extra == 'dev'", specifier = ">=1.10" },
|
| 1153 |
{ name = "openai", specifier = ">=1.0.0" },
|
| 1154 |
{ name = "pip-audit", marker = "extra == 'dev'", specifier = ">=2.7.0" },
|
|
|
|
| 1165 |
{ name = "requests", specifier = ">=2.32.5" },
|
| 1166 |
{ name = "respx", marker = "extra == 'dev'", specifier = ">=0.21" },
|
| 1167 |
{ name = "ruff", marker = "extra == 'dev'", specifier = ">=0.4.0" },
|
| 1168 |
+
{ name = "sentence-transformers", specifier = ">=2.2.2" },
|
| 1169 |
{ name = "structlog", specifier = ">=24.1" },
|
| 1170 |
{ name = "tenacity", specifier = ">=8.2" },
|
| 1171 |
{ name = "typer", marker = "extra == 'dev'", specifier = ">=0.9.0" },
|
| 1172 |
{ name = "urllib3", specifier = ">=2.5.0" },
|
| 1173 |
{ name = "xmltodict", specifier = ">=0.13" },
|
| 1174 |
]
|
| 1175 |
+
provides-extras = ["dev", "magentic", "rag"]
|
| 1176 |
|
| 1177 |
[[package]]
|
| 1178 |
name = "defusedxml"
|
|
|
|
| 1814 |
{ url = "https://files.pythonhosted.org/packages/19/41/0b430b01a2eb38ee887f88c1f07644a1df8e289353b78e82b37ef988fb64/grpcio-1.76.0-cp314-cp314-win_amd64.whl", hash = "sha256:922fa70ba549fce362d2e2871ab542082d66e2aaf0c19480ea453905b01f384e", size = 4834462 },
|
| 1815 |
]
|
| 1816 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1817 |
[[package]]
|
| 1818 |
name = "h11"
|
| 1819 |
version = "0.16.0"
|
|
|
|
| 1823 |
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
|
| 1824 |
]
|
| 1825 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1826 |
[[package]]
|
| 1827 |
name = "hf-xet"
|
| 1828 |
version = "1.2.0"
|
|
|
|
| 1852 |
{ url = "https://files.pythonhosted.org/packages/cb/44/870d44b30e1dcfb6a65932e3e1506c103a8a5aea9103c337e7a53180322c/hf_xet-1.2.0-cp37-abi3-win_amd64.whl", hash = "sha256:e6584a52253f72c9f52f9e549d5895ca7a471608495c4ecaa6cc73dba2b24d69", size = 2905735 },
|
| 1853 |
]
|
| 1854 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1855 |
[[package]]
|
| 1856 |
name = "httpcore"
|
| 1857 |
version = "1.0.9"
|
|
|
|
| 1961 |
{ url = "https://files.pythonhosted.org/packages/f0/0f/310fb31e39e2d734ccaa2c0fb981ee41f7bd5056ce9bc29b2248bd569169/humanfriendly-10.0-py2.py3-none-any.whl", hash = "sha256:1697e1a8a8f550fd43c2865cd84542fc175a61dcb779b6fee18cf6b6ccba1477", size = 86794 },
|
| 1962 |
]
|
| 1963 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1964 |
[[package]]
|
| 1965 |
name = "identify"
|
| 1966 |
version = "2.6.15"
|
|
|
|
| 3067 |
{ url = "https://files.pythonhosted.org/packages/6a/fc/0e61d9a4e29c8679356795a40e48f647b4aad58d71bfc969f0f8f56fb912/mmh3-5.2.0-cp314-cp314t-win_arm64.whl", hash = "sha256:e7884931fe5e788163e7b3c511614130c2c59feffdc21112290a194487efb2e9", size = 40455 },
|
| 3068 |
]
|
| 3069 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3070 |
[[package]]
|
| 3071 |
name = "more-itertools"
|
| 3072 |
version = "10.8.0"
|
|
|
|
| 5801 |
{ url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353 },
|
| 5802 |
]
|
| 5803 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5804 |
[[package]]
|
| 5805 |
name = "temporalio"
|
| 5806 |
version = "1.19.0"
|
|
|
|
| 6109 |
{ url = "https://files.pythonhosted.org/packages/78/64/7713ffe4b5983314e9d436a90d5bd4f63b6054e2aca783a3cfc44cb95bbf/typer-0.20.0-py3-none-any.whl", hash = "sha256:5b463df6793ec1dca6213a3cf4c0f03bc6e322ac5e16e13ddd622a889489784a", size = 47028 },
|
| 6110 |
]
|
| 6111 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6112 |
[[package]]
|
| 6113 |
name = "types-protobuf"
|
| 6114 |
version = "6.32.1.20251105"
|
|
|
|
| 6130 |
{ url = "https://files.pythonhosted.org/packages/2a/20/9a227ea57c1285986c4cf78400d0a91615d25b24e257fd9e2969606bdfae/types_requests-2.32.4.20250913-py3-none-any.whl", hash = "sha256:78c9c1fffebbe0fa487a418e0fa5252017e9c60d1a2da394077f1780f655d7e1", size = 20658 },
|
| 6131 |
]
|
| 6132 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6133 |
[[package]]
|
| 6134 |
name = "typing-extensions"
|
| 6135 |
version = "4.15.0"
|