openvibers/openvibe.chat Commits

Try again asks AI anew: the retried answer's runs have their own idempotency key (-r<the answer it replaces>), so AI no longer replays the first answer's run (the same text, or a stopped run's fallback) (#187)

OP OpenVibers committed 20 days ago · f1f061aca8 · 3 files, +8 −3

Changes

ddiff --git a/server/rooms/ai-members.js b/server/rooms/ai-members.js
iindex 988b8ad..47abf13 100644
---- a/server/rooms/ai-members.js
++++ b/server/rooms/ai-members.js
@@@ -350,7 +350,7 @@ async function afterPost(room, message, user, deps = {}) {
broadcast({ type: 'room_ai_typing', room: room.slug, name: member.name });
const history = await rooms.history(room, { limit: TRANSCRIPT });
const context = await contextFor(member);
-- const opts = { force: true, attribution: { service: 'chat', type: 'room', id: String(room.id), label: String(room.name || room.slug).slice(0, 200) }, idempotencyKey: `chat-ai-${room.id}-${message.id}`,
++ const opts = { force: true, attribution: { service: 'chat', type: 'room', id: String(room.id), label: String(room.name || room.slug).slice(0, 200) }, idempotencyKey: `chat-ai-${room.id}-${message.id}${deps.retryOf ? `-r${Number(deps.retryOf)}` : ''}`,
...(own ? { credential: { subject: member.engine_subject } } : {}) };
const transcript = transcriptOf(history, vibe ? { each: 6000, total: 24000 } : {}) || '(no earlier messages)';
const askedBy = (user.display_name || user.username || 'someone').slice(0, 80);
ddiff --git a/server/rooms/vibe.js b/server/rooms/vibe.js
iindex 8350917..17733fc 100644
---- a/server/rooms/vibe.js
++++ b/server/rooms/vibe.js
@@@ -204,7 +204,8 @@ async function retry(room, user) {
if (!last) fail(409, 'vibe.nothing_to_retry', 'Only Vibe\'s answer to your last message can be tried again');
await db.run('UPDATE room_messages SET is_deleted = 1, deleted_by = ? WHERE id = ? AND is_deleted = 0', [user.id, last.answer.id]);
try { require('../chat/chat-server').broadcastToRoom(room.id, { type: 'room_message_deleted', room: room.slug, id: Number(last.answer.id) }); } catch { /* sockets are optional */ }
-- setImmediate(() => { require('./ai-members').afterPost(room, last.asked, user).catch(() => {}); });
++ // A new key for AI (the answer it replaces names it): the same message's key would replay that answer's run.
++ setImmediate(() => { require('./ai-members').afterPost(room, last.asked, user, { retryOf: last.answer.id }).catch(() => {}); });
return { removed: Number(last.answer.id) };
}
ddiff --git a/test/vibe-retry.test.js b/test/vibe-retry.test.js
iindex 3bd09c2..ff37c1f 100644
---- a/test/vibe-retry.test.js
++++ b/test/vibe-retry.test.js
@@@ -12,6 +12,7 @@ const t = suite('vibe retry');
const SITE = 'https://openvibe.chat';
let h, ann, bob, aiClient, vibe;
let n = 0;
++const keys = [];
const vibeApi = (method, path, u, body) => h.http(method, `/api/chat/vibe${path}`, { token: u.token, body });
const web = async (method, path, u, fields) => {
const headers = { cookie: `ov_token=${u.token}`, ...(fields ? { 'content-type': 'application/x-www-form-urlencoded', origin: SITE } : {}) };
@@@ -34,7 +35,7 @@ t('boot', async () => {
ann = h.addUser('ann', { subject: ids.newId('user') });
bob = h.addUser('bob', { subject: ids.newId('user') });
for (const u of [ann, bob]) await h.ctx.upsertUser(h.live.users.get(u.id));
-- aiClient._setClient({ runs: { create: async () => { n++; return { run: { status: 'succeeded', output: { choices: [{ message: { content: `Answer ${n}\n\n\`\`\`js\nconsole.log(${n});\n\`\`\`` } }] } } }; } } });
++ aiClient._setClient({ runs: { create: async (workflow, input, o = {}) => { keys.push(o.idempotencyKey); n++; return { run: { status: 'succeeded', output: { choices: [{ message: { content: `Answer ${n}\n\n\`\`\`js\nconsole.log(${n});\n\`\`\`` } }] } } }; } } });
});
let slug;
@@@ -51,6 +52,9 @@ t('Try again removes the answer and writes a new one, counted like any answer',
assert.notStrictEqual(after[0].id, first.id);
assert.match(after[0].message, /^Answer 2/);
assert.strictEqual(await vibe.repliesToday({ id: ann.id }), used + 1);
++ // AI answers once per idempotency key: the retry has its own, or AI would replay the first answer's run.
++ assert.match(keys[0], /^chat-ai-\d+-\d+-t0$/);
++ assert.strictEqual(keys[1], keys[0].replace(/-t0$/, `-r${first.id}-t0`));
});
t('only the answer to the person\'s last message; another person\'s chat is not there', async () => {