Files
MobilityOps/backend/app/api/routers/knowledge.py
T
NuklearRabbit efab8d816f
MobilityOps acceptance / backend (push) Canceled after 0s
MobilityOps acceptance / frontend (push) Canceled after 0s
M31: verify RAG inventory and polish attention queue
2026-08-10 20:58:26 +02:00

136 lines
4.3 KiB
Python

from __future__ import annotations
import uuid
from typing import Literal
from fastapi import APIRouter, Depends, HTTPException
from pydantic import BaseModel, Field
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.api.deps import get_current_user, get_db
from app.models.audit import AuditEvent
from app.schemas import CurrentUser
from app.services.audit import record_audit_event
from app.services.knowledge import GroundedAnswer, KnowledgeHealth, get_knowledge_provider
router = APIRouter(prefix="/api/v1/knowledge", tags=["knowledge"])
SupportedLanguage = Literal["nl-BE", "en-GB", "fr-BE"]
class AskQuestionRequest(BaseModel):
question: str = Field(min_length=3, max_length=1000)
language: SupportedLanguage = "en-GB"
class KnowledgeFeedbackRequest(BaseModel):
correlation_id: uuid.UUID
helpful: bool
@router.post("/questions", response_model=GroundedAnswer)
def ask_question(
body: AskQuestionRequest,
db: Session = Depends(get_db),
user: CurrentUser = Depends(get_current_user),
) -> GroundedAnswer:
correlation_id = str(uuid.uuid4())
provider = get_knowledge_provider()
answer = provider.ask(body.question, correlation_id, body.language)
record_audit_event(
db,
actor_type="user",
actor_label=user.display_name,
action="knowledge_question_asked",
entity_type="knowledge",
correlation_id=uuid.UUID(correlation_id),
metadata={
"evidence_state": answer.evidence_state,
"provider": answer.provider,
"source_ids": [s.document_id for s in answer.sources],
"question_length": len(body.question),
"language": body.language,
},
)
db.commit()
return answer
@router.post("/feedback")
def record_feedback(
body: KnowledgeFeedbackRequest,
db: Session = Depends(get_db),
user: CurrentUser = Depends(get_current_user),
) -> dict[str, str]:
question_event = db.scalar(
select(AuditEvent.id).where(
AuditEvent.action == "knowledge_question_asked",
AuditEvent.correlation_id == body.correlation_id,
AuditEvent.actor_label == user.display_name,
)
)
if question_event is None:
raise HTTPException(status_code=404, detail="Knowledge exchange not found")
existing = db.scalar(
select(AuditEvent).where(
AuditEvent.action == "knowledge_feedback_recorded",
AuditEvent.correlation_id == body.correlation_id,
AuditEvent.actor_label == user.display_name,
)
)
if existing is not None:
existing.metadata_json = {"helpful": body.helpful}
else:
record_audit_event(
db,
actor_type="user",
actor_label=user.display_name,
action="knowledge_feedback_recorded",
entity_type="knowledge",
correlation_id=body.correlation_id,
metadata={"helpful": body.helpful},
)
db.commit()
return {"status": "recorded"}
@router.get("/status", response_model=KnowledgeHealth)
def knowledge_status(
language: SupportedLanguage = "en-GB",
db: Session = Depends(get_db),
_user: CurrentUser = Depends(get_current_user),
) -> KnowledgeHealth:
health = get_knowledge_provider().health(language)
if health.provider != "ragcore":
return health
latest_sync = db.scalar(
select(AuditEvent)
.where(AuditEvent.action == "n8n_procedures_synced")
.order_by(AuditEvent.occurred_at.desc())
.limit(1)
)
if latest_sync is None:
return health
reported = latest_sync.after_json or {}
synced = reported.get("synced")
failed = reported.get("failed")
return health.model_copy(
update={
"reported_synced_document_count": synced if isinstance(synced, int) else None,
"reported_failed_document_count": failed if isinstance(failed, int) else None,
"last_sync_at": latest_sync.occurred_at,
# A persisted sync callback is useful additional provenance, but must not
# downgrade stronger provider-side verification to merely "reported".
"statistics_state": (
health.statistics_state
if health.statistics_state == "verified"
else "sync_reported"
),
}
)