diff --git a/docs/docs_skeleton/docs/expression_language/index.mdx b/docs/docs_skeleton/docs/expression_language/index.mdx
index e8615574dcfc..1eb47eec92b2 100644
--- a/docs/docs_skeleton/docs/expression_language/index.mdx
+++ b/docs/docs_skeleton/docs/expression_language/index.mdx
@@ -13,3 +13,6 @@ The base interface shared by all LCEL objects
#### [Cookbook](/docs/expression_language/cookbook)
Examples of common LCEL usage patterns
+
+#### [Why use LCEL](/docs/expression_language/why)
+A deeper dive into the benefits of LCEL
\ No newline at end of file
diff --git a/docs/docs_skeleton/docs/modules/chains/popular/chat_vector_db.mdx b/docs/docs_skeleton/docs/modules/chains/popular/chat_vector_db.mdx
index 5eb184025398..1adc551ac9ba 100644
--- a/docs/docs_skeleton/docs/modules/chains/popular/chat_vector_db.mdx
+++ b/docs/docs_skeleton/docs/modules/chains/popular/chat_vector_db.mdx
@@ -3,11 +3,19 @@ sidebar_position: 2
---
# Conversational Retrieval QA
-The ConversationalRetrievalQA chain builds on RetrievalQAChain to provide a chat history component.
-It first combines the chat history (either explicitly passed in or retrieved from the provided memory) and the question into a standalone question, then looks up relevant documents from the retriever, and finally passes those documents and the question to a question answering chain to return a response.
+:::info
+Looking for the older, non-LCEL version? Click [here](/docs/modules/chains/popular/chat_vector_db_legacy).
+:::
-To create one, you will need a retriever. In the below example, we will create one from a vector store, which can be created from embeddings.
+A common requirement for retrieval-augmented generation chains is support for followup questions.
+Followup questions can contain references to past chat history (e.g. "What did Biden say about Justice Breyer", followed by "Was that nice?"), which make them ill-suited
+to direct retriever similarity search .
+
+To support followups, you can add an additional step prior to retrieval that combines the chat history (either explicitly passed in or retrieved from the provided memory) and the question into a standalone question.
+It then performs the standard retrieval steps of looking up relevant documents from the retriever and passing those documents and the question into a question answering chain to return a response.
+
+To create a conversational question-answering chain, you will need a retriever. In the below example, we will create one from a vector store, which can be created from embeddings.
import Example from "@snippets/modules/chains/popular/chat_vector_db.mdx"
diff --git a/docs/docs_skeleton/docs/modules/chains/popular/chat_vector_db_legacy.mdx b/docs/docs_skeleton/docs/modules/chains/popular/chat_vector_db_legacy.mdx
new file mode 100644
index 000000000000..56ffddce6dd9
--- /dev/null
+++ b/docs/docs_skeleton/docs/modules/chains/popular/chat_vector_db_legacy.mdx
@@ -0,0 +1,15 @@
+# Conversational Retrieval QA
+
+:::info
+Looking for the LCEL version? Click [here](/docs/modules/chains/popular/chat_vector_db).
+:::
+
+The ConversationalRetrievalQA chain builds on RetrievalQAChain to provide a chat history component.
+
+It first combines the chat history (either explicitly passed in or retrieved from the provided memory) and the question into a standalone question, then looks up relevant documents from the retriever, and finally passes those documents and the question to a question answering chain to return a response.
+
+To create one, you will need a retriever. In the below example, we will create one from a vector store, which can be created from embeddings.
+
+import Example from "@snippets/modules/chains/popular/chat_vector_db.mdx"
+
+
diff --git a/docs/docs_skeleton/docs/modules/chains/popular/sqlite.mdx b/docs/docs_skeleton/docs/modules/chains/popular/sqlite.mdx
index e6f60869627d..419ce336d43b 100644
--- a/docs/docs_skeleton/docs/modules/chains/popular/sqlite.mdx
+++ b/docs/docs_skeleton/docs/modules/chains/popular/sqlite.mdx
@@ -1,6 +1,6 @@
# SQL
-This example demonstrates the use of the `SQLDatabaseChain` for answering questions over a SQL database.
+This example demonstrates the use of `Runnables` with questions and more on a SQL database.
import Example from "@snippets/modules/chains/popular/sqlite.mdx"
diff --git a/docs/docs_skeleton/docs/modules/chains/popular/sqlite_legacy.mdx b/docs/docs_skeleton/docs/modules/chains/popular/sqlite_legacy.mdx
new file mode 100644
index 000000000000..8cd1742e91b3
--- /dev/null
+++ b/docs/docs_skeleton/docs/modules/chains/popular/sqlite_legacy.mdx
@@ -0,0 +1,11 @@
+---
+sidebar_class_name: hidden
+---
+
+# SQL
+
+This example demonstrates the use of the `SQLDatabaseChain` for answering questions over a SQL database.
+
+import Example from "@snippets/modules/chains/popular/sqlite_legacy.mdx"
+
+
diff --git a/docs/extras/expression_language/why.mdx b/docs/extras/expression_language/why.mdx
new file mode 100644
index 000000000000..919284e3fec5
--- /dev/null
+++ b/docs/extras/expression_language/why.mdx
@@ -0,0 +1,8 @@
+# Why use LCEL?
+
+The LangChain Expression Language was designed from day 1 to **support putting prototypes in production, with no code changes**, from the simplest “prompt + LLM” chain to the most complex chains (we’ve seen folks successfully running in production LCEL chains with 100s of steps). To highlight a few of the reasons you might want to use LCEL:
+
+- optimised parallel execution: whenever your LCEL chains have steps that can be executed in parallel (eg if you fetch documents from multiple retrievers) we automatically do it, for the smallest possible latency.
+- support for retries and fallbacks: more recently we’ve added support for configuring retries and fallbacks for any part of your LCEL chain. This is a great way to make your chains more reliable at scale. We’re currently working on adding streaming support for retries/fallbacks, so you can get the added reliability without any latency cost.
+- accessing intermediate results: for more complex chains it’s often very useful to access the results of intermediate steps even before the final output is produced. This can be used let end-users know something is happening, or even just to debug your chain. We’ve added support for [streaming intermediate results](https://x.com/LangChainAI/status/1711806009097044193?s=20), and it’s available on every LangServe server.
+- tracing with LangSmith: all chains built with LCEL have first-class tracing support, which can be used to debug your chains, or to understand what’s happening in production. To enable this all you have to do is add your [LangSmith](https://www.langchain.com/langsmith) API key as an environment variable.
\ No newline at end of file
diff --git a/docs/extras/modules/data_connection/vectorstores/integrations/cassandra.mdx b/docs/extras/modules/data_connection/vectorstores/integrations/cassandra.mdx
index 693d40e0d56b..1ea61f8af664 100644
--- a/docs/extras/modules/data_connection/vectorstores/integrations/cassandra.mdx
+++ b/docs/extras/modules/data_connection/vectorstores/integrations/cassandra.mdx
@@ -37,47 +37,43 @@ npm install cassandra-driver
import { CassandraStore } from "langchain/vectorstores/cassandra";
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
-// text sample from Godel, Escher, Bach
+
+const config = {
+ cloud: {
+ secureConnectBundle: process.env.CASSANDRA_SCB as string,
+ },
+ credentials: {
+ username: "token",
+ password: process.env.CASSANDRA_TOKEN as string,
+ },
+ keyspace: "test",
+ dimensions: 1536,
+ table: "test",
+ primaryKey: {
+ name: "id",
+ type: "int",
+ },
+ metadataColumns: [
+ {
+ name: "name",
+ type: "text",
+ },
+ ],
+ };
+
const vectorStore = await CassandraStore.fromTexts(
+ ["I am blue", "Green yellow purple", "Hello there hello"],
[
- "Tortoise: Labyrinth? Labyrinth? Could it Are we in the notorious Little\
- Harmonic Labyrinth of the dreaded Majotaur?",
- "Achilles: Yiikes! What is that?",
- "Tortoise: They say-although I person never believed it myself-that an I\
- Majotaur has created a tiny labyrinth sits in a pit in the middle of\
- it, waiting innocent victims to get lost in its fears complexity.\
- Then, when they wander and dazed into the center, he laughs and\
- laughs at them-so hard, that he laughs them to death!",
- "Achilles: Oh, no!",
- "Tortoise: But it's only a myth. Courage, Achilles.",
+ { id: 2, name: "2" },
+ { id: 1, name: "1" },
+ { id: 3, name: "3" },
],
- [{ id: 2 }, { id: 1 }, { id: 3 }, { id: 4 }, { id: 5 }],
new OpenAIEmbeddings(),
- {
- collectionName: "goldel_escher_bach",
- }
+ cassandraConfig
);
-// or alternatively from docs
-const vectorStore = await CassandraStore.fromDocuments(docs, new OpenAIEmbeddings(), {
- collectionName: "goldel_escher_bach",
-});
-
-const response = await vectorStore.similaritySearch("scared", 2);
-```
-
-## Query docs from existing collection
-
-```typescript
-import { CassandraStore } from "langchain/vectorstores/cassandra";
-import { OpenAIEmbeddings } from "langchain/embeddings/openai";
-
-const vectorStore = await CassandraStore.fromExistingCollection(
- new OpenAIEmbeddings(),
- {
- collectionName: "goldel_escher_bach",
- }
+const results = await vectorStore.similaritySearch(
+ "Green yellow purple",
+ 1
);
-
-const response = await vectorStore.similaritySearch("scared", 2);
-```
+```
\ No newline at end of file
diff --git a/docs/extras/modules/model_io/models/chat/how_to/function_calling.mdx b/docs/extras/modules/model_io/models/chat/how_to/function_calling.mdx
index 28bc5059d788..101065c334e5 100644
--- a/docs/extras/modules/model_io/models/chat/how_to/function_calling.mdx
+++ b/docs/extras/modules/model_io/models/chat/how_to/function_calling.mdx
@@ -17,10 +17,82 @@ directly to the model and call it, as shown below.
## Usage
+There are two main ways to apply functions to your OpenAI calls.
+
+The first and most simple is by attaching a function directly to the `.invoke({})` method:
+
+```typescript
+/* Define your function schema */
+const extractionFunctionSchema = {...}
+
+/* Instantiate ChatOpenAI class */
+const model = new ChatOpenAI({ modelName: "gpt-4" });
+
+/**
+ * Call the .invoke method on the model, directly passing
+ * the function arguments as call args.
+ */
+const result = await model.invoke([new HumanMessage("What a beautiful day!")], {
+ functions: [extractionFunctionSchema],
+ function_call: { name: "extractor" },
+});
+
+console.log({ result });
+```
+
+The second way is by directly binding the function to your model. Binding function arguments to your model is useful when you want to call the same function twice.
+Calling the `.bind({})` method attaches any call arguments passed in to all future calls to the model.
+
+```typescript
+/* Define your function schema */
+const extractionFunctionSchema = {...}
+
+/* Instantiate ChatOpenAI class and bind function arguments to the model */
+const model = new ChatOpenAI({ modelName: "gpt-4" }).bind({
+ functions: [extractionFunctionSchema],
+ function_call: { name: "extractor" },
+});
+
+/* Now we can call the model without having to pass the function arguments in again */
+const result = await model.invoke([new HumanMessage("What a beautiful day!")]);
+
+console.log({ result });
+```
+
OpenAI requires parameter schemas in the format below, where `parameters` must be [JSON Schema](https://json-schema.org/).
-Specifying the `function_call` parameter will force the model to return a response using the specified function.
+When adding call arguments to your model, specifying the `function_call` argument will force the model to return a response using the specified function.
This is useful if you have multiple schemas you'd like the model to pick from.
+Example function schema:
+
+```typescript
+const extractionFunctionSchema = {
+ name: "extractor",
+ description: "Extracts fields from the input.",
+ parameters: {
+ type: "object",
+ properties: {
+ tone: {
+ type: "string",
+ enum: ["positive", "negative"],
+ description: "The overall tone of the input",
+ },
+ word_count: {
+ type: "number",
+ description: "The number of words in the input",
+ },
+ chat_response: {
+ type: "string",
+ description: "A response to the human's input",
+ },
+ },
+ required: ["tone", "word_count", "chat_response"],
+ },
+};
+```
+
+Now to put it all together:
+
{OpenAIFunctionsExample}
## Usage with Zod
diff --git a/docs/extras/modules/model_io/models/llms/integrations/yandex.mdx b/docs/extras/modules/model_io/models/llms/integrations/yandex.mdx
new file mode 100644
index 000000000000..a04e71414c4a
--- /dev/null
+++ b/docs/extras/modules/model_io/models/llms/integrations/yandex.mdx
@@ -0,0 +1,21 @@
+# YandexGPT
+
+LangChain.js supports calling [YandexGPT](https://cloud.yandex.com/en/services/yandexgpt) LLMs.
+
+## Setup
+
+First, you should [create service account](https://cloud.yandex.com/en/docs/iam/operations/sa/create) with the `ai.languageModels.user` role.
+
+Next, you have two authentication options:
+
+- [IAM token](https://cloud.yandex.com/en/docs/iam/operations/iam-token/create-for-sa).
+ You can specify the token in a constructor parameter `iam_token` or in an environment variable `YC_IAM_TOKEN`.
+- [API key](https://cloud.yandex.com/en/docs/iam/operations/api-key/create)
+ You can specify the key in a constructor parameter `api_key` or in an environment variable `YC_API_KEY`.
+
+## Usage
+
+import CodeBlock from "@theme/CodeBlock";
+import YandexGPTExample from "@examples/models/llm/yandex.ts";
+
+{YandexGPTExample}
diff --git a/docs/snippets/modules/chains/popular/chat_vector_db.mdx b/docs/snippets/modules/chains/popular/chat_vector_db.mdx
index 7901d33daa55..04953e25ccf8 100644
--- a/docs/snippets/modules/chains/popular/chat_vector_db.mdx
+++ b/docs/snippets/modules/chains/popular/chat_vector_db.mdx
@@ -1,36 +1,20 @@
import CodeBlock from "@theme/CodeBlock";
import ConvoRetrievalQAExample from "@examples/chains/conversational_qa.ts";
-import Example from "@examples/chains/conversational_qa.ts";
{ConvoRetrievalQAExample}
-In the above code snippet, the fromLLM method of the `ConversationalRetrievalQAChain` class has the following signature:
+Here's an explanation of each step in the `RunnableSequence.from()` call above:
-```typescript
-static fromLLM(
- llm: BaseLanguageModel,
- retriever: BaseRetriever,
- options?: {
- questionGeneratorChainOptions?: {
- llm?: BaseLanguageModel;
- template?: string;
- };
- qaChainOptions?: QAChainParams;
- returnSourceDocuments?: boolean;
- }
-): ConversationalRetrievalQAChain
-```
+- The first input passed is an object containing a `question` key. This key is used as the main input for whatever question a user may ask.
+- The next key is `chatHistory`. This is a string of all previous chats (human & AI) concatenated together. This is used to help the model understand the context of the question.
+- The `context` key is used to fetch relevant documents from the loaded context (in this case the State Of The Union speech). It performs a call to the `getRelevantDocuments` method on the retriever, passing in the user's question as the query. We then pass it to our `serializeDocs` util which maps over all returned documents, joins them with newlines and returns a string.
-Here's an explanation of each of the attributes of the options object:
+After getting and formatting all inputs we pipe them through the following operations:
+- `questionPrompt` - this is the prompt template which we pass to the model in the next step. Behind the scenes it's taking the inputs outlined above and formatting them into the proper spots outlined in our template.
+- The formatted prompt with context then gets passed to the LLM and a response is generated.
+- Finally, we pipe the result of the LLM call to an output parser which formats the response into a readable string.
-- `questionGeneratorChainOptions`: An object that allows you to pass a custom template and LLM to the underlying question generation chain.
- - If the template is provided, the `ConversationalRetrievalQAChain` will use this template to generate a question from the conversation context instead of using the question provided in the question parameter.
- - Passing in a separate LLM (`llm`) here allows you to use a cheaper/faster model to create the condensed question while using a more powerful model for the final response, and can reduce unnecessary latency.
-- `qaChainOptions`: Options that allow you to customize the specific QA chain used in the final step. The default is the [`StuffDocumentsChain`](/docs/modules/chains/document/stuff), but you can customize which chain is used by passing in a `type` parameter.
- **Passing specific options here is completely optional**, but can be useful if you want to customize the way the response is presented to the end user, or if you have too many documents for the default `StuffDocumentsChain`.
- You can see [the API reference of the usable fields here](/docs/api/chains/types/QAChainParams). In case you want to make chat_history available to the final answering `qaChain`, which ultimately answers the user question, you HAVE to pass a custom qaTemplate with chat_history as input, as it is not present in the default Template, which only gets passed `context` documents and generated `question`.
-- `returnSourceDocuments`: A boolean value that indicates whether the `ConversationalRetrievalQAChain` should return the source documents that were used to retrieve the answer. If set to true, the documents will be included in the result returned by the call() method. This can be useful if you want to allow the user to see the sources used to generate the answer. If not set, the default value will be false.
- - If you are using this option and passing in a memory instance, set `inputKey` and `outputKey` on the memory instance to the same values as the chain input and final conversational chain output. These default to `"question"` and `"text"` respectively, and specify the values that the memory should store.
+Using this `RunnableSequence` we can pass questions, and chat history to the model for informed conversational question answering.
## Built-in Memory
@@ -44,37 +28,8 @@ import ConvoQABuiltInExample from "@examples/chains/conversational_qa_built_in_m
## Streaming
-You can also use the above concept of using two different LLMs to stream only the final response from the chain, and not output from the intermediate standalone question generation step. Here's an example:
+You can also stream results from the chain. This is useful if you want to stream the output of the chain to a client, or if you want to stream the output of the chain to another chain.
import ConvoQAStreamingExample from "@examples/chains/conversational_qa_streaming.ts";
{ConvoQAStreamingExample}
-
-## Externally-Managed Memory
-
-For this chain, if you'd like to format the chat history in a custom way (or pass in chat messages directly for convenience), you can also pass the chat history in explicitly by omitting the `memory` option and supplying
-a `chat_history` string or array of [HumanMessages](/docs/api/schema/classes/HumanMessage) and [AIMessages](/docs/api/schema/classes/AIMessage) directly into the `chain.call` method:
-
-import ConvoQAExternalMemoryExample from "@examples/chains/conversational_qa_external_memory.ts";
-
-{ConvoQAExternalMemoryExample}
-
-## Prompt Customization
-
-If you want to further change the chain's behavior, you can change the prompts for both the underlying question generation chain and the QA chain.
-
-One case where you might want to do this is to improve the chain's ability to answer meta questions about the chat history.
-By default, the only input to the QA chain is the standalone question generated from the question generation chain.
-This poses a challenge when asking meta questions about information in previous interactions from the chat history.
-
-For example, if you introduce a friend Bob and mention his age as 28, the chain is unable to provide his age upon asking a question like "How old is Bob?".
-This limitation occurs because the bot searches for Bob in the vector store, rather than considering the message history.
-
-You can pass an alternative prompt for the question generation chain that also returns parts of the chat history relevant to the answer,
-allowing the QA chain to answer meta questions with the additional context:
-
-import ConvoRetrievalQAWithCustomPrompt from "@examples/chains/conversation_qa_custom_prompt.ts";
-
-{ConvoRetrievalQAWithCustomPrompt}
-
-Keep in mind that adding more context to the prompt in this way may distract the LLM from other relevant retrieved information.
diff --git a/docs/snippets/modules/chains/popular/chat_vector_db_legacy.mdx b/docs/snippets/modules/chains/popular/chat_vector_db_legacy.mdx
new file mode 100644
index 000000000000..fbbde4ea7fb2
--- /dev/null
+++ b/docs/snippets/modules/chains/popular/chat_vector_db_legacy.mdx
@@ -0,0 +1,79 @@
+import CodeBlock from "@theme/CodeBlock";
+import ConvoRetrievalQAExample from "@examples/chains/conversational_qa_legacy.ts";
+
+{ConvoRetrievalQAExample}
+
+In the above code snippet, the fromLLM method of the `ConversationalRetrievalQAChain` class has the following signature:
+
+```typescript
+static fromLLM(
+ llm: BaseLanguageModel,
+ retriever: BaseRetriever,
+ options?: {
+ questionGeneratorChainOptions?: {
+ llm?: BaseLanguageModel;
+ template?: string;
+ };
+ qaChainOptions?: QAChainParams;
+ returnSourceDocuments?: boolean;
+ }
+): ConversationalRetrievalQAChain
+```
+
+Here's an explanation of each of the attributes of the options object:
+
+- `questionGeneratorChainOptions`: An object that allows you to pass a custom template and LLM to the underlying question generation chain.
+ - If the template is provided, the `ConversationalRetrievalQAChain` will use this template to generate a question from the conversation context instead of using the question provided in the question parameter.
+ - Passing in a separate LLM (`llm`) here allows you to use a cheaper/faster model to create the condensed question while using a more powerful model for the final response, and can reduce unnecessary latency.
+- `qaChainOptions`: Options that allow you to customize the specific QA chain used in the final step. The default is the [`StuffDocumentsChain`](/docs/modules/chains/document/stuff), but you can customize which chain is used by passing in a `type` parameter.
+ **Passing specific options here is completely optional**, but can be useful if you want to customize the way the response is presented to the end user, or if you have too many documents for the default `StuffDocumentsChain`.
+ You can see [the API reference of the usable fields here](/docs/api/chains/types/QAChainParams). In case you want to make chat_history available to the final answering `qaChain`, which ultimately answers the user question, you HAVE to pass a custom qaTemplate with chat_history as input, as it is not present in the default Template, which only gets passed `context` documents and generated `question`.
+- `returnSourceDocuments`: A boolean value that indicates whether the `ConversationalRetrievalQAChain` should return the source documents that were used to retrieve the answer. If set to true, the documents will be included in the result returned by the call() method. This can be useful if you want to allow the user to see the sources used to generate the answer. If not set, the default value will be false.
+ - If you are using this option and passing in a memory instance, set `inputKey` and `outputKey` on the memory instance to the same values as the chain input and final conversational chain output. These default to `"question"` and `"text"` respectively, and specify the values that the memory should store.
+
+## Built-in Memory
+
+Here's a customization example using a faster LLM to generate questions and a slower, more comprehensive LLM for the final answer. It uses a built-in memory object and returns the referenced source documents.
+Because we have `returnSourceDocuments` set and are thus returning multiple values from the chain, we must set `inputKey` and `outputKey` on the memory instance
+to let it know which values to store.
+
+import ConvoQABuiltInExample from "@examples/chains/conversational_qa_built_in_memory_legacy.ts";
+
+{ConvoQABuiltInExample}
+
+## Streaming
+
+You can also use the above concept of using two different LLMs to stream only the final response from the chain, and not output from the intermediate standalone question generation step. Here's an example:
+
+import ConvoQAStreamingExample from "@examples/chains/conversational_qa_streaming_legacy.ts";
+
+{ConvoQAStreamingExample}
+
+## Externally-Managed Memory
+
+For this chain, if you'd like to format the chat history in a custom way (or pass in chat messages directly for convenience), you can also pass the chat history in explicitly by omitting the `memory` option and supplying
+a `chat_history` string or array of [HumanMessages](/docs/api/schema/classes/HumanMessage) and [AIMessages](/docs/api/schema/classes/AIMessage) directly into the `chain.call` method:
+
+import ConvoQAExternalMemoryExample from "@examples/chains/conversational_qa_external_memory_legacy.ts";
+
+{ConvoQAExternalMemoryExample}
+
+## Prompt Customization
+
+If you want to further change the chain's behavior, you can change the prompts for both the underlying question generation chain and the QA chain.
+
+One case where you might want to do this is to improve the chain's ability to answer meta questions about the chat history.
+By default, the only input to the QA chain is the standalone question generated from the question generation chain.
+This poses a challenge when asking meta questions about information in previous interactions from the chat history.
+
+For example, if you introduce a friend Bob and mention his age as 28, the chain is unable to provide his age upon asking a question like "How old is Bob?".
+This limitation occurs because the bot searches for Bob in the vector store, rather than considering the message history.
+
+You can pass an alternative prompt for the question generation chain that also returns parts of the chat history relevant to the answer,
+allowing the QA chain to answer meta questions with the additional context:
+
+import ConvoRetrievalQAWithCustomPrompt from "@examples/chains/conversation_qa_custom_prompt_legacy.ts";
+
+{ConvoRetrievalQAWithCustomPrompt}
+
+Keep in mind that adding more context to the prompt in this way may distract the LLM from other relevant retrieved information.
\ No newline at end of file
diff --git a/docs/snippets/modules/chains/popular/sqlite.mdx b/docs/snippets/modules/chains/popular/sqlite.mdx
index 521c326e4721..7671d712422d 100644
--- a/docs/snippets/modules/chains/popular/sqlite.mdx
+++ b/docs/snippets/modules/chains/popular/sqlite.mdx
@@ -1,11 +1,13 @@
import CodeBlock from "@theme/CodeBlock";
import SqlDBExample from "@examples/chains/sql_db.ts";
import SqlDBSqlOutputExample from "@examples/chains/sql_db_sql_output.ts";
-import SqlDBSAPHANAExample from "@examples/chains/sql_db_saphana.ts";
-import SqlDBSqlCustomPromptExample from "@examples/chains/sql_db_custom_prompt.ts";
This example uses Chinook database, which is a sample database available for SQL Server, Oracle, MySQL, etc.
+:::info
+Looking for the older, non-LCEL version? Click [here](/docs/modules/chains/popular/sqlite_legacy).
+:::
+
## Set up
First install `typeorm`:
@@ -14,14 +16,13 @@ First install `typeorm`:
npm install typeorm
```
-Then install the dependencies needed for your database. For example, for SQLite:
+Then, install the dependencies needed for your database. For example, for SQLite:
```bash npm2yarn
npm install sqlite3
```
-For other databases see https://typeorm.io/#installation. Currently, LangChain.js has default prompts for
-Postgres, SQLite, Microsoft SQL Server, MySQL, and SAP HANA.
+LangChain offers default prompts for: default SQL, Postgres, SQLite, Microsoft SQL Server, MySQL, and SAP HANA.
Finally follow the instructions on https://database.guide/2-sample-databases-sqlite/ to get the sample database for this example.
@@ -40,15 +41,3 @@ const db = await SqlDatabase.fromDataSourceParams({
If desired, you can return the used SQL command when calling the chain.
{SqlDBSqlOutputExample}
-
-## SAP Hana
-
-Here's an example of using the chain with a SAP HANA database:
-
-{SqlDBSAPHANAExample}
-
-## Custom prompt
-
-You can also customize the prompt that is used. Here is an example prompting the model to understand that "foobar" is the same as the Employee table:
-
-{SqlDBSqlCustomPromptExample}
diff --git a/docs/snippets/modules/chains/popular/sqlite_legacy.mdx b/docs/snippets/modules/chains/popular/sqlite_legacy.mdx
new file mode 100644
index 000000000000..bfb05d1cafc3
--- /dev/null
+++ b/docs/snippets/modules/chains/popular/sqlite_legacy.mdx
@@ -0,0 +1,60 @@
+import CodeBlock from "@theme/CodeBlock";
+import SqlDBExample from "@examples/chains/sql_db_legacy.ts";
+import SqlDBSqlOutputExample from "@examples/chains/sql_db_sql_output_legacy.ts";
+import SqlDBSAPHANAExample from "@examples/chains/sql_db_saphana_legacy.ts";
+import SqlDBSqlCustomPromptExample from "@examples/chains/sql_db_custom_prompt_legacy.ts";
+
+This example uses Chinook database, which is a sample database available for SQL Server, Oracle, MySQL, etc.
+
+:::info
+These are legacy docs. It is now recommended to use LCEL over legacy implementations.
+
+Looking for the LCEL docs? Click [here](/docs/modules/chains/popular/sqlite).
+:::
+
+## Set up
+
+First install `typeorm`:
+
+```bash npm2yarn
+npm install typeorm
+```
+
+Then install the dependencies needed for your database. For example, for SQLite:
+
+```bash npm2yarn
+npm install sqlite3
+```
+
+Currently, LangChain.js has default prompts for
+Postgres, SQLite, Microsoft SQL Server, MySQL, and SAP HANA.
+
+Finally follow the instructions on https://database.guide/2-sample-databases-sqlite/ to get the sample database for this example.
+
+{SqlDBExample}
+
+You can include or exclude tables when creating the `SqlDatabase` object to help the chain focus on the tables you want.
+It can also reduce the number of tokens used in the chain.
+
+```typescript
+const db = await SqlDatabase.fromDataSourceParams({
+ appDataSource: datasource,
+ includesTables: ["Track"],
+});
+```
+
+If desired, you can return the used SQL command when calling the chain.
+
+{SqlDBSqlOutputExample}
+
+## SAP Hana
+
+Here's an example of using the chain with a SAP HANA database:
+
+{SqlDBSAPHANAExample}
+
+## Custom prompt
+
+You can also customize the prompt that is used. Here is an example prompting the model to understand that "foobar" is the same as the Employee table:
+
+{SqlDBSqlCustomPromptExample}
diff --git a/environment_tests/test-exports-bun/src/entrypoints.js b/environment_tests/test-exports-bun/src/entrypoints.js
index cbdbfa704ebe..6e08cbe691fc 100644
--- a/environment_tests/test-exports-bun/src/entrypoints.js
+++ b/environment_tests/test-exports-bun/src/entrypoints.js
@@ -18,6 +18,7 @@ export * from "langchain/llms/ai21";
export * from "langchain/llms/aleph_alpha";
export * from "langchain/llms/ollama";
export * from "langchain/llms/fireworks";
+export * from "langchain/llms/yandex";
export * from "langchain/prompts";
export * from "langchain/vectorstores/base";
export * from "langchain/vectorstores/memory";
diff --git a/environment_tests/test-exports-cf/src/entrypoints.js b/environment_tests/test-exports-cf/src/entrypoints.js
index cbdbfa704ebe..6e08cbe691fc 100644
--- a/environment_tests/test-exports-cf/src/entrypoints.js
+++ b/environment_tests/test-exports-cf/src/entrypoints.js
@@ -18,6 +18,7 @@ export * from "langchain/llms/ai21";
export * from "langchain/llms/aleph_alpha";
export * from "langchain/llms/ollama";
export * from "langchain/llms/fireworks";
+export * from "langchain/llms/yandex";
export * from "langchain/prompts";
export * from "langchain/vectorstores/base";
export * from "langchain/vectorstores/memory";
diff --git a/environment_tests/test-exports-cjs/src/entrypoints.js b/environment_tests/test-exports-cjs/src/entrypoints.js
index 6f4259ce1997..4391d69fb0e0 100644
--- a/environment_tests/test-exports-cjs/src/entrypoints.js
+++ b/environment_tests/test-exports-cjs/src/entrypoints.js
@@ -18,6 +18,7 @@ const llms_ai21 = require("langchain/llms/ai21");
const llms_aleph_alpha = require("langchain/llms/aleph_alpha");
const llms_ollama = require("langchain/llms/ollama");
const llms_fireworks = require("langchain/llms/fireworks");
+const llms_yandex = require("langchain/llms/yandex");
const prompts = require("langchain/prompts");
const vectorstores_base = require("langchain/vectorstores/base");
const vectorstores_memory = require("langchain/vectorstores/memory");
diff --git a/environment_tests/test-exports-esbuild/src/entrypoints.js b/environment_tests/test-exports-esbuild/src/entrypoints.js
index 7c01c36dc2a4..d430748fbcd4 100644
--- a/environment_tests/test-exports-esbuild/src/entrypoints.js
+++ b/environment_tests/test-exports-esbuild/src/entrypoints.js
@@ -18,6 +18,7 @@ import * as llms_ai21 from "langchain/llms/ai21";
import * as llms_aleph_alpha from "langchain/llms/aleph_alpha";
import * as llms_ollama from "langchain/llms/ollama";
import * as llms_fireworks from "langchain/llms/fireworks";
+import * as llms_yandex from "langchain/llms/yandex";
import * as prompts from "langchain/prompts";
import * as vectorstores_base from "langchain/vectorstores/base";
import * as vectorstores_memory from "langchain/vectorstores/memory";
diff --git a/environment_tests/test-exports-esm/src/entrypoints.js b/environment_tests/test-exports-esm/src/entrypoints.js
index 7c01c36dc2a4..d430748fbcd4 100644
--- a/environment_tests/test-exports-esm/src/entrypoints.js
+++ b/environment_tests/test-exports-esm/src/entrypoints.js
@@ -18,6 +18,7 @@ import * as llms_ai21 from "langchain/llms/ai21";
import * as llms_aleph_alpha from "langchain/llms/aleph_alpha";
import * as llms_ollama from "langchain/llms/ollama";
import * as llms_fireworks from "langchain/llms/fireworks";
+import * as llms_yandex from "langchain/llms/yandex";
import * as prompts from "langchain/prompts";
import * as vectorstores_base from "langchain/vectorstores/base";
import * as vectorstores_memory from "langchain/vectorstores/memory";
diff --git a/environment_tests/test-exports-vercel/src/entrypoints.js b/environment_tests/test-exports-vercel/src/entrypoints.js
index cbdbfa704ebe..6e08cbe691fc 100644
--- a/environment_tests/test-exports-vercel/src/entrypoints.js
+++ b/environment_tests/test-exports-vercel/src/entrypoints.js
@@ -18,6 +18,7 @@ export * from "langchain/llms/ai21";
export * from "langchain/llms/aleph_alpha";
export * from "langchain/llms/ollama";
export * from "langchain/llms/fireworks";
+export * from "langchain/llms/yandex";
export * from "langchain/prompts";
export * from "langchain/vectorstores/base";
export * from "langchain/vectorstores/memory";
diff --git a/environment_tests/test-exports-vite/src/entrypoints.js b/environment_tests/test-exports-vite/src/entrypoints.js
index cbdbfa704ebe..6e08cbe691fc 100644
--- a/environment_tests/test-exports-vite/src/entrypoints.js
+++ b/environment_tests/test-exports-vite/src/entrypoints.js
@@ -18,6 +18,7 @@ export * from "langchain/llms/ai21";
export * from "langchain/llms/aleph_alpha";
export * from "langchain/llms/ollama";
export * from "langchain/llms/fireworks";
+export * from "langchain/llms/yandex";
export * from "langchain/prompts";
export * from "langchain/vectorstores/base";
export * from "langchain/vectorstores/memory";
diff --git a/examples/.eslintrc.cjs b/examples/.eslintrc.cjs
index 8b69f0877284..44c60fcc1981 100644
--- a/examples/.eslintrc.cjs
+++ b/examples/.eslintrc.cjs
@@ -10,7 +10,7 @@ module.exports = {
parser: "@typescript-eslint/parser",
sourceType: "module",
},
- plugins: ["@typescript-eslint"],
+ plugins: ["@typescript-eslint", "unused-imports"],
rules: {
"@typescript-eslint/explicit-module-boundary-types": 0,
"@typescript-eslint/no-empty-function": 0,
@@ -41,5 +41,6 @@ module.exports = {
"no-useless-constructor": 0,
"no-else-return": 0,
semi: ["error", "always"],
+ "unused-imports/no-unused-imports": "error",
},
};
diff --git a/examples/package.json b/examples/package.json
index 31d438f7cd9b..b9c1c64314ca 100644
--- a/examples/package.json
+++ b/examples/package.json
@@ -72,6 +72,7 @@
"eslint-config-prettier": "^8.6.0",
"eslint-plugin-import": "^2.27.5",
"eslint-plugin-prettier": "^4.2.1",
+ "eslint-plugin-unused-imports": "^3.0.0",
"prettier": "^2.8.3",
"tsx": "^3.12.3",
"typescript": "^5.0.0"
diff --git a/examples/src/agents/custom_llm_agent.ts b/examples/src/agents/custom_llm_agent.ts
index 8bcdc87657f7..37bfd3be8b54 100644
--- a/examples/src/agents/custom_llm_agent.ts
+++ b/examples/src/agents/custom_llm_agent.ts
@@ -6,7 +6,6 @@ import {
import { LLMChain } from "langchain/chains";
import { OpenAI } from "langchain/llms/openai";
import {
- BasePromptTemplate,
BaseStringPromptTemplate,
SerializedBasePromptTemplate,
renderTemplate,
diff --git a/examples/src/agents/custom_llm_agent_chat.ts b/examples/src/agents/custom_llm_agent_chat.ts
index 5ea436b493f7..1b7493ca567a 100644
--- a/examples/src/agents/custom_llm_agent_chat.ts
+++ b/examples/src/agents/custom_llm_agent_chat.ts
@@ -7,7 +7,6 @@ import { LLMChain } from "langchain/chains";
import { ChatOpenAI } from "langchain/chat_models/openai";
import {
BaseChatPromptTemplate,
- BasePromptTemplate,
SerializedBasePromptTemplate,
renderTemplate,
} from "langchain/prompts";
diff --git a/examples/src/chains/advanced_subclass.ts b/examples/src/chains/advanced_subclass.ts
index 806709013ac8..db200264605a 100644
--- a/examples/src/chains/advanced_subclass.ts
+++ b/examples/src/chains/advanced_subclass.ts
@@ -1,5 +1,4 @@
import { CallbackManagerForChainRun } from "langchain/callbacks";
-import { BaseChain as _ } from "langchain/chains";
import { BaseMemory } from "langchain/memory";
import { ChainValues } from "langchain/schema";
diff --git a/examples/src/chains/conversation_qa_custom_prompt.ts b/examples/src/chains/conversation_qa_custom_prompt_legacy.ts
similarity index 100%
rename from examples/src/chains/conversation_qa_custom_prompt.ts
rename to examples/src/chains/conversation_qa_custom_prompt_legacy.ts
diff --git a/examples/src/chains/conversational_qa.ts b/examples/src/chains/conversational_qa.ts
index d1cb28f81ddc..d282753a8331 100644
--- a/examples/src/chains/conversational_qa.ts
+++ b/examples/src/chains/conversational_qa.ts
@@ -1,38 +1,97 @@
import { ChatOpenAI } from "langchain/chat_models/openai";
-import { ConversationalRetrievalQAChain } from "langchain/chains";
import { HNSWLib } from "langchain/vectorstores/hnswlib";
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
-import { BufferMemory } from "langchain/memory";
import * as fs from "fs";
+import { PromptTemplate } from "langchain/prompts";
+import { RunnableSequence } from "langchain/schema/runnable";
+import { Document } from "langchain/document";
+import { StringOutputParser } from "langchain/schema/output_parser";
-export const run = async () => {
- /* Initialize the LLM to use to answer the question */
- const model = new ChatOpenAI({});
- /* Load in the file we want to do question answering over */
- const text = fs.readFileSync("state_of_the_union.txt", "utf8");
- /* Split the text into chunks */
- const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000 });
- const docs = await textSplitter.createDocuments([text]);
- /* Create the vectorstore */
- const vectorStore = await HNSWLib.fromDocuments(docs, new OpenAIEmbeddings());
- /* Create the chain */
- const chain = ConversationalRetrievalQAChain.fromLLM(
- model,
- vectorStore.asRetriever(),
- {
- memory: new BufferMemory({
- memoryKey: "chat_history", // Must be set to "chat_history"
- }),
- }
- );
- /* Ask it a question */
- const question = "What did the president say about Justice Breyer?";
- const res = await chain.call({ question });
- console.log(res);
- /* Ask it a follow up question */
- const followUpRes = await chain.call({
- question: "Was that nice?",
- });
- console.log(followUpRes);
+/* Initialize the LLM to use to answer the question */
+const model = new ChatOpenAI({});
+/* Load in the file we want to do question answering over */
+const text = fs.readFileSync("state_of_the_union.txt", "utf8");
+/* Split the text into chunks */
+const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000 });
+const docs = await textSplitter.createDocuments([text]);
+/* Create the vectorstore */
+const vectorStore = await HNSWLib.fromDocuments(docs, new OpenAIEmbeddings());
+const retriever = vectorStore.asRetriever();
+
+const serializeDocs = (docs: Array) =>
+ docs.map((doc) => doc.pageContent).join("\n\n");
+
+const formatChatHistory = (
+ human: string,
+ ai: string,
+ previousChatHistory?: string
+) => {
+ const newInteraction = `Human: ${human}\nAI: ${ai}`;
+ if (!previousChatHistory) {
+ return newInteraction;
+ }
+ return `${previousChatHistory}\n\n${newInteraction}`;
};
+
+/**
+ * Create a prompt template for generating an answer based on context and
+ * a question.
+ *
+ * Chat history will be an empty string if it's the first question.
+ *
+ * inputVariables: ["chatHistory", "context", "question"]
+ */
+const questionPrompt = PromptTemplate.fromTemplate(
+ `Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.
+ ----------------
+ CONTEXT: {context}
+ ----------------
+ CHAT HISTORY: {chatHistory}
+ ----------------
+ QUESTION: {question}
+ ----------------
+ Helpful Answer:`
+);
+
+const chain = RunnableSequence.from([
+ {
+ question: (input: { question: string; chatHistory?: string }) =>
+ input.question,
+ chatHistory: (input: { question: string; chatHistory?: string }) =>
+ input.chatHistory ?? "",
+ context: async (input: { question: string; chatHistory?: string }) => {
+ const relevantDocs = await retriever.getRelevantDocuments(input.question);
+ const serialized = serializeDocs(relevantDocs);
+ return serialized;
+ },
+ },
+ questionPrompt,
+ model,
+ new StringOutputParser(),
+]);
+
+const questionOne = "What did the president say about Justice Breyer?";
+
+const resultOne = await chain.invoke({
+ question: questionOne,
+});
+
+console.log({ resultOne });
+/**
+ * {
+ * resultOne: 'The president thanked Justice Breyer for his service and described him as an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court.'
+ * }
+ */
+
+const resultTwo = await chain.invoke({
+ chatHistory: formatChatHistory(resultOne, questionOne),
+ question: "Was it nice?",
+});
+
+console.log({ resultTwo });
+/**
+ * {
+ * resultTwo: "Yes, the president's description of Justice Breyer was positive."
+ * }
+ */
diff --git a/examples/src/chains/conversational_qa_built_in_memory.ts b/examples/src/chains/conversational_qa_built_in_memory.ts
index 3c1b2a1be51a..b174b4139b5a 100644
--- a/examples/src/chains/conversational_qa_built_in_memory.ts
+++ b/examples/src/chains/conversational_qa_built_in_memory.ts
@@ -1,44 +1,172 @@
+import { Document } from "langchain/document";
import { ChatOpenAI } from "langchain/chat_models/openai";
-import { ConversationalRetrievalQAChain } from "langchain/chains";
+import { LLMChain } from "langchain/chains";
import { HNSWLib } from "langchain/vectorstores/hnswlib";
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
import { BufferMemory } from "langchain/memory";
-
import * as fs from "fs";
+import { PromptTemplate } from "langchain/prompts";
+import { RunnableSequence } from "langchain/schema/runnable";
+import { BaseMessage } from "langchain/schema";
-export const run = async () => {
- const text = fs.readFileSync("state_of_the_union.txt", "utf8");
- const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000 });
- const docs = await textSplitter.createDocuments([text]);
- const vectorStore = await HNSWLib.fromDocuments(docs, new OpenAIEmbeddings());
- const fasterModel = new ChatOpenAI({
- modelName: "gpt-3.5-turbo",
- });
- const slowerModel = new ChatOpenAI({
- modelName: "gpt-4",
+const text = fs.readFileSync("state_of_the_union.txt", "utf8");
+
+const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000 });
+const docs = await textSplitter.createDocuments([text]);
+
+const vectorStore = await HNSWLib.fromDocuments(docs, new OpenAIEmbeddings());
+const retriever = vectorStore.asRetriever();
+
+const memory = new BufferMemory({
+ memoryKey: "chatHistory",
+ inputKey: "question", // The key for the input to the chain
+ outputKey: "text", // The key for the final conversational output of the chain
+ returnMessages: true, // If using with a chat model (e.g. gpt-3.5 or gpt-4)
+});
+
+const serializeDocs = (docs: Array): string =>
+ docs.map((doc) => doc.pageContent).join("\n");
+
+const serializeChatHistory = (chatHistory: Array): string =>
+ chatHistory
+ .map((chatMessage) => {
+ if (chatMessage._getType() === "human") {
+ return `Human: ${chatMessage.content}`;
+ } else if (chatMessage._getType() === "ai") {
+ return `Assistant: ${chatMessage.content}`;
+ } else {
+ return `${chatMessage.content}`;
+ }
+ })
+ .join("\n");
+
+/**
+ * Create two prompt templates, one for answering questions, and one for
+ * generating questions.
+ */
+const questionPrompt = PromptTemplate.fromTemplate(
+ `Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.
+----------
+CONTEXT: {context}
+----------
+CHAT HISTORY: {chatHistory}
+----------
+QUESTION: {question}
+----------
+Helpful Answer:`
+);
+const questionGeneratorTemplate = PromptTemplate.fromTemplate(
+ `Given the following conversation and a follow up question, rephrase the follow up question to be a standalone question.
+----------
+CHAT HISTORY: {chatHistory}
+----------
+FOLLOWUP QUESTION: {question}
+----------
+Standalone question:`
+);
+
+// Initialize fast and slow LLMs, along with chains for each
+const fasterModel = new ChatOpenAI({
+ modelName: "gpt-3.5-turbo",
+});
+const fasterChain = new LLMChain({
+ llm: fasterModel,
+ prompt: questionGeneratorTemplate,
+});
+
+const slowerModel = new ChatOpenAI({
+ modelName: "gpt-4",
+});
+const slowerChain = new LLMChain({
+ llm: slowerModel,
+ prompt: questionPrompt,
+});
+
+const performQuestionAnswering = async (input: {
+ question: string;
+ chatHistory: Array | null;
+ context: Array;
+}): Promise<{ result: string; sourceDocuments: Array }> => {
+ let newQuestion = input.question;
+ // Serialize context and chat history into strings
+ const serializedDocs = serializeDocs(input.context);
+ const chatHistoryString = input.chatHistory
+ ? serializeChatHistory(input.chatHistory)
+ : null;
+
+ if (chatHistoryString) {
+ // Call the faster chain to generate a new question
+ const { text } = await fasterChain.invoke({
+ chatHistory: chatHistoryString,
+ context: serializedDocs,
+ question: input.question,
+ });
+
+ newQuestion = text;
+ }
+
+ const response = await slowerChain.invoke({
+ chatHistory: chatHistoryString ?? "",
+ context: serializedDocs,
+ question: newQuestion,
});
- const chain = ConversationalRetrievalQAChain.fromLLM(
- slowerModel,
- vectorStore.asRetriever(),
+
+ // Save the chat history to memory
+ await memory.saveContext(
{
- returnSourceDocuments: true,
- memory: new BufferMemory({
- memoryKey: "chat_history",
- inputKey: "question", // The key for the input to the chain
- outputKey: "text", // The key for the final conversational output of the chain
- returnMessages: true, // If using with a chat model (e.g. gpt-3.5 or gpt-4)
- }),
- questionGeneratorChainOptions: {
- llm: fasterModel,
- },
+ question: input.question,
+ },
+ {
+ text: response.text,
}
);
- /* Ask it a question */
- const question = "What did the president say about Justice Breyer?";
- const res = await chain.call({ question });
- console.log(res);
- const followUpRes = await chain.call({ question: "Was that nice?" });
- console.log(followUpRes);
+ return {
+ result: response.text,
+ sourceDocuments: input.context,
+ };
};
+
+const chain = RunnableSequence.from([
+ {
+ // Pipe the question through unchanged
+ question: (input: { question: string }) => input.question,
+ // Fetch the chat history, and return the history or null if not present
+ chatHistory: async () => {
+ const savedMemory = await memory.loadMemoryVariables({});
+ const hasHistory = savedMemory.chatHistory.length > 0;
+ return hasHistory ? savedMemory.chatHistory : null;
+ },
+ // Fetch relevant context based on the question
+ context: async (input: { question: string }) =>
+ retriever.getRelevantDocuments(input.question),
+ },
+ performQuestionAnswering,
+]);
+
+const resultOne = await chain.invoke({
+ question: "What did the president say about Justice Breyer?",
+});
+console.log({ resultOne });
+/**
+ * {
+ * resultOne: {
+ * result: "The president thanked Justice Breyer for his service and described him as an Army veteran, Constitutional scholar, and retiring Justice of the United States Supreme Court.",
+ * sourceDocuments: [...]
+ * }
+ * }
+ */
+
+const resultTwo = await chain.invoke({
+ question: "Was he nice?",
+});
+console.log({ resultTwo });
+/**
+ * {
+ * resultTwo: {
+ * result: "Yes, the president's description of Justice Breyer was positive."
+ * sourceDocuments: [...]
+ * }
+ * }
+ */
diff --git a/examples/src/chains/conversational_qa_built_in_memory_legacy.ts b/examples/src/chains/conversational_qa_built_in_memory_legacy.ts
new file mode 100644
index 000000000000..3c1b2a1be51a
--- /dev/null
+++ b/examples/src/chains/conversational_qa_built_in_memory_legacy.ts
@@ -0,0 +1,44 @@
+import { ChatOpenAI } from "langchain/chat_models/openai";
+import { ConversationalRetrievalQAChain } from "langchain/chains";
+import { HNSWLib } from "langchain/vectorstores/hnswlib";
+import { OpenAIEmbeddings } from "langchain/embeddings/openai";
+import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
+import { BufferMemory } from "langchain/memory";
+
+import * as fs from "fs";
+
+export const run = async () => {
+ const text = fs.readFileSync("state_of_the_union.txt", "utf8");
+ const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000 });
+ const docs = await textSplitter.createDocuments([text]);
+ const vectorStore = await HNSWLib.fromDocuments(docs, new OpenAIEmbeddings());
+ const fasterModel = new ChatOpenAI({
+ modelName: "gpt-3.5-turbo",
+ });
+ const slowerModel = new ChatOpenAI({
+ modelName: "gpt-4",
+ });
+ const chain = ConversationalRetrievalQAChain.fromLLM(
+ slowerModel,
+ vectorStore.asRetriever(),
+ {
+ returnSourceDocuments: true,
+ memory: new BufferMemory({
+ memoryKey: "chat_history",
+ inputKey: "question", // The key for the input to the chain
+ outputKey: "text", // The key for the final conversational output of the chain
+ returnMessages: true, // If using with a chat model (e.g. gpt-3.5 or gpt-4)
+ }),
+ questionGeneratorChainOptions: {
+ llm: fasterModel,
+ },
+ }
+ );
+ /* Ask it a question */
+ const question = "What did the president say about Justice Breyer?";
+ const res = await chain.call({ question });
+ console.log(res);
+
+ const followUpRes = await chain.call({ question: "Was that nice?" });
+ console.log(followUpRes);
+};
diff --git a/examples/src/chains/conversational_qa_external_memory.ts b/examples/src/chains/conversational_qa_external_memory_legacy.ts
similarity index 100%
rename from examples/src/chains/conversational_qa_external_memory.ts
rename to examples/src/chains/conversational_qa_external_memory_legacy.ts
diff --git a/examples/src/chains/conversational_qa_legacy.ts b/examples/src/chains/conversational_qa_legacy.ts
new file mode 100644
index 000000000000..d1cb28f81ddc
--- /dev/null
+++ b/examples/src/chains/conversational_qa_legacy.ts
@@ -0,0 +1,38 @@
+import { ChatOpenAI } from "langchain/chat_models/openai";
+import { ConversationalRetrievalQAChain } from "langchain/chains";
+import { HNSWLib } from "langchain/vectorstores/hnswlib";
+import { OpenAIEmbeddings } from "langchain/embeddings/openai";
+import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
+import { BufferMemory } from "langchain/memory";
+import * as fs from "fs";
+
+export const run = async () => {
+ /* Initialize the LLM to use to answer the question */
+ const model = new ChatOpenAI({});
+ /* Load in the file we want to do question answering over */
+ const text = fs.readFileSync("state_of_the_union.txt", "utf8");
+ /* Split the text into chunks */
+ const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000 });
+ const docs = await textSplitter.createDocuments([text]);
+ /* Create the vectorstore */
+ const vectorStore = await HNSWLib.fromDocuments(docs, new OpenAIEmbeddings());
+ /* Create the chain */
+ const chain = ConversationalRetrievalQAChain.fromLLM(
+ model,
+ vectorStore.asRetriever(),
+ {
+ memory: new BufferMemory({
+ memoryKey: "chat_history", // Must be set to "chat_history"
+ }),
+ }
+ );
+ /* Ask it a question */
+ const question = "What did the president say about Justice Breyer?";
+ const res = await chain.call({ question });
+ console.log(res);
+ /* Ask it a follow up question */
+ const followUpRes = await chain.call({
+ question: "Was that nice?",
+ });
+ console.log(followUpRes);
+};
diff --git a/examples/src/chains/conversational_qa_streaming.ts b/examples/src/chains/conversational_qa_streaming.ts
index a991cc3f76d6..125f30ba4bce 100644
--- a/examples/src/chains/conversational_qa_streaming.ts
+++ b/examples/src/chains/conversational_qa_streaming.ts
@@ -1,52 +1,97 @@
+import { Document } from "langchain/document";
import { ChatOpenAI } from "langchain/chat_models/openai";
-import { ConversationalRetrievalQAChain } from "langchain/chains";
import { HNSWLib } from "langchain/vectorstores/hnswlib";
import { OpenAIEmbeddings } from "langchain/embeddings/openai";
import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
-import { BufferMemory } from "langchain/memory";
-
import * as fs from "fs";
+import { PromptTemplate } from "langchain/prompts";
+import { StringOutputParser } from "langchain/schema/output_parser";
+import { RunnableSequence } from "langchain/schema/runnable";
+
+/* Initialize the LLM & set streaming to true */
+const model = new ChatOpenAI({
+ streaming: true,
+});
+/* Load in the file we want to do question answering over */
+const text = fs.readFileSync("state_of_the_union.txt", "utf8");
+/* Split the text into chunks */
+const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000 });
+const docs = await textSplitter.createDocuments([text]);
+/* Create the vectorstore */
+const vectorStore = await HNSWLib.fromDocuments(docs, new OpenAIEmbeddings());
+const retriever = vectorStore.asRetriever();
+
+const serializeDocs = (docs: Array) =>
+ docs.map((doc) => doc.pageContent).join("\n\n");
+
+/**
+ * Create a prompt template for generating an answer based on context and
+ * a question.
+ *
+ * Chat history will be an empty string if it's the first question.
+ *
+ * inputVariables: ["chatHistory", "context", "question"]
+ */
+const questionPrompt = PromptTemplate.fromTemplate(
+ `Use the following pieces of context to answer the question at the end. If you don't know the answer, just say that you don't know, don't try to make up an answer.
+----------
+CONTEXT: {context}
+----------
+CHAT HISTORY: {chatHistory}
+----------
+QUESTION: {question}
+----------
+Helpful Answer:`
+);
+
+const chain = RunnableSequence.from([
+ {
+ question: (input: { question: string; chatHistory?: string }) =>
+ input.question,
+ chatHistory: (input: { question: string; chatHistory?: string }) =>
+ input.chatHistory ?? "",
+ context: async (input: { question: string; chatHistory?: string }) => {
+ const relevantDocs = await retriever.getRelevantDocuments(input.question);
+ const serialized = serializeDocs(relevantDocs);
+ return serialized;
+ },
+ },
+ questionPrompt,
+ model,
+ new StringOutputParser(),
+]);
+
+const stream = await chain.stream({
+ question: "What did the president say about Justice Breyer?",
+});
-export const run = async () => {
- const text = fs.readFileSync("state_of_the_union.txt", "utf8");
- const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000 });
- const docs = await textSplitter.createDocuments([text]);
- const vectorStore = await HNSWLib.fromDocuments(docs, new OpenAIEmbeddings());
- let streamedResponse = "";
- const streamingModel = new ChatOpenAI({
- streaming: true,
- callbacks: [
- {
- handleLLMNewToken(token) {
- streamedResponse += token;
- },
- },
- ],
- });
- const nonStreamingModel = new ChatOpenAI({});
- const chain = ConversationalRetrievalQAChain.fromLLM(
- streamingModel,
- vectorStore.asRetriever(),
- {
- returnSourceDocuments: true,
- memory: new BufferMemory({
- memoryKey: "chat_history",
- inputKey: "question", // The key for the input to the chain
- outputKey: "text", // The key for the final conversational output of the chain
- returnMessages: true, // If using with a chat model
- }),
- questionGeneratorChainOptions: {
- llm: nonStreamingModel,
- },
- }
- );
- /* Ask it a question */
- const question = "What did the president say about Justice Breyer?";
- const res = await chain.call({ question });
- console.log({ streamedResponse });
- /*
- {
- streamedResponse: 'President Biden thanked Justice Breyer for his service, and honored him as an Army veteran, Constitutional scholar and retiring Justice of the United States Supreme Court.'
- }
- */
-};
+let streamedResult = "";
+for await (const chunk of stream) {
+ streamedResult += chunk;
+ console.log(streamedResult);
+}
+/**
+ * The
+ * The president
+ * The president honored
+ * The president honored Justice
+ * The president honored Justice Stephen
+ * The president honored Justice Stephen B
+ * The president honored Justice Stephen Brey
+ * The president honored Justice Stephen Breyer
+ * The president honored Justice Stephen Breyer,
+ * The president honored Justice Stephen Breyer, a
+ * The president honored Justice Stephen Breyer, a retiring
+ * The president honored Justice Stephen Breyer, a retiring Justice
+ * The president honored Justice Stephen Breyer, a retiring Justice of
+ * The president honored Justice Stephen Breyer, a retiring Justice of the
+ * The president honored Justice Stephen Breyer, a retiring Justice of the United
+ * The president honored Justice Stephen Breyer, a retiring Justice of the United States
+ * The president honored Justice Stephen Breyer, a retiring Justice of the United States Supreme
+ * The president honored Justice Stephen Breyer, a retiring Justice of the United States Supreme Court
+ * The president honored Justice Stephen Breyer, a retiring Justice of the United States Supreme Court,
+ * The president honored Justice Stephen Breyer, a retiring Justice of the United States Supreme Court, for
+ * The president honored Justice Stephen Breyer, a retiring Justice of the United States Supreme Court, for his
+ * The president honored Justice Stephen Breyer, a retiring Justice of the United States Supreme Court, for his service
+ * The president honored Justice Stephen Breyer, a retiring Justice of the United States Supreme Court, for his service.
+ */
diff --git a/examples/src/chains/conversational_qa_streaming_legacy.ts b/examples/src/chains/conversational_qa_streaming_legacy.ts
new file mode 100644
index 000000000000..a991cc3f76d6
--- /dev/null
+++ b/examples/src/chains/conversational_qa_streaming_legacy.ts
@@ -0,0 +1,52 @@
+import { ChatOpenAI } from "langchain/chat_models/openai";
+import { ConversationalRetrievalQAChain } from "langchain/chains";
+import { HNSWLib } from "langchain/vectorstores/hnswlib";
+import { OpenAIEmbeddings } from "langchain/embeddings/openai";
+import { RecursiveCharacterTextSplitter } from "langchain/text_splitter";
+import { BufferMemory } from "langchain/memory";
+
+import * as fs from "fs";
+
+export const run = async () => {
+ const text = fs.readFileSync("state_of_the_union.txt", "utf8");
+ const textSplitter = new RecursiveCharacterTextSplitter({ chunkSize: 1000 });
+ const docs = await textSplitter.createDocuments([text]);
+ const vectorStore = await HNSWLib.fromDocuments(docs, new OpenAIEmbeddings());
+ let streamedResponse = "";
+ const streamingModel = new ChatOpenAI({
+ streaming: true,
+ callbacks: [
+ {
+ handleLLMNewToken(token) {
+ streamedResponse += token;
+ },
+ },
+ ],
+ });
+ const nonStreamingModel = new ChatOpenAI({});
+ const chain = ConversationalRetrievalQAChain.fromLLM(
+ streamingModel,
+ vectorStore.asRetriever(),
+ {
+ returnSourceDocuments: true,
+ memory: new BufferMemory({
+ memoryKey: "chat_history",
+ inputKey: "question", // The key for the input to the chain
+ outputKey: "text", // The key for the final conversational output of the chain
+ returnMessages: true, // If using with a chat model
+ }),
+ questionGeneratorChainOptions: {
+ llm: nonStreamingModel,
+ },
+ }
+ );
+ /* Ask it a question */
+ const question = "What did the president say about Justice Breyer?";
+ const res = await chain.call({ question });
+ console.log({ streamedResponse });
+ /*
+ {
+ streamedResponse: 'President Biden thanked Justice Breyer for his service, and honored him as an Army veteran, Constitutional scholar and retiring Justice of the United States Supreme Court.'
+ }
+ */
+};
diff --git a/examples/src/chains/retrieval_qa.ts b/examples/src/chains/retrieval_qa.ts
index 3e2832db8202..f8a6eabd750b 100644
--- a/examples/src/chains/retrieval_qa.ts
+++ b/examples/src/chains/retrieval_qa.ts
@@ -11,7 +11,6 @@ import { StringOutputParser } from "langchain/schema/output_parser";
import {
ChatPromptTemplate,
HumanMessagePromptTemplate,
- PromptTemplate,
SystemMessagePromptTemplate,
} from "langchain/prompts";
import { ChatOpenAI } from "langchain/chat_models/openai";
diff --git a/examples/src/chains/sql_db.ts b/examples/src/chains/sql_db.ts
index 00f8608e88fd..382c9ade89d5 100644
--- a/examples/src/chains/sql_db.ts
+++ b/examples/src/chains/sql_db.ts
@@ -1,7 +1,9 @@
import { DataSource } from "typeorm";
-import { OpenAI } from "langchain/llms/openai";
import { SqlDatabase } from "langchain/sql_db";
-import { SqlDatabaseChain } from "langchain/chains/sql_db";
+import { PromptTemplate } from "langchain/prompts";
+import { RunnableSequence } from "langchain/schema/runnable";
+import { ChatOpenAI } from "langchain/chat_models/openai";
+import { StringOutputParser } from "langchain/schema/output_parser";
/**
* This example uses Chinook database, which is a sample database available for SQL Server, Oracle, MySQL, etc.
@@ -17,11 +19,101 @@ const db = await SqlDatabase.fromDataSourceParams({
appDataSource: datasource,
});
-const chain = new SqlDatabaseChain({
- llm: new OpenAI({ temperature: 0 }),
- database: db,
+const llm = new ChatOpenAI();
+
+/**
+ * Create the first prompt template used for getting the SQL query.
+ */
+const prompt =
+ PromptTemplate.fromTemplate(`Based on the provided SQL table schema below, write a SQL query that would answer the user's question.
+------------
+SCHEMA: {schema}
+------------
+QUESTION: {question}
+------------
+SQL QUERY:`);
+/**
+ * You can also load a default prompt by importing from "langchain/sql_db"
+ *
+ * import {
+ * DEFAULT_SQL_DATABASE_PROMPT
+ * SQL_POSTGRES_PROMPT
+ * SQL_SQLITE_PROMPT
+ * SQL_MSSQL_PROMPT
+ * SQL_MYSQL_PROMPT
+ * SQL_SAP_HANA_PROMPT
+ * } from "langchain/sql_db";
+ *
+ */
+
+/**
+ * Create a new RunnableSequence where we pipe the output from `db.getTableInfo()`
+ * and the users question, into the prompt template, and then into the llm.
+ * We're also applying a stop condition to the llm, so that it stops when it
+ * sees the `\nSQLResult:` token.
+ */
+const sqlQueryChain = RunnableSequence.from([
+ {
+ schema: async () => db.getTableInfo(),
+ question: (input: { question: string }) => input.question,
+ },
+ prompt,
+ llm.bind({ stop: ["\nSQLResult:"] }),
+ new StringOutputParser(),
+]);
+
+const res = await sqlQueryChain.invoke({
+ question: "How many employees are there?",
});
+console.log({ res });
+
+/**
+ * { res: 'SELECT COUNT(*) FROM tracks;' }
+ */
-const res = await chain.run("How many tracks are there?");
-console.log(res);
-// There are 3503 tracks.
+/**
+ * Create the final prompt template which is tasked with getting the natural language response.
+ */
+const finalResponsePrompt =
+ PromptTemplate.fromTemplate(`Based on the table schema below, question, SQL query, and SQL response, write a natural language response:
+------------
+SCHEMA: {schema}
+------------
+QUESTION: {question}
+------------
+SQL QUERY: {query}
+------------
+SQL RESPONSE: {response}
+------------
+NATURAL LANGUAGE RESPONSE:`);
+
+/**
+ * Create a new RunnableSequence where we pipe the output from the previous chain, the users question,
+ * and the SQL query, into the prompt template, and then into the llm.
+ * Using the result from the `sqlQueryChain` we can run the SQL query via `db.run(input.query)`.
+ */
+const finalChain = RunnableSequence.from([
+ {
+ question: (input) => input.question,
+ query: sqlQueryChain,
+ },
+ {
+ schema: async () => db.getTableInfo(),
+ question: (input) => input.question,
+ query: (input) => input.query,
+ response: (input) => db.run(input.query),
+ },
+ finalResponsePrompt,
+ llm,
+ new StringOutputParser(),
+]);
+
+const finalResponse = await finalChain.invoke({
+ question: "How many employees are there?",
+});
+
+console.log({ finalResponse });
+
+/**
+ * { finalResponse: 'There are 8 employees.' }
+ */
diff --git a/examples/src/chains/sql_db_custom_prompt_legacy.ts b/examples/src/chains/sql_db_custom_prompt_legacy.ts
new file mode 100644
index 000000000000..b4f63152e458
--- /dev/null
+++ b/examples/src/chains/sql_db_custom_prompt_legacy.ts
@@ -0,0 +1,56 @@
+import { DataSource } from "typeorm";
+import { OpenAI } from "langchain/llms/openai";
+import { SqlDatabase } from "langchain/sql_db";
+import { SqlDatabaseChain } from "langchain/chains/sql_db";
+import { PromptTemplate } from "langchain/prompts";
+
+const template = `Given an input question, first create a syntactically correct {dialect} query to run, then look at the results of the query and return the answer.
+Use the following format:
+
+Question: "Question here"
+SQLQuery: "SQL Query to run"
+SQLResult: "Result of the SQLQuery"
+Answer: "Final answer here"
+
+Only use the following tables:
+
+{table_info}
+
+If someone asks for the table foobar, they really mean the employee table.
+
+Question: {input}`;
+
+const prompt = PromptTemplate.fromTemplate(template);
+
+/**
+ * This example uses Chinook database, which is a sample database available for SQL Server, Oracle, MySQL, etc.
+ * To set it up follow the instructions on https://database.guide/2-sample-databases-sqlite/, placing the .db file
+ * in the examples folder.
+ */
+const datasource = new DataSource({
+ type: "sqlite",
+ database: "data/Chinook.db",
+});
+
+const db = await SqlDatabase.fromDataSourceParams({
+ appDataSource: datasource,
+});
+
+const chain = new SqlDatabaseChain({
+ llm: new OpenAI({ temperature: 0 }),
+ database: db,
+ sqlOutputKey: "sql",
+ prompt,
+});
+
+const res = await chain.call({
+ query: "How many employees are there in the foobar table?",
+});
+console.log(res);
+
+/*
+ {
+ result: ' There are 8 employees in the foobar table.',
+ sql: ' SELECT COUNT(*) FROM Employee;'
+ }
+*/
diff --git a/examples/src/chains/sql_db_legacy.ts b/examples/src/chains/sql_db_legacy.ts
new file mode 100644
index 000000000000..00f8608e88fd
--- /dev/null
+++ b/examples/src/chains/sql_db_legacy.ts
@@ -0,0 +1,27 @@
+import { DataSource } from "typeorm";
+import { OpenAI } from "langchain/llms/openai";
+import { SqlDatabase } from "langchain/sql_db";
+import { SqlDatabaseChain } from "langchain/chains/sql_db";
+
+/**
+ * This example uses Chinook database, which is a sample database available for SQL Server, Oracle, MySQL, etc.
+ * To set it up follow the instructions on https://database.guide/2-sample-databases-sqlite/, placing the .db file
+ * in the examples folder.
+ */
+const datasource = new DataSource({
+ type: "sqlite",
+ database: "Chinook.db",
+});
+
+const db = await SqlDatabase.fromDataSourceParams({
+ appDataSource: datasource,
+});
+
+const chain = new SqlDatabaseChain({
+ llm: new OpenAI({ temperature: 0 }),
+ database: db,
+});
+
+const res = await chain.run("How many tracks are there?");
+console.log(res);
+// There are 3503 tracks.
diff --git a/examples/src/chains/sql_db_saphana_legacy.ts b/examples/src/chains/sql_db_saphana_legacy.ts
new file mode 100644
index 000000000000..bdc6ca1e14aa
--- /dev/null
+++ b/examples/src/chains/sql_db_saphana_legacy.ts
@@ -0,0 +1,39 @@
+import { DataSource } from "typeorm";
+import { OpenAI } from "langchain/llms/openai";
+import { SqlDatabase } from "langchain/sql_db";
+import { SqlDatabaseChain } from "langchain/chains/sql_db";
+
+/**
+ * This example uses a SAP HANA Cloud database. You can create a free trial database via https://developers.sap.com/tutorials/hana-cloud-deploying.html
+ *
+ * You will need to add the following packages to your package.json as they are required when using typeorm with SAP HANA:
+ *
+ * "hdb-pool": "^0.1.6", (or latest version)
+ * "@sap/hana-client": "^2.17.22" (or latest version)
+ *
+ */
+const datasource = new DataSource({
+ type: "sap",
+ host: ".hanacloud.ondemand.com",
+ port: 443,
+ username: "",
+ password: "",
+ schema: "",
+ encrypt: true,
+ extra: {
+ sslValidateCertificate: false,
+ },
+});
+
+const db = await SqlDatabase.fromDataSourceParams({
+ appDataSource: datasource,
+});
+
+const chain = new SqlDatabaseChain({
+ llm: new OpenAI({ temperature: 0 }),
+ database: db,
+});
+
+const res = await chain.run("How many tracks are there?");
+console.log(res);
+// There are 3503 tracks.
diff --git a/examples/src/chains/sql_db_sql_output.ts b/examples/src/chains/sql_db_sql_output.ts
index a3a6e40a6ef4..567d1b6c037f 100644
--- a/examples/src/chains/sql_db_sql_output.ts
+++ b/examples/src/chains/sql_db_sql_output.ts
@@ -1,7 +1,9 @@
import { DataSource } from "typeorm";
-import { OpenAI } from "langchain/llms/openai";
import { SqlDatabase } from "langchain/sql_db";
-import { SqlDatabaseChain } from "langchain/chains/sql_db";
+import { ChatOpenAI } from "langchain/chat_models/openai";
+import { PromptTemplate } from "langchain/prompts";
+import { RunnableSequence } from "langchain/schema/runnable";
+import { StringOutputParser } from "langchain/schema/output_parser";
/**
* This example uses Chinook database, which is a sample database available for SQL Server, Oracle, MySQL, etc.
@@ -17,17 +19,90 @@ const db = await SqlDatabase.fromDataSourceParams({
appDataSource: datasource,
});
-const chain = new SqlDatabaseChain({
- llm: new OpenAI({ temperature: 0 }),
- database: db,
- sqlOutputKey: "sql",
+const llm = new ChatOpenAI();
+
+/**
+ * Create the first prompt template used for getting the SQL query.
+ */
+const prompt =
+ PromptTemplate.fromTemplate(`Based on the provided SQL table schema below, write a SQL query that would answer the user's question.
+------------
+SCHEMA: {schema}
+------------
+QUESTION: {question}
+------------
+SQL QUERY:`);
+
+/**
+ * Create a new RunnableSequence where we pipe the output from `db.getTableInfo()`
+ * and the users question, into the prompt template, and then into the llm.
+ * We're also applying a stop condition to the llm, so that it stops when it
+ * sees the `\nSQLResult:` token.
+ */
+const sqlQueryChain = RunnableSequence.from([
+ {
+ schema: async () => db.getTableInfo(),
+ question: (input: { question: string }) => input.question,
+ },
+ prompt,
+ llm.bind({ stop: ["\nSQLResult:"] }),
+ new StringOutputParser(),
+]);
+
+/**
+ * Create the final prompt template which is tasked with getting the natural
+ * language response to the SQL query.
+ */
+const finalResponsePrompt =
+ PromptTemplate.fromTemplate(`Based on the table schema below, question, SQL query, and SQL response, write a natural language response:
+------------
+SCHEMA: {schema}
+------------
+QUESTION: {question}
+------------
+SQL QUERY: {query}
+------------
+SQL RESPONSE: {response}
+------------
+NATURAL LANGUAGE RESPONSE:`);
+
+/**
+ * Create a new RunnableSequence where we pipe the output from the previous chain, the users question,
+ * and the SQL query, into the prompt template, and then into the llm.
+ * Using the result from the `sqlQueryChain` we can run the SQL query via `db.run(input.query)`.
+ *
+ * Lastly we're piping the result of the first chain (the outputted SQL query) so it is
+ * logged along with the natural language response.
+ */
+const finalChain = RunnableSequence.from([
+ {
+ question: (input) => input.question,
+ query: sqlQueryChain,
+ },
+ {
+ schema: async () => db.getTableInfo(),
+ question: (input) => input.question,
+ query: (input) => input.query,
+ response: (input) => db.run(input.query),
+ },
+ {
+ result: finalResponsePrompt.pipe(llm).pipe(new StringOutputParser()),
+ // Pipe the query through here unchanged so it gets logged alongside the result.
+ sql: (previousStepResult) => previousStepResult.query,
+ },
+]);
+
+const finalResponse = await finalChain.invoke({
+ question: "How many employees are there?",
});
-const res = await chain.call({ query: "How many tracks are there?" });
-/* Expected result:
+console.log({ finalResponse });
+
+/**
* {
- * result: ' There are 3503 tracks.',
- * sql: ' SELECT COUNT(*) FROM "Track";'
+ * finalResponse: {
+ * result: 'There are 8 employees.',
+ * sql: 'SELECT COUNT(*) FROM tracks;'
+ * }
* }
*/
-console.log(res);
diff --git a/examples/src/chains/sql_db_sql_output_legacy.ts b/examples/src/chains/sql_db_sql_output_legacy.ts
new file mode 100644
index 000000000000..a3a6e40a6ef4
--- /dev/null
+++ b/examples/src/chains/sql_db_sql_output_legacy.ts
@@ -0,0 +1,33 @@
+import { DataSource } from "typeorm";
+import { OpenAI } from "langchain/llms/openai";
+import { SqlDatabase } from "langchain/sql_db";
+import { SqlDatabaseChain } from "langchain/chains/sql_db";
+
+/**
+ * This example uses Chinook database, which is a sample database available for SQL Server, Oracle, MySQL, etc.
+ * To set it up follow the instructions on https://database.guide/2-sample-databases-sqlite/, placing the .db file
+ * in the examples folder.
+ */
+const datasource = new DataSource({
+ type: "sqlite",
+ database: "Chinook.db",
+});
+
+const db = await SqlDatabase.fromDataSourceParams({
+ appDataSource: datasource,
+});
+
+const chain = new SqlDatabaseChain({
+ llm: new OpenAI({ temperature: 0 }),
+ database: db,
+ sqlOutputKey: "sql",
+});
+
+const res = await chain.call({ query: "How many tracks are there?" });
+/* Expected result:
+ * {
+ * result: ' There are 3503 tracks.',
+ * sql: ' SELECT COUNT(*) FROM "Track";'
+ * }
+ */
+console.log(res);
diff --git a/examples/src/guides/expression_language/how_to_routing_custom_function.ts b/examples/src/guides/expression_language/how_to_routing_custom_function.ts
index 25181053281d..40fd7cfda779 100644
--- a/examples/src/guides/expression_language/how_to_routing_custom_function.ts
+++ b/examples/src/guides/expression_language/how_to_routing_custom_function.ts
@@ -1,7 +1,7 @@
import { ChatAnthropic } from "langchain/chat_models/anthropic";
import { PromptTemplate } from "langchain/prompts";
import { StringOutputParser } from "langchain/schema/output_parser";
-import { RunnableLambda, RunnableSequence } from "langchain/schema/runnable";
+import { RunnableSequence } from "langchain/schema/runnable";
const promptTemplate =
PromptTemplate.fromTemplate(`Given the user question below, classify it as either being about \`LangChain\`, \`Anthropic\`, or \`Other\`.
diff --git a/examples/src/models/chat/openai_functions.ts b/examples/src/models/chat/openai_functions.ts
index e3e74053a063..fcd56dfeda34 100644
--- a/examples/src/models/chat/openai_functions.ts
+++ b/examples/src/models/chat/openai_functions.ts
@@ -25,10 +25,6 @@ const extractionFunctionSchema = {
},
};
-// Bind function arguments to the model.
-// All subsequent invoke calls will use the bound parameters.
-// "functions.parameters" must be formatted as JSON Schema
-// Omit "function_call" if you want the model to choose a function to call.
const model = new ChatOpenAI({
modelName: "gpt-4",
}).bind({
@@ -39,34 +35,22 @@ const model = new ChatOpenAI({
const result = await model.invoke([new HumanMessage("What a beautiful day!")]);
console.log(result);
-
/*
- AIMessage {
- content: '',
- name: undefined,
- additional_kwargs: {
- function_call: {
- name: 'extractor',
- arguments: '{\n' +
- ' "tone": "positive",\n' +
- ' "word_count": 4,\n' +
- ' "chat_response": "It certainly is a beautiful day!"\n' +
- '}'
- }
+AIMessage {
+ lc_serializable: true,
+ lc_kwargs: { content: '', additional_kwargs: { function_call: [Object] } },
+ lc_namespace: [ 'langchain', 'schema' ],
+ content: '',
+ name: undefined,
+ additional_kwargs: {
+ function_call: {
+ name: 'extractor',
+ arguments: '{\n' +
+ ' "tone": "positive",\n' +
+ ' "word_count": 4,\n' +
+ ` "chat_response": "I'm glad you're enjoying the day! What makes it so beautiful for you?"\n` +
+ '}'
}
}
-*/
-
-// Alternatively, you can pass function call arguments as an additional argument as a one-off:
-/*
-const model = new ChatOpenAI({
- modelName: "gpt-4",
-});
-
-const result = await model.call([
- new HumanMessage("What a beautiful day!")
-], {
- functions: [extractionFunctionSchema],
- function_call: {name: "extractor"}
-});
+}
*/
diff --git a/examples/src/models/chat/openai_functions_zod.ts b/examples/src/models/chat/openai_functions_zod.ts
index 390e6faa65b0..ba167d0c06ef 100644
--- a/examples/src/models/chat/openai_functions_zod.ts
+++ b/examples/src/models/chat/openai_functions_zod.ts
@@ -3,54 +3,52 @@ import { HumanMessage } from "langchain/schema";
import { z } from "zod";
import { zodToJsonSchema } from "zod-to-json-schema";
-const extractionFunctionZodSchema = z.object({
- tone: z
- .enum(["positive", "negative"])
- .describe("The overall tone of the input"),
- entity: z.string().describe("The entity mentioned in the input"),
- word_count: z.number().describe("The number of words in the input"),
- chat_response: z.string().describe("A response to the human's input"),
- final_punctuation: z
- .optional(z.string())
- .describe("The final punctuation mark in the input, if any."),
-});
+const extractionFunctionSchema = {
+ name: "extractor",
+ description: "Extracts fields from the input.",
+ parameters: zodToJsonSchema(
+ z.object({
+ tone: z
+ .enum(["positive", "negative"])
+ .describe("The overall tone of the input"),
+ entity: z.string().describe("The entity mentioned in the input"),
+ word_count: z.number().describe("The number of words in the input"),
+ chat_response: z.string().describe("A response to the human's input"),
+ final_punctuation: z
+ .optional(z.string())
+ .describe("The final punctuation mark in the input, if any."),
+ })
+ ),
+};
-// Bind function arguments to the model.
-// "functions.parameters" must be formatted as JSON Schema.
-// We translate the above Zod schema into JSON schema using the "zodToJsonSchema" package.
-// Omit "function_call" if you want the model to choose a function to call.
const model = new ChatOpenAI({
modelName: "gpt-4",
}).bind({
- functions: [
- {
- name: "extractor",
- description: "Extracts fields from the input.",
- parameters: zodToJsonSchema(extractionFunctionZodSchema),
- },
- ],
+ functions: [extractionFunctionSchema],
function_call: { name: "extractor" },
});
const result = await model.invoke([new HumanMessage("What a beautiful day!")]);
console.log(result);
-
/*
- AIMessage {
- content: '',
- name: undefined,
- additional_kwargs: {
- function_call: {
- name: 'extractor',
- arguments: '{\n' +
- ' "tone": "positive",\n' +
- ' "entity": "day",\n' +
- ' "word_count": 4,\n' +
- ' "chat_response": "It certainly is a gorgeous day!",\n' +
- ' "final_punctuation": "!"\n' +
- '}'
- }
+AIMessage {
+ lc_serializable: true,
+ lc_kwargs: { content: '', additional_kwargs: { function_call: [Object] } },
+ lc_namespace: [ 'langchain', 'schema' ],
+ content: '',
+ name: undefined,
+ additional_kwargs: {
+ function_call: {
+ name: 'extractor',
+ arguments: '{\n' +
+ '"tone": "positive",\n' +
+ '"entity": "day",\n' +
+ '"word_count": 4,\n' +
+ `"chat_response": "I'm glad you're enjoying the day!",\n` +
+ '"final_punctuation": "!"\n' +
+ '}'
}
}
+}
*/
diff --git a/examples/src/models/llm/yandex.ts b/examples/src/models/llm/yandex.ts
new file mode 100644
index 000000000000..95e3bafe638c
--- /dev/null
+++ b/examples/src/models/llm/yandex.ts
@@ -0,0 +1,7 @@
+import { YandexGPT } from "langchain/llms/yandex";
+
+const model = new YandexGPT();
+
+const res = await model.call('Translate "I love programming" into French.');
+
+console.log({ res });
diff --git a/examples/src/retrievers/multi_vector_hypothetical.ts b/examples/src/retrievers/multi_vector_hypothetical.ts
index b65a79693155..fcbcc6a05e35 100644
--- a/examples/src/retrievers/multi_vector_hypothetical.ts
+++ b/examples/src/retrievers/multi_vector_hypothetical.ts
@@ -2,7 +2,6 @@ import * as uuid from "uuid";
import { ChatOpenAI } from "langchain/chat_models/openai";
import { PromptTemplate } from "langchain/prompts";
-import { StringOutputParser } from "langchain/schema/output_parser";
import { RunnableSequence } from "langchain/schema/runnable";
import { MultiVectorRetriever } from "langchain/retrievers/multi_vector";
diff --git a/langchain/.gitignore b/langchain/.gitignore
index ee93d42417a4..11eb34bbe6b7 100644
--- a/langchain/.gitignore
+++ b/langchain/.gitignore
@@ -163,6 +163,9 @@ llms/writer.d.ts
llms/portkey.cjs
llms/portkey.js
llms/portkey.d.ts
+llms/yandex.cjs
+llms/yandex.js
+llms/yandex.d.ts
prompts.cjs
prompts.js
prompts.d.ts
diff --git a/langchain/package.json b/langchain/package.json
index beb682c993a0..2bee8e51f54c 100644
--- a/langchain/package.json
+++ b/langchain/package.json
@@ -175,6 +175,9 @@
"llms/portkey.cjs",
"llms/portkey.js",
"llms/portkey.d.ts",
+ "llms/yandex.cjs",
+ "llms/yandex.js",
+ "llms/yandex.d.ts",
"prompts.cjs",
"prompts.js",
"prompts.d.ts",
@@ -1529,6 +1532,11 @@
"import": "./llms/portkey.js",
"require": "./llms/portkey.cjs"
},
+ "./llms/yandex": {
+ "types": "./llms/yandex.d.ts",
+ "import": "./llms/yandex.js",
+ "require": "./llms/yandex.cjs"
+ },
"./prompts": {
"types": "./prompts.d.ts",
"import": "./prompts.js",
diff --git a/langchain/scripts/create-entrypoints.js b/langchain/scripts/create-entrypoints.js
index 0883a51afb49..ad1361e4f6b2 100644
--- a/langchain/scripts/create-entrypoints.js
+++ b/langchain/scripts/create-entrypoints.js
@@ -69,6 +69,7 @@ const entrypoints = {
"llms/llama_cpp": "llms/llama_cpp",
"llms/writer": "llms/writer",
"llms/portkey": "llms/portkey",
+ "llms/yandex": "llms/yandex",
// prompts
prompts: "prompts/index",
"prompts/load": "prompts/load",
@@ -94,6 +95,7 @@ const entrypoints = {
"vectorstores/opensearch": "vectorstores/opensearch",
"vectorstores/pgvector": "vectorstores/pgvector",
"vectorstores/milvus": "vectorstores/milvus",
+ "vectorstores/neo4j_vector": "vectorstores/neo4j_vector",
"vectorstores/prisma": "vectorstores/prisma",
"vectorstores/typeorm": "vectorstores/typeorm",
"vectorstores/myscale": "vectorstores/myscale",
@@ -344,6 +346,7 @@ const requiresOptionalDependency = [
"vectorstores/typeorm",
"vectorstores/milvus",
"vectorstores/myscale",
+ "vectorstores/neo4j_vector",
"vectorstores/redis",
"vectorstores/singlestore",
"vectorstores/typesense",
diff --git a/langchain/src/embeddings/bedrock.ts b/langchain/src/embeddings/bedrock.ts
index b4f236fb303f..eefb6a972201 100644
--- a/langchain/src/embeddings/bedrock.ts
+++ b/langchain/src/embeddings/bedrock.ts
@@ -2,7 +2,6 @@ import {
BedrockRuntimeClient,
InvokeModelCommand,
} from "@aws-sdk/client-bedrock-runtime";
-
import { Embeddings, EmbeddingsParams } from "./base.js";
import type { CredentialType } from "../util/bedrock.js";
@@ -40,6 +39,8 @@ export class BedrockEmbeddings
client: BedrockRuntimeClient;
+ batchSize = 512;
+
constructor(fields?: BedrockEmbeddingsParams) {
super(fields ?? {});
@@ -53,28 +54,48 @@ export class BedrockEmbeddings
});
}
+ /**
+ * Protected method to make a request to the Bedrock API to generate
+ * embeddings. Handles the retry logic and returns the response from the
+ * API.
+ * @param request Request to send to the Bedrock API.
+ * @returns Promise that resolves to the response from the API.
+ */
protected async _embedText(text: string): Promise {
- // replace newlines, which can negatively affect performance.
- const cleanedText = text.replace(/\n/g, " ");
-
- const res = await this.client.send(
- new InvokeModelCommand({
- modelId: this.model,
- body: JSON.stringify({
- inputText: cleanedText,
- }),
- contentType: "application/json",
- accept: "application/json",
- })
- );
-
- try {
- const body = new TextDecoder().decode(res.body);
-
- return JSON.parse(body).embedding;
- } catch (e) {
- throw new Error("An invalid response was returned by Bedrock.");
- }
+ return this.caller.call(async () => {
+ try {
+ // replace newlines, which can negatively affect performance.
+ const cleanedText = text.replace(/\n/g, " ");
+
+ const res = await this.client.send(
+ new InvokeModelCommand({
+ modelId: this.model,
+ body: JSON.stringify({
+ inputText: cleanedText,
+ }),
+ contentType: "application/json",
+ accept: "application/json",
+ })
+ );
+
+ const body = new TextDecoder().decode(res.body);
+ return JSON.parse(body).embedding;
+ } catch (e) {
+ console.error({
+ error: e,
+ });
+ // eslint-disable-next-line no-instanceof/no-instanceof
+ if (e instanceof Error) {
+ throw new Error(
+ `An error occurred while embedding documents with Bedrock: ${e.message}`
+ );
+ }
+
+ throw new Error(
+ "An error occurred while embedding documents with Bedrock"
+ );
+ }
+ });
}
/**
@@ -93,13 +114,12 @@ export class BedrockEmbeddings
}
/**
- * Method that takes an array of documents as input and returns a promise
- * that resolves to a 2D array of embeddings for each document. It calls
- * the _embedText method for each document in the array.
- * @param documents Array of documents for which to generate embeddings.
+ * Method to generate embeddings for an array of texts. Calls _embedText
+ * method which batches and handles retry logic when calling the AWS Bedrock API.
+ * @param documents Array of texts for which to generate embeddings.
* @returns Promise that resolves to a 2D array of embeddings for each input document.
*/
- embedDocuments(documents: string[]): Promise {
+ async embedDocuments(documents: string[]): Promise {
return Promise.all(documents.map((document) => this._embedText(document)));
}
}
diff --git a/langchain/src/embeddings/tests/bedrock.int.test.ts b/langchain/src/embeddings/tests/bedrock.int.test.ts
index 19ea5ddd2a0a..e6e0f72dfa7d 100644
--- a/langchain/src/embeddings/tests/bedrock.int.test.ts
+++ b/langchain/src/embeddings/tests/bedrock.int.test.ts
@@ -6,32 +6,42 @@ import { BedrockRuntimeClient } from "@aws-sdk/client-bedrock-runtime";
import { HNSWLib } from "../../vectorstores/hnswlib.js";
import { BedrockEmbeddings } from "../bedrock.js";
-const client = new BedrockRuntimeClient({
- region: process.env.BEDROCK_AWS_REGION!,
- credentials: {
- accessKeyId: process.env.BEDROCK_AWS_ACCESS_KEY_ID!,
- secretAccessKey: process.env.BEDROCK_AWS_SECRET_ACCESS_KEY!,
- },
-});
+const getClient = () => {
+ if (
+ !process.env.BEDROCK_AWS_REGION ||
+ !process.env.BEDROCK_AWS_ACCESS_KEY_ID ||
+ !process.env.BEDROCK_AWS_SECRET_ACCESS_KEY
+ ) {
+ throw new Error("Missing environment variables for AWS");
+ }
+
+ const client = new BedrockRuntimeClient({
+ region: process.env.BEDROCK_AWS_REGION,
+ credentials: {
+ accessKeyId: process.env.BEDROCK_AWS_ACCESS_KEY_ID,
+ secretAccessKey: process.env.BEDROCK_AWS_SECRET_ACCESS_KEY,
+ },
+ });
+
+ return client;
+};
test("Test BedrockEmbeddings.embedQuery", async () => {
+ const client = getClient();
const embeddings = new BedrockEmbeddings({
maxRetries: 1,
client,
});
const res = await embeddings.embedQuery("Hello world");
- console.log(res);
+ // console.log(res);
expect(typeof res[0]).toBe("number");
});
test("Test BedrockEmbeddings.embedDocuments with passed region and credentials", async () => {
+ const client = getClient();
const embeddings = new BedrockEmbeddings({
maxRetries: 1,
- region: process.env.BEDROCK_AWS_REGION!,
- credentials: {
- accessKeyId: process.env.BEDROCK_AWS_ACCESS_KEY_ID!,
- secretAccessKey: process.env.BEDROCK_AWS_SECRET_ACCESS_KEY!,
- },
+ client,
});
const res = await embeddings.embedDocuments([
"Hello world",
@@ -41,7 +51,7 @@ test("Test BedrockEmbeddings.embedDocuments with passed region and credentials",
"six documents",
"to test pagination",
]);
- console.log(res);
+ // console.log(res);
expect(res).toHaveLength(6);
res.forEach((r) => {
expect(typeof r[0]).toBe("number");
@@ -49,6 +59,7 @@ test("Test BedrockEmbeddings.embedDocuments with passed region and credentials",
});
test("Test end to end with HNSWLib", async () => {
+ const client = getClient();
const vectorStore = await HNSWLib.fromTexts(
["Hello world", "Bye bye", "hello nice world"],
[{ id: 2 }, { id: 1 }, { id: 3 }],
diff --git a/langchain/src/llms/yandex.ts b/langchain/src/llms/yandex.ts
new file mode 100644
index 000000000000..96b70e7ced55
--- /dev/null
+++ b/langchain/src/llms/yandex.ts
@@ -0,0 +1,123 @@
+import { getEnvironmentVariable } from "../util/env.js";
+import { LLM, BaseLLMParams } from "./base.js";
+
+const apiUrl = "https://llm.api.cloud.yandex.net/llm/v1alpha/instruct";
+
+export interface YandexGPTInputs extends BaseLLMParams {
+ /**
+ * What sampling temperature to use.
+ * Should be a double number between 0 (inclusive) and 1 (inclusive).
+ */
+ temperature?: number;
+
+ /**
+ * Maximum limit on the total number of tokens
+ * used for both the input prompt and the generated response.
+ */
+ maxTokens?: number;
+
+ /** Model name to use. */
+ model?: string;
+
+ /**
+ * Yandex Cloud Api Key for service account
+ * with the `ai.languageModels.user` role.
+ */
+ apiKey?: string;
+
+ /**
+ * Yandex Cloud IAM token for service account
+ * with the `ai.languageModels.user` role.
+ */
+ iamToken?: string;
+}
+
+export class YandexGPT extends LLM implements YandexGPTInputs {
+ static lc_name() {
+ return "Yandex GPT";
+ }
+
+ get lc_secrets(): { [key: string]: string } | undefined {
+ return {
+ apiKey: "YC_API_KEY",
+ iamToken: "YC_IAM_TOKEN",
+ };
+ }
+
+ temperature = 0.6;
+
+ maxTokens = 1700;
+
+ model = "general";
+
+ apiKey?: string;
+
+ iamToken?: string;
+
+ constructor(fields?: YandexGPTInputs) {
+ super(fields ?? {});
+
+ const apiKey = fields?.apiKey ?? getEnvironmentVariable("YC_API_KEY");
+
+ const iamToken = fields?.iamToken ?? getEnvironmentVariable("YC_IAM_TOKEN");
+
+ if (apiKey === undefined && iamToken === undefined) {
+ throw new Error(
+ "Please set the YC_API_KEY or YC_IAM_TOKEN environment variable or pass it to the constructor as the apiKey or iamToken field."
+ );
+ }
+
+ this.apiKey = apiKey;
+ this.iamToken = iamToken;
+ this.maxTokens = fields?.maxTokens ?? this.maxTokens;
+ this.temperature = fields?.temperature ?? this.temperature;
+ this.model = fields?.model ?? this.model;
+ }
+
+ _llmType() {
+ return "yandexgpt";
+ }
+
+ /** @ignore */
+ async _call(
+ prompt: string,
+ options: this["ParsedCallOptions"]
+ ): Promise {
+ // Hit the `generate` endpoint on the `large` model
+ return this.caller.callWithOptions({ signal: options.signal }, async () => {
+ const headers = { "Content-Type": "application/json", Authorization: "" };
+ if (this.apiKey !== undefined) {
+ headers.Authorization = `Api-Key ${this.apiKey}`;
+ } else {
+ headers.Authorization = `Bearer ${this.iamToken}`;
+ }
+ const bodyData = {
+ model: this.model,
+ generationOptions: {
+ temperature: this.temperature,
+ maxTokens: this.maxTokens,
+ },
+
+ requestText: prompt,
+ };
+
+ try {
+ const response = await fetch(apiUrl, {
+ method: "POST",
+ headers,
+ body: JSON.stringify(bodyData),
+ });
+ if (!response.ok) {
+ throw new Error(
+ `Failed to fetch ${apiUrl} from YandexGPT: ${response.status}`
+ );
+ }
+
+ const responseData = await response.json();
+ return responseData.result.alternatives[0].text;
+ } catch (error) {
+ throw new Error(`Failed to fetch ${apiUrl} from YandexGPT ${error}`);
+ }
+ });
+ }
+}
diff --git a/langchain/src/load/import_map.ts b/langchain/src/load/import_map.ts
index 98ad14022bcb..3ad007a2c345 100644
--- a/langchain/src/load/import_map.ts
+++ b/langchain/src/load/import_map.ts
@@ -19,6 +19,7 @@ export * as llms__ai21 from "../llms/ai21.js";
export * as llms__aleph_alpha from "../llms/aleph_alpha.js";
export * as llms__ollama from "../llms/ollama.js";
export * as llms__fireworks from "../llms/fireworks.js";
+export * as llms__yandex from "../llms/yandex.js";
export * as prompts from "../prompts/index.js";
export * as vectorstores__base from "../vectorstores/base.js";
export * as vectorstores__memory from "../vectorstores/memory.js";
diff --git a/langchain/src/load/import_type.d.ts b/langchain/src/load/import_type.d.ts
index 05e1a040c520..6de8ef86959f 100644
--- a/langchain/src/load/import_type.d.ts
+++ b/langchain/src/load/import_type.d.ts
@@ -478,6 +478,8 @@ export interface SecretMap {
VECTARA_CUSTOMER_ID?: string;
WRITER_API_KEY?: string;
WRITER_ORG_ID?: string;
+ YC_API_KEY?: string;
+ YC_IAM_TOKEN?: string;
ZAPIER_NLA_API_KEY?: string;
ZEP_API_KEY?: string;
ZEP_API_URL?: string;
diff --git a/langchain/src/retrievers/multi_vector.ts b/langchain/src/retrievers/multi_vector.ts
index 5519247a6a76..f438382fb3d1 100644
--- a/langchain/src/retrievers/multi_vector.ts
+++ b/langchain/src/retrievers/multi_vector.ts
@@ -1,4 +1,4 @@
-import { BaseStore } from "../schema/storage.js";
+import { BaseStoreInterface } from "../schema/storage.js";
import { Document } from "../document.js";
import { BaseRetriever, BaseRetrieverInput } from "../schema/retriever.js";
import { VectorStore } from "../vectorstores/base.js";
@@ -8,7 +8,7 @@ import { VectorStore } from "../vectorstores/base.js";
*/
export interface MultiVectorRetrieverInput extends BaseRetrieverInput {
vectorstore: VectorStore;
- docstore: BaseStore;
+ docstore: BaseStoreInterface;
idKey?: string;
childK?: number;
parentK?: number;
@@ -28,7 +28,7 @@ export class MultiVectorRetriever extends BaseRetriever {
public vectorstore: VectorStore;
- public docstore: BaseStore;
+ public docstore: BaseStoreInterface;
protected idKey: string;
diff --git a/langchain/src/retrievers/parent_document.ts b/langchain/src/retrievers/parent_document.ts
index c9583fe93ac9..b10b4026f9b0 100644
--- a/langchain/src/retrievers/parent_document.ts
+++ b/langchain/src/retrievers/parent_document.ts
@@ -66,12 +66,11 @@ export class ParentDocumentRetriever extends MultiVectorRetriever {
}
}
const parentDocs: Document[] = [];
- for (const parentDocId of parentDocIds) {
- const storedParentDocs = await this.docstore.mget([parentDocId]);
- if (storedParentDocs[0] !== undefined) {
- parentDocs.push(storedParentDocs[0]);
- }
- }
+ const storedParentDocs = await this.docstore.mget(parentDocIds);
+ const retrievedDocs: Document[] = storedParentDocs.filter(
+ (doc?: Document): doc is Document => doc !== undefined
+ );
+ parentDocs.push(...retrievedDocs);
return parentDocs.slice(0, this.parentK);
}
diff --git a/langchain/src/retrievers/tests/parent_document.int.test.ts b/langchain/src/retrievers/tests/parent_document.int.test.ts
index 03dc7e34c145..455593f84731 100644
--- a/langchain/src/retrievers/tests/parent_document.int.test.ts
+++ b/langchain/src/retrievers/tests/parent_document.int.test.ts
@@ -1,5 +1,6 @@
import { expect, test } from "@jest/globals";
import { TextLoader } from "../../document_loaders/fs/text.js";
+import { InMemoryDocstore } from "../../stores/doc/in_memory.js";
import { InMemoryStore } from "../../storage/in_memory.js";
import { OpenAIEmbeddings } from "../../embeddings/openai.js";
import { MemoryVectorStore } from "../../vectorstores/memory.js";
@@ -56,3 +57,54 @@ test("Should return a part of a document if a parent splitter is passed", async
expect(retrievedDocs.length).toBeGreaterThan(1);
expect(retrievedDocs[0].pageContent.length).toBeGreaterThan(100);
});
+
+test("Should work with a backwards compatible docstore too", async () => {
+ const vectorstore = new MemoryVectorStore(new OpenAIEmbeddings());
+ const retriever = new ParentDocumentRetriever({
+ vectorstore,
+ docstore: new InMemoryDocstore(),
+ childSplitter: new RecursiveCharacterTextSplitter({
+ chunkOverlap: 0,
+ chunkSize: 100,
+ }),
+ });
+ const docs = await new TextLoader(
+ "../examples/state_of_the_union.txt"
+ ).load();
+ await retriever.addDocuments(docs);
+
+ const query = "justice breyer";
+ const retrievedDocs = await retriever.getRelevantDocuments(query);
+ expect(retrievedDocs.length).toEqual(1);
+ expect(retrievedDocs[0].pageContent.length).toBeGreaterThan(1000);
+});
+
+test("Should return a part of a document if a parent splitter is passed", async () => {
+ const vectorstore = new MemoryVectorStore(new OpenAIEmbeddings());
+ const docstore = new InMemoryStore();
+ const retriever = new ParentDocumentRetriever({
+ vectorstore,
+ docstore,
+ parentSplitter: new RecursiveCharacterTextSplitter({
+ chunkOverlap: 0,
+ chunkSize: 500,
+ }),
+ childSplitter: new RecursiveCharacterTextSplitter({
+ chunkOverlap: 0,
+ chunkSize: 50,
+ }),
+ });
+ const docs = await new TextLoader(
+ "../examples/state_of_the_union.txt"
+ ).load();
+ await retriever.addDocuments(docs);
+ const query = "justice breyer";
+ const retrievedDocs = await retriever.getRelevantDocuments(query);
+ const vectorstoreRetreivedDocs = await vectorstore.similaritySearch(
+ "justice breyer"
+ );
+ console.log(vectorstoreRetreivedDocs, vectorstoreRetreivedDocs.length);
+ console.log(retrievedDocs);
+ expect(retrievedDocs.length).toBeGreaterThan(1);
+ expect(retrievedDocs[0].pageContent.length).toBeGreaterThan(100);
+});
diff --git a/langchain/src/schema/storage.ts b/langchain/src/schema/storage.ts
index 5a82f535cf4f..80e2f4829448 100644
--- a/langchain/src/schema/storage.ts
+++ b/langchain/src/schema/storage.ts
@@ -1,9 +1,43 @@
import { Serializable } from "../load/serializable.js";
+/** @deprecated For backwards compatibility only. Remove on next minor version upgrade. */
+export interface BaseStoreInterface {
+ /**
+ * Method to get multiple values for a set of keys.
+ * @param {K[]} keys - An array of keys.
+ * @returns {Promise<(V | undefined)[]>} - A Promise that resolves with array of values or undefined if key not found.
+ */
+ mget(keys: K[]): Promise<(V | undefined)[]>;
+
+ /**
+ * Method to set a value for multiple keys.
+ * @param {[K, V][]} keyValuePairs - An array of key-value pairs.
+ * @returns {Promise} - A Promise that resolves when the operation is complete.
+ */
+ mset(keyValuePairs: [K, V][]): Promise;
+
+ /**
+ * Method to delete multiple keys.
+ * @param {K[]} keys - An array of keys to delete.
+ * @returns {Promise} - A Promise that resolves when the operation is complete.
+ */
+ mdelete(keys: K[]): Promise;
+
+ /**
+ * Method to yield keys optionally based on a prefix.
+ * @param {string} prefix - Optional prefix to filter keys.
+ * @returns {AsyncGenerator} - An asynchronous generator that yields keys on iteration.
+ */
+ yieldKeys(prefix?: string): AsyncGenerator;
+}
+
/**
* Abstract interface for a key-value store.
*/
-export abstract class BaseStore extends Serializable {
+export abstract class BaseStore
+ extends Serializable
+ implements BaseStoreInterface
+{
/**
* Abstract method to get multiple values for a set of keys.
* @param {K[]} keys - An array of keys.
diff --git a/langchain/src/storage/encoder_backed.ts b/langchain/src/storage/encoder_backed.ts
index 761737211e28..cd1eec93196b 100644
--- a/langchain/src/storage/encoder_backed.ts
+++ b/langchain/src/storage/encoder_backed.ts
@@ -84,13 +84,19 @@ export class EncoderBackedStore extends BaseStore<
}
}
-export function createDocumentStoreFromByteStore(store: BaseStore) {
+export function createDocumentStoreFromByteStore(
+ store: BaseStore
+) {
const encoder = new TextEncoder();
const decoder = new TextDecoder();
return new EncoderBackedStore({
store,
keyEncoder: (key: string) => key,
- valueSerializer: (doc: Document) => encoder.encode(JSON.stringify({ pageContent: doc.pageContent, metadata: doc.metadata })),
- valueDeserializer: (bytes: Uint8Array) => new Document(JSON.parse(decoder.decode(bytes))),
- })
-}
\ No newline at end of file
+ valueSerializer: (doc: Document) =>
+ encoder.encode(
+ JSON.stringify({ pageContent: doc.pageContent, metadata: doc.metadata })
+ ),
+ valueDeserializer: (bytes: Uint8Array) =>
+ new Document(JSON.parse(decoder.decode(bytes))),
+ });
+}
diff --git a/langchain/src/storage/tests/vercel_kv.int.test.ts b/langchain/src/storage/tests/vercel_kv.int.test.ts
index 35853f707226..70bd6682aa96 100644
--- a/langchain/src/storage/tests/vercel_kv.int.test.ts
+++ b/langchain/src/storage/tests/vercel_kv.int.test.ts
@@ -22,7 +22,10 @@ test("VercelKVStore", async () => {
["key2", encoder.encode(value2)],
]);
const retrievedValues = await store.mget(["key1", "key2"]);
- expect(retrievedValues).toEqual([encoder.encode(value1), encoder.encode(value2)]);
+ expect(retrievedValues).toEqual([
+ encoder.encode(value1),
+ encoder.encode(value2),
+ ]);
for await (const key of store.yieldKeys()) {
console.log(key);
}
@@ -32,15 +35,20 @@ test("VercelKVStore", async () => {
});
test("Encoder-backed", async () => {
- const store = createDocumentStoreFromByteStore(new VercelKVStore({
- client: createClient({
- url: process.env.VERCEL_KV_API_URL!,
- token: process.env.VERCEL_KV_API_TOKEN!,
- }),
- }));
+ const store = createDocumentStoreFromByteStore(
+ new VercelKVStore({
+ client: createClient({
+ url: process.env.VERCEL_KV_API_URL!,
+ token: process.env.VERCEL_KV_API_TOKEN!,
+ }),
+ })
+ );
const value1 = new Date().toISOString();
const value2 = new Date().toISOString() + new Date().toISOString();
- const [doc1, doc2] = [new Document({ pageContent: value1 }), new Document({ pageContent: value2 })];
+ const [doc1, doc2] = [
+ new Document({ pageContent: value1 }),
+ new Document({ pageContent: value2 }),
+ ];
await store.mset([
["key1", doc1],
["key2", doc2],
@@ -53,4 +61,4 @@ test("Encoder-backed", async () => {
await store.mdelete(["key1", "key2"]);
const retrievedValues2 = await store.mget(["key1", "key2"]);
expect(retrievedValues2).toEqual([undefined, undefined]);
-})
+});
diff --git a/langchain/src/stores/doc/in_memory.ts b/langchain/src/stores/doc/in_memory.ts
index feeb4f917cd5..f11220f66170 100644
--- a/langchain/src/stores/doc/in_memory.ts
+++ b/langchain/src/stores/doc/in_memory.ts
@@ -1,11 +1,15 @@
import { Document } from "../../document.js";
import { Docstore } from "../../schema/index.js";
+import { BaseStoreInterface } from "../../schema/storage.js";
/**
* Class for storing and retrieving documents in memory asynchronously.
* Extends the Docstore class.
*/
-export class InMemoryDocstore extends Docstore {
+export class InMemoryDocstore
+ extends Docstore
+ implements BaseStoreInterface
+{
_docs: Map;
constructor(docs?: Map) {
@@ -44,6 +48,25 @@ export class InMemoryDocstore extends Docstore {
this._docs.set(key, value);
}
}
+
+ async mget(keys: string[]): Promise {
+ return Promise.all(keys.map((key) => this.search(key)));
+ }
+
+ async mset(keyValuePairs: [string, Document][]): Promise {
+ await Promise.all(
+ keyValuePairs.map(([key, value]) => this.add({ [key]: value }))
+ );
+ }
+
+ async mdelete(_keys: string[]): Promise {
+ throw new Error("Not implemented.");
+ }
+
+ // eslint-disable-next-line require-yield
+ async *yieldKeys(_prefix?: string): AsyncGenerator {
+ throw new Error("Not implemented");
+ }
}
/**
diff --git a/langchain/src/vectorstores/cassandra.ts b/langchain/src/vectorstores/cassandra.ts
index e191514d70f8..9161fddfd351 100644
--- a/langchain/src/vectorstores/cassandra.ts
+++ b/langchain/src/vectorstores/cassandra.ts
@@ -195,7 +195,8 @@ export class CassandraStore extends VectorStore {
vector VECTOR
);`);
- await this.client.execute(`CREATE CUSTOM INDEX IF NOT EXISTS ann_index
+ await this.client
+ .execute(`CREATE CUSTOM INDEX IF NOT EXISTS idx_vector_${this.table}
ON ${this.keyspace}.${this.table}(vector) USING 'StorageAttachedIndex';`);
this.isInitialized = true;
}
@@ -218,9 +219,13 @@ export class CassandraStore extends VectorStore {
const metadataColNames = Object.keys(document.metadata);
const metadataVals = Object.values(document.metadata);
- const query = `INSERT INTO ${this.keyspace}.${this.table} (vector, text${
- metadataColNames.length > 0 ? ", " + metadataColNames.join(", ") : ""
- }) VALUES ([${vector}], '${document.pageContent}'${
+ const metadataInsert =
+ metadataColNames.length > 0 ? ", " + metadataColNames.join(", ") : "";
+ const query = `INSERT INTO ${this.keyspace}.${
+ this.table
+ } (vector, text${metadataInsert}) VALUES ([${vector}], '${
+ document.pageContent
+ }'${
metadataVals.length > 0
? ", " +
metadataVals
diff --git a/langchain/src/vectorstores/elasticsearch.ts b/langchain/src/vectorstores/elasticsearch.ts
index 81b7c0252332..b3d4f07d6e45 100644
--- a/langchain/src/vectorstores/elasticsearch.ts
+++ b/langchain/src/vectorstores/elasticsearch.ts
@@ -76,7 +76,9 @@ export class ElasticVectorSearch extends VectorStore {
this.efConstruction = args.vectorSearchOptions?.efConstruction ?? 100;
this.candidates = args.vectorSearchOptions?.candidates ?? 200;
- this.client = args.client;
+ this.client = args.client.child({
+ headers: { "user-agent": "langchain-js-vs/0.0.1" },
+ });
this.indexName = args.indexName ?? "documents";
}
diff --git a/langchain/src/vectorstores/neo4j_vector.ts b/langchain/src/vectorstores/neo4j_vector.ts
new file mode 100644
index 000000000000..b82afbfec374
--- /dev/null
+++ b/langchain/src/vectorstores/neo4j_vector.ts
@@ -0,0 +1,717 @@
+import neo4j from "neo4j-driver";
+import * as uuid from "uuid";
+import { Document } from "../document.js";
+import { Embeddings } from "../embeddings/base.js";
+import { VectorStore } from "./base.js";
+
+export type SearchType = "vector" | "hybrid";
+
+export type DistanceStrategy = "euclidean" | "cosine";
+
+interface Neo4jVectorStoreArgs {
+ url: string;
+ username: string;
+ password: string;
+ database?: string;
+ preDeleteCollection?: boolean;
+ textNodeProperty?: string;
+ textNodeProperties?: string[];
+ embeddingNodeProperty?: string;
+ keywordIndexName?: string;
+ indexName?: string;
+ searchType?: SearchType;
+ retrievalQuery?: string;
+ nodeLabel?: string;
+ createIdIndex?: boolean;
+}
+
+const DEFAULT_SEARCH_TYPE = "vector";
+const DEFAULT_DISTANCE_STRATEGY = "cosine";
+
+export class Neo4jVectorStore extends VectorStore {
+ private driver: neo4j.Driver;
+
+ private database: string;
+
+ private preDeleteCollection: boolean;
+
+ private nodeLabel: string;
+
+ private embeddingNodeProperty: string;
+
+ private embeddingDimension: number;
+
+ private textNodeProperty: string;
+
+ private keywordIndexName: string;
+
+ private indexName: string;
+
+ private retrievalQuery: string;
+
+ private searchType: SearchType;
+
+ private distanceStrategy: DistanceStrategy = DEFAULT_DISTANCE_STRATEGY;
+
+ _vectorstoreType(): string {
+ return "neo4jvector";
+ }
+
+ constructor(embeddings: Embeddings, config: Neo4jVectorStoreArgs) {
+ super(embeddings, config);
+ }
+
+ static async initialize(
+ embeddings: Embeddings,
+ config: Neo4jVectorStoreArgs
+ ) {
+ const store = new Neo4jVectorStore(embeddings, config);
+ await store._initializeDriver(config);
+ await store._verifyConnectivity();
+
+ const {
+ preDeleteCollection = false,
+ nodeLabel = "Chunk",
+ textNodeProperty = "text",
+ embeddingNodeProperty = "embedding",
+ keywordIndexName = "keyword",
+ indexName = "vector",
+ retrievalQuery = "",
+ searchType = DEFAULT_SEARCH_TYPE,
+ } = config;
+
+ store.embeddingDimension = (await embeddings.embedQuery("foo")).length;
+ store.preDeleteCollection = preDeleteCollection;
+ store.nodeLabel = nodeLabel;
+ store.textNodeProperty = textNodeProperty;
+ store.embeddingNodeProperty = embeddingNodeProperty;
+ store.keywordIndexName = keywordIndexName;
+ store.indexName = indexName;
+ store.retrievalQuery = retrievalQuery;
+ store.searchType = searchType;
+
+ if (store.preDeleteCollection) {
+ await store._dropIndex();
+ }
+
+ return store;
+ }
+
+ async _initializeDriver({
+ url,
+ username,
+ password,
+ database = "neo4j",
+ }: Neo4jVectorStoreArgs) {
+ try {
+ this.driver = neo4j.driver(url, neo4j.auth.basic(username, password));
+ this.database = database;
+ } catch (error) {
+ throw new Error(
+ "Could not create a Neo4j driver instance. Please check the connection details."
+ );
+ }
+ }
+
+ async _verifyConnectivity() {
+ await this.driver.verifyAuthentication();
+ }
+
+ async close() {
+ await this.driver.close();
+ }
+
+ async _dropIndex() {
+ try {
+ await this.query(`
+ MATCH (n:\`${this.nodeLabel}\`)
+ CALL {
+ WITH n
+ DETACH DELETE n
+ }
+ IN TRANSACTIONS OF 10000 ROWS;
+ `);
+ await this.query(`DROP INDEX ${this.indexName}`);
+ } catch (error) {
+ console.error("An error occurred while dropping the index:", error);
+ }
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ async query(query: string, params: any = {}): Promise {
+ const session = this.driver.session({ database: this.database });
+ const result = await session.run(query, params);
+ return toObjects(result.records);
+ }
+
+ static async fromTexts(
+ texts: string[],
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ metadatas: any,
+ embeddings: Embeddings,
+ config: Neo4jVectorStoreArgs
+ ): Promise {
+ const docs = [];
+
+ for (let i = 0; i < texts.length; i += 1) {
+ const metadata = Array.isArray(metadatas) ? metadatas[i] : metadatas;
+ const newDoc = new Document({
+ pageContent: texts[i],
+ metadata,
+ });
+ docs.push(newDoc);
+ }
+
+ return Neo4jVectorStore.fromDocuments(docs, embeddings, config);
+ }
+
+ static async fromDocuments(
+ docs: Document[],
+ embeddings: Embeddings,
+ config: Neo4jVectorStoreArgs
+ ): Promise {
+ const {
+ searchType = DEFAULT_SEARCH_TYPE,
+ createIdIndex = true,
+ textNodeProperties = [],
+ } = config;
+
+ const store = await this.initialize(embeddings, config);
+
+ const embeddingDimension = await store.retrieveExistingIndex();
+
+ if (!embeddingDimension) {
+ await store.createNewIndex();
+ } else if (store.embeddingDimension !== embeddingDimension) {
+ throw new Error(
+ `Index with name "${store.indexName}" already exists. The provided embedding function and vector index dimensions do not match.
+ Embedding function dimension: ${store.embeddingDimension}
+ Vector index dimension: ${embeddingDimension}`
+ );
+ }
+
+ if (searchType === "hybrid") {
+ const ftsNodeLabel = await store.retrieveExistingFtsIndex();
+
+ if (!ftsNodeLabel) {
+ await store.createNewKeywordIndex(textNodeProperties);
+ } else {
+ if (ftsNodeLabel !== store.nodeLabel) {
+ throw Error(
+ "Vector and keyword index don't index the same node label"
+ );
+ }
+ }
+ }
+
+ if (createIdIndex) {
+ await store.query(
+ `CREATE CONSTRAINT IF NOT EXISTS FOR (n:${store.nodeLabel}) REQUIRE n.id IS UNIQUE;`
+ );
+ }
+
+ await store.addDocuments(docs);
+
+ return store;
+ }
+
+ static async fromExistingIndex(
+ embeddings: Embeddings,
+ config: Neo4jVectorStoreArgs
+ ) {
+ const { searchType = DEFAULT_SEARCH_TYPE, keywordIndexName = "keyword" } =
+ config;
+
+ if (searchType === "hybrid" && !keywordIndexName) {
+ throw Error(
+ "keyword_index name has to be specified when using hybrid search option"
+ );
+ }
+
+ const store = await this.initialize(embeddings, config);
+ const embeddingDimension = await store.retrieveExistingIndex();
+
+ if (!embeddingDimension) {
+ throw Error(
+ "The specified vector index name does not exist. Make sure to check if you spelled it correctly"
+ );
+ }
+
+ if (store.embeddingDimension !== embeddingDimension) {
+ throw new Error(
+ `The provided embedding function and vector index dimensions do not match.
+ Embedding function dimension: ${store.embeddingDimension}
+ Vector index dimension: ${embeddingDimension}`
+ );
+ }
+
+ if (searchType === "hybrid") {
+ const ftsNodeLabel = await store.retrieveExistingFtsIndex();
+
+ if (!ftsNodeLabel) {
+ throw Error(
+ "The specified keyword index name does not exist. Make sure to check if you spelled it correctly"
+ );
+ } else {
+ if (ftsNodeLabel !== store.nodeLabel) {
+ throw Error(
+ "Vector and keyword index don't index the same node label"
+ );
+ }
+ }
+ }
+
+ return store;
+ }
+
+ static async fromExistingGraph(
+ embeddings: Embeddings,
+ config: Neo4jVectorStoreArgs
+ ) {
+ const {
+ textNodeProperties = [],
+ embeddingNodeProperty,
+ searchType = DEFAULT_SEARCH_TYPE,
+ retrievalQuery = "",
+ nodeLabel,
+ } = config;
+
+ let _retrievalQuery = retrievalQuery;
+
+ if (textNodeProperties.length === 0) {
+ throw Error(
+ "Parameter `text_node_properties` must not be an empty array"
+ );
+ }
+
+ if (!retrievalQuery) {
+ _retrievalQuery = `
+ RETURN reduce(str='', k IN ${JSON.stringify(textNodeProperties)} |
+ str + '\\n' + k + ': ' + coalesce(node[k], '')) AS text,
+ node {.*, \`${embeddingNodeProperty}\`: Null, id: Null, ${textNodeProperties
+ .map((prop) => `\`${prop}\`: Null`)
+ .join(", ")} } AS metadata, score
+ `;
+ }
+
+ const store = await this.initialize(embeddings, {
+ ...config,
+ retrievalQuery: _retrievalQuery,
+ });
+
+ const embeddingDimension = await store.retrieveExistingIndex();
+
+ if (!embeddingDimension) {
+ await store.createNewIndex();
+ } else if (store.embeddingDimension !== embeddingDimension) {
+ throw new Error(
+ `Index with name ${store.indexName} already exists. The provided embedding function and vector index dimensions do not match.\nEmbedding function dimension: ${store.embeddingDimension}\nVector index dimension: ${embeddingDimension}`
+ );
+ }
+
+ if (searchType === "hybrid") {
+ const ftsNodeLabel = await store.retrieveExistingFtsIndex(
+ textNodeProperties
+ );
+
+ if (!ftsNodeLabel) {
+ await store.createNewKeywordIndex(textNodeProperties);
+ } else {
+ if (ftsNodeLabel !== store.nodeLabel) {
+ throw Error(
+ "Vector and keyword index don't index the same node label"
+ );
+ }
+ }
+ }
+
+ // eslint-disable-next-line no-constant-condition
+ while (true) {
+ const fetchQuery = `
+ MATCH (n:\`${nodeLabel}\`)
+ WHERE n.${embeddingNodeProperty} IS null
+ AND any(k in $props WHERE n[k] IS NOT null)
+ RETURN elementId(n) AS id, reduce(str='', k IN $props |
+ str + '\\n' + k + ':' + coalesce(n[k], '')) AS text
+ LIMIT 1000
+ `;
+
+ const data = await store.query(fetchQuery, { props: textNodeProperties });
+
+ if (!data) {
+ continue;
+ }
+
+ const textEmbeddings = await embeddings.embedDocuments(
+ data.map((el) => el.text)
+ );
+
+ const params = {
+ data: data.map((el, index) => ({
+ id: el.id,
+ embedding: textEmbeddings[index],
+ })),
+ };
+
+ await store.query(
+ `
+ UNWIND $data AS row
+ MATCH (n:\`${nodeLabel}\`)
+ WHERE elementId(n) = row.id
+ CALL db.create.setVectorProperty(n, '${embeddingNodeProperty}', row.embedding)
+ YIELD node RETURN count(*)
+ `,
+ params
+ );
+
+ if (data.length < 1000) {
+ break;
+ }
+ }
+
+ return store;
+ }
+
+ async createNewIndex(): Promise {
+ const indexQuery = `
+ CALL db.index.vector.createNodeIndex(
+ $index_name,
+ $node_label,
+ $embedding_node_property,
+ toInteger($embedding_dimension),
+ $similarity_metric
+ )
+ `;
+
+ const parameters = {
+ index_name: this.indexName,
+ node_label: this.nodeLabel,
+ embedding_node_property: this.embeddingNodeProperty,
+ embedding_dimension: this.embeddingDimension,
+ similarity_metric: this.distanceStrategy,
+ };
+
+ await this.query(indexQuery, parameters);
+ }
+
+ async retrieveExistingIndex() {
+ let indexInformation = await this.query(
+ `
+ SHOW INDEXES YIELD name, type, labelsOrTypes, properties, options
+ WHERE type = 'VECTOR' AND (name = $index_name
+ OR (labelsOrTypes[0] = $node_label AND
+ properties[0] = $embedding_node_property))
+ RETURN name, labelsOrTypes, properties, options
+ `,
+ {
+ index_name: this.indexName,
+ node_label: this.nodeLabel,
+ embedding_node_property: this.embeddingNodeProperty,
+ }
+ );
+
+ if (indexInformation) {
+ indexInformation = this.sortByIndexName(indexInformation, this.indexName);
+
+ try {
+ const [index] = indexInformation;
+ const [labelOrType] = index.labelsOrTypes;
+ const [property] = index.properties;
+
+ this.indexName = index.name;
+ this.nodeLabel = labelOrType;
+ this.embeddingNodeProperty = property;
+
+ const embeddingDimension =
+ index.options.indexConfig["vector.dimensions"];
+ return Number(embeddingDimension);
+ } catch (error) {
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ async retrieveExistingFtsIndex(
+ textNodeProperties: string[] = []
+ ): Promise {
+ const indexInformation = await this.query(
+ `
+ SHOW INDEXES YIELD name, type, labelsOrTypes, properties, options
+ WHERE type = 'FULLTEXT' AND (name = $keyword_index_name
+ OR (labelsOrTypes = [$node_label] AND
+ properties = $text_node_property))
+ RETURN name, labelsOrTypes, properties, options
+ `,
+ {
+ keyword_index_name: this.keywordIndexName,
+ node_label: this.nodeLabel,
+ text_node_property:
+ textNodeProperties.length > 0
+ ? textNodeProperties
+ : [this.textNodeProperty],
+ }
+ );
+
+ if (indexInformation) {
+ // Sort the index information by index name
+ const sortedIndexInformation = this.sortByIndexName(
+ indexInformation,
+ this.indexName
+ );
+
+ try {
+ const [index] = sortedIndexInformation;
+ const [labelOrType] = index.labelsOrTypes;
+ const [property] = index.properties;
+
+ this.keywordIndexName = index.name;
+ this.textNodeProperty = property;
+ this.nodeLabel = labelOrType;
+
+ return labelOrType;
+ } catch (error) {
+ return null;
+ }
+ }
+
+ return null;
+ }
+
+ async createNewKeywordIndex(
+ textNodeProperties: string[] = []
+ ): Promise {
+ const nodeProps =
+ textNodeProperties.length > 0
+ ? textNodeProperties
+ : [this.textNodeProperty];
+
+ // Construct the Cypher query to create a new full text index
+ const ftsIndexQuery = `
+ CREATE FULLTEXT INDEX ${this.keywordIndexName}
+ FOR (n:\`${this.nodeLabel}\`) ON EACH
+ [${nodeProps.map((prop) => `n.\`${prop}\``).join(", ")}]
+ `;
+
+ await this.query(ftsIndexQuery);
+ }
+
+ sortByIndexName(
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ values: Array<{ [key: string]: any }>,
+ indexName: string
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ ): Array<{ [key: string]: any }> {
+ return values.sort(
+ (a, b) =>
+ (a.index_name === indexName ? -1 : 0) -
+ (b.index_name === indexName ? -1 : 0)
+ );
+ }
+
+ async addVectors(
+ vectors: number[][],
+ documents: Document[],
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ metadatas?: Record[],
+ ids?: string[]
+ ): Promise {
+ let _ids = ids;
+ let _metadatas = metadatas;
+
+ if (!_ids) {
+ _ids = documents.map(() => uuid.v1());
+ }
+
+ if (!metadatas) {
+ _metadatas = documents.map(() => ({}));
+ }
+
+ const importQuery = `
+ UNWIND $data AS row
+ CALL {
+ WITH row
+ MERGE (c:\`${this.nodeLabel}\` {id: row.id})
+ WITH c, row
+ CALL db.create.setVectorProperty(c, '${this.embeddingNodeProperty}', row.embedding)
+ YIELD node
+ SET c.\`${this.textNodeProperty}\` = row.text
+ SET c += row.metadata
+ } IN TRANSACTIONS OF 1000 ROWS
+ `;
+
+ const parameters = {
+ data: documents.map(({ pageContent, metadata }, index) => ({
+ text: pageContent,
+ metadata: _metadatas ? _metadatas[index] : metadata,
+ embedding: vectors[index],
+ id: _ids ? _ids[index] : null,
+ })),
+ };
+
+ await this.query(importQuery, parameters);
+
+ return _ids;
+ }
+
+ async addDocuments(documents: Document[]): Promise {
+ const texts = documents.map(({ pageContent }) => pageContent);
+
+ return this.addVectors(
+ await this.embeddings.embedDocuments(texts),
+ documents
+ );
+ }
+
+ async similaritySearch(query: string, k = 4): Promise {
+ const embedding = await this.embeddings.embedQuery(query);
+
+ const results = await this.similaritySearchVectorWithScore(
+ embedding,
+ k,
+ query
+ );
+
+ return results.map((result) => result[0]);
+ }
+
+ async similaritySearchVectorWithScore(
+ vector: number[],
+ k: number,
+ query: string
+ ): Promise<[Document, number][]> {
+ const defaultRetrieval = `
+ RETURN node.${this.textNodeProperty} AS text, score,
+ node {.*, ${this.textNodeProperty}: Null,
+ ${this.embeddingNodeProperty}: Null, id: Null } AS metadata
+ `;
+
+ const retrievalQuery = this.retrievalQuery
+ ? this.retrievalQuery
+ : defaultRetrieval;
+
+ const readQuery = `${getSearchIndexQuery(
+ this.searchType
+ )} ${retrievalQuery}`;
+
+ const parameters = {
+ index: this.indexName,
+ k: Number(k),
+ embedding: vector,
+ keyword_index: this.keywordIndexName,
+ query,
+ };
+ const results = await this.query(readQuery, parameters);
+
+ if (results) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const docs: [Document, number][] = results.map((result: any) => [
+ new Document({
+ pageContent: result.text,
+ metadata: Object.fromEntries(
+ Object.entries(result.metadata).filter(([_, v]) => v !== null)
+ ),
+ }),
+ result.score,
+ ]);
+
+ return docs;
+ }
+
+ return [];
+ }
+}
+
+function toObjects(records: neo4j.Record[]) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const recordValues: Record[] = records.map((record) => {
+ const rObj = record.toObject();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const out: { [key: string]: any } = {};
+ Object.keys(rObj).forEach((key) => {
+ out[key] = itemIntToString(rObj[key]);
+ });
+ return out;
+ });
+ return recordValues;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function itemIntToString(item: any): any {
+ if (neo4j.isInt(item)) return item.toString();
+ if (Array.isArray(item)) return item.map((ii) => itemIntToString(ii));
+ if (["number", "string", "boolean"].indexOf(typeof item) !== -1) return item;
+ if (item === null) return item;
+ if (typeof item === "object") return objIntToString(item);
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function objIntToString(obj: any) {
+ const entry = extractFromNeoObjects(obj);
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ let newObj: any = null;
+ if (Array.isArray(entry)) {
+ newObj = entry.map((item) => itemIntToString(item));
+ } else if (entry !== null && typeof entry === "object") {
+ newObj = {};
+ Object.keys(entry).forEach((key) => {
+ newObj[key] = itemIntToString(entry[key]);
+ });
+ }
+ return newObj;
+}
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+function extractFromNeoObjects(obj: any) {
+ if (
+ // eslint-disable-next-line
+ obj instanceof (neo4j.types.Node as any) ||
+ // eslint-disable-next-line
+ obj instanceof (neo4j.types.Relationship as any)
+ ) {
+ return obj.properties;
+ // eslint-disable-next-line
+ } else if (obj instanceof (neo4j.types.Path as any)) {
+ // eslint-disable-next-line
+ return [].concat.apply([], extractPathForRows(obj));
+ }
+ return obj;
+}
+
+function extractPathForRows(path: neo4j.Path) {
+ let { segments } = path;
+ // Zero length path. No relationship, end === start
+ if (!Array.isArray(path.segments) || path.segments.length < 1) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ segments = [{ ...path, end: null } as any];
+ }
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ return segments.map((segment: any) =>
+ [
+ objIntToString(segment.start),
+ objIntToString(segment.relationship),
+ objIntToString(segment.end),
+ ].filter((part) => part !== null)
+ );
+}
+
+function getSearchIndexQuery(searchType: SearchType): string {
+ const typeToQueryMap: { [key in SearchType]: string } = {
+ vector:
+ "CALL db.index.vector.queryNodes($index, $k, $embedding) YIELD node, score",
+ hybrid: `
+ CALL {
+ CALL db.index.vector.queryNodes($index, $k, $embedding) YIELD node, score
+ RETURN node, score UNION
+ CALL db.index.fulltext.queryNodes($keyword_index, $query, {limit: $k}) YIELD node, score
+ WITH collect({node: node, score: score}) AS nodes, max(score) AS max
+ UNWIND nodes AS n
+ RETURN n.node AS node, (n.score / max) AS score
+ }
+ WITH node, max(score) AS score ORDER BY score DESC LIMIT toInteger($k)
+ `,
+ };
+
+ return typeToQueryMap[searchType];
+}
diff --git a/langchain/src/vectorstores/tests/cassandra.int.test.ts b/langchain/src/vectorstores/tests/cassandra.int.test.ts
index 902b8fa06342..38775d49948d 100644
--- a/langchain/src/vectorstores/tests/cassandra.int.test.ts
+++ b/langchain/src/vectorstores/tests/cassandra.int.test.ts
@@ -5,6 +5,7 @@ import { CassandraStore } from "../cassandra.js";
import { OpenAIEmbeddings } from "../../embeddings/openai.js";
import { Document } from "../../document.js";
+// yarn test:single /langchain/src/vectorstores/tests/cassandra.int.test.ts
describe.skip("CassandraStore", () => {
const cassandraConfig = {
cloud: {
diff --git a/langchain/src/vectorstores/tests/elasticsearch.int.test.ts b/langchain/src/vectorstores/tests/elasticsearch.int.test.ts
index 361e753240cb..4aa3be383bfc 100644
--- a/langchain/src/vectorstores/tests/elasticsearch.int.test.ts
+++ b/langchain/src/vectorstores/tests/elasticsearch.int.test.ts
@@ -1,6 +1,6 @@
/* eslint-disable no-process-env */
import { test, expect } from "@jest/globals";
-import { Client } from "@elastic/elasticsearch";
+import { Client, ClientOptions } from "@elastic/elasticsearch";
import { OpenAIEmbeddings } from "../../embeddings/openai.js";
import { ElasticVectorSearch } from "../elasticsearch.js";
import { Document } from "../../document.js";
@@ -13,8 +13,7 @@ describe("ElasticVectorSearch", () => {
throw new Error("ELASTIC_URL not set");
}
- /* eslint-disable @typescript-eslint/no-explicit-any */
- const config: any = {
+ const config: ClientOptions = {
node: process.env.ELASTIC_URL,
};
if (process.env.ELASTIC_API_KEY) {
@@ -32,7 +31,7 @@ describe("ElasticVectorSearch", () => {
const indexName = "test_index";
const embeddings = new OpenAIEmbeddings();
- const store = new ElasticVectorSearch(embeddings, { client, indexName });
+ store = new ElasticVectorSearch(embeddings, { client, indexName });
await store.deleteIfExists();
expect(store).toBeDefined();
diff --git a/langchain/src/vectorstores/tests/neo4j_vector.int.test.ts b/langchain/src/vectorstores/tests/neo4j_vector.int.test.ts
new file mode 100644
index 000000000000..8fb9268fa076
--- /dev/null
+++ b/langchain/src/vectorstores/tests/neo4j_vector.int.test.ts
@@ -0,0 +1,471 @@
+/* eslint-disable no-process-env */
+import { FakeEmbeddings } from "../../embeddings/fake.js";
+import { Neo4jVectorStore } from "../neo4j_vector.js";
+import { Document } from "../../document.js";
+
+const OS_TOKEN_COUNT = 1536;
+
+const texts = ["foo", "bar", "baz"];
+
+class FakeEmbeddingsWithOsDimension extends FakeEmbeddings {
+ async embedDocuments(documents: string[]): Promise {
+ return Promise.resolve(
+ documents.map((_, i) =>
+ Array(OS_TOKEN_COUNT - 1)
+ .fill(1.0)
+ .concat([i + 1.0])
+ )
+ );
+ }
+
+ async embedQuery(text: string): Promise {
+ const index = texts.indexOf(text);
+
+ if (index !== -1) {
+ return Array(OS_TOKEN_COUNT - 1)
+ .fill(1.0)
+ .concat([index + 1]);
+ } else {
+ throw new Error(`Text '${text}' not found in the 'texts' array.`);
+ }
+ }
+}
+
+async function dropVectorIndexes(store: Neo4jVectorStore) {
+ const allIndexes = await store.query(`
+ SHOW INDEXES YIELD name, type
+ WHERE type = "VECTOR"
+ RETURN name
+ `);
+
+ if (allIndexes) {
+ for (const index of allIndexes) {
+ await store.query(`DROP INDEX ${index.name}`);
+ }
+ }
+}
+
+test("Test fromTexts", async () => {
+ const url = process.env.NEO4J_URI as string;
+ const username = process.env.NEO4J_USERNAME as string;
+ const password = process.env.NEO4J_PASSWORD as string;
+
+ expect(url).toBeDefined();
+ expect(username).toBeDefined();
+ expect(password).toBeDefined();
+
+ const embeddings = new FakeEmbeddingsWithOsDimension();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const metadatas: any[] = [];
+
+ const neo4jVectorStore = await Neo4jVectorStore.fromTexts(
+ texts,
+ metadatas,
+ embeddings,
+ {
+ url,
+ username,
+ password,
+ preDeleteCollection: true,
+ }
+ );
+
+ const output = await neo4jVectorStore.similaritySearch("foo", 2);
+
+ const expectedResult = [
+ new Document({
+ pageContent: "foo",
+ metadata: {},
+ }),
+ new Document({
+ pageContent: "bar",
+ metadata: {},
+ }),
+ ];
+
+ expect(output).toStrictEqual(expectedResult);
+ await dropVectorIndexes(neo4jVectorStore);
+ await neo4jVectorStore.close();
+});
+
+test("Test fromTexts Hybrid", async () => {
+ const url = process.env.NEO4J_URI as string;
+ const username = process.env.NEO4J_USERNAME as string;
+ const password = process.env.NEO4J_PASSWORD as string;
+
+ expect(url).toBeDefined();
+ expect(username).toBeDefined();
+ expect(password).toBeDefined();
+
+ const embeddings = new FakeEmbeddingsWithOsDimension();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const metadatas: any[] = [];
+
+ const neo4jVectorStore = await Neo4jVectorStore.fromTexts(
+ texts,
+ metadatas,
+ embeddings,
+ {
+ url,
+ username,
+ password,
+ preDeleteCollection: true,
+ searchType: "hybrid",
+ }
+ );
+
+ const output = await neo4jVectorStore.similaritySearch("foo", 2);
+
+ const expectedResult = [
+ new Document({
+ pageContent: "foo",
+ metadata: {},
+ }),
+ new Document({
+ pageContent: "bar",
+ metadata: {},
+ }),
+ ];
+
+ expect(output).toStrictEqual(expectedResult);
+ await dropVectorIndexes(neo4jVectorStore);
+ await neo4jVectorStore.close();
+});
+
+test("Test fromExistingIndex", async () => {
+ const url = process.env.NEO4J_URI as string;
+ const username = process.env.NEO4J_USERNAME as string;
+ const password = process.env.NEO4J_PASSWORD as string;
+
+ expect(url).toBeDefined();
+ expect(username).toBeDefined();
+ expect(password).toBeDefined();
+
+ const embeddings = new FakeEmbeddingsWithOsDimension();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const metadatas: any[] = [];
+
+ const neo4jVectorStore = await Neo4jVectorStore.fromTexts(
+ texts,
+ metadatas,
+ embeddings,
+ {
+ url,
+ username,
+ password,
+ indexName: "vector",
+ preDeleteCollection: true,
+ }
+ );
+
+ const existingIndex = await Neo4jVectorStore.fromExistingIndex(embeddings, {
+ url,
+ username,
+ password,
+ indexName: "vector",
+ });
+
+ const output = await existingIndex.similaritySearch("foo", 2);
+
+ const expectedResult = [
+ new Document({
+ pageContent: "foo",
+ metadata: {},
+ }),
+ new Document({
+ pageContent: "bar",
+ metadata: {},
+ }),
+ ];
+
+ expect(output).toStrictEqual(expectedResult);
+ await dropVectorIndexes(neo4jVectorStore);
+ await neo4jVectorStore.close();
+ await existingIndex.close();
+});
+
+test("Test fromExistingIndex Hybrid", async () => {
+ const url = process.env.NEO4J_URI as string;
+ const username = process.env.NEO4J_USERNAME as string;
+ const password = process.env.NEO4J_PASSWORD as string;
+
+ expect(url).toBeDefined();
+ expect(username).toBeDefined();
+ expect(password).toBeDefined();
+
+ const embeddings = new FakeEmbeddingsWithOsDimension();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const metadatas: any[] = [];
+
+ const neo4jVectorStore = await Neo4jVectorStore.fromTexts(
+ texts,
+ metadatas,
+ embeddings,
+ {
+ url,
+ username,
+ password,
+ indexName: "vector",
+ keywordIndexName: "keyword",
+ searchType: "hybrid",
+ preDeleteCollection: true,
+ }
+ );
+
+ const existingIndex = await Neo4jVectorStore.fromExistingIndex(embeddings, {
+ url,
+ username,
+ password,
+ indexName: "vector",
+ keywordIndexName: "keyword",
+ searchType: "hybrid",
+ });
+
+ const output = await existingIndex.similaritySearch("foo", 2);
+
+ const expectedResult = [
+ new Document({
+ pageContent: "foo",
+ metadata: {},
+ }),
+ new Document({
+ pageContent: "bar",
+ metadata: {},
+ }),
+ ];
+
+ expect(output).toStrictEqual(expectedResult);
+ await dropVectorIndexes(neo4jVectorStore);
+ await neo4jVectorStore.close();
+ await existingIndex.close();
+});
+
+test("Test retrievalQuery", async () => {
+ const url = process.env.NEO4J_URI as string;
+ const username = process.env.NEO4J_USERNAME as string;
+ const password = process.env.NEO4J_PASSWORD as string;
+
+ expect(url).toBeDefined();
+ expect(username).toBeDefined();
+ expect(password).toBeDefined();
+
+ const embeddings = new FakeEmbeddingsWithOsDimension();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const metadatas: any[] = [];
+
+ const neo4jVectorStore = await Neo4jVectorStore.fromTexts(
+ texts,
+ metadatas,
+ embeddings,
+ {
+ url,
+ username,
+ password,
+ indexName: "vector",
+ preDeleteCollection: true,
+ retrievalQuery:
+ "RETURN node.text AS text, score, {foo:'bar'} AS metadata",
+ }
+ );
+
+ const output = await neo4jVectorStore.similaritySearch("foo", 2);
+
+ const expectedResult = [
+ new Document({
+ pageContent: "foo",
+ metadata: { foo: "bar" },
+ }),
+ new Document({
+ pageContent: "bar",
+ metadata: { foo: "bar" },
+ }),
+ ];
+
+ expect(output).toStrictEqual(expectedResult);
+ await dropVectorIndexes(neo4jVectorStore);
+ await neo4jVectorStore.close();
+});
+
+test("Test fromExistingGraph", async () => {
+ const url = process.env.NEO4J_URI as string;
+ const username = process.env.NEO4J_USERNAME as string;
+ const password = process.env.NEO4J_PASSWORD as string;
+
+ expect(url).toBeDefined();
+ expect(username).toBeDefined();
+ expect(password).toBeDefined();
+
+ const embeddings = new FakeEmbeddingsWithOsDimension();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const metadatas: any[] = [];
+
+ const neo4jVectorStore = await Neo4jVectorStore.fromTexts(
+ texts,
+ metadatas,
+ embeddings,
+ {
+ url,
+ username,
+ password,
+ indexName: "vector",
+ preDeleteCollection: true,
+ }
+ );
+
+ await neo4jVectorStore.query("MATCH (n) DETACH DELETE n");
+
+ await neo4jVectorStore.query(
+ "CREATE (:Test {name:'Foo'}), (:Test {name:'Bar', foo:'bar'})"
+ );
+
+ const existingGraph = await Neo4jVectorStore.fromExistingGraph(embeddings, {
+ url,
+ username,
+ password,
+ indexName: "vector1",
+ nodeLabel: "Test",
+ textNodeProperties: ["name"],
+ embeddingNodeProperty: "embedding",
+ });
+
+ const output = await existingGraph.similaritySearch("foo", 2);
+
+ const expectedResult = [
+ new Document({
+ pageContent: "\nname: Foo",
+ metadata: {},
+ }),
+ new Document({
+ pageContent: "\nname: Bar",
+ metadata: { foo: "bar" },
+ }),
+ ];
+
+ expect(output).toStrictEqual(expectedResult);
+ await dropVectorIndexes(neo4jVectorStore);
+ await neo4jVectorStore.close();
+ await existingGraph.close();
+});
+
+test("Test fromExistingGraph multiple properties", async () => {
+ const url = process.env.NEO4J_URI as string;
+ const username = process.env.NEO4J_USERNAME as string;
+ const password = process.env.NEO4J_PASSWORD as string;
+
+ expect(url).toBeDefined();
+ expect(username).toBeDefined();
+ expect(password).toBeDefined();
+
+ const embeddings = new FakeEmbeddingsWithOsDimension();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const metadatas: any[] = [];
+
+ const neo4jVectorStore = await Neo4jVectorStore.fromTexts(
+ texts,
+ metadatas,
+ embeddings,
+ {
+ url,
+ username,
+ password,
+ indexName: "vector",
+ preDeleteCollection: true,
+ }
+ );
+
+ await neo4jVectorStore.query("MATCH (n) DETACH DELETE n");
+
+ await neo4jVectorStore.query(
+ "CREATE (:Test {name:'Foo', name2:'Fooz'}), (:Test {name:'Bar', foo:'bar'})"
+ );
+
+ const existingGraph = await Neo4jVectorStore.fromExistingGraph(embeddings, {
+ url,
+ username,
+ password,
+ indexName: "vector1",
+ nodeLabel: "Test",
+ textNodeProperties: ["name", "name2"],
+ embeddingNodeProperty: "embedding",
+ });
+
+ const output = await existingGraph.similaritySearch("foo", 2);
+
+ const expectedResult = [
+ new Document({
+ pageContent: "\nname: Foo\nname2: Fooz",
+ metadata: {},
+ }),
+ new Document({
+ pageContent: "\nname: Bar\nname2: ",
+ metadata: { foo: "bar" },
+ }),
+ ];
+
+ expect(output).toStrictEqual(expectedResult);
+ await dropVectorIndexes(neo4jVectorStore);
+ await neo4jVectorStore.close();
+ await existingGraph.close();
+});
+
+test("Test fromExistingGraph multiple properties hybrid", async () => {
+ const url = process.env.NEO4J_URI as string;
+ const username = process.env.NEO4J_USERNAME as string;
+ const password = process.env.NEO4J_PASSWORD as string;
+
+ expect(url).toBeDefined();
+ expect(username).toBeDefined();
+ expect(password).toBeDefined();
+
+ const embeddings = new FakeEmbeddingsWithOsDimension();
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ const metadatas: any[] = [];
+
+ const neo4jVectorStore = await Neo4jVectorStore.fromTexts(
+ texts,
+ metadatas,
+ embeddings,
+ {
+ url,
+ username,
+ password,
+ indexName: "vector",
+ preDeleteCollection: true,
+ }
+ );
+
+ await neo4jVectorStore.query("MATCH (n) DETACH DELETE n");
+
+ await neo4jVectorStore.query(
+ "CREATE (:Test {name:'Foo', name2:'Fooz'}), (:Test {name:'Bar', foo:'bar'})"
+ );
+
+ const existingGraph = await Neo4jVectorStore.fromExistingGraph(embeddings, {
+ url,
+ username,
+ password,
+ indexName: "vector1",
+ nodeLabel: "Test",
+ textNodeProperties: ["name", "name2"],
+ embeddingNodeProperty: "embedding",
+ searchType: "hybrid",
+ });
+
+ const output = await existingGraph.similaritySearch("foo", 2);
+
+ const expectedResult = [
+ new Document({
+ pageContent: "\nname: Foo\nname2: Fooz",
+ metadata: {},
+ }),
+ new Document({
+ pageContent: "\nname: Bar\nname2: ",
+ metadata: { foo: "bar" },
+ }),
+ ];
+
+ expect(output).toStrictEqual(expectedResult);
+ await dropVectorIndexes(neo4jVectorStore);
+ await neo4jVectorStore.close();
+ await existingGraph.close();
+});
diff --git a/langchain/tsconfig.json b/langchain/tsconfig.json
index f3c795f4853a..594e57d0f128 100644
--- a/langchain/tsconfig.json
+++ b/langchain/tsconfig.json
@@ -87,6 +87,7 @@
"src/llms/llama_cpp.ts",
"src/llms/writer.ts",
"src/llms/portkey.ts",
+ "src/llms/yandex.ts",
"src/prompts/index.ts",
"src/prompts/load.ts",
"src/vectorstores/analyticdb.ts",
diff --git a/yarn.lock b/yarn.lock
index e57234ee47ac..28198230a181 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -11741,6 +11741,28 @@ __metadata:
languageName: node
linkType: hard
+"eslint-plugin-unused-imports@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "eslint-plugin-unused-imports@npm:3.0.0"
+ dependencies:
+ eslint-rule-composer: ^0.3.0
+ peerDependencies:
+ "@typescript-eslint/eslint-plugin": ^6.0.0
+ eslint: ^8.0.0
+ peerDependenciesMeta:
+ "@typescript-eslint/eslint-plugin":
+ optional: true
+ checksum: 51666f62cc8dccba2895ced83f3c1e0b78b68c357e17360e156c4db548bfdeda34cbd8725192fb4903f22d5069400fb22ded6039631df01ee82fd618dc307247
+ languageName: node
+ linkType: hard
+
+"eslint-rule-composer@npm:^0.3.0":
+ version: 0.3.0
+ resolution: "eslint-rule-composer@npm:0.3.0"
+ checksum: c2f57cded8d1c8f82483e0ce28861214347e24fd79fd4144667974cd334d718f4ba05080aaef2399e3bbe36f7d6632865110227e6b176ed6daa2d676df9281b1
+ languageName: node
+ linkType: hard
+
"eslint-scope@npm:^5.1.1":
version: 5.1.1
resolution: "eslint-scope@npm:5.1.1"
@@ -11996,6 +12018,7 @@ __metadata:
eslint-config-prettier: ^8.6.0
eslint-plugin-import: ^2.27.5
eslint-plugin-prettier: ^4.2.1
+ eslint-plugin-unused-imports: ^3.0.0
faiss-node: ^0.3.0
graphql: ^16.6.0
ioredis: ^5.3.2