Cocoindex Extraction Contract — PRODUCT
Cocoindex Extraction Contract — PRODUCT
Section titled “Cocoindex Extraction Contract — PRODUCT”Status: RATIFIED-S241 (Q-EX2 — discriminated-union Pydantic with
ExtractByLlmtypedoutput_type). Ratification source:docs/specs/core-docs-pathway-assessment/S239-still-open-consolidation.mdlines 216-219 (S241 closure addendum row 6). Originating recommendation:docs/plans/phase-0-investigation/phase-b-prerequisite-2-cocoindex-deep-dive.md§4 Recommendation 1 (“single biggest missing primitive in KH plan”).
Summary
Section titled “Summary”The cocoindex extraction contract defines the typed Python schemas that govern every LLM extraction in the Knowledge Hub pipeline. A single discriminated-union root type — ExtractionOutput — is supplied to cocoindex’s ExtractByLlm primitive as output_type, so each LLM response is validated against the declared shape before any Postgres UPSERT. Three variants are mandatory at v1: a typed Q&A form extraction (one form metadata block plus zero-or-more Q&A pairs), a typed entity-mention extraction (named-entity dedup substrate per the canonical 12-value entity taxonomy), and a typed classification result (content_type + primary_domain + confidence). Every extraction carries a discriminator field, a stable correlation handle (op_id), and source-attribution back to the content_items row that triggered it. Validation failures are loud (the extraction is recorded via pipeline_runs with a typed error class and no partial write reaches the target tables); silent fallback to untyped output is forbidden.
Behavior
Section titled “Behavior”Each numbered invariant below is
[RATIFIED-S241]unless explicitly tagged otherwise. Per CLAUDE.md no-fabrication discipline, source citations are inline (file:line where possible). Gap-flag format per construction-guide §5.3 used for genuine open items.
Schema shape — the discriminated union
Section titled “Schema shape — the discriminated union”-
ExtractionOutputis a discriminated-union Pydantic root type.[RATIFIED-S241]Every LLM extraction call in the cocoindex pipeline (per the 6-stage topology indocs/plans/phase-0-investigation/architecture/02-data-flow.md§3.1 — “LLM extraction” stage) suppliesoutput_type=ExtractionOutput(or a sub-variant) to itsExtractByLlminvocation. The discriminator field isextraction_kind: Literal["q_a_form", "entity_mention", "classification"]. No untyped ordict[str, Any]extraction shape is permitted in v1. Source:phase-b-prerequisite-2a-cocoindex-examples.md§“Capability: cocoindex.functions.ExtractByLlm” lines 311-329 (theExtractByLlmcapability section confirmsoutput_typeas a typed Python class — dataclass, Pydantic, NamedTuple — with nested-schema support). -
The
q_a_formvariant carries form metadata plus zero-or-more Q&A pairs.[RATIFIED-S241]When the extractor fires against a procurement form (one of the eight procurement form_type values —bid/rfp/pqq/itt/tender/framework/dps/gcloudperdocs/ontology/26-form-type.mdlines 65-79) or a non-procurement form (checklist/questionnaire/sales_proposal_templateper the same canonical CV) or against a curated Q&A markdown sidecar, the response shape is oneFormMetadatablock (form_type, form_format, optional form_title, optional issuing_organisation, optional deadline) plus a list ofQAPairblocks (question_text, optional answer_text,expected_response_kind, optional evaluation_criteria, optional evidence_requirements). Theq_a_formvariant binds to a single downstream target:q_a_extractions(per Q&A pair, withextractor_kind='llm_extraction'per05-qa-flow.md§3.2). ItsFormMetadatablock is not persisted by this LLM path —form_templates/form_template_fieldsare written by the deterministic Path-B form extractor (ExtractedForm, ID-52), which Inv-19 keeps disjoint from the Path-A Q&A write. [Factual reconciliation S287 / bl-184(a): the original “perFormMetadata→form_templates(where Theme A wiring applies)” binding never materialised through the LLM variant; form-table writes flow only from Path-B. Seedocs/research/s286-bl184-formmetadata-persistence-gap.md§1.6.] Source for the full 11-valueform_typeenum:docs/ontology/26-form-type.mdlines 65-79 baseline values table.expected_response_kindrename rationale. The drafter spec named this fieldquestion_kind, which collides withquestion_matches.question_kindperdocs/plans/phase-0-investigation/architecture/05-qa-flow.md§7.2. That column is the form-type discriminator on thequestion_matchestable — it holds form-type values (bid/rfp/pqq/ etc.). To avoid name collision when extracted Q&A rows project into or join againstquestion_matches, this contract renames the per-Q&A obligation field toexpected_response_kind. Canonical values areLiteral["mandatory", "optional"]per the ratified shape atphase-b-prerequisite-2a-cocoindex-examples.mdline 212 (the deep-dive’s Pydantic illustration). A third “info-only” variant appears in pseudo-code at line 341 of the same file but is NOT ratified and is omitted from the v1 closed enumeration; if downstream procurement workflows surface a third state, it lands via spec-update with citation rather than via prompt drift. -
The
entity_mentionvariant carries one named entity plus its source span.[RATIFIED-S241]Eachentity_mentionextraction names exactly one entity from the canonical 12-value taxonomy inlib/validation/schemas.ts:1495-1508(organisation,certification,regulation,framework,capability,person,technology,project,sector,product,standard,methodology). The variant carriesentity_type,entity_name(raw mention text), optionalcanonical_name(pre-resolution), source-span character offsets withincontent_text(start + end), and amention_confidencefloat. This variant binds downstream toentity_mentions(per02-data-flow.md§3.1 “Entity resolution” stage). Source: 12-value entity taxonomy perlib/validation/schemas.ts:1495-1508+docs/reference/entity-type-taxonomy-spec.md(cited in CLAUDE.md Gotcha “Entity classification: false positives, not type errors”). -
The
classificationvariant carries content_type, primary_domain, and confidence.[RATIFIED-S241]Eachclassificationextraction declares acontent_typefrom the closed enumeration inlib/validation/schemas.ts:43-52(VALID_CONTENT_TYPESre-exported fromlib/ontology/content-type-registry.ts), aprimary_domainstring (free-form but constrained by the per-workspace domain taxonomy at extraction time), and aclassification_confidencefloat in[0.0, 1.0]. Optional fields:secondary_classifications(list of content_type values for multi-topic content) andrationale(model-supplied free text). This variant binds downstream tocontent_items.content_type+content_items.primary_domain+content_items.classification_confidence. Source:lib/validation/schemas.ts:43-52(existingcontent_typeenum); supersedes the bespokeclassify.tschain per02-data-flow.md§3.3 RATIFIED-RETIRE classification telemetry. -
Every variant carries
op_id,content_items_id, andextracted_at.[RATIFIED-S241]EachExtractionOutputinstance carries anop_id: UUID(the cocoindex per-flowop_idper02-data-flow.md§5.1 hybrid op_id pattern + N7 RESOLVED-S236), acontent_items_id: UUID(FK to thecontent_itemsrow whosecontent_textwas the extraction input — this is the source-attribution marker per CLAUDE.md no-fabrication discipline), andextracted_at: datetime(UTC timestamp set by the extractor at LLM-call time). These three fields are populated by the outer-tier cocoindex flow wrapper, not by the LLM — the model does not generate them.
When each variant fires
Section titled “When each variant fires”-
The
classificationvariant fires for every newly-ingestedcontent_itemsrow.[RATIFIED-S241]The classification stage runs on every content_text that enters the cocoindex flow (per the 6-stage topology in02-data-flow.md§3.1). It supersedes the pre-cocoindexclassifyContentpath per02-data-flow.md§3.3 RATIFIED-RETIRE roadmap §3 classification telemetry. The output rewritescontent_items.content_type/primary_domain/classification_confidencefor the row. Re-runs on subsequent ingest only whencontent_textchanges (per the inner-tier memo behaviour established in invariant 12). -
The
q_a_formvariant fires only when the content_type indicates a form or Q&A sidecar.[RATIFIED-S241]Cocoindex routes a content row to theq_a_formextractor when the classification stage (or the manual ingest seed) declarescontent_typein {q_a_pair, plus form variants perform_typesCV indocs/ontology/26-form-type.md}. Non-form content (case_study,policy,methodology,capabilityperlib/validation/schemas.ts:899) does NOT route through this extractor. Open question: the exact set of routing content_types depends on theform_typesCV instance table landing in T2 of the canonical-pipeline PLAN (per04-workspace-types.md§8 item 7) — pre-launch this list is constrained to the four legacy content_types until the CV table lands.[GAP-Q-EX2-001] Category: schema alignment. Sources checked: lib/validation/schemas.ts:899, docs/ontology/26-form-type.md lines 65-79, docs/specs/id-31-canonical-pipeline-implementation-plan/PLAN.md §4.2 T2 subtask 1. Prerequisite work: T2 (Q-OQR1-16 combined PR) lands the form_types CV instance table; routing finalises post-T2. -
The
entity_mentionvariant fires when the entity-resolution stage runs.[RATIFIED-S241]Per02-data-flow.md§3.1 “Entity resolution” stage, the cocoindex pipeline emits zero-or-more entity-mention extractions per content_text. The extractor runs on every content row regardless of content_type (entities can appear in any text), but invariants 9-10 bound which mentions are persisted. Selective adoption perphase-b-prerequisite-2-cocoindex-deep-dive.md§4 Recommendation 3: named-entity dedup is in scope (organisation/person/product/technologyper Phew domain); Q&A pair dedup and content_items dedup remain KH-side. -
Entity mentions below
mention_confidencethreshold are not persisted.[RATIFIED-S241]Eachentity_mentionwhosemention_confidence < 0.5(threshold ratified at the entity-resolution stage per Phew’s named-entity domain — see CLAUDE.md Gotcha “Entity classification: false positives, not type errors”) is dropped before write-back toentity_mentions. The threshold is configurable perentity_typebut defaults to 0.5 globally. The filter is applied at the entity-resolution stage (downstream of this contract per invariant 8 + §3.1 row 5) — not at the LLM extraction stage. This invariant therefore documents the persistence rule; enforcement is owned by T8 entity-resolution wiring, not by the Pydantic shape. -
A single content_text can yield mixed variants in one flow run.
[RATIFIED-S241]Per02-data-flow.md§3.1, the LLM extraction and entity-resolution stages run in the same cocoindex flow run for a given content_items row. The output of one run can therefore be: oneclassification+ manyentity_mention(always); plus, when routed per invariant 7, oneq_a_formcontaining nested Q&A pairs. Each output carries the sameop_idper invariant 5, providing the correlation handle across variants.
Validation rules (what counts as well-formed)
Section titled “Validation rules (what counts as well-formed)”-
Pydantic validation runs before Postgres UPSERT.
[RATIFIED-S241]The cocoindexExtractByLlmprimitive validates the LLM response against the declaredoutput_typeschema before passing the extraction downstream. Perphase-b-prerequisite-2a-cocoindex-examples.mdline 329 (“Memoised: output_type + instruction + LLM model + input markdown all participate in the code-hash”), validation is part of the cocoindex contract. A response that does not parse as the declared discriminated union is treated as a validation failure per invariant 13. -
Inner-tier post-processing functions consume
content_text: strand the typed extracted column, neverFileLike.[RATIFIED-S241]Perphase-b-prerequisite-2-cocoindex-deep-dive.md§3 +0.9-spike-S9-cocoindex-idempotency.md§7.1 + §7.2, the layered fn-shape requirement is load-bearing for memoisation correctness. Any@coco.fn(memo=True)post-processing helper that runs against the LLM output (for example, span-offset reconciliation, normalisation, or confidence-threshold filtering) takescontent_text: str(and where needed the typedExtractionOutputvalue) as its primary input parameter, NOTFileLike. If a file-handle-tier wrapper is added (e.g.process_content_item(file: FileLike, ...)), it is the OUTER tier — it parses the file once via the binary-conversion stage, then delegates to the flow-scopeExtractByLlmstep with the parsedcontent_text. Violations (passingFileLiketo an inner tier) re-run the expensive LLM call on every metadata-only edit, defeating idempotency. -
A validation failure writes a structured failure record via
pipeline_runs, not a half-typed payload.[RATIFIED-S241]When the LLM response fails Pydantic parsing (missing required fields, invalid enum value, unknown discriminator value, type-coercion error), the cocoindex flow run records a structured failure via thepipeline_runsrollup (per02-data-flow.md§5.1 trigger-driven audit_log +recordPipelineRun()from@/lib/pipeline/record-runper CLAUDE.md Gotcha). The failure record carries:op_id,content_items_id,extraction_kind(attempted variant),error_class(Pydantic error category —missing_required,invalid_enum,invalid_discriminator,type_coercion,unexpected_field), and the raw LLM response (redacted of PII per the standard logging surface). No partial extraction row is written toq_a_extractions/entity_mentions/content_items. The pipeline continues processing subsequent rows; the failed row is queued for HITL review or retry per cocoindex’s native DLQ (per02-data-flow.md§3.2 retry/back-off/DLQ). -
A subsequent successful extraction supersedes a prior failure for the same content_items_id.
[RATIFIED-S241]Failures are not terminal. When the samecontent_items_idre-runs (e.g. after a prompt-template version bump per0.9-spike-S9-cocoindex-idempotency.md§7.4 version-bump cascade, or after operator-initiated retry), a successful extraction overwrites the prior failure record on the target tables. The failure record inpipeline_runsis retained for audit per its retention policy but no longer blocks downstream UPSERTs.
Source-attribution invariants
Section titled “Source-attribution invariants”-
Every extraction row in
q_a_extractions/entity_mentions/content_itemscarries the originatingop_id.[RATIFIED-S241]Per02-data-flow.md§5.1 N7 hybrid op_id pattern, the cocoindex per-flowop_idpropagates into the target Postgres tables. This gives the pipeline-correlation surface (“which rows did extraction run X produce?”) via direct SQL on the target table — no separate ledger join required for that question. -
Every extraction row references
content_items.id, neversource_documents.iddirectly.[RATIFIED-S241]The extraction contract operates on parsedcontent_text, which is thecontent_itemsbody. The source-document linkage is recovered viacontent_items.source_document_idif present, NOT via a directextraction → source_documentsedge. This preserves the per-02-data-flow.md§2.1 invariant that the source folder is canonical andcontent_itemsis the materialised index — extractions belong to the index, not to the canonical source.
Failure modes + error envelope
Section titled “Failure modes + error envelope”-
An LLM API failure (network, rate limit, provider 5xx) retries per cocoindex native back-off.
[RATIFIED-S241]Per02-data-flow.md§3.2 “Retry / back-off / DLQ — cocoindex provides these natively. KH does not build apipeline_failurestable (COCO.7 DO-NOT-BUILD per00-synthesis-v2.md§3.10)”. Retry behaviour is configured at the cocoindex flow level; this contract does not override it. After exhausting retries, the failure routes to invariant 13’spipeline_runsfailure record. -
An Anthropic API response that mismatches the declared
output_typeis treated as a validation failure, not a transient retry.[RATIFIED-S241]Perphase-b-prerequisite-2a-cocoindex-examples.mdline 329 + the typing contract in invariant 11, a malformed response (e.g. the LLM returnedentity_type: "unknown"when the enum requires one of the 12 canonical values) is a schema mismatch, not a transient API issue. The flow does NOT retry on validation mismatch — retrying yields the same malformed shape. The failure is recorded per invariant 13 and surfaced for prompt-template or schema review. -
A discriminator-field absence is the highest-severity validation failure.
[RATIFIED-S241]If the LLM response omits theextraction_kindfield entirely (or returns a discriminator value not in the union), the failure is logged witherror_class='invalid_discriminator'. This signal is the load-bearing alert that the prompt is no longer producing typed output — distinct from individual-field validation failures which can indicate prompt drift on a single variant.
Provenance + visibility
Section titled “Provenance + visibility”-
Extraction failures are visible to operators via
pipeline_runsMCP surface.[RATIFIED-S241]Per the AI-invisible-infrastructure invariant (CLAUDE.md “AI is invisible infrastructure”), the user-facing surface does not display “LLM extraction failed for this content” — that would surface AI plumbing. Instead, failed extractions surface to operators viapipeline_runsand the cocoindex ledger API (the sibling spec being drafted in T1.3 —docs/specs/id-36-cocoindex-ledger-api/TECH.md). End users see only that the extraction has not yet completed (e.g.q_a_extractionsrow absent,content_items.content_typestill NULL); they do not see error envelopes. -
Prompt-template version is tracked in the cocoindex code-hash.
[RATIFIED-S241]Perphase-b-prerequisite-2a-cocoindex-examples.mdline 329 (“Memoised: output_type + instruction + LLM model + input markdown all participate in the code-hash. Changing the schema or instruction → all rows re-extract.”), the prompt template’s text is part of the memoisation hash. Updating the prompt invalidates all cached extractions and triggers full re-extraction per0.9-spike-S9-cocoindex-idempotency.md§7.4. The contract does not declare a separateprompt_versioncolumn; the cocoindex code-hash IS the version identifier.
Anti-patterns (RATIFIED-DO-NOT-BUILD)
Section titled “Anti-patterns (RATIFIED-DO-NOT-BUILD)”-
The contract does not include a
raw_llm_responsefield as a routine output column.[RATIFIED-S241]Persisting the raw LLM response per extraction row would violate the AI-invisible-infrastructure invariant (it surfaces the LLM as a visible artefact rather than infrastructure). Raw responses are retained only inpipeline_runsfor failed extractions per invariant 13, where they serve operator debugging. -
The contract does not include a per-call “extract-everything” variant.
[RATIFIED-S241]EachExtractByLlminvocation declares ONE variant (classification, q_a_form, or entity_mention). Per02-data-flow.md§3.1, separate cocoindex stages run separate extractors — bundling all three into one mega-prompt would conflate memoisation tiers and violate the layered fn-shape requirement in invariant 12. The discriminated union is for response-validation at the cocoindex contract layer, not for prompt design. -
The contract does not bind to
LlmApiType.OLLAMAat v1.[RATIFIED-S241]Perphase-b-prerequisite-2a-cocoindex-examples.mdline 320 (“api_type can be OLLAMA for on-premise, OPENAI, ANTHROPIC, GEMINI, etc.”), cocoindex supports multiple LLM backends. KH v1 binds toLlmApiType.ANTHROPICexclusively per the Phew procurement domain’s accuracy requirements. Local/on-premise variants are[DEFERRED-v1.1].
Open questions surfaced for verification
Section titled “Open questions surfaced for verification”-
Anthropic prompt-cache passthrough for
LlmApiType.ANTHROPICis unverified.[GAP-Q-EX2-002] Category: cost-projection verification. Sources checked: docs/plans/phase-0-investigation/phase-b-prerequisite-2-cocoindex-deep-dive.md §5 still-open table row "Anthropic prompt-cache passthrough", docs/specs/core-docs-pathway-assessment/S239-still-open-consolidation.md S241 closure addendum row 7 (CATEGORISED-S241 — investigation spike needed, 1-2h). Prerequisite work: T13 prompt-cache verification spike (per canonical-pipeline PLAN §4.13 subtask 1) confirms whethercache_controlblocks pass through to the Anthropic API or are stripped by the cocoindexLlmApiType.ANTHROPICadapter. Outcome affects cost projections for high-volume extraction (Phew's bid library at ~395 q_a_pairs + ongoing ingest).Perphase-b-prerequisite-2-cocoindex-deep-dive.md§5, this is a MEDIUM-priority verification, NOT a v1 blocker — extraction works without cache passthrough, just at higher cost. -
Per-extraction-kind nested-schema depth limits are unverified for Anthropic.
[GAP-Q-EX2-003] Category: capability verification. Sources checked: docs/plans/phase-0-investigation/phase-b-prerequisite-2a-cocoindex-examples.md §"patient_intake_extraction" lines 191-204 (confirms nested dataclass support for OpenAI gpt-4o). Prerequisite work: cocoindex 1.0.3 docs read against the LlmApiType.ANTHROPIC adapter to confirm whether nested Pydantic models (e.g.Formcontaininglist[QAPair]per the q_a_form variant in invariant 2) survive the Anthropic JSON-mode contract end-to-end. If a depth-flattening transform is needed, it lands in the outer-tier wrapper before the flow-scope ExtractByLlm step.This is a capability-confirmation question, not a design-block — the discriminated-union direction holds regardless of how the nesting is materialised.
Cross-references
Section titled “Cross-references”- Implementation references: see companion
TECH.mdin this directory. - 6-stage cocoindex topology:
docs/plans/phase-0-investigation/architecture/02-data-flow.md§3.1. - Layered fn-shape rationale:
docs/plans/phase-0-investigation/0.9-spike-S9-cocoindex-idempotency.md§7 (especially §7.1 + §7.2). - ExtractByLlm API capability + canonical flow-scope pattern:
docs/plans/phase-0-investigation/phase-b-prerequisite-2a-cocoindex-examples.md§“Capability: cocoindex.functions.ExtractByLlm” lines 311-329 + lines 362-368 (flow-scopedoc["..."].transform(ExtractByLlm(...))). - Q-EX2 ratification provenance:
docs/specs/core-docs-pathway-assessment/S239-still-open-consolidation.mdlines 216-219 (S241 closure addendum row 6). - Downstream consumer (q_a_extractions extractor_kind enum):
docs/plans/phase-0-investigation/architecture/05-qa-flow.md§3.2. - Question-matches
question_kindcollision rationale:docs/plans/phase-0-investigation/architecture/05-qa-flow.md§7.2. - Canonical entity taxonomy:
lib/validation/schemas.ts:1495-1508(VALID_ENTITY_TYPES). - Canonical content_type enumeration:
lib/validation/schemas.ts:43-52(VALID_CONTENT_TYPES). - Form-type CV (11 baseline values):
docs/ontology/26-form-type.mdlines 65-79. - Pattern A/B parser retire context:
docs/plans/phase-0-investigation/architecture/02-data-flow.md§10.1. - T1.1 / T1.2 task scope:
docs/specs/id-31-canonical-pipeline-implementation-plan/PLAN.md§4.1 lines 122-139. - Sibling spec (failure-path consumer):
docs/specs/id-36-cocoindex-ledger-api/TECH.md(T1.3 draft) — ownsrecord_extraction_failuresignature.