Spaces:
Paused
Paused
Sahil commited on
Commit Β·
75efae8
1
Parent(s): 49e98e5
Multilingual support added (Marathi , Hindi , English)
Browse files- app/__pycache__/__init__.cpython-313.pyc +0 -0
- app/__pycache__/config.cpython-313.pyc +0 -0
- app/routers/assistant.py +2 -0
- app/routers/buddy.py +7 -2
- app/services/__pycache__/__init__.cpython-313.pyc +0 -0
- app/services/__pycache__/ai.cpython-313.pyc +0 -0
- app/services/__pycache__/rag_pipeline.cpython-313.pyc +0 -0
- app/services/__pycache__/tts.cpython-313.pyc +0 -0
- app/services/ai.py +21 -7
- app/services/tts.py +4 -4
app/__pycache__/__init__.cpython-313.pyc
CHANGED
|
Binary files a/app/__pycache__/__init__.cpython-313.pyc and b/app/__pycache__/__init__.cpython-313.pyc differ
|
|
|
app/__pycache__/config.cpython-313.pyc
CHANGED
|
Binary files a/app/__pycache__/config.cpython-313.pyc and b/app/__pycache__/config.cpython-313.pyc differ
|
|
|
app/routers/assistant.py
CHANGED
|
@@ -17,6 +17,7 @@ class MessageIn(BaseModel):
|
|
| 17 |
class ChatRequest(BaseModel):
|
| 18 |
conversation_id: Optional[str] = None
|
| 19 |
message: str
|
|
|
|
| 20 |
|
| 21 |
|
| 22 |
class ConversationCreate(BaseModel):
|
|
@@ -137,6 +138,7 @@ async def chat(
|
|
| 137 |
user_message=body.message,
|
| 138 |
history=history,
|
| 139 |
profile_context=profile_context,
|
|
|
|
| 140 |
)
|
| 141 |
ai_reply = ai_result["reply"]
|
| 142 |
ai_sources = ai_result.get("sources", [])
|
|
|
|
| 17 |
class ChatRequest(BaseModel):
|
| 18 |
conversation_id: Optional[str] = None
|
| 19 |
message: str
|
| 20 |
+
preferred_language: str = "English"
|
| 21 |
|
| 22 |
|
| 23 |
class ConversationCreate(BaseModel):
|
|
|
|
| 138 |
user_message=body.message,
|
| 139 |
history=history,
|
| 140 |
profile_context=profile_context,
|
| 141 |
+
preferred_lang=body.preferred_language,
|
| 142 |
)
|
| 143 |
ai_reply = ai_result["reply"]
|
| 144 |
ai_sources = ai_result.get("sources", [])
|
app/routers/buddy.py
CHANGED
|
@@ -15,6 +15,10 @@ router = APIRouter(prefix="/buddy", tags=["emotional-buddy"])
|
|
| 15 |
class BuddyTextRequest(BaseModel):
|
| 16 |
text: str
|
| 17 |
history: Optional[List[dict]] = None
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
|
| 20 |
@router.post("/chat")
|
|
@@ -30,14 +34,15 @@ async def text_chat(
|
|
| 30 |
raise HTTPException(status_code=422, detail="Text must not be empty")
|
| 31 |
|
| 32 |
history = body.history or []
|
|
|
|
| 33 |
|
| 34 |
# AI response
|
| 35 |
-
ai_text, mood_score, emotion = await emotional_buddy_respond(body.text, history)
|
| 36 |
|
| 37 |
# TTS β non-critical: failure returns empty audio, client can still show text
|
| 38 |
audio_response = b""
|
| 39 |
try:
|
| 40 |
-
audio_response = await text_to_speech_bytes(ai_text)
|
| 41 |
except Exception as tts_err:
|
| 42 |
import logging
|
| 43 |
logging.getLogger(__name__).warning("TTS failed: %s", tts_err)
|
|
|
|
| 15 |
class BuddyTextRequest(BaseModel):
|
| 16 |
text: str
|
| 17 |
history: Optional[List[dict]] = None
|
| 18 |
+
preferred_language: str = "English"
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
_LANG_CODE: dict[str, str] = {"English": "en", "Hindi": "hi", "Marathi": "mr"}
|
| 22 |
|
| 23 |
|
| 24 |
@router.post("/chat")
|
|
|
|
| 34 |
raise HTTPException(status_code=422, detail="Text must not be empty")
|
| 35 |
|
| 36 |
history = body.history or []
|
| 37 |
+
lang_code = _LANG_CODE.get(body.preferred_language, "en")
|
| 38 |
|
| 39 |
# AI response
|
| 40 |
+
ai_text, mood_score, emotion = await emotional_buddy_respond(body.text, history, body.preferred_language)
|
| 41 |
|
| 42 |
# TTS β non-critical: failure returns empty audio, client can still show text
|
| 43 |
audio_response = b""
|
| 44 |
try:
|
| 45 |
+
audio_response = await text_to_speech_bytes(ai_text, lang_code)
|
| 46 |
except Exception as tts_err:
|
| 47 |
import logging
|
| 48 |
logging.getLogger(__name__).warning("TTS failed: %s", tts_err)
|
app/services/__pycache__/__init__.cpython-313.pyc
CHANGED
|
Binary files a/app/services/__pycache__/__init__.cpython-313.pyc and b/app/services/__pycache__/__init__.cpython-313.pyc differ
|
|
|
app/services/__pycache__/ai.cpython-313.pyc
CHANGED
|
Binary files a/app/services/__pycache__/ai.cpython-313.pyc and b/app/services/__pycache__/ai.cpython-313.pyc differ
|
|
|
app/services/__pycache__/rag_pipeline.cpython-313.pyc
CHANGED
|
Binary files a/app/services/__pycache__/rag_pipeline.cpython-313.pyc and b/app/services/__pycache__/rag_pipeline.cpython-313.pyc differ
|
|
|
app/services/__pycache__/tts.cpython-313.pyc
CHANGED
|
Binary files a/app/services/__pycache__/tts.cpython-313.pyc and b/app/services/__pycache__/tts.cpython-313.pyc differ
|
|
|
app/services/ai.py
CHANGED
|
@@ -79,7 +79,10 @@ entertainment, general trivia, etc. β respond ONLY with:
|
|
| 79 |
β’ Do NOT use markdown headers (##), bold (**), or bullet-heavy formatting β write in clean prose
|
| 80 |
β’ End complex answers with a gentle reminder to consult a qualified healthcare provider
|
| 81 |
|
| 82 |
-
User medical profile context will be provided when available β use it to personalise responses.
|
|
|
|
|
|
|
|
|
|
| 83 |
|
| 84 |
_RAG_MEDICAL_SYSTEM = """\
|
| 85 |
You are Aarogyan's Medical Health Assistant β a supportive, evidence-based AI health companion.
|
|
@@ -108,6 +111,9 @@ Use ONLY the provided context to answer. If the context is insufficient, say so
|
|
| 108 |
β’ Write in clean prose β no markdown headers, no bold, no repeated ideas across paragraphs
|
| 109 |
β’ End with a brief recommendation to consult a healthcare provider if the topic warrants it
|
| 110 |
|
|
|
|
|
|
|
|
|
|
| 111 |
--- Retrieved Medical Context ---
|
| 112 |
{context}
|
| 113 |
--- End of Context ---
|
|
@@ -174,7 +180,10 @@ IMPORTANT: Always respond in JSON format:
|
|
| 174 |
"response": "your empathetic, warm reply here",
|
| 175 |
"mood_score": <integer 1-10>,
|
| 176 |
"emotion": "<one of: happy, sad, angry, fearful, disgusted, surprised, neutral>"
|
| 177 |
-
}
|
|
|
|
|
|
|
|
|
|
| 178 |
|
| 179 |
|
| 180 |
async def _call_groq(messages: list[dict], system: str, temperature: float = 0.7) -> str:
|
|
@@ -222,6 +231,7 @@ async def _chat_with_rag(
|
|
| 222 |
history: list[dict],
|
| 223 |
profile_context: str,
|
| 224 |
is_complex: bool = False,
|
|
|
|
| 225 |
) -> dict:
|
| 226 |
"""RAG-augmented chat: retrieve context then synthesise with Groq.
|
| 227 |
|
|
@@ -240,7 +250,7 @@ async def _chat_with_rag(
|
|
| 240 |
|
| 241 |
if not context_str:
|
| 242 |
logger.warning("RAG returned no context β falling back to plain LLM")
|
| 243 |
-
return await _chat_plain(user_message, history, profile_context)
|
| 244 |
|
| 245 |
profile_section = ""
|
| 246 |
if profile_context:
|
|
@@ -250,6 +260,7 @@ async def _chat_with_rag(
|
|
| 250 |
context=context_str,
|
| 251 |
profile_section=profile_section,
|
| 252 |
)
|
|
|
|
| 253 |
|
| 254 |
messages = [*history, {"role": "user", "content": user_message}]
|
| 255 |
reply = await _call_groq(messages, system, temperature=0.2)
|
|
@@ -261,6 +272,7 @@ async def _chat_plain(
|
|
| 261 |
user_message: str,
|
| 262 |
history: list[dict],
|
| 263 |
profile_context: str,
|
|
|
|
| 264 |
) -> dict:
|
| 265 |
"""Plain LLM chat without RAG (fallback when Qdrant returns nothing).
|
| 266 |
Returns {"reply": str, "sources": []}
|
|
@@ -268,6 +280,7 @@ async def _chat_plain(
|
|
| 268 |
system = MEDICAL_ASSISTANT_SYSTEM
|
| 269 |
if profile_context:
|
| 270 |
system += f"\n\n--- User Health Profile ---\n{profile_context}"
|
|
|
|
| 271 |
messages = [*history, {"role": "user", "content": user_message}]
|
| 272 |
reply = await _call_groq(messages, system)
|
| 273 |
return {"reply": reply.strip(), "sources": []}
|
|
@@ -277,10 +290,11 @@ async def chat_with_ai(
|
|
| 277 |
user_message: str,
|
| 278 |
history: list[dict],
|
| 279 |
profile_context: str,
|
|
|
|
| 280 |
) -> dict:
|
| 281 |
"""Returns {"reply": str, "sources": list[str]}."""
|
| 282 |
is_complex = await _route_query(user_message)
|
| 283 |
-
return await _chat_with_rag(user_message, history, profile_context, is_complex=is_complex)
|
| 284 |
|
| 285 |
|
| 286 |
async def summarise_document(ocr_text: str) -> dict:
|
|
@@ -308,11 +322,11 @@ async def summarise_document(ocr_text: str) -> dict:
|
|
| 308 |
import re as _re
|
| 309 |
|
| 310 |
|
| 311 |
-
async def emotional_buddy_respond(user_text: str, history: list[dict] | None = None) -> tuple[str, int, str]:
|
| 312 |
"""Returns (buddy_reply_text, mood_score, emotion)."""
|
| 313 |
-
|
| 314 |
messages.append({"role": "user", "content": user_text})
|
| 315 |
-
raw = await _call_groq(messages,
|
| 316 |
|
| 317 |
reply = raw
|
| 318 |
mood_score = 5
|
|
|
|
| 79 |
β’ Do NOT use markdown headers (##), bold (**), or bullet-heavy formatting β write in clean prose
|
| 80 |
β’ End complex answers with a gentle reminder to consult a qualified healthcare provider
|
| 81 |
|
| 82 |
+
User medical profile context will be provided when available β use it to personalise responses.
|
| 83 |
+
|
| 84 |
+
βββ LANGUAGE βββ
|
| 85 |
+
Detect the language of the user's message (English, Hindi, or Marathi) and respond in that exact same language. Use the user's preferred language as the fallback when the language is ambiguous."""
|
| 86 |
|
| 87 |
_RAG_MEDICAL_SYSTEM = """\
|
| 88 |
You are Aarogyan's Medical Health Assistant β a supportive, evidence-based AI health companion.
|
|
|
|
| 111 |
β’ Write in clean prose β no markdown headers, no bold, no repeated ideas across paragraphs
|
| 112 |
β’ End with a brief recommendation to consult a healthcare provider if the topic warrants it
|
| 113 |
|
| 114 |
+
βββ LANGUAGE βββ
|
| 115 |
+
Detect the language of the user's message (English, Hindi, or Marathi) and respond in that exact same language. Use the user's preferred language as the fallback when the language is ambiguous.
|
| 116 |
+
|
| 117 |
--- Retrieved Medical Context ---
|
| 118 |
{context}
|
| 119 |
--- End of Context ---
|
|
|
|
| 180 |
"response": "your empathetic, warm reply here",
|
| 181 |
"mood_score": <integer 1-10>,
|
| 182 |
"emotion": "<one of: happy, sad, angry, fearful, disgusted, surprised, neutral>"
|
| 183 |
+
}
|
| 184 |
+
|
| 185 |
+
βββ LANGUAGE βββ
|
| 186 |
+
Detect the language of the user's message (English, Hindi, or Marathi) and write the "response" value in that exact same language. If the language is ambiguous, use the user's preferred language as the fallback. Always keep the JSON keys in English."""
|
| 187 |
|
| 188 |
|
| 189 |
async def _call_groq(messages: list[dict], system: str, temperature: float = 0.7) -> str:
|
|
|
|
| 231 |
history: list[dict],
|
| 232 |
profile_context: str,
|
| 233 |
is_complex: bool = False,
|
| 234 |
+
preferred_lang: str = "English",
|
| 235 |
) -> dict:
|
| 236 |
"""RAG-augmented chat: retrieve context then synthesise with Groq.
|
| 237 |
|
|
|
|
| 250 |
|
| 251 |
if not context_str:
|
| 252 |
logger.warning("RAG returned no context β falling back to plain LLM")
|
| 253 |
+
return await _chat_plain(user_message, history, profile_context, preferred_lang=preferred_lang)
|
| 254 |
|
| 255 |
profile_section = ""
|
| 256 |
if profile_context:
|
|
|
|
| 260 |
context=context_str,
|
| 261 |
profile_section=profile_section,
|
| 262 |
)
|
| 263 |
+
system += f"\n\nThe user's preferred language is {preferred_lang}."
|
| 264 |
|
| 265 |
messages = [*history, {"role": "user", "content": user_message}]
|
| 266 |
reply = await _call_groq(messages, system, temperature=0.2)
|
|
|
|
| 272 |
user_message: str,
|
| 273 |
history: list[dict],
|
| 274 |
profile_context: str,
|
| 275 |
+
preferred_lang: str = "English",
|
| 276 |
) -> dict:
|
| 277 |
"""Plain LLM chat without RAG (fallback when Qdrant returns nothing).
|
| 278 |
Returns {"reply": str, "sources": []}
|
|
|
|
| 280 |
system = MEDICAL_ASSISTANT_SYSTEM
|
| 281 |
if profile_context:
|
| 282 |
system += f"\n\n--- User Health Profile ---\n{profile_context}"
|
| 283 |
+
system += f"\n\nThe user's preferred language is {preferred_lang}."
|
| 284 |
messages = [*history, {"role": "user", "content": user_message}]
|
| 285 |
reply = await _call_groq(messages, system)
|
| 286 |
return {"reply": reply.strip(), "sources": []}
|
|
|
|
| 290 |
user_message: str,
|
| 291 |
history: list[dict],
|
| 292 |
profile_context: str,
|
| 293 |
+
preferred_lang: str = "English",
|
| 294 |
) -> dict:
|
| 295 |
"""Returns {"reply": str, "sources": list[str]}."""
|
| 296 |
is_complex = await _route_query(user_message)
|
| 297 |
+
return await _chat_with_rag(user_message, history, profile_context, is_complex=is_complex, preferred_lang=preferred_lang)
|
| 298 |
|
| 299 |
|
| 300 |
async def summarise_document(ocr_text: str) -> dict:
|
|
|
|
| 322 |
import re as _re
|
| 323 |
|
| 324 |
|
| 325 |
+
async def emotional_buddy_respond(user_text: str, history: list[dict] | None = None, preferred_lang: str = "English") -> tuple[str, int, str]:
|
| 326 |
"""Returns (buddy_reply_text, mood_score, emotion)."""
|
| 327 |
+
system = EMOTIONAL_BUDDY_SYSTEM + f"\n\nThe user's preferred language is {preferred_lang}."
|
| 328 |
messages.append({"role": "user", "content": user_text})
|
| 329 |
+
raw = await _call_groq(messages, system, temperature=0.75)
|
| 330 |
|
| 331 |
reply = raw
|
| 332 |
mood_score = 5
|
app/services/tts.py
CHANGED
|
@@ -30,12 +30,12 @@ def _split_text(text: str, limit: int = _MAX_CHARS) -> list[str]:
|
|
| 30 |
return chunks or [text[:limit]]
|
| 31 |
|
| 32 |
|
| 33 |
-
async def _fetch_chunk(client: httpx.AsyncClient, chunk: str) -> bytes:
|
| 34 |
url = "https://translate.google.com/translate_tts"
|
| 35 |
params = {
|
| 36 |
"ie": "UTF-8",
|
| 37 |
"q": chunk,
|
| 38 |
-
"tl":
|
| 39 |
"client": "tw-ob",
|
| 40 |
"total": "1",
|
| 41 |
"idx": "0",
|
|
@@ -47,7 +47,7 @@ async def _fetch_chunk(client: httpx.AsyncClient, chunk: str) -> bytes:
|
|
| 47 |
return resp.content
|
| 48 |
|
| 49 |
|
| 50 |
-
async def text_to_speech_bytes(text: str) -> bytes:
|
| 51 |
"""Convert text to speech using Google Translate TTS. Returns MP3 bytes.
|
| 52 |
Splits long text into chunks and concatenates the audio.
|
| 53 |
Raises on failure after 20 seconds total.
|
|
@@ -55,7 +55,7 @@ async def text_to_speech_bytes(text: str) -> bytes:
|
|
| 55 |
chunks = _split_text(text)
|
| 56 |
async with httpx.AsyncClient(timeout=15) as client:
|
| 57 |
parts = await asyncio.wait_for(
|
| 58 |
-
asyncio.gather(*[_fetch_chunk(client, c) for c in chunks]),
|
| 59 |
timeout=20,
|
| 60 |
)
|
| 61 |
return b"".join(parts)
|
|
|
|
| 30 |
return chunks or [text[:limit]]
|
| 31 |
|
| 32 |
|
| 33 |
+
async def _fetch_chunk(client: httpx.AsyncClient, chunk: str, lang: str = "en") -> bytes:
|
| 34 |
url = "https://translate.google.com/translate_tts"
|
| 35 |
params = {
|
| 36 |
"ie": "UTF-8",
|
| 37 |
"q": chunk,
|
| 38 |
+
"tl": lang,
|
| 39 |
"client": "tw-ob",
|
| 40 |
"total": "1",
|
| 41 |
"idx": "0",
|
|
|
|
| 47 |
return resp.content
|
| 48 |
|
| 49 |
|
| 50 |
+
async def text_to_speech_bytes(text: str, lang: str = "en") -> bytes:
|
| 51 |
"""Convert text to speech using Google Translate TTS. Returns MP3 bytes.
|
| 52 |
Splits long text into chunks and concatenates the audio.
|
| 53 |
Raises on failure after 20 seconds total.
|
|
|
|
| 55 |
chunks = _split_text(text)
|
| 56 |
async with httpx.AsyncClient(timeout=15) as client:
|
| 57 |
parts = await asyncio.wait_for(
|
| 58 |
+
asyncio.gather(*[_fetch_chunk(client, c, lang) for c in chunks]),
|
| 59 |
timeout=20,
|
| 60 |
)
|
| 61 |
return b"".join(parts)
|