Retrieves skills from a directory of Markdown skill files (Claude skills format). Supports two retrieval modes: * ``"template"`` – keyword-based task-type detection, zero latency * ``"embedding"`` – cosine similarity via SentenceTransformer
| 140 | # ------------------------------------------------------------------ # |
| 141 | |
| 142 | class SkillManager: |
| 143 | """ |
| 144 | Retrieves skills from a directory of Markdown skill files (Claude skills format). |
| 145 | |
| 146 | Supports two retrieval modes: |
| 147 | * ``"template"`` – keyword-based task-type detection, zero latency |
| 148 | * ``"embedding"`` – cosine similarity via SentenceTransformer |
| 149 | """ |
| 150 | |
| 151 | def __init__( |
| 152 | self, |
| 153 | skills_dir: str, |
| 154 | retrieval_mode: str = "template", |
| 155 | embedding_model_path: Optional[str] = None, |
| 156 | task_specific_top_k: Optional[int] = None, |
| 157 | ): |
| 158 | if retrieval_mode not in ("template", "embedding"): |
| 159 | raise ValueError( |
| 160 | f"retrieval_mode must be 'template' or 'embedding', got '{retrieval_mode}'" |
| 161 | ) |
| 162 | if not os.path.isdir(skills_dir): |
| 163 | raise FileNotFoundError(f"Skills directory not found: {skills_dir}") |
| 164 | |
| 165 | self._skills_dir = skills_dir |
| 166 | self.retrieval_mode = retrieval_mode |
| 167 | self.embedding_model_path = embedding_model_path or "Qwen/Qwen3-Embedding-0.6B" |
| 168 | self.task_specific_top_k = task_specific_top_k |
| 169 | |
| 170 | self._embedding_model = None |
| 171 | self._skill_embeddings_cache: Optional[Dict] = None |
| 172 | |
| 173 | # Monotonically-increasing counter. Incremented each time new skills are |
| 174 | # successfully added via add_skills(). Used by the RL trainer to discard |
| 175 | # training samples collected before a skill evolution (MAML support/query |
| 176 | # separation: samples that triggered evolution are not reused for RL updates). |
| 177 | self.generation: int = 0 |
| 178 | |
| 179 | self.skills = self._load_skills() |
| 180 | |
| 181 | n_gen = len(self.skills.get("general_skills", [])) |
| 182 | n_task = sum(len(v) for v in self.skills.get("task_specific_skills", {}).values()) |
| 183 | n_mistakes = len(self.skills.get("common_mistakes", [])) |
| 184 | logger.info( |
| 185 | "[SkillManager] loaded %d general + %d task-specific + %d mistakes " |
| 186 | "from %s | mode=%s", |
| 187 | n_gen, n_task, n_mistakes, skills_dir, retrieval_mode, |
| 188 | ) |
| 189 | |
| 190 | if retrieval_mode == "embedding": |
| 191 | self._compute_skill_embeddings() |
| 192 | |
| 193 | # ------------------------------------------------------------------ # |
| 194 | # Loading # |
| 195 | # ------------------------------------------------------------------ # |
| 196 | |
| 197 | def _load_skills(self) -> Dict[str, Any]: |
| 198 | """Scan skills_dir for */SKILL.md files and parse each into the internal dict.""" |
| 199 | result: Dict[str, Any] = { |
no outgoing calls