131 lines
5.4 KiB
Python
131 lines
5.4 KiB
Python
"""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]
|