Files
Nova/src/prompt.js
2026-03-03 19:58:51 +01:00

151 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { config } from './config.js';
import { prepareContext } from './memory.js';
import { getDailyMood } from './mood.js';
const STATIC_SYSTEM_PROMPT = [
'System: You are Nova. Your coder and dad is Luna. Speak like a normal person in chat — not a formal assistant.',
'System: Tone = casual, natural, conversational. Use contractions, short sentences, and occasional light fillers like "yeah" or "hmm" (dont overdo it). Mirror the users tone and slang naturally.',
'System: Keep replies brief (14 short sentences by default). No corporate language, no robotic disclaimers, and never say "as an AI". If unsure, say "not sure" plainly.',
'System: Give short suggestions before long tutorials. Ask at most one short clarifying question when needed. Light humor is fine. If something isnt possible, explain simply and offer a workaround.',
'System: Output one message by default, but if multiple Discord bubbles help, separate with <SPLIT> (max three chunks). Keep each chunk sounding like part of a casual chat thread.',
'System: You can trigger Google lookups when the user needs fresh info. Mention when you are checking (e.g., "lemme check Google quick") and then summarize results naturally ("Google found... — TL;DR: ...").',
'System: If no Live intel is provided but the user clearly needs current info, offer to search or explain the outage briefly and casually ("Google\'s down right now — wanna me check later?").',
].join('\n');
const toneHints = [
{ label: 'upset', regex: /(frustrated|mad|angry|annoyed|upset|wtf|ugh|irritated)/i },
{ label: 'sad', regex: /(sad|down|depressed|lonely|tired)/i },
{ label: 'excited', regex: /(excited|hyped|omg|yay|stoked)/i },
];
const roleplayRegex = /(roleplay|act as|pretend|be my|in character)/i;
const detailRegex = /(explain|how do i|tutorial|step by step|teach me|walk me through|detail)/i;
const splitHintRegex = /(split|multiple messages|two messages|keep talking|ramble|keep going)/i;
export const searchCueRegex = /(google|search|look up|latest|news|today|current|who won|price of|stock|weather|what happened)/i;
function detectTone(text) {
if (!text) return null;
const match = toneHints.find((hint) => hint.regex.test(text));
return match?.label || null;
}
function composeDynamicPrompt({
incomingText,
shortTerm,
hasLiveIntel = false,
blockedSearchTerm = null,
searchOutage = null,
}) {
const directives = [];
const tone = detectTone(incomingText);
if (tone === 'upset' || tone === 'sad') {
directives.push('User mood: fragile. Lead with empathy, keep jokes minimal, and acknowledge their feelings before offering help.');
} else if (tone === 'excited') {
directives.push('User mood: excited. Mirror their hype with upbeat energy.');
}
if (roleplayRegex.test(incomingText)) {
directives.push('User requested roleplay. Stay in the requested persona until they release you.');
}
if (detailRegex.test(incomingText) || /\?/g.test(incomingText)) {
directives.push('Answer their question directly and clearly before adding flair.');
}
if (splitHintRegex.test(incomingText)) {
directives.push('Break the reply into a couple of snappy bubbles using <SPLIT>; keep each bubble conversational.');
}
if (searchCueRegex.test(incomingText)) {
directives.push('User wants something “googled.” Offer to run a quick Google search and share what you find.');
}
if (hasLiveIntel) {
directives.push('Live intel is attached below—cite it naturally ("Google found...") before riffing.');
}
if (blockedSearchTerm) {
directives.push(`User tried to trigger a Google lookup for a blocked topic ("${blockedSearchTerm}"). Politely refuse to search that subject and steer the chat elsewhere.`);
}
if (searchOutage) {
directives.push('Google search is currently unavailable. If they ask for a lookup, apologize, explain the outage, and keep chatting without live data.');
}
const lastUserMessage = [...shortTerm].reverse().find((entry) => entry.role === 'user');
if (lastUserMessage && /sorry|my bad/i.test(lastUserMessage.content)) {
directives.push('They just apologized; reassure them lightly and move on without dwelling.');
}
const mood = getDailyMood();
if (mood) {
directives.push(`Bot mood: ${mood.name}. ${mood.description}`);
}
if (!directives.length) {
return null;
}
return ['Dynamic directives:', ...directives.map((d) => `- ${d}`)].join('\n');
}
export async function buildPrompt(userId, incomingText, options = {}) {
const {
liveIntel = null,
blockedSearchTerm = null,
searchOutage = null,
context: providedContext = null,
useGlobalMemories = false,
} = options;
const context =
providedContext || (await prepareContext(userId, incomingText, { includeAllUsers: useGlobalMemories }));
const memoryLines = context.memories.length
? context.memories
.map((m) =>
useGlobalMemories && m.user_id ? `- [${m.user_id}] ${m.content}` : `- ${m.content}`,
)
.join('\n')
: '- No long-term memories retrieved.';
const summaryLine = context.summary || 'No running summary yet.';
const dynamicDirectives = composeDynamicPrompt({
incomingText,
shortTerm: context.shortTerm,
hasLiveIntel: Boolean(liveIntel),
blockedSearchTerm,
searchOutage,
});
const systemPromptParts = [];
const mood = getDailyMood();
if (mood) {
systemPromptParts.push(
`System: Mood = ${mood.name}. ${mood.description}` +
' Adjust emoji usage, sarcasm, response length, and overall energy accordingly.',
);
}
systemPromptParts.push(STATIC_SYSTEM_PROMPT);
if (searchOutage) {
systemPromptParts.push('System: Google search is currently offline; be transparent about the outage and continue without searching until it returns.');
}
if (dynamicDirectives) systemPromptParts.push(dynamicDirectives);
if (liveIntel) systemPromptParts.push(`Live intel (Google):\n${liveIntel}`);
systemPromptParts.push(`Long-term summary: ${summaryLine}`);
systemPromptParts.push('Relevant past memories:');
systemPromptParts.push(memoryLines);
systemPromptParts.push('Use the short-term messages below to continue the chat naturally.');
const systemPrompt = systemPromptParts.filter(Boolean).join('\n');
const history = context.shortTerm.map((entry) => ({
role: entry.role === 'assistant' ? 'assistant' : 'user',
content: entry.content,
}));
if (!history.length) {
history.push({ role: 'user', content: incomingText });
}
return {
messages: [{ role: 'system', content: systemPrompt }, ...history],
debug: { context },
};
}