Implement importing of agent flows from community hub (#3867)

* implement importing of agent flows from community hub

* auto enable flow on import

* remove unused blocks for docker
prevent importing or saving of agent flows that have unsupported blocks for version or platform

* dev build

---------

Co-authored-by: Timothy Carambat <rambat1010@gmail.com>
This commit is contained in:
Sean Hatfield
2025-06-05 15:08:58 -07:00
committed by GitHub
parent 77f6262290
commit ef0928993e
18 changed files with 167 additions and 130 deletions

View File

@@ -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/*'

View File

@@ -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())

View File

@@ -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,
});
}
};

View File

@@ -6,6 +6,7 @@ const DEFAULT_USER_ITEMS = {
agentSkills: { items: [] },
systemPrompts: { items: [] },
slashCommands: { items: [] },
agentFlows: { items: [] },
},
teamItems: [],
};

View File

@@ -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.
</p>
{settings.item.itemType === "agent-flow" && (
<Link
to={paths.settings.agentSkills()}
className="text-theme-text-primary hover:text-blue-500 hover:underline"
>
View "{settings.item.name}" in Agent Skills
</Link>
)}
<p>
Any changes you make to this {settings.item.itemType} will not be
reflected in the community hub. You can now modify as needed.

View File

@@ -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 (
<div className="flex flex-col mt-4 gap-y-4">
<div className="flex flex-col gap-y-1">
<h2 className="text-base text-theme-text-primary font-semibold">
Import Agent Flow &quot;{item.name}&quot;
</h2>
{item.creatorUsername && (
<p className="text-white/60 light:text-theme-text-secondary text-xs font-mono">
Created by{" "}
<a
href={paths.communityHub.profile(item.creatorUsername)}
target="_blank"
className="hover:text-blue-500 hover:underline"
rel="noreferrer"
>
@{item.creatorUsername}
</a>
</p>
)}
</div>
<div className="flex flex-col gap-y-[25px] text-white/80 light:text-theme-text-secondary text-sm">
<p>
Agent flows allow you to create reusable sequences of actions that can
be triggered by your agent.
</p>
<div className="flex flex-col gap-y-2">
<p className="font-semibold">Flow Details:</p>
<p>Description: {item.description}</p>
<p className="font-semibold">Steps ({flowInfo.steps.length}):</p>
<ul className="list-disc pl-6">
{flowInfo.steps.map((step, index) => (
<li key={index}>{step.type}</li>
))}
</ul>
</div>
</div>
<CTAButton
disabled={loading}
className="text-dark-text w-full mt-[18px] h-[34px] hover:bg-accent"
onClick={importAgentFlow}
>
{loading ? <CircleNotch size={16} className="animate-spin" /> : null}
{loading ? "Importing..." : "Import agent flow"}
</CTAButton>
</div>
);
}

View File

@@ -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,
};

View File

@@ -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);

View File

@@ -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 (
<Link
to={paths.communityHub.importItem(item.importId)}
className="bg-black/70 light:bg-slate-100 rounded-lg p-3 hover:bg-black/60 light:hover:bg-slate-200 transition-all duration-200 cursor-pointer group border border-transparent hover:border-slate-400 flex flex-col h-full"
>
<div className="flex gap-x-2 items-center">
<p className="text-white text-sm font-medium">{item.name}</p>
<VisibilityIcon visibility={item.visibility} />
</div>
<div className="flex flex-col gap-2 flex-1">
<p className="text-white/60 text-xs mt-1">{item.description}</p>
<label className="text-white/60 text-xs font-semibold mt-4">
Steps ({flow.steps.length}):
</label>
<p className="text-white/60 text-xs bg-zinc-900 light:bg-slate-200 px-2 py-1 rounded-md font-mono border border-slate-800 light:border-slate-300">
<ul className="list-disc pl-4">
{flow.steps.map((step, index) => (
<li key={index}>{step.type}</li>
))}
</ul>
</p>
</div>
<div className="flex justify-end mt-2">
<Link
to={paths.communityHub.importItem(item.importId)}
className="text-primary-button hover:text-primary-button/80 text-sm font-medium px-3 py-1.5 rounded-md bg-black/30 light:bg-slate-200 group-hover:bg-black/50 light:group-hover:bg-slate-300 transition-all"
>
Import
</Link>
</div>
</Link>
);
}

View File

@@ -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 <SlashCommandHubCard item={item} />;
case "agentSkills":
return <AgentSkillHubCard item={item} />;
case "agentFlows":
return <AgentFlowHubCard item={item} />;
default:
return <GenericHubCard item={item} />;
}

View File

@@ -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";
}
}

View File

@@ -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", {

View File

@@ -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;

View File

@@ -1,12 +0,0 @@
/**
* Execute a code flow step
* @param {Object} config Flow step configuration
* @returns {Promise<Object>} 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;

View File

@@ -1,12 +0,0 @@
/**
* Execute a file operation flow step
* @param {Object} config Flow step configuration
* @returns {Promise<Object>} 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;

View File

@@ -1,12 +0,0 @@
/**
* Execute a website interaction flow step
* @param {Object} config Flow step configuration
* @returns {Promise<Object>} 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;

View File

@@ -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",

View File

@@ -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) {