mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(blocks): Enabled block Usage for Smart Decision Maker Block (#9514)
Originally we did not allow Blocks to be used as tools due to the limitations of communicating the correct tool function signatures. It has however, been decided to allow them to be used knowing that there are limitations with them. ### Changes 🏗️ - Added ability to execute blocks as tools ### Checklist 📋 <img width="613" alt="Screenshot 2025-02-25 at 12 49 26" src="https://github.com/user-attachments/assets/e614f56d-2bdc-46c9-8c2c-e56f80343bde" /> - create an agent with an SDM block and a block as a tool - run agent and make sure the block can be called as a tool #### 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: --------- Co-authored-by: Zamil Majdy <zamil.majdy@agpt.co>
This commit is contained in:
@@ -1,14 +1,25 @@
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
from typing import Any, List
|
||||
from typing import TYPE_CHECKING, Any, List
|
||||
|
||||
from autogpt_libs.utils.cache import thread_cached
|
||||
|
||||
import backend.blocks.llm as llm
|
||||
from backend.data.block import Block, BlockCategory, BlockOutput, BlockSchema, BlockType
|
||||
from backend.blocks.agent import AgentExecutorBlock
|
||||
from backend.data.block import (
|
||||
Block,
|
||||
BlockCategory,
|
||||
BlockOutput,
|
||||
BlockSchema,
|
||||
BlockType,
|
||||
get_blocks,
|
||||
)
|
||||
from backend.data.model import SchemaField
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from backend.data.graph import Graph, Link, Node
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
@@ -96,7 +107,7 @@ class SmartDecisionMakerBlock(Block):
|
||||
)
|
||||
|
||||
# If I import Graph here, it will break with a circular import.
|
||||
def _get_tool_graph_metadata(self, node_id: str, graph: Any) -> List[Any]:
|
||||
def _get_tool_graph_metadata(self, node_id: str, graph: "Graph") -> List["Graph"]:
|
||||
"""
|
||||
Retrieves metadata for tool graphs linked to a specified node within a graph.
|
||||
|
||||
@@ -121,7 +132,7 @@ class SmartDecisionMakerBlock(Block):
|
||||
|
||||
for link_id in tool_links:
|
||||
node = next((node for node in graph.nodes if node.id == link_id), None)
|
||||
if node:
|
||||
if node and node.block_id == AgentExecutorBlock().id:
|
||||
node_graph_meta = db_client.get_graph_metadata(
|
||||
node.input_default["graph_id"], node.input_default["graph_version"]
|
||||
)
|
||||
@@ -130,12 +141,130 @@ class SmartDecisionMakerBlock(Block):
|
||||
|
||||
return graph_meta
|
||||
|
||||
@staticmethod
|
||||
def _create_block_function_signature(
|
||||
sink_node: "Node", links: list["Link"]
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Creates a function signature for a block node.
|
||||
|
||||
Args:
|
||||
sink_node: The node for which to create a function signature.
|
||||
links: The list of links connected to the sink node.
|
||||
|
||||
Returns:
|
||||
A dictionary representing the function signature in the format expected by LLM tools.
|
||||
|
||||
Raises:
|
||||
ValueError: If the block specified by sink_node.block_id is not found.
|
||||
"""
|
||||
block = get_blocks()[sink_node.block_id]
|
||||
if not block:
|
||||
raise ValueError(f"Block not found: {sink_node.block_id}")
|
||||
|
||||
tool_function: dict[str, Any] = {
|
||||
"name": re.sub(r"[^a-zA-Z0-9_-]", "_", block().name).lower(),
|
||||
"description": block().description,
|
||||
}
|
||||
|
||||
properties = {}
|
||||
required = []
|
||||
|
||||
for link in links:
|
||||
sink_block_input_schema = block().input_schema
|
||||
description = (
|
||||
sink_block_input_schema.model_fields[link.sink_name].description
|
||||
if link.sink_name in sink_block_input_schema.model_fields
|
||||
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()] = {
|
||||
"type": "string",
|
||||
"description": description,
|
||||
}
|
||||
|
||||
tool_function["parameters"] = {
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
"required": required,
|
||||
"additionalProperties": False,
|
||||
"strict": True,
|
||||
}
|
||||
|
||||
return {"type": "function", "function": tool_function}
|
||||
|
||||
@staticmethod
|
||||
def _create_agent_function_signature(
|
||||
sink_node: "Node", links: list["Link"], tool_graph_metadata: list["Graph"]
|
||||
) -> dict[str, Any]:
|
||||
"""
|
||||
Creates a function signature for an agent node.
|
||||
|
||||
Args:
|
||||
sink_node: The agent node for which to create a function signature.
|
||||
links: The list of links connected to the sink node.
|
||||
tool_graph_metadata: List of metadata for available tool graphs.
|
||||
|
||||
Returns:
|
||||
A dictionary representing the function signature in the format expected by LLM tools.
|
||||
|
||||
Raises:
|
||||
ValueError: If the graph metadata for the specified graph_id and graph_version is not found.
|
||||
"""
|
||||
graph_id = sink_node.input_default["graph_id"]
|
||||
graph_version = sink_node.input_default["graph_version"]
|
||||
|
||||
sink_graph_meta = next(
|
||||
(
|
||||
meta
|
||||
for meta in tool_graph_metadata
|
||||
if meta.id == graph_id and meta.version == graph_version
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not sink_graph_meta:
|
||||
raise ValueError(
|
||||
f"Sink graph metadata not found: {graph_id} {graph_version}"
|
||||
)
|
||||
|
||||
tool_function: dict[str, Any] = {
|
||||
"name": re.sub(r"[^a-zA-Z0-9_-]", "_", sink_graph_meta.name).lower(),
|
||||
"description": sink_graph_meta.description,
|
||||
}
|
||||
|
||||
properties = {}
|
||||
required = []
|
||||
|
||||
for link in links:
|
||||
sink_block_input_schema = sink_node.input_default["input_schema"]
|
||||
description = (
|
||||
sink_block_input_schema["properties"][link.sink_name]["description"]
|
||||
if "description"
|
||||
in sink_block_input_schema["properties"][link.sink_name]
|
||||
else f"The {link.sink_name} of the tool"
|
||||
)
|
||||
properties[link.sink_name.lower()] = {
|
||||
"type": "string",
|
||||
"description": description,
|
||||
}
|
||||
|
||||
tool_function["parameters"] = {
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
"required": required,
|
||||
"additionalProperties": False,
|
||||
"strict": True,
|
||||
}
|
||||
|
||||
return {"type": "function", "function": tool_function}
|
||||
|
||||
@staticmethod
|
||||
def _create_function_signature(
|
||||
# If I import Graph here, it will break with a circular import.
|
||||
node_id: str,
|
||||
graph: Any,
|
||||
tool_graph_metadata: List[Any],
|
||||
graph: "Graph",
|
||||
tool_graph_metadata: List["Graph"],
|
||||
) -> list[dict[str, Any]]:
|
||||
"""
|
||||
Creates function signatures for tools linked to a specified node within a graph.
|
||||
@@ -192,55 +321,19 @@ class SmartDecisionMakerBlock(Block):
|
||||
if not sink_node:
|
||||
raise ValueError(f"Sink node not found: {links[0].sink_id}")
|
||||
|
||||
graph_id = sink_node.input_default["graph_id"]
|
||||
graph_version = sink_node.input_default["graph_version"]
|
||||
|
||||
sink_graph_meta = next(
|
||||
(
|
||||
meta
|
||||
for meta in tool_graph_metadata
|
||||
if meta.id == graph_id and meta.version == graph_version
|
||||
),
|
||||
None,
|
||||
)
|
||||
|
||||
if not sink_graph_meta:
|
||||
raise ValueError(
|
||||
f"Sink graph metadata not found: {graph_id} {graph_version}"
|
||||
if sink_node.block_id == AgentExecutorBlock().id:
|
||||
return_tool_functions.append(
|
||||
SmartDecisionMakerBlock._create_agent_function_signature(
|
||||
sink_node, links, tool_graph_metadata
|
||||
)
|
||||
)
|
||||
else:
|
||||
return_tool_functions.append(
|
||||
SmartDecisionMakerBlock._create_block_function_signature(
|
||||
sink_node, links
|
||||
)
|
||||
)
|
||||
|
||||
tool_function: dict[str, Any] = {
|
||||
"name": re.sub(r"[^a-zA-Z0-9_-]", "_", sink_graph_meta.name).lower(),
|
||||
"description": sink_graph_meta.description,
|
||||
}
|
||||
|
||||
properties = {}
|
||||
required = []
|
||||
|
||||
for link in links:
|
||||
sink_block_input_schema = sink_node.input_default["input_schema"]
|
||||
description = (
|
||||
sink_block_input_schema["properties"][link.sink_name]["description"]
|
||||
if "description"
|
||||
in sink_block_input_schema["properties"][link.sink_name]
|
||||
else f"The {link.sink_name} of the tool"
|
||||
)
|
||||
properties[link.sink_name.lower()] = {
|
||||
"type": "string",
|
||||
"description": description,
|
||||
}
|
||||
|
||||
tool_function["parameters"] = {
|
||||
"type": "object",
|
||||
"properties": properties,
|
||||
"required": required,
|
||||
"additionalProperties": False,
|
||||
"strict": True,
|
||||
}
|
||||
|
||||
return_tool_functions.append(
|
||||
{"type": "function", "function": tool_function}
|
||||
)
|
||||
return return_tool_functions
|
||||
|
||||
def run(
|
||||
|
||||
@@ -342,16 +342,6 @@ class GraphModel(Graph):
|
||||
for link in self.links:
|
||||
input_links[link.sink_id].append(link)
|
||||
|
||||
# Check if the link is a tool link from a smart decision maker to a non-agent node
|
||||
if (
|
||||
link.source_id in smart_decision_maker_nodes
|
||||
and link.source_name.startswith("tools_^_")
|
||||
and link.sink_id not in agent_nodes
|
||||
):
|
||||
raise ValueError(
|
||||
f"Smart decision maker node {link.source_id} cannot link to non-agent node {link.sink_id}"
|
||||
)
|
||||
|
||||
# Nodes: required fields are filled or connected and dependencies are satisfied
|
||||
for node in self.nodes:
|
||||
if (block := nodes_block.get(node.id)) is None:
|
||||
@@ -444,16 +434,16 @@ class GraphModel(Graph):
|
||||
if i == 0:
|
||||
fields = (
|
||||
block.output_schema.get_fields()
|
||||
if block.block_type not in [BlockType.AGENT, BlockType.AI]
|
||||
if block.block_type not in [BlockType.AGENT]
|
||||
else vals.get("output_schema", {}).get("properties", {}).keys()
|
||||
)
|
||||
else:
|
||||
fields = (
|
||||
block.input_schema.get_fields()
|
||||
if block.block_type not in [BlockType.AGENT, BlockType.AI]
|
||||
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 name.startswith("tools_^_"):
|
||||
fields_msg = f"Allowed fields: {fields}"
|
||||
raise ValueError(f"{suffix}, `{name}` invalid, {fields_msg}")
|
||||
|
||||
|
||||
@@ -195,6 +195,9 @@ async def test_smart_decision_maker_function_signature(server: SpinTestServer):
|
||||
"output_schema": test_tool_graph.output_schema,
|
||||
},
|
||||
),
|
||||
graph.Node(
|
||||
block_id=StoreValueBlock().id,
|
||||
),
|
||||
]
|
||||
|
||||
links = [
|
||||
@@ -210,6 +213,12 @@ async def test_smart_decision_maker_function_signature(server: SpinTestServer):
|
||||
source_name="tools_^_sample_tool_input_2",
|
||||
sink_name="input_2",
|
||||
),
|
||||
graph.Link(
|
||||
source_id=nodes[0].id,
|
||||
sink_id=nodes[2].id,
|
||||
source_name="tools_^_store_value_input",
|
||||
sink_name="input",
|
||||
),
|
||||
]
|
||||
|
||||
test_graph = graph.Graph(
|
||||
@@ -224,26 +233,25 @@ async def test_smart_decision_maker_function_signature(server: SpinTestServer):
|
||||
test_graph.nodes[0].id, test_graph, [test_tool_graph]
|
||||
)
|
||||
assert tool_functions is not None, "Tool functions should not be None"
|
||||
assert (
|
||||
len(tool_functions) == 1
|
||||
), f"Expected 1 tool function, got {len(tool_functions)}"
|
||||
|
||||
tool_function = next(
|
||||
filter(lambda x: x["function"]["name"] == "testgraph", tool_functions),
|
||||
None,
|
||||
assert (
|
||||
len(tool_functions) == 2
|
||||
), f"Expected 2 tool functions, got {len(tool_functions)}"
|
||||
|
||||
# Check the first tool function (testgraph)
|
||||
assert tool_functions[0]["type"] == "function"
|
||||
assert tool_functions[0]["function"]["name"] == "testgraph"
|
||||
assert tool_functions[0]["function"]["description"] == "Test graph description"
|
||||
assert "input_1" in tool_functions[0]["function"]["parameters"]["properties"]
|
||||
assert "input_2" in tool_functions[0]["function"]["parameters"]["properties"]
|
||||
|
||||
# Check the second tool function (storevalueblock)
|
||||
assert tool_functions[1]["type"] == "function"
|
||||
assert tool_functions[1]["function"]["name"] == "storevalueblock"
|
||||
assert "input" in tool_functions[1]["function"]["parameters"]["properties"]
|
||||
assert (
|
||||
tool_functions[1]["function"]["parameters"]["properties"]["input"][
|
||||
"description"
|
||||
]
|
||||
== "Trigger the block to produce the output. The value is only used when `data` is None."
|
||||
)
|
||||
assert tool_function is not None, f"testgraph function not found: {tool_functions}"
|
||||
assert (
|
||||
tool_function["function"]["name"] == "testgraph"
|
||||
), "Incorrect function name for testgraph"
|
||||
assert (
|
||||
tool_function["function"]["parameters"]["properties"]["input_1"]["type"]
|
||||
== "string"
|
||||
), "Input type for input_1 should be 'string'"
|
||||
assert (
|
||||
tool_function["function"]["parameters"]["properties"]["input_2"]["type"]
|
||||
== "string"
|
||||
), "Input type for input_2 should be 'string'"
|
||||
assert (
|
||||
tool_function["function"]["parameters"]["required"] == []
|
||||
), "Required parameters should be an empty list"
|
||||
|
||||
@@ -800,7 +800,13 @@ export default function useAgentGraph(
|
||||
|
||||
const links = edges.map((edge) => {
|
||||
let sourceName = edge.sourceHandle || "";
|
||||
if (sourceName.toLowerCase() === "tools") {
|
||||
const sourceNode = nodes.find((node) => node.id === edge.source);
|
||||
|
||||
// Special case for SmartDecisionMakerBlock
|
||||
if (
|
||||
sourceNode?.data.block_id === "3b191d9f-356f-482d-8238-ba04b6d18381" &&
|
||||
sourceName.toLowerCase() === "tools"
|
||||
) {
|
||||
const sinkNode = nodes.find((node) => node.id === edge.target);
|
||||
|
||||
const sinkNodeName = sinkNode
|
||||
@@ -814,7 +820,7 @@ export default function useAgentGraph(
|
||||
?.name?.toLowerCase()
|
||||
.replace(/ /g, "_") || "agentexecutorblock"
|
||||
: "agentexecutorblock"
|
||||
: sinkNode.data.title.toLowerCase().replace(/ /g, "_")
|
||||
: sinkNode.data.title.toLowerCase().replace(/ /g, "_").split("_")[0]
|
||||
: "";
|
||||
|
||||
sourceName =
|
||||
|
||||
Reference in New Issue
Block a user