From 2b1940bf4d773e133df533e2fce4baf7164cc4bf Mon Sep 17 00:00:00 2001 From: Danil Nikolaev Date: Thu, 14 May 2026 21:20:43 +0300 Subject: [PATCH] shitton --- src/ai/tools/market-rates.ts | 10 +- src/ai/tools/{list-notes.ts => notes.ts} | 110 +++++++++++++++++++++- src/ai/tools/registry.ts | 89 +++++++++--------- src/ai/tools/send-note-as-file.ts | 113 ----------------------- src/ai/unified-ai-runner.ollama.ts | 11 ++- 5 files changed, 169 insertions(+), 164 deletions(-) rename src/ai/tools/{list-notes.ts => notes.ts} (76%) delete mode 100644 src/ai/tools/send-note-as-file.ts diff --git a/src/ai/tools/market-rates.ts b/src/ai/tools/market-rates.ts index fe0e763..13dfc33 100644 --- a/src/ai/tools/market-rates.ts +++ b/src/ai/tools/market-rates.ts @@ -4,12 +4,12 @@ import {toolsLogger} from "./tool-logger"; 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 = { type: "function", function: { - name: GET_FINANCIAL_MARKET_DATA, + name: GET_FINANCIAL_MARKET_DATA_TOOL_NAME, 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.", parameters: { @@ -22,9 +22,9 @@ export const getFinancialMarketData = { export const financialMarketDataToolPrompt = [ "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}\` 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}\` whenever the answer depends on current exchange rates, crypto prices, or gold price.`, + `- 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_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.", "- 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.", diff --git a/src/ai/tools/list-notes.ts b/src/ai/tools/notes.ts similarity index 76% rename from src/ai/tools/list-notes.ts rename to src/ai/tools/notes.ts index 8558436..5b8ebdd 100644 --- a/src/ai/tools/list-notes.ts +++ b/src/ai/tools/notes.ts @@ -1,9 +1,10 @@ import {AiTool} from "../tool-types"; 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 {asNonEmptyString} from "./utils"; import {toolsLogger} from "./tool-logger"; +import {z} from "zod"; const logger = toolsLogger.child("notes"); @@ -337,4 +338,111 @@ async function removeNoteLinkFromRoot(noteFilePath: string): Promise { function escapeRegExp(value: string): string { 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, +): Promise { + 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}`}; + } } \ No newline at end of file diff --git a/src/ai/tools/registry.ts b/src/ai/tools/registry.ts index b8261d5..c42cc38 100644 --- a/src/ai/tools/registry.ts +++ b/src/ai/tools/registry.ts @@ -7,7 +7,7 @@ import {ToolHandler} from "./types"; import {getWeather, getWeatherTool} from "./weather"; import { financialMarketDataToolPrompt, - GET_FINANCIAL_MARKET_DATA, + GET_FINANCIAL_MARKET_DATA_TOOL_NAME, getFinancialMarketData, getMarketRates } from "./market-rates"; @@ -38,33 +38,45 @@ import { getNoteContentTool, listNotes, listNotesTool, + sendNoteAsFile, + sendNoteAsFileTool, updateNoteContent, updateNoteContentTool -} from "./list-notes"; -import {sendNoteAsFileTool, sendNoteAsFile} from "./send-note-as-file"; +} from "./notes"; import {searchNotes, searchNotesTool} from "./search-notes"; -export const getTools = () => { +export const defaultFileTools: AiTool[] = [ + getCurrentDateTimeTool, + getFinancialMarketData, +] + +export const fileSystemTools: AiTool[] = [ + readFileTool, + listDirectoryTool, + createFileTool, + createDirectoryTool, + updateFileTool, + renamePathTool, + copyPathTool, + deletePathTool, +]; + +export const notesFileTools: AiTool[] = [ + createNoteTool, + listNotesTool, + getNoteContentTool, + updateNoteContentTool, + deleteNoteTool, + sendNoteAsFileTool, + searchNotesTool +] + +export const getTools = (forCreator?: boolean) => { const tools: AiTool[] = [ - getCurrentDateTimeTool, - getFinancialMarketData, - createNoteTool, - listNotesTool, - getNoteContentTool, - updateNoteContentTool, - deleteNoteTool, - sendNoteAsFileTool, - searchNotesTool + ...defaultFileTools, + ...notesFileTools ]; - if (Environment.ENABLE_PYTHON_INTERPRETER) { - tools.push(pythonInterpreterTool); - } - - if (Environment.ENABLE_UNSAFE_EVAL) { - tools.push(shellExecuteTool); - } - if (Environment.BRAVE_SEARCH_API_KEY) { tools.push(braveSearchTool); } @@ -73,30 +85,21 @@ export const getTools = () => { tools.push(getWeatherTool); } - if (Environment.FILE_TOOLS_ROOT_DIR && Environment.ENABLE_FS_TOOLS) { - tools.push( - 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); + } + + if (Environment.FILE_TOOLS_ROOT_DIR && Environment.ENABLE_FS_TOOLS) { + tools.push(...fileSystemTools); + } } - return tools; - // return [ - // createNoteTool, - // listNotesTool, - // getNoteContentTool, - // updateNoteContentTool, - // deleteNoteTool, - // getNoteFileTool, - // searchNotesTool - // ]; }; export const getToolHandlers = () => { @@ -162,7 +165,7 @@ export function getToolPrompts(toolNames: string[]): string[] { for (const toolName of toolNames) { switch (toolName) { - case GET_FINANCIAL_MARKET_DATA: + case GET_FINANCIAL_MARKET_DATA_TOOL_NAME: prompts.push(financialMarketDataToolPrompt); break; default: diff --git a/src/ai/tools/send-note-as-file.ts b/src/ai/tools/send-note-as-file.ts deleted file mode 100644 index 6dddac2..0000000 --- a/src/ai/tools/send-note-as-file.ts +++ /dev/null @@ -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, -): Promise { - 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}`}; - } -} \ No newline at end of file diff --git a/src/ai/unified-ai-runner.ollama.ts b/src/ai/unified-ai-runner.ollama.ts index 6cc04e8..7928bed 100644 --- a/src/ai/unified-ai-runner.ollama.ts +++ b/src/ai/unified-ai-runner.ollama.ts @@ -16,7 +16,6 @@ import {getFinancialMarketData} from "./tools/market-rates"; import {getWeatherTool} from "./tools/weather"; import {loadOllamaModel, unloadAllOllamaModels} from "./tools/utils"; 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 { @@ -42,7 +41,15 @@ import { import {latestUserTextFromOllamaMessages, OllamaToolRanker} from "./unified-ai-runner.tool-ranker"; import {getToolPrompts} from "./tools/registry"; import {createNoteTool} from "./tools/create-note"; -import {deleteNoteTool, getNoteContentTool, listNotesTool, updateNoteContentTool} from "./tools/list-notes"; +import { + deleteNoteTool, + getNoteContentTool, + GetNoteFileResult, + GetNoteFileResultSchema, + listNotesTool, + sendNoteAsFileTool, + updateNoteContentTool +} from "./tools/notes"; import {searchNotesTool} from "./tools/search-notes"; export async function runOllama(