fix(platform/library): Fix UX for webhook-triggered runs (#9680)

- Resolves #9679

### Changes 🏗️

Frontend:
- Fix crash on `payload` graph input
- Fix crash on object type agent I/O values
- Hide "+ New run" if `graph.webhook_id` is set

Backend:
- Add computed field `webhook_id` to `GraphModel`
  - Add computed property `webhook_input_node` to `GraphModel`
- Refactor:
  - Move `Node.webhook_id` -> `NodeModel.webhook_id`
  - Move `NodeModel.block` -> `Node.block` (computed property)
  - Replace `get_block(node.block_id)` with `node.block` where sensible

### 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] Create and run a simple graph
  - [x] Create a graph with a webhook trigger and ensure it works
- [x] Check out the runs of a webhook-triggered graph and ensure the
page works
This commit is contained in:
Reinier van der Leer
2025-04-03 19:31:02 +02:00
committed by GitHub
parent 7179f9cea0
commit 77a44b1213
8 changed files with 108 additions and 103 deletions

View File

@@ -14,7 +14,6 @@ from backend.data.block import (
BlockOutput,
BlockSchema,
BlockType,
get_block,
)
from backend.data.model import SchemaField
from backend.util import json
@@ -264,9 +263,7 @@ class SmartDecisionMakerBlock(Block):
Raises:
ValueError: If the block specified by sink_node.block_id is not found.
"""
block = get_block(sink_node.block_id)
if not block:
raise ValueError(f"Block not found: {sink_node.block_id}")
block = sink_node.block
tool_function: dict[str, Any] = {
"name": re.sub(r"[^a-zA-Z0-9_-]", "_", block.name).lower(),

View File

@@ -53,22 +53,23 @@ class Node(BaseDbModel):
input_links: list[Link] = []
output_links: list[Link] = []
webhook_id: Optional[str] = None
@property
def block(self) -> Block[BlockSchema, BlockSchema]:
block = get_block(self.block_id)
if not block:
raise ValueError(
f"Block #{self.block_id} does not exist -> Node #{self.id} is invalid"
)
return block
class NodeModel(Node):
graph_id: str
graph_version: int
webhook_id: Optional[str] = None
webhook: Optional[Webhook] = None
@property
def block(self) -> Block[BlockSchema, BlockSchema]:
block = get_block(self.block_id)
if not block:
raise ValueError(f"Block #{self.block_id} does not exist")
return block
@staticmethod
def from_db(node: AgentNode, for_export: bool = False) -> "NodeModel":
obj = NodeModel(
@@ -88,8 +89,7 @@ class NodeModel(Node):
return obj
def is_triggered_by_event_type(self, event_type: str) -> bool:
if not (block := get_block(self.block_id)):
raise ValueError(f"Block #{self.block_id} not found for node #{self.id}")
block = self.block
if not block.webhook_config:
raise TypeError("This method can't be used on non-webhook blocks")
if not block.webhook_config.event_filter_input:
@@ -166,11 +166,10 @@ class BaseGraph(BaseDbModel):
def input_schema(self) -> dict[str, Any]:
return self._generate_schema(
*(
(b.input_schema, node.input_default)
(block.input_schema, node.input_default)
for node in self.nodes
if (b := get_block(node.block_id))
and b.block_type == BlockType.INPUT
and issubclass(b.input_schema, AgentInputBlock.Input)
if (block := node.block).block_type == BlockType.INPUT
and issubclass(block.input_schema, AgentInputBlock.Input)
)
)
@@ -179,11 +178,10 @@ class BaseGraph(BaseDbModel):
def output_schema(self) -> dict[str, Any]:
return self._generate_schema(
*(
(b.input_schema, node.input_default)
(block.input_schema, node.input_default)
for node in self.nodes
if (b := get_block(node.block_id))
and b.block_type == BlockType.OUTPUT
and issubclass(b.input_schema, AgentOutputBlock.Input)
if (block := node.block).block_type == BlockType.OUTPUT
and issubclass(block.input_schema, AgentOutputBlock.Input)
)
)
@@ -228,13 +226,16 @@ class GraphModel(Graph):
user_id: str
nodes: list[NodeModel] = [] # type: ignore
@computed_field
@property
def starting_nodes(self) -> list[Node]:
def has_webhook_trigger(self) -> bool:
return self.webhook_input_node is not None
@property
def starting_nodes(self) -> list[NodeModel]:
outbound_nodes = {link.sink_id for link in self.links}
input_nodes = {
v.id
for v in self.nodes
if (b := get_block(v.block_id)) and b.block_type == BlockType.INPUT
node.id for node in self.nodes if node.block.block_type == BlockType.INPUT
}
return [
node
@@ -242,6 +243,18 @@ class GraphModel(Graph):
if node.id not in outbound_nodes or node.id in input_nodes
]
@property
def webhook_input_node(self) -> NodeModel | None:
return next(
(
node
for node in self.nodes
if node.block.block_type
in (BlockType.WEBHOOK, BlockType.WEBHOOK_MANUAL)
),
None,
)
def reassign_ids(self, user_id: str, reassign_graph_id: bool = False):
"""
Reassigns all IDs in the graph to new UUIDs.
@@ -391,9 +404,7 @@ class GraphModel(Graph):
node_map = {v.id: v for v in graph.nodes}
def is_static_output_block(nid: str) -> bool:
bid = node_map[nid].block_id
b = get_block(bid)
return b.static_output if b else False
return node_map[nid].block.static_output
# Links: links are connected and the connected pin data type are compatible.
for link in graph.links:
@@ -747,7 +758,6 @@ async def __create_graph(tx, graph: Graph, user_id: str):
"agentBlockId": node.block_id,
"constantInput": Json(node.input_default),
"metadata": Json(node.metadata),
"webhookId": node.webhook_id,
}
for graph in graphs
for node in graph.nodes

View File

@@ -157,10 +157,7 @@ def execute_node(
node = db_client.get_node(node_id)
node_block = get_block(node.block_id)
if not node_block:
logger.error(f"Block {node.block_id} not found.")
return
node_block = node.block
def push_output(output_name: str, output_data: Any) -> None:
_push_node_execution_output(
@@ -1016,10 +1013,10 @@ class ExecutionManager(AppService):
nodes_input = []
for node in graph.starting_nodes:
input_data = {}
block = get_block(node.block_id)
block = node.block
# Invalid block & Note block should never be executed.
if not block or block.block_type == BlockType.NOTE:
# Note block should never be executed.
if block.block_type == BlockType.NOTE:
continue
# Extract request input data, and assign it to the input pin.
@@ -1127,9 +1124,7 @@ class ExecutionManager(AppService):
"""Checks all credentials for all nodes of the graph"""
for node in graph.nodes:
block = get_block(node.block_id)
if not block:
raise ValueError(f"Unknown block {node.block_id} for node #{node.id}")
block = node.block
# Find any fields of type CredentialsMetaInput
credentials_fields = cast(

View File

@@ -1,7 +1,7 @@
import logging
from typing import TYPE_CHECKING, Callable, Optional, cast
from backend.data.block import BlockSchema, BlockWebhookConfig, get_block
from backend.data.block import BlockSchema, BlockWebhookConfig
from backend.data.graph import set_node_webhook
from backend.integrations.webhooks import get_webhook_manager, supports_webhooks
@@ -29,12 +29,7 @@ async def on_graph_activate(
# Compare nodes in new_graph_version with previous_graph_version
updated_nodes = []
for new_node in graph.nodes:
block = get_block(new_node.block_id)
if not block:
raise ValueError(
f"Node #{new_node.id} is instance of unknown block #{new_node.block_id}"
)
block_input_schema = cast(BlockSchema, block.input_schema)
block_input_schema = cast(BlockSchema, new_node.block.input_schema)
node_credentials = None
if (
@@ -75,12 +70,7 @@ async def on_graph_deactivate(
"""
updated_nodes = []
for node in graph.nodes:
block = get_block(node.block_id)
if not block:
raise ValueError(
f"Node #{node.id} is instance of unknown block #{node.block_id}"
)
block_input_schema = cast(BlockSchema, block.input_schema)
block_input_schema = cast(BlockSchema, node.block.input_schema)
node_credentials = None
if (
@@ -113,11 +103,7 @@ async def on_node_activate(
) -> "NodeModel":
"""Hook to be called when the node is activated/created"""
block = get_block(node.block_id)
if not block:
raise ValueError(
f"Node #{node.id} is instance of unknown block #{node.block_id}"
)
block = node.block
if not block.webhook_config:
return node
@@ -224,11 +210,7 @@ async def on_node_deactivate(
"""Hook to be called when node is deactivated/deleted"""
logger.debug(f"Deactivating node #{node.id}")
block = get_block(node.block_id)
if not block:
raise ValueError(
f"Node #{node.id} is instance of unknown block #{node.block_id}"
)
block = node.block
if not block.webhook_config:
return node

View File

@@ -8,8 +8,8 @@ import {
GraphExecution,
GraphExecutionID,
GraphExecutionMeta,
Graph,
GraphID,
GraphMeta,
LibraryAgent,
LibraryAgentID,
Schedule,
@@ -30,7 +30,7 @@ export default function AgentRunsPage(): React.ReactElement {
// ============================ STATE =============================
const [graph, setGraph] = useState<GraphMeta | null>(null);
const [graph, setGraph] = useState<Graph | null>(null);
const [agent, setAgent] = useState<LibraryAgent | null>(null);
const [agentRuns, setAgentRuns] = useState<GraphExecutionMeta[]>([]);
const [schedules, setSchedules] = useState<Schedule[]>([]);
@@ -63,9 +63,7 @@ export default function AgentRunsPage(): React.ReactElement {
setSelectedSchedule(schedule);
}, []);
const [graphVersions, setGraphVersions] = useState<Record<number, GraphMeta>>(
{},
);
const [graphVersions, setGraphVersions] = useState<Record<number, Graph>>({});
const getGraphVersion = useCallback(
async (graphID: GraphID, version: number) => {
if (graphVersions[version]) return graphVersions[version];
@@ -262,6 +260,7 @@ export default function AgentRunsPage(): React.ReactElement {
agentRuns={agentRuns}
schedules={schedules}
selectedView={selectedView}
allowDraftNewRun={!graph.has_webhook_trigger}
onSelectRun={selectRun}
onSelectSchedule={selectSchedule}
onSelectDraftNewRun={openRunDraftView}

View File

@@ -4,10 +4,10 @@ import moment from "moment";
import { useBackendAPI } from "@/lib/autogpt-server-api/context";
import {
Graph,
GraphExecution,
GraphExecutionID,
GraphExecutionMeta,
GraphMeta,
} from "@/lib/autogpt-server-api";
import type { ButtonAction } from "@/components/agptui/types";
@@ -29,7 +29,7 @@ export default function AgentRunDetailsView({
onRun,
deleteRun,
}: {
graph: GraphMeta;
graph: Graph;
run: GraphExecution | GraphExecutionMeta;
agentActions: ButtonAction[];
onRun: (runID: GraphExecutionID) => void;
@@ -64,7 +64,14 @@ export default function AgentRunDetailsView({
}, [run, runStatus]);
const agentRunInputs:
| Record<string, { title?: string; /* type: BlockIOSubType; */ value: any }>
| Record<
string,
{
title?: string;
/* type: BlockIOSubType; */
value: string | number | undefined;
}
>
| undefined = useMemo(() => {
if (!("inputs" in run)) return undefined;
// TODO: show (link to) preset - https://github.com/Significant-Gravitas/AutoGPT/issues/9168
@@ -74,9 +81,9 @@ export default function AgentRunDetailsView({
Object.entries(run.inputs).map(([k, v]) => [
k,
{
title: graph.input_schema.properties[k].title,
title: graph.input_schema.properties[k]?.title,
// type: graph.input_schema.properties[k].type, // TODO: implement typed graph inputs
value: v,
value: typeof v == "object" ? JSON.stringify(v, undefined, 2) : v,
},
]),
);
@@ -106,7 +113,11 @@ export default function AgentRunDetailsView({
const agentRunOutputs:
| Record<
string,
{ title?: string; /* type: BlockIOSubType; */ values: Array<any> }
{
title?: string;
/* type: BlockIOSubType; */
values: Array<React.ReactNode>;
}
>
| null
| undefined = useMemo(() => {
@@ -115,12 +126,14 @@ export default function AgentRunDetailsView({
// Add type info from agent input schema
return Object.fromEntries(
Object.entries(run.outputs).map(([k, v]) => [
Object.entries(run.outputs).map(([k, vv]) => [
k,
{
title: graph.output_schema.properties[k].title,
/* type: agent.output_schema.properties[k].type */
values: v,
values: vv.map((v) =>
typeof v == "object" ? JSON.stringify(v, undefined, 2) : v,
),
},
]),
);
@@ -142,7 +155,8 @@ export default function AgentRunDetailsView({
},
] satisfies ButtonAction[])
: []),
...(["success", "failed", "stopped"].includes(runStatus)
...(["success", "failed", "stopped"].includes(runStatus) &&
!graph.has_webhook_trigger
? [
{
label: (

View File

@@ -23,6 +23,7 @@ interface AgentRunsSelectorListProps {
agentRuns: GraphExecutionMeta[];
schedules: Schedule[];
selectedView: { type: "run" | "schedule"; id?: string };
allowDraftNewRun?: boolean;
onSelectRun: (id: GraphExecutionID) => void;
onSelectSchedule: (schedule: Schedule) => void;
onSelectDraftNewRun: () => void;
@@ -36,6 +37,7 @@ export default function AgentRunsSelectorList({
agentRuns,
schedules,
selectedView,
allowDraftNewRun = true,
onSelectRun,
onSelectSchedule,
onSelectDraftNewRun,
@@ -49,19 +51,21 @@ export default function AgentRunsSelectorList({
return (
<aside className={cn("flex flex-col gap-4", className)}>
<Button
size="card"
className={
"mb-4 hidden h-16 w-72 items-center gap-2 py-6 lg:flex xl:w-80 " +
(selectedView.type == "run" && !selectedView.id
? "agpt-card-selected text-accent"
: "")
}
onClick={onSelectDraftNewRun}
>
<Plus className="h-6 w-6" />
<span>New run</span>
</Button>
{allowDraftNewRun && (
<Button
size="card"
className={
"mb-4 hidden h-16 w-72 items-center gap-2 py-6 lg:flex xl:w-80 " +
(selectedView.type == "run" && !selectedView.id
? "agpt-card-selected text-accent"
: "")
}
onClick={onSelectDraftNewRun}
>
<Plus className="h-6 w-6" />
<span>New run</span>
</Button>
)}
<div className="flex gap-2">
<Badge
@@ -89,19 +93,21 @@ export default function AgentRunsSelectorList({
<ScrollArea className="lg:h-[calc(100vh-200px)]">
<div className="flex gap-2 lg:flex-col">
{/* New Run button - only in small layouts */}
<Button
size="card"
className={
"flex h-28 w-40 items-center gap-2 py-6 lg:hidden " +
(selectedView.type == "run" && !selectedView.id
? "agpt-card-selected text-accent"
: "")
}
onClick={onSelectDraftNewRun}
>
<Plus className="h-6 w-6" />
<span>New run</span>
</Button>
{allowDraftNewRun && (
<Button
size="card"
className={
"flex h-28 w-40 items-center gap-2 py-6 lg:hidden " +
(selectedView.type == "run" && !selectedView.id
? "agpt-card-selected text-accent"
: "")
}
onClick={onSelectDraftNewRun}
>
<Plus className="h-6 w-6" />
<span>New run</span>
</Button>
)}
{activeListTab === "runs"
? agentRuns

View File

@@ -302,6 +302,7 @@ export type GraphIOSubSchema = Omit<
export type Graph = GraphMeta & {
nodes: Array<Node>;
links: Array<Link>;
has_webhook_trigger: boolean;
};
export type GraphUpdateable = Omit<
@@ -312,6 +313,7 @@ export type GraphUpdateable = Omit<
| "links"
| "input_schema"
| "output_schema"
| "has_webhook_trigger"
> & {
version?: number;
is_active?: boolean;