TypeScript OSS: Langchain Integration (#2556)

This commit is contained in:
Saket Aryan
2025-04-15 20:08:41 +05:30
committed by GitHub
parent 9f204dc557
commit c3c9205ffa
18 changed files with 1075 additions and 55 deletions

View File

@@ -22,6 +22,7 @@ export const DEFAULT_MEMORY_CONFIG: MemoryConfig = {
config: {
apiKey: process.env.OPENAI_API_KEY || "",
model: "gpt-4-turbo-preview",
modelProperties: undefined,
},
},
enableGraph: false,

View File

@@ -9,43 +9,84 @@ export class ConfigManager {
provider:
userConfig.embedder?.provider ||
DEFAULT_MEMORY_CONFIG.embedder.provider,
config: {
apiKey:
userConfig.embedder?.config?.apiKey ||
DEFAULT_MEMORY_CONFIG.embedder.config.apiKey,
model:
userConfig.embedder?.config?.model ||
DEFAULT_MEMORY_CONFIG.embedder.config.model,
},
config: (() => {
const defaultConf = DEFAULT_MEMORY_CONFIG.embedder.config;
const userConf = userConfig.embedder?.config;
let finalModel: string | any = defaultConf.model;
if (userConf?.model && typeof userConf.model === "object") {
finalModel = userConf.model;
} else if (userConf?.model && typeof userConf.model === "string") {
finalModel = userConf.model;
}
return {
apiKey:
userConf?.apiKey !== undefined
? userConf.apiKey
: defaultConf.apiKey,
model: finalModel,
url: userConf?.url,
};
})(),
},
vectorStore: {
provider:
userConfig.vectorStore?.provider ||
DEFAULT_MEMORY_CONFIG.vectorStore.provider,
config: {
collectionName:
userConfig.vectorStore?.config?.collectionName ||
DEFAULT_MEMORY_CONFIG.vectorStore.config.collectionName,
dimension:
userConfig.vectorStore?.config?.dimension ||
DEFAULT_MEMORY_CONFIG.vectorStore.config.dimension,
...userConfig.vectorStore?.config,
},
config: (() => {
const defaultConf = DEFAULT_MEMORY_CONFIG.vectorStore.config;
const userConf = userConfig.vectorStore?.config;
// Prioritize user-provided client instance
if (userConf?.client && typeof userConf.client === "object") {
return {
client: userConf.client,
// Include other fields from userConf if necessary, or omit defaults
collectionName: userConf.collectionName, // Can be undefined
dimension: userConf.dimension || defaultConf.dimension, // Merge dimension
...userConf, // Include any other passthrough fields from user
};
} else {
// If no client provided, merge standard fields
return {
collectionName:
userConf?.collectionName || defaultConf.collectionName,
dimension: userConf?.dimension || defaultConf.dimension,
// Ensure client is not carried over from defaults if not provided by user
client: undefined,
// Include other passthrough fields from userConf even if no client
...userConf,
};
}
})(),
},
llm: {
provider:
userConfig.llm?.provider || DEFAULT_MEMORY_CONFIG.llm.provider,
config: {
apiKey:
userConfig.llm?.config?.apiKey ||
DEFAULT_MEMORY_CONFIG.llm.config.apiKey,
model:
userConfig.llm?.config?.model ||
DEFAULT_MEMORY_CONFIG.llm.config.model,
modelProperties:
userConfig.llm?.config?.modelProperties ||
DEFAULT_MEMORY_CONFIG.llm.config.modelProperties,
},
config: (() => {
const defaultConf = DEFAULT_MEMORY_CONFIG.llm.config;
const userConf = userConfig.llm?.config;
let finalModel: string | any = defaultConf.model;
if (userConf?.model && typeof userConf.model === "object") {
finalModel = userConf.model;
} else if (userConf?.model && typeof userConf.model === "string") {
finalModel = userConf.model;
}
return {
apiKey:
userConf?.apiKey !== undefined
? userConf.apiKey
: defaultConf.apiKey,
model: finalModel,
modelProperties:
userConf?.modelProperties !== undefined
? userConf.modelProperties
: defaultConf.modelProperties,
};
})(),
},
historyDbPath:
userConfig.historyDbPath || DEFAULT_MEMORY_CONFIG.historyDbPath,

View File

@@ -0,0 +1,50 @@
import { Embeddings } from "@langchain/core/embeddings";
import { Embedder } from "./base";
import { EmbeddingConfig } from "../types";
export class LangchainEmbedder implements Embedder {
private embedderInstance: Embeddings;
private batchSize?: number; // Some LC embedders have batch size
constructor(config: EmbeddingConfig) {
// Check if config.model is provided and is an object (the instance)
if (!config.model || typeof config.model !== "object") {
throw new Error(
"Langchain embedder provider requires an initialized Langchain Embeddings instance passed via the 'model' field in the embedder config.",
);
}
// Basic check for embedding methods
if (
typeof (config.model as any).embedQuery !== "function" ||
typeof (config.model as any).embedDocuments !== "function"
) {
throw new Error(
"Provided Langchain 'instance' in the 'model' field does not appear to be a valid Langchain Embeddings instance (missing embedQuery or embedDocuments method).",
);
}
this.embedderInstance = config.model as Embeddings;
// Store batch size if the instance has it (optional)
this.batchSize = (this.embedderInstance as any).batchSize;
}
async embed(text: string): Promise<number[]> {
try {
// Use embedQuery for single text embedding
return await this.embedderInstance.embedQuery(text);
} catch (error) {
console.error("Error embedding text with Langchain Embedder:", error);
throw error;
}
}
async embedBatch(texts: string[]): Promise<number[][]> {
try {
// Use embedDocuments for batch embedding
// Langchain's embedDocuments handles batching internally if needed/supported
return await this.embedderInstance.embedDocuments(texts);
} catch (error) {
console.error("Error embedding batch with Langchain Embedder:", error);
throw error;
}
}
}

View File

@@ -1,3 +1,5 @@
import { z } from "zod";
export interface GraphToolParameters {
source: string;
destination: string;
@@ -21,6 +23,58 @@ export interface GraphRelationsParameters {
}>;
}
// --- Zod Schemas for Tool Arguments ---
// Schema for simple relationship arguments (Update, Delete)
export const GraphSimpleRelationshipArgsSchema = z.object({
source: z
.string()
.describe("The identifier of the source node in the relationship."),
relationship: z
.string()
.describe("The relationship between the source and destination nodes."),
destination: z
.string()
.describe("The identifier of the destination node in the relationship."),
});
// Schema for adding a relationship (includes types)
export const GraphAddRelationshipArgsSchema =
GraphSimpleRelationshipArgsSchema.extend({
source_type: z
.string()
.describe("The type or category of the source node."),
destination_type: z
.string()
.describe("The type or category of the destination node."),
});
// Schema for extracting entities
export const GraphExtractEntitiesArgsSchema = z.object({
entities: z
.array(
z.object({
entity: z.string().describe("The name or identifier of the entity."),
entity_type: z.string().describe("The type or category of the entity."),
}),
)
.describe("An array of entities with their types."),
});
// Schema for establishing relationships
export const GraphRelationsArgsSchema = z.object({
entities: z
.array(GraphSimpleRelationshipArgsSchema)
.describe("An array of relationships (source, relationship, destination)."),
});
// --- Tool Definitions (using JSON schema, keep as is) ---
// Note: The tool definitions themselves still use JSON schema format
// as expected by the LLM APIs. The Zod schemas above are for internal
// validation and potentially for use with Langchain's .withStructuredOutput
// if we adapt it to handle tool calls via schema.
export const UPDATE_MEMORY_TOOL_GRAPH = {
type: "function",
function: {

View File

@@ -5,6 +5,7 @@ export * from "./embeddings/base";
export * from "./embeddings/openai";
export * from "./embeddings/ollama";
export * from "./embeddings/google";
export * from "./embeddings/langchain";
export * from "./llms/base";
export * from "./llms/openai";
export * from "./llms/google";
@@ -13,8 +14,11 @@ export * from "./llms/anthropic";
export * from "./llms/groq";
export * from "./llms/ollama";
export * from "./llms/mistral";
export * from "./llms/langchain";
export * from "./vector_stores/base";
export * from "./vector_stores/memory";
export * from "./vector_stores/qdrant";
export * from "./vector_stores/redis";
export * from "./vector_stores/supabase";
export * from "./vector_stores/langchain";
export * from "./utils/factory";

View File

@@ -12,7 +12,7 @@ export interface LLMResponse {
export interface LLM {
generateResponse(
messages: Array<{ role: string; content: string }>,
response_format: { type: string },
response_format?: { type: string },
tools?: any[],
): Promise<any>;
generateChat(messages: Message[]): Promise<LLMResponse>;

View File

@@ -0,0 +1,255 @@
import { BaseLanguageModel } from "@langchain/core/language_models/base";
import {
AIMessage,
HumanMessage,
SystemMessage,
BaseMessage,
} from "@langchain/core/messages";
import { z } from "zod";
import { LLM, LLMResponse } from "./base";
import { LLMConfig, Message } from "../types/index";
// Import the schemas directly into LangchainLLM
import { FactRetrievalSchema, MemoryUpdateSchema } from "../prompts";
// Import graph tool argument schemas
import {
GraphExtractEntitiesArgsSchema,
GraphRelationsArgsSchema,
GraphSimpleRelationshipArgsSchema, // Used for delete tool
} from "../graphs/tools";
const convertToLangchainMessages = (messages: Message[]): BaseMessage[] => {
return messages.map((msg) => {
const content =
typeof msg.content === "string"
? msg.content
: JSON.stringify(msg.content);
switch (msg.role?.toLowerCase()) {
case "system":
return new SystemMessage(content);
case "user":
case "human":
return new HumanMessage(content);
case "assistant":
case "ai":
return new AIMessage(content);
default:
console.warn(
`Unsupported message role '${msg.role}' for Langchain. Treating as 'human'.`,
);
return new HumanMessage(content);
}
});
};
export class LangchainLLM implements LLM {
private llmInstance: BaseLanguageModel;
private modelName: string;
constructor(config: LLMConfig) {
if (!config.model || typeof config.model !== "object") {
throw new Error(
"Langchain provider requires an initialized Langchain instance passed via the 'model' field in the LLM config.",
);
}
if (typeof (config.model as any).invoke !== "function") {
throw new Error(
"Provided Langchain 'instance' in the 'model' field does not appear to be a valid Langchain language model (missing invoke method).",
);
}
this.llmInstance = config.model as BaseLanguageModel;
this.modelName =
(this.llmInstance as any).modelId ||
(this.llmInstance as any).model ||
"langchain-model";
}
async generateResponse(
messages: Message[],
response_format?: { type: string },
tools?: any[],
): Promise<string | LLMResponse> {
const langchainMessages = convertToLangchainMessages(messages);
let runnable: any = this.llmInstance;
const invokeOptions: Record<string, any> = {};
let isStructuredOutput = false;
let selectedSchema: z.ZodSchema<any> | null = null;
let isToolCallResponse = false;
// --- Internal Schema Selection Logic (runs regardless of response_format) ---
const systemPromptContent =
(messages.find((m) => m.role === "system")?.content as string) || "";
const userPromptContent =
(messages.find((m) => m.role === "user")?.content as string) || "";
const toolNames = tools?.map((t) => t.function.name) || [];
// Prioritize tool call argument schemas
if (toolNames.includes("extract_entities")) {
selectedSchema = GraphExtractEntitiesArgsSchema;
isToolCallResponse = true;
} else if (toolNames.includes("establish_relationships")) {
selectedSchema = GraphRelationsArgsSchema;
isToolCallResponse = true;
} else if (toolNames.includes("delete_graph_memory")) {
selectedSchema = GraphSimpleRelationshipArgsSchema;
isToolCallResponse = true;
}
// Check for memory prompts if no tool schema matched
else if (
systemPromptContent.includes("Personal Information Organizer") &&
systemPromptContent.includes("extract relevant pieces of information")
) {
selectedSchema = FactRetrievalSchema;
} else if (
userPromptContent.includes("smart memory manager") &&
userPromptContent.includes("Compare newly retrieved facts")
) {
selectedSchema = MemoryUpdateSchema;
}
// --- Apply Structured Output if Schema Selected ---
if (
selectedSchema &&
typeof (this.llmInstance as any).withStructuredOutput === "function"
) {
// Apply if a schema was selected (for memory or single tool calls)
if (
!isToolCallResponse ||
(isToolCallResponse && tools && tools.length === 1)
) {
try {
runnable = (this.llmInstance as any).withStructuredOutput(
selectedSchema,
{ name: tools?.[0]?.function.name },
);
isStructuredOutput = true;
} catch (e) {
isStructuredOutput = false; // Ensure flag is false on error
// No fallback to response_format here unless explicitly passed
if (response_format?.type === "json_object") {
invokeOptions.response_format = { type: "json_object" };
}
}
} else if (isToolCallResponse) {
// If multiple tools, don't apply structured output, handle via tool binding below
}
} else if (selectedSchema && response_format?.type === "json_object") {
// Schema selected, but no .withStructuredOutput. Try basic response_format only if explicitly requested.
if (
(this.llmInstance as any)._identifyingParams?.response_format ||
(this.llmInstance as any).response_format
) {
invokeOptions.response_format = { type: "json_object" };
}
} else if (!selectedSchema && response_format?.type === "json_object") {
// Explicit JSON request, but no schema inferred. Try basic response_format.
if (
(this.llmInstance as any)._identifyingParams?.response_format ||
(this.llmInstance as any).response_format
) {
invokeOptions.response_format = { type: "json_object" };
}
}
// --- Handle tool binding ---
if (tools && tools.length > 0) {
if (typeof (runnable as any).bindTools === "function") {
try {
runnable = (runnable as any).bindTools(tools);
} catch (e) {}
} else {
}
}
// --- Invoke and Process Response ---
try {
const response = await runnable.invoke(langchainMessages, invokeOptions);
if (isStructuredOutput && !isToolCallResponse) {
// Memory prompt with structured output
return JSON.stringify(response);
} else if (isStructuredOutput && isToolCallResponse) {
// Tool call with structured arguments
if (response?.tool_calls && Array.isArray(response.tool_calls)) {
const mappedToolCalls = response.tool_calls.map((call: any) => ({
name: call.name || tools?.[0]?.function.name || "unknown_tool",
arguments:
typeof call.args === "string"
? call.args
: JSON.stringify(call.args),
}));
return {
content: response.content || "",
role: "assistant",
toolCalls: mappedToolCalls,
};
} else {
// Direct object response for tool args
return {
content: "",
role: "assistant",
toolCalls: [
{
name: tools?.[0]?.function.name || "unknown_tool",
arguments: JSON.stringify(response),
},
],
};
}
} else if (
response &&
response.tool_calls &&
Array.isArray(response.tool_calls)
) {
// Standard tool call response (no structured output used/failed)
const mappedToolCalls = response.tool_calls.map((call: any) => ({
name: call.name || "unknown_tool",
arguments:
typeof call.args === "string"
? call.args
: JSON.stringify(call.args),
}));
return {
content: response.content || "",
role: "assistant",
toolCalls: mappedToolCalls,
};
} else if (response && typeof response.content === "string") {
// Standard text response
return response.content;
} else {
// Fallback for unexpected formats
return JSON.stringify(response);
}
} catch (error) {
throw error;
}
}
async generateChat(messages: Message[]): Promise<LLMResponse> {
const langchainMessages = convertToLangchainMessages(messages);
try {
const response = await this.llmInstance.invoke(langchainMessages);
if (response && typeof response.content === "string") {
return {
content: response.content,
role: (response as BaseMessage).lc_id ? "assistant" : "assistant",
};
} else {
console.warn(
`Unexpected response format from Langchain instance (${this.modelName}) for generateChat:`,
response,
);
return {
content: JSON.stringify(response),
role: "assistant",
};
}
} catch (error) {
console.error(
`Error invoking Langchain instance (${this.modelName}) for generateChat:`,
error,
);
throw error;
}
}
}

View File

@@ -43,7 +43,7 @@ export class Memory {
private vectorStore: VectorStore;
private llm: LLM;
private db: HistoryManager;
private collectionName: string;
private collectionName: string | undefined;
private apiVersion: string;
private graphMemory?: MemoryGraph;
private enableGraph: boolean;
@@ -241,12 +241,10 @@ export class Memory {
}
const parsedMessages = messages.map((m) => m.content).join("\n");
// Get prompts
const [systemPrompt, userPrompt] = this.customPrompt
? [this.customPrompt, `Input:\n${parsedMessages}`]
: getFactRetrievalMessages(parsedMessages);
// Extract facts using LLM
const response = await this.llm.generateResponse(
[
{ role: "system", content: systemPrompt },
@@ -255,8 +253,18 @@ export class Memory {
{ type: "json_object" },
);
const cleanResponse = removeCodeBlocks(response);
const facts = JSON.parse(cleanResponse).facts || [];
const cleanResponse = removeCodeBlocks(response as string);
let facts: string[] = [];
try {
facts = JSON.parse(cleanResponse).facts || [];
} catch (e) {
console.error(
"Failed to parse facts from LLM response:",
cleanResponse,
e,
);
facts = [];
}
// Get embeddings for new facts
const newMessageEmbeddings: Record<string, number[]> = {};
@@ -292,13 +300,24 @@ export class Memory {
// Get memory update decisions
const updatePrompt = getUpdateMemoryMessages(uniqueOldMemories, facts);
const updateResponse = await this.llm.generateResponse(
[{ role: "user", content: updatePrompt }],
{ type: "json_object" },
);
const cleanUpdateResponse = removeCodeBlocks(updateResponse);
const memoryActions = JSON.parse(cleanUpdateResponse).memory || [];
const cleanUpdateResponse = removeCodeBlocks(updateResponse as string);
let memoryActions: any[] = [];
try {
memoryActions = JSON.parse(cleanUpdateResponse).memory || [];
} catch (e) {
console.error(
"Failed to parse memory actions from LLM response:",
cleanUpdateResponse,
e,
);
memoryActions = [];
}
// Process memory actions
const results: MemoryItem[] = [];
@@ -511,14 +530,47 @@ export class Memory {
async reset(): Promise<void> {
await this._captureEvent("reset");
await this.db.reset();
await this.vectorStore.deleteCol();
if (this.graphMemory) {
await this.graphMemory.deleteAll({ userId: "default" });
// Check provider before attempting deleteCol
if (this.config.vectorStore.provider.toLowerCase() !== "langchain") {
try {
await this.vectorStore.deleteCol();
} catch (e) {
console.error(
`Failed to delete collection for provider '${this.config.vectorStore.provider}':`,
e,
);
// Decide if you want to re-throw or just log
}
} else {
console.warn(
"Memory.reset(): Skipping vector store collection deletion as 'langchain' provider is used. Underlying Langchain vector store data is not cleared by this operation.",
);
}
if (this.graphMemory) {
await this.graphMemory.deleteAll({ userId: "default" }); // Assuming this is okay, or needs similar check?
}
// Re-initialize factories/clients based on the original config
this.embedder = EmbedderFactory.create(
this.config.embedder.provider,
this.config.embedder.config,
);
// Re-create vector store instance - crucial for Langchain to reset wrapper state if needed
this.vectorStore = VectorStoreFactory.create(
this.config.vectorStore.provider,
this.config.vectorStore.config,
this.config.vectorStore.config, // This will pass the original client instance back
);
this.llm = LLMFactory.create(
this.config.llm.provider,
this.config.llm.config,
);
// Re-init DB if needed (though db.reset() likely handles its state)
// Re-init Graph if needed
// Re-initialize telemetry
this._initializeTelemetry();
}
async getAll(config: GetAllMemoryOptions): Promise<SearchResult> {

View File

@@ -1,3 +1,37 @@
import { z } from "zod";
// Define Zod schema for fact retrieval output
export const FactRetrievalSchema = z.object({
facts: z
.array(z.string())
.describe("An array of distinct facts extracted from the conversation."),
});
// Define Zod schema for memory update output
export const MemoryUpdateSchema = z.object({
memory: z
.array(
z.object({
id: z.string().describe("The unique identifier of the memory item."),
text: z.string().describe("The content of the memory item."),
event: z
.enum(["ADD", "UPDATE", "DELETE", "NONE"])
.describe(
"The action taken for this memory item (ADD, UPDATE, DELETE, or NONE).",
),
old_memory: z
.string()
.optional()
.describe(
"The previous content of the memory item if the event was UPDATE.",
),
}),
)
.describe(
"An array representing the state of memory items after processing new facts.",
),
});
export function getFactRetrievalMessages(
parsedMessages: string,
): [string, string] {

View File

@@ -14,13 +14,15 @@ export interface Message {
export interface EmbeddingConfig {
apiKey?: string;
model?: string;
model?: string | any;
url?: string;
}
export interface VectorStoreConfig {
collectionName: string;
collectionName?: string;
dimension?: number;
client?: any;
instance?: any;
[key: string]: any;
}
@@ -38,7 +40,7 @@ export interface LLMConfig {
provider?: string;
config?: Record<string, any>;
apiKey?: string;
model?: string;
model?: string | any;
modelProperties?: Record<string, any>;
}
@@ -110,24 +112,25 @@ export const MemoryConfigSchema = z.object({
embedder: z.object({
provider: z.string(),
config: z.object({
apiKey: z.string(),
model: z.string().optional(),
apiKey: z.string().optional(),
model: z.union([z.string(), z.any()]).optional(),
}),
}),
vectorStore: z.object({
provider: z.string(),
config: z
.object({
collectionName: z.string(),
collectionName: z.string().optional(),
dimension: z.number().optional(),
client: z.any().optional(),
})
.passthrough(),
}),
llm: z.object({
provider: z.string(),
config: z.object({
apiKey: z.string(),
model: z.string().optional(),
apiKey: z.string().optional(),
model: z.union([z.string(), z.any()]).optional(),
modelProperties: z.record(z.string(), z.any()).optional(),
}),
}),

View File

@@ -26,6 +26,9 @@ import { HistoryManager } from "../storage/base";
import { GoogleEmbedder } from "../embeddings/google";
import { GoogleLLM } from "../llms/google";
import { AzureOpenAILLM } from "../llms/azure";
import { LangchainLLM } from "../llms/langchain";
import { LangchainEmbedder } from "../embeddings/langchain";
import { LangchainVectorStore } from "../vector_stores/langchain";
export class EmbedderFactory {
static create(provider: string, config: EmbeddingConfig): Embedder {
@@ -36,6 +39,8 @@ export class EmbedderFactory {
return new OllamaEmbedder(config);
case "google":
return new GoogleEmbedder(config);
case "langchain":
return new LangchainEmbedder(config);
default:
throw new Error(`Unsupported embedder provider: ${provider}`);
}
@@ -44,7 +49,7 @@ export class EmbedderFactory {
export class LLMFactory {
static create(provider: string, config: LLMConfig): LLM {
switch (provider) {
switch (provider.toLowerCase()) {
case "openai":
return new OpenAILLM(config);
case "openai_structured":
@@ -61,6 +66,8 @@ export class LLMFactory {
return new AzureOpenAILLM(config);
case "mistral":
return new MistralLLM(config);
case "langchain":
return new LangchainLLM(config);
default:
throw new Error(`Unsupported LLM provider: ${provider}`);
}
@@ -73,11 +80,13 @@ export class VectorStoreFactory {
case "memory":
return new MemoryVectorStore(config);
case "qdrant":
return new Qdrant(config as any); // Type assertion needed as config is extended
return new Qdrant(config as any);
case "redis":
return new RedisDB(config as any); // Type assertion needed as config is extended
return new RedisDB(config as any);
case "supabase":
return new SupabaseDB(config as any); // Type assertion needed as config is extended
return new SupabaseDB(config as any);
case "langchain":
return new LangchainVectorStore(config as any);
default:
throw new Error(`Unsupported vector store provider: ${provider}`);
}

View File

@@ -0,0 +1,231 @@
import { VectorStore as LangchainVectorStoreInterface } from "@langchain/core/vectorstores";
import { Document } from "@langchain/core/documents";
import { VectorStore } from "./base"; // mem0's VectorStore interface
import { SearchFilters, VectorStoreConfig, VectorStoreResult } from "../types";
// Config specifically for the Langchain wrapper
interface LangchainStoreConfig extends VectorStoreConfig {
client: LangchainVectorStoreInterface;
// dimension might still be useful for validation if not automatically inferred
}
export class LangchainVectorStore implements VectorStore {
private lcStore: LangchainVectorStoreInterface;
private dimension?: number;
private storeUserId: string = "anonymous-langchain-user"; // Simple in-memory user ID
constructor(config: LangchainStoreConfig) {
if (!config.client || typeof config.client !== "object") {
throw new Error(
"Langchain vector store provider requires an initialized Langchain VectorStore instance passed via the 'client' field.",
);
}
// Basic checks for core methods
if (
typeof config.client.addVectors !== "function" ||
typeof config.client.similaritySearchVectorWithScore !== "function"
) {
throw new Error(
"Provided Langchain 'client' does not appear to be a valid Langchain VectorStore (missing addVectors or similaritySearchVectorWithScore method).",
);
}
this.lcStore = config.client;
this.dimension = config.dimension;
// Attempt to get dimension from the underlying store if not provided
if (
!this.dimension &&
(this.lcStore as any).embeddings?.embeddingDimension
) {
this.dimension = (this.lcStore as any).embeddings.embeddingDimension;
}
if (
!this.dimension &&
(this.lcStore as any).embedding?.embeddingDimension
) {
this.dimension = (this.lcStore as any).embedding.embeddingDimension;
}
// If still no dimension, we might need to throw or warn, as it's needed for validation
if (!this.dimension) {
console.warn(
"LangchainVectorStore: Could not determine embedding dimension. Input validation might be skipped.",
);
}
}
// --- Method Mappings ---
async insert(
vectors: number[][],
ids: string[],
payloads: Record<string, any>[],
): Promise<void> {
if (!ids || ids.length !== vectors.length) {
throw new Error(
"IDs array must be provided and have the same length as vectors.",
);
}
if (this.dimension) {
vectors.forEach((v, i) => {
if (v.length !== this.dimension) {
throw new Error(
`Vector dimension mismatch at index ${i}. Expected ${this.dimension}, got ${v.length}`,
);
}
});
}
// Convert payloads to Langchain Document metadata format
const documents = payloads.map((payload, i) => {
// Provide empty pageContent, store mem0 id and other data in metadata
return new Document({
pageContent: "", // Add required empty pageContent
metadata: { ...payload, _mem0_id: ids[i] },
});
});
// Use addVectors. Note: Langchain stores often generate their own internal IDs.
// We store the mem0 ID in the metadata (`_mem0_id`).
try {
await this.lcStore.addVectors(vectors, documents, { ids }); // Pass mem0 ids if the store supports it
} catch (e) {
// Fallback if the store doesn't support passing ids directly during addVectors
console.warn(
"Langchain store might not support custom IDs on insert. Trying without IDs.",
e,
);
await this.lcStore.addVectors(vectors, documents);
}
}
async search(
query: number[],
limit: number = 5,
filters?: SearchFilters, // filters parameter is received but will be ignored
): Promise<VectorStoreResult[]> {
if (this.dimension && query.length !== this.dimension) {
throw new Error(
`Query vector dimension mismatch. Expected ${this.dimension}, got ${query.length}`,
);
}
// --- Remove filter processing logic ---
// Filters passed via mem0 interface are not reliably translatable to generic Langchain stores.
// let lcFilter: any = undefined;
// if (filters && ...) { ... }
// console.warn("LangchainVectorStore: Passing filters directly..."); // Remove warning
// Call similaritySearchVectorWithScore WITHOUT the filter argument
const results = await this.lcStore.similaritySearchVectorWithScore(
query,
limit,
// Do not pass lcFilter here
);
// Map Langchain results [Document, score] back to mem0 VectorStoreResult
return results.map(([doc, score]) => ({
id: doc.metadata._mem0_id || "unknown_id",
payload: doc.metadata,
score: score,
}));
}
// --- Methods with No Direct Langchain Equivalent (Throwing Errors) ---
async get(vectorId: string): Promise<VectorStoreResult | null> {
// Most Langchain stores lack a direct getById. Simulation is inefficient.
console.error(
`LangchainVectorStore: The 'get' method is not directly supported by most Langchain VectorStores.`,
);
throw new Error(
"Method 'get' not reliably supported by LangchainVectorStore wrapper.",
);
// Potential (inefficient) simulation:
// Perform a search with a filter like { _mem0_id: vectorId }, limit 1.
// This requires the underlying store to support filtering on _mem0_id.
}
async update(
vectorId: string,
vector: number[],
payload: Record<string, any>,
): Promise<void> {
// Updates often require delete + add in Langchain.
console.error(
`LangchainVectorStore: The 'update' method is not directly supported. Use delete followed by insert.`,
);
throw new Error(
"Method 'update' not supported by LangchainVectorStore wrapper.",
);
// Possible implementation: Check if store has delete, call delete({_mem0_id: vectorId}), then insert.
}
async delete(vectorId: string): Promise<void> {
// Check if the underlying store supports deletion by ID
if (typeof (this.lcStore as any).delete === "function") {
try {
// We need to delete based on our stored _mem0_id.
// Langchain's delete often takes its own internal IDs or filter.
// Attempting deletion via filter is the most likely approach.
console.warn(
"LangchainVectorStore: Attempting delete via filter on '_mem0_id'. Success depends on the specific Langchain VectorStore's delete implementation.",
);
await (this.lcStore as any).delete({ filter: { _mem0_id: vectorId } });
// OR if it takes IDs directly (less common for *our* IDs):
// await (this.lcStore as any).delete({ ids: [vectorId] });
} catch (e) {
console.error(
`LangchainVectorStore: Delete failed. Underlying store's delete method might expect different arguments or filters. Error: ${e}`,
);
throw new Error(`Delete failed in underlying Langchain store: ${e}`);
}
} else {
console.error(
`LangchainVectorStore: The underlying Langchain store instance does not seem to support a 'delete' method.`,
);
throw new Error(
"Method 'delete' not available on the provided Langchain VectorStore client.",
);
}
}
async list(
filters?: SearchFilters,
limit: number = 100,
): Promise<[VectorStoreResult[], number]> {
// No standard list method in Langchain core interface.
console.error(
`LangchainVectorStore: The 'list' method is not supported by the generic LangchainVectorStore wrapper.`,
);
throw new Error(
"Method 'list' not supported by LangchainVectorStore wrapper.",
);
// Could potentially be implemented if the underlying store has a specific list/scroll/query capability.
}
async deleteCol(): Promise<void> {
console.error(
`LangchainVectorStore: The 'deleteCol' method is not supported by the generic LangchainVectorStore wrapper.`,
);
throw new Error(
"Method 'deleteCol' not supported by LangchainVectorStore wrapper.",
);
}
// --- Wrapper-Specific Methods (In-Memory User ID) ---
async getUserId(): Promise<string> {
return this.storeUserId;
}
async setUserId(userId: string): Promise<void> {
this.storeUserId = userId;
}
async initialize(): Promise<void> {
// No specific initialization needed for the wrapper itself,
// assuming the passed Langchain client is already initialized.
return Promise.resolve();
}
}