Developer Guide¶
This guide covers everything you need to contribute to or extend Beyond Retrieval v2.
Development Setup¶
Backend (Bare-Metal)¶
cd beyond-retrieval-pythonv/backend
python -m venv venv
source venv/bin/activate # Linux / macOS
venv\Scripts\activate # Windows
pip install -r requirements.txt
cp ../.env.production.example ../.env
# Edit ../.env with your credentials
uvicorn main:app --reload --port 8000
Windows Hot-Reload
--reload does NOT detect new files on Windows. Restart uvicorn when adding new router/service/schema files.
Frontend¶
Docker (Recommended)¶
Code Patterns¶
Router → Service → Database¶
# Router — HTTP concerns only
@router.post("/{notebook_id}/process")
async def process(
notebook_id: str,
request: MyRequest,
supabase: Client = Depends(get_supabase),
user: dict = Depends(get_current_user),
):
await check_notebook_access(notebook_id, user)
result = await my_service.process(supabase, request)
return APIResponse(data=result.model_dump()) # MUST use envelope
# Service — business logic, receives Supabase client
class MyService:
async def process(self, supabase: Client, data: MyRequest) -> MyResponse:
result = supabase.table("my_table").select("*").eq("id", data.id).execute()
return MyResponse(**result.data[0])
my_service = MyService() # singleton
Dependency Injection¶
from dependencies import (
get_supabase, # Active client (cloud or local)
get_default_supabase, # Always cloud
get_current_user, # JWT verification or dev-user
check_notebook_access, # Returns "admin" or "chat_only"
require_admin, # Raises 403 if not admin
)
LLM Provider Factory¶
from utils.openai_client import (
get_llm_client, # AsyncOpenAI for chat
get_embedding_client, # (AsyncOpenAI | None, is_openrouter)
embed_ollama, # Ollama-specific embedding
normalize_embedding_model, # Provider-aware model ID normalization
)
Dynamic API Keys¶
from services.api_keys_service import get_effective_key
# Resolution: user DB key > server .env key > empty string
key = get_effective_key(supabase, user_id, "openrouter_api_key")
Testing¶
Running Tests¶
cd beyond-retrieval-pythonv/backend
pip install -r requirements-test.txt
pytest # Run all 1213+ tests
pytest -x # Stop on first failure
pytest tests/test_chat_service.py # Specific file
pytest -k "test_send_message" # Pattern match
pytest -v --tb=short # Verbose with short tracebacks
Test Organization (40+ files)¶
| Category | Key Files | Tests |
|---|---|---|
| Chat | test_chat_service.py, test_chat_endpoints.py | 169+ |
| Judge | test_judge_service.py | 106 |
| Documents | test_document_service.py | 200+ |
| Retrieval | test_retrieval.py | 50 |
| Enhancement | test_enhancement.py | 46 |
| OneDrive | test_onedrive.py | 43 |
| Auth | test_auth.py | 20 |
Mock Patterns¶
Lazy import mocking — Patch at the source module:
# CORRECT
@patch("config.get_settings")
def test_something(mock_settings): ...
# WRONG — does not intercept
@patch("services.chat_service.get_settings")
Supabase mock chain:
mock_supabase = MagicMock()
mock_supabase.table.return_value.select.return_value.eq.return_value.execute.return_value = (
MagicMock(data=[{"notebook_id": "nb1"}])
)
Adding a New Feature¶
1. Schema¶
# schemas/my_feature.py
class MyRequest(BaseModel):
notebook_id: str
query: str
class MyResponse(BaseModel):
result: str
2. Service¶
# services/my_feature_service.py
class MyFeatureService:
async def process(self, supabase, data):
result = supabase.table("t").select("*").eq("id", data.notebook_id).execute()
return MyResponse(result="done")
my_feature_service = MyFeatureService()
3. Router¶
# routers/my_feature.py
router = APIRouter(prefix="/api/my-feature", tags=["my-feature"])
@router.post("/{notebook_id}/process")
async def process(notebook_id: str, request: MyRequest, ...):
result = await my_feature_service.process(supabase, request)
return APIResponse(data=result.model_dump())
4. Register in main.py¶
5. Write Tests¶
class TestMyFeature:
def setup_method(self):
self.service = MyFeatureService()
self.mock_supabase = MagicMock()
@pytest.mark.asyncio
async def test_process(self):
self.mock_supabase.table.return_value.select.return_value.eq.return_value.execute.return_value = (
MagicMock(data=[{"id": "123"}])
)
result = await self.service.process(self.mock_supabase, MyRequest(...))
assert result.result == "done"
Database Migrations¶
- Primary schema:
db/migrations/001_initial_schema.sql - Local mode:
start_services.pycopies schema to init-scripts automatically - Cloud mode: Apply via Supabase SQL Editor
After ALTER TABLE, restart PostgREST:
Common Pitfalls¶
| Problem | Fix |
|---|---|
Frontend gets undefined | Wrap return in APIResponse(data=...) |
| Pydantic AI missing API key | Use explicit OpenAIModel(...) — never string form |
.env not loaded by libraries | pydantic-settings doesn't set os.environ — pass keys explicitly |
Supabase .single() breaks mocks | Use .execute() + [0] indexing |
| Supabase RPC returns 0 results | Pass plain dicts, not json.dumps() |
| Ollama structured output fails | Use plain-text fallback path |
.env changes not picked up | Use docker compose up -d, not restart |