This commit is contained in:
2026-05-14 21:20:43 +03:00
parent c1a913d5e4
commit 2b1940bf4d
5 changed files with 169 additions and 164 deletions
+5 -5
View File
@@ -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.",
@@ -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");
@@ -338,3 +339,110 @@ async function removeNoteLinkFromRoot(noteFilePath: string): Promise<void> {
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<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}`};
}
}
+37 -34
View File
@@ -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,16 +38,30 @@ 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 = () => {
const tools: AiTool[] = [
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,
@@ -55,16 +69,14 @@ export const getTools = () => {
deleteNoteTool,
sendNoteAsFileTool,
searchNotesTool
]
export const getTools = (forCreator?: boolean) => {
const tools: AiTool[] = [
...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:
-113
View File
@@ -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 -2
View File
@@ -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(