diff --git a/.github/workflows/dev-build.yaml b/.github/workflows/dev-build.yaml index 679cf7e72f..4f972c48e9 100644 --- a/.github/workflows/dev-build.yaml +++ b/.github/workflows/dev-build.yaml @@ -6,7 +6,7 @@ concurrency: on: push: - branches: ['3872-feat-direct-output-to-chat-from-agent-flows'] # put your current branch to create a build. Core team only. + branches: ['3866-feat-import-agent-flows-from-community-hub'] # put your current branch to create a build. Core team only. paths-ignore: - '**.md' - 'cloud-deployments/*' diff --git a/frontend/src/models/agentFlows.js b/frontend/src/models/agentFlows.js index fcb82c597a..cb29a71a7f 100644 --- a/frontend/src/models/agentFlows.js +++ b/frontend/src/models/agentFlows.js @@ -19,7 +19,7 @@ const AgentFlows = { body: JSON.stringify({ name, config, uuid }), }) .then((res) => { - if (!res.ok) throw new Error(response.error || "Failed to save flow"); + if (!res.ok) throw new Error(res.error || "Failed to save flow"); return res; }) .then((res) => res.json()) diff --git a/frontend/src/pages/Admin/AgentBuilder/index.jsx b/frontend/src/pages/Admin/AgentBuilder/index.jsx index 3838529817..72b4d5c88b 100644 --- a/frontend/src/pages/Admin/AgentBuilder/index.jsx +++ b/frontend/src/pages/Admin/AgentBuilder/index.jsx @@ -224,7 +224,9 @@ export default function AgentBuilder() { await loadAvailableFlows(); } catch (error) { console.error("Save error details:", error); - showToast("Failed to save agent flow", "error", { clear: true }); + showToast(`Failed to save agent flow. ${error.message}`, "error", { + clear: true, + }); } }; diff --git a/frontend/src/pages/GeneralSettings/CommunityHub/Authentication/useUserItems.js b/frontend/src/pages/GeneralSettings/CommunityHub/Authentication/useUserItems.js index 3222f84740..49274aaea2 100644 --- a/frontend/src/pages/GeneralSettings/CommunityHub/Authentication/useUserItems.js +++ b/frontend/src/pages/GeneralSettings/CommunityHub/Authentication/useUserItems.js @@ -6,6 +6,7 @@ const DEFAULT_USER_ITEMS = { agentSkills: { items: [] }, systemPrompts: { items: [] }, slashCommands: { items: [] }, + agentFlows: { items: [] }, }, teamItems: [], }; diff --git a/frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/Completed/index.jsx b/frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/Completed/index.jsx index 15724a77a8..b7b6fff7a7 100644 --- a/frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/Completed/index.jsx +++ b/frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/Completed/index.jsx @@ -1,5 +1,7 @@ import CommunityHubImportItemSteps from ".."; import CTAButton from "@/components/lib/CTAButton"; +import { Link } from "react-router-dom"; +import paths from "@/utils/paths"; export default function Completed({ settings, setSettings, setStep }) { return ( @@ -15,6 +17,14 @@ export default function Completed({ settings, setSettings, setStep }) { imported successfully! It is now available in your AnythingLLM instance.

+ {settings.item.itemType === "agent-flow" && ( + + View "{settings.item.name}" in Agent Skills + + )}

Any changes you make to this {settings.item.itemType} will not be reflected in the community hub. You can now modify as needed. diff --git a/frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/PullAndReview/HubItem/AgentFlow.jsx b/frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/PullAndReview/HubItem/AgentFlow.jsx new file mode 100644 index 0000000000..39f8c344b9 --- /dev/null +++ b/frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/PullAndReview/HubItem/AgentFlow.jsx @@ -0,0 +1,80 @@ +import CTAButton from "@/components/lib/CTAButton"; +import CommunityHubImportItemSteps from "../.."; +import showToast from "@/utils/toast"; +import paths from "@/utils/paths"; +import { CircleNotch } from "@phosphor-icons/react"; +import { useState } from "react"; +import AgentFlows from "@/models/agentFlows"; +import { safeJsonParse } from "@/utils/request"; + +export default function AgentFlow({ item, setStep }) { + const flowInfo = safeJsonParse(item.flow, { steps: [] }); + const [loading, setLoading] = useState(false); + + async function importAgentFlow() { + try { + setLoading(true); + const { success, error, flow } = await AgentFlows.saveFlow( + item.name, + flowInfo + ); + if (!success) throw new Error(error); + if (!!flow?.uuid) await AgentFlows.toggleFlow(flow.uuid, true); // Enable the flow automatically after import + + showToast(`Agent flow imported successfully!`, "success"); + setStep(CommunityHubImportItemSteps.completed.key); + } catch (e) { + console.error(e); + showToast(`Failed to import agent flow. ${e.message}`, "error"); + } finally { + setLoading(false); + } + } + + return ( +

+
+

+ Import Agent Flow "{item.name}" +

+ {item.creatorUsername && ( +

+ Created by{" "} + + @{item.creatorUsername} + +

+ )} +
+
+

+ Agent flows allow you to create reusable sequences of actions that can + be triggered by your agent. +

+
+

Flow Details:

+

Description: {item.description}

+

Steps ({flowInfo.steps.length}):

+
    + {flowInfo.steps.map((step, index) => ( +
  • {step.type}
  • + ))} +
+
+
+ + {loading ? : null} + {loading ? "Importing..." : "Import agent flow"} + +
+ ); +} diff --git a/frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/PullAndReview/HubItem/index.js b/frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/PullAndReview/HubItem/index.js index 725ae45f52..4362043c16 100644 --- a/frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/PullAndReview/HubItem/index.js +++ b/frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/PullAndReview/HubItem/index.js @@ -2,11 +2,13 @@ import SystemPrompt from "./SystemPrompt"; import SlashCommand from "./SlashCommand"; import UnknownItem from "./Unknown"; import AgentSkill from "./AgentSkill"; +import AgentFlow from "./AgentFlow"; const HubItemComponent = { "agent-skill": AgentSkill, "system-prompt": SystemPrompt, "slash-command": SlashCommand, + "agent-flow": AgentFlow, unknown: UnknownItem, }; diff --git a/frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/PullAndReview/index.jsx b/frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/PullAndReview/index.jsx index 375ec58886..040df74393 100644 --- a/frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/PullAndReview/index.jsx +++ b/frontend/src/pages/GeneralSettings/CommunityHub/ImportItem/Steps/PullAndReview/index.jsx @@ -3,7 +3,6 @@ import CommunityHubImportItemSteps from ".."; import CTAButton from "@/components/lib/CTAButton"; import { useEffect, useState } from "react"; import HubItemComponent from "./HubItem"; -import PreLoader from "@/components/Preloader"; function useGetCommunityHubItem({ importId, updateSettings }) { const [item, setItem] = useState(null); diff --git a/frontend/src/pages/GeneralSettings/CommunityHub/Trending/HubItems/HubItemCard/agentFlow.jsx b/frontend/src/pages/GeneralSettings/CommunityHub/Trending/HubItems/HubItemCard/agentFlow.jsx new file mode 100644 index 0000000000..d2c73f5ec0 --- /dev/null +++ b/frontend/src/pages/GeneralSettings/CommunityHub/Trending/HubItems/HubItemCard/agentFlow.jsx @@ -0,0 +1,39 @@ +import { Link } from "react-router-dom"; +import paths from "@/utils/paths"; +import { VisibilityIcon } from "./generic"; + +export default function AgentFlowHubCard({ item }) { + const flow = JSON.parse(item.flow); + return ( + +
+

{item.name}

+ +
+
+

{item.description}

+ +

+

+

+
+
+ + Import → + +
+ + ); +} diff --git a/frontend/src/pages/GeneralSettings/CommunityHub/Trending/HubItems/HubItemCard/index.jsx b/frontend/src/pages/GeneralSettings/CommunityHub/Trending/HubItems/HubItemCard/index.jsx index f2d53c0f77..371343a68a 100644 --- a/frontend/src/pages/GeneralSettings/CommunityHub/Trending/HubItems/HubItemCard/index.jsx +++ b/frontend/src/pages/GeneralSettings/CommunityHub/Trending/HubItems/HubItemCard/index.jsx @@ -2,6 +2,7 @@ import GenericHubCard from "./generic"; import SystemPromptHubCard from "./systemPrompt"; import SlashCommandHubCard from "./slashCommand"; import AgentSkillHubCard from "./agentSkill"; +import AgentFlowHubCard from "./agentFlow"; export default function HubItemCard({ type, item }) { switch (type) { @@ -11,6 +12,8 @@ export default function HubItemCard({ type, item }) { return ; case "agentSkills": return ; + case "agentFlows": + return ; default: return ; } diff --git a/frontend/src/pages/GeneralSettings/CommunityHub/utils.js b/frontend/src/pages/GeneralSettings/CommunityHub/utils.js index 379da33d48..782dae2f55 100644 --- a/frontend/src/pages/GeneralSettings/CommunityHub/utils.js +++ b/frontend/src/pages/GeneralSettings/CommunityHub/utils.js @@ -1,6 +1,6 @@ /** * Convert a type to a readable string for the community hub. - * @param {("agentSkills" | "agentSkill" | "systemPrompts" | "systemPrompt" | "slashCommands" | "slashCommand")} type + * @param {("agentSkills" | "agentSkill" | "systemPrompts" | "systemPrompt" | "slashCommands" | "slashCommand" | "agentFlows" | "agentFlow")} type * @returns {string} */ export function readableType(type) { @@ -14,12 +14,15 @@ export function readableType(type) { case "slashCommand": case "slashCommands": return "Slash Commands"; + case "agentFlows": + case "agentFlow": + return "Agent Flows"; } } /** * Convert a type to a path for the community hub. - * @param {("agentSkill" | "agentSkills" | "systemPrompt" | "systemPrompts" | "slashCommand" | "slashCommands")} type + * @param {("agentSkill" | "agentSkills" | "systemPrompt" | "systemPrompts" | "slashCommand" | "slashCommands" | "agentFlow" | "agentFlows")} type * @returns {string} */ export function typeToPath(type) { @@ -33,5 +36,8 @@ export function typeToPath(type) { case "slashCommand": case "slashCommands": return "slash-commands"; + case "agentFlow": + case "agentFlows": + return "agent-flows"; } } diff --git a/server/endpoints/agentFlows.js b/server/endpoints/agentFlows.js index 75a16c774a..0f3c164acc 100644 --- a/server/endpoints/agentFlows.js +++ b/server/endpoints/agentFlows.js @@ -25,12 +25,10 @@ function agentFlowEndpoints(app) { } const flow = AgentFlows.saveFlow(name, config, uuid); - if (!flow) { - return response.status(500).json({ - success: false, - error: "Failed to save flow", - }); - } + if (!flow || !flow.success) + return response + .status(200) + .json({ flow: null, error: flow.error || "Failed to save flow" }); if (!uuid) { await Telemetry.sendTelemetry("agent_flow_created", { diff --git a/server/utils/agentFlows/executor.js b/server/utils/agentFlows/executor.js index 81126e2ee7..c41ca9c490 100644 --- a/server/utils/agentFlows/executor.js +++ b/server/utils/agentFlows/executor.js @@ -1,8 +1,5 @@ const { FLOW_TYPES } = require("./flowTypes"); const executeApiCall = require("./executors/api-call"); -const executeWebsite = require("./executors/website"); -const executeFile = require("./executors/file"); -const executeCode = require("./executors/code"); const executeLLMInstruction = require("./executors/llm-instruction"); const executeWebScraping = require("./executors/web-scraping"); const { Telemetry } = require("../../models/telemetry"); @@ -161,15 +158,6 @@ class FlowExecutor { case FLOW_TYPES.API_CALL.type: result = await executeApiCall(config, context); break; - case FLOW_TYPES.WEBSITE.type: - result = await executeWebsite(config, context); - break; - case FLOW_TYPES.FILE.type: - result = await executeFile(config, context); - break; - case FLOW_TYPES.CODE.type: - result = await executeCode(config, context); - break; case FLOW_TYPES.LLM_INSTRUCTION.type: result = await executeLLMInstruction(config, context); break; diff --git a/server/utils/agentFlows/executors/code.js b/server/utils/agentFlows/executors/code.js deleted file mode 100644 index 6f2dfead3b..0000000000 --- a/server/utils/agentFlows/executors/code.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Execute a code flow step - * @param {Object} config Flow step configuration - * @returns {Promise} Result of the code execution - */ -async function executeCode(config) { - // For now just log what would happen - console.log("Code execution:", config); - return { success: true, message: "Code executed (placeholder)" }; -} - -module.exports = executeCode; diff --git a/server/utils/agentFlows/executors/file.js b/server/utils/agentFlows/executors/file.js deleted file mode 100644 index 0b3bd23c8d..0000000000 --- a/server/utils/agentFlows/executors/file.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Execute a file operation flow step - * @param {Object} config Flow step configuration - * @returns {Promise} Result of the file operation - */ -async function executeFile(config) { - // For now just log what would happen - console.log("File operation:", config); - return { success: true, message: "File operation executed (placeholder)" }; -} - -module.exports = executeFile; diff --git a/server/utils/agentFlows/executors/website.js b/server/utils/agentFlows/executors/website.js deleted file mode 100644 index 369a0e79de..0000000000 --- a/server/utils/agentFlows/executors/website.js +++ /dev/null @@ -1,12 +0,0 @@ -/** - * Execute a website interaction flow step - * @param {Object} config Flow step configuration - * @returns {Promise} Result of the website interaction - */ -async function executeWebsite(config) { - // For now just log what would happen - console.log("Website action:", config); - return { success: true, message: "Website action executed (placeholder)" }; -} - -module.exports = executeWebsite; diff --git a/server/utils/agentFlows/flowTypes.js b/server/utils/agentFlows/flowTypes.js index 89ec8a6794..0572bd1b32 100644 --- a/server/utils/agentFlows/flowTypes.js +++ b/server/utils/agentFlows/flowTypes.js @@ -47,75 +47,6 @@ const FLOW_TYPES = { }, ], }, - WEBSITE: { - type: "website", - description: "Interact with a website", - parameters: { - url: { type: "string", description: "The URL of the website" }, - selector: { - type: "string", - description: "CSS selector for targeting elements", - }, - action: { - type: "string", - description: "Action to perform (read, click, type)", - }, - value: { type: "string", description: "Value to use for type action" }, - resultVariable: { - type: "string", - description: "Variable to store the result", - }, - directOutput: { - type: "boolean", - description: - "Whether to return the result directly to the user without LLM processing", - }, - }, - }, - FILE: { - type: "file", - description: "Perform file system operations", - parameters: { - path: { type: "string", description: "Path to the file" }, - operation: { - type: "string", - description: "Operation to perform (read, write, append)", - }, - content: { - type: "string", - description: "Content for write/append operations", - }, - resultVariable: { - type: "string", - description: "Variable to store the result", - }, - directOutput: { - type: "boolean", - description: - "Whether to return the result directly to the user without LLM processing", - }, - }, - }, - CODE: { - type: "code", - description: "Execute code in various languages", - parameters: { - language: { - type: "string", - description: "Programming language to execute", - }, - code: { type: "string", description: "Code to execute" }, - resultVariable: { - type: "string", - description: "Variable to store the result", - }, - directOutput: { - type: "boolean", - description: - "Whether to return the result directly to the user without LLM processing", - }, - }, - }, LLM_INSTRUCTION: { type: "llmInstruction", description: "Process data using LLM instructions", diff --git a/server/utils/agentFlows/index.js b/server/utils/agentFlows/index.js index f759898e1e..e4944d2c86 100644 --- a/server/utils/agentFlows/index.js +++ b/server/utils/agentFlows/index.js @@ -1,7 +1,7 @@ const fs = require("fs"); const path = require("path"); const { v4: uuidv4 } = require("uuid"); -const { FlowExecutor } = require("./executor"); +const { FlowExecutor, FLOW_TYPES } = require("./executor"); const { normalizePath } = require("../files"); const { safeJsonParse } = require("../http"); @@ -100,6 +100,20 @@ class AgentFlows { if (!uuid) uuid = uuidv4(); const normalizedUuid = normalizePath(`${uuid}.json`); const filePath = path.join(AgentFlows.flowsDir, normalizedUuid); + + // Prevent saving flows with unsupported blocks or importing + // flows with unsupported blocks (eg: file writing or code execution on Desktop importing to Docker) + 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) {