M17: ground knowledge and integration evidence

This commit is contained in:
NuklearRabbit
2026-08-10 12:27:17 +02:00
parent 686795a452
commit 8030753dbc
15 changed files with 480 additions and 74 deletions
+47 -1
View File
@@ -3,11 +3,13 @@ from __future__ import annotations
import uuid
from typing import Literal
from fastapi import APIRouter, Depends
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
@@ -22,6 +24,11 @@ class AskQuestionRequest(BaseModel):
language: SupportedLanguage = "en-GB"
class KnowledgeFeedbackRequest(BaseModel):
correlation_id: uuid.UUID
helpful: bool
@router.post("/questions", response_model=GroundedAnswer)
def ask_question(
body: AskQuestionRequest,
@@ -51,6 +58,45 @@ def ask_question(
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",
+17 -3
View File
@@ -17,9 +17,10 @@ _GROUNDED_ANSWERABILITY = {"answerable", "partially_answerable"}
_LEAD_ANSWER_TEMPLATE = {
"en-GB": 'Per "{title}": {excerpt}',
"nl-BE": 'Volgens "{title}": {excerpt}',
"fr-BE": 'Selon « {title} » : {excerpt}',
"fr-BE": "Selon « {title} » : {excerpt}",
}
_DEFAULT_LANGUAGE = "en-GB"
_MAX_SOURCE_CARDS = 3
_DOMAIN_CONCEPTS: dict[str, tuple[str, ...]] = {
"damage": ("damage", "damaged", "schade", "beschadigd", "dommage", "endommagé"),
@@ -48,14 +49,26 @@ def _question_concepts(question: str) -> set[str]:
def _deduplicate_sources(sources: list[SourceCard]) -> list[SourceCard]:
seen: set[tuple[str, str]] = set()
"""Collapse duplicate chunks and re-uploaded document versions.
RAGcore document/version UUIDs change across uploads, so they are not useful
deduplication keys. Human-visible citation identity is the normalized title,
section and excerpt.
"""
seen: set[tuple[str, str, str]] = set()
unique: list[SourceCard] = []
for source in sources:
key = (source.document_id, source.section)
key = (
source.title.strip().casefold(),
source.section.strip().casefold(),
" ".join(source.excerpt.split()).casefold(),
)
if key in seen:
continue
seen.add(key)
unique.append(source)
if len(unique) == _MAX_SOURCE_CARDS:
break
return unique
@@ -193,6 +206,7 @@ class RAGcoreKnowledgeProvider:
)
for citation in citations.values()
]
sources = _deduplicate_sources(sources)
answerability = body.get("answerability", "not_answerable")
is_grounded = answerability in _GROUNDED_ANSWERABILITY and sources
evidence_state: EvidenceState = "grounded" if is_grounded else "insufficient"