Spaces:
Build error
Build error
| """ | |
| JAIM - RAG Pipeline (v3) | |
| Core Retrieval-Augmented Generation engine with: | |
| - BAAI/bge-large-en-v1.5 embeddings (1024 dims) | |
| - Query Expansion via Groq (3 rephrasings + original = 4 queries) | |
| - Deduplication by parent entry ID β single best match to LLM | |
| - Forward + Backward chaining inference engine | |
| Uses `requests` for HTTP calls for Python 3.14 compatibility. | |
| """ | |
| import os | |
| import json | |
| import time | |
| import requests | |
| from dotenv import load_dotenv | |
| from sentence_transformers import SentenceTransformer | |
| from inference.engine import VrikshayurvedaInferenceEngine | |
| load_dotenv() | |
| # βββ Config ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| PINECONE_API_KEY = os.getenv("PINECONE_API_KEY") | |
| GROQ_API_KEY = os.getenv("GROQ_API_KEY") | |
| INDEX_NAME = os.getenv("PINECONE_INDEX_NAME", "jaim3") | |
| EMBEDDING_MODEL = "BAAI/bge-large-en-v1.5" | |
| GROQ_MODEL = "llama-3.3-70b-versatile" | |
| # BGE spec: queries must be prefixed with this instruction | |
| QUERY_PREFIX = "Represent this sentence for searching relevant passages: " | |
| TOP_K = 3 | |
| class JAIMPipeline: | |
| """ | |
| JAIM RAG Pipeline v3: | |
| 1. Expands user query into 4 variants via Groq | |
| 2. Encodes all variants using BGE-large-en-v1.5 (with query prefix) | |
| 3. Retrieves top-K from Pinecone for each variant | |
| 4. Deduplicates by parent_id, keeps highest score per entry | |
| 5. Runs forward + backward chaining inference engine | |
| 6. Sends the best match's context + inference grounding to Groq LLM | |
| """ | |
| def __init__(self): | |
| print("π Initializing JAIM Pipeline...") | |
| print(" π¦ Loading BGE-large-en-v1.5 embedding model...") | |
| self.embed_model = SentenceTransformer(EMBEDDING_MODEL) | |
| print(" π² Connecting to Pinecone...") | |
| self.pinecone_host = self._get_pinecone_host() | |
| print(f" Index host: {self.pinecone_host}") | |
| print(" π€ Groq (Llama 3.3 70B) configured") | |
| print(" π§ Initializing forward/backward chaining inference engine...") | |
| self.inference_engine = VrikshayurvedaInferenceEngine() | |
| print("β JAIM Pipeline ready!\n") | |
| # βββ Pinecone Host Discovery ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _get_pinecone_host(self) -> str: | |
| """Get the Pinecone index host URL via the control plane API.""" | |
| url = f"https://api.pinecone.io/indexes/{INDEX_NAME}" | |
| headers = {"Api-Key": PINECONE_API_KEY} | |
| resp = requests.get(url, headers=headers, timeout=15) | |
| resp.raise_for_status() | |
| return resp.json()["host"] | |
| # βββ Query Expansion via Groq βββββββββββββββββββββββββββββββββββββββββββββ | |
| def expand_query(self, query: str) -> list[str]: | |
| """ | |
| Use Groq to generate 3 rephrasings of the user query. | |
| Returns [original, rephrasing1, rephrasing2, rephrasing3]. | |
| """ | |
| url = "https://api.groq.com/openai/v1/chat/completions" | |
| headers = { | |
| "Authorization": f"Bearer {GROQ_API_KEY}", | |
| "Content-Type": "application/json", | |
| } | |
| prompt = f"""Generate exactly 3 different rephrasings of the following plant symptom query. | |
| Each rephrasing should use different vocabulary while preserving the meaning. | |
| Return ONLY a valid JSON array of 3 strings. No extra text, no markdown. | |
| Original query: "{query}" | |
| """ | |
| payload = { | |
| "model": GROQ_MODEL, | |
| "messages": [ | |
| {"role": "system", "content": "You rephrase queries. Return only valid JSON arrays."}, | |
| {"role": "user", "content": prompt}, | |
| ], | |
| "temperature": 0.7, | |
| "max_tokens": 256, | |
| } | |
| for attempt in range(3): | |
| resp = requests.post(url, json=payload, headers=headers, timeout=15) | |
| if resp.status_code == 429: | |
| time.sleep(2 ** (attempt + 1)) | |
| continue | |
| resp.raise_for_status() | |
| break | |
| else: | |
| print(" β οΈ Query expansion failed (rate limited), using original only") | |
| return [query] | |
| content = resp.json()["choices"][0]["message"]["content"].strip() | |
| if content.startswith("```"): | |
| content = content.split("\n", 1)[1].rsplit("```", 1)[0].strip() | |
| try: | |
| rephrasings = json.loads(content) | |
| return [query] + rephrasings[:3] | |
| except json.JSONDecodeError: | |
| print(" β οΈ Query expansion parse error, using original only") | |
| return [query] | |
| # βββ Pinecone Retrieval βββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def retrieve_single(self, query_embedding: list, top_k: int = TOP_K) -> list[dict]: | |
| """Query Pinecone with a single embedding vector.""" | |
| url = f"https://{self.pinecone_host}/query" | |
| headers = { | |
| "Api-Key": PINECONE_API_KEY, | |
| "Content-Type": "application/json", | |
| } | |
| payload = { | |
| "vector": query_embedding, | |
| "topK": top_k, | |
| "includeMetadata": True, | |
| } | |
| resp = requests.post(url, json=payload, headers=headers, timeout=15) | |
| resp.raise_for_status() | |
| matches = [] | |
| for match in resp.json().get("matches", []): | |
| matches.append({ | |
| "id": match["id"], | |
| "score": round(match["score"], 4), | |
| "metadata": match.get("metadata", {}) | |
| }) | |
| return matches | |
| def retrieve_with_variants(self, variants: list[str], top_k: int = TOP_K) -> list[dict]: | |
| """ | |
| Retrieval pipeline using pre-expanded query variants: | |
| 1. Embed each variant with BGE-large (query prefix applied) | |
| 2. Retrieve top-K from Pinecone for each variant | |
| 3. Deduplicate by parent_id, keep highest score per entry | |
| """ | |
| print(f" π Retrieving with {len(variants)} query variants") | |
| all_matches = [] | |
| for variant in variants: | |
| prefixed = f"{QUERY_PREFIX}{variant}" | |
| embedding = self.embed_model.encode(prefixed).tolist() | |
| matches = self.retrieve_single(embedding, top_k) | |
| all_matches.extend(matches) | |
| # Deduplicate by parent_id, keep highest score per entry | |
| best_by_parent = {} | |
| for match in all_matches: | |
| parent = match["metadata"].get("parent_id", match["id"]) | |
| if parent not in best_by_parent or match["score"] > best_by_parent[parent]["score"]: | |
| best_by_parent[parent] = match | |
| # Sort by score descending | |
| deduped = sorted(best_by_parent.values(), key=lambda x: x["score"], reverse=True) | |
| return deduped | |
| # βββ Context Building βββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def build_context(self, matches: list[dict]) -> str: | |
| """Build context from the single highest-scoring deduplicated match.""" | |
| if not matches: | |
| return "No relevant entries found in the Vrikshayurveda database." | |
| # Use only the single highest-scoring match | |
| match = matches[0] | |
| meta = match["metadata"] | |
| possible_causes = meta.get("possible_causes", "N/A") | |
| if isinstance(possible_causes, list): | |
| possible_causes = ", ".join(possible_causes) | |
| return ( | |
| f"--- Best Match (Relevance: {match['score']}) ---\n" | |
| f"Disorder Type: {meta.get('disorder', 'N/A')}\n" | |
| f"Cause (Dosha): {meta.get('cause_given', 'N/A')}\n" | |
| f"Symptoms: {meta.get('symptoms', 'N/A')}\n" | |
| f"Cause Elaborated: {meta.get('cause_elaborated', 'N/A')}\n" | |
| f"Possible Causes: {possible_causes}\n" | |
| f"Ayurvedic Treatment: {meta.get('treatment_material', 'N/A')}\n" | |
| ) | |
| # βββ LLM Response Generation ββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _format_inference_context(self, inference_result) -> str: | |
| """Convert chaining inference output into compact prompt context.""" | |
| if inference_result is None: | |
| return "No deterministic diagnosis available." | |
| if hasattr(inference_result, 'llm_context'): | |
| return inference_result.llm_context | |
| return "No inference context available." | |
| def generate_response(self, query: str, context: str, inference_result=None) -> str: | |
| """Use Groq REST API (Llama 3.3 70B) to generate a response.""" | |
| system_prompt = """You are JAIM (Jnana AI for Marga β Knowledge AI for the Path), an expert Ayurvedic plant care assistant powered by the ancient Vrikshayurveda (Science of Plant Life). | |
| You help farmers, gardeners, and plant enthusiasts diagnose plant disorders and recommend traditional Ayurvedic treatments based on the Vrikshayurveda text by Surapala. | |
| IMPORTANT RULES: | |
| 1. Base your response PRIMARILY on the retrieved Vrikshayurveda knowledge provided. | |
| 2. The Inference Engine Analysis provides a formal reasoning chain β use it to validate and strengthen your response. | |
| 3. Provide the specific Ayurvedic treatment from the retrieved data based on the matching symptoms. | |
| 4. You may add brief modern context to help the user understand, but always prioritize the traditional treatment. | |
| 5. Be clear, structured, and helpful. | |
| 6. If the query doesn't match any known disorder well, say so honestly.""" | |
| llm_grounding = self._format_inference_context(inference_result) | |
| user_message = f"""βββββββββββββββββββββββββββββββββββββββ | |
| [Inference Engine Analysis] | |
| βββββββββββββββββββββββββββββββββββββββ | |
| {llm_grounding} | |
| βββββββββββββββββββββββββββββββββββββββ | |
| [Retrieved Vrikshayurveda Passages] | |
| βββββββββββββββββββββββββββββββββββββββ | |
| {context} | |
| βββββββββββββββββββββββββββββββββββββββ | |
| [User Question] | |
| βββββββββββββββββββββββββββββββββββββββ | |
| {query} | |
| Please provide a comprehensive, well-structured response:""" | |
| url = "https://api.groq.com/openai/v1/chat/completions" | |
| headers = { | |
| "Authorization": f"Bearer {GROQ_API_KEY}", | |
| "Content-Type": "application/json", | |
| } | |
| payload = { | |
| "model": GROQ_MODEL, | |
| "messages": [ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": user_message}, | |
| ], | |
| "temperature": 0.7, | |
| "max_tokens": 2048, | |
| } | |
| # Retry with backoff for rate limits | |
| for attempt in range(3): | |
| resp = requests.post(url, json=payload, headers=headers, timeout=30) | |
| if resp.status_code == 429: | |
| wait_time = 2 ** (attempt + 1) | |
| print(f" β³ Rate limited, retrying in {wait_time}s...") | |
| time.sleep(wait_time) | |
| continue | |
| resp.raise_for_status() | |
| break | |
| else: | |
| return "β οΈ Rate limit exceeded. Please try again in a minute." | |
| result = resp.json() | |
| try: | |
| return result["choices"][0]["message"]["content"] | |
| except (KeyError, IndexError): | |
| return f"Error parsing response: {json.dumps(result, indent=2)}" | |
| # βββ Full RAG Pipeline ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def query(self, user_query: str, top_k: int = TOP_K) -> dict: | |
| """ | |
| Full RAG pipeline: | |
| expand β retrieve β infer (chaining) β build context β generate response. | |
| """ | |
| # Step 1: Expand query into variants | |
| expanded_queries = self.expand_query(user_query) | |
| print(f" π Query expanded into {len(expanded_queries)} variants") | |
| # Step 2: Retrieve with pre-expanded variants | |
| matches = self.retrieve_with_variants(expanded_queries, top_k) | |
| # Step 3: Extract symptom text from retrieved matches for inference | |
| retrieved_chunks = [ | |
| m["metadata"].get("symptoms", "") | |
| for m in matches | |
| if m.get("metadata", {}).get("symptoms") | |
| ] | |
| # Step 4: Run forward + backward chaining inference engine | |
| diagnosis_result = self.inference_engine.diagnose( | |
| user_query=user_query, | |
| expanded_queries=expanded_queries, | |
| retrieved_chunks=retrieved_chunks, | |
| ) | |
| # Step 5: Build context and generate LLM response | |
| context = self.build_context(matches) | |
| response = self.generate_response( | |
| user_query, context, inference_result=diagnosis_result | |
| ) | |
| return { | |
| "query": user_query, | |
| "response": response, | |
| "diagnosis": diagnosis_result, | |
| "sources": matches[:top_k], | |
| "num_sources": len(matches) | |
| } | |
| # βββ Quick Test βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| if __name__ == "__main__": | |
| pipeline = JAIMPipeline() | |
| test_query = "My tree trunk is bent and the fruits are hard and not juicy" | |
| print(f"π Query: {test_query}\n") | |
| result = pipeline.query(test_query) | |
| print("=" * 60) | |
| print("π JAIM RESPONSE:") | |
| print("=" * 60) | |
| print(result["response"]) | |
| print("\n" + "=" * 60) | |
| print(f"π Sources used: {result['num_sources']}") | |
| for src in result["sources"]: | |
| print(f" β’ {src['id']} (score: {src['score']})") | |