mirror of
https://github.com/Mintplex-Labs/anything-llm
synced 2026-04-25 17:15:37 +02:00
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:
2
.github/workflows/dev-build.yaml
vendored
2
.github/workflows/dev-build.yaml
vendored
@@ -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/*'
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const DEFAULT_USER_ITEMS = {
|
||||
agentSkills: { items: [] },
|
||||
systemPrompts: { items: [] },
|
||||
slashCommands: { items: [] },
|
||||
agentFlows: { items: [] },
|
||||
},
|
||||
teamItems: [],
|
||||
};
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 "{item.name}"
|
||||
</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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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} />;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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", {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -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",
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user