mirror of
https://github.com/legop3/MultiRoombaRover.git
synced 2026-09-16 01:21:20 -04:00
new system for chat and stuff
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
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 {
|
return {
|
||||||
activity: {
|
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,
|
||||||
|
|||||||
Reference in New Issue
Block a user