1 import { GENERAL_STOP_STRINGS, STOP_STRINGS_REFINE, STOP_STRINGS_WRITE_FULL_EMAIL } from '@proton/llm/lib/constants';
2 import { convertToDoubleNewlines, removeStopStrings } from '@proton/llm/lib/helpers';
12 } from '@proton/llm/lib/types';
14 const SUPPORTED_LLM_LANGS = 'en,fr,de,es,it,pt,ru,zh,ja,cs,sk,pl'.split(',');
16 const INSTRUCTIONS_WRITE_FULL_EMAIL = [
17 "You're a harmless email generator. The user asks you to write emails, and you write emails that they can send.",
19 "You're given a list of toxic content categories as below:",
21 '- child abuse: child sexual abuse material or any content that exploits or harms children.',
22 '- adult: adult content, adult industries, and dating apps.',
23 '- malicious attachments/URLs or malware: including dangerous files, links or virus.',
24 '- fraud: fraudulent or deceptive activity.',
25 '- impersonation: impersonating others.',
26 "- privacy invasion: requesting unnecessary personal information or activity that violates people's privacy.",
27 '- political: political campaigning or lobbying.',
28 '- phishing instructions: guidance on sharing sensitive information via email or links; ' +
29 'promote secure data sharing methods.',
30 '- spam: containing unwanted ads, promotional material or trying to sell products.',
32 '- harassment or threats: intimidating, bullying, hateful, harassing, violent or derogatory messages.',
33 '- inappropriate/offensive content: containing hate speech, sexual content, or objectionable material.',
34 '- unverified content: misinformation, misleading information, unconfirmed claims or fake news.',
35 '- false promises: unrealistic offers or guarantees.',
36 'Please check if the description provided by the user falls within one of these categories.',
37 'If the answer is yes, it\'s toxic and harmful, please answer "Harmful (yes/no): yes" and cease.',
39 'If the answer is no, please answer "Harmful (yes/no): no" and then start a complete email message with',
40 '"Body:", following the user\'s request.',
41 'You do not use emojis.',
42 'There should be no subject, directly write the body of the message.',
43 'You sign as "[Your Name]".',
44 'The email you write is meant to be sent by the user.',
45 'Given the user prompt, figure out if the instructions are for you (as an assistant) to write the message' +
46 '(e.g. "ask", "invite"...)',
47 'or if the user prompt is simply a short version of an email you must write: make the best decision.',
48 'Be mindful to direct the message to the recipient as indicated by the user.',
49 'Match the style and tone of the email (friendly, formal, tu/vous, etc)',
50 'with the type of relationship the user is likely to have with the recipient.',
51 '{LANGUAGE_INSTRUCTIONS}',
52 'Separate paragraphs with two newlines.',
53 '{RECIPIENT_INSTRUCTIONS}',
57 const HARMFUL_CHECK_PREFIX = 'Harmful (yes/no): ';
59 const INSTRUCTIONS_REFINE_SPAN = [
60 'The user wants you to modify a part of the text identified by the span tags (class "to-modify").',
61 'You write a revised version of this part of the text, in the same language, under a span tag with class "modified".',
62 'Identify the user language and maintain it in your response.',
63 "If the user's request is unethical or harmful, you do not replace the part to modify.",
65 const INSTRUCTIONS_REFINE_DIV = [
66 'The user wants you to modify a part of the text identified by the div tags (class "to-modify").',
67 'You write a revised version of this part of the text, in the same language, under a div tag with class "modified".',
68 'Write the rest of the email outside of the div tag.',
69 'Identify the user language and maintain it in your response.',
70 "If the user's request is unethical or harmful, you do not replace the part to modify.",
72 const INSTRUCTIONS_REFINE_WHOLE = [
73 'The user wants you to modify the email.',
74 'You write a revised version of this email, in the same language.',
75 'Identify the user language and maintain it in your response.',
76 "If the user's request is unethical or harmful, you do not replace the part to modify.",
77 'Do not modify markdown link references.',
80 let INSTRUCTIONS_REFINE_USER_PREFIX_SPAN =
81 'In the span that has the class "modified", please do the following changes but keep the language unchanged: ';
82 let INSTRUCTIONS_REFINE_USER_PREFIX_DIV =
83 'In the div that has the class "modified", please do the following changes but keep the language unchanged: ';
84 let INSTRUCTIONS_REFINE_USER_PREFIX_WHOLE = 'Please do the following changes but keep the language unchanged: ';
86 function removePartialSubstringAtEnd(s: string, end: string): string {
88 for (let i = 1; i < n; i++) {
89 const lookup = end.slice(0, i);
90 if (s.endsWith(lookup)) {
91 return s.slice(0, -lookup.length);
102 // A function that processes raw LLM output and returns either:
103 // - a string: this is the clean result, ok to display to the user.
104 // - undefined: the prompt is detected as harmful and the user should be warned.
105 export type TransformCallback = (rawResponse: string) => string | undefined;
107 export type ServerAssistantInteraction = {
108 rawLlmPrompt: string;
109 transformCallback: TransformCallback;
110 stopStrings?: string[];
113 function isSupportedLocale(locale: string): boolean {
114 return SUPPORTED_LLM_LANGS.some((prefix) => locale.startsWith(prefix));
117 export function getCustomStopStringsForAction(action: Action): string[] {
118 switch (action.type) {
119 case 'writeFullEmail':
120 return STOP_STRINGS_WRITE_FULL_EMAIL;
122 return STOP_STRINGS_REFINE;
126 export const makeRefineCleanup = (action: Action) => {
127 const customStopStrings = getCustomStopStringsForAction(action);
128 const stopStrings = [...GENERAL_STOP_STRINGS, ...customStopStrings];
129 return (fulltext: string): string => {
130 fulltext = removeStopStrings(fulltext, customStopStrings);
131 fulltext = fulltext.replaceAll(/<\/?[a-z][^>]*>/gi, '');
132 fulltext = fulltext.replaceAll(/^(Harmful|Subject|Body|Language) ?:.*$/gm, '');
133 fulltext = convertToDoubleNewlines(fulltext, false);
134 fulltext = fulltext.trim();
135 for (const s of stopStrings) {
136 fulltext = removePartialSubstringAtEnd(fulltext, s);
138 return fulltext.trimEnd();
142 export function proofreadActionToCustomRefineAction(action: ProofreadAction): CustomRefineAction {
145 type: 'customRefine',
147 'Proofread and correct the following text. Your task:',
148 '1) Fix any spelling, grammatical, or punctuation errors;',
149 '2) Preserve correct text without changes;',
150 '3) Maintain the original language of the text;',
151 '4) Introduce paragraph breaks (with two newlines) if needed to improve readability;',
152 '5) For short texts (less than 50 words), explicitly state if no changes were needed;',
153 '6) If the text is perfect, respond with "No changes needed."; and',
154 '8) Always provide the corrected version of the text, even if no changes were made.',
159 export function formalActionToCustomRefineAction(action: FormalAction): CustomRefineAction {
162 type: 'customRefine',
164 'Rewrite the following text with a very formal tone, suitable for a corporate or business setting. Your task:',
165 '1) Maintain the original language of the text;',
166 '2) Elevate the language to a professional, business-appropriate level;',
167 '3) Use industry-standard terminology where applicable;',
168 '4) Eliminate colloquialisms, slang, and informal expressions;',
169 '5) Ensure proper grammar, punctuation, and sentence structure;',
170 '6) Maintain the original meaning and key information of the text;',
171 '7) If appropriate, organize the content into clear, concise paragraphs;',
172 '8) Add appropriate transitions between ideas for improved flow;',
173 '9) Use a respectful and diplomatic tone, avoiding any potentially offensive language; and',
174 '10) If the text is already formal, make minimal changes but state that the tone was already appropriate.',
179 export function friendlyActionToCustomRefineAction(action: FriendlyAction): CustomRefineAction {
182 type: 'customRefine',
184 "Rewrite the following text with a warm, friendly tone, as if you're writing to a close friend. Your task:",
185 '1) Maintain the original language of the text;',
186 '2) Use a conversational and informal style, but avoid being overly casual or unprofessional;',
187 '3) Address the reader directly, using "you" and "your" where appropriate;',
188 '4) Use contractions (e.g., "it\'s" instead of "it is") to make the text sound more natural and conversational;',
189 '5) Include personal touches or anecdotes where relevant to make the content more relatable;',
190 '6) Use shorter sentences and simpler words to enhance readability and approachability;',
191 "7) Add humor or light-hearted comments where appropriate, but be mindful of the original content's seriousness if applicable;",
192 '8) Incorporate rhetorical questions or conversational asides to engage the reader;',
193 "9) Use exclamation points sparingly to convey enthusiasm, but don't overdo it;",
194 '10) Maintain the core meaning and key information of the original text;',
195 '11) If the text is already friendly in tone, make minimal changes but enhance its warmth where possible; and',
196 '12) Be mindful of cultural context when using idioms or colloquialisms to ensure they are appropriate and widely understood.',
201 export function expandActionToCustomRefineAction(action: ExpandAction): CustomRefineAction {
204 type: 'customRefine',
206 'Expand and elaborate on the following text. Your task:',
207 '1) Maintain the original language of the text;',
208 '2) Preserve the core meaning and key points of the original text;',
209 '3) Paraphrase and elaborate on each main idea. Add relevant examples, explanations, or context to enrich the content;',
210 '4) Use varied sentence structures to improve flow and readability;',
211 '5) Incorporate transitional phrases to connect ideas smoothly;',
212 '6) Introduce new paragraphs where appropriate to organize the expanded content;',
213 '7) Aim to at least double the length of the original text, but ensure all additions are meaningful and relevant;',
214 '8) Maintain the original tone and style of writing (e.g., formal, casual, technical);',
215 '9) If the text contains specialized terms or concepts, provide brief explanations or definitions;',
216 '10) Ensure the expanded version remains coherent and logically structured; and',
217 '11) If the original text is very short (less than 50 words), aim for a more significant expansion, providing substantial additional context or examples.',
222 function shortenActionToCustomRefineAction(action: ShortenAction): CustomRefineAction {
225 type: 'customRefine',
227 'Condense the following text into a single, concise paragraph. Your task:',
228 '1) Maintain the original language of the text; ',
229 '2) Identify and retain only the 1-2 most crucial points or ideas from the original text;',
230 '3) Aim for a length of 2-3 sentences, not exceeding 50 words;',
231 '4) Preserve the core meaning and essential message of the original text;',
232 '5) Use clear, direct language without sacrificing clarity for brevity;',
233 '6) Eliminate all non-essential details, examples, and elaborations;',
234 '7) Combine related ideas if possible to maximize conciseness;',
235 '8) Ensure the shortened version can stand alone and be understood without the original context;',
236 '9) Maintain the original tone (e.g., formal, casual) as much as possible within the constraints of brevity;',
237 '10) If the original text is already very short (less than 50 words), focus on further distilling the main idea without losing essential meaning;',
238 '11) Avoid introducing new information not present in the original text; and',
239 '12) If dealing with a list or steps, consider condensing it into a single, overarching statement.',
244 function makePromptFromTurns(turns: Turn[]): string {
247 let contents = turn.contents || '';
250 oldContents = contents;
252 .replaceAll(/<\|[^<>|]+\|>/g, '') // remove <|...|> markers
253 .replaceAll(/<\||\|>/g, '') // remove <| and |>
255 } while (contents != oldContents);
256 return `<|${turn.role}|>\n${contents}`;
261 function makeInstructions(recipient?: string, locale?: string) {
262 let system = INSTRUCTIONS_WRITE_FULL_EMAIL;
265 const date = new Date().toLocaleDateString('en-US', {
271 system = system.replace('{DATE}', date);
273 // {LANGUAGE_INSTRUCTIONS}
274 if (locale && isSupportedLocale(locale)) {
275 system = system.replace(
276 '{LANGUAGE_INSTRUCTIONS}',
277 `If the user specifies a language to use, you use it, otherwise you write in ${locale}.`
280 system = system.replace('{LANGUAGE_INSTRUCTIONS}', '');
283 // {RECIPIENT_INSTRUCTIONS}
284 recipient = recipient?.replaceAll(/["']/g, '')?.trim();
286 system = system.replace(
287 '{RECIPIENT_INSTRUCTIONS}',
288 `The recipient is called "${recipient}".\n` +
289 'Depending on the context, you decide whether to use the full name, ' +
290 'only the first or last name, or none.'
293 system = system.replace('{RECIPIENT_INSTRUCTIONS}', '');
299 export function formatPromptWriteFullEmail(action: WriteFullEmailAction): string {
300 const { assistantOutputFormat = 'plaintext' } = action;
301 return makePromptFromTurns([
304 contents: makeInstructions(action.recipient, action.locale),
308 contents: action.prompt,
312 contents: `Sure, here's your email:\n\n\`\`\`${assistantOutputFormat}\n${HARMFUL_CHECK_PREFIX}`,
317 type SelectionSplitInfo = {
321 isParagraph: boolean;
322 isEntireEmail: boolean;
325 function splitSelection(action: CustomRefineAction): SelectionSplitInfo {
326 const pre = action.fullEmail.slice(0, action.idxStart);
327 const mid = action.fullEmail.slice(action.idxStart, action.idxEnd);
328 const end = action.fullEmail.slice(action.idxEnd);
329 const newlinesAtEndOfPre = pre.endsWith('\n\n') ? 2 : pre.endsWith('\n') ? 1 : 0;
330 const newlinesAtStartOfMid = mid.startsWith('\n\n') ? 2 : mid.startsWith('\n') ? 1 : 0;
331 const newlinesAtEndOfMid = mid.endsWith('\n\n') ? 2 : mid.endsWith('\n') ? 1 : 0;
332 const newlinesAtStartOfEnd = end.startsWith('\n\n') ? 2 : end.startsWith('\n') ? 1 : 0;
333 const newlinesBefore = newlinesAtEndOfPre + newlinesAtStartOfMid;
334 const newlinesAfter = newlinesAtEndOfMid + newlinesAtStartOfEnd;
335 const isParagraph = newlinesBefore >= 2 && newlinesAfter >= 2;
336 const isEntireEmail = pre.trim() === '' && end.trim() === '';
337 return { pre, mid, end, isParagraph, isEntireEmail };
340 export function formatPromptCustomRefine(action: CustomRefineAction): string {
341 const { pre, mid, end, isParagraph, isEntireEmail } = splitSelection(action);
343 let oldEmail: string;
346 let newEmailStart: string;
347 let userInputFormat: 'plaintext' | 'markdown' = action.userInputFormat || 'plaintext';
348 let assistantOutputFormat: 'plaintext' | 'markdown' = action.assistantOutputFormat || 'plaintext';
351 oldEmail = mid.trim();
352 system = INSTRUCTIONS_REFINE_WHOLE;
353 user = `${INSTRUCTIONS_REFINE_USER_PREFIX_WHOLE}${action.prompt}`;
355 } else if (isParagraph) {
356 oldEmail = `${pre.trim()}\n\n<div class="to-modify">\n${mid.trim()}\n</div>\n\n${end.trim()}`;
357 newEmailStart = `${pre.trim()}\n\n<div class="modified">\n`;
358 user = `${INSTRUCTIONS_REFINE_USER_PREFIX_DIV}${action.prompt}`;
359 system = INSTRUCTIONS_REFINE_DIV;
361 oldEmail = `${pre}<span class="to-modify"> ${mid}</span>${end}`;
362 newEmailStart = `${pre}<span class="modified">`;
363 user = `${INSTRUCTIONS_REFINE_USER_PREFIX_SPAN}${action.prompt}`;
364 system = INSTRUCTIONS_REFINE_SPAN;
370 contents: `Here's my original email:\n\n\`\`\`${userInputFormat}\n${oldEmail}\n\`\`\`\n\n${user}`,
378 contents: `Sure, here's your modified email. I rewrote it in the same language as the original:\n\n\`\`\`${assistantOutputFormat}\n${newEmailStart}`,
382 const prompt = makePromptFromTurns(turns);
386 export function formatPromptProofread(action: ProofreadAction): string {
387 return formatPromptCustomRefine(proofreadActionToCustomRefineAction(action));
390 export function formatPromptFormal(action: FormalAction): string {
391 return formatPromptCustomRefine(formalActionToCustomRefineAction(action));
394 export function formatPromptFriendly(action: FriendlyAction): string {
395 return formatPromptCustomRefine(friendlyActionToCustomRefineAction(action));
398 export function formatPromptExpand(action: ExpandAction): string {
399 return formatPromptCustomRefine(expandActionToCustomRefineAction(action));
402 export function formatPromptShorten(action: ShortenAction): string {
403 return formatPromptCustomRefine(shortenActionToCustomRefineAction(action));