"""Rückmeldungen abgeben und auswerten. Abgeben darf, wer den Chat sieht (alle Mitglieder des Workspace).""" from dataclasses import dataclass, field from datetime import datetime from typing import Any from sqlalchemy import case, delete, func, select from sqlalchemy.orm import Session from app.chat import service as chats from app.chat.models import ChatSession, Message from app.core.database import now from app.core.errors import Invalid, NotFound from app.feedback.models import DOWN, UP, AnswerFeedback from app.identity import Principal from app.identity.models import User from app.workspaces.models import Workspace MAX_COMMENT = 1000 RECENT = 50 def rate(session: Session, principal: Principal, message_id: int, rating: int, comment: str | None = None) -> AnswerFeedback | None: """rating 1/-1 setzt, 0 nimmt die Rückmeldung zurück; comment None lässt den Kommentar unverändert.""" message = session.get(Message, message_id) if message is None or message.role != "assistant": raise NotFound chats.get(session, principal, message.chat_session_id) if rating not in (UP, DOWN, 0): raise Invalid("Ungültige Bewertung.") key = (message_id, principal.user_id) if rating == 0: session.execute(delete(AnswerFeedback).where(AnswerFeedback.message_id == message_id, AnswerFeedback.user_id == principal.user_id)) return None found = session.get(AnswerFeedback, key) if found is None: found = AnswerFeedback(message_id=message_id, user_id=principal.user_id, rating=rating) session.add(found) found.rating, found.updated = rating, now() if comment is not None: found.comment = " ".join(comment.split())[:MAX_COMMENT] session.flush() return found def mine(session: Session, user_id: int, message_ids: list[int]) -> dict[int, AnswerFeedback]: if not message_ids: return {} rows = session.scalars(select(AnswerFeedback).where(AnswerFeedback.user_id == user_id, AnswerFeedback.message_id.in_(message_ids))) return {f.message_id: f for f in rows} @dataclass class Row: time: datetime rating: int comment: str user: str workspace: str chat_id: int chat_title: str message_id: int model: str tools: int @dataclass class Report: up: int = 0 down: int = 0 by_model: list[tuple[str, int, int]] = field(default_factory=list) recent: list[Row] = field(default_factory=list) @property def total(self) -> int: return self.up + self.down @property def share(self) -> int | None: """Anteil „hilfreich“ in Prozent.""" return round(100 * self.up / self.total) if self.total else None def query(since: datetime, until: datetime, workspace_id: int | None): stmt = (select(AnswerFeedback, Message, ChatSession, User.display_name, Workspace.name) .join(Message, Message.id == AnswerFeedback.message_id) .join(ChatSession, ChatSession.id == Message.chat_session_id) .join(Workspace, Workspace.id == ChatSession.workspace_id) .join(User, User.id == AnswerFeedback.user_id) .where(AnswerFeedback.updated >= since, AnswerFeedback.updated < until)) if workspace_id is not None: stmt = stmt.where(ChatSession.workspace_id == workspace_id) return stmt def row(feedback: AnswerFeedback, message: Message, chat: ChatSession, user: str, workspace: str) -> Row: tools = sum(1 for item in message.items or [] if isinstance(item, dict) and item.get("type") == "function_call") return Row(feedback.updated, feedback.rating, feedback.comment, user, workspace, chat.id, chat.title, message.id, message.model, tools) def report(session: Session, since: datetime, until: datetime, workspace_id: int | None = None) -> Report: base = query(since, until, workspace_id).subquery() up = func.count(case((base.c.rating == UP, 1))) down = func.count(case((base.c.rating == DOWN, 1))) result = Report() for model, ups, downs in session.execute(select(base.c.model, up, down).group_by(base.c.model) .order_by(func.count().desc())): result.by_model.append((model or "–", ups, downs)) result.up, result.down = result.up + ups, result.down + downs # Zuerst die mit Kommentar und die negativen: daraus lernt man am meisten stmt = query(since, until, workspace_id).order_by( (AnswerFeedback.comment != "").desc(), AnswerFeedback.rating, AnswerFeedback.updated.desc()).limit(RECENT) result.recent = [row(*r) for r in session.execute(stmt)] return result def rows(session: Session, since: datetime, until: datetime, workspace_id: int | None = None) -> list[Row]: stmt = query(since, until, workspace_id).order_by(AnswerFeedback.updated) return [row(*r) for r in session.execute(stmt)] def csv_rows(found: list[Row]) -> list[list[Any]]: head = ["Zeit", "Bewertung", "Kommentar", "Person", "Workspace", "Chat", "Chat-ID", "Antwort-ID", "Modell", "Tool-Aufrufe"] return [head] + [[r.time.astimezone().strftime("%Y-%m-%d %H:%M"), "hilfreich" if r.rating == UP else "nicht hilfreich", r.comment, r.user, r.workspace, r.chat_title, r.chat_id, r.message_id, r.model, r.tools] for r in found]