@loomwork/knowledge 0.0.0-stage → 0.1.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/LICENSE +202 -0
- package/NOTICE +8 -0
- package/README.md +72 -2
- package/dist/KnowledgeBase.d.ts +212 -0
- package/dist/KnowledgeBase.d.ts.map +1 -0
- package/dist/KnowledgeBase.js +394 -0
- package/dist/KnowledgeBase.js.map +1 -0
- package/dist/chunk.d.ts +18 -0
- package/dist/chunk.d.ts.map +1 -0
- package/dist/chunk.js +44 -0
- package/dist/chunk.js.map +1 -0
- package/dist/citations.d.ts +45 -0
- package/dist/citations.d.ts.map +1 -0
- package/dist/citations.js +76 -0
- package/dist/citations.js.map +1 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +16 -0
- package/dist/index.js.map +1 -0
- package/dist/prompt.d.ts +41 -0
- package/dist/prompt.d.ts.map +1 -0
- package/dist/prompt.js +47 -0
- package/dist/prompt.js.map +1 -0
- package/dist/types.d.ts +94 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +3 -0
- package/dist/types.js.map +1 -0
- package/package.json +57 -3
- package/src/KnowledgeBase.ts +489 -0
- package/src/chunk.ts +41 -0
- package/src/citations.ts +88 -0
- package/src/index.ts +16 -0
- package/src/prompt.ts +56 -0
- package/src/types.ts +97 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";;;AAAA,iDAAmE;AAA1D,8GAAA,aAAa,OAAA;AAAE,kHAAA,iBAAiB,OAAA;AAUzC,iCAAyC;AAAhC,uGAAA,cAAc,OAAA;AACvB,yCAAsF;AAA7E,2GAAA,cAAc,OAAA;AAAE,2GAAA,cAAc,OAAA;AAAE,oHAAA,uBAAuB,OAAA;AAEhE,mCAA4D;AAAnD,yGAAA,eAAe,OAAA;AAAE,yGAAA,eAAe,OAAA"}
|
package/dist/prompt.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { LlmMessageInput, LlmRequestInput } from "@loomwork/llm";
|
|
2
|
+
import type { Passage } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Retrieved passages as model messages: one `user` message per passage, each still
|
|
5
|
+
* carrying the taint it was ingested with.
|
|
6
|
+
*
|
|
7
|
+
* This shape is the whole safety argument, and it is the shape almost nobody writes first.
|
|
8
|
+
* The natural thing — join the passages into one string under a "here is the relevant
|
|
9
|
+
* documentation" header, and put it in the SYSTEM prompt — is refused by `guardRequest`
|
|
10
|
+
* with an `UntrustedContentError`, because the system prompt is the operator's voice and a
|
|
11
|
+
* corpus anyone can edit is not the operator. Two properties make this function the fix
|
|
12
|
+
* rather than a wrapper around the same mistake:
|
|
13
|
+
*
|
|
14
|
+
* - ONE message per passage, never a join. Taint is attached to a VALUE, so joining means
|
|
15
|
+
* unwrapping first, and provenance stops travelling exactly where it starts mattering.
|
|
16
|
+
* - the `user` role, which is the only position `guardRequest` fences rather than refuses.
|
|
17
|
+
*
|
|
18
|
+
* `guardRequest` then labels each fenced block with the passage's origin before any
|
|
19
|
+
* provider sees it, so the model is told which passages are data and where each came from.
|
|
20
|
+
*/
|
|
21
|
+
export declare function passageMessages(passages: readonly Passage[]): LlmMessageInput[];
|
|
22
|
+
export interface GroundedRequestOptions {
|
|
23
|
+
/** The operator's instructions. Trusted content — a plain string, never a passage. */
|
|
24
|
+
instructions: string;
|
|
25
|
+
/** The question, appended after the passages so it reads last. */
|
|
26
|
+
question: string;
|
|
27
|
+
passages: readonly Passage[];
|
|
28
|
+
maxTokens?: number;
|
|
29
|
+
temperature?: number;
|
|
30
|
+
}
|
|
31
|
+
/**
|
|
32
|
+
* A complete grounded-answer request: operator instructions plus the untrusted-content
|
|
33
|
+
* note in `system`, the retrieved passages as fenced `user` messages, the question last.
|
|
34
|
+
*
|
|
35
|
+
* `instructions` is typed `string` rather than `LlmContent` on purpose. The system slot is
|
|
36
|
+
* where a passage must never land, and a signature that accepts a tainted value there
|
|
37
|
+
* would move the refusal from compile time to run time — the check exists either way, but
|
|
38
|
+
* only one of the two tells you before you ship.
|
|
39
|
+
*/
|
|
40
|
+
export declare function groundedRequest(opts: GroundedRequestOptions): LlmRequestInput;
|
|
41
|
+
//# sourceMappingURL=prompt.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.d.ts","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,eAAe,EAAE,eAAe,EAAE,MAAM,eAAe,CAAC;AACtE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,SAAS,CAAC;AAEvC;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,eAAe,CAAC,QAAQ,EAAE,SAAS,OAAO,EAAE,GAAG,eAAe,EAAE,CAE/E;AAED,MAAM,WAAW,sBAAsB;IACrC,sFAAsF;IACtF,YAAY,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,SAAS,OAAO,EAAE,CAAC;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED;;;;;;;;GAQG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,sBAAsB,GAAG,eAAe,CAU7E"}
|
package/dist/prompt.js
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.passageMessages = passageMessages;
|
|
4
|
+
exports.groundedRequest = groundedRequest;
|
|
5
|
+
const llm_1 = require("@loomwork/llm");
|
|
6
|
+
/**
|
|
7
|
+
* Retrieved passages as model messages: one `user` message per passage, each still
|
|
8
|
+
* carrying the taint it was ingested with.
|
|
9
|
+
*
|
|
10
|
+
* This shape is the whole safety argument, and it is the shape almost nobody writes first.
|
|
11
|
+
* The natural thing — join the passages into one string under a "here is the relevant
|
|
12
|
+
* documentation" header, and put it in the SYSTEM prompt — is refused by `guardRequest`
|
|
13
|
+
* with an `UntrustedContentError`, because the system prompt is the operator's voice and a
|
|
14
|
+
* corpus anyone can edit is not the operator. Two properties make this function the fix
|
|
15
|
+
* rather than a wrapper around the same mistake:
|
|
16
|
+
*
|
|
17
|
+
* - ONE message per passage, never a join. Taint is attached to a VALUE, so joining means
|
|
18
|
+
* unwrapping first, and provenance stops travelling exactly where it starts mattering.
|
|
19
|
+
* - the `user` role, which is the only position `guardRequest` fences rather than refuses.
|
|
20
|
+
*
|
|
21
|
+
* `guardRequest` then labels each fenced block with the passage's origin before any
|
|
22
|
+
* provider sees it, so the model is told which passages are data and where each came from.
|
|
23
|
+
*/
|
|
24
|
+
function passageMessages(passages) {
|
|
25
|
+
return passages.map((passage) => ({ role: "user", content: passage.text }));
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* A complete grounded-answer request: operator instructions plus the untrusted-content
|
|
29
|
+
* note in `system`, the retrieved passages as fenced `user` messages, the question last.
|
|
30
|
+
*
|
|
31
|
+
* `instructions` is typed `string` rather than `LlmContent` on purpose. The system slot is
|
|
32
|
+
* where a passage must never land, and a signature that accepts a tainted value there
|
|
33
|
+
* would move the refusal from compile time to run time — the check exists either way, but
|
|
34
|
+
* only one of the two tells you before you ship.
|
|
35
|
+
*/
|
|
36
|
+
function groundedRequest(opts) {
|
|
37
|
+
return {
|
|
38
|
+
system: `${opts.instructions} ${llm_1.UNTRUSTED_CONTENT_SYSTEM_NOTE}`,
|
|
39
|
+
messages: [
|
|
40
|
+
...passageMessages(opts.passages),
|
|
41
|
+
{ role: "user", content: opts.question },
|
|
42
|
+
],
|
|
43
|
+
...(opts.maxTokens !== undefined && { maxTokens: opts.maxTokens }),
|
|
44
|
+
...(opts.temperature !== undefined && { temperature: opts.temperature }),
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=prompt.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"prompt.js","sourceRoot":"","sources":["../src/prompt.ts"],"names":[],"mappings":";;AAsBA,0CAEC;AAqBD,0CAUC;AAvDD,uCAA8D;AAI9D;;;;;;;;;;;;;;;;;GAiBG;AACH,SAAgB,eAAe,CAAC,QAA4B;IAC1D,OAAO,QAAQ,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,MAAe,EAAE,OAAO,EAAE,OAAO,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC;AACvF,CAAC;AAYD;;;;;;;;GAQG;AACH,SAAgB,eAAe,CAAC,IAA4B;IAC1D,OAAO;QACL,MAAM,EAAE,GAAG,IAAI,CAAC,YAAY,IAAI,mCAA6B,EAAE;QAC/D,QAAQ,EAAE;YACR,GAAG,eAAe,CAAC,IAAI,CAAC,QAAQ,CAAC;YACjC,EAAE,IAAI,EAAE,MAAe,EAAE,OAAO,EAAE,IAAI,CAAC,QAAQ,EAAE;SAClD;QACD,GAAG,CAAC,IAAI,CAAC,SAAS,KAAK,SAAS,IAAI,EAAE,SAAS,EAAE,IAAI,CAAC,SAAS,EAAE,CAAC;QAClE,GAAG,CAAC,IAAI,CAAC,WAAW,KAAK,SAAS,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC;KACzE,CAAC;AACJ,CAAC"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { TaintedValue } from "@loomwork/classify";
|
|
2
|
+
/**
|
|
3
|
+
* A source document, as the application already has it: an id it can name the document
|
|
4
|
+
* by again tomorrow, its text, and whatever metadata it wants carried onto every passage.
|
|
5
|
+
*
|
|
6
|
+
* There is deliberately no `source`, `url`, `author`, `section` or `updatedAt` here. Those
|
|
7
|
+
* are a vocabulary, they differ per corpus, and a framework that named them would be
|
|
8
|
+
* asking every app to translate into its dialect — put them in `meta`, which travels
|
|
9
|
+
* verbatim and is indexed like any other content.
|
|
10
|
+
*/
|
|
11
|
+
export interface KnowledgeDoc {
|
|
12
|
+
/** Stable across revisions. This is what a re-ingest supersedes and what a citation names. */
|
|
13
|
+
id: string;
|
|
14
|
+
/** Optional label, carried onto every passage so a citation can read as a title. */
|
|
15
|
+
title?: string;
|
|
16
|
+
text: string;
|
|
17
|
+
/** Carried onto every passage verbatim. Must be JSON — it is stored in a MemoryEntry. */
|
|
18
|
+
meta?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* One retrieved (or just-ingested) piece of a document.
|
|
22
|
+
*
|
|
23
|
+
* `text` is TAINTED (ADR 0085) and stays tainted all the way into the model request. A
|
|
24
|
+
* corpus is edited by whoever can edit the source, so a passage is third-party content by
|
|
25
|
+
* definition — the same class as an MCP tool result — and `guardRequest` refuses it in
|
|
26
|
+
* every instruction position. Tainting at INGEST rather than at the prompt is what makes
|
|
27
|
+
* that hold on every path: the wrapper is plain JSON, so it survives the store, the
|
|
28
|
+
* recorded retrieval, and a replay that rebuilds the prompt from the log.
|
|
29
|
+
*/
|
|
30
|
+
export interface Passage {
|
|
31
|
+
docId: string;
|
|
32
|
+
title?: string;
|
|
33
|
+
/** 0-based position within the document, in source order. */
|
|
34
|
+
index: number;
|
|
35
|
+
text: TaintedValue<string>;
|
|
36
|
+
meta?: Record<string, unknown>;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Splits a document into passage texts, in source order.
|
|
40
|
+
*
|
|
41
|
+
* Injected, never inferred. How a document splits is a property of the corpus — a runbook
|
|
42
|
+
* splits on paragraphs, a legal contract on numbered clauses, a chat export on turns — and
|
|
43
|
+
* the retrieval quality of a knowledge base is mostly decided here. {@link packParagraphs}
|
|
44
|
+
* is a default worth having, not a default worth hiding.
|
|
45
|
+
*/
|
|
46
|
+
export type Chunker = (doc: KnowledgeDoc) => string[];
|
|
47
|
+
/**
|
|
48
|
+
* The manifest row for one document: what a corpus knows about a document without
|
|
49
|
+
* reading its passages.
|
|
50
|
+
*
|
|
51
|
+
* `chunkCount` is what makes removal possible at all — passage ids are derived
|
|
52
|
+
* (`docId#0…docId#n`), so the count IS the id list, in one small row instead of n.
|
|
53
|
+
*
|
|
54
|
+
* `fingerprint` is over the document's content, so re-ingesting an unchanged document is
|
|
55
|
+
* free: that is what turns {@link KnowledgeBase.sync} from "rebuild the corpus" into "write
|
|
56
|
+
* the documents that actually changed".
|
|
57
|
+
*
|
|
58
|
+
* `committed` is the crash-safety flag, and the reason ingest writes a manifest row TWICE.
|
|
59
|
+
* The dangerous interleaving is not a lost write, it is a manifest that claims a revision
|
|
60
|
+
* whose passages were never appended: the next sync would compare fingerprints, see a
|
|
61
|
+
* match, and skip the document forever — a corpus silently missing a document that every
|
|
62
|
+
* status view calls present. Writing `committed: false` first, and flipping it only after
|
|
63
|
+
* the passages land, makes the crash window converge on a re-run instead.
|
|
64
|
+
*/
|
|
65
|
+
export interface DocRecord {
|
|
66
|
+
docId: string;
|
|
67
|
+
chunkCount: number;
|
|
68
|
+
fingerprint: string;
|
|
69
|
+
committed: boolean;
|
|
70
|
+
/**
|
|
71
|
+
* WHO manages this document — the writer that may revise or delete it. `undefined` is a
|
|
72
|
+
* source of its own (the corpus's default writer), not a wildcard.
|
|
73
|
+
*
|
|
74
|
+
* It exists because `sync` is a destructive reconcile over everything the manifest holds,
|
|
75
|
+
* and a corpus is not always written by one thing. An agent that writes down what it
|
|
76
|
+
* learned from feedback puts documents in the same scope as an authored corpus — and the
|
|
77
|
+
* nightly sync of a wiki export would then delete every one of them, because they are not
|
|
78
|
+
* in the set the wiki produced. Stamping each document with its writer makes that
|
|
79
|
+
* impossible instead of documented: a `sync` only ever reconciles its own documents.
|
|
80
|
+
*/
|
|
81
|
+
source?: string;
|
|
82
|
+
/**
|
|
83
|
+
* Generation of this manifest ROW, not a version of the document.
|
|
84
|
+
*
|
|
85
|
+
* Manifest rows are only ever APPENDED — `docId#0`, `docId#1`, … — and the highest one
|
|
86
|
+
* wins, because the alternative (delete the old row, append the new one) has a crash
|
|
87
|
+
* window in which the document has no manifest row at all. That window is the worst of
|
|
88
|
+
* the three: passages nothing counts are passages nothing can remove. Superseded rows
|
|
89
|
+
* are pruned after the new one is durable, so a crash costs a stale row and never a
|
|
90
|
+
* missing one.
|
|
91
|
+
*/
|
|
92
|
+
revision: number;
|
|
93
|
+
}
|
|
94
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;;;;GAQG;AACH,MAAM,WAAW,YAAY;IAC3B,8FAA8F;IAC9F,EAAE,EAAE,MAAM,CAAC;IACX,oFAAoF;IACpF,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,IAAI,EAAE,MAAM,CAAC;IACb,yFAAyF;IACzF,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED;;;;;;;;;GASG;AACH,MAAM,WAAW,OAAO;IACtB,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,6DAA6D;IAC7D,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,YAAY,CAAC,MAAM,CAAC,CAAC;IAC3B,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CAChC;AAED;;;;;;;GAOG;AACH,MAAM,MAAM,OAAO,GAAG,CAAC,GAAG,EAAE,YAAY,KAAK,MAAM,EAAE,CAAC;AAEtD;;;;;;;;;;;;;;;;;GAiBG;AACH,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,EAAE,OAAO,CAAC;IACnB;;;;;;;;;;OAUG;IACH,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB;;;;;;;;;OASG;IACH,QAAQ,EAAE,MAAM,CAAC;CAClB"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
CHANGED
|
@@ -1,6 +1,60 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loomwork/knowledge",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"
|
|
5
|
-
"
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Document corpora over memory — ingest, revise, retrieve with provenance",
|
|
5
|
+
"main": "./dist/index.js",
|
|
6
|
+
"types": "./dist/index.d.ts",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"require": "./dist/index.js",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"dependencies": {
|
|
15
|
+
"@loomwork/classify": "^0.1.0",
|
|
16
|
+
"@loomwork/core": "^0.1.0",
|
|
17
|
+
"@loomwork/llm": "^0.1.0",
|
|
18
|
+
"@loomwork/memory": "^0.1.0",
|
|
19
|
+
"@loomwork/verify": "^0.1.0"
|
|
20
|
+
},
|
|
21
|
+
"devDependencies": {
|
|
22
|
+
"@types/node": "^22.0.0",
|
|
23
|
+
"typescript": "^5.6.3",
|
|
24
|
+
"vitest": "^2.1.8",
|
|
25
|
+
"@loomwork/plugin-sqlite": "0.1.0"
|
|
26
|
+
},
|
|
27
|
+
"license": "Apache-2.0",
|
|
28
|
+
"repository": {
|
|
29
|
+
"type": "git",
|
|
30
|
+
"url": "git+https://github.com/chhetrisushil/loom.git",
|
|
31
|
+
"directory": "packages/knowledge"
|
|
32
|
+
},
|
|
33
|
+
"files": [
|
|
34
|
+
"dist",
|
|
35
|
+
"src",
|
|
36
|
+
"!src/**/*.test.ts",
|
|
37
|
+
"!src/**/*.test.tsx",
|
|
38
|
+
"!src/**/__tests__",
|
|
39
|
+
"!src/**/__fixtures__",
|
|
40
|
+
"!src/**/__snapshots__",
|
|
41
|
+
"!src/**/__golden__",
|
|
42
|
+
"LICENSE",
|
|
43
|
+
"NOTICE"
|
|
44
|
+
],
|
|
45
|
+
"publishConfig": {
|
|
46
|
+
"access": "public",
|
|
47
|
+
"tag": "next"
|
|
48
|
+
},
|
|
49
|
+
"engines": {
|
|
50
|
+
"node": ">=20.0.0"
|
|
51
|
+
},
|
|
52
|
+
"scripts": {
|
|
53
|
+
"build": "tsc --build",
|
|
54
|
+
"dev": "tsc --build --watch",
|
|
55
|
+
"test": "vitest run",
|
|
56
|
+
"test:watch": "vitest",
|
|
57
|
+
"clean": "rm -rf dist tsconfig.tsbuildinfo",
|
|
58
|
+
"typecheck:tests": "tsc -p tsconfig.test.json"
|
|
59
|
+
}
|
|
6
60
|
}
|