File size: 12,427 Bytes
6246bba
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
"""
User Feedback Tracking System

Tracks user feedback on search results for continuous improvement:
- Thumbs up/down on answers
- Relevance ratings on sources
- Intent classification accuracy
- Search strategy effectiveness

Stores feedback in ClickHouse for analysis and model improvement.
"""

import logging
from typing import Dict, List, Any, Optional
from datetime import datetime
from dataclasses import dataclass, asdict
import json

logger = logging.getLogger(__name__)


@dataclass
class FeedbackEvent:
    """User feedback event"""
    # Identifiers
    session_id: str
    query_id: str
    user_id: Optional[int]
    
    # Query info
    query: str
    expanded_query: Optional[str]
    
    # Classification info
    intent_classified: str
    intent_confidence: float
    intent_method: str
    
    # Search info
    search_strategy: str
    live_results_count: int
    db_results_count: int
    total_sources: int
    
    # Feedback
    feedback_type: str  # "thumbs_up", "thumbs_down", "source_rating", "intent_correction"
    feedback_value: Any  # True/False for thumbs, 1-5 for rating, corrected intent for correction
    feedback_comment: Optional[str]
    
    # Metadata
    timestamp: str
    response_time_ms: float
    cache_hit: bool


class FeedbackTracker:
    """
    Track and store user feedback for continuous improvement.
    
    Features:
    - Multiple feedback types (thumbs, ratings, corrections)
    - ClickHouse storage for analytics
    - Async logging (non-blocking)
    - Aggregation and reporting
    """
    
    def __init__(self, analytics_db=None):
        """
        Initialize feedback tracker.
        
        Args:
            analytics_db: ClickHouse analytics database adapter
        """
        self.analytics_db = analytics_db
        self._ensure_table_exists()
    
    def _ensure_table_exists(self):
        """Create feedback table if it doesn't exist"""
        if not self.analytics_db:
            return
        
        try:
            create_table_query = """
            CREATE TABLE IF NOT EXISTS user_feedback (
                session_id String,
                query_id String,
                user_id Nullable(Int32),
                
                query String,
                expanded_query Nullable(String),
                
                intent_classified String,
                intent_confidence Float32,
                intent_method String,
                
                search_strategy String,
                live_results_count Int32,
                db_results_count Int32,
                total_sources Int32,
                
                feedback_type String,
                feedback_value String,
                feedback_comment Nullable(String),
                
                timestamp DateTime,
                response_time_ms Float32,
                cache_hit UInt8
            ) ENGINE = MergeTree()
            ORDER BY (timestamp, session_id)
            """
            
            self.analytics_db.execute(create_table_query)
            logger.info("βœ… Feedback table ensured")
            
        except Exception as e:
            logger.error(f"Failed to create feedback table: {e}")
    
    def record_feedback(
        self,
        session_id: str,
        query: str,
        feedback_type: str,
        feedback_value: Any,
        query_metadata: Dict[str, Any],
        feedback_comment: Optional[str] = None,
        user_id: Optional[int] = None
    ):
        """
        Record user feedback.
        
        Args:
            session_id: User session ID
            query: Original query
            feedback_type: Type of feedback (thumbs_up, thumbs_down, etc.)
            feedback_value: Feedback value
            query_metadata: Metadata about the query and response
            feedback_comment: Optional comment from user
            user_id: Optional user ID
        """
        try:
            # Create feedback event
            event = FeedbackEvent(
                session_id=session_id,
                query_id=query_metadata.get("query_id", f"{session_id}_{int(datetime.utcnow().timestamp())}"),
                user_id=user_id,
                
                query=query,
                expanded_query=query_metadata.get("expanded_query"),
                
                intent_classified=query_metadata.get("intent", "UNKNOWN"),
                intent_confidence=query_metadata.get("intent_confidence", 0.0),
                intent_method=query_metadata.get("intent_method", "unknown"),
                
                search_strategy=query_metadata.get("search_strategy", "unknown"),
                live_results_count=query_metadata.get("live_results_count", 0),
                db_results_count=query_metadata.get("db_results_count", 0),
                total_sources=query_metadata.get("total_sources", 0),
                
                feedback_type=feedback_type,
                feedback_value=str(feedback_value),
                feedback_comment=feedback_comment,
                
                timestamp=datetime.utcnow().isoformat(),
                response_time_ms=query_metadata.get("response_time_ms", 0.0),
                cache_hit=query_metadata.get("cache_hit", False)
            )
            
            # Store in ClickHouse
            if self.analytics_db:
                self._store_feedback(event)
            
            # Log feedback
            logger.info(
                f"Feedback recorded: {feedback_type}={feedback_value} "
                f"for query='{query}' (intent={event.intent_classified})"
            )
            
        except Exception as e:
            logger.error(f"Failed to record feedback: {e}")
    
    def _store_feedback(self, event: FeedbackEvent):
        """Store feedback event in ClickHouse"""
        try:
            insert_query = """
            INSERT INTO user_feedback (
                session_id, query_id, user_id,
                query, expanded_query,
                intent_classified, intent_confidence, intent_method,
                search_strategy, live_results_count, db_results_count, total_sources,
                feedback_type, feedback_value, feedback_comment,
                timestamp, response_time_ms, cache_hit
            ) VALUES
            """
            
            values = (
                event.session_id,
                event.query_id,
                event.user_id,
                event.query,
                event.expanded_query,
                event.intent_classified,
                event.intent_confidence,
                event.intent_method,
                event.search_strategy,
                event.live_results_count,
                event.db_results_count,
                event.total_sources,
                event.feedback_type,
                event.feedback_value,
                event.feedback_comment,
                event.timestamp,
                event.response_time_ms,
                1 if event.cache_hit else 0
            )
            
            self.analytics_db.execute(insert_query, [values])
            
        except Exception as e:
            logger.error(f"Failed to store feedback in ClickHouse: {e}")
    
    def get_feedback_stats(self, days: int = 7) -> Dict[str, Any]:
        """
        Get feedback statistics for the last N days.
        
        Args:
            days: Number of days to analyze
        
        Returns:
            Dictionary with feedback statistics
        """
        if not self.analytics_db:
            return {}
        
        try:
            query = f"""
            SELECT
                feedback_type,
                COUNT(*) as count,
                AVG(intent_confidence) as avg_confidence,
                AVG(response_time_ms) as avg_response_time,
                SUM(cache_hit) / COUNT(*) as cache_hit_rate
            FROM user_feedback
            WHERE timestamp >= now() - INTERVAL {days} DAY
            GROUP BY feedback_type
            ORDER BY count DESC
            """
            
            results = self.analytics_db.query(query)
            
            stats = {
                "total_feedback": sum(r["count"] for r in results),
                "by_type": {
                    r["feedback_type"]: {
                        "count": r["count"],
                        "avg_confidence": r["avg_confidence"],
                        "avg_response_time": r["avg_response_time"],
                        "cache_hit_rate": r["cache_hit_rate"]
                    }
                    for r in results
                },
                "period_days": days
            }
            
            return stats
            
        except Exception as e:
            logger.error(f"Failed to get feedback stats: {e}")
            return {}
    
    def get_intent_accuracy(self, days: int = 7) -> Dict[str, Any]:
        """
        Get intent classification accuracy based on user corrections.
        
        Args:
            days: Number of days to analyze
        
        Returns:
            Dictionary with accuracy metrics
        """
        if not self.analytics_db:
            return {}
        
        try:
            query = f"""
            SELECT
                intent_classified,
                COUNT(*) as total,
                SUM(CASE WHEN feedback_type = 'intent_correction' THEN 1 ELSE 0 END) as corrections,
                AVG(intent_confidence) as avg_confidence
            FROM user_feedback
            WHERE timestamp >= now() - INTERVAL {days} DAY
            GROUP BY intent_classified
            ORDER BY total DESC
            """
            
            results = self.analytics_db.query(query)
            
            accuracy = {
                "by_intent": {
                    r["intent_classified"]: {
                        "total": r["total"],
                        "corrections": r["corrections"],
                        "accuracy": 1.0 - (r["corrections"] / r["total"]) if r["total"] > 0 else 0.0,
                        "avg_confidence": r["avg_confidence"]
                    }
                    for r in results
                },
                "period_days": days
            }
            
            return accuracy
            
        except Exception as e:
            logger.error(f"Failed to get intent accuracy: {e}")
            return {}
    
    def get_low_confidence_queries(self, threshold: float = 0.7, limit: int = 100) -> List[Dict[str, Any]]:
        """
        Get queries with low intent classification confidence.
        
        Args:
            threshold: Confidence threshold (queries below this)
            limit: Maximum number of queries to return
        
        Returns:
            List of low-confidence queries
        """
        if not self.analytics_db:
            return []
        
        try:
            query = f"""
            SELECT
                query,
                intent_classified,
                intent_confidence,
                intent_method,
                COUNT(*) as occurrences
            FROM user_feedback
            WHERE intent_confidence < {threshold}
            GROUP BY query, intent_classified, intent_confidence, intent_method
            ORDER BY occurrences DESC, intent_confidence ASC
            LIMIT {limit}
            """
            
            results = self.analytics_db.query(query)
            return results
            
        except Exception as e:
            logger.error(f"Failed to get low confidence queries: {e}")
            return []


# ═══════════════════════════════════════════════════════════════════════════
# SINGLETON INSTANCE
# ═══════════════════════════════════════════════════════════════════════════

# Will be initialized with dependencies in main.py
feedback_tracker: Optional[FeedbackTracker] = None


def initialize_feedback_tracker(analytics_db=None):
    """Initialize global feedback tracker instance"""
    global feedback_tracker
    feedback_tracker = FeedbackTracker(analytics_db)
    logger.info("Feedback tracker initialized")