feat(ai): add configurable default provider + refactor telegram handlers
- add AiProvider enum and DEFAULT_AI_PROVIDER env var (fallback: OLLAMA) - route chat execution based on selected provider (Ollama/Gemini/Mistral/OpenAI) - move inline query / callback / edited message / my_chat_member handlers into utils - minor cleanup (command requirements placement, whitespace)
This commit is contained in:
@@ -18,11 +18,11 @@ export class GeminiChat extends ChatCommand {
|
|||||||
command = "gemini";
|
command = "gemini";
|
||||||
argsMode = "required" as const;
|
argsMode = "required" as const;
|
||||||
|
|
||||||
|
requirements = Requirements.Build(Requirement.BOT_CREATOR);
|
||||||
|
|
||||||
title = "/gemini";
|
title = "/gemini";
|
||||||
description = "Chat with AI (Gemini)";
|
description = "Chat with AI (Gemini)";
|
||||||
|
|
||||||
requirements = Requirements.Build(Requirement.BOT_CREATOR);
|
|
||||||
|
|
||||||
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
|
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
|
||||||
console.log("match", match);
|
console.log("match", match);
|
||||||
return this.executeGemini(msg, match?.[3]);
|
return this.executeGemini(msg, match?.[3]);
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ export class MistralChat extends ChatCommand {
|
|||||||
command = "mistral";
|
command = "mistral";
|
||||||
argsMode = "required" as const;
|
argsMode = "required" as const;
|
||||||
|
|
||||||
|
requirements = Requirements.Build(Requirement.BOT_CREATOR);
|
||||||
|
|
||||||
title = "/mistral";
|
title = "/mistral";
|
||||||
description = "Chat with AI (Mistral)";
|
description = "Chat with AI (Mistral)";
|
||||||
|
|
||||||
requirements = Requirements.Build(Requirement.BOT_CREATOR);
|
|
||||||
|
|
||||||
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
|
async execute(msg: Message, match?: RegExpExecArray): Promise<void> {
|
||||||
console.log("match", match);
|
console.log("match", match);
|
||||||
return this.executeMistral(msg, match?.[3]);
|
return this.executeMistral(msg, match?.[3]);
|
||||||
|
|||||||
@@ -133,8 +133,6 @@ export class OpenAIChat extends ChatCommand {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
await editor.tick();
|
await editor.tick();
|
||||||
|
|||||||
@@ -3,6 +3,13 @@ import {saveData} from "../db/database";
|
|||||||
import {Answers} from "../model/answers";
|
import {Answers} from "../model/answers";
|
||||||
import {ifTrue} from "../util/utils";
|
import {ifTrue} from "../util/utils";
|
||||||
|
|
||||||
|
export enum AiProvider {
|
||||||
|
OLLAMA = "OLLAMA",
|
||||||
|
GEMINI = "GEMINI",
|
||||||
|
MISTRAL = "MISTRAL",
|
||||||
|
OPENAI = "OPENAI",
|
||||||
|
}
|
||||||
|
|
||||||
export class Environment {
|
export class Environment {
|
||||||
static BOT_TOKEN: string;
|
static BOT_TOKEN: string;
|
||||||
static TEST_ENVIRONMENT: boolean;
|
static TEST_ENVIRONMENT: boolean;
|
||||||
@@ -24,6 +31,8 @@ export class Environment {
|
|||||||
|
|
||||||
static MAX_PHOTO_SIZE: number;
|
static MAX_PHOTO_SIZE: number;
|
||||||
|
|
||||||
|
static DEFAULT_AI_PROVIDER: AiProvider;
|
||||||
|
|
||||||
static SYSTEM_PROMPT?: string;
|
static SYSTEM_PROMPT?: string;
|
||||||
|
|
||||||
static OLLAMA_ADDRESS?: string;
|
static OLLAMA_ADDRESS?: string;
|
||||||
@@ -65,6 +74,13 @@ export class Environment {
|
|||||||
|
|
||||||
Environment.MAX_PHOTO_SIZE = Number(process.env.MAX_PHOTO_SIZE || "1280");
|
Environment.MAX_PHOTO_SIZE = Number(process.env.MAX_PHOTO_SIZE || "1280");
|
||||||
|
|
||||||
|
const aiProvider = process.env.DEFAULT_AI_PROVIDER || "OLLAMA";
|
||||||
|
if (Object.values(AiProvider).includes(aiProvider as AiProvider)) {
|
||||||
|
Environment.DEFAULT_AI_PROVIDER = aiProvider as AiProvider;
|
||||||
|
} else {
|
||||||
|
Environment.DEFAULT_AI_PROVIDER = AiProvider.OLLAMA;
|
||||||
|
}
|
||||||
|
|
||||||
Environment.SYSTEM_PROMPT = process.env.SYSTEM_PROMPT?.trim();
|
Environment.SYSTEM_PROMPT = process.env.SYSTEM_PROMPT?.trim();
|
||||||
|
|
||||||
Environment.OLLAMA_ADDRESS = process.env.OLLAMA_ADDRESS;
|
Environment.OLLAMA_ADDRESS = process.env.OLLAMA_ADDRESS;
|
||||||
|
|||||||
+11
-74
@@ -1,14 +1,16 @@
|
|||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import {Environment} from "./common/environment";
|
import {Environment} from "./common/environment";
|
||||||
import {InlineQueryResult, TelegramBot, User} from "typescript-telegram-bot-api";
|
import {TelegramBot, User} from "typescript-telegram-bot-api";
|
||||||
import {Command} from "./base/command";
|
import {Command} from "./base/command";
|
||||||
import {
|
import {
|
||||||
delay,
|
delay,
|
||||||
extractTextMessage,
|
|
||||||
findAndExecuteCallbackCommand,
|
|
||||||
ignore,
|
ignore,
|
||||||
initSystemSpecs,
|
initSystemSpecs,
|
||||||
logError,
|
logError,
|
||||||
|
processCallbackQuery,
|
||||||
|
processEditedMessage,
|
||||||
|
processInlineQuery,
|
||||||
|
processMyChatMember,
|
||||||
processNewMessage
|
processNewMessage
|
||||||
} from "./util/utils";
|
} from "./util/utils";
|
||||||
import {Ae} from "./commands/ae";
|
import {Ae} from "./commands/ae";
|
||||||
@@ -27,7 +29,6 @@ import {RandomInt} from "./commands/random-int";
|
|||||||
import {Ban} from "./commands/ban";
|
import {Ban} from "./commands/ban";
|
||||||
import {Quote} from "./commands/quote";
|
import {Quote} from "./commands/quote";
|
||||||
import {Ollama} from "ollama";
|
import {Ollama} from "ollama";
|
||||||
import {WebSearchResponse} from "./model/web-search-response";
|
|
||||||
import {OllamaSearch} from "./commands/ollama-search";
|
import {OllamaSearch} from "./commands/ollama-search";
|
||||||
import {Id} from "./commands/id";
|
import {Id} from "./commands/id";
|
||||||
import {OllamaPrompt} from "./commands/ollama-prompt";
|
import {OllamaPrompt} from "./commands/ollama-prompt";
|
||||||
@@ -37,7 +38,6 @@ import {Shutdown} from "./commands/shutdown";
|
|||||||
import {Leave} from "./commands/leave";
|
import {Leave} from "./commands/leave";
|
||||||
import {OllamaChat} from "./commands/ollama-chat";
|
import {OllamaChat} from "./commands/ollama-chat";
|
||||||
import {Start} from "./commands/start";
|
import {Start} from "./commands/start";
|
||||||
import {MessageStore} from "./common/message-store";
|
|
||||||
import {GeminiChat} from "./commands/gemini-chat";
|
import {GeminiChat} from "./commands/gemini-chat";
|
||||||
import {Choice} from "./commands/choice";
|
import {Choice} from "./commands/choice";
|
||||||
import {Coin} from "./commands/coin";
|
import {Coin} from "./commands/coin";
|
||||||
@@ -221,7 +221,8 @@ async function main() {
|
|||||||
`TEST_ENVIRONMENT: ${Environment.TEST_ENVIRONMENT}\n` +
|
`TEST_ENVIRONMENT: ${Environment.TEST_ENVIRONMENT}\n` +
|
||||||
`DATA_PATH: ${Environment.DATA_PATH}\n` +
|
`DATA_PATH: ${Environment.DATA_PATH}\n` +
|
||||||
`MAX_PHOTO_SIZE: ${Environment.MAX_PHOTO_SIZE}\n` +
|
`MAX_PHOTO_SIZE: ${Environment.MAX_PHOTO_SIZE}\n` +
|
||||||
`ONLY_FOR_CREATOR: ${Environment.ONLY_FOR_CREATOR_MODE}`
|
`ONLY_FOR_CREATOR: ${Environment.ONLY_FOR_CREATOR_MODE}\n` +
|
||||||
|
`DEFAULT_AI_PROVIDER: ${Environment.DEFAULT_AI_PROVIDER}`
|
||||||
);
|
);
|
||||||
|
|
||||||
fs.mkdir(photoDir, ignore);
|
fs.mkdir(photoDir, ignore);
|
||||||
@@ -272,74 +273,10 @@ async function main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
bot.on("my_chat_member", async (u) => {
|
bot.on("my_chat_member", processMyChatMember);
|
||||||
console.log("my_chat_member", u);
|
bot.on("edited_message", processEditedMessage);
|
||||||
});
|
|
||||||
|
|
||||||
bot.on("edited_message", async (msg) => {
|
|
||||||
console.log("edited_message", msg);
|
|
||||||
|
|
||||||
await UserStore.put(msg.from);
|
|
||||||
|
|
||||||
if (!extractTextMessage(msg) || msg.from.id === botUser.id) return;
|
|
||||||
|
|
||||||
await MessageStore.put(msg);
|
|
||||||
});
|
|
||||||
|
|
||||||
bot.on("message", processNewMessage);
|
bot.on("message", processNewMessage);
|
||||||
|
bot.on("inline_query", processInlineQuery);
|
||||||
bot.on("inline_query", async (query) => {
|
bot.on("callback_query", processCallbackQuery);
|
||||||
console.log("query", query);
|
|
||||||
|
|
||||||
if (Environment.CREATOR_ID !== query.from.id) {
|
|
||||||
await bot.answerInlineQuery({
|
|
||||||
inline_query_id: query.id,
|
|
||||||
results: [],
|
|
||||||
button: {
|
|
||||||
text: "No access",
|
|
||||||
start_parameter: "nope"
|
|
||||||
}
|
|
||||||
}).catch(logError);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (query.query.trim().length !== 0) {
|
|
||||||
try {
|
|
||||||
const queryResults: InlineQueryResult[] = [];
|
|
||||||
const results = await ollama.webSearch({query: query.query});
|
|
||||||
|
|
||||||
console.log("results", results);
|
|
||||||
|
|
||||||
results.results.forEach((result, i) => {
|
|
||||||
const r = result as WebSearchResponse;
|
|
||||||
queryResults.push({
|
|
||||||
type: "article",
|
|
||||||
id: `${i}`,
|
|
||||||
title: `${r.title}`,
|
|
||||||
input_message_content: {
|
|
||||||
message_text: `${r.title}\n\n${r.url}`
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await bot.answerInlineQuery({
|
|
||||||
inline_query_id: query.id,
|
|
||||||
results: queryResults,
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logError(e);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await bot.answerInlineQuery({
|
|
||||||
inline_query_id: query.id,
|
|
||||||
results: [],
|
|
||||||
}).catch(logError);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
bot.on("callback_query", async (query) => {
|
|
||||||
console.log(query);
|
|
||||||
await findAndExecuteCallbackCommand(callbackCommands, query);
|
|
||||||
});
|
|
||||||
|
|
||||||
main().catch(logError);
|
main().catch(logError);
|
||||||
+111
-22
@@ -4,15 +4,18 @@ import {CallbackCommand} from "../base/callback-command";
|
|||||||
import {
|
import {
|
||||||
CallbackQuery,
|
CallbackQuery,
|
||||||
ChatMember,
|
ChatMember,
|
||||||
|
ChatMemberUpdated,
|
||||||
InlineKeyboardMarkup,
|
InlineKeyboardMarkup,
|
||||||
|
InlineQuery,
|
||||||
|
InlineQueryResult,
|
||||||
Message,
|
Message,
|
||||||
ParseMode,
|
ParseMode,
|
||||||
PhotoSize,
|
PhotoSize,
|
||||||
User
|
User
|
||||||
} from "typescript-telegram-bot-api";
|
} from "typescript-telegram-bot-api";
|
||||||
import {Environment} from "../common/environment";
|
import {AiProvider, Environment} from "../common/environment";
|
||||||
import {TelegramError} from "typescript-telegram-bot-api/dist/errors";
|
import {TelegramError} from "typescript-telegram-bot-api/dist/errors";
|
||||||
import {bot, botUser, commands, messageDao} from "../index";
|
import {bot, botUser, callbackCommands, commands, messageDao, ollama} from "../index";
|
||||||
import os from "os";
|
import os from "os";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import {MessagePart} from "../common/message-part";
|
import {MessagePart} from "../common/message-part";
|
||||||
@@ -30,6 +33,10 @@ import {OllamaChat} from "../commands/ollama-chat";
|
|||||||
import {getYouTubeVideoId} from "./ytdl";
|
import {getYouTubeVideoId} from "./ytdl";
|
||||||
import {YouTubeDownload} from "../commands/youtube-download";
|
import {YouTubeDownload} from "../commands/youtube-download";
|
||||||
import {ChatCommand} from "../base/chat-command";
|
import {ChatCommand} from "../base/chat-command";
|
||||||
|
import {WebSearchResponse} from "../model/web-search-response";
|
||||||
|
import {GeminiChat} from "../commands/gemini-chat";
|
||||||
|
import {MistralChat} from "../commands/mistral-chat";
|
||||||
|
import {OpenAIChat} from "../commands/openai-chat";
|
||||||
|
|
||||||
export const ignore = () => {
|
export const ignore = () => {
|
||||||
};
|
};
|
||||||
@@ -1028,8 +1035,34 @@ export function boolToEmoji(bool: boolean): string {
|
|||||||
|
|
||||||
export const albumCache = new Map<string, { messages: Message[], timer: NodeJS.Timeout }>();
|
export const albumCache = new Map<string, { messages: Message[], timer: NodeJS.Timeout }>();
|
||||||
|
|
||||||
export async function processNewMessage(msg: Message) {
|
async function processAlbum(groupId: string): Promise<string[]> {
|
||||||
console.log("message", msg);
|
const entry = albumCache.get(groupId);
|
||||||
|
if (!entry) return;
|
||||||
|
|
||||||
|
const allPhotos = entry.messages
|
||||||
|
.filter(m => m.photo)
|
||||||
|
.map(m => m.photo);
|
||||||
|
|
||||||
|
const allPhotoMaxSizes = await Promise.all(allPhotos.map(photo => getPhotoMaxSize(photo)));
|
||||||
|
const ids = await loadImagesFromFileIds(allPhotoMaxSizes);
|
||||||
|
|
||||||
|
console.log(`Received album ${groupId} with ${ids.length} photos.`);
|
||||||
|
console.log("File IDs:", ids);
|
||||||
|
|
||||||
|
albumCache.delete(groupId);
|
||||||
|
return ids;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function photoPathByUniqueId(uniqueId: string): string {
|
||||||
|
return path.join(Environment.DATA_PATH, "photo", uniqueId + ".jpg");
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processMyChatMember(u: ChatMemberUpdated): Promise<void> {
|
||||||
|
console.log("my_chat_member", u);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processNewMessage(msg: Message): Promise<void> {
|
||||||
|
console.log("New Message", msg);
|
||||||
|
|
||||||
let storedMsg: StoredMessage | null = null;
|
let storedMsg: StoredMessage | null = null;
|
||||||
|
|
||||||
@@ -1133,30 +1166,86 @@ export async function processNewMessage(msg: Message) {
|
|||||||
if (!startsWithPrefix && msg.chat.type !== "private") return;
|
if (!startsWithPrefix && msg.chat.type !== "private") return;
|
||||||
if (msg.chat.type === "private" && !Environment.ADMIN_IDS.has(msg.chat.id)) return;
|
if (msg.chat.type === "private" && !Environment.ADMIN_IDS.has(msg.chat.id)) return;
|
||||||
|
|
||||||
const chat = commands.find(e => e instanceof OllamaChat);
|
switch (Environment.DEFAULT_AI_PROVIDER) {
|
||||||
if (await checkRequirements(chat, msg)) {
|
case AiProvider.OLLAMA: {
|
||||||
await chat.executeOllama(msg, textToCheck);
|
await commands.find(e => e instanceof OllamaChat).executeOllama(msg, textToCheck);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AiProvider.GEMINI: {
|
||||||
|
await commands.find(e => e instanceof GeminiChat).executeGemini(msg, textToCheck);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AiProvider.MISTRAL: {
|
||||||
|
await commands.find(e => e instanceof MistralChat).executeMistral(msg, textToCheck);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case AiProvider.OPENAI: {
|
||||||
|
await commands.find(e => e instanceof OpenAIChat).executeOpenAI(msg, textToCheck);
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function processAlbum(groupId: string): Promise<string[]> {
|
export async function processEditedMessage(msg: Message): Promise<void> {
|
||||||
const entry = albumCache.get(groupId);
|
console.log("Edited Message", msg);
|
||||||
if (!entry) return;
|
|
||||||
|
|
||||||
const allPhotos = entry.messages
|
await UserStore.put(msg.from);
|
||||||
.filter(m => m.photo)
|
|
||||||
.map(m => m.photo);
|
|
||||||
|
|
||||||
const allPhotoMaxSizes = await Promise.all(allPhotos.map(photo => getPhotoMaxSize(photo)));
|
if (!extractTextMessage(msg) || msg.from.id === botUser.id) return;
|
||||||
const ids = await loadImagesFromFileIds(allPhotoMaxSizes);
|
|
||||||
|
|
||||||
console.log(`Received album ${groupId} with ${ids.length} photos.`);
|
await MessageStore.put(msg);
|
||||||
console.log("File IDs:", ids);
|
|
||||||
|
|
||||||
albumCache.delete(groupId);
|
|
||||||
return ids;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function photoPathByUniqueId(uniqueId: string): string {
|
export async function processInlineQuery(query: InlineQuery): Promise<void> {
|
||||||
return path.join(Environment.DATA_PATH, "photo", uniqueId + ".jpg");
|
console.log("InlineQuery", query);
|
||||||
|
|
||||||
|
if (Environment.CREATOR_ID !== query.from.id) {
|
||||||
|
await bot.answerInlineQuery({
|
||||||
|
inline_query_id: query.id,
|
||||||
|
results: [],
|
||||||
|
button: {
|
||||||
|
text: "No access",
|
||||||
|
start_parameter: "nope"
|
||||||
|
}
|
||||||
|
}).catch(logError);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (query.query.trim().length !== 0) {
|
||||||
|
try {
|
||||||
|
const queryResults: InlineQueryResult[] = [];
|
||||||
|
const results = await ollama.webSearch({query: query.query});
|
||||||
|
|
||||||
|
console.log("results", results);
|
||||||
|
|
||||||
|
results.results.forEach((result, i) => {
|
||||||
|
const r = result as WebSearchResponse;
|
||||||
|
queryResults.push({
|
||||||
|
type: "article",
|
||||||
|
id: `${i}`,
|
||||||
|
title: `${r.title}`,
|
||||||
|
input_message_content: {
|
||||||
|
message_text: `${r.title}\n\n${r.url}`
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
await bot.answerInlineQuery({
|
||||||
|
inline_query_id: query.id,
|
||||||
|
results: queryResults,
|
||||||
|
});
|
||||||
|
} catch (e) {
|
||||||
|
logError(e);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await bot.answerInlineQuery({
|
||||||
|
inline_query_id: query.id,
|
||||||
|
results: [],
|
||||||
|
}).catch(logError);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function processCallbackQuery(query: CallbackQuery): Promise<void> {
|
||||||
|
console.log("CallbackQuery", query);
|
||||||
|
await findAndExecuteCallbackCommand(callbackCommands, query);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user