@flavoai/fastfold
Version:
Flavo frontend package
231 lines (229 loc) • 8.22 kB
JavaScript
/**
* 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