From f7ae6353789f4410e2b059b39eb396eb2f31b598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Wed, 25 Mar 2026 17:57:22 +0100 Subject: [PATCH 1/2] fix: control plane UI fixes for recall and data view - Sanitize NaN cross-encoder scores to 0.0 in reranking pipeline (Pydantic serializes NaN as JSON null, breaking UI score display) - Add null-coalesce for score in search debug view to prevent crash - Switch data view text filter from debounced onChange to Enter key (avoids slow ILIKE queries on every keystroke for large banks) - Show loading spinner in search icon during filter requests - Preserve search/tag filters when clicking "Load more" --- .../hindsight_api/engine/search/reranking.py | 17 +++++-- .../src/components/data-view.tsx | 48 +++++++++---------- .../src/components/search-debug-view.tsx | 2 +- 3 files changed, 39 insertions(+), 28 deletions(-) diff --git a/hindsight-api-slim/hindsight_api/engine/search/reranking.py b/hindsight-api-slim/hindsight_api/engine/search/reranking.py index 0d948aa70..7d0c37cfe 100644 --- a/hindsight-api-slim/hindsight_api/engine/search/reranking.py +++ b/hindsight-api-slim/hindsight_api/engine/search/reranking.py @@ -153,6 +153,8 @@ async def rerank(self, query: str, candidates: list[MergedCandidate]) -> list[Sc # Normalize scores using sigmoid to [0, 1] range # Cross-encoder returns logits which can be negative + import math + import numpy as np def sigmoid(x): @@ -163,11 +165,20 @@ def sigmoid(x): # Create ScoredResult objects with cross-encoder scores scored_results = [] for candidate, raw_score, norm_score in zip(candidates, scores, normalized_scores): + # Sanitize NaN scores (cross-encoder can return NaN for certain inputs). + # NaN propagates through all downstream scoring and Pydantic serializes + # NaN as JSON null, which breaks clients expecting numeric values. + raw = float(raw_score) + norm = float(norm_score) + if math.isnan(raw): + raw = 0.0 + if math.isnan(norm): + norm = 0.0 scored_result = ScoredResult( candidate=candidate, - cross_encoder_score=float(raw_score), - cross_encoder_score_normalized=float(norm_score), - weight=float(norm_score), # Initial weight is just cross-encoder score + cross_encoder_score=raw, + cross_encoder_score_normalized=norm, + weight=norm, # Initial weight is just cross-encoder score ) scored_results.append(scored_result) diff --git a/hindsight-control-plane/src/components/data-view.tsx b/hindsight-control-plane/src/components/data-view.tsx index 40b8d2c82..6d9922e30 100644 --- a/hindsight-control-plane/src/components/data-view.tsx +++ b/hindsight-control-plane/src/components/data-view.tsx @@ -227,29 +227,19 @@ export function DataView({ factType }: DataViewProps) { // Reset to first page when filters change useEffect(() => { setCurrentPage(1); - }, [searchQuery, tagFilters]); - - // Debounce ref for text search - const searchDebounceRef = useRef | null>(null); + }, [tagFilters]); - // Trigger server-side reload when text filter changes (debounced 300ms) - useEffect(() => { - if (searchDebounceRef.current) { - clearTimeout(searchDebounceRef.current); + // Trigger text search on Enter key + const executeSearch = () => { + if (currentBank) { + setCurrentPage(1); + loadData( + undefined, + searchQuery || undefined, + tagFilters.length > 0 ? tagFilters : undefined + ); } - searchDebounceRef.current = setTimeout(() => { - if (currentBank) { - loadData( - undefined, - searchQuery || undefined, - tagFilters.length > 0 ? tagFilters : undefined - ); - } - }, 300); - return () => { - if (searchDebounceRef.current) clearTimeout(searchDebounceRef.current); - }; - }, [searchQuery]); + }; // Trigger server-side reload immediately when tag filters change useEffect(() => { @@ -292,12 +282,22 @@ export function DataView({ factType }: DataViewProps) {
{/* Text search */}
- + {loading ? ( + + ) : ( + + )} setSearchQuery(e.target.value)} - placeholder="Filter by text or context..." + onKeyDown={(e) => { + if (e.key === "Enter") { + e.preventDefault(); + executeSearch(); + } + }} + placeholder="Filter by text or context (press Enter)..." className="pl-8 h-9" />
@@ -356,7 +356,7 @@ export function DataView({ factType }: DataViewProps) { onClick={() => { const newLimit = Math.min(data.total_units, fetchLimit + 1000); setFetchLimit(newLimit); - loadData(newLimit); + loadData(newLimit, searchQuery || undefined, tagFilters.length > 0 ? tagFilters : undefined); }} className="ml-2 text-primary hover:underline" > diff --git a/hindsight-control-plane/src/components/search-debug-view.tsx b/hindsight-control-plane/src/components/search-debug-view.tsx index f278bc421..293e57fec 100644 --- a/hindsight-control-plane/src/components/search-debug-view.tsx +++ b/hindsight-control-plane/src/components/search-debug-view.tsx @@ -411,7 +411,7 @@ export function SearchDebugView() {
-
{score.toFixed(3)}
+
{(score ?? 0).toFixed(3)}
score
From c528d7e609ebc978352d823b5448905cada2a1e4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nicol=C3=B2=20Boschi?= Date: Wed, 25 Mar 2026 18:25:27 +0100 Subject: [PATCH 2/2] chore: sync generated files after rebase --- hindsight-control-plane/src/components/data-view.tsx | 12 ++++++------ .../references/developer/configuration.md | 9 ++++++++- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/hindsight-control-plane/src/components/data-view.tsx b/hindsight-control-plane/src/components/data-view.tsx index 6d9922e30..1e073c925 100644 --- a/hindsight-control-plane/src/components/data-view.tsx +++ b/hindsight-control-plane/src/components/data-view.tsx @@ -233,11 +233,7 @@ export function DataView({ factType }: DataViewProps) { const executeSearch = () => { if (currentBank) { setCurrentPage(1); - loadData( - undefined, - searchQuery || undefined, - tagFilters.length > 0 ? tagFilters : undefined - ); + loadData(undefined, searchQuery || undefined, tagFilters.length > 0 ? tagFilters : undefined); } }; @@ -356,7 +352,11 @@ export function DataView({ factType }: DataViewProps) { onClick={() => { const newLimit = Math.min(data.total_units, fetchLimit + 1000); setFetchLimit(newLimit); - loadData(newLimit, searchQuery || undefined, tagFilters.length > 0 ? tagFilters : undefined); + loadData( + newLimit, + searchQuery || undefined, + tagFilters.length > 0 ? tagFilters : undefined + ); }} className="ml-2 text-primary hover:underline" > diff --git a/skills/hindsight-docs/references/developer/configuration.md b/skills/hindsight-docs/references/developer/configuration.md index dd6ad06c4..20ada9e30 100644 --- a/skills/hindsight-docs/references/developer/configuration.md +++ b/skills/hindsight-docs/references/developer/configuration.md @@ -160,7 +160,7 @@ To switch between backends: | Variable | Description | Default | |----------|-------------|---------| -| `HINDSIGHT_API_LLM_PROVIDER` | Provider: `openai`, `openai-codex`, `claude-code`, `anthropic`, `gemini`, `groq`, `minimax`, `ollama`, `lmstudio`, `vertexai`, `bedrock`, `litellm` | `openai` | +| `HINDSIGHT_API_LLM_PROVIDER` | Provider: `openai`, `openai-codex`, `claude-code`, `anthropic`, `gemini`, `groq`, `minimax`, `ollama`, `lmstudio`, `vertexai`, `bedrock`, `litellm`, `none` | `openai` | | `HINDSIGHT_API_LLM_API_KEY` | API key for LLM provider | - | | `HINDSIGHT_API_LLM_MODEL` | Model name | `gpt-5-mini` | | `HINDSIGHT_API_LLM_BASE_URL` | Custom LLM endpoint | Provider default | @@ -250,6 +250,13 @@ export HINDSIGHT_API_LLM_MODEL=azure/gpt-4o export HINDSIGHT_API_LLM_PROVIDER=litellm export HINDSIGHT_API_LLM_API_KEY=your-together-api-key export HINDSIGHT_API_LLM_MODEL=together_ai/meta-llama/Llama-3-70b-chat-hf + +# No LLM (chunk storage + semantic search only, no API key needed) +export HINDSIGHT_API_LLM_PROVIDER=none +# Retain automatically uses chunks mode (no fact extraction) +# Recall works normally (semantic search, BM25, graph retrieval) +# Reflect returns HTTP 400 (requires an LLM) +# Consolidation/observations are disabled ``` :::tip OpenAI Codex, Claude Code & Vertex AI Setup