kaburia commited on
Commit
c0a58c4
·
1 Parent(s): 5d99375
Files changed (1) hide show
  1. app.py +89 -16
app.py CHANGED
@@ -16,6 +16,7 @@ try:
16
  from utils.query_constraints import parse_query_constraints, page_matches, doc_matches
17
  from utils.conversation_logging import load_history, log_exchange
18
  from langchain.schema import Document
 
19
  except ImportError as e:
20
  print(f"Import error: {e}")
21
  print("Make sure you're running from the correct directory and all dependencies are installed.")
@@ -30,7 +31,49 @@ ENABLE_COHERENCE = True
30
  # Load persisted history (if any) for memory retention
31
  PERSISTED_HISTORY = load_history()
32
 
33
- def chat_response(message, history):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
34
  """
35
  Generate response for chat interface.
36
 
@@ -47,17 +90,28 @@ def chat_response(message, history):
47
  want_page = constraints.get("page")
48
  doc_tokens = constraints.get("doc_tokens", [])
49
 
50
- # Increase initial recall if a specific page is requested
51
- base_k = 120 if want_page is not None else 50
52
- reranked_results = retrieve_and_rerank(
53
- query_text=message,
54
- vectorstore=vectorstore,
55
- k=base_k,
56
- rerank_model="cross-encoder/ms-marco-MiniLM-L-6-v2",
57
- top_m=40 if want_page is not None else 20,
58
- min_score=0.4 if want_page is not None else 0.5, # relax threshold for page-constrained queries
59
- only_docs=False
60
- )
 
 
 
 
 
 
 
 
 
 
 
61
 
62
  if not reranked_results:
63
  return "I'm sorry, I couldn't find any relevant information in the policy documents to answer your question. Could you try rephrasing your question or asking about a different topic?"
@@ -96,6 +150,24 @@ def chat_response(message, history):
96
 
97
  top_docs = [doc for doc, score in reranked_results]
98
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
99
  # Perform sentiment and coherence analysis if enabled
100
  sentiment_rollup = get_sentiment(top_docs) if ENABLE_SENTIMENT else {}
101
  coherence_report_ = coherence_report(reranked_results=top_docs, input_text=message) if ENABLE_COHERENCE else ""
@@ -274,9 +346,9 @@ with gr.Blocks(title="Kenya Policy Assistant - Chat", theme=gr.themes.Soft()) as
274
  )
275
 
276
  # Chat functionality
277
- def respond(message, history):
278
  if message.strip():
279
- bot_message = chat_response(message, history)
280
  history.append([message, ""])
281
 
282
  for partial_response in bot_message:
@@ -285,8 +357,9 @@ with gr.Blocks(title="Kenya Policy Assistant - Chat", theme=gr.themes.Soft()) as
285
  else:
286
  yield history, ""
287
 
288
- submit_btn.click(respond, [msg, chatbot], [chatbot, msg])
289
- msg.submit(respond, [msg, chatbot], [chatbot, msg])
 
290
  clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg])
291
 
292
  # Update settings when toggles change
 
16
  from utils.query_constraints import parse_query_constraints, page_matches, doc_matches
17
  from utils.conversation_logging import load_history, log_exchange
18
  from langchain.schema import Document
19
+ from utils.hybrid_retrieval import HybridRetriever, consolidate_page
20
  except ImportError as e:
21
  print(f"Import error: {e}")
22
  print("Make sure you're running from the correct directory and all dependencies are installed.")
 
31
  # Load persisted history (if any) for memory retention
32
  PERSISTED_HISTORY = load_history()
33
 
34
+ # Default verbatim mode flag (quotes only, no generative summarization)
35
+ VERBATIM_MODE_DEFAULT = True
36
+
37
+ def _citation(meta):
38
+ src = os.path.basename(meta.get('source','Unknown'))
39
+ page = meta.get('page_label') or meta.get('page') or 'unknown'
40
+ return f"({src} p.{page})"
41
+
42
+ def _extract_quotes(query: str, docs, max_quotes: int = 12):
43
+ import re, math
44
+ terms = [t.lower() for t in re.findall(r"[A-Za-z0-9]+", query) if len(t) > 2]
45
+ term_set = set(terms)
46
+ scored = []
47
+ for d in docs:
48
+ meta = getattr(d,'metadata',{})
49
+ # split on sentence end punctuation heuristically
50
+ sentences = re.split(r"(?<=[\.!?])\s+", d.page_content)
51
+ for sent in sentences:
52
+ s = sent.strip()
53
+ if not s:
54
+ continue
55
+ toks = [w.lower() for w in re.findall(r"[A-Za-z0-9]+", s)]
56
+ if not toks:
57
+ continue
58
+ overlap = len(term_set.intersection(toks))
59
+ if overlap == 0:
60
+ continue
61
+ score = overlap / math.log(len(toks)+1, 2)
62
+ scored.append((score, s, meta))
63
+ scored.sort(key=lambda x: x[0], reverse=True)
64
+ out = []
65
+ seen = set()
66
+ for score, s, meta in scored:
67
+ key = (s, meta.get('source'), meta.get('page_label'))
68
+ if key in seen:
69
+ continue
70
+ seen.add(key)
71
+ out.append(f"• \"{s}\" {_citation(meta)}")
72
+ if len(out) >= max_quotes:
73
+ break
74
+ return out
75
+
76
+ def chat_response(message, history, verbatim_mode=True):
77
  """
78
  Generate response for chat interface.
79
 
 
90
  want_page = constraints.get("page")
91
  doc_tokens = constraints.get("doc_tokens", [])
92
 
93
+ # Attempt hybrid deterministic retrieval first
94
+ reranked_results = []
95
+ try:
96
+ hybrid = HybridRetriever(vectorstore)
97
+ filters = {}
98
+ if want_page is not None:
99
+ filters['page_label'] = str(want_page)
100
+ hybrid_docs = hybrid.fetch(message, k_dense=40, k_bm25=40, filters=filters if filters else None, rerank_top=30)
101
+ reranked_results = [(d, 0.0) for d in hybrid_docs]
102
+ except Exception:
103
+ pass
104
+ if not reranked_results: # fallback legacy path
105
+ base_k = 120 if want_page is not None else 50
106
+ reranked_results = retrieve_and_rerank(
107
+ query_text=message,
108
+ vectorstore=vectorstore,
109
+ k=base_k,
110
+ rerank_model="cross-encoder/ms-marco-MiniLM-L-6-v2",
111
+ top_m=40 if want_page is not None else 20,
112
+ min_score=0.4 if want_page is not None else 0.5,
113
+ only_docs=False
114
+ )
115
 
116
  if not reranked_results:
117
  return "I'm sorry, I couldn't find any relevant information in the policy documents to answer your question. Could you try rephrasing your question or asking about a different topic?"
 
150
 
151
  top_docs = [doc for doc, score in reranked_results]
152
 
153
+ # If single page requested consolidate all fragments so quotes not truncated
154
+ if want_page is not None:
155
+ consolidated = consolidate_page(top_docs, str(want_page))
156
+ if consolidated:
157
+ top_docs = consolidated
158
+
159
+ if verbatim_mode:
160
+ quotes = _extract_quotes(message, top_docs)
161
+ if not quotes:
162
+ return "Not found in sources."
163
+ answer = "Quoted Policy Excerpts (verbatim)\n" + "\n".join(quotes)
164
+ yield answer
165
+ try:
166
+ log_exchange(message, answer, meta={"mode":"verbatim","page": want_page})
167
+ except Exception:
168
+ pass
169
+ return
170
+
171
  # Perform sentiment and coherence analysis if enabled
172
  sentiment_rollup = get_sentiment(top_docs) if ENABLE_SENTIMENT else {}
173
  coherence_report_ = coherence_report(reranked_results=top_docs, input_text=message) if ENABLE_COHERENCE else ""
 
346
  )
347
 
348
  # Chat functionality
349
+ def respond(message, history, verbatim_mode):
350
  if message.strip():
351
+ bot_message = chat_response(message, history, verbatim_mode=verbatim_mode)
352
  history.append([message, ""])
353
 
354
  for partial_response in bot_message:
 
357
  else:
358
  yield history, ""
359
 
360
+ verbatim_toggle = gr.Checkbox(label="Verbatim Mode", value=VERBATIM_MODE_DEFAULT, info="Return only exact quoted excerpts (no generation).")
361
+ submit_btn.click(respond, [msg, chatbot, verbatim_toggle], [chatbot, msg])
362
+ msg.submit(respond, [msg, chatbot, verbatim_toggle], [chatbot, msg])
363
  clear_btn.click(lambda: ([], ""), outputs=[chatbot, msg])
364
 
365
  # Update settings when toggles change