/** * @license * Copyright 2024 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ import { Content, GenerateContentRequest, GenerateContentResult, GenerateContentStreamResult, Part, RequestOptions, StartChatParams, } from "../../types"; import { formatNewContent } from "../requests/request-helpers"; import { formatBlockErrorMessage } from "../requests/response-helpers"; import { validateChatHistory } from "./chat-session-helpers"; import { generateContent, generateContentStream } from "./generate-content"; /** * Do not log a message for this error. */ const SILENT_ERROR = "SILENT_ERROR"; /** * ChatSession class that enables sending chat messages and stores * history of sent and received messages so far. * * @public */ export class ChatSession { private _apiKey: string; private _history: Content[] = []; private _sendPromise: Promise = Promise.resolve(); constructor( apiKey: string, public model: string, public params?: StartChatParams, public requestOptions?: RequestOptions, ) { this._apiKey = apiKey; if (params?.history) { validateChatHistory(params.history); this._history = params.history; } } /** * Gets the chat history so far. Blocked prompts are not added to history. * Blocked candidates are not added to history, nor are the prompts that * generated them. */ async getHistory(): Promise { await this._sendPromise; return this._history; } /** * Sends a chat message and receives a non-streaming * {@link GenerateContentResult} */ async sendMessage( request: string | Array, ): Promise { await this._sendPromise; const newContent = formatNewContent(request); const generateContentRequest: GenerateContentRequest = { safetySettings: this.params?.safetySettings, generationConfig: this.params?.generationConfig, tools: this.params?.tools, toolConfig: this.params?.toolConfig, systemInstruction: this.params?.systemInstruction, cachedContent: this.params?.cachedContent, contents: [...this._history, newContent], }; let finalResult; // Add onto the chain. this._sendPromise = this._sendPromise .then(() => generateContent( this._apiKey, this.model, generateContentRequest, this.requestOptions, ), ) .then((result) => { if ( result.response.candidates && result.response.candidates.length > 0 ) { this._history.push(newContent); const responseContent: Content = { parts: [], // Response seems to come back without a role set. role: "model", ...result.response.candidates?.[0].content, }; this._history.push(responseContent); } else { const blockErrorMessage = formatBlockErrorMessage(result.response); if (blockErrorMessage) { console.warn( `sendMessage() was unsuccessful. ${blockErrorMessage}. Inspect response object for details.`, ); } } finalResult = result; }); await this._sendPromise; return finalResult; } /** * Sends a chat message and receives the response as a * {@link GenerateContentStreamResult} containing an iterable stream * and a response promise. */ async sendMessageStream( request: string | Array, ): Promise { await this._sendPromise; const newContent = formatNewContent(request); const generateContentRequest: GenerateContentRequest = { safetySettings: this.params?.safetySettings, generationConfig: this.params?.generationConfig, tools: this.params?.tools, toolConfig: this.params?.toolConfig, systemInstruction: this.params?.systemInstruction, cachedContent: this.params?.cachedContent, contents: [...this._history, newContent], }; const streamPromise = generateContentStream( this._apiKey, this.model, generateContentRequest, this.requestOptions, ); // Add onto the chain. this._sendPromise = this._sendPromise .then(() => streamPromise) // This must be handled to avoid unhandled rejection, but jump // to the final catch block with a label to not log this error. .catch((_ignored) => { throw new Error(SILENT_ERROR); }) .then((streamResult) => streamResult.response) .then((response) => { if (response.candidates && response.candidates.length > 0) { this._history.push(newContent); const responseContent = { ...response.candidates[0].content }; // Response seems to come back without a role set. if (!responseContent.role) { responseContent.role = "model"; } this._history.push(responseContent); } else { const blockErrorMessage = formatBlockErrorMessage(response); if (blockErrorMessage) { console.warn( `sendMessageStream() was unsuccessful. ${blockErrorMessage}. Inspect response object for details.`, ); } } }) .catch((e) => { // Errors in streamPromise are already catchable by the user as // streamPromise is returned. // Avoid duplicating the error message in logs. if (e.message !== SILENT_ERROR) { // Users do not have access to _sendPromise to catch errors // downstream from streamPromise, so they should not throw. console.error(e); } }); return streamPromise; } }