| import fs from 'fs/promises'; |
| import path from 'path'; |
| import { fileURLToPath } from 'url'; |
| import { loadEncryptedJson, saveEncryptedJson } from './cryptoUtils.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 <span data-color="{COLOR NAME}">...</span> 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 |
| <svg>...</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 <session_name>2-4 word title summarizing this conversation</session_name>. |
| - Example: <session_name>React State Management</session_name>. |
| - 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); |
| } |
|
|
| async function ensureLoaded() { |
| if (state.loaded) 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, |
| }; |
| } |
|
|
| export const systemPromptStore = { |
| async getDefaultPrompt() { |
| return loadDefaultPrompt(); |
| }, |
|
|
| async getUserPrompt(userId) { |
| if (!userId) return null; |
| 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'); |
| 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'); |
| await ensureLoaded(); |
| delete state.prompts[userId]; |
| await saveIndex(); |
| return this.getPersonalization(userId); |
| }, |
| }; |
|
|