Skip to content
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
208 changes: 208 additions & 0 deletions docs/devel_doc/openapi.json
Original file line number Diff line number Diff line change
Expand Up @@ -1671,6 +1671,156 @@
}
}
},
"/v1/skills": {
"get": {
"tags": [
"skills"
],
"summary": "Skills Endpoint Handler",
"description": "Handle requests to the /skills endpoint.\n\nProcess GET requests to the /skills endpoint, returning a list of loaded\nagent skills with their metadata (name, description).\n\n### Parameters:\n- request: The incoming HTTP request (used by middleware).\n- auth: Authentication tuple from the auth dependency (used by middleware).\n\n### Raises:\n- HTTPException: with status 401 for unauthorized access.\n- HTTPException: with status 403 if permission is denied.\n- HTTPException: with status 500 and a detail object containing `response`\n and `cause` when service configuration is wrong or incomplete.\n\n### Returns:\n- SkillsResponse: An object containing the list of loaded skills.",
"operationId": "skills_endpoint_handler_v1_skills_get",
"responses": {
"200": {
"description": "Successful response",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/SkillsResponse"
},
"example": {
"skills": [
{
"description": "Review code for quality and security",
"name": "code-review"
},
{
"description": "Troubleshoot OpenShift cluster issues",
"name": "openshift-troubleshooting"
}
]
}
}
}
},
"401": {
"description": "Unauthorized",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/UnauthorizedResponse"
},
"examples": {
"missing header": {
"value": {
"detail": {
"cause": "No Authorization header found",
"response": "Missing or invalid credentials provided by client"
}
}
},
"missing token": {
"value": {
"detail": {
"cause": "No token found in Authorization header",
"response": "Missing or invalid credentials provided by client"
}
}
},
"expired token": {
"value": {
"detail": {
"cause": "Token has expired",
"response": "Missing or invalid credentials provided by client"
}
}
},
"invalid signature": {
"value": {
"detail": {
"cause": "Invalid token signature",
"response": "Missing or invalid credentials provided by client"
}
}
},
"invalid key": {
"value": {
"detail": {
"cause": "Token signed by unknown key",
"response": "Missing or invalid credentials provided by client"
}
}
},
"missing claim": {
"value": {
"detail": {
"cause": "Token missing claim: user_id",
"response": "Missing or invalid credentials provided by client"
}
}
},
"invalid k8s token": {
"value": {
"detail": {
"cause": "Invalid or expired Kubernetes token",
"response": "Missing or invalid credentials provided by client"
}
}
},
"invalid jwk token": {
"value": {
"detail": {
"cause": "Authentication key server returned invalid data",
"response": "Missing or invalid credentials provided by client"
}
}
}
}
}
}
},
"403": {
"description": "Permission denied",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ForbiddenResponse"
},
"examples": {
"endpoint": {
"value": {
"detail": {
"cause": "User 6789 is not authorized to access this endpoint.",
"response": "User does not have permission to access this endpoint"
}
}
}
}
}
}
},
"500": {
"description": "Internal server error",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/InternalServerErrorResponse"
},
"examples": {
"configuration": {
"value": {
"detail": {
"cause": "Lightspeed Stack configuration has not been initialized.",
"response": "Configuration is not loaded"
}
}
}
}
}
}
}
}
}
},
"/v1/providers": {
"get": {
"tags": [
Expand Down Expand Up @@ -11646,6 +11796,7 @@
"feedback",
"get_models",
"get_tools",
"get_skills",
"get_shields",
"list_providers",
"get_provider",
Expand Down Expand Up @@ -20795,6 +20946,27 @@
}
]
},
"SkillMetadata": {
"properties": {
"name": {
"type": "string",
"title": "Name",
"description": "Unique name of the skill"
},
"description": {
"type": "string",
"title": "Description",
"description": "Human readable description of what the skill does"
}
},
"type": "object",
"required": [
"name",
"description"
],
"title": "SkillMetadata",
"description": "Metadata describing a single loaded agent skill.\n\nAttributes:\n name: Unique name of the skill.\n description: Human readable description of what the skill does."
},
"SkillsConfiguration": {
"properties": {
"paths": {
Expand All @@ -20812,6 +20984,38 @@
"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."
},
"SkillsResponse": {
"properties": {
"skills": {
"items": {
"$ref": "#/components/schemas/SkillMetadata"
},
"type": "array",
"title": "Skills",
"description": "List of loaded skills with metadata"
}
},
"type": "object",
"required": [
"skills"
],
"title": "SkillsResponse",
"description": "Model representing a response to skills request.",
"examples": [
{
"skills": [
{
"description": "Review code for quality and security",
"name": "code-review"
},
{
"description": "Troubleshoot OpenShift cluster issues",
"name": "openshift-troubleshooting"
}
]
}
]
},
"SolrVectorSearchRequest": {
"properties": {
"mode": {
Expand Down Expand Up @@ -22392,6 +22596,10 @@
"name": "shields",
"description": "Safety shields."
},
{
"name": "skills",
"description": "Agent skills."
},
{
"name": "streaming_query",
"description": "Streaming query (SSE)."
Expand Down
69 changes: 69 additions & 0 deletions src/app/endpoints/skills.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
"""Handler for REST API call to list loaded agent skills."""

from typing import Annotated, Any

from fastapi import APIRouter, Request
from fastapi.concurrency import run_in_threadpool
from fastapi.params import Depends

from authentication import get_auth_dependency
from authentication.interface import AuthTuple
from authorization.middleware import authorize
from configuration import configuration
from log import get_logger
from models.api.responses.constants import UNAUTHORIZED_OPENAPI_EXAMPLES
from models.api.responses.error import (
ForbiddenResponse,
InternalServerErrorResponse,
UnauthorizedResponse,
)
from models.api.responses.successful import SkillsResponse
from models.config import Action
from utils.endpoints import check_configuration_loaded
from utils.pydantic_ai_helpers import get_skills_metadata

logger = get_logger(__name__)
router = APIRouter(tags=["skills"])


skills_responses: dict[int | str, dict[str, Any]] = {
200: SkillsResponse.openapi_response(),
401: UnauthorizedResponse.openapi_response(examples=UNAUTHORIZED_OPENAPI_EXAMPLES),
403: ForbiddenResponse.openapi_response(examples=["endpoint"]),
500: InternalServerErrorResponse.openapi_response(examples=["configuration"]),
}


@router.get("/skills", responses=skills_responses)
@authorize(Action.GET_SKILLS)
async def skills_endpoint_handler(
request: Request,
auth: Annotated[AuthTuple, Depends(get_auth_dependency())],
) -> SkillsResponse:
"""Handle requests to the /skills endpoint.

Process GET requests to the /skills endpoint, returning a list of loaded
agent skills with their metadata (name, description).

### Parameters:
- request: The incoming HTTP request (used by middleware).
- auth: Authentication tuple from the auth dependency (used by middleware).

### Raises:
- HTTPException: with status 401 for unauthorized access.
- HTTPException: with status 403 if permission is denied.
- HTTPException: with status 500 and a detail object containing `response`
and `cause` when service configuration is wrong or incomplete.

### Returns:
- SkillsResponse: An object containing the list of loaded skills.
"""
_ = auth
_ = request

check_configuration_loaded(configuration)

skills_metadata = await run_in_threadpool(
get_skills_metadata, configuration.configuration.skills
)
return SkillsResponse(skills=skills_metadata)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
1 change: 1 addition & 0 deletions src/app/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@
"description": "Saved prompts configuration and management.",
},
{"name": "shields", "description": "Safety shields."},
{"name": "skills", "description": "Agent skills."},
{"name": "streaming_query", "description": "Streaming query (SSE)."},
{"name": "streaming_query_interrupt", "description": "Streaming interrupt."},
{"name": "tools", "description": "Tools."},
Expand Down
2 changes: 2 additions & 0 deletions src/app/routers.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
root,
saved_prompts,
shields,
skills,
stream_interrupt,
streaming_query,
tools,
Expand Down Expand Up @@ -55,6 +56,7 @@ def include_routers(app: FastAPI) -> None:
app.include_router(mcp_auth.router, prefix="/v1")
app.include_router(mcp_servers.router, prefix="/v1")
app.include_router(shields.router, prefix="/v1")
app.include_router(skills.router, prefix="/v1")
app.include_router(providers.router, prefix="/v1")
app.include_router(prompts.router, prefix="/v1")
app.include_router(rags.router, prefix="/v1")
Expand Down
2 changes: 2 additions & 0 deletions src/models/api/responses/successful/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
RAGInfoResponse,
RAGListResponse,
ShieldsResponse,
SkillsResponse,
ToolsResponse,
)
from models.api.responses.successful.configuration import ConfigurationResponse
Expand Down Expand Up @@ -100,6 +101,7 @@
"SavedPromptsConfigResponse",
"SavedPromptsListResponse",
"ShieldsResponse",
"SkillsResponse",
"StatusResponse",
"StreamingInterruptResponse",
"StreamingQueryResponse",
Expand Down
28 changes: 28 additions & 0 deletions src/models/api/responses/successful/catalog.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,34 @@
from pydantic import Field

from models.api.responses.successful.bases import AbstractSuccessfulResponse
from models.common.skills import SkillMetadata


class SkillsResponse(AbstractSuccessfulResponse):
"""Model representing a response to skills request."""
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Outdated

skills: list[SkillMetadata] = Field(
description="List of loaded skills with metadata",
)

model_config = {
"json_schema_extra": {
"examples": [
{
"skills": [
{
"name": "code-review",
"description": "Review code for quality and security",
},
{
"name": "openshift-troubleshooting",
"description": "Troubleshoot OpenShift cluster issues",
},
],
}
]
}
}


class ModelsResponse(AbstractSuccessfulResponse):
Expand Down
Loading
Loading