From abcf2d88e229b67a39a5ae1f845b692b3b8881ff Mon Sep 17 00:00:00 2001 From: K7ZVX Date: Tue, 5 May 2026 06:08:58 +0000 Subject: [PATCH] =?UTF-8?q?feat:=20Geographic=20context=20=E2=80=94=20loca?= =?UTF-8?q?l=20names,=20accurate=20Idaho/Utah=20geography,=20no=20Unknown?= =?UTF-8?q?=20gaps?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Magic Valley, Treasure Valley, Panhandle, Dixie — local names in all output - Southern Idaho correctly maps to Magic Valley, not lumped with Boise/IF - Unlocated nodes excluded from coverage gap recommendations - LLM response rules override brevity for mesh questions - Node detail shows region with local context Co-Authored-By: Claude Opus 4.5 --- meshai/mesh_reporter.py | 38 ++++++++++++++++++--- meshai/router.py | 73 ++++++++++++++++++++++++++++++++++------- 2 files changed, 94 insertions(+), 17 deletions(-) diff --git a/meshai/mesh_reporter.py b/meshai/mesh_reporter.py index 7e70116..f601f86 100644 --- a/meshai/mesh_reporter.py +++ b/meshai/mesh_reporter.py @@ -36,6 +36,20 @@ PORTNUM_DISPLAY = { "ATAK_FORWARDER": "ATAK", } +# Geographic context for region names +REGION_CONTEXT = { + "South Central ID": "Magic Valley (Twin Falls area)", + "South Western ID": "Treasure Valley (Boise metro)", + "South Eastern ID": "Eastern Idaho (Idaho Falls area)", + "Central ID": "Idaho backcountry (Salmon area)", + "Northern ID": "North Idaho (Panhandle)", + "Northern UT": "Northern Utah (Ogden/Logan)", + "Central UT": "Wasatch Front (Salt Lake City)", + "Eastern UT": "Eastern Utah (Vernal/Moab)", + "Western UT": "Western Utah (Tooele)", + "Southern UT": "Southern Utah / Dixie (St. George)", +} + def _clean_portnum(portnum: str) -> str: """Convert raw portnum to display name.""" @@ -298,14 +312,24 @@ class MeshReporter: region = "Unlocated" region_coverage.setdefault(region, []).append(n.avg_gateways) - sorted_regions = sorted(region_coverage.items(), key=lambda x: sum(x[1])/len(x[1])) + # Sort regions but handle Unlocated separately + sorted_regions = sorted( + [(r, c) for r, c in region_coverage.items() if r not in ("Unlocated", "", "Unknown", None)], + key=lambda x: sum(x[1])/len(x[1]) + ) lines.append(" By region:") for region, counts in sorted_regions[:6]: avg = sum(counts) / len(counts) single = sum(1 for c in counts if c <= 1.0) flag = " !!" if avg < 2.0 else "" single_str = f" ({single} 1-gw)" if single > 0 else "" - lines.append(f" {region}: {len(counts)} nodes, {avg:.1f} avg{single_str}{flag}") + context = REGION_CONTEXT.get(region, "") + context_str = f" ({context})" if context else "" + lines.append(f" {region}{context_str}: {len(counts)} nodes, {avg:.1f} avg{single_str}{flag}") + # Show unlocated as informational (no coverage recommendations for these) + if "Unlocated" in region_coverage: + unl = region_coverage["Unlocated"] + lines.append(f" Unlocated: {len(unl)} nodes (no GPS — stale or transient)") else: deliver = self.data_store.get_mesh_deliverability() if deliver.get("avg_gateways") is not None: @@ -320,8 +344,10 @@ class MeshReporter: rs = region.score flag = _tier_flag(rs.tier) infra_str = f"{rs.infra_online}/{rs.infra_total} infra" + context = REGION_CONTEXT.get(region.name, "") + context_str = f" ({context})" if context else "" lines.append( - f" {region.name}: {rs.composite:.0f}/100 - {infra_str}, {rs.util_percent:.0f}% util{flag}" + f" {region.name}{context_str}: {rs.composite:.0f}/100 - {infra_str}, {rs.util_percent:.0f}% util{flag}" ) # Top issues @@ -530,8 +556,10 @@ class MeshReporter: return f"REGION DETAIL: {region_name}\nRegion not found." rs = region.score + context = REGION_CONTEXT.get(region.name, "") + context_str = f" — {context}" if context else "" lines = [ - f"REGION DETAIL: {region.name}", + f"REGION DETAIL: {region.name}{context_str}", f"Score: {rs.composite:.0f}/100 ({rs.tier})", "", f"Infrastructure ({rs.infra_online}/{rs.infra_total}):", @@ -732,7 +760,7 @@ class MeshReporter: f"ID: !{node.node_num:08x} (dec: {node.node_num})", f"Hardware: {node.hw_model or 'Unknown'}", f"Role: {node.role} ({'Infrastructure' if node.is_infrastructure else 'Client'})", - f"Region: {node.region or 'Unknown'} / Locality: {node.locality or 'Unknown'}", + f"Region: {node.region or 'Unknown'}{' — ' + REGION_CONTEXT.get(node.region, '') if node.region and REGION_CONTEXT.get(node.region) else ''} / Locality: {node.locality or 'Unknown'}", ] if node.latitude and node.longitude: diff --git a/meshai/router.py b/meshai/router.py index 19a9ec2..0f93207 100644 --- a/meshai/router.py +++ b/meshai/router.py @@ -125,19 +125,68 @@ _CITY_TO_REGION = { # Mesh awareness instruction for LLM _MESH_AWARENESS_PROMPT = """ -MESH DATA RESPONSE RULES (these OVERRIDE the brevity rules above for mesh questions): -- When answering about mesh health, nodes, coverage, or network status: give DETAILED responses -- Include actual numbers: scores, percentages, node names, packet counts, battery levels -- Use the data injected above — don't summarize it to one sentence -- Structure your response with the key data points the user asked about -- For node questions: include hardware, region, battery, channel utilization, coverage, neighbors, packets -- For region questions: include score, infrastructure status, coverage breakdown, flagged nodes, environment -- For mesh questions: include overall score by pillar, regional breakdown, top issues, coverage gaps -- For coverage questions: break down by region showing node counts, avg gateways, single-gateway nodes -- For "where do we need infrastructure": name specific regions with poor coverage, how many nodes are affected +MESH DATA RESPONSE RULES (OVERRIDE brevity rules for mesh/network questions): + +RESPONSE STYLE: +- Give DETAILED, data-driven responses with actual numbers +- Talk in GEOGRAPHIC terms — reference cities, valleys, corridors people know +- Name specific infrastructure nodes by their long name, not just shortnames +- Include scores, percentages, node counts, battery levels, gateway counts - You CAN use 3-5 messages if needed — LoRa chunking handles splitting -- Be specific and data-driven, not vague summaries -- Still no markdown formatting — plain text only +- No markdown formatting — plain text only +- Be specific and data-driven. Do NOT compress to vague one-liners + +REGION GEOGRAPHY — use these local names and landmarks: + +IDAHO: +- Northern ID: "North Idaho" or "the Panhandle" — Coeur d'Alene, Moscow, Lewiston, Sandpoint. Along I-90/US-95. +- Central ID: Idaho backcountry — Salmon, Stanley, McCall, Challis. Remote, mountainous, very sparse. Frank Church wilderness. +- South Western ID: "Treasure Valley" — Boise, Meridian, Nampa, Caldwell, Eagle, Mountain Home. Idaho's most populated region along I-84. Do NOT call this "southern Idaho." +- South Central ID: "Magic Valley" — Twin Falls, Burley, Jerome, Hailey, Gooding, Shoshone, Sun Valley. Snake River Canyon corridor along I-84/US-93. When locals say "southern Idaho" they usually mean this area. +- South Eastern ID: "Eastern Idaho" — Idaho Falls, Pocatello, Rexburg, Blackfoot. Upper Snake River Plain, gateway to Yellowstone. + +IMPORTANT IDAHO GEOGRAPHY: +- "Southern Idaho" is ambiguous. Do NOT lump Treasure Valley (Boise), Magic Valley (Twin Falls), and Eastern Idaho together. They are distinct areas 100+ miles apart. +- If someone says "southern Idaho" they most likely mean the Magic Valley (South Central ID — Twin Falls area). +- If unclear, describe each region separately rather than grouping. +- "Treasure Valley" always means the Boise metro area (South Western ID). +- "Magic Valley" always means the Twin Falls area (South Central ID). +- "Eastern Idaho" means Idaho Falls/Pocatello area (South Eastern ID). + +UTAH: +- Northern UT: Ogden, Logan, Brigham City, Cache Valley. Northern Wasatch Front. +- Central UT: "The Wasatch Front" or "Salt Lake" — Salt Lake City, Provo, Park City, Orem. About 2/3 of Utah's population lives here. +- Eastern UT: Vernal and the Uinta Basin in the north, Moab and canyon country in the south. Dinosaur NM, Arches, Canyonlands. +- Western UT: Tooele, Wendover. West desert, Bonneville Salt Flats. Very sparse. +- Southern UT: "Dixie" or "Color Country" — St. George, Cedar City, Hurricane. Zion, Bryce Canyon country. Fastest growing area in Utah. + +ANSWERING COVERAGE QUESTIONS: +- Reference the geographic area by local name: "Coverage across the Magic Valley from Burley through Twin Falls to Jerome..." +- Name infrastructure nodes by long name: "Mount Harrison Router, Indian Springs Router, Two Towers, and North Shoshone relay provide backbone coverage" +- Give avg gateways AND explain what it means: "nodes reach an average of 5.7 gateways — excellent redundancy" +- Identify single-gateway nodes as risks: "28 nodes in the Treasure Valley only reach 1 gateway" +- For "where do we need more infrastructure" — name the geographic area, explain how many nodes are underserved +- Do NOT recommend infrastructure for "Unlocated" nodes — those have no known position + +ANSWERING NODE QUESTIONS: +- Include: hardware model, role, region with local name ("in the Magic Valley near Twin Falls"), battery status, channel utilization, TX airtime, coverage (X/Y gateways), neighbor count, recent traffic with packet breakdown +- Name neighbors by long name +- Compare metrics to thresholds: "18.5% channel utilization is moderate — approaching the 20% caution threshold" +- If the node has environmental sensors, include readings + +ANSWERING MESH OVERVIEW QUESTIONS: +- Lead with composite score and tier +- Break down all 5 pillars with actual scores +- Highlight problem areas by geographic/local name +- Include top senders, coverage gaps, battery warnings by name +- Include traffic trends and sensor data if available + +ABOUT UNLOCATED NODES: +- About 280 nodes have no GPS position and no neighbor data +- These are stale, transient, or from sources that don't track position +- Do NOT recommend infrastructure for "Unknown" or "Unlocated" areas +- When asked about coverage gaps, focus ONLY on located regions +- If asked about unlocated nodes specifically, explain they're nodes without position data that can't be placed on the map """