@retinue/agentkit 0.2.0 → 0.3.1
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 +72 -0
- package/dist/server/bin.js +304 -0
- package/dist/server/boot.d.ts +20 -2
- package/dist/server/boot.js +29 -3
- package/dist/server/cli-worker.js +6 -3
- package/dist/server/cli.d.ts +13 -0
- package/dist/server/cli.js +10 -5
- package/dist/server/config.d.ts +11 -1
- package/dist/server/config.js +18 -0
- package/dist/server/doctor.d.ts +85 -0
- package/dist/server/doctor.js +280 -0
- package/dist/server/pool.d.ts +47 -0
- package/dist/server/pool.js +73 -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,246 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `graph-global` — map-reduce over community summaries. REQ-064 (#270), task #274.
|
|
3
|
+
*
|
|
4
|
+
* The mode that answers *"what are the main themes in our incident reports?"* — a question that requires having
|
|
5
|
+
* read everything, which top-k retrieval by construction does not.
|
|
6
|
+
*
|
|
7
|
+
* ## The output shape, decided — AC-7
|
|
8
|
+
*
|
|
9
|
+
* The issue left this open, and it is the one real design question here. Three candidates:
|
|
10
|
+
*
|
|
11
|
+
* 1. **A synthesised answer** in a new shape. Most honest about what happened, and it breaks every existing
|
|
12
|
+
* consumer of `RetrievalOutcome` — citations, the empty-result union, the tools above.
|
|
13
|
+
* 2. **Synthetic chunks** whose content is the community summary. Tempting and wrong: a `KnowledgeChunk` has an
|
|
14
|
+
* id that resolves and a source that exists, and a fabricated one produces a citation pointing at nothing.
|
|
15
|
+
* The model would present a generated summary as though a document said it, which is the failure provenance
|
|
16
|
+
* exists to prevent.
|
|
17
|
+
* 3. **Real chunk hits from the communities the map phase selected.** Citations resolve, every consumer works
|
|
18
|
+
* unchanged, and the corpus-level reasoning is real — it happened in *choosing which communities matter*,
|
|
19
|
+
* which is what reading everything buys you.
|
|
20
|
+
*
|
|
21
|
+
* **Three**, and the summaries are not discarded: they come back on `GraphGlobalResult` alongside coverage, for
|
|
22
|
+
* a caller that wants the thematic answer rather than the passages behind it. What this mode does *not* do is
|
|
23
|
+
* hand the model a summary dressed as a source.
|
|
24
|
+
*
|
|
25
|
+
* ## The ceiling is the design, not a guard bolted on
|
|
26
|
+
*
|
|
27
|
+
* Cost scales with the number of communities, which scales with the corpus. A large tenant's global query could
|
|
28
|
+
* issue hundreds of model calls. So the ceiling is checked **before spending**, from the community count — a
|
|
29
|
+
* refusal that arrives after two hundred calls is not a ceiling, it is a bill with an apology.
|
|
30
|
+
*
|
|
31
|
+
* And it **refuses** rather than truncating. A partial map-reduce presented as a whole-corpus answer is a wrong
|
|
32
|
+
* answer with a confident shape, which is worse than no answer: "the main themes are X and Y" is false if it
|
|
33
|
+
* read a third of the corpus, and nothing in the sentence says so.
|
|
34
|
+
*/
|
|
35
|
+
import { AgentPlatformError } from "../core/errors.js";
|
|
36
|
+
import { isCommunityStale, } from "../persistence/index.js";
|
|
37
|
+
/**
|
|
38
|
+
* How many model calls one global query may make, by default.
|
|
39
|
+
*
|
|
40
|
+
* Forty is a corpus of a few hundred documents at a coarse level. It is deliberately a number somebody has to
|
|
41
|
+
* raise consciously: the failure this bounds is not a slow query, it is a tenant growing until one question
|
|
42
|
+
* costs more than a day of ordinary use.
|
|
43
|
+
*/
|
|
44
|
+
export const DEFAULT_GLOBAL_CALL_CEILING = 40;
|
|
45
|
+
/** A companion ceiling, because a small number of enormous summaries costs as much as many small ones. */
|
|
46
|
+
export const DEFAULT_GLOBAL_TOKEN_CEILING = 120_000;
|
|
47
|
+
/** Below this the map phase considered the community irrelevant and its chunks are not gathered. */
|
|
48
|
+
export const DEFAULT_RELEVANCE_SCORE = 1;
|
|
49
|
+
/**
|
|
50
|
+
* Which level of the hierarchy to read.
|
|
51
|
+
*
|
|
52
|
+
* The coarsest available, by default. Reading every level multiplies cost for largely repeated content — a
|
|
53
|
+
* level-1 community contains the level-0 ones under it, so summarising both says the same thing twice at twice
|
|
54
|
+
* the price. Coarse suits "what are the main themes"; a caller with a narrower question passes a finer level.
|
|
55
|
+
*
|
|
56
|
+
* Exported because the choice is worth testing on its own: picking the wrong level is the difference between
|
|
57
|
+
* an answer about the corpus and an answer about one team.
|
|
58
|
+
*/
|
|
59
|
+
export const chooseLevel = (available, requested) => {
|
|
60
|
+
if (available.length === 0)
|
|
61
|
+
return 0;
|
|
62
|
+
if (requested !== undefined && available.includes(requested))
|
|
63
|
+
return requested;
|
|
64
|
+
return Math.max(...available);
|
|
65
|
+
};
|
|
66
|
+
export const createGraphGlobalSearch = (deps) => {
|
|
67
|
+
const callCeiling = Math.max(1, deps.callCeiling ?? DEFAULT_GLOBAL_CALL_CEILING);
|
|
68
|
+
const tokenCeiling = Math.max(1, deps.tokenCeiling ?? DEFAULT_GLOBAL_TOKEN_CEILING);
|
|
69
|
+
const minScore = deps.minScore ?? DEFAULT_RELEVANCE_SCORE;
|
|
70
|
+
const chunksPerCommunity = Math.max(1, deps.chunksPerCommunity ?? 3);
|
|
71
|
+
const now = deps.now ?? (() => Date.now());
|
|
72
|
+
const allCommunities = async (tenantId) => {
|
|
73
|
+
const out = [];
|
|
74
|
+
let cursor;
|
|
75
|
+
do {
|
|
76
|
+
const page = await deps.graph.listCommunities({
|
|
77
|
+
tenantId,
|
|
78
|
+
limit: 200,
|
|
79
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
80
|
+
});
|
|
81
|
+
out.push(...page.items);
|
|
82
|
+
cursor = page.nextCursor;
|
|
83
|
+
} while (cursor !== undefined);
|
|
84
|
+
return out;
|
|
85
|
+
};
|
|
86
|
+
return {
|
|
87
|
+
async search(context, input) {
|
|
88
|
+
const started = now();
|
|
89
|
+
const everything = await allCommunities(context.tenantId);
|
|
90
|
+
const level = chooseLevel([...new Set(everything.map((c) => c.level))], input.level ?? deps.level);
|
|
91
|
+
const atLevel = everything.filter((community) => community.level === level);
|
|
92
|
+
const empty = {
|
|
93
|
+
hits: [],
|
|
94
|
+
themes: [],
|
|
95
|
+
coverage: {
|
|
96
|
+
level,
|
|
97
|
+
communitiesTotal: atLevel.length,
|
|
98
|
+
communitiesRead: 0,
|
|
99
|
+
communitiesRelevant: 0,
|
|
100
|
+
communitiesUnsummarised: atLevel.filter((c) => c.summary === undefined).length,
|
|
101
|
+
staleSummaries: 0,
|
|
102
|
+
calls: 0,
|
|
103
|
+
inputTokens: 0,
|
|
104
|
+
outputTokens: 0,
|
|
105
|
+
elapsedMs: now() - started,
|
|
106
|
+
},
|
|
107
|
+
};
|
|
108
|
+
if (input.authSubjects.length === 0 || atLevel.length === 0)
|
|
109
|
+
return empty;
|
|
110
|
+
// Only summarised communities can be mapped. Counted rather than quietly skipped — a corpus that is half
|
|
111
|
+
// unsummarised produces a partial answer, and the caller has to be able to see that.
|
|
112
|
+
const readable = atLevel.filter((community) => community.summary !== undefined);
|
|
113
|
+
if (readable.length === 0)
|
|
114
|
+
return empty;
|
|
115
|
+
/**
|
|
116
|
+
* **The ceiling, checked before spending.** AC-3, AC-4.
|
|
117
|
+
*
|
|
118
|
+
* From the community count, which is known without calling anything. A check after the loop, or inside
|
|
119
|
+
* it, would refuse having already spent most of what it was refusing.
|
|
120
|
+
*/
|
|
121
|
+
if (readable.length > callCeiling) {
|
|
122
|
+
throw new AgentPlatformError({
|
|
123
|
+
code: "budget_exceeded",
|
|
124
|
+
message: `A corpus-wide answer at level ${level} would read ${readable.length} community summaries, and the ` +
|
|
125
|
+
`ceiling is ${callCeiling} model calls. Raise the ceiling, ask at a coarser level, or narrow the ` +
|
|
126
|
+
"question so graph-local can answer it. Nothing was spent.",
|
|
127
|
+
retryable: false,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
let calls = 0;
|
|
131
|
+
let inputTokens = 0;
|
|
132
|
+
let outputTokens = 0;
|
|
133
|
+
const scored = [];
|
|
134
|
+
// Sorted, so a run is reproducible and a token-ceiling stop cuts at the same place.
|
|
135
|
+
for (const community of [...readable].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
|
|
136
|
+
const { relevance, usage } = await deps.mapper.map({ query: input.query, community });
|
|
137
|
+
calls += 1;
|
|
138
|
+
inputTokens += usage?.inputTokens ?? 0;
|
|
139
|
+
outputTokens += usage?.outputTokens ?? 0;
|
|
140
|
+
scored.push({ community, relevance });
|
|
141
|
+
/**
|
|
142
|
+
* The token ceiling can only be checked *during* the loop — summary sizes are not knowable up front —
|
|
143
|
+
* so unlike the call ceiling this one can be reached mid-flight. It still refuses rather than
|
|
144
|
+
* returning what it has, for the same reason: a partial map-reduce presented as a whole-corpus answer
|
|
145
|
+
* is confidently wrong.
|
|
146
|
+
*/
|
|
147
|
+
if (inputTokens + outputTokens > tokenCeiling) {
|
|
148
|
+
throw new AgentPlatformError({
|
|
149
|
+
code: "budget_exceeded",
|
|
150
|
+
message: `A corpus-wide answer at level ${level} passed the ${tokenCeiling}-token ceiling after ` +
|
|
151
|
+
`${calls} of ${readable.length} community summaries. Raise the ceiling or ask at a coarser level. ` +
|
|
152
|
+
"A partial reading is not reported as a whole-corpus answer.",
|
|
153
|
+
retryable: false,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
const relevant = scored
|
|
158
|
+
.filter((entry) => entry.relevance.score >= minScore)
|
|
159
|
+
// Best first; ties by id so the order is total and the result reproducible.
|
|
160
|
+
.sort((a, b) => b.relevance.score - a.relevance.score || (a.community.id < b.community.id ? -1 : 1));
|
|
161
|
+
const allowed = new Set(input.authSubjects);
|
|
162
|
+
const wantedTypes = input.sourceTypes === undefined ? null : new Set(input.sourceTypes);
|
|
163
|
+
// Mutable locals, widened from the readonly result type — the arrays are built here and frozen by
|
|
164
|
+
// the return type, which is the shape every other result in this module uses.
|
|
165
|
+
const hits = [];
|
|
166
|
+
const best = relevant[0]?.relevance.score ?? 1;
|
|
167
|
+
for (const entry of relevant) {
|
|
168
|
+
let taken = 0;
|
|
169
|
+
for (const chunkId of entry.community.chunkIds) {
|
|
170
|
+
if (taken >= chunksPerCommunity || hits.length >= input.limit)
|
|
171
|
+
break;
|
|
172
|
+
const chunk = await deps.knowledge.get({ tenantId: context.tenantId, id: chunkId });
|
|
173
|
+
if (chunk === null)
|
|
174
|
+
continue;
|
|
175
|
+
/**
|
|
176
|
+
* AC-8. The same primary-key read `graph-local` guards, and the same reasoning: a community spans
|
|
177
|
+
* whatever its entities span, including sources this principal cannot read.
|
|
178
|
+
*
|
|
179
|
+
* The *summary* is the harder half of AC-8 and is handled below — see `themes`.
|
|
180
|
+
*/
|
|
181
|
+
if (!allowed.has(chunk.authSubject))
|
|
182
|
+
continue;
|
|
183
|
+
if (wantedTypes !== null && !wantedTypes.has(chunk.sourceType))
|
|
184
|
+
continue;
|
|
185
|
+
hits.push({
|
|
186
|
+
chunk,
|
|
187
|
+
score: best === 0 ? 0 : entry.relevance.score / best,
|
|
188
|
+
communityId: entry.community.id,
|
|
189
|
+
});
|
|
190
|
+
taken += 1;
|
|
191
|
+
}
|
|
192
|
+
if (hits.length >= input.limit)
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* **The summary leak, and what is actually done about it — AC-8.**
|
|
197
|
+
*
|
|
198
|
+
* A community summary is generated text over every chunk in the community, so it can paraphrase a
|
|
199
|
+
* document this principal cannot read. Filtering the summary is not possible: it is prose, and there is
|
|
200
|
+
* no way to remove one source's contribution from a sentence after the fact.
|
|
201
|
+
*
|
|
202
|
+
* So a theme is returned **only when the principal can read every chunk behind it**. That is stricter
|
|
203
|
+
* than necessary — a summary may not in fact mention the restricted source — and it is the only rule
|
|
204
|
+
* that cannot leak. The alternative, returning it and hoping, is exactly the kind of "probably fine" that
|
|
205
|
+
* a permission boundary must not be built on.
|
|
206
|
+
*
|
|
207
|
+
* The cost is real and worth naming: on a corpus with mixed permissions, a principal sees fewer themes
|
|
208
|
+
* than a colleague, and the coverage numbers say how many. That is a comprehensible answer. A summary
|
|
209
|
+
* quoting a document they were never allowed to open is not.
|
|
210
|
+
*/
|
|
211
|
+
const themes = [];
|
|
212
|
+
for (const entry of relevant) {
|
|
213
|
+
const chunks = await Promise.all(entry.community.chunkIds.map((id) => deps.knowledge.get({ tenantId: context.tenantId, id })));
|
|
214
|
+
const readableToCaller = chunks.every((chunk) => chunk === null || allowed.has(chunk.authSubject));
|
|
215
|
+
if (!readableToCaller)
|
|
216
|
+
continue;
|
|
217
|
+
themes.push({
|
|
218
|
+
communityId: entry.community.id,
|
|
219
|
+
summary: entry.community.summary,
|
|
220
|
+
score: entry.relevance.score,
|
|
221
|
+
points: entry.relevance.points,
|
|
222
|
+
// Disclosed rather than hidden: an answer built on summaries written before the last three documents
|
|
223
|
+
// landed is defensible; presenting it as current is not.
|
|
224
|
+
stale: isCommunityStale(entry.community),
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
return {
|
|
228
|
+
hits,
|
|
229
|
+
themes,
|
|
230
|
+
coverage: {
|
|
231
|
+
level,
|
|
232
|
+
communitiesTotal: atLevel.length,
|
|
233
|
+
communitiesRead: calls,
|
|
234
|
+
communitiesRelevant: relevant.length,
|
|
235
|
+
communitiesUnsummarised: atLevel.length - readable.length,
|
|
236
|
+
staleSummaries: themes.filter((theme) => theme.stale).length,
|
|
237
|
+
calls,
|
|
238
|
+
inputTokens,
|
|
239
|
+
outputTokens,
|
|
240
|
+
elapsedMs: now() - started,
|
|
241
|
+
},
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
};
|
|
245
|
+
};
|
|
246
|
+
//# sourceMappingURL=graph-global.js.map
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `graph-local` — entity-centric retrieval. REQ-064 (#270), task #273.
|
|
3
|
+
*
|
|
4
|
+
* The mode that answers *"which teams depend on the retry budget?"* — a question whose answer exists in no
|
|
5
|
+
* single chunk. Semantic search finds chunks that look like the question; this finds the chunks that are
|
|
6
|
+
* *connected to what the question is about*.
|
|
7
|
+
*
|
|
8
|
+
* Four steps, and the interesting decisions are in the first and the last:
|
|
9
|
+
*
|
|
10
|
+
* 1. **Resolve** the entities the question names.
|
|
11
|
+
* 2. **Traverse** their neighbourhood, bounded.
|
|
12
|
+
* 3. **Gather** the chunks those entities and edges came from.
|
|
13
|
+
* 4. **Filter and rank**, returning ordinary `RetrievalHit`s.
|
|
14
|
+
*
|
|
15
|
+
* ## Why the query is swept for n-grams rather than read by a model
|
|
16
|
+
*
|
|
17
|
+
* The obvious way to find the entities in a question is to ask a model. It costs a call per query, it is not
|
|
18
|
+
* reproducible across model versions, and #275 has to measure this against a fixed baseline.
|
|
19
|
+
*
|
|
20
|
+
* So instead every 1-to-4 word window of the question is normalised and looked up, and **the graph itself is
|
|
21
|
+
* the filter** — a window that is not an entity simply matches nothing. That is deterministic, free, and uses
|
|
22
|
+
* exactly the normalisation the index used, which is what makes AC-3 hold by construction rather than by two
|
|
23
|
+
* functions agreeing.
|
|
24
|
+
*
|
|
25
|
+
* It will miss an entity the question refers to obliquely ("the budget thing"). That is a real limit, it is
|
|
26
|
+
* measurable, and #275 is where it gets measured.
|
|
27
|
+
*
|
|
28
|
+
* ## Why there is no fallback to semantic search
|
|
29
|
+
*
|
|
30
|
+
* When nothing resolves, this returns an honest empty result. Falling back would mean a caller attributing
|
|
31
|
+
* semantic results to `graph-local` — which is precisely why `not-configured` exists as its own reason rather
|
|
32
|
+
* than as `no-match`, and the same reasoning applies one level down.
|
|
33
|
+
*/
|
|
34
|
+
import type { TenantId } from "../core/ids.js";
|
|
35
|
+
import type { GraphStore, KnowledgeChunk, KnowledgeRelationship, KnowledgeSourceType, KnowledgeStore } from "../persistence/index.js";
|
|
36
|
+
import type { EmbeddingProvider } from "./index.js";
|
|
37
|
+
/**
|
|
38
|
+
* How far a traversal goes by default.
|
|
39
|
+
*
|
|
40
|
+
* One hop is often too few — "which teams depend on the retry budget" is one hop, but "what does the retry
|
|
41
|
+
* budget affect downstream" is two. Three is a large fraction of a connected graph, and the cost is not the
|
|
42
|
+
* traversal but the chunks it drags in. Two is the useful middle, and it is a parameter because the right
|
|
43
|
+
* answer is corpus-shaped.
|
|
44
|
+
*/
|
|
45
|
+
export declare const DEFAULT_GRAPH_DEPTH = 2;
|
|
46
|
+
/** Edges considered per hop. A hub entity can have hundreds; taking all of them buries the specific ones. */
|
|
47
|
+
export declare const DEFAULT_NEIGHBOUR_LIMIT = 50;
|
|
48
|
+
/** The longest phrase treated as a possible entity name. Four words covers "the retry budget policy". */
|
|
49
|
+
export declare const MAX_ENTITY_NGRAM = 4;
|
|
50
|
+
/**
|
|
51
|
+
* Every phrase in the query that could name an entity, normalised and deduplicated.
|
|
52
|
+
*
|
|
53
|
+
* Longest first, which matters for ranking rather than correctness: "retry budget policy" is a more specific
|
|
54
|
+
* match than "retry budget", and a caller that stops early should see the specific one.
|
|
55
|
+
*
|
|
56
|
+
* Exported for its own test — this is the function that decides whether a real question finds anything, and
|
|
57
|
+
* its failure mode is silent (an empty result that looks like an empty corpus).
|
|
58
|
+
*/
|
|
59
|
+
export declare const entityCandidates: (query: string) => readonly string[];
|
|
60
|
+
/** A chunk reached through the graph, with the reason it was reached. */
|
|
61
|
+
export type GraphChunkHit = {
|
|
62
|
+
readonly chunk: KnowledgeChunk;
|
|
63
|
+
/** 0–1 relative to the best hit in this result, as every other mode's score is. */
|
|
64
|
+
readonly score: number;
|
|
65
|
+
/** The entities that led here, so the answer can say *why* this chunk is relevant. */
|
|
66
|
+
readonly viaEntities: readonly string[];
|
|
67
|
+
};
|
|
68
|
+
export type GraphLocalResult = {
|
|
69
|
+
readonly hits: readonly GraphChunkHit[];
|
|
70
|
+
/** The entities the question resolved to. Empty means nothing matched — an honest empty result. */
|
|
71
|
+
readonly matchedEntities: readonly string[];
|
|
72
|
+
/**
|
|
73
|
+
* The edges traversed, with their provenance — AC-6.
|
|
74
|
+
*
|
|
75
|
+
* Returned rather than folded into the chunks because "Team A depends on the retry budget" is a fact stored
|
|
76
|
+
* on an *edge*, and a model handed two adjacent chunks would have to infer the connection. Handing it the
|
|
77
|
+
* relationship lets it state one.
|
|
78
|
+
*/
|
|
79
|
+
readonly relationships: readonly KnowledgeRelationship[];
|
|
80
|
+
/** True when the traversal hit a bound. Reported, never silent — the same rule pagination follows. */
|
|
81
|
+
readonly truncated: boolean;
|
|
82
|
+
/**
|
|
83
|
+
* Which ranking produced the order — #277, AC-1.
|
|
84
|
+
*
|
|
85
|
+
* Reported because the two are not interchangeable and a measurement of one must never be mistaken for a
|
|
86
|
+
* measurement of the other. `connectivity` means no embedder was configured.
|
|
87
|
+
*/
|
|
88
|
+
readonly rankedBy: "relevance" | "connectivity";
|
|
89
|
+
};
|
|
90
|
+
export type GraphLocalSearchDeps = {
|
|
91
|
+
readonly graph: GraphStore;
|
|
92
|
+
readonly knowledge: KnowledgeStore;
|
|
93
|
+
readonly depth?: number;
|
|
94
|
+
readonly neighbourLimit?: number;
|
|
95
|
+
/**
|
|
96
|
+
* Ranks the chunks the traversal gathered — #277.
|
|
97
|
+
*
|
|
98
|
+
* **Optional, and the mode is materially worse without it.** Measured on this repository's corpus,
|
|
99
|
+
* connectivity-only ranking scored 20.8% success@5 against semantic search's 75%. The gap between its own
|
|
100
|
+
* columns was the tell: it found the right *document* 45.8% of the time and the right *chunk* half as often
|
|
101
|
+
* again, because an entity's provenance is every chunk that mentioned it and counting entity touches cannot
|
|
102
|
+
* tell two chunks of one document apart.
|
|
103
|
+
*
|
|
104
|
+
* So the graph is used for **selection** and embeddings for **ranking**, which is what Microsoft's local
|
|
105
|
+
* search does and what ours did not.
|
|
106
|
+
*
|
|
107
|
+
* Left optional rather than required because a deployment that has a graph and no embedder still gets a
|
|
108
|
+
* working traversal, and `docs/29` says plainly what it costs. `rankedBy` in the result reports which
|
|
109
|
+
* happened, so a measurement can never silently be of the other one.
|
|
110
|
+
*/
|
|
111
|
+
readonly embeddings?: EmbeddingProvider;
|
|
112
|
+
/**
|
|
113
|
+
* How many candidates to gather before ranking, as a multiple of the caller's limit.
|
|
114
|
+
*
|
|
115
|
+
* Ranking can only reorder what selection handed it, so a pool of exactly `limit` makes the re-rank a no-op
|
|
116
|
+
* — which is precisely the bug: the old code stopped gathering at `limit`, so the top-k by connectivity were
|
|
117
|
+
* the only chunks that could ever be returned.
|
|
118
|
+
*/
|
|
119
|
+
readonly poolFactor?: number;
|
|
120
|
+
};
|
|
121
|
+
/** Gathered wide, then ranked. Below this a re-rank has too little to choose between. */
|
|
122
|
+
export declare const DEFAULT_POOL_FACTOR = 8;
|
|
123
|
+
export declare const MIN_POOL = 40;
|
|
124
|
+
/**
|
|
125
|
+
* How much an entity's breadth discounts it — the hub problem, in one line.
|
|
126
|
+
*
|
|
127
|
+
* `docs/01-architecture.md` and `concepts/architecture.md` appeared in miss after miss. Generic entities
|
|
128
|
+
* ("run", "tool", "platform") are extracted from nearly every chunk, link to nearly everything, and drag the
|
|
129
|
+
* architecture overview into every traversal. An entity that touches half the corpus tells you almost nothing
|
|
130
|
+
* about *which* chunk you want, and counting it equally with a specific one is the mechanism by which the
|
|
131
|
+
* overview outranks the page that answers the question.
|
|
132
|
+
*
|
|
133
|
+
* A logarithmic discount rather than an IDF proper, because IDF needs the corpus size and this needs only what
|
|
134
|
+
* is already in hand. An entity in 2 chunks keeps most of its weight; one in 200 keeps about a seventh of it.
|
|
135
|
+
*/
|
|
136
|
+
export declare const hubDiscount: (provenanceSize: number) => number;
|
|
137
|
+
/** Cosine similarity. Vectors from one provider share a dimension, and a mismatch is a bug worth surfacing. */
|
|
138
|
+
export declare const cosine: (a: readonly number[], b: readonly number[]) => number;
|
|
139
|
+
export interface GraphLocalSearch {
|
|
140
|
+
search(context: {
|
|
141
|
+
readonly tenantId: TenantId;
|
|
142
|
+
}, input: {
|
|
143
|
+
readonly query: string;
|
|
144
|
+
readonly authSubjects: readonly string[];
|
|
145
|
+
readonly limit: number;
|
|
146
|
+
readonly sourceTypes?: readonly KnowledgeSourceType[];
|
|
147
|
+
}): Promise<GraphLocalResult>;
|
|
148
|
+
}
|
|
149
|
+
export declare const createGraphLocalSearch: (deps: GraphLocalSearchDeps) => GraphLocalSearch;
|
|
150
|
+
//# sourceMappingURL=graph-retrieval.d.ts.map
|