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
This commit is contained in:
Otto
2026-01-31 13:51:14 +00:00
parent 7ee94d986c
commit 8d0527f9c4
4 changed files with 58 additions and 26 deletions

View File

@@ -7,6 +7,7 @@ from typing import Any, NotRequired, TypedDict
from backend.api.features.library import db as library_db
from backend.api.features.store import db as store_db
from backend.api.features.store import model as store_model
from backend.data.graph import (
Graph,
Link,
@@ -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,18 +287,49 @@ 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,
# Filter to agents that have a graph ID
agents_with_graphs = [
agent for agent in response.agents if agent.agent_graph_id
]
if not agents_with_graphs:
return []
# Batch-fetch graphs to get input/output schemas
# Use get_graph with user_id=None for public marketplace graphs
import asyncio
async def fetch_graph_schema(
agent: store_model.StoreAgent,
) -> LibraryAgentSummary | None:
try:
graph = await get_graph(
graph_id=agent.agent_graph_id, # type: ignore
version=None, # Get active version
user_id=None, # Public graph
)
)
return results
if graph:
return 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,
)
except Exception as e:
logger.debug(
f"Failed to fetch schema for marketplace agent {agent.agent_name}: {e}"
)
return None
# Fetch all schemas concurrently
results = await asyncio.gather(
*[fetch_graph_schema(agent) for agent in agents_with_graphs]
)
# Filter out None results
return [r for r in results if r is not None]
except Exception as e:
logger.warning(f"Failed to search marketplace agents: {e}")
return []
@@ -327,8 +359,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 +396,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 | None = None # Graph ID for sub-agent composition
class StoreAgentsResponse(pydantic.BaseModel):