UNPKG

ai-functions

Version:

Core AI primitives for building intelligent applications

617 lines 21.7 kB
/** * DigitalObjectsFunctionRegistry - Persistent function registry using digital-objects * * This implementation stores function definitions as Things and function calls as Actions, * providing full persistence and audit trail capabilities. * * Nouns: * - CodeFunction: Functions that generate executable code * - GenerativeFunction: Functions that use AI to generate content * - AgenticFunction: Functions that run in a loop with tools * - HumanFunction: Functions that require human input/approval * * Verbs: * - define: Function definition action * - call: Function invocation action * - complete: Successful completion action * - fail: Failed execution action * * @packageDocumentation */ import { getLogger } from './logger.js'; /** * Noun names for function types */ export const FUNCTION_NOUNS = { CODE: 'CodeFunction', GENERATIVE: 'GenerativeFunction', AGENTIC: 'AgenticFunction', HUMAN: 'HumanFunction', }; /** * Verb names for function actions */ export const FUNCTION_VERBS = { DEFINE: 'define', CALL: 'call', COMPLETE: 'complete', FAIL: 'fail', }; /** * Map function type to noun name */ function typeToNoun(type) { switch (type) { case 'code': return FUNCTION_NOUNS.CODE; case 'generative': return FUNCTION_NOUNS.GENERATIVE; case 'agentic': return FUNCTION_NOUNS.AGENTIC; case 'human': return FUNCTION_NOUNS.HUMAN; } } /** * Convert a FunctionDefinition to storable data */ function definitionToData(definition) { const base = { name: definition.name, type: definition.type, ...(definition.description !== undefined && { description: definition.description }), args: definition.args, ...(definition.returnType !== undefined && { returnType: definition.returnType }), }; switch (definition.type) { case 'code': { const codeDef = definition; // A `handler` is a live function reference and cannot be persisted; only // an inline `code` body round-trips. Definitions stored with a handler // (no `code`) must be re-supplied with their handler when reloaded. return { ...base, ...(codeDef.code !== undefined && { code: codeDef.code }), ...(codeDef.language !== undefined && { language: codeDef.language }), ...(codeDef.instructions !== undefined && { instructions: codeDef.instructions }), }; } case 'generative': { const genDef = definition; return { ...base, ...(genDef.output !== undefined && { output: genDef.output }), ...(genDef.system !== undefined && { system: genDef.system }), ...(genDef.promptTemplate !== undefined && { promptTemplate: genDef.promptTemplate }), ...(genDef.model !== undefined && { model: genDef.model }), ...(genDef.temperature !== undefined && { temperature: genDef.temperature }), }; } case 'agentic': { const agentDef = definition; return { ...base, instructions: agentDef.instructions, ...(agentDef.promptTemplate !== undefined && { promptTemplate: agentDef.promptTemplate }), ...(agentDef.tools !== undefined && { tools: agentDef.tools }), ...(agentDef.maxIterations !== undefined && { maxIterations: agentDef.maxIterations }), ...(agentDef.model !== undefined && { model: agentDef.model }), ...(agentDef.stream !== undefined && { stream: agentDef.stream }), }; } case 'human': { const humanDef = definition; return { ...base, ...(humanDef.channel !== undefined && { channel: humanDef.channel }), instructions: humanDef.instructions, ...(humanDef.promptTemplate !== undefined && { promptTemplate: humanDef.promptTemplate }), ...(humanDef.timeout !== undefined && { timeout: humanDef.timeout }), ...(humanDef.assignee !== undefined && { assignee: humanDef.assignee }), }; } } } /** * Convert stored data back to a FunctionDefinition */ function dataToDefinition(data) { switch (data.type) { case 'code': { const def = { type: 'code', name: data.name, args: data.args, }; if (data.description !== undefined) def.description = data.description; if (data.returnType !== undefined) def.returnType = data.returnType; if (data.code !== undefined) def.code = data.code; if (data.language !== undefined) def.language = data.language; if (data.instructions !== undefined) def.instructions = data.instructions; return def; } case 'generative': { const def = { type: 'generative', name: data.name, args: data.args, output: (data.output ?? 'string'), }; if (data.description !== undefined) def.description = data.description; if (data.returnType !== undefined) def.returnType = data.returnType; if (data.system !== undefined) def.system = data.system; if (data.promptTemplate !== undefined) def.promptTemplate = data.promptTemplate; if (data.model !== undefined) def.model = data.model; if (data.temperature !== undefined) def.temperature = data.temperature; return def; } case 'agentic': { const def = { type: 'agentic', name: data.name, args: data.args, instructions: data.instructions ?? '', }; if (data.description !== undefined) def.description = data.description; if (data.returnType !== undefined) def.returnType = data.returnType; if (data.promptTemplate !== undefined) def.promptTemplate = data.promptTemplate; if (data.tools !== undefined) def.tools = data.tools; if (data.maxIterations !== undefined) def.maxIterations = data.maxIterations; if (data.model !== undefined) def.model = data.model; if (data.stream !== undefined) def.stream = data.stream; return def; } case 'human': { const def = { type: 'human', name: data.name, args: data.args, channel: (data.channel ?? 'web'), instructions: data.instructions ?? '', }; if (data.description !== undefined) def.description = data.description; if (data.returnType !== undefined) def.returnType = data.returnType; if (data.promptTemplate !== undefined) def.promptTemplate = data.promptTemplate; if (data.timeout !== undefined) def.timeout = data.timeout; if (data.assignee !== undefined) def.assignee = data.assignee; return def; } } } /** * DigitalObjectsFunctionRegistry - Persistent function registry using digital-objects * * This class implements the FunctionRegistry interface using digital-objects for storage. * Function definitions are stored as Things, and function calls are tracked as Actions. * * @example * ```ts * import { createMemoryProvider } from 'digital-objects' * import { createDigitalObjectsRegistry, defineFunction } from 'ai-functions' * * const provider = createMemoryProvider() * const registry = await createDigitalObjectsRegistry({ provider }) * * // Define a function * const summarize = defineFunction({ * type: 'generative', * name: 'summarize', * args: { text: 'Text to summarize' }, * output: 'string', * }) * * // Store it in the registry * registry.set('summarize', summarize) * * // Later, retrieve it * const fn = registry.get('summarize') * if (fn) { * const result = await fn.call({ text: 'Long article...' }) * } * ``` */ export class DigitalObjectsFunctionRegistry { provider; initialized = false; autoInitialize; initPromise = null; // In-memory cache for DefinedFunction instances (they contain the call implementation) functionCache = new Map(); constructor(options) { this.provider = options.provider; this.autoInitialize = options.autoInitialize ?? true; } /** * Initialize the registry by defining all necessary nouns and verbs */ async initialize() { if (this.initialized) return; if (this.initPromise) return this.initPromise; this.initPromise = this._initialize(); await this.initPromise; this.initialized = true; } async _initialize() { // Define function type nouns await Promise.all([ this.provider.defineNoun({ name: FUNCTION_NOUNS.CODE, description: 'Function that generates executable code', schema: { name: 'string', description: 'string?', language: 'string?', instructions: 'string?', }, }), this.provider.defineNoun({ name: FUNCTION_NOUNS.GENERATIVE, description: 'Function that uses AI to generate content', schema: { name: 'string', description: 'string?', output: 'string', system: 'string?', promptTemplate: 'string?', }, }), this.provider.defineNoun({ name: FUNCTION_NOUNS.AGENTIC, description: 'Function that runs in a loop with tools', schema: { name: 'string', description: 'string?', instructions: 'string', maxIterations: 'number?', }, }), this.provider.defineNoun({ name: FUNCTION_NOUNS.HUMAN, description: 'Function that requires human input or approval', schema: { name: 'string', description: 'string?', channel: 'string', instructions: 'string', }, }), ]); // Define function action verbs await Promise.all([ this.provider.defineVerb({ name: FUNCTION_VERBS.DEFINE, description: 'Define a new function', }), this.provider.defineVerb({ name: FUNCTION_VERBS.CALL, description: 'Call/invoke a function', }), this.provider.defineVerb({ name: FUNCTION_VERBS.COMPLETE, description: 'Mark a function call as successfully completed', }), this.provider.defineVerb({ name: FUNCTION_VERBS.FAIL, description: 'Mark a function call as failed', }), ]); } /** * Ensure the registry is initialized before operations */ async ensureInitialized() { if (this.autoInitialize && !this.initialized) { await this.initialize(); } } /** * Get a function by name */ get(name) { // Return from cache if available return this.functionCache.get(name); } /** * Get a function by name (async version for loading from storage) */ async getAsync(name) { await this.ensureInitialized(); // Check cache first const cached = this.functionCache.get(name); if (cached) return cached; // Search across all function nouns for (const noun of Object.values(FUNCTION_NOUNS)) { const things = await this.provider.find(noun, { name }); const firstThing = things[0]; if (firstThing) { const definition = dataToDefinition(firstThing.data); // Note: The caller needs to provide the call implementation // This returns the definition info but not a callable function return { definition, call: async () => { throw new Error(`Function '${name}' was loaded from storage but has no call implementation. ` + 'Use defineFunction() to create a callable function.'); }, asTool: () => ({ name: definition.name, description: definition.description ?? `Execute ${definition.name}`, parameters: { type: 'object', properties: {}, required: [] }, handler: async () => { throw new Error('Function loaded from storage is not callable'); }, }), }; } } return undefined; } /** * Store a function definition */ set(name, fn) { // Store in cache for immediate access this.functionCache.set(name, fn); // Store in digital-objects asynchronously (fire and forget for sync interface) this.setAsync(name, fn).catch((err) => { getLogger().error(`Failed to persist function '${name}' to digital-objects:`, err); }); } /** * Store a function definition (async version) */ async setAsync(name, fn) { await this.ensureInitialized(); const definition = fn.definition; const noun = typeToNoun(definition.type); const data = definitionToData(definition); // Check if function already exists const existing = await this.provider.find(noun, { name }); const existingThing = existing[0]; let thing; if (existingThing) { // Update existing thing = await this.provider.update(existingThing.id, data); } else { // Create new thing = await this.provider.create(noun, data); // Record the define action await this.provider.perform(FUNCTION_VERBS.DEFINE, undefined, // subject (could be user ID in future) thing.id, { name, type: definition.type }); } // Update cache this.functionCache.set(name, fn); return thing; } /** * Check if a function exists */ has(name) { return this.functionCache.has(name); } /** * Check if a function exists (async version that also checks storage) */ async hasAsync(name) { if (this.functionCache.has(name)) return true; await this.ensureInitialized(); // Search across all function nouns for (const noun of Object.values(FUNCTION_NOUNS)) { const things = await this.provider.find(noun, { name }); if (things.length > 0) return true; } return false; } /** * List all function names */ list() { return Array.from(this.functionCache.keys()); } /** * List all function names (async version that includes storage) */ async listAsync() { await this.ensureInitialized(); const names = new Set(this.functionCache.keys()); // Get all functions from storage for (const noun of Object.values(FUNCTION_NOUNS)) { const things = await this.provider.list(noun); for (const thing of things) { names.add(thing.data.name); } } return Array.from(names); } /** * Delete a function */ delete(name) { const existed = this.functionCache.has(name); this.functionCache.delete(name); // Delete from storage asynchronously this.deleteAsync(name).catch((err) => { getLogger().error(`Failed to delete function '${name}' from digital-objects:`, err); }); return existed; } /** * Delete a function (async version) */ async deleteAsync(name) { await this.ensureInitialized(); this.functionCache.delete(name); // Search across all function nouns and delete for (const noun of Object.values(FUNCTION_NOUNS)) { const things = await this.provider.find(noun, { name }); for (const thing of things) { await this.provider.delete(thing.id); } if (things.length > 0) return true; } return false; } /** * Clear all functions */ clear() { this.functionCache.clear(); // Clear storage asynchronously this.clearAsync().catch((err) => { getLogger().error('Failed to clear functions from digital-objects:', err); }); } /** * Clear all functions (async version) */ async clearAsync() { await this.ensureInitialized(); this.functionCache.clear(); // Delete all functions from storage for (const noun of Object.values(FUNCTION_NOUNS)) { const things = await this.provider.list(noun); for (const thing of things) { await this.provider.delete(thing.id); } } } // ============================================================================ // Function Call Tracking (Actions) // ============================================================================ /** * Record a function call as an Action */ async trackCall(functionName, args) { await this.ensureInitialized(); // Find the function thing let functionId; for (const noun of Object.values(FUNCTION_NOUNS)) { const things = await this.provider.find(noun, { name: functionName, }); const firstThing = things[0]; if (firstThing) { functionId = firstThing.id; break; } } return this.provider.perform(FUNCTION_VERBS.CALL, undefined, // subject (caller) functionId, // object (the function) { args }); } /** * Record a successful function completion */ async trackCompletion(callActionId, result, duration) { await this.ensureInitialized(); const data = { args: undefined, result }; if (duration !== undefined) data.duration = duration; return this.provider.perform(FUNCTION_VERBS.COMPLETE, undefined, callActionId, data); } /** * Record a function failure */ async trackFailure(callActionId, error, duration) { await this.ensureInitialized(); const data = { args: undefined, error }; if (duration !== undefined) data.duration = duration; return this.provider.perform(FUNCTION_VERBS.FAIL, undefined, callActionId, data); } /** * Get call history for a function */ async getCallHistory(functionName) { await this.ensureInitialized(); // Find the function thing for (const noun of Object.values(FUNCTION_NOUNS)) { const things = await this.provider.find(noun, { name: functionName, }); const firstThing = things[0]; if (firstThing) { return this.provider.listActions({ verb: FUNCTION_VERBS.CALL, object: firstThing.id, }); } } return []; } /** * Get all recent function calls */ async getRecentCalls(limit = 10) { await this.ensureInitialized(); return this.provider.listActions({ verb: FUNCTION_VERBS.CALL, limit, }); } /** * Get the underlying provider for advanced operations */ getProvider() { return this.provider; } } /** * Create a DigitalObjectsFunctionRegistry * * @param options - Configuration options including the provider * @returns An initialized DigitalObjectsFunctionRegistry * * @example * ```ts * import { createMemoryProvider } from 'digital-objects' * import { createDigitalObjectsRegistry } from 'ai-functions' * * const provider = createMemoryProvider() * const registry = await createDigitalObjectsRegistry({ provider }) * * // Use the registry * registry.set('myFunc', definedFunction) * const fn = registry.get('myFunc') * ``` */ export async function createDigitalObjectsRegistry(options) { const registry = new DigitalObjectsFunctionRegistry(options); if (options.autoInitialize !== false) { await registry.initialize(); } return registry; } //# sourceMappingURL=digital-objects-registry.js.map