# Phase 1 Implementation Spec: Foundation & Tooling **Goal**: Establish a "Gucci Banger" development environment using 2025 best practices. **Philosophy**: "If the build isn't solid, the agent won't be." **Estimated Effort**: 2-3 hours --- ## 1. Prerequisites Before starting, ensure these are installed: ```bash # Install uv (Rust-based package manager) curl -LsSf https://astral.sh/uv/install.sh | sh # Verify uv --version # Should be >= 0.4.0 ``` --- ## 2. Project Initialization ```bash # From project root uv init --name deepcritical uv python install 3.11 # Pin Python version ``` --- ## 3. The Tooling Stack (Exact Dependencies) ### `pyproject.toml` (Complete, Copy-Paste Ready) ```toml [project] name = "deepcritical" version = "0.1.0" description = "AI-Native Drug Repurposing Research Agent" readme = "README.md" requires-python = ">=3.11" dependencies = [ # Core "pydantic>=2.7", "pydantic-settings>=2.2", # For BaseSettings (config) "pydantic-ai>=0.0.16", # Agent framework # HTTP & Parsing "httpx>=0.27", # Async HTTP client "beautifulsoup4>=4.12", # HTML parsing "xmltodict>=0.13", # PubMed XML -> dict # Search "duckduckgo-search>=6.0", # Free web search # UI "gradio>=5.0", # Chat interface # Utils "python-dotenv>=1.0", # .env loading "tenacity>=8.2", # Retry logic "structlog>=24.1", # Structured logging ] [project.optional-dependencies] dev = [ # Testing "pytest>=8.0", "pytest-asyncio>=0.23", "pytest-sugar>=1.0", "pytest-cov>=5.0", "pytest-mock>=3.12", "respx>=0.21", # Mock httpx requests # Quality "ruff>=0.4.0", "mypy>=1.10", "pre-commit>=3.7", ] [build-system] requires = ["hatchling"] build-backend = "hatchling.build" [tool.hatch.build.targets.wheel] packages = ["src"] # ============== RUFF CONFIG ============== [tool.ruff] line-length = 100 target-version = "py311" src = ["src", "tests"] [tool.ruff.lint] select = [ "E", # pycodestyle errors "F", # pyflakes "B", # flake8-bugbear "I", # isort "N", # pep8-naming "UP", # pyupgrade "PL", # pylint "RUF", # ruff-specific ] ignore = [ "PLR0913", # Too many arguments (agents need many params) ] [tool.ruff.lint.isort] known-first-party = ["src"] # ============== MYPY CONFIG ============== [tool.mypy] python_version = "3.11" strict = true ignore_missing_imports = true disallow_untyped_defs = true warn_return_any = true warn_unused_ignores = true # ============== PYTEST CONFIG ============== [tool.pytest.ini_options] testpaths = ["tests"] asyncio_mode = "auto" addopts = [ "-v", "--tb=short", "--strict-markers", ] markers = [ "unit: Unit tests (mocked)", "integration: Integration tests (real APIs)", "slow: Slow tests", ] # ============== COVERAGE CONFIG ============== [tool.coverage.run] source = ["src"] omit = ["*/__init__.py"] [tool.coverage.report] exclude_lines = [ "pragma: no cover", "if TYPE_CHECKING:", "raise NotImplementedError", ] ``` --- ## 4. Directory Structure (Using Maintainer's Template) The maintainer already created empty scaffolding. We just need to add `__init__.py` files and tests. ```bash # The following folders already exist (from maintainer): # src/agent_factory/, src/tools/, src/utils/, src/prompts/, # src/middleware/, src/database_services/, src/retrieval_factory/ # Create __init__.py files (required for imports) touch src/__init__.py touch src/agent_factory/__init__.py touch src/tools/__init__.py touch src/utils/__init__.py touch src/prompts/__init__.py # Create test directories mkdir -p tests/unit/utils mkdir -p tests/unit/tools mkdir -p tests/unit/agent_factory mkdir -p tests/integration # Create test __init__.py files touch tests/__init__.py touch tests/unit/__init__.py touch tests/unit/utils/__init__.py touch tests/unit/tools/__init__.py touch tests/unit/agent_factory/__init__.py touch tests/integration/__init__.py ``` --- ## 5. Configuration Files ### `.env.example` (Copy to `.env` and fill) ```bash # LLM Provider (choose one) OPENAI_API_KEY=sk-your-key-here ANTHROPIC_API_KEY=sk-ant-your-key-here # Optional: For HuggingFace deployment HF_TOKEN=hf_your-token-here # Agent Config MAX_ITERATIONS=10 LOG_LEVEL=INFO ``` ### `.pre-commit-config.yaml` ```yaml repos: - repo: https://github.com/astral-sh/ruff-pre-commit rev: v0.4.4 hooks: - id: ruff args: [--fix] - id: ruff-format - repo: https://github.com/pre-commit/mirrors-mypy rev: v1.10.0 hooks: - id: mypy additional_dependencies: - pydantic>=2.7 - pydantic-settings>=2.2 args: [--ignore-missing-imports] ``` ### `tests/conftest.py` (Pytest Fixtures) ```python """Shared pytest fixtures for all tests.""" import pytest from unittest.mock import AsyncMock @pytest.fixture def mock_httpx_client(mocker): """Mock httpx.AsyncClient for API tests.""" mock = mocker.patch("httpx.AsyncClient") mock.return_value.__aenter__ = AsyncMock(return_value=mock.return_value) mock.return_value.__aexit__ = AsyncMock(return_value=None) return mock @pytest.fixture def mock_llm_response(): """Factory fixture for mocking LLM responses.""" def _mock(content: str): return AsyncMock(return_value=content) return _mock @pytest.fixture def sample_evidence(): """Sample Evidence objects for testing.""" from src.utils.models import Evidence, Citation return [ Evidence( content="Metformin shows promise in Alzheimer's...", citation=Citation( source="pubmed", title="Metformin and Alzheimer's Disease", url="https://pubmed.ncbi.nlm.nih.gov/12345678/", date="2024-01-15" ), relevance=0.85 ) ] ``` --- ## 6. Shared Kernel Implementation ### `src/utils/config.py` ```python """Application configuration using Pydantic Settings.""" from pydantic_settings import BaseSettings, SettingsConfigDict from pydantic import Field from typing import Literal import structlog class Settings(BaseSettings): """Strongly-typed application settings.""" model_config = SettingsConfigDict( env_file=".env", env_file_encoding="utf-8", case_sensitive=False, extra="ignore", ) # LLM Configuration openai_api_key: str | None = Field(default=None, description="OpenAI API key") anthropic_api_key: str | None = Field(default=None, description="Anthropic API key") llm_provider: Literal["openai", "anthropic"] = Field( default="openai", description="Which LLM provider to use" ) llm_model: str = Field( default="gpt-4o-mini", description="Model name to use" ) # Agent Configuration max_iterations: int = Field(default=10, ge=1, le=50) search_timeout: int = Field(default=30, description="Seconds to wait for search") # Logging log_level: Literal["DEBUG", "INFO", "WARNING", "ERROR"] = "INFO" def get_api_key(self) -> str: """Get the API key for the configured provider.""" if self.llm_provider == "openai": if not self.openai_api_key: raise ValueError("OPENAI_API_KEY not set") return self.openai_api_key else: if not self.anthropic_api_key: raise ValueError("ANTHROPIC_API_KEY not set") return self.anthropic_api_key def get_settings() -> Settings: """Factory function to get settings (allows mocking in tests).""" return Settings() def configure_logging(settings: Settings) -> None: """Configure structured logging.""" structlog.configure( processors=[ structlog.stdlib.filter_by_level, structlog.stdlib.add_logger_name, structlog.stdlib.add_log_level, structlog.processors.TimeStamper(fmt="iso"), structlog.processors.JSONRenderer(), ], wrapper_class=structlog.stdlib.BoundLogger, context_class=dict, logger_factory=structlog.stdlib.LoggerFactory(), ) # Singleton for easy import settings = get_settings() ``` ### `src/utils/exceptions.py` ```python """Custom exceptions for DeepCritical.""" class DeepCriticalError(Exception): """Base exception for all DeepCritical errors.""" pass class SearchError(DeepCriticalError): """Raised when a search operation fails.""" pass class JudgeError(DeepCriticalError): """Raised when the judge fails to assess evidence.""" pass class ConfigurationError(DeepCriticalError): """Raised when configuration is invalid.""" pass class RateLimitError(SearchError): """Raised when we hit API rate limits.""" pass ``` --- ## 7. TDD Workflow: First Test ### `tests/unit/utils/test_config.py` ```python """Unit tests for configuration loading.""" import pytest from unittest.mock import patch import os class TestSettings: """Tests for Settings class.""" def test_default_max_iterations(self): """Settings should have default max_iterations of 10.""" from src.utils.config import Settings # Clear any env vars with patch.dict(os.environ, {}, clear=True): settings = Settings() assert settings.max_iterations == 10 def test_max_iterations_from_env(self): """Settings should read MAX_ITERATIONS from env.""" from src.utils.config import Settings with patch.dict(os.environ, {"MAX_ITERATIONS": "25"}): settings = Settings() assert settings.max_iterations == 25 def test_invalid_max_iterations_raises(self): """Settings should reject invalid max_iterations.""" from src.utils.config import Settings from pydantic import ValidationError with patch.dict(os.environ, {"MAX_ITERATIONS": "100"}): with pytest.raises(ValidationError): Settings() # 100 > 50 (max) def test_get_api_key_openai(self): """get_api_key should return OpenAI key when provider is openai.""" from src.utils.config import Settings with patch.dict(os.environ, { "LLM_PROVIDER": "openai", "OPENAI_API_KEY": "sk-test-key" }): settings = Settings() assert settings.get_api_key() == "sk-test-key" def test_get_api_key_missing_raises(self): """get_api_key should raise when key is not set.""" from src.utils.config import Settings with patch.dict(os.environ, {"LLM_PROVIDER": "openai"}, clear=True): settings = Settings() with pytest.raises(ValueError, match="OPENAI_API_KEY not set"): settings.get_api_key() ``` --- ## 8. Execution Commands ```bash # Install all dependencies uv sync --all-extras # Run tests (should pass after implementing config.py) uv run pytest tests/unit/utils/test_config.py -v # Run full test suite with coverage uv run pytest --cov=src --cov-report=term-missing # Run linting uv run ruff check src tests uv run ruff format src tests # Run type checking uv run mypy src # Set up pre-commit hooks uv run pre-commit install ``` --- ## 9. Implementation Checklist - [ ] Install `uv` and verify version - [ ] Run `uv init --name deepcritical` - [ ] Create `pyproject.toml` (copy from above) - [ ] Create `__init__.py` files and test directories (run touch/mkdir commands) - [ ] Create `.env.example` and `.env` - [ ] Create `.pre-commit-config.yaml` - [ ] Create `tests/conftest.py` - [ ] Implement `src/utils/config.py` - [ ] Implement `src/utils/exceptions.py` - [ ] Write tests in `tests/unit/utils/test_config.py` - [ ] Run `uv sync --all-extras` - [ ] Run `uv run pytest` — **ALL TESTS MUST PASS** - [ ] Run `uv run ruff check` — **NO ERRORS** - [ ] Run `uv run mypy src` — **NO ERRORS** - [ ] Run `uv run pre-commit install` - [ ] Commit: `git commit -m "feat: phase 1 foundation complete"` --- ## 10. Definition of Done Phase 1 is **COMPLETE** when: 1. ✅ `uv run pytest` passes with 100% of tests green 2. ✅ `uv run ruff check src tests` has 0 errors 3. ✅ `uv run mypy src` has 0 errors 4. ✅ Pre-commit hooks are installed and working 5. ✅ `from src.utils.config import settings` works in Python REPL **Proceed to Phase 2 ONLY after all checkboxes are complete.**