File size: 8,123 Bytes
d1d1019
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201

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