"""Skill registry CRUD endpoints + skill execution.""" from __future__ import annotations from fastapi import APIRouter, HTTPException, UploadFile, File from pydantic import BaseModel from Project.Sanad.config import AUDIO_RECORDINGS_DIR from Project.Sanad.dashboard.routes._safe_io import ( safe_path_under, check_upload_size, atomic_write_bytes, ) router = APIRouter() class SkillCreate(BaseModel): id: str = "" audio_file: str = "" motion_file: str = "" callback: str = "" sync_mode: str = "parallel" enabled: bool = True description: str = "" class SkillUpdate(BaseModel): audio_file: str | None = None motion_file: str | None = None callback: str | None = None sync_mode: str | None = None enabled: bool | None = None description: str | None = None @router.get("/") async def list_skills(): from Project.Sanad.main import brain return {"skills": brain.registry.list_skills()} @router.get("/{skill_id}") async def get_skill(skill_id: str): from Project.Sanad.main import brain skill = brain.registry.get(skill_id) if skill is None: raise HTTPException(404, f"Skill not found: {skill_id}") return skill.to_dict() @router.post("/") async def create_skill(payload: SkillCreate): from Project.Sanad.main import brain from Project.Sanad.core.skill_registry import Skill try: skill = Skill(**payload.model_dump()) created = brain.registry.add(skill) except ValueError as exc: raise HTTPException(400, str(exc)) return {"ok": True, "skill": created.to_dict()} @router.put("/{skill_id}") async def update_skill(skill_id: str, payload: SkillUpdate): from Project.Sanad.main import brain updates = {k: v for k, v in payload.model_dump().items() if v is not None} try: updated = brain.registry.update(skill_id, updates) except ValueError as exc: raise HTTPException(400, str(exc)) if updated is None: raise HTTPException(404, f"Skill not found: {skill_id}") return {"ok": True, "skill": updated.to_dict()} @router.delete("/{skill_id}") async def delete_skill(skill_id: str): from Project.Sanad.main import brain deleted = brain.registry.delete(skill_id) if not deleted: raise HTTPException(404, f"Skill not found: {skill_id}") return {"ok": True, "deleted": deleted} @router.post("/{skill_id}/execute") async def execute_skill(skill_id: str): from Project.Sanad.main import brain result = await brain.execute_skill(skill_id) return result @router.post("/upload-audio") async def upload_audio(file: UploadFile = File(...)): """Upload a .wav file for skill binding.""" if not file.filename or not file.filename.lower().endswith(".wav"): raise HTTPException(400, "Only .wav files are accepted.") AUDIO_RECORDINGS_DIR.mkdir(parents=True, exist_ok=True) dest = safe_path_under(AUDIO_RECORDINGS_DIR, file.filename) content = await file.read() check_upload_size(content) atomic_write_bytes(dest, content) return {"ok": True, "path": str(dest), "size_bytes": len(content)}