@retinue/agentkit 0.2.0 → 0.3.0
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/README.md +1 -1
- package/dist/adapters/audio/openai.d.ts +36 -0
- package/dist/adapters/audio/openai.js +136 -0
- package/dist/adapters/bullmq/lock.d.ts +26 -2
- package/dist/adapters/memory/connections.d.ts +10 -0
- package/dist/adapters/memory/connections.js +87 -0
- package/dist/adapters/memory/graph.d.ts +17 -0
- package/dist/adapters/memory/graph.js +267 -0
- package/dist/adapters/memory/index.d.ts +3 -0
- package/dist/adapters/memory/index.js +3 -0
- package/dist/adapters/memory/rate-limit.d.ts +14 -0
- package/dist/adapters/memory/rate-limit.js +28 -0
- package/dist/adapters/memory/runtime.js +0 -0
- package/dist/adapters/postgres/connections.d.ts +15 -0
- package/dist/adapters/postgres/connections.js +134 -0
- package/dist/adapters/postgres/graph.d.ts +24 -0
- package/dist/adapters/postgres/graph.js +325 -0
- package/dist/adapters/postgres/index.d.ts +2 -0
- package/dist/adapters/postgres/index.js +2 -0
- package/dist/adapters/postgres/migrations.js +198 -0
- package/dist/adapters/postgres/run-store.js +26 -2
- package/dist/adapters/postgres/schema.d.ts +61 -2
- package/dist/adapters/postgres/schema.js +95 -10
- package/dist/adapters/redis/index.d.ts +1 -0
- package/dist/adapters/redis/index.js +1 -0
- package/dist/adapters/redis/rate-limit.d.ts +46 -0
- package/dist/adapters/redis/rate-limit.js +55 -0
- package/dist/adapters/supabase/index.d.ts +15 -0
- package/dist/adapters/supabase/index.js +15 -0
- package/dist/adapters/supabase/rls.js +33 -0
- package/dist/agents/agent.d.ts +78 -1
- package/dist/agents/agent.js +118 -5
- package/dist/agents/define.js +24 -4
- package/dist/agents/engine.d.ts +20 -0
- package/dist/agents/engine.js +177 -6
- package/dist/audio/index.d.ts +151 -0
- package/dist/audio/index.js +183 -0
- package/dist/connections/cipher.d.ts +103 -0
- package/dist/connections/cipher.js +141 -0
- package/dist/connections/index.d.ts +156 -0
- package/dist/connections/index.js +72 -0
- package/dist/connections/oauth/client.d.ts +84 -0
- package/dist/connections/oauth/client.js +141 -0
- package/dist/connections/oauth/index.d.ts +166 -0
- package/dist/connections/oauth/index.js +226 -0
- package/dist/connections/oauth/service.d.ts +90 -0
- package/dist/connections/oauth/service.js +132 -0
- package/dist/connections/pause.d.ts +68 -0
- package/dist/connections/pause.js +97 -0
- package/dist/connections/resolver.d.ts +59 -0
- package/dist/connections/resolver.js +138 -0
- package/dist/connections/resume.d.ts +64 -0
- package/dist/connections/resume.js +99 -0
- package/dist/connections/vault-cipher.d.ts +95 -0
- package/dist/connections/vault-cipher.js +206 -0
- package/dist/core/content-parts.d.ts +18 -2
- package/dist/core/content-parts.js +1 -0
- package/dist/core/context.d.ts +25 -0
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/errors.js +15 -0
- package/dist/core/events.d.ts +44 -2
- package/dist/core/events.js +31 -2
- package/dist/core/validation.js +19 -0
- package/dist/entries/connections.d.ts +10 -0
- package/dist/entries/connections.js +10 -0
- package/dist/entries/knowledge.d.ts +8 -0
- package/dist/entries/knowledge.js +8 -0
- package/dist/entries/mcp-server.d.ts +9 -0
- package/dist/entries/mcp-server.js +9 -0
- package/dist/entries/runtime.d.ts +10 -0
- package/dist/entries/runtime.js +8 -0
- package/dist/entries/testing.d.ts +28 -0
- package/dist/entries/testing.js +28 -0
- package/dist/graphql/resolvers.d.ts +3 -1
- package/dist/graphql/resolvers.js +14 -0
- package/dist/knowledge/communities.d.ts +166 -0
- package/dist/knowledge/communities.js +377 -0
- package/dist/knowledge/graph-global.d.ts +153 -0
- package/dist/knowledge/graph-global.js +246 -0
- package/dist/knowledge/graph-retrieval.d.ts +150 -0
- package/dist/knowledge/graph-retrieval.js +303 -0
- package/dist/knowledge/graph.d.ts +223 -0
- package/dist/knowledge/graph.js +369 -0
- package/dist/knowledge/index.d.ts +24 -0
- package/dist/knowledge/index.js +36 -1
- package/dist/knowledge/retrieval.d.ts +26 -1
- package/dist/knowledge/retrieval.js +77 -1
- package/dist/mcp-server/index.d.ts +168 -0
- package/dist/mcp-server/index.js +175 -0
- package/dist/models/extraction.d.ts +73 -0
- package/dist/models/extraction.js +118 -0
- package/dist/models/index.d.ts +40 -1
- package/dist/models/index.js +22 -1
- package/dist/models/pricing.d.ts +2 -0
- package/dist/models/pricing.js +17 -1
- package/dist/models/provider-factory.js +9 -7
- package/dist/models/streaming.d.ts +89 -0
- package/dist/models/streaming.js +179 -2
- package/dist/persistence/index.d.ts +230 -0
- package/dist/persistence/index.js +2 -0
- package/dist/runtime/index.d.ts +1 -1
- package/dist/runtime/index.js +12 -0
- package/dist/runtime/worker.d.ts +36 -1
- package/dist/runtime/worker.js +37 -4
- package/dist/security/checklist.js +46 -0
- package/dist/server/bin.d.ts +19 -0
- package/dist/server/bin.js +226 -0
- package/dist/server/boot.d.ts +20 -2
- package/dist/server/boot.js +29 -3
- package/dist/server/cli.d.ts +13 -0
- package/dist/server/cli.js +7 -4
- package/dist/server/doctor.d.ts +74 -0
- package/dist/server/doctor.js +280 -0
- package/dist/skills/context.d.ts +61 -0
- package/dist/skills/context.js +107 -0
- package/dist/skills/index.d.ts +1 -0
- package/dist/skills/index.js +1 -0
- package/dist/telemetry/spans.js +5 -0
- package/dist/testing/conformance/artifact-exports.d.ts +20 -0
- package/dist/testing/conformance/artifact-exports.js +172 -0
- package/dist/testing/conformance/artifacts.d.ts +11 -0
- package/dist/testing/conformance/artifacts.js +316 -0
- package/dist/testing/conformance/audio.d.ts +38 -0
- package/dist/testing/conformance/audio.js +145 -0
- package/dist/testing/conformance/capability.d.ts +23 -0
- package/dist/testing/conformance/capability.js +27 -0
- package/dist/testing/conformance/checkpoint-store.d.ts +15 -0
- package/dist/testing/conformance/checkpoint-store.js +72 -0
- package/dist/testing/conformance/connections.d.ts +14 -0
- package/dist/testing/conformance/connections.js +171 -0
- package/dist/testing/conformance/conversation-store.d.ts +10 -0
- package/dist/testing/conformance/conversation-store.js +60 -0
- package/dist/testing/conformance/evaluation.d.ts +10 -0
- package/dist/testing/conformance/evaluation.js +295 -0
- package/dist/testing/conformance/files.d.ts +19 -0
- package/dist/testing/conformance/files.js +454 -0
- package/dist/testing/conformance/flows.d.ts +16 -0
- package/dist/testing/conformance/flows.js +193 -0
- package/dist/testing/conformance/graph.d.ts +22 -0
- package/dist/testing/conformance/graph.js +500 -0
- package/dist/testing/conformance/hitl.d.ts +25 -0
- package/dist/testing/conformance/hitl.js +523 -0
- package/dist/testing/conformance/index.d.ts +163 -0
- package/dist/testing/conformance/index.js +321 -0
- package/dist/testing/conformance/invariants.d.ts +23 -0
- package/dist/testing/conformance/invariants.js +80 -0
- package/dist/testing/conformance/knowledge.d.ts +41 -0
- package/dist/testing/conformance/knowledge.js +592 -0
- package/dist/testing/conformance/parents.d.ts +50 -0
- package/dist/testing/conformance/parents.js +39 -0
- package/dist/testing/conformance/rate-limit.d.ts +25 -0
- package/dist/testing/conformance/rate-limit.js +71 -0
- package/dist/testing/conformance/records.d.ts +36 -0
- package/dist/testing/conformance/records.js +400 -0
- package/dist/testing/conformance/rollups.d.ts +30 -0
- package/dist/testing/conformance/rollups.js +425 -0
- package/dist/testing/conformance/run-coordinator.d.ts +27 -0
- package/dist/testing/conformance/run-coordinator.js +150 -0
- package/dist/testing/conformance/run-event-log.d.ts +9 -0
- package/dist/testing/conformance/run-event-log.js +138 -0
- package/dist/testing/conformance/run-store.d.ts +12 -0
- package/dist/testing/conformance/run-store.js +294 -0
- package/dist/testing/conformance/session-state.d.ts +38 -0
- package/dist/testing/conformance/session-state.js +198 -0
- package/dist/testing/conformance/usage-limits.d.ts +10 -0
- package/dist/testing/conformance/usage-limits.js +309 -0
- package/dist/testing/conformance.d.ts +8 -0
- package/dist/testing/conformance.js +8 -0
- package/dist/testing/memory-backend.d.ts +39 -0
- package/dist/testing/memory-backend.js +41 -0
- package/dist/testing/pglite.d.ts +55 -0
- package/dist/testing/pglite.js +78 -0
- package/dist/testing/stub-model.d.ts +58 -0
- package/dist/testing/stub-model.js +71 -0
- package/dist/testing/supabase-storage-double.d.ts +35 -0
- package/dist/testing/supabase-storage-double.js +101 -0
- package/dist/toolkit/http.d.ts +27 -0
- package/dist/toolkit/http.js +48 -0
- package/dist/toolkit/index.d.ts +27 -1
- package/dist/toolkit/index.js +24 -1
- package/dist/toolkit/mime.d.ts +97 -0
- package/dist/toolkit/mime.js +233 -0
- package/dist/toolkit/ssrf.d.ts +139 -0
- package/dist/toolkit/ssrf.js +349 -0
- package/dist/toolkit/vendor.d.ts +91 -0
- package/dist/toolkit/vendor.js +153 -0
- package/dist/tools/credentials.d.ts +265 -6
- package/dist/tools/credentials.js +318 -3
- package/dist/tools/define.d.ts +14 -0
- package/dist/tools/define.js +3 -0
- package/dist/tools/index.d.ts +21 -0
- package/dist/tools/index.js +7 -0
- package/dist/tools/library/audio.d.ts +63 -0
- package/dist/tools/library/audio.js +148 -0
- package/dist/tools/library/index.d.ts +8 -0
- package/dist/tools/library/index.js +7 -0
- package/dist/tools/registry.d.ts +33 -0
- package/dist/tools/registry.js +72 -2
- package/dist/usage/index.d.ts +21 -0
- package/dist/usage/index.js +1 -0
- package/dist/usage/rate-limit.d.ts +122 -0
- package/dist/usage/rate-limit.js +131 -0
- package/dist/usage/recorder.js +3 -0
- package/package.json +29 -2
|
@@ -0,0 +1,369 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The knowledge graph — REQ-064 (#270), task #271.
|
|
3
|
+
*
|
|
4
|
+
* Retrieval finds the chunks that *look like* the question. That cannot answer "which teams depend on the retry
|
|
5
|
+
* budget?", because no chunk says so: the fact is spread across a dozen documents, each naming one team and one
|
|
6
|
+
* dependency. This module builds the structure that does — entities, the relationships between them, and where
|
|
7
|
+
* each came from.
|
|
8
|
+
*
|
|
9
|
+
* Nothing here retrieves anything. #273 and #274 do that; this produces the graph and, just as importantly,
|
|
10
|
+
* proves it costs nothing when nobody asked for it.
|
|
11
|
+
*
|
|
12
|
+
* ## Three properties everything else rests on
|
|
13
|
+
*
|
|
14
|
+
* **Off costs nothing.** The extractor is optional in the dependencies and gated twice more at run time. A
|
|
15
|
+
* deployment that never enables GraphRAG supplies no extractor, makes no model calls, and writes no graph rows
|
|
16
|
+
* — measured by a test that counts calls rather than by reading this paragraph.
|
|
17
|
+
*
|
|
18
|
+
* **Determinism.** The same corpus produces byte-identical graphs twice. This is not tidiness: #275 measures
|
|
19
|
+
* GraphRAG against a fixed baseline, and a graph that shifts between runs cannot be measured, cannot be
|
|
20
|
+
* debugged, and cannot have its incremental rebuild (#272) verified. Every ordering here is explicit, and
|
|
21
|
+
* entity resolution is rule-based rather than similarity-based for exactly this reason — embedding similarity
|
|
22
|
+
* is not stable across model versions.
|
|
23
|
+
*
|
|
24
|
+
* **Provenance.** Every entity and edge records its chunks. The retriever will present graph-derived material
|
|
25
|
+
* as citable, so an untraceable claim is one the model states as though a document said it.
|
|
26
|
+
*
|
|
27
|
+
* ## What extraction is allowed to do to us
|
|
28
|
+
*
|
|
29
|
+
* Extraction is a model call, so it can return prose, malformed JSON, entities with no names, or an edge
|
|
30
|
+
* between two things it never mentioned. **None of that may corrupt the graph or fail the index.** A chunk
|
|
31
|
+
* whose extraction is unusable simply contributes nothing — it stays embedded and findable by every existing
|
|
32
|
+
* mode, and the graph is the only thing missing. `sanitiseExtraction` is where that judgement lives, and it is
|
|
33
|
+
* deliberately strict: an edge naming an entity that was not extracted is dropped rather than repaired,
|
|
34
|
+
* because repairing it would invent a provenance nobody asserted.
|
|
35
|
+
*/
|
|
36
|
+
/** Words dropped from the front of a name during normalisation. Articles only — see `normaliseName`. */
|
|
37
|
+
const LEADING_ARTICLES = ["the", "a", "an"];
|
|
38
|
+
/**
|
|
39
|
+
* A name reduced to what makes two spellings the same thing.
|
|
40
|
+
*
|
|
41
|
+
* Case, whitespace, separators and a leading article. That gets "the retry budget", "Retry Budget" and
|
|
42
|
+
* "retry-budget" to one entity, which is the merge AC-4 names.
|
|
43
|
+
*
|
|
44
|
+
* **What it deliberately does not do** is anything cleverer. Stemming would merge "billing" with "bill";
|
|
45
|
+
* embedding similarity would merge whatever the model of the week thinks is close and would stop being
|
|
46
|
+
* reproducible the moment that model changed. Both would make the graph unmeasurable, and the second would
|
|
47
|
+
* make it undebuggable too.
|
|
48
|
+
*
|
|
49
|
+
* Parenthetical qualifiers survive as words, so "Ana (engineering)" and "Ana (sales)" stay distinct — which
|
|
50
|
+
* AC-4 asks for, and which a punctuation-stripping rule gets right only by accident. It is worth being explicit
|
|
51
|
+
* that this is *why* punctuation becomes a space rather than nothing.
|
|
52
|
+
*/
|
|
53
|
+
export const normaliseName = (raw) => {
|
|
54
|
+
const collapsed = raw
|
|
55
|
+
.normalize("NFKD")
|
|
56
|
+
// Diacritics dropped so "Zurich" and "Zürich" meet. Same reasoning as case.
|
|
57
|
+
.replace(/[̀-ͯ]/g, "")
|
|
58
|
+
.toLowerCase()
|
|
59
|
+
// A separator becomes a space, never nothing: joining "retry" and "budget" into "retrybudget" would fail to
|
|
60
|
+
// match the spaced form, and dropping "(engineering)" entirely would merge two different people.
|
|
61
|
+
.replace(/[^\p{L}\p{N}]+/gu, " ")
|
|
62
|
+
.trim()
|
|
63
|
+
.replace(/\s+/g, " ");
|
|
64
|
+
const words = collapsed.split(" ").filter((word) => word !== "");
|
|
65
|
+
// Only when something follows it: "The" alone is a name, however unlikely, and stripping it leaves nothing.
|
|
66
|
+
if (words.length > 1 && LEADING_ARTICLES.includes(words[0])) {
|
|
67
|
+
return words.slice(1).join(" ");
|
|
68
|
+
}
|
|
69
|
+
return words.join(" ");
|
|
70
|
+
};
|
|
71
|
+
/** An entity type, normalised the same way but without the article rule — a type is one word in practice. */
|
|
72
|
+
export const normaliseType = (raw) => raw
|
|
73
|
+
.toLowerCase()
|
|
74
|
+
.replace(/[^\p{L}\p{N}]+/gu, "-")
|
|
75
|
+
.replace(/^-+|-+$/g, "") || "unknown";
|
|
76
|
+
/**
|
|
77
|
+
* The identity of an entity: its type and its normalised name.
|
|
78
|
+
*
|
|
79
|
+
* Readable rather than hashed, deliberately. `concept:retry budget` in a log, an error message or a database
|
|
80
|
+
* row tells whoever is reading it what went wrong; `sha256:9f2a…` tells them to go and write a script. The id
|
|
81
|
+
* is not a secret and its length is not a problem at this scale.
|
|
82
|
+
*
|
|
83
|
+
* Type participates because a person and a project can share a name and are not the same thing.
|
|
84
|
+
*/
|
|
85
|
+
export const entityId = (type, name) => `${normaliseType(type)}:${normaliseName(name)}`;
|
|
86
|
+
/** The identity of an edge. Direction is part of it: "A depends on B" is not "B depends on A". */
|
|
87
|
+
export const relationshipId = (fromId, type, toId) => `${fromId}|${normaliseType(type)}|${toId}`;
|
|
88
|
+
const asText = (value) => (typeof value === "string" ? value.trim() : "");
|
|
89
|
+
/**
|
|
90
|
+
* How long a description may be before it is a chunk rather than a description.
|
|
91
|
+
*
|
|
92
|
+
* A model asked for a short description sometimes returns the paragraph. Truncating rather than refusing keeps
|
|
93
|
+
* the entity — the name and the edges are the valuable part — and stops one verbose extraction from dominating
|
|
94
|
+
* the storage of a whole corpus.
|
|
95
|
+
*/
|
|
96
|
+
export const MAX_DESCRIPTION_CHARS = 480;
|
|
97
|
+
const clamp = (text) => text.length <= MAX_DESCRIPTION_CHARS ? text : `${text.slice(0, MAX_DESCRIPTION_CHARS - 1).trimEnd()}…`;
|
|
98
|
+
/**
|
|
99
|
+
* One chunk's raw extraction, turned into rows that can be trusted — or into nothing.
|
|
100
|
+
*
|
|
101
|
+
* Total: any input produces a valid contribution, possibly empty. That is what makes AC-7 hold everywhere at
|
|
102
|
+
* once rather than at each call site.
|
|
103
|
+
*
|
|
104
|
+
* The strict part is edges. An extractor routinely names an endpoint it did not extract — a pronoun it
|
|
105
|
+
* resolved, a thing it inferred — and the tempting fix is to create the missing entity. That invents an entity
|
|
106
|
+
* with a provenance nobody asserted, which is the exact failure provenance exists to prevent. So the edge is
|
|
107
|
+
* dropped, and the chunk keeps whatever else it gave us.
|
|
108
|
+
*/
|
|
109
|
+
export const sanitiseExtraction = (chunkId, raw) => {
|
|
110
|
+
const entities = new Map();
|
|
111
|
+
for (const candidate of Array.isArray(raw.entities) ? raw.entities : []) {
|
|
112
|
+
const name = asText(candidate?.name);
|
|
113
|
+
const type = asText(candidate?.type) || "concept";
|
|
114
|
+
if (name === "" || normaliseName(name) === "")
|
|
115
|
+
continue;
|
|
116
|
+
const id = entityId(type, name);
|
|
117
|
+
const description = clamp(asText(candidate?.description));
|
|
118
|
+
const existing = entities.get(id);
|
|
119
|
+
if (existing === undefined) {
|
|
120
|
+
entities.set(id, { name, type, ...(description === "" ? {} : { description }) });
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
// The same entity named twice in one chunk: keep the longer description, and the name that sorts first so
|
|
124
|
+
// the choice does not depend on extraction order.
|
|
125
|
+
const better = description.length > (existing.description ?? "").length ? description : existing.description;
|
|
126
|
+
entities.set(id, {
|
|
127
|
+
name: existing.name < name ? existing.name : name,
|
|
128
|
+
type: existing.type,
|
|
129
|
+
...(better === undefined || better === "" ? {} : { description: better }),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
const relationships = new Map();
|
|
133
|
+
for (const candidate of Array.isArray(raw.relationships) ? raw.relationships : []) {
|
|
134
|
+
const from = asText(candidate?.from);
|
|
135
|
+
const to = asText(candidate?.to);
|
|
136
|
+
const type = asText(candidate?.type) || "related-to";
|
|
137
|
+
if (from === "" || to === "")
|
|
138
|
+
continue;
|
|
139
|
+
// Endpoints are matched by *normalised name across any type*, because an extractor names an entity in a
|
|
140
|
+
// relationship without repeating its type and a type-qualified lookup would then miss every edge.
|
|
141
|
+
const fromId = [...entities.keys()].find((id) => id.slice(id.indexOf(":") + 1) === normaliseName(from));
|
|
142
|
+
const toId = [...entities.keys()].find((id) => id.slice(id.indexOf(":") + 1) === normaliseName(to));
|
|
143
|
+
if (fromId === undefined || toId === undefined)
|
|
144
|
+
continue; // See the note above: dropped, never invented.
|
|
145
|
+
if (fromId === toId)
|
|
146
|
+
continue; // A self-edge carries no information and clutters every traversal.
|
|
147
|
+
const description = clamp(asText(candidate?.description));
|
|
148
|
+
const id = relationshipId(fromId, type, toId);
|
|
149
|
+
const existing = relationships.get(id);
|
|
150
|
+
if (existing === undefined) {
|
|
151
|
+
relationships.set(id, { fromId, toId, type, ...(description === "" ? {} : { description }) });
|
|
152
|
+
}
|
|
153
|
+
else if (description.length > (existing.description ?? "").length) {
|
|
154
|
+
relationships.set(id, { ...existing, description });
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return {
|
|
158
|
+
entities: [...entities.entries()]
|
|
159
|
+
.map(([id, entity]) => ({
|
|
160
|
+
id,
|
|
161
|
+
name: entity.name,
|
|
162
|
+
type: normaliseType(entity.type),
|
|
163
|
+
...(entity.description === undefined ? {} : { description: entity.description }),
|
|
164
|
+
surfaceForms: [entity.name],
|
|
165
|
+
provenance: [chunkId],
|
|
166
|
+
}))
|
|
167
|
+
// Sorted here, not by the caller: determinism is a property of this function's output, so it cannot be
|
|
168
|
+
// forgotten at one of several call sites.
|
|
169
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)),
|
|
170
|
+
relationships: [...relationships.entries()]
|
|
171
|
+
.map(([id, edge]) => ({
|
|
172
|
+
id,
|
|
173
|
+
fromId: edge.fromId,
|
|
174
|
+
toId: edge.toId,
|
|
175
|
+
type: normaliseType(edge.type),
|
|
176
|
+
...(edge.description === undefined ? {} : { description: edge.description }),
|
|
177
|
+
weight: 1,
|
|
178
|
+
provenance: [chunkId],
|
|
179
|
+
}))
|
|
180
|
+
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)),
|
|
181
|
+
};
|
|
182
|
+
};
|
|
183
|
+
const uniqueSorted = (values) => [...new Set(values)].sort();
|
|
184
|
+
/**
|
|
185
|
+
* Many chunks' contributions, merged into one source's contribution.
|
|
186
|
+
*
|
|
187
|
+
* Where surface forms accumulate and edge weights are counted. The canonical `name` is the one that sorts
|
|
188
|
+
* first among everything merged — an arbitrary rule, chosen because it is *stable*: "first seen" would depend
|
|
189
|
+
* on chunk order, and chunk order is a thing that can change without the document changing.
|
|
190
|
+
*/
|
|
191
|
+
export const mergeContributions = (parts) => {
|
|
192
|
+
const entities = new Map();
|
|
193
|
+
for (const part of parts) {
|
|
194
|
+
for (const entity of part.entities) {
|
|
195
|
+
const existing = entities.get(entity.id);
|
|
196
|
+
if (existing === undefined) {
|
|
197
|
+
entities.set(entity.id, entity);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const description = (entity.description ?? "").length > (existing.description ?? "").length ? entity.description : existing.description;
|
|
201
|
+
entities.set(entity.id, {
|
|
202
|
+
id: entity.id,
|
|
203
|
+
name: existing.name < entity.name ? existing.name : entity.name,
|
|
204
|
+
type: existing.type,
|
|
205
|
+
...(description === undefined ? {} : { description }),
|
|
206
|
+
surfaceForms: uniqueSorted([...existing.surfaceForms, ...entity.surfaceForms]),
|
|
207
|
+
provenance: uniqueSorted([...existing.provenance, ...entity.provenance]),
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
const relationships = new Map();
|
|
212
|
+
for (const part of parts) {
|
|
213
|
+
for (const edge of part.relationships) {
|
|
214
|
+
const existing = relationships.get(edge.id);
|
|
215
|
+
if (existing === undefined) {
|
|
216
|
+
relationships.set(edge.id, edge);
|
|
217
|
+
continue;
|
|
218
|
+
}
|
|
219
|
+
const description = (edge.description ?? "").length > (existing.description ?? "").length ? edge.description : existing.description;
|
|
220
|
+
const provenance = uniqueSorted([...existing.provenance, ...edge.provenance]);
|
|
221
|
+
relationships.set(edge.id, {
|
|
222
|
+
...existing,
|
|
223
|
+
...(description === undefined ? {} : { description }),
|
|
224
|
+
// Chunks that asserted it, not times it was seen — so re-reading one chunk cannot inflate a weight.
|
|
225
|
+
weight: provenance.length,
|
|
226
|
+
provenance,
|
|
227
|
+
});
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
// An edge whose endpoints did not survive the merge is dropped, for the same reason `sanitiseExtraction`
|
|
231
|
+
// drops one: the alternative is an edge pointing at nothing.
|
|
232
|
+
const kept = [...relationships.values()].filter((edge) => entities.has(edge.fromId) && entities.has(edge.toId));
|
|
233
|
+
return {
|
|
234
|
+
entities: [...entities.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)),
|
|
235
|
+
relationships: kept.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0)),
|
|
236
|
+
};
|
|
237
|
+
};
|
|
238
|
+
/**
|
|
239
|
+
* Extraction runs one chunk at a time by default.
|
|
240
|
+
*
|
|
241
|
+
* Sequential rather than parallel because the cost is the point: a corpus of 603 chunks is 603 model calls, and
|
|
242
|
+
* firing them concurrently is how a re-index becomes a rate-limit incident. A deployment that wants throughput
|
|
243
|
+
* raises this deliberately, having read the number.
|
|
244
|
+
*/
|
|
245
|
+
export const DEFAULT_EXTRACTION_CONCURRENCY = 1;
|
|
246
|
+
export const createGraphIndexer = (deps) => {
|
|
247
|
+
const now = deps.now ?? (() => Date.now());
|
|
248
|
+
const log = deps.log ?? (() => { });
|
|
249
|
+
return {
|
|
250
|
+
extractorId: deps.extractor.id,
|
|
251
|
+
/**
|
|
252
|
+
* Whether this source would be graph-indexed — both switches, in the cheap order.
|
|
253
|
+
*
|
|
254
|
+
* The tenant switch first, so a deployment with GraphRAG off does one settings read and stops, rather than
|
|
255
|
+
* one read per source. AC-2's "flag is inert while the tenant switch is off" is this ordering: the source
|
|
256
|
+
* flag is never even consulted, and it stays stored for the day the tenant switch flips.
|
|
257
|
+
*/
|
|
258
|
+
async shouldIndex(context, source) {
|
|
259
|
+
const settings = await deps.store.getSettings({ tenantId: context.tenantId });
|
|
260
|
+
if (!settings.enabled)
|
|
261
|
+
return false;
|
|
262
|
+
return deps.store.isSourceEnabled({ tenantId: context.tenantId, ...source });
|
|
263
|
+
},
|
|
264
|
+
/**
|
|
265
|
+
* Extract one source's chunks into the graph, replacing whatever that source contributed before.
|
|
266
|
+
*
|
|
267
|
+
* The caller has already decided this should run — `indexSource` calls `shouldIndex` first — so this does
|
|
268
|
+
* not re-check. Keeping the decision in one place is what makes "off costs nothing" a property of one
|
|
269
|
+
* function rather than a convention two functions share.
|
|
270
|
+
*/
|
|
271
|
+
async indexSource(context, input) {
|
|
272
|
+
const started = now();
|
|
273
|
+
const concurrency = Math.max(1, input.concurrency ?? DEFAULT_EXTRACTION_CONCURRENCY);
|
|
274
|
+
const parts = [];
|
|
275
|
+
let extractionCalls = 0;
|
|
276
|
+
let inputTokens = 0;
|
|
277
|
+
let outputTokens = 0;
|
|
278
|
+
let unusableChunks = 0;
|
|
279
|
+
const runOne = async (chunk) => {
|
|
280
|
+
extractionCalls += 1;
|
|
281
|
+
try {
|
|
282
|
+
const { extraction, usage } = await deps.extractor.extract(chunk);
|
|
283
|
+
inputTokens += usage?.inputTokens ?? 0;
|
|
284
|
+
outputTokens += usage?.outputTokens ?? 0;
|
|
285
|
+
const contribution = sanitiseExtraction(chunk.id, extraction ?? {});
|
|
286
|
+
if (contribution.entities.length === 0)
|
|
287
|
+
unusableChunks += 1;
|
|
288
|
+
return contribution;
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
/**
|
|
292
|
+
* AC-7. A model call can fail or return prose, and neither may fail the index.
|
|
293
|
+
*
|
|
294
|
+
* Swallowed here rather than propagated because the chunk is *already embedded and findable* — every
|
|
295
|
+
* existing retrieval mode works on it, and the graph is the only thing missing. Failing the index
|
|
296
|
+
* would take the working part down with the optional one.
|
|
297
|
+
*/
|
|
298
|
+
unusableChunks += 1;
|
|
299
|
+
log("graph extraction failed for a chunk", {
|
|
300
|
+
chunkId: chunk.id,
|
|
301
|
+
reason: error instanceof Error ? error.message : String(error),
|
|
302
|
+
});
|
|
303
|
+
return { entities: [], relationships: [] };
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
for (let offset = 0; offset < input.chunks.length; offset += concurrency) {
|
|
307
|
+
const window = input.chunks.slice(offset, offset + concurrency);
|
|
308
|
+
// Order-independent by construction — `mergeContributions` sorts — but collected in order anyway, so a
|
|
309
|
+
// log read alongside the corpus makes sense.
|
|
310
|
+
parts.push(...(await Promise.all(window.map(runOne))));
|
|
311
|
+
}
|
|
312
|
+
const contribution = mergeContributions(parts);
|
|
313
|
+
const written = await deps.store.replaceSourceGraph({
|
|
314
|
+
tenantId: context.tenantId,
|
|
315
|
+
sourceType: input.sourceType,
|
|
316
|
+
sourceId: input.sourceId,
|
|
317
|
+
contribution,
|
|
318
|
+
});
|
|
319
|
+
const result = {
|
|
320
|
+
ran: true,
|
|
321
|
+
entities: written.entities,
|
|
322
|
+
relationships: written.relationships,
|
|
323
|
+
pruned: written.pruned,
|
|
324
|
+
extractionCalls,
|
|
325
|
+
inputTokens,
|
|
326
|
+
outputTokens,
|
|
327
|
+
unusableChunks,
|
|
328
|
+
elapsedMs: now() - started,
|
|
329
|
+
};
|
|
330
|
+
if (unusableChunks > 0)
|
|
331
|
+
log("some chunks contributed nothing to the graph", {
|
|
332
|
+
sourceId: input.sourceId,
|
|
333
|
+
unusableChunks,
|
|
334
|
+
of: input.chunks.length,
|
|
335
|
+
});
|
|
336
|
+
return result;
|
|
337
|
+
},
|
|
338
|
+
};
|
|
339
|
+
};
|
|
340
|
+
/** What `indexSource` reports when the graph was not touched. Every counter zero, and `ran` says why. */
|
|
341
|
+
export const GRAPH_NOT_RUN = {
|
|
342
|
+
ran: false,
|
|
343
|
+
entities: 0,
|
|
344
|
+
relationships: 0,
|
|
345
|
+
pruned: 0,
|
|
346
|
+
extractionCalls: 0,
|
|
347
|
+
inputTokens: 0,
|
|
348
|
+
outputTokens: 0,
|
|
349
|
+
unusableChunks: 0,
|
|
350
|
+
elapsedMs: 0,
|
|
351
|
+
};
|
|
352
|
+
/**
|
|
353
|
+
* The default `EntityExtractor`: one model call per chunk — AC-2.
|
|
354
|
+
*
|
|
355
|
+
* Thin on purpose. The provider call lives in `models/extraction.ts` behind boundary rule R3, so this is the
|
|
356
|
+
* adapter between that function and the port — which means a deployment can replace the extractor entirely
|
|
357
|
+
* (a smaller model, a local one, a rules-based one for a known corpus) without touching the pipeline.
|
|
358
|
+
*
|
|
359
|
+
* `model` is a factory rather than a value because a `LanguageModel` is resolved from a policy per tenant, and
|
|
360
|
+
* a single resolved model captured at construction would be the same one for every tenant in the deployment.
|
|
361
|
+
*/
|
|
362
|
+
export const createModelEntityExtractor = (deps) => ({
|
|
363
|
+
id: deps.id ?? "model",
|
|
364
|
+
async extract(chunk) {
|
|
365
|
+
const { extraction, usage } = await deps.extract(chunk.content);
|
|
366
|
+
return { extraction, usage };
|
|
367
|
+
},
|
|
368
|
+
});
|
|
369
|
+
//# sourceMappingURL=graph.js.map
|
|
@@ -16,6 +16,7 @@ import type { TenantId } from "../core/ids.js";
|
|
|
16
16
|
import type { DocumentBlock } from "../documents/index.js";
|
|
17
17
|
import type { EmbeddingModelRef, KnowledgeSourceType, KnowledgeStore } from "../persistence/index.js";
|
|
18
18
|
import { type ChunkingLimits } from "./chunking.js";
|
|
19
|
+
import { type GraphIndexResult, type GraphIndexer } from "./graph.js";
|
|
19
20
|
/**
|
|
20
21
|
* How long newly added material may take to become findable — AC-4.
|
|
21
22
|
*
|
|
@@ -59,6 +60,14 @@ export type IndexResult = {
|
|
|
59
60
|
/** Wall-clock, so the freshness target can be measured rather than assumed. */
|
|
60
61
|
readonly elapsedMs: number;
|
|
61
62
|
readonly model: EmbeddingModelRef;
|
|
63
|
+
/**
|
|
64
|
+
* What the graph cost, when GraphRAG ran — REQ-064 (#270), task #271.
|
|
65
|
+
*
|
|
66
|
+
* Always present, never optional, and `ran: false` with every counter zero when it did not. An optional field
|
|
67
|
+
* would let a caller read "no graph work" and "GraphRAG is off" as the same thing, and they are not: the
|
|
68
|
+
* first is a source with nothing extractable, the second is a deployment that never asked.
|
|
69
|
+
*/
|
|
70
|
+
readonly graph: GraphIndexResult;
|
|
62
71
|
};
|
|
63
72
|
export type EmbeddingPipelineDeps = {
|
|
64
73
|
readonly knowledge: KnowledgeStore;
|
|
@@ -69,6 +78,17 @@ export type EmbeddingPipelineDeps = {
|
|
|
69
78
|
/** Injectable so a test measures elapsed time without waiting for it. */
|
|
70
79
|
readonly now?: () => number;
|
|
71
80
|
readonly log?: (message: string, detail?: Readonly<Record<string, unknown>>) => void;
|
|
81
|
+
/**
|
|
82
|
+
* The graph indexer — REQ-064 (#270). **Absent means GraphRAG cannot run at all.**
|
|
83
|
+
*
|
|
84
|
+
* Optional at the type level rather than switched at run time, so a deployment that never enables it supplies
|
|
85
|
+
* no `EntityExtractor` and therefore configures no language model to index a document. That is the outermost
|
|
86
|
+
* of the three gates behind AC-4's "off costs nothing": absent here, off for the tenant, unflagged for the
|
|
87
|
+
* source. The first is free, and the second costs one settings read.
|
|
88
|
+
*/
|
|
89
|
+
readonly graph?: GraphIndexer;
|
|
90
|
+
/** Chunks extracted concurrently. See `DEFAULT_EXTRACTION_CONCURRENCY` — the default is 1, deliberately. */
|
|
91
|
+
readonly graphConcurrency?: number;
|
|
72
92
|
};
|
|
73
93
|
/** Deterministic chunk id, so re-indexing the same source overwrites its own rows rather than duplicating. */
|
|
74
94
|
export declare const chunkId: (sourceType: string, sourceId: string, index: number) => string;
|
|
@@ -117,4 +137,8 @@ export type EmbeddingPipeline = ReturnType<typeof createEmbeddingPipeline>;
|
|
|
117
137
|
export * from "./chunking.js";
|
|
118
138
|
export * from "./retrieval.js";
|
|
119
139
|
export * from "./navigate.js";
|
|
140
|
+
export * from "./graph.js";
|
|
141
|
+
export * from "./graph-retrieval.js";
|
|
142
|
+
export * from "./communities.js";
|
|
143
|
+
export * from "./graph-global.js";
|
|
120
144
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/knowledge/index.js
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
*/
|
|
15
15
|
import { AgentPlatformError } from "../core/errors.js";
|
|
16
16
|
import { DEFAULT_CHUNKING_LIMITS, chunkDocument } from "./chunking.js";
|
|
17
|
+
import { GRAPH_NOT_RUN } from "./graph.js";
|
|
17
18
|
/**
|
|
18
19
|
* How long newly added material may take to become findable — AC-4.
|
|
19
20
|
*
|
|
@@ -54,12 +55,24 @@ export const createEmbeddingPipeline = (deps) => {
|
|
|
54
55
|
sourceType: input.sourceType,
|
|
55
56
|
sourceId: input.sourceId,
|
|
56
57
|
});
|
|
58
|
+
// The graph goes with the content. A document extracted down to nothing must not leave entities
|
|
59
|
+
// asserting what it used to say — the same reasoning as removing its chunks.
|
|
60
|
+
let graph = GRAPH_NOT_RUN;
|
|
61
|
+
if (deps.graph !== undefined && (await deps.graph.shouldIndex(context, input))) {
|
|
62
|
+
const pruned = await deps.graph.indexSource(context, {
|
|
63
|
+
sourceType: input.sourceType,
|
|
64
|
+
sourceId: input.sourceId,
|
|
65
|
+
chunks: [],
|
|
66
|
+
});
|
|
67
|
+
graph = pruned;
|
|
68
|
+
}
|
|
57
69
|
return {
|
|
58
70
|
written: 0,
|
|
59
71
|
removed: cleared.removed,
|
|
60
72
|
batches: 0,
|
|
61
73
|
elapsedMs: now() - started,
|
|
62
74
|
model: deps.embeddings.model,
|
|
75
|
+
graph,
|
|
63
76
|
};
|
|
64
77
|
}
|
|
65
78
|
const embedded = [];
|
|
@@ -98,6 +111,24 @@ export const createEmbeddingPipeline = (deps) => {
|
|
|
98
111
|
sourceId: input.sourceId,
|
|
99
112
|
chunks: embedded,
|
|
100
113
|
});
|
|
114
|
+
/**
|
|
115
|
+
* The graph, after the chunks are written and only if asked — AC-1, AC-4.
|
|
116
|
+
*
|
|
117
|
+
* After, because the graph's provenance is chunk ids: writing edges that point at chunks which then fail
|
|
118
|
+
* to persist would leave the graph asserting sources that are not there.
|
|
119
|
+
*
|
|
120
|
+
* `shouldIndex` is one settings read when GraphRAG is off for the tenant, and zero model calls. When
|
|
121
|
+
* `deps.graph` is absent it is not even that.
|
|
122
|
+
*/
|
|
123
|
+
let graph = GRAPH_NOT_RUN;
|
|
124
|
+
if (deps.graph !== undefined && (await deps.graph.shouldIndex(context, input))) {
|
|
125
|
+
graph = await deps.graph.indexSource(context, {
|
|
126
|
+
sourceType: input.sourceType,
|
|
127
|
+
sourceId: input.sourceId,
|
|
128
|
+
chunks: embedded.map((chunk) => ({ id: chunk.id, content: chunk.content })),
|
|
129
|
+
...(deps.graphConcurrency === undefined ? {} : { concurrency: deps.graphConcurrency }),
|
|
130
|
+
});
|
|
131
|
+
}
|
|
101
132
|
const elapsedMs = now() - started;
|
|
102
133
|
if (elapsedMs > FRESHNESS_TARGET_MS)
|
|
103
134
|
// Reported rather than thrown: the material *is* indexed, and the useful action is to know the target
|
|
@@ -107,7 +138,7 @@ export const createEmbeddingPipeline = (deps) => {
|
|
|
107
138
|
elapsedMs,
|
|
108
139
|
targetMs: FRESHNESS_TARGET_MS,
|
|
109
140
|
});
|
|
110
|
-
return { ...written, batches, elapsedMs, model: deps.embeddings.model };
|
|
141
|
+
return { ...written, batches, elapsedMs, model: deps.embeddings.model, graph };
|
|
111
142
|
},
|
|
112
143
|
/**
|
|
113
144
|
* One page of a re-index — AC-5.
|
|
@@ -164,4 +195,8 @@ export const createEmbeddingPipeline = (deps) => {
|
|
|
164
195
|
export * from "./chunking.js";
|
|
165
196
|
export * from "./retrieval.js";
|
|
166
197
|
export * from "./navigate.js";
|
|
198
|
+
export * from "./graph.js";
|
|
199
|
+
export * from "./graph-retrieval.js";
|
|
200
|
+
export * from "./communities.js";
|
|
201
|
+
export * from "./graph-global.js";
|
|
167
202
|
//# sourceMappingURL=index.js.map
|
|
@@ -41,6 +41,8 @@ import type { TenantId } from "../core/ids.js";
|
|
|
41
41
|
import type { KeywordIndex, KnowledgeChunk, KnowledgeSourceType, VectorIndex } from "../persistence/index.js";
|
|
42
42
|
import type { EmbeddingProvider } from "./index.js";
|
|
43
43
|
import type { Navigator } from "./navigate.js";
|
|
44
|
+
import type { GraphLocalSearch } from "./graph-retrieval.js";
|
|
45
|
+
import type { GraphGlobalSearch } from "./graph-global.js";
|
|
44
46
|
/** The rank-fusion constant. See the note above on why 60 and why rank rather than score. */
|
|
45
47
|
export declare const RRF_K = 60;
|
|
46
48
|
/** How many candidates each signal contributes before fusion. */
|
|
@@ -64,7 +66,14 @@ export declare const DEFAULT_RELEVANCE_FLOOR = 0.4;
|
|
|
64
66
|
*
|
|
65
67
|
* See `navigate.ts` for what it is and `docs/26-retrieval-quality.md` for what it scored.
|
|
66
68
|
*/
|
|
67
|
-
|
|
69
|
+
/**
|
|
70
|
+
* `graph-local` joins the union in REQ-064 (#270), task #273.
|
|
71
|
+
*
|
|
72
|
+
* A closed union read in several places, so a new member is threaded rather than cast past — the same
|
|
73
|
+
* discipline #219 applied when it added `not-configured`. Like `navigate`, it is *delegated whole*: it shares
|
|
74
|
+
* no step with the fusion path, because it answers a different kind of question.
|
|
75
|
+
*/
|
|
76
|
+
export type RetrievalMode = "semantic" | "keyword" | "hybrid" | "navigate" | "graph-local" | "graph-global";
|
|
68
77
|
/** What a citation needs, derived from a hit so there is one shape rather than each caller's own (AC-6). */
|
|
69
78
|
export type SourceReference = {
|
|
70
79
|
readonly sourceType: KnowledgeSourceType;
|
|
@@ -149,6 +158,22 @@ export type RetrieverDeps = {
|
|
|
149
158
|
* mechanism, which is the only way this spike could have done harm.
|
|
150
159
|
*/
|
|
151
160
|
readonly navigator?: Navigator;
|
|
161
|
+
/**
|
|
162
|
+
* Serves `mode: "graph-local"` — REQ-064 (#270), task #273.
|
|
163
|
+
*
|
|
164
|
+
* Optional for the same reason `navigator` is, and its absence is the same named refusal. GraphRAG is opt-in
|
|
165
|
+
* and expensive to index, so a deployment that never enabled it supplies nothing here and asking for the
|
|
166
|
+
* mode says so rather than quietly returning embeddings.
|
|
167
|
+
*/
|
|
168
|
+
readonly graphLocal?: GraphLocalSearch;
|
|
169
|
+
/**
|
|
170
|
+
* Serves `mode: "graph-global"` — REQ-064 (#270), task #274.
|
|
171
|
+
*
|
|
172
|
+
* Separate from `graphLocal` rather than one graph dependency, because they are independently useful: a
|
|
173
|
+
* deployment can run entity-centric retrieval without paying for community summaries at all, and #273 landed
|
|
174
|
+
* before #272 for exactly that reason.
|
|
175
|
+
*/
|
|
176
|
+
readonly graphGlobal?: GraphGlobalSearch;
|
|
152
177
|
};
|
|
153
178
|
export type RetrieveInput = {
|
|
154
179
|
readonly query: string;
|
|
@@ -63,11 +63,17 @@ export const NO_RESULT_REASONS = [
|
|
|
63
63
|
"below-threshold",
|
|
64
64
|
"no-access",
|
|
65
65
|
/**
|
|
66
|
-
* The mode asked for is not wired
|
|
66
|
+
* The mode asked for is not wired.
|
|
67
67
|
*
|
|
68
68
|
* Its own reason rather than `no-match`, because the two want opposite responses: one says rephrase, this says
|
|
69
69
|
* a deployment has not configured what you asked for. Falling back to another mode silently would be worse
|
|
70
70
|
* than either, since the caller would attribute the results to the mode it named.
|
|
71
|
+
*
|
|
72
|
+
* **Live, not a placeholder** — audited for #256 AC-8. Three code paths return it (`navigate` with no
|
|
73
|
+
* navigator, `graph-local` and `graph-global` with no GraphRAG), and three test files assert it. The optional
|
|
74
|
+
* modes are exactly why it has to exist: GraphRAG ships **off by default** on the measured evidence in
|
|
75
|
+
* `docs/29`, so "you asked for a mode this deployment did not switch on" is an ordinary answer rather than an
|
|
76
|
+
* edge case. The comment previously cited task #219 as if it were pending; it closed.
|
|
71
77
|
*/
|
|
72
78
|
"not-configured",
|
|
73
79
|
];
|
|
@@ -119,6 +125,76 @@ export const createRetriever = (deps) => {
|
|
|
119
125
|
rerankerId: deps.reranker?.id ?? null,
|
|
120
126
|
async retrieve(context, input) {
|
|
121
127
|
const mode = input.mode ?? "hybrid";
|
|
128
|
+
/**
|
|
129
|
+
* The graph mode, delegated whole — task #273.
|
|
130
|
+
*
|
|
131
|
+
* Before the `authSubjects` check below only because it does its own: an empty subject list means the
|
|
132
|
+
* same thing here, and the graph search returns an empty result for it rather than traversing first and
|
|
133
|
+
* filtering after. Placed with `navigate` because both are delegated modes rather than fusion inputs.
|
|
134
|
+
*/
|
|
135
|
+
if (mode === "graph-local") {
|
|
136
|
+
if (deps.graphLocal === undefined)
|
|
137
|
+
return { found: false, reason: "not-configured", message: NO_RESULT_MESSAGES["not-configured"], mode };
|
|
138
|
+
if (input.authSubjects.length === 0)
|
|
139
|
+
return { found: false, reason: "no-access", message: NO_RESULT_MESSAGES["no-access"], mode };
|
|
140
|
+
const result = await deps.graphLocal.search(context, {
|
|
141
|
+
query: input.query,
|
|
142
|
+
authSubjects: input.authSubjects,
|
|
143
|
+
limit: input.limit,
|
|
144
|
+
...(input.sourceTypes === undefined ? {} : { sourceTypes: input.sourceTypes }),
|
|
145
|
+
});
|
|
146
|
+
if (result.hits.length === 0)
|
|
147
|
+
/**
|
|
148
|
+
* An honest empty result, and the reason distinguishes the two ways it happens.
|
|
149
|
+
*
|
|
150
|
+
* Nothing in the question named an entity the graph knows is `no-match` — rephrasing might work.
|
|
151
|
+
* Entities matched but reached no readable chunk is also `no-match` rather than `no-access`, because
|
|
152
|
+
* the principal may genuinely have access to a corpus that simply says nothing more.
|
|
153
|
+
*/
|
|
154
|
+
return { found: false, reason: "no-match", message: NO_RESULT_MESSAGES["no-match"], mode };
|
|
155
|
+
return {
|
|
156
|
+
found: true,
|
|
157
|
+
mode,
|
|
158
|
+
hits: result.hits.map((hit) => ({
|
|
159
|
+
chunk: hit.chunk,
|
|
160
|
+
score: hit.score,
|
|
161
|
+
signals: ["graph-local"],
|
|
162
|
+
reference: referenceFor(hit.chunk),
|
|
163
|
+
})),
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
167
|
+
* The corpus-level mode — task #274.
|
|
168
|
+
*
|
|
169
|
+
* Its cost ceiling **throws** rather than returning an outcome, and that is deliberate: exceeding it is
|
|
170
|
+
* not "found nothing", it is "this question is too expensive to answer this way", and the two want
|
|
171
|
+
* different responses. `RetrievalOutcome`'s `found: false` reasons are all about the corpus; this is
|
|
172
|
+
* about the request.
|
|
173
|
+
*/
|
|
174
|
+
if (mode === "graph-global") {
|
|
175
|
+
if (deps.graphGlobal === undefined)
|
|
176
|
+
return { found: false, reason: "not-configured", message: NO_RESULT_MESSAGES["not-configured"], mode };
|
|
177
|
+
if (input.authSubjects.length === 0)
|
|
178
|
+
return { found: false, reason: "no-access", message: NO_RESULT_MESSAGES["no-access"], mode };
|
|
179
|
+
const result = await deps.graphGlobal.search(context, {
|
|
180
|
+
query: input.query,
|
|
181
|
+
authSubjects: input.authSubjects,
|
|
182
|
+
limit: input.limit,
|
|
183
|
+
...(input.sourceTypes === undefined ? {} : { sourceTypes: input.sourceTypes }),
|
|
184
|
+
});
|
|
185
|
+
if (result.hits.length === 0)
|
|
186
|
+
return { found: false, reason: "no-match", message: NO_RESULT_MESSAGES["no-match"], mode };
|
|
187
|
+
return {
|
|
188
|
+
found: true,
|
|
189
|
+
mode,
|
|
190
|
+
hits: result.hits.map((hit) => ({
|
|
191
|
+
chunk: hit.chunk,
|
|
192
|
+
score: hit.score,
|
|
193
|
+
signals: ["graph-global"],
|
|
194
|
+
reference: referenceFor(hit.chunk),
|
|
195
|
+
})),
|
|
196
|
+
};
|
|
197
|
+
}
|
|
122
198
|
// The spike's mode, delegated whole: it shares no step with the fusion path below.
|
|
123
199
|
if (mode === "navigate") {
|
|
124
200
|
if (deps.navigator === undefined)
|