commands: switch AI commands to unified runtime

This commit is contained in:
2026-05-10 22:53:22 +03:00
parent 1b94760b21
commit 3d14e3c0d5
24 changed files with 465 additions and 3580 deletions
+142
View File
@@ -0,0 +1,142 @@
import {CallbackCommand} from "../base/callback-command";
import {CallbackQuery, InlineKeyboardMarkup, Message} from "typescript-telegram-bot-api";
import {abortAiRequest, getAiCancelRequest} from "../ai/cancel-registry";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Environment} from "../common/environment";
import {AiProvider} from "../model/ai-provider";
import {MessageStore} from "../common/message-store";
import {bot} from "../index";
import {buildCancelledGenerationText, logError} from "../util/utils";
import {enqueueTelegramApiCall} from "../util/telegram-api-queue";
import {prepareTelegramMarkdownV2} from "../util/markdown-v2-renderer";
import {buildAiRegenerateCallbackData} from "../ai/regenerate-callback";
import {isAiProviderConfigured, resolveEffectiveAiProviderForUser} from "../common/user-ai-settings";
const TELEGRAM_TEXT_LIMIT = 4096;
const TELEGRAM_CAPTION_LIMIT = 1024;
export class AiCancel extends CallbackCommand {
data = "/cancel_ai";
text = Environment.aiCancelCallbackText;
requirements = Requirements.Build(Requirement.SAME_USER);
async execute(query: CallbackQuery): Promise<void> {
if (!query.message || !query.data) return;
const parsed = this.parseCallbackData(query.data);
if (!parsed) return;
const request = getAiCancelRequest(parsed.requestId);
if (!request) {
await this.markMessageAsCancelled(query, parsed.provider);
return;
}
if (request.fromId !== query.from.id && query.from.id !== Environment.CREATOR_ID) return;
const cancelled = await abortAiRequest(parsed.requestId);
if (!cancelled) return;
}
private parseCallbackData(data: string): { requestId: string; provider?: AiProvider } | null {
const [, requestId, provider] = data.split(/\s+/);
if (!requestId) return null;
return {
requestId,
provider: Object.values(AiProvider).includes(provider as AiProvider) ? provider as AiProvider : undefined,
};
}
private async markMessageAsCancelled(query: CallbackQuery, providerFromCallback?: AiProvider): Promise<void> {
const callbackMessage = query.message;
if (!callbackMessage || callbackMessage.date === 0) return;
const message = callbackMessage as Message;
const stored = await MessageStore.get(message.chat.id, message.message_id).catch(e => {
logError(e);
return null;
});
const sourceFromId = await this.resolveSourceFromId(message, stored).catch(e => {
logError(e);
return undefined;
});
const regenerateProvider = providerFromCallback && isAiProviderConfigured(providerFromCallback)
? providerFromCallback
: await resolveEffectiveAiProviderForUser(sourceFromId ?? query.from.id);
const providerName = (providerFromCallback ?? regenerateProvider).toLowerCase();
const isCaption = this.isCaptionMessage(message);
const limit = isCaption ? TELEGRAM_CAPTION_LIMIT : TELEGRAM_TEXT_LIMIT;
const baseText = stored?.text ?? message.text ?? message.caption ?? "";
const cancelledText = buildCancelledGenerationText(baseText, providerName, limit);
const replyMarkup = this.regenerateKeyboard(regenerateProvider);
const formatted = prepareTelegramMarkdownV2(cancelledText, {mode: "final"});
try {
const result = isCaption
? await enqueueTelegramApiCall(
() => bot.editMessageCaption({
chat_id: message.chat.id,
message_id: message.message_id,
caption: formatted,
parse_mode: "MarkdownV2",
reply_markup: replyMarkup,
}),
{method: "editMessageCaption", chatId: message.chat.id, chatType: message.chat.type}
)
: await enqueueTelegramApiCall(
() => bot.editMessageText({
chat_id: message.chat.id,
message_id: message.message_id,
text: formatted,
parse_mode: "MarkdownV2",
reply_markup: replyMarkup,
}),
{method: "editMessageText", chatId: message.chat.id, chatType: message.chat.type}
);
if (result && result !== true) {
await MessageStore.put({...result, text: cancelledText} as Message);
} else {
await MessageStore.put({
chatId: message.chat.id,
id: message.message_id,
replyToMessageId: stored?.replyToMessageId ?? this.replyToMessageId(message),
fromId: message.from?.id ?? stored?.fromId ?? 0,
text: cancelledText,
date: message.date ?? stored?.date ?? Math.floor(Date.now() / 1000),
photoMaxSizeFilePath: stored?.photoMaxSizeFilePath,
attachments: stored?.attachments,
});
}
} catch (e) {
logError(e);
}
}
private regenerateKeyboard(provider: AiProvider): InlineKeyboardMarkup {
return {
inline_keyboard: [[{
text: Environment.regenerateText,
callback_data: buildAiRegenerateCallbackData(provider),
}]],
};
}
private async resolveSourceFromId(message: Message, stored: Awaited<ReturnType<typeof MessageStore.get>>): Promise<number | undefined> {
const reply = "reply_to_message" in message ? message.reply_to_message : undefined;
if (reply?.from?.id) return reply.from.id;
const source = await MessageStore.get(message.chat.id, stored?.replyToMessageId);
return source?.fromId;
}
private replyToMessageId(message: Message): number | undefined {
return "reply_to_message" in message ? message.reply_to_message?.message_id : undefined;
}
private isCaptionMessage(message: Message): boolean {
return message.caption !== undefined;
}
}
+83
View File
@@ -0,0 +1,83 @@
import {CallbackQuery, Message} from "typescript-telegram-bot-api";
import {CallbackCommand} from "../base/callback-command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {MessageStore} from "../common/message-store";
import {StoredMessage} from "../model/stored-message";
import {cutPrefixes, logError} from "../util/utils";
import {runUnifiedAi} from "../ai/unified-ai-runner";
import {AI_REGENERATE_CALLBACK, parseAiRegenerateCallbackData} from "../ai/regenerate-callback";
import {isAiProviderConfigured, resolveEffectiveAiProviderForUser} from "../common/user-ai-settings";
import {Environment} from "../common/environment";
export class AiRegenerate extends CallbackCommand {
data = AI_REGENERATE_CALLBACK;
text = Environment.aiRegenerateCallbackText;
requirements = Requirements.Build(Requirement.SAME_USER);
async execute(query: CallbackQuery): Promise<void> {
if (!query.message || !query.data) return;
const parsed = parseAiRegenerateCallbackData(query.data);
if (!parsed) return;
const source = await this.resolveSourceMessage(query);
if (!source) return;
const sourceFromId = source.stored?.fromId ?? source.message.from?.id;
if (!sourceFromId || (sourceFromId !== query.from.id && query.from.id !== Environment.CREATOR_ID)) return;
const provider = isAiProviderConfigured(parsed.provider)
? parsed.provider
: await resolveEffectiveAiProviderForUser(source.message.from?.id ?? query.from.id);
const text = cutPrefixes(source.stored ?? source.message) ?? "";
runUnifiedAi({
provider,
msg: source.message,
text,
stream: true,
think: parsed.think,
targetMessage: query.message,
}).catch(logError);
}
private async resolveSourceMessage(query: CallbackQuery): Promise<{
message: Message;
stored: StoredMessage | null;
} | null> {
const responseMessage = query.message;
if (!responseMessage) return null;
const directSource = "reply_to_message" in responseMessage ? responseMessage.reply_to_message : undefined;
if (directSource) {
const stored = await MessageStore.put(directSource).catch(e => {
logError(e);
return null;
});
return {message: directSource, stored};
}
const storedResponse = await MessageStore.get(responseMessage.chat.id, responseMessage.message_id);
const storedSource = await MessageStore.get(responseMessage.chat.id, storedResponse?.replyToMessageId);
if (!storedSource) return null;
return {
message: this.storedToMessage(storedSource, responseMessage, query),
stored: storedSource,
};
}
private storedToMessage(stored: StoredMessage, responseMessage: Message, query: CallbackQuery): Message {
return {
message_id: stored.id,
chat: responseMessage.chat,
date: stored.date,
from: query.from.id === stored.fromId
? query.from
: {id: stored.fromId, is_bot: false, first_name: ""},
text: stored.text ?? undefined,
} as Message;
}
}
-74
View File
@@ -1,74 +0,0 @@
import {CallbackCommand} from "../base/callback-command";
import {CallbackQuery} from "typescript-telegram-bot-api";
import {abortOllamaRequest, bot, getOllamaRequest} from "../index";
import {escapeMarkdownV2Text, logError} from "../util/utils";
import {MessageStore} from "../common/message-store";
import {StoredMessage} from "../model/stored-message";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Environment} from "../common/environment";
export class OllamaCancel extends CallbackCommand {
data = "/cancel_ollama";
text = "Cancel Ollama generation";
requirements = Requirements.Build(Requirement.SAME_USER);
async execute(query: CallbackQuery): Promise<void> {
if (!query.message || !query.data) return;
const chatId = query.message.chat.id;
const fromId = query.from.id;
const messageId = query.message.message_id;
const uuid = query.data.split(" ")[1];
if (!uuid) return;
const request = getOllamaRequest(uuid);
if (request) {
if (request.fromId !== fromId && fromId !== Environment.CREATOR_ID) return;
const aborted = abortOllamaRequest(uuid);
console.log(`aborted request ${uuid}:`, aborted);
} else {
console.log(`no request with uuid "${uuid}" found`);
}
let msg: StoredMessage | null = null;
try {
msg = await MessageStore.get(chatId, messageId);
} catch (e) {
logError(e);
}
console.log(`Message for ${chatId}-${messageId}:`, msg);
let content: string | null = null;
if (msg?.text?.trim()?.length) {
content = msg?.text.trim();
if (content.length + Environment.ollamaCancelledText.length > 4096) {
content = content.substring(0, 4096 - Environment.ollamaCancelledText.length - 2) + "\n";
}
}
const newText = `${content ? content : ""}${Environment.ollamaCancelledText}`;
try {
await bot.editMessageText({
chat_id: chatId,
message_id: messageId,
text: escapeMarkdownV2Text(newText),
parse_mode: "MarkdownV2",
reply_markup: {inline_keyboard: []},
});
if (msg) {
await MessageStore.put(msg);
}
} catch (e) {
logError(e);
}
}
}
+10 -181
View File
@@ -1,196 +1,25 @@
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {Environment} from "../common/environment"; import {ChatCommand} from "../base/chat-command";
import {bot, googleAi} from "../index";
import {MessageStore} from "../common/message-store";
import {Requirements} from "../base/requirements"; import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
import {AiProvider} from "../model/ai-provider";
import { import {runUnifiedAi} from "../ai/unified-ai-runner";
collectReplyChainText, import {Environment} from "../common/environment";
escapeMarkdownV2Text,
logError,
oldReplyToMessage,
replyToMessage,
startIntervalEditor
} from "../util/utils";
import {ChatCommand} from "../base/chat-command";
import {ApiError} from "@google/genai";
export class GeminiChat extends ChatCommand { export class GeminiChat extends ChatCommand {
command = "gemini"; command = ["gemini", "gemini-chat"];
argsMode = "required" as const; argsMode = "required" as const;
requirements = Requirements.Build(Requirement.BOT_CREATOR); requirements = Requirements.Build(Requirement.BOT_CREATOR);
title = "/gemini"; title = Environment.commandTitles.geminiChat;
description = "Chat with AI (Gemini)"; description = Environment.commandDescriptions.geminiChat;
async execute(msg: Message, match?: RegExpExecArray): Promise<void> { async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("match", match);
return this.executeGemini(msg, match?.[3] || ""); return this.executeGemini(msg, match?.[3] || "");
} }
async executeGemini(msg: Message, text: string): Promise<void> { async executeGemini(msg: Message, text: string, stream: boolean = true): Promise<void> {
if (!text || !text.trim().length) return; await runUnifiedAi({provider: AiProvider.GEMINI, msg, text, stream});
const chatId = msg.chat.id;
const storedMsg = await MessageStore.get(chatId, msg.message_id);
const messageParts = await collectReplyChainText(storedMsg);
console.log("MESSAGE PARTS", messageParts);
const chatMessages = messageParts.map(part => {
return {
role: part.bot ? "assistant" : "user",
content: (Environment.USE_NAMES_IN_PROMPT && !part.bot ? `MESSAGE FROM USER "${part.name}":\n` : "") + part.content
};
});
chatMessages.reverse();
if (Environment.SYSTEM_PROMPT && Environment.USE_SYSTEM_PROMPT) {
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();
const input = [];
input.push(
{
type: "text",
text: chatContent
}
);
// TODO: 12/02/2026, Danil Nikolaev: support for multiple images
if (messageParts.some(p => p.images?.length)) {
const firstImages = messageParts.find(p => p.images?.length)?.images ?? [];
firstImages.forEach(image => {
input.push({
type: "image",
data: image,
mime_type: "image/png"
});
});
}
let waitMessage: Message | null = null;
const startTime = Date.now();
try {
const imagesCount = input.some(e => e.type === "image");
waitMessage = await bot.sendMessage({
chat_id: chatId,
text: imagesCount ? Environment.analyzingPictureText : Environment.waitThinkText,
reply_parameters: {
chat_id: chatId,
message_id: msg.message_id
}
});
const stream = await googleAi.interactions.create({
model: Environment.GEMINI_MODEL,
input: input as any,
stream: true
});
let currentText = "";
let shouldBreak = false;
const editor = startIntervalEditor({
intervalMs: 4500,
getText: () => currentText,
editFn: async (text) => {
await bot.editMessageText(
{
chat_id: chatId,
message_id: <number>waitMessage?.message_id,
text: escapeMarkdownV2Text(text),
parse_mode: "MarkdownV2"
}
).catch(logError);
console.log("editMessageText", text);
if (waitMessage) {
waitMessage.reply_to_message = msg;
waitMessage.text = text;
await MessageStore.put(waitMessage);
}
},
onStop: async () => {
}
});
await editor.tick();
try {
for await (const event of stream) {
switch (event.event_type) {
case "content.delta":
switch (event.delta?.type) {
case "text": {
const text = event.delta.text;
currentText += text;
if (currentText.length > 4096) {
currentText = currentText.slice(0, 4093) + "...";
shouldBreak = true;
}
console.log("messageText", currentText);
console.log("length", currentText.length);
if (shouldBreak) {
console.log("break", true);
break;
}
break;
}
case "image": {
const image = event.delta.data;
console.log("image", image);
}
}
}
}
} finally {
await editor.tick();
await editor.stop();
if (!shouldBreak) {
console.log("ended", true);
}
const diff = Math.abs(Date.now() - startTime) / 1000.0;
console.log("time", diff);
waitMessage.reply_to_message = msg;
waitMessage.text = currentText;
await MessageStore.put(waitMessage);
if (Environment.SEND_TIME_TOOK) {
await replyToMessage({message: waitMessage, text: `⏱️ ${diff}s`});
}
}
} catch (e: any) {
logError(e);
if (waitMessage) {
if (e instanceof ApiError) {
if (e.status === 429) {
await oldReplyToMessage(waitMessage, "На сегодня всё, лимиты закончились.").catch(logError);
return;
}
}
await oldReplyToMessage(waitMessage, `Произошла ошибка!\n${e.toString()}`).catch(logError);
}
}
} }
} }
-62
View File
@@ -1,62 +0,0 @@
import {Command} from "../base/command";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
import {googleAi} from "../index";
import {logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
export class GeminiGenerateImage extends Command {
command = "geminiGenImage";
argsMode = "required" as const;
title = "/geminiGenImage";
description = "Generate image with Gemini";
requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("match", match);
const prompt = match?.[3] || "";
return this.executeGenImage(msg, prompt);
}
async executeGenImage(msg: Message, text: string): Promise<void> {
if (!text || !text.trim().length) return;
let waitMessage: Message | null = null;
try {
waitMessage = await replyToMessage({
message: msg,
text: Environment.genImageText,
});
const interaction = await googleAi.interactions.create({
model: Environment.GEMINI_IMAGE_MODEL,
response_modalities: ["image"],
input: text,
});
interaction.outputs?.forEach((output, index) => {
if (output.type === "image") {
// const image = output.data;
console.log(`Image output ${index + 1}:`, output);
} else {
console.log(`Output ${index + 1}: ${output}`);
}
});
} catch (e: any) {
logError(e);
if (waitMessage) {
await replyToMessage({
message: waitMessage,
text: `Произошла ошибка!\n${e.toString()}`,
link_preview_options: {is_disabled: true}
}).catch(logError);
}
}
}
}
+6 -23
View File
@@ -1,32 +1,15 @@
import {Command} from "../base/command"; import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils"; import {logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {formatRuntimeModelInfo} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {googleAi} from "../index";
import {AiModelCapabilities} from "../model/ai-model-capabilities";
export class GeminiGetModel extends Command { export class GeminiGetModel extends Command {
title = "/geminiGetModel"; title = Environment.commandTitles.geminiGetModel;
description = "Get current Gemini model"; description = Environment.commandDescriptions.geminiGetModel;
async execute(msg: Message): Promise<void> { async execute(msg: Message): Promise<void> {
await replyToMessage({message: msg, text: `Текущая модель: "${Environment.GEMINI_MODEL}"`}).catch(logError); await replyToMessage({message: msg, text: await formatRuntimeModelInfo(AiProvider.GEMINI)}).catch(logError);
} }
}
async getModelCapabilities(): Promise<AiModelCapabilities | null> {
try {
const info = await googleAi.models.get({model: Environment.GEMINI_MODEL});
console.log(info);
return {
vision: {supported: true},
ocr: undefined,
thinking: {supported: info.thinking},
tools: undefined
};
} catch (e) {
logError(e);
return null;
}
}
}
+12 -20
View File
@@ -2,35 +2,27 @@ import {Command} from "../base/command";
import {Requirements} from "../base/requirements"; import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {googleAi} from "../index"; import {escapeHtml, logError, replyToMessage} from "../util/utils";
import {logError, replyToMessage} from "../util/utils"; import {AiProvider} from "../model/ai-provider";
import {listProviderModels} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment";
export class GeminiListModels extends Command { export class GeminiListModels extends Command {
title = "/geminiListModels"; title = Environment.commandTitles.geminiListModels;
description = "List all Gemini models"; description = Environment.commandDescriptions.geminiListModels;
requirements = Requirements.Build(Requirement.BOT_CREATOR); requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message): Promise<void> { async execute(msg: Message): Promise<void> {
try { try {
const listResponse = await googleAi.models.list(); const models = (await listProviderModels(AiProvider.GEMINI)).sort((a, b) => a.localeCompare(b));
console.log(listResponse); const modelsString = escapeHtml(models.join("\n").substring(0, 4000));
const text = Environment.modelListHeaderText + "<blockquote expandable>" + modelsString + "</blockquote>";
const modelsString = listResponse.page await replyToMessage({message: msg, text, parse_mode: "HTML"});
.sort((a, b) => (a.name || "").localeCompare((b.name || "")))
.map(e => `${e.name}`)
.join("\n");
const text = "Доступные модели:\n\n" + "<blockquote expandable>" + modelsString + "</blockquote>";
await replyToMessage({
message: msg,
text: text,
parse_mode: "HTML"
});
} catch (e) { } catch (e) {
logError(e); logError(e);
await replyToMessage({message: msg, text: "Не получилось загрузить список моделей"}).catch(logError); await replyToMessage({message: msg, text: Environment.modelListLoadFailedText}).catch(logError);
} }
} }
} }
+13 -9
View File
@@ -2,24 +2,28 @@ import {Command} from "../base/command";
import {Requirements} from "../base/requirements"; import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {Environment} from "../common/environment";
import {logError, replyToMessage} from "../util/utils"; import {logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {getRuntimeModel, setRuntimeModel, formatRuntimeModelInfo} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment";
export class GeminiSetModel extends Command { export class GeminiSetModel extends Command {
argsMode = "required" as const; argsMode = "required" as const;
title = "/geminiSetModel"; title = Environment.commandTitles.geminiSetModel;
description = "Set Gemini model"; description = Environment.commandDescriptions.geminiSetModel;
requirements = Requirements.Build(Requirement.BOT_CREATOR); requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> { async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
const newModel = match?.[3]; const newModel = match?.[3]?.trim();
Environment.setGeminiModel(newModel || Environment.GEMINI_MODEL); if (newModel) setRuntimeModel(AiProvider.GEMINI, newModel);
const text = newModel ? `Выбрана модель "${newModel}"` const model = getRuntimeModel(AiProvider.GEMINI);
: `Модель не задана. Будет использоваться стандартная модель "${Environment.GEMINI_MODEL}".`; const text = newModel
? Environment.getSelectedModelWithInfoText(model, await formatRuntimeModelInfo(AiProvider.GEMINI))
: Environment.getModelIsNotSetCurrentText(model);
await replyToMessage({message: msg, text: text}).catch(logError); await replyToMessage({message: msg, text}).catch(logError);
} }
} }
+47 -38
View File
@@ -2,67 +2,76 @@ import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {callbackCommands, commands} from "../index"; import {callbackCommands, commands} from "../index";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {boolToEmoji, getCurrentModel, getCurrentModelCapabilities, logError, replyToMessage} from "../util/utils"; import {getCurrentModel, logError, replyToMessage} from "../util/utils";
import {AiModelCapabilities} from "../model/ai-model-capabilities"; import {AiModelCapabilities} from "../model/ai-model-capabilities";
import {AiProvider} from "../model/ai-provider"; import {AiProvider} from "../model/ai-provider";
import {Command} from "../base/command"; import {Command} from "../base/command";
import {formatRuntimeModelInfo, getRuntimeCapabilities} from "../ai/provider-model-runtime";
import {getProviderTools} from "../ai/tool-mappers";
import {prepareTelegramMarkdownV2} from "../util/markdown-v2-renderer";
export class Info extends Command { export class Info extends Command {
command = ["info", "v"]; command = ["info", "v"];
title = "/info"; title = Environment.commandTitles.info;
description = "Info about bot"; description = Environment.commandDescriptions.info;
async execute(msg: Message): Promise<void> { async execute(msg: Message): Promise<void> {
const aiProvider = Environment.DEFAULT_AI_PROVIDER; const aiProvider = Environment.DEFAULT_AI_PROVIDER;
const aiModel = getCurrentModel(); const aiModel = getCurrentModel();
let aiModelCapabilities: AiModelCapabilities | null = {}; if (!aiModel) return;
let aiModelCapabilities: AiModelCapabilities | null = null;
try { try {
aiModelCapabilities = await getCurrentModelCapabilities(); aiModelCapabilities = await getRuntimeCapabilities(aiProvider, aiModel);
} catch (e) { } catch (e) {
logError(e); logError(e);
await replyToMessage({message: msg, text: `Произошла ошибка: ${e}`}).catch(logError); await replyToMessage({message: msg, text: Environment.getErrorText(e)}).catch(logError);
return; return;
} }
const supportedProvidersLength = Object.keys(AiProvider).filter(key => isNaN(Number(key))).length;
const aiInfo = "```" + const getAiInfo = async () => {
"AI\n" + return Environment.getInfoAiBlockText(
`supported providers: ${Object.keys(AiProvider).filter(key => isNaN(Number(key))).length}\n\n` + supportedProvidersLength,
await formatRuntimeModelInfo(aiProvider, aiModel, aiModelCapabilities),
);
};
`provider: ${aiProvider.toLowerCase()}\n` + const getToolsInfo = async () => {
`model: ${aiModel}\n\n` + const tools = getProviderTools(aiProvider);
`vision${aiModelCapabilities?.vision?.external ? "(ext)" : ""}: ${boolToEmoji(aiModelCapabilities?.vision?.supported)}\n` + return Environment.getInfoToolsBlockText(tools.map(t => t.function.name));
`ocr${aiModelCapabilities?.ocr?.external ? "(ext)" : ""}: ${boolToEmoji(aiModelCapabilities?.ocr?.supported)}\n` + };
`thinking${aiModelCapabilities?.thinking?.external ? "(ext)" : ""}: ${boolToEmoji(aiModelCapabilities?.thinking?.supported)}\n` +
`tools${aiModelCapabilities?.tools?.external ? "(ext)" : ""}: ${boolToEmoji(aiModelCapabilities?.tools?.supported)}\n` +
`audio${aiModelCapabilities?.audio?.external ? "(ext)" : ""}: ${boolToEmoji(aiModelCapabilities?.audio?.supported)}` +
"```";
const cmds = commands.filter(c => !(c instanceof ChatCommand)); const getCommandsInfo = async () => {
const chatCmds = commands.filter(c => c instanceof ChatCommand); const cmds = commands.filter(c => !(c instanceof ChatCommand));
const callbackCmds = callbackCommands; const chatCmds = commands.filter(c => c instanceof ChatCommand);
const callbackCmds = callbackCommands;
const publicCmdsLength = cmds.filter(c => c.requirements?.isPublic()).length;
const privateCmdsLength = cmds.length - publicCmdsLength;
const chatCmdsLength = chatCmds.length;
const callbackCmdsLength = callbackCmds.length;
const publicCmdsLength = cmds.filter(c => c.requirements?.isPublic()).length; return Environment.getInfoCommandsBlockText({
const privateCmdsLength = cmds.length - publicCmdsLength; publicCommands: publicCmdsLength,
privateCommands: privateCmdsLength,
chatCommands: chatCmdsLength,
callbackCommands: callbackCmdsLength,
});
};
const chatCmdsLength = chatCmds.length;
const callbackCmdsLength = callbackCmds.length; const finalText = [
await getAiInfo(),
await getToolsInfo(),
await getCommandsInfo()
].join("\n");
const text = await replyToMessage({
aiInfo + "\n\n" + message: msg,
text: prepareTelegramMarkdownV2(finalText, {mode: "final"}),
"```" + parse_mode: "MarkdownV2"
"Commands\n" + }).catch(logError);
`Public: ${publicCmdsLength}\n` +
`Private: ${privateCmdsLength}\n` +
`Chat: ${chatCmdsLength}\n` +
`Callback: ${callbackCmdsLength}\n` +
"```"
;
await replyToMessage({message: msg, text: text, parse_mode: "Markdown"}).catch(logError);
} }
} }
+10 -170
View File
@@ -1,185 +1,25 @@
import {Message} from "typescript-telegram-bot-api";
import {ChatCommand} from "../base/chat-command";
import {Requirements} from "../base/requirements"; import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api"; import {AiProvider} from "../model/ai-provider";
import { import {runUnifiedAi} from "../ai/unified-ai-runner";
collectReplyChainText,
escapeMarkdownV2Text,
logError,
oldReplyToMessage,
replyToMessage,
startIntervalEditor
} from "../util/utils";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {bot, commands, mistralAi} from "../index";
import {MessageStore} from "../common/message-store";
import {ChatCommand} from "../base/chat-command";
import {MistralGetModel} from "./mistral-get-model";
export class MistralChat extends ChatCommand { export class MistralChat extends ChatCommand {
command = "mistral"; command = ["mistral", "mistral-chat"];
argsMode = "required" as const; argsMode = "required" as const;
requirements = Requirements.Build(Requirement.BOT_CREATOR); requirements = Requirements.Build(Requirement.BOT_CREATOR);
title = "/mistral"; title = Environment.commandTitles.mistralChat;
description = "Chat with AI (Mistral)"; description = Environment.commandDescriptions.mistralChat;
async execute(msg: Message, match?: RegExpExecArray): Promise<void> { async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("match", match);
return this.executeMistral(msg, match?.[3] || ""); return this.executeMistral(msg, match?.[3] || "");
} }
async executeMistral(msg: Message, text: string): Promise<void> { async executeMistral(msg: Message, text: string, stream: boolean = true): Promise<void> {
if (!text || !text.trim().length) return; await runUnifiedAi({provider: AiProvider.MISTRAL, msg, text, stream});
const chatId = msg.chat.id;
const storedMsg = await MessageStore.get(chatId, msg.message_id);
const messageParts = await collectReplyChainText(storedMsg);
console.log("MESSAGE PARTS", messageParts);
const chatMessages = messageParts.map(part => {
const content = [];
content.push({
type: "text",
text: (Environment.USE_NAMES_IN_PROMPT && !part.bot ? `MESSAGE FROM USER "${part.name}":\n` : "") + part.content,
});
for (const image of part.images) {
content.push({
type: "image_url",
imageUrl: "data:image/jpeg;base64," + image
});
}
return {
role: part.bot ? "assistant" : "user",
content: content,
};
});
chatMessages.reverse();
if (Environment.SYSTEM_PROMPT && Environment.USE_SYSTEM_PROMPT) {
chatMessages.unshift({role: "system", content: [{type: "text", text: Environment.SYSTEM_PROMPT}]});
}
let waitMessage: Message | null = null;
const startTime = Date.now();
try {
const imagesCount = chatMessages.reduce((total, curr) => {
return total + (curr.content.filter(c => c.type === "image_url")?.length ?? 0);
}, 0);
if (imagesCount) {
try {
const modelInfo = await commands.find(c => c instanceof MistralGetModel)?.getModelCapabilities();
if (modelInfo) {
if (!modelInfo.vision?.supported) {
await replyToMessage({
message: msg,
text: "Моя текущая модель не умеет анализировать изображения 🥹"
});
return;
}
}
} catch (e) {
logError(e);
}
}
waitMessage = await bot.sendMessage({
chat_id: chatId,
text: imagesCount ?
imagesCount > 1 ? Environment.analyzingPicturesText : Environment.analyzingPictureText
: Environment.waitThinkText,
reply_parameters: {
chat_id: chatId,
message_id: msg.message_id
}
});
const stream = await mistralAi.chat.stream({
model: Environment.MISTRAL_MODEL,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
messages: chatMessages as any
});
let currentText = "";
let shouldBreak = false;
const editor = startIntervalEditor({
intervalMs: 4500,
getText: () => currentText,
editFn: async (text) => {
await bot.editMessageText(
{
chat_id: chatId,
message_id: <number>waitMessage?.message_id,
text: escapeMarkdownV2Text(text),
parse_mode: "MarkdownV2"
}
).catch(logError);
console.log("editMessageText", text);
if (waitMessage) {
waitMessage.reply_to_message = msg;
waitMessage.text = text;
await MessageStore.put(waitMessage);
}
},
onStop: async () => {
}
});
await editor.tick();
try {
for await (const chunk of stream) {
console.log("chunk", chunk);
const text = chunk.data.choices[0].delta.content;
currentText += text;
if (currentText.length > 4096) {
currentText = currentText.slice(0, 4093) + "...";
shouldBreak = true;
}
console.log("messageText", currentText);
console.log("length", currentText.length);
if (shouldBreak) {
console.log("break", true);
break;
}
}
} finally {
await editor.tick();
await editor.stop();
if (!shouldBreak) {
console.log("ended", true);
}
const diff = Math.abs(Date.now() - startTime) / 1000.0;
console.log("time", diff);
waitMessage.reply_to_message = msg;
waitMessage.text = currentText;
await MessageStore.put(waitMessage);
if (Environment.SEND_TIME_TOOK) {
await replyToMessage({message: waitMessage, text: `⏱️ ${diff}s`});
}
}
} catch (e: any) {
logError(e);
if (waitMessage) {
await oldReplyToMessage(waitMessage, `Произошла ошибка!\n${e.toString()}`).catch(logError);
}
}
} }
} }
+6 -28
View File
@@ -1,37 +1,15 @@
import {Command} from "../base/command"; import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils"; import {logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {formatRuntimeModelInfo} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {mistralAi} from "../index";
import {AiModelCapabilities} from "../model/ai-model-capabilities";
export class MistralGetModel extends Command { export class MistralGetModel extends Command {
title = "/mistralGetModel"; title = Environment.commandTitles.mistralGetModel;
description = "Get current Mistral model"; description = Environment.commandDescriptions.mistralGetModel;
requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message): Promise<void> { async execute(msg: Message): Promise<void> {
await replyToMessage({message: msg, text: `Текущая модель: "${Environment.MISTRAL_MODEL}"`}).catch(logError); await replyToMessage({message: msg, text: await formatRuntimeModelInfo(AiProvider.MISTRAL)}).catch(logError);
} }
}
async getModelCapabilities(): Promise<AiModelCapabilities | null> {
try {
const info = await mistralAi.models.retrieve({modelId: Environment.MISTRAL_MODEL}) as any;
console.log(info);
return {
vision: {supported: info.capabilities.vision},
ocr: {supported: info.capabilities.ocr},
thinking: undefined,
tools: {supported: info.capabilities.functionCalling},
audio: {supported: info.capabilities.audioTranscription}
};
} catch (e) {
logError(e);
return null;
}
}
}
+11 -60
View File
@@ -2,76 +2,27 @@ import {Command} from "../base/command";
import {Requirements} from "../base/requirements"; import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {mistralAi} from "../index"; import {escapeHtml, logError, replyToMessage} from "../util/utils";
import {logError, oldReplyToMessage, replyToMessage} from "../util/utils"; import {AiProvider} from "../model/ai-provider";
import {listProviderModels} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment";
export class MistralListModels extends Command { export class MistralListModels extends Command {
title = "/mistralListModels"; title = Environment.commandTitles.mistralListModels;
description = "List all Mistral models"; description = Environment.commandDescriptions.mistralListModels;
requirements = Requirements.Build(Requirement.BOT_CREATOR); requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message): Promise<void> { async execute(msg: Message): Promise<void> {
try { try {
const listResponse = await mistralAi.models.list() as { const models = (await listProviderModels(AiProvider.MISTRAL)).sort((a, b) => a.localeCompare(b));
object: string; const modelsString = escapeHtml(models.join("\n").substring(0, 4000));
data: Array<BaseModelCard> const text = Environment.modelListHeaderText + "<blockquote expandable>" + modelsString + "</blockquote>";
};
console.log(listResponse);
const modelsString = listResponse.data await replyToMessage({message: msg, text, parse_mode: "HTML"});
.sort((a, b) => a?.name?.localeCompare(b.name || "") || -1)
.map(e => `${e.id}`)
.join("\n");
const text = "Доступные модели:\n\n" + "<blockquote expandable>" + modelsString + "</blockquote>";
await replyToMessage({
message: msg,
text: text,
parse_mode: "HTML"
});
} catch (e) { } catch (e) {
logError(e); logError(e);
await oldReplyToMessage(msg, "Не получилось загрузить список моделей").catch(logError); await replyToMessage({message: msg, text: Environment.modelListLoadFailedText}).catch(logError);
} }
} }
} }
type BaseModelCard = {
id: string;
object: string;
created?: number | undefined;
ownedBy: string;
/**
* This is populated by Harmattan, but some fields have a name
*
* @remarks
* that we don't want to expose in the API.
*/
capabilities: ModelCapabilities;
name?: string | null | undefined;
description?: string | null | undefined;
maxContextLength: number;
aliases?: Array<string> | undefined;
deprecation?: Date | null | undefined;
deprecationReplacementModel?: string | null | undefined;
defaultModelTemperature?: number | null | undefined;
type: "base";
};
type ModelCapabilities = {
completionChat: boolean;
functionCalling: boolean;
reasoning: boolean;
completionFim: boolean;
fineTuning: boolean;
vision: boolean;
ocr: boolean;
classification: boolean;
moderation: boolean;
audio: boolean;
audioTranscription: boolean;
audioTranscriptionRealtime: boolean;
audioSpeech: boolean;
};
+13 -9
View File
@@ -2,24 +2,28 @@ import {Command} from "../base/command";
import {Requirements} from "../base/requirements"; import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {Environment} from "../common/environment";
import {logError, replyToMessage} from "../util/utils"; import {logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {getRuntimeModel, setRuntimeModel, formatRuntimeModelInfo} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment";
export class MistralSetModel extends Command { export class MistralSetModel extends Command {
argsMode = "required" as const; argsMode = "required" as const;
title = "/mistralSetModel"; title = Environment.commandTitles.mistralSetModel;
description = "Set Mistral model"; description = Environment.commandDescriptions.mistralSetModel;
requirements = Requirements.Build(Requirement.BOT_CREATOR); requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> { async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
const newModel = match?.[3]; const newModel = match?.[3]?.trim();
Environment.setMistralModel(newModel || Environment.MISTRAL_MODEL); if (newModel) setRuntimeModel(AiProvider.MISTRAL, newModel);
const text = newModel ? `Выбрана модель "${newModel}"` const model = getRuntimeModel(AiProvider.MISTRAL);
: `Модель не задана. Будет использоваться стандартная модель "${Environment.MISTRAL_MODEL}".`; const text = newModel
? Environment.getSelectedModelWithInfoText(model, await formatRuntimeModelInfo(AiProvider.MISTRAL))
: Environment.getModelIsNotSetCurrentText(model);
await replyToMessage({message: msg, text: text}).catch(logError); await replyToMessage({message: msg, text}).catch(logError);
} }
} }
File diff suppressed because it is too large Load Diff
+7 -113
View File
@@ -1,121 +1,15 @@
import {Command} from "../base/command"; import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {boolToEmoji, logError, replyToMessage} from "../util/utils"; import {logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {formatRuntimeModelInfo} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {ollama} from "../index";
import {AiModelCapabilities} from "../model/ai-model-capabilities";
export class OllamaGetModel extends Command { export class OllamaGetModel extends Command {
title = "/ollamaGetModel"; title = Environment.commandTitles.ollamaGetModel;
description = "Ollama model info"; description = Environment.commandDescriptions.ollamaGetModel;
async execute(msg: Message): Promise<void> { async execute(msg: Message): Promise<void> {
try { await replyToMessage({message: msg, text: await formatRuntimeModelInfo(AiProvider.OLLAMA)}).catch(logError);
const model = Environment.OLLAMA_MODEL;
const imageModel = Environment.OLLAMA_IMAGE_MODEL;
const thinkModel = Environment.OLLAMA_THINK_MODEL;
const promises: (Promise<AiModelCapabilities | null> | null)[] = [this.getModelCapabilities()];
if (imageModel && imageModel !== model) {
promises.push(this.loadImageModelInfo());
} else {
promises.push(null);
}
if (thinkModel && thinkModel !== model) {
promises.push(this.loadThinkModelInfo());
} else {
promises.push(null);
}
const infos = await Promise.all(promises);
let modelInfo = infos[0];
const modelText = "```Text\n" + this.getModelText(model, modelInfo) + "```";
modelInfo = infos[1];
const imageModelText = modelInfo ?
"```Image\n" + this.getModelText(imageModel, modelInfo) + "```" : null;
modelInfo = infos[2];
const thinkModelText = modelInfo ?
"```Think\n" + this.getModelText(thinkModel, modelInfo) + "```" : null;
const modelInfos = [modelText];
if (imageModelText) {
modelInfos.push(imageModelText);
}
if (thinkModelText) {
modelInfos.push(thinkModelText);
}
await replyToMessage({
message: msg,
text: modelInfos.join("\n\n"),
parse_mode: "Markdown"
}).catch(logError);
} catch (e: any) {
logError(e);
await replyToMessage({message: msg, text: e.toString()}).catch(logError);
}
} }
}
private getModelText(model: string | undefined, info: AiModelCapabilities | null): string {
return `model: ${model}\n\n` +
`vision: ${boolToEmoji(info?.vision?.supported)}\n` +
`ocr: ${boolToEmoji(info?.ocr?.supported)}\n` +
`thinking: ${boolToEmoji(info?.thinking?.supported)}\n` +
`tools: ${boolToEmoji(info?.tools?.supported)}\n` +
`audio: ${boolToEmoji(info?.audio?.supported)}`;
}
async getModelCapabilities(model: string | undefined = Environment.OLLAMA_MODEL): Promise<AiModelCapabilities | null> {
if (!model) return null;
try {
const info = await ollama.show({model: model});
console.log(info);
return {
vision: {
supported: info.capabilities.includes("vision"),
external: model !== Environment.OLLAMA_MODEL,
model: model
},
ocr: {
supported: info.capabilities.includes("ocr"),
external: model !== Environment.OLLAMA_MODEL,
model: model
},
thinking: {
supported: info.capabilities.includes("thinking"),
external: model !== Environment.OLLAMA_MODEL,
model: model
},
tools: {
supported: info.capabilities.includes("tools"),
external: model !== Environment.OLLAMA_MODEL,
model: model
},
audio: {
supported: info.capabilities.includes("audio"),
external: model !== Environment.OLLAMA_MODEL,
model: model
}
};
} catch (e) {
logError(e);
return null;
}
}
async loadImageModelInfo(): Promise<AiModelCapabilities | null> {
return this.getModelCapabilities(Environment.OLLAMA_IMAGE_MODEL);
}
async loadThinkModelInfo(): Promise<AiModelCapabilities | null> {
return this.getModelCapabilities(Environment.OLLAMA_THINK_MODEL);
}
}
+20 -21
View File
@@ -1,36 +1,35 @@
import {Command} from "../base/command"; import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {ollama} from "../index";
import {logError, oldReplyToMessage, replyToMessage} from "../util/utils";
import {Requirements} from "../base/requirements"; import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api";
import {Environment} from "../common/environment";
import {escapeHtml, logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {listProviderModels} from "../ai/provider-model-runtime";
import {createOllamaClient, resolveAiRuntimeTarget} from "../ai/ai-runtime-target";
export class OllamaListModels extends Command { export class OllamaListModels extends Command {
title = "/ollamaListModels"; title = Environment.commandTitles.ollamaListModels;
description = "List all Ollama models"; description = Environment.commandDescriptions.ollamaListModels;
requirements = Requirements.Build(Requirement.BOT_CREATOR); requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message): Promise<void> { async execute(msg: Message): Promise<void> {
try { try {
const listResponse = await ollama.list(); const target = resolveAiRuntimeTarget(AiProvider.OLLAMA, "chat");
console.log(listResponse); const models = (await listProviderModels(AiProvider.OLLAMA)).sort((a, b) => a.localeCompare(b));
const modelsString = escapeHtml(models.join("\n").substring(0, 4000));
const loadedModels = ((await createOllamaClient(target).ps())?.models ?? [])
.map(model => model.model || model.name)
.filter((model): model is string => !!model);
const text =
Environment.getLoadedModelsText(loadedModels) + "\n\n" +
Environment.modelListHeaderText + "<blockquote expandable>" + modelsString + "</blockquote>";
const modelsString = listResponse.models await replyToMessage({message: msg, text, parse_mode: "HTML"});
.sort((a, b) => a.name.localeCompare(b.name))
.map(e => `${e.model}`)
.join("\n");
const text = "Доступные модели:\n\n" + "<blockquote expandable>" + modelsString + "</blockquote>";
await replyToMessage({
message: msg,
text: text,
parse_mode: "HTML"
});
} catch (e) { } catch (e) {
logError(e); logError(e);
await oldReplyToMessage(msg, "Не получилось загрузить список моделей").catch(logError); await replyToMessage({message: msg, text: Environment.modelListLoadFailedText}).catch(logError);
} }
} }
} }
-195
View File
@@ -1,195 +0,0 @@
import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api";
import {abortOllamaRequest, bot, getOllamaRequest, ollama, ollamaRequests} from "../index";
import {escapeMarkdownV2Text, logError, oldReplyToMessage, startIntervalEditor} from "../util/utils";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {Environment} from "../common/environment";
import {Cancel} from "../callback_commands/cancel";
import {OllamaCancel} from "../callback_commands/ollama-cancel";
import {MessageStore} from "../common/message-store";
export class OllamaPrompt extends Command {
command = "ollamaPrompt";
argsMode = "required" as const;
title = "/ollamaPrompt";
description = "Custom prompt for AI (Ollama)";
requirements = Requirements.Build(Requirement.BOT_ADMIN);
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("match", match);
return this.executeOllama(msg, match?.[3] || "");
}
async executeOllama(msg: Message, text: string): Promise<void> {
if (!text || !text.trim().length) return;
if (!msg.from) return;
const chatId = msg.chat.id;
let waitMessage: Message | null = null;
const startTime = Date.now();
try {
const uuid = crypto.randomUUID();
const cancelMarkup = {inline_keyboard: [[Cancel.withData(new OllamaCancel().data + " " + uuid).asButton()]]};
waitMessage = await bot.sendMessage({
chat_id: chatId,
text: Environment.waitThinkText,
reply_parameters: {
chat_id: chatId,
message_id: msg.message_id
}
});
const stream = await ollama.generate({
model: <string>Environment.OLLAMA_MODEL,
stream: true,
think: false,
prompt: text
});
const newRequest = {
uuid: uuid,
stream: stream,
done: false,
fromId: msg.from.id,
chatId: msg.chat.id,
};
console.log("Pushing new request", newRequest);
ollamaRequests.push(newRequest);
await bot.editMessageReplyMarkup(
{
chat_id: chatId,
message_id: waitMessage.message_id,
reply_markup: cancelMarkup
}
).catch(logError);
let currentText = "";
let shouldBreak = false;
const editor = startIntervalEditor({
uuid: uuid,
intervalMs: 4500,
getText: () => currentText,
editFn: async (text) => {
if (getOllamaRequest(uuid)?.done) return;
try {
await bot.editMessageText({
chat_id: chatId,
message_id: <number>waitMessage?.message_id,
text: escapeMarkdownV2Text(text),
parse_mode: "Markdown",
reply_markup: cancelMarkup
}).catch(logError);
console.log("editMessageText", text);
if (waitMessage) {
waitMessage.reply_to_message = msg;
waitMessage.text = text;
await MessageStore.put(waitMessage);
}
} catch (e) {
logError(e);
}
}
});
await editor.tick();
try {
let isThinking = false;
for await (const chunk of stream) {
const content = chunk.response;
if (content === "<think>" || chunk.thinking) {
if (!isThinking) {
await bot.editMessageText({
chat_id: chatId,
message_id: waitMessage.message_id,
text: "🤔 Размышляю...",
parse_mode: "Markdown",
}).catch(logError);
}
isThinking = true;
}
if (!isThinking) {
currentText += content;
}
if (isThinking && !chunk.thinking) {
currentText += content;
}
if (content === "</think>" || !chunk.thinking) {
isThinking = false;
}
if (currentText.length > 4096) {
currentText = currentText.slice(0, 4093) + "...";
shouldBreak = true;
}
if (getOllamaRequest(uuid)?.done) {
shouldBreak = true;
}
if (shouldBreak || chunk.done) {
console.log("messageText", currentText);
console.log("length", currentText.length);
if (shouldBreak) {
console.log("break", true);
} else {
console.log("ended", true);
}
const diff = Math.abs(Date.now() - startTime) / 1000;
await editor.tick();
await editor.stop();
console.log(`aborted request ${uuid}:`, abortOllamaRequest(uuid));
waitMessage.reply_to_message = msg;
waitMessage.text = currentText;
await MessageStore.put(waitMessage);
await oldReplyToMessage(waitMessage, `⏱️ ${diff}s`);
break;
}
}
} finally {
await bot.editMessageReplyMarkup({
chat_id: chatId,
message_id: waitMessage.message_id,
reply_markup: {inline_keyboard: []}
}).catch(logError);
}
} catch (e: any) {
if (e.message.toLowerCase().includes("aborted")) return;
logError(e);
if (waitMessage) {
await bot.editMessageReplyMarkup({
chat_id: chatId,
message_id: waitMessage.message_id,
reply_markup: {inline_keyboard: []}
}).catch(logError);
await oldReplyToMessage(waitMessage, `Произошла ошибка!\n${e.toString()}`).catch(logError);
}
}
}
}
+17 -30
View File
@@ -2,52 +2,39 @@ import {Command} from "../base/command";
import {Requirements} from "../base/requirements"; import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {bot, ollama} from "../index"; import {escapeHtml, logError, replyToMessage} from "../util/utils";
import {WebSearchResponse} from "../model/web-search-response";
import {logError, oldEditMessageText} from "../util/utils";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {createOllamaClient, resolveAiRuntimeTarget} from "../ai/ai-runtime-target";
import {AiProvider} from "../model/ai-provider";
export class OllamaSearch extends Command { export class OllamaSearch extends Command {
command = ["s", "search"]; command = ["s", "search"];
argsMode = "required" as const; argsMode = "required" as const;
title = "/search"; title = Environment.commandTitles.ollamaSearch;
description = "Web search via Ollama"; description = Environment.commandDescriptions.ollamaSearch;
override requirements = Requirements.Build(Requirement.BOT_ADMIN); override requirements = Requirements.Build(Requirement.BOT_ADMIN);
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> { async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
console.log("match", match);
const query = match?.[3] || ""; const query = match?.[3] || "";
if (!query || !query.length) return; if (!query || !query.length) return;
const chatId = msg.chat.id;
try { try {
const wait = await bot.sendMessage({ const target = resolveAiRuntimeTarget(AiProvider.OLLAMA, "chat");
chat_id: chatId, const result = await createOllamaClient(target).webSearch({query, maxResults: 10});
text: Environment.waitThinkText, const body = (result.results ?? [])
reply_parameters: { .map((item, index) => `${index + 1}. ${item.content}`)
chat_id: chatId, .join("\n\n");
message_id: msg.message_id
}, await replyToMessage({
parse_mode: "Markdown" message: msg,
text: Environment.searchResultsHeaderText + "<blockquote expandable>" + escapeHtml(body) + "</blockquote>",
parse_mode: "HTML",
}); });
const results = await ollama.webSearch({query: query});
console.log("results", results);
let message = "Результаты:\n\n";
results.results.forEach((result, index) => {
const r = result as WebSearchResponse;
message += `${index + 1}. ${r.url}\n`;
});
await oldEditMessageText(chatId, wait.message_id, message);
} catch (error) { } catch (error) {
logError(error); logError(error);
await replyToMessage({message: msg, text: Environment.errorText}).catch(logError);
} }
return Promise.resolve();
} }
} }
+15 -21
View File
@@ -1,35 +1,29 @@
import {Message} from "typescript-telegram-bot-api";
import {Command} from "../base/command"; import {Command} from "../base/command";
import {Environment} from "../common/environment";
import {logError, replyToMessage} from "../util/utils";
import {Requirements} from "../base/requirements"; import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
import {ollama} from "../index"; import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {getRuntimeModel, setRuntimeModel, formatRuntimeModelInfo} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment";
export class OllamaSetModel extends Command { export class OllamaSetModel extends Command {
argsMode = "required" as const; argsMode = "required" as const;
title = "/ollamaSetModel"; title = Environment.commandTitles.ollamaSetModel;
description = "Set Ollama model"; description = Environment.commandDescriptions.ollamaSetModel;
requirements = Requirements.Build(Requirement.BOT_CREATOR); requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> { async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
const newModel = match?.[3] || ""; const newModel = match?.[3]?.trim();
if (!newModel || !newModel.length) return; if (newModel) setRuntimeModel(AiProvider.OLLAMA, newModel);
try { const model = getRuntimeModel(AiProvider.OLLAMA);
await ollama.show({model: newModel}); const text = newModel
? Environment.getSelectedModelWithInfoText(model, await formatRuntimeModelInfo(AiProvider.OLLAMA))
: Environment.getModelIsNotSetCurrentText(model);
Environment.setOllamaModel(newModel || <string>Environment.OLLAMA_MODEL); await replyToMessage({message: msg, text}).catch(logError);
const text = newModel ? `Выбрана модель "${newModel}"`
: `Модель не задана. Будет использоваться стандартная модель "${Environment.OLLAMA_MODEL}".`;
await replyToMessage({message: msg, text: text}).catch(logError);
} catch (e: any) {
logError(e);
await replyToMessage({message: msg, text: e.toString()}).catch(logError);
}
} }
} }
+9 -156
View File
@@ -1,17 +1,10 @@
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {MessageStore} from "../common/message-store"; import {ChatCommand} from "../base/chat-command";
import {
collectReplyChainText,
escapeMarkdownV2Text,
logError,
replyToMessage,
startIntervalEditor
} from "../util/utils";
import {Environment} from "../common/environment";
import {bot, openAi} from "../index";
import {Requirements} from "../base/requirements"; import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
import {ChatCommand} from "../base/chat-command"; import {AiProvider} from "../model/ai-provider";
import {runUnifiedAi} from "../ai/unified-ai-runner";
import {Environment} from "../common/environment";
export class OpenAIChat extends ChatCommand { export class OpenAIChat extends ChatCommand {
command = ["openai", "chatgpt"]; command = ["openai", "chatgpt"];
@@ -19,154 +12,14 @@ export class OpenAIChat extends ChatCommand {
requirements = Requirements.Build(Requirement.BOT_CREATOR); requirements = Requirements.Build(Requirement.BOT_CREATOR);
title = "/openAI"; title = Environment.commandTitles.openAiChat;
description = "Chat with AI (OpenAI)"; description = Environment.commandDescriptions.openAiChat;
async execute(msg: Message, match?: RegExpExecArray): Promise<void> { async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
console.log("OpenAI Chat: ", match);
return this.executeOpenAI(msg, match?.[3] || ""); return this.executeOpenAI(msg, match?.[3] || "");
} }
async executeOpenAI(msg: Message, text: string): Promise<void> { async executeOpenAI(msg: Message, text: string, stream: boolean = true): Promise<void> {
if (!text || !text.trim().length) return; await runUnifiedAi({provider: AiProvider.OPENAI, msg, text, stream});
const chatId = msg.chat.id;
const storedMsg = await MessageStore.get(chatId, msg.message_id);
const messageParts = await collectReplyChainText(storedMsg);
console.log("MESSAGE PARTS", messageParts);
const chatMessages = messageParts.map(part => {
const content = [];
content.push({
type: part.bot ? "output_text" : "input_text",
text: (Environment.USE_NAMES_IN_PROMPT && !part.bot ? `MESSAGE FROM USER "${part.name}":\n` : "") + part.content,
});
// TODO: 03/02/2026, Danil Nikolaev: upload file then add here
// for (const image of part.images) {
// content.push({
// type: "image_url",
// imageUrl: "data:image/jpeg;base64," + image
// });
// }
return {
role: part.bot ? "assistant" : "user",
content: content,
type: "message",
};
});
chatMessages.reverse();
if (Environment.SYSTEM_PROMPT && Environment.USE_SYSTEM_PROMPT) {
chatMessages.unshift({
role: "system",
content: [{type: "input_text", text: Environment.SYSTEM_PROMPT}],
type: "message"
});
}
let waitMessage: Message | null = null;
const startTime = Date.now();
try {
waitMessage = await bot.sendMessage({
chat_id: chatId,
text: Environment.waitThinkText,
reply_parameters: {
chat_id: chatId,
message_id: msg.message_id
}
});
const stream = await openAi.responses.create({
model: Environment.OPENAI_MODEL,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
input: chatMessages as any,
stream: true
});
let currentText = "";
let shouldBreak = false;
const editor = startIntervalEditor({
intervalMs: 4500,
getText: () => currentText,
editFn: async (text) => {
await bot.editMessageText(
{
chat_id: chatId,
message_id: <number>waitMessage?.message_id,
text: escapeMarkdownV2Text(text),
parse_mode: "MarkdownV2"
}
).catch(logError);
console.log("editMessageText", text);
if (waitMessage) {
waitMessage.reply_to_message = msg;
waitMessage.text = text;
await MessageStore.put(waitMessage);
}
},
onStop: async () => {
}
});
await editor.tick();
try {
for await (const chunk of stream) {
console.log("chunk", chunk);
if (chunk.type === "response.output_text.delta") {
const text = chunk.delta;
currentText += text;
if (currentText.length > 4096) {
currentText = currentText.slice(0, 4093) + "...";
shouldBreak = true;
}
console.log("messageText", currentText);
console.log("length", currentText.length);
if (shouldBreak) {
console.log("break", true);
break;
}
}
}
} finally {
await editor.tick();
await editor.stop();
if (!shouldBreak) {
console.log("ended", true);
}
const diff = Math.abs(Date.now() - startTime) / 1000.0;
console.log("time", diff);
waitMessage.reply_to_message = msg;
waitMessage.text = currentText;
await MessageStore.put(waitMessage);
if (Environment.SEND_TIME_TOOK) {
await replyToMessage({message: waitMessage, text: `⏱️ ${diff}s`});
}
}
} catch (e: any) {
logError(e);
if (waitMessage) {
await replyToMessage({
message: waitMessage,
text: `Произошла ошибка!\n${e.toString()}`
}).catch(logError);
}
}
} }
} }
-117
View File
@@ -1,117 +0,0 @@
import {ChatCommand} from "../base/chat-command";
import {Message} from "typescript-telegram-bot-api";
import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement";
import {bot, openAi, photoGenDir} from "../index";
import fs from "node:fs";
import path from "node:path";
import {oldEditMessageText, logError, replyToMessage} from "../util/utils";
import {Environment} from "../common/environment";
import {APIError} from "openai";
export class OpenAIGenImage extends ChatCommand {
command = ["openAiGenImage", "chatGPTGenImage", "imgen"];
title = "/openAIGenImage";
description = "Generate image from OpenAI";
argsMode = "required" as const;
requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
const prompt = match?.[3]?.trim();
if (!prompt?.length) return;
let waitMessage: Message | null = null;
try {
const totalParts = 3;
const model = Environment.OPENAI_IMAGE_MODEL;
const fileFullName = `${msg.chat.id}_${msg.message_id}.png`;
const getFileLocation = (fn: string) => {
return path.join(photoGenDir, fn);
};
waitMessage = await replyToMessage({message: msg, text: "🌈 Генерирую изображение..."});
const stream = await openAi.images.generate({
model: model,
prompt: prompt,
n: 1,
size: "auto",
stream: true,
partial_images: totalParts,
moderation: "low",
output_format: "png",
});
const then = Date.now();
for await (const event of stream) {
switch (event.type) {
case "image_generation.partial_image": {
console.log(` Partial image ${event.partial_image_index + 1}/3 received`);
console.log(` Size: ${event.b64_json.length} characters (base64)`);
const fileName = `partial_${event.partial_image_index + 1}_${fileFullName}`;
const imageBuffer = Buffer.from(event.b64_json, "base64");
const fileLocation = getFileLocation(fileName);
fs.writeFileSync(fileLocation, imageBuffer);
console.log(` 💾 Saved to: ${path.resolve(fileLocation)}`);
await bot.editMessageMedia({
chat_id: msg.chat.id,
message_id: waitMessage.message_id,
media: {
type: "photo",
media: imageBuffer,
caption: `🌈 Генерирую изображение (${(event.partial_image_index + 1)}/${totalParts})...`
}
});
break;
}
case "image_generation.completed": {
console.log("\n✅ Final image completed!");
console.log(` Size: ${event.b64_json.length} characters (base64)`);
const imageBuffer = Buffer.from(event.b64_json, "base64");
const fileLocation = getFileLocation(fileFullName);
fs.writeFileSync(fileLocation, imageBuffer);
console.log(` Saved to: ${path.resolve(fileLocation)}`);
const diff = Date.now() - then;
await bot.editMessageMedia({
chat_id: msg.chat.id,
message_id: waitMessage.message_id,
media: {
type: "photo",
media: imageBuffer,
caption: `🌈 Изображение по запросу "${prompt}" сгенерировано моделью "${model}" размеров ${event.size} за ${diff}ms`
}
});
break;
}
default:
console.log(`❓ Unknown event: ${event}`);
}
}
} catch (e) {
logError(e);
if (e instanceof APIError && e.error.code === "moderation_blocked") {
const text = "❌ Мне запрещено такое генерировать 😠";
if (waitMessage) {
await oldEditMessageText(msg.chat.id, waitMessage.message_id, text).catch(logError);
} else {
await replyToMessage({message: msg, text: text}).catch(logError);
}
} else {
await replyToMessage({
message: waitMessage ? waitMessage : msg,
text: `Произошла ошибка: ${e}`
}).catch(logError);
}
}
}
}
+6 -20
View File
@@ -1,29 +1,15 @@
import {Command} from "../base/command"; import {Command} from "../base/command";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {logError, replyToMessage} from "../util/utils"; import {logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {formatRuntimeModelInfo} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment"; import {Environment} from "../common/environment";
import {AiModelCapabilities} from "../model/ai-model-capabilities";
export class OpenAIGetModel extends Command { export class OpenAIGetModel extends Command {
title = "/openAIGetModel"; title = Environment.commandTitles.openAiGetModel;
description = "Get current OpenAI model"; description = Environment.commandDescriptions.openAiGetModel;
async execute(msg: Message): Promise<void> { async execute(msg: Message): Promise<void> {
await replyToMessage({message: msg, text: `Текущая модель: "${Environment.OPENAI_MODEL}"`}).catch(logError); await replyToMessage({message: msg, text: await formatRuntimeModelInfo(AiProvider.OPENAI)}).catch(logError);
} }
}
async getModelCapabilities(): Promise<AiModelCapabilities | null> {
// TODO: 12/02/2026, Danil Nikolaev: find solution
try {
return {
vision: {supported: true},
ocr: undefined,
thinking: {supported: true},
tools: {supported: true},
};
} catch (e) {
logError(e);
return null;
}
}
}
+12 -21
View File
@@ -2,36 +2,27 @@ import {Command} from "../base/command";
import {Requirements} from "../base/requirements"; import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {openAi} from "../index"; import {escapeHtml, logError, replyToMessage} from "../util/utils";
import {logError, replyToMessage} from "../util/utils"; import {AiProvider} from "../model/ai-provider";
import {listProviderModels} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment";
export class OpenAIListModels extends Command { export class OpenAIListModels extends Command {
title = "/openAIListModels"; title = Environment.commandTitles.openAiListModels;
description = "List all OpenAI models"; description = Environment.commandDescriptions.openAiListModels;
requirements = Requirements.Build(Requirement.BOT_CREATOR); requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message): Promise<void> { async execute(msg: Message): Promise<void> {
try { try {
const listResponse = await openAi.models.list(); const models = (await listProviderModels(AiProvider.OPENAI)).sort((a, b) => a.localeCompare(b));
console.log(listResponse); const modelsString = escapeHtml(models.join("\n").substring(0, 4000));
const text = Environment.modelListHeaderText + "<blockquote expandable>" + modelsString + "</blockquote>";
const modelsString = listResponse.data await replyToMessage({message: msg, text, parse_mode: "HTML"});
.map(e => `${e.id}`)
.sort((a, b) => a.localeCompare(b))
.join("\n")
.substring(0, 4000);
const text = "Доступные модели:\n\n" + "<blockquote expandable>" + modelsString + "</blockquote>";
await replyToMessage({
message: msg,
text: text,
parse_mode: "HTML"
});
} catch (e) { } catch (e) {
logError(e); logError(e);
await replyToMessage({message: msg, text: "Не получилось загрузить список моделей"}).catch(logError); await replyToMessage({message: msg, text: Environment.modelListLoadFailedText}).catch(logError);
} }
} }
} }
+13 -9
View File
@@ -2,24 +2,28 @@ import {Command} from "../base/command";
import {Requirements} from "../base/requirements"; import {Requirements} from "../base/requirements";
import {Requirement} from "../base/requirement"; import {Requirement} from "../base/requirement";
import {Message} from "typescript-telegram-bot-api"; import {Message} from "typescript-telegram-bot-api";
import {Environment} from "../common/environment";
import {logError, replyToMessage} from "../util/utils"; import {logError, replyToMessage} from "../util/utils";
import {AiProvider} from "../model/ai-provider";
import {getRuntimeModel, setRuntimeModel, formatRuntimeModelInfo} from "../ai/provider-model-runtime";
import {Environment} from "../common/environment";
export class OpenAISetModel extends Command { export class OpenAISetModel extends Command {
argsMode = "required" as const; argsMode = "required" as const;
title = "/openAISetModel"; title = Environment.commandTitles.openAiSetModel;
description = "Set OpenAI model"; description = Environment.commandDescriptions.openAiSetModel;
requirements = Requirements.Build(Requirement.BOT_CREATOR); requirements = Requirements.Build(Requirement.BOT_CREATOR);
async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> { async execute(msg: Message, match?: RegExpExecArray | null): Promise<void> {
const newModel = match?.[3]; const newModel = match?.[3]?.trim();
Environment.setOpenAIModel(newModel || Environment.OPENAI_MODEL); if (newModel) setRuntimeModel(AiProvider.OPENAI, newModel);
const text = newModel ? `Выбрана модель "${newModel}"` const model = getRuntimeModel(AiProvider.OPENAI);
: `Модель не задана. Будет использоваться стандартная модель "${Environment.OPENAI_MODEL}".`; const text = newModel
? Environment.getSelectedModelWithInfoText(model, await formatRuntimeModelInfo(AiProvider.OPENAI))
: Environment.getModelIsNotSetCurrentText(model);
await replyToMessage({message: msg, text: text}).catch(logError); await replyToMessage({message: msg, text}).catch(logError);
} }
} }