Skip to content

article_generation Lambda Overview

Triggered by messages on the SQS article-generation queue, this Lambda produces a "10-second race result summary" (one headline plus three sentences) from the race result, the race video and โ€” for horse racing โ€” the course image, then hands it to the article_builder Lambda over the SQS article-builder queue.

This Lambda does not build the article HTML. Rendering and persistence (moving status from generating to draft) are article_builder's job.

See I/O definition for the schemas.


Trigger

Sender When
App Lambda (odds_poc_app) On an article generation request (POST /v1/articles/generation)
App Lambda (odds_poc_app) On a regeneration request for a failed article (POST /v1/articles/{article_id}/regeneration)

Two-stage generation

Generation is split into stage 1: overall race analysis and stage 2: article writing, because multimodal analysis and text writing call for different models.

Stage Package Model What it does
1 race_analysis_agent Amazon Nova Pro Analyses the race video, course image and race result together into a single summary
2 article_generation_agent Claude Sonnet (falls back to Nova Pro) Writes the 10-second summary, grounded in confirmed facts and the stage-1 summary

Stage 2 uses Claude Sonnet when BEDROCK_CLAUDE_SONNET_PROFILE_ARN is set, and falls back to Nova Pro otherwise. Stage 1 stays on Nova Pro, which supports video input.


Processing flow

  1. Receive the SQS event, parse and validate the payload
  2. Connect to DocumentDB (before generation, since failure recording also needs the DB)
  3. Check the target article is still status=generating; otherwise exit without doing anything (guards against redelivery for a discarded article)
  4. Check the race result has a finishing order (not void / cancelled); otherwise mark status=failed and exit
  5. Fetch media from S3
    • Race video {racing_type}/video/{race_id}.mp4 โ€” passed to Bedrock as an S3 URI (too large for the Converse API's 25 MB inline limit)
    • Course image horse_racing/course_image/{race_id}.gif (horse racing only) โ€” passed as inline bytes
    • Both are optional: missing media is treated as absent rather than failing generation
  6. Load NG words from DocumentDB and build the Aho-Corasick matcher
  7. Stage 1: analyse the race with race_analysis_agent
  8. Stage 2: write the article with article_generation_agent (subject to the guards below)
  9. Assemble the accuracy-verification data (accuracy_test_data)
  10. Send the result to the SQS article-builder queue
flowchart TD
    AppLambda([App Lambda]) -->|SQS| Start
    Start[Receive SQS trigger] --> Parse[Parse payload]
    Parse --> DB[Connect to DocumentDB]
    DB --> Guard[Check article is generating]
    Guard -->|not generating| Skip[Exit without work]
    Guard -->|OK| Void[Check finishing order]
    Void -->|none| Failed[Mark status=failed and exit]
    Void -->|OK| Fetch[Fetch video / course image from S3<br/>continue if missing]
    Fetch --> Analyze[Stage 1: race_analysis_agent]
    Analyze --> Write[Stage 2: article_generation_agent]
    Write -->|guards unresolved| Error[Raise]
    Write -->|success| SendSQS[Send to article-builder queue]
    SendSQS -.->|SQS| Builder([article_builder Lambda])
    Error -->|final attempt| MarkFailed[Mark status=failed with the reason]

Factual guards

Asking an LLM to transcribe values or perform multi-step matching produces mix-ups, so the division of labour is: the machine interprets, matches and transcribes; the LLM only writes the Japanese.

Guard What it does Owner
Eligibility Races with no finishing order (void / cancelled) are never generated race_result_facts.void_reason
Confirmed facts Lap-by-lap and corner-by-corner leaders are resolved down to "number + name" before being handed over race_result_facts.build_fact_block
Placeholder substitution Entrant "number + name", payout amounts, combinations and popularity are written as placeholders and filled in from the official data by code article_generation_agent.placeholders
Popularity correction Popularity written directly (without a placeholder), and "the Nth favourite finished Mth" mix-ups, are corrected to the official values race_result_facts.article_checks
Fact checking Number-to-name mapping and per-bet-type payouts are machine-checked against the official data race_result_facts.find_fact_errors
NG word screening The generated text is scanned for prohibited words (matches inside entrant/jockey names are excluded) ng_word_filter
Length Each sentence must be 60 characters or fewer article_generation_agent

When a guard trips, the findings are fed back into the prompt and the article is rewritten (up to twice). If it still cannot be resolved, an exception is raised โ€” articles that contradict the facts or contain prohibited words are never published. Length is the exception: it is not a factual problem, so an over-length sentence is logged but does not block publication.


Error handling

Situation Behaviour
Failure on a non-final attempt Re-raise, leaving recovery to SQS redelivery
Failure on the final attempt Mark the article as status=failed with the reason in generation_error, swallow the exception and finish

Whether a delivery is the final attempt is decided from the SQS record's ApproximateReceiveCount and SQS_MAX_RECEIVE_COUNT (default 2, kept in sync with the queue's redrive_policy.maxReceiveCount).

Why the final attempt is not left in the DLQ

Reprocessing the same input fails for the same reason, so the message has no value in the DLQ. Rebuilding is left to regeneration from the admin UI (POST /v1/articles/{article_id}/regeneration, with extra instructions). Since the exception is swallowed, the stack trace is logged for investigation.

Generation failures used to hard-delete the article, which made races with "generated but no article" indistinguishable in the list. They are now kept as failed with a reason, and act as the starting point for regeneration.


Bedrock retries

Transient Bedrock errors are retried in the application, since botocore does not retry them.

Error code Handling
ModelErrorException (corrupted ToolUse token sequence) Retry after a short delay
ModelTimeoutException / ServiceUnavailableException Retry after a short delay
ThrottlingException (TPM quota exceeded) Retry after a longer delay, enough for the one-minute window to recover

AWS services and libraries

Service / library Use
Amazon SQS Trigger (article-generation) and result dispatch (article-builder)
Amazon S3 Fetching the race video and course image
Amazon Bedrock Stage-1 analysis (Nova Pro) and stage-2 writing (Claude Sonnet / Nova Pro)
Amazon DocumentDB Loading NG words, checking article status, recording failures
LangGraph Building and running the stage-1 analysis agent (tool-calling loop)

DocumentDB access

This Lambda does not store the article body, but it does reach DocumentDB to load NG words and to record generation failures. Storing the body is article_builder's job.