@rubytech/create-realagent 1.0.693 → 1.0.696
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/payload/platform/lib/graph-search/dist/index.d.ts +127 -0
- package/payload/platform/lib/graph-search/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/graph-search/dist/index.js +393 -0
- package/payload/platform/lib/graph-search/dist/index.js.map +1 -0
- package/payload/platform/lib/graph-search/src/__tests__/bm25-only.test.ts +129 -0
- package/payload/platform/lib/graph-search/src/__tests__/escape-and-normalise.test.ts +53 -0
- package/payload/platform/lib/graph-search/src/__tests__/hybrid.test.ts +190 -0
- package/payload/platform/lib/graph-search/src/index.ts +498 -0
- package/payload/platform/lib/graph-search/tsconfig.json +9 -0
- package/payload/platform/lib/graph-search/vitest.config.ts +9 -0
- package/payload/platform/lib/graph-write/dist/index.d.ts +61 -0
- package/payload/platform/lib/graph-write/dist/index.d.ts.map +1 -0
- package/payload/platform/lib/graph-write/dist/index.js +97 -0
- package/payload/platform/lib/graph-write/dist/index.js.map +1 -0
- package/payload/platform/lib/graph-write/src/index.ts +167 -0
- package/payload/platform/lib/graph-write/tsconfig.json +8 -0
- package/payload/platform/package.json +2 -2
- package/payload/platform/plugins/admin/mcp/dist/index.js +19 -8
- package/payload/platform/plugins/admin/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/admin/skills/unzip-attachment/SKILL.md +58 -0
- package/payload/platform/plugins/admin/skills/unzip-attachment/references/safety.md +81 -0
- package/payload/platform/plugins/contacts/mcp/dist/index.js +27 -3
- package/payload/platform/plugins/contacts/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts +4 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js +10 -6
- package/payload/platform/plugins/contacts/mcp/dist/tools/contact-create.js.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.d.ts +2 -0
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.d.ts.map +1 -1
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.js +43 -36
- package/payload/platform/plugins/contacts/mcp/dist/tools/group-create.js.map +1 -1
- package/payload/platform/plugins/docs/references/attachments.md +44 -0
- package/payload/platform/plugins/docs/references/memory-guide.md +6 -0
- package/payload/platform/plugins/memory/mcp/dist/index.js +44 -3
- package/payload/platform/plugins/memory/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts +3 -32
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js +18 -381
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-search.js.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts +9 -5
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.d.ts.map +1 -1
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js +10 -23
- package/payload/platform/plugins/memory/mcp/dist/tools/memory-write.js.map +1 -1
- package/payload/platform/plugins/memory/references/graph-primitives.md +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/index.js +8 -1
- package/payload/platform/plugins/scheduling/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts +2 -0
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.d.ts.map +1 -1
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js +24 -10
- package/payload/platform/plugins/scheduling/mcp/dist/tools/schedule-event.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/index.js +8 -2
- package/payload/platform/plugins/tasks/mcp/dist/index.js.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts +2 -0
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.d.ts.map +1 -1
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js +45 -18
- package/payload/platform/plugins/tasks/mcp/dist/tools/task-create.js.map +1 -1
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-execute.js +12 -2
- package/payload/platform/plugins/workflows/mcp/dist/tools/workflow-execute.js.map +1 -1
- package/payload/server/chunk-IAIGB5WN.js +11406 -0
- package/payload/server/maxy-edge.js +1 -1
- package/payload/server/public/assets/{admin-zbb1g-mh.js → admin-BZSstsyc.js} +60 -60
- package/payload/server/public/index.html +1 -1
- package/payload/server/server.js +660 -22
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { bm25Only } from "../index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Stub Session records every `run()` call — we assert on the Cypher text and
|
|
6
|
+
* params to pin down the clause-composition invariants from the task brief.
|
|
7
|
+
* No real Neo4j is contacted; this is pure unit coverage of the SQL-building
|
|
8
|
+
* decision layer.
|
|
9
|
+
*/
|
|
10
|
+
function makeStubSession(records: Array<Record<string, unknown>> = []) {
|
|
11
|
+
const calls: Array<{ query: string; params: Record<string, unknown> }> = [];
|
|
12
|
+
const session = {
|
|
13
|
+
run(query: string, params: Record<string, unknown>) {
|
|
14
|
+
calls.push({ query, params });
|
|
15
|
+
return Promise.resolve({
|
|
16
|
+
records: records.map((r) => ({
|
|
17
|
+
get: (k: string) => r[k],
|
|
18
|
+
})),
|
|
19
|
+
});
|
|
20
|
+
},
|
|
21
|
+
} as unknown as import("neo4j-driver").Session;
|
|
22
|
+
return { session, calls };
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("bm25Only Cypher composition", () => {
|
|
26
|
+
it("omits scope clause when allowedScopes is not set", async () => {
|
|
27
|
+
const { session, calls } = makeStubSession();
|
|
28
|
+
await bm25Only(session, { query: "foo", accountId: "acc-1", limit: 10 });
|
|
29
|
+
expect(calls).toHaveLength(1);
|
|
30
|
+
expect(calls[0].query).not.toContain("node.scope");
|
|
31
|
+
expect(calls[0].params).not.toHaveProperty("allowedScopes");
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it("adds scope clause only when allowedScopes is set", async () => {
|
|
35
|
+
const { session, calls } = makeStubSession();
|
|
36
|
+
await bm25Only(session, {
|
|
37
|
+
query: "foo",
|
|
38
|
+
accountId: "acc-1",
|
|
39
|
+
limit: 10,
|
|
40
|
+
allowedScopes: ["public", "account"],
|
|
41
|
+
});
|
|
42
|
+
expect(calls[0].query).toContain("node.scope IS NULL OR node.scope IN $allowedScopes");
|
|
43
|
+
expect(calls[0].params.allowedScopes).toEqual(["public", "account"]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("omits agent clause when agentSlug is not set", async () => {
|
|
47
|
+
const { session, calls } = makeStubSession();
|
|
48
|
+
await bm25Only(session, { query: "foo", accountId: "acc-1", limit: 10 });
|
|
49
|
+
expect(calls[0].query).not.toContain("node.agents");
|
|
50
|
+
expect(calls[0].params).not.toHaveProperty("agentSlug");
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it("adds agent clause when agentSlug is set", async () => {
|
|
54
|
+
const { session, calls } = makeStubSession();
|
|
55
|
+
await bm25Only(session, {
|
|
56
|
+
query: "foo",
|
|
57
|
+
accountId: "acc-1",
|
|
58
|
+
limit: 10,
|
|
59
|
+
agentSlug: "support",
|
|
60
|
+
});
|
|
61
|
+
expect(calls[0].query).toContain("$agentSlug IN node.agents");
|
|
62
|
+
expect(calls[0].params.agentSlug).toBe("support");
|
|
63
|
+
});
|
|
64
|
+
|
|
65
|
+
it("omits keyword clause when keywords is not set", async () => {
|
|
66
|
+
const { session, calls } = makeStubSession();
|
|
67
|
+
await bm25Only(session, { query: "foo", accountId: "acc-1", limit: 10 });
|
|
68
|
+
expect(calls[0].query).not.toContain("node.keywords IS NULL");
|
|
69
|
+
expect(calls[0].params).not.toHaveProperty("keywords");
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("adds keyword clause with ANY (default) when keywords is set", async () => {
|
|
73
|
+
const { session, calls } = makeStubSession();
|
|
74
|
+
await bm25Only(session, {
|
|
75
|
+
query: "foo",
|
|
76
|
+
accountId: "acc-1",
|
|
77
|
+
limit: 10,
|
|
78
|
+
keywords: ["Alpha", "Beta"],
|
|
79
|
+
});
|
|
80
|
+
expect(calls[0].query).toContain("ANY(kw IN $keywords WHERE kw IN node.keywords)");
|
|
81
|
+
expect(calls[0].params.keywords).toEqual(["alpha", "beta"]);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("uses ALL when keywordMatch='all'", async () => {
|
|
85
|
+
const { session, calls } = makeStubSession();
|
|
86
|
+
await bm25Only(session, {
|
|
87
|
+
query: "foo",
|
|
88
|
+
accountId: "acc-1",
|
|
89
|
+
limit: 10,
|
|
90
|
+
keywords: ["a"],
|
|
91
|
+
keywordMatch: "all",
|
|
92
|
+
});
|
|
93
|
+
expect(calls[0].query).toContain("ALL(kw IN $keywords WHERE kw IN node.keywords)");
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it("always applies notTrashed filter", async () => {
|
|
97
|
+
const { session, calls } = makeStubSession();
|
|
98
|
+
await bm25Only(session, { query: "foo", accountId: "acc-1", limit: 10 });
|
|
99
|
+
expect(calls[0].query).toContain("NOT `node`:Trashed");
|
|
100
|
+
expect(calls[0].query).toContain("`node`.deletedAt IS NULL");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("escapes Lucene special chars in query param", async () => {
|
|
104
|
+
const { session, calls } = makeStubSession();
|
|
105
|
+
await bm25Only(session, { query: "foo:bar (x)", accountId: "acc-1", limit: 10 });
|
|
106
|
+
expect(calls[0].params.query).toBe("foo\\:bar \\(x\\)");
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("returns [] when the fulltext index does not exist", async () => {
|
|
110
|
+
const session = {
|
|
111
|
+
run() {
|
|
112
|
+
return Promise.reject(new Error("There is no such fulltext index"));
|
|
113
|
+
},
|
|
114
|
+
} as unknown as import("neo4j-driver").Session;
|
|
115
|
+
const hits = await bm25Only(session, { query: "foo", accountId: "acc-1", limit: 10 });
|
|
116
|
+
expect(hits).toEqual([]);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("rethrows non-index errors", async () => {
|
|
120
|
+
const session = {
|
|
121
|
+
run() {
|
|
122
|
+
return Promise.reject(new Error("ServiceUnavailable: database offline"));
|
|
123
|
+
},
|
|
124
|
+
} as unknown as import("neo4j-driver").Session;
|
|
125
|
+
await expect(
|
|
126
|
+
bm25Only(session, { query: "foo", accountId: "acc-1", limit: 10 }),
|
|
127
|
+
).rejects.toThrow("ServiceUnavailable");
|
|
128
|
+
});
|
|
129
|
+
});
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import { escapeLucene, normaliseBm25Scores } from "../index.js";
|
|
3
|
+
|
|
4
|
+
describe("escapeLucene", () => {
|
|
5
|
+
it("escapes Lucene operators in field:value syntax", () => {
|
|
6
|
+
expect(escapeLucene("foo:bar")).toBe("foo\\:bar");
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
it("escapes boolean-grouping parens and OR", () => {
|
|
10
|
+
expect(escapeLucene("(a OR b)")).toBe("\\(a OR b\\)");
|
|
11
|
+
});
|
|
12
|
+
|
|
13
|
+
it("escapes wildcard characters", () => {
|
|
14
|
+
expect(escapeLucene("bar*")).toBe("bar\\*");
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it("escapes backslash", () => {
|
|
18
|
+
expect(escapeLucene("a\\b")).toBe("a\\\\b");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
it("preserves non-special characters", () => {
|
|
22
|
+
expect(escapeLucene("hello world 123")).toBe("hello world 123");
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
it("escapes a mix of specials in one pass", () => {
|
|
26
|
+
expect(escapeLucene("a+b-c&d|e!f")).toBe("a\\+b\\-c\\&d\\|e\\!f");
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
describe("normaliseBm25Scores", () => {
|
|
31
|
+
it("returns empty array for empty input", () => {
|
|
32
|
+
expect(normaliseBm25Scores([])).toEqual([]);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("maps [min..max] to [0..1] via min-max", () => {
|
|
36
|
+
expect(normaliseBm25Scores([1, 2, 3])).toEqual([0, 0.5, 1]);
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("returns 1.0 for all values when range is zero (all scores equal)", () => {
|
|
40
|
+
expect(normaliseBm25Scores([5, 5, 5])).toEqual([1, 1, 1]);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it("returns 1.0 for single-element input (range is zero)", () => {
|
|
44
|
+
expect(normaliseBm25Scores([42])).toEqual([1]);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("handles non-integer scores", () => {
|
|
48
|
+
const result = normaliseBm25Scores([0.5, 1.5, 2.5]);
|
|
49
|
+
expect(result[0]).toBeCloseTo(0);
|
|
50
|
+
expect(result[1]).toBeCloseTo(0.5);
|
|
51
|
+
expect(result[2]).toBeCloseTo(1);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach } from "vitest";
|
|
2
|
+
import { hybrid, clearIndexCache } from "../index.js";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Hybrid tests cover the two decisions that make this lib non-trivial:
|
|
6
|
+
* 1. overlapping vector+BM25 nodeIds combine as 0.7*vec + 0.3*bm25_norm
|
|
7
|
+
* 2. degradeOnEmbedFailure=true with a failing embed returns bm25-only
|
|
8
|
+
* and annotates mode="bm25"
|
|
9
|
+
* Deeper integration (index discovery, expand, keyword subscriptions) is
|
|
10
|
+
* covered by the downstream MCP tool tests against a real Neo4j.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
interface StubRun {
|
|
14
|
+
match: (query: string) => boolean;
|
|
15
|
+
records: Array<Record<string, unknown>>;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function record(fields: Record<string, unknown>) {
|
|
19
|
+
return { get: (k: string) => fields[k] };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function makeStubSession(scripted: StubRun[]) {
|
|
23
|
+
const calls: Array<{ query: string; params: Record<string, unknown> }> = [];
|
|
24
|
+
const session = {
|
|
25
|
+
run(query: string, params: Record<string, unknown>) {
|
|
26
|
+
calls.push({ query, params });
|
|
27
|
+
const hit = scripted.find((s) => s.match(query));
|
|
28
|
+
if (!hit) return Promise.resolve({ records: [] });
|
|
29
|
+
return Promise.resolve({ records: hit.records.map(record) });
|
|
30
|
+
},
|
|
31
|
+
} as unknown as import("neo4j-driver").Session;
|
|
32
|
+
return { session, calls };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
clearIndexCache();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
describe("hybrid — score combination", () => {
|
|
40
|
+
it("combines overlapping nodeIds as 0.7*vec + 0.3*bm25_norm (expand skipped)", async () => {
|
|
41
|
+
const session = makeStubSession([
|
|
42
|
+
{
|
|
43
|
+
match: (q) => q.includes("SHOW INDEXES"),
|
|
44
|
+
records: [{ name: "vec_knowledge", labelsOrTypes: ["KnowledgeDocument"] }],
|
|
45
|
+
},
|
|
46
|
+
{
|
|
47
|
+
match: (q) => q.includes("db.index.vector.queryNodes"),
|
|
48
|
+
records: [
|
|
49
|
+
{
|
|
50
|
+
nodeId: "n1",
|
|
51
|
+
nodeLabels: ["KnowledgeDocument"],
|
|
52
|
+
node: { properties: { title: "A" } },
|
|
53
|
+
score: 0.9,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
nodeId: "n2",
|
|
57
|
+
nodeLabels: ["KnowledgeDocument"],
|
|
58
|
+
node: { properties: { title: "B" } },
|
|
59
|
+
score: 0.5,
|
|
60
|
+
},
|
|
61
|
+
],
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
match: (q) => q.includes("db.index.fulltext.queryNodes"),
|
|
65
|
+
records: [
|
|
66
|
+
{
|
|
67
|
+
nodeId: "n1",
|
|
68
|
+
nodeLabels: ["KnowledgeDocument"],
|
|
69
|
+
node: { properties: { title: "A" } },
|
|
70
|
+
score: 10,
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
nodeId: "n3",
|
|
74
|
+
nodeLabels: ["KnowledgeDocument"],
|
|
75
|
+
node: { properties: { title: "C" } },
|
|
76
|
+
score: 5,
|
|
77
|
+
},
|
|
78
|
+
],
|
|
79
|
+
},
|
|
80
|
+
]).session;
|
|
81
|
+
|
|
82
|
+
const embed = async () => [0.1, 0.2, 0.3];
|
|
83
|
+
const res = await hybrid(session, embed, {
|
|
84
|
+
query: "test",
|
|
85
|
+
accountId: "acc-1",
|
|
86
|
+
limit: 10,
|
|
87
|
+
expandHops: 0,
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
expect(res.mode).toBe("hybrid");
|
|
91
|
+
// bm25 normalised: [10,5] -> [1,0]; n1: 0.7*0.9 + 0.3*1 = 0.93
|
|
92
|
+
// n2 (vec only): 0.7*0.5 + 0.3*0 = 0.35
|
|
93
|
+
// n3 (bm25 only): 0.7*0 + 0.3*0 = 0
|
|
94
|
+
const n1 = res.results.find((r) => r.nodeId === "n1");
|
|
95
|
+
const n2 = res.results.find((r) => r.nodeId === "n2");
|
|
96
|
+
const n3 = res.results.find((r) => r.nodeId === "n3");
|
|
97
|
+
expect(n1?.score).toBeCloseTo(0.93);
|
|
98
|
+
expect(n2?.score).toBeCloseTo(0.35);
|
|
99
|
+
expect(n3?.score).toBeCloseTo(0);
|
|
100
|
+
// Ranking: n1 > n2 > n3
|
|
101
|
+
expect(res.results.map((r) => r.nodeId)).toEqual(["n1", "n2", "n3"]);
|
|
102
|
+
});
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
describe("hybrid — embed degrade", () => {
|
|
106
|
+
const failingEmbed = async () => {
|
|
107
|
+
throw new Error("Ollama unreachable");
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
it("throws when degradeOnEmbedFailure is false (default)", async () => {
|
|
111
|
+
const { session } = makeStubSession([
|
|
112
|
+
{ match: () => true, records: [] },
|
|
113
|
+
]);
|
|
114
|
+
await expect(
|
|
115
|
+
hybrid(session, failingEmbed, { query: "x", accountId: "a", limit: 5 }),
|
|
116
|
+
).rejects.toThrow("Ollama unreachable");
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("returns bm25-only result with mode='bm25' when degradeOnEmbedFailure is true", async () => {
|
|
120
|
+
const { session } = makeStubSession([
|
|
121
|
+
{
|
|
122
|
+
match: (q) => q.includes("db.index.fulltext.queryNodes"),
|
|
123
|
+
records: [
|
|
124
|
+
{
|
|
125
|
+
nodeId: "n1",
|
|
126
|
+
nodeLabels: ["KnowledgeDocument"],
|
|
127
|
+
node: { properties: { title: "hit" } },
|
|
128
|
+
score: 3,
|
|
129
|
+
},
|
|
130
|
+
],
|
|
131
|
+
},
|
|
132
|
+
]);
|
|
133
|
+
|
|
134
|
+
const res = await hybrid(session, failingEmbed, {
|
|
135
|
+
query: "x",
|
|
136
|
+
accountId: "a",
|
|
137
|
+
limit: 5,
|
|
138
|
+
degradeOnEmbedFailure: true,
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
expect(res.mode).toBe("bm25");
|
|
142
|
+
expect(res.embedError).toContain("Ollama unreachable");
|
|
143
|
+
expect(res.results).toHaveLength(1);
|
|
144
|
+
expect(res.results[0]?.nodeId).toBe("n1");
|
|
145
|
+
// score is raw BM25 score (no hybrid combine in degrade path)
|
|
146
|
+
expect(res.results[0]?.score).toBe(3);
|
|
147
|
+
// related[] is [] in degrade path (no expand)
|
|
148
|
+
expect(res.results[0]?.related).toEqual([]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it("returns empty results with mode='bm25' when embed fails and index is missing", async () => {
|
|
152
|
+
const session = {
|
|
153
|
+
run: (q: string) => {
|
|
154
|
+
if (q.includes("db.index.fulltext.queryNodes")) {
|
|
155
|
+
return Promise.reject(new Error("There is no such fulltext index"));
|
|
156
|
+
}
|
|
157
|
+
return Promise.resolve({ records: [] });
|
|
158
|
+
},
|
|
159
|
+
} as unknown as import("neo4j-driver").Session;
|
|
160
|
+
|
|
161
|
+
const res = await hybrid(session, failingEmbed, {
|
|
162
|
+
query: "x",
|
|
163
|
+
accountId: "a",
|
|
164
|
+
limit: 5,
|
|
165
|
+
degradeOnEmbedFailure: true,
|
|
166
|
+
});
|
|
167
|
+
expect(res.mode).toBe("bm25");
|
|
168
|
+
expect(res.results).toEqual([]);
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("hybrid — label filter short-circuit", () => {
|
|
173
|
+
it("returns empty results when requested labels have no vector index", async () => {
|
|
174
|
+
const { session } = makeStubSession([
|
|
175
|
+
{
|
|
176
|
+
match: (q) => q.includes("SHOW INDEXES"),
|
|
177
|
+
records: [{ name: "vec_knowledge", labelsOrTypes: ["KnowledgeDocument"] }],
|
|
178
|
+
},
|
|
179
|
+
]);
|
|
180
|
+
const embed = async () => [0.1];
|
|
181
|
+
const res = await hybrid(session, embed, {
|
|
182
|
+
query: "x",
|
|
183
|
+
accountId: "a",
|
|
184
|
+
limit: 5,
|
|
185
|
+
labels: ["UnknownLabel"],
|
|
186
|
+
});
|
|
187
|
+
expect(res.results).toEqual([]);
|
|
188
|
+
expect(res.mode).toBe("hybrid");
|
|
189
|
+
});
|
|
190
|
+
});
|