diff --git a/bun.lock b/bun.lock index 2c2e29e..b067712 100644 --- a/bun.lock +++ b/bun.lock @@ -7,6 +7,7 @@ "dependencies": { "@google/genai": "^1.35.0", "@libsql/client": "^0.17.0", + "@mistralai/mistralai": "^1.11.0", "@napi-rs/canvas": "^0.1.88", "axios": "^1.13.2", "dotenv": "^17.2.3", @@ -233,6 +234,8 @@ "@libsql/win32-x64-msvc": ["@libsql/win32-x64-msvc@0.5.22", "", { "os": "win32", "cpu": "x64" }, "sha512-Fj0j8RnBpo43tVZUVoNK6BV/9AtDUM5S7DF3LB4qTYg1LMSZqi3yeCneUTLJD6XomQJlZzbI4mst89yspVSAnA=="], + "@mistralai/mistralai": ["@mistralai/mistralai@1.11.0", "", { "dependencies": { "zod": "^3.20.0", "zod-to-json-schema": "^3.24.1" } }, "sha512-6/BVj2mcaggYbpMzNSxtqtM2Tv/Jb5845XFd2CMYFO+O5VBkX70iLjtkBBTI4JFhh1l9vTCIMYXBVOjLoBVHGQ=="], + "@napi-rs/canvas": ["@napi-rs/canvas@0.1.88", "", { "optionalDependencies": { "@napi-rs/canvas-android-arm64": "0.1.88", "@napi-rs/canvas-darwin-arm64": "0.1.88", "@napi-rs/canvas-darwin-x64": "0.1.88", "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.88", "@napi-rs/canvas-linux-arm64-gnu": "0.1.88", "@napi-rs/canvas-linux-arm64-musl": "0.1.88", "@napi-rs/canvas-linux-riscv64-gnu": "0.1.88", "@napi-rs/canvas-linux-x64-gnu": "0.1.88", "@napi-rs/canvas-linux-x64-musl": "0.1.88", "@napi-rs/canvas-win32-arm64-msvc": "0.1.88", "@napi-rs/canvas-win32-x64-msvc": "0.1.88" } }, "sha512-/p08f93LEbsL5mDZFQ3DBxcPv/I4QG9EDYRRq1WNlCOXVfAHBTHMSVMwxlqG/AtnSfUr9+vgfN7MKiyDo0+Weg=="], "@napi-rs/canvas-android-arm64": ["@napi-rs/canvas-android-arm64@0.1.88", "", { "os": "android", "cpu": "arm64" }, "sha512-KEaClPnZuVxJ8smUWjV1wWFkByBO/D+vy4lN+Dm5DFH514oqwukxKGeck9xcKJhaWJGjfruGmYGiwRe//+/zQQ=="], @@ -753,6 +756,10 @@ "yocto-queue": ["yocto-queue@0.1.0", "", {}, "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q=="], + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], + "@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag=="], "@eslint/eslintrc/ignore": ["ignore@5.3.2", "", {}, "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g=="], diff --git a/package-lock.json b/package-lock.json index c701b48..30f5c4c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@google/genai": "^1.35.0", "@libsql/client": "^0.17.0", + "@mistralai/mistralai": "^1.11.0", "@napi-rs/canvas": "^0.1.88", "axios": "^1.13.2", "dotenv": "^17.2.3", @@ -1676,6 +1677,15 @@ "win32" ] }, + "node_modules/@mistralai/mistralai": { + "version": "1.11.0", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.11.0.tgz", + "integrity": "sha512-6/BVj2mcaggYbpMzNSxtqtM2Tv/Jb5845XFd2CMYFO+O5VBkX70iLjtkBBTI4JFhh1l9vTCIMYXBVOjLoBVHGQ==", + "dependencies": { + "zod": "^3.20.0", + "zod-to-json-schema": "^3.24.1" + } + }, "node_modules/@napi-rs/canvas": { "version": "0.1.88", "resolved": "https://registry.npmjs.org/@napi-rs/canvas/-/canvas-0.1.88.tgz", @@ -6039,6 +6049,25 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.1", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.1.tgz", + "integrity": "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA==", + "license": "ISC", + "peerDependencies": { + "zod": "^3.25 || ^4" + } } } } diff --git a/package.json b/package.json index df97d45..a67eeec 100644 --- a/package.json +++ b/package.json @@ -10,6 +10,7 @@ "dependencies": { "@google/genai": "^1.35.0", "@libsql/client": "^0.17.0", + "@mistralai/mistralai": "^1.11.0", "@napi-rs/canvas": "^0.1.88", "axios": "^1.13.2", "dotenv": "^17.2.3", diff --git a/src/commands/gemini-chat.ts b/src/commands/gemini-chat.ts index 8d83c5b..50d8c0b 100644 --- a/src/commands/gemini-chat.ts +++ b/src/commands/gemini-chat.ts @@ -9,11 +9,11 @@ import { startIntervalEditor } from "../util/utils"; import {Environment} from "../common/environment"; -import {bot, googleAi} from "../index"; +import {bot} from "../index"; import {MessageStore} from "../common/message-store"; import {Requirements} from "../base/requirements"; import {Requirement} from "../base/requirement"; -import {ApiError} from "@google/genai"; +import {ApiError, GoogleGenAI} from "@google/genai"; export class GeminiChat extends ChatCommand { regexp = /^\/gemini\s([^]+)/i; @@ -22,6 +22,8 @@ export class GeminiChat extends ChatCommand { requirements = Requirements.Build(Requirement.BOT_CREATOR); + private googleAi = new GoogleGenAI({apiKey: Environment.GEMINI_API_KEY}); + async execute(msg: Message, match?: RegExpExecArray): Promise { console.log("match", match); return this.executeGemini(msg, match?.[1]); @@ -65,7 +67,7 @@ export class GeminiChat extends ChatCommand { } }); - const stream = await googleAi.models.generateContentStream({ + const stream = await this.googleAi.models.generateContentStream({ model: "gemini-2.5-flash", contents: chatContent, }); diff --git a/src/commands/mistral-chat.ts b/src/commands/mistral-chat.ts new file mode 100644 index 0000000..cd001b1 --- /dev/null +++ b/src/commands/mistral-chat.ts @@ -0,0 +1,146 @@ +import {ChatCommand} from "../base/chat-command"; +import {Requirements} from "../base/requirements"; +import {Requirement} from "../base/requirement"; +import {Message} from "typescript-telegram-bot-api"; +import { + collectReplyChainText, + editMessageText, + escapeMarkdownV2Text, + logError, + replyToMessage, + startIntervalEditor +} from "../util/utils"; +import {Environment} from "../common/environment"; +import {bot} from "../index"; +import {MessageStore} from "../common/message-store"; +import {Mistral} from "@mistralai/mistralai"; + +export class MistralChat extends ChatCommand { + regexp = /^\/mistral\s([^]+)/i; + title = "/mistral"; + description = "Chat with AI (Mistral)"; + + requirements = Requirements.Build(Requirement.BOT_CREATOR); + + private mistralAi = new Mistral({apiKey: Environment.MISTRAL_API_KEY}); + + async execute(msg: Message, match?: RegExpExecArray): Promise { + console.log("match", match); + return this.executeMistral(msg, match?.[1]); + } + + async executeMistral(msg: Message, text: string): Promise { + if (!text || text.trim().length === 0) return; + + const chatId = msg.chat.id; + + const messageParts = await collectReplyChainText(msg, "/mistral"); + console.log("MESSAGE PARTS", messageParts); + + const chatMessages = messageParts.map(part => { + return { + role: part.bot ? "assistant" : "user", + content: part.content + }; + }); + chatMessages.reverse(); + chatMessages.unshift({role: "system", content: Environment.SYSTEM_PROMPT}); + + // let chatContent = ""; + // for (const part of chatMessages) { + // chatContent += `${part.role.toUpperCase()}:\n${part.content}\n\n`; + // } + + // chatContent = chatContent.trim(); + + let waitMessage: Message; + + const startTime = new Date().getSeconds(); + + try { + waitMessage = await bot.sendMessage({ + chat_id: chatId, + text: Environment.waitText, + reply_parameters: { + chat_id: chatId, + message_id: msg.message_id + } + }); + + const stream = await this.mistralAi.chat.stream({ + model: "mistral-small-latest", + messages: chatMessages as any + }); + + let messageText = ""; + let shouldBreak = false; + let diff = 0; + + const editor = startIntervalEditor({ + intervalMs: 4500, + getText: () => messageText, + editFn: async (text) => { + await editMessageText(chatId, waitMessage.message_id, escapeMarkdownV2Text(text), "Markdown"); + }, + onStop: async () => { + } + }); + + try { + for await (const chunk of stream) { + // const text = chunk.text; + const text = chunk.data.choices[0].delta.content; + console.log("chunk", chunk); + + // const text = ""; + const length = (messageText + text).length; + if (length > 4096) { + messageText = messageText.slice(0, 4093) + "..."; + shouldBreak = true; + } else { + messageText += text; + } + + if (shouldBreak) { + console.log("messageText", messageText); + console.log("length", length); + console.log("break", true); + + diff = Math.abs(new Date().getSeconds() - startTime); + await editor.tick(); + await editor.stop(); + break; + } + + console.log("messageText", messageText); + console.log("length", messageText.length); + + diff = Math.abs(new Date().getSeconds() - startTime); + } + } finally { + await editor.tick(); + await editor.stop(); + + console.log("time", diff); + console.log("ended", true); + + waitMessage.reply_to_message = msg; + waitMessage.text = messageText; + MessageStore.put(waitMessage); + + await replyToMessage(waitMessage, `⏱️ ${diff}s`); + } + } catch (error) { + console.error(error); + + // if (error instanceof ApiError) { + // if (error.status === 429) { + // await replyToMessage(waitMessage, "На сегодня всё, лимиты закончились.").catch(logError); + // return; + // } + // } + + await replyToMessage(waitMessage, `Произошла ошибка!\n${error.toString()}`).catch(logError); + } + } +} \ No newline at end of file diff --git a/src/commands/ollama-kill.ts b/src/commands/ollama-kill.ts deleted file mode 100644 index ba37c57..0000000 --- a/src/commands/ollama-kill.ts +++ /dev/null @@ -1,24 +0,0 @@ -import {ChatCommand} from "../base/chat-command"; -import {Message} from "typescript-telegram-bot-api"; -import {oldSendMessage} from "../util/utils"; -import {ollama} from "../index"; -import {Requirements} from "../base/requirements"; -import {Requirement} from "../base/requirement"; - -export class OllamaKill extends ChatCommand { - regexp = /^\/killollama/i; - title = "/killOllama"; - description = "dunno, do some shit"; - - requirements = Requirements.Build(Requirement.BOT_CREATOR); - - async execute(msg: Message): Promise { - try { - ollama.abort(); - } catch (e) { - console.error(e); - } - - await oldSendMessage(msg, "Остановил все генерации"); - } -} \ No newline at end of file diff --git a/src/common/environment.ts b/src/common/environment.ts index 865a536..5c88c77 100644 --- a/src/common/environment.ts +++ b/src/common/environment.ts @@ -25,6 +25,7 @@ export class Environment { static MAX_PHOTO_SIZE: number; static GEMINI_API_KEY?: string; + static MISTRAL_API_KEY?: string; static waitText = "⏳ Дайте-ка подумать..."; @@ -50,6 +51,7 @@ export class Environment { Environment.MAX_PHOTO_SIZE = Number(process.env.MAX_PHOTO_SIZE || "1280"); Environment.GEMINI_API_KEY = process.env.GEMINI_API_KEY; + Environment.MISTRAL_API_KEY = process.env.MISTRAL_API_KEY; } static setAdmins(admins: Set) { diff --git a/src/index.ts b/src/index.ts index 248ee02..c6863c1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -35,13 +35,11 @@ import {OllamaPrompt} from "./commands/ollama-prompt"; import {AdminsAdd} from "./commands/admins-add"; import {AdminsRemove} from "./commands/admins-remove"; import {Shutdown} from "./commands/shutdown"; -import {OllamaKill} from "./commands/ollama-kill"; import {Leave} from "./commands/leave"; import {OllamaChat} from "./commands/ollama-chat"; import {Start} from "./commands/start"; import {MessageStore} from "./common/message-store"; import {PrefixResponse} from "./commands/prefix-response"; -import {GoogleGenAI} from "@google/genai"; import {GeminiChat} from "./commands/gemini-chat"; import {Choice} from "./commands/choice"; import {Coin} from "./commands/coin"; @@ -57,6 +55,7 @@ import {UserStore} from "./common/user-store"; import {OllamaRequest} from "./model/ollama-request"; import {CallbackCommand} from "./base/callback-command"; import {OllamaCancel} from "./callback_commands/ollama-cancel"; +import {MistralChat} from "./commands/mistral-chat"; process.setUncaughtExceptionCaptureCallback(console.error); @@ -101,8 +100,6 @@ export function abortOllamaRequest(uuid: string): boolean { } } -export const googleAi = new GoogleGenAI({apiKey: Environment.GEMINI_API_KEY}); - export let systemInfoText: string = ""; export function setSystemInfo(info: string) { @@ -149,7 +146,7 @@ export const callbackCommands: CallbackCommand[] = [ ]; if (Environment.OLLAMA_ADDRESS && Environment.OLLAMA_MODEL && Environment.SYSTEM_PROMPT) { - chatCommands.push(new OllamaChat(), new OllamaPrompt(), new OllamaKill()); + chatCommands.push(new OllamaChat(), new OllamaPrompt()); } if (Environment.OLLAMA_API_KEY) { @@ -160,6 +157,10 @@ if (Environment.GEMINI_API_KEY) { chatCommands.push(new GeminiChat()); } +if (Environment.MISTRAL_API_KEY) { + chatCommands.push(new MistralChat()); +} + async function main() { console.log( `TEST_ENVIRONMENT: ${Environment.TEST_ENVIRONMENT}\n` +