Review finding from @karthik-indla on this PR. src/mem0-utils.ts carried PROVIDER_VERSION = "3.0.2" as a literal. It matches package.json today and misreports the client version from the next release bump onwards, which is the one thing X-Mem0-Client exists to carry. Every other client in the repo injects at build: mem0-ts via __MEM0_SDK_VERSION__, the Python side via importlib.metadata, the CLIs via __CLI_VERSION__. tsup now defines __MEM0_PROVIDER_VERSION__ from package.json, and the source falls back to "dev" only when run unbundled, such as in tests. Verified in the built bundle: PROVIDER_VERSION resolves to "3.0.2" and no placeholder survives. resolveJsonModule is enabled alongside it, matching mem0-ts, because tsc --noEmit covers tsup.config.ts and the package.json import fails without it. Type check clean, build clean. The jest suite's failure is pre-existing and unrelated: it requires a live MEM0_API_KEY, confirmed by running it on a stashed tree. Python side 311 passed, 8 skipped; pi-agent 94 passed. Claude-Session: https://claude.ai/code/session_01C7tEmH86HAr7GoAAKCEHZb
415 lines
17 KiB
TypeScript
415 lines
17 KiB
TypeScript
declare const __MEM0_PROVIDER_VERSION__: string | undefined;
|
|
|
|
// Replaced at build time by tsup `define`. The fallback only applies when the
|
|
// source is run unbundled, such as in tests.
|
|
const PROVIDER_VERSION =
|
|
typeof __MEM0_PROVIDER_VERSION__ !== "undefined" ? __MEM0_PROVIDER_VERSION__ : "dev";
|
|
|
|
import { LanguageModelV3Prompt } from '@ai-sdk/provider';
|
|
import { Mem0ConfigSettings } from './mem0-types';
|
|
import { loadApiKey } from '@ai-sdk/provider-utils';
|
|
interface MultimodalContent {
|
|
type: 'text' | 'image_url' | 'mdx_url' | 'pdf_url';
|
|
text?: string;
|
|
image_url?: {
|
|
url: string;
|
|
};
|
|
mdx_url?: {
|
|
url: string;
|
|
};
|
|
pdf_url?: {
|
|
url: string;
|
|
};
|
|
}
|
|
|
|
interface FileContent {
|
|
type: 'file';
|
|
data: string; // fileDataUrl
|
|
mediaType: string; // e.g., 'application/pdf', 'text/markdown', 'image/jpeg'
|
|
}
|
|
|
|
interface Message {
|
|
role: string;
|
|
content: string | MultimodalContent | Array<MultimodalContent>;
|
|
}
|
|
|
|
const flattenPrompt = (prompt: LanguageModelV3Prompt) => {
|
|
try {
|
|
return prompt.map((part) => {
|
|
if (part.role === "user") {
|
|
if (typeof part.content === 'string') {
|
|
return part.content;
|
|
} else if (Array.isArray(part.content)) {
|
|
return part.content
|
|
.filter((obj) => obj.type === 'text')
|
|
.map((obj) => obj.text)
|
|
.join(" ");
|
|
} else if (part.content && typeof part.content === 'object' && 'type' in part.content) {
|
|
const content = part.content as any;
|
|
if (content.type === 'text' && content.text) {
|
|
return content.text;
|
|
} else if (content.type === 'file') {
|
|
if (content.mediaType === 'application/pdf') {
|
|
return '[PDF document]';
|
|
} else if (content.mediaType === 'text/markdown' || content.mediaType === 'application/mdx') {
|
|
return '[Markdown document]';
|
|
} else if (content.mediaType && content.mediaType.startsWith('image/')) {
|
|
return '[Image]';
|
|
} else {
|
|
return '[File attachment]';
|
|
}
|
|
}
|
|
}
|
|
return "[multimodal content]";
|
|
}
|
|
return "";
|
|
}).join(" ");
|
|
} catch (error) {
|
|
console.error("Error in flattenPrompt:", error);
|
|
return "";
|
|
}
|
|
}
|
|
|
|
const convertToMem0Format = (messages: LanguageModelV3Prompt) => {
|
|
try {
|
|
return messages.flatMap((message: any) => {
|
|
try {
|
|
if (typeof message.content === 'string') {
|
|
return {
|
|
role: message.role,
|
|
content: message.content,
|
|
};
|
|
}
|
|
else if (Array.isArray(message.content)) {
|
|
return message.content.map((obj: any) => {
|
|
try {
|
|
if (obj.type === "text") {
|
|
return {
|
|
role: message.role,
|
|
content: obj.text,
|
|
};
|
|
} else if (obj.type === "file") {
|
|
if (obj.mediaType === "application/pdf") {
|
|
return {
|
|
role: message.role,
|
|
content: {
|
|
type: "pdf_url",
|
|
pdf_url: {
|
|
url: obj.data
|
|
}
|
|
}
|
|
};
|
|
} else if (obj.mediaType === "text/markdown" || obj.mediaType === "application/mdx") {
|
|
return {
|
|
role: message.role,
|
|
content: {
|
|
type: "mdx_url",
|
|
mdx_url: {
|
|
url: obj.data
|
|
}
|
|
}
|
|
};
|
|
} else if (obj.mediaType && obj.mediaType.startsWith("image/")) {
|
|
return {
|
|
role: message.role,
|
|
content: {
|
|
type: "image_url",
|
|
image_url: {
|
|
url: obj.data
|
|
}
|
|
}
|
|
};
|
|
}
|
|
} else if (obj.type === "image_url" || obj.type === "image") {
|
|
return {
|
|
role: message.role,
|
|
content: {
|
|
type: "image_url",
|
|
image_url: {
|
|
url: obj.image_url?.url || obj.image?.url || obj.url
|
|
}
|
|
}
|
|
};
|
|
} else if (obj.type === "mdx_url" || obj.type === "mdx") {
|
|
return {
|
|
role: message.role,
|
|
content: {
|
|
type: "mdx_url",
|
|
mdx_url: {
|
|
url: obj.mdx_url?.url || obj.mdx?.url || obj.url
|
|
}
|
|
}
|
|
};
|
|
} else if (obj.type === "pdf_url" || obj.type === "pdf") {
|
|
return {
|
|
role: message.role,
|
|
content: {
|
|
type: "pdf_url",
|
|
pdf_url: {
|
|
url: obj.pdf_url?.url || obj.pdf?.url || obj.url
|
|
}
|
|
}
|
|
};
|
|
}
|
|
return null;
|
|
} catch (error) {
|
|
console.error("Error processing content object:", error);
|
|
return null;
|
|
}
|
|
}).filter((item: null) => item !== null);
|
|
} else {
|
|
const obj = message.content;
|
|
if (obj.type === "text") {
|
|
return {
|
|
role: message.role,
|
|
content: obj.text,
|
|
};
|
|
} else if (obj.type === "file") {
|
|
if (obj.mediaType === "application/pdf") {
|
|
return {
|
|
role: message.role,
|
|
content: {
|
|
type: "pdf_url",
|
|
pdf_url: {
|
|
url: obj.data
|
|
}
|
|
}
|
|
};
|
|
} else if (obj.mediaType === "text/markdown" || obj.mediaType === "application/mdx") {
|
|
return {
|
|
role: message.role,
|
|
content: {
|
|
type: "mdx_url",
|
|
mdx_url: {
|
|
url: obj.data
|
|
}
|
|
}
|
|
};
|
|
} else if (obj.mediaType && obj.mediaType.startsWith("image/")) {
|
|
return {
|
|
role: message.role,
|
|
content: {
|
|
type: "image_url",
|
|
image_url: {
|
|
url: obj.data
|
|
}
|
|
}
|
|
};
|
|
}
|
|
} else if (obj.type === "image_url" || obj.type === "image") {
|
|
return {
|
|
role: message.role,
|
|
content: {
|
|
type: "image_url",
|
|
image_url: {
|
|
url: obj.image_url?.url || obj.image?.url || obj.url
|
|
}
|
|
}
|
|
};
|
|
} else if (obj.type === "mdx_url" || obj.type === "mdx") {
|
|
return {
|
|
role: message.role,
|
|
content: {
|
|
type: "mdx_url",
|
|
mdx_url: {
|
|
url: obj.mdx_url?.url || obj.mdx?.url || obj.url
|
|
}
|
|
}
|
|
};
|
|
} else if (obj.type === "pdf_url" || obj.type === "pdf") {
|
|
return {
|
|
role: message.role,
|
|
content: {
|
|
type: "pdf_url",
|
|
pdf_url: {
|
|
url: obj.pdf_url?.url || obj.pdf?.url || obj.url
|
|
}
|
|
}
|
|
};
|
|
}
|
|
return null;
|
|
}
|
|
} catch (error) {
|
|
console.error("Error processing message:", error);
|
|
return [];
|
|
}
|
|
});
|
|
} catch (error) {
|
|
console.error("Error in convertToMem0Format:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
const searchInternalMemories = async (query: string, config?: Mem0ConfigSettings, top_k: number = 10) => {
|
|
try {
|
|
// v3: entity IDs go inside the filters object, not as top-level params
|
|
const filters: Record<string, any> = {
|
|
...(config?.filters ?? {}),
|
|
};
|
|
if (config?.user_id) {
|
|
filters.user_id = config.user_id;
|
|
}
|
|
if (config?.app_id) {
|
|
filters.app_id = config.app_id;
|
|
}
|
|
if (config?.agent_id) {
|
|
filters.agent_id = config.agent_id;
|
|
}
|
|
if (config?.run_id) {
|
|
filters.run_id = config.run_id;
|
|
}
|
|
|
|
const apiKey = loadApiKey({
|
|
apiKey: config?.mem0ApiKey,
|
|
environmentVariableName: "MEM0_API_KEY",
|
|
description: "Mem0",
|
|
});
|
|
|
|
const body: Record<string, any> = {
|
|
query,
|
|
filters,
|
|
top_k: config?.top_k ?? top_k,
|
|
};
|
|
if (config?.threshold != null) {
|
|
body.threshold = config.threshold;
|
|
}
|
|
if (config?.rerank != null) {
|
|
body.rerank = config.rerank;
|
|
}
|
|
if (config?.metadata) {
|
|
body.metadata = config.metadata;
|
|
}
|
|
|
|
const options = {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Token ${apiKey}`,
|
|
'Content-Type': 'application/json',
|
|
// Surface attribution. Set-once by contract: this wrapper is the
|
|
// outermost layer on these raw fetch calls.
|
|
'X-Mem0-Source': 'VERCEL_AI_SDK',
|
|
'X-Mem0-Client': `mem0-vercel-ai-provider/${PROVIDER_VERSION}`
|
|
},
|
|
body: JSON.stringify(body),
|
|
};
|
|
|
|
const baseUrl = config?.host || 'https://api.mem0.ai';
|
|
const response = await fetch(`${baseUrl}/v3/memories/search/`, options);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
console.error("Error in searchInternalMemories:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const addMemories = async (messages: LanguageModelV3Prompt, config?: Mem0ConfigSettings) => {
|
|
try {
|
|
let finalMessages: Array<Message> = [];
|
|
if (typeof messages === "string") {
|
|
finalMessages = [{ role: "user", content: messages }];
|
|
} else {
|
|
finalMessages = convertToMem0Format(messages);
|
|
}
|
|
const response = await updateMemories(finalMessages, config);
|
|
return response;
|
|
} catch (error) {
|
|
console.error("Error in addMemories:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const updateMemories = async (messages: Array<Message>, config?: Mem0ConfigSettings) => {
|
|
try {
|
|
const apiKey = loadApiKey({
|
|
apiKey: config?.mem0ApiKey,
|
|
environmentVariableName: "MEM0_API_KEY",
|
|
description: "Mem0",
|
|
});
|
|
|
|
const body: Record<string, any> = { messages };
|
|
if (config?.user_id) body.user_id = config.user_id;
|
|
if (config?.app_id) body.app_id = config.app_id;
|
|
if (config?.agent_id) body.agent_id = config.agent_id;
|
|
if (config?.run_id) body.run_id = config.run_id;
|
|
if (config?.metadata) body.metadata = config.metadata;
|
|
if (config?.infer != null) body.infer = config.infer;
|
|
|
|
const options = {
|
|
method: 'POST',
|
|
headers: {
|
|
Authorization: `Token ${apiKey}`,
|
|
'Content-Type': 'application/json',
|
|
// Surface attribution. Set-once by contract: this wrapper is the
|
|
// outermost layer on these raw fetch calls.
|
|
'X-Mem0-Source': 'VERCEL_AI_SDK',
|
|
'X-Mem0-Client': `mem0-vercel-ai-provider/${PROVIDER_VERSION}`
|
|
},
|
|
body: JSON.stringify(body),
|
|
};
|
|
|
|
const baseUrl = config?.host || 'https://api.mem0.ai';
|
|
const response = await fetch(`${baseUrl}/v3/memories/add/`, options);
|
|
if (!response.ok) {
|
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
}
|
|
const data = await response.json();
|
|
return data;
|
|
} catch (error) {
|
|
console.error("Error in updateMemories:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const retrieveMemories = async (prompt: LanguageModelV3Prompt | string, config?: Mem0ConfigSettings) => {
|
|
try {
|
|
const message = typeof prompt === 'string' ? prompt : flattenPrompt(prompt);
|
|
const systemPrompt = "These are the memories I have stored. Give more weightage to the question by users and try to answer that first. You have to modify your answer based on the memories I have provided. If the memories are irrelevant you can ignore them. Also don't reply to this section of the prompt, or the memories, they are only for your reference. The System prompt starts after text System Message: \n\n";
|
|
|
|
const data = await searchInternalMemories(message, config);
|
|
// The API response may be a flat array or an object with a results key
|
|
const memories: any[] = Array.isArray(data) ? data : (data?.results ?? []);
|
|
|
|
if (memories.length === 0) {
|
|
return "";
|
|
}
|
|
|
|
const memoriesText = memories
|
|
.map((memory: any) => `Memory: ${memory.memory}\n\n`)
|
|
.join("\n\n");
|
|
|
|
return `System Message: ${systemPrompt} ${memoriesText}`;
|
|
} catch (error) {
|
|
console.error("Error in retrieveMemories:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const getMemories = async (prompt: LanguageModelV3Prompt | string, config?: Mem0ConfigSettings) => {
|
|
try {
|
|
const message = typeof prompt === 'string' ? prompt : flattenPrompt(prompt);
|
|
const data = await searchInternalMemories(message, config);
|
|
// Normalize: always return a flat array regardless of API response shape
|
|
return Array.isArray(data) ? data : (data?.results ?? []);
|
|
} catch (error) {
|
|
console.error("Error in getMemories:", error);
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
const searchMemories = async (prompt: LanguageModelV3Prompt | string, config?: Mem0ConfigSettings) => {
|
|
try {
|
|
const message = typeof prompt === 'string' ? prompt : flattenPrompt(prompt);
|
|
const memories = await searchInternalMemories(message, config);
|
|
return memories;
|
|
} catch (error) {
|
|
console.error("Error in searchMemories:", error);
|
|
return [];
|
|
}
|
|
}
|
|
|
|
export {addMemories, updateMemories, retrieveMemories, flattenPrompt, searchMemories, getMemories};
|