Coverage for app \ storage \ feedback.py: 20%
65 statements
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-30 09:36 +0100
« prev ^ index » next coverage.py v7.12.0, created at 2025-11-30 09:36 +0100
1"""
2Feedback logger for storing positive user feedback
4Logs successful interactions to be used for:
5- RAG context enrichment (learning from good examples)
6- Future fine-tuning or distillation
7- Automata pattern discovery (Plasticity V2)
8"""
9import logging
10import sqlite3
11from datetime import datetime
12from typing import Optional, List, Dict, Any
13from pathlib import Path
15from app.config import settings
17logger = logging.getLogger(__name__)
20class FeedbackLogger:
21 """Logs successful user-approved interactions"""
23 def __init__(self, db_path: Optional[Path] = None):
24 """
25 Initialize feedback logger
26 Args:
27 db_path: Path to SQLite database
28 """
29 self.db_path = db_path or settings.db_path
30 self._ensure_db()
32 def _ensure_db(self):
33 """Ensure the feedback table exists in the database"""
34 try:
35 self.db_path.parent.mkdir(parents=True, exist_ok=True)
36 conn = sqlite3.connect(self.db_path)
37 cursor = conn.cursor()
39 cursor.execute("""
40 CREATE TABLE IF NOT EXISTS feedback_log (
41 id INTEGER PRIMARY KEY AUTOINCREMENT,
42 timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
43 task TEXT NOT NULL,
44 language TEXT NOT NULL,
45 request_code TEXT NOT NULL,
46 response_code TEXT,
47 response_explanation TEXT,
48 source TEXT DEFAULT 'user_cli',
49 processed_for_rag BOOLEAN DEFAULT 0,
50 UNIQUE(task, request_code, response_code)
51 )
52 """)
54 conn.commit()
55 conn.close()
56 logger.info("Feedback log table initialized successfully.")
57 except Exception as e:
58 logger.error(f"Failed to initialize feedback_log table: {e}")
59 raise
61 def log_feedback(
62 self,
63 task: str,
64 language: str,
65 request_code: str,
66 response_code: Optional[str],
67 response_explanation: Optional[str],
68 source: str = "user_cli"
69 ) -> bool:
70 """
71 Log a positive feedback entry.
73 Returns:
74 bool: True if a new entry was created, False otherwise (e.g., duplicate).
75 """
76 try:
77 conn = sqlite3.connect(self.db_path)
78 cursor = conn.cursor()
80 cursor.execute("""
81 INSERT INTO feedback_log (
82 task, language, request_code, response_code, response_explanation, source
83 ) VALUES (?, ?, ?, ?, ?, ?)
84 ON CONFLICT(task, request_code, response_code) DO NOTHING
85 """, (
86 task,
87 language,
88 request_code,
89 response_code,
90 response_explanation,
91 source
92 ))
94 conn.commit()
95 changes = conn.total_changes
96 conn.close()
98 if changes > 0:
99 logger.info("Logged new feedback entry.")
100 return True
101 else:
102 logger.warning("Attempted to log duplicate feedback entry. Ignored.")
103 return False
105 except Exception as e:
106 logger.error(f"Failed to log feedback: {e}")
107 return False
109 def get_unprocessed_feedback(self) -> List[Dict[str, Any]]:
110 """Retrieve all feedback entries that have not been processed for RAG yet."""
111 try:
112 conn = sqlite3.connect(self.db_path)
113 conn.row_factory = sqlite3.Row
114 cursor = conn.cursor()
116 cursor.execute("""
117 SELECT * FROM feedback_log WHERE processed_for_rag = 0
118 """)
120 unprocessed = [dict(row) for row in cursor.fetchall()]
121 conn.close()
122 return unprocessed
124 except Exception as e:
125 logger.error(f"Failed to get unprocessed feedback: {e}")
126 return []
128 def mark_feedback_as_processed(self, ids: List[int]):
129 """Mark feedback entries as processed for RAG by their IDs."""
130 if not ids:
131 return
133 try:
134 conn = sqlite3.connect(self.db_path)
135 cursor = conn.cursor()
137 # Using parameter substitution to prevent SQL injection
138 placeholders = ', '.join('?' for _ in ids)
139 query = f"UPDATE feedback_log SET processed_for_rag = 1 WHERE id IN ({placeholders})"
141 cursor.execute(query, ids)
142 conn.commit()
143 conn.close()
145 logger.info(f"Marked {len(ids)} feedback entries as processed for RAG.")
147 except Exception as e:
148 logger.error(f"Failed to mark feedback as processed: {e}")