Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 19 additions & 5 deletions src/components/ops/doc-intelligence-drawer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,25 @@ export function DocIntelligenceBody({ documentId }: { documentId: string }) {
icon={<Network className="size-6" />}
title="No entities found"
hint={
<>
No entities are stored for this document. Extraction may not have run yet (enable
it with <code>KB_INTELLIGENCE_ENABLED</code>), or the document yielded none. Try
Re-extract.
</>
intel.data?.capability && !intel.data.capability.intelligence_enabled ? (
<>
Intelligence extraction is disabled — enable it with{" "}
<code>KB_INTELLIGENCE_ENABLED</code>, or use <em>Re-extract</em>, 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{" "}
<code>OPENROUTER_API_KEY</code>), then <em>Re-extract</em>.
</>
) : (
<>
No entities are stored for this document — it may genuinely yield none. Try{" "}
<em>Re-extract</em>.
</>
)
}
/>
) : (
Expand Down
76 changes: 41 additions & 35 deletions src/components/ops/graph-lens-helpers.test.ts
Original file line number Diff line number Diff line change
@@ -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> = {}): 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",
);
});
});
7 changes: 6 additions & 1 deletion src/components/ops/graph-lens-helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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";
}

Expand Down
15 changes: 15 additions & 0 deletions src/components/ops/graph-lens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,21 @@ export function GraphLens({ scope, lockScope }: { scope: GraphScope; lockScope?:
</>
}
/>
) : graphState === "no-credential" ? (
<EmptyState
icon={<Network className="size-6" />}
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 <code>OPENROUTER_API_KEY</code>).
{data?.capability?.extraction_model && (
<ModelNote model={data.capability.extraction_model} />
)}
</>
}
/>
) : graphState === "empty" ? (
<EmptyState
icon={<Network className="size-6" />}
Expand Down
4 changes: 4 additions & 0 deletions src/lib/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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[];
Expand Down
Loading