Compare commits

...

7 Commits

Author SHA1 Message Date
Otto
5cecaf0f58 refactor: rename to get_store_listed_graphs
Clearer name indicating these are store-listed public graphs
that don't require permission checks.
2026-01-31 14:52:21 +00:00
Otto
1b34c871d7 refactor: move batch graph query to data layer
- Added get_graphs_by_ids to backend/data/graph.py
- Uses StoreListingVersion lookup (same pattern as get_graph for public graphs)
- Removed inline Prisma query from feature code
2026-01-31 14:13:12 +00:00
Otto
9558d3b67a refactor: use Prisma find_many for true batch query
Replaced asyncio.gather with a single Prisma find_many query using
id IN operator. This is more efficient than N concurrent calls.
2026-01-31 14:08:12 +00:00
Otto
6c50c3de42 fix: make agent_graph_id required field
Updated StoreAgent model to have agent_graph_id as required str.
Updated test files to include the new required field.
2026-01-31 14:06:11 +00:00
Otto
192c4333da fix: make agent_graph_id non-nullable with empty default
The StoreAgent view defines agentGraphId as String (non-nullable).
Using empty string default for backward compatibility - empty strings
are falsy and filtered out in the agent generation code.
2026-01-31 14:03:34 +00:00
Otto
7a96ac2eee refactor: address PR feedback
- Move asyncio import to module level
- Create get_graphs_by_ids batch function with varargs
- Remove duck typing - properly handle nullable agent_graph_id
- Remove obvious comments
2026-01-31 14:00:50 +00:00
Otto
8d0527f9c4 fix: include graph schemas for marketplace agents in Agent Generator
When marketplace agents are included in the library_agents payload to
the Agent Generator service, they were missing required fields
(graph_id, graph_version, input_schema, output_schema), causing
Pydantic validation to fail with HTTP 422.

Changes:
- Add agent_graph_id field to StoreAgent model
- Include agentGraphId in hybrid search SQL query
- Update search_marketplace_agents_for_generation to fetch full graph
  schemas for marketplace agents
- Marketplace agents now return LibraryAgentSummary (unified type)
  with complete schemas for sub-agent composition
- Update deduplication logic to use graph_id instead of name

This fixes agent creation failures on dev where the decompose-description
endpoint was returning 422 Unprocessable Entity.

Fixes: SECRT-1817
2026-01-31 13:51:14 +00:00
7 changed files with 82 additions and 25 deletions

View File

@@ -14,6 +14,7 @@ from backend.data.graph import (
create_graph,
get_graph,
get_graph_all_versions,
get_store_listed_graphs,
)
from backend.util.exceptions import DatabaseError, NotFoundError
@@ -266,18 +267,18 @@ async def get_library_agents_for_generation(
async def search_marketplace_agents_for_generation(
search_query: str,
max_results: int = 10,
) -> list[MarketplaceAgentSummary]:
) -> list[LibraryAgentSummary]:
"""Search marketplace agents formatted for Agent Generator.
Note: This returns basic agent info. Full input/output schemas would require
additional graph fetches and is a potential future enhancement.
Fetches marketplace agents and their full schemas so they can be used
as sub-agents in generated workflows.
Args:
search_query: Search term to find relevant public agents
max_results: Maximum number of agents to return (default 10)
Returns:
List of MarketplaceAgentSummary (without detailed schemas for now)
List of LibraryAgentSummary with full input/output schemas
"""
try:
response = await store_db.get_store_agents(
@@ -286,17 +287,31 @@ async def search_marketplace_agents_for_generation(
page_size=max_results,
)
results: list[MarketplaceAgentSummary] = []
for agent in response.agents:
results.append(
MarketplaceAgentSummary(
name=agent.agent_name,
description=agent.description,
sub_heading=agent.sub_heading,
creator=agent.creator,
is_marketplace_agent=True,
agents_with_graphs = [
agent for agent in response.agents if agent.agent_graph_id
]
if not agents_with_graphs:
return []
graph_ids = [agent.agent_graph_id for agent in agents_with_graphs]
graphs = await get_store_listed_graphs(*graph_ids)
results: list[LibraryAgentSummary] = []
for agent in agents_with_graphs:
graph_id = agent.agent_graph_id
if graph_id and graph_id in graphs:
graph = graphs[graph_id]
results.append(
LibraryAgentSummary(
graph_id=graph.id,
graph_version=graph.version,
name=agent.agent_name,
description=agent.description,
input_schema=graph.input_schema,
output_schema=graph.output_schema,
)
)
)
return results
except Exception as e:
logger.warning(f"Failed to search marketplace agents: {e}")
@@ -327,8 +342,7 @@ async def get_all_relevant_agents_for_generation(
max_marketplace_results: Max marketplace agents to return (default 10)
Returns:
List of AgentSummary, library agents first (with full schemas),
then marketplace agents (basic info only)
List of AgentSummary with full schemas (both library and marketplace agents)
"""
agents: list[AgentSummary] = []
seen_graph_ids: set[str] = set()
@@ -365,16 +379,12 @@ async def get_all_relevant_agents_for_generation(
search_query=search_query,
max_results=max_marketplace_results,
)
library_names: set[str] = set()
for a in agents:
name = a.get("name")
if name and isinstance(name, str):
library_names.add(name.lower())
# Deduplicate by graph_id (marketplace agents now have full schemas)
for agent in marketplace_agents:
agent_name = agent.get("name")
if agent_name and isinstance(agent_name, str):
if agent_name.lower() not in library_names:
agents.append(agent)
graph_id = agent.get("graph_id")
if graph_id and graph_id not in seen_graph_ids:
agents.append(agent)
seen_graph_ids.add(graph_id)
return agents

View File

@@ -112,6 +112,7 @@ async def get_store_agents(
description=agent["description"],
runs=agent["runs"],
rating=agent["rating"],
agent_graph_id=agent.get("agentGraphId"),
)
store_agents.append(store_agent)
except Exception as e:
@@ -170,6 +171,7 @@ async def get_store_agents(
description=agent.description,
runs=agent.runs,
rating=agent.rating,
agent_graph_id=agent.agentGraphId,
)
# Add to the list only if creation was successful
store_agents.append(store_agent)

View File

@@ -600,6 +600,7 @@ async def hybrid_search(
sa.featured,
sa.is_available,
sa.updated_at,
sa."agentGraphId",
-- Searchable text for BM25 reranking
COALESCE(sa.agent_name, '') || ' ' || COALESCE(sa.sub_heading, '') || ' ' || COALESCE(sa.description, '') as searchable_text,
-- Semantic score
@@ -659,6 +660,7 @@ async def hybrid_search(
featured,
is_available,
updated_at,
"agentGraphId",
searchable_text,
semantic_score,
lexical_score,

View File

@@ -38,6 +38,7 @@ class StoreAgent(pydantic.BaseModel):
description: str
runs: int
rating: float
agent_graph_id: str
class StoreAgentsResponse(pydantic.BaseModel):

View File

@@ -26,11 +26,13 @@ def test_store_agent():
description="Test description",
runs=50,
rating=4.5,
agent_graph_id="test-graph-id",
)
assert agent.slug == "test-agent"
assert agent.agent_name == "Test Agent"
assert agent.runs == 50
assert agent.rating == 4.5
assert agent.agent_graph_id == "test-graph-id"
def test_store_agents_response():
@@ -46,6 +48,7 @@ def test_store_agents_response():
description="Test description",
runs=50,
rating=4.5,
agent_graph_id="test-graph-id",
)
],
pagination=store_model.Pagination(

View File

@@ -82,6 +82,7 @@ def test_get_agents_featured(
description="Featured agent description",
runs=100,
rating=4.5,
agent_graph_id="test-graph-1",
)
],
pagination=store_model.Pagination(
@@ -127,6 +128,7 @@ def test_get_agents_by_creator(
description="Creator agent description",
runs=50,
rating=4.0,
agent_graph_id="test-graph-2",
)
],
pagination=store_model.Pagination(
@@ -172,6 +174,7 @@ def test_get_agents_sorted(
description="Top agent description",
runs=1000,
rating=5.0,
agent_graph_id="test-graph-3",
)
],
pagination=store_model.Pagination(
@@ -306,6 +309,7 @@ def test_get_agents_pagination(
description=f"Agent {i} description",
runs=i * 10,
rating=4.0,
agent_graph_id="test-graph-2",
)
for i in range(5)
],
@@ -373,6 +377,7 @@ def test_get_agent_details(
categories=["category1", "category2"],
runs=100,
rating=4.5,
agent_graph_id="test-graph-1",
versions=["1.0.0", "1.1.0"],
agentGraphVersions=["1", "2"],
agentGraphId="test-graph-id",
@@ -436,6 +441,7 @@ def test_get_creators_pagination(
avatar_url=f"avatar{i}.jpg",
num_agents=1,
agent_rating=4.5,
agent_graph_id="test-graph-1",
agent_runs=100,
is_featured=False,
)

View File

@@ -1028,6 +1028,39 @@ async def get_graph(
return GraphModel.from_db(graph, for_export)
async def get_store_listed_graphs(*graph_ids: str) -> dict[str, GraphModel]:
"""Batch-fetch multiple store-listed graphs by their IDs.
Only returns graphs that have approved store listings (publicly available).
Does not require permission checks since store-listed graphs are public.
Args:
*graph_ids: Variable number of graph IDs to fetch
Returns:
Dict mapping graph_id to GraphModel for graphs with approved store listings
"""
if not graph_ids:
return {}
store_listings = await StoreListingVersion.prisma().find_many(
where={
"agentGraphId": {"in": list(graph_ids)},
"submissionStatus": SubmissionStatus.APPROVED,
"isDeleted": False,
},
include={"AgentGraph": {"include": AGENT_GRAPH_INCLUDE}},
distinct=["agentGraphId"],
order={"agentGraphVersion": "desc"},
)
return {
listing.agentGraphId: GraphModel.from_db(listing.AgentGraph)
for listing in store_listings
if listing.AgentGraph
}
async def get_graph_as_admin(
graph_id: str,
version: int | None = None,