diff --git a/src/langbot/pkg/api/http/service/skill.py b/src/langbot/pkg/api/http/service/skill.py index 09e640d7..0b413fd7 100644 --- a/src/langbot/pkg/api/http/service/skill.py +++ b/src/langbot/pkg/api/http/service/skill.py @@ -21,7 +21,6 @@ _FRONTMATTER_FIELDS = ( 'name', 'display_name', 'description', - 'auto_activate', ) _PUBLIC_SKILL_FIELDS = ( @@ -30,7 +29,6 @@ _PUBLIC_SKILL_FIELDS = ( 'description', 'instructions', 'package_root', - 'auto_activate', 'created_at', 'updated_at', ) @@ -51,8 +49,6 @@ def _build_skill_md(metadata: dict, instructions: str) -> str: value = metadata.get(key) if value is None: continue - if key == 'auto_activate' and value is True: - continue if isinstance(value, str) and not value.strip(): continue frontmatter[key] = value @@ -118,7 +114,6 @@ class SkillService: 'name': name, 'display_name': self._resolve_create_field(data, 'display_name', imported_skill_data, default=''), 'description': self._resolve_create_field(data, 'description', imported_skill_data, default=''), - 'auto_activate': self._resolve_create_bool(data, 'auto_activate', imported_skill_data, default=True), } instructions = self._resolve_create_field(data, 'instructions', imported_skill_data, default='') self._write_skill_md(target_root, metadata, instructions) @@ -147,7 +142,6 @@ class SkillService: 'name': skill['name'], 'display_name': data.get('display_name', skill.get('display_name', '')), 'description': data.get('description', skill.get('description', '')), - 'auto_activate': data.get('auto_activate', skill.get('auto_activate', True)), } instructions = str(data.get('instructions', skill.get('instructions', '')) or '') self._write_skill_md(skill['package_root'], metadata, instructions) @@ -372,7 +366,6 @@ class SkillService: 'display_name': str(metadata.get('display_name') or '').strip(), 'description': str(metadata.get('description') or '').strip(), 'instructions': instructions, - 'auto_activate': bool(metadata.get('auto_activate', True)), } async def _reload_skills(self) -> None: @@ -400,7 +393,6 @@ class SkillService: 'display_name': str(metadata.get('display_name') or '').strip(), 'description': str(metadata.get('description') or '').strip(), 'instructions': instructions, - 'auto_activate': bool(metadata.get('auto_activate', True)), } async def _download_github_skill_to_temp(self, asset_url: str, tmp_dir: str) -> str: @@ -488,7 +480,6 @@ class SkillService: 'display_name': str(metadata.get('display_name') or '').strip(), 'description': str(metadata.get('description') or '').strip(), 'instructions': instructions, - 'auto_activate': bool(metadata.get('auto_activate', True)), 'package_root': self._build_preview_target_dir(base_target_name, relative_path, suffix), } ) @@ -626,14 +617,6 @@ class SkillService: return str(imported_skill_data.get(field, default) or default) return value - @staticmethod - def _resolve_create_bool(data: dict, field: str, imported_skill_data: dict | None, *, default: bool) -> bool: - if field in data and data[field] is not None: - return bool(data[field]) - if imported_skill_data is not None: - return bool(imported_skill_data.get(field, default)) - return default - def _write_skill_md(self, package_root: str, metadata: dict, instructions: str) -> None: package_root = self._normalize_package_root(package_root) os.makedirs(package_root, exist_ok=True) diff --git a/src/langbot/pkg/pipeline/preproc/preproc.py b/src/langbot/pkg/pipeline/preproc/preproc.py index 5ff2add6..ab8d5bfe 100644 --- a/src/langbot/pkg/pipeline/preproc/preproc.py +++ b/src/langbot/pkg/pipeline/preproc/preproc.py @@ -275,7 +275,7 @@ class PreProcessor(stage.PipelineStage): f'Skill index injected into system prompt: ' f'pipeline={query.pipeline_uuid} ' f'bound_skills={bound_skills or "all"} ' - f'available_skills=[{", ".join(s["name"] for s in self.ap.skill_mgr.skills.values() if s.get("auto_activate", True))}]' + f'available_skills=[{", ".join(s["name"] for s in self.ap.skill_mgr.skills.values() if bound_skills is None or s["name"] in bound_skills)}]' ) # Append skill instruction to the first system message if query.prompt.messages and query.prompt.messages[0].role == 'system': diff --git a/src/langbot/pkg/provider/tools/loaders/skill_authoring.py b/src/langbot/pkg/provider/tools/loaders/skill_authoring.py index 9866a095..209979b9 100644 --- a/src/langbot/pkg/provider/tools/loaders/skill_authoring.py +++ b/src/langbot/pkg/provider/tools/loaders/skill_authoring.py @@ -105,7 +105,6 @@ class SkillAuthoringToolLoader(loader.ToolLoader): 'display_name': str(parameters.get('display_name', '') or '').strip(), 'description': str(parameters.get('description', '') or '').strip(), 'instructions': instructions, - 'auto_activate': parameters.get('auto_activate', True), } ) return { @@ -137,7 +136,7 @@ class SkillAuthoringToolLoader(loader.ToolLoader): raise ValueError('name is required') data = {'name': name} - for field in ('display_name', 'description', 'instructions', 'auto_activate'): + for field in ('display_name', 'description', 'instructions'): if field in parameters: data[field] = parameters[field] @@ -172,7 +171,6 @@ class SkillAuthoringToolLoader(loader.ToolLoader): 'description': str(parameters.get('description') or scanned.get('description', '')).strip(), 'instructions': str(parameters.get('instructions') or scanned.get('instructions', '')), 'package_root': host_path, - 'auto_activate': parameters.get('auto_activate', scanned.get('auto_activate', True)), } ) return { @@ -228,10 +226,6 @@ class SkillAuthoringToolLoader(loader.ToolLoader): 'type': 'string', 'description': 'The SKILL.md body instructions for the new skill.', }, - 'auto_activate': { - 'type': 'boolean', - 'description': 'Whether the skill should be considered for automatic activation. Defaults to true.', - }, }, 'required': ['name', 'instructions'], 'additionalProperties': False, @@ -299,10 +293,6 @@ class SkillAuthoringToolLoader(loader.ToolLoader): 'type': 'string', 'description': 'Optional replacement SKILL.md body instructions.', }, - 'auto_activate': { - 'type': 'boolean', - 'description': 'Optional new auto_activate value.', - }, }, 'required': ['name'], 'additionalProperties': False, @@ -363,10 +353,6 @@ class SkillAuthoringToolLoader(loader.ToolLoader): 'type': 'string', 'description': 'Optional instructions override.', }, - 'auto_activate': { - 'type': 'boolean', - 'description': 'Optional auto_activate override.', - }, }, 'required': ['path'], 'additionalProperties': False, diff --git a/src/langbot/pkg/skill/activation.py b/src/langbot/pkg/skill/activation.py index 6fd9fd93..4f142a0c 100644 --- a/src/langbot/pkg/skill/activation.py +++ b/src/langbot/pkg/skill/activation.py @@ -119,7 +119,12 @@ def prepare_skill_activation( if not response_content or not getattr(ap, 'skill_mgr', None): return None - activated_skill_names = ap.skill_mgr.detect_skill_activations(response_content) + visible_skills = skill_loader.get_visible_skills(ap, query) + activated_skill_names = [ + skill_name + for skill_name in ap.skill_mgr.detect_skill_activations(response_content) + if skill_name in visible_skills + ] if not activated_skill_names: return None diff --git a/src/langbot/pkg/skill/manager.py b/src/langbot/pkg/skill/manager.py index bf8b48e4..d227fceb 100644 --- a/src/langbot/pkg/skill/manager.py +++ b/src/langbot/pkg/skill/manager.py @@ -135,7 +135,6 @@ class SkillManager: 'raw_content': content, 'package_root': package_root, 'entry_file': entry_file, - 'auto_activate': bool(metadata.get('auto_activate', True)), '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(), } @@ -154,8 +153,6 @@ class SkillManager: def get_skill_index(self, pipeline_uuid: str | None = None, bound_skills: list[str] | None = None) -> str: skills_to_index = [] for skill in self.skills.values(): - if not skill.get('auto_activate', True): - continue if bound_skills is not None and skill['name'] not in bound_skills: continue skills_to_index.append(skill) diff --git a/tests/unit_tests/provider/test_skill_tools.py b/tests/unit_tests/provider/test_skill_tools.py index dc7cd182..867d5dc1 100644 --- a/tests/unit_tests/provider/test_skill_tools.py +++ b/tests/unit_tests/provider/test_skill_tools.py @@ -22,7 +22,6 @@ def _make_skill_data( instructions='Do something', package_root='', entry_file='SKILL.md', - auto_activate=True, **kwargs, ): return { @@ -32,7 +31,6 @@ def _make_skill_data( 'instructions': instructions, 'package_root': package_root, 'entry_file': entry_file, - 'auto_activate': auto_activate, **kwargs, } @@ -150,6 +148,30 @@ class TestSkillActivationHelper: assert activation.cleaned_content == 'Working on it.' assert set(query.variables[ACTIVATED_SKILLS_KEY].keys()) == {'primary', 'aux'} + def test_prepare_skill_activation_ignores_skills_not_bound_to_pipeline(self): + from langbot.pkg.skill.activation import prepare_skill_activation + from langbot.pkg.provider.tools.loaders.skill import ACTIVATED_SKILLS_KEY, PIPELINE_BOUND_SKILLS_KEY + from langbot.pkg.skill.manager import SkillManager + + ap = _make_ap() + mgr = SkillManager(ap) + mgr.skills = { + 'primary': _make_skill_data(name='primary', instructions='Primary instructions'), + 'hidden': _make_skill_data(name='hidden', instructions='Hidden instructions'), + } + ap.skill_mgr = mgr + + query = SimpleNamespace(variables={PIPELINE_BOUND_SKILLS_KEY: ['primary']}, use_funcs=[]) + activation = prepare_skill_activation( + ap, + query, + '[ACTIVATE_SKILL: hidden]\n[ACTIVATE_SKILL: primary]\nWorking on it.', + ) + + assert activation is not None + assert activation.activated_skill_names == ['primary'] + assert set(query.variables[ACTIVATED_SKILLS_KEY].keys()) == {'primary'} + class TestSkillPathHelpers: def test_get_visible_skills_filters_by_bound_names(self): @@ -252,7 +274,6 @@ class TestSkillAuthoringToolLoader: 'display_name': 'Prompt Skill', 'description': 'Prompt only skill', 'instructions': 'Follow these steps carefully.', - 'auto_activate': False, }, SimpleNamespace(), ) @@ -263,7 +284,6 @@ class TestSkillAuthoringToolLoader: 'display_name': 'Prompt Skill', 'description': 'Prompt only skill', 'instructions': 'Follow these steps carefully.', - 'auto_activate': False, } ) assert result == { @@ -342,7 +362,6 @@ class TestSkillAuthoringToolLoader: 'name': 'time-now', 'description': 'Fixed to Beijing time', 'instructions': 'Always use Asia/Shanghai and never offer other timezones.', - 'auto_activate': True, }, SimpleNamespace(), ) @@ -353,7 +372,6 @@ class TestSkillAuthoringToolLoader: 'name': 'time-now', 'description': 'Fixed to Beijing time', 'instructions': 'Always use Asia/Shanghai and never offer other timezones.', - 'auto_activate': True, }, ) assert result == { @@ -400,7 +418,6 @@ class TestSkillAuthoringToolLoader: 'display_name': 'Cloned Skill', 'description': 'Imported from clone', 'instructions': 'Do work', - 'auto_activate': True, } ), create_skill=AsyncMock(return_value=_make_skill_data(name='cloned-skill', package_root='/repo/root')), @@ -430,7 +447,6 @@ class TestSkillAuthoringToolLoader: 'description': 'Imported from clone', 'instructions': 'Do work', 'package_root': os.path.realpath(repo_dir), - 'auto_activate': True, } ) assert result['imported'] is True diff --git a/tests/unit_tests/test_skill_service.py b/tests/unit_tests/test_skill_service.py index acba02bd..178563f3 100644 --- a/tests/unit_tests/test_skill_service.py +++ b/tests/unit_tests/test_skill_service.py @@ -15,14 +15,11 @@ def _create_skill_file( name: str = 'imported-skill', display_name: str = '', description: str = 'Imported from local directory', - auto_activate: bool = True, body: str = 'Skill instructions', ) -> None: frontmatter = ['name: ' + name, 'description: ' + description] if display_name: frontmatter.insert(1, 'display_name: ' + display_name) - if not auto_activate: - frontmatter.append('auto_activate: false') path.write_text( '---\n' + '\n'.join(frontmatter) + f'\n---\n\n{body}\n', @@ -83,7 +80,6 @@ async def test_create_skill_import_preserves_existing_skill_content_when_form_fi source_dir / 'SKILL.md', display_name='Imported Skill', description='Imported description', - auto_activate=False, body='Original instructions', ) @@ -96,7 +92,6 @@ async def test_create_skill_import_preserves_existing_skill_content_when_form_fi 'package_root': str(managed_root.resolve()), 'description': 'Imported description', 'instructions': 'Original instructions', - 'auto_activate': False, } ) @@ -115,7 +110,6 @@ async def test_create_skill_import_preserves_existing_skill_content_when_form_fi content = (managed_root / 'SKILL.md').read_text(encoding='utf-8') assert 'display_name: Imported Skill' in content assert 'description: Imported description' in content - assert 'auto_activate: false' in content assert content.endswith('Original instructions') @@ -139,7 +133,6 @@ async def test_create_skill_reuses_existing_managed_directory_without_copying(tm 'package_root': str(managed_root.resolve()), 'description': 'Already managed', 'instructions': 'Managed instructions', - 'auto_activate': True, } ) @@ -167,11 +160,7 @@ def _build_skill_archive() -> bytes: with zipfile.ZipFile(stream, 'w') as archive: archive.writestr( 'demo-repo-main/skills/nested-skill/SKILL.md', - '---\n' - 'name: imported-skill\n' - 'description: Imported from GitHub archive\n' - '---\n\n' - 'Skill instructions\n', + '---\nname: imported-skill\ndescription: Imported from GitHub archive\n---\n\nSkill instructions\n', ) return stream.getvalue() @@ -336,7 +325,6 @@ async def test_update_skill_rejects_package_root_change(tmp_path): 'display_name': 'Writer', 'description': 'Writes things', 'instructions': 'Do work', - 'auto_activate': True, } ) diff --git a/web/src/app/home/skills/components/skill-form/SkillForm.tsx b/web/src/app/home/skills/components/skill-form/SkillForm.tsx index 9db5d2c3..93b32171 100644 --- a/web/src/app/home/skills/components/skill-form/SkillForm.tsx +++ b/web/src/app/home/skills/components/skill-form/SkillForm.tsx @@ -3,7 +3,6 @@ import { useTranslation } from 'react-i18next'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Textarea } from '@/components/ui/textarea'; -import { Switch } from '@/components/ui/switch'; import { Button } from '@/components/ui/button'; import { FolderSearch, ChevronDown, ChevronRight } from 'lucide-react'; import { httpClient } from '@/app/infra/http/HttpClient'; @@ -30,7 +29,6 @@ const emptySkillDraft: SkillFormDraft = { description: '', instructions: '', package_root: '', - auto_activate: true, }, showAdvanced: false, }; @@ -95,7 +93,6 @@ export default function SkillForm({ description: prev.description || result.description, package_root: result.package_root, instructions: result.instructions, - auto_activate: result.auto_activate ?? true, })); toast.success(t('skills.scanSuccess')); } catch (error) { @@ -123,7 +120,6 @@ export default function SkillForm({ display_name: skill.display_name || '', description: skill.description || '', instructions: skill.instructions || '', - auto_activate: skill.auto_activate ?? true, }; try { @@ -203,23 +199,6 @@ export default function SkillForm({ /> -
-
- -

- {t('skills.autoActivateDescription')} -

-
- - setSkill({ ...skill, auto_activate: checked }) - } - /> -
-