v2.6.0 (#524)
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai> Co-authored-by: Gauthier Guinet <43207538+Gguinet@users.noreply.github.com> Co-authored-by: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Quentin <torroba.q@gmail.com> Co-authored-by: Simon <80467011+sorgfresser@users.noreply.github.com> Co-authored-by: Simon Van de Kerckhove <simon.vandekerckhove@mistral.ai> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: angelapopopo <angele.lenglemetz@mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
5103019b01
commit
eb580209d4
180 changed files with 11136 additions and 1030 deletions
139
vibe/core/tools/builtins/skill.py
Normal file
139
vibe/core/tools/builtins/skill.py
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from typing import ClassVar
|
||||
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
from vibe.core.skills.parser import SkillParseError, parse_frontmatter
|
||||
from vibe.core.tools.base import (
|
||||
BaseTool,
|
||||
BaseToolConfig,
|
||||
BaseToolState,
|
||||
InvokeContext,
|
||||
ToolError,
|
||||
ToolPermission,
|
||||
)
|
||||
from vibe.core.tools.permissions import (
|
||||
PermissionContext,
|
||||
PermissionScope,
|
||||
RequiredPermission,
|
||||
)
|
||||
from vibe.core.tools.ui import ToolCallDisplay, ToolResultDisplay, ToolUIData
|
||||
from vibe.core.types import ToolResultEvent, ToolStreamEvent
|
||||
from vibe.core.utils.io import read_safe
|
||||
|
||||
_MAX_LISTED_FILES = 10
|
||||
|
||||
|
||||
class SkillArgs(BaseModel):
|
||||
name: str = Field(description="The name of the skill to load from available_skills")
|
||||
|
||||
|
||||
class SkillResult(BaseModel):
|
||||
name: str = Field(description="The name of the loaded skill")
|
||||
content: str = Field(description="The full skill content block")
|
||||
skill_dir: str = Field(description="Absolute path to the skill directory")
|
||||
|
||||
|
||||
class SkillToolConfig(BaseToolConfig):
|
||||
permission: ToolPermission = ToolPermission.ASK
|
||||
|
||||
|
||||
class Skill(
|
||||
BaseTool[SkillArgs, SkillResult, SkillToolConfig, BaseToolState],
|
||||
ToolUIData[SkillArgs, SkillResult],
|
||||
):
|
||||
description: ClassVar[str] = (
|
||||
"Load a specialized skill that provides domain-specific instructions and workflows. "
|
||||
"When you recognize that a task matches one of the available skills listed in your system prompt, "
|
||||
"use this tool to load the full skill instructions. "
|
||||
"The skill will inject detailed instructions, workflows, and access to bundled resources "
|
||||
"(scripts, references, templates) into the conversation context."
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def format_call_display(cls, args: SkillArgs) -> ToolCallDisplay:
|
||||
return ToolCallDisplay(summary=f"Loading skill: {args.name}")
|
||||
|
||||
@classmethod
|
||||
def get_result_display(cls, event: ToolResultEvent) -> ToolResultDisplay:
|
||||
if event.error:
|
||||
return ToolResultDisplay(success=False, message=event.error)
|
||||
if not isinstance(event.result, SkillResult):
|
||||
return ToolResultDisplay(success=True, message="Skill loaded")
|
||||
return ToolResultDisplay(
|
||||
success=True, message=f"Loaded skill: {event.result.name}"
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_status_text(cls) -> str:
|
||||
return "Loading skill"
|
||||
|
||||
def resolve_permission(self, args: SkillArgs) -> PermissionContext | None:
|
||||
return PermissionContext(
|
||||
permission=self.config.permission,
|
||||
required_permissions=[
|
||||
RequiredPermission(
|
||||
scope=PermissionScope.FILE_PATTERN,
|
||||
invocation_pattern=args.name,
|
||||
session_pattern=args.name,
|
||||
label=f"Load skill: {args.name}",
|
||||
)
|
||||
],
|
||||
)
|
||||
|
||||
async def run(
|
||||
self, args: SkillArgs, ctx: InvokeContext | None = None
|
||||
) -> AsyncGenerator[ToolStreamEvent | SkillResult, None]:
|
||||
if ctx is None or ctx.skill_manager is None:
|
||||
raise ToolError("Skill manager not available")
|
||||
|
||||
skill_manager = ctx.skill_manager
|
||||
skill_info = skill_manager.get_skill(args.name)
|
||||
|
||||
if skill_info is None:
|
||||
available = ", ".join(sorted(skill_manager.available_skills.keys()))
|
||||
raise ToolError(
|
||||
f'Skill "{args.name}" not found. Available skills: {available or "none"}'
|
||||
)
|
||||
|
||||
try:
|
||||
raw = read_safe(skill_info.skill_path)
|
||||
_, body = parse_frontmatter(raw)
|
||||
except (OSError, SkillParseError) as e:
|
||||
raise ToolError(f"Cannot load skill file: {e}") from e
|
||||
|
||||
skill_dir = skill_info.skill_dir
|
||||
files: list[str] = []
|
||||
try:
|
||||
for entry in sorted(skill_dir.rglob("*")):
|
||||
if not entry.is_file():
|
||||
continue
|
||||
if entry.name == "SKILL.md":
|
||||
continue
|
||||
files.append(str(entry.relative_to(skill_dir)))
|
||||
if len(files) >= _MAX_LISTED_FILES:
|
||||
break
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
file_lines = "\n".join(f"<file>{f}</file>" for f in files)
|
||||
|
||||
output = "\n".join([
|
||||
f'<skill_content name="{args.name}">',
|
||||
f"# Skill: {args.name}",
|
||||
"",
|
||||
body.strip(),
|
||||
"",
|
||||
f"Base directory for this skill: {skill_dir}",
|
||||
"Relative paths in this skill are relative to this base directory.",
|
||||
"Note: file list is sampled.",
|
||||
"",
|
||||
"<skill_files>",
|
||||
file_lines,
|
||||
"</skill_files>",
|
||||
"</skill_content>",
|
||||
])
|
||||
|
||||
yield SkillResult(name=args.name, content=output, skill_dir=str(skill_dir))
|
||||
Loading…
Add table
Add a link
Reference in a new issue