From 5f66b69c9c7ed06daa319d9eefa2e9d46f955cc8 Mon Sep 17 00:00:00 2001 From: root Date: Sun, 3 May 2026 05:45:58 +0000 Subject: [PATCH] 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 --- meshai/cli/configurator.py | 106 +++++------ meshai/config.py | 44 +++-- meshai/main.py | 19 +- meshai/meshmonitor.py | 350 ++++++++++++++++++------------------- meshai/router.py | 75 ++++---- 5 files changed, 296 insertions(+), 298 deletions(-) diff --git a/meshai/cli/configurator.py b/meshai/cli/configurator.py index 16e2108..bc73377 100644 --- a/meshai/cli/configurator.py +++ b/meshai/cli/configurator.py @@ -79,7 +79,8 @@ class Configurator: 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}") 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]") console.print(table) @@ -89,13 +90,13 @@ class Configurator: if self.modified: console.print("[yellow]* Unsaved changes[/yellow]") console.print() - console.print("[white]10. Save[/white] [dim]Save config, stay in menu[/dim]") - console.print("[green]11. 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. Exit without Saving[/white]") + console.print("[white]11. Save[/white] [dim]Save config, stay in menu[/dim]") + console.print("[green]12. Save & Restart Bot[/green] [dim]Apply changes now[/dim]") + console.print("[white]13. Save & Exit[/white] [dim]Save, restart bot, exit[/dim]") + console.print("[white]14. Exit without Saving[/white]") console.print() - choice = IntPrompt.ask("Select option", default=11) + choice = IntPrompt.ask("Select option", default=12) if choice == 1: self._bot_settings() @@ -606,26 +607,23 @@ class Configurator: self.config.memory.summarize_threshold = value self.modified = True - def _meshmonitor_settings(self) -> None: """MeshMonitor sync settings submenu.""" while True: self._clear() - console.print("[bold]MeshMonitor Sync Settings[/bold] -") - console.print("[dim]Auto-ignore messages that match MeshMonitor trigger patterns.[/dim] -") + console.print("[bold]MeshMonitor Sync Settings[/bold]\n") + console.print("[dim]Sync auto-responder triggers from MeshMonitor to avoid duplicate responses.[/dim]\n") table = Table(box=box.ROUNDED) table.add_column("Option", style="cyan", width=4) table.add_column("Setting", style="white") 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("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("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", "") console.print(table) @@ -639,74 +637,52 @@ class Configurator: self.config.meshmonitor.enabled = not self.config.meshmonitor.enabled self.modified = True elif choice == 2: - value = Prompt.ask("Triggers file path", default=self.config.meshmonitor.triggers_file or "/data/triggers.json") - if value != self.config.meshmonitor.triggers_file: - self.config.meshmonitor.triggers_file = value + value = Prompt.ask("MeshMonitor URL (e.g., http://100.64.0.11:3333)", + default=self.config.meshmonitor.url) + if value != self.config.meshmonitor.url: + self.config.meshmonitor.url = value self.modified = True elif choice == 3: self.config.meshmonitor.inject_into_prompt = not self.config.meshmonitor.inject_into_prompt self.modified = True 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() - - def _view_meshmonitor_triggers(self) -> None: - """Display loaded MeshMonitor trigger patterns.""" + """Fetch and display MeshMonitor triggers.""" self._clear() - console.print("[bold]MeshMonitor Triggers[/bold] -") + console.print("[bold]MeshMonitor Triggers[/bold]\n") - triggers_file = self.config.meshmonitor.triggers_file - if not triggers_file: - console.print("[yellow]No triggers file configured.[/yellow]") - input(" -Press Enter to continue...") + if not self.config.meshmonitor.url: + console.print("[yellow]MeshMonitor URL not configured.[/yellow]") + input("\nPress Enter to continue...") return - from pathlib import Path - 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 + console.print(f"[dim]Fetching from {self.config.meshmonitor.url}...[/dim]\n") try: - with open(triggers_path) as f: - data = json.load(f) - except json.JSONDecodeError as e: - console.print(f"[red]Invalid JSON in triggers file: {e}[/red]") - input(" -Press Enter to continue...") - return + from ..meshmonitor import MeshMonitorSync + sync = MeshMonitorSync(self.config.meshmonitor.url) + count = sync.load() - if not data: - console.print("[dim]Triggers file is empty.[/dim]") - input(" -Press Enter to continue...") - return - - # 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"]) + if count == 0: + if sync.last_error: + console.print(f"[red]Error: {sync.last_error}[/red]") + else: + console.print("[yellow]No triggers configured in MeshMonitor.[/yellow]") 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) - console.print(f" -[dim]Total: {len(data)} trigger(s)[/dim]") - input(" -Press Enter to continue...") + input("\nPress Enter to continue...") def _setup_wizard(self) -> None: """First-time setup wizard.""" diff --git a/meshai/config.py b/meshai/config.py index 3b1466b..c40d6f1 100644 --- a/meshai/config.py +++ b/meshai/config.py @@ -75,16 +75,6 @@ class ContextConfig: 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 class CommandsConfig: """Command settings.""" @@ -106,12 +96,24 @@ class LLMConfig: timeout: int = 30 system_prompt: str = ( - "You are a helpful assistant on a Meshtastic mesh network. " - "Keep responses VERY brief - under 250 characters total. " - "Be concise but friendly. No markdown formatting. " - "You can passively observe recent mesh traffic when available. " - "If asked about mesh activity and no recent traffic is shown below, " - "say you haven't observed any traffic yet rather than claiming you lack access." + "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" + "- 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 web_search: bool = False # Enable web search (Open WebUI feature) @@ -143,6 +145,16 @@ class WeatherConfig: 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 class Config: """Main configuration container.""" diff --git a/meshai/main.py b/meshai/main.py index 367329b..b582e85 100644 --- a/meshai/main.py +++ b/meshai/main.py @@ -73,6 +73,10 @@ class MeshAI: while self._running: await asyncio.sleep(1) + # Periodic MeshMonitor refresh + if self.meshmonitor_sync: + self.meshmonitor_sync.maybe_refresh() + # Periodic cleanup if time.time() - self._last_cleanup >= 3600: await self.history.cleanup_expired() @@ -80,10 +84,6 @@ class MeshAI: self.context.prune() 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: """Stop the bot.""" logger.info("Stopping MeshAI...") @@ -163,14 +163,17 @@ class MeshAI: self.context = None # MeshMonitor trigger sync - self.meshmonitor_sync = None 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 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 self.router = MessageRouter( diff --git a/meshai/meshmonitor.py b/meshai/meshmonitor.py index f205f27..57beae6 100644 --- a/meshai/meshmonitor.py +++ b/meshai/meshmonitor.py @@ -1,179 +1,171 @@ -"""Dynamic MeshMonitor trigger sync for MeshAI. - -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 logging -import re -from pathlib import Path -from typing import Optional - -logger = logging.getLogger(__name__) - - -def _trigger_to_regex(trigger: str) -> re.Pattern: - """Convert a MeshMonitor trigger pattern to a compiled regex. - - MeshMonitor trigger format: - - "weather {location}" -> matches "weather miami" - - "w {city},{state}" -> matches "w parkland,fl" - - "trivia" -> matches "trivia" exactly - - "ask {question}" -> matches "ask what is mesh" - - "{name:regex}" -> custom regex per param - - Args: - trigger: MeshMonitor trigger pattern string - - Returns: - Compiled regex pattern (case insensitive) - """ - pattern = trigger.strip() - - # Temporarily replace {param} and {param:regex} blocks - param_blocks = [] - def _save_param(m): - param_blocks.append(m.group(0)) - return f"__PARAM_{len(param_blocks) - 1}__" - - pattern = re.sub(r"\{[^}]+\}", _save_param, pattern) - - # 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: - """Syncs MeshMonitor auto-responder triggers for MeshAI to ignore. - - Reads trigger patterns from a JSON file and compiles them to regex. - Watches the file mtime so edits are picked up without restart. - """ - - def __init__(self, triggers_file: str): - self._triggers_file = Path(triggers_file) - self._patterns: list[re.Pattern] = [] - self._raw_triggers: list[str] = [] - self._file_mtime: float = 0.0 - - @property - def trigger_list(self) -> list[str]: - """Get raw trigger strings (for system prompt injection).""" - return self._raw_triggers - - def load(self) -> int: - """Load triggers from JSON file. - - Returns: - Number of trigger patterns loaded - """ - if not self._triggers_file.exists(): - logger.warning(f"Triggers file not found: {self._triggers_file}") - return 0 - - try: - self._file_mtime = self._triggers_file.stat().st_mtime - - with open(self._triggers_file) as f: - data = json.load(f) - - if isinstance(data, list): - raw_triggers = data - elif isinstance(data, dict): - raw_triggers = data.get("triggers", []) - else: - logger.warning(f"Unexpected triggers file format: {type(data)}") - return 0 - - self._raw_triggers = raw_triggers - self._compile_patterns(raw_triggers) - - logger.info( - f"Loaded {len(self._patterns)} MeshMonitor trigger patterns " - f"from {self._triggers_file}" - ) - return len(self._patterns) - - except Exception as e: - logger.error(f"Failed to load triggers file: {e}") - return 0 - - def maybe_refresh(self) -> bool: - """Reload triggers if the file has changed on disk. - - Returns: - True if triggers were refreshed - """ - if not self._triggers_file.exists(): - return False - - mtime = self._triggers_file.stat().st_mtime - if mtime > self._file_mtime: - self.load() - return True - - return False - - def matches(self, text: str) -> bool: - """Check if text matches any MeshMonitor trigger pattern. - - Args: - text: Incoming message text (stripped) - - Returns: - True if the message matches a MeshMonitor trigger - """ - for pattern in self._patterns: - if pattern.match(text): - logger.debug( - f"Message matches MeshMonitor trigger: " - f"{text[:40]}... -> {pattern.pattern}" - ) - return True - return False - - def _compile_patterns(self, raw_triggers: list[str]) -> None: - """Compile trigger strings into regex patterns. - - Handles comma-separated multi-patterns per trigger. - """ - patterns = [] - - for trigger in raw_triggers: - # Split multi-pattern triggers on comma - sub_patterns = [t.strip() for t in trigger.split(",")] - - 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 +"""MeshMonitor trigger sync via HTTP.""" + +import json +import logging +import re +import time +from typing import Optional +from urllib.error import HTTPError, URLError +from urllib.request import Request, urlopen + +logger = logging.getLogger(__name__) + + +def _trigger_to_regex(trigger: str) -> re.Pattern: + """Convert MeshMonitor trigger pattern to compiled regex. + + MeshMonitor patterns: + - !weather {location:.+} -> !weather (.+) + - !ping -> !ping + - !status -> !status + + Args: + trigger: MeshMonitor trigger pattern + + Returns: + Compiled regex pattern + """ + # Extract just the command part (before any {param} placeholders) + # Replace {name:pattern} with just the pattern for matching + pattern = re.sub(r"\{[^:}]+:([^}]+)\}", r"(\1)", trigger) + # Replace {name} (no pattern) with (.+) + pattern = re.sub(r"\{[^}]+\}", r"(.+)", pattern) + # Escape regex special chars except what we just inserted + # Actually, we need to be careful - just anchor it + pattern = "^" + pattern + "$" + return re.compile(pattern, re.IGNORECASE) + + +class MeshMonitorSync: + """Sync auto-responder triggers from MeshMonitor HTTP API.""" + + def __init__(self, url: str, refresh_interval: int = 300): + """Initialize MeshMonitor sync. + + Args: + 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._raw_triggers: list[str] = [] + self._last_refresh: float = 0.0 + self._last_error: Optional[str] = None + + @property + def raw_triggers(self) -> list[str]: + """Get raw trigger patterns (for display).""" + 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: + """Fetch triggers from MeshMonitor API. + + Returns: + Number of triggers loaded + """ + endpoint = f"{self._url}/api/settings" + try: + req = Request(endpoint, headers={"Accept": "application/json"}) + with urlopen(req, timeout=10) as resp: + data = json.loads(resp.read().decode("utf-8")) + + # autoResponderTriggers is a JSON string inside the response + triggers_json = data.get("autoResponderTriggers", "[]") + if isinstance(triggers_json, str): + triggers = json.loads(triggers_json) + else: + triggers = triggers_json + + # Extract trigger patterns + self._raw_triggers = [] + self._patterns = [] + for item in triggers: + if isinstance(item, dict): + trigger_value = item.get("trigger", "") + else: + trigger_value = item + + # Handle both string and list of strings + if isinstance(trigger_value, list): + 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) + + 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: + self._last_error = str(e) + logger.error(f"Failed to fetch MeshMonitor triggers: {self._last_error}") + return 0 + + def maybe_refresh(self) -> bool: + """Refresh triggers if interval has passed. + + Returns: + True if refresh was performed + """ + if time.time() - self._last_refresh >= self._refresh_interval: + self.load() + return True + return False + + def matches(self, text: str) -> bool: + """Check if text matches any MeshMonitor trigger. + + Args: + text: Message text to check + + Returns: + True if MeshMonitor will handle this message + """ + text = text.strip() + for pattern in self._patterns: + if pattern.match(text): + return True + return False + + def get_commands_summary(self) -> str: + """Get a summary of MeshMonitor commands for prompt injection. + + Returns: + Human-readable summary of available commands + """ + if not self._raw_triggers: + return "" + + lines = ["MeshMonitor handles these commands (do not respond to them):"] + for trigger in self._raw_triggers: + lines.append(f" - {trigger}") + return "\n".join(lines) diff --git a/meshai/router.py b/meshai/router.py index 09f47b3..a100f13 100644 --- a/meshai/router.py +++ b/meshai/router.py @@ -104,14 +104,10 @@ class MessageRouter: logger.debug(f"Ignoring advBBS message from {message.sender_id}: {message.text[:40]}...") return False - # Ignore messages that match MeshMonitor auto-responder triggers - if self.meshmonitor_sync and message.text: - if self.meshmonitor_sync.matches(message.text.strip()): - logger.debug( - f"Ignoring DM from {message.sender_id}: " - f"matches MeshMonitor trigger" - ) - return False + # Ignore messages that MeshMonitor will handle + if self.meshmonitor_sync and self.meshmonitor_sync.matches(message.text): + logger.debug(f"Ignoring MeshMonitor-handled message: {message.text[:40]}...") + return False return True @@ -157,12 +153,48 @@ class MessageRouter: # Get conversation history history = await self.history.get_history_for_llm(message.sender_id) - # Get system prompt from config - system_prompt = "" - if getattr(self.config.llm, 'use_system_prompt', True): - system_prompt = self.config.llm.system_prompt + # Build system prompt in order: identity -> static -> meshmonitor -> context - # 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: max_items = getattr(self.config.context, 'max_context_items', 20) 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.]" ) - # 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: response = await self.llm.generate( messages=history,