mirror of
http://100.103.83.12:3003/fegger/pv-agent.git
synced 2026-09-17 15:46:23 +00:00
feat(agent): add index bootstrap and answer feedback
This commit is contained in:
+65
-2
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user