openvibers/openvibe.ai Commits
AI: chat.member.turn, a room AI member's turn with tools (Chat Completions passthrough in Chat's namespace) (#96)
Changes
@@@ -415,6 +415,10 @@ the service boots without Valkey; with it on, production refuses to boot withoutit should be; when someone names it, Chat sends the recent messages, the room's notes and any shared context a linkedservice gave the room (Food: a household's pantry and plan), and posts the reply. The prompt forbids inventing factsabout the people or their things and claiming actions beyond writing in the chat.++ `chat.member.turn` is the same member with tools: a Chat Completions request (Chat's system message, the transcript,++ the tools Chat runs) passed through like `ai.complete` (the prompt is not kept) on route `chat.member`, so Chat's grant++ and quota apply and a person's own key reaches only their provider; the choices come back with any tool calls, and++ Chat sends the results in its next turn.`overview.source` says which (`local_model`, `paid`, `extractive`), and `gaps` lists what is missing (no localmodel, no speech-to-text, no budget). The output is not cached: callers keep it. Live's grant names@@@ -10,7 +10,15 @@* (an alias of default.chat: the shared provider, falling over to the local model), text out.** Chat renders the transcript itself (one line per message, oldest first), so the template takes plain strings.++ *++ * chat.member.turn the same member with tools (the owner's "modular AI conversation… tool calling"): Chat sends a++ * Chat Completions request (its own system message, the transcript, the tools it can run: search++ * the network, the time, dice, a household's pantry) and gets the choices back, tool calls++ * included; Chat runs the tools and sends the results in the next turn, a few turns at most.++ * The same passthrough as ai.complete (the prompt is not kept), in Chat's namespace and on its++ * route, so Chat's grant and quota apply; with a person's own key it goes to their provider.*/++const core = require('./core');const STR = (n) => ({ type: 'string', maxLength: n });const OBJ = (props, required) => ({ type: 'object', additionalProperties: false, required: required || Object.keys(props), properties: props });@@@ -56,6 +64,14 @@ const workflows = [steps: [{ kind: 'llm', template: 'chat.member.reply', operation: 'chat', output: 'text', params: { max_tokens: 600, temperature: 0.7, timeout_ms: 25000 } }],default_route: 'chat.member', cache_mode: 'none', metadata: { seed: true },},++ {++ key: 'chat.member.turn', name: 'Chat: an AI member\'s turn, with tools', namespace: 'chat',++ description: 'One turn of a room\'s AI member as a Chat Completions request with the tools Chat runs (network search, the time, dice, a household\'s pantry); the choices come back with any tool calls. The prompt is not kept.',++ input_schema: core.workflows.find((w) => w.key === 'ai.complete').input_schema,++ output_schema: core.workflows.find((w) => w.key === 'ai.complete').output_schema,++ metadata: { passthrough: true, seed: true },++ steps: [{ kind: 'complete', route: 'chat.member' }], cache_mode: 'none', default_route: 'chat.member',++ },];module.exports = { templates, workflows };@@@ -53,6 +53,7 @@ const EXAMPLES = {'chat.member.reply': { ai_name: 'Pantry', persona: 'A cheerful cook who keeps it cheap.', room: 'Kim and Lee\'s kitchen', memory: 'Lee is vegetarian.', context: 'Pantry: rice, onions, canned tomatoes, lentils.', context_source: 'OpenVibe.Food household', transcript: 'kim: home late tonight\nlee: same', asked_by: 'kim', message: '@Pantry what can we cook in 20 minutes?' },'food.read_pantry_photo': { image: { data_url: PNG }, kind: 'receipt', foods: ['Large Eggs (1 dozen)', 'Whole Milk (1 gallon)'] },'pics.describe_image': { image: { data_url: PNG }, title: 'The coast' },++ 'chat.member.turn': { messages: [{ role: 'system', content: 'You are Pantry.' }, { role: 'user', content: 'kim: @Pantry roll a die' }], tools: [{ type: 'function', function: { name: 'roll_dice', parameters: { type: 'object', properties: { sides: { type: 'integer' } } } } }] },'tools.describe': { tool: 'img.convert', sources: [{ source_type: 'tools.tool', title: 'img.convert descriptor', content: '{"id":"img.convert","inputs":["file","format"]}' }] },};for (const k of new Set(Object.values(KIND_TO_WORKFLOW).concat('live.complete'))) EXAMPLES[k] = PASS;@@@ -97,7 +98,7 @@ t.test('boot seeds every required workflow key', async () => {h = await boot({ env: { OV_MEDIA_INTERNAL_URL: mediaBase, AI_IMAGE_MODEL: 'gpt-image-1' } });const keys = (await h.registry.listWorkflows()).map(w => w.key);for (const k of ['wiki.generate_space', 'wiki.generate_page', 'blog.draft_post', 'blog.write_digest', 'blog.write_satire', 'news.summarize_story', 'news.compare_perspectives', 'reviews.summarize_entity', 'deals.enrich_deal',-- 'coupons.extract_coupon', 'trade.summarize_market_context', 'codes.generate_docs', 'games.generate_lore', 'moderation.classify', 'tools.describe', 'food.suggest_recipes', 'food.read_pantry_photo', 'pics.describe_image', 'rent.screen_listing', 'chat.member.reply',++ 'coupons.extract_coupon', 'trade.summarize_market_context', 'codes.generate_docs', 'games.generate_lore', 'moderation.classify', 'tools.describe', 'food.suggest_recipes', 'food.read_pantry_photo', 'pics.describe_image', 'rent.screen_listing', 'chat.member.reply', 'chat.member.turn','live.translate', 'live.stream.describe_frame', 'live.stream.summarize', 'live.viewers.reply', 'live.chat.insight', 'live.hero.slogans', 'live.stream.recap', 'network.site_copy']) {assert.ok(keys.includes(k), `missing ${k}`);}
@@ -415,6 +415,10 @@ the service boots without Valkey; with it on, production refuses to boot without
@@ -415,6 +415,10 @@ the service boots without Valkey; with it on, production refuses to boot without
it should be; when someone names it, Chat sends the recent messages, the room's notes and any shared context a linked
it should be; when someone names it, Chat sends the recent messages, the room's notes and any shared context a linked
service gave the room (Food: a household's pantry and plan), and posts the reply. The prompt forbids inventing facts
service gave the room (Food: a household's pantry and plan), and posts the reply. The prompt forbids inventing facts
about the people or their things and claiming actions beyond writing in the chat.
about the people or their things and claiming actions beyond writing in the chat.
+ `chat.member.turn` is the same member with tools: a Chat Completions request (Chat's system message, the transcript,
+ the tools Chat runs) passed through like `ai.complete` (the prompt is not kept) on route `chat.member`, so Chat's grant
+ and quota apply and a person's own key reaches only their provider; the choices come back with any tool calls, and
+ Chat sends the results in its next turn.
`overview.source` says which (`local_model`, `paid`, `extractive`), and `gaps` lists what is missing (no local
`overview.source` says which (`local_model`, `paid`, `extractive`), and `gaps` lists what is missing (no local
model, no speech-to-text, no budget). The output is not cached: callers keep it. Live's grant names
model, no speech-to-text, no budget). The output is not cached: callers keep it. Live's grant names
@@ -10,7 +10,15 @@
@@ -10,7 +10,15 @@
* (an alias of default.chat: the shared provider, falling over to the local model), text out.
* (an alias of default.chat: the shared provider, falling over to the local model), text out.
*
*
* Chat renders the transcript itself (one line per message, oldest first), so the template takes plain strings.
* Chat renders the transcript itself (one line per message, oldest first), so the template takes plain strings.
+ *
+ * chat.member.turn the same member with tools (the owner's "modular AI conversation… tool calling"): Chat sends a
+ * Chat Completions request (its own system message, the transcript, the tools it can run: search
+ * the network, the time, dice, a household's pantry) and gets the choices back, tool calls
+ * included; Chat runs the tools and sends the results in the next turn, a few turns at most.
+ * The same passthrough as ai.complete (the prompt is not kept), in Chat's namespace and on its
+ * route, so Chat's grant and quota apply; with a person's own key it goes to their provider.
*/
*/
+const core = require('./core');
const STR = (n) => ({ type: 'string', maxLength: n });
const STR = (n) => ({ type: 'string', maxLength: n });
const OBJ = (props, required) => ({ type: 'object', additionalProperties: false, required: required || Object.keys(props), properties: props });
const OBJ = (props, required) => ({ type: 'object', additionalProperties: false, required: required || Object.keys(props), properties: props });
@@ -56,6 +64,14 @@ const workflows = [
@@ -56,6 +64,14 @@ const workflows = [
steps: [{ kind: 'llm', template: 'chat.member.reply', operation: 'chat', output: 'text', params: { max_tokens: 600, temperature: 0.7, timeout_ms: 25000 } }],
steps: [{ kind: 'llm', template: 'chat.member.reply', operation: 'chat', output: 'text', params: { max_tokens: 600, temperature: 0.7, timeout_ms: 25000 } }],
default_route: 'chat.member', cache_mode: 'none', metadata: { seed: true },
default_route: 'chat.member', cache_mode: 'none', metadata: { seed: true },
},
},
+ {
+ key: 'chat.member.turn', name: 'Chat: an AI member\'s turn, with tools', namespace: 'chat',
+ description: 'One turn of a room\'s AI member as a Chat Completions request with the tools Chat runs (network search, the time, dice, a household\'s pantry); the choices come back with any tool calls. The prompt is not kept.',
+ input_schema: core.workflows.find((w) => w.key === 'ai.complete').input_schema,
+ output_schema: core.workflows.find((w) => w.key === 'ai.complete').output_schema,
+ metadata: { passthrough: true, seed: true },
+ steps: [{ kind: 'complete', route: 'chat.member' }], cache_mode: 'none', default_route: 'chat.member',
+ },
];
];
module.exports = { templates, workflows };
module.exports = { templates, workflows };
@@ -53,6 +53,7 @@ const EXAMPLES = {
@@ -53,6 +53,7 @@ const EXAMPLES = {
'chat.member.reply': { ai_name: 'Pantry', persona: 'A cheerful cook who keeps it cheap.', room: 'Kim and Lee\'s kitchen', memory: 'Lee is vegetarian.', context: 'Pantry: rice, onions, canned tomatoes, lentils.', context_source: 'OpenVibe.Food household', transcript: 'kim: home late tonight\nlee: same', asked_by: 'kim', message: '@Pantry what can we cook in 20 minutes?' },
'chat.member.reply': { ai_name: 'Pantry', persona: 'A cheerful cook who keeps it cheap.', room: 'Kim and Lee\'s kitchen', memory: 'Lee is vegetarian.', context: 'Pantry: rice, onions, canned tomatoes, lentils.', context_source: 'OpenVibe.Food household', transcript: 'kim: home late tonight\nlee: same', asked_by: 'kim', message: '@Pantry what can we cook in 20 minutes?' },
'food.read_pantry_photo': { image: { data_url: PNG }, kind: 'receipt', foods: ['Large Eggs (1 dozen)', 'Whole Milk (1 gallon)'] },
'food.read_pantry_photo': { image: { data_url: PNG }, kind: 'receipt', foods: ['Large Eggs (1 dozen)', 'Whole Milk (1 gallon)'] },
'pics.describe_image': { image: { data_url: PNG }, title: 'The coast' },
'pics.describe_image': { image: { data_url: PNG }, title: 'The coast' },
+ 'chat.member.turn': { messages: [{ role: 'system', content: 'You are Pantry.' }, { role: 'user', content: 'kim: @Pantry roll a die' }], tools: [{ type: 'function', function: { name: 'roll_dice', parameters: { type: 'object', properties: { sides: { type: 'integer' } } } } }] },
'tools.describe': { tool: 'img.convert', sources: [{ source_type: 'tools.tool', title: 'img.convert descriptor', content: '{"id":"img.convert","inputs":["file","format"]}' }] },
'tools.describe': { tool: 'img.convert', sources: [{ source_type: 'tools.tool', title: 'img.convert descriptor', content: '{"id":"img.convert","inputs":["file","format"]}' }] },
};
};
for (const k of new Set(Object.values(KIND_TO_WORKFLOW).concat('live.complete'))) EXAMPLES[k] = PASS;
for (const k of new Set(Object.values(KIND_TO_WORKFLOW).concat('live.complete'))) EXAMPLES[k] = PASS;
@@ -97,7 +98,7 @@ t.test('boot seeds every required workflow key', async () => {
@@ -97,7 +98,7 @@ t.test('boot seeds every required workflow key', async () => {
h = await boot({ env: { OV_MEDIA_INTERNAL_URL: mediaBase, AI_IMAGE_MODEL: 'gpt-image-1' } });
h = await boot({ env: { OV_MEDIA_INTERNAL_URL: mediaBase, AI_IMAGE_MODEL: 'gpt-image-1' } });
const keys = (await h.registry.listWorkflows()).map(w => w.key);
const keys = (await h.registry.listWorkflows()).map(w => w.key);
for (const k of ['wiki.generate_space', 'wiki.generate_page', 'blog.draft_post', 'blog.write_digest', 'blog.write_satire', 'news.summarize_story', 'news.compare_perspectives', 'reviews.summarize_entity', 'deals.enrich_deal',
for (const k of ['wiki.generate_space', 'wiki.generate_page', 'blog.draft_post', 'blog.write_digest', 'blog.write_satire', 'news.summarize_story', 'news.compare_perspectives', 'reviews.summarize_entity', 'deals.enrich_deal',
- 'coupons.extract_coupon', 'trade.summarize_market_context', 'codes.generate_docs', 'games.generate_lore', 'moderation.classify', 'tools.describe', 'food.suggest_recipes', 'food.read_pantry_photo', 'pics.describe_image', 'rent.screen_listing', 'chat.member.reply',
+ 'coupons.extract_coupon', 'trade.summarize_market_context', 'codes.generate_docs', 'games.generate_lore', 'moderation.classify', 'tools.describe', 'food.suggest_recipes', 'food.read_pantry_photo', 'pics.describe_image', 'rent.screen_listing', 'chat.member.reply', 'chat.member.turn',
'live.translate', 'live.stream.describe_frame', 'live.stream.summarize', 'live.viewers.reply', 'live.chat.insight', 'live.hero.slogans', 'live.stream.recap', 'network.site_copy']) {
'live.translate', 'live.stream.describe_frame', 'live.stream.summarize', 'live.viewers.reply', 'live.chat.insight', 'live.hero.slogans', 'live.stream.recap', 'network.site_copy']) {
assert.ok(keys.includes(k), `missing ${k}`);
assert.ok(keys.includes(k), `missing ${k}`);
}
}