Fix follow-ups: explicit session-cached conversation + balanced decline rule
Browse files- Replace ChatInterface with a gr.Blocks chat that holds the conversation in the
Chatbot component (per-session cache) and explicitly passes the prior turns to
retrieval and the model — so multi-turn is controlled and verifiable (couldn't
confirm ChatInterface's history via the API, which strips it).
- Retrieval query now carries the last few user turns (not just one), so the
topic persists across several follow-ups.
- RULE 1 rebalanced: decline only when nothing relevant is present; answer when
it is (incl. follow-ups). Verified: maternity -> "financially how does it
change?" -> "and paternity leave?" all stay on topic and answer correctly;
car tax still declines; revenue/deficit correct.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- app/ollama_client.py +6 -5
- app/rag.py +5 -3
- app/ui.py +41 -22
|
@@ -39,11 +39,12 @@ def _extra() -> dict:
|
|
| 39 |
_SYSTEM = (
|
| 40 |
"You are a precise analyst of the Republic of Mauritius National Budget 2026-2027, answering from "
|
| 41 |
"the numbered budget excerpts in the latest message.\n"
|
| 42 |
-
"RULE 1 (most important):
|
| 43 |
-
"
|
| 44 |
-
"
|
| 45 |
-
"
|
| 46 |
-
"
|
|
|
|
| 47 |
"RULE 2: when the question IS covered, lead with the direct answer, cite the excerpts you use inline "
|
| 48 |
"like [1], and do NOT repeat the excerpt headers (the 'Document: …' lines).\n"
|
| 49 |
"RULE 3: be brief — answer in 1-2 short sentences. Quote figures exactly with their unit "
|
|
|
|
| 39 |
_SYSTEM = (
|
| 40 |
"You are a precise analyst of the Republic of Mauritius National Budget 2026-2027, answering from "
|
| 41 |
"the numbered budget excerpts in the latest message.\n"
|
| 42 |
+
"RULE 1 (most important): answer only from information actually present in the excerpts. If the "
|
| 43 |
+
"excerpts contain nothing relevant to the question's topic, reply that the budget documents provided "
|
| 44 |
+
"do not appear to address it, and stop — never answer using an excerpt about an unrelated topic "
|
| 45 |
+
"(e.g. do not answer about cars/vehicles using a plastic-bottle, sugar or tobacco excerpt) and never "
|
| 46 |
+
"invent, guess or estimate a figure. But when relevant information IS present, answer it fully, "
|
| 47 |
+
"including for follow-up questions about a topic already discussed in the conversation.\n"
|
| 48 |
"RULE 2: when the question IS covered, lead with the direct answer, cite the excerpts you use inline "
|
| 49 |
"like [1], and do NOT repeat the excerpt headers (the 'Document: …' lines).\n"
|
| 50 |
"RULE 3: be brief — answer in 1-2 short sentences. Quote figures exactly with their unit "
|
|
@@ -75,11 +75,13 @@ def _clean_history(history: list | None) -> list:
|
|
| 75 |
|
| 76 |
|
| 77 |
def _retrieval_query(message: str, history: list | None) -> str:
|
| 78 |
-
"""Prepend
|
| 79 |
-
|
|
|
|
| 80 |
prev = [m["content"] for m in (history or [])
|
| 81 |
if m.get("role") == "user" and isinstance(m.get("content"), str)]
|
| 82 |
-
|
|
|
|
| 83 |
|
| 84 |
|
| 85 |
def answer(message: str, history: list | None = None) -> Iterator[str]:
|
|
|
|
| 75 |
|
| 76 |
|
| 77 |
def _retrieval_query(message: str, history: list | None) -> str:
|
| 78 |
+
"""Prepend recent user turns so a terse follow-up still retrieves the right
|
| 79 |
+
passages (e.g. 'financially how does it change' -> keeps 'maternity'). Uses the
|
| 80 |
+
last few user turns so the topic persists across several follow-ups."""
|
| 81 |
prev = [m["content"] for m in (history or [])
|
| 82 |
if m.get("role") == "user" and isinstance(m.get("content"), str)]
|
| 83 |
+
recent = prev[-_HISTORY_TURNS:]
|
| 84 |
+
return (" ".join(recent + [message])).strip() if recent else message
|
| 85 |
|
| 86 |
|
| 87 |
def answer(message: str, history: list | None = None) -> Iterator[str]:
|
|
@@ -1,8 +1,10 @@
|
|
| 1 |
"""Gradio UI for the Mauritius Budget 2026-2027 RAG assistant.
|
| 2 |
|
| 3 |
-
|
| 4 |
-
conversation
|
| 5 |
-
|
|
|
|
|
|
|
| 6 |
from __future__ import annotations
|
| 7 |
|
| 8 |
import gradio as gr
|
|
@@ -13,13 +15,11 @@ EXAMPLES = [
|
|
| 13 |
"What is the projected total revenue for 2026-2027?",
|
| 14 |
"How much is allocated to education?",
|
| 15 |
"How long is maternity leave being extended to?",
|
| 16 |
-
"What is the projected budget deficit
|
| 17 |
-
"Summarise the macroeconomic outlook
|
| 18 |
"What are the main capital projects in the PSIP?",
|
| 19 |
]
|
| 20 |
|
| 21 |
-
# Mobile-first: cap content width on desktop, let it go full-bleed on phones,
|
| 22 |
-
# and make citation links stand out.
|
| 23 |
CSS = """
|
| 24 |
.gradio-container {max-width: 900px !important; margin: 0 auto !important}
|
| 25 |
#intro h1 {font-size: 1.4rem; margin-bottom: .2rem}
|
|
@@ -44,12 +44,26 @@ def _docs_markdown() -> str:
|
|
| 44 |
return "\n".join(lines)
|
| 45 |
|
| 46 |
|
| 47 |
-
def
|
|
|
|
| 48 |
message = (message or "").strip()
|
| 49 |
if not message:
|
| 50 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 51 |
return
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
|
| 55 |
def build_ui() -> gr.Blocks:
|
|
@@ -63,23 +77,28 @@ def build_ui() -> gr.Blocks:
|
|
| 63 |
f"<sub>Model: <code>{model}</code> · local Ollama · retrieval-augmented (RAG)</sub>",
|
| 64 |
elem_id="intro",
|
| 65 |
)
|
| 66 |
-
gr.
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
cache_examples=False,
|
| 70 |
-
chatbot=gr.Chatbot(
|
| 71 |
-
height=560,
|
| 72 |
-
render_markdown=True,
|
| 73 |
-
resizable=True,
|
| 74 |
-
latex_delimiters=[],
|
| 75 |
-
placeholder="Ask a question about the Mauritius 2026-2027 budget…",
|
| 76 |
-
),
|
| 77 |
-
fill_height=True,
|
| 78 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 79 |
with gr.Accordion("📄 Indexed documents", open=False):
|
| 80 |
gr.Markdown(_docs_markdown())
|
| 81 |
gr.Markdown(
|
| 82 |
"<sub>Source: Ministry of Finance, Economic Planning and Development, "
|
| 83 |
"Republic of Mauritius — National Budget 2026-2027.</sub>"
|
| 84 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 85 |
return demo
|
|
|
|
| 1 |
"""Gradio UI for the Mauritius Budget 2026-2027 RAG assistant.
|
| 2 |
|
| 3 |
+
A full-width, mobile-responsive chat built on explicit gr.Blocks state: the
|
| 4 |
+
conversation lives in the Chatbot component (a per-session cache), and the bot
|
| 5 |
+
handler passes the prior turns to retrieval + the model itself — so follow-up
|
| 6 |
+
questions reliably keep their context. Citations are clickable/hoverable; the
|
| 7 |
+
indexed-document list sits in a collapsible accordion at the bottom."""
|
| 8 |
from __future__ import annotations
|
| 9 |
|
| 10 |
import gradio as gr
|
|
|
|
| 15 |
"What is the projected total revenue for 2026-2027?",
|
| 16 |
"How much is allocated to education?",
|
| 17 |
"How long is maternity leave being extended to?",
|
| 18 |
+
"What is the projected budget deficit?",
|
| 19 |
+
"Summarise the macroeconomic outlook.",
|
| 20 |
"What are the main capital projects in the PSIP?",
|
| 21 |
]
|
| 22 |
|
|
|
|
|
|
|
| 23 |
CSS = """
|
| 24 |
.gradio-container {max-width: 900px !important; margin: 0 auto !important}
|
| 25 |
#intro h1 {font-size: 1.4rem; margin-bottom: .2rem}
|
|
|
|
| 44 |
return "\n".join(lines)
|
| 45 |
|
| 46 |
|
| 47 |
+
def _add_user(message: str, history: list):
|
| 48 |
+
"""Append the user's message to the conversation and clear the textbox."""
|
| 49 |
message = (message or "").strip()
|
| 50 |
if not message:
|
| 51 |
+
return history, ""
|
| 52 |
+
return history + [{"role": "user", "content": message}], ""
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def _bot_reply(history: list):
|
| 56 |
+
"""Stream the grounded answer, passing the PRIOR turns (the session cache)
|
| 57 |
+
to the RAG layer so follow-up questions keep their context."""
|
| 58 |
+
if not history or history[-1].get("role") != "user":
|
| 59 |
+
yield history
|
| 60 |
return
|
| 61 |
+
message = history[-1]["content"]
|
| 62 |
+
prior = history[:-1] # everything before this question
|
| 63 |
+
history = history + [{"role": "assistant", "content": ""}]
|
| 64 |
+
for partial in rag.answer(message, prior):
|
| 65 |
+
history[-1]["content"] = partial
|
| 66 |
+
yield history
|
| 67 |
|
| 68 |
|
| 69 |
def build_ui() -> gr.Blocks:
|
|
|
|
| 77 |
f"<sub>Model: <code>{model}</code> · local Ollama · retrieval-augmented (RAG)</sub>",
|
| 78 |
elem_id="intro",
|
| 79 |
)
|
| 80 |
+
chatbot = gr.Chatbot(
|
| 81 |
+
height=520, render_markdown=True, resizable=True,
|
| 82 |
+
placeholder="Ask a question about the 2026-2027 budget…",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 83 |
)
|
| 84 |
+
with gr.Row():
|
| 85 |
+
msg = gr.Textbox(
|
| 86 |
+
placeholder="e.g. What is the projected total revenue for 2026-2027?",
|
| 87 |
+
scale=8, show_label=False, autofocus=True, submit_btn=True,
|
| 88 |
+
)
|
| 89 |
+
clear = gr.Button("Clear", scale=1, variant="secondary")
|
| 90 |
+
|
| 91 |
+
gr.Examples(examples=EXAMPLES, inputs=msg, label="Try one")
|
| 92 |
+
|
| 93 |
with gr.Accordion("📄 Indexed documents", open=False):
|
| 94 |
gr.Markdown(_docs_markdown())
|
| 95 |
gr.Markdown(
|
| 96 |
"<sub>Source: Ministry of Finance, Economic Planning and Development, "
|
| 97 |
"Republic of Mauritius — National Budget 2026-2027.</sub>"
|
| 98 |
)
|
| 99 |
+
|
| 100 |
+
# Submit -> show the user message, then stream the grounded reply.
|
| 101 |
+
msg.submit(_add_user, [msg, chatbot], [chatbot, msg], queue=False).then(
|
| 102 |
+
_bot_reply, chatbot, chatbot)
|
| 103 |
+
clear.click(lambda: ([], ""), None, [chatbot, msg], queue=False)
|
| 104 |
return demo
|