SamDNX Claude Opus 4.8 commited on
Commit
74b54ea
·
1 Parent(s): 6d71be2

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>

Files changed (3) hide show
  1. app/ollama_client.py +6 -5
  2. app/rag.py +5 -3
  3. app/ui.py +41 -22
app/ollama_client.py CHANGED
@@ -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): use an excerpt ONLY if it is specifically about the SAME subject as the "
43
- "question. If none of the excerpts specifically address the asked subject, reply that the budget "
44
- "documents provided do not appear to address it, and stop — it is better to decline than to answer "
45
- "using an excerpt about a different subject (e.g. never answer about cars/vehicles using a "
46
- "plastic-bottle, sugar or tobacco excerpt). NEVER invent, guess or estimate a figure.\n"
 
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 "
app/rag.py CHANGED
@@ -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 the most recent user turn so a terse follow-up still retrieves the
79
- right passages (e.g. 'financially how does it change' -> includes 'maternity')."""
 
80
  prev = [m["content"] for m in (history or [])
81
  if m.get("role") == "user" and isinstance(m.get("content"), str)]
82
- return f"{prev[-1]} {message}".strip() if prev else message
 
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]:
app/ui.py CHANGED
@@ -1,8 +1,10 @@
1
  """Gradio UI for the Mauritius Budget 2026-2027 RAG assistant.
2
 
3
- Pure-Python front end: a full-width, mobile-responsive streaming chat that keeps
4
- conversation context, with clickable/hoverable source citations and a collapsible
5
- list of indexed documents at the bottom."""
 
 
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 and how is it financed?",
17
- "Summarise the macroeconomic outlook and growth forecast.",
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 _respond(message, history):
 
48
  message = (message or "").strip()
49
  if not message:
50
- yield "Please type a question about the 2026-2027 budget."
 
 
 
 
 
 
 
 
51
  return
52
- yield from rag.answer(message, history)
 
 
 
 
 
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.ChatInterface(
67
- fn=_respond,
68
- examples=EXAMPLES,
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