[{'Text': 'Edited /Users/peterdonaghey/Projects/point-topic-mcp/_explore_gbs.py:\n\n```diff\n@@ -1,5 +1,5 @@\n-"""Final tool design — inline test of minimal GBS tools."""\n-import json, os, subprocess\n+"""Final GBS tool design — single mongosh call per invocation."""\n+import json, os, subprocess, time\n from collections import defaultdict\n \n URI = os.environ["PT_RESEARCH_DATABASE_URI"]\n@@ -11,56 +11,35 @@\n         u = f"{u}{sep}tlsAllowInvalidCertificates=true"\n     return u\n \n-def run(js, timeout=30):\n+def run_js(js, timeout=30):\n+    """Single mongosh call. Returns parsed JSON."""\n     r = subprocess.run(["mongosh", _uri(), "--eval", js, "--quiet"],\n                        capture_output=True, text=True, timeout=timeout)\n     if r.returncode != 0: raise RuntimeError(r.stderr or r.stdout)\n-    out = r.stdout.strip()\n-    return json.loads(out) if out else []\n+    return json.loads(r.stdout.strip()) if r.stdout.strip() else None\n \n-def find(js, timeout=30):\n-    """Run JS that returns a single EJSON value."""\n-    return run(js, timeout)\n+# ─── THE ENTIRE GBS API IN 2 FUNCTIONS ───────────────────────────\n \n-# ─── PROTOTYPE TOOLS ─────────────────────────────────────────────\n-\n-def get_current_period():\n-    """Get the current stats period from GlobalVariables."""\n-    docs = run("EJSON.stringify(db.GlobalVariables.find({isCurrent: true}).toArray())")\n-    if not docs:\n-        return None\n-    gv = docs[0]\n-    period = gv.get("currentStatsPeriod")\n-    tariffs_period = gv.get("currentTariffsPeriod")\n-    updated_by = gv.get("updatedBy", "")\n-    gv_id = gv.get("_id", {}).get("$oid", "")\n-    return {\n-        "stats_period": period,\n-        "tariffs_period": tariffs_period,\n-        "updated_by": updated_by,\n-        "gv_id": gv_id,\n-        "full": gv,\n-    }\n-\n-def get_operator_status(country, operator_name=None, period=None):\n+def gbs_get_status(country: str, operator: str = None, period: str = None):\n     """\n-    Hierarchical GBS status.\n+    ONE mongosh call. Returns status at any level.\n     \n-    - No period → uses current\n-    - No operator → all operators in country\n-    - Both → specific operator\n+    Levels:\n+      country only      → roll-up: total, with data, without, top operators\n+      + operator        → that operator\'s detail for period\n+      + period          → that specific period (default: current)\n     """\n-    current = get_current_period()\n-    period = period or current["stats_period"]\n+    # Build the JS that does everything in one eval\n+    op_filter = f\'name: {json.dumps(operator)}, \' if operator else \'\'\n     \n-    # Build aggregation pipeline\n-    match = {"country": country}\n-    if operator_name:\n-        match["name"] = operator_name\n-    match["isArchived"] = {"$ne": True}\n-    \n+    # We build the aggregation pipeline conditionally\n     pipeline = [\n-        {"$match": match},\n+        {"$match": {"country": country, "isArchived": {"$ne": True}}},\n+    ]\n+    if operator:\n+        pipeline[0]["$match"]["name"] = operator\n+    \n+    pipeline += [\n         {"$lookup": {\n             "from": "Statistics",\n             "let": {"oid": "$_id"},\n@@ -68,153 +47,194 @@\n                 {"$match": {"$expr": {\n                     "$and": [\n                         {"$eq": ["$operatorId", "$$oid"]},\n-                        {"$eq": ["$period", period]}\n+                        {"$eq": ["$period", period]} if period else {"$eq": ["$period", "$$periodVal"]}\n                     ]\n                 }}}\n             ],\n             "as": "stats"\n         }},\n         {"$addFields": {"statCount": {"$size": "$stats"}}},\n-        {"$project": {"name": 1, "technologies": 1, "statCount": 1, "stats": 1}},\n+        {"$project": {"name": 1, "techs": "$technologies", "statCount": 1, "stats": 1}},\n         {"$sort": {"name": 1}},\n     ]\n     \n-    operators = run(f"EJSON.stringify(db.Operator.aggregate({json.dumps(pipeline)}).toArray())", timeout=60)\n+    # Single JS eval: get period + operators\n+    period_expr = json.dumps(period) if period else "null"\n     \n-    result = {\n-        "current_period": period,\n-        "country": country,\n-        "total_operators": len(operators),\n-    }\n+    # Simpler approach: hardcode the period variable by resolving in JS\n+    js = f"""\n+    const gv = db.GlobalVariables.findOne({{isCurrent: true}});\n+    const currentPeriod = gv ? gv.currentStatsPeriod : null;\n+    const targetPeriod = {period_expr} || currentPeriod;\n     \n-    if operator_name:\n+    const pipeline = {json.dumps(pipeline)};\n+    \n+    // Replace placeholder $periodVal with actual target period\n+    const pipelineStr = JSON.stringify(pipeline).replace(\'"$periodVal"\', JSON.stringify(targetPeriod));\n+    const fixedPipeline = JSON.parse(pipelineStr);\n+    \n+    const operators = db.Operator.aggregate(fixedPipeline).toArray();\n+    EJSON.stringify({{currentPeriod, targetPeriod, operators}});\n+    """\n+    \n+    result = run_js(js, timeout=60)\n+    if not result:\n+        return {"error": "No data returned"}\n+    \n+    cp = result["currentPeriod"]\n+    tp = result["targetPeriod"]\n+    operators = result["operators"]\n+    \n+    if operator:\n         if not operators:\n-            return {**result, "error": f"Operator \'{operator_name}\' not found in {country}"}\n-        op = operators[0]\n-        result["operator"] = _analyze_operator(op, current["stats_period"])\n-        return result\n+            return {"error": f"Operator \'{operator}\' not found in {country}", "period": tp}\n+        return _format_operator(operators[0], cp)\n     \n-    # Country roll-up\n+    return _format_country(operators, cp, country)\n+\n+\n+def _format_country(operators, current_period, country):\n+    """Country-level roll-up."""\n     with_data = [o for o in operators if o["statCount"] > 0]\n     without = [o for o in operators if o["statCount"] == 0]\n-    result["with_data"] = len(with_data)\n-    result["without_data"] = len(without)\n-    result["coverage_pct"] = round(100 * len(with_data) / max(len(operators), 1), 1)\n-    result["operators_with_data"] = [{\n-        "name": o["name"], "records": o["statCount"],\n-        "techs": len(o.get("technologies", []))\n-    } for o in sorted(with_data, key=lambda x: -x["statCount"])[:50]]\n-    result["operators_without_data"] = [o["name"] for o in without[:30]]\n-    return result\n+    total = len(operators)\n+    \n+    return {\n+        "period": current_period,\n+        "country": country,\n+        "total_operators": total,\n+        "with_data": len(with_data),\n+        "without_data": len(without),\n+        "coverage_pct": round(100 * len(with_data) / max(total, 1), 1),\n+        "operators_with_data": [\n+            {"name": o["name"], "records": o["statCount"]}\n+            for o in sorted(with_data, key=lambda x: -x["statCount"])[:50]\n+        ],\n+        "operators_without_data": [o["name"] for o in without[:30]],\n+    }\n \n-def _analyze_operator(op, current_period):\n-    """Analyze a single operator: what exists, gaps, state distribution."""\n+\n+def _format_operator(op, current_period):\n+    """Single operator detail."""\n     name = op["name"]\n-    techs = op.get("technologies", [])\n     stats = op.get("stats", [])\n+    techs = op.get("techs", [])\n     \n-    VALID_TYPES = ["broadband", "mobile", "iptv"]\n-    VALID_CHANNELS = ["Infrastructure", "Retail"]\n-    VALID_DOMAINS = ["Residential", "Business", "Total"]\n-    \n-    # What exists\n+    # Existing records analysis\n     by_state = defaultdict(int)\n     by_type = defaultdict(int)\n-    by_tech = defaultdict(int)\n-    combos = set()\n+    records = []\n+    seen_combos = set()\n     \n     for s in stats:\n-        by_state[s.get("state")] += 1\n+        st = s.get("state", 0)\n+        by_state[st] += 1\n         by_type[s.get("type", "?")] += 1\n-        by_tech[s.get("tech", "?")] += 1\n-        combos.add((s.get("type"), s.get("tech"), s.get("channel"), s.get("domain")))\n+        combo = (s.get("type"), s.get("tech"), s.get("channel"), s.get("domain"))\n+        seen_combos.add(combo)\n+        records.append({\n+            "type": s.get("type"),\n+            "tech": s.get("tech"),\n+            "channel": s.get("channel"),\n+            "domain": s.get("domain"),\n+            "subscribers": s.get("subscribers"),\n+            "state": {1: "pending", 2: "approved", 3: "published"}.get(st, str(st)),\n+            "is_estimate": s.get("is_estimate", False),\n+        })\n+    \n+    # Gap analysis: only check combos that could reasonably be expected\n+    # base this on techs that actually appear in the data (if any) or the first 3 techs\n+    techs_in_use = sorted(set(s.get("tech") for s in stats)) or techs[:3]\n+    if not techs_in_use:\n+        techs_in_use = ["?"]  \n+    \n+    gaps = []\n+    for t in ["broadband", "mobile", "iptv"]:\n+        for tech in techs_in_use:\n+            for ch in ["Infrastructure", "Retail"]:\n+                for dom in ["Residential", "Business", "Total"]:\n+                    if (t, tech, ch, dom) not in seen_combos:\n+                        gaps.append(f"{t}/{tech}/{ch}/{dom}")\n     \n     state_names = {1: "pending", 2: "approved", 3: "published"}\n-    \n-    # Fill count: how many {type, tech, channel, domain} combos are populated\n-    filled = len(combos)\n-    \n-    # Expected combos: only for types that exist in the operator\'s data or all types if no data\n-    techs_in_use = sorted(set(s.get("tech") for s in stats)) or techs[:3]\n-    expected = len(VALID_TYPES) * len(techs_in_use) * len(VALID_CHANNELS) * len(VALID_DOMAINS)\n-    \n-    # Gaps by type/tech\n-    gaps_by_type = defaultdict(list)\n-    for t in VALID_TYPES:\n-        for tech in techs_in_use:\n-            for ch in VALID_CHANNELS:\n-                for dom in VALID_DOMAINS:\n-                    if (t, tech, ch, dom) not in combos:\n-                        gaps_by_type[t].append(f"{tech}/{ch}/{dom}")\n     \n     return {\n         "name": name,\n         "operator_id": op.get("_id", {}).get("$oid", ""),\n         "technologies": techs,\n         "total_records": len(stats),\n-        "filled_combos": filled,\n-        "gap_combos": expected - filled,\n+        "filled_combos": len(seen_combos),\n+        "gap_count": len(gaps),\n         "states": {state_names.get(k, str(k)): v for k, v in sorted(by_state.items())},\n         "by_type": dict(by_type),\n-        "by_tech": dict(by_tech),\n-        "gaps": {t: glist for t, glist in gaps_by_type.items() if glist},\n+        "records": records,\n+        "gaps": gaps,\n     }\n \n \n-# ─── INLINE TESTS ────────────────────────────────────────────────\n+# ─── KEEP EXISTING: add_statistic, create_source, list_operators ─\n+# These already work well and don\'t need changes except adding a \n+# helper to generate an admin URL.\n \n-print("═══ TEST 1: get_current_period() ═══")\n-cp = get_current_period()\n-print(f"Stats period: {cp[\'stats_period\']}")\n-print(f"Tariffs period: {cp[\'tariffs_period\']}")\n-print(f"Updated by: {cp[\'updated_by\']}")\n \n-print("\\n═══ TEST 2: Country status (UK, no operator) ═══")\n-uk = get_operator_status("United Kingdom")\n-print(f"Current period: {uk[\'current_period\']}")\n-print(f"Total UK operators: {uk[\'total_operators\']}")\n-print(f"With data: {uk[\'with_data\']}, Without: {uk[\'without_data\']} ({uk[\'coverage_pct\']}%)")\n-print(f"\\nTop operators with data:")\n-for op in uk["operators_with_data"][:5]:\n-    print(f"  {op[\'name\'][:30]:30s} {op[\'records\']} records")\n-print(f"\\nOperators without data (first 5): {uk[\'operators_without_data\'][:5]}")\n+# ─── TESTS ───────────────────────────────────────────────────────\n \n-print("\\n═══ TEST 3: Operator status (BT Group, 2025Q3) ═══")\n-bt = get_operator_status("United Kingdom", "BT Group", "2025Q3")\n-op = bt["operator"]\n-print(f"Operator: {op[\'name\']} ({op[\'operator_id\']})")\n-print(f"Records: {op[\'total_records\']}, Filled combos: {op[\'filled_combos\']}, Gaps: {op[\'gap_combos\']}")\n-print(f"States: {op[\'states\']}")\n-print(f"By type: {op[\'by_type\']}")\n-print(f"By tech: {op[\'by_tech\']}")\n-# Show first few gaps per type\n-for t, glist in op[\'gaps\'].items():\n-    print(f"  {t} gaps (showing first 3): {glist[:3]}")\n-\n-print("\\n═══ TEST 4: Operator status (unknown operator) ═══")\n-unknown = get_operator_status("United Kingdom", "NoSuchOperator")\n-print(f"Error expected: {unknown.get(\'error\', \'none\')}")\n-\n-print("\\n═══ TEST 5: Operator with NO data (first from list) ═══")\n-# Find first operator without data\n-empty = get_operator_status("United Kingdom")\n-first_empty = empty["operators_without_data"][0] if empty["operators_without_data"] else None\n-if first_empty:\n-    print(f"Checking: {first_empty}")\n-    empty_op = get_operator_status("United Kingdom", first_empty)\n-    print(f"Records: {empty_op[\'operator\'][\'total_records\']}, Gaps: {empty_op[\'operator\'][\'gap_combos\']}")\n-    print(f"Technologies: {empty_op[\'operator\'][\'technologies\']}")\n-\n-print("\\n═══ TEST 6: Performance ═══")\n-import time\n+print("═══ 1. Country status (UK) ═══")\n t0 = time.time()\n-_ = get_operator_status("United Kingdom")\n+uk = gbs_get_status("United Kingdom")\n t1 = time.time()\n-print(f"Country-level query: {t1-t0:.2f}s")\n+print(f"  Time: {t1-t0:.2f}s")\n+print(f"  Period: {uk[\'period\']}")\n+print(f"  Total: {uk[\'total_operators\']}, With: {uk[\'with_data\']}, Without: {uk[\'without_data\']} ({uk[\'coverage_pct\']}%)")\n+print(f"  Top 3: {[o[\'name\'] for o in uk[\'operators_with_data\'][:3]]}")\n+print(f"  First 3 without: {uk[\'operators_without_data\'][:3]}")\n \n+print("\\n═══ 2. Operator detail (AllPoints Fibre) ═══")\n t0 = time.time()\n-_ = get_operator_status("United Kingdom", "BT Group", "2025Q3")\n+op = gbs_get_status("United Kingdom", "AllPoints Fibre")\n t1 = time.time()\n-print(f"Operator-level query: {t1-t0:.2f}s")\n+print(f"  Time: {t1-t0:.2f}s")\n+print(f"  Name: {op[\'name\']}")\n+print(f"  Records: {op[\'total_records\']}, Combos: {op[\'filled_combos\']}, Gaps: {op[\'gap_count\']}")\n+print(f"  States: {op[\'states\']}")\n+print(f"  By type: {op[\'by_type\']}")\n+print(f"  Records:")\n+for r in op[\'records\']:\n+    print(f"    {r[\'type\']:10s} {r[\'tech\']:15s} {r[\'channel\']:15s} {r[\'domain\']:12s} subs={r[\'subscribers\']:>8} ({r[\'state\']})")\n+print(f"  Gaps (first 5): {op[\'gaps\'][:5]}")\n+\n+print("\\n═══ 3. Operator detail (BT Group, 2025Q3) ═══")\n+t0 = time.time()\n+bt = gbs_get_status("United Kingdom", "BT Group", "2025Q3")\n+t1 = time.time()\n+print(f"  Time: {t1-t0:.2f}s")\n+print(f"  Name: {bt[\'name\']}")\n+print(f"  Records: {bt[\'total_records\']}, Combos: {bt[\'filled_combos\']}, Gaps: {bt[\'gap_count\']}")\n+print(f"  States: {bt[\'states\']}")\n+for r in bt[\'records\']:\n+    print(f"    {r[\'type\']:10s} {r[\'tech\']:15s} {r[\'channel\']:15s} {r[\'domain\']:12s} subs={r[\'subscribers\']:>8} ({r[\'state\']})")\n+\n+print("\\n═══ 4. Non-existent operator ═══")\n+err = gbs_get_status("United Kingdom", "NoSuchOp")\n+print(f"  Error: {err.get(\'error\')}")\n+\n+print("\\n═══ 5. Operator with NO data ═══")\n+nodata = gbs_get_status("United Kingdom", "186K")\n+print(f"  Name: {nodata.get(\'name\', \'N/A\')}")\n+print(f"  Records: {nodata[\'total_records\']}, Gaps: {nodata[\'gap_count\']}")\n+print(f"  Techs: {nodata[\'technologies\']}")\n+\n+print("\\n═══ 6. Admin URL helper ═══")\n+ADMIN_BASE = "https://research-app.pt-research.net/admin"\n+def gbs_admin_url(country=None, operator_id=None):\n+    parts = [ADMIN_BASE]\n+    if country:\n+        parts.append(f"?country={country}")\n+    if operator_id:\n+        sep = "&" if country else "?"\n+        parts.append(f"{sep}operator={operator_id}")\n+    return "".join(parts)\n+print(f"  UK: {gbs_admin_url(country=\'United Kingdom\')}")\n+print(f"  BT: {gbs_admin_url(country=\'United Kingdom\', operator_id=bt.get(\'operator_id\', \'\'))}")\n \n print("\\n═══ DONE ═══")\n\n```'}]