openvibers/openvibe.ai Commits
AI: food.suggest_recipes workflow (recipes from a pantry, ingredients cited to Food's food list) (#57)
Changes
@@@ -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, Tools) decides what to publish.++ * Blog, News, Reviews, Deals, Coupons, Trade, Codes, Games, Tools, Food) 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;@@@ -55,6 +55,8 @@ const templates = ['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('food.suggest_recipes', 'Food: suggest recipes', 'Suggest up to three simple, low-cost recipes the person can cook, built first from their pantry. An ingredient from the pantry has from_pantry true and needs no citation. Every other ingredient must be one of the foods in the numbered sources (OpenVibe.Food\'s food list) and cite it; if a recipe needs something the sources do not list, leave that recipe out and name the missing food in gaps. State amounts in everyday units. Never state prices, calories, nutrition or health claims: the app works out costs from its own data. Keep steps short and safe (cook meat, poultry and eggs through).', 'food.suggest',++ 'Pantry: {{json pantry}}{{#people}}\nCooking for: {{people}}{{/people}}{{#ask}}\nRequest: {{ask}}{{/ask}}\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'),];@@@ -156,6 +158,19 @@ const workflows = [}),default_route: 'tools.describe',},++ {++ key: 'food.suggest_recipes', name: 'Suggest recipes from a pantry', namespace: 'food',++ input_schema: OBJ({ pantry: ARR(STR(120), 60), people: { type: 'integer', minimum: 1, maximum: 12 }, ask: STR(500), sources: SOURCES(1) }, ['pantry', 'sources']),++ output_schema: OBJ({++ recipes: ARR(OBJ({++ title: STR(160), serves: { type: 'integer', minimum: 1, maximum: 12 }, minutes: { type: 'integer', minimum: 1, maximum: 600 },++ ingredients: ARR(OBJ({ name: STR(120), amount: STR(80), from_pantry: { type: 'boolean' }, citations: CITES }), 25),++ steps: ARR(STR(600), 20), notes: STR(600),++ }), 3),++ citations: CITES, gaps: GAPS,++ }),++ default_route: 'food.suggest',++ },{key: 'moderation.classify', name: 'Classify content for moderation', namespace: 'moderation',input_schema: OBJ({ text: STR(8000), context: STR(1000), sources: SOURCES(0) }, ['text']),@@@ -21,7 +21,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', 'tools.describe'];++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', 'food.suggest'];function sharedProviderRecord(config, env) {const s = config.shared;@@@ -44,6 +44,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' },++ 'food.suggest_recipes': { pantry: ['rice', 'onions', 'canned tomatoes'], people: 2, sources: [{ source_type: 'food.food', source_id: 'lentils', title: 'Lentils, dried', content: '{"id":"lentils","unit":"kg"}' }] },'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;@@@ -87,11 +88,11 @@ t.test('boot seeds every required workflow key', async () => {h = await boot({ env: { OV_MEDIA_INTERNAL_URL: mediaBase } });const keys = (await 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', 'tools.describe',++ 'coupons.extract_coupon', 'trade.summarize_market_context', 'codes.generate_docs', 'games.generate_lore', 'moderation.classify', 'tools.describe', 'food.suggest_recipes','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', 'tools.describe', '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', 'food.suggest']) {assert.ok(await h.registry.getRoute(k), `missing historical route ${k}`);}});
@@ -3,7 +3,7 @@
@@ -3,7 +3,7 @@
* Seed workflows the publication products consume (plan §15.13). Every one returns a DRAFT /
* 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
* 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,
* input.sources, plus `gaps` for what the sources do not support. The owning product (Wiki,
- * Blog, News, Reviews, Deals, Coupons, Trade, Codes, Games, Tools) decides what to publish.
+ * Blog, News, Reviews, Deals, Coupons, Trade, Codes, Games, Tools, Food) decides what to publish.
*
*
* Hard rules carried into the prompts and the schemas:
* Hard rules carried into the prompts and the schemas:
* - no invented facts, prices, ratings, dates, quotes (Reviews has no rating field at all;
* - no invented facts, prices, ratings, dates, quotes (Reviews has no rating field at all;
@@ -55,6 +55,8 @@ const templates = [
@@ -55,6 +55,8 @@ const templates = [
'World: {{world}}\nRequest: {{prompt}}\n\n'),
'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',
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'),
'Tool: {{tool}}{{#audience}}\nAudience: {{audience}}{{/audience}}\n\n'),
+ productTemplate('food.suggest_recipes', 'Food: suggest recipes', 'Suggest up to three simple, low-cost recipes the person can cook, built first from their pantry. An ingredient from the pantry has from_pantry true and needs no citation. Every other ingredient must be one of the foods in the numbered sources (OpenVibe.Food\'s food list) and cite it; if a recipe needs something the sources do not list, leave that recipe out and name the missing food in gaps. State amounts in everyday units. Never state prices, calories, nutrition or health claims: the app works out costs from its own data. Keep steps short and safe (cook meat, poultry and eggs through).', 'food.suggest',
+ 'Pantry: {{json pantry}}{{#people}}\nCooking for: {{people}}{{/people}}{{#ask}}\nRequest: {{ask}}{{/ask}}\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',
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'),
'{{#context}}Context: {{context}}\n\n{{/context}}Content to classify:\n"""\n{{text}}\n"""\n\n'),
];
];
@@ -156,6 +158,19 @@ const workflows = [
@@ -156,6 +158,19 @@ const workflows = [
}),
}),
default_route: 'tools.describe',
default_route: 'tools.describe',
},
},
+ {
+ key: 'food.suggest_recipes', name: 'Suggest recipes from a pantry', namespace: 'food',
+ input_schema: OBJ({ pantry: ARR(STR(120), 60), people: { type: 'integer', minimum: 1, maximum: 12 }, ask: STR(500), sources: SOURCES(1) }, ['pantry', 'sources']),
+ output_schema: OBJ({
+ recipes: ARR(OBJ({
+ title: STR(160), serves: { type: 'integer', minimum: 1, maximum: 12 }, minutes: { type: 'integer', minimum: 1, maximum: 600 },
+ ingredients: ARR(OBJ({ name: STR(120), amount: STR(80), from_pantry: { type: 'boolean' }, citations: CITES }), 25),
+ steps: ARR(STR(600), 20), notes: STR(600),
+ }), 3),
+ citations: CITES, gaps: GAPS,
+ }),
+ default_route: 'food.suggest',
+ },
{
{
key: 'moderation.classify', name: 'Classify content for moderation', namespace: 'moderation',
key: 'moderation.classify', name: 'Classify content for moderation', namespace: 'moderation',
input_schema: OBJ({ text: STR(8000), context: STR(1000), sources: SOURCES(0) }, ['text']),
input_schema: OBJ({ text: STR(8000), context: STR(1000), sources: SOURCES(0) }, ['text']),
@@ -21,7 +21,7 @@ const products = require('./products');
@@ -21,7 +21,7 @@ const products = require('./products');
const { resolveSecret } = require('../util');
const { resolveSecret } = require('../util');
const ROLE_TIMEOUT = { chat: 20000, vision: 30000, director: 25000, summary: 30000, legacy: 30000 };
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', 'tools.describe'];
+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', 'food.suggest'];
function sharedProviderRecord(config, env) {
function sharedProviderRecord(config, env) {
const s = config.shared;
const s = config.shared;
@@ -44,6 +44,7 @@ const EXAMPLES = {
@@ -44,6 +44,7 @@ const EXAMPLES = {
'codes.generate_docs': { project: 'demo', sources: [{ source_type: 'code.file', title: 'index.js', content: 'module.exports = 1;' }] },
'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) },
'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' },
'moderation.classify': { text: 'you absolute potato, gg', context: 'arena chat' },
+ 'food.suggest_recipes': { pantry: ['rice', 'onions', 'canned tomatoes'], people: 2, sources: [{ source_type: 'food.food', source_id: 'lentils', title: 'Lentils, dried', content: '{"id":"lentils","unit":"kg"}' }] },
'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;
@@ -87,11 +88,11 @@ t.test('boot seeds every required workflow key', async () => {
@@ -87,11 +88,11 @@ t.test('boot seeds every required workflow key', async () => {
h = await boot({ env: { OV_MEDIA_INTERNAL_URL: mediaBase } });
h = await boot({ env: { OV_MEDIA_INTERNAL_URL: mediaBase } });
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', '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', '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',
+ 'coupons.extract_coupon', 'trade.summarize_market_context', 'codes.generate_docs', 'games.generate_lore', 'moderation.classify', 'tools.describe', 'food.suggest_recipes',
'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}`);
}
}
- 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']) {
+ 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', 'food.suggest']) {
assert.ok(await h.registry.getRoute(k), `missing historical route ${k}`);
assert.ok(await h.registry.getRoute(k), `missing historical route ${k}`);
}
}
});
});