mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(platform): Disable Trigger Setup through Builder (#10418)
We want users to set up triggers through the Library rather than the Builder. - Resolves #10413 https://github.com/user-attachments/assets/515ed80d-6569-4e26-862f-2a663115218c ### Changes 🏗️ - Update node UI to push users to Library for trigger set-up and management - Add note redirecting to Library for trigger set-up - Remove webhook status indicator and webhook URL section - Add `libraryAgent: LibraryAgent` to `BuilderContext` for access inside `CustomNode` - Move library agent loader from `FlowEditor` to `useAgentGraph` - Implement `migrate_legacy_triggered_graphs` migrator function - Remove `on_node_activate` hook (which previously handled webhook setup) - Propagate `created_at` from DB to `GraphModel` and `LibraryAgentPreset` models ### Checklist 📋 #### For code changes: - [x] I have clearly listed my changes in the PR description - [x] I have made a test plan - [x] I have tested my changes according to the test plan: - [x] Existing node triggers are converted to triggered presets (visible in the Library) - [x] Converted triggered presets work - [x] Trigger node inputs are disabled and handles are hidden - [x] Trigger node message links to the correct Library Agent when saved
This commit is contained in:
committed by
GitHub
parent
c2f11dbcfa
commit
7d2ab61546
@@ -1,6 +1,7 @@
|
||||
import logging
|
||||
import uuid
|
||||
from collections import defaultdict
|
||||
from datetime import datetime, timezone
|
||||
from typing import TYPE_CHECKING, Any, Literal, Optional, cast
|
||||
|
||||
from prisma.enums import SubmissionStatus
|
||||
@@ -381,6 +382,8 @@ class GraphModel(Graph):
|
||||
user_id: str
|
||||
nodes: list[NodeModel] = [] # type: ignore
|
||||
|
||||
created_at: datetime
|
||||
|
||||
@property
|
||||
def starting_nodes(self) -> list[NodeModel]:
|
||||
outbound_nodes = {link.sink_id for link in self.links}
|
||||
@@ -393,6 +396,10 @@ class GraphModel(Graph):
|
||||
if node.id not in outbound_nodes or node.id in input_nodes
|
||||
]
|
||||
|
||||
@property
|
||||
def webhook_input_node(self) -> NodeModel | None: # type: ignore
|
||||
return cast(NodeModel, super().webhook_input_node)
|
||||
|
||||
def meta(self) -> "GraphMeta":
|
||||
"""
|
||||
Returns a GraphMeta object with metadata about the graph.
|
||||
@@ -694,6 +701,7 @@ class GraphModel(Graph):
|
||||
version=graph.version,
|
||||
forked_from_id=graph.forkedFromId,
|
||||
forked_from_version=graph.forkedFromVersion,
|
||||
created_at=graph.createdAt,
|
||||
is_active=graph.isActive,
|
||||
name=graph.name or "",
|
||||
description=graph.description or "",
|
||||
@@ -1144,6 +1152,7 @@ def make_graph_model(creatable_graph: Graph, user_id: str) -> GraphModel:
|
||||
return GraphModel(
|
||||
**creatable_graph.model_dump(exclude={"nodes"}),
|
||||
user_id=user_id,
|
||||
created_at=datetime.now(tz=timezone.utc),
|
||||
nodes=[
|
||||
NodeModel(
|
||||
**creatable_node.model_dump(),
|
||||
|
||||
@@ -7,10 +7,9 @@ from backend.data.graph import set_node_webhook
|
||||
from backend.integrations.creds_manager import IntegrationCredentialsManager
|
||||
|
||||
from . import get_webhook_manager, supports_webhooks
|
||||
from .utils import setup_webhook_for_block
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from backend.data.graph import BaseGraph, GraphModel, Node, NodeModel
|
||||
from backend.data.graph import BaseGraph, GraphModel, NodeModel
|
||||
from backend.data.model import Credentials
|
||||
|
||||
from ._base import BaseWebhooksManager
|
||||
@@ -43,32 +42,19 @@ async def _on_graph_activate(graph: "BaseGraph", user_id: str) -> "BaseGraph": .
|
||||
|
||||
async def _on_graph_activate(graph: "BaseGraph | GraphModel", user_id: str):
|
||||
get_credentials = credentials_manager.cached_getter(user_id)
|
||||
updated_nodes = []
|
||||
for new_node in graph.nodes:
|
||||
block_input_schema = cast(BlockSchema, new_node.block.input_schema)
|
||||
|
||||
node_credentials = None
|
||||
if (
|
||||
# Webhook-triggered blocks are only allowed to have 1 credentials input
|
||||
(
|
||||
creds_field_name := next(
|
||||
iter(block_input_schema.get_credentials_fields()), None
|
||||
for creds_field_name in block_input_schema.get_credentials_fields().keys():
|
||||
# Prevent saving graph with non-existent credentials
|
||||
if (
|
||||
creds_meta := new_node.input_default.get(creds_field_name)
|
||||
) and not await get_credentials(creds_meta["id"]):
|
||||
raise ValueError(
|
||||
f"Node #{new_node.id} input '{creds_field_name}' updated with "
|
||||
f"non-existent credentials #{creds_meta['id']}"
|
||||
)
|
||||
)
|
||||
and (creds_meta := new_node.input_default.get(creds_field_name))
|
||||
and not (node_credentials := await get_credentials(creds_meta["id"]))
|
||||
):
|
||||
raise ValueError(
|
||||
f"Node #{new_node.id} input '{creds_field_name}' updated with "
|
||||
f"non-existent credentials #{creds_meta['id']}"
|
||||
)
|
||||
|
||||
updated_node = await on_node_activate(
|
||||
user_id, graph.id, new_node, credentials=node_credentials
|
||||
)
|
||||
updated_nodes.append(updated_node)
|
||||
|
||||
graph.nodes = updated_nodes
|
||||
return graph
|
||||
|
||||
|
||||
@@ -85,20 +71,14 @@ async def on_graph_deactivate(graph: "GraphModel", user_id: str):
|
||||
block_input_schema = cast(BlockSchema, node.block.input_schema)
|
||||
|
||||
node_credentials = None
|
||||
if (
|
||||
# Webhook-triggered blocks are only allowed to have 1 credentials input
|
||||
(
|
||||
creds_field_name := next(
|
||||
iter(block_input_schema.get_credentials_fields()), None
|
||||
for creds_field_name in block_input_schema.get_credentials_fields().keys():
|
||||
if (creds_meta := node.input_default.get(creds_field_name)) and not (
|
||||
node_credentials := await get_credentials(creds_meta["id"])
|
||||
):
|
||||
logger.warning(
|
||||
f"Node #{node.id} input '{creds_field_name}' referenced "
|
||||
f"non-existent credentials #{creds_meta['id']}"
|
||||
)
|
||||
)
|
||||
and (creds_meta := node.input_default.get(creds_field_name))
|
||||
and not (node_credentials := await get_credentials(creds_meta["id"]))
|
||||
):
|
||||
logger.error(
|
||||
f"Node #{node.id} input '{creds_field_name}' referenced non-existent "
|
||||
f"credentials #{creds_meta['id']}"
|
||||
)
|
||||
|
||||
updated_node = await on_node_deactivate(
|
||||
user_id, node, credentials=node_credentials
|
||||
@@ -109,32 +89,6 @@ async def on_graph_deactivate(graph: "GraphModel", user_id: str):
|
||||
return graph
|
||||
|
||||
|
||||
async def on_node_activate(
|
||||
user_id: str,
|
||||
graph_id: str,
|
||||
node: "Node",
|
||||
*,
|
||||
credentials: Optional["Credentials"] = None,
|
||||
) -> "Node":
|
||||
"""Hook to be called when the node is activated/created"""
|
||||
|
||||
if node.block.webhook_config:
|
||||
new_webhook, feedback = await setup_webhook_for_block(
|
||||
user_id=user_id,
|
||||
trigger_block=node.block,
|
||||
trigger_config=node.input_default,
|
||||
for_graph_id=graph_id,
|
||||
)
|
||||
if new_webhook:
|
||||
node = await set_node_webhook(node.id, new_webhook.id)
|
||||
else:
|
||||
logger.debug(
|
||||
f"Node #{node.id} does not have everything for a webhook: {feedback}"
|
||||
)
|
||||
|
||||
return node
|
||||
|
||||
|
||||
async def on_node_deactivate(
|
||||
user_id: str,
|
||||
node: "NodeModel",
|
||||
|
||||
@@ -4,7 +4,6 @@ from typing import TYPE_CHECKING, Optional, cast
|
||||
from pydantic import JsonValue
|
||||
|
||||
from backend.integrations.creds_manager import IntegrationCredentialsManager
|
||||
from backend.integrations.providers import ProviderName
|
||||
from backend.util.settings import Config
|
||||
|
||||
from . import get_webhook_manager, supports_webhooks
|
||||
@@ -13,6 +12,7 @@ if TYPE_CHECKING:
|
||||
from backend.data.block import Block, BlockSchema
|
||||
from backend.data.integrations import Webhook
|
||||
from backend.data.model import Credentials
|
||||
from backend.integrations.providers import ProviderName
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
app_config = Config()
|
||||
@@ -20,7 +20,7 @@ credentials_manager = IntegrationCredentialsManager()
|
||||
|
||||
|
||||
# TODO: add test to assert this matches the actual API route
|
||||
def webhook_ingress_url(provider_name: ProviderName, webhook_id: str) -> str:
|
||||
def webhook_ingress_url(provider_name: "ProviderName", webhook_id: str) -> str:
|
||||
return (
|
||||
f"{app_config.platform_base_url}/api/integrations/{provider_name.value}"
|
||||
f"/webhooks/{webhook_id}/ingress"
|
||||
@@ -144,3 +144,62 @@ async def setup_webhook_for_block(
|
||||
)
|
||||
logger.debug(f"Acquired webhook: {webhook}")
|
||||
return webhook, None
|
||||
|
||||
|
||||
async def migrate_legacy_triggered_graphs():
|
||||
from prisma.models import AgentGraph
|
||||
|
||||
from backend.data.graph import AGENT_GRAPH_INCLUDE, GraphModel, set_node_webhook
|
||||
from backend.data.model import is_credentials_field_name
|
||||
from backend.server.v2.library.db import create_preset
|
||||
from backend.server.v2.library.model import LibraryAgentPresetCreatable
|
||||
|
||||
triggered_graphs = [
|
||||
GraphModel.from_db(_graph)
|
||||
for _graph in await AgentGraph.prisma().find_many(
|
||||
where={
|
||||
"isActive": True,
|
||||
"Nodes": {"some": {"NOT": [{"webhookId": None}]}},
|
||||
},
|
||||
include=AGENT_GRAPH_INCLUDE,
|
||||
)
|
||||
]
|
||||
|
||||
n_migrated_webhooks = 0
|
||||
|
||||
for graph in triggered_graphs:
|
||||
if not ((trigger_node := graph.webhook_input_node) and trigger_node.webhook_id):
|
||||
continue
|
||||
|
||||
# Use trigger node's inputs for the preset
|
||||
preset_credentials = {
|
||||
field_name: creds_meta
|
||||
for field_name, creds_meta in trigger_node.input_default.items()
|
||||
if is_credentials_field_name(field_name)
|
||||
}
|
||||
preset_inputs = {
|
||||
field_name: value
|
||||
for field_name, value in trigger_node.input_default.items()
|
||||
if not is_credentials_field_name(field_name)
|
||||
}
|
||||
|
||||
# Create a triggered preset for the graph
|
||||
await create_preset(
|
||||
graph.user_id,
|
||||
LibraryAgentPresetCreatable(
|
||||
graph_id=graph.id,
|
||||
graph_version=graph.version,
|
||||
inputs=preset_inputs,
|
||||
credentials=preset_credentials,
|
||||
name=graph.name,
|
||||
description=graph.description,
|
||||
webhook_id=trigger_node.webhook_id,
|
||||
is_active=True,
|
||||
),
|
||||
)
|
||||
# Detach webhook from the graph node
|
||||
await set_node_webhook(trigger_node.id, None)
|
||||
|
||||
n_migrated_webhooks += 1
|
||||
|
||||
logger.info(f"Migrated {n_migrated_webhooks} node triggers to triggered presets")
|
||||
|
||||
@@ -18,6 +18,7 @@ import backend.data.block
|
||||
import backend.data.db
|
||||
import backend.data.graph
|
||||
import backend.data.user
|
||||
import backend.integrations.webhooks.utils
|
||||
import backend.server.routers.postmark.postmark
|
||||
import backend.server.routers.v1
|
||||
import backend.server.v2.admin.credit_admin_routes
|
||||
@@ -79,6 +80,8 @@ async def lifespan_context(app: fastapi.FastAPI):
|
||||
await backend.data.user.migrate_and_encrypt_user_integrations()
|
||||
await backend.data.graph.fix_llm_provider_credentials()
|
||||
await backend.data.graph.migrate_llm_models(LlmModel.GPT4O)
|
||||
await backend.integrations.webhooks.utils.migrate_legacy_triggered_graphs()
|
||||
|
||||
with launch_darkly_context():
|
||||
yield
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import json
|
||||
from datetime import datetime
|
||||
from io import BytesIO
|
||||
from unittest.mock import AsyncMock, Mock, patch
|
||||
|
||||
@@ -265,6 +266,7 @@ def test_get_graphs(
|
||||
name="Test Graph",
|
||||
description="A test graph",
|
||||
user_id=test_user_id,
|
||||
created_at=datetime(2025, 9, 4, 13, 37),
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
@@ -299,6 +301,7 @@ def test_get_graph(
|
||||
name="Test Graph",
|
||||
description="A test graph",
|
||||
user_id=test_user_id,
|
||||
created_at=datetime(2025, 9, 4, 13, 37),
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
@@ -348,6 +351,7 @@ def test_delete_graph(
|
||||
name="Test Graph",
|
||||
description="A test graph",
|
||||
user_id=test_user_id,
|
||||
created_at=datetime(2025, 9, 4, 13, 37),
|
||||
)
|
||||
|
||||
mocker.patch(
|
||||
|
||||
@@ -795,10 +795,7 @@ async def create_preset(
|
||||
)
|
||||
for name, data in {
|
||||
**preset.inputs,
|
||||
**{
|
||||
key: creds_meta.model_dump(exclude_none=True)
|
||||
for key, creds_meta in preset.credentials.items()
|
||||
},
|
||||
**preset.credentials,
|
||||
}.items()
|
||||
]
|
||||
},
|
||||
|
||||
@@ -261,6 +261,7 @@ class LibraryAgentPreset(LibraryAgentPresetCreatable):
|
||||
|
||||
id: str
|
||||
user_id: str
|
||||
created_at: datetime.datetime
|
||||
updated_at: datetime.datetime
|
||||
|
||||
webhook: "Webhook | None"
|
||||
@@ -290,6 +291,7 @@ class LibraryAgentPreset(LibraryAgentPresetCreatable):
|
||||
return cls(
|
||||
id=preset.id,
|
||||
user_id=preset.userId,
|
||||
created_at=preset.createdAt,
|
||||
updated_at=preset.updatedAt,
|
||||
graph_id=preset.agentGraphId,
|
||||
graph_version=preset.agentGraphVersion,
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"created_at": "2025-09-04T13:37:00",
|
||||
"credentials_input_schema": {
|
||||
"properties": {},
|
||||
"title": "TestGraphCredentialsInputSchema",
|
||||
|
||||
@@ -5743,6 +5743,11 @@
|
||||
"default": []
|
||||
},
|
||||
"user_id": { "type": "string", "title": "User Id" },
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Created At"
|
||||
},
|
||||
"input_schema": {
|
||||
"additionalProperties": true,
|
||||
"type": "object",
|
||||
@@ -5779,6 +5784,7 @@
|
||||
"name",
|
||||
"description",
|
||||
"user_id",
|
||||
"created_at",
|
||||
"input_schema",
|
||||
"output_schema",
|
||||
"has_external_trigger",
|
||||
@@ -6003,6 +6009,11 @@
|
||||
},
|
||||
"id": { "type": "string", "title": "Id" },
|
||||
"user_id": { "type": "string", "title": "User Id" },
|
||||
"created_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
"title": "Created At"
|
||||
},
|
||||
"updated_at": {
|
||||
"type": "string",
|
||||
"format": "date-time",
|
||||
@@ -6025,6 +6036,7 @@
|
||||
"description",
|
||||
"id",
|
||||
"user_id",
|
||||
"created_at",
|
||||
"updated_at",
|
||||
"webhook"
|
||||
],
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
import "./customedge.css";
|
||||
import { X } from "lucide-react";
|
||||
import { useBezierPath } from "@/hooks/useBezierPath";
|
||||
import { FlowContext } from "./Flow";
|
||||
import { BuilderContext } from "./Flow";
|
||||
import { NodeExecutionResult } from "@/lib/autogpt-server-api";
|
||||
|
||||
export type CustomEdgeData = {
|
||||
@@ -60,7 +60,7 @@ export function CustomEdge({
|
||||
targetY - 5,
|
||||
);
|
||||
const { deleteElements } = useReactFlow<Node, CustomEdge>();
|
||||
const { visualizeBeads } = useContext(FlowContext) ?? {
|
||||
const { visualizeBeads } = useContext(BuilderContext) ?? {
|
||||
visualizeBeads: "no",
|
||||
};
|
||||
|
||||
|
||||
@@ -4,8 +4,8 @@ import React, {
|
||||
useCallback,
|
||||
useRef,
|
||||
useContext,
|
||||
useMemo,
|
||||
} from "react";
|
||||
import Link from "next/link";
|
||||
import { NodeProps, useReactFlow, Node as XYNode, Edge } from "@xyflow/react";
|
||||
import "@xyflow/react/dist/style.css";
|
||||
import "./customnode.css";
|
||||
@@ -16,12 +16,10 @@ import {
|
||||
BlockIOSubSchema,
|
||||
BlockIOStringSubSchema,
|
||||
Category,
|
||||
Node,
|
||||
NodeExecutionResult,
|
||||
BlockUIType,
|
||||
BlockCost,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import {
|
||||
beautifyString,
|
||||
cn,
|
||||
@@ -40,13 +38,14 @@ import {
|
||||
NodeTextBoxInput,
|
||||
} from "./node-input-components";
|
||||
import { getPrimaryCategoryColor } from "@/lib/utils";
|
||||
import { FlowContext } from "./Flow";
|
||||
import { BuilderContext } from "./Flow";
|
||||
import { Badge } from "./ui/badge";
|
||||
import NodeOutputs from "./NodeOutputs";
|
||||
import SchemaTooltip from "./SchemaTooltip";
|
||||
import { IconCoin } from "./ui/icons";
|
||||
import * as Separator from "@radix-ui/react-separator";
|
||||
import * as ContextMenu from "@radix-ui/react-context-menu";
|
||||
import { Alert, AlertDescription } from "./ui/alert";
|
||||
import {
|
||||
DotsVerticalIcon,
|
||||
TrashIcon,
|
||||
@@ -54,7 +53,7 @@ import {
|
||||
ExitIcon,
|
||||
Pencil1Icon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import { Key } from "@phosphor-icons/react";
|
||||
import { InfoIcon, Key } from "@phosphor-icons/react";
|
||||
import useCredits from "@/hooks/useCredits";
|
||||
import { getV1GetAyrshareSsoUrl } from "@/app/api/__generated__/endpoints/integrations/integrations";
|
||||
import { toast } from "@/components/molecules/Toast/use-toast";
|
||||
@@ -85,7 +84,6 @@ export type CustomNodeData = {
|
||||
outputSchema: BlockIORootSchema;
|
||||
hardcodedValues: { [key: string]: any };
|
||||
connections: ConnectionData;
|
||||
webhook?: Node["webhook"];
|
||||
isOutputOpen: boolean;
|
||||
status?: NodeExecutionResult["status"];
|
||||
/** executionResults contains outputs across multiple executions
|
||||
@@ -126,27 +124,26 @@ export const CustomNode = React.memo(
|
||||
Edge
|
||||
>();
|
||||
const isInitialSetup = useRef(true);
|
||||
const flowContext = useContext(FlowContext);
|
||||
const api = useBackendAPI();
|
||||
const builderContext = useContext(BuilderContext);
|
||||
const { formatCredits } = useCredits();
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
|
||||
let nodeFlowId = "";
|
||||
let subGraphID = "";
|
||||
|
||||
if (data.uiType === BlockUIType.AGENT) {
|
||||
// Display the graph's schema instead AgentExecutorBlock's schema.
|
||||
data.inputSchema = data.hardcodedValues?.input_schema || {};
|
||||
data.outputSchema = data.hardcodedValues?.output_schema || {};
|
||||
nodeFlowId = data.hardcodedValues?.graph_id || nodeFlowId;
|
||||
subGraphID = data.hardcodedValues?.graph_id || subGraphID;
|
||||
}
|
||||
|
||||
if (!flowContext) {
|
||||
if (!builderContext) {
|
||||
throw new Error(
|
||||
"FlowContext consumer must be inside FlowEditor component",
|
||||
"BuilderContext consumer must be inside FlowEditor component",
|
||||
);
|
||||
}
|
||||
|
||||
const { setIsAnyModalOpen, getNextNodeId } = flowContext;
|
||||
const { libraryAgent, setIsAnyModalOpen, getNextNodeId } = builderContext;
|
||||
|
||||
useEffect(() => {
|
||||
if (data.executionResults || data.status) {
|
||||
@@ -366,7 +363,6 @@ export const CustomNode = React.memo(
|
||||
return (
|
||||
<div key={noteKey}>
|
||||
<NodeTextBoxInput
|
||||
className=""
|
||||
selfKey={noteKey}
|
||||
schema={noteSchema as BlockIOStringSubSchema}
|
||||
value={getValue(noteKey, data.hardcodedValues)}
|
||||
@@ -402,7 +398,11 @@ export const CustomNode = React.memo(
|
||||
return (
|
||||
!isHidden &&
|
||||
(isRequired || isAdvancedOpen || isConnected || !isAdvanced) && (
|
||||
<div key={propKey} data-id={`input-handle-${propKey}`}>
|
||||
<div
|
||||
key={propKey}
|
||||
data-id={`input-handle-${propKey}`}
|
||||
className="mb-4"
|
||||
>
|
||||
{isConnectable &&
|
||||
!(
|
||||
"oneOf" in propSchema &&
|
||||
@@ -729,60 +729,6 @@ export const CustomNode = React.memo(
|
||||
isCostFilterMatch(cost.cost_filter, inputValues),
|
||||
);
|
||||
|
||||
const [webhookStatus, setWebhookStatus] = useState<
|
||||
"works" | "exists" | "broken" | "none" | "pending" | null
|
||||
>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
![BlockUIType.WEBHOOK, BlockUIType.WEBHOOK_MANUAL].includes(data.uiType)
|
||||
)
|
||||
return;
|
||||
if (!data.webhook) {
|
||||
setWebhookStatus("none");
|
||||
return;
|
||||
}
|
||||
|
||||
setWebhookStatus("pending");
|
||||
api
|
||||
.pingWebhook(data.webhook.id)
|
||||
.then((pinged) => setWebhookStatus(pinged ? "works" : "exists"))
|
||||
.catch((error: Error) =>
|
||||
error.message.includes("ping timed out")
|
||||
? setWebhookStatus("broken")
|
||||
: setWebhookStatus("none"),
|
||||
);
|
||||
}, [data.uiType, data.webhook, api, setWebhookStatus]);
|
||||
|
||||
const webhookStatusDot = useMemo(
|
||||
() =>
|
||||
webhookStatus && (
|
||||
<div
|
||||
className={cn(
|
||||
"size-4 rounded-full border-2",
|
||||
{
|
||||
pending: "animate-pulse border-gray-300 bg-gray-400",
|
||||
works: "border-green-300 bg-green-400",
|
||||
exists: "border-green-200 bg-green-300",
|
||||
broken: "border-red-400 bg-red-500",
|
||||
none: "border-gray-300 bg-gray-400",
|
||||
}[webhookStatus],
|
||||
)}
|
||||
title={
|
||||
{
|
||||
pending: "Checking connection status...",
|
||||
works: "Connected",
|
||||
exists:
|
||||
"Connected (but we could not verify the real-time status)",
|
||||
broken: "The connected webhook is not working",
|
||||
none: "Not connected. Fill out all the required block inputs and save the agent to connect.",
|
||||
}[webhookStatus]
|
||||
}
|
||||
/>
|
||||
),
|
||||
[webhookStatus],
|
||||
);
|
||||
|
||||
const LineSeparator = () => (
|
||||
<div className="bg-white pt-6 dark:bg-gray-800">
|
||||
<Separator.Root className="h-[1px] w-full bg-gray-300 dark:bg-gray-600"></Separator.Root>
|
||||
@@ -798,9 +744,9 @@ export const CustomNode = React.memo(
|
||||
<CopyIcon className="mr-2 h-5 w-5 dark:text-gray-100" />
|
||||
<span className="dark:text-gray-100">Copy</span>
|
||||
</ContextMenu.Item>
|
||||
{nodeFlowId && (
|
||||
{subGraphID && (
|
||||
<ContextMenu.Item
|
||||
onSelect={() => window.open(`/build?flowID=${nodeFlowId}`)}
|
||||
onSelect={() => window.open(`/build?flowID=${subGraphID}`)}
|
||||
className="flex cursor-pointer items-center rounded-md px-3 py-2 hover:bg-gray-100 dark:hover:bg-gray-700"
|
||||
>
|
||||
<ExitIcon className="mr-2 h-5 w-5 dark:text-gray-100" />
|
||||
@@ -917,7 +863,6 @@ export const CustomNode = React.memo(
|
||||
|
||||
<div className="w-auto grow" />
|
||||
|
||||
{webhookStatusDot}
|
||||
<button
|
||||
aria-label="Options"
|
||||
className="cursor-pointer rounded-full border-none bg-transparent p-1 hover:bg-gray-100"
|
||||
@@ -958,35 +903,8 @@ export const CustomNode = React.memo(
|
||||
<div className="mx-5 my-6 rounded-b-xl">
|
||||
{/* Input Handles */}
|
||||
{data.uiType !== BlockUIType.NOTE ? (
|
||||
<div data-id="input-handles">
|
||||
<div data-id="input-handles" className="mb-4">
|
||||
<div>
|
||||
{data.uiType === BlockUIType.WEBHOOK_MANUAL &&
|
||||
(data.webhook ? (
|
||||
<div className="nodrag mr-5 flex flex-col gap-1">
|
||||
Webhook URL:
|
||||
<div className="flex gap-2 rounded-md bg-gray-50 p-2">
|
||||
<code className="select-all text-sm">
|
||||
{data.webhook.url}
|
||||
</code>
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="size-7 flex-none"
|
||||
onClick={() =>
|
||||
data.webhook &&
|
||||
navigator.clipboard.writeText(data.webhook.url)
|
||||
}
|
||||
title="Copy webhook URL"
|
||||
>
|
||||
<CopyIcon className="size-4" />
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="italic text-gray-500">
|
||||
(A Webhook URL will be generated when you save the agent)
|
||||
</p>
|
||||
))}
|
||||
{data.uiType === BlockUIType.AYRSHARE ? (
|
||||
<>
|
||||
{generateAyrshareSSOHandles()}
|
||||
@@ -995,6 +913,33 @@ export const CustomNode = React.memo(
|
||||
BlockUIType.STANDARD,
|
||||
)}
|
||||
</>
|
||||
) : [BlockUIType.WEBHOOK, BlockUIType.WEBHOOK_MANUAL].includes(
|
||||
data.uiType,
|
||||
) ? (
|
||||
<>
|
||||
<Alert className="mb-3 select-none">
|
||||
<AlertDescription className="flex items-center">
|
||||
<InfoIcon className="mr-2 size-6" />
|
||||
<span>
|
||||
You can set up and manage this trigger in your{" "}
|
||||
<Link
|
||||
href={
|
||||
libraryAgent
|
||||
? `/library/agents/${libraryAgent.id}`
|
||||
: "/library"
|
||||
}
|
||||
className="underline"
|
||||
>
|
||||
Agent Library
|
||||
</Link>
|
||||
{!data.backend_id && " (after saving the graph)"}.
|
||||
</span>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
<div className="pointer-events-none opacity-50">
|
||||
{generateInputHandles(data.inputSchema, data.uiType)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
data.inputSchema &&
|
||||
generateInputHandles(data.inputSchema, data.uiType)
|
||||
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
GraphID,
|
||||
LibraryAgent,
|
||||
} from "@/lib/autogpt-server-api";
|
||||
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
|
||||
import { Key, storage } from "@/services/storage/local-storage";
|
||||
import {
|
||||
getTypeColor,
|
||||
@@ -69,7 +68,8 @@ import { Flag, useGetFlag } from "@/services/feature-flags/use-get-flag";
|
||||
// It helps to prevent spamming the history with small movements especially when pressing on a input in a block
|
||||
const MINIMUM_MOVE_BEFORE_LOG = 50;
|
||||
|
||||
type FlowContextType = {
|
||||
type BuilderContextType = {
|
||||
libraryAgent: LibraryAgent | null;
|
||||
visualizeBeads: "no" | "static" | "animate";
|
||||
setIsAnyModalOpen: (isOpen: boolean) => void;
|
||||
getNextNodeId: () => string;
|
||||
@@ -84,7 +84,7 @@ export type NodeDimension = {
|
||||
};
|
||||
};
|
||||
|
||||
export const FlowContext = createContext<FlowContextType | null>(null);
|
||||
export const BuilderContext = createContext<BuilderContextType | null>(null);
|
||||
|
||||
const FlowEditor: React.FC<{
|
||||
flowID?: GraphID;
|
||||
@@ -120,6 +120,7 @@ const FlowEditor: React.FC<{
|
||||
agentRecommendedScheduleCron,
|
||||
setAgentRecommendedScheduleCron,
|
||||
savedAgent,
|
||||
libraryAgent,
|
||||
availableBlocks,
|
||||
availableFlows,
|
||||
getOutputType,
|
||||
@@ -142,20 +143,6 @@ const FlowEditor: React.FC<{
|
||||
flowExecutionID,
|
||||
visualizeBeads !== "no",
|
||||
);
|
||||
const api = useBackendAPI();
|
||||
const [libraryAgent, setLibraryAgent] = useState<LibraryAgent | null>(null);
|
||||
useEffect(() => {
|
||||
if (!flowID) return;
|
||||
api
|
||||
.getLibraryAgentByGraphID(flowID, flowVersion)
|
||||
.then((libraryAgent) => setLibraryAgent(libraryAgent))
|
||||
.catch((error) => {
|
||||
console.warn(
|
||||
`Failed to fetch LibraryAgent for graph #${flowID} v${flowVersion}`,
|
||||
error,
|
||||
);
|
||||
});
|
||||
}, [api, flowID, flowVersion]);
|
||||
|
||||
const router = useRouter();
|
||||
const pathname = usePathname();
|
||||
@@ -835,8 +822,8 @@ const FlowEditor: React.FC<{
|
||||
);
|
||||
|
||||
return (
|
||||
<FlowContext.Provider
|
||||
value={{ visualizeBeads, setIsAnyModalOpen, getNextNodeId }}
|
||||
<BuilderContext.Provider
|
||||
value={{ libraryAgent, visualizeBeads, setIsAnyModalOpen, getNextNodeId }}
|
||||
>
|
||||
<div className={className}>
|
||||
<ReactFlow
|
||||
@@ -931,7 +918,7 @@ const FlowEditor: React.FC<{
|
||||
: "will listen"}{" "}
|
||||
for its trigger and will run when the time is right.
|
||||
<br />
|
||||
You can view its activity in your
|
||||
You can view its activity in your{" "}
|
||||
<Link
|
||||
href={
|
||||
libraryAgent
|
||||
@@ -964,7 +951,7 @@ const FlowEditor: React.FC<{
|
||||
className="fixed bottom-4 right-4 z-20"
|
||||
/>
|
||||
</Suspense>
|
||||
</FlowContext.Provider>
|
||||
</BuilderContext.Provider>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -4,11 +4,6 @@
|
||||
transition: border-color 0.3s ease-in-out;
|
||||
}
|
||||
|
||||
.custom-node [data-id="input-handles"],
|
||||
.custom-node [data-id="input-handles"] > div > div {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
|
||||
.custom-node .custom-switch {
|
||||
padding: 0.5rem 1.25rem;
|
||||
display: flex;
|
||||
|
||||
@@ -14,6 +14,7 @@ import {
|
||||
GraphExecutionID,
|
||||
GraphID,
|
||||
GraphMeta,
|
||||
LibraryAgent,
|
||||
LinkCreatable,
|
||||
NodeCreatable,
|
||||
NodeExecutionResult,
|
||||
@@ -48,6 +49,7 @@ export default function useAgentGraph(
|
||||
const [savedAgent, setSavedAgent] = useState<Graph | null>(null);
|
||||
const [agentDescription, setAgentDescription] = useState<string>("");
|
||||
const [agentName, setAgentName] = useState<string>("");
|
||||
const [libraryAgent, setLibraryAgent] = useState<LibraryAgent | null>(null);
|
||||
const [agentRecommendedScheduleCron, setAgentRecommendedScheduleCron] =
|
||||
useState<string>("");
|
||||
const [allBlocks, setAllBlocks] = useState<Block[]>([]);
|
||||
@@ -197,7 +199,6 @@ export default function useAgentGraph(
|
||||
inputSchema: block.inputSchema,
|
||||
outputSchema: block.outputSchema,
|
||||
hardcodedValues: node.input_default,
|
||||
webhook: node.webhook,
|
||||
uiType: block.uiType,
|
||||
metadata: node.metadata,
|
||||
connections: graph.links
|
||||
@@ -435,6 +436,20 @@ export default function useAgentGraph(
|
||||
});
|
||||
}, [flowID, flowVersion, availableBlocks, api]);
|
||||
|
||||
// Load library agent
|
||||
useEffect(() => {
|
||||
if (!flowID) return;
|
||||
api
|
||||
.getLibraryAgentByGraphID(flowID, flowVersion)
|
||||
.then((libraryAgent) => setLibraryAgent(libraryAgent))
|
||||
.catch((error) => {
|
||||
console.warn(
|
||||
`Failed to fetch LibraryAgent for graph #${flowID} v${flowVersion}`,
|
||||
error,
|
||||
);
|
||||
});
|
||||
}, [api, flowID, flowVersion]);
|
||||
|
||||
// Check if local graph state is in sync with backend
|
||||
const nodesSyncedWithSavedAgent = useMemo(() => {
|
||||
if (!savedAgent || xyNodes.length === 0) return false;
|
||||
@@ -940,6 +955,7 @@ export default function useAgentGraph(
|
||||
agentRecommendedScheduleCron,
|
||||
setAgentRecommendedScheduleCron,
|
||||
savedAgent,
|
||||
libraryAgent,
|
||||
availableBlocks,
|
||||
availableFlows,
|
||||
getOutputType,
|
||||
|
||||
@@ -348,6 +348,7 @@ export type GraphTriggerInfo = {
|
||||
|
||||
/* Mirror of backend/data/graph.py:Graph */
|
||||
export type Graph = GraphMeta & {
|
||||
created_at: Date;
|
||||
nodes: Node[];
|
||||
links: Link[];
|
||||
sub_graphs: Omit<Graph, "sub_graphs">[]; // Flattened sub-graphs
|
||||
@@ -357,6 +358,7 @@ export type GraphUpdateable = Omit<
|
||||
Graph,
|
||||
| "user_id"
|
||||
| "version"
|
||||
| "created_at"
|
||||
| "is_active"
|
||||
| "nodes"
|
||||
| "links"
|
||||
@@ -461,6 +463,7 @@ export type LibraryAgentResponse = {
|
||||
|
||||
export type LibraryAgentPreset = {
|
||||
id: LibraryAgentPresetID;
|
||||
created_at: Date;
|
||||
updated_at: Date;
|
||||
graph_id: GraphID;
|
||||
graph_version: number;
|
||||
@@ -489,7 +492,7 @@ export type LibraryAgentPresetResponse = {
|
||||
|
||||
export type LibraryAgentPresetCreatable = Omit<
|
||||
LibraryAgentPreset,
|
||||
"id" | "updated_at" | "is_active"
|
||||
"id" | "created_at" | "updated_at" | "is_active"
|
||||
> & {
|
||||
is_active?: boolean;
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user