|
|
const fs = require("fs"); |
|
|
const path = require("path"); |
|
|
const { v4: uuidv4 } = require("uuid"); |
|
|
const { FlowExecutor, FLOW_TYPES } = require("./executor"); |
|
|
const { normalizePath } = require("../files"); |
|
|
const { safeJsonParse } = require("../http"); |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class AgentFlows { |
|
|
static flowsDir = process.env.STORAGE_DIR |
|
|
? path.join(process.env.STORAGE_DIR, "plugins", "agent-flows") |
|
|
: path.join(process.cwd(), "storage", "plugins", "agent-flows"); |
|
|
|
|
|
constructor() {} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static createOrCheckFlowsDir() { |
|
|
try { |
|
|
if (fs.existsSync(AgentFlows.flowsDir)) return true; |
|
|
fs.mkdirSync(AgentFlows.flowsDir, { recursive: true }); |
|
|
return true; |
|
|
} catch (error) { |
|
|
console.error("Failed to create flows directory:", error); |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static getAllFlows() { |
|
|
AgentFlows.createOrCheckFlowsDir(); |
|
|
const files = fs.readdirSync(AgentFlows.flowsDir); |
|
|
const flows = {}; |
|
|
|
|
|
for (const file of files) { |
|
|
if (!file.endsWith(".json")) continue; |
|
|
try { |
|
|
const filePath = path.join(AgentFlows.flowsDir, file); |
|
|
const content = fs.readFileSync(normalizePath(filePath), "utf8"); |
|
|
const config = JSON.parse(content); |
|
|
const id = file.replace(".json", ""); |
|
|
flows[id] = config; |
|
|
} catch (error) { |
|
|
console.error(`Error reading flow file ${file}:`, error); |
|
|
} |
|
|
} |
|
|
|
|
|
return flows; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static loadFlow(uuid) { |
|
|
try { |
|
|
const flowJsonPath = normalizePath( |
|
|
path.join(AgentFlows.flowsDir, `${uuid}.json`) |
|
|
); |
|
|
if (!uuid || !fs.existsSync(flowJsonPath)) return null; |
|
|
const flow = safeJsonParse(fs.readFileSync(flowJsonPath, "utf8"), null); |
|
|
if (!flow) return null; |
|
|
|
|
|
return { |
|
|
name: flow.name, |
|
|
uuid, |
|
|
config: flow, |
|
|
}; |
|
|
} catch (error) { |
|
|
console.error("Failed to load flow:", error); |
|
|
return null; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static saveFlow(name, config, uuid = null) { |
|
|
try { |
|
|
AgentFlows.createOrCheckFlowsDir(); |
|
|
|
|
|
if (!uuid) uuid = uuidv4(); |
|
|
const normalizedUuid = normalizePath(`${uuid}.json`); |
|
|
const filePath = path.join(AgentFlows.flowsDir, normalizedUuid); |
|
|
|
|
|
|
|
|
|
|
|
const supportedFlowTypes = Object.values(FLOW_TYPES).map( |
|
|
(definition) => definition.type |
|
|
); |
|
|
const supportsAllBlocks = config.steps.every((step) => |
|
|
supportedFlowTypes.includes(step.type) |
|
|
); |
|
|
if (!supportsAllBlocks) |
|
|
throw new Error( |
|
|
"This flow includes unsupported blocks. They may not be supported by your version of AnythingLLM or are not available on this platform." |
|
|
); |
|
|
|
|
|
fs.writeFileSync(filePath, JSON.stringify({ ...config, name }, null, 2)); |
|
|
return { success: true, uuid }; |
|
|
} catch (error) { |
|
|
console.error("Failed to save flow:", error); |
|
|
return { success: false, error: error.message }; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static listFlows() { |
|
|
try { |
|
|
const flows = AgentFlows.getAllFlows(); |
|
|
return Object.entries(flows).map(([uuid, flow]) => ({ |
|
|
name: flow.name, |
|
|
uuid, |
|
|
description: flow.description, |
|
|
active: flow.active !== false, |
|
|
})); |
|
|
} catch (error) { |
|
|
console.error("Failed to list flows:", error); |
|
|
return []; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static deleteFlow(uuid) { |
|
|
try { |
|
|
const filePath = normalizePath( |
|
|
path.join(AgentFlows.flowsDir, `${uuid}.json`) |
|
|
); |
|
|
if (!fs.existsSync(filePath)) throw new Error(`Flow ${uuid} not found`); |
|
|
fs.rmSync(filePath); |
|
|
return { success: true }; |
|
|
} catch (error) { |
|
|
console.error("Failed to delete flow:", error); |
|
|
return { success: false, error: error.message }; |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static async executeFlow(uuid, variables = {}, aibitat = null) { |
|
|
const flow = AgentFlows.loadFlow(uuid); |
|
|
if (!flow) throw new Error(`Flow ${uuid} not found`); |
|
|
const flowExecutor = new FlowExecutor(); |
|
|
return await flowExecutor.executeFlow(flow, variables, aibitat); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static activeFlowPlugins() { |
|
|
const flows = AgentFlows.getAllFlows(); |
|
|
return Object.entries(flows) |
|
|
.filter(([_, flow]) => flow.active !== false) |
|
|
.map(([uuid]) => `@@flow_${uuid}`); |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static loadFlowPlugin(uuid) { |
|
|
const flow = AgentFlows.loadFlow(uuid); |
|
|
if (!flow) return null; |
|
|
|
|
|
const startBlock = flow.config.steps?.find((s) => s.type === "start"); |
|
|
const variables = startBlock?.config?.variables || []; |
|
|
|
|
|
return { |
|
|
name: `flow_${uuid}`, |
|
|
description: `Execute agent flow: ${flow.name}`, |
|
|
plugin: (_runtimeArgs = {}) => ({ |
|
|
name: `flow_${uuid}`, |
|
|
description: |
|
|
flow.config.description || `Execute agent flow: ${flow.name}`, |
|
|
setup: (aibitat) => { |
|
|
aibitat.function({ |
|
|
name: `flow_${uuid}`, |
|
|
description: |
|
|
flow.config.description || `Execute agent flow: ${flow.name}`, |
|
|
parameters: { |
|
|
type: "object", |
|
|
properties: variables.reduce((acc, v) => { |
|
|
if (v.name) { |
|
|
acc[v.name] = { |
|
|
type: "string", |
|
|
description: |
|
|
v.description || `Value for variable ${v.name}`, |
|
|
}; |
|
|
} |
|
|
return acc; |
|
|
}, {}), |
|
|
}, |
|
|
handler: async (args) => { |
|
|
aibitat.introspect(`Executing flow: ${flow.name}`); |
|
|
const result = await AgentFlows.executeFlow(uuid, args, aibitat); |
|
|
if (!result.success) { |
|
|
aibitat.introspect( |
|
|
`Flow failed: ${result.results[0]?.error || "Unknown error"}` |
|
|
); |
|
|
return `Flow execution failed: ${result.results[0]?.error || "Unknown error"}`; |
|
|
} |
|
|
aibitat.introspect(`${flow.name} completed successfully`); |
|
|
|
|
|
// If the flow result has directOutput, return it |
|
|
// as the aibitat result so that no other processing is done |
|
|
if (!!result.directOutput) { |
|
|
aibitat.skipHandleExecution = true; |
|
|
return AgentFlows.stringifyResult(result.directOutput); |
|
|
} |
|
|
|
|
|
return AgentFlows.stringifyResult(result); |
|
|
}, |
|
|
}); |
|
|
}, |
|
|
}), |
|
|
flowName: flow.name, |
|
|
}; |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
static stringifyResult(input) { |
|
|
return typeof input === "object" ? JSON.stringify(input) : String(input); |
|
|
} |
|
|
} |
|
|
|
|
|
module.exports.AgentFlows = AgentFlows; |
|
|
|