Skip to content

Commit

Permalink
Adding Redis Memory Support (langchain-ai#951)
Browse files Browse the repository at this point in the history
* Merge post upstream sync (#1)

* Added RedisMemory with tests and docs

* fixed Redis test file name

* TODOs for quick refactor

* updated memory to accept a client

* Added TODOS

* ongoing testing

* Updated tests

* Updated tests for memory return option

* finalized tests + updated docs

* Adding docs

* Readded init and cleaned up func

* fixed test typing

* redo yarn lock

* remove yarn.lock

* updated yarn lock and namespaced redis

* fix merge conflict

* updated BaseChatMemoryInput

* Updated with lint fixes

* Fixed docs to match memory instantiation

* yarn format docs

* Merging fixes to address ForkPR comments (langchain-ai#2)

* updating for pr

* Removed redis_memory in favor of chat memory

* Fixed tests and updated docs

* Bump docs

* lint results

* fixes from lint and format

---------

Co-authored-by: Chris Toomey <[email protected]>

* Update RedisChatMessageHistory class to initialize a client internally

* Update Redis chat message history docs to reflect the fact that Redis is a peer dependency, allow direct passing of Redis client

* Patch typing issues with passing node-redis client directly into a RedisChatMessageHistory instance

---------

Co-authored-by: Chris Toomey <[email protected]>
Co-authored-by: Jacob Lee <[email protected]>
  • Loading branch information
3 people authored May 11, 2023
1 parent dad9643 commit ace3ee7
Show file tree
Hide file tree
Showing 13 changed files with 386 additions and 2 deletions.
2 changes: 1 addition & 1 deletion docs/docs/modules/memory/examples/dynamodb.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ First, install the AWS DynamoDB client in your project:
npm install @aws-sdk/client-dynamodb
```

Next, sign into your AWS account and create a DynamoDB table. Name the table `langchain`, and name your partition key `id` and make sure it's a string. You can leave sort key and the other settings alone.
Next, sign into your AWS account and create a DynamoDB table. Name the table `langchain`, and name your partition key `id`. Make sure your partition key is a string. You can leave sort key and the other settings alone.

You'll also need to retrieve an AWS access key and secret key for a role or user that has access to the table and add them to your environment variables.

Expand Down
36 changes: 36 additions & 0 deletions docs/docs/modules/memory/examples/redis.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
hide_table_of_contents: true
---

import CodeBlock from "@theme/CodeBlock";

# Redis-Backed Chat Memory

For longer-term persistence across chat sessions, you can swap out the default in-memory `chatHistory` that backs chat memory classes like `BufferMemory` for a [Redis](https://redis.io/) instance.

## Setup

You will need to install [node-redis](https://github.com/redis/node-redis) in your project:

```bash npm2yarn
npm install redis
```

You will also need a Redis instance to connect to. See instructions on [the official Redis website](https://redis.io/docs/getting-started/) for running the server locally.

## Usage

Each chat history session stored in Redis must have a unique id. You can provide an optional `sessionTTL` to make sessions expire after a give number of seconds.
The `config` parameter is passed directly into the `createClient` method of [node-redis](https://github.com/redis/node-redis), and takes all the same arguments.

import Example from "@examples/memory/redis.ts";

<CodeBlock language="typescript">{Example}</CodeBlock>

## Advanced Usage

You can also directly pass in a previously created [node-redis](https://github.com/redis/node-redis) client instance:

import AdvancedExample from "@examples/memory/redis-advanced.ts";

<CodeBlock language="typescript">{AdvancedExample}</CodeBlock>
2 changes: 1 addition & 1 deletion docs/docs/modules/memory/examples/vector_store_memory.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ hide_table_of_contents: true
import CodeBlock from "@theme/CodeBlock";
import Example from "@examples/memory/vector_store.ts";

# VectorStore-backed Memory
# VectorStore-Backed Memory

`VectorStoreRetrieverMemory` stores memories in a VectorDB and queries the top-K most "salient" docs every time it is called.

Expand Down
1 change: 1 addition & 0 deletions examples/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
"ml-distance": "^4.0.0",
"mongodb": "^5.2.0",
"prisma": "^4.11.0",
"redis": "^4.6.6",
"sqlite3": "^5.1.4",
"typeorm": "^0.3.12",
"weaviate-ts-client": "^1.0.0",
Expand Down
45 changes: 45 additions & 0 deletions examples/src/memory/redis-advanced.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { createClient } from "redis";
import { BufferMemory } from "langchain/memory";
import { RedisChatMessageHistory } from "langchain/stores/message/redis";
import { ChatOpenAI } from "langchain/chat_models/openai";
import { ConversationChain } from "langchain/chains";

const client = createClient({
url: "redis://localhost:6379",
});

const memory = new BufferMemory({
chatHistory: new RedisChatMessageHistory({
sessionId: new Date().toISOString(),
sessionTTL: 300,
client,
}),
});

const model = new ChatOpenAI({
modelName: "gpt-3.5-turbo",
temperature: 0,
});

const chain = new ConversationChain({ llm: model, memory });

const res1 = await chain.call({ input: "Hi! I'm Jim." });
console.log({ res1 });
/*
{
res1: {
text: "Hello Jim! It's nice to meet you. My name is AI. How may I assist you today?"
}
}
*/

const res2 = await chain.call({ input: "What did I just say my name was?" });
console.log({ res2 });

/*
{
res1: {
text: "You said your name was Jim."
}
}
*/
42 changes: 42 additions & 0 deletions examples/src/memory/redis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import { BufferMemory } from "langchain/memory";
import { RedisChatMessageHistory } from "langchain/stores/message/redis";
import { ChatOpenAI } from "langchain/chat_models/openai";
import { ConversationChain } from "langchain/chains";

const memory = new BufferMemory({
chatHistory: new RedisChatMessageHistory({
sessionId: new Date().toISOString(), // Or some other unique identifier for the conversation
sessionTTL: 300, // 5 minutes, omit this parameter to make sessions never expire
config: {
url: "redis://localhost:6379", // Default value, override with your own instance's URL
},
}),
});

const model = new ChatOpenAI({
modelName: "gpt-3.5-turbo",
temperature: 0,
});

const chain = new ConversationChain({ llm: model, memory });

const res1 = await chain.call({ input: "Hi! I'm Jim." });
console.log({ res1 });
/*
{
res1: {
text: "Hello Jim! It's nice to meet you. My name is AI. How may I assist you today?"
}
}
*/

const res2 = await chain.call({ input: "What did I just say my name was?" });
console.log({ res2 });

/*
{
res1: {
text: "You said your name was Jim."
}
}
*/
3 changes: 3 additions & 0 deletions langchain/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -265,6 +265,9 @@ stores/file/node.d.ts
stores/message/dynamodb.cjs
stores/message/dynamodb.js
stores/message/dynamodb.d.ts
stores/message/redis.cjs
stores/message/redis.js
stores/message/redis.d.ts
experimental/autogpt.cjs
experimental/autogpt.js
experimental/autogpt.d.ts
Expand Down
8 changes: 8 additions & 0 deletions langchain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -277,6 +277,9 @@
"stores/message/dynamodb.cjs",
"stores/message/dynamodb.js",
"stores/message/dynamodb.d.ts",
"stores/message/redis.cjs",
"stores/message/redis.js",
"stores/message/redis.d.ts",
"experimental/autogpt.cjs",
"experimental/autogpt.js",
"experimental/autogpt.d.ts",
Expand Down Expand Up @@ -1007,6 +1010,11 @@
"import": "./stores/message/dynamodb.js",
"require": "./stores/message/dynamodb.cjs"
},
"./stores/message/redis": {
"types": "./stores/message/redis.d.ts",
"import": "./stores/message/redis.js",
"require": "./stores/message/redis.cjs"
},
"./experimental/autogpt": {
"types": "./experimental/autogpt.d.ts",
"import": "./experimental/autogpt.js",
Expand Down
2 changes: 2 additions & 0 deletions langchain/scripts/create-entrypoints.js
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ const entrypoints = {
"stores/file/in_memory": "stores/file/in_memory",
"stores/file/node": "stores/file/node",
"stores/message/dynamodb": "stores/message/dynamodb",
"stores/message/redis": "stores/message/redis",
// experimental
"experimental/autogpt": "experimental/autogpt/index",
"experimental/babyagi": "experimental/babyagi/index",
Expand Down Expand Up @@ -191,6 +192,7 @@ const requiresOptionalDependency = [
"cache/redis",
"stores/file/node",
"stores/message/dynamodb",
"stores/message/redis",
];

// List of test-exports-* packages which we use to test that the exports field
Expand Down
85 changes: 85 additions & 0 deletions langchain/src/stores/message/redis.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import {
createClient,
RedisClientOptions,
RedisClientType,
RedisModules,
RedisFunctions,
RedisScripts,
} from "redis";
import {
BaseChatMessage,
BaseListChatMessageHistory,
} from "../../schema/index.js";
import {
StoredMessage,
mapChatMessagesToStoredMessages,
mapStoredMessagesToChatMessages,
} from "./utils.js";

export type RedisChatMessageHistoryInput = {
sessionId: string;
sessionTTL?: number;
config?: RedisClientOptions;
// Typing issues with createClient output: https://github.com/redis/node-redis/issues/1865
// eslint-disable-next-line @typescript-eslint/no-explicit-any
client?: any;
};

export class RedisChatMessageHistory extends BaseListChatMessageHistory {
public client: RedisClientType<RedisModules, RedisFunctions, RedisScripts>;

private sessionId: string;

private sessionTTL?: number;

constructor(fields: RedisChatMessageHistoryInput) {
const { sessionId, sessionTTL, config, client } = fields;
super();
this.client = (client ?? createClient(config ?? {})) as RedisClientType<
RedisModules,
RedisFunctions,
RedisScripts
>;
this.sessionId = sessionId;
this.sessionTTL = sessionTTL;
}

async ensureReadiness() {
if (!this.client.isReady) {
await this.client.connect();
}
return true;
}

async getMessages(): Promise<BaseChatMessage[]> {
await this.ensureReadiness();
const rawStoredMessages = await this.client.lRange(this.sessionId, 0, -1);
const orderedMessages = rawStoredMessages
.reverse()
.map((message) => JSON.parse(message));
const previousMessages = orderedMessages
.map((item) => ({
type: item.type,
role: item.role,
text: item.text,
}))
.filter(
(x): x is StoredMessage => x.type !== undefined && x.text !== undefined
);
return mapStoredMessagesToChatMessages(previousMessages);
}

async addMessage(message: BaseChatMessage): Promise<void> {
await this.ensureReadiness();
const messageToAdd = mapChatMessagesToStoredMessages([message]);
await this.client.lPush(this.sessionId, JSON.stringify(messageToAdd[0]));
if (this.sessionTTL) {
await this.client.expire(this.sessionId, this.sessionTTL);
}
}

async clear(): Promise<void> {
await this.ensureReadiness();
await this.client.del(this.sessionId);
}
}
Loading

0 comments on commit ace3ee7

Please sign in to comment.