openvibers/openvibe.ai Commits
Add Chat person memory workflow through Exchange (#462)
Changes
@@@ -602,4 +602,30 @@ const CHAT_OWNED = [['live.chat.global', 'chat.global'], ['live.chat.profile', 'return { ...w, key, namespace: 'chat', description: `${w.description} Run by OpenVibe.Chat under its own namespace.` };});--module.exports = { templates, workflows: [...structured, ...CHAT_OWNED, ...passthrough], KIND_TO_WORKFLOW, ROLES, PASSTHROUGH_INPUT, PASSTHROUGH_OUTPUT };++// Chat's compacted per-person memory. A day reads messages; a week reads day summaries;++// a month reads week summaries. The source index lets Chat attach a verified stream time.++templates.push({++ key: 'chat.person-memory', name: 'A person in chat, compacted',++ description: 'Summarize one period of a consenting person\'s chat activity without sensitive traits.',++ input_schema: { type: 'object' },++ output_schema: { type: 'object' },++ system_prompt: 'Summarize only the supplied chat activity. Do not infer appearance, identity, health, politics, religion, sexuality or other sensitive traits. Do not include other people\'s names or usernames. Never invent an event, time or count. Output JSON only.',++ user_prompt: 'Period: {{level}} beginning {{period}}. Previous summary: {{prior_summary}}. Sources, with zero-based indexes: {{sources_text}}. Return {"summary":"under 400 characters","topics":["up to five topics"],"highlights":[{"index":0,"detail":"notable moment under 180 characters"}],"tone":"how this person writes, under 120 characters","catchphrases":["up to five phrases actually used"]}. Include a highlight only when a source supports it.',++ default_route: 'live.legacy', owner: 'chat', metadata: { json_name: 'person_memory' },++});++const CHAT_PERSON_MEMORY = {++ key: 'chat.person-memory', name: 'A person in chat, compacted', namespace: 'chat',++ description: 'Incremental chat memory for a day, week or month; weeks read days and months read weeks.',++ input_schema: { type: 'object', additionalProperties: false, required: ['level', 'period', 'prior_summary', 'sources_text'], properties: {++ level: { enum: ['day', 'week', 'month'] }, period: STR(10), prior_summary: STR(500), sources_text: STR(15000),++ } },++ output_schema: { type: 'object', additionalProperties: false, required: ['summary', 'topics', 'highlights', 'tone', 'catchphrases'], properties: {++ summary: STR(500), topics: { type: 'array', maxItems: 5, items: STR(100) },++ highlights: { type: 'array', maxItems: 5, items: { type: 'object', additionalProperties: false, required: ['index', 'detail'], properties: { index: { type: 'integer', minimum: 0 }, detail: STR(180) } } },++ tone: STR(120), catchphrases: { type: 'array', maxItems: 5, items: STR(100) },++ } },++ steps: [{ kind: 'llm', template: 'chat.person-memory', operation: 'generate', output: 'json', params: { max_tokens: 450, temperature: 0.2 } }],++ cache_mode: 'none',++};++++module.exports = { templates, workflows: [...structured, ...CHAT_OWNED, CHAT_PERSON_MEMORY, ...passthrough], KIND_TO_WORKFLOW, ROLES, PASSTHROUGH_INPUT, PASSTHROUGH_OUTPUT };@@@ -14,6 +14,17 @@ const live = require('../server/workflows/live');const t = suite('live-templates');const tpl = (live.templates || []).find((x) => x.key === 'live.moments.pick');++t.test('Chat person memory has an authorized workflow and an evidence-only prompt', () => {++ const workflow = live.workflows.find(x => x.key === 'chat.person-memory');++ const template = live.templates.find(x => x.key === 'chat.person-memory');++ assert.strictEqual(workflow.namespace, 'chat');++ assert.strictEqual(workflow.steps[0].params.max_tokens, 450);++ const prompt = render(template.user_prompt, { level: 'day', period: '2026-11-11', prior_summary: 'A game', sources_text: '[{"message":"hello"}]' });++ assert.match(prompt, /Sources, with zero-based indexes/);++ assert.match(template.system_prompt, /Do not infer appearance/);++ assert.ok(!prompt.includes('{{'));++});++t.test('the prompt renders as Live rendered it', () => {assert.ok(tpl, 'the template exists');const prep = PREPARE['live.moments.pick']({ title: 'Rust night', flavor: 'clip', timeline: [{ t: 95, text: 'streamer jumps up' }], transcript: [{ t: 94, text: 'IT COMPILES' }], sounds: [{ t: 96, label: 'Cheering' }], clipped: [95, 3605], spikes: [96], avoid: [300, 600] });@@@ -84,6 +84,7 @@ EXAMPLES['live.chat.profile'] = { subject_kind: 'relay', name: 'KickFan', platfo// Chat runs the same two workflows under its own namespace (plan T3).EXAMPLES['chat.global'] = EXAMPLES['live.chat.global'];EXAMPLES['chat.profile'] = EXAMPLES['live.chat.profile'];++EXAMPLES['chat.person-memory'] = { level: 'day', period: '2026-11-10', prior_summary: '', sources_text: '[{"message":"hello chat","timestamp":"2026-11-10 12:00:00","stream_id":1}]' };EXAMPLES['live.person.overview'] = { as_streamer: 'Codes rust late at night.', as_chatter: 'Jokes about croutons.' };EXAMPLES['live.stream.titles'] = { summaries: ['Fixed a borrow checker bug live', 'Built a robot arm and it waved'] };EXAMPLES['live.moments.rank'] = { vods: [{ title: 'Rust night', overview: 'the build finally compiles', views: 30, clips: 2, peak_viewers: 5 }, { title: 'Chill', overview: 'sitting at a desk', views: 3, clips: 0, peak_viewers: 1 }], want: 2 };
@@ -602,4 +602,30 @@ const CHAT_OWNED = [['live.chat.global', 'chat.global'], ['live.chat.profile', '
@@ -602,4 +602,30 @@ const CHAT_OWNED = [['live.chat.global', 'chat.global'], ['live.chat.profile', '
return { ...w, key, namespace: 'chat', description: `${w.description} Run by OpenVibe.Chat under its own namespace.` };
return { ...w, key, namespace: 'chat', description: `${w.description} Run by OpenVibe.Chat under its own namespace.` };
});
});
-module.exports = { templates, workflows: [...structured, ...CHAT_OWNED, ...passthrough], KIND_TO_WORKFLOW, ROLES, PASSTHROUGH_INPUT, PASSTHROUGH_OUTPUT };
+// Chat's compacted per-person memory. A day reads messages; a week reads day summaries;
+// a month reads week summaries. The source index lets Chat attach a verified stream time.
+templates.push({
+ key: 'chat.person-memory', name: 'A person in chat, compacted',
+ description: 'Summarize one period of a consenting person\'s chat activity without sensitive traits.',
+ input_schema: { type: 'object' },
+ output_schema: { type: 'object' },
+ system_prompt: 'Summarize only the supplied chat activity. Do not infer appearance, identity, health, politics, religion, sexuality or other sensitive traits. Do not include other people\'s names or usernames. Never invent an event, time or count. Output JSON only.',
+ user_prompt: 'Period: {{level}} beginning {{period}}. Previous summary: {{prior_summary}}. Sources, with zero-based indexes: {{sources_text}}. Return {"summary":"under 400 characters","topics":["up to five topics"],"highlights":[{"index":0,"detail":"notable moment under 180 characters"}],"tone":"how this person writes, under 120 characters","catchphrases":["up to five phrases actually used"]}. Include a highlight only when a source supports it.',
+ default_route: 'live.legacy', owner: 'chat', metadata: { json_name: 'person_memory' },
+});
+const CHAT_PERSON_MEMORY = {
+ key: 'chat.person-memory', name: 'A person in chat, compacted', namespace: 'chat',
+ description: 'Incremental chat memory for a day, week or month; weeks read days and months read weeks.',
+ input_schema: { type: 'object', additionalProperties: false, required: ['level', 'period', 'prior_summary', 'sources_text'], properties: {
+ level: { enum: ['day', 'week', 'month'] }, period: STR(10), prior_summary: STR(500), sources_text: STR(15000),
+ } },
+ output_schema: { type: 'object', additionalProperties: false, required: ['summary', 'topics', 'highlights', 'tone', 'catchphrases'], properties: {
+ summary: STR(500), topics: { type: 'array', maxItems: 5, items: STR(100) },
+ highlights: { type: 'array', maxItems: 5, items: { type: 'object', additionalProperties: false, required: ['index', 'detail'], properties: { index: { type: 'integer', minimum: 0 }, detail: STR(180) } } },
+ tone: STR(120), catchphrases: { type: 'array', maxItems: 5, items: STR(100) },
+ } },
+ steps: [{ kind: 'llm', template: 'chat.person-memory', operation: 'generate', output: 'json', params: { max_tokens: 450, temperature: 0.2 } }],
+ cache_mode: 'none',
+};
+
+module.exports = { templates, workflows: [...structured, ...CHAT_OWNED, CHAT_PERSON_MEMORY, ...passthrough], KIND_TO_WORKFLOW, ROLES, PASSTHROUGH_INPUT, PASSTHROUGH_OUTPUT };
@@ -14,6 +14,17 @@ const live = require('../server/workflows/live');
@@ -14,6 +14,17 @@ const live = require('../server/workflows/live');
const t = suite('live-templates');
const t = suite('live-templates');
const tpl = (live.templates || []).find((x) => x.key === 'live.moments.pick');
const tpl = (live.templates || []).find((x) => x.key === 'live.moments.pick');
+t.test('Chat person memory has an authorized workflow and an evidence-only prompt', () => {
+ const workflow = live.workflows.find(x => x.key === 'chat.person-memory');
+ const template = live.templates.find(x => x.key === 'chat.person-memory');
+ assert.strictEqual(workflow.namespace, 'chat');
+ assert.strictEqual(workflow.steps[0].params.max_tokens, 450);
+ const prompt = render(template.user_prompt, { level: 'day', period: '2026-11-11', prior_summary: 'A game', sources_text: '[{"message":"hello"}]' });
+ assert.match(prompt, /Sources, with zero-based indexes/);
+ assert.match(template.system_prompt, /Do not infer appearance/);
+ assert.ok(!prompt.includes('{{'));
+});
+
t.test('the prompt renders as Live rendered it', () => {
t.test('the prompt renders as Live rendered it', () => {
assert.ok(tpl, 'the template exists');
assert.ok(tpl, 'the template exists');
const prep = PREPARE['live.moments.pick']({ title: 'Rust night', flavor: 'clip', timeline: [{ t: 95, text: 'streamer jumps up' }], transcript: [{ t: 94, text: 'IT COMPILES' }], sounds: [{ t: 96, label: 'Cheering' }], clipped: [95, 3605], spikes: [96], avoid: [300, 600] });
const prep = PREPARE['live.moments.pick']({ title: 'Rust night', flavor: 'clip', timeline: [{ t: 95, text: 'streamer jumps up' }], transcript: [{ t: 94, text: 'IT COMPILES' }], sounds: [{ t: 96, label: 'Cheering' }], clipped: [95, 3605], spikes: [96], avoid: [300, 600] });
@@ -84,6 +84,7 @@ EXAMPLES['live.chat.profile'] = { subject_kind: 'relay', name: 'KickFan', platfo
@@ -84,6 +84,7 @@ EXAMPLES['live.chat.profile'] = { subject_kind: 'relay', name: 'KickFan', platfo
// Chat runs the same two workflows under its own namespace (plan T3).
// Chat runs the same two workflows under its own namespace (plan T3).
EXAMPLES['chat.global'] = EXAMPLES['live.chat.global'];
EXAMPLES['chat.global'] = EXAMPLES['live.chat.global'];
EXAMPLES['chat.profile'] = EXAMPLES['live.chat.profile'];
EXAMPLES['chat.profile'] = EXAMPLES['live.chat.profile'];
+EXAMPLES['chat.person-memory'] = { level: 'day', period: '2026-11-10', prior_summary: '', sources_text: '[{"message":"hello chat","timestamp":"2026-11-10 12:00:00","stream_id":1}]' };
EXAMPLES['live.person.overview'] = { as_streamer: 'Codes rust late at night.', as_chatter: 'Jokes about croutons.' };
EXAMPLES['live.person.overview'] = { as_streamer: 'Codes rust late at night.', as_chatter: 'Jokes about croutons.' };
EXAMPLES['live.stream.titles'] = { summaries: ['Fixed a borrow checker bug live', 'Built a robot arm and it waved'] };
EXAMPLES['live.stream.titles'] = { summaries: ['Fixed a borrow checker bug live', 'Built a robot arm and it waved'] };
EXAMPLES['live.moments.rank'] = { vods: [{ title: 'Rust night', overview: 'the build finally compiles', views: 30, clips: 2, peak_viewers: 5 }, { title: 'Chill', overview: 'sitting at a desk', views: 3, clips: 0, peak_viewers: 1 }], want: 2 };
EXAMPLES['live.moments.rank'] = { vods: [{ title: 'Rust night', overview: 'the build finally compiles', views: 30, clips: 2, peak_viewers: 5 }, { title: 'Chill', overview: 'sitting at a desk', views: 3, clips: 0, peak_viewers: 1 }], want: 2 };