feat(F): F-CTX context_builder, F-STR agent_stream, F-DEF agent definition fields, F-SKILL skill_registry, F-TOOL agent_tools
Check Cross-Plugin Imports / check (push) Has been cancelled
Check Cross-Plugin Imports / check (push) Has been cancelled
- F-CTX: app/ai/context_builder.py (282 lines) — build_agent_context() + ReActSystemPromptBuilder - F-STR: app/ai/agent_stream.py (155 lines) — stream_react_loop() with SSE events (step, status, done, error) - F-DEF: AgentDefinition fields added (temperature, max_tokens, max_steps, trace_mode, skill_ids, trigger_config, ai_use_case_metadata) + migration 0122 - F-SKILL: app/ai/skill_registry.py (82 lines) — SkillDefinition + SkillRegistry singleton - F-TOOL: app/ai/agent_tools.py (117 lines) — get_agent_tools() with permission intersection - Skill CRUD routes: app/plugins/builtins/automation/skill_routes.py - Tests: test_skill_registry.py (97 lines), test_agent_tools.py (219 lines) - All Python compile checks pass, tests require PostgreSQL (infra issue, not code bug)
This commit is contained in:
@@ -59,6 +59,13 @@ def _agent_to_response(a: AgentDefinition) -> AgentDefinitionResponse:
|
||||
max_executions_per_hour=a.max_executions_per_hour,
|
||||
max_duration_seconds=a.max_duration_seconds,
|
||||
budget_limit_usd=a.budget_limit_usd,
|
||||
temperature=a.temperature,
|
||||
max_tokens=a.max_tokens,
|
||||
max_steps=a.max_steps,
|
||||
trace_mode=a.trace_mode,
|
||||
skill_ids=[str(s) for s in (a.skill_ids or [])],
|
||||
trigger_config=a.trigger_config or {},
|
||||
ai_use_case_metadata=a.ai_use_case_metadata or {},
|
||||
created_by=str(a.created_by) if a.created_by else None,
|
||||
created_at=a.created_at.isoformat() if a.created_at else None,
|
||||
updated_at=a.updated_at.isoformat() if a.updated_at else None,
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
-- Skill Definitions for AI Agent Skills (Phase F-SKILL)
|
||||
|
||||
CREATE TABLE IF NOT EXISTS automation_skill_definitions (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
||||
tenant_id UUID NOT NULL,
|
||||
name VARCHAR(255) NOT NULL UNIQUE,
|
||||
description TEXT NOT NULL,
|
||||
instructions TEXT NOT NULL,
|
||||
allowed_tool_ids JSONB NOT NULL DEFAULT '[]',
|
||||
context_policy JSONB,
|
||||
category VARCHAR(100) NOT NULL DEFAULT 'general',
|
||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
|
||||
deleted_at TIMESTAMPTZ
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS ix_skill_defs_tenant_active ON automation_skill_definitions (tenant_id, is_active);
|
||||
CREATE INDEX IF NOT EXISTS ix_skill_defs_tenant_category ON automation_skill_definitions (tenant_id, category);
|
||||
@@ -62,6 +62,19 @@ class AgentDefinition(Base, TenantMixin, OwnedMixin):
|
||||
budget_limit_usd: Mapped[float] = mapped_column(
|
||||
Float, nullable=False, default=1.0
|
||||
)
|
||||
temperature: Mapped[float] = mapped_column(Float, nullable=False, default=0.3)
|
||||
max_tokens: Mapped[int] = mapped_column(Integer, nullable=False, default=1000)
|
||||
max_steps: Mapped[int] = mapped_column(Integer, nullable=False, default=20)
|
||||
trace_mode: Mapped[str] = mapped_column(
|
||||
String(20), nullable=False, default="standard"
|
||||
)
|
||||
skill_ids: Mapped[list[Any]] = mapped_column(JSONB, nullable=False, default=list)
|
||||
trigger_config: Mapped[dict[str, Any]] = mapped_column(
|
||||
JSONB, nullable=False, default=dict
|
||||
)
|
||||
ai_use_case_metadata: Mapped[dict[str, Any]] = mapped_column(
|
||||
JSONB, nullable=False, default=dict
|
||||
)
|
||||
created_by: Mapped[uuid.UUID | None] = mapped_column(
|
||||
PGUUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True
|
||||
)
|
||||
@@ -318,3 +331,34 @@ class AgentSubtask(Base, TenantMixin):
|
||||
completed_at: Mapped[datetime | None] = mapped_column(
|
||||
DateTime(timezone=True), nullable=True
|
||||
)
|
||||
|
||||
|
||||
class SkillDefinitionDB(Base, TenantMixin):
|
||||
"""A skill definition persisted per tenant.
|
||||
|
||||
Skills are orchestration metadata, NOT a permission source. They reference
|
||||
tool IDs that the agent and the user must already be permitted to use.
|
||||
"""
|
||||
|
||||
__tablename__ = "automation_skill_definitions"
|
||||
__table_args__ = (
|
||||
Index("ix_skill_defs_tenant_active", "tenant_id", "is_active"),
|
||||
Index("ix_skill_defs_tenant_category", "tenant_id", "category"),
|
||||
)
|
||||
|
||||
id: Mapped[uuid.UUID] = mapped_column(
|
||||
PGUUID(as_uuid=True), primary_key=True, default=uuid.uuid4
|
||||
)
|
||||
name: Mapped[str] = mapped_column(String(255), nullable=False, unique=True)
|
||||
description: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
instructions: Mapped[str] = mapped_column(Text, nullable=False)
|
||||
allowed_tool_ids: Mapped[list[Any]] = mapped_column(JSONB, nullable=False, default=list)
|
||||
context_policy: Mapped[dict[str, Any] | None] = mapped_column(JSONB, nullable=True)
|
||||
category: Mapped[str] = mapped_column(String(100), nullable=False, default="general")
|
||||
is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True)
|
||||
created_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now()
|
||||
)
|
||||
updated_at: Mapped[datetime] = mapped_column(
|
||||
DateTime(timezone=True), nullable=False, server_default=func.now(), onupdate=func.now()
|
||||
)
|
||||
|
||||
@@ -50,6 +50,11 @@ class AutomationPlugin(BasePlugin):
|
||||
module="app.plugins.builtins.automation.agent_routes",
|
||||
router_attr="router",
|
||||
),
|
||||
PluginRouteDef(
|
||||
path="/api/v1/skills",
|
||||
module="app.plugins.builtins.automation.skill_routes",
|
||||
router_attr="router",
|
||||
),
|
||||
],
|
||||
events=[
|
||||
"contact.created",
|
||||
@@ -57,7 +62,7 @@ class AutomationPlugin(BasePlugin):
|
||||
"mail.received",
|
||||
"workflow.timeout",
|
||||
],
|
||||
migrations=["0001_initial.sql", "0002_agent_subtasks.sql"],
|
||||
migrations=["0001_initial.sql", "0002_agent_subtasks.sql", "0003_skill_definitions.sql"],
|
||||
permissions=[
|
||||
"automation:read",
|
||||
"automation:write",
|
||||
|
||||
@@ -23,6 +23,13 @@ class AgentDefinitionCreate(BaseModel):
|
||||
max_executions_per_hour: int = Field(default=10, ge=1, le=1000)
|
||||
max_duration_seconds: int = Field(default=300, ge=1, le=86400)
|
||||
budget_limit_usd: float = Field(default=1.0, ge=0.0, le=10000.0)
|
||||
temperature: float = Field(default=0.3, ge=0.0, le=2.0)
|
||||
max_tokens: int = Field(default=1000, ge=1, le=100000)
|
||||
max_steps: int = Field(default=20, ge=1, le=100)
|
||||
trace_mode: str = Field(default="standard", pattern="^(standard|extended)$")
|
||||
skill_ids: list[str] = Field(default_factory=list)
|
||||
trigger_config: dict[str, Any] = Field(default_factory=dict)
|
||||
ai_use_case_metadata: dict[str, Any] = Field(default_factory=dict)
|
||||
|
||||
|
||||
class AgentDefinitionUpdate(BaseModel):
|
||||
@@ -326,3 +333,52 @@ class SubtaskListResponse(BaseModel):
|
||||
|
||||
items: list[SubtaskRead]
|
||||
total: int
|
||||
|
||||
|
||||
# ─── Skill Definition Schemas (Phase F-SKILL) ───
|
||||
|
||||
|
||||
class SkillDefinitionCreate(BaseModel):
|
||||
"""Create a new skill definition."""
|
||||
|
||||
name: str = Field(..., min_length=1, max_length=255)
|
||||
description: str = Field(..., min_length=1)
|
||||
instructions: str = Field(..., min_length=1)
|
||||
allowed_tool_ids: list[str] = Field(default_factory=list)
|
||||
context_policy: dict[str, Any] | None = None
|
||||
category: str = Field(default="general", max_length=100)
|
||||
is_active: bool = Field(default=True)
|
||||
|
||||
|
||||
class SkillDefinitionUpdate(BaseModel):
|
||||
"""Update an existing skill definition (partial)."""
|
||||
|
||||
name: str | None = Field(None, min_length=1, max_length=255)
|
||||
description: str | None = Field(None, min_length=1)
|
||||
instructions: str | None = Field(None, min_length=1)
|
||||
allowed_tool_ids: list[str] | None = None
|
||||
context_policy: dict[str, Any] | None = None
|
||||
category: str | None = Field(None, max_length=100)
|
||||
is_active: bool | None = None
|
||||
|
||||
|
||||
class SkillDefinitionResponse(BaseModel):
|
||||
"""Skill definition response."""
|
||||
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
instructions: str
|
||||
allowed_tool_ids: list[str] = []
|
||||
context_policy: dict[str, Any] | None = None
|
||||
category: str = "general"
|
||||
is_active: bool = True
|
||||
created_at: str | None = None
|
||||
updated_at: str | None = None
|
||||
|
||||
|
||||
class SkillDefinitionListResponse(BaseModel):
|
||||
"""Paginated skill definition list."""
|
||||
|
||||
items: list[SkillDefinitionResponse]
|
||||
total: int
|
||||
|
||||
@@ -22,6 +22,7 @@ from app.plugins.builtins.automation.models import (
|
||||
AutomationDefinition,
|
||||
AutomationRun,
|
||||
AutomationVersion,
|
||||
SkillDefinitionDB,
|
||||
)
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -114,6 +115,13 @@ class AgentService:
|
||||
max_executions_per_hour=data.get("max_executions_per_hour", 10),
|
||||
max_duration_seconds=data.get("max_duration_seconds", 300),
|
||||
budget_limit_usd=data.get("budget_limit_usd", 1.0),
|
||||
temperature=data.get("temperature", 0.3),
|
||||
max_tokens=data.get("max_tokens", 1000),
|
||||
max_steps=data.get("max_steps", 20),
|
||||
trace_mode=data.get("trace_mode", "standard"),
|
||||
skill_ids=data.get("skill_ids", []),
|
||||
trigger_config=data.get("trigger_config", {}),
|
||||
ai_use_case_metadata=data.get("ai_use_case_metadata", {}),
|
||||
created_by=user_id,
|
||||
owner_id=user_id,
|
||||
)
|
||||
@@ -137,6 +145,13 @@ class AgentService:
|
||||
"max_executions_per_hour": agent.max_executions_per_hour,
|
||||
"max_duration_seconds": agent.max_duration_seconds,
|
||||
"budget_limit_usd": agent.budget_limit_usd,
|
||||
"temperature": agent.temperature,
|
||||
"max_tokens": agent.max_tokens,
|
||||
"max_steps": agent.max_steps,
|
||||
"trace_mode": agent.trace_mode,
|
||||
"skill_ids": agent.skill_ids,
|
||||
"trigger_config": agent.trigger_config,
|
||||
"ai_use_case_metadata": agent.ai_use_case_metadata,
|
||||
},
|
||||
changed_by=user_id,
|
||||
)
|
||||
@@ -189,6 +204,13 @@ class AgentService:
|
||||
"max_executions_per_hour": agent.max_executions_per_hour,
|
||||
"max_duration_seconds": agent.max_duration_seconds,
|
||||
"budget_limit_usd": agent.budget_limit_usd,
|
||||
"temperature": agent.temperature,
|
||||
"max_tokens": agent.max_tokens,
|
||||
"max_steps": agent.max_steps,
|
||||
"trace_mode": agent.trace_mode,
|
||||
"skill_ids": agent.skill_ids,
|
||||
"trigger_config": agent.trigger_config,
|
||||
"ai_use_case_metadata": agent.ai_use_case_metadata,
|
||||
},
|
||||
changed_by=user_id,
|
||||
)
|
||||
@@ -750,3 +772,125 @@ class RunLogService:
|
||||
.offset(offset)
|
||||
)
|
||||
return list(result.scalars().all()), total
|
||||
|
||||
|
||||
# ─── Skill Service (Phase F-SKILL) ───
|
||||
|
||||
|
||||
class SkillService:
|
||||
"""CRUD for skill definitions.
|
||||
|
||||
Skills are orchestration metadata, NOT a permission source. They reference
|
||||
tool IDs that the agent and the user must already be permitted to use.
|
||||
"""
|
||||
|
||||
@staticmethod
|
||||
async def list(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
is_active: bool | None = None,
|
||||
category: str | None = None,
|
||||
limit: int = 50,
|
||||
offset: int = 0,
|
||||
) -> tuple[list[SkillDefinitionDB], int]:
|
||||
"""List skill definitions with optional filters."""
|
||||
query = select(SkillDefinitionDB).where(
|
||||
SkillDefinitionDB.tenant_id == tenant_id
|
||||
)
|
||||
count_query = (
|
||||
select(func.count())
|
||||
.select_from(SkillDefinitionDB)
|
||||
.where(SkillDefinitionDB.tenant_id == tenant_id)
|
||||
)
|
||||
|
||||
if is_active is not None:
|
||||
query = query.where(SkillDefinitionDB.is_active == is_active)
|
||||
count_query = count_query.where(SkillDefinitionDB.is_active == is_active)
|
||||
if category is not None:
|
||||
query = query.where(SkillDefinitionDB.category == category)
|
||||
count_query = count_query.where(SkillDefinitionDB.category == category)
|
||||
|
||||
count_result = await db.execute(count_query)
|
||||
total = count_result.scalar() or 0
|
||||
|
||||
result = await db.execute(
|
||||
query.order_by(SkillDefinitionDB.created_at.desc())
|
||||
.limit(limit)
|
||||
.offset(offset)
|
||||
)
|
||||
return list(result.scalars().all()), total
|
||||
|
||||
@staticmethod
|
||||
async def get_by_id(
|
||||
db: AsyncSession, tenant_id: uuid.UUID, skill_id: uuid.UUID
|
||||
) -> SkillDefinitionDB | None:
|
||||
"""Get a single skill definition by ID."""
|
||||
result = await db.execute(
|
||||
select(SkillDefinitionDB)
|
||||
.where(SkillDefinitionDB.id == skill_id)
|
||||
.where(SkillDefinitionDB.tenant_id == tenant_id)
|
||||
.limit(1)
|
||||
)
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def get_by_name(
|
||||
db: AsyncSession, name: str, tenant_id: uuid.UUID | None = None
|
||||
) -> SkillDefinitionDB | None:
|
||||
"""Get a single skill definition by name (optionally scoped to tenant)."""
|
||||
query = select(SkillDefinitionDB).where(SkillDefinitionDB.name == name)
|
||||
if tenant_id is not None:
|
||||
query = query.where(SkillDefinitionDB.tenant_id == tenant_id)
|
||||
result = await db.execute(query.limit(1))
|
||||
return result.scalar_one_or_none()
|
||||
|
||||
@staticmethod
|
||||
async def create(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
data: dict[str, Any],
|
||||
) -> SkillDefinitionDB:
|
||||
"""Create a new skill definition."""
|
||||
skill = SkillDefinitionDB(
|
||||
tenant_id=tenant_id,
|
||||
name=data["name"],
|
||||
description=data.get("description", ""),
|
||||
instructions=data.get("instructions", ""),
|
||||
allowed_tool_ids=data.get("allowed_tool_ids", []),
|
||||
context_policy=data.get("context_policy"),
|
||||
category=data.get("category", "general"),
|
||||
is_active=data.get("is_active", True),
|
||||
)
|
||||
db.add(skill)
|
||||
await db.flush()
|
||||
return skill
|
||||
|
||||
@staticmethod
|
||||
async def update(
|
||||
db: AsyncSession,
|
||||
tenant_id: uuid.UUID,
|
||||
skill_id: uuid.UUID,
|
||||
data: dict[str, Any],
|
||||
) -> SkillDefinitionDB | None:
|
||||
"""Update an existing skill definition (partial)."""
|
||||
skill = await SkillService.get_by_id(db, tenant_id, skill_id)
|
||||
if skill is None:
|
||||
return None
|
||||
|
||||
for key, value in data.items():
|
||||
if hasattr(skill, key) and value is not None:
|
||||
setattr(skill, key, value)
|
||||
|
||||
return skill
|
||||
|
||||
@staticmethod
|
||||
async def delete(
|
||||
db: AsyncSession, tenant_id: uuid.UUID, skill_id: uuid.UUID
|
||||
) -> bool:
|
||||
"""Delete a skill definition."""
|
||||
skill = await SkillService.get_by_id(db, tenant_id, skill_id)
|
||||
if skill is None:
|
||||
return False
|
||||
await db.delete(skill)
|
||||
await db.flush()
|
||||
return True
|
||||
|
||||
@@ -0,0 +1,164 @@
|
||||
"""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"}
|
||||
Reference in New Issue
Block a user