プロジェクト構成
リポジトリ全体
jester-backend は uv のワークスペースで構成されたモノレポです。
apps/ に Lambda(および FastAPI アプリ)、packages/ に共有ロジックを置きます。
jester-backend/
├── apps/
│ ├── odds_poc_app/ # FastAPI アプリ(Backend API / App Lambda)
│ ├── article_generation/ # Lambda: レース結果10秒サマリーの生成(Python)
│ ├── article_builder/ # Lambda: 記事 HTML の構築・保存(TypeScript / Node.js)
│ ├── blog_generation/ # Lambda: レース回顧・予想ブログの書き下ろし(Python)
│ ├── blog_rewriter/ # Lambda: 過去ブログのリライト(Python)
│ ├── blog_builder/ # Lambda: ブログ HTML の構築・保存(TypeScript / Node.js)
│ └── thumbnail_generation/ # Lambda: サムネイル生成(Python)
├── packages/ # 共有パッケージ(→ 共有パッケージのページを参照)
│ ├── models/ # DocumentDB モデル(Beanie)
│ ├── race_result_facts/ # レース結果の決定論的な解釈・照合
│ ├── race_analysis_agent/ # レース総合解析エージェント
│ ├── media_analysis_sdk/ # Bedrock メディア解析 SDK
│ ├── article_generation_agent/
│ ├── blog_generation_agent/
│ ├── blog_parts/ # ブロック JSON・着順テーブル・レースカード
│ ├── thumbnail_generation_agent/
│ ├── ng_word_filter/
│ └── tone_of_voice/
├── bin/ # 運用スクリプト(一括登録・アップロード等)
├── scripts/ # スクレイピング・精度検証スクリプト
├── data/ # ローカル作業用データ
├── api-collections/ # API クライアントのコレクション
├── .github/workflows/ # Lambda ごとのデプロイワークフロー
├── pyproject.toml # uv ワークスペース定義
├── ruff.toml / mypy.ini # Lint / 型チェック設定
└── mise.toml # ツールバージョン管理
odds_poc_app(FastAPI)
apps/odds_poc_app/
├── pyproject.toml
└── src/
├── app.py # FastAPI アプリ設定・Mangum ハンドラ
├── config/env.py # pydantic-settings による設定
├── constants/pagination.py # ページネーションの既定値
├── libs/ # 外部サービスクライアント
│ ├── bedrock.py # 埋め込みベクトル生成
│ ├── documentdb.py # Beanie 初期化
│ ├── s3.py # アップロード・署名付き URL 生成
│ └── sqs.py # ジョブ投入
├── middlewares/
├── repositories/ # データアクセス層(コレクションごと)
├── routes/
│ └── v1/ # API v1 ルート(article / blog / category / media / ng_word / old_blog / ping / race)
├── schemas/ # リクエスト・レスポンスの Pydantic モデル
├── services/ # ビジネスロジック
└── utils/ # embedding / race_result / related_info
エントリーポイントは src/app.py です。ENVIRONMENT が local 以外のとき、
Mangum が API Gateway のイベントを FastAPI へ変換します。
ルートは /v1 プレフィックスで routes/__init__.py の load_router() がまとめて登録します。
Lambda アプリ(Python)
article_generation / blog_generation / blog_rewriter / thumbnail_generation は、
HTTP ルーティングを持たないイベント駆動型の構成です。
apps/article_generation/ # 例: 記事生成 Lambda
├── pyproject.toml
├── handler.py # Lambda エントリーポイント
├── docker/Dockerfile
└── src/
├── config/env.py # pydantic-settings による設定
├── schemas/event.py # SQS イベントのペイロード型定義
├── services/ # ビジネスロジック
├── repositories/ # データアクセス層
└── libs/ # 外部サービスクライアント
├── bedrock.py
├── documentdb.py
├── s3.py
├── sqs.py
└── sqs_retry.py # 最終試行かどうかの判定
各ファイルの役割
| ファイル | 役割 |
|---|---|
handler.py |
lambda_handler(event, context) を定義する。SQS レコードのパースと services の呼び出しのみを行う |
src/config/env.py |
環境変数を pydantic-settings で管理する |
src/schemas/event.py |
SQS イベントのペイロードを Pydantic モデルで型定義・バリデーションする |
src/services/ |
ビジネスロジック。handler.py から呼ばれ、repositories と libs を使う |
src/repositories/ |
DocumentDB へのデータアクセス。services からのみ呼ばれる |
src/libs/ |
Bedrock・S3・SQS・DocumentDB のクライアント初期化とラッパー |
src/libs/sqs_retry.py |
ApproximateReceiveCount から「この配信が最終試行か」を判定する |
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 は SQS の最終試行のときだけ True になります。
最終試行でなければ失敗しても何もせず、SQS の再配信による自己復旧に委ねます。
層間の呼び出し規則
handler.py
└─► services/ (ビジネスロジック)
├─► repositories/ (DB アクセス)
├─► libs/ (外部サービス: Bedrock / S3 / SQS)
└─► packages/ (共有ロジック: エージェント・事実照合など)
handler.pyはservicesのみを呼び出すservicesはrepositories・libs・packagesを呼び出すrepositoriesはlibsを呼び出してよいが、servicesは呼び出してはいけないpackages/models/の DocumentDB モデルはrepositoriesから参照する
Lambda アプリ(TypeScript)
article_builder / blog_builder は React コンポーネントで HTML を構築するため、
TypeScript(Node.js 22)で実装します。
apps/blog_builder/
├── package.json
├── tsconfig.json
├── handler.ts # Lambda エントリーポイント
├── docker/Dockerfile
├── scripts/
│ ├── esbuild-options.mjs # バンドル設定(SCSS・アイコン allowlist 等)
│ └── sync-oddspark.mjs # oddspark-static-pages からの同期
├── vendor/oddspark/ # 同期したコンポーネント・SCSS・アセット(コミット対象)
└── src/
├── article/ # 記事テンプレートとブロック描画
├── config/env.ts
├── libs/
│ ├── documentdb.ts # mongodb クライアント(再接続対応)
│ └── sqs-retry.ts # 最終試行かどうかの判定
├── repositories/
├── schemas/event.ts # zod による SQS ペイロード検証
└── services/
article_builder も同じ構成ですが、vendor/ を持たず、
src/components/race-summary/ に自前のサマリーコンポーネントを置きます。
odds_poc_app と Lambda アプリの違い
| 項目 | odds_poc_app(FastAPI) | Lambda アプリ |
|---|---|---|
| エントリーポイント | src/app.py(FastAPI + Mangum) |
handler.py / handler.ts |
| ルーティング | routes/ 層 |
なし(ハンドラが直接受け取る) |
| イベント型 | HTTP Request(API Gateway) | SQS メッセージ |
| レスポンス | HTTP Response | なし(次の SQS へ送信、または DB に保存) |
| 失敗時 | HTTP エラーを返す | 最終試行かどうかで挙動を分ける(再送出 or 破棄・失敗記録) |
テスト
テストは対象パッケージ直下に test_*.py として置き、uv run python <path> で実行します。
| ファイル | 対象 |
|---|---|
apps/odds_poc_app/test_related_info.py |
関連情報セクションの組み立て |
packages/article_generation_agent/test_placeholders.py |
プレースホルダー差し込み |
packages/blog_generation_agent/test_main.py |
ブログ執筆の回帰テスト |
packages/race_result_facts/test_checks.py |
事実照合 |
packages/race_result_facts/test_prediction.py |
予想データの扱い |
packages/race_result_facts/test_source_mix.py |
記事の由来(レース結果 / 動画)の測定 |
TypeScript 側(article_builder / blog_builder)は npm run typecheck / npm run lint と
ビルド成功・描画確認で担保します。