Add global memory toggle
This commit is contained in:
@@ -47,3 +47,7 @@ All notable changes made during this working session (March 1, 2026).
|
|||||||
|
|
||||||
### Configuration / examples
|
### Configuration / examples
|
||||||
- Updated `.env.example` to include `OPENAI_API_KEY`.
|
- Updated `.env.example` to include `OPENAI_API_KEY`.
|
||||||
|
|
||||||
|
### Global memory toggle (2026-03-03)
|
||||||
|
- Added `useGlobalMemories` to `buildPrompt`, allowing long-term retrieval across every stored `user_id` without losing the current user's short-term history.
|
||||||
|
- Long-term results now carry their `user_id` so the prompt can label each snippet (e.g., `- [userId] ...`) and the retrieval scoring still ranks by cosine similarity plus importance.
|
||||||
|
|||||||
59
README.md
59
README.md
@@ -2,60 +2,18 @@
|
|||||||
|
|
||||||
Nova is a friendly, slightly witty Discord companion that chats naturally in DMs or when mentioned in servers. It runs on Node.js, uses `discord.js` v14, and supports OpenRouter (recommended) or OpenAI backends for model access, plus lightweight local memory for persistent personality.
|
Nova is a friendly, slightly witty Discord companion that chats naturally in DMs or when mentioned in servers. It runs on Node.js, uses `discord.js` v14, and supports OpenRouter (recommended) or OpenAI backends for model access, plus lightweight local memory for persistent personality.
|
||||||
|
|
||||||
## Recent changes (2026-03-01)
|
## Recent changes (2026-03-03)
|
||||||
- Added token-usage + performance optimizations (prompt builder + context caching + smaller injected payloads).
|
- Added a global memory mode that optionally pulls long-term entries from every user while still tagging each snippet by `user_id`.
|
||||||
- Upgraded the local memory dashboard: long-term memory create/edit, pagination (15 per page), search preview helper, and a recall timeline view.
|
- Documented the `useGlobalMemories` flag so prompt construction can switch to the cross-user context when needed without losing per-user summaries.
|
||||||
- Added Discord-side extras: `🧠` memory-injected reaction badge + a `/blackjack` embed mini-game with buttons.
|
- Full session log lives in `CHANGELOG.md` (and mirrors the latest update below).
|
||||||
- Full session log lives in `CHANGELOG.md` (and is mirrored below). test
|
|
||||||
|
|
||||||
<details>
|
<details>
|
||||||
<summary>Full update log (session)</summary>
|
<summary>Full update log (session)</summary>
|
||||||
|
|
||||||
- Token + performance optimizations
|
- Global memory toggle (2026-03-03)
|
||||||
- Added `src/prompt.js` to centralize prompt construction (`buildPrompt`) and reduce repeated prompt-building logic.
|
- Added `useGlobalMemories` support to `buildPrompt` so cross-user long-term memory retrieval can be enabled without losing the local context.
|
||||||
- Added a short-lived in-memory context cache in `src/bot.js` to reuse prepared context across the continuation loop and normal replies.
|
- `retrieveRelevantMemories` now surfaces each `user_id` and the prompt prefixes them (e.g., `- [123456] ...`) when the toggle is active, letting Nova know who owns every snippet.
|
||||||
- Reduced default memory/prompt sizes in `src/config.js`:
|
- Cosine relevance still ranks entries by similarity plus importance, so the top-K results pick the best matches even across users.
|
||||||
- `shortTermLimit`: 10 -> 6
|
|
||||||
- `summaryTriggerChars`: 3000 -> 2200
|
|
||||||
- `relevantMemoryCount`: 5 -> 3
|
|
||||||
- Added `longTermFetchLimit` (default 120)
|
|
||||||
- Limited long-term memory retrieval to a recent window before similarity scoring in `src/memory.js` (uses `longTermFetchLimit`).
|
|
||||||
- Summarized live web-search intel before injecting it into the prompt (keeps the payload shorter) in `src/bot.js`.
|
|
||||||
- Debounced memory DB persistence in `src/memory.js` to batch multiple writes (instead of exporting/writing on every mutation).
|
|
||||||
|
|
||||||
- Dashboard (local memory UI)
|
|
||||||
- Revamped the dashboard UI layout + styling in `src/public/index.html`.
|
|
||||||
- Added long-term memory create/edit support:
|
|
||||||
- API: `POST /api/users/:id/long` in `src/dashboard.js`
|
|
||||||
- Store: `upsertLongTerm()` in `src/memory.js`
|
|
||||||
- Added long-term memory pagination:
|
|
||||||
- API: `GET /api/users/:id/long?page=&per=` returns `{ rows, total, page, per, totalPages }` via `getLongTermMemoriesPage()` in `src/memory.js`
|
|
||||||
- UI: paging controls; long-term list shows 15 per page (`LONG_TERM_PER_PAGE = 15`)
|
|
||||||
- Added "search preview" UX in the dashboard to quickly reuse a similar memory result as an edit/create starting point ("Use this memory").
|
|
||||||
- Added a simple recall timeline:
|
|
||||||
- API: `GET /api/users/:id/timeline?days=` in `src/dashboard.js`
|
|
||||||
- Store: `getMemoryTimeline()` in `src/memory.js`
|
|
||||||
- UI: lightweight bar chart in `src/public/index.html`
|
|
||||||
|
|
||||||
- Fixes
|
|
||||||
- Fixed dashboard long-term pagination wiring (`getLongTermMemoriesPage` import/usage) in `src/dashboard.js`.
|
|
||||||
- Fixed dashboard long-term "Edit" button behavior by wiring row handlers in `src/public/index.html`.
|
|
||||||
- Prevented button interactions from crashing the bot on late/invalid updates by deferring updates and editing the message in `src/bot.js`.
|
|
||||||
|
|
||||||
- Discord-side features
|
|
||||||
- Added a memory-aware reaction badge: bot reacts with `🧠` when long-term memories were injected into the prompt (`src/bot.js`).
|
|
||||||
- Added a lightweight blackjack mini-game:
|
|
||||||
- Start via text trigger `/blackjack` (not a registered slash command).
|
|
||||||
- Single-embed game UI with button components for actions (Hit / Stand; Split is present as a placeholder).
|
|
||||||
- Improved interaction handling to avoid "Unknown interaction" crashes by deferring updates and editing the message (`src/bot.js`).
|
|
||||||
|
|
||||||
- Reliability / guardrails
|
|
||||||
- Relaxed the "empty response" guard in `src/openai.js`:
|
|
||||||
- Still throws when the provider returns no choices.
|
|
||||||
- If choices exist but content is blank, returns an empty string instead of forcing fallback (reduces noisy false-positive failures).
|
|
||||||
|
|
||||||
- Configuration / examples
|
|
||||||
- Updated `.env.example` to include `OPENAI_API_KEY`.
|
|
||||||
|
|
||||||
</details>
|
</details>
|
||||||
|
|
||||||
@@ -63,6 +21,7 @@ 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.
|
||||||
|
- Optional global memory retrieval: set `useGlobalMemories=true` when calling `buildPrompt` so Nova can pull long-term memories across every `user_id`, and each snippet is labeled (e.g., `- [123] ...`) to keep the source clear while keeping summaries scoped to the active user.
|
||||||
- **Rotating “daily mood” engine** that adjusts Nova’s personality each day (calm, goblin, philosopher, etc.). Mood influences emoji use, sarcasm, response length, and hype. (Now randomized each run rather than fixed by calendar date.)
|
- **Rotating “daily mood” engine** that adjusts Nova’s personality each day (calm, goblin, philosopher, etc.). Mood influences emoji use, sarcasm, response length, and hype. (Now randomized each run rather than fixed by calendar date.)
|
||||||
- **LLM-powered live–intel web search**: Nova uses the LLM itself to decide whether a topic needs a live web search. If you mention something unfamiliar or that requires current info, it automatically Googles first and uses the results in its response—without triggering on casual chat.
|
- **LLM-powered live–intel web search**: Nova uses the LLM itself to decide whether a topic needs a live web search. If you mention something unfamiliar or that requires current info, it automatically Googles first and uses the results in its response—without triggering on casual chat.
|
||||||
- **Optional local memory dashboard** (enabled with `ENABLE_DASHBOARD=true`): spin up a simple browser UI alongside the bot. Inspect stored memories by user, delete entries, run similarity queries, view importance scores, and peek at Nova’s current mood and quirky “status” of the day. The dashboard runs on `DASHBOARD_PORT` (3000 by default) and is entirely optional.
|
- **Optional local memory dashboard** (enabled with `ENABLE_DASHBOARD=true`): spin up a simple browser UI alongside the bot. Inspect stored memories by user, delete entries, run similarity queries, view importance scores, and peek at Nova’s current mood and quirky “status” of the day. The dashboard runs on `DASHBOARD_PORT` (3000 by default) and is entirely optional.
|
||||||
|
|||||||
@@ -313,15 +313,22 @@ const migrateLegacyStore = async (db) => {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const retrieveRelevantMemories = async (db, userId, query) => {
|
const retrieveRelevantMemories = async (db, userId, query, options = {}) => {
|
||||||
if (!query?.trim()) {
|
if (!query?.trim()) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
const limit = config.longTermFetchLimit || 200;
|
const limit = config.longTermFetchLimit || 200;
|
||||||
|
const { includeAllUsers = false } = options;
|
||||||
|
const params = [];
|
||||||
|
const whereClause = includeAllUsers ? '' : ' WHERE user_id = ?';
|
||||||
|
if (!includeAllUsers) {
|
||||||
|
params.push(userId);
|
||||||
|
}
|
||||||
|
params.push(limit);
|
||||||
const rows = all(
|
const rows = all(
|
||||||
db,
|
db,
|
||||||
'SELECT id, content, embedding, importance, timestamp FROM long_term WHERE user_id = ? ORDER BY timestamp DESC LIMIT ?',
|
`SELECT id, user_id, content, embedding, importance, timestamp FROM long_term${whereClause} ORDER BY timestamp DESC LIMIT ?`,
|
||||||
[userId, limit],
|
params,
|
||||||
);
|
);
|
||||||
if (!rows.length) {
|
if (!rows.length) {
|
||||||
return [];
|
return [];
|
||||||
@@ -354,12 +361,13 @@ export async function appendShortTerm(userId, role, content) {
|
|||||||
await persistDb(db);
|
await persistDb(db);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function prepareContext(userId, incomingMessage) {
|
export async function prepareContext(userId, incomingMessage, options = {}) {
|
||||||
const db = await loadDatabase();
|
const db = await loadDatabase();
|
||||||
ensureUser(db, userId);
|
ensureUser(db, userId);
|
||||||
const userRow = get(db, 'SELECT summary FROM users WHERE id = ?', [userId]) || { summary: '' };
|
const userRow = get(db, 'SELECT summary FROM users WHERE id = ?', [userId]) || { summary: '' };
|
||||||
const shortTerm = getShortTermHistory(db, userId, config.shortTermLimit);
|
const shortTerm = getShortTermHistory(db, userId, config.shortTermLimit);
|
||||||
const memories = await retrieveRelevantMemories(db, userId, incomingMessage);
|
const { includeAllUsers = false } = options;
|
||||||
|
const memories = await retrieveRelevantMemories(db, userId, incomingMessage, { includeAllUsers });
|
||||||
return {
|
return {
|
||||||
shortTerm,
|
shortTerm,
|
||||||
summary: userRow.summary || '',
|
summary: userRow.summary || '',
|
||||||
@@ -492,9 +500,9 @@ export async function upsertLongTerm(userId, entry) {
|
|||||||
return { id: newId, timestamp: now, created: true };
|
return { id: newId, timestamp: now, created: true };
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function findSimilar(userId, query) {
|
export async function findSimilar(userId, query, options = {}) {
|
||||||
const db = await loadDatabase();
|
const db = await loadDatabase();
|
||||||
return retrieveRelevantMemories(db, userId, query);
|
return retrieveRelevantMemories(db, userId, query, options);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function getDailyThoughtFromDb(date) {
|
export async function getDailyThoughtFromDb(date) {
|
||||||
|
|||||||
@@ -93,10 +93,16 @@ export async function buildPrompt(userId, incomingText, options = {}) {
|
|||||||
blockedSearchTerm = null,
|
blockedSearchTerm = null,
|
||||||
searchOutage = null,
|
searchOutage = null,
|
||||||
context: providedContext = null,
|
context: providedContext = null,
|
||||||
|
useGlobalMemories = false,
|
||||||
} = options;
|
} = options;
|
||||||
const context = providedContext || (await prepareContext(userId, incomingText));
|
const context =
|
||||||
|
providedContext || (await prepareContext(userId, incomingText, { includeAllUsers: useGlobalMemories }));
|
||||||
const memoryLines = context.memories.length
|
const memoryLines = context.memories.length
|
||||||
? context.memories.map((m) => `- ${m.content}`).join('\n')
|
? context.memories
|
||||||
|
.map((m) =>
|
||||||
|
useGlobalMemories && m.user_id ? `- [${m.user_id}] ${m.content}` : `- ${m.content}`,
|
||||||
|
)
|
||||||
|
.join('\n')
|
||||||
: '- No long-term memories retrieved.';
|
: '- No long-term memories retrieved.';
|
||||||
const summaryLine = context.summary || 'No running summary yet.';
|
const summaryLine = context.summary || 'No running summary yet.';
|
||||||
const dynamicDirectives = composeDynamicPrompt({
|
const dynamicDirectives = composeDynamicPrompt({
|
||||||
|
|||||||
Reference in New Issue
Block a user