Skip to content

Commit 30b82a6

Browse files
authored
Merge pull request lightspeed-core#1736 from anik120/skills-configuration-model
LCORE-2072: Add SkillsConfiguration model to config file
2 parents 4dddbde + 0f6a586 commit 30b82a6

5 files changed

Lines changed: 220 additions & 0 deletions

File tree

docs/openapi.json

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12132,6 +12132,18 @@
1213212132
"$ref": "#/components/schemas/RerankerConfiguration",
1213312133
"title": "Reranker configuration",
1213412134
"description": "Configuration for neural reranking of RAG chunks using cross-encoder."
12135+
},
12136+
"skills": {
12137+
"anyOf": [
12138+
{
12139+
"$ref": "#/components/schemas/SkillsConfiguration"
12140+
},
12141+
{
12142+
"type": "null"
12143+
}
12144+
],
12145+
"title": "Agent skills",
12146+
"description": "Agent skills configuration. Specifies paths to skill directories."
1213512147
}
1213612148
},
1213712149
"additionalProperties": false,
@@ -19288,6 +19300,23 @@
1928819300
}
1928919301
]
1929019302
},
19303+
"SkillsConfiguration": {
19304+
"properties": {
19305+
"paths": {
19306+
"items": {
19307+
"type": "string",
19308+
"format": "path"
19309+
},
19310+
"type": "array",
19311+
"title": "Skill paths",
19312+
"description": "Paths to skill directories or directories containing skill subdirectories."
19313+
}
19314+
},
19315+
"additionalProperties": false,
19316+
"type": "object",
19317+
"title": "SkillsConfiguration",
19318+
"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."
19319+
},
1929119320
"SolrVectorSearchRequest": {
1929219321
"properties": {
1929319322
"mode": {
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: Lightspeed Core Service (LCS) with Skills
2+
service:
3+
host: localhost
4+
port: 8080
5+
auth_enabled: false
6+
workers: 1
7+
color_log: true
8+
access_log: true
9+
llama_stack:
10+
use_as_library_client: true
11+
library_client_config_path: run.yaml
12+
user_data_collection:
13+
feedback_enabled: true
14+
feedback_storage: "/tmp/data/feedback"
15+
transcripts_enabled: true
16+
transcripts_storage: "/tmp/data/transcripts"
17+
authentication:
18+
module: "noop"
19+
# Agent skills configuration
20+
# Skills provide domain-specific instructions and reference materials
21+
# that the LLM can load on demand when relevant to the current task
22+
skills:
23+
paths:
24+
# Option A: Directory containing multiple skill subdirectories
25+
# Each subdirectory must contain a SKILL.md file
26+
- "/var/skills/"
27+
28+
# Option B: Individual skill paths for fine-grained control
29+
# - "/var/skills/openshift-troubleshooting/"
30+
# - "/var/skills/code-review/"
31+
# - "/opt/custom-skills/deployment-guide/"

src/models/config.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2015,6 +2015,26 @@ class AzureEntraIdConfiguration(ConfigurationBase):
20152015
)
20162016

20172017

2018+
class SkillsConfiguration(ConfigurationBase):
2019+
"""Agent skills configuration.
2020+
2021+
Specifies paths to skill directories. Skill metadata (name, description)
2022+
is read from SKILL.md frontmatter at startup.
2023+
2024+
Each path can point to either:
2025+
- A directory containing a SKILL.md file (single skill)
2026+
- A directory containing subdirectories with SKILL.md files (multiple skills)
2027+
2028+
Paths are validated at startup to ensure they exist and contain valid SKILL.md files.
2029+
"""
2030+
2031+
paths: list[Path] = Field(
2032+
default_factory=list,
2033+
title="Skill paths",
2034+
description="Paths to skill directories or directories containing skill subdirectories.",
2035+
)
2036+
2037+
20182038
class Configuration(ConfigurationBase):
20192039
"""Global service configuration."""
20202040

@@ -2187,6 +2207,12 @@ class Configuration(ConfigurationBase):
21872207
description="Configuration for neural reranking of RAG chunks using cross-encoder.",
21882208
)
21892209

2210+
skills: Optional[SkillsConfiguration] = Field(
2211+
default=None,
2212+
title="Agent skills",
2213+
description="Agent skills configuration. Specifies paths to skill directories.",
2214+
)
2215+
21902216
@model_validator(mode="after")
21912217
def validate_mcp_auth_headers(self) -> Self:
21922218
"""

tests/unit/models/config/test_dump_configuration.py

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@
2222
QuotaLimiterConfiguration,
2323
QuotaSchedulerConfiguration,
2424
ServiceConfiguration,
25+
SkillsConfiguration,
2526
TLSConfiguration,
2627
UserDataCollection,
2728
)
@@ -249,6 +250,7 @@ def test_dump_configuration(tmp_path: Path) -> None:
249250
"enabled": False,
250251
"model": "cross-encoder/ms-marco-MiniLM-L6-v2",
251252
},
253+
"skills": None,
252254
}
253255

254256

@@ -613,6 +615,7 @@ def test_dump_configuration_with_quota_limiters(tmp_path: Path) -> None:
613615
"enabled": False,
614616
"model": "cross-encoder/ms-marco-MiniLM-L6-v2",
615617
},
618+
"skills": None,
616619
}
617620

618621

@@ -861,6 +864,7 @@ def test_dump_configuration_with_quota_limiters_different_values(
861864
"enabled": False,
862865
"model": "cross-encoder/ms-marco-MiniLM-L6-v2",
863866
},
867+
"skills": None,
864868
}
865869

866870

@@ -1084,6 +1088,7 @@ def test_dump_configuration_byok(tmp_path: Path) -> None:
10841088
"enabled": False,
10851089
"model": "cross-encoder/ms-marco-MiniLM-L6-v2",
10861090
},
1091+
"skills": None,
10871092
}
10881093

10891094

@@ -1292,4 +1297,84 @@ def test_dump_configuration_pg_namespace(tmp_path: Path) -> None:
12921297
"enabled": False,
12931298
"model": "cross-encoder/ms-marco-MiniLM-L6-v2",
12941299
},
1300+
"skills": None,
1301+
}
1302+
1303+
1304+
def test_dump_configuration_with_skills(tmp_path: Path) -> None:
1305+
"""
1306+
Test that Configuration with skills paths can be serialized to JSON.
1307+
1308+
Verifies that skills paths are properly dumped and serialized as strings.
1309+
"""
1310+
cfg = Configuration(
1311+
name="test_name",
1312+
service=ServiceConfiguration(
1313+
tls_config=TLSConfiguration(
1314+
tls_certificate_path=Path("tests/configuration/server.crt"),
1315+
tls_key_path=Path("tests/configuration/server.key"),
1316+
tls_key_password=Path("tests/configuration/password"),
1317+
),
1318+
cors=CORSConfiguration(
1319+
allow_origins=["foo_origin", "bar_origin", "baz_origin"],
1320+
allow_credentials=False,
1321+
allow_methods=["foo_method", "bar_method", "baz_method"],
1322+
allow_headers=["foo_header", "bar_header", "baz_header"],
1323+
),
1324+
),
1325+
llama_stack=LlamaStackConfiguration(
1326+
use_as_library_client=True,
1327+
library_client_config_path="tests/configuration/run.yaml",
1328+
api_key=SecretStr("whatever"),
1329+
),
1330+
user_data_collection=UserDataCollection(
1331+
feedback_enabled=False, feedback_storage=None
1332+
),
1333+
database=DatabaseConfiguration(
1334+
sqlite=None,
1335+
postgres=PostgreSQLDatabaseConfiguration(
1336+
db="lightspeed_stack",
1337+
user="ls_user",
1338+
password=SecretStr("ls_password"),
1339+
port=5432,
1340+
ca_cert_path=None,
1341+
ssl_mode="require",
1342+
gss_encmode="disable",
1343+
),
1344+
),
1345+
mcp_servers=[],
1346+
customization=None,
1347+
inference=InferenceConfiguration(
1348+
default_provider="default_provider",
1349+
default_model="default_model",
1350+
),
1351+
skills=SkillsConfiguration(
1352+
paths=[
1353+
"/var/skills/openshift-troubleshooting",
1354+
"/var/skills/code-review",
1355+
"/opt/custom-skills",
1356+
]
1357+
),
1358+
)
1359+
assert cfg is not None
1360+
dump_file = tmp_path / "test.json"
1361+
cfg.dump(dump_file)
1362+
1363+
with open(dump_file, "r", encoding="utf-8") as fin:
1364+
content = json.load(fin)
1365+
# content should be loaded
1366+
assert content is not None
1367+
1368+
# skills section must exist
1369+
assert "skills" in content
1370+
assert content["skills"] is not None
1371+
assert "paths" in content["skills"]
1372+
1373+
# verify skills paths are properly serialized
1374+
assert content["skills"] == {
1375+
"paths": [
1376+
"/var/skills/openshift-troubleshooting",
1377+
"/var/skills/code-review",
1378+
"/opt/custom-skills",
1379+
]
12951380
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
"""Unit tests for SkillsConfiguration model."""
2+
3+
# pylint: disable=no-member
4+
# Pydantic Field(default_factory=...) pattern confuses pylint's static analysis
5+
6+
from pathlib import Path
7+
8+
import pytest
9+
from pydantic import ValidationError
10+
11+
from models.config import SkillsConfiguration
12+
13+
14+
class TestSkillsConfiguration:
15+
"""Tests for SkillsConfiguration model."""
16+
17+
def test_empty_paths_list(self) -> None:
18+
"""Test that an explicit empty paths list is allowed."""
19+
config = SkillsConfiguration(paths=[])
20+
assert config.paths == []
21+
22+
def test_no_unknown_fields_allowed(self) -> None:
23+
"""Test that SkillsConfiguration rejects unknown fields."""
24+
with pytest.raises(ValidationError, match="Extra inputs are not permitted"):
25+
SkillsConfiguration(unknown_field="value") # type: ignore[call-arg]
26+
27+
def test_skill_paths(self) -> None:
28+
"""Test configuration with multiple skill paths."""
29+
config = SkillsConfiguration(
30+
paths=[
31+
"/var/skills/openshift-troubleshooting",
32+
"/var/skills/code-review",
33+
"/opt/custom-skills",
34+
]
35+
)
36+
assert len(config.paths) == 3
37+
assert Path("/var/skills/openshift-troubleshooting") in config.paths
38+
assert Path("/var/skills/code-review") in config.paths
39+
assert Path("/opt/custom-skills") in config.paths
40+
41+
def test_mixed_absolute_and_relative_paths(self) -> None:
42+
"""Test that both absolute and relative paths can be mixed."""
43+
config = SkillsConfiguration(
44+
paths=["/var/skills", "./local-skills", "/opt/skills"]
45+
)
46+
assert len(config.paths) == 3
47+
assert Path("/var/skills") in config.paths
48+
assert Path("./local-skills") in config.paths
49+
assert Path("/opt/skills") in config.paths

0 commit comments

Comments
 (0)