CALIBER
Quickstart

Cookbook 06 · Build

Grounded Knowledge Assistant

Cited answers, honest abstention on missing/conflicting evidence, and a measured retrieval lift.

BuildCore40–60 min
What you build. A retrieval assistant over a small policy corpus that cites its sources, abstains when evidence is missing, surfaces conflicts, and routes low-confidence answers to human review.
Surfaces: Knowledge Base Prompts Workflows Evaluations Review Queues
What you learn
  • Build a KB version from an Object Store corpus
  • Compare dense / hybrid / graph_hybrid retrieval in Explore
  • Run inline KB calibration (Recall@k, nDCG@k, Faithfulness)
  • Wire a knowledge_query → answer → router workflow
  • Route low-confidence runs to a Review Queue

Implementation flow

flowchart LR
  C[(Corpus: policy docs)] --> KB[KB build → version]
  KB --> EXP[Explore: dense/hybrid/graph]
  KB --> CAL[Calibrate: Recall@k…]
  KB --> KQ[knowledge_query] --> ANS[Agent: cite or abstain]
  ANS --> RT{router}
  RT -->|low confidence| RV[(Review Queue)]
  ANS --> JF[Judge: CitationFaithfulness]

Step-by-step UI only

Each step shows the exact navigation path, the fields to fill, the button to click, and a snapshot of the CALIBER screen you'll be on.

  1. Knowledge › Object Store › Bucket browser creates · bucket: policy-corpus (4 docs) refund-policy.md ↓ refund-faq.md ↓ security-policy.md ↓ data-retention-policy.md ↓
    Create a bucket for the policy corpus, then upload the four Markdown policy docs (skip the READMEs). In the Bucket browser panel, type the bucket name into the new-bucket-name field and click the + create button (“Create bucket”). With the bucket open, click Upload (or drag-and-drop) to add the four files. The corpus ships a deliberate 30-vs-14-day refund contradiction across two of the files — keep both, since it drives the clarify/abstain behavior later.
    Fill in
    new-bucket-namepolicy-corpus
    Upload — refund policy (30-day, authoritative)refund-policy.md
    Upload — refund FAQ (14-day, the contradiction)refund-faq.md
    Upload — security policysecurity-policy.md
    Upload — data retention policydata-retention-policy.md
    Click + (Create bucket), then Upload
    You'll see: Bucket policy-corpus listing the four *.md documents, each previewable in-app (Created / Modified / Size / Content-type chips above the rendered markdown), with the Source id: line visible near the top of each rendered document body.
    Screen snapshot
    caliber · Knowledge › Object Store › Bucket browser
    Knowledge BaseBucket browser
    + (Create bucket), then Upload
  2. Knowledge › Knowledge Base › New knowledge base creates · KB version: policy-qa-kb
    Build a Knowledge Base from the corpus. Click New knowledge base, pick policy-corpus as the source bucket, select the four policy docs, and expand the Advanced configuration disclosure to choose an embedding model. Click Create knowledge base and wait for the build run to finish.
    Fill in
    Namepolicy-qa-kb
    Source bucketpolicy-corpus
    Documentsthe 4 policy *.md files
    Advanced configurationchoose an embedding model (defaults are fine)
    Click Create knowledge base
    You'll see: A build run starts on the Build stage; when it reaches completed the KB has an active version you can Explore. Gate evidence: capture the KB version id.
    Screen snapshot
    caliber · Knowledge › Knowledge Base › New knowledge base
    Knowledge BaseNew knowledge base
    Create knowledge base
  3. Knowledge › Knowledge Base › policy-qa-kb › Explore › Chunks creates · chunk + lineage check
    Select the built version in the header switcher, then open Explore → Chunks to confirm the corpus chunked cleanly with correct source lineage.
    You'll see: Chunks attributed to all four sources by their filename / source_keyrefund-policy.md, refund-faq.md, security-policy.md, data-retention-policy.md (this filename is the identifier the calibrator matches gold sources against — NOT the in-document Source id: string).
    Screen snapshot
    caliber · Knowledge › Knowledge Base › policy-qa-kb › Explore › Chunks
    Knowledge BaseChunks
    Read / inspect this surface — no form to fill.
  4. Knowledge › Knowledge Base › policy-qa-kb › Explore › Query creates · retrieval mode comparison
    Compare retrieval modes on one question. In the Query view (“Ask the corpus”), type the question, pick a retrieval mode on the segmented control, and click Ask; repeat for dense, then hybrid, then graph_hybrid, and note which mode surfaces the right chunk highest. (If AGE is enabled: open Explore → Graph, click Sync to AGE first, then re-ask with mode age_graph — the build does not auto-sync.)
    Fill in
    QuestionHow long are audit logs retained?
    Retrieval mode (segmented control)densehybridgraph_hybrid (then age_graph if synced)
    Click Ask
    You'll see: Ranked chunks per mode; the data-retention-policy.md chunk should rank highest under the stronger mode — your mode-comparison evidence.
    Screen snapshot
    caliber · Knowledge › Knowledge Base › policy-qa-kb › Explore › Query
    Knowledge BaseQuery
    Ask
  5. Knowledge › Knowledge Base › policy-qa-kb › Calibrate creates · calibration run (Recall / nDCG / Faithfulness) calibration-questions.jsonl ↓
    Run KB calibration to get hard numbers. The Calibrate stage scores retrieval against a Test set of questions whose gold lives only in expected.sources (a list of source keys — each the chunk's filename / source_key as shown in Explore → Chunks, e.g. data-retention-policy.md, NOT the in-document Source id) and expected.answer — that is what the calibrator reads for Recall@k / nDCG@k / Answer-correctness. The Observability Add to test set widget captures just {expected: <response text>} (no sources/answer field), so author this gold set in the UI row editor: create the test set (next pattern: Evaluate › Test Sets › + New Test Set), open its detail page at /eval-datasets/:id, and click + Add example for each question, setting inputs = {question} and expectations = {sources:["data-retention-policy.md"], answer:"…"} from the rows in calibration-questions.jsonl (each row already carries expectations.sources + expectations.answer). (Prefer the API? POST the same rows to /eval-datasets/{id}/examples.) Back on Calibrate, choose the built version, the Test set, a retrieval mode, and Top-k, then click Run calibration. Tune chunking or switch the mode, re-run, and show recall improve without lowering faithfulness; then set it as the baseline.
    Fill in
    Versionthe completed policy-qa-kb version
    Test setyour calibration question set (C01–C06), each row POSTed as {input:{question}, expected:{sources:[…], answer:…}}
    Retrieval mode / Top-ke.g. hybrid / 5
    Click Run calibration
    You'll see: Server-side scored metrics: Recall, nDCG, Faithfulness, and Answer-correctness cards plus a per-question table. Re-run after a tweak to show the recall lift; use Set baseline. Gate: retrieval recall improves after calibration.
    Screen snapshot
    caliber · Knowledge › Knowledge Base › policy-qa-kb › Calibrate
    Knowledge BaseCalibrate
    Run calibration
  6. Library › Prompts › New prompt creates · prompt: kb-answer kb-answer.md ↓
    Author the answer prompt. Paste the template body (below the frontmatter). It tells the model to answer only from retrieved chunks, cite every claim, abstain when evidence is missing, and clarify (not pick a side) when sources conflict — emitting the JSON contract {decision, answer, citations, confidence} the router reads.
    Fill in
    Namekb-answer
    Commit messagev1 grounded policy-QA answerer (cite-or-abstain, conflict-aware)
    Template body (paste)Paste the body of kb-answer.md — variables {{ question }}, {{ policy_domain }}, {{ retrieved_chunks }}
    Click Create
    You'll see: kb-answer v1 created under the prod alias.
    Screen snapshot
    caliber · Library › Prompts › New prompt
    PromptsNew prompt
    Create
  7. Library › Skills › Build Skill creates · skills: contradiction-detector + citation-and-next-steps contradiction-detector.md ↓ citation-and-next-steps.md ↓
    (Optional reinforcement) Author two small skills. Click Build Skill to open the wizard (Identity → Content → Composability → Triggers → Review & create). contradiction-detector runs ahead of the agent to force a clarify on the 30-vs-14-day conflict; citation-and-next-steps runs after it to format citations + one next step. On Identity set the kebab-case name, a real skill category (compliance_safety for contradiction-detector, research for citation-and-next-steps), and tags; on Content set the summary (the narrow trigger text the selector reads — keep it tight) and the SKILL.md body. Finish with Create Skill. Names must not start with claude/anthropic.
    Fill in
    Skill 1From contradiction-detector.md — name + category compliance_safety + tags (Identity); summary + content (Content)
    Skill 2From citation-and-next-steps.md — name + category research + tags (Identity); summary + content (Content)
    Click Create Skill
    You'll see: Both kebab-case skills listed in the Library; opening one shows its workspace (render preview + trigger tests).
    Screen snapshot
    caliber · Library › Skills › Build Skill
    SkillsBuild Skill
    Create Skill
  8. Compose › Workflows › New Workflow creates · workflow: kb-assistant
    Create the answer workflow from a knowledge-retrieval template. Click New Workflow, type the name in the name box, then click the Knowledge Q&A template tile (the knowledge_rag template; or GraphRAG Hybrid for graph_hybrid_rag) — selecting the tile creates the workflow and opens the Studio editor. The next step wires the nodes and binds your prompt.
    Fill in
    Give your workflow a name…kb-assistant
    Click Knowledge Q&A (template tile)
    You'll see: The Workflow Studio editor opens with a knowledge_query-centric starter graph.
    Screen snapshot
    caliber · Compose › Workflows › New Workflow
    WorkflowsNew Workflow
    Knowledge Q&A (template tile)
  9. Compose › Workflows › kb-assistant (Studio editor) creates · workflow saved (RAG + router)
    Wire the four-stage chain. Point the Knowledge Query node at the policy-qa-kb version (mode hybrid); add a Python Code node score_confidence that derives a confidence + conflict flag from the retrieved set (define run_python_node(...) returning a dict); bind prompt kb-answer on the Agent node draft_answer (pass question, policy_domain, retrieved_chunks); add a Router that branches on decision (answer | abstain | clarify), routing the non-answer branches to the output so they’re flagged for review. Click Save. (Routing uncertain runs to a human queue happens in the last step via Review Queues, by enqueuing the run’s trace id — there is no review-enqueue node in the palette.)
    Fill in
    retrieve_chunks (Knowledge Query)KB = policy-qa-kb, mode = hybrid
    score_confidence (Python Code)derive confidence + conflict flag from retrieved chunks
    draft_answer (Agent)bind kb-answer; inputs ← question / policy_domain / retrieved_chunks
    abstain_or_answer (Router)branch on decision = answer | abstain | clarify
    Click Save
    You'll see: Saved graph: knowledge_query → score_confidence → draft_answer(kb-answer) → router → output, with abstain/clarify branches surfaced for review.
    Screen snapshot
    caliber · Compose › Workflows › kb-assistant (Studio editor)
    Workflowskb-assistant (Studio editor)
    Save
  10. Compose › Workflows › kb-assistant › Runs creates · workflow runs: answer / abstain / clarify
    On the workflow detail page open the Runs tab. For each behavior, put the question in the run input box and click Run Pipeline (or “Queue Run”). An answerable question returns a cited answer; a missing-evidence question abstains with a missing_evidence list; the conflicting refund-window question retrieves both refund docs and returns clarify (not a silently-picked side).
    Fill in
    AnswerableHow long do we retain security audit logs?decision: answer, cites DATA-RETENTION-POLICY (365 days)
    Missing-evidenceWhat is the enterprise incident SLA, in minutes?decision: abstain + missing_evidence
    ConflictingHow many days do I have to request a refund?decision: clarify (30 vs 14 days)
    Click Run Pipeline
    You'll see: Three run rows under the Runs tab showing answer / abstain / clarify. Note their trace ids for evaluation + review.
    Screen snapshot
    caliber · Compose › Workflows › kb-assistant › Runs
    WorkflowsRuns
    Run Pipeline
  11. Evaluate › Judges › + New Judge creates · judge: CitationFaithfulness citation-faithfulness.judge.json ↓
    Create the LLM faithfulness judge. Click + New Judge and fill the fields. The Instructions must reference {{ inputs }} / {{ outputs }} / {{ expectations }} (use the insert buttons) and pass an answer only if every claim is cited, an abstain only if it declined with a missing_evidence list, and a clarify only if it surfaced the conflict. Set Returns to bool (pass/fail), then click Create judge.
    Fill in
    NameCitationFaithfulness
    Modelyour configured judge model (e.g. openai:/gpt-4o-mini / gateway default)
    Returnsbool
    Instructions (paste)Paste instructions from citation-faithfulness.judge.json (must reference {{ inputs }}/{{ outputs }}/{{ expectations }})
    Click Create judge
    You'll see: Judge CitationFaithfulness appears in the Judges list. It is a registered judge for tracing/assessments and — because the Evaluations scorer picker exposes a Custom LLM judges section — is also selectable as a Judge.<id> scorer on the Evaluations page, which you wire in as a grader two steps from now.
    Screen snapshot
    caliber · Evaluate › Judges › + New Judge
    Judges+ New Judge
    Create judge
  12. Evaluate › Test Sets › + New Test Set creates · test set: policy-qa-eval qa-eval.jsonl ↓
    Build the scored evaluation set. Click + New Test Set, give it a Name and Owner (both required), and click Create — the set is created empty. Then open its detail page at /eval-datasets/:id and author rows with + Add example: set each row's curated top-level expected substring that contains_expected reads (e.g. 365 days, AES-256) directly — these cannot be set through Observability's Add to test set (it auto-derives expected from the full trace response). Add each row from qa-eval.jsonl (each answerable row carries its curated expected substring), covering answer, clarify (Q05), and abstain (Q06/Q07) rows. (Prefer the API? POST the same rows to /eval-datasets/{id}/examples.)
    Fill in
    Namepolicy-qa-eval
    Owneryour handle (required, e.g. @you)
    Click Create
    You'll see: Dataset policy-qa-eval appears; at /eval-datasets/:id use + Add example to add its golden answer rows plus the clarify (Q05) and abstain (Q06/Q07) rows (each answerable row's curated expected substring) per qa-eval.jsonl (Q01–Q08).
    Screen snapshot
    caliber · Evaluate › Test Sets › + New Test Set
    Test Sets+ New Test Set
    Create
  13. Evaluate › Evaluations › Run evaluation creates · eval run: faithfulness + abstention
    Score faithfulness and abstention. Click Run evaluation to open the run panel, pick policy-qa-eval, and check the deterministic scorers — Contains expected (contains_expected) checks whether the gold substring in each row's top-level expected field appears in the prediction, and Token F1/Exact match sharpen it. The scorer reads the substring from expected (e.g. "365 days", "AES-256"); the answerable + clarify rows carry one, while the abstain rows (Q06/Q07) set expected: "" — so contains_expected is meaningful only on the answerable rows, and abstain/clarify behavior is judged by the decision rows, not by this substring scorer. Also tick the CitationFaithfulness judge from the prior step under Custom LLM judges so it runs as a Judge.<id> scorer for an automatic per-row faithfulness verdict. Click Run, read the scorecard, pin a baseline, and optionally compare a weaker prompt/mode in the compare view.
    Fill in
    Datasetpolicy-qa-eval
    Scorerscheck Contains expected (contains_expected); add Token F1 / Exact match as desired; tick CitationFaithfulness under Custom LLM judges
    Click Run
    You'll see: A scorecard with per-example pass/fail + overall, including the CitationFaithfulness judge column. Gate: abstain/clarify rows behave correctly (abstention_policy_compliance = 1.0); faithfulness from the judge column (faithfulness_min ≥ 0.90).
    Screen snapshot
    caliber · Evaluate › Evaluations › Run evaluation
    EvaluationsRun evaluation
    Run
  14. Observe › Review Queues › + New Queue creates · review queue: policy-qa-review
    Route the uncertain runs to human review. Click + New Queue, give it a name and one or more reviewer questions, and click Create queue. Then paste the trace ids of the abstain and clarify runs (from the workflow Runs tab) into the queue's enqueue box and click Enqueue. Answer the questions — answers write back onto the trace, closing the feedback loop.
    Fill in
    Namepolicy-qa-review
    Questionse.g. “Is the abstention justified by missing evidence?”, “Did the answer cite the right source?”
    Enqueue (trace ids)the abstain + clarify run trace ids, space/newline separated
    Click Create queue
    You'll see: A trace-linked review queue; enqueued traces appear as items, and submitted answers write back on each trace as assessments/expectations.
    Screen snapshot
    caliber · Observe › Review Queues › + New Queue
    Review Queues+ New Queue
    Create queue

Assets (copy-paste)

The exact files this cookbook uses — copy each into the matching field. Source: docs-site/cookbooks/06-knowledge-retrieval-policy-qa-lab/assets/.

assets/dataset/sources/refund-policy.md
# Refund Policy

Source id: REFUND-POLICY
Policy domain: billing
Owner: Finance Operations
Last reviewed: 2026-03-01

## Eligibility

Customers on a monthly or annual subscription may request a refund for the most
recent charge. To be eligible, the account must be in good standing (no open
billing disputes filed with a card network) at the time of the request.

## Refund window

A refund request must be submitted within **30 days** of the charge date.
Requests received after 30 days are reviewed on a case-by-case basis by Finance
Operations and are not guaranteed.

## How to request a refund

1. Sign in to the Billing portal at Account → Billing → Charges.
2. Locate the charge and select **Request refund**.
3. Choose a reason and submit. You will receive a confirmation email.

Refunds are issued to the original payment method. Card refunds typically settle
within 5–10 business days; the exact timing is set by the card issuer, not by us.

## Partial and prorated refunds

When a customer downgrades or cancels mid-cycle, the unused portion of an annual
plan is refunded on a prorated basis. Monthly plans are not prorated; service
continues until the end of the paid month.

## What is not refundable

One-time setup fees and usage-based overage charges that have already been
metered and invoiced are non-refundable.
assets/dataset/sources/refund-faq.md
# Refund FAQ (customer-facing)

Source id: REFUND-FAQ
Policy domain: billing
Owner: Support Enablement
Last reviewed: 2025-11-12

> NOTE: This FAQ is the deliberately CONTRADICTORY companion to
> `refund-policy.md`. It states a different refund window on purpose so the
> assistant's "sources conflict → clarify" path can be demonstrated. See
> `sources/README.md`.

## How long do I have to ask for a refund?

You can request a refund within **14 days** of your charge. After 14 days the
charge is considered final.

## How do I get my money back?

Open the Billing portal, find the charge, and choose **Request refund**. Refunds
go back to the card you paid with and usually arrive within about a week.

## Do annual plans get prorated?

Yes — if you cancel an annual plan partway through, we refund the unused months.

## Are setup fees refundable?

No. One-time setup fees are non-refundable.
assets/dataset/sources/security-policy.md
# Information Security Policy

Source id: SECURITY-POLICY
Policy domain: security
Owner: Security & Trust
Last reviewed: 2026-02-15

## Authentication

All employee and customer-admin accounts must use multi-factor authentication
(MFA). Password-only access is not permitted for any account that can change
billing, manage users, or access customer data. Single sign-on (SSO) via SAML or
OIDC is available on Enterprise plans and, when enabled, MFA is enforced by the
identity provider.

## Encryption

Customer data is encrypted in transit using TLS 1.2 or higher. Data at rest is
encrypted using AES-256. Encryption keys are managed in a dedicated key
management service and rotated at least every 12 months.

## Access control

Access to production systems follows least privilege and is granted on a
need-to-know basis. Production access requires an approved access request and is
reviewed quarterly. All production access is logged.

## Incident response

Suspected security incidents must be reported to the Security & Trust team
immediately. Confirmed incidents follow a documented response process:
containment, eradication, recovery, and post-incident review. Affected customers
are notified in line with the contractual and regulatory timelines in their
agreement.

## Vendor and subprocessor security

Third-party subprocessors are reviewed before onboarding and must meet our
security and data-protection requirements. The current subprocessor list is
maintained by Security & Trust and available on request.
assets/dataset/sources/data-retention-policy.md
# Data Retention Policy

Source id: DATA-RETENTION-POLICY
Policy domain: security
Owner: Security & Trust / Legal
Last reviewed: 2026-01-20

## Purpose

This policy defines how long different categories of data are retained and when
they are deleted. Retention periods balance operational need, customer
expectations, and legal or contractual obligations.

## Audit logs

Security and access audit logs are retained for **365 days**, after which they
are automatically purged. Audit logs may be retained longer only when required
by an active legal hold.

## Customer content

Customer content (documents, records, and uploads) is retained for the life of
the subscription. After an account is closed, customer content is deleted within
**30 days**, unless a longer period is required by contract or law.

## Backups

Encrypted backups are kept for **35 days** on a rolling basis and are then
overwritten. Deletion requests do not remove data from backups already taken;
that data ages out of the backup window naturally.

## Legal holds

When a legal hold is in effect, the affected data is exempt from automatic
deletion until the hold is released, regardless of the periods above.

## Deletion requests

Customers may request deletion of their personal data. Verified requests are
completed within 30 days for active systems; backups follow the rolling window
described above.
assets/dataset/calibration-questions.jsonl
{"id": "C01", "inputs": {"question": "How long are security audit logs retained before they are purged?"}, "expectations": {"sources": ["data-retention-policy.md"], "answer": "Audit logs are retained for 365 days, then automatically purged unless under a legal hold."}}
{"id": "C02", "inputs": {"question": "What authentication is required for accounts that can change billing or access customer data?"}, "expectations": {"sources": ["security-policy.md"], "answer": "Multi-factor authentication (MFA) is required; password-only access is not permitted for those accounts."}}
{"id": "C03", "inputs": {"question": "What encryption is used for customer data at rest and in transit?"}, "expectations": {"sources": ["security-policy.md"], "answer": "Data at rest is encrypted with AES-256; data in transit uses TLS 1.2 or higher."}}
{"id": "C04", "inputs": {"question": "What are the steps for a customer to request a refund?"}, "expectations": {"sources": ["refund-policy.md"], "answer": "Sign in to the Billing portal under Account > Billing > Charges, select the charge, choose Request refund, pick a reason, and submit."}}
{"id": "C05", "inputs": {"question": "How long are encrypted backups kept?"}, "expectations": {"sources": ["data-retention-policy.md"], "answer": "Encrypted backups are kept on a rolling 35-day basis and are then overwritten."}}
{"id": "C06", "inputs": {"question": "Are one-time setup fees refundable?"}, "expectations": {"sources": ["refund-policy.md"], "answer": "No. One-time setup fees are non-refundable."}}
assets/prompts/kb-answer.md
---
name: kb-answer
model_hint: a capable instruct model that follows JSON contracts and abstains reliably (grounded QA, not creative writing)
variables: [question, policy_domain, retrieved_chunks]
commit_message: "v1 grounded policy-QA answerer (cite-or-abstain, conflict-aware)"
---

You are a grounded policy assistant. You answer questions about internal company
policy using ONLY the evidence retrieved for this question. You never rely on
prior knowledge, assumptions, or anything outside `{{ retrieved_chunks }}`.

The question is scoped to the policy domain `{{ policy_domain }}` (e.g. billing,
security, support, legal). Stay within that scope.

## Evidence

`{{ retrieved_chunks }}` is a list of retrieved passages. Each chunk has an id
and a source id (e.g. `chunk_id`, `source_id` like `REFUND-POLICY`). Treat each
chunk as the only ground truth you may use. If a chunk is irrelevant to the
question, ignore it.

## Decision rules

Choose exactly one `decision`:

- **answer** — The retrieved chunks contain enough evidence to answer, and the
  evidence does not conflict. Every substantive claim in your answer MUST be
  backed by a citation to the specific chunk/source it came from.
- **abstain** — The retrieved chunks do NOT contain the evidence needed to
  answer (the topic is missing, or only tangentially mentioned), or the question
  asks for something outside documented policy (e.g. personal legal advice). Do
  not guess. State plainly that the documented policy does not cover it, and list
  what evidence would be needed in `missing_evidence`.
- **clarify** — The retrieved chunks CONFLICT on the fact being asked about
  (two sources give incompatible answers, e.g. a 30-day vs a 14-day window).
  Do not silently pick one. Present both positions with their citations and ask
  a single clarifying question so a human can resolve which source governs.

When in doubt between `answer` and `abstain`, prefer `abstain`. When sources
disagree, prefer `clarify` over `answer`. Never invent a citation, and never
cite a chunk or source id that is not present in `{{ retrieved_chunks }}`.

## Citations

- For `answer`: cite every substantive claim inline by source id and/or chunk id
  (e.g. "Refund requests must be filed within 30 days [REFUND-POLICY]."), and
  list each cited chunk in the `citations` array.
- For `clarify`: cite each conflicting source so the conflict is auditable.
- For `abstain`: `citations` is empty.

## Confidence

`confidence` is a number in [0,1] reflecting how well the retrieved evidence
supports your decision. Lower it when chunks are thin, partially on-topic, or
when you had to stretch. A low confidence on an `answer` is a signal for
downstream review routing; do not inflate it.

## Output

Return JSON ONLY — no prose, no markdown, no code fences — with exactly this
shape:

{
  "decision": one of ["answer", "abstain", "clarify"],
  "answer": string,            // the grounded answer, the conflict summary + clarifying question, or the abstention message
  "citations": [               // chunk/source references actually used; empty for abstain
    {"source_id": string, "chunk_id": string, "quote": short supporting snippet}
  ],
  "confidence": number,        // 0..1
  "missing_evidence": [string] // present (possibly empty) for abstain/clarify; what evidence would resolve it
}

Constraints:
- Valid JSON, strict-parseable, no trailing commas.
- Do not include any claim in `answer` that is not supported by a chunk in
  `citations` (for `answer`) — if you cannot cite it, do not say it.
- Keep `answer` concise and factual; quote/paraphrase the policy, do not editorialize.

Policy domain: {{ policy_domain }}
Question:
"""
{{ question }}
"""
Retrieved chunks:
"""
{{ retrieved_chunks }}
"""

Return only the JSON record.
assets/skills/contradiction-detector.md
---
name: contradiction-detector
summary: "Detect when retrieved policy/KB chunks disagree on the same fact and force a clarify-or-abstain instead of blending them; applies when answering FROM multiple retrieved evidence passages, NOT to single-source answers or free-form text."
category: compliance_safety
tags: [contradiction, conflict, grounding, abstention, clarify, policy-qa]
render_variables: [question, retrieved_chunks]
---

You inspect the retrieved evidence BEFORE an answer is written and decide whether
the sources agree. Your sole job is to catch conflicts so the assistant does not
silently blend incompatible facts into one confident-sounding answer.

Apply this only when answering from multiple retrieved chunks (policy/KB
question answering). It does not apply to single-source answers or to free-form
text — there is nothing to cross-check there.

## What counts as a contradiction

Two (or more) chunks contradict when they answer the SAME question with
incompatible facts, for example:

- Different numeric thresholds for the same rule (refund window of 30 days in one
  source, 14 days in another).
- Different durations, amounts, dates, or eligibility conditions for the same
  policy.
- One source permits what another forbids.

It is NOT a contradiction when chunks merely cover different topics, give
different levels of detail, or one is silent on the point. Stale-vs-current or
authoritative-vs-FAQ phrasing still counts as a conflict to surface — do not
assume which one wins.

## How to check

1. Identify the specific fact `{{ question }}` is asking for (the window, the
   retention period, the requirement, etc.).
2. Scan `{{ retrieved_chunks }}` for every chunk that asserts that fact.
3. Compare them. If two assert incompatible values for that same fact, it is a
   conflict.

## What to do

- **No conflict** → report `conflict: false`; the answerer proceeds normally
  (cite-or-abstain on its own merits).
- **Conflict found** → report `conflict: true` and force the downstream decision
  to `clarify` (or `abstain` if the conflict cannot be framed as a single
  question). List each conflicting source id and the value it asserts. Do NOT
  pick a winner and do NOT average the values.

## Output

Return JSON ONLY, strict-parseable, no code fences:

{
  "conflict": boolean,
  "fact_in_question": short string describing the fact being checked,
  "conflicting_sources": [
    {"source_id": string, "chunk_id": string, "asserted_value": string}
  ],
  "recommended_decision": one of ["proceed", "clarify", "abstain"]
}

`conflicting_sources` is empty when `conflict` is false. Only reference source
and chunk ids that actually appear in `{{ retrieved_chunks }}`.

Question:
"""
{{ question }}
"""
Retrieved chunks:
"""
{{ retrieved_chunks }}
"""

Return only the JSON record.
assets/skills/citation-and-next-steps.md
---
name: citation-and-next-steps
summary: "Format inline citations and propose concrete next steps for a grounded policy answer or abstention; applies to answering questions FROM retrieved policy/KB evidence, NOT to drafting free-form prose or summarizing untrusted text."
category: research
tags: [citation, grounding, abstention, next-steps, policy-qa]
render_variables: [decision, answer, citations]
---

You take a grounded policy result and make it auditable and actionable. You do
NOT change the underlying decision or the facts — the decision (`answer`,
`abstain`, or `clarify`) and the evidence arrive in `{{ decision }}`,
`{{ answer }}`, and `{{ citations }}`. Your job is to attach clean citations and
a single clear next step.

Apply this only when a result is grounded in retrieved evidence (policy/KB
question answering). It does not apply to free-form drafting, brainstorming, or
summarizing untrusted text — defer to the relevant skill there.

## Citation rules

- Cite by the source/chunk ids present in `{{ citations }}` (e.g.
  `[REFUND-POLICY]`, `[DATA-RETENTION-POLICY #chunk_3]`). Place the citation
  inline, right after the claim it supports.
- Cite ONLY ids that appear in `{{ citations }}`. Never invent a source id and
  never cite an id you were not given.
- One claim, one (or more) citation. If a sentence asserts two facts from two
  sources, cite both.
- If `{{ decision }}` is `answer` and a claim has no matching entry in
  `{{ citations }}`, do not state that claim — drop it rather than leave it
  uncited.

## Next-step rules

End with exactly one concrete next step, chosen by `{{ decision }}`:

- **answer** — Offer the most relevant follow-up the cited policy enables (e.g.
  "Want the step-by-step for requesting the refund in the Billing portal?").
- **abstain** — Name what is missing and the realistic path to resolve it (e.g.
  "I don't see this in the current policy set — I can route this to the policy
  owner to confirm."). Do not improvise an answer to fill the gap.
- **clarify** — Ask the single clarifying question that resolves the conflict
  (e.g. "Two sources disagree on the refund window (30 vs 14 days). Which one
  governs for this account — the policy or the customer FAQ?"). Ask one thing.

## Output

Produce only the finished, citation-annotated reply text plus its single next
step. No headers, no internal notes, no JSON, no commentary about these rules.

Decision: {{ decision }}
Citations available (source/chunk ids you may cite):
"""
{{ citations }}
"""
Draft answer to annotate:
"""
{{ answer }}
"""

Write the final cited reply with its next step now.
assets/judges/citation-faithfulness.judge.json
{
  "name": "CitationFaithfulness",
  "model": "set to your configured judge model (e.g. the gateway default)",
  "feedback_value_type": "bool",
  "instructions": "You audit a grounded policy assistant for citation faithfulness and correct abstention. You are given the original request {{ inputs }} (question, policy_domain), the model output {{ outputs }} (decision, answer, citations, confidence, missing_evidence), and the labeled {{ expectations }} (decision, must_cite, optional expected gold substring).\n\nReturn true ONLY if ALL of the following hold:\n1. {{ outputs }} is a single valid JSON object with a decision that is one of [answer, abstain, clarify], and its decision matches {{ expectations }}.decision.\n2. If {{ outputs }}.decision is \"answer\": every substantive claim in {{ outputs }}.answer is supported by an entry in {{ outputs }}.citations (a source_id/chunk_id that the answer actually relies on), there are no uncited factual claims, and no citation refers to a source that does not appear to back the claim. If {{ expectations }}.must_cite is true, {{ outputs }}.citations must be non-empty. If {{ expectations }}.expected is a non-empty string, {{ outputs }}.answer must contain that text (case-insensitive substring).\n3. If {{ outputs }}.decision is \"abstain\": the model did NOT assert a substantive policy answer, {{ outputs }}.citations is empty, and {{ outputs }}.missing_evidence names what evidence is missing. This is the required behavior when the retrieved evidence does not cover the question or the question is outside documented policy.\n4. If {{ outputs }}.decision is \"clarify\": the model surfaced a genuine conflict between sources rather than picking one, cited each conflicting source in {{ outputs }}.citations, and asked a clarifying question instead of asserting a single answer.\n5. The model abstained or asked to clarify whenever evidence was missing or sources conflicted (it did not fabricate an answer or a citation to fill a gap).\n\nOtherwise return false. Judge faithfulness and abstention discipline against the cited evidence and {{ expectations }} only — do not reward a fluent answer that is uncited, and do not penalize a correct abstention for 'not answering'.",
  "notes": "feedback_value_type bool -> pass/fail. This is the real LLM 'CitationFaithfulness' judge from verification.yaml (custom_judge). Select this judge in an Evaluations run under 'Custom LLM judges' (it runs as a Judge.<id> scorer) alongside the deterministic scorer 'contains_expected' (which reads the gold substring from the top-level expectations.expected for the answerable rows). Template vars {{ inputs }}/{{ outputs }}/{{ expectations }} are bound by the eval runner per row in dataset/qa-eval.jsonl. Gate target: faithfulness_min >= 0.90, abstention_policy_compliance = 1.0."
}
assets/dataset/qa-eval.jsonl
{"id": "Q01", "tags": ["golden", "answer"], "inputs": {"question": "How long do we retain security audit logs?", "policy_domain": "security"}, "expectations": {"decision": "answer", "must_cite": true, "expected": "365 days"}}
{"id": "Q02", "tags": ["golden", "answer"], "inputs": {"question": "Is multi-factor authentication required for admin accounts?", "policy_domain": "security"}, "expectations": {"decision": "answer", "must_cite": true, "expected": "MFA"}}
{"id": "Q03", "tags": ["golden", "answer"], "inputs": {"question": "How does a customer request a refund for a recent charge?", "policy_domain": "billing"}, "expectations": {"decision": "answer", "must_cite": true, "expected": "Billing portal"}}
{"id": "Q04", "tags": ["golden", "answer"], "inputs": {"question": "How is customer data encrypted at rest?", "policy_domain": "security"}, "expectations": {"decision": "answer", "must_cite": true, "expected": "AES-256"}}
{"id": "Q05", "tags": ["edge", "clarify", "contradiction"], "inputs": {"question": "How many days do I have to request a refund?", "policy_domain": "billing"}, "expectations": {"decision": "clarify", "must_cite": true, "expected": "30"}}
{"id": "Q06", "tags": ["edge", "abstain", "missing_evidence"], "inputs": {"question": "What is the guaranteed SLA response time for an enterprise incident, in minutes?", "policy_domain": "security"}, "expectations": {"decision": "abstain", "must_cite": false, "expected": ""}}
{"id": "Q07", "tags": ["negative", "abstain", "out_of_scope"], "inputs": {"question": "Can you give me legal advice on whether I can sue a competitor over this policy?", "policy_domain": "legal"}, "expectations": {"decision": "abstain", "must_cite": false, "expected": ""}}
{"id": "Q08", "tags": ["golden", "answer"], "inputs": {"question": "After an account is closed, when is the customer's content deleted?", "policy_domain": "security"}, "expectations": {"decision": "answer", "must_cite": true, "expected": "30 days"}}

Evaluation & quality gates

Quality gateTarget
Citation faithfulness≥ 0.90
Abstention policy compliance100% on missing/conflicting
Retrieval lift after calibrationRecall@k improves, faithfulness holds
Developer notes & gotchas
  • Explore sub-views are Query / Chunks / Graph. AGE retrieval needs a manual Sync to AGE first.
  • KB Calibrate runs inline and returns the four metrics immediately.
  • CitationFaithfulness is a real LLM judge; the abstain/clarify behavior is enforced by the prompt contract.

CALIBER : Contextual Adaptive Lifecycle for Intelligent Build, Evaluation, and Refinement — Cookbooks — every recipe is UI-implementable on the shipped platform. Source + assets under docs-site/cookbooks/<nn>-…/. Regenerate with python3 docs-site/cookbooks/training/build.py.