Files
2026-01-07 19:20:22 -05:00

399 lines
12 KiB
Python

"""
Utility functions for managing custom skills with Claude's Skills API.
This module provides helper functions for:
- Creating and uploading custom skills
- Listing and retrieving skill information
- Managing skill versions
- Testing skills with Claude
- Deleting skills
"""
from pathlib import Path
from typing import Any
from anthropic import Anthropic
from anthropic.lib import files_from_dir
def create_skill(client: Anthropic, skill_path: str, display_title: str) -> dict[str, Any]:
"""
Create a new custom skill from a directory.
The directory must contain:
- SKILL.md file with YAML frontmatter (name, description)
- Optional: scripts, resources, REFERENCE.md
Args:
client: Anthropic client instance with Skills beta
skill_path: Path to skill directory containing SKILL.md
display_title: Human-readable name for the skill
Returns:
Dictionary with skill creation results:
{
'success': bool,
'skill_id': str (if successful),
'display_title': str,
'latest_version': str,
'created_at': str,
'source': str ('custom'),
'error': str (if failed)
}
Example:
>>> client = Anthropic(api_key="...", default_headers={"anthropic-beta": "skills-2025-10-02"})
>>> result = create_skill(client, "custom_skills/financial_analyzer", "Financial Analyzer")
>>> if result['success']:
... print(f"Created skill: {result['skill_id']}")
"""
try:
# Validate skill directory
skill_dir = Path(skill_path)
if not skill_dir.exists():
return {"success": False, "error": f"Skill directory does not exist: {skill_path}"}
skill_md = skill_dir / "SKILL.md"
if not skill_md.exists():
return {"success": False, "error": f"SKILL.md not found in {skill_path}"}
# Create skill using files_from_dir
skill = client.beta.skills.create(
display_title=display_title, files=files_from_dir(skill_path)
)
return {
"success": True,
"skill_id": skill.id,
"display_title": skill.display_title,
"latest_version": skill.latest_version,
"created_at": skill.created_at,
"source": skill.source,
}
except Exception as e:
return {"success": False, "error": str(e)}
def list_custom_skills(client: Anthropic) -> list[dict[str, Any]]:
"""
List all custom skills in the workspace.
Args:
client: Anthropic client instance with Skills beta
Returns:
List of skill dictionaries with metadata
Example:
>>> skills = list_custom_skills(client)
>>> for skill in skills:
... print(f"{skill['display_title']}: {skill['skill_id']}")
"""
try:
skills_response = client.beta.skills.list(source="custom")
skills = []
for skill in skills_response.data:
skills.append(
{
"skill_id": skill.id,
"display_title": skill.display_title,
"latest_version": skill.latest_version,
"created_at": skill.created_at,
"updated_at": skill.updated_at,
}
)
return skills
except Exception as e:
print(f"Error listing skills: {e}")
return []
def get_skill_version(
client: Anthropic, skill_id: str, version: str = "latest"
) -> dict[str, Any] | None:
"""
Get detailed information about a specific skill version.
Args:
client: Anthropic client instance
skill_id: ID of the skill
version: Version to retrieve (default: "latest")
Returns:
Dictionary with version details or None if not found
"""
try:
# Get latest version if not specified
if version == "latest":
skill = client.beta.skills.retrieve(skill_id)
version = skill.latest_version
version_info = client.beta.skills.versions.retrieve(skill_id=skill_id, version=version)
return {
"version": version_info.version,
"skill_id": version_info.skill_id,
"name": version_info.name,
"description": version_info.description,
"directory": version_info.directory,
"created_at": version_info.created_at,
}
except Exception as e:
print(f"Error getting skill version: {e}")
return None
def create_skill_version(client: Anthropic, skill_id: str, skill_path: str) -> dict[str, Any]:
"""
Create a new version of an existing skill.
Args:
client: Anthropic client instance
skill_id: ID of the existing skill
skill_path: Path to updated skill directory
Returns:
Dictionary with version creation results
"""
try:
version = client.beta.skills.versions.create(
skill_id=skill_id, files=files_from_dir(skill_path)
)
return {
"success": True,
"version": version.version,
"skill_id": version.skill_id,
"created_at": version.created_at,
}
except Exception as e:
return {"success": False, "error": str(e)}
def delete_skill(client: Anthropic, skill_id: str, delete_versions: bool = True) -> bool:
"""
Delete a custom skill and optionally all its versions.
Note: All versions must be deleted before the skill can be deleted.
Args:
client: Anthropic client instance
skill_id: ID of skill to delete
delete_versions: Whether to delete all versions first
Returns:
True if successful, False otherwise
"""
try:
if delete_versions:
# First delete all versions
versions = client.beta.skills.versions.list(skill_id=skill_id)
for version in versions.data:
client.beta.skills.versions.delete(skill_id=skill_id, version=version.version)
print(f" Deleted version: {version.version}")
# Then delete the skill itself
client.beta.skills.delete(skill_id)
print(f"✓ Deleted skill: {skill_id}")
return True
except Exception as e:
print(f"Error deleting skill: {e}")
return False
def test_skill(
client: Anthropic,
skill_id: str,
test_prompt: str,
model: str = "claude-sonnet-4-5",
include_anthropic_skills: list[str] | None = None,
) -> Any:
"""
Test a custom skill with a prompt.
Args:
client: Anthropic client instance
skill_id: ID of skill to test
test_prompt: Prompt to test the skill
model: Model to use for testing
include_anthropic_skills: Optional list of Anthropic skill IDs to include
Returns:
Response from Claude
Example:
>>> response = test_skill(
... client,
... "skill_abc123",
... "Calculate P/E ratio for a company with price $50 and earnings $2.50",
... include_anthropic_skills=["xlsx"]
... )
"""
# Build skills list
skills = [{"type": "custom", "skill_id": skill_id, "version": "latest"}]
# Add Anthropic skills if requested
if include_anthropic_skills:
for anthropic_skill in include_anthropic_skills:
skills.append({"type": "anthropic", "skill_id": anthropic_skill, "version": "latest"})
response = client.beta.messages.create(
model=model,
max_tokens=4096,
container={"skills": skills},
tools=[{"type": "code_execution_20250825", "name": "code_execution"}],
messages=[{"role": "user", "content": test_prompt}],
betas=["code-execution-2025-08-25", "files-api-2025-04-14", "skills-2025-10-02"],
)
return response
def list_skill_versions(client: Anthropic, skill_id: str) -> list[dict[str, Any]]:
"""
List all versions of a skill.
Args:
client: Anthropic client instance
skill_id: ID of the skill
Returns:
List of version dictionaries
"""
try:
versions_response = client.beta.skills.versions.list(skill_id=skill_id)
versions = []
for version in versions_response.data:
versions.append(
{
"version": version.version,
"skill_id": version.skill_id,
"created_at": version.created_at,
}
)
return versions
except Exception as e:
print(f"Error listing versions: {e}")
return []
def validate_skill_directory(skill_path: str) -> dict[str, Any]:
"""
Validate a skill directory structure before upload.
Checks for:
- SKILL.md exists
- YAML frontmatter is valid
- Directory name matches skill name
- Total size is under 8MB
Args:
skill_path: Path to skill directory
Returns:
Dictionary with validation results
"""
result = {"valid": True, "errors": [], "warnings": [], "info": {}}
skill_dir = Path(skill_path)
# Check directory exists
if not skill_dir.exists():
result["valid"] = False
result["errors"].append(f"Directory does not exist: {skill_path}")
return result
# Check for SKILL.md
skill_md = skill_dir / "SKILL.md"
if not skill_md.exists():
result["valid"] = False
result["errors"].append("SKILL.md file is required")
else:
# Read and validate SKILL.md
content = skill_md.read_text()
# Check for YAML frontmatter
if not content.startswith("---"):
result["valid"] = False
result["errors"].append("SKILL.md must start with YAML frontmatter (---)")
else:
# Extract frontmatter
try:
end_idx = content.index("---", 3)
frontmatter = content[3:end_idx].strip()
# Check for required fields
if "name:" not in frontmatter:
result["valid"] = False
result["errors"].append("YAML frontmatter must include 'name' field")
if "description:" not in frontmatter:
result["valid"] = False
result["errors"].append("YAML frontmatter must include 'description' field")
# Check frontmatter size
if len(frontmatter) > 1024:
result["valid"] = False
result["errors"].append(
f"YAML frontmatter exceeds 1024 chars (found: {len(frontmatter)})"
)
except ValueError:
result["valid"] = False
result["errors"].append("Invalid YAML frontmatter format")
# Check total size
total_size = sum(f.stat().st_size for f in skill_dir.rglob("*") if f.is_file())
result["info"]["total_size_mb"] = total_size / (1024 * 1024)
if total_size > 8 * 1024 * 1024:
result["valid"] = False
result["errors"].append(
f"Total size exceeds 8MB (found: {total_size / (1024 * 1024):.2f} MB)"
)
# Count files
files = list(skill_dir.rglob("*"))
result["info"]["file_count"] = len([f for f in files if f.is_file()])
result["info"]["directory_count"] = len([f for f in files if f.is_dir()])
# Check for common files
if (skill_dir / "REFERENCE.md").exists():
result["info"]["has_reference"] = True
if (skill_dir / "scripts").exists():
result["info"]["has_scripts"] = True
result["info"]["script_files"] = [
f.name for f in (skill_dir / "scripts").iterdir() if f.is_file()
]
return result
def print_skill_summary(skill_info: dict[str, Any]) -> None:
"""
Print a formatted summary of a skill.
Args:
skill_info: Dictionary with skill information
"""
print(f"📦 Skill: {skill_info.get('display_title', 'Unknown')}")
print(f" ID: {skill_info.get('skill_id', 'N/A')}")
print(f" Version: {skill_info.get('latest_version', 'N/A')}")
print(f" Source: {skill_info.get('source', 'N/A')}")
print(f" Created: {skill_info.get('created_at', 'N/A')}")
if "error" in skill_info:
print(f" ❌ Error: {skill_info['error']}")