Skip to content

Commit 186012e

Browse files
committed
fix(skills): 修复 skills 加载以及按需暴露问题,并新增 reporter 技能,已可以替代数据库报表智能体
1 parent 96ce4db commit 186012e

File tree

4 files changed

+82
-48
lines changed

4 files changed

+82
-48
lines changed

src/agents/common/context.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ def update(self, data: dict):
9393
metadata={
9494
"name": "Skills",
9595
"options": [],
96-
"description": "可选技能列表(由超级管理员维护)。运行时仅挂载并只读暴露选中的 skills。",
96+
"description": "可选技能列表(由超级管理员维护)。运行时仅挂载并只读暴露选中的 skills。技能依赖的工具和 MCP 服务器也会被自动挂载。",
9797
"type": "list",
9898
},
9999
)

src/agents/common/middlewares/skills_middleware.py

Lines changed: 53 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
from langgraph.types import Command
1414
from sqlalchemy.ext.asyncio import AsyncSession
1515

16+
from src.agents.common.toolkits import get_all_tool_instances
1617
from src.repositories.skill_repository import SkillRepository
1718
from src.services.mcp_service import get_enabled_mcp_tools
1819
from src.services.skill_service import _normalize_string_list, is_valid_skill_slug
@@ -171,6 +172,21 @@ def __init__(
171172
self.skills_context_name = skills_context_name
172173
self.enable_skills_prompt = enable_skills_prompt
173174
self.skills_sources_for_prompt = skills_sources_for_prompt or ["/skills/"]
175+
# 实例级缓存:避免每次模型调用都查数据库
176+
self._dependency_map_cache: dict[str, SkillDependencyNode] | None = None
177+
self._prompt_metadata_cache: dict[str, SkillPromptMetadata] | None = None
178+
179+
async def _get_dependency_map_cached(self) -> dict[str, SkillDependencyNode]:
180+
"""获取依赖映射(带缓存)"""
181+
if self._dependency_map_cache is None:
182+
self._dependency_map_cache = await get_dependency_map()
183+
return self._dependency_map_cache
184+
185+
async def _get_prompt_metadata_cached(self) -> dict[str, SkillPromptMetadata]:
186+
"""获取提示词元数据(带缓存)"""
187+
if self._prompt_metadata_cache is None:
188+
self._prompt_metadata_cache = await get_prompt_metadata()
189+
return self._prompt_metadata_cache
174190

175191
async def abefore_agent(self, state: SkillsState, runtime) -> dict[str, Any] | None:
176192
"""在 agent 执行前注入 skills 提示词"""
@@ -182,8 +198,8 @@ async def abefore_agent(self, state: SkillsState, runtime) -> dict[str, Any] | N
182198
if getattr(runtime_context, "_skills_prompt_injected", False):
183199
return None
184200

185-
# 从数据库加载 skills 数据
186-
dependency_map = await get_dependency_map()
201+
# 从数据库加载 skills 数据(使用缓存)
202+
dependency_map = await self._get_dependency_map_cached()
187203

188204
# 获取配置的 skills
189205
configured_skills = getattr(runtime_context, self.skills_context_name, None) or []
@@ -219,8 +235,8 @@ async def awrap_model_call(
219235
"""包装模型调用,处理动态激活和依赖展开"""
220236
runtime_context = request.runtime.context
221237

222-
# 从数据库加载 skills 数据
223-
dependency_map = await get_dependency_map()
238+
# 从缓存加载 skills 数据
239+
dependency_map = await self._get_dependency_map_cached()
224240

225241
# 1. 获取配置的 skills
226242
configured_skills = getattr(runtime_context, self.skills_context_name, None) or []
@@ -239,40 +255,47 @@ async def awrap_model_call(
239255
# 4. 更新 runtime_context 中的 visible_skills
240256
setattr(runtime_context, "_visible_skills", visible_skills)
241257

242-
# 5. 构建依赖包
243-
deps_bundle = await self._build_dependency_bundle(visible_skills)
258+
# 5. 构建依赖包(只从直接激活的 skills 获取依赖,不包含闭包展开的依赖)
259+
deps_bundle = await self._build_dependency_bundle(activated)
244260

245-
# 6. 加载依赖的工具
246-
if deps_bundle["tools"] or deps_bundle["mcps"]:
247-
enabled_tools = await self._get_tools_from_context(
261+
# 6. 加载依赖的工具(普通工具 + MCP 工具)
262+
enabled_tools = []
263+
264+
# 6.1 从 toolkits 获取普通工具
265+
if deps_bundle["tools"]:
266+
all_tools = get_all_tool_instances()
267+
required_tool_names = set(deps_bundle["tools"])
268+
enabled_tools = [t for t in all_tools if t.name in required_tool_names]
269+
270+
# 6.2 获取 MCP 工具
271+
if deps_bundle["mcps"]:
272+
mcp_tools = await self._get_mcp_tools_from_context(
248273
runtime_context,
249-
extra_tool_names=deps_bundle["tools"],
250274
extra_mcps=deps_bundle["mcps"],
251275
)
276+
enabled_tools.extend(mcp_tools)
252277

253-
# 合并工具
254-
if enabled_tools:
255-
existing_tools = list(request.tools or [])
256-
enabled_tool_names = {t.name for t in enabled_tools}
257-
merged_tools = []
258-
for t_bind in existing_tools:
259-
if t_bind.name in enabled_tool_names:
260-
merged_tools.append(t_bind)
261-
if merged_tools:
262-
request = request.override(tools=merged_tools)
278+
# 合并工具:保留原有工具 + 追加依赖的新工具
279+
if enabled_tools:
280+
existing_tool_names = {t.name for t in request.tools or []}
281+
merged_tools = list(request.tools or [])
282+
for t in enabled_tools:
283+
if t.name not in existing_tool_names:
284+
merged_tools.append(t)
285+
request = request.override(tools=merged_tools)
263286

264287
return await handler(request)
265288

266-
async def _build_dependency_bundle(self, visible_skills: list[str]) -> dict[str, list[str]]:
267-
"""根据 visible_skills 构建依赖包"""
268-
dependency_map = await get_dependency_map()
289+
async def _build_dependency_bundle(self, activated_skills: list[str]) -> dict[str, list[str]]:
290+
"""根据直接激活的 skills 构建依赖包(不包含闭包展开的依赖)"""
291+
dependency_map = await self._get_dependency_map_cached()
269292

270293
tools: list[str] = []
271294
mcps: list[str] = []
272295
seen_tools: set[str] = set()
273296
seen_mcps: set[str] = set()
274297

275-
for slug in visible_skills:
298+
for slug in activated_skills:
276299
dep = dependency_map.get(slug, {})
277300
for tool_name in dep.get("tools", []):
278301
if tool_name in seen_tools:
@@ -285,11 +308,11 @@ async def _build_dependency_bundle(self, visible_skills: list[str]) -> dict[str,
285308
seen_mcps.add(mcp_name)
286309
mcps.append(mcp_name)
287310

288-
return {"tools": tools, "mcps": mcps, "skills": visible_skills}
311+
return {"tools": tools, "mcps": mcps, "skills": activated_skills}
289312

290313
async def _collect_prompt_metadata(self, slugs: list[str]) -> list[SkillPromptMetadata]:
291314
"""收集指定 slugs 的提示词元数据"""
292-
prompt_metadata = await get_prompt_metadata()
315+
prompt_metadata = await self._get_prompt_metadata_cached()
293316

294317
result: list[SkillPromptMetadata] = []
295318
seen: set[str] = set()
@@ -310,28 +333,16 @@ async def _collect_prompt_metadata(self, slugs: list[str]) -> list[SkillPromptMe
310333

311334
return result
312335

313-
async def _get_tools_from_context(
336+
async def _get_mcp_tools_from_context(
314337
self,
315338
context,
316339
*,
317-
extra_tool_names: list[str] | None = None,
318340
extra_mcps: list[str] | None = None,
319341
) -> list:
320-
"""从上下文配置中获取工具列表"""
342+
"""从上下文配置中获取 MCP 工具列表"""
321343
import asyncio
322344

323-
selected_tools = []
324-
325-
# 1. 工具(从 extra_tool_names 获取)
326-
all_tool_names: list[str] = []
327-
for tool_name in extra_tool_names or []:
328-
if isinstance(tool_name, str):
329-
all_tool_names.append(tool_name)
330-
331-
# 这里简化处理:假设工具已经在其他 middleware 中加载
332-
# SkillsMiddleware 主要负责 MCP 工具的加载
333-
334-
# 2. MCP 工具(并行加载)
345+
# MCP 工具(并行加载)
335346
mcps = getattr(context, "mcps", None) or []
336347
all_mcp_names: list[str] = []
337348
for server_name in mcps:
@@ -357,6 +368,7 @@ async def load_mcp_tools(server_name: str) -> list:
357368

358369
# 并行加载所有 MCP 工具
359370
results = await asyncio.gather(*[load_mcp_tools(name) for name in unique_mcp_names])
371+
selected_tools = []
360372
for tools in results:
361373
selected_tools.extend(tools)
362374

src/agents/common/toolkits/mysql/tools.py

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,12 @@ def get_connection_manager() -> MySQLConnectionManager:
4646
return _connection_manager
4747

4848

49-
class TableListModel(BaseModel):
50-
"""获取表名列表的参数模型"""
51-
52-
pass
53-
5449

5550
@tool(
5651
category="mysql",
5752
tags=["数据库", "查询"],
5853
display_name="列出MySQL表",
5954
name_or_callable="mysql_list_tables",
60-
args_schema=TableListModel,
6155
)
6256
def mysql_list_tables() -> str:
6357
"""【查询表名及说明】获取数据库中的所有表名
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
name: sql-reporter
3+
description: "生成 SQL 查询报表并生成可视化图表。当用户需要查询数据库并以报表形式展示结果时使用此技能,包括:统计销售数据、分析用户行为、生成业务报表、查询业务指标等。"
4+
---
5+
6+
# SQL 报表技能
7+
8+
根据用户的指令,使用数据库工具和图表绘制工具,构建 SQL 查询报告。
9+
10+
## 操作流程
11+
12+
1. 理解用户的指令,明确报表的需求和目标
13+
2. 使用 MySQL 工具生成正确的 SQL 查询
14+
3. 执行查询并获取结果
15+
4. 使用 Charts MCP 生成图表
16+
5. 将图表以 markdown 图片格式嵌入报表
17+
18+
## 关键约束
19+
20+
- 生成的 SQL 查询必须正确且高效,避免全表扫描
21+
- 图表生成工具的返回结果不会默认渲染,必须在最终报表中以 `![描述](图片URL)` 格式嵌入
22+
- 只返回报表相关的结论,不要返回原始 SQL 查询语句
23+
24+
## 允许的工具
25+
26+
- MySQL 工具:执行 SQL 查询
27+
- Charts MCP:生成可视化图表
28+
- 网络检索工具:必要时补充背景信息

0 commit comments

Comments
 (0)