@evandu/super-pi 0.1.84 → 0.1.85
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/agents/super-pi-knowledge/index.ts +40 -0
- package/assets/knowledge-model/README.md +37 -0
- package/assets/knowledge-model/Xenova/bge-small-zh-v1.5/config.json +31 -0
- package/assets/knowledge-model/Xenova/bge-small-zh-v1.5/onnx/model_quantized.onnx +0 -0
- package/assets/knowledge-model/Xenova/bge-small-zh-v1.5/special_tokens_map.json +7 -0
- package/assets/knowledge-model/Xenova/bge-small-zh-v1.5/tokenizer.json +21278 -0
- package/assets/knowledge-model/Xenova/bge-small-zh-v1.5/tokenizer_config.json +15 -0
- package/assets/knowledge-model/Xenova/bge-small-zh-v1.5/vocab.txt +21128 -0
- package/dist/.super-pi-check-inputs.json +4 -0
- package/dist/client/.super-pi-client-sources.json +8 -7
- package/dist/client/assets/index-B_jOwjyp.js +72 -0
- package/dist/client/assets/index-CNNGl4eF.css +1 -0
- package/dist/client/index.html +2 -2
- package/dist/server/application/run-service.d.ts +50 -11
- package/dist/server/application/run-service.js +206 -77
- package/dist/server/command-handler.js +16 -4
- package/dist/server/http-server.js +59 -1
- package/dist/server/index.js +7 -0
- package/dist/server/knowledge-vectors.d.ts +133 -0
- package/dist/server/knowledge-vectors.js +302 -0
- package/dist/server/knowledge.d.ts +263 -0
- package/dist/server/knowledge.js +743 -0
- package/dist/server/persistence/app-state-repository.d.ts +25 -1
- package/dist/server/persistence/sqlite-state-repository.js +47 -4
- package/dist/server/runtime.d.ts +48 -1
- package/dist/server/runtime.js +83 -7
- package/dist/server/server-context.d.ts +1 -0
- package/dist/server/store.d.ts +153 -68
- package/dist/server/store.js +405 -187
- package/dist/server/types.d.ts +97 -65
- package/package.json +163 -155
- package/dist/client/assets/index-8Pf3MG2d.js +0 -72
- package/dist/client/assets/index-Jnj15dhT.css +0 -1
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { VectorIndex } from "./knowledge-vectors.js";
|
|
2
|
+
import type { Store } from "./store.js";
|
|
3
|
+
import type { Conversation } from "./types.js";
|
|
4
|
+
/** One document as it lives on disk: the ORIGINAL text, never the chunks. */
|
|
5
|
+
export type KnowledgeDocument = {
|
|
6
|
+
id: string;
|
|
7
|
+
workspaceId: string;
|
|
8
|
+
title: string;
|
|
9
|
+
/** Where it came from - a workspace-relative path, or `pasted` for hand-entered text. */
|
|
10
|
+
source: string;
|
|
11
|
+
format: string;
|
|
12
|
+
characters: number;
|
|
13
|
+
createdAt: string;
|
|
14
|
+
updatedAt: string;
|
|
15
|
+
/**
|
|
16
|
+
* Content fingerprint, used to recognise an import the user already made.
|
|
17
|
+
*
|
|
18
|
+
* WHY IT IS STORED RATHER THAN COMPUTED ON COMPARISON. Deduplication runs inside `add`, which already
|
|
19
|
+
* reads every document in the workspace; hashing each one's full text there would be O(corpus) on
|
|
20
|
+
* every import. Storing it turns the check into one string comparison, and a document written by an
|
|
21
|
+
* older build simply hashes on read (`digestOf`) instead.
|
|
22
|
+
*/
|
|
23
|
+
digest: string;
|
|
24
|
+
text: string;
|
|
25
|
+
};
|
|
26
|
+
/** A retrievable slice. `heading` is the nearest preceding markdown heading, so a hit can be cited. */
|
|
27
|
+
export type KnowledgeChunk = {
|
|
28
|
+
index: number;
|
|
29
|
+
heading: string;
|
|
30
|
+
text: string;
|
|
31
|
+
};
|
|
32
|
+
export type KnowledgeHit = {
|
|
33
|
+
documentId: string;
|
|
34
|
+
title: string;
|
|
35
|
+
source: string;
|
|
36
|
+
heading: string;
|
|
37
|
+
chunk: number;
|
|
38
|
+
score: number;
|
|
39
|
+
text: string;
|
|
40
|
+
};
|
|
41
|
+
/** 1 MB. Big enough for a design doc or a spec, small enough that chunking stays instant. */
|
|
42
|
+
export declare const MAX_DOCUMENT_BYTES: number;
|
|
43
|
+
/** A pasted document rides inside a JSON body, so the request cap leaves room for escaping overhead. */
|
|
44
|
+
export declare const MAX_KNOWLEDGE_REQUEST_BYTES: number;
|
|
45
|
+
/** Per workspace, across every document. A knowledge base is a curated asset, not a file server. */
|
|
46
|
+
export declare const MAX_WORKSPACE_BYTES: number;
|
|
47
|
+
export declare const DEFAULT_RESULT_LIMIT = 6;
|
|
48
|
+
export declare const MAX_RESULT_LIMIT = 20;
|
|
49
|
+
/** What one search may be asked for. `rerank` and `conversation` exist only for the agent's own calls. */
|
|
50
|
+
export type KnowledgeSearch = {
|
|
51
|
+
limit?: number;
|
|
52
|
+
/** Re-order with the session's model. Off for the settings preview, which must stay instant. */
|
|
53
|
+
rerank?: boolean;
|
|
54
|
+
/** Required by `rerank`: it answers with this conversation's model and workspace. */
|
|
55
|
+
conversation?: Conversation;
|
|
56
|
+
signal?: AbortSignal;
|
|
57
|
+
};
|
|
58
|
+
/**
|
|
59
|
+
* The slice of the runtime the reranker needs, declared structurally.
|
|
60
|
+
*
|
|
61
|
+
* Declared here rather than imported so this file does not depend on the spawn machinery that depends
|
|
62
|
+
* on it - `runtime.ts` imports `KNOWLEDGE_TOOL_NAME` from here, and a type-only back edge into that
|
|
63
|
+
* module would make the cycle visible in every import graph.
|
|
64
|
+
*/
|
|
65
|
+
export type KnowledgeRuntime = {
|
|
66
|
+
answer(input: {
|
|
67
|
+
conversation: Conversation;
|
|
68
|
+
prompt: string;
|
|
69
|
+
cwd: string;
|
|
70
|
+
signal: AbortSignal;
|
|
71
|
+
knowledgeRerank?: boolean;
|
|
72
|
+
}): AsyncIterable<string>;
|
|
73
|
+
};
|
|
74
|
+
/**
|
|
75
|
+
* The first integer array in the model's answer, or null.
|
|
76
|
+
*
|
|
77
|
+
* WHY TOLERANT PARSING. The prompt asks for a bare JSON array, and a model that mostly complies will
|
|
78
|
+
* still sometimes wrap it in a sentence or a code fence. Recovering the array from that is worth doing;
|
|
79
|
+
* guessing when there is no array at all is not, so anything unparseable is null and the caller keeps
|
|
80
|
+
* the lexical order.
|
|
81
|
+
*/
|
|
82
|
+
export declare function parseRanking(output: string): number[] | null;
|
|
83
|
+
/** Duplicated as a literal in `agents/super-pi-knowledge/index.ts`, the way `TASK_TOOL_NAME` is. */
|
|
84
|
+
export declare const KNOWLEDGE_TOOL_NAME = "knowledge_search";
|
|
85
|
+
/**
|
|
86
|
+
* Index terms for one string: CJK bigrams plus Latin words.
|
|
87
|
+
*
|
|
88
|
+
* WHY BIGRAMS AND NOT WORDS. Chinese has no spaces, so the word-list tokenizer the rest of this
|
|
89
|
+
* server uses (`ActiveMemory.search`) returns one enormous "word" for a whole sentence and matches
|
|
90
|
+
* nothing usefully. Character bigrams need no dictionary and no dependency, and they carry enough
|
|
91
|
+
* precision that 知识库 and 记忆库 share 识库 but not the whole run - which is what makes a two-term
|
|
92
|
+
* overlap meaningful rather than accidental.
|
|
93
|
+
*/
|
|
94
|
+
export declare function tokenize(text: string): string[];
|
|
95
|
+
/**
|
|
96
|
+
* The longer-range channel: character TRIGRAMS, for the passage that repeats a phrase verbatim.
|
|
97
|
+
*
|
|
98
|
+
* WHY A SECOND GRANULARITY AND NOT JUST BIGRAMS. Bigrams answer "do these share any two-character
|
|
99
|
+
* run", which is high recall but low precision - two unrelated Chinese sentences collide constantly on
|
|
100
|
+
* a shared pair. Trigrams only fire on a real three-character overlap, so when the user's question
|
|
101
|
+
* quotes the document's own wording this channel separates it sharply from the rest of the corpus.
|
|
102
|
+
* It is scored on its own and fused with the bigram channel rather than merged into it, because the
|
|
103
|
+
* two answer different questions and a term that is strong for one is usually weak for the other.
|
|
104
|
+
*/
|
|
105
|
+
export declare function tokenizePhrases(text: string): string[];
|
|
106
|
+
/** The identity of a document's CONTENT, independent of its name, its path and when it arrived. */
|
|
107
|
+
export declare function digestOf(text: string): string;
|
|
108
|
+
/**
|
|
109
|
+
* Splits a document into overlapping chunks, remembering the heading each one sits under.
|
|
110
|
+
*
|
|
111
|
+
* WHY OVERLAP. A passage that spans a boundary is the passage most likely to be the answer - the
|
|
112
|
+
* sentence that defines a term usually starts in one chunk and ends in the next - and a retrieval
|
|
113
|
+
* system that cut there would return a fragment ending mid-thought. The 150-character tail is
|
|
114
|
+
* repeated at the head of the next chunk to keep that sentence whole in at least one hit.
|
|
115
|
+
*/
|
|
116
|
+
export declare function chunkDocument(text: string): KnowledgeChunk[];
|
|
117
|
+
export declare class KnowledgeBase {
|
|
118
|
+
readonly directory: string;
|
|
119
|
+
private store;
|
|
120
|
+
private runtime?;
|
|
121
|
+
private vectors?;
|
|
122
|
+
private capabilities;
|
|
123
|
+
private indexes;
|
|
124
|
+
private serial;
|
|
125
|
+
constructor(directory: string, store: Store, runtime?: KnowledgeRuntime | undefined, vectors?: VectorIndex | undefined);
|
|
126
|
+
/** `SUPER_PI_KNOWLEDGE_ENABLED=0` turns the whole feature off, the same way memory turns off. */
|
|
127
|
+
enabled(): boolean;
|
|
128
|
+
/** The opaque workspace identity on disk, encoded. Paths never come from the model or the request. */
|
|
129
|
+
private folder;
|
|
130
|
+
private file;
|
|
131
|
+
private requireWorkspace;
|
|
132
|
+
private locked;
|
|
133
|
+
/** Metadata only - the caller listing documents must never receive the full text of a workspace. */
|
|
134
|
+
private summarise;
|
|
135
|
+
private readAll;
|
|
136
|
+
private save;
|
|
137
|
+
private index;
|
|
138
|
+
/**
|
|
139
|
+
* Loads (or rebuilds) one workspace's index. Cache miss is the only thing that reads the disk.
|
|
140
|
+
*
|
|
141
|
+
* WHY REBUILT PER WORKSPACE AND NOT ONE GLOBAL INDEX. Isolation is a security property here, not a
|
|
142
|
+
* performance one: a hit may only ever come from the workspace the turn's capability names. A
|
|
143
|
+
* single shared index would need every search to filter by workspace after scoring, which is one
|
|
144
|
+
* forgotten filter away from a cross-tenant leak. Separate caches cannot express that mistake.
|
|
145
|
+
*
|
|
146
|
+
* WHY THERE IS NO ON-DISK INDEX. Measured, not assumed: rebuilding the index of a 16 MB workspace -
|
|
147
|
+
* the feature's own capacity cap - costs about 180 ms, once per process. A serialised index would be
|
|
148
|
+
* larger than the corpus it describes and JSON.parse of it is not reliably cheaper than the
|
|
149
|
+
* tokenisation, so it would trade a correctness-neutral optimisation for a second on-disk artefact
|
|
150
|
+
* that can drift from the documents.
|
|
151
|
+
*/
|
|
152
|
+
private load;
|
|
153
|
+
private invalidate;
|
|
154
|
+
list(workspaceId: string): Promise<Array<Omit<KnowledgeDocument, "text">>>;
|
|
155
|
+
/**
|
|
156
|
+
* Whether this workspace has any document at all, which is what decides whether the turn mounts the
|
|
157
|
+
* extension.
|
|
158
|
+
*
|
|
159
|
+
* WHY A DIRECTORY LISTING AND NOT `list()`. This runs once per turn, before every spawn. `list()`
|
|
160
|
+
* would read and parse every document's full text to answer a yes/no question, so a workspace with
|
|
161
|
+
* a dozen real documents would pay for parsing all of them on every message - including the
|
|
162
|
+
* catalogue fetch the extension makes the moment it loads. `readdir` answers the same question for
|
|
163
|
+
* the price of one directory entry, and a name is all this needs.
|
|
164
|
+
*/
|
|
165
|
+
hasDocuments(workspaceId: string): Promise<boolean>;
|
|
166
|
+
/**
|
|
167
|
+
* Adds a document, either from a workspace file or from pasted text.
|
|
168
|
+
*
|
|
169
|
+
* WHY THE FILE IS READ HERE AND NOT UPLOADED. A knowledge base is made of documents the person
|
|
170
|
+
* ALREADY has in their workspace - re-uploading a copy of the file would give the agent a second,
|
|
171
|
+
* newer version that diverges from the one the workspace serves. The path therefore goes through
|
|
172
|
+
* the same `resolveInsideRoot` guard the file editor uses, so symlink and `..` escapes are refused
|
|
173
|
+
* by construction instead of by a check someone has to remember.
|
|
174
|
+
*/
|
|
175
|
+
add(workspaceId: string, body: {
|
|
176
|
+
path?: string;
|
|
177
|
+
title?: string;
|
|
178
|
+
text?: string;
|
|
179
|
+
}): Promise<Omit<KnowledgeDocument, "text">>;
|
|
180
|
+
remove(workspaceId: string, documentId: string): Promise<void>;
|
|
181
|
+
/**
|
|
182
|
+
* Scores this workspace's chunks on four independent lexical channels and fuses them.
|
|
183
|
+
*
|
|
184
|
+
* WHY LEXICAL AND NOT ONLY EMBEDDINGS. See docs/KNOWLEDGE_BASE.md: the vector channel is fused in
|
|
185
|
+
* here rather than replacing this one, because each is wrong in a way the other catches. BM25
|
|
186
|
+
* misses a paraphrase completely; a vector search will happily return the nearest thing at a
|
|
187
|
+
* confident-looking 0.58 when the answer is not in the knowledge base at all. Summing a normalised
|
|
188
|
+
* lexical score with a normalised cosine score keeps both signals - and keeps "no hit" reachable.
|
|
189
|
+
*
|
|
190
|
+
* WHY NORMALISE PER CHANNEL AND SUM, RATHER THAN RRF. Reciprocal rank fusion is the usual way to
|
|
191
|
+
* combine ranked lists, and it would be a regression here: it discards score magnitude, so a
|
|
192
|
+
* passage that matches decisively and one that matches barely would arrive at the same rank and
|
|
193
|
+
* finish within a couple of percent of each other. The gap between a clear winner and a near miss is
|
|
194
|
+
* exactly what BM25 was measuring. Each channel is therefore divided by its own best score for this
|
|
195
|
+
* query, which preserves the shape of that channel's confidence while making the weights comparable.
|
|
196
|
+
*/
|
|
197
|
+
search(workspaceId: string, query: string, options?: KnowledgeSearch): Promise<KnowledgeHit[]>;
|
|
198
|
+
/**
|
|
199
|
+
* Re-orders the candidate pool with the session's own model.
|
|
200
|
+
*
|
|
201
|
+
* WHY THIS EXISTS AND WHAT IT IS WORTH. Lexical retrieval ranks on shared characters, so a passage
|
|
202
|
+
* that answers in different words sits below passages that merely repeat the question's wording -
|
|
203
|
+
* often below the fold, where the character budget has already cut it off. Reading the candidates
|
|
204
|
+
* is something the model can already do, so it is asked to pick.
|
|
205
|
+
*
|
|
206
|
+
* ITS CEILING, STATED PLAINLY. Reranking can only reorder what the first stage returned. A query
|
|
207
|
+
* with NO lexical overlap at all never reaches this function, which is why the vector channel and
|
|
208
|
+
* not this one is what closes the paraphrase gap - see docs/KNOWLEDGE_BASE.md.
|
|
209
|
+
*
|
|
210
|
+
* WHY IT FAILS OPEN. Every outcome that is not a clean parse returns the lexical order. A reranker
|
|
211
|
+
* that made things worse, hung, or answered with prose would degrade the feature invisibly, and a
|
|
212
|
+
* retrieval layer that is allowed to get worse by being clever is worse than one that is not.
|
|
213
|
+
*/
|
|
214
|
+
private rerank;
|
|
215
|
+
/**
|
|
216
|
+
* Mints a read-only token for one turn. The agent can retrieve, never write: everything a person
|
|
217
|
+
* puts in a knowledge base is theirs to curate from the settings window.
|
|
218
|
+
*/
|
|
219
|
+
issue(conversationId: string): string;
|
|
220
|
+
release(token: string): void;
|
|
221
|
+
private identity;
|
|
222
|
+
/** The agent-facing endpoint: a title catalogue and a search. Nothing that writes. */
|
|
223
|
+
tool(token: string, body: {
|
|
224
|
+
kind?: string;
|
|
225
|
+
query?: string;
|
|
226
|
+
limit?: number;
|
|
227
|
+
}, signal?: AbortSignal): Promise<{
|
|
228
|
+
documents: {
|
|
229
|
+
title: string;
|
|
230
|
+
source: string;
|
|
231
|
+
characters: number;
|
|
232
|
+
}[];
|
|
233
|
+
hits?: undefined;
|
|
234
|
+
reranked?: undefined;
|
|
235
|
+
} | {
|
|
236
|
+
documents?: undefined;
|
|
237
|
+
hits: KnowledgeHit[];
|
|
238
|
+
reranked: boolean;
|
|
239
|
+
}>;
|
|
240
|
+
/** The browser-facing endpoint, behind the ordinary access token and same-origin check. */
|
|
241
|
+
manage(workspaceId: string, body: {
|
|
242
|
+
action?: string;
|
|
243
|
+
path?: string;
|
|
244
|
+
title?: string;
|
|
245
|
+
text?: string;
|
|
246
|
+
documentId?: string;
|
|
247
|
+
query?: string;
|
|
248
|
+
limit?: number;
|
|
249
|
+
}): Promise<{
|
|
250
|
+
hits: KnowledgeHit[];
|
|
251
|
+
documents: Omit<KnowledgeDocument, "text">[];
|
|
252
|
+
semantic: import("./knowledge-vectors.js").VectorStatus | null;
|
|
253
|
+
}>;
|
|
254
|
+
/**
|
|
255
|
+
* What the panel shows, including whether semantic retrieval is actually working.
|
|
256
|
+
*
|
|
257
|
+
* WHY THE STATE IS WORTH SHOWING. The vector half depends on an optional package and a download from
|
|
258
|
+
* a host the machine may not reach, and both fail quietly by design. Without this on screen, "the
|
|
259
|
+
* feature is off" is indistinguishable from "the feature is broken here" - which is the difference
|
|
260
|
+
* between not wanting a feature and not being able to have it.
|
|
261
|
+
*/
|
|
262
|
+
private overview;
|
|
263
|
+
}
|