From 4dae956725203c04f1d9b9fbd86b5069d8ef9979 Mon Sep 17 00:00:00 2001 From: Jesse Miller Date: Wed, 4 Feb 2026 07:43:52 -0800 Subject: [PATCH] Add support for skills --- docs/en/concepts/knowledge.mdx | 29 ++++++ lib/crewai/src/crewai/cli/create_crew.py | 10 ++ .../src/crewai/cli/templates/crew/crew.py | 7 ++ lib/crewai/src/crewai/project/crew_base.py | 53 +++++++++++ lib/crewai/src/crewai/project/wrappers.py | 4 + lib/crewai/src/crewai/utilities/llm_utils.py | 5 +- lib/crewai/tests/test_project.py | 92 +++++++++++++++++++ pyproject.toml | 1 + 8 files changed, 200 insertions(+), 1 deletion(-) diff --git a/docs/en/concepts/knowledge.mdx b/docs/en/concepts/knowledge.mdx index 937cca1fd..fc55009e7 100644 --- a/docs/en/concepts/knowledge.mdx +++ b/docs/en/concepts/knowledge.mdx @@ -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. +### 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//.agents//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. diff --git a/lib/crewai/src/crewai/cli/create_crew.py b/lib/crewai/src/crewai/cli/create_crew.py index e4d84e8bc..5c4430519 100644 --- a/lib/crewai/src/crewai/cli/create_crew.py +++ b/lib/crewai/src/crewai/cli/create_crew.py @@ -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) diff --git a/lib/crewai/src/crewai/cli/templates/crew/crew.py b/lib/crewai/src/crewai/cli/templates/crew/crew.py index 43a2608a4..cfc7ada2f 100644 --- a/lib/crewai/src/crewai/cli/templates/crew/crew.py +++ b/lib/crewai/src/crewai/cli/templates/crew/crew.py @@ -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// 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/ ) diff --git a/lib/crewai/src/crewai/project/crew_base.py b/lib/crewai/src/crewai/project/crew_base.py index 202d98898..d2e4e5dde 100644 --- a/lib/crewai/src/crewai/project/crew_base.py +++ b/lib/crewai/src/crewai/project/crew_base.py @@ -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// and return them as knowledge sources. + + Looks for src//.agents//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, diff --git a/lib/crewai/src/crewai/project/wrappers.py b/lib/crewai/src/crewai/project/wrappers.py index 28cd39525..c901af508 100644 --- a/lib/crewai/src/crewai/project/wrappers.py +++ b/lib/crewai/src/crewai/project/wrappers.py @@ -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]] diff --git a/lib/crewai/src/crewai/utilities/llm_utils.py b/lib/crewai/src/crewai/utilities/llm_utils.py index 129f064d5..600611408 100644 --- a/lib/crewai/src/crewai/utilities/llm_utils.py +++ b/lib/crewai/src/crewai/utilities/llm_utils.py @@ -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] diff --git a/lib/crewai/tests/test_project.py b/lib/crewai/tests/test_project.py index 4962ff08c..9ff80298b 100644 --- a/lib/crewai/tests/test_project.py +++ b/lib/crewai/tests/test_project.py @@ -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//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 == [] diff --git a/pyproject.toml b/pyproject.toml index df0a62288..f89a9ee50 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -149,6 +149,7 @@ members = [ "lib/crewai-tools", "lib/devtools", "lib/crewai-files", + "testing_agents", ]