Skip to content

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.py calls only services
  • services calls repositories, libs and packages
  • repositories may call libs but must not call services
  • DocumentDB models from packages/models/ are used from repositories

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.