-
Notifications
You must be signed in to change notification settings - Fork 2.2k
/
firestore.ts
244 lines (219 loc) Β· 6.94 KB
/
firestore.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
import type { AppOptions } from "firebase-admin";
import { getApps, initializeApp } from "firebase-admin/app";
import {
getFirestore,
DocumentData,
Firestore,
DocumentReference,
FieldValue,
} from "firebase-admin/firestore";
import { BaseListChatMessageHistory } from "@langchain/core/chat_history";
import {
BaseMessage,
StoredMessage,
mapChatMessagesToStoredMessages,
mapStoredMessagesToChatMessages,
} from "@langchain/core/messages";
/**
* Interface for FirestoreDBChatMessageHistory. It includes the collection
* name, session ID, user ID, and optionally, the app index and
* configuration for the Firebase app.
*/
export interface FirestoreDBChatMessageHistory {
/**
* An array of collection names, should match the length of `docs` field.
* @TODO make required variable in 0.2
*/
collections?: string[];
/**
* An array of doc names, should match the length of `collections` field,
* or undefined if the collections field has a length of 1. In this case,
* it will default to use `sessionId` as the doc name.
* @TODO make required variable in 0.2
*/
docs?: string[];
/**
* @deprecated Will be removed in 0.2 use `collections` field instead.
*/
collectionName?: string;
sessionId: string;
userId: string;
appIdx?: number;
config?: AppOptions;
}
/**
* Class for managing chat message history using Google's Firestore as a
* storage backend. Extends the BaseListChatMessageHistory class.
* @example
* ```typescript
* const chatHistory = new FirestoreChatMessageHistory({
* collectionName: "langchain",
* sessionId: "lc-example",
* userId: "a@example.com",
* config: { projectId: "your-project-id" },
* });
*
* const chain = new ConversationChain({
* llm: new ChatOpenAI(),
* memory: new BufferMemory({ chatHistory }),
* });
*
* const response = await chain.invoke({
* input: "What did I just say my name was?",
* });
* console.log({ response });
* ```
*/
export class FirestoreChatMessageHistory extends BaseListChatMessageHistory {
lc_namespace = ["langchain", "stores", "message", "firestore"];
private collections: string[];
private docs: string[];
private sessionId: string;
private userId: string;
private appIdx: number;
private config: AppOptions;
private firestoreClient: Firestore;
private document: DocumentReference<DocumentData> | null;
constructor({
collectionName,
collections,
docs,
sessionId,
userId,
appIdx = 0,
config,
}: FirestoreDBChatMessageHistory) {
super();
if (collectionName && collections) {
throw new Error(
"Can not pass in collectionName and collections. Please use collections only."
);
}
if (!collectionName && !collections) {
throw new Error(
"Must pass in a list of collections. Fields `collectionName` and `collections` are both undefined."
);
}
if (collections || docs) {
// This checks that the 'collections' and 'docs' arrays have the same length,
// which means each collection has a corresponding document name. The only exception allowed is
// when there is exactly one collection provided and 'docs' is not defined. In this case, it is
// assumed that the 'sessionId' will be used as the document name for that single collection.
if (
!(
collections?.length === docs?.length ||
(collections?.length === 1 && !docs)
)
) {
throw new Error(
"Collections and docs options must have the same length, or collections must have a length of 1 if docs is not defined."
);
}
}
this.collections = collections || ([collectionName] as string[]);
this.docs = docs || ([sessionId] as string[]);
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.collections.reduce<DocumentReference<DocumentData>>(
(acc, collection, index) =>
acc.collection(collection).doc(this.docs[index]),
this.firestoreClient as unknown as DocumentReference<DocumentData>
);
}
/**
* Method to retrieve all messages from the Firestore collection
* associated with the current session. Returns an array of BaseMessage
* objects.
* @returns Array of stored messages
*/
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);
}
/**
* Method to add a new message to the Firestore collection. The message is
* passed as a BaseMessage object.
* @param message The message to be added as a BaseMessage object.
*/
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()}`);
});
}
/**
* Method to delete all messages from the Firestore collection associated
* with the current session.
*/
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()}`);
});
}
}