| import os |
| import json |
| from uuid import uuid4 |
| from groq import Groq |
| from langchain_core.documents import Document |
|
|
| |
| from langchain_huggingface import HuggingFaceEndpointEmbeddings |
| from langchain_chroma import Chroma |
| from dotenv import load_dotenv |
| import random |
| import shutil |
| from optimized_quiz import OPTIMIZED_QUESTIONS |
| from chat_resources import * |
| from datetime import datetime, timedelta |
|
|
| |
| load_dotenv(dotenv_path=".env") |
|
|
| |
| load_dotenv(dotenv_path=".secrets.env", override=True) |
|
|
| |
| GROQ_API_KEY = os.getenv("GROQ_API_KEY") |
| HF_API_KEY = os.getenv("HF_API_KEY") |
| DATA_PATH = "data.json" |
| CHROMA_PATH = "chroma_db" |
|
|
| TEMPERATURE = float(os.getenv("G_TEMPERATURE", 0.7)) |
| MAX_TOKENS = int(os.getenv("G_MAX_TOKENS", 400)) |
| RETRIEVE_K = int(os.getenv("G_RETRIEVE_K", 3)) |
| TOP_P = float(os.getenv("G_TOP_P", 1.0)) |
| MAX_CONVERSATION_HISTORY = int(os.getenv("G_MAX_CONVERSATION_HISTORY", 5)) |
| MMR = str(os.getenv("MMR", "mmr")) |
| G_FETCH_K = int(os.getenv("G_FETCH_K", 20)) |
| LAMBDA_MULT = float(os.getenv("LAMBDA_MULT", 0.5)) |
|
|
|
|
| class GroqClient: |
| def __init__(self): |
| self._sessions = {} |
| self.SESSION_TIMEOUT = timedelta(minutes=30) |
|
|
| self.documents = self.load_json_data(DATA_PATH) |
| if not self.documents: |
| raise RuntimeError("No data loaded") |
|
|
| self.vector_store = self.init_vector_store(self.documents) |
|
|
| self.retriever = self.vector_store.as_retriever( |
| search_type=MMR, |
| search_kwargs={ |
| "k": RETRIEVE_K, |
| "fetch_k": G_FETCH_K, |
| "lambda_mult": LAMBDA_MULT, |
| }, |
| ) |
|
|
| if not GROQ_API_KEY: |
| raise RuntimeError("GROQ_API_KEY not found in environment") |
|
|
| self.client = Groq(api_key=GROQ_API_KEY) |
|
|
| self.SYSTEM_MESSSAGE = SYSTEM_MESSSAGE |
|
|
| self.PROMPT_TEMPLATE = PROMPT_TEMPLATE |
|
|
| self.BLACKLIST = BLACKLIST |
|
|
| def load_json_data(self, path): |
| try: |
| with open(path, "r", encoding="utf-8") as f: |
| data = json.load(f) |
|
|
| documents = [] |
|
|
| if "qa" in data: |
| for item in data["qa"]: |
| text = f"Q: {item['question']}\nA: {item['answer']}" |
| documents.append( |
| Document( |
| page_content=text, |
| metadata={ |
| "id": item.get("id", str(uuid4())), |
| "category": item.get("category", "QA"), |
| }, |
| ) |
| ) |
|
|
| if "chunks" in data: |
| for item in data["chunks"]: |
| documents.append( |
| Document( |
| page_content=item["chunk"], |
| metadata={ |
| "id": item.get("id", str(uuid4())), |
| "category": "Chunk", |
| }, |
| ) |
| ) |
|
|
| return documents |
|
|
| except Exception as e: |
| print(f"Error loading JSON data: {e}") |
| return [] |
|
|
| def init_vector_store(self, documents): |
| |
| |
| if not HF_API_KEY: |
| raise RuntimeError("HF_API_KEY not found in environment") |
|
|
| embeddings_model = HuggingFaceEndpointEmbeddings( |
| model="sentence-transformers/all-MiniLM-L6-v2", |
| huggingfacehub_api_token=HF_API_KEY, |
| ) |
|
|
| |
| if os.path.exists(CHROMA_PATH): |
| shutil.rmtree(CHROMA_PATH) |
|
|
| uuids = [str(uuid4()) for _ in documents] |
|
|
| vector_store = Chroma( |
| collection_name="user_data", |
| embedding_function=embeddings_model, |
| persist_directory=CHROMA_PATH, |
| ) |
|
|
| |
| vector_store.add_documents(documents=documents, ids=uuids) |
| return vector_store |
|
|
| def handle_unknown_query(self): |
| return random.choice(FALLBACK_RESPONSES) |
|
|
| |
| |
| def cleanup_expired_sessions(self): |
| """Remove expired sessions to avoid memory overload.""" |
| current_time = datetime.now() |
| expired_ips = [ |
| ip |
| for ip, data in self._sessions.items() |
| if current_time - data["last_activity"] > self.SESSION_TIMEOUT |
| ] |
| for ip in expired_ips: |
| del self._sessions[ip] |
|
|
| def get_next_questions(self, ip: str): |
| """Return 3 non-repeated random questions within the session.""" |
| self.cleanup_expired_sessions() |
|
|
| current_time = datetime.now() |
| if ip not in self._sessions: |
| self._sessions[ip] = {"shown": set(), "last_activity": current_time} |
| else: |
| if ( |
| current_time - self._sessions[ip]["last_activity"] |
| > self.SESSION_TIMEOUT |
| ): |
| self._sessions[ip]["shown"].clear() |
| self._sessions[ip]["last_activity"] = current_time |
|
|
| shown = self._sessions[ip]["shown"] |
| remaining = [q for q in OPTIMIZED_QUESTIONS if q not in shown] |
|
|
| if len(remaining) < 3: |
| shown.clear() |
| remaining = OPTIMIZED_QUESTIONS[:] |
|
|
| selected = random.sample(remaining, 3) |
| shown.update(selected) |
| return selected |
|
|
| |
|
|
| |
| |
| def ask(self, raw_query: str) -> tuple[str, str | None]: |
| if ( |
| not raw_query |
| or raw_query is None |
| or raw_query == "" |
| or len(raw_query) > 1000 |
| ): |
| return "Please provide a valid query under 1,000 characters.", None |
|
|
| if raw_query.lower() in GREETINGS_TRIGGERS: |
| return random.choice(GREETINGS), None |
|
|
| try: |
| docs = self.retriever.invoke(raw_query) |
| except Exception as e: |
| return f"Error retrieving documents: {e}", None |
|
|
| if not docs: |
| return self.handle_unknown_query(), None |
|
|
| context = "\n".join([d.page_content for d in docs]) |
| fallback = self.handle_unknown_query() |
| prompt = self.PROMPT_TEMPLATE.format( |
| context=context, question=raw_query, fallback_response=fallback |
| ) |
|
|
| messages = [ |
| { |
| "role": "system", |
| "content": self.SYSTEM_MESSSAGE, |
| }, |
| ] + [ |
| {"role": "user", "content": prompt}, |
| ] |
|
|
| |
| models_to_try = [ |
| "openai/gpt-oss-120b", |
| "openai/gpt-oss-20b", |
| "compound-beta-mini", |
| "llama-3.1-8b-instant", |
| "llama-3.3-70b-versatile", |
| ] |
|
|
| backup_models = [ |
| "compound-beta-mini", |
| "llama-3.1-8b-instant", |
| "llama-3.3-70b-versatile", |
| ] |
|
|
| random.shuffle(models_to_try) |
|
|
| for model in models_to_try: |
| try: |
| completion = self.client.chat.completions.create( |
| model=model, |
| messages=messages, |
| temperature=TEMPERATURE, |
| max_completion_tokens=MAX_TOKENS, |
| top_p=TOP_P, |
| stream=False, |
| ) |
| response = completion.choices[0].message.content |
| if response and response.strip(): |
| return response.strip(), model |
| else: |
| continue |
|
|
| except Exception as e: |
| |
| if "rate_limit_exceeded" in str(e) or "429" in str(e): |
| |
| continue |
| else: |
| |
| return f"Error while calling LLM: {e}", None |
|
|
| |
| return "I'm temporarily experiencing high demand. Please try again in a few minutes or rephrase your question.", None |
|
|
| |
| |
| def ask_stream(self, raw_query: str): |
| """Generator function that yields response chunks in real-time""" |
|
|
| |
| if not raw_query or raw_query is None or raw_query == "" or len(raw_query) > 1000: |
| yield "Please provide a valid query under 1,000 characters." |
| return |
|
|
| if raw_query.lower() in GREETINGS_TRIGGERS: |
| yield random.choice(GREETINGS) |
| return |
|
|
| |
| try: |
| docs = self.retriever.invoke(raw_query) |
| except Exception as e: |
| yield f"Error retrieving documents: {e}" |
| return |
|
|
| if not docs: |
| yield self.handle_unknown_query() |
| return |
|
|
| |
| context = "\n".join([d.page_content for d in docs]) |
| fallback = self.handle_unknown_query() |
| prompt = self.PROMPT_TEMPLATE.format( |
| context=context, question=raw_query, fallback_response=fallback |
| ) |
|
|
| messages = [ |
| { |
| "role": "system", |
| "content": self.SYSTEM_MESSSAGE, |
| }, |
| {"role": "user", "content": prompt}, |
| ] |
|
|
| |
| models_to_try = [ |
| "openai/gpt-oss-120b", |
| "openai/gpt-oss-20b", |
| "compound-beta-mini", |
| "llama-3.1-8b-instant", |
| "llama-3.3-70b-versatile", |
| ] |
|
|
| random.shuffle(models_to_try) |
|
|
| for model in models_to_try: |
| try: |
| completion = self.client.chat.completions.create( |
| model=model, |
| messages=messages, |
| temperature=TEMPERATURE, |
| max_completion_tokens=MAX_TOKENS, |
| top_p=TOP_P, |
| stream=True, |
| ) |
|
|
| |
| has_content = False |
| for chunk in completion: |
| if chunk.choices[0].delta.content: |
| has_content = True |
| yield chunk.choices[0].delta.content |
|
|
| |
| if chunk.choices[0].finish_reason: |
| break |
|
|
| |
| if has_content: |
| return |
|
|
| except Exception as e: |
| |
| if "rate_limit_exceeded" in str(e) or "429" in str(e): |
| continue |
| else: |
| yield f"Error while calling LLM: {e}" |
| return |
|
|
| |
| yield "I'm temporarily experiencing high demand. Please try again in a few minutes or rephrase your question." |
|
|