@flowlab/all
Version:
A cool library focusing on handling various flows
294 lines (270 loc) • 16.9 kB
Markdown
## Detailed Usage Example (AI Flow - RAG + Function Call)
``` typescript
import {
WorkflowDefinition, createWorkflowContext, WorkflowExecutor, NodeRegistry,
InMemoryEventManager, LocalScheduler, ConsoleLogger, StepType, // 引入 FlowLab 核心组件
// 假设 Core 包提供了一些基础节点:
LogNode, // 用于在控制台打印日志的节点
HttpRequestNode, // 用于发起 HTTP 请求的节点
} from '@flowlab/core';
// 引入 @flowlab/ai 包的相关组件
import {
aiProviderRegistry, // 全局 AI Provider 注册表实例
loadAIConfigFromFile, // (可选) 从文件加载 AI 配置的辅助函数
LLMCompletionNode, // 用于文本生成的 AI 节点
EmbeddingNode, // 用于生成文本向量嵌入的 AI 节点
FunctionCallingNode, // 用于让 AI 决定是否调用以及如何调用外部函数的节点
aiConditionHelper, // 用于在条件步骤中使用 AI 判断的辅助对象
// 假设存在一个用于向量搜索的节点 (可能在 @flowlab/ai 或其他扩展包中)
VectorSearchNode, // 输入: 查询向量, TopK; 输出: 相关文档列表[]
} from '@flowlab/ai'; // 假设这些都从 @flowlab/ai 导出
// --- 第 1 步: 初始化与配置 ---
// 1.1 加载 AI Provider 的配置 (例如,从环境变量或配置文件)
// loadAIConfigFromFile('path/to/ai-config.json'); // 可以取消注释这行,如果你的配置在文件中
aiProviderRegistry.loadGlobalConfigs({ // 使用代码直接加载配置,或者从环境变量读取
'openai': { // 定义名为 'openai' 的 Provider 配置
apiKey: process.env.OPENAI_API_KEY, // 从环境变量安全地读取 API Key
// defaultModel: 'gpt-4' // 可以设置默认模型
},
// 'gemini': { apiKey: process.env.GEMINI_API_KEY }, // 可以添加其他 Provider 配置
});
// 注意: 如果你的 Provider (比如自定义的本地模型 Provider) 需要注册,可以在这里调用 aiProviderRegistry.registerProvider(...)
// 1.2 设置 FlowLab 核心组件
const logger = new ConsoleLogger('[AI-Flow]'); // 创建日志记录器,方便调试
const eventManager = new InMemoryEventManager(); // 创建内存事件管理器,用于节点间或工作流间通信(本例未使用,但大型应用可能需要)
// 创建调度器,如果 AI 任务可能耗时较长,希望异步执行而不阻塞主流程时会用到 (需要配合 BaseAINode 的 scheduleTask 输入)
const scheduler = new LocalScheduler(eventManager, logger); // 本地调度器,用于在同一进程中延迟或异步执行
const nodeRegistry = new NodeRegistry(); // 创建节点注册表,用于管理所有可用的节点类型
// --- 第 2 步: 注册所有需要在工作流中使用的节点 ---
// 必须将所有会用到的节点类型(无论是 Core 节点还是 AI 节点)注册到 NodeRegistry 中
// 注册 Core 基础节点
nodeRegistry.register(new LogNode());
nodeRegistry.register(new HttpRequestNode()); // 假设这个节点用于调用外部 API
// 注册 AI 节点实例 (如果节点需要调度器,请在构造时传入)
nodeRegistry.register(new LLMCompletionNode(scheduler)); // 文本生成节点
nodeRegistry.register(new EmbeddingNode(scheduler)); // 向量嵌入节点
nodeRegistry.register(new FunctionCallingNode(scheduler)); // 函数调用决策节点
nodeRegistry.register(new VectorSearchNode()); // 向量搜索节点 (假设它不需要调度器)
// --- 第 3 步: 定义包含 RAG 和函数调用的 AI 工作流 ---
const ragWorkflow = new WorkflowDefinition('rag-function-call-flow', 'RAG问答与函数调用流程'); // 创建工作流定义,给它一个 ID 和名称
ragWorkflow // 使用链式调用定义工作流的步骤
// 起始步骤: 记录收到的用户查询
.addStep({
id: 'logStart', // 步骤的唯一标识符
nodeId: 'LogNode', // 使用哪个注册的节点类型来执行此步骤
input: { // 传递给 LogNode 的输入参数
// 使用模板字符串动态插入工作流的初始输入 'query'
message: "RAG 流程启动。用户查询: ${workflow.input.query}"
},
nextStepId: 'generateQueryEmbedding' // 定义下一个要执行的步骤的 ID
})
// RAG 步骤 1: 为用户查询生成向量嵌入 (Embedding)
.addStep({
id: 'generateQueryEmbedding',
nodeId: 'EmbeddingNode', // 使用向量嵌入 AI 节点
input: { // EmbeddingNode 的输入参数
providerName: 'openai', // 指定使用哪个 AI Provider (需要已配置)
text: '${workflow.input.query}', // 从工作流初始输入获取查询文本
modelOptions: { model: 'text-embedding-ada-002' } // 指定嵌入模型 (示例)
},
outputMapping: { // 将节点的输出映射到工作流变量中,供后续步骤使用
// 将 EmbeddingNode 输出的 embedding 字段,存入名为 'queryEmbedding' 的工作流变量
'workflow.variables.queryEmbedding': 'output.embedding'
},
nextStepId: 'searchVectorDB' // 下一步:进行向量搜索
})
// RAG 步骤 2: 使用查询向量在向量数据库中搜索相关文档
.addStep({
id: 'searchVectorDB',
nodeId: 'VectorSearchNode', // 使用(假设存在的)向量搜索节点
input: { // 向量搜索节点的输入
queryEmbedding: '${workflow.variables.queryEmbedding}', // 使用上一步生成的查询向量
topK: 3, // 指定返回最相关的 3 个文档
// 此处通常还需要传递向量数据库的连接信息或配置
// vectorDbConfig: { host: '...', indexName: '...' }
},
outputMapping: {
// 将搜索结果 (假设是文档数组) 存入 'retrievedDocs' 工作流变量
'workflow.variables.retrievedDocs': 'output.documents' // 期望输出格式: [{content: '...', score: 0.9}, ...]
},
nextStepId: 'checkDocsFound' // 下一步:检查是否找到了文档
})
// 条件步骤: 检查 RAG 步骤是否找到了相关文档
.addConditionStep({
id: 'checkDocsFound',
condition: (context) => // 使用一个函数来评估条件,函数接收当前工作流上下文
// 检查 'retrievedDocs' 变量是否存在且包含元素
(context.variables.retrievedDocs && context.variables.retrievedDocs.length > 0),
branches: { // 定义不同条件结果对应的下一个步骤 ID
'true': 'synthesizeAnswer', // 如果条件为 true (找到文档),则跳转到 'synthesizeAnswer'
'false': 'handleNoDocs' // 如果条件为 false (未找到文档),则跳转到 'handleNoDocs'
}
// 注意:如果 condition 函数需要异步操作(例如调用 AI),需要使用 async 并确保返回 Promise<boolean|string>
// condition: async (context) => { ... }
})
// RAG 步骤 3a (路径: 找到文档): 使用 LLM 结合用户查询和检索到的文档来生成答案
.addStep({
id: 'synthesizeAnswer',
nodeId: 'LLMCompletionNode', // 使用文本生成 AI 节点
input: { // 文本生成节点的输入
providerName: 'openai', // 指定 AI Provider
modelOptions: { model: 'gpt-4', temperature: 0.5 }, // 指定模型和参数
// 核心: 构建 Prompt,包含原始查询和检索到的上下文信息
promptTemplate: `用户查询: \${workflow.input.query}\n\n相关上下文:\n${'${workflow.variables.retrievedDocs.map(d => `- ${d.content}`).join("\\n")}'}\n\n请*仅*根据上面提供的上下文来回答用户查询。如果上下文中没有答案,请明确说明。`,
// 注意: 上面模板中使用了 JS 模板字符串来动态构建上下文部分。
// 对于复杂的模板处理,可能需要在专门的脚本节点中预处理,或者使用更强大的模板引擎。
// 这里假设 FlowLab 的模板渲染能处理简单的 JS 表达式(如果不能,需要调整)。
},
outputMapping: {
// 将 LLM 生成的答案文本存入 'synthesizedAnswer' 工作流变量
'workflow.variables.synthesizedAnswer': 'output.completionText'
},
nextStepId: 'decideNextAction' // 下一步:让 AI 决定是否需要额外操作
})
// RAG 步骤 3b (路径: 未找到文档): 处理未找到相关文档的情况
.addStep({
id: 'handleNoDocs',
nodeId: 'LogNode', // 可以仅记录日志,或设置一个默认回答
input: { message: "未能根据查询找到相关文档。" },
outputMapping: { // 设置一个默认的、表示未找到答案的文本到 'synthesizedAnswer' 变量
'workflow.variables.synthesizedAnswer': '"抱歉,我无法在知识库中找到您查询的相关信息。"'
},
nextStepId: 'decideNextAction' // 即使没找到文档,也继续下一步判断是否需要其他操作
})
// 函数调用步骤 1: 使用 Function Calling 节点让 AI 决定是否需要调用外部函数 (如下单、创建工单等)
.addStep({
id: 'decideNextAction',
nodeId: 'FunctionCallingNode', // 使用函数调用决策 AI 节点
input: { // 函数调用节点的输入
providerName: 'openai', // 指定 AI Provider
modelOptions: { model: 'gpt-4' }, // 指定支持函数调用的模型
// Prompt 指导 AI 基于查询和已生成的答案,判断是否需要创建工单
promptTemplate: `根据用户原始查询 "\${workflow.input.query}" 以及系统提供的答案 "\${workflow.variables.synthesizedAnswer}",请判断是否需要为用户创建一个支持工单。仅当用户看起来不满意、问题未解决或需要进一步协助时才调用函数。`,
// 定义可供 AI 选择调用的函数列表 (遵循特定格式,如 OpenAI Function Calling API)
functions: [
{
name: 'create_support_ticket', // 函数名称
description: '为用户创建一个支持工单。', // 函数描述,供 AI 理解其作用
parameters: { // 函数参数的 JSON Schema 定义
type: 'object',
properties: {
summary: { type: 'string', description: '用户问题的简短摘要。' },
query: { type: 'string', description: '用户的原始查询文本。' }
},
required: ['summary', 'query'] // 必须提供的参数
}
}
// 可以定义更多函数供 AI 选择
]
},
outputMapping: {
// 将 FunctionCallingNode 的整个输出 (包含 functionCall 或 responseText) 存入 'actionDecision' 变量
'workflow.variables.actionDecision': 'output' // 输出结构: { functionCall?: { name: string, arguments: object }, responseText?: string, usage?: ... }
},
nextStepId: 'checkFunctionCall' // 下一步:检查 AI 是否要求调用函数
})
// 条件步骤: 检查上一步 AI 是否决定调用函数
.addConditionStep({
id: 'checkFunctionCall',
// 检查 'actionDecision' 变量中是否存在 functionCall 字段
condition: (context) => !!context.variables.actionDecision?.functionCall,
branches: {
'true': 'callCreateTicketAPI', // 如果 AI 要求调用函数,则跳转到执行 API 调用步骤
'false': 'logFinalAnswer' // 如果 AI 未要求调用函数,则直接记录最终答案
}
})
// 函数调用步骤 2a (路径: 需要调用函数): 实际执行外部 API 调用来创建工单
.addStep({
id: 'callCreateTicketAPI',
nodeId: 'HttpRequestNode', // 使用通用的 HTTP 请求节点
input: { // HTTP 请求节点的输入
method: 'POST', // 请求方法
url: 'https://api.example.com/support/tickets', // 你的实际工单系统 API 地址
headers: { // 请求头,例如认证信息
'Authorization': 'Bearer YOUR_API_TOKEN', // 替换为你的 API Token
'Content-Type': 'application/json'
},
// 从 AI 决定的函数调用的参数中,动态构建请求体 (body)
body: {
summary: '${workflow.variables.actionDecision.functionCall.arguments.summary}', // 获取 AI 提供的摘要
original_query: '${workflow.variables.actionDecision.functionCall.arguments.query}', // 获取 AI 提供的原始查询
context_answer: '${workflow.variables.synthesizedAnswer}' // 可以将 RAG 生成的答案也一并提交
// 注意: 模板替换需要能正确处理嵌套 JSON 路径
}
},
outputMapping: { // 将 API 调用的结果存储起来
'workflow.variables.ticketResult': 'output.data' // 假设 HTTP 节点将响应体放在 output.data 中
},
nextStepId: 'logTicketCreated' // 下一步:记录工单创建结果
})
// 日志步骤 (路径: 工单已创建)
.addStep({
id: 'logTicketCreated',
nodeId: 'LogNode',
input: {
// 记录日志,包含 API 的响应信息 (需要确保 ticketResult 是可序列化的)
message: "已请求创建支持工单。API 响应: ${JSON.stringify(workflow.variables.ticketResult ?? 'N/A')}"
},
nextStepId: 'finalLog' // 跳转到最终日志步骤
})
// 函数调用步骤 2b (路径: 无需调用函数): 记录 AI 直接提供的最终文本答案
.addStep({
id: 'logFinalAnswer',
nodeId: 'LogNode',
input: {
// 从 actionDecision 中获取 AI 的直接文本回复,如果不存在则使用 RAG 生成的答案
message: "最终答案 (无需创建工单): ${workflow.variables.actionDecision.responseText || workflow.variables.synthesizedAnswer}"
},
nextStepId: 'finalLog' // 跳转到最终日志步骤
})
// 结束步骤: 记录整个流程完成
.addStep({
id: 'finalLog',
nodeId: 'LogNode',
input: { message: "RAG 与函数调用流程已完成。" }
// 没有 nextStepId,表示这个分支的流程结束
})
// 最后,设置整个工作流的起始步骤 ID
.setStartStep('logStart');
// --- 第 4 步: 执行工作流 ---
async function runFlow() {
// 4.1 定义工作流的初始输入数据
const initialInput = { query: "我的密码重置链接好像失效了,无法重置密码,怎么办?" };
// 4.2 创建本次执行的工作流上下文,传入工作流定义 ID 和初始输入
const context = createWorkflowContext(ragWorkflow.id, initialInput);
// 4.3 创建工作流执行器实例
const executor = new WorkflowExecutor(
ragWorkflow, // 要执行的工作流定义
context, // 本次执行的上下文
{ // 执行器选项
nodeRegistry, // 必须提供节点注册表
eventManager, // 提供事件管理器 (可选)
scheduler, // 提供调度器 (可选, 仅当使用 scheduleTask 时需要)
logger, // 提供日志记录器 (推荐)
// maxLoopIterations: 100 // (可选) 防止无限循环的最大迭代次数
// 可以注入其他依赖项给节点使用,例如数据库连接池等
}
);
// 4.4 运行工作流并处理结果
try {
console.log("--- 开始执行工作流 ---");
const result = await executor.run(); // 异步执行工作流
// 工作流执行完毕后,输出结果
console.log(`\n--- 工作流执行结果 ---`);
console.log(`最终状态: ${result.status}`); // 输出工作流的最终状态 (COMPLETED, FAILED)
console.log(`最终工作流变量:`, JSON.stringify(result.variables, null, 2)); // 输出执行后所有的工作流变量
// (可选) 输出详细的执行历史记录,用于调试
// console.log(`\n--- 执行历史 ---`);
// result.history.forEach(h => console.log(
// `步骤: ${h.stepId} (节点: ${h.nodeId}) -> 状态: ${h.status} ` +
// `[开始: ${h.startTime.toISOString()}${h.endTime ? ' - 结束: ' + h.endTime.toISOString() : ''}]` +
// `${h.error ? ' 错误: ' + h.error.message : ''}`
// ));
} catch (error) {
// 如果执行器在运行过程中抛出未捕获的异常
console.error(`工作流执行失败:`, error);
}
}
// 启动执行
runFlow();
```