Spaces:
Running
Running
| """ | |
| groq_service.py — Anti-Hallucination LLM Layer | |
| ================================================= | |
| Groq (LLaMA3-70B) is used ONLY for: | |
| 1. Fallback query parsing (when rule-based router fails) | |
| 2. Generating human-friendly explanations of VERIFIED data | |
| 3. Formatting/tone — never as a source of truth | |
| Key safety features: | |
| - System prompts explicitly forbid guessing or inventing data | |
| - Temperature kept very low (0.05 for facts, 0.2 for explanations) | |
| - Data summaries exclude raw arrays to keep prompts focused | |
| - Fallback text returned if LLM is unavailable | |
| """ | |
| import os | |
| import json | |
| import re | |
| import logging | |
| import hashlib | |
| from datetime import datetime | |
| from typing import Optional, Dict, Any | |
| from groq import Groq | |
| from dotenv import load_dotenv | |
| from cachetools import TTLCache | |
| from tenacity import retry, stop_after_attempt, wait_exponential, retry_if_exception_type | |
| # Load environment variables for local testing | |
| load_dotenv() | |
| # GROQ_API_KEY is now retrieved directly from the environment (e.g., HF Secrets) | |
| GROQ_API_KEY = os.environ.get("GROQ_API_KEY", "") | |
| MODEL_NAME = "llama-3.3-70b-versatile" | |
| # Initializing global client with timeout | |
| client = None | |
| if GROQ_API_KEY: | |
| try: | |
| client = Groq(api_key=GROQ_API_KEY, timeout=10.0) | |
| except Exception as e: | |
| logging.error(f"Failed to initialize Groq client: {e}") | |
| # Persistent-like in-memory cache for parsed queries (TTL: 1 hour) | |
| _query_cache = TTLCache(maxsize=500, ttl=3600) | |
| # --------------------------------------------------------------------------- | |
| # ANTI-HALLUCINATION SYSTEM PROMPTS | |
| # --------------------------------------------------------------------------- | |
| SYSTEM_PROMPT_PARSER = ( | |
| "You are a specialized financial query parsing engine. " | |
| "You extract structured information from user queries. " | |
| "NEVER invent or guess information. If a field cannot be determined, use null. " | |
| "Return ONLY valid JSON. No conversation, no markdown." | |
| ) | |
| SYSTEM_PROMPT_INSIGHT = ( | |
| "You are a senior financial advisor explaining pre-verified market data to beginners. " | |
| "STRICT RULES:\n" | |
| "1. You are explaining data that has ALREADY been verified. Do NOT invent any numbers.\n" | |
| "2. Be concise, professional, and empathetic.\n" | |
| "3. Use standard paragraph formatting with single line breaks between points. No double spacing or extra spaces.\n" | |
| "4. Use only the data provided to you. Do NOT add statistics, prices, or percentages that are not in the input.\n" | |
| "5. If you're unsure about something, say 'Based on the available data' instead of guessing.\n" | |
| "6. NEVER use phrases like 'guaranteed profit', 'risk-free', or 'sure shot'.\n" | |
| "7. Use simple, clear, beginner-friendly language.\n" | |
| "8. Be polite, motivating, and realistic - no false promises." | |
| ) | |
| SYSTEM_PROMPT_EDUCATION = ( | |
| "You are a financial educator explaining concepts to absolute beginners. " | |
| "STRICT RULES:\n" | |
| "1. Explain ONLY the topic given. Do NOT add unrelated information.\n" | |
| "2. If you are not confident about a specific detail, say 'I'm not fully sure about this specific detail'.\n" | |
| "3. Use analogies and simple language — imagine explaining to a college student.\n" | |
| "4. NEVER recommend specific stocks, funds, or financial products by name.\n" | |
| "5. Always mention that the user should consult a financial advisor for personalized advice.\n" | |
| "6. Keep response under 200 words.\n" | |
| "7. Return response in a structured JSON format if requested." | |
| ) | |
| SYSTEM_PROMPT_STRUCTURED = ( | |
| "You are a financial information architect. " | |
| "You provide data in strict JSON format for a mobile application UI. " | |
| "NEVER include markdown code fences or conversational text in the response. " | |
| "Your output must be parseable by a standard JSON decoder." | |
| ) | |
| def _cache_key(text: str) -> str: | |
| return hashlib.md5(text.strip().lower().encode()).hexdigest() | |
| def _clean_json_response(text: str) -> str: | |
| """Extract JSON from LLM response, stripping markdown fences and common garbage.""" | |
| # Remove markdown code blocks | |
| text = re.sub(r"```json\s*", "", text) | |
| text = re.sub(r"```\s*", "", text) | |
| text = text.strip() | |
| # Find the first '{' and last '}' | |
| start_idx = text.find("{") | |
| end_idx = text.rfind("}") | |
| if start_idx != -1 and end_idx != -1 and end_idx > start_idx: | |
| return text[start_idx : end_idx + 1] | |
| return text | |
| def _call_groq_api(messages: list, response_format: Optional[Dict[str, str]] = None, temperature: float = 0.1, max_tokens: int = 500) -> str: | |
| """Internal helper to call Groq with retries.""" | |
| if not client: | |
| raise RuntimeError("Groq client not initialized") | |
| kwargs = { | |
| "messages": messages, | |
| "model": MODEL_NAME, | |
| "temperature": temperature, | |
| "max_tokens": max_tokens, | |
| } | |
| if response_format: | |
| kwargs["response_format"] = response_format | |
| completion = client.chat.completions.create(**kwargs) | |
| return completion.choices[0].message.content | |
| def parse_query_with_llm(query: str, today: str = "") -> dict: | |
| """ | |
| Use Groq (LLaMA3-70B) to extract structured financial intent. | |
| NOTE: This is now a FALLBACK — called only when query_router rules fail. | |
| Supports English and Hindi natively. | |
| """ | |
| if not client: | |
| logging.error("Groq client missing. Using fallback.") | |
| return {} | |
| if not today: | |
| today = datetime.now().strftime("%Y-%m-%d") | |
| cache_idx = _cache_key(query) | |
| if cache_idx in _query_cache: | |
| logging.info(f"Groq parse cache hit: {query[:30]}...") | |
| return _query_cache[cache_idx] | |
| prompt = f""" | |
| Today's date is {today}. | |
| Extract the following fields from the user query into a valid JSON object: | |
| - intent: "analysis" (default) | "strategy" (if mentions 'bought/if i/profit/return') | "education" (if asks 'what is/how to/define') | "guide" (if asks about account opening/getting started) | "irrelevant" | |
| - asset_name: Full name of stock, commodity, or crypto. null if not mentioned. | |
| - symbol: The exchange-ready ticker if identifiable. | |
| - Gold -> GC=F, Silver -> SI=F, Crude -> CL=F | |
| - Bitcoin -> BTC-USD, Ethereum -> ETH-USD | |
| - Indian Stocks -> Symbol.NS (e.g., Reliance -> RELIANCE.NS, MRF -> MRF.NS) | |
| - US Stocks -> Symbol (e.g., Apple -> AAPL, Tesla -> TSLA) | |
| - quantity: Numeric amount user holds/wants to buy. null if not mentioned. | |
| - unit: "gram" (default for gold/silver) | "shares" (default for stocks) | "oz" | "kg" | |
| - investment_amount: Total money mentioned (e.g., "invested 1 lakh" -> 100000). null if not mentioned. | |
| - buy_date: YYYY-MM-DD. | |
| - If query says "5 years ago", "since last year", "bought on 5th March 2023", CALCULATE the exact date based on {today}. | |
| - null if not mentioned. | |
| - timeframe: "1mo" | "3mo" | "6mo" | "1y" | "5y". Default "6mo". | |
| - cleaned_query: The query without filler words. | |
| IMPORTANT: | |
| - If the user asks about multiple stocks, focus on the primary one mentioned. | |
| - If you cannot determine a field with confidence, use null. | |
| - Return ONLY the JSON object. No conversation, no markdown. | |
| User Query: "{query}" | |
| """ | |
| try: | |
| raw_output = _call_groq_api( | |
| messages=[ | |
| {"role": "system", "content": SYSTEM_PROMPT_PARSER}, | |
| {"role": "user", "content": prompt}, | |
| ], | |
| response_format={"type": "json_object"}, | |
| temperature=0.05, # Very low — deterministic parsing | |
| ) | |
| cleaned = _clean_json_response(raw_output) | |
| result = json.loads(cleaned) | |
| # Post-process for consistency | |
| if "intent" not in result: | |
| result["intent"] = "analysis" | |
| # Mark as LLM-parsed | |
| result["_parsed_by"] = "llm" | |
| # Store in cache | |
| _query_cache[cache_idx] = result | |
| logging.info(f"Groq Parsed Successfully: {json.dumps(result)}") | |
| return result | |
| except Exception as e: | |
| logging.error(f"Groq parse failed after retries for '{query}': {e}") | |
| return {} | |
| def generate_insight(data: dict, intent: str = "analysis") -> str: | |
| """ | |
| Generate human-friendly, professional financial insight using Groq. | |
| CRITICAL: The LLM is explaining PRE-VERIFIED data, not generating new facts. | |
| All numbers in the prompt come from backend calculations. | |
| """ | |
| if not client: | |
| return _get_fallback_insight(data, intent) | |
| # Prevent large data blobs in prompt | |
| data_summary = {k: v for k, v in data.items() if k not in ["prices", "volumes", "dates", "prices_inr_per_gram"]} | |
| if intent == "strategy": | |
| prompt = f""" | |
| Analyze this investment performance using ONLY the data provided below. | |
| DATA (verified): | |
| {json.dumps(data_summary)} | |
| Investment: ₹{data.get('investment')} | |
| Current Value: ₹{data.get('current_value')} | |
| Profit/Loss: ₹{data.get('profit_inr')} ({data.get('profit_pct')}%) | |
| Write EXACTLY 4 points, clearly separated by newlines: | |
| 1. Summarize the performance using ONLY the numbers above. | |
| 2. Mention one factor about the asset's volatility or trend direction. | |
| 3. Provide a Buy/Hold/Sell suggestion based on the data. | |
| 4. Add a cautionary note: remind user that past performance ≠ future results. | |
| RULES: | |
| - Do NOT invent any numbers. Use ONLY what is provided. | |
| - Be empathetic and professional. | |
| - If profit is negative, be encouraging but realistic. | |
| - DO NOT add extra spaces or trailing spaces between lines. | |
| """ | |
| elif intent == "education": | |
| query = data.get("query", "financial markets") | |
| prompt = f""" | |
| Explain this financial concept in simple terms for a beginner: "{query}" | |
| RULES: | |
| - Keep it under 150 words. | |
| - Use an analogy if helpful. | |
| - Do NOT recommend specific stocks or products. | |
| - If you're unsure about a detail, say so. | |
| - End with a suggestion to consult a financial advisor. | |
| """ | |
| elif intent == "guide": | |
| # For guides, LLM polishes the knowledge base content | |
| title = data.get("title", "Financial Guide") | |
| content = data.get("content", "") | |
| prompt = f""" | |
| Rewrite this guide introduction in a friendly, motivating tone for a beginner: | |
| Title: {title} | |
| Content: {content} | |
| RULES: | |
| - Keep it under 100 words. | |
| - Be encouraging and welcoming. | |
| - Do NOT add any new factual information. | |
| - Do NOT change any steps or numbers. | |
| """ | |
| else: | |
| # Analysis intent | |
| prompt = f""" | |
| Provide a concise analysis summary using ONLY the data below. | |
| DATA (verified): | |
| {json.dumps(data_summary)} | |
| Write EXACTLY 3 points, clearly separated by newlines: | |
| 1. Current trend direction and strength. | |
| 2. Risk level assessment and what it means for the investor. | |
| 3. Short-term outlook based on the data. | |
| RULES: | |
| - Use ONLY the numbers provided. Do NOT invent prices, percentages, or statistics. | |
| - If data is limited, say "Based on available data" instead of guessing. | |
| - Keep it professional and beginner-friendly. | |
| """ | |
| try: | |
| system_prompt = SYSTEM_PROMPT_EDUCATION if intent == "education" else SYSTEM_PROMPT_INSIGHT | |
| chat_completion = _call_groq_api( | |
| messages=[ | |
| {"role": "system", "content": system_prompt}, | |
| {"role": "user", "content": prompt}, | |
| ], | |
| temperature=0.2, | |
| max_tokens=400, | |
| ).strip() | |
| # Post-processing to clean up whitespace | |
| lines = [line.strip() for line in chat_completion.split('\n') if line.strip()] | |
| return '\n\n'.join(lines) | |
| except Exception as e: | |
| logging.warning(f"Groq insight generation failed: {e}") | |
| return _get_fallback_insight(data, intent) | |
| def generate_structured_guide(query: str) -> dict: | |
| """Generate a step-by-step guide for financial tasks (e.g. opening an account).""" | |
| if not client: | |
| return {"title": query, "steps": ["Check your internet connection.", "Visit the official website."], "notes": []} | |
| prompt = f""" | |
| Create a step-by-step beginner's guide for: "{query}" | |
| Return a JSON object with: | |
| - title: Catchy title for the guide. | |
| - steps: List of 4-6 concise, actionable steps. | |
| - definition: A brief 2-sentence introduction. | |
| - notes: 2-3 important tips or document requirements. | |
| Example Schema: | |
| {{ | |
| "title": "Opening a Demat Account", | |
| "steps": ["Step 1...", "Step 2..."], | |
| "definition": "A demat account is...", | |
| "notes": ["Keep PAN card ready", "Aadhar linkage is must"] | |
| }} | |
| """ | |
| try: | |
| raw = _call_groq_api( | |
| messages=[ | |
| {"role": "system", "content": SYSTEM_PROMPT_STRUCTURED}, | |
| {"role": "user", "content": prompt}, | |
| ], | |
| response_format={"type": "json_object"}, | |
| temperature=0.2 | |
| ) | |
| return json.loads(_clean_json_response(raw)) | |
| except Exception as e: | |
| logging.error(f"Structured guide generation failed: {e}") | |
| return {"title": query, "steps": ["Unable to generate steps at this moment."], "notes": ["Try again later."]} | |
| def generate_structured_education(query: str) -> dict: | |
| """Generate an educational definition/explanation for a financial term.""" | |
| if not client: | |
| return {"term": query, "definition": "Information currently unavailable."} | |
| prompt = f""" | |
| Explain the financial concept: "{query}" for a complete beginner. | |
| Return a JSON object with: | |
| - term: The concept name. | |
| - definition: A simple, clear 3-4 sentence explanation. | |
| - notes: 2-3 key takeaways or related terms. | |
| - title: A friendly header (e.g. "Understanding SIP"). | |
| Example Schema: | |
| {{ | |
| "term": "SIP", | |
| "definition": "SIP stands for...", | |
| "notes": ["Power of compounding", "Rupee cost averaging"], | |
| "title": "The Basics of SIP" | |
| }} | |
| """ | |
| try: | |
| raw = _call_groq_api( | |
| messages=[ | |
| {"role": "system", "content": SYSTEM_PROMPT_STRUCTURED}, | |
| {"role": "user", "content": prompt}, | |
| ], | |
| response_format={"type": "json_object"}, | |
| temperature=0.2 | |
| ) | |
| return json.loads(_clean_json_response(raw)) | |
| except Exception as e: | |
| logging.error(f"Structured education generation failed: {e}") | |
| return {"term": query, "definition": "We are having trouble explaining this right now.", "notes": [], "title": query} | |
| def _get_fallback_insight(data: dict, intent: str) -> str: | |
| """ | |
| Deterministic fallback insights when LLM is unavailable. | |
| Uses actual data values to construct a basic but accurate response. | |
| """ | |
| if intent == "strategy": | |
| profit = data.get("profit_inr", 0) | |
| pct = data.get("profit_pct", 0) | |
| if profit >= 0: | |
| return ( | |
| f"Your investment has generated a return of {pct:.1f}%. " | |
| f"This shows positive performance over the period.\n\n" | |
| f"Consider reviewing your position periodically. " | |
| f"Past performance does not guarantee future results." | |
| ) | |
| else: | |
| return ( | |
| f"Your investment is currently showing a loss of {abs(pct):.1f}%. " | |
| f"Market fluctuations are normal — consider your investment horizon.\n\n" | |
| f"Avoid panic selling. Consult a financial advisor for personalized guidance." | |
| ) | |
| elif intent == "education": | |
| query = data.get("query", "this topic") | |
| return ( | |
| f"'{query}' is a financial concept worth understanding. " | |
| f"For detailed and accurate information, we recommend consulting " | |
| f"SEBI-registered resources or a financial advisor." | |
| ) | |
| elif intent == "guide": | |
| return ( | |
| "Follow the steps listed below to get started. " | |
| "Make sure you have all required documents ready before beginning the process." | |
| ) | |
| else: | |
| trend = data.get("trend", "stable") | |
| risk = data.get("risk", "moderate") | |
| return ( | |
| f"The asset is currently showing a {trend.lower()} trend with {risk.lower()} risk. " | |
| f"Consider reviewing technical indicators for a clearer picture.\n\n" | |
| f"Always do your own research before making investment decisions." | |
| ) | |