diff --git a/gns3server/agent/gns3_copilot/skills/loader.py b/gns3server/agent/gns3_copilot/skills/loader.py index 307b81e89..4d585cac5 100644 --- a/gns3server/agent/gns3_copilot/skills/loader.py +++ b/gns3server/agent/gns3_copilot/skills/loader.py @@ -98,7 +98,7 @@ class SkillsLoader: def load_device_skills(self) -> Dict[str, Dict[str, Any]]: """ - Load all device/feature skills from YAML files. + Load all device skills from YAML files. Returns: Dictionary mapping skill keys to skill definitions @@ -131,7 +131,48 @@ class SkillsLoader: except Exception as e: logger.error(f"Failed to load skill from {yaml_file}: {e}") - logger.debug(f"Loaded {len(skills)} device skills from {device_dir}") + logger.debug(f"Loaded {len(skills)} device skills from device directory") + return skills + + def load_feature_skills(self) -> Dict[str, Dict[str, Any]]: + """ + Load all feature skills from YAML files. + + Feature skills are network planning and design functionalities + (e.g., topology planner), not device-specific features. + + Returns: + Dictionary mapping skill keys to skill definitions + """ + if yaml is None: + logger.error("PyYAML is not installed. Cannot load skills from YAML.") + return {} + + skills = {} + feature_dir = self.skills_dir / "feature" + + if not feature_dir.exists(): + logger.warning(f"Feature skills directory not found: {feature_dir}") + return {} + + for yaml_file in feature_dir.glob("*.yaml"): + try: + skill_data = self._load_yaml(yaml_file) + if not skill_data: + logger.warning(f"Skipping empty YAML file: {yaml_file}") + continue + # Use device_type from YAML content as the key + # Fallback to filename stem if device_type not present + skill_key = skill_data.get("device_type") if isinstance(skill_data, dict) else None + if not skill_key: + skill_key = yaml_file.stem + logger.warning(f"No device_type in {yaml_file}, using filename '{skill_key}' as key") + skills[skill_key] = skill_data + logger.debug(f"Loaded feature skill: {skill_key} from {yaml_file}") + except Exception as e: + logger.error(f"Failed to load feature skill from {yaml_file}: {e}") + + logger.debug(f"Loaded {len(skills)} feature skills from feature directory") return skills def load_prompt(self, prompt_name: str) -> str: diff --git a/gns3server/agent/gns3_copilot/skills/manager.py b/gns3server/agent/gns3_copilot/skills/manager.py index 12fb3d39e..6a663e0e8 100644 --- a/gns3server/agent/gns3_copilot/skills/manager.py +++ b/gns3server/agent/gns3_copilot/skills/manager.py @@ -227,9 +227,17 @@ class SkillsManager: logger.error(f"Invalid skill format for {skill_key}, skipping") continue - # Load new device/feature skills from YAML files + # Load new device skills from YAML files new_device_skills = self.loader.load_device_skills() + # Load new feature skills from YAML files + new_feature_skills = self.loader.load_feature_skills() + + # Merge device and feature skills into single registry + all_skills = {} + all_skills.update(new_device_skills) + all_skills.update(new_feature_skills) + # Update registries (safe replace - never leaves dict empty) for k in list(INJECTION_SKILLS_REGISTRY): if k not in new_injection_skills: @@ -237,11 +245,11 @@ class SkillsManager: INJECTION_SKILLS_REGISTRY.update(new_injection_skills) for k in list(SKILLS_REGISTRY): - if k not in new_device_skills: + if k not in all_skills: del SKILLS_REGISTRY[k] - SKILLS_REGISTRY.update(new_device_skills) + SKILLS_REGISTRY.update(all_skills) - logger.info(f"Loaded {len(new_injection_skills)} injection skills and {len(new_device_skills)} device skills") + logger.info(f"Loaded {len(new_injection_skills)} injection skills, {len(new_device_skills)} device skills, and {len(new_feature_skills)} feature skills") return True except Exception as e: logger.error(f"Failed to reload skills: {e}")