"""Capability Library loader + lightweight keyword pre-filter. The design uses a vector ANN pre-filter to cut LLM calls ~60%. For the MVP we approximate that with keyword overlap scoring (no embedding service required). The interface is the same: given a signal, return candidate capabilities. CAPABILITY SETS: the loop can target a subset of the portfolio at runtime. - "ALL" (default): data/capabilities.json — the full portfolio. - "": data/capability_sets/.json — a one-off targeted set (same schema). Add new products by dropping another file in that folder and passing `loop --capability `. """ from __future__ import annotations import json import re from functools import lru_cache from . import config # Selected via set_active_set() before the loop runs; "ALL" = full portfolio. _active_set = "ALL" def available_sets() -> list[str]: sets_dir = config.DATA_DIR / "capability_sets" names = sorted(p.stem for p in sets_dir.glob("*.json")) if sets_dir.exists() else [] return ["ALL"] + names def set_active_set(name: str) -> None: """Select which capability library the loop reasons against.""" global _active_set name = (name or "ALL").strip() if name.upper() == "ALL": _active_set = "ALL" else: path = config.DATA_DIR / "capability_sets" / f"{name}.json" if not path.exists(): raise SystemExit( f"Unknown capability set '{name}'. Available: {', '.join(available_sets())}" ) _active_set = name load_library.cache_clear() def active_set() -> str: return _active_set @lru_cache(maxsize=1) def load_library() -> dict: path = (config.DATA_DIR / "capabilities.json" if _active_set == "ALL" else config.DATA_DIR / "capability_sets" / f"{_active_set}.json") try: lib = json.loads(path.read_text()) except json.JSONDecodeError as e: raise SystemExit(f"Capability set '{_active_set}' has invalid JSON " f"({path.name}, line {e.lineno}): {e.msg}. Fix the file and re-run.") if not lib.get("capabilities"): raise SystemExit(f"Capability set '{_active_set}' ({path.name}) has no 'capabilities'.") return lib def capability_by_id(cap_id: str) -> dict | None: for cap in load_library()["capabilities"]: if cap["id"] == cap_id: return cap return None def _tokens(text: str) -> set[str]: return set(re.findall(r"[a-z]{3,}", text.lower())) def prefilter(signal_content: str, top_k: int = 3) -> list[dict]: """Return capabilities whose keywords overlap the signal, best first. Stand-in for the vector ANN pre-filter in the design (section 5.2). In a targeted set (<=5 capabilities) the pre-filter is skipped: the whole point of a targeted run is to evaluate EVERY signal against the chosen product, and the LLM prompt stays small anyway. """ caps = load_library()["capabilities"] if _active_set != "ALL" and len(caps) <= 5: return caps sig_tokens = _tokens(signal_content) scored: list[tuple[float, dict]] = [] for cap in load_library()["capabilities"]: kw = set(k.lower() for k in cap.get("keywords", [])) kw_tokens: set[str] = set() for k in kw: kw_tokens |= _tokens(k) overlap = len(sig_tokens & kw_tokens) if overlap: scored.append((overlap, cap)) scored.sort(key=lambda x: x[0], reverse=True) return [cap for _, cap in scored[:top_k]] def library_summary_for_prompt(candidates: list[dict]) -> str: """Compact capability descriptions for the relevance-filter prompt.""" lines = [] for c in candidates: lines.append( f"- {c['id']} | {c['name']}: {c['description']}\n" f" Buying signals: {'; '.join(c['buyingSignals'])}\n" f" Anti-signals: {'; '.join(c['antiSignals'])}\n" f" Target buyer: {', '.join(c['targetBuyer']['titles'])}" ) return "\n".join(lines)