Made a new web search system, instead of relying on keywords, AI questions itself if it wants to research
This commit is contained in:
@@ -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.
|
- 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.
|
- 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.
|
- Short-term, long-term, and summarized memory layers with cosine-similarity retrieval.
|
||||||
|
- **Rotating “daily mood” engine** that adjusts Nova’s personality each day (calm, goblin, philosopher, etc.). Mood influences emoji use, sarcasm, response length, and hype.
|
||||||
|
- **Smarter live‑intel web search**: Nova now tries to detect when you’re discussing a specific topic (games, movies, proper‑nouns) and will automatically Google it to enrich context. It’s not triggered by every message, just enough to catch “outside” topics.
|
||||||
|
|
||||||
- Automatic memory pruning, importance scoring, and transcript summarization when chats grow long.
|
- 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).
|
- 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 (0–6h) when `CODER_USER_ID` is set.
|
- Optional "miss u" pings that DM your coder at random intervals (0–6h) 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
|
## Conversation Flow
|
||||||
1. Incoming message triggers only if it is a DM, mentions the bot, or appears in the configured channel.
|
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.
|
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 today’s “mood” directive (e.g. calm, goblin, philosopher) when building the prompt, so the bot’s 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).
|
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.
|
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.
|
6. Long chats automatically summarize; low-value memories eventually get pruned.
|
||||||
|
|||||||
90
src/bot.js
90
src/bot.js
@@ -19,6 +19,9 @@ let coderPingTimer;
|
|||||||
const continuationState = new Map();
|
const continuationState = new Map();
|
||||||
let isSleeping = false;
|
let isSleeping = false;
|
||||||
|
|
||||||
|
// mood override for testing
|
||||||
|
let overrideMood = null;
|
||||||
|
|
||||||
function enterSleepMode() {
|
function enterSleepMode() {
|
||||||
if (isSleeping) return;
|
if (isSleeping) return;
|
||||||
console.log('[bot] entering sleep mode: pausing coder pings and proactive continuation');
|
console.log('[bot] entering sleep mode: pausing coder pings and proactive continuation');
|
||||||
@@ -120,6 +123,8 @@ function stopContinuationForUser(userId) {
|
|||||||
client.once('clientReady', () => {
|
client.once('clientReady', () => {
|
||||||
console.log(`[bot] Logged in as ${client.user.tag}`);
|
console.log(`[bot] Logged in as ${client.user.tag}`);
|
||||||
scheduleCoderPing();
|
scheduleCoderPing();
|
||||||
|
const m = getDailyMood();
|
||||||
|
console.log(`[bot] current mood on startup: ${m.name} — ${m.description}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
function shouldRespond(message) {
|
function shouldRespond(message) {
|
||||||
@@ -155,6 +160,28 @@ const toneHints = [
|
|||||||
{ label: 'excited', regex: /(excited|hyped|omg|yay|stoked)/i },
|
{ label: 'excited', regex: /(excited|hyped|omg|yay|stoked)/i },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const dailyMoods = [
|
||||||
|
['Calm','Soft tone; minimal emojis; low sarcasm; concise and soothing.'],
|
||||||
|
['Goblin','Chaotic, high‑energy 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) {
|
function detectTone(text) {
|
||||||
if (!text) return null;
|
if (!text) return null;
|
||||||
const match = toneHints.find((hint) => hint.regex.test(text));
|
const match = toneHints.find((hint) => hint.regex.test(text));
|
||||||
@@ -179,6 +206,19 @@ function isInstructionOverrideAttempt(text) {
|
|||||||
return instructionOverridePatterns.some((pattern) => pattern.test(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) {
|
function wantsWebSearch(text) {
|
||||||
if (!text) return false;
|
if (!text) return false;
|
||||||
const questionMarks = (text.match(/\?/g) || []).length;
|
const questionMarks = (text.match(/\?/g) || []).length;
|
||||||
@@ -187,7 +227,12 @@ function wantsWebSearch(text) {
|
|||||||
|
|
||||||
async function maybeFetchLiveIntel(userId, text) {
|
async function maybeFetchLiveIntel(userId, text) {
|
||||||
if (!config.enableWebSearch) return null;
|
if (!config.enableWebSearch) return null;
|
||||||
if (!wantsWebSearch(text)) return null;
|
if (!wantsWebSearch(text)) {
|
||||||
|
const ask = await shouldSearchTopic(text);
|
||||||
|
if (!ask) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const { results, proxy } = await searchWeb(text, 3);
|
const { results, proxy } = await searchWeb(text, 3);
|
||||||
if (!results.length) {
|
if (!results.length) {
|
||||||
@@ -251,6 +296,10 @@ function composeDynamicPrompt({ incomingText, shortTerm, hasLiveIntel = false, b
|
|||||||
if (lastUserMessage && /sorry|my bad/i.test(lastUserMessage.content)) {
|
if (lastUserMessage && /sorry|my bad/i.test(lastUserMessage.content)) {
|
||||||
directives.push('They just apologized; reassure them lightly and move on without dwelling.');
|
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) {
|
if (!directives.length) {
|
||||||
return null;
|
return null;
|
||||||
@@ -287,6 +336,13 @@ async function buildPrompt(userId, incomingText, options = {}) {
|
|||||||
searchOutage,
|
searchOutage,
|
||||||
});
|
});
|
||||||
const systemPromptParts = [];
|
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: Your name is Nova. Your coder and dad is Luna. Speak like a regular person in chat — not like a formal assistant.');
|
||||||
systemPromptParts.push(
|
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".'
|
'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 userId = message.author.id;
|
||||||
const cleaned = cleanMessageContent(message) || message.content;
|
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 (cleaned && cleaned.trim().toLowerCase() === '/sleep' && userId === config.coderUserId) {
|
||||||
if (isSleeping) {
|
if (isSleeping) {
|
||||||
exitSleepMode();
|
exitSleepMode();
|
||||||
@@ -397,7 +479,7 @@ client.on('messageCreate', async (message) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isSleeping) return; // ignore other messages while sleeping
|
if (isSleeping) return;
|
||||||
|
|
||||||
if (!shouldRespond(message)) return;
|
if (!shouldRespond(message)) return;
|
||||||
const overrideAttempt = isInstructionOverrideAttempt(cleaned);
|
const overrideAttempt = isInstructionOverrideAttempt(cleaned);
|
||||||
@@ -420,7 +502,6 @@ client.on('messageCreate', async (message) => {
|
|||||||
console.warn('[bot] Failed to reset continuation timer:', err);
|
console.warn('[bot] Failed to reset continuation timer:', err);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the user indicates they are leaving, stop proactive continuation
|
|
||||||
if (stopCueRegex.test(cleaned)) {
|
if (stopCueRegex.test(cleaned)) {
|
||||||
stopContinuationForUser(userId);
|
stopContinuationForUser(userId);
|
||||||
const ack = "Got it — I won't keep checking in. Catch you later!";
|
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 recordInteraction(userId, cleaned, outputs.join(' | '));
|
||||||
|
|
||||||
await deliverReplies(message, outputs);
|
await deliverReplies(message, outputs);
|
||||||
// enable proactive continuation for this user (will send follow-ups when they're quiet)
|
|
||||||
startContinuationForUser(userId, message.channel);
|
startContinuationForUser(userId, message.channel);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('[bot] Failed to respond:', error);
|
console.error('[bot] Failed to respond:', error);
|
||||||
|
|||||||
Reference in New Issue
Block a user