Files
anything-llm/server/utils/agents/aibitat/plugins/chat-history.js
2026-04-24 10:32:49 -07:00

163 lines
6.4 KiB
JavaScript

const { WorkspaceChats } = require("../../../../models/workspaceChats");
/**
* Plugin to save chat history to AnythingLLM DB.
*/
const chatHistory = {
name: "chat-history",
startupConfig: {
params: {},
},
plugin: function () {
return {
name: this.name,
setup: function (aibitat) {
// If the agent is aborted (e.g. user sent /reset mid-response), skip
// the pending save so a completing in-flight response doesn't reappear.
aibitat.onAbort(() => {
aibitat._aborted = true;
});
// pre-register a workspace chat ID to secure it in the DB
aibitat.onMessage(async (message) => {
if (message.from !== "USER") return;
/**
* If we don't have a tracked chat ID, we need to create a new one so we can upsert the response later.
* Normally, if this was a totally fresh chat from the user, we can assume that the message from the socket is
* the message we want to store for the prompt. However, if this is a regeneration of a previous message and that message
* called tools the history could include intermediate messages so need to search backwards to find the most recent user message
* as that is actually the prompt.
*/
if (!aibitat.trackedChatId) {
let userMessage = message.content;
if (userMessage.startsWith("@agent:")) {
const lastUserMsgIndex = aibitat._chats.findLastIndex(
(c) => c.from === "USER" && !c.content.startsWith("@agent:")
);
// When regenerating a message, we need to use the last user message as the prompt.
// Also prune the chats array to only include the messages before target prompt to re-run
// or else tool call results from the previous run will be included in the history and the model will not re-call tools
// that previously worked for the to-be-regenerated prompt.
if (lastUserMsgIndex !== -1) {
userMessage = aibitat._chats[lastUserMsgIndex].content;
aibitat._chats = aibitat._chats.slice(0, lastUserMsgIndex + 1);
}
}
const { chat } = await WorkspaceChats.new({
workspaceId: Number(aibitat.handlerProps.invocation.workspace_id),
user: { id: aibitat.handlerProps.invocation.user_id || null },
threadId: aibitat.handlerProps.invocation.thread_id || null,
include: false,
prompt: userMessage,
response: {},
});
if (chat) aibitat.registerChatId(chat.id);
}
});
aibitat.onMessage(async () => {
try {
if (aibitat._aborted) return;
const lastResponses = aibitat.chats.slice(-2);
if (lastResponses.length !== 2) return;
const [prev, last] = lastResponses;
// We need a full conversation reply with prev being from
// the USER and the last being from anyone other than the user.
if (prev.from !== "USER" || last.from === "USER") return;
// Extract attachments from user message if present
const attachments = prev.attachments || [];
// If we have a post-reply flow we should save the chat using this special flow
// so that post save cleanup and other unique properties can be run as opposed to regular chat.
if (aibitat.hasOwnProperty("_replySpecialAttributes")) {
await this._storeSpecial(aibitat, {
prompt: prev.content,
response: last.content,
attachments,
options: aibitat._replySpecialAttributes,
});
delete aibitat._replySpecialAttributes;
return;
}
await this._store(aibitat, {
prompt: prev.content,
response: last.content,
attachments,
});
} catch {}
});
},
_store: async function (
aibitat,
{ prompt, response, attachments = [] } = {}
) {
const invocation = aibitat.handlerProps.invocation;
const metrics = aibitat.provider?.getUsage?.() ?? {};
const citations = aibitat._pendingCitations ?? [];
const outputs = aibitat._pendingOutputs ?? [];
await WorkspaceChats.upsert(aibitat.trackedChatId, {
workspaceId: Number(invocation.workspace_id),
prompt,
response: {
text: response,
sources: citations,
type: "chat",
attachments,
metrics,
...(outputs.length > 0 ? { outputs } : {}),
},
user: { id: invocation?.user_id || null },
threadId: invocation?.thread_id || null,
include: true,
});
this._cleanup(aibitat);
},
_storeSpecial: async function (
aibitat,
{ prompt, response, attachments = [], options = {} } = {}
) {
const invocation = aibitat.handlerProps.invocation;
const metrics = aibitat.provider?.getUsage?.() ?? {};
const citations = aibitat._pendingCitations ?? [];
const outputs = aibitat._pendingOutputs ?? [];
const existingSources = options?.sources ?? [];
await WorkspaceChats.upsert(aibitat.trackedChatId, {
workspaceId: Number(invocation.workspace_id),
prompt,
response: {
sources: [...existingSources, ...citations],
// when we have a _storeSpecial called the options param can include a storedResponse() function
// that will override the text property to store extra information in, depending on the special type of chat.
text: options.hasOwnProperty("storedResponse")
? options.storedResponse(response)
: response,
type: options?.saveAsType ?? "chat",
attachments,
metrics,
...(outputs.length > 0 ? { outputs } : {}),
},
user: { id: invocation?.user_id || null },
threadId: invocation?.thread_id || null,
include: true,
});
options?.postSave();
this._cleanup(aibitat);
},
_cleanup: function (aibitat) {
aibitat.clearCitations?.();
aibitat._pendingOutputs = [];
aibitat.clearTrackedChatId();
},
};
},
};
module.exports = { chatHistory };