agsamantha/node_modules/@langchain/community/dist/chat_models/baiduwenxin.js
2024-10-02 15:15:21 -05:00

526 lines
19 KiB
JavaScript

import { BaseChatModel, } from "@langchain/core/language_models/chat_models";
import { AIMessage, AIMessageChunk, ChatMessage, } from "@langchain/core/messages";
import { ChatGenerationChunk, } from "@langchain/core/outputs";
import { getEnvironmentVariable } from "@langchain/core/utils/env";
import { convertEventStreamToIterableReadableDataStream } from "../utils/event_source_parse.js";
/**
* Function that extracts the custom role of a generic chat message.
* @param message Chat message from which to extract the custom role.
* @returns The custom role of the chat message.
*/
function extractGenericMessageCustomRole(message) {
if (message.role !== "assistant" && message.role !== "user") {
console.warn(`Unknown message role: ${message.role}`);
}
return message.role;
}
/**
* Function that converts a base message to a Wenxin message role.
* @param message Base message to convert.
* @returns The Wenxin message role.
*/
function messageToWenxinRole(message) {
const type = message._getType();
switch (type) {
case "ai":
return "assistant";
case "human":
return "user";
case "system":
throw new Error("System messages should not be here");
case "function":
throw new Error("Function messages not supported");
case "generic": {
if (!ChatMessage.isInstance(message))
throw new Error("Invalid generic chat message");
return extractGenericMessageCustomRole(message);
}
default:
throw new Error(`Unknown message type: ${type}`);
}
}
/**
* @deprecated Install and import from @langchain/baidu-qianfan instead.
* Wrapper around Baidu ERNIE large language models that use the Chat endpoint.
*
* To use you should have the `BAIDU_API_KEY` and `BAIDU_SECRET_KEY`
* environment variable set.
*
* @augments BaseLLM
* @augments BaiduERNIEInput
* @example
* ```typescript
* const ernieTurbo = new ChatBaiduWenxin({
* apiKey: "YOUR-API-KEY",
* baiduSecretKey: "YOUR-SECRET-KEY",
* });
*
* const ernie = new ChatBaiduWenxin({
* model: "ERNIE-Bot",
* temperature: 1,
* apiKey: "YOUR-API-KEY",
* baiduSecretKey: "YOUR-SECRET-KEY",
* });
*
* const messages = [new HumanMessage("Hello")];
*
* let res = await ernieTurbo.call(messages);
*
* res = await ernie.call(messages);
* ```
*/
export class ChatBaiduWenxin extends BaseChatModel {
static lc_name() {
return "ChatBaiduWenxin";
}
get callKeys() {
return ["stop", "signal", "options"];
}
get lc_secrets() {
return {
baiduApiKey: "BAIDU_API_KEY",
apiKey: "BAIDU_API_KEY",
baiduSecretKey: "BAIDU_SECRET_KEY",
};
}
get lc_aliases() {
return undefined;
}
constructor(fields) {
super(fields ?? {});
Object.defineProperty(this, "lc_serializable", {
enumerable: true,
configurable: true,
writable: true,
value: true
});
Object.defineProperty(this, "baiduApiKey", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "apiKey", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "baiduSecretKey", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "accessToken", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "streaming", {
enumerable: true,
configurable: true,
writable: true,
value: false
});
Object.defineProperty(this, "prefixMessages", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "userId", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "modelName", {
enumerable: true,
configurable: true,
writable: true,
value: "ERNIE-Bot-turbo"
});
Object.defineProperty(this, "model", {
enumerable: true,
configurable: true,
writable: true,
value: "ERNIE-Bot-turbo"
});
Object.defineProperty(this, "apiUrl", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "temperature", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "topP", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
Object.defineProperty(this, "penaltyScore", {
enumerable: true,
configurable: true,
writable: true,
value: void 0
});
this.baiduApiKey =
fields?.apiKey ??
fields?.baiduApiKey ??
getEnvironmentVariable("BAIDU_API_KEY");
if (!this.baiduApiKey) {
throw new Error("Baidu API key not found");
}
this.apiKey = this.baiduApiKey;
this.baiduSecretKey =
fields?.baiduSecretKey ?? getEnvironmentVariable("BAIDU_SECRET_KEY");
if (!this.baiduSecretKey) {
throw new Error("Baidu Secret key not found");
}
this.streaming = fields?.streaming ?? this.streaming;
this.prefixMessages = fields?.prefixMessages ?? this.prefixMessages;
this.userId = fields?.userId ?? this.userId;
this.temperature = fields?.temperature ?? this.temperature;
this.topP = fields?.topP ?? this.topP;
this.penaltyScore = fields?.penaltyScore ?? this.penaltyScore;
this.modelName = fields?.model ?? fields?.modelName ?? this.model;
this.model = this.modelName;
const models = {
"ERNIE-Bot": "completions",
"ERNIE-Bot-turbo": "eb-instant",
"ERNIE-Bot-4": "completions_pro",
"ERNIE-Speed-8K": "ernie_speed",
"ERNIE-Speed-128K": "ernie-speed-128k",
"ERNIE-4.0-8K": "completions_pro",
"ERNIE-4.0-8K-Preview": "ernie-4.0-8k-preview",
"ERNIE-3.5-8K": "completions",
"ERNIE-3.5-8K-Preview": "ernie-3.5-8k-preview",
"ERNIE-Lite-8K": "eb-instant",
"ERNIE-Tiny-8K": "ernie-tiny-8k",
"ERNIE-Character-8K": "ernie-char-8k",
"ERNIE Speed-AppBuilder": "ai_apaas",
};
if (this.model in models) {
this.apiUrl = `https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat/${models[this.model]}`;
}
else {
throw new Error(`Invalid model name: ${this.model}`);
}
}
/**
* Method that retrieves the access token for making requests to the Baidu
* API.
* @param options Optional parsed call options.
* @returns The access token for making requests to the Baidu API.
*/
async getAccessToken(options) {
const url = `https://aip.baidubce.com/oauth/2.0/token?grant_type=client_credentials&client_id=${this.apiKey}&client_secret=${this.baiduSecretKey}`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
Accept: "application/json",
},
signal: options?.signal,
});
if (!response.ok) {
const text = await response.text();
const error = new Error(`Baidu get access token failed with status code ${response.status}, response: ${text}`);
// eslint-disable-next-line @typescript-eslint/no-explicit-any
error.response = response;
throw error;
}
const json = await response.json();
return json.access_token;
}
/**
* Get the parameters used to invoke the model
*/
invocationParams() {
return {
stream: this.streaming,
user_id: this.userId,
temperature: this.temperature,
top_p: this.topP,
penalty_score: this.penaltyScore,
};
}
/**
* Get the identifying parameters for the model
*/
identifyingParams() {
return {
model_name: this.model,
...this.invocationParams(),
};
}
_ensureMessages(messages) {
return messages.map((message) => ({
role: messageToWenxinRole(message),
content: message.text,
}));
}
/** @ignore */
async _generate(messages, options, runManager) {
const tokenUsage = {};
const params = this.invocationParams();
// Wenxin requires the system message to be put in the params, not messages array
const systemMessage = messages.find((message) => message._getType() === "system");
if (systemMessage) {
// eslint-disable-next-line no-param-reassign
messages = messages.filter((message) => message !== systemMessage);
params.system = systemMessage.text;
}
const messagesMapped = this._ensureMessages(messages);
const data = params.stream
? await new Promise((resolve, reject) => {
let response;
let rejected = false;
let resolved = false;
this.completionWithRetry({
...params,
messages: messagesMapped,
}, true, options?.signal, (event) => {
const data = JSON.parse(event.data);
if (data?.error_code) {
if (rejected) {
return;
}
rejected = true;
reject(new Error(data?.error_msg));
return;
}
const message = data;
// on the first message set the response properties
if (!response) {
response = {
id: message.id,
object: message.object,
created: message.created,
result: message.result,
need_clear_history: message.need_clear_history,
usage: message.usage,
};
}
else {
response.result += message.result;
response.created = message.created;
response.need_clear_history = message.need_clear_history;
response.usage = message.usage;
}
// TODO this should pass part.index to the callback
// when that's supported there
// eslint-disable-next-line no-void
void runManager?.handleLLMNewToken(message.result ?? "");
if (message.is_end) {
if (resolved || rejected) {
return;
}
resolved = true;
resolve(response);
}
}).catch((error) => {
if (!rejected) {
rejected = true;
reject(error);
}
});
})
: await this.completionWithRetry({
...params,
messages: messagesMapped,
}, false, options?.signal).then((data) => {
if (data?.error_code) {
throw new Error(data?.error_msg);
}
return data;
});
const { completion_tokens: completionTokens, prompt_tokens: promptTokens, total_tokens: totalTokens, } = data.usage ?? {};
if (completionTokens) {
tokenUsage.completionTokens =
(tokenUsage.completionTokens ?? 0) + completionTokens;
}
if (promptTokens) {
tokenUsage.promptTokens = (tokenUsage.promptTokens ?? 0) + promptTokens;
}
if (totalTokens) {
tokenUsage.totalTokens = (tokenUsage.totalTokens ?? 0) + totalTokens;
}
const generations = [];
const text = data.result ?? "";
generations.push({
text,
message: new AIMessage(text),
});
return {
generations,
llmOutput: { tokenUsage },
};
}
/** @ignore */
async completionWithRetry(request, stream, signal, onmessage) {
// The first run will get the accessToken
if (!this.accessToken) {
this.accessToken = await this.getAccessToken();
}
const findFirstNewlineIndex = (data) => {
for (let i = 0; i < data.length;) {
if (data[i] === 10)
return i;
if ((data[i] & 0b11100000) === 0b11000000) {
i += 2;
}
else if ((data[i] & 0b11110000) === 0b11100000) {
i += 3;
}
else if ((data[i] & 0b11111000) === 0b11110000) {
i += 4;
}
else {
i += 1;
}
}
return -1;
};
const makeCompletionRequest = async () => {
const url = `${this.apiUrl}?access_token=${this.accessToken}`;
const response = await fetch(url, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(request),
signal,
});
if (!stream) {
return response.json();
}
else {
if (response.body) {
// response will not be a stream if an error occurred
if (!response.headers
.get("content-type")
?.startsWith("text/event-stream")) {
onmessage?.(new MessageEvent("message", {
data: await response.text(),
}));
return;
}
const reader = response.body.getReader();
const decoder = new TextDecoder("utf-8");
let dataArrayBuffer = new Uint8Array(0);
let continueReading = true;
while (continueReading) {
const { done, value } = await reader.read();
if (done) {
continueReading = false;
break;
}
// merge the data first then decode in case of the Chinese characters are split between chunks
const mergedArray = new Uint8Array(dataArrayBuffer.length + value.length);
mergedArray.set(dataArrayBuffer);
mergedArray.set(value, dataArrayBuffer.length);
dataArrayBuffer = mergedArray;
let continueProcessing = true;
while (continueProcessing) {
const newlineIndex = findFirstNewlineIndex(dataArrayBuffer);
if (newlineIndex === -1) {
continueProcessing = false;
break;
}
const lineArrayBuffer = dataArrayBuffer.slice(0, findFirstNewlineIndex(dataArrayBuffer));
const line = decoder.decode(lineArrayBuffer);
dataArrayBuffer = dataArrayBuffer.slice(findFirstNewlineIndex(dataArrayBuffer) + 1);
if (line.startsWith("data:")) {
const event = new MessageEvent("message", {
data: line.slice("data:".length).trim(),
});
onmessage?.(event);
}
}
}
}
}
};
return this.caller.call(makeCompletionRequest);
}
async getFullApiUrl() {
if (!this.accessToken) {
this.accessToken = await this.getAccessToken();
}
return `${this.apiUrl}?access_token=${this.accessToken}`;
}
async createWenxinStream(request, signal) {
const url = await this.getFullApiUrl();
const response = await fetch(url, {
method: "POST",
headers: {
Accept: "text/event-stream",
"Content-Type": "application/json",
},
body: JSON.stringify(request),
signal,
});
if (!response.body) {
throw new Error("Could not begin Wenxin stream. Please check the given URL and try again.");
}
return convertEventStreamToIterableReadableDataStream(response.body);
}
_deserialize(json) {
try {
return JSON.parse(json);
}
catch (e) {
console.warn(`Received a non-JSON parseable chunk: ${json}`);
}
}
async *_streamResponseChunks(messages, options, runManager) {
const parameters = {
...this.invocationParams(),
stream: true,
};
// Wenxin requires the system message to be put in the params, not messages array
const systemMessage = messages.find((message) => message._getType() === "system");
if (systemMessage) {
// eslint-disable-next-line no-param-reassign
messages = messages.filter((message) => message !== systemMessage);
parameters.system = systemMessage.text;
}
const messagesMapped = this._ensureMessages(messages);
const stream = await this.caller.call(async () => this.createWenxinStream({
...parameters,
messages: messagesMapped,
}, options?.signal));
for await (const chunk of stream) {
const deserializedChunk = this._deserialize(chunk);
const { result, is_end, id } = deserializedChunk;
yield new ChatGenerationChunk({
text: result,
message: new AIMessageChunk({ content: result }),
generationInfo: is_end
? {
is_end,
request_id: id,
usage: chunk.usage,
}
: undefined,
});
await runManager?.handleLLMNewToken(result);
}
}
_llmType() {
return "baiduwenxin";
}
/** @ignore */
_combineLLMOutput() {
return [];
}
}