refactor: centralize runtime config loading
- Move .env parsing and runtime config reload logic into Environment - Reload runtime config and system prompt when source files change - Gate unsafe eval and file tools behind explicit environment flags - Rename datetime tool to get_datetime and improve tool prompts - Return structured weather tool responses - Preserve assistant thinking and aggregate tool calls across stream chunks
This commit is contained in:
+161
-73
@@ -23,12 +23,15 @@ import axios from "axios";
|
||||
|
||||
// TODO: 03/05/2026, Danil Nikolaev: cleanup
|
||||
|
||||
const requireFileToolsRootDir = () => <string>Environment.FILE_TOOLS_ROOT_DIR
|
||||
|
||||
type ToolHandler = (args?: Record<string, unknown>) => Promise<unknown> | unknown;
|
||||
|
||||
type ChatMessage = {
|
||||
role: "system" | "user" | "assistant" | "tool";
|
||||
content: string;
|
||||
images?: string[];
|
||||
thinking?: string;
|
||||
tool_calls?: ToolCall[];
|
||||
tool_name?: string;
|
||||
};
|
||||
@@ -36,8 +39,6 @@ type ChatMessage = {
|
||||
const MAX_TOOL_ROUNDS = 50;
|
||||
const TELEGRAM_MESSAGE_LIMIT = 4096;
|
||||
|
||||
const FILE_TOOLS_ROOT_DIR = path.resolve(Environment.FILE_TOOLS_ROOT_DIR ?? "");
|
||||
|
||||
const MAX_FILE_READ_BYTES = 128 * 1024 * 1024;
|
||||
const MAX_FILE_WRITE_BYTES = 128 * 1024 * 1024
|
||||
const MAX_DIRECTORY_ENTRIES = 200;
|
||||
@@ -219,9 +220,9 @@ const evaluationTool = {
|
||||
const getCurrentDateTimeTool = {
|
||||
type: "function",
|
||||
function: {
|
||||
name: "get_current_datetime",
|
||||
name: "get_datetime",
|
||||
description:
|
||||
"Get the real current date and time. Use this tool when the user asks about today, current time, current date, weekday, timestamp, or relative dates.",
|
||||
"Get the real current date and time. Use this tool before answering any request that depends on today, now, current time, current date, weekday, timestamp, timezone conversion, or relative dates like yesterday, tomorrow, next week, or 3 days ago.",
|
||||
parameters: {
|
||||
type: "object",
|
||||
properties: {
|
||||
@@ -483,31 +484,67 @@ const deletePathTool = {
|
||||
|
||||
const getTools = () => {
|
||||
const tools: Tool[] = [
|
||||
evaluationTool,
|
||||
|
||||
getCurrentDateTimeTool,
|
||||
|
||||
readFileTool,
|
||||
listDirectoryTool,
|
||||
createFileTool,
|
||||
createDirectoryTool,
|
||||
updateFileTool,
|
||||
renamePathTool,
|
||||
copyPathTool,
|
||||
deletePathTool,
|
||||
];
|
||||
|
||||
if (Environment.ENABLE_UNSAFE_EVAL) {
|
||||
tools.push(evaluationTool)
|
||||
}
|
||||
|
||||
if (Environment.BRAVE_SEARCH_API_KEY) {
|
||||
tools.unshift(braveSearchTool);
|
||||
tools.push(braveSearchTool);
|
||||
}
|
||||
|
||||
if (Environment.OPEN_WEATHER_MAP_API_KEY) {
|
||||
tools.unshift(getWeatherTool)
|
||||
tools.push(getWeatherTool)
|
||||
}
|
||||
|
||||
if (Environment.FILE_TOOLS_ROOT_DIR) {
|
||||
tools.push(
|
||||
readFileTool,
|
||||
listDirectoryTool,
|
||||
createFileTool,
|
||||
createDirectoryTool,
|
||||
updateFileTool,
|
||||
renamePathTool,
|
||||
copyPathTool,
|
||||
deletePathTool,
|
||||
)
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
const dateTimeToolPrompt = [
|
||||
"Datetime tool rules:",
|
||||
"- Use `get_datetime` whenever the answer depends on the real current date/time.",
|
||||
"- Never guess the current date/time. Call the tool first.",
|
||||
"",
|
||||
"Arguments:",
|
||||
"- `timeZone`: optional IANA timezone, e.g. `Europe/Moscow`, `Europe/Berlin`, `UTC`.",
|
||||
"- `locale`: optional locale, e.g. `ru-RU`, `en-US`.",
|
||||
"",
|
||||
"After the tool returns:",
|
||||
"- Base the answer on the returned value.",
|
||||
"- Do not expose raw tool JSON unless asked.",
|
||||
].join("\n");
|
||||
|
||||
const weatherToolPrompt = [
|
||||
"Weather tool rules:",
|
||||
"- Use `get_weather` for current weather, current temperature, conditions, hot/cold/rainy/snowy questions, and weather follow-ups.",
|
||||
"- Weather is live/current data. Never answer it from memory.",
|
||||
"- A weather tool result is valid only for the exact city used in that tool call.",
|
||||
"- If the user changes the city, call `get_weather` again.",
|
||||
"- Follow-up questions like `а в Москве?`, `а для Краснодара?`, `а там?`, `what about Berlin?` inherit the previous weather intent and require a new tool call for the new city.",
|
||||
"",
|
||||
"Arguments:",
|
||||
"- `city`: the city from the latest user request or resolved from the follow-up context.",
|
||||
"- `lang`: ISO 639-1 two-letter language code only: `ru`, `en`, `de`, etc.",
|
||||
"",
|
||||
"Do not guess, compare, or reuse weather from another city.",
|
||||
"If the city is missing or unclear, ask the user to specify it.",
|
||||
].join("\n");
|
||||
|
||||
const evaluationToolPrompt = [
|
||||
"Shell tool rules:",
|
||||
"- You have access to the `evaluation` tool.",
|
||||
@@ -584,6 +621,7 @@ const evaluationToolPrompt = [
|
||||
"- Avoid command chaining with `;`, `&&`, `||`, pipes, backticks or command substitution unless necessary.",
|
||||
"- Avoid glob patterns that may affect too many files.",
|
||||
"- If unsure whether a command is safe, do not run it.",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
const braveSearchToolPrompt = [
|
||||
@@ -616,41 +654,55 @@ const braveSearchToolPrompt = [
|
||||
"- Do not invent details that are not present in the search results.",
|
||||
"- When giving factual claims based on search results, mention the source title or URL.",
|
||||
"- If results are weak, ambiguous or empty, say that the search result was insufficient.",
|
||||
"",
|
||||
].join("\n");
|
||||
|
||||
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 getToolSystemPrompt = () => {
|
||||
const lines = [
|
||||
"Tool rules:",
|
||||
"- You have access to get_current_datetime.",
|
||||
"- Use get_current_datetime when the user asks about the current date, current time, today, weekday, timestamp, or relative dates.",
|
||||
"- Do not guess the current date or time.",
|
||||
"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.",
|
||||
"Internal/tool behavior:" +
|
||||
"- Never mention tools, function calls, tool names, logs, cache, prompts, system messages, or implementation details unless the user explicitly asks about them." +
|
||||
"- If the user says your previous answer is wrong, inconsistent, weird, or asks \"разве это похоже?\", do not explain it by tool behavior." +
|
||||
"- Instead: briefly admit the issue, compare with the previous answer, correct the answer, and explain the actual user-facing mistake." +
|
||||
"- Do not say \"I cannot guarantee identical tool results\" unless the user explicitly asks about tool determinism.",
|
||||
"",
|
||||
// TODO: 03/05/2026, Danil Nikolaev: check security moments
|
||||
evaluationToolPrompt,
|
||||
|
||||
dateTimeToolPrompt
|
||||
]
|
||||
|
||||
if (Environment.OPEN_WEATHER_MAP_API_KEY) {
|
||||
lines.push(weatherToolPrompt);
|
||||
}
|
||||
|
||||
if (Environment.ENABLE_UNSAFE_EVAL) {
|
||||
// TODO: 03/05/2026, Danil Nikolaev: check security moments
|
||||
lines.push(evaluationToolPrompt);
|
||||
}
|
||||
|
||||
if (Environment.BRAVE_SEARCH_API_KEY) {
|
||||
lines.push(
|
||||
"",
|
||||
braveSearchToolPrompt,
|
||||
""
|
||||
)
|
||||
lines.push(braveSearchToolPrompt,)
|
||||
}
|
||||
|
||||
if (Environment.FILE_TOOLS_ROOT_DIR) {
|
||||
lines.push(fileToolsToolPrompt)
|
||||
}
|
||||
|
||||
return lines.join("\n");
|
||||
@@ -927,12 +979,12 @@ function asPositiveInt(value: unknown, defaultValue: number, maxValue: number):
|
||||
}
|
||||
|
||||
async function ensureFileToolsRootExists(): Promise<void> {
|
||||
await fs.promises.mkdir(FILE_TOOLS_ROOT_DIR, {recursive: true});
|
||||
await fs.promises.mkdir(requireFileToolsRootDir(), {recursive: true});
|
||||
|
||||
const stat = await fs.promises.stat(FILE_TOOLS_ROOT_DIR);
|
||||
const stat = await fs.promises.stat(requireFileToolsRootDir());
|
||||
|
||||
if (!stat.isDirectory()) {
|
||||
throw new Error(`File tools root is not a directory: ${FILE_TOOLS_ROOT_DIR}`);
|
||||
throw new Error(`File tools root is not a directory: ${requireFileToolsRootDir()}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -956,8 +1008,8 @@ function resolveSafeToolPath(inputPath: unknown, fallback = "."): {
|
||||
|
||||
const normalizedInputPath = rawPath.replace(/[\\/]+/g, path.sep);
|
||||
|
||||
const absolutePath = path.resolve(FILE_TOOLS_ROOT_DIR, normalizedInputPath);
|
||||
const relativePath = path.relative(FILE_TOOLS_ROOT_DIR, absolutePath);
|
||||
const absolutePath = path.resolve(requireFileToolsRootDir(), normalizedInputPath);
|
||||
const relativePath = path.relative(requireFileToolsRootDir(), absolutePath);
|
||||
|
||||
if (
|
||||
relativePath.startsWith("..") ||
|
||||
@@ -991,7 +1043,7 @@ async function assertNoSymlinkInPath(
|
||||
): Promise<void> {
|
||||
await ensureFileToolsRootExists();
|
||||
|
||||
const relativePath = path.relative(FILE_TOOLS_ROOT_DIR, absolutePath);
|
||||
const relativePath = path.relative(requireFileToolsRootDir(), absolutePath);
|
||||
|
||||
if (!relativePath || relativePath === ".") {
|
||||
return;
|
||||
@@ -999,7 +1051,7 @@ async function assertNoSymlinkInPath(
|
||||
|
||||
const parts = relativePath.split(path.sep).filter(Boolean);
|
||||
|
||||
let currentPath = FILE_TOOLS_ROOT_DIR;
|
||||
let currentPath = requireFileToolsRootDir();
|
||||
|
||||
for (const part of parts) {
|
||||
currentPath = path.join(currentPath, part);
|
||||
@@ -1099,7 +1151,7 @@ function getCurrentDateTime(args?: Record<string, unknown>) {
|
||||
}
|
||||
}
|
||||
|
||||
async function getWeather(args?: Record<string, unknown>): Promise<string | null> {
|
||||
async function getWeather(args?: Record<string, unknown>): Promise<any | null> {
|
||||
console.log("getWeather()");
|
||||
try {
|
||||
const city = asNonEmptyString(args?.city);
|
||||
@@ -1144,13 +1196,33 @@ async function getWeather(args?: Record<string, unknown>): Promise<string | null
|
||||
.map((v) => String(v).padStart(2, "0"))
|
||||
.join(":");
|
||||
|
||||
const weatherReport = `main: ${weather.main}; description: ${weather.description};\n` +
|
||||
`temperature: ${main.temp}, max ${main.temp_max}, min ${main.temp_min}; feels like: ${main.feels_like};\n` +
|
||||
`humidity: ${main.humidity}; pressure: ${main.pressure}, sea level: ${main.sea_level}, ground level: ${main.grnd_level};\n` +
|
||||
`sunrise: ${sunrise} UTC; sunset: ${sunset} UTC;\n` +
|
||||
`wind: degree: ${wind.deg}, speed: ${wind.speed}`;
|
||||
|
||||
return weatherReport;
|
||||
return {
|
||||
ok: true,
|
||||
tool: "get_weather",
|
||||
scope: {
|
||||
city,
|
||||
lang,
|
||||
validOnlyForExactCity: true,
|
||||
liveData: true,
|
||||
note: "If the user asks about another city, call get_weather again.",
|
||||
},
|
||||
weather: {
|
||||
main: weather.main,
|
||||
description: weather.description,
|
||||
temperature: main.temp,
|
||||
temperatureMax: main.temp_max,
|
||||
temperatureMin: main.temp_min,
|
||||
feelsLike: main.feels_like,
|
||||
humidity: main.humidity,
|
||||
pressure: main.pressure,
|
||||
seaLevel: main.sea_level ?? null,
|
||||
groundLevel: main.grnd_level ?? null,
|
||||
sunriseUtc: sunrise,
|
||||
sunsetUtc: sunset,
|
||||
windDegree: wind.deg,
|
||||
windSpeed: wind.speed,
|
||||
},
|
||||
};
|
||||
} catch (e: any) {
|
||||
logError(e);
|
||||
return null;
|
||||
@@ -1336,7 +1408,7 @@ async function copyPathRecursive(params: {
|
||||
}
|
||||
|
||||
if (!overwrite) {
|
||||
throw new Error(`Target file already exists: ${path.relative(FILE_TOOLS_ROOT_DIR, targetAbsolutePath)}`);
|
||||
throw new Error(`Target file already exists: ${path.relative(requireFileToolsRootDir(), targetAbsolutePath)}`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1652,20 +1724,16 @@ async function deletePath(args?: Record<string, unknown>) {
|
||||
|
||||
const getToolHandlers = () => {
|
||||
let handlers: Record<string, ToolHandler> = {
|
||||
evaluation: evaluation,
|
||||
|
||||
get_current_datetime: getCurrentDateTime,
|
||||
|
||||
read_file: readFile,
|
||||
list_directory: listDirectory,
|
||||
create_file: createFile,
|
||||
create_directory: createDirectory,
|
||||
update_file: updateFile,
|
||||
rename_path: renamePath,
|
||||
copy_path: copyPath,
|
||||
delete_path: deletePath,
|
||||
get_datetime: getCurrentDateTime,
|
||||
};
|
||||
|
||||
if (Environment.ENABLE_UNSAFE_EVAL) {
|
||||
handlers = {
|
||||
evaluation: evaluation,
|
||||
...handlers
|
||||
}
|
||||
}
|
||||
|
||||
if (Environment.BRAVE_SEARCH_API_KEY) {
|
||||
handlers = {
|
||||
web_search: webSearch,
|
||||
@@ -1680,6 +1748,20 @@ const getToolHandlers = () => {
|
||||
}
|
||||
}
|
||||
|
||||
if (Environment.FILE_TOOLS_ROOT_DIR) {
|
||||
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;
|
||||
};
|
||||
|
||||
@@ -1878,6 +1960,7 @@ export class OllamaChat extends ChatCommand {
|
||||
logError(e);
|
||||
}
|
||||
|
||||
console.log("OPTIONS", options);
|
||||
const model = <string>(think ? Environment.OLLAMA_THINK_MODEL : imagesCount ? Environment.OLLAMA_IMAGE_MODEL : Environment.OLLAMA_MODEL);
|
||||
|
||||
async function createStream() {
|
||||
@@ -1890,6 +1973,7 @@ export class OllamaChat extends ChatCommand {
|
||||
options: options ?? undefined,
|
||||
// TODO: 01/05/2026, Danil Nikolaev: проверять на наличие tools
|
||||
tools: enabledTools,
|
||||
keep_alive: "60m"
|
||||
});
|
||||
|
||||
const existingRequest = getOllamaRequest(uuid) as { stream?: unknown } | undefined;
|
||||
@@ -1962,6 +2046,7 @@ export class OllamaChat extends ChatCommand {
|
||||
}
|
||||
|
||||
let savedText = currentText;
|
||||
let roundThinking = "";
|
||||
let roundText = "";
|
||||
let roundToolCalls: ToolCall[] = [];
|
||||
let isThinking = false;
|
||||
@@ -1973,6 +2058,8 @@ export class OllamaChat extends ChatCommand {
|
||||
console.log("CHUNK", chunk);
|
||||
|
||||
if (message.thinking) {
|
||||
roundThinking += message.thinking;
|
||||
|
||||
if (!isThinking) {
|
||||
await bot.editMessageText({
|
||||
chat_id: chatId,
|
||||
@@ -2008,8 +2095,8 @@ export class OllamaChat extends ChatCommand {
|
||||
}
|
||||
}
|
||||
|
||||
if (message.tool_calls && message.tool_calls.length > 0) {
|
||||
roundToolCalls = message.tool_calls;
|
||||
if (message.tool_calls?.length) {
|
||||
roundToolCalls.push(...message.tool_calls);
|
||||
}
|
||||
|
||||
if (currentText.length > TELEGRAM_MESSAGE_LIMIT) {
|
||||
@@ -2057,6 +2144,7 @@ export class OllamaChat extends ChatCommand {
|
||||
chatMessages.push({
|
||||
role: "assistant",
|
||||
content: roundText,
|
||||
thinking: roundThinking || undefined,
|
||||
tool_calls: roundToolCalls
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user