@devilsdev/rag-core 3.2.0 → 3.4.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/dist/all.d.ts +5 -0
- package/dist/index.cjs +65 -7
- package/dist/types/adapters/ai-sdk-middleware.d.ts +173 -0
- package/dist/types/adapters/mastra.d.ts +55 -0
- package/dist/types/evaluate/scoring.d.ts +41 -1
- package/dist/types/grounding/check-answer.d.ts +109 -1
- package/dist/types/grounding/llm-judge.d.ts +86 -0
- package/dist/types/testing/matchers.d.ts +53 -0
- package/dist/types/testing.d.ts +5 -1
- package/package.json +1 -1
package/dist/all.d.ts
CHANGED
|
@@ -410,6 +410,7 @@ export { scoreAnswer } from "./types/evaluate/scoring";
|
|
|
410
410
|
export { computeBLEU } from "./types/evaluate/scoring";
|
|
411
411
|
export { computeROUGE } from "./types/evaluate/scoring";
|
|
412
412
|
export { checkNumericConsistency } from "./types/evaluate/scoring";
|
|
413
|
+
export { checkPolarityConsistency } from "./types/evaluate/scoring";
|
|
413
414
|
export { extractNumbers } from "./types/evaluate/scoring";
|
|
414
415
|
export { computeFaithfulness } from "./types/evaluate/faithfulness";
|
|
415
416
|
export { computeSemanticSimilarity } from "./types/evaluate/semantic-similarity";
|
|
@@ -429,6 +430,10 @@ export { CitationTracker } from "./types/citation/citation-tracker";
|
|
|
429
430
|
export { checkAnswer } from "./types/grounding/check-answer";
|
|
430
431
|
export { normalizeSources } from "./types/grounding/check-answer";
|
|
431
432
|
export { createAiSdkAdapter } from "./types/adapters/ai-sdk";
|
|
433
|
+
export { createMastraScorer } from "./types/adapters/mastra";
|
|
434
|
+
export { createMastraGuardrails } from "./types/adapters/mastra";
|
|
435
|
+
export { groundingMiddleware } from "./types/adapters/ai-sdk-middleware";
|
|
436
|
+
export { GroundingBlockedError } from "./types/adapters/ai-sdk-middleware";
|
|
432
437
|
export { MemoryCache } from "./types/cache/memory-cache";
|
|
433
438
|
export { RedisCache } from "./types/cache/redis-cache";
|
|
434
439
|
export { mapSentenceToSources } from "./types/citation/source-mapper";
|
package/dist/index.cjs
CHANGED
|
@@ -1296,7 +1296,7 @@ var require_package = __commonJS({
|
|
|
1296
1296
|
"package.json"(exports2, module2) {
|
|
1297
1297
|
module2.exports = {
|
|
1298
1298
|
name: "@devilsdev/rag-pipeline-utils",
|
|
1299
|
-
version: "3.
|
|
1299
|
+
version: "3.4.0",
|
|
1300
1300
|
description: "Composable retrieval-augmented generation (RAG) for Node.js: grounded citations, evaluation, guardrails, hybrid retrieval and a CLI.",
|
|
1301
1301
|
main: "dist/index.cjs",
|
|
1302
1302
|
type: "commonjs",
|
|
@@ -1542,7 +1542,7 @@ var require_package = __commonJS({
|
|
|
1542
1542
|
prettier: "^3.5.3",
|
|
1543
1543
|
"prop-types": "^15.8.1",
|
|
1544
1544
|
rimraf: "^6.0.1",
|
|
1545
|
-
sharp: "^0.
|
|
1545
|
+
sharp: "^0.35.5",
|
|
1546
1546
|
"ts-jest": "^29.3.2",
|
|
1547
1547
|
typedoc: "0.28.20",
|
|
1548
1548
|
"typedoc-plugin-markdown": "4.14.1",
|
|
@@ -1550,10 +1550,10 @@ var require_package = __commonJS({
|
|
|
1550
1550
|
"why-is-node-running": "^3.2.2"
|
|
1551
1551
|
},
|
|
1552
1552
|
dependencies: {
|
|
1553
|
-
"@devilsdev/rag-connectors": "3.
|
|
1554
|
-
"@devilsdev/rag-core": "3.
|
|
1555
|
-
"@devilsdev/rag-eval": "3.
|
|
1556
|
-
"@devilsdev/rag-guardrails": "3.
|
|
1553
|
+
"@devilsdev/rag-connectors": "3.4.0",
|
|
1554
|
+
"@devilsdev/rag-core": "3.4.0",
|
|
1555
|
+
"@devilsdev/rag-eval": "3.4.0",
|
|
1556
|
+
"@devilsdev/rag-guardrails": "3.4.0",
|
|
1557
1557
|
ajv: "^8.17.1",
|
|
1558
1558
|
axios: "^1.21.0",
|
|
1559
1559
|
chokidar: "^4.0.3",
|
|
@@ -1590,14 +1590,16 @@ var require_package = __commonJS({
|
|
|
1590
1590
|
},
|
|
1591
1591
|
overrides: {
|
|
1592
1592
|
"docusaurus-plugin-llms": {
|
|
1593
|
-
minimatch: "^
|
|
1593
|
+
minimatch: "^11.0.1",
|
|
1594
1594
|
yaml: "^2.8.3"
|
|
1595
1595
|
},
|
|
1596
1596
|
"fast-uri": "^3.1.9",
|
|
1597
1597
|
"follow-redirects": "^1.16.2",
|
|
1598
1598
|
"form-data": "^4.0.6",
|
|
1599
1599
|
qs: "^6.16.1",
|
|
1600
|
+
"serialize-javascript": "^7.1.3",
|
|
1600
1601
|
tinypool: "^2.2.0",
|
|
1602
|
+
uuid: "^11.1.1",
|
|
1601
1603
|
"ws@^8.0.0": "^8.21.0",
|
|
1602
1604
|
zod: "^4.6.7"
|
|
1603
1605
|
}
|
|
@@ -2443,6 +2445,56 @@ var require_scoring = __commonJS({
|
|
|
2443
2445
|
const missing = claimNumbers.filter((n) => !docNumbers.has(n));
|
|
2444
2446
|
return { consistent: missing.length === 0, claimNumbers, missing };
|
|
2445
2447
|
}
|
|
2448
|
+
var NEGATION_WORDS = new Set(
|
|
2449
|
+
"not no never none nobody nothing nowhere neither nor cannot without dont doesnt didnt isnt arent wasnt werent wont wouldnt shouldnt couldnt cant hasnt havent hadnt mustnt neednt".split(" ")
|
|
2450
|
+
);
|
|
2451
|
+
var NON_NEGATING_NEXT = {
|
|
2452
|
+
not: /* @__PURE__ */ new Set(["only", "just", "merely"]),
|
|
2453
|
+
no: /* @__PURE__ */ new Set(["doubt", "matter"]),
|
|
2454
|
+
nothing: /* @__PURE__ */ new Set(["but"])
|
|
2455
|
+
};
|
|
2456
|
+
function isNegated(tokens) {
|
|
2457
|
+
let count = 0;
|
|
2458
|
+
for (let i = 0; i < tokens.length; i++) {
|
|
2459
|
+
const t = tokens[i];
|
|
2460
|
+
if (!NEGATION_WORDS.has(t)) continue;
|
|
2461
|
+
if (NON_NEGATING_NEXT[t] && NON_NEGATING_NEXT[t].has(tokens[i + 1])) {
|
|
2462
|
+
continue;
|
|
2463
|
+
}
|
|
2464
|
+
count += 1;
|
|
2465
|
+
}
|
|
2466
|
+
return count % 2 === 1;
|
|
2467
|
+
}
|
|
2468
|
+
function checkPolarityConsistency(claimText, docText) {
|
|
2469
|
+
const claimTokens = tokenize(claimText);
|
|
2470
|
+
const claimNegated = isNegated(claimTokens);
|
|
2471
|
+
const claimWords = [
|
|
2472
|
+
...new Set(
|
|
2473
|
+
contentTokens(claimTokens.filter((t) => !NEGATION_WORDS.has(t)))
|
|
2474
|
+
)
|
|
2475
|
+
];
|
|
2476
|
+
if (claimWords.length === 0) {
|
|
2477
|
+
return { consistent: true, claimNegated, sourceSentence: null };
|
|
2478
|
+
}
|
|
2479
|
+
const candidates = splitSentences(docText).map((sentence) => {
|
|
2480
|
+
const tokens = tokenize(sentence);
|
|
2481
|
+
const set = new Set(tokens);
|
|
2482
|
+
const support = claimWords.filter((t) => set.has(t)).length / claimWords.length;
|
|
2483
|
+
return { sentence, tokens, support };
|
|
2484
|
+
}).filter((c) => c.support > 0).sort((a, b) => b.support - a.support);
|
|
2485
|
+
if (candidates.length === 0) {
|
|
2486
|
+
return { consistent: true, claimNegated, sourceSentence: null };
|
|
2487
|
+
}
|
|
2488
|
+
const best = candidates[0].support;
|
|
2489
|
+
const compared = candidates.filter((c) => c.support === best);
|
|
2490
|
+
const match = compared.find((c) => isNegated(c.tokens) === claimNegated);
|
|
2491
|
+
return {
|
|
2492
|
+
consistent: Boolean(match),
|
|
2493
|
+
claimNegated,
|
|
2494
|
+
sourceSentence: (match || compared[0]).sentence
|
|
2495
|
+
};
|
|
2496
|
+
}
|
|
2497
|
+
var POLARITY_MISMATCH_FACTOR = 0.4;
|
|
2446
2498
|
var NUMERIC_MISMATCH_FACTOR = 0.4;
|
|
2447
2499
|
function computeSupportScore(claimTokens, docTokens, idfWeights = null, options = {}) {
|
|
2448
2500
|
const claim = [...new Set(contentTokens(claimTokens))];
|
|
@@ -2463,6 +2515,9 @@ var require_scoring = __commonJS({
|
|
|
2463
2515
|
const share = (numeric.claimNumbers.length - numeric.missing.length) / numeric.claimNumbers.length;
|
|
2464
2516
|
score *= NUMERIC_MISMATCH_FACTOR * share;
|
|
2465
2517
|
}
|
|
2518
|
+
if (!checkPolarityConsistency(claimText, docText).consistent) {
|
|
2519
|
+
score *= POLARITY_MISMATCH_FACTOR;
|
|
2520
|
+
}
|
|
2466
2521
|
}
|
|
2467
2522
|
return score;
|
|
2468
2523
|
}
|
|
@@ -2471,8 +2526,11 @@ var require_scoring = __commonJS({
|
|
|
2471
2526
|
contentTokens,
|
|
2472
2527
|
computeSupportScore,
|
|
2473
2528
|
checkNumericConsistency,
|
|
2529
|
+
checkPolarityConsistency,
|
|
2530
|
+
isNegated,
|
|
2474
2531
|
extractNumbers,
|
|
2475
2532
|
NUMERIC_MISMATCH_FACTOR,
|
|
2533
|
+
POLARITY_MISMATCH_FACTOR,
|
|
2476
2534
|
scoreAnswer,
|
|
2477
2535
|
tokenize,
|
|
2478
2536
|
computeBLEU,
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
export type GroundingMiddlewareOptions = {
|
|
2
|
+
/**
|
|
3
|
+
* - Any retriever plugin (`retrieve()`); needed unless `pipeline` is given
|
|
4
|
+
*/
|
|
5
|
+
retriever?: any;
|
|
6
|
+
/**
|
|
7
|
+
* - Embeds the query for vector retrievers
|
|
8
|
+
*/
|
|
9
|
+
embedder?: any;
|
|
10
|
+
/**
|
|
11
|
+
* - Optional reranker plugin
|
|
12
|
+
*/
|
|
13
|
+
reranker?: any;
|
|
14
|
+
/**
|
|
15
|
+
* - A createRagPipeline() result to retrieve with, instead of retriever/embedder/reranker; its llm is not called
|
|
16
|
+
*/
|
|
17
|
+
pipeline?: any;
|
|
18
|
+
/**
|
|
19
|
+
* - Passages per call
|
|
20
|
+
*/
|
|
21
|
+
topK?: number;
|
|
22
|
+
/**
|
|
23
|
+
* - Token budget for the passages (see packContext)
|
|
24
|
+
*/
|
|
25
|
+
maxContextTokens?: number;
|
|
26
|
+
/**
|
|
27
|
+
* - Token counter for maxContextTokens
|
|
28
|
+
*/
|
|
29
|
+
countTokens?: Function;
|
|
30
|
+
/**
|
|
31
|
+
* - Replaces the default RAG system prompt
|
|
32
|
+
*/
|
|
33
|
+
systemPrompt?: string;
|
|
34
|
+
/**
|
|
35
|
+
* - Run checkAnswer on the answer
|
|
36
|
+
*/
|
|
37
|
+
check?: boolean;
|
|
38
|
+
/**
|
|
39
|
+
* - An LLM with `generate()` (this library's LLM contract, e.g. `createAiSdkAdapter({ ai, languageModel }).llm`) that rules on the sentences the lexical check failed for lack of word overlap; see checkAnswer. Give it the base model, not the wrapped one, or each judge call is grounded again
|
|
40
|
+
*/
|
|
41
|
+
judge?: any;
|
|
42
|
+
/**
|
|
43
|
+
* - Return the passages as source parts
|
|
44
|
+
*/
|
|
45
|
+
sources?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* - PreRetrievalGuard options (true for defaults): injection detection on the query
|
|
48
|
+
*/
|
|
49
|
+
preRetrieval?: any | boolean;
|
|
50
|
+
/**
|
|
51
|
+
* - RetrievalGuard options (true for defaults): ACL, minimum score, injection in documents
|
|
52
|
+
*/
|
|
53
|
+
retrieval?: any | boolean;
|
|
54
|
+
/**
|
|
55
|
+
* - PostGenerationGuard options (true for defaults): PII redaction. Streams are buffered when it is on
|
|
56
|
+
*/
|
|
57
|
+
postGeneration?: any | boolean;
|
|
58
|
+
/**
|
|
59
|
+
* - Throw when a guard flags the query or the answer, instead of continuing
|
|
60
|
+
*/
|
|
61
|
+
strict?: boolean;
|
|
62
|
+
/**
|
|
63
|
+
* - `(params) => string`: the retrieval query; defaults to the text of the last user message
|
|
64
|
+
*/
|
|
65
|
+
getQuery?: Function;
|
|
66
|
+
/**
|
|
67
|
+
* - Called with each grounding report
|
|
68
|
+
*/
|
|
69
|
+
onResult?: Function;
|
|
70
|
+
/**
|
|
71
|
+
* - Middleware version for TypeScript: 'v2' for AI SDK 5, 'v3' for 6, 'v4' for 7 (the AI SDK ignores it at runtime)
|
|
72
|
+
*/
|
|
73
|
+
specificationVersion?: string;
|
|
74
|
+
};
|
|
75
|
+
export type GroundingReport = {
|
|
76
|
+
/**
|
|
77
|
+
* - The query passages were retrieved for
|
|
78
|
+
*/
|
|
79
|
+
query: string;
|
|
80
|
+
/**
|
|
81
|
+
* - The passages given to the model
|
|
82
|
+
*/
|
|
83
|
+
sources: Array<{
|
|
84
|
+
id: string;
|
|
85
|
+
score?: number;
|
|
86
|
+
}>;
|
|
87
|
+
/**
|
|
88
|
+
* - Every sentence of the answer is supported by a passage
|
|
89
|
+
*/
|
|
90
|
+
grounded?: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* - Share of supported sentences, 0..1
|
|
93
|
+
*/
|
|
94
|
+
groundedness?: number;
|
|
95
|
+
/**
|
|
96
|
+
* - Sentences no passage supports
|
|
97
|
+
*/
|
|
98
|
+
unsupported?: string[];
|
|
99
|
+
/**
|
|
100
|
+
* - Per-sentence citations
|
|
101
|
+
*/
|
|
102
|
+
citations?: Array<any>;
|
|
103
|
+
/**
|
|
104
|
+
* - With the `judge` option: the verdicts, the number of calls, the token usage when known, and `error` when the judge failed (its sentences then stay unsupported)
|
|
105
|
+
*/
|
|
106
|
+
judge?: any;
|
|
107
|
+
/**
|
|
108
|
+
* - What each enabled guard found
|
|
109
|
+
*/
|
|
110
|
+
guardrails: any;
|
|
111
|
+
};
|
|
112
|
+
/**
|
|
113
|
+
* Create AI SDK language model middleware that retrieves context for each
|
|
114
|
+
* call and checks the answer against it.
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* import { generateText, wrapLanguageModel } from "ai";
|
|
118
|
+
* import { openai } from "@ai-sdk/openai";
|
|
119
|
+
* import { groundingMiddleware } from "@devilsdev/rag-pipeline-utils";
|
|
120
|
+
*
|
|
121
|
+
* const model = wrapLanguageModel({
|
|
122
|
+
* model: openai("gpt-4o-mini"),
|
|
123
|
+
* middleware: groundingMiddleware({ retriever, embedder }),
|
|
124
|
+
* });
|
|
125
|
+
* const { text, sources, providerMetadata } = await generateText({
|
|
126
|
+
* model,
|
|
127
|
+
* prompt: "How many vacation days do I get?",
|
|
128
|
+
* });
|
|
129
|
+
* providerMetadata.ragPipeline.grounded; // true when every sentence is supported
|
|
130
|
+
*
|
|
131
|
+
* @param {GroundingMiddlewareOptions} options
|
|
132
|
+
* @returns {Object} An AI SDK LanguageModelMiddleware
|
|
133
|
+
*/
|
|
134
|
+
export function groundingMiddleware(options?: GroundingMiddlewareOptions): any;
|
|
135
|
+
/**
|
|
136
|
+
* @typedef {Object} GroundingMiddlewareOptions
|
|
137
|
+
* @property {Object} [retriever] - Any retriever plugin (`retrieve()`); needed unless `pipeline` is given
|
|
138
|
+
* @property {Object} [embedder] - Embeds the query for vector retrievers
|
|
139
|
+
* @property {Object} [reranker] - Optional reranker plugin
|
|
140
|
+
* @property {Object} [pipeline] - A createRagPipeline() result to retrieve with, instead of retriever/embedder/reranker; its llm is not called
|
|
141
|
+
* @property {number} [topK=3] - Passages per call
|
|
142
|
+
* @property {number} [maxContextTokens] - Token budget for the passages (see packContext)
|
|
143
|
+
* @property {Function} [countTokens] - Token counter for maxContextTokens
|
|
144
|
+
* @property {string} [systemPrompt] - Replaces the default RAG system prompt
|
|
145
|
+
* @property {boolean} [check=true] - Run checkAnswer on the answer
|
|
146
|
+
* @property {Object} [judge] - An LLM with `generate()` (this library's LLM contract, e.g. `createAiSdkAdapter({ ai, languageModel }).llm`) that rules on the sentences the lexical check failed for lack of word overlap; see checkAnswer. Give it the base model, not the wrapped one, or each judge call is grounded again
|
|
147
|
+
* @property {boolean} [sources=true] - Return the passages as source parts
|
|
148
|
+
* @property {Object|boolean} [preRetrieval] - PreRetrievalGuard options (true for defaults): injection detection on the query
|
|
149
|
+
* @property {Object|boolean} [retrieval] - RetrievalGuard options (true for defaults): ACL, minimum score, injection in documents
|
|
150
|
+
* @property {Object|boolean} [postGeneration] - PostGenerationGuard options (true for defaults): PII redaction. Streams are buffered when it is on
|
|
151
|
+
* @property {boolean} [strict=false] - Throw when a guard flags the query or the answer, instead of continuing
|
|
152
|
+
* @property {Function} [getQuery] - `(params) => string`: the retrieval query; defaults to the text of the last user message
|
|
153
|
+
* @property {Function} [onResult] - Called with each grounding report
|
|
154
|
+
* @property {string} [specificationVersion='v3'] - Middleware version for TypeScript: 'v2' for AI SDK 5, 'v3' for 6, 'v4' for 7 (the AI SDK ignores it at runtime)
|
|
155
|
+
*/
|
|
156
|
+
/**
|
|
157
|
+
* @typedef {Object} GroundingReport
|
|
158
|
+
* @property {string} query - The query passages were retrieved for
|
|
159
|
+
* @property {Array<{id: string, score?: number}>} sources - The passages given to the model
|
|
160
|
+
* @property {boolean} [grounded] - Every sentence of the answer is supported by a passage
|
|
161
|
+
* @property {number} [groundedness] - Share of supported sentences, 0..1
|
|
162
|
+
* @property {string[]} [unsupported] - Sentences no passage supports
|
|
163
|
+
* @property {Array<Object>} [citations] - Per-sentence citations
|
|
164
|
+
* @property {Object} [judge] - With the `judge` option: the verdicts, the number of calls, the token usage when known, and `error` when the judge failed (its sentences then stay unsupported)
|
|
165
|
+
* @property {Object} guardrails - What each enabled guard found
|
|
166
|
+
*/
|
|
167
|
+
export class GroundingBlockedError extends Error {
|
|
168
|
+
constructor(message: any, report: any);
|
|
169
|
+
code: string;
|
|
170
|
+
report: any;
|
|
171
|
+
}
|
|
172
|
+
/** Key of this middleware in providerOptions (input) and providerMetadata (output). */
|
|
173
|
+
export const METADATA_KEY: "ragPipeline";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Create a Mastra scorer from one of this library's metrics.
|
|
3
|
+
*
|
|
4
|
+
* @example
|
|
5
|
+
* import { createScorer } from "@mastra/core/evals";
|
|
6
|
+
* import { createMastraScorer } from "@devilsdev/rag-pipeline-utils";
|
|
7
|
+
*
|
|
8
|
+
* const groundedness = createMastraScorer({ createScorer, metric: "groundedness" });
|
|
9
|
+
* const agent = new Agent({ ..., scorers: { groundedness: { scorer: groundedness } } });
|
|
10
|
+
*
|
|
11
|
+
* @param {Object} options
|
|
12
|
+
* @param {Function} options.createScorer - `createScorer` from "@mastra/core/evals"
|
|
13
|
+
* @param {'groundedness'|'faithfulness'|'contextPrecision'|'contextRecall'} [options.metric='groundedness']
|
|
14
|
+
* @param {Array} [options.context] - Fixed passages to score against (strings, { id, content }, LangChain Documents, LlamaIndex nodes)
|
|
15
|
+
* @param {Function} [options.getContext] - `(run) => passages`; defaults to the results of the agent's tool calls
|
|
16
|
+
* @param {string} [options.id] - Scorer id (default `rag-<metric>`)
|
|
17
|
+
* @returns {Object} A Mastra scorer
|
|
18
|
+
*/
|
|
19
|
+
export function createMastraScorer(options?: {
|
|
20
|
+
createScorer: Function;
|
|
21
|
+
metric?: "groundedness" | "faithfulness" | "contextPrecision" | "contextRecall";
|
|
22
|
+
context?: any[];
|
|
23
|
+
getContext?: Function;
|
|
24
|
+
id?: string;
|
|
25
|
+
}): any;
|
|
26
|
+
/**
|
|
27
|
+
* Create a Mastra processor with this library's guardrails: prompt-injection
|
|
28
|
+
* detection on the user's message (input) and PII redaction of the answer
|
|
29
|
+
* (output). A flagged message calls Mastra's `abort()`, which stops the run
|
|
30
|
+
* with a tripwire.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* const agent = new Agent({
|
|
34
|
+
* ...,
|
|
35
|
+
* inputProcessors: [guardrails],
|
|
36
|
+
* outputProcessors: [guardrails],
|
|
37
|
+
* });
|
|
38
|
+
* const guardrails = createMastraGuardrails({ injection: true, pii: "redact" });
|
|
39
|
+
*
|
|
40
|
+
* @param {Object} [options]
|
|
41
|
+
* @param {boolean|Object} [options.injection=true] - Check the last user message; PreRetrievalGuard options
|
|
42
|
+
* @param {'redact'|'block'|false} [options.pii='redact'] - Redact PII in the answer, or abort the run
|
|
43
|
+
* @param {Object<string, RegExp>} [options.piiPatterns] - Extra PII patterns by name
|
|
44
|
+
* @param {string} [options.id='rag-pipeline-guardrails']
|
|
45
|
+
* @returns {Object} A Mastra processor
|
|
46
|
+
*/
|
|
47
|
+
export function createMastraGuardrails(options?: {
|
|
48
|
+
injection?: boolean | any;
|
|
49
|
+
pii?: "redact" | "block" | false;
|
|
50
|
+
piiPatterns?: {
|
|
51
|
+
[x: string]: RegExp;
|
|
52
|
+
};
|
|
53
|
+
id?: string;
|
|
54
|
+
}): any;
|
|
55
|
+
export declare let MASTRA_METRICS: string[];
|
|
@@ -24,12 +24,15 @@ export function contentTokens(tokens: string[]): string[];
|
|
|
24
24
|
* checkNumericConsistency), the score becomes
|
|
25
25
|
* lexicalScore * NUMERIC_MISMATCH_FACTOR (0.4) * matchedNumbers / claimNumbers
|
|
26
26
|
* which is always below 0.4, so the claim fails the default 0.5 threshold.
|
|
27
|
+
* Polarity consistency: with the same texts, a claim that is negated where
|
|
28
|
+
* the document is not, or the reverse (see checkPolarityConsistency), has its
|
|
29
|
+
* score multiplied by POLARITY_MISMATCH_FACTOR (0.4).
|
|
27
30
|
*
|
|
28
31
|
* @param {string[]} claimTokens - Tokens of the sentence/claim/query
|
|
29
32
|
* @param {string[]|Set<string>} docTokens - Tokens of the document
|
|
30
33
|
* @param {Map<string, number>|null} [idfWeights] - Optional token weights
|
|
31
34
|
* @param {{ claimText?: string, docText?: string }} [options] - Raw texts
|
|
32
|
-
* enabling the numeric consistency
|
|
35
|
+
* enabling the numeric and polarity consistency checks
|
|
33
36
|
* @returns {number} Support score between 0 and 1
|
|
34
37
|
*/
|
|
35
38
|
export function computeSupportScore(claimTokens: string[], docTokens: string[] | Set<string>, idfWeights?: Map<string, number> | null, options?: {
|
|
@@ -51,6 +54,37 @@ export function checkNumericConsistency(claimText: string, docText: string): {
|
|
|
51
54
|
claimNumbers: string[];
|
|
52
55
|
missing: string[];
|
|
53
56
|
};
|
|
57
|
+
/**
|
|
58
|
+
* Polarity consistency between a claim and a document. Word overlap alone
|
|
59
|
+
* grounds "employees never receive paid vacation" against a source that says
|
|
60
|
+
* "employees receive twenty days of paid vacation", because the negation is
|
|
61
|
+
* one word among many. The claim is compared with the document sentence that
|
|
62
|
+
* shares most of its content words (any of them, on a tie): it is consistent
|
|
63
|
+
* when that sentence is negated exactly when the claim is.
|
|
64
|
+
*
|
|
65
|
+
* Errs on the side of not grounding: a source that negates in other words
|
|
66
|
+
* ("contractors are excluded from vacation") does not match a negated claim,
|
|
67
|
+
* and a sentence joining a positive and a negative statement is judged by
|
|
68
|
+
* the source sentence closest to it as a whole.
|
|
69
|
+
*
|
|
70
|
+
* @param {string} claimText
|
|
71
|
+
* @param {string} docText
|
|
72
|
+
* @returns {{ consistent: boolean, claimNegated: boolean, sourceSentence: string|null }}
|
|
73
|
+
* sourceSentence: the closest document sentence the claim was compared with
|
|
74
|
+
*/
|
|
75
|
+
export function checkPolarityConsistency(claimText: string, docText: string): {
|
|
76
|
+
consistent: boolean;
|
|
77
|
+
claimNegated: boolean;
|
|
78
|
+
sourceSentence: string | null;
|
|
79
|
+
};
|
|
80
|
+
/**
|
|
81
|
+
* Whether a token list reads as negated: an odd number of negations, so
|
|
82
|
+
* "not without approval" counts as positive.
|
|
83
|
+
*
|
|
84
|
+
* @param {string[]} tokens
|
|
85
|
+
* @returns {boolean}
|
|
86
|
+
*/
|
|
87
|
+
export function isNegated(tokens: string[]): boolean;
|
|
54
88
|
/**
|
|
55
89
|
* Extract the numbers stated in a text, as normalised value strings, so
|
|
56
90
|
* "20", "20%", "twenty" and "Twenty" all yield "20", "1,000" yields "1000"
|
|
@@ -72,6 +106,12 @@ export function extractNumbers(text: string): {
|
|
|
72
106
|
* keeps the score below 0.4, i.e. under the default 0.5 support threshold.
|
|
73
107
|
*/
|
|
74
108
|
export const NUMERIC_MISMATCH_FACTOR: 0.4;
|
|
109
|
+
/**
|
|
110
|
+
* Multiplier applied to the support score when a claim's polarity
|
|
111
|
+
* contradicts the document (see checkPolarityConsistency). It keeps the
|
|
112
|
+
* score below 0.4, under the default 0.5 support threshold.
|
|
113
|
+
*/
|
|
114
|
+
export const POLARITY_MISMATCH_FACTOR: 0.4;
|
|
75
115
|
/**
|
|
76
116
|
* Score a prompt-answer pair using multiple metrics
|
|
77
117
|
* @param {string} actual - LLM response
|
|
@@ -37,6 +37,74 @@ export type AnswerCheck = {
|
|
|
37
37
|
content: string;
|
|
38
38
|
metadata: any;
|
|
39
39
|
}>;
|
|
40
|
+
/**
|
|
41
|
+
* - Only when the `judge` option is given
|
|
42
|
+
*/
|
|
43
|
+
judge?: JudgeReport;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Any LLM for the `judge` option: this library's LLM plugin contract.
|
|
47
|
+
* `generate` may return the text, or an object with `text` or `content`
|
|
48
|
+
* (and optionally `usage`).
|
|
49
|
+
*/
|
|
50
|
+
export type JudgeLLM = {
|
|
51
|
+
generate: (prompt: string, context: Array<any>, options?: any) => (string | {
|
|
52
|
+
text?: string;
|
|
53
|
+
content?: string;
|
|
54
|
+
usage?: any;
|
|
55
|
+
} | Promise<string | {
|
|
56
|
+
text?: string;
|
|
57
|
+
content?: string;
|
|
58
|
+
usage?: any;
|
|
59
|
+
}>);
|
|
60
|
+
};
|
|
61
|
+
/**
|
|
62
|
+
* The verdict on one sentence when a judge is used.
|
|
63
|
+
*/
|
|
64
|
+
export type JudgeVerdict = {
|
|
65
|
+
sentence: string;
|
|
66
|
+
/**
|
|
67
|
+
* - The combined verdict
|
|
68
|
+
*/
|
|
69
|
+
verdict: "supported" | "unsupported";
|
|
70
|
+
/**
|
|
71
|
+
* - Id of the supporting source, when supported
|
|
72
|
+
*/
|
|
73
|
+
sourceId: string | null;
|
|
74
|
+
/**
|
|
75
|
+
* - Why, in words (the judge's reason when it decided)
|
|
76
|
+
*/
|
|
77
|
+
reason: string;
|
|
78
|
+
/**
|
|
79
|
+
* - What the lexical check found. Only `low_overlap` sentences go to the judge
|
|
80
|
+
*/
|
|
81
|
+
lexicalReason: "supported" | "low_overlap" | "number_mismatch" | "polarity_mismatch" | "no_sources";
|
|
82
|
+
decidedBy: "lexical" | "judge";
|
|
83
|
+
};
|
|
84
|
+
/**
|
|
85
|
+
* What the judge did.
|
|
86
|
+
*/
|
|
87
|
+
export type JudgeReport = {
|
|
88
|
+
/**
|
|
89
|
+
* - One per sentence, in order
|
|
90
|
+
*/
|
|
91
|
+
verdicts: JudgeVerdict[];
|
|
92
|
+
/**
|
|
93
|
+
* - Calls made to `judge.generate` (0 or 1)
|
|
94
|
+
*/
|
|
95
|
+
calls: number;
|
|
96
|
+
/**
|
|
97
|
+
* - Token usage summed over the calls, when the LLM result carries `usage`
|
|
98
|
+
*/
|
|
99
|
+
usage?: {
|
|
100
|
+
inputTokens: number;
|
|
101
|
+
outputTokens: number;
|
|
102
|
+
totalTokens: number;
|
|
103
|
+
};
|
|
104
|
+
/**
|
|
105
|
+
* - Why the judge's reply could not be parsed
|
|
106
|
+
*/
|
|
107
|
+
error?: string;
|
|
40
108
|
};
|
|
41
109
|
/**
|
|
42
110
|
* @typedef {Object} AnswerCheck
|
|
@@ -51,6 +119,37 @@ export type AnswerCheck = {
|
|
|
51
119
|
* referenceAnswer is given); only when `evaluate` is not false
|
|
52
120
|
* @property {Array<{id: string, content: string, metadata: Object}>} sources
|
|
53
121
|
* - The sources as they were checked
|
|
122
|
+
* @property {JudgeReport} [judge] - Only when the `judge` option is given
|
|
123
|
+
*/
|
|
124
|
+
/**
|
|
125
|
+
* Any LLM for the `judge` option: this library's LLM plugin contract.
|
|
126
|
+
* `generate` may return the text, or an object with `text` or `content`
|
|
127
|
+
* (and optionally `usage`).
|
|
128
|
+
*
|
|
129
|
+
* @typedef {Object} JudgeLLM
|
|
130
|
+
* @property {(prompt: string, context: Array<any>, options?: Object) => (string|{text?: string, content?: string, usage?: Object}|Promise<string|{text?: string, content?: string, usage?: Object}>)} generate
|
|
131
|
+
*/
|
|
132
|
+
/**
|
|
133
|
+
* The verdict on one sentence when a judge is used.
|
|
134
|
+
*
|
|
135
|
+
* @typedef {Object} JudgeVerdict
|
|
136
|
+
* @property {string} sentence
|
|
137
|
+
* @property {'supported'|'unsupported'} verdict - The combined verdict
|
|
138
|
+
* @property {string|null} sourceId - Id of the supporting source, when supported
|
|
139
|
+
* @property {string} reason - Why, in words (the judge's reason when it decided)
|
|
140
|
+
* @property {'supported'|'low_overlap'|'number_mismatch'|'polarity_mismatch'|'no_sources'} lexicalReason
|
|
141
|
+
* - What the lexical check found. Only `low_overlap` sentences go to the judge
|
|
142
|
+
* @property {'lexical'|'judge'} decidedBy
|
|
143
|
+
*/
|
|
144
|
+
/**
|
|
145
|
+
* What the judge did.
|
|
146
|
+
*
|
|
147
|
+
* @typedef {Object} JudgeReport
|
|
148
|
+
* @property {JudgeVerdict[]} verdicts - One per sentence, in order
|
|
149
|
+
* @property {number} calls - Calls made to `judge.generate` (0 or 1)
|
|
150
|
+
* @property {{inputTokens: number, outputTokens: number, totalTokens: number}} [usage]
|
|
151
|
+
* - Token usage summed over the calls, when the LLM result carries `usage`
|
|
152
|
+
* @property {string} [error] - Why the judge's reply could not be parsed
|
|
54
153
|
*/
|
|
55
154
|
/**
|
|
56
155
|
* Check an answer against the sources it was generated from.
|
|
@@ -74,15 +173,24 @@ export type AnswerCheck = {
|
|
|
74
173
|
* @param {Object} [params.embedder] - With a referenceAnswer, adds
|
|
75
174
|
* `evaluation.scores.semanticSimilarity` (computeSemanticSimilarity)
|
|
76
175
|
* @param {boolean} [params.evaluate=true] - Also compute evaluation scores
|
|
176
|
+
* @param {JudgeLLM} [params.judge] - Opt-in LLM judge for sentences the
|
|
177
|
+
* lexical check failed for lack of word overlap (correct paraphrases).
|
|
178
|
+
* It never overturns a sentence that failed the number or negation check,
|
|
179
|
+
* and a "supported" verdict must name a source whose numbers and negation
|
|
180
|
+
* match the sentence. Unparseable output keeps a sentence unsupported.
|
|
181
|
+
* With a judge, `grounded`, `groundedness` and `unsupported` combine both
|
|
182
|
+
* checks and `judge` reports the verdicts; `citations` and `evaluation`
|
|
183
|
+
* stay lexical. Errors thrown by `judge.generate` are not caught.
|
|
77
184
|
* @returns {Promise<AnswerCheck>}
|
|
78
185
|
*/
|
|
79
|
-
export function checkAnswer({ answer, sources, question, referenceAnswer, embedder, evaluate, }?: {
|
|
186
|
+
export function checkAnswer({ answer, sources, question, referenceAnswer, embedder, evaluate, judge, }?: {
|
|
80
187
|
answer: string;
|
|
81
188
|
sources: Array<string | any>;
|
|
82
189
|
question?: string;
|
|
83
190
|
referenceAnswer?: string;
|
|
84
191
|
embedder?: any;
|
|
85
192
|
evaluate?: boolean;
|
|
193
|
+
judge?: JudgeLLM;
|
|
86
194
|
}): Promise<AnswerCheck>;
|
|
87
195
|
/**
|
|
88
196
|
* Normalize sources to `{ id, content, metadata }`.
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
export namespace REASONS {
|
|
2
|
+
let supported: string;
|
|
3
|
+
let lowOverlap: string;
|
|
4
|
+
let numberMismatch: string;
|
|
5
|
+
let polarityMismatch: string;
|
|
6
|
+
let noSources: string;
|
|
7
|
+
}
|
|
8
|
+
/** Instructions at the top of every judge prompt. */
|
|
9
|
+
export const JUDGE_INSTRUCTIONS: string;
|
|
10
|
+
/**
|
|
11
|
+
* Build the judge prompt for the given claims.
|
|
12
|
+
*
|
|
13
|
+
* @param {string[]} claims - Sentences to rule on
|
|
14
|
+
* @param {Array<{id: string, content: string}>} docs
|
|
15
|
+
* @returns {string}
|
|
16
|
+
*/
|
|
17
|
+
export function buildJudgePrompt(claims: string[], docs: Array<{
|
|
18
|
+
id: string;
|
|
19
|
+
content: string;
|
|
20
|
+
}>): string;
|
|
21
|
+
/**
|
|
22
|
+
* Parse the judge's reply into one verdict per claim. Tolerates a code fence
|
|
23
|
+
* around the JSON; anything else that is not the expected array leaves every
|
|
24
|
+
* claim without a verdict.
|
|
25
|
+
*
|
|
26
|
+
* @param {*} raw - What llm.generate() returned
|
|
27
|
+
* @param {number} claimCount
|
|
28
|
+
* @returns {{ verdicts: Array<{verdict: string, source: string|null, reason: string}|null>, error: string|null }}
|
|
29
|
+
* verdicts[i] is null when claim i+1 got no well-formed verdict
|
|
30
|
+
*/
|
|
31
|
+
export function parseJudgeVerdicts(raw: any, claimCount: number): {
|
|
32
|
+
verdicts: Array<{
|
|
33
|
+
verdict: string;
|
|
34
|
+
source: string | null;
|
|
35
|
+
reason: string;
|
|
36
|
+
} | null>;
|
|
37
|
+
error: string | null;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* The lexical reason for every sentence, in order.
|
|
41
|
+
*
|
|
42
|
+
* @param {Array<{text: string, classification: string}>} sentences - From
|
|
43
|
+
* CitationTracker's hallucination report
|
|
44
|
+
* @param {Array<{content: string}>} docs
|
|
45
|
+
* @returns {string[]}
|
|
46
|
+
*/
|
|
47
|
+
export function lexicalReasons(sentences: Array<{
|
|
48
|
+
text: string;
|
|
49
|
+
classification: string;
|
|
50
|
+
}>, docs: Array<{
|
|
51
|
+
content: string;
|
|
52
|
+
}>): string[];
|
|
53
|
+
/**
|
|
54
|
+
* Run the judge on the sentences that failed for lack of overlap and
|
|
55
|
+
* combine its verdicts with the lexical result.
|
|
56
|
+
*
|
|
57
|
+
* Overturn rule: a judge "supported" counts only when the sentence failed
|
|
58
|
+
* for lack of overlap, the verdict is well formed, it names a source that
|
|
59
|
+
* exists, and that source passes the numeric and polarity checks against
|
|
60
|
+
* the sentence. Everything else keeps the lexical verdict.
|
|
61
|
+
*
|
|
62
|
+
* @param {Object} params
|
|
63
|
+
* @param {{generate: Function}} params.judge
|
|
64
|
+
* @param {Array<{text: string, classification: string, bestSource: Object|null}>} params.sentences
|
|
65
|
+
* @param {Array<{id: string, content: string}>} params.docs
|
|
66
|
+
* @returns {Promise<{verdicts: Array<Object>, calls: number, usage?: Object, error?: string}>}
|
|
67
|
+
*/
|
|
68
|
+
export function runJudge({ judge, sentences, docs }: {
|
|
69
|
+
judge: {
|
|
70
|
+
generate: Function;
|
|
71
|
+
};
|
|
72
|
+
sentences: Array<{
|
|
73
|
+
text: string;
|
|
74
|
+
classification: string;
|
|
75
|
+
bestSource: any | null;
|
|
76
|
+
}>;
|
|
77
|
+
docs: Array<{
|
|
78
|
+
id: string;
|
|
79
|
+
content: string;
|
|
80
|
+
}>;
|
|
81
|
+
}): Promise<{
|
|
82
|
+
verdicts: Array<any>;
|
|
83
|
+
calls: number;
|
|
84
|
+
usage?: any;
|
|
85
|
+
error?: string;
|
|
86
|
+
}>;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
export namespace ragMatchers {
|
|
2
|
+
export { toBeGroundedIn };
|
|
3
|
+
export { toCite };
|
|
4
|
+
export { toContainNoPII };
|
|
5
|
+
}
|
|
6
|
+
/**
|
|
7
|
+
* Passes when every sentence of the answer is supported by the sources: its
|
|
8
|
+
* content words, numbers and negation match a source (see checkAnswer).
|
|
9
|
+
*
|
|
10
|
+
* @param {string|{answer: string, results?: Array}} received - An answer, or a run() result
|
|
11
|
+
* @param {Array} [sources] - Defaults to the run() result's `results`
|
|
12
|
+
* @param {{threshold?: number}} [options] - Support threshold per sentence (default 0.5)
|
|
13
|
+
*/
|
|
14
|
+
export function toBeGroundedIn(received: string | {
|
|
15
|
+
answer: string;
|
|
16
|
+
results?: any[];
|
|
17
|
+
}, sources?: any[], options?: {
|
|
18
|
+
threshold?: number;
|
|
19
|
+
}): {
|
|
20
|
+
pass: boolean;
|
|
21
|
+
message: () => string;
|
|
22
|
+
};
|
|
23
|
+
/**
|
|
24
|
+
* Passes when the answer cites each of the given sources: at least one of its
|
|
25
|
+
* sentences is supported by that source.
|
|
26
|
+
*
|
|
27
|
+
* @param {string|{answer: string, results?: Array}} received - An answer, or a run() result
|
|
28
|
+
* @param {string|string[]} ids - Source id(s) that must be cited
|
|
29
|
+
* @param {Array} [sources] - Defaults to the run() result's `results`
|
|
30
|
+
*/
|
|
31
|
+
export function toCite(received: string | {
|
|
32
|
+
answer: string;
|
|
33
|
+
results?: any[];
|
|
34
|
+
}, ids: string | string[], sources?: any[]): {
|
|
35
|
+
pass: boolean;
|
|
36
|
+
message: () => string;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* Passes when no email address, phone number, US social security number or
|
|
40
|
+
* card number (the post-generation guard's patterns) appears in the value: a
|
|
41
|
+
* string, or every string nested in an object such as a run() result.
|
|
42
|
+
*
|
|
43
|
+
* @param {*} received
|
|
44
|
+
* @param {{patterns?: Object<string, RegExp>}} [options] - Extra patterns by name
|
|
45
|
+
*/
|
|
46
|
+
export function toContainNoPII(received: any, options?: {
|
|
47
|
+
patterns?: {
|
|
48
|
+
[x: string]: RegExp;
|
|
49
|
+
};
|
|
50
|
+
}): {
|
|
51
|
+
pass: boolean;
|
|
52
|
+
message: () => string;
|
|
53
|
+
};
|
package/dist/types/testing.d.ts
CHANGED
|
@@ -169,5 +169,9 @@ export function createTestPipeline({ documents, embedder, llm, pipeline: pipelin
|
|
|
169
169
|
}>;
|
|
170
170
|
import { runContractTests } from "./testing/contract-tests";
|
|
171
171
|
import { checkContract } from "./testing/contract-tests";
|
|
172
|
+
import { ragMatchers } from "./testing/matchers";
|
|
173
|
+
import { toBeGroundedIn } from "./testing/matchers";
|
|
174
|
+
import { toCite } from "./testing/matchers";
|
|
175
|
+
import { toContainNoPII } from "./testing/matchers";
|
|
172
176
|
import { MemoryRetriever } from "./connectors/memory-retriever";
|
|
173
|
-
export { runContractTests, checkContract };
|
|
177
|
+
export { runContractTests, checkContract, ragMatchers, toBeGroundedIn, toCite, toContainNoPII };
|