fix(backend/copilot): address review — parallel fetch, None logging, failure tests

- Use asyncio.gather for parallel graph fetching instead of sequential loop
- Cap graph fetches at 10 to prevent excessive DB calls on broad searches
- Log warning when get_agent_as_json returns None (graph not found)
- Add tests for exception and None return failure paths
This commit is contained in:
Zamil Majdy
2026-03-31 15:55:49 +02:00
parent eb96b019c5
commit ee6cc20cbc
2 changed files with 80 additions and 6 deletions

View File

@@ -2,6 +2,7 @@
from __future__ import annotations
import asyncio
import logging
from typing import TYPE_CHECKING, Literal
@@ -204,16 +205,25 @@ async def _search_library(
)
_MAX_GRAPH_FETCHES = 10
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
fetchable = [a for a in agents if a.graph_id][:_MAX_GRAPH_FETCHES]
if not fetchable:
return
async def _fetch(agent: AgentInfo) -> None:
try:
agent.graph = await get_agent_as_json(graph_id, user_id)
graph_json = await get_agent_as_json(agent.graph_id, user_id) # type: ignore[arg-type]
if graph_json is None:
logger.warning(f"Graph not found for agent {agent.graph_id}")
agent.graph = graph_json
except Exception as e:
logger.warning(f"Failed to fetch graph for agent {graph_id}: {e}")
logger.warning(f"Failed to fetch graph for agent {agent.graph_id}: {e}")
await asyncio.gather(*[_fetch(a) for a in fetchable])
def _marketplace_agent_to_info(agent: StoreAgent | StoreAgentDetails) -> AgentInfo:

View File

@@ -247,3 +247,67 @@ class TestLibraryUUIDLookup:
assert isinstance(response, AgentsFoundResponse)
assert response.agents[0].graph is None
mock_get_json.assert_not_awaited()
@pytest.mark.asyncio(loop_scope="session")
async def test_include_graph_handles_fetch_failure(self):
"""include_graph=True still returns agents when graph fetch fails."""
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,
side_effect=Exception("DB connection failed"),
),
):
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.count == 1
assert response.agents[0].graph is None
@pytest.mark.asyncio(loop_scope="session")
async def test_include_graph_handles_none_return(self):
"""include_graph=True handles get_agent_as_json returning None."""
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,
return_value=None,
),
):
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.count == 1
assert response.agents[0].graph is None