Skip to content

Commit 2087080

Browse files
authored
Add agent marketplace repository and version pinning for sub-agents (#3239)
* feat: add agent marketplace repository and pin sub-agent versions at publish Introduce ag_agent_repository_t with list/status/publish/import APIs for frozen agent snapshots. Pin selected_agent_version_no on agent relations when publishing so sub-agents resolve to a fixed version at runtime. Extend agent export/import to bundle skills in ZIP payloads and add embedding model fallback when no model name is provided. * feat: add agent marketplace repository and pin sub-agent versions at publish Introduce ag_agent_repository_t with list/status/publish/import APIs for frozen agent snapshots. Pin selected_agent_version_no on agent relations when publishing so sub-agents resolve to a fixed version at runtime. Extend agent export/import to bundle skills in ZIP payloads and add embedding model fallback when no model name is provided. * feat: add agent marketplace repository and pin sub-agent versions at publish Introduce ag_agent_repository_t with list/status/publish/import APIs for frozen agent snapshots. Pin selected_agent_version_no on agent relations when publishing so sub-agents resolve to a fixed version at runtime. Extend agent export/import to bundle skills in ZIP payloads and add embedding model fallback when no model name is provided. * feat: add agent marketplace repository and pin sub-agent versions at publish Introduce ag_agent_repository_t with list/status/publish/import APIs for frozen agent snapshots. Pin selected_agent_version_no on agent relations when publishing so sub-agents resolve to a fixed version at runtime. Extend agent export/import to bundle skills in ZIP payloads and add embedding model fallback when no model name is provided.
1 parent 8961efc commit 2087080

27 files changed

Lines changed: 2518 additions & 233 deletions

backend/agents/create_agent_info.py

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,11 @@
2222
from database.a2a_agent_db import PROTOCOL_JSONRPC
2323
from services.memory_config_service import build_memory_context
2424
from services.image_service import get_video_understanding_model, get_vlm_model
25-
from database.agent_db import search_agent_info_by_agent_id, query_sub_agents_id_list
25+
from database.agent_db import (
26+
search_agent_info_by_agent_id,
27+
query_sub_agent_relations,
28+
resolve_sub_agent_version_no,
29+
)
2630
from database.agent_version_db import query_current_version_no
2731
from database.tool_db import search_tools_for_sub_agent
2832
from database.model_management_db import get_model_records, get_model_by_model_id
@@ -315,13 +319,16 @@ async def create_agent_config(
315319
agent_id=agent_id, tenant_id=tenant_id, version_no=version_no)
316320

317321
# create sub agent
318-
sub_agent_id_list = query_sub_agents_id_list(
322+
sub_agent_relations = query_sub_agent_relations(
319323
main_agent_id=agent_id, tenant_id=tenant_id, version_no=version_no)
320324
managed_agents = []
321-
for sub_agent_id in sub_agent_id_list:
322-
# Get the current published version for this sub-agent (from draft version 0)
323-
sub_agent_version_no = query_current_version_no(
324-
agent_id=sub_agent_id, tenant_id=tenant_id) or 0
325+
for rel in sub_agent_relations:
326+
sub_agent_id = rel['selected_agent_id']
327+
sub_agent_version_no = resolve_sub_agent_version_no(
328+
selected_agent_id=sub_agent_id,
329+
selected_agent_version_no=rel.get('selected_agent_version_no'),
330+
tenant_id=tenant_id,
331+
)
325332
sub_agent_config = await create_agent_config(
326333
agent_id=sub_agent_id,
327334
tenant_id=tenant_id,

backend/apps/agent_app.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -195,8 +195,6 @@ async def export_agent_api(request: AgentIDRequest, authorization: Optional[str]
195195
"Content-Disposition": f"attachment; filename=\"{result.get('filename', 'agent_export.zip')}\""
196196
}
197197
)
198-
if isinstance(result, str):
199-
result = json.loads(result)
200198
return ConversationResponse(code=0, message="success", data=result)
201199
except Exception as e:
202200
logger.error(f"Agent export error: {str(e)}")
@@ -621,3 +619,5 @@ async def list_published_agents_api(
621619
raise HTTPException(
622620
status_code=HTTPStatus.INTERNAL_SERVER_ERROR, detail="Published agents list error."
623621
)
622+
623+
Lines changed: 134 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,134 @@
1+
import logging
2+
from http import HTTPStatus
3+
from typing import Optional
4+
5+
from fastapi import APIRouter, Body, Header, HTTPException, Query
6+
from starlette.responses import JSONResponse
7+
8+
from consts.exceptions import SkillDuplicateError, UnauthorizedError
9+
from services.agent_repository_service import (
10+
create_agent_repository_listing_impl,
11+
import_agent_from_repository_impl,
12+
list_agent_repository_listings_impl,
13+
update_agent_repository_status_impl,
14+
)
15+
from utils.auth_utils import get_current_user_id
16+
17+
agent_repository_router = APIRouter(prefix="/repository/agent")
18+
logger = logging.getLogger("agent_repository_app")
19+
20+
21+
@agent_repository_router.get("")
22+
async def list_agent_repository_listings_api(
23+
status: Optional[str] = Query(None, description="Filter by listing status"),
24+
authorization: str = Header(None),
25+
):
26+
"""List all marketplace repository listings with optional status filter."""
27+
try:
28+
get_current_user_id(authorization)
29+
result = list_agent_repository_listings_impl(status=status)
30+
return JSONResponse(status_code=HTTPStatus.OK, content=result)
31+
except UnauthorizedError as e:
32+
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
33+
except ValueError as e:
34+
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
35+
except Exception as e:
36+
logger.error(f"List agent repository listings error: {str(e)}")
37+
raise HTTPException(
38+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
39+
detail="List agent repository listings error.",
40+
)
41+
42+
43+
@agent_repository_router.patch("/{agent_repository_id}/status")
44+
async def update_agent_repository_status_api(
45+
agent_repository_id: int,
46+
status: str = Body(
47+
...,
48+
embed=True,
49+
description=(
50+
"New status: NOT_SHARED (未共享) / PENDING_REVIEW (待审核) / "
51+
"REJECTED (审核驳回) / SHARED (已共享)"
52+
),
53+
),
54+
authorization: str = Header(None),
55+
):
56+
"""Update marketplace repository listing status (share, unshare, approve, reject)."""
57+
try:
58+
user_id, _ = get_current_user_id(authorization)
59+
result = update_agent_repository_status_impl(
60+
agent_repository_id=agent_repository_id,
61+
status=status,
62+
user_id=user_id,
63+
)
64+
return JSONResponse(status_code=HTTPStatus.OK, content=result)
65+
except UnauthorizedError as e:
66+
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
67+
except ValueError as e:
68+
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
69+
except Exception as e:
70+
logger.error(f"Update agent repository status error: {str(e)}")
71+
raise HTTPException(
72+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
73+
detail="Update agent repository status error.",
74+
)
75+
76+
77+
@agent_repository_router.post("/{agent_id}/versions/{version_no}")
78+
async def create_agent_repository_listing_api(
79+
agent_id: int,
80+
version_no: int,
81+
authorization: str = Header(None),
82+
):
83+
"""Create or update a marketplace repository listing from an agent version snapshot."""
84+
try:
85+
user_id, tenant_id = get_current_user_id(authorization)
86+
result = await create_agent_repository_listing_impl(
87+
agent_id=agent_id,
88+
tenant_id=tenant_id,
89+
user_id=user_id,
90+
version_no=version_no,
91+
)
92+
return JSONResponse(status_code=HTTPStatus.OK, content=result)
93+
except UnauthorizedError as e:
94+
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
95+
except ValueError as e:
96+
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
97+
except Exception as e:
98+
logger.error(f"Create agent repository listing error: {str(e)}")
99+
raise HTTPException(
100+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
101+
detail="Create agent repository listing error.",
102+
)
103+
104+
105+
@agent_repository_router.post("/{agent_repository_id}/import")
106+
async def import_agent_from_repository_api(
107+
agent_repository_id: int,
108+
authorization: Optional[str] = Header(None),
109+
):
110+
"""Import an agent tree from a marketplace repository listing into the current tenant."""
111+
try:
112+
await import_agent_from_repository_impl(
113+
agent_repository_id=agent_repository_id,
114+
authorization=authorization,
115+
)
116+
return JSONResponse(status_code=HTTPStatus.OK, content={})
117+
except UnauthorizedError as e:
118+
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
119+
except SkillDuplicateError as exc:
120+
raise HTTPException(
121+
status_code=HTTPStatus.CONFLICT,
122+
detail={
123+
"type": "skill_duplicate",
124+
"duplicate_skills": exc.duplicate_names,
125+
},
126+
)
127+
except ValueError as e:
128+
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail=str(e))
129+
except Exception as e:
130+
logger.error(f"Import agent from repository error: {str(e)}")
131+
raise HTTPException(
132+
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
133+
detail="Import agent from repository error.",
134+
)

backend/apps/config_app.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from apps.app_factory import create_app
44
from apps.agent_app import agent_config_router as agent_router
5+
from apps.agent_repository_app import agent_repository_router
56
from apps.config_sync_app import router as config_sync_router
67
from apps.datamate_app import router as datamate_router
78
from apps.vectordatabase_app import router as vectordatabase_router
@@ -55,6 +56,7 @@ async def sync_default_prompt_template_on_startup():
5556
app.include_router(model_manager_router)
5657
app.include_router(config_sync_router)
5758
app.include_router(agent_router)
59+
app.include_router(agent_repository_router)
5860
app.include_router(vectordatabase_router)
5961
app.include_router(datamate_router)
6062
app.include_router(voice_router)

backend/consts/model.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -557,6 +557,7 @@ class MessageIdRequest(BaseModel):
557557

558558
class ExportAndImportAgentInfo(BaseModel):
559559
agent_id: int
560+
tenant_id: Optional[str] = None
560561
name: str
561562
display_name: Optional[str] = None
562563
description: str
@@ -593,6 +594,11 @@ class ExportAndImportDataFormat(BaseModel):
593594
mcp_info: List[MCPInfo]
594595

595596

597+
class AgentRepositorySnapshot(ExportAndImportDataFormat):
598+
"""Frozen marketplace snapshot: export format plus optional skill ZIP payloads."""
599+
skills: Optional[List["SkillZipEntry"]] = None
600+
601+
596602
class SkillZipEntry(BaseModel):
597603
"""A skill bundled inside an agent export ZIP."""
598604
skill_name: str

backend/database/agent_db.py

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import logging
2-
from typing import List
2+
from typing import List, Optional
33
from sqlalchemy import or_, update
44

55
from database.client import get_db_session, as_dict, filter_property
66
from database.db_models import AgentInfo, ToolInstance, AgentRelation
7+
from database.agent_version_db import query_current_version_no
78
from consts.const import ASSET_OWNER_TENANT_ID
89
from utils.str_utils import convert_list_to_string
910

@@ -102,6 +103,40 @@ def query_sub_agents_id_list(main_agent_id: int, tenant_id: str, version_no: int
102103
return [relation.selected_agent_id for relation in relations]
103104

104105

106+
def query_sub_agent_relations(main_agent_id: int, tenant_id: str, version_no: int = 0) -> List[dict]:
107+
"""
108+
Query sub-agent relations by main agent id, including pinned version info.
109+
Default version_no=0 queries the draft version.
110+
111+
Args:
112+
main_agent_id: Parent agent ID
113+
tenant_id: Tenant ID
114+
version_no: Version number to filter. Default 0 = draft/editing state
115+
"""
116+
with get_db_session() as session:
117+
query = session.query(AgentRelation).filter(
118+
AgentRelation.parent_agent_id == main_agent_id,
119+
AgentRelation.tenant_id == tenant_id,
120+
AgentRelation.version_no == version_no,
121+
AgentRelation.delete_flag != 'Y')
122+
relations = query.all()
123+
return [as_dict(relation) for relation in relations]
124+
125+
126+
def resolve_sub_agent_version_no(
127+
selected_agent_id: int,
128+
selected_agent_version_no: Optional[int],
129+
tenant_id: str,
130+
) -> int:
131+
"""
132+
Resolve the effective version number for a sub-agent relation.
133+
Uses pinned version when set; otherwise falls back to child's current published version.
134+
"""
135+
if selected_agent_version_no is not None:
136+
return selected_agent_version_no
137+
return query_current_version_no(agent_id=selected_agent_id, tenant_id=tenant_id) or 0
138+
139+
105140
def clear_agent_new_mark(agent_id: int, tenant_id: str, user_id: str, version_no: int = 0):
106141
"""
107142
Clear the NEW mark for an agent.

0 commit comments

Comments
 (0)