feat(agent): log answer comments

This commit is contained in:
2026-09-17 00:02:31 +02:00
parent c005b9b306
commit d9c612e354
10 changed files with 241 additions and 55 deletions
+3 -2
View File
@@ -77,8 +77,9 @@ docker compose up -d
UI: `http://100.103.83.12:8080/`. `data/` wird schreibbar und
`wissensbasis/` read-only eingebunden; weder Index noch lokale Quellkorpora
landen im Image. Fehlt `data/index.db`, baut der Container ihn vor dem API-Start
automatisch mit vollständigen Embeddings auf. Fragen, Antworten und Bewertungen
werden im Compose-Profil standardmäßig nach `data/audit.db` und als
automatisch mit vollständigen Embeddings auf. Fragen, Antworten, Bewertungen
und unabhängige Antwortkommentare werden im Compose-Profil standardmäßig nach
`data/audit.db` und als
strukturierte `AUDIT`-Zeilen in die Containerlogs geschrieben.
## Konfiguration (Umgebungsvariablen)
+40
View File
@@ -124,6 +124,18 @@ class RatingResponse(StrictModel):
accepted: Literal[True] = True
class CommentRequest(StrictModel):
request_id: str = Field(pattern=r"^[A-Za-z0-9._:-]{1,128}$")
comment: str = Field(min_length=1, max_length=2000)
class CommentResponse(StrictModel):
api_version: Literal["v1"] = API_VERSION
request_id: str
comment_id: int
accepted: Literal[True] = True
class AppState:
def __init__(self) -> None:
self.cfg: Config | None = None
@@ -435,6 +447,34 @@ def rate_answer(req: RatingRequest, request: Request) -> RatingResponse:
return RatingResponse(request_id=req.request_id, rating=req.rating)
@app.post(
"/v1/comments",
response_model=CommentResponse,
dependencies=[Depends(require_api_access)],
)
def comment_answer(req: CommentRequest, request: Request) -> CommentResponse:
rag: AppState = request.app.state.rag
audit = rag.get_audit()
if audit is None:
raise HTTPException(status_code=503, detail="Kommentare sind nicht aktiviert.")
comment = req.comment.strip()
if not comment:
raise HTTPException(status_code=422, detail="Kommentar darf nicht leer sein.")
try:
comment_id = audit.record_comment(req.request_id, comment)
except KeyError as exc:
raise HTTPException(status_code=404, detail="Antwort nicht gefunden.") from exc
except Exception as exc:
logger.exception("Kommentar fehlgeschlagen request_id=%s", req.request_id)
raise HTTPException(
status_code=503, detail="Kommentar konnte nicht gespeichert werden."
) from exc
return CommentResponse(
request_id=req.request_id,
comment_id=comment_id,
)
def _reindex(request: Request) -> dict:
rag: AppState = request.app.state.rag
cfg = rag.ensure()
+65 -2
View File
@@ -35,10 +35,19 @@ CREATE TABLE IF NOT EXISTS ratings (
feedback TEXT,
FOREIGN KEY (request_id) REFERENCES interactions(request_id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS comments (
comment_id INTEGER PRIMARY KEY AUTOINCREMENT,
request_id TEXT NOT NULL,
created_at INTEGER NOT NULL,
comment TEXT,
FOREIGN KEY (request_id) REFERENCES interactions(request_id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS interactions_created_at_idx
ON interactions(created_at DESC);
CREATE INDEX IF NOT EXISTS ratings_updated_at_idx
ON ratings(updated_at DESC);
CREATE INDEX IF NOT EXISTS comments_request_created_idx
ON comments(request_id, created_at DESC);
"""
@@ -94,11 +103,20 @@ class AuditStore:
)
with self._lock:
self._con.execute(
"INSERT OR REPLACE INTO interactions("
"INSERT INTO interactions("
"request_id, created_at, question, answer, status, verified, refused, "
"citations_json, sources_json, conflicts_json, planned_queries_json, "
"model, latency_ms, n_context, regenerations"
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
") VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) "
"ON CONFLICT(request_id) DO UPDATE SET "
"created_at=excluded.created_at, question=excluded.question, "
"answer=excluded.answer, status=excluded.status, "
"verified=excluded.verified, refused=excluded.refused, "
"citations_json=excluded.citations_json, sources_json=excluded.sources_json, "
"conflicts_json=excluded.conflicts_json, "
"planned_queries_json=excluded.planned_queries_json, "
"model=excluded.model, latency_ms=excluded.latency_ms, "
"n_context=excluded.n_context, regenerations=excluded.regenerations",
values,
)
self._con.commit()
@@ -155,6 +173,40 @@ class AuditStore:
flush=True,
)
def record_comment(self, request_id: str, comment: str) -> int:
now = int(time.time())
stored_comment = comment if self.cfg.audit_log_content else None
with self._lock:
exists = self._con.execute(
"SELECT 1 FROM interactions WHERE request_id = ?", (request_id,)
).fetchone()
if exists is None:
raise KeyError(request_id)
cursor = self._con.execute(
"INSERT INTO comments(request_id, created_at, comment) VALUES (?, ?, ?)",
(request_id, now, stored_comment),
)
self._con.commit()
if cursor.lastrowid is None:
raise RuntimeError("Kommentar wurde ohne ID gespeichert")
comment_id = int(cursor.lastrowid)
if self.cfg.audit_stdout:
print(
"AUDIT "
+ json.dumps(
{
"event": "agent_comment",
"created_at": now,
"request_id": request_id,
"comment_id": comment_id,
"comment": stored_comment,
},
ensure_ascii=False,
),
flush=True,
)
return comment_id
def recent(self, limit: int = 20) -> list[dict]:
with self._lock:
rows = self._con.execute(
@@ -163,6 +215,16 @@ class AuditStore:
"ORDER BY i.created_at DESC LIMIT ?",
(limit,),
).fetchall()
comments_by_request: dict[str, list[dict]] = {}
for row in rows:
comment_rows = self._con.execute(
"SELECT comment_id, created_at, comment FROM comments "
"WHERE request_id = ? ORDER BY created_at, comment_id",
(row["request_id"],),
).fetchall()
comments_by_request[row["request_id"]] = [
dict(comment_row) for comment_row in comment_rows
]
out = []
for row in rows:
item = dict(row)
@@ -175,5 +237,6 @@ class AuditStore:
item[field.removesuffix("_json")] = json.loads(item.pop(field))
item["verified"] = bool(item["verified"])
item["refused"] = bool(item["refused"])
item["comments"] = comments_by_request[item["request_id"]]
out.append(item)
return out