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
3 changes: 2 additions & 1 deletion backend/apps/agent_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
get_version_list_impl,
get_version_impl,
get_version_detail_impl,
_get_version_detail_or_draft,
rollback_version_impl,
update_version_status_impl,
update_version_impl,
Expand Down Expand Up @@ -447,7 +448,7 @@ async def get_version_detail_api(
"""
try:
_, tenant_id = get_current_user_id(authorization)
result = get_version_detail_impl(
result = _get_version_detail_or_draft(
agent_id=agent_id,
tenant_id=tenant_id,
version_no=version_no,
Expand Down
116 changes: 104 additions & 12 deletions backend/apps/agent_repository_app.py
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
import logging
from http import HTTPStatus
from typing import Optional
from typing import Annotated, 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 (
check_repository_import_precheck_impl,
create_agent_repository_listing_impl,
get_agent_repository_listing_detail_impl,
import_agent_from_repository_impl,
Expand All @@ -16,42 +18,49 @@
)
from utils.auth_utils import get_current_user_id

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


@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"),
deduplicate_by_agent_id: Optional[bool] = Query(
None,
description="Whether to return one listing per agent",
),
category_id: Optional[int] = Query(
None,
description="Filter by marketplace category ID",
),
page: Annotated[int, Query(ge=1, description="Page number starting from 1")] = 1,
page_size: Annotated[
int, Query(ge=1, le=100, description="Page size from 1 to 100")
] = 10,
search: Optional[str] = Query(
None, description="Filter by name, description, author, or tags"
),

Check warning on line 39 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=AZ8H0m2EpqeD2E2GwWXy&open=AZ8H0m2EpqeD2E2GwWXy&pullRequest=3316
authorization: str = Header(None),
):
"""List all marketplace repository listings with optional status filter."""
try:
_, 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,
page=page,
page_size=page_size,
search=search,
)
return JSONResponse(status_code=HTTPStatus.OK, content=result)
except UnauthorizedError as e:
logger.warning(
f"Unauthorized agent repository listings access attempt: {str(e)}"
)
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
except ValueError as e:
logger.warning(
f"Invalid agent repository listings request parameters: {str(e)}"
)
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))


Expand All @@ -61,20 +70,41 @@
"all",
description="Filter by ownership: all / created / others",
),
page: Annotated[int, Query(ge=1, description="Page number starting from 1")] = 1,
page_size: Annotated[
int, Query(ge=1, le=100, description="Page size from 1 to 100")
] = 10,
search: Optional[str] = Query(
None, description="Filter by agent name or description"
),

Check warning on line 79 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=AZ8H0m2EpqeD2E2GwWXz&open=AZ8H0m2EpqeD2E2GwWXz&pullRequest=3316
new_agent_padding: bool = Query(
False,
description="Reserve first slot on page 1 for create-agent placeholder",
),

Check warning on line 83 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=AZ8H0m2EpqeD2E2GwWX0&open=AZ8H0m2EpqeD2E2GwWX0&pullRequest=3316
authorization: str = Header(None),
):
"""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(
result = await list_my_editable_agents_impl(
tenant_id=tenant_id,
user_id=user_id,
ownership=ownership or "all",
page=page,
page_size=page_size,
search=search,
new_agent_padding=new_agent_padding,
)
return JSONResponse(status_code=HTTPStatus.OK, content=result)
except UnauthorizedError as e:
logger.warning(
f"Unauthorized my editable agents access attempt: {str(e)}"
)
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
except ValueError as e:
logger.warning(
f"Invalid my editable agents request parameters (ownership={ownership}): {str(e)}"
)
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))


Expand All @@ -92,8 +122,15 @@
)
return JSONResponse(status_code=HTTPStatus.OK, content=result)
except UnauthorizedError as e:
logger.warning(
f"Unauthorized agent repository listing detail access attempt "
f"(id={agent_repository_id}): {str(e)}"
)
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
except ValueError as e:
logger.warning(
f"Agent repository listing not found (id={agent_repository_id}): {str(e)}"
)
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail=str(e))


Expand Down Expand Up @@ -121,8 +158,16 @@
)
return JSONResponse(status_code=HTTPStatus.OK, content=result)
except UnauthorizedError as e:
logger.warning(
f"Unauthorized agent repository status update attempt "
f"(id={agent_repository_id}, status={status}): {str(e)}"
)
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
except ValueError as e:
logger.warning(
f"Invalid agent repository status update "
f"(id={agent_repository_id}, status={status}): {str(e)}"
)
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))


Expand All @@ -146,8 +191,43 @@
)
return JSONResponse(status_code=HTTPStatus.OK, content=result)
except UnauthorizedError as e:
logger.warning(
f"Unauthorized agent repository listing creation attempt "
f"(agent_id={agent_id}, version_no={version_no}): {str(e)}"
)
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
except ValueError as e:
logger.warning(
f"Invalid agent repository listing creation parameters "
f"(agent_id={agent_id}, version_no={version_no}): {str(e)}"
)
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))


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

Check warning on line 210 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=AZ8H0m2EpqeD2E2GwWX1&open=AZ8H0m2EpqeD2E2GwWX1&pullRequest=3316
):
"""Precheck import dependencies for a shared marketplace listing."""
try:
_, tenant_id = get_current_user_id(authorization)
result = check_repository_import_precheck_impl(
agent_repository_id=agent_repository_id,
tenant_id=tenant_id,
)
return JSONResponse(status_code=HTTPStatus.OK, content=result)
except UnauthorizedError as e:
logger.warning(
f"Unauthorized agent repository import precheck attempt "
f"(id={agent_repository_id}): {str(e)}"
)
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
except ValueError as e:
logger.warning(
f"Agent repository import precheck failed "
f"(id={agent_repository_id}): {str(e)}"
)
raise HTTPException(status_code=HTTPStatus.BAD_REQUEST, detail=str(e))


Expand All @@ -166,8 +246,16 @@
)
return JSONResponse(status_code=HTTPStatus.OK, content={})
except UnauthorizedError as e:
logger.warning(
f"Unauthorized agent repository import attempt "
f"(id={agent_repository_id}): {str(e)}"
)
raise HTTPException(status_code=HTTPStatus.UNAUTHORIZED, detail=str(e))
except SkillDuplicateError as exc:
logger.warning(
f"Skill duplicate on repository import (id={agent_repository_id}): "
f"{exc.duplicate_names}"
)
raise HTTPException(
status_code=HTTPStatus.CONFLICT,
detail={
Expand All @@ -176,4 +264,8 @@
},
)
except ValueError as e:
logger.warning(
f"Agent repository listing not found for import "
f"(id={agent_repository_id}): {str(e)}"
)
raise HTTPException(status_code=HTTPStatus.NOT_FOUND, detail=str(e))
25 changes: 25 additions & 0 deletions backend/consts/agent_repository.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
"""Domain constants for agent marketplace repository listings."""

# Listing status: not_shared (未共享), pending_review (待审核),
# rejected (审核驳回), shared (已共享)
STATUS_NOT_SHARED = "not_shared"
STATUS_PENDING_REVIEW = "pending_review"
STATUS_REJECTED = "rejected"
STATUS_SHARED = "shared"

VALID_REPOSITORY_STATUSES = frozenset({
STATUS_NOT_SHARED,
STATUS_PENDING_REVIEW,
STATUS_REJECTED,
STATUS_SHARED,
})

OWNERSHIP_ALL = "all"
OWNERSHIP_CREATED = "created"
OWNERSHIP_OTHERS = "others"

VALID_OWNERSHIP_FILTERS = frozenset({
OWNERSHIP_ALL,
OWNERSHIP_CREATED,
OWNERSHIP_OTHERS,
})
26 changes: 26 additions & 0 deletions backend/consts/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,6 +680,32 @@ class AgentRepositorySnapshot(ExportAndImportDataFormat):
skills: Optional[List["SkillZipEntry"]] = None


RepositoryImportRequirementType = Literal[
"model", "knowledge_base", "mcp", "skill", "tool"
]


class RepositoryImportRequirementItem(BaseModel):
"""Single dependency item for repository import precheck."""
type: RepositoryImportRequirementType
key: str
name: str
description: Optional[str] = None
available: bool
reason_code: Optional[str] = None


class RepositoryImportPrecheckResponse(BaseModel):
"""Response payload for repository import precheck."""
agent_repository_id: int
display_name: str
total_count: int
available_count: int
percent: int
has_abnormal: bool
items: List[RepositoryImportRequirementItem]


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)")
Expand Down
Loading
Loading