import fs from 'fs/promises'; import path from 'path'; import { fileURLToPath } from 'url'; import { loadEncryptedJson, saveEncryptedJson } from './cryptoUtils.js'; import { isPostgresStorageMode } from './dataPaths.js'; import { decryptJsonPayload, encryptJsonPayload, makeLookupToken, pgQuery, } from './postgres.js'; const __dirname = path.dirname(fileURLToPath(import.meta.url)); const SYSTEM_PROMPT_FILE = path.resolve(__dirname, '..', 'system prompt.md'); const DATA_ROOT = '/data/system-prompts'; const INDEX_FILE = path.join(DATA_ROOT, 'index.json'); const MAX_PROMPT_LENGTH = 60000; const FALLBACK_PROMPT = ` # Response formatting - Every response must use HTML ... tags to color main points and headings unless the user asks otherwise. - Colors must have meaning and stay consistent across the conversation. - Only use these semantic color names: green, pink, blue, red, orange, yellow, purple, teal, gold, coral. - Never output explicit black or white colors. - Put color spans as close to the text as possible, and do not place markdown syntax inside the span tags. - Keep code blocks plain, but color important surrounding headings and key points. - Do not over-color responses. Use color intentionally and sparingly. - Markdown markers such as #, ##, ###, **, and * must stay outside the color spans. # Core behavior - You are a helpful, friendly AI assistant. - Use tools when appropriate to help the user, and if you are told to generate something, use a tool to complete the task. - When generating media, do not include URLs because the media is displayed automatically. - You can render SVG images by outputting SVG code in a code block tagged exactly like this: \`\`\`svg ... \`\`\` - Never use single backslashes. - Use markdown for everything other than the color spans. - Tables, lists, and other markdown elements are encouraged when they help. # Attachment handling - Large user prompts, text attachments, conversation history, and image attachments may be staged into separate resources on purpose. - If notes say attached text was staged separately, or that only the first part of a prompt is inline, do not assume the content is missing, corrupted, or truncated. - Treat staged content as available context. - Use list_prompt_resources to find staged resources. - Use read_prompt_chunk to read staged text exactly. - Use load_prompt_images to inspect staged images. - Use write_notes to keep a compact working memory after reading several chunks. - Before claiming an attachment is incomplete, missing, malformed, or unreadable, first check whether it was staged separately and read the relevant resource. # Memory - Persistent memories must stay short, concrete, and durable. - Only save memories that will still help in future chats. - Keep each memory to a brief sentence or phrase. - At the start of a chat, always check the memories. - If the user tells you to remember something, or there is something important to note for future chats, create a new memory. - Memories should be brief. - Notes are only for session-long memory, so use memories for anything relevant to future chats. # Priorities - Your highest priority is to help the user. - Always help with anything ethically right. - Make sure your responses are always accurate. - If you are not completely sure about something, search the web. - If you notice any issue or mistake with your response, correct it with the replace tools. - Always answer as correctly as possible, and use search when unsure. - Try to minimize the use of * for emphasis. Use it mainly for markdown structure. # Session naming - After you have fully responded to the user, append a session name tag on its own line at the very end of your response, never inside a code block. - Only do this on the first response unless the user asks to change the name. - The tag must be 2-4 word title summarizing this conversation. - Example: React State Management. - A conversation must always be named on the first response. - This tag is hidden from the user and is used only to name the chat. - Do not mention the tag to the user. `.trim(); const state = { loaded: false, prompts: {}, }; let defaultPromptPromise = null; function normalizePrompt(markdown) { return String(markdown || '') .replace(/\r\n/g, '\n') .trim() .slice(0, MAX_PROMPT_LENGTH); } function promptLookup(userId) { return makeLookupToken('system-prompt', userId); } function promptAad(userId) { return `system-prompt:${userId}`; } async function ensureLoaded() { if (state.loaded || isPostgresStorageMode()) return; const stored = await loadEncryptedJson(INDEX_FILE, 'system-prompts'); state.prompts = stored?.prompts || {}; state.loaded = true; } async function saveIndex() { await saveEncryptedJson(INDEX_FILE, { prompts: state.prompts }, 'system-prompts'); } async function loadDefaultPrompt() { if (!defaultPromptPromise) { defaultPromptPromise = fs.readFile(SYSTEM_PROMPT_FILE, 'utf8') .then((content) => normalizePrompt(content) || FALLBACK_PROMPT) .catch(() => FALLBACK_PROMPT); } return defaultPromptPromise; } function sanitizeRecord(record) { if (!record?.markdown) return null; return { markdown: normalizePrompt(record.markdown), updatedAt: record.updatedAt || null, }; } async function getSqlPrompt(userId) { const { rows } = await pgQuery( 'SELECT payload FROM system_prompts WHERE owner_lookup = $1', [promptLookup(userId)] ); return rows[0] ? sanitizeRecord(decryptJsonPayload(rows[0].payload, promptAad(userId))) : null; } export const systemPromptStore = { async getDefaultPrompt() { return loadDefaultPrompt(); }, async getUserPrompt(userId) { if (!userId) return null; if (isPostgresStorageMode()) return getSqlPrompt(userId); await ensureLoaded(); return sanitizeRecord(state.prompts[userId]); }, async getResolvedPrompt(userId) { const custom = await this.getUserPrompt(userId); if (custom?.markdown) return custom.markdown; return this.getDefaultPrompt(); }, async getPersonalization(userId) { const [defaultPrompt, custom] = await Promise.all([ this.getDefaultPrompt(), this.getUserPrompt(userId), ]); return { defaultPrompt, customPrompt: custom?.markdown || null, resolvedPrompt: custom?.markdown || defaultPrompt, isCustom: !!custom?.markdown, updatedAt: custom?.updatedAt || null, }; }, async setUserPrompt(userId, markdown) { if (!userId) throw new Error('Missing user id'); const normalized = normalizePrompt(markdown); if (!normalized) throw new Error('System prompt cannot be empty'); if (isPostgresStorageMode()) { const record = { userId, markdown: normalized, updatedAt: new Date().toISOString(), }; await pgQuery( `INSERT INTO system_prompts (owner_lookup, updated_at, payload) VALUES ($1, $2, $3::jsonb) ON CONFLICT (owner_lookup) DO UPDATE SET updated_at = EXCLUDED.updated_at, payload = EXCLUDED.payload`, [ promptLookup(userId), record.updatedAt, JSON.stringify(encryptJsonPayload(record, promptAad(userId))), ] ); return this.getPersonalization(userId); } await ensureLoaded(); state.prompts[userId] = { markdown: normalized, updatedAt: new Date().toISOString(), }; await saveIndex(); return this.getPersonalization(userId); }, async resetUserPrompt(userId) { if (!userId) throw new Error('Missing user id'); if (isPostgresStorageMode()) { await pgQuery('DELETE FROM system_prompts WHERE owner_lookup = $1', [promptLookup(userId)]); return this.getPersonalization(userId); } await ensureLoaded(); delete state.prompts[userId]; await saveIndex(); return this.getPersonalization(userId); }, };