Compare commits

...

1 Commits

Author SHA1 Message Date
leehuwuj e150892f9f init new memory api 2025-06-18 18:09:26 +07:00
10 changed files with 1062 additions and 3 deletions
+70 -2
View File
@@ -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);
}
}
+55
View File
@@ -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);
}
+14 -1
View File
@@ -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";
+140
View File
@@ -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;
}
}
+33
View File
@@ -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";
};
+74
View File
@@ -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);
});
});
+238
View File
@@ -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);
}
});
});
});