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