TradeSense-backend / groq_service.py
Vansh Bhardwaj
Strategy and bot questions
f4a26bc
"""
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
@retry(
stop=stop_after_attempt(3),
wait=wait_exponential(multiplier=1, min=1, max=4),
retry=retry_if_exception_type(Exception),
reraise=True
)
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."
)