@flowlab/all
Version:
A cool library focusing on handling various flows
185 lines (163 loc) • 7.96 kB
text/typescript
import {
BaseNode, INodeContext, INodeOutput, NodeStatus, IWorkflowContext,
IScheduler, // Import Scheduler interface from core
WorkflowError
} from '@flowlab/core';
import { AIProviderRegistry, IAIProvider, AIModelOptions } from '../types';
import { aiProviderRegistry } from '../providerRegistry'; // Use the singleton registry
// MARK: 基础AI节点输入
// Define standard input properties expected by AI nodes
export interface BaseAINodeInput {
providerName: string; // Name of the AI provider (e.g., 'openai')
promptTemplate?: string; // Template string for prompts
modelOptions?: AIModelOptions; // Overrides for model parameters
providerConfig?: object; // Override provider config for this node
inputVariables?: Record<string, string>; // Explicit mapping from context vars to template vars
scheduleTask?: boolean; // Flag to request scheduling via core scheduler
scheduleOptions?: any; // Options for the scheduler (delay, priority etc.)
}
// MARK: 基础AI节点
export abstract class BaseAINode extends BaseNode {
// Access the shared registry
protected registry: AIProviderRegistry = aiProviderRegistry;
// Scheduler instance, potentially injected via executor options
protected scheduler?: IScheduler;
constructor(scheduler?: IScheduler) { // Accept scheduler optionally
super();
this.scheduler = scheduler;
}
// MARK: 执行AI
// Abstract method for the core AI logic
protected abstract executeAI(context: INodeContext, provider: IAIProvider, renderedInput: any): Promise<Record<string, any>>;
// MARK: 执行
async execute(context: INodeContext): Promise<INodeOutput> {
const nodeInput = context.input as BaseAINodeInput;
// --- Scheduling Logic ---
if (nodeInput.scheduleTask === true) {
if (!this.scheduler) {
context.logs.push('Warning: scheduleTask=true but no scheduler configured/available.');
} else {
try {
// NOTE: Scheduling requires context serialization and a mechanism
// for the worker to report back. This is a complex topic involving
// potentially durable workflows and state persistence.
// Here, we just initiate the scheduling. FlowLab core needs to handle the rest.
const taskId = await this.scheduler.scheduleNode(
this.id, // Schedule this node type
{ // Pass necessary context parts (needs careful selection/serialization)
...context,
input: context.input, // Ensure input is passed
workflowContext: { // Only pass essential workflow context if needed
workflowId: context.workflowContext.workflowId,
variables: context.workflowContext.variables,
// Potentially tenantId, userId if needed for scheduled task logic
},
// Remove potentially non-serializable parts or large objects if needed
},
nodeInput.scheduleOptions || {}
);
// Indicate the task is scheduled and waiting for the external worker
return {
status: NodeStatus.PENDING, // Or a custom 'SCHEDULED' status if core supports it
output: { scheduledTaskId: taskId }
};
} catch (error: any) {
context.logs.push(`Error scheduling AI task: ${error.message}`);
context.error = new WorkflowError(`Failed to schedule AI task: ${error.message}`);
return { status: NodeStatus.FAILED, output: { error: context.error.message } };
}
}
}
// MARK: 直接执行
try {
const provider = this.registry.getProvider(nodeInput.providerName, nodeInput.providerConfig);
// Render input (e.g., prompt template)
const renderedInput = this.renderInput(nodeInput, context);
// Delegate to specific AI execution
const aiResult = await this.executeAI(context, provider, renderedInput);
// Standardize output structure slightly
return {
status: NodeStatus.COMPLETED,
output: {
...aiResult, // Include AI-specific results (text, data, usage etc.)
provider: nodeInput.providerName
}
};
} catch (error: any) {
context.logs.push(`AI Node execution failed: ${error.message}`);
context.error = error instanceof Error ? error : new NodeExecutionError(String(error), this.id, context.stepId);
// TODO: Map specific AI provider errors (e.g., rate limit, content filter) if possible
return {
status: NodeStatus.FAILED,
output: { error: context.error.message, provider: nodeInput.providerName }
};
}
}
// MARK: 输入渲染(e.g., Prompt Templating)
protected renderInput(nodeInput: BaseAINodeInput, context: INodeContext): any {
if (nodeInput.promptTemplate) {
return this.renderPromptTemplate(nodeInput.promptTemplate, nodeInput.inputVariables || {}, context);
}
// If no template, pass other relevant inputs directly (needs refinement based on node type)
return { ...nodeInput };
}
// MARK: 简单模板引擎(replace with a more robust one if needed)
protected renderPromptTemplate(template: string, mapping: Record<string, string>, context: INodeContext): string {
let rendered = template;
const combinedContext = {
...context.workflowContext.variables, // workflow variables
...context.input // Step input (use mapping if provided)
};
// Priority 1: Explicit mapping from inputVariables
for (const templateVar in mapping) {
const contextPath = mapping[templateVar]; // e.g., "workflow.variables.someVar" or "input.someInput"
const value = this.resolveContextPath(contextPath, context);
const regex = new RegExp(`\\$\\{${templateVar}\\}`, 'g');
rendered = rendered.replace(regex, String(value ?? ''));
}
// MARK: 优先2:直接上下文访问(简单变量)
// Matches ${workflow.variables.varName} or ${input.varName}
rendered = rendered.replace(/\$\{(workflow\.variables|input)\.(\w+)\}/g, (match, scope, varName) => {
const contextPath = `${scope}.${varName}`;
if (!mapping || !Object.values(mapping).includes(contextPath)) { // Avoid double replacement
const value = this.resolveContextPath(contextPath, context);
return String(value ?? '');
}
return match; // Already replaced by mapping
});
// Log the rendered prompt for debugging (optional)
context.logs.push(`Rendered Prompt: ${rendered.substring(0, 100)}...`);
return rendered;
}
// MARK: 解析简单点符号路径
private resolveContextPath(path: string, context: INodeContext): any {
const parts = path.split('.');
let currentVal: any = null;
if (parts[0] === 'workflow' && parts[1] === 'variables') {
currentVal = context.workflowContext.variables;
parts.splice(0, 2);
} else if (parts[0] === 'input') {
currentVal = context.input;
parts.splice(0, 1);
} else {
return undefined; // Unsupported path root
}
for (const part of parts) {
if (currentVal === null || typeof currentVal !== 'object') return undefined;
currentVal = currentVal[part];
}
return currentVal;
}
// MARK: 默认输入验证(在特定节点中重写)
// Default input validation (override in specific nodes)
validateInput(input: Record<string, any>): boolean {
if (!input.providerName || typeof input.providerName !== 'string') {
console.error(`Validation Error [${this.id}]: Missing or invalid 'providerName'.`);
return false;
}
// Specific nodes will add checks for promptTemplate, schema, labels etc.
return true;
}
// Optional: 如果AI操作需要回滚,则实现补偿
// async compensate(context: INodeContext): Promise<void> { ... }
}