khubchand commited on
Commit
42e9ea7
·
1 Parent(s): 12aa926

feat: implement semantic chunking, dual-retrieval cross-lingual search, and direct Q&A bypass routing

Browse files
Files changed (5) hide show
  1. config.py +1 -1
  2. llm/inference.py +90 -38
  3. rag/chunking.py +61 -9
  4. rag/prompt_builder.py +56 -10
  5. rag/rag_pipeline.py +169 -38
config.py CHANGED
@@ -6,5 +6,5 @@ EMBEDDING_MODEL = "sentence-transformers/all-MiniLM-L6-v2"
6
  CHUNK_SIZE = 500
7
  CHUNK_OVERLAP = 50
8
  MAX_TOKENS = 150
9
- TEMPERATURE = 0.7
10
  USE_OLLAMA = os.getenv("USE_OLLAMA", "True").lower() == "true"
 
6
  CHUNK_SIZE = 500
7
  CHUNK_OVERLAP = 50
8
  MAX_TOKENS = 150
9
+ TEMPERATURE = 0.0
10
  USE_OLLAMA = os.getenv("USE_OLLAMA", "True").lower() == "true"
llm/inference.py CHANGED
@@ -201,51 +201,103 @@ def generate_response(prompt: str, max_tokens: int = None) -> str:
201
 
202
  def transliterate_hinglish_to_hindi(text: str) -> str:
203
  """
204
- Use the LLM to transliterate Romanized Hinglish to Devanagari Hindi.
 
205
  """
206
- prompt = (
207
- "<|im_start|>system\n"
208
- "You are a precise transliteration tool. Transliterate Romanized Hinglish (Latin script) into Devanagari Hindi script. "
209
- "Keep English technical words as they are, but write them in Devanagari script (e.g. 'rules' -> 'रूल्स'). "
210
- "Respond ONLY with the transliterated text. Do not add explanations or extra text.\n"
211
- "<|im_end|>\n"
212
- f"<|im_start|>user\n{text}<|im_end|>\n"
213
- "<|im_start|>assistant\n"
214
- )
215
  try:
216
- # Transliteration targets a short text, limit max_tokens to 100 for safety
217
- result = generate_response(prompt, max_tokens=100)
218
- # Fallback if empty or failed
219
- if not result.strip():
220
- return text
221
- return result
222
- except Exception:
223
- return text
 
 
 
 
 
 
224
 
225
 
226
  def transliterate_hindi_to_hinglish(text: str) -> str:
227
  """
228
- Use the LLM to transliterate Devanagari Hindi to Romanized Hinglish.
 
229
  """
230
- prompt = (
231
- "<|im_start|>system\n"
232
- "You are a precise transliteration tool. Transliterate Devanagari Hindi script into Romanized Hinglish (Latin script).\n"
233
- "Example:\n"
234
- "Input: मेरी ताकत में टीमवर्क, संचार (communication) और समस्या समाधान (problem-solving) कौशल शामिल हैं।\n"
235
- "Output: Meri strengths me teamwork, communication, aur problem-solving skills shamil hain.\n"
236
- "Respond ONLY with the transliterated text. Do not add explanations.\n"
237
- "<|im_end|>\n"
238
- f"<|im_start|>user\nInput: {text}<|im_end|>\n"
239
- "<|im_start|>assistant\n"
240
- )
241
- try:
242
- # Transliteration targets a short text, limit max_tokens to 100 for safety
243
- result = generate_response(prompt, max_tokens=100)
244
- if not result.strip():
245
- return text
246
- return result
247
- except Exception:
248
- return text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
249
 
250
 
251
  def translate_to_english(text: str) -> str:
 
201
 
202
  def transliterate_hinglish_to_hindi(text: str) -> str:
203
  """
204
+ Translate Hinglish to Devanagari Hindi by first translating to English
205
+ and then translating the English to Hindi. This is highly accurate and fast.
206
  """
 
 
 
 
 
 
 
 
 
207
  try:
208
+ from deep_translator import GoogleTranslator
209
+ # Translate to English first
210
+ translator_en = GoogleTranslator(source='auto', target='en')
211
+ english_text = translator_en.translate(text)
212
+ if english_text and english_text.strip():
213
+ # Translate English to Hindi
214
+ translator_hi = GoogleTranslator(source='en', target='hi')
215
+ hindi_text = translator_hi.translate(english_text)
216
+ if hindi_text and hindi_text.strip():
217
+ return hindi_text.strip()
218
+ except Exception as e:
219
+ print(f"[WARNING] Hinglish to Hindi translation chain failed: {e}")
220
+
221
+ return text
222
 
223
 
224
  def transliterate_hindi_to_hinglish(text: str) -> str:
225
  """
226
+ Deterministically transliterate Devanagari Hindi text to Romanized Hinglish (Latin script).
227
+ Uses rule-based mappings to avoid LLM hallucinations, gibberish, and latency.
228
  """
229
+ vowels = {
230
+ 'अ': 'a', 'आ': 'aa', 'इ': 'i', 'ई': 'ee', 'उ': 'u', 'ऊ': 'oo',
231
+ 'ए': 'e', 'ऐ': 'ai', 'ओ': 'o', 'औ': 'au', 'ऋ': 'ri', 'एॅ': 'e', 'ऑ': 'o'
232
+ }
233
+
234
+ vowel_signs = {
235
+ 'ा': 'a', 'ि': 'i', 'ी': 'ee', 'ु': 'u', 'ू': 'oo',
236
+ 'े': 'e', 'ै': 'ai', 'ो': 'o', 'ौ': 'au', 'ृ': 'ri',
237
+ 'ॅ': 'e', 'ॉ': 'o', 'ं': 'n', 'ँ': 'n', 'ः': 'h'
238
+ }
239
+
240
+ consonants = {
241
+ 'क': 'k', 'ख': 'kh', 'ग': 'g', 'घ': 'gh', 'ङ': 'n',
242
+ 'च': 'ch', 'छ': 'chh', 'ज': 'j', 'झ': 'jh', 'ञ': 'n',
243
+ 'ट': 't', 'ठ': 'th', 'ड': 'd', 'ढ': 'dh', 'ण': 'n',
244
+ 'त': 't', 'थ': 'th', 'द': 'd', 'ध': 'dh', 'न': 'n',
245
+ 'प': 'p', 'फ': 'ph', 'ब': 'b', 'भ': 'bh', 'म': 'm',
246
+ 'य': 'y', 'र': 'r', 'ल': 'l', 'व': 'v',
247
+ 'श': 'sh', 'ष': 'sh', 'स': 's', 'ह': 'h',
248
+ 'क्ष': 'ksh', 'त्र': 'tr', 'ज्ञ': 'gy', 'श्र': 'shr',
249
+ 'ड़': 'd', 'ढ़': 'dh'
250
+ }
251
+
252
+ result = []
253
+ i = 0
254
+ n = len(text)
255
+
256
+ while i < n:
257
+ char = text[i]
258
+
259
+ # Look ahead for conjuncts or dotted characters
260
+ if i + 1 < n and text[i:i+2] in consonants:
261
+ char = text[i:i+2]
262
+ i += 1
263
+ elif i + 1 < n and text[i+1] == '़': # nukta
264
+ char = text[i:i+2]
265
+ i += 1
266
+
267
+ if char in vowels:
268
+ result.append(vowels[char])
269
+ elif char in consonants:
270
+ base = consonants[char]
271
+ has_modifier = False
272
+ if i + 1 < n:
273
+ next_char = text[i+1]
274
+ if next_char == '्':
275
+ result.append(base)
276
+ i += 1
277
+ has_modifier = True
278
+ elif next_char in vowel_signs:
279
+ result.append(base + vowel_signs[next_char])
280
+ i += 1
281
+ has_modifier = True
282
+ if not has_modifier:
283
+ is_end_of_word = True
284
+ if i + 1 < n:
285
+ next_char = text[i+1]
286
+ if '\u0900' <= next_char <= '\u097F':
287
+ is_end_of_word = False
288
+
289
+ if is_end_of_word:
290
+ result.append(base)
291
+ else:
292
+ result.append(base + 'a')
293
+ elif char in vowel_signs:
294
+ result.append(vowel_signs[char])
295
+ else:
296
+ result.append(char)
297
+
298
+ i += 1
299
+
300
+ return "".join(result)
301
 
302
 
303
  def translate_to_english(text: str) -> str:
rag/chunking.py CHANGED
@@ -1,12 +1,64 @@
1
- from langchain_text_splitters import RecursiveCharacterTextSplitter
2
- from config import CHUNK_SIZE, CHUNK_OVERLAP
3
 
4
  def split_documents(documents):
5
-
6
- splitter = RecursiveCharacterTextSplitter(
7
- chunk_size=CHUNK_SIZE,
8
- chunk_overlap=CHUNK_OVERLAP
9
- )
10
- chunks = splitter.split_documents(documents)
11
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
12
  return chunks
 
1
+ import re
2
+ from langchain_core.documents import Document
3
 
4
  def split_documents(documents):
5
+ """
6
+ Splits documents semantically by separating each Q&A block and section/heading
7
+ into its own self-contained chunk. This ensures high vector search accuracy.
8
+ """
9
+ chunks = []
10
+
11
+ for doc in documents:
12
+ content = doc.page_content
13
+ metadata = doc.metadata
14
+
15
+ lines = content.split('\n')
16
+
17
+ current_chunk_lines = []
18
+ in_qa = False
19
+
20
+ for line in lines:
21
+ line_str = line.strip()
22
+ if not line_str:
23
+ continue
24
+
25
+ # Detect new QA block starting
26
+ is_new_q = re.match(r'^(?:Q\d+\.|Question:|Q:|प्रश्न\s*\d*:|प्रश्न\s*\d+\.|प्र\s*\d+:)\s*(.*)', line_str, re.IGNORECASE)
27
+
28
+ if is_new_q:
29
+ # Save previous chunk if any
30
+ if current_chunk_lines:
31
+ chunks.append(Document(
32
+ page_content="\n".join(current_chunk_lines),
33
+ metadata=metadata.copy()
34
+ ))
35
+ current_chunk_lines = [line_str]
36
+ in_qa = True
37
+ continue
38
+
39
+ # Detect new major headings/sections (e.g. numbered headings)
40
+ is_heading = (
41
+ re.match(r'^\d+\.\s+[A-Z]', line_str) or
42
+ (len(line_str) < 50 and line_str.isupper())
43
+ )
44
+
45
+ if is_heading and in_qa:
46
+ # End Q&A context and start a new section chunk
47
+ if current_chunk_lines:
48
+ chunks.append(Document(
49
+ page_content="\n".join(current_chunk_lines),
50
+ metadata=metadata.copy()
51
+ ))
52
+ current_chunk_lines = [line_str]
53
+ in_qa = False
54
+ continue
55
+
56
+ current_chunk_lines.append(line_str)
57
+
58
+ if current_chunk_lines:
59
+ chunks.append(Document(
60
+ page_content="\n".join(current_chunk_lines),
61
+ metadata=metadata.copy()
62
+ ))
63
+
64
  return chunks
rag/prompt_builder.py CHANGED
@@ -1,4 +1,55 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  def build_prompt(context: str, question: str, language: str = "english"):
 
 
2
  if language == "hindi":
3
  system_prompt = (
4
  "आप एक सहायक एआई सहायक हैं। नीचे दिए गए संदर्भ (context) से सीधे और संक्षिप्त उत्तर दें (3 वाक्यों से कम)।\n"
@@ -14,26 +65,21 @@ def build_prompt(context: str, question: str, language: str = "english"):
14
  )
15
  else:
16
  system_prompt = (
17
- "You are a helpful AI assistant. Answer the user's question based ONLY on the provided context. Keep your answers brief and under 3 sentences.\n"
18
- "If the answer is not available in the context, respond exactly with: 'Sorry, I can't help in this context.'\n\n"
19
- "Example:\n"
20
- "Context:\n"
21
- "Q3. What are your strengths?\n"
22
- "Answer: My strengths include teamwork, communication, and problem-solving skills.\n"
23
- "Question: What are your strengths?\n"
24
- "Response: My strengths include teamwork, communication, and problem-solving skills."
25
  )
26
 
27
  prompt = f"""<|im_start|>system
28
  {system_prompt}<|im_end|>
29
  <|im_start|>user
30
  Context:
31
- {context}
32
 
33
  Question:
34
  {question}<|im_end|>
35
  <|im_start|>assistant
36
- """
37
  return prompt
38
 
39
 
 
1
+ import re
2
+
3
+ def format_context_as_xml(context: str) -> str:
4
+ """
5
+ Format raw context text containing QA pairs and plain text into structured XML blocks.
6
+ This ensures no information is discarded and helps small LLMs process documents cleanly.
7
+ """
8
+ lines = context.split('\n')
9
+ qa_pairs = []
10
+ plain_text_lines = []
11
+ current_q = None
12
+ current_a = []
13
+
14
+ for line in lines:
15
+ line_str = line.strip()
16
+ if not line_str:
17
+ continue
18
+
19
+ q_match = re.match(r'^(?:Q\d+\.|Question:|Q:)\s*(.*)', line_str, re.IGNORECASE)
20
+ if q_match:
21
+ if current_q and current_a:
22
+ qa_pairs.append((current_q, " ".join(current_a)))
23
+ current_q = q_match.group(1).strip()
24
+ current_a = []
25
+ continue
26
+
27
+ a_match = re.match(r'^(?:Answer:|Ans:|A:)\s*(.*)', line_str, re.IGNORECASE)
28
+ if a_match:
29
+ current_a.append(a_match.group(1).strip())
30
+ continue
31
+
32
+ if current_q and current_a:
33
+ current_a.append(line_str)
34
+ else:
35
+ plain_text_lines.append(line_str)
36
+
37
+ if current_q and current_a:
38
+ qa_pairs.append((current_q, " ".join(current_a)))
39
+
40
+ xml_parts = []
41
+ if plain_text_lines:
42
+ xml_parts.append("<information>\n" + "\n".join(plain_text_lines) + "\n</information>")
43
+
44
+ for q, a in qa_pairs:
45
+ xml_parts.append(f"<qa>\n<question>{q}</question>\n<answer>{a}</answer>\n</qa>")
46
+
47
+ return "\n".join(xml_parts)
48
+
49
+
50
  def build_prompt(context: str, question: str, language: str = "english"):
51
+ formatted_context = format_context_as_xml(context)
52
+
53
  if language == "hindi":
54
  system_prompt = (
55
  "आप एक सहायक एआई सहायक हैं। नीचे दिए गए संदर्भ (context) से सीधे और संक्षिप्त उत्तर दें (3 वाक्यों से कम)।\n"
 
65
  )
66
  else:
67
  system_prompt = (
68
+ "You are a helpful AI assistant. Answer the user's question directly based on the XML qa blocks in the context. "
69
+ "Find the matching question and return its exact answer. Do not add meta-commentary or extra text.\n"
70
+ "If the answer is not available in the context, respond exactly with: 'Sorry, I can't help in this context.'"
 
 
 
 
 
71
  )
72
 
73
  prompt = f"""<|im_start|>system
74
  {system_prompt}<|im_end|>
75
  <|im_start|>user
76
  Context:
77
+ {formatted_context}
78
 
79
  Question:
80
  {question}<|im_end|>
81
  <|im_start|>assistant
82
+ Answer: """
83
  return prompt
84
 
85
 
rag/rag_pipeline.py CHANGED
@@ -10,14 +10,15 @@ GREETINGS = {
10
  "thank you", "thanks", "hell", "helo", "namaste", "pranam"
11
  }
12
 
13
- # Common Hinglish words (pronouns, auxiliary verbs, particles, and simple verbs)
14
  HINGLISH_KEYWORDS = {
15
  "kya", "kyu", "kyoon", "kab", "kaha", "kahin", "kaise", "kaun", "kis",
16
- "hai", "hain", "hoon", "ho", "tha", "the", "thi", "raha", "rahe", "rahi", "rha", "rhe", "rhi",
17
- "mera", "meri", "mere", "mujhe", "apna", "apni", "apne", "tum", "tumhara", "tumhari", "aap", "aapka", "aapki", "aapke",
18
- "hum", "humara", "humari", "se", "ko", "par", "pe", "ka", "ki", "ke", "liye", "bhi", "hi", "toh", "to", "aur", "ya", "lekin", "magar",
 
19
  "karo", "kar", "karte", "karta", "karti", "kiya", "kr", "kra", "kro", "krna", "karna",
20
- "batao", "bolo", "likho", "dikhao", "suno", "samjho", "chahiye", "de", "do", "le", "lo", "aao", "jaao", "jao"
21
  }
22
 
23
 
@@ -86,6 +87,75 @@ def _clean_response(text: str) -> str:
86
  return cleaned
87
 
88
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  def generate_rag_response(question: str):
90
  # 1. Detect query language
91
  lang = detect_language(question)
@@ -97,19 +167,21 @@ def generate_rag_response(question: str):
97
 
98
  # 3. Translate query to English if it is Hindi or Hinglish (as uploaded documents are in English)
99
  search_query = question
 
100
  if lang in ["hindi", "hinglish"]:
101
  search_query = translate_to_english(question)
 
 
102
 
103
  # 4. Clean text
104
  search_query = clean_text(search_query)
105
 
106
  # 5. Retrieve context using similarity search
107
  context = retrieve_context(search_query)
108
-
109
- # Clean the retrieved context of QA interview formatting to avoid model grading/evaluation triggers
110
- if context:
111
- context = re.sub(r'(?i)Q\d+\.\s*', '', context)
112
- context = re.sub(r'(?i)(Answer|Ans|A):\s*', '', context)
113
 
114
  # Fast check: If the retrieved context is empty/whitespace, return the localized fallback immediately
115
  if not context or not context.strip():
@@ -120,42 +192,101 @@ def generate_rag_response(question: str):
120
  else:
121
  return "Sorry, I can't help in this context."
122
 
123
- # 6. Format prompt targeting English generation for high-quality fact retrieval
124
- prompt = build_prompt(context, search_query, language="english")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
125
 
126
- # 7. Generate response in English
127
- response = generate_response(prompt)
128
- response = _clean_response(response)
 
 
 
 
 
129
 
130
  # 8. Post-process language-specific translation/fallback
131
- if "sorry, i can't help" in response.lower() or "i don't know" in response.lower():
 
 
 
 
132
  if lang == "hindi":
133
  return "माफ़ कीजिए, मैं इस संदर्भ में आपकी मदद नहीं कर सकता।"
134
  elif lang == "hinglish":
135
  return "Sorry, main is context me help nahi kar sakta."
136
  else:
137
- return response
 
 
 
138
 
139
- if lang == "hindi":
140
- try:
141
- from deep_translator import GoogleTranslator
142
- translated = GoogleTranslator(source='en', target='hi').translate(response)
143
- if translated and translated.strip():
144
- return translated.strip()
145
- except Exception as e:
146
- print(f"[WARNING] English to Hindi translation failed: {e}")
147
-
148
- elif lang == "hinglish":
149
- try:
150
- from deep_translator import GoogleTranslator
151
- # Translate to Hindi first for vocabulary alignment
152
- translated_hi = GoogleTranslator(source='en', target='hi').translate(response)
153
- if translated_hi and translated_hi.strip():
154
- # Transliterate Hindi to Hinglish phonetically
155
- hinglish_res = transliterate_hindi_to_hinglish(translated_hi)
156
  if hinglish_res and hinglish_res.strip():
157
  return hinglish_res.strip()
158
- except Exception as e:
159
- print(f"[WARNING] English to Hinglish translation/transliteration failed: {e}")
160
-
161
- return response
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  "thank you", "thanks", "hell", "helo", "namaste", "pranam"
11
  }
12
 
13
+ # Common Hinglish words (non-overlapping with standard English vocabulary to avoid false positives)
14
  HINGLISH_KEYWORDS = {
15
  "kya", "kyu", "kyoon", "kab", "kaha", "kahin", "kaise", "kaun", "kis",
16
+ "hai", "hain", "hoon", "tha", "the", "thi", "raha", "rahe", "rahi", "rha", "rhe", "rhi",
17
+ "mera", "meri", "mere", "mujhe", "apna", "apni", "apne", "tum", "tumhara", "tumhari",
18
+ "aap", "aapka", "aapki", "aapke", "hum", "humara", "humari",
19
+ "liye", "lekin", "magar", "bhi", "toh",
20
  "karo", "kar", "karte", "karta", "karti", "kiya", "kr", "kra", "kro", "krna", "karna",
21
+ "batao", "bolo", "likho", "dikhao", "suno", "samjho", "chahiye", "aao", "jaao", "jao"
22
  }
23
 
24
 
 
87
  return cleaned
88
 
89
 
90
+ def query_matches_question(query: str, question: str) -> bool:
91
+ """
92
+ Check if query matches a target question by looking at keyword overlap.
93
+ Filters out common stop words to keep match focus on meaningful words.
94
+ """
95
+ def get_keywords(text: str):
96
+ text = re.sub(r'[^\w\s]', '', text.lower())
97
+ words = text.split()
98
+ stop_words = {"what", "why", "how", "who", "whom", "when", "where", "which", "is", "are", "do", "does", "you", "your", "we", "i", "the", "a", "an", "in", "on", "at", "to", "for", "of", "with", "about"}
99
+ return {w for w in words if w not in stop_words and len(w) > 2}
100
+
101
+ q_words = get_keywords(query)
102
+ target_words = get_keywords(question)
103
+
104
+ if not q_words or not target_words:
105
+ return False
106
+
107
+ intersection = q_words.intersection(target_words)
108
+ if len(intersection) >= min(len(q_words), len(target_words)) * 0.5:
109
+ return True
110
+ return False
111
+
112
+
113
+ def parse_context_hybrid(context: str):
114
+ """
115
+ Parse retrieved context into structured QA blocks and general plain text.
116
+ """
117
+ lines = context.split('\n')
118
+ qa_blocks = []
119
+ plain_text_lines = []
120
+
121
+ current_q = None
122
+ current_a = []
123
+
124
+ for line in lines:
125
+ line_str = line.strip()
126
+ if not line_str:
127
+ continue
128
+
129
+ q_match = re.match(r'^(?:Q\d+\.|Question:|Q:|प्रश्न\s*\d*:|प्रश्न\s*\d+\.|प्र\s*\d+:)\s*(.*)', line_str, re.IGNORECASE)
130
+ if q_match:
131
+ if current_q and current_a:
132
+ qa_blocks.append({
133
+ "question": current_q,
134
+ "answer": " ".join(current_a)
135
+ })
136
+ current_q = q_match.group(1).strip()
137
+ current_a = []
138
+ continue
139
+
140
+ a_match = re.match(r'^(?:Answer:|Ans:|A:|उत्तर:)\s*(.*)', line_str, re.IGNORECASE)
141
+ if a_match:
142
+ current_a.append(a_match.group(1).strip())
143
+ continue
144
+
145
+ if current_q and current_a:
146
+ current_a.append(line_str)
147
+ else:
148
+ plain_text_lines.append(line_str)
149
+
150
+ if current_q and current_a:
151
+ qa_blocks.append({
152
+ "question": current_q,
153
+ "answer": " ".join(current_a)
154
+ })
155
+
156
+ return qa_blocks, "\n".join(plain_text_lines)
157
+
158
+
159
  def generate_rag_response(question: str):
160
  # 1. Detect query language
161
  lang = detect_language(question)
 
167
 
168
  # 3. Translate query to English if it is Hindi or Hinglish (as uploaded documents are in English)
169
  search_query = question
170
+ hindi_query = None
171
  if lang in ["hindi", "hinglish"]:
172
  search_query = translate_to_english(question)
173
+ from llm.inference import transliterate_hinglish_to_hindi
174
+ hindi_query = transliterate_hinglish_to_hindi(question)
175
 
176
  # 4. Clean text
177
  search_query = clean_text(search_query)
178
 
179
  # 5. Retrieve context using similarity search
180
  context = retrieve_context(search_query)
181
+ if lang in ["hindi", "hinglish"] and hindi_query:
182
+ context_hi = retrieve_context(clean_text(hindi_query))
183
+ if context_hi and context_hi.strip():
184
+ context = context + "\n" + context_hi
 
185
 
186
  # Fast check: If the retrieved context is empty/whitespace, return the localized fallback immediately
187
  if not context or not context.strip():
 
192
  else:
193
  return "Sorry, I can't help in this context."
194
 
195
+ # Parse hybrid context
196
+ qa_blocks, general_text = parse_context_hybrid(context)
197
+
198
+ # Check if the query matches any QA block
199
+ matching_qa = None
200
+ for qa in qa_blocks:
201
+ if query_matches_question(search_query, qa["question"]):
202
+ matching_qa = qa
203
+ break
204
+ if hindi_query and query_matches_question(hindi_query, qa["question"]):
205
+ matching_qa = qa
206
+ break
207
+
208
+ if matching_qa:
209
+ # Directly extract the answer string for exact Q&A matches.
210
+ # This completely bypasses LLM latency and hallucination risks.
211
+ response = matching_qa["answer"]
212
+ else:
213
+ if general_text.strip():
214
+ system_prompt = (
215
+ "You are a helpful AI assistant. Answer the user's question directly based ONLY on the provided context. "
216
+ "Keep your answer brief, factual, and under 3 sentences. Do not add meta-commentary or repeat the context.\n"
217
+ "If the answer is not available in the context, respond exactly with: 'Sorry, I can't help in this context.'"
218
+ )
219
+ prompt = f"""<|im_start|>system
220
+ {system_prompt}<|im_end|>
221
+ <|im_start|>user
222
+ Context:
223
+ {general_text}
224
 
225
+ Question:
226
+ {search_query}<|im_end|>
227
+ <|im_start|>assistant
228
+ """
229
+ response = generate_response(prompt)
230
+ response = _clean_response(response)
231
+ else:
232
+ response = "Sorry, I can't help in this context."
233
 
234
  # 8. Post-process language-specific translation/fallback
235
+ resp_lower = response.lower()
236
+ fallback_signals = ["can't help", "cannot help", "don't know", "do not know", "sorry", "not available", "not present", "no information"]
237
+ is_fallback = any(sig in resp_lower for sig in fallback_signals)
238
+
239
+ if is_fallback:
240
  if lang == "hindi":
241
  return "माफ़ कीजिए, मैं इस संदर्भ में आपकी मदद नहीं कर सकता।"
242
  elif lang == "hinglish":
243
  return "Sorry, main is context me help nahi kar sakta."
244
  else:
245
+ return "Sorry, I can't help in this context."
246
+
247
+ # Robust multi-directional translation based on content language
248
+ has_hindi = any('\u0900' <= char <= '\u097F' for char in response)
249
 
250
+ if has_hindi:
251
+ if lang == "hindi":
252
+ return response
253
+ elif lang == "hinglish":
254
+ try:
255
+ hinglish_res = transliterate_hindi_to_hinglish(response)
 
 
 
 
 
 
 
 
 
 
 
256
  if hinglish_res and hinglish_res.strip():
257
  return hinglish_res.strip()
258
+ except Exception as e:
259
+ print(f"[WARNING] Hindi to Hinglish transliteration failed: {e}")
260
+ return response
261
+ else: # english
262
+ try:
263
+ from deep_translator import GoogleTranslator
264
+ translated = GoogleTranslator(source='hi', target='en').translate(response)
265
+ if translated and translated.strip():
266
+ return translated.strip()
267
+ except Exception as e:
268
+ print(f"[WARNING] Hindi to English translation failed: {e}")
269
+ return response
270
+ else:
271
+ if lang == "hindi":
272
+ try:
273
+ from deep_translator import GoogleTranslator
274
+ translated = GoogleTranslator(source='en', target='hi').translate(response)
275
+ if translated and translated.strip():
276
+ return translated.strip()
277
+ except Exception as e:
278
+ print(f"[WARNING] English to Hindi translation failed: {e}")
279
+ elif lang == "hinglish":
280
+ try:
281
+ from deep_translator import GoogleTranslator
282
+ # Translate to Hindi first for vocabulary alignment
283
+ translated_hi = GoogleTranslator(source='en', target='hi').translate(response)
284
+ if translated_hi and translated_hi.strip():
285
+ # Transliterate Hindi to Hinglish phonetically
286
+ hinglish_res = transliterate_hindi_to_hinglish(translated_hi)
287
+ if hinglish_res and hinglish_res.strip():
288
+ return hinglish_res.strip()
289
+ except Exception as e:
290
+ print(f"[WARNING] English to Hinglish translation/transliteration failed: {e}")
291
+
292
+ return response