VibecoderMcSwaggins commited on
Commit
069f0a0
·
1 Parent(s): 953b850

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 ADDED
@@ -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
.pre-commit-config.yaml ADDED
@@ -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]
.python-version ADDED
@@ -0,0 +1 @@
 
 
1
+ 3.11
main.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ def main():
2
+ print("Hello from deepcritical!")
3
+
4
+
5
+ if __name__ == "__main__":
6
+ main()
pyproject.toml ADDED
@@ -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
+ ]
reference_repos/README.md DELETED
@@ -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
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/__init__.py ADDED
File without changes
src/agent_factory/__init__.py ADDED
File without changes
src/prompts/__init__.py ADDED
File without changes
src/tools/__init__.py ADDED
File without changes
src/utils/__init__.py ADDED
File without changes
src/utils/config.py ADDED
@@ -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()
src/utils/exceptions.py ADDED
@@ -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
tests/__init__.py ADDED
File without changes
tests/conftest.py ADDED
@@ -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 [...]
tests/integration/__init__.py ADDED
File without changes
tests/unit/__init__.py ADDED
File without changes
tests/unit/agent_factory/__init__.py ADDED
File without changes
tests/unit/tools/__init__.py ADDED
File without changes
tests/unit/utils/__init__.py ADDED
File without changes
tests/unit/utils/test_config.py ADDED
@@ -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()
tests/unit/utils/test_exceptions.py ADDED
@@ -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)
uv.lock ADDED
The diff for this file is too large to render. See raw diff