forked from langchain-ai/langchainjs
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add firestore chat message history (langchain-ai#1983)
* 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
1 parent
ffa24ae
commit a121849
Showing
12 changed files
with
680 additions
and
3 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -28,3 +28,6 @@ Chinook_Sqlite.sql | |
*.swo | ||
|
||
.node-version | ||
|
||
firebase-debug.log | ||
firestore-debug.log |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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." | ||
} | ||
} | ||
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()}`); | ||
}); | ||
} | ||
} |
Oops, something went wrong.