Datalumina

The pipeline

GL categorization

Add a pipeline step that suggests a general ledger account from a fixed ten-account catalog using structured output.

By the end of this stage the pipeline produces its final piece of metadata, a suggested general ledger account for every document, picked from a fixed catalog, with a confidence and a reason Maya can read.

daveebbelaar/invoice-review1:30:23

Why a separate step

GL categorization could have been folded into the classification prompt. Dave builds it as its own step on purpose. Each step keeps one responsibility, the step could later run on a different model than the classifier, and, because the pipeline is a chain, adding it costs one line in build_default_pipeline. This is the payoff of the design-pattern conversation from the previous stage. The runner just loops over steps in order, so a new step is configuration, not surgery.

The catalog

The brief mentions GL coding but names no accounts, so the build invents ten plausible ones for a facilities company. They live as a typed catalog in backend/app/accounting/catalog.py, an enum of codes plus a description of when each applies:

CodeAccountUsed for
6100Cleaning servicesJanitorial, window cleaning, waste disposal
6110Building maintenanceGeneral repairs, handyman work, routine upkeep
6120Electrical servicesElectrician work, lighting, electrical repairs
6130Plumbing and HVACPlumbing, heating, ventilation, air conditioning
6140Equipment and toolsTool rental, small equipment, safety gear
6150Office suppliesStationery, printer supplies, consumables
6160Professional feesLegal, accounting, consulting
6170Travel and transportFuel, parking, mileage, public transport
6180UtilitiesElectricity, water, gas
6190Miscellaneous operating expensesEverything the accounts above do not cover

The descriptions are not documentation garnish. They are formatted into the agent's instructions at import time, so the catalog is simultaneously the source of truth for the code and for the prompt.

The enum is the guardrail

The suggestion model reuses the structured-output pattern from classification, with one important twist:

backend/app/pipeline/gl_categorization.py
class GlSuggestion(BaseModel):
    account_code: GlAccountCode
    confidence: float = Field(ge=0.0, le=1.0)
    reasoning: str

account_code is typed as the GlAccountCode enum, not a string. The model literally cannot return an account that does not exist; anything outside the ten codes fails validation and Pydantic AI retries. The constraint lives in the schema, not in prompt wording that a model might drift past.

The step also reads only normalized fields, never the file bytes:

backend/app/pipeline/gl_categorization.py
class GlCategorizationStep:
    name = "gl_categorization"

    def run(self, ctx: PipelineContext) -> PipelineContext:
        source = ctx.review_data if ctx.review_data is not None else ctx.extraction
        suggestion = self._categorizer.run(source)
        return ctx.model_copy(update={"gl_suggestion": suggestion})

By this point in the chain the document has already been read twice. Vendor name, line items, and totals are enough to pick an expense account; sending the PDF a third time would only add cost.

Run it

Rerun the pipeline playground script and the payload gains a gl_suggestion block. The interesting test is an ambiguous document. In the video, a generic sample lands on 6190, miscellaneous operating expenses, and Dave judges that correct behavior. When nothing fits well, the catalog's catch-all is the right answer, and the reasoning field says why. The fuel receipt, by contrast, lands cleanly on 6170.

That completes the intelligence layer of the requirements. The system can classify, extract, validate, and code every document. What is missing is everything Maya touches.

Checkpoint

  • Every processed sample receives a valid catalog code with confidence and reasoning
  • You can explain why the enum, not the prompt, prevents invented accounts
  • You can say what data this step reads and what it deliberately does not

On this page