Spaces:
Sleeping
Sleeping
| #!/usr/bin/env python3 | |
| """ | |
| rag_explainer.py | |
| Retrieves relevant context from ChromaDB and generates a grounded | |
| financial consistency explanation via the Groq LLM API. | |
| Flask entry point : run_rag_explanation(query) | |
| Groq client : call load_groq_client() once in create_app() | |
| ChromaDB : reuses the singleton from build_vector_db.py | |
| """ | |
| import os | |
| import re | |
| from typing import Optional | |
| from groq import Groq | |
| from pipelines.rag.build_vector_db import get_collection | |
| # ββ Module-level singleton ββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| _groq_client: Optional[Groq] = None | |
| # ββ Loader (call once in create_app()) βββββββββββββββββββββββββββββββββββββββ | |
| def load_groq_client() -> None: | |
| global _groq_client | |
| if _groq_client is not None: | |
| return | |
| api_key = os.getenv("GROQ_API_KEY") | |
| if not api_key: | |
| raise RuntimeError("GROQ_API_KEY environment variable is not set.") | |
| _groq_client = Groq(api_key=api_key) | |
| def is_groq_loaded() -> bool: | |
| return _groq_client is not None | |
| # ββ Helpers βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _is_noise(text: str) -> bool: | |
| if len(text) < 40: return True | |
| if "Page" in text or "Classification" in text: return True | |
| return False | |
| # ββ Retrieval βββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def retrieve_context(query: str, company: str, top_k: int = 12) -> list[str]: | |
| """ | |
| Queries ChromaDB for the most relevant transcript excerpts for a specific company. | |
| """ | |
| collection = get_collection() | |
| where_filter = {"company": company} if company else None | |
| results = collection.query( | |
| query_texts=[query + " financial performance growth decline guidance margins"], | |
| n_results=top_k * 2, | |
| where=where_filter, | |
| ) | |
| docs = results.get("documents", [[]])[0] | |
| # Deduplicate and filter noise | |
| seen, clean = set(), [] | |
| for d in docs: | |
| if d not in seen and not _is_noise(d): | |
| seen.add(d) | |
| clean.append(d) | |
| return clean[:top_k] | |
| # ββ LLM generation ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def _generate_analysis(query: str, context: list[str]) -> dict: | |
| """Calls Groq LLM to synthesize the context into a structured response.""" | |
| if not context: | |
| return { | |
| "explanation": "Insufficient transcript data available to synthesize an analysis.", | |
| "positive": [], | |
| "negative": [] | |
| } | |
| context_text = "\n".join(f"- {x}" for x in context) | |
| prompt = f"""You are an expert financial analyst. Read the following excerpts from a company's recent earnings transcripts. | |
| CONTEXT: | |
| {context_text} | |
| TASK: | |
| 1. Write a cohesive, 3-sentence executive summary of the company's performance and forward-looking outlook. | |
| 2. Identify 2 specific positive signals/tailwinds mentioned in the text. | |
| 3. Identify 2 specific negative signals/headwinds/risks mentioned in the text. | |
| You MUST format your exact response like this: | |
| SUMMARY: [Your 3 sentence summary here] | |
| POS: [First positive signal] | |
| POS: [Second positive signal] | |
| NEG: [First negative signal] | |
| NEG: [Second negative signal] | |
| """ | |
| response = _groq_client.chat.completions.create( | |
| model="llama-3.3-70b-versatile", | |
| messages=[{"role": "user", "content": prompt}], | |
| temperature=0.2, | |
| max_tokens=500 | |
| ) | |
| raw_text = response.choices[0].message.content.strip() | |
| # Parse the LLM output | |
| explanation = "" | |
| positive = [] | |
| negative = [] | |
| for line in raw_text.split('\n'): | |
| line = line.strip() | |
| if line.startswith("SUMMARY:"): | |
| explanation = line.replace("SUMMARY:", "").strip() | |
| elif line.startswith("POS:"): | |
| positive.append(line.replace("POS:", "").strip()) | |
| elif line.startswith("NEG:"): | |
| negative.append(line.replace("NEG:", "").strip()) | |
| # Fallback if LLM didn't format perfectly | |
| if not explanation: | |
| explanation = raw_text | |
| return { | |
| "explanation": explanation, | |
| "positive": positive, | |
| "negative": negative | |
| } | |
| # ββ Flask entry point βββββββββββββββββββββββββββββββββββββββββββββββββββββββββ | |
| def run_rag_explanation(query: str, company: str, top_k: int = 10) -> dict: | |
| """ | |
| Main entry point for the Flask app. | |
| """ | |
| if not is_groq_loaded(): | |
| raise RuntimeError( | |
| "Groq client is not loaded. Call load_groq_client() " | |
| "inside create_app() before handling requests." | |
| ) | |
| context = retrieve_context(query, company=company, top_k=top_k) | |
| analysis = _generate_analysis(query, context) | |
| return { | |
| "query": query, | |
| "positive_signals": analysis["positive"], | |
| "negative_signals": analysis["negative"], | |
| "explanation": analysis["explanation"], | |
| } |