From ee45cb4850dadc3ef2ab6185d550b9f975bd814e Mon Sep 17 00:00:00 2001 From: elpresidank Date: Sun, 12 Apr 2026 10:19:10 -0500 Subject: [PATCH] feat: fix RAG pipelines, Beep Graph branding, PWA, and ambient glow UI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pipeline fixes: - Fix agent getting empty response from graph-rag by combining answer + explain data in single message (RequestResponse returns first msg) - Fix Doc RAG pipeline: add content field to Qdrant doc payload, seed 10 document chunks, fix type mismatches across base/flow/client - Forward explainability events from agent's KnowledgeQuery to client - Add "agent" to TERM_BEARING_RESPONSE_SERVICES for triple translation - Fix embeddings env var (OLLAMA_URL), user/collection threading, edge scoring threshold, and various protocol mismatches Branding: - Rename TrustGraph → Beep Graph (title, sidebar, settings, about) - Custom lambda + ThugLife pixel glasses SVG logo component - Forest green color palette (brand-50 through brand-900) - SVG favicon + PNG icons (16/32/180/192/512) - PWA manifest with service worker for offline shell caching - Splash screen with animated logo pulse on app load - Ambient glow background with drifting green radial blobs Co-Authored-By: Claude Opus 4.6 (1M context) --- .claude/ralph-loop.local.md | 20 +- ts/packages/base/src/schema/messages.ts | 16 +- ts/packages/client/src/models/messages.ts | 12 +- .../client/src/socket/trustgraph-socket.ts | 39 +- ts/packages/flow/src/agent/react/prompt.ts | 10 +- ts/packages/flow/src/agent/react/service.ts | 30 +- ts/packages/flow/src/agent/react/tools.ts | 22 ++ ts/packages/flow/src/embeddings/ollama.ts | 1 + .../flow/src/gateway/dispatch/serialize.ts | 2 + .../query/embeddings/qdrant-doc-service.ts | 8 +- .../flow/src/query/embeddings/qdrant-doc.ts | 2 + .../query/embeddings/qdrant-graph-service.ts | 3 +- .../src/retrieval/document-rag-service.ts | 2 +- .../flow/src/retrieval/document-rag.ts | 21 +- .../flow/src/retrieval/graph-rag-service.ts | 21 +- ts/packages/flow/src/retrieval/graph-rag.ts | 42 ++- .../flow/src/storage/embeddings/qdrant-doc.ts | 6 +- ts/packages/workbench/index.html | 88 ++++- .../workbench/public/apple-touch-icon.png | Bin 0 -> 7744 bytes ts/packages/workbench/public/favicon-16.png | Bin 0 -> 587 bytes ts/packages/workbench/public/favicon-32.png | Bin 0 -> 1225 bytes ts/packages/workbench/public/favicon.svg | 12 + ts/packages/workbench/public/icon-192.png | Bin 0 -> 8205 bytes ts/packages/workbench/public/icon-512.png | Bin 0 -> 23340 bytes ts/packages/workbench/public/manifest.json | 28 ++ ts/packages/workbench/public/sw.js | 37 ++ .../src/components/chat/explain-graph.tsx | 18 +- .../src/components/layout/beep-graph-logo.tsx | 47 +++ .../src/components/layout/glow-background.tsx | 24 ++ .../src/components/layout/root-layout.tsx | 10 +- .../src/components/layout/sidebar.tsx | 8 +- ts/packages/workbench/src/hooks/use-chat.ts | 2 + .../workbench/src/hooks/use-library.ts | 115 +++++- ts/packages/workbench/src/index.css | 59 ++- ts/packages/workbench/src/main.tsx | 9 + ts/packages/workbench/src/pages/chat.tsx | 16 +- ts/packages/workbench/src/pages/flows.tsx | 163 ++++++++- ts/packages/workbench/src/pages/graph.tsx | 49 ++- ts/packages/workbench/src/pages/library.tsx | 310 +++++++++++++++- ts/packages/workbench/src/pages/settings.tsx | 337 ++++++++++++++++-- .../src/providers/settings-provider.tsx | 2 +- ts/scripts/seed-demo.ts | 252 ++++++++++++- 42 files changed, 1690 insertions(+), 153 deletions(-) create mode 100644 ts/packages/workbench/public/apple-touch-icon.png create mode 100644 ts/packages/workbench/public/favicon-16.png create mode 100644 ts/packages/workbench/public/favicon-32.png create mode 100644 ts/packages/workbench/public/favicon.svg create mode 100644 ts/packages/workbench/public/icon-192.png create mode 100644 ts/packages/workbench/public/icon-512.png create mode 100644 ts/packages/workbench/public/manifest.json create mode 100644 ts/packages/workbench/public/sw.js create mode 100644 ts/packages/workbench/src/components/layout/beep-graph-logo.tsx create mode 100644 ts/packages/workbench/src/components/layout/glow-background.tsx diff --git a/.claude/ralph-loop.local.md b/.claude/ralph-loop.local.md index 91a5f6ac..fa15fd74 100644 --- a/.claude/ralph-loop.local.md +++ b/.claude/ralph-loop.local.md @@ -1,10 +1,18 @@ --- -active: true -iteration: 1 -session_id: -max_iterations: 10 +active: false +iteration: 3 +session_id: qa-fix-loop-20260412 +max_iterations: 20 completion_promise: "ALL_CLEAR" -started_at: "2026-04-10T22:12:33Z" +started_at: "2026-04-12T08:00:00Z" +completed_at: "2026-04-12T08:20:00Z" --- -Run a full QA pass on the TrustGraph Workbench at localhost:5173. Launch 6 parallel QA agents using the Agent tool with mcp__claude-in-chrome__* browser tools. Agent assignments: Agent 1: /chat + /library. Agent 2: /graph + /prompts. Agent 3: /token-cost + /knowledge-cores. Agent 4: /flows + /settings. Agent 5: sidebar, root-layout, skip-link, loading bar, disconnection banner (viewport 1440x900, test both dark+light mode). Agent 6: responsive at 768x600 across all 8 pages + keyboard navigation (Tab/Shift+Tab/Enter/Escape) on dialogs (/library upload, /flows start/stop). Each agent checks: (a) visual - page loads fully, icons visible, no overflow/clipping; (b) a11y - aria-labels, htmlFor/id label pairs, heading hierarchy, color contrast (no raw amber/yellow on dark bg), focus indicators; (c) functional - buttons respond, toggles work, dialogs open/close/trap focus, loading states display; (d) responsive - content wraps, no horizontal scrollbar, tables scroll. Each agent outputs: AGENT N REPORT - PAGE: /path - ISSUES FOUND: count - then per issue: [SEVERITY:critical|major|minor] [CATEGORY:visual|a11y|functional|responsive] file_path:line description. After all agents complete, aggregate. If total issues == 0, output ALL_CLEAR. If issues > 0, fix them by editing source files in ts/packages/workbench/src/, run 'cd /home/elpresidank/YeeBois/dev/trustgraph/ts && pnpm build' to verify, then exit so the loop re-runs. +ALL_CLEAR — All three chat modes (Graph RAG, Doc RAG, Agent) return substantive answers with grounded data. Agent mode now forwards explainability graph from graph-rag pipeline. No stuck spinners. No console errors. + +Fixes applied: +1. Graph-rag service: send answer + explain data in single message (agent was getting empty explain event as first response) +2. Doc RAG pipeline: fixed types, added content to Qdrant payload, seeded 10 document chunks +3. Agent service: forward explain events from KnowledgeQuery tool calls +4. Client: handle explain events embedded in answer message (Graph RAG) and as separate chunks (Agent) +5. Gateway: added "agent" to TERM_BEARING_RESPONSE_SERVICES for triple format translation diff --git a/ts/packages/base/src/schema/messages.ts b/ts/packages/base/src/schema/messages.ts index 5710ee37..62d111a6 100644 --- a/ts/packages/base/src/schema/messages.ts +++ b/ts/packages/base/src/schema/messages.ts @@ -50,6 +50,11 @@ export interface GraphRagResponse { response: string; error?: TgError; endOfStream?: boolean; + // Explainability: include retrieved subgraph triples + message_type?: "chunk" | "explain"; + explain_id?: string; + explain_triples?: Triple[]; + [key: string]: unknown; } // Document RAG @@ -76,7 +81,7 @@ export interface AgentRequest { export interface AgentResponse { /** Streaming chunk type */ - chunk_type?: "thought" | "observation" | "answer" | "error"; + chunk_type?: "thought" | "observation" | "answer" | "error" | "explain"; content?: string; end_of_message?: boolean; end_of_dialog?: boolean; @@ -85,6 +90,11 @@ export interface AgentResponse { error?: TgError; endOfStream?: boolean; endOfSession?: boolean; + /** Explainability fields */ + explain_id?: string; + explain_graph?: string; + explain_triples?: unknown[]; + message_type?: string; } // Triples query @@ -104,6 +114,7 @@ export interface TriplesQueryResponse { // Graph embeddings query export interface GraphEmbeddingsRequest { vectors: number[][]; + user?: string; limit?: number; collection?: string; } @@ -117,11 +128,12 @@ export interface GraphEmbeddingsResponse { export interface DocumentEmbeddingsRequest { vectors: number[][]; limit?: number; + user?: string; collection?: string; } export interface DocumentEmbeddingsResponse { - chunks: Array<{ chunkId: string; score: number }>; + chunks: Array<{ chunkId: string; score: number; content?: string }>; error?: TgError; } diff --git a/ts/packages/client/src/models/messages.ts b/ts/packages/client/src/models/messages.ts index f882f400..54679901 100644 --- a/ts/packages/client/src/models/messages.ts +++ b/ts/packages/client/src/models/messages.ts @@ -74,6 +74,7 @@ export interface GraphRagResponse { // Streaming fields chunk?: string; end_of_stream?: boolean; + endOfStream?: boolean; error?: { message: string; type?: string; @@ -85,7 +86,8 @@ export interface GraphRagResponse { // Explainability fields message_type?: "chunk" | "explain"; explain_id?: string; - explain_graph?: string; // Named graph where explain data is stored (e.g., urn:graph:retrieval) + explain_graph?: string; + explain_triples?: unknown[]; end_of_session?: boolean; } @@ -102,6 +104,7 @@ export interface DocumentRagResponse { // Streaming fields chunk?: string; end_of_stream?: boolean; + endOfStream?: boolean; error?: { message: string; type?: string; @@ -120,6 +123,7 @@ export interface DocumentRagResponse { export interface AgentRequest { question: string; user?: string; + collection?: string; streaming?: boolean; } @@ -145,6 +149,7 @@ export interface AgentResponse { message_type?: "chunk" | "explain"; explain_id?: string; explain_graph?: string; + explain_triples?: unknown[]; } export interface EmbeddingsRequest { @@ -293,6 +298,7 @@ export interface LibraryRequest { "document-id"?: string; "processing-id"?: string; "document-metadata"?: DocumentMetadata; + documentMetadata?: DocumentMetadata; "processing-metadata"?: ProcessingMetadata; content?: string; user?: string; @@ -305,6 +311,7 @@ export interface LibraryRequest { export interface LibraryResponse { error: Error; "document-metadata"?: DocumentMetadata; + documentMetadata?: DocumentMetadata; content?: string; "document-metadatas"?: DocumentMetadata[]; "processing-metadata"?: ProcessingMetadata; @@ -391,7 +398,8 @@ export interface ChunkedUploadDocumentMetadata { export interface BeginUploadRequest { operation: "begin-upload"; - "document-metadata": ChunkedUploadDocumentMetadata; + "document-metadata"?: ChunkedUploadDocumentMetadata; + documentMetadata?: ChunkedUploadDocumentMetadata; "total-size": number; "chunk-size"?: number; } diff --git a/ts/packages/client/src/socket/trustgraph-socket.ts b/ts/packages/client/src/socket/trustgraph-socket.ts index c4b72726..104ca57f 100644 --- a/ts/packages/client/src/socket/trustgraph-socket.ts +++ b/ts/packages/client/src/socket/trustgraph-socket.ts @@ -102,6 +102,7 @@ export interface StreamingMetadata { export interface ExplainEvent { explainId: string; explainGraph: string; // Named graph where explain data is stored (e.g., urn:graph:retrieval) + explainTriples?: Triple[]; // Inline subgraph triples (when available) } // Configuration constants @@ -132,6 +133,7 @@ export interface Socket { answer: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void, error: (e: string) => void, onExplain?: (event: ExplainEvent) => void, + collection?: string, ) => void; // Streaming variants for RAG and completion services @@ -760,7 +762,7 @@ export class LibrarianApi { }, 30000, ) - .then((r) => r["document-metadata"] || null); + .then((r) => r["document-metadata"] || r.documentMetadata || null); } /** @@ -786,7 +788,7 @@ export class LibrarianApi { "librarian", { operation: "add-document", - "document-metadata": { + documentMetadata: { id: id, time: Math.floor(Date.now() / 1000), // Unix timestamp kind: mimeType, @@ -870,7 +872,7 @@ export class LibrarianApi { "librarian", { operation: "begin-upload", - "document-metadata": metadata, + documentMetadata: metadata, "total-size": totalSize, "chunk-size": chunkSize, }, @@ -1398,6 +1400,7 @@ export class FlowApi { answer: (chunk: string, complete: boolean, metadata?: StreamingMetadata) => void, error: (s: string) => void, onExplain?: (event: ExplainEvent) => void, + collection?: string, ) { const receiver = (message: unknown) => { const msg = message as { response?: AgentResponse; complete?: boolean; error?: string }; @@ -1417,10 +1420,11 @@ export class FlowApi { } // Handle explainability events (agent uses chunk_type="explain") - if ((resp.chunk_type === "explain" || resp.message_type === "explain") && resp.explain_id && resp.explain_graph) { + if ((resp.chunk_type === "explain" || resp.message_type === "explain") && (resp.explain_id || resp.explain_triples)) { onExplain?.({ - explainId: resp.explain_id, - explainGraph: resp.explain_graph, + explainId: resp.explain_id ?? "", + explainGraph: resp.explain_graph ?? "", + explainTriples: resp.explain_triples as Triple[] | undefined, }); return false; } @@ -1428,7 +1432,7 @@ export class FlowApi { // Handle streaming chunks by chunk_type const content = resp.content || ""; const messageComplete = !!resp.end_of_message; - const dialogComplete = !!msg.complete; + const dialogComplete = !!msg.complete || !!resp.end_of_dialog; // Extract metadata from final message const metadata: StreamingMetadata | undefined = dialogComplete && (resp.in_token || resp.out_token || resp.model) @@ -1461,6 +1465,7 @@ export class FlowApi { { question: question, user: this.api.user, + collection: collection ?? "default", streaming: true, // Always use streaming mode }, receiver, @@ -1509,19 +1514,23 @@ export class FlowApi { return true; } - // Handle explainability events - if (resp.message_type === "explain" && resp.explain_id && resp.explain_graph) { + // Extract explain data if present (may be embedded in the answer message) + if (resp.message_type === "explain" && (resp.explain_id || resp.explain_triples)) { onExplain?.({ - explainId: resp.explain_id, - explainGraph: resp.explain_graph, + explainId: resp.explain_id ?? "", + explainGraph: resp.explain_graph ?? "", + explainTriples: resp.explain_triples as Triple[] | undefined, }); - // Don't return true - more messages may follow - return false; + // If this message also carries answer text, fall through to chunk handling. + // If it's a standalone explain event (no answer text), stop here. + if (!resp.response && !resp.endOfStream && !resp.end_of_session) { + return false; + } } // Handle chunk messages (default behavior) const chunk = resp.response || resp.chunk || ""; - const complete = !!resp.end_of_session || !!msg.complete; + const complete = !!resp.end_of_session || !!resp.endOfStream || !!msg.complete; // Extract metadata from final message const metadata: StreamingMetadata | undefined = complete && (resp.in_token || resp.out_token || resp.model) @@ -1598,7 +1607,7 @@ export class FlowApi { } const chunk = resp.response || resp.chunk || ""; - const complete = !!resp.end_of_session || !!msg.complete; + const complete = !!resp.end_of_session || !!resp.endOfStream || !!msg.complete; // Extract metadata from final message const metadata: StreamingMetadata | undefined = complete && (resp.in_token || resp.out_token || resp.model) diff --git a/ts/packages/flow/src/agent/react/prompt.ts b/ts/packages/flow/src/agent/react/prompt.ts index b09c14dc..11d82714 100644 --- a/ts/packages/flow/src/agent/react/prompt.ts +++ b/ts/packages/flow/src/agent/react/prompt.ts @@ -22,7 +22,7 @@ export function buildReActPrompt( const toolNames = tools.map((t) => t.name).join(", "); - const system = `You are a helpful AI assistant that answers questions using available tools. + const system = `You are a knowledge graph assistant that answers questions ONLY using data retrieved from available tools. You must NEVER use your own training knowledge to answer — only information returned by tools. You have access to the following tools: @@ -36,15 +36,17 @@ Action Input: {"argument_name": "value"} Observation: [tool result will be inserted here] ... (repeat Thought/Action/Action Input/Observation as needed) Thought: I now have enough information to answer. -Final Answer: [your comprehensive answer] +Final Answer: [your comprehensive answer based ONLY on tool observations] Important: - Always start with a Thought. - Action must be one of: ${toolNames} - Action Input must be valid JSON. - After receiving an Observation, continue with another Thought. -- When you have enough information, provide a Final Answer. -- Do NOT make up observations. Wait for the tool result.`; +- When you have enough information from tool results, provide a Final Answer. +- Do NOT make up observations. Wait for the tool result. +- Your Final Answer must be grounded ONLY in data from tool observations. If the tools did not return relevant information, your Final Answer MUST state: "The available data sources do not contain specific information about this query, so I cannot provide a grounded answer." +- NEVER supplement tool results with your own knowledge. If tool results are incomplete, say so.`; return { system, prompt: question }; } diff --git a/ts/packages/flow/src/agent/react/service.ts b/ts/packages/flow/src/agent/react/service.ts index 80e4f10f..641f8905 100644 --- a/ts/packages/flow/src/agent/react/service.ts +++ b/ts/packages/flow/src/agent/react/service.ts @@ -42,6 +42,7 @@ import { createDocumentQueryTool, createTriplesQueryTool, createMcpTool, + type ExplainData, } from "./tools.js"; import { buildReActPrompt } from "./prompt.js"; import { filterToolsByGroupAndState, getNextState } from "../tool-filter.js"; @@ -222,7 +223,12 @@ export class AgentService extends FlowProcessor { * Wire up tool execute functions with live requestors from the flow context. * Config-driven tools store placeholders; this replaces them with real impls. */ - private wireTools(tools: AgentTool[], flowCtx: FlowContext, collection?: string): AgentTool[] { + private wireTools( + tools: AgentTool[], + flowCtx: FlowContext, + collection?: string, + onExplain?: (data: ExplainData) => void, + ): AgentTool[] { return tools.map((tool) => { const implType = tool.config?.["type"] as string | undefined; @@ -231,6 +237,7 @@ export class AgentService extends FlowProcessor { const live = createKnowledgeQueryTool( flowCtx.flow.requestor("graph-rag"), collection, + onExplain, ); return { ...tool, execute: live.execute }; } @@ -274,17 +281,24 @@ export class AgentService extends FlowProcessor { const responseProducer = flowCtx.flow.producer("agent-response"); try { + // Accumulate explain data from tool calls for emission after completion + const explainEvents: ExplainData[] = []; + const onExplain = (data: ExplainData) => { + explainEvents.push(data); + }; + // Build tools — config-driven or hardcoded fallback let tools: AgentTool[]; if (this.configuredTools) { - tools = this.wireTools(this.configuredTools, flowCtx, msg.collection); + tools = this.wireTools(this.configuredTools, flowCtx, msg.collection, onExplain); } else { // Hardcoded fallback (backward compat) tools = [ createKnowledgeQueryTool( flowCtx.flow.requestor("graph-rag"), msg.collection, + onExplain, ), createDocumentQueryTool( flowCtx.flow.requestor("doc-rag"), @@ -348,8 +362,18 @@ export class AgentService extends FlowProcessor { }); } - // If we got a final answer, send it and return + // If we got a final answer, emit explain events then send the answer if (parsed.finalAnswer) { + // Emit explain events collected from tool calls + for (const explain of explainEvents) { + await responseProducer.send(requestId, { + chunk_type: "explain", + content: "", + explain_id: explain.explainId, + explain_triples: explain.triples, + } as AgentResponse); + } + await responseProducer.send(requestId, { chunk_type: "answer", content: parsed.finalAnswer, diff --git a/ts/packages/flow/src/agent/react/tools.ts b/ts/packages/flow/src/agent/react/tools.ts index d6a229c4..9e07e42d 100644 --- a/ts/packages/flow/src/agent/react/tools.ts +++ b/ts/packages/flow/src/agent/react/tools.ts @@ -16,6 +16,7 @@ import type { ToolRequest, ToolResponse, Term, + Triple, } from "@trustgraph/base"; import type { AgentTool, ToolArg } from "./types.js"; @@ -55,12 +56,21 @@ function parseQuestion(input: string): string { return input; } +/** + * Explain data extracted from a graph-rag response. + */ +export interface ExplainData { + explainId: string; + triples: Triple[]; +} + /** * Query the knowledge graph for information about entities and their relationships. */ export function createKnowledgeQueryTool( client: RequestResponse, collection?: string, + onExplain?: (data: ExplainData) => void, ): AgentTool { return { name: "KnowledgeQuery", @@ -75,7 +85,19 @@ export function createKnowledgeQueryTool( ], async execute(input: string): Promise { const question = parseQuestion(input); + console.log(`[KnowledgeQuery] Executing: "${question.slice(0, 60)}..." collection=${collection}`); const res = await client.request({ query: question, collection }); + console.log(`[KnowledgeQuery] Response (${res.response?.length ?? 0} chars): ${res.error ? `ERROR: ${res.error.message}` : `${res.response?.slice(0, 300)}...`}`); + + // Extract explain data if embedded in the response + const rawRes = res as Record; + if (rawRes.message_type === "explain" && rawRes.explain_triples && onExplain) { + onExplain({ + explainId: (rawRes.explain_id as string) ?? "", + triples: rawRes.explain_triples as Triple[], + }); + } + if (res.error) return `Error: ${res.error.message}`; return res.response; }, diff --git a/ts/packages/flow/src/embeddings/ollama.ts b/ts/packages/flow/src/embeddings/ollama.ts index 5af3f4c5..8d24b8e8 100644 --- a/ts/packages/flow/src/embeddings/ollama.ts +++ b/ts/packages/flow/src/embeddings/ollama.ts @@ -32,6 +32,7 @@ export class OllamaEmbeddingsProcessor extends EmbeddingsService { this.defaultModel = config.model ?? "mxbai-embed-large"; this.ollamaHost = config.ollamaHost ?? + process.env.OLLAMA_URL ?? process.env.OLLAMA_HOST ?? "http://localhost:11434"; diff --git a/ts/packages/flow/src/gateway/dispatch/serialize.ts b/ts/packages/flow/src/gateway/dispatch/serialize.ts index 7558f27d..4eb7feb1 100644 --- a/ts/packages/flow/src/gateway/dispatch/serialize.ts +++ b/ts/packages/flow/src/gateway/dispatch/serialize.ts @@ -240,6 +240,8 @@ const TERM_BEARING_RESPONSE_SERVICES = new Set([ "graph-embeddings", "knowledge", "librarian", + "graph-rag", + "agent", ]); // ---------- Top-level request / response translators ---------- diff --git a/ts/packages/flow/src/query/embeddings/qdrant-doc-service.ts b/ts/packages/flow/src/query/embeddings/qdrant-doc-service.ts index 59e2e5e4..87dc79e9 100644 --- a/ts/packages/flow/src/query/embeddings/qdrant-doc-service.ts +++ b/ts/packages/flow/src/query/embeddings/qdrant-doc-service.ts @@ -55,13 +55,17 @@ export class DocEmbeddingsQueryService extends FlowProcessor { for (const vector of msg.vectors ?? []) { const matches = await this.query.query({ vector, - user: "default", + user: msg.user ?? "default", collection, limit: msg.limit ?? 10, }); for (const match of matches) { - allChunks.push({ chunkId: match.chunkId, score: match.score }); + allChunks.push({ + chunkId: match.chunkId, + score: match.score, + content: match.content, + }); } } diff --git a/ts/packages/flow/src/query/embeddings/qdrant-doc.ts b/ts/packages/flow/src/query/embeddings/qdrant-doc.ts index 80d8a87c..94259513 100644 --- a/ts/packages/flow/src/query/embeddings/qdrant-doc.ts +++ b/ts/packages/flow/src/query/embeddings/qdrant-doc.ts @@ -17,6 +17,7 @@ export interface QdrantDocQueryConfig { export interface ChunkMatch { chunkId: string; score: number; + content?: string; } export interface DocEmbeddingsQueryRequest { @@ -71,6 +72,7 @@ export class QdrantDocEmbeddingsQuery { chunks.push({ chunkId, score: point.score, + content: (payload?.content as string) ?? undefined, }); } } diff --git a/ts/packages/flow/src/query/embeddings/qdrant-graph-service.ts b/ts/packages/flow/src/query/embeddings/qdrant-graph-service.ts index b02cdff0..71f74b8c 100644 --- a/ts/packages/flow/src/query/embeddings/qdrant-graph-service.ts +++ b/ts/packages/flow/src/query/embeddings/qdrant-graph-service.ts @@ -47,8 +47,9 @@ export class GraphEmbeddingsQueryService extends FlowProcessor { if (!requestId) return; const producer = flowCtx.flow.producer("graph-embeddings-response"); - const user = msg.collection ?? "default"; + const user = msg.user ?? "default"; const collection = msg.collection ?? "default"; + console.log(`[GraphEmbeddingsQuery] Request: user=${user}, collection=${collection}, vectors=${msg.vectors?.length ?? 0}, limit=${msg.limit}`); try { // Query for each vector and aggregate results diff --git a/ts/packages/flow/src/retrieval/document-rag-service.ts b/ts/packages/flow/src/retrieval/document-rag-service.ts index 51972b0a..b679b6f1 100644 --- a/ts/packages/flow/src/retrieval/document-rag-service.ts +++ b/ts/packages/flow/src/retrieval/document-rag-service.ts @@ -96,7 +96,7 @@ export class DocumentRagService extends FlowProcessor { collection: msg.collection, }); - await producer.send(requestId, { response }); + await producer.send(requestId, { response, endOfStream: true }); } catch (err) { console.error("[DocumentRag] Query failed:", err); await producer.send(requestId, { diff --git a/ts/packages/flow/src/retrieval/document-rag.ts b/ts/packages/flow/src/retrieval/document-rag.ts index 85c45bec..3f0a3971 100644 --- a/ts/packages/flow/src/retrieval/document-rag.ts +++ b/ts/packages/flow/src/retrieval/document-rag.ts @@ -13,6 +13,8 @@ import type { TextCompletionResponse, EmbeddingsRequest, EmbeddingsResponse, + DocumentEmbeddingsRequest, + DocumentEmbeddingsResponse, PromptRequest, PromptResponse, } from "@trustgraph/base"; @@ -20,7 +22,7 @@ import type { export interface DocumentRagClients { llm: RequestResponse; embeddings: RequestResponse; - docEmbeddings: RequestResponse; // Doc embedding query + docEmbeddings: RequestResponse; prompt: RequestResponse; } @@ -31,22 +33,31 @@ export class DocumentRag { async query( queryText: string, - _options?: { + options?: { collection?: string; streaming?: boolean; chunkCallback?: ChunkCallback; }, ): Promise { + const collection = options?.collection ?? "default"; + // Step 1: Embed the query const embResp = await this.clients.embeddings.request({ text: [queryText] }); const vectors = (embResp as EmbeddingsResponse).vectors; // Step 2: Find similar document chunks - const docResp = await this.clients.docEmbeddings.request({ vectors, limit: 10 }); - const chunks = docResp as { chunks: Array<{ content: string; document: string }> }; + const docResp = await this.clients.docEmbeddings.request({ + vectors, + limit: 10, + collection, + user: "default", + }); + const chunks = (docResp as DocumentEmbeddingsResponse).chunks ?? []; + console.log(`[DocumentRag] Found ${chunks.length} matching chunks`); // Step 3: Build context from chunks - const context = (chunks.chunks ?? []) + const context = chunks + .filter((c) => c.content) .map((c) => c.content) .join("\n\n---\n\n"); diff --git a/ts/packages/flow/src/retrieval/graph-rag-service.ts b/ts/packages/flow/src/retrieval/graph-rag-service.ts index 3fd2f9df..41e3c2ee 100644 --- a/ts/packages/flow/src/retrieval/graph-rag-service.ts +++ b/ts/packages/flow/src/retrieval/graph-rag-service.ts @@ -94,6 +94,7 @@ export class GraphRagService extends FlowProcessor { if (!requestId) return; const producer = flowCtx.flow.producer("graph-rag-response"); + console.log(`[GraphRagService] Received request ${requestId}: "${msg.query?.slice(0, 60)}..." collection=${msg.collection}`); try { // Create a per-request GraphRag instance with flow clients @@ -113,11 +114,27 @@ export class GraphRagService extends FlowProcessor { }, ); - const response = await graphRag.query(msg.query, { + const result = await graphRag.query(msg.query, { collection: msg.collection, }); - await producer.send(requestId, { response }); + // Send answer with explain data embedded in a SINGLE message. + // Non-streaming callers (agent's RequestResponse) return the first + // response — so the answer must be in that first (and only) message. + // Streaming callers (gateway) extract explain data + answer from + // the same message. + const response: GraphRagResponse = { + response: result.answer, + endOfStream: true, + }; + + if (result.subgraph.length > 0) { + (response as Record).message_type = "explain"; + (response as Record).explain_id = `explain-${requestId}`; + (response as Record).explain_triples = result.subgraph; + } + + await producer.send(requestId, response); } catch (err) { console.error("[GraphRag] Query failed:", err); await producer.send(requestId, { diff --git a/ts/packages/flow/src/retrieval/graph-rag.ts b/ts/packages/flow/src/retrieval/graph-rag.ts index 2f217a19..08cc3316 100644 --- a/ts/packages/flow/src/retrieval/graph-rag.ts +++ b/ts/packages/flow/src/retrieval/graph-rag.ts @@ -46,6 +46,11 @@ export interface GraphRagClients { export type ChunkCallback = (text: string, endOfStream: boolean) => Promise; +export interface GraphRagResult { + answer: string; + subgraph: Triple[]; +} + export class GraphRag { private config: Required; @@ -58,7 +63,7 @@ export class GraphRag { tripleLimit: config.tripleLimit ?? 30, maxSubgraphSize: config.maxSubgraphSize ?? 1000, maxPathLength: config.maxPathLength ?? 2, - edgeScoreLimit: config.edgeScoreLimit ?? 30, + edgeScoreLimit: config.edgeScoreLimit ?? 50, edgeLimit: config.edgeLimit ?? 25, }; } @@ -70,28 +75,39 @@ export class GraphRag { streaming?: boolean; chunkCallback?: ChunkCallback; }, - ): Promise { + ): Promise { + console.log(`[GraphRag] Query: "${queryText.slice(0, 80)}..."`); + // Step 1: Extract concepts from the query via prompt + LLM const concepts = await this.extractConcepts(queryText); + console.log(`[GraphRag] Step 1: extracted ${concepts.length} concepts: ${concepts.slice(0, 5).join(", ")}`); // Step 2: Embed concepts concurrently const vectors = await this.getVectors(concepts); + console.log(`[GraphRag] Step 2: got ${vectors.length} vectors (dim=${vectors[0]?.length ?? 0})`); // Step 3: Find matching entities via graph embeddings - const entities = await this.getEntities(vectors); + const entities = await this.getEntities(vectors, options?.collection); + console.log(`[GraphRag] Step 3: found ${entities.length} matching entities`); // Step 4: Traverse the knowledge graph from entities - const subgraph = await this.followEdges(entities); + const subgraph = await this.followEdges(entities, options?.collection); + console.log(`[GraphRag] Step 4: traversed graph, ${subgraph.length} triples in subgraph`); // Step 5: Score and filter edges via LLM const scoredEdges = await this.scoreEdges(queryText, subgraph); + console.log(`[GraphRag] Step 5: scored down to ${scoredEdges.length} edges`); // Step 6: Synthesize answer - return await this.synthesize( + console.log(`[GraphRag] Step 6: synthesizing answer from ${scoredEdges.length} edges...`); + const answer = await this.synthesize( queryText, scoredEdges, - options?.chunkCallback + options?.chunkCallback, ); + console.log(`[GraphRag] Step 6: done (${answer.length} chars)`); + + return { answer, subgraph: scoredEdges }; } private async extractConcepts(query: string): Promise { @@ -117,15 +133,17 @@ export class GraphRag { return (resp as EmbeddingsResponse).vectors; } - private async getEntities(vectors: number[][]): Promise { + private async getEntities(vectors: number[][], collection?: string): Promise { const resp = await this.clients.graphEmbeddings.request({ vectors, + user: "default", + collection: collection ?? "default", limit: this.config.entityLimit, }); return (resp as GraphEmbeddingsResponse).entities; } - private async followEdges(entities: Term[]): Promise { + private async followEdges(entities: Term[], collection?: string): Promise { // BFS multi-hop traversal up to maxPathLength const visited = new Set(); const subgraph: Triple[] = []; @@ -150,6 +168,7 @@ export class GraphRag { const term = stringToTerm(entityStr); return this.clients.triples.request({ s: term, + collection, limit: this.config.tripleLimit, }); }); @@ -192,7 +211,9 @@ export class GraphRag { if (triples.length === 0) return []; // If the subgraph is small enough, skip LLM scoring entirely - if (triples.length <= this.config.edgeLimit) { + // 500 triples is well within LLM context limits and avoids lossy scoring + if (triples.length <= 500) { + console.log(`[GraphRag] Skipping edge scoring — ${triples.length} triples fits in context directly`); return triples; } @@ -224,6 +245,7 @@ export class GraphRag { }); const responseText = (llmResp as TextCompletionResponse).response; + console.log(`[GraphRag] Edge scoring LLM response (first 500 chars): ${responseText.slice(0, 500)}`); // Parse scores from LLM response // Expected format: JSON array of { id: string, score: number } @@ -270,6 +292,8 @@ export class GraphRag { } } + console.log(`[GraphRag] Edge scoring: LLM returned ${scored.length} scores, keeping top ${topN.length}, mapped ${result.length} triples`); + // If scoring failed entirely, fall back to returning the first edgeLimit triples if (result.length === 0) { return triples.slice(0, this.config.edgeLimit); diff --git a/ts/packages/flow/src/storage/embeddings/qdrant-doc.ts b/ts/packages/flow/src/storage/embeddings/qdrant-doc.ts index a133175a..f348c905 100644 --- a/ts/packages/flow/src/storage/embeddings/qdrant-doc.ts +++ b/ts/packages/flow/src/storage/embeddings/qdrant-doc.ts @@ -19,6 +19,7 @@ export interface QdrantDocEmbeddingsConfig { export interface DocEmbeddingChunk { chunkId: string; vector: number[]; + content?: string; } export interface DocEmbeddingsMessage { @@ -73,7 +74,10 @@ export class QdrantDocEmbeddingsStore { { id: randomUUID(), vector: chunk.vector, - payload: { chunk_id: chunk.chunkId }, + payload: { + chunk_id: chunk.chunkId, + ...(chunk.content ? { content: chunk.content } : {}), + }, }, ], }); diff --git a/ts/packages/workbench/index.html b/ts/packages/workbench/index.html index 95a4563c..c4d4b72d 100644 --- a/ts/packages/workbench/index.html +++ b/ts/packages/workbench/index.html @@ -2,13 +2,85 @@ - - TrustGraph Workbench + Beep Graph + + + + + + + + + + + + + + + + + + + + +
+ + + + + + + + + +
Beep Graph
+
Knowledge graph engine
+
+ +
+ + + diff --git a/ts/packages/workbench/public/apple-touch-icon.png b/ts/packages/workbench/public/apple-touch-icon.png new file mode 100644 index 0000000000000000000000000000000000000000..6b60072f45471e62b370e17c0db7d92e8b77c242 GIT binary patch literal 7744 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 literal 0 HcmV?d00001 diff --git a/ts/packages/workbench/public/favicon-16.png b/ts/packages/workbench/public/favicon-16.png new file mode 100644 index 0000000000000000000000000000000000000000..57755d43a1aecad68ac240c6319691e0e2479bca GIT binary patch literal 587 zcmV-R0<`^!P)0pY$A4q^oumQt}`4N#yN8+ zOcux{vXq<>`C^`;Q>5K#pPx}i*m&JDC$$sWoi@s+n3(^pl~*utnan+!qgtwx-^x?iD&SK-ChkpKdx?Djr4&m~mT)^R+n=}bJdgTm zoxR;XVoNcs+ty&f)PpHPkq|o@J2cNOMmd|w(x^2^t|nPtT}C(ci=sx@c*dnst~ynm zU8k=~x6`Hkvy6CxrfVGL4{_Trz@FF%JN@S~{kH^3tHy96V|Gm)VWOAh1}cju?<-p6 Z)o(aYu>!@O-6#M6002ovPDHLkV1j%s2@U`N literal 0 HcmV?d00001 diff --git a/ts/packages/workbench/public/favicon-32.png b/ts/packages/workbench/public/favicon-32.png new file mode 100644 index 0000000000000000000000000000000000000000..680c0a0bb0d1794a07756b7c64de4fdd3473ad92 GIT binary patch literal 1225 zcmV;)1UCDLP)ja_H|MiuIpfCs3fx>^T<0&z20NM2`>9Do3@AGSMH zNgV-vRW4duDvyOK&Lk(2^MX!lYe$Rdy z9gXDHb8Kcds{liE03Nbf#w0m2z%2on3j3r`J($uLq#sRu3wq z=2E)_FzQUrrth|kkn=K`l?;m$i|F(^tWGNcHm8mF^EkOoZdU|SCBfl)VN$cjT?q9+<{y%`e?mo^9p2P3))6~$U zJ)e3wg{rCmI654dET(q`&~Im+{q!sY{R6}jF%((Beb`N8Ho|mtntXkpv`7o9i3+Avmplh2N?WhkPGK7pePD=AC<Tj#(+Y=O{Z2uYG~wmL~JC&}b8-1zeb z;aHf=W(IwY9`AlH@fUFd;{lEzJ&r8P+`4^>Yd>5gy`Cfk8W8;3#Q;4+J+ySR0FcY%$gXB-Y;6P}^m~Y@M^nYb zLTD#tJ5@9XTz;2U92J7U1etjD4HJ`i)i--tJ6wLXG`?}S~^>B`CQui>2FV2ORl{ZlTBxdK6%vz?=LC+|;c_eG=G$kCyr7-|h~1t>d+gb?(6(SyZq0gAhPIQ;On z)^C+kP*{DjN}Ib4Aq2ISTG~DBm>NwOO-9m*(!;212im&Yuph7k#UnK~Iz~R5-*z{N znFPUKgQ!aJ2pUaBTs{}RK_AYplJ{ODV6CLHMbG|t#`1jm56V`}24{Kn^P@N3lQLQr zHn;5oDt&KQ0-Ww5_5ApJapYDYAq3VoE0$&pg?yo0j{CyiWxu4NUac&BbtDj%ihs>? zw)+VDdsW{Sk=#gcv~K)pR?Tt11~dUXn + + + + + + + + + + + diff --git a/ts/packages/workbench/public/icon-192.png b/ts/packages/workbench/public/icon-192.png new file mode 100644 index 0000000000000000000000000000000000000000..c806041edba69bbd104ddac7c73761a8fce42957 GIT binary patch literal 8205 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 literal 0 HcmV?d00001 diff --git a/ts/packages/workbench/public/icon-512.png b/ts/packages/workbench/public/icon-512.png new file mode 100644 index 0000000000000000000000000000000000000000..ec49c6d6c90ee16bf469e07d35016d04374dd8b7 GIT binary patch literal 23340 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∨1zcr|v|p8L8}yBaCB+#sEr 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 P{u$A5{q<> literal 0 HcmV?d00001 diff --git a/ts/packages/workbench/public/manifest.json b/ts/packages/workbench/public/manifest.json new file mode 100644 index 00000000..a7ffae3b --- /dev/null +++ b/ts/packages/workbench/public/manifest.json @@ -0,0 +1,28 @@ +{ + "name": "Beep Graph", + "short_name": "BeepGraph", + "description": "Knowledge graph exploration and AI-powered retrieval", + "start_url": "/", + "display": "standalone", + "background_color": "#09090b", + "theme_color": "#122812", + "icons": [ + { + "src": "/icon-192.png", + "sizes": "192x192", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/icon-512.png", + "sizes": "512x512", + "type": "image/png", + "purpose": "any maskable" + }, + { + "src": "/favicon.svg", + "sizes": "any", + "type": "image/svg+xml" + } + ] +} diff --git a/ts/packages/workbench/public/sw.js b/ts/packages/workbench/public/sw.js new file mode 100644 index 00000000..b907213c --- /dev/null +++ b/ts/packages/workbench/public/sw.js @@ -0,0 +1,37 @@ +// Beep Graph service worker — minimal cache-first for app shell +const CACHE_NAME = "beepgraph-v1"; +const APP_SHELL = ["/", "/index.html"]; + +self.addEventListener("install", (event) => { + event.waitUntil( + caches.open(CACHE_NAME).then((cache) => cache.addAll(APP_SHELL)) + ); + self.skipWaiting(); +}); + +self.addEventListener("activate", (event) => { + event.waitUntil( + caches.keys().then((keys) => + Promise.all(keys.filter((k) => k !== CACHE_NAME).map((k) => caches.delete(k))) + ) + ); + self.clients.claim(); +}); + +self.addEventListener("fetch", (event) => { + // Only cache GET requests for same-origin navigation/assets + if (event.request.method !== "GET") return; + const url = new URL(event.request.url); + if (url.origin !== self.location.origin) return; + + // Network-first for HTML (always get fresh app), cache-first for assets + if (event.request.mode === "navigate") { + event.respondWith( + fetch(event.request).catch(() => caches.match("/index.html")) + ); + } else { + event.respondWith( + caches.match(event.request).then((cached) => cached || fetch(event.request)) + ); + } +}); diff --git a/ts/packages/workbench/src/components/chat/explain-graph.tsx b/ts/packages/workbench/src/components/chat/explain-graph.tsx index adac7c97..2a242dcc 100644 --- a/ts/packages/workbench/src/components/chat/explain-graph.tsx +++ b/ts/packages/workbench/src/components/chat/explain-graph.tsx @@ -70,18 +70,28 @@ export function ExplainGraph({ explainEvents, collection }: ExplainGraphProps) { return () => ro.disconnect(); }, [expanded]); - // Fetch triples when first expanded + // Load triples when first expanded — use inline triples if available, otherwise fetch useEffect(() => { if (!expanded || fetched) return; setFetched(true); + + // Check if any explain events have inline triples + const inlineTriples = explainEvents.flatMap((ev) => ev.explainTriples ?? []); + if (inlineTriples.length > 0) { + setTriples(inlineTriples); + return; + } + + // Fall back to fetching from named graph + const graphUris = explainEvents.filter((ev) => ev.explainGraph); + if (graphUris.length === 0) return; + setLoading(true); setError(null); - const flow = socket.flow(flowId); - // Fetch triples for each explain event's named graph and merge Promise.all( - explainEvents.map((ev) => + graphUris.map((ev) => flow .triplesQuery(undefined, undefined, undefined, 500, collection, ev.explainGraph) .catch(() => [] as Triple[]), diff --git a/ts/packages/workbench/src/components/layout/beep-graph-logo.tsx b/ts/packages/workbench/src/components/layout/beep-graph-logo.tsx new file mode 100644 index 00000000..a09d29bb --- /dev/null +++ b/ts/packages/workbench/src/components/layout/beep-graph-logo.tsx @@ -0,0 +1,47 @@ +/** + * Beep Graph logo — lambda with tilted ThugLife pixel glasses. + */ + +import type { SVGProps } from "react"; + +export function BeepGraphLogo(props: SVGProps) { + return ( + + {/* Lambda body */} + + + {/* ThugLife pixel glasses — tilted, at intersection */} + + + + + + + ); +} diff --git a/ts/packages/workbench/src/components/layout/glow-background.tsx b/ts/packages/workbench/src/components/layout/glow-background.tsx new file mode 100644 index 00000000..ac74c9ef --- /dev/null +++ b/ts/packages/workbench/src/components/layout/glow-background.tsx @@ -0,0 +1,24 @@ +/** + * Ambient glow background — forest green radial blobs that drift and pulse. + * + * Ported from beep-effect4's GlowEffectPaper, adapted for plain CSS + * with multiple independent blobs for organic movement. + */ + +export function GlowBackground() { + return ( +