2026-03-05 01:42:53 +08:00
|
|
|
|
"""Skills 中间件 - 处理 skills 提示词注入、依赖展开、动态激活"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from collections.abc import Callable
|
|
|
|
|
|
from pathlib import PurePosixPath
|
|
|
|
|
|
from typing import Annotated, Any, NotRequired, TypedDict
|
|
|
|
|
|
|
|
|
|
|
|
from deepagents.middleware.skills import SKILLS_SYSTEM_PROMPT
|
|
|
|
|
|
from langchain.agents import AgentState
|
|
|
|
|
|
from langchain.agents.middleware import AgentMiddleware, ModelRequest, ModelResponse
|
|
|
|
|
|
from langchain.tools.tool_node import ToolCallRequest
|
|
|
|
|
|
from langgraph.types import Command
|
|
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
|
|
|
2026-03-06 10:15:58 +08:00
|
|
|
|
from src.agents.common.toolkits import get_all_tool_instances
|
2026-03-05 01:42:53 +08:00
|
|
|
|
from src.repositories.skill_repository import SkillRepository
|
|
|
|
|
|
from src.services.mcp_service import get_enabled_mcp_tools
|
|
|
|
|
|
from src.services.skill_service import _normalize_string_list, is_valid_skill_slug
|
|
|
|
|
|
from src.storage.postgres.manager import pg_manager
|
|
|
|
|
|
from src.utils.logging_config import logger
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 类型定义
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SkillPromptMetadata(TypedDict):
|
|
|
|
|
|
name: str
|
|
|
|
|
|
description: str
|
|
|
|
|
|
path: str
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SkillDependencyNode(TypedDict):
|
|
|
|
|
|
tools: list[str]
|
|
|
|
|
|
mcps: list[str]
|
|
|
|
|
|
skills: list[str]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
# 运行时数据加载函数
|
|
|
|
|
|
# =============================================================================
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _list_skills_from_db(db: AsyncSession | None = None) -> list:
|
|
|
|
|
|
"""从数据库加载 skills 列表"""
|
|
|
|
|
|
if db is not None:
|
|
|
|
|
|
repo = SkillRepository(db)
|
|
|
|
|
|
return await repo.list_all()
|
|
|
|
|
|
|
|
|
|
|
|
async with pg_manager.get_async_session_context() as session:
|
|
|
|
|
|
repo = SkillRepository(session)
|
|
|
|
|
|
return await repo.list_all()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_prompt_metadata(db: AsyncSession | None = None) -> dict[str, SkillPromptMetadata]:
|
|
|
|
|
|
"""获取提示词元数据(直接从数据库加载)"""
|
|
|
|
|
|
skills = await _list_skills_from_db(db)
|
|
|
|
|
|
return {
|
|
|
|
|
|
item.slug: {
|
|
|
|
|
|
"name": item.name,
|
|
|
|
|
|
"description": item.description,
|
|
|
|
|
|
"path": f"/skills/{item.slug}/SKILL.md",
|
|
|
|
|
|
}
|
|
|
|
|
|
for item in skills
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def get_dependency_map(db: AsyncSession | None = None) -> dict[str, SkillDependencyNode]:
|
|
|
|
|
|
"""获取依赖关系映射(直接从数据库加载)"""
|
|
|
|
|
|
skills = await _list_skills_from_db(db)
|
|
|
|
|
|
result: dict[str, SkillDependencyNode] = {}
|
|
|
|
|
|
for item in skills:
|
|
|
|
|
|
result[item.slug] = {
|
|
|
|
|
|
"tools": normalize_selected_skills(item.tool_dependencies or []),
|
|
|
|
|
|
"mcps": normalize_selected_skills(item.mcp_dependencies or []),
|
|
|
|
|
|
"skills": normalize_selected_skills(item.skill_dependencies or []),
|
|
|
|
|
|
}
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def normalize_selected_skills(selected_skills: list[str] | None) -> list[str]:
|
|
|
|
|
|
"""规范化 skills 列表,去重并过滤无效值"""
|
|
|
|
|
|
return _normalize_string_list(selected_skills)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def expand_skill_closure(
|
|
|
|
|
|
slugs: list[str] | None,
|
|
|
|
|
|
dependency_map: dict[str, SkillDependencyNode],
|
|
|
|
|
|
) -> list[str]:
|
|
|
|
|
|
"""展开 skills 依赖闭包,返回包含所有依赖的列表"""
|
|
|
|
|
|
ordered_roots = normalize_selected_skills(slugs)
|
|
|
|
|
|
if not ordered_roots:
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
result: list[str] = []
|
|
|
|
|
|
seen: set[str] = set()
|
|
|
|
|
|
|
|
|
|
|
|
def dfs(slug: str, stack: set[str]) -> None:
|
|
|
|
|
|
if slug in stack:
|
|
|
|
|
|
logger.warning(f"Cycle detected in skill dependencies, skip: {' -> '.join([*stack, slug])}")
|
|
|
|
|
|
return
|
|
|
|
|
|
if slug in seen:
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
node = dependency_map.get(slug)
|
|
|
|
|
|
if not node:
|
|
|
|
|
|
logger.warning(f"Skill dependency target not found in DB, skip: {slug}")
|
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
seen.add(slug)
|
|
|
|
|
|
result.append(slug)
|
|
|
|
|
|
next_stack = set(stack)
|
|
|
|
|
|
next_stack.add(slug)
|
|
|
|
|
|
for dep in node.get("skills", []):
|
|
|
|
|
|
dfs(dep, next_stack)
|
|
|
|
|
|
|
|
|
|
|
|
for root in ordered_roots:
|
|
|
|
|
|
dfs(root, set())
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _activated_skills_reducer(left: list[str] | None, right: list[str] | None) -> list[str]:
|
|
|
|
|
|
"""合并 activated_skills 列表"""
|
|
|
|
|
|
merged: list[str] = []
|
|
|
|
|
|
seen: set[str] = set()
|
|
|
|
|
|
for group in (left or [], right or []):
|
|
|
|
|
|
for value in group:
|
|
|
|
|
|
if not isinstance(value, str):
|
|
|
|
|
|
continue
|
|
|
|
|
|
slug = value.strip()
|
|
|
|
|
|
if not slug or slug in seen:
|
|
|
|
|
|
continue
|
|
|
|
|
|
seen.add(slug)
|
|
|
|
|
|
merged.append(slug)
|
|
|
|
|
|
return merged
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SkillsState(AgentState):
|
|
|
|
|
|
"""Skills 状态定义"""
|
|
|
|
|
|
|
|
|
|
|
|
activated_skills: NotRequired[Annotated[list[str], _activated_skills_reducer]]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class SkillsMiddleware(AgentMiddleware):
|
|
|
|
|
|
"""Skills 中间件 - 处理 skills 提示词注入、依赖展开、动态激活
|
|
|
|
|
|
|
|
|
|
|
|
职责:
|
|
|
|
|
|
- Skills 提示词注入(直接从数据库加载)
|
|
|
|
|
|
- 依赖展开(用户配置 + 动态激活)
|
|
|
|
|
|
- 工具/MCP 动态加载
|
|
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
state_schema = SkillsState
|
|
|
|
|
|
|
|
|
|
|
|
def __init__(
|
|
|
|
|
|
self,
|
|
|
|
|
|
*,
|
|
|
|
|
|
skills_context_name: str = "skills",
|
|
|
|
|
|
enable_skills_prompt: bool = True,
|
|
|
|
|
|
skills_sources_for_prompt: list[str] | None = None,
|
|
|
|
|
|
):
|
|
|
|
|
|
"""初始化中间件
|
|
|
|
|
|
|
|
|
|
|
|
Args:
|
|
|
|
|
|
skills_context_name: 上下文中的 skills 列表字段名称(默认 "skills")
|
|
|
|
|
|
enable_skills_prompt: 是否启用 skills 提示段注入(默认 True)
|
|
|
|
|
|
skills_sources_for_prompt: skills 来源路径(用于提示词展示,默认 ["/skills/"])
|
|
|
|
|
|
"""
|
|
|
|
|
|
super().__init__()
|
|
|
|
|
|
self.skills_context_name = skills_context_name
|
|
|
|
|
|
self.enable_skills_prompt = enable_skills_prompt
|
|
|
|
|
|
self.skills_sources_for_prompt = skills_sources_for_prompt or ["/skills/"]
|
2026-03-06 10:15:58 +08:00
|
|
|
|
# 实例级缓存:避免每次模型调用都查数据库
|
|
|
|
|
|
self._dependency_map_cache: dict[str, SkillDependencyNode] | None = None
|
|
|
|
|
|
self._prompt_metadata_cache: dict[str, SkillPromptMetadata] | None = None
|
|
|
|
|
|
|
|
|
|
|
|
async def _get_dependency_map_cached(self) -> dict[str, SkillDependencyNode]:
|
|
|
|
|
|
"""获取依赖映射(带缓存)"""
|
|
|
|
|
|
if self._dependency_map_cache is None:
|
|
|
|
|
|
self._dependency_map_cache = await get_dependency_map()
|
|
|
|
|
|
return self._dependency_map_cache
|
|
|
|
|
|
|
|
|
|
|
|
async def _get_prompt_metadata_cached(self) -> dict[str, SkillPromptMetadata]:
|
|
|
|
|
|
"""获取提示词元数据(带缓存)"""
|
|
|
|
|
|
if self._prompt_metadata_cache is None:
|
|
|
|
|
|
self._prompt_metadata_cache = await get_prompt_metadata()
|
|
|
|
|
|
return self._prompt_metadata_cache
|
2026-03-05 01:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
async def abefore_agent(self, state: SkillsState, runtime) -> dict[str, Any] | None:
|
|
|
|
|
|
"""在 agent 执行前注入 skills 提示词"""
|
|
|
|
|
|
runtime_context = runtime.context
|
|
|
|
|
|
|
|
|
|
|
|
# 检查是否需要注入
|
|
|
|
|
|
if not self.enable_skills_prompt:
|
|
|
|
|
|
return None
|
|
|
|
|
|
if getattr(runtime_context, "_skills_prompt_injected", False):
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
2026-03-06 10:15:58 +08:00
|
|
|
|
# 从数据库加载 skills 数据(使用缓存)
|
|
|
|
|
|
dependency_map = await self._get_dependency_map_cached()
|
2026-03-05 01:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
# 获取配置的 skills
|
|
|
|
|
|
configured_skills = getattr(runtime_context, self.skills_context_name, None) or []
|
|
|
|
|
|
selected_skills = normalize_selected_skills(configured_skills)
|
|
|
|
|
|
|
|
|
|
|
|
if not selected_skills:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# 计算 visible_skills
|
|
|
|
|
|
visible_skills = expand_skill_closure(selected_skills, dependency_map)
|
|
|
|
|
|
|
|
|
|
|
|
if not visible_skills:
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
# 收集提示词元数据并构建提示段
|
|
|
|
|
|
skills_meta = await self._collect_prompt_metadata(visible_skills)
|
|
|
|
|
|
skills_section = self._build_skills_section(skills_meta)
|
|
|
|
|
|
|
|
|
|
|
|
# 注入提示词
|
|
|
|
|
|
base_prompt = getattr(runtime_context, "system_prompt", "") or ""
|
|
|
|
|
|
merged_prompt = f"{base_prompt}\n\n{skills_section}" if base_prompt else skills_section
|
|
|
|
|
|
setattr(runtime_context, "system_prompt", merged_prompt)
|
|
|
|
|
|
setattr(runtime_context, "_skills_prompt_injected", True)
|
|
|
|
|
|
|
|
|
|
|
|
# 存储 visible_skills 供后续使用
|
|
|
|
|
|
setattr(runtime_context, "_visible_skills", visible_skills)
|
|
|
|
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
async def awrap_model_call(
|
|
|
|
|
|
self, request: ModelRequest, handler: Callable[[ModelRequest], ModelResponse]
|
|
|
|
|
|
) -> ModelResponse:
|
|
|
|
|
|
"""包装模型调用,处理动态激活和依赖展开"""
|
|
|
|
|
|
runtime_context = request.runtime.context
|
|
|
|
|
|
|
2026-03-06 10:15:58 +08:00
|
|
|
|
# 从缓存加载 skills 数据
|
|
|
|
|
|
dependency_map = await self._get_dependency_map_cached()
|
2026-03-05 01:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
# 1. 获取配置的 skills
|
|
|
|
|
|
configured_skills = getattr(runtime_context, self.skills_context_name, None) or []
|
|
|
|
|
|
configured = normalize_selected_skills(configured_skills)
|
|
|
|
|
|
|
|
|
|
|
|
# 2. 获取运行时动态激活的 skills
|
|
|
|
|
|
state = request.state if isinstance(request.state, dict) else {}
|
|
|
|
|
|
activated = state.get("activated_skills", []) or []
|
|
|
|
|
|
if not isinstance(activated, list):
|
|
|
|
|
|
activated = []
|
|
|
|
|
|
|
|
|
|
|
|
# 3. 合并并展开闭包
|
|
|
|
|
|
all_skills = normalize_selected_skills(configured + activated)
|
|
|
|
|
|
visible_skills = expand_skill_closure(all_skills, dependency_map)
|
|
|
|
|
|
|
|
|
|
|
|
# 4. 更新 runtime_context 中的 visible_skills
|
|
|
|
|
|
setattr(runtime_context, "_visible_skills", visible_skills)
|
|
|
|
|
|
|
2026-03-06 10:15:58 +08:00
|
|
|
|
# 5. 构建依赖包(只从直接激活的 skills 获取依赖,不包含闭包展开的依赖)
|
|
|
|
|
|
deps_bundle = await self._build_dependency_bundle(activated)
|
2026-03-05 01:42:53 +08:00
|
|
|
|
|
2026-03-06 10:15:58 +08:00
|
|
|
|
# 6. 加载依赖的工具(普通工具 + MCP 工具)
|
|
|
|
|
|
enabled_tools = []
|
|
|
|
|
|
|
|
|
|
|
|
# 6.1 从 toolkits 获取普通工具
|
|
|
|
|
|
if deps_bundle["tools"]:
|
|
|
|
|
|
all_tools = get_all_tool_instances()
|
|
|
|
|
|
required_tool_names = set(deps_bundle["tools"])
|
|
|
|
|
|
enabled_tools = [t for t in all_tools if t.name in required_tool_names]
|
|
|
|
|
|
|
|
|
|
|
|
# 6.2 获取 MCP 工具
|
|
|
|
|
|
if deps_bundle["mcps"]:
|
|
|
|
|
|
mcp_tools = await self._get_mcp_tools_from_context(
|
2026-03-05 01:42:53 +08:00
|
|
|
|
runtime_context,
|
|
|
|
|
|
extra_mcps=deps_bundle["mcps"],
|
|
|
|
|
|
)
|
2026-03-06 10:15:58 +08:00
|
|
|
|
enabled_tools.extend(mcp_tools)
|
2026-03-05 01:42:53 +08:00
|
|
|
|
|
2026-03-06 10:15:58 +08:00
|
|
|
|
# 合并工具:保留原有工具 + 追加依赖的新工具
|
|
|
|
|
|
if enabled_tools:
|
|
|
|
|
|
existing_tool_names = {t.name for t in request.tools or []}
|
|
|
|
|
|
merged_tools = list(request.tools or [])
|
|
|
|
|
|
for t in enabled_tools:
|
|
|
|
|
|
if t.name not in existing_tool_names:
|
|
|
|
|
|
merged_tools.append(t)
|
|
|
|
|
|
request = request.override(tools=merged_tools)
|
2026-03-05 01:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
return await handler(request)
|
|
|
|
|
|
|
2026-03-06 10:15:58 +08:00
|
|
|
|
async def _build_dependency_bundle(self, activated_skills: list[str]) -> dict[str, list[str]]:
|
|
|
|
|
|
"""根据直接激活的 skills 构建依赖包(不包含闭包展开的依赖)"""
|
|
|
|
|
|
dependency_map = await self._get_dependency_map_cached()
|
2026-03-05 01:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
tools: list[str] = []
|
|
|
|
|
|
mcps: list[str] = []
|
|
|
|
|
|
seen_tools: set[str] = set()
|
|
|
|
|
|
seen_mcps: set[str] = set()
|
|
|
|
|
|
|
2026-03-06 10:15:58 +08:00
|
|
|
|
for slug in activated_skills:
|
2026-03-05 01:42:53 +08:00
|
|
|
|
dep = dependency_map.get(slug, {})
|
|
|
|
|
|
for tool_name in dep.get("tools", []):
|
|
|
|
|
|
if tool_name in seen_tools:
|
|
|
|
|
|
continue
|
|
|
|
|
|
seen_tools.add(tool_name)
|
|
|
|
|
|
tools.append(tool_name)
|
|
|
|
|
|
for mcp_name in dep.get("mcps", []):
|
|
|
|
|
|
if mcp_name in seen_mcps:
|
|
|
|
|
|
continue
|
|
|
|
|
|
seen_mcps.add(mcp_name)
|
|
|
|
|
|
mcps.append(mcp_name)
|
|
|
|
|
|
|
2026-03-06 10:15:58 +08:00
|
|
|
|
return {"tools": tools, "mcps": mcps, "skills": activated_skills}
|
2026-03-05 01:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
async def _collect_prompt_metadata(self, slugs: list[str]) -> list[SkillPromptMetadata]:
|
|
|
|
|
|
"""收集指定 slugs 的提示词元数据"""
|
2026-03-06 10:15:58 +08:00
|
|
|
|
prompt_metadata = await self._get_prompt_metadata_cached()
|
2026-03-05 01:42:53 +08:00
|
|
|
|
|
|
|
|
|
|
result: list[SkillPromptMetadata] = []
|
|
|
|
|
|
seen: set[str] = set()
|
|
|
|
|
|
|
|
|
|
|
|
for slug in slugs:
|
|
|
|
|
|
if not isinstance(slug, str):
|
|
|
|
|
|
continue
|
|
|
|
|
|
normalized = slug.strip()
|
|
|
|
|
|
if not normalized or normalized in seen:
|
|
|
|
|
|
continue
|
|
|
|
|
|
seen.add(normalized)
|
|
|
|
|
|
|
|
|
|
|
|
item = prompt_metadata.get(normalized)
|
|
|
|
|
|
if not item:
|
|
|
|
|
|
logger.debug(f"Skill slug not found in prompt metadata, skip: {normalized}")
|
|
|
|
|
|
continue
|
|
|
|
|
|
result.append(dict(item))
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
2026-03-06 10:15:58 +08:00
|
|
|
|
async def _get_mcp_tools_from_context(
|
2026-03-05 01:42:53 +08:00
|
|
|
|
self,
|
|
|
|
|
|
context,
|
|
|
|
|
|
*,
|
|
|
|
|
|
extra_mcps: list[str] | None = None,
|
|
|
|
|
|
) -> list:
|
2026-03-06 10:15:58 +08:00
|
|
|
|
"""从上下文配置中获取 MCP 工具列表"""
|
2026-03-05 01:42:53 +08:00
|
|
|
|
import asyncio
|
|
|
|
|
|
|
2026-03-06 10:15:58 +08:00
|
|
|
|
# MCP 工具(并行加载)
|
2026-03-05 01:42:53 +08:00
|
|
|
|
mcps = getattr(context, "mcps", None) or []
|
|
|
|
|
|
all_mcp_names: list[str] = []
|
|
|
|
|
|
for server_name in mcps:
|
|
|
|
|
|
if isinstance(server_name, str):
|
|
|
|
|
|
all_mcp_names.append(server_name)
|
|
|
|
|
|
for server_name in extra_mcps or []:
|
|
|
|
|
|
if isinstance(server_name, str):
|
|
|
|
|
|
all_mcp_names.append(server_name)
|
|
|
|
|
|
|
|
|
|
|
|
# 去重
|
|
|
|
|
|
unique_mcp_names = list(dict.fromkeys(all_mcp_names))
|
|
|
|
|
|
|
|
|
|
|
|
async def load_mcp_tools(server_name: str) -> list:
|
|
|
|
|
|
"""加载单个 MCP 服务器的工具"""
|
|
|
|
|
|
try:
|
|
|
|
|
|
mcp_tools = await get_enabled_mcp_tools(server_name)
|
|
|
|
|
|
if not mcp_tools:
|
|
|
|
|
|
logger.warning(f"SkillsMiddleware: mcp dependency unavailable, skip: {server_name}")
|
|
|
|
|
|
return mcp_tools
|
|
|
|
|
|
except Exception as e:
|
|
|
|
|
|
logger.warning(f"SkillsMiddleware: failed to load mcp dependency '{server_name}': {e}")
|
|
|
|
|
|
return []
|
|
|
|
|
|
|
|
|
|
|
|
# 并行加载所有 MCP 工具
|
|
|
|
|
|
results = await asyncio.gather(*[load_mcp_tools(name) for name in unique_mcp_names])
|
2026-03-06 10:15:58 +08:00
|
|
|
|
selected_tools = []
|
2026-03-05 01:42:53 +08:00
|
|
|
|
for tools in results:
|
|
|
|
|
|
selected_tools.extend(tools)
|
|
|
|
|
|
|
|
|
|
|
|
return selected_tools
|
|
|
|
|
|
|
|
|
|
|
|
def _process_tool_call_result(self, result: Any, request: ToolCallRequest) -> Any:
|
|
|
|
|
|
"""处理工具调用结果,检查并处理 skill 动态激活"""
|
|
|
|
|
|
if request.tool_call.get("name") != "read_file":
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
args = request.tool_call.get("args") or {}
|
|
|
|
|
|
file_path = args.get("file_path") if isinstance(args, dict) else None
|
|
|
|
|
|
slug = self._extract_skill_slug_from_skill_md_path(file_path)
|
|
|
|
|
|
|
|
|
|
|
|
if not slug:
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
if not self._is_visible_skill_slug(request, slug):
|
|
|
|
|
|
logger.warning(f"SkillsMiddleware: deny skill activation for invisible slug: {slug}")
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
logger.debug(f"SkillsMiddleware: activated skill by read_file: {slug}")
|
|
|
|
|
|
return self._merge_activated_skill_update(result, slug)
|
|
|
|
|
|
|
|
|
|
|
|
async def awrap_tool_call(
|
|
|
|
|
|
self,
|
|
|
|
|
|
request: ToolCallRequest,
|
|
|
|
|
|
handler: Callable[[ToolCallRequest], Any],
|
|
|
|
|
|
):
|
|
|
|
|
|
"""包装工具调用,处理 skill 动态激活"""
|
|
|
|
|
|
result = await handler(request)
|
|
|
|
|
|
return self._process_tool_call_result(result, request)
|
|
|
|
|
|
|
|
|
|
|
|
def wrap_tool_call(
|
|
|
|
|
|
self,
|
|
|
|
|
|
request: ToolCallRequest,
|
|
|
|
|
|
handler: Callable[[ToolCallRequest], Any],
|
|
|
|
|
|
):
|
|
|
|
|
|
"""同步版本的工具调用包装"""
|
|
|
|
|
|
result = handler(request)
|
|
|
|
|
|
return self._process_tool_call_result(result, request)
|
|
|
|
|
|
|
|
|
|
|
|
def _extract_skill_slug_from_skill_md_path(self, file_path: Any) -> str | None:
|
|
|
|
|
|
"""从文件路径中提取 skill slug"""
|
|
|
|
|
|
if not isinstance(file_path, str):
|
|
|
|
|
|
return None
|
|
|
|
|
|
raw = file_path.strip()
|
|
|
|
|
|
if not raw:
|
|
|
|
|
|
return None
|
|
|
|
|
|
pure = PurePosixPath(raw if raw.startswith("/") else f"/{raw}")
|
|
|
|
|
|
parts = [p for p in pure.parts if p not in ("/", "")]
|
|
|
|
|
|
if len(parts) != 3:
|
|
|
|
|
|
return None
|
|
|
|
|
|
if parts[0] != "skills" or parts[2] != "SKILL.md":
|
|
|
|
|
|
return None
|
|
|
|
|
|
slug = parts[1]
|
|
|
|
|
|
if not is_valid_skill_slug(slug):
|
|
|
|
|
|
return None
|
|
|
|
|
|
return slug
|
|
|
|
|
|
|
|
|
|
|
|
def _is_visible_skill_slug(self, request: ToolCallRequest, slug: str) -> bool:
|
|
|
|
|
|
"""检查 slug 是否可见"""
|
|
|
|
|
|
runtime_context = request.runtime.context
|
|
|
|
|
|
visible_skills = getattr(runtime_context, "_visible_skills", None)
|
|
|
|
|
|
|
|
|
|
|
|
if isinstance(visible_skills, list):
|
|
|
|
|
|
return slug in visible_skills
|
|
|
|
|
|
|
|
|
|
|
|
# 后备:从配置的 skills 检查
|
|
|
|
|
|
configured_skills = getattr(runtime_context, self.skills_context_name, None) or []
|
|
|
|
|
|
normalized = normalize_selected_skills(configured_skills)
|
|
|
|
|
|
return slug in normalized
|
|
|
|
|
|
|
|
|
|
|
|
def _merge_activated_skill_update(self, result: Any, slug: str):
|
|
|
|
|
|
"""合并动态激活的 skill 更新"""
|
|
|
|
|
|
from langchain_core.messages import ToolMessage
|
|
|
|
|
|
|
|
|
|
|
|
if isinstance(result, Command):
|
|
|
|
|
|
update = dict(result.update or {})
|
|
|
|
|
|
current = update.get("activated_skills") or []
|
|
|
|
|
|
update["activated_skills"] = _activated_skills_reducer(current, [slug])
|
|
|
|
|
|
return Command(graph=result.graph, update=update, resume=result.resume, goto=result.goto)
|
|
|
|
|
|
|
|
|
|
|
|
if isinstance(result, ToolMessage):
|
|
|
|
|
|
return Command(update={"messages": [result], "activated_skills": [slug]})
|
|
|
|
|
|
|
|
|
|
|
|
return result
|
|
|
|
|
|
|
|
|
|
|
|
def _format_skills_locations(self, sources: list[str]) -> str:
|
|
|
|
|
|
"""格式化 skills 位置信息"""
|
|
|
|
|
|
locations = []
|
|
|
|
|
|
for i, source_path in enumerate(sources):
|
|
|
|
|
|
name = PurePosixPath(source_path.rstrip("/")).name.capitalize()
|
|
|
|
|
|
suffix = " (higher priority)" if i == len(sources) - 1 else ""
|
|
|
|
|
|
locations.append(f"**{name} Skills**: `{source_path}`{suffix}")
|
|
|
|
|
|
return "\n".join(locations)
|
|
|
|
|
|
|
|
|
|
|
|
def _format_skills_list(self, skills_meta: list[dict[str, str]]) -> str:
|
|
|
|
|
|
"""格式化 skills 列表"""
|
|
|
|
|
|
if not skills_meta:
|
|
|
|
|
|
return f"(No skills available yet. You can create skills in {' or '.join(self.skills_sources_for_prompt)})"
|
|
|
|
|
|
|
|
|
|
|
|
lines = []
|
|
|
|
|
|
for skill in skills_meta:
|
|
|
|
|
|
lines.append(f"- **{skill['name']}**: {skill['description']}")
|
|
|
|
|
|
lines.append(f" -> Read `{skill['path']}` for full instructions")
|
|
|
|
|
|
return "\n".join(lines)
|
|
|
|
|
|
|
|
|
|
|
|
def _build_skills_section(self, skills_meta: list[dict[str, str]]) -> str:
|
|
|
|
|
|
"""构建 skills 提示段"""
|
|
|
|
|
|
skills_locations = self._format_skills_locations(self.skills_sources_for_prompt)
|
|
|
|
|
|
skills_list = self._format_skills_list(skills_meta)
|
|
|
|
|
|
return SKILLS_SYSTEM_PROMPT.format(
|
|
|
|
|
|
skills_locations=skills_locations,
|
|
|
|
|
|
skills_list=skills_list,
|
|
|
|
|
|
)
|