mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-05-21 23:24:44 +02:00
feat: Named single-gw nodes, infra-only monitoring, commands overhaul
Coverage: - Single-gateway infra nodes named as critical risks per region - Client single-gw nodes counted but not individually named - Mesh-wide single-gw infra summary Monitoring rules by node type: - Infrastructure: full detail - battery, offline, coverage, neighbors, hardware - Clients causing problems: named - high util, top senders - Clients otherwise: counted per region, not individually tracked - POWER breakdown now infra-only Commands: - Removed hardcoded command list from config.py system_prompt - Dynamic command list in router.py from dispatcher (only enabled commands) - MeshMonitor commands no longer listed as MeshAI commands - !help overhaul: grouped by category, per-command detailed help - LLM explicitly told to only mention listed commands Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
56536f55c8
commit
aef78996a9
4 changed files with 177 additions and 21 deletions
|
|
@ -8,18 +8,132 @@ class HelpCommand(CommandHandler):
|
|||
|
||||
name = "help"
|
||||
description = "Show available commands"
|
||||
usage = "!help"
|
||||
usage = "!help [command]"
|
||||
|
||||
def __init__(self, dispatcher):
|
||||
self._dispatcher = dispatcher
|
||||
|
||||
async def execute(self, args: str, context: CommandContext) -> str:
|
||||
"""List all available commands."""
|
||||
if args and args.strip():
|
||||
return self._command_help(args.strip().lower())
|
||||
return self._list_all()
|
||||
|
||||
def _list_all(self) -> str:
|
||||
"""List all commands grouped by category."""
|
||||
commands = self._dispatcher.get_commands()
|
||||
|
||||
# Build compact help text
|
||||
lines = ["Commands:"]
|
||||
for cmd in sorted(commands, key=lambda c: c.name):
|
||||
lines.append(f"!{cmd.name} - {cmd.description}")
|
||||
# Deduplicate aliases
|
||||
seen = set()
|
||||
unique = []
|
||||
for cmd in commands:
|
||||
if cmd.name.lower() not in seen:
|
||||
seen.add(cmd.name.lower())
|
||||
unique.append(cmd)
|
||||
|
||||
return " | ".join(lines)
|
||||
# Group by category
|
||||
health_names = {"health", "region", "neighbors"}
|
||||
sub_names = {"sub", "unsub", "mysubs"}
|
||||
|
||||
health_cmds = [c for c in unique if c.name.lower() in health_names]
|
||||
sub_cmds = [c for c in unique if c.name.lower() in sub_names]
|
||||
other_cmds = [c for c in unique if c.name.lower() not in health_names and c.name.lower() not in sub_names and c.name.lower() != "help"]
|
||||
|
||||
lines = ["Commands:"]
|
||||
|
||||
if health_cmds:
|
||||
lines.append("")
|
||||
lines.append("Mesh Health:")
|
||||
for c in sorted(health_cmds, key=lambda x: x.name):
|
||||
lines.append(f" !{c.name} - {c.description}")
|
||||
|
||||
if sub_cmds:
|
||||
lines.append("")
|
||||
lines.append("Subscriptions:")
|
||||
for c in sorted(sub_cmds, key=lambda x: x.name):
|
||||
lines.append(f" !{c.name} - {c.description}")
|
||||
|
||||
if other_cmds:
|
||||
lines.append("")
|
||||
lines.append("Other:")
|
||||
for c in sorted(other_cmds, key=lambda x: x.name):
|
||||
lines.append(f" !{c.name} - {c.description}")
|
||||
|
||||
lines.append("")
|
||||
lines.append("!help [cmd] for details")
|
||||
lines.append("Or just ask me naturally!")
|
||||
|
||||
return "\n".join(lines)
|
||||
|
||||
def _command_help(self, cmd_name: str) -> str:
|
||||
"""Detailed help for a specific command."""
|
||||
aliases = {
|
||||
"sub": "sub", "subscribe": "sub", "subscription": "sub", "subscriptions": "sub",
|
||||
"unsub": "unsub", "unsubscribe": "unsub",
|
||||
"mysubs": "mysubs", "subs": "mysubs",
|
||||
"health": "health", "mesh": "health",
|
||||
"region": "region", "reg": "region",
|
||||
"neighbors": "neighbors", "nbr": "neighbors", "nb": "neighbors",
|
||||
"clear": "clear", "reset": "clear",
|
||||
}
|
||||
resolved = aliases.get(cmd_name, cmd_name)
|
||||
|
||||
# Check if this command is actually registered
|
||||
registered = {c.name.lower() for c in self._dispatcher.get_commands()}
|
||||
|
||||
texts = {
|
||||
"sub": (
|
||||
"Subscribe to Reports & Alerts\n\n"
|
||||
"Daily report:\n"
|
||||
" !sub daily 6pm\n"
|
||||
" !sub daily 7:30am region SCID\n"
|
||||
" !sub daily 6pm node MHR\n\n"
|
||||
"Weekly digest:\n"
|
||||
" !sub weekly 8am sun\n\n"
|
||||
"Alerts (instant DM on issues):\n"
|
||||
" !sub alerts\n"
|
||||
" !sub alerts region Wood River\n\n"
|
||||
"Time: 6pm, 6:30pm, 1830, 18:30\n"
|
||||
"Regions: SCID, SWID, Magic Valley, Twin Falls\n\n"
|
||||
"Manage:\n"
|
||||
" !mysubs - list yours\n"
|
||||
" !unsub daily - remove daily\n"
|
||||
" !unsub all - remove everything"
|
||||
),
|
||||
"unsub": (
|
||||
"Unsubscribe\n\n"
|
||||
" !unsub daily - remove daily report\n"
|
||||
" !unsub weekly - remove weekly digest\n"
|
||||
" !unsub alerts - remove alerts\n"
|
||||
" !unsub all - remove everything"
|
||||
),
|
||||
"mysubs": "!mysubs - list your active subscriptions",
|
||||
"health": (
|
||||
"Mesh Health\n\n"
|
||||
" !health - 5-pillar health summary\n"
|
||||
" !health now - force fresh data\n\n"
|
||||
"Or ask: 'how's the mesh?'"
|
||||
),
|
||||
"region": (
|
||||
"Region Info\n\n"
|
||||
" !region - list all regions\n"
|
||||
" !region SCID - South Central ID\n"
|
||||
" !region boise - South Western ID"
|
||||
),
|
||||
"neighbors": (
|
||||
"Neighbors\n\n"
|
||||
" !neighbors MHR - infra neighbors + signal\n"
|
||||
" !nb T2T - alias"
|
||||
),
|
||||
"clear": "!clear or !reset - clears conversation history",
|
||||
"ping": "!ping - connectivity test, responds with pong",
|
||||
"status": "!status - shows version, uptime, message count",
|
||||
"weather": "!weather [location] - weather lookup",
|
||||
}
|
||||
|
||||
help_text = texts.get(resolved)
|
||||
if help_text and resolved in registered:
|
||||
return help_text
|
||||
elif help_text and resolved not in registered:
|
||||
return f"The !{resolved} command is not currently enabled."
|
||||
else:
|
||||
return f"No help available for '{cmd_name}'. Try !help"
|
||||
|
|
|
|||
|
|
@ -96,23 +96,15 @@ class LLMConfig:
|
|||
timeout: int = 30
|
||||
|
||||
system_prompt: str = (
|
||||
"YOUR COMMANDS (handled directly by you via DM):\n"
|
||||
"!help — List available commands.\n"
|
||||
"!ping — Connectivity test, responds with pong.\n"
|
||||
"!status — Shows your version, uptime, user count, and message count.\n"
|
||||
"!weather [location] — Weather lookup using Open-Meteo API.\n"
|
||||
"!reset — Clears conversation history and memory.\n"
|
||||
"!clear — Same as !reset.\n\n"
|
||||
"YOUR ARCHITECTURE: Modular Python — pluggable LLM backends (OpenAI, Anthropic, "
|
||||
"Google, local), per-user SQLite conversation history, rolling summary memory, "
|
||||
"passive mesh context buffer (observes channel traffic), smart chunking for LoRa "
|
||||
"message limits, prompt injection defense, advBBS filtering.\n\n"
|
||||
"RESPONSE RULES:\n"
|
||||
"- For casual conversation, keep responses brief (1-2 sentences).\n"
|
||||
"- For mesh health questions, give detailed data-driven responses.\n"
|
||||
"- Be concise but friendly. No markdown formatting.\n"
|
||||
"- If asked about mesh activity and no recent traffic is shown, say you haven't "
|
||||
"observed any yet.\n"
|
||||
"- When asked about yourself or commands, answer conversationally based on "
|
||||
"the command list provided below. Don't dump lists unless asked.\n"
|
||||
"- You are part of the freq51 mesh.\n"
|
||||
"- When asked about yourself or commands, answer conversationally. Don't dump lists.\n"
|
||||
"- You are part of the freq51 mesh in the Twin Falls, Idaho area."
|
||||
)
|
||||
|
|
|
|||
|
|
@ -268,6 +268,19 @@ class MeshReporter:
|
|||
total_gw = len(self.data_store._sources)
|
||||
lines.append(f" Coverage: {len(region_gw)} nodes, avg {avg_gw:.1f}/{total_gw} gw, {single} single-gateway")
|
||||
|
||||
# Name single-gateway INFRASTRUCTURE nodes (critical risks)
|
||||
if single > 0:
|
||||
single_gw_nodes = [n for n in region_gw if n.avg_gateways is not None and n.avg_gateways <= 1.0]
|
||||
single_infra = [n for n in single_gw_nodes if n.is_infrastructure]
|
||||
single_clients = len(single_gw_nodes) - len(single_infra)
|
||||
|
||||
if single_infra:
|
||||
for sgn in single_infra:
|
||||
sgn_name = _node_display_name(sgn.long_name, sgn.short_name, str(sgn.node_num))
|
||||
lines.append(f" INFRA at risk: {sgn_name} - only 1 gateway")
|
||||
if single_clients > 0:
|
||||
lines.append(f" + {single_clients} client nodes at single gateway")
|
||||
|
||||
env_in_region = []
|
||||
for nid_str in region.node_ids:
|
||||
try:
|
||||
|
|
@ -306,7 +319,7 @@ class MeshReporter:
|
|||
lines.append(f" [OFFLINE] {name}: infra offline for {age}, {region}")
|
||||
|
||||
critical_bat = [n for n in health.nodes.values()
|
||||
if n.battery_percent is not None and 0 < n.battery_percent < 10]
|
||||
if n.is_infrastructure and n.battery_percent is not None and 0 < n.battery_percent < 10]
|
||||
if critical_bat:
|
||||
problems_found = True
|
||||
for node in sorted(critical_bat, key=lambda n: n.battery_percent):
|
||||
|
|
@ -316,7 +329,7 @@ class MeshReporter:
|
|||
lines.append(f" [CRITICAL] {name}: battery {node.battery_percent:.0f}%{trend}, {region}")
|
||||
|
||||
low_bat = [n for n in health.nodes.values()
|
||||
if n.battery_percent is not None and 10 <= n.battery_percent < 20]
|
||||
if n.is_infrastructure and n.battery_percent is not None and 10 <= n.battery_percent < 20]
|
||||
if low_bat:
|
||||
problems_found = True
|
||||
for node in sorted(low_bat, key=lambda n: n.battery_percent)[:5]:
|
||||
|
|
@ -375,6 +388,16 @@ class MeshReporter:
|
|||
lines.append(f" Full coverage ({total_sources}/{total_sources} gw): {full_gw} nodes")
|
||||
lines.append(f" Single gateway (1/{total_sources} gw): {single_gw} nodes - at risk if gateway drops")
|
||||
|
||||
# Name single-gateway infra nodes (these are critical risks)
|
||||
single_gw_infra = [n for n in nodes_with_gw
|
||||
if n.avg_gateways is not None and n.avg_gateways <= 1.0 and n.is_infrastructure]
|
||||
if single_gw_infra:
|
||||
lines.append(f" Single-gateway INFRASTRUCTURE (critical - if gateway drops, these go dark):")
|
||||
for n in single_gw_infra:
|
||||
name = _node_display_name(n.long_name, n.short_name, str(n.node_num))
|
||||
region = n.region or "Unlocated"
|
||||
lines.append(f" {name} [{region}]")
|
||||
|
||||
if health.has_traceroute_data:
|
||||
lines.append("")
|
||||
lines.append(f"NETWORK TOPOLOGY: {health.traceroute_count} traceroutes, avg {health.avg_hop_count:.1f} hops, max {health.max_hop_count}")
|
||||
|
|
@ -397,7 +420,7 @@ class MeshReporter:
|
|||
if pb["ok"]: parts.append(f"{pb['ok']} battery ok")
|
||||
if pb["low"]: parts.append(f"{pb['low']} battery low")
|
||||
if pb["critical"]: parts.append(f"{pb['critical']} battery critical")
|
||||
lines.append(f"POWER: {', '.join(parts)}")
|
||||
lines.append(f"POWER (infra): {', '.join(parts)}")
|
||||
|
||||
lines.append("")
|
||||
lines.append(f"TOTAL: {health.total_nodes} nodes across {health.total_regions} regions.")
|
||||
|
|
@ -420,6 +443,8 @@ class MeshReporter:
|
|||
critical = 0
|
||||
|
||||
for node in health.nodes.values():
|
||||
if not node.is_infrastructure:
|
||||
continue # Only track power for infrastructure
|
||||
if node.battery_percent is None:
|
||||
continue
|
||||
if node.battery_percent > 100:
|
||||
|
|
|
|||
|
|
@ -498,6 +498,31 @@ class MessageRouter:
|
|||
|
||||
system_prompt = identity + static_prompt
|
||||
|
||||
# 2b. Dynamic command list (only shows enabled commands)
|
||||
if self.dispatcher:
|
||||
commands = self.dispatcher.get_commands()
|
||||
if commands:
|
||||
# Deduplicate aliases
|
||||
seen_names = set()
|
||||
unique_commands = []
|
||||
for cmd in commands:
|
||||
name_lower = cmd.name.lower()
|
||||
if name_lower not in seen_names:
|
||||
seen_names.add(name_lower)
|
||||
unique_commands.append(cmd)
|
||||
|
||||
cmd_lines = [
|
||||
"\nYOUR COMMANDS (only mention these - do NOT mention any commands not listed here):"
|
||||
]
|
||||
for cmd in sorted(unique_commands, key=lambda c: c.name):
|
||||
cmd_lines.append(f" !{cmd.name} - {cmd.description}")
|
||||
cmd_lines.append("")
|
||||
cmd_lines.append(
|
||||
"CRITICAL: ONLY mention commands in the list above when asked about commands. "
|
||||
"If a command is not listed here, it does NOT exist. Do not invent commands."
|
||||
)
|
||||
system_prompt += "\n".join(cmd_lines)
|
||||
|
||||
# 3. MeshMonitor info (only when enabled)
|
||||
if (
|
||||
self.meshmonitor_sync
|
||||
|
|
@ -509,7 +534,7 @@ class MessageRouter:
|
|||
"meshtasticd node. MeshMonitor handles web dashboard, maps, telemetry, "
|
||||
"traceroutes, security scanning, and auto-responder commands. Its trigger "
|
||||
"commands are listed below ??? if someone asks what commands are available, "
|
||||
"mention both yours and MeshMonitor's. If someone asks where to get "
|
||||
"ONLY list YOUR commands from YOUR COMMANDS above. If someone asks where to get "
|
||||
"MeshMonitor, direct them to github.com/Yeraze/meshmonitor"
|
||||
)
|
||||
system_prompt += meshmonitor_intro
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue