M5: implement RAGcore knowledge integration
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.
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import { useEffect, useState, type FormEvent } from "react";
|
||||
import { api, ApiError } from "../api/client";
|
||||
import type { GroundedAnswer, KnowledgeHealth } from "../api/types";
|
||||
|
||||
interface Exchange {
|
||||
question: string;
|
||||
answer: GroundedAnswer;
|
||||
}
|
||||
|
||||
const EVIDENCE_LABEL: Record<GroundedAnswer["evidence_state"], string> = {
|
||||
grounded: "Grounded in cited procedures",
|
||||
insufficient: "Insufficient evidence",
|
||||
unavailable: "Knowledge service unavailable",
|
||||
};
|
||||
|
||||
export function Knowledge() {
|
||||
const [status, setStatus] = useState<KnowledgeHealth | null>(null);
|
||||
const [question, setQuestion] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [exchanges, setExchanges] = useState<Exchange[]>([]);
|
||||
|
||||
useEffect(() => {
|
||||
api
|
||||
.get<KnowledgeHealth>("/api/v1/knowledge/status")
|
||||
.then(setStatus)
|
||||
.catch(() => setStatus(null));
|
||||
}, []);
|
||||
|
||||
async function handleSubmit(e: FormEvent) {
|
||||
e.preventDefault();
|
||||
if (!question.trim()) return;
|
||||
setError(null);
|
||||
setSubmitting(true);
|
||||
try {
|
||||
const answer = await api.post<GroundedAnswer>("/api/v1/knowledge/questions", { question });
|
||||
setExchanges((prev) => [{ question, answer }, ...prev]);
|
||||
setQuestion("");
|
||||
} catch (err) {
|
||||
setError(err instanceof ApiError ? err.message : "Could not reach the knowledge service.");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="page">
|
||||
<h1>Knowledge</h1>
|
||||
{status && (
|
||||
<p className="knowledge-status">
|
||||
Provider: <strong>{status.provider}</strong> ·{" "}
|
||||
{status.available ? "available" : "unavailable"} · {status.document_count} procedures
|
||||
indexed
|
||||
</p>
|
||||
)}
|
||||
|
||||
<form className="panel knowledge-form" onSubmit={handleSubmit} aria-labelledby="ask-heading">
|
||||
<h2 id="ask-heading">Ask a procedure question</h2>
|
||||
<label htmlFor="knowledge-question" className="visually-hidden">
|
||||
Question
|
||||
</label>
|
||||
<div className="knowledge-input-row">
|
||||
<input
|
||||
id="knowledge-question"
|
||||
type="text"
|
||||
value={question}
|
||||
onChange={(e) => setQuestion(e.target.value)}
|
||||
placeholder="e.g. What must I do when a vehicle returns with damage?"
|
||||
minLength={3}
|
||||
maxLength={1000}
|
||||
required
|
||||
/>
|
||||
<button type="submit" disabled={submitting}>
|
||||
{submitting ? "Asking…" : "Ask"}
|
||||
</button>
|
||||
</div>
|
||||
{error && <p className="error" role="alert">{error}</p>}
|
||||
</form>
|
||||
|
||||
{exchanges.length === 0 && !error && (
|
||||
<p>Ask a question about one of the ten operational procedures to see cited sources.</p>
|
||||
)}
|
||||
|
||||
<ul className="exchange-list">
|
||||
{exchanges.map((exchange, index) => (
|
||||
<li key={index} className="panel exchange">
|
||||
<p className="exchange-question">
|
||||
<strong>Q:</strong> {exchange.question}
|
||||
</p>
|
||||
<p className={`evidence-state evidence-${exchange.answer.evidence_state}`}>
|
||||
{EVIDENCE_LABEL[exchange.answer.evidence_state]}
|
||||
</p>
|
||||
|
||||
{exchange.answer.evidence_state === "unavailable" ? (
|
||||
<p>
|
||||
The knowledge service is currently unreachable. Operational features are
|
||||
unaffected — try again later.
|
||||
</p>
|
||||
) : (
|
||||
<p>{exchange.answer.answer}</p>
|
||||
)}
|
||||
|
||||
{exchange.answer.sources.length > 0 && (
|
||||
<ul className="source-cards">
|
||||
{exchange.answer.sources.map((source) => (
|
||||
<li key={`${source.document_id}-${source.section}`} className="source-card">
|
||||
<p className="source-title">
|
||||
{source.title} <span className="source-version">v{source.version}</span>
|
||||
</p>
|
||||
<p className="source-section">{source.section}</p>
|
||||
<p className="source-excerpt">{source.excerpt}</p>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user