Course ES
← back to chapter

Grounded refuse

RAG's real value is refusing what the documents don't support. The corpus is a

Showcase — Grounded refuse

RAG's real value is refusing what the documents don't support. The corpus is a narrow HR policy. Ask about PTO, sick leave, remote work, or expenses and it answers with a citation; ask about the stock price or the weather and it replies "That isn't covered in the HR policies I have" instead of improvising. The refusal is the guardrail against confident hallucination.

Run

bash bootstrap-secrets.sh              # reads ../../../../.env, writes secrets/
docker compose up --build              # default: PROVIDER=openai

Open http://localhost:3000. Gemini: PROVIDER=gemini docker compose up --build.

What's where

  • backend/store.py — the shared VectorStore.
  • backend/ai_openai.py / backend/ai_gemini.py — the narrow policy KB and a system prompt with a hard refusal rule.
  • frontend/app/page.tsx — question box; try in-scope and out-of-scope questions.

Stop

docker compose down

Run locally

Download the project as a ZIP and run it with Docker. Brings up a FastAPI backend + Next.js frontend on localhost:3000.

Download grounded-refuse.zip

unzip grounded-refuse.zip
cd grounded-refuse
bash bootstrap-secrets.sh   # one-time: pulls API keys into ./secrets
docker compose up --build   # default provider: openai
# or:  PROVIDER=gemini docker compose up --build

Type some input, pick a provider, and run the same code shown in Source against the live API. Sign-in required.


  

The same modules the Run button hits. The whole project (frontend, Dockerfile, compose) is in the ZIP under README.

backend/ai_openai.py

"""Showcase 2 (OpenAI): grounded answers that refuse when unsupported.

RAG's real value isn't answering — it's REFUSING to answer what the documents
don't support. The corpus here is a narrow HR policy. Ask about leave or
expenses and it answers with a citation; ask about anything else — the stock
price, the weather, last night's game — and it declines instead of improvising.
That refusal is the guardrail against confident hallucination.
"""
from openai import OpenAI

from store import VectorStore

_client = OpenAI()

_EMBED_MODEL = "text-embedding-3-small"
_CHAT_MODEL = "gpt-5.4-nano"

_KB = [
    {"id": "pto", "text": "Full-time employees accrue 15 days of paid time off per year, rolling over up to 5 days."},
    {"id": "sick", "text": "Sick leave is separate from PTO: 8 paid sick days per year, no rollover."},
    {"id": "remote", "text": "Employees may work remotely up to 3 days per week with manager approval."},
    {"id": "expenses", "text": "Reimbursable expenses must be submitted within 30 days with a receipt via the Expenses portal."},
    {"id": "parental", "text": "Parental leave is 12 weeks paid, available to all parents within a year of the birth or adoption."},
]

_SYSTEM = (
    "You are an HR policy assistant. Answer strictly from the policy context "
    "below and cite the [id] you used. If the answer is not in the context, "
    "reply exactly: 'That isn't covered in the HR policies I have.' Do not guess "
    "and do not use outside knowledge.\n\nContext:\n{context}"
)

_store: VectorStore | None = None


def _embed(texts):
    return [item.embedding for item in _client.embeddings.create(model=_EMBED_MODEL, input=texts).data]


def _get_store() -> VectorStore:
    global _store
    if _store is None:
        _store = VectorStore(_embed)
        _store.add(_KB)
    return _store


def run(question: str) -> str:
    hits = _get_store().search(question.strip(), k=3)
    context = "\n".join(f"[{item['id']}] {item['text']}" for _, item in hits)
    response = _client.responses.create(
        model=_CHAT_MODEL,
        instructions=_SYSTEM.format(context=context),
        input=[{"role": "user", "content": question.strip()}],
    )
    return response.output_text

backend/ai_gemini.py

"""Showcase 2 (Gemini): grounded answers that refuse when unsupported.

Same narrow policy corpus and the same hard refusal rule, on Gemini.
"""
import os

from google import genai
from google.genai import types

from store import VectorStore

_client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])

_EMBED_MODEL = "gemini-embedding-001"
_CHAT_MODEL = "gemini-3.1-flash-lite"

_KB = [
    {"id": "pto", "text": "Full-time employees accrue 15 days of paid time off per year, rolling over up to 5 days."},
    {"id": "sick", "text": "Sick leave is separate from PTO: 8 paid sick days per year, no rollover."},
    {"id": "remote", "text": "Employees may work remotely up to 3 days per week with manager approval."},
    {"id": "expenses", "text": "Reimbursable expenses must be submitted within 30 days with a receipt via the Expenses portal."},
    {"id": "parental", "text": "Parental leave is 12 weeks paid, available to all parents within a year of the birth or adoption."},
]

_SYSTEM = (
    "You are an HR policy assistant. Answer strictly from the policy context "
    "below and cite the [id] you used. If the answer is not in the context, "
    "reply exactly: 'That isn't covered in the HR policies I have.' Do not guess "
    "and do not use outside knowledge.\n\nContext:\n{context}"
)

_store: VectorStore | None = None


def _embed(texts):
    return [e.values for e in _client.models.embed_content(model=_EMBED_MODEL, contents=texts).embeddings]


def _get_store() -> VectorStore:
    global _store
    if _store is None:
        _store = VectorStore(_embed)
        _store.add(_KB)
    return _store


def run(question: str) -> str:
    hits = _get_store().search(question.strip(), k=3)
    context = "\n".join(f"[{item['id']}] {item['text']}" for _, item in hits)
    response = _client.models.generate_content(
        model=_CHAT_MODEL,
        contents=[types.Content(role="user", parts=[types.Part(text=question.strip())])],
        config=types.GenerateContentConfig(system_instruction=_SYSTEM.format(context=context)),
    )
    return response.text or ""

Project files

  • .gitignore
  • README.es.md
  • README.md
  • backend/Dockerfile
  • backend/ai_gemini.py
  • backend/ai_openai.py
  • backend/main.py
  • backend/requirements.txt
  • backend/store.py
  • bootstrap-secrets.sh
  • docker-compose.yml
  • frontend/Dockerfile
  • frontend/app/layout.tsx
  • frontend/app/page.tsx
  • frontend/next.config.ts
  • frontend/package.json
  • frontend/tsconfig.json