Setup, conventions, and workflows for contributors.
git clone <repo>
cd agentic-rag-api
uv sync --group dev # installs runtime + dev tools (ruff, black, interrogate)
cp .env.example .env # fill in local values
docker-compose up -d # start API + worker + PostgreSQL + RedisThe API is available at http://localhost:8000. Interactive docs at http://localhost:8000/docs.
app/
├── main.py # app factory, middleware, exception handlers, lifespan
├── api/
│ ├── dependencies.py # JWT decode + role guards
│ └── v1/endpoints/ # thin transport handlers only (request parsing + response mapping)
├── services/ # business logic layer used by endpoints
│ ├── auth_service.py
│ ├── companies_service.py
│ ├── users_service.py
│ ├── documents_service.py
│ └── chat_service.py
├── agent/
│ ├── graph.py # LangGraph StateGraph (compiled once at startup)
│ └── tools/ # search_documents
├── core/
│ ├── config.py # all settings (Pydantic BaseSettings)
│ ├── logger.py # JSON structured logging
│ ├── exceptions.py # AppException hierarchy + global handlers
│ └── security.py # JWT + bcrypt utilities
├── db/
│ ├── session.py # async engine, session factory
│ └── models/ # SQLAlchemy ORM models (one file per model)
├── schemas/ # Pydantic request/response models
├── storage/ # pluggable file storage (local + S3)
└── worker/
├── celery_app.py # Celery instance
└── tasks.py # process_pdf_task
All quality gates run in CI. Run them locally before pushing:
uv run ruff check . # lint (pycodestyle, pyflakes, isort, pyupgrade, bugbear)
uv run ruff check . --fix # auto-fix all fixable violations
uv run black --check . # verify formatting
uv run python scripts/lint_thin_endpoints.py # enforce thin endpoint imports
uv run interrogate app/ # docstring coverage (minimum 80%)
uv run vulture app scripts --min-confidence 70 # dead-code scan (production code only)
uv run pip-audit --ignore-vuln CVE-2025-3000 # dependency vulnerability scan
uv run bandit -r app -q -x app/schemas # static security analysisConfiguration lives in pyproject.toml under [tool.ruff], [tool.black],
[tool.interrogate], and [tool.vulture].
- Endpoint modules in
app/api/v1/endpoints/must stay thin and avoid business logic. - Put domain/business logic in
app/services/modules. - Endpoints should delegate to services and only handle HTTP-level concerns (routing, dependency injection, status codes, response models).
120 characters maximum — enforced by black and checked by ruff. This applies to code, docstrings, and inline comments.
Google style with text starting on a new line after """:
def my_function(arg1: str, arg2: int) -> bool:
"""
Short one-line summary.
Extended description if needed. Keep lines under 120 chars.
Args:
arg1: Description of arg1.
arg2: Description of arg2.
Returns:
True if successful, False otherwise.
Raises:
ValueError: If arg1 is empty.
"""Single-line docstrings stay on one line:
def simple() -> str:
"""Return the configured value."""Managed by ruff (I rules). Groups in order:
- Standard library
- Third-party packages
- First-party (
app.*)
Run ruff check app/ --fix to auto-sort.
- Use
X | Yunion syntax (Python 3.10+), notOptional[X]orUnion[X, Y]. - Use
list[X]anddict[K, V](lowercase), notList[X]orDict[K, V]. - Use
(str, enum.Enum)for SQLAlchemy string enums in this project.
Expensive objects must be initialised once per process:
# CORRECT — module-level singleton
embeddings_model = HuggingFaceEmbeddings(model_name=settings.EMBEDDING_MODEL)
# CORRECT — lazy singleton (use when the module is imported by a process
# that never actually calls the function, to avoid wasted memory)
_model: SomeModel | None = None
def _get_model() -> SomeModel:
global _model
if _model is None:
_model = SomeModel(...)
return _model
# WRONG — instantiates on every request/task
@router.post("/endpoint")
async def handler():
model = SomeModel(...) # re-creates every request!See ARCHITECTURE.md for the full singleton table.
- Create or update the router file in
app/api/v1/endpoints/. - Use the appropriate dependency:
require_admin,require_super_admin, orrequire_company_user. - Add a Pydantic request schema in
app/schemas/if needed. - Register the router in
app/main.py. - Add a row to the API endpoints table in README.md.
- Add logging at appropriate levels (see existing endpoints for patterns).
- Create
app/storage/your_backend.pyimplementingStorageBackendfromapp/storage/base.py. - Add the new option to
DOCUMENT_STORAGEinapp/core/config.py(extend theLiteral). - Update the
get_storage()factory inapp/storage/__init__.py. - Add validation in
Settings._validate_cloud_storageif credentials are required.
Alembic manages the schema. Always create a new migration after changing a model:
# After editing an ORM model
alembic revision --autogenerate -m "short description"
alembic upgrade headReview the generated migration file before committing — autogenerate sometimes misses rename operations or vector column changes.
alembic downgrade -1 # roll back one step
alembic current # show active revisionuv run pytest -qTests live in tests/ and currently include:
- Unit tests for
app/services/*with mocked async session behavior. - Endpoint integration tests for role/scoping behavior using FastAPI dependency overrides.
These tests do not require a dedicated test database.
Always use the module-level logger:
from app.core.logger import get_logger
logger = get_logger(__name__)
logger.info("Operation succeeded", extra={"doc_id": doc.id, "company_id": company_id})
logger.warning("Access denied", extra={"user_id": user.id, "reason": "wrong company"})
logger.error("Unexpected failure", exc_info=exc)- Use
INFOfor normal operations. - Use
WARNINGfor expected failures (bad credentials, 403, 404). - Use
ERRORfor unexpected failures that degrade functionality. - Use
CRITICALfor startup failures or total service loss. - Never log passwords, tokens, or PII.
- Always include relevant IDs (
user_id,doc_id,company_id) asextrafields so logs are filterable.