bifroest/app/feedback/service.py
2026-10-27 08:34:17 +01:00

131 lines
5.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""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]