chat.ts 28 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931
  1. import {
  2. getMessageTextContent,
  3. isDalle3,
  4. safeLocalStorage,
  5. trimTopic,
  6. } from "../utils";
  7. import { indexedDBStorage } from "@/app/utils/indexedDB-storage";
  8. import { nanoid } from "nanoid";
  9. import type {
  10. ClientApi,
  11. MultimodalContent,
  12. RequestMessage,
  13. } from "../client/api";
  14. import { getClientApi } from "../client/api";
  15. import { ChatControllerPool } from "../client/controller";
  16. import { showToast } from "../components/ui-lib";
  17. import {
  18. DEFAULT_INPUT_TEMPLATE,
  19. DEFAULT_MODELS,
  20. DEFAULT_SYSTEM_TEMPLATE,
  21. GEMINI_SUMMARIZE_MODEL,
  22. DEEPSEEK_SUMMARIZE_MODEL,
  23. KnowledgeCutOffDate,
  24. MCP_SYSTEM_TEMPLATE,
  25. MCP_TOOLS_TEMPLATE,
  26. ServiceProvider,
  27. StoreKey,
  28. SUMMARIZE_MODEL,
  29. } from "../constant";
  30. import Locale, { getLang } from "../locales";
  31. import { prettyObject } from "../utils/format";
  32. import { createPersistStore } from "../utils/store";
  33. import { estimateTokenLength } from "../utils/token";
  34. import { ModelConfig, ModelType, useAppConfig } from "./config";
  35. import { useAccessStore } from "./access";
  36. import { collectModelsWithDefaultModel } from "../utils/model";
  37. import { createEmptyMask, Mask } from "./mask";
  38. import { executeMcpAction, getAllTools, isMcpEnabled } from "../mcp/actions";
  39. import { extractMcpJson, isMcpJson } from "../mcp/utils";
  40. const localStorage = safeLocalStorage();
  41. export type ChatMessageTool = {
  42. id: string;
  43. index?: number;
  44. type?: string;
  45. function?: {
  46. name: string;
  47. arguments?: string;
  48. };
  49. content?: string;
  50. isError?: boolean;
  51. errorMsg?: string;
  52. };
  53. export type ChatMessage = RequestMessage & {
  54. date: string;
  55. streaming?: boolean;
  56. isError?: boolean;
  57. id: string;
  58. model?: ModelType;
  59. tools?: ChatMessageTool[];
  60. audio_url?: string;
  61. isMcpResponse?: boolean;
  62. };
  63. export function createMessage(override: Partial<ChatMessage>): ChatMessage {
  64. return {
  65. id: nanoid(),
  66. date: new Date().toLocaleString(),
  67. role: "user",
  68. content: "",
  69. ...override,
  70. };
  71. }
  72. export interface ChatStat {
  73. tokenCount: number;
  74. wordCount: number;
  75. charCount: number;
  76. }
  77. export interface ChatSession {
  78. id: string;
  79. topic: string;
  80. memoryPrompt: string;
  81. messages: ChatMessage[];
  82. stat: ChatStat;
  83. lastUpdate: number;
  84. lastSummarizeIndex: number;
  85. clearContextIndex?: number;
  86. mask: Mask;
  87. }
  88. export const DEFAULT_TOPIC = Locale.Store.DefaultTopic;
  89. export const BOT_HELLO: ChatMessage = createMessage({
  90. role: "assistant",
  91. content: Locale.Store.BotHello,
  92. });
  93. function createEmptySession(): ChatSession {
  94. return {
  95. id: nanoid(),
  96. topic: DEFAULT_TOPIC,
  97. memoryPrompt: "",
  98. messages: [],
  99. stat: {
  100. tokenCount: 0,
  101. wordCount: 0,
  102. charCount: 0,
  103. },
  104. lastUpdate: Date.now(),
  105. lastSummarizeIndex: 0,
  106. mask: createEmptyMask(),
  107. };
  108. }
  109. function getSummarizeModel(
  110. currentModel: string,
  111. providerName: string,
  112. ): string[] {
  113. // if it is using gpt-* models, force to use 4o-mini to summarize
  114. if (currentModel.startsWith("gpt") || currentModel.startsWith("chatgpt")) {
  115. const configStore = useAppConfig.getState();
  116. const accessStore = useAccessStore.getState();
  117. const allModel = collectModelsWithDefaultModel(
  118. configStore.models,
  119. [configStore.customModels, accessStore.customModels].join(","),
  120. accessStore.defaultModel,
  121. );
  122. const summarizeModel = allModel.find(
  123. (m) => m.name === SUMMARIZE_MODEL && m.available,
  124. );
  125. if (summarizeModel) {
  126. return [
  127. summarizeModel.name,
  128. summarizeModel.provider?.providerName as string,
  129. ];
  130. }
  131. }
  132. if (currentModel.startsWith("gemini")) {
  133. return [GEMINI_SUMMARIZE_MODEL, ServiceProvider.Google];
  134. } else if (currentModel.startsWith("deepseek-")) {
  135. return [DEEPSEEK_SUMMARIZE_MODEL, ServiceProvider.DeepSeek];
  136. }
  137. return [currentModel, providerName];
  138. }
  139. function countMessages(msgs: ChatMessage[]) {
  140. return msgs.reduce(
  141. (pre, cur) => pre + estimateTokenLength(getMessageTextContent(cur)),
  142. 0,
  143. );
  144. }
  145. function fillTemplateWith(input: string, modelConfig: ModelConfig) {
  146. const cutoff =
  147. KnowledgeCutOffDate[modelConfig.model] ?? KnowledgeCutOffDate.default;
  148. // Find the model in the DEFAULT_MODELS array that matches the modelConfig.model
  149. const modelInfo = DEFAULT_MODELS.find((m) => m.name === modelConfig.model);
  150. var serviceProvider = "OpenAI";
  151. if (modelInfo) {
  152. // TODO: auto detect the providerName from the modelConfig.model
  153. // Directly use the providerName from the modelInfo
  154. serviceProvider = modelInfo.provider.providerName;
  155. }
  156. const vars = {
  157. ServiceProvider: serviceProvider,
  158. cutoff,
  159. model: modelConfig.model,
  160. time: new Date().toString(),
  161. lang: getLang(),
  162. input: input,
  163. };
  164. let output = modelConfig.template ?? DEFAULT_INPUT_TEMPLATE;
  165. // remove duplicate
  166. if (input.startsWith(output)) {
  167. output = "";
  168. }
  169. // must contains {{input}}
  170. const inputVar = "{{input}}";
  171. if (!output.includes(inputVar)) {
  172. output += "\n" + inputVar;
  173. }
  174. Object.entries(vars).forEach(([name, value]) => {
  175. const regex = new RegExp(`{{${name}}}`, "g");
  176. output = output.replace(regex, value.toString()); // Ensure value is a string
  177. });
  178. return output;
  179. }
  180. async function getMcpSystemPrompt(): Promise<string> {
  181. const tools = await getAllTools();
  182. let toolsStr = "";
  183. tools.forEach((i) => {
  184. // error client has no tools
  185. if (!i.tools) return;
  186. toolsStr += MCP_TOOLS_TEMPLATE.replace(
  187. "{{ clientId }}",
  188. i.clientId,
  189. ).replace(
  190. "{{ tools }}",
  191. i.tools.tools.map((p: object) => JSON.stringify(p, null, 2)).join("\n"),
  192. );
  193. });
  194. return MCP_SYSTEM_TEMPLATE.replace("{{ MCP_TOOLS }}", toolsStr);
  195. }
  196. const DEFAULT_CHAT_STATE = {
  197. sessions: [createEmptySession()],
  198. currentSessionIndex: 0,
  199. lastInput: "",
  200. };
  201. export const useChatStore = createPersistStore(
  202. DEFAULT_CHAT_STATE,
  203. (set, _get) => {
  204. function get() {
  205. return {
  206. ..._get(),
  207. ...methods,
  208. };
  209. }
  210. const methods = {
  211. forkSession() {
  212. // 获取当前会话
  213. const currentSession = get().currentSession();
  214. if (!currentSession) return;
  215. const newSession = createEmptySession();
  216. newSession.topic = currentSession.topic;
  217. // 深拷贝消息
  218. newSession.messages = currentSession.messages.map((msg) => ({
  219. ...msg,
  220. id: nanoid(), // 生成新的消息 ID
  221. }));
  222. newSession.mask = {
  223. ...currentSession.mask,
  224. modelConfig: {
  225. ...currentSession.mask.modelConfig,
  226. },
  227. };
  228. set((state) => ({
  229. currentSessionIndex: 0,
  230. sessions: [newSession, ...state.sessions],
  231. }));
  232. },
  233. clearSessions() {
  234. set(() => ({
  235. sessions: [createEmptySession()],
  236. currentSessionIndex: 0,
  237. }));
  238. },
  239. selectSession(index: number) {
  240. set({
  241. currentSessionIndex: index,
  242. });
  243. },
  244. moveSession(from: number, to: number) {
  245. set((state) => {
  246. const { sessions, currentSessionIndex: oldIndex } = state;
  247. // move the session
  248. const newSessions = [...sessions];
  249. const session = newSessions[from];
  250. newSessions.splice(from, 1);
  251. newSessions.splice(to, 0, session);
  252. // modify current session id
  253. let newIndex = oldIndex === from ? to : oldIndex;
  254. if (oldIndex > from && oldIndex <= to) {
  255. newIndex -= 1;
  256. } else if (oldIndex < from && oldIndex >= to) {
  257. newIndex += 1;
  258. }
  259. return {
  260. currentSessionIndex: newIndex,
  261. sessions: newSessions,
  262. };
  263. });
  264. },
  265. newSession(mask?: Mask) {
  266. const session = createEmptySession();
  267. if (mask) {
  268. const config = useAppConfig.getState();
  269. const globalModelConfig = config.modelConfig;
  270. session.mask = {
  271. ...mask,
  272. modelConfig: {
  273. ...globalModelConfig,
  274. ...mask.modelConfig,
  275. },
  276. };
  277. session.topic = mask.name;
  278. }
  279. set((state) => ({
  280. currentSessionIndex: 0,
  281. sessions: [session].concat(state.sessions),
  282. }));
  283. },
  284. nextSession(delta: number) {
  285. const n = get().sessions.length;
  286. const limit = (x: number) => (x + n) % n;
  287. const i = get().currentSessionIndex;
  288. get().selectSession(limit(i + delta));
  289. },
  290. deleteSession(index: number) {
  291. const deletingLastSession = get().sessions.length === 1;
  292. const deletedSession = get().sessions.at(index);
  293. if (!deletedSession) return;
  294. const sessions = get().sessions.slice();
  295. sessions.splice(index, 1);
  296. const currentIndex = get().currentSessionIndex;
  297. let nextIndex = Math.min(
  298. currentIndex - Number(index < currentIndex),
  299. sessions.length - 1,
  300. );
  301. if (deletingLastSession) {
  302. nextIndex = 0;
  303. sessions.push(createEmptySession());
  304. }
  305. // for undo delete action
  306. const restoreState = {
  307. currentSessionIndex: get().currentSessionIndex,
  308. sessions: get().sessions.slice(),
  309. };
  310. set(() => ({
  311. currentSessionIndex: nextIndex,
  312. sessions,
  313. }));
  314. showToast(
  315. Locale.Home.DeleteToast,
  316. {
  317. text: Locale.Home.Revert,
  318. onClick() {
  319. set(() => restoreState);
  320. },
  321. },
  322. 5000,
  323. );
  324. },
  325. currentSession() {
  326. let index = get().currentSessionIndex;
  327. const sessions = get().sessions;
  328. if (index < 0 || index >= sessions.length) {
  329. index = Math.min(sessions.length - 1, Math.max(0, index));
  330. set(() => ({ currentSessionIndex: index }));
  331. }
  332. const session = sessions[index];
  333. return session;
  334. },
  335. onNewMessage(message: ChatMessage, targetSession: ChatSession) {
  336. get().updateTargetSession(targetSession, (session) => {
  337. session.messages = session.messages.concat();
  338. session.lastUpdate = Date.now();
  339. });
  340. get().updateStat(message, targetSession);
  341. get().checkMcpJson(message);
  342. get().summarizeSession(false, targetSession);
  343. },
  344. async onUserInput(
  345. content: string,
  346. attachImages?: string[],
  347. isMcpResponse?: boolean,
  348. ) {
  349. const session = get().currentSession();
  350. const modelConfig = session.mask.modelConfig;
  351. // MCP Response no need to fill template
  352. let mContent: string | MultimodalContent[] = isMcpResponse
  353. ? content
  354. : fillTemplateWith(content, modelConfig);
  355. if (!isMcpResponse && attachImages && attachImages.length > 0) {
  356. mContent = [
  357. ...(content ? [{ type: "text" as const, text: content }] : []),
  358. ...attachImages.map((url) => ({
  359. type: "image_url" as const,
  360. image_url: { url },
  361. })),
  362. ];
  363. }
  364. let userMessage: ChatMessage = createMessage({
  365. role: "user",
  366. content: mContent,
  367. isMcpResponse,
  368. });
  369. const botMessage: ChatMessage = createMessage({
  370. role: "assistant",
  371. streaming: true,
  372. model: modelConfig.model,
  373. });
  374. // get recent messages
  375. const recentMessages = await get().getMessagesWithMemory();
  376. const sendMessages = recentMessages.concat(userMessage);
  377. const messageIndex = session.messages.length + 1;
  378. // save user's and bot's message
  379. get().updateTargetSession(session, (session) => {
  380. const savedUserMessage = {
  381. ...userMessage,
  382. content: mContent,
  383. };
  384. session.messages = session.messages.concat([
  385. savedUserMessage,
  386. botMessage,
  387. ]);
  388. });
  389. const api: ClientApi = getClientApi(modelConfig.providerName);
  390. // make request
  391. api.llm.chat({
  392. messages: sendMessages,
  393. config: { ...modelConfig, stream: true },
  394. onUpdate(message) {
  395. botMessage.streaming = true;
  396. if (message) {
  397. botMessage.content = message;
  398. }
  399. get().updateTargetSession(session, (session) => {
  400. session.messages = session.messages.concat();
  401. });
  402. },
  403. async onFinish(message) {
  404. botMessage.streaming = false;
  405. if (message) {
  406. botMessage.content = message;
  407. botMessage.date = new Date().toLocaleString();
  408. get().onNewMessage(botMessage, session);
  409. }
  410. ChatControllerPool.remove(session.id, botMessage.id);
  411. },
  412. onBeforeTool(tool: ChatMessageTool) {
  413. (botMessage.tools = botMessage?.tools || []).push(tool);
  414. get().updateTargetSession(session, (session) => {
  415. session.messages = session.messages.concat();
  416. });
  417. },
  418. onAfterTool(tool: ChatMessageTool) {
  419. botMessage?.tools?.forEach((t, i, tools) => {
  420. if (tool.id == t.id) {
  421. tools[i] = { ...tool };
  422. }
  423. });
  424. get().updateTargetSession(session, (session) => {
  425. session.messages = session.messages.concat();
  426. });
  427. },
  428. onError(error) {
  429. const isAborted = error.message?.includes?.("aborted");
  430. botMessage.content +=
  431. "\n\n" +
  432. prettyObject({
  433. error: true,
  434. message: error.message,
  435. });
  436. botMessage.streaming = false;
  437. userMessage.isError = !isAborted;
  438. botMessage.isError = !isAborted;
  439. get().updateTargetSession(session, (session) => {
  440. session.messages = session.messages.concat();
  441. });
  442. ChatControllerPool.remove(
  443. session.id,
  444. botMessage.id ?? messageIndex,
  445. );
  446. console.error("[Chat] failed ", error);
  447. },
  448. onController(controller) {
  449. // collect controller for stop/retry
  450. ChatControllerPool.addController(
  451. session.id,
  452. botMessage.id ?? messageIndex,
  453. controller,
  454. );
  455. },
  456. });
  457. },
  458. getMemoryPrompt() {
  459. const session = get().currentSession();
  460. if (session.memoryPrompt.length) {
  461. return {
  462. role: "system",
  463. content: Locale.Store.Prompt.History(session.memoryPrompt),
  464. date: "",
  465. } as ChatMessage;
  466. }
  467. },
  468. async getMessagesWithMemory() {
  469. const session = get().currentSession();
  470. const modelConfig = session.mask.modelConfig;
  471. const clearContextIndex = session.clearContextIndex ?? 0;
  472. const messages = session.messages.slice();
  473. const totalMessageCount = session.messages.length;
  474. // in-context prompts
  475. const contextPrompts = session.mask.context.slice();
  476. // system prompts, to get close to OpenAI Web ChatGPT
  477. const shouldInjectSystemPrompts =
  478. modelConfig.enableInjectSystemPrompts &&
  479. (session.mask.modelConfig.model.startsWith("gpt-") ||
  480. session.mask.modelConfig.model.startsWith("chatgpt-"));
  481. const mcpEnabled = await isMcpEnabled();
  482. const mcpSystemPrompt = mcpEnabled ? await getMcpSystemPrompt() : "";
  483. var systemPrompts: ChatMessage[] = [];
  484. if (shouldInjectSystemPrompts) {
  485. systemPrompts = [
  486. createMessage({
  487. role: "system",
  488. content:
  489. fillTemplateWith("", {
  490. ...modelConfig,
  491. template: DEFAULT_SYSTEM_TEMPLATE,
  492. }) + mcpSystemPrompt,
  493. }),
  494. ];
  495. } else if (mcpEnabled) {
  496. systemPrompts = [
  497. createMessage({
  498. role: "system",
  499. content: mcpSystemPrompt,
  500. }),
  501. ];
  502. }
  503. if (shouldInjectSystemPrompts || mcpEnabled) {
  504. console.log(
  505. "[Global System Prompt] ",
  506. systemPrompts.at(0)?.content ?? "empty",
  507. );
  508. }
  509. const memoryPrompt = get().getMemoryPrompt();
  510. // long term memory
  511. const shouldSendLongTermMemory =
  512. modelConfig.sendMemory &&
  513. session.memoryPrompt &&
  514. session.memoryPrompt.length > 0 &&
  515. session.lastSummarizeIndex > clearContextIndex;
  516. const longTermMemoryPrompts =
  517. shouldSendLongTermMemory && memoryPrompt ? [memoryPrompt] : [];
  518. const longTermMemoryStartIndex = session.lastSummarizeIndex;
  519. // short term memory
  520. const shortTermMemoryStartIndex = Math.max(
  521. 0,
  522. totalMessageCount - modelConfig.historyMessageCount,
  523. );
  524. // lets concat send messages, including 4 parts:
  525. // 0. system prompt: to get close to OpenAI Web ChatGPT
  526. // 1. long term memory: summarized memory messages
  527. // 2. pre-defined in-context prompts
  528. // 3. short term memory: latest n messages
  529. // 4. newest input message
  530. const memoryStartIndex = shouldSendLongTermMemory
  531. ? Math.min(longTermMemoryStartIndex, shortTermMemoryStartIndex)
  532. : shortTermMemoryStartIndex;
  533. // and if user has cleared history messages, we should exclude the memory too.
  534. const contextStartIndex = Math.max(clearContextIndex, memoryStartIndex);
  535. const maxTokenThreshold = modelConfig.max_tokens;
  536. // get recent messages as much as possible
  537. const reversedRecentMessages = [];
  538. for (
  539. let i = totalMessageCount - 1, tokenCount = 0;
  540. i >= contextStartIndex && tokenCount < maxTokenThreshold;
  541. i -= 1
  542. ) {
  543. const msg = messages[i];
  544. if (!msg || msg.isError) continue;
  545. tokenCount += estimateTokenLength(getMessageTextContent(msg));
  546. reversedRecentMessages.push(msg);
  547. }
  548. // concat all messages
  549. const recentMessages = [
  550. ...systemPrompts,
  551. ...longTermMemoryPrompts,
  552. ...contextPrompts,
  553. ...reversedRecentMessages.reverse(),
  554. ];
  555. return recentMessages;
  556. },
  557. updateMessage(
  558. sessionIndex: number,
  559. messageIndex: number,
  560. updater: (message?: ChatMessage) => void,
  561. ) {
  562. const sessions = get().sessions;
  563. const session = sessions.at(sessionIndex);
  564. const messages = session?.messages;
  565. updater(messages?.at(messageIndex));
  566. set(() => ({ sessions }));
  567. },
  568. resetSession(session: ChatSession) {
  569. get().updateTargetSession(session, (session) => {
  570. session.messages = [];
  571. session.memoryPrompt = "";
  572. });
  573. },
  574. summarizeSession(
  575. refreshTitle: boolean = false,
  576. targetSession: ChatSession,
  577. ) {
  578. const config = useAppConfig.getState();
  579. const session = targetSession;
  580. const modelConfig = session.mask.modelConfig;
  581. // skip summarize when using dalle3?
  582. if (isDalle3(modelConfig.model)) {
  583. return;
  584. }
  585. // if not config compressModel, then using getSummarizeModel
  586. const [model, providerName] = modelConfig.compressModel
  587. ? [modelConfig.compressModel, modelConfig.compressProviderName]
  588. : getSummarizeModel(
  589. session.mask.modelConfig.model,
  590. session.mask.modelConfig.providerName,
  591. );
  592. const api: ClientApi = getClientApi(providerName as ServiceProvider);
  593. // remove error messages if any
  594. const messages = session.messages;
  595. // should summarize topic after chating more than 50 words
  596. const SUMMARIZE_MIN_LEN = 50;
  597. if (
  598. (config.enableAutoGenerateTitle &&
  599. session.topic === DEFAULT_TOPIC &&
  600. countMessages(messages) >= SUMMARIZE_MIN_LEN) ||
  601. refreshTitle
  602. ) {
  603. const startIndex = Math.max(
  604. 0,
  605. messages.length - modelConfig.historyMessageCount,
  606. );
  607. const topicMessages = messages
  608. .slice(
  609. startIndex < messages.length ? startIndex : messages.length - 1,
  610. messages.length,
  611. )
  612. .concat(
  613. createMessage({
  614. role: "user",
  615. content: Locale.Store.Prompt.Topic,
  616. }),
  617. );
  618. api.llm.chat({
  619. messages: topicMessages,
  620. config: {
  621. model,
  622. stream: false,
  623. providerName,
  624. },
  625. onFinish(message, responseRes) {
  626. if (responseRes?.status === 200) {
  627. get().updateTargetSession(
  628. session,
  629. (session) =>
  630. (session.topic =
  631. message.length > 0 ? trimTopic(message) : DEFAULT_TOPIC),
  632. );
  633. }
  634. },
  635. });
  636. }
  637. const summarizeIndex = Math.max(
  638. session.lastSummarizeIndex,
  639. session.clearContextIndex ?? 0,
  640. );
  641. let toBeSummarizedMsgs = messages
  642. .filter((msg) => !msg.isError)
  643. .slice(summarizeIndex);
  644. const historyMsgLength = countMessages(toBeSummarizedMsgs);
  645. if (historyMsgLength > (modelConfig?.max_tokens || 4000)) {
  646. const n = toBeSummarizedMsgs.length;
  647. toBeSummarizedMsgs = toBeSummarizedMsgs.slice(
  648. Math.max(0, n - modelConfig.historyMessageCount),
  649. );
  650. }
  651. const memoryPrompt = get().getMemoryPrompt();
  652. if (memoryPrompt) {
  653. // add memory prompt
  654. toBeSummarizedMsgs.unshift(memoryPrompt);
  655. }
  656. const lastSummarizeIndex = session.messages.length;
  657. console.log(
  658. "[Chat History] ",
  659. toBeSummarizedMsgs,
  660. historyMsgLength,
  661. modelConfig.compressMessageLengthThreshold,
  662. );
  663. if (
  664. historyMsgLength > modelConfig.compressMessageLengthThreshold &&
  665. modelConfig.sendMemory
  666. ) {
  667. /** Destruct max_tokens while summarizing
  668. * this param is just shit
  669. **/
  670. const { max_tokens, ...modelcfg } = modelConfig;
  671. api.llm.chat({
  672. messages: toBeSummarizedMsgs.concat(
  673. createMessage({
  674. role: "system",
  675. content: Locale.Store.Prompt.Summarize,
  676. date: "",
  677. }),
  678. ),
  679. config: {
  680. ...modelcfg,
  681. stream: true,
  682. model,
  683. providerName,
  684. },
  685. onUpdate(message) {
  686. session.memoryPrompt = message;
  687. },
  688. onFinish(message, responseRes) {
  689. if (responseRes?.status === 200) {
  690. console.log("[Memory] ", message);
  691. get().updateTargetSession(session, (session) => {
  692. session.lastSummarizeIndex = lastSummarizeIndex;
  693. session.memoryPrompt = message; // Update the memory prompt for stored it in local storage
  694. });
  695. }
  696. },
  697. onError(err) {
  698. console.error("[Summarize] ", err);
  699. },
  700. });
  701. }
  702. },
  703. updateStat(message: ChatMessage, session: ChatSession) {
  704. get().updateTargetSession(session, (session) => {
  705. session.stat.charCount += message.content.length;
  706. // TODO: should update chat count and word count
  707. });
  708. },
  709. updateTargetSession(
  710. targetSession: ChatSession,
  711. updater: (session: ChatSession) => void,
  712. ) {
  713. const sessions = get().sessions;
  714. const index = sessions.findIndex((s) => s.id === targetSession.id);
  715. if (index < 0) return;
  716. updater(sessions[index]);
  717. set(() => ({ sessions }));
  718. },
  719. async clearAllData() {
  720. await indexedDBStorage.clear();
  721. localStorage.clear();
  722. location.reload();
  723. },
  724. setLastInput(lastInput: string) {
  725. set({
  726. lastInput,
  727. });
  728. },
  729. /** check if the message contains MCP JSON and execute the MCP action */
  730. checkMcpJson(message: ChatMessage) {
  731. const mcpEnabled = isMcpEnabled();
  732. if (!mcpEnabled) return;
  733. const content = getMessageTextContent(message);
  734. if (isMcpJson(content)) {
  735. try {
  736. const mcpRequest = extractMcpJson(content);
  737. if (mcpRequest) {
  738. console.debug("[MCP Request]", mcpRequest);
  739. executeMcpAction(mcpRequest.clientId, mcpRequest.mcp)
  740. .then((result) => {
  741. console.log("[MCP Response]", result);
  742. const mcpResponse =
  743. typeof result === "object"
  744. ? JSON.stringify(result)
  745. : String(result);
  746. get().onUserInput(
  747. `\`\`\`json:mcp-response:${mcpRequest.clientId}\n${mcpResponse}\n\`\`\``,
  748. [],
  749. true,
  750. );
  751. })
  752. .catch((error) => showToast("MCP execution failed", error));
  753. }
  754. } catch (error) {
  755. console.error("[Check MCP JSON]", error);
  756. }
  757. }
  758. },
  759. };
  760. return methods;
  761. },
  762. {
  763. name: StoreKey.Chat,
  764. version: 3.3,
  765. migrate(persistedState, version) {
  766. const state = persistedState as any;
  767. const newState = JSON.parse(
  768. JSON.stringify(state),
  769. ) as typeof DEFAULT_CHAT_STATE;
  770. if (version < 2) {
  771. newState.sessions = [];
  772. const oldSessions = state.sessions;
  773. for (const oldSession of oldSessions) {
  774. const newSession = createEmptySession();
  775. newSession.topic = oldSession.topic;
  776. newSession.messages = [...oldSession.messages];
  777. newSession.mask.modelConfig.sendMemory = true;
  778. newSession.mask.modelConfig.historyMessageCount = 4;
  779. newSession.mask.modelConfig.compressMessageLengthThreshold = 1000;
  780. newState.sessions.push(newSession);
  781. }
  782. }
  783. if (version < 3) {
  784. // migrate id to nanoid
  785. newState.sessions.forEach((s) => {
  786. s.id = nanoid();
  787. s.messages.forEach((m) => (m.id = nanoid()));
  788. });
  789. }
  790. // Enable `enableInjectSystemPrompts` attribute for old sessions.
  791. // Resolve issue of old sessions not automatically enabling.
  792. if (version < 3.1) {
  793. newState.sessions.forEach((s) => {
  794. if (
  795. // Exclude those already set by user
  796. !s.mask.modelConfig.hasOwnProperty("enableInjectSystemPrompts")
  797. ) {
  798. // Because users may have changed this configuration,
  799. // the user's current configuration is used instead of the default
  800. const config = useAppConfig.getState();
  801. s.mask.modelConfig.enableInjectSystemPrompts =
  802. config.modelConfig.enableInjectSystemPrompts;
  803. }
  804. });
  805. }
  806. // add default summarize model for every session
  807. if (version < 3.2) {
  808. newState.sessions.forEach((s) => {
  809. const config = useAppConfig.getState();
  810. s.mask.modelConfig.compressModel = config.modelConfig.compressModel;
  811. s.mask.modelConfig.compressProviderName =
  812. config.modelConfig.compressProviderName;
  813. });
  814. }
  815. // revert default summarize model for every session
  816. if (version < 3.3) {
  817. newState.sessions.forEach((s) => {
  818. const config = useAppConfig.getState();
  819. s.mask.modelConfig.compressModel = "";
  820. s.mask.modelConfig.compressProviderName = "";
  821. });
  822. }
  823. return newState as any;
  824. },
  825. },
  826. );