openvibers/openvibe.ai Commits

AI: chat.member.turn, a room AI member's turn with tools (Chat Completions passthrough in Chat's namespace) (#96)

OP OpenVibers committed 28 days ago · 737945ab73 · 3 files, +22 −1

Changes

ddiff --git a/README.md b/README.md
iindex 8a76349..b985ac5 100644
---- a/README.md
++++ b/README.md
@@@ -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
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.
++ `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
model, no speech-to-text, no budget). The output is not cached: callers keep it. Live's grant names
ddiff --git a/server/workflows/chat.js b/server/workflows/chat.js
iindex 494bc02..ab58d9c 100644
---- a/server/workflows/chat.js
++++ b/server/workflows/chat.js
@@@ -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 };
ddiff --git a/test/workflows.test.js b/test/workflows.test.js
iindex 9d8f82e..a75084b 100644
---- a/test/workflows.test.js
++++ b/test/workflows.test.js
@@@ -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}`);
}