mirror of
https://github.com/NickCrew/Claude-Cortex.git
synced 2026-09-14 20:17:11 +08:00
399 lines
12 KiB
Python
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']}")
|