diff --git a/src/langbot/pkg/provider/tools/loaders/skill_authoring.py b/src/langbot/pkg/provider/tools/loaders/skill_authoring.py index ba1f729e..d8d41349 100644 --- a/src/langbot/pkg/provider/tools/loaders/skill_authoring.py +++ b/src/langbot/pkg/provider/tools/loaders/skill_authoring.py @@ -97,12 +97,11 @@ class SkillToolLoader(loader.ToolLoader): if skill_data is None: visible_skills = getattr(skill_mgr, 'skills', {}) available_names = ', '.join(sorted(visible_skills.keys())) or 'none' - raise ValueError( - f'Skill "{skill_name}" not found. Available skills: {available_names}' - ) + raise ValueError(f'Skill "{skill_name}" not found. Available skills: {available_names}') # Register activated skill for sandbox mount path resolution from . import skill as skill_loader + skill_loader.register_activated_skill(query, skill_data) # Return SKILL.md content as Tool Result (injects into context) @@ -112,17 +111,17 @@ class SkillToolLoader(loader.ToolLoader): # Build Tool Result content result_content = f'The "{skill_name}" skill is activated\n' - result_content += f'\n' + result_content += '\n' result_content += f'{skill_name}\n' result_content += f'{mount_path}\n' result_content += f'{package_root}\n' result_content += f'\n## Instructions\n{instructions}\n' - result_content += f'\n## Runtime Context\n' + result_content += '\n## Runtime Context\n' result_content += f'The skill package is mounted at {mount_path}. Use the standard tools to interact with it:\n' result_content += f'- Use `read` to inspect files under {mount_path}\n' result_content += f'- Use `exec` with workdir set to {mount_path} to run commands in that package\n' - result_content += f'- Use `write` and `edit` on that path when the instructions require updating files\n' - result_content += f'\n' + result_content += '- Use `write` and `edit` on that path when the instructions require updating files\n' + result_content += '\n' return { 'activated': True, @@ -190,7 +189,7 @@ class SkillToolLoader(loader.ToolLoader): if not normalized_path.startswith('/workspace'): raise ValueError('path must be under /workspace') - relative = normalized_path[len('/workspace'):].lstrip('/') + relative = normalized_path[len('/workspace') :].lstrip('/') host_root = os.path.realpath(workspace_root) host_path = os.path.realpath(os.path.join(host_root, relative)) @@ -213,7 +212,7 @@ class SkillToolLoader(loader.ToolLoader): 'properties': { 'skill_name': { 'type': 'string', - 'description': 'The skill name to activate (no arguments). E.g., "pdf" or "create-skill"', + 'description': 'The skill name to activate (no arguments). E.g., "pdf" or "data-analysis"', }, }, 'required': ['skill_name'], @@ -227,7 +226,7 @@ class SkillToolLoader(loader.ToolLoader): name=REGISTER_SKILL_TOOL_NAME, human_desc='Register a skill from sandbox', description=( - 'Register a skill package from a directory under /workspace into LangBot\'s skill store. ' + "Register a skill package from a directory under /workspace into LangBot's skill store. " 'Use this after creating or preparing a skill in the sandbox with exec/read/write/edit. ' 'The directory must contain a SKILL.md file. ' 'After registration, the skill can be activated with the activate tool.' @@ -276,15 +275,15 @@ class SkillToolLoader(loader.ToolLoader): available_skills_lines = [''] for skill_name, skill_data in sorted(skills.items()): description = skill_data.get('description', '') - available_skills_lines.append(f'') + available_skills_lines.append('') available_skills_lines.append(f'{skill_name}') available_skills_lines.append(f'{description}') - available_skills_lines.append(f'') + available_skills_lines.append('') available_skills_lines.append('') available_skills_block = '\n'.join(available_skills_lines) - return f'''Activate a skill within the main conversation. + return f"""Activate a skill within the main conversation. When users ask you to perform tasks, check if any of the available skills @@ -299,7 +298,7 @@ The skill is activated - The skill's instructions will be provided in the tool result - Examples: - skill_name: "pdf" - invoke the pdf skill - - skill_name: "create-skill" - invoke the create-skill skill for creating new skills + - skill_name: "data-analysis" - invoke the data-analysis skill Important: - Only use skills listed in below @@ -307,4 +306,4 @@ Important: - To create a new skill: prepare it in /workspace, then use register_skill tool -{available_skills_block}''' \ No newline at end of file +{available_skills_block}""" diff --git a/src/langbot/pkg/skill/manager.py b/src/langbot/pkg/skill/manager.py index 1db8afba..c5578743 100644 --- a/src/langbot/pkg/skill/manager.py +++ b/src/langbot/pkg/skill/manager.py @@ -2,7 +2,6 @@ from __future__ import annotations import datetime as dt import os -import shutil import typing from ..core import app @@ -16,9 +15,7 @@ if typing.TYPE_CHECKING: 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 loaded from data/skills/ and managed by users. Skills are activated through the `activate` tool (Tool Call mechanism), aligned with Claude Code's design. This protects KV Cache and follows @@ -38,8 +35,7 @@ class SkillManager: async def reload_skills(self): """Reload all skills. - Builtin skills (templates/skills/) are copied to data/skills/ on first run, - then all skills are loaded from data/skills/. + All skills are loaded from data/skills/. NOTE: This performs a full scan. For registering a single new skill, consider adding it directly to self.skills instead of reloading all. @@ -51,25 +47,6 @@ class SkillManager: managed_root = self.get_managed_skills_root() os.makedirs(managed_root, exist_ok=True) - # Copy builtin skills to data/skills/ if not already present - 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'] - target_path = os.path.join(managed_root, skill_name) - - # Only copy if target doesn't exist (preserve user modifications) - if not os.path.exists(target_path): - shutil.copytree(package_root, target_path) - self.ap.logger.info(f'Copied builtin skill "{skill_name}" to data/skills/') - # Load all skills from data/skills/ if os.path.isdir(managed_root): for package_root, entry_file in self._discover_skill_directories(managed_root): @@ -105,11 +82,6 @@ class SkillManager: """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]] = [] @@ -189,4 +161,4 @@ class SkillManager: 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 + return self.skills.get(name) diff --git a/src/langbot/templates/skills/create-skill/SKILL.md b/src/langbot/templates/skills/create-skill/SKILL.md deleted file mode 100644 index 2ae143c5..00000000 --- a/src/langbot/templates/skills/create-skill/SKILL.md +++ /dev/null @@ -1,104 +0,0 @@ ---- -name: create-skill -description: Create new skills for LangBot. Use when users want to create, modify, or register a skill. Helps draft SKILL.md with proper frontmatter and register skills from sandbox directories. ---- - -# Create Skill - -A skill for creating new LangBot skills. - -## Skill Structure - -A skill is a directory containing, at minimum, a `SKILL.md` file: - -``` -skill-name/ -├── SKILL.md # Required: metadata + instructions -├── scripts/ # Optional: helper scripts -├── references/ # Optional: documentation -├── assets/ # Optional: templates, resources -``` - -## SKILL.md Format - -SKILL.md must contain YAML frontmatter followed by Markdown content: - -```markdown ---- -name: skill-name -display_name: Skill Display Name -description: What this skill does and when to use it. ---- - -# Skill Title - -Instructions for how to use this skill... - -## Examples - -- Example 1 -- Example 2 -``` - -### Frontmatter Fields - -| Field | Required | Description | -|-------|----------|-------------| -| `name` | Yes | Skill identifier. 1-64 chars, lowercase letters, numbers, hyphens only. Must not start or end with hyphen. | -| `display_name` | No | Human-readable name shown in UI. | -| `description` | Yes | What the skill does and when to use it. Max 1024 chars. Include keywords for triggering. | -| `license` | No | License name or reference. | -| `metadata` | No | Additional key-value metadata. | - -### Body Content - -The Markdown body contains skill instructions. Recommended sections: -- Step-by-step instructions -- Examples -- Common edge cases -- Guidelines - -Keep SKILL.md under 500 lines. Move detailed content to `references/` directory. - -## Creating a Skill - -1. **Understand requirements**: Ask what the skill should do -2. **Draft SKILL.md**: Create frontmatter + instructions -3. **Create in sandbox**: Write files to `/workspace/{skill-name}/` -4. **Register**: Use `register_skill` tool to register to LangBot store - -### Name Rules - -- Lowercase letters, numbers, hyphens only -- Cannot start or end with hyphen -- No consecutive hyphens (`--`) -- 1-64 characters - -Valid: `pdf-processing`, `data-analysis`, `code-review` -Invalid: `PDF-Processing`, `-pdf`, `pdf--processing` - -### Description Tips - -Good description describes both what and when: -```yaml -description: Extracts text from PDF files. Use when working with PDF documents or when user mentions PDFs. -``` - -Poor description: -```yaml -description: Helps with PDFs. -``` - -## Workflow Example - -1. User: "Create a skill for generating reports" -2. Ask clarifying questions about report format, templates, etc. -3. Create `/workspace/report-generator/SKILL.md` -4. Optionally create helper scripts in `/workspace/report-generator/scripts/` -5. Call `register_skill(path="/workspace/report-generator", name="report-generator")` -6. Skill is now available via `activate(skill_name="report-generator")` - -## After Registration - -The skill package is copied to `data/skills/` and loaded by LangBot. -User can activate it immediately with the `activate` tool. \ No newline at end of file diff --git a/web/src/app/home/layout.tsx b/web/src/app/home/layout.tsx index 15e75061..9ac9770a 100644 --- a/web/src/app/home/layout.tsx +++ b/web/src/app/home/layout.tsx @@ -76,18 +76,26 @@ export default function HomeLayout({ // Auto-redirect to wizard on first visit (wizard not yet completed on this instance) useEffect(() => { + let cancelled = false; + const checkWizard = async () => { try { // Always re-fetch to ensure we have the latest wizard_status from backend - await initializeSystemInfo(); - if (systemInfo.wizard_status === 'none') { - navigate('/wizard'); + await initializeSystemInfo({ throwOnError: true }); + if (!cancelled && systemInfo.wizard_status === 'none') { + navigate('/wizard', { replace: true }); } } catch { - // If fetching system info fails, don't redirect + if (!cancelled) { + navigate('/backend-unavailable', { replace: true }); + } } }; checkWizard(); + + return () => { + cancelled = true; + }; }, [navigate]); return ( diff --git a/web/src/app/home/plugins/components/plugin-installed/ExtensionCardComponent.tsx b/web/src/app/home/plugins/components/plugin-installed/ExtensionCardComponent.tsx index d7587f71..d6b50940 100644 --- a/web/src/app/home/plugins/components/plugin-installed/ExtensionCardComponent.tsx +++ b/web/src/app/home/plugins/components/plugin-installed/ExtensionCardComponent.tsx @@ -50,17 +50,6 @@ export default function ExtensionCardComponent({ cardVO.iconURL || httpClient.getPluginIconURL(cardVO.author, cardVO.name); const showFallback = iconFailed || !iconSrc; - const getTypeBadgeColor = (type: ExtensionType) => { - switch (type) { - case 'mcp': - return 'border-sky-500 text-sky-600 dark:border-sky-400 dark:text-sky-300'; - case 'skill': - return 'border-emerald-500 text-emerald-600 dark:border-emerald-400 dark:text-emerald-300'; - default: - return 'border-violet-500 text-violet-600 dark:border-violet-400 dark:text-violet-300'; - } - }; - const getTypeLabel = (type: ExtensionType) => { switch (type) { case 'mcp': @@ -72,6 +61,30 @@ export default function ExtensionCardComponent({ } }; + const getTypeIcon = (type: ExtensionType) => { + switch (type) { + case 'mcp': + return Server; + case 'skill': + return Sparkles; + default: + return Puzzle; + } + }; + + const renderTypeBadge = (type: ExtensionType) => { + const TypeIcon = getTypeIcon(type); + return ( + + + {getTypeLabel(type)} + + ); + }; + const renderPluginContent = () => ( <>
@@ -84,12 +97,7 @@ export default function ExtensionCardComponent({ v{cardVO.version} - - {getTypeLabel(cardVO.type)} - + {renderTypeBadge(cardVO.type)} {cardVO.debug && ( {cardVO.label}
- - MCP - + {renderTypeBadge('mcp')} {cardVO.mode && (
- {cardVO.description || t('mcp.noToolsFound')} - {cardVO.tools !== undefined && cardVO.tools > 0 && ( - - {t('mcp.toolCount', { count: cardVO.tools })} - - )} + {cardVO.description || + (cardVO.tools !== undefined && cardVO.tools > 0 + ? t('mcp.toolCount', { count: cardVO.tools }) + : t('mcp.noToolsFound'))}
); @@ -188,12 +189,7 @@ export default function ExtensionCardComponent({
{cardVO.label}
- - {t('common.skill')} - + {renderTypeBadge('skill')}
{cardVO.description} diff --git a/web/src/app/infra/http/index.ts b/web/src/app/infra/http/index.ts index b992545d..98b660c6 100644 --- a/web/src/app/infra/http/index.ts +++ b/web/src/app/infra/http/index.ts @@ -90,12 +90,17 @@ export const getCloudServiceClientSync = (): CloudServiceClient => { * 手动初始化系统信息 * 可以在应用启动时调用此方法预先获取系统信息 */ -export const initializeSystemInfo = async (): Promise => { +export const initializeSystemInfo = async (options?: { + throwOnError?: boolean; +}): Promise => { try { Object.assign(systemInfo, await backendClient.getSystemInfo()); cloudServiceClient.updateBaseURL(systemInfo.cloud_service_url); } catch (error) { console.error('Failed to initialize system info:', error); + if (options?.throwOnError) { + throw error; + } } }; diff --git a/web/src/components/BackendUnavailablePage.tsx b/web/src/components/BackendUnavailablePage.tsx new file mode 100644 index 00000000..fe133473 --- /dev/null +++ b/web/src/components/BackendUnavailablePage.tsx @@ -0,0 +1,47 @@ +import { useTranslation } from 'react-i18next'; +import { useNavigate } from 'react-router-dom'; +import { AlertCircle, Home, RefreshCw } from 'lucide-react'; + +import { Button } from '@/components/ui/button'; + +export default function BackendUnavailablePage() { + const { t } = useTranslation(); + const navigate = useNavigate(); + + return ( +
+
+
+ +
+ +

+ {t('errorPage.backendUnavailableStatus')} +

+ +

+ {t('common.loginLoadError')} +

+ +

+ {t('common.loginLoadErrorDesc')} +

+ +
+ + +
+
+
+ ); +} diff --git a/web/src/i18n/locales/en-US.ts b/web/src/i18n/locales/en-US.ts index eb876b0e..44e9aed9 100644 --- a/web/src/i18n/locales/en-US.ts +++ b/web/src/i18n/locales/en-US.ts @@ -1500,8 +1500,10 @@ const enUS = { notFound: 'Page not found', notFoundDescription: 'The page you are looking for does not exist or has been moved.', + backendUnavailableStatus: 'Backend unavailable', goBack: 'Go Back', backToHome: 'Back to Home', + backToLogin: 'Back to Login', }, pluginPages: { selectFromSidebar: 'Select a plugin page from the sidebar', diff --git a/web/src/i18n/locales/es-ES.ts b/web/src/i18n/locales/es-ES.ts index 48ebf804..fde5e463 100644 --- a/web/src/i18n/locales/es-ES.ts +++ b/web/src/i18n/locales/es-ES.ts @@ -1429,8 +1429,10 @@ const esES = { 'Ocurrió un error inesperado. Por favor, inténtelo de nuevo más tarde.', notFound: 'Página no encontrada', notFoundDescription: 'La página que buscas no existe o ha sido movida.', + backendUnavailableStatus: 'Backend no disponible', goBack: 'Volver', backToHome: 'Ir al inicio', + backToLogin: 'Volver al inicio de sesión', }, pluginPages: { selectFromSidebar: 'Selecciona una página de plugin en la barra lateral', diff --git a/web/src/i18n/locales/ja-JP.ts b/web/src/i18n/locales/ja-JP.ts index 2f3b1488..e89c90f4 100644 --- a/web/src/i18n/locales/ja-JP.ts +++ b/web/src/i18n/locales/ja-JP.ts @@ -1411,8 +1411,10 @@ const jaJP = { notFound: 'ページが見つかりません', notFoundDescription: 'お探しのページは存在しないか、移動された可能性があります。', + backendUnavailableStatus: 'バックエンドを利用できません', goBack: '戻る', backToHome: 'ホームに戻る', + backToLogin: 'ログインに戻る', }, pluginPages: { selectFromSidebar: 'サイドバーからプラグインページを選択してください', diff --git a/web/src/i18n/locales/ru-RU.ts b/web/src/i18n/locales/ru-RU.ts index ad2e63f7..8f9cf606 100644 --- a/web/src/i18n/locales/ru-RU.ts +++ b/web/src/i18n/locales/ru-RU.ts @@ -1393,6 +1393,18 @@ const ruRU = { backToWorkbench: 'Вернуться к рабочей панели', }, }, + errorPage: { + unexpectedError: 'Что-то пошло не так', + unexpectedErrorDescription: + 'Произошла непредвиденная ошибка. Повторите попытку позже.', + notFound: 'Страница не найдена', + notFoundDescription: + 'Страница, которую вы ищете, не существует или была перемещена.', + backendUnavailableStatus: 'Бэкенд недоступен', + goBack: 'Назад', + backToHome: 'На главную', + backToLogin: 'Вернуться к входу', + }, pluginPages: { selectFromSidebar: 'Выберите страницу плагина на боковой панели', invalidPage: 'Недопустимая страница плагина', diff --git a/web/src/i18n/locales/th-TH.ts b/web/src/i18n/locales/th-TH.ts index 7775923e..e830cf7e 100644 --- a/web/src/i18n/locales/th-TH.ts +++ b/web/src/i18n/locales/th-TH.ts @@ -1369,8 +1369,10 @@ const thTH = { 'เกิดข้อผิดพลาดที่ไม่คาดคิด กรุณาลองใหม่อีกครั้งในภายหลัง', notFound: 'ไม่พบหน้า', notFoundDescription: 'หน้าที่คุณกำลังมองหาไม่มีอยู่หรือถูกย้ายแล้ว', + backendUnavailableStatus: 'แบ็กเอนด์ไม่พร้อมใช้งาน', goBack: 'ย้อนกลับ', backToHome: 'กลับหน้าหลัก', + backToLogin: 'กลับไปหน้าเข้าสู่ระบบ', }, pluginPages: { selectFromSidebar: 'เลือกหน้าปลั๊กอินจากแถบด้านข้าง', diff --git a/web/src/i18n/locales/vi-VN.ts b/web/src/i18n/locales/vi-VN.ts index badd73d4..5bacea14 100644 --- a/web/src/i18n/locales/vi-VN.ts +++ b/web/src/i18n/locales/vi-VN.ts @@ -1393,8 +1393,10 @@ const viVN = { notFound: 'Không tìm thấy trang', notFoundDescription: 'Trang bạn tìm kiếm không tồn tại hoặc đã được di chuyển.', + backendUnavailableStatus: 'Backend không khả dụng', goBack: 'Quay lại', backToHome: 'Về trang chủ', + backToLogin: 'Quay lại đăng nhập', }, pluginPages: { selectFromSidebar: 'Chọn một trang plugin từ thanh bên', diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index 61efb2f8..405ae004 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -1436,8 +1436,10 @@ const zhHans = { unexpectedErrorDescription: '发生了意外错误,请稍后重试。', notFound: '页面未找到', notFoundDescription: '你访问的页面不存在或已被移动。', + backendUnavailableStatus: '后端服务不可用', goBack: '返回上页', backToHome: '返回首页', + backToLogin: '返回登录', }, pluginPages: { selectFromSidebar: '从侧边栏选择一个插件页面', diff --git a/web/src/i18n/locales/zh-Hant.ts b/web/src/i18n/locales/zh-Hant.ts index 2b445948..00aca7c5 100644 --- a/web/src/i18n/locales/zh-Hant.ts +++ b/web/src/i18n/locales/zh-Hant.ts @@ -1343,8 +1343,10 @@ const zhHant = { unexpectedErrorDescription: '發生了意外錯誤,請稍後重試。', notFound: '頁面未找到', notFoundDescription: '你訪問的頁面不存在或已被移動。', + backendUnavailableStatus: '後端服務不可用', goBack: '返回上頁', backToHome: '返回首頁', + backToLogin: '返回登入', }, pluginPages: { selectFromSidebar: '從側邊欄選擇一個插件頁面', diff --git a/web/src/router.tsx b/web/src/router.tsx index b775bb65..0a60a331 100644 --- a/web/src/router.tsx +++ b/web/src/router.tsx @@ -25,6 +25,7 @@ import MCPPage from '@/app/home/mcp/page'; import KnowledgePage from '@/app/home/knowledge/page'; import SkillsPage from '@/app/home/skills/page'; import ErrorPage from '@/components/ErrorPage'; +import BackendUnavailablePage from '@/components/BackendUnavailablePage'; import PluginPagesPage from '@/app/home/plugin-pages/page'; const Loading = () =>
Loading...
; @@ -65,6 +66,10 @@ export const router = createBrowserRouter([ path: '/wizard', element: , }, + { + path: '/backend-unavailable', + element: , + }, { path: '/auth/space/callback', element: ,