chore(deps): drop @langchain/classic; inline ChatBufferMemory + remove dead ChainFactory

Replaces BufferMemory/BufferWindowMemory from @langchain/classic with a minimal
in-tree ChatBufferMemory wrapping @langchain/core's InMemoryChatMessageHistory.
Deletes the deprecated ChainFactory (chain runners already call chat models
directly) and the chain-validation helpers in utils. Bumps @langchain/anthropic
to 1.3.29 so BedrockChatModel's tool-schema cast is no longer needed.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Zero Liu 2026-05-15 13:03:33 -07:00
parent 6a71cf8586
commit 0c1a0d605f
No known key found for this signature in database
15 changed files with 138 additions and 608 deletions

237
package-lock.json generated
View file

@ -12,8 +12,7 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@langchain/anthropic": "^1.0.0",
"@langchain/classic": "^1.0.9",
"@langchain/anthropic": "^1.3.29",
"@langchain/core": "^1.1.29",
"@langchain/deepseek": "^1.0.0",
"@langchain/google-genai": "^2.1.23",
@ -58,8 +57,6 @@
"zod": "^3.25.76"
},
"devDependencies": {
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.36.4",
"@eslint-react/eslint-plugin": "^1.38.4",
"@google/generative-ai": "^0.24.0",
"@jest/globals": "^29.7.0",
@ -123,6 +120,26 @@
"node": ">=6.0.0"
}
},
"node_modules/@anthropic-ai/sdk": {
"version": "0.91.1",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz",
"integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==",
"license": "MIT",
"dependencies": {
"json-schema-to-ts": "^3.1.1"
},
"bin": {
"anthropic-ai-sdk": "bin/cli"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/@babel/code-frame": {
"version": "7.26.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz",
@ -649,6 +666,7 @@
"integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@marijn/find-cluster-break": "^1.0.0"
}
@ -659,6 +677,7 @@
"integrity": "sha512-qiS0z1bKs5WOvHIAC0Cybmv4AJSkAXgX5aD6Mqd2epSLlVJsQl8NG23jCVouIgkh4All/mrbdsf2UOLFnJw0tw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"@codemirror/state": "^6.5.0",
"crelt": "^1.0.6",
@ -2323,137 +2342,39 @@
"dev": true
},
"node_modules/@langchain/anthropic": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.0.0.tgz",
"integrity": "sha512-Lud/FrkFmXMYW5R9y0FC+RGdgjBBVQ2JAnG3A8E1I4+sqv5JgJttw3HKRpFkyBUSyacs6LMfSn5dbJ6TT9nMiQ==",
"version": "1.3.29",
"resolved": "https://registry.npmjs.org/@langchain/anthropic/-/anthropic-1.3.29.tgz",
"integrity": "sha512-ep1qBIcV07bajsg3fDqMd39rYwoRLOEK/6lk+MCxlm1YB5SRoKKJAZANrblQ/4RYhZJnxf95c6BSQu8VoNbVAQ==",
"license": "MIT",
"dependencies": {
"@anthropic-ai/sdk": "^0.65.0"
},
"engines": {
"node": ">=20"
},
"peerDependencies": {
"@langchain/core": "^1.0.0"
}
},
"node_modules/@langchain/anthropic/node_modules/@anthropic-ai/sdk": {
"version": "0.65.0",
"resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.65.0.tgz",
"integrity": "sha512-zIdPOcrCVEI8t3Di40nH4z9EoeyGZfXbYSvWdDLsB/KkaSYMnEgC7gmcgWu83g2NTn1ZTpbMvpdttWDGGIk6zw==",
"license": "MIT",
"dependencies": {
"json-schema-to-ts": "^3.1.1"
},
"bin": {
"anthropic-ai-sdk": "bin/cli"
},
"peerDependencies": {
"zod": "^3.25.0 || ^4.0.0"
},
"peerDependenciesMeta": {
"zod": {
"optional": true
}
}
},
"node_modules/@langchain/classic": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/@langchain/classic/-/classic-1.0.9.tgz",
"integrity": "sha512-UQ6JFLvPPPHHq+c3bM3UjadsugYP013qzk6FMoULpj2oNsVU4Fd66R+V6p1R7gQQRmN4Y7DWRLePn2NzgVYFIA==",
"license": "MIT",
"dependencies": {
"@langchain/openai": "1.2.2",
"@langchain/textsplitters": "1.0.1",
"handlebars": "^4.7.8",
"js-yaml": "^4.1.1",
"jsonpointer": "^5.0.1",
"openapi-types": "^12.1.3",
"uuid": "^10.0.0",
"yaml": "^2.2.1",
"@anthropic-ai/sdk": "^0.91.1",
"zod": "^3.25.76 || ^4"
},
"engines": {
"node": ">=20"
},
"optionalDependencies": {
"langsmith": ">=0.4.0 <1.0.0"
},
"peerDependencies": {
"@langchain/core": "^1.0.0",
"cheerio": "*",
"peggy": "^3.0.2",
"typeorm": "*"
},
"peerDependenciesMeta": {
"cheerio": {
"optional": true
},
"peggy": {
"optional": true
},
"typeorm": {
"optional": true
}
}
},
"node_modules/@langchain/classic/node_modules/uuid": {
"version": "10.0.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-10.0.0.tgz",
"integrity": "sha512-8XkAphELsDnEGrDxUOHB3RGvXz6TeuYSGEZBOjtTtPm2lwhGBjLgOzLHB63IUWfBpNucQjND6d3AOudO+H3RWQ==",
"funding": [
"https://github.com/sponsors/broofa",
"https://github.com/sponsors/ctavan"
],
"license": "MIT",
"bin": {
"uuid": "dist/bin/uuid"
"@langchain/core": "^1.1.45"
}
},
"node_modules/@langchain/core": {
"version": "1.1.29",
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.29.tgz",
"integrity": "sha512-BPoegTtIdZX4gl2kxcSXAlLrrJFl1cxeRsk9DM/wlIuvyPrFwjWqrEK5NwF5diDt5XSArhQxIFaifGAl4F7fgw==",
"version": "1.1.46",
"resolved": "https://registry.npmjs.org/@langchain/core/-/core-1.1.46.tgz",
"integrity": "sha512-i8rDC83BpItxChCw4Lf+6tAr+k+OUcbirc5ZkrhI9ywYWmvxegUljLGOGYvtJNTbEAIFkhYIODPE5QRqyjF6sA==",
"license": "MIT",
"dependencies": {
"@cfworker/json-schema": "^4.0.2",
"ansi-styles": "^5.0.0",
"camelcase": "6",
"decamelize": "1.2.0",
"@standard-schema/spec": "^1.1.0",
"js-tiktoken": "^1.0.12",
"langsmith": ">=0.5.0 <1.0.0",
"mustache": "^4.2.0",
"p-queue": "^6.6.2",
"uuid": "^11.1.0",
"zod": "^3.25.76 || ^4"
},
"engines": {
"node": ">=20"
}
},
"node_modules/@langchain/core/node_modules/ansi-styles": {
"version": "5.2.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-5.2.0.tgz",
"integrity": "sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==",
"license": "MIT",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/chalk/ansi-styles?sponsor=1"
}
},
"node_modules/@langchain/core/node_modules/camelcase": {
"version": "6.3.0",
"resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz",
"integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==",
"engines": {
"node": ">=10"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/@langchain/core/node_modules/p-queue": {
"version": "6.6.2",
"resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz",
@ -2845,7 +2766,8 @@
"resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz",
"integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/@mixmark-io/domino": {
"version": "2.2.0",
@ -6169,6 +6091,12 @@
"@sinonjs/commons": "^2.0.0"
}
},
"node_modules/@standard-schema/spec": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz",
"integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==",
"license": "MIT"
},
"node_modules/@szmarczak/http-timer": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
@ -7149,7 +7077,8 @@
"node_modules/argparse": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==",
"dev": true
},
"node_modules/aria-hidden": {
"version": "1.2.4",
@ -8014,7 +7943,8 @@
"resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz",
"integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/cross-spawn": {
"version": "7.0.6",
@ -8215,14 +8145,6 @@
}
}
},
"node_modules/decamelize": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz",
"integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/decimal.js": {
"version": "10.4.3",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.3.tgz",
@ -10695,27 +10617,6 @@
"undici-types": "~5.26.4"
}
},
"node_modules/handlebars": {
"version": "4.7.8",
"resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.8.tgz",
"integrity": "sha512-vafaFqs8MZkRrSX7sFVUdo3ap/eNiLnb4IakshzvP56X5Nr1iGKAIqdX6tMlm6HcNRIkr6AxO5jFEoJzzpT8aQ==",
"license": "MIT",
"dependencies": {
"minimist": "^1.2.5",
"neo-async": "^2.6.2",
"source-map": "^0.6.1",
"wordwrap": "^1.0.0"
},
"bin": {
"handlebars": "bin/handlebars"
},
"engines": {
"node": ">=0.4.7"
},
"optionalDependencies": {
"uglify-js": "^3.1.4"
}
},
"node_modules/has-bigints": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.0.2.tgz",
@ -12746,6 +12647,7 @@
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz",
"integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==",
"dev": true,
"license": "MIT",
"dependencies": {
"argparse": "^2.0.1"
@ -12929,15 +12831,6 @@
"graceful-fs": "^4.1.6"
}
},
"node_modules/jsonpointer": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-5.0.1.tgz",
"integrity": "sha512-p/nXbhSEcu3pZRdkW1OfJhpsVtW1gd4Wa1fnQc9YLiTfAjn0312eMKimbdIQzuZl9aa9xUGaRlP9T/CJE/ditQ==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/jsx-ast-utils": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-3.3.5.tgz",
@ -13359,6 +13252,7 @@
"version": "1.2.8",
"resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
"integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
"dev": true,
"license": "MIT",
"funding": {
"url": "https://github.com/sponsors/ljharb"
@ -13459,12 +13353,6 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true
},
"node_modules/neo-async": {
"version": "2.6.2",
"resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz",
"integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==",
"license": "MIT"
},
"node_modules/node-domexception": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz",
@ -13931,12 +13819,6 @@
}
}
},
"node_modules/openapi-types": {
"version": "12.1.3",
"resolved": "https://registry.npmjs.org/openapi-types/-/openapi-types-12.1.3.tgz",
"integrity": "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw==",
"license": "MIT"
},
"node_modules/optionator": {
"version": "0.9.4",
"resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@ -15331,6 +15213,7 @@
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"engines": {
"node": ">=0.10.0"
}
@ -15608,7 +15491,8 @@
"resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz",
"integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==",
"dev": true,
"license": "MIT"
"license": "MIT",
"peer": true
},
"node_modules/sucrase": {
"version": "3.35.0",
@ -16338,19 +16222,6 @@
"typescript": ">=4.8.4 <6.1.0"
}
},
"node_modules/uglify-js": {
"version": "3.19.3",
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.19.3.tgz",
"integrity": "sha512-v3Xu+yuwBXisp6QYTcH4UbH+xYJXqnq2m/LtQVWKWzYc1iehYnLixoQDN9FH6/j9/oybfd6W9Ghwkl8+UMKTKQ==",
"license": "BSD-2-Clause",
"optional": true,
"bin": {
"uglifyjs": "bin/uglifyjs"
},
"engines": {
"node": ">=0.8.0"
}
},
"node_modules/unbox-primitive": {
"version": "1.1.0",
"resolved": "https://registry.npmjs.org/unbox-primitive/-/unbox-primitive-1.1.0.tgz",
@ -16513,7 +16384,8 @@
"version": "2.2.6",
"resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.6.tgz",
"integrity": "sha512-f+fciywl1SJEniZHD6H+kUO8gOnwIr7f4ijKA6+ZvJFjeGi1r4PDLl53Ayud9O/rk64RqgoQine0feoeOU0kXg==",
"dev": true
"dev": true,
"peer": true
},
"node_modules/w3c-xmlserializer": {
"version": "4.0.0",
@ -16707,12 +16579,6 @@
"node": ">=0.10.0"
}
},
"node_modules/wordwrap": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz",
"integrity": "sha512-gvVzJFlPycKc5dZN4yPkP8w7Dc37BtP1yczEneOb4uq34pXZcvrtRTmWV8W+Ume+XCxKgbjM+nevkyFPMybd4Q==",
"license": "MIT"
},
"node_modules/wrap-ansi": {
"version": "7.0.0",
"resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz",
@ -16822,6 +16688,7 @@
"version": "2.9.0",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz",
"integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==",
"dev": true,
"license": "ISC",
"bin": {
"yaml": "bin.mjs"

View file

@ -33,8 +33,6 @@
},
"license": "AGPL-3.0",
"devDependencies": {
"@codemirror/state": "^6.5.2",
"@codemirror/view": "^6.36.4",
"@eslint-react/eslint-plugin": "^1.38.4",
"@google/generative-ai": "^0.24.0",
"@jest/globals": "^29.7.0",
@ -76,8 +74,7 @@
"@dnd-kit/core": "^6.3.1",
"@dnd-kit/sortable": "^10.0.0",
"@dnd-kit/utilities": "^3.2.2",
"@langchain/anthropic": "^1.0.0",
"@langchain/classic": "^1.0.9",
"@langchain/anthropic": "^1.3.29",
"@langchain/core": "^1.1.29",
"@langchain/deepseek": "^1.0.0",
"@langchain/google-genai": "^2.1.23",

View file

@ -142,9 +142,7 @@ export class BedrockChatModel extends BaseChatModel<BedrockChatModelCallOptions>
let inputSchema: Record<string, unknown> = { type: "object", properties: {} };
if (tool.schema) {
// Use LangChain's schema conversion utilities
inputSchema = (
isInteropZodSchema(tool.schema) ? toJsonSchema(tool.schema) : tool.schema
) as Record<string, unknown>;
inputSchema = isInteropZodSchema(tool.schema) ? toJsonSchema(tool.schema) : tool.schema;
}
return {
name: tool.name,

View file

@ -5,7 +5,6 @@ import {
SetChainOptions,
setChainType,
} from "@/aiParams";
import ChainFactory, { Document } from "@/chainFactory";
import { ChainType } from "@/chainType";
import { BUILTIN_CHAT_MODELS, USER_SENDER } from "@/constants";
import {
@ -20,14 +19,14 @@ import { logError, logInfo } from "@/logger";
import { getSettings, subscribeToSettingsChange } from "@/settings/model";
import { getSystemPrompt } from "@/system-prompts/systemPromptBuilder";
import { ChatMessage } from "@/types/message";
import { findCustomModel, isOSeriesModel, isSupportedChain } from "@/utils";
import { findCustomModel, isOSeriesModel } from "@/utils";
import { MissingModelKeyError } from "@/error";
import {
ChatPromptTemplate,
HumanMessagePromptTemplate,
MessagesPlaceholder,
} from "@langchain/core/prompts";
import { RunnableSequence } from "@langchain/core/runnables";
import { Document } from "@langchain/core/documents";
import { App, Notice } from "obsidian";
import ChatModelManager from "./chatModelManager";
import MemoryManager from "./memoryManager";
@ -35,10 +34,6 @@ import PromptManager from "./promptManager";
import { UserMemoryManager } from "@/memory/UserMemoryManager";
export default class ChainManager {
// TODO: These chains are deprecated since we now use direct chat model calls in chain runners
// Consider removing after verifying no dependencies remain
private chain: RunnableSequence;
private retrievalChain: RunnableSequence;
private retrievedDocuments: Document[] = [];
public getRetrievedDocuments(): Document[] {
@ -74,16 +69,6 @@ export default class ChainManager {
await this.createChainWithNewModel();
}
// TODO: These methods are deprecated - chain runners now use direct chat model calls
// Remove after confirming no usage remains
public getChain(): RunnableSequence {
return this.chain;
}
public getRetrievalChain(): RunnableSequence {
return this.retrievalChain;
}
private validateChainType(chainType: ChainType): void {
if (chainType === undefined || chainType === null) throw new Error("No chain type set");
}
@ -100,17 +85,6 @@ export default class ChainManager {
}
}
// TODO: This method is deprecated - chain validation no longer needed
// Remove after confirming no dependencies
private validateChainInitialization() {
if (!this.chain || !isSupportedChain(this.chain)) {
logInfo("Reinitializing chat chain after detecting missing or unsupported instance.");
void this.createChainWithNewModel({}, false).catch((err) =>
logError("createChainWithNewModel failed", err)
);
}
}
public storeRetrieverDocuments(documents: Document[]) {
this.retrievedDocuments = documents;
}
@ -175,9 +149,6 @@ export default class ChainManager {
this.pendingModelError = null;
}
// Must update the chatModel for chain because ChainFactory always
// retrieves the old chain without the chatModel change if it exists!
// Create a new chain with the new chatModel
await this.setChain(chainType, options);
logInfo(`Setting model to ${newModelKey}`);
} catch (error) {
@ -187,8 +158,6 @@ export default class ChainManager {
}
}
// TODO: This method is deprecated - chain runners now handle chain logic directly
// Remove after confirming no usage remains
async setChain(chainType: ChainType, options: SetChainOptions = {}): Promise<void> {
if (!this.chatModelManager.validateChatModel(this.chatModelManager.getChatModel())) {
console.error("setChain failed: No chat model set.");
@ -197,98 +166,15 @@ export default class ChainManager {
this.validateChainType(chainType);
// Get chatModel, memory, prompt, and embeddingAPI from respective managers
const chatModel = this.chatModelManager.getChatModel();
const memory = this.memoryManager.getMemory();
const chatPrompt = this.promptManager.getChatPrompt();
switch (chainType) {
case ChainType.LLM_CHAIN: {
// TODO: LLMChainRunner now handles this directly without chains
this.chain = ChainFactory.createNewLLMChain({
llm: chatModel,
memory: memory,
prompt: options.prompt || chatPrompt,
abortController: options.abortController,
});
setChainType(ChainType.LLM_CHAIN);
break;
}
case ChainType.VAULT_QA_CHAIN: {
// TODO: VaultQAChainRunner now handles this directly without chains
await this.initializeQAChain(options);
// Create retriever based on semantic search setting
const settings = getSettings();
const retriever = settings.enableSemanticSearchV3
? new (await import("@/search/hybridRetriever")).HybridRetriever({
minSimilarityScore: 0.01,
maxK: settings.maxSourceChunks,
salientTerms: [],
})
: new (await import("@/search/v3/TieredLexicalRetriever")).TieredLexicalRetriever(app, {
minSimilarityScore: 0.01,
maxK: settings.maxSourceChunks,
salientTerms: [],
textWeight: undefined,
returnAll: false,
useRerankerThreshold: undefined,
});
// Create new conversational retrieval chain
this.retrievalChain = ChainFactory.createConversationalRetrievalChain(
{
llm: chatModel,
retriever: retriever,
systemMessage: getSystemPrompt(),
},
this.storeRetrieverDocuments.bind(this) as (
documents: import("@langchain/core/documents").Document[]
) => void,
getSettings().debug
);
setChainType(ChainType.VAULT_QA_CHAIN);
if (getSettings().debug) {
logInfo("New Vault QA chain with hybrid retriever created for entire vault");
logInfo("Set chain:", ChainType.VAULT_QA_CHAIN);
}
break;
}
case ChainType.COPILOT_PLUS_CHAIN: {
// For initial load of the plugin
await this.initializeQAChain(options);
this.chain = ChainFactory.createNewLLMChain({
llm: chatModel,
memory: memory,
prompt: options.prompt || chatPrompt,
abortController: options.abortController,
});
setChainType(ChainType.COPILOT_PLUS_CHAIN);
break;
}
case ChainType.PROJECT_CHAIN: {
// For initial load of the plugin
await this.initializeQAChain(options);
this.chain = ChainFactory.createNewLLMChain({
llm: chatModel,
memory: memory,
prompt: options.prompt || chatPrompt,
abortController: options.abortController,
});
setChainType(ChainType.PROJECT_CHAIN);
break;
}
default:
this.validateChainType(chainType);
break;
if (
chainType === ChainType.VAULT_QA_CHAIN ||
chainType === ChainType.COPILOT_PLUS_CHAIN ||
chainType === ChainType.PROJECT_CHAIN
) {
await this.initializeQAChain(options);
}
setChainType(chainType);
}
private getChainRunner(): ChainRunner {
@ -346,7 +232,6 @@ export default class ChainManager {
);
this.validateChatModel();
this.validateChainInitialization();
const chatModel = this.chatModelManager.getChatModel();

View file

@ -0,0 +1,58 @@
import { BaseChatMessageHistory, InMemoryChatMessageHistory } from "@langchain/core/chat_history";
import { BaseMessage } from "@langchain/core/messages";
export interface ChatBufferMemoryOptions {
k?: number;
memoryKey?: string;
inputKey?: string;
returnMessages?: boolean;
chatHistory?: BaseChatMessageHistory;
}
/**
* Minimal in-process replacement for `BufferMemory` / `BufferWindowMemory`
* from `@langchain/classic/memory`. Implements the narrow API surface used
* by the project (loadMemoryVariables / saveContext / clear / chatHistory)
* so we can drop the `@langchain/classic` dep.
*/
export class ChatBufferMemory {
readonly chatHistory: BaseChatMessageHistory;
private readonly k?: number;
private readonly memoryKey: string;
private readonly inputKey?: string;
constructor(options: ChatBufferMemoryOptions = {}) {
this.k = options.k;
this.memoryKey = options.memoryKey ?? "history";
this.inputKey = options.inputKey;
this.chatHistory = options.chatHistory ?? new InMemoryChatMessageHistory();
}
async loadMemoryVariables(
_: Record<string, unknown> = {}
): Promise<Record<string, BaseMessage[]>> {
const messages = await this.chatHistory.getMessages();
const windowed =
this.k !== undefined && messages.length > this.k ? messages.slice(-this.k) : messages;
return { [this.memoryKey]: windowed };
}
async saveContext(
input: Record<string, unknown>,
output: Record<string, unknown>
): Promise<void> {
const inputKey = this.inputKey ?? Object.keys(input)[0];
const inputValue = input[inputKey];
const outputValue = output[Object.keys(output)[0]];
await this.chatHistory.addUserMessage(stringify(inputValue));
await this.chatHistory.addAIMessage(stringify(outputValue));
}
async clear(): Promise<void> {
await this.chatHistory.clear();
}
}
function stringify(value: unknown): string {
return typeof value === "string" ? value : JSON.stringify(value);
}

View file

@ -1,12 +1,12 @@
import { compactAssistantOutput } from "@/context/ChatHistoryCompactor";
import { logInfo } from "@/logger";
import { getSettings, subscribeToSettingsChange } from "@/settings/model";
import { BaseChatMemory, BufferWindowMemory } from "@langchain/classic/memory";
import { BaseChatMessageHistory } from "@langchain/core/chat_history";
import { ChatBufferMemory } from "./chatBufferMemory";
export default class MemoryManager {
private static instance: MemoryManager;
private memory: BaseChatMemory;
private memory: ChatBufferMemory;
private debug: boolean;
private constructor() {
@ -27,7 +27,7 @@ export default class MemoryManager {
private initMemory(chatHistory?: BaseChatMessageHistory): void {
const chatContextTurns = getSettings().contextTurns;
this.memory = new BufferWindowMemory({
this.memory = new ChatBufferMemory({
k: chatContextTurns * 2,
memoryKey: "history",
inputKey: "input",
@ -39,7 +39,7 @@ export default class MemoryManager {
}
}
getMemory(): BaseChatMemory {
getMemory(): ChatBufferMemory {
return this.memory;
}

View file

@ -1,208 +0,0 @@
// TODO(logan): This entire file is deprecated since we moved to direct chat model calls in chain runners
// Consider removing after verifying no dependencies remain
import { BaseLanguageModel } from "@langchain/core/language_models/base";
import { StringOutputParser } from "@langchain/core/output_parsers";
import { ChatPromptTemplate, PromptTemplate } from "@langchain/core/prompts";
import { BaseRetriever } from "@langchain/core/retrievers";
import { RunnablePassthrough, RunnableSequence } from "@langchain/core/runnables";
import { BaseChatMemory } from "@langchain/classic/memory";
import { formatDocumentsAsString } from "@langchain/classic/util/document";
import { ChainType } from "./chainType";
import { logInfo } from "./logger";
import { removeErrorTags, removeThinkTags } from "./utils";
export interface LLMChainInput {
llm: BaseLanguageModel;
memory: BaseChatMemory;
prompt: ChatPromptTemplate;
abortController?: AbortController;
}
export interface RetrievalChainParams {
llm: BaseLanguageModel;
retriever: BaseRetriever;
options?: {
returnSourceDocuments?: boolean;
};
}
export interface ConversationalRetrievalChainParams {
llm: BaseLanguageModel;
retriever: BaseRetriever;
systemMessage: string;
options?: {
returnSourceDocuments?: boolean;
questionGeneratorTemplate?: string;
qaTemplate?: string;
};
}
export interface Document<T = Record<string, unknown>> {
// Structure of Document, possibly including pageContent, metadata, etc.
pageContent: string;
metadata: T;
}
type ConversationalRetrievalQAChainInput = {
question: string;
chat_history: [string, string][];
};
// Issue where conversational retrieval chain gives rephrased question
// when streaming: https://github.com/hwchase17/langchainjs/issues/754#issuecomment-1540257078
// Temp workaround triggers CORS issue 'refused to set header user-agent'
class ChainFactory {
public static instances: Map<string, RunnableSequence> = new Map();
/**
* Create a new LLM chain using the provided LLMChainInput.
*
* @param {LLMChainInput} args - the input for creating the LLM chain
* @return {RunnableSequence} the newly created LLM chain
*/
public static createNewLLMChain(args: LLMChainInput): RunnableSequence {
const { llm, memory, prompt, abortController } = args;
const model = llm.withConfig({ signal: abortController?.signal });
const instance = RunnableSequence.from([
{
input: (initialInput: { input: unknown }) => initialInput.input,
memory: () => memory.loadMemoryVariables({}),
},
{
input: (previousOutput: { input: unknown; memory: { history: unknown } }) =>
previousOutput.input,
history: (previousOutput: { input: unknown; memory: { history: unknown } }) =>
previousOutput.memory.history,
},
prompt,
model,
]);
ChainFactory.instances.set(ChainType.LLM_CHAIN, instance);
logInfo("New LLM chain created.");
return instance;
}
/**
* Gets the LLM chain singleton from the map.
*
* @param {LLMChainInput} args - the input for the LLM chain
* @return {RunnableSequence} the LLM chain instance
*/
public static getLLMChainFromMap(args: LLMChainInput): RunnableSequence {
let instance = ChainFactory.instances.get(ChainType.LLM_CHAIN);
if (!instance) {
instance = ChainFactory.createNewLLMChain(args);
}
return instance;
}
/**
* Create a conversational retrieval chain with the given parameters. Not a singleton.
*
* Example invocation:
*
* ```ts
* const conversationalRetrievalChain = ChainFactory.createConversationalRetrievalChain({
* llm: model,
* retriever: retriever
* });
*
* const response = await conversationalRetrievalChain.invoke({
* question: "What are they made out of?",
* chat_history: [
* [
* "What is the powerhouse of the cell?",
* "The powerhouse of the cell is the mitochondria.",
* ],
* ],
* });
* ```
*
* @param {ConversationalRetrievalChainParams} args - the parameters for the retrieval chain
* @return {RunnableSequence} a new conversational retrieval chain
*/
public static createConversationalRetrievalChain(
args: ConversationalRetrievalChainParams,
onDocumentsRetrieved: (documents: Document[]) => void,
debug?: boolean
): RunnableSequence {
const { llm, retriever, systemMessage } = args;
// NOTE: This is a tricky part of the Conversational RAG. Weaker models may fail this instruction
// and lose the follow up question altogether.
const condenseQuestionTemplate = `Given the following conversation and a follow up question,
summarize the conversation as context and keep the follow up question unchanged, in its original language.
If the follow up question is unrelated to its preceding messages, return this follow up question directly.
If it is related, then combine the summary and the follow up question to construct a standalone question.
Make sure to keep any [[]] wrapped note titles in the question unchanged.
Chat History:
{chat_history}
Follow Up Input: {question}
Standalone question:`;
const CONDENSE_QUESTION_PROMPT = PromptTemplate.fromTemplate(condenseQuestionTemplate);
const answerTemplate = `{system_message}
Answer the question with as detailed as possible based only on the following context:
{context}
Question: {question}
`;
const ANSWER_PROMPT = PromptTemplate.fromTemplate(answerTemplate);
const formatChatHistory = (chatHistory: [string, string][]) => {
const formattedDialogueTurns = chatHistory.map(
(dialogueTurn) => `Human: ${dialogueTurn[0]}\nAssistant: ${dialogueTurn[1]}`
);
return formattedDialogueTurns.join("\n");
};
const standaloneQuestionChain = RunnableSequence.from([
{
question: (input: ConversationalRetrievalQAChainInput) => {
if (debug) logInfo("Input Question: ", input.question);
return input.question;
},
chat_history: (input: ConversationalRetrievalQAChainInput) => {
const formattedChatHistory = formatChatHistory(input.chat_history);
if (debug) logInfo("Formatted Chat History: ", formattedChatHistory);
return formattedChatHistory;
},
},
CONDENSE_QUESTION_PROMPT,
llm,
new StringOutputParser(),
(output) => {
const thinkTagsCleaned = removeThinkTags(output);
const cleanedOutput = removeErrorTags(thinkTagsCleaned);
if (debug) logInfo("Standalone Question: ", cleanedOutput);
return cleanedOutput;
},
]);
const formatDocumentsAsStringAndStore = async (documents: Document[]) => {
// Store or log documents for debugging
onDocumentsRetrieved(documents);
return formatDocumentsAsString(documents);
};
const answerChain = RunnableSequence.from([
{
context: retriever.pipe(formatDocumentsAsStringAndStore),
question: new RunnablePassthrough(),
system_message: () => systemMessage,
},
ANSWER_PROMPT,
llm,
]);
const conversationalRetrievalQAChain = standaloneQuestionChain.pipe(answerChain);
return conversationalRetrievalQAChain as RunnableSequence;
}
}
export default ChainFactory;

View file

@ -4,7 +4,6 @@
*/
import { RunnableSequence } from "@langchain/core/runnables";
import { BaseChatMemory, BufferMemory } from "@langchain/classic/memory";
import {
ChatPromptTemplate,
HumanMessagePromptTemplate,
@ -12,13 +11,14 @@ import {
SystemMessagePromptTemplate,
} from "@langchain/core/prompts";
import ChatModelManager from "@/LLMProviders/chatModelManager";
import { ChatBufferMemory } from "@/LLMProviders/chatBufferMemory";
import { CustomModel } from "@/aiParams";
/**
* Creates a new BufferMemory instance for chat history.
* Creates a new ChatBufferMemory instance for chat history.
*/
export function createChatMemory(): BufferMemory {
return new BufferMemory({
export function createChatMemory(): ChatBufferMemory {
return new ChatBufferMemory({
returnMessages: true,
memoryKey: "history",
});
@ -35,7 +35,7 @@ export function createChatMemory(): BufferMemory {
export async function createChatChain(
selectedModel: CustomModel,
systemPrompt: string,
memory: BaseChatMemory
memory: ChatBufferMemory
): Promise<RunnableSequence> {
const chatModel = await ChatModelManager.getInstance().createModelInstance(selectedModel);

View file

@ -1,13 +1,3 @@
// Mock chainFactory before importing anything else to avoid import errors
jest.mock("@/chainFactory", () => ({
ChainType: {
LLM_CHAIN: "llm_chain",
VAULT_QA_CHAIN: "vault_qa",
COPILOT_PLUS_CHAIN: "copilot_plus",
PROJECT_CHAIN: "project",
},
}));
import { ContextProcessor } from "@/contextProcessor";
import { DATAVIEW_BLOCK_TAG } from "@/constants";

View file

@ -1,11 +1,3 @@
jest.mock("@/chainFactory", () => ({
ChainType: {
LLM_CHAIN: "llm_chain",
COPILOT_PLUS_CHAIN: "copilot_plus",
PROJECT_CHAIN: "project_chain",
},
}));
import { ContextProcessor } from "@/contextProcessor";
import type { FileParserManager } from "@/tools/FileParserManager";
import { EMBEDDED_NOTE_TAG } from "@/constants";

View file

@ -12,14 +12,6 @@ jest.mock("@/chatUtils", () => ({
updateChatMemory: jest.fn(),
}));
jest.mock("@/chainFactory", () => ({
ChainType: {
LLM_CHAIN: "llm_chain",
COPILOT_PLUS_CHAIN: "copilot_plus_chain",
PROJECT_CHAIN: "project_chain",
},
}));
jest.mock("./ChatPersistenceManager", () => ({
ChatPersistenceManager: jest.fn().mockImplementation(() => ({
saveChat: jest.fn().mockResolvedValue({ success: true, path: "/test/path.md" }),

View file

@ -9,14 +9,6 @@
import { PromptContextEnvelope, PromptLayerSegment } from "@/context/PromptContextTypes";
// Minimal mocks to avoid deep dependency chains
jest.mock("@/chainFactory", () => ({
ChainType: {
LLM_CHAIN: "llm_chain",
COPILOT_PLUS_CHAIN: "copilot_plus_chain",
PROJECT_CHAIN: "project_chain",
},
}));
jest.mock("@/aiParams", () => ({
getSelectedTextContexts: jest.fn().mockReturnValue([]),
}));

View file

@ -12,7 +12,7 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { RunnableSequence } from "@langchain/core/runnables";
import type { BaseChatMemory } from "@langchain/classic/memory";
import type { ChatBufferMemory } from "@/LLMProviders/chatBufferMemory";
import type { CustomModel } from "@/aiParams";
import { createChatChain, createChatMemory } from "@/commands/customCommandChatEngine";
@ -70,7 +70,7 @@ export interface StreamingChatSessionApi {
reset: () => void;
/** Get the current memory instance (for saving partial context on stop). */
getMemory: () => BaseChatMemory | null;
getMemory: () => ChatBufferMemory | null;
/** Get the latest streaming text from ref (bypasses RAF throttle). */
getLatestStreamingText: () => string;
@ -78,7 +78,7 @@ export interface StreamingChatSessionApi {
interface ChainAndMemory {
chain: RunnableSequence;
memory: BaseChatMemory;
memory: ChatBufferMemory;
}
/** Returns a stable key for caching chain per model. */
@ -129,7 +129,7 @@ export function useStreamingChatSession(
onNonAbortErrorRef.current = onNonAbortError;
}, [onNoModel, onNonAbortError]);
const memoryRef = useRef<BaseChatMemory | null>(null);
const memoryRef = useRef<ChatBufferMemory | null>(null);
const chainRef = useRef<RunnableSequence | null>(null);
const currentModelKeyRef = useRef<string | null>(null);
const currentSystemPromptRef = useRef<string | null>(null);
@ -215,7 +215,7 @@ export function useStreamingChatSession(
return !hasSavedContextOnceRef.current;
}, []);
const getMemory = useCallback((): BaseChatMemory | null => {
const getMemory = useCallback((): ChatBufferMemory | null => {
return memoryRef.current;
}, []);
@ -283,7 +283,7 @@ export function useStreamingChatSession(
const thinkStreamer = new ThinkBlockStreamer(turnScopedDelta, excludeThinking);
let didNonAbortError = false;
let memory: BaseChatMemory | null = null;
let memory: ChatBufferMemory | null = null;
let prompt = "";
let committed: string | null = null;

View file

@ -86,19 +86,6 @@ Array.prototype.contains = Array.prototype.includes;
// Increase test timeout to 120 seconds for real LLM calls
jest.setTimeout(120000);
// Mock only the essential dependencies
jest.mock("@/chainFactory", () => ({
ChainType: {
LLM_CHAIN: "llm_chain",
VAULT_QA_CHAIN: "vault_qa",
COPILOT_PLUS_CHAIN: "copilot_plus",
PROJECT_CHAIN: "project",
},
default: jest.fn().mockImplementation(() => ({
instances: new Map(),
})),
}));
// Mock Obsidian - essential for tool initialization
jest.mock("obsidian", () => ({
App: jest.fn(),

View file

@ -3,7 +3,6 @@
// eslint-disable-next-line import/no-nodejs-modules
import { Buffer } from "buffer";
import { Document } from "@/chainFactory";
import { ChainType } from "@/chainType";
import {
ALLOWED_NOTE_CONTEXT_EXTENSIONS,
@ -19,9 +18,8 @@ import {
import { logInfo, logWarn } from "@/logger";
import { CopilotSettings } from "@/settings/model";
import { BaseChatModel } from "@langchain/core/language_models/chat_models";
import { Document } from "@langchain/core/documents";
import { MemoryVariables } from "@langchain/core/memory";
import { RunnableSequence } from "@langchain/core/runnables";
import { BaseChain, RetrievalQAChain } from "@langchain/classic/chains";
import { DateTime } from "luxon";
import { MarkdownView, Notice, TFile, Vault, normalizePath, requestUrl } from "obsidian";
import { CustomModel } from "./aiParams";
@ -254,24 +252,6 @@ export function getNotesFromTags(vault: Vault, tags: string[], noteFiles?: TFile
return filesWithTag;
}
// TODO: These chain validation functions are deprecated
// Remove after confirming chainManager no longer uses them
const isLLMChain = (chain: RunnableSequence): chain is RunnableSequence => {
const c = chain as unknown as Record<string, unknown>;
const last = c.last as Record<string, unknown> | undefined;
return Boolean(last?.modelName || last?.model);
};
const isRetrievalQAChain = (chain: BaseChain): chain is RetrievalQAChain => {
const c = chain as unknown as Record<string, unknown>;
const last = c.last as Record<string, unknown> | undefined;
return last?.retriever !== undefined;
};
export const isSupportedChain = (chain: RunnableSequence): chain is RunnableSequence => {
return isLLMChain(chain) || isRetrievalQAChain(chain);
};
export interface FormattedDateTime {
fileName: string;
display: string;