Spaces:
Running
Running
| 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); | |
| }, | |
| }; | |