openvibers/openvibe.ai Commits
AI: rent.screen_listing, a rental listing's scam signs for Rent's staff (#87)
Changes
@@@ -368,7 +368,8 @@ the service boots without Valkey; with it on, production refuses to boot without- **Seed product workflows** — `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`. They take numbered++ `codes.generate_docs`, `games.generate_lore`, `moderation.classify`, `food.suggest_recipes`,++ `rent.screen_listing` (a rental listing's scam signs for Rent's staff: `allow` or `review`, quoting the words). They take numbered`sources`, return `citations` (indices the engine bounds to the sources given) and `gaps`, andnever 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. The@@@ -57,6 +57,8 @@ const templates = ['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('rent.screen_listing', 'Rent: screen a listing for scam signs', 'Read one rental listing a person posted and say whether a member of staff should look at it for the signs of a rental scam. The signs: asking for a deposit, rent or any payment before the place can be viewed (payment_before_viewing); asking to pay by wire transfer, gift cards, cryptocurrency or a payment app sent to a stranger (unusual_payment); an owner who is abroad or otherwise cannot show the place in person (owner_unavailable); pressure to decide or pay at once, such as many other applicants or today only (pressure); asking for identity documents, a social security number or bank details before any viewing (personal_data_request); moving the conversation to a private email or messaging app while refusing calls or visits (off_platform_only). A low price alone is not a sign: you cannot know the area. Ordinary rules (no pets, references, a deposit paid at signing, proof of income at application) are not signs. Quote the exact words behind each sign you name. When no sign is clearly present, answer allow with signals none; choose review only when the words show at least one sign.', 'rent.screen',++ 'Listing:\n"""\n{{json listing}}\n"""\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'),];@@@ -171,6 +173,21 @@ const workflows = [}),default_route: 'food.suggest',},++ {++ key: 'rent.screen_listing', name: 'Screen a rental listing for scam signs', namespace: 'rent',++ input_schema: OBJ({++ listing: OBJ({ title: STR(200), kind: STR(40), price: { type: 'number' }, currency: STR(8), period: STR(20), city: STR(120), country: STR(8), description: STR(5000) }, ['title']),++ sources: SOURCES(0),++ }, ['listing']),++ output_schema: OBJ({++ signals: { type: 'array', maxItems: 7, items: { enum: ['payment_before_viewing', 'unusual_payment', 'owner_unavailable', 'pressure', 'personal_data_request', 'off_platform_only', 'none'] } },++ action: { enum: ['allow', 'review'] },++ rationale: STR(1000),++ spans: ARR(OBJ({ quote: STR(500), signal: STR(40) }), 20),++ citations: CITES,++ }),++ default_route: 'rent.screen', cache_mode: 'private',++ },{key: 'moderation.classify', name: 'Classify content for moderation', namespace: 'moderation',input_schema: OBJ({ text: STR(8000), context: STR(1000), sources: SOURCES(0) }, ['text']),@@@ -187,7 +204,7 @@ const workflows = [].map(w => ({cache_mode: 'private',...w,-- steps: [{ kind: 'llm', template: w.key, operation: w.key === 'moderation.classify' ? 'classify' : 'generate', output: 'json', prepare: 'sources', cite: true, params: { max_tokens: 2400, temperature: 0.3 } }],++ steps: [{ kind: 'llm', template: w.key, operation: w.key === 'moderation.classify' || w.key === 'rent.screen_listing' ? 'classify' : 'generate', output: 'json', prepare: 'sources', cite: true, params: { max_tokens: 2400, temperature: 0.3 } }],metadata: { draft: true, seed: true },}));@@@ -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', 'food.suggest'];++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', 'rent.screen'];function sharedProviderRecord(config, env) {const s = config.shared;@@@ -46,6 +46,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' },++ 'rent.screen_listing': { listing: { title: 'Bright room near the river', kind: 'room', price: 650, currency: 'EUR', period: 'month', city: 'Porto', country: 'PT', description: 'I am abroad for work; send the deposit by wire and I will mail the keys.' } },'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"]}' }] },};@@@ -91,11 +92,11 @@ 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', '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',++ 'coupons.extract_coupon', 'trade.summarize_market_context', 'codes.generate_docs', 'games.generate_lore', 'moderation.classify', 'tools.describe', 'food.suggest_recipes', 'rent.screen_listing','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', 'default.image', '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']) {++ for (const k of ['default.chat', 'default.json', 'default.embedding', 'default.image', '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', 'rent.screen']) {assert.ok(await h.registry.getRoute(k), `missing historical route ${k}`);}});
@@ -368,7 +368,8 @@ the service boots without Valkey; with it on, production refuses to boot without
@@ -368,7 +368,8 @@ the service boots without Valkey; with it on, production refuses to boot without
- **Seed product workflows** — `wiki.generate_space`, `wiki.generate_page`, `blog.draft_post`,
- **Seed product workflows** — `wiki.generate_space`, `wiki.generate_page`, `blog.draft_post`,
`news.summarize_story`, `news.compare_perspectives`, `reviews.summarize_entity`,
`news.summarize_story`, `news.compare_perspectives`, `reviews.summarize_entity`,
`deals.enrich_deal`, `coupons.extract_coupon`, `trade.summarize_market_context`,
`deals.enrich_deal`, `coupons.extract_coupon`, `trade.summarize_market_context`,
- `codes.generate_docs`, `games.generate_lore`, `moderation.classify`. They take numbered
+ `codes.generate_docs`, `games.generate_lore`, `moderation.classify`, `food.suggest_recipes`,
+ `rent.screen_listing` (a rental listing's scam signs for Rent's staff: `allow` or `review`, quoting the words). They take numbered
`sources`, return `citations` (indices the engine bounds to the sources given) and `gaps`, and
`sources`, return `citations` (indices the engine bounds to the sources given) and `gaps`, and
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
@@ -57,6 +57,8 @@ const templates = [
@@ -57,6 +57,8 @@ const templates = [
'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',
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'),
'Pantry: {{json pantry}}{{#people}}\nCooking for: {{people}}{{/people}}{{#ask}}\nRequest: {{ask}}{{/ask}}\n\n'),
+ productTemplate('rent.screen_listing', 'Rent: screen a listing for scam signs', 'Read one rental listing a person posted and say whether a member of staff should look at it for the signs of a rental scam. The signs: asking for a deposit, rent or any payment before the place can be viewed (payment_before_viewing); asking to pay by wire transfer, gift cards, cryptocurrency or a payment app sent to a stranger (unusual_payment); an owner who is abroad or otherwise cannot show the place in person (owner_unavailable); pressure to decide or pay at once, such as many other applicants or today only (pressure); asking for identity documents, a social security number or bank details before any viewing (personal_data_request); moving the conversation to a private email or messaging app while refusing calls or visits (off_platform_only). A low price alone is not a sign: you cannot know the area. Ordinary rules (no pets, references, a deposit paid at signing, proof of income at application) are not signs. Quote the exact words behind each sign you name. When no sign is clearly present, answer allow with signals none; choose review only when the words show at least one sign.', 'rent.screen',
+ 'Listing:\n"""\n{{json listing}}\n"""\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'),
];
];
@@ -171,6 +173,21 @@ const workflows = [
@@ -171,6 +173,21 @@ const workflows = [
}),
}),
default_route: 'food.suggest',
default_route: 'food.suggest',
},
},
+ {
+ key: 'rent.screen_listing', name: 'Screen a rental listing for scam signs', namespace: 'rent',
+ input_schema: OBJ({
+ listing: OBJ({ title: STR(200), kind: STR(40), price: { type: 'number' }, currency: STR(8), period: STR(20), city: STR(120), country: STR(8), description: STR(5000) }, ['title']),
+ sources: SOURCES(0),
+ }, ['listing']),
+ output_schema: OBJ({
+ signals: { type: 'array', maxItems: 7, items: { enum: ['payment_before_viewing', 'unusual_payment', 'owner_unavailable', 'pressure', 'personal_data_request', 'off_platform_only', 'none'] } },
+ action: { enum: ['allow', 'review'] },
+ rationale: STR(1000),
+ spans: ARR(OBJ({ quote: STR(500), signal: STR(40) }), 20),
+ citations: CITES,
+ }),
+ default_route: 'rent.screen', cache_mode: 'private',
+ },
{
{
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']),
@@ -187,7 +204,7 @@ const workflows = [
@@ -187,7 +204,7 @@ const workflows = [
].map(w => ({
].map(w => ({
cache_mode: 'private',
cache_mode: 'private',
...w,
...w,
- steps: [{ kind: 'llm', template: w.key, operation: w.key === 'moderation.classify' ? 'classify' : 'generate', output: 'json', prepare: 'sources', cite: true, params: { max_tokens: 2400, temperature: 0.3 } }],
+ steps: [{ kind: 'llm', template: w.key, operation: w.key === 'moderation.classify' || w.key === 'rent.screen_listing' ? 'classify' : 'generate', output: 'json', prepare: 'sources', cite: true, params: { max_tokens: 2400, temperature: 0.3 } }],
metadata: { draft: true, seed: true },
metadata: { draft: true, seed: true },
}));
}));
@@ -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', 'food.suggest'];
+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', 'rent.screen'];
function sharedProviderRecord(config, env) {
function sharedProviderRecord(config, env) {
const s = config.shared;
const s = config.shared;
@@ -46,6 +46,7 @@ const EXAMPLES = {
@@ -46,6 +46,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' },
+ 'rent.screen_listing': { listing: { title: 'Bright room near the river', kind: 'room', price: 650, currency: 'EUR', period: 'month', city: 'Porto', country: 'PT', description: 'I am abroad for work; send the deposit by wire and I will mail the keys.' } },
'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"}' }] },
'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"]}' }] },
};
};
@@ -91,11 +92,11 @@ t.test('boot seeds every required workflow key', async () => {
@@ -91,11 +92,11 @@ t.test('boot seeds every required workflow key', async () => {
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', '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',
+ 'coupons.extract_coupon', 'trade.summarize_market_context', 'codes.generate_docs', 'games.generate_lore', 'moderation.classify', 'tools.describe', 'food.suggest_recipes', 'rent.screen_listing',
'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', 'default.image', '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']) {
+ for (const k of ['default.chat', 'default.json', 'default.embedding', 'default.image', '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', 'rent.screen']) {
assert.ok(await h.registry.getRoute(k), `missing historical route ${k}`);
assert.ok(await h.registry.getRoute(k), `missing historical route ${k}`);
}
}
});
});