mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-05-21 23:24:44 +02:00
feat: !health personality with emojis, skip empty regions, AIDA physical node identity
- Replace build_lora_compact with personality version using emojis - Add _region_lora_compact helper for region-specific display - Skip empty regions in 3 loops (build_tier1_summary, utilization, list_regions_compact) - Update AIDA identity: she IS a physical node (!27780c47 AIDA-N2) - AIDA now knows she has real GPS coordinates and radio connections Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
a97d346449
commit
1662d80f02
2 changed files with 118 additions and 22 deletions
|
|
@ -297,6 +297,8 @@ class MeshReporter:
|
||||||
lines.append("REGIONS:")
|
lines.append("REGIONS:")
|
||||||
|
|
||||||
for region in health.regions:
|
for region in health.regions:
|
||||||
|
if not region.node_ids:
|
||||||
|
continue
|
||||||
rs = region.score
|
rs = region.score
|
||||||
context = self._region_context(region.name)
|
context = self._region_context(region.name)
|
||||||
context_str = f" - {context}" if context else ""
|
context_str = f" - {context}" if context else ""
|
||||||
|
|
@ -708,6 +710,8 @@ class MeshReporter:
|
||||||
|
|
||||||
# Utilization issues
|
# Utilization issues
|
||||||
for region in health.regions:
|
for region in health.regions:
|
||||||
|
if not region.node_ids:
|
||||||
|
continue
|
||||||
if region.score.util_percent >= 25:
|
if region.score.util_percent >= 25:
|
||||||
issues.append(
|
issues.append(
|
||||||
f"{region.name}: channel utilization at {region.score.util_percent:.0f}% (High)"
|
f"{region.name}: channel utilization at {region.score.util_percent:.0f}% (High)"
|
||||||
|
|
@ -1491,33 +1495,120 @@ class MeshReporter:
|
||||||
return recs
|
return recs
|
||||||
|
|
||||||
def build_lora_compact(self, scope: str, scope_value: str = None) -> str:
|
def build_lora_compact(self, scope: str, scope_value: str = None) -> str:
|
||||||
"""Build LoRa-optimized compact summary for !health command."""
|
"""Build LoRa-optimized summary with personality for !health command."""
|
||||||
health = self.health_engine.mesh_health
|
health = self.health_engine.mesh_health
|
||||||
if not health:
|
if not health:
|
||||||
return "Mesh: No data"
|
return "📡 No mesh data yet."
|
||||||
|
|
||||||
if scope == "region" and scope_value:
|
if scope == "region" and scope_value:
|
||||||
region = self._find_region(scope_value)
|
region = self._find_region(scope_value)
|
||||||
if not region:
|
if not region:
|
||||||
return f"Region '{scope_value}' not found"
|
return f"Region '{scope_value}' not found."
|
||||||
|
return self._region_lora_compact(region)
|
||||||
|
|
||||||
|
if scope == "node" and scope_value:
|
||||||
|
return self.build_node_compact(scope_value)
|
||||||
|
|
||||||
|
s = health.score
|
||||||
|
|
||||||
|
if s.composite >= 90:
|
||||||
|
header = f"📡 freq51 looking great — {s.composite:.0f}/100"
|
||||||
|
elif s.composite >= 75:
|
||||||
|
header = f"📡 freq51 running solid — {s.composite:.0f}/100"
|
||||||
|
elif s.composite >= 60:
|
||||||
|
header = f"📡 freq51 needs attention — {s.composite:.0f}/100"
|
||||||
|
else:
|
||||||
|
header = f"🚨 freq51 struggling — {s.composite:.0f}/100"
|
||||||
|
|
||||||
|
lines = [header, ""]
|
||||||
|
|
||||||
|
offline_infra = [n for n in health.nodes.values() if n.is_infrastructure and not n.is_online]
|
||||||
|
if offline_infra:
|
||||||
|
offline_names = ", ".join(n.short_name or str(n.node_num) for n in offline_infra[:3])
|
||||||
|
more = f" +{len(offline_infra)-3}" if len(offline_infra) > 3 else ""
|
||||||
|
lines.append(f"🏗️ {s.infra_online}/{s.infra_total} routers up")
|
||||||
|
lines.append(f" ❌ Down: {offline_names}{more}")
|
||||||
|
else:
|
||||||
|
lines.append(f"🏗️ All {s.infra_total} routers up ✅")
|
||||||
|
|
||||||
|
nodes_with_gw = [n for n in health.nodes.values() if n.avg_gateways is not None]
|
||||||
|
if nodes_with_gw:
|
||||||
|
total_sources = len(self.data_store._sources)
|
||||||
|
full = sum(1 for n in nodes_with_gw if n.avg_gateways >= total_sources)
|
||||||
|
single = sum(1 for n in nodes_with_gw if n.avg_gateways <= 1.0)
|
||||||
|
if single > 0:
|
||||||
|
lines.append(f"📶 {full} nodes full coverage, {single} on thin ice with 1 gw")
|
||||||
|
else:
|
||||||
|
lines.append(f"📶 {full} nodes full coverage ✅")
|
||||||
|
|
||||||
|
high_util = [n for n in health.nodes.values()
|
||||||
|
if n.channel_utilization is not None and n.channel_utilization > 15]
|
||||||
|
if high_util:
|
||||||
|
worst = max(high_util, key=lambda n: n.channel_utilization)
|
||||||
|
lines.append(f"🔥 {worst.short_name} running hot at {worst.channel_utilization:.0f}% util")
|
||||||
|
|
||||||
|
infra_nodes = [n for n in health.nodes.values() if n.is_infrastructure]
|
||||||
|
bat_low = [n for n in infra_nodes if n.battery_percent is not None and 0 < n.battery_percent < 20]
|
||||||
|
if bat_low:
|
||||||
|
low_names = ", ".join(n.short_name for n in bat_low)
|
||||||
|
lines.append(f"🔋 ⚠️ {low_names} low battery")
|
||||||
|
else:
|
||||||
|
lines.append(f"🔋 All infra powered up ✅")
|
||||||
|
|
||||||
|
env_nodes = [n for n in health.nodes.values() if n.has_environment_sensor]
|
||||||
|
if env_nodes:
|
||||||
|
temps = [n.temperature for n in env_nodes if _is_valid_temperature(n.temperature)]
|
||||||
|
if temps:
|
||||||
|
lines.append(f"🌡️ {min(temps):.0f}–{max(temps):.0f}°C across {len(env_nodes)} sensors")
|
||||||
|
|
||||||
|
lines.append("")
|
||||||
|
region_parts = []
|
||||||
|
for region in health.regions:
|
||||||
|
if not region.node_ids:
|
||||||
|
continue
|
||||||
rs = region.score
|
rs = region.score
|
||||||
context = self._region_context(region.name)
|
context = self._region_context(region.name)
|
||||||
name = context.split("(")[0].strip() if context else region.name
|
name = context.split("(")[0].strip() if context else region.name
|
||||||
return f"{name} {rs.composite:.0f}/100 | {rs.infra_online}/{rs.infra_total} infra | {rs.util_percent:.0f}% util"
|
if rs.composite >= 90:
|
||||||
if scope == "node" and scope_value:
|
region_parts.append(f"{name} ✅")
|
||||||
return self.build_node_compact(scope_value)
|
elif rs.composite >= 70:
|
||||||
s = health.score
|
region_parts.append(f"{name} ⚠️")
|
||||||
lines = [f"Mesh {s.composite:.0f}/100 | {s.infra_online}/{s.infra_total} infra | {s.util_percent:.0f}% util"]
|
else:
|
||||||
for region in health.regions:
|
region_parts.append(f"{name} ❌")
|
||||||
if region.score.composite < 60:
|
if region_parts:
|
||||||
offline = region.score.infra_total - region.score.infra_online
|
lines.append(" | ".join(region_parts))
|
||||||
context = self._region_context(region.name)
|
|
||||||
name = context.split("(")[0].strip() if context else region.name
|
return "\n".join(lines)
|
||||||
lines.append(f"! {name} {region.score.composite:.0f}/100 - {offline} infra offline")
|
|
||||||
battery_warnings = self.health_engine.get_battery_warnings()
|
def _region_lora_compact(self, region) -> str:
|
||||||
for node in battery_warnings[:2]:
|
"""Compact region display for !region [name]."""
|
||||||
if node.is_infrastructure:
|
rs = region.score
|
||||||
lines.append(f"! {node.short_name or str(node.node_num)} bat {node.battery_percent:.0f}%")
|
context = self._region_context(region.name)
|
||||||
return "\n".join(lines[:5])
|
name = context.split("(")[0].strip() if context else region.name
|
||||||
|
|
||||||
|
if rs.composite >= 90:
|
||||||
|
header = f"📡 {name} looking good — {rs.composite:.0f}/100"
|
||||||
|
elif rs.composite >= 70:
|
||||||
|
header = f"📡 {name} needs attention — {rs.composite:.0f}/100"
|
||||||
|
else:
|
||||||
|
header = f"🚨 {name} in trouble — {rs.composite:.0f}/100"
|
||||||
|
|
||||||
|
lines = [header]
|
||||||
|
lines.append(f"🏗️ {rs.infra_online}/{rs.infra_total} infra | 📡 {rs.util_percent:.0f}% util")
|
||||||
|
|
||||||
|
offline = []
|
||||||
|
for nid_str in region.node_ids:
|
||||||
|
try:
|
||||||
|
nid = int(nid_str)
|
||||||
|
except (ValueError, TypeError):
|
||||||
|
continue
|
||||||
|
node = self.health_engine.mesh_health.nodes.get(nid)
|
||||||
|
if node and node.is_infrastructure and not node.is_online:
|
||||||
|
offline.append(node.short_name or str(nid))
|
||||||
|
if offline:
|
||||||
|
lines.append(f" ❌ Down: {', '.join(offline)}")
|
||||||
|
|
||||||
|
return "\n".join(lines)
|
||||||
|
|
||||||
def list_regions_compact(self) -> str:
|
def list_regions_compact(self) -> str:
|
||||||
"""List all regions with scores for !region command."""
|
"""List all regions with scores for !region command."""
|
||||||
|
|
@ -1526,6 +1617,8 @@ class MeshReporter:
|
||||||
return "No regions configured."
|
return "No regions configured."
|
||||||
lines = ["Regions:"]
|
lines = ["Regions:"]
|
||||||
for region in health.regions:
|
for region in health.regions:
|
||||||
|
if not region.node_ids:
|
||||||
|
continue
|
||||||
s = region.score
|
s = region.score
|
||||||
flag = _tier_flag(s.tier)
|
flag = _tier_flag(s.tier)
|
||||||
context = self._region_context(region.name)
|
context = self._region_context(region.name)
|
||||||
|
|
|
||||||
|
|
@ -488,11 +488,14 @@ class MessageRouter:
|
||||||
bot_owner = self.config.bot.owner or "Unknown"
|
bot_owner = self.config.bot.owner or "Unknown"
|
||||||
|
|
||||||
identity = (
|
identity = (
|
||||||
f"You are {bot_name}, an LLM-powered conversational assistant running on a "
|
f"You are {bot_name}, an LLM-powered assistant on the freq51 Meshtastic mesh network. "
|
||||||
f"Meshtastic mesh network. Your managing operator is {bot_owner}. "
|
f"Your managing operator is {bot_owner}. "
|
||||||
f"You are open source at github.com/zvx-echo6/meshai.\n\n"
|
f"You are open source at github.com/zvx-echo6/meshai.\n\n"
|
||||||
f"IDENTITY: Your name is {bot_name}. You respond to DMs only. You connect "
|
f"IDENTITY: Your name is {bot_name}. You ARE a physical node on the mesh — "
|
||||||
f"to a Meshtastic node via TCP through meshtasticd.\n\n"
|
f"node !27780c47 (AIDA-N2). You have a real location, real GPS coordinates, "
|
||||||
|
f"and real radio connections. When someone asks how far something is from you, "
|
||||||
|
f"check the mesh data for your node's position and calculate. "
|
||||||
|
f"You are NOT just software — you are a node that other nodes can see, hear, and route through.\n\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
# 2. Static system prompt from config
|
# 2. Static system prompt from config
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue