Spaces:
Sleeping
Sleeping
Deploy v1 — single-Docker FastAPI + Next.js + RAG + voice + faithfulness
Browse files- backend/orchestrator.py +37 -18
- backend/translator.py +92 -0
backend/orchestrator.py
CHANGED
|
@@ -78,23 +78,15 @@ class BrainPick:
|
|
| 78 |
|
| 79 |
|
| 80 |
def pick_brain(intent: str, language: str) -> BrainPick:
|
| 81 |
-
"""Route to the right brain per Doc decisions.md D-016 (
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
factual on this slice vs Llama at 100%; DeepSeek is stronger still
|
| 89 |
-
with citation-discipline as bonus)
|
| 90 |
-
- Llama-3.3-70B reserved as grader AND cross-check rescue brain
|
| 91 |
"""
|
| 92 |
-
|
| 93 |
-
return BrainPick(SarvamLLM(), "indic-query")
|
| 94 |
-
if intent in ("comparison", "recommendation"):
|
| 95 |
-
return BrainPick(OpenRouterLLM(), f"complex-{intent}")
|
| 96 |
-
# English simple QA → DeepSeek-V3 (was Sarvam-M; rebalanced from eval data)
|
| 97 |
-
return BrainPick(OpenRouterLLM(), "simple-qa")
|
| 98 |
|
| 99 |
|
| 100 |
# ---------- main entrypoint ----------
|
|
@@ -127,7 +119,20 @@ async def handle_turn(
|
|
| 127 |
intent = classify_intent(user_text)
|
| 128 |
language = detect_language(user_text)
|
| 129 |
|
| 130 |
-
# 1a.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 131 |
# bypass retrieval + faithfulness; we ask the next discovery question.
|
| 132 |
if intent == "fact_find":
|
| 133 |
from backend.needs_finder import Profile, next_question
|
|
@@ -257,11 +262,25 @@ async def handle_turn(
|
|
| 257 |
for c in chunks
|
| 258 |
]
|
| 259 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
return TurnResult(
|
| 261 |
reply_text=reply,
|
| 262 |
citations=citations,
|
| 263 |
retrieved_chunk_ids=[c.chunk_id for c in chunks],
|
| 264 |
-
brain_used=
|
| 265 |
intent=intent,
|
| 266 |
language=language,
|
| 267 |
latency_ms=int((time.time() - t0) * 1000),
|
|
|
|
| 78 |
|
| 79 |
|
| 80 |
def pick_brain(intent: str, language: str) -> BrainPick:
|
| 81 |
+
"""Route to the right brain per Doc decisions.md D-016 (rev 2026-05-13).
|
| 82 |
+
|
| 83 |
+
English: DeepSeek-V3 always (reasoning quality > Sarvam-M for English Q&A).
|
| 84 |
+
Indic: handled in handle_turn via translation cascade (Sarvam translates
|
| 85 |
+
in, DeepSeek reasons, Sarvam translates back). This function returns the
|
| 86 |
+
REASONING brain in both cases; the Indic in/out translation is done
|
| 87 |
+
separately by translator.py.
|
|
|
|
|
|
|
|
|
|
| 88 |
"""
|
| 89 |
+
return BrainPick(OpenRouterLLM(), f"reasoning-{intent}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
|
| 91 |
|
| 92 |
# ---------- main entrypoint ----------
|
|
|
|
| 119 |
intent = classify_intent(user_text)
|
| 120 |
language = detect_language(user_text)
|
| 121 |
|
| 122 |
+
# 1a. INDIC CASCADE — translate Indic query → English, reason in DeepSeek,
|
| 123 |
+
# translate answer back. Capture original-language user_text for logging.
|
| 124 |
+
original_user_text = user_text
|
| 125 |
+
translated_query = None
|
| 126 |
+
if language == "indic":
|
| 127 |
+
try:
|
| 128 |
+
from backend.translator import translate_to_english
|
| 129 |
+
translated_query = await translate_to_english(user_text)
|
| 130 |
+
if translated_query and translated_query.strip() and translated_query != user_text:
|
| 131 |
+
user_text = translated_query # use English for retrieval + reasoning
|
| 132 |
+
except Exception:
|
| 133 |
+
pass # fall through with original; if Sarvam translator fails, DeepSeek can still try
|
| 134 |
+
|
| 135 |
+
# 1b. Fact-find branch — conversational openers / advice-seeking queries
|
| 136 |
# bypass retrieval + faithfulness; we ask the next discovery question.
|
| 137 |
if intent == "fact_find":
|
| 138 |
from backend.needs_finder import Profile, next_question
|
|
|
|
| 262 |
for c in chunks
|
| 263 |
]
|
| 264 |
|
| 265 |
+
# 7. INDIC CASCADE — translate the English reply back into Hinglish/Hindi
|
| 266 |
+
# so the user hears it in their language. Citations stay intact (the
|
| 267 |
+
# translation prompt preserves them).
|
| 268 |
+
final_brain_tag = f"{pick.provider.name}::{pick.reason}"
|
| 269 |
+
if language == "indic" and not blocked and reply:
|
| 270 |
+
try:
|
| 271 |
+
from backend.translator import translate_to_indic
|
| 272 |
+
reply_indic = await translate_to_indic(reply, target_lang="hi-IN")
|
| 273 |
+
if reply_indic and reply_indic.strip():
|
| 274 |
+
reply = reply_indic
|
| 275 |
+
final_brain_tag = f"cascade::sarvam-trans+{pick.provider.name}+sarvam-trans"
|
| 276 |
+
except Exception:
|
| 277 |
+
pass # if translation fails, return English; better than nothing
|
| 278 |
+
|
| 279 |
return TurnResult(
|
| 280 |
reply_text=reply,
|
| 281 |
citations=citations,
|
| 282 |
retrieved_chunk_ids=[c.chunk_id for c in chunks],
|
| 283 |
+
brain_used=final_brain_tag,
|
| 284 |
intent=intent,
|
| 285 |
language=language,
|
| 286 |
latency_ms=int((time.time() - t0) * 1000),
|
backend/translator.py
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""Translation helpers — Sarvam-M as the Indic specialist for query/answer
|
| 2 |
+
translation in the cascade routing pattern.
|
| 3 |
+
|
| 4 |
+
The cascade pattern for Hindi/Hinglish queries:
|
| 5 |
+
1. Sarvam-M translates the user's Hinglish query → clean English
|
| 6 |
+
2. DeepSeek-V3 reasons over the retrieved policy chunks → English answer
|
| 7 |
+
(with full citation grammar preserved)
|
| 8 |
+
3. Sarvam-M translates the English answer → natural Hinglish
|
| 9 |
+
|
| 10 |
+
Why this is better than either model alone:
|
| 11 |
+
- Sarvam-M has the best Indic comprehension + cultural context but mid-tier
|
| 12 |
+
English reasoning. Don't rely on it for the reasoning step.
|
| 13 |
+
- DeepSeek-V3 has SOTA open-source reasoning but is English-trained.
|
| 14 |
+
Don't rely on it for the Indic understanding/generation step.
|
| 15 |
+
- The cascade gets the best of both — at a +3s latency cost.
|
| 16 |
+
"""
|
| 17 |
+
|
| 18 |
+
from __future__ import annotations
|
| 19 |
+
|
| 20 |
+
from backend.providers.base import ChatMessage
|
| 21 |
+
from backend.providers.sarvam_llm import SarvamLLM
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
_TRANSLATE_TO_EN_SYSTEM = """You are a precise translator from Hindi / Hinglish / code-switched Indian English to clean standard English.
|
| 25 |
+
|
| 26 |
+
RULES:
|
| 27 |
+
1. Output ONLY the translated English sentence. No preamble, no quotes, no explanation.
|
| 28 |
+
2. Preserve insurance/finance/medical terms exactly as in the source.
|
| 29 |
+
3. Names of insurers (Star Health, HDFC ERGO, Niva Bupa, Care, ICICI Lombard, Bajaj Allianz, Aditya Birla, etc.) and policy product names must remain unchanged.
|
| 30 |
+
4. Numbers, currencies (₹, lakh, crore), durations (din/months/years) stay numeric — convert "do saal" → "2 years"; "tees din" → "30 days".
|
| 31 |
+
5. If the source is already English, return it as-is unchanged."""
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
_TRANSLATE_TO_INDIC_SYSTEM = """You are a precise translator from English to natural conversational Hindi / Hinglish (code-switched Indian English) — the way a buyer in urban India actually speaks.
|
| 35 |
+
|
| 36 |
+
RULES:
|
| 37 |
+
1. Output ONLY the translated text. No preamble, no quotes.
|
| 38 |
+
2. Use Devanagari for Hindi words, English for English words that are commonly kept in English in spoken Hindi (insurance, policy, coverage, premium, hospital, claim, network, copay, etc.).
|
| 39 |
+
3. Names of insurers and policies stay unchanged.
|
| 40 |
+
4. Numbers stay numeric.
|
| 41 |
+
5. Keep the tone conversational + warm, like an experienced advisor speaking to a friend.
|
| 42 |
+
6. Maximum 60 words; if the source is longer, condense to the key facts. Do not invent details.
|
| 43 |
+
|
| 44 |
+
Example:
|
| 45 |
+
English: "Yes, HDFC ERGO Optima Secure covers Ayurveda treatment at recognized AYUSH hospitals. The waiting period is 30 days from policy start."
|
| 46 |
+
Hinglish: "Haan, HDFC ERGO Optima Secure mein Ayurveda treatment cover hai — but sirf recognized AYUSH hospitals mein. Policy shuru hone se 30 din ka waiting period rahega."
|
| 47 |
+
"""
|
| 48 |
+
|
| 49 |
+
|
| 50 |
+
async def translate_to_english(text: str, sarvam: SarvamLLM | None = None) -> str:
|
| 51 |
+
"""Translate a Hinglish/Hindi query into clean English for the reasoning brain."""
|
| 52 |
+
if not text.strip():
|
| 53 |
+
return text
|
| 54 |
+
sarvam = sarvam or SarvamLLM()
|
| 55 |
+
res = await sarvam.chat(
|
| 56 |
+
messages=[
|
| 57 |
+
ChatMessage(role="system", content=_TRANSLATE_TO_EN_SYSTEM),
|
| 58 |
+
ChatMessage(role="user", content=text),
|
| 59 |
+
],
|
| 60 |
+
temperature=0.0,
|
| 61 |
+
max_tokens=400,
|
| 62 |
+
)
|
| 63 |
+
out = res.text.strip()
|
| 64 |
+
# Strip <think> tags if Sarvam-M went into reasoning mode
|
| 65 |
+
from backend.persona import strip_think_tags
|
| 66 |
+
return strip_think_tags(out) or text
|
| 67 |
+
|
| 68 |
+
|
| 69 |
+
async def translate_to_indic(
|
| 70 |
+
english: str,
|
| 71 |
+
target_lang: str = "hi-IN",
|
| 72 |
+
sarvam: SarvamLLM | None = None,
|
| 73 |
+
) -> str:
|
| 74 |
+
"""Translate an English answer back into natural Hinglish for the user.
|
| 75 |
+
|
| 76 |
+
Preserves any [Source: ...] citation tags so the faithfulness gate can
|
| 77 |
+
still verify the citation chain after translation.
|
| 78 |
+
"""
|
| 79 |
+
if not english.strip():
|
| 80 |
+
return english
|
| 81 |
+
sarvam = sarvam or SarvamLLM()
|
| 82 |
+
res = await sarvam.chat(
|
| 83 |
+
messages=[
|
| 84 |
+
ChatMessage(role="system", content=_TRANSLATE_TO_INDIC_SYSTEM),
|
| 85 |
+
ChatMessage(role="user", content=english),
|
| 86 |
+
],
|
| 87 |
+
temperature=0.2,
|
| 88 |
+
max_tokens=600,
|
| 89 |
+
)
|
| 90 |
+
out = res.text.strip()
|
| 91 |
+
from backend.persona import strip_think_tags
|
| 92 |
+
return strip_think_tags(out) or english
|