# GBS Tools Documentation

GBS tools provide MCP access to the PT Research App MongoDB cluster for managing Global Broadband Statistics (GBS) data. Human-supervised AI data entry for Jolanta.

## Setup

Requires:
- `mongosh` installed on the system (uses subprocess for robust MongoDB Atlas connectivity)
- `PT_RESEARCH_DATABASE_URI` environment variable set (MongoDB Atlas cluster URI)

## Tools

### `list_operators`

Discover operators in the GBS database. Use before `get_gbs_status`, `add_statistic`, `create_source`.

**Parameters:**
- `country` (str, optional): Filter by country name (exact match, e.g. "United Kingdom"). Leave empty for all.
- `limit` (int, optional): Cap the number of returned operators. 0 = no limit. Useful to prevent context overflow in agents.

**Returns:**
- Operators sorted alphabetically by name, with country, operator_id, and technologies.
- If no country filter and >400 operators, warns to use a country filter or limit.
- If limit is set, shows "N of M" at the end.

**Example:**
```
list_operators(country="United Kingdom", limit=10)
```

Output (excerpt):
```
(country filter: 'United Kingdom', 214 match(es))
Operators:
  186K | United Kingdom | id=662ff7a3f9d5751ffbf3024d | techs=['ADSL']
  AAISP | United Kingdom | id=662ff7a3f9d5751ffbf301bd | techs=['ADSL', 'FTTH', 'FTTP', 'VDSL', 'G.fast cabinet', '3G', '4G']
  ...
... showing 10 of 214 operators
```

---

### `get_gbs_status`

Check what GBS data exists for an operator/period, identify gaps, and check staleness vs. current period.

**Parameters:**
- `country` (str): Country name (exact match, e.g. "United Kingdom")
- `operator` (str): Operator name (exact match, case-sensitive)
- `period` (str): Quarter string, e.g. "2025Q1"

**Returns:**
- operator_id and technologies list
- Existing record counts broken down by type (broadband/mobile/iptv) and state (pending/approved/published)
- Staleness: shows quarter distance from current period (e.g. "2 quarters behind current (2025Q3)")
- Gaps: missing combinations grouped by type for readability

**Example:**
```
get_gbs_status(country="United Kingdom", operator="BT Group", period="2025Q3")
```

Output (excerpt):
```
GBS status: BT Group (United Kingdom) — 2025Q3
operator_id: 662ff7a3f9d5751ffbf300f6
technologies: ADSL, ADSL2+, DSL, Fibre, FTTP, FTTx, ...

Existing: 9 records
  by type: broadband: 8, iptv: 1
  by state: published: 9

Staleness: current

Gaps (369 missing combinations):
  broadband (118):
    ADSL / Infrastructure / Residential
    ADSL / Infrastructure / Business
    ...
  mobile (126):
    ...
  iptv (125):
    ...
```

---

### `add_statistic`

Insert a GBS Statistic (subscriber count) for an operator/period/technology.

Operator name and country are auto-filled from the operator record — no need to pass them.
Rejects duplicates (same operator/period/type/tech/channel/domain).

**Parameters:**
- `type` (str): broadband | mobile | iptv
- `operator_id` (str): Operator ObjectId (from list_operators or get_gbs_status)
- `period` (str): Quarter (e.g. "2025Q1") — validated format
- `tech` (str): Technology (must match operator's available technologies)
- `channel` (str): Infrastructure | Retail
- `domain` (str): Residential | Business | Total
- `subscribers` (int): Subscriber count (must be >= 0)
- `is_estimate` (bool, optional): Whether value is estimated. Default: false.
- `restated` (bool, optional): Whether value was restated. Default: false.
- `notes` (str, optional): Notes about the statistic.
- `created_by` (str, optional): Creator identifier. Default: "gbs-agent".

**Returns:**
- Inserted record ID, state, and echo of what was inserted — or rejection reason (duplicate, invalid input).

**Example:**
```
add_statistic(
  type="broadband",
  operator_id="662ff7a3f9d5751ffbf2fffe",
  period="2025Q1",
  tech="FTTP",
  channel="Infrastructure",
  domain="Residential",
  subscribers=50000,
  notes="Q1 2025 FTTP residential subscribers"
)
```

Output:
```
Created statistic 69ccd63b054a1c59505a74f0 (state: pending)
  seethelight (United Kingdom) | 2025Q1 | broadband | FTTP | Infrastructure | Residential | subscribers=50000
```

---

### `create_source`

Insert a Source (document/report reference) for an operator and period.

Rejects duplicates (same operator/year/quarter/type/url). Requires at least one of url or file_url.

**Parameters:**
- `operator_id` (str): Operator ObjectId
- `year` (int): Year (e.g. 2025) — validated range 1990-2100
- `quarter` (int): Quarter 1-4
- `type` (str): Source type (e.g. "report", "earnings_call", "tariff", "regulatory_filing") — must not be empty
- `url` (str, optional): Source URL (web link)
- `file_url` (str, optional): File URL (e.g. S3 path)

**Returns:**
- Inserted record ID with operator name and period — or rejection reason (duplicate, missing url).

**Example:**
```
create_source(
  operator_id="662ff7a3f9d5751ffbf2fffe",
  year=2025,
  quarter=1,
  type="earnings_call",
  url="https://investor.example.com/q1-2025-earnings-call"
)
```

Output:
```
Created source 69ccd63e391c82b96ea4b085
  seethelight | 2025Q1 | type=earnings_call | url=https://investor.example.com/q1-2025-earnings-call
```

---

## BSON Data Format

All inserts use BSON Date fields to align with Prisma web app expectations:

- **Statistics**: `date`, `createdAt`, `updatedAt` all set to current BSON Date
- **Sources**: `createdAt`, `updatedAt` set to current BSON Date
- **Statistics**: `is_archived` always set to `false` on insert

Web app reads these fields as-is; if missing or wrong type, the app may crash or show incomplete data.

---

## Workflow Example

1. **List UK operators** to find the one you want:
   ```
   list_operators(country="United Kingdom", limit=20)
   ```

2. **Check gaps** for a specific operator/period:
   ```
   get_gbs_status(country="United Kingdom", operator="seethelight", period="2025Q1")
   ```

3. **Add statistics** for each gap:
   ```
   add_statistic(type="broadband", operator="seethelight", operator_id="...", ...)
   ```

4. **Create sources** to reference the data:
   ```
   create_source(operator_id="...", year=2025, quarter=1, type="report", url="...")
   ```

---

## Errors & Troubleshooting

### "PT_RESEARCH_DATABASE_URI is not set"
- Export the URI before running: `export PT_RESEARCH_DATABASE_URI=$(tr -d '\n' < .mongodb_uri)`

### "mongosh not found"
- Install mongosh:
  - macOS: `brew install mongosh`
  - Ubuntu/Debian: `sudo apt install mongodb-mongosh`
  - Amazon Linux/RHEL: `sudo yum install mongodb-mongosh`

### "Operator not found"
- Check spelling and country exactly; both are case-sensitive.
- Use `list_operators()` to find the exact name.

### "Tech '<tech>' not valid for operator"
- The technology must match one of operator's registered technologies.
- Use `list_operators()` to see technologies for an operator.

### Document inserts succeed but web app shows incomplete data
- Check that `createdAt`, `updatedAt` are present and are BSON Date type (not null or string).
- Run verification script: `PT_RESEARCH_DATABASE_URI=$(tr -d '\n' < .mongodb_uri) uv run python scripts/verify_gbs_bson_shapes.py`

---

## Testing

Unit tests (no real DB needed):
```bash
uv run pytest tests/test_gbs_tools.py -v
```

Integration tests (requires PT_RESEARCH_DATABASE_URI):
```bash
PT_RESEARCH_DATABASE_URI=$(tr -d '\n' < .mongodb_uri) uv run python -m point_topic_mcp.tools.gbs_tools
```

BSON shape verification:
```bash
PT_RESEARCH_DATABASE_URI=$(tr -d '\n' < .mongodb_uri) uv run python scripts/verify_gbs_bson_shapes.py
```
