|
|
import os |
|
|
os.environ["TRANSFORMERS_CACHE"] = "/app/.cache/transformers" |
|
|
os.environ["HF_HOME"] = "/app/.cache/huggingface" |
|
|
|
|
|
import uvicorn |
|
|
|
|
|
|
|
|
from fastapi import FastAPI, File, UploadFile |
|
|
from fastapi.responses import StreamingResponse |
|
|
from fastapi.middleware.cors import CORSMiddleware |
|
|
import openai |
|
|
from dotenv import load_dotenv |
|
|
from sentence_transformers import SentenceTransformer |
|
|
import math |
|
|
from collections import Counter |
|
|
import json |
|
|
import pandas as pd |
|
|
import asyncio |
|
|
import numpy as np |
|
|
from fastapi.staticfiles import StaticFiles |
|
|
from fastapi.responses import HTMLResponse |
|
|
import openai as _openai_mod |
|
|
import requests |
|
|
|
|
|
import time |
|
|
from fastapi import UploadFile, File |
|
|
from starlette.responses import StreamingResponse |
|
|
from pydub import AudioSegment |
|
|
from openai import OpenAI |
|
|
load_dotenv() |
|
|
|
|
|
client = OpenAI(api_key=os.getenv("OPENAI_API_KEY")) |
|
|
openai.api_key = os.getenv("OPENAI_API_KEY") |
|
|
|
|
|
app = FastAPI() |
|
|
|
|
|
app.add_middleware( |
|
|
CORSMiddleware, |
|
|
allow_origins=["*"], |
|
|
allow_credentials=True, |
|
|
allow_methods=["*"], |
|
|
allow_headers=["*"], |
|
|
) |
|
|
|
|
|
app.mount("/static", StaticFiles(directory="static"), name="static") |
|
|
|
|
|
@app.get("/", response_class=HTMLResponse) |
|
|
async def serve_html(): |
|
|
with open("templates/index.html", "r", encoding="utf-8") as f: |
|
|
html_content = f.read() |
|
|
return HTMLResponse(content=html_content) |
|
|
|
|
|
|
|
|
chat_messages = [{"role": "system", "content": ''' |
|
|
Your task is to answer the user queries in **telugu language**(I mean telugu characters). You are Kammi, a friendly, medical assistant specializing in orthopedic surgery, human-like voice assistant built by Facile AI Solutions |
|
|
You assist customers specifically with knee replacement surgery queries and you are the assistant of Dr.Sandeep, a highly experienced knee replacement surgeon. |
|
|
|
|
|
Rules for your responses: |
|
|
|
|
|
1. **Context-driven answers only**: Answer strictly based on the provided context and previous conversation history. Do not use external knowledge. Respond in **Telugu** language. The user only understands telugu not English. |
|
|
|
|
|
2. **General conversation**: Engage in greetings and casual conversation. If the user mentions their name, greet them personally using their name |
|
|
|
|
|
3. **Technical/medical queries**: |
|
|
- If the question is relevant to knee replacement surgery and the answer is in the context or chat history, provide the answer. |
|
|
- If the question is relevant but not present in the context, respond: "దయచేసి డాక్టర్ సందీప్ లేదా రిసెప్షన్ ను సంప్రదించండి." |
|
|
|
|
|
- Translate medical and technical terms into simple, **understandable words in Telugu** wherever possible. |
|
|
- The output must be in Telugu script, but common English medical or technical terms (like knee, hip, surgery, replacement, physiotherapy, arthritis, etc.) should be transliterated in Telugu. |
|
|
|
|
|
Example: |
|
|
“knee replacement” → మోకాలు రీప్లేస్మెంట్ |
|
|
“hip replacement” → హిప్ రీప్లేస్మెంట్ |
|
|
“surgery” → సర్జరీ |
|
|
“physiotherapy” → ఫిజియోథెరపీ |
|
|
Ensure the language sounds simple, natural, and conversational for Telugu-speaking patients. |
|
|
|
|
|
4. **Irrelevant queries**: |
|
|
- If the question is completely unrelated to knee replacement surgery, politely decline in Telugu: "నేను కేవలం మోకాలు రీప్లేస్మెంట్ సర్జరీ సంబంధిత ప్రశ్నలకు సహాయం చేస్తాను." |
|
|
|
|
|
5. **Readable voice output**: |
|
|
- Break sentences at natural punctuation: , . ? ! : ; |
|
|
- Do not use #, **, or other markdown symbols. |
|
|
Telugu Output Guidelines: |
|
|
All numbers, decimals, and points MUST be fully spelled out in Telugu words. |
|
|
Example: 2.5 lakh → రెండు లక్షల యాభై వేల రూపాయలు |
|
|
|
|
|
6. **Concise and human-like**: |
|
|
- Keep answers short, conversational, and natural |
|
|
- Maximum 40 words / ~20 seconds of speech. |
|
|
|
|
|
7. **Tone and style**: |
|
|
- Helpful, friendly, approachable, and human-like. |
|
|
- Maintain professionalism while being conversational. |
|
|
|
|
|
8. **About Dr.Sandeep**: |
|
|
- Over 5 years of experience in orthopedic and joint replacement surgery. |
|
|
- Specializes in total and partial knee replacement procedures. |
|
|
- Known for a patient-friendly approach, focusing on pre-surgery preparation, post-surgery rehabilitation, and pain management. |
|
|
- Actively keeps up-to-date with the latest techniques and technologies in knee replacement surgery. |
|
|
- Highly approachable and prefers that patients are well-informed about their treatment options and recovery process. |
|
|
|
|
|
Always provide readable, streaming-friendly sentences in **Telugu** language so that output is read smoothly. Drive conversation forward while staying strictly on knee replacement surgery topics, and suggest follow-up questions for which you have context-based answers. |
|
|
'''}] |
|
|
|
|
|
class BM25: |
|
|
def __init__(self, corpus, k1=1.2, b=0.75): |
|
|
self.corpus = [doc.split() if isinstance(doc, str) else doc for doc in corpus] |
|
|
self.k1 = k1 |
|
|
self.b = b |
|
|
self.N = len(self.corpus) |
|
|
self.avgdl = sum(len(doc) for doc in self.corpus) / self.N |
|
|
self.doc_freqs = self._compute_doc_frequencies() |
|
|
self.idf = self._compute_idf() |
|
|
|
|
|
def _compute_doc_frequencies(self): |
|
|
"""Count how many documents contain each term""" |
|
|
df = {} |
|
|
for doc in self.corpus: |
|
|
unique_terms = set(doc) |
|
|
for term in unique_terms: |
|
|
df[term] = df.get(term, 0) + 1 |
|
|
return df |
|
|
|
|
|
def _compute_idf(self): |
|
|
"""Compute the IDF for each term in the corpus""" |
|
|
idf = {} |
|
|
for term, df in self.doc_freqs.items(): |
|
|
idf[term] = math.log((self.N - df + 0.5) / (df + 0.5) + 1) |
|
|
return idf |
|
|
|
|
|
def score(self, query, document): |
|
|
"""Compute the BM25 score for one document and one query""" |
|
|
query_terms = query.split() if isinstance(query, str) else query |
|
|
doc_terms = document.split() if isinstance(document, str) else document |
|
|
score = 0.0 |
|
|
freqs = Counter(doc_terms) |
|
|
doc_len = len(doc_terms) |
|
|
|
|
|
for term in query_terms: |
|
|
if term not in freqs: |
|
|
continue |
|
|
f = freqs[term] |
|
|
idf = self.idf.get(term, 0) |
|
|
denom = f + self.k1 * (1 - self.b + self.b * doc_len / self.avgdl) |
|
|
score += idf * (f * (self.k1 + 1)) / denom |
|
|
return score |
|
|
|
|
|
def rank(self, query): |
|
|
"""Rank all documents for a given query""" |
|
|
return [(i, self.score(query, doc)) for i, doc in enumerate(self.corpus)] |
|
|
|
|
|
|
|
|
def sigmoid_scaled(x, midpoint=3.0): |
|
|
""" |
|
|
Sigmoid function with shifting. |
|
|
`midpoint` controls where the output is 0.5. |
|
|
""" |
|
|
return 1 / (1 + math.exp(-(x - midpoint))) |
|
|
|
|
|
def cosine_similarity(a: np.ndarray, b: np.ndarray) -> float: |
|
|
|
|
|
return np.dot(a, b) / (np.linalg.norm(a) * np.linalg.norm(b)) |
|
|
|
|
|
async def compute_similarity(query: str, query_embedding: np.ndarray, chunk_text: str, chunk_embedding: np.ndarray, sem_weight: float,syn_weight:float,bm25) -> float: |
|
|
|
|
|
semantic_score = cosine_similarity(query_embedding, chunk_embedding) |
|
|
|
|
|
|
|
|
syntactic_score = bm25.score(query,chunk_text) |
|
|
final_syntactic_score = sigmoid_scaled(syntactic_score) |
|
|
|
|
|
combined_score = sem_weight * semantic_score + syn_weight * final_syntactic_score |
|
|
|
|
|
return combined_score |
|
|
|
|
|
async def retrieve_top_k_hybrid(query, k, sem_weight,syn_weight,bm25): |
|
|
emb_strt = time.time() |
|
|
query_embedding = model.encode(query) |
|
|
emb_end = time.time() |
|
|
print("\n\nTime for Query Embedding", emb_end-emb_strt) |
|
|
|
|
|
tasks = [ |
|
|
|
|
|
compute_similarity(query, query_embedding, row["Chunks"], row["Embeddings"] , sem_weight,syn_weight,bm25) |
|
|
|
|
|
for _, row in df_expanded.iterrows() |
|
|
|
|
|
] |
|
|
|
|
|
similarities = await asyncio.gather(*tasks) |
|
|
|
|
|
df_expanded["similarity"] = similarities |
|
|
|
|
|
top_results = df_expanded.sort_values(by="similarity", ascending=False).head(k) |
|
|
|
|
|
|
|
|
|
|
|
print("\n\nRetrieval Time", time.time() - emb_end) |
|
|
return top_results["telugu_chunk"].to_list() |
|
|
|
|
|
|
|
|
os.makedirs("/tmp/transformers_cache", exist_ok=True) |
|
|
|
|
|
model = SentenceTransformer("abhinand/MedEmbed-large-v0.1") |
|
|
df_expanded = pd.read_excel("Database.xlsx") |
|
|
df_expanded["Embeddings"] = df_expanded["Embeddings"].map(lambda x: json.loads(x)) |
|
|
corpus = df_expanded['Chunks'].to_list() |
|
|
bm25 = BM25(corpus) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def tts_chunk_stream(text_chunk: str, lang: str = "en"): |
|
|
""" |
|
|
REST-based OpenAI TTS fallback for older openai SDKs (e.g. 0.28). |
|
|
Returns a generator yielding MP3 byte chunks (1024 bytes). |
|
|
""" |
|
|
if not text_chunk or not text_chunk.strip(): |
|
|
return [] |
|
|
|
|
|
|
|
|
language_map = { |
|
|
"en": "en-US", |
|
|
"en-US": "en-US", |
|
|
"en-GB": "en-GB", |
|
|
"hi": "hi-IN", |
|
|
} |
|
|
language_code = language_map.get(lang, "en-GB") |
|
|
|
|
|
|
|
|
model = "gpt-4o-mini-tts" |
|
|
voice = "alloy" |
|
|
fmt = "mp3" |
|
|
|
|
|
|
|
|
api_key = None |
|
|
try: |
|
|
|
|
|
api_key = getattr(_openai_mod, "api_key", None) |
|
|
except Exception: |
|
|
api_key = None |
|
|
|
|
|
if not api_key: |
|
|
api_key = os.getenv("OPENAI_API_KEY") |
|
|
|
|
|
if not api_key: |
|
|
print("OpenAI API key not found. Set openai.api_key or env var OPENAI_API_KEY.") |
|
|
return [] |
|
|
|
|
|
url = "https://api.openai.com/v1/audio/speech" |
|
|
headers = { |
|
|
"Authorization": f"Bearer {api_key}", |
|
|
"Content-Type": "application/json", |
|
|
} |
|
|
|
|
|
payload = { |
|
|
"model": model, |
|
|
"voice": voice, |
|
|
"input": text_chunk, |
|
|
"format": fmt, |
|
|
"temperature" : 0 |
|
|
|
|
|
|
|
|
} |
|
|
|
|
|
try: |
|
|
|
|
|
resp = requests.post(url, headers=headers, json=payload, stream=True, timeout=60) |
|
|
except Exception as e: |
|
|
print("OpenAI TTS request failed:", e) |
|
|
return [] |
|
|
|
|
|
if resp.status_code != 200: |
|
|
|
|
|
try: |
|
|
err = resp.json() |
|
|
except Exception: |
|
|
err = resp.text |
|
|
print(f"OpenAI TTS REST call failed {resp.status_code}: {err}") |
|
|
try: |
|
|
resp.close() |
|
|
except Exception: |
|
|
pass |
|
|
return [] |
|
|
|
|
|
|
|
|
def audio_stream(): |
|
|
try: |
|
|
for chunk in resp.iter_content(chunk_size=1024): |
|
|
if chunk: |
|
|
yield chunk |
|
|
finally: |
|
|
try: |
|
|
resp.close() |
|
|
except Exception: |
|
|
pass |
|
|
return audio_stream() |
|
|
|
|
|
|
|
|
async def get_rag_response(user_message_english: str, user_message_telugu: str): |
|
|
global chat_messages |
|
|
start_time = time.time() |
|
|
Chunks = await retrieve_top_k_hybrid(user_message_english,15, 0.9, 0.1,bm25) |
|
|
end_time = time.time() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
context = "======================================================================================================\n".join(map(str,Chunks)) |
|
|
chat_messages.append({"role": "user", "content": f''' |
|
|
Context : {context} |
|
|
User Query: {user_message_telugu}'''}) |
|
|
|
|
|
return [chat_messages[0]]+chat_messages[-7:] |
|
|
|
|
|
|
|
|
|
|
|
async def gpt_tts_stream(prompt: str,telugu_text: str): |
|
|
global chat_messages |
|
|
chat_messages = await get_rag_response(prompt,telugu_text) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
bot_response = "" |
|
|
buffer = "" |
|
|
buffer_size = 30 |
|
|
count1 = 0 |
|
|
count2 = 0 |
|
|
count3 = 0 |
|
|
count4 = 0 |
|
|
|
|
|
start_time = time.time() |
|
|
with client.chat.completions.stream( |
|
|
model="gpt-4o", |
|
|
messages=chat_messages, |
|
|
) as stream: |
|
|
for event in stream: |
|
|
if count1 == 0: |
|
|
end_time = time.time() |
|
|
|
|
|
|
|
|
print(f"gpt duration for first token : {end_time - start_time}") |
|
|
count1 += 1 |
|
|
if event.type == "content.delta": |
|
|
delta = event.delta |
|
|
bot_response = bot_response + delta |
|
|
buffer += delta |
|
|
if len(buffer) >= buffer_size and buffer.endswith((".", "!", ",", "?", "\n", ";", ":")): |
|
|
if count2 == 0: |
|
|
count2 += 1 |
|
|
end_time = time.time() |
|
|
|
|
|
print(f"gpt duration for first buffer : {end_time - start_time}") |
|
|
print(buffer) |
|
|
|
|
|
start_time = time.time() |
|
|
for audio_chunk in tts_chunk_stream(buffer): |
|
|
if count3 == 0: |
|
|
count3+=1 |
|
|
end_time = time.time() |
|
|
|
|
|
|
|
|
print(f"tts duration for first buffer : {end_time - start_time}") |
|
|
|
|
|
yield audio_chunk |
|
|
buffer = "" |
|
|
|
|
|
|
|
|
|
|
|
elif event.type == "content.done": |
|
|
|
|
|
if buffer.strip(): |
|
|
start_time = time.time() |
|
|
|
|
|
print(buffer.strip()) |
|
|
for audio_chunk in tts_chunk_stream(buffer): |
|
|
|
|
|
|
|
|
yield audio_chunk |
|
|
|
|
|
|
|
|
start_time = time.time() |
|
|
|
|
|
|
|
|
bot_response = bot_response.strip() |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
chat_messages.append({"role": "assistant", "content": bot_response}) |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@app.post("/chat_stream") |
|
|
async def chat_stream(file: UploadFile = File(...)): |
|
|
start_time = time.time() |
|
|
audio_bytes = await file.read() |
|
|
|
|
|
transcription = client.audio.transcriptions.create( |
|
|
model="gpt-4o-transcribe", |
|
|
file=(file.filename, audio_bytes), |
|
|
language="te", |
|
|
prompt="Medical terms related to knee replacement surgery" |
|
|
) |
|
|
|
|
|
telugu_text = transcription.text |
|
|
end_time = time.time() |
|
|
|
|
|
|
|
|
print(f"transcription total time : {end_time-start_time}") |
|
|
print(f"the text is : {telugu_text}") |
|
|
|
|
|
start_time = time.time() |
|
|
translation = client.responses.create( |
|
|
model="gpt-4o-mini", |
|
|
temperature = 0, |
|
|
top_p = 0, |
|
|
input=f''' your task is to Translate the following Telugu user query into English: |
|
|
{telugu_text} |
|
|
Give only the english translation, These queries are generally relevant to knee replacement surgery. Make sure you correct minor mistakes and return the user query in a proper english.''') |
|
|
|
|
|
english_text = translation.output[0].content[0].text |
|
|
end_time = time.time() |
|
|
|
|
|
|
|
|
|
|
|
print(f"translation total time : {end_time-start_time}") |
|
|
print(f"the english text is : {english_text}") |
|
|
|
|
|
return StreamingResponse(gpt_tts_stream(english_text,telugu_text), media_type="audio/mpeg") |
|
|
|
|
|
@app.post("/reset_chat") |
|
|
async def reset_chat(): |
|
|
global chat_messages |
|
|
chat_messages = [{"role": "system", "content": ''' |
|
|
Your task is to answer the user queries in **telugu language**(I mean telugu characters). You are Kammi, a friendly, medical assistant specializing in orthopedic surgery, human-like voice assistant built by Facile AI Solutions |
|
|
You assist customers specifically with knee replacement surgery queries and you are the assistant of Dr.Sandeep, a highly experienced knee replacement surgeon. |
|
|
|
|
|
Rules for your responses: |
|
|
|
|
|
1. **Context-driven answers only**: Answer strictly based on the provided context and previous conversation history. Do not use external knowledge. Respond in **Telugu** language. The user only understands telugu not English. |
|
|
|
|
|
2. **General conversation**: Engage in greetings and casual conversation. If the user mentions their name, greet them personally using their name |
|
|
|
|
|
3. **Technical/medical queries**: |
|
|
- If the question is relevant to knee replacement surgery and the answer is in the context or chat history, provide the answer. |
|
|
- If the question is relevant but not present in the context, respond: "దయచేసి డాక్టర్ సందీప్ లేదా రిసెప్షన్ ను సంప్రదించండి." |
|
|
|
|
|
- Translate medical and technical terms into simple, **understandable words in Telugu** wherever possible. |
|
|
- The output must be in Telugu script, but common English medical or technical terms (like knee, hip, surgery, replacement, physiotherapy, arthritis, etc.) should be transliterated in Telugu. |
|
|
|
|
|
Example: |
|
|
“knee replacement” → మోకాలు రీప్లేస్మెంట్ |
|
|
“hip replacement” → హిప్ రీప్లేస్మెంట్ |
|
|
“surgery” → సర్జరీ |
|
|
“physiotherapy” → ఫిజియోథెరపీ |
|
|
Ensure the language sounds simple, natural, and conversational for Telugu-speaking patients. |
|
|
|
|
|
4. **Irrelevant queries**: |
|
|
- If the question is completely unrelated to knee replacement surgery, politely decline in Telugu: "నేను కేవలం మోకాలు రీప్లేస్మెంట్ సర్జరీ సంబంధిత ప్రశ్నలకు సహాయం చేస్తాను." |
|
|
|
|
|
5. **Readable voice output**: |
|
|
- Break sentences at natural punctuation: , . ? ! : ; |
|
|
- Do not use #, **, or other markdown symbols. |
|
|
Telugu Output Guidelines: |
|
|
All numbers, decimals, and points MUST be fully spelled out in Telugu words. |
|
|
Example: 2.5 lakh → రెండు లక్షల యాభై వేల రూపాయలు |
|
|
|
|
|
6. **Concise and human-like**: |
|
|
- Keep answers short, conversational, and natural |
|
|
- Maximum 40 words / ~20 seconds of speech. |
|
|
|
|
|
7. **Tone and style**: |
|
|
- Helpful, friendly, approachable, and human-like. |
|
|
- Maintain professionalism while being conversational. |
|
|
|
|
|
8. **About Dr.Sandeep**: |
|
|
- Over 5 years of experience in orthopedic and joint replacement surgery. |
|
|
- Specializes in total and partial knee replacement procedures. |
|
|
- Known for a patient-friendly approach, focusing on pre-surgery preparation, post-surgery rehabilitation, and pain management. |
|
|
- Actively keeps up-to-date with the latest techniques and technologies in knee replacement surgery. |
|
|
- Highly approachable and prefers that patients are well-informed about their treatment options and recovery process. |
|
|
|
|
|
Always provide readable, streaming-friendly sentences in **Telugu** language so that output is read smoothly. Drive conversation forward while staying strictly on knee replacement surgery topics, and suggest follow-up questions for which you have context-based answers. |
|
|
'''}] |
|
|
|
|
|
return {"message": "Chat history reset successfully."} |