Files
2026-07-13 13:34:48 +08:00

248 lines
8.0 KiB
TypeScript

import { describe, it, expect, beforeEach, afterEach, afterAll } from 'vitest'
import { BotpressDocumentation, getClient, getZai, metadata } from './utils'
import { TableAdapter } from '../src/adapters/botpress-table'
import { check } from '@botpress/vai'
const getValues = <T extends Record<string, { value: boolean }>>(records: T) =>
Object.entries(records).reduce((acc, [key, value]) => {
acc[key] = value.value
return acc
}, {}) as Record<keyof T, boolean>
describe('zai.label', { timeout: 60_000 }, () => {
const zai = getZai()
it('simple labels on small text', async () => {
const { output: labels } = await zai
.label(
{
name: 'John',
story: ['John donated to charity last month.', 'John is loved by his community.'],
criminal_record: 'John has no criminal record.',
},
{
is_human: 'is the person a human?',
good_person: 'is the person a good person?',
bad_person: 'is the person a bad person?',
is_criminal: 'is the person a criminal?',
}
)
.result()
expect(getValues(labels)).toMatchInlineSnapshot(`
{
"bad_person": false,
"good_person": true,
"is_criminal": false,
"is_human": true,
}
`)
})
it('simple labels with example', async () => {
const labels = {
is_human: 'is the person a human?',
good_person: 'is the person a good person?',
bad_person: 'is the person a bad person?',
canadian: 'is the person canadian?',
is_french: 'is the person french?',
}
const initial = await zai.label(`Sylvain Perron has no criminal record.`, labels)
expect(initial.canadian).toBe(false)
expect(initial.is_french).toBe(false)
expect(initial.bad_person).toBe(false)
expect(initial.is_human).toBe(true)
const second = await zai.label(`Sylvain Perron has no criminal record.`, labels, {
examples: [
{
input: 'Sylvain Pellerin has no criminal record.',
labels: {
is_french: {
label: 'ABSOLUTELY_YES',
explanation: 'Important: Sylvain Pellerin is a common French name.',
},
canadian: {
label: 'ABSOLUTELY_YES',
explanation: 'Important: We assume all person named Sylvain are Canadian (business rule).',
},
},
},
{
input: 'Sylvain Bouchard is a criminal.',
labels: {
bad_person: {
label: 'PROBABLY_YES',
explanation: 'Important: Sylvain Bouchard is a criminal, so probably a bad person.',
},
is_french: {
label: 'ABSOLUTELY_YES',
explanation: 'Important: Sylvain is a common French name.',
},
canadian: {
label: 'ABSOLUTELY_YES',
explanation: 'Important: We assume all person named Sylvain are Canadian (business rule).',
},
},
},
],
})
expect(second.canadian).toBe(true)
expect(second.is_french).toBe(true)
expect(second.is_human).toBe(true)
expect(second.bad_person).toBe(false)
})
it('label a huge text', async () => {
const labels = await zai.label(BotpressDocumentation, {
is_about_animals: 'is the text about animals?',
contains_lua_code: 'does the text contain Lua code?',
contains_python_code: 'does the text contain Python code?',
contains_js_code: 'does the text contain JavaScript code?',
is_botpress: 'is the text about Botpress?',
is_rasa: 'is the text about Rasa?',
has_flows: 'does the text mention flows?',
has_api: 'does the text mention the Botpress API?',
has_enterprise: 'does the text mention Botpress Enterprise?',
has_workspaces: 'does the text mention workspaces?',
has_webchat: 'does the text mention the Webchat?',
has_hitl: 'does the text mention HITL (human in the loop)?',
})
expect(labels).toMatchInlineSnapshot(`
{
"contains_js_code": true,
"contains_lua_code": false,
"contains_python_code": false,
"has_api": true,
"has_enterprise": true,
"has_flows": true,
"has_hitl": true,
"has_webchat": true,
"has_workspaces": true,
"is_about_animals": false,
"is_botpress": true,
"is_rasa": false,
}
`)
})
})
describe.sequential('zai.learn.label', { timeout: 60_000 }, () => {
const client = getClient()
let tableName = 'ZaiTestLabelInternalTable'
let taskId = 'label'
let zai = getZai()
beforeEach(async () => {
zai = getZai().with({
activeLearning: {
enable: true,
taskId,
tableName,
},
})
})
afterEach(async () => {
try {
await client.deleteTableRows({ table: tableName, deleteAllRows: true })
} catch (err) {}
})
afterAll(async () => {
try {
await client.deleteTable({ table: tableName })
} catch (err) {}
})
it('learns a labelling rule from examples', async () => {
const adapter = new TableAdapter({
client,
tableName,
})
const value = await zai.learn(taskId).label(`Sylvain Perron has no criminal record.`, {
is_human: 'is the person a human?',
good_person: 'is the person a good person?',
bad_person: 'is the person a bad person?',
canadian: 'is the person canadian?',
is_french: 'is the person french?',
})
expect(value.is_human).toBe(true)
expect(value.is_french).toBe(false)
expect(value.canadian).toBe(false)
let rows = await client.findTableRows({ table: tableName })
expect(rows.rows.length).toBe(1)
await adapter.saveExample({
key: 't1',
taskId: `zai/${taskId}`,
taskType: 'zai.label',
instructions: 'label the sentence',
input: 'Sylvain Pellerin has no criminal record.',
output: {
is_french: {
label: 'ABSOLUTELY_YES',
explanation: 'Important: Sylvain is a common French name.',
},
canadian: {
label: 'ABSOLUTELY_YES',
explanation: 'Since we are doing business only in Canada, we assume all users are Canadians.',
},
},
// The below doesn't make sense on purpose, it's just to test the influence of the explanation on the next prediction
explanation: `IMPORTANT: Sylvain is a common French name and since we're doing business only in Canada, we assume ALL users are Canadians as soon as they mention a French name.`,
metadata,
status: 'approved',
})
await adapter.saveExample({
key: 't2',
taskId: `zai/${taskId}`,
taskType: 'zai.label',
instructions: 'label the sentence',
input: 'Joannie Côté has a dog.',
output: {
is_french: {
label: 'ABSOLUTELY_YES',
explanation: 'Important: Joannie is a common French name and Côté is a common French last name.',
},
canadian: {
label: 'ABSOLUTELY_YES',
explanation: 'Since we are doing business only in Canada, we assume all users are Canadians.',
},
},
metadata,
status: 'approved',
})
rows = await client.findTableRows({ table: tableName })
expect(rows.rows.length).toBe(3)
const second = await zai.learn(taskId).label(`Sylvain Perron has no criminal record.`, {
is_human: 'is the person a human?',
good_person: 'is the person a good person?',
bad_person: 'is the person a bad person?',
canadian: 'is the person canadian?',
is_french: 'is the person french?',
})
expect(second.is_human).toBe(true)
expect(second.is_french).toBe(true)
expect(second.canadian).toBe(true)
rows = await client.findTableRows({ table: tableName })
expect(rows.rows.length).toBe(3)
check(rows.rows[0].output.value.canadian, 'label is positive (yes) and there is an explanation of why').toBe(true)
check(rows.rows[0].output.value.is_french, 'label is positive (yes) and there is an explanation of why').toBe(true)
})
})