Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai> Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Paul Cacheux <paul.cacheux@mistral.ai> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Simon <80467011+sorgfresser@users.noreply.github.com> Co-authored-by: angelapopopo <angele.lenglemetz@mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
98 lines
2.9 KiB
Python
98 lines
2.9 KiB
Python
from __future__ import annotations
|
|
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
from pydantic import BaseModel, Field, field_validator
|
|
|
|
|
|
class SkillMetadata(BaseModel):
|
|
model_config = {"populate_by_name": True}
|
|
|
|
name: str = Field(
|
|
...,
|
|
min_length=1,
|
|
max_length=64,
|
|
pattern=r"^[a-z0-9]+(-[a-z0-9]+)*$",
|
|
description="Skill identifier. Lowercase letters, numbers, and hyphens only.",
|
|
)
|
|
description: str = Field(
|
|
...,
|
|
min_length=1,
|
|
max_length=1024,
|
|
description="What this skill does and when to use it.",
|
|
)
|
|
license: str | None = Field(
|
|
default=None, description="License name or reference to a bundled license file."
|
|
)
|
|
compatibility: str | None = Field(
|
|
default=None,
|
|
max_length=500,
|
|
description="Environment requirements (intended product, system packages, etc.).",
|
|
)
|
|
metadata: dict[str, str] = Field(
|
|
default_factory=dict,
|
|
description="Arbitrary key-value mapping for additional metadata.",
|
|
)
|
|
allowed_tools: list[str] = Field(
|
|
default_factory=list,
|
|
validation_alias="allowed-tools",
|
|
description="Space-delimited list of pre-approved tools (experimental).",
|
|
)
|
|
user_invocable: bool = Field(
|
|
default=True,
|
|
validation_alias="user-invocable",
|
|
description="Controls whether the skill appears in the slash command menu.",
|
|
)
|
|
|
|
@field_validator("allowed_tools", mode="before")
|
|
@classmethod
|
|
def parse_allowed_tools(cls, v: str | list[str] | None) -> list[str]:
|
|
if v is None:
|
|
return []
|
|
if isinstance(v, str):
|
|
return v.split()
|
|
return list(v)
|
|
|
|
@field_validator("metadata", mode="before")
|
|
@classmethod
|
|
def normalize_metadata(cls, v: dict[str, Any] | None) -> dict[str, str]:
|
|
if v is None:
|
|
return {}
|
|
return {str(k): str(val) for k, val in v.items()}
|
|
|
|
|
|
class SkillInfo(BaseModel):
|
|
name: str
|
|
description: str
|
|
license: str | None = None
|
|
compatibility: str | None = None
|
|
metadata: dict[str, str] = Field(default_factory=dict)
|
|
allowed_tools: list[str] = Field(default_factory=list)
|
|
user_invocable: bool = True
|
|
skill_path: Path
|
|
|
|
model_config = {"arbitrary_types_allowed": True}
|
|
|
|
@property
|
|
def skill_dir(self) -> Path:
|
|
return self.skill_path.parent.resolve()
|
|
|
|
@classmethod
|
|
def from_metadata(cls, meta: SkillMetadata, skill_path: Path) -> SkillInfo:
|
|
return cls(
|
|
name=meta.name,
|
|
description=meta.description,
|
|
license=meta.license,
|
|
compatibility=meta.compatibility,
|
|
metadata=meta.metadata,
|
|
allowed_tools=meta.allowed_tools,
|
|
user_invocable=meta.user_invocable,
|
|
skill_path=skill_path.resolve(),
|
|
)
|
|
|
|
|
|
class ParsedSkillCommand(BaseModel):
|
|
name: str
|
|
content: str
|
|
extra_instructions: str | None = None
|