mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-01 07:13:00 +00:00
chore: cleanup dead code, docs
This commit is contained in:
@@ -8,10 +8,18 @@ from __future__ import annotations
|
||||
|
||||
from enum import IntEnum
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from pydantic import BaseModel, Field, field_validator
|
||||
from pydantic import BaseModel, Field, model_validator
|
||||
|
||||
from crewai.skills.validation import validate_skill_name
|
||||
from crewai.skills.validation import (
|
||||
MAX_SKILL_NAME_LENGTH,
|
||||
MIN_SKILL_NAME_LENGTH,
|
||||
SKILL_NAME_PATTERN,
|
||||
)
|
||||
|
||||
|
||||
MAX_DESCRIPTION_LENGTH: int = 1024
|
||||
|
||||
|
||||
class DisclosureLevel(IntEnum):
|
||||
@@ -28,30 +36,41 @@ class DisclosureLevel(IntEnum):
|
||||
RESOURCES = 3
|
||||
|
||||
|
||||
class SkillFrontmatter(BaseModel, frozen=True):
|
||||
class SkillFrontmatter(BaseModel):
|
||||
"""YAML frontmatter from a SKILL.md file.
|
||||
|
||||
Attributes:
|
||||
name: Unique skill identifier (1-64 chars, lowercase alphanumeric + hyphens).
|
||||
description: Human-readable description of the skill.
|
||||
license: Optional SPDX license identifier.
|
||||
compatibility: Optional compatibility information.
|
||||
description: Human-readable description (1-1024 chars).
|
||||
license: Optional license name or reference.
|
||||
compatibility: Optional compatibility information (max 500 chars).
|
||||
metadata: Optional additional metadata as string key-value pairs.
|
||||
allowed_tools: Optional list of tools the skill may use.
|
||||
allowed_tools: Optional space-delimited list of pre-approved tools.
|
||||
"""
|
||||
|
||||
name: str
|
||||
description: str
|
||||
license: str | None = None
|
||||
compatibility: str | None = None
|
||||
metadata: dict[str, str] | None = None
|
||||
allowed_tools: list[str] | None = None
|
||||
model_config = {"frozen": True, "populate_by_name": True}
|
||||
|
||||
@field_validator("name")
|
||||
name: str = Field(
|
||||
min_length=MIN_SKILL_NAME_LENGTH,
|
||||
max_length=MAX_SKILL_NAME_LENGTH,
|
||||
pattern=SKILL_NAME_PATTERN,
|
||||
)
|
||||
description: str = Field(min_length=1, max_length=MAX_DESCRIPTION_LENGTH)
|
||||
license: str | None = None
|
||||
compatibility: str | None = Field(default=None, max_length=500)
|
||||
metadata: dict[str, str] | None = None
|
||||
allowed_tools: list[str] | None = Field(default=None, alias="allowed-tools")
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
def check_name(cls, v: str) -> str:
|
||||
"""Validate skill name against spec constraints."""
|
||||
return validate_skill_name(v)
|
||||
def parse_allowed_tools(cls, values: dict[str, Any]) -> dict[str, Any]:
|
||||
"""Parse space-delimited allowed-tools string into a list."""
|
||||
key = "allowed-tools"
|
||||
alt_key = "allowed_tools"
|
||||
raw = values.get(key) or values.get(alt_key)
|
||||
if isinstance(raw, str):
|
||||
values[key] = raw.split()
|
||||
return values
|
||||
|
||||
|
||||
class Skill(BaseModel):
|
||||
@@ -96,18 +115,6 @@ class Skill(BaseModel):
|
||||
"""Path to the assets directory."""
|
||||
return self.path / "assets"
|
||||
|
||||
def has_scripts(self) -> bool:
|
||||
"""Check if the skill has a scripts directory."""
|
||||
return self.scripts_dir.is_dir()
|
||||
|
||||
def has_references(self) -> bool:
|
||||
"""Check if the skill has a references directory."""
|
||||
return self.references_dir.is_dir()
|
||||
|
||||
def has_assets(self) -> bool:
|
||||
"""Check if the skill has an assets directory."""
|
||||
return self.assets_dir.is_dir()
|
||||
|
||||
def with_disclosure_level(
|
||||
self,
|
||||
level: DisclosureLevel,
|
||||
|
||||
@@ -146,8 +146,11 @@ def load_skill_resources(skill: Skill) -> Skill:
|
||||
skill = load_skill_instructions(skill)
|
||||
|
||||
resource_files: dict[str, list[str]] = {}
|
||||
for dir_name in ("scripts", "references", "assets"):
|
||||
resource_dir = skill.path / dir_name
|
||||
for dir_name, resource_dir in (
|
||||
("scripts", skill.scripts_dir),
|
||||
("references", skill.references_dir),
|
||||
("assets", skill.assets_dir),
|
||||
):
|
||||
if resource_dir.is_dir():
|
||||
resource_files[dir_name] = sorted(
|
||||
str(f.relative_to(resource_dir))
|
||||
|
||||
@@ -6,49 +6,11 @@ Validates skill names and directory structures per the Agent Skills standard.
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
|
||||
|
||||
MAX_SKILL_NAME_LENGTH: int = 64
|
||||
MIN_SKILL_NAME_LENGTH: int = 1
|
||||
|
||||
_SKILL_NAME_PATTERN: re.Pattern[str] = re.compile(r"^[a-z0-9]+(?:-[a-z0-9]+)*$")
|
||||
|
||||
|
||||
def validate_skill_name(name: str) -> str:
|
||||
"""Validate a skill name against the Agent Skills specification.
|
||||
|
||||
Names must be 1-64 characters, lowercase alphanumeric with hyphens,
|
||||
no leading/trailing hyphens, and no consecutive hyphens.
|
||||
|
||||
Args:
|
||||
name: The skill name to validate.
|
||||
|
||||
Returns:
|
||||
The validated skill name.
|
||||
|
||||
Raises:
|
||||
ValueError: If the name violates any constraint.
|
||||
"""
|
||||
if len(name) < MIN_SKILL_NAME_LENGTH:
|
||||
msg = "Skill name must not be empty"
|
||||
raise ValueError(msg)
|
||||
|
||||
if len(name) > MAX_SKILL_NAME_LENGTH:
|
||||
msg = (
|
||||
f"Skill name must be at most {MAX_SKILL_NAME_LENGTH} characters, "
|
||||
f"got {len(name)}"
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
if not _SKILL_NAME_PATTERN.match(name):
|
||||
msg = (
|
||||
f"Invalid skill name '{name}'. Names must be lowercase alphanumeric "
|
||||
f"with single hyphens, no leading/trailing hyphens."
|
||||
)
|
||||
raise ValueError(msg)
|
||||
|
||||
return name
|
||||
SKILL_NAME_PATTERN: str = r"^[a-z0-9]+(?:-[a-z0-9]+)*$"
|
||||
|
||||
|
||||
def validate_directory_name(skill_dir: Path, skill_name: str) -> None:
|
||||
|
||||
@@ -6,9 +6,7 @@ compatibility: crewai>=0.1.0
|
||||
metadata:
|
||||
author: test
|
||||
version: "1.0"
|
||||
allowed_tools:
|
||||
- web-search
|
||||
- file-read
|
||||
allowed-tools: web-search file-read
|
||||
---
|
||||
|
||||
## Instructions
|
||||
|
||||
@@ -73,27 +73,6 @@ class TestSkill:
|
||||
assert skill.references_dir == skill_dir / "references"
|
||||
assert skill.assets_dir == skill_dir / "assets"
|
||||
|
||||
def test_has_dirs_false(self, tmp_path: Path) -> None:
|
||||
skill_dir = tmp_path / "test-skill"
|
||||
skill_dir.mkdir()
|
||||
fm = SkillFrontmatter(name="test-skill", description="desc")
|
||||
skill = Skill(frontmatter=fm, path=skill_dir)
|
||||
assert not skill.has_scripts()
|
||||
assert not skill.has_references()
|
||||
assert not skill.has_assets()
|
||||
|
||||
def test_has_dirs_true(self, tmp_path: Path) -> None:
|
||||
skill_dir = tmp_path / "test-skill"
|
||||
skill_dir.mkdir()
|
||||
(skill_dir / "scripts").mkdir()
|
||||
(skill_dir / "references").mkdir()
|
||||
(skill_dir / "assets").mkdir()
|
||||
fm = SkillFrontmatter(name="test-skill", description="desc")
|
||||
skill = Skill(frontmatter=fm, path=skill_dir)
|
||||
assert skill.has_scripts()
|
||||
assert skill.has_references()
|
||||
assert skill.has_assets()
|
||||
|
||||
def test_with_disclosure_level(self, tmp_path: Path) -> None:
|
||||
fm = SkillFrontmatter(name="test-skill", description="desc")
|
||||
skill = Skill(frontmatter=fm, path=tmp_path)
|
||||
|
||||
@@ -1,77 +1,81 @@
|
||||
"""Tests for skills/validation.py."""
|
||||
"""Tests for skills validation."""
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from crewai.skills.models import SkillFrontmatter
|
||||
from crewai.skills.validation import (
|
||||
MAX_SKILL_NAME_LENGTH,
|
||||
validate_directory_name,
|
||||
validate_skill_name,
|
||||
)
|
||||
|
||||
|
||||
class TestValidateSkillName:
|
||||
"""Tests for validate_skill_name."""
|
||||
def _make(name: str) -> SkillFrontmatter:
|
||||
"""Create a SkillFrontmatter with the given name."""
|
||||
return SkillFrontmatter(name=name, description="desc")
|
||||
|
||||
|
||||
class TestSkillNameValidation:
|
||||
"""Tests for skill name constraints via SkillFrontmatter."""
|
||||
|
||||
def test_simple_name(self) -> None:
|
||||
assert validate_skill_name("web-search") == "web-search"
|
||||
assert _make("web-search").name == "web-search"
|
||||
|
||||
def test_single_word(self) -> None:
|
||||
assert validate_skill_name("search") == "search"
|
||||
assert _make("search").name == "search"
|
||||
|
||||
def test_numeric(self) -> None:
|
||||
assert validate_skill_name("tool3") == "tool3"
|
||||
assert _make("tool3").name == "tool3"
|
||||
|
||||
def test_all_digits(self) -> None:
|
||||
assert validate_skill_name("123") == "123"
|
||||
assert _make("123").name == "123"
|
||||
|
||||
def test_single_char(self) -> None:
|
||||
assert validate_skill_name("a") == "a"
|
||||
assert _make("a").name == "a"
|
||||
|
||||
def test_max_length(self) -> None:
|
||||
name = "a" * MAX_SKILL_NAME_LENGTH
|
||||
assert validate_skill_name(name) == name
|
||||
assert _make(name).name == name
|
||||
|
||||
def test_multi_hyphen_segments(self) -> None:
|
||||
assert validate_skill_name("my-cool-skill") == "my-cool-skill"
|
||||
assert _make("my-cool-skill").name == "my-cool-skill"
|
||||
|
||||
def test_empty_raises(self) -> None:
|
||||
with pytest.raises(ValueError, match="must not be empty"):
|
||||
validate_skill_name("")
|
||||
with pytest.raises(ValueError):
|
||||
_make("")
|
||||
|
||||
def test_too_long_raises(self) -> None:
|
||||
name = "a" * (MAX_SKILL_NAME_LENGTH + 1)
|
||||
with pytest.raises(ValueError, match="at most"):
|
||||
validate_skill_name(name)
|
||||
with pytest.raises(ValueError):
|
||||
_make("a" * (MAX_SKILL_NAME_LENGTH + 1))
|
||||
|
||||
def test_uppercase_raises(self) -> None:
|
||||
with pytest.raises(ValueError, match="Invalid skill name"):
|
||||
validate_skill_name("MySkill")
|
||||
with pytest.raises(ValueError):
|
||||
_make("MySkill")
|
||||
|
||||
def test_leading_hyphen_raises(self) -> None:
|
||||
with pytest.raises(ValueError, match="Invalid skill name"):
|
||||
validate_skill_name("-skill")
|
||||
with pytest.raises(ValueError):
|
||||
_make("-skill")
|
||||
|
||||
def test_trailing_hyphen_raises(self) -> None:
|
||||
with pytest.raises(ValueError, match="Invalid skill name"):
|
||||
validate_skill_name("skill-")
|
||||
with pytest.raises(ValueError):
|
||||
_make("skill-")
|
||||
|
||||
def test_consecutive_hyphens_raises(self) -> None:
|
||||
with pytest.raises(ValueError, match="Invalid skill name"):
|
||||
validate_skill_name("my--skill")
|
||||
with pytest.raises(ValueError):
|
||||
_make("my--skill")
|
||||
|
||||
def test_underscore_raises(self) -> None:
|
||||
with pytest.raises(ValueError, match="Invalid skill name"):
|
||||
validate_skill_name("my_skill")
|
||||
with pytest.raises(ValueError):
|
||||
_make("my_skill")
|
||||
|
||||
def test_space_raises(self) -> None:
|
||||
with pytest.raises(ValueError, match="Invalid skill name"):
|
||||
validate_skill_name("my skill")
|
||||
with pytest.raises(ValueError):
|
||||
_make("my skill")
|
||||
|
||||
def test_special_chars_raises(self) -> None:
|
||||
with pytest.raises(ValueError, match="Invalid skill name"):
|
||||
validate_skill_name("skill@v1")
|
||||
with pytest.raises(ValueError):
|
||||
_make("skill@v1")
|
||||
|
||||
|
||||
class TestValidateDirectoryName:
|
||||
|
||||
Reference in New Issue
Block a user