mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-04-07 03:28:29 +00:00
feat: integrate skill discovery and activation into agent execution
This commit is contained in:
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from collections.abc import Callable, Coroutine, Sequence
|
||||
from pathlib import Path
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
@@ -26,6 +27,7 @@ from typing_extensions import Self
|
||||
|
||||
from crewai.agent.utils import (
|
||||
ahandle_knowledge_retrieval,
|
||||
append_skill_context,
|
||||
apply_training_data,
|
||||
build_task_prompt_with_schema,
|
||||
format_task_with_context,
|
||||
@@ -74,6 +76,8 @@ from crewai.mcp.transports.stdio import StdioTransport
|
||||
from crewai.memory.contextual.contextual_memory import ContextualMemory
|
||||
from crewai.rag.embeddings.types import EmbedderConfig
|
||||
from crewai.security.fingerprint import Fingerprint
|
||||
from crewai.skills.loader import activate_skill, discover_skills
|
||||
from crewai.skills.models import Skill as SkillModel
|
||||
from crewai.tools.agent_tools.agent_tools import AgentTools
|
||||
from crewai.utilities.agent_utils import (
|
||||
get_tool_names,
|
||||
@@ -310,6 +314,36 @@ class Agent(BaseAgent):
|
||||
except (TypeError, ValueError) as e:
|
||||
raise ValueError(f"Invalid Knowledge Configuration: {e!s}") from e
|
||||
|
||||
def set_skills(self) -> None:
|
||||
"""Resolve skill paths into loaded Skill objects.
|
||||
|
||||
Path entries trigger discovery and activation. Skill entries pass through.
|
||||
Crew-level skill paths are merged in.
|
||||
"""
|
||||
crew_skills: list[Path] | None = (
|
||||
self.crew.skills
|
||||
if self.crew and isinstance(self.crew.skills, list)
|
||||
else None
|
||||
)
|
||||
|
||||
if not self.skills and not crew_skills:
|
||||
return
|
||||
|
||||
resolved: list[Path | SkillModel] = []
|
||||
items: list[Path | SkillModel] = list(self.skills) if self.skills else []
|
||||
|
||||
if crew_skills:
|
||||
items.extend(crew_skills)
|
||||
|
||||
for item in items:
|
||||
if isinstance(item, Path):
|
||||
discovered = discover_skills(item, source=self)
|
||||
resolved.extend(activate_skill(s, source=self) for s in discovered)
|
||||
elif isinstance(item, SkillModel):
|
||||
resolved.append(item)
|
||||
|
||||
self.skills = resolved if resolved else None
|
||||
|
||||
def _is_any_available_memory(self) -> bool:
|
||||
"""Check if any memory is available."""
|
||||
if not self.crew:
|
||||
@@ -432,6 +466,8 @@ class Agent(BaseAgent):
|
||||
self.crew.query_knowledge if self.crew else lambda *a, **k: None,
|
||||
)
|
||||
|
||||
task_prompt = append_skill_context(self, task_prompt)
|
||||
|
||||
prepare_tools(self, tools, task)
|
||||
task_prompt = apply_training_data(self, task_prompt)
|
||||
|
||||
@@ -666,6 +702,8 @@ class Agent(BaseAgent):
|
||||
self, task, task_prompt, knowledge_config
|
||||
)
|
||||
|
||||
task_prompt = append_skill_context(self, task_prompt)
|
||||
|
||||
prepare_tools(self, tools, task)
|
||||
task_prompt = apply_training_data(self, task_prompt)
|
||||
|
||||
|
||||
@@ -203,6 +203,30 @@ def _combine_knowledge_context(agent: Agent) -> str:
|
||||
return agent_ctx + separator + crew_ctx
|
||||
|
||||
|
||||
def append_skill_context(agent: Agent, task_prompt: str) -> str:
|
||||
"""Append activated skill context sections to the task prompt.
|
||||
|
||||
Args:
|
||||
agent: The agent with optional skills.
|
||||
task_prompt: The current task prompt.
|
||||
|
||||
Returns:
|
||||
The task prompt with skill context appended.
|
||||
"""
|
||||
if not agent.skills:
|
||||
return task_prompt
|
||||
|
||||
from crewai.skills.loader import format_skill_context
|
||||
from crewai.skills.models import Skill
|
||||
|
||||
skill_sections = [
|
||||
format_skill_context(s) for s in agent.skills if isinstance(s, Skill)
|
||||
]
|
||||
if skill_sections:
|
||||
task_prompt += "\n\n" + "\n\n".join(skill_sections)
|
||||
return task_prompt
|
||||
|
||||
|
||||
def apply_training_data(agent: Agent, task_prompt: str) -> str:
|
||||
"""Apply training data to the task prompt.
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ from abc import ABC, abstractmethod
|
||||
from collections.abc import Callable
|
||||
from copy import copy as shallow_copy
|
||||
from hashlib import md5
|
||||
from pathlib import Path
|
||||
from typing import Any, Literal
|
||||
import uuid
|
||||
|
||||
@@ -28,6 +29,7 @@ from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
|
||||
from crewai.mcp.config import MCPServerConfig
|
||||
from crewai.rag.embeddings.types import EmbedderConfig
|
||||
from crewai.security.security_config import SecurityConfig
|
||||
from crewai.skills.models import Skill
|
||||
from crewai.tools.base_tool import BaseTool, Tool
|
||||
from crewai.utilities.config import process_config
|
||||
from crewai.utilities.i18n import I18N, get_i18n
|
||||
@@ -199,6 +201,10 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
|
||||
default=None,
|
||||
description="List of MCP server references. Supports 'https://server.com/path' for external servers and 'crewai-amp:mcp-name' for AMP marketplace. Use '#tool_name' suffix for specific tools.",
|
||||
)
|
||||
skills: list[Path | Skill] | None = Field(
|
||||
default=None,
|
||||
description="Agent Skills. Accepts Paths for discovery or pre-loaded Skill objects.",
|
||||
)
|
||||
|
||||
@model_validator(mode="before")
|
||||
@classmethod
|
||||
@@ -469,3 +475,6 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
|
||||
|
||||
def set_knowledge(self, crew_embedder: EmbedderConfig | None = None) -> None:
|
||||
pass
|
||||
|
||||
def set_skills(self) -> None:
|
||||
pass
|
||||
|
||||
@@ -6,6 +6,7 @@ from concurrent.futures import Future
|
||||
from copy import copy as shallow_copy
|
||||
from hashlib import md5
|
||||
import json
|
||||
from pathlib import Path
|
||||
import re
|
||||
from typing import (
|
||||
TYPE_CHECKING,
|
||||
@@ -311,6 +312,10 @@ class Crew(FlowTrackable, BaseModel):
|
||||
default=None,
|
||||
description="Knowledge for the crew.",
|
||||
)
|
||||
skills: list[Path] | None = Field(
|
||||
default=None,
|
||||
description="Skill search paths applied to all agents in the crew.",
|
||||
)
|
||||
security_config: SecurityConfig = Field(
|
||||
default_factory=SecurityConfig,
|
||||
description="Security configuration for the crew, including fingerprinting.",
|
||||
|
||||
@@ -70,6 +70,7 @@ def setup_agents(
|
||||
for agent in agents:
|
||||
agent.crew = crew
|
||||
agent.set_knowledge(crew_embedder=embedder)
|
||||
agent.set_skills()
|
||||
if not agent.function_calling_llm: # type: ignore[attr-defined]
|
||||
agent.function_calling_llm = function_calling_llm # type: ignore[attr-defined]
|
||||
if not agent.step_callback: # type: ignore[attr-defined]
|
||||
|
||||
170
lib/crewai/src/crewai/skills/loader.py
Normal file
170
lib/crewai/src/crewai/skills/loader.py
Normal file
@@ -0,0 +1,170 @@
|
||||
"""Filesystem discovery and progressive loading for Agent Skills.
|
||||
|
||||
Provides functions to discover skills in directories, activate them
|
||||
for agent use, and format skill context for prompt injection.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from typing import Any
|
||||
|
||||
from crewai.events.event_bus import crewai_event_bus
|
||||
from crewai.events.types.skill_events import (
|
||||
SkillActivatedEvent,
|
||||
SkillDiscoveryCompletedEvent,
|
||||
SkillDiscoveryStartedEvent,
|
||||
SkillLoadFailedEvent,
|
||||
SkillLoadedEvent,
|
||||
)
|
||||
from crewai.skills.models import DisclosureLevel, Skill
|
||||
from crewai.skills.parser import (
|
||||
SKILL_FILENAME,
|
||||
load_skill_instructions,
|
||||
load_skill_metadata,
|
||||
load_skill_resources,
|
||||
)
|
||||
|
||||
|
||||
def discover_skills(
|
||||
search_path: Path,
|
||||
source: Any | None = None,
|
||||
) -> list[Skill]:
|
||||
"""Scan a directory for skill directories containing SKILL.md.
|
||||
|
||||
Loads each discovered skill at METADATA disclosure level.
|
||||
|
||||
Args:
|
||||
search_path: Directory to scan for skill subdirectories.
|
||||
source: Optional event source (agent or crew) for event emission.
|
||||
|
||||
Returns:
|
||||
List of Skill instances at METADATA level.
|
||||
"""
|
||||
skills: list[Skill] = []
|
||||
|
||||
if not search_path.is_dir():
|
||||
return skills
|
||||
|
||||
if source is not None:
|
||||
crewai_event_bus.emit(
|
||||
source,
|
||||
event=SkillDiscoveryStartedEvent(
|
||||
search_path=str(search_path),
|
||||
),
|
||||
)
|
||||
|
||||
for child in sorted(search_path.iterdir()):
|
||||
if not child.is_dir():
|
||||
continue
|
||||
skill_md = child / SKILL_FILENAME
|
||||
if not skill_md.is_file():
|
||||
continue
|
||||
try:
|
||||
skill = load_skill_metadata(child)
|
||||
skills.append(skill)
|
||||
if source is not None:
|
||||
crewai_event_bus.emit(
|
||||
source,
|
||||
event=SkillLoadedEvent(
|
||||
skill_name=skill.name,
|
||||
skill_path=str(skill.path),
|
||||
disclosure_level=skill.disclosure_level.value,
|
||||
),
|
||||
)
|
||||
except Exception as e:
|
||||
if source is not None:
|
||||
crewai_event_bus.emit(
|
||||
source,
|
||||
event=SkillLoadFailedEvent(
|
||||
skill_name=child.name,
|
||||
skill_path=str(child),
|
||||
error=str(e),
|
||||
),
|
||||
)
|
||||
|
||||
if source is not None:
|
||||
crewai_event_bus.emit(
|
||||
source,
|
||||
event=SkillDiscoveryCompletedEvent(
|
||||
search_path=str(search_path),
|
||||
skills_found=len(skills),
|
||||
skill_names=[s.name for s in skills],
|
||||
),
|
||||
)
|
||||
|
||||
return skills
|
||||
|
||||
|
||||
def activate_skill(
|
||||
skill: Skill,
|
||||
source: Any | None = None,
|
||||
) -> Skill:
|
||||
"""Promote a skill to INSTRUCTIONS disclosure level.
|
||||
|
||||
Idempotent: returns the skill unchanged if already at or above INSTRUCTIONS.
|
||||
|
||||
Args:
|
||||
skill: Skill to activate.
|
||||
source: Optional event source for event emission.
|
||||
|
||||
Returns:
|
||||
Skill at INSTRUCTIONS level or higher.
|
||||
"""
|
||||
if skill.disclosure_level >= DisclosureLevel.INSTRUCTIONS:
|
||||
return skill
|
||||
|
||||
activated = load_skill_instructions(skill)
|
||||
|
||||
if source is not None:
|
||||
crewai_event_bus.emit(
|
||||
source,
|
||||
event=SkillActivatedEvent(
|
||||
skill_name=activated.name,
|
||||
skill_path=str(activated.path),
|
||||
disclosure_level=activated.disclosure_level.value,
|
||||
),
|
||||
)
|
||||
|
||||
return activated
|
||||
|
||||
|
||||
def load_resources(skill: Skill) -> Skill:
|
||||
"""Promote a skill to RESOURCES disclosure level.
|
||||
|
||||
Args:
|
||||
skill: Skill to promote.
|
||||
|
||||
Returns:
|
||||
Skill at RESOURCES level.
|
||||
"""
|
||||
return load_skill_resources(skill)
|
||||
|
||||
|
||||
def format_skill_context(skill: Skill) -> str:
|
||||
"""Format skill information for agent prompt injection.
|
||||
|
||||
At METADATA level: returns name and description only.
|
||||
At INSTRUCTIONS level or above: returns full SKILL.md body.
|
||||
|
||||
Args:
|
||||
skill: The skill to format.
|
||||
|
||||
Returns:
|
||||
Formatted skill context string.
|
||||
"""
|
||||
if skill.disclosure_level >= DisclosureLevel.INSTRUCTIONS and skill.instructions:
|
||||
parts = [
|
||||
f"## Skill: {skill.name}",
|
||||
skill.description,
|
||||
"",
|
||||
skill.instructions,
|
||||
]
|
||||
if skill.disclosure_level >= DisclosureLevel.RESOURCES and skill.resource_files:
|
||||
parts.append("")
|
||||
parts.append("### Available Resources")
|
||||
for dir_name, files in sorted(skill.resource_files.items()):
|
||||
if files:
|
||||
parts.append(f"- **{dir_name}/**: {', '.join(files)}")
|
||||
return "\n".join(parts)
|
||||
return f"## Skill: {skill.name}\n{skill.description}"
|
||||
Reference in New Issue
Block a user