openvibers/openvibe.ai Commits
AI: blog.write_digest (a desk's cited digest) and blog.write_satire (labelled fiction with its own rules) (#93)
Changes
@@@ -383,6 +383,11 @@ the service boots without Valkey; with it on, production refuses to boot withoutnever invent facts: Reviews has no rating field, a Deals price is null unless cited, a Couponsexpiry stays unknown unless stated, Trade carries its informational-only disclaimer. Thehistorical route keys (`wiki.generate`, `news.summarize`, …) are explicit aliases of `default.json`.++ `blog.write_digest` writes an OpenVibe.Blog desk's digest from its sources' items, every section cited.++- **Satire** — `blog.write_satire` (Blog's "Is This Real?" desk) is invented on purpose and takes no sources: absurd++ mock-explainers that never name or quote a real person or put words in a real organisation's mouth, never joke about++ tragedies, minors or groups of people, and never give steps for anything dangerous. Blog labels each post as satire++ written by AI.- **Live's features** — `live.translate`, `live.paste.describe_image`, `live.paste.summarize_text`,`live.stream.describe_frame`, `live.stream.summarize`, `live.streamer.overview`,`live.media.overview`, `live.stream.recap`, `live.media.transcribe`, `network.site_copy`@@@ -37,6 +37,8 @@ const templates = ['Page title: {{title}}{{#space}}\nSpace: {{space}}{{/space}}{{#outline}}\nRequested outline: {{json outline}}{{/outline}}\n\n'),productTemplate('blog.draft_post', 'Blog: draft a post', 'Draft a blog post in Markdown for the brief. Claims of fact must cite sources; opinion and framing need none. If there are no sources, write only what needs no facts and list the facts you would need in gaps.', 'blog.draft','Topic: {{topic}}{{#brief}}\nBrief: {{brief}}{{/brief}}{{#tone}}\nTone: {{tone}}{{/tone}}{{#audience}}\nAudience: {{audience}}{{/audience}}\n\n'),++ productTemplate('blog.write_digest', 'Blog: write a desk digest', 'Write one digest post for an OpenVibe.Blog desk from the numbered sources (the items its sources published in the period). Group related items into a few sections, most important first, and say plainly what each item is and why a reader of this desk would care, as the sources state it. Every sentence of fact cites the sources it rests on (each section\'s `citations`); connecting words and section headings need none. Neutral and readable: no opinion stated as fact, no hype, no advice, no predictions. Name prices, dates and numbers only as the sources give them. Skip items that say nothing a reader could use, and list what you skipped or could not tell in gaps. title: what the period was about, not "Digest". dek: one sentence. body of each section: plain sentences, Markdown links not needed (the page links the sources).', 'blog.draft',++ 'Desk: {{desk}}{{#about}}\nThe desk covers: {{about}}{{/about}}\nPeriod: {{period}}\n\n'),productTemplate('news.summarize_story', 'News: summarize a story', 'Write a neutral, factual summary of the story the sources report: a headline, a summary, key points and a timeline, all cited. Attribute contested claims to their source. The tone, when given, changes the length and the explaining, never the facts or the neutrality: brief is two or three sentences and at most three key points; explained also says, from the sources, what the subject is and why it matters, for someone new to it.', 'news.summarize','{{#topic}}Topic: {{topic}}\n\n{{/topic}}{{#tone}}Tone: {{tone}}\n\n{{/tone}}'),productTemplate('news.compare_perspectives', 'News: compare perspectives', 'Group the sources into distinct perspectives on the question, summarize each fairly in its own terms, and list where they agree and disagree. Do not pick a winner.', 'news.summarize',@@@ -82,6 +84,13 @@ const workflows = [output_schema: OBJ({ title: STR(200), dek: STR(400), body_markdown: STR(40000), tags: ARR(STR(40), 10), citations: CITES, gaps: GAPS }),default_route: 'blog.draft',},++ {++ // OpenVibe.Blog's AI desks (server/desks.js): a digest of what a desk's sources published, each section cited.++ key: 'blog.write_digest', name: 'Write a desk digest', namespace: 'blog',++ input_schema: OBJ({ desk: STR(120), about: STR(400), period: STR(120), sources: SOURCES(2) }, ['desk', 'period', 'sources']),++ output_schema: OBJ({ title: STR(200), dek: STR(400), sections: ARR(OBJ({ heading: STR(160), body: STR(4000), citations: CITES }), 12), citations: CITES, gaps: GAPS }),++ default_route: 'blog.draft',++ },{key: 'news.summarize_story', name: 'Summarize a news story', namespace: 'news',// tone (OpenVibe.News' reader control): brief (two or three sentences), neutral (the default), explained (for@@@ -234,4 +243,24 @@ workflows.push({metadata: { seed: true },});++// OpenVibe.Blog's satire desk ("Is This Real?"): invented on purpose, so it is not a sourced evidence package and has its++// own rules. The page labels every post as satire written by AI; the rules keep the jokes away from real people.++templates.push({++ key: 'blog.write_satire', name: 'Blog: write a satire post',++ description: 'An absurd, plainly fictional mock-explainer for OpenVibe.Blog\'s satire desk.',++ input_schema: { type: 'object' }, output_schema: { type: 'object' },++ system_prompt: 'You write for "Is This Real?", OpenVibe.Blog\'s satire desk: absurd, deadpan mock-explainers that treat a ridiculous question as if it were a serious public-health or consumer issue, in the style of an earnest internet explainer that has completely lost the plot. Escalate the absurdity section by section; invented studies, invented institutes and invented experts with obviously silly names are the joke. Rules you never break: the piece must be plainly fiction to any reader; never name, quote or describe a real living person, and never put words in the mouth of a real company, brand, government body or organisation; no jokes about real tragedies, victims, illness, minors, or any group of people by race, religion, gender, sexuality or disability; never give steps, amounts or methods for anything dangerous (drugs, weapons, self-harm, dangerous stunts), even as a joke, and when the topic brushes against one, the punchline is that nobody should try it; no slurs. Profanity is allowed but use it sparingly. title: the question, as a headline. dek: one deadpan sentence. body_markdown: 350 to 700 words in Markdown with two to four ## sections; no title heading. tags: up to five lower-case words. Output only JSON matching the schema.',++ user_prompt: 'Question: {{topic}}{{#angle}}\nAngle: {{angle}}{{/angle}}',++ default_route: 'blog.draft', owner: 'blog', metadata: { json_name: 'blog_write_satire' },++});++workflows.push({++ key: 'blog.write_satire', name: 'Write a satire post', namespace: 'blog',++ input_schema: OBJ({ topic: STR(300), angle: STR(300) }, ['topic']),++ output_schema: OBJ({ title: STR(200), dek: STR(300), body_markdown: STR(12000), tags: ARR(STR(40), 5) }),++ steps: [{ kind: 'llm', template: 'blog.write_satire', operation: 'generate', output: 'json', params: { max_tokens: 2400, temperature: 0.9 } }],++ default_route: 'blog.draft',++ cache_mode: 'private',++ metadata: { seed: true },++});++module.exports = { templates, workflows };@@@ -37,6 +37,8 @@ const EXAMPLES = {'wiki.generate_space': { topic: 'SQLite', sources: SRC(2) },'wiki.generate_page': { title: 'WAL mode', sources: SRC(3) },'blog.draft_post': { topic: 'Why we self-host', sources: SRC(1) },++ 'blog.write_digest': { desk: 'News digest', about: 'Government and space agency announcements', period: '14 October 2026', sources: SRC(3) },++ 'blog.write_satire': { topic: 'Is it safe to let your air fryer vote?', angle: 'a consumer safety explainer' },'news.summarize_story': { topic: 'Example', sources: SRC(3) },'news.compare_perspectives': { topic: 'Example', sources: SRC(2) },'reviews.summarize_entity': { entity: { name: 'Widget', type: 'product' }, sources: SRC(2) },@@@ -93,7 +95,7 @@ t.test('boot seeds every required workflow key', async () => {// AI_IMAGE_MODEL seeds default.image, so ai.image runs end to end on the stub's PNG like every other workflow.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', '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', 'rent.screen_listing', 'chat.member.reply','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}`);
@@ -383,6 +383,11 @@ the service boots without Valkey; with it on, production refuses to boot without
@@ -383,6 +383,11 @@ the service boots without Valkey; with it on, production refuses to boot without
never invent facts: Reviews has no rating field, a Deals price is null unless cited, a Coupons
never invent facts: Reviews has no rating field, a Deals price is null unless cited, a Coupons
expiry stays unknown unless stated, Trade carries its informational-only disclaimer. The
expiry stays unknown unless stated, Trade carries its informational-only disclaimer. The
historical route keys (`wiki.generate`, `news.summarize`, …) are explicit aliases of `default.json`.
historical route keys (`wiki.generate`, `news.summarize`, …) are explicit aliases of `default.json`.
+ `blog.write_digest` writes an OpenVibe.Blog desk's digest from its sources' items, every section cited.
+- **Satire** — `blog.write_satire` (Blog's "Is This Real?" desk) is invented on purpose and takes no sources: absurd
+ mock-explainers that never name or quote a real person or put words in a real organisation's mouth, never joke about
+ tragedies, minors or groups of people, and never give steps for anything dangerous. Blog labels each post as satire
+ written by AI.
- **Live's features** — `live.translate`, `live.paste.describe_image`, `live.paste.summarize_text`,
- **Live's features** — `live.translate`, `live.paste.describe_image`, `live.paste.summarize_text`,
`live.stream.describe_frame`, `live.stream.summarize`, `live.streamer.overview`,
`live.stream.describe_frame`, `live.stream.summarize`, `live.streamer.overview`,
`live.media.overview`, `live.stream.recap`, `live.media.transcribe`, `network.site_copy`
`live.media.overview`, `live.stream.recap`, `live.media.transcribe`, `network.site_copy`
@@ -37,6 +37,8 @@ const templates = [
@@ -37,6 +37,8 @@ const templates = [
'Page title: {{title}}{{#space}}\nSpace: {{space}}{{/space}}{{#outline}}\nRequested outline: {{json outline}}{{/outline}}\n\n'),
'Page title: {{title}}{{#space}}\nSpace: {{space}}{{/space}}{{#outline}}\nRequested outline: {{json outline}}{{/outline}}\n\n'),
productTemplate('blog.draft_post', 'Blog: draft a post', 'Draft a blog post in Markdown for the brief. Claims of fact must cite sources; opinion and framing need none. If there are no sources, write only what needs no facts and list the facts you would need in gaps.', 'blog.draft',
productTemplate('blog.draft_post', 'Blog: draft a post', 'Draft a blog post in Markdown for the brief. Claims of fact must cite sources; opinion and framing need none. If there are no sources, write only what needs no facts and list the facts you would need in gaps.', 'blog.draft',
'Topic: {{topic}}{{#brief}}\nBrief: {{brief}}{{/brief}}{{#tone}}\nTone: {{tone}}{{/tone}}{{#audience}}\nAudience: {{audience}}{{/audience}}\n\n'),
'Topic: {{topic}}{{#brief}}\nBrief: {{brief}}{{/brief}}{{#tone}}\nTone: {{tone}}{{/tone}}{{#audience}}\nAudience: {{audience}}{{/audience}}\n\n'),
+ productTemplate('blog.write_digest', 'Blog: write a desk digest', 'Write one digest post for an OpenVibe.Blog desk from the numbered sources (the items its sources published in the period). Group related items into a few sections, most important first, and say plainly what each item is and why a reader of this desk would care, as the sources state it. Every sentence of fact cites the sources it rests on (each section\'s `citations`); connecting words and section headings need none. Neutral and readable: no opinion stated as fact, no hype, no advice, no predictions. Name prices, dates and numbers only as the sources give them. Skip items that say nothing a reader could use, and list what you skipped or could not tell in gaps. title: what the period was about, not "Digest". dek: one sentence. body of each section: plain sentences, Markdown links not needed (the page links the sources).', 'blog.draft',
+ 'Desk: {{desk}}{{#about}}\nThe desk covers: {{about}}{{/about}}\nPeriod: {{period}}\n\n'),
productTemplate('news.summarize_story', 'News: summarize a story', 'Write a neutral, factual summary of the story the sources report: a headline, a summary, key points and a timeline, all cited. Attribute contested claims to their source. The tone, when given, changes the length and the explaining, never the facts or the neutrality: brief is two or three sentences and at most three key points; explained also says, from the sources, what the subject is and why it matters, for someone new to it.', 'news.summarize',
productTemplate('news.summarize_story', 'News: summarize a story', 'Write a neutral, factual summary of the story the sources report: a headline, a summary, key points and a timeline, all cited. Attribute contested claims to their source. The tone, when given, changes the length and the explaining, never the facts or the neutrality: brief is two or three sentences and at most three key points; explained also says, from the sources, what the subject is and why it matters, for someone new to it.', 'news.summarize',
'{{#topic}}Topic: {{topic}}\n\n{{/topic}}{{#tone}}Tone: {{tone}}\n\n{{/tone}}'),
'{{#topic}}Topic: {{topic}}\n\n{{/topic}}{{#tone}}Tone: {{tone}}\n\n{{/tone}}'),
productTemplate('news.compare_perspectives', 'News: compare perspectives', 'Group the sources into distinct perspectives on the question, summarize each fairly in its own terms, and list where they agree and disagree. Do not pick a winner.', 'news.summarize',
productTemplate('news.compare_perspectives', 'News: compare perspectives', 'Group the sources into distinct perspectives on the question, summarize each fairly in its own terms, and list where they agree and disagree. Do not pick a winner.', 'news.summarize',
@@ -82,6 +84,13 @@ const workflows = [
@@ -82,6 +84,13 @@ const workflows = [
output_schema: OBJ({ title: STR(200), dek: STR(400), body_markdown: STR(40000), tags: ARR(STR(40), 10), citations: CITES, gaps: GAPS }),
output_schema: OBJ({ title: STR(200), dek: STR(400), body_markdown: STR(40000), tags: ARR(STR(40), 10), citations: CITES, gaps: GAPS }),
default_route: 'blog.draft',
default_route: 'blog.draft',
},
},
+ {
+ // OpenVibe.Blog's AI desks (server/desks.js): a digest of what a desk's sources published, each section cited.
+ key: 'blog.write_digest', name: 'Write a desk digest', namespace: 'blog',
+ input_schema: OBJ({ desk: STR(120), about: STR(400), period: STR(120), sources: SOURCES(2) }, ['desk', 'period', 'sources']),
+ output_schema: OBJ({ title: STR(200), dek: STR(400), sections: ARR(OBJ({ heading: STR(160), body: STR(4000), citations: CITES }), 12), citations: CITES, gaps: GAPS }),
+ default_route: 'blog.draft',
+ },
{
{
key: 'news.summarize_story', name: 'Summarize a news story', namespace: 'news',
key: 'news.summarize_story', name: 'Summarize a news story', namespace: 'news',
// tone (OpenVibe.News' reader control): brief (two or three sentences), neutral (the default), explained (for
// tone (OpenVibe.News' reader control): brief (two or three sentences), neutral (the default), explained (for
@@ -234,4 +243,24 @@ workflows.push({
@@ -234,4 +243,24 @@ workflows.push({
metadata: { seed: true },
metadata: { seed: true },
});
});
+// OpenVibe.Blog's satire desk ("Is This Real?"): invented on purpose, so it is not a sourced evidence package and has its
+// own rules. The page labels every post as satire written by AI; the rules keep the jokes away from real people.
+templates.push({
+ key: 'blog.write_satire', name: 'Blog: write a satire post',
+ description: 'An absurd, plainly fictional mock-explainer for OpenVibe.Blog\'s satire desk.',
+ input_schema: { type: 'object' }, output_schema: { type: 'object' },
+ system_prompt: 'You write for "Is This Real?", OpenVibe.Blog\'s satire desk: absurd, deadpan mock-explainers that treat a ridiculous question as if it were a serious public-health or consumer issue, in the style of an earnest internet explainer that has completely lost the plot. Escalate the absurdity section by section; invented studies, invented institutes and invented experts with obviously silly names are the joke. Rules you never break: the piece must be plainly fiction to any reader; never name, quote or describe a real living person, and never put words in the mouth of a real company, brand, government body or organisation; no jokes about real tragedies, victims, illness, minors, or any group of people by race, religion, gender, sexuality or disability; never give steps, amounts or methods for anything dangerous (drugs, weapons, self-harm, dangerous stunts), even as a joke, and when the topic brushes against one, the punchline is that nobody should try it; no slurs. Profanity is allowed but use it sparingly. title: the question, as a headline. dek: one deadpan sentence. body_markdown: 350 to 700 words in Markdown with two to four ## sections; no title heading. tags: up to five lower-case words. Output only JSON matching the schema.',
+ user_prompt: 'Question: {{topic}}{{#angle}}\nAngle: {{angle}}{{/angle}}',
+ default_route: 'blog.draft', owner: 'blog', metadata: { json_name: 'blog_write_satire' },
+});
+workflows.push({
+ key: 'blog.write_satire', name: 'Write a satire post', namespace: 'blog',
+ input_schema: OBJ({ topic: STR(300), angle: STR(300) }, ['topic']),
+ output_schema: OBJ({ title: STR(200), dek: STR(300), body_markdown: STR(12000), tags: ARR(STR(40), 5) }),
+ steps: [{ kind: 'llm', template: 'blog.write_satire', operation: 'generate', output: 'json', params: { max_tokens: 2400, temperature: 0.9 } }],
+ default_route: 'blog.draft',
+ cache_mode: 'private',
+ metadata: { seed: true },
+});
+
module.exports = { templates, workflows };
module.exports = { templates, workflows };
@@ -37,6 +37,8 @@ const EXAMPLES = {
@@ -37,6 +37,8 @@ const EXAMPLES = {
'wiki.generate_space': { topic: 'SQLite', sources: SRC(2) },
'wiki.generate_space': { topic: 'SQLite', sources: SRC(2) },
'wiki.generate_page': { title: 'WAL mode', sources: SRC(3) },
'wiki.generate_page': { title: 'WAL mode', sources: SRC(3) },
'blog.draft_post': { topic: 'Why we self-host', sources: SRC(1) },
'blog.draft_post': { topic: 'Why we self-host', sources: SRC(1) },
+ 'blog.write_digest': { desk: 'News digest', about: 'Government and space agency announcements', period: '14 October 2026', sources: SRC(3) },
+ 'blog.write_satire': { topic: 'Is it safe to let your air fryer vote?', angle: 'a consumer safety explainer' },
'news.summarize_story': { topic: 'Example', sources: SRC(3) },
'news.summarize_story': { topic: 'Example', sources: SRC(3) },
'news.compare_perspectives': { topic: 'Example', sources: SRC(2) },
'news.compare_perspectives': { topic: 'Example', sources: SRC(2) },
'reviews.summarize_entity': { entity: { name: 'Widget', type: 'product' }, sources: SRC(2) },
'reviews.summarize_entity': { entity: { name: 'Widget', type: 'product' }, sources: SRC(2) },
@@ -93,7 +95,7 @@ t.test('boot seeds every required workflow key', async () => {
@@ -93,7 +95,7 @@ t.test('boot seeds every required workflow key', async () => {
// AI_IMAGE_MODEL seeds default.image, so ai.image runs end to end on the stub's PNG like every other workflow.
// AI_IMAGE_MODEL seeds default.image, so ai.image runs end to end on the stub's PNG like every other workflow.
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', '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', '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', 'rent.screen_listing', 'chat.member.reply',
'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}`);