@devilsdev/rag-pipeline-utils 2.5.0 → 2.6.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/CHANGELOG.md +86 -0
- package/README.md +1 -1
- package/dist/index.cjs +1484 -147
- package/dist/index.d.ts +49 -0
- package/dist/index.mjs +1 -1
- package/dist/testing.cjs +2 -0
- package/dist/testing.d.ts +1 -0
- package/dist/testing.mjs +5 -0
- package/dist/types/adapters/ai-sdk.d.ts +53 -0
- package/dist/types/cache/component-keys.d.ts +7 -0
- package/dist/types/cache/memory-cache.d.ts +54 -0
- package/dist/types/cache/pipeline-cache.d.ts +66 -0
- package/dist/types/cache/redis-cache.d.ts +46 -0
- package/dist/types/connectors/anthropic-connector.d.ts +7 -2
- package/dist/types/connectors/cohere-connector.d.ts +7 -2
- package/dist/types/connectors/local-embedder.d.ts +9 -0
- package/dist/types/connectors/memory-retriever.d.ts +8 -0
- package/dist/types/connectors/ollama-connector.d.ts +7 -2
- package/dist/types/connectors/openai-connector.d.ts +16 -1
- package/dist/types/core/create-pipeline.d.ts +8 -0
- package/dist/types/core/plugin-registry.d.ts +136 -0
- package/dist/types/core/stage-contracts.d.ts +36 -0
- package/dist/types/core/validate-plugin.d.ts +45 -0
- package/dist/types/evaluate/semantic-similarity.d.ts +14 -0
- package/dist/types/grounding/check-answer.d.ts +101 -0
- package/dist/types/retrieval/file-vector-store.d.ts +12 -0
- package/dist/types/testing.d.ts +168 -0
- package/package.json +22 -7
- package/src/adapters/ai-sdk.js +215 -0
- package/src/cache/component-keys.js +38 -0
- package/src/cache/memory-cache.js +87 -0
- package/src/cache/pipeline-cache.js +260 -0
- package/src/cache/redis-cache.js +83 -0
- package/src/cli/commands/ai.js +17 -12
- package/src/cli/commands/plugin-hub.js +18 -13
- package/src/cli/enhanced-cli-commands.js +45 -29
- package/src/cli/handlers/query-handler.js +174 -11
- package/src/cli/services/instrument.js +83 -0
- package/src/cli/services/rag-service.js +51 -12
- package/src/cli/wizard/init-wizard.js +2 -17
- package/src/config/enhanced-ragrc-schema.js +0 -2
- package/src/config/validate-schema.js +4 -1
- package/src/connectors/anthropic-connector.js +22 -2
- package/src/connectors/cohere-connector.js +20 -2
- package/src/connectors/local-embedder.js +20 -0
- package/src/connectors/memory-retriever.js +18 -0
- package/src/connectors/ollama-connector.js +29 -2
- package/src/connectors/openai-connector.js +49 -4
- package/src/core/create-pipeline.js +112 -15
- package/src/core/performance/parallel-processor.js +62 -1
- package/src/core/plugin-registry.js +42 -19
- package/src/core/stage-contracts.js +36 -0
- package/src/core/validate-plugin.js +85 -0
- package/src/debug/execution-tracer.js +14 -4
- package/src/dev/dev-server.js +8 -1
- package/src/dev/hot-reload.js +4 -3
- package/src/enterprise/sso-integration.js +4 -3
- package/src/evaluate/semantic-similarity.js +79 -0
- package/src/grounding/check-answer.js +173 -0
- package/src/index.js +22 -0
- package/src/loader/csv-loader.js +3 -2
- package/src/loader/html-loader.js +3 -1
- package/src/loader/markdown-loader.js +3 -2
- package/src/retrieval/file-vector-store.js +30 -1
- package/src/security/jwt-validator.js +5 -4
- package/src/testing.js +254 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
export type PluginValidationResult = {
|
|
2
|
+
/**
|
|
3
|
+
* - True when there are no errors
|
|
4
|
+
*/
|
|
5
|
+
valid: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* - Problems that stop the plugin from working
|
|
8
|
+
*/
|
|
9
|
+
errors: string[];
|
|
10
|
+
/**
|
|
11
|
+
* - Methods some commands need, e.g. `store`
|
|
12
|
+
* for `rag-pipeline ingest`
|
|
13
|
+
*/
|
|
14
|
+
warnings: string[];
|
|
15
|
+
};
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {Object} PluginValidationResult
|
|
18
|
+
* @property {boolean} valid - True when there are no errors
|
|
19
|
+
* @property {string[]} errors - Problems that stop the plugin from working
|
|
20
|
+
* @property {string[]} warnings - Methods some commands need, e.g. `store`
|
|
21
|
+
* for `rag-pipeline ingest`
|
|
22
|
+
*/
|
|
23
|
+
/**
|
|
24
|
+
* Checks that an object can be used as a pipeline stage, without throwing,
|
|
25
|
+
* so plugin authors can assert on it in their own tests.
|
|
26
|
+
*
|
|
27
|
+
* By default it checks what createRagPipeline() calls. With
|
|
28
|
+
* `{ registry: true }` it also applies the rules pluginRegistry.register()
|
|
29
|
+
* enforces: `metadata` with a string `name` and `version` and a `type`
|
|
30
|
+
* equal to the stage, plus the methods in the stage's contract.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const { validatePlugin } = require('@devilsdev/rag-pipeline-utils');
|
|
34
|
+
* const result = validatePlugin('llm', myLlm, { registry: true });
|
|
35
|
+
* expect(result.errors).toEqual([]);
|
|
36
|
+
*
|
|
37
|
+
* @param {'loader'|'embedder'|'retriever'|'reranker'|'llm'} stage
|
|
38
|
+
* @param {object} plugin - Plugin instance or object
|
|
39
|
+
* @param {Object} [options]
|
|
40
|
+
* @param {boolean} [options.registry=false] - Also check registry rules
|
|
41
|
+
* @returns {PluginValidationResult}
|
|
42
|
+
*/
|
|
43
|
+
export function validatePlugin(stage: "loader" | "embedder" | "retriever" | "reranker" | "llm", plugin: object, options?: {
|
|
44
|
+
registry?: boolean;
|
|
45
|
+
}): PluginValidationResult;
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @param {string} answer - The generated answer
|
|
3
|
+
* @param {string} reference - The reference (ground-truth) answer
|
|
4
|
+
* @param {Object} options
|
|
5
|
+
* @param {Object} options.embedder - Any embedder with embed() or embedQuery()
|
|
6
|
+
* (a built-in connector, LocalEmbedder fitted to your corpus, an AI SDK
|
|
7
|
+
* adapter, HashEmbedder from /testing)
|
|
8
|
+
* @param {AbortSignal} [options.signal]
|
|
9
|
+
* @returns {Promise<number>} Cosine similarity clamped to 0..1
|
|
10
|
+
*/
|
|
11
|
+
export function computeSemanticSimilarity(answer: string, reference: string, { embedder, signal }?: {
|
|
12
|
+
embedder: any;
|
|
13
|
+
signal?: AbortSignal;
|
|
14
|
+
}): Promise<number>;
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
export type AnswerCheck = {
|
|
2
|
+
/**
|
|
3
|
+
* - Every sentence is supported by a source
|
|
4
|
+
*/
|
|
5
|
+
grounded: boolean;
|
|
6
|
+
/**
|
|
7
|
+
* - Share of supported sentences, 0..1
|
|
8
|
+
*/
|
|
9
|
+
groundedness: number;
|
|
10
|
+
/**
|
|
11
|
+
* - Sentences no source supports (this
|
|
12
|
+
* includes a sentence whose numbers do not match its source)
|
|
13
|
+
*/
|
|
14
|
+
unsupported: string[];
|
|
15
|
+
/**
|
|
16
|
+
* - Sources
|
|
17
|
+
* supporting each sentence
|
|
18
|
+
*/
|
|
19
|
+
citations: Array<{
|
|
20
|
+
sentence: string;
|
|
21
|
+
sources: any[];
|
|
22
|
+
}>;
|
|
23
|
+
/**
|
|
24
|
+
* - Faithfulness, relevance, context precision (and context recall when a
|
|
25
|
+
* referenceAnswer is given); only when `evaluate` is not false
|
|
26
|
+
*/
|
|
27
|
+
evaluation: {
|
|
28
|
+
scores: any;
|
|
29
|
+
details: any;
|
|
30
|
+
metadata: any;
|
|
31
|
+
} | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* - The sources as they were checked
|
|
34
|
+
*/
|
|
35
|
+
sources: Array<{
|
|
36
|
+
id: string;
|
|
37
|
+
content: string;
|
|
38
|
+
metadata: any;
|
|
39
|
+
}>;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* @typedef {Object} AnswerCheck
|
|
43
|
+
* @property {boolean} grounded - Every sentence is supported by a source
|
|
44
|
+
* @property {number} groundedness - Share of supported sentences, 0..1
|
|
45
|
+
* @property {string[]} unsupported - Sentences no source supports (this
|
|
46
|
+
* includes a sentence whose numbers do not match its source)
|
|
47
|
+
* @property {Array<{sentence: string, sources: Array}>} citations - Sources
|
|
48
|
+
* supporting each sentence
|
|
49
|
+
* @property {{scores: Object, details: Object, metadata: Object}|undefined} evaluation
|
|
50
|
+
* - Faithfulness, relevance, context precision (and context recall when a
|
|
51
|
+
* referenceAnswer is given); only when `evaluate` is not false
|
|
52
|
+
* @property {Array<{id: string, content: string, metadata: Object}>} sources
|
|
53
|
+
* - The sources as they were checked
|
|
54
|
+
*/
|
|
55
|
+
/**
|
|
56
|
+
* Check an answer against the sources it was generated from.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* // A LangChain chain's output
|
|
60
|
+
* const check = await checkAnswer({
|
|
61
|
+
* question: input,
|
|
62
|
+
* answer: result.answer,
|
|
63
|
+
* sources: result.context, // LangChain Documents
|
|
64
|
+
* });
|
|
65
|
+
* if (!check.grounded) console.warn(check.unsupported);
|
|
66
|
+
*
|
|
67
|
+
* @param {Object} params
|
|
68
|
+
* @param {string} params.answer - The generated answer
|
|
69
|
+
* @param {Array<string|Object>} params.sources - Documents the answer was
|
|
70
|
+
* generated from (see normalizeSources for accepted shapes)
|
|
71
|
+
* @param {string} [params.question] - The question; needed for relevance
|
|
72
|
+
* @param {string} [params.referenceAnswer] - Ground truth, for context recall
|
|
73
|
+
* (and semantic similarity when an embedder is given)
|
|
74
|
+
* @param {Object} [params.embedder] - With a referenceAnswer, adds
|
|
75
|
+
* `evaluation.scores.semanticSimilarity` (computeSemanticSimilarity)
|
|
76
|
+
* @param {boolean} [params.evaluate=true] - Also compute evaluation scores
|
|
77
|
+
* @returns {Promise<AnswerCheck>}
|
|
78
|
+
*/
|
|
79
|
+
export function checkAnswer({ answer, sources, question, referenceAnswer, embedder, evaluate, }?: {
|
|
80
|
+
answer: string;
|
|
81
|
+
sources: Array<string | any>;
|
|
82
|
+
question?: string;
|
|
83
|
+
referenceAnswer?: string;
|
|
84
|
+
embedder?: any;
|
|
85
|
+
evaluate?: boolean;
|
|
86
|
+
}): Promise<AnswerCheck>;
|
|
87
|
+
/**
|
|
88
|
+
* Normalize sources to `{ id, content, metadata }`.
|
|
89
|
+
*
|
|
90
|
+
* Accepts strings, `{ id, content }` (this library), LangChain Documents
|
|
91
|
+
* (`{ pageContent, metadata }`), LlamaIndex nodes (`{ text }`) and
|
|
92
|
+
* `NodeWithScore` (`{ node, score }`).
|
|
93
|
+
*
|
|
94
|
+
* @param {Array<string|Object>} sources
|
|
95
|
+
* @returns {Array<{id: string, content: string, metadata: Object}>}
|
|
96
|
+
*/
|
|
97
|
+
export function normalizeSources(sources: Array<string | any>): Array<{
|
|
98
|
+
id: string;
|
|
99
|
+
content: string;
|
|
100
|
+
metadata: any;
|
|
101
|
+
}>;
|
|
@@ -17,6 +17,18 @@ export class FileVectorStore {
|
|
|
17
17
|
});
|
|
18
18
|
path: string;
|
|
19
19
|
_index: any;
|
|
20
|
+
_loadedDigest: string;
|
|
21
|
+
_cacheId: string;
|
|
22
|
+
_revision: number;
|
|
23
|
+
/**
|
|
24
|
+
* Cache identity for createRagPipeline({ cache }): a digest of the index
|
|
25
|
+
* as loaded, so processes reading the same unchanged index share cached
|
|
26
|
+
* results; after this instance changes the index the key also carries the
|
|
27
|
+
* instance and a revision.
|
|
28
|
+
* @param {'retriever'} role
|
|
29
|
+
* @returns {string|null}
|
|
30
|
+
*/
|
|
31
|
+
getCacheKey(role: "retriever"): string | null;
|
|
20
32
|
_emptyIndex(): {
|
|
21
33
|
version: number;
|
|
22
34
|
embedder: any;
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic embedder: hashed bag of words, L2-normalised. Texts that
|
|
3
|
+
* share words get similar vectors, so retrieval ranks sensibly in tests.
|
|
4
|
+
*/
|
|
5
|
+
export class HashEmbedder {
|
|
6
|
+
/**
|
|
7
|
+
* @param {Object} [options]
|
|
8
|
+
* @param {number} [options.dimensions=256] - Vector length
|
|
9
|
+
*/
|
|
10
|
+
constructor({ dimensions }?: {
|
|
11
|
+
dimensions?: number;
|
|
12
|
+
});
|
|
13
|
+
dimensions: number;
|
|
14
|
+
metadata: {
|
|
15
|
+
name: string;
|
|
16
|
+
version: string;
|
|
17
|
+
type: string;
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Cache identity for createRagPipeline({ cache }).
|
|
21
|
+
* @param {string} role
|
|
22
|
+
* @returns {string|null}
|
|
23
|
+
*/
|
|
24
|
+
getCacheKey(role: string): string | null;
|
|
25
|
+
/** @private */
|
|
26
|
+
private _vector;
|
|
27
|
+
/**
|
|
28
|
+
* @param {string} text
|
|
29
|
+
* @param {{ signal?: AbortSignal }} [options]
|
|
30
|
+
* @returns {Promise<number[]>}
|
|
31
|
+
*/
|
|
32
|
+
embed(text: string, { signal }?: {
|
|
33
|
+
signal?: AbortSignal;
|
|
34
|
+
}): Promise<number[]>;
|
|
35
|
+
/**
|
|
36
|
+
* Same as embed().
|
|
37
|
+
* @param {string} text
|
|
38
|
+
* @param {{ signal?: AbortSignal }} [options]
|
|
39
|
+
* @returns {Promise<number[]>}
|
|
40
|
+
*/
|
|
41
|
+
embedQuery(text: string, options?: {
|
|
42
|
+
signal?: AbortSignal;
|
|
43
|
+
}): Promise<number[]>;
|
|
44
|
+
/**
|
|
45
|
+
* @param {string[]} texts
|
|
46
|
+
* @param {{ signal?: AbortSignal }} [options]
|
|
47
|
+
* @returns {Promise<number[][]>}
|
|
48
|
+
*/
|
|
49
|
+
embedBatch(texts: string[], { signal }?: {
|
|
50
|
+
signal?: AbortSignal;
|
|
51
|
+
}): Promise<number[][]>;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* LLM stand-in. Answers with `answer` (a string, or a function of the query
|
|
55
|
+
* and documents), or else with the top document's content. Every call is
|
|
56
|
+
* recorded in `calls`, so tests can assert on what the pipeline sent.
|
|
57
|
+
*/
|
|
58
|
+
export class EchoLLM {
|
|
59
|
+
/**
|
|
60
|
+
* @param {Object} [options]
|
|
61
|
+
* @param {string|function(string, Array<Object>): string} [options.answer]
|
|
62
|
+
* @param {string} [options.fallback='No information found.'] - Answer when
|
|
63
|
+
* there are no documents and no `answer`
|
|
64
|
+
*/
|
|
65
|
+
constructor({ answer, fallback }?: {
|
|
66
|
+
answer?: string | ((arg0: string, arg1: Array<any>) => string);
|
|
67
|
+
fallback?: string;
|
|
68
|
+
});
|
|
69
|
+
answer: string | ((arg0: string, arg1: Array<any>) => string);
|
|
70
|
+
fallback: string;
|
|
71
|
+
/** @type {Array<{query: string, docs: Array<Object>, options: Object}>} */
|
|
72
|
+
calls: Array<{
|
|
73
|
+
query: string;
|
|
74
|
+
docs: Array<any>;
|
|
75
|
+
options: any;
|
|
76
|
+
}>;
|
|
77
|
+
metadata: {
|
|
78
|
+
name: string;
|
|
79
|
+
version: string;
|
|
80
|
+
type: string;
|
|
81
|
+
};
|
|
82
|
+
/**
|
|
83
|
+
* Cache identity for createRagPipeline({ cache }); none when `answer` is a
|
|
84
|
+
* function, since its output cannot be identified.
|
|
85
|
+
* @param {string} role
|
|
86
|
+
* @returns {string|null}
|
|
87
|
+
*/
|
|
88
|
+
getCacheKey(role: string): string | null;
|
|
89
|
+
/** @private */
|
|
90
|
+
private _answer;
|
|
91
|
+
/**
|
|
92
|
+
* @param {string} query
|
|
93
|
+
* @param {Array<Object>} [docs]
|
|
94
|
+
* @param {{ signal?: AbortSignal }} [options]
|
|
95
|
+
* @returns {Promise<string>}
|
|
96
|
+
*/
|
|
97
|
+
generate(query: string, docs?: Array<any>, options?: {
|
|
98
|
+
signal?: AbortSignal;
|
|
99
|
+
}): Promise<string>;
|
|
100
|
+
/**
|
|
101
|
+
* Streams the same answer word by word, then a done chunk.
|
|
102
|
+
* @param {string} query
|
|
103
|
+
* @param {Array<Object>} [docs]
|
|
104
|
+
* @param {{ signal?: AbortSignal }} [options]
|
|
105
|
+
* @yields {{ token: string, done: boolean }}
|
|
106
|
+
*/
|
|
107
|
+
generateStream(query: string, docs?: Array<any>, options?: {
|
|
108
|
+
signal?: AbortSignal;
|
|
109
|
+
}): AsyncGenerator<{
|
|
110
|
+
token: string;
|
|
111
|
+
done: boolean;
|
|
112
|
+
}, void, unknown>;
|
|
113
|
+
}
|
|
114
|
+
/** Short documents for tests and examples. */
|
|
115
|
+
export const fixtures: Readonly<{
|
|
116
|
+
documents: readonly (Readonly<{
|
|
117
|
+
id: "vacation";
|
|
118
|
+
content: "Full-time employees receive twenty days of paid vacation per year. Unused days carry over to March.";
|
|
119
|
+
metadata: Readonly<{
|
|
120
|
+
source: "vacation.md";
|
|
121
|
+
}>;
|
|
122
|
+
}> | Readonly<{
|
|
123
|
+
id: "remote-work";
|
|
124
|
+
content: "Employees may work remotely up to three days per week with manager approval.";
|
|
125
|
+
metadata: Readonly<{
|
|
126
|
+
source: "remote-work.md";
|
|
127
|
+
}>;
|
|
128
|
+
}> | Readonly<{
|
|
129
|
+
id: "expenses";
|
|
130
|
+
content: "Submit receipts within thirty days. Travel above 500 EUR needs pre-approval from finance.";
|
|
131
|
+
metadata: Readonly<{
|
|
132
|
+
source: "expenses.md";
|
|
133
|
+
}>;
|
|
134
|
+
}>)[];
|
|
135
|
+
}>;
|
|
136
|
+
/**
|
|
137
|
+
* A pipeline with HashEmbedder, a MemoryRetriever already holding
|
|
138
|
+
* `documents`, and EchoLLM (or the llm you pass).
|
|
139
|
+
*
|
|
140
|
+
* @example
|
|
141
|
+
* const { createTestPipeline } = require('@devilsdev/rag-pipeline-utils/testing');
|
|
142
|
+
* const { pipeline, llm } = await createTestPipeline();
|
|
143
|
+
* const result = await pipeline.run({ query: 'How many vacation days?' });
|
|
144
|
+
* // result.results[0].id === 'vacation'; llm.calls.length === 1
|
|
145
|
+
*
|
|
146
|
+
* @param {Object} [options]
|
|
147
|
+
* @param {Array<{id: string, content: string, metadata?: Object}>} [options.documents=fixtures.documents]
|
|
148
|
+
* @param {Object} [options.embedder] - Defaults to a new HashEmbedder
|
|
149
|
+
* @param {Object|null} [options.llm] - Defaults to a new EchoLLM; null for retrieval only
|
|
150
|
+
* @param {Object} [options.pipeline] - Extra createRagPipeline options (e.g. reranker)
|
|
151
|
+
* @returns {Promise<{ pipeline: Object, embedder: Object, retriever: MemoryRetriever, llm: Object|null }>}
|
|
152
|
+
*/
|
|
153
|
+
export function createTestPipeline({ documents, embedder, llm, pipeline: pipelineOptions, }?: {
|
|
154
|
+
documents?: Array<{
|
|
155
|
+
id: string;
|
|
156
|
+
content: string;
|
|
157
|
+
metadata?: any;
|
|
158
|
+
}>;
|
|
159
|
+
embedder?: any;
|
|
160
|
+
llm?: any | null;
|
|
161
|
+
pipeline?: any;
|
|
162
|
+
}): Promise<{
|
|
163
|
+
pipeline: any;
|
|
164
|
+
embedder: any;
|
|
165
|
+
retriever: MemoryRetriever;
|
|
166
|
+
llm: any | null;
|
|
167
|
+
}>;
|
|
168
|
+
import { MemoryRetriever } from "./connectors/memory-retriever";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@devilsdev/rag-pipeline-utils",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Composable retrieval-augmented generation (RAG) for Node.js: grounded citations, evaluation, guardrails, hybrid retrieval and a CLI.",
|
|
5
5
|
"main": "dist/index.cjs",
|
|
6
6
|
"type": "commonjs",
|
|
@@ -9,6 +9,9 @@
|
|
|
9
9
|
"*": {
|
|
10
10
|
"experimental": [
|
|
11
11
|
"dist/experimental.d.ts"
|
|
12
|
+
],
|
|
13
|
+
"testing": [
|
|
14
|
+
"dist/testing.d.ts"
|
|
12
15
|
]
|
|
13
16
|
}
|
|
14
17
|
},
|
|
@@ -23,6 +26,11 @@
|
|
|
23
26
|
"require": "./dist/experimental.cjs",
|
|
24
27
|
"import": "./dist/experimental.mjs"
|
|
25
28
|
},
|
|
29
|
+
"./testing": {
|
|
30
|
+
"types": "./dist/testing.d.ts",
|
|
31
|
+
"require": "./dist/testing.cjs",
|
|
32
|
+
"import": "./dist/testing.mjs"
|
|
33
|
+
},
|
|
26
34
|
"./cli": "./bin/cli.js"
|
|
27
35
|
},
|
|
28
36
|
"bin": {
|
|
@@ -59,16 +67,16 @@
|
|
|
59
67
|
"lint-staged": "lint-staged",
|
|
60
68
|
"postinstall": "husky install",
|
|
61
69
|
"verify:mocks": "node scripts/verify-fixture-mocks.js",
|
|
62
|
-
"build:docs": "
|
|
70
|
+
"build:docs": "npm run docs:build",
|
|
63
71
|
"ci": "node scripts/ci-runner.js",
|
|
64
|
-
"docs:dev": "
|
|
72
|
+
"docs:dev": "cd docs-site && npm run start",
|
|
65
73
|
"docs:version": "npx docusaurus docs:version",
|
|
66
74
|
"watch:lqip": "node docs-site/scripts/watch-images.js",
|
|
67
75
|
"docs:clear": "docusaurus clear --site-dir docs-site",
|
|
68
|
-
"docs:start": "
|
|
76
|
+
"docs:start": "cd docs-site && npm run start",
|
|
69
77
|
"docs:build": "cd docs-site && npm run build",
|
|
70
78
|
"docs:serve": "docusaurus serve docs-site",
|
|
71
|
-
"docs:deploy": "docusaurus deploy docs-site",
|
|
79
|
+
"docs:deploy": "node docs-site/scripts/sync-changelog.js && docusaurus deploy docs-site",
|
|
72
80
|
"roadmap:close": "node scripts/close-done-roadmap-issues.js",
|
|
73
81
|
"labels:sync": "node scripts/ensure-roadmap-labels.js",
|
|
74
82
|
"release:tag": "node scripts/generate-release-note.js v$(node -p \"require('./package.json').version\")",
|
|
@@ -176,8 +184,12 @@
|
|
|
176
184
|
"devDependencies": {
|
|
177
185
|
"@babel/core": "^7.26.10",
|
|
178
186
|
"@babel/preset-env": "^7.26.9",
|
|
179
|
-
"@docusaurus/core": "3.10.
|
|
180
|
-
"@docusaurus/preset-classic": "3.10.
|
|
187
|
+
"@docusaurus/core": "3.10.2",
|
|
188
|
+
"@docusaurus/preset-classic": "3.10.2",
|
|
189
|
+
"@docusaurus/theme-mermaid": "3.10.2",
|
|
190
|
+
"@easyops-cn/docusaurus-search-local": "0.55.3",
|
|
191
|
+
"@fontsource/inter": "5.3.0",
|
|
192
|
+
"@fontsource/jetbrains-mono": "5.3.0",
|
|
181
193
|
"@octokit/rest": "^21.1.1",
|
|
182
194
|
"@opentelemetry/api": "^1.9.0",
|
|
183
195
|
"@opentelemetry/auto-instrumentations-node": "^0.52.1",
|
|
@@ -192,8 +204,11 @@
|
|
|
192
204
|
"@types/jest": "^29.5.14",
|
|
193
205
|
"@types/node": "22.14.1",
|
|
194
206
|
"acorn": "8.15.0",
|
|
207
|
+
"ai": "6.0.317",
|
|
208
|
+
"ai-v7": "npm:ai@7.0.156",
|
|
195
209
|
"babel-jest": "^29.7.0",
|
|
196
210
|
"cross-env": "7.0.3",
|
|
211
|
+
"docusaurus-plugin-llms": "0.6.1",
|
|
197
212
|
"esbuild": "^0.25.2",
|
|
198
213
|
"eslint": "^8.57.1",
|
|
199
214
|
"eslint-plugin-storybook": "^0.12.0",
|
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Adapter for the Vercel AI SDK (`ai` package): use an AI SDK embedding
|
|
5
|
+
* model and language model as this library's `embedder` and `llm`.
|
|
6
|
+
*
|
|
7
|
+
* The AI SDK is not a dependency. You pass in the functions you already
|
|
8
|
+
* import from it, so any AI SDK version from 5 on works, from ESM or
|
|
9
|
+
* CommonJS, and this package keeps supporting Node 18 (AI SDK 7 needs Node
|
|
10
|
+
* 22 and is ESM-only).
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const { buildRagPrompt } = require('../connectors/rag-prompt');
|
|
14
|
+
const { componentCacheKey } = require('../cache/component-keys');
|
|
15
|
+
|
|
16
|
+
/** Identity of an AI SDK model object (provider and model id). */
|
|
17
|
+
const modelId = (model) =>
|
|
18
|
+
`${model.provider || 'unknown'}:${model.modelId || model.modelName || 'unknown'}`;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {Object} AiSdkFunctions
|
|
22
|
+
* @property {Function} [embed] - `embed` from 'ai' (needed for an embedder)
|
|
23
|
+
* @property {Function} [embedMany] - `embedMany` from 'ai' (batch embedding)
|
|
24
|
+
* @property {Function} [generateText] - `generateText` from 'ai' (needed for an llm)
|
|
25
|
+
* @property {Function} [streamText] - `streamText` from 'ai' (streaming)
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
function requireFunction(ai, name, purpose) {
|
|
29
|
+
if (typeof ai[name] !== 'function') {
|
|
30
|
+
throw new Error(
|
|
31
|
+
`createAiSdkAdapter: ${purpose} needs ai.${name}. Pass the AI SDK functions, e.g. import { ${name} } from "ai".`,
|
|
32
|
+
);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Options for every generateText / streamText call. The RAG system prompt
|
|
38
|
+
* (retrieved text is untrusted reference data, not instructions) is passed
|
|
39
|
+
* as both `instructions` (AI SDK 7) and `system` (AI SDK 5 and 6; 7 reads
|
|
40
|
+
* `instructions ?? system`), so it reaches the model on every version.
|
|
41
|
+
*/
|
|
42
|
+
function textCallOptions(model, query, docs, settings, signal, systemPrompt) {
|
|
43
|
+
const { system, user } = buildRagPrompt(query, docs, { systemPrompt });
|
|
44
|
+
return {
|
|
45
|
+
...settings,
|
|
46
|
+
model,
|
|
47
|
+
...(system && { system, instructions: system }),
|
|
48
|
+
prompt: user,
|
|
49
|
+
...(signal && { abortSignal: signal }),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Per-call settings the pipeline may pass through to the model. */
|
|
54
|
+
const CALL_SETTINGS = [
|
|
55
|
+
'temperature',
|
|
56
|
+
'maxOutputTokens',
|
|
57
|
+
'topP',
|
|
58
|
+
'topK',
|
|
59
|
+
'presencePenalty',
|
|
60
|
+
'frequencyPenalty',
|
|
61
|
+
'seed',
|
|
62
|
+
'stopSequences',
|
|
63
|
+
'maxRetries',
|
|
64
|
+
];
|
|
65
|
+
|
|
66
|
+
function pickSettings(options) {
|
|
67
|
+
const out = {};
|
|
68
|
+
for (const key of CALL_SETTINGS) {
|
|
69
|
+
if (options[key] !== undefined) out[key] = options[key];
|
|
70
|
+
}
|
|
71
|
+
return out;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Wrap AI SDK models as an embedder and an llm for createRagPipeline().
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* import { embed, embedMany, generateText, streamText } from "ai";
|
|
79
|
+
* import { openai } from "@ai-sdk/openai";
|
|
80
|
+
* import { createAiSdkAdapter, createRagPipeline } from "@devilsdev/rag-pipeline-utils";
|
|
81
|
+
*
|
|
82
|
+
* const { embedder, llm } = createAiSdkAdapter({
|
|
83
|
+
* ai: { embed, embedMany, generateText, streamText },
|
|
84
|
+
* embeddingModel: openai.embedding("text-embedding-3-small"),
|
|
85
|
+
* languageModel: openai("gpt-4o-mini"),
|
|
86
|
+
* });
|
|
87
|
+
* const pipeline = createRagPipeline({ embedder, retriever, llm });
|
|
88
|
+
*
|
|
89
|
+
* @param {Object} options
|
|
90
|
+
* @param {AiSdkFunctions} options.ai - Functions imported from 'ai'
|
|
91
|
+
* @param {Object} [options.embeddingModel] - AI SDK embedding model
|
|
92
|
+
* @param {Object} [options.languageModel] - AI SDK language model
|
|
93
|
+
* @param {Object} [options.settings] - Defaults for every text call, e.g.
|
|
94
|
+
* `{ temperature: 0, maxOutputTokens: 512 }`
|
|
95
|
+
* @param {string} [options.systemPrompt] - Replaces the built-in RAG system
|
|
96
|
+
* prompt
|
|
97
|
+
* @returns {{ embedder: Object|null, llm: Object|null }}
|
|
98
|
+
*/
|
|
99
|
+
function createAiSdkAdapter(options = {}) {
|
|
100
|
+
const {
|
|
101
|
+
ai,
|
|
102
|
+
embeddingModel,
|
|
103
|
+
languageModel,
|
|
104
|
+
settings = {},
|
|
105
|
+
systemPrompt,
|
|
106
|
+
} = options;
|
|
107
|
+
if (!ai || typeof ai !== 'object') {
|
|
108
|
+
throw new Error(
|
|
109
|
+
'createAiSdkAdapter: pass `ai`, the AI SDK functions, e.g. { embed, embedMany, generateText, streamText } from "ai".',
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
if (!embeddingModel && !languageModel) {
|
|
113
|
+
throw new Error(
|
|
114
|
+
'createAiSdkAdapter: pass an embeddingModel, a languageModel, or both.',
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
let embedder = null;
|
|
119
|
+
if (embeddingModel) {
|
|
120
|
+
requireFunction(ai, 'embed', 'an embeddingModel');
|
|
121
|
+
const embedOne = async (text, { signal } = {}) => {
|
|
122
|
+
const { embedding } = await ai.embed({
|
|
123
|
+
model: embeddingModel,
|
|
124
|
+
value: String(text),
|
|
125
|
+
...(signal && { abortSignal: signal }),
|
|
126
|
+
});
|
|
127
|
+
return embedding;
|
|
128
|
+
};
|
|
129
|
+
embedder = {
|
|
130
|
+
metadata: { name: 'ai-sdk-embedder', version: '1.0.0', type: 'embedder' },
|
|
131
|
+
getCacheKey: (role) =>
|
|
132
|
+
role === 'embedder'
|
|
133
|
+
? componentCacheKey('ai-sdk-embed', {
|
|
134
|
+
model: modelId(embeddingModel),
|
|
135
|
+
})
|
|
136
|
+
: null,
|
|
137
|
+
embed: embedOne,
|
|
138
|
+
embedQuery: embedOne,
|
|
139
|
+
/** One vector per text; uses embedMany when it was passed in. */
|
|
140
|
+
async embedBatch(texts, { signal } = {}) {
|
|
141
|
+
if (typeof ai.embedMany !== 'function') {
|
|
142
|
+
const vectors = [];
|
|
143
|
+
for (const text of texts)
|
|
144
|
+
vectors.push(await embedOne(text, { signal }));
|
|
145
|
+
return vectors;
|
|
146
|
+
}
|
|
147
|
+
const { embeddings } = await ai.embedMany({
|
|
148
|
+
model: embeddingModel,
|
|
149
|
+
values: texts.map(String),
|
|
150
|
+
...(signal && { abortSignal: signal }),
|
|
151
|
+
});
|
|
152
|
+
return embeddings;
|
|
153
|
+
},
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let llm = null;
|
|
158
|
+
if (languageModel) {
|
|
159
|
+
requireFunction(ai, 'generateText', 'a languageModel');
|
|
160
|
+
llm = {
|
|
161
|
+
metadata: { name: 'ai-sdk-llm', version: '1.0.0', type: 'llm' },
|
|
162
|
+
// The AI SDK leaves an unset temperature to the provider, whose default
|
|
163
|
+
// is usually 1, so unset counts as non-deterministic: generation
|
|
164
|
+
// caching needs `settings.temperature: 0`.
|
|
165
|
+
config: { temperature: settings.temperature ?? 1 },
|
|
166
|
+
getCacheKey: (role) =>
|
|
167
|
+
role === 'llm'
|
|
168
|
+
? componentCacheKey('ai-sdk', {
|
|
169
|
+
model: modelId(languageModel),
|
|
170
|
+
settings: JSON.stringify(settings),
|
|
171
|
+
systemPrompt,
|
|
172
|
+
})
|
|
173
|
+
: null,
|
|
174
|
+
async generate(query, docs = [], callOptions = {}) {
|
|
175
|
+
const { text } = await ai.generateText(
|
|
176
|
+
textCallOptions(
|
|
177
|
+
languageModel,
|
|
178
|
+
query,
|
|
179
|
+
docs,
|
|
180
|
+
{ ...settings, ...pickSettings(callOptions) },
|
|
181
|
+
callOptions.signal,
|
|
182
|
+
systemPrompt,
|
|
183
|
+
),
|
|
184
|
+
);
|
|
185
|
+
return text;
|
|
186
|
+
},
|
|
187
|
+
};
|
|
188
|
+
if (typeof ai.streamText === 'function') {
|
|
189
|
+
llm.generateStream = async function* generateStream(
|
|
190
|
+
query,
|
|
191
|
+
docs = [],
|
|
192
|
+
callOptions = {},
|
|
193
|
+
) {
|
|
194
|
+
const result = ai.streamText(
|
|
195
|
+
textCallOptions(
|
|
196
|
+
languageModel,
|
|
197
|
+
query,
|
|
198
|
+
docs,
|
|
199
|
+
{ ...settings, ...pickSettings(callOptions) },
|
|
200
|
+
callOptions.signal,
|
|
201
|
+
systemPrompt,
|
|
202
|
+
),
|
|
203
|
+
);
|
|
204
|
+
for await (const token of result.textStream) {
|
|
205
|
+
yield { token, done: false };
|
|
206
|
+
}
|
|
207
|
+
yield { token: '', done: true };
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
return { embedder, llm };
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
module.exports = { createAiSdkAdapter };
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Builds the cache identity a built-in component reports through
|
|
5
|
+
* getCacheKey(role): every setting that changes its output, so two
|
|
6
|
+
* configurations never share cached results.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const crypto = require('crypto');
|
|
10
|
+
|
|
11
|
+
const digest = (value) =>
|
|
12
|
+
crypto
|
|
13
|
+
.createHash('sha256')
|
|
14
|
+
.update(String(value ?? ''))
|
|
15
|
+
.digest('hex')
|
|
16
|
+
.slice(0, 16);
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* @param {string} kind - e.g. 'openai'
|
|
20
|
+
* @param {Object} fields - Settings that change the output
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
function componentCacheKey(kind, fields) {
|
|
24
|
+
const parts = Object.keys(fields)
|
|
25
|
+
.sort()
|
|
26
|
+
.map((name) => {
|
|
27
|
+
const value = fields[name];
|
|
28
|
+
// Long free text (system prompts) is hashed to keep keys short.
|
|
29
|
+
const text =
|
|
30
|
+
typeof value === 'string' && value.length > 64
|
|
31
|
+
? `#${digest(value)}`
|
|
32
|
+
: value;
|
|
33
|
+
return `${name}=${text === undefined ? '' : text}`;
|
|
34
|
+
});
|
|
35
|
+
return `${kind}|${parts.join('|')}`;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
module.exports = { componentCacheKey, digest };
|