UNPKG

ai-functions

Version:

Core AI primitives for building intelligent applications

861 lines (855 loc) 30.9 kB
/** * AI Function Primitives with Promise Pipelining * * All functions return AIPromise for: * - Dynamic schema inference from destructuring * - Promise pipelining without await * - Magical .map() for batch processing * - Dependency graph resolution * * @example * ```ts * // No await needed until the end! * const { summary, keyPoints, conclusion } = ai`write about ${topic}` * const isValid = is`${conclusion} is solid given ${keyPoints}` * const improved = ai`improve ${conclusion} using ${keyPoints}` * * // Batch processing with map * const ideas = list`startup ideas` * const evaluated = await ideas.map(idea => ({ * idea, * viable: is`${idea} is viable`, * market: ai`market size for ${idea}`, * })) * * // Only await at the end * if (await isValid) { * console.log(await improved) * } * ``` * * @packageDocumentation */ import { AIPromise, createAITemplateFunction, parseTemplateWithDependencies, isAIPromise, } from './ai-promise.js'; import { generateObject, generateText } from './generate.js'; import { createDefinedFunction, defineFunction, functions, generateCode, } from './function-registry.js'; // ============================================================================ // Core generate() primitive // ============================================================================ /** * Core generate primitive - all other functions use this under the hood */ export async function generate(type, prompt, options) { const { model = 'sonnet', schema, language, format, slides: slideCount, ...rest } = options || {}; switch (type) { case 'text': case 'markdown': return generateTextContent(prompt, model, rest); case 'json': return generateJsonContent(prompt, model, schema, rest); case 'code': return generateCodeContent(prompt, model, language || 'typescript', rest); case 'list': return generateListContent(prompt, model, rest); case 'lists': return generateListsContent(prompt, model, rest); case 'boolean': return generateBooleanContent(prompt, model, rest); case 'summary': return generateSummaryContent(prompt, model, rest); case 'extract': return generateExtractContent(prompt, model, schema, rest); case 'yaml': return generateYamlContent(prompt, model, rest); case 'diagram': return generateDiagramContent(prompt, model, format || 'mermaid', rest); case 'slides': return generateSlidesContent(prompt, model, slideCount || 10, rest); default: throw new Error(`Unknown generate type: ${type}`); } } // Helper functions async function generateTextContent(prompt, model, options) { const result = await generateText({ model, prompt, ...(options.system !== undefined && { system: options.system }), ...(options.temperature !== undefined && { temperature: options.temperature }), ...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }), }); return result.text; } async function generateJsonContent(prompt, model, schema, options) { const effectiveSchema = schema || { result: 'The generated result' }; const result = await generateObject({ model, schema: effectiveSchema, prompt, ...(options.system !== undefined && { system: options.system }), ...(options.temperature !== undefined && { temperature: options.temperature }), ...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }), }); return result.object; } async function generateCodeContent(prompt, model, language, options) { const result = await generateObject({ model, schema: { code: `The ${language} implementation code` }, prompt: `Generate ${language} code for: ${prompt}`, system: `You are an expert ${language} developer. Generate clean, well-documented code.`, ...(options.temperature !== undefined && { temperature: options.temperature }), ...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }), }); return result.object.code; } async function generateListContent(prompt, model, options) { const result = await generateObject({ model, schema: { items: ['List items'] }, prompt, system: options.system || 'Generate a list of items based on the prompt.', ...(options.temperature !== undefined && { temperature: options.temperature }), ...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }), }); return result.object.items; } async function generateListsContent(prompt, model, options) { const result = await generateObject({ model, schema: { categories: ['Category names as strings'], data: 'JSON string containing the categorized lists', }, prompt: `Generate categorized lists for: ${prompt}\n\nFirst identify appropriate category names, then provide the lists as a JSON object.`, system: options.system || 'Generate multiple categorized lists. Determine appropriate categories based on the prompt.', ...(options.temperature !== undefined && { temperature: options.temperature }), ...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }), }); const obj = result.object; try { return JSON.parse(obj.data); } catch { const lists = {}; for (const cat of obj.categories || []) { lists[cat] = []; } return lists; } } async function generateBooleanContent(prompt, model, options) { const result = await generateObject({ model, schema: { answer: 'true | false' }, prompt, system: options.system || 'Answer the question with true or false.', temperature: options.temperature ?? 0, ...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }), }); return result.object.answer === 'true'; } async function generateSummaryContent(prompt, model, options) { const result = await generateObject({ model, schema: { summary: 'A concise summary of the content' }, prompt: `Summarize the following:\n\n${prompt}`, system: options.system || 'Create a clear, concise summary.', ...(options.temperature !== undefined && { temperature: options.temperature }), ...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }), }); return result.object.summary; } async function generateExtractContent(prompt, model, schema, options) { const effectiveSchema = schema || { items: ['Array of extracted items as strings - extract ALL matching items from the text'], }; const result = await generateObject({ model, schema: effectiveSchema, prompt: `Extract the following from the text below. Return ALL matching items in the items array. Task: ${prompt} IMPORTANT: Return the extracted items as an array. If the task asks for email addresses, return all email addresses found. If it asks for names, return all names found. Do not return an empty array if there are items to extract.`, system: options.system || 'You are a precise data extraction assistant. Extract exactly what is requested and return it as an array of items. Be thorough - find ALL matching items in the text.', temperature: options.temperature ?? 0, // Use low temperature for extraction tasks ...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }), }); const obj = result.object; if ('items' in obj && Array.isArray(obj['items'])) { return obj['items']; } return Object.values(obj).flat(); } async function generateYamlContent(prompt, model, options) { const result = await generateObject({ model, schema: { yaml: 'The YAML content' }, prompt: `Generate YAML for: ${prompt}`, system: options.system || 'Generate valid YAML content.', ...(options.temperature !== undefined && { temperature: options.temperature }), ...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }), }); return result.object.yaml; } async function generateDiagramContent(prompt, model, format, options) { const result = await generateObject({ model, schema: { diagram: `The ${format} diagram code` }, prompt: `Generate a ${format} diagram for: ${prompt}`, system: options.system || `Generate ${format} diagram syntax.`, ...(options.temperature !== undefined && { temperature: options.temperature }), ...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }), }); return result.object.diagram; } async function generateSlidesContent(prompt, model, slideCount, options) { const result = await generateObject({ model, schema: { slides: `Slidev/Marp markdown with ${slideCount} slides` }, prompt: `Generate a ${slideCount}-slide presentation about: ${prompt}`, system: options.system || 'Generate markdown slides in Slidev/Marp format.', ...(options.temperature !== undefined && { temperature: options.temperature }), ...(options.maxTokens !== undefined && { maxTokens: options.maxTokens }), }); return result.object.slides; } // ============================================================================ // AIPromise-based Functions // ============================================================================ /** * General-purpose AI function with dynamic schema inference * * @example * ```ts * // Simple text generation * const text = await ai`write a poem about ${topic}` * * // Dynamic schema from destructuring - no await needed! * const { summary, keyPoints, conclusion } = ai`write about ${topic}` * console.log(await summary) * * // Chain with other functions * const isValid = is`${conclusion} is solid` * const improved = ai`improve ${conclusion}` * ``` */ export const ai = createAITemplateFunction('object'); /** * Generate text content * * @example * ```ts * const post = await write`blog post about ${topic}` * ``` */ export const write = createAITemplateFunction('text'); /** * Generate code * * @example * ```ts * const code = await code`email validation function` * ``` */ export const code = createAITemplateFunction('text', { system: 'You are an expert programmer. Generate clean, well-documented code.', }); /** * Generate a list of items with .map() support * * @example * ```ts * // Simple list * const ideas = await list`startup ideas` * * // With map - batch processes in ONE call! * const evaluated = await list`startup ideas`.map(idea => ({ * idea, * viable: is`${idea} is viable`, * market: ai`market size for ${idea}`, * })) * * // Async iteration * for await (const idea of list`startup ideas`) { * console.log(idea) * } * ``` */ export const list = createAITemplateFunction('list'); /** * Generate multiple named lists with dynamic schema * * @example * ```ts * // Destructuring infers the schema! * const { pros, cons } = await lists`pros and cons of ${topic}` * * // No await - pipeline with other functions * const { benefits, risks, costs } = lists`analysis of ${project}` * const summary = ai`summarize: benefits=${benefits}, risks=${risks}` * console.log(await summary) * ``` */ export const lists = createAITemplateFunction('lists'); /** * Extract structured data with dynamic schema * * @example * ```ts * // Dynamic schema from destructuring * const { name, email, phone } = await extract`contact info from ${document}` * * // As array * const emails = await extract`email addresses from ${text}` * ``` */ export const extract = createAITemplateFunction('extract'); /** * Summarize text * * @example * ```ts * const summary = await summarize`${longArticle}` * ``` */ export const summarize = createAITemplateFunction('text', { system: 'Create a clear, concise summary.', }); /** * Check if something is true/false * * @example * ```ts * // Simple check * const isColor = await is`${topic} a color` * * // Pipeline - no await needed! * const { conclusion } = ai`write about ${topic}` * const isValid = is`${conclusion} is well-argued` * if (await isValid) { ... } * ``` */ export const is = createAITemplateFunction('boolean'); /** * Generate a diagram * * @example * ```ts * const diagram = await diagram`user authentication flow` * ``` */ export const diagram = createAITemplateFunction('text', { system: 'Generate a Mermaid diagram.', }); /** * Generate presentation slides * * @example * ```ts * const slides = await slides`quarterly review` * ``` */ export const slides = createAITemplateFunction('text', { system: 'Generate markdown slides in Slidev/Marp format.', }); /** * Generate an image */ export const image = createAITemplateFunction('text'); /** * Generate a video */ export const video = createAITemplateFunction('text'); // ============================================================================ // Agentic Functions // ============================================================================ /** * Execute a task * * @example * ```ts * const { summary, actions } = await do`send welcome email to ${user}` * ``` */ function doImpl(promptOrStrings, ...args) { let prompt; let dependencies = []; if (Array.isArray(promptOrStrings) && 'raw' in promptOrStrings) { const parsed = parseTemplateWithDependencies(promptOrStrings, ...args); prompt = parsed.prompt; dependencies = parsed.dependencies; } else { prompt = promptOrStrings; } const promise = new AIPromise(prompt, { type: 'object', baseSchema: { summary: 'Summary of what was done', actions: ['List of actions taken'], }, system: 'You are a task executor. Describe what actions you would take.', }); for (const dep of dependencies) { promise.addDependency(dep.promise, dep.path); } return promise; } export { doImpl as do }; /** * Conduct research on a topic * * @example * ```ts * const { summary, findings, sources } = await research`${competitor} vs our product` * ``` */ export const research = createAITemplateFunction('object', { system: 'You are a research analyst. Provide thorough research.', }); // ============================================================================ // Web Functions // ============================================================================ /** * Read a URL and convert to markdown */ export const read = createAITemplateFunction('text'); /** * Browse a URL with browser automation * * @experimental This function is experimental and returns mock data. * The actual implementation will use Stagehand or Playwright for browser automation. * Do not rely on this function in production code until it is fully implemented. * * @param urlOrStrings - URL string or template literal * @param args - Template literal values * @returns Browser automation interface with do, extract, screenshot, and close methods * * @example * ```ts * const browser = await browse`https://example.com` * await browser.do('click the login button') * const data = await browser.extract('user profile information') * const screenshot = await browser.screenshot() * await browser.close() * ``` */ export async function browse(urlOrStrings, ...args) { // EXPERIMENTAL: This is a placeholder implementation returning mock data. // Actual implementation would use Stagehand or Playwright for browser automation. return { do: async () => { }, extract: async () => ({}), screenshot: async () => Buffer.from('screenshot'), close: async () => { }, }; } // ============================================================================ // Decision Functions // ============================================================================ /** * LLM as judge - compare options and pick the best * * @example * ```ts * const winner = await decide`higher click-through rate`(headlineA, headlineB) * ``` */ export function decide(criteriaOrStrings, ...templateArgs) { let criteria; if (Array.isArray(criteriaOrStrings) && 'raw' in criteriaOrStrings) { criteria = criteriaOrStrings.reduce((acc, str, i) => acc + str + (templateArgs[i] ?? ''), ''); } else { criteria = criteriaOrStrings; } return (...options) => { const optionDescriptions = options .map((opt, i) => `Option ${i + 1}: ${JSON.stringify(opt)}`) .join('\n'); const promise = new AIPromise(`Given these options:\n${optionDescriptions}\n\nChoose the best option based on: ${criteria}`, { type: 'object', baseSchema: { chosenIndex: 'The index (1-based) of the best option as a number', reasoning: 'Brief explanation of why this option is best', }, }); // Override resolve to return the actual option const originalResolve = promise.resolve.bind(promise); promise.resolve = async () => { const result = (await originalResolve()); const index = typeof result.chosenIndex === 'string' ? parseInt(result.chosenIndex, 10) : result.chosenIndex; return options[index - 1]; }; return promise; }; } /** * Ask a human for input */ export const ask = createAITemplateFunction('object', { system: 'Generate content for human interaction.', }); /** * Request human approval */ export const approve = createAITemplateFunction('object', { system: 'Generate an approval request.', }); /** * Request human review */ export const review = createAITemplateFunction('object', { system: 'Generate a review request.', }); // ============================================================================ // Auto-Define Functions // // Inlined from former ai-proxy.ts. These helpers provide the auto-define // convenience layer (analyze a name + example args, infer a function type and // schema, register it). Property-access tracking lives entirely in // ai-promise.ts; this section is a shallow dispatch over define() and // generateObject(). // ============================================================================ /** * Analyze a function call and determine what type of function it should be */ async function analyzeFunction(name, args) { // Convert camelCase/snake_case to readable name const readableName = name .replace(/([A-Z])/g, ' $1') .replace(/_/g, ' ') .toLowerCase() .trim(); const argDescriptions = Object.entries(args) .map(([key, value]) => { const type = Array.isArray(value) ? 'array' : typeof value; return ` - ${key}: ${type} (example: ${JSON.stringify(value).slice(0, 50)})`; }) .join('\n'); const result = await generateObject({ model: 'sonnet', schema: { type: 'code | generative | agentic | human', reasoning: 'Why this function type is appropriate (1-2 sentences)', description: 'What this function does', output: 'string | object | image | video | audio', returnType: 'Schema for the return type as a SimpleSchema object', system: 'System prompt for the AI (if generative/agentic)', promptTemplate: 'Prompt template with {{arg}} placeholders', instructions: 'Instructions for agentic/human functions', needsTools: 'true | false', suggestedTools: ['Names of tools that might be needed'], channel: 'slack | email | web | sms | custom', }, system: `You are an expert at designing AI functions. Analyze the function name and arguments to determine the best function type. Function Types: - "code": A DETERMINISTIC algorithm with no model at run time — a pure calculation, data transformation, or rule that always produces the same output for the same input (e.g. tax math, parsing, formatting). A self-contained TypeScript body can be written for it once. - "generative": For generating content (text, summaries, translations, creative writing, structured data) — needs a model on every call - "agentic": For complex tasks requiring multiple steps, research, or tool use (research, planning, multi-step workflows) - "human": For tasks requiring human judgment, approval, or input (approvals, reviews, decisions) Guidelines: - Most functions should be "generative" - they generate content or structured data - Use "code" ONLY when the task is a deterministic algorithm expressible as a self-contained function body (no model, no external state). If it needs a model at run time, it is "generative", not "code". - Use "agentic" when the task requires research, multiple steps, or external tool use - Use "human" when human judgment/approval is essential`, prompt: `Analyze this function call and determine how to define it: Function Name: ${name} Readable Name: ${readableName} Arguments: ${argDescriptions || ' (no arguments)'} Determine: 1. What type of function this should be 2. What it should return 3. How it should be implemented`, }); const analysis = result.object; // Build the function definition based on the analysis let definition; const baseDefinition = { name, description: analysis.description, args: inferArgsSchema(args), returnType: analysis.returnType, }; switch (analysis.type) { case 'code': { // `Code` is deterministic — it cannot be a call-time model invocation. // Author a self-contained TS body ONCE here (at define time), then carry // it as an inline `code` body so every subsequent call is deterministic. const authored = await generateCode({ name, description: analysis.description, args: baseDefinition.args, returnType: baseDefinition.returnType, language: 'typescript', instructions: `${analysis.instructions ?? ''}\n\nWrite the body of a single function that receives a parameter named \`args\` (an object with the keys above) and \`return\`s the result. Do not include the function signature, imports, or surrounding declarations — only the statements that go inside the function body.`.trim(), }, args); definition = { ...baseDefinition, type: 'code', language: 'typescript', instructions: analysis.instructions, code: authored, }; break; } case 'agentic': definition = { ...baseDefinition, type: 'agentic', instructions: analysis.instructions || `Complete the ${readableName} task`, promptTemplate: analysis.promptTemplate, tools: [], // Tools would need to be provided separately maxIterations: 10, }; break; case 'human': definition = { ...baseDefinition, type: 'human', channel: (analysis.channel || 'web'), instructions: analysis.instructions || `Please review and respond to this ${readableName} request`, promptTemplate: analysis.promptTemplate, }; break; case 'generative': default: definition = { ...baseDefinition, type: 'generative', output: (analysis.output || 'object'), system: analysis.system, promptTemplate: analysis.promptTemplate || `{{${Object.keys(args)[0] || 'input'}}}`, }; break; } return { type: analysis.type, reasoning: analysis.reasoning, definition, }; } /** * Infer a schema from example arguments */ function inferArgsSchema(args) { const schema = {}; for (const [key, value] of Object.entries(args)) { if (typeof value === 'string') { schema[key] = `The ${key.replace(/([A-Z])/g, ' $1').toLowerCase()}`; } else if (typeof value === 'number') { schema[key] = `The ${key.replace(/([A-Z])/g, ' $1').toLowerCase()} (number)`; } else if (typeof value === 'boolean') { schema[key] = `Whether ${key.replace(/([A-Z])/g, ' $1').toLowerCase()} (boolean)`; } else if (Array.isArray(value)) { if (value.length > 0 && typeof value[0] === 'string') { schema[key] = [`List of ${key.replace(/([A-Z])/g, ' $1').toLowerCase()}`]; } else { schema[key] = [`Items for ${key.replace(/([A-Z])/g, ' $1').toLowerCase()}`]; } } else if (typeof value === 'object' && value !== null) { schema[key] = inferArgsSchema(value); } else { schema[key] = `The ${key.replace(/([A-Z])/g, ' $1').toLowerCase()}`; } } return schema; } /** * Auto-define a function based on its name and arguments, or define with explicit definition * * When called with (name, args), uses AI to analyze and determine: * - What type of function it should be (code, generative, agentic, human) * - What it should return * - How it should be implemented * * When called with a FunctionDefinition, creates the function directly. * * @example * ```ts * // Auto-define from name and example args * const planTrip = await define('planTrip', { destination: 'Tokyo', travelers: 2 }) * * // Or define explicitly * const summarize = define.generative({ * name: 'summarize', * args: { text: 'Text to summarize' }, * output: 'string', * }) * * // Or with full definition * const fn = defineFunction({ * type: 'generative', * name: 'translate', * args: { text: 'Text', lang: 'Target language' }, * output: 'string', * }) * ``` */ async function autoDefineImpl(name, args) { // Check if already defined const existing = functions.get(name); if (existing) { return existing; } // Analyze and define the function const { definition } = await analyzeFunction(name, args); // Create the defined function const definedFn = createDefinedFunction(definition); // Store in registry functions.set(name, definedFn); return definedFn; } /** * Define functions - auto-define or use typed helpers */ export const define = Object.assign(autoDefineImpl, { /** * Define a **deterministic** code function (a handler — no LLM at call time). * * Supply a `handler` (canonical) or an inline `code` body. To have a model * *author* code instead, use {@link generateCode} or `define.generative`. */ code: (definition) => { const fn = defineFunction({ type: 'code', ...definition }); functions.set(definition.name, fn); return fn; }, /** * Define a generative function */ generative: (definition) => { const fn = defineFunction({ type: 'generative', ...definition }); functions.set(definition.name, fn); return fn; }, /** * Define an agentic function */ agentic: (definition) => { const fn = defineFunction({ type: 'agentic', ...definition }); functions.set(definition.name, fn); return fn; }, /** * Define a human-in-the-loop function */ human: (definition) => { const fn = defineFunction({ type: 'human', ...definition }); functions.set(definition.name, fn); return fn; }, }); // ============================================================================ // AI Proxy - Smart AI Client with Auto-Definition // ============================================================================ /** Known built-in method names that should not be auto-defined */ const BUILTIN_METHODS = new Set([ 'do', 'is', 'code', 'decide', 'diagram', 'generate', 'image', 'video', 'write', 'list', 'lists', 'functions', 'define', 'defineFunction', 'then', 'catch', 'finally', ]); /** * Create a smart AI client that auto-defines functions on first call * * @example * ```ts * const ai = createSmartAI() * * // First call - auto-defines the function * const trip = await ai.planTrip({ * destination: 'Tokyo', * dates: { start: '2024-03-01', end: '2024-03-10' }, * travelers: 2, * }) * * // Second call - uses cached definition (in-memory) * const trip2 = await ai.planTrip({ * destination: 'Paris', * dates: { start: '2024-06-01', end: '2024-06-07' }, * travelers: 4, * }) * * // Access registry and define * console.log(ai.functions.list()) // ['planTrip'] * ai.define.generative({ name: 'summarize', ... }) * ``` */ export function createSmartAI() { const base = { functions, define, defineFunction, }; return new Proxy(base, { get(target, prop) { // Return built-in properties if (prop in target) { return target[prop]; } // Skip internal properties if (typeof prop === 'symbol' || prop.startsWith('_') || BUILTIN_METHODS.has(prop)) { return undefined; } // Return a function that auto-defines and calls return async (args = {}) => { // Check if function is already defined let fn = functions.get(prop); if (!fn) { // Auto-define the function fn = await define(prop, args); } // Call the function return fn.call(args); }; }, }); } /** * Default AI proxy instance with auto-define capability. * * This is the smart proxy `aiProxy` (re-exported from index.ts as `aiProxy`). * It is intentionally distinct from the `ai` template-tag primitive above — * see `ai` (line ~354) for the template function used like `ai\`...\``. * * @example * ```ts * import { aiProxy } from 'ai-functions' * * // Auto-define and call * const result = await aiProxy.summarize({ text: 'Long article...' }) * * // Access functions registry * aiProxy.functions.list() * * // Define explicitly * aiProxy.define.generative({ name: 'translate', ... }) * ``` */ export const aiProxy = createSmartAI(); //# sourceMappingURL=primitives.js.map