Skip to content
Merged
28 changes: 22 additions & 6 deletions backend/apps/skill_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
stream_skill_creation,
update_skill_list,
get_official_skills_with_status,
install_skills_from_zip_for_tenant,
)
from consts.model import SkillInstanceInfoRequest, SkillCreateRequest, SkillCreateInteractiveRequest, SkillUpdateRequest, SkillResponse
from utils.auth_utils import get_current_user_id, get_current_user_info
Expand Down Expand Up @@ -46,6 +47,8 @@ def _build_skill_update_data(request: SkillUpdateRequest) -> Dict[str, Any]:
"content",
"tags",
"source",
"group_ids",
"ingroup_permission",
"config_schemas",
"config_values",
):
Expand All @@ -66,11 +69,14 @@ async def list_skills(
) -> JSONResponse:
"""List all available skills for the current tenant (or a specific tenant for super admin)."""
try:
_, current_tenant_id = get_current_user_id(authorization)
user_id, current_tenant_id = get_current_user_id(authorization)
# Super admin can query a specific tenant's skills; otherwise use current user's tenant
effective_tenant_id = tenant_id if tenant_id else current_tenant_id
service = SkillService(tenant_id=effective_tenant_id)
skills = service.list_skills(tenant_id=effective_tenant_id)
skills = service.list_visible_skills(
tenant_id=effective_tenant_id,
user_id=user_id,
)
return JSONResponse(content={"skills": skills})
except SkillException as e:
raise HTTPException(status_code=500, detail=str(e))
Expand Down Expand Up @@ -122,8 +128,6 @@ async def install_skills(
"""
try:
user_id, current_tenant_id = get_current_user_id(authorization)
from services.skill_service import install_skills_from_zip_for_tenant

effective_tenant_id = tenant_id if tenant_id else current_tenant_id
installed_names = install_skills_from_zip_for_tenant(
skill_names=request.skill_names,
Expand Down Expand Up @@ -165,6 +169,8 @@ async def create_skill(
"tool_ids": tool_ids,
"tags": request.tags,
"source": request.source,
"group_ids": request.group_ids,
"ingroup_permission": request.ingroup_permission,
"config_schemas": request.config_schemas,
"config_values": request.config_values,
"files": request.files if request.files else [],
Expand Down Expand Up @@ -310,7 +316,10 @@ async def get_skill_file_content(
raise HTTPException(status_code=500, detail="Internal server error")


@router.put("/{skill_name}/upload")
@router.put(
"/{skill_name}/upload",
responses={403: {"description": "Not authorized to update this skill"}},
)
async def update_skill_from_file(
skill_name: str,
file: UploadFile = File(..., description="SKILL.md file or ZIP archive"),
Expand Down Expand Up @@ -343,6 +352,8 @@ async def update_skill_from_file(
return JSONResponse(content=skill)
except UnauthorizedError as e:
raise HTTPException(status_code=401, detail=str(e))
except ForbiddenError as e:
raise HTTPException(status_code=403, detail=str(e))
except SkillException as e:
if _NOT_FOUND_TEXT in str(e).lower():
raise HTTPException(status_code=404, detail=str(e))
Expand Down Expand Up @@ -603,7 +614,10 @@ async def get_skill(skill_name: str, authorization: Optional[str] = Header(None)
raise HTTPException(status_code=500, detail="Internal server error")


@router.put("/{skill_name}")
@router.put(
"/{skill_name}",
responses={403: {"description": "Not authorized to update this skill"}},
)
async def update_skill(
skill_name: str,
request: SkillUpdateRequest,
Expand Down Expand Up @@ -644,6 +658,8 @@ async def update_skill(
return JSONResponse(content=skill)
except UnauthorizedError as e:
raise HTTPException(status_code=401, detail=str(e))
except ForbiddenError as e:
raise HTTPException(status_code=403, detail=str(e))
except SkillException as e:
if _NOT_FOUND_TEXT in str(e).lower():
raise HTTPException(status_code=404, detail=str(e))
Expand Down
3 changes: 2 additions & 1 deletion backend/apps/skill_repository_app.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,10 @@ async def list_skill_repository_listings_api(
):
"""List all skill marketplace repository listings with optional filters."""
try:
_, tenant_id = get_current_user_id(authorization)
user_id, tenant_id = get_current_user_id(authorization)
result = list_skill_repository_listings_impl(
tenant_id,
user_id=user_id,
status=status,
skill_id=skill_id,
category_id=category_id,
Expand Down
6 changes: 6 additions & 0 deletions backend/consts/model.py
Original file line number Diff line number Diff line change
Expand Up @@ -1336,6 +1336,8 @@ class SkillCreateRequest(BaseModel):
tool_names: Optional[List[str]] = []
tags: Optional[List[str]] = []
source: Optional[str] = "custom"
group_ids: Optional[List[int]] = None
ingroup_permission: Optional[str] = None
config_schemas: Optional[Dict[str, Any]] = None
config_values: Optional[Dict[str, Any]] = None
files: Optional[List[Dict[str, str]]] = Field(
Expand All @@ -1361,6 +1363,8 @@ class SkillUpdateRequest(BaseModel):
tool_names: Optional[List[str]] = None
tags: Optional[List[str]] = None
source: Optional[str] = None
group_ids: Optional[List[int]] = None
ingroup_permission: Optional[str] = None
config_schemas: Optional[Dict[str, Any]] = None
config_values: Optional[Dict[str, Any]] = None
files: Optional[List[SkillFileData]] = Field(
Expand All @@ -1379,6 +1383,8 @@ class SkillResponse(BaseModel):
tool_ids: List[int]
tags: List[str]
source: str
group_ids: Optional[List[int]] = None
ingroup_permission: Optional[str] = None
config_schemas: Optional[Dict[str, Any]] = None
config_values: Optional[Dict[str, Any]] = None
created_by: Optional[str] = None
Expand Down
7 changes: 5 additions & 2 deletions backend/database/db_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
_PUBLISHER_TENANT_ID_DOC = "Publisher tenant ID"
_PUBLISHER_USER_ID_DOC = "Publisher user ID"
_MCP_NAME_DOC = "MCP name"
_INGROUP_PERMISSION_DOC = "In-group permission: EDIT, READ_ONLY, PRIVATE"

# Base class for tables without audit fields

Expand Down Expand Up @@ -611,7 +612,7 @@ class AgentInfo(TableBase):
group_ids = Column(String, doc="Agent group IDs list")
is_new = Column(Boolean, default=False, doc="Whether this agent is marked as new for the user")
current_version_no = Column(Integer, nullable=True, doc="Current published version number. NULL means no version published yet")
ingroup_permission = Column(String(30), doc="In-group permission: EDIT, READ_ONLY, PRIVATE")
ingroup_permission = Column(String(30), doc=_INGROUP_PERMISSION_DOC)
requested_output_tokens = Column(
Integer,
doc=(
Expand Down Expand Up @@ -708,7 +709,7 @@ class KnowledgeRecord(TableBase):
tenant_id = Column(String(100), doc="Tenant ID")
group_ids = Column(String, doc="Knowledge base group IDs list")
ingroup_permission = Column(
String(30), doc="In-group permission: EDIT, READ_ONLY, PRIVATE")
String(30), doc=_INGROUP_PERMISSION_DOC)
summary_frequency = Column(String(10), nullable=True,
doc="Auto-summary frequency: '3h', '5h', '1d', '1w', or NULL (disabled)")
last_summary_time = Column(TIMESTAMP(timezone=False), nullable=True,
Expand Down Expand Up @@ -1202,6 +1203,8 @@ class SkillInfo(TableBase):
JSON, doc="Runtime parameter values from config/config.yaml")
source = Column(String(30), nullable=False, default="official",
doc="Skill source: official, custom, etc.")
group_ids = Column(String, doc="Skill group IDs list")
ingroup_permission = Column(String(30), doc=_INGROUP_PERMISSION_DOC)


class SkillToolRelation(TableBase):
Expand Down
18 changes: 18 additions & 0 deletions backend/database/skill_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from database.client import get_db_session, filter_property, as_dict
from database.db_models import SkillInfo, SkillToolRelation, SkillInstance, ToolInfo
from utils.skill_params_utils import strip_params_comments_for_db
from utils.str_utils import convert_list_to_string, convert_string_to_list

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -231,10 +232,16 @@ def _build_skill_update_values(
"content": "skill_content",
"tags": "skill_tags",
"source": "source",
"ingroup_permission": "ingroup_permission",
}
for input_field, model_field in field_mapping.items():
if input_field in skill_data:
row_values[model_field] = skill_data[input_field]
if "group_ids" in skill_data:
group_ids = skill_data["group_ids"]
row_values["group_ids"] = (
convert_list_to_string(group_ids) if isinstance(group_ids, list) else group_ids
)

for field in ("config_schemas", "config_values"):
if field in skill_data:
Expand Down Expand Up @@ -270,6 +277,8 @@ def _to_dict(skill: SkillInfo) -> Dict[str, Any]:
"config_schemas": skill.config_schemas,
"config_values": skill.config_values,
"source": skill.source,
"group_ids": convert_string_to_list(skill.group_ids),
"ingroup_permission": skill.ingroup_permission,
"created_by": skill.created_by,
"create_time": skill.create_time.isoformat() if skill.create_time else None,
"updated_by": skill.updated_by,
Expand All @@ -287,6 +296,9 @@ def list_skills(tenant_id: str) -> List[Dict[str, Any]]:
skills = session.query(SkillInfo).filter(
SkillInfo.tenant_id == tenant_id,
SkillInfo.delete_flag != 'Y'
).order_by(
SkillInfo.create_time.desc(),
SkillInfo.skill_id.desc(),
).all()
results = []
for s in skills:
Expand Down Expand Up @@ -396,6 +408,12 @@ def create_skill(skill_data: Dict[str, Any], tenant_id: str) -> Dict[str, Any]:
config_values=_params_value_for_db(
skill_data.get("config_values")),
source=skill_data.get("source", "custom"),
group_ids=(
convert_list_to_string(skill_data.get("group_ids"))
if isinstance(skill_data.get("group_ids"), list)
else skill_data.get("group_ids")
),
ingroup_permission=skill_data.get("ingroup_permission"),
created_by=skill_data.get("created_by"),
create_time=datetime.now(),
updated_by=skill_data.get("updated_by"),
Expand Down
30 changes: 29 additions & 1 deletion backend/database/skill_repository_db.py
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ def get_skill_repository_by_skill_id(
skill_id: int,
*,
publisher_tenant_id: Optional[str] = None,
statuses: Optional[Collection[str]] = None,
) -> Optional[dict]:
"""Fetch an active repository listing by source skill_id."""
with get_db_session() as session:
Expand All @@ -77,7 +78,9 @@ def get_skill_repository_by_skill_id(
query = query.filter(
SkillRepository.publisher_tenant_id == publisher_tenant_id,
)
record = query.first()
if statuses is not None:
query = query.filter(SkillRepository.status.in_(list(statuses)))
record = query.order_by(SkillRepository.update_time.desc()).first()
return as_dict(record) if record else None


Expand Down Expand Up @@ -133,6 +136,7 @@ def list_skill_repository_summaries(
query = session.query(
SkillRepository.skill_repository_id,
SkillRepository.skill_id,
SkillRepository.publisher_user_id,
SkillRepository.submitted_by,
SkillRepository.name,
SkillRepository.description,
Expand Down Expand Up @@ -171,6 +175,7 @@ def list_skill_repository_summaries(
{
"skill_repository_id": row.skill_repository_id,
"skill_id": row.skill_id,
"publisher_user_id": row.publisher_user_id,
"submitted_by": row.submitted_by,
"name": row.name,
"description": row.description,
Expand Down Expand Up @@ -265,6 +270,29 @@ def update_skill_repository_status_by_id(
return int(result.rowcount or 0)


def reset_skill_repository_status(
*,
repository_id: int,
skill_id: int,
status: str,
publisher_tenant_id: str,
) -> int:
"""Set other active listings with the same skill and status to not_shared."""
with get_db_session() as session:
result = session.execute(
update(SkillRepository)
.where(
SkillRepository.skill_id == skill_id,
SkillRepository.status == status,
SkillRepository.skill_repository_id != repository_id,
SkillRepository.publisher_tenant_id == publisher_tenant_id,
SkillRepository.delete_flag != "Y",
)
.values(status=STATUS_NOT_SHARED)
)
return int(result.rowcount or 0)


def increment_skill_repository_downloads(
*,
repository_id: int,
Expand Down
Loading
Loading