fix: Allow snake case for skill name
Kebab-case is the standard for skill names/directories given by the Skill spec. However, to enable support for importing within skill scripts (python does not allow imports with kebab-case, only snake case), allow snake case for the skill name. Co-authored-by: Kathy Wu <wukathy@google.com> PiperOrigin-RevId: 886351132 Change-Id: Ifb6c60603aaa00a399e4f5f14cd51c35737c25c1
This commit is contained in:
@@ -54,6 +54,7 @@ class FeatureName(str, Enum):
|
||||
TOOL_CONFIG = "TOOL_CONFIG"
|
||||
TOOL_CONFIRMATION = "TOOL_CONFIRMATION"
|
||||
PLUGGABLE_AUTH = "PLUGGABLE_AUTH"
|
||||
SNAKE_CASE_SKILL_NAME = "SNAKE_CASE_SKILL_NAME"
|
||||
V1_LLM_AGENT = "V1_LLM_AGENT"
|
||||
|
||||
|
||||
@@ -174,6 +175,9 @@ _FEATURE_REGISTRY: dict[FeatureName, FeatureConfig] = {
|
||||
FeatureName.PLUGGABLE_AUTH: FeatureConfig(
|
||||
FeatureStage.EXPERIMENTAL, default_on=True
|
||||
),
|
||||
FeatureName.SNAKE_CASE_SKILL_NAME: FeatureConfig(
|
||||
FeatureStage.EXPERIMENTAL, default_on=False
|
||||
),
|
||||
}
|
||||
|
||||
# Track which experimental features have already warned (warn only once)
|
||||
|
||||
@@ -26,14 +26,20 @@ from pydantic import ConfigDict
|
||||
from pydantic import Field
|
||||
from pydantic import field_validator
|
||||
|
||||
_NAME_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
|
||||
from ..features import FeatureName
|
||||
from ..features import is_feature_enabled
|
||||
|
||||
_KEBAB_NAME_PATTERN = re.compile(r"^[a-z0-9]+(-[a-z0-9]+)*$")
|
||||
_SNAKE_OR_KEBAB_NAME_PATTERN = re.compile(
|
||||
r"^([a-z0-9]+(-[a-z0-9]+)*|[a-z0-9]+(_[a-z0-9]+)*)$"
|
||||
)
|
||||
|
||||
|
||||
class Frontmatter(BaseModel):
|
||||
"""L1 skill content: metadata parsed from SKILL.md for skill discovery.
|
||||
|
||||
Attributes:
|
||||
name: Skill name in kebab-case (required).
|
||||
name: Skill name in kebab-case or snake_case (required).
|
||||
description: What the skill does and when the model should use it
|
||||
(required).
|
||||
license: License for the skill (optional).
|
||||
@@ -78,11 +84,23 @@ class Frontmatter(BaseModel):
|
||||
v = unicodedata.normalize("NFKC", v)
|
||||
if len(v) > 64:
|
||||
raise ValueError("name must be at most 64 characters")
|
||||
if not _NAME_PATTERN.match(v):
|
||||
raise ValueError(
|
||||
"name must be lowercase kebab-case (a-z, 0-9, hyphens),"
|
||||
" with no leading, trailing, or consecutive hyphens"
|
||||
if is_feature_enabled(FeatureName.SNAKE_CASE_SKILL_NAME):
|
||||
pattern = _SNAKE_OR_KEBAB_NAME_PATTERN
|
||||
msg = (
|
||||
"name must be lowercase kebab-case (a-z, 0-9, hyphens) or"
|
||||
" snake_case (a-z, 0-9, underscores), with no leading, trailing,"
|
||||
" or consecutive delimiters. Mixing hyphens and underscores is"
|
||||
" not allowed."
|
||||
)
|
||||
else:
|
||||
pattern = _KEBAB_NAME_PATTERN
|
||||
msg = (
|
||||
"name must be lowercase kebab-case (a-z, 0-9,"
|
||||
" hyphens), with no leading, trailing, or"
|
||||
" consecutive delimiters"
|
||||
)
|
||||
if not pattern.match(v):
|
||||
raise ValueError(msg)
|
||||
return v
|
||||
|
||||
@field_validator("description")
|
||||
|
||||
@@ -14,6 +14,8 @@
|
||||
|
||||
"""Unit tests for skill models."""
|
||||
|
||||
from google.adk.features import FeatureName
|
||||
from google.adk.features._feature_registry import temporary_feature_override
|
||||
from google.adk.skills import models
|
||||
from pydantic import ValidationError
|
||||
import pytest
|
||||
@@ -99,16 +101,39 @@ def test_name_consecutive_hyphens():
|
||||
models.Frontmatter(name="my--skill", description="desc")
|
||||
|
||||
|
||||
def test_name_invalid_chars_underscore():
|
||||
def test_name_underscore_rejected_by_default():
|
||||
with pytest.raises(ValidationError, match="lowercase kebab-case"):
|
||||
models.Frontmatter(name="my_skill", description="desc")
|
||||
|
||||
|
||||
def test_name_valid_underscore_preserved_with_flag():
|
||||
with temporary_feature_override(FeatureName.SNAKE_CASE_SKILL_NAME, True):
|
||||
fm = models.Frontmatter(name="my_skill", description="desc")
|
||||
assert fm.name == "my_skill"
|
||||
|
||||
|
||||
def test_name_invalid_chars_ampersand():
|
||||
with pytest.raises(ValidationError, match="lowercase kebab-case"):
|
||||
with pytest.raises(
|
||||
ValidationError, match="name must be lowercase kebab-case"
|
||||
):
|
||||
models.Frontmatter(name="skill&name", description="desc")
|
||||
|
||||
|
||||
def test_name_mixed_delimiters_rejected_by_default():
|
||||
with pytest.raises(
|
||||
ValidationError, match="name must be lowercase kebab-case"
|
||||
):
|
||||
models.Frontmatter(name="my-skill_1", description="desc")
|
||||
|
||||
|
||||
def test_name_mixed_delimiters_rejected_with_flag():
|
||||
with temporary_feature_override(FeatureName.SNAKE_CASE_SKILL_NAME, True):
|
||||
with pytest.raises(
|
||||
ValidationError, match="Mixing hyphens and underscores is not allowed"
|
||||
):
|
||||
models.Frontmatter(name="my-skill_1", description="desc")
|
||||
|
||||
|
||||
def test_name_valid_passes():
|
||||
fm = models.Frontmatter(name="my-skill-2", description="desc")
|
||||
assert fm.name == "my-skill-2"
|
||||
|
||||
Reference in New Issue
Block a user