Path: blob/master/src/packages/frontend/client/llm.ts
1503 views
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45import { delay } from "awaiting";6import { EventEmitter } from "events";7import { redux } from "@cocalc/frontend/app-framework";8import {9LanguageModel,10LanguageServiceCore,11getSystemPrompt,12isFreeModel,13model2service,14} from "@cocalc/util/db-schema/llm-utils";15import type { WebappClient } from "./client";16import type { History } from "./types";17import {18LOCALIZATIONS,19OTHER_SETTINGS_LOCALE_KEY,20OTHER_SETTINGS_REPLY_ENGLISH_KEY,21} from "@cocalc/util/i18n/const";22import { sanitizeLocale } from "@cocalc/frontend/i18n";2324interface QueryLLMProps {25input: string;26model: LanguageModel;27system?: string;28history?: History;29project_id?: string;30path?: string;31chatStream?: ChatStream; // if given, uses chat stream32tag?: string;33startStreamExplicitly?: boolean;34}3536export class LLMClient {37private client: WebappClient;3839constructor(client: WebappClient) {40this.client = client;41}4243public async query(opts: QueryLLMProps): Promise<string> {44return await this.queryLanguageModel(opts);45}4647// ATTN/TODO: startExplicitly seems to be broken48public queryStream(opts, startExplicitly = false): ChatStream {49const chatStream = new ChatStream();50(async () => {51try {52await this.queryLanguageModel({ ...opts, chatStream });53if (!startExplicitly) {54chatStream.emit("start");55}56} catch (err) {57chatStream.emit("error", err);58}59})();60return chatStream;61}6263private async queryLanguageModel({64input,65model,66system, // if not set, a default system prompt is used – disable by setting to ""67history,68project_id,69path,70chatStream,71tag = "",72}: QueryLLMProps): Promise<string> {73system ??= getSystemPrompt(model, path);7475// remove all date entries from all history objects76if (history != null) {77for (const h of history) {78delete h.date;79}80}8182if (!redux.getStore("projects").hasLanguageModelEnabled(project_id, tag)) {83throw new Error(84`Language model support is not currently enabled ${85project_id ? "in this project" : "on this server"86}. [tag=${tag}]`,87);88}8990input = input.trim();91if (chatStream == null) {92if (!input || input == "test") {93return "Great! What can I assist you with today?";94}95if (input == "ping") {96await delay(1000);97return "Pong";98}99}100101// append a sentence to request to translate the output to the user's language – unless disabled102const other_settings = redux.getStore("account").get("other_settings");103const alwaysEnglish = !!other_settings.get(104OTHER_SETTINGS_REPLY_ENGLISH_KEY,105);106const locale = sanitizeLocale(107other_settings.get(OTHER_SETTINGS_LOCALE_KEY),108);109if (!alwaysEnglish && locale != "en") {110const lang = LOCALIZATIONS[locale].name; // name is always in english111system = `${system}\n\nYour answer must be written in the language ${lang}.`;112}113114const is_cocalc_com = redux.getStore("customize").get("is_cocalc_com");115116if (!isFreeModel(model, is_cocalc_com)) {117// Ollama and others are treated as "free"118const service = model2service(model) as LanguageServiceCore;119// when client gets non-free openai model request, check if allowed. If not, show quota modal.120const { allowed, reason } =121await this.client.purchases_client.isPurchaseAllowed(service);122123if (!allowed) {124await this.client.purchases_client.quotaModal({125service,126reason,127allowed,128});129}130// Now check again after modal dismissed...131const x = await this.client.purchases_client.isPurchaseAllowed(service);132if (!x.allowed) {133throw Error(reason);134}135}136137// do not import until needed -- it is HUGE!138const {139numTokensUpperBound,140truncateHistory,141truncateMessage,142getMaxTokens,143} = await import("@cocalc/frontend/misc/llm");144145// We always leave some room for output:146const maxTokens = getMaxTokens(model) - 1000;147input = truncateMessage(input, maxTokens);148const n = numTokensUpperBound(input, getMaxTokens(model));149if (n >= maxTokens) {150history = undefined;151} else if (history != null) {152history = truncateHistory(history, maxTokens - n, model);153}154// console.log("chatgpt", { input, system, history, project_id, path });155const options = {156input,157system,158project_id,159path,160history,161model,162tag: `app:${tag}`,163};164165if (chatStream == null) {166// not streaming167return await this.client.conat_client.llm(options);168}169170chatStream.once("start", async () => {171// streaming version172try {173await this.client.conat_client.llm({174...options,175stream: chatStream.process,176});177} catch (err) {178chatStream.error(err);179}180});181182return "see stream for output";183}184}185186class ChatStream extends EventEmitter {187constructor() {188super();189}190191process = (text: string | null) => {192// emits undefined text when done (or err below)193this.emit("token", text);194};195196error = (err) => {197this.emit("error", err);198};199}200201export type { ChatStream };202203204