713 lines
26 KiB
TypeScript
713 lines
26 KiB
TypeScript
// eslint-disable consistent-type-definitions
|
|
import { afterAll, afterEach, beforeEach, describe, expect, it } from 'vitest'
|
|
import { getClient, getZai, metadata } from './utils'
|
|
import { TableAdapter } from '../src/adapters/botpress-table'
|
|
|
|
describe('zai.rate', () => {
|
|
describe('string instructions', () => {
|
|
it('should rate a single item with string instructions', async () => {
|
|
const zai = getZai()
|
|
// Unambiguous: complete student record with all fields filled
|
|
const students = [
|
|
{
|
|
name: 'Alice Johnson',
|
|
studentId: 'STU-001',
|
|
email: 'alice@school.edu',
|
|
phone: '+1-555-0100',
|
|
address: '123 Main St, City, State 12345',
|
|
enrollmentDate: '2024-01-15',
|
|
gpa: 3.8,
|
|
credits: 120,
|
|
},
|
|
]
|
|
const ratings = await zai.rate(students, 'how complete is this student record?')
|
|
|
|
expect(ratings).toHaveLength(1)
|
|
// Should be rated high (4-5 per criterion) since all fields are present
|
|
expect(ratings[0]).toBeGreaterThanOrEqual(12) // 3 criteria * 4 minimum
|
|
expect(ratings[0]).toBeLessThanOrEqual(25) // max possible with 5 criteria * 5
|
|
})
|
|
|
|
it('should rate multiple items with clear differences', async () => {
|
|
const zai = getZai()
|
|
const students = [
|
|
// Complete record - should rate high
|
|
{
|
|
name: 'Alice Johnson',
|
|
studentId: 'STU-001',
|
|
email: 'alice@school.edu',
|
|
phone: '+1-555-0100',
|
|
address: '123 Main St',
|
|
enrollmentDate: '2024-01-15',
|
|
},
|
|
// Minimal record - should rate low
|
|
{
|
|
name: 'Bob',
|
|
studentId: null,
|
|
email: null,
|
|
phone: null,
|
|
address: null,
|
|
enrollmentDate: null,
|
|
},
|
|
// Partial record - should rate medium
|
|
{
|
|
name: 'Carol Smith',
|
|
studentId: 'STU-003',
|
|
email: 'carol@school.edu',
|
|
phone: null,
|
|
address: null,
|
|
enrollmentDate: '2024-02-01',
|
|
},
|
|
]
|
|
const ratings = await zai.rate(students, 'how complete is this student record?')
|
|
|
|
expect(ratings).toHaveLength(3)
|
|
// Alice (complete) should be rated higher than Bob (minimal)
|
|
expect(ratings[0]).toBeGreaterThan(ratings[1]!)
|
|
// Carol (partial) should be between Alice and Bob
|
|
expect(ratings[2]).toBeGreaterThan(ratings[1]!)
|
|
expect(ratings[0]).toBeGreaterThan(ratings[2]!)
|
|
})
|
|
|
|
it('should provide detailed results with string instructions', async () => {
|
|
const zai = getZai()
|
|
const forms = [
|
|
{
|
|
firstName: 'John',
|
|
lastName: 'Doe',
|
|
email: 'john.doe@example.com',
|
|
phone: '+1-555-0123',
|
|
address: '456 Oak Ave',
|
|
city: 'Springfield',
|
|
state: 'IL',
|
|
zipCode: '62701',
|
|
},
|
|
]
|
|
const response = zai.rate(forms, 'how complete is this form submission?')
|
|
const { output, usage, elapsed } = await response.result()
|
|
|
|
expect(output).toHaveLength(1)
|
|
expect(output[0]).toHaveProperty('total')
|
|
expect(output[0]?.total).toBeGreaterThanOrEqual(3)
|
|
expect(output[0]?.total).toBeLessThanOrEqual(25) // max 5 criteria * 5 rating
|
|
|
|
// Should have multiple criteria (3-5)
|
|
const criteriaKeys = Object.keys(output[0] ?? {}).filter((k) => k !== 'total')
|
|
expect(criteriaKeys.length).toBeGreaterThanOrEqual(3)
|
|
expect(criteriaKeys.length).toBeLessThanOrEqual(5)
|
|
|
|
// Each criterion should be rated 1-5
|
|
criteriaKeys.forEach((key) => {
|
|
const score = output[0]?.[key]
|
|
expect(score).toBeGreaterThanOrEqual(1)
|
|
expect(score).toBeLessThanOrEqual(5)
|
|
expect(Number.isInteger(score)).toBe(true)
|
|
})
|
|
|
|
expect(usage.tokens.total).toBeGreaterThan(0)
|
|
expect(elapsed).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
describe('record instructions', () => {
|
|
it('should rate items with fixed criteria', async () => {
|
|
const zai = getZai()
|
|
const passwords = [
|
|
{
|
|
password: 'Tr0ng!P@ssw0rd#2024',
|
|
length: 20,
|
|
hasUppercase: true,
|
|
hasLowercase: true,
|
|
hasNumbers: true,
|
|
hasSymbols: true,
|
|
},
|
|
{ password: 'weak', length: 4, hasUppercase: false, hasLowercase: true, hasNumbers: false, hasSymbols: false },
|
|
]
|
|
const ratings = await zai.rate(passwords, {
|
|
length: 'password length (12+ chars = very_good, 8-11 = good, 6-7 = average, 4-5 = bad, <4 = very_bad)',
|
|
complexity:
|
|
'character variety (uppercase+lowercase+numbers+symbols = very_good, 3 types = good, 2 types = average, 1 type = bad)',
|
|
strength: 'overall password strength against brute force attacks',
|
|
})
|
|
|
|
expect(ratings).toHaveLength(2)
|
|
|
|
// First password is strong
|
|
expect(ratings[0]).toHaveProperty('length')
|
|
expect(ratings[0]).toHaveProperty('complexity')
|
|
expect(ratings[0]).toHaveProperty('strength')
|
|
expect(ratings[0]).toHaveProperty('total')
|
|
expect(ratings[0]?.length).toBeGreaterThanOrEqual(4) // 20 chars = very_good or good
|
|
expect(ratings[0]?.complexity).toBeGreaterThanOrEqual(4) // all types = very_good or good
|
|
expect(ratings[0]?.strength).toBeGreaterThanOrEqual(4)
|
|
expect(ratings[0]?.total).toBe(
|
|
(ratings[0]?.length ?? 0) + (ratings[0]?.complexity ?? 0) + (ratings[0]?.strength ?? 0)
|
|
)
|
|
|
|
// Second password is weak
|
|
expect(ratings[1]?.length).toBeLessThanOrEqual(2) // 4 chars = bad or very_bad
|
|
expect(ratings[1]?.complexity).toBeLessThanOrEqual(2) // only lowercase = bad or very_bad
|
|
expect(ratings[1]?.strength).toBeLessThanOrEqual(2)
|
|
expect(ratings[1]?.total).toBe(
|
|
(ratings[1]?.length ?? 0) + (ratings[1]?.complexity ?? 0) + (ratings[1]?.strength ?? 0)
|
|
)
|
|
|
|
// Strong password should have higher total than weak
|
|
expect(ratings[0]?.total).toBeGreaterThan(ratings[1]?.total ?? 0)
|
|
})
|
|
|
|
it('should rate with single criterion', async () => {
|
|
const zai = getZai()
|
|
const emails = [
|
|
{ email: 'valid.email@example.com', hasAt: true, hasDomain: true, hasTLD: true },
|
|
{ email: 'invalid-email', hasAt: false, hasDomain: false, hasTLD: false },
|
|
]
|
|
const ratings = await zai.rate(emails, {
|
|
validity: 'email format validity (has @ symbol, domain, and TLD)',
|
|
})
|
|
|
|
expect(ratings).toHaveLength(2)
|
|
expect(ratings[0]).toHaveProperty('validity')
|
|
expect(ratings[0]).toHaveProperty('total')
|
|
// Valid email should rate high
|
|
expect(ratings[0]?.validity).toBeGreaterThanOrEqual(4)
|
|
expect(ratings[0]?.total).toBe(ratings[0]?.validity)
|
|
|
|
// Invalid email should rate low
|
|
expect(ratings[1]?.validity).toBeLessThanOrEqual(2)
|
|
expect(ratings[1]?.total).toBe(ratings[1]?.validity)
|
|
})
|
|
|
|
it('should provide detailed results with record instructions', async () => {
|
|
const zai = getZai()
|
|
const files = [
|
|
{
|
|
filename: 'document.pdf',
|
|
sizeBytes: 1024000,
|
|
hasExtension: true,
|
|
validName: true,
|
|
reasonableSize: true,
|
|
},
|
|
]
|
|
const response = zai.rate(files, {
|
|
naming: 'filename follows conventions (has extension, no special chars)',
|
|
size: 'file size is reasonable (not too large or too small)',
|
|
})
|
|
const { output, usage, elapsed } = await response.result()
|
|
|
|
expect(output).toHaveLength(1)
|
|
expect(output[0]).toHaveProperty('naming')
|
|
expect(output[0]).toHaveProperty('size')
|
|
expect(output[0]).toHaveProperty('total')
|
|
expect(output[0]?.total).toBe((output[0]?.naming ?? 0) + (output[0]?.size ?? 0))
|
|
|
|
expect(usage.tokens.total).toBeGreaterThan(0)
|
|
expect(elapsed).toBeGreaterThan(0)
|
|
})
|
|
})
|
|
|
|
describe('large arrays', () => {
|
|
it('should rate many items efficiently with parallelization', async () => {
|
|
const zai = getZai()
|
|
// Create 500 items to force chunking and parallel processing
|
|
const items = Array.from({ length: 500 }, (_, i) => ({
|
|
id: i,
|
|
// Every 10th item is complete, others are incomplete
|
|
requiredField1: i % 10 === 0 ? 'present' : null,
|
|
requiredField2: i % 10 === 0 ? 'present' : null,
|
|
requiredField3: i % 10 === 0 ? 'present' : null,
|
|
optionalField: i % 10 === 0 ? 'present' : null,
|
|
}))
|
|
|
|
const startTime = Date.now()
|
|
const ratings = await zai.rate(items, 'how complete are the required fields?')
|
|
const elapsed = Date.now() - startTime
|
|
|
|
expect(ratings).toHaveLength(500)
|
|
|
|
// Verify ratings are in correct order (matching input order)
|
|
for (let i = 0; i < 500; i++) {
|
|
expect(ratings[i]).toBeDefined()
|
|
expect(typeof ratings[i]).toBe('number')
|
|
|
|
if (i % 10 === 0) {
|
|
// Complete items should rate high
|
|
expect(ratings[i]).toBeGreaterThanOrEqual(12) // 3 criteria * 4
|
|
} else {
|
|
// Incomplete items should rate low
|
|
expect(ratings[i]).toBeLessThanOrEqual(10) // Should be lower
|
|
}
|
|
}
|
|
|
|
// With 500 items and max 50 per chunk, we should have ~10 chunks
|
|
// Parallel processing should complete significantly faster than sequential
|
|
// (this is a sanity check, not a strict performance test)
|
|
console.log(`Rated 500 items in ${elapsed}ms`)
|
|
expect(elapsed).toBeLessThan(300000) // Should complete within 5 minutes
|
|
})
|
|
|
|
it('should maintain order with large parallel chunks', async () => {
|
|
const zai = getZai()
|
|
// Create 300 items with predictable patterns
|
|
const items = Array.from({ length: 300 }, (_, i) => ({
|
|
id: i,
|
|
value: i * 2,
|
|
isEven: i % 2 === 0,
|
|
}))
|
|
|
|
const ratings = await zai.rate(items, {
|
|
id_presence: 'does it have an id field?',
|
|
value_presence: 'does it have a value field?',
|
|
})
|
|
|
|
expect(ratings).toHaveLength(300)
|
|
|
|
// Verify order is maintained
|
|
for (let i = 0; i < 300; i++) {
|
|
expect(ratings[i]).toBeDefined()
|
|
// All items have id and value, so all should rate high
|
|
expect(ratings[i]?.id_presence).toBeGreaterThanOrEqual(4)
|
|
expect(ratings[i]?.value_presence).toBeGreaterThanOrEqual(4)
|
|
expect(ratings[i]?.total).toBe((ratings[i]?.id_presence ?? 0) + (ratings[i]?.value_presence ?? 0))
|
|
}
|
|
})
|
|
|
|
it('should rate many items efficiently', async () => {
|
|
const zai = getZai()
|
|
// Create 100 items with objective completion scores
|
|
const items = Array.from({ length: 100 }, (_, i) => ({
|
|
id: i,
|
|
// Every 10th item is complete, others are incomplete
|
|
requiredField1: i % 10 === 0 ? 'present' : null,
|
|
requiredField2: i % 10 === 0 ? 'present' : null,
|
|
requiredField3: i % 10 === 0 ? 'present' : null,
|
|
optionalField: i % 10 === 0 ? 'present' : null,
|
|
}))
|
|
|
|
const ratings = await zai.rate(items, 'how complete are the required fields?')
|
|
|
|
expect(ratings).toHaveLength(100)
|
|
|
|
// Items at index 0, 10, 20, etc. should be rated higher
|
|
for (let i = 0; i < 100; i++) {
|
|
if (i % 10 === 0) {
|
|
// Complete items should rate high
|
|
expect(ratings[i]).toBeGreaterThanOrEqual(12) // 3 criteria * 4
|
|
} else {
|
|
// Incomplete items should rate low
|
|
expect(ratings[i]).toBeLessThanOrEqual(10) // Should be lower
|
|
}
|
|
}
|
|
})
|
|
|
|
it('should handle chunking with record instructions', async () => {
|
|
const zai = getZai()
|
|
const items = Array.from({ length: 75 }, (_, i) => ({
|
|
id: i,
|
|
isEven: i % 2 === 0,
|
|
isDivisibleBy5: i % 5 === 0,
|
|
}))
|
|
|
|
const ratings = await zai.rate(items, {
|
|
evenness: 'is the ID an even number?',
|
|
divisibility: 'is the ID divisible by 5?',
|
|
})
|
|
|
|
expect(ratings).toHaveLength(75)
|
|
ratings.forEach((rating, idx) => {
|
|
expect(rating).toHaveProperty('evenness')
|
|
expect(rating).toHaveProperty('divisibility')
|
|
expect(rating).toHaveProperty('total')
|
|
expect(rating.total).toBe(rating.evenness + rating.divisibility)
|
|
|
|
// Check logical ratings
|
|
if (idx % 2 === 0) {
|
|
// Even numbers should rate high on evenness
|
|
expect(rating.evenness).toBeGreaterThanOrEqual(4)
|
|
} else {
|
|
// Odd numbers should rate low on evenness
|
|
expect(rating.evenness).toBeLessThanOrEqual(2)
|
|
}
|
|
|
|
if (idx % 5 === 0) {
|
|
// Divisible by 5 should rate high
|
|
expect(rating.divisibility).toBeGreaterThanOrEqual(4)
|
|
}
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('edge cases', () => {
|
|
it('should handle empty array', async () => {
|
|
const zai = getZai()
|
|
const ratings = await zai.rate([], 'rate this')
|
|
|
|
expect(ratings).toHaveLength(0)
|
|
expect(ratings).toEqual([])
|
|
})
|
|
|
|
it('should handle single item edge case', async () => {
|
|
const zai = getZai()
|
|
const items = [{ status: 'complete', fields: 10, validated: true }]
|
|
const ratings = await zai.rate(items, 'is this item complete?')
|
|
|
|
expect(ratings).toHaveLength(1)
|
|
expect(ratings[0]).toBeGreaterThanOrEqual(3)
|
|
})
|
|
|
|
it('should handle items with clear binary outcomes', async () => {
|
|
const zai = getZai()
|
|
const checks = [
|
|
{ testsPassing: true, buildSuccessful: true, lintClean: true, coverageAbove80: true },
|
|
{ testsPassing: false, buildSuccessful: false, lintClean: false, coverageAbove80: false },
|
|
]
|
|
const ratings = await zai.rate(checks, 'how healthy is this codebase status?')
|
|
|
|
expect(ratings).toHaveLength(2)
|
|
// First item (all passing) should rate significantly higher than second (all failing)
|
|
expect(ratings[0]).toBeGreaterThan(ratings[1]! * 2)
|
|
})
|
|
})
|
|
|
|
describe('options', () => {
|
|
it('should respect tokensPerItem option', async () => {
|
|
const zai = getZai()
|
|
const largeItem = {
|
|
description: 'A'.repeat(10000),
|
|
hasDescription: true,
|
|
isVeryLong: true,
|
|
}
|
|
const ratings = await zai.rate([largeItem], 'does this have a description?', { tokensPerItem: 50 })
|
|
|
|
expect(ratings).toHaveLength(1)
|
|
expect(ratings[0]).toBeGreaterThanOrEqual(3)
|
|
})
|
|
|
|
it('should respect maxItemsPerChunk option', async () => {
|
|
const zai = getZai()
|
|
const items = Array.from({ length: 10 }, (_, i) => ({ id: i, isPresent: true }))
|
|
const ratings = await zai.rate(items, 'is the id field present?', { maxItemsPerChunk: 3 })
|
|
|
|
expect(ratings).toHaveLength(10)
|
|
// All should rate high since id is always present
|
|
ratings.forEach((rating) => {
|
|
expect(rating).toBeGreaterThanOrEqual(3)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe('progress tracking', () => {
|
|
it('should emit progress events', async () => {
|
|
const zai = getZai()
|
|
const items = Array.from({ length: 20 }, (_, i) => ({ id: i, value: i }))
|
|
const response = zai.rate(items, 'rate these')
|
|
|
|
const progressEvents: number[] = []
|
|
response.on('progress', (usage) => {
|
|
progressEvents.push(usage.requests.percentage)
|
|
})
|
|
|
|
await response
|
|
|
|
expect(progressEvents.length).toBeGreaterThan(0)
|
|
// Last event should be 100% or close to it
|
|
const lastProgress = progressEvents[progressEvents.length - 1]
|
|
expect(lastProgress).toBeGreaterThan(0)
|
|
expect(lastProgress).toBeLessThanOrEqual(1)
|
|
})
|
|
})
|
|
|
|
describe('abort signal', () => {
|
|
it('should support abort signal', async () => {
|
|
const zai = getZai()
|
|
// Use more items to ensure the operation takes longer
|
|
const items = Array.from({ length: 200 }, (_, i) => ({ id: i, data: 'x'.repeat(100) }))
|
|
const controller = new AbortController()
|
|
const response = zai.rate(items, 'rate these items')
|
|
response.bindSignal(controller.signal)
|
|
|
|
// Abort immediately
|
|
controller.abort()
|
|
|
|
await expect(response).rejects.toThrow()
|
|
})
|
|
})
|
|
|
|
describe('objective numerical criteria', () => {
|
|
it('should rate based on clear numerical thresholds', async () => {
|
|
const zai = getZai()
|
|
const servers = [
|
|
{ cpu: 95, memory: 90, disk: 85, uptime: 30 }, // Critical - should rate low
|
|
{ cpu: 50, memory: 55, disk: 60, uptime: 99.9 }, // Healthy - should rate high
|
|
{ cpu: 75, memory: 70, disk: 65, uptime: 95 }, // Warning - should rate medium
|
|
]
|
|
|
|
const ratings = await zai.rate(servers, {
|
|
cpu_health:
|
|
'CPU usage health (0-50% = very_good, 51-70% = good, 71-80% = average, 81-90% = bad, 90%+ = very_bad)',
|
|
memory_health:
|
|
'Memory usage health (0-50% = very_good, 51-70% = good, 71-80% = average, 81-90% = bad, 90%+ = very_bad)',
|
|
disk_health:
|
|
'Disk usage health (0-50% = very_good, 51-70% = good, 71-80% = average, 81-90% = bad, 90%+ = very_bad)',
|
|
})
|
|
|
|
expect(ratings).toHaveLength(3)
|
|
|
|
// Server 0 (critical): all metrics >85% should rate badly
|
|
expect(ratings[0]?.cpu_health).toBeLessThanOrEqual(2)
|
|
expect(ratings[0]?.memory_health).toBeLessThanOrEqual(2)
|
|
expect(ratings[0]?.disk_health).toBeLessThanOrEqual(2)
|
|
|
|
// Server 1 (healthy): all metrics 50-60% should rate well
|
|
expect(ratings[1]?.cpu_health).toBeGreaterThanOrEqual(4)
|
|
expect(ratings[1]?.memory_health).toBeGreaterThanOrEqual(4)
|
|
expect(ratings[1]?.disk_health).toBeGreaterThanOrEqual(3)
|
|
|
|
// Healthy server should have higher total than critical server
|
|
expect(ratings[1]?.total).toBeGreaterThan(ratings[0]?.total ?? 0)
|
|
})
|
|
|
|
it('should rate based on count criteria', async () => {
|
|
const zai = getZai()
|
|
const repos = [
|
|
{ name: 'repo-a', openIssues: 2, contributors: 50, stars: 1000 },
|
|
{ name: 'repo-b', openIssues: 200, contributors: 1, stars: 5 },
|
|
]
|
|
|
|
const ratings = await zai.rate(repos, {
|
|
issue_management:
|
|
'open issues count (0-10 = very_good, 11-50 = good, 51-100 = average, 101-200 = bad, 200+ = very_bad)',
|
|
community: 'contributor count (50+ = very_good, 20-49 = good, 10-19 = average, 5-9 = bad, <5 = very_bad)',
|
|
popularity: 'star count (1000+ = very_good, 500-999 = good, 100-499 = average, 50-99 = bad, <50 = very_bad)',
|
|
})
|
|
|
|
expect(ratings).toHaveLength(2)
|
|
|
|
// repo-a has good metrics
|
|
expect(ratings[0]?.issue_management).toBeGreaterThanOrEqual(4) // 2 issues = very_good
|
|
expect(ratings[0]?.community).toBeGreaterThanOrEqual(4) // 50 contributors = very_good
|
|
expect(ratings[0]?.popularity).toBeGreaterThanOrEqual(4) // 1000 stars = very_good
|
|
|
|
// repo-b has poor metrics
|
|
expect(ratings[1]?.issue_management).toBeLessThanOrEqual(2) // 200 issues = bad
|
|
expect(ratings[1]?.community).toBeLessThanOrEqual(2) // 1 contributor = very_bad
|
|
expect(ratings[1]?.popularity).toBeLessThanOrEqual(2) // 5 stars = very_bad
|
|
|
|
expect(ratings[0]?.total).toBeGreaterThan(ratings[1]?.total ?? 0)
|
|
})
|
|
})
|
|
})
|
|
|
|
describe.sequential('zai.learn.rate', () => {
|
|
const client = getClient()
|
|
const tableName = 'ZaiTestRateInternalTable'
|
|
const taskId = 'rate'
|
|
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) {}
|
|
})
|
|
|
|
// TODO: fix and re-enable
|
|
it.skip('learns counterintuitive rating pattern that LLM cannot guess', async () => {
|
|
const adapter = new TableAdapter({
|
|
client,
|
|
tableName,
|
|
})
|
|
|
|
// Test a COUNTERINTUITIVE pattern: Custom email domain ratings
|
|
// Custom company-specific domain importance rules that LLM cannot know without learning:
|
|
// @sequoia.vc = our investor (highest importance - rate 5/5/5)
|
|
// @a16z.com = potential investor (high importance - rate 4/4/4)
|
|
// @google.com = competitor (medium importance - rate 3/3/3)
|
|
// analyst@* = spam regardless of domain (lowest importance - rate 1/1/1)
|
|
|
|
// Add approved examples showing the domain pattern
|
|
await adapter.saveExample({
|
|
key: 'email_domain1',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.rate',
|
|
instructions: 'rate email sender importance',
|
|
input: JSON.stringify([{ from: 'partner@sequoia.vc', subject: 'Q4 Review' }]),
|
|
output: [{ trustworthiness: 5, priority: 5, relevance: 5, total: 15 }],
|
|
explanation: 'RULE: @sequoia.vc is our investor - highest importance rating',
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
await adapter.saveExample({
|
|
key: 'email_domain2',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.rate',
|
|
instructions: 'rate email sender importance',
|
|
input: JSON.stringify([{ from: 'analyst@bankofamerica.com', subject: 'Market Report' }]),
|
|
output: [{ trustworthiness: 1, priority: 1, relevance: 1, total: 3 }],
|
|
explanation: 'RULE: analyst@* prefix is spam - lowest importance rating',
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
await adapter.saveExample({
|
|
key: 'email_domain3',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.rate',
|
|
instructions: 'rate email sender importance',
|
|
input: JSON.stringify([{ from: 'ben@a16z.com', subject: 'Investment Discussion' }]),
|
|
output: [{ trustworthiness: 4, priority: 4, relevance: 4, total: 12 }],
|
|
explanation: 'RULE: @a16z.com is potential investor - high importance rating',
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
await adapter.saveExample({
|
|
key: 'email_domain4',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.rate',
|
|
instructions: 'rate email sender importance',
|
|
input: JSON.stringify([{ from: 'team@google.com', subject: 'Partnership Proposal' }]),
|
|
output: [{ trustworthiness: 3, priority: 3, relevance: 3, total: 9 }],
|
|
explanation: 'RULE: @google.com is competitor - medium importance rating',
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
await adapter.saveExample({
|
|
key: 'email_domain5',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.rate',
|
|
instructions: 'rate email sender importance',
|
|
input: JSON.stringify([{ from: 'roelof@sequoia.vc', subject: 'Portfolio Update' }]),
|
|
output: [{ trustworthiness: 5, priority: 5, relevance: 5, total: 15 }],
|
|
explanation: 'RULE: @sequoia.vc is our investor - highest importance rating',
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
await adapter.saveExample({
|
|
key: 'email_domain6',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.rate',
|
|
instructions: 'rate email sender importance',
|
|
input: JSON.stringify([{ from: 'analyst@jpmorgan.com', subject: 'Stock Analysis' }]),
|
|
output: [{ trustworthiness: 1, priority: 1, relevance: 1, total: 3 }],
|
|
explanation: 'RULE: analyst@* prefix is spam - lowest importance rating',
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
// Now test with learned examples
|
|
const ratings = await zai.learn(taskId).rate(
|
|
[
|
|
{ from: 'sarah@sequoia.vc', subject: 'Board Meeting' }, // investor - should rate HIGH
|
|
{ from: 'analyst@goldmansachs.com', subject: 'Earnings Report' }, // analyst spam - should rate LOW
|
|
{ from: 'marc@a16z.com', subject: 'Funding Round' }, // potential investor - should rate HIGH
|
|
{ from: 'recruiter@google.com', subject: 'Hiring' }, // competitor - should rate MEDIUM
|
|
],
|
|
'rate email sender importance'
|
|
)
|
|
|
|
expect(ratings).toHaveLength(4)
|
|
|
|
// With the pattern learned, domain ratings should follow the rules
|
|
const sequoiaScore = ratings[0]! // @sequoia.vc (investor - HIGH)
|
|
const analystScore = ratings[1]! // analyst@* (spam - LOW)
|
|
const a16zScore = ratings[2]! // @a16z.com (potential investor - HIGH)
|
|
const googleScore = ratings[3]! // @google.com (competitor - MEDIUM)
|
|
|
|
// Most reliable pattern: analyst@ should ALWAYS score lowest
|
|
expect(analystScore).toBeLessThan(sequoiaScore)
|
|
expect(analystScore).toBeLessThan(a16zScore)
|
|
expect(analystScore).toBeLessThanOrEqual(googleScore)
|
|
|
|
// Investors should score higher than competitor
|
|
const investorAvg = (sequoiaScore + a16zScore) / 2
|
|
expect(investorAvg).toBeGreaterThan(googleScore)
|
|
|
|
// Sequoia (our investor) should score very high
|
|
expect(sequoiaScore).toBeGreaterThanOrEqual(10) // At least 10/15
|
|
|
|
const rows = await client.findTableRows({ table: tableName })
|
|
expect(rows.rows.length).toBeGreaterThanOrEqual(6) // 6 examples + new ratings
|
|
})
|
|
|
|
it('learns rating patterns from examples with record instructions', async () => {
|
|
const adapter = new TableAdapter({
|
|
client,
|
|
tableName,
|
|
})
|
|
|
|
// Add approved examples with specific criteria
|
|
await adapter.saveExample({
|
|
key: 'password1',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.rate',
|
|
instructions: 'rate password strength',
|
|
input: JSON.stringify([{ password: 'Str0ng!P@ss#2024', length: 16, hasAll: true }]),
|
|
output: [{ length: 5, complexity: 5, strength: 5, total: 15 }],
|
|
explanation: 'Strong password: 16 chars, all character types',
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
await adapter.saveExample({
|
|
key: 'password2',
|
|
taskId: `zai/${taskId}`,
|
|
taskType: 'zai.rate',
|
|
instructions: 'rate password strength',
|
|
input: JSON.stringify([{ password: 'weak', length: 4, hasAll: false }]),
|
|
output: [{ length: 1, complexity: 1, strength: 1, total: 3 }],
|
|
explanation: 'Weak password: only 4 chars, missing character types',
|
|
metadata,
|
|
status: 'approved',
|
|
})
|
|
|
|
// Rate passwords with learned patterns
|
|
const ratings = await zai.learn(taskId).rate(
|
|
[
|
|
{ password: 'MyStr0ng!Pass', length: 13, hasAll: true }, // Strong
|
|
{ password: 'bad', length: 3, hasAll: false }, // Weak
|
|
],
|
|
{
|
|
length: 'password length (12+ = very_good, 8-11 = good, 6-7 = average, 4-5 = bad, <4 = very_bad)',
|
|
complexity: 'character variety (all types = very_good)',
|
|
strength: 'overall password strength',
|
|
}
|
|
)
|
|
|
|
expect(ratings).toHaveLength(2)
|
|
|
|
// Strong password should rate high (learned pattern)
|
|
expect(ratings[0]?.length).toBeGreaterThanOrEqual(4) // 13 chars
|
|
expect(ratings[0]?.complexity).toBeGreaterThanOrEqual(4) // has all types
|
|
expect(ratings[0]?.strength).toBeGreaterThanOrEqual(4)
|
|
expect(ratings[0]?.total).toBeGreaterThanOrEqual(12)
|
|
|
|
// Weak password should rate low (learned pattern)
|
|
expect(ratings[1]?.length).toBeLessThanOrEqual(2) // 3 chars
|
|
expect(ratings[1]?.complexity).toBeLessThanOrEqual(2) // missing types
|
|
expect(ratings[1]?.strength).toBeLessThanOrEqual(2)
|
|
expect(ratings[1]?.total).toBeLessThanOrEqual(6)
|
|
|
|
const rows = await client.findTableRows({ table: tableName })
|
|
expect(rows.rows.length).toBeGreaterThanOrEqual(2)
|
|
})
|
|
})
|