from __future__ import annotations from typing import Any from sqlalchemy import select from sqlalchemy.orm import Session from app.db.base import Base from app.models.agent_feedback import AgentOperationFeedback from app.schemas.agent_feedback import ( AgentFeedbackCreate, AgentFeedbackRead, AgentFeedbackSummaryRead, ) LOW_RATING_MAX = 3 class AgentFeedbackService: def __init__(self, db: Session) -> None: self.db = db def ensure_storage_ready(self) -> None: Base.metadata.create_all(bind=self.db.get_bind(), tables=[AgentOperationFeedback.__table__]) def create_feedback(self, payload: AgentFeedbackCreate) -> AgentFeedbackRead: self.ensure_storage_ready() feedback = AgentOperationFeedback( run_id=payload.run_id, conversation_id=payload.conversation_id, user_id=payload.user_id, agent=payload.agent or "", source=payload.source or "", session_type=payload.session_type or "", operation_type=payload.operation_type or "assistant_round", operation_status=payload.operation_status or "", rating=int(payload.rating), reason=self._normalize_reason(payload.reason), context_json=self._normalize_context(payload.context_json), ) self.db.add(feedback) self.db.commit() self.db.refresh(feedback) return AgentFeedbackRead.model_validate(feedback) def summarize_feedback( self, *, agent: str | None = None, session_type: str | None = None, limit: int = 200, ) -> AgentFeedbackSummaryRead: self.ensure_storage_ready() stmt = select(AgentOperationFeedback).order_by(AgentOperationFeedback.created_at.desc()).limit(limit) if agent: stmt = stmt.where(AgentOperationFeedback.agent == agent) if session_type: stmt = stmt.where(AgentOperationFeedback.session_type == session_type) feedback_items = list(self.db.scalars(stmt).all()) rating_distribution = {str(score): 0 for score in range(1, 6)} agents: dict[str, int] = {} session_types: dict[str, int] = {} low_feedback: list[dict[str, Any]] = [] total_rating = 0 for item in feedback_items: rating = max(1, min(int(item.rating or 0), 5)) total_rating += rating rating_distribution[str(rating)] = rating_distribution.get(str(rating), 0) + 1 if item.agent: agents[item.agent] = agents.get(item.agent, 0) + 1 if item.session_type: session_types[item.session_type] = session_types.get(item.session_type, 0) + 1 if rating <= LOW_RATING_MAX: low_feedback.append( { "feedback_id": item.feedback_id, "run_id": item.run_id, "conversation_id": item.conversation_id, "user_id": item.user_id, "agent": item.agent, "session_type": item.session_type, "rating": rating, "reason": item.reason, "created_at": item.created_at, } ) total_feedback = len(feedback_items) average_rating = round(total_rating / total_feedback, 2) if total_feedback else 0.0 return AgentFeedbackSummaryRead( window_limit=limit, total_feedback=total_feedback, average_rating=average_rating, low_rating_count=len(low_feedback), rating_distribution=rating_distribution, agents=agents, session_types=session_types, recent_low_feedback=low_feedback[:10], ) @staticmethod def _normalize_reason(value: str | None) -> str | None: normalized = str(value or "").strip() return normalized[:1000] if normalized else None @staticmethod def _normalize_context(value: dict[str, Any] | None) -> dict[str, Any]: if not isinstance(value, dict): return {} return value