self-trained2 / agent /feedback.py
DeepImagix's picture
Upload feedback.py
660a19a verified
Raw History Blame Contribute Delete
8.74 kB
"""
NeuraPrompt Agent β€” Feedback Module (v1.0)
============================================
Collects user feedback on AI responses. Stored in MongoDB (neuraprompt DB,
separate collection). Reuses main.py's MongoClient connection.
Collections:
neuraprompt.feedback β€” all feedback types in one collection with a "type" field
Feedback types:
- "binary" β€” thumbs up/down on a specific message
- "rating" β€” 1-5 stars for helpfulness, accuracy, relevance
- "tags" β€” categorical tags (inaccurate, creative, etc.)
- "text" β€” free text "what went wrong?"
- "correction" β€” preferred answer (RLHF training data)
- "product" β€” bug report, feature request, safety concern
Endpoints:
POST /api/feedback β€” Submit feedback
GET /api/feedback/{uid} β€” Get user's feedback history (for admin)
GET /api/feedback/stats β€” Aggregate feedback stats (for admin)
"""
from __future__ import annotations
import os
import sys
import logging
from datetime import datetime, timezone
from typing import Optional, Dict, Any, List
from fastapi import APIRouter, Request, HTTPException, Query
from fastapi.responses import JSONResponse
from pydantic import BaseModel, Field
log = logging.getLogger("agent.feedback")
router = APIRouter(prefix="/api/feedback", tags=["feedback"])
# ── MONGODB (reuse main.py's connection) ──────────────────────────────────────
_db = None
def _get_db():
"""Get the neuraprompt database. Reuses main.py's MongoClient."""
global _db
if _db is not None:
return _db
try:
main_mod = sys.modules.get("main")
if main_mod and hasattr(main_mod, "neuraprompt_db"):
_db = main_mod.neuraprompt_db
return _db
except Exception:
pass
# Fallback: own connection
try:
from pymongo import MongoClient
from pymongo.server_api import ServerApi
MONGO_URL = os.environ.get("MONGO_URI", "")
client = MongoClient(
MONGO_URL, ssl=True,
tlsAllowInvalidCertificates=False,
tlsCAFile="/etc/ssl/certs/ca-certificates.crt",
server_api=ServerApi("1"),
)
_db = client["neuraprompt"]
return _db
except Exception as e:
log.error(f"[Feedback] DB connection failed: {e}")
return None
# ── PYDANTIC MODELS ───────────────────────────────────────────────────────────
class FeedbackRequest(BaseModel):
user_id: str = Field(..., description="Firebase UID")
feedback_type: str = Field(..., description="binary | rating | tags | text | correction | product")
message_content: str = Field(default="", description="The AI message being feedbacked (truncated)")
user_message: str = Field(default="", description="What the user asked")
# Binary
thumbs: Optional[str] = Field(default=None, description="up | down")
# Rating (1-5)
helpfulness: Optional[int] = Field(default=None, description="1-5")
accuracy: Optional[int] = Field(default=None, description="1-5")
relevance: Optional[int] = Field(default=None, description="1-5")
# Tags
tags: Optional[List[str]] = Field(default=None, description="List of tag names")
# Free text
comment: Optional[str] = Field(default=None, description="Free text feedback")
# Correction
preferred_answer: Optional[str] = Field(default=None, description="What the AI should have said")
# Product feedback
product_type: Optional[str] = Field(default=None, description="bug | feature | safety")
# Metadata
mode: Optional[str] = Field(default=None, description="fast | web | agent | kype")
model_used: Optional[str] = Field(default=None, description="Model that generated the response")
# ── ENDPOINTS ─────────────────────────────────────────────────────────────────
@router.post("")
async def submit_feedback(req: FeedbackRequest):
"""Submit user feedback. Stored in neuraprompt.feedback collection."""
db = _get_db()
if db is None:
return JSONResponse({"error": "Database unavailable"}, status_code=503)
try:
doc = {
"user_id": req.user_id,
"feedback_type": req.feedback_type,
"message_content": req.message_content[:500], # truncate for storage
"user_message": req.user_message[:500],
"thumbs": req.thumbs,
"helpfulness": req.helpfulness,
"accuracy": req.accuracy,
"relevance": req.relevance,
"tags": req.tags,
"comment": req.comment[:2000] if req.comment else None,
"preferred_answer": req.preferred_answer[:2000] if req.preferred_answer else None,
"product_type": req.product_type,
"mode": req.mode,
"model_used": req.model_used,
"created_at": datetime.now(timezone.utc),
"status": "new", # new | reviewed | resolved
}
result = db.feedback.insert_one(doc)
log.info(f"[Feedback] Saved {req.feedback_type} feedback from {req.user_id}: {result.inserted_id}")
return {
"status": "success",
"feedback_id": str(result.inserted_id),
"message": "Thank you for your feedback!"
}
except Exception as e:
log.error(f"[Feedback] Submit failed: {e}")
return JSONResponse({"error": str(e)}, status_code=500)
@router.get("/{user_id}")
async def get_user_feedback(user_id: str):
"""Get a user's feedback history."""
db = _get_db()
if db is None:
return JSONResponse({"error": "Database unavailable"}, status_code=503)
try:
feedbacks = []
cursor = db.feedback.find({"user_id": user_id}).sort("created_at", -1).limit(50)
for f in cursor:
f["_id"] = str(f["_id"])
if "created_at" in f and hasattr(f["created_at"], "isoformat"):
f["created_at"] = f["created_at"].isoformat()
feedbacks.append(f)
return {"feedbacks": feedbacks, "count": len(feedbacks)}
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)
@router.get("/stats/summary")
async def get_feedback_stats():
"""Get aggregate feedback statistics. For admin dashboard."""
db = _get_db()
if db is None:
return JSONResponse({"error": "Database unavailable"}, status_code=503)
try:
total = db.feedback.count_documents({})
thumbs_up = db.feedback.count_documents({"thumbs": "up"})
thumbs_down = db.feedback.count_documents({"thumbs": "down"})
# Tag distribution
tag_pipeline = [
{"$unwind": "$tags"},
{"$group": {"_id": "$tags", "count": {"$sum": 1}}},
{"$sort": {"count": -1}},
]
tag_data = list(db.feedback.aggregate(tag_pipeline))
tag_dist = {t["_id"]: t["count"] for t in tag_data}
# Type distribution
type_pipeline = [
{"$group": {"_id": "$feedback_type", "count": {"$sum": 1}}},
]
type_data = list(db.feedback.aggregate(type_pipeline))
type_dist = {t["_id"]: t["count"] for t in type_data}
# Average ratings
avg_pipeline = [
{"$match": {"helpfulness": {"$gte": 1}}},
{"$group": {
"_id": None,
"avg_helpfulness": {"$avg": "$helpfulness"},
"avg_accuracy": {"$avg": "$accuracy"},
"avg_relevance": {"$avg": "$relevance"},
"count": {"$sum": 1},
}},
]
avg_data = list(db.feedback.aggregate(avg_pipeline))
avg_ratings = avg_data[0] if avg_data else {}
return {
"total_feedback": total,
"thumbs_up": thumbs_up,
"thumbs_down": thumbs_down,
"thumb_ratio": round(thumbs_up / max(thumbs_up + thumbs_down, 1), 2),
"tag_distribution": tag_dist,
"type_distribution": type_dist,
"average_ratings": {
"helpfulness": round(avg_ratings.get("avg_helpfulness", 0), 1),
"accuracy": round(avg_ratings.get("avg_accuracy", 0), 1),
"relevance": round(avg_ratings.get("avg_relevance", 0), 1),
"count": avg_ratings.get("count", 0),
},
}
except Exception as e:
return JSONResponse({"error": str(e)}, status_code=500)