Spaces:
Sleeping
Sleeping
| """ | |
| rag.py | |
| ───────────────────────────────────────────── | |
| Core RAG (Retrieval Augmented Generation) pipeline. | |
| Loads the vector store, retrieves relevant chunks for a query, | |
| and uses Groq's free Llama 3 API to generate an answer. | |
| Includes: | |
| - Improved retrieval (more chunks, relevance scoring) | |
| - Fallback to general medical knowledge when verified docs | |
| don't have enough info — clearly labeled either way. | |
| ───────────────────────────────────────────── | |
| """ | |
| import os | |
| from dotenv import load_dotenv | |
| from langchain_community.embeddings import HuggingFaceEmbeddings | |
| from langchain_community.vectorstores import Chroma | |
| from langchain_groq import ChatGroq | |
| from langchain.prompts import ChatPromptTemplate | |
| load_dotenv() | |
| BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) | |
| VECTORSTORE_DIR = os.path.join(BASE_DIR, "vectorstore") | |
| GROQ_API_KEY = os.getenv("GROQ_API_KEY") | |
| # Relevance threshold — Chroma returns L2 distance (lower = more similar). | |
| # Chunks above this distance are considered "not relevant enough" to trust. | |
| RELEVANCE_DISTANCE_THRESHOLD_ENGLISH = 1.25 | |
| RELEVANCE_DISTANCE_THRESHOLD_OTHER = 1.15 | |
| # ───────────────────────────────────────────── | |
| # PROMPT — grounded answer (verified sources found) | |
| # ───────────────────────────────────────────── | |
| GROUNDED_PROMPT = """You are a helpful Rural Healthcare Assistant. Answer the question using \ | |
| ONLY the verified context below, which comes from government health guidelines and medical \ | |
| knowledge sources. | |
| Rules: | |
| 1. Answer clearly and simply — assume the person may not have medical knowledge. | |
| 2. Keep answers concise but complete — use simple language, avoid jargon. | |
| 3. For anything serious or urgent, always recommend visiting the nearest health center or doctor. | |
| 4. Respond in the same language the question was asked in. | |
| 5. Do not mention "the context" or "the document" explicitly — just answer naturally. | |
| 6. Always respond in the same language as the question shown above (in the "Question:" line below) — typically English unless the question itself contains Hindi or another language. | |
| Conversation so far: | |
| {history} | |
| Context: | |
| {context} | |
| Question: {question} | |
| Answer:""" | |
| # ───────────────────────────────────────────── | |
| # PROMPT — general fallback (no good verified match found) | |
| # ───────────────────────────────────────────── | |
| FALLBACK_PROMPT = """You are a helpful Rural Healthcare Assistant. The verified knowledge base \ | |
| does NOT contain enough information to answer this question, so you must answer using your own \ | |
| general medical knowledge instead. | |
| Rules: | |
| 1. Answer clearly and simply — assume the person may not have medical knowledge. | |
| 2. Keep answers concise but complete. | |
| 3. Always recommend visiting the nearest health center or doctor for confirmation, since this \ | |
| answer is not from a verified local source. | |
| 4. Respond in the same language the question was asked in. | |
| 5. Be accurate and conservative — if you are not confident, say so honestly. | |
| 6. Always respond in the same language as the question shown above (in the "Question:" line below) — typically English unless the question itself contains Hindi or another language. | |
| Conversation so far: | |
| {history} | |
| Question: {question} | |
| Answer:""" | |
| def load_vectorstore(): | |
| """Load the persisted Chroma vector store.""" | |
| if not os.path.exists(VECTORSTORE_DIR): | |
| raise FileNotFoundError( | |
| "Vector store not found! Please run 'python src/ingest.py' first." | |
| ) | |
| embeddings = HuggingFaceEmbeddings( | |
| model_name="sentence-transformers/all-MiniLM-L6-v2" | |
| ) | |
| vectordb = Chroma( | |
| persist_directory=VECTORSTORE_DIR, | |
| embedding_function=embeddings | |
| ) | |
| return vectordb | |
| def get_llm(): | |
| """Initialize the free Groq-hosted Llama 3 model.""" | |
| if not GROQ_API_KEY or GROQ_API_KEY == "paste_your_groq_api_key_here": | |
| raise ValueError( | |
| "GROQ_API_KEY not set! Please add your key to the .env file." | |
| ) | |
| return ChatGroq( | |
| groq_api_key=GROQ_API_KEY, | |
| model_name="llama-3.1-8b-instant", | |
| temperature=0.3, | |
| max_tokens=600, | |
| ) | |
| def answer_question(query, vectordb, llm, chat_history=None, k=20): | |
| """ | |
| Retrieve relevant chunks for the query and generate an answer. | |
| Falls back to general knowledge (clearly labeled) if no chunk is relevant enough. | |
| Returns: (answer_text, list_of_source_documents, is_verified: bool) | |
| """ | |
| history_text = "" | |
| import re | |
| current_is_hindi = bool(re.search(r'[\u0900-\u097F\u0A00-\u0A7F]', query)) | |
| followup_signals = ["it", "this", "that", "they", "them", "इसे", "यह", "वह", "ਇਹ", "ਉਹ"] | |
| is_likely_followup = any(f" {word} " in f" {query.lower()} " for word in followup_signals) or len(query.split()) <= 4 | |
| if chat_history and is_likely_followup: | |
| for h in chat_history[-2:]: | |
| msg_is_hindi = bool(re.search(r'[\u0900-\u097F\u0A00-\u0A7F]', h.get("content", ""))) | |
| if msg_is_hindi == current_is_hindi: | |
| role = "User" if h["role"] == "user" else "Assistant" | |
| history_text += f"{role}: {h['content']}\n" | |
| # Step 1: Retrieve relevant chunks WITH relevance scores | |
| import re | |
| has_non_latin = bool(re.search(r'[\u0900-\u097F\u0A00-\u0A7F]', query)) | |
| results = vectordb.similarity_search_with_score(query, k=k) | |
| if not has_non_latin: | |
| results = [(doc, score) for doc, score in results | |
| if doc.metadata.get("category") != "patient_record"] | |
| # Step 2: Filter to only chunks that pass the relevance threshold | |
| threshold = RELEVANCE_DISTANCE_THRESHOLD_OTHER if has_non_latin else RELEVANCE_DISTANCE_THRESHOLD_ENGLISH | |
| relevant_results = [(doc, score) for doc, score in results if score <= threshold] | |
| if not relevant_results: | |
| # ── FALLBACK: no good match found in verified docs ── | |
| try: | |
| prompt = ChatPromptTemplate.from_template(FALLBACK_PROMPT) | |
| chain = prompt | llm | |
| response = chain.invoke({"question": query, "history": history_text}) | |
| return response.content, [], False | |
| except Exception as e: | |
| return "⚠️ The service is temporarily busy. Please try again in a moment.", [], None | |
| # ── GROUNDED: build context from relevant chunks ── | |
| context_parts = [] | |
| sources = [] | |
| for doc, score in relevant_results[:5]: | |
| context_parts.append(doc.page_content) | |
| source_name = doc.metadata.get("source", "Unknown") | |
| if source_name not in sources: | |
| sources.append(source_name) | |
| context = "\n\n---\n\n".join(context_parts) | |
| try: | |
| prompt = ChatPromptTemplate.from_template(GROUNDED_PROMPT) | |
| chain = prompt | llm | |
| response = chain.invoke({ | |
| "context": context, | |
| "question": query, | |
| "history": history_text | |
| }) | |
| return response.content, sources, True | |
| except Exception as e: | |
| return "⚠️ The service is temporarily busy. Please try again in a moment.", [], None | |
| SYMPTOM_CHECKER_PROMPT = """You are a careful Rural Healthcare symptom-checker assistant. \ | |
| Based on the symptoms described below, provide a structured assessment. | |
| Rules: | |
| 1. List 2-3 possible (NOT definitive) conditions that match these symptoms. | |
| 2. Rate urgency as: LOW, MEDIUM, or HIGH. | |
| 3. Give clear next-step advice (home care vs see doctor vs emergency). | |
| 4. Always state this is NOT a diagnosis. | |
| 5. Respond in the same language as the input. | |
| Symptoms described: {symptoms} | |
| Provide your assessment in this format: | |
| **Possible considerations:** ... | |
| **Urgency level:** ... | |
| **Recommended action:** ... | |
| """ | |
| def check_symptoms(symptoms_text, llm): | |
| """Structured symptom assessment — separate from general Q&A.""" | |
| prompt = ChatPromptTemplate.from_template(SYMPTOM_CHECKER_PROMPT) | |
| chain = prompt | llm | |
| response = chain.invoke({"symptoms": symptoms_text}) | |
| return response.content | |
| if __name__ == "__main__": | |
| # Quick test mode — run this file directly to test in terminal | |
| print("🔧 Loading vector store...") | |
| vectordb = load_vectorstore() | |
| print("🔧 Loading LLM...") | |
| llm = get_llm() | |
| print("\n✅ Ready! Ask a health question (type 'quit' to exit)\n") | |
| while True: | |
| query = input("🙋 You: ") | |
| if query.lower() in ["quit", "exit"]: | |
| break | |
| answer, sources, is_verified = answer_question(query, vectordb, llm) | |
| tag = "✅ VERIFIED" if is_verified else "🌐 GENERAL KNOWLEDGE" | |
| print(f"\n🤖 [{tag}] Assistant: {answer}") | |
| if sources: | |
| print(f"\n📚 Sources: {', '.join(sources)}") | |
| print() | |