Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
105 changes: 75 additions & 30 deletions backend/apps/agent_repository_app.py
Original file line number Diff line number Diff line change
@@ -1,43 +1,100 @@
import logging
from http import HTTPStatus
from typing import Optional

from fastapi import APIRouter, Body, Header, HTTPException, Query
from starlette.responses import JSONResponse

from consts.exceptions import SkillDuplicateError, UnauthorizedError
from consts.model import AgentRepositoryListingCreateRequest
from services.agent_repository_service import (
create_agent_repository_listing_impl,
get_agent_repository_listing_detail_impl,
import_agent_from_repository_impl,
list_agent_repository_listings_impl,
list_my_editable_agents_impl,
update_agent_repository_status_impl,
)
from utils.auth_utils import get_current_user_id

agent_repository_router = APIRouter(prefix="/repository/agent")
logger = logging.getLogger("agent_repository_app")


@agent_repository_router.get("")
async def list_agent_repository_listings_api(
status: Optional[str] = Query(None, description="Filter by listing status"),
agent_id: Optional[int] = Query(None, description="Filter by source agent ID"),

Check warning on line 25 in backend/apps/agent_repository_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ70gM8UCS1JGHu2kOMQ&open=AZ70gM8UCS1JGHu2kOMQ&pullRequest=3289
deduplicate_by_agent_id: Optional[bool] = Query(
None,
description="Whether to return one listing per agent",
),

Check warning on line 29 in backend/apps/agent_repository_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ70gM8UCS1JGHu2kOMR&open=AZ70gM8UCS1JGHu2kOMR&pullRequest=3289
category_id: Optional[int] = Query(
None,
description="Filter by marketplace category ID",
),

Check warning on line 33 in backend/apps/agent_repository_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ70gM8UCS1JGHu2kOMS&open=AZ70gM8UCS1JGHu2kOMS&pullRequest=3289
authorization: str = Header(None),
):
"""List all marketplace repository listings with optional status filter."""
try:
get_current_user_id(authorization)
result = list_agent_repository_listings_impl(status=status)
_, tenant_id = get_current_user_id(authorization)
should_deduplicate = (
agent_id is None
if deduplicate_by_agent_id is None
else deduplicate_by_agent_id
)
result = list_agent_repository_listings_impl(
tenant_id,
status=status,
agent_id=agent_id,
deduplicate_by_agent_id=should_deduplicate,
category_id=category_id,
)
return JSONResponse(status_code=HTTPStatus.OK, content=result)
except UnauthorizedError as e:
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
except ValueError as e:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
except Exception as e:
logger.error(f"List agent repository listings error: {str(e)}")
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="List agent repository listings error.",


@agent_repository_router.get("/mine")
async def list_my_editable_agents_api(
ownership: Optional[str] = Query(
"all",
description="Filter by ownership: all / created / others",
),

Check warning on line 63 in backend/apps/agent_repository_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ70gM8UCS1JGHu2kOMV&open=AZ70gM8UCS1JGHu2kOMV&pullRequest=3289
authorization: str = Header(None),

Check warning on line 64 in backend/apps/agent_repository_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ70gM8UCS1JGHu2kOMW&open=AZ70gM8UCS1JGHu2kOMW&pullRequest=3289
):
"""List editable draft agents for the current user with repository listing info."""
try:
user_id, tenant_id = get_current_user_id(authorization)
result = list_my_editable_agents_impl(
tenant_id=tenant_id,
user_id=user_id,
ownership=ownership or "all",
)
return JSONResponse(status_code=HTTPStatus.OK, content=result)
except UnauthorizedError as e:
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
except ValueError as e:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))


@agent_repository_router.get("/{agent_repository_id}")
async def get_agent_repository_listing_detail_api(
agent_repository_id: int,
authorization: str = Header(None),

Check warning on line 84 in backend/apps/agent_repository_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ70gM8UCS1JGHu2kOMY&open=AZ70gM8UCS1JGHu2kOMY&pullRequest=3289
):
"""Get detailed marketplace repository listing by primary key."""
try:
_, tenant_id = get_current_user_id(authorization)
result = get_agent_repository_listing_detail_impl(
agent_repository_id,
tenant_id,
)
return JSONResponse(status_code=HTTPStatus.OK, content=result)
except UnauthorizedError as e:
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
except ValueError as e:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail=str(e))


@agent_repository_router.patch("/{agent_repository_id}/status")
Expand All @@ -47,59 +104,51 @@
...,
embed=True,
description=(
"New status: NOT_SHARED (未共享) / PENDING_REVIEW (待审核) / "
"REJECTED (审核驳回) / SHARED (已共享)"
"New status: not_shared (未共享) / pending_review (待审核) / "
"rejected (审核驳回) / shared (已共享)"
),
),
authorization: str = Header(None),
):
"""Update marketplace repository listing status (share, unshare, approve, reject)."""
try:
user_id, _ = get_current_user_id(authorization)
user_id, tenant_id = get_current_user_id(authorization)
result = update_agent_repository_status_impl(
agent_repository_id=agent_repository_id,
status=status,
user_id=user_id,
tenant_id=tenant_id,
)
return JSONResponse(status_code=HTTPStatus.OK, content=result)
except UnauthorizedError as e:
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
except ValueError as e:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
except Exception as e:
logger.error(f"Update agent repository status error: {str(e)}")
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Update agent repository status error.",
)


@agent_repository_router.post("/{agent_id}/versions/{version_no}")
async def create_agent_repository_listing_api(
agent_id: int,
version_no: int,
payload: Optional[AgentRepositoryListingCreateRequest] = Body(None),

Check warning on line 133 in backend/apps/agent_repository_app.py

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Use "Annotated" type hints for FastAPI dependency injection

See more on https://sonarcloud.io/project/issues?id=ModelEngine-Group_nexent&issues=AZ70gM8UCS1JGHu2kOMa&open=AZ70gM8UCS1JGHu2kOMa&pullRequest=3289
authorization: str = Header(None),
):
"""Create or update a marketplace repository listing from an agent version snapshot."""
try:
user_id, tenant_id = get_current_user_id(authorization)
card_fields = payload.model_dump(exclude_none=True) if payload else None
result = await create_agent_repository_listing_impl(
agent_id=agent_id,
tenant_id=tenant_id,
user_id=user_id,
version_no=version_no,
card_fields=card_fields,
)
return JSONResponse(status_code=HTTPStatus.OK, content=result)
except UnauthorizedError as e:
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
except ValueError as e:
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))
except Exception as e:
logger.error(f"Create agent repository listing error: {str(e)}")
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Create agent repository listing error.",
)


@agent_repository_router.post("/{agent_repository_id}/import")
Expand All @@ -109,8 +158,10 @@
):
"""Import an agent tree from a marketplace repository listing into the current tenant."""
try:
_, tenant_id = get_current_user_id(authorization)
await import_agent_from_repository_impl(
agent_repository_id=agent_repository_id,
tenant_id=tenant_id,
authorization=authorization,
)
return JSONResponse(status_code=HTTPStatus.OK, content={})
Expand All @@ -126,9 +177,3 @@
)
except ValueError as e:
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail=str(e))
except Exception as e:
logger.error(f"Import agent from repository error: {str(e)}")
raise HTTPException(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR,
detail="Import agent from repository error.",
)
36 changes: 36 additions & 0 deletions backend/consts/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -627,6 +627,42 @@ class AgentRepositorySnapshot(ExportAndImportDataFormat):
skills: Optional[List["SkillZipEntry"]] = None


class AgentRepositoryListingCreateRequest(BaseModel):
"""Request body for creating a marketplace listing from an agent version."""
icon: Optional[str] = Field(None, description="Marketplace card icon (emoji or URL)")
downloads: int = Field(0, ge=0, description="Initial download/copy count for card display")
tags: Optional[List[str]] = Field(None, description="Marketplace tags")
category_id: Optional[int] = Field(0, description="Optional marketplace category ID")
tool_count: Optional[int] = Field(
None, ge=0, description="Total tool count across all agents in the bundle"
)


class AgentRepositoryCategoryItem(BaseModel):
"""Marketplace category option for agent repository filtering."""
id: int
key: str
name: str


class AgentRepositoryListingDetailResponse(BaseModel):
"""Detailed marketplace listing payload for repository detail view."""
agent_repository_id: int
agent_id: Optional[int] = None
name: str
display_name: Optional[str] = None
description: Optional[str] = None
author: Optional[str] = None
icon: Optional[str] = None
status: str
version_label: Optional[str] = None
downloads: int = 0
created_at: Optional[str] = None
model_name: Optional[str] = None
duty_prompt: Optional[str] = None
tools: List[str] = Field(default_factory=list)


class SkillZipEntry(BaseModel):
"""A skill bundled inside an agent export ZIP."""
skill_name: str
Expand Down
Loading
Loading