"""Deterministic, offline stub backend (no network, no provider SDK). ``StubBackend`` is the test double referenced by CLAUDE.md and the core smoke test: it satisfies the :class:`~doc_agent.backends.base.ExtractionBackend` protocol but returns fixed, schema-valid ``Document`` data instead of calling a model. That keeps the core end-to-end testable offline -- the smoke test can run ``process_document`` with no API key, no Ollama server, and no quota -- while the real adapters (build plan phases 2.5/2.6) are deferred. The default payload is a small, internally-consistent receipt chosen so every validation rule passes (the happy path that can auto-accept). Tests that need a specific shape -- a malformed value, a hard-rule violation, a low-confidence field -- inject their own ``data``/``field_confidence`` rather than mutating a shared template; :meth:`StubBackend.extract` deep-copies its output so a caller populating pipeline fields can never corrupt the next call. """ from __future__ import annotations import copy from typing import Any from pydantic import BaseModel from doc_agent.backends.base import BackendResult, DocumentPayload # A fixed, internally-consistent receipt used as the stub's default output. # Chosen so every validation rule passes: subtotal + tax == total (H2), line # amounts sum to subtotal (H3), per-line quantity*unit_price == amount (S4), # total present and non-negative (H4), critical fields correctly typed (H1), and # the soft fields (date, currency, vendor) all present and plausible. This lets # the core smoke test exercise the clean-document auto-accept path without a # network or a real model. DEFAULT_STUB_DOCUMENT: dict[str, Any] = { "doc_type": "receipt", "vendor_name": "Stub Coffee House", "vendor_address": "1 Test Street, Singapore 000000", "invoice_number": "STUB-0001", "document_date": "2024-01-15", "due_date": None, "currency": "SGD", "line_items": [ {"description": "Espresso", "quantity": 2, "unit_price": 3.50, "amount": 7.00}, {"description": "Croissant", "quantity": 1, "unit_price": 4.00, "amount": 4.00}, ], "subtotal": 11.00, "tax": 0.77, "total": 11.77, } # Per-field confidence the stub reports, uniform and high so the core's # aggregation yields a strong model signal on the happy path. Keyed by schema # field name, matching the populated fields of ``DEFAULT_STUB_DOCUMENT``. DEFAULT_STUB_CONFIDENCE: dict[str, float] = { "doc_type": 0.99, "vendor_name": 0.97, "vendor_address": 0.95, "invoice_number": 0.96, "document_date": 0.98, "currency": 0.99, "subtotal": 0.98, "tax": 0.97, "total": 0.99, } class StubBackend: """An offline ``ExtractionBackend`` returning fixed, schema-valid data. The stub ignores the payload entirely and returns its configured ``data`` on every call, so its output is deterministic across runs (no clock, no randomness, no network). Construct it with custom ``data``/``field_confidence`` to drive a specific pipeline path in a test. Attributes: name: The backend identifier ("stub"), used in logs and the factory. """ name = "stub" def __init__( self, data: dict[str, Any] | None = None, field_confidence: dict[str, float] | None = None, ) -> None: """Initialize the stub with the data it should return. Args: data: The extracted-fields dict to return; defaults to ``DEFAULT_STUB_DOCUMENT`` (a clean, reconciling receipt). field_confidence: Per-field confidence to return; defaults to ``DEFAULT_STUB_CONFIDENCE``. Pass ``None`` explicitly via a custom value to simulate a backend that exposes no signal. """ self._data = DEFAULT_STUB_DOCUMENT if data is None else data self._field_confidence = ( DEFAULT_STUB_CONFIDENCE if field_confidence is None else field_confidence ) def extract(self, payload: DocumentPayload, schema: type[BaseModel]) -> BackendResult: """Return the configured deterministic data, ignoring the payload. The returned ``data`` is a deep copy of the template so a caller that mutates it (the core populates pipeline fields on the validated document, not this dict, but defensiveness here keeps repeated calls independent) cannot affect subsequent calls. Args: payload: The document payload; ignored by the stub. schema: The output-contract model class; accepted for interface conformance, not used to constrain the fixed data. Returns: A ``BackendResult`` whose ``data`` validates against ``schema``. """ return BackendResult( data=copy.deepcopy(self._data), field_confidence=( dict(self._field_confidence) if self._field_confidence is not None else None ), raw={"backend": self.name, "note": "deterministic offline stub"}, )