feat(agent): add index bootstrap and answer feedback

This commit is contained in:
2026-09-16 22:40:06 +02:00
parent fc656188cf
commit c005b9b306
19 changed files with 757 additions and 41 deletions
+65 -2
View File
@@ -20,6 +20,7 @@ from fastapi.responses import FileResponse
from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from pydantic import BaseModel, ConfigDict, Field
from .audit import AuditStore
from .config import Config
from .generate import CITE_RE, answer_question
from .ingest import build_index
@@ -97,6 +98,7 @@ class AskResponse(StrictModel):
model: str
latency_ms: int
regenerations: int = 0
ratings_enabled: bool = False
class HealthResponse(StrictModel):
@@ -106,6 +108,20 @@ class HealthResponse(StrictModel):
index: dict
ollama_up: bool
authentication_enabled: bool
ratings_enabled: bool
class RatingRequest(StrictModel):
request_id: str = Field(pattern=r"^[A-Za-z0-9._:-]{1,128}$")
rating: Literal["up", "down"]
feedback: str | None = Field(default=None, max_length=1000)
class RatingResponse(StrictModel):
api_version: Literal["v1"] = API_VERSION
request_id: str
rating: Literal["up", "down"]
accepted: Literal[True] = True
class AppState:
@@ -113,6 +129,7 @@ class AppState:
self.cfg: Config | None = None
self.client: OllamaClient | None = None
self.retriever: Retriever | None = None
self.audit: AuditStore | None = None
self.maintenance_lock = threading.Lock()
self.state_lock = threading.RLock()
@@ -139,6 +156,14 @@ class AppState:
self.retriever = Retriever(self.ensure())
return self.retriever
def get_audit(self) -> AuditStore | None:
with self.state_lock:
if not self.ensure().audit_enabled:
return None
if self.audit is None:
self.audit = AuditStore(self.ensure())
return self.audit
def reset_retriever(self) -> None:
with self.state_lock:
if self.retriever is not None:
@@ -152,6 +177,8 @@ async def lifespan(app: FastAPI):
yield
rag: AppState = app.state.rag
rag.reset_retriever()
if rag.audit:
rag.audit.close()
if rag.client:
rag.client.close()
@@ -266,7 +293,9 @@ def _extract_clarification(answer: str, refused: bool) -> str | None:
return question or None
def _response_from_result(result: dict, request_id: str) -> AskResponse:
def _response_from_result(
result: dict, request_id: str, ratings_enabled: bool = False
) -> AskResponse:
if not result["verified"]:
status = "uncertain"
elif result["refused"]:
@@ -304,6 +333,7 @@ def _response_from_result(result: dict, request_id: str) -> AskResponse:
model=result["model"],
latency_ms=result["latency_ms"],
regenerations=result["regenerations"],
ratings_enabled=ratings_enabled,
)
@@ -326,7 +356,17 @@ def _ask(req: AskRequest, request: Request) -> AskResponse:
detail="Der Wissensdienst ist vorübergehend nicht verfügbar.",
) from exc
result.pop("draft", None)
return _response_from_result(result, request_id)
response = _response_from_result(
result, request_id, ratings_enabled=cfg.audit_enabled
)
audit = rag.get_audit()
if audit is not None:
try:
audit.record_interaction(response.model_dump(mode="json"))
except Exception:
# Die Fachantwort darf bei einem reinen Audit-Fehler nicht verloren gehen.
logger.exception("Audit-Protokollierung fehlgeschlagen request_id=%s", request_id)
return response
@app.post("/v1/ask", response_model=AskResponse, dependencies=[Depends(require_api_access)])
@@ -359,6 +399,7 @@ def _health(request: Request) -> HealthResponse:
index=index,
ollama_up=ollama_up,
authentication_enabled=bool(cfg.api_key),
ratings_enabled=cfg.audit_enabled,
)
@@ -372,6 +413,28 @@ def health_compat(request: Request) -> HealthResponse:
return _health(request)
@app.post(
"/v1/ratings",
response_model=RatingResponse,
dependencies=[Depends(require_api_access)],
)
def rate_answer(req: RatingRequest, request: Request) -> RatingResponse:
rag: AppState = request.app.state.rag
audit = rag.get_audit()
if audit is None:
raise HTTPException(status_code=503, detail="Bewertungen sind nicht aktiviert.")
try:
audit.record_rating(req.request_id, req.rating, req.feedback)
except KeyError as exc:
raise HTTPException(status_code=404, detail="Antwort nicht gefunden.") from exc
except Exception as exc:
logger.exception("Bewertung fehlgeschlagen request_id=%s", req.request_id)
raise HTTPException(
status_code=503, detail="Bewertung konnte nicht gespeichert werden."
) from exc
return RatingResponse(request_id=req.request_id, rating=req.rating)
def _reindex(request: Request) -> dict:
rag: AppState = request.app.state.rag
cfg = rag.ensure()