From 0045778dbbd2255f8ed35e7be22978a4d4497341 Mon Sep 17 00:00:00 2001 From: NuklearRabbit <145918611+NuklearRabbit@users.noreply.github.com> Date: Fri, 21 Aug 2026 17:22:17 +0200 Subject: [PATCH] M42: calibrate grounded RAGcore fallback --- PROJECT_STATE.md | 18 ++++++++++++++++++ backend/app/core/config.py | 4 +++- backend/tests/test_knowledge.py | 6 ++++-- 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/PROJECT_STATE.md b/PROJECT_STATE.md index e9e0411..d8f8a49 100644 --- a/PROJECT_STATE.md +++ b/PROJECT_STATE.md @@ -1,5 +1,23 @@ # Project state +## M42 — calibrate grounded RAGcore fallback (2026-08-21) + +- The first live five-minute acceptance run exposed a real calibration error in M41's + new fallback threshold: `/v1/answers` timed out, while `/v1/search` correctly returned + `damage-procedure.md` at rank one with fused score `0.0163934426`. RAGcore uses + reciprocal-rank fusion (roughly `1 / (60 + rank)`), so the normalized-looking `0.05` + threshold could never accept a legitimate result. +- Set the default minimum to `0.016`: this accepts the first two normal RRF ranks while + still rejecting missing scores and the existing low-score `0.01` abuse case. Damage + questions continue to require explicit damage evidence and out-of-domain questions + continue to return `insufficient`; no generated answer is trusted when retrieval is + absent or weak. +- Added a regression using RAGcore's observed rank-one fused score. Focused knowledge + tests **36 passed**; complete isolated PostgreSQL backend suite **270 passed**; Ruff and + mypy are clean and `git diff --check` passes. +- Exact next action: commit and push M42, deploy that exact archive over the healthy M41 + production deployment, then repeat the live five-minute demo and final health checks. + ## M41 — full review remediation and hardening (2026-08-21) - Closed all findings from the repository-wide review without expanding the locked PoC: diff --git a/backend/app/core/config.py b/backend/app/core/config.py index c9bc7df..7135be6 100644 --- a/backend/app/core/config.py +++ b/backend/app/core/config.py @@ -24,7 +24,9 @@ class Settings(BaseSettings): ragcore_space_id: str = "" ragcore_http_timeout_seconds: float = 5.0 # Search fallback is only labelled grounded above this explicit retrieval threshold. - ragcore_min_search_score: float = 0.05 + # RAGcore's fused score is reciprocal-rank based (top ranks are ~1/61), so this + # accepts only leading results while still rejecting absent and low-ranked evidence. + ragcore_min_search_score: float = 0.016 n8n_webhook_url: str = "http://n8n:5678/webhook/mobilityops-return" n8n_webhook_trigger_token: str = "replace-me-n8n-webhook-trigger-token" n8n_callback_token: str = "replace-me-n8n-callback-token" diff --git a/backend/tests/test_knowledge.py b/backend/tests/test_knowledge.py index 3ebd7c1..60f5246 100644 --- a/backend/tests/test_knowledge.py +++ b/backend/tests/test_knowledge.py @@ -711,7 +711,7 @@ def test_ragcore_search_fallback_prefers_damage_procedure(monkeypatch): assert any(source.document_id == "damage-procedure" for source in answer.sources) -def test_ragcore_search_fallback_accepts_relevant_damage_evidence_above_threshold(monkeypatch): +def test_ragcore_search_fallback_accepts_top_ranked_ragcore_damage_evidence(monkeypatch): provider = RAGcoreKnowledgeProvider() monkeypatch.setattr(provider._settings, "ragcore_space_id", "space-1") search = _search_body() @@ -725,7 +725,9 @@ def test_ragcore_search_fallback_accepts_relevant_damage_evidence_above_threshol "excerpt": "Record damage and keep the vehicle blocked.", }, "rank": 1, - "scores": {"fused": 0.5}, + # RAGcore uses reciprocal-rank fusion; a genuine rank-one result is about + # 1 / (60 + 1), not a normalized 0..1 relevance score. + "scores": {"fused": 0.01639344262295082}, } ) monkeypatch.setattr(