Compare commits

..

1 Commits

Author SHA1 Message Date
Jesse Miller
4dae956725 Add support for skills 2026-02-04 07:43:52 -08:00
8 changed files with 200 additions and 1 deletions

View File

@@ -25,6 +25,35 @@ For file-based Knowledge Sources, make sure to place your files in a `knowledge`
Also, use relative paths from the `knowledge` directory when creating the source.
</Tip>
### Skills.md for Crews (CrewBase)
When using the `@CrewBase` decorator and project layout (e.g. from `crewai create crew`), you can add **Skills.md** files as crew-level knowledge sources. Place one markdown file per skill under:
```
src/<project>/.agents/<skill_name>/Skills.md
```
Each `Skills.md` is loaded as a knowledge source so the crew can query it via RAG at runtime. Use `get_skills_knowledge_sources()` when building your crew:
```python
from crewai.project import CrewBase, agent, crew, task
@CrewBase
class MyCrew():
# ...
@crew
def crew(self) -> Crew:
return Crew(
agents=self.agents,
tasks=self.tasks,
process=Process.sequential,
knowledge_sources=self.get_skills_knowledge_sources(),
)
```
You can combine skills with other knowledge sources: `knowledge_sources=[*self.get_skills_knowledge_sources(), other_source]`. If the `.agents` directory is missing or has no `Skills.md` files, `get_skills_knowledge_sources()` returns an empty list. To use a different directory than `.agents`, set the class attribute `skills_directory = "my_skills"`. Skills.md support requires the `docling` package (`uv add docling`).
### Vector store (RAG) client configuration
CrewAI exposes a provider-neutral RAG client abstraction for vector stores. The default provider is ChromaDB, and Qdrant is supported as well. You can switch providers using configuration utilities.

View File

@@ -110,6 +110,9 @@ def create_folder_structure(name, parent_folder=None):
(folder_path / "src" / folder_name).mkdir(parents=True)
(folder_path / "src" / folder_name / "tools").mkdir(parents=True)
(folder_path / "src" / folder_name / "config").mkdir(parents=True)
(folder_path / "src" / folder_name / ".agents" / "research").mkdir(
parents=True
)
return folder_path, folder_name, class_name
@@ -154,6 +157,13 @@ def copy_template_files(folder_path, name, class_name, parent_folder):
dst_file = src_folder / file_name
copy_template(src_file, dst_file, name, class_name, folder_path.name)
# Copy Skills.md from .agents/research template
skills_src = templates_dir / ".agents" / "research" / "Skills.md"
skills_dst = src_folder / ".agents" / "research" / "Skills.md"
if skills_src.exists():
skills_dst.parent.mkdir(parents=True, exist_ok=True)
copy_template(skills_src, skills_dst, name, class_name, folder_path.name)
def create_crew(name, provider=None, skip_provider=False, parent_folder=None):
folder_path, folder_name, class_name = create_folder_structure(name, parent_folder)

View File

@@ -54,11 +54,18 @@ class {{crew_name}}():
"""Creates the {{crew_name}} crew"""
# To learn how to add knowledge sources to your crew, check out the documentation:
# https://docs.crewai.com/concepts/knowledge#what-is-knowledge
# Skills.md files under .agents/<skill_name>/ are loaded via get_skills_knowledge_sources()
skills_sources = (
self.get_skills_knowledge_sources()
if hasattr(self, "get_skills_knowledge_sources")
else []
)
return Crew(
agents=self.agents, # Automatically created by the @agent decorator
tasks=self.tasks, # Automatically created by the @task decorator
process=Process.sequential,
verbose=True,
knowledge_sources=skills_sources,
# process=Process.hierarchical, # In case you wanna use that instead https://docs.crewai.com/how-to/Hierarchical/
)

View File

@@ -178,6 +178,57 @@ def _set_mcp_params(cls: type[CrewClass]) -> None:
cls.mcp_connect_timeout = getattr(cls, "mcp_connect_timeout", 30)
def _set_skills_params(cls: type[CrewClass]) -> None:
"""Set the skills directory path for the crew class.
Args:
cls: Crew class to configure.
"""
cls.skills_directory = getattr(cls, "skills_directory", ".agents")
def get_skills_knowledge_sources(self: CrewInstance) -> list[Any]:
"""Discover Skills.md files under .agents/<skill_name>/ and return them as knowledge sources.
Looks for src/<project>/.agents/<skill_name>/Skills.md (relative to the crew class
base_directory). Each found file is wrapped in a CrewDoclingSource so the crew can
query it via RAG. Requires the docling package; if not installed, returns an empty list.
Returns:
List of knowledge sources (CrewDoclingSource instances), or empty list if
.agents is missing, has no Skills.md files, or docling is not installed.
"""
skills_dir_name = getattr(self, "skills_directory", ".agents")
skills_dir = self.base_directory / skills_dir_name
if not skills_dir.exists() or not skills_dir.is_dir():
return []
try:
from crewai.knowledge.source.base_knowledge_source import BaseKnowledgeSource
from crewai.knowledge.source.crew_docling_source import CrewDoclingSource
except ImportError:
logging.warning(
"Skills.md support requires the docling package. "
"Install it with: uv add docling"
)
return []
sources: list[Any] = []
for subdir in sorted(skills_dir.iterdir()):
if not subdir.is_dir():
continue
skills_md = subdir / "Skills.md"
if skills_md.exists():
try:
source = CrewDoclingSource(file_paths=[skills_md])
sources.append(source)
except Exception as e:
logging.warning(
f"Could not create knowledge source for {skills_md}: {e}"
)
return sources
def _is_string_list(value: list[str] | list[BaseTool]) -> TypeGuard[list[str]]:
"""Type guard to check if list contains strings rather than BaseTool instances.
@@ -731,6 +782,7 @@ _CLASS_SETUP_FUNCTIONS: tuple[Callable[[type[CrewClass]], None], ...] = (
_set_base_directory,
_set_config_paths,
_set_mcp_params,
_set_skills_params,
)
_METHODS_TO_INJECT = (
@@ -739,6 +791,7 @@ _METHODS_TO_INJECT = (
_load_config,
load_configurations,
staticmethod(load_yaml),
get_skills_knowledge_sources,
map_all_agent_variables,
_map_agent_variables,
map_all_task_variables,

View File

@@ -81,8 +81,10 @@ class CrewInstance(Protocol):
tasks_config: dict[str, Any]
mcp_server_params: Any
mcp_connect_timeout: int
skills_directory: str
def load_configurations(self) -> None: ...
def get_skills_knowledge_sources(self) -> list[Any]: ...
def map_all_agent_variables(self) -> None: ...
def map_all_task_variables(self) -> None: ...
def close_mcp_server(self, instance: Self, outputs: CrewOutput) -> CrewOutput: ...
@@ -122,8 +124,10 @@ class CrewClass(Protocol):
original_tasks_config_path: str
mcp_server_params: Any
mcp_connect_timeout: int
skills_directory: str
close_mcp_server: Callable[..., Any]
get_mcp_tools: Callable[..., list[BaseTool]]
get_skills_knowledge_sources: Callable[..., list[Any]]
_load_config: Callable[..., dict[str, Any]]
load_configurations: Callable[..., None]
load_yaml: Callable[..., dict[str, Any]]

View File

@@ -148,7 +148,10 @@ def _llm_via_environment_or_fallback() -> LLM | None:
"AWS_SECRET_ACCESS_KEY",
"AWS_REGION_NAME",
]
set_provider = model_name.partition("/")[0] if "/" in model_name else "openai"
if "/" in model_name:
set_provider = model_name.partition("/")[0]
else:
set_provider = LLM._infer_provider_from_model(model_name)
if set_provider in ENV_VARS:
env_vars_for_provider = ENV_VARS[set_provider]

View File

@@ -1,3 +1,5 @@
import tempfile
from pathlib import Path
from typing import Any, ClassVar
from unittest.mock import Mock, patch
@@ -382,3 +384,93 @@ def test_internal_crew_with_mcp():
adapter_mock.assert_called_once_with(
{"host": "localhost", "port": 8000}, connect_timeout=120
)
def test_get_skills_knowledge_sources_discovery():
"""get_skills_knowledge_sources discovers .agents/<skill_name>/Skills.md and returns sources."""
@CrewBase
class SkillsCrew:
agents_config = "nonexistent/agents.yaml"
tasks_config = "nonexistent/tasks.yaml"
agents: list[BaseAgent]
tasks: list[Task]
@agent
def researcher(self):
return Agent(
role="Researcher",
goal="Research",
backstory="Expert researcher",
)
@task
def research_task(self):
return Task(
description="Research", expected_output="Report", agent=self.researcher()
)
@crew
def crew(self):
return Crew(agents=self.agents, tasks=self.tasks, verbose=True)
with tempfile.TemporaryDirectory() as tmp:
base = Path(tmp)
(base / ".agents" / "skill_a").mkdir(parents=True)
(base / ".agents" / "skill_b").mkdir(parents=True)
(base / ".agents" / "skill_a" / "Skills.md").write_text("# Skill A")
(base / ".agents" / "skill_b" / "Skills.md").write_text("# Skill B")
crew_instance = SkillsCrew()
crew_instance.base_directory = base
sources = crew_instance.get_skills_knowledge_sources()
# With docling installed we get 2 sources; without it we get []
if len(sources) == 2:
paths = []
for s in sources:
paths.extend(getattr(s, "file_paths", []) or getattr(s, "safe_file_paths", []))
path_strs = {str(Path(p).resolve()) for p in paths}
expected_a = str((base / ".agents" / "skill_a" / "Skills.md").resolve())
expected_b = str((base / ".agents" / "skill_b" / "Skills.md").resolve())
assert expected_a in path_strs
assert expected_b in path_strs
else:
assert len(sources) == 0
def test_get_skills_knowledge_sources_missing_dir_returns_empty():
"""get_skills_knowledge_sources returns [] when .agents does not exist."""
@CrewBase
class NoSkillsCrew:
agents_config = "nonexistent/agents.yaml"
tasks_config = "nonexistent/tasks.yaml"
agents: list[BaseAgent]
tasks: list[Task]
@agent
def researcher(self):
return Agent(
role="Researcher",
goal="Research",
backstory="Expert researcher",
)
@task
def research_task(self):
return Task(
description="Research", expected_output="Report", agent=self.researcher()
)
@crew
def crew(self):
return Crew(agents=self.agents, tasks=self.tasks, verbose=True)
with tempfile.TemporaryDirectory() as tmp:
base = Path(tmp)
crew_instance = NoSkillsCrew()
crew_instance.base_directory = base
sources = crew_instance.get_skills_knowledge_sources()
assert sources == []

View File

@@ -149,6 +149,7 @@ members = [
"lib/crewai-tools",
"lib/devtools",
"lib/crewai-files",
"testing_agents",
]