Skip to content

Commit

Permalink
Add firestore chat message history (langchain-ai#1983)
Browse files Browse the repository at this point in the history
* Add firestore

* Update tests

* Add firestore docs

* Remove redundant this.messages

* Readd package.json, cleanup

* Narrow import from firebase-admin

* Update docs with suggestive firestore rules

* Fix formatting

* Fix yarn link

---------

Co-authored-by: jacoblee93 <[email protected]>
  • Loading branch information
qalqi and jacoblee93 authored Jul 18, 2023
1 parent ffa24ae commit a121849
Show file tree
Hide file tree
Showing 12 changed files with 680 additions and 3 deletions.
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,6 @@ Chinook_Sqlite.sql
*.swo

.node-version

firebase-debug.log
firestore-debug.log
43 changes: 43 additions & 0 deletions docs/docs/modules/memory/examples/firestore.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
---
hide_table_of_contents: true
---

import CodeBlock from "@theme/CodeBlock";

# Firestore 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 firestore.

## Setup

First, install the Firebase admin package in your project:

```bash npm2yarn
yarn add firebase-admin
```

Go to your the Settings icon Project settings in the Firebase console.
In the Your apps card, select the nickname of the app for which you need a config object.
Select Config from the Firebase SDK snippet pane.
Copy the config object snippet, then add it to your firebase functions FirestoreChatMessageHistory.

## Usage

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

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

## Firestore Rules

If your collection name is "chathistory," you can configure Firestore rules as follows.

```
match /chathistory/{sessionId} {
allow read: if request.auth.uid == resource.data.createdBy;
allow write: if request.auth.uid == request.resource.data.createdBy;
}
match /chathistory/{sessionId}/messages/{messageId} {
allow read: if request.auth.uid == resource.data.createdBy;
allow write: if request.auth.uid == request.resource.data.createdBy;
}
```
37 changes: 37 additions & 0 deletions examples/src/memory/firestore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { BufferMemory } from "langchain/memory";
import { FirestoreChatMessageHistory } from "langchain/stores/message/firestore";
import { ChatOpenAI } from "langchain/chat_models/openai";
import { ConversationChain } from "langchain/chains";

const memory = new BufferMemory({
chatHistory: new FirestoreChatMessageHistory({
collectionName: "langchain",
sessionId: "lc-example",
userId: "[email protected]",
config: { projectId: "your-project-id" },
}),
});

const model = new ChatOpenAI();
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 @@ -424,6 +424,9 @@ stores/message/in_memory.d.ts
stores/message/dynamodb.cjs
stores/message/dynamodb.js
stores/message/dynamodb.d.ts
stores/message/firestore.cjs
stores/message/firestore.js
stores/message/firestore.d.ts
stores/message/momento.cjs
stores/message/momento.js
stores/message/momento.d.ts
Expand Down
13 changes: 13 additions & 0 deletions langchain/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -436,6 +436,9 @@
"stores/message/dynamodb.cjs",
"stores/message/dynamodb.js",
"stores/message/dynamodb.d.ts",
"stores/message/firestore.cjs",
"stores/message/firestore.js",
"stores/message/firestore.d.ts",
"stores/message/momento.cjs",
"stores/message/momento.js",
"stores/message/momento.d.ts",
Expand Down Expand Up @@ -548,6 +551,7 @@
"eslint-plugin-no-instanceof": "^1.0.1",
"eslint-plugin-prettier": "^4.2.1",
"faiss-node": "^0.2.1",
"firebase-admin": "^11.9.0",
"google-auth-library": "^8.9.0",
"graphql": "^16.6.0",
"hnswlib-node": "^1.4.2",
Expand Down Expand Up @@ -615,6 +619,7 @@
"d3-dsv": "^2.0.0",
"epub2": "^3.0.1",
"faiss-node": "^0.2.1",
"firebase-admin": "^11.9.0",
"google-auth-library": "^8.9.0",
"hnswlib-node": "^1.4.2",
"html-to-text": "^9.0.5",
Expand Down Expand Up @@ -739,6 +744,9 @@
"faiss-node": {
"optional": true
},
"firebase-admin": {
"optional": true
},
"google-auth-library": {
"optional": true
},
Expand Down Expand Up @@ -1577,6 +1585,11 @@
"import": "./stores/message/dynamodb.js",
"require": "./stores/message/dynamodb.cjs"
},
"./stores/message/firestore": {
"types": "./stores/message/firestore.d.ts",
"import": "./stores/message/firestore.js",
"require": "./stores/message/firestore.cjs"
},
"./stores/message/momento": {
"types": "./stores/message/momento.d.ts",
"import": "./stores/message/momento.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 @@ -173,6 +173,7 @@ const entrypoints = {
"stores/file/node": "stores/file/node",
"stores/message/in_memory": "stores/message/in_memory",
"stores/message/dynamodb": "stores/message/dynamodb",
"stores/message/firestore": "stores/message/firestore",
"stores/message/momento": "stores/message/momento",
"stores/message/redis": "stores/message/redis",
"stores/message/ioredis": "stores/message/ioredis",
Expand Down Expand Up @@ -291,6 +292,7 @@ const requiresOptionalDependency = [
"stores/doc/gcs",
"stores/file/node",
"stores/message/dynamodb",
"stores/message/firestore",
"stores/message/momento",
"stores/message/redis",
"stores/message/ioredis",
Expand Down
1 change: 1 addition & 0 deletions langchain/src/load/import_constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ export const optionalImportEntrypoints = [
"langchain/stores/doc/gcs",
"langchain/stores/file/node",
"langchain/stores/message/dynamodb",
"langchain/stores/message/firestore",
"langchain/stores/message/momento",
"langchain/stores/message/redis",
"langchain/stores/message/ioredis",
Expand Down
3 changes: 3 additions & 0 deletions langchain/src/load/import_type.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,9 @@ export interface OptionalImportMap {
"langchain/stores/message/dynamodb"?:
| typeof import("../stores/message/dynamodb.js")
| Promise<typeof import("../stores/message/dynamodb.js")>;
"langchain/stores/message/firestore"?:
| typeof import("../stores/message/firestore.js")
| Promise<typeof import("../stores/message/firestore.js")>;
"langchain/stores/message/momento"?:
| typeof import("../stores/message/momento.js")
| Promise<typeof import("../stores/message/momento.js")>;
Expand Down
152 changes: 152 additions & 0 deletions langchain/src/stores/message/firestore.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
import type { AppOptions } from "firebase-admin";
import { getApps, initializeApp } from "firebase-admin/app";
import {
getFirestore,
DocumentData,
Firestore,
DocumentReference,
FieldValue,
} from "firebase-admin/firestore";

import {
StoredMessage,
BaseMessage,
BaseListChatMessageHistory,
} from "../../schema/index.js";
import {
mapChatMessagesToStoredMessages,
mapStoredMessagesToChatMessages,
} from "./utils.js";

export interface FirestoreDBChatMessageHistory {
collectionName: string;
sessionId: string;
userId: string;
appIdx?: number;
config?: AppOptions;
}
export class FirestoreChatMessageHistory extends BaseListChatMessageHistory {
lc_namespace = ["langchain", "stores", "message", "firestore"];

private collectionName: string;

private sessionId: string;

private userId: string;

private appIdx: number;

private config: AppOptions;

private firestoreClient: Firestore;

private document: DocumentReference<DocumentData> | null;

constructor({
collectionName,
sessionId,
userId,
appIdx = 0,
config,
}: FirestoreDBChatMessageHistory) {
super();
this.collectionName = collectionName;
this.sessionId = sessionId;
this.userId = userId;
this.document = null;
this.appIdx = appIdx;
if (config) this.config = config;

try {
this.ensureFirestore();
} catch (error) {
throw new Error(`Unknown response type`);
}
}

private ensureFirestore(): void {
let app;
// Check if the app is already initialized else get appIdx
if (!getApps().length) app = initializeApp(this.config);
else app = getApps()[this.appIdx];

this.firestoreClient = getFirestore(app);

this.document = this.firestoreClient
.collection(this.collectionName)
.doc(this.sessionId);
}

async getMessages(): Promise<BaseMessage[]> {
if (!this.document) {
throw new Error("Document not initialized");
}

const querySnapshot = await this.document
.collection("messages")
.orderBy("createdAt", "asc")
.get()
.catch((err) => {
throw new Error(`Unknown response type: ${err.toString()}`);
});

const response: StoredMessage[] = [];
querySnapshot.forEach((doc) => {
const { type, data } = doc.data();
response.push({ type, data });
});

return mapStoredMessagesToChatMessages(response);
}

public async addMessage(message: BaseMessage) {
const messages = mapChatMessagesToStoredMessages([message]);
await this.upsertMessage(messages[0]);
}

private async upsertMessage(message: StoredMessage): Promise<void> {
if (!this.document) {
throw new Error("Document not initialized");
}
await this.document.set(
{
id: this.sessionId,
user_id: this.userId,
},
{ merge: true }
);
await this.document
.collection("messages")
.add({
type: message.type,
data: message.data,
createdBy: this.userId,
createdAt: FieldValue.serverTimestamp(),
})
.catch((err) => {
throw new Error(`Unknown response type: ${err.toString()}`);
});
}

public async clear(): Promise<void> {
if (!this.document) {
throw new Error("Document not initialized");
}
await this.document
.collection("messages")
.get()
.then((querySnapshot) => {
querySnapshot.docs.forEach((snapshot) => {
snapshot.ref.delete().catch((err) => {
throw new Error(`Unknown response type: ${err.toString()}`);
});
});
})
.catch((err) => {
throw new Error(`Unknown response type: ${err.toString()}`);
});
await this.document.delete().catch((err) => {
throw new Error(`Unknown response type: ${err.toString()}`);
});
}
}
Loading

0 comments on commit a121849

Please sign in to comment.