diff --git a/src/components/ops/doc-intelligence-drawer.tsx b/src/components/ops/doc-intelligence-drawer.tsx index 71923c4..0928aa2 100644 --- a/src/components/ops/doc-intelligence-drawer.tsx +++ b/src/components/ops/doc-intelligence-drawer.tsx @@ -95,11 +95,25 @@ export function DocIntelligenceBody({ documentId }: { documentId: string }) { icon={} title="No entities found" hint={ - <> - No entities are stored for this document. Extraction may not have run yet (enable - it with KB_INTELLIGENCE_ENABLED), or the document yielded none. Try - Re-extract. - + intel.data?.capability && !intel.data.capability.intelligence_enabled ? ( + <> + Intelligence extraction is disabled — enable it with{" "} + KB_INTELLIGENCE_ENABLED, or use Re-extract, which works + while disabled. + + ) : intel.data?.capability && + intel.data.capability.credential_configured === false ? ( + <> + Extraction is enabled but no API key resolves for the extraction endpoint, so + extraction fails silently. Add a key under Knowledge Base settings (or set{" "} + OPENROUTER_API_KEY), then Re-extract. + + ) : ( + <> + No entities are stored for this document — it may genuinely yield none. Try{" "} + Re-extract. + + ) } /> ) : ( diff --git a/src/components/ops/graph-lens-helpers.test.ts b/src/components/ops/graph-lens-helpers.test.ts index 06bfec4..2f180a5 100644 --- a/src/components/ops/graph-lens-helpers.test.ts +++ b/src/components/ops/graph-lens-helpers.test.ts @@ -1,43 +1,49 @@ -import { describe, it, expect } from "vitest"; -import { deriveGraphState, isSmallModel, fromIntelligence } from "./graph-lens-helpers"; -import type { KbCapability, KbDocumentIntelligence } from "@/lib/types"; +import { describe, expect, it } from "vitest"; -const on: KbCapability = { intelligence_enabled: true, extraction_model: "x" }; -const off: KbCapability = { intelligence_enabled: false, extraction_model: "x" }; +import type { KbCapability } from "@/lib/types"; -describe("deriveGraphState", () => { - it("loading before first data", () => - expect(deriveGraphState(undefined, undefined, true, false)).toBe("loading")); - it("disabled when capability is off", () => - expect(deriveGraphState(off, 0, false, true)).toBe("disabled")); - it("empty when enabled with 0 entities", () => - expect(deriveGraphState(on, 0, false, true)).toBe("empty")); - it("ready when entities present", () => - expect(deriveGraphState(on, 5, false, true)).toBe("ready")); +import { deriveGraphState } from "./graph-lens-helpers"; + +// Plan 097 (RantAIClaw): the console's honesty about WHY a graph is empty +// lives entirely in this pure function — these are the only tests pinning it. + +const cap = (over: Partial = {}): KbCapability => ({ + intelligence_enabled: true, + extraction_model: "openai/gpt-4.1-nano", + credential_configured: true, + ...over, }); -describe("isSmallModel", () => { - it("flags nano/mini, not large models", () => { - expect(isSmallModel("openai/gpt-4.1-nano")).toBe(true); - expect(isSmallModel("openai/gpt-4o")).toBe(false); - expect(isSmallModel(undefined)).toBe(false); +describe("deriveGraphState", () => { + it("loading before the first response", () => { + expect(deriveGraphState(undefined, undefined, true, false)).toBe("loading"); + }); + + it("disabled when intelligence is off", () => { + expect(deriveGraphState(cap({ intelligence_enabled: false }), 0, false, true)).toBe( + "disabled", + ); + }); + + it("no-credential when enabled but no key resolves", () => { + expect(deriveGraphState(cap({ credential_configured: false }), 0, false, true)).toBe( + "no-credential", + ); + }); + + it("empty when enabled with a key and zero entities", () => { + expect(deriveGraphState(cap(), 0, false, true)).toBe("empty"); + }); + + it("ready when entities exist", () => { + expect(deriveGraphState(cap(), 12, false, true)).toBe("ready"); }); -}); -describe("fromIntelligence", () => { - it("builds nodes/edges and computes degree from relations", () => { - const intel: KbDocumentIntelligence = { - entities: [ - { id: "a", name: "A", entity_type: "person", confidence: 1 }, - { id: "b", name: "B", entity_type: "person", confidence: 1 }, - ], - relations: [{ id: "r", source: "a", target: "b", relation_type: "knows", confidence: 1 }], - stats: {}, - }; - const g = fromIntelligence(intel); - expect(g.nodes.length).toBe(2); - expect(g.edges.length).toBe(1); - expect(g.nodes.find((n) => n.id === "a")!.degree).toBe(1); - expect(g.stats?.corpus_entities).toBe(2); + it("older gateways without the credential field fall through to empty, never no-credential", () => { + // credential_configured is optional in the wire type; undefined must not + // be treated as "missing credential". + expect(deriveGraphState(cap({ credential_configured: undefined }), 0, false, true)).toBe( + "empty", + ); }); }); diff --git a/src/components/ops/graph-lens-helpers.ts b/src/components/ops/graph-lens-helpers.ts index 4f9ddb1..31fba4e 100644 --- a/src/components/ops/graph-lens-helpers.ts +++ b/src/components/ops/graph-lens-helpers.ts @@ -6,7 +6,7 @@ import type { KbGraphNode, } from "@/lib/types"; -export type GraphState = "loading" | "disabled" | "empty" | "ready"; +export type GraphState = "loading" | "disabled" | "no-credential" | "empty" | "ready"; /** * Derive the honest render state from the capability signal + scope-wide entity @@ -20,6 +20,11 @@ export function deriveGraphState( ): GraphState { if (loading && !hasData) return "loading"; if (cap && !cap.intelligence_enabled) return "disabled"; + // Enabled but no key resolves: extraction fails per chunk and is + // swallowed server-side, so without this distinction the operator sees + // the same "empty" state as a genuinely entity-free corpus. + if (cap && cap.intelligence_enabled && cap.credential_configured === false) + return "no-credential"; return (corpusEntities ?? 0) === 0 ? "empty" : "ready"; } diff --git a/src/components/ops/graph-lens.tsx b/src/components/ops/graph-lens.tsx index 4aabfaf..0c6f02f 100644 --- a/src/components/ops/graph-lens.tsx +++ b/src/components/ops/graph-lens.tsx @@ -159,6 +159,21 @@ export function GraphLens({ scope, lockScope }: { scope: GraphScope; lockScope?: } /> + ) : graphState === "no-credential" ? ( + } + title="Extraction is on, but no credential resolves" + hint={ + <> + Intelligence extraction is enabled but no API key resolves for the extraction + endpoint, so every extraction attempt fails silently. Add a key under Knowledge + Base settings (or set OPENROUTER_API_KEY). + {data?.capability?.extraction_model && ( + + )} + + } + /> ) : graphState === "empty" ? ( } diff --git a/src/lib/types.ts b/src/lib/types.ts index 601e3b9..3697a0a 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -232,6 +232,10 @@ export interface KbGraphEdge { export interface KbCapability { intelligence_enabled: boolean; extraction_model: string; + /** Presence-only credential signal — the key itself never crosses the API. */ + credential_configured?: boolean; + graphrag_enabled?: boolean; + resolution?: string; } export interface KbGraph { nodes: KbGraphNode[];