From 8e8089b84d8cfaa58c6feb5014bd96317e30452e Mon Sep 17 00:00:00 2001 From: shipitdev Date: Tue, 10 Nov 2026 19:43:47 +0530 Subject: [PATCH 1/2] Collect run feedback and require reviewed references for regression cases --- .gitignore | 6 ++ README.md | 18 +++- frontend/src/app/api/feedback/route.ts | 22 ++++ frontend/src/app/globals.css | 11 ++ frontend/src/components/workspace.tsx | 58 ++++++++++ rag_app/api.py | 35 +++++- rag_app/feedback.py | 61 +++++++++++ scripts/eval_ragas.py | 26 +++-- scripts/eval_suite.py | 8 +- scripts/feedback_cases.py | 119 +++++++++++++++++++++ scripts/publish_langsmith.py | 14 ++- tests/test_feedback.py | 141 +++++++++++++++++++++++++ tests/test_ragas_eval.py | 11 ++ 13 files changed, 514 insertions(+), 16 deletions(-) create mode 100644 frontend/src/app/api/feedback/route.ts create mode 100644 rag_app/feedback.py create mode 100644 scripts/feedback_cases.py create mode 100644 tests/test_feedback.py diff --git a/.gitignore b/.gitignore index 59613b8..adb2746 100644 --- a/.gitignore +++ b/.gitignore @@ -134,3 +134,9 @@ !/tests/test_langsmith_export.py !/evaluations/results/observability_v1.json + +!/rag_app/feedback.py +!/tests/test_feedback.py +!/scripts/feedback_cases.py +!/frontend/src/app/api/feedback/ +!/frontend/src/app/api/feedback/route.ts diff --git a/README.md b/README.md index e880cea..228b346 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,22 @@ Publishing replays saved outputs into a LangSmith experiment with response statu The [November 10 observability batch](evaluations/results/observability_v1.json) selected **12 training questions**. Retrieval selected the labeled article for **11/12**. **3 answers** were released and passed citation-format checks. **9 provider quota failures** prevented completion. The run used **15 HTTP attempts and 33,230 reported tokens**. This batch establishes dashboard visibility and exposes a quota-handling issue, which is now covered by a stop-on-quota regression test. New semantic RAGAS scores await available provider quota. +## Feedback and future evaluation cases + +Every completed response offers Helpful and Not helpful, with an optional explanation. Sending feedback saves the question, resolved follow-up, answer, sources, private diagnostics and pipeline versions in `data/app/feedback/`. These private records persist after the session ends. A stable feedback record also attaches the rating and comment to the LangSmith trace. If cloud synchronization fails, the local record remains available and `python -m scripts.feedback_cases sync` retries it. + +Review the saved case against its sources, write a checked reference answer in a local text file, then approve and export it. + +```bash +python -m scripts.feedback_cases approve --run-id RUN_UUID \ + --reference-file /path/to/checked-answer.txt --notes "Verified against the original source" +python -m scripts.feedback_cases export --output data/ragas/feedback_review_v1 +python -m scripts.eval_ragas run --cases data/ragas/feedback_review_v1 \ + --metrics faithfulness factual_correctness answer_completeness --limit 5 --max-requests 20 +``` + +Approval and review notes are required for feedback-based RAGAS evaluation. Use these cases as a regression set alongside the fixed training sample. Human votes and comments identify examples for review, while checked references support correctness scoring. Human ratings can also help assess and improve judge agreement. Each export uses a new directory to preserve the earlier evaluation version. + ## Evaluation suite ![Evaluation layers: components, pipeline, application, regression and planned monitoring](evaluations/overview.svg) @@ -54,7 +70,7 @@ The [November 10 observability batch](evaluations/results/observability_v1.json) | Pipeline | Selected context precision/recall, answer faithfulness and relevancy | [RAGAS runner](scripts/eval_ragas.py), measured on the pilot below. Reference text stays outside inference. | | Application | Reference-based correctness and completeness; citation validity; withholding and layout contracts; recorded timing/tokens/cache use | Correctness = RAGAS `FactualCorrectness(mode="f1")`; completeness = its `mode="recall"` proxy. Both are opt-in and not yet benchmarked. [Free saved-case checks](scripts/eval_suite.py), [API contracts](tests/test_app_api.py), [layout checks](frontend/tests/genui-contract.test.mjs) are implemented. | | Regression | Re-run a fixed case set, then inspect per-case differences | [Comparison CLI](scripts/eval_suite.py) rejects changed cohorts/references. Semantic comparisons also require matching judge settings and exclude scores flagged for review. | -| Online | Sampled live judging, feedback, drift and failure-to-test-set promotion | **Planned.** Content-free operational logs exist; live quality scoring, Langfuse integration and automated feedback loops do not. | +| Online | User feedback, reviewed regression cases, sampled judging and drift | Helpful/unhelpful votes and optional comments save the run for review and sync to LangSmith. Reviewed references can be exported for RAGAS. Sampled live judging and drift monitoring remain planned. | Style rubrics and broad toxicity/PII/jailbreak evaluation are also planned. Existing origin/session, withholding and generated-component checks cover specific application contracts; they are not a comprehensive safety benchmark. Optional semantic metrics compare against available references, which may omit valid alternative answers. diff --git a/frontend/src/app/api/feedback/route.ts b/frontend/src/app/api/feedback/route.ts new file mode 100644 index 0000000..a86346a --- /dev/null +++ b/frontend/src/app/api/feedback/route.ts @@ -0,0 +1,22 @@ +import { isAllowedOrigin } from "@/lib/request-origin"; + +export async function POST(request: Request) { + if (!isAllowedOrigin(request)) return Response.json({ message: "Cross-origin requests are not allowed." }, { status: 403 }); + if (Number(request.headers.get("content-length") || 0) > 16_000) return Response.json({ message: "Feedback is too large." }, { status: 413 }); + let body: string; + try { body = await request.text(); } + catch { return Response.json({ message: "Feedback could not be read." }, { status: 400 }); } + if (body.length > 16_000) return Response.json({ message: "Feedback is too large." }, { status: 413 }); + try { + const upstream = await fetch(new URL("feedback", process.env.RAG_API_URL || "http://127.0.0.1:8000/ask"), { + method: "POST", + headers: { "content-type": "application/json", ...(request.headers.get("cookie") ? { cookie: request.headers.get("cookie")! } : {}) }, + body, + cache: "no-store", + signal: AbortSignal.timeout(10_000), + }); + return new Response(upstream.body, { status: upstream.status, headers: { "content-type": "application/json", "cache-control": "no-store" } }); + } catch { + return Response.json({ message: "Feedback could not be saved. Please retry." }, { status: 502 }); + } +} diff --git a/frontend/src/app/globals.css b/frontend/src/app/globals.css index 180dd3a..f2c7669 100644 --- a/frontend/src/app/globals.css +++ b/frontend/src/app/globals.css @@ -238,3 +238,14 @@ body { --body: var(--font-geist), Arial, sans-serif; --mono: var(--font-geist-mo @media (min-width: 901px) { .rail-label { position: absolute; width: 1px; height: 1px; margin: -1px; padding: 0; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; } } +.answer-feedback { margin-top: 28px; padding-top: 20px; border-top: 1px solid var(--border); font-size: 13px; } +.answer-feedback fieldset { min-width: 0; border: 0; margin: 0; padding: 0; } +.answer-feedback legend { margin-bottom: 12px; font-weight: 500; } +.feedback-choices { display: flex; gap: 8px; margin-bottom: 16px; } +.feedback-choices button { min-height: 36px; padding: 0 12px; border: 1px solid #d4d4d4; border-radius: 6px; background: white; } +.feedback-choices button[aria-pressed="true"] { border-color: #141414; background: #eee; } +.answer-feedback label { display: block; margin-bottom: 8px; } +.answer-feedback label span, .feedback-note { color: #626262; } +.answer-feedback textarea { width: 100%; padding: 10px 12px; border: 1px solid #d4d4d4; border-radius: 6px; resize: vertical; background: white; } +.feedback-note { font-size: 12px; } +.answer-feedback button:disabled { opacity: .6; cursor: default; } diff --git a/frontend/src/components/workspace.tsx b/frontend/src/components/workspace.tsx index 5adc215..1ad0522 100644 --- a/frontend/src/components/workspace.tsx +++ b/frontend/src/components/workspace.tsx @@ -19,6 +19,14 @@ type FinalEvent = { type Result = FinalEvent & { question: string; runId: number; genUiValid?: boolean }; type ServiceStatus = { api: string; presentation_status: string }; +type FeedbackState = { + choice: boolean | null; + comment: string; + state: "idle" | "saving" | "saved" | "error"; + message: string; +}; +const emptyFeedback: FeedbackState = { choice: null, comment: "", state: "idle", message: "" }; + function validSource(value: unknown): value is VerifiedSource { if (!value || typeof value !== "object") return false; const source = value as Record; @@ -102,6 +110,7 @@ async function* readEvents(response: Response) { } export default function Workspace() { + const [feedback, setFeedback] = useState(emptyFeedback); const [elapsed, setElapsed] = useState(0); const [question, setQuestion] = useState(""); const [busy, setBusy] = useState(false); @@ -129,6 +138,7 @@ export default function Workspace() { runCounter.current += 1; setQuestion(""); setResult(null); + setFeedback(emptyFeedback); setStages([]); setRetrievedSources([]); setError(""); @@ -162,6 +172,7 @@ export default function Workspace() { setFailedRun(null); setOpenUiMessage(""); setResult(null); + setFeedback(emptyFeedback); setStages([]); setRetrievedSources([]); setQuestion(submitted); @@ -228,6 +239,25 @@ export default function Workspace() { } } + async function submitFeedback(event: FormEvent) { + event.preventDefault(); + if (!result?.request_id || feedback.choice === null || feedback.state === "saving") return; + const runId = result.runId; + setFeedback((current) => ({ ...current, state: "saving", message: "" })); + try { + const response = await fetch("/api/feedback", { + method: "POST", + headers: { "content-type": "application/json" }, + body: JSON.stringify({ request_id: result.request_id, helpful: feedback.choice, comment: feedback.comment }), + signal: AbortSignal.timeout(10_000), + }); + if (!response.ok) throw new Error("Feedback could not be saved. Please retry."); + if (runCounter.current === runId) setFeedback((current) => ({ ...current, state: "saved", message: "Feedback saved for review." })); + } catch { + if (runCounter.current === runId) setFeedback((current) => ({ ...current, state: "error", message: "Feedback could not be saved. Please retry." })); + } + } + function stopRequest(event: MouseEvent) { // Cancellation can switch this same button back to submit during the click. event.preventDefault(); @@ -248,6 +278,7 @@ export default function Workspace() { runCounter.current += 1; setQuestion(""); setResult(null); + setFeedback(emptyFeedback); setStages([]); setRetrievedSources([]); setError(""); @@ -379,6 +410,33 @@ export default function Workspace() {

{result.status === "needs_clarification" ? "Refine your question with the missing system, version, or context and ask again." : "Try a narrower question or check that the relevant documents are in this knowledge base."}

)} + {result.request_id && ( +
+
+ Was this answer helpful? +
+ {[true, false].map((choice) => ( + + ))} +
+ {feedback.choice !== null && ( + <> + +