feat: Dynamic identity system prompt from bot config

- Build system prompt dynamically using bot.name and bot.owner from config
- Reorder prompt: identity -> static prompt -> MeshMonitor (conditional) -> mesh context
- MeshMonitor description only injected when meshmonitor.enabled is true
- Update default system_prompt to static parts only (commands, architecture, rules)
- Fix meshmonitor.py to handle trigger arrays (not just strings)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
root 2026-05-03 05:45:58 +00:00
commit 5f66b69c9c
5 changed files with 296 additions and 298 deletions

View file

@ -79,7 +79,8 @@ class Configurator:
table.add_row("7", "Context", f"{ctx_status} {self.config.context.max_context_items} items") table.add_row("7", "Context", f"{ctx_status} {self.config.context.max_context_items} items")
table.add_row("8", "Weather", f"{self.config.weather.primary}") table.add_row("8", "Weather", f"{self.config.weather.primary}")
mm_status = self._status_icon(self.config.meshmonitor.enabled) mm_status = self._status_icon(self.config.meshmonitor.enabled)
table.add_row("9", "MeshMonitor Sync", f"{mm_status}") mm_url = self.config.meshmonitor.url or "[dim]not set[/dim]"
table.add_row("9", "MeshMonitor Sync", f"{mm_status} {mm_url}")
table.add_row("10", "Setup Wizard", "[dim]First-time setup[/dim]") table.add_row("10", "Setup Wizard", "[dim]First-time setup[/dim]")
console.print(table) console.print(table)
@ -89,13 +90,13 @@ class Configurator:
if self.modified: if self.modified:
console.print("[yellow]* Unsaved changes[/yellow]") console.print("[yellow]* Unsaved changes[/yellow]")
console.print() console.print()
console.print("[white]10. Save[/white] [dim]Save config, stay in menu[/dim]") console.print("[white]11. Save[/white] [dim]Save config, stay in menu[/dim]")
console.print("[green]11. Save & Restart Bot[/green] [dim]Apply changes now[/dim]") console.print("[green]12. Save & Restart Bot[/green] [dim]Apply changes now[/dim]")
console.print("[white]12. Save & Exit[/white] [dim]Save, restart bot, exit[/dim]") console.print("[white]13. Save & Exit[/white] [dim]Save, restart bot, exit[/dim]")
console.print("[white]13. Exit without Saving[/white]") console.print("[white]14. Exit without Saving[/white]")
console.print() console.print()
choice = IntPrompt.ask("Select option", default=11) choice = IntPrompt.ask("Select option", default=12)
if choice == 1: if choice == 1:
self._bot_settings() self._bot_settings()
@ -606,26 +607,23 @@ class Configurator:
self.config.memory.summarize_threshold = value self.config.memory.summarize_threshold = value
self.modified = True self.modified = True
def _meshmonitor_settings(self) -> None: def _meshmonitor_settings(self) -> None:
"""MeshMonitor sync settings submenu.""" """MeshMonitor sync settings submenu."""
while True: while True:
self._clear() self._clear()
console.print("[bold]MeshMonitor Sync Settings[/bold] console.print("[bold]MeshMonitor Sync Settings[/bold]\n")
") console.print("[dim]Sync auto-responder triggers from MeshMonitor to avoid duplicate responses.[/dim]\n")
console.print("[dim]Auto-ignore messages that match MeshMonitor trigger patterns.[/dim]
")
table = Table(box=box.ROUNDED) table = Table(box=box.ROUNDED)
table.add_column("Option", style="cyan", width=4) table.add_column("Option", style="cyan", width=4)
table.add_column("Setting", style="white") table.add_column("Setting", style="white")
table.add_column("Value", style="green") table.add_column("Value", style="green")
triggers_file = self.config.meshmonitor.triggers_file or "[dim]not set[/dim]"
table.add_row("1", "Enabled", self._status_icon(self.config.meshmonitor.enabled)) table.add_row("1", "Enabled", self._status_icon(self.config.meshmonitor.enabled))
table.add_row("2", "Triggers File", triggers_file) table.add_row("2", "MeshMonitor URL", self.config.meshmonitor.url or "[dim]not set[/dim]")
table.add_row("3", "Inject into Prompt", self._status_icon(self.config.meshmonitor.inject_into_prompt)) table.add_row("3", "Inject into Prompt", self._status_icon(self.config.meshmonitor.inject_into_prompt))
table.add_row("4", "View Triggers", "[dim]show loaded patterns[/dim]") table.add_row("4", "Refresh Interval", f"{self.config.meshmonitor.refresh_interval}s")
table.add_row("5", "View Triggers", "[dim]Fetch and display[/dim]")
table.add_row("0", "Back", "") table.add_row("0", "Back", "")
console.print(table) console.print(table)
@ -639,74 +637,52 @@ class Configurator:
self.config.meshmonitor.enabled = not self.config.meshmonitor.enabled self.config.meshmonitor.enabled = not self.config.meshmonitor.enabled
self.modified = True self.modified = True
elif choice == 2: elif choice == 2:
value = Prompt.ask("Triggers file path", default=self.config.meshmonitor.triggers_file or "/data/triggers.json") value = Prompt.ask("MeshMonitor URL (e.g., http://100.64.0.11:3333)",
if value != self.config.meshmonitor.triggers_file: default=self.config.meshmonitor.url)
self.config.meshmonitor.triggers_file = value if value != self.config.meshmonitor.url:
self.config.meshmonitor.url = value
self.modified = True self.modified = True
elif choice == 3: elif choice == 3:
self.config.meshmonitor.inject_into_prompt = not self.config.meshmonitor.inject_into_prompt self.config.meshmonitor.inject_into_prompt = not self.config.meshmonitor.inject_into_prompt
self.modified = True self.modified = True
elif choice == 4: elif choice == 4:
value = IntPrompt.ask("Refresh interval (seconds)", default=self.config.meshmonitor.refresh_interval)
if value != self.config.meshmonitor.refresh_interval:
self.config.meshmonitor.refresh_interval = value
self.modified = True
elif choice == 5:
self._view_meshmonitor_triggers() self._view_meshmonitor_triggers()
def _view_meshmonitor_triggers(self) -> None: def _view_meshmonitor_triggers(self) -> None:
"""Display loaded MeshMonitor trigger patterns.""" """Fetch and display MeshMonitor triggers."""
self._clear() self._clear()
console.print("[bold]MeshMonitor Triggers[/bold] console.print("[bold]MeshMonitor Triggers[/bold]\n")
")
triggers_file = self.config.meshmonitor.triggers_file if not self.config.meshmonitor.url:
if not triggers_file: console.print("[yellow]MeshMonitor URL not configured.[/yellow]")
console.print("[yellow]No triggers file configured.[/yellow]") input("\nPress Enter to continue...")
input("
Press Enter to continue...")
return return
from pathlib import Path console.print(f"[dim]Fetching from {self.config.meshmonitor.url}...[/dim]\n")
import json
triggers_path = Path(triggers_file)
if not triggers_path.exists():
console.print(f"[yellow]Triggers file not found: {triggers_file}[/yellow]")
input("
Press Enter to continue...")
return
try: try:
with open(triggers_path) as f: from ..meshmonitor import MeshMonitorSync
data = json.load(f) sync = MeshMonitorSync(self.config.meshmonitor.url)
except json.JSONDecodeError as e: count = sync.load()
console.print(f"[red]Invalid JSON in triggers file: {e}[/red]")
input("
Press Enter to continue...")
return
if not data: if count == 0:
console.print("[dim]Triggers file is empty.[/dim]") if sync.last_error:
input(" console.print(f"[red]Error: {sync.last_error}[/red]")
Press Enter to continue...") else:
return console.print("[yellow]No triggers configured in MeshMonitor.[/yellow]")
# Display triggers in a table
table = Table(box=box.ROUNDED, title="Loaded Triggers")
table.add_column("Command", style="cyan")
table.add_column("Pattern", style="white")
for name, pattern in data.items():
if isinstance(pattern, str):
table.add_row(name, pattern)
elif isinstance(pattern, dict) and "pattern" in pattern:
table.add_row(name, pattern["pattern"])
else: else:
table.add_row(name, "[dim]complex[/dim]") console.print(f"[green]Loaded {count} triggers:[/green]\n")
for trigger in sync.raw_triggers:
console.print(f" [cyan]{trigger}[/cyan]")
except Exception as e:
console.print(f"[red]Failed to fetch triggers: {e}[/red]")
console.print(table) input("\nPress Enter to continue...")
console.print(f"
[dim]Total: {len(data)} trigger(s)[/dim]")
input("
Press Enter to continue...")
def _setup_wizard(self) -> None: def _setup_wizard(self) -> None:
"""First-time setup wizard.""" """First-time setup wizard."""

View file

@ -75,16 +75,6 @@ class ContextConfig:
max_context_items: int = 20 # Max observations injected into LLM context max_context_items: int = 20 # Max observations injected into LLM context
@dataclass
class MeshMonitorConfig:
"""MeshMonitor trigger sync settings."""
enabled: bool = False
triggers_file: str = ""
inject_into_prompt: bool = True
@dataclass @dataclass
class CommandsConfig: class CommandsConfig:
"""Command settings.""" """Command settings."""
@ -106,12 +96,24 @@ class LLMConfig:
timeout: int = 30 timeout: int = 30
system_prompt: str = ( system_prompt: str = (
"You are a helpful assistant on a Meshtastic mesh network. " "YOUR COMMANDS (handled directly by you via DM):\n"
"Keep responses VERY brief - under 250 characters total. " "!help — List available commands.\n"
"Be concise but friendly. No markdown formatting. " "!ping — Connectivity test, responds with pong.\n"
"You can passively observe recent mesh traffic when available. " "!status — Shows your version, uptime, user count, and message count.\n"
"If asked about mesh activity and no recent traffic is shown below, " "!weather [location] — Weather lookup using Open-Meteo API.\n"
"say you haven't observed any traffic yet rather than claiming you lack access." "!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"
"- Keep responses VERY brief — under 200 characters total.\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. Don't dump lists.\n"
"- You are part of the freq51 mesh in the Twin Falls, Idaho area."
) )
use_system_prompt: bool = True # Toggle to disable sending system prompt use_system_prompt: bool = True # Toggle to disable sending system prompt
web_search: bool = False # Enable web search (Open WebUI feature) web_search: bool = False # Enable web search (Open WebUI feature)
@ -143,6 +145,16 @@ class WeatherConfig:
wttr: WttrConfig = field(default_factory=WttrConfig) wttr: WttrConfig = field(default_factory=WttrConfig)
@dataclass
class MeshMonitorConfig:
"""MeshMonitor trigger sync settings."""
enabled: bool = False
url: str = "" # e.g., http://100.64.0.11:3333
inject_into_prompt: bool = True # Tell LLM about MeshMonitor commands
refresh_interval: int = 300 # Seconds between refreshes
@dataclass @dataclass
class Config: class Config:
"""Main configuration container.""" """Main configuration container."""

View file

@ -73,6 +73,10 @@ class MeshAI:
while self._running: while self._running:
await asyncio.sleep(1) await asyncio.sleep(1)
# Periodic MeshMonitor refresh
if self.meshmonitor_sync:
self.meshmonitor_sync.maybe_refresh()
# Periodic cleanup # Periodic cleanup
if time.time() - self._last_cleanup >= 3600: if time.time() - self._last_cleanup >= 3600:
await self.history.cleanup_expired() await self.history.cleanup_expired()
@ -80,10 +84,6 @@ class MeshAI:
self.context.prune() self.context.prune()
self._last_cleanup = time.time() self._last_cleanup = time.time()
# Refresh MeshMonitor triggers if file changed
if self.meshmonitor_sync:
self.meshmonitor_sync.maybe_refresh()
async def stop(self) -> None: async def stop(self) -> None:
"""Stop the bot.""" """Stop the bot."""
logger.info("Stopping MeshAI...") logger.info("Stopping MeshAI...")
@ -163,14 +163,17 @@ class MeshAI:
self.context = None self.context = None
# MeshMonitor trigger sync # MeshMonitor trigger sync
self.meshmonitor_sync = None
mm_cfg = self.config.meshmonitor mm_cfg = self.config.meshmonitor
if mm_cfg.enabled and mm_cfg.triggers_file: if mm_cfg.enabled and mm_cfg.url:
from .meshmonitor import MeshMonitorSync from .meshmonitor import MeshMonitorSync
self.meshmonitor_sync = MeshMonitorSync( self.meshmonitor_sync = MeshMonitorSync(
triggers_file=mm_cfg.triggers_file, url=mm_cfg.url,
refresh_interval=mm_cfg.refresh_interval,
) )
self.meshmonitor_sync.load() count = self.meshmonitor_sync.load()
logger.info(f"MeshMonitor sync enabled, loaded {count} triggers")
else:
self.meshmonitor_sync = None
# Message router # Message router
self.router = MessageRouter( self.router = MessageRouter(

View file

@ -1,179 +1,171 @@
"""Dynamic MeshMonitor trigger sync for MeshAI. """MeshMonitor trigger sync via HTTP."""
Reads MeshMonitor auto-responder triggers from a JSON file and converts
them to compiled regex patterns. The router uses these patterns to ignore
messages that MeshMonitor will handle.
Trigger file format (triggers.json):
{"triggers": ["weather {location}", "trivia", "ask {question}"]}
Or simple list:
["weather {location}", "trivia", "ask {question}"]
"""
import json import json
import logging import logging
import re import re
from pathlib import Path import time
from typing import Optional from typing import Optional
from urllib.error import HTTPError, URLError
from urllib.request import Request, urlopen
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
def _trigger_to_regex(trigger: str) -> re.Pattern: def _trigger_to_regex(trigger: str) -> re.Pattern:
"""Convert a MeshMonitor trigger pattern to a compiled regex. """Convert MeshMonitor trigger pattern to compiled regex.
MeshMonitor trigger format: MeshMonitor patterns:
- "weather {location}" -> matches "weather miami" - !weather {location:.+} -> !weather (.+)
- "w {city},{state}" -> matches "w parkland,fl" - !ping -> !ping
- "trivia" -> matches "trivia" exactly - !status -> !status
- "ask {question}" -> matches "ask what is mesh"
- "{name:regex}" -> custom regex per param
Args: Args:
trigger: MeshMonitor trigger pattern string trigger: MeshMonitor trigger pattern
Returns: Returns:
Compiled regex pattern (case insensitive) Compiled regex pattern
""" """
pattern = trigger.strip() # Extract just the command part (before any {param} placeholders)
# Replace {name:pattern} with just the pattern for matching
# Temporarily replace {param} and {param:regex} blocks pattern = re.sub(r"\{[^:}]+:([^}]+)\}", r"(\1)", trigger)
param_blocks = [] # Replace {name} (no pattern) with (.+)
def _save_param(m): pattern = re.sub(r"\{[^}]+\}", r"(.+)", pattern)
param_blocks.append(m.group(0)) # Escape regex special chars except what we just inserted
return f"__PARAM_{len(param_blocks) - 1}__" # Actually, we need to be careful - just anchor it
pattern = "^" + pattern + "$"
pattern = re.sub(r"\{[^}]+\}", _save_param, pattern) return re.compile(pattern, re.IGNORECASE)
# Escape remaining special chars
pattern = re.escape(pattern)
# Restore param blocks as regex groups
for i, block in enumerate(param_blocks):
placeholder = re.escape(f"__PARAM_{i}__")
# Check for custom regex: {name:regex}
custom_match = re.match(r"\{(\w+):(.+)\}", block)
if custom_match:
_name, custom_regex = custom_match.groups()
replacement = f"({custom_regex})"
else:
# Default: match one or more characters
replacement = r"(.+)"
pattern = pattern.replace(placeholder, replacement)
return re.compile(f"^{pattern}$", re.IGNORECASE)
class MeshMonitorSync: class MeshMonitorSync:
"""Syncs MeshMonitor auto-responder triggers for MeshAI to ignore. """Sync auto-responder triggers from MeshMonitor HTTP API."""
Reads trigger patterns from a JSON file and compiles them to regex. def __init__(self, url: str, refresh_interval: int = 300):
Watches the file mtime so edits are picked up without restart. """Initialize MeshMonitor sync.
"""
def __init__(self, triggers_file: str): Args:
self._triggers_file = Path(triggers_file) url: Base URL of MeshMonitor (e.g., http://100.64.0.11:3333)
refresh_interval: Seconds between refresh checks (default 5 minutes)
"""
self._url = url.rstrip("/")
self._refresh_interval = refresh_interval
self._patterns: list[re.Pattern] = [] self._patterns: list[re.Pattern] = []
self._raw_triggers: list[str] = [] self._raw_triggers: list[str] = []
self._file_mtime: float = 0.0 self._last_refresh: float = 0.0
self._last_error: Optional[str] = None
@property @property
def trigger_list(self) -> list[str]: def raw_triggers(self) -> list[str]:
"""Get raw trigger strings (for system prompt injection).""" """Get raw trigger patterns (for display)."""
return self._raw_triggers return list(self._raw_triggers)
@property
def last_error(self) -> Optional[str]:
"""Get last error message if any."""
return self._last_error
def load(self) -> int: def load(self) -> int:
"""Load triggers from JSON file. """Fetch triggers from MeshMonitor API.
Returns: Returns:
Number of trigger patterns loaded Number of triggers loaded
""" """
if not self._triggers_file.exists(): endpoint = f"{self._url}/api/settings"
logger.warning(f"Triggers file not found: {self._triggers_file}")
return 0
try: try:
self._file_mtime = self._triggers_file.stat().st_mtime req = Request(endpoint, headers={"Accept": "application/json"})
with urlopen(req, timeout=10) as resp:
data = json.loads(resp.read().decode("utf-8"))
with open(self._triggers_file) as f: # autoResponderTriggers is a JSON string inside the response
data = json.load(f) triggers_json = data.get("autoResponderTriggers", "[]")
if isinstance(triggers_json, str):
if isinstance(data, list): triggers = json.loads(triggers_json)
raw_triggers = data
elif isinstance(data, dict):
raw_triggers = data.get("triggers", [])
else: else:
logger.warning(f"Unexpected triggers file format: {type(data)}") triggers = triggers_json
return 0
self._raw_triggers = raw_triggers # Extract trigger patterns
self._compile_patterns(raw_triggers) self._raw_triggers = []
self._patterns = []
for item in triggers:
if isinstance(item, dict):
trigger_value = item.get("trigger", "")
else:
trigger_value = item
logger.info( # Handle both string and list of strings
f"Loaded {len(self._patterns)} MeshMonitor trigger patterns " if isinstance(trigger_value, list):
f"from {self._triggers_file}" trigger_list = trigger_value
) elif isinstance(trigger_value, str) and trigger_value:
trigger_list = [trigger_value]
else:
continue
for trigger in trigger_list:
if not trigger:
continue
self._raw_triggers.append(trigger)
try:
self._patterns.append(_trigger_to_regex(trigger))
except re.error as e:
logger.warning(f"Invalid trigger pattern '{trigger}': {e}")
self._last_refresh = time.time()
self._last_error = None
logger.info(f"Loaded {len(self._patterns)} MeshMonitor triggers")
return len(self._patterns) return len(self._patterns)
except HTTPError as e:
self._last_error = f"HTTP {e.code}: {e.reason}"
logger.error(f"Failed to fetch MeshMonitor triggers: {self._last_error}")
return 0
except URLError as e:
self._last_error = f"Connection error: {e.reason}"
logger.error(f"Failed to fetch MeshMonitor triggers: {self._last_error}")
return 0
except json.JSONDecodeError as e:
self._last_error = f"Invalid JSON: {e}"
logger.error(f"Failed to parse MeshMonitor response: {self._last_error}")
return 0
except Exception as e: except Exception as e:
logger.error(f"Failed to load triggers file: {e}") self._last_error = str(e)
logger.error(f"Failed to fetch MeshMonitor triggers: {self._last_error}")
return 0 return 0
def maybe_refresh(self) -> bool: def maybe_refresh(self) -> bool:
"""Reload triggers if the file has changed on disk. """Refresh triggers if interval has passed.
Returns: Returns:
True if triggers were refreshed True if refresh was performed
""" """
if not self._triggers_file.exists(): if time.time() - self._last_refresh >= self._refresh_interval:
return False
mtime = self._triggers_file.stat().st_mtime
if mtime > self._file_mtime:
self.load() self.load()
return True return True
return False return False
def matches(self, text: str) -> bool: def matches(self, text: str) -> bool:
"""Check if text matches any MeshMonitor trigger pattern. """Check if text matches any MeshMonitor trigger.
Args: Args:
text: Incoming message text (stripped) text: Message text to check
Returns: Returns:
True if the message matches a MeshMonitor trigger True if MeshMonitor will handle this message
""" """
text = text.strip()
for pattern in self._patterns: for pattern in self._patterns:
if pattern.match(text): if pattern.match(text):
logger.debug(
f"Message matches MeshMonitor trigger: "
f"{text[:40]}... -> {pattern.pattern}"
)
return True return True
return False return False
def _compile_patterns(self, raw_triggers: list[str]) -> None: def get_commands_summary(self) -> str:
"""Compile trigger strings into regex patterns. """Get a summary of MeshMonitor commands for prompt injection.
Handles comma-separated multi-patterns per trigger. Returns:
Human-readable summary of available commands
""" """
patterns = [] if not self._raw_triggers:
return ""
for trigger in raw_triggers: lines = ["MeshMonitor handles these commands (do not respond to them):"]
# Split multi-pattern triggers on comma for trigger in self._raw_triggers:
sub_patterns = [t.strip() for t in trigger.split(",")] lines.append(f" - {trigger}")
return "\n".join(lines)
for sub in sub_patterns:
if not sub:
continue
try:
compiled = _trigger_to_regex(sub)
patterns.append(compiled)
except Exception as e:
logger.warning(
f"Failed to compile trigger pattern: {e}"
)
self._patterns = patterns

View file

@ -104,14 +104,10 @@ class MessageRouter:
logger.debug(f"Ignoring advBBS message from {message.sender_id}: {message.text[:40]}...") logger.debug(f"Ignoring advBBS message from {message.sender_id}: {message.text[:40]}...")
return False return False
# Ignore messages that match MeshMonitor auto-responder triggers # Ignore messages that MeshMonitor will handle
if self.meshmonitor_sync and message.text: if self.meshmonitor_sync and self.meshmonitor_sync.matches(message.text):
if self.meshmonitor_sync.matches(message.text.strip()): logger.debug(f"Ignoring MeshMonitor-handled message: {message.text[:40]}...")
logger.debug( return False
f"Ignoring DM from {message.sender_id}: "
f"matches MeshMonitor trigger"
)
return False
return True return True
@ -157,12 +153,48 @@ class MessageRouter:
# Get conversation history # Get conversation history
history = await self.history.get_history_for_llm(message.sender_id) history = await self.history.get_history_for_llm(message.sender_id)
# Get system prompt from config # Build system prompt in order: identity -> static -> meshmonitor -> context
system_prompt = ""
if getattr(self.config.llm, 'use_system_prompt', True):
system_prompt = self.config.llm.system_prompt
# Inject mesh context if available # 1. Dynamic identity from bot config
bot_name = self.config.bot.name or "MeshAI"
bot_owner = self.config.bot.owner or "Unknown"
identity = (
f"You are {bot_name}, an LLM-powered conversational assistant running on a "
f"Meshtastic mesh network. Your managing operator is {bot_owner}. "
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"to a Meshtastic node via TCP through meshtasticd.\n\n"
)
# 2. Static system prompt from config
static_prompt = ""
if getattr(self.config.llm, 'use_system_prompt', True):
static_prompt = self.config.llm.system_prompt
system_prompt = identity + static_prompt
# 3. MeshMonitor info (only when enabled)
if (
self.meshmonitor_sync
and self.config.meshmonitor.enabled
and self.config.meshmonitor.inject_into_prompt
):
meshmonitor_intro = (
"\n\nMESHMONITOR: You run alongside MeshMonitor (by Yeraze) on the same "
"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 "
"MeshMonitor, direct them to github.com/Yeraze/meshmonitor"
)
system_prompt += meshmonitor_intro
commands_summary = self.meshmonitor_sync.get_commands_summary()
if commands_summary:
system_prompt += "\n\n" + commands_summary
# 4. Inject mesh context if available
if self.context: if self.context:
max_items = getattr(self.config.context, 'max_context_items', 20) max_items = getattr(self.config.context, 'max_context_items', 20)
context_block = self.context.get_context_block(max_items=max_items) context_block = self.context.get_context_block(max_items=max_items)
@ -176,23 +208,6 @@ class MessageRouter:
"\n\n[No recent mesh traffic observed yet.]" "\n\n[No recent mesh traffic observed yet.]"
) )
# Inject MeshMonitor commands into prompt
if (
self.meshmonitor_sync
and getattr(self.config.meshmonitor, "inject_into_prompt", False)
and self.meshmonitor_sync.trigger_list
):
trigger_lines = ", ".join(
t for t in self.meshmonitor_sync.trigger_list
if t not in ("commands", "command")
)
system_prompt += (
"\n\nMESHMONITOR COMMANDS (handled by MeshMonitor, not you): "
+ trigger_lines
+ "\nIf someone asks what commands are available, mention these too."
)
try: try:
response = await self.llm.generate( response = await self.llm.generate(
messages=history, messages=history,