openvibers/openvibe.ai Commits

AI: blog.write_digest (a desk's cited digest) and blog.write_satire (labelled fiction with its own rules) (#93)

OP OpenVibers committed 29 days ago · 941f615a42 · 3 files, +37 −1

Changes

ddiff --git a/README.md b/README.md
iindex 0e73e5b..8a76349 100644
---- a/README.md
++++ b/README.md
@@@ -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
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`.
++ `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`
ddiff --git a/server/workflows/products.js b/server/workflows/products.js
iindex 61567f8..46a4e0d 100644
---- a/server/workflows/products.js
++++ b/server/workflows/products.js
@@@ -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 };
ddiff --git a/test/workflows.test.js b/test/workflows.test.js
iindex 37f77ab..9bd0232 100644
---- a/test/workflows.test.js
++++ b/test/workflows.test.js
@@@ -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}`);