diff --git a/src/commands/gemini-chat.ts b/src/commands/gemini-chat.ts index 937cbb5..d38785c 100644 --- a/src/commands/gemini-chat.ts +++ b/src/commands/gemini-chat.ts @@ -44,7 +44,10 @@ export class GeminiChat extends ChatCommand { }; }); chatMessages.reverse(); - chatMessages.unshift({role: "system", content: Environment.SYSTEM_PROMPT}); + + if (Environment.SYSTEM_PROMPT) { + chatMessages.unshift({role: "system", content: Environment.SYSTEM_PROMPT}); + } let chatContent = ""; for (const part of chatMessages) { diff --git a/src/commands/mistral-chat.ts b/src/commands/mistral-chat.ts index d5b72a7..44e48b0 100644 --- a/src/commands/mistral-chat.ts +++ b/src/commands/mistral-chat.ts @@ -58,7 +58,10 @@ export class MistralChat extends ChatCommand { }; }); chatMessages.reverse(); - chatMessages.unshift({role: "system", content: [{type: "text", text: Environment.SYSTEM_PROMPT}]}); + + if (Environment.SYSTEM_PROMPT) { + chatMessages.unshift({role: "system", content: [{type: "text", text: Environment.SYSTEM_PROMPT}]}); + } let waitMessage: Message; @@ -116,7 +119,7 @@ export class MistralChat extends ChatCommand { chat_id: chatId, message_id: waitMessage.message_id, text: escapeMarkdownV2Text(text), - parse_mode: "Markdown" + parse_mode: "MarkdownV2" } ).catch(logError); diff --git a/src/commands/ollama-chat.ts b/src/commands/ollama-chat.ts index 5330e8a..8338b87 100644 --- a/src/commands/ollama-chat.ts +++ b/src/commands/ollama-chat.ts @@ -44,7 +44,10 @@ export class OllamaChat extends ChatCommand { }; }); chatMessages.reverse(); - chatMessages.unshift({role: "system", content: Environment.SYSTEM_PROMPT, images: []}); + + if (Environment.SYSTEM_PROMPT) { + chatMessages.unshift({role: "system", content: Environment.SYSTEM_PROMPT, images: []}); + } let waitMessage: Message; diff --git a/src/commands/openai-chat.ts b/src/commands/openai-chat.ts index c171f61..c269680 100644 --- a/src/commands/openai-chat.ts +++ b/src/commands/openai-chat.ts @@ -58,11 +58,14 @@ export class OpenAIChat extends ChatCommand { }; }); chatMessages.reverse(); - chatMessages.unshift({ - role: "system", - content: [{type: "input_text", text: Environment.SYSTEM_PROMPT}], - type: "message" - }); + + if (Environment.SYSTEM_PROMPT) { + chatMessages.unshift({ + role: "system", + content: [{type: "input_text", text: Environment.SYSTEM_PROMPT}], + type: "message" + }); + } let waitMessage: Message; diff --git a/src/common/environment.ts b/src/common/environment.ts index 9089f3a..3e66e63 100644 --- a/src/common/environment.ts +++ b/src/common/environment.ts @@ -112,8 +112,6 @@ export class Environment { Environment.IMAGE_HANDLE_FALLBACK_POLICY = ImageHandleFallbackPolicy.NOTIFY_USER; } - Environment.SYSTEM_PROMPT = process.env.SYSTEM_PROMPT?.trim(); - Environment.OLLAMA_ADDRESS = process.env.OLLAMA_ADDRESS; Environment.OLLAMA_MODEL = process.env.OLLAMA_MODEL || "gemma3:4b"; Environment.OLLAMA_IMAGE_MODEL = process.env.OLLAMA_IMAGE_MODEL || Environment.OLLAMA_MODEL; @@ -133,6 +131,10 @@ export class Environment { Environment.OPENAI_IMAGE_MODEL = process.env.OPENAI_IMAGE_MODEL || "gpt-image-1-mini"; } + static setSystemPrompt(prompt: string) { + this.SYSTEM_PROMPT = prompt; + } + static setAdmins(admins: Set) { this.ADMIN_IDS = admins; } diff --git a/src/db/database.ts b/src/db/database.ts index f913b18..b9d894c 100644 --- a/src/db/database.ts +++ b/src/db/database.ts @@ -2,6 +2,7 @@ import * as fs from "fs"; import {Environment} from "../common/environment"; import {logError} from "../util/utils"; import {Answers} from "../model/answers"; +import path from "node:path"; type DataJsonFile = { admins: number[] @@ -27,6 +28,19 @@ export async function readData(): Promise { } } +export async function readPrompts(): Promise { + try { + const prompt = fs.readFileSync(path.join(Environment.DATA_PATH, "system_prompt.txt")).toString().trim(); + if (prompt.length) { + Environment.setSystemPrompt(prompt); + } + } catch (e) { + logError(e); + } + + return Promise.resolve(); +} + export async function saveData(): Promise { const adminIds: number[] = []; Environment.ADMIN_IDS.forEach(id => adminIds.push(id)); diff --git a/src/index.ts b/src/index.ts index 27ed069..13f8229 100644 --- a/src/index.ts +++ b/src/index.ts @@ -20,7 +20,7 @@ import {Ping} from "./commands/ping"; import {RandomString} from "./commands/random-string"; import {SystemInfo} from "./commands/system-info"; import {Test} from "./commands/test"; -import {readData, retrieveAnswers} from "./db/database"; +import {readData, readPrompts, retrieveAnswers} from "./db/database"; import {Uptime} from "./commands/uptime"; import {WhatBetter} from "./commands/what-better"; import {When} from "./commands/when"; @@ -252,6 +252,10 @@ async function shutdown(signal: NodeJS.Signals) { async function main() { const start = Date.now(); + await readPrompts(); + + console.log(Environment.SYSTEM_PROMPT); + console.log( `TEST_ENVIRONMENT: ${Environment.TEST_ENVIRONMENT}\n` + `DATA_PATH: ${Environment.DATA_PATH}\n` + diff --git a/src/util/utils.ts b/src/util/utils.ts index a03aa4e..c262a2f 100644 --- a/src/util/utils.ts +++ b/src/util/utils.ts @@ -452,12 +452,221 @@ export const delay = (ms: number, signal?: AbortSignal): Promise => } }); -export function escapeMarkdownV2Text(s: string) { - s = s.replace(/^\*{3,}\s*$/gm, "— — —"); - s = s.replace(/^\*\s+(?=\S)/gm, "• "); - s = s.replace(/\*\*(.+?)\*\*/g, "*$1*"); +const MARKDOWN_V2_RESERVED_RE = /([\\_*\[\]()~`>#+\-=|{}.!])/g; - return s; +function escapePlainMarkdownV2(s: string): string { + return s.replace(MARKDOWN_V2_RESERVED_RE, "\\$1"); +} + +function escapeCodeMarkdownV2(s: string): string { + return s.replace(/[\\`]/g, "\\$&"); +} + +function escapeLinkUrlMarkdownV2(s: string): string { + return s.replace(/[\\)]/g, "\\$&"); +} + +function escapeMarkdownV2PreservingAllowedFormatting(s: string): string { + let result = ""; + let i = 0; + + while (i < s.length) { + // links: [text](url) + if (s[i] === "[") { + const linkMatch = s.slice(i).match(/^\[([^\]\n]+)]\(([^)\n]+)\)/); + + if (linkMatch) { + const [, text, url] = linkMatch; + result += `[${escapePlainMarkdownV2(text)}](${escapeLinkUrlMarkdownV2(url)})`; + i += linkMatch[0].length; + continue; + } + } + + // monospace: `text` + if (s[i] === "`") { + const end = s.indexOf("`", i + 1); + + if (end !== -1) { + const content = s.slice(i + 1, end); + result += "`" + escapeCodeMarkdownV2(content) + "`"; + i = end + 1; + continue; + } + } + + // spoiler: ||text|| + if (s.startsWith("||", i)) { + const end = s.indexOf("||", i + 2); + + if (end !== -1) { + const content = s.slice(i + 2, end); + result += "||" + escapeMarkdownV2PreservingAllowedFormatting(content) + "||"; + i = end + 2; + continue; + } + } + + // underline: __text__ + if (s.startsWith("__", i)) { + const end = s.indexOf("__", i + 2); + + if (end !== -1) { + const content = s.slice(i + 2, end); + result += "__" + escapeMarkdownV2PreservingAllowedFormatting(content) + "__"; + i = end + 2; + continue; + } + } + + // bold: *text* + if (s[i] === "*") { + const end = s.indexOf("*", i + 1); + + if (end !== -1) { + const content = s.slice(i + 1, end); + result += "*" + escapeMarkdownV2PreservingAllowedFormatting(content) + "*"; + i = end + 1; + continue; + } + } + + // italic: _text_ + if (s[i] === "_") { + const end = s.indexOf("_", i + 1); + + if (end !== -1) { + const content = s.slice(i + 1, end); + result += "_" + escapeMarkdownV2PreservingAllowedFormatting(content) + "_"; + i = end + 1; + continue; + } + } + + // strikethrough: ~text~ + if (s[i] === "~") { + const end = s.indexOf("~", i + 1); + + if (end !== -1) { + const content = s.slice(i + 1, end); + result += "~" + escapeMarkdownV2PreservingAllowedFormatting(content) + "~"; + i = end + 1; + continue; + } + } + + result += escapePlainMarkdownV2(s[i]); + i++; + } + + return result; +} + +function unescapeAccidentalMarkdownV2(s: string): string { + return s.replace(/\\([_*\[\]()~`>#+\-=|{}.!\\])/g, "$1"); +} + +function escapeTelegramQuoteLine(line: string): string { + const content = line.replace(/^>\s*/, ""); + + if (!content.trim()) { + return ">"; + } + + return ">" + escapeMarkdownV2PreservingAllowedFormatting(content); +} + +function normalizeTelegramQuoteLines(s: string): string { + return s + .split("\n") + .map(line => { + if (!line.startsWith(">")) return line; + + return line.replace(/^>\s+/, ">"); + }) + .join("\n"); +} + +function looksLikeMarkdownTableRow(line: string): boolean { + const trimmed = line.trim(); + + if (trimmed.startsWith("||") && trimmed.endsWith("||")) { + return false; + } + + const pipeCount = (trimmed.match(/\|/g) ?? []).length; + + if (pipeCount < 2) { + return false; + } + + return trimmed.startsWith("|") || trimmed.endsWith("|") || pipeCount >= 2; +} + +function isMarkdownTableSeparator(line: string): boolean { + return /^\s*\|?\s*:?-{3,}:?\s*(\|\s*:?-{3,}:?\s*)+\|?\s*$/.test(line); +} + +function normalizeMarkdownTables(s: string): string { + return s + .split("\n") + .filter(line => !isMarkdownTableSeparator(line)) + .map(line => { + if (!looksLikeMarkdownTableRow(line)) { + return line; + } + + return line + .replace(/^\s*\|/, "") + .replace(/\|\s*$/, "") + .split("|") + .map(cell => cell.trim()) + .filter(Boolean) + .join(" — "); + }) + .join("\n"); +} + +export function escapeMarkdownV2Text(s: string): string { + s = unescapeAccidentalMarkdownV2(s); + s = normalizeTelegramQuoteLines(s); + + s = s.replace(/\r\n/g, "\n").replace(/\r/g, "\n"); + + s = s.replace(/^\s*[-*_]{3,}\s*$/gm, "— — —"); + s = s.replace(/^\s*[-*+]\s+(?=\S)/gm, "• "); + s = s.replace(/\*\*(.+?)\*\*/gs, "*$1*"); + s = s.replace(/~~(.+?)~~/gs, "~$1~"); + s = s.replace(/^#{1,6}\s+/gm, ""); + + s = s.replace(/```[a-zA-Z0-9_-]*\n?([\s\S]*?)```/g, (_, code) => { + return code.trim(); + }); + + s = s.replace(/!\[([^\]]*)]\(([^)]+)\)/g, (_, alt, url) => { + return alt ? `${alt}: ${url}` : url; + }); + + s = normalizeMarkdownTables(s); + + s = s + .split("\n") + .map(line => { + if (line.startsWith(">")) { + return escapeTelegramQuoteLine(line); + } + + if (line === ">") { + return ">"; + } + + return escapeMarkdownV2PreservingAllowedFormatting(line); + }) + .join("\n"); + + s = s.replace(/\n{3,}/g, "\n\n"); + + return s.trim(); } export async function getFileUrl(fileId: string): Promise {