langchain 1.2.25 → 1.2.27
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 +20 -0
- package/dist/agents/ReactAgent.cjs.map +1 -1
- package/dist/agents/ReactAgent.d.cts +2 -2
- package/dist/agents/ReactAgent.d.ts +2 -2
- package/dist/agents/ReactAgent.js.map +1 -1
- package/dist/agents/index.d.cts +1 -1
- package/dist/agents/index.d.ts +1 -1
- package/dist/agents/middleware/dynamicSystemPrompt.cjs +1 -1
- package/dist/agents/middleware/dynamicSystemPrompt.cjs.map +1 -1
- package/dist/agents/middleware/dynamicSystemPrompt.d.cts +1 -1
- package/dist/agents/middleware/dynamicSystemPrompt.d.ts +1 -1
- package/dist/agents/middleware/dynamicSystemPrompt.js +1 -1
- package/dist/agents/middleware/dynamicSystemPrompt.js.map +1 -1
- package/dist/agents/middleware/llmToolSelector.cjs.map +1 -1
- package/dist/agents/middleware/llmToolSelector.d.cts +37 -2
- package/dist/agents/middleware/llmToolSelector.d.cts.map +1 -1
- package/dist/agents/middleware/llmToolSelector.d.ts +37 -2
- package/dist/agents/middleware/llmToolSelector.d.ts.map +1 -1
- package/dist/agents/middleware/llmToolSelector.js.map +1 -1
- package/dist/agents/middleware/modelFallback.cjs.map +1 -1
- package/dist/agents/middleware/modelFallback.d.cts +2 -1
- package/dist/agents/middleware/modelFallback.d.cts.map +1 -1
- package/dist/agents/middleware/modelFallback.d.ts +2 -1
- package/dist/agents/middleware/modelFallback.d.ts.map +1 -1
- package/dist/agents/middleware/modelFallback.js.map +1 -1
- package/dist/agents/middleware/modelRetry.cjs.map +1 -1
- package/dist/agents/middleware/modelRetry.d.cts +43 -1
- package/dist/agents/middleware/modelRetry.d.cts.map +1 -1
- package/dist/agents/middleware/modelRetry.d.ts +43 -1
- package/dist/agents/middleware/modelRetry.d.ts.map +1 -1
- package/dist/agents/middleware/modelRetry.js.map +1 -1
- package/dist/agents/middleware/pii.cjs +7 -4
- package/dist/agents/middleware/pii.cjs.map +1 -1
- package/dist/agents/middleware/pii.d.cts +28 -2
- package/dist/agents/middleware/pii.d.cts.map +1 -1
- package/dist/agents/middleware/pii.d.ts +28 -2
- package/dist/agents/middleware/pii.d.ts.map +1 -1
- package/dist/agents/middleware/pii.js +7 -4
- package/dist/agents/middleware/pii.js.map +1 -1
- package/dist/agents/middleware/piiRedaction.cjs.map +1 -1
- package/dist/agents/middleware/piiRedaction.d.cts +15 -2
- package/dist/agents/middleware/piiRedaction.d.cts.map +1 -1
- package/dist/agents/middleware/piiRedaction.d.ts +15 -2
- package/dist/agents/middleware/piiRedaction.d.ts.map +1 -1
- package/dist/agents/middleware/piiRedaction.js.map +1 -1
- package/dist/agents/middleware/provider/anthropic/promptCaching.cjs +4 -4
- package/dist/agents/middleware/provider/anthropic/promptCaching.cjs.map +1 -1
- package/dist/agents/middleware/provider/anthropic/promptCaching.d.cts +4 -4
- package/dist/agents/middleware/provider/anthropic/promptCaching.d.ts +4 -4
- package/dist/agents/middleware/provider/anthropic/promptCaching.js +4 -4
- package/dist/agents/middleware/provider/anthropic/promptCaching.js.map +1 -1
- package/dist/agents/middleware/toolEmulator.cjs.map +1 -1
- package/dist/agents/middleware/toolEmulator.d.cts +2 -2
- package/dist/agents/middleware/toolEmulator.d.cts.map +1 -1
- package/dist/agents/middleware/toolEmulator.d.ts +2 -2
- package/dist/agents/middleware/toolEmulator.d.ts.map +1 -1
- package/dist/agents/middleware/toolEmulator.js.map +1 -1
- package/dist/agents/middleware/toolRetry.cjs.map +1 -1
- package/dist/agents/middleware/toolRetry.d.cts +55 -1
- package/dist/agents/middleware/toolRetry.d.cts.map +1 -1
- package/dist/agents/middleware/toolRetry.d.ts +55 -1
- package/dist/agents/middleware/toolRetry.d.ts.map +1 -1
- package/dist/agents/middleware/toolRetry.js.map +1 -1
- package/dist/agents/middleware/types.cjs.map +1 -1
- package/dist/agents/middleware/types.d.cts +22 -18
- package/dist/agents/middleware/types.d.cts.map +1 -1
- package/dist/agents/middleware/types.d.ts +22 -18
- package/dist/agents/middleware/types.d.ts.map +1 -1
- package/dist/agents/middleware/types.js.map +1 -1
- package/dist/agents/nodes/AgentNode.cjs +2 -0
- package/dist/agents/nodes/AgentNode.cjs.map +1 -1
- package/dist/agents/nodes/AgentNode.js +2 -0
- package/dist/agents/nodes/AgentNode.js.map +1 -1
- package/dist/agents/types.d.cts +7 -7
- package/dist/agents/types.d.cts.map +1 -1
- package/dist/agents/types.d.ts +7 -7
- package/dist/agents/types.d.ts.map +1 -1
- package/dist/chat_models/universal.cjs +3 -3
- package/dist/chat_models/universal.cjs.map +1 -1
- package/dist/chat_models/universal.js +3 -3
- package/dist/chat_models/universal.js.map +1 -1
- package/dist/index.d.cts +2 -2
- package/dist/index.d.ts +2 -2
- package/package.json +7 -7
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AgentMiddleware } from "./types.js";
|
|
2
|
+
import * as _langchain_core_tools0 from "@langchain/core/tools";
|
|
2
3
|
import { InferInteropZodInput } from "@langchain/core/utils/types";
|
|
3
4
|
import { z } from "zod/v3";
|
|
4
5
|
|
|
@@ -209,7 +210,32 @@ declare function piiMiddleware(piiType: BuiltInPIIType | string, options?: {
|
|
|
209
210
|
applyToInput?: boolean;
|
|
210
211
|
applyToOutput?: boolean;
|
|
211
212
|
applyToToolResults?: boolean;
|
|
212
|
-
}):
|
|
213
|
+
}): AgentMiddleware<undefined, z.ZodObject<{
|
|
214
|
+
/**
|
|
215
|
+
* Whether to check user messages before model call
|
|
216
|
+
*/
|
|
217
|
+
applyToInput: z.ZodOptional<z.ZodBoolean>;
|
|
218
|
+
/**
|
|
219
|
+
* Whether to check AI messages after model call
|
|
220
|
+
*/
|
|
221
|
+
applyToOutput: z.ZodOptional<z.ZodBoolean>;
|
|
222
|
+
/**
|
|
223
|
+
* Whether to check tool result messages after tool execution
|
|
224
|
+
*/
|
|
225
|
+
applyToToolResults: z.ZodOptional<z.ZodBoolean>;
|
|
226
|
+
}, "strip", z.ZodTypeAny, {
|
|
227
|
+
applyToInput?: boolean | undefined;
|
|
228
|
+
applyToOutput?: boolean | undefined;
|
|
229
|
+
applyToToolResults?: boolean | undefined;
|
|
230
|
+
}, {
|
|
231
|
+
applyToInput?: boolean | undefined;
|
|
232
|
+
applyToOutput?: boolean | undefined;
|
|
233
|
+
applyToToolResults?: boolean | undefined;
|
|
234
|
+
}>, {
|
|
235
|
+
applyToInput?: boolean | undefined;
|
|
236
|
+
applyToOutput?: boolean | undefined;
|
|
237
|
+
applyToToolResults?: boolean | undefined;
|
|
238
|
+
}, readonly (_langchain_core_tools0.ServerTool | _langchain_core_tools0.ClientTool)[]>;
|
|
213
239
|
//#endregion
|
|
214
240
|
export { BuiltInPIIType, PIIDetectionError, PIIDetector, PIIMatch, PIIMiddlewareConfig, PIIStrategy, RedactionRuleConfig, ResolvedRedactionRule, applyStrategy, detectCreditCard, detectEmail, detectIP, detectMacAddress, detectUrl, piiMiddleware, resolveRedactionRule };
|
|
215
241
|
//# sourceMappingURL=pii.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pii.d.ts","names":[],"sources":["../../../src/agents/middleware/pii.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"pii.d.ts","names":[],"sources":["../../../src/agents/middleware/pii.ts"],"mappings":";;;;;;;;;UAUiB,QAAA;EAAA;;;EAIf,IAAA;EAAA;;;EAIA,KAAA;EAIG;AAML;;EANE,GAAA;AAAA;;;;cAMW,iBAAA,SAA0B,KAAA;EAAA,SAEnB,OAAA;EAAA,SACA,OAAA,EAAS,QAAA;EAF3B,WAAA,CACkB,OAAA,UACA,OAAA,EAAS,QAAA;AAAA;;;;KAUjB,WAAA;;;AAAZ;KAKY,cAAA;;;;KAUA,WAAA,IAAe,OAAA,aAAoB,QAAA;AAAA,KACnC,QAAA,GAAW,WAAA,GAAc,MAAA;;;;UAKpB,mBAAA;EANM;;;EAUrB,OAAA,EAAS,cAAA;EATC;;;EAaV,QAAA,EAAU,WAAA;EAb+B;AAK3C;;EAYE,QAAA,GAAW,QAAA;AAAA;;;;UAMI,qBAAA;EACf,OAAA;EACA,QAAA,EAAU,WAAA;EACV,QAAA,EAAU,WAAA;AAAA;;;;iBAoEI,WAAA,CAAY,OAAA,WAAkB,QAAA;AAvE9C;;;AAAA,iBAsFgB,gBAAA,CAAiB,OAAA,WAAkB,QAAA;;;;iBAuBnC,QAAA,CAAS,OAAA,WAAkB,QAAA;;;;iBA0B3B,gBAAA,CAAiB,OAAA,WAAkB,QAAA;;;;iBAenC,SAAA,CAAU,OAAA,WAAkB,QAAA;AAhE5C;;;AAAA,iBA0FgB,oBAAA,CACd,MAAA,EAAQ,mBAAA,GACP,qBAAA;;AArEH;;iBAgNgB,aAAA,CACd,OAAA,UACA,OAAA,EAAS,QAAA,IACT,QAAA,EAAU,WAAA,EACV,OAAA;;;AA1LF;cAiNM,aAAA,EAAa,CAAA,CAAA,SAAA;;;;;EAlMM;;;;EA0BT;;;;;;;;;;;;;KAuLJ,mBAAA,GAAsB,oBAAA,QAA4B,aAAA;;;;;;;;AApB7D;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAoBD;;;;;AA0GA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,aAAA,CACd,OAAA,EAAS,cAAA,WACT,OAAA;EACE,QAAA,GAAW,WAAA;EACX,QAAA,GAAW,QAAA;EACX,YAAA;EACA,aAAA;EACA,kBAAA;AAAA,+BACI,CAAA,CAAA,SAAA;;;;;;;;;;;;;;;;;;;;;;;;;aAAA,sBAAA,CAAA,UAAA"}
|
|
@@ -399,10 +399,13 @@ function piiMiddleware(piiType, options = {}) {
|
|
|
399
399
|
if (!messages || messages.length === 0) return;
|
|
400
400
|
let lastAiIdx = null;
|
|
401
401
|
let lastAiMsg = null;
|
|
402
|
-
for (let i = messages.length - 1; i >= 0; i--)
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
402
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
403
|
+
const msg = messages[i];
|
|
404
|
+
if (AIMessage.isInstance(msg)) {
|
|
405
|
+
lastAiMsg = msg;
|
|
406
|
+
lastAiIdx = i;
|
|
407
|
+
break;
|
|
408
|
+
}
|
|
406
409
|
}
|
|
407
410
|
if (lastAiIdx === null || !lastAiMsg || !lastAiMsg.content) return;
|
|
408
411
|
const { content: newContent, matches } = processContent(String(lastAiMsg.content), resolvedRule);
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"pii.js","names":[],"sources":["../../../src/agents/middleware/pii.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport { sha256 } from \"@langchain/core/utils/hash\";\nimport { AIMessage, HumanMessage, ToolMessage } from \"@langchain/core/messages\";\nimport type { InferInteropZodInput } from \"@langchain/core/utils/types\";\n\nimport { createMiddleware } from \"../middleware.js\";\n\n/**\n * Represents a detected PII match in content\n */\nexport interface PIIMatch {\n /**\n * The matched text\n */\n text: string;\n /**\n * The start index of the match\n */\n start: number;\n /**\n * The end index of the match\n */\n end: number;\n}\n\n/**\n * Error thrown when PII is detected and strategy is 'block'\n */\nexport class PIIDetectionError extends Error {\n constructor(\n public readonly piiType: string,\n public readonly matches: PIIMatch[]\n ) {\n super(`PII detected: ${piiType} found ${matches.length} occurrence(s)`);\n this.name = \"PIIDetectionError\";\n }\n}\n\n/**\n * Strategy for handling detected PII\n */\nexport type PIIStrategy = \"block\" | \"redact\" | \"mask\" | \"hash\";\n\n/**\n * Built-in PII types\n */\nexport type BuiltInPIIType =\n | \"email\"\n | \"credit_card\"\n | \"ip\"\n | \"mac_address\"\n | \"url\";\n\n/**\n * Custom detector function that takes content and returns matches\n */\nexport type PIIDetector = (content: string) => PIIMatch[];\nexport type Detector = PIIDetector | RegExp | string;\n\n/**\n * Configuration for a redaction rule\n */\nexport interface RedactionRuleConfig {\n /**\n * Type of PII to detect (built-in or custom name)\n */\n piiType: BuiltInPIIType | string;\n /**\n * Strategy for handling detected PII\n */\n strategy: PIIStrategy;\n /**\n * Custom detector function or regex pattern string\n */\n detector?: Detector;\n}\n\n/**\n * Resolved redaction rule with a concrete detector function\n */\nexport interface ResolvedRedactionRule {\n piiType: string;\n strategy: PIIStrategy;\n detector: PIIDetector;\n}\n\n/**\n * Email detection regex pattern\n */\nconst EMAIL_PATTERN = /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g;\n\n/**\n * Credit card detection regex pattern (basic, will be validated with Luhn)\n */\nconst CREDIT_CARD_PATTERN = /\\b(?:\\d{4}[-\\s]?){3}\\d{4}\\b/g;\n\n/**\n * IP address detection regex pattern\n */\nconst IP_PATTERN =\n /\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\b/g;\n\n/**\n * MAC address detection regex pattern\n */\nconst MAC_ADDRESS_PATTERN = /\\b(?:[0-9A-Fa-f]{2}[:-]){5}(?:[0-9A-Fa-f]{2})\\b/g;\n\n/**\n * URL detection regex pattern\n */\nconst URL_PATTERN = /(?:https?:\\/\\/|www\\.)[^\\s<>\"{}|\\\\^`[\\]]+/gi;\n\n/**\n * Luhn algorithm for credit card validation\n */\nfunction luhnCheck(cardNumber: string): boolean {\n const digits = cardNumber.replace(/\\D/g, \"\");\n let sum = 0;\n let isEven = false;\n\n for (let i = digits.length - 1; i >= 0; i--) {\n let digit = parseInt(digits[i], 10);\n\n if (isEven) {\n digit *= 2;\n if (digit > 9) {\n digit -= 9;\n }\n }\n\n sum += digit;\n isEven = !isEven;\n }\n\n return sum % 10 === 0;\n}\n\n/**\n * Convert regex match to PIIMatch\n */\nfunction regexMatchToPIIMatch(match: RegExpMatchArray): PIIMatch {\n return {\n text: match[0],\n start: match.index ?? 0,\n end: (match.index ?? 0) + match[0].length,\n };\n}\n\n/**\n * Detect email addresses in content\n */\nexport function detectEmail(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(EMAIL_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n}\n\n/**\n * Detect credit card numbers in content (validated with Luhn algorithm)\n */\nexport function detectCreditCard(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(CREDIT_CARD_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n const cardNumber = match[0].replace(/\\D/g, \"\");\n // Credit cards are typically 13-19 digits\n if (\n cardNumber.length >= 13 &&\n cardNumber.length <= 19 &&\n luhnCheck(cardNumber)\n ) {\n matches.push(regexMatchToPIIMatch(match));\n }\n }\n\n return matches;\n}\n\n/**\n * Detect IP addresses in content (validated)\n */\nexport function detectIP(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(IP_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n const ip = match[0];\n // Additional validation: each octet should be 0-255\n const parts = ip.split(\".\");\n if (\n parts.length === 4 &&\n parts.every((part) => {\n const num = parseInt(part, 10);\n return num >= 0 && num <= 255;\n })\n ) {\n matches.push(regexMatchToPIIMatch(match));\n }\n }\n\n return matches;\n}\n\n/**\n * Detect MAC addresses in content\n */\nexport function detectMacAddress(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(MAC_ADDRESS_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n}\n\n/**\n * Detect URLs in content\n */\nexport function detectUrl(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(URL_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n}\n\n/**\n * Built-in detector map\n */\nconst BUILT_IN_DETECTORS: Record<BuiltInPIIType, PIIDetector> = {\n email: detectEmail,\n credit_card: detectCreditCard,\n ip: detectIP,\n mac_address: detectMacAddress,\n url: detectUrl,\n};\n\n/**\n * Resolve a redaction rule to a concrete detector function\n */\nexport function resolveRedactionRule(\n config: RedactionRuleConfig\n): ResolvedRedactionRule {\n let detector: PIIDetector;\n\n if (config.detector) {\n if (typeof config.detector === \"string\") {\n // Regex pattern string\n const regex = new RegExp(config.detector, \"g\");\n detector = (content: string) => {\n const matches: PIIMatch[] = [];\n let match: RegExpMatchArray | null;\n const regexCopy = new RegExp(regex);\n\n while ((match = regexCopy.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n };\n // eslint-disable-next-line no-instanceof/no-instanceof\n } else if (config.detector instanceof RegExp) {\n detector = (content: string) => {\n // eslint-disable-next-line no-instanceof/no-instanceof\n if (!(config.detector instanceof RegExp)) {\n throw new Error(\"Detector is required\");\n }\n const matches: PIIMatch[] = [];\n let match: RegExpMatchArray | null;\n while ((match = config.detector.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n };\n } else {\n detector = config.detector;\n }\n } else {\n // Use built-in detector\n const builtInType = config.piiType as BuiltInPIIType;\n if (!BUILT_IN_DETECTORS[builtInType]) {\n throw new Error(\n `Unknown PII type: ${config.piiType}. Must be one of: ${Object.keys(\n BUILT_IN_DETECTORS\n ).join(\", \")}, or provide a custom detector.`\n );\n }\n detector = BUILT_IN_DETECTORS[builtInType];\n }\n\n return {\n piiType: config.piiType,\n strategy: config.strategy,\n detector,\n };\n}\n\n/**\n * Apply redact strategy: replace with [REDACTED_TYPE]\n */\nfunction applyRedactStrategy(\n content: string,\n matches: PIIMatch[],\n piiType: string\n): string {\n let result = content;\n // Process matches in reverse order to preserve indices\n for (let i = matches.length - 1; i >= 0; i--) {\n const match = matches[i];\n const replacement = `[REDACTED_${piiType.toUpperCase()}]`;\n result =\n result.slice(0, match.start) + replacement + result.slice(match.end);\n }\n return result;\n}\n\n/**\n * Apply mask strategy: partially mask PII (show last few characters)\n */\nfunction applyMaskStrategy(\n content: string,\n matches: PIIMatch[],\n piiType: string\n): string {\n let result = content;\n // Process matches in reverse order to preserve indices\n for (let i = matches.length - 1; i >= 0; i--) {\n const match = matches[i];\n const text = match.text;\n let masked: string;\n\n if (piiType === \"credit_card\") {\n // Show last 4 digits: ****-****-****-1234\n const digits = text.replace(/\\D/g, \"\");\n const last4 = digits.slice(-4);\n masked = `****-****-****-${last4}`;\n } else if (piiType === \"email\") {\n // Show first char and domain: j***@example.com\n const [local, domain] = text.split(\"@\");\n if (local && domain) {\n masked = `${local[0]}***@${domain}`;\n } else {\n masked = \"***\";\n }\n } else {\n // Default: show last 4 characters\n const visibleChars = Math.min(4, text.length);\n masked = `${\"*\".repeat(\n Math.max(0, text.length - visibleChars)\n )}${text.slice(-visibleChars)}`;\n }\n\n result = result.slice(0, match.start) + masked + result.slice(match.end);\n }\n return result;\n}\n\n/**\n * Apply hash strategy: replace with deterministic hash\n */\nfunction applyHashStrategy(\n content: string,\n matches: PIIMatch[],\n piiType: string\n): string {\n let result = content;\n // Process matches in reverse order to preserve indices\n for (let i = matches.length - 1; i >= 0; i--) {\n const match = matches[i];\n const hash = sha256(match.text).slice(0, 8);\n const replacement = `<${piiType}_hash:${hash}>`;\n result =\n result.slice(0, match.start) + replacement + result.slice(match.end);\n }\n return result;\n}\n\n/**\n * Apply strategy to content based on matches\n */\nexport function applyStrategy(\n content: string,\n matches: PIIMatch[],\n strategy: PIIStrategy,\n piiType: string\n): string {\n if (matches.length === 0) {\n return content;\n }\n\n switch (strategy) {\n case \"block\":\n throw new PIIDetectionError(piiType, matches);\n case \"redact\":\n return applyRedactStrategy(content, matches, piiType);\n case \"mask\":\n return applyMaskStrategy(content, matches, piiType);\n case \"hash\":\n return applyHashStrategy(content, matches, piiType);\n default:\n throw new Error(`Unknown strategy: ${strategy}`);\n }\n}\n\n/**\n * Configuration schema for PII middleware\n */\nconst contextSchema = z.object({\n /**\n * Whether to check user messages before model call\n */\n applyToInput: z.boolean().optional(),\n /**\n * Whether to check AI messages after model call\n */\n applyToOutput: z.boolean().optional(),\n /**\n * Whether to check tool result messages after tool execution\n */\n applyToToolResults: z.boolean().optional(),\n});\n\nexport type PIIMiddlewareConfig = InferInteropZodInput<typeof contextSchema>;\n\n/**\n * Process content for PII detection and apply strategy\n */\nfunction processContent(\n content: string,\n rule: ResolvedRedactionRule\n): { content: string; matches: PIIMatch[] } {\n const matches = rule.detector(content);\n if (matches.length === 0) {\n return { content, matches: [] };\n }\n\n const sanitized = applyStrategy(\n content,\n matches,\n rule.strategy,\n rule.piiType\n );\n return { content: sanitized, matches };\n}\n\n/**\n * Creates a middleware that detects and handles personally identifiable information (PII)\n * in conversations.\n *\n * This middleware detects common PII types and applies configurable strategies to handle them.\n * It can detect emails, credit cards, IP addresses, MAC addresses, and URLs in both user input\n * and agent output.\n *\n * Built-in PII types:\n * - `email`: Email addresses\n * - `credit_card`: Credit card numbers (validated with Luhn algorithm)\n * - `ip`: IP addresses (validated)\n * - `mac_address`: MAC addresses\n * - `url`: URLs (both `http`/`https` and bare URLs)\n *\n * Strategies:\n * - `block`: Raise an exception when PII is detected\n * - `redact`: Replace PII with `[REDACTED_TYPE]` placeholders\n * - `mask`: Partially mask PII (e.g., `****-****-****-1234` for credit card)\n * - `hash`: Replace PII with deterministic hash (e.g., `<email_hash:a1b2c3d4>`)\n *\n * Strategy Selection Guide:\n * | Strategy | Preserves Identity? | Best For |\n * | -------- | ------------------- | --------------------------------------- |\n * | `block` | N/A | Avoid PII completely |\n * | `redact` | No | General compliance, log sanitization |\n * | `mask` | No | Human readability, customer service UIs |\n * | `hash` | Yes (pseudonymous) | Analytics, debugging |\n *\n * @param piiType - Type of PII to detect. Can be a built-in type (`email`, `credit_card`, `ip`, `mac_address`, `url`) or a custom type name.\n * @param options - Configuration options\n * @param options.strategy - How to handle detected PII. Defaults to `\"redact\"`.\n * @param options.detector - Custom detector function or regex pattern string. If not provided, uses built-in detector for the `piiType`.\n * @param options.applyToInput - Whether to check user messages before model call. Defaults to `true`.\n * @param options.applyToOutput - Whether to check AI messages after model call. Defaults to `false`.\n * @param options.applyToToolResults - Whether to check tool result messages after tool execution. Defaults to `false`.\n *\n * @returns Middleware instance for use with `createAgent`\n *\n * @throws {PIIDetectionError} When PII is detected and strategy is `'block'`\n * @throws {Error} If `piiType` is not built-in and no detector is provided\n *\n * @example Basic usage\n * ```typescript\n * import { piiMiddleware } from \"langchain\";\n * import { createAgent } from \"langchain\";\n *\n * // Redact all emails in user input\n * const agent = createAgent({\n * model: \"openai:gpt-4\",\n * middleware: [\n * piiMiddleware(\"email\", { strategy: \"redact\" }),\n * ],\n * });\n * ```\n *\n * @example Different strategies for different PII types\n * ```typescript\n * const agent = createAgent({\n * model: \"openai:gpt-4o\",\n * middleware: [\n * piiMiddleware(\"credit_card\", { strategy: \"mask\" }),\n * piiMiddleware(\"url\", { strategy: \"redact\" }),\n * piiMiddleware(\"ip\", { strategy: \"hash\" }),\n * ],\n * });\n * ```\n *\n * @example Custom PII type with regex\n * ```typescript\n * const agent = createAgent({\n * model: \"openai:gpt-4\",\n * middleware: [\n * piiMiddleware(\"api_key\", {\n * detector: \"sk-[a-zA-Z0-9]{32}\",\n * strategy: \"block\",\n * }),\n * ],\n * });\n * ```\n *\n * @public\n */\nexport function piiMiddleware(\n piiType: BuiltInPIIType | string,\n options: {\n strategy?: PIIStrategy;\n detector?: Detector;\n applyToInput?: boolean;\n applyToOutput?: boolean;\n applyToToolResults?: boolean;\n } = {}\n): ReturnType<typeof createMiddleware> {\n const { strategy = \"redact\", detector } = options;\n const resolvedRule = resolveRedactionRule({\n piiType,\n strategy,\n detector,\n });\n\n const middlewareName = `PIIMiddleware[${resolvedRule.piiType}]`;\n\n return createMiddleware({\n name: middlewareName,\n contextSchema,\n beforeModel: async (state, runtime) => {\n const applyToInput =\n runtime.context.applyToInput ?? options.applyToInput ?? true;\n const applyToToolResults =\n runtime.context.applyToToolResults ??\n options.applyToToolResults ??\n false;\n\n if (!applyToInput && !applyToToolResults) {\n return;\n }\n\n const messages = state.messages;\n if (!messages || messages.length === 0) {\n return;\n }\n\n const newMessages = [...messages];\n let anyModified = false;\n\n // Check user input if enabled\n if (applyToInput) {\n // Get last user message\n let lastUserIdx: number | null = null;\n for (let i = messages.length - 1; i >= 0; i--) {\n if (HumanMessage.isInstance(messages[i])) {\n lastUserIdx = i;\n break;\n }\n }\n\n if (lastUserIdx !== null) {\n const lastUserMsg = messages[lastUserIdx];\n if (lastUserMsg && lastUserMsg.content) {\n const content = String(lastUserMsg.content);\n const { content: newContent, matches } = processContent(\n content,\n resolvedRule\n );\n\n if (matches.length > 0) {\n newMessages[lastUserIdx] = new HumanMessage({\n content: newContent,\n id: lastUserMsg.id,\n name: lastUserMsg.name,\n });\n anyModified = true;\n }\n }\n }\n }\n\n // Check tool results if enabled\n if (applyToToolResults) {\n // Find the last AIMessage, then process all ToolMessage objects after it\n let lastAiIdx: number | null = null;\n for (let i = messages.length - 1; i >= 0; i--) {\n if (AIMessage.isInstance(messages[i])) {\n lastAiIdx = i;\n break;\n }\n }\n\n if (lastAiIdx !== null) {\n // Get all tool messages after the last AI message\n for (let i = lastAiIdx + 1; i < messages.length; i++) {\n const msg = messages[i];\n if (ToolMessage.isInstance(msg)) {\n if (!msg.content) {\n continue;\n }\n\n const content = String(msg.content);\n const { content: newContent, matches } = processContent(\n content,\n resolvedRule\n );\n\n if (matches.length > 0) {\n newMessages[i] = new ToolMessage({\n content: newContent,\n id: msg.id,\n name: msg.name,\n tool_call_id: msg.tool_call_id,\n });\n anyModified = true;\n }\n }\n }\n }\n }\n\n if (anyModified) {\n return { messages: newMessages };\n }\n\n return;\n },\n afterModel: async (state, runtime) => {\n const applyToOutput =\n runtime.context.applyToOutput ?? options.applyToOutput ?? false;\n\n if (!applyToOutput) {\n return;\n }\n\n const messages = state.messages;\n if (!messages || messages.length === 0) {\n return;\n }\n\n // Get last AI message\n let lastAiIdx: number | null = null;\n let lastAiMsg: AIMessage | null = null;\n for (let i = messages.length - 1; i >= 0; i--) {\n if (AIMessage.isInstance(messages[i])) {\n lastAiMsg = messages[i];\n lastAiIdx = i;\n break;\n }\n }\n\n if (lastAiIdx === null || !lastAiMsg || !lastAiMsg.content) {\n return;\n }\n\n // Detect PII in message content\n const content = String(lastAiMsg.content);\n const { content: newContent, matches } = processContent(\n content,\n resolvedRule\n );\n\n if (matches.length === 0) {\n return;\n }\n\n // Create updated message\n const updatedMessage = new AIMessage({\n content: newContent,\n id: lastAiMsg.id,\n name: lastAiMsg.name,\n tool_calls: lastAiMsg.tool_calls,\n });\n\n // Return updated messages\n const newMessages = [...messages];\n newMessages[lastAiIdx] = updatedMessage;\n return { messages: newMessages };\n },\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"pii.js","names":[],"sources":["../../../src/agents/middleware/pii.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport { sha256 } from \"@langchain/core/utils/hash\";\nimport { AIMessage, HumanMessage, ToolMessage } from \"@langchain/core/messages\";\nimport type { InferInteropZodInput } from \"@langchain/core/utils/types\";\n\nimport { createMiddleware } from \"../middleware.js\";\n\n/**\n * Represents a detected PII match in content\n */\nexport interface PIIMatch {\n /**\n * The matched text\n */\n text: string;\n /**\n * The start index of the match\n */\n start: number;\n /**\n * The end index of the match\n */\n end: number;\n}\n\n/**\n * Error thrown when PII is detected and strategy is 'block'\n */\nexport class PIIDetectionError extends Error {\n constructor(\n public readonly piiType: string,\n public readonly matches: PIIMatch[]\n ) {\n super(`PII detected: ${piiType} found ${matches.length} occurrence(s)`);\n this.name = \"PIIDetectionError\";\n }\n}\n\n/**\n * Strategy for handling detected PII\n */\nexport type PIIStrategy = \"block\" | \"redact\" | \"mask\" | \"hash\";\n\n/**\n * Built-in PII types\n */\nexport type BuiltInPIIType =\n | \"email\"\n | \"credit_card\"\n | \"ip\"\n | \"mac_address\"\n | \"url\";\n\n/**\n * Custom detector function that takes content and returns matches\n */\nexport type PIIDetector = (content: string) => PIIMatch[];\nexport type Detector = PIIDetector | RegExp | string;\n\n/**\n * Configuration for a redaction rule\n */\nexport interface RedactionRuleConfig {\n /**\n * Type of PII to detect (built-in or custom name)\n */\n piiType: BuiltInPIIType | string;\n /**\n * Strategy for handling detected PII\n */\n strategy: PIIStrategy;\n /**\n * Custom detector function or regex pattern string\n */\n detector?: Detector;\n}\n\n/**\n * Resolved redaction rule with a concrete detector function\n */\nexport interface ResolvedRedactionRule {\n piiType: string;\n strategy: PIIStrategy;\n detector: PIIDetector;\n}\n\n/**\n * Email detection regex pattern\n */\nconst EMAIL_PATTERN = /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g;\n\n/**\n * Credit card detection regex pattern (basic, will be validated with Luhn)\n */\nconst CREDIT_CARD_PATTERN = /\\b(?:\\d{4}[-\\s]?){3}\\d{4}\\b/g;\n\n/**\n * IP address detection regex pattern\n */\nconst IP_PATTERN =\n /\\b(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\b/g;\n\n/**\n * MAC address detection regex pattern\n */\nconst MAC_ADDRESS_PATTERN = /\\b(?:[0-9A-Fa-f]{2}[:-]){5}(?:[0-9A-Fa-f]{2})\\b/g;\n\n/**\n * URL detection regex pattern\n */\nconst URL_PATTERN = /(?:https?:\\/\\/|www\\.)[^\\s<>\"{}|\\\\^`[\\]]+/gi;\n\n/**\n * Luhn algorithm for credit card validation\n */\nfunction luhnCheck(cardNumber: string): boolean {\n const digits = cardNumber.replace(/\\D/g, \"\");\n let sum = 0;\n let isEven = false;\n\n for (let i = digits.length - 1; i >= 0; i--) {\n let digit = parseInt(digits[i], 10);\n\n if (isEven) {\n digit *= 2;\n if (digit > 9) {\n digit -= 9;\n }\n }\n\n sum += digit;\n isEven = !isEven;\n }\n\n return sum % 10 === 0;\n}\n\n/**\n * Convert regex match to PIIMatch\n */\nfunction regexMatchToPIIMatch(match: RegExpMatchArray): PIIMatch {\n return {\n text: match[0],\n start: match.index ?? 0,\n end: (match.index ?? 0) + match[0].length,\n };\n}\n\n/**\n * Detect email addresses in content\n */\nexport function detectEmail(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(EMAIL_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n}\n\n/**\n * Detect credit card numbers in content (validated with Luhn algorithm)\n */\nexport function detectCreditCard(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(CREDIT_CARD_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n const cardNumber = match[0].replace(/\\D/g, \"\");\n // Credit cards are typically 13-19 digits\n if (\n cardNumber.length >= 13 &&\n cardNumber.length <= 19 &&\n luhnCheck(cardNumber)\n ) {\n matches.push(regexMatchToPIIMatch(match));\n }\n }\n\n return matches;\n}\n\n/**\n * Detect IP addresses in content (validated)\n */\nexport function detectIP(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(IP_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n const ip = match[0];\n // Additional validation: each octet should be 0-255\n const parts = ip.split(\".\");\n if (\n parts.length === 4 &&\n parts.every((part) => {\n const num = parseInt(part, 10);\n return num >= 0 && num <= 255;\n })\n ) {\n matches.push(regexMatchToPIIMatch(match));\n }\n }\n\n return matches;\n}\n\n/**\n * Detect MAC addresses in content\n */\nexport function detectMacAddress(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(MAC_ADDRESS_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n}\n\n/**\n * Detect URLs in content\n */\nexport function detectUrl(content: string): PIIMatch[] {\n const matches: PIIMatch[] = [];\n const regex = new RegExp(URL_PATTERN);\n let match: RegExpMatchArray | null;\n\n while ((match = regex.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n}\n\n/**\n * Built-in detector map\n */\nconst BUILT_IN_DETECTORS: Record<BuiltInPIIType, PIIDetector> = {\n email: detectEmail,\n credit_card: detectCreditCard,\n ip: detectIP,\n mac_address: detectMacAddress,\n url: detectUrl,\n};\n\n/**\n * Resolve a redaction rule to a concrete detector function\n */\nexport function resolveRedactionRule(\n config: RedactionRuleConfig\n): ResolvedRedactionRule {\n let detector: PIIDetector;\n\n if (config.detector) {\n if (typeof config.detector === \"string\") {\n // Regex pattern string\n const regex = new RegExp(config.detector, \"g\");\n detector = (content: string) => {\n const matches: PIIMatch[] = [];\n let match: RegExpMatchArray | null;\n const regexCopy = new RegExp(regex);\n\n while ((match = regexCopy.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n };\n // eslint-disable-next-line no-instanceof/no-instanceof\n } else if (config.detector instanceof RegExp) {\n detector = (content: string) => {\n // eslint-disable-next-line no-instanceof/no-instanceof\n if (!(config.detector instanceof RegExp)) {\n throw new Error(\"Detector is required\");\n }\n const matches: PIIMatch[] = [];\n let match: RegExpMatchArray | null;\n while ((match = config.detector.exec(content)) !== null) {\n matches.push(regexMatchToPIIMatch(match));\n }\n\n return matches;\n };\n } else {\n detector = config.detector;\n }\n } else {\n // Use built-in detector\n const builtInType = config.piiType as BuiltInPIIType;\n if (!BUILT_IN_DETECTORS[builtInType]) {\n throw new Error(\n `Unknown PII type: ${config.piiType}. Must be one of: ${Object.keys(\n BUILT_IN_DETECTORS\n ).join(\", \")}, or provide a custom detector.`\n );\n }\n detector = BUILT_IN_DETECTORS[builtInType];\n }\n\n return {\n piiType: config.piiType,\n strategy: config.strategy,\n detector,\n };\n}\n\n/**\n * Apply redact strategy: replace with [REDACTED_TYPE]\n */\nfunction applyRedactStrategy(\n content: string,\n matches: PIIMatch[],\n piiType: string\n): string {\n let result = content;\n // Process matches in reverse order to preserve indices\n for (let i = matches.length - 1; i >= 0; i--) {\n const match = matches[i];\n const replacement = `[REDACTED_${piiType.toUpperCase()}]`;\n result =\n result.slice(0, match.start) + replacement + result.slice(match.end);\n }\n return result;\n}\n\n/**\n * Apply mask strategy: partially mask PII (show last few characters)\n */\nfunction applyMaskStrategy(\n content: string,\n matches: PIIMatch[],\n piiType: string\n): string {\n let result = content;\n // Process matches in reverse order to preserve indices\n for (let i = matches.length - 1; i >= 0; i--) {\n const match = matches[i];\n const text = match.text;\n let masked: string;\n\n if (piiType === \"credit_card\") {\n // Show last 4 digits: ****-****-****-1234\n const digits = text.replace(/\\D/g, \"\");\n const last4 = digits.slice(-4);\n masked = `****-****-****-${last4}`;\n } else if (piiType === \"email\") {\n // Show first char and domain: j***@example.com\n const [local, domain] = text.split(\"@\");\n if (local && domain) {\n masked = `${local[0]}***@${domain}`;\n } else {\n masked = \"***\";\n }\n } else {\n // Default: show last 4 characters\n const visibleChars = Math.min(4, text.length);\n masked = `${\"*\".repeat(\n Math.max(0, text.length - visibleChars)\n )}${text.slice(-visibleChars)}`;\n }\n\n result = result.slice(0, match.start) + masked + result.slice(match.end);\n }\n return result;\n}\n\n/**\n * Apply hash strategy: replace with deterministic hash\n */\nfunction applyHashStrategy(\n content: string,\n matches: PIIMatch[],\n piiType: string\n): string {\n let result = content;\n // Process matches in reverse order to preserve indices\n for (let i = matches.length - 1; i >= 0; i--) {\n const match = matches[i];\n const hash = sha256(match.text).slice(0, 8);\n const replacement = `<${piiType}_hash:${hash}>`;\n result =\n result.slice(0, match.start) + replacement + result.slice(match.end);\n }\n return result;\n}\n\n/**\n * Apply strategy to content based on matches\n */\nexport function applyStrategy(\n content: string,\n matches: PIIMatch[],\n strategy: PIIStrategy,\n piiType: string\n): string {\n if (matches.length === 0) {\n return content;\n }\n\n switch (strategy) {\n case \"block\":\n throw new PIIDetectionError(piiType, matches);\n case \"redact\":\n return applyRedactStrategy(content, matches, piiType);\n case \"mask\":\n return applyMaskStrategy(content, matches, piiType);\n case \"hash\":\n return applyHashStrategy(content, matches, piiType);\n default:\n throw new Error(`Unknown strategy: ${strategy}`);\n }\n}\n\n/**\n * Configuration schema for PII middleware\n */\nconst contextSchema = z.object({\n /**\n * Whether to check user messages before model call\n */\n applyToInput: z.boolean().optional(),\n /**\n * Whether to check AI messages after model call\n */\n applyToOutput: z.boolean().optional(),\n /**\n * Whether to check tool result messages after tool execution\n */\n applyToToolResults: z.boolean().optional(),\n});\n\nexport type PIIMiddlewareConfig = InferInteropZodInput<typeof contextSchema>;\n\n/**\n * Process content for PII detection and apply strategy\n */\nfunction processContent(\n content: string,\n rule: ResolvedRedactionRule\n): { content: string; matches: PIIMatch[] } {\n const matches = rule.detector(content);\n if (matches.length === 0) {\n return { content, matches: [] };\n }\n\n const sanitized = applyStrategy(\n content,\n matches,\n rule.strategy,\n rule.piiType\n );\n return { content: sanitized, matches };\n}\n\n/**\n * Creates a middleware that detects and handles personally identifiable information (PII)\n * in conversations.\n *\n * This middleware detects common PII types and applies configurable strategies to handle them.\n * It can detect emails, credit cards, IP addresses, MAC addresses, and URLs in both user input\n * and agent output.\n *\n * Built-in PII types:\n * - `email`: Email addresses\n * - `credit_card`: Credit card numbers (validated with Luhn algorithm)\n * - `ip`: IP addresses (validated)\n * - `mac_address`: MAC addresses\n * - `url`: URLs (both `http`/`https` and bare URLs)\n *\n * Strategies:\n * - `block`: Raise an exception when PII is detected\n * - `redact`: Replace PII with `[REDACTED_TYPE]` placeholders\n * - `mask`: Partially mask PII (e.g., `****-****-****-1234` for credit card)\n * - `hash`: Replace PII with deterministic hash (e.g., `<email_hash:a1b2c3d4>`)\n *\n * Strategy Selection Guide:\n * | Strategy | Preserves Identity? | Best For |\n * | -------- | ------------------- | --------------------------------------- |\n * | `block` | N/A | Avoid PII completely |\n * | `redact` | No | General compliance, log sanitization |\n * | `mask` | No | Human readability, customer service UIs |\n * | `hash` | Yes (pseudonymous) | Analytics, debugging |\n *\n * @param piiType - Type of PII to detect. Can be a built-in type (`email`, `credit_card`, `ip`, `mac_address`, `url`) or a custom type name.\n * @param options - Configuration options\n * @param options.strategy - How to handle detected PII. Defaults to `\"redact\"`.\n * @param options.detector - Custom detector function or regex pattern string. If not provided, uses built-in detector for the `piiType`.\n * @param options.applyToInput - Whether to check user messages before model call. Defaults to `true`.\n * @param options.applyToOutput - Whether to check AI messages after model call. Defaults to `false`.\n * @param options.applyToToolResults - Whether to check tool result messages after tool execution. Defaults to `false`.\n *\n * @returns Middleware instance for use with `createAgent`\n *\n * @throws {PIIDetectionError} When PII is detected and strategy is `'block'`\n * @throws {Error} If `piiType` is not built-in and no detector is provided\n *\n * @example Basic usage\n * ```typescript\n * import { piiMiddleware } from \"langchain\";\n * import { createAgent } from \"langchain\";\n *\n * // Redact all emails in user input\n * const agent = createAgent({\n * model: \"openai:gpt-4\",\n * middleware: [\n * piiMiddleware(\"email\", { strategy: \"redact\" }),\n * ],\n * });\n * ```\n *\n * @example Different strategies for different PII types\n * ```typescript\n * const agent = createAgent({\n * model: \"openai:gpt-4o\",\n * middleware: [\n * piiMiddleware(\"credit_card\", { strategy: \"mask\" }),\n * piiMiddleware(\"url\", { strategy: \"redact\" }),\n * piiMiddleware(\"ip\", { strategy: \"hash\" }),\n * ],\n * });\n * ```\n *\n * @example Custom PII type with regex\n * ```typescript\n * const agent = createAgent({\n * model: \"openai:gpt-4\",\n * middleware: [\n * piiMiddleware(\"api_key\", {\n * detector: \"sk-[a-zA-Z0-9]{32}\",\n * strategy: \"block\",\n * }),\n * ],\n * });\n * ```\n *\n * @public\n */\nexport function piiMiddleware(\n piiType: BuiltInPIIType | string,\n options: {\n strategy?: PIIStrategy;\n detector?: Detector;\n applyToInput?: boolean;\n applyToOutput?: boolean;\n applyToToolResults?: boolean;\n } = {}\n) {\n const { strategy = \"redact\", detector } = options;\n const resolvedRule = resolveRedactionRule({\n piiType,\n strategy,\n detector,\n });\n\n const middlewareName = `PIIMiddleware[${resolvedRule.piiType}]`;\n\n return createMiddleware({\n name: middlewareName,\n contextSchema,\n beforeModel: async (state, runtime) => {\n const applyToInput =\n runtime.context.applyToInput ?? options.applyToInput ?? true;\n const applyToToolResults =\n runtime.context.applyToToolResults ??\n options.applyToToolResults ??\n false;\n\n if (!applyToInput && !applyToToolResults) {\n return;\n }\n\n const messages = state.messages;\n if (!messages || messages.length === 0) {\n return;\n }\n\n const newMessages = [...messages];\n let anyModified = false;\n\n // Check user input if enabled\n if (applyToInput) {\n // Get last user message\n let lastUserIdx: number | null = null;\n for (let i = messages.length - 1; i >= 0; i--) {\n if (HumanMessage.isInstance(messages[i])) {\n lastUserIdx = i;\n break;\n }\n }\n\n if (lastUserIdx !== null) {\n const lastUserMsg = messages[lastUserIdx];\n if (lastUserMsg && lastUserMsg.content) {\n const content = String(lastUserMsg.content);\n const { content: newContent, matches } = processContent(\n content,\n resolvedRule\n );\n\n if (matches.length > 0) {\n newMessages[lastUserIdx] = new HumanMessage({\n content: newContent,\n id: lastUserMsg.id,\n name: lastUserMsg.name,\n });\n anyModified = true;\n }\n }\n }\n }\n\n // Check tool results if enabled\n if (applyToToolResults) {\n // Find the last AIMessage, then process all ToolMessage objects after it\n let lastAiIdx: number | null = null;\n for (let i = messages.length - 1; i >= 0; i--) {\n if (AIMessage.isInstance(messages[i])) {\n lastAiIdx = i;\n break;\n }\n }\n\n if (lastAiIdx !== null) {\n // Get all tool messages after the last AI message\n for (let i = lastAiIdx + 1; i < messages.length; i++) {\n const msg = messages[i];\n if (ToolMessage.isInstance(msg)) {\n if (!msg.content) {\n continue;\n }\n\n const content = String(msg.content);\n const { content: newContent, matches } = processContent(\n content,\n resolvedRule\n );\n\n if (matches.length > 0) {\n newMessages[i] = new ToolMessage({\n content: newContent,\n id: msg.id,\n name: msg.name,\n tool_call_id: msg.tool_call_id,\n });\n anyModified = true;\n }\n }\n }\n }\n }\n\n if (anyModified) {\n return { messages: newMessages };\n }\n\n return;\n },\n afterModel: async (state, runtime) => {\n const applyToOutput =\n runtime.context.applyToOutput ?? options.applyToOutput ?? false;\n\n if (!applyToOutput) {\n return;\n }\n\n const messages = state.messages;\n if (!messages || messages.length === 0) {\n return;\n }\n\n // Get last AI message\n let lastAiIdx: number | null = null;\n let lastAiMsg: AIMessage | null = null;\n for (let i = messages.length - 1; i >= 0; i--) {\n const msg = messages[i];\n if (AIMessage.isInstance(msg)) {\n lastAiMsg = msg;\n lastAiIdx = i;\n break;\n }\n }\n\n if (lastAiIdx === null || !lastAiMsg || !lastAiMsg.content) {\n return;\n }\n\n // Detect PII in message content\n const content = String(lastAiMsg.content);\n const { content: newContent, matches } = processContent(\n content,\n resolvedRule\n );\n\n if (matches.length === 0) {\n return;\n }\n\n // Create updated message\n const updatedMessage = new AIMessage({\n content: newContent,\n id: lastAiMsg.id,\n name: lastAiMsg.name,\n tool_calls: lastAiMsg.tool_calls,\n });\n\n // Return updated messages\n const newMessages = [...messages];\n newMessages[lastAiIdx] = updatedMessage;\n return { messages: newMessages };\n },\n });\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"piiRedaction.cjs","names":["z","HumanMessage","ToolMessage","SystemMessage","AIMessage","createMiddleware","RemoveMessage"],"sources":["../../../src/agents/middleware/piiRedaction.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport {\n BaseMessage,\n AIMessage,\n HumanMessage,\n ToolMessage,\n RemoveMessage,\n SystemMessage,\n} from \"@langchain/core/messages\";\nimport type { InferInteropZodInput } from \"@langchain/core/utils/types\";\n\nimport { createMiddleware } from \"../middleware.js\";\n\n/**\n * Type for the redaction map that stores original values by ID\n */\ntype RedactionMap = Record<string, string>;\n\n/**\n * Configuration schema for the Input Guardrails middleware\n */\nconst contextSchema = z.object({\n /**\n * A record of PII detection rules to apply\n * @default DEFAULT_PII_RULES (with enabled rules only)\n */\n rules: z\n .record(\n z.string(),\n z.instanceof(RegExp).describe(\"Regular expression pattern to match PII\")\n )\n .optional(),\n});\n\n/**\n * @deprecated\n */\nexport type PIIRedactionMiddlewareConfig = InferInteropZodInput<\n typeof contextSchema\n>;\n\n/**\n * Generate a unique ID for a redaction\n */\nfunction generateRedactionId(): string {\n return Math.random().toString(36).substring(2, 11);\n}\n\n/**\n * Apply PII detection rules to text with ID tracking\n */\nfunction applyPIIRules(\n text: string,\n rules: Record<string, RegExp>,\n redactionMap: RedactionMap\n): string {\n let processedText = text;\n\n for (const [name, pattern] of Object.entries(rules)) {\n const replacement = name.toUpperCase().replace(/[^a-zA-Z0-9_-]/g, \"\");\n processedText = processedText.replace(pattern, (match) => {\n const id = generateRedactionId();\n redactionMap[id] = match;\n // Create a trackable replacement like [REDACTED_SSN_abc123]\n return `[REDACTED_${replacement}_${id}]`;\n });\n }\n\n return processedText;\n}\n\ninterface ProcessHumanMessageConfig {\n rules: Record<string, RegExp>;\n redactionMap: RedactionMap;\n}\n\n/**\n * Process a single human message for PII detection and redaction\n */\nasync function processMessage(\n message: BaseMessage,\n config: ProcessHumanMessageConfig\n): Promise<BaseMessage> {\n /**\n * handle basic message types\n */\n if (\n HumanMessage.isInstance(message) ||\n ToolMessage.isInstance(message) ||\n SystemMessage.isInstance(message)\n ) {\n const content = message.content as string;\n const processedContent = await applyPIIRules(\n content,\n config.rules,\n config.redactionMap\n );\n\n if (processedContent !== content) {\n const MessageConstructor = Object.getPrototypeOf(message).constructor;\n return new MessageConstructor({\n ...message,\n content: processedContent,\n });\n }\n\n return message;\n }\n\n /**\n * Handle AI messages\n */\n if (AIMessage.isInstance(message)) {\n const content =\n typeof message.content === \"string\"\n ? message.content\n : JSON.stringify(message.content);\n const toolCalls = JSON.stringify(message.tool_calls);\n const processedContent = await applyPIIRules(\n content,\n config.rules,\n config.redactionMap\n );\n const processedToolCalls = await applyPIIRules(\n toolCalls,\n config.rules,\n config.redactionMap\n );\n\n if (processedContent !== content || processedToolCalls !== toolCalls) {\n return new AIMessage({\n ...message,\n content:\n typeof message.content === \"string\"\n ? processedContent\n : JSON.parse(processedContent),\n tool_calls: JSON.parse(processedToolCalls),\n });\n }\n\n return message;\n }\n\n throw new Error(`Unsupported message type: ${message.type}`);\n}\n\n/**\n * Restore original values from redacted text using the redaction map\n */\nfunction restoreRedactedValues(\n text: string,\n redactionMap: RedactionMap\n): string {\n let restoredText = text;\n\n // Pattern to match redacted values like [REDACTED_SSN_abc123]\n const redactionPattern = /\\[REDACTED_[A-Z_]+_(\\w+)\\]/g;\n\n restoredText = restoredText.replace(redactionPattern, (match, id) => {\n if (redactionMap[id]) {\n return redactionMap[id];\n }\n return match; // Keep original if no mapping found\n });\n\n return restoredText;\n}\n\n/**\n * Restore redacted values in a message (creates a new message object)\n */\nfunction restoreMessage(\n message: BaseMessage,\n redactionMap: RedactionMap\n): { message: BaseMessage; changed: boolean } {\n /**\n * handle basic message types\n */\n if (\n HumanMessage.isInstance(message) ||\n ToolMessage.isInstance(message) ||\n SystemMessage.isInstance(message)\n ) {\n const content = message.content as string;\n const restoredContent = restoreRedactedValues(content, redactionMap);\n if (restoredContent !== content) {\n const MessageConstructor = Object.getPrototypeOf(message).constructor;\n const newMessage = new MessageConstructor({\n ...message,\n content: restoredContent,\n });\n return { message: newMessage, changed: true };\n }\n return { message, changed: false };\n }\n\n /**\n * handle AI messages\n */\n if (AIMessage.isInstance(message)) {\n const content =\n typeof message.content === \"string\"\n ? message.content\n : JSON.stringify(message.content);\n const toolCalls = JSON.stringify(message.tool_calls);\n const processedContent = restoreRedactedValues(content, redactionMap);\n const processedToolCalls = restoreRedactedValues(toolCalls, redactionMap);\n if (processedContent !== content || processedToolCalls !== toolCalls) {\n return {\n message: new AIMessage({\n ...message,\n content:\n typeof message.content === \"string\"\n ? processedContent\n : JSON.parse(processedContent),\n tool_calls: JSON.parse(processedToolCalls),\n }),\n changed: true,\n };\n }\n\n return { message, changed: false };\n }\n\n throw new Error(`Unsupported message type: ${message.type}`);\n}\n\n/**\n * Creates a middleware that detects and redacts personally identifiable information (PII)\n * from messages before they are sent to model providers, and restores original values\n * in model responses for tool execution.\n *\n * ## Mechanism\n *\n * The middleware intercepts agent execution at two points:\n *\n * ### Request Phase (`wrapModelCall`)\n * - Applies regex-based pattern matching to all message content (HumanMessage, ToolMessage, SystemMessage, AIMessage)\n * - Processes both message text and AIMessage tool call arguments\n * - Each matched pattern generates:\n * - Unique identifier: `generateRedactionId()` → `\"abc123\"`\n * - Redaction marker: `[REDACTED_{RULE_NAME}_{ID}]` → `\"[REDACTED_SSN_abc123]\"`\n * - Redaction map entry: `{ \"abc123\": \"123-45-6789\" }`\n * - Returns modified request with redacted message content\n *\n * ### Response Phase (`afterModel`)\n * - Scans AIMessage responses for redaction markers matching pattern: `/\\[REDACTED_[A-Z_]+_(\\w+)\\]/g`\n * - Replaces markers with original values from redaction map\n * - Handles both standard responses and structured output (via tool calls or JSON content)\n * - For structured output, restores values in both the tool call arguments and the `structuredResponse` state field\n * - Returns new message instances via RemoveMessage/AIMessage to update state\n *\n * ## Data Flow\n *\n * ```\n * User Input: \"My SSN is 123-45-6789\"\n * ↓ [beforeModel]\n * Model Request: \"My SSN is [REDACTED_SSN_abc123]\"\n * ↓ [model invocation]\n * Model Response: tool_call({ \"ssn\": \"[REDACTED_SSN_abc123]\" })\n * ↓ [afterModel]\n * Tool Execution: tool({ \"ssn\": \"123-45-6789\" })\n * ```\n *\n * ## Limitations\n *\n * This middleware provides model provider isolation only. PII may still be present in:\n * - LangGraph state checkpoints (memory, databases)\n * - Network traffic between client and application server\n * - Application logs and trace data\n * - Tool execution arguments and responses\n * - Final agent output\n *\n * For comprehensive PII protection, implement additional controls at the application,\n * network, and storage layers.\n *\n * @param options - Configuration options\n * @param options.rules - Record of detection rules mapping rule names to regex patterns.\n * Rule names are normalized to uppercase and used in redaction markers.\n * Patterns must use the global flag (`/pattern/g`) to match all occurrences.\n *\n * @returns Middleware instance for use with `createAgent`\n *\n * @example Basic usage with custom rules\n * ```typescript\n * import { piiRedactionMiddleware } from \"langchain\";\n * import { createAgent } from \"langchain\";\n * import { tool } from \"@langchain/core/tools\";\n * import { z } from \"zod/v3\";\n *\n * const PII_RULES = {\n * ssn: /\\b\\d{3}-?\\d{2}-?\\d{4}\\b/g,\n * email: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g,\n * phone: /\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b/g,\n * };\n *\n * const lookupUser = tool(async ({ ssn }) => {\n * // Receives original value: \"123-45-6789\"\n * return { name: \"John Doe\", account: \"active\" };\n * }, {\n * name: \"lookup_user\",\n * description: \"Look up user by SSN\",\n * schema: z.object({ ssn: z.string() })\n * });\n *\n * const agent = createAgent({\n * model: new ChatOpenAI({ model: \"gpt-4\" }),\n * tools: [lookupUser],\n * middleware: [piiRedactionMiddleware({ rules: PII_RULES })]\n * });\n *\n * const result = await agent.invoke({\n * messages: [new HumanMessage(\"Look up SSN 123-45-6789\")]\n * });\n * // Model request: \"Look up SSN [REDACTED_SSN_abc123]\"\n * // Model response: tool_call({ \"ssn\": \"[REDACTED_SSN_abc123]\" })\n * // Tool receives: { \"ssn\": \"123-45-6789\" }\n * ```\n *\n * @example Runtime rule configuration via context\n * ```typescript\n * const agent = createAgent({\n * model: new ChatOpenAI({ model: \"gpt-4\" }),\n * tools: [someTool],\n * middleware: [piiRedactionMiddleware()]\n * });\n *\n * // Configure rules at runtime via middleware context\n * const result = await agent.invoke(\n * { messages: [new HumanMessage(\"...\")] },\n * {\n * configurable: {\n * PIIRedactionMiddleware: {\n * rules: {\n * ssn: /\\b\\d{3}-?\\d{2}-?\\d{4}\\b/g,\n * email: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g,\n * }\n * }\n * }\n * }\n * );\n * ```\n *\n * @example Custom rule patterns\n * ```typescript\n * const customRules = {\n * employee_id: /EMP-\\d{6}/g,\n * api_key: /sk-[a-zA-Z0-9]{32}/g,\n * credit_card: /\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b/g,\n * };\n *\n * const middleware = piiRedactionMiddleware({ rules: customRules });\n * // Generates markers like: [REDACTED_EMPLOYEE_ID_xyz789]\n * ```\n *\n * @deprecated\n */\nexport function piiRedactionMiddleware(\n options: PIIRedactionMiddlewareConfig = {}\n): ReturnType<typeof createMiddleware> {\n const redactionMap: RedactionMap = {};\n\n console.warn(\n \"DEPRECATED: piiRedactionMiddleware is deprecated. Please use piiMiddleware instead, go to https://docs.langchain.com/oss/javascript/langchain/middleware/built-in#pii-detection for more information.\"\n );\n\n return createMiddleware({\n name: \"PIIRedactionMiddleware\",\n contextSchema,\n wrapModelCall: async (request, handler) => {\n /**\n * Merge options with context, following bigTool.ts pattern\n */\n const rules = request.runtime.context.rules ?? options.rules ?? {};\n\n /**\n * If no rules are provided, skip processing\n */\n if (Object.keys(rules).length === 0) {\n return handler(request);\n }\n\n const processedMessages = await Promise.all(\n request.state.messages.map((message: BaseMessage) =>\n processMessage(message, {\n rules,\n redactionMap,\n })\n )\n );\n\n return handler({\n ...request,\n messages: processedMessages,\n });\n },\n afterModel: async (state) => {\n /**\n * If no redactions were made, skip processing\n */\n if (Object.keys(redactionMap).length === 0) {\n return;\n }\n\n const lastMessage = state.messages.at(-1);\n if (!AIMessage.isInstance(lastMessage)) {\n return;\n }\n\n /**\n * In cases where we do structured output via tool calls, we also have to look at the second last message\n * as we add a custom last message to the messages array.\n */\n const secondLastMessage = state.messages.at(-2);\n\n const { message: restoredLastMessage, changed } = restoreMessage(\n lastMessage,\n redactionMap\n );\n\n if (!changed) {\n return;\n }\n\n /**\n * Identify if the last message is a structured response and restore the values if so\n */\n let structuredResponse: Record<string, unknown> | undefined;\n if (\n AIMessage.isInstance(lastMessage) &&\n lastMessage?.tool_calls?.length === 0 &&\n typeof lastMessage.content === \"string\" &&\n lastMessage.content.startsWith(\"{\") &&\n lastMessage.content.endsWith(\"}\")\n ) {\n try {\n structuredResponse = JSON.parse(\n restoreRedactedValues(lastMessage.content, redactionMap)\n );\n } catch {\n // ignore\n }\n }\n\n /**\n * Check if the second last message is a structured response tool call\n */\n const isStructuredResponseToolCall =\n AIMessage.isInstance(secondLastMessage) &&\n secondLastMessage?.tool_calls?.length !== 0 &&\n secondLastMessage?.tool_calls?.some((call) =>\n call.name.startsWith(\"extract-\")\n );\n if (isStructuredResponseToolCall) {\n const {\n message: restoredSecondLastMessage,\n changed: changedSecondLastMessage,\n } = restoreMessage(secondLastMessage, redactionMap);\n const structuredResponseRedacted = secondLastMessage.tool_calls?.find(\n (call) => call.name.startsWith(\"extract-\")\n )?.args;\n const structuredResponse = structuredResponseRedacted\n ? JSON.parse(\n restoreRedactedValues(\n JSON.stringify(structuredResponseRedacted),\n redactionMap\n )\n )\n : undefined;\n if (changed || changedSecondLastMessage) {\n return {\n ...state,\n ...(structuredResponse ? { structuredResponse } : {}),\n messages: [\n new RemoveMessage({ id: secondLastMessage.id as string }),\n new RemoveMessage({ id: lastMessage.id as string }),\n restoredSecondLastMessage,\n restoredLastMessage,\n ],\n };\n }\n }\n\n return {\n ...state,\n ...(structuredResponse ? { structuredResponse } : {}),\n messages: [\n new RemoveMessage({ id: lastMessage.id as string }),\n restoredLastMessage,\n ],\n };\n },\n });\n}\n"],"mappings":";;;;;;;;;AAqBA,MAAM,gBAAgBA,SAAE,OAAO,EAK7B,OAAOA,SACJ,OACCA,SAAE,QAAQ,EACVA,SAAE,WAAW,OAAO,CAAC,SAAS,0CAA0C,CACzE,CACA,UAAU,EACd,CAAC;;;;AAYF,SAAS,sBAA8B;AACrC,QAAO,KAAK,QAAQ,CAAC,SAAS,GAAG,CAAC,UAAU,GAAG,GAAG;;;;;AAMpD,SAAS,cACP,MACA,OACA,cACQ;CACR,IAAI,gBAAgB;AAEpB,MAAK,MAAM,CAAC,MAAM,YAAY,OAAO,QAAQ,MAAM,EAAE;EACnD,MAAM,cAAc,KAAK,aAAa,CAAC,QAAQ,mBAAmB,GAAG;AACrE,kBAAgB,cAAc,QAAQ,UAAU,UAAU;GACxD,MAAM,KAAK,qBAAqB;AAChC,gBAAa,MAAM;AAEnB,UAAO,aAAa,YAAY,GAAG,GAAG;IACtC;;AAGJ,QAAO;;;;;AAWT,eAAe,eACb,SACA,QACsB;;;;AAItB,KACEC,sCAAa,WAAW,QAAQ,IAChCC,qCAAY,WAAW,QAAQ,IAC/BC,uCAAc,WAAW,QAAQ,EACjC;EACA,MAAM,UAAU,QAAQ;EACxB,MAAM,mBAAmB,MAAM,cAC7B,SACA,OAAO,OACP,OAAO,aACR;AAED,MAAI,qBAAqB,SAAS;GAChC,MAAM,qBAAqB,OAAO,eAAe,QAAQ,CAAC;AAC1D,UAAO,IAAI,mBAAmB;IAC5B,GAAG;IACH,SAAS;IACV,CAAC;;AAGJ,SAAO;;;;;AAMT,KAAIC,mCAAU,WAAW,QAAQ,EAAE;EACjC,MAAM,UACJ,OAAO,QAAQ,YAAY,WACvB,QAAQ,UACR,KAAK,UAAU,QAAQ,QAAQ;EACrC,MAAM,YAAY,KAAK,UAAU,QAAQ,WAAW;EACpD,MAAM,mBAAmB,MAAM,cAC7B,SACA,OAAO,OACP,OAAO,aACR;EACD,MAAM,qBAAqB,MAAM,cAC/B,WACA,OAAO,OACP,OAAO,aACR;AAED,MAAI,qBAAqB,WAAW,uBAAuB,UACzD,QAAO,IAAIA,mCAAU;GACnB,GAAG;GACH,SACE,OAAO,QAAQ,YAAY,WACvB,mBACA,KAAK,MAAM,iBAAiB;GAClC,YAAY,KAAK,MAAM,mBAAmB;GAC3C,CAAC;AAGJ,SAAO;;AAGT,OAAM,IAAI,MAAM,6BAA6B,QAAQ,OAAO;;;;;AAM9D,SAAS,sBACP,MACA,cACQ;CACR,IAAI,eAAe;AAKnB,gBAAe,aAAa,QAFH,gCAE8B,OAAO,OAAO;AACnE,MAAI,aAAa,IACf,QAAO,aAAa;AAEtB,SAAO;GACP;AAEF,QAAO;;;;;AAMT,SAAS,eACP,SACA,cAC4C;;;;AAI5C,KACEH,sCAAa,WAAW,QAAQ,IAChCC,qCAAY,WAAW,QAAQ,IAC/BC,uCAAc,WAAW,QAAQ,EACjC;EACA,MAAM,UAAU,QAAQ;EACxB,MAAM,kBAAkB,sBAAsB,SAAS,aAAa;AACpE,MAAI,oBAAoB,SAAS;GAC/B,MAAM,qBAAqB,OAAO,eAAe,QAAQ,CAAC;AAK1D,UAAO;IAAE,SAJU,IAAI,mBAAmB;KACxC,GAAG;KACH,SAAS;KACV,CAAC;IAC4B,SAAS;IAAM;;AAE/C,SAAO;GAAE;GAAS,SAAS;GAAO;;;;;AAMpC,KAAIC,mCAAU,WAAW,QAAQ,EAAE;EACjC,MAAM,UACJ,OAAO,QAAQ,YAAY,WACvB,QAAQ,UACR,KAAK,UAAU,QAAQ,QAAQ;EACrC,MAAM,YAAY,KAAK,UAAU,QAAQ,WAAW;EACpD,MAAM,mBAAmB,sBAAsB,SAAS,aAAa;EACrE,MAAM,qBAAqB,sBAAsB,WAAW,aAAa;AACzE,MAAI,qBAAqB,WAAW,uBAAuB,UACzD,QAAO;GACL,SAAS,IAAIA,mCAAU;IACrB,GAAG;IACH,SACE,OAAO,QAAQ,YAAY,WACvB,mBACA,KAAK,MAAM,iBAAiB;IAClC,YAAY,KAAK,MAAM,mBAAmB;IAC3C,CAAC;GACF,SAAS;GACV;AAGH,SAAO;GAAE;GAAS,SAAS;GAAO;;AAGpC,OAAM,IAAI,MAAM,6BAA6B,QAAQ,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAqI9D,SAAgB,uBACd,UAAwC,EAAE,EACL;CACrC,MAAM,eAA6B,EAAE;AAErC,SAAQ,KACN,wMACD;AAED,QAAOC,oCAAiB;EACtB,MAAM;EACN;EACA,eAAe,OAAO,SAAS,YAAY;;;;GAIzC,MAAM,QAAQ,QAAQ,QAAQ,QAAQ,SAAS,QAAQ,SAAS,EAAE;;;;AAKlE,OAAI,OAAO,KAAK,MAAM,CAAC,WAAW,EAChC,QAAO,QAAQ,QAAQ;GAGzB,MAAM,oBAAoB,MAAM,QAAQ,IACtC,QAAQ,MAAM,SAAS,KAAK,YAC1B,eAAe,SAAS;IACtB;IACA;IACD,CAAC,CACH,CACF;AAED,UAAO,QAAQ;IACb,GAAG;IACH,UAAU;IACX,CAAC;;EAEJ,YAAY,OAAO,UAAU;;;;AAI3B,OAAI,OAAO,KAAK,aAAa,CAAC,WAAW,EACvC;GAGF,MAAM,cAAc,MAAM,SAAS,GAAG,GAAG;AACzC,OAAI,CAACD,mCAAU,WAAW,YAAY,CACpC;;;;;GAOF,MAAM,oBAAoB,MAAM,SAAS,GAAG,GAAG;GAE/C,MAAM,EAAE,SAAS,qBAAqB,YAAY,eAChD,aACA,aACD;AAED,OAAI,CAAC,QACH;;;;GAMF,IAAI;AACJ,OACEA,mCAAU,WAAW,YAAY,IACjC,aAAa,YAAY,WAAW,KACpC,OAAO,YAAY,YAAY,YAC/B,YAAY,QAAQ,WAAW,IAAI,IACnC,YAAY,QAAQ,SAAS,IAAI,CAEjC,KAAI;AACF,yBAAqB,KAAK,MACxB,sBAAsB,YAAY,SAAS,aAAa,CACzD;WACK;AAcV,OALEA,mCAAU,WAAW,kBAAkB,IACvC,mBAAmB,YAAY,WAAW,KAC1C,mBAAmB,YAAY,MAAM,SACnC,KAAK,KAAK,WAAW,WAAW,CACjC,EAC+B;IAChC,MAAM,EACJ,SAAS,2BACT,SAAS,6BACP,eAAe,mBAAmB,aAAa;IACnD,MAAM,6BAA6B,kBAAkB,YAAY,MAC9D,SAAS,KAAK,KAAK,WAAW,WAAW,CAC3C,EAAE;IACH,MAAM,qBAAqB,6BACvB,KAAK,MACH,sBACE,KAAK,UAAU,2BAA2B,EAC1C,aACD,CACF,GACD;AACJ,QAAI,WAAW,yBACb,QAAO;KACL,GAAG;KACH,GAAI,qBAAqB,EAAE,oBAAoB,GAAG,EAAE;KACpD,UAAU;MACR,IAAIE,uCAAc,EAAE,IAAI,kBAAkB,IAAc,CAAC;MACzD,IAAIA,uCAAc,EAAE,IAAI,YAAY,IAAc,CAAC;MACnD;MACA;MACD;KACF;;AAIL,UAAO;IACL,GAAG;IACH,GAAI,qBAAqB,EAAE,oBAAoB,GAAG,EAAE;IACpD,UAAU,CACR,IAAIA,uCAAc,EAAE,IAAI,YAAY,IAAc,CAAC,EACnD,oBACD;IACF;;EAEJ,CAAC"}
|
|
1
|
+
{"version":3,"file":"piiRedaction.cjs","names":["z","HumanMessage","ToolMessage","SystemMessage","AIMessage","createMiddleware","RemoveMessage"],"sources":["../../../src/agents/middleware/piiRedaction.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport {\n BaseMessage,\n AIMessage,\n HumanMessage,\n ToolMessage,\n RemoveMessage,\n SystemMessage,\n} from \"@langchain/core/messages\";\nimport type { InferInteropZodInput } from \"@langchain/core/utils/types\";\n\nimport { createMiddleware } from \"../middleware.js\";\n\n/**\n * Type for the redaction map that stores original values by ID\n */\ntype RedactionMap = Record<string, string>;\n\n/**\n * Configuration schema for the Input Guardrails middleware\n */\nconst contextSchema = z.object({\n /**\n * A record of PII detection rules to apply\n * @default DEFAULT_PII_RULES (with enabled rules only)\n */\n rules: z\n .record(\n z.string(),\n z.instanceof(RegExp).describe(\"Regular expression pattern to match PII\")\n )\n .optional(),\n});\n\n/**\n * @deprecated\n */\nexport type PIIRedactionMiddlewareConfig = InferInteropZodInput<\n typeof contextSchema\n>;\n\n/**\n * Generate a unique ID for a redaction\n */\nfunction generateRedactionId(): string {\n return Math.random().toString(36).substring(2, 11);\n}\n\n/**\n * Apply PII detection rules to text with ID tracking\n */\nfunction applyPIIRules(\n text: string,\n rules: Record<string, RegExp>,\n redactionMap: RedactionMap\n): string {\n let processedText = text;\n\n for (const [name, pattern] of Object.entries(rules)) {\n const replacement = name.toUpperCase().replace(/[^a-zA-Z0-9_-]/g, \"\");\n processedText = processedText.replace(pattern, (match) => {\n const id = generateRedactionId();\n redactionMap[id] = match;\n // Create a trackable replacement like [REDACTED_SSN_abc123]\n return `[REDACTED_${replacement}_${id}]`;\n });\n }\n\n return processedText;\n}\n\ninterface ProcessHumanMessageConfig {\n rules: Record<string, RegExp>;\n redactionMap: RedactionMap;\n}\n\n/**\n * Process a single human message for PII detection and redaction\n */\nasync function processMessage(\n message: BaseMessage,\n config: ProcessHumanMessageConfig\n): Promise<BaseMessage> {\n /**\n * handle basic message types\n */\n if (\n HumanMessage.isInstance(message) ||\n ToolMessage.isInstance(message) ||\n SystemMessage.isInstance(message)\n ) {\n const content = message.content as string;\n const processedContent = await applyPIIRules(\n content,\n config.rules,\n config.redactionMap\n );\n\n if (processedContent !== content) {\n const MessageConstructor = Object.getPrototypeOf(message).constructor;\n return new MessageConstructor({\n ...message,\n content: processedContent,\n });\n }\n\n return message;\n }\n\n /**\n * Handle AI messages\n */\n if (AIMessage.isInstance(message)) {\n const content =\n typeof message.content === \"string\"\n ? message.content\n : JSON.stringify(message.content);\n const toolCalls = JSON.stringify(message.tool_calls);\n const processedContent = await applyPIIRules(\n content,\n config.rules,\n config.redactionMap\n );\n const processedToolCalls = await applyPIIRules(\n toolCalls,\n config.rules,\n config.redactionMap\n );\n\n if (processedContent !== content || processedToolCalls !== toolCalls) {\n return new AIMessage({\n ...message,\n content:\n typeof message.content === \"string\"\n ? processedContent\n : JSON.parse(processedContent),\n tool_calls: JSON.parse(processedToolCalls),\n });\n }\n\n return message;\n }\n\n throw new Error(`Unsupported message type: ${message.type}`);\n}\n\n/**\n * Restore original values from redacted text using the redaction map\n */\nfunction restoreRedactedValues(\n text: string,\n redactionMap: RedactionMap\n): string {\n let restoredText = text;\n\n // Pattern to match redacted values like [REDACTED_SSN_abc123]\n const redactionPattern = /\\[REDACTED_[A-Z_]+_(\\w+)\\]/g;\n\n restoredText = restoredText.replace(redactionPattern, (match, id) => {\n if (redactionMap[id]) {\n return redactionMap[id];\n }\n return match; // Keep original if no mapping found\n });\n\n return restoredText;\n}\n\n/**\n * Restore redacted values in a message (creates a new message object)\n */\nfunction restoreMessage(\n message: BaseMessage,\n redactionMap: RedactionMap\n): { message: BaseMessage; changed: boolean } {\n /**\n * handle basic message types\n */\n if (\n HumanMessage.isInstance(message) ||\n ToolMessage.isInstance(message) ||\n SystemMessage.isInstance(message)\n ) {\n const content = message.content as string;\n const restoredContent = restoreRedactedValues(content, redactionMap);\n if (restoredContent !== content) {\n const MessageConstructor = Object.getPrototypeOf(message).constructor;\n const newMessage = new MessageConstructor({\n ...message,\n content: restoredContent,\n });\n return { message: newMessage, changed: true };\n }\n return { message, changed: false };\n }\n\n /**\n * handle AI messages\n */\n if (AIMessage.isInstance(message)) {\n const content =\n typeof message.content === \"string\"\n ? message.content\n : JSON.stringify(message.content);\n const toolCalls = JSON.stringify(message.tool_calls);\n const processedContent = restoreRedactedValues(content, redactionMap);\n const processedToolCalls = restoreRedactedValues(toolCalls, redactionMap);\n if (processedContent !== content || processedToolCalls !== toolCalls) {\n return {\n message: new AIMessage({\n ...message,\n content:\n typeof message.content === \"string\"\n ? processedContent\n : JSON.parse(processedContent),\n tool_calls: JSON.parse(processedToolCalls),\n }),\n changed: true,\n };\n }\n\n return { message, changed: false };\n }\n\n throw new Error(`Unsupported message type: ${message.type}`);\n}\n\n/**\n * Creates a middleware that detects and redacts personally identifiable information (PII)\n * from messages before they are sent to model providers, and restores original values\n * in model responses for tool execution.\n *\n * ## Mechanism\n *\n * The middleware intercepts agent execution at two points:\n *\n * ### Request Phase (`wrapModelCall`)\n * - Applies regex-based pattern matching to all message content (HumanMessage, ToolMessage, SystemMessage, AIMessage)\n * - Processes both message text and AIMessage tool call arguments\n * - Each matched pattern generates:\n * - Unique identifier: `generateRedactionId()` → `\"abc123\"`\n * - Redaction marker: `[REDACTED_{RULE_NAME}_{ID}]` → `\"[REDACTED_SSN_abc123]\"`\n * - Redaction map entry: `{ \"abc123\": \"123-45-6789\" }`\n * - Returns modified request with redacted message content\n *\n * ### Response Phase (`afterModel`)\n * - Scans AIMessage responses for redaction markers matching pattern: `/\\[REDACTED_[A-Z_]+_(\\w+)\\]/g`\n * - Replaces markers with original values from redaction map\n * - Handles both standard responses and structured output (via tool calls or JSON content)\n * - For structured output, restores values in both the tool call arguments and the `structuredResponse` state field\n * - Returns new message instances via RemoveMessage/AIMessage to update state\n *\n * ## Data Flow\n *\n * ```\n * User Input: \"My SSN is 123-45-6789\"\n * ↓ [beforeModel]\n * Model Request: \"My SSN is [REDACTED_SSN_abc123]\"\n * ↓ [model invocation]\n * Model Response: tool_call({ \"ssn\": \"[REDACTED_SSN_abc123]\" })\n * ↓ [afterModel]\n * Tool Execution: tool({ \"ssn\": \"123-45-6789\" })\n * ```\n *\n * ## Limitations\n *\n * This middleware provides model provider isolation only. PII may still be present in:\n * - LangGraph state checkpoints (memory, databases)\n * - Network traffic between client and application server\n * - Application logs and trace data\n * - Tool execution arguments and responses\n * - Final agent output\n *\n * For comprehensive PII protection, implement additional controls at the application,\n * network, and storage layers.\n *\n * @param options - Configuration options\n * @param options.rules - Record of detection rules mapping rule names to regex patterns.\n * Rule names are normalized to uppercase and used in redaction markers.\n * Patterns must use the global flag (`/pattern/g`) to match all occurrences.\n *\n * @returns Middleware instance for use with `createAgent`\n *\n * @example Basic usage with custom rules\n * ```typescript\n * import { piiRedactionMiddleware } from \"langchain\";\n * import { createAgent } from \"langchain\";\n * import { tool } from \"@langchain/core/tools\";\n * import { z } from \"zod/v3\";\n *\n * const PII_RULES = {\n * ssn: /\\b\\d{3}-?\\d{2}-?\\d{4}\\b/g,\n * email: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g,\n * phone: /\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b/g,\n * };\n *\n * const lookupUser = tool(async ({ ssn }) => {\n * // Receives original value: \"123-45-6789\"\n * return { name: \"John Doe\", account: \"active\" };\n * }, {\n * name: \"lookup_user\",\n * description: \"Look up user by SSN\",\n * schema: z.object({ ssn: z.string() })\n * });\n *\n * const agent = createAgent({\n * model: new ChatOpenAI({ model: \"gpt-4\" }),\n * tools: [lookupUser],\n * middleware: [piiRedactionMiddleware({ rules: PII_RULES })]\n * });\n *\n * const result = await agent.invoke({\n * messages: [new HumanMessage(\"Look up SSN 123-45-6789\")]\n * });\n * // Model request: \"Look up SSN [REDACTED_SSN_abc123]\"\n * // Model response: tool_call({ \"ssn\": \"[REDACTED_SSN_abc123]\" })\n * // Tool receives: { \"ssn\": \"123-45-6789\" }\n * ```\n *\n * @example Runtime rule configuration via context\n * ```typescript\n * const agent = createAgent({\n * model: new ChatOpenAI({ model: \"gpt-4\" }),\n * tools: [someTool],\n * middleware: [piiRedactionMiddleware()]\n * });\n *\n * // Configure rules at runtime via middleware context\n * const result = await agent.invoke(\n * { messages: [new HumanMessage(\"...\")] },\n * {\n * configurable: {\n * PIIRedactionMiddleware: {\n * rules: {\n * ssn: /\\b\\d{3}-?\\d{2}-?\\d{4}\\b/g,\n * email: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g,\n * }\n * }\n * }\n * }\n * );\n * ```\n *\n * @example Custom rule patterns\n * ```typescript\n * const customRules = {\n * employee_id: /EMP-\\d{6}/g,\n * api_key: /sk-[a-zA-Z0-9]{32}/g,\n * credit_card: /\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b/g,\n * };\n *\n * const middleware = piiRedactionMiddleware({ rules: customRules });\n * // Generates markers like: [REDACTED_EMPLOYEE_ID_xyz789]\n * ```\n *\n * @deprecated\n */\nexport function piiRedactionMiddleware(\n options: PIIRedactionMiddlewareConfig = {}\n) {\n const redactionMap: RedactionMap = {};\n\n console.warn(\n \"DEPRECATED: piiRedactionMiddleware is deprecated. Please use piiMiddleware instead, go to https://docs.langchain.com/oss/javascript/langchain/middleware/built-in#pii-detection for more information.\"\n );\n\n return createMiddleware({\n name: \"PIIRedactionMiddleware\",\n contextSchema,\n wrapModelCall: async (request, handler) => {\n /**\n * Merge options with context, following bigTool.ts pattern\n */\n const rules = request.runtime.context.rules ?? options.rules ?? {};\n\n /**\n * If no rules are provided, skip processing\n */\n if (Object.keys(rules).length === 0) {\n return handler(request);\n }\n\n const processedMessages = await Promise.all(\n request.state.messages.map((message: BaseMessage) =>\n processMessage(message, {\n rules,\n redactionMap,\n })\n )\n );\n\n return handler({\n ...request,\n messages: processedMessages,\n });\n },\n afterModel: async (state) => {\n /**\n * If no redactions were made, skip processing\n */\n if (Object.keys(redactionMap).length === 0) {\n return;\n }\n\n const lastMessage = state.messages.at(-1);\n if (!AIMessage.isInstance(lastMessage)) {\n return;\n }\n\n /**\n * In cases where we do structured output via tool calls, we also have to look at the second last message\n * as we add a custom last message to the messages array.\n */\n const secondLastMessage = state.messages.at(-2);\n\n const { message: restoredLastMessage, changed } = restoreMessage(\n lastMessage,\n redactionMap\n );\n\n if (!changed) {\n return;\n }\n\n /**\n * Identify if the last message is a structured response and restore the values if so\n */\n let structuredResponse: Record<string, unknown> | undefined;\n if (\n AIMessage.isInstance(lastMessage) &&\n lastMessage?.tool_calls?.length === 0 &&\n typeof lastMessage.content === \"string\" &&\n lastMessage.content.startsWith(\"{\") &&\n lastMessage.content.endsWith(\"}\")\n ) {\n try {\n structuredResponse = JSON.parse(\n restoreRedactedValues(lastMessage.content, redactionMap)\n );\n } catch {\n // ignore\n }\n }\n\n /**\n * Check if the second last message is a structured response tool call\n */\n const isStructuredResponseToolCall =\n AIMessage.isInstance(secondLastMessage) &&\n secondLastMessage?.tool_calls?.length !== 0 &&\n secondLastMessage?.tool_calls?.some((call) =>\n call.name.startsWith(\"extract-\")\n );\n if (isStructuredResponseToolCall) {\n const {\n message: restoredSecondLastMessage,\n changed: changedSecondLastMessage,\n } = restoreMessage(secondLastMessage, redactionMap);\n const structuredResponseRedacted = secondLastMessage.tool_calls?.find(\n (call) => call.name.startsWith(\"extract-\")\n )?.args;\n const structuredResponse = structuredResponseRedacted\n ? JSON.parse(\n restoreRedactedValues(\n JSON.stringify(structuredResponseRedacted),\n redactionMap\n )\n )\n : undefined;\n if (changed || changedSecondLastMessage) {\n return {\n ...state,\n ...(structuredResponse ? { structuredResponse } : {}),\n messages: [\n new RemoveMessage({ id: secondLastMessage.id as string }),\n new RemoveMessage({ id: lastMessage.id as string }),\n restoredSecondLastMessage,\n restoredLastMessage,\n ],\n };\n }\n }\n\n return {\n ...state,\n ...(structuredResponse ? { structuredResponse } : {}),\n messages: [\n new RemoveMessage({ id: lastMessage.id as string }),\n restoredLastMessage,\n ],\n };\n },\n });\n}\n"],"mappings":"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"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AgentMiddleware } from "./types.cjs";
|
|
2
|
+
import * as _langchain_core_tools0 from "@langchain/core/tools";
|
|
2
3
|
import { InferInteropZodInput } from "@langchain/core/utils/types";
|
|
3
4
|
import { z } from "zod/v3";
|
|
4
5
|
|
|
@@ -151,7 +152,19 @@ type PIIRedactionMiddlewareConfig = InferInteropZodInput<typeof contextSchema>;
|
|
|
151
152
|
*
|
|
152
153
|
* @deprecated
|
|
153
154
|
*/
|
|
154
|
-
declare function piiRedactionMiddleware(options?: PIIRedactionMiddlewareConfig):
|
|
155
|
+
declare function piiRedactionMiddleware(options?: PIIRedactionMiddlewareConfig): AgentMiddleware<undefined, z.ZodObject<{
|
|
156
|
+
/**
|
|
157
|
+
* A record of PII detection rules to apply
|
|
158
|
+
* @default DEFAULT_PII_RULES (with enabled rules only)
|
|
159
|
+
*/
|
|
160
|
+
rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<RegExp, z.ZodTypeDef, RegExp>>>;
|
|
161
|
+
}, "strip", z.ZodTypeAny, {
|
|
162
|
+
rules?: Record<string, RegExp> | undefined;
|
|
163
|
+
}, {
|
|
164
|
+
rules?: Record<string, RegExp> | undefined;
|
|
165
|
+
}>, {
|
|
166
|
+
rules?: Record<string, RegExp> | undefined;
|
|
167
|
+
}, readonly (_langchain_core_tools0.ServerTool | _langchain_core_tools0.ClientTool)[]>;
|
|
155
168
|
//#endregion
|
|
156
169
|
export { PIIRedactionMiddlewareConfig, piiRedactionMiddleware };
|
|
157
170
|
//# sourceMappingURL=piiRedaction.d.cts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"piiRedaction.d.cts","names":[],"sources":["../../../src/agents/middleware/piiRedaction.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"piiRedaction.d.cts","names":[],"sources":["../../../src/agents/middleware/piiRedaction.ts"],"mappings":";;;;;;;;;cAqBM,aAAA,EAAa,CAAA,CAAA,SAAA;EAAb;;;;;;;;;;;;;KAgBM,4BAAA,GAA+B,oBAAA,QAClC,aAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AADT;;;;;AAgUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,sBAAA,CACd,OAAA,GAAS,4BAAA,8BAAiC,CAAA,CAAA,SAAA"}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AgentMiddleware } from "./types.js";
|
|
2
|
+
import * as _langchain_core_tools0 from "@langchain/core/tools";
|
|
2
3
|
import { InferInteropZodInput } from "@langchain/core/utils/types";
|
|
3
4
|
import { z } from "zod/v3";
|
|
4
5
|
|
|
@@ -151,7 +152,19 @@ type PIIRedactionMiddlewareConfig = InferInteropZodInput<typeof contextSchema>;
|
|
|
151
152
|
*
|
|
152
153
|
* @deprecated
|
|
153
154
|
*/
|
|
154
|
-
declare function piiRedactionMiddleware(options?: PIIRedactionMiddlewareConfig):
|
|
155
|
+
declare function piiRedactionMiddleware(options?: PIIRedactionMiddlewareConfig): AgentMiddleware<undefined, z.ZodObject<{
|
|
156
|
+
/**
|
|
157
|
+
* A record of PII detection rules to apply
|
|
158
|
+
* @default DEFAULT_PII_RULES (with enabled rules only)
|
|
159
|
+
*/
|
|
160
|
+
rules: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodType<RegExp, z.ZodTypeDef, RegExp>>>;
|
|
161
|
+
}, "strip", z.ZodTypeAny, {
|
|
162
|
+
rules?: Record<string, RegExp> | undefined;
|
|
163
|
+
}, {
|
|
164
|
+
rules?: Record<string, RegExp> | undefined;
|
|
165
|
+
}>, {
|
|
166
|
+
rules?: Record<string, RegExp> | undefined;
|
|
167
|
+
}, readonly (_langchain_core_tools0.ServerTool | _langchain_core_tools0.ClientTool)[]>;
|
|
155
168
|
//#endregion
|
|
156
169
|
export { PIIRedactionMiddlewareConfig, piiRedactionMiddleware };
|
|
157
170
|
//# sourceMappingURL=piiRedaction.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"piiRedaction.d.ts","names":[],"sources":["../../../src/agents/middleware/piiRedaction.ts"],"mappings":"
|
|
1
|
+
{"version":3,"file":"piiRedaction.d.ts","names":[],"sources":["../../../src/agents/middleware/piiRedaction.ts"],"mappings":";;;;;;;;;cAqBM,aAAA,EAAa,CAAA,CAAA,SAAA;EAAb;;;;;;;;;;;;;KAgBM,4BAAA,GAA+B,oBAAA,QAClC,aAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AADT;;;;;AAgUA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAAgB,sBAAA,CACd,OAAA,GAAS,4BAAA,8BAAiC,CAAA,CAAA,SAAA"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"piiRedaction.js","names":[],"sources":["../../../src/agents/middleware/piiRedaction.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport {\n BaseMessage,\n AIMessage,\n HumanMessage,\n ToolMessage,\n RemoveMessage,\n SystemMessage,\n} from \"@langchain/core/messages\";\nimport type { InferInteropZodInput } from \"@langchain/core/utils/types\";\n\nimport { createMiddleware } from \"../middleware.js\";\n\n/**\n * Type for the redaction map that stores original values by ID\n */\ntype RedactionMap = Record<string, string>;\n\n/**\n * Configuration schema for the Input Guardrails middleware\n */\nconst contextSchema = z.object({\n /**\n * A record of PII detection rules to apply\n * @default DEFAULT_PII_RULES (with enabled rules only)\n */\n rules: z\n .record(\n z.string(),\n z.instanceof(RegExp).describe(\"Regular expression pattern to match PII\")\n )\n .optional(),\n});\n\n/**\n * @deprecated\n */\nexport type PIIRedactionMiddlewareConfig = InferInteropZodInput<\n typeof contextSchema\n>;\n\n/**\n * Generate a unique ID for a redaction\n */\nfunction generateRedactionId(): string {\n return Math.random().toString(36).substring(2, 11);\n}\n\n/**\n * Apply PII detection rules to text with ID tracking\n */\nfunction applyPIIRules(\n text: string,\n rules: Record<string, RegExp>,\n redactionMap: RedactionMap\n): string {\n let processedText = text;\n\n for (const [name, pattern] of Object.entries(rules)) {\n const replacement = name.toUpperCase().replace(/[^a-zA-Z0-9_-]/g, \"\");\n processedText = processedText.replace(pattern, (match) => {\n const id = generateRedactionId();\n redactionMap[id] = match;\n // Create a trackable replacement like [REDACTED_SSN_abc123]\n return `[REDACTED_${replacement}_${id}]`;\n });\n }\n\n return processedText;\n}\n\ninterface ProcessHumanMessageConfig {\n rules: Record<string, RegExp>;\n redactionMap: RedactionMap;\n}\n\n/**\n * Process a single human message for PII detection and redaction\n */\nasync function processMessage(\n message: BaseMessage,\n config: ProcessHumanMessageConfig\n): Promise<BaseMessage> {\n /**\n * handle basic message types\n */\n if (\n HumanMessage.isInstance(message) ||\n ToolMessage.isInstance(message) ||\n SystemMessage.isInstance(message)\n ) {\n const content = message.content as string;\n const processedContent = await applyPIIRules(\n content,\n config.rules,\n config.redactionMap\n );\n\n if (processedContent !== content) {\n const MessageConstructor = Object.getPrototypeOf(message).constructor;\n return new MessageConstructor({\n ...message,\n content: processedContent,\n });\n }\n\n return message;\n }\n\n /**\n * Handle AI messages\n */\n if (AIMessage.isInstance(message)) {\n const content =\n typeof message.content === \"string\"\n ? message.content\n : JSON.stringify(message.content);\n const toolCalls = JSON.stringify(message.tool_calls);\n const processedContent = await applyPIIRules(\n content,\n config.rules,\n config.redactionMap\n );\n const processedToolCalls = await applyPIIRules(\n toolCalls,\n config.rules,\n config.redactionMap\n );\n\n if (processedContent !== content || processedToolCalls !== toolCalls) {\n return new AIMessage({\n ...message,\n content:\n typeof message.content === \"string\"\n ? processedContent\n : JSON.parse(processedContent),\n tool_calls: JSON.parse(processedToolCalls),\n });\n }\n\n return message;\n }\n\n throw new Error(`Unsupported message type: ${message.type}`);\n}\n\n/**\n * Restore original values from redacted text using the redaction map\n */\nfunction restoreRedactedValues(\n text: string,\n redactionMap: RedactionMap\n): string {\n let restoredText = text;\n\n // Pattern to match redacted values like [REDACTED_SSN_abc123]\n const redactionPattern = /\\[REDACTED_[A-Z_]+_(\\w+)\\]/g;\n\n restoredText = restoredText.replace(redactionPattern, (match, id) => {\n if (redactionMap[id]) {\n return redactionMap[id];\n }\n return match; // Keep original if no mapping found\n });\n\n return restoredText;\n}\n\n/**\n * Restore redacted values in a message (creates a new message object)\n */\nfunction restoreMessage(\n message: BaseMessage,\n redactionMap: RedactionMap\n): { message: BaseMessage; changed: boolean } {\n /**\n * handle basic message types\n */\n if (\n HumanMessage.isInstance(message) ||\n ToolMessage.isInstance(message) ||\n SystemMessage.isInstance(message)\n ) {\n const content = message.content as string;\n const restoredContent = restoreRedactedValues(content, redactionMap);\n if (restoredContent !== content) {\n const MessageConstructor = Object.getPrototypeOf(message).constructor;\n const newMessage = new MessageConstructor({\n ...message,\n content: restoredContent,\n });\n return { message: newMessage, changed: true };\n }\n return { message, changed: false };\n }\n\n /**\n * handle AI messages\n */\n if (AIMessage.isInstance(message)) {\n const content =\n typeof message.content === \"string\"\n ? message.content\n : JSON.stringify(message.content);\n const toolCalls = JSON.stringify(message.tool_calls);\n const processedContent = restoreRedactedValues(content, redactionMap);\n const processedToolCalls = restoreRedactedValues(toolCalls, redactionMap);\n if (processedContent !== content || processedToolCalls !== toolCalls) {\n return {\n message: new AIMessage({\n ...message,\n content:\n typeof message.content === \"string\"\n ? processedContent\n : JSON.parse(processedContent),\n tool_calls: JSON.parse(processedToolCalls),\n }),\n changed: true,\n };\n }\n\n return { message, changed: false };\n }\n\n throw new Error(`Unsupported message type: ${message.type}`);\n}\n\n/**\n * Creates a middleware that detects and redacts personally identifiable information (PII)\n * from messages before they are sent to model providers, and restores original values\n * in model responses for tool execution.\n *\n * ## Mechanism\n *\n * The middleware intercepts agent execution at two points:\n *\n * ### Request Phase (`wrapModelCall`)\n * - Applies regex-based pattern matching to all message content (HumanMessage, ToolMessage, SystemMessage, AIMessage)\n * - Processes both message text and AIMessage tool call arguments\n * - Each matched pattern generates:\n * - Unique identifier: `generateRedactionId()` → `\"abc123\"`\n * - Redaction marker: `[REDACTED_{RULE_NAME}_{ID}]` → `\"[REDACTED_SSN_abc123]\"`\n * - Redaction map entry: `{ \"abc123\": \"123-45-6789\" }`\n * - Returns modified request with redacted message content\n *\n * ### Response Phase (`afterModel`)\n * - Scans AIMessage responses for redaction markers matching pattern: `/\\[REDACTED_[A-Z_]+_(\\w+)\\]/g`\n * - Replaces markers with original values from redaction map\n * - Handles both standard responses and structured output (via tool calls or JSON content)\n * - For structured output, restores values in both the tool call arguments and the `structuredResponse` state field\n * - Returns new message instances via RemoveMessage/AIMessage to update state\n *\n * ## Data Flow\n *\n * ```\n * User Input: \"My SSN is 123-45-6789\"\n * ↓ [beforeModel]\n * Model Request: \"My SSN is [REDACTED_SSN_abc123]\"\n * ↓ [model invocation]\n * Model Response: tool_call({ \"ssn\": \"[REDACTED_SSN_abc123]\" })\n * ↓ [afterModel]\n * Tool Execution: tool({ \"ssn\": \"123-45-6789\" })\n * ```\n *\n * ## Limitations\n *\n * This middleware provides model provider isolation only. PII may still be present in:\n * - LangGraph state checkpoints (memory, databases)\n * - Network traffic between client and application server\n * - Application logs and trace data\n * - Tool execution arguments and responses\n * - Final agent output\n *\n * For comprehensive PII protection, implement additional controls at the application,\n * network, and storage layers.\n *\n * @param options - Configuration options\n * @param options.rules - Record of detection rules mapping rule names to regex patterns.\n * Rule names are normalized to uppercase and used in redaction markers.\n * Patterns must use the global flag (`/pattern/g`) to match all occurrences.\n *\n * @returns Middleware instance for use with `createAgent`\n *\n * @example Basic usage with custom rules\n * ```typescript\n * import { piiRedactionMiddleware } from \"langchain\";\n * import { createAgent } from \"langchain\";\n * import { tool } from \"@langchain/core/tools\";\n * import { z } from \"zod/v3\";\n *\n * const PII_RULES = {\n * ssn: /\\b\\d{3}-?\\d{2}-?\\d{4}\\b/g,\n * email: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g,\n * phone: /\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b/g,\n * };\n *\n * const lookupUser = tool(async ({ ssn }) => {\n * // Receives original value: \"123-45-6789\"\n * return { name: \"John Doe\", account: \"active\" };\n * }, {\n * name: \"lookup_user\",\n * description: \"Look up user by SSN\",\n * schema: z.object({ ssn: z.string() })\n * });\n *\n * const agent = createAgent({\n * model: new ChatOpenAI({ model: \"gpt-4\" }),\n * tools: [lookupUser],\n * middleware: [piiRedactionMiddleware({ rules: PII_RULES })]\n * });\n *\n * const result = await agent.invoke({\n * messages: [new HumanMessage(\"Look up SSN 123-45-6789\")]\n * });\n * // Model request: \"Look up SSN [REDACTED_SSN_abc123]\"\n * // Model response: tool_call({ \"ssn\": \"[REDACTED_SSN_abc123]\" })\n * // Tool receives: { \"ssn\": \"123-45-6789\" }\n * ```\n *\n * @example Runtime rule configuration via context\n * ```typescript\n * const agent = createAgent({\n * model: new ChatOpenAI({ model: \"gpt-4\" }),\n * tools: [someTool],\n * middleware: [piiRedactionMiddleware()]\n * });\n *\n * // Configure rules at runtime via middleware context\n * const result = await agent.invoke(\n * { messages: [new HumanMessage(\"...\")] },\n * {\n * configurable: {\n * PIIRedactionMiddleware: {\n * rules: {\n * ssn: /\\b\\d{3}-?\\d{2}-?\\d{4}\\b/g,\n * email: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g,\n * }\n * }\n * }\n * }\n * );\n * ```\n *\n * @example Custom rule patterns\n * ```typescript\n * const customRules = {\n * employee_id: /EMP-\\d{6}/g,\n * api_key: /sk-[a-zA-Z0-9]{32}/g,\n * credit_card: /\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b/g,\n * };\n *\n * const middleware = piiRedactionMiddleware({ rules: customRules });\n * // Generates markers like: [REDACTED_EMPLOYEE_ID_xyz789]\n * ```\n *\n * @deprecated\n */\nexport function piiRedactionMiddleware(\n options: PIIRedactionMiddlewareConfig = {}\n): ReturnType<typeof createMiddleware> {\n const redactionMap: RedactionMap = {};\n\n console.warn(\n \"DEPRECATED: piiRedactionMiddleware is deprecated. Please use piiMiddleware instead, go to https://docs.langchain.com/oss/javascript/langchain/middleware/built-in#pii-detection for more information.\"\n );\n\n return createMiddleware({\n name: \"PIIRedactionMiddleware\",\n contextSchema,\n wrapModelCall: async (request, handler) => {\n /**\n * Merge options with context, following bigTool.ts pattern\n */\n const rules = request.runtime.context.rules ?? options.rules ?? {};\n\n /**\n * If no rules are provided, skip processing\n */\n if (Object.keys(rules).length === 0) {\n return handler(request);\n }\n\n const processedMessages = await Promise.all(\n request.state.messages.map((message: BaseMessage) =>\n processMessage(message, {\n rules,\n redactionMap,\n })\n )\n );\n\n return handler({\n ...request,\n messages: processedMessages,\n });\n },\n afterModel: async (state) => {\n /**\n * If no redactions were made, skip processing\n */\n if (Object.keys(redactionMap).length === 0) {\n return;\n }\n\n const lastMessage = state.messages.at(-1);\n if (!AIMessage.isInstance(lastMessage)) {\n return;\n }\n\n /**\n * In cases where we do structured output via tool calls, we also have to look at the second last message\n * as we add a custom last message to the messages array.\n */\n const secondLastMessage = state.messages.at(-2);\n\n const { message: restoredLastMessage, changed } = restoreMessage(\n lastMessage,\n redactionMap\n );\n\n if (!changed) {\n return;\n }\n\n /**\n * Identify if the last message is a structured response and restore the values if so\n */\n let structuredResponse: Record<string, unknown> | undefined;\n if (\n AIMessage.isInstance(lastMessage) &&\n lastMessage?.tool_calls?.length === 0 &&\n typeof lastMessage.content === \"string\" &&\n lastMessage.content.startsWith(\"{\") &&\n lastMessage.content.endsWith(\"}\")\n ) {\n try {\n structuredResponse = JSON.parse(\n restoreRedactedValues(lastMessage.content, redactionMap)\n );\n } catch {\n // ignore\n }\n }\n\n /**\n * Check if the second last message is a structured response tool call\n */\n const isStructuredResponseToolCall =\n AIMessage.isInstance(secondLastMessage) &&\n secondLastMessage?.tool_calls?.length !== 0 &&\n secondLastMessage?.tool_calls?.some((call) =>\n call.name.startsWith(\"extract-\")\n );\n if (isStructuredResponseToolCall) {\n const {\n message: restoredSecondLastMessage,\n changed: changedSecondLastMessage,\n } = restoreMessage(secondLastMessage, redactionMap);\n const structuredResponseRedacted = secondLastMessage.tool_calls?.find(\n (call) => call.name.startsWith(\"extract-\")\n )?.args;\n const structuredResponse = structuredResponseRedacted\n ? JSON.parse(\n restoreRedactedValues(\n JSON.stringify(structuredResponseRedacted),\n redactionMap\n )\n )\n : undefined;\n if (changed || changedSecondLastMessage) {\n return {\n ...state,\n ...(structuredResponse ? { structuredResponse } : {}),\n messages: [\n new RemoveMessage({ id: secondLastMessage.id as string }),\n new RemoveMessage({ id: lastMessage.id as string }),\n restoredSecondLastMessage,\n restoredLastMessage,\n ],\n };\n }\n }\n\n return {\n ...state,\n ...(structuredResponse ? { structuredResponse } : {}),\n messages: [\n new RemoveMessage({ id: lastMessage.id as string }),\n restoredLastMessage,\n ],\n };\n },\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"piiRedaction.js","names":[],"sources":["../../../src/agents/middleware/piiRedaction.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport {\n BaseMessage,\n AIMessage,\n HumanMessage,\n ToolMessage,\n RemoveMessage,\n SystemMessage,\n} from \"@langchain/core/messages\";\nimport type { InferInteropZodInput } from \"@langchain/core/utils/types\";\n\nimport { createMiddleware } from \"../middleware.js\";\n\n/**\n * Type for the redaction map that stores original values by ID\n */\ntype RedactionMap = Record<string, string>;\n\n/**\n * Configuration schema for the Input Guardrails middleware\n */\nconst contextSchema = z.object({\n /**\n * A record of PII detection rules to apply\n * @default DEFAULT_PII_RULES (with enabled rules only)\n */\n rules: z\n .record(\n z.string(),\n z.instanceof(RegExp).describe(\"Regular expression pattern to match PII\")\n )\n .optional(),\n});\n\n/**\n * @deprecated\n */\nexport type PIIRedactionMiddlewareConfig = InferInteropZodInput<\n typeof contextSchema\n>;\n\n/**\n * Generate a unique ID for a redaction\n */\nfunction generateRedactionId(): string {\n return Math.random().toString(36).substring(2, 11);\n}\n\n/**\n * Apply PII detection rules to text with ID tracking\n */\nfunction applyPIIRules(\n text: string,\n rules: Record<string, RegExp>,\n redactionMap: RedactionMap\n): string {\n let processedText = text;\n\n for (const [name, pattern] of Object.entries(rules)) {\n const replacement = name.toUpperCase().replace(/[^a-zA-Z0-9_-]/g, \"\");\n processedText = processedText.replace(pattern, (match) => {\n const id = generateRedactionId();\n redactionMap[id] = match;\n // Create a trackable replacement like [REDACTED_SSN_abc123]\n return `[REDACTED_${replacement}_${id}]`;\n });\n }\n\n return processedText;\n}\n\ninterface ProcessHumanMessageConfig {\n rules: Record<string, RegExp>;\n redactionMap: RedactionMap;\n}\n\n/**\n * Process a single human message for PII detection and redaction\n */\nasync function processMessage(\n message: BaseMessage,\n config: ProcessHumanMessageConfig\n): Promise<BaseMessage> {\n /**\n * handle basic message types\n */\n if (\n HumanMessage.isInstance(message) ||\n ToolMessage.isInstance(message) ||\n SystemMessage.isInstance(message)\n ) {\n const content = message.content as string;\n const processedContent = await applyPIIRules(\n content,\n config.rules,\n config.redactionMap\n );\n\n if (processedContent !== content) {\n const MessageConstructor = Object.getPrototypeOf(message).constructor;\n return new MessageConstructor({\n ...message,\n content: processedContent,\n });\n }\n\n return message;\n }\n\n /**\n * Handle AI messages\n */\n if (AIMessage.isInstance(message)) {\n const content =\n typeof message.content === \"string\"\n ? message.content\n : JSON.stringify(message.content);\n const toolCalls = JSON.stringify(message.tool_calls);\n const processedContent = await applyPIIRules(\n content,\n config.rules,\n config.redactionMap\n );\n const processedToolCalls = await applyPIIRules(\n toolCalls,\n config.rules,\n config.redactionMap\n );\n\n if (processedContent !== content || processedToolCalls !== toolCalls) {\n return new AIMessage({\n ...message,\n content:\n typeof message.content === \"string\"\n ? processedContent\n : JSON.parse(processedContent),\n tool_calls: JSON.parse(processedToolCalls),\n });\n }\n\n return message;\n }\n\n throw new Error(`Unsupported message type: ${message.type}`);\n}\n\n/**\n * Restore original values from redacted text using the redaction map\n */\nfunction restoreRedactedValues(\n text: string,\n redactionMap: RedactionMap\n): string {\n let restoredText = text;\n\n // Pattern to match redacted values like [REDACTED_SSN_abc123]\n const redactionPattern = /\\[REDACTED_[A-Z_]+_(\\w+)\\]/g;\n\n restoredText = restoredText.replace(redactionPattern, (match, id) => {\n if (redactionMap[id]) {\n return redactionMap[id];\n }\n return match; // Keep original if no mapping found\n });\n\n return restoredText;\n}\n\n/**\n * Restore redacted values in a message (creates a new message object)\n */\nfunction restoreMessage(\n message: BaseMessage,\n redactionMap: RedactionMap\n): { message: BaseMessage; changed: boolean } {\n /**\n * handle basic message types\n */\n if (\n HumanMessage.isInstance(message) ||\n ToolMessage.isInstance(message) ||\n SystemMessage.isInstance(message)\n ) {\n const content = message.content as string;\n const restoredContent = restoreRedactedValues(content, redactionMap);\n if (restoredContent !== content) {\n const MessageConstructor = Object.getPrototypeOf(message).constructor;\n const newMessage = new MessageConstructor({\n ...message,\n content: restoredContent,\n });\n return { message: newMessage, changed: true };\n }\n return { message, changed: false };\n }\n\n /**\n * handle AI messages\n */\n if (AIMessage.isInstance(message)) {\n const content =\n typeof message.content === \"string\"\n ? message.content\n : JSON.stringify(message.content);\n const toolCalls = JSON.stringify(message.tool_calls);\n const processedContent = restoreRedactedValues(content, redactionMap);\n const processedToolCalls = restoreRedactedValues(toolCalls, redactionMap);\n if (processedContent !== content || processedToolCalls !== toolCalls) {\n return {\n message: new AIMessage({\n ...message,\n content:\n typeof message.content === \"string\"\n ? processedContent\n : JSON.parse(processedContent),\n tool_calls: JSON.parse(processedToolCalls),\n }),\n changed: true,\n };\n }\n\n return { message, changed: false };\n }\n\n throw new Error(`Unsupported message type: ${message.type}`);\n}\n\n/**\n * Creates a middleware that detects and redacts personally identifiable information (PII)\n * from messages before they are sent to model providers, and restores original values\n * in model responses for tool execution.\n *\n * ## Mechanism\n *\n * The middleware intercepts agent execution at two points:\n *\n * ### Request Phase (`wrapModelCall`)\n * - Applies regex-based pattern matching to all message content (HumanMessage, ToolMessage, SystemMessage, AIMessage)\n * - Processes both message text and AIMessage tool call arguments\n * - Each matched pattern generates:\n * - Unique identifier: `generateRedactionId()` → `\"abc123\"`\n * - Redaction marker: `[REDACTED_{RULE_NAME}_{ID}]` → `\"[REDACTED_SSN_abc123]\"`\n * - Redaction map entry: `{ \"abc123\": \"123-45-6789\" }`\n * - Returns modified request with redacted message content\n *\n * ### Response Phase (`afterModel`)\n * - Scans AIMessage responses for redaction markers matching pattern: `/\\[REDACTED_[A-Z_]+_(\\w+)\\]/g`\n * - Replaces markers with original values from redaction map\n * - Handles both standard responses and structured output (via tool calls or JSON content)\n * - For structured output, restores values in both the tool call arguments and the `structuredResponse` state field\n * - Returns new message instances via RemoveMessage/AIMessage to update state\n *\n * ## Data Flow\n *\n * ```\n * User Input: \"My SSN is 123-45-6789\"\n * ↓ [beforeModel]\n * Model Request: \"My SSN is [REDACTED_SSN_abc123]\"\n * ↓ [model invocation]\n * Model Response: tool_call({ \"ssn\": \"[REDACTED_SSN_abc123]\" })\n * ↓ [afterModel]\n * Tool Execution: tool({ \"ssn\": \"123-45-6789\" })\n * ```\n *\n * ## Limitations\n *\n * This middleware provides model provider isolation only. PII may still be present in:\n * - LangGraph state checkpoints (memory, databases)\n * - Network traffic between client and application server\n * - Application logs and trace data\n * - Tool execution arguments and responses\n * - Final agent output\n *\n * For comprehensive PII protection, implement additional controls at the application,\n * network, and storage layers.\n *\n * @param options - Configuration options\n * @param options.rules - Record of detection rules mapping rule names to regex patterns.\n * Rule names are normalized to uppercase and used in redaction markers.\n * Patterns must use the global flag (`/pattern/g`) to match all occurrences.\n *\n * @returns Middleware instance for use with `createAgent`\n *\n * @example Basic usage with custom rules\n * ```typescript\n * import { piiRedactionMiddleware } from \"langchain\";\n * import { createAgent } from \"langchain\";\n * import { tool } from \"@langchain/core/tools\";\n * import { z } from \"zod/v3\";\n *\n * const PII_RULES = {\n * ssn: /\\b\\d{3}-?\\d{2}-?\\d{4}\\b/g,\n * email: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g,\n * phone: /\\b\\d{3}[-.]?\\d{3}[-.]?\\d{4}\\b/g,\n * };\n *\n * const lookupUser = tool(async ({ ssn }) => {\n * // Receives original value: \"123-45-6789\"\n * return { name: \"John Doe\", account: \"active\" };\n * }, {\n * name: \"lookup_user\",\n * description: \"Look up user by SSN\",\n * schema: z.object({ ssn: z.string() })\n * });\n *\n * const agent = createAgent({\n * model: new ChatOpenAI({ model: \"gpt-4\" }),\n * tools: [lookupUser],\n * middleware: [piiRedactionMiddleware({ rules: PII_RULES })]\n * });\n *\n * const result = await agent.invoke({\n * messages: [new HumanMessage(\"Look up SSN 123-45-6789\")]\n * });\n * // Model request: \"Look up SSN [REDACTED_SSN_abc123]\"\n * // Model response: tool_call({ \"ssn\": \"[REDACTED_SSN_abc123]\" })\n * // Tool receives: { \"ssn\": \"123-45-6789\" }\n * ```\n *\n * @example Runtime rule configuration via context\n * ```typescript\n * const agent = createAgent({\n * model: new ChatOpenAI({ model: \"gpt-4\" }),\n * tools: [someTool],\n * middleware: [piiRedactionMiddleware()]\n * });\n *\n * // Configure rules at runtime via middleware context\n * const result = await agent.invoke(\n * { messages: [new HumanMessage(\"...\")] },\n * {\n * configurable: {\n * PIIRedactionMiddleware: {\n * rules: {\n * ssn: /\\b\\d{3}-?\\d{2}-?\\d{4}\\b/g,\n * email: /\\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}\\b/g,\n * }\n * }\n * }\n * }\n * );\n * ```\n *\n * @example Custom rule patterns\n * ```typescript\n * const customRules = {\n * employee_id: /EMP-\\d{6}/g,\n * api_key: /sk-[a-zA-Z0-9]{32}/g,\n * credit_card: /\\b\\d{4}[- ]?\\d{4}[- ]?\\d{4}[- ]?\\d{4}\\b/g,\n * };\n *\n * const middleware = piiRedactionMiddleware({ rules: customRules });\n * // Generates markers like: [REDACTED_EMPLOYEE_ID_xyz789]\n * ```\n *\n * @deprecated\n */\nexport function piiRedactionMiddleware(\n options: PIIRedactionMiddlewareConfig = {}\n) {\n const redactionMap: RedactionMap = {};\n\n console.warn(\n \"DEPRECATED: piiRedactionMiddleware is deprecated. Please use piiMiddleware instead, go to https://docs.langchain.com/oss/javascript/langchain/middleware/built-in#pii-detection for more information.\"\n );\n\n return createMiddleware({\n name: \"PIIRedactionMiddleware\",\n contextSchema,\n wrapModelCall: async (request, handler) => {\n /**\n * Merge options with context, following bigTool.ts pattern\n */\n const rules = request.runtime.context.rules ?? options.rules ?? {};\n\n /**\n * If no rules are provided, skip processing\n */\n if (Object.keys(rules).length === 0) {\n return handler(request);\n }\n\n const processedMessages = await Promise.all(\n request.state.messages.map((message: BaseMessage) =>\n processMessage(message, {\n rules,\n redactionMap,\n })\n )\n );\n\n return handler({\n ...request,\n messages: processedMessages,\n });\n },\n afterModel: async (state) => {\n /**\n * If no redactions were made, skip processing\n */\n if (Object.keys(redactionMap).length === 0) {\n return;\n }\n\n const lastMessage = state.messages.at(-1);\n if (!AIMessage.isInstance(lastMessage)) {\n return;\n }\n\n /**\n * In cases where we do structured output via tool calls, we also have to look at the second last message\n * as we add a custom last message to the messages array.\n */\n const secondLastMessage = state.messages.at(-2);\n\n const { message: restoredLastMessage, changed } = restoreMessage(\n lastMessage,\n redactionMap\n );\n\n if (!changed) {\n return;\n }\n\n /**\n * Identify if the last message is a structured response and restore the values if so\n */\n let structuredResponse: Record<string, unknown> | undefined;\n if (\n AIMessage.isInstance(lastMessage) &&\n lastMessage?.tool_calls?.length === 0 &&\n typeof lastMessage.content === \"string\" &&\n lastMessage.content.startsWith(\"{\") &&\n lastMessage.content.endsWith(\"}\")\n ) {\n try {\n structuredResponse = JSON.parse(\n restoreRedactedValues(lastMessage.content, redactionMap)\n );\n } catch {\n // ignore\n }\n }\n\n /**\n * Check if the second last message is a structured response tool call\n */\n const isStructuredResponseToolCall =\n AIMessage.isInstance(secondLastMessage) &&\n secondLastMessage?.tool_calls?.length !== 0 &&\n secondLastMessage?.tool_calls?.some((call) =>\n call.name.startsWith(\"extract-\")\n );\n if (isStructuredResponseToolCall) {\n const {\n message: restoredSecondLastMessage,\n changed: changedSecondLastMessage,\n } = restoreMessage(secondLastMessage, redactionMap);\n const structuredResponseRedacted = secondLastMessage.tool_calls?.find(\n (call) => call.name.startsWith(\"extract-\")\n )?.args;\n const structuredResponse = structuredResponseRedacted\n ? JSON.parse(\n restoreRedactedValues(\n JSON.stringify(structuredResponseRedacted),\n redactionMap\n )\n )\n : undefined;\n if (changed || changedSecondLastMessage) {\n return {\n ...state,\n ...(structuredResponse ? { structuredResponse } : {}),\n messages: [\n new RemoveMessage({ id: secondLastMessage.id as string }),\n new RemoveMessage({ id: lastMessage.id as string }),\n restoredSecondLastMessage,\n restoredLastMessage,\n ],\n };\n }\n }\n\n return {\n ...state,\n ...(structuredResponse ? { structuredResponse } : {}),\n messages: [\n new RemoveMessage({ id: lastMessage.id as string }),\n restoredLastMessage,\n ],\n };\n },\n });\n}\n"],"mappings":"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"}
|
|
@@ -60,7 +60,7 @@ var PromptCachingMiddlewareError = class extends Error {
|
|
|
60
60
|
* import { anthropicPromptCachingMiddleware } from "langchain";
|
|
61
61
|
*
|
|
62
62
|
* const agent = createAgent({
|
|
63
|
-
* model: "anthropic:claude-
|
|
63
|
+
* model: "anthropic:claude-sonnet-4-5",
|
|
64
64
|
* middleware: [
|
|
65
65
|
* anthropicPromptCachingMiddleware()
|
|
66
66
|
* ]
|
|
@@ -76,7 +76,7 @@ var PromptCachingMiddlewareError = class extends Error {
|
|
|
76
76
|
* });
|
|
77
77
|
*
|
|
78
78
|
* const agent = createAgent({
|
|
79
|
-
* model: "anthropic:claude-
|
|
79
|
+
* model: "anthropic:claude-sonnet-4-5",
|
|
80
80
|
* systemPrompt: "You are a helpful assistant with deep knowledge of...", // Long system prompt
|
|
81
81
|
* middleware: [cachingMiddleware]
|
|
82
82
|
* });
|
|
@@ -86,7 +86,7 @@ var PromptCachingMiddlewareError = class extends Error {
|
|
|
86
86
|
* Conditional caching based on runtime context
|
|
87
87
|
* ```typescript
|
|
88
88
|
* const agent = createAgent({
|
|
89
|
-
* model: "anthropic:claude-
|
|
89
|
+
* model: "anthropic:claude-sonnet-4-5",
|
|
90
90
|
* middleware: [
|
|
91
91
|
* anthropicPromptCachingMiddleware({
|
|
92
92
|
* enableCaching: true,
|
|
@@ -110,7 +110,7 @@ var PromptCachingMiddlewareError = class extends Error {
|
|
|
110
110
|
* Optimal setup for customer support chatbot
|
|
111
111
|
* ```typescript
|
|
112
112
|
* const supportAgent = createAgent({
|
|
113
|
-
* model: "anthropic:claude-
|
|
113
|
+
* model: "anthropic:claude-sonnet-4-5",
|
|
114
114
|
* systemPrompt: `You are a customer support agent for ACME Corp.
|
|
115
115
|
*
|
|
116
116
|
* Company policies:
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"promptCaching.cjs","names":["z","createMiddleware"],"sources":["../../../../../src/agents/middleware/provider/anthropic/promptCaching.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport { InferInteropZodInput } from \"@langchain/core/utils/types\";\n\nimport { ConfigurableModel } from \"../../../../chat_models/universal.js\";\nimport { createMiddleware } from \"../../../middleware.js\";\n\nconst DEFAULT_ENABLE_CACHING = true;\nconst DEFAULT_TTL = \"5m\";\nconst DEFAULT_MIN_MESSAGES_TO_CACHE = 3;\nconst DEFAULT_UNSUPPORTED_MODEL_BEHAVIOR = \"warn\";\n\nconst contextSchema = z.object({\n /**\n * Whether to enable prompt caching.\n * @default true\n */\n enableCaching: z.boolean().optional(),\n /**\n * The time-to-live for the cached prompt.\n * @default \"5m\"\n */\n ttl: z.enum([\"5m\", \"1h\"]).optional(),\n /**\n * The minimum number of messages required before caching is applied.\n * @default 3\n */\n minMessagesToCache: z.number().optional(),\n /**\n * The behavior to take when an unsupported model is used.\n * - \"ignore\" will ignore the unsupported model and continue without caching.\n * - \"warn\" will warn the user and continue without caching.\n * - \"raise\" will raise an error and stop the agent.\n * @default \"warn\"\n */\n unsupportedModelBehavior: z.enum([\"ignore\", \"warn\", \"raise\"]).optional(),\n});\nexport type PromptCachingMiddlewareConfig = Partial<\n InferInteropZodInput<typeof contextSchema>\n>;\n\nclass PromptCachingMiddlewareError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"PromptCachingMiddlewareError\";\n }\n}\n\n/**\n * Creates a prompt caching middleware for Anthropic models to optimize API usage.\n *\n * This middleware automatically adds cache control headers to the last messages when using Anthropic models,\n * enabling their prompt caching feature. This can significantly reduce costs for applications with repetitive\n * prompts, long system messages, or extensive conversation histories.\n *\n * ## How It Works\n *\n * The middleware intercepts model requests and adds cache control metadata that tells Anthropic's\n * API to cache processed prompt prefixes. On subsequent requests with matching prefixes, the\n * cached representations are reused, skipping redundant token processing.\n *\n * ## Benefits\n *\n * - **Cost Reduction**: Avoid reprocessing the same tokens repeatedly (up to 90% savings on cached portions)\n * - **Lower Latency**: Cached prompts are processed faster as embeddings are pre-computed\n * - **Better Scalability**: Reduced computational load enables handling more requests\n * - **Consistent Performance**: Stable response times for repetitive queries\n *\n * @param middlewareOptions - Configuration options for the caching behavior\n * @param middlewareOptions.enableCaching - Whether to enable prompt caching (default: `true`)\n * @param middlewareOptions.ttl - Cache time-to-live: `\"5m\"` for 5 minutes or `\"1h\"` for 1 hour (default: `\"5m\"`)\n * @param middlewareOptions.minMessagesToCache - Minimum number of messages required before caching is applied (default: `3`)\n * @param middlewareOptions.unsupportedModelBehavior - The behavior to take when an unsupported model is used (default: `\"warn\"`)\n *\n * @returns A middleware instance that can be passed to `createAgent`\n *\n * @throws {Error} If used with non-Anthropic models\n *\n * @example\n * Basic usage with default settings\n * ```typescript\n * import { createAgent } from \"langchain\";\n * import { anthropicPromptCachingMiddleware } from \"langchain\";\n *\n * const agent = createAgent({\n * model: \"anthropic:claude-3-5-sonnet\",\n * middleware: [\n * anthropicPromptCachingMiddleware()\n * ]\n * });\n * ```\n *\n * @example\n * Custom configuration for longer conversations\n * ```typescript\n * const cachingMiddleware = anthropicPromptCachingMiddleware({\n * ttl: \"1h\", // Cache for 1 hour instead of default 5 minutes\n * minMessagesToCache: 5 // Only cache after 5 messages\n * });\n *\n * const agent = createAgent({\n * model: \"anthropic:claude-3-5-sonnet\",\n * systemPrompt: \"You are a helpful assistant with deep knowledge of...\", // Long system prompt\n * middleware: [cachingMiddleware]\n * });\n * ```\n *\n * @example\n * Conditional caching based on runtime context\n * ```typescript\n * const agent = createAgent({\n * model: \"anthropic:claude-3-5-sonnet\",\n * middleware: [\n * anthropicPromptCachingMiddleware({\n * enableCaching: true,\n * ttl: \"5m\"\n * })\n * ]\n * });\n *\n * // Disable caching for specific requests\n * await agent.invoke(\n * { messages: [new HumanMessage(\"Process this without caching\")] },\n * {\n * configurable: {\n * middleware_context: { enableCaching: false }\n * }\n * }\n * );\n * ```\n *\n * @example\n * Optimal setup for customer support chatbot\n * ```typescript\n * const supportAgent = createAgent({\n * model: \"anthropic:claude-3-5-sonnet\",\n * systemPrompt: `You are a customer support agent for ACME Corp.\n *\n * Company policies:\n * - Always be polite and professional\n * - Refer to knowledge base for product information\n * - Escalate billing issues to human agents\n * ... (extensive policies and guidelines)\n * `,\n * tools: [searchKnowledgeBase, createTicket, checkOrderStatus],\n * middleware: [\n * anthropicPromptCachingMiddleware({\n * ttl: \"1h\", // Long TTL for stable system prompt\n * minMessagesToCache: 1 // Cache immediately due to large system prompt\n * })\n * ]\n * });\n * ```\n *\n * @remarks\n * - **Anthropic Only**: This middleware only works with Anthropic models and will throw an error if used with other providers\n * - **Automatic Application**: Caching is applied automatically when message count exceeds `minMessagesToCache`\n * - **Cache Scope**: Caches are isolated per API key and cannot be shared across different keys\n * - **TTL Options**: Only supports \"5m\" (5 minutes) and \"1h\" (1 hour) as TTL values per Anthropic's API\n * - **Best Use Cases**: Long system prompts, multi-turn conversations, repetitive queries, RAG applications\n * - **Cost Impact**: Cached tokens are billed at 10% of the base input token price, cache writes are billed at 25% of the base\n *\n * @see {@link createAgent} for agent creation\n * @see {@link https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching} Anthropic's prompt caching documentation\n * @public\n */\nexport function anthropicPromptCachingMiddleware(\n middlewareOptions?: PromptCachingMiddlewareConfig\n) {\n return createMiddleware({\n name: \"PromptCachingMiddleware\",\n contextSchema,\n wrapModelCall: (request, handler) => {\n /**\n * Prefer runtime context values over middleware options values over defaults\n */\n const enableCaching =\n request.runtime.context.enableCaching ??\n middlewareOptions?.enableCaching ??\n DEFAULT_ENABLE_CACHING;\n const ttl =\n request.runtime.context.ttl ?? middlewareOptions?.ttl ?? DEFAULT_TTL;\n const minMessagesToCache =\n request.runtime.context.minMessagesToCache ??\n middlewareOptions?.minMessagesToCache ??\n DEFAULT_MIN_MESSAGES_TO_CACHE;\n const unsupportedModelBehavior =\n request.runtime.context.unsupportedModelBehavior ??\n middlewareOptions?.unsupportedModelBehavior ??\n DEFAULT_UNSUPPORTED_MODEL_BEHAVIOR;\n\n // Skip if caching is disabled\n if (!enableCaching || !request.model) {\n return handler(request);\n }\n\n const isAnthropicModel =\n request.model.getName() === \"ChatAnthropic\" ||\n (request.model.getName() === \"ConfigurableModel\" &&\n (request.model as ConfigurableModel)._defaultConfig?.modelProvider ===\n \"anthropic\");\n if (!isAnthropicModel) {\n // Get model name for better error context\n const modelName = request.model.getName();\n const modelInfo =\n request.model.getName() === \"ConfigurableModel\"\n ? `${modelName} (${\n (request.model as ConfigurableModel)._defaultConfig\n ?.modelProvider\n })`\n : modelName;\n\n const baseMessage = `Unsupported model '${modelInfo}'. Prompt caching requires an Anthropic model`;\n\n if (unsupportedModelBehavior === \"raise\") {\n throw new PromptCachingMiddlewareError(\n `${baseMessage} (e.g., 'anthropic:claude-4-0-sonnet').`\n );\n } else if (unsupportedModelBehavior === \"warn\") {\n console.warn(\n `PromptCachingMiddleware: Skipping caching for ${modelName}. Consider switching to an Anthropic model for caching benefits.`\n );\n }\n return handler(request);\n }\n\n const messagesCount =\n request.state.messages.length + (request.systemPrompt ? 1 : 0);\n\n if (messagesCount < minMessagesToCache) {\n return handler(request);\n }\n\n /**\n * The cache_control is applied at the final message formatting layer in ChatAnthropic,\n * which avoids issues with message content block manipulation during earlier\n * processing stages (e.g., streaming response reassembly).\n *\n * @see https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching\n */\n return handler({\n ...request,\n modelSettings: {\n ...request.modelSettings,\n cache_control: {\n type: \"ephemeral\" as const,\n ttl,\n },\n },\n });\n },\n });\n}\n"],"mappings":";;;;;AAMA,MAAM,yBAAyB;AAC/B,MAAM,cAAc;AACpB,MAAM,gCAAgC;AACtC,MAAM,qCAAqC;AAE3C,MAAM,gBAAgBA,SAAE,OAAO;CAK7B,eAAeA,SAAE,SAAS,CAAC,UAAU;CAKrC,KAAKA,SAAE,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,UAAU;CAKpC,oBAAoBA,SAAE,QAAQ,CAAC,UAAU;CAQzC,0BAA0BA,SAAE,KAAK;EAAC;EAAU;EAAQ;EAAQ,CAAC,CAAC,UAAU;CACzE,CAAC;AAKF,IAAM,+BAAN,cAA2C,MAAM;CAC/C,YAAY,SAAiB;AAC3B,QAAM,QAAQ;AACd,OAAK,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0HhB,SAAgB,iCACd,mBACA;AACA,QAAOC,oCAAiB;EACtB,MAAM;EACN;EACA,gBAAgB,SAAS,YAAY;;;;GAInC,MAAM,gBACJ,QAAQ,QAAQ,QAAQ,iBACxB,mBAAmB,iBACnB;GACF,MAAM,MACJ,QAAQ,QAAQ,QAAQ,OAAO,mBAAmB,OAAO;GAC3D,MAAM,qBACJ,QAAQ,QAAQ,QAAQ,sBACxB,mBAAmB,sBACnB;GACF,MAAM,2BACJ,QAAQ,QAAQ,QAAQ,4BACxB,mBAAmB,4BACnB;AAGF,OAAI,CAAC,iBAAiB,CAAC,QAAQ,MAC7B,QAAO,QAAQ,QAAQ;AAQzB,OAAI,EAJF,QAAQ,MAAM,SAAS,KAAK,mBAC3B,QAAQ,MAAM,SAAS,KAAK,uBAC1B,QAAQ,MAA4B,gBAAgB,kBACnD,cACiB;IAErB,MAAM,YAAY,QAAQ,MAAM,SAAS;IASzC,MAAM,cAAc,sBAPlB,QAAQ,MAAM,SAAS,KAAK,sBACxB,GAAG,UAAU,IACV,QAAQ,MAA4B,gBACjC,cACL,KACD,UAE8C;AAEpD,QAAI,6BAA6B,QAC/B,OAAM,IAAI,6BACR,GAAG,YAAY,yCAChB;aACQ,6BAA6B,OACtC,SAAQ,KACN,iDAAiD,UAAU,kEAC5D;AAEH,WAAO,QAAQ,QAAQ;;AAMzB,OAFE,QAAQ,MAAM,SAAS,UAAU,QAAQ,eAAe,IAAI,KAE1C,mBAClB,QAAO,QAAQ,QAAQ;;;;;;;;AAUzB,UAAO,QAAQ;IACb,GAAG;IACH,eAAe;KACb,GAAG,QAAQ;KACX,eAAe;MACb,MAAM;MACN;MACD;KACF;IACF,CAAC;;EAEL,CAAC"}
|
|
1
|
+
{"version":3,"file":"promptCaching.cjs","names":["z","createMiddleware"],"sources":["../../../../../src/agents/middleware/provider/anthropic/promptCaching.ts"],"sourcesContent":["import { z } from \"zod/v3\";\nimport { InferInteropZodInput } from \"@langchain/core/utils/types\";\n\nimport { ConfigurableModel } from \"../../../../chat_models/universal.js\";\nimport { createMiddleware } from \"../../../middleware.js\";\n\nconst DEFAULT_ENABLE_CACHING = true;\nconst DEFAULT_TTL = \"5m\";\nconst DEFAULT_MIN_MESSAGES_TO_CACHE = 3;\nconst DEFAULT_UNSUPPORTED_MODEL_BEHAVIOR = \"warn\";\n\nconst contextSchema = z.object({\n /**\n * Whether to enable prompt caching.\n * @default true\n */\n enableCaching: z.boolean().optional(),\n /**\n * The time-to-live for the cached prompt.\n * @default \"5m\"\n */\n ttl: z.enum([\"5m\", \"1h\"]).optional(),\n /**\n * The minimum number of messages required before caching is applied.\n * @default 3\n */\n minMessagesToCache: z.number().optional(),\n /**\n * The behavior to take when an unsupported model is used.\n * - \"ignore\" will ignore the unsupported model and continue without caching.\n * - \"warn\" will warn the user and continue without caching.\n * - \"raise\" will raise an error and stop the agent.\n * @default \"warn\"\n */\n unsupportedModelBehavior: z.enum([\"ignore\", \"warn\", \"raise\"]).optional(),\n});\nexport type PromptCachingMiddlewareConfig = Partial<\n InferInteropZodInput<typeof contextSchema>\n>;\n\nclass PromptCachingMiddlewareError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"PromptCachingMiddlewareError\";\n }\n}\n\n/**\n * Creates a prompt caching middleware for Anthropic models to optimize API usage.\n *\n * This middleware automatically adds cache control headers to the last messages when using Anthropic models,\n * enabling their prompt caching feature. This can significantly reduce costs for applications with repetitive\n * prompts, long system messages, or extensive conversation histories.\n *\n * ## How It Works\n *\n * The middleware intercepts model requests and adds cache control metadata that tells Anthropic's\n * API to cache processed prompt prefixes. On subsequent requests with matching prefixes, the\n * cached representations are reused, skipping redundant token processing.\n *\n * ## Benefits\n *\n * - **Cost Reduction**: Avoid reprocessing the same tokens repeatedly (up to 90% savings on cached portions)\n * - **Lower Latency**: Cached prompts are processed faster as embeddings are pre-computed\n * - **Better Scalability**: Reduced computational load enables handling more requests\n * - **Consistent Performance**: Stable response times for repetitive queries\n *\n * @param middlewareOptions - Configuration options for the caching behavior\n * @param middlewareOptions.enableCaching - Whether to enable prompt caching (default: `true`)\n * @param middlewareOptions.ttl - Cache time-to-live: `\"5m\"` for 5 minutes or `\"1h\"` for 1 hour (default: `\"5m\"`)\n * @param middlewareOptions.minMessagesToCache - Minimum number of messages required before caching is applied (default: `3`)\n * @param middlewareOptions.unsupportedModelBehavior - The behavior to take when an unsupported model is used (default: `\"warn\"`)\n *\n * @returns A middleware instance that can be passed to `createAgent`\n *\n * @throws {Error} If used with non-Anthropic models\n *\n * @example\n * Basic usage with default settings\n * ```typescript\n * import { createAgent } from \"langchain\";\n * import { anthropicPromptCachingMiddleware } from \"langchain\";\n *\n * const agent = createAgent({\n * model: \"anthropic:claude-sonnet-4-5\",\n * middleware: [\n * anthropicPromptCachingMiddleware()\n * ]\n * });\n * ```\n *\n * @example\n * Custom configuration for longer conversations\n * ```typescript\n * const cachingMiddleware = anthropicPromptCachingMiddleware({\n * ttl: \"1h\", // Cache for 1 hour instead of default 5 minutes\n * minMessagesToCache: 5 // Only cache after 5 messages\n * });\n *\n * const agent = createAgent({\n * model: \"anthropic:claude-sonnet-4-5\",\n * systemPrompt: \"You are a helpful assistant with deep knowledge of...\", // Long system prompt\n * middleware: [cachingMiddleware]\n * });\n * ```\n *\n * @example\n * Conditional caching based on runtime context\n * ```typescript\n * const agent = createAgent({\n * model: \"anthropic:claude-sonnet-4-5\",\n * middleware: [\n * anthropicPromptCachingMiddleware({\n * enableCaching: true,\n * ttl: \"5m\"\n * })\n * ]\n * });\n *\n * // Disable caching for specific requests\n * await agent.invoke(\n * { messages: [new HumanMessage(\"Process this without caching\")] },\n * {\n * configurable: {\n * middleware_context: { enableCaching: false }\n * }\n * }\n * );\n * ```\n *\n * @example\n * Optimal setup for customer support chatbot\n * ```typescript\n * const supportAgent = createAgent({\n * model: \"anthropic:claude-sonnet-4-5\",\n * systemPrompt: `You are a customer support agent for ACME Corp.\n *\n * Company policies:\n * - Always be polite and professional\n * - Refer to knowledge base for product information\n * - Escalate billing issues to human agents\n * ... (extensive policies and guidelines)\n * `,\n * tools: [searchKnowledgeBase, createTicket, checkOrderStatus],\n * middleware: [\n * anthropicPromptCachingMiddleware({\n * ttl: \"1h\", // Long TTL for stable system prompt\n * minMessagesToCache: 1 // Cache immediately due to large system prompt\n * })\n * ]\n * });\n * ```\n *\n * @remarks\n * - **Anthropic Only**: This middleware only works with Anthropic models and will throw an error if used with other providers\n * - **Automatic Application**: Caching is applied automatically when message count exceeds `minMessagesToCache`\n * - **Cache Scope**: Caches are isolated per API key and cannot be shared across different keys\n * - **TTL Options**: Only supports \"5m\" (5 minutes) and \"1h\" (1 hour) as TTL values per Anthropic's API\n * - **Best Use Cases**: Long system prompts, multi-turn conversations, repetitive queries, RAG applications\n * - **Cost Impact**: Cached tokens are billed at 10% of the base input token price, cache writes are billed at 25% of the base\n *\n * @see {@link createAgent} for agent creation\n * @see {@link https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching} Anthropic's prompt caching documentation\n * @public\n */\nexport function anthropicPromptCachingMiddleware(\n middlewareOptions?: PromptCachingMiddlewareConfig\n) {\n return createMiddleware({\n name: \"PromptCachingMiddleware\",\n contextSchema,\n wrapModelCall: (request, handler) => {\n /**\n * Prefer runtime context values over middleware options values over defaults\n */\n const enableCaching =\n request.runtime.context.enableCaching ??\n middlewareOptions?.enableCaching ??\n DEFAULT_ENABLE_CACHING;\n const ttl =\n request.runtime.context.ttl ?? middlewareOptions?.ttl ?? DEFAULT_TTL;\n const minMessagesToCache =\n request.runtime.context.minMessagesToCache ??\n middlewareOptions?.minMessagesToCache ??\n DEFAULT_MIN_MESSAGES_TO_CACHE;\n const unsupportedModelBehavior =\n request.runtime.context.unsupportedModelBehavior ??\n middlewareOptions?.unsupportedModelBehavior ??\n DEFAULT_UNSUPPORTED_MODEL_BEHAVIOR;\n\n // Skip if caching is disabled\n if (!enableCaching || !request.model) {\n return handler(request);\n }\n\n const isAnthropicModel =\n request.model.getName() === \"ChatAnthropic\" ||\n (request.model.getName() === \"ConfigurableModel\" &&\n (request.model as ConfigurableModel)._defaultConfig?.modelProvider ===\n \"anthropic\");\n if (!isAnthropicModel) {\n // Get model name for better error context\n const modelName = request.model.getName();\n const modelInfo =\n request.model.getName() === \"ConfigurableModel\"\n ? `${modelName} (${\n (request.model as ConfigurableModel)._defaultConfig\n ?.modelProvider\n })`\n : modelName;\n\n const baseMessage = `Unsupported model '${modelInfo}'. Prompt caching requires an Anthropic model`;\n\n if (unsupportedModelBehavior === \"raise\") {\n throw new PromptCachingMiddlewareError(\n `${baseMessage} (e.g., 'anthropic:claude-4-0-sonnet').`\n );\n } else if (unsupportedModelBehavior === \"warn\") {\n console.warn(\n `PromptCachingMiddleware: Skipping caching for ${modelName}. Consider switching to an Anthropic model for caching benefits.`\n );\n }\n return handler(request);\n }\n\n const messagesCount =\n request.state.messages.length + (request.systemPrompt ? 1 : 0);\n\n if (messagesCount < minMessagesToCache) {\n return handler(request);\n }\n\n /**\n * The cache_control is applied at the final message formatting layer in ChatAnthropic,\n * which avoids issues with message content block manipulation during earlier\n * processing stages (e.g., streaming response reassembly).\n *\n * @see https://docs.anthropic.com/en/docs/build-with-claude/prompt-caching\n */\n return handler({\n ...request,\n modelSettings: {\n ...request.modelSettings,\n cache_control: {\n type: \"ephemeral\" as const,\n ttl,\n },\n },\n });\n },\n });\n}\n"],"mappings":";;;;;AAMA,MAAM,yBAAyB;AAC/B,MAAM,cAAc;AACpB,MAAM,gCAAgC;AACtC,MAAM,qCAAqC;AAE3C,MAAM,gBAAgBA,SAAE,OAAO;CAK7B,eAAeA,SAAE,SAAS,CAAC,UAAU;CAKrC,KAAKA,SAAE,KAAK,CAAC,MAAM,KAAK,CAAC,CAAC,UAAU;CAKpC,oBAAoBA,SAAE,QAAQ,CAAC,UAAU;CAQzC,0BAA0BA,SAAE,KAAK;EAAC;EAAU;EAAQ;EAAQ,CAAC,CAAC,UAAU;CACzE,CAAC;AAKF,IAAM,+BAAN,cAA2C,MAAM;CAC/C,YAAY,SAAiB;AAC3B,QAAM,QAAQ;AACd,OAAK,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA0HhB,SAAgB,iCACd,mBACA;AACA,QAAOC,oCAAiB;EACtB,MAAM;EACN;EACA,gBAAgB,SAAS,YAAY;;;;GAInC,MAAM,gBACJ,QAAQ,QAAQ,QAAQ,iBACxB,mBAAmB,iBACnB;GACF,MAAM,MACJ,QAAQ,QAAQ,QAAQ,OAAO,mBAAmB,OAAO;GAC3D,MAAM,qBACJ,QAAQ,QAAQ,QAAQ,sBACxB,mBAAmB,sBACnB;GACF,MAAM,2BACJ,QAAQ,QAAQ,QAAQ,4BACxB,mBAAmB,4BACnB;AAGF,OAAI,CAAC,iBAAiB,CAAC,QAAQ,MAC7B,QAAO,QAAQ,QAAQ;AAQzB,OAAI,EAJF,QAAQ,MAAM,SAAS,KAAK,mBAC3B,QAAQ,MAAM,SAAS,KAAK,uBAC1B,QAAQ,MAA4B,gBAAgB,kBACnD,cACiB;IAErB,MAAM,YAAY,QAAQ,MAAM,SAAS;IASzC,MAAM,cAAc,sBAPlB,QAAQ,MAAM,SAAS,KAAK,sBACxB,GAAG,UAAU,IACV,QAAQ,MAA4B,gBACjC,cACL,KACD,UAE8C;AAEpD,QAAI,6BAA6B,QAC/B,OAAM,IAAI,6BACR,GAAG,YAAY,yCAChB;aACQ,6BAA6B,OACtC,SAAQ,KACN,iDAAiD,UAAU,kEAC5D;AAEH,WAAO,QAAQ,QAAQ;;AAMzB,OAFE,QAAQ,MAAM,SAAS,UAAU,QAAQ,eAAe,IAAI,KAE1C,mBAClB,QAAO,QAAQ,QAAQ;;;;;;;;AAUzB,UAAO,QAAQ;IACb,GAAG;IACH,eAAe;KACb,GAAG,QAAQ;KACX,eAAe;MACb,MAAM;MACN;MACD;KACF;IACF,CAAC;;EAEL,CAAC"}
|
|
@@ -77,7 +77,7 @@ type PromptCachingMiddlewareConfig = Partial<InferInteropZodInput<typeof context
|
|
|
77
77
|
* import { anthropicPromptCachingMiddleware } from "langchain";
|
|
78
78
|
*
|
|
79
79
|
* const agent = createAgent({
|
|
80
|
-
* model: "anthropic:claude-
|
|
80
|
+
* model: "anthropic:claude-sonnet-4-5",
|
|
81
81
|
* middleware: [
|
|
82
82
|
* anthropicPromptCachingMiddleware()
|
|
83
83
|
* ]
|
|
@@ -93,7 +93,7 @@ type PromptCachingMiddlewareConfig = Partial<InferInteropZodInput<typeof context
|
|
|
93
93
|
* });
|
|
94
94
|
*
|
|
95
95
|
* const agent = createAgent({
|
|
96
|
-
* model: "anthropic:claude-
|
|
96
|
+
* model: "anthropic:claude-sonnet-4-5",
|
|
97
97
|
* systemPrompt: "You are a helpful assistant with deep knowledge of...", // Long system prompt
|
|
98
98
|
* middleware: [cachingMiddleware]
|
|
99
99
|
* });
|
|
@@ -103,7 +103,7 @@ type PromptCachingMiddlewareConfig = Partial<InferInteropZodInput<typeof context
|
|
|
103
103
|
* Conditional caching based on runtime context
|
|
104
104
|
* ```typescript
|
|
105
105
|
* const agent = createAgent({
|
|
106
|
-
* model: "anthropic:claude-
|
|
106
|
+
* model: "anthropic:claude-sonnet-4-5",
|
|
107
107
|
* middleware: [
|
|
108
108
|
* anthropicPromptCachingMiddleware({
|
|
109
109
|
* enableCaching: true,
|
|
@@ -127,7 +127,7 @@ type PromptCachingMiddlewareConfig = Partial<InferInteropZodInput<typeof context
|
|
|
127
127
|
* Optimal setup for customer support chatbot
|
|
128
128
|
* ```typescript
|
|
129
129
|
* const supportAgent = createAgent({
|
|
130
|
-
* model: "anthropic:claude-
|
|
130
|
+
* model: "anthropic:claude-sonnet-4-5",
|
|
131
131
|
* systemPrompt: `You are a customer support agent for ACME Corp.
|
|
132
132
|
*
|
|
133
133
|
* Company policies:
|