Files
Agentic-OS/backend/app/api/skills.py
nearxos 0375b20bb4 Enhance MCP development features and introduce skills management
- Added configuration options for requiring human approval before applying LLM-generated MCP patches.
- Updated Docker setup to include skills directory.
- Integrated skills management into the backend, allowing for procedural guides and skill matching.
- Refactored database initialization to apply Alembic migrations.
- Enhanced task approval process to handle MCP patch applications with optional approval.
- Introduced new schemas for skills and updated existing APIs to support skills functionality.

This commit lays the groundwork for improved agent capabilities and better management of MCP development processes.
2026-06-14 22:27:24 +03:00

98 lines
3.1 KiB
Python

"""Agent skills API — list, view, edit, and preview skill matching."""
from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException
from app.core.deps import require_admin, require_readonly
from app.database import get_db
from app.models.user import User
from app.schemas import SkillOut, SkillUpdate, SkillsListOut
from app.services.audit import record_audit
from app.services.skill_registry import (
get_skill,
list_skills_summary,
match_skills,
read_skill_raw,
save_skill_raw,
)
from sqlalchemy.ext.asyncio import AsyncSession
router = APIRouter(prefix="/api/skills", tags=["skills"])
@router.get("", response_model=SkillsListOut)
async def list_skills(_: User = Depends(require_readonly)):
return SkillsListOut(
skills=list_skills_summary(),
path_hint="skills/",
)
@router.post("/preview")
async def preview_skill_match(
body: dict,
_: User = Depends(require_readonly),
):
"""Preview which skills match a task title + issue (and optional rule id)."""
title = str(body.get("title") or "")
issue = str(body.get("issue") or "")
matched_rule_id = body.get("matched_rule_id")
if matched_rule_id is not None:
matched_rule_id = str(matched_rule_id)
matched = match_skills(title, issue, matched_rule_id=matched_rule_id)
return {"matched": [s.as_dict() for s in matched]}
@router.get("/{skill_id}", response_model=SkillOut)
async def get_skill_detail(skill_id: str, _: User = Depends(require_readonly)):
skill = get_skill(skill_id)
if not skill:
raise HTTPException(status_code=404, detail="Skill not found")
return SkillOut(
id=skill["id"],
name=skill["name"],
description=skill["description"],
priority=skill.get("priority") or 0,
rule_ids=list(skill.get("rule_ids") or []),
match=skill.get("match") or {},
markdown=read_skill_raw(skill_id),
path_hint=f"skills/{skill_id}/SKILL.md",
)
@router.put("/{skill_id}", response_model=SkillOut)
async def update_skill(
skill_id: str,
body: SkillUpdate,
db: AsyncSession = Depends(get_db),
user: User = Depends(require_admin),
):
try:
save_skill_raw(skill_id, body.markdown)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
except Exception as exc: # noqa: BLE001
raise HTTPException(status_code=400, detail=f"Invalid skill: {exc}") from exc
await record_audit(
db,
user,
"skills.update",
"skills",
0,
{"skill_id": skill_id, "bytes": len(body.markdown)},
)
skill = get_skill(skill_id)
if not skill:
raise HTTPException(status_code=404, detail="Skill not found")
return SkillOut(
id=skill["id"],
name=skill["name"],
description=skill["description"],
priority=skill.get("priority") or 0,
rule_ids=list(skill.get("rule_ids") or []),
match=skill.get("match") or {},
markdown=read_skill_raw(skill_id),
path_hint=f"skills/{skill_id}/SKILL.md",
)