#!/usr/bin/env python3 """ get_indicators.py — sliced catalog accessor for Step 2/3. Given a list of indicator ids, returns ONLY those catalog entries and ONLY the fields needed for binding. This is the context fix: instead of the agent reading whole cluster YAML files (wash.yaml = 85 KB, cccm.yaml = 65 KB), it calls this script and gets a targeted ~1–3 KB slice per indicator. The agent MUST use this script to access catalog content during Steps 2 and 3. It MUST NOT read whole catalog/*.yaml cluster files directly. Usage: python3 scripts/get_indicators.py \\ --ids rcsi jmp_water_basic jmp_water_safely_managed \\ [--fields definition,formula,thresholds,common_implementation_errors,ki_assessment_note] Output JSON (keyed by id): { "rcsi": { "id": "rcsi", "label": "Reduced Coping Strategies Index", "cluster": "food_security", "definition": "...", # verbatim from catalog "common_implementation_errors": [...], "ki_assessment_note": "...", ... }, ... } If an id is not found in any cluster, it appears in "not_found" list in the response. Default --fields: definition, formula, thresholds, common_implementation_errors, ki_assessment_note, components_note (Omits: provenance, synonyms, step1_framework_anchor, full components detail — fields that add bulk without helping the binding decision.) """ import argparse import json import os import sys try: import yaml except ImportError: sys.exit("get_indicators.py requires pyyaml: pip3 install pyyaml --break-system-packages") SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) SKILL_DIR = os.path.dirname(SCRIPT_DIR) CATALOG_DIR = os.path.join(SKILL_DIR, "catalog") INDEX_PATH = os.path.join(CATALOG_DIR, "index.yaml") CLUSTER_FILES = { "food_security": "food_security.yaml", "wash": "wash.yaml", "cccm": "cccm.yaml", } DEFAULT_FIELDS = [ "definition", "formula", "thresholds", "common_implementation_errors", "ki_assessment_note", "components_note", ] def load_index() -> dict: """Return {id: {label, cluster, priority}} from index.yaml.""" with open(INDEX_PATH, encoding="utf-8") as f: entries = yaml.safe_load(f) if not entries: sys.exit(f"get_indicators.py: {INDEX_PATH} is empty") return { e["id"]: { "label": e.get("label", e["id"]), "cluster": e.get("cluster", ""), "priority": bool(e.get("priority", False)), } for e in entries if isinstance(e, dict) and "id" in e } def load_cluster(cluster: str) -> dict: """Return {id: full_entry} for one cluster YAML.""" fname = CLUSTER_FILES.get(cluster) if not fname: return {} path = os.path.join(CATALOG_DIR, fname) if not os.path.exists(path): return {} with open(path, encoding="utf-8") as f: entries = yaml.safe_load(f) if not entries: return {} return {e["id"]: e for e in entries if isinstance(e, dict) and "id" in e} def project(entry: dict, fields: list) -> dict: """Return only the requested fields from an entry (plus id and label always).""" out = {"id": entry["id"], "label": entry.get("label", entry["id"])} for f in fields: if f in entry: out[f] = entry[f] return out def main(): ap = argparse.ArgumentParser( description="Return projected catalog entries for a list of indicator ids." ) ap.add_argument("--ids", nargs="+", required=True, help="Indicator ids to fetch, e.g. rcsi jmp_water_basic") ap.add_argument("--fields", default=",".join(DEFAULT_FIELDS), help=f"Comma-separated field names to include. Default: {','.join(DEFAULT_FIELDS)}") ap.add_argument("-o", "--out", default=None, help="Write JSON to this path (also prints to stdout)") a = ap.parse_args() fields = [f.strip() for f in a.fields.split(",") if f.strip()] requested_ids = a.ids index = load_index() # Determine which clusters we need and load only those clusters_needed = set() for iid in requested_ids: if iid in index: clusters_needed.add(index[iid]["cluster"]) loaded_clusters: dict[str, dict] = {} for c in clusters_needed: loaded_clusters[c] = load_cluster(c) result = {} not_found = [] for iid in requested_ids: if iid not in index: not_found.append(iid) continue cluster = index[iid]["cluster"] cluster_data = loaded_clusters.get(cluster, {}) if iid not in cluster_data: not_found.append(iid) continue entry = cluster_data[iid] # Always include cluster and priority from index (may not be in cluster YAML) entry_with_meta = dict(entry) entry_with_meta.setdefault("label", index[iid]["label"]) projected = project(entry_with_meta, fields) projected["cluster"] = cluster projected["priority"] = index[iid]["priority"] result[iid] = projected output = {"indicators": result} if not_found: output["not_found"] = not_found out_str = json.dumps(output, ensure_ascii=False, indent=2) + "\n" if a.out: with open(a.out, "w", encoding="utf-8") as f: f.write(out_str) print(f"wrote {a.out}", file=sys.stderr) sys.stdout.write(out_str) if __name__ == "__main__": main()