diff --git a/ai-hub/app/api/routes/api.py b/ai-hub/app/api/routes/api.py
index efa4a46..7161f26 100644
--- a/ai-hub/app/api/routes/api.py
+++ b/ai-hub/app/api/routes/api.py
@@ -9,6 +9,7 @@
from .stt import create_stt_router
from .user import create_users_router
from .nodes import create_nodes_router
+from .skills import create_skills_router
def create_api_router(services: ServiceContainer) -> APIRouter:
"""
@@ -25,5 +26,6 @@
router.include_router(create_stt_router(services))
router.include_router(create_users_router(services))
router.include_router(create_nodes_router(services))
+ router.include_router(create_skills_router(services))
return router
\ No newline at end of file
diff --git a/ai-hub/app/api/routes/skills.py b/ai-hub/app/api/routes/skills.py
new file mode 100644
index 0000000..40a0e6d
--- /dev/null
+++ b/ai-hub/app/api/routes/skills.py
@@ -0,0 +1,104 @@
+from fastapi import APIRouter, Depends, HTTPException
+from sqlalchemy.orm import Session
+from typing import List
+from app.db.database import get_db
+from app.api.dependencies import ServiceContainer, get_current_user
+from app.db import models
+from app.api import schemas
+
+def create_skills_router(services: ServiceContainer) -> APIRouter:
+ router = APIRouter(prefix="/skills", tags=["Skills"])
+
+ @router.get("/", response_model=List[schemas.SkillResponse])
+ def list_skills(
+ db: Session = Depends(get_db),
+ current_user: models.User = Depends(get_current_user)
+ ):
+ """List all skills accessible to the user (their own, group skills, and system skills)."""
+ system_skills = db.query(models.Skill).filter(models.Skill.is_system == True).all()
+ user_skills = db.query(models.Skill).filter(
+ models.Skill.owner_id == current_user.id,
+ models.Skill.is_system == False
+ ).all()
+ group_skills = []
+ if current_user.group_id:
+ group_skills = db.query(models.Skill).filter(
+ models.Skill.group_id == current_user.group_id,
+ models.Skill.owner_id != current_user.id,
+ models.Skill.is_system == False
+ ).all()
+ return system_skills + user_skills + group_skills
+
+ @router.post("/", response_model=schemas.SkillResponse)
+ def create_skill(
+ skill: schemas.SkillCreate,
+ db: Session = Depends(get_db),
+ current_user: models.User = Depends(get_current_user)
+ ):
+ """Create a new skill."""
+ existing = db.query(models.Skill).filter(models.Skill.name == skill.name).first()
+ if existing:
+ raise HTTPException(status_code=400, detail="Skill with this name already exists")
+
+ db_skill = models.Skill(
+ name=skill.name,
+ description=skill.description,
+ skill_type=skill.skill_type,
+ config=skill.config,
+ owner_id=current_user.id,
+ group_id=skill.group_id,
+ is_system=skill.is_system if current_user.role == 'admin' else False
+ )
+ db.add(db_skill)
+ db.commit()
+ db.refresh(db_skill)
+ return db_skill
+
+ @router.put("/{skill_id}", response_model=schemas.SkillResponse)
+ def update_skill(
+ skill_id: int,
+ skill_update: schemas.SkillUpdate,
+ db: Session = Depends(get_db),
+ current_user: models.User = Depends(get_current_user)
+ ):
+ """Update an existing skill. User must be admin or the owner."""
+ db_skill = db.query(models.Skill).filter(models.Skill.id == skill_id).first()
+ if not db_skill:
+ raise HTTPException(status_code=404, detail="Skill not found")
+
+ if db_skill.owner_id != current_user.id and current_user.role != 'admin':
+ raise HTTPException(status_code=403, detail="Not authorized to update this skill")
+
+ if skill_update.name is not None and skill_update.name != db_skill.name:
+ existing = db.query(models.Skill).filter(models.Skill.name == skill_update.name).first()
+ if existing:
+ raise HTTPException(status_code=400, detail="Skill with this name already exists")
+
+ for key, value in skill_update.model_dump(exclude_unset=True).items():
+ if key == 'is_system' and current_user.role != 'admin':
+ continue
+ setattr(db_skill, key, value)
+
+ db.commit()
+ db.refresh(db_skill)
+ return db_skill
+
+ @router.delete("/{skill_id}")
+ def delete_skill(
+ skill_id: int,
+ db: Session = Depends(get_db),
+ current_user: models.User = Depends(get_current_user)
+ ):
+ """Delete a skill."""
+ db_skill = db.query(models.Skill).filter(models.Skill.id == skill_id).first()
+ if not db_skill:
+ raise HTTPException(status_code=404, detail="Skill not found")
+
+ if db_skill.owner_id != current_user.id and current_user.role != 'admin':
+ raise HTTPException(status_code=403, detail="Not authorized to delete this skill")
+
+ db.delete(db_skill)
+ db.commit()
+ return {"message": "Skill deleted"}
+
+ return router
diff --git a/ai-hub/app/api/schemas.py b/ai-hub/app/api/schemas.py
index 7d8b7af..4777f1f 100644
--- a/ai-hub/app/api/schemas.py
+++ b/ai-hub/app/api/schemas.py
@@ -69,11 +69,38 @@
stt: dict = Field(default_factory=dict)
statuses: Optional[dict] = Field(default_factory=dict)
+# --- Config & General Schemas ---
class ConfigResponse(BaseModel):
"""Schema for returning user preferences alongside effective settings."""
preferences: UserPreferences
effective: dict = Field(default_factory=dict)
+# --- Skill Schemas ---
+class SkillBase(BaseModel):
+ name: str
+ description: Optional[str] = None
+ skill_type: str = "local" # local, remote_grpc, mcp
+ config: dict = Field(default_factory=dict)
+ is_system: bool = False
+
+class SkillCreate(SkillBase):
+ group_id: Optional[str] = None
+
+class SkillUpdate(BaseModel):
+ name: Optional[str] = None
+ description: Optional[str] = None
+ skill_type: Optional[str] = None
+ config: Optional[dict] = None
+ is_system: Optional[bool] = None
+ group_id: Optional[str] = None
+
+class SkillResponse(SkillBase):
+ id: int
+ owner_id: str
+ group_id: Optional[str] = None
+ created_at: datetime
+ model_config = ConfigDict(from_attributes=True)
+
# --- Chat Schemas ---
class ChatRequest(BaseModel):
"""Defines the shape of a request to the /chat endpoint."""
diff --git a/ui/client-app/src/App.js b/ui/client-app/src/App.js
index 9e49cf6..aac4d04 100644
--- a/ui/client-app/src/App.js
+++ b/ui/client-app/src/App.js
@@ -8,6 +8,7 @@
import SettingsPage from "./pages/SettingsPage";
import ProfilePage from "./pages/ProfilePage";
import NodesPage from "./pages/NodesPage";
+import SkillsPage from "./pages/SkillsPage";
import { getUserStatus, logout, getUserProfile } from "./services/apiService";
const Icon = ({ path, onClick, className }) => (
@@ -33,7 +34,7 @@
const [userId, setUserId] = useState(null);
const [userProfile, setUserProfile] = useState(null);
- const authenticatedPages = ["voice-chat", "coding-assistant", "settings", "profile", "nodes"];
+ const authenticatedPages = ["voice-chat", "coding-assistant", "settings", "profile", "nodes", "skills"];
useEffect(() => {
@@ -140,6 +141,8 @@
return
+ Create, manage, and share AI capabilities and workflows. +
++ {skill.description || "No description provided."} +
+ +No skills found.
+Create your first skill to extend the AI's capabilities.
+