Rutvij07 commited on
Commit
f8ff263
·
1 Parent(s): 25e2a5d

improved chat

Browse files
Files changed (2) hide show
  1. app.py +99 -17
  2. scripts/mintoak/chat_server.py +54 -9
app.py CHANGED
@@ -103,16 +103,10 @@ PREAMBLE_RES = [
103
  ]
104
 
105
  OPTIMIZER_PROMPT = (
106
- "You are a search query optimizer. Your job is to translate user questions into database search keywords.\n"
107
  "Translate synonyms into our core product areas: 'onboarding/acquisition' -> 'DigiOnboard', "
108
- "'payments' -> 'SmartPayments', 'voice/soundbox' -> 'SoundHub', 'cross-sell' -> 'SellSmart'.\n"
109
- "Output ONLY the keywords, separated by spaces. Do not write explanations or punctuation.\n\n"
110
- "Examples:\n"
111
- "User: Tell me about Mintoak's loyalty program -> Keywords: loyalty program\n"
112
- "User: How to onboard a merchant? -> Keywords: DigiOnboard merchant onboarding\n"
113
- "User: What is the soundbox device? -> Keywords: SoundHub soundbox device\n"
114
- "User: How do they accept payments? -> Keywords: SmartPayments accept payments\n"
115
- "User: Tell me about business360 -> Keywords: business360"
116
  )
117
 
118
  SYSTEM_PROMPT = (
@@ -148,7 +142,8 @@ QUERY_ENHANCEMENT_RULES = [
148
  },
149
  {
150
  "keywords": ["loyalty", "reward", "rewards", "campaign", "rewardrun"],
151
- "expansion": "Mintoak RewardRun gamified loyalty campaigns merchant activation retention",
 
152
  "prompt_note": "Ensure you mention Mintoak RewardRun as the gamified loyalty and merchant engagement platform.",
153
  "top_k": 3
154
  }
@@ -156,6 +151,31 @@ QUERY_ENHANCEMENT_RULES = [
156
 
157
  MAX_CHUNK_CHARS = 500
158
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  def retrieve_context(query: str, top_k: int = 2):
160
  query_lower = query.lower()
161
  search_keywords = None
@@ -164,7 +184,10 @@ def retrieve_context(query: str, top_k: int = 2):
164
  # Step 1: Check config-driven expansion registry first
165
  for rule in QUERY_ENHANCEMENT_RULES:
166
  if any(kw in query_lower for kw in rule["keywords"]):
167
- search_keywords = f"{query} {rule['expansion']}"
 
 
 
168
  top_k = rule["top_k"]
169
  prompt_note = rule.get("prompt_note")
170
  break
@@ -173,6 +196,16 @@ def retrieve_context(query: str, top_k: int = 2):
173
  if not search_keywords:
174
  messages = [
175
  {"role": "system", "content": OPTIMIZER_PROMPT},
 
 
 
 
 
 
 
 
 
 
176
  {"role": "user", "content": query}
177
  ]
178
  prompt = _tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
@@ -231,9 +264,34 @@ def retrieve_context(query: str, top_k: int = 2):
231
 
232
  return "\n\n---\n\n".join(context_parts), url_to_title, prompt_note
233
 
234
- def clean_response(text: str) -> str:
235
  for rx in PREAMBLE_RES:
236
  text = rx.sub("", text).strip()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
237
  if text and text[0].islower():
238
  text = text[0].upper() + text[1:]
239
  return text
@@ -330,6 +388,7 @@ def index():
330
  def chat():
331
  data = request.get_json(force=True)
332
  query = (data.get("query") or "").strip()
 
333
  if not query:
334
  return Response("data: " + json.dumps({"error": "Empty query"}) + "\n\n", mimetype="text/event-stream")
335
 
@@ -372,14 +431,22 @@ def chat():
372
  def stream_rag():
373
  yield f"data: {json.dumps({'event': 'sources', 'sources': sources})}\n\n"
374
 
 
 
 
375
  user_content = f"Context:\n{context}\n\nQuestion: {query}"
376
  if prompt_note:
377
  user_content += f"\n\nNote: {prompt_note}"
378
 
379
- messages = [
380
- {"role": "system", "content": SYSTEM_PROMPT},
381
- {"role": "user", "content": user_content}
382
- ]
 
 
 
 
 
383
  prompt = _tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
384
 
385
  t0 = time.time()
@@ -403,21 +470,36 @@ def chat():
403
  thread.start()
404
 
405
  citation_started = False
 
406
  for token_text in streamer:
407
  full_response += token_text
408
 
409
  if citation_started:
 
 
 
410
  continue
411
 
412
  last_snippet = full_response[-40:].lower()
413
  if "👉" in token_text or "for more details" in last_snippet or "details, visit" in last_snippet:
414
  citation_started = True
415
  continue
 
 
 
 
 
 
416
 
417
  yield f"data: {json.dumps({'event': 'token', 'token': token_text})}\n\n"
 
 
 
 
 
418
 
419
  latency = round(time.time() - t0, 2)
420
- cleaned_response = clean_response(full_response.strip())
421
 
422
  # If references were not generated, append them
423
  if "Source:" not in cleaned_response and sources:
 
103
  ]
104
 
105
  OPTIMIZER_PROMPT = (
106
+ "You are a search query optimizer. Your job is to translate user questions into database search keywords. "
107
  "Translate synonyms into our core product areas: 'onboarding/acquisition' -> 'DigiOnboard', "
108
+ "'payments' -> 'SmartPayments', 'voice/soundbox' -> 'SoundHub', 'cross-sell' -> 'SellSmart'. "
109
+ "Output ONLY the keywords, separated by spaces. Do not write explanations or punctuation."
 
 
 
 
 
 
110
  )
111
 
112
  SYSTEM_PROMPT = (
 
142
  },
143
  {
144
  "keywords": ["loyalty", "reward", "rewards", "campaign", "rewardrun"],
145
+ "expansion": "loyalty_programs",
146
+ "override": True,
147
  "prompt_note": "Ensure you mention Mintoak RewardRun as the gamified loyalty and merchant engagement platform.",
148
  "top_k": 3
149
  }
 
151
 
152
  MAX_CHUNK_CHARS = 500
153
 
154
+ def has_lead_intent(query: str) -> bool:
155
+ q = query.lower()
156
+ # Check for demo requests
157
+ if any(kw in q for kw in ["book a demo", "schedule a demo", "request a demo", "book demo", "schedule demo", "request demo", "get a demo", "live demo"]):
158
+ return True
159
+ # Check for pricing/cost queries
160
+ if any(kw in q for kw in ["pricing", "price list", "pricing plans", "subscription cost", "licensing cost", "what are the charges", "setup fees"]):
161
+ return True
162
+ if "cost" in q and any(w in q for w in ["how much", "what is", "what's"]):
163
+ return True
164
+ if "how much" in q and any(w in q for w in ["cost", "charge", "pay", "fee"]):
165
+ return True
166
+ # Check for partnership queries
167
+ if any(kw in q for kw in ["become a partner", "partnering with you", "partnership opportunities", "partner with mintoak"]):
168
+ return True
169
+ # Check for contact/sales queries
170
+ if any(kw in q for kw in ["talk to sales", "sales team", "contact sales", "sales department", "sales contact"]):
171
+ return True
172
+ if any(kw in q for kw in ["get in touch", "how to contact", "contact us", "contact details", "callback", "call back"]):
173
+ return True
174
+ # Check for request to contact
175
+ if "call me" in q or "phone number" in q or "email address" in q:
176
+ return True
177
+ return False
178
+
179
  def retrieve_context(query: str, top_k: int = 2):
180
  query_lower = query.lower()
181
  search_keywords = None
 
184
  # Step 1: Check config-driven expansion registry first
185
  for rule in QUERY_ENHANCEMENT_RULES:
186
  if any(kw in query_lower for kw in rule["keywords"]):
187
+ if rule.get("override"):
188
+ search_keywords = rule["expansion"]
189
+ else:
190
+ search_keywords = f"{query} {rule['expansion']}"
191
  top_k = rule["top_k"]
192
  prompt_note = rule.get("prompt_note")
193
  break
 
196
  if not search_keywords:
197
  messages = [
198
  {"role": "system", "content": OPTIMIZER_PROMPT},
199
+ {"role": "user", "content": "Tell me about Mintoak's loyalty program"},
200
+ {"role": "assistant", "content": "loyalty program"},
201
+ {"role": "user", "content": "How to onboard a merchant?"},
202
+ {"role": "assistant", "content": "DigiOnboard merchant onboarding"},
203
+ {"role": "user", "content": "What is the soundbox device?"},
204
+ {"role": "assistant", "content": "SoundHub soundbox device"},
205
+ {"role": "user", "content": "How do they accept payments?"},
206
+ {"role": "assistant", "content": "SmartPayments accept payments"},
207
+ {"role": "user", "content": "Tell me about business360"},
208
+ {"role": "assistant", "content": "business360"},
209
  {"role": "user", "content": query}
210
  ]
211
  prompt = _tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
 
264
 
265
  return "\n\n---\n\n".join(context_parts), url_to_title, prompt_note
266
 
267
+ def clean_response(text: str, allow_lead_capture: bool = True) -> str:
268
  for rx in PREAMBLE_RES:
269
  text = rx.sub("", text).strip()
270
+
271
+ # Strip any model-generated citation lines
272
+ text = re.sub(r'(?i)(?:\n\s*)*👉?\s*For\s+more\s+details,?\s+visit:.*$', '', text).strip()
273
+
274
+ has_lead = "[CAPTURE_LEAD]" in text and allow_lead_capture
275
+ text_no_lead = text.replace("[CAPTURE_LEAD]", "").strip()
276
+ text_lower = text_no_lead.lower()
277
+
278
+ fallback_triggers = [
279
+ "not fully answered", "not mentioned", "not provided", "unable to answer",
280
+ "cannot be determined", "no mention", "not find any information",
281
+ "does not contain", "no information", "not in the context", "not explicitly mentioned",
282
+ "isn't mentioned", "not found"
283
+ ]
284
+ if any(trigger in text_lower for trigger in fallback_triggers):
285
+ text_no_lead = (
286
+ "The requested information does not currently exist on www.mintoak.com. "
287
+ "You can get in touch with our team at https://www.mintoak.com/contact-us."
288
+ )
289
+
290
+ if has_lead:
291
+ text = text_no_lead + " [CAPTURE_LEAD]"
292
+ else:
293
+ text = text_no_lead
294
+
295
  if text and text[0].islower():
296
  text = text[0].upper() + text[1:]
297
  return text
 
388
  def chat():
389
  data = request.get_json(force=True)
390
  query = (data.get("query") or "").strip()
391
+ history = data.get("history") or []
392
  if not query:
393
  return Response("data: " + json.dumps({"error": "Empty query"}) + "\n\n", mimetype="text/event-stream")
394
 
 
431
  def stream_rag():
432
  yield f"data: {json.dumps({'event': 'sources', 'sources': sources})}\n\n"
433
 
434
+ num_user_questions = sum(1 for msg in history if msg.get("role") == "user") + 1
435
+ allow_lead_capture = has_lead_intent(query) or (num_user_questions >= 3)
436
+
437
  user_content = f"Context:\n{context}\n\nQuestion: {query}"
438
  if prompt_note:
439
  user_content += f"\n\nNote: {prompt_note}"
440
 
441
+ # Build messages from system prompt + conversation history + current user query
442
+ messages = [{"role": "system", "content": SYSTEM_PROMPT}]
443
+ for msg in history:
444
+ role = "user" if msg.get("role") == "user" else "assistant"
445
+ content = msg.get("content", "").strip()
446
+ content = content.replace("[CAPTURE_LEAD]", "").strip()
447
+ messages.append({"role": role, "content": content})
448
+
449
+ messages.append({"role": "user", "content": user_content})
450
  prompt = _tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True)
451
 
452
  t0 = time.time()
 
470
  thread.start()
471
 
472
  citation_started = False
473
+ lead_tag_sent = False
474
  for token_text in streamer:
475
  full_response += token_text
476
 
477
  if citation_started:
478
+ if "[CAPTURE_LEAD]" in full_response and not lead_tag_sent and allow_lead_capture:
479
+ lead_tag_sent = True
480
+ yield f"data: {json.dumps({'event': 'token', 'token': '[CAPTURE_LEAD]'})}\n\n"
481
  continue
482
 
483
  last_snippet = full_response[-40:].lower()
484
  if "👉" in token_text or "for more details" in last_snippet or "details, visit" in last_snippet:
485
  citation_started = True
486
  continue
487
+
488
+ if "[CAPTURE_LEAD]" in token_text:
489
+ if allow_lead_capture and not lead_tag_sent:
490
+ lead_tag_sent = True
491
+ yield f"data: {json.dumps({'event': 'token', 'token': '[CAPTURE_LEAD]'})}\n\n"
492
+ continue
493
 
494
  yield f"data: {json.dumps({'event': 'token', 'token': token_text})}\n\n"
495
+
496
+ # Ensure lead tag is sent if generated or proactively triggered
497
+ should_send_lead = allow_lead_capture and ("[CAPTURE_LEAD]" in full_response or num_user_questions >= 3)
498
+ if should_send_lead and not lead_tag_sent:
499
+ yield f"data: {json.dumps({'event': 'token', 'token': ' [CAPTURE_LEAD]'})}\n\n"
500
 
501
  latency = round(time.time() - t0, 2)
502
+ cleaned_response = clean_response(full_response.strip(), allow_lead_capture=allow_lead_capture)
503
 
504
  # If references were not generated, append them
505
  if "Source:" not in cleaned_response and sources:
scripts/mintoak/chat_server.py CHANGED
@@ -119,7 +119,7 @@ QUERY_ENHANCEMENT_RULES = [
119
  },
120
  {
121
  "keywords": ["document", "documents", "checklist", "paperwork", "certificates"],
122
- "expansion": "Mintoak DigiOnboard merchant onboarding documents checklist GST PAN registration certificates cancelled cheques board resolutions",
123
  "prompt_note": "Ensure you list the specific documents needed: registration certificates, GST certificates, cancelled cheques, and board resolutions.",
124
  "top_k": 3
125
  },
@@ -128,11 +128,43 @@ QUERY_ENHANCEMENT_RULES = [
128
  "expansion": "Mintoak DigiOnboard merchant onboarding KYC KYB acquisition banks",
129
  "prompt_note": None,
130
  "top_k": 2
 
 
 
 
 
 
 
131
  }
132
  ]
133
 
134
  MAX_CHUNK_CHARS = 400 # Truncate each chunk to cap prompt size & speed up prefill
135
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
136
  def retrieve_context(query: str, history: list = None, top_k: int = 2):
137
  """Return (context_text, url_to_title_dict, prompt_note) or ('OUT_OF_SCOPE_REFUSAL', {}, None)."""
138
  query_lower = query.lower()
@@ -166,7 +198,10 @@ def retrieve_context(query: str, history: list = None, top_k: int = 2):
166
  # Apply configuration-driven expansion rules dynamically (without wiping out original query keywords)
167
  for rule in QUERY_ENHANCEMENT_RULES:
168
  if any(kw in query_lower for kw in rule["keywords"]):
169
- search_query = f"{query} {rule['expansion']}"
 
 
 
170
  top_k = rule["top_k"]
171
  prompt_note = rule.get("prompt_note")
172
  break
@@ -216,7 +251,7 @@ def retrieve_context(query: str, history: list = None, top_k: int = 2):
216
  return "\n\n---\n\n".join(context_parts), url_to_title, prompt_note
217
 
218
 
219
- def clean_response(text: str) -> str:
220
  """Strip robotic preambles and fix capitalisation."""
221
  for rx in PREAMBLE_RES:
222
  text = rx.sub("", text).strip()
@@ -225,7 +260,7 @@ def clean_response(text: str) -> str:
225
  text = re.sub(r'(?i)(?:\n\s*)*👉?\s*For\s+more\s+details,?\s+visit:.*$', '', text).strip()
226
 
227
  # Handle LLM fallback non-compliance phrases and override with the brand-approved fallback
228
- has_lead = "[CAPTURE_LEAD]" in text
229
  text_no_lead = text.replace("[CAPTURE_LEAD]", "").strip()
230
  text_lower = text_no_lead.lower()
231
 
@@ -432,6 +467,9 @@ def chat():
432
  # First send sources so UI can render them instantly
433
  yield f"data: {json.dumps({'event': 'sources', 'sources': sources})}\n\n"
434
 
 
 
 
435
  # Rewrite the query passed to the LLM if it is a conversational feedback query
436
  llm_query = query
437
  if history:
@@ -478,7 +516,7 @@ def chat():
478
  full_response += token_text
479
 
480
  if citation_started:
481
- if "[CAPTURE_LEAD]" in full_response and not lead_tag_sent:
482
  lead_tag_sent = True
483
  yield f"data: {json.dumps({'event': 'token', 'token': '[CAPTURE_LEAD]'})}\n\n"
484
  continue
@@ -488,14 +526,21 @@ def chat():
488
  citation_started = True
489
  continue
490
 
 
 
 
 
 
 
491
  yield f"data: {json.dumps({'event': 'token', 'token': token_text})}\n\n"
492
 
493
- # Ensure lead tag is sent if generated but not yet streamed
494
- if "[CAPTURE_LEAD]" in full_response and not lead_tag_sent:
495
- yield f"data: {json.dumps({'event': 'token', 'token': '[CAPTURE_LEAD]'})}\n\n"
 
496
 
497
  latency = round(time.time() - t0, 2)
498
- cleaned_response = clean_response(full_response.strip())
499
 
500
  # If references were not generated in the response body, append them dynamically
501
  if "Source:" not in cleaned_response and "details, visit:" not in cleaned_response and sources:
 
119
  },
120
  {
121
  "keywords": ["document", "documents", "checklist", "paperwork", "certificates"],
122
+ "expansion": "What documents are needed for merchant onboarding? DigiOnboard KYC KYB",
123
  "prompt_note": "Ensure you list the specific documents needed: registration certificates, GST certificates, cancelled cheques, and board resolutions.",
124
  "top_k": 3
125
  },
 
128
  "expansion": "Mintoak DigiOnboard merchant onboarding KYC KYB acquisition banks",
129
  "prompt_note": None,
130
  "top_k": 2
131
+ },
132
+ {
133
+ "keywords": ["loyalty", "reward", "rewards", "campaign", "rewardrun"],
134
+ "expansion": "loyalty_programs",
135
+ "override": True,
136
+ "prompt_note": "Ensure you mention Mintoak RewardRun as the gamified loyalty and merchant engagement platform.",
137
+ "top_k": 3
138
  }
139
  ]
140
 
141
  MAX_CHUNK_CHARS = 400 # Truncate each chunk to cap prompt size & speed up prefill
142
 
143
+ def has_lead_intent(query: str) -> bool:
144
+ q = query.lower()
145
+ # Check for demo requests
146
+ if any(kw in q for kw in ["book a demo", "schedule a demo", "request a demo", "book demo", "schedule demo", "request demo", "get a demo", "live demo"]):
147
+ return True
148
+ # Check for pricing/cost queries
149
+ if any(kw in q for kw in ["pricing", "price list", "pricing plans", "subscription cost", "licensing cost", "what are the charges", "setup fees"]):
150
+ return True
151
+ if "cost" in q and any(w in q for w in ["how much", "what is", "what's"]):
152
+ return True
153
+ if "how much" in q and any(w in q for w in ["cost", "charge", "pay", "fee"]):
154
+ return True
155
+ # Check for partnership queries
156
+ if any(kw in q for kw in ["become a partner", "partnering with you", "partnership opportunities", "partner with mintoak"]):
157
+ return True
158
+ # Check for contact/sales queries
159
+ if any(kw in q for kw in ["talk to sales", "sales team", "contact sales", "sales department", "sales contact"]):
160
+ return True
161
+ if any(kw in q for kw in ["get in touch", "how to contact", "contact us", "contact details", "callback", "call back"]):
162
+ return True
163
+ # Check for request to contact
164
+ if "call me" in q or "phone number" in q or "email address" in q:
165
+ return True
166
+ return False
167
+
168
  def retrieve_context(query: str, history: list = None, top_k: int = 2):
169
  """Return (context_text, url_to_title_dict, prompt_note) or ('OUT_OF_SCOPE_REFUSAL', {}, None)."""
170
  query_lower = query.lower()
 
198
  # Apply configuration-driven expansion rules dynamically (without wiping out original query keywords)
199
  for rule in QUERY_ENHANCEMENT_RULES:
200
  if any(kw in query_lower for kw in rule["keywords"]):
201
+ if rule.get("override"):
202
+ search_query = rule["expansion"]
203
+ else:
204
+ search_query = f"{query} {rule['expansion']}"
205
  top_k = rule["top_k"]
206
  prompt_note = rule.get("prompt_note")
207
  break
 
251
  return "\n\n---\n\n".join(context_parts), url_to_title, prompt_note
252
 
253
 
254
+ def clean_response(text: str, allow_lead_capture: bool = True) -> str:
255
  """Strip robotic preambles and fix capitalisation."""
256
  for rx in PREAMBLE_RES:
257
  text = rx.sub("", text).strip()
 
260
  text = re.sub(r'(?i)(?:\n\s*)*👉?\s*For\s+more\s+details,?\s+visit:.*$', '', text).strip()
261
 
262
  # Handle LLM fallback non-compliance phrases and override with the brand-approved fallback
263
+ has_lead = "[CAPTURE_LEAD]" in text and allow_lead_capture
264
  text_no_lead = text.replace("[CAPTURE_LEAD]", "").strip()
265
  text_lower = text_no_lead.lower()
266
 
 
467
  # First send sources so UI can render them instantly
468
  yield f"data: {json.dumps({'event': 'sources', 'sources': sources})}\n\n"
469
 
470
+ num_user_questions = sum(1 for msg in history if msg.get("role") == "user") + 1
471
+ allow_lead_capture = has_lead_intent(query) or (num_user_questions >= 3)
472
+
473
  # Rewrite the query passed to the LLM if it is a conversational feedback query
474
  llm_query = query
475
  if history:
 
516
  full_response += token_text
517
 
518
  if citation_started:
519
+ if "[CAPTURE_LEAD]" in full_response and not lead_tag_sent and allow_lead_capture:
520
  lead_tag_sent = True
521
  yield f"data: {json.dumps({'event': 'token', 'token': '[CAPTURE_LEAD]'})}\n\n"
522
  continue
 
526
  citation_started = True
527
  continue
528
 
529
+ if "[CAPTURE_LEAD]" in token_text:
530
+ if allow_lead_capture and not lead_tag_sent:
531
+ lead_tag_sent = True
532
+ yield f"data: {json.dumps({'event': 'token', 'token': '[CAPTURE_LEAD]'})}\n\n"
533
+ continue
534
+
535
  yield f"data: {json.dumps({'event': 'token', 'token': token_text})}\n\n"
536
 
537
+ # Ensure lead tag is sent if generated or proactively triggered
538
+ should_send_lead = allow_lead_capture and ("[CAPTURE_LEAD]" in full_response or num_user_questions >= 3)
539
+ if should_send_lead and not lead_tag_sent:
540
+ yield f"data: {json.dumps({'event': 'token', 'token': ' [CAPTURE_LEAD]'})}\n\n"
541
 
542
  latency = round(time.time() - t0, 2)
543
+ cleaned_response = clean_response(full_response.strip(), allow_lead_capture=allow_lead_capture)
544
 
545
  # If references were not generated in the response body, append them dynamically
546
  if "Source:" not in cleaned_response and "details, visit:" not in cleaned_response and sources: