openvibers/openvibe.chat Commits

"Plan my day" reads the day before the answer; the prompt says what Vibe's own chat can do (#333)

OP OpenVibers committed 11 days ago · f91bb566f5 · 2 files, +41 −4

The production proof (11-02) got a generic plan: the model never called my_day. When the person plainly asks to plan
their day (PLAN_DAY: "plan my day", "what's on today", "how's my week looking"…), the day is read before the first
round, as if Vibe had called my_day, and the turn counts as having read it (the confirm-first guard holds). The prompt
said "you cannot set reminders" even in the person's own chat, where the tools exist; there it now names them and
how to plan a day.

Changes

ddiff --git a/server/rooms/ai-members.js b/server/rooms/ai-members.js
iindex a3d29dd..8b46514 100644
---- a/server/rooms/ai-members.js
++++ b/server/rooms/ai-members.js
@@@ -231,7 +231,7 @@ function dateLine(now, timezone, askedBy) {
return `Today is ${utc} (UTC); for ${askedBy} it is ${local} in ${timezone}.`;
}
--function vibePrompt(member, askedBy, memory = null, { now = Date.now(), timezone = null } = {}) {
++function vibePrompt(member, askedBy, memory = null, { now = Date.now(), timezone = null, ownTools = false } = {}) {
return [
`You are ${member.name}, OpenVibe's assistant, in a private chat with ${askedBy} on OpenVibe.Chat. Only they read it.`,
dateLine(now, timezoneOf(timezone), askedBy),
@@@ -247,7 +247,9 @@ function vibePrompt(member, askedBy, memory = null, { now = Date.now(), timezone
'- News: OpenVibe.News has no stories yet, so for news go to web_search with freshness "day" (or "week" for a roundup) and say which web source and date each story came from; never present a recorded stream or video as today\'s news.',
'- When they send a picture you see it: describe what matters for their question (text in it, objects, a screenshot\'s error). Never guess who a person in it is.',
'- Recurring work ("every morning, find me new remote jobs"): set_up_agent makes it one of their OpenVibe Agents, run daily or weekly on their own allowance with every result on its page. First say what it would do and when, and set it up only once they agree. my_agents lists the agents they have.',
-- '- You cannot yet make pictures, use their computer or set reminders. Say so plainly when asked, and offer what you can do instead.',
++ ownTools
++ ? '- In their own chat you can set reminders (set_reminder), list and stop them, start or stop their timer, and read their day (my_day). For "plan my day" or "what is on today": call my_day, suggest a plan around what is already there (what to do when, which reminders or a focus timer would help), and ask before you set anything; set what they agree to when they reply. You cannot yet make pictures or use their computer.'
++ : '- You cannot yet make pictures, use their computer or set reminders. Say so plainly when asked, and offer what you can do instead.',
'- The earlier messages are this conversation: build on them. Never invent facts about the person or their plans; when you do not know, say so and ask.',
memory === null
? '- Their memory is off: you keep no notes about them. If they ask you to remember something, say they can turn memory on at openvibe.chat/vibe/memory.'
@@@ -302,6 +304,9 @@ function userPrompt({ member, context, transcript, askedBy, text }) {
].join('');
}
++// "plan my day", "plan out my afternoon", "what's on today", "what does my day look like", "how's my week looking"
++const PLAN_DAY = /\b(plan(?:\s+out)?\s+(?:my|the)\s+(?:day|morning|afternoon|evening|tomorrow)|what(?:'s|\s+is)\s+on\s+(?:my\s+)?(?:today|tomorrow|my\s+(?:day|agenda|calendar))|what\s+does\s+my\s+(?:day|morning|afternoon|tomorrow)\s+look|how(?:'s|s|\s+is)\s+my\s+(?:day|week)\s+looking|my\s+agenda\b)/i;
++
/**
* The answer through chat.member.turn with tools: up to MAX_TOOL_ROUNDS rounds of tool calls run here, then a turn
* with tool_choice none. → the reply text, or null when a turn failed (the caller falls back to chat.member.reply).
@@@ -312,10 +317,20 @@ async function answerWithTools({ room, member, context, transcript, askedBy, tex
const tz = timezoneOf(timezone);
const prompt = userPrompt({ member, context, transcript, askedBy, text });
const messages = [
-- { role: 'system', content: vibe ? vibePrompt(member, askedBy, memory, { timezone: tz }) : systemPrompt(member, room) },
++ { role: 'system', content: vibe ? vibePrompt(member, askedBy, memory, { timezone: tz, ownTools: tools.some((t) => t.function && t.function.name === 'my_day') }) : systemPrompt(member, room) },
// Pictures the person sent (picturesFor): the model sees them with the text.
{ role: 'user', content: images.length ? [{ type: 'text', text: `${prompt}\n\n(${images.length === 1 ? 'The picture they sent is' : 'The pictures they sent are'} attached.)` }, ...images.map((url) => ({ type: 'image_url', image_url: { url } }))] : prompt },
];
++ if (vibe && PLAN_DAY.test(String(text || '')) && tools.some((t) => t.function && t.function.name === 'my_day')) {
++ // "Plan my day" (L48 V3): the day is read before the first round, as if Vibe had called my_day itself, so a
++ // small model cannot plan without it; the turn then counts as having read the day (nothing is set in it).
++ called.add('my_day');
++ if (steps) require('./how').step(steps, 'my_day', '{}');
++ const tool = await aiTools.runWithResult('my_day', '{}', { member, contextFor, subject, room, user, messageId, memory: memory !== null, timezone: tz, requestText: text, called, idempotencyKey: `${opts.idempotencyKey}-day` });
++ if (steps) require('./how').cite(steps, 'my_day', tool.result);
++ messages.push({ role: 'assistant', content: null, tool_calls: [{ id: 'call_my_day', type: 'function', function: { name: 'my_day', arguments: '{}' } }] });
++ messages.push({ role: 'tool', tool_call_id: 'call_my_day', content: tool.text });
++ }
for (let round = 0; round <= MAX_TOOL_ROUNDS; round++) {
if (signal && signal.aborted) return null; // stopped (stop()): the caller keeps what was written
const last = round === MAX_TOOL_ROUNDS;
@@@ -506,4 +521,4 @@ const toWire = (m) => ({
function _reset() { inFlight.clear(); personHour.clear(); stoppers.clear(); }
--module.exports = { MAX_PER_ROOM, LIMITS, AiMemberError, enabled, list, byId, add, update, remove, dropKey, relabelKey, modelOf, mentioned, handleOf, afterPost, stop, transcriptOf, vibePrompt, toWire, _reset };
++module.exports = { PLAN_DAY, MAX_PER_ROOM, LIMITS, AiMemberError, enabled, list, byId, add, update, remove, dropKey, relabelKey, modelOf, mentioned, handleOf, afterPost, stop, transcriptOf, vibePrompt, toWire, _reset };
ddiff --git a/test/vibe-plan-my-day.test.js b/test/vibe-plan-my-day.test.js
iindex 24f0f74..518a236 100644
---- a/test/vibe-plan-my-day.test.js
++++ b/test/vibe-plan-my-day.test.js
@@@ -85,6 +85,28 @@ t('when Today cannot answer, Vibe is told so plainly', async () => {
assert.match(out.error, /unreachable/);
});
++t('"plan my day" reaches the model with the day already read, so a small model cannot skip it', async () => {
++ const { PLAN_DAY } = require('../server/rooms/ai-members');
++ ['Plan my day for me', 'plan out my afternoon', "what's on today?", 'What does my day look like', 'hows my week looking'].forEach((m) => assert.ok(PLAN_DAY.test(m), m));
++ ['Plan a trip to Lisbon', 'remind me at 3pm', 'what is on TV'].forEach((m) => assert.ok(!PLAN_DAY.test(m), m));
++ const aiClient = require('../server/ai/client');
++ const seen = [];
++ const say = (content) => ({ choices: [{ index: 0, message: { role: 'assistant', content }, finish_reason: 'stop' }] });
++ aiClient._setClient({ runs: { create: async (workflow, ...rest) => { seen.push(JSON.stringify(rest)); return { run: { status: 'succeeded', output: workflow === 'chat.member.turn' ? say('Here is a plan. Shall I set the reminders?') : { text: 'ok' } } }; } } });
++ const ask = await h.http('POST', '/api/vibe/ask', { token: ann.token, body: { message: 'Plan my day for me', timezone: 'Asia/Tokyo', fresh: true } });
++ assert.strictEqual(ask.status, 201, ask.text);
++ let answer = null;
++ let tries = 0;
++ while (!answer && tries++ < 60) {
++ const r = await h.http('GET', `/api/vibe/chat?slug=${encodeURIComponent(ask.body.chat.slug)}&after=${ask.body.message.id}`, { token: ann.token });
++ answer = (r.body.messages || []).find((m) => m.from === 'vibe') || null;
++ if (!answer) await new Promise((r2) => setTimeout(r2, 100));
++ }
++ assert.ok(answer, 'Vibe answered');
++ assert.ok(seen.some((x) => x.includes('call_my_day')), 'the model saw the day as a my_day result');
++ aiClient._setClient(null);
++});
++
t('done', async () => { await h.close(); });
t.run();