import requests import json import os import joblib import numpy as np from sentence_transformers import SentenceTransformer OLLAMA_API_URL = "http://localhost:11434/api/generate" MODEL_NAME = "llama3.2" class LLMInterface: def __init__(self): self.available = self.check_availability() self.reward_model = self.load_reward_model() # We need an encoder here for the reward model if it's not passed in # Ideally we share the one from RAG engine, but for simplicity/robustness loading a small one here or passing it in generate try: self.encoder = SentenceTransformer('all-MiniLM-L6-v2') except: self.encoder = None if self.available: print(f"✓ Connected to Ollama ({MODEL_NAME})") else: print("⚠ Ollama not detected. Using Logic-Based Fallback.") def load_reward_model(self): try: data = joblib.load("model/rlhf_model.joblib") print("✓ Loaded RLHF Models (Reward + VPL)") return data except: print("⚠ No RLHF Models found. Self-Correction disabled.") return None def check_availability(self): try: response = requests.get("http://localhost:11434/api/tags", timeout=2) if response.status_code == 200: models = [m['name'] for m in response.json().get('models', [])] if any(MODEL_NAME in m for m in models): return True print(f"⚠ Ollama running but '{MODEL_NAME}' not found. Please run 'ollama pull {MODEL_NAME}'") return False except: return False def score_response(self, query, response_text): """Calculate Reward Score & Uncertainty using VPL""" if not self.reward_model or not self.encoder: return 0.5, 0.0 # Neutral score, no uncertainty try: full_text = f"Q: {query} A: {response_text}" embedding = self.encoder.encode([full_text]) # Use VPL if available (Pluralistic Alignment) if 'vpl' in self.reward_model: vpl = self.reward_model['vpl'] mean_reward, std_reward = vpl.predict_reward(embedding) score = 1 / (1 + np.exp(-mean_reward[0])) uncertainty = std_reward[0] return score, uncertainty # Fallback to simple RM elif 'rm' in self.reward_model: score = self.reward_model['rm'].forward(embedding)[0] return 1 / (1 + np.exp(-score)), 0.0 return 0.5, 0.0 except Exception as e: print(f"Scoring Error: {e}") return 0.5, 0.0 def generate_response(self, user_query, rag_context, user_profile=None, journal_context=None): if not self.available: return self.fallback_response(user_query, rag_context) # Self-Correction Loop best_response = "" best_score = -1.0 final_uncertainty = 0.0 system_prompt = self.build_system_prompt(user_profile) context_str = self.format_context(rag_context, journal_context) for attempt in range(2): prompt = self.build_prompt(user_query, context_str, attempt, best_response) current_response = self.call_ollama(prompt, system_prompt) current_score, current_uncertainty = self.score_response(user_query, current_response) print(f"Attempt {attempt+1}: Score={current_score:.2f}, Uncertainty={current_uncertainty:.2f}") if current_score > best_score: best_score = current_score best_response = current_response final_uncertainty = current_uncertainty if current_score > 0.7: break # Active Learning Trigger (Silent) # We still calculate uncertainty for logging, but we don't annoy the user. if final_uncertainty > 0.1: pass # Log internally if needed elif best_score > 0.8: pass # Log internally return best_response def rewrite_query(self, user_query): """Optimize user query for vector search using LLM""" if not self.available: return user_query # Fallback system_prompt = "You are an expert search engine optimizer. Your goal is to rewrite queries to be precise, medical, and context-rich for a mental health database." prompt = f""" Original Query: {user_query} Task: Rewrite this query to improve retrieval of relevant mental health advice. - Expand vague terms (e.g. "tired" -> "fatigue burnout symptoms"). - Keep it concise (max 10 words). - Remove conversational filler. - Output ONLY the rewritten query string. """ rewritten = self.call_ollama(prompt, system_prompt) # Clean up response (some models are chatty) clean = rewritten.replace('"', '').replace("Here is the rewritten query:", "").strip() print(f"RAG Optimization: '{user_query}' -> '{clean}'") return clean def build_prompt(self, query, context, attempt, previous_bad_response): prompt = f""" Context Information: {context} User Question: {query} Instructions: Answer the user's question using the provided context. Be empathetic, professional, and concise. """ if attempt > 0: prompt += f"\nCRITIQUE: Your previous answer was: '{previous_bad_response[:100]}...'. It was not helpful enough. Improve it." return prompt def call_ollama(self, prompt, system_prompt): try: payload = { "model": MODEL_NAME, "prompt": prompt, "system": system_prompt, "stream": False, "options": { "temperature": 0.7, "num_ctx": 4096 } } response = requests.post(OLLAMA_API_URL, json=payload, timeout=60) if response.status_code == 200: return response.json().get('response', "I'm having trouble thinking right now.") return f"Error: {response.text}" except Exception as e: print(f"Gen Error: {e}") return "Thinking failed." def format_context(self, rag_context, journal_context): context_str = "" for item in rag_context: context_str += f"- [{item['source']}] {item['title']}: {item['content'][:300]}...\n" if journal_context: context_str += f"\n[User Journal Context]: {journal_context}\n" return context_str def build_system_prompt(self, user_profile): base_prompt = ( "You are the DC Well Being Assistant, a knowledgeable mental health expert. " "Your goal is to answer the user's questions DIRECTLY and FACTUALLY. " "DO NOT act like a therapist asking reflection questions (e.g. 'How does that make you feel?'). " "DO NOT say 'I'm here to support you' or 'I glad you reached out'. " "If the user asks for an explanation (e.g. 'explain x'), provide the explanation immediately. " "Be empathetic but conscise. " ) if user_profile: name = user_profile.get('name', 'User') risk = user_profile.get('overall_risk', 'unknown') base_prompt += f"\nUser: {name}. Risk: '{risk}'." return base_prompt def fallback_response(self, query, rag_context): """Legacy logic-based response if LLM is down""" text = "Based on my database:\n\n" for i, r in enumerate(rag_context[:3], 1): text += f"**{r['title']}**\n{r['content'][:200]}...\n\n" text += "_[Ollama Llama 3.2 is offline]_" return text