diff --git a/docs/openapi.json b/docs/openapi.json index ade470712..c4221846c 100644 --- a/docs/openapi.json +++ b/docs/openapi.json @@ -12081,6 +12081,18 @@ "$ref": "#/components/schemas/RerankerConfiguration", "title": "Reranker configuration", "description": "Configuration for neural reranking of RAG chunks using cross-encoder." + }, + "skills": { + "anyOf": [ + { + "$ref": "#/components/schemas/SkillsConfiguration" + }, + { + "type": "null" + } + ], + "title": "Agent skills", + "description": "Agent skills configuration. Specifies paths to skill directories." } }, "additionalProperties": false, @@ -19381,6 +19393,22 @@ } ] }, + "SkillsConfiguration": { + "properties": { + "paths": { + "items": { + "type": "string" + }, + "type": "array", + "title": "Skill paths", + "description": "Paths to skill directories or directories containing skill subdirectories." + } + }, + "additionalProperties": false, + "type": "object", + "title": "SkillsConfiguration", + "description": "Agent skills configuration.\n\nSpecifies paths to skill directories. Skill metadata (name, description)\nis read from SKILL.md frontmatter at startup.\n\nEach path can point to either:\n- A directory containing a SKILL.md file (single skill)\n- A directory containing subdirectories with SKILL.md files (multiple skills)\n\nPaths are validated at startup to ensure they exist and contain valid SKILL.md files." + }, "SolrVectorSearchRequest": { "properties": { "mode": { diff --git a/examples/lightspeed-stack-skills.yaml b/examples/lightspeed-stack-skills.yaml new file mode 100644 index 000000000..3f33488b2 --- /dev/null +++ b/examples/lightspeed-stack-skills.yaml @@ -0,0 +1,31 @@ +name: Lightspeed Core Service (LCS) with Skills +service: + host: localhost + port: 8080 + auth_enabled: false + workers: 1 + color_log: true + access_log: true +llama_stack: + use_as_library_client: true + library_client_config_path: run.yaml +user_data_collection: + feedback_enabled: true + feedback_storage: "/tmp/data/feedback" + transcripts_enabled: true + transcripts_storage: "/tmp/data/transcripts" +authentication: + module: "noop" +# Agent skills configuration +# Skills provide domain-specific instructions and reference materials +# that the LLM can load on demand when relevant to the current task +skills: + paths: + # Option A: Directory containing multiple skill subdirectories + # Each subdirectory must contain a SKILL.md file + - "/var/skills/" + + # Option B: Individual skill paths for fine-grained control + # - "/var/skills/openshift-troubleshooting/" + # - "/var/skills/code-review/" + # - "/opt/custom-skills/deployment-guide/" diff --git a/src/models/config.py b/src/models/config.py index c245a4da3..f68bdd5b4 100644 --- a/src/models/config.py +++ b/src/models/config.py @@ -1944,6 +1944,26 @@ class AzureEntraIdConfiguration(ConfigurationBase): ) +class SkillsConfiguration(ConfigurationBase): + """Agent skills configuration. + + Specifies paths to skill directories. Skill metadata (name, description) + is read from SKILL.md frontmatter at startup. + + Each path can point to either: + - A directory containing a SKILL.md file (single skill) + - A directory containing subdirectories with SKILL.md files (multiple skills) + + Paths are validated at startup to ensure they exist and contain valid SKILL.md files. + """ + + paths: list[str] = Field( + default_factory=list, + title="Skill paths", + description="Paths to skill directories or directories containing skill subdirectories.", + ) + + class Configuration(ConfigurationBase): """Global service configuration.""" @@ -2110,6 +2130,12 @@ class Configuration(ConfigurationBase): description="Configuration for neural reranking of RAG chunks using cross-encoder.", ) + skills: Optional[SkillsConfiguration] = Field( + default=None, + title="Agent skills", + description="Agent skills configuration. Specifies paths to skill directories.", + ) + @model_validator(mode="after") def validate_mcp_auth_headers(self) -> Self: """ diff --git a/tests/unit/models/config/test_dump_configuration.py b/tests/unit/models/config/test_dump_configuration.py index 371e0d459..3ff46f5ea 100644 --- a/tests/unit/models/config/test_dump_configuration.py +++ b/tests/unit/models/config/test_dump_configuration.py @@ -235,6 +235,7 @@ def test_dump_configuration(tmp_path: Path) -> None: "enabled": False, "model": "cross-encoder/ms-marco-MiniLM-L6-v2", }, + "skills": None, } @@ -606,6 +607,7 @@ def test_dump_configuration_with_quota_limiters(tmp_path: Path) -> None: "enabled": False, "model": "cross-encoder/ms-marco-MiniLM-L6-v2", }, + "skills": None, } @@ -853,6 +855,7 @@ def test_dump_configuration_with_quota_limiters_different_values( "enabled": False, "model": "cross-encoder/ms-marco-MiniLM-L6-v2", }, + "skills": None, } @@ -1075,6 +1078,7 @@ def test_dump_configuration_byok(tmp_path: Path) -> None: "enabled": False, "model": "cross-encoder/ms-marco-MiniLM-L6-v2", }, + "skills": None, } @@ -1282,4 +1286,5 @@ def test_dump_configuration_pg_namespace(tmp_path: Path) -> None: "enabled": False, "model": "cross-encoder/ms-marco-MiniLM-L6-v2", }, + "skills": None, } diff --git a/tests/unit/models/config/test_skills_configuration.py b/tests/unit/models/config/test_skills_configuration.py new file mode 100644 index 000000000..0c339df42 --- /dev/null +++ b/tests/unit/models/config/test_skills_configuration.py @@ -0,0 +1,47 @@ +"""Unit tests for SkillsConfiguration model.""" + +# pylint: disable=no-member +# Pydantic Field(default_factory=...) pattern confuses pylint's static analysis + +import pytest +from pydantic import ValidationError + +from models.config import SkillsConfiguration + + +class TestSkillsConfiguration: + """Tests for SkillsConfiguration model.""" + + def test_empty_paths_list(self) -> None: + """Test that an explicit empty paths list is allowed.""" + config = SkillsConfiguration(paths=[]) + assert config.paths == [] + + def test_no_unknown_fields_allowed(self) -> None: + """Test that SkillsConfiguration rejects unknown fields.""" + with pytest.raises(ValidationError, match="Extra inputs are not permitted"): + SkillsConfiguration(unknown_field="value") # type: ignore[call-arg] + + def test_skill_paths(self) -> None: + """Test configuration with multiple skill paths.""" + config = SkillsConfiguration( + paths=[ + "/var/skills/openshift-troubleshooting", + "/var/skills/code-review", + "/opt/custom-skills", + ] + ) + assert len(config.paths) == 3 + assert "/var/skills/openshift-troubleshooting" in config.paths + assert "/var/skills/code-review" in config.paths + assert "/opt/custom-skills" in config.paths + + def test_mixed_absolute_and_relative_paths(self) -> None: + """Test that both absolute and relative paths can be mixed.""" + config = SkillsConfiguration( + paths=["/var/skills", "./local-skills", "/opt/skills"] + ) + assert len(config.paths) == 3 + assert "/var/skills" in config.paths + assert "./local-skills" in config.paths + assert "/opt/skills" in config.paths