Project Structure
Repository layout
jester-backend is a monorepo built as a uv workspace. apps/ holds the Lambdas (and the FastAPI
app); packages/ holds shared logic.
jester-backend/
โโโ apps/
โ โโโ odds_poc_app/ # FastAPI app (Backend API / App Lambda)
โ โโโ article_generation/ # Lambda: writes the 10-second race result summary (Python)
โ โโโ article_builder/ # Lambda: builds and stores article HTML (TypeScript / Node.js)
โ โโโ blog_generation/ # Lambda: writes race recap / prediction blogs (Python)
โ โโโ blog_rewriter/ # Lambda: rewrites past blogs (Python)
โ โโโ blog_builder/ # Lambda: builds and stores blog HTML (TypeScript / Node.js)
โ โโโ thumbnail_generation/ # Lambda: thumbnail generation (Python)
โโโ packages/ # Shared packages (see the Shared Packages page)
โ โโโ models/ # DocumentDB models (Beanie)
โ โโโ race_result_facts/ # Deterministic interpretation and checking of race results
โ โโโ race_analysis_agent/ # Combined race analysis agent
โ โโโ media_analysis_sdk/ # Bedrock media analysis SDK
โ โโโ article_generation_agent/
โ โโโ blog_generation_agent/
โ โโโ blog_parts/ # Block JSON, results table, race card
โ โโโ thumbnail_generation_agent/
โ โโโ ng_word_filter/
โ โโโ tone_of_voice/
โโโ bin/ # Operational scripts (bulk registration, uploads, โฆ)
โโโ scripts/ # Scraping and accuracy-verification scripts
โโโ data/ # Local working data
โโโ api-collections/ # API client collections
โโโ .github/workflows/ # Per-Lambda deploy workflows
โโโ pyproject.toml # uv workspace definition
โโโ ruff.toml / mypy.ini # Lint / type-check configuration
โโโ mise.toml # Tool version management
odds_poc_app (FastAPI)
apps/odds_poc_app/
โโโ pyproject.toml
โโโ src/
โโโ app.py # FastAPI app configuration and Mangum handler
โโโ config/env.py # pydantic-settings configuration
โโโ constants/pagination.py # Pagination defaults
โโโ libs/ # External service clients
โ โโโ bedrock.py # Embedding generation
โ โโโ documentdb.py # Beanie initialisation
โ โโโ s3.py # Uploads and presigned URLs
โ โโโ sqs.py # Job dispatch
โโโ middlewares/
โโโ repositories/ # Data access layer, one module per collection
โโโ routes/
โ โโโ v1/ # API v1 routes (article / blog / category / media / ng_word / old_blog / ping / race)
โโโ schemas/ # Pydantic request and response models
โโโ services/ # Business logic
โโโ utils/ # embedding / race_result / related_info
The entry point is src/app.py. When ENVIRONMENT is not local, Mangum adapts API Gateway events
to FastAPI. Routes are registered together by load_router() in routes/__init__.py under the /v1
prefix.
Python Lambdas
article_generation, blog_generation, blog_rewriter and thumbnail_generation are event-driven
and carry no HTTP routing.
apps/article_generation/ # e.g. the article generation Lambda
โโโ pyproject.toml
โโโ handler.py # Lambda entry point
โโโ docker/Dockerfile
โโโ src/
โโโ config/env.py # pydantic-settings configuration
โโโ schemas/event.py # SQS event payload models
โโโ services/ # Business logic
โโโ repositories/ # Data access layer
โโโ libs/ # External service clients
โโโ bedrock.py
โโโ documentdb.py
โโโ s3.py
โโโ sqs.py
โโโ sqs_retry.py # Decides whether a delivery is the final attempt
Responsibilities
| File | Role |
|---|---|
handler.py |
Defines lambda_handler(event, context). Only parses SQS records and calls services |
src/config/env.py |
Manages environment variables with pydantic-settings |
src/schemas/event.py |
Types and validates the SQS event payload with Pydantic |
src/services/ |
Business logic; called from handler.py, uses repositories and libs |
src/repositories/ |
DocumentDB access; called only from services |
src/libs/ |
Client initialisation and wrappers for Bedrock, S3, SQS and DocumentDB |
src/libs/sqs_retry.py |
Decides from ApproximateReceiveCount whether this delivery is the final attempt |
Shape of handler.py
import asyncio
from typing import Any
from apps.article_generation.src.libs import sqs_retry
from apps.article_generation.src.schemas.event import ArticleGenerationPayload
from apps.article_generation.src.services import article as article_service
def lambda_handler(event: dict[str, Any], context: object) -> None:
for record in event["Records"]:
payload = ArticleGenerationPayload.model_validate_json(record["body"])
asyncio.run(
article_service.generate(
payload,
discard_on_error=sqs_retry.is_final_attempt(record),
)
)
discard_on_error is True only on the final SQS attempt. On earlier attempts a failure does
nothing special and recovery is left to SQS redelivery.
Layering rules
handler.py
โโโบ services/ (business logic)
โโโบ repositories/ (database access)
โโโบ libs/ (external services: Bedrock / S3 / SQS)
โโโบ packages/ (shared logic: agents, fact checking, โฆ)
handler.pycalls onlyservicesservicescallsrepositories,libsandpackagesrepositoriesmay calllibsbut must not callservices- DocumentDB models from
packages/models/are used fromrepositories
TypeScript Lambdas
article_builder and blog_builder build HTML from React components, so they are written in
TypeScript (Node.js 22).
apps/blog_builder/
โโโ package.json
โโโ tsconfig.json
โโโ handler.ts # Lambda entry point
โโโ docker/Dockerfile
โโโ scripts/
โ โโโ esbuild-options.mjs # Bundle configuration (SCSS, icon allowlist, โฆ)
โ โโโ sync-oddspark.mjs # Sync from oddspark-static-pages
โโโ vendor/oddspark/ # Synced components, SCSS and assets (committed)
โโโ src/
โโโ article/ # Article templates and block rendering
โโโ config/env.ts
โโโ libs/
โ โโโ documentdb.ts # mongodb client with reconnect support
โ โโโ sqs-retry.ts # Final-attempt detection
โโโ repositories/
โโโ schemas/event.ts # zod validation of the SQS payload
โโโ services/
article_builder has the same shape but no vendor/; its summary component lives in
src/components/race-summary/.
odds_poc_app vs. the Lambdas
| Item | odds_poc_app (FastAPI) | Lambda apps |
|---|---|---|
| Entry point | src/app.py (FastAPI + Mangum) |
handler.py / handler.ts |
| Routing | routes/ layer |
None; the handler receives events directly |
| Event type | HTTP request (API Gateway) | SQS message |
| Response | HTTP response | None (sends to the next SQS queue or stores in the DB) |
| On failure | Returns an HTTP error | Depends on whether it is the final attempt (re-raise, or discard / record the failure) |
Tests
Tests live next to the package they cover as test_*.py and run with uv run python <path>.
| File | Covers |
|---|---|
apps/odds_poc_app/test_related_info.py |
Assembling the related-information section |
packages/article_generation_agent/test_placeholders.py |
Placeholder substitution |
packages/blog_generation_agent/test_main.py |
Regression tests for blog writing |
packages/race_result_facts/test_checks.py |
Fact checking |
packages/race_result_facts/test_prediction.py |
Handling of prediction data |
packages/race_result_facts/test_source_mix.py |
Measuring article provenance (race result vs. video) |
The TypeScript side (article_builder / blog_builder) is covered by npm run typecheck,
npm run lint, a successful build and visual verification.