Files
leocrm/app/plugins/builtins/automation/skill_routes.py
T

165 lines
5.1 KiB
Python
Raw Normal View History

"""API routes for skill definitions — /api/v1/skills.
Endpoints: skill definitions CRUD. Skills are orchestration metadata, NOT a
permission source: they reference tool IDs that the agent and the user must
already be permitted to use.
"""
from __future__ import annotations
import logging
import uuid
from typing import Any
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.db import get_db
from app.deps import get_current_user, require_permission
from app.plugins.builtins.automation.models import SkillDefinitionDB
from app.plugins.builtins.automation.schemas import (
SkillDefinitionCreate,
SkillDefinitionListResponse,
SkillDefinitionResponse,
SkillDefinitionUpdate,
)
from app.plugins.builtins.automation.services import SkillService
logger = logging.getLogger(__name__)
router = APIRouter(prefix="/api/v1/skills", tags=["skills"])
# ─── Helper Functions ───
def _skill_to_response(s: SkillDefinitionDB) -> SkillDefinitionResponse:
"""Convert SkillDefinitionDB model to response schema."""
return SkillDefinitionResponse(
id=str(s.id),
name=s.name,
description=s.description or "",
instructions=s.instructions or "",
allowed_tool_ids=[str(t) for t in (s.allowed_tool_ids or [])],
context_policy=s.context_policy,
category=s.category or "general",
is_active=s.is_active,
created_at=s.created_at.isoformat() if s.created_at else None,
updated_at=s.updated_at.isoformat() if s.updated_at else None,
)
# ─── CRUD Endpoints ───
@router.get(
"",
dependencies=[Depends(require_permission("automation:read"))],
response_model=SkillDefinitionListResponse,
)
async def list_skills(
is_active: bool | None = Query(None),
category: str | None = Query(None),
limit: int = Query(50, ge=1, le=200),
offset: int = Query(0, ge=0),
current_user: dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""List skill definitions with optional filters."""
tenant_id = uuid.UUID(current_user["tenant_id"])
items, total = await SkillService.list(
db, tenant_id, is_active=is_active, category=category, limit=limit, offset=offset
)
return SkillDefinitionListResponse(
items=[_skill_to_response(s) for s in items],
total=total,
)
@router.post(
"/",
dependencies=[Depends(require_permission("automation:write"))],
response_model=SkillDefinitionResponse,
status_code=201,
)
async def create_skill(
data: SkillDefinitionCreate,
current_user: dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Create a new skill definition."""
tenant_id = uuid.UUID(current_user["tenant_id"])
skill = await SkillService.create(db, tenant_id, data.model_dump())
return _skill_to_response(skill)
@router.get(
"/{skill_id}",
dependencies=[Depends(require_permission("automation:read"))],
response_model=SkillDefinitionResponse,
)
async def get_skill(
skill_id: str,
current_user: dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Get a single skill definition by ID."""
tenant_id = uuid.UUID(current_user["tenant_id"])
try:
sid = uuid.UUID(skill_id)
except (ValueError, TypeError):
raise HTTPException(status_code=400, detail="Invalid skill ID") from None
skill = await SkillService.get_by_id(db, tenant_id, sid)
if skill is None:
raise HTTPException(status_code=404, detail="Skill not found")
return _skill_to_response(skill)
@router.patch(
"/{skill_id}",
dependencies=[Depends(require_permission("automation:write"))],
response_model=SkillDefinitionResponse,
)
async def update_skill(
skill_id: str,
data: SkillDefinitionUpdate,
current_user: dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Update an existing skill definition."""
tenant_id = uuid.UUID(current_user["tenant_id"])
try:
sid = uuid.UUID(skill_id)
except (ValueError, TypeError):
raise HTTPException(status_code=400, detail="Invalid skill ID") from None
skill = await SkillService.update(
db, tenant_id, sid, data.model_dump(exclude_none=True)
)
if skill is None:
raise HTTPException(status_code=404, detail="Skill not found")
return _skill_to_response(skill)
@router.delete(
"/{skill_id}",
dependencies=[Depends(require_permission("automation:delete"))],
)
async def delete_skill(
skill_id: str,
current_user: dict[str, Any] = Depends(get_current_user),
db: AsyncSession = Depends(get_db),
):
"""Delete a skill definition."""
tenant_id = uuid.UUID(current_user["tenant_id"])
try:
sid = uuid.UUID(skill_id)
except (ValueError, TypeError):
raise HTTPException(status_code=400, detail="Invalid skill ID") from None
success = await SkillService.delete(db, tenant_id, sid)
if not success:
raise HTTPException(status_code=404, detail="Skill not found")
return {"status": "ok"}