2026-04-05 22:44:45 -05:00
|
|
|
/**
|
|
|
|
|
* FalkorDB triples query service — queries RDF triples from FalkorDB.
|
|
|
|
|
*
|
|
|
|
|
* Implements all SPO query patterns (S, P, O, SP, SO, PO, SPO, *).
|
|
|
|
|
*
|
|
|
|
|
* Python reference: trustgraph-flow/trustgraph/query/triples/falkordb/service.py
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { createClient, Graph } from "falkordb";
|
|
|
|
|
import type { Term, Triple } from "@trustgraph/base";
|
|
|
|
|
|
|
|
|
|
export interface FalkorDBQueryConfig {
|
|
|
|
|
url?: string;
|
|
|
|
|
database?: string;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function termToValue(term: Term | undefined): string | null {
|
2026-05-12 08:06:58 -05:00
|
|
|
if (term === undefined) return null;
|
2026-04-05 22:44:45 -05:00
|
|
|
switch (term.type) {
|
|
|
|
|
case "IRI": return term.iri;
|
|
|
|
|
case "LITERAL": return term.value;
|
|
|
|
|
case "BLANK": return term.id;
|
|
|
|
|
default: return null;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function createTerm(value: string): Term {
|
2026-05-12 08:06:58 -05:00
|
|
|
if (value.length === 0) {
|
fix: comprehensive QA — resolve 13 bugs, add UX improvements across all services
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) <noreply@anthropic.com>
2026-04-07 05:20:10 -05:00
|
|
|
return { type: "LITERAL", value: "" };
|
|
|
|
|
}
|
2026-04-05 22:44:45 -05:00
|
|
|
if (value.startsWith("http://") || value.startsWith("https://")) {
|
|
|
|
|
return { type: "IRI", iri: value };
|
|
|
|
|
}
|
|
|
|
|
return { type: "LITERAL", value };
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-10 04:59:36 -05:00
|
|
|
/** Extract a string field from a FalkorDB result row (returns object with named keys). */
|
|
|
|
|
function field(row: unknown, key: string): string {
|
|
|
|
|
return (row as Record<string, unknown>)?.[key] as string ?? "";
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-05 22:44:45 -05:00
|
|
|
export class FalkorDBTriplesQuery {
|
|
|
|
|
private graph: Graph;
|
2026-04-07 02:19:12 -05:00
|
|
|
private connectPromise: Promise<void>;
|
2026-04-05 22:44:45 -05:00
|
|
|
|
|
|
|
|
constructor(config: FalkorDBQueryConfig = {}) {
|
|
|
|
|
const url = config.url ?? process.env.FALKORDB_URL ?? "redis://localhost:6379";
|
|
|
|
|
const database = config.database ?? "falkordb";
|
|
|
|
|
|
|
|
|
|
const client = createClient({ url });
|
|
|
|
|
this.graph = new Graph(client, database);
|
2026-04-07 02:19:12 -05:00
|
|
|
this.connectPromise = client.connect().then(() => {
|
|
|
|
|
console.log(`[FalkorDBTriplesQuery] Connected to ${url}, graph: ${database}`);
|
fix: comprehensive QA audit — light mode, accessibility, error handling, code quality
- Fix light mode: theme-aware graph node labels, remove prose-invert for
theme-safe markdown, add brand/semantic color overrides for light backgrounds
- Add 404 catch-all route redirecting unknown paths to /chat
- FalkorDB: add .catch() to connectPromise, add ensureConnected() to all
store methods (createLiteral, relateNode, relateLiteral, deleteCollection)
- Accessibility: dialog role/aria-modal, toast aria-live, dismiss/zoom/search
button aria-labels, close panel aria-label
- Lazy-load ForceGraph2D (splits 189KB into separate chunk, main bundle -26%)
- Cap conversation localStorage at 200 messages to prevent quota overflow
- Fix pnpm test: add --passWithNoTests to cli/mcp packages
- Add upload error notification instead of silent catch
- Remove unused class-variance-authority dep and dead tabs.tsx component
- Add @types/node to flow package devDependencies
- Remove stale FIXME comment in messages.ts
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 09:15:59 -05:00
|
|
|
}).catch((err) => {
|
|
|
|
|
console.error(`[FalkorDBTriplesQuery] Connection failed:`, err);
|
|
|
|
|
throw err;
|
2026-04-07 02:19:12 -05:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async ensureConnected(): Promise<void> {
|
|
|
|
|
await this.connectPromise;
|
2026-04-05 22:44:45 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async queryTriples(
|
|
|
|
|
s?: Term,
|
|
|
|
|
p?: Term,
|
|
|
|
|
o?: Term,
|
|
|
|
|
limit = 100,
|
|
|
|
|
): Promise<Triple[]> {
|
2026-04-07 02:19:12 -05:00
|
|
|
await this.ensureConnected();
|
2026-04-05 22:44:45 -05:00
|
|
|
const sv = termToValue(s);
|
|
|
|
|
const pv = termToValue(p);
|
|
|
|
|
const ov = termToValue(o);
|
|
|
|
|
|
|
|
|
|
const rawTriples: [string, string, string][] = [];
|
|
|
|
|
|
|
|
|
|
// Query both Node and Literal targets for each pattern
|
2026-05-12 08:06:58 -05:00
|
|
|
if (sv !== null && pv !== null && ov !== null) {
|
2026-04-05 22:44:45 -05:00
|
|
|
// SPO — exact match
|
|
|
|
|
await this.matchPattern(rawTriples, sv, pv, ov, limit);
|
2026-05-12 08:06:58 -05:00
|
|
|
} else if (sv !== null && pv !== null) {
|
2026-04-05 22:44:45 -05:00
|
|
|
// SP — known subject + predicate
|
|
|
|
|
await this.matchSP(rawTriples, sv, pv, limit);
|
2026-05-12 08:06:58 -05:00
|
|
|
} else if (sv !== null && ov !== null) {
|
2026-04-05 22:44:45 -05:00
|
|
|
// SO — known subject + object
|
|
|
|
|
await this.matchSO(rawTriples, sv, ov, limit);
|
2026-05-12 08:06:58 -05:00
|
|
|
} else if (pv !== null && ov !== null) {
|
2026-04-05 22:44:45 -05:00
|
|
|
// PO — known predicate + object
|
|
|
|
|
await this.matchPO(rawTriples, pv, ov, limit);
|
2026-05-12 08:06:58 -05:00
|
|
|
} else if (sv !== null) {
|
2026-04-05 22:44:45 -05:00
|
|
|
// S only
|
|
|
|
|
await this.matchS(rawTriples, sv, limit);
|
2026-05-12 08:06:58 -05:00
|
|
|
} else if (pv !== null) {
|
2026-04-05 22:44:45 -05:00
|
|
|
// P only
|
|
|
|
|
await this.matchP(rawTriples, pv, limit);
|
2026-05-12 08:06:58 -05:00
|
|
|
} else if (ov !== null) {
|
2026-04-05 22:44:45 -05:00
|
|
|
// O only
|
|
|
|
|
await this.matchO(rawTriples, ov, limit);
|
|
|
|
|
} else {
|
|
|
|
|
// Wildcard — all triples
|
|
|
|
|
await this.matchAll(rawTriples, limit);
|
|
|
|
|
}
|
|
|
|
|
|
fix: comprehensive QA — resolve 13 bugs, add UX improvements across all services
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) <noreply@anthropic.com>
2026-04-07 05:20:10 -05:00
|
|
|
return rawTriples
|
2026-05-12 08:06:58 -05:00
|
|
|
.filter(([s, p, o]) => s !== null && p !== null && o !== null)
|
fix: comprehensive QA — resolve 13 bugs, add UX improvements across all services
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) <noreply@anthropic.com>
2026-04-07 05:20:10 -05:00
|
|
|
.slice(0, limit)
|
|
|
|
|
.map(([s, p, o]) => ({
|
|
|
|
|
s: createTerm(s),
|
|
|
|
|
p: createTerm(p),
|
|
|
|
|
o: createTerm(o),
|
|
|
|
|
}));
|
2026-04-05 22:44:45 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async matchPattern(
|
|
|
|
|
out: [string, string, string][],
|
|
|
|
|
sv: string, pv: string, ov: string, limit: number,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
for (const destType of ["Literal", "Node"] as const) {
|
|
|
|
|
const destKey = destType === "Literal" ? "value" : "uri";
|
|
|
|
|
const result = await this.graph.query(
|
|
|
|
|
`MATCH (src:Node {uri: $src})-[rel:Rel {uri: $rel}]->(dest:${destType} {${destKey}: $dest}) ` +
|
|
|
|
|
`RETURN src.uri LIMIT ${limit}`,
|
|
|
|
|
{ params: { src: sv, rel: pv, dest: ov } },
|
|
|
|
|
);
|
2026-04-10 04:59:36 -05:00
|
|
|
for (const _rec of (result.data ?? [])) {
|
2026-04-05 22:44:45 -05:00
|
|
|
out.push([sv, pv, ov]);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async matchSP(
|
|
|
|
|
out: [string, string, string][],
|
|
|
|
|
sv: string, pv: string, limit: number,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
// Literals
|
|
|
|
|
const litResult = await this.graph.query(
|
|
|
|
|
`MATCH (src:Node {uri: $src})-[rel:Rel {uri: $rel}]->(dest:Literal) ` +
|
|
|
|
|
`RETURN dest.value as dest LIMIT ${limit}`,
|
|
|
|
|
{ params: { src: sv, rel: pv } },
|
|
|
|
|
);
|
2026-04-10 04:59:36 -05:00
|
|
|
for (const rec of (litResult.data ?? [])) {
|
|
|
|
|
out.push([sv, pv, field(rec, "dest")]);
|
2026-04-05 22:44:45 -05:00
|
|
|
}
|
|
|
|
|
// Nodes
|
|
|
|
|
const nodeResult = await this.graph.query(
|
|
|
|
|
`MATCH (src:Node {uri: $src})-[rel:Rel {uri: $rel}]->(dest:Node) ` +
|
|
|
|
|
`RETURN dest.uri as dest LIMIT ${limit}`,
|
|
|
|
|
{ params: { src: sv, rel: pv } },
|
|
|
|
|
);
|
2026-04-10 04:59:36 -05:00
|
|
|
for (const rec of (nodeResult.data ?? [])) {
|
|
|
|
|
out.push([sv, pv, field(rec, "dest")]);
|
2026-04-05 22:44:45 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async matchSO(
|
|
|
|
|
out: [string, string, string][],
|
|
|
|
|
sv: string, ov: string, limit: number,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
for (const [destType, destKey] of [["Literal", "value"], ["Node", "uri"]] as const) {
|
|
|
|
|
const result = await this.graph.query(
|
|
|
|
|
`MATCH (src:Node {uri: $src})-[rel:Rel]->(dest:${destType} {${destKey}: $dest}) ` +
|
|
|
|
|
`RETURN rel.uri as rel LIMIT ${limit}`,
|
|
|
|
|
{ params: { src: sv, dest: ov } },
|
|
|
|
|
);
|
2026-04-10 04:59:36 -05:00
|
|
|
for (const rec of (result.data ?? [])) {
|
|
|
|
|
out.push([sv, field(rec, "rel"), ov]);
|
2026-04-05 22:44:45 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async matchPO(
|
|
|
|
|
out: [string, string, string][],
|
|
|
|
|
pv: string, ov: string, limit: number,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
for (const [destType, destKey] of [["Literal", "value"], ["Node", "uri"]] as const) {
|
|
|
|
|
const result = await this.graph.query(
|
|
|
|
|
`MATCH (src:Node)-[rel:Rel {uri: $rel}]->(dest:${destType} {${destKey}: $dest}) ` +
|
|
|
|
|
`RETURN src.uri as src LIMIT ${limit}`,
|
|
|
|
|
{ params: { rel: pv, dest: ov } },
|
|
|
|
|
);
|
2026-04-10 04:59:36 -05:00
|
|
|
for (const rec of (result.data ?? [])) {
|
|
|
|
|
out.push([field(rec, "src"), pv, ov]);
|
2026-04-05 22:44:45 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async matchS(
|
|
|
|
|
out: [string, string, string][],
|
|
|
|
|
sv: string, limit: number,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const litResult = await this.graph.query(
|
|
|
|
|
`MATCH (src:Node {uri: $src})-[rel:Rel]->(dest:Literal) ` +
|
|
|
|
|
`RETURN rel.uri as rel, dest.value as dest LIMIT ${limit}`,
|
|
|
|
|
{ params: { src: sv } },
|
|
|
|
|
);
|
2026-04-10 04:59:36 -05:00
|
|
|
for (const rec of (litResult.data ?? [])) {
|
|
|
|
|
out.push([sv, field(rec, "rel"), field(rec, "dest")]);
|
2026-04-05 22:44:45 -05:00
|
|
|
}
|
|
|
|
|
const nodeResult = await this.graph.query(
|
|
|
|
|
`MATCH (src:Node {uri: $src})-[rel:Rel]->(dest:Node) ` +
|
|
|
|
|
`RETURN rel.uri as rel, dest.uri as dest LIMIT ${limit}`,
|
|
|
|
|
{ params: { src: sv } },
|
|
|
|
|
);
|
2026-04-10 04:59:36 -05:00
|
|
|
for (const rec of (nodeResult.data ?? [])) {
|
|
|
|
|
out.push([sv, field(rec, "rel"), field(rec, "dest")]);
|
2026-04-05 22:44:45 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async matchP(
|
|
|
|
|
out: [string, string, string][],
|
|
|
|
|
pv: string, limit: number,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const litResult = await this.graph.query(
|
|
|
|
|
`MATCH (src:Node)-[rel:Rel {uri: $rel}]->(dest:Literal) ` +
|
|
|
|
|
`RETURN src.uri as src, dest.value as dest LIMIT ${limit}`,
|
|
|
|
|
{ params: { rel: pv } },
|
|
|
|
|
);
|
2026-04-10 04:59:36 -05:00
|
|
|
for (const rec of (litResult.data ?? [])) {
|
|
|
|
|
out.push([field(rec, "src"), pv, field(rec, "dest")]);
|
2026-04-05 22:44:45 -05:00
|
|
|
}
|
|
|
|
|
const nodeResult = await this.graph.query(
|
|
|
|
|
`MATCH (src:Node)-[rel:Rel {uri: $rel}]->(dest:Node) ` +
|
|
|
|
|
`RETURN src.uri as src, dest.uri as dest LIMIT ${limit}`,
|
|
|
|
|
{ params: { rel: pv } },
|
|
|
|
|
);
|
2026-04-10 04:59:36 -05:00
|
|
|
for (const rec of (nodeResult.data ?? [])) {
|
|
|
|
|
out.push([field(rec, "src"), pv, field(rec, "dest")]);
|
2026-04-05 22:44:45 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async matchO(
|
|
|
|
|
out: [string, string, string][],
|
|
|
|
|
ov: string, limit: number,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
for (const [destType, destKey] of [["Literal", "value"], ["Node", "uri"]] as const) {
|
|
|
|
|
const result = await this.graph.query(
|
|
|
|
|
`MATCH (src:Node)-[rel:Rel]->(dest:${destType} {${destKey}: $dest}) ` +
|
|
|
|
|
`RETURN src.uri as src, rel.uri as rel LIMIT ${limit}`,
|
|
|
|
|
{ params: { dest: ov } },
|
|
|
|
|
);
|
2026-04-10 04:59:36 -05:00
|
|
|
for (const rec of (result.data ?? [])) {
|
|
|
|
|
out.push([field(rec, "src"), field(rec, "rel"), ov]);
|
2026-04-05 22:44:45 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async matchAll(
|
|
|
|
|
out: [string, string, string][],
|
|
|
|
|
limit: number,
|
|
|
|
|
): Promise<void> {
|
|
|
|
|
const litResult = await this.graph.query(
|
|
|
|
|
`MATCH (src:Node)-[rel:Rel]->(dest:Literal) ` +
|
|
|
|
|
`RETURN src.uri as src, rel.uri as rel, dest.value as dest LIMIT ${limit}`,
|
|
|
|
|
);
|
2026-04-10 04:59:36 -05:00
|
|
|
for (const rec of (litResult.data ?? [])) {
|
|
|
|
|
out.push([field(rec, "src"), field(rec, "rel"), field(rec, "dest")]);
|
2026-04-05 22:44:45 -05:00
|
|
|
}
|
|
|
|
|
const nodeResult = await this.graph.query(
|
|
|
|
|
`MATCH (src:Node)-[rel:Rel]->(dest:Node) ` +
|
|
|
|
|
`RETURN src.uri as src, rel.uri as rel, dest.uri as dest LIMIT ${limit}`,
|
|
|
|
|
);
|
2026-04-10 04:59:36 -05:00
|
|
|
for (const rec of (nodeResult.data ?? [])) {
|
|
|
|
|
out.push([field(rec, "src"), field(rec, "rel"), field(rec, "dest")]);
|
2026-04-05 22:44:45 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|