Skip to content

Custom Structured Output

Use StructuredPipeline when the required output is a Pydantic model other than SVIS. Use ChunkedStructuredPipeline when the source can exceed one provider request and your application can define how to combine chunk results.

These APIs use the same local source adapters, resource limits, strict provider contract, retry policy, and result envelope as SurveyScribe. They do not apply SVIS field rules, confidence policy, duplicate handling, or routing semantics.

One-call pipeline

StructuredPipeline normalizes the source and makes exactly one provider call. The complete normalized document must fit both max_request_tokens and the provider capability limit. An oversized request fails before transport. The provider response must validate as the supplied Pydantic model.

This executable example uses the package fake. It does not load a provider SDK, read a credential, or access the network.

# docs-exec: structured-pipeline-fake
from pydantic import BaseModel
from pathlib import Path
from tempfile import TemporaryDirectory

from survey_scribe import ResultStatus, StructuredPipeline
from survey_scribe.providers import CapabilityEvidence, ModelCapabilities
from survey_scribe.providers.testing import DeterministicFakeProvider


class Summary(BaseModel):
    title: str
    question_count: int


capabilities = ModelCapabilities(
    provider="synthetic",
    model="deterministic-fake",
    structured_output=True,
    strict_schema=True,
    max_input_tokens=32_000,
    max_output_tokens=4_096,
    supported_generation_settings=frozenset(
        {"temperature", "max_output_tokens", "seed"}
    ),
    evidence=CapabilityEvidence.configuration_only,
    tested_sdk_version="synthetic-no-sdk",
)
provider = DeterministicFakeProvider(
    capabilities=capabilities,
    responder=lambda _request: Summary(
        title="Synthetic household questionnaire",
        question_count=2,
    ),
)
with TemporaryDirectory() as temporary_directory:
    source = Path(temporary_directory) / "synthetic-questionnaire.txt"
    source.write_text("Age in years\n\nEmployment status", encoding="utf-8")

    result = StructuredPipeline(provider, Summary).convert(source)

    assert result.status is ResultStatus.success
    assert result.output == Summary(
        title="Synthetic household questionnaire",
        question_count=2,
    )
    assert provider.call_count == 1

In an application, inject a reviewed StructuredProvider instead of DeterministicFakeProvider. The custom instructions value is a system instruction. Source text remains inside a separate untrusted-data envelope.

Chunked pipeline

ChunkedStructuredPipeline creates deterministic, ordered chunks under the provider and request token ceilings. It calls the provider once per chunk under one max_concurrency limit. It then passes successful ProviderResponse values and ordered FailedBlock values to your reducer.

from pydantic import BaseModel

from survey_scribe import ChunkedStructuredPipeline
from survey_scribe.providers import ProviderResponse
from survey_scribe.results import FailedBlock


class ChunkSummary(BaseModel):
    labels: tuple[str, ...]


class CombinedSummary(BaseModel):
    labels: tuple[str, ...]
    failed_chunks: tuple[str, ...]


def combine(
    responses: tuple[ProviderResponse[ChunkSummary], ...],
    failures: tuple[FailedBlock, ...],
) -> CombinedSummary:
    return CombinedSummary(
        labels=tuple(label for response in responses for label in response.output.labels),
        failed_chunks=tuple(failure.block_id for failure in failures),
    )


pipeline = ChunkedStructuredPipeline(
    provider,
    ChunkSummary,
    combine,
    max_request_tokens=32_000,
    overlap_tokens=1_000,
    max_concurrency=4,
    allow_partial=False,
)

The reducer owns merge, deduplication, ordering beyond chunk order, and domain validation. The package does not infer these rules for a custom model. Overlap can repeat source text, so a reducer must handle repeated facts when needed.

With the default allow_partial=False, any failed chunk produces a failed result and the reducer does not run. With allow_partial=True, the reducer receives all successful responses and failures; usable reducer output with failures has partial status. An empty document calls the reducer with two empty tuples.

Both pipelines return ExtractionResult. They convert operational exceptions to safe failed results, but they re-raise cancellation and process-control signals. Synchronous convert() cannot run in an active event loop; async applications must use aconvert() or extract().

See the Results guide for statuses and generic JSON artifacts, and the providers reference for capability evidence.