fix(block): Fix Smart Decision Block missing input beads & incompability with input in special characters (#9875)

Smart Decision Block was not able to work with sub agent with custom
name input & the bead were not properly propagated in the execution UI.
The scope of this PR is fixing it.

### Changes 🏗️

* Introduce an easy to parse format of tool edge:
`{tool}_^_{func}_~_{arg}`. Graph using SmartDecisionBlock needs to be
re-saved before execution to work.
* Reduce cluttering on a smart decision block logic.
* Fix beads not being shown for a smart decision block tool calling.

### 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] Execute an SDM with some special character input as a tool

<img width="672" alt="image"
src="https://github.com/user-attachments/assets/873556b3-c16a-4dd1-ad84-bc86c636c406"
/>
This commit is contained in:
Zamil Majdy
2025-04-24 21:24:41 +02:00
committed by GitHub
parent 11a69170b5
commit 91f34966c8
5 changed files with 65 additions and 33 deletions

View File

@@ -246,6 +246,10 @@ class SmartDecisionMakerBlock(Block):
test_credentials=llm.TEST_CREDENTIALS,
)
@staticmethod
def cleanup(s: str):
return re.sub(r"[^a-zA-Z0-9_-]", "_", s).lower()
@staticmethod
def _create_block_function_signature(
sink_node: "Node", links: list["Link"]
@@ -266,7 +270,7 @@ class SmartDecisionMakerBlock(Block):
block = sink_node.block
tool_function: dict[str, Any] = {
"name": re.sub(r"[^a-zA-Z0-9_-]", "_", block.name).lower(),
"name": SmartDecisionMakerBlock.cleanup(block.name),
"description": block.description,
}
@@ -281,7 +285,7 @@ class SmartDecisionMakerBlock(Block):
and sink_block_input_schema.model_fields[link.sink_name].description
else f"The {link.sink_name} of the tool"
)
properties[link.sink_name.lower()] = {
properties[SmartDecisionMakerBlock.cleanup(link.sink_name)] = {
"type": "string",
"description": description,
}
@@ -326,7 +330,7 @@ class SmartDecisionMakerBlock(Block):
)
tool_function: dict[str, Any] = {
"name": re.sub(r"[^a-zA-Z0-9_-]", "_", sink_graph_meta.name).lower(),
"name": SmartDecisionMakerBlock.cleanup(sink_graph_meta.name),
"description": sink_graph_meta.description,
}
@@ -341,7 +345,7 @@ class SmartDecisionMakerBlock(Block):
in sink_block_input_schema["properties"][link.sink_name]
else f"The {link.sink_name} of the tool"
)
properties[link.sink_name.lower()] = {
properties[SmartDecisionMakerBlock.cleanup(link.sink_name)] = {
"type": "string",
"description": description,
}
@@ -503,7 +507,7 @@ class SmartDecisionMakerBlock(Block):
tool_args = json.loads(tool_call.function.arguments)
for arg_name, arg_value in tool_args.items():
yield f"tools_^_{tool_name}_{arg_name}".lower(), arg_value
yield f"tools_^_{tool_name}_~_{arg_name}", arg_value
response.prompt.append(response.raw_response)
yield "conversations", response.prompt

View File

@@ -411,10 +411,13 @@ class GraphModel(Graph):
@staticmethod
def _validate_graph(graph: BaseGraph, for_run: bool = False):
def is_tool_pin(name: str) -> bool:
return name.startswith("tools_^_")
def sanitize(name):
sanitized_name = name.split("_#_")[0].split("_@_")[0].split("_$_")[0]
if sanitized_name.startswith("tools_^_"):
return sanitized_name.split("_^_")[0]
if is_tool_pin(sanitized_name):
return "tools"
return sanitized_name
# Validate smart decision maker nodes
@@ -555,7 +558,7 @@ class GraphModel(Graph):
if block.block_type not in [BlockType.AGENT]
else vals.get("input_schema", {}).get("properties", {}).keys()
)
if sanitized_name not in fields and not name.startswith("tools_^_"):
if sanitized_name not in fields and not is_tool_pin(name):
fields_msg = f"Allowed fields: {fields}"
raise ValueError(f"{prefix}, `{name}` invalid, {fields_msg}")

View File

@@ -9,6 +9,7 @@ import BackendAPI, {
GraphExecutionID,
GraphID,
NodeExecutionResult,
SpecialBlockID,
} from "@/lib/autogpt-server-api";
import {
deepEquals,
@@ -25,6 +26,7 @@ import { InputItem } from "@/components/RunnerUIWrapper";
import { GraphMeta } from "@/lib/autogpt-server-api";
import { default as NextLink } from "next/link";
import { useOnboarding } from "@/components/onboarding/onboarding-provider";
import { get } from "lodash";
const ajv = new Ajv({ strict: false, allErrors: true });
@@ -205,9 +207,7 @@ export default function useAgentGraph(
const newNodes = _newNodes.filter((n) => n !== null);
setEdges(() =>
graph.links.map((link) => {
const adjustedSourceName = link.source_name?.startsWith("tools_^_")
? "tools"
: link.source_name;
const adjustedSourceName = cleanupSourceName(link.source_name);
return {
id: formatEdgeID(link),
type: "custom",
@@ -250,6 +250,37 @@ export default function useAgentGraph(
[],
);
/** --- Smart Decision Maker Block helper functions --- */
const isToolSourceName = (sourceName: string) =>
sourceName.startsWith("tools_^_");
const cleanupSourceName = (sourceName: string) =>
isToolSourceName(sourceName) ? "tools" : sourceName;
const getToolArgName = (sourceName: string) =>
isToolSourceName(sourceName) ? sourceName.split("_~_")[1] : null;
const getToolFuncName = (nodeId: string) => {
const sinkNode = nodes.find((node) => node.id === nodeId);
const sinkNodeName = sinkNode
? sinkNode.data.block_id === SpecialBlockID.AGENT
? sinkNode.data.hardcodedValues?.graph_id
? availableFlows.find(
(flow) => flow.id === sinkNode.data.hardcodedValues.graph_id,
)?.name || "agentexecutorblock"
: "agentexecutorblock"
: sinkNode.data.title.split(" ")[0]
: "";
return sinkNodeName;
};
const normalizeToolName = (str: string) =>
str.replace(/[^a-zA-Z0-9_-]/g, "_").toLowerCase(); // This normalization rule has to match with the one on smart_decision_maker.py
/** ------------------------------ */
const updateEdgeBeads = useCallback(
(executionData: NodeExecutionResult) => {
setEdges((edges) => {
@@ -261,8 +292,18 @@ export default function useAgentGraph(
for (let key in executionData.output_data) {
if (
edge.source !== getFrontendId(executionData.node_id, nodes) ||
edge.sourceHandle !== key
edge.sourceHandle !== cleanupSourceName(key) ||
(isToolSourceName(key) &&
getToolArgName(key) !== edge.targetHandle)
) {
console.log(
key,
cleanupSourceName(key),
edge.targetHandle,
" are not equal ",
getToolArgName(key),
edge.sourceHandle,
);
continue;
}
const count = executionData.output_data[key].length;
@@ -825,27 +866,10 @@ export default function useAgentGraph(
// Special case for SmartDecisionMakerBlock
if (
sourceNode?.data.block_id === "3b191d9f-356f-482d-8238-ba04b6d18381" &&
sourceNode?.data.block_id === SpecialBlockID.SMART_DECISION &&
sourceName.toLowerCase() === "tools"
) {
const sinkNode = nodes.find((node) => node.id === edge.target);
const sinkNodeName = sinkNode
? sinkNode.data.block_id === "e189baac-8c20-45a1-94a7-55177ea42565" // AgentExecutorBlock ID
? sinkNode.data.hardcodedValues?.graph_id
? availableFlows
.find(
(flow) =>
flow.id === sinkNode.data.hardcodedValues.graph_id,
)
?.name?.toLowerCase()
.replace(/ /g, "_") || "agentexecutorblock"
: "agentexecutorblock"
: sinkNode.data.title.toLowerCase().replace(/ /g, "_").split("_")[0]
: "";
sourceName =
`tools_^_${sinkNodeName}_${edge.targetHandle || ""}`.toLowerCase();
sourceName = `tools_^_${normalizeToolName(getToolFuncName(edge.target))}_~_${normalizeToolName(edge.targetHandle || "")}`;
}
return {
source_id: edge.source,
@@ -894,7 +918,7 @@ export default function useAgentGraph(
console.debug(
"Saving new Graph version; old vs new:",
comparedPayload,
payload,
comparedSavedAgent,
);
setNodesSyncedWithSavedAgent(false);

View File

@@ -558,6 +558,7 @@ export enum BlockUIType {
export enum SpecialBlockID {
AGENT = "e189baac-8c20-45a1-94a7-55177ea42565",
SMART_DECISION = "3b191d9f-356f-482d-8238-ba04b6d18381",
OUTPUT = "363ae599-353e-4804-937e-b2ee3cef3da4",
}

View File

@@ -23,7 +23,7 @@ export function hashString(str: string): number {
/** Derived from https://stackoverflow.com/a/32922084 */
export function deepEquals(x: any, y: any): boolean {
const ok = Object.keys,
const ok = (obj: any) => Object.keys(obj).filter((key) => obj[key] !== null),
tx = typeof x,
ty = typeof y;