The one-shot problem

Asking a language model to enrich metadata is easy. Give it a column name, a logical type, and a few vocabulary labels, and it will return a classification, a business description, a handful of concept mappings, and a guess about whether the field is personal data. For a clean, well-named column, the answer is usually good.

The failure mode is quieter than a wrong answer that announces itself. A one-shot model produces a confident answer and no signal about how much to trust it. It classifies cust_ref as INTERNAL without noticing that an adjacent column carries a national ID which should have pulled the whole record to HIGH_CONFIDENTIAL. It maps amt to a generic schema.org concept when a precise fibo-fnd-acc-cur:MonetaryAmount mapping was available in the dataset's vocabulary. It misses that "direct identifier" and "personal information" are not the same flag. None of these are hallucinations exactly. They are the ordinary errors of a model that only looked once.

"A one-shot model gives you an answer. It does not give you a reason to believe the answer."

The standard fix for this in human teams is not "hire a smarter analyst." It is review. Someone drafts; someone else checks the draft against the full context and pushes back. ODIN applies the same structure to its AI layer: metadata enrichment runs as a proposer/reviewer loop, not a single call.

The proposer/reviewer pattern

The pattern is simple to state and surprisingly powerful in practice. Two agents play distinct roles against the same dataset:

proposer

Drafts a complete enrichment for every element in a logical model (description, classification, vocabulary concept mappings, and PII / direct-identifier flags) each with its own short reasoning.

reviewer

Audits the draft against the dataset's full DCAT context (title, description, keywords, every sibling element ) and returns a verdict: APPROVE, or REJECT with specific, per-issue comments.

The asymmetry is the point. The proposer optimises for a complete, plausible draft. The reviewer optimises for finding what is wrong with it. Giving each model one job and the reviewer the whole dataset rather than one element at a time. It surfaces exactly the cross-element mistakes that a one-shot call cannot see, because the reviewer is looking at the proposal and the context side by side.

When the reviewer rejects, its comments are not free text for a human to read later. Each comment is structured; it names the element, the dimension in question (description, classification, vocabulary, or PII), and the issue. The proposer consumes those comments and produces a revised draft. The loop continues until the reviewer returns APPROVE, or until a hard iteration cap is hit.

How ODIN's loop runs

A review is kicked off against a single logical model and streams its progress back to the caller. The server runs the loop on a virtual thread so a long review never blocks a request thread.

Start an agentic review (Server-Sent Events)
# POST /api/v1/agentic-review (streams text/event-stream)
curl -N -X POST https://catalog.example/api/v1/agentic-review \
  -H "Content-Type: application/json" \
  -H "Accept: text/event-stream" \
  -d '{ "datasetId": "3fa85f64-...", "modelId": "7c9e6679-..." }'

The request body is just the dataset and the model to review. Everything else (the current metadata, the available vocabularies, the lessons from past reviews) is gathered server-side. The loop then proceeds through a fixed set of phases, each emitted as an event so the UI can render progress live:

01

Gather context

The service loads the dataset's full DCAT metadata and the model's elements, scoped to the caller's tenant and user. The bearer token is captured on the request thread and forwarded so every downstream call stays correctly scoped.

02

Recall lessons from memory

Before drafting anything, the loop retrieves relevant lessons from prior reviews (recurring corrections the reviewer has made before) and folds them into the proposer's instructions so it does not relitigate the same mistakes.

03

Propose

The proposer drafts every element's description, classification, vocabulary concept mappings, and PII / direct-identifier flags; each accompanied by a one-line reasoning that the reviewer can attack.

04

Review

The reviewer reads the draft against the full dataset context and returns APPROVE or REJECT, with a per-issue comment list and a one-line summary. Dimensions the reviewer is satisfied with are locked, so subsequent iterations refine only what is still contested instead of oscillating over settled ground.

05

Converge or cap

On APPROVE the loop finishes and the converged proposal is persisted. If the conversation has not converged after ten iterations, the loop stops anyway and returns its best proposal; bounded cost, no runaway loops.

The event stream

Every step above is a JSON AgenticEvent on the SSE stream. The phase markers let a UI show exactly where the loop is; the substantive events carry the proposal and the review. The terminal event tells the client how it ended.

Phase Meaning Payload
CONTEXT · MEMORY Setup markers Context gathered; lessons recalled from review memory
PROPOSING · PROPOSAL Proposer turn The drafted per-element enrichment for this iteration
REVIEWING · REVIEW Reviewer turn Verdict (APPROVE / REJECT), per-issue comments, summary
LOCKED Convergence guard A dimension the reviewer has approved and the loop has frozen
DONE · MAX_REACHED Terminal The final, persisted proposal; converged, or capped
ERROR Terminal A human-readable message; the stream then completes
Why stream it at all?

An agentic review can take several model round-trips. Streaming the proposer's drafts and the reviewer's objections as they happen turns a black box into a glass one: the data owner watches the argument unfold, sees why a classification moved from INTERNAL to CONFIDENTIAL, and arrives at the final proposal already understanding it. Swagger UI cannot render SSE; the producer console and curl -N can.

Memory: the loop gets better over time

A review loop that starts from zero every time is doomed to make the same correction forever. ODIN's reviewer writes down what it learns. When a review converges, the lessons embedded in the reviewer's objections ("fields named *_ref in this tenant are foreign keys, not identifiers," "monetary columns here should map to FIBO, not schema.org") are persisted to a long-term review memory.

On the next review, those lessons are recalled during the MEMORY phase and handed to the proposer up front. The effect compounds: the proposer's first draft on the hundredth dataset is markedly better than its first draft on the first, because it inherits a hundred datasets' worth of corrections. Convergence gets faster, and the reviewer spends its objections on genuinely new problems instead of recurring ones.

"The first review of a dataset is a conversation. The thousandth is a conversation that remembers the previous nine hundred and ninety-nine."

The human still signs off

It would be tempting to let a converged, self-reviewed proposal write itself straight into the catalog. ODIN deliberately does not. When the loop finishes, the result is persisted to the model's elements as a recommendation; exactly the same owner-gated state that a one-shot suggestion lands in. The data owner reviews it and accepts or rejects, element by element or in bulk.

This is not timidity about AI. It is the same principle that runs through the rest of ODIN: AI accelerates metadata capture, but a named human remains accountable for what the catalog asserts. A two-agent loop produces a far stronger draft than a single call but a draft that converged between two models is still a draft until the person accountable for the dataset says otherwise.

🔁
Self-critiqued

Two agents in a loop catch cross-element mistakes a single call cannot see.

🧠
Memory-backed

Lessons from past reviews carry forward, so the loop converges faster over time.

📡
Transparent

Every proposal and objection streams live over SSE; the reasoning is visible, not hidden.

🏛️
Owner-gated

The converged result is a recommendation; a named owner accepts or rejects it.

Drafts you can argue with

The interesting thing about a proposer/reviewer loop is not that it produces better metadata, though it does. It is that it produces metadata with an argument attached. Every accepted classification has a trail: what the proposer suggested, what the reviewer objected to, how it was resolved. That trail is exactly what a governance framework asks for; not "the catalog says this column is confidential," but "here is why, and here is who agreed."

One-shot enrichment optimises for a populated catalog. An agentic, owner-gated loop optimises for a catalog that is both populated and defensible. In a domain where the difference between INTERNAL and HIGH_CONFIDENTIAL is a regulatory event, that is the difference that matters.