mirror of
https://github.com/Significant-Gravitas/AutoGPT.git
synced 2026-04-08 03:00:28 -04:00
fix(backend): address PR review items for include_graph feature
- Surface truncation notice to copilot via response message when >_MAX_GRAPH_FETCHES agents are skipped, instead of only logging - Add guidance in agent_generation_guide to use include_graph only after narrowing to a specific agent by UUID - Add tests for truncation, mixed graph_id presence, partial success/failure across multiple agents, and keyword-search enrichment path
This commit is contained in:
@@ -20,10 +20,12 @@ or trigger. If so:
|
||||
|
||||
### Workflow for Creating/Editing Agents
|
||||
|
||||
1. **If editing**: Call
|
||||
`find_library_agent(query="<agent_id>", include_graph=true)` to fetch the
|
||||
full graph structure (nodes + links). **Never edit blindly** — always
|
||||
inspect the current graph first so you know exactly what to change.
|
||||
1. **If editing**: First narrow to the specific agent by UUID, then fetch its
|
||||
graph: `find_library_agent(query="<agent_id>", include_graph=true)`. This
|
||||
returns the full graph structure (nodes + links). **Never edit blindly** —
|
||||
always inspect the current graph first so you know exactly what to change.
|
||||
Avoid using `include_graph=true` with broad keyword searches, as fetching
|
||||
multiple graphs at once is expensive and consumes LLM context budget.
|
||||
2. **Discover blocks**: Call `find_block(query, include_schemas=true)` to
|
||||
search for relevant blocks. This returns block IDs, names, descriptions,
|
||||
and full input/output schemas.
|
||||
|
||||
@@ -154,8 +154,9 @@ async def _search_library(
|
||||
session_id=session_id,
|
||||
)
|
||||
|
||||
truncation_notice: str | None = None
|
||||
if include_graph and agents:
|
||||
await _enrich_agents_with_graph(agents, user_id)
|
||||
truncation_notice = await _enrich_agents_with_graph(agents, user_id)
|
||||
|
||||
if not agents:
|
||||
if not query:
|
||||
@@ -190,13 +191,17 @@ async def _search_library(
|
||||
else:
|
||||
title = f"Found {len(agents)} agent{'s' if len(agents) != 1 else ''} in your library for '{query}'"
|
||||
|
||||
message = (
|
||||
"Found agents in the user's library. You can provide a link to view "
|
||||
"an agent at: /library/agents/{agent_id}. Use agent_output to get "
|
||||
"execution results, or run_agent to execute. Let the user know we can "
|
||||
"create a custom agent for them based on their needs."
|
||||
)
|
||||
if truncation_notice:
|
||||
message = f"{message}\n\nNote: {truncation_notice}"
|
||||
|
||||
return AgentsFoundResponse(
|
||||
message=(
|
||||
"Found agents in the user's library. You can provide a link to view "
|
||||
"an agent at: /library/agents/{agent_id}. Use agent_output to get "
|
||||
"execution results, or run_agent to execute. Let the user know we can "
|
||||
"create a custom agent for them based on their needs."
|
||||
),
|
||||
message=message,
|
||||
title=title,
|
||||
agents=agents,
|
||||
count=len(agents),
|
||||
@@ -210,21 +215,26 @@ _MAX_GRAPH_FETCHES = 10
|
||||
_GRAPH_FETCH_TIMEOUT = 15 # seconds
|
||||
|
||||
|
||||
async def _enrich_agents_with_graph(agents: list[AgentInfo], user_id: str) -> None:
|
||||
async def _enrich_agents_with_graph(
|
||||
agents: list[AgentInfo], user_id: str
|
||||
) -> str | None:
|
||||
"""Fetch and attach full Graph (nodes + links) to each agent in-place.
|
||||
|
||||
Only the first ``_MAX_GRAPH_FETCHES`` agents with a ``graph_id`` are
|
||||
enriched. If some agents are skipped, a warning is logged so callers
|
||||
are aware of the truncation.
|
||||
enriched. If some agents are skipped, a truncation notice is returned
|
||||
so the caller can surface it to the copilot.
|
||||
|
||||
Graphs are fetched with ``for_export=True`` so that credentials, API keys,
|
||||
and other secrets in ``input_default`` are stripped before the data reaches
|
||||
the LLM context.
|
||||
|
||||
Returns a truncation notice string when some agents were skipped, or
|
||||
``None`` when all eligible agents were enriched.
|
||||
"""
|
||||
with_graph_id = [a for a in agents if a.graph_id]
|
||||
fetchable = with_graph_id[:_MAX_GRAPH_FETCHES]
|
||||
if not fetchable:
|
||||
return
|
||||
return None
|
||||
|
||||
gdb = graph_db()
|
||||
|
||||
@@ -252,8 +262,6 @@ async def _enrich_agents_with_graph(agents: list[AgentInfo], user_id: str) -> No
|
||||
logger.warning(
|
||||
"include_graph: timed out after %ds fetching graphs", _GRAPH_FETCH_TIMEOUT
|
||||
)
|
||||
# Clear partially-enriched graphs so callers see consistent state
|
||||
# (all-or-nothing) rather than a mix of enriched and non-enriched agents.
|
||||
for a in fetchable:
|
||||
a.graph = None
|
||||
|
||||
@@ -267,6 +275,13 @@ async def _enrich_agents_with_graph(agents: list[AgentInfo], user_id: str) -> No
|
||||
_MAX_GRAPH_FETCHES,
|
||||
skipped,
|
||||
)
|
||||
return (
|
||||
f"Graph data included for {len(fetchable)} of "
|
||||
f"{len(with_graph_id)} eligible agents (limit: {_MAX_GRAPH_FETCHES}). "
|
||||
f"To fetch graphs for remaining agents, narrow your search to a "
|
||||
f"specific agent by UUID."
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
def _marketplace_agent_to_info(agent: StoreAgent | StoreAgentDetails) -> AgentInfo:
|
||||
|
||||
@@ -314,3 +314,210 @@ class TestLibraryUUIDLookup:
|
||||
|
||||
assert isinstance(response, AgentsFoundResponse)
|
||||
assert response.agents[0].graph is None
|
||||
|
||||
|
||||
class TestEnrichAgentsWithGraph:
|
||||
"""Tests for _enrich_agents_with_graph edge cases."""
|
||||
|
||||
@staticmethod
|
||||
def _make_mock_library_agent(
|
||||
agent_id: str = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
|
||||
graph_id: str | None = "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
|
||||
) -> MagicMock:
|
||||
mock_agent = MagicMock()
|
||||
mock_agent.id = f"lib-{agent_id[:8]}"
|
||||
mock_agent.name = f"Agent {agent_id[:8]}"
|
||||
mock_agent.description = "A library agent"
|
||||
mock_agent.creator_name = "testuser"
|
||||
mock_agent.status.value = "HEALTHY"
|
||||
mock_agent.can_access_graph = True
|
||||
mock_agent.has_external_trigger = False
|
||||
mock_agent.new_output = False
|
||||
mock_agent.graph_id = graph_id
|
||||
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_truncation_surfaces_in_response(self):
|
||||
"""When >_MAX_GRAPH_FETCHES agents have graphs, the response contains a truncation notice."""
|
||||
from backend.copilot.tools.agent_search import _MAX_GRAPH_FETCHES
|
||||
from backend.data.graph import BaseGraph
|
||||
|
||||
agent_count = _MAX_GRAPH_FETCHES + 5
|
||||
mock_agents = []
|
||||
for i in range(agent_count):
|
||||
uid = f"a1b2c3d4-e5f6-4a7b-8c9d-{i:012d}"
|
||||
mock_agents.append(self._make_mock_library_agent(uid, uid))
|
||||
|
||||
mock_lib_db = MagicMock()
|
||||
mock_search_results = MagicMock()
|
||||
mock_search_results.agents = mock_agents
|
||||
mock_lib_db.list_library_agents = AsyncMock(return_value=mock_search_results)
|
||||
|
||||
fake_graph = BaseGraph(id="x", name="g", description="d")
|
||||
mock_gdb = MagicMock()
|
||||
mock_gdb.get_graph = AsyncMock(return_value=fake_graph)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"backend.copilot.tools.agent_search.library_db",
|
||||
return_value=mock_lib_db,
|
||||
),
|
||||
patch(
|
||||
"backend.copilot.tools.agent_search.graph_db",
|
||||
return_value=mock_gdb,
|
||||
),
|
||||
):
|
||||
response = await search_agents(
|
||||
query="",
|
||||
source="library",
|
||||
session_id="s",
|
||||
user_id=_TEST_USER_ID,
|
||||
include_graph=True,
|
||||
)
|
||||
|
||||
assert isinstance(response, AgentsFoundResponse)
|
||||
assert mock_gdb.get_graph.await_count == _MAX_GRAPH_FETCHES
|
||||
enriched = [a for a in response.agents if a.graph is not None]
|
||||
assert len(enriched) == _MAX_GRAPH_FETCHES
|
||||
assert "Graph data included for" in response.message
|
||||
assert str(_MAX_GRAPH_FETCHES) in response.message
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_mixed_graph_id_presence(self):
|
||||
"""Agents without graph_id are skipped during enrichment."""
|
||||
from backend.data.graph import BaseGraph
|
||||
|
||||
agent_with = self._make_mock_library_agent(
|
||||
"aaaa0000-0000-0000-0000-000000000001",
|
||||
"aaaa0000-0000-0000-0000-000000000001",
|
||||
)
|
||||
agent_without = self._make_mock_library_agent(
|
||||
"bbbb0000-0000-0000-0000-000000000002",
|
||||
graph_id=None,
|
||||
)
|
||||
|
||||
mock_lib_db = MagicMock()
|
||||
mock_search_results = MagicMock()
|
||||
mock_search_results.agents = [agent_with, agent_without]
|
||||
mock_lib_db.list_library_agents = AsyncMock(return_value=mock_search_results)
|
||||
|
||||
fake_graph = BaseGraph(
|
||||
id="aaaa0000-0000-0000-0000-000000000001", name="g", description="d"
|
||||
)
|
||||
mock_gdb = MagicMock()
|
||||
mock_gdb.get_graph = AsyncMock(return_value=fake_graph)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"backend.copilot.tools.agent_search.library_db",
|
||||
return_value=mock_lib_db,
|
||||
),
|
||||
patch(
|
||||
"backend.copilot.tools.agent_search.graph_db",
|
||||
return_value=mock_gdb,
|
||||
),
|
||||
):
|
||||
response = await search_agents(
|
||||
query="",
|
||||
source="library",
|
||||
session_id="s",
|
||||
user_id=_TEST_USER_ID,
|
||||
include_graph=True,
|
||||
)
|
||||
|
||||
assert isinstance(response, AgentsFoundResponse)
|
||||
assert len(response.agents) == 2
|
||||
assert response.agents[0].graph is not None
|
||||
assert response.agents[1].graph is None
|
||||
mock_gdb.get_graph.assert_awaited_once()
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_partial_failure_across_multiple_agents(self):
|
||||
"""When some graph fetches fail, successful ones still have graphs attached."""
|
||||
from backend.data.graph import BaseGraph
|
||||
|
||||
id_ok = "aaaa0000-0000-0000-0000-000000000001"
|
||||
id_fail = "bbbb0000-0000-0000-0000-000000000002"
|
||||
agent_ok = self._make_mock_library_agent(id_ok, id_ok)
|
||||
agent_fail = self._make_mock_library_agent(id_fail, id_fail)
|
||||
|
||||
mock_lib_db = MagicMock()
|
||||
mock_search_results = MagicMock()
|
||||
mock_search_results.agents = [agent_ok, agent_fail]
|
||||
mock_lib_db.list_library_agents = AsyncMock(return_value=mock_search_results)
|
||||
|
||||
fake_graph = BaseGraph(id=id_ok, name="g", description="d")
|
||||
|
||||
async def _side_effect(graph_id, **kwargs):
|
||||
if graph_id == id_fail:
|
||||
raise Exception("DB error")
|
||||
return fake_graph
|
||||
|
||||
mock_gdb = MagicMock()
|
||||
mock_gdb.get_graph = AsyncMock(side_effect=_side_effect)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"backend.copilot.tools.agent_search.library_db",
|
||||
return_value=mock_lib_db,
|
||||
),
|
||||
patch(
|
||||
"backend.copilot.tools.agent_search.graph_db",
|
||||
return_value=mock_gdb,
|
||||
),
|
||||
):
|
||||
response = await search_agents(
|
||||
query="",
|
||||
source="library",
|
||||
session_id="s",
|
||||
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.id == id_ok
|
||||
assert response.agents[1].graph is None
|
||||
|
||||
@pytest.mark.asyncio(loop_scope="session")
|
||||
async def test_keyword_search_with_include_graph(self):
|
||||
"""include_graph works via keyword search (non-UUID path)."""
|
||||
from backend.data.graph import BaseGraph
|
||||
|
||||
agent_id = "cccc0000-0000-0000-0000-000000000003"
|
||||
mock_agent = self._make_mock_library_agent(agent_id, agent_id)
|
||||
|
||||
mock_lib_db = MagicMock()
|
||||
mock_search_results = MagicMock()
|
||||
mock_search_results.agents = [mock_agent]
|
||||
mock_lib_db.list_library_agents = AsyncMock(return_value=mock_search_results)
|
||||
|
||||
fake_graph = BaseGraph(id=agent_id, name="g", description="d")
|
||||
mock_gdb = MagicMock()
|
||||
mock_gdb.get_graph = AsyncMock(return_value=fake_graph)
|
||||
|
||||
with (
|
||||
patch(
|
||||
"backend.copilot.tools.agent_search.library_db",
|
||||
return_value=mock_lib_db,
|
||||
),
|
||||
patch(
|
||||
"backend.copilot.tools.agent_search.graph_db",
|
||||
return_value=mock_gdb,
|
||||
),
|
||||
):
|
||||
response = await search_agents(
|
||||
query="email",
|
||||
source="library",
|
||||
session_id="s",
|
||||
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.id == agent_id
|
||||
mock_gdb.get_graph.assert_awaited_once()
|
||||
|
||||
Reference in New Issue
Block a user