@retinue/agentkit 0.1.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 +59 -277
- 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/embeddings/openai.d.ts +45 -0
- package/dist/adapters/embeddings/openai.js +109 -0
- 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 +99 -1
- package/dist/agents/agent.js +215 -16
- package/dist/agents/define.js +24 -4
- package/dist/agents/engine.d.ts +48 -0
- package/dist/agents/engine.js +370 -13
- package/dist/audio/index.d.ts +151 -0
- package/dist/audio/index.js +183 -0
- package/dist/capabilities/index.d.ts +5 -1
- package/dist/capabilities/index.js +23 -0
- package/dist/capabilities/runtime.d.ts +8 -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/budget.d.ts +55 -0
- package/dist/core/budget.js +56 -0
- package/dist/core/content-parts.d.ts +26 -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 +110 -2
- package/dist/core/events.js +33 -2
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.js +1 -0
- package/dist/core/validation.js +19 -0
- package/dist/documents/index.d.ts +14 -0
- package/dist/documents/parsers/text.d.ts +16 -0
- package/dist/documents/parsers/text.js +54 -2
- package/dist/entries/connections.d.ts +10 -0
- package/dist/entries/connections.js +10 -0
- package/dist/entries/guardrails.d.ts +14 -0
- package/dist/entries/guardrails.js +14 -0
- package/dist/entries/knowledge.d.ts +17 -0
- package/dist/entries/knowledge.js +16 -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 +7 -1
- package/dist/graphql/resolvers.js +20 -0
- package/dist/graphql/schema.d.ts +1 -1
- package/dist/graphql/schema.js +44 -0
- package/dist/guardrails/index.d.ts +115 -0
- package/dist/guardrails/index.js +108 -0
- package/dist/guardrails/moderation.d.ts +53 -0
- package/dist/guardrails/moderation.js +75 -0
- package/dist/guardrails/pii.d.ts +75 -0
- package/dist/guardrails/pii.js +193 -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 +25 -0
- package/dist/knowledge/index.js +37 -1
- package/dist/knowledge/navigate.d.ts +89 -0
- package/dist/knowledge/navigate.js +107 -0
- package/dist/knowledge/retrieval.d.ts +98 -5
- package/dist/knowledge/retrieval.js +158 -28
- 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 +111 -1
- package/dist/models/streaming.js +184 -3
- 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 +55 -0
- package/dist/security/findings.js +18 -9
- 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/catalogue.d.ts +49 -0
- package/dist/skills/catalogue.js +61 -0
- package/dist/skills/context.d.ts +61 -0
- package/dist/skills/context.js +107 -0
- package/dist/skills/index.d.ts +2 -0
- package/dist/skills/index.js +2 -0
- package/dist/telemetry/spans.js +17 -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/files.d.ts +125 -0
- package/dist/toolkit/files.js +320 -0
- package/dist/toolkit/http.d.ts +27 -0
- package/dist/toolkit/http.js +48 -0
- package/dist/toolkit/index.d.ts +31 -1
- package/dist/toolkit/index.js +26 -1
- package/dist/toolkit/mime.d.ts +97 -0
- package/dist/toolkit/mime.js +233 -0
- package/dist/toolkit/sandbox.d.ts +119 -0
- package/dist/toolkit/sandbox.js +239 -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/toolkit/web.d.ts +13 -0
- package/dist/toolkit/web.js +7 -1
- package/dist/tools/budget.d.ts +28 -0
- package/dist/tools/budget.js +35 -0
- package/dist/tools/credentials.d.ts +316 -0
- package/dist/tools/credentials.js +369 -0
- package/dist/tools/define.d.ts +45 -0
- package/dist/tools/define.js +26 -0
- package/dist/tools/find.d.ts +109 -0
- package/dist/tools/find.js +210 -0
- package/dist/tools/index.d.ts +35 -2
- package/dist/tools/index.js +11 -0
- package/dist/tools/library/audio.d.ts +63 -0
- package/dist/tools/library/audio.js +148 -0
- package/dist/tools/library/fs.d.ts +24 -0
- package/dist/tools/library/fs.js +102 -0
- package/dist/tools/library/index.d.ts +37 -2
- package/dist/tools/library/index.js +47 -0
- package/dist/tools/library/shell.d.ts +45 -0
- package/dist/tools/library/shell.js +70 -0
- package/dist/tools/meta-tools.js +8 -0
- package/dist/tools/registry.d.ts +146 -0
- package/dist/tools/registry.js +250 -4
- 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 +33 -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;
|
|
@@ -116,4 +136,9 @@ export declare const createEmbeddingPipeline: (deps: EmbeddingPipelineDeps) => {
|
|
|
116
136
|
export type EmbeddingPipeline = ReturnType<typeof createEmbeddingPipeline>;
|
|
117
137
|
export * from "./chunking.js";
|
|
118
138
|
export * from "./retrieval.js";
|
|
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";
|
|
119
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.
|
|
@@ -163,4 +194,9 @@ export const createEmbeddingPipeline = (deps) => {
|
|
|
163
194
|
};
|
|
164
195
|
export * from "./chunking.js";
|
|
165
196
|
export * from "./retrieval.js";
|
|
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";
|
|
166
202
|
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Retrieval without vectors — REQ-050 (#209), task #219, AC-4.
|
|
3
|
+
*
|
|
4
|
+
* A **spike**, and the deliverable is a decision with numbers rather than a subsystem. See
|
|
5
|
+
* `docs/26-retrieval-quality.md` for what it scored.
|
|
6
|
+
*
|
|
7
|
+
* ## The idea being tested
|
|
8
|
+
*
|
|
9
|
+
* Embedding-based retrieval matches a query against fragments of text and hopes the fragments it surfaces are the
|
|
10
|
+
* ones that answer it. A person looking something up in a manual does something else entirely: they read the
|
|
11
|
+
* table of contents, decide which chapter is relevant, and then read it. That needs no index, no embedding cost
|
|
12
|
+
* and no re-indexing when a document changes — and its citations name a *document* somebody chose rather than a
|
|
13
|
+
* fragment a cosine distance surfaced.
|
|
14
|
+
*
|
|
15
|
+
* The cost is a model call per query, and latency measured in seconds rather than milliseconds.
|
|
16
|
+
*
|
|
17
|
+
* ## Two things this prototype found immediately
|
|
18
|
+
*
|
|
19
|
+
* **`KnowledgeStore` cannot enumerate its sources.** There is `listBySource`, `get`, `deleteSource` and
|
|
20
|
+
* `staleSources`, and no way to ask "what documents are in here". That is correct for a vector-based design —
|
|
21
|
+
* nothing needed it — and it is exactly what a navigating retriever needs first. So the outline arrives through
|
|
22
|
+
* a port the *host* supplies (`OutlineCatalogue`), which is honest but means this mode is not a drop-in for a
|
|
23
|
+
* deployment that already has hybrid retrieval working.
|
|
24
|
+
*
|
|
25
|
+
* **It does fit behind `RetrievalMode`**, which the issue asked to test. `createRetriever` gains one optional
|
|
26
|
+
* dependency and a fourth mode; every caller — `search_knowledge` included — is unchanged, and a deployment that
|
|
27
|
+
* has not wired a navigator gets a named refusal rather than a silent fall back to semantic search. If it had
|
|
28
|
+
* *not* fit, that would have been the finding; it fits.
|
|
29
|
+
*/
|
|
30
|
+
import type { ExecutionContext } from "../core/context.js";
|
|
31
|
+
import type { KnowledgeChunk, KnowledgeStore } from "../persistence/index.js";
|
|
32
|
+
import type { RetrievalOutcome } from "./retrieval.js";
|
|
33
|
+
/** What a chooser sees of one document: enough to decide, and not the document itself. */
|
|
34
|
+
export type SourceOutline = {
|
|
35
|
+
readonly sourceType: KnowledgeChunk["sourceType"];
|
|
36
|
+
readonly sourceId: string;
|
|
37
|
+
readonly title: string;
|
|
38
|
+
/** The heading trail, in document order. This is the table of contents a person would read. */
|
|
39
|
+
readonly headings: readonly string[];
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* Where the outline comes from.
|
|
43
|
+
*
|
|
44
|
+
* A port because the store cannot answer it (see the note above). A host that keeps documents in its own tables —
|
|
45
|
+
* which most do, since `KnowledgeStore` holds *chunks* — already has this list.
|
|
46
|
+
*/
|
|
47
|
+
export interface OutlineCatalogue {
|
|
48
|
+
list(context: {
|
|
49
|
+
readonly tenantId: ExecutionContext["tenantId"];
|
|
50
|
+
readonly authSubjects: readonly string[];
|
|
51
|
+
}): Promise<readonly SourceOutline[]>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Whatever decides which documents to read. A model, in practice.
|
|
55
|
+
*
|
|
56
|
+
* A port rather than a model call, for the reason every model call in this package is a port: the platform must
|
|
57
|
+
* not acquire a provider, and a test must be able to pin the choice.
|
|
58
|
+
*/
|
|
59
|
+
export interface DocumentChooser {
|
|
60
|
+
readonly id: string;
|
|
61
|
+
choose(input: {
|
|
62
|
+
readonly query: string;
|
|
63
|
+
readonly catalogue: readonly SourceOutline[];
|
|
64
|
+
readonly limit: number;
|
|
65
|
+
}): Promise<readonly string[]>;
|
|
66
|
+
}
|
|
67
|
+
export type NavigatorDeps = {
|
|
68
|
+
readonly store: KnowledgeStore;
|
|
69
|
+
readonly catalogue: OutlineCatalogue;
|
|
70
|
+
readonly chooser: DocumentChooser;
|
|
71
|
+
/** Documents the chooser may pick. More than a handful and the model is guessing rather than choosing. */
|
|
72
|
+
readonly maxSources?: number;
|
|
73
|
+
/** Chunks read per chosen document. A whole 200-page document would not fit the caller's context. */
|
|
74
|
+
readonly maxChunksPerSource?: number;
|
|
75
|
+
};
|
|
76
|
+
export declare const DEFAULT_MAX_SOURCES = 3;
|
|
77
|
+
export declare const DEFAULT_MAX_CHUNKS_PER_SOURCE = 40;
|
|
78
|
+
export interface Navigator {
|
|
79
|
+
readonly id: string;
|
|
80
|
+
navigate(context: {
|
|
81
|
+
readonly tenantId: ExecutionContext["tenantId"];
|
|
82
|
+
}, input: {
|
|
83
|
+
readonly query: string;
|
|
84
|
+
readonly authSubjects: readonly string[];
|
|
85
|
+
readonly limit: number;
|
|
86
|
+
}): Promise<RetrievalOutcome>;
|
|
87
|
+
}
|
|
88
|
+
export declare const createNavigator: (deps: NavigatorDeps) => Navigator;
|
|
89
|
+
//# sourceMappingURL=navigate.d.ts.map
|