"""Standardize an uploaded Skill into the agent drive (ENG-594). A validated Skill package is normalized into two **drive-owned** objects committed to the agent drive (Agent Files §5.4 / §4): * ``/SKILL.md`` — the canonical entry, the source of truth for loading. * ``/.DIFY-SKILL-FULL.zip`` — the full archive, kept only to restore the complete skill contents. Both are stored as ``ToolFile`` records and bound via ``AgentDriveService.commit`` with ``value_owned_by_drive=True`` (the drive owns their lifecycle). The returned skill ref records the stable drive paths + file ids (not just the raw upload id), so the Composer can reload the bound skill list. """ from __future__ import annotations import re from typing import Any from core.tools.tool_file_manager import ToolFileManager from models.agent_config_entities import AgentSkillRefConfig from services.agent.skill_package_service import SkillPackageService from services.agent_drive_service import AgentDriveService, DriveCommitItem, DriveFileRef _FULL_ARCHIVE_NAME = ".DIFY-SKILL-FULL.zip" _SKILL_MD_NAME = "SKILL.md" _SLUG_RE = re.compile(r"[^a-z0-9._-]+") def slugify_skill_name(name: str) -> str: slug = _SLUG_RE.sub("-", (name or "").strip().lower()).strip("-._") return slug or "skill" class SkillStandardizeService: """Validate + standardize a Skill package into a per-agent drive.""" def __init__( self, *, package_service: SkillPackageService | None = None, drive_service: AgentDriveService | None = None, tool_file_manager: ToolFileManager | None = None, ) -> None: self._package = package_service or SkillPackageService() self._drive = drive_service or AgentDriveService() self._tool_files = tool_file_manager or ToolFileManager() def standardize( self, *, content: bytes, filename: str, tenant_id: str, user_id: str, agent_id: str, ) -> dict[str, Any]: manifest = self._package.validate_and_extract(content=content, filename=filename) skill_md_bytes = self._package.read_member_bytes(content=content, member_path=manifest.entry_path) slug = slugify_skill_name(manifest.name) # Two drive-owned ToolFiles: canonical SKILL.md + the full archive. md_tool_file = self._tool_files.create_file_by_raw( user_id=user_id, tenant_id=tenant_id, conversation_id=None, file_binary=skill_md_bytes, mimetype="text/markdown", filename=_SKILL_MD_NAME, ) archive_tool_file = self._tool_files.create_file_by_raw( user_id=user_id, tenant_id=tenant_id, conversation_id=None, file_binary=content, mimetype="application/zip", filename=_FULL_ARCHIVE_NAME, ) skill_md_key = f"{slug}/{_SKILL_MD_NAME}" archive_key = f"{slug}/{_FULL_ARCHIVE_NAME}" self._drive.commit( tenant_id=tenant_id, user_id=user_id, agent_id=agent_id, items=[ DriveCommitItem( key=skill_md_key, file_ref=DriveFileRef(kind="tool_file", id=md_tool_file.id), value_owned_by_drive=True, ), DriveCommitItem( key=archive_key, file_ref=DriveFileRef(kind="tool_file", id=archive_tool_file.id), value_owned_by_drive=True, ), ], ) skill_ref = AgentSkillRefConfig.model_validate( { "id": manifest.hash, "name": manifest.name, "description": manifest.description, "file_id": archive_tool_file.id, "path": slug, "size": manifest.size, "hash": manifest.hash, "entry_path": skill_md_key, "skill_md_file_id": md_tool_file.id, "skill_md_key": skill_md_key, "full_archive_file_id": archive_tool_file.id, "full_archive_key": archive_key, } ) return { "skill": skill_ref.model_dump(exclude_none=True), "manifest": manifest.model_dump(), } __all__ = ["SkillStandardizeService", "slugify_skill_name"]