mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
feat(backend/copilot): add include_graph option to find_library_agent for agent debugging/editing
The copilot's edit_agent tool required the LLM to provide a complete agent JSON (nodes + links) without ever seeing the current graph structure — it was editing blindly. This adds an `include_graph` boolean parameter to the existing `find_library_agent` tool so the copilot can fetch the full graph before making modifications. Also updates the agent generation guide to split creating vs editing workflows, instructing the LLM to always fetch the current graph first.
This commit is contained in:
@@ -18,7 +18,7 @@ or trigger. If so:
|
||||
**Skip this** when the goal already specifies all dimensions (e.g.
|
||||
"scrape prices from Amazon and email me daily").
|
||||
|
||||
### Workflow for Creating/Editing Agents
|
||||
### Workflow for Creating Agents
|
||||
|
||||
1. **Discover blocks**: Call `find_block(query, include_schemas=true)` to
|
||||
search for relevant blocks. This returns block IDs, names, descriptions,
|
||||
@@ -36,8 +36,25 @@ or trigger. If so:
|
||||
for errors
|
||||
6. **Fix if needed**: Call `fix_agent_graph` to auto-fix common issues,
|
||||
or fix manually based on the error descriptions. Iterate until valid.
|
||||
7. **Save**: Call `create_agent` (new) or `edit_agent` (existing) with
|
||||
the final `agent_json`
|
||||
7. **Save**: Call `create_agent` with the final `agent_json`
|
||||
|
||||
### Workflow for Editing Agents
|
||||
|
||||
1. **Fetch the current graph**: Call
|
||||
`find_library_agent(query="<agent_id>", include_graph=true)` to get the
|
||||
full graph structure (nodes + links). **Never edit blindly** — always
|
||||
inspect the current graph first so you know exactly what to change.
|
||||
2. **Understand the graph**: Review the returned nodes, links, and
|
||||
`input_default` values. Identify which nodes and links need changes.
|
||||
3. **Discover blocks** (if needed): Call `find_block` to look up new blocks
|
||||
you need to add.
|
||||
4. **Modify the JSON**: Apply targeted changes to the agent JSON — add,
|
||||
remove, or update specific nodes and links. Preserve unchanged parts.
|
||||
5. **Validate**: Call `validate_agent_graph` with the modified JSON.
|
||||
6. **Fix if needed**: Call `fix_agent_graph` or fix manually. Iterate until
|
||||
valid.
|
||||
7. **Save**: Call `edit_agent` with the agent ID and the modified
|
||||
`agent_json`
|
||||
|
||||
### Agent JSON Structure
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ if TYPE_CHECKING:
|
||||
from backend.data.db_accessors import library_db, store_db
|
||||
from backend.util.exceptions import DatabaseError, NotFoundError
|
||||
|
||||
from .agent_generator import get_agent_as_json
|
||||
from .models import (
|
||||
AgentInfo,
|
||||
AgentsFoundResponse,
|
||||
@@ -34,12 +35,13 @@ async def search_agents(
|
||||
source: SearchSource,
|
||||
session_id: str | None = None,
|
||||
user_id: str | None = None,
|
||||
include_graph: bool = False,
|
||||
) -> ToolResponseBase:
|
||||
"""Search for agents in marketplace or user library."""
|
||||
if source == "marketplace":
|
||||
return await _search_marketplace(query, session_id)
|
||||
else:
|
||||
return await _search_library(query, session_id, user_id)
|
||||
return await _search_library(query, session_id, user_id, include_graph)
|
||||
|
||||
|
||||
async def _search_marketplace(query: str, session_id: str | None) -> ToolResponseBase:
|
||||
@@ -105,7 +107,10 @@ async def _search_marketplace(query: str, session_id: str | None) -> ToolRespons
|
||||
|
||||
|
||||
async def _search_library(
|
||||
query: str, session_id: str | None, user_id: str | None
|
||||
query: str,
|
||||
session_id: str | None,
|
||||
user_id: str | None,
|
||||
include_graph: bool = False,
|
||||
) -> ToolResponseBase:
|
||||
"""Search user's library agents, with direct UUID lookup fallback."""
|
||||
if not user_id:
|
||||
@@ -149,6 +154,9 @@ async def _search_library(
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
if include_graph and agents:
|
||||
await _enrich_agents_with_graph(agents, user_id)
|
||||
|
||||
if not agents:
|
||||
if not query:
|
||||
return NoResultsResponse(
|
||||
@@ -196,6 +204,18 @@ async def _search_library(
|
||||
)
|
||||
|
||||
|
||||
async def _enrich_agents_with_graph(agents: list[AgentInfo], user_id: str) -> None:
|
||||
"""Fetch and attach full graph JSON (nodes + links) to each agent in-place."""
|
||||
for agent in agents:
|
||||
graph_id = agent.graph_id
|
||||
if not graph_id:
|
||||
continue
|
||||
try:
|
||||
agent.graph = await get_agent_as_json(graph_id, user_id)
|
||||
except Exception as e:
|
||||
logger.warning(f"Failed to fetch graph for agent {graph_id}: {e}")
|
||||
|
||||
|
||||
def _marketplace_agent_to_info(agent: StoreAgent | StoreAgentDetails) -> AgentInfo:
|
||||
"""Convert a marketplace agent (StoreAgent or StoreAgentDetails) to an AgentInfo."""
|
||||
return AgentInfo(
|
||||
|
||||
@@ -133,10 +133,10 @@ class TestMarketplaceSlugLookup:
|
||||
class TestLibraryUUIDLookup:
|
||||
"""Tests for UUID direct lookup in library search."""
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_uuid_lookup_found_by_graph_id(self):
|
||||
"""UUID query matching a graph_id returns the agent directly."""
|
||||
agent_id = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
|
||||
@staticmethod
|
||||
def _make_mock_library_agent(
|
||||
agent_id: str = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
|
||||
) -> MagicMock:
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.id = "lib-agent-id"
|
||||
mock_agent.name = "My Library Agent"
|
||||
@@ -150,6 +150,13 @@ class TestLibraryUUIDLookup:
|
||||
mock_agent.graph_version = 1
|
||||
mock_agent.input_schema = {}
|
||||
mock_agent.output_schema = {}
|
||||
return mock_agent
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_uuid_lookup_found_by_graph_id(self):
|
||||
"""UUID query matching a graph_id returns the agent directly."""
|
||||
agent_id = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
|
||||
mock_agent = self._make_mock_library_agent(agent_id)
|
||||
|
||||
mock_lib_db = MagicMock()
|
||||
mock_lib_db.get_library_agent_by_graph_id = AsyncMock(return_value=mock_agent)
|
||||
@@ -168,3 +175,75 @@ class TestLibraryUUIDLookup:
|
||||
assert isinstance(response, AgentsFoundResponse)
|
||||
assert response.count == 1
|
||||
assert response.agents[0].name == "My Library Agent"
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_include_graph_fetches_nodes_and_links(self):
|
||||
"""include_graph=True attaches full graph JSON to agent results."""
|
||||
agent_id = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
|
||||
mock_agent = self._make_mock_library_agent(agent_id)
|
||||
|
||||
mock_lib_db = MagicMock()
|
||||
mock_lib_db.get_library_agent_by_graph_id = AsyncMock(return_value=mock_agent)
|
||||
|
||||
fake_graph = {
|
||||
"id": agent_id,
|
||||
"name": "My Library Agent",
|
||||
"nodes": [{"id": "node-1", "block_id": "block-1"}],
|
||||
"links": [{"id": "link-1", "source_id": "node-1", "sink_id": "node-2"}],
|
||||
}
|
||||
|
||||
with (
|
||||
patch(
|
||||
"backend.copilot.tools.agent_search.library_db",
|
||||
return_value=mock_lib_db,
|
||||
),
|
||||
patch(
|
||||
"backend.copilot.tools.agent_search.get_agent_as_json",
|
||||
new_callable=AsyncMock,
|
||||
return_value=fake_graph,
|
||||
) as mock_get_json,
|
||||
):
|
||||
response = await search_agents(
|
||||
query=agent_id,
|
||||
source="library",
|
||||
session_id="test-session",
|
||||
user_id=_TEST_USER_ID,
|
||||
include_graph=True,
|
||||
)
|
||||
|
||||
assert isinstance(response, AgentsFoundResponse)
|
||||
assert response.agents[0].graph is not None
|
||||
assert response.agents[0].graph["nodes"] == fake_graph["nodes"]
|
||||
assert response.agents[0].graph["links"] == fake_graph["links"]
|
||||
mock_get_json.assert_awaited_once_with(agent_id, _TEST_USER_ID)
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_include_graph_false_does_not_fetch(self):
|
||||
"""include_graph=False (default) does not fetch graph data."""
|
||||
agent_id = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
|
||||
mock_agent = self._make_mock_library_agent(agent_id)
|
||||
|
||||
mock_lib_db = MagicMock()
|
||||
mock_lib_db.get_library_agent_by_graph_id = AsyncMock(return_value=mock_agent)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"backend.copilot.tools.agent_search.library_db",
|
||||
return_value=mock_lib_db,
|
||||
),
|
||||
patch(
|
||||
"backend.copilot.tools.agent_search.get_agent_as_json",
|
||||
new_callable=AsyncMock,
|
||||
) as mock_get_json,
|
||||
):
|
||||
response = await search_agents(
|
||||
query=agent_id,
|
||||
source="library",
|
||||
session_id="test-session",
|
||||
user_id=_TEST_USER_ID,
|
||||
include_graph=False,
|
||||
)
|
||||
|
||||
assert isinstance(response, AgentsFoundResponse)
|
||||
assert response.agents[0].graph is None
|
||||
mock_get_json.assert_not_awaited()
|
||||
|
||||
@@ -20,7 +20,8 @@ class FindLibraryAgentTool(BaseTool):
|
||||
def description(self) -> str:
|
||||
return (
|
||||
"Search user's library agents. Returns graph_id, schemas for sub-agent composition. "
|
||||
"Omit query to list all."
|
||||
"Omit query to list all. Set include_graph=true to also fetch the full "
|
||||
"graph structure (nodes + links) for debugging or editing."
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -32,6 +33,15 @@ class FindLibraryAgentTool(BaseTool):
|
||||
"type": "string",
|
||||
"description": "Search by name/description. Omit to list all.",
|
||||
},
|
||||
"include_graph": {
|
||||
"type": "boolean",
|
||||
"description": (
|
||||
"When true, includes the full graph structure "
|
||||
"(nodes + links) for each found agent. "
|
||||
"Use when you need to inspect, debug, or edit an agent."
|
||||
),
|
||||
"default": False,
|
||||
},
|
||||
},
|
||||
"required": [],
|
||||
}
|
||||
@@ -52,4 +62,5 @@ class FindLibraryAgentTool(BaseTool):
|
||||
source="library",
|
||||
session_id=session.session_id,
|
||||
user_id=user_id,
|
||||
include_graph=kwargs.get("include_graph", False),
|
||||
)
|
||||
|
||||
@@ -122,6 +122,10 @@ class AgentInfo(BaseModel):
|
||||
default=None,
|
||||
description="Input schema for the agent, including field names, types, and defaults",
|
||||
)
|
||||
graph: dict[str, Any] | None = Field(
|
||||
default=None,
|
||||
description="Full graph structure (nodes + links) when include_graph is requested",
|
||||
)
|
||||
|
||||
|
||||
class AgentsFoundResponse(ToolResponseBase):
|
||||
|
||||
@@ -7460,6 +7460,14 @@
|
||||
],
|
||||
"title": "Inputs",
|
||||
"description": "Input schema for the agent, including field names, types, and defaults"
|
||||
},
|
||||
"graph": {
|
||||
"anyOf": [
|
||||
{ "additionalProperties": true, "type": "object" },
|
||||
{ "type": "null" }
|
||||
],
|
||||
"title": "Graph",
|
||||
"description": "Full graph structure (nodes + links) when include_graph is requested"
|
||||
}
|
||||
},
|
||||
"type": "object",
|
||||
|
||||
Reference in New Issue
Block a user