UNPKG

ai-functions

Version:

Core AI primitives for building intelligent applications

408 lines (407 loc) 19.3 kB
/** * Tests for function type inference from name + args * * The ai.* magic proxy infers function type based on: * 1. Function name (e.g., fizzBuzz → Code, storyBrand → Generative) * 2. Argument structure (e.g., { max: 100 } → Code parameters) * 3. Argument values (e.g., { amount: 50000 } → Human approval needed) * * Function Types: * - Generative: Generate content (text, json, lists, images) * - Code: Generate and execute code * - Agentic: Multi-step tasks with tools * - Human: Requires human input or approval */ import { describe, it, expect, vi, beforeEach } from 'vitest'; // ============================================================================ // Mock type inference logic // ============================================================================ /** * Infer function type from name patterns */ function inferTypeFromName(name) { // Code patterns - computing, algorithms, transformations const codePatterns = [ /^(fizz|buzz|calculate|compute|sort|filter|map|reduce|transform)/i, /^(validate|parse|format|convert|encode|decode|hash|compress)/i, /^(find|search|match|replace|split|join|merge)/i, /(algorithm|function|method|handler|processor)$/i, ]; // Agentic patterns - tasks, workflows, automation const agenticPatterns = [ /^(launch|deploy|publish|schedule|automate)/i, /^(research|investigate|analyze|audit|monitor)/i, /^(create|build|setup|configure|integrate)/i, /(workflow|pipeline|process|task|job)$/i, ]; // Human patterns - approval, review, decisions const humanPatterns = [ /^(approve|reject|review|verify|confirm)/i, /^(decide|choose|select|pick)/i, /(approval|review|confirmation)$/i, ]; // Check patterns in order of specificity for (const pattern of humanPatterns) { if (pattern.test(name)) return 'human'; } for (const pattern of agenticPatterns) { if (pattern.test(name)) return 'agentic'; } for (const pattern of codePatterns) { if (pattern.test(name)) return 'code'; } // Default to generative return 'generative'; } /** * Infer function type from argument structure */ function inferTypeFromArgs(args) { if (typeof args !== 'object' || args === null) return null; const argObj = args; const keys = Object.keys(argObj); // Code indicators - numeric parameters, input/output pairs const codeIndicators = ['max', 'min', 'count', 'input', 'output', 'format', 'precision']; if (codeIndicators.some(k => keys.includes(k))) { return 'code'; } // Agentic indicators - URLs, targets, configurations const agenticIndicators = ['url', 'target', 'destination', 'webhook', 'schedule']; if (agenticIndicators.some(k => keys.includes(k))) { return 'agentic'; } // Human indicators - requires explicit approval flags const humanIndicators = ['requiresApproval', 'needsReview', 'manualStep']; if (humanIndicators.some(k => keys.includes(k))) { return 'human'; } return null; } /** * Infer if human approval needed based on values (subjective judgment) */ function requiresHumanApproval(name, args) { if (typeof args !== 'object' || args === null) return false; const argObj = args; // High-value financial transactions if ('amount' in argObj && typeof argObj.amount === 'number') { if (argObj.amount > 10000) return true; } // Sensitive operations if ('action' in argObj) { const sensitiveActions = ['delete', 'terminate', 'cancel', 'refund', 'transfer']; if (sensitiveActions.includes(String(argObj.action).toLowerCase())) { return true; } } // Large scale operations if ('count' in argObj && typeof argObj.count === 'number') { if (argObj.count > 1000) return true; } // External communications if (name.toLowerCase().includes('email') || name.toLowerCase().includes('notify')) { if ('recipients' in argObj && Array.isArray(argObj.recipients)) { if (argObj.recipients.length > 100) return true; } } return false; } /** * Full type inference combining name, args, and values */ function inferFunctionType(name, args) { // First check if human approval is needed based on values if (requiresHumanApproval(name, args)) { return 'human'; } // Then check args structure const argsType = inferTypeFromArgs(args); if (argsType) return argsType; // Finally fall back to name inference return inferTypeFromName(name) || 'generative'; } // ============================================================================ // Type inference from name tests // ============================================================================ describe('type inference from function name', () => { describe('Code function patterns', () => { it('infers code type from algorithm names', () => { expect(inferTypeFromName('fizzBuzz')).toBe('code'); expect(inferTypeFromName('calculateSum')).toBe('code'); expect(inferTypeFromName('computeHash')).toBe('code'); expect(inferTypeFromName('sortArray')).toBe('code'); }); it('infers code type from transformation names', () => { expect(inferTypeFromName('validateEmail')).toBe('code'); expect(inferTypeFromName('parseJSON')).toBe('code'); expect(inferTypeFromName('formatDate')).toBe('code'); expect(inferTypeFromName('convertCurrency')).toBe('code'); }); it('infers code type from data operation names', () => { expect(inferTypeFromName('findUser')).toBe('code'); expect(inferTypeFromName('searchRecords')).toBe('code'); expect(inferTypeFromName('filterProducts')).toBe('code'); expect(inferTypeFromName('mergeArrays')).toBe('code'); }); }); describe('Generative function patterns', () => { it('infers generative type from content names', () => { expect(inferTypeFromName('storyBrand')).toBe('generative'); expect(inferTypeFromName('blogPost')).toBe('generative'); expect(inferTypeFromName('productDescription')).toBe('generative'); expect(inferTypeFromName('marketingCopy')).toBe('generative'); }); it('defaults unknown names to generative', () => { expect(inferTypeFromName('foo')).toBe('generative'); expect(inferTypeFromName('customThing')).toBe('generative'); expect(inferTypeFromName('randomName')).toBe('generative'); }); }); describe('Agentic function patterns', () => { it('infers agentic type from automation names', () => { expect(inferTypeFromName('launchProduct')).toBe('agentic'); expect(inferTypeFromName('deployApplication')).toBe('agentic'); expect(inferTypeFromName('publishArticle')).toBe('agentic'); expect(inferTypeFromName('scheduleTask')).toBe('agentic'); }); it('infers agentic type from research names', () => { expect(inferTypeFromName('researchMarket')).toBe('agentic'); expect(inferTypeFromName('investigateIssue')).toBe('agentic'); expect(inferTypeFromName('analyzeCompetitors')).toBe('agentic'); }); it('infers agentic type from workflow suffixes', () => { expect(inferTypeFromName('onboardingWorkflow')).toBe('agentic'); expect(inferTypeFromName('deploymentPipeline')).toBe('agentic'); expect(inferTypeFromName('dataProcess')).toBe('agentic'); }); }); describe('Human function patterns', () => { it('infers human type from approval names', () => { expect(inferTypeFromName('approveRefund')).toBe('human'); expect(inferTypeFromName('rejectApplication')).toBe('human'); expect(inferTypeFromName('reviewCode')).toBe('human'); }); it('infers human type from decision names', () => { expect(inferTypeFromName('decideOutcome')).toBe('human'); expect(inferTypeFromName('chooseOption')).toBe('human'); expect(inferTypeFromName('selectWinner')).toBe('human'); }); }); }); // ============================================================================ // Type inference from args tests // ============================================================================ describe('type inference from argument structure', () => { describe('Code argument patterns', () => { it('infers code from numeric parameters', () => { expect(inferTypeFromArgs({ max: 100 })).toBe('code'); expect(inferTypeFromArgs({ min: 0, max: 100 })).toBe('code'); expect(inferTypeFromArgs({ count: 10 })).toBe('code'); }); it('infers code from input/output pairs', () => { expect(inferTypeFromArgs({ input: 'data', format: 'json' })).toBe('code'); expect(inferTypeFromArgs({ output: 'file.txt' })).toBe('code'); }); }); describe('Agentic argument patterns', () => { it('infers agentic from URL parameters', () => { expect(inferTypeFromArgs({ url: 'https://example.com' })).toBe('agentic'); expect(inferTypeFromArgs({ target: 'https://api.example.com' })).toBe('agentic'); }); it('infers agentic from webhook/schedule parameters', () => { expect(inferTypeFromArgs({ webhook: 'https://hooks.example.com' })).toBe('agentic'); expect(inferTypeFromArgs({ schedule: '0 9 * * *' })).toBe('agentic'); }); }); describe('Human argument patterns', () => { it('infers human from explicit approval flags', () => { expect(inferTypeFromArgs({ requiresApproval: true })).toBe('human'); expect(inferTypeFromArgs({ needsReview: true })).toBe('human'); expect(inferTypeFromArgs({ manualStep: true })).toBe('human'); }); }); describe('No inference from args', () => { it('returns null for unrecognized arg patterns', () => { expect(inferTypeFromArgs({ name: 'John' })).toBeNull(); expect(inferTypeFromArgs({ text: 'Hello' })).toBeNull(); expect(inferTypeFromArgs({})).toBeNull(); }); it('returns null for non-object args', () => { expect(inferTypeFromArgs('string')).toBeNull(); expect(inferTypeFromArgs(123)).toBeNull(); expect(inferTypeFromArgs(null)).toBeNull(); }); }); }); // ============================================================================ // Human approval from values (subjective judgment) // ============================================================================ describe('human approval from values (subjective judgment)', () => { describe('high-value transactions', () => { it('requires human approval for large amounts', () => { expect(requiresHumanApproval('processRefund', { amount: 50000 })).toBe(true); expect(requiresHumanApproval('processRefund', { amount: 12.99 })).toBe(false); }); it('threshold is $10,000', () => { expect(requiresHumanApproval('transfer', { amount: 9999 })).toBe(false); expect(requiresHumanApproval('transfer', { amount: 10001 })).toBe(true); }); }); describe('sensitive actions', () => { it('requires approval for destructive actions', () => { expect(requiresHumanApproval('performAction', { action: 'delete' })).toBe(true); expect(requiresHumanApproval('performAction', { action: 'terminate' })).toBe(true); expect(requiresHumanApproval('performAction', { action: 'cancel' })).toBe(true); }); it('does not require approval for safe actions', () => { expect(requiresHumanApproval('performAction', { action: 'read' })).toBe(false); expect(requiresHumanApproval('performAction', { action: 'list' })).toBe(false); }); }); describe('large scale operations', () => { it('requires approval for operations affecting many items', () => { expect(requiresHumanApproval('bulkOperation', { count: 5000 })).toBe(true); expect(requiresHumanApproval('bulkOperation', { count: 100 })).toBe(false); }); }); describe('mass communications', () => { it('requires approval for mass emails', () => { const manyRecipients = Array(500).fill('user@example.com'); expect(requiresHumanApproval('sendEmail', { recipients: manyRecipients })).toBe(true); const fewRecipients = ['user1@example.com', 'user2@example.com']; expect(requiresHumanApproval('sendEmail', { recipients: fewRecipients })).toBe(false); }); }); }); // ============================================================================ // Combined inference tests // ============================================================================ describe('combined function type inference', () => { it('prioritizes human approval based on values', () => { // Even though name suggests code, high amount triggers human expect(inferFunctionType('processRefund', { amount: 50000 })).toBe('human'); }); it('uses args when name is ambiguous', () => { expect(inferFunctionType('handleData', { max: 100 })).toBe('code'); expect(inferFunctionType('handleData', { url: 'https://api.com' })).toBe('agentic'); }); it('falls back to name when args are generic', () => { expect(inferFunctionType('fizzBuzz', { n: 100 })).toBe('code'); expect(inferFunctionType('storyBrand', { hero: 'developers' })).toBe('generative'); }); describe('README examples', () => { it('ai.fizzBuzz({ max: 100 }) → code', () => { expect(inferFunctionType('fizzBuzz', { max: 100 })).toBe('code'); }); it('ai.storyBrand({ hero: "developers" }) → generative', () => { expect(inferFunctionType('storyBrand', { hero: 'developers' })).toBe('generative'); }); it('ai.launchProductHunt({ product }) → agentic', () => { expect(inferFunctionType('launchProductHunt', { product: {} })).toBe('agentic'); }); it('ai.processRefund({ amount: 12.99 }) → generative (small amount, automated)', () => { // Small refund can be automated - falls through to generative // The key insight is it's NOT human (which would be triggered by high amounts) expect(inferFunctionType('processRefund', { amount: 12.99 })).toBe('generative'); }); it('ai.processRefund({ amount: 50000 }) → human (large amount)', () => { // Large refund needs human approval expect(inferFunctionType('processRefund', { amount: 50000 })).toBe('human'); }); }); }); // ============================================================================ // Magic proxy behavior tests // ============================================================================ describe('magic proxy ai.* behavior', () => { // Mock the magic proxy const mockDefine = vi.fn(); const mockExecute = vi.fn(); function createMockAiProxy() { return new Proxy({}, { get(_target, prop) { if (typeof prop !== 'string') return undefined; // Return a function that infers type and executes return (args) => { const type = inferFunctionType(prop, args); mockDefine(prop, type, args); return mockExecute(prop, type, args); }; }, }); } beforeEach(() => { mockDefine.mockReset(); mockExecute.mockReset(); }); it('infers type when accessing property', async () => { const ai = createMockAiProxy(); mockExecute.mockResolvedValue('1, 2, Fizz, 4, Buzz...'); await ai.fizzBuzz({ max: 100 }); expect(mockDefine).toHaveBeenCalledWith('fizzBuzz', 'code', { max: 100 }); }); it('creates new function for each call', async () => { const ai = createMockAiProxy(); mockExecute.mockResolvedValue('result'); await ai.functionA({ a: 1 }); await ai.functionB({ b: 2 }); expect(mockDefine).toHaveBeenCalledTimes(2); expect(mockDefine).toHaveBeenNthCalledWith(1, 'functionA', expect.any(String), { a: 1 }); expect(mockDefine).toHaveBeenNthCalledWith(2, 'functionB', expect.any(String), { b: 2 }); }); it('same function name can have different types based on args', async () => { const ai = createMockAiProxy(); mockExecute.mockResolvedValue('result'); // Same name, different args → different types await ai.process({ max: 100 }); // code (due to max) await ai.process({ url: 'https://api.com' }); // agentic (due to url) await ai.process({ amount: 50000 }); // human (due to high amount) expect(mockDefine).toHaveBeenNthCalledWith(1, 'process', 'code', { max: 100 }); expect(mockDefine).toHaveBeenNthCalledWith(2, 'process', 'agentic', { url: 'https://api.com' }); expect(mockDefine).toHaveBeenNthCalledWith(3, 'process', 'human', { amount: 50000 }); }); }); // ============================================================================ // Function type execution behavior // ============================================================================ describe('function type execution behavior', () => { it('documents expected behavior per type', () => { const typeBehaviors = { generative: { description: 'Generate content', examples: ['ai.storyBrand()', 'ai.blogPost()', 'ai.productDescription()'], returns: 'Generated content (text, JSON, etc.)', }, code: { description: 'Generate and execute code', examples: ['ai.fizzBuzz()', 'ai.validateEmail()', 'ai.sortArray()'], returns: 'Execution result', integration: 'Uses ai-sandbox evaluate()', }, agentic: { description: 'Multi-step tasks with tools', examples: ['ai.launchProductHunt()', 'ai.researchCompetitors()'], returns: 'Task completion result', features: ['Tool use', 'Multi-step execution', 'External integrations'], }, human: { description: 'Requires human input or approval', examples: ['ai.approveRefund()', 'ai.processRefund({ amount: 50000 })'], returns: 'Result after human approval', flow: ['Generate proposal', 'Wait for approval', 'Execute if approved'], }, }; expect(Object.keys(typeBehaviors)).toHaveLength(4); expect(typeBehaviors.code.integration).toBe('Uses ai-sandbox evaluate()'); }); });