Spaces:
Running
feat(phase1): Foundation & Tooling complete
Browse files* feat: initialize deepcritical project structure with core configuration and main functionality
- Added initial project files including `.python-version`, `pyproject.toml`, and main application logic in `main.py`.
- Implemented application configuration using Pydantic in `src/utils/config.py` with environment variable support.
- Created directory structure for source code and tests, including placeholders for agents, prompts, tools, and utilities.
- Added unit tests for configuration loading and validation in `tests/unit/utils/test_config.py`.
Review Score: 100/100 (Ironclad Gucci Banger Edition)
* feat(phase1): complete foundation with Python 3.11, tests, and tooling
- Fix Python version to 3.11 for HuggingFace Spaces compatibility
- Add config.py with pydantic-settings for typed configuration
- Add exceptions.py with custom exception hierarchy
- Add conftest.py with test fixtures (sample_evidence deferred to Phase 2)
- Add .env.example and .pre-commit-config.yaml
- Remove reference_repos from git tracking
- All 8 tests pass, ruff clean, mypy src clean
* fix(phase1): address Sourcery + CodeRabbit review feedback
- Use ConfigurationError instead of ValueError in get_api_key()
- Handle unknown LLM provider explicitly
- Wire up log_level in configure_logging()
- Add Anthropic provider tests (success + missing key)
- Add ConfigurationError hierarchy test
- Add catch-all base exception test
12 tests passing, ruff clean, mypy clean
- .env.example +13 -0
- .pre-commit-config.yaml +17 -0
- .python-version +1 -0
- main.py +6 -0
- pyproject.toml +111 -0
- reference_repos/README.md +0 -54
- src/__init__.py +0 -0
- src/agent_factory/__init__.py +0 -0
- src/prompts/__init__.py +0 -0
- src/tools/__init__.py +0 -0
- src/utils/__init__.py +0 -0
- src/utils/config.py +89 -0
- src/utils/exceptions.py +31 -0
- tests/__init__.py +0 -0
- tests/conftest.py +32 -0
- tests/integration/__init__.py +0 -0
- tests/unit/__init__.py +0 -0
- tests/unit/agent_factory/__init__.py +0 -0
- tests/unit/tools/__init__.py +0 -0
- tests/unit/utils/__init__.py +0 -0
- tests/unit/utils/test_config.py +60 -0
- tests/unit/utils/test_exceptions.py +33 -0
- uv.lock +0 -0
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# LLM Provider (choose one)
|
| 2 |
+
OPENAI_API_KEY=sk-your-key-here
|
| 3 |
+
ANTHROPIC_API_KEY=sk-ant-your-key-here
|
| 4 |
+
|
| 5 |
+
# Optional: PubMed API key (higher rate limits)
|
| 6 |
+
NCBI_API_KEY=your-ncbi-key-here
|
| 7 |
+
|
| 8 |
+
# Optional: For HuggingFace deployment
|
| 9 |
+
HF_TOKEN=hf_your-token-here
|
| 10 |
+
|
| 11 |
+
# Agent Config
|
| 12 |
+
MAX_ITERATIONS=10
|
| 13 |
+
LOG_LEVEL=INFO
|
|
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
repos:
|
| 2 |
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
| 3 |
+
rev: v0.4.4
|
| 4 |
+
hooks:
|
| 5 |
+
- id: ruff
|
| 6 |
+
args: [--fix]
|
| 7 |
+
- id: ruff-format
|
| 8 |
+
|
| 9 |
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
| 10 |
+
rev: v1.10.0
|
| 11 |
+
hooks:
|
| 12 |
+
- id: mypy
|
| 13 |
+
files: ^src/
|
| 14 |
+
additional_dependencies:
|
| 15 |
+
- pydantic>=2.7
|
| 16 |
+
- pydantic-settings>=2.2
|
| 17 |
+
args: [--ignore-missing-imports]
|
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
3.11
|
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
def main():
|
| 2 |
+
print("Hello from deepcritical!")
|
| 3 |
+
|
| 4 |
+
|
| 5 |
+
if __name__ == "__main__":
|
| 6 |
+
main()
|
|
@@ -0,0 +1,111 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[project]
|
| 2 |
+
name = "deepcritical"
|
| 3 |
+
version = "0.1.0"
|
| 4 |
+
description = "AI-Native Drug Repurposing Research Agent"
|
| 5 |
+
readme = "README.md"
|
| 6 |
+
requires-python = ">=3.11"
|
| 7 |
+
dependencies = [
|
| 8 |
+
# Core
|
| 9 |
+
"pydantic>=2.7",
|
| 10 |
+
"pydantic-settings>=2.2", # For BaseSettings (config)
|
| 11 |
+
"pydantic-ai>=0.0.16", # Agent framework
|
| 12 |
+
|
| 13 |
+
# HTTP & Parsing
|
| 14 |
+
"httpx>=0.27", # Async HTTP client
|
| 15 |
+
"beautifulsoup4>=4.12", # HTML parsing
|
| 16 |
+
"xmltodict>=0.13", # PubMed XML -> dict
|
| 17 |
+
|
| 18 |
+
# Search
|
| 19 |
+
"duckduckgo-search>=6.0", # Free web search
|
| 20 |
+
|
| 21 |
+
# UI
|
| 22 |
+
"gradio>=5.0", # Chat interface
|
| 23 |
+
|
| 24 |
+
# Utils
|
| 25 |
+
"python-dotenv>=1.0", # .env loading
|
| 26 |
+
"tenacity>=8.2", # Retry logic
|
| 27 |
+
"structlog>=24.1", # Structured logging
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
+
[project.optional-dependencies]
|
| 31 |
+
dev = [
|
| 32 |
+
# Testing
|
| 33 |
+
"pytest>=8.0",
|
| 34 |
+
"pytest-asyncio>=0.23",
|
| 35 |
+
"pytest-sugar>=1.0",
|
| 36 |
+
"pytest-cov>=5.0",
|
| 37 |
+
"pytest-mock>=3.12",
|
| 38 |
+
"respx>=0.21", # Mock httpx requests
|
| 39 |
+
|
| 40 |
+
# Quality
|
| 41 |
+
"ruff>=0.4.0",
|
| 42 |
+
"mypy>=1.10",
|
| 43 |
+
"pre-commit>=3.7",
|
| 44 |
+
]
|
| 45 |
+
|
| 46 |
+
[build-system]
|
| 47 |
+
requires = ["hatchling"]
|
| 48 |
+
build-backend = "hatchling.build"
|
| 49 |
+
|
| 50 |
+
[tool.hatch.build.targets.wheel]
|
| 51 |
+
packages = ["src"]
|
| 52 |
+
|
| 53 |
+
# ============== RUFF CONFIG ==============
|
| 54 |
+
[tool.ruff]
|
| 55 |
+
line-length = 100
|
| 56 |
+
target-version = "py311"
|
| 57 |
+
src = ["src", "tests"]
|
| 58 |
+
|
| 59 |
+
[tool.ruff.lint]
|
| 60 |
+
select = [
|
| 61 |
+
"E", # pycodestyle errors
|
| 62 |
+
"F", # pyflakes
|
| 63 |
+
"B", # flake8-bugbear
|
| 64 |
+
"I", # isort
|
| 65 |
+
"N", # pep8-naming
|
| 66 |
+
"UP", # pyupgrade
|
| 67 |
+
"PL", # pylint
|
| 68 |
+
"RUF", # ruff-specific
|
| 69 |
+
]
|
| 70 |
+
ignore = [
|
| 71 |
+
"PLR0913", # Too many arguments (agents need many params)
|
| 72 |
+
]
|
| 73 |
+
|
| 74 |
+
[tool.ruff.lint.isort]
|
| 75 |
+
known-first-party = ["src"]
|
| 76 |
+
|
| 77 |
+
# ============== MYPY CONFIG ==============
|
| 78 |
+
[tool.mypy]
|
| 79 |
+
python_version = "3.11"
|
| 80 |
+
strict = true
|
| 81 |
+
ignore_missing_imports = true
|
| 82 |
+
disallow_untyped_defs = true
|
| 83 |
+
warn_return_any = true
|
| 84 |
+
warn_unused_ignores = true
|
| 85 |
+
|
| 86 |
+
# ============== PYTEST CONFIG ==============
|
| 87 |
+
[tool.pytest.ini_options]
|
| 88 |
+
testpaths = ["tests"]
|
| 89 |
+
asyncio_mode = "auto"
|
| 90 |
+
addopts = [
|
| 91 |
+
"-v",
|
| 92 |
+
"--tb=short",
|
| 93 |
+
"--strict-markers",
|
| 94 |
+
]
|
| 95 |
+
markers = [
|
| 96 |
+
"unit: Unit tests (mocked)",
|
| 97 |
+
"integration: Integration tests (real APIs)",
|
| 98 |
+
"slow: Slow tests",
|
| 99 |
+
]
|
| 100 |
+
|
| 101 |
+
# ============== COVERAGE CONFIG ==============
|
| 102 |
+
[tool.coverage.run]
|
| 103 |
+
source = ["src"]
|
| 104 |
+
omit = ["*/__init__.py"]
|
| 105 |
+
|
| 106 |
+
[tool.coverage.report]
|
| 107 |
+
exclude_lines = [
|
| 108 |
+
"pragma: no cover",
|
| 109 |
+
"if TYPE_CHECKING:",
|
| 110 |
+
"raise NotImplementedError",
|
| 111 |
+
]
|
|
@@ -1,54 +0,0 @@
|
|
| 1 |
-
# Reference Repositories
|
| 2 |
-
|
| 3 |
-
This directory contains reference implementations that inform our architecture. These repos are **git-ignored** and should be cloned locally.
|
| 4 |
-
|
| 5 |
-
## Clone Commands
|
| 6 |
-
|
| 7 |
-
```bash
|
| 8 |
-
cd reference_repos
|
| 9 |
-
|
| 10 |
-
# PydanticAI Research Agent (Brave Search + Agent patterns)
|
| 11 |
-
git clone --depth 1 https://github.com/coleam00/PydanticAI-Research-Agent.git pydanticai-research-agent
|
| 12 |
-
rm -rf pydanticai-research-agent/.git
|
| 13 |
-
|
| 14 |
-
# PubMed MCP Server (Production-grade, TypeScript)
|
| 15 |
-
git clone --depth 1 https://github.com/cyanheads/pubmed-mcp-server.git pubmed-mcp-server
|
| 16 |
-
rm -rf pubmed-mcp-server/.git
|
| 17 |
-
|
| 18 |
-
# Microsoft AutoGen (Multi-agent orchestration)
|
| 19 |
-
git clone --depth 1 https://github.com/microsoft/autogen.git autogen-microsoft
|
| 20 |
-
rm -rf autogen-microsoft/.git
|
| 21 |
-
|
| 22 |
-
# Claude Agent SDK (Anthropic's agent framework)
|
| 23 |
-
git clone --depth 1 https://github.com/anthropics/claude-agent-sdk-python.git claude-agent-sdk
|
| 24 |
-
rm -rf claude-agent-sdk/.git
|
| 25 |
-
```
|
| 26 |
-
|
| 27 |
-
## What Each Repo Provides
|
| 28 |
-
|
| 29 |
-
| Repository | Key Patterns | Reference In Docs |
|
| 30 |
-
|------------|--------------|-------------------|
|
| 31 |
-
| **pydanticai-research-agent** | @agent.tool decorator, Brave Search, dependency injection | Section 16 |
|
| 32 |
-
| **pubmed-mcp-server** | PubMed E-utilities, MCP server patterns, research agent | Section 16 |
|
| 33 |
-
| **autogen-microsoft** | Multi-agent orchestration, reflect_on_tool_use | Sections 14, 15 |
|
| 34 |
-
| **claude-agent-sdk** | @tool decorator, hooks system, in-process MCP | Sections 14, 15 |
|
| 35 |
-
|
| 36 |
-
## Quick Reference Files
|
| 37 |
-
|
| 38 |
-
### PydanticAI Research Agent
|
| 39 |
-
- `agents/research_agent.py` - Agent with @agent.tool pattern
|
| 40 |
-
- `tools/brave_search.py` - Brave Search implementation
|
| 41 |
-
- `models/research_models.py` - Pydantic models
|
| 42 |
-
|
| 43 |
-
### PubMed MCP Server
|
| 44 |
-
- `src/mcp-server/tools/pubmedSearchArticles/` - PubMed search
|
| 45 |
-
- `src/mcp-server/tools/pubmedResearchAgent/` - Research orchestrator
|
| 46 |
-
- `src/services/NCBI/` - NCBI E-utilities client
|
| 47 |
-
|
| 48 |
-
### AutoGen
|
| 49 |
-
- `python/packages/autogen-agentchat/` - Agent patterns
|
| 50 |
-
- `python/packages/autogen-core/` - Core abstractions
|
| 51 |
-
|
| 52 |
-
### Claude Agent SDK
|
| 53 |
-
- `src/claude_agent_sdk/client.py` - SDK client
|
| 54 |
-
- `examples/mcp_calculator.py` - @tool decorator example
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,89 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Application configuration using Pydantic Settings."""
|
| 2 |
+
|
| 3 |
+
import logging
|
| 4 |
+
from typing import Literal
|
| 5 |
+
|
| 6 |
+
import structlog
|
| 7 |
+
from pydantic import Field
|
| 8 |
+
from pydantic_settings import BaseSettings, SettingsConfigDict
|
| 9 |
+
|
| 10 |
+
from src.utils.exceptions import ConfigurationError
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class Settings(BaseSettings):
|
| 14 |
+
"""Strongly-typed application settings."""
|
| 15 |
+
|
| 16 |
+
model_config = SettingsConfigDict(
|
| 17 |
+
env_file=".env",
|
| 18 |
+
env_file_encoding="utf-8",
|
| 19 |
+
case_sensitive=False,
|
| 20 |
+
extra="ignore",
|
| 21 |
+
)
|
| 22 |
+
|
| 23 |
+
# LLM Configuration
|
| 24 |
+
openai_api_key: str | None = Field(default=None, description="OpenAI API key")
|
| 25 |
+
anthropic_api_key: str | None = Field(default=None, description="Anthropic API key")
|
| 26 |
+
llm_provider: Literal["openai", "anthropic"] = Field(
|
| 27 |
+
default="openai", description="Which LLM provider to use"
|
| 28 |
+
)
|
| 29 |
+
openai_model: str = Field(default="gpt-4o", description="OpenAI model name")
|
| 30 |
+
anthropic_model: str = Field(
|
| 31 |
+
default="claude-3-5-sonnet-20241022", description="Anthropic model"
|
| 32 |
+
)
|
| 33 |
+
|
| 34 |
+
# PubMed Configuration
|
| 35 |
+
ncbi_api_key: str | None = Field(
|
| 36 |
+
default=None, description="NCBI API key for higher rate limits"
|
| 37 |
+
)
|
| 38 |
+
|
| 39 |
+
# Agent Configuration
|
| 40 |
+
max_iterations: int = Field(default=10, ge=1, le=50)
|
| 41 |
+
search_timeout: int = Field(default=30, description="Seconds to wait for search")
|
| 42 |
+
|
| 43 |
+
# Logging
|
| 44 |
+
log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO"
|
| 45 |
+
|
| 46 |
+
def get_api_key(self) -> str:
|
| 47 |
+
"""Get the API key for the configured provider."""
|
| 48 |
+
if self.llm_provider == "openai":
|
| 49 |
+
if not self.openai_api_key:
|
| 50 |
+
raise ConfigurationError("OPENAI_API_KEY not set")
|
| 51 |
+
return self.openai_api_key
|
| 52 |
+
|
| 53 |
+
if self.llm_provider == "anthropic":
|
| 54 |
+
if not self.anthropic_api_key:
|
| 55 |
+
raise ConfigurationError("ANTHROPIC_API_KEY not set")
|
| 56 |
+
return self.anthropic_api_key
|
| 57 |
+
|
| 58 |
+
raise ConfigurationError(f"Unknown LLM provider: {self.llm_provider}")
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
def get_settings() -> Settings:
|
| 62 |
+
"""Factory function to get settings (allows mocking in tests)."""
|
| 63 |
+
return Settings()
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
def configure_logging(settings: Settings) -> None:
|
| 67 |
+
"""Configure structured logging with the configured log level."""
|
| 68 |
+
# Set stdlib logging level from settings
|
| 69 |
+
logging.basicConfig(
|
| 70 |
+
level=getattr(logging, settings.log_level),
|
| 71 |
+
format="%(message)s",
|
| 72 |
+
)
|
| 73 |
+
|
| 74 |
+
structlog.configure(
|
| 75 |
+
processors=[
|
| 76 |
+
structlog.stdlib.filter_by_level,
|
| 77 |
+
structlog.stdlib.add_logger_name,
|
| 78 |
+
structlog.stdlib.add_log_level,
|
| 79 |
+
structlog.processors.TimeStamper(fmt="iso"),
|
| 80 |
+
structlog.processors.JSONRenderer(),
|
| 81 |
+
],
|
| 82 |
+
wrapper_class=structlog.stdlib.BoundLogger,
|
| 83 |
+
context_class=dict,
|
| 84 |
+
logger_factory=structlog.stdlib.LoggerFactory(),
|
| 85 |
+
)
|
| 86 |
+
|
| 87 |
+
|
| 88 |
+
# Singleton for easy import
|
| 89 |
+
settings = get_settings()
|
|
@@ -0,0 +1,31 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Custom exceptions for DeepCritical."""
|
| 2 |
+
|
| 3 |
+
|
| 4 |
+
class DeepCriticalError(Exception):
|
| 5 |
+
"""Base exception for all DeepCritical errors."""
|
| 6 |
+
|
| 7 |
+
pass
|
| 8 |
+
|
| 9 |
+
|
| 10 |
+
class SearchError(DeepCriticalError):
|
| 11 |
+
"""Raised when a search operation fails."""
|
| 12 |
+
|
| 13 |
+
pass
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
class JudgeError(DeepCriticalError):
|
| 17 |
+
"""Raised when the judge fails to assess evidence."""
|
| 18 |
+
|
| 19 |
+
pass
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
class ConfigurationError(DeepCriticalError):
|
| 23 |
+
"""Raised when configuration is invalid."""
|
| 24 |
+
|
| 25 |
+
pass
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
class RateLimitError(SearchError):
|
| 29 |
+
"""Raised when we hit API rate limits."""
|
| 30 |
+
|
| 31 |
+
pass
|
|
File without changes
|
|
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Shared pytest fixtures for all tests."""
|
| 2 |
+
|
| 3 |
+
from unittest.mock import AsyncMock
|
| 4 |
+
|
| 5 |
+
import pytest
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
@pytest.fixture
|
| 9 |
+
def mock_httpx_client(mocker):
|
| 10 |
+
"""Mock httpx.AsyncClient for API tests."""
|
| 11 |
+
mock = mocker.patch("httpx.AsyncClient")
|
| 12 |
+
mock.return_value.__aenter__ = AsyncMock(return_value=mock.return_value)
|
| 13 |
+
mock.return_value.__aexit__ = AsyncMock(return_value=None)
|
| 14 |
+
return mock
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
@pytest.fixture
|
| 18 |
+
def mock_llm_response():
|
| 19 |
+
"""Factory fixture for mocking LLM responses."""
|
| 20 |
+
|
| 21 |
+
def _mock(content: str):
|
| 22 |
+
return AsyncMock(return_value=content)
|
| 23 |
+
|
| 24 |
+
return _mock
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
# NOTE: sample_evidence fixture will be added in Phase 2 when models.py exists
|
| 28 |
+
# @pytest.fixture
|
| 29 |
+
# def sample_evidence():
|
| 30 |
+
# """Sample Evidence objects for testing."""
|
| 31 |
+
# from src.utils.models import Citation, Evidence
|
| 32 |
+
# return [...]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for configuration loading."""
|
| 2 |
+
|
| 3 |
+
import os
|
| 4 |
+
from unittest.mock import patch
|
| 5 |
+
|
| 6 |
+
import pytest
|
| 7 |
+
from pydantic import ValidationError
|
| 8 |
+
|
| 9 |
+
from src.utils.config import Settings
|
| 10 |
+
from src.utils.exceptions import ConfigurationError
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
class TestSettings:
|
| 14 |
+
"""Tests for Settings class."""
|
| 15 |
+
|
| 16 |
+
def test_default_max_iterations(self):
|
| 17 |
+
"""Settings should have default max_iterations of 10."""
|
| 18 |
+
with patch.dict(os.environ, {}, clear=True):
|
| 19 |
+
settings = Settings()
|
| 20 |
+
assert settings.max_iterations == 10 # noqa: PLR2004
|
| 21 |
+
|
| 22 |
+
def test_max_iterations_from_env(self):
|
| 23 |
+
"""Settings should read MAX_ITERATIONS from env."""
|
| 24 |
+
with patch.dict(os.environ, {"MAX_ITERATIONS": "25"}):
|
| 25 |
+
settings = Settings()
|
| 26 |
+
assert settings.max_iterations == 25 # noqa: PLR2004
|
| 27 |
+
|
| 28 |
+
def test_invalid_max_iterations_raises(self):
|
| 29 |
+
"""Settings should reject invalid max_iterations."""
|
| 30 |
+
with patch.dict(os.environ, {"MAX_ITERATIONS": "100"}):
|
| 31 |
+
with pytest.raises(ValidationError):
|
| 32 |
+
Settings() # 100 > 50 (max)
|
| 33 |
+
|
| 34 |
+
def test_get_api_key_openai(self):
|
| 35 |
+
"""get_api_key should return OpenAI key when provider is openai."""
|
| 36 |
+
with patch.dict(os.environ, {"LLM_PROVIDER": "openai", "OPENAI_API_KEY": "sk-test-key"}):
|
| 37 |
+
settings = Settings()
|
| 38 |
+
assert settings.get_api_key() == "sk-test-key"
|
| 39 |
+
|
| 40 |
+
def test_get_api_key_openai_missing_raises(self):
|
| 41 |
+
"""get_api_key should raise ConfigurationError when OpenAI key is not set."""
|
| 42 |
+
with patch.dict(os.environ, {"LLM_PROVIDER": "openai"}, clear=True):
|
| 43 |
+
settings = Settings()
|
| 44 |
+
with pytest.raises(ConfigurationError, match="OPENAI_API_KEY not set"):
|
| 45 |
+
settings.get_api_key()
|
| 46 |
+
|
| 47 |
+
def test_get_api_key_anthropic(self):
|
| 48 |
+
"""get_api_key should return Anthropic key when provider is anthropic."""
|
| 49 |
+
with patch.dict(
|
| 50 |
+
os.environ, {"LLM_PROVIDER": "anthropic", "ANTHROPIC_API_KEY": "sk-ant-test-key"}
|
| 51 |
+
):
|
| 52 |
+
settings = Settings()
|
| 53 |
+
assert settings.get_api_key() == "sk-ant-test-key"
|
| 54 |
+
|
| 55 |
+
def test_get_api_key_anthropic_missing_raises(self):
|
| 56 |
+
"""get_api_key should raise ConfigurationError when Anthropic key is not set."""
|
| 57 |
+
with patch.dict(os.environ, {"LLM_PROVIDER": "anthropic"}, clear=True):
|
| 58 |
+
settings = Settings()
|
| 59 |
+
with pytest.raises(ConfigurationError, match="ANTHROPIC_API_KEY not set"):
|
| 60 |
+
settings.get_api_key()
|
|
@@ -0,0 +1,33 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Unit tests for custom exceptions."""
|
| 2 |
+
|
| 3 |
+
from src.utils.exceptions import (
|
| 4 |
+
ConfigurationError,
|
| 5 |
+
DeepCriticalError,
|
| 6 |
+
JudgeError,
|
| 7 |
+
RateLimitError,
|
| 8 |
+
SearchError,
|
| 9 |
+
)
|
| 10 |
+
|
| 11 |
+
|
| 12 |
+
class TestExceptions:
|
| 13 |
+
"""Tests for exception hierarchy."""
|
| 14 |
+
|
| 15 |
+
def test_search_error_is_deepcritical_error(self):
|
| 16 |
+
assert issubclass(SearchError, DeepCriticalError)
|
| 17 |
+
|
| 18 |
+
def test_rate_limit_error_is_search_error(self):
|
| 19 |
+
assert issubclass(RateLimitError, SearchError)
|
| 20 |
+
|
| 21 |
+
def test_judge_error_is_deepcritical_error(self):
|
| 22 |
+
assert issubclass(JudgeError, DeepCriticalError)
|
| 23 |
+
|
| 24 |
+
def test_configuration_error_is_deepcritical_error(self):
|
| 25 |
+
assert issubclass(ConfigurationError, DeepCriticalError)
|
| 26 |
+
|
| 27 |
+
def test_subclass_caught_as_base(self):
|
| 28 |
+
"""Verify subclasses can be caught via DeepCriticalError."""
|
| 29 |
+
try:
|
| 30 |
+
raise RateLimitError("rate limited")
|
| 31 |
+
except DeepCriticalError as exc:
|
| 32 |
+
assert isinstance(exc, RateLimitError)
|
| 33 |
+
assert isinstance(exc, DeepCriticalError)
|
|
The diff for this file is too large to render.
See raw diff
|
|
|