File size: 8,038 Bytes
1a7c116
 
 
 
bff1056
 
 
 
 
 
 
1a7c116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bff1056
 
 
 
 
 
 
 
1a7c116
bff1056
1a7c116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bff1056
 
 
 
 
 
 
 
1a7c116
 
 
 
 
 
 
bff1056
 
1a7c116
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
bff1056
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1a7c116
 
 
 
 
 
 
 
 
 
 
bff1056
 
 
 
 
 
1a7c116
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
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 <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);
}

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