UNPKG

@flavoai/fastfold

Version:

Flavo frontend package

231 lines (229 loc) 8.22 kB
/** * FastFold AI Hooks * Automatic AI transformations for CRUD operations */ import { generateText, embed } from 'ai'; import { createOpenAI } from '@ai-sdk/openai'; import { createAnthropic } from '@ai-sdk/anthropic'; import { createGoogleGenerativeAI } from '@ai-sdk/google'; import { aiLogger } from '../lib/aiLogger'; // ============ Provider Helpers ============ function getProviderConfig(provider, config) { switch (provider) { case 'openai': return { apiKey: config?.providers?.openai?.apiKey || process.env.OPENAI_API_KEY, baseURL: config?.providers?.openai?.baseURL || process.env.OPENAI_BASE_URL, }; case 'anthropic': return { apiKey: config?.providers?.anthropic?.apiKey || process.env.ANTHROPIC_API_KEY, }; case 'google': return { apiKey: config?.providers?.google?.apiKey || process.env.GOOGLE_GENERATIVE_AI_API_KEY, }; default: return { apiKey: process.env.OPENAI_API_KEY }; } } function getProvider(providerName, config) { const providerConfig = getProviderConfig(providerName, config); switch (providerName) { case 'openai': return createOpenAI({ apiKey: providerConfig.apiKey, baseURL: providerConfig.baseURL, }); case 'anthropic': return createAnthropic({ apiKey: providerConfig.apiKey, }); case 'google': return createGoogleGenerativeAI({ apiKey: providerConfig.apiKey, }); default: return createOpenAI({ apiKey: providerConfig.apiKey, }); } } /** * Get fast/cheap model for hooks (embeddings, summarization, classification) * Hooks use fast models since they run on every CRUD operation * User-specified models in hook config ALWAYS take priority */ function getDefaultModel(provider) { switch (provider) { case 'openai': return 'gpt-4o-mini'; case 'anthropic': return 'claude-haiku-4-5-20260101'; case 'google': return 'gemini-3-flash'; default: return 'gpt-4o-mini'; } } function getEmbeddingModel(provider) { switch (provider) { case 'openai': return 'text-embedding-3-small'; case 'google': return 'text-embedding-004'; default: return 'text-embedding-3-small'; } } // ============ AI Hooks Processor ============ export class AIHooksProcessor { aiHooks; aiConfig; defaultProvider; constructor(aiHooks, aiConfig) { this.aiHooks = aiHooks; this.aiConfig = aiConfig; this.defaultProvider = aiConfig?.defaultProvider || 'openai'; } /** * Process AI hooks before create/update */ async processBeforeWrite(tableName, data, context) { let processedData = { ...data }; // Process embeddings if (this.aiHooks.embeddings?.[tableName]) { processedData = await this.processEmbeddings(tableName, processedData); } // Process summarization if (this.aiHooks.summarize?.[tableName]) { processedData = await this.processSummarize(tableName, processedData); } // Process classification if (this.aiHooks.classify?.[tableName]) { processedData = await this.processClassify(tableName, processedData); } // Process custom transforms if (this.aiHooks.transform?.[tableName]) { processedData = await this.aiHooks.transform[tableName](processedData, context); } return processedData; } /** * Generate embeddings for specified fields */ async processEmbeddings(tableName, data) { const config = this.aiHooks.embeddings[tableName]; const provider = config.provider || this.defaultProvider; // Combine source fields into a single text const sourceText = config.sourceFields .map(field => data[field]) .filter(Boolean) .join(' '); if (!sourceText) { return data; } try { const providerInstance = getProvider(provider, this.aiConfig); const modelName = config.model || getEmbeddingModel(provider); const embeddingModel = providerInstance.embeddingModel(modelName); const result = await embed({ model: embeddingModel, value: sourceText, }); return { ...data, [config.targetField]: result.embedding, }; } catch (error) { aiLogger.logHook('embeddings', tableName, false, error.message); return data; } } /** * Generate summary for specified field */ async processSummarize(tableName, data) { const config = this.aiHooks.summarize[tableName]; const provider = config.provider || this.defaultProvider; const sourceContent = data[config.sourceField]; if (!sourceContent) { return data; } try { const providerInstance = getProvider(provider, this.aiConfig); const modelName = config.model || getDefaultModel(provider); const model = providerInstance(modelName); const maxLength = config.maxLength || 200; const result = await generateText({ model, prompt: `Summarize the following text in ${maxLength} characters or less. Be concise and capture the key points:\n\n${sourceContent}`, maxOutputTokens: Math.ceil(maxLength / 3), // Rough estimate }); return { ...data, [config.targetField]: result.text.trim(), }; } catch (error) { aiLogger.logHook('summarize', tableName, false, error.message); return data; } } /** * Classify content into categories */ async processClassify(tableName, data) { const config = this.aiHooks.classify[tableName]; const provider = config.provider || this.defaultProvider; const sourceContent = data[config.sourceField]; if (!sourceContent) { return data; } try { const providerInstance = getProvider(provider, this.aiConfig); const modelName = config.model || getDefaultModel(provider); const model = providerInstance(modelName); const categories = config.categories.join(', '); const result = await generateText({ model, prompt: `Classify the following text into exactly one of these categories: ${categories} Text: ${sourceContent} Respond with only the category name, nothing else.`, maxOutputTokens: 50, }); const classification = result.text.trim(); // Validate the classification is one of the allowed categories const validCategory = config.categories.find(cat => cat.toLowerCase() === classification.toLowerCase()); return { ...data, [config.targetField]: validCategory || config.categories[0], }; } catch (error) { aiLogger.logHook('classify', tableName, false, error.message); return data; } } /** * Check if table has any AI hooks configured */ hasHooksForTable(tableName) { return !!(this.aiHooks.embeddings?.[tableName] || this.aiHooks.summarize?.[tableName] || this.aiHooks.classify?.[tableName] || this.aiHooks.transform?.[tableName]); } } /** * Create an AI hooks processor instance */ export function createAIHooksProcessor(aiHooks, aiConfig) { if (!aiHooks || Object.keys(aiHooks).length === 0) { return null; } return new AIHooksProcessor(aiHooks, aiConfig); } export default AIHooksProcessor; //# sourceMappingURL=hooks.js.map