""" Groq API Integration for Fallback Responses FREE tier: 14,400 requests/day """ import os import logging logger = logging.getLogger(__name__) class GroqFallback: """Handles fallback to Groq API for non-ALU questions""" def __init__(self): self.api_key = os.getenv("GROQ_API_KEY", "") self.enabled = bool(self.api_key) self.client = None if self.enabled: try: from groq import Groq self.client = Groq(api_key=self.api_key) logger.info("[OK] Groq fallback enabled (FREE tier)") except ImportError: logger.warning("[!] Groq library not installed") self.enabled = False except Exception as e: logger.error(f"[FAIL] Failed to initialize Groq: {e}") self.enabled = False else: logger.info("[INFO] Groq fallback disabled (no API key)") def should_use_fallback(self, query: str, best_score: float, threshold: float = 50.0) -> bool: """Determine if we should use Groq fallback""" if not self.enabled: return False if best_score < threshold: logger.info(f"[GROQ] Using fallback (score {best_score:.1f} < {threshold})") return True return False # Grounded system prompt — mirrors the anti-hallucination discipline of the # Claude engine so a fallback answer is held to the same standard. We do NOT # hardcode tuition/program facts here: those go stale and the model will # state them confidently even when wrong. Facts come from the context only. SYSTEM_PROMPT = """You are the ALU Student Companion, an AI assistant for African Leadership University (ALU). # Most important rule Answer ONLY from the context provided below. If the context does not contain a fact, do not invent it. Specifically NEVER invent course codes, credit hours, module names, tuition amounts, fees, scholarship values, faculty names/titles, deadlines, term dates, or statistics. When the context doesn't have what's needed, say so plainly and point to the official source — students would rather hear "I don't have that, here's where to find it" than a confident wrong answer. # Style - Lead with a one-sentence direct answer, then detail. - Use markdown sparingly; bullet lists only for genuine lists. - No emoji. No "Is there anything else..." filler. - When a Source in the context has a url=..., cite it as a markdown link the first time you use that fact. # Facts you may always state without context - ALU was founded by Fred Swaniker in 2015, part of the African Leadership Group. - Campuses in Kigali, Rwanda and Pamplemousses, Mauritius. - Main site https://www.alueducation.com ; support https://support.alueducation.com - Student emails end @alustudent.com ; staff/public @alueducation.com Everything else — programmes, fees, dates, people — must come from the context. If it is not there, say you don't know and point to support.alueducation.com.""" def generate_response(self, query: str, alu_context: str = "", history=None) -> str: """ Generate a grounded fallback answer. - `query`: the student's current question - `alu_context`: the SAME formatted context block the Claude engine builds (titles/departments/urls + full text), not a truncated blob - `history`: prior [{role, content}, ...] turns, so follow-ups keep their thread instead of answering in a vacuum """ if not self.enabled or not self.client: return None try: messages = [{"role": "system", "content": self.SYSTEM_PROMPT}] # Forward recent conversation turns (cap to keep input bounded). for entry in (history or [])[-8:]: role = entry.get("role") content = entry.get("content", "") if role in ("user", "assistant") and content: messages.append({"role": role, "content": content}) if alu_context: user_message = ( "Use the following context from the ALU knowledge base to " "answer the question. If the context does not contain the " "answer, say so honestly and point to the official source.\n\n" f"=== CONTEXT ===\n{alu_context}\n=== END CONTEXT ===\n\n" f"Question: {query}" ) else: user_message = query messages.append({"role": "user", "content": user_message}) response = self.client.chat.completions.create( model="llama-3.3-70b-versatile", messages=messages, temperature=0.4, # lower = more faithful to context (was 0.7) max_tokens=1024, # was 500 — matches Claude, avoids cut-offs top_p=0.9, ) answer = response.choices[0].message.content.strip() logger.info(f"[OK] Groq response generated ({len(answer)} chars)") return answer except Exception as e: logger.error(f"[FAIL] Groq API error: {e}") return None def get_status(self) -> dict: """Get current status""" return { "enabled": self.enabled, "api_key_set": bool(self.api_key), "client_initialized": self.client is not None, "model": "llama-3.3-70b-versatile" if self.enabled else None, "free_tier": "14,400 requests/day" if self.enabled else None } # Global instance groq_fallback = GroqFallback()