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 00000000..6b60072f Binary files /dev/null and b/ts/packages/workbench/public/apple-touch-icon.png differ diff --git a/ts/packages/workbench/public/favicon-16.png b/ts/packages/workbench/public/favicon-16.png new file mode 100644 index 00000000..57755d43 Binary files /dev/null and b/ts/packages/workbench/public/favicon-16.png differ diff --git a/ts/packages/workbench/public/favicon-32.png b/ts/packages/workbench/public/favicon-32.png new file mode 100644 index 00000000..680c0a0b Binary files /dev/null and b/ts/packages/workbench/public/favicon-32.png differ diff --git a/ts/packages/workbench/public/favicon.svg b/ts/packages/workbench/public/favicon.svg new file mode 100644 index 00000000..645f5106 --- /dev/null +++ b/ts/packages/workbench/public/favicon.svg @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/ts/packages/workbench/public/icon-192.png b/ts/packages/workbench/public/icon-192.png new file mode 100644 index 00000000..c806041e Binary files /dev/null and b/ts/packages/workbench/public/icon-192.png differ diff --git a/ts/packages/workbench/public/icon-512.png b/ts/packages/workbench/public/icon-512.png new file mode 100644 index 00000000..ec49c6d6 Binary files /dev/null and b/ts/packages/workbench/public/icon-512.png differ 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 ( +