Compare commits
2 Commits
c1a913d5e4
...
9b367f29c5
| Author | SHA1 | Date | |
|---|---|---|---|
| 9b367f29c5 | |||
| 2b1940bf4d |
@@ -374,7 +374,7 @@ export async function sendSynthesizedSpeech(sourceMessage: Message, speech: Synt
|
|||||||
reply_parameters: {message_id: sourceMessage.message_id},
|
reply_parameters: {message_id: sourceMessage.message_id},
|
||||||
});
|
});
|
||||||
} finally {
|
} finally {
|
||||||
destroyUpload(upload);
|
// destroyUpload(upload);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{method: "sendVoice", chatId: sourceMessage.chat.id, chatType: sourceMessage.chat.type}
|
{method: "sendVoice", chatId: sourceMessage.chat.id, chatType: sourceMessage.chat.type}
|
||||||
|
|||||||
+16
-16
@@ -1,13 +1,13 @@
|
|||||||
import {AiTool} from "./tool-types";
|
import {AiTool} from "./tool-types";
|
||||||
import {AiProvider} from "../model/ai-provider";
|
import {AiProvider} from "../model/ai-provider";
|
||||||
import {getTools} from "./tools/registry";
|
import {getTools} from "./tools/registry";
|
||||||
import {WEB_SEARCH_TOOL_NAME} from "./tools/brave-search";
|
import {WEB_SEARCH_TOOL_NAME} from "./tools/web-search";
|
||||||
import {PYTHON_INTERPRETER_TOOL_NAME} from "./tools/python-interpretator";
|
import {PYTHON_INTERPRETER_TOOL_NAME} from "./tools/python-interpretator";
|
||||||
|
|
||||||
export type AiProviderName = "ollama" | "openai" | "gemini" | "mistral";
|
export type AiProviderName = "ollama" | "openai" | "gemini" | "mistral";
|
||||||
|
|
||||||
export function getOllamaTools(): AiTool[] {
|
export function getOllamaTools(forCreator?: boolean): AiTool[] {
|
||||||
return getTools();
|
return getTools(forCreator);
|
||||||
}
|
}
|
||||||
|
|
||||||
const openAiForbiddenTools = [
|
const openAiForbiddenTools = [
|
||||||
@@ -19,8 +19,8 @@ function allowedOpenAiTool(tool: AiTool): boolean {
|
|||||||
return !openAiForbiddenTools.includes(tool.function.name)
|
return !openAiForbiddenTools.includes(tool.function.name)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOpenAITools(): AiTool[] {
|
export function getOpenAITools(forCreator?: boolean): AiTool[] {
|
||||||
return getTools().filter(allowedOpenAiTool).map(tool => ({
|
return getTools(forCreator).filter(allowedOpenAiTool).map(tool => ({
|
||||||
type: "function",
|
type: "function",
|
||||||
function: tool.function,
|
function: tool.function,
|
||||||
}));
|
}));
|
||||||
@@ -43,8 +43,8 @@ export type OpenAiCodeInterpreterTool = {
|
|||||||
} | string;
|
} | string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function getOpenAIResponsesTools(): OpenAiResponseTool[] {
|
export function getOpenAIResponsesTools(forCreator?: boolean): OpenAiResponseTool[] {
|
||||||
return getTools().filter(allowedOpenAiTool).map(tool => ({
|
return getTools(forCreator).filter(allowedOpenAiTool).map(tool => ({
|
||||||
type: "function",
|
type: "function",
|
||||||
name: tool.function.name,
|
name: tool.function.name,
|
||||||
description: tool.function.description,
|
description: tool.function.description,
|
||||||
@@ -62,8 +62,8 @@ export function getOpenAICodeInterpreterTool(): OpenAiCodeInterpreterTool {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getMistralTools(): AiTool[] {
|
export function getMistralTools(forCreator?: boolean): AiTool[] {
|
||||||
return getTools().map(tool => ({
|
return getTools(forCreator).map(tool => ({
|
||||||
type: "function",
|
type: "function",
|
||||||
function: tool.function,
|
function: tool.function,
|
||||||
}));
|
}));
|
||||||
@@ -77,8 +77,8 @@ export type GeminiTool = {
|
|||||||
}>;
|
}>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getGeminiTools(): GeminiTool[] {
|
export function getGeminiTools(forCreator?: boolean): GeminiTool[] {
|
||||||
const functionDeclarations = getTools().map(tool => ({
|
const functionDeclarations = getTools(forCreator).map(tool => ({
|
||||||
name: tool.function.name,
|
name: tool.function.name,
|
||||||
description: tool.function.description,
|
description: tool.function.description,
|
||||||
parametersJsonSchema: tool.function.parameters,
|
parametersJsonSchema: tool.function.parameters,
|
||||||
@@ -87,15 +87,15 @@ export function getGeminiTools(): GeminiTool[] {
|
|||||||
return functionDeclarations.length ? [{functionDeclarations}] : [];
|
return functionDeclarations.length ? [{functionDeclarations}] : [];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getProviderTools(provider: AiProvider): AiTool[] {
|
export function getProviderTools(provider: AiProvider, forCreator?: boolean): AiTool[] {
|
||||||
switch (provider) {
|
switch (provider) {
|
||||||
case AiProvider.OLLAMA:
|
case AiProvider.OLLAMA:
|
||||||
return getOllamaTools();
|
return getOllamaTools(forCreator);
|
||||||
case AiProvider.GEMINI:
|
case AiProvider.GEMINI:
|
||||||
return getTools();
|
return getTools(forCreator);
|
||||||
case AiProvider.MISTRAL:
|
case AiProvider.MISTRAL:
|
||||||
return getMistralTools();
|
return getMistralTools(forCreator);
|
||||||
case AiProvider.OPENAI:
|
case AiProvider.OPENAI:
|
||||||
return getOpenAITools();
|
return getOpenAITools(forCreator);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,852 +0,0 @@
|
|||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import {Environment} from "../../common/environment";
|
|
||||||
import {AiTool} from "../tool-types";
|
|
||||||
import {MAX_COPY_ENTRIES, MAX_COPY_TOTAL_BYTES, MAX_DIRECTORY_ENTRIES, MAX_FILE_READ_BYTES, MAX_FILE_WRITE_BYTES} from "./limits";
|
|
||||||
import {asBoolean, asNonEmptyString, asPositiveInt, asString} from "./utils";
|
|
||||||
|
|
||||||
export const readFileTool = {
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "read_file",
|
|
||||||
description:
|
|
||||||
"Read a UTF-8 text file inside the hardcoded root directory. Use only relative paths. Going up with ../ and absolute paths are forbidden.",
|
|
||||||
parameters: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
path: {
|
|
||||||
type: "string",
|
|
||||||
description: "Relative file path inside the root directory, for example notes/task.txt.",
|
|
||||||
},
|
|
||||||
maxBytes: {
|
|
||||||
type: "number",
|
|
||||||
description: `Optional max bytes to read. Maximum allowed value is ${MAX_FILE_READ_BYTES}.`,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["path"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies AiTool;
|
|
||||||
|
|
||||||
export const listDirectoryTool = {
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "list_directory",
|
|
||||||
description:
|
|
||||||
"List files and directories inside the hardcoded root directory. Use only relative paths. Going up with ../ and absolute paths are forbidden.",
|
|
||||||
parameters: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
path: {
|
|
||||||
type: "string",
|
|
||||||
description: "Relative directory path inside the root directory. Use . for root.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies AiTool;
|
|
||||||
|
|
||||||
export const createFileTool = {
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "create_file",
|
|
||||||
description:
|
|
||||||
"Create a UTF-8 text file inside the hardcoded root directory. Use only relative paths. Going up with ../ and absolute paths are forbidden.",
|
|
||||||
parameters: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
path: {
|
|
||||||
type: "string",
|
|
||||||
description: "Relative file path inside the root directory.",
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
type: "string",
|
|
||||||
description: "File content.",
|
|
||||||
},
|
|
||||||
overwrite: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "Whether to overwrite the file if it already exists. Default is false.",
|
|
||||||
},
|
|
||||||
createParents: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "Whether to create parent directories automatically. Default is true.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["path"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies AiTool;
|
|
||||||
|
|
||||||
export const createDirectoryTool = {
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "create_directory",
|
|
||||||
description:
|
|
||||||
"Create a directory inside the hardcoded root directory. Use only relative paths. Going up with ../ and absolute paths are forbidden.",
|
|
||||||
parameters: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
path: {
|
|
||||||
type: "string",
|
|
||||||
description: "Relative directory path inside the root directory.",
|
|
||||||
},
|
|
||||||
recursive: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "Whether to create parent directories automatically. Default is true.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["path"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies AiTool;
|
|
||||||
|
|
||||||
export const copyPathTool = {
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "copy_path",
|
|
||||||
description:
|
|
||||||
"Copy a file or directory inside the hardcoded root directory. Use only relative paths. Going up with ../ and absolute paths are forbidden. Directory copy requires recursive=true. Symlinks are forbidden.",
|
|
||||||
parameters: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
sourcePath: {
|
|
||||||
type: "string",
|
|
||||||
description: "Relative source file or directory path inside the root directory.",
|
|
||||||
},
|
|
||||||
targetPath: {
|
|
||||||
type: "string",
|
|
||||||
description: "Relative target file or directory path inside the root directory.",
|
|
||||||
},
|
|
||||||
recursive: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "Required for copying directories. Default is false.",
|
|
||||||
},
|
|
||||||
overwrite: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "Whether to overwrite existing files. Directory merge is allowed, but existing directories are not deleted. Default is false.",
|
|
||||||
},
|
|
||||||
createParents: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "Whether to create target parent directories automatically. Default is true.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["sourcePath", "targetPath"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies AiTool;
|
|
||||||
|
|
||||||
export const updateFileTool = {
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "update_file",
|
|
||||||
description:
|
|
||||||
"Update a UTF-8 text file inside the hardcoded root directory. Supports replace, append and prepend. Use only relative paths. Going up with ../ and absolute paths are forbidden.",
|
|
||||||
parameters: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
path: {
|
|
||||||
type: "string",
|
|
||||||
description: "Relative file path inside the root directory.",
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
type: "string",
|
|
||||||
description: "Content to write.",
|
|
||||||
},
|
|
||||||
mode: {
|
|
||||||
type: "string",
|
|
||||||
enum: ["replace", "append", "prepend"],
|
|
||||||
description: "Update mode. Default is replace.",
|
|
||||||
},
|
|
||||||
createIfMissing: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "Whether to create the file if it does not exist. Default is false.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["path", "content"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies AiTool;
|
|
||||||
|
|
||||||
export const renamePathTool = {
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "rename_path",
|
|
||||||
description:
|
|
||||||
"Rename or move a file/directory inside the hardcoded root directory. This is the main directory modification tool. Use only relative paths. Going up with ../ and absolute paths are forbidden.",
|
|
||||||
parameters: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
sourcePath: {
|
|
||||||
type: "string",
|
|
||||||
description: "Relative source path inside the root directory.",
|
|
||||||
},
|
|
||||||
targetPath: {
|
|
||||||
type: "string",
|
|
||||||
description: "Relative target path inside the root directory.",
|
|
||||||
},
|
|
||||||
overwrite: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "Whether to overwrite an existing target file. Directory overwrite is not supported. Default is false.",
|
|
||||||
},
|
|
||||||
createParents: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "Whether to create target parent directories automatically. Default is false.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["sourcePath", "targetPath"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies AiTool;
|
|
||||||
|
|
||||||
export const deletePathTool = {
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "delete_path",
|
|
||||||
description:
|
|
||||||
"Delete a file or directory inside the hardcoded root directory. Use only relative paths. Going up with ../ and absolute paths are forbidden. Recursive deletion requires recursive=true.",
|
|
||||||
parameters: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
path: {
|
|
||||||
type: "string",
|
|
||||||
description: "Relative file or directory path inside the root directory.",
|
|
||||||
},
|
|
||||||
recursive: {
|
|
||||||
type: "boolean",
|
|
||||||
description: "Whether to delete non-empty directories recursively. Default is false.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["path"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies AiTool;
|
|
||||||
|
|
||||||
export const fileToolsToolPrompt = [
|
|
||||||
"Filesystem tool rules:",
|
|
||||||
"- You have access to filesystem tools working only inside the hardcoded root directory.",
|
|
||||||
"- All filesystem paths must be relative to the root directory.",
|
|
||||||
"- You may go into child directories.",
|
|
||||||
"- You must never go up to parent directories.",
|
|
||||||
"- Do not use ../ paths.",
|
|
||||||
"- Do not use absolute paths.",
|
|
||||||
"- Do not try to access symlinks.",
|
|
||||||
"- Use read_file for reading files.",
|
|
||||||
"- Use list_directory for reading directories.",
|
|
||||||
"- Use create_file for creating files.",
|
|
||||||
"- Use create_directory for creating directories.",
|
|
||||||
"- Use update_file for replacing, appending or prepending file content.",
|
|
||||||
"- Use rename_path for renaming or moving files/directories inside the root.",
|
|
||||||
"- Use delete_path for deleting files/directories inside the root.",
|
|
||||||
""
|
|
||||||
].join("\n");
|
|
||||||
|
|
||||||
const requireFileToolsRootDir = () => <string>Environment.FILE_TOOLS_ROOT_DIR;
|
|
||||||
|
|
||||||
async function ensureFileToolsRootExists(): Promise<void> {
|
|
||||||
await fs.promises.mkdir(requireFileToolsRootDir(), {recursive: true});
|
|
||||||
|
|
||||||
const stat = await fs.promises.stat(requireFileToolsRootDir());
|
|
||||||
|
|
||||||
if (!stat.isDirectory()) {
|
|
||||||
throw new Error(`File tools root is not a directory: ${requireFileToolsRootDir()}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function resolveSafeToolPath(inputPath: unknown, fallback = "."): {
|
|
||||||
absolutePath: string;
|
|
||||||
relativePath: string;
|
|
||||||
} {
|
|
||||||
const rawPath = asNonEmptyString(inputPath) ?? fallback;
|
|
||||||
|
|
||||||
if (rawPath.includes("\0")) {
|
|
||||||
throw new Error("Path must not contain null bytes.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (
|
|
||||||
path.isAbsolute(rawPath) ||
|
|
||||||
path.win32.isAbsolute(rawPath) ||
|
|
||||||
path.posix.isAbsolute(rawPath)
|
|
||||||
) {
|
|
||||||
throw new Error("Absolute paths are not allowed. Use only relative paths inside the root directory.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedInputPath = rawPath.replace(/[\\/]+/g, path.sep);
|
|
||||||
|
|
||||||
const absolutePath = path.resolve(requireFileToolsRootDir(), normalizedInputPath);
|
|
||||||
const relativePath = path.relative(requireFileToolsRootDir(), absolutePath);
|
|
||||||
|
|
||||||
if (
|
|
||||||
relativePath.startsWith("..") ||
|
|
||||||
path.isAbsolute(relativePath)
|
|
||||||
) {
|
|
||||||
throw new Error("Path escapes the root directory. Going up is not allowed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
absolutePath,
|
|
||||||
relativePath: relativePath || ".",
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertTargetIsNotInsideSource(sourceAbsolutePath: string, targetAbsolutePath: string): void {
|
|
||||||
const relative = path.relative(sourceAbsolutePath, targetAbsolutePath);
|
|
||||||
|
|
||||||
if (
|
|
||||||
relative === "" ||
|
|
||||||
(!relative.startsWith("..") && !path.isAbsolute(relative))
|
|
||||||
) {
|
|
||||||
throw new Error("Cannot copy a directory into itself.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function assertNoSymlinkInPath(
|
|
||||||
absolutePath: string,
|
|
||||||
options?: {
|
|
||||||
allowMissingTail?: boolean;
|
|
||||||
}
|
|
||||||
): Promise<void> {
|
|
||||||
await ensureFileToolsRootExists();
|
|
||||||
|
|
||||||
const relativePath = path.relative(requireFileToolsRootDir(), absolutePath);
|
|
||||||
|
|
||||||
if (!relativePath || relativePath === ".") {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const parts = relativePath.split(path.sep).filter(Boolean);
|
|
||||||
|
|
||||||
let currentPath = requireFileToolsRootDir();
|
|
||||||
|
|
||||||
for (const part of parts) {
|
|
||||||
currentPath = path.join(currentPath, part);
|
|
||||||
|
|
||||||
try {
|
|
||||||
const stat = await fs.promises.lstat(currentPath);
|
|
||||||
|
|
||||||
if (stat.isSymbolicLink()) {
|
|
||||||
throw new Error("Symlinks are not allowed in file tool paths.");
|
|
||||||
}
|
|
||||||
} catch (e: unknown) {
|
|
||||||
if ((e as NodeJS.ErrnoException).code === "ENOENT" && options?.allowMissingTail) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function pathExists(absolutePath: string): Promise<boolean> {
|
|
||||||
try {
|
|
||||||
await fs.promises.lstat(absolutePath);
|
|
||||||
return true;
|
|
||||||
} catch (e: unknown) {
|
|
||||||
if ((e as NodeJS.ErrnoException).code === "ENOENT") return false;
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function assertNotRoot(relativePath: string): void {
|
|
||||||
if (relativePath === ".") {
|
|
||||||
throw new Error("Operation on the root directory itself is not allowed.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function getEntryType(stat: fs.Stats): "file" | "directory" | "symlink" | "other" {
|
|
||||||
if (stat.isSymbolicLink()) return "symlink";
|
|
||||||
if (stat.isFile()) return "file";
|
|
||||||
if (stat.isDirectory()) return "directory";
|
|
||||||
return "other";
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function readFile(args?: Record<string, unknown>) {
|
|
||||||
const {absolutePath, relativePath} = resolveSafeToolPath(args?.path);
|
|
||||||
|
|
||||||
await assertNoSymlinkInPath(absolutePath);
|
|
||||||
|
|
||||||
const stat = await fs.promises.lstat(absolutePath);
|
|
||||||
|
|
||||||
if (!stat.isFile()) {
|
|
||||||
throw new Error(`Path is not a file: ${relativePath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const maxBytes = asPositiveInt(args?.maxBytes, MAX_FILE_READ_BYTES, MAX_FILE_READ_BYTES);
|
|
||||||
|
|
||||||
if (stat.size > maxBytes) {
|
|
||||||
throw new Error(`File is too large: ${stat.size} bytes. Max allowed: ${maxBytes} bytes.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const buffer = await fs.promises.readFile(absolutePath);
|
|
||||||
|
|
||||||
if (buffer.includes(0)) {
|
|
||||||
throw new Error("Binary files are not supported.");
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
path: relativePath,
|
|
||||||
sizeBytes: stat.size,
|
|
||||||
content: buffer.toString("utf8"),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function listDirectory(args?: Record<string, unknown>) {
|
|
||||||
const {absolutePath, relativePath} = resolveSafeToolPath(args?.path, ".");
|
|
||||||
|
|
||||||
await assertNoSymlinkInPath(absolutePath);
|
|
||||||
|
|
||||||
const stat = await fs.promises.lstat(absolutePath);
|
|
||||||
|
|
||||||
if (!stat.isDirectory()) {
|
|
||||||
throw new Error(`Path is not a directory: ${relativePath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const dirEntries = await fs.promises.readdir(absolutePath, {
|
|
||||||
withFileTypes: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
const limitedEntries = dirEntries.slice(0, MAX_DIRECTORY_ENTRIES);
|
|
||||||
|
|
||||||
const entries = await Promise.all(limitedEntries.map(async entry => {
|
|
||||||
const entryAbsolutePath = path.join(absolutePath, entry.name);
|
|
||||||
const entryRelativePath = relativePath === "."
|
|
||||||
? entry.name
|
|
||||||
: path.join(relativePath, entry.name);
|
|
||||||
|
|
||||||
const entryStat = await fs.promises.lstat(entryAbsolutePath);
|
|
||||||
|
|
||||||
return {
|
|
||||||
name: entry.name,
|
|
||||||
path: entryRelativePath,
|
|
||||||
type: getEntryType(entryStat),
|
|
||||||
sizeBytes: entryStat.isFile() ? entryStat.size : null,
|
|
||||||
modifiedAt: entryStat.mtime.toISOString(),
|
|
||||||
};
|
|
||||||
}));
|
|
||||||
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
path: relativePath,
|
|
||||||
entries,
|
|
||||||
totalEntries: dirEntries.length,
|
|
||||||
returnedEntries: entries.length,
|
|
||||||
truncated: dirEntries.length > entries.length,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createFile(args?: Record<string, unknown>) {
|
|
||||||
const {absolutePath, relativePath} = resolveSafeToolPath(args?.path);
|
|
||||||
|
|
||||||
assertNotRoot(relativePath);
|
|
||||||
|
|
||||||
const content = asString(args?.content, "");
|
|
||||||
const overwrite = asBoolean(args?.overwrite, false);
|
|
||||||
const createParents = asBoolean(args?.createParents, true);
|
|
||||||
|
|
||||||
const contentSizeBytes = Buffer.byteLength(content, "utf8");
|
|
||||||
|
|
||||||
if (contentSizeBytes > MAX_FILE_WRITE_BYTES) {
|
|
||||||
throw new Error(`Content is too large: ${contentSizeBytes} bytes. Max allowed: ${MAX_FILE_WRITE_BYTES} bytes.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentPath = path.dirname(absolutePath);
|
|
||||||
|
|
||||||
if (createParents) {
|
|
||||||
await assertNoSymlinkInPath(parentPath, {allowMissingTail: true});
|
|
||||||
await fs.promises.mkdir(parentPath, {recursive: true});
|
|
||||||
} else {
|
|
||||||
await assertNoSymlinkInPath(parentPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await pathExists(absolutePath)) {
|
|
||||||
const stat = await fs.promises.lstat(absolutePath);
|
|
||||||
|
|
||||||
if (stat.isSymbolicLink()) {
|
|
||||||
throw new Error("Symlink targets are not allowed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
throw new Error(`Path is a directory, not a file: ${relativePath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!overwrite) {
|
|
||||||
throw new Error(`File already exists: ${relativePath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.promises.writeFile(absolutePath, content, {
|
|
||||||
encoding: "utf8",
|
|
||||||
flag: overwrite ? "w" : "wx",
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
path: relativePath,
|
|
||||||
sizeBytes: contentSizeBytes,
|
|
||||||
overwritten: overwrite,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
type CopyPathStats = {
|
|
||||||
entries: number;
|
|
||||||
totalBytes: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function copyPathRecursive(params: {
|
|
||||||
sourceAbsolutePath: string;
|
|
||||||
targetAbsolutePath: string;
|
|
||||||
overwrite: boolean;
|
|
||||||
stats: CopyPathStats;
|
|
||||||
}): Promise<void> {
|
|
||||||
const {
|
|
||||||
sourceAbsolutePath,
|
|
||||||
targetAbsolutePath,
|
|
||||||
overwrite,
|
|
||||||
stats,
|
|
||||||
} = params;
|
|
||||||
|
|
||||||
if (stats.entries >= MAX_COPY_ENTRIES) {
|
|
||||||
throw new Error(`Too many entries to copy. Max allowed: ${MAX_COPY_ENTRIES}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
stats.entries++;
|
|
||||||
|
|
||||||
const sourceStat = await fs.promises.lstat(sourceAbsolutePath);
|
|
||||||
|
|
||||||
if (sourceStat.isSymbolicLink()) {
|
|
||||||
throw new Error("Symlinks are not allowed in copied paths.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceStat.isFile()) {
|
|
||||||
stats.totalBytes += sourceStat.size;
|
|
||||||
|
|
||||||
if (stats.totalBytes > MAX_COPY_TOTAL_BYTES) {
|
|
||||||
throw new Error(`Copied data is too large. Max allowed: ${MAX_COPY_TOTAL_BYTES} bytes.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await pathExists(targetAbsolutePath)) {
|
|
||||||
const targetStat = await fs.promises.lstat(targetAbsolutePath);
|
|
||||||
|
|
||||||
if (targetStat.isSymbolicLink()) {
|
|
||||||
throw new Error("Symlink targets are not allowed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (targetStat.isDirectory()) {
|
|
||||||
throw new Error("Cannot overwrite a directory with a file.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!overwrite) {
|
|
||||||
throw new Error(`Target file already exists: ${path.relative(requireFileToolsRootDir(), targetAbsolutePath)}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.promises.copyFile(
|
|
||||||
sourceAbsolutePath,
|
|
||||||
targetAbsolutePath,
|
|
||||||
overwrite ? 0 : fs.constants.COPYFILE_EXCL,
|
|
||||||
);
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceStat.isDirectory()) {
|
|
||||||
if (await pathExists(targetAbsolutePath)) {
|
|
||||||
const targetStat = await fs.promises.lstat(targetAbsolutePath);
|
|
||||||
|
|
||||||
if (targetStat.isSymbolicLink()) {
|
|
||||||
throw new Error("Symlink targets are not allowed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!targetStat.isDirectory()) {
|
|
||||||
throw new Error("Cannot overwrite a file with a directory.");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await fs.promises.mkdir(targetAbsolutePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const entries = await fs.promises.readdir(sourceAbsolutePath);
|
|
||||||
|
|
||||||
for (const entry of entries) {
|
|
||||||
const childSourcePath = path.join(sourceAbsolutePath, entry);
|
|
||||||
const childTargetPath = path.join(targetAbsolutePath, entry);
|
|
||||||
|
|
||||||
await copyPathRecursive({
|
|
||||||
sourceAbsolutePath: childSourcePath,
|
|
||||||
targetAbsolutePath: childTargetPath,
|
|
||||||
overwrite,
|
|
||||||
stats,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Only files and directories can be copied.");
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function copyPath(args?: Record<string, unknown>) {
|
|
||||||
const source = resolveSafeToolPath(args?.sourcePath);
|
|
||||||
const target = resolveSafeToolPath(args?.targetPath);
|
|
||||||
|
|
||||||
assertNotRoot(source.relativePath);
|
|
||||||
assertNotRoot(target.relativePath);
|
|
||||||
|
|
||||||
await assertNoSymlinkInPath(source.absolutePath);
|
|
||||||
|
|
||||||
const sourceStat = await fs.promises.lstat(source.absolutePath);
|
|
||||||
|
|
||||||
if (sourceStat.isSymbolicLink()) {
|
|
||||||
throw new Error("Symlink sources are not allowed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const recursive = asBoolean(args?.recursive, false);
|
|
||||||
const overwrite = asBoolean(args?.overwrite, false);
|
|
||||||
const createParents = asBoolean(args?.createParents, true);
|
|
||||||
|
|
||||||
if (sourceStat.isDirectory() && !recursive) {
|
|
||||||
throw new Error("Source is a directory. Set recursive=true to copy directories.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceStat.isDirectory()) {
|
|
||||||
assertTargetIsNotInsideSource(source.absolutePath, target.absolutePath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const targetParentPath = path.dirname(target.absolutePath);
|
|
||||||
|
|
||||||
if (createParents) {
|
|
||||||
await assertNoSymlinkInPath(targetParentPath, {
|
|
||||||
allowMissingTail: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await fs.promises.mkdir(targetParentPath, {
|
|
||||||
recursive: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await assertNoSymlinkInPath(targetParentPath);
|
|
||||||
} else {
|
|
||||||
await assertNoSymlinkInPath(targetParentPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
const stats: CopyPathStats = {
|
|
||||||
entries: 0,
|
|
||||||
totalBytes: 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
await copyPathRecursive({
|
|
||||||
sourceAbsolutePath: source.absolutePath,
|
|
||||||
targetAbsolutePath: target.absolutePath,
|
|
||||||
overwrite,
|
|
||||||
stats,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
from: source.relativePath,
|
|
||||||
to: target.relativePath,
|
|
||||||
recursive,
|
|
||||||
overwrite,
|
|
||||||
entriesCopied: stats.entries,
|
|
||||||
bytesCopied: stats.totalBytes,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function createDirectory(args?: Record<string, unknown>) {
|
|
||||||
const {absolutePath, relativePath} = resolveSafeToolPath(args?.path);
|
|
||||||
|
|
||||||
const recursive = asBoolean(args?.recursive, true);
|
|
||||||
|
|
||||||
await assertNoSymlinkInPath(absolutePath, {
|
|
||||||
allowMissingTail: true,
|
|
||||||
});
|
|
||||||
|
|
||||||
await fs.promises.mkdir(absolutePath, {
|
|
||||||
recursive,
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
path: relativePath,
|
|
||||||
recursive,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function updateFile(args?: Record<string, unknown>) {
|
|
||||||
const {absolutePath, relativePath} = resolveSafeToolPath(args?.path);
|
|
||||||
|
|
||||||
assertNotRoot(relativePath);
|
|
||||||
|
|
||||||
const content = asString(args?.content, "");
|
|
||||||
const mode = (asNonEmptyString(args?.mode) ?? "replace").toLowerCase();
|
|
||||||
const createIfMissing = asBoolean(args?.createIfMissing, false);
|
|
||||||
|
|
||||||
if (!["replace", "append", "prepend"].includes(mode)) {
|
|
||||||
throw new Error(`Unsupported update mode: ${mode}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentSizeBytes = Buffer.byteLength(content, "utf8");
|
|
||||||
|
|
||||||
if (contentSizeBytes > MAX_FILE_WRITE_BYTES) {
|
|
||||||
throw new Error(`Content is too large: ${contentSizeBytes} bytes. Max allowed: ${MAX_FILE_WRITE_BYTES} bytes.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const parentPath = path.dirname(absolutePath);
|
|
||||||
|
|
||||||
await assertNoSymlinkInPath(parentPath);
|
|
||||||
|
|
||||||
const exists = await pathExists(absolutePath);
|
|
||||||
|
|
||||||
if (!exists && !createIfMissing) {
|
|
||||||
throw new Error(`File does not exist: ${relativePath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (exists) {
|
|
||||||
await assertNoSymlinkInPath(absolutePath);
|
|
||||||
|
|
||||||
const stat = await fs.promises.lstat(absolutePath);
|
|
||||||
|
|
||||||
if (!stat.isFile()) {
|
|
||||||
throw new Error(`Path is not a file: ${relativePath}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (mode === "replace") {
|
|
||||||
await fs.promises.writeFile(absolutePath, content, {
|
|
||||||
encoding: "utf8",
|
|
||||||
flag: "w",
|
|
||||||
});
|
|
||||||
} else if (mode === "append") {
|
|
||||||
await fs.promises.appendFile(absolutePath, content, {
|
|
||||||
encoding: "utf8",
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
const oldContent = exists
|
|
||||||
? await fs.promises.readFile(absolutePath, "utf8")
|
|
||||||
: "";
|
|
||||||
|
|
||||||
const resultSizeBytes = Buffer.byteLength(content + oldContent, "utf8");
|
|
||||||
|
|
||||||
if (resultSizeBytes > MAX_FILE_WRITE_BYTES) {
|
|
||||||
throw new Error(`Result file content is too large: ${resultSizeBytes} bytes. Max allowed: ${MAX_FILE_WRITE_BYTES} bytes.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.promises.writeFile(absolutePath, content + oldContent, {
|
|
||||||
encoding: "utf8",
|
|
||||||
flag: "w",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const newStat = await fs.promises.stat(absolutePath);
|
|
||||||
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
path: relativePath,
|
|
||||||
mode,
|
|
||||||
sizeBytes: newStat.size,
|
|
||||||
created: !exists,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function renamePath(args?: Record<string, unknown>) {
|
|
||||||
const source = resolveSafeToolPath(args?.sourcePath);
|
|
||||||
const target = resolveSafeToolPath(args?.targetPath);
|
|
||||||
|
|
||||||
assertNotRoot(source.relativePath);
|
|
||||||
assertNotRoot(target.relativePath);
|
|
||||||
|
|
||||||
await assertNoSymlinkInPath(source.absolutePath);
|
|
||||||
|
|
||||||
const sourceStat = await fs.promises.lstat(source.absolutePath);
|
|
||||||
|
|
||||||
if (sourceStat.isSymbolicLink()) {
|
|
||||||
throw new Error("Symlink targets are not allowed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const relativeTargetInsideSource = path.relative(source.absolutePath, target.absolutePath);
|
|
||||||
|
|
||||||
if (
|
|
||||||
relativeTargetInsideSource === "" ||
|
|
||||||
(!relativeTargetInsideSource.startsWith("..") && !path.isAbsolute(relativeTargetInsideSource))
|
|
||||||
) {
|
|
||||||
throw new Error("Cannot move a directory into itself.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const overwrite = asBoolean(args?.overwrite, false);
|
|
||||||
const createParents = asBoolean(args?.createParents, false);
|
|
||||||
|
|
||||||
const targetParentPath = path.dirname(target.absolutePath);
|
|
||||||
|
|
||||||
if (createParents) {
|
|
||||||
await assertNoSymlinkInPath(targetParentPath, {allowMissingTail: true});
|
|
||||||
await fs.promises.mkdir(targetParentPath, {recursive: true});
|
|
||||||
} else {
|
|
||||||
await assertNoSymlinkInPath(targetParentPath);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (await pathExists(target.absolutePath)) {
|
|
||||||
const targetStat = await fs.promises.lstat(target.absolutePath);
|
|
||||||
|
|
||||||
if (targetStat.isSymbolicLink()) {
|
|
||||||
throw new Error("Symlink targets are not allowed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!overwrite) {
|
|
||||||
throw new Error(`Target already exists: ${target.relativePath}`);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceStat.isDirectory() || targetStat.isDirectory()) {
|
|
||||||
throw new Error("Overwrite for directories is not supported.");
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.promises.rm(target.absolutePath, {
|
|
||||||
force: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
await fs.promises.rename(source.absolutePath, target.absolutePath);
|
|
||||||
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
from: source.relativePath,
|
|
||||||
to: target.relativePath,
|
|
||||||
overwrite,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function deletePath(args?: Record<string, unknown>) {
|
|
||||||
const {absolutePath, relativePath} = resolveSafeToolPath(args?.path);
|
|
||||||
|
|
||||||
assertNotRoot(relativePath);
|
|
||||||
|
|
||||||
await assertNoSymlinkInPath(absolutePath);
|
|
||||||
|
|
||||||
const stat = await fs.promises.lstat(absolutePath);
|
|
||||||
|
|
||||||
if (stat.isSymbolicLink()) {
|
|
||||||
throw new Error("Symlink targets are not allowed.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const recursive = asBoolean(args?.recursive, false);
|
|
||||||
|
|
||||||
if (stat.isDirectory()) {
|
|
||||||
if (recursive) {
|
|
||||||
await fs.promises.rm(absolutePath, {
|
|
||||||
recursive: true,
|
|
||||||
force: false,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
await fs.promises.rmdir(absolutePath);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
await fs.promises.rm(absolutePath, {
|
|
||||||
force: false,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
ok: true,
|
|
||||||
path: relativePath,
|
|
||||||
recursive,
|
|
||||||
deleted: true,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -3,3 +3,15 @@ export const MAX_FILE_WRITE_BYTES = 128 * 1024 * 1024;
|
|||||||
export const MAX_DIRECTORY_ENTRIES = 200;
|
export const MAX_DIRECTORY_ENTRIES = 200;
|
||||||
export const MAX_COPY_TOTAL_BYTES = 10 * 1024 * 1024;
|
export const MAX_COPY_TOTAL_BYTES = 10 * 1024 * 1024;
|
||||||
export const MAX_COPY_ENTRIES = 500;
|
export const MAX_COPY_ENTRIES = 500;
|
||||||
|
export const MAX_PATCH_OPERATIONS = 20;
|
||||||
|
export const MAX_PATCH_SEARCH_BYTES = 64 * 1024;
|
||||||
|
export const MAX_PATCH_REPLACE_BYTES = 256 * 1024;
|
||||||
|
export const MAX_PATCH_PREVIEW_CHARS = 6000;
|
||||||
|
export const MAX_FILE_SEARCH_ENTRIES = 5000;
|
||||||
|
export const MAX_FILE_SEARCH_RESULTS = 100;
|
||||||
|
export const MAX_FILE_SEARCH_CONTENT_BYTES = 512 * 1024;
|
||||||
|
export const MAX_FILE_SEARCH_SNIPPET_CHARS = 300;
|
||||||
|
export const MAX_FILE_WRITE_CHUNK_BYTES = 64 * 1024;
|
||||||
|
export const MAX_STREAM_WRITE_SESSIONS = 20;
|
||||||
|
export const MAX_STREAM_WRITE_IDLE_MS = 15 * 60 * 1000;
|
||||||
|
export const MAX_FILE_ATTACHMENT_BYTES = 20 * 1024 * 1024;
|
||||||
@@ -4,12 +4,12 @@ import {toolsLogger} from "./tool-logger";
|
|||||||
|
|
||||||
const logger = toolsLogger.child("market-rates");
|
const logger = toolsLogger.child("market-rates");
|
||||||
|
|
||||||
export const GET_FINANCIAL_MARKET_DATA = "get_financial_market_data";
|
export const GET_FINANCIAL_MARKET_DATA_TOOL_NAME = "get_financial_market_data";
|
||||||
|
|
||||||
export const getFinancialMarketData = {
|
export const getFinancialMarketData = {
|
||||||
type: "function",
|
type: "function",
|
||||||
function: {
|
function: {
|
||||||
name: GET_FINANCIAL_MARKET_DATA,
|
name: GET_FINANCIAL_MARKET_DATA_TOOL_NAME,
|
||||||
description:
|
description:
|
||||||
"Retrieve the latest exchange rates for supported currency, crypto, and precious metal pairs, including 24-hour change data when available. Supported pairs: USD/RUB, USD/EUR, USD/KZT, USD/UAH, USD/BYN, USD/GBP, USD/CNY, TON/USD, BTC/USD, ETH/USD, SOL/USD, and XAU/USD. Use this tool when the user asks for current rates, currency conversion, crypto prices, gold price, or recent 24-hour movement. This tool takes no parameters.",
|
"Retrieve the latest exchange rates for supported currency, crypto, and precious metal pairs, including 24-hour change data when available. Supported pairs: USD/RUB, USD/EUR, USD/KZT, USD/UAH, USD/BYN, USD/GBP, USD/CNY, TON/USD, BTC/USD, ETH/USD, SOL/USD, and XAU/USD. Use this tool when the user asks for current rates, currency conversion, crypto prices, gold price, or recent 24-hour movement. This tool takes no parameters.",
|
||||||
parameters: {
|
parameters: {
|
||||||
@@ -20,11 +20,11 @@ export const getFinancialMarketData = {
|
|||||||
},
|
},
|
||||||
} satisfies AiTool;
|
} satisfies AiTool;
|
||||||
|
|
||||||
export const financialMarketDataToolPrompt = [
|
export const getFinancialMarketDataToolPrompt = [
|
||||||
"Currency rates tool rules:",
|
"Currency rates tool rules:",
|
||||||
`- Use \`${GET_FINANCIAL_MARKET_DATA}\` whenever the answer depends on current exchange rates, crypto prices, or gold price.`,
|
`- Use \`${GET_FINANCIAL_MARKET_DATA_TOOL_NAME}\` whenever the answer depends on current exchange rates, crypto prices, or gold price.`,
|
||||||
`- Use \`${GET_FINANCIAL_MARKET_DATA}\` when the user asks whether a supported asset went up or down recently.`,
|
`- Use \`${GET_FINANCIAL_MARKET_DATA_TOOL_NAME}\` when the user asks whether a supported asset went up or down recently.`,
|
||||||
`- Use \`${GET_FINANCIAL_MARKET_DATA}\` when the user asks for the 24-hour change, percentage change, or movement direction for a supported pair.`,
|
`- Use \`${GET_FINANCIAL_MARKET_DATA_TOOL_NAME}\` when the user asks for the 24-hour change, percentage change, or movement direction for a supported pair.`,
|
||||||
"- Never guess current rates, prices, or 24-hour changes. Call the tool first.",
|
"- Never guess current rates, prices, or 24-hour changes. Call the tool first.",
|
||||||
"- Do not use this tool for unsupported pairs unless the user asks about one of the supported pairs listed below.",
|
"- Do not use this tool for unsupported pairs unless the user asks about one of the supported pairs listed below.",
|
||||||
"- Do not use this tool for historical rates beyond the provided 24-hour comparison.",
|
"- Do not use this tool for historical rates beyond the provided 24-hour comparison.",
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import {AiTool} from "../tool-types";
|
import {AiTool} from "../tool-types";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import {readdir, readFile, unlink, writeFile} from "node:fs/promises";
|
import {readdir, readFile, stat, unlink, writeFile} from "node:fs/promises";
|
||||||
import {notesDir, notesRootFile} from "../../index";
|
import {notesDir, notesRootFile} from "../../index";
|
||||||
import {asNonEmptyString} from "./utils";
|
import {asNonEmptyString} from "./utils";
|
||||||
import {toolsLogger} from "./tool-logger";
|
import {toolsLogger} from "./tool-logger";
|
||||||
|
import {z} from "zod";
|
||||||
|
|
||||||
const logger = toolsLogger.child("notes");
|
const logger = toolsLogger.child("notes");
|
||||||
|
|
||||||
@@ -338,3 +339,110 @@ async function removeNoteLinkFromRoot(noteFilePath: string): Promise<void> {
|
|||||||
function escapeRegExp(value: string): string {
|
function escapeRegExp(value: string): string {
|
||||||
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export type NoteFileAttachment = {
|
||||||
|
type: "local_file";
|
||||||
|
fileName: string;
|
||||||
|
// filePath: string;
|
||||||
|
relativePath: string;
|
||||||
|
mimeType: "text/markdown";
|
||||||
|
sizeBytes: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type GetNoteFileResult =
|
||||||
|
| {
|
||||||
|
success: true;
|
||||||
|
attachment: NoteFileAttachment;
|
||||||
|
} | { success: false; error: string };
|
||||||
|
|
||||||
|
export const NoteFileAttachmentSchema = z.object({
|
||||||
|
type: z.literal("local_file"),
|
||||||
|
fileName: z.string(),
|
||||||
|
// filePath: z.string(),
|
||||||
|
relativePath: z.string(),
|
||||||
|
mimeType: z.literal("text/markdown"),
|
||||||
|
sizeBytes: z.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export const GetNoteFileResultSchema = z.discriminatedUnion("success", [
|
||||||
|
z.object({
|
||||||
|
success: z.literal(true),
|
||||||
|
attachment: NoteFileAttachmentSchema,
|
||||||
|
}),
|
||||||
|
z.object({
|
||||||
|
success: z.literal(false),
|
||||||
|
error: z.string(),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
export const sendNoteAsFileTool = {
|
||||||
|
type: "function",
|
||||||
|
function: {
|
||||||
|
name: "send_note_as_file",
|
||||||
|
description:
|
||||||
|
"Prepare a Markdown note file to be sent to the user as a .md attachment. Returns a local file descriptor that the host application should use to upload or send the file.",
|
||||||
|
parameters: {
|
||||||
|
type: "object",
|
||||||
|
properties: {
|
||||||
|
fileName: {
|
||||||
|
type: "string",
|
||||||
|
description:
|
||||||
|
"The file name of the note to send. It may be provided with or without the .md extension. Must not contain forbidden or unsafe characters such as /, \\, :, *, ?, \", <, >, |, or control characters.",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
required: ["fileName"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
} satisfies AiTool;
|
||||||
|
|
||||||
|
export async function sendNoteAsFile(
|
||||||
|
args?: Record<string, unknown>,
|
||||||
|
): Promise<GetNoteFileResult> {
|
||||||
|
logger.debug("start", {args});
|
||||||
|
|
||||||
|
const fileName = asNonEmptyString(args?.fileName) ?? "";
|
||||||
|
if (!fileName.trim().length) {
|
||||||
|
return {success: false, error: "No file name provided"};
|
||||||
|
}
|
||||||
|
|
||||||
|
const noteFilePath = buildSafeNoteFilePath(fileName);
|
||||||
|
if (!noteFilePath) {
|
||||||
|
return {success: false, error: "Invalid or unsafe file name provided"};
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Проверяем, что файл существует и действительно читается.
|
||||||
|
await readFile(noteFilePath, "utf-8");
|
||||||
|
|
||||||
|
const fileStat = await stat(noteFilePath);
|
||||||
|
if (!fileStat.isFile()) {
|
||||||
|
return {success: false, error: "Note path is not a file"};
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizedFileName = path.basename(noteFilePath);
|
||||||
|
const relativePath = path.relative(path.dirname(notesRootFile), noteFilePath);
|
||||||
|
|
||||||
|
const result: GetNoteFileResult = {
|
||||||
|
success: true,
|
||||||
|
attachment: {
|
||||||
|
type: "local_file",
|
||||||
|
fileName: normalizedFileName,
|
||||||
|
// filePath: noteFilePath,
|
||||||
|
relativePath,
|
||||||
|
mimeType: "text/markdown",
|
||||||
|
sizeBytes: fileStat.size,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.debug("done", {
|
||||||
|
fileName: result.attachment.fileName,
|
||||||
|
relativePath: result.attachment.relativePath,
|
||||||
|
sizeBytes: result.attachment.sizeBytes
|
||||||
|
});
|
||||||
|
|
||||||
|
return result;
|
||||||
|
} catch (error) {
|
||||||
|
const errorMessage = error instanceof Error ? error.message : String(error);
|
||||||
|
return {success: false, error: `Failed to prepare note file: ${errorMessage}`};
|
||||||
|
}
|
||||||
|
}
|
||||||
+122
-107
@@ -1,18 +1,22 @@
|
|||||||
import {Environment} from "../../common/environment";
|
import {Environment} from "../../common/environment";
|
||||||
import {AiTool} from "../tool-types";
|
import {AiTool} from "../tool-types";
|
||||||
import {braveSearchTool, webSearch} from "./brave-search";
|
import {WEB_SEARCH_TOOL_NAME, webSearch, webSearchTool, webSearchToolPrompt} from "./web-search";
|
||||||
import {getCurrentDateTime, getCurrentDateTimeTool} from "./datetime";
|
import {getCurrentDateTime, getCurrentDateTimeTool} from "./datetime";
|
||||||
import {shellExecute, shellExecuteTool} from "./shell";
|
import {shellExecute, shellExecuteTool} from "./shell";
|
||||||
import {ToolHandler} from "./types";
|
import {ToolHandler} from "./types";
|
||||||
import {getWeather, getWeatherTool} from "./weather";
|
import {getWeather, getWeatherTool} from "./weather";
|
||||||
import {
|
import {
|
||||||
financialMarketDataToolPrompt,
|
GET_FINANCIAL_MARKET_DATA_TOOL_NAME,
|
||||||
GET_FINANCIAL_MARKET_DATA,
|
|
||||||
getFinancialMarketData,
|
getFinancialMarketData,
|
||||||
|
getFinancialMarketDataToolPrompt,
|
||||||
getMarketRates
|
getMarketRates
|
||||||
} from "./market-rates";
|
} from "./market-rates";
|
||||||
import {pythonInterpreterTool, runPythonInterpreter} from "./python-interpretator";
|
import {pythonInterpreterTool, runPythonInterpreter} from "./python-interpretator";
|
||||||
import {
|
import {
|
||||||
|
beginFileWrite,
|
||||||
|
beginFileWriteTool,
|
||||||
|
cancelFileWrite,
|
||||||
|
cancelFileWriteTool,
|
||||||
copyPath,
|
copyPath,
|
||||||
copyPathTool,
|
copyPathTool,
|
||||||
createDirectory,
|
createDirectory,
|
||||||
@@ -21,52 +25,71 @@ import {
|
|||||||
createFileTool,
|
createFileTool,
|
||||||
deletePath,
|
deletePath,
|
||||||
deletePathTool,
|
deletePathTool,
|
||||||
|
editFilePatch,
|
||||||
|
editFilePatchTool,
|
||||||
|
fileToolsToolPrompt,
|
||||||
|
finishFileWrite,
|
||||||
|
finishFileWriteTool,
|
||||||
listDirectory,
|
listDirectory,
|
||||||
listDirectoryTool,
|
listDirectoryTool,
|
||||||
readFile,
|
readFile,
|
||||||
readFileTool,
|
readFileTool,
|
||||||
renamePath,
|
renamePath,
|
||||||
renamePathTool,
|
renamePathTool,
|
||||||
|
searchFiles,
|
||||||
|
searchFilesTool,
|
||||||
|
sendFileAsAttachment,
|
||||||
|
sendFileAsAttachmentTool,
|
||||||
updateFile,
|
updateFile,
|
||||||
updateFileTool
|
updateFileTool,
|
||||||
} from "./file-system";
|
writeFileChunk,
|
||||||
import {createNote, createNoteTool} from "./create-note";
|
writeFileChunkTool
|
||||||
import {
|
} from "./files";
|
||||||
deleteNote,
|
|
||||||
deleteNoteTool,
|
|
||||||
getNoteContent,
|
|
||||||
getNoteContentTool,
|
|
||||||
listNotes,
|
|
||||||
listNotesTool,
|
|
||||||
updateNoteContent,
|
|
||||||
updateNoteContentTool
|
|
||||||
} from "./list-notes";
|
|
||||||
import {sendNoteAsFileTool, sendNoteAsFile} from "./send-note-as-file";
|
|
||||||
import {searchNotes, searchNotesTool} from "./search-notes";
|
|
||||||
|
|
||||||
export const getTools = () => {
|
export const defaultTools: AiTool[] = [
|
||||||
|
getCurrentDateTimeTool,
|
||||||
|
getFinancialMarketData,
|
||||||
|
]
|
||||||
|
|
||||||
|
export const fileTools = [
|
||||||
|
readFileTool,
|
||||||
|
listDirectoryTool,
|
||||||
|
searchFilesTool,
|
||||||
|
|
||||||
|
createFileTool,
|
||||||
|
beginFileWriteTool,
|
||||||
|
writeFileChunkTool,
|
||||||
|
finishFileWriteTool,
|
||||||
|
cancelFileWriteTool,
|
||||||
|
|
||||||
|
sendFileAsAttachmentTool,
|
||||||
|
|
||||||
|
createDirectoryTool,
|
||||||
|
copyPathTool,
|
||||||
|
updateFileTool,
|
||||||
|
editFilePatchTool,
|
||||||
|
renamePathTool,
|
||||||
|
deletePathTool,
|
||||||
|
] satisfies AiTool[];
|
||||||
|
|
||||||
|
// export const notesFileTools: AiTool[] = [
|
||||||
|
// createNoteTool,
|
||||||
|
// listNotesTool,
|
||||||
|
// getNoteContentTool,
|
||||||
|
// updateNoteContentTool,
|
||||||
|
// deleteNoteTool,
|
||||||
|
// sendNoteAsFileTool,
|
||||||
|
// searchNotesTool
|
||||||
|
// ]
|
||||||
|
|
||||||
|
export const getTools = (forCreator?: boolean) => {
|
||||||
const tools: AiTool[] = [
|
const tools: AiTool[] = [
|
||||||
getCurrentDateTimeTool,
|
...defaultTools,
|
||||||
getFinancialMarketData,
|
// ...notesFileTools
|
||||||
createNoteTool,
|
|
||||||
listNotesTool,
|
|
||||||
getNoteContentTool,
|
|
||||||
updateNoteContentTool,
|
|
||||||
deleteNoteTool,
|
|
||||||
sendNoteAsFileTool,
|
|
||||||
searchNotesTool
|
|
||||||
];
|
];
|
||||||
|
|
||||||
if (Environment.ENABLE_PYTHON_INTERPRETER) {
|
|
||||||
tools.push(pythonInterpreterTool);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Environment.ENABLE_UNSAFE_EVAL) {
|
|
||||||
tools.push(shellExecuteTool);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Environment.BRAVE_SEARCH_API_KEY) {
|
if (Environment.BRAVE_SEARCH_API_KEY) {
|
||||||
tools.push(braveSearchTool);
|
tools.push(webSearchTool);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Environment.OPEN_WEATHER_MAP_API_KEY) {
|
if (Environment.OPEN_WEATHER_MAP_API_KEY) {
|
||||||
@@ -74,86 +97,69 @@ export const getTools = () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (Environment.FILE_TOOLS_ROOT_DIR && Environment.ENABLE_FS_TOOLS) {
|
if (Environment.FILE_TOOLS_ROOT_DIR && Environment.ENABLE_FS_TOOLS) {
|
||||||
tools.push(
|
tools.push(...fileTools);
|
||||||
readFileTool,
|
|
||||||
listDirectoryTool,
|
|
||||||
createFileTool,
|
|
||||||
createDirectoryTool,
|
|
||||||
updateFileTool,
|
|
||||||
renamePathTool,
|
|
||||||
copyPathTool,
|
|
||||||
deletePathTool,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (forCreator) {
|
||||||
|
if (Environment.ENABLE_PYTHON_INTERPRETER) {
|
||||||
|
tools.push(pythonInterpreterTool);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Environment.ENABLE_UNSAFE_EVAL) {
|
||||||
|
tools.push(shellExecuteTool);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return tools;
|
return tools;
|
||||||
// return [
|
};
|
||||||
// createNoteTool,
|
|
||||||
// listNotesTool,
|
export const fileToolHandlers = {
|
||||||
// getNoteContentTool,
|
read_file: readFile,
|
||||||
// updateNoteContentTool,
|
list_directory: listDirectory,
|
||||||
// deleteNoteTool,
|
search_files: searchFiles,
|
||||||
// getNoteFileTool,
|
|
||||||
// searchNotesTool
|
create_file: createFile,
|
||||||
// ];
|
begin_file_write: beginFileWrite,
|
||||||
|
write_file_chunk: writeFileChunk,
|
||||||
|
finish_file_write: finishFileWrite,
|
||||||
|
cancel_file_write: cancelFileWrite,
|
||||||
|
|
||||||
|
send_file_as_attachment: sendFileAsAttachment,
|
||||||
|
|
||||||
|
create_directory: createDirectory,
|
||||||
|
copy_path: copyPath,
|
||||||
|
update_file: updateFile,
|
||||||
|
edit_file_patch: editFilePatch,
|
||||||
|
rename_path: renamePath,
|
||||||
|
delete_path: deletePath,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getToolHandlers = () => {
|
export const getToolHandlers = () => {
|
||||||
let handlers: Record<string, ToolHandler> = {
|
let handlers: Record<string, ToolHandler> = {
|
||||||
get_datetime: getCurrentDateTime,
|
get_datetime: getCurrentDateTime,
|
||||||
get_financial_market_data: getMarketRates,
|
get_financial_market_data: getMarketRates,
|
||||||
create_note: createNote,
|
|
||||||
list_notes: listNotes,
|
// create_note: createNote,
|
||||||
get_note_content: getNoteContent,
|
// list_notes: listNotes,
|
||||||
update_note_content: updateNoteContent,
|
// get_note_content: getNoteContent,
|
||||||
delete_note: deleteNote,
|
// update_note_content: updateNoteContent,
|
||||||
send_note_as_file: sendNoteAsFile,
|
// delete_note: deleteNote,
|
||||||
search_notes: searchNotes
|
// send_note_as_file: sendNoteAsFile,
|
||||||
|
// search_notes: searchNotes,
|
||||||
|
|
||||||
|
...fileToolHandlers,
|
||||||
|
|
||||||
|
|
||||||
|
python_interpreter: runPythonInterpreter,
|
||||||
|
|
||||||
|
shell_execute: shellExecute,
|
||||||
|
|
||||||
|
web_search: webSearch,
|
||||||
|
|
||||||
|
get_weather: getWeather,
|
||||||
|
|
||||||
};
|
};
|
||||||
|
|
||||||
if (Environment.ENABLE_PYTHON_INTERPRETER) {
|
|
||||||
handlers = {
|
|
||||||
python_interpreter: runPythonInterpreter,
|
|
||||||
...handlers
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Environment.ENABLE_UNSAFE_EVAL) {
|
|
||||||
handlers = {
|
|
||||||
shell_execute: shellExecute,
|
|
||||||
...handlers,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Environment.BRAVE_SEARCH_API_KEY) {
|
|
||||||
handlers = {
|
|
||||||
web_search: webSearch,
|
|
||||||
...handlers,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Environment.OPEN_WEATHER_MAP_API_KEY) {
|
|
||||||
handlers = {
|
|
||||||
get_weather: getWeather,
|
|
||||||
...handlers,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
if (Environment.FILE_TOOLS_ROOT_DIR && Environment.ENABLE_FS_TOOLS) {
|
|
||||||
handlers = {
|
|
||||||
read_file: readFile,
|
|
||||||
list_directory: listDirectory,
|
|
||||||
create_file: createFile,
|
|
||||||
create_directory: createDirectory,
|
|
||||||
update_file: updateFile,
|
|
||||||
rename_path: renamePath,
|
|
||||||
copy_path: copyPath,
|
|
||||||
delete_path: deletePath,
|
|
||||||
...handlers,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return handlers;
|
return handlers;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -161,9 +167,18 @@ export function getToolPrompts(toolNames: string[]): string[] {
|
|||||||
const prompts: string[] = [];
|
const prompts: string[] = [];
|
||||||
|
|
||||||
for (const toolName of toolNames) {
|
for (const toolName of toolNames) {
|
||||||
|
if (!prompts.includes(fileToolsToolPrompt) &&
|
||||||
|
fileTools.map(t => t.function.name).includes(toolName)) {
|
||||||
|
prompts.push(fileToolsToolPrompt);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
switch (toolName) {
|
switch (toolName) {
|
||||||
case GET_FINANCIAL_MARKET_DATA:
|
case GET_FINANCIAL_MARKET_DATA_TOOL_NAME:
|
||||||
prompts.push(financialMarketDataToolPrompt);
|
prompts.push(getFinancialMarketDataToolPrompt);
|
||||||
|
break;
|
||||||
|
case WEB_SEARCH_TOOL_NAME:
|
||||||
|
prompts.push(webSearchToolPrompt);
|
||||||
break;
|
break;
|
||||||
default:
|
default:
|
||||||
break;
|
break;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ function stringifyToolResult(result: unknown): string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function executeToolCall(
|
export async function executeToolCall(
|
||||||
|
userId: number | undefined | null,
|
||||||
name: string,
|
name: string,
|
||||||
args?: unknown,
|
args?: unknown,
|
||||||
context: ToolRuntimeContext = {},
|
context: ToolRuntimeContext = {},
|
||||||
@@ -31,7 +32,7 @@ export async function executeToolCall(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (name === PYTHON_INTERPRETER_TOOL_NAME) {
|
if (name === PYTHON_INTERPRETER_TOOL_NAME) {
|
||||||
const result = await runPythonInterpreter(normalizeToolArguments(args), {
|
const result = await runPythonInterpreter(normalizeToolArguments(args, userId), {
|
||||||
executionTimeoutMs: 8_000,
|
executionTimeoutMs: 8_000,
|
||||||
syntaxTimeoutMs: 3_000,
|
syntaxTimeoutMs: 3_000,
|
||||||
maxCodeChars: 100_000,
|
maxCodeChars: 100_000,
|
||||||
@@ -45,7 +46,8 @@ export async function executeToolCall(
|
|||||||
return s;
|
return s;
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await handler(normalizeToolArguments(args));
|
const arguments1 = normalizeToolArguments(args, userId);
|
||||||
|
const result = await handler(arguments1);
|
||||||
const s = stringifyToolResult(result);
|
const s = stringifyToolResult(result);
|
||||||
logger.debug("execute.done", {name, chars: s.length, duration: logger.duration(startedAt)});
|
logger.debug("execute.done", {name, chars: s.length, duration: logger.duration(startedAt)});
|
||||||
return s;
|
return s;
|
||||||
|
|||||||
@@ -1,113 +0,0 @@
|
|||||||
import {AiTool} from "../tool-types";
|
|
||||||
import path from "node:path";
|
|
||||||
import {readFile, stat} from "node:fs/promises";
|
|
||||||
import {notesRootFile} from "../../index";
|
|
||||||
import {asNonEmptyString} from "./utils";
|
|
||||||
import {buildSafeNoteFilePath} from "./list-notes";
|
|
||||||
import z from "zod";
|
|
||||||
import {toolsLogger} from "./tool-logger";
|
|
||||||
|
|
||||||
const logger = toolsLogger.child("get-note-file");
|
|
||||||
|
|
||||||
export type NoteFileAttachment = {
|
|
||||||
type: "local_file";
|
|
||||||
fileName: string;
|
|
||||||
// filePath: string;
|
|
||||||
relativePath: string;
|
|
||||||
mimeType: "text/markdown";
|
|
||||||
sizeBytes: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type GetNoteFileResult =
|
|
||||||
| {
|
|
||||||
success: true;
|
|
||||||
attachment: NoteFileAttachment;
|
|
||||||
} | { success: false; error: string };
|
|
||||||
|
|
||||||
export const NoteFileAttachmentSchema = z.object({
|
|
||||||
type: z.literal("local_file"),
|
|
||||||
fileName: z.string(),
|
|
||||||
// filePath: z.string(),
|
|
||||||
relativePath: z.string(),
|
|
||||||
mimeType: z.literal("text/markdown"),
|
|
||||||
sizeBytes: z.number(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const GetNoteFileResultSchema = z.discriminatedUnion("success", [
|
|
||||||
z.object({
|
|
||||||
success: z.literal(true),
|
|
||||||
attachment: NoteFileAttachmentSchema,
|
|
||||||
}),
|
|
||||||
z.object({
|
|
||||||
success: z.literal(false),
|
|
||||||
error: z.string(),
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
export const sendNoteAsFileTool = {
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: "send_note_as_file",
|
|
||||||
description:
|
|
||||||
"Prepare a Markdown note file to be sent to the user as a .md attachment. Returns a local file descriptor that the host application should use to upload or send the file.",
|
|
||||||
parameters: {
|
|
||||||
type: "object",
|
|
||||||
properties: {
|
|
||||||
fileName: {
|
|
||||||
type: "string",
|
|
||||||
description:
|
|
||||||
"The file name of the note to send. It may be provided with or without the .md extension. Must not contain forbidden or unsafe characters such as /, \\, :, *, ?, \", <, >, |, or control characters.",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
required: ["fileName"],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
} satisfies AiTool;
|
|
||||||
|
|
||||||
export async function sendNoteAsFile(
|
|
||||||
args?: Record<string, unknown>,
|
|
||||||
): Promise<GetNoteFileResult> {
|
|
||||||
logger.debug("start", {args});
|
|
||||||
|
|
||||||
const fileName = asNonEmptyString(args?.fileName) ?? "";
|
|
||||||
if (!fileName.trim().length) {
|
|
||||||
return {success: false, error: "No file name provided"};
|
|
||||||
}
|
|
||||||
|
|
||||||
const noteFilePath = buildSafeNoteFilePath(fileName);
|
|
||||||
if (!noteFilePath) {
|
|
||||||
return {success: false, error: "Invalid or unsafe file name provided"};
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// Проверяем, что файл существует и действительно читается.
|
|
||||||
await readFile(noteFilePath, "utf-8");
|
|
||||||
|
|
||||||
const fileStat = await stat(noteFilePath);
|
|
||||||
if (!fileStat.isFile()) {
|
|
||||||
return {success: false, error: "Note path is not a file"};
|
|
||||||
}
|
|
||||||
|
|
||||||
const normalizedFileName = path.basename(noteFilePath);
|
|
||||||
const relativePath = path.relative(path.dirname(notesRootFile), noteFilePath);
|
|
||||||
|
|
||||||
const result: GetNoteFileResult = {
|
|
||||||
success: true,
|
|
||||||
attachment: {
|
|
||||||
type: "local_file",
|
|
||||||
fileName: normalizedFileName,
|
|
||||||
// filePath: noteFilePath,
|
|
||||||
relativePath,
|
|
||||||
mimeType: "text/markdown",
|
|
||||||
sizeBytes: fileStat.size,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
logger.debug("done", {fileName: result.attachment.fileName, relativePath: result.attachment.relativePath, sizeBytes: result.attachment.sizeBytes});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = error instanceof Error ? error.message : String(error);
|
|
||||||
return {success: false, error: `Failed to prepare note file: ${errorMessage}`};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -9,7 +9,7 @@ export function asNonEmptyString(value: unknown): string | undefined {
|
|||||||
: undefined;
|
: undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function normalizeToolArguments(args: unknown): Record<string, unknown> {
|
export function normalizeToolArguments(args: unknown, userId?: number | null): Record<string, unknown> {
|
||||||
if (!args) return {};
|
if (!args) return {};
|
||||||
|
|
||||||
if (typeof args === "string") {
|
if (typeof args === "string") {
|
||||||
@@ -29,7 +29,11 @@ export function normalizeToolArguments(args: unknown): Record<string, unknown> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (typeof args === "object" && !Array.isArray(args)) {
|
if (typeof args === "object" && !Array.isArray(args)) {
|
||||||
return args as Record<string, unknown>;
|
const userIdObject = userId ? {"userId": userId} : {};
|
||||||
|
return {
|
||||||
|
...args as Record<string, unknown>,
|
||||||
|
...userIdObject as Record<string, unknown>
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {};
|
return {};
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ type BraveSearchApiResponse = {
|
|||||||
|
|
||||||
export const WEB_SEARCH_TOOL_NAME = "web_search";
|
export const WEB_SEARCH_TOOL_NAME = "web_search";
|
||||||
|
|
||||||
export const braveSearchTool = {
|
export const webSearchTool = {
|
||||||
type: "function",
|
type: "function",
|
||||||
function: {
|
function: {
|
||||||
name: WEB_SEARCH_TOOL_NAME,
|
name: WEB_SEARCH_TOOL_NAME,
|
||||||
@@ -163,7 +163,7 @@ export const braveSearchTool = {
|
|||||||
},
|
},
|
||||||
} satisfies AiTool;
|
} satisfies AiTool;
|
||||||
|
|
||||||
export const braveSearchToolPrompt = [
|
export const webSearchToolPrompt = [
|
||||||
"Brave Search tool rules:",
|
"Brave Search tool rules:",
|
||||||
"- You have access to `web_search`.",
|
"- You have access to `web_search`.",
|
||||||
"- Use `web_search` when the user asks for current information, recent events, fresh prices, documentation lookup, source lookup, product info, news, public facts, or anything that may have changed.",
|
"- Use `web_search` when the user asks for current information, recent events, fresh prices, documentation lookup, source lookup, product info, news, public facts, or anything that may have changed.",
|
||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
ToolCallData,
|
ToolCallData,
|
||||||
ToolExecutionMemory
|
ToolExecutionMemory
|
||||||
} from "./unified-ai-runner.shared";
|
} from "./unified-ai-runner.shared";
|
||||||
|
import {Message} from "typescript-telegram-bot-api";
|
||||||
|
|
||||||
function collectGeminiResponseText(response: GeminiResponseLike & { text?: string }): string {
|
function collectGeminiResponseText(response: GeminiResponseLike & { text?: string }): string {
|
||||||
if (typeof response.text === "string") return response.text;
|
if (typeof response.text === "string") return response.text;
|
||||||
@@ -82,6 +83,7 @@ function appendGeminiToolRound(messages: GeminiMessage[], calls: ToolCallData[],
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function runGemini(
|
export async function runGemini(
|
||||||
|
msg: Message,
|
||||||
messages: GeminiMessage[],
|
messages: GeminiMessage[],
|
||||||
streamMessage: TelegramStreamMessage,
|
streamMessage: TelegramStreamMessage,
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
@@ -143,7 +145,7 @@ export async function runGemini(
|
|||||||
});
|
});
|
||||||
if (!calls.length) return;
|
if (!calls.length) return;
|
||||||
|
|
||||||
appendGeminiToolRound(messages, calls, await executeToolBatch(calls, streamMessage, toolContext, toolMemory));
|
appendGeminiToolRound(messages, calls, await executeToolBatch(msg.from?.id, calls, streamMessage, toolContext, toolMemory));
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,7 +168,7 @@ export async function runGemini(
|
|||||||
calls: calls.map(aiLogToolCall),
|
calls: calls.map(aiLogToolCall),
|
||||||
});
|
});
|
||||||
if (!calls.length) return;
|
if (!calls.length) return;
|
||||||
appendGeminiToolRound(messages, calls, await executeToolBatch(calls, streamMessage, toolContext, toolMemory));
|
appendGeminiToolRound(messages, calls, await executeToolBatch(msg.from?.id, calls, streamMessage, toolContext, toolMemory));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
// Mistral provider runner extracted from unified-ai-runner.ts.
|
|
||||||
import {Environment} from "../common/environment";
|
import {Environment} from "../common/environment";
|
||||||
import {getMistralTools} from "./tool-mappers";
|
import {getMistralTools} from "./tool-mappers";
|
||||||
import {TelegramStreamMessage} from "./telegram-stream-message";
|
import {TelegramStreamMessage} from "./telegram-stream-message";
|
||||||
@@ -7,9 +6,24 @@ import {MistralChatMessage} from "./mistral-chat-message";
|
|||||||
import {createMistralClient} from "./ai-runtime-target";
|
import {createMistralClient} from "./ai-runtime-target";
|
||||||
import {aiLog, aiLogDuration, aiLogProviderTarget, aiLogToolCall} from "../logging/ai-logger";
|
import {aiLog, aiLogDuration, aiLogProviderTarget, aiLogToolCall} from "../logging/ai-logger";
|
||||||
|
|
||||||
import {MAX_TOOL_ROUNDS, MistralDeltaLike, MistralDocumentReference, RuntimeConfigSnapshot, StreamingToolCallAccumulator, ToolCallData, ToolExecutionMemory, contentFromMistralDelta, executeToolBatch, mistralToolCalls, normalizeMistralToolCalls, roundStatus} from "./unified-ai-runner.shared";
|
import {
|
||||||
|
contentFromMistralDelta,
|
||||||
|
executeToolBatch,
|
||||||
|
MAX_TOOL_ROUNDS,
|
||||||
|
MistralDeltaLike,
|
||||||
|
MistralDocumentReference,
|
||||||
|
mistralToolCalls,
|
||||||
|
normalizeMistralToolCalls,
|
||||||
|
roundStatus,
|
||||||
|
RuntimeConfigSnapshot,
|
||||||
|
StreamingToolCallAccumulator,
|
||||||
|
ToolCallData,
|
||||||
|
ToolExecutionMemory
|
||||||
|
} from "./unified-ai-runner.shared";
|
||||||
|
import {Message} from "typescript-telegram-bot-api";
|
||||||
|
|
||||||
export async function runMistral(
|
export async function runMistral(
|
||||||
|
msg: Message,
|
||||||
messages: MistralChatMessage[],
|
messages: MistralChatMessage[],
|
||||||
documents: MistralDocumentReference[],
|
documents: MistralDocumentReference[],
|
||||||
streamMessage: TelegramStreamMessage,
|
streamMessage: TelegramStreamMessage,
|
||||||
@@ -43,14 +57,14 @@ export async function runMistral(
|
|||||||
const request = {
|
const request = {
|
||||||
model: config.mistralChatTarget.model,
|
model: config.mistralChatTarget.model,
|
||||||
messages,
|
messages,
|
||||||
tools: getMistralTools(),
|
tools: getMistralTools(msg.from?.id === Environment.CREATOR_ID),
|
||||||
documents: documents
|
documents: documents
|
||||||
} as unknown as Parameters<typeof mistralAi.chat.complete>[0];
|
} as unknown as Parameters<typeof mistralAi.chat.complete>[0];
|
||||||
const response = await mistralAi.chat.complete(request, {signal});
|
const response = await mistralAi.chat.complete(request, {signal});
|
||||||
const msg = response.choices?.[0]?.message;
|
const message = response.choices?.[0]?.message;
|
||||||
const text = typeof msg?.content === "string" ? msg.content : JSON.stringify(msg?.content ?? "");
|
const text = typeof message?.content === "string" ? message.content : JSON.stringify(message?.content ?? "");
|
||||||
streamMessage.append(text);
|
streamMessage.append(text);
|
||||||
const calls = normalizeMistralToolCalls(mistralToolCalls(msg));
|
const calls = normalizeMistralToolCalls(mistralToolCalls(message));
|
||||||
aiLog(calls.length ? "info" : "success", calls.length ? "mistral.tool_calls" : "mistral.run.done", {
|
aiLog(calls.length ? "info" : "success", calls.length ? "mistral.tool_calls" : "mistral.run.done", {
|
||||||
round,
|
round,
|
||||||
duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt),
|
duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt),
|
||||||
@@ -66,7 +80,7 @@ export async function runMistral(
|
|||||||
function: {name: call.name, arguments: call.argumentsText},
|
function: {name: call.name, arguments: call.argumentsText},
|
||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
const toolResults = await executeToolBatch(calls, streamMessage, toolContext, toolMemory);
|
const toolResults = await executeToolBatch(msg.from?.id, calls, streamMessage, toolContext, toolMemory);
|
||||||
for (const [index, call] of calls.entries()) {
|
for (const [index, call] of calls.entries()) {
|
||||||
messages.push({
|
messages.push({
|
||||||
role: "tool",
|
role: "tool",
|
||||||
@@ -81,7 +95,7 @@ export async function runMistral(
|
|||||||
const request = {
|
const request = {
|
||||||
model: config.mistralChatTarget.model,
|
model: config.mistralChatTarget.model,
|
||||||
messages,
|
messages,
|
||||||
tools: getMistralTools(),
|
tools: getMistralTools(msg.from?.id === Environment.CREATOR_ID),
|
||||||
documents: documents
|
documents: documents
|
||||||
} as unknown as Parameters<typeof mistralAi.chat.stream>[0];
|
} as unknown as Parameters<typeof mistralAi.chat.stream>[0];
|
||||||
const streamResponse = await mistralAi.chat.stream(request, {signal});
|
const streamResponse = await mistralAi.chat.stream(request, {signal});
|
||||||
@@ -119,7 +133,7 @@ export async function runMistral(
|
|||||||
content: roundText,
|
content: roundText,
|
||||||
toolCalls: calls.map(c => ({id: c.id, function: {name: c.name, arguments: c.argumentsText}}))
|
toolCalls: calls.map(c => ({id: c.id, function: {name: c.name, arguments: c.argumentsText}}))
|
||||||
});
|
});
|
||||||
const toolResults = await executeToolBatch(calls, streamMessage, toolContext, toolMemory);
|
const toolResults = await executeToolBatch(msg.from?.id, calls, streamMessage, toolContext, toolMemory);
|
||||||
for (const [index, call] of calls.entries()) {
|
for (const [index, call] of calls.entries()) {
|
||||||
messages.push({
|
messages.push({
|
||||||
role: "tool",
|
role: "tool",
|
||||||
@@ -130,8 +144,3 @@ export async function runMistral(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export class MistralProviderRunner {
|
|
||||||
static run = runMistral;
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
// Ollama provider runner extracted from unified-ai-runner.ts.
|
// Ollama provider runner extracted from unified-ai-runner.ts.
|
||||||
import {Message} from "typescript-telegram-bot-api";
|
|
||||||
import * as fs from "node:fs";
|
import * as fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import {Environment} from "../common/environment";
|
import {Environment} from "../common/environment";
|
||||||
@@ -11,12 +10,8 @@ import {ChatMessage} from "./chat-messages-types";
|
|||||||
import {ChatRequest, Tool} from "ollama";
|
import {ChatRequest, Tool} from "ollama";
|
||||||
import {ToolRuntimeContext} from "./tools/runtime";
|
import {ToolRuntimeContext} from "./tools/runtime";
|
||||||
import {enqueueTelegramApiCall} from "../util/telegram-api-queue";
|
import {enqueueTelegramApiCall} from "../util/telegram-api-queue";
|
||||||
import {getCurrentDateTimeTool} from "./tools/datetime";
|
|
||||||
import {getFinancialMarketData} from "./tools/market-rates";
|
|
||||||
import {getWeatherTool} from "./tools/weather";
|
|
||||||
import {loadOllamaModel, unloadAllOllamaModels} from "./tools/utils";
|
import {loadOllamaModel, unloadAllOllamaModels} from "./tools/utils";
|
||||||
import {createOllamaClient} from "./ai-runtime-target";
|
import {createOllamaClient} from "./ai-runtime-target";
|
||||||
import {GetNoteFileResult, GetNoteFileResultSchema, sendNoteAsFileTool} from "./tools/send-note-as-file";
|
|
||||||
import {aiLog, aiLogDuration, aiLogMessageIdentity, aiLogProviderTarget, aiLogToolCall} from "../logging/ai-logger";
|
import {aiLog, aiLogDuration, aiLogMessageIdentity, aiLogProviderTarget, aiLogToolCall} from "../logging/ai-logger";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -41,9 +36,10 @@ import {
|
|||||||
} from "./unified-ai-runner.shared";
|
} from "./unified-ai-runner.shared";
|
||||||
import {latestUserTextFromOllamaMessages, OllamaToolRanker} from "./unified-ai-runner.tool-ranker";
|
import {latestUserTextFromOllamaMessages, OllamaToolRanker} from "./unified-ai-runner.tool-ranker";
|
||||||
import {getToolPrompts} from "./tools/registry";
|
import {getToolPrompts} from "./tools/registry";
|
||||||
import {createNoteTool} from "./tools/create-note";
|
import {GetNoteFileResult, GetNoteFileResultSchema} from "./tools/notes";
|
||||||
import {deleteNoteTool, getNoteContentTool, listNotesTool, updateNoteContentTool} from "./tools/list-notes";
|
import {getModelCapabilities} from "./provider-model-runtime";
|
||||||
import {searchNotesTool} from "./tools/search-notes";
|
import {AiProvider} from "../model/ai-provider";
|
||||||
|
import {Message} from "typescript-telegram-bot-api";
|
||||||
|
|
||||||
export async function runOllama(
|
export async function runOllama(
|
||||||
msg: Message,
|
msg: Message,
|
||||||
@@ -57,7 +53,6 @@ export async function runOllama(
|
|||||||
toolContext: ToolRuntimeContext,
|
toolContext: ToolRuntimeContext,
|
||||||
contextSize?: number,
|
contextSize?: number,
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const fromId = msg.from?.id;
|
|
||||||
const runnerStartedAt = Date.now();
|
const runnerStartedAt = Date.now();
|
||||||
|
|
||||||
const audioCount = messages.reduce((sum, m) => sum + (m.audioParts?.length || m.audios?.length || 0), 0);
|
const audioCount = messages.reduce((sum, m) => sum + (m.audioParts?.length || m.audios?.length || 0), 0);
|
||||||
@@ -185,68 +180,57 @@ export async function runOllama(
|
|||||||
};
|
};
|
||||||
|
|
||||||
let activeToolNames: string[] = [];
|
let activeToolNames: string[] = [];
|
||||||
// if ((await getModelCapabilities(AiProvider.OLLAMA, model, "tools"))?.tools?.supported) {
|
if ((await getModelCapabilities(AiProvider.OLLAMA, model, "tools"))?.tools?.supported) {
|
||||||
const availableOllamaTools: Tool[] =
|
const availableOllamaTools: Tool[] = getOllamaTools(msg.from?.id === Environment.CREATOR_ID) as Tool[];
|
||||||
fromId !== Environment.CREATOR_ID ? [
|
|
||||||
getCurrentDateTimeTool,
|
|
||||||
getFinancialMarketData,
|
|
||||||
getWeatherTool,
|
|
||||||
createNoteTool,
|
|
||||||
listNotesTool,
|
|
||||||
getNoteContentTool,
|
|
||||||
updateNoteContentTool,
|
|
||||||
deleteNoteTool,
|
|
||||||
sendNoteAsFileTool,
|
|
||||||
searchNotesTool
|
|
||||||
] : getOllamaTools() as Tool[];
|
|
||||||
|
|
||||||
aiLog("debug", "ollama.tools.available", {
|
aiLog("debug", "ollama.tools.available", {
|
||||||
round,
|
round,
|
||||||
tools: allToolSchemaNames(availableOllamaTools),
|
tools: allToolSchemaNames(availableOllamaTools),
|
||||||
rankerEnabled: !!config.ollamaToolRankerTarget,
|
rankerEnabled: !!config.ollamaToolRankerTarget,
|
||||||
});
|
});
|
||||||
|
|
||||||
const rankerSelection = await new OllamaToolRanker(config).selectTools({
|
const rankerSelection = await new OllamaToolRanker(config).selectTools({
|
||||||
userQuery: latestUserTextFromOllamaMessages(messages),
|
userQuery: latestUserTextFromOllamaMessages(messages),
|
||||||
availableTools: availableOllamaTools,
|
availableTools: availableOllamaTools,
|
||||||
round,
|
round,
|
||||||
signal,
|
signal,
|
||||||
});
|
});
|
||||||
|
|
||||||
activeToolNames = rankerSelection.tools.map(t => t.function.name ?? "");
|
const filteredTools = [...new Set(rankerSelection.tools)];
|
||||||
if (rankerSelection.tools.length > 0) {
|
activeToolNames = filteredTools.map(t => t.function.name ?? "");
|
||||||
request.tools = [...rankerSelection.tools, ...rankerSelection.tools];
|
if (filteredTools.length > 0) {
|
||||||
request.options = {
|
request.tools = [...filteredTools];
|
||||||
...request.options,
|
request.options = {
|
||||||
temperature: 0
|
...request.options,
|
||||||
|
temperature: 0
|
||||||
|
}
|
||||||
|
|
||||||
|
const newMessage = messages[messages.length - 1];
|
||||||
|
if (newMessage) {
|
||||||
|
newMessage.content += "\n" + "Suggested tools to call: " + activeToolNames.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const systemMessage = messages.find(m => m.role === "system");
|
||||||
|
if (systemMessage) {
|
||||||
|
systemMessage.content += "\n\n" + getToolPrompts(activeToolNames).join("\n\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
request.model = config.ollamaToolTarget.model;
|
||||||
|
} else {
|
||||||
|
delete request.tools;
|
||||||
}
|
}
|
||||||
|
|
||||||
const newMessage = messages[messages.length - 1];
|
// TODO: 14.05.2026, Danil Nikolaev: check if model supports tools
|
||||||
if (newMessage) {
|
|
||||||
newMessage.content += "\n" + "Suggested tools to call: " + activeToolNames.join(", ");
|
|
||||||
}
|
|
||||||
|
|
||||||
const systemMessage = messages.find(m => m.role === "system");
|
|
||||||
if (systemMessage) {
|
|
||||||
systemMessage.content += "\n\n" + getToolPrompts(activeToolNames).join("\n\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
request.model = config.ollamaToolTarget.model;
|
aiLog("debug", "ollama.tools.selected", {
|
||||||
} else {
|
round,
|
||||||
delete request.tools;
|
tools: activeToolNames,
|
||||||
|
count: activeToolNames.length,
|
||||||
|
usedRanker: rankerSelection.usedRanker,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: 14.05.2026, Danil Nikolaev: check if model supports tools
|
|
||||||
|
|
||||||
|
|
||||||
aiLog("debug", "ollama.tools.selected", {
|
|
||||||
round,
|
|
||||||
tools: activeToolNames,
|
|
||||||
count: activeToolNames.length,
|
|
||||||
usedRanker: rankerSelection.usedRanker,
|
|
||||||
});
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
const response = await ollama.chat({
|
const response = await ollama.chat({
|
||||||
...request,
|
...request,
|
||||||
@@ -309,7 +293,7 @@ export async function runOllama(
|
|||||||
appendOllamaToolResults(
|
appendOllamaToolResults(
|
||||||
messages,
|
messages,
|
||||||
calls,
|
calls,
|
||||||
await executeToolBatch(calls, streamMessage, toolContext, toolMemory),
|
await executeToolBatch(msg.from?.id, calls, streamMessage, toolContext, toolMemory),
|
||||||
);
|
);
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -418,7 +402,7 @@ export async function runOllama(
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
const toolResults = await executeToolBatch(calls, streamMessage, toolContext, toolMemory);
|
const toolResults = await executeToolBatch(msg.from?.id, calls, streamMessage, toolContext, toolMemory);
|
||||||
|
|
||||||
let successGetNoteFileResult: GetNoteFileResult | undefined = undefined;
|
let successGetNoteFileResult: GetNoteFileResult | undefined = undefined;
|
||||||
|
|
||||||
|
|||||||
+316
-301
@@ -1,7 +1,6 @@
|
|||||||
// OpenAI and OpenAI-compatible provider runners extracted from unified-ai-runner.ts.
|
// OpenAI and OpenAI-compatible provider runners extracted from unified-ai-runner.ts.
|
||||||
import {Message} from "typescript-telegram-bot-api";
|
import {Message} from "typescript-telegram-bot-api";
|
||||||
import {Environment} from "../common/environment";
|
import {Environment} from "../common/environment";
|
||||||
import {getOpenAITools} from "./tool-mappers";
|
|
||||||
import {TelegramStreamMessage} from "./telegram-stream-message";
|
import {TelegramStreamMessage} from "./telegram-stream-message";
|
||||||
import {ToolRuntimeContext} from "./tools/runtime";
|
import {ToolRuntimeContext} from "./tools/runtime";
|
||||||
import {OpenAIChatMessage} from "./openai-chat-message";
|
import {OpenAIChatMessage} from "./openai-chat-message";
|
||||||
@@ -11,45 +10,35 @@ import type {
|
|||||||
ResponseInputItem,
|
ResponseInputItem,
|
||||||
ResponseStreamEvent
|
ResponseStreamEvent
|
||||||
} from "openai/resources/responses/responses";
|
} from "openai/resources/responses/responses";
|
||||||
import type {
|
import {createOpenAiClient} from "./ai-runtime-target";
|
||||||
ChatCompletionCreateParamsNonStreaming,
|
|
||||||
ChatCompletionCreateParamsStreaming
|
|
||||||
} from "openai/resources/chat/completions";
|
|
||||||
import {createGeminiOpenAiClient, createOpenAiClient} from "./ai-runtime-target";
|
|
||||||
import {aiLog, aiLogDuration, aiLogMessageIdentity, aiLogProviderTarget, aiLogToolCall} from "../logging/ai-logger";
|
import {aiLog, aiLogDuration, aiLogMessageIdentity, aiLogProviderTarget, aiLogToolCall} from "../logging/ai-logger";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
AsyncIterableStream,
|
AsyncIterableStream,
|
||||||
collectOpenAiResponseFunctionCalls,
|
buildSystemInstruction,
|
||||||
collectOpenAiResponseCodeInterpreterCalls,
|
collectOpenAiResponseCodeInterpreterCalls,
|
||||||
|
collectOpenAiResponseFunctionCalls,
|
||||||
collectOpenAiResponseImages,
|
collectOpenAiResponseImages,
|
||||||
collectOpenAiResponseText,
|
collectOpenAiResponseText,
|
||||||
executeToolBatch,
|
executeToolBatch,
|
||||||
getOpenAIResponsesToolsWithImage,
|
getOpenAIResponsesToolsWithImage,
|
||||||
isRecord,
|
|
||||||
MAX_TOOL_ROUNDS,
|
MAX_TOOL_ROUNDS,
|
||||||
OPENAI_IMAGE_PARTIALS,
|
OPENAI_IMAGE_PARTIALS,
|
||||||
OpenAiChatCompletionResponseLike,
|
|
||||||
OpenAiChatCompletionStreamChunkLike,
|
|
||||||
OpenAiChatToolCallLike,
|
|
||||||
OpenAiCompatibleChatMessage,
|
|
||||||
OpenAiCompatibleContentPart,
|
|
||||||
openAiResponseItemCallId,
|
openAiResponseItemCallId,
|
||||||
OpenAiResponseLike,
|
OpenAiResponseLike,
|
||||||
OpenAiResponseOutputItem,
|
OpenAiResponseOutputItem,
|
||||||
roundStatus,
|
|
||||||
RuntimeConfigSnapshot,
|
RuntimeConfigSnapshot,
|
||||||
safeJsonParseObject,
|
safeJsonParseObject,
|
||||||
showOpenAiGeneratedImage,
|
showOpenAiGeneratedImage,
|
||||||
StreamingToolCallAccumulator,
|
|
||||||
ToolCallData,
|
ToolCallData,
|
||||||
ToolExecutionMemory
|
ToolExecutionMemory
|
||||||
} from "./unified-ai-runner.shared";
|
} from "./unified-ai-runner.shared";
|
||||||
import {GetNoteFileResult, GetNoteFileResultSchema} from "./tools/send-note-as-file";
|
import {bot, filesDir} from "../index";
|
||||||
import {bot, notesDir} from "../index";
|
|
||||||
import fs from "node:fs";
|
import fs from "node:fs";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import {logError} from "../util/utils";
|
import {logError} from "../util/utils";
|
||||||
|
import {SendFileAttachmentResult, SendFileAttachmentResultSchema} from "./tools/files";
|
||||||
|
import {DEFAULT_AI_RESPONSE_LANGUAGE} from "../common/user-ai-settings";
|
||||||
|
|
||||||
export async function runOpenAi(
|
export async function runOpenAi(
|
||||||
msg: Message,
|
msg: Message,
|
||||||
@@ -57,19 +46,16 @@ export async function runOpenAi(
|
|||||||
streamMessage: TelegramStreamMessage,
|
streamMessage: TelegramStreamMessage,
|
||||||
signal: AbortSignal,
|
signal: AbortSignal,
|
||||||
stream: boolean,
|
stream: boolean,
|
||||||
firstRoundStatus: string,
|
|
||||||
sourceMessage: Message,
|
sourceMessage: Message,
|
||||||
config: RuntimeConfigSnapshot,
|
config: RuntimeConfigSnapshot,
|
||||||
toolContext: ToolRuntimeContext,
|
toolContext: ToolRuntimeContext,
|
||||||
think?: boolean
|
|
||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
// TODO: 13.05.2026: remove
|
|
||||||
firstRoundStatus;
|
|
||||||
think;
|
|
||||||
const runnerStartedAt = Date.now();
|
const runnerStartedAt = Date.now();
|
||||||
let responseInput: unknown[] = [...messages];
|
let responseInput: unknown[] = [...messages];
|
||||||
const openAi = createOpenAiClient(config.openAiChatTarget);
|
const openAi = createOpenAiClient(config.openAiChatTarget);
|
||||||
|
|
||||||
|
const systemPrompt = buildSystemInstruction(config, DEFAULT_AI_RESPONSE_LANGUAGE, false);
|
||||||
|
|
||||||
aiLog("info", "openai.run.start", {
|
aiLog("info", "openai.run.start", {
|
||||||
stream,
|
stream,
|
||||||
target: aiLogProviderTarget(config.openAiChatTarget),
|
target: aiLogProviderTarget(config.openAiChatTarget),
|
||||||
@@ -89,8 +75,8 @@ export async function runOpenAi(
|
|||||||
const request: ResponseCreateParamsNonStreaming = {
|
const request: ResponseCreateParamsNonStreaming = {
|
||||||
model: config.openAiChatTarget.model,
|
model: config.openAiChatTarget.model,
|
||||||
input: responseInput as ResponseInputItem[],
|
input: responseInput as ResponseInputItem[],
|
||||||
tools: getOpenAIResponsesToolsWithImage(config) as ResponseCreateParamsNonStreaming["tools"],
|
tools: getOpenAIResponsesToolsWithImage(config, msg.from?.id === Environment.CREATOR_ID) as ResponseCreateParamsNonStreaming["tools"],
|
||||||
instructions: config.systemPrompt,
|
instructions: systemPrompt,
|
||||||
};
|
};
|
||||||
const response = await openAi.responses.create(request, {signal}) as unknown as OpenAiResponseLike;
|
const response = await openAi.responses.create(request, {signal}) as unknown as OpenAiResponseLike;
|
||||||
|
|
||||||
@@ -146,38 +132,26 @@ export async function runOpenAi(
|
|||||||
name: call.name,
|
name: call.name,
|
||||||
argumentsText: call.argumentsText,
|
argumentsText: call.argumentsText,
|
||||||
}));
|
}));
|
||||||
const toolResults = await executeToolBatch(toolCalls, streamMessage, toolContext, toolMemory);
|
const toolResults = await executeToolBatch(msg.from?.id, toolCalls, streamMessage, toolContext, toolMemory);
|
||||||
|
|
||||||
let successGetNoteFileResult: GetNoteFileResult | undefined = undefined;
|
|
||||||
|
|
||||||
for (const toolResult of toolResults) {
|
|
||||||
try {
|
|
||||||
const raw = JSON.parse(toolResult);
|
|
||||||
const res = GetNoteFileResultSchema.safeParse(raw);
|
|
||||||
|
|
||||||
if (res.success && res.data.success) {
|
|
||||||
successGetNoteFileResult = res.data;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Not every tool result is JSON.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (successGetNoteFileResult && "attachment" in successGetNoteFileResult) {
|
|
||||||
await bot.sendDocument({
|
|
||||||
chat_id: msg.chat.id,
|
|
||||||
reply_parameters: {
|
|
||||||
message_id: msg.message_id,
|
|
||||||
},
|
|
||||||
document: fs.createReadStream(path.join(notesDir, successGetNoteFileResult.attachment.relativePath)),
|
|
||||||
}).catch(logError);
|
|
||||||
}
|
|
||||||
|
|
||||||
const toolOutputs = calls.map((call, index) => ({
|
const toolOutputs = calls.map((call, index) => ({
|
||||||
type: "function_call_output" as const,
|
type: "function_call_output" as const,
|
||||||
call_id: call.callId,
|
call_id: call.callId,
|
||||||
output: toolResults[index] ?? "",
|
output: toolResults[index] ?? "",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const uploadFilesResult = await tryToUploadFiles(msg, toolResults);
|
||||||
|
if (uploadFilesResult.found) {
|
||||||
|
if (!uploadFilesResult.uploaded) {
|
||||||
|
const old = toolOutputs[uploadFilesResult.toolIndex];
|
||||||
|
delete toolOutputs[uploadFilesResult.toolIndex];
|
||||||
|
toolOutputs.push({
|
||||||
|
type: "function_call_output" as const,
|
||||||
|
call_id: old.call_id,
|
||||||
|
output: "Error: " + uploadFilesResult.error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
responseInput = [...responseInput, ...(response.output ?? []), ...toolOutputs];
|
responseInput = [...responseInput, ...(response.output ?? []), ...toolOutputs];
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -187,8 +161,9 @@ export async function runOpenAi(
|
|||||||
model: config.openAiChatTarget.model,
|
model: config.openAiChatTarget.model,
|
||||||
input: responseInput as ResponseInputItem[],
|
input: responseInput as ResponseInputItem[],
|
||||||
stream: true,
|
stream: true,
|
||||||
tools: getOpenAIResponsesToolsWithImage(config) as ResponseCreateParamsStreaming["tools"],
|
tools: getOpenAIResponsesToolsWithImage(config, msg.from?.id === Environment.CREATOR_ID) as ResponseCreateParamsStreaming["tools"],
|
||||||
parallel_tool_calls: true
|
parallel_tool_calls: true,
|
||||||
|
instructions: systemPrompt
|
||||||
};
|
};
|
||||||
const response = await openAi.responses.create(request, {signal}) as unknown as AsyncIterableStream<ResponseStreamEvent>;
|
const response = await openAi.responses.create(request, {signal}) as unknown as AsyncIterableStream<ResponseStreamEvent>;
|
||||||
|
|
||||||
@@ -338,273 +313,313 @@ export async function runOpenAi(
|
|||||||
name: call.name,
|
name: call.name,
|
||||||
argumentsText: call.argumentsText,
|
argumentsText: call.argumentsText,
|
||||||
}));
|
}));
|
||||||
const toolResults = await executeToolBatch(toolCalls, streamMessage, toolContext, toolMemory);
|
const toolResults = await executeToolBatch(msg.from?.id, toolCalls, streamMessage, toolContext, toolMemory);
|
||||||
|
|
||||||
let successGetNoteFileResult: GetNoteFileResult | undefined = undefined;
|
|
||||||
|
|
||||||
for (const toolResult of toolResults) {
|
|
||||||
try {
|
|
||||||
const raw = JSON.parse(toolResult);
|
|
||||||
const res = GetNoteFileResultSchema.safeParse(raw);
|
|
||||||
|
|
||||||
if (res.success && res.data.success) {
|
|
||||||
successGetNoteFileResult = res.data;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Not every tool result is JSON.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (successGetNoteFileResult && "attachment" in successGetNoteFileResult) {
|
|
||||||
await bot.sendDocument({
|
|
||||||
chat_id: msg.chat.id,
|
|
||||||
reply_parameters: {
|
|
||||||
message_id: msg.message_id,
|
|
||||||
},
|
|
||||||
document: fs.createReadStream(path.join(notesDir, successGetNoteFileResult.attachment.relativePath)),
|
|
||||||
}).catch(logError);
|
|
||||||
}
|
|
||||||
|
|
||||||
const toolOutputs = calls.map((call, index) => ({
|
const toolOutputs = calls.map((call, index) => ({
|
||||||
type: "function_call_output",
|
type: "function_call_output",
|
||||||
call_id: call.callId,
|
call_id: call.callId,
|
||||||
output: toolResults[index] ?? "",
|
output: toolResults[index] ?? "",
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const uploadFilesResult = await tryToUploadFiles(msg, toolResults);
|
||||||
|
if (uploadFilesResult.found) {
|
||||||
|
if (!uploadFilesResult.uploaded) {
|
||||||
|
const old = toolOutputs[uploadFilesResult.toolIndex];
|
||||||
|
delete toolOutputs[uploadFilesResult.toolIndex];
|
||||||
|
toolOutputs.push({
|
||||||
|
type: "function_call_output" as const,
|
||||||
|
call_id: old.call_id,
|
||||||
|
output: "Error: " + uploadFilesResult.error
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
responseInput = [...responseInput, ...(completedResponse.output ?? []), ...toolOutputs];
|
responseInput = [...responseInput, ...(completedResponse.output ?? []), ...toolOutputs];
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function tryToUploadFiles(
|
||||||
function openAiResponseContentToText(content: unknown): string {
|
|
||||||
if (typeof content === "string") return content;
|
|
||||||
if (!Array.isArray(content)) return "";
|
|
||||||
return content.map(part => isRecord(part) ? part.text ?? part.content ?? part.refusal ?? "" : "").join("");
|
|
||||||
}
|
|
||||||
|
|
||||||
function openAiResponseMessagesToChatCompletions(messages: OpenAIChatMessage[]): OpenAiCompatibleChatMessage[] {
|
|
||||||
return messages.map((message): OpenAiCompatibleChatMessage => {
|
|
||||||
if (message.role === "system" || message.role === "assistant") {
|
|
||||||
return {
|
|
||||||
role: message.role,
|
|
||||||
content: openAiResponseContentToText(message.content),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const content = Array.isArray(message.content)
|
|
||||||
? message.content.map((part): OpenAiCompatibleContentPart => {
|
|
||||||
if (isRecord(part) && part.type === "input_image") {
|
|
||||||
return {
|
|
||||||
type: "image_url",
|
|
||||||
image_url: {url: String(part.image_url ?? "")},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
type: "text",
|
|
||||||
text: isRecord(part) && typeof part.text === "string" ? part.text : "",
|
|
||||||
};
|
|
||||||
})
|
|
||||||
: message.content;
|
|
||||||
|
|
||||||
return {role: "user", content};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeOpenAiChatToolCalls(toolCalls: OpenAiChatToolCallLike[] = []): ToolCallData[] {
|
|
||||||
return toolCalls.map((call, i) => ({
|
|
||||||
id: call.id || `openai_chat_${Date.now()}_${i}`,
|
|
||||||
name: call.function?.name || call.name || "",
|
|
||||||
argumentsText: typeof call.function?.arguments === "string"
|
|
||||||
? call.function.arguments
|
|
||||||
: JSON.stringify(call.function?.arguments ?? call.arguments ?? {}),
|
|
||||||
})).filter(call => call.name);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function appendOpenAiChatToolResults(
|
|
||||||
messages: OpenAiCompatibleChatMessage[],
|
|
||||||
calls: ToolCallData[],
|
|
||||||
results: string[],
|
|
||||||
): Promise<void> {
|
|
||||||
for (const [index, call] of calls.entries()) {
|
|
||||||
messages.push({
|
|
||||||
role: "tool",
|
|
||||||
tool_call_id: call.id,
|
|
||||||
content: results[index] ?? "",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runOpenAiCompatibleChat(
|
|
||||||
msg: Message,
|
msg: Message,
|
||||||
messages: OpenAIChatMessage[],
|
toolResults: string[]
|
||||||
streamMessage: TelegramStreamMessage,
|
): Promise<
|
||||||
signal: AbortSignal,
|
| { found: false }
|
||||||
stream: boolean,
|
| { found: true, uploaded: true }
|
||||||
firstRoundStatus: string,
|
| { found: boolean, uploaded: false, error: string, toolIndex: number }
|
||||||
config: RuntimeConfigSnapshot,
|
> {
|
||||||
toolContext: ToolRuntimeContext,
|
let sendFileAttachment: {
|
||||||
): Promise<void> {
|
result: SendFileAttachmentResult & { success: true },
|
||||||
const runnerStartedAt = Date.now();
|
toolIndex: number
|
||||||
const geminiOpenAi = createGeminiOpenAiClient(config.geminiChatTarget);
|
} | null = null;
|
||||||
const chatMessages = openAiResponseMessagesToChatCompletions(messages);
|
|
||||||
const toolMemory: ToolExecutionMemory = new Map();
|
|
||||||
|
|
||||||
aiLog("info", "openai_compatible.run.start", {
|
let found = false;
|
||||||
stream,
|
|
||||||
target: aiLogProviderTarget(config.geminiChatTarget),
|
|
||||||
inputMessages: messages.length,
|
|
||||||
chatMessages: chatMessages.length,
|
|
||||||
hasToolInputFiles: !!toolContext.pythonInputFiles?.length,
|
|
||||||
});
|
|
||||||
|
|
||||||
for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
|
||||||
const roundStartedAt = Date.now();
|
|
||||||
aiLog("debug", "openai_compatible.round.start", {round, messages: chatMessages.length, stream});
|
|
||||||
streamMessage.setStatus(roundStatus(round, firstRoundStatus) ?? "");
|
|
||||||
await streamMessage.flush();
|
|
||||||
|
|
||||||
if (!stream) {
|
|
||||||
const request: ChatCompletionCreateParamsNonStreaming = {
|
|
||||||
model: config.geminiChatTarget.model,
|
|
||||||
messages: chatMessages,
|
|
||||||
tools: getOpenAITools(),
|
|
||||||
// temperature: 0.6,
|
|
||||||
};
|
|
||||||
const response = await geminiOpenAi.chat.completions.create(request, {signal}) as unknown as OpenAiChatCompletionResponseLike;
|
|
||||||
const message = response.choices?.[0]?.message;
|
|
||||||
streamMessage.append(message?.content ?? "");
|
|
||||||
const calls = normalizeOpenAiChatToolCalls(message?.tool_calls ?? []);
|
|
||||||
aiLog(calls.length ? "info" : "success", calls.length ? "openai_compatible.tool_calls" : "openai_compatible.run.done", {
|
|
||||||
round,
|
|
||||||
duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt),
|
|
||||||
textChars: message?.content?.length ?? 0,
|
|
||||||
calls: calls.map(aiLogToolCall),
|
|
||||||
});
|
|
||||||
if (!calls.length) return;
|
|
||||||
|
|
||||||
chatMessages.push({
|
|
||||||
role: "assistant",
|
|
||||||
content: message?.content ?? "",
|
|
||||||
tool_calls: calls.map(call => ({
|
|
||||||
id: call.id,
|
|
||||||
type: "function" as const,
|
|
||||||
function: {
|
|
||||||
name: call.name,
|
|
||||||
arguments: call.argumentsText,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
|
|
||||||
const toolResults = await executeToolBatch(calls, streamMessage, toolContext, toolMemory);
|
|
||||||
|
|
||||||
let successGetNoteFileResult: GetNoteFileResult | undefined = undefined;
|
|
||||||
|
|
||||||
for (const toolResult of toolResults) {
|
|
||||||
try {
|
|
||||||
const raw = JSON.parse(toolResult);
|
|
||||||
const res = GetNoteFileResultSchema.safeParse(raw);
|
|
||||||
|
|
||||||
if (res.success && res.data.success) {
|
|
||||||
successGetNoteFileResult = res.data;
|
|
||||||
}
|
|
||||||
} catch {
|
|
||||||
// Not every tool result is JSON.
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (successGetNoteFileResult && "attachment" in successGetNoteFileResult) {
|
|
||||||
await bot.sendDocument({
|
|
||||||
chat_id: msg.chat.id,
|
|
||||||
reply_parameters: {
|
|
||||||
message_id: msg.message_id,
|
|
||||||
},
|
|
||||||
document: fs.createReadStream(path.join(notesDir, successGetNoteFileResult.attachment.relativePath)),
|
|
||||||
}).catch(logError);
|
|
||||||
}
|
|
||||||
|
|
||||||
await appendOpenAiChatToolResults(chatMessages, calls, toolResults);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
const request: ChatCompletionCreateParamsStreaming = {
|
|
||||||
model: config.geminiChatTarget.model,
|
|
||||||
messages: chatMessages,
|
|
||||||
tools: getOpenAITools(),
|
|
||||||
// temperature: 0.6,
|
|
||||||
stream: true,
|
|
||||||
parallel_tool_calls: true
|
|
||||||
};
|
|
||||||
const response = await geminiOpenAi.chat.completions.create(request, {signal}) as unknown as AsyncIterableStream<OpenAiChatCompletionStreamChunkLike>;
|
|
||||||
|
|
||||||
aiLog("debug", "openai_compatible.stream.open", {round});
|
|
||||||
// const streamToolCalls: OpenAiChatToolCallLike[] = [];
|
|
||||||
const roundTextStart = streamMessage.getText().length;
|
|
||||||
const toolCallAccumulator = new StreamingToolCallAccumulator("openai_chat_stream", round);
|
|
||||||
let calls: ToolCallData[] = [];
|
|
||||||
|
|
||||||
for await (const chunk of response) {
|
|
||||||
if (signal.aborted) throw new Error("Aborted");
|
|
||||||
|
|
||||||
const delta = chunk.choices?.[0]?.delta;
|
|
||||||
streamMessage.append(delta?.content ?? "");
|
|
||||||
|
|
||||||
if (delta?.tool_calls?.length) {
|
|
||||||
calls = toolCallAccumulator.add(delta.tool_calls);
|
|
||||||
streamMessage.setStatus(Environment.getUseToolText(calls));
|
|
||||||
await streamMessage.flush();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// const calls = collectOpenAiChatStreamToolCalls(streamToolCalls);
|
|
||||||
aiLog(calls.length ? "info" : "success", calls.length ? "openai_compatible.tool_calls" : "openai_compatible.run.done", {
|
|
||||||
round,
|
|
||||||
duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt),
|
|
||||||
textChars: streamMessage.getText().slice(roundTextStart).length,
|
|
||||||
calls: calls.map(aiLogToolCall),
|
|
||||||
});
|
|
||||||
if (!calls.length) return;
|
|
||||||
|
|
||||||
const roundText = streamMessage.getText().slice(roundTextStart);
|
|
||||||
chatMessages.push({
|
|
||||||
role: "assistant",
|
|
||||||
content: roundText,
|
|
||||||
tool_calls: calls.map(call => ({
|
|
||||||
id: call.id,
|
|
||||||
type: "function",
|
|
||||||
function: {
|
|
||||||
name: call.name,
|
|
||||||
arguments: call.argumentsText,
|
|
||||||
},
|
|
||||||
})),
|
|
||||||
});
|
|
||||||
|
|
||||||
const toolResults = await executeToolBatch(calls, streamMessage, toolContext, toolMemory);
|
|
||||||
|
|
||||||
let successGetNoteFileResult: GetNoteFileResult | undefined = undefined;
|
|
||||||
|
|
||||||
|
try {
|
||||||
for (const toolResult of toolResults) {
|
for (const toolResult of toolResults) {
|
||||||
try {
|
const raw = JSON.parse(toolResult);
|
||||||
const raw = JSON.parse(toolResult);
|
const res = SendFileAttachmentResultSchema.safeParse(raw);
|
||||||
const res = GetNoteFileResultSchema.safeParse(raw);
|
|
||||||
|
|
||||||
if (res.success && res.data.success) {
|
if (res.success) {
|
||||||
successGetNoteFileResult = res.data;
|
found = true;
|
||||||
|
|
||||||
|
if (res.data.success) {
|
||||||
|
sendFileAttachment = {result: res.data, toolIndex: toolResults.indexOf(toolResult)};
|
||||||
}
|
}
|
||||||
} catch {
|
|
||||||
// Not every tool result is JSON.
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (successGetNoteFileResult && "attachment" in successGetNoteFileResult) {
|
if (!found) {
|
||||||
await bot.sendDocument({
|
return {found: false};
|
||||||
chat_id: msg.chat.id,
|
|
||||||
reply_parameters: {
|
|
||||||
message_id: msg.message_id,
|
|
||||||
},
|
|
||||||
document: fs.createReadStream(path.join(notesDir, successGetNoteFileResult.attachment.relativePath)),
|
|
||||||
}).catch(logError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await appendOpenAiChatToolResults(chatMessages, calls, toolResults);
|
await bot.sendDocument({
|
||||||
|
chat_id: msg.chat.id,
|
||||||
|
reply_parameters: {
|
||||||
|
message_id: msg.message_id,
|
||||||
|
},
|
||||||
|
document: fs.createReadStream(path.join(filesDir, String(msg.from?.id), sendFileAttachment?.result?.attachment?.relativePath ?? "")),
|
||||||
|
})
|
||||||
|
|
||||||
|
return {found: true, uploaded: true};
|
||||||
|
} catch (e: unknown) {
|
||||||
|
logError(e);
|
||||||
|
return {
|
||||||
|
found: found,
|
||||||
|
uploaded: false,
|
||||||
|
error: (e as any)?.message ?? "",
|
||||||
|
toolIndex: sendFileAttachment?.toolIndex ?? -1
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// function openAiResponseContentToText(content: unknown): string {
|
||||||
|
// if (typeof content === "string") return content;
|
||||||
|
// if (!Array.isArray(content)) return "";
|
||||||
|
// return content.map(part => isRecord(part) ? part.text ?? part.content ?? part.refusal ?? "" : "").join("");
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function openAiResponseMessagesToChatCompletions(messages: OpenAIChatMessage[]): OpenAiCompatibleChatMessage[] {
|
||||||
|
// return messages.map((message): OpenAiCompatibleChatMessage => {
|
||||||
|
// if (message.role === "system" || message.role === "assistant") {
|
||||||
|
// return {
|
||||||
|
// role: message.role,
|
||||||
|
// content: openAiResponseContentToText(message.content),
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// const content = Array.isArray(message.content)
|
||||||
|
// ? message.content.map((part): OpenAiCompatibleContentPart => {
|
||||||
|
// if (isRecord(part) && part.type === "input_image") {
|
||||||
|
// return {
|
||||||
|
// type: "image_url",
|
||||||
|
// image_url: {url: String(part.image_url ?? "")},
|
||||||
|
// };
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// return {
|
||||||
|
// type: "text",
|
||||||
|
// text: isRecord(part) && typeof part.text === "string" ? part.text : "",
|
||||||
|
// };
|
||||||
|
// })
|
||||||
|
// : message.content;
|
||||||
|
//
|
||||||
|
// return {role: "user", content};
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function normalizeOpenAiChatToolCalls(toolCalls: OpenAiChatToolCallLike[] = []): ToolCallData[] {
|
||||||
|
// return toolCalls.map((call, i) => ({
|
||||||
|
// id: call.id || `openai_chat_${Date.now()}_${i}`,
|
||||||
|
// name: call.function?.name || call.name || "",
|
||||||
|
// argumentsText: typeof call.function?.arguments === "string"
|
||||||
|
// ? call.function.arguments
|
||||||
|
// : JSON.stringify(call.function?.arguments ?? call.arguments ?? {}),
|
||||||
|
// })).filter(call => call.name);
|
||||||
|
// }
|
||||||
|
|
||||||
|
// async function appendOpenAiChatToolResults(
|
||||||
|
// messages: OpenAiCompatibleChatMessage[],
|
||||||
|
// calls: ToolCallData[],
|
||||||
|
// results: string[],
|
||||||
|
// ): Promise<void> {
|
||||||
|
// for (const [index, call] of calls.entries()) {
|
||||||
|
// messages.push({
|
||||||
|
// role: "tool",
|
||||||
|
// tool_call_id: call.id,
|
||||||
|
// content: results[index] ?? "",
|
||||||
|
// });
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|
||||||
|
// export async function runOpenAiCompatibleChat(
|
||||||
|
// msg: Message,
|
||||||
|
// messages: OpenAIChatMessage[],
|
||||||
|
// streamMessage: TelegramStreamMessage,
|
||||||
|
// signal: AbortSignal,
|
||||||
|
// stream: boolean,
|
||||||
|
// firstRoundStatus: string,
|
||||||
|
// config: RuntimeConfigSnapshot,
|
||||||
|
// toolContext: ToolRuntimeContext,
|
||||||
|
// ): Promise<void> {
|
||||||
|
// const runnerStartedAt = Date.now();
|
||||||
|
// const geminiOpenAi = createGeminiOpenAiClient(config.geminiChatTarget);
|
||||||
|
// const chatMessages = openAiResponseMessagesToChatCompletions(messages);
|
||||||
|
// const toolMemory: ToolExecutionMemory = new Map();
|
||||||
|
//
|
||||||
|
// aiLog("info", "openai_compatible.run.start", {
|
||||||
|
// stream,
|
||||||
|
// target: aiLogProviderTarget(config.geminiChatTarget),
|
||||||
|
// inputMessages: messages.length,
|
||||||
|
// chatMessages: chatMessages.length,
|
||||||
|
// hasToolInputFiles: !!toolContext.pythonInputFiles?.length,
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// for (let round = 0; round < MAX_TOOL_ROUNDS; round++) {
|
||||||
|
// const roundStartedAt = Date.now();
|
||||||
|
// aiLog("debug", "openai_compatible.round.start", {round, messages: chatMessages.length, stream});
|
||||||
|
// streamMessage.setStatus(roundStatus(round, firstRoundStatus) ?? "");
|
||||||
|
// await streamMessage.flush();
|
||||||
|
//
|
||||||
|
// if (!stream) {
|
||||||
|
// const request: ChatCompletionCreateParamsNonStreaming = {
|
||||||
|
// model: config.geminiChatTarget.model,
|
||||||
|
// messages: chatMessages,
|
||||||
|
// tools: getOpenAITools(msg.from?.id === Environment.CREATOR_ID),
|
||||||
|
// // temperature: 0.6,
|
||||||
|
// };
|
||||||
|
// const response = await geminiOpenAi.chat.completions.create(request, {signal}) as unknown as OpenAiChatCompletionResponseLike;
|
||||||
|
// const message = response.choices?.[0]?.message;
|
||||||
|
// streamMessage.append(message?.content ?? "");
|
||||||
|
// const calls = normalizeOpenAiChatToolCalls(message?.tool_calls ?? []);
|
||||||
|
// aiLog(calls.length ? "info" : "success", calls.length ? "openai_compatible.tool_calls" : "openai_compatible.run.done", {
|
||||||
|
// round,
|
||||||
|
// duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt),
|
||||||
|
// textChars: message?.content?.length ?? 0,
|
||||||
|
// calls: calls.map(aiLogToolCall),
|
||||||
|
// });
|
||||||
|
// if (!calls.length) return;
|
||||||
|
//
|
||||||
|
// chatMessages.push({
|
||||||
|
// role: "assistant",
|
||||||
|
// content: message?.content ?? "",
|
||||||
|
// tool_calls: calls.map(call => ({
|
||||||
|
// id: call.id,
|
||||||
|
// type: "function" as const,
|
||||||
|
// function: {
|
||||||
|
// name: call.name,
|
||||||
|
// arguments: call.argumentsText,
|
||||||
|
// },
|
||||||
|
// })),
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// const toolResults = await executeToolBatch(calls, streamMessage, toolContext, toolMemory);
|
||||||
|
//
|
||||||
|
// let successGetNoteFileResult: GetNoteFileResult | undefined = undefined;
|
||||||
|
//
|
||||||
|
// for (const toolResult of toolResults) {
|
||||||
|
// try {
|
||||||
|
// const raw = JSON.parse(toolResult);
|
||||||
|
// const res = GetNoteFileResultSchema.safeParse(raw);
|
||||||
|
//
|
||||||
|
// if (res.success && res.data.success) {
|
||||||
|
// successGetNoteFileResult = res.data;
|
||||||
|
// }
|
||||||
|
// } catch {
|
||||||
|
// // Not every tool result is JSON.
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (successGetNoteFileResult && "attachment" in successGetNoteFileResult) {
|
||||||
|
// await bot.sendDocument({
|
||||||
|
// chat_id: msg.chat.id,
|
||||||
|
// reply_parameters: {
|
||||||
|
// message_id: msg.message_id,
|
||||||
|
// },
|
||||||
|
// document: fs.createReadStream(path.join(notesDir, successGetNoteFileResult.attachment.relativePath)),
|
||||||
|
// }).catch(logError);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// await appendOpenAiChatToolResults(chatMessages, calls, toolResults);
|
||||||
|
// continue;
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// const request: ChatCompletionCreateParamsStreaming = {
|
||||||
|
// model: config.geminiChatTarget.model,
|
||||||
|
// messages: chatMessages,
|
||||||
|
// tools: getOpenAITools(msg.from?.id === Environment.CREATOR_ID),
|
||||||
|
// // temperature: 0.6,
|
||||||
|
// stream: true,
|
||||||
|
// parallel_tool_calls: true
|
||||||
|
// };
|
||||||
|
// const response = await geminiOpenAi.chat.completions.create(request, {signal}) as unknown as AsyncIterableStream<OpenAiChatCompletionStreamChunkLike>;
|
||||||
|
//
|
||||||
|
// aiLog("debug", "openai_compatible.stream.open", {round});
|
||||||
|
// // const streamToolCalls: OpenAiChatToolCallLike[] = [];
|
||||||
|
// const roundTextStart = streamMessage.getText().length;
|
||||||
|
// const toolCallAccumulator = new StreamingToolCallAccumulator("openai_chat_stream", round);
|
||||||
|
// let calls: ToolCallData[] = [];
|
||||||
|
//
|
||||||
|
// for await (const chunk of response) {
|
||||||
|
// if (signal.aborted) throw new Error("Aborted");
|
||||||
|
//
|
||||||
|
// const delta = chunk.choices?.[0]?.delta;
|
||||||
|
// streamMessage.append(delta?.content ?? "");
|
||||||
|
//
|
||||||
|
// if (delta?.tool_calls?.length) {
|
||||||
|
// calls = toolCallAccumulator.add(delta.tool_calls);
|
||||||
|
// streamMessage.setStatus(Environment.getUseToolText(calls));
|
||||||
|
// await streamMessage.flush();
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// // const calls = collectOpenAiChatStreamToolCalls(streamToolCalls);
|
||||||
|
// aiLog(calls.length ? "info" : "success", calls.length ? "openai_compatible.tool_calls" : "openai_compatible.run.done", {
|
||||||
|
// round,
|
||||||
|
// duration: calls.length ? aiLogDuration(roundStartedAt) : aiLogDuration(runnerStartedAt),
|
||||||
|
// textChars: streamMessage.getText().slice(roundTextStart).length,
|
||||||
|
// calls: calls.map(aiLogToolCall),
|
||||||
|
// });
|
||||||
|
// if (!calls.length) return;
|
||||||
|
//
|
||||||
|
// const roundText = streamMessage.getText().slice(roundTextStart);
|
||||||
|
// chatMessages.push({
|
||||||
|
// role: "assistant",
|
||||||
|
// content: roundText,
|
||||||
|
// tool_calls: calls.map(call => ({
|
||||||
|
// id: call.id,
|
||||||
|
// type: "function",
|
||||||
|
// function: {
|
||||||
|
// name: call.name,
|
||||||
|
// arguments: call.argumentsText,
|
||||||
|
// },
|
||||||
|
// })),
|
||||||
|
// });
|
||||||
|
//
|
||||||
|
// const toolResults = await executeToolBatch(calls, streamMessage, toolContext, toolMemory);
|
||||||
|
//
|
||||||
|
// let successGetNoteFileResult: GetNoteFileResult | undefined = undefined;
|
||||||
|
//
|
||||||
|
// for (const toolResult of toolResults) {
|
||||||
|
// try {
|
||||||
|
// const raw = JSON.parse(toolResult);
|
||||||
|
// const res = GetNoteFileResultSchema.safeParse(raw);
|
||||||
|
//
|
||||||
|
// if (res.success && res.data.success) {
|
||||||
|
// successGetNoteFileResult = res.data;
|
||||||
|
// }
|
||||||
|
// } catch {
|
||||||
|
// // Not every tool result is JSON.
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// if (successGetNoteFileResult && "attachment" in successGetNoteFileResult) {
|
||||||
|
// await bot.sendDocument({
|
||||||
|
// chat_id: msg.chat.id,
|
||||||
|
// reply_parameters: {
|
||||||
|
// message_id: msg.message_id,
|
||||||
|
// },
|
||||||
|
// document: fs.createReadStream(path.join(notesDir, successGetNoteFileResult.attachment.relativePath)),
|
||||||
|
// }).catch(logError);
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// await appendOpenAiChatToolResults(chatMessages, calls, toolResults);
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
|||||||
@@ -1269,6 +1269,7 @@ export async function prepareMistralDocuments(downloads: AiDownloadedFile[], mes
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function executeTool(
|
export async function executeTool(
|
||||||
|
userId: number | undefined | null,
|
||||||
toolCall: ToolCallData,
|
toolCall: ToolCallData,
|
||||||
message: TelegramStreamMessage,
|
message: TelegramStreamMessage,
|
||||||
context: ToolRuntimeContext,
|
context: ToolRuntimeContext,
|
||||||
@@ -1298,7 +1299,7 @@ export async function executeTool(
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const rawResult = await executeToolCall(toolCall.name, parsedArgs.args, context);
|
const rawResult = await executeToolCall(userId, toolCall.name, parsedArgs.args, context);
|
||||||
const result = stringifyToolExecutionResult(rawResult);
|
const result = stringifyToolExecutionResult(rawResult);
|
||||||
|
|
||||||
await sendToolArtifacts(toolCall, result, message);
|
await sendToolArtifacts(toolCall, result, message);
|
||||||
@@ -1367,16 +1368,18 @@ export async function runWithToolLocks<T>(keys: string[], task: () => Promise<T>
|
|||||||
}
|
}
|
||||||
|
|
||||||
export async function executeScheduledTool(
|
export async function executeScheduledTool(
|
||||||
|
userId: number | undefined | null,
|
||||||
toolCall: ToolCallData,
|
toolCall: ToolCallData,
|
||||||
message: TelegramStreamMessage,
|
message: TelegramStreamMessage,
|
||||||
context: ToolRuntimeContext,
|
context: ToolRuntimeContext,
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
const keys = toolResourceKeys(toolCall);
|
const keys = toolResourceKeys(toolCall);
|
||||||
if (!keys.length) return executeTool(toolCall, message, context);
|
if (!keys.length) return executeTool(userId, toolCall, message, context);
|
||||||
return runWithToolLocks(keys, () => executeTool(toolCall, message, context));
|
return runWithToolLocks(keys, () => executeTool(userId, toolCall, message, context));
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function executeToolBatch(
|
export async function executeToolBatch(
|
||||||
|
userId: number | undefined | null,
|
||||||
toolCalls: ToolCallData[],
|
toolCalls: ToolCallData[],
|
||||||
message: TelegramStreamMessage,
|
message: TelegramStreamMessage,
|
||||||
context: ToolRuntimeContext,
|
context: ToolRuntimeContext,
|
||||||
@@ -1417,7 +1420,7 @@ export async function executeToolBatch(
|
|||||||
message.setStatus(Environment.getUseToolText(statusCalls));
|
message.setStatus(Environment.getUseToolText(statusCalls));
|
||||||
await message.flush();
|
await message.flush();
|
||||||
|
|
||||||
const resultText = await executeScheduledTool(toolCall, message, context);
|
const resultText = await executeScheduledTool(userId, toolCall, message, context);
|
||||||
|
|
||||||
memory.set(signature, {
|
memory.set(signature, {
|
||||||
count: (previous?.count ?? 0) + 1,
|
count: (previous?.count ?? 0) + 1,
|
||||||
@@ -1626,9 +1629,9 @@ export function allToolSchemaNames(tools: readonly unknown[]): string[] {
|
|||||||
return [...new Set(tools.flatMap(toolSchemaNames))];
|
return [...new Set(tools.flatMap(toolSchemaNames))];
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getOpenAIResponsesToolsWithImage(config: RuntimeConfigSnapshot): Array<OpenAiResponseTool | LooseRecord> {
|
export function getOpenAIResponsesToolsWithImage(config: RuntimeConfigSnapshot, forCreator?: boolean): Array<OpenAiResponseTool | LooseRecord> {
|
||||||
return [
|
return [
|
||||||
...getOpenAIResponsesTools(),
|
...getOpenAIResponsesTools(forCreator),
|
||||||
getOpenAICodeInterpreterTool(),
|
getOpenAICodeInterpreterTool(),
|
||||||
{
|
{
|
||||||
type: "image_generation",
|
type: "image_generation",
|
||||||
@@ -1667,7 +1670,7 @@ export type OpenAiCodeInterpreterCall = {
|
|||||||
code: string | null;
|
code: string | null;
|
||||||
containerId: string;
|
containerId: string;
|
||||||
status: string;
|
status: string;
|
||||||
outputs: Array<{type?: "logs" | "image"; logs?: string; url?: string}>;
|
outputs: Array<{ type?: "logs" | "image"; logs?: string; url?: string }>;
|
||||||
};
|
};
|
||||||
|
|
||||||
export function collectOpenAiResponseCodeInterpreterCalls(response: OpenAiResponseLike): OpenAiCodeInterpreterCall[] {
|
export function collectOpenAiResponseCodeInterpreterCalls(response: OpenAiResponseLike): OpenAiCodeInterpreterCall[] {
|
||||||
|
|||||||
+26
-12
@@ -19,15 +19,13 @@ import {isTranscribableAudioDownload} from "./speech-to-text";
|
|||||||
import {OpenAIChatMessage} from "./openai-chat-message";
|
import {OpenAIChatMessage} from "./openai-chat-message";
|
||||||
import {MistralChatMessage} from "./mistral-chat-message";
|
import {MistralChatMessage} from "./mistral-chat-message";
|
||||||
import {OllamaChatMessage} from "./ollama-chat-message";
|
import {OllamaChatMessage} from "./ollama-chat-message";
|
||||||
import {GeminiMessage} from "./gemini-chat-message";
|
|
||||||
import {buildAiRegenerateCallbackData} from "./regenerate-callback";
|
import {buildAiRegenerateCallbackData} from "./regenerate-callback";
|
||||||
import {createOllamaClient, getGeminiApiMode} from "./ai-runtime-target";
|
import {createOllamaClient} from "./ai-runtime-target";
|
||||||
import {aiLog, aiLogDuration, aiLogMessageIdentity, aiLogProviderTarget} from "../logging/ai-logger";
|
import {aiLog, aiLogDuration, aiLogMessageIdentity, aiLogProviderTarget} from "../logging/ai-logger";
|
||||||
|
|
||||||
import {runOpenAi, runOpenAiCompatibleChat} from "./unified-ai-runner.openai";
|
import {runOpenAi} from "./unified-ai-runner.openai";
|
||||||
import {runOllama} from "./unified-ai-runner.ollama";
|
import {runOllama} from "./unified-ai-runner.ollama";
|
||||||
import {runMistral} from "./unified-ai-runner.mistral";
|
import {runMistral} from "./unified-ai-runner.mistral";
|
||||||
import {runGemini} from "./unified-ai-runner.gemini";
|
|
||||||
import {
|
import {
|
||||||
AI_REQUEST_TIMEOUT_MS,
|
AI_REQUEST_TIMEOUT_MS,
|
||||||
appendTranscriptToChatMessages,
|
appendTranscriptToChatMessages,
|
||||||
@@ -35,6 +33,7 @@ import {
|
|||||||
collectRequestedAttachmentKinds,
|
collectRequestedAttachmentKinds,
|
||||||
collectTextMessages,
|
collectTextMessages,
|
||||||
deleteMistralLibrary,
|
deleteMistralLibrary,
|
||||||
|
GeminiMessage,
|
||||||
hasAudioAttachmentKind,
|
hasAudioAttachmentKind,
|
||||||
initialStatus,
|
initialStatus,
|
||||||
isAbortError,
|
isAbortError,
|
||||||
@@ -51,6 +50,8 @@ import {
|
|||||||
transcribeAudioIfNeeded,
|
transcribeAudioIfNeeded,
|
||||||
UnifiedRunOptions
|
UnifiedRunOptions
|
||||||
} from "./unified-ai-runner.shared";
|
} from "./unified-ai-runner.shared";
|
||||||
|
import {runGemini} from "./unified-ai-runner.gemini";
|
||||||
|
import {resolveTextToSpeechProviderForUser, sendSynthesizedSpeech, synthesizeSpeech} from "./text-to-speech";
|
||||||
|
|
||||||
export type {ToolCallData} from "./unified-ai-runner.shared";
|
export type {ToolCallData} from "./unified-ai-runner.shared";
|
||||||
export {snapshotModel, providerTargets, ollamaModelNames} from "./unified-ai-runner.shared";
|
export {snapshotModel, providerTargets, ollamaModelNames} from "./unified-ai-runner.shared";
|
||||||
@@ -165,8 +166,7 @@ async function executeUnifiedAiRequest(
|
|||||||
|
|
||||||
switch (options.provider) {
|
switch (options.provider) {
|
||||||
case AiProvider.OPENAI:
|
case AiProvider.OPENAI:
|
||||||
await runOpenAi(options.msg, chatMessages as OpenAIChatMessage[], streamMessage, controller.signal, options.stream ?? true, firstRoundStatus, options.msg, config, toolContext,
|
await runOpenAi(options.msg, chatMessages as OpenAIChatMessage[], streamMessage, controller.signal, options.stream ?? true, options.msg, config, toolContext);
|
||||||
!!options.think);
|
|
||||||
break;
|
break;
|
||||||
case AiProvider.OLLAMA:
|
case AiProvider.OLLAMA:
|
||||||
const currentModel = config.ollamaChatTarget.model;
|
const currentModel = config.ollamaChatTarget.model;
|
||||||
@@ -179,14 +179,10 @@ async function executeUnifiedAiRequest(
|
|||||||
await runOllama(options.msg, chatMessages as ChatMessage[], streamMessage, controller.signal, ifTrue(options.stream), options.think ?? false, firstRoundStatus, config, toolContext, options.contextSize);
|
await runOllama(options.msg, chatMessages as ChatMessage[], streamMessage, controller.signal, ifTrue(options.stream), options.think ?? false, firstRoundStatus, config, toolContext, options.contextSize);
|
||||||
break;
|
break;
|
||||||
case AiProvider.MISTRAL:
|
case AiProvider.MISTRAL:
|
||||||
await runMistral(chatMessages as MistralChatMessage[], documents, streamMessage, controller.signal, options.stream ?? true, firstRoundStatus, config, toolContext);
|
await runMistral(options.msg, chatMessages as MistralChatMessage[], documents, streamMessage, controller.signal, options.stream ?? true, firstRoundStatus, config, toolContext);
|
||||||
break;
|
break;
|
||||||
case AiProvider.GEMINI:
|
case AiProvider.GEMINI:
|
||||||
if (getGeminiApiMode(config.geminiChatTarget) === "openai") {
|
await runGemini(options.msg, chatMessages as GeminiMessage[], streamMessage, controller.signal, options.stream ?? true, firstRoundStatus, config, toolContext);
|
||||||
await runOpenAiCompatibleChat(options.msg, chatMessages as OpenAIChatMessage[], streamMessage, controller.signal, options.stream ?? true, firstRoundStatus, config, toolContext);
|
|
||||||
} else {
|
|
||||||
await runGemini(chatMessages as GeminiMessage[], streamMessage, controller.signal, options.stream ?? true, firstRoundStatus, config, toolContext);
|
|
||||||
}
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,6 +212,24 @@ async function executeUnifiedAiRequest(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// noinspection JSUnusedLocalSymbols
|
||||||
|
// @ts-ignore
|
||||||
|
async function sendVoiceResponseIfNeeded(options: UnifiedRunOptions, downloads: AiDownloadedFile[], text: string): Promise<void> {
|
||||||
|
if (!downloads.some(isTranscribableAudioDownload)) return;
|
||||||
|
if (!options.msg.from?.id) return;
|
||||||
|
|
||||||
|
const trimmed = text.trim();
|
||||||
|
if (!trimmed) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const provider = (await resolveTextToSpeechProviderForUser(options.msg.from.id)).provider;
|
||||||
|
const speech = await synthesizeSpeech({provider, text: trimmed});
|
||||||
|
await sendSynthesizedSpeech(options.msg, speech);
|
||||||
|
} catch (e) {
|
||||||
|
logError(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export async function runUnifiedAi(options: UnifiedRunOptions): Promise<void> {
|
export async function runUnifiedAi(options: UnifiedRunOptions): Promise<void> {
|
||||||
const startedAt = Date.now();
|
const startedAt = Date.now();
|
||||||
const config = snapshotRuntimeConfig();
|
const config = snapshotRuntimeConfig();
|
||||||
|
|||||||
+8
-2
@@ -190,6 +190,7 @@ export const videoDir = path.join(cacheDir, "video");
|
|||||||
export const videoNotesDir = path.join(cacheDir, "video-note");
|
export const videoNotesDir = path.join(cacheDir, "video-note");
|
||||||
export const videoTempDir = path.join(videoDir, "temp");
|
export const videoTempDir = path.join(videoDir, "temp");
|
||||||
|
|
||||||
|
export const filesDir = path.join(Environment.DATA_PATH, "files");
|
||||||
|
|
||||||
export const NOTES_HEADER = "## Notes\n";
|
export const NOTES_HEADER = "## Notes\n";
|
||||||
export const notesDir = path.join(Environment.DATA_PATH, "notes");
|
export const notesDir = path.join(Environment.DATA_PATH, "notes");
|
||||||
@@ -224,7 +225,7 @@ async function main() {
|
|||||||
dbPath: Environment.DB_PATH,
|
dbPath: Environment.DB_PATH,
|
||||||
});
|
});
|
||||||
|
|
||||||
const dirsToCheck = [cacheDir, photoDir, photoGenDir, documentDir, audioDir, videoDir, videoNotesDir, videoTempDir, notesDir];
|
const dirsToCheck = [cacheDir, photoDir, photoGenDir, documentDir, audioDir, videoDir, videoNotesDir, videoTempDir, notesDir, filesDir];
|
||||||
dirsToCheck.forEach(dir => {
|
dirsToCheck.forEach(dir => {
|
||||||
if (!fs.existsSync(dir)) {
|
if (!fs.existsSync(dir)) {
|
||||||
fs.mkdirSync(dir, {recursive: true});
|
fs.mkdirSync(dir, {recursive: true});
|
||||||
@@ -282,7 +283,12 @@ async function main() {
|
|||||||
|
|
||||||
const end = Date.now();
|
const end = Date.now();
|
||||||
const diff = Math.abs(end - start);
|
const diff = Math.abs(end - start);
|
||||||
logger.success("startup.ready", {duration: `${diff}ms`, commands: cmds.length, botId: botUser.id, botUsername: botUser.username});
|
logger.success("startup.ready", {
|
||||||
|
duration: `${diff}ms`,
|
||||||
|
commands: cmds.length,
|
||||||
|
botId: botUser.id,
|
||||||
|
botUsername: botUser.username
|
||||||
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logError(error);
|
logError(error);
|
||||||
}
|
}
|
||||||
|
|||||||
+11
-2
@@ -2122,7 +2122,12 @@ export async function processNewMessage(msg: Message, isGuest?: boolean): Promis
|
|||||||
UserStore.put(from)
|
UserStore.put(from)
|
||||||
]
|
]
|
||||||
);
|
);
|
||||||
messageLogger.debug("message.persisted", {chatId: msg.chat.id, messageId: msg.message_id, fromId: from.id, duration: logger.duration(startedAt)});
|
messageLogger.debug("message.persisted", {
|
||||||
|
chatId: msg.chat.id,
|
||||||
|
messageId: msg.message_id,
|
||||||
|
fromId: from.id,
|
||||||
|
duration: logger.duration(startedAt)
|
||||||
|
});
|
||||||
|
|
||||||
storedMsg = results[0];
|
storedMsg = results[0];
|
||||||
locale = await resolveInterfaceLocaleForUser(from.id, from.language_code);
|
locale = await resolveInterfaceLocaleForUser(from.id, from.language_code);
|
||||||
@@ -2202,7 +2207,11 @@ export async function processNewMessage(msg: Message, isGuest?: boolean): Promis
|
|||||||
|| !!msg.video_note;
|
|| !!msg.video_note;
|
||||||
const hasImageAttachment = !!msg.photo?.length || !!msg.document?.mime_type?.startsWith("image/");
|
const hasImageAttachment = !!msg.photo?.length || !!msg.document?.mime_type?.startsWith("image/");
|
||||||
if (executed) {
|
if (executed) {
|
||||||
messageLogger.debug("message.command_executed", {chatId: msg.chat.id, messageId: msg.message_id, command: cmd?.title});
|
messageLogger.debug("message.command_executed", {
|
||||||
|
chatId: msg.chat.id,
|
||||||
|
messageId: msg.message_id,
|
||||||
|
command: cmd?.title
|
||||||
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user