mirror of
http://100.103.83.12:3003/fegger/odoo-at-payroll.git
synced 2026-09-17 16:56:42 +00:00
feat(agent): log answer comments
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user