mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-05-04 16:52:37 +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
|
import asyncio
|
||||||
from collections.abc import Callable, Coroutine, Sequence
|
from collections.abc import Callable, Coroutine, Sequence
|
||||||
|
from pathlib import Path
|
||||||
import shutil
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
import time
|
import time
|
||||||
@@ -26,6 +27,7 @@ from typing_extensions import Self
|
|||||||
|
|
||||||
from crewai.agent.utils import (
|
from crewai.agent.utils import (
|
||||||
ahandle_knowledge_retrieval,
|
ahandle_knowledge_retrieval,
|
||||||
|
append_skill_context,
|
||||||
apply_training_data,
|
apply_training_data,
|
||||||
build_task_prompt_with_schema,
|
build_task_prompt_with_schema,
|
||||||
format_task_with_context,
|
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.memory.contextual.contextual_memory import ContextualMemory
|
||||||
from crewai.rag.embeddings.types import EmbedderConfig
|
from crewai.rag.embeddings.types import EmbedderConfig
|
||||||
from crewai.security.fingerprint import Fingerprint
|
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.tools.agent_tools.agent_tools import AgentTools
|
||||||
from crewai.utilities.agent_utils import (
|
from crewai.utilities.agent_utils import (
|
||||||
get_tool_names,
|
get_tool_names,
|
||||||
@@ -310,6 +314,36 @@ class Agent(BaseAgent):
|
|||||||
except (TypeError, ValueError) as e:
|
except (TypeError, ValueError) as e:
|
||||||
raise ValueError(f"Invalid Knowledge Configuration: {e!s}") from 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:
|
def _is_any_available_memory(self) -> bool:
|
||||||
"""Check if any memory is available."""
|
"""Check if any memory is available."""
|
||||||
if not self.crew:
|
if not self.crew:
|
||||||
@@ -432,6 +466,8 @@ class Agent(BaseAgent):
|
|||||||
self.crew.query_knowledge if self.crew else lambda *a, **k: None,
|
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)
|
prepare_tools(self, tools, task)
|
||||||
task_prompt = apply_training_data(self, task_prompt)
|
task_prompt = apply_training_data(self, task_prompt)
|
||||||
|
|
||||||
@@ -666,6 +702,8 @@ class Agent(BaseAgent):
|
|||||||
self, task, task_prompt, knowledge_config
|
self, task, task_prompt, knowledge_config
|
||||||
)
|
)
|
||||||
|
|
||||||
|
task_prompt = append_skill_context(self, task_prompt)
|
||||||
|
|
||||||
prepare_tools(self, tools, task)
|
prepare_tools(self, tools, task)
|
||||||
task_prompt = apply_training_data(self, task_prompt)
|
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
|
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:
|
def apply_training_data(agent: Agent, task_prompt: str) -> str:
|
||||||
"""Apply training data to the task prompt.
|
"""Apply training data to the task prompt.
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from abc import ABC, abstractmethod
|
|||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from copy import copy as shallow_copy
|
from copy import copy as shallow_copy
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
|
from pathlib import Path
|
||||||
from typing import Any, Literal
|
from typing import Any, Literal
|
||||||
import uuid
|
import uuid
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
|
|||||||
from crewai.mcp.config import MCPServerConfig
|
from crewai.mcp.config import MCPServerConfig
|
||||||
from crewai.rag.embeddings.types import EmbedderConfig
|
from crewai.rag.embeddings.types import EmbedderConfig
|
||||||
from crewai.security.security_config import SecurityConfig
|
from crewai.security.security_config import SecurityConfig
|
||||||
|
from crewai.skills.models import Skill
|
||||||
from crewai.tools.base_tool import BaseTool, Tool
|
from crewai.tools.base_tool import BaseTool, Tool
|
||||||
from crewai.utilities.config import process_config
|
from crewai.utilities.config import process_config
|
||||||
from crewai.utilities.i18n import I18N, get_i18n
|
from crewai.utilities.i18n import I18N, get_i18n
|
||||||
@@ -199,6 +201,10 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
|
|||||||
default=None,
|
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.",
|
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")
|
@model_validator(mode="before")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -469,3 +475,6 @@ class BaseAgent(BaseModel, ABC, metaclass=AgentMeta):
|
|||||||
|
|
||||||
def set_knowledge(self, crew_embedder: EmbedderConfig | None = None) -> None:
|
def set_knowledge(self, crew_embedder: EmbedderConfig | None = None) -> None:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def set_skills(self) -> None:
|
||||||
|
pass
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ from concurrent.futures import Future
|
|||||||
from copy import copy as shallow_copy
|
from copy import copy as shallow_copy
|
||||||
from hashlib import md5
|
from hashlib import md5
|
||||||
import json
|
import json
|
||||||
|
from pathlib import Path
|
||||||
import re
|
import re
|
||||||
from typing import (
|
from typing import (
|
||||||
TYPE_CHECKING,
|
TYPE_CHECKING,
|
||||||
@@ -311,6 +312,10 @@ class Crew(FlowTrackable, BaseModel):
|
|||||||
default=None,
|
default=None,
|
||||||
description="Knowledge for the crew.",
|
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(
|
security_config: SecurityConfig = Field(
|
||||||
default_factory=SecurityConfig,
|
default_factory=SecurityConfig,
|
||||||
description="Security configuration for the crew, including fingerprinting.",
|
description="Security configuration for the crew, including fingerprinting.",
|
||||||
|
|||||||
@@ -70,6 +70,7 @@ def setup_agents(
|
|||||||
for agent in agents:
|
for agent in agents:
|
||||||
agent.crew = crew
|
agent.crew = crew
|
||||||
agent.set_knowledge(crew_embedder=embedder)
|
agent.set_knowledge(crew_embedder=embedder)
|
||||||
|
agent.set_skills()
|
||||||
if not agent.function_calling_llm: # type: ignore[attr-defined]
|
if not agent.function_calling_llm: # type: ignore[attr-defined]
|
||||||
agent.function_calling_llm = 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]
|
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