Files
JARVIS/docs/superpowers/plans/2026-03-21-skill-system-implementation.md

28 KiB
Raw Permalink Blame History

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

# 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
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

# 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
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

# 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
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

# 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:

from app.routers.skill import router as skill_router
  • Step 3: Register in main.py

Add import and include_router:

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
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

# 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:

skill_context = build_skill_context(agent_type)
# Append to system message
  • Step 3: Commit
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

// 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<AxiosResponse<Skill[]>> => {
    return api.get('/api/skills', { params })
  },

  get: (id: string): Promise<AxiosResponse<Skill>> => {
    return api.get(`/api/skills/${id}`)
  },

  create: (data: SkillCreate): Promise<AxiosResponse<Skill>> => {
    return api.post('/api/skills', data)
  },

  update: (id: string, data: SkillUpdate): Promise<AxiosResponse<Skill>> => {
    return api.put(`/api/skills/${id}`, data)
  },

  delete: (id: string): Promise<AxiosResponse<void>> => {
    return api.delete(`/api/skills/${id}`)
  },
}
  • Step 2: Commit
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

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
import { skillApi, type Skill } from '@/api/skill'
import { Bot, Plus, Edit2, Trash2, Eye, EyeOff, Copy } from 'lucide-vue-next'

const skills = ref<Skill[]>([])
const loading = ref(false)
const showModal = ref(false)
const editingSkill = ref<Skill | null>(null)

const form = ref({
  name: '',
  description: '',
  instructions: '',
  agent_type: 'planner',
  tools: [] as string[],
  visibility: 'private' as const,
  is_active: true,
})

const agentTypes = [
  { value: 'master', label: 'Master' },
  { value: 'planner', label: 'Planner' },
  { value: 'executor', label: 'Executor' },
  { value: 'librarian', label: 'Librarian' },
  { value: 'analyst', label: 'Analyst' },
]

const availableTools = [
  'search_knowledge', 'hybrid_search', 'get_knowledge_graph_context',
  'build_knowledge_graph', 'get_tasks', 'create_task', 'update_task_status',
  'get_forum_posts', 'create_forum_post', 'scan_forum_for_instructions',
]

onMounted(loadSkills)

async function loadSkills() {
  loading.value = true
  try {
    const res = await skillApi.list()
    skills.value = res.data
  } catch (e) {
    console.error('加载失败', e)
  } finally {
    loading.value = false
  }
}

function openCreate() {
  editingSkill.value = null
  form.value = {
    name: '',
    description: '',
    instructions: '',
    agent_type: 'planner',
    tools: [],
    visibility: 'private',
    is_active: true,
  }
  showModal.value = true
}

function openEdit(skill: Skill) {
  editingSkill.value = skill
  form.value = {
    name: skill.name,
    description: skill.description || '',
    instructions: skill.instructions,
    agent_type: skill.agent_type,
    tools: skill.tools || [],
    visibility: skill.visibility,
    is_active: skill.is_active,
  }
  showModal.value = true
}

async function saveSkill() {
  try {
    if (editingSkill.value) {
      await skillApi.update(editingSkill.value.id, form.value)
    } else {
      await skillApi.create(form.value)
    }
    showModal.value = false
    await loadSkills()
  } catch (e) {
    console.error('保存失败', e)
  }
}

async function toggleActive(skill: Skill) {
  try {
    await skillApi.update(skill.id, { is_active: !skill.is_active })
    await loadSkills()
  } catch (e) {
    console.error('更新失败', e)
  }
}

async function deleteSkill(skill: Skill) {
  if (!confirm(`确定删除 Skill "${skill.name}"`)) return
  try {
    await skillApi.delete(skill.id)
    await loadSkills()
  } catch (e) {
    console.error('删除失败', e)
  }
}

function copySkill(skill: Skill) {
  form.value = {
    name: skill.name + '_copy',
    description: skill.description || '',
    instructions: skill.instructions,
    agent_type: skill.agent_type,
    tools: skill.tools || [],
    visibility: 'private',
    is_active: true,
  }
  editingSkill.value = null
  showModal.value = true
}

function getAgentLabel(type: string) {
  return agentTypes.find(a => a.value === type)?.label || type
}
</script>

<template>
  <div class="skill-view">
    <div class="view-header">
      <h2>Skill 市场</h2>
      <button class="primary-btn" @click="openCreate">
        <Plus :size="14" /> 新建 Skill
      </button>
    </div>

    <div v-if="loading" class="loading">加载中...</div>

    <div v-else class="skill-list">
      <div v-for="skill in skills" :key="skill.id" class="skill-card">
        <div class="skill-header">
          <div class="skill-name">
            <Bot :size="14" />
            {{ skill.name }}
          </div>
          <div class="skill-badge" :class="skill.agent_type">
            {{ getAgentLabel(skill.agent_type) }}
          </div>
        </div>

        <div class="skill-desc">{{ skill.description || '无描述' }}</div>

        <div class="skill-tools">
          <span v-for="tool in skill.tools" :key="tool" class="tool-tag">
            {{ tool }}
          </span>
          <span v-if="!skill.tools?.length" class="tool-tag empty">无工具</span>
        </div>

        <div class="skill-footer">
          <span class="skill-visibility">{{ skill.visibility }}</span>
          <div class="skill-actions">
            <button class="icon-btn" @click="toggleActive(skill)" :title="skill.is_active ? '禁用' : '启用'">
              <component :is="skill.is_active ? Eye : EyeOff" :size="14" />
            </button>
            <button class="icon-btn" @click="copySkill(skill)" title="复制">
              <Copy :size="14" />
            </button>
            <button class="icon-btn" @click="openEdit(skill)" title="编辑">
              <Edit2 :size="14" />
            </button>
            <button class="icon-btn danger" @click="deleteSkill(skill)" title="删除">
              <Trash2 :size="14" />
            </button>
          </div>
        </div>
      </div>

      <div v-if="skills.length === 0" class="empty-state">
        暂无 Skill点击上方按钮创建
      </div>
    </div>

    <!-- Modal -->
    <div v-if="showModal" class="modal-overlay" @click.self="showModal = false">
      <div class="modal">
        <h3>{{ editingSkill ? '编辑 Skill' : '新建 Skill' }}</h3>

        <div class="form-group">
          <label>名称</label>
          <input v-model="form.name" placeholder="Skill 名称" />
        </div>

        <div class="form-group">
          <label>描述</label>
          <input v-model="form.description" placeholder="供 LLM 理解的描述" />
        </div>

        <div class="form-group">
          <label>适用 Agent</label>
          <select v-model="form.agent_type">
            <option v-for="a in agentTypes" :key="a.value" :value="a.value">
              {{ a.label }}
            </option>
          </select>
        </div>

        <div class="form-group">
          <label>引用工具</label>
          <div class="tools-grid">
            <label v-for="tool in availableTools" :key="tool" class="checkbox-item">
              <input type="checkbox" :value="tool" v-model="form.tools" />
              {{ tool }}
            </label>
          </div>
        </div>

        <div class="form-group">
          <label>指令模板</label>
          <textarea v-model="form.instructions" rows="6" placeholder="Agent 执行时的指令..."></textarea>
        </div>

        <div class="form-group">
          <label>可见性</label>
          <select v-model="form.visibility">
            <option value="private">私有</option>
            <option value="team">团队共享</option>
            <option value="market">市场</option>
          </select>
        </div>

        <div class="modal-actions">
          <button @click="showModal = false">取消</button>
          <button class="primary-btn" @click="saveSkill">保存</button>
        </div>
      </div>
    </div>
  </div>
</template>

<style scoped>
.skill-view {
  padding: 24px;
}

.view-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 24px;
}

.view-header h2 {
  font-size: 18px;
  color: var(--text-primary);
}

.primary-btn {
  display: flex;
  align-items: center;
  gap: 6px;
  padding: 8px 16px;
  background: var(--accent-cyan-dim);
  border: 1px solid var(--border-mid);
  border-radius: var(--radius-md);
  color: var(--accent-cyan);
  font-size: 12px;
  cursor: pointer;
}

.skill-list {
  display: grid;
  gap: 16px;
}

.skill-card {
  background: var(--bg-card);
  border: 1px solid var(--border-dim);
  border-radius: var(--radius-lg);
  padding: 16px;
}

.skill-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin-bottom: 8px;
}

.skill-name {
  display: flex;
  align-items: center;
  gap: 8px;
  font-weight: 600;
  color: var(--text-primary);
}

.skill-badge {
  font-size: 10px;
  padding: 2px 8px;
  border-radius: 10px;
  background: var(--accent-cyan-dim);
  color: var(--accent-cyan);
}

.skill-badge.master { background: rgba(249,168,37,0.1); color: var(--accent-amber); }
.skill-badge.executor { background: rgba(255,71,87,0.1); color: var(--accent-red); }

.skill-desc {
  font-size: 12px;
  color: var(--text-secondary);
  margin-bottom: 12px;
}

.skill-tools {
  display: flex;
  flex-wrap: wrap;
  gap: 6px;
  margin-bottom: 12px;
}

.tool-tag {
  font-size: 10px;
  padding: 2px 8px;
  background: rgba(0,245,212,0.05);
  border: 1px solid var(--border-dim);
  border-radius: 4px;
  color: var(--text-dim);
}

.skill-footer {
  display: flex;
  justify-content: space-between;
  align-items: center;
}

.skill-visibility {
  font-size: 10px;
  color: var(--text-muted);
}

.skill-actions {
  display: flex;
  gap: 4px;
}

.icon-btn {
  padding: 4px;
  background: none;
  border: none;
  color: var(--text-dim);
  cursor: pointer;
  border-radius: 4px;
}

.icon-btn:hover {
  background: var(--bg-panel);
  color: var(--text-primary);
}

.icon-btn.danger:hover {
  background: rgba(255,71,87,0.1);
  color: var(--accent-red);
}

.empty-state {
  text-align: center;
  color: var(--text-dim);
  padding: 40px;
}

.loading {
  text-align: center;
  color: var(--text-dim);
  padding: 40px;
}

/* Modal styles */
.modal-overlay {
  position: fixed;
  inset: 0;
  background: rgba(0,0,0,0.6);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 100;
}

.modal {
  background: var(--bg-card);
  border: 1px solid var(--border-mid);
  border-radius: var(--radius-lg);
  padding: 24px;
  width: 500px;
  max-height: 80vh;
  overflow-y: auto;
}

.modal h3 {
  margin-bottom: 20px;
  font-size: 16px;
}

.form-group {
  margin-bottom: 16px;
}

.form-group label {
  display: block;
  font-size: 12px;
  color: var(--text-secondary);
  margin-bottom: 6px;
}

.form-group input,
.form-group select,
.form-group textarea {
  width: 100%;
  padding: 8px 12px;
  background: var(--bg-panel);
  border: 1px solid var(--border-dim);
  border-radius: var(--radius-md);
  color: var(--text-primary);
  font-size: 13px;
}

.tools-grid {
  display: grid;
  grid-template-columns: repeat(2, 1fr);
  gap: 8px;
}

.checkbox-item {
  display: flex;
  align-items: center;
  gap: 6px;
  font-size: 12px;
  color: var(--text-secondary);
  cursor: pointer;
}

.modal-actions {
  display: flex;
  justify-content: flex-end;
  gap: 12px;
  margin-top: 20px;
}
</style>
  • Step 2: Commit
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:

{
  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:

{ name: 'Skill 市场', path: '/skills', icon: Bot },

Also add Bot to the import from lucide-vue-next.

  • Step 3: Commit
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:

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
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