healthcare-rag-api / services /feedback_service.py
Santhakumar Ramesh
feat: initial deploy to HF Space
3872518
Raw
History Blame Contribute Delete
8.23 kB
"""
Feedback Service - Continuous Learning from User Feedback
Collects user feedback on responses to improve the system over time.
Persists all feedback to SQLite so data survives restarts.
"""
import os
import sqlite3
from contextlib import contextmanager
from typing import Dict, List, Optional
from datetime import datetime
from loguru import logger
# ── SQLite path ───────────────────────────────────────────────────────────────
_DB_PATH = os.getenv("FEEDBACK_DB_PATH", "./data/feedback.db")
@contextmanager
def _db_conn():
"""Thread-safe SQLite context manager."""
os.makedirs(os.path.dirname(_DB_PATH) if os.path.dirname(_DB_PATH) else ".", exist_ok=True)
conn = sqlite3.connect(_DB_PATH, check_same_thread=False)
conn.row_factory = sqlite3.Row
try:
yield conn
conn.commit()
except Exception:
conn.rollback()
raise
finally:
conn.close()
def _init_db():
"""Create table if it doesn't exist."""
with _db_conn() as conn:
conn.execute("""
CREATE TABLE IF NOT EXISTS feedback (
interaction_id TEXT PRIMARY KEY,
user_id TEXT,
query TEXT NOT NULL,
response TEXT,
rating TEXT NOT NULL,
quality_score INTEGER,
comment TEXT,
correction TEXT,
timestamp TEXT NOT NULL
)
""")
class FeedbackService:
"""
Collects and analyses user feedback for continuous improvement.
Features:
- Thumbs up/down on responses
- Quality ratings (1–5)
- Correction suggestions
- Feedback analytics
- SQLite-backed persistence (falls back to in-memory on DB error)
"""
def __init__(self):
try:
_init_db()
logger.info(f"[FeedbackService] SQLite persistence enabled at {_DB_PATH}")
except Exception as exc:
logger.warning(f"[FeedbackService] SQLite unavailable ({exc}); using in-memory")
# In-memory fallback
self._fallback: Dict = {}
logger.info("[FeedbackService] Initialized")
# ── Write ─────────────────────────────────────────────────────────────────
def add_feedback(
self,
interaction_id: str,
user_id: Optional[str],
query: str,
response: str,
rating: str, # "positive" | "negative"
quality_score: Optional[int] = None, # 1–5
comment: Optional[str] = None,
correction: Optional[str] = None,
):
"""Record user feedback on a response."""
ts = datetime.now().isoformat()
try:
with _db_conn() as conn:
conn.execute(
"""INSERT OR REPLACE INTO feedback
(interaction_id, user_id, query, response, rating,
quality_score, comment, correction, timestamp)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)""",
(
interaction_id,
user_id,
query,
response[:500] if response else "",
rating,
quality_score,
comment,
correction,
ts,
),
)
except Exception as exc:
logger.warning(f"[FeedbackService] SQLite write failed: {exc} β€” using in-memory")
self._fallback[interaction_id] = {
"interaction_id": interaction_id,
"user_id": user_id,
"query": query,
"response": (response or "")[:500],
"rating": rating,
"quality_score": quality_score,
"comment": comment,
"correction": correction,
"timestamp": ts,
}
logger.info(f"[FeedbackService] Recorded {rating} feedback for {interaction_id}")
# ── Read ──────────────────────────────────────────────────────────────────
def get_feedback_stats(self) -> Dict:
"""Return aggregate feedback statistics."""
try:
with _db_conn() as conn:
row = conn.execute(
"""SELECT
COUNT(*) AS total,
SUM(CASE WHEN rating='positive' THEN 1 END) AS positive,
SUM(CASE WHEN rating='negative' THEN 1 END) AS negative,
AVG(CAST(quality_score AS REAL)) AS avg_rating
FROM feedback"""
).fetchone()
total = row["total"] or 0
positive = row["positive"] or 0
negative = row["negative"] or 0
avg_r = round(float(row["avg_rating"] or 0), 2)
except Exception:
# Fallback stats from in-memory dict
items = list(self._fallback.values())
total = len(items)
positive = sum(1 for i in items if i["rating"] == "positive")
negative = total - positive
scores = [i["quality_score"] for i in items if i.get("quality_score")]
avg_r = round(sum(scores) / len(scores), 2) if scores else 0.0
return {
"total_feedback": total,
"positive_count": positive,
"negative_count": negative,
"positive_rate": round(positive / total, 3) if total else 0,
"negative_rate": round(negative / total, 3) if total else 0,
"avg_rating": avg_r,
}
def get_improvement_suggestions(self, limit: int = 10) -> List[Dict]:
"""Return the most-recent negative feedback entries."""
try:
with _db_conn() as conn:
rows = conn.execute(
"""SELECT query, response, comment FROM feedback
WHERE rating='negative' ORDER BY timestamp DESC LIMIT ?""",
(limit,),
).fetchall()
return [dict(r) for r in rows]
except Exception:
items = [i for i in self._fallback.values() if i["rating"] == "negative"]
return items[-limit:]
def export_feedback_for_training(self) -> List[Dict]:
"""Export positive / corrected pairs for fine-tuning."""
try:
with _db_conn() as conn:
rows = conn.execute(
"""SELECT query, response, correction, quality_score, rating
FROM feedback
WHERE rating='positive' OR quality_score >= 4 OR correction IS NOT NULL"""
).fetchall()
items = [dict(r) for r in rows]
except Exception:
items = list(self._fallback.values())
training_data = []
for fb in items:
if fb.get("rating") == "positive" or (fb.get("quality_score") or 0) >= 4:
training_data.append({
"query": fb["query"],
"response": fb["response"],
"quality": "good",
"score": fb.get("quality_score") or 5,
})
elif fb.get("correction"):
training_data.append({
"query": fb["query"],
"response": fb["response"],
"corrected_response": fb["correction"],
"quality": "needs_improvement",
"score": fb.get("quality_score") or 2,
})
return training_data
# Singleton instance
feedback_service = FeedbackService()