openvibers/openvibe.ai Commits

Seed workflow tools.describe (roadmap WS-O task 11): a draft description of one tool from its descriptor (summary, how-to, inputs and outputs, limits, FAQ), every statement cited to the descriptor and unknowns in gaps, never auto-published; route alias tools.describe. The plan's other seeds exist (wiki.generate_page, news.summarize_story, reviews.summarize_entity, deals.enrich_deal, trade.summarize_market_context, and coupons.extract_coupon and codes.generate_docs under those names)

OP OpenVibers committed 26 Sept 2026 · 855eca02c1 · 3 files, +19 −4


Changes

ddiff --git a/server/workflows/products.js b/server/workflows/products.js
iindex dced1ae..f0145c1 100644
---- a/server/workflows/products.js
++++ b/server/workflows/products.js
@@@ -3,7 +3,7 @@
* Seed workflows the publication products consume (plan §15.13). Every one returns a DRAFT /
* EVIDENCE package — never publication truth — with citations that index into the numbered
* input.sources, plus `gaps` for what the sources do not support. The owning product (Wiki,
-- * Blog, News, Reviews, Deals, Coupons, Trade, Codes, Games) decides what to publish.
++ * Blog, News, Reviews, Deals, Coupons, Trade, Codes, Games, Tools) decides what to publish.
*
* Hard rules carried into the prompts and the schemas:
* - no invented facts, prices, ratings, dates, quotes (Reviews has no rating field at all;
@@@ -53,6 +53,8 @@ const templates = [
'Project: {{project}}{{#audience}}\nAudience: {{audience}}{{/audience}}\n\n'),
productTemplate('games.generate_lore', 'Games: generate lore', 'Write lore for the world that stays consistent with the canon sources (cite them where you rely on them), and list any contradiction with canon you had to resolve. Invented fiction is expected here; invented canon citations are not.', 'games.generate_lore',
'World: {{world}}\nRequest: {{prompt}}\n\n'),
++ productTemplate('tools.describe', 'Tools: describe a tool', 'Describe one OpenVibe tool for the people who use it, from its descriptor (the numbered sources: its registry entry, inputs, outputs and limits): a one-line summary, what it does, how to use it step by step, what each input and output means, its limits, and a short FAQ. Cite the descriptor fields each statement rests on. Do not claim formats, sizes or features the descriptor does not list; say what is unknown in gaps.', 'tools.describe',
++ 'Tool: {{tool}}{{#audience}}\nAudience: {{audience}}{{/audience}}\n\n'),
productTemplate('moderation.classify', 'Moderation: classify content', 'Classify the content under OpenVibe policy. Profanity, insults, crude jokes and trash talk are ALLOWED and are not violations. Flag only: credible threats of violence, any sexual content involving minors, doxxing (sharing private personal information), targeted harassment campaigns, spam/scams, sexual content where not allowed, and self-harm encouragement. Quote the exact spans you flag. When unsure, choose review, not block.', 'moderation.classify',
'{{#context}}Context: {{context}}\n\n{{/context}}Content to classify:\n"""\n{{text}}\n"""\n\n'),
];
@@@ -142,6 +144,18 @@ const workflows = [
output_schema: OBJ({ title: STR(200), lore_markdown: STR(20000), entities: ARR(OBJ({ name: STR(120), kind: STR(60), description: STR(1000) }), 30), contradictions: ARR(STR(500), 10), citations: CITES, gaps: GAPS }),
default_route: 'games.generate_lore',
},
++ {
++ key: 'tools.describe', name: 'Describe a tool', namespace: 'tools',
++ input_schema: OBJ({ tool: STR(120), audience: STR(200), sources: SOURCES(1) }, ['tool', 'sources']),
++ output_schema: OBJ({
++ summary: STR(300), description_markdown: STR(6000),
++ how_to: ARR(OBJ({ step: STR(600), citations: CITES }), 15),
++ fields: ARR(OBJ({ name: STR(120), direction: { enum: ['input', 'output'] }, meaning: STR(600), citations: CITES }), 40),
++ limits: ARR(CLAIM, 15), faq: ARR(OBJ({ question: STR(300), answer: STR(1200), citations: CITES }), 12),
++ citations: CITES, gaps: GAPS,
++ }),
++ default_route: 'tools.describe',
++ },
{
key: 'moderation.classify', name: 'Classify content for moderation', namespace: 'moderation',
input_schema: OBJ({ text: STR(8000), context: STR(1000), sources: SOURCES(0) }, ['text']),
ddiff --git a/server/workflows/seed.js b/server/workflows/seed.js
iindex 3825771..6979972 100644
---- a/server/workflows/seed.js
++++ b/server/workflows/seed.js
@@@ -19,7 +19,7 @@ const products = require('./products');
const { resolveSecret } = require('../util');
const ROLE_TIMEOUT = { chat: 20000, vision: 30000, director: 25000, summary: 30000, legacy: 30000 };
--const HISTORICAL_ROUTES = ['wiki.generate', 'blog.draft', 'news.summarize', 'reviews.summarize', 'deals.enrich', 'coupons.extract', 'trade.summarize', 'codes.generate_docs', 'games.generate_lore', 'moderation.classify'];
++const HISTORICAL_ROUTES = ['wiki.generate', 'blog.draft', 'news.summarize', 'reviews.summarize', 'deals.enrich', 'coupons.extract', 'trade.summarize', 'codes.generate_docs', 'games.generate_lore', 'moderation.classify', 'tools.describe'];
function sharedProviderRecord(config, env) {
const s = config.shared;
ddiff --git a/test/workflows.test.js b/test/workflows.test.js
iindex ec9362d..030086f 100644
---- a/test/workflows.test.js
++++ b/test/workflows.test.js
@@@ -43,6 +43,7 @@ const EXAMPLES = {
'codes.generate_docs': { project: 'demo', sources: [{ source_type: 'code.file', title: 'index.js', content: 'module.exports = 1;' }] },
'games.generate_lore': { world: 'Hobo Quest', prompt: 'The origin of the tin-can knights', sources: SRC(1) },
'moderation.classify': { text: 'you absolute potato, gg', context: 'arena chat' },
++ '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;
@@@ -59,11 +60,11 @@ t.test('boot seeds every required workflow key', async () => {
h = await boot({ env: { OV_MEDIA_INTERNAL_URL: mediaBase } });
const keys = h.registry.listWorkflows().map(w => w.key);
for (const k of ['wiki.generate_space', 'wiki.generate_page', 'blog.draft_post', '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',
++ 'coupons.extract_coupon', 'trade.summarize_market_context', 'codes.generate_docs', 'games.generate_lore', 'moderation.classify', 'tools.describe',
'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}`);
}
-- for (const k of ['default.chat', 'default.json', 'default.embedding', 'wiki.generate', 'blog.draft', 'news.summarize', 'reviews.summarize', 'deals.enrich', 'coupons.extract', 'trade.summarize', 'codes.generate_docs', 'games.generate_lore', 'moderation.classify']) {
++ for (const k of ['default.chat', 'default.json', 'default.embedding', 'wiki.generate', 'blog.draft', 'news.summarize', 'reviews.summarize', 'deals.enrich', 'coupons.extract', 'trade.summarize', 'codes.generate_docs', 'games.generate_lore', 'tools.describe', 'moderation.classify']) {
assert.ok(h.registry.getRoute(k), `missing historical route ${k}`);
}
});