SamDNX Claude Opus 4.8 commited on
Commit
8f702fb
·
1 Parent(s): a7b3b56

Fix grounding: qwen2.5:1.5b + embedding re-rank + topic-relevance prompt

Browse files

The 0.5b model hallucinated on questions whose answer isn't the top BM25 hit
(e.g. invented a "car importation tax" figure, parroted excerpt headers). Fixes:
- Re-enable embedding re-rank (auto) — bridges lexical gaps (car tax -> excise/
customs); chunks pre-warmed in a background thread so queries stay fast.
- qwen2.5:1.5b: grounds reliably where 1b/0.5b hallucinated (e.g. deficit).
- Prompt now requires same-subject excerpts and to decline (not invent) when the
topic isn't covered. Verified: car tax correctly declines; maternity/revenue/
deficit/tobacco/motor-vehicle measures all correct.
- Restore recall (top_k 4, budget 4000, num_ctx 4096); EMBED_MODEL pulled at boot.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

Files changed (6) hide show
  1. Dockerfile +2 -2
  2. README.md +1 -1
  3. app/ollama_client.py +9 -5
  4. app/rag.py +3 -1
  5. app/retrieval.py +16 -0
  6. config.yaml +4 -4
Dockerfile CHANGED
@@ -20,9 +20,9 @@ ENV HOME=/home/user \
20
  HOST=0.0.0.0 \
21
  PORT=7860 \
22
  OLLAMA_URL=http://localhost:11434 \
23
- OLLAMA_MODEL=qwen2.5:0.5b \
 
24
  INDEX_PATH=/tmp/mru_budget_index.parquet \
25
- RERANK=off \
26
  OMP_NUM_THREADS=2 \
27
  OLLAMA_NUM_PARALLEL=1
28
 
 
20
  HOST=0.0.0.0 \
21
  PORT=7860 \
22
  OLLAMA_URL=http://localhost:11434 \
23
+ OLLAMA_MODEL=qwen2.5:1.5b \
24
+ EMBED_MODEL=nomic-embed-text \
25
  INDEX_PATH=/tmp/mru_budget_index.parquet \
 
26
  OMP_NUM_THREADS=2 \
27
  OLLAMA_NUM_PARALLEL=1
28
 
README.md CHANGED
@@ -34,7 +34,7 @@ embeddings** for retrieval, **Ollama** for the LLM, and **Gradio** for the UI.
34
  Requires [Ollama](https://ollama.com) running on your machine.
35
 
36
  ```bash
37
- ollama pull qwen2.5:0.5b # the answer model (any chat model works)
38
  ollama pull nomic-embed-text # optional: enables embedding re-rank
39
  pip install -r requirements.txt
40
  python main.py # serves http://127.0.0.1:7860
 
34
  Requires [Ollama](https://ollama.com) running on your machine.
35
 
36
  ```bash
37
+ ollama pull qwen2.5:1.5b # the answer model (any chat model works)
38
  ollama pull nomic-embed-text # optional: enables embedding re-rank
39
  pip install -r requirements.txt
40
  python main.py # serves http://127.0.0.1:7860
app/ollama_client.py CHANGED
@@ -38,12 +38,16 @@ def _extra() -> dict:
38
 
39
  _SYSTEM = (
40
  "You are a precise analyst of the Republic of Mauritius National Budget 2026-2027. "
41
- "Answer the user's question using ONLY the budget excerpts in the latest message. "
42
- "Each excerpt is prefixed with a number like [1]; cite the ones you use inline, e.g. [1]. "
43
- "Monetary figures are in Mauritian Rupees (Rs) — quote them exactly with their unit "
44
- "(million/billion). Be concise and direct: lead with the answer, no preamble. "
 
 
45
  "Use earlier conversation turns to resolve follow-up questions (e.g. 'how does it change' "
46
- "refers to the topic just discussed). If the excerpts do not contain the answer, say so plainly."
 
 
47
  )
48
 
49
 
 
38
 
39
  _SYSTEM = (
40
  "You are a precise analyst of the Republic of Mauritius National Budget 2026-2027. "
41
+ "Answer the user's question using ONLY the numbered budget excerpts in the latest message. "
42
+ "Before using an excerpt, check it is about the SAME subject as the question — e.g. do NOT answer "
43
+ "a question about cars or vehicles using an excerpt about plastic bottles or sugar. "
44
+ "Cite the excerpts you actually use inline, e.g. [1]. Monetary figures are in Mauritian Rupees "
45
+ "(Rs) — quote them exactly with their unit (million/billion). Be concise and direct: lead with "
46
+ "the answer, no preamble, and do NOT repeat the excerpt headers (the 'Document: …' lines). "
47
  "Use earlier conversation turns to resolve follow-up questions (e.g. 'how does it change' "
48
+ "refers to the topic just discussed). If no excerpt specifically addresses the asked subject, "
49
+ "reply that the budget documents provided do not appear to address it — NEVER invent, guess or "
50
+ "estimate a figure."
51
  )
52
 
53
 
app/rag.py CHANGED
@@ -13,7 +13,7 @@ from typing import Iterator
13
  from app.config import CONFIG
14
  from app.ingest import build_index, corpus_summary
15
  from app.ollama_client import chat_stream, pick_model, warm_model
16
- from app.retrieval import build_context
17
 
18
  log = logging.getLogger("rag")
19
 
@@ -31,6 +31,8 @@ def init() -> None:
31
  _STATE["summary"] = corpus_summary(df)
32
  if CONFIG["ollama"].get("warm_on_start", True):
33
  threading.Thread(target=warm_model, daemon=True).start()
 
 
34
 
35
 
36
  def summary() -> list[dict]:
 
13
  from app.config import CONFIG
14
  from app.ingest import build_index, corpus_summary
15
  from app.ollama_client import chat_stream, pick_model, warm_model
16
+ from app.retrieval import build_context, prewarm_embeddings
17
 
18
  log = logging.getLogger("rag")
19
 
 
31
  _STATE["summary"] = corpus_summary(df)
32
  if CONFIG["ollama"].get("warm_on_start", True):
33
  threading.Thread(target=warm_model, daemon=True).start()
34
+ # Embed all chunks in the background so re-ranked queries are fast.
35
+ threading.Thread(target=prewarm_embeddings, args=(df,), daemon=True).start()
36
 
37
 
38
  def summary() -> list[dict]:
app/retrieval.py CHANGED
@@ -65,6 +65,22 @@ def _embed_docs(texts: list, model: str) -> list:
65
  return [_emb_store[k] for k in keys]
66
 
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  def _rerank(question: str, rows: list, idxs: list) -> list:
69
  import numpy as np
70
 
 
65
  return [_emb_store[k] for k in keys]
66
 
67
 
68
+ def prewarm_embeddings(df: pd.DataFrame) -> None:
69
+ """Embed every chunk up front (in batches) so the first re-ranked queries
70
+ don't pay a per-chunk embedding cost. No-op when re-rank is disabled."""
71
+ if not _embeddings_enabled():
72
+ return
73
+ model = _R.get("embed_model", "nomic-embed-text")
74
+ texts = [(r["text"][:1500] or "_") for r in df.to_dict("records")]
75
+ B = 64
76
+ try:
77
+ for i in range(0, len(texts), B):
78
+ _embed_docs(texts[i:i + B], model)
79
+ log.info("prewarmed %d chunk embeddings", len(texts))
80
+ except Exception as e:
81
+ log.warning("embedding prewarm stopped early: %s", e)
82
+
83
+
84
  def _rerank(question: str, rows: list, idxs: list) -> list:
85
  import numpy as np
86
 
config.yaml CHANGED
@@ -15,15 +15,15 @@ ollama:
15
  model: null # null = first installed model
16
  request_timeout: 300
17
  keep_alive: 30m # keep the model resident between questions
18
- num_ctx: 2048 # context window: smaller = much faster prefill on CPU
19
  num_predict: 400 # cap answer tokens so long answers don't stall on CPU
20
  temperature: 0 # greedy decoding: far more reliable for factual extraction
21
  warm_on_start: true # preload the model at startup so the first answer is fast
22
  history_turns: 4 # prior chat messages fed back for follow-up questions
23
 
24
  retrieval:
25
- top_k: 3
26
- context_budget: 2600 # chars of excerpt context handed to the model (smaller = faster prefill on CPU)
27
- rerank: off # embedding re-rank of BM25 hits: auto | on | off (off = no extra round-trip)
28
  embed_model: nomic-embed-text # Ollama embedding model (auto-used only if installed)
29
  rerank_k: 30 # BM25 candidates to re-rank by embedding similarity
 
15
  model: null # null = first installed model
16
  request_timeout: 300
17
  keep_alive: 30m # keep the model resident between questions
18
+ num_ctx: 4096 # context window: fits a few re-ranked excerpts + answer
19
  num_predict: 400 # cap answer tokens so long answers don't stall on CPU
20
  temperature: 0 # greedy decoding: far more reliable for factual extraction
21
  warm_on_start: true # preload the model at startup so the first answer is fast
22
  history_turns: 4 # prior chat messages fed back for follow-up questions
23
 
24
  retrieval:
25
+ top_k: 4
26
+ context_budget: 4000 # chars of excerpt context handed to the model
27
+ rerank: auto # embedding re-rank of BM25 hits: auto | on | off (auto = on when embed model installed)
28
  embed_model: nomic-embed-text # Ollama embedding model (auto-used only if installed)
29
  rerank_k: 30 # BM25 candidates to re-rank by embedding similarity