From 3a8087248245236d5617c887f68f9bc75fd5a8b6 Mon Sep 17 00:00:00 2001 From: elpresidank Date: Tue, 7 Apr 2026 05:20:10 -0500 Subject: [PATCH] =?UTF-8?q?fix:=20comprehensive=20QA=20=E2=80=94=20resolve?= =?UTF-8?q?=2013=20bugs,=20add=20UX=20improvements=20across=20all=20servic?= =?UTF-8?q?es?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Client SDK: add .catch() to graphRagStreaming/documentRagStreaming (silent timeout), null-guard JSON.parse in getPrompts/getSystemPrompt/getPrompt. Backend: implement "getvalues" config operation for token costs, null-check createTerm() in FalkorDB triples query, add knowledge-cores service entrypoint and Docker entry, return proper HTTP 400/404 for gateway error responses. Workbench: cancel button + elapsed timer for chat, clear agent spinner on error, flow dialog inline validation, responsive header wrapping, knowledge cores loading timeout, sidebar/page naming consistency, theme toggle indicator. Infrastructure: enable Grafana Explore for viewers, add gateway Prometheus scrape target, fix RAG pipeline dashboard layout (6 panels visible), filter Service Health to configured targets only. Co-Authored-By: Claude Opus 4.6 (1M context) --- ts/.dockerignore | 1 + ts/Containerfile | 2 +- ts/deploy/docker-compose.yml | 15 +++++++++- ts/deploy/grafana/dashboards/overview.json | 2 +- .../grafana/dashboards/rag-pipeline.json | 12 ++++---- ts/deploy/prometheus/prometheus.yml | 18 ++++++------ ts/entrypoints/cores.mjs | 6 ++++ ts/packages/base/src/schema/messages.ts | 3 +- .../client/src/socket/trustgraph-socket.ts | 19 +++++++++---- ts/packages/flow/src/config/service.ts | 19 +++++++++++++ ts/packages/flow/src/gateway/server.ts | 14 ++++++++-- .../flow/src/query/triples/falkordb.ts | 16 +++++++---- ts/packages/workbench/nginx.conf | 12 +++++++- .../src/components/layout/sidebar.tsx | 2 +- ts/packages/workbench/src/hooks/use-chat.ts | 28 +++++++++++++++++-- ts/packages/workbench/src/pages/chat.tsx | 27 +++++++++++++++--- ts/packages/workbench/src/pages/flows.tsx | 14 ++++++++++ .../workbench/src/pages/knowledge-cores.tsx | 18 ++++++++---- ts/packages/workbench/src/pages/prompts.tsx | 2 +- ts/packages/workbench/src/pages/settings.tsx | 3 ++ .../workbench/src/pages/token-cost.tsx | 2 +- .../src/providers/socket-provider.tsx | 21 +++++++++----- 22 files changed, 202 insertions(+), 54 deletions(-) create mode 100644 ts/entrypoints/cores.mjs diff --git a/ts/.dockerignore b/ts/.dockerignore index aa9d1653..e583e204 100644 --- a/ts/.dockerignore +++ b/ts/.dockerignore @@ -3,5 +3,6 @@ node_modules deploy **/dist **/.turbo +**/*.tsbuildinfo *.log .env* diff --git a/ts/Containerfile b/ts/Containerfile index acdf0ab5..388992a7 100644 --- a/ts/Containerfile +++ b/ts/Containerfile @@ -21,7 +21,7 @@ RUN pnpm install --frozen-lockfile # Copy source and build COPY packages/ packages/ -RUN pnpm build +RUN pnpm build --filter=@trustgraph/base --filter=@trustgraph/client --filter=@trustgraph/flow # --------------------------------------------------------------------------- # Stage 2: Runtime diff --git a/ts/deploy/docker-compose.yml b/ts/deploy/docker-compose.yml index c35afc49..8f40c79b 100644 --- a/ts/deploy/docker-compose.yml +++ b/ts/deploy/docker-compose.yml @@ -66,7 +66,7 @@ services: networks: - trustgraph healthcheck: - test: ["CMD", "wget", "--spider", "-q", "http://localhost:6333/healthz"] + test: ["CMD-SHELL", "bash -c 'echo > /dev/tcp/localhost/6333'"] interval: 10s timeout: 5s retries: 3 @@ -179,6 +179,7 @@ services: - GF_AUTH_DISABLE_LOGIN_FORM=false - GF_USERS_DEFAULT_THEME=dark - GF_EXPLORE_ENABLED=true + - GF_USERS_VIEWERS_CAN_EDIT=true - GF_FEATURE_TOGGLES_ENABLE=traceqlEditor tempoSearch tempoServiceGraph depends_on: prometheus: @@ -323,6 +324,18 @@ services: - trustgraph restart: unless-stopped + knowledge-cores: + image: trustgraph-ts:local + command: ["node", "entrypoints/cores.mjs"] + environment: + - NATS_URL=nats://nats:4222 + depends_on: + nats: + condition: service_healthy + networks: + - trustgraph + restart: unless-stopped + # --------------------------------------------------------------------------- # Document Processing Pipeline # --------------------------------------------------------------------------- diff --git a/ts/deploy/grafana/dashboards/overview.json b/ts/deploy/grafana/dashboards/overview.json index bd6f4f8d..62e21482 100644 --- a/ts/deploy/grafana/dashboards/overview.json +++ b/ts/deploy/grafana/dashboards/overview.json @@ -32,7 +32,7 @@ "id": 1, "targets": [ { - "expr": "up", + "expr": "up{job=~\"prometheus|otel-collector|gateway\"}", "legendFormat": "{{job}}", "refId": "A" } diff --git a/ts/deploy/grafana/dashboards/rag-pipeline.json b/ts/deploy/grafana/dashboards/rag-pipeline.json index d0d5d972..f8ff0df8 100644 --- a/ts/deploy/grafana/dashboards/rag-pipeline.json +++ b/ts/deploy/grafana/dashboards/rag-pipeline.json @@ -28,7 +28,7 @@ "type": "prometheus", "uid": "tg-prometheus" }, - "gridPos": { "h": 8, "w": 24, "x": 0, "y": 0 }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 0 }, "id": 1, "targets": [ { @@ -92,7 +92,7 @@ "type": "prometheus", "uid": "tg-prometheus" }, - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 0 }, "id": 2, "targets": [ { @@ -151,7 +151,7 @@ "type": "prometheus", "uid": "tg-prometheus" }, - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 8 }, "id": 3, "targets": [ { @@ -210,7 +210,7 @@ "type": "prometheus", "uid": "tg-prometheus" }, - "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 8 }, "id": 4, "targets": [ { @@ -269,7 +269,7 @@ "type": "prometheus", "uid": "tg-prometheus" }, - "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 }, + "gridPos": { "h": 8, "w": 12, "x": 0, "y": 16 }, "id": 5, "targets": [ { @@ -328,7 +328,7 @@ "type": "prometheus", "uid": "tg-prometheus" }, - "gridPos": { "h": 8, "w": 24, "x": 0, "y": 24 }, + "gridPos": { "h": 8, "w": 12, "x": 12, "y": 16 }, "id": 6, "targets": [ { diff --git a/ts/deploy/prometheus/prometheus.yml b/ts/deploy/prometheus/prometheus.yml index b58b54f2..ce78ac35 100644 --- a/ts/deploy/prometheus/prometheus.yml +++ b/ts/deploy/prometheus/prometheus.yml @@ -13,13 +13,10 @@ scrape_configs: - targets: - "prometheus:9090" - # NATS monitoring - - job_name: "nats" - scrape_interval: 15s - metrics_path: "/varz" - static_configs: - - targets: - - "nats:8222" + # NATS monitoring (uses nats-prometheus-exporter format) + # NATS exposes JSON at /varz, not Prometheus format. + # To get proper Prometheus metrics, deploy nats-exporter sidecar. + # For now, we rely on NATS healthcheck and JetStream monitoring via /jsz. # OpenTelemetry Collector (exposes Prometheus metrics from OTLP pipeline) - job_name: "otel-collector" @@ -28,9 +25,10 @@ scrape_configs: - targets: - "otel-collector:8889" - # TrustGraph gateway (enabled when gateway container is running) + # TrustGraph gateway metrics (prom-client) - job_name: "gateway" - scrape_interval: 5s + scrape_interval: 15s + metrics_path: "/api/v1/metrics" static_configs: - targets: - - "gateway:8000" + - "gateway:8088" diff --git a/ts/entrypoints/cores.mjs b/ts/entrypoints/cores.mjs new file mode 100644 index 00000000..a5df1bb1 --- /dev/null +++ b/ts/entrypoints/cores.mjs @@ -0,0 +1,6 @@ +import("../packages/flow/dist/cores/service.js") + .then((m) => m.run()) + .catch((err) => { + console.error(err); + process.exit(1); + }); diff --git a/ts/packages/base/src/schema/messages.ts b/ts/packages/base/src/schema/messages.ts index 0cf5fbc3..3422b2dd 100644 --- a/ts/packages/base/src/schema/messages.ts +++ b/ts/packages/base/src/schema/messages.ts @@ -126,12 +126,13 @@ export interface DocumentEmbeddingsResponse { } // Config -export type ConfigOperation = "get" | "list" | "delete" | "put" | "config"; +export type ConfigOperation = "get" | "list" | "delete" | "put" | "config" | "getvalues"; export interface ConfigRequest { operation: ConfigOperation; keys?: string[]; values?: Record; + type?: string; } export interface ConfigResponse { diff --git a/ts/packages/client/src/socket/trustgraph-socket.ts b/ts/packages/client/src/socket/trustgraph-socket.ts index c864849e..29c86d79 100644 --- a/ts/packages/client/src/socket/trustgraph-socket.ts +++ b/ts/packages/client/src/socket/trustgraph-socket.ts @@ -1174,7 +1174,8 @@ export class FlowsApi { string, Record> >; - return JSON.parse(config.config.prompt["template-index"]); + const raw = config.config?.prompt?.["template-index"]; + return raw ? JSON.parse(raw) : []; }); } @@ -1187,7 +1188,8 @@ export class FlowsApi { string, Record> >; - return JSON.parse(config.config.prompt[`template.${id}`]); + const raw = config.config?.prompt?.[`template.${id}`]; + return raw ? JSON.parse(raw) : null; }); } @@ -1200,7 +1202,8 @@ export class FlowsApi { string, Record> >; - return JSON.parse(config.config.prompt.system); + const raw = config.config?.prompt?.system; + return raw ? JSON.parse(raw) : ""; }); } @@ -1546,7 +1549,10 @@ export class FlowApi { 60000, undefined, this.flowId, - ); + ).catch((err) => { + const errorMessage = err instanceof Error ? err.message : err?.toString() || "Unknown error"; + onError(`Graph RAG request failed: ${errorMessage}`); + }); } /** @@ -1617,7 +1623,10 @@ export class FlowApi { 60000, undefined, this.flowId, - ); + ).catch((err) => { + const errorMessage = err instanceof Error ? err.message : err?.toString() || "Unknown error"; + onError(`Document RAG request failed: ${errorMessage}`); + }); } /** diff --git a/ts/packages/flow/src/config/service.ts b/ts/packages/flow/src/config/service.ts index c1213614..1dfd14cd 100644 --- a/ts/packages/flow/src/config/service.ts +++ b/ts/packages/flow/src/config/service.ts @@ -129,6 +129,9 @@ export class ConfigService extends AsyncProcessor { case "config": return this.handleConfigDump(); + case "getvalues": + return this.handleGetValues(request); + default: throw new Error(`Unknown config operation: ${op as string}`); } @@ -237,6 +240,22 @@ export class ConfigService extends AsyncProcessor { }; } + private handleGetValues(request: ConfigRequest): ConfigResponse { + const type = request.type ?? ""; + + const values: { key: string; value: unknown }[] = []; + + for (const [namespace, subMap] of this.store) { + if (!type || namespace === type || namespace.startsWith(`${type}.`) || namespace.startsWith(`${type}/`)) { + for (const [k, v] of subMap) { + values.push({ key: `${namespace}.${k}`, value: v }); + } + } + } + + return { version: this.version, values: values as unknown as Record }; + } + private handleConfigDump(): ConfigResponse { const config: Record = {}; diff --git a/ts/packages/flow/src/gateway/server.ts b/ts/packages/flow/src/gateway/server.ts index 1b02a5a5..6ad8271a 100644 --- a/ts/packages/flow/src/gateway/server.ts +++ b/ts/packages/flow/src/gateway/server.ts @@ -47,7 +47,12 @@ export async function createGateway(config: GatewayConfig) { const body = request.body as Record; try { - const result = await dispatcher.dispatchGlobalService(kind, body); + const result = await dispatcher.dispatchGlobalService(kind, body) as Record; + const err = result?.error as { type?: string; message?: string } | undefined; + if (err) { + const statusCode = err.type === "not-found" ? 404 : 400; + return reply.code(statusCode).send(result); + } return result; } catch (err) { reply.code(500).send({ error: { type: "internal", message: String(err) } }); @@ -62,7 +67,12 @@ export async function createGateway(config: GatewayConfig) { const body = request.body as Record; try { - const result = await dispatcher.dispatchFlowService(flow, kind, body); + const result = await dispatcher.dispatchFlowService(flow, kind, body) as Record; + const err = result?.error as { type?: string; message?: string } | undefined; + if (err) { + const statusCode = err.type === "not-found" ? 404 : 400; + return reply.code(statusCode).send(result); + } return result; } catch (err) { reply.code(500).send({ error: { type: "internal", message: String(err) } }); diff --git a/ts/packages/flow/src/query/triples/falkordb.ts b/ts/packages/flow/src/query/triples/falkordb.ts index b6dbd3af..5ae1de02 100644 --- a/ts/packages/flow/src/query/triples/falkordb.ts +++ b/ts/packages/flow/src/query/triples/falkordb.ts @@ -25,6 +25,9 @@ function termToValue(term: Term | undefined): string | null { } function createTerm(value: string): Term { + if (!value) { + return { type: "LITERAL", value: "" }; + } if (value.startsWith("http://") || value.startsWith("https://")) { return { type: "IRI", iri: value }; } @@ -90,11 +93,14 @@ export class FalkorDBTriplesQuery { await this.matchAll(rawTriples, limit); } - return rawTriples.slice(0, limit).map(([s, p, o]) => ({ - s: createTerm(s), - p: createTerm(p), - o: createTerm(o), - })); + return rawTriples + .filter(([s, p, o]) => s != null && p != null && o != null) + .slice(0, limit) + .map(([s, p, o]) => ({ + s: createTerm(s), + p: createTerm(p), + o: createTerm(o), + })); } private async matchPattern( diff --git a/ts/packages/workbench/nginx.conf b/ts/packages/workbench/nginx.conf index 7629f1fe..86a2c6c5 100644 --- a/ts/packages/workbench/nginx.conf +++ b/ts/packages/workbench/nginx.conf @@ -16,7 +16,17 @@ server { proxy_set_header X-Real-IP $remote_addr; } - # WebSocket proxy + # WebSocket proxy (client connects to /api/socket, gateway listens on /api/v1/socket) + location /api/socket { + proxy_pass http://gateway:8088/api/v1/socket; + proxy_http_version 1.1; + proxy_set_header Upgrade $http_upgrade; + proxy_set_header Connection "upgrade"; + proxy_set_header Host $host; + proxy_read_timeout 86400; + } + + # WebSocket proxy (direct v1 path) location /api/v1/socket { proxy_pass http://gateway:8088; proxy_http_version 1.1; diff --git a/ts/packages/workbench/src/components/layout/sidebar.tsx b/ts/packages/workbench/src/components/layout/sidebar.tsx index 309f269f..4f3ae550 100644 --- a/ts/packages/workbench/src/components/layout/sidebar.tsx +++ b/ts/packages/workbench/src/components/layout/sidebar.tsx @@ -160,7 +160,7 @@ export function Sidebar() { - + diff --git a/ts/packages/workbench/src/hooks/use-chat.ts b/ts/packages/workbench/src/hooks/use-chat.ts index fc0749e4..97f8802e 100644 --- a/ts/packages/workbench/src/hooks/use-chat.ts +++ b/ts/packages/workbench/src/hooks/use-chat.ts @@ -1,4 +1,4 @@ -import { useCallback } from "react"; +import { useCallback, useRef } from "react"; import { useSocket } from "@/providers/socket-provider"; import { useConversation, @@ -16,6 +16,7 @@ import type { StreamingMetadata } from "@trustgraph/client"; export interface UseChatReturn { submitMessage: (opts: { input: string }) => void; + cancelRequest: () => void; } /** @@ -33,10 +34,32 @@ export function useChat(): UseChatReturn { const addActivity = useProgressStore((s) => s.addActivity); const removeActivity = useProgressStore((s) => s.removeActivity); + const abortControllerRef = useRef(null); + + const cancelRequest = useCallback(() => { + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + abortControllerRef.current = null; + } + updateLastMessage((prev) => ({ + ...prev, + content: prev.content || "(Cancelled)", + isStreaming: false, + activePhase: undefined, + })); + removeActivity("Chat request"); + }, [updateLastMessage, removeActivity]); + const submitMessage = useCallback( ({ input }: { input: string }) => { if (!input.trim()) return; + // Abort any in-flight request + if (abortControllerRef.current) { + abortControllerRef.current.abort(); + } + abortControllerRef.current = new AbortController(); + const activityLabel = "Chat request"; // 1. Add the user message @@ -101,6 +124,7 @@ export function useChat(): UseChatReturn { ...prev, content: prev.content || `Error: ${error}`, isStreaming: false, + activePhase: undefined, })); removeActivity(activityLabel); }; @@ -211,5 +235,5 @@ export function useChat(): UseChatReturn { ], ); - return { submitMessage }; + return { submitMessage, cancelRequest }; } diff --git a/ts/packages/workbench/src/pages/chat.tsx b/ts/packages/workbench/src/pages/chat.tsx index 86a527b2..98687947 100644 --- a/ts/packages/workbench/src/pages/chat.tsx +++ b/ts/packages/workbench/src/pages/chat.tsx @@ -15,6 +15,7 @@ import { ChevronDown, ChevronRight, Loader2, + X, } from "lucide-react"; import Markdown from "react-markdown"; import { cn } from "@/lib/utils"; @@ -189,12 +190,23 @@ export default function ChatPage() { const setInput = useConversation((s) => s.setInput); const setChatMode = useConversation((s) => s.setChatMode); const clearMessages = useConversation((s) => s.clearMessages); - const { submitMessage } = useChat(); + const { submitMessage, cancelRequest } = useChat(); const collection = useSettings((s) => s.settings.collection); const isLoading = useProgressStore((s) => s.isLoading); const scrollRef = useRef(null); + // Elapsed time counter while loading + const [elapsed, setElapsed] = useState(0); + useEffect(() => { + if (!isLoading) { + setElapsed(0); + return; + } + const interval = setInterval(() => setElapsed((e) => e + 1), 1000); + return () => clearInterval(interval); + }, [isLoading]); + // Auto-scroll to bottom when messages change useEffect(() => { scrollRef.current?.scrollIntoView({ behavior: "smooth" }); @@ -219,7 +231,7 @@ export default function ChatPage() { return (
{/* Header */} -
+

Chat

@@ -228,7 +240,7 @@ export default function ChatPage() {
-
+
{/* Mode selector */}
{MODES.map((mode) => ( @@ -279,7 +291,14 @@ export default function ChatPage() { {isLoading && (
- Processing... + Processing... {elapsed}s +
)} diff --git a/ts/packages/workbench/src/pages/flows.tsx b/ts/packages/workbench/src/pages/flows.tsx index a5758d87..1ef490bf 100644 --- a/ts/packages/workbench/src/pages/flows.tsx +++ b/ts/packages/workbench/src/pages/flows.tsx @@ -43,6 +43,7 @@ function StartFlowDialog({ const [paramsJson, setParamsJson] = useState("{}"); const [submitting, setSubmitting] = useState(false); const [paramsError, setParamsError] = useState(null); + const [submitted, setSubmitted] = useState(false); // Fetch blueprints when dialog opens useEffect(() => { @@ -70,9 +71,13 @@ function StartFlowDialog({ setParamsJson("{}"); setParamsError(null); setSubmitting(false); + setSubmitted(false); }; const handleSubmit = async () => { + setSubmitted(true); + if (!isValid) return; + let params: Record = {}; try { params = JSON.parse(paramsJson); @@ -140,6 +145,9 @@ function StartFlowDialog({ placeholder="my-flow-id" className="w-full rounded-lg border border-border bg-surface-100 px-3 py-2 text-sm text-fg placeholder:text-fg-subtle focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500" /> + {submitted && !id.trim() && ( +

Flow ID is required

+ )}
{/* Blueprint name */} @@ -167,6 +175,9 @@ function StartFlowDialog({ ))} )} + {submitted && !blueprint && ( +

Blueprint is required

+ )}
{/* Description */} @@ -181,6 +192,9 @@ function StartFlowDialog({ placeholder="Human-readable description" className="w-full rounded-lg border border-border bg-surface-100 px-3 py-2 text-sm text-fg placeholder:text-fg-subtle focus:border-brand-500 focus:outline-none focus:ring-1 focus:ring-brand-500" /> + {submitted && !description.trim() && ( +

Description is required

+ )}
{/* Parameters (JSON) */} diff --git a/ts/packages/workbench/src/pages/knowledge-cores.tsx b/ts/packages/workbench/src/pages/knowledge-cores.tsx index 685c7942..974462ef 100644 --- a/ts/packages/workbench/src/pages/knowledge-cores.tsx +++ b/ts/packages/workbench/src/pages/knowledge-cores.tsx @@ -83,7 +83,13 @@ export default function KnowledgeCoresPage() { try { setLoading(true); setError(null); - const ids = await socket.knowledge().getKnowledgeCores(); + const timeoutPromise = new Promise((_, reject) => + setTimeout(() => reject(new Error("Request timed out")), 15000), + ); + const ids = await Promise.race([ + socket.knowledge().getKnowledgeCores(), + timeoutPromise, + ]); setCores(Array.isArray(ids) ? ids : []); } catch (err) { const msg = err instanceof Error ? err.message : String(err); @@ -144,13 +150,15 @@ export default function KnowledgeCoresPage() { return (
{/* Header */} -
+

Knowledge Cores

- - {cores.length} core{cores.length !== 1 ? "s" : ""} - + {!loading && ( + + {cores.length} core{cores.length !== 1 ? "s" : ""} + + )}