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, "statistics_state": "sync_reported", } )