Skip to content

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

cd beyond-retrieval-pythonv/frontend
npm install
npm run dev
cd beyond-retrieval-pythonv
cp .env.production.example .env
python start_services.py dev --build

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

from routers.my_feature import router as my_feature_router
app.include_router(my_feature_router)

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.py copies schema to init-scripts automatically
  • Cloud mode: Apply via Supabase SQL Editor

After ALTER TABLE, restart PostgREST:

docker compose restart supabase-rest

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