diff --git a/src/langbot/pkg/provider/tools/toolmgr.py b/src/langbot/pkg/provider/tools/toolmgr.py deleted file mode 100644 index ae664cd3..00000000 --- a/src/langbot/pkg/provider/tools/toolmgr.py +++ /dev/null @@ -1,124 +0,0 @@ -from __future__ import annotations - -import typing -from typing import TYPE_CHECKING - -import langbot_plugin.api.entities.builtin.resource.tool as resource_tool -from langbot_plugin.api.entities.events import pipeline_query - -if TYPE_CHECKING: - from ...core import app - from langbot.pkg.provider.tools.loaders import ( - mcp as mcp_loader, - native as native_loader, - plugin as plugin_loader, - skill_authoring as skill_authoring_loader, - ) - - -class ToolManager: - """LLM工具管理器""" - - ap: app.Application - - native_tool_loader: native_loader.NativeToolLoader - plugin_tool_loader: plugin_loader.PluginToolLoader - mcp_tool_loader: mcp_loader.MCPLoader - skill_authoring_tool_loader: skill_authoring_loader.SkillAuthoringToolLoader - - def __init__(self, ap: app.Application): - self.ap = ap - - async def initialize(self): - from langbot.pkg.utils import importutil - from langbot.pkg.provider.tools import loaders - from langbot.pkg.provider.tools.loaders import ( - mcp as mcp_loader, - native as native_loader, - plugin as plugin_loader, - skill_authoring as skill_authoring_loader, - ) - - importutil.import_modules_in_pkg(loaders) - - self.native_tool_loader = native_loader.NativeToolLoader(self.ap) - await self.native_tool_loader.initialize() - - # Log native (sandbox) tool availability once at startup - box_service = getattr(self.ap, 'box_service', None) - if box_service and getattr(box_service, 'available', False): - self.ap.logger.info('Native sandbox tools (exec/read/write/edit/glob/grep) are available.') - else: - self.ap.logger.warning( - 'Native sandbox tools (exec/read/write/edit/glob/grep) are NOT available. ' - 'Box runtime is not connected — the LLM will not have access to code execution tools.' - ) - - self.plugin_tool_loader = plugin_loader.PluginToolLoader(self.ap) - await self.plugin_tool_loader.initialize() - self.mcp_tool_loader = mcp_loader.MCPLoader(self.ap) - await self.mcp_tool_loader.initialize() - self.skill_authoring_tool_loader = skill_authoring_loader.SkillAuthoringToolLoader(self.ap) - await self.skill_authoring_tool_loader.initialize() - - async def get_all_tools( - self, - bound_plugins: list[str] | None = None, - bound_mcp_servers: list[str] | None = None, - include_skill_authoring: bool = False, - ) -> list[resource_tool.LLMTool]: - all_functions: list[resource_tool.LLMTool] = [] - - all_functions.extend(await self.native_tool_loader.get_tools()) - if include_skill_authoring: - all_functions.extend(await self.skill_authoring_tool_loader.get_tools()) - all_functions.extend(await self.plugin_tool_loader.get_tools(bound_plugins)) - all_functions.extend(await self.mcp_tool_loader.get_tools(bound_mcp_servers)) - - return all_functions - - async def generate_tools_for_openai(self, use_funcs: list[resource_tool.LLMTool]) -> list: - tools = [] - - for function in use_funcs: - function_schema = { - 'type': 'function', - 'function': { - 'name': function.name, - 'description': function.description, - 'parameters': function.parameters, - }, - } - tools.append(function_schema) - - return tools - - async def generate_tools_for_anthropic(self, use_funcs: list[resource_tool.LLMTool]) -> list: - tools = [] - - for function in use_funcs: - function_schema = { - 'name': function.name, - 'description': function.description, - 'input_schema': function.parameters, - } - tools.append(function_schema) - - return tools - - async def execute_func_call(self, name: str, parameters: dict, query: pipeline_query.Query) -> typing.Any: - if await self.native_tool_loader.has_tool(name): - return await self.native_tool_loader.invoke_tool(name, parameters, query) - if await self.plugin_tool_loader.has_tool(name): - return await self.plugin_tool_loader.invoke_tool(name, parameters, query) - if await self.mcp_tool_loader.has_tool(name): - return await self.mcp_tool_loader.invoke_tool(name, parameters, query) - if await self.skill_authoring_tool_loader.has_tool(name): - return await self.skill_authoring_tool_loader.invoke_tool(name, parameters, query) - raise ValueError(f'未找到工具: {name}') - - async def shutdown(self): - await self.native_tool_loader.shutdown() - await self.plugin_tool_loader.shutdown() - await self.mcp_tool_loader.shutdown() - await self.skill_authoring_tool_loader.shutdown() diff --git a/src/langbot/pkg/skill/manager.py b/src/langbot/pkg/skill/manager.py deleted file mode 100644 index e29027b2..00000000 --- a/src/langbot/pkg/skill/manager.py +++ /dev/null @@ -1,186 +0,0 @@ -from __future__ import annotations - -import datetime as dt -import os -import typing - -from ..core import app -from .utils import parse_frontmatter -from ..utils import paths - -if typing.TYPE_CHECKING: - pass - - -class SkillManager: - """Skill manager backed by filesystem packages. - - Skills are loaded from two sources: - 1. Builtin skills: templates/skills/ (shipped with LangBot) - 2. User skills: data/skills/ (created by users) - - Skills are activated through the `activate` tool (Tool Call mechanism), - aligned with Claude Code's design. This protects KV Cache and follows - industry standard. - """ - - ap: app.Application - skills: dict[str, dict] - - def __init__(self, ap: app.Application): - self.ap = ap - self.skills = {} - - async def initialize(self): - await self.reload_skills() - - async def reload_skills(self): - """Reload all skills from builtin and user directories. - - NOTE: This performs a full scan of both directories. For registering a single - new skill, consider adding it directly to self.skills instead of reloading all. - Current implementation is acceptable for typical skill counts (<50). - """ - self.skills = {} - - # Load builtin skills first (templates/skills/) - builtin_root = self.get_builtin_skills_root() - if os.path.isdir(builtin_root): - for package_root, entry_file in self._discover_skill_directories(builtin_root): - skill_data = { - 'package_root': package_root, - 'entry_file': entry_file, - } - if not self._load_skill_file(skill_data): - continue - - skill_name = skill_data['name'] - self.skills[skill_name] = skill_data - - # Load user skills (data/skills/) - can override builtin if same name - managed_root = self.get_managed_skills_root() - if os.path.isdir(managed_root): - for package_root, entry_file in self._discover_skill_directories(managed_root): - skill_data = { - 'package_root': package_root, - 'entry_file': entry_file, - } - if not self._load_skill_file(skill_data): - continue - - skill_name = skill_data['name'] - if skill_name in self.skills: - self.ap.logger.warning( - f'User skill "{skill_name}" overrides builtin skill' - ) - self.skills[skill_name] = skill_data - - builtin_count = sum(1 for s in self.skills.values() if s.get('package_root', '').startswith(builtin_root)) - user_count = len(self.skills) - builtin_count - self.ap.logger.info(f'Loaded {len(self.skills)} skills ({builtin_count} builtin, {user_count} user)') - - def refresh_skill_from_disk(self, skill_name: str) -> bool: - """Refresh a single skill from disk.""" - if not skill_name: - return False - - skill_data = self.skills.get(skill_name) - if not skill_data: - return False - - if not self._load_skill_file(skill_data): - return False - - self.skills[skill_name] = skill_data - return True - - @staticmethod - def get_managed_skills_root() -> str: - """Get the root directory for managed user skills.""" - return paths.get_data_path('skills') - - @staticmethod - def get_builtin_skills_root() -> str: - """Get the root directory for builtin skills (templates/skills/).""" - return paths.get_resource_path('templates/skills') - - def _discover_skill_directories(self, root_path: str, max_depth: int = 6) -> list[tuple[str, str]]: - """Discover all skill directories under root_path.""" - discovered: list[tuple[str, str]] = [] - root_path = os.path.realpath(os.path.abspath(root_path)) - root_depth = root_path.rstrip(os.sep).count(os.sep) - - for current_root, dirs, _files in os.walk(root_path): - current_root = os.path.realpath(current_root) - depth = current_root.rstrip(os.sep).count(os.sep) - root_depth - if depth > max_depth: - dirs[:] = [] - continue - - found = self._find_skill_entry(current_root) - if found is not None: - discovered.append(found) - dirs[:] = [] - - discovered.sort(key=lambda item: item[0]) - return discovered - - @staticmethod - def _find_skill_entry(path: str) -> tuple[str, str] | None: - """Find SKILL.md entry file in a directory.""" - for candidate in ('SKILL.md', 'skill.md'): - if os.path.isfile(os.path.join(path, candidate)): - return path, candidate - return None - - def _load_skill_file(self, skill_data: dict) -> bool: - """Load skill data from SKILL.md file.""" - package_root = self._normalize_package_root(skill_data.get('package_root', '')) - entry_file = skill_data.get('entry_file', 'SKILL.md') - if not package_root: - self.ap.logger.warning('Skill package_root is empty, skipping') - return False - - entry_path = os.path.join(package_root, entry_file) - try: - with open(entry_path, 'r', encoding='utf-8') as f: - content = f.read() - except FileNotFoundError: - self.ap.logger.warning(f'Skill entry file not found: {entry_path}, skipping') - return False - except OSError as exc: - self.ap.logger.warning(f'Failed to read skill entry file {entry_path}: {exc}, skipping') - return False - - metadata, instructions = parse_frontmatter(content) - name = str(metadata.get('name') or os.path.basename(os.path.normpath(package_root))).strip() - if not name: - self.ap.logger.warning(f'Skill at {package_root} has no valid name, skipping') - return False - - stat = os.stat(entry_path) - skill_data.clear() - skill_data.update( - { - 'name': name, - 'display_name': str(metadata.get('display_name') or name).strip(), - 'description': str(metadata.get('description') or '').strip(), - 'instructions': instructions, - 'raw_content': content, - 'package_root': package_root, - 'entry_file': entry_file, - 'created_at': dt.datetime.fromtimestamp(stat.st_ctime, tz=dt.timezone.utc).isoformat(), - 'updated_at': dt.datetime.fromtimestamp(stat.st_mtime, tz=dt.timezone.utc).isoformat(), - } - ) - return True - - @staticmethod - def _normalize_package_root(package_root: str) -> str: - if not package_root: - return '' - return os.path.realpath(os.path.abspath(package_root)) - - def get_skill_by_name(self, name: str) -> dict | None: - """Get skill data by name.""" - return self.skills.get(name) \ No newline at end of file