new system for chat and stuff

This commit is contained in:
legop3
2026-02-25 12:47:17 -05:00
parent ad31749813
commit b22ba216db
2 changed files with 157 additions and 43 deletions
+38 -19
View File
@@ -1,26 +1,45 @@
You are the automated overseer of the rovers. You are the automated overseer of the rovers.
Output exactly one line: SKIP or one chat message (max 140 chars).
No emojis. No markdown. No extra lines. No assistant framing.
SKIP is the default and preferred output. Output contract:
Only speak when there is a clearly notable, new moment. - Return exactly one line.
If uncertain, output SKIP. - Output must be either SKIP or one chat message (max 140 chars).
- No emojis, no markdown, no extra lines, no assistant framing.
Use rover state/activity and chat together when possible. Conversation format you will receive:
If chat is not clearly tied to rover state, output SKIP. - System message (this prompt).
Do not invent facts. - User run_meta message.
Do not assume people's intentions, motivations, or next actions. - Ordered timeline of user chat messages (with rover_ctx) and assistant bot messages.
- Optional user snapshot message(s) inserted in the timeline.
- Final user snapshot_final message with full current rover state.
Use self-quieting: Decision policy:
- If self_talk_recent_30m >= 5, strongly prefer SKIP. - SKIP is the default.
- Speak only for clearly notable, new moments.
- If uncertain, output SKIP.
Significant examples: wheels_off_ground true, dock/charge change, battery_low true, clear chat+rover event tie. Grounding policy:
Never post generic filler like "X is on the move". - Use timeline context for narrative flow.
If last_message_focus indicates same rover and same topic with no new change, output SKIP. - Use snapshot_final as current source of truth.
- Do not invent facts.
- Do not assume people's intentions, motivations, or next actions.
Numeric fields are internal context only. Significant examples:
Never directly quote or list snapshot numbers, counters, percentages, or timers. - wheels_off_ground true
Use numbers only to decide whether something is notable. - dock/charge transitions
- battery_low becoming true
- bump activity with meaningful chat tie-in
- clear multi-user chat+rover moment
Every non-SKIP line must still be grounded in real snapshot state, but phrased naturally. Repetition policy:
Style: slightly ominous commentary. No dry status reports. Be creative and atmospheric. - If last_message_focus indicates same rover and same topic with no new change, output SKIP.
- Never post generic filler (for example: "X is on the move").
Numeric policy:
- Numeric fields are internal context only.
- Never directly quote counters, percentages, or timers.
- Use numbers only to judge significance.
Style:
- Slightly ominous, atmospheric, playful.
- Natural commentary, not a dry status report.
- Short and punchy.
+115 -20
View File
@@ -22,6 +22,7 @@ const SKIP_TOKEN = 'SKIP';
const ACTIVITY_WINDOW_MS = 30000; const ACTIVITY_WINDOW_MS = 30000;
const ACTIVITY_BUCKET_MS = 1000; const ACTIVITY_BUCKET_MS = 1000;
const SELF_TALK_WINDOW_MS = 30 * 60 * 1000; const SELF_TALK_WINDOW_MS = 30 * 60 * 1000;
const MAX_CONTEXT_EVENTS = 100;
const config = loadConfig(); const config = loadConfig();
const commentaryConfig = config.llmCommentary || {}; const commentaryConfig = config.llmCommentary || {};
@@ -253,6 +254,19 @@ function buildLastMessageFocus(lastBotMessage, rovers = []) {
}; };
} }
function compactRoverForContext(rover) {
if (!rover) return null;
return {
id: rover.id,
status_tag: rover.status_tag,
battery_low: rover.battery_low,
docked: rover.docked,
charging: rover.charging,
wheels_off_ground: rover.wheels_off_ground,
activity_30s: rover.activity_30s,
};
}
function buildSnapshot() { function buildSnapshot() {
const now = new Date(); const now = new Date();
const nowMs = now.getTime(); const nowMs = now.getTime();
@@ -316,16 +330,18 @@ function buildSnapshot() {
nextRoverStateById.forEach((value, roverId) => { nextRoverStateById.forEach((value, roverId) => {
lastRoverStateById.set(roverId, value); lastRoverStateById.set(roverId, value);
}); });
const roverById = new Map(rovers.map((rover) => [String(rover.id), rover]));
const chatRecent = getRecentMessages(60, { includeSystem: false }) const allRecentMessages = getRecentMessages(300, { includeSystem: true })
.filter((entry) => Number(entry?.ts) >= contextResetAt) .filter((entry) => Number(entry?.ts) >= contextResetAt);
const chatRecent = allRecentMessages
.filter((entry) => !entry?.system)
.slice(-MAX_CHAT_MESSAGES) .slice(-MAX_CHAT_MESSAGES)
.map((entry) => ({ .map((entry) => ({
nickname: entry.nickname || entry.socketId?.slice(0, 6) || 'unknown', nickname: entry.nickname || entry.socketId?.slice(0, 6) || 'unknown',
text: entry.text || '', text: entry.text || '',
})); }));
const botRecentWindow = getRecentMessages(200, { includeSystem: true }) const botRecentWindow = allRecentMessages
.filter((entry) => Number(entry?.ts) >= contextResetAt) .filter((entry) => Number(entry?.ts) >= contextResetAt)
.filter((entry) => entry?.system); .filter((entry) => entry?.system);
const lastBotMessage = botRecentWindow.length ? botRecentWindow[botRecentWindow.length - 1] : null; const lastBotMessage = botRecentWindow.length ? botRecentWindow[botRecentWindow.length - 1] : null;
@@ -333,17 +349,46 @@ function buildSnapshot() {
(entry) => nowMs - Number(entry?.ts || 0) <= SELF_TALK_WINDOW_MS, (entry) => nowMs - Number(entry?.ts || 0) <= SELF_TALK_WINDOW_MS,
); );
const eventStream = allRecentMessages.slice(-MAX_CONTEXT_EVENTS).map((entry) => {
if (entry?.system) {
return { return {
activity: { type: 'bot',
nickname: entry.nickname || 'Rover Bot',
text: entry.text || '',
};
}
const roverId = entry?.roverId ? String(entry.roverId) : null;
const rover = roverId ? roverById.get(roverId) : null;
return {
type: 'chat',
nickname: entry.nickname || entry.socketId?.slice(0, 6) || 'unknown',
text: entry.text || '',
rover_id: roverId,
rover_ctx: compactRoverForContext(rover),
};
});
const hasRecentChat = eventStream.some((event) => event.type === 'chat');
if (!hasRecentChat) {
eventStream.push({
type: 'snapshot',
reason: 'chat_quiet',
rovers,
});
}
return {
run_meta: {
version: 'commentary_v2',
self_talk_recent_30m: botRecent30m.length,
last_message_focus: buildLastMessageFocus(lastBotMessage, rovers),
active_driver_count: driverEntries.length, active_driver_count: driverEntries.length,
driving_rovers: driverEntries.map(([roverId]) => String(roverId)), driving_rovers: driverEntries.map(([roverId]) => String(roverId)),
}, },
self_talk_recent_30m: botRecent30m.length, event_stream: eventStream,
last_message_focus: buildLastMessageFocus(lastBotMessage, rovers), current_snapshot: {
rovers, rovers,
chat_recent: chatRecent, chat_recent: chatRecent,
// Keep this compact: expose only one previous bot message summary via last_message_focus. },
// your_last_message: botRecent,
}; };
} }
@@ -383,19 +428,68 @@ async function generateCommentary(systemPrompt, snapshot) {
if (!ollamaClient) { if (!ollamaClient) {
throw new Error('Ollama client unavailable'); throw new Error('Ollama client unavailable');
} }
const messages = [];
messages.push({ role: 'system', content: systemPrompt });
messages.push({
role: 'user',
content: JSON.stringify({
type: 'run_meta',
run_meta: snapshot?.run_meta || {},
}),
});
const timeline = Array.isArray(snapshot?.event_stream) ? snapshot.event_stream : [];
timeline.forEach((event) => {
if (!event || typeof event !== 'object') return;
if (event.type === 'bot') {
const text = String(event.text || '').trim();
if (text) {
messages.push({ role: 'assistant', content: text });
}
return;
}
if (event.type === 'chat') {
messages.push({
role: 'user',
content: JSON.stringify({
type: 'chat',
nickname: event.nickname || 'unknown',
text: event.text || '',
rover_id: event.rover_id || null,
rover_ctx: event.rover_ctx || null,
}),
});
return;
}
if (event.type === 'snapshot') {
messages.push({
role: 'user',
content: JSON.stringify({
type: 'snapshot',
reason: event.reason || null,
rovers: event.rovers || [],
}),
});
}
});
// Always end with a full rover snapshot user message.
messages.push({
role: 'user',
content: JSON.stringify({
type: 'snapshot_final',
current_snapshot: snapshot?.current_snapshot || {},
}),
});
const payload = await ollamaClient.chat({ const payload = await ollamaClient.chat({
model, model,
stream: false, stream: false,
keep_alive: -1, keep_alive: -1,
options: { options: {
temperature: 0.35, temperature: 0.45,
top_p: 0.8, top_p: 0.9,
num_predict: 80, num_predict: 128,
}, },
messages: [ messages,
{ role: 'system', content: systemPrompt },
{ role: 'user', content: JSON.stringify(snapshot) },
],
}); });
return normalizeCommentary(payload?.message?.content); return normalizeCommentary(payload?.message?.content);
} }
@@ -451,10 +545,11 @@ async function runTick() {
return; return;
} }
const snapshotSummary = { const snapshotSummary = {
activeDrivers: snapshot.activity.active_driver_count, activeDrivers: snapshot?.run_meta?.active_driver_count || 0,
rovers: snapshot.rovers.length, rovers: snapshot?.current_snapshot?.rovers?.length || 0,
chatMessages: snapshot.chat_recent.length, chatMessages: snapshot?.current_snapshot?.chat_recent?.length || 0,
drivingRovers: snapshot.activity.driving_rovers, drivingRovers: snapshot?.run_meta?.driving_rovers || [],
eventCount: snapshot?.event_stream?.length || 0,
}; };
logger.info('Commentary tick started', { logger.info('Commentary tick started', {
tickId, tickId,