From fcd13d8d9f7269e1a458b9a3cdf25f49b7713cbb Mon Sep 17 00:00:00 2001 From: "DESKTOP-72TV0V4\\caoxiaozhu" Date: Sat, 21 Mar 2026 11:22:29 +0800 Subject: [PATCH] Add skill system implementation plan --- .../2026-03-21-skill-system-implementation.md | 1117 +++++++++++++++++ 1 file changed, 1117 insertions(+) create mode 100644 docs/superpowers/plans/2026-03-21-skill-system-implementation.md diff --git a/docs/superpowers/plans/2026-03-21-skill-system-implementation.md b/docs/superpowers/plans/2026-03-21-skill-system-implementation.md new file mode 100644 index 0000000..81492a3 --- /dev/null +++ b/docs/superpowers/plans/2026-03-21-skill-system-implementation.md @@ -0,0 +1,1117 @@ +# Skill System Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Build a Skill system where Agent can autonomously call Skills based on LLM judgment. + +**Architecture:** Skill is an "ability plugin" for Agents - a combination of instructions + tools. LLM decides when to use which Skill. Skills are stored in DB, loaded at runtime, and injected into Agent context. + +**Tech Stack:** FastAPI (backend), Vue 3 + Pinia (frontend), SQLAlchemy (ORM), existing LangGraph agent system + +--- + +## File Structure + +### Backend (New Files) +- `backend/app/models/skill.py` - Skill ORM model +- `backend/app/schemas/skill.py` - Pydantic schemas for API +- `backend/app/services/skill_service.py` - Business logic +- `backend/app/routers/skill.py` - API endpoints +- `backend/app/agents/skill_registry.py` - Loads skills for agents at runtime +- `backend/app/agents/skill_executor.py` - Executes skill instructions with tools + +### Backend (Modified Files) +- `backend/app/main.py` - Register skill_router +- `backend/app/routers/__init__.py` - Export skill_router + +### Frontend (New Files) +- `frontend/src/api/skill.ts` - Skill API client +- `frontend/src/views/SkillView.vue` - Skill management UI +- `frontend/src/stores/skill.ts` - Skill state (optional, if needed) + +### Frontend (Modified Files) +- `frontend/src/router/index.ts` - Add /skills route +- `frontend/src/components/SidebarNav.vue` - Add "Skill 市场" nav item + +--- + +## Task 1: Skill Model & Database + +**Files:** +- Create: `backend/app/models/skill.py` +- Modify: `backend/app/database.py` (auto-import) + +- [ ] **Step 1: Create Skill model** + +```python +# backend/app/models/skill.py +from sqlalchemy import Column, String, Text, Boolean, JSON, ForeignKey +from sqlalchemy.orm import relationship +from app.models.base import BaseModel + + +class Skill(BaseModel): + __tablename__ = "skills" + + name = Column(String(100), nullable=False, unique=True, index=True) + description = Column(Text, nullable=True) # 供 LLM 理解用途 + instructions = Column(Text, nullable=False) # Agent 执行时的指令模板 + agent_type = Column(String(50), nullable=False, index=True) # master/planner/executor/librarian/analyst + tools = Column(JSON, default=list) # 引用的工具名称列表 + required_context = Column(JSON, default=list) # 需要的前置数据 + output_format = Column(Text, nullable=True) # 输出格式要求 + visibility = Column(String(20), default="private") # private/team/market + team_id = Column(String(36), ForeignKey("users.id"), nullable=True) + is_active = Column(Boolean, default=True) + owner_id = Column(String(36), ForeignKey("users.id"), nullable=False) + + owner = relationship("User", foreign_keys=[owner_id]) + team = relationship("User", foreign_keys=[team_id]) +``` + +- [ ] **Step 2: Run database migration (auto-create table)** + +Run: `cd backend && python -c "from app.database import engine, Base; import app.models.skill; Base.metadata.create_all(engine); print('Table created')"` + +Expected: "Table created" + +- [ ] **Step 3: Commit** + +```bash +git add backend/app/models/skill.py +git commit -m "feat: add Skill model" +``` + +--- + +## Task 2: Skill Schema (Pydantic) + +**Files:** +- Create: `backend/app/schemas/skill.py` + +- [ ] **Step 1: Create Skill schemas** + +```python +# backend/app/schemas/skill.py +from pydantic import BaseModel +from typing import Optional + + +class SkillCreate(BaseModel): + name: str + description: Optional[str] = None + instructions: str + agent_type: str # master/planner/executor/librarian/analyst + tools: list[str] = [] + required_context: list[str] = [] + output_format: Optional[str] = None + visibility: str = "private" + team_id: Optional[str] = None + is_active: bool = True + + +class SkillUpdate(BaseModel): + name: Optional[str] = None + description: Optional[str] = None + instructions: Optional[str] = None + agent_type: Optional[str] = None + tools: Optional[list[str]] = None + required_context: Optional[list[str]] = None + output_format: Optional[str] = None + visibility: Optional[str] = None + team_id: Optional[str] = None + is_active: Optional[bool] = None + + +class SkillOut(BaseModel): + id: str + name: str + description: Optional[str] + instructions: str + agent_type: str + tools: list[str] + required_context: list[str] + output_format: Optional[str] + visibility: str + team_id: Optional[str] + is_active: bool + owner_id: str + created_at: str + updated_at: str + + model_config = {"from_attributes": True} +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend/app/schemas/skill.py +git commit -m "feat: add Skill Pydantic schemas" +``` + +--- + +## Task 3: Skill Service + +**Files:** +- Create: `backend/app/services/skill_service.py` + +- [ ] **Step 1: Create SkillService class** + +```python +# backend/app/services/skill_service.py +from typing import Optional +from sqlalchemy.ext.asyncio import AsyncSession +from sqlalchemy import select, and_, or_ +from app.models.skill import Skill +from app.models.user import User + + +class SkillService: + def __init__(self, db: AsyncSession): + self.db = db + + async def create(self, user_id: str, data: dict) -> Skill: + skill = Skill( + owner_id=user_id, + **data + ) + self.db.add(skill) + await self.db.commit() + await self.db.refresh(skill) + return skill + + async def get_by_id(self, skill_id: str) -> Optional[Skill]: + result = await self.db.execute(select(Skill).where(Skill.id == skill_id)) + return result.scalar_one_or_none() + + async def list_for_user( + self, + user_id: str, + agent_type: Optional[str] = None, + visibility: Optional[str] = None, + ) -> list[Skill]: + """列出用户可访问的 Skills""" + conditions = [ + or_( + Skill.owner_id == user_id, + Skill.visibility == "market", + and_(Skill.visibility == "team", Skill.team_id == user_id), + ) + ] + if agent_type: + conditions.append(Skill.agent_type == agent_type) + if visibility: + conditions.append(Skill.visibility == visibility) + + result = await self.db.execute( + select(Skill) + .where(*conditions) + .where(Skill.is_active == True) + .order_by(Skill.created_at.desc()) + ) + return list(result.scalars().all()) + + async def update(self, skill_id: str, user_id: str, data: dict) -> Optional[Skill]: + skill = await self.get_by_id(skill_id) + if not skill or skill.owner_id != user_id: + return None + for key, value in data.items(): + if value is not None: + setattr(skill, key, value) + await self.db.commit() + await self.db.refresh(skill) + return skill + + async def delete(self, skill_id: str, user_id: str) -> bool: + skill = await self.get_by_id(skill_id) + if not skill or skill.owner_id != user_id: + return False + await self.db.delete(skill) + await self.db.commit() + return True + + async def get_by_agent_type(self, agent_type: str) -> list[Skill]: + """获取指定 Agent 类型的所有可用 Skills(供 Agent 运行时加载)""" + result = await self.db.execute( + select(Skill) + .where( + and_( + Skill.agent_type == agent_type, + Skill.is_active == True, + Skill.visibility.in_(["market", "private"]), + ) + ) + ) + return list(result.scalars().all()) +``` + +- [ ] **Step 2: Commit** + +```bash +git add backend/app/services/skill_service.py +git commit -m "feat: add SkillService" +``` + +--- + +## Task 4: Skill Router (API Endpoints) + +**Files:** +- Create: `backend/app/routers/skill.py` +- Modify: `backend/app/routers/__init__.py` +- Modify: `backend/app/main.py` + +- [ ] **Step 1: Create skill router** + +```python +# backend/app/routers/skill.py +from fastapi import APIRouter, Depends, HTTPException +from sqlalchemy.ext.asyncio import AsyncSession +from app.database import get_db +from app.models.user import User +from app.routers.auth import get_current_user +from app.schemas.skill import SkillCreate, SkillUpdate, SkillOut +from app.services.skill_service import SkillService + +router = APIRouter(prefix="/api/skills", tags=["Skill"]) + + +@router.post("", response_model=SkillOut, status_code=201) +async def create_skill( + data: SkillCreate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """创建新 Skill""" + svc = SkillService(db) + skill = await svc.create(current_user.id, data.model_dump()) + return skill + + +@router.get("", response_model=list[SkillOut]) +async def list_skills( + agent_type: str | None = None, + visibility: str | None = None, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """列出用户可访问的 Skills""" + svc = SkillService(db) + skills = await svc.list_for_user(current_user.id, agent_type, visibility) + return skills + + +@router.get("/{skill_id}", response_model=SkillOut) +async def get_skill( + skill_id: str, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """获取 Skill 详情""" + svc = SkillService(db) + skill = await svc.get_by_id(skill_id) + if not skill: + raise HTTPException(status_code=404, detail="Skill not found") + return skill + + +@router.put("/{skill_id}", response_model=SkillOut) +async def update_skill( + skill_id: str, + data: SkillUpdate, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """更新 Skill""" + svc = SkillService(db) + skill = await svc.update(skill_id, current_user.id, data.model_dump(exclude_unset=True)) + if not skill: + raise HTTPException(status_code=404, detail="Skill not found or not owned") + return skill + + +@router.delete("/{skill_id}", status_code=204) +async def delete_skill( + skill_id: str, + current_user: User = Depends(get_current_user), + db: AsyncSession = Depends(get_db), +): + """删除 Skill""" + svc = SkillService(db) + deleted = await svc.delete(skill_id, current_user.id) + if not deleted: + raise HTTPException(status_code=404, detail="Skill not found or not owned") +``` + +- [ ] **Step 2: Update routers __init__.py** + +Add to `backend/app/routers/__init__.py`: +```python +from app.routers.skill import router as skill_router +``` + +- [ ] **Step 3: Register in main.py** + +Add import and include_router: +```python +from app.routers.skill import router as skill_router +# ... +app.include_router(skill_router) +``` + +- [ ] **Step 4: Test API** + +Run: `cd backend && python -c "from app.main import app; print('Routes:', [r.path for r in app.routes if 'skill' in r.path])"` + +Expected: Routes containing `/api/skills` + +- [ ] **Step 5: Commit** + +```bash +git add backend/app/routers/skill.py backend/app/routers/__init__.py backend/app/main.py +git commit -m "feat: add Skill API endpoints" +``` + +--- + +## Task 5: Skill Registry (Agent Integration) + +**Files:** +- Create: `backend/app/agents/skill_registry.py` +- Modify: `backend/app/agents/prompts.py` (extend system prompts) + +- [ ] **Step 1: Create skill registry** + +```python +# backend/app/agents/skill_registry.py +""" +Skill Registry - Agent 运行时加载 Skills +""" +from typing import Optional +from app.database import async_session +from app.services.skill_service import SkillService + + +# 缓存:agent_type -> list[Skill] +_skill_cache: dict[str, list] = {} + + +async def load_skills_for_agent(agent_type: str, force_reload: bool = False) -> list: + """加载指定 Agent 类型的可用 Skills""" + if not force_reload and agent_type in _skill_cache: + return _skill_cache[agent_type] + + async with async_session() as db: + svc = SkillService(db) + skills = await svc.get_by_agent_type(agent_type) + _skill_cache[agent_type] = skills + return skills + + +def get_skills_for_agent(agent_type: str) -> list: + """同步接口:返回缓存的 Skills(供 Agent 节点调用)""" + return _skill_cache.get(agent_type, []) + + +def build_skill_context(agent_type: str) -> str: + """ + 构建 Skill 上下文,供注入到 Agent 系统提示 + 格式:Skill 名称 + 描述 + 工具列表 + """ + skills = get_skills_for_agent(agent_type) + if not skills: + return "" + + lines = ["\n\n【可用的 Skills】"] + for s in skills: + tools_str = ", ".join(s.tools) if s.tools else "无" + lines.append(f""" +## {s.name} +- 描述: {s.description or '无'} +- 工具: {tools_str} +- 指令: {s.instructions[:200]}...""" if len(s.instructions) > 200 else f"- 指令: {s.instructions}") + + return "\n".join(lines) + + +def clear_cache(): + """清除缓存(配置变更时调用)""" + global _skill_cache + _skill_cache = {} +``` + +- [ ] **Step 2: Update prompts to include skill context** + +Modify `backend/app/agents/graph.py` to inject skill context into each agent's system prompt: + +In each agent node function (planner_node, executor_node, etc.), append skill context: +```python +skill_context = build_skill_context(agent_type) +# Append to system message +``` + +- [ ] **Step 3: Commit** + +```bash +git add backend/app/agents/skill_registry.py +git commit -m "feat: add SkillRegistry for agent integration" +``` + +--- + +## Task 6: Frontend - Skill API Client + +**Files:** +- Create: `frontend/src/api/skill.ts` + +- [ ] **Step 1: Create skill API client** + +```typescript +// frontend/src/api/skill.ts +import api from './index' +import type { AxiosResponse } from 'axios' + +export interface Skill { + id: string + name: string + description: string | null + instructions: string + agent_type: string + tools: string[] + required_context: string[] + output_format: string | null + visibility: 'private' | 'team' | 'market' + team_id: string | null + is_active: boolean + owner_id: string + created_at: string + updated_at: string +} + +export interface SkillCreate { + name: string + description?: string + instructions: string + agent_type: string + tools?: string[] + required_context?: string[] + output_format?: string + visibility?: 'private' | 'team' | 'market' + team_id?: string + is_active?: boolean +} + +export interface SkillUpdate { + name?: string + description?: string + instructions?: string + agent_type?: string + tools?: string[] + required_context?: string[] + output_format?: string + visibility?: 'private' | 'team' | 'market' + team_id?: string + is_active?: boolean +} + +export const skillApi = { + list: (params?: { agent_type?: string; visibility?: string }): Promise> => { + return api.get('/api/skills', { params }) + }, + + get: (id: string): Promise> => { + return api.get(`/api/skills/${id}`) + }, + + create: (data: SkillCreate): Promise> => { + return api.post('/api/skills', data) + }, + + update: (id: string, data: SkillUpdate): Promise> => { + return api.put(`/api/skills/${id}`, data) + }, + + delete: (id: string): Promise> => { + return api.delete(`/api/skills/${id}`) + }, +} +``` + +- [ ] **Step 2: Commit** + +```bash +git add frontend/src/api/skill.ts +git commit -m "feat: add skill API client" +``` + +--- + +## Task 7: Frontend - SkillView Page + +**Files:** +- Create: `frontend/src/views/SkillView.vue` + +- [ ] **Step 1: Create SkillView page** + +```vue + + + + + +``` + +- [ ] **Step 2: Commit** + +```bash +git add frontend/src/views/SkillView.vue +git commit -m "feat: add SkillView page" +``` + +--- + +## Task 8: Frontend - Router & Navigation + +**Files:** +- Modify: `frontend/src/router/index.ts` +- Modify: `frontend/src/components/SidebarNav.vue` + +- [ ] **Step 1: Add route to router** + +In `frontend/src/router/index.ts`, add to children array: +```typescript +{ + path: 'skills', + name: 'skills', + component: () => import('@/views/SkillView.vue'), +}, +``` + +- [ ] **Step 2: Add nav item to SidebarNav** + +In `frontend/src/components/SidebarNav.vue`, add to navItems array: +```typescript +{ name: 'Skill 市场', path: '/skills', icon: Bot }, +``` + +Also add Bot to the import from lucide-vue-next. + +- [ ] **Step 3: Commit** + +```bash +git add frontend/src/router/index.ts frontend/src/components/SidebarNav.vue +git commit -m "feat: add Skill route and navigation" +``` + +--- + +## Task 9: Integration - Inject Skill Context into Agent + +**Files:** +- Modify: `backend/app/agents/graph.py` + +- [ ] **Step 1: Modify agent nodes to include skill context** + +In each agent node function, after creating the system message, append skill context: + +```python +from app.agents.skill_registry import build_skill_context + +async def planner_node(state: AgentState) -> AgentState: + # ... existing code ... + system_msgs: list[BaseMessage] = [SystemMessage(content=PLANNER_SYSTEM_PROMPT)] + + # Inject skill context + skill_ctx = build_skill_context("planner") + if skill_ctx: + system_msgs.append(SystemMessage(content=skill_ctx)) + + # ... rest of code ... +``` + +Apply same pattern to: master_node, executor_node, librarian_node, analyst_node + +- [ ] **Step 2: Commit** + +```bash +git add backend/app/agents/graph.py +git commit -m "feat: inject skill context into agent prompts" +``` + +--- + +## Summary + +| Task | Description | Files | +|------|-------------|-------| +| 1 | Skill Model | `backend/app/models/skill.py` | +| 2 | Skill Schema | `backend/app/schemas/skill.py` | +| 3 | Skill Service | `backend/app/services/skill_service.py` | +| 4 | Skill Router | `backend/app/routers/skill.py`, `main.py` | +| 5 | Skill Registry | `backend/app/agents/skill_registry.py` | +| 6 | Frontend API | `frontend/src/api/skill.ts` | +| 7 | SkillView Page | `frontend/src/views/SkillView.vue` | +| 8 | Router & Nav | `frontend/src/router/index.ts`, `SidebarNav.vue` | +| 9 | Agent Integration | `backend/app/agents/graph.py` | + +--- + +## Verification + +1. **Backend API Test:** + - Start backend: `cd backend && python -m uvicorn app.main:app --reload` + - Test: `curl -X POST http://localhost:8000/api/skills -H "Content-Type: application/json" -d '{"name":"test","instructions":"test","agent_type":"planner"}'` + +2. **Frontend Test:** + - Start frontend: `cd frontend && npm run dev` + - Navigate to `/skills`, verify page loads + +3. **Agent Integration Test:** + - Create a Skill via API + - Send message to chat that triggers the skill's agent type + - Verify skill context appears in agent logs