mirror of
https://github.com/run-llama/LlamaIndexTS.git
synced 2026-07-04 03:40:26 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e150892f9f |
@@ -1,7 +1,8 @@
|
||||
import { Settings } from "../global";
|
||||
import type { ChatMessage } from "../llms";
|
||||
import { type BaseChatStore, SimpleChatStore } from "../storage/chat-store";
|
||||
import type { ChatMessage, MessageContent, MessageType } from "../llms";
|
||||
import { SimpleChatStore, type BaseChatStore } from "../storage/chat-store";
|
||||
import { extractText } from "../utils";
|
||||
import type { MemoryBlockContent } from "./types";
|
||||
|
||||
export const DEFAULT_TOKEN_LIMIT_RATIO = 0.75;
|
||||
export const DEFAULT_CHAT_STORE_KEY = "chat_history";
|
||||
@@ -83,3 +84,70 @@ export abstract class BaseChatStoreMemory<
|
||||
this.chatStore.deleteMessages(this.chatStoreKey);
|
||||
}
|
||||
}
|
||||
|
||||
export abstract class BaseMemoryBlock<
|
||||
AdditionalMessageOptions extends object = object,
|
||||
> {
|
||||
protected priority: number;
|
||||
protected content: MemoryBlockContent[];
|
||||
|
||||
constructor(priority: number, content: MemoryBlockContent[]) {
|
||||
this.priority = priority;
|
||||
this.content = content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all content from the memory block.
|
||||
* @returns An array of content.
|
||||
*/
|
||||
get(): MemoryBlockContent[] {
|
||||
return this.content;
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a new content to the memory block.
|
||||
* @param content The content to be added to the memory block.
|
||||
*/
|
||||
add(content: MemoryBlockContent): void {
|
||||
this.content.push(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clears all content from the memory block.
|
||||
*/
|
||||
clear(): void {
|
||||
this.content = [];
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the priority of the memory block.
|
||||
* @returns The priority of the memory block.
|
||||
*/
|
||||
getPriority = (): number => this.priority;
|
||||
|
||||
/**
|
||||
* Converts content to messages.
|
||||
* @param content The content to be converted to messages.
|
||||
* @returns An array of messages.
|
||||
*/
|
||||
toMessages(): ChatMessage<AdditionalMessageOptions>[] {
|
||||
return this.content.map((entry) => {
|
||||
// If entry is a ChatMessage, return it
|
||||
if (typeof entry === "object" && "content" in entry && "role" in entry) {
|
||||
return entry as ChatMessage<AdditionalMessageOptions>;
|
||||
}
|
||||
// Else, create a new ChatMessage with the entry as content
|
||||
return {
|
||||
content: entry as MessageContent,
|
||||
role: "system" as MessageType,
|
||||
options: {} as AdditionalMessageOptions,
|
||||
};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export class StaticMemoryBlock extends BaseMemoryBlock {
|
||||
constructor(content: MemoryBlockContent[], priority: number) {
|
||||
super(priority, content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
import { StaticMemoryBlock } from "./base";
|
||||
import { Memory } from "./memory";
|
||||
import type { MemoryBlockContent, MemoryOptions } from "./types";
|
||||
|
||||
/**
|
||||
* Create a new enhanced memory instance with support for dual message formats,
|
||||
* snapshots, and memory blocks
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Basic usage
|
||||
* const memory = createMemory();
|
||||
*
|
||||
* // With configuration
|
||||
* const memory = createMemory({
|
||||
* tokenLimit: 10_000,
|
||||
* blocks: [
|
||||
* staticMemoryBlock(["You are a helpful assistant named Claude."], 0),
|
||||
* ],
|
||||
* });
|
||||
*
|
||||
* // Add messages in different formats
|
||||
* await memory.add({
|
||||
* content: "Hello, how are you?",
|
||||
* role: "user",
|
||||
* });
|
||||
*
|
||||
* // Get messages in Vercel AI format
|
||||
* const uiMessages = await memory.get({ type: "vercel" });
|
||||
*
|
||||
* // Save and restore memory state
|
||||
* const snapshot = memory.snapshot();
|
||||
* await memory.loadSnapshot(snapshot);
|
||||
* ```
|
||||
*
|
||||
* @param options Configuration options for the memory instance
|
||||
* @returns A new EnhancedMemory instance
|
||||
*/
|
||||
export function createMemory(options: MemoryOptions = {}): Memory {
|
||||
return new Memory(options);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new static memory block instance
|
||||
*
|
||||
* @param content The content of the memory block
|
||||
* @param priority The priority of the memory block
|
||||
* @returns A new StaticMemoryBlock instance
|
||||
*/
|
||||
export function staticMemoryBlock(
|
||||
content: MemoryBlockContent[],
|
||||
priority: number,
|
||||
): StaticMemoryBlock {
|
||||
return new StaticMemoryBlock(content, priority);
|
||||
}
|
||||
@@ -1,3 +1,16 @@
|
||||
export { BaseMemory } from "./base";
|
||||
// Existing exports (backward compatibility)
|
||||
export { BaseChatStoreMemory, BaseMemory } from "./base";
|
||||
export { ChatMemoryBuffer } from "./chat-memory-buffer";
|
||||
export { ChatSummaryMemoryBuffer } from "./summary-memory";
|
||||
|
||||
export { createMemory, staticMemoryBlock } from "./factory";
|
||||
export { Memory } from "./memory";
|
||||
export { MessageConverter } from "./message-converter";
|
||||
|
||||
// Type exports
|
||||
export type {
|
||||
GetMessageOptions,
|
||||
MemoryOptions,
|
||||
UIMessage,
|
||||
UIPart,
|
||||
} from "./types";
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
import { Settings, type JSONValue } from "../global";
|
||||
import type { ChatMessage } from "../llms";
|
||||
import { extractText } from "../utils";
|
||||
import { BaseMemoryBlock, StaticMemoryBlock } from "./base";
|
||||
import { MessageConverter } from "./message-converter";
|
||||
import type { GetMessageOptions, MemoryOptions, UIMessage } from "./types";
|
||||
import { serializeChatMessage, serializeMessageContent } from "./utils";
|
||||
|
||||
const DEFAULT_TOKEN_LIMIT = 4096;
|
||||
|
||||
export class Memory {
|
||||
private blocks: BaseMemoryBlock[] = [];
|
||||
private tokenLimit: number;
|
||||
|
||||
constructor(options: MemoryOptions = {}) {
|
||||
this.blocks = options.blocks || [];
|
||||
this.tokenLimit = options.tokenLimit || DEFAULT_TOKEN_LIMIT;
|
||||
}
|
||||
|
||||
private async getAllMessages(): Promise<ChatMessage[]> {
|
||||
// Order blocks by priority
|
||||
const orderedBlocks = this.blocks.sort(
|
||||
(a, b) => b.getPriority() - a.getPriority(),
|
||||
);
|
||||
|
||||
// Get all messages
|
||||
return orderedBlocks.map((block) => block.toMessages()).flat();
|
||||
}
|
||||
|
||||
async getMessagesWithLimit(tokenLimit: number): Promise<ChatMessage[]> {
|
||||
const messages = await this.getAllMessages();
|
||||
return this.applyTokenLimit(messages, tokenLimit);
|
||||
}
|
||||
|
||||
/**
|
||||
* @returns Return a serialized snapshot of the memory in JSON format.
|
||||
*/
|
||||
snapshot(): JSONValue {
|
||||
return {
|
||||
blocks: this.blocks.map((block) =>
|
||||
block.get().map((message) => {
|
||||
if (MessageConverter.isChatMessage(message)) {
|
||||
return serializeChatMessage(message);
|
||||
}
|
||||
return serializeMessageContent(message);
|
||||
}),
|
||||
),
|
||||
metadata: {
|
||||
tokenLimit: this.tokenLimit,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async loadSnapshot(snapshot: JSONValue): Promise<void> {}
|
||||
|
||||
async add(message: ChatMessage | UIMessage): Promise<void> {
|
||||
let llamaMessage: ChatMessage;
|
||||
|
||||
if (MessageConverter.isUIMessage(message)) {
|
||||
llamaMessage = MessageConverter.toLlamaIndexMessage(message);
|
||||
} else if (MessageConverter.isChatMessage(message)) {
|
||||
llamaMessage = message as ChatMessage;
|
||||
} else {
|
||||
throw new Error(
|
||||
"Invalid message format. Expected ChatMessage or UIMessage.",
|
||||
);
|
||||
}
|
||||
|
||||
// Convert message to block
|
||||
const block = new StaticMemoryBlock([llamaMessage.content], 0);
|
||||
|
||||
this.blocks.push(block);
|
||||
}
|
||||
|
||||
async get(options?: GetMessageOptions): Promise<ChatMessage[] | UIMessage[]> {
|
||||
const messages = await this.getAllMessages();
|
||||
|
||||
if (options?.type === "vercel") {
|
||||
return messages.map((message) => MessageConverter.toUIMessage(message));
|
||||
}
|
||||
|
||||
// Default to LlamaIndex format
|
||||
return messages;
|
||||
}
|
||||
|
||||
async getLLM(): Promise<ChatMessage[]> {
|
||||
// Convert all blocks to messages
|
||||
const allMessages = await this.getAllMessages();
|
||||
|
||||
// Apply token limit
|
||||
return this.applyTokenLimit(allMessages, this.tokenLimit);
|
||||
}
|
||||
|
||||
async clear(): Promise<void> {
|
||||
this.blocks = [];
|
||||
}
|
||||
|
||||
private applyTokenLimit(
|
||||
messages: ChatMessage[],
|
||||
tokenLimit: number,
|
||||
): ChatMessage[] {
|
||||
if (messages.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
let tokenCount = 0;
|
||||
const result: ChatMessage[] = [];
|
||||
|
||||
// Process messages in reverse order (keep most recent)
|
||||
for (let i = messages.length - 1; i >= 0; i--) {
|
||||
const message = messages[i];
|
||||
if (!message) continue;
|
||||
|
||||
const messageTokens = this.countMessagesToken([message]);
|
||||
|
||||
if (tokenCount + messageTokens <= tokenLimit) {
|
||||
result.unshift(message);
|
||||
tokenCount += messageTokens;
|
||||
} else {
|
||||
// If we can't fit any more messages, break
|
||||
// But always try to include at least the most recent message
|
||||
if (result.length === 0 && messageTokens <= tokenLimit) {
|
||||
result.push(message);
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
import { randomUUID } from "@llamaindex/env";
|
||||
import type {
|
||||
ChatMessage,
|
||||
MessageContent,
|
||||
MessageContentDetail,
|
||||
} from "../llms";
|
||||
import { extractText } from "../utils";
|
||||
import type { UIMessage, UIPart } from "./types";
|
||||
|
||||
/**
|
||||
* Utility class for converting between LlamaIndex ChatMessage and Vercel UI Message formats
|
||||
*/
|
||||
export class MessageConverter {
|
||||
/**
|
||||
* Convert Vercel UI Message to LlamaIndex ChatMessage format
|
||||
*/
|
||||
static toLlamaIndexMessage<AdditionalMessageOptions extends object = object>(
|
||||
uiMessage: UIMessage,
|
||||
): ChatMessage<AdditionalMessageOptions> {
|
||||
// Convert UI message role to MessageType
|
||||
let role: ChatMessage["role"];
|
||||
switch (uiMessage.role) {
|
||||
case "system":
|
||||
role = "system";
|
||||
break;
|
||||
case "user":
|
||||
role = "user";
|
||||
break;
|
||||
case "assistant":
|
||||
role = "assistant";
|
||||
break;
|
||||
case "data":
|
||||
role = "system"; // Map data role to system
|
||||
break;
|
||||
default:
|
||||
role = "user"; // Default fallback
|
||||
}
|
||||
|
||||
// Convert parts to MessageContent
|
||||
const content = this.convertPartsToMessageContent(uiMessage.parts);
|
||||
|
||||
return {
|
||||
content: content || uiMessage.content,
|
||||
role,
|
||||
options: undefined as unknown as AdditionalMessageOptions,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert LlamaIndex ChatMessage to Vercel UI Message format
|
||||
*/
|
||||
static toUIMessage(llamaMessage: ChatMessage): UIMessage {
|
||||
const parts: UIPart[] = this.convertMessageContentToParts(
|
||||
llamaMessage.content,
|
||||
);
|
||||
|
||||
// Convert role to UI message role
|
||||
let role: UIMessage["role"];
|
||||
switch (llamaMessage.role) {
|
||||
case "user":
|
||||
role = "user";
|
||||
break;
|
||||
case "assistant":
|
||||
role = "assistant";
|
||||
break;
|
||||
case "system":
|
||||
case "memory":
|
||||
case "developer":
|
||||
role = "system";
|
||||
break;
|
||||
default:
|
||||
role = "user";
|
||||
}
|
||||
|
||||
return {
|
||||
id: randomUUID(),
|
||||
role,
|
||||
content: extractText(llamaMessage.content),
|
||||
parts,
|
||||
createdAt: new Date(),
|
||||
annotations: [],
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if object matches UIMessage structure
|
||||
*/
|
||||
static isUIMessage(message: unknown): message is UIMessage {
|
||||
if (!message || typeof message !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const msg = message as Record<string, unknown>;
|
||||
|
||||
return (
|
||||
typeof msg.id === "string" &&
|
||||
typeof msg.role === "string" &&
|
||||
["system", "user", "assistant", "data"].includes(msg.role as string) &&
|
||||
typeof msg.content === "string" &&
|
||||
Array.isArray(msg.parts)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate if object matches ChatMessage structure
|
||||
*/
|
||||
static isChatMessage(message: unknown): message is ChatMessage {
|
||||
if (!message || typeof message !== "object") {
|
||||
return false;
|
||||
}
|
||||
|
||||
const msg = message as Record<string, unknown>;
|
||||
|
||||
return (
|
||||
(typeof msg.content === "string" || Array.isArray(msg.content)) &&
|
||||
typeof msg.role === "string" &&
|
||||
["user", "assistant", "system", "memory", "developer"].includes(
|
||||
msg.role as string,
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert UI parts to MessageContent
|
||||
*/
|
||||
private static convertPartsToMessageContent(parts: UIPart[]): MessageContent {
|
||||
if (parts.length === 0) {
|
||||
return "";
|
||||
}
|
||||
|
||||
const details: MessageContentDetail[] = [];
|
||||
|
||||
for (const part of parts) {
|
||||
switch (part.type) {
|
||||
case "text": {
|
||||
details.push({
|
||||
type: "text",
|
||||
text: part.content || "",
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "tool": {
|
||||
const toolPart = part as unknown as UIPart;
|
||||
details.push({
|
||||
type: "text",
|
||||
text: `Tool: ${part.data?.toolName}`,
|
||||
});
|
||||
break;
|
||||
}
|
||||
case "reasoning": {
|
||||
const resultPart = part as unknown as UIPart;
|
||||
details.push({
|
||||
type: "text",
|
||||
text: `Result: ${JSON.stringify(part.data?.result)}`,
|
||||
});
|
||||
break;
|
||||
}
|
||||
default:
|
||||
// For other part types, convert to text
|
||||
details.push({
|
||||
type: "text",
|
||||
text: JSON.stringify(part),
|
||||
});
|
||||
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 static convertMessageContentToParts(
|
||||
content: MessageContent,
|
||||
): UIPart[] {
|
||||
if (typeof content === "string") {
|
||||
return [
|
||||
{
|
||||
type: "text",
|
||||
text: content,
|
||||
} as UIPart,
|
||||
];
|
||||
}
|
||||
|
||||
const parts: UIPart[] = [];
|
||||
|
||||
for (const detail of content) {
|
||||
switch (detail.type) {
|
||||
case "text":
|
||||
parts.push({
|
||||
type: "text",
|
||||
text: detail.text,
|
||||
} as UIPart);
|
||||
break;
|
||||
case "image_url":
|
||||
// Convert image to text representation for UI
|
||||
parts.push({
|
||||
type: "text",
|
||||
text: `[Image: ${detail.image_url.url}]`,
|
||||
} as UIPart);
|
||||
break;
|
||||
case "audio":
|
||||
parts.push({
|
||||
type: "text",
|
||||
text: `[Audio: ${detail.mimeType}]`,
|
||||
} as UIPart);
|
||||
break;
|
||||
case "video":
|
||||
parts.push({
|
||||
type: "text",
|
||||
text: `[Video: ${detail.mimeType}]`,
|
||||
} as UIPart);
|
||||
break;
|
||||
case "image":
|
||||
parts.push({
|
||||
type: "text",
|
||||
text: `[Image: ${detail.mimeType}]`,
|
||||
} as UIPart);
|
||||
break;
|
||||
case "file":
|
||||
parts.push({
|
||||
type: "text",
|
||||
text: `[File: ${detail.mimeType}]`,
|
||||
} as UIPart);
|
||||
break;
|
||||
default:
|
||||
// For unknown types, create a text representation
|
||||
parts.push({
|
||||
type: "text",
|
||||
text: JSON.stringify(detail),
|
||||
} as UIPart);
|
||||
}
|
||||
}
|
||||
|
||||
return parts;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
import type { JSONValue } from "../global";
|
||||
import type { ChatMessage, MessageContent } from "../llms";
|
||||
import type { BaseMemoryBlock } from "./base";
|
||||
|
||||
export type MemoryBlockContent = MessageContent | ChatMessage;
|
||||
|
||||
export type MemoryOptions = {
|
||||
blocks?: BaseMemoryBlock[];
|
||||
tokenLimit?: number;
|
||||
};
|
||||
|
||||
/**
|
||||
* Vercel AI SDK message types (avoid dependency)
|
||||
* These types mirror the Vercel AI SDK without requiring it as a dependency
|
||||
*/
|
||||
export interface UIMessage {
|
||||
id: string;
|
||||
role: "system" | "user" | "assistant" | "data";
|
||||
content: string;
|
||||
createdAt?: Date;
|
||||
annotations?: Array<JSONValue>;
|
||||
parts: Array<UIPart>;
|
||||
}
|
||||
|
||||
export interface UIPart {
|
||||
type: "text" | "tool" | "reasoning" | "source" | "step";
|
||||
content?: string;
|
||||
data?: Record<string, unknown>; // TODO: Can expand this to be more specific later
|
||||
}
|
||||
|
||||
export type GetMessageOptions = {
|
||||
type?: "llamaindex" | "vercel";
|
||||
};
|
||||
@@ -0,0 +1,74 @@
|
||||
import { type JSONValue } from "../global";
|
||||
import type { ChatMessage, MessageContent } from "../llms";
|
||||
|
||||
/**
|
||||
* Serialize a message content to a JSON value.
|
||||
* @param message - The message content to serialize.
|
||||
* @returns The serialized message content.
|
||||
*/
|
||||
export function serializeMessageContent(message: MessageContent): JSONValue {
|
||||
if (typeof message === "string") {
|
||||
return message;
|
||||
}
|
||||
|
||||
// message is an array of MessageContentDetail
|
||||
return message.map((detail) => {
|
||||
const serialized: { [key: string]: JSONValue } = {
|
||||
type: detail.type,
|
||||
};
|
||||
|
||||
switch (detail.type) {
|
||||
case "text":
|
||||
serialized.text = detail.text;
|
||||
break;
|
||||
case "image_url":
|
||||
serialized.image_url = {
|
||||
url: detail.image_url.url,
|
||||
};
|
||||
if (detail.detail) {
|
||||
(serialized.image_url as { [key: string]: JSONValue }).detail =
|
||||
detail.detail;
|
||||
}
|
||||
break;
|
||||
case "audio":
|
||||
serialized.data = detail.data;
|
||||
serialized.mimeType = detail.mimeType;
|
||||
break;
|
||||
case "video":
|
||||
serialized.data = detail.data;
|
||||
serialized.mimeType = detail.mimeType;
|
||||
break;
|
||||
case "image":
|
||||
serialized.data = detail.data;
|
||||
serialized.mimeType = detail.mimeType;
|
||||
break;
|
||||
case "file":
|
||||
serialized.data = detail.data;
|
||||
serialized.mimeType = detail.mimeType;
|
||||
break;
|
||||
default:
|
||||
// For any unknown types, serialize all properties
|
||||
Object.assign(serialized, detail);
|
||||
}
|
||||
|
||||
return serialized;
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Serialize a chat message to a JSON value.
|
||||
* @param message - The chat message to serialize.
|
||||
* @returns The serialized chat message.
|
||||
*/
|
||||
export function serializeChatMessage(message: ChatMessage): JSONValue {
|
||||
const serialized: { [key: string]: JSONValue } = {
|
||||
content: serializeMessageContent(message.content),
|
||||
role: message.role,
|
||||
};
|
||||
|
||||
if (message.options) {
|
||||
serialized.options = message.options as JSONValue;
|
||||
}
|
||||
|
||||
return serialized;
|
||||
}
|
||||
@@ -0,0 +1,96 @@
|
||||
import { Settings } from "@llamaindex/core/global";
|
||||
import type { ChatMessage } from "@llamaindex/core/llms";
|
||||
import { createMemory, staticMemoryBlock } from "@llamaindex/core/memory";
|
||||
import { beforeEach, describe, expect, test } from "vitest";
|
||||
|
||||
describe("createMemory factory function", () => {
|
||||
beforeEach(() => {
|
||||
// Mock the Settings.llm
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(Settings.llm as any) = {
|
||||
metadata: {
|
||||
contextWindow: 1000,
|
||||
maxTokens: 100,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
test("creates memory with default settings", () => {
|
||||
const memory = createMemory();
|
||||
expect(memory).toBeDefined();
|
||||
expect(typeof memory.add).toBe("function");
|
||||
expect(typeof memory.get).toBe("function");
|
||||
expect(typeof memory.getLLM).toBe("function");
|
||||
expect(typeof memory.clear).toBe("function");
|
||||
expect(typeof memory.snapshot).toBe("function");
|
||||
expect(typeof memory.loadSnapshot).toBe("function");
|
||||
});
|
||||
|
||||
test("creates memory with custom token limit", () => {
|
||||
const memory = createMemory({ tokenLimit: 5000 });
|
||||
expect(memory).toBeDefined();
|
||||
});
|
||||
|
||||
test("creates memory with memory blocks", () => {
|
||||
const blocks = [staticMemoryBlock(["You are a helpful assistant."], 0)];
|
||||
const memory = createMemory({ blocks });
|
||||
expect(memory).toBeDefined();
|
||||
});
|
||||
|
||||
test("supports basic operations", async () => {
|
||||
const memory = createMemory({ tokenLimit: 1000 });
|
||||
|
||||
// Test add and get
|
||||
const message: ChatMessage = { content: "Hello", role: "user" };
|
||||
await memory.add(message);
|
||||
|
||||
const messages = await memory.get();
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0]?.content).toBe("Hello");
|
||||
});
|
||||
|
||||
// test("supports snapshot operations", async () => {
|
||||
// const memory = createMemory({ tokenLimit: 1000 });
|
||||
|
||||
// await memory.add({ content: "Test message", role: "user" });
|
||||
|
||||
// const snapshot = memory.snapshot();
|
||||
// expect(snapshot).toHaveProperty("blocks");
|
||||
// expect(snapshot).toHaveProperty("metadata");
|
||||
|
||||
// const newMemory = createMemory();
|
||||
// await newMemory.loadSnapshot(snapshot);
|
||||
|
||||
// const messages = await newMemory.get();
|
||||
// expect(messages).toHaveLength(1);
|
||||
// expect(messages[0]?.content).toBe("Test message");
|
||||
// });
|
||||
|
||||
test("supports backward compatibility methods", async () => {
|
||||
const memory = createMemory({ tokenLimit: 1000 });
|
||||
|
||||
// Test legacy methods exist and work
|
||||
const message: ChatMessage = { content: "Hello", role: "user" };
|
||||
memory.add(message);
|
||||
|
||||
const messages = await memory.get();
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0]?.content).toBe("Hello");
|
||||
|
||||
await memory.clear();
|
||||
const emptyMessages = await memory.get();
|
||||
expect(emptyMessages).toHaveLength(0);
|
||||
});
|
||||
|
||||
test("creates memory with different message format support", async () => {
|
||||
const memory = createMemory({ tokenLimit: 1000 });
|
||||
|
||||
await memory.add({ content: "LlamaIndex message", role: "user" });
|
||||
|
||||
const llamaMessages = await memory.get({ type: "llamaindex" });
|
||||
expect(llamaMessages).toHaveLength(1);
|
||||
|
||||
const uiMessages = await memory.get({ type: "vercel" });
|
||||
expect(uiMessages).toHaveLength(1);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,238 @@
|
||||
import { Settings } from "@llamaindex/core/global";
|
||||
import type { ChatMessage } from "@llamaindex/core/llms";
|
||||
import { beforeEach, describe, expect, test } from "vitest";
|
||||
|
||||
import type { UIMessage, UIPart } from "@llamaindex/core/memory";
|
||||
import { Memory, staticMemoryBlock } from "@llamaindex/core/memory";
|
||||
|
||||
describe("Memory", () => {
|
||||
beforeEach(() => {
|
||||
// Mock the Settings.llm
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
(Settings.llm as any) = {
|
||||
metadata: {
|
||||
contextWindow: 1000,
|
||||
maxTokens: 100,
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
describe("constructor and initialization", () => {
|
||||
test("creates with default options", () => {
|
||||
const memory = new Memory();
|
||||
expect(memory).toBeInstanceOf(Memory);
|
||||
});
|
||||
|
||||
test("creates with custom token limit", () => {
|
||||
const memory = new Memory({ tokenLimit: 5000 });
|
||||
// Test through behavior rather than private property access
|
||||
expect(memory).toBeInstanceOf(Memory);
|
||||
});
|
||||
|
||||
test("creates with memory blocks", () => {
|
||||
const blocks = [staticMemoryBlock([], 0)];
|
||||
const memory = new Memory({ blocks });
|
||||
// Test through behavior rather than private property access
|
||||
expect(memory).toBeInstanceOf(Memory);
|
||||
});
|
||||
});
|
||||
|
||||
describe("new API methods", () => {
|
||||
let memory: Memory;
|
||||
|
||||
beforeEach(() => {
|
||||
memory = new Memory({ tokenLimit: 1000 });
|
||||
});
|
||||
|
||||
describe("add method", () => {
|
||||
test("adds ChatMessage successfully", async () => {
|
||||
const message: ChatMessage = { content: "Hello", role: "user" };
|
||||
await memory.add(message);
|
||||
|
||||
const messages = await memory.get();
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0]?.content).toBe("Hello");
|
||||
});
|
||||
|
||||
test("adds UIMessage successfully", async () => {
|
||||
const uiMessage: UIMessage = {
|
||||
id: "msg-1",
|
||||
content: "Hello from UI",
|
||||
role: "user",
|
||||
parts: [{ type: "text", content: "Hello from UI" } as UIPart],
|
||||
createdAt: new Date(),
|
||||
};
|
||||
|
||||
await memory.add(uiMessage);
|
||||
const messages = await memory.get();
|
||||
expect(messages).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("adds multiple messages in sequence", async () => {
|
||||
const messages = [
|
||||
{ content: "Hello", role: "user" as const },
|
||||
{ content: "Hi there!", role: "assistant" as const },
|
||||
{ content: "How are you?", role: "user" as const },
|
||||
];
|
||||
|
||||
for (const msg of messages) {
|
||||
await memory.add(msg);
|
||||
}
|
||||
|
||||
const storedMessages = await memory.get();
|
||||
expect(storedMessages).toHaveLength(3);
|
||||
});
|
||||
|
||||
test("throws error for invalid message format", async () => {
|
||||
const invalidMessage = { content: "test" }; // missing role
|
||||
await expect(
|
||||
memory.add(invalidMessage as unknown as ChatMessage),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe("get method", () => {
|
||||
beforeEach(async () => {
|
||||
await memory.add({ content: "Hello", role: "user" });
|
||||
await memory.add({ content: "Hi there!", role: "assistant" });
|
||||
});
|
||||
|
||||
test("returns messages in LlamaIndex format by default", async () => {
|
||||
const messages = await memory.get();
|
||||
expect(messages).toHaveLength(2);
|
||||
expect(messages[0]).toHaveProperty("content");
|
||||
expect(messages[0]).toHaveProperty("role");
|
||||
});
|
||||
|
||||
test("returns messages in LlamaIndex format when specified", async () => {
|
||||
const messages = await memory.get({ type: "llamaindex" });
|
||||
expect(messages).toHaveLength(2);
|
||||
expect(messages[0]).toHaveProperty("content");
|
||||
expect(messages[0]).toHaveProperty("role");
|
||||
});
|
||||
|
||||
test("returns messages in UI format when specified", async () => {
|
||||
const messages = await memory.get({ type: "vercel" });
|
||||
expect(messages).toHaveLength(2);
|
||||
// UI messages should have id, parts, etc.
|
||||
expect(messages[0]).toHaveProperty("id");
|
||||
expect(messages[0]).toHaveProperty("parts");
|
||||
expect(messages[0]).toHaveProperty("content");
|
||||
expect(messages[0]).toHaveProperty("role");
|
||||
});
|
||||
|
||||
test("handles empty memory", async () => {
|
||||
const emptyMemory = new Memory();
|
||||
const messages = await emptyMemory.get();
|
||||
expect(messages).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("getLLM method", () => {
|
||||
test("returns token-limited messages", async () => {
|
||||
const shortMemory = new Memory({ tokenLimit: 10 });
|
||||
|
||||
// Add messages that exceed token limit
|
||||
await shortMemory.add({
|
||||
content: "Very long message that exceeds token limit",
|
||||
role: "user",
|
||||
});
|
||||
await shortMemory.add({
|
||||
content: "Another long message",
|
||||
role: "assistant",
|
||||
});
|
||||
await shortMemory.add({ content: "Short", role: "user" });
|
||||
|
||||
const llmMessages = await shortMemory.getLLM();
|
||||
const allMessages = await shortMemory.get();
|
||||
|
||||
expect(llmMessages.length).toBeLessThanOrEqual(allMessages.length);
|
||||
});
|
||||
|
||||
test("includes memory blocks in LLM messages", async () => {
|
||||
const blocks = [staticMemoryBlock(["System instruction"], 0)];
|
||||
const memoryWithBlocks = new Memory({
|
||||
blocks,
|
||||
tokenLimit: 1000,
|
||||
});
|
||||
|
||||
await memoryWithBlocks.add({ content: "User message", role: "user" });
|
||||
|
||||
const llmMessages = await memoryWithBlocks.getLLM();
|
||||
expect(llmMessages.length).toBeGreaterThan(1); // Should include block + user message
|
||||
});
|
||||
|
||||
test("prioritizes recent messages when token limit is exceeded", async () => {
|
||||
const memory = new Memory({ tokenLimit: 20 });
|
||||
|
||||
await memory.add({ content: "Old message", role: "user" });
|
||||
await memory.add({ content: "Recent message", role: "user" });
|
||||
|
||||
const llmMessages = await memory.getLLM();
|
||||
const lastMessage = llmMessages[llmMessages.length - 1];
|
||||
expect(lastMessage?.content).toBe("Recent message");
|
||||
});
|
||||
});
|
||||
|
||||
describe("clear method", () => {
|
||||
test("clears all messages", async () => {
|
||||
await memory.add({ content: "Hello", role: "user" });
|
||||
await memory.add({ content: "Hi there!", role: "assistant" });
|
||||
|
||||
let messages = await memory.get();
|
||||
expect(messages).toHaveLength(2);
|
||||
|
||||
await memory.clear();
|
||||
messages = await memory.get();
|
||||
expect(messages).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("memory blocks functionality", () => {
|
||||
test("includes static blocks in LLM messages", async () => {
|
||||
const blocks = [
|
||||
staticMemoryBlock(["You are a helpful assistant."], 0),
|
||||
staticMemoryBlock(["Always be polite."], 0),
|
||||
];
|
||||
|
||||
const memory = new Memory({ blocks, tokenLimit: 1000 });
|
||||
await memory.add({ content: "Hello", role: "user" });
|
||||
|
||||
const llmMessages = await memory.getLLM();
|
||||
expect(llmMessages.length).toBe(3);
|
||||
|
||||
// First messages should be from blocks (converted to system messages)
|
||||
expect(llmMessages[0]?.content).toBe("You are a helpful assistant.");
|
||||
expect(llmMessages[1]?.content).toBe("Always be polite.");
|
||||
expect(llmMessages[2]?.content).toBe("Hello");
|
||||
});
|
||||
});
|
||||
|
||||
describe("error handling", () => {
|
||||
let memory: Memory;
|
||||
|
||||
beforeEach(() => {
|
||||
memory = new Memory({ tokenLimit: 1000 });
|
||||
});
|
||||
|
||||
test("handles invalid message types gracefully", async () => {
|
||||
const invalidMessage = { content: 123, role: "user" }; // invalid content type
|
||||
await expect(
|
||||
memory.add(invalidMessage as unknown as ChatMessage),
|
||||
).rejects.toThrow();
|
||||
});
|
||||
|
||||
test("handles token limit edge cases", async () => {
|
||||
const verySmallMemory = new Memory({ tokenLimit: 1 });
|
||||
await verySmallMemory.add({
|
||||
content: "This message is too long for the token limit",
|
||||
role: "user",
|
||||
});
|
||||
|
||||
const llmMessages = await verySmallMemory.getLLM();
|
||||
// Should handle gracefully, possibly returning empty array
|
||||
expect(Array.isArray(llmMessages)).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,99 @@
|
||||
import type { ChatMessage } from "@llamaindex/core/llms";
|
||||
import type { UIMessage } from "@llamaindex/core/memory";
|
||||
import { MessageConverter } from "@llamaindex/core/memory";
|
||||
import { describe, expect, test } from "vitest";
|
||||
|
||||
describe("MessageConverter", () => {
|
||||
describe("toUIMessage", () => {
|
||||
test("converts simple ChatMessage to UIMessage", () => {
|
||||
const chatMessage: ChatMessage = {
|
||||
content: "Hello, how are you?",
|
||||
role: "user",
|
||||
};
|
||||
|
||||
const uiMessage = MessageConverter.toUIMessage(chatMessage);
|
||||
|
||||
expect(uiMessage).toHaveProperty("id");
|
||||
expect(uiMessage.role).toBe("user");
|
||||
expect(uiMessage.content).toBe("Hello, how are you?");
|
||||
expect(uiMessage).toHaveProperty("parts");
|
||||
expect(uiMessage.parts).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("generates unique IDs for each conversion", () => {
|
||||
const chatMessage: ChatMessage = {
|
||||
content: "Test message",
|
||||
role: "user",
|
||||
};
|
||||
|
||||
const uiMessage1 = MessageConverter.toUIMessage(chatMessage);
|
||||
const uiMessage2 = MessageConverter.toUIMessage(chatMessage);
|
||||
|
||||
expect(uiMessage1.id).not.toBe(uiMessage2.id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("toLlamaIndexMessage", () => {
|
||||
test("converts simple UIMessage to ChatMessage", () => {
|
||||
const uiMessage: UIMessage = {
|
||||
id: "msg-123",
|
||||
content: "Hello, how are you?",
|
||||
role: "user",
|
||||
parts: [],
|
||||
};
|
||||
|
||||
const chatMessage = MessageConverter.toLlamaIndexMessage(uiMessage);
|
||||
|
||||
expect(chatMessage.content).toBe("Hello, how are you?");
|
||||
expect(chatMessage.role).toBe("user");
|
||||
});
|
||||
});
|
||||
|
||||
describe("type guards", () => {
|
||||
test("isChatMessage identifies valid ChatMessage", () => {
|
||||
const validChatMessage = {
|
||||
content: "Hello",
|
||||
role: "user",
|
||||
};
|
||||
|
||||
expect(MessageConverter.isChatMessage(validChatMessage)).toBe(true);
|
||||
});
|
||||
|
||||
test("isChatMessage rejects invalid objects", () => {
|
||||
const invalidObjects = [
|
||||
null,
|
||||
undefined,
|
||||
{ content: "Hello" }, // missing role
|
||||
{ role: "user" }, // missing content
|
||||
];
|
||||
|
||||
for (const obj of invalidObjects) {
|
||||
expect(MessageConverter.isChatMessage(obj)).toBe(false);
|
||||
}
|
||||
});
|
||||
|
||||
test("isUIMessage identifies valid UIMessage", () => {
|
||||
const validUIMessage = {
|
||||
id: "msg-123",
|
||||
content: "Hello",
|
||||
role: "user",
|
||||
parts: [],
|
||||
};
|
||||
|
||||
expect(MessageConverter.isUIMessage(validUIMessage)).toBe(true);
|
||||
});
|
||||
|
||||
test("isUIMessage rejects invalid objects", () => {
|
||||
const invalidObjects = [
|
||||
null,
|
||||
undefined,
|
||||
{ content: "Hello", role: "user" }, // missing id and parts
|
||||
{ id: "123", role: "user" }, // missing content and parts
|
||||
];
|
||||
|
||||
for (const obj of invalidObjects) {
|
||||
expect(MessageConverter.isUIMessage(obj)).toBe(false);
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user