KnowledgeProvider protocol with a deterministic TF-IDF-weighted extractive demo provider (never generative, always cites real excerpts) and a RAGcore HTTP adapter that degrades cleanly to unavailable. Knowledge nav + chat-style Q&A UI with source cards and honest grounded/insufficient/unavailable states. 57 backend tests passing, ruff clean. Fixed a real relevance bug (generic terms like "vehicle" crowding out distinctive matches) via IDF weighting, found by testing the actual S6 scenario. Verified end-to-end in the browser: grounded damage question cites both expected procedures; unrelated question honestly returns insufficient evidence with no fabrication.
85 lines
2.9 KiB
Python
85 lines
2.9 KiB
Python
from __future__ import annotations
|
|
|
|
import httpx
|
|
|
|
from app.services.knowledge.demo import DemoKnowledgeProvider
|
|
from app.services.knowledge.ragcore import RAGcoreKnowledgeProvider
|
|
|
|
|
|
def test_s6_damage_question_is_grounded_with_expected_sources():
|
|
provider = DemoKnowledgeProvider()
|
|
answer = provider.ask(
|
|
"What must I do when a vehicle returns with damage?", "test-correlation-1"
|
|
)
|
|
assert answer.evidence_state == "grounded"
|
|
document_ids = {s.document_id for s in answer.sources}
|
|
assert "damage-procedure" in document_ids
|
|
assert "vehicle-return-procedure" in document_ids
|
|
assert answer.answer # never empty for a grounded answer
|
|
for source in answer.sources:
|
|
assert source.excerpt
|
|
|
|
|
|
def test_unrelated_question_is_insufficient():
|
|
provider = DemoKnowledgeProvider()
|
|
answer = provider.ask("What is the capital of France?", "test-correlation-2")
|
|
assert answer.evidence_state == "insufficient"
|
|
assert "France" not in answer.answer # never fabricates an answer beyond the procedures
|
|
|
|
|
|
def test_demo_provider_health_reports_document_count():
|
|
provider = DemoKnowledgeProvider()
|
|
health = provider.health()
|
|
assert health.provider == "demo"
|
|
assert health.available is True
|
|
assert health.document_count == 10
|
|
|
|
|
|
def test_ask_question_endpoint_grounded(ops_client):
|
|
response = ops_client.post(
|
|
"/api/v1/knowledge/questions",
|
|
json={"question": "What must I do when a vehicle returns with damage?"},
|
|
)
|
|
assert response.status_code == 200
|
|
body = response.json()
|
|
assert body["evidence_state"] == "grounded"
|
|
assert body["provider"] == "demo"
|
|
assert len(body["sources"]) > 0
|
|
|
|
|
|
def test_ask_question_requires_authentication(client):
|
|
response = client.post("/api/v1/knowledge/questions", json={"question": "Anything?"})
|
|
assert response.status_code == 401
|
|
|
|
|
|
def test_ask_question_is_audited_without_leaking_full_text(ops_client):
|
|
ops_client.post(
|
|
"/api/v1/knowledge/questions",
|
|
json={"question": "What must I do when a vehicle returns with damage?"},
|
|
)
|
|
events = ops_client.get(
|
|
"/api/v1/audit", params={"action": "knowledge_question_asked"}
|
|
).json()
|
|
assert len(events) >= 1
|
|
metadata = events[0]["metadata"]
|
|
assert "evidence_state" in metadata
|
|
assert "source_ids" in metadata
|
|
assert "question" not in metadata
|
|
|
|
|
|
def test_knowledge_status_endpoint(ops_client):
|
|
response = ops_client.get("/api/v1/knowledge/status")
|
|
assert response.status_code == 200
|
|
assert response.json()["provider"] == "demo"
|
|
|
|
|
|
def test_ragcore_provider_degrades_to_unavailable(monkeypatch):
|
|
def fake_client(*args, **kwargs):
|
|
raise httpx.ConnectError("no ragcore in this environment")
|
|
|
|
provider = RAGcoreKnowledgeProvider()
|
|
monkeypatch.setattr(provider, "_client", fake_client)
|
|
answer = provider.ask("Anything?", "test-correlation-3")
|
|
assert answer.evidence_state == "unavailable"
|
|
assert answer.sources == []
|