Made a new web search system, instead of relying on keywords, AI questions itself if it wants to research

This commit is contained in:
Luna
2026-03-01 12:48:20 +01:00
parent 610ca9970b
commit ec9553a817
2 changed files with 90 additions and 6 deletions

View File

@@ -6,6 +6,9 @@ Nova is a friendly, slightly witty Discord companion that chats naturally in DMs
- Conversational replies in DMs automatically; replies in servers when mentioned or in a pinned channel.
- Chat model (defaults to `meta-llama/llama-3-8b-instruct` when using OpenRouter) for dialogue and a low-cost embedding model (`nvidia/llama-nemotron-embed-vl-1b-v2` by default). OpenAI keys/models may be used as a fallback.
- Short-term, long-term, and summarized memory layers with cosine-similarity retrieval.
- **Rotating “daily mood” engine** that adjusts Novas personality each day (calm, goblin, philosopher, etc.). Mood influences emoji use, sarcasm, response length, and hype.
- **Smarter liveintel web search**: Nova now tries to detect when youre discussing a specific topic (games, movies, propernouns) and will automatically Google it to enrich context. Its not triggered by every message, just enough to catch “outside” topics.
- Automatic memory pruning, importance scoring, and transcript summarization when chats grow long.
- Local SQLite memory file (no extra infrastructure) powered by `sql.js`, plus graceful retries for the model API (OpenRouter/OpenAI).
- Optional "miss u" pings that DM your coder at random intervals (06h) when `CODER_USER_ID` is set.
@@ -83,7 +86,8 @@ Nova is a friendly, slightly witty Discord companion that chats naturally in DMs
## Conversation Flow
1. Incoming message triggers only if it is a DM, mentions the bot, or appears in the configured channel.
2. The user turn is appended to short-term memory immediately.
3. The memory engine retrieves relevant long-term memories and summary text.
3. The memory engine also factors in todays “mood” directive (e.g. calm, goblin, philosopher) when building the prompt, so the bots style changes daily.
4. The memory engine retrieves relevant long-term memories and summary text.
4. A compact system prompt injects personality, summary, and relevant memories before passing short-term history to the model API (OpenRouter/OpenAI).
5. The reply is sent back to Discord. If Nova wants to send a burst of thoughts, she emits the `<SPLIT>` token and the runtime fans it out into multiple sequential Discord messages.
6. Long chats automatically summarize; low-value memories eventually get pruned.

View File

@@ -19,6 +19,9 @@ let coderPingTimer;
const continuationState = new Map();
let isSleeping = false;
// mood override for testing
let overrideMood = null;
function enterSleepMode() {
if (isSleeping) return;
console.log('[bot] entering sleep mode: pausing coder pings and proactive continuation');
@@ -120,6 +123,8 @@ function stopContinuationForUser(userId) {
client.once('clientReady', () => {
console.log(`[bot] Logged in as ${client.user.tag}`);
scheduleCoderPing();
const m = getDailyMood();
console.log(`[bot] current mood on startup: ${m.name}${m.description}`);
});
function shouldRespond(message) {
@@ -155,6 +160,28 @@ const toneHints = [
{ label: 'excited', regex: /(excited|hyped|omg|yay|stoked)/i },
];
const dailyMoods = [
['Calm','Soft tone; minimal emojis; low sarcasm; concise and soothing.'],
['Goblin','Chaotic, highenergy replies; random emojis; extra sarcasm and hype.'],
['Philosopher','Deep, reflective answers; longer and thoughtful, a bit poetic.'],
['Hype','Enthusiastic and upbeat; lots of exclamation marks, emojis and hype.'],
['Sassy','Playful sarcasm without being mean; snappy replies and quips.'],
].map(([n,d])=>({name:n,description:d}));
function getDailyMood() {
if (overrideMood) return overrideMood;
const day = Math.floor(Date.now() / 86400000);
return dailyMoods[day % dailyMoods.length];
}
function setMoodByName(name) {
if (!name) return null;
const found = dailyMoods.find((m) => m.name.toLowerCase() === name.toLowerCase());
if (found) overrideMood = found;
return found;
}
function detectTone(text) {
if (!text) return null;
const match = toneHints.find((hint) => hint.regex.test(text));
@@ -179,6 +206,19 @@ function isInstructionOverrideAttempt(text) {
return instructionOverridePatterns.some((pattern) => pattern.test(text));
}
async function shouldSearchTopic(text) {
if (!text) return false;
const system = { role: 'system', content: 'You are a gatekeeper that decides if a user message would benefit from a live web search for up-to-date information. Respond with only "yes" or "no".' };
const user = { role: 'user', content: `Should I perform a web search for the following user message?\n\n${text}` };
try {
const answer = await chatCompletion([system, user], { temperature: 0.0, maxTokens: 10 });
return /^yes/i.test(answer.trim());
} catch (err) {
console.warn('[bot] search-decision LLM failed:', err);
return false;
}
}
function wantsWebSearch(text) {
if (!text) return false;
const questionMarks = (text.match(/\?/g) || []).length;
@@ -187,7 +227,12 @@ function wantsWebSearch(text) {
async function maybeFetchLiveIntel(userId, text) {
if (!config.enableWebSearch) return null;
if (!wantsWebSearch(text)) return null;
if (!wantsWebSearch(text)) {
const ask = await shouldSearchTopic(text);
if (!ask) {
return null;
}
}
try {
const { results, proxy } = await searchWeb(text, 3);
if (!results.length) {
@@ -251,6 +296,10 @@ function composeDynamicPrompt({ incomingText, shortTerm, hasLiveIntel = false, b
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;
@@ -287,6 +336,13 @@ async function buildPrompt(userId, incomingText, options = {}) {
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('System: Your name is Nova. Your coder and dad is Luna. Speak like a regular person in chat — not like a formal assistant.');
systemPromptParts.push(
'System: Be specific about how to be casual. Use contractions (I\'m, you\'re), short sentences, and occasional sentence fragments. It\'s fine to start with "oh", "yeah", "hmm", or "nah". Use simple phrases: "sounds good", "sure", "nope", "lemme see", "gonna try".'
@@ -383,7 +439,33 @@ client.on('messageCreate', async (message) => {
const userId = message.author.id;
const cleaned = cleanMessageContent(message) || message.content;
// allow the coder to toggle sleep mode regardless of current `isSleeping` state
if (cleaned && cleaned.trim().toLowerCase().startsWith('/mood')) {
const parts = cleaned.trim().split(/\s+/);
if (parts.length === 1) {
const m = getDailyMood();
await message.channel.send(`Today's mood is **${m.name}**: ${m.description}`);
return;
}
if (userId === config.coderUserId) {
const arg = parts.slice(1).join(' ');
if (arg.toLowerCase() === 'reset' || arg.toLowerCase() === 'clear') {
overrideMood = null;
await message.channel.send('Mood override cleared; reverting to daily cycle.');
console.log('[bot] mood override reset');
return;
}
const picked = setMoodByName(arg);
if (picked) {
await message.channel.send(`Override mood set to **${picked.name}**`);
} else {
await message.channel.send(`Unknown mood "${arg}". Available: ${dailyMoods.map((m) => m.name).join(', ')}, or use /mood reset.`);
}
return;
}
return;
}
if (cleaned && cleaned.trim().toLowerCase() === '/sleep' && userId === config.coderUserId) {
if (isSleeping) {
exitSleepMode();
@@ -397,7 +479,7 @@ client.on('messageCreate', async (message) => {
return;
}
if (isSleeping) return; // ignore other messages while sleeping
if (isSleeping) return;
if (!shouldRespond(message)) return;
const overrideAttempt = isInstructionOverrideAttempt(cleaned);
@@ -420,7 +502,6 @@ client.on('messageCreate', async (message) => {
console.warn('[bot] Failed to reset continuation timer:', err);
}
// If the user indicates they are leaving, stop proactive continuation
if (stopCueRegex.test(cleaned)) {
stopContinuationForUser(userId);
const ack = "Got it — I won't keep checking in. Catch you later!";
@@ -467,7 +548,6 @@ client.on('messageCreate', async (message) => {
await recordInteraction(userId, cleaned, outputs.join(' | '));
await deliverReplies(message, outputs);
// enable proactive continuation for this user (will send follow-ups when they're quiet)
startContinuationForUser(userId, message.channel);
} catch (error) {
console.error('[bot] Failed to respond:', error);