@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 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.2.0",
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.34.1",
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.2.0",
1554
- "@devilsdev/rag-core": "3.2.0",
1555
- "@devilsdev/rag-eval": "3.2.0",
1556
- "@devilsdev/rag-guardrails": "3.2.0",
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: "^9.0.9",
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 check
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
+ };
@@ -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 };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@devilsdev/rag-core",
3
- "version": "3.2.0",
3
+ "version": "3.4.0",
4
4
  "description": "createRagPipeline and its building blocks: retrieval, reranking, caching, cost tracking and model routing.",
5
5
  "keywords": [
6
6
  "rag",