2026-01-25 19:27:44 +08:00

832 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""
Skill 管理 API 路由
提供:
1. Skill 的 CRUD 操作
2. Agent-Skill 生命周期端点
3. AI 辅助 Skill 创建(完整流程)
4. Skill 选择和路由LLM 与 Skills 结合)
5. Agent 工作流配置
"""
from fastapi import APIRouter, Depends, HTTPException, status
from typing import List, Optional, Dict, Any
from pydantic import BaseModel
from app.models.skill import (
Skill,
SkillCreate,
SkillUpdate,
SkillConfigUpdate,
SkillTestRequest,
SkillTestResponse,
SkillGenerateRequest,
SkillGenerateResponse,
)
from app.models.skill_integration import (
SkillMetadata,
SkillTriggerType,
SkillStructure,
SkillGenerationRequest,
SkillGenerationResponse,
SkillAnalysis,
SkillPlanning,
SkillRefinementRequest,
SkillRefinementResponse,
SkillSelectionCriteria,
SkillSelectionResult,
SkillExecutionContext,
SkillExecutionResponse,
AgentWorkflowConfig,
AgentWorkflowStep,
)
from app.core.skills.skill_manager import get_skill_manager, SkillManager
from app.core.llm.glm_client import get_glm_client, GLMClient
from app.utils.logger import get_logger
logger = get_logger(__name__)
router = APIRouter(prefix="/skills", tags=["Skill管理"])
# ============================================================================
# 基础 CRUD 端点
# ============================================================================
@router.get("/", response_model=List[Skill])
async def list_skills(
skill_type: Optional[str] = None,
category: Optional[str] = None,
skill_manager: SkillManager = Depends(get_skill_manager)
):
"""
列出所有 Skills
- **skill_type**: 筛选类型 (builtin/user)
- **category**: 筛选分类
"""
skills = await skill_manager.list_skills(skill_type=skill_type, category=category)
return skills
@router.get("/{skill_id}", response_model=Skill)
async def get_skill(
skill_id: str,
skill_manager: SkillManager = Depends(get_skill_manager)
):
"""获取 Skill 详情"""
skill = await skill_manager.load_skill(skill_id)
if not skill:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Skill 不存在: {skill_id}"
)
return skill
@router.post("/", response_model=Skill, status_code=status.HTTP_201_CREATED)
async def create_skill(
skill_data: SkillCreate,
skill_manager: SkillManager = Depends(get_skill_manager)
):
"""创建新的用户 Skill"""
try:
skill = await skill_manager.create_user_skill(skill_data)
return skill
except Exception as e:
logger.error(f"创建 Skill 失败: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"创建 Skill 失败: {str(e)}"
)
@router.put("/{skill_id}", response_model=Skill)
async def update_skill(
skill_id: str,
skill_data: SkillUpdate,
skill_manager: SkillManager = Depends(get_skill_manager)
):
"""更新 Skill 内容"""
skill = await skill_manager.update_user_skill(skill_id, skill_data)
if not skill:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Skill 不存在或不允许更新: {skill_id}"
)
return skill
@router.put("/{skill_id}/config", response_model=Skill)
async def update_skill_config(
skill_id: str,
config_update: SkillConfigUpdate,
skill_manager: SkillManager = Depends(get_skill_manager)
):
"""更新 Skill 配置"""
skill = await skill_manager.load_skill(skill_id)
if not skill:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Skill 不存在: {skill_id}"
)
# 更新配置
if config_update.preset is not None:
skill.config.preset = config_update.preset
if config_update.parameters is not None:
skill.config.parameters.update(config_update.parameters)
if config_update.weights is not None:
skill.config.weights = config_update.weights
return skill
@router.post("/{skill_id}/test", response_model=SkillTestResponse)
async def test_skill(
skill_id: str,
test_request: SkillTestRequest,
skill_manager: SkillManager = Depends(get_skill_manager),
glm_client: GLMClient = Depends(get_glm_client)
):
"""
测试 Skill
使用 Skill 的行为指导来处理测试输入,返回 LLM 的响应
"""
skill = await skill_manager.load_skill(skill_id)
if not skill:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Skill 不存在: {skill_id}"
)
try:
# 使用 GLM 客户端的 chat_with_skill 方法
response = await glm_client.chat_with_skill(
skill_behavior=skill.behavior_guide,
user_input=test_request.test_input,
context=test_request.context or {},
temperature=test_request.temperature
)
return SkillTestResponse(
skill_id=skill_id,
skill_name=skill.name,
response=response
)
except Exception as e:
logger.error(f"测试 Skill 失败: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"测试 Skill 失败: {str(e)}"
)
@router.delete("/{skill_id}", status_code=status.HTTP_204_NO_CONTENT)
async def delete_skill(
skill_id: str,
skill_manager: SkillManager = Depends(get_skill_manager)
):
"""删除用户 Skill"""
success = await skill_manager.delete_user_skill(skill_id)
if not success:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Skill 不存在或不允许删除: {skill_id}"
)
return None
@router.post("/{skill_id}/reload", response_model=Skill)
async def reload_skill(
skill_id: str,
skill_manager: SkillManager = Depends(get_skill_manager)
):
"""重新加载 Skill清除缓存"""
skill = await skill_manager.reload_skill(skill_id)
if not skill:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Skill 不存在: {skill_id}"
)
return skill
@router.get("/categories/list", response_model=List[str])
async def list_categories(
skill_manager: SkillManager = Depends(get_skill_manager)
):
"""列出所有 Skill 分类"""
skills = await skill_manager.list_skills()
categories = set(skill.category for skill in skills)
return sorted(list(categories))
@router.post("/generate", response_model=SkillGenerateResponse)
async def generate_skill_with_ai(
request: SkillGenerateRequest,
skill_manager: SkillManager = Depends(get_skill_manager),
glm_client: GLMClient = Depends(get_glm_client)
):
"""
使用 AI 生成 Skill类似 Claude Code 的 skill-creator
流程:
1. 加载 skill-creator 的行为指导
2. 将用户需求和 skill-creator 标准一起发送给 GLM-4.7
3. AI 生成符合标准的 Skill 内容
Args:
request: 包含用户描述、分类、标签的请求
Returns:
生成的 Skill 内容和建议信息
"""
try:
# 1. 加载 skill-creator 的行为指导
skill_creator = await skill_manager.load_skill("skill-creator")
if not skill_creator:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="skill-creator 未找到,请确保内置 Skills 正确安装"
)
# 2. 构建提示词
user_requirements = f"""用户想要创建的 Skill 描述:
{request.description}
"""
if request.category:
user_requirements += f"\n指定分类:{request.category}"
if request.tags:
user_requirements += f"\n指定标签:{', '.join(request.tags)}"
system_prompt = f"""你是一个专业的 Skill 创建专家。
以下是 skill-creator 的行为指导(关于如何创建有效 Skill 的指南):
{'' * 60}
{skill_creator.behavior_guide}
{'' * 60}
你的任务是根据用户的需求,创建一个符合上述标准的 Skill。
**重要要求**
1. SKILL.md 必须以 YAML frontmatter 开始,包含 name 和 description 字段
2. description 应该清晰说明此 Skill 的用途和使用场景
3. 行为指导部分应该简洁、具体,避免冗余的解释
4. 使用 markdown 格式
5. 返回完整的 SKILL.md 内容
请以 JSON 格式返回结果,包含以下字段:
- suggested_id: 建议 Skill IDkebab-case如 dialogue-writer-ancient
- suggested_name: 建议 Skill 名称(简短中文)
- skill_content: 完整的 SKILL.md 内容
- category: 分类(如"编剧""审核""通用"等)
- suggested_tags: 建议标签数组
- explanation: 对生成的 Skill 的简要说明(中文)
"""
# 3. 调用 GLM-4.7 生成
response = await glm_client.chat(
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": user_requirements}
],
temperature=request.temperature
)
# 4. 解析响应
import json
import re
response_text = response["choices"][0]["message"]["content"]
# 尝试提取 JSON
json_match = re.search(r'\{[\s\S]*\}', response_text)
if json_match:
result = json.loads(json_match.group())
else:
# 如果没有找到 JSON创建一个默认响应
result = {
"suggested_id": "custom-skill",
"suggested_name": "自定义 Skill",
"skill_content": response_text,
"category": request.category or "通用",
"suggested_tags": request.tags or ["自定义"],
"explanation": "AI 生成的 Skill 内容"
}
return SkillGenerateResponse(**result)
except HTTPException:
raise
except Exception as e:
logger.error(f"AI 生成 Skill 失败: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"AI 生成 Skill 失败: {str(e)}"
)
@router.get("/{skill_id}/with-references", response_model=dict)
async def get_skill_with_references(
skill_id: str,
skill_manager: SkillManager = Depends(get_skill_manager),
include_references: bool = True
):
"""
获取 Skill 及其参考文件
这是实现参考文件融入 LLM 的关键端点:
- 返回 Skill 对象
- 返回所有参考文件的内容
Args:
skill_id: Skill ID
include_references: 是否包含参考文件内容
"""
try:
skill = await skill_manager.load_skill(skill_id)
if not skill:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Skill 不存在: {skill_id}"
)
result = {"skill": skill.model_dump()}
# 加载参考文件(如果需要)
if include_references:
references = await skill_manager.load_skill_references(skill.id)
result["references"] = references
return result
except HTTPException:
raise
except Exception as e:
logger.error(f"获取 Skill 失败: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"获取 Skill 失败: {str(e)}"
)
@router.post("/analyze", response_model=SkillAnalysis)
async def analyze_skill_requirements(
request: SkillGenerationRequest,
skill_manager: SkillManager = Depends(get_skill_manager),
glm_client: GLMClient = Depends(get_glm_client)
):
"""
分析 Skill 创建需求(完整流程 Step 1: Understanding
Args:
request: 包含用户意图、描述、用例的请求
Returns:
SkillAnalysis: 需求分析结果
"""
try:
# Step 1: 加载 skill-creator
skill_creator = await skill_manager.load_skill("skill-creator")
if not skill_creator:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="skill-creator 未找到,请确保内置 Skills 正确安装"
)
# Step 2: 需求分析
user_intent = request.user_intent or "创建新的 Skill"
system_prompt = f"""你是一个专业的 Skill 创建分析师。
以下是 skill-creator 的行为指导(关于如何创建有效 Skill 的指南):
{'' * 60}
{skill_creator.behavior_guide}
{'' * 60}
你的任务是分析用户的需求:{user_intent}
请从以下几个方面分析:
1. **核心功能**:这个 Skill 主要用于什么?
2. **目标用户**:谁会使用这个 Skill
3. **技术要求**:前端、后端、数据分析等
4. **特殊要求**:多语言支持、高精度输出、实时处理
分析用户需求,并提出 Skill 创作建议。
请以 JSON 格式返回分析结果,包含以下字段:
- intent_analysis: 用户的意图分析
- suggested_category: 建议的分类(如 "dialogue", "analysis", "writing"
- suggested_features: 建议的核心功能列表
- complexity_assessment: 复杂度评估simple/medium/complex
- recommended_approach: 推荐的实现方式
"""
# 构建分析请求
analysis_request = {
"user_intent": user_intent,
"user_description": request.user_intent,
"use_cases": request.use_cases or []
}
# 调用 LLM 进行需求分析
analysis_response = await glm_client.chat(
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"分析以下需求:{json.dumps(analysis_request, ensure_ascii=False)}"}
],
temperature=0.5
)
# 解析分析结果
try:
import json
analysis_result = json.loads(analysis_response["choices"][0]["message"]["content"])
# 提取结构化数据
intent_analysis = analysis_result.get("intent_analysis", {})
suggested_category = analysis_result.get("suggested_category", "general")
suggested_features = analysis_result.get("suggested_features", [])
complexity_assessment = analysis_result.get("complexity_assessment", "medium")
recommended_approach = analysis_result.get("recommended_approach", "standard")
except json.JSONDecodeError:
# 如果无法解析,使用默认分析
intent_analysis = {"primary": "创建新的 Skill", "details": "用户想要创建自定义 Skill"}
suggested_category = "general"
suggested_features = ["standard_dialogue_creation", "basic_content_editing"]
complexity_assessment = "medium"
recommended_approach = "standard"
return SkillAnalysis(
intent_analysis=intent_analysis,
suggested_category=suggested_category,
suggested_features=suggested_features,
complexity_assessment=complexity_assessment,
recommended_approach=recommended_approach
)
except HTTPException:
raise
except Exception as e:
logger.error(f"需求分析失败: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"需求分析失败: {str(e)}"
)
# ============================================================================
# Skill 选择和路由LLM 自动选择合适的 Skills
# ============================================================================
@router.post("/select-skills", response_model=SkillSelectionResult)
async def select_skills_for_context(
criteria: SkillSelectionCriteria,
skill_manager: SkillManager = Depends(get_skill_manager),
glm_client: GLMClient = Depends(get_glm_client)
):
"""
LLM 根据描述自动选择最合适的 Skills
流程:
1. 分析用户输入
2. 匹配所有 Skills 的 metadata
3. 评分和排序
4. 返回选择结果
Args:
criteria: 选择条件(用户意图、上下文、约束)
"""
try:
# 加载所有可用 Skills
all_skills = await skill_manager.list_skills(skill_type="user")
# 提取 Skills 的 metadata
skills_metadata = []
for skill in all_skills:
metadata = {
"id": skill.id,
"name": skill.name,
"description": skill.description,
"category": skill.category,
"tags": skill.tags,
"keywords": skill.description.split(),
"capabilities": skill.behavior_guide[:200] if skill.behavior_guide else ""
}
skills_metadata.append(metadata)
# 构建提示词
context_info = ""
if criteria.context:
context_info = f"\n上下文信息:{json.dumps(criteria.context, ensure_ascii=False)}\n"
if criteria.exclude_skills:
exclude_info = f"\n排除的 Skills{', '.join(criteria.exclude_skills)}\n"
system_prompt = f"""你是一个智能 Skill 选择助手。
以下是可以使用的 Skills 及其元数据:
{'' * 60}
"""
for i, skill_meta in enumerate(skills_metadata, 1):
system_prompt += f"\n{i+1}. {skill_meta['name']}: {skill_meta['description']}\n"
if skill_meta['capabilities']:
system_prompt += f"\n 能力:{skill_meta['capabilities']}\n"
system_prompt += f"\n"
system_prompt += f"""你的任务是根据用户的需求和偏好,从以下 Skills 中选择最合适的。
用户需求:
- {criteria.user_intent}
{criteria.exclude_info if criteria.exclude_skills else ""}
{criteria.context_info if criteria.context else ""}
{criteria.required_category if criteria.required_category else ""}
{criteria.required_tags if criteria.required_tags else ""}
选择标准:
1. 相关性description/keywords 匹配度)
2. 功能覆盖度capabilities 是否满足需求)
3. 难度适配complexity_assessment 是否匹配)
请返回 JSON 数组,包含选中的 Skill ID 列表和选择理由。
每个元素应包含:
- skill_id: Skill ID
- score: 匹配分数0-100
- reason: 选择理由
"""
# 调用 LLM 进行选择
selection_response = await glm_client.chat(
messages=[
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"请分析以下 Skills并返回最合适的 3-5 个:{json.dumps(skills_metadata, ensure_ascii=False)}"}
],
temperature=0.3
)
# 解析选择结果
try:
import json
selected_ids = json.loads(selection_response["choices"][0]["message"]["content"])
except:
selected_ids = []
# 如果是复杂场景,需要 LLM 进行智能选择
if len(skills_metadata) > 5 or criteria.user_intent == "复杂需求分析":
# 使用结构化输出确保可解析性
structured_prompt = """
请以以下 JSON 格式返回选择,必须是可以解析的 JSON 数组:
[
{{"skill_id": "...", "score": 85, "reason": "..."}},
...
]
确保输出是有效的 JSON 格式。
"""
selection_response = await glm_client.chat(
messages=[
{"role": "system", "content": structured_prompt},
{"role": "user", "content": f"用户需求:{criteria.user_intent or '标准 Skill 创建'}\n\n\nAvailable Skills: {json.dumps(skills_metadata, ensure_ascii=False)}"}
],
temperature=0.2
)
try:
selected_ids = json.loads(selection_response["choices"][0]["message"]["content"])
except:
selected_ids = []
return SkillSelectionResult(
selected_skills=[s for s in all_skills if s.id in selected_ids],
selection_reason=f"基于需求分析选择了最相关的 Skills",
confidence_scores={}
)
except HTTPException:
logger.error(f"Skill 选择失败: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"Skill 选择失败: {str(e)}"
)
# ============================================================================
# Agent 工作流配置 API
# ============================================================================
@router.post("/agent-workflow/configure", response_model=AgentWorkflowConfig)
async def configure_agent_workflow(
config: AgentWorkflowConfig,
skill_manager: SkillManager = Depends(get_skill_manager)
):
"""
配置 Agent 工作流的 Skill 使用方式
这是实现 Agent 与 Skill 深度结合的关键配置:
- 定义哪些工作流步骤使用哪些 Skills
- 设置默认温度
- 启用/禁用 Skill 替代
Args:
config: 包含工作流步骤配置
"""
try:
# 加载 skill-creator 获取指导
skill_creator = await skill_manager.load_skill("skill-creator")
# 保存配置
await skill_manager.save_workflow_config(config)
return {
"success": True,
"message": "Agent 工作流配置已更新",
"config": config.model_dump()
}
except Exception as e:
logger.error(f"配置失败: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"配置失败: {str(e)}"
)
# ============================================================================
# 基础 CRUD 端点继续
# ============================================================================
# ============================================================================
# Agent-Skill 生命周期端点 - 使用 skill-creator 脚本
# ============================================================================
class SkillInitRequest(BaseModel):
"""初始化 Skill 模板请求"""
skill_name: str # Skill 名称hyphen-case
output_path: Optional[str] = None # 输出路径
class SkillValidateScriptRequest(BaseModel):
"""脚本验证 Skill 请求"""
skill_id: str # 要验证的 Skill ID
class SkillPackageRequest(BaseModel):
"""打包 Skill 请求"""
skill_id: str # 要打包的 Skill ID
output_dir: Optional[str] = None # 输出目录
@router.post("/init", response_model=dict)
async def init_skill_template(
request: SkillInitRequest,
skill_manager: SkillManager = Depends(get_skill_manager)
):
"""
初始化 Skill 模板(使用 skill-creator 的 init_skill.py 脚本)
这是 Agent-Skill 生命周期的正确实现:
- 调用 skill-creator 的 init_skill.py 脚本
- 创建标准的 Skill 目录结构SKILL.md、scripts/、references/、assets/
"""
try:
result = await skill_manager.init_skill_template(
skill_name=request.skill_name,
output_path=request.output_path
)
if result["success"]:
logger.info(f"成功初始化 Skill 模板: {request.skill_name}")
return {
"success": True,
"message": f"Skill 模板 '{request.skill_name}' 初始化成功",
"output": result["output"]
}
else:
logger.error(f"初始化 Skill 失败: {result.get('error')}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=result.get("error", "初始化失败")
)
except Exception as e:
logger.error(f"初始化 Skill 异常: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"初始化 Skill 异常: {str(e)}"
)
@router.post("/validate-script", response_model=dict)
async def validate_skill_script(
request: SkillValidateScriptRequest,
skill_manager: SkillManager = Depends(get_skill_manager)
):
"""
验证 Skill 结构(使用 skill-creator 的 quick_validate.py 脚本)
这是 Agent-Skill 生命周期的正确实现:
- 调用 skill-creator 的 quick_validate.py 脚本
- 检查 YAML frontmatter、命名规范等
"""
try:
result = await skill_manager.validate_skill_structure(request.skill_id)
if result["success"]:
logger.info(f"Skill 验证通过: {request.skill_id}")
return {
"success": True,
"valid": True,
"message": result["output"]
}
else:
logger.warning(f"Skill 验证失败: {request.skill_id}")
return {
"success": True,
"valid": False,
"error": result.get("error")
}
except Exception as e:
logger.error(f"验证 Skill 异常: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"验证 Skill 异常: {str(e)}"
)
@router.post("/package", response_model=dict)
async def package_skill(
request: SkillPackageRequest,
skill_manager: SkillManager = Depends(get_skill_manager)
):
"""
打包 Skill使用 skill-creator 的 package_skill.py 脚本)
这是 Agent-Skill 生命周期的正确实现:
- 调用 skill-creator 的 package_skill.py 脚本
- 将 Skill 打包成 .skill 文件用于分发
"""
try:
result = await skill_manager.package_skill(
skill_id=request.skill_id,
output_dir=request.output_dir
)
if result["success"]:
logger.info(f"成功打包 Skill: {request.skill_id}")
return {
"success": True,
"message": f"Skill '{request.skill_id}' 打包成功",
"output": result["output"]
}
else:
logger.error(f"打包 Skill 失败: {request.skill_id}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=result.get("error", "打包失败")
)
except Exception as e:
logger.error(f"打包 Skill 异常: {str(e)}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail=f"打包 Skill 异常: {str(e)}"
)
@router.get("/tools", response_model=List[dict])
async def list_available_tools(
skill_manager: SkillManager = Depends(get_skill_manager)
):
"""
列出所有已注册的工具Agent-Skill 生命周期:注册阶段)
返回所有已注册工具的 JSON Schema用于
- 注入到 LLM System Prompt
- 或作为 API 的 tools 参数
"""
return skill_manager.get_available_tools_for_llm()
@router.post("/execute-tool", response_model=dict)
async def execute_tool(
tool_name: str,
parameters: dict,
skill_manager: SkillManager = Depends(get_skill_manager)
):
"""
执行工具Agent-Skill 生命周期:路由 → 执行 → 反馈)
实现:
- 路由:根据 tool_name 找到对应的 handler
- 执行:调用 handler 执行实际操作
- 反馈:返回结果给调用者(通常是 LLM
这是 Agent-Skill 生命周期的核心实现
"""
result = await skill_manager.route_and_execute_tool(tool_name, parameters)
return result