Turn the invoice-note summariser into a batch endpoint with per-row outcomes

Python · LLM apps · beginner · greenfield

Turns the one-off invoice-note summariser into a batch endpoint: `POST /summaries/batch` walks the rows in order, streams each model call and accumulates the text deltas, and when the model asks for `normalize_amount` the streamed fragments are merged by index, the tool runs once per `tool_call_id` and the result goes back as a `role: "tool"` message before the follow-up call. Tool-call chunks are expected here — the notes are full of money amounts — and a chunk with no text contributes nothing to the summary. Off-schema tool arguments come back to the model as a tool error instead of raising, and `normalize_amount` itself never throws. Exercised against a seeded batch of 40 invoice notes; the amounts were normalized, the summaries came back one sentence each and `failed` was empty.

Back-office nightly job: an operator triggers it from the admin UI with batches of up to 50 invoice-note rows. Notes are free text and usually contain money amounts in mixed formats (`1 200,50 EUR`, `$1,200.50`, `EUR 1200`), so the model routinely calls `normalize_amount`. Batches run during shared-org peak hours, so a `RateLimitError` that survives the SDK's two retries, or a stream that times out mid-way, hits one row in most nightly runs. The caller reads `failed` to decide which rows to re-queue for the next run.

Requirements

Files touched

--- app/batch_summarizer.py
+"""Nightly batch summariser for invoice notes.
+
+One streamed model call per row, plus the `normalize_amount` tool the model
+uses to turn a free-text money amount into integer cents. The batch endpoint
+reports per-row outcomes: a row either lands in `summaries` or its id lands in
+`failed`.
+"""
+
+from __future__ import annotations
+
+import json
+import re
+from decimal import ROUND_HALF_UP, Decimal, InvalidOperation
+from typing import Any
+
+from fastapi import APIRouter
+from openai import AsyncOpenAI
+from openai.types.chat import ChatCompletionMessageParam
+from pydantic import BaseModel, Field, model_validator
+
+MODEL = "gpt-4o-mini-2024-07-18"  # pinned dated snapshot, per ADR-011
+MAX_ROWS = 50
+
+SYSTEM_PROMPT = (
+    "You summarise invoice notes for the finance back office. Reply with one "
+    "sentence of plain text and nothing else. When a note mentions a money "
+    "amount, call normalize_amount to get the amount in cents before you "
+    "write the summary; never mention the tool or its raw result."
+)

Review this PR

Python practice