mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-05-21 15:14:45 +02:00
- 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>
171 lines
5.9 KiB
Python
171 lines
5.9 KiB
Python
"""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)
|