feat(ollama): add tool calling support
Add Ollama tool integration for web search, weather, datetime, filesystem operations, and shell evaluation. Implement multi-round tool call handling, streaming updates, thinking cleanup, and safer path validation for file tools. Also add related environment configuration, command execution helper, MarkdownV2 cancel handling fixes, and remove unused TryAgain callback command.
This commit is contained in:
@@ -13,7 +13,7 @@ export class Cancel extends CallbackCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static withData(data?: string): Cancel {
|
static withData(data?: string): Cancel {
|
||||||
return new Cancel("", data);
|
return new Cancel(undefined, data);
|
||||||
}
|
}
|
||||||
|
|
||||||
async execute(): Promise<void> {
|
async execute(): Promise<void> {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import {CallbackCommand} from "../base/callback-command";
|
import {CallbackCommand} from "../base/callback-command";
|
||||||
import {CallbackQuery} from "typescript-telegram-bot-api";
|
import {CallbackQuery} from "typescript-telegram-bot-api";
|
||||||
import {abortOllamaRequest, bot, getOllamaRequest} from "../index";
|
import {abortOllamaRequest, bot, getOllamaRequest} from "../index";
|
||||||
import {logError} from "../util/utils";
|
import {escapeMarkdownV2Text, logError} from "../util/utils";
|
||||||
import {MessageStore} from "../common/message-store";
|
import {MessageStore} from "../common/message-store";
|
||||||
import {StoredMessage} from "../model/stored-message";
|
import {StoredMessage} from "../model/stored-message";
|
||||||
import {Requirements} from "../base/requirements";
|
import {Requirements} from "../base/requirements";
|
||||||
@@ -59,8 +59,8 @@ export class OllamaCancel extends CallbackCommand {
|
|||||||
await bot.editMessageText({
|
await bot.editMessageText({
|
||||||
chat_id: chatId,
|
chat_id: chatId,
|
||||||
message_id: messageId,
|
message_id: messageId,
|
||||||
text: newText,
|
text: escapeMarkdownV2Text(newText),
|
||||||
parse_mode: "Markdown",
|
parse_mode: "MarkdownV2",
|
||||||
reply_markup: {inline_keyboard: []},
|
reply_markup: {inline_keyboard: []},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,21 +0,0 @@
|
|||||||
import {CallbackCommand} from "../base/callback-command";
|
|
||||||
|
|
||||||
export class TryAgain extends CallbackCommand {
|
|
||||||
data = "";
|
|
||||||
text = "🔁 Повторить";
|
|
||||||
|
|
||||||
constructor(text?: string, data?: string) {
|
|
||||||
super();
|
|
||||||
|
|
||||||
this.text = text ?? this.text;
|
|
||||||
this.data = data ?? this.data;
|
|
||||||
}
|
|
||||||
|
|
||||||
static withData(data?: string): TryAgain {
|
|
||||||
return new TryAgain("", data);
|
|
||||||
}
|
|
||||||
|
|
||||||
async execute(): Promise<void> {
|
|
||||||
return Promise.resolve();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
+25
-5
@@ -7,6 +7,8 @@ import {Requirement} from "../base/requirement";
|
|||||||
export class Ae extends Command {
|
export class Ae extends Command {
|
||||||
argsMode = "required" as const;
|
argsMode = "required" as const;
|
||||||
|
|
||||||
|
command = ["ae"];
|
||||||
|
|
||||||
title = "/ae";
|
title = "/ae";
|
||||||
description = "evaluation";
|
description = "evaluation";
|
||||||
|
|
||||||
@@ -16,11 +18,8 @@ export class Ae extends Command {
|
|||||||
const match = params?.[3] || "";
|
const match = params?.[3] || "";
|
||||||
|
|
||||||
try {
|
try {
|
||||||
let e = eval(match);
|
let result = this.executeEvaluation(match);
|
||||||
|
await oldSendMessage(msg, result).catch(async () => await errorPlaceholder(msg));
|
||||||
e = ((typeof e == "string") ? e : JSON.stringify(e));
|
|
||||||
|
|
||||||
await oldSendMessage(msg, e).catch(async () => await errorPlaceholder(msg));
|
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
const text = e.message.toString();
|
const text = e.message.toString();
|
||||||
|
|
||||||
@@ -35,4 +34,25 @@ export class Ae extends Command {
|
|||||||
await oldSendMessage(msg, text).catch(logError);
|
await oldSendMessage(msg, text).catch(logError);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
executeEvaluation(evaluation: string): string {
|
||||||
|
try {
|
||||||
|
let e = eval(evaluation);
|
||||||
|
|
||||||
|
e = ((typeof e == "string") ? e : JSON.stringify(e));
|
||||||
|
|
||||||
|
return e;
|
||||||
|
} catch (e: any) {
|
||||||
|
const text = e.message.toString();
|
||||||
|
|
||||||
|
if (text.includes("is not defined")) {
|
||||||
|
return "Variable not defined";
|
||||||
|
}
|
||||||
|
|
||||||
|
logError(`${text}
|
||||||
|
* Stacktrace: ${e.stack}`);
|
||||||
|
|
||||||
|
return text;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
+1930
-90
File diff suppressed because it is too large
Load Diff
@@ -28,12 +28,18 @@ export class Environment {
|
|||||||
|
|
||||||
static PROCESS_LINKS: boolean;
|
static PROCESS_LINKS: boolean;
|
||||||
|
|
||||||
static DEFAULT_AI_PROVIDER: AiProvider;
|
|
||||||
|
|
||||||
static RATE_LIMIT_FALLBACK_POLICY: RateLimitFallbackPolicy;
|
static RATE_LIMIT_FALLBACK_POLICY: RateLimitFallbackPolicy;
|
||||||
static IMAGE_HANDLE_POLICY: ImageHandlePolicy;
|
static IMAGE_HANDLE_POLICY: ImageHandlePolicy;
|
||||||
static IMAGE_HANDLE_FALLBACK_POLICY: ImageHandleFallbackPolicy;
|
static IMAGE_HANDLE_FALLBACK_POLICY: ImageHandleFallbackPolicy;
|
||||||
|
|
||||||
|
static BRAVE_SEARCH_API_KEY?: string;
|
||||||
|
static OPEN_WEATHER_MAP_API_KEY?: string;
|
||||||
|
|
||||||
|
static FILE_TOOLS_ROOT_DIR?: string;
|
||||||
|
|
||||||
|
// AI Stuff
|
||||||
|
static DEFAULT_AI_PROVIDER: AiProvider;
|
||||||
|
|
||||||
static SYSTEM_PROMPT?: string;
|
static SYSTEM_PROMPT?: string;
|
||||||
static USE_NAMES_IN_PROMPT: boolean;
|
static USE_NAMES_IN_PROMPT: boolean;
|
||||||
static USE_SYSTEM_PROMPT: boolean;
|
static USE_SYSTEM_PROMPT: boolean;
|
||||||
@@ -84,13 +90,6 @@ export class Environment {
|
|||||||
|
|
||||||
Environment.PROCESS_LINKS = ifTrue(process.env.PROCESS_LINKS);
|
Environment.PROCESS_LINKS = ifTrue(process.env.PROCESS_LINKS);
|
||||||
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
const rateLimitFallbackPolicy = process.env.RATE_LIMIT_FALLBACK_POLICY || "NOTIFY_USER";
|
const rateLimitFallbackPolicy = process.env.RATE_LIMIT_FALLBACK_POLICY || "NOTIFY_USER";
|
||||||
if (Object.values(RateLimitFallbackPolicy).includes(rateLimitFallbackPolicy as RateLimitFallbackPolicy)) {
|
if (Object.values(RateLimitFallbackPolicy).includes(rateLimitFallbackPolicy as RateLimitFallbackPolicy)) {
|
||||||
Environment.RATE_LIMIT_FALLBACK_POLICY = rateLimitFallbackPolicy as RateLimitFallbackPolicy;
|
Environment.RATE_LIMIT_FALLBACK_POLICY = rateLimitFallbackPolicy as RateLimitFallbackPolicy;
|
||||||
@@ -112,6 +111,18 @@ export class Environment {
|
|||||||
Environment.IMAGE_HANDLE_FALLBACK_POLICY = ImageHandleFallbackPolicy.NOTIFY_USER;
|
Environment.IMAGE_HANDLE_FALLBACK_POLICY = ImageHandleFallbackPolicy.NOTIFY_USER;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Environment.BRAVE_SEARCH_API_KEY = process.env.BRAVE_SEARCH_API_KEY;
|
||||||
|
Environment.OPEN_WEATHER_MAP_API_KEY = process.env.OPEN_WEATHER_MAP_API_KEY;
|
||||||
|
|
||||||
|
Environment.FILE_TOOLS_ROOT_DIR = process.env.FILE_TOOLS_ROOT_DIR;
|
||||||
|
|
||||||
|
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.USE_NAMES_IN_PROMPT = ifTrue(process.env.USE_NAMES_IN_PROMPT);
|
Environment.USE_NAMES_IN_PROMPT = ifTrue(process.env.USE_NAMES_IN_PROMPT);
|
||||||
Environment.USE_SYSTEM_PROMPT = ifTrue(process.env.USE_SYSTEM_PROMPT || "true");
|
Environment.USE_SYSTEM_PROMPT = ifTrue(process.env.USE_SYSTEM_PROMPT || "true");
|
||||||
Environment.SEND_TIME_TOOK = ifTrue(process.env.SEND_TOOK_TIME || "false");
|
Environment.SEND_TIME_TOOK = ifTrue(process.env.SEND_TOOK_TIME || "false");
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ import {SendOptions} from "../model/send-options";
|
|||||||
import {EditOptions} from "../model/edit-options";
|
import {EditOptions} from "../model/edit-options";
|
||||||
import {StoredUser} from "../model/stored-user";
|
import {StoredUser} from "../model/stored-user";
|
||||||
import {performFFmpeg} from "./ffmpeg";
|
import {performFFmpeg} from "./ffmpeg";
|
||||||
|
import {exec} from "node:child_process";
|
||||||
|
|
||||||
export const ignore = () => {
|
export const ignore = () => {
|
||||||
};
|
};
|
||||||
@@ -2171,4 +2172,70 @@ export async function processInlineQuery(query: InlineQuery): Promise<void> {
|
|||||||
export async function processCallbackQuery(query: CallbackQuery): Promise<void> {
|
export async function processCallbackQuery(query: CallbackQuery): Promise<void> {
|
||||||
console.log("CallbackQuery", query);
|
console.log("CallbackQuery", query);
|
||||||
await findAndExecuteCallbackCommand(callbackCommands, query);
|
await findAndExecuteCallbackCommand(callbackCommands, query);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runCommand(cmd: string):
|
||||||
|
Promise<{
|
||||||
|
stdout: string | null | undefined;
|
||||||
|
stderr: string | null | undefined
|
||||||
|
}> {
|
||||||
|
|
||||||
|
if (cmd.length > 500) {
|
||||||
|
throw new Error("Command is too long");
|
||||||
|
}
|
||||||
|
|
||||||
|
const forbiddenPatterns = [
|
||||||
|
/\bsudo\b/,
|
||||||
|
/\bsu\b/,
|
||||||
|
/\brm\b/,
|
||||||
|
/\brmdir\b/,
|
||||||
|
/\bchmod\b/,
|
||||||
|
/\bchown\b/,
|
||||||
|
/\bdd\b/,
|
||||||
|
/\bmkfs\b/,
|
||||||
|
/\bmount\b/,
|
||||||
|
/\bumount\b/,
|
||||||
|
/\breboot\b/,
|
||||||
|
/\bshutdown\b/,
|
||||||
|
/\bkill\b/,
|
||||||
|
/\bcurl\b/,
|
||||||
|
/\bwget\b/,
|
||||||
|
/\bssh\b/,
|
||||||
|
/\bscp\b/,
|
||||||
|
/\brsync\b/,
|
||||||
|
/\bnc\b/,
|
||||||
|
/\bnmap\b/,
|
||||||
|
/\.\./,
|
||||||
|
/\/etc\/?/,
|
||||||
|
/\/home\/?/,
|
||||||
|
/\/root\/?/,
|
||||||
|
/~\//,
|
||||||
|
/\.ssh/,
|
||||||
|
/\.env/,
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const pattern of forbiddenPatterns) {
|
||||||
|
if (pattern.test(cmd)) {
|
||||||
|
throw new Error(`Forbidden shell command pattern: ${pattern}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
try {
|
||||||
|
const {stdout, stderr} = exec(cmd);
|
||||||
|
if (stdout) {
|
||||||
|
console.log("COMMAND: ", cmd, "\n", 'Output:', stdout);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (stderr) {
|
||||||
|
console.error("COMMAND: ", cmd, "\n", 'Error:', stderr);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {stdout: (await stdout?.toArray())?.join(""), stderr: (await stderr?.toArray())?.join("")}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error code:', error.code);
|
||||||
|
console.error('Stderr:', error.stderr);
|
||||||
|
|
||||||
|
return {stdout: null, stderr: error.stderr};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user