feat: new memory api (#2028)

Co-authored-by: Marcus Schiesser <mail@marcusschiesser.de>
This commit is contained in:
Huu Le
2025-07-01 09:30:49 +07:00
committed by GitHub
parent 9f745d1941
commit d578889e21
37 changed files with 2487 additions and 118 deletions
+5
View File
@@ -0,0 +1,5 @@
---
"@llamaindex/core": patch
---
Add new memory API
@@ -0,0 +1,121 @@
---
title: Memory
description: Manage conversation history and context with intelligent memory blocks
---
Memory provides intelligent conversation history management with automatic context window optimization for LLMs and long-term memory storage through configurable memory blocks.
## Overview
The Memory class handles:
- **Message Storage**: Store and retrieve conversation messages with different adapters
- **Context Management**: Automatically fit messages within LLM context windows. Balance short-term and long-term memory usage within token limits.
- **Memory Blocks**: Include predefined contextual information or process messages into long-term memory blocks for summarization or retrieval
- **Snapshots**: Save and restore memory state for persistence
## Basic Usage
```ts twoslash
import { openai } from "@llamaindex/openai";
import { agent } from "@llamaindex/workflow";
import { createMemory, staticBlock } from "llamaindex";
const llm = openai({ model: "gpt-4o-mini" });
// Create memory with predefined context
const memory = createMemory({
memoryBlocks: [
staticBlock({
content:
"The user is a software engineer who loves TypeScript and LlamaIndex.",
}),
],
});
// Create an agent with the memory
const workflow = agent({
name: "assistant",
llm,
memory,
});
const result1 = await workflow.run("Hi, my name is John. Do you know me?");
console.log("Response:", result1.data.result);
const result2 = await workflow.run("What is my name?");
console.log("Response:", result2.data.result);
// You can also manually get messages with transient messages:
const messages = await memory.getLLM(llm, [
{
role: "user",
content: "What is my name?", // This message will be included in the result and won't be stored in the memory
},
]);
// You can also put messages in Vercel format directly to the memory
await memory.add({
id: "1",
createdAt: new Date(),
role: "user",
content: "Hello!",
options: {
parts: [
{
type: "file",
data: "base64...",
mimeType: "image/png",
},
],
},
});
// and get it back in Vercel format
const messages = await memory.get({ type: "vercel" });
console.log(messages);
```
## Configuration Options
Configure memory behavior with `MemoryOptions`:
- `tokenLimit`: Maximum tokens for memory retrieval.
- `shortTermTokenLimitRatio`: Ratio of tokens for short-term vs long-term memory (default: 0.5)
- `customAdapters`: Custom message adapters for different message formats. LlamaIndex (ChatMessageAdapter) and Vercel (VercelMessageAdapter) are built-in adapters.
- `memoryBlocks`: Memory blocks for long-term storage
## Memory Blocks
Memory blocks hold contextual information that always included in the memory or long-term information that enriches the context of the conversation that can be included in priority order within token limits. The order of messages retrieved from getLLM() method are:
1. StaticMemoryBlock (always included)
2. LongTermMemoryBlock
3. ShortTermMemoryBlock
4. Transient messages
We provided some built-in memory blocks for you:
- [Static Memory Block](/docs/api/classes/StaticMemoryBlock): Keeps track of static, non-changing information
- [Fact Extraction Memory Block](/docs/api/classes/FactExtractionMemoryBlock) (long-term): Populates a list of facts extracted from the conversation when the messages are outside of Memory token limits. Check out [this example](https://github.com/run-llama/LlamaIndexTS/tree/main/examples/agents/memory/fact-extraction.ts) for the usage of the Fact Extraction Memory Block.
## Persistence with Snapshots
Save and restore memory state:
```ts twoslash
import { createMemory, loadMemory } from "llamaindex";
const memory = createMemory();
// Add some messages
await memory.add({ role: "user", content: "Hello!" });
// Create snapshot
const snapshot = memory.snapshot();
// Later, restore from the snapshot
const restoredMemory = loadMemory(snapshot);
```
Want to learn more about the Memory class? Check out our example codes in [Github](https://github.com/run-llama/LlamaIndexTS/tree/main/examples/agents/memory).
+36
View File
@@ -0,0 +1,36 @@
import { openai } from "@llamaindex/openai";
import { agent } from "@llamaindex/workflow";
import { createMemory, staticBlock } from "llamaindex";
// Simple example: Agent with Predefined Memory
async function simpleAgentMemoryExample() {
console.log("=== Simple Agent Memory Example ===");
const memory = createMemory({
memoryBlocks: [
staticBlock({
content:
"The user is a software engineer who loves TypeScript and LlamaIndex.",
}),
],
});
// Create agent workflow
const workflow = agent({
name: "assistant",
llm: openai({ model: "gpt-4.1-nano" }),
memory,
});
// Test - agent should remember John and the shopping cart context
console.log("\n--- Testing Memory Context ---");
const result = await workflow.run("Hi, my name is John. Do you know me?");
console.log("Assistant Response:", result.data.result);
const result2 = await workflow.run("What is my name?");
console.log("Assistant Response:", result2.data.result);
}
// Run the example
simpleAgentMemoryExample().catch(console.error);
+58
View File
@@ -0,0 +1,58 @@
import { openai } from "@llamaindex/openai";
import { createMemory } from "llamaindex";
// Example: Basic Memory Usage with Factory
async function basicMemoryExample() {
console.log("\n=== Example: Basic Memory Usage with Factory ===");
const memory = createMemory({ tokenLimit: 30 });
// Add messages to memory
await memory.add({
role: "user",
content: "Hi, my name is John and I'm a software engineer.",
});
await memory.add({
role: "assistant",
content: "Hello John! Nice to meet you. How can I help you today?",
});
await memory.add({
role: "user",
content: "I love working with TypeScript and React.",
});
// Not all messages are included because of token limit is set to 30
const llmMessages = await memory.getLLM();
console.log(
`\nLLM messages (${llmMessages.length} messages) limited by a small token limit:`,
);
llmMessages.forEach((msg, idx) => {
console.log(`${idx + 1}. ${msg.role}: ${msg.content}`);
});
// But the token limit above will be the window size of an LLM instance if you use getLLM with LLM
const llm = openai({ model: "gpt-4.1-mini" });
const llmMessagesWithLLM = await memory.getLLM(llm);
// Now all the messages are included because of the LLM window size of the model is much larger
console.log(
`\nLLM messages with LLM (${llmMessagesWithLLM.length} messages) limited by LLM window size:`,
);
llmMessagesWithLLM.forEach((msg, idx) => {
console.log(`${idx + 1}. ${msg.role}: ${msg.content}`);
});
}
// Main function
async function main() {
console.log("🧠 Basic Memory Factory Examples");
console.log("===============================");
try {
await basicMemoryExample();
} catch (error) {
console.error("Error running basic memory examples:", error);
}
}
main().catch(console.error);
+101
View File
@@ -0,0 +1,101 @@
import { openai } from "@llamaindex/openai";
import { createMemory, factExtractionBlock } from "llamaindex";
// Configure OpenAI
const llm = openai({ model: "gpt-4.1-mini" });
// Example: Memory with Fact Extraction
async function factExtractionMemoryExample() {
console.log("\n=== Memory with Fact Extraction ===");
// Create memory with a fact extraction
const memory = createMemory([], {
tokenLimit: 100,
shortTermTokenLimitRatio: 0.7, // 70% for short-term, 30% for long-term
memoryBlocks: [
factExtractionBlock({
id: "user-facts",
priority: 5,
llm: llm,
maxFacts: 10,
isLongTerm: true,
}),
],
});
// Simulate a conversation with facts
const conversationTurns = [
{
role: "user",
content: "Hi, I'm Sarah and I work as a data scientist at Google.",
},
{
role: "assistant",
content:
"Hello Sarah! It's great to meet you. Data science at Google must be exciting!",
},
{
role: "user",
content:
"Yes, I specialize in machine learning and natural language processing.",
},
{
role: "assistant",
content: "That's impressive! ML and NLP are fascinating fields.",
},
{
role: "user",
content:
"I have a PhD in Computer Science from Stanford, and I love hiking on weekends.",
},
{
role: "assistant",
content:
"Wow, Stanford PhD! And hiking is a great way to unwind from tech work.",
},
{
role: "user",
content: "I also have two cats named Whiskers and Mittens.",
},
{
role: "assistant",
content:
"Cats make wonderful companions! Whiskers and Mittens are cute names.",
},
];
// Add conversation turns to memory
console.log("Adding conversation to memory...");
for (const turn of conversationTurns) {
await memory.add(turn);
}
// Get messages - facts should be extracted and included
const messages = await memory.getLLM(llm);
console.log("\nMessages with extracted facts:");
messages.forEach((msg, idx) => {
console.log(`${idx + 1}. ${msg.role ?? "unknown"}: ${msg.content}`);
});
//Messages with extracted facts:
// 1. assistant: Cats make wonderful companions! Whiskers and Mittens are cute names.
// 2. user: I also have two cats named Whiskers and Mittens.
// 3. assistant: Wow, Stanford PhD! And hiking is a great way to unwind from tech work.
// 4. memory: Sarah works as a data scientist at Google
// Sarah specializes in machine learning and natural language processing
// Sarah has a PhD in Computer Science from Stanford
// Sarah enjoys hiking on weekends
}
// Main function
async function main() {
console.log("🧠 Fact Extraction Memory Example");
console.log("=================================");
try {
await factExtractionMemoryExample();
} catch (error) {
console.error("Error running fact extraction memory example:", error);
}
}
main().catch(console.error);
+62
View File
@@ -0,0 +1,62 @@
import { openai } from "@llamaindex/openai";
import { createMemory, staticBlock } from "llamaindex";
// Configure OpenAI
const llm = openai({ model: "gpt-4.1-mini" });
// Example: Memory with Static Blocks
async function staticMemoryBlockExample() {
console.log("\n=== Memory with Static Blocks ===");
console.log("- Memory always include static block");
console.log("- Memory cut off the messages within token limit\n");
// Create memory with a static block
const memory = createMemory([], {
tokenLimit: 30, // A small token limit which is not enough for the whole conversation below
memoryBlocks: [
staticBlock({
content:
"The user's name is John and he is a software engineer who loves TypeScript and LlamaIndex.",
}),
],
});
// Add some messages to the memory
await memory.add({
role: "user",
content: "What do you know about me?",
});
await memory.add({
role: "assistant",
content:
"Based on our conversation, I know you're John, a software engineer who enjoys working with TypeScript and LlamaIndex!",
});
await memory.add({
role: "user",
content: "Which language does LlamaIndex support?",
});
// Get messages
// static block will always be included
// only the last message will be included because of token limit set above
const messages = await memory.getLLM(llm);
messages.forEach((msg, idx) => {
console.log(`${idx + 1}. ${msg.role}: ${msg.content}`);
});
// Messages with static block:
// 1. user: The user's name is John and he is a software engineer who loves TypeScript and LlamaIndex.
// 2. user: Which language does LlamaIndex support?
}
// Main function
async function main() {
try {
await staticMemoryBlockExample();
} catch (error) {
console.error("Error running static memory blocks example:", error);
}
}
main().catch(console.error);
@@ -1,5 +1,5 @@
import { Anthropic } from "@llamaindex/anthropic";
import { ChatMemoryBuffer, SimpleChatEngine } from "llamaindex";
import { createMemory, SimpleChatEngine } from "llamaindex";
import { stdin as input, stdout as output } from "node:process";
import readline from "node:readline/promises";
@@ -9,14 +9,12 @@ import readline from "node:readline/promises";
model: "claude-3-7-sonnet",
});
// chatHistory will store all the messages in the conversation
const chatHistory = new ChatMemoryBuffer({
chatHistory: [
{
content: "You want to talk in rhymes.",
role: "system",
},
],
});
const chatHistory = createMemory([
{
content: "You want to talk in rhymes.",
role: "system",
},
]);
const chatEngine = new SimpleChatEngine({
llm,
memory: chatHistory,
+8 -13
View File
@@ -2,11 +2,7 @@ import { stdin as input, stdout as output } from "node:process";
import readline from "node:readline/promises";
import { OpenAI } from "@llamaindex/openai";
import {
ChatSummaryMemoryBuffer,
Settings,
SimpleChatEngine,
} from "llamaindex";
import { createMemory, Settings, SimpleChatEngine } from "llamaindex";
if (process.env.NODE_ENV === "development") {
Settings.callbackManager.on("llm-end", (event) => {
@@ -15,10 +11,13 @@ if (process.env.NODE_ENV === "development") {
}
async function main() {
// Set maxTokens to 75% of the context window size of 4096
// This will trigger the summarizer once the chat history reaches 25% of the context window size (1024 tokens)
const llm = new OpenAI({ model: "gpt-3.5-turbo", maxTokens: 4096 * 0.75 });
const chatHistory = new ChatSummaryMemoryBuffer({ llm });
const llm = new OpenAI({ model: "gpt-3.5-turbo" });
const chatHistory = createMemory([
{
content: "You are a helpful assistant.",
role: "system",
},
]);
const chatEngine = new SimpleChatEngine({ llm });
const rl = readline.createInterface({ input, output });
@@ -29,10 +28,6 @@ async function main() {
chatHistory,
stream: true,
});
if (chatHistory.getLastSummary()) {
// Print the summary of the conversation so far that is produced by the SummaryChatHistory
console.log(`Summary: ${chatHistory.getLastSummary()?.content}`);
}
for await (const chunk of stream) {
process.stdout.write(chunk.response);
}
+2
View File
@@ -152,6 +152,7 @@ export type AgentParamsBase<
/**
* Worker will schedule tasks and handle the task execution
* @deprecated Use agent instead.
*/
export abstract class AgentWorker<
AI extends LLM,
@@ -250,6 +251,7 @@ export abstract class AgentWorker<
/**
* Runner will manage the task execution and provide a high-level API for the user
* @deprecated Use agent instead.
*/
export abstract class AgentRunner<
AI extends LLM,
+3
View File
@@ -62,6 +62,9 @@ export class LLMAgentWorker extends AgentWorker<LLM> {
taskHandler = AgentRunner.defaultTaskHandler;
}
/**
* @deprecated Use agent instead.
*/
export class LLMAgent extends AgentRunner<LLM> {
constructor(params: LLMAgentParams<LLM>) {
validateAgentParams(params);
+2 -4
View File
@@ -1,5 +1,5 @@
import type { ChatMessage, MessageContent } from "../llms";
import type { BaseMemory } from "../memory";
import type { Memory } from "../memory";
import { EngineResponse } from "../schema";
export interface BaseChatEngineParams<
@@ -9,9 +9,7 @@ export interface BaseChatEngineParams<
/**
* Optional chat history if you want to customize the chat history.
*/
chatHistory?:
| ChatMessage<AdditionalMessageOptions>[]
| BaseMemory<AdditionalMessageOptions>;
chatHistory?: ChatMessage<AdditionalMessageOptions>[] | Memory;
}
export interface StreamingChatEngineParams<
@@ -1,7 +1,7 @@
import { wrapEventCaller } from "../decorator";
import { Settings } from "../global";
import type { ChatMessage, LLM, MessageContent, MessageType } from "../llms";
import { BaseMemory, ChatMemoryBuffer } from "../memory";
import { Memory, createMemory } from "../memory";
import type { BaseNodePostprocessor } from "../postprocessor";
import {
type ContextSystemPrompt,
@@ -23,7 +23,7 @@ import type { ContextGenerator } from "./type";
export type ContextChatEngineOptions = {
retriever: BaseRetriever;
chatModel?: LLM | undefined;
chatHistory?: ChatMessage[] | undefined;
chatHistory?: ChatMessage[] | Memory | undefined;
contextSystemPrompt?: ContextSystemPrompt | undefined;
nodePostprocessors?: BaseNodePostprocessor[] | undefined;
systemPrompt?: string | undefined;
@@ -37,18 +37,21 @@ export type ContextChatEngineOptions = {
*/
export class ContextChatEngine extends PromptMixin implements BaseChatEngine {
chatModel: LLM;
memory: BaseMemory;
memory: Memory;
contextGenerator: ContextGenerator & PromptMixin;
systemPrompt?: string | undefined;
get chatHistory() {
return this.memory.getMessages();
return this.memory.getLLM();
}
constructor(init: ContextChatEngineOptions) {
super();
this.chatModel = init.chatModel ?? Settings.llm;
this.memory = new ChatMemoryBuffer({ chatHistory: init?.chatHistory });
this.memory =
init?.chatHistory instanceof Memory
? init.chatHistory
: createMemory(init?.chatHistory ?? []);
this.contextGenerator = new DefaultContextGenerator({
retriever: init.retriever,
contextSystemPrompt: init?.contextSystemPrompt,
@@ -87,12 +90,9 @@ export class ContextChatEngine extends PromptMixin implements BaseChatEngine {
): Promise<EngineResponse | AsyncIterable<EngineResponse>> {
const { message, stream } = params;
const chatHistory = params.chatHistory
? new ChatMemoryBuffer({
chatHistory:
params.chatHistory instanceof BaseMemory
? await params.chatHistory.getMessages()
: params.chatHistory,
})
? params.chatHistory instanceof Memory
? params.chatHistory
: createMemory(params.chatHistory)
: this.memory;
const requestMessages = await this.prepareRequestMessages(
message,
@@ -110,7 +110,7 @@ export class ContextChatEngine extends PromptMixin implements BaseChatEngine {
initialValue: "",
reducer: (accumulator, part) => (accumulator += part.delta),
finished: (accumulator) => {
chatHistory.put({ content: accumulator, role: "assistant" });
void chatHistory.add({ content: accumulator, role: "assistant" });
},
}),
(r) => EngineResponse.fromChatResponseChunk(r, requestMessages.nodes),
@@ -120,26 +120,26 @@ export class ContextChatEngine extends PromptMixin implements BaseChatEngine {
messages: requestMessages.messages,
additionalChatOptions: params.chatOptions as object,
});
chatHistory.put(response.message);
await chatHistory.add(response.message);
return EngineResponse.fromChatResponse(response, requestMessages.nodes);
}
reset() {
this.memory.reset();
async reset() {
await this.memory.clear();
}
private async prepareRequestMessages(
message: MessageContent,
chatHistory: BaseMemory,
chatHistory: Memory,
) {
chatHistory.put({
await chatHistory.add({
content: message,
role: "user",
});
const textOnly = extractText(message);
const context = await this.contextGenerator.generate(textOnly);
const systemMessage = this.prependSystemPrompt(context.message);
const messages = await chatHistory.getMessages([systemMessage]);
const messages = await chatHistory.getLLM(this.chatModel, [systemMessage]);
return { nodes: context.nodes, messages };
}
@@ -1,5 +1,5 @@
import type { LLM } from "../llms";
import { BaseMemory, ChatMemoryBuffer } from "../memory";
import { createMemory, Memory } from "../memory";
import { EngineResponse } from "../schema";
import { streamConverter, streamReducer } from "../utils";
import type {
@@ -16,20 +16,16 @@ import { Settings } from "../global";
*/
export class SimpleChatEngine implements BaseChatEngine {
memory: BaseMemory;
memory: Memory;
llm: LLM;
get chatHistory() {
return this.memory.getMessages();
return this.memory.getLLM();
}
constructor(init?: Partial<SimpleChatEngine>) {
this.llm = init?.llm ?? Settings.llm;
this.memory =
init?.memory ??
new ChatMemoryBuffer({
llm: this.llm,
});
this.memory = init?.memory ?? createMemory();
}
chat(params: NonStreamingChatEngineParams): Promise<EngineResponse>;
@@ -43,19 +39,15 @@ export class SimpleChatEngine implements BaseChatEngine {
const { message, stream } = params;
const chatHistory = params.chatHistory
? new ChatMemoryBuffer({
llm: this.llm,
chatHistory:
params.chatHistory instanceof BaseMemory
? await params.chatHistory.getMessages()
: params.chatHistory,
})
? params.chatHistory instanceof Memory
? params.chatHistory
: createMemory(params.chatHistory)
: this.memory;
chatHistory.put({ content: message, role: "user" });
await chatHistory.add({ content: message, role: "user" });
if (stream) {
const stream = await this.llm.chat({
messages: await chatHistory.getMessages(),
messages: await chatHistory.getLLM(this.llm),
stream: true,
});
return streamConverter(
@@ -64,7 +56,7 @@ export class SimpleChatEngine implements BaseChatEngine {
initialValue: "",
reducer: (accumulator, part) => accumulator + part.delta,
finished: (accumulator) => {
chatHistory.put({ content: accumulator, role: "assistant" });
void chatHistory.add({ content: accumulator, role: "assistant" });
},
}),
EngineResponse.fromChatResponseChunk,
@@ -73,13 +65,13 @@ export class SimpleChatEngine implements BaseChatEngine {
const response = await this.llm.chat({
stream: false,
messages: await chatHistory.getMessages(),
messages: await chatHistory.getLLM(this.llm),
});
chatHistory.put(response.message);
await chatHistory.add(response.message);
return EngineResponse.fromChatResponse(response);
}
reset() {
this.memory.reset();
async reset() {
await this.memory.clear();
}
}
+7
View File
@@ -0,0 +1,7 @@
import type { MemoryMessage } from "../types";
export interface MessageAdapter<T, TMessageOptions extends object = object> {
fromMemory(message: MemoryMessage<TMessageOptions>): T;
toMemory(message: T): MemoryMessage<TMessageOptions>;
isCompatible(message: unknown): message is T;
}
+43
View File
@@ -0,0 +1,43 @@
import { randomUUID } from "@llamaindex/env";
import type { ChatMessage } from "../../llms";
import type { MemoryMessage } from "../types";
import { type MessageAdapter } from "./base";
export class ChatMessageAdapter<
AdditionalMessageOptions extends object = object,
> implements
MessageAdapter<
ChatMessage<AdditionalMessageOptions>,
AdditionalMessageOptions
>
{
fromMemory(
message: MemoryMessage<AdditionalMessageOptions>,
): ChatMessage<AdditionalMessageOptions> {
return {
content: message.content,
role: message.role,
options: message.options,
};
}
toMemory(
message: ChatMessage<AdditionalMessageOptions>,
): MemoryMessage<AdditionalMessageOptions> {
return {
id: randomUUID(),
createdAt: new Date(),
...message,
};
}
isCompatible(
message: unknown,
): message is ChatMessage<AdditionalMessageOptions> {
return !!(
message &&
typeof message === "object" &&
"role" in message &&
message.role &&
"content" in message
);
}
}
@@ -0,0 +1,3 @@
export * from "./base";
export * from "./chat";
export * from "./vercel";
+198
View File
@@ -0,0 +1,198 @@
import type {
ChatMessage,
MessageContent,
MessageContentDetail,
} from "../../llms";
import { extractText } from "../../utils";
import type { MemoryMessage } from "../types";
import type { MessageAdapter } from "./base";
// UIMessage from the vercel/ai package (external)
export type VercelMessage = {
id: string;
role: "system" | "user" | "assistant" | "data";
content: string;
createdAt?: Date | undefined;
annotations?: Array<unknown> | undefined;
parts: Array<{ type: string; [key: string]: unknown }>;
};
/**
* Utility class for converting between LlamaIndex ChatMessage and Vercel UI Message formats
*/
export class VercelMessageAdapter<
AdditionalMessageOptions extends object = object,
> implements MessageAdapter<VercelMessage, AdditionalMessageOptions>
{
/**
* Convert LlamaIndex ChatMessage to Vercel UI Message format
*/
fromMemory(memoryMessage: MemoryMessage<object>): VercelMessage {
const parts = this.convertMessageContentToVercelParts(
memoryMessage.content,
);
// Convert role to UI message role
let role: VercelMessage["role"];
switch (memoryMessage.role) {
case "system":
case "user":
case "assistant":
role = memoryMessage.role;
break;
case "memory":
role = "system";
break;
case "developer":
role = "user";
break;
default:
role = "user"; // Default fallback, should not happen
}
return {
id: memoryMessage.id,
role,
content: extractText(memoryMessage.content),
parts,
createdAt: memoryMessage.createdAt,
annotations: memoryMessage.annotations,
};
}
/**
* Convert Vercel UI Message to LlamaIndex ChatMessage format
*/
toMemory(uiMessage: VercelMessage): MemoryMessage<AdditionalMessageOptions> {
// Convert UI message role to MessageType
let role: ChatMessage["role"];
switch (uiMessage.role) {
case "system":
case "user":
case "assistant":
role = uiMessage.role;
break;
case "data":
role = "user"; // Map data role to user
break;
default:
role = "user"; // Default fallback, should not happen
}
// Convert parts to MessageContent
const content = this.convertVercelPartsToMessageContent(uiMessage.parts);
return {
id: uiMessage.id,
content: content ?? uiMessage.content,
role,
createdAt: uiMessage.createdAt,
annotations: uiMessage.annotations,
};
}
/**
* Validate if object matches VercelMessage structure
*/
isCompatible(message: unknown): message is VercelMessage {
return !!(
message &&
typeof message === "object" &&
"role" in message &&
"content" in message &&
"parts" in message
);
}
/**
* Convert UI parts to MessageContent
*/
private convertVercelPartsToMessageContent(
parts: VercelMessage["parts"],
): MessageContent | null {
if (parts.length === 0) {
return null;
}
const details: MessageContentDetail[] = [];
for (const part of parts) {
switch (part.type) {
case "file": {
details.push({
type: "file",
data: part.data as string,
mimeType: part.mimeType as string,
});
break;
}
default:
// For other part types, convert to text
details.push({
type: "text",
text: part.text as string,
});
break;
}
}
// If only one text detail, return as string
if (details.length === 1 && details[0]?.type === "text") {
return details[0].text;
}
return details;
}
/**
* Convert MessageContent to UI parts
*/
private convertMessageContentToVercelParts(
content: MessageContent,
): VercelMessage["parts"] {
if (typeof content === "string") {
return [
{
type: "text",
text: content,
},
];
}
const parts: VercelMessage["parts"] = [];
for (const detail of content) {
switch (detail.type) {
case "text":
parts.push({
type: "text",
text: detail.text,
});
break;
case "image_url":
parts.push({
type: "text",
text: `[Image URL: ${detail.image_url.url}]`,
});
break;
case "audio":
case "video":
case "image":
case "file":
parts.push({
type: "file",
data: detail.data,
mimeType: detail.type,
});
break;
default:
// For unknown types, create a text representation
parts.push({
type: "text",
text: JSON.stringify(detail),
});
}
}
return parts;
}
}
+50
View File
@@ -0,0 +1,50 @@
import { randomUUID } from "@llamaindex/env";
import type { MemoryMessage } from "../types";
export type MemoryBlockOptions = {
/**
* The id of the memory block.
*/
id?: string;
/**
* The priority of the memory block.
* Note: if priority is 0, the block content is always included in the memory context.
*/
priority: number;
/**
* Whether the memory block is long term.
* Default is true.
*/
isLongTerm?: boolean;
};
/**
* A base class for memory blocks.
*/
export abstract class BaseMemoryBlock<
TAdditionalMessageOptions extends object = object,
> {
public readonly id: string;
public readonly priority: number;
public readonly isLongTerm: boolean;
constructor(options: MemoryBlockOptions) {
this.id = options.id ?? `memory-block-${randomUUID()}`;
this.priority = options.priority;
this.isLongTerm = options.isLongTerm ?? true;
}
/**
* Pull the memory block content (async).
*
* @returns The memory block content as an array of ChatMessage.
*/
abstract get(): Promise<MemoryMessage<TAdditionalMessageOptions>[]>;
/**
* Store the messages in the memory block.
*/
abstract put(
messages: MemoryMessage<TAdditionalMessageOptions>[],
): Promise<void>;
}
+153
View File
@@ -0,0 +1,153 @@
import type { LLM, MessageType } from "../../llms";
import type { MemoryMessage } from "../types";
import { BaseMemoryBlock, type MemoryBlockOptions } from "./base";
const DEFAULT_EXTRACTION_PROMPT = `
You are a precise fact extraction system designed to identify key information from conversations.
CONVERSATION SEGMENT:
{{conversation}}
EXISTING FACTS:
{{existing_facts}}
INSTRUCTIONS:
1. Review the conversation segment provided above.
2. Extract specific, concrete facts the user has disclosed or important information discovered
3. Focus on factual information like preferences, personal details, requirements, constraints, or context
4. Do not include opinions, summaries, or interpretations - only extract explicit information
5. Do not duplicate facts that are already in the existing facts list
Respond with the new facts from the conversation segment using the following JSON format:
{
"facts": ["fact1", "fact2", "fact3", ...]
}
`;
const DEFAULT_SUMMARY_PROMPT = `
You are a precise fact condensing system designed to summarize facts in a concise manner.
EXISTING FACTS:
{{existing_facts}}
INSTRUCTIONS:
1. Review the current list of existing facts
2. Condense the facts into a more concise list, less than {{ max_facts }} facts
3. Focus on factual information like preferences, personal details, requirements, constraints, or context
4. Do not include opinions, summaries, or interpretations - only extract explicit information
5. Do not duplicate facts that are already in the existing facts list
Respond with the condensed facts using the following JSON format:
{
"facts": ["fact1", "fact2", "fact3", ...]
}
`;
/**
* The options for the fact extraction memory block.
*/
export type FactExtractionMemoryBlockOptions = {
/**
* The fact extraction model to use.
*/
llm: LLM;
/**
* The maximum number of facts to extract.
*/
maxFacts: number;
/**
* The prompt to use for fact extraction.
*/
extractionPrompt?: string;
/**
* The prompt to use for fact summary.
*/
summaryPrompt?: string;
} & MemoryBlockOptions & {
isLongTerm?: true;
};
/**
* A memory block that stores facts extracted from conversations.
*/
export class FactExtractionMemoryBlock<
TAdditionalMessageOptions extends object = object,
> extends BaseMemoryBlock<TAdditionalMessageOptions> {
private readonly llm: LLM;
private facts: string[] = [];
private readonly maxFacts: number;
private readonly extractionPrompt: string;
private readonly summaryPrompt: string;
constructor(options: FactExtractionMemoryBlockOptions) {
super(options);
this.llm = options.llm;
this.maxFacts = options.maxFacts;
this.extractionPrompt =
options.extractionPrompt ?? DEFAULT_EXTRACTION_PROMPT;
this.summaryPrompt = options.summaryPrompt ?? DEFAULT_SUMMARY_PROMPT;
}
async get(): Promise<MemoryMessage<TAdditionalMessageOptions>[]> {
const fact = {
id: this.id,
content: this.facts.join("\n"),
role: "memory" as MessageType,
};
return [fact];
}
async put(
messages: MemoryMessage<TAdditionalMessageOptions>[],
): Promise<void> {
if (messages.length === 0) {
return;
}
// Format existing facts
const existingFactsStr = `{ facts: [${this.facts.join(", ")}] }`;
// Format conversation
const conversation = `\n\t${messages.map((m) => m.content).join("\n\t")}`;
// Format prompt
const prompt = this.extractionPrompt
.replace("{{conversation}}", conversation)
.replace("{{existing_facts}}", existingFactsStr);
// Call the LLM
const response = await this.llm.complete({
prompt,
});
// Parse and validate the response
const newFacts = JSON.parse(response.text);
if (newFacts.facts === undefined || !Array.isArray(newFacts.facts)) {
throw new Error(
`[FactExtraction] Invalid response from LLM: ${response.text}`,
);
}
// No new facts, so no need to update the facts
if (newFacts.facts.length === 0) {
return;
}
// Update the facts
this.facts.push(...newFacts.facts);
// Condense the facts
if (this.facts.length > this.maxFacts) {
const existingFactsStr = `{ facts: [${this.facts.join(", ")}] }`;
const prompt = this.summaryPrompt
.replace("{{existing_facts}}", existingFactsStr)
.replace("{{max_facts}}", this.maxFacts.toString());
const response = await this.llm.complete({
prompt,
});
const condensedFacts = JSON.parse(response.text);
if (
condensedFacts.facts === undefined ||
!Array.isArray(condensedFacts.facts) ||
condensedFacts.facts.length === 0
) {
throw new Error("Invalid response from LLM");
}
// Only get the first maxFacts facts (in case the LLM returned more)
this.facts = condensedFacts.facts.slice(0, this.maxFacts);
}
}
}
+3
View File
@@ -0,0 +1,3 @@
export { BaseMemoryBlock } from "./base";
export { FactExtractionMemoryBlock } from "./fact";
export { StaticMemoryBlock } from "./static";
+51
View File
@@ -0,0 +1,51 @@
import type { MessageContent, MessageType } from "../../llms";
import type { MemoryMessage } from "../types";
import { BaseMemoryBlock, type MemoryBlockOptions } from "./base";
export type StaticMemoryBlockOptions = {
/**
* The static content to store.
*/
content: MessageContent;
/**
* The role of the message.
*/
messageRole?: MessageType;
} & Omit<MemoryBlockOptions, "priority" | "isLongTerm">;
/**
* A memory block that stores static content that doesn't change.
* Static content is always included in the memory context.
*/
export class StaticMemoryBlock<
TAdditionalMessageOptions extends object = object,
> extends BaseMemoryBlock<TAdditionalMessageOptions> {
private readonly content: MessageContent;
private readonly messageRole: MessageType;
constructor(options: StaticMemoryBlockOptions) {
super({ ...options, priority: 0, isLongTerm: false });
this.content = options.content;
this.messageRole = options.messageRole ?? "user";
}
/**
* Returns the static content.
* The messages parameter is ignored since this block contains static content.
*/
async get(): Promise<MemoryMessage<TAdditionalMessageOptions>[]> {
return [
{
id: this.id,
role: this.messageRole,
content: this.content,
},
];
}
async put(
_messages: MemoryMessage<TAdditionalMessageOptions>[],
): Promise<void> {
// No-op: static content doesn't change
}
}
@@ -1,13 +1,14 @@
import { Settings } from "../global";
import type { ChatMessage } from "../llms";
import { type BaseChatStore, SimpleChatStore } from "../storage/chat-store";
import { extractText } from "../utils";
import { Settings } from "../../global";
import type { ChatMessage } from "../../llms";
import { type BaseChatStore, SimpleChatStore } from "../../storage/chat-store";
import { extractText } from "../../utils";
export const DEFAULT_TOKEN_LIMIT_RATIO = 0.75;
export const DEFAULT_CHAT_STORE_KEY = "chat_history";
/**
* A ChatMemory is used to keep the state of back and forth chat messages
* @deprecated Use Memory instead.
*/
export abstract class BaseMemory<
AdditionalMessageOptions extends object = object,
@@ -55,6 +56,9 @@ export abstract class BaseMemory<
}
}
/**
* @deprecated Use Memory with snapshot feature with your own storage instead.
*/
export abstract class BaseChatStoreMemory<
AdditionalMessageOptions extends object = object,
> extends BaseMemory<AdditionalMessageOptions> {
@@ -1,6 +1,6 @@
import { Settings } from "../global";
import type { ChatMessage, LLM } from "../llms";
import { type BaseChatStore } from "../storage/chat-store";
import { Settings } from "../../global";
import type { ChatMessage, LLM } from "../../llms";
import { type BaseChatStore } from "../../storage/chat-store";
import { BaseChatStoreMemory, DEFAULT_TOKEN_LIMIT_RATIO } from "./base";
type ChatMemoryBufferOptions<AdditionalMessageOptions extends object = object> =
@@ -12,6 +12,9 @@ type ChatMemoryBufferOptions<AdditionalMessageOptions extends object = object> =
llm?: LLM<object, AdditionalMessageOptions> | undefined;
};
/**
* @deprecated Use Memory instead.
*/
export class ChatMemoryBuffer<
AdditionalMessageOptions extends object = object,
> extends BaseChatStoreMemory<AdditionalMessageOptions> {
@@ -1,10 +1,13 @@
import { type Tokenizer, tokenizers } from "@llamaindex/env/tokenizers";
import { Settings } from "../global";
import type { ChatMessage, LLM, MessageType } from "../llms";
import { defaultSummaryPrompt, type SummaryPrompt } from "../prompts";
import { extractText, messagesToHistory } from "../utils";
import { Settings } from "../../global";
import type { ChatMessage, LLM, MessageType } from "../../llms";
import { defaultSummaryPrompt, type SummaryPrompt } from "../../prompts";
import { extractText, messagesToHistory } from "../../utils";
import { BaseMemory } from "./base";
/**
* @deprecated Use Memory instead.
*/
export class ChatSummaryMemoryBuffer extends BaseMemory {
/**
* Tokenizer function that converts text to tokens,
+136
View File
@@ -0,0 +1,136 @@
import type { ChatMessage } from "../llms";
import { ChatMessageAdapter } from "./adapter/chat";
import {
FactExtractionMemoryBlock,
type FactExtractionMemoryBlockOptions,
} from "./block/fact";
import {
StaticMemoryBlock,
type StaticMemoryBlockOptions,
} from "./block/static";
import { DEFAULT_TOKEN_LIMIT, Memory, type MemoryOptions } from "./memory";
import type { MemoryMessage } from "./types";
/**
* Create a Memory instance with default options
* @returns A new Memory instance with default configuration
*/
export function createMemory<TMessageOptions extends object = object>(): Memory<
Record<string, never>,
TMessageOptions
>;
/**
* Create a Memory instance with options only
* @param options - Memory configuration options
* @returns A new Memory instance
*/
export function createMemory<TMessageOptions extends object = object>(
options: MemoryOptions<TMessageOptions>,
): Memory<Record<string, never>, TMessageOptions>;
/**
* Create a Memory instance with ChatMessage array (IDs will be generated)
* @param messages - Initial ChatMessage array for the memory
* @param options - Memory configuration options
* @returns A new Memory instance
*/
export function createMemory<TMessageOptions extends object = object>(
messages: ChatMessage<TMessageOptions>[],
options?: MemoryOptions<TMessageOptions>,
): Memory<Record<string, never>, TMessageOptions>;
/**
* Create a Memory instance with MemoryMessage array and options
* @param messages - Initial MemoryMessage array for the memory
* @param options - Memory configuration options
* @returns A new Memory instance
*/
export function createMemory<TMessageOptions extends object = object>(
messages: MemoryMessage<TMessageOptions>[],
options: MemoryOptions<TMessageOptions>,
): Memory<Record<string, never>, TMessageOptions>;
/**
* Create a Memory instance
* @param messagesOrOptions - Either initial messages or options
* @param options - Memory configuration options (when first param is messages)
* @returns A new Memory instance
*/
export function createMemory<TMessageOptions extends object = object>(
messagesOrOptions:
| ChatMessage<TMessageOptions>[]
| MemoryMessage<TMessageOptions>[]
| MemoryOptions<TMessageOptions> = [],
options: MemoryOptions<TMessageOptions> = {},
): Memory<Record<string, never>, TMessageOptions> {
let messages: MemoryMessage<TMessageOptions>[] = [];
if (Array.isArray(messagesOrOptions)) {
const firstMessage = messagesOrOptions[0];
if (firstMessage) {
if ("id" in firstMessage) {
messages = messagesOrOptions as MemoryMessage<TMessageOptions>[];
} else {
const adapter = new ChatMessageAdapter<TMessageOptions>();
messages = messagesOrOptions.map((chatMessage) =>
adapter.toMemory(chatMessage),
);
}
}
}
return new Memory<Record<string, never>, TMessageOptions>(messages, options);
}
/**
* create a StaticMemoryBlock
* @param options - Configuration options for the static memory block
* @returns A new StaticMemoryBlock instance
*/
export function staticBlock<TMessageOptions extends object = object>(
options: StaticMemoryBlockOptions,
): StaticMemoryBlock<TMessageOptions> {
return new StaticMemoryBlock<TMessageOptions>(options);
}
/**
* create a FactExtractionMemoryBlock
* @param options - Configuration options for the fact extraction memory block
* @returns A new FactExtractionMemoryBlock instance
*/
export function factExtractionBlock<TMessageOptions extends object = object>(
options: FactExtractionMemoryBlockOptions,
): FactExtractionMemoryBlock<TMessageOptions> {
return new FactExtractionMemoryBlock<TMessageOptions>(options);
}
/**
* Creates a new Memory instance from a snapshot
* @param snapshot The snapshot to load from
* @param options Optional MemoryOptions to apply when loading (including memory blocks)
* @returns A new Memory instance with the snapshot data and provided options
*/
export function loadMemory<TMessageOptions extends object = object>(
snapshot: string,
options?: MemoryOptions<TMessageOptions>,
): Memory<Record<string, never>, TMessageOptions> {
const { messages, tokenLimit, memoryCursor } = JSON.parse(snapshot);
// Merge snapshot data with provided options
const mergedOptions: MemoryOptions<TMessageOptions> = {
tokenLimit: options?.tokenLimit ?? tokenLimit ?? DEFAULT_TOKEN_LIMIT,
...(options?.shortTermTokenLimitRatio && {
shortTermTokenLimitRatio: options.shortTermTokenLimitRatio,
}),
...(options?.customAdapters && {
customAdapters: options.customAdapters,
}),
memoryBlocks: options?.memoryBlocks ?? [],
memoryCursor: memoryCursor ?? 0,
};
return new Memory<Record<string, never>, TMessageOptions>(
messages,
mergedOptions,
);
}
+9 -3
View File
@@ -1,3 +1,9 @@
export { BaseMemory } from "./base";
export { ChatMemoryBuffer } from "./chat-memory-buffer";
export { ChatSummaryMemoryBuffer } from "./summary-memory";
export { BaseMemory } from "./deprecated/base";
export { ChatMemoryBuffer } from "./deprecated/chat-memory-buffer";
export { ChatSummaryMemoryBuffer } from "./deprecated/summary-memory";
export * from "./adapter";
export * from "./block";
export * from "./factories";
export { Memory } from "./memory";
export * from "./types";
+401
View File
@@ -0,0 +1,401 @@
import { Settings } from "../global";
import type { ChatMessage, LLM } from "../llms";
import { extractText } from "../utils";
import { type MessageAdapter } from "./adapter/base";
import { ChatMessageAdapter } from "./adapter/chat";
import { VercelMessageAdapter } from "./adapter/vercel";
import type { BaseMemoryBlock } from "./block/base.js";
import { DEFAULT_TOKEN_LIMIT_RATIO } from "./deprecated/base";
import type { MemoryMessage } from "./types";
export const DEFAULT_TOKEN_LIMIT = 4096;
const DEFAULT_SHORT_TERM_TOKEN_LIMIT_RATIO = 0.5;
type BuiltinAdapters<TMessageOptions extends object = object> = {
vercel: VercelMessageAdapter;
llamaindex: ChatMessageAdapter<TMessageOptions>;
};
export type MemoryOptions<TMessageOptions extends object = object> = {
tokenLimit?: number;
/**
* How much of the token limit is used for short term memory.
* The remaining token limit is used for long term memory.
* Default is 0.5.
*/
shortTermTokenLimitRatio?: number;
customAdapters?: Record<string, MessageAdapter<unknown, object>>;
memoryBlocks?: BaseMemoryBlock<TMessageOptions>[];
/**
* The cursor position for tracking processed messages into long-term memory.
* Used internally for memory restoration from snapshots.
*/
memoryCursor?: number;
};
export class Memory<
TAdapters extends Record<
string,
MessageAdapter<unknown, TMessageOptions>
> = Record<string, never>,
TMessageOptions extends object = object,
> {
/**
* Hold all messages put into the memory.
*/
private messages: MemoryMessage<TMessageOptions>[] = [];
/**
* The token limit for memory retrieval results.
*/
private tokenLimit: number = DEFAULT_TOKEN_LIMIT;
/**
* The ratio of the token limit for short term memory.
*/
private shortTermTokenLimitRatio: number =
DEFAULT_SHORT_TERM_TOKEN_LIMIT_RATIO;
/**
* The adapters for the memory.
*/
private adapters: TAdapters & BuiltinAdapters<TMessageOptions>;
/**
* The memory blocks for the memory.
*/
private memoryBlocks: BaseMemoryBlock<TMessageOptions>[] = [];
/**
* The cursor for the messages that have been processed into long-term memory.
*/
private memoryCursor: number = 0;
constructor(
messages: MemoryMessage<TMessageOptions>[] = [],
options: MemoryOptions<TMessageOptions> = {},
) {
this.messages = messages;
this.tokenLimit = options.tokenLimit ?? DEFAULT_TOKEN_LIMIT;
this.shortTermTokenLimitRatio =
options.shortTermTokenLimitRatio ?? DEFAULT_SHORT_TERM_TOKEN_LIMIT_RATIO;
this.memoryBlocks = options.memoryBlocks ?? [];
this.memoryCursor = options.memoryCursor ?? 0;
this.adapters = {
...options.customAdapters,
vercel: new VercelMessageAdapter(),
llamaindex: new ChatMessageAdapter(),
} as TAdapters & BuiltinAdapters<TMessageOptions>;
}
/**
* Add a message to the memory
* @param message - The message to add to the memory
*/
async add(message: unknown): Promise<void> {
let memoryMessage: MemoryMessage<TMessageOptions> | null = null;
// Try to find a compatible adapter among the other adapters
for (const key in this.adapters) {
const adapter = this.adapters[key as keyof typeof this.adapters];
if (adapter?.isCompatible(message)) {
memoryMessage = adapter.toMemory(message);
break;
}
}
if (memoryMessage) {
this.messages.push(memoryMessage);
// Automatically manage memory blocks when new messages are added
await this.manageMemoryBlocks();
} else {
throw new Error(
`None of the adapters ${Object.keys(this.adapters).join(", ")} are compatible with the message. ${JSON.stringify(message)}`,
);
}
}
/**
* Get the messages of specific type from the memory
* @param options - The options for the get method
* @returns The messages of specific type
*/
async get<
K extends keyof (TAdapters &
BuiltinAdapters<TMessageOptions>) = "llamaindex",
>(
options: {
type?: K;
transientMessages?: ChatMessage<TMessageOptions>[];
} = {},
): Promise<
K extends keyof (TAdapters & BuiltinAdapters<TMessageOptions>)
? ReturnType<
(TAdapters & BuiltinAdapters<TMessageOptions>)[K]["fromMemory"]
>[]
: never
> {
const { type = "llamaindex", transientMessages } = options;
const adapter = this.adapters[type as keyof typeof this.adapters];
if (!adapter) {
throw new Error(`No adapter registered for type "${String(type)}"`);
}
let messages = this.messages;
if (transientMessages && transientMessages.length > 0) {
messages = [
...this.messages,
...transientMessages.map((m) => this.adapters.llamaindex.toMemory(m)),
];
}
// Convert memory messages to chat messages for memory block processing
const chatMessages = messages.map((m) => adapter.fromMemory(m));
return chatMessages as unknown as Promise<
K extends keyof (TAdapters & BuiltinAdapters<TMessageOptions>)
? ReturnType<
(TAdapters & BuiltinAdapters<TMessageOptions>)[K]["fromMemory"]
>[]
: never
>;
}
/**
* Get the messages from the memory, optionally including transient messages.
* only return messages that are within context window of the LLM
* @param llm - To fit the result messages to the context window of the LLM. If not provided, the default token limit will be used.
* @param transientMessages - Optional transient messages to include.
* @returns The messages from the memory, optionally including transient messages.
*/
async getLLM(
llm?: LLM,
transientMessages?: ChatMessage<TMessageOptions>[],
): Promise<ChatMessage[]> {
// Priority of result messages:
// [Fixed blocks (priority=0), Long term blocks, Short term messages(oldest to newest), Transient messages]
const contextWindow = llm?.metadata.contextWindow;
const tokenLimit = contextWindow
? Math.ceil(contextWindow * DEFAULT_TOKEN_LIMIT_RATIO)
: this.tokenLimit;
// Start with fixed block messages (priority=0)
// as it must always be included in the retrieval result
const messages = await this.getMemoryBlockMessages(
this.memoryBlocks.filter((block) => block.priority === 0),
tokenLimit,
);
// remaining token limit for short-term and memory blocks content
const remainingTokenLimit =
tokenLimit -
this.countMessagesToken([...messages, ...(transientMessages || [])]);
// if transient messages are provided, we need to check if they fit within the token limit
if (remainingTokenLimit < 0) {
throw new Error(
`Could not fit fixed blocks and transient messages within memory context`,
);
}
// Get messages for short-term and memory blocks
const shortTermTokenLimit = Math.ceil(
remainingTokenLimit * this.shortTermTokenLimitRatio,
);
const memoryBlocksTokenLimit = remainingTokenLimit - shortTermTokenLimit;
// Add long-term memory blocks (priority > 0)
const longTermBlocks = [...this.memoryBlocks]
.filter((block) => block.priority !== 0)
.sort((a, b) => b.priority - a.priority);
const longTermBlockMessages = await this.getMemoryBlockMessages(
longTermBlocks,
memoryBlocksTokenLimit,
);
messages.push(...longTermBlockMessages);
// Process short-term messages (newest first for token efficiency, but maintain chronological order in result)
const shortTermMessagesResult: ChatMessage<TMessageOptions>[] = [];
const unprocessedMessages = this.messages.slice(this.memoryCursor);
// Process from newest to oldest for token efficiency
for (let i = unprocessedMessages.length - 1; i >= 0; i--) {
const memoryMessage = unprocessedMessages[i];
if (!memoryMessage) continue;
const chatMessage = this.adapters.llamaindex.fromMemory(memoryMessage);
// Check if adding this message would exceed token limit
const newTokenCount =
this.countMessagesToken(shortTermMessagesResult) +
this.countMessagesToken([chatMessage]) +
this.countMessagesToken(transientMessages || []);
if (newTokenCount > shortTermTokenLimit) {
// Token limit reached, stop processing older messages
break;
}
shortTermMessagesResult.push(chatMessage);
}
// reverse the short-term messages to maintain chronological order (oldest to newest)
messages.push(...shortTermMessagesResult.reverse());
// Add transient messages at the end
if (transientMessages && transientMessages.length > 0) {
messages.push(...transientMessages);
}
return messages;
}
/**
* Get the content from the memory blocks
* also convert the content to chat messages
* @param blocks - The blocks to get the content from
* @param tokenLimit - The token limit for the memory blocks, if not provided, all the memory blocks will be included
*/
private async getMemoryBlockMessages(
blocks: BaseMemoryBlock<TMessageOptions>[],
tokenLimit?: number,
): Promise<ChatMessage<TMessageOptions>[]> {
if (blocks.length === 0) {
return [];
}
// Sort memory blocks by priority (highest first)
const sortedBlocks = [...blocks].sort((a, b) => b.priority - a.priority);
const memoryContent: ChatMessage<TMessageOptions>[] = [];
// Get up to the token limit of the memory blocks
let addedTokenCount = 0;
for (const block of sortedBlocks) {
try {
const content = await block.get();
for (const message of content) {
const chatMessage = this.adapters.llamaindex.fromMemory(message);
const messageTokenCount = this.countMessagesToken([chatMessage]);
if (tokenLimit && addedTokenCount + messageTokenCount > tokenLimit) {
return memoryContent;
}
memoryContent.push(chatMessage);
addedTokenCount += messageTokenCount;
}
} catch (error) {
console.warn(
`Failed to get content from memory block ${block.id}:`,
error,
);
}
}
return memoryContent;
}
/**
* Manage the memory blocks
* This method processes new messages into memory blocks when short-term memory exceeds its token limit.
* It uses a cursor system to track which messages have already been processed into long-term memory.
*/
async manageMemoryBlocks(): Promise<void> {
// Early return if no memory blocks configured
if (this.memoryBlocks.length === 0) {
return;
}
// Should always calculate the number
const shortTermTokenLimit = Math.ceil(
this.tokenLimit * this.shortTermTokenLimitRatio,
);
// Check if unprocessed messages exceed the short term token limit
const unprocessedMessages = this.getUnprocessedMessages();
const unprocessedMessagesTokenCount =
this.countMemoryMessagesToken(unprocessedMessages);
if (unprocessedMessagesTokenCount <= shortTermTokenLimit) {
// No need to manage memory blocks yet
return;
}
await this.processMessagesIntoMemoryBlocks(unprocessedMessages);
this.updateMemoryCursor(unprocessedMessages.length);
}
/**
* Get messages that haven't been processed into long-term memory yet
*/
private getUnprocessedMessages(): MemoryMessage<TMessageOptions>[] {
if (this.memoryCursor >= this.messages.length) {
return [];
}
return this.messages.slice(this.memoryCursor);
}
/**
* Process new messages into all memory blocks
*/
private async processMessagesIntoMemoryBlocks(
newMessages: MemoryMessage<TMessageOptions>[],
): Promise<void> {
const longTermMemoryBlocks = this.memoryBlocks.filter(
(block) => block.isLongTerm,
);
const promises = longTermMemoryBlocks.map(async (block) => {
try {
await block.put(newMessages);
} catch (error) {
console.warn(
`Failed to process messages into memory block ${block.id}:`,
error,
);
// Continue processing other blocks even if one fails
}
});
// Wait for all memory blocks to process the messages
await Promise.all(promises);
}
/**
* Update the memory cursor after successful processing
*/
private updateMemoryCursor(processedCount: number): void {
this.memoryCursor += processedCount;
// Ensure cursor doesn't exceed message count
this.memoryCursor = Math.min(this.memoryCursor, this.messages.length);
}
/**
* Clear all the messages in the memory
*/
async clear(): Promise<void> {
this.messages = [];
this.memoryCursor = 0; // Reset cursor when clearing messages
}
/**
* Creates a snapshot of the current memory state
* Note: Memory blocks are not included in snapshots as they may contain non-serializable content.
* Memory blocks should be recreated when loading from snapshot.
* @returns A JSON-serializable object containing the memory state
*/
snapshot(): string {
return JSON.stringify({
messages: this.messages,
memoryCursor: this.memoryCursor,
});
}
private countMemoryMessagesToken(
messages: MemoryMessage<TMessageOptions>[],
): number {
return this.countMessagesToken(
messages.map((m) =>
this.adapters.llamaindex.fromMemory(m),
) as ChatMessage[],
);
}
private countMessagesToken(messages: ChatMessage[]): number {
if (messages.length === 0) {
return 0;
}
const tokenizer = Settings.tokenizer;
const str = messages.map((m) => extractText(m.content)).join(" ");
return tokenizer.encode(str).length;
}
}
+19
View File
@@ -0,0 +1,19 @@
import type { ChatMessage } from "../llms";
/**
* Additional properties for storing additional data to memory messages
* using the same properties as vercel/ai for simplicity
*/
export type MemoryMessageExtension = {
id: string;
createdAt?: Date | undefined;
annotations?: Array<unknown> | undefined;
};
export type MemoryMessage<AdditionalMessageOptions extends object = object> =
ChatMessage<AdditionalMessageOptions> & MemoryMessageExtension;
export type MemorySnapshot = {
messages: MemoryMessage[];
tokenLimit: number;
};
+395
View File
@@ -0,0 +1,395 @@
import { Settings } from "@llamaindex/core/global";
import type { ChatMessage, LLM } from "@llamaindex/core/llms";
import { createMemory, Memory } from "@llamaindex/core/memory";
import { MockLLM } from "@llamaindex/core/utils";
import type { Tokenizer } from "@llamaindex/env/tokenizers";
import {
afterAll,
beforeAll,
beforeEach,
describe,
expect,
test,
} from "vitest";
// Mock tokenizer that returns predictable token counts
const createMockTokenizer = (): Tokenizer => ({
encode: (text: string): Uint32Array => {
// Simple mock: 1 token per 4 characters (rounded up)
const tokenCount = Math.ceil(text.length / 4);
return new Uint32Array(Array.from({ length: tokenCount }, (_, i) => i));
},
decode: (tokens: Uint32Array): string => {
// Simple mock: just return a string based on token count
return `decoded_${tokens.length}_tokens`;
},
});
// Helper function to create mock LLMs with different context windows
const createMockLLM = (contextWindow: number): LLM =>
new MockLLM({
metadata: {
contextWindow,
model: "test-model",
temperature: 0.7,
topP: 1.0,
tokenizer: undefined,
structuredOutput: false,
},
});
describe("Memory", () => {
let memory: Memory;
let originalTokenizer: Tokenizer;
beforeAll(() => {
// Save original tokenizer and set mock
originalTokenizer = Settings.tokenizer;
Settings.tokenizer = createMockTokenizer();
});
afterAll(() => {
// Restore original tokenizer
Settings.tokenizer = originalTokenizer;
});
beforeEach(() => {
memory = createMemory();
});
describe("add", () => {
test("should add LlamaIndex ChatMessage", async () => {
const message: ChatMessage = {
role: "user",
content: "Hello, world!",
};
await memory.add(message);
const messages = await memory.get();
expect(messages).toHaveLength(1);
expect(messages[0]).toEqual(message);
});
test("should add Vercel UI Message and convert to ChatMessage", async () => {
const vercelMessage = {
id: "test-id",
role: "user",
content: "Hello from Vercel!",
parts: [{ type: "text", text: "Hello from Vercel!" }],
createdAt: new Date(),
annotations: [],
};
await memory.add(vercelMessage);
const messages = await memory.get();
expect(messages).toHaveLength(1);
expect(messages[0]).toEqual({
role: "user",
content: "Hello from Vercel!",
});
});
test("should add multiple messages in sequence", async () => {
const message1: ChatMessage = { role: "user", content: "First message" };
const message2: ChatMessage = {
role: "assistant",
content: "Second message",
};
await memory.add(message1);
await memory.add(message2);
const messages = await memory.get();
expect(messages).toHaveLength(2);
expect(messages[0]).toEqual(message1);
expect(messages[1]).toEqual(message2);
});
});
describe("get", () => {
beforeEach(async () => {
// Add some test messages
await memory.add({ role: "user", content: "User message" });
await memory.add({ role: "assistant", content: "Assistant response" });
});
test("should return messages in LlamaIndex format by default", async () => {
const messages = await memory.get();
expect(messages).toHaveLength(2);
expect(messages[0]).toEqual({ role: "user", content: "User message" });
expect(messages[1]).toEqual({
role: "assistant",
content: "Assistant response",
});
});
test("should return messages in LlamaIndex format when explicitly requested", async () => {
const messages = await memory.get({ type: "llamaindex" });
expect(messages).toHaveLength(2);
expect(messages[0]).toEqual({ role: "user", content: "User message" });
expect(messages[1]).toEqual({
role: "assistant",
content: "Assistant response",
});
});
test("should add and get messages in LlamaIndex format when explicitly requested with options", async () => {
const message = {
role: "user",
content: "Hello, world!",
options: {
temperature: 0.7,
topP: 1.0,
},
};
await memory.add(message);
const messages = await memory.get({ type: "llamaindex" });
expect(messages[messages.length - 1]).toEqual({
role: "user",
content: "Hello, world!",
options: {
temperature: 0.7,
topP: 1.0,
},
});
});
test("should return messages in Vercel format when requested", async () => {
const messages = await memory.get({ type: "vercel" });
expect(messages).toHaveLength(2);
expect(messages[0]).toMatchObject({
role: "user",
content: "User message",
parts: [{ type: "text", text: "User message" }],
});
expect(messages[1]).toMatchObject({
role: "assistant",
content: "Assistant response",
parts: [{ type: "text", text: "Assistant response" }],
});
// Check that IDs and timestamps are generated
expect(typeof messages[0]).toBe("object");
expect(messages[0]).toHaveProperty("id");
expect(messages[0]).toHaveProperty("parts");
expect(messages[0]?.parts).toHaveLength(1);
expect(messages[1]).toHaveProperty("parts");
expect(messages[1]?.parts).toHaveLength(1);
});
test("should include transient messages without storing them", async () => {
const transientMessages: ChatMessage[] = [
{ role: "system", content: "Transient system message" },
{ role: "user", content: "Transient user message" },
];
const messages = await memory.get({ transientMessages });
// Should return stored messages + transient messages
expect(messages).toHaveLength(4);
expect(messages[0]).toEqual({ role: "user", content: "User message" });
expect(messages[1]).toEqual({
role: "assistant",
content: "Assistant response",
});
expect(messages[2]).toEqual({
role: "system",
content: "Transient system message",
});
expect(messages[3]).toEqual({
role: "user",
content: "Transient user message",
});
// Verify transient messages are not stored permanently
const storedMessages = await memory.get();
expect(storedMessages).toHaveLength(2);
expect(storedMessages[0]).toEqual({
role: "user",
content: "User message",
});
expect(storedMessages[1]).toEqual({
role: "assistant",
content: "Assistant response",
});
});
});
describe("getLLM", () => {
beforeEach(async () => {
// Add test messages with varying lengths
await memory.add({ role: "user", content: "Short message 1" });
await memory.add({
role: "assistant",
content:
"This is a longer assistant response with more content to test token limits",
});
await memory.add({ role: "user", content: "Another user message" });
await memory.add({
role: "assistant",
content: "Final assistant response",
});
});
test("should return all messages when no LLM is provided", async () => {
const messages = await memory.getLLM();
expect(messages).toHaveLength(4);
expect(messages[0]?.content).toBe("Short message 1");
expect(messages[1]?.content).toBe(
"This is a longer assistant response with more content to test token limits",
);
expect(messages[2]?.content).toBe("Another user message");
expect(messages[3]?.content).toBe("Final assistant response");
});
test("should include transient messages in token calculation", async () => {
const transientMessages: ChatMessage[] = [
{ role: "system", content: "System instruction" },
{ role: "user", content: "Transient user question" },
];
const messages = await memory.getLLM(
createMockLLM(500),
transientMessages,
);
// Should include some combination of stored and transient messages
expect(messages.length).toBeGreaterThan(0);
// Check if transient messages are included (they should be recent)
const messageContents = messages.map((m) => m.content);
const hasTransientMessage = messageContents.some(
(content) =>
content === "System instruction" ||
content === "Transient user question",
);
expect(hasTransientMessage).toBe(true);
});
test("should handle empty memory with transient messages", async () => {
const emptyMemory = createMemory();
const transientMessages: ChatMessage[] = [
{ role: "system", content: "System message" },
{ role: "user", content: "User question" },
];
const messages = await emptyMemory.getLLM(
createMockLLM(1000),
transientMessages,
);
expect(messages).toHaveLength(2);
expect(messages[0]?.content).toBe("System message");
expect(messages[1]?.content).toBe("User question");
});
});
describe("token limit handling", () => {
beforeEach(async () => {
// Add messages with different lengths for testing
await memory.add({
role: "assistant",
content:
"This is a medium length response that should take up more tokens than the previous message",
});
await memory.add({ role: "user", content: "Short" }); // has 2 tokens
await memory.add({ role: "assistant", content: "Last message" }); // has 4 tokens
});
test("should return messages in token limit", async () => {
const messages = await memory.getLLM(createMockLLM(1000));
expect(messages).toHaveLength(3);
expect(messages[0]?.content).toBe(
"This is a medium length response that should take up more tokens than the previous message",
);
expect(messages[1]?.content).toBe("Short");
expect(messages[2]?.content).toBe("Last message");
});
test("should only return messages that fit in the token limit", async () => {
const messages = await memory.getLLM(createMockLLM(6));
expect(messages).toHaveLength(1);
expect(messages[0]?.content).toBe("Last message");
});
});
describe("clear", () => {
test("should clear all messages", async () => {
await memory.add({ role: "user", content: "Test message" });
await memory.add({ role: "assistant", content: "Test response" });
expect(await memory.get()).toHaveLength(2);
await memory.clear();
expect(await memory.get()).toHaveLength(0);
});
test("should allow adding messages after clearing", async () => {
await memory.add({ role: "user", content: "First message" });
await memory.clear();
await memory.add({ role: "user", content: "After clear" });
const messages = await memory.get();
expect(messages).toHaveLength(1);
expect(messages[0]?.content).toBe("After clear");
});
});
describe("edge cases", () => {
test("should handle message with empty content", async () => {
await memory.add({ role: "user", content: "" });
const messages = await memory.get();
expect(messages).toHaveLength(1);
expect(messages[0]?.content).toBe("");
});
test("should handle different role types", async () => {
const roles: ChatMessage["role"][] = [
"user",
"assistant",
"system",
"memory",
"developer",
];
for (const role of roles) {
await memory.add({ role, content: `Message from ${role}` });
}
const messages = await memory.get();
expect(messages).toHaveLength(roles.length);
roles.forEach((role, index) => {
expect(messages[index]?.role).toBe(role);
expect(messages[index]?.content).toBe(`Message from ${role}`);
});
});
test("should handle Vercel message with data role", async () => {
const vercelMessage = {
id: "test-id",
role: "data",
content: "Data message",
parts: [{ type: "text", text: "Data message" }],
createdAt: new Date(),
annotations: [],
};
await memory.add(vercelMessage);
const messages = await memory.get();
expect(messages[0]?.role).toBe("user"); // data role should be mapped to user
});
});
});
@@ -0,0 +1,397 @@
import type { ChatMessage, MessageContentDetail } from "@llamaindex/core/llms";
import type { MemoryMessage, VercelMessage } from "@llamaindex/core/memory";
import { VercelMessageAdapter } from "@llamaindex/core/memory";
import { describe, expect, test } from "vitest";
describe("VercelMessageAdapter", () => {
const adapter = new VercelMessageAdapter();
describe("toLlamaIndexMessage", () => {
test("should convert basic Vercel message to LlamaIndex message", () => {
const vercelMessage: VercelMessage = {
id: "test-id",
role: "user",
content: "Hello, world!",
parts: [{ type: "text", text: "Hello, world!" }],
createdAt: new Date(),
annotations: [],
};
const result = adapter.toMemory(vercelMessage);
expect(result).toEqual({
id: "test-id",
role: "user",
content: "Hello, world!",
annotations: [],
createdAt: vercelMessage.createdAt,
});
});
test("should handle all supported Vercel message roles", () => {
const roles: Array<VercelMessage["role"]> = [
"system",
"user",
"assistant",
"data",
];
roles.forEach((role) => {
const vercelMessage: VercelMessage = {
id: "test-id",
role,
content: `Message from ${role}`,
parts: [{ type: "text", text: `Message from ${role}` }],
createdAt: new Date(),
annotations: [],
};
const result = adapter.toMemory(vercelMessage);
// Data role should be mapped to user
const expectedRole = role === "data" ? "user" : role;
expect(result.role).toBe(expectedRole);
expect(result.content).toBe(`Message from ${role}`);
});
});
test("should convert file parts to MessageContent", () => {
const vercelMessage: VercelMessage = {
id: "test-id",
role: "user",
content: "File message",
parts: [
{ type: "file", data: "base64data", mimeType: "image/png" },
{ type: "text", text: "Description" },
],
createdAt: new Date(),
annotations: [],
};
const result = adapter.toMemory(vercelMessage);
expect(result.content).toEqual([
{ type: "file", data: "base64data", mimeType: "image/png" },
{ type: "text", text: "Description" },
]);
});
test("should handle empty parts array", () => {
const vercelMessage: VercelMessage = {
id: "test-id",
role: "user",
content: "Fallback content",
parts: [],
createdAt: new Date(),
annotations: [],
};
const result = adapter.toMemory(vercelMessage);
expect(result.content).toBe("Fallback content");
});
test("should handle single text part", () => {
const vercelMessage: VercelMessage = {
id: "test-id",
role: "user",
content: "Original content",
parts: [{ type: "text", text: "Single text part" }],
createdAt: new Date(),
annotations: [],
};
const result = adapter.toMemory(vercelMessage);
expect(result.content).toBe("Single text part");
});
});
describe("toUIMessage", () => {
test("should convert basic MemoryMessage to Vercel message", () => {
const memoryMessage: MemoryMessage = {
id: "test-id",
role: "user",
content: "Hello, LlamaIndex!",
createdAt: new Date(),
annotations: [],
};
const result = adapter.fromMemory(memoryMessage);
expect(result).toMatchObject({
id: "test-id",
role: "user",
content: "Hello, LlamaIndex!",
parts: [{ type: "text", text: "Hello, LlamaIndex!" }],
annotations: [],
});
});
test("should convert MemoryMessage with options to Vercel message", () => {
const createdAt = new Date();
const annotations = ["test"];
const memoryMessage: MemoryMessage = {
id: "test-id",
role: "user",
content: "Hello, LlamaIndex!",
createdAt,
annotations,
};
const result = adapter.fromMemory(memoryMessage);
expect(result).toMatchObject({
role: "user",
content: "Hello, LlamaIndex!",
parts: [{ type: "text", text: "Hello, LlamaIndex!" }],
id: "test-id",
createdAt,
annotations,
});
});
test("should handle all MemoryMessage roles", () => {
const roles: Array<MemoryMessage["role"]> = [
"user",
"assistant",
"system",
"memory",
"developer",
];
roles.forEach((role) => {
const memoryMessage: MemoryMessage = {
id: "test-id",
role,
content: `Message from ${role}`,
createdAt: new Date(),
annotations: [],
};
const result = adapter.fromMemory(memoryMessage);
// Memory role should be mapped to system, developer to user
let expectedRole: VercelMessage["role"];
switch (role) {
case "memory":
expectedRole = "system";
break;
case "developer":
expectedRole = "user";
break;
default:
expectedRole = role as VercelMessage["role"];
}
expect(result.role).toBe(expectedRole);
expect(result.content).toBe(`Message from ${role}`);
});
});
test("should convert multi-modal content to parts", () => {
const memoryMessage: MemoryMessage = {
id: "test-id",
role: "user",
content: [
{ type: "text", text: "Text content" },
{
type: "image_url",
image_url: { url: "https://example.com/image.jpg" },
},
{ type: "file", data: "base64data", mimeType: "application/pdf" },
] as MessageContentDetail[],
};
const result = adapter.fromMemory(memoryMessage);
expect(result.parts).toEqual([
{ type: "text", text: "Text content" },
{ type: "text", text: "[Image URL: https://example.com/image.jpg]" },
{ type: "file", data: "base64data", mimeType: "file" },
]);
expect(result.content).toBe("Text content");
});
test("should handle different media types", () => {
const memoryMessage: MemoryMessage = {
id: "test-id",
role: "user",
content: [
{ type: "audio", data: "audio-data", mimeType: "audio/mp3" },
{ type: "video", data: "video-data", mimeType: "video/mp4" },
{ type: "image", data: "image-data", mimeType: "image/png" },
] as MessageContentDetail[],
};
const result = adapter.fromMemory(memoryMessage);
expect(result.parts).toEqual([
{ type: "file", data: "audio-data", mimeType: "audio" },
{ type: "file", data: "video-data", mimeType: "video" },
{ type: "file", data: "image-data", mimeType: "image" },
]);
});
test("should handle unknown content types", () => {
const memoryMessage: MemoryMessage = {
id: "test-id",
role: "user",
content: [
{
type: "unknown",
data: "unknown-data",
} as unknown as MessageContentDetail,
],
};
const result = adapter.fromMemory(memoryMessage);
expect(result.parts).toEqual([
{
type: "text",
text: JSON.stringify({ type: "unknown", data: "unknown-data" }),
},
]);
});
});
describe("isVercelMessage", () => {
test("should return true for valid Vercel message", () => {
const validMessage: VercelMessage = {
id: "test-id",
role: "user",
content: "Test content",
parts: [],
createdAt: new Date(),
annotations: [],
};
expect(adapter.isCompatible(validMessage)).toBe(true);
});
test("should return true for all valid roles", () => {
const roles: Array<VercelMessage["role"]> = [
"system",
"user",
"assistant",
"data",
];
roles.forEach((role) => {
const message = {
id: "test-id",
role,
content: "Test content",
parts: [],
};
expect(adapter.isCompatible(message)).toBe(true);
});
});
});
describe("isLlamaIndexMessage", () => {
test("should return true for valid LlamaIndex message", () => {
const validMessage: ChatMessage = {
role: "user",
content: "Test content",
};
expect(adapter.isCompatible(validMessage)).toBe(false);
});
test("should return true for all valid roles", () => {
const roles: Array<ChatMessage["role"]> = [
"user",
"assistant",
"system",
"memory",
"developer",
];
roles.forEach((role) => {
const message = {
role,
content: "Test content",
};
expect(adapter.isCompatible(message)).toBe(false);
});
});
test("should return false for invalid message structures", () => {
const invalidMessages = [
null,
undefined,
"string",
123,
{},
{ role: "user" }, // missing content
{ content: "test" }, // missing role
{ role: "invalid", content: "test" }, // invalid role
{ role: "user", content: 123 }, // invalid content type (not string or array)
];
invalidMessages.forEach((message) => {
expect(adapter.isCompatible(message)).toBe(false);
});
});
});
describe("edge cases and error handling", () => {
test("should handle conversion with undefined optional fields", () => {
const vercelMessage = {
id: "test-id",
role: "user" as const,
content: "Test content",
parts: [{ type: "text" as const, text: "Test content" }],
// missing optional fields
};
const result = adapter.toMemory(vercelMessage);
expect(result.role).toBe("user");
expect(result.content).toBe("Test content");
});
test("should handle empty string content", () => {
const memoryMessage: MemoryMessage = {
id: "test-id",
role: "user",
content: "",
};
const result = adapter.fromMemory(memoryMessage);
expect(result.content).toBe("");
expect(result.parts).toEqual([{ type: "text", text: "" }]);
});
test("should handle empty array content", () => {
const memoryMessage: MemoryMessage = {
id: "test-id",
role: "user",
content: [],
};
const result = adapter.fromMemory(memoryMessage);
expect(result.content).toBe("");
expect(result.parts).toEqual([]);
});
test("should generate unique IDs", () => {
const memoryMessage: MemoryMessage = {
id: "test-id",
role: "user",
content: "Test",
};
const result1 = adapter.fromMemory(memoryMessage);
const result2 = adapter.toMemory(result1);
// Both should have valid UUIDs (they will be different)
expect(typeof result1.id).toBe("string");
expect(result1.id.length).toBeGreaterThan(0);
});
});
});
+118
View File
@@ -0,0 +1,118 @@
import {
createMemory,
loadMemory,
type MemoryMessage,
} from "@llamaindex/core/memory";
import { describe, expect, it } from "vitest";
describe("Memory Snapshot", () => {
it("should create a snapshot of empty memory", () => {
const memory = createMemory();
const snapshot = memory.snapshot();
const parsedSnapshot = JSON.parse(snapshot);
expect(typeof snapshot).toBe("string");
expect(parsedSnapshot).toEqual({
messages: [],
memoryCursor: 0,
});
});
it("should create a snapshot with messages", async () => {
const memory = createMemory();
const message1: MemoryMessage = {
id: "test-id",
role: "user",
content: "Hello",
};
const message2: MemoryMessage = {
id: "test-id",
role: "assistant",
content: "Hi there!",
};
await memory.add(message1);
await memory.add(message2);
const snapshot = memory.snapshot();
const parsedSnapshot = JSON.parse(snapshot);
expect(typeof snapshot).toBe("string");
expect(parsedSnapshot.messages).toHaveLength(2);
expect(parsedSnapshot.messages[0].id).toBe(message1.id);
expect(parsedSnapshot.messages[1].id).toBe(message2.id);
});
it("should load memory from snapshot", async () => {
const originalMemory = createMemory();
const message: MemoryMessage = {
id: "test-id",
role: "user",
content: "Test message",
};
await originalMemory.add(message);
const snapshot = originalMemory.snapshot();
const loadedMemory = loadMemory(snapshot);
const loadedSnapshot = JSON.parse(loadedMemory.snapshot());
expect(loadedSnapshot).toEqual(JSON.parse(snapshot));
});
it("should load memory with correct messages", async () => {
const message1: MemoryMessage = {
id: "test-id-1",
role: "user",
content: "First message",
};
const message2: MemoryMessage = {
id: "test-id-2",
role: "assistant",
content: "Second message",
};
const snapshot = JSON.stringify({
messages: [message1, message2],
});
const memory = loadMemory(snapshot);
const messages = await memory.get();
expect(messages).toHaveLength(2);
expect(messages[0]?.content).toBe(message1.content);
expect(messages[1]?.content).toBe(message2.content);
const vercelMessages = await memory.get({ type: "vercel" });
expect(vercelMessages).toHaveLength(2);
expect(vercelMessages[0]?.id).toBe(message1.id);
expect(vercelMessages[1]?.id).toBe(message2.id);
});
it("should create independent memory instances", async () => {
const originalMemory = createMemory();
const message: MemoryMessage = {
id: "test-id",
role: "user",
content: "Original message",
};
await originalMemory.add(message);
const snapshot = originalMemory.snapshot();
const loadedMemory = loadMemory(snapshot);
const newMessage: MemoryMessage = {
id: "test-id-2",
role: "user",
content: "New message",
};
await loadedMemory.add(newMessage);
const originalMessages = await originalMemory.get();
const loadedMessages = await loadedMemory.get();
expect(originalMessages).toHaveLength(1);
expect(loadedMessages).toHaveLength(2);
});
});
@@ -1,5 +1,5 @@
import { SimpleChatEngine } from "@llamaindex/core/chat-engine";
import { ChatMemoryBuffer } from "@llamaindex/core/memory";
import { Memory } from "@llamaindex/core/memory";
import { MockLLM } from "@llamaindex/core/utils";
import { describe, expect, test } from "vitest";
@@ -8,7 +8,6 @@ describe("SimpleChatEngine", () => {
const llm = new MockLLM();
const engine = new SimpleChatEngine({ llm });
expect(engine.llm).toBe(llm);
expect(engine.memory).toBeInstanceOf(ChatMemoryBuffer);
expect((engine.memory as ChatMemoryBuffer).tokenLimit).toBe(768);
expect(engine.memory).toBeInstanceOf(Memory);
});
});
@@ -5,7 +5,7 @@ import {
} from "@llamaindex/core/chat-engine";
import { wrapEventCaller } from "@llamaindex/core/decorator";
import type { ChatMessage, LLM } from "@llamaindex/core/llms";
import { BaseMemory, ChatMemoryBuffer } from "@llamaindex/core/memory";
import { createMemory, Memory } from "@llamaindex/core/memory";
import {
type CondenseQuestionPrompt,
defaultCondenseQuestionPrompt,
@@ -32,12 +32,12 @@ import { Settings } from "../../Settings.js";
*/
export class CondenseQuestionChatEngine extends BaseChatEngine {
queryEngine: BaseQueryEngine;
memory: BaseMemory;
memory: Memory;
llm: LLM;
condenseMessagePrompt: CondenseQuestionPrompt;
get chatHistory() {
return this.memory.getMessages();
return this.memory.getLLM();
}
constructor(init: {
@@ -48,9 +48,7 @@ export class CondenseQuestionChatEngine extends BaseChatEngine {
super();
this.queryEngine = init.queryEngine;
this.memory = new ChatMemoryBuffer({
chatHistory: init?.chatHistory,
});
this.memory = createMemory(init.chatHistory);
this.llm = Settings.llm;
this.condenseMessagePrompt =
init?.condenseMessagePrompt ?? defaultCondenseQuestionPrompt;
@@ -74,8 +72,8 @@ export class CondenseQuestionChatEngine extends BaseChatEngine {
}
}
private async condenseQuestion(chatHistory: BaseMemory, question: string) {
const chatHistoryStr = messagesToHistory(await chatHistory.getMessages());
private async condenseQuestion(chatHistory: Memory, question: string) {
const chatHistoryStr = messagesToHistory(await chatHistory.getLLM());
return this.llm.complete({
prompt: this.condenseMessagePrompt.format({
@@ -95,18 +93,15 @@ export class CondenseQuestionChatEngine extends BaseChatEngine {
): Promise<EngineResponse | AsyncIterable<EngineResponse>> {
const { message, stream } = params;
const chatHistory = params.chatHistory
? new ChatMemoryBuffer({
chatHistory:
params.chatHistory instanceof BaseMemory
? await params.chatHistory.getMessages()
: params.chatHistory,
})
? params.chatHistory instanceof Memory
? params.chatHistory
: createMemory(params.chatHistory)
: this.memory;
const condensedQuestion = (
await this.condenseQuestion(chatHistory, extractText(message))
).text;
chatHistory.put({ content: message, role: "user" });
await chatHistory.add({ content: message, role: "user" });
if (stream) {
const stream = await this.queryEngine.query({
@@ -119,14 +114,14 @@ export class CondenseQuestionChatEngine extends BaseChatEngine {
reducer: (accumulator, part) =>
(accumulator += extractText(part.message.content)),
finished: (accumulator) => {
chatHistory.put({ content: accumulator, role: "assistant" });
void chatHistory.add({ content: accumulator, role: "assistant" });
},
});
}
const response = await this.queryEngine.query({
query: condensedQuestion,
});
chatHistory.put({
await chatHistory.add({
content: response.message.content,
role: "assistant",
});
@@ -135,6 +130,6 @@ export class CondenseQuestionChatEngine extends BaseChatEngine {
}
reset() {
this.memory.reset();
void this.memory.clear();
}
}
+27 -13
View File
@@ -1,6 +1,5 @@
import { Settings } from "@llamaindex/core/global";
import type { ChatMessage, MessageContent } from "@llamaindex/core/llms";
import { ChatMemoryBuffer } from "@llamaindex/core/memory";
import { createMemory, Memory } from "@llamaindex/core/memory";
import { PromptTemplate } from "@llamaindex/core/prompts";
import { tool } from "@llamaindex/core/tools";
import { stringifyJSONToMessageContent } from "@llamaindex/core/utils";
@@ -85,6 +84,11 @@ export type AgentStep = {
export const agentStepEvent = workflowEvent<AgentStep>();
export type SingleAgentParams = FunctionAgentParams & {
/**
* Optional predefined memory to use for the workflow.
* If not provided, a new empty memory will be created.
*/
memory?: Memory;
/**
* Whether to log verbose output
*/
@@ -108,6 +112,11 @@ export type AgentWorkflowParams = {
* Can also be an AgentWorkflow object, in which case the workflow must have exactly one agent.
*/
rootAgent: BaseWorkflowAgent | AgentWorkflow;
/**
* Optional predefined memory to use for the workflow.
* If not provided, a new empty memory will be created.
*/
memory?: Memory | undefined;
verbose?: boolean;
/**
* Timeout for the workflow in seconds.
@@ -148,9 +157,13 @@ export class AgentWorkflow implements Workflow {
private agents: Map<string, BaseWorkflowAgent> = new Map();
private verbose: boolean;
private rootAgentName: string;
private initialMemory?: Memory;
constructor({ agents, rootAgent, verbose }: AgentWorkflowParams) {
constructor({ agents, rootAgent, memory, verbose }: AgentWorkflowParams) {
this.verbose = verbose ?? false;
if (memory) {
this.initialMemory = memory;
}
// Handle AgentWorkflow cases for agents
const processedAgents: BaseWorkflowAgent[] = [];
@@ -286,12 +299,15 @@ export class AgentWorkflow implements Workflow {
canHandoffTo: params.canHandoffTo,
});
const workflow = new AgentWorkflow({
const workflowParams: AgentWorkflowParams = {
agents: [agent],
rootAgent: agent,
verbose: params.verbose ?? false,
timeout: params.timeout ?? 60,
});
memory: params.memory,
};
const workflow = new AgentWorkflow(workflowParams);
return workflow;
}
@@ -304,7 +320,7 @@ export class AgentWorkflow implements Workflow {
const memory = state.memory;
if (chatHistory) {
chatHistory.forEach((message: ChatMessage) => {
memory.put(message);
memory.add(message);
});
}
if (userInput) {
@@ -312,7 +328,7 @@ export class AgentWorkflow implements Workflow {
role: "user",
content: userInput,
};
memory.put(userMessage);
memory.add(userMessage);
} else if (chatHistory) {
// If no user message, use the last message from chat history as user_msg_str
const lastMessage = chatHistory[chatHistory.length - 1];
@@ -328,7 +344,7 @@ export class AgentWorkflow implements Workflow {
console.log(`[Agent ${this.rootAgentName}]: Starting agent`);
}
return agentInputEvent.with({
input: await memory.getMessages(),
input: await memory.getLLM(this.agents.get(this.rootAgentName)?.llm),
currentAgentName: this.rootAgentName,
});
};
@@ -514,7 +530,7 @@ export class AgentWorkflow implements Workflow {
const messages = await this.stateful
.getContext()
.state.memory.getMessages();
.state.memory.getLLM(this.agents.get(nextAgentName)?.llm);
if (this.verbose) {
console.log(`[Agent ${nextAgentName}]: Starting agent`);
}
@@ -534,7 +550,7 @@ export class AgentWorkflow implements Workflow {
// Continue with another agent step
const messages = await this.stateful
.getContext()
.state.memory.getMessages();
.state.memory.getLLM(this.agents.get(agent.name)?.llm);
return agentInputEvent.with({
input: messages,
currentAgentName: agent.name,
@@ -562,9 +578,7 @@ export class AgentWorkflow implements Workflow {
private createInitialState(): AgentWorkflowState {
return {
memory: new ChatMemoryBuffer({
llm: this.agents.get(this.rootAgentName)?.llm ?? Settings.llm,
}),
memory: this.initialMemory ?? createMemory(),
scratchpad: [],
currentAgentName: this.rootAgentName,
agents: Array.from(this.agents.keys()),
+2 -2
View File
@@ -1,10 +1,10 @@
import type { BaseToolWithCall, ChatMessage, LLM } from "@llamaindex/core/llms";
import { BaseMemory } from "@llamaindex/core/memory";
import { Memory } from "@llamaindex/core/memory";
import type { WorkflowContext } from "@llamaindex/workflow-core";
import type { AgentOutput, AgentToolCallResult } from "./events";
export type AgentWorkflowState = {
memory: BaseMemory;
memory: Memory;
scratchpad: ChatMessage[];
agents: string[];
currentAgentName: string;
@@ -262,7 +262,7 @@ export class FunctionAgent implements BaseWorkflowAgent {
const scratchpad: ChatMessage[] = state.scratchpad;
for (const msg of scratchpad) {
state.memory.put(msg);
state.memory.add(msg);
}
// Clear scratchpad after finalization
@@ -233,7 +233,7 @@ describe("agent", () => {
},
]);
const messages = response.data.state!.memory.getAllMessages();
const messages = await response.data.state!.memory.get();
const fileMessage = messages[0].content[0];
expect(fileMessage.type).toEqual("file");