mirror of
https://github.com/zvx-echo6/meshai.git
synced 2026-05-21 23:24:44 +02:00
Simplify configurator to essential config options
Reduced TUI menu to 10 core options: 1. Bot Settings 2. Connection 3. LLM Backend 4. Response Settings 5. Channels 6. History & Memory 7. Rate Limits 8. Web Status Page 9. Announcements 10. Setup Wizard Added fq51BBS-style save/restart options: - 11. Save (stay in menu) - 12. Save & Restart Bot (apply changes now) - 13. Save & Exit (save, restart, exit) - 14. Exit without Saving Removed from UI (still in code for future use): - Safety & Filtering - User Management - Commands/Custom Commands - Personality/Personas - Logging - Webhooks Simplified default config and example config to match. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
1747edd150
commit
95b194967c
3 changed files with 70 additions and 697 deletions
|
|
@ -38,92 +38,6 @@ rate_limits:
|
||||||
cooldown_seconds: 5.0 # Min time between responses to same user
|
cooldown_seconds: 5.0 # Min time between responses to same user
|
||||||
burst_allowance: 3 # Allow short bursts before limiting
|
burst_allowance: 3 # Allow short bursts before limiting
|
||||||
|
|
||||||
# === LOGGING ===
|
|
||||||
logging:
|
|
||||||
level: INFO # DEBUG | INFO | WARNING | ERROR
|
|
||||||
file: "" # Log file path (empty = console only)
|
|
||||||
max_size_mb: 10 # Max log file size
|
|
||||||
backup_count: 3 # Number of backup log files
|
|
||||||
log_messages: true # Log incoming messages
|
|
||||||
log_responses: true # Log outgoing responses
|
|
||||||
log_api_calls: false # Log raw LLM API requests (verbose)
|
|
||||||
|
|
||||||
# === LLM BACKEND ===
|
|
||||||
llm:
|
|
||||||
backend: openai # openai | anthropic | google
|
|
||||||
api_key: "" # API key (or use LLM_API_KEY env var)
|
|
||||||
base_url: https://api.openai.com/v1 # API base URL
|
|
||||||
model: gpt-4o-mini # Model name
|
|
||||||
timeout: 30 # Request timeout (seconds)
|
|
||||||
system_prompt: >-
|
|
||||||
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.
|
|
||||||
|
|
||||||
# Fallback backend (optional) - used if primary fails
|
|
||||||
# fallback:
|
|
||||||
# backend: openai
|
|
||||||
# api_key: ""
|
|
||||||
# base_url: https://api.openai.com/v1
|
|
||||||
# model: gpt-4o-mini
|
|
||||||
# timeout: 30
|
|
||||||
|
|
||||||
retry_attempts: 2 # Retries before fallback
|
|
||||||
fallback_on_error: true # Use fallback on errors
|
|
||||||
fallback_on_timeout: true # Use fallback on timeouts
|
|
||||||
|
|
||||||
# === SAFETY & FILTERING ===
|
|
||||||
safety:
|
|
||||||
max_response_length: 250 # Hard cap on response length
|
|
||||||
filter_profanity: false # Basic profanity filter
|
|
||||||
blocked_phrases: [] # Phrases to filter out of responses
|
|
||||||
require_mention: true # Only respond when name is mentioned
|
|
||||||
ignore_self: true # Don't respond to own messages
|
|
||||||
emergency_keywords: # Always respond to these (bypass rate limits)
|
|
||||||
- emergency
|
|
||||||
- help
|
|
||||||
- sos
|
|
||||||
|
|
||||||
# === USER MANAGEMENT ===
|
|
||||||
users:
|
|
||||||
blocklist: [] # Never respond to these node IDs
|
|
||||||
# - "!abc12345"
|
|
||||||
allowlist_only: false # If true, only respond to allowlist
|
|
||||||
allowlist: [] # Exclusive users (if allowlist_only=true)
|
|
||||||
admin_nodes: [] # Nodes with admin command access
|
|
||||||
vip_nodes: [] # Nodes that bypass rate limits
|
|
||||||
|
|
||||||
# === COMMANDS ===
|
|
||||||
commands:
|
|
||||||
enabled: true
|
|
||||||
prefix: "!" # Command prefix (e.g., !weather, !help)
|
|
||||||
disabled_commands: [] # Built-in commands to disable
|
|
||||||
# - reset # Disable !reset command
|
|
||||||
custom_commands: {} # User-defined static response commands
|
|
||||||
# Example custom commands:
|
|
||||||
# custom_commands:
|
|
||||||
# ping:
|
|
||||||
# response: "Pong! MeshAI online."
|
|
||||||
# rules:
|
|
||||||
# response: "Be respectful. Keep it brief. No spam."
|
|
||||||
# freq:
|
|
||||||
# response: "Primary: 906.875 MHz | Alt: 903.125 MHz"
|
|
||||||
|
|
||||||
# === PERSONALITY ===
|
|
||||||
personality:
|
|
||||||
system_prompt: "" # Override llm.system_prompt if set
|
|
||||||
context_injection: "" # Template with {time}, {sender_name}, {channel}
|
|
||||||
# Example: "Current time: {time}. Speaking with {sender_name}."
|
|
||||||
personas: {} # Named personality variants
|
|
||||||
# Example personas:
|
|
||||||
# personas:
|
|
||||||
# serious:
|
|
||||||
# trigger: "!serious"
|
|
||||||
# prompt: "Respond formally and technically. No jokes."
|
|
||||||
# casual:
|
|
||||||
# trigger: "!casual"
|
|
||||||
# prompt: "Be casual and use humor when appropriate."
|
|
||||||
|
|
||||||
# === CONVERSATION HISTORY ===
|
# === CONVERSATION HISTORY ===
|
||||||
history:
|
history:
|
||||||
database: /data/conversations.db
|
database: /data/conversations.db
|
||||||
|
|
@ -139,6 +53,18 @@ memory:
|
||||||
window_size: 4 # Recent message pairs to keep in full
|
window_size: 4 # Recent message pairs to keep in full
|
||||||
summarize_threshold: 8 # Messages before re-summarizing
|
summarize_threshold: 8 # Messages before re-summarizing
|
||||||
|
|
||||||
|
# === LLM BACKEND ===
|
||||||
|
llm:
|
||||||
|
backend: openai # openai | anthropic | google
|
||||||
|
api_key: "" # API key (or use LLM_API_KEY env var)
|
||||||
|
base_url: https://api.openai.com/v1 # API base URL
|
||||||
|
model: gpt-4o-mini # Model name
|
||||||
|
timeout: 30 # Request timeout (seconds)
|
||||||
|
system_prompt: >-
|
||||||
|
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.
|
||||||
|
|
||||||
# === WEB STATUS PAGE ===
|
# === WEB STATUS PAGE ===
|
||||||
web_status:
|
web_status:
|
||||||
enabled: false # Enable web status page
|
enabled: false # Enable web status page
|
||||||
|
|
@ -161,31 +87,3 @@ announcements:
|
||||||
# - "MeshAI online. Mention 'ai' for help!"
|
# - "MeshAI online. Mention 'ai' for help!"
|
||||||
# - "Type !help for available commands."
|
# - "Type !help for available commands."
|
||||||
random_order: true # Randomize message order
|
random_order: true # Randomize message order
|
||||||
|
|
||||||
# === WEATHER ===
|
|
||||||
weather:
|
|
||||||
primary: openmeteo # openmeteo | wttr | llm
|
|
||||||
fallback: llm # Fallback provider
|
|
||||||
default_location: "" # Default location if none specified
|
|
||||||
openmeteo:
|
|
||||||
url: https://api.open-meteo.com/v1
|
|
||||||
wttr:
|
|
||||||
url: https://wttr.in
|
|
||||||
|
|
||||||
# === INTEGRATIONS ===
|
|
||||||
integrations:
|
|
||||||
weather: # Duplicate of top-level weather (for nesting)
|
|
||||||
primary: openmeteo
|
|
||||||
fallback: llm
|
|
||||||
default_location: ""
|
|
||||||
openmeteo:
|
|
||||||
url: https://api.open-meteo.com/v1
|
|
||||||
wttr:
|
|
||||||
url: https://wttr.in
|
|
||||||
webhook:
|
|
||||||
enabled: false # Enable webhook notifications
|
|
||||||
url: "" # Webhook URL
|
|
||||||
events: # Events to send
|
|
||||||
- message_received
|
|
||||||
- response_sent
|
|
||||||
- error
|
|
||||||
|
|
|
||||||
|
|
@ -41,15 +41,6 @@ rate_limits:
|
||||||
cooldown_seconds: 5.0
|
cooldown_seconds: 5.0
|
||||||
burst_allowance: 3
|
burst_allowance: 3
|
||||||
|
|
||||||
logging:
|
|
||||||
level: INFO
|
|
||||||
file: /data/meshai.log
|
|
||||||
max_size_mb: 10
|
|
||||||
backup_count: 3
|
|
||||||
log_messages: true
|
|
||||||
log_responses: true
|
|
||||||
log_api_calls: false
|
|
||||||
|
|
||||||
history:
|
history:
|
||||||
database: /data/conversations.db
|
database: /data/conversations.db
|
||||||
max_messages_per_user: 50
|
max_messages_per_user: 50
|
||||||
|
|
@ -73,38 +64,6 @@ llm:
|
||||||
You are a helpful assistant on a Meshtastic mesh network.
|
You are a helpful assistant on a Meshtastic mesh network.
|
||||||
Keep responses VERY brief - under 250 characters total.
|
Keep responses VERY brief - under 250 characters total.
|
||||||
Be concise but friendly. No markdown formatting.
|
Be concise but friendly. No markdown formatting.
|
||||||
retry_attempts: 2
|
|
||||||
fallback_on_error: true
|
|
||||||
fallback_on_timeout: true
|
|
||||||
|
|
||||||
safety:
|
|
||||||
max_response_length: 250
|
|
||||||
filter_profanity: false
|
|
||||||
blocked_phrases: []
|
|
||||||
require_mention: true
|
|
||||||
ignore_self: true
|
|
||||||
emergency_keywords:
|
|
||||||
- emergency
|
|
||||||
- help
|
|
||||||
- sos
|
|
||||||
|
|
||||||
users:
|
|
||||||
blocklist: []
|
|
||||||
allowlist_only: false
|
|
||||||
allowlist: []
|
|
||||||
admin_nodes: []
|
|
||||||
vip_nodes: []
|
|
||||||
|
|
||||||
commands:
|
|
||||||
enabled: true
|
|
||||||
prefix: "!"
|
|
||||||
disabled_commands: []
|
|
||||||
custom_commands: {}
|
|
||||||
|
|
||||||
personality:
|
|
||||||
system_prompt: ""
|
|
||||||
context_injection: ""
|
|
||||||
personas: {}
|
|
||||||
|
|
||||||
web_status:
|
web_status:
|
||||||
enabled: false
|
enabled: false
|
||||||
|
|
@ -122,28 +81,6 @@ announcements:
|
||||||
channel: 0
|
channel: 0
|
||||||
messages: []
|
messages: []
|
||||||
random_order: true
|
random_order: true
|
||||||
|
|
||||||
weather:
|
|
||||||
primary: openmeteo
|
|
||||||
fallback: llm
|
|
||||||
default_location: ""
|
|
||||||
openmeteo:
|
|
||||||
url: https://api.open-meteo.com/v1
|
|
||||||
wttr:
|
|
||||||
url: https://wttr.in
|
|
||||||
|
|
||||||
integrations:
|
|
||||||
weather:
|
|
||||||
primary: openmeteo
|
|
||||||
fallback: llm
|
|
||||||
default_location: ""
|
|
||||||
webhook:
|
|
||||||
enabled: false
|
|
||||||
url: ""
|
|
||||||
events:
|
|
||||||
- message_received
|
|
||||||
- response_sent
|
|
||||||
- error
|
|
||||||
EOF
|
EOF
|
||||||
echo "Default config created. Configure via http://localhost:7682"
|
echo "Default config created. Configure via http://localhost:7682"
|
||||||
fi
|
fi
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,5 @@
|
||||||
"""Rich-based TUI configurator for MeshAI."""
|
"""Rich-based TUI configurator for MeshAI."""
|
||||||
|
|
||||||
import os
|
|
||||||
import signal
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Optional
|
from typing import Optional
|
||||||
|
|
||||||
|
|
@ -65,8 +61,7 @@ class Configurator:
|
||||||
self._clear()
|
self._clear()
|
||||||
self._show_header()
|
self._show_header()
|
||||||
|
|
||||||
# Page 1 - Core Settings
|
table = Table(box=box.ROUNDED, show_header=False)
|
||||||
table = Table(box=box.ROUNDED, show_header=False, title="[bold]Core Settings[/bold]")
|
|
||||||
table.add_column("Option", style="cyan", width=4)
|
table.add_column("Option", style="cyan", width=4)
|
||||||
table.add_column("Description", style="white")
|
table.add_column("Description", style="white")
|
||||||
table.add_column("Status", style="dim")
|
table.add_column("Status", style="dim")
|
||||||
|
|
@ -75,51 +70,29 @@ class Configurator:
|
||||||
table.add_row("2", "Connection", f"{self.config.connection.type}")
|
table.add_row("2", "Connection", f"{self.config.connection.type}")
|
||||||
table.add_row("3", "LLM Backend", f"{self.config.llm.backend}/{self.config.llm.model}")
|
table.add_row("3", "LLM Backend", f"{self.config.llm.backend}/{self.config.llm.model}")
|
||||||
table.add_row("4", "Response Settings", f"{self.config.response.max_length}ch max")
|
table.add_row("4", "Response Settings", f"{self.config.response.max_length}ch max")
|
||||||
table.add_row("5", "Channel Filtering", f"{self.config.channels.mode}")
|
table.add_row("5", "Channels", f"{self.config.channels.mode}")
|
||||||
table.add_row("6", "History & Memory", f"{self.config.history.max_messages_per_user} msgs")
|
table.add_row("6", "History & Memory", f"{self.config.history.max_messages_per_user} msgs")
|
||||||
|
table.add_row("7", "Rate Limits", f"{self.config.rate_limits.messages_per_minute}/min")
|
||||||
|
table.add_row("8", "Web Status Page", self._status_icon(self.config.web_status.enabled))
|
||||||
|
table.add_row("9", "Announcements", self._status_icon(self.config.announcements.enabled))
|
||||||
|
table.add_row("10", "Setup Wizard", "[dim]First-time setup[/dim]")
|
||||||
|
|
||||||
console.print(table)
|
console.print(table)
|
||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
# Page 2 - Advanced Settings
|
# Exit options
|
||||||
table2 = Table(box=box.ROUNDED, show_header=False, title="[bold]Advanced Settings[/bold]")
|
if self.modified:
|
||||||
table2.add_column("Option", style="cyan", width=4)
|
console.print("[yellow]* Unsaved changes[/yellow]")
|
||||||
table2.add_column("Description", style="white")
|
console.print()
|
||||||
table2.add_column("Status", style="dim")
|
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]")
|
||||||
table2.add_row("7", "Rate Limits", f"{self.config.rate_limits.messages_per_minute}/min")
|
console.print("[white]13. Save & Exit[/white] [dim]Save, restart bot, exit[/dim]")
|
||||||
table2.add_row("8", "Safety & Filtering", self._status_icon(self.config.safety.filter_profanity))
|
console.print("[white]14. Exit without Saving[/white]")
|
||||||
table2.add_row("9", "User Management", f"{len(self.config.users.blocklist)} blocked")
|
|
||||||
table2.add_row("10", "Commands", f"prefix: {self.config.commands.prefix}")
|
|
||||||
table2.add_row("11", "Personality", f"{len(self.config.personality.personas)} personas")
|
|
||||||
table2.add_row("12", "Logging", f"{self.config.logging.level}")
|
|
||||||
|
|
||||||
console.print(table2)
|
|
||||||
console.print()
|
console.print()
|
||||||
|
|
||||||
# Page 3 - Features
|
choice = IntPrompt.ask("Select option", default=12)
|
||||||
table3 = Table(box=box.ROUNDED, show_header=False, title="[bold]Features[/bold]")
|
|
||||||
table3.add_column("Option", style="cyan", width=4)
|
|
||||||
table3.add_column("Description", style="white")
|
|
||||||
table3.add_column("Status", style="dim")
|
|
||||||
|
|
||||||
table3.add_row("13", "Weather", f"{self.config.weather.primary}")
|
if choice == 1:
|
||||||
table3.add_row("14", "Web Status Page", self._status_icon(self.config.web_status.enabled))
|
|
||||||
table3.add_row("15", "Announcements", self._status_icon(self.config.announcements.enabled))
|
|
||||||
table3.add_row("16", "Webhooks", self._status_icon(self.config.integrations.webhook.enabled))
|
|
||||||
table3.add_row("", "", "")
|
|
||||||
table3.add_row("20", "Setup Wizard", "[dim]First-time setup[/dim]")
|
|
||||||
table3.add_row("0", "Save & Exit", self._get_modified_indicator())
|
|
||||||
|
|
||||||
console.print(table3)
|
|
||||||
console.print()
|
|
||||||
|
|
||||||
choice = IntPrompt.ask("Select option", default=0)
|
|
||||||
|
|
||||||
if choice == 0:
|
|
||||||
self._handle_exit()
|
|
||||||
break
|
|
||||||
elif choice == 1:
|
|
||||||
self._bot_settings()
|
self._bot_settings()
|
||||||
elif choice == 2:
|
elif choice == 2:
|
||||||
self._connection_settings()
|
self._connection_settings()
|
||||||
|
|
@ -134,25 +107,20 @@ class Configurator:
|
||||||
elif choice == 7:
|
elif choice == 7:
|
||||||
self._rate_limits_settings()
|
self._rate_limits_settings()
|
||||||
elif choice == 8:
|
elif choice == 8:
|
||||||
self._safety_settings()
|
|
||||||
elif choice == 9:
|
|
||||||
self._users_settings()
|
|
||||||
elif choice == 10:
|
|
||||||
self._commands_settings()
|
|
||||||
elif choice == 11:
|
|
||||||
self._personality_settings()
|
|
||||||
elif choice == 12:
|
|
||||||
self._logging_settings()
|
|
||||||
elif choice == 13:
|
|
||||||
self._weather_settings()
|
|
||||||
elif choice == 14:
|
|
||||||
self._web_status_settings()
|
self._web_status_settings()
|
||||||
elif choice == 15:
|
elif choice == 9:
|
||||||
self._announcements_settings()
|
self._announcements_settings()
|
||||||
elif choice == 16:
|
elif choice == 10:
|
||||||
self._webhook_settings()
|
|
||||||
elif choice == 20:
|
|
||||||
self._setup_wizard()
|
self._setup_wizard()
|
||||||
|
elif choice == 11:
|
||||||
|
self._save_only()
|
||||||
|
elif choice == 12:
|
||||||
|
self._save_and_restart()
|
||||||
|
elif choice == 13:
|
||||||
|
self._save_restart_exit()
|
||||||
|
break
|
||||||
|
elif choice == 14:
|
||||||
|
break
|
||||||
|
|
||||||
def _show_header(self) -> None:
|
def _show_header(self) -> None:
|
||||||
"""Show compact header with modified indicator."""
|
"""Show compact header with modified indicator."""
|
||||||
|
|
@ -598,370 +566,6 @@ class Configurator:
|
||||||
self.config.rate_limits.burst_allowance = value
|
self.config.rate_limits.burst_allowance = value
|
||||||
self.modified = True
|
self.modified = True
|
||||||
|
|
||||||
def _safety_settings(self) -> None:
|
|
||||||
"""Safety and filtering settings submenu."""
|
|
||||||
while True:
|
|
||||||
self._clear()
|
|
||||||
console.print("[bold]Safety & Filtering[/bold]\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")
|
|
||||||
|
|
||||||
blocked_str = ", ".join(self.config.safety.blocked_phrases[:3])
|
|
||||||
if len(self.config.safety.blocked_phrases) > 3:
|
|
||||||
blocked_str += f"... (+{len(self.config.safety.blocked_phrases) - 3})"
|
|
||||||
emergency_str = ", ".join(self.config.safety.emergency_keywords[:3])
|
|
||||||
|
|
||||||
table.add_row("1", "Max Response Length", str(self.config.safety.max_response_length))
|
|
||||||
table.add_row("2", "Filter Profanity", self._status_icon(self.config.safety.filter_profanity))
|
|
||||||
table.add_row("3", "Blocked Phrases", blocked_str or "[dim]none[/dim]")
|
|
||||||
table.add_row("4", "Require @mention", self._status_icon(self.config.safety.require_mention))
|
|
||||||
table.add_row("5", "Ignore Self", self._status_icon(self.config.safety.ignore_self))
|
|
||||||
table.add_row("6", "Emergency Keywords", emergency_str)
|
|
||||||
table.add_row("0", "Back", "")
|
|
||||||
|
|
||||||
console.print(table)
|
|
||||||
console.print()
|
|
||||||
|
|
||||||
choice = IntPrompt.ask("Select option", default=0)
|
|
||||||
|
|
||||||
if choice == 0:
|
|
||||||
return
|
|
||||||
elif choice == 1:
|
|
||||||
value = IntPrompt.ask("Max response length", default=self.config.safety.max_response_length)
|
|
||||||
if value != self.config.safety.max_response_length:
|
|
||||||
self.config.safety.max_response_length = value
|
|
||||||
self.modified = True
|
|
||||||
elif choice == 2:
|
|
||||||
value = Confirm.ask("Filter profanity?", default=self.config.safety.filter_profanity)
|
|
||||||
if value != self.config.safety.filter_profanity:
|
|
||||||
self.config.safety.filter_profanity = value
|
|
||||||
self.modified = True
|
|
||||||
elif choice == 3:
|
|
||||||
console.print("\n[dim]Current:[/dim]", ", ".join(self.config.safety.blocked_phrases) or "none")
|
|
||||||
value = Prompt.ask("Blocked phrases (comma-separated)", default=",".join(self.config.safety.blocked_phrases))
|
|
||||||
phrases = [p.strip() for p in value.split(",") if p.strip()]
|
|
||||||
if phrases != self.config.safety.blocked_phrases:
|
|
||||||
self.config.safety.blocked_phrases = phrases
|
|
||||||
self.modified = True
|
|
||||||
elif choice == 4:
|
|
||||||
value = Confirm.ask("Require @mention?", default=self.config.safety.require_mention)
|
|
||||||
if value != self.config.safety.require_mention:
|
|
||||||
self.config.safety.require_mention = value
|
|
||||||
self.modified = True
|
|
||||||
elif choice == 5:
|
|
||||||
value = Confirm.ask("Ignore self messages?", default=self.config.safety.ignore_self)
|
|
||||||
if value != self.config.safety.ignore_self:
|
|
||||||
self.config.safety.ignore_self = value
|
|
||||||
self.modified = True
|
|
||||||
elif choice == 6:
|
|
||||||
value = Prompt.ask("Emergency keywords (comma-separated)", default=",".join(self.config.safety.emergency_keywords))
|
|
||||||
keywords = [k.strip() for k in value.split(",") if k.strip()]
|
|
||||||
if keywords != self.config.safety.emergency_keywords:
|
|
||||||
self.config.safety.emergency_keywords = keywords
|
|
||||||
self.modified = True
|
|
||||||
|
|
||||||
def _users_settings(self) -> None:
|
|
||||||
"""User management settings submenu."""
|
|
||||||
while True:
|
|
||||||
self._clear()
|
|
||||||
console.print("[bold]User Management[/bold]\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")
|
|
||||||
|
|
||||||
table.add_row("1", "Blocklist", f"{len(self.config.users.blocklist)} users")
|
|
||||||
table.add_row("2", "Allowlist Only Mode", self._status_icon(self.config.users.allowlist_only))
|
|
||||||
table.add_row("3", "Allowlist", f"{len(self.config.users.allowlist)} users")
|
|
||||||
table.add_row("4", "Admin Nodes", f"{len(self.config.users.admin_nodes)} users")
|
|
||||||
table.add_row("5", "VIP Nodes (bypass limits)", f"{len(self.config.users.vip_nodes)} users")
|
|
||||||
table.add_row("0", "Back", "")
|
|
||||||
|
|
||||||
console.print(table)
|
|
||||||
console.print()
|
|
||||||
|
|
||||||
choice = IntPrompt.ask("Select option", default=0)
|
|
||||||
|
|
||||||
if choice == 0:
|
|
||||||
return
|
|
||||||
elif choice == 1:
|
|
||||||
self._edit_node_list("Blocklist", self.config.users.blocklist)
|
|
||||||
elif choice == 2:
|
|
||||||
value = Confirm.ask("Allowlist only mode?", default=self.config.users.allowlist_only)
|
|
||||||
if value != self.config.users.allowlist_only:
|
|
||||||
self.config.users.allowlist_only = value
|
|
||||||
self.modified = True
|
|
||||||
elif choice == 3:
|
|
||||||
self._edit_node_list("Allowlist", self.config.users.allowlist)
|
|
||||||
elif choice == 4:
|
|
||||||
self._edit_node_list("Admin Nodes", self.config.users.admin_nodes)
|
|
||||||
elif choice == 5:
|
|
||||||
self._edit_node_list("VIP Nodes", self.config.users.vip_nodes)
|
|
||||||
|
|
||||||
def _edit_node_list(self, name: str, node_list: list) -> None:
|
|
||||||
"""Edit a list of node IDs."""
|
|
||||||
while True:
|
|
||||||
self._clear()
|
|
||||||
console.print(f"[bold]{name}[/bold]\n")
|
|
||||||
|
|
||||||
if node_list:
|
|
||||||
for i, node in enumerate(node_list, 1):
|
|
||||||
console.print(f" {i}. {node}")
|
|
||||||
else:
|
|
||||||
console.print(" [dim]No nodes[/dim]")
|
|
||||||
|
|
||||||
console.print("\n[cyan]a[/cyan] Add node")
|
|
||||||
console.print("[cyan]r[/cyan] Remove node")
|
|
||||||
console.print("[cyan]0[/cyan] Back")
|
|
||||||
console.print()
|
|
||||||
|
|
||||||
choice = Prompt.ask("Select", default="0")
|
|
||||||
|
|
||||||
if choice == "0":
|
|
||||||
return
|
|
||||||
elif choice.lower() == "a":
|
|
||||||
value = Prompt.ask("Node ID (e.g., !abc12345)")
|
|
||||||
if value and value not in node_list:
|
|
||||||
node_list.append(value)
|
|
||||||
self.modified = True
|
|
||||||
elif choice.lower() == "r":
|
|
||||||
if node_list:
|
|
||||||
idx = IntPrompt.ask("Remove which number", default=1)
|
|
||||||
if 1 <= idx <= len(node_list):
|
|
||||||
node_list.pop(idx - 1)
|
|
||||||
self.modified = True
|
|
||||||
|
|
||||||
def _commands_settings(self) -> None:
|
|
||||||
"""Commands settings submenu."""
|
|
||||||
while True:
|
|
||||||
self._clear()
|
|
||||||
console.print("[bold]Commands[/bold]\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")
|
|
||||||
|
|
||||||
table.add_row("1", "Commands Enabled", self._status_icon(self.config.commands.enabled))
|
|
||||||
table.add_row("2", "Prefix", self.config.commands.prefix)
|
|
||||||
table.add_row("3", "Disabled Commands", ", ".join(self.config.commands.disabled_commands) or "[dim]none[/dim]")
|
|
||||||
table.add_row("4", "Custom Commands", f"{len(self.config.commands.custom_commands)} defined")
|
|
||||||
table.add_row("0", "Back", "")
|
|
||||||
|
|
||||||
console.print(table)
|
|
||||||
console.print()
|
|
||||||
|
|
||||||
choice = IntPrompt.ask("Select option", default=0)
|
|
||||||
|
|
||||||
if choice == 0:
|
|
||||||
return
|
|
||||||
elif choice == 1:
|
|
||||||
value = Confirm.ask("Enable commands?", default=self.config.commands.enabled)
|
|
||||||
if value != self.config.commands.enabled:
|
|
||||||
self.config.commands.enabled = value
|
|
||||||
self.modified = True
|
|
||||||
elif choice == 2:
|
|
||||||
value = Prompt.ask("Command prefix", default=self.config.commands.prefix)
|
|
||||||
if value != self.config.commands.prefix:
|
|
||||||
self.config.commands.prefix = value
|
|
||||||
self.modified = True
|
|
||||||
elif choice == 3:
|
|
||||||
console.print("\n[dim]Built-in: help, ping, reset, status, weather[/dim]")
|
|
||||||
value = Prompt.ask("Disabled commands (comma-separated)", default=",".join(self.config.commands.disabled_commands))
|
|
||||||
commands = [c.strip() for c in value.split(",") if c.strip()]
|
|
||||||
if commands != self.config.commands.disabled_commands:
|
|
||||||
self.config.commands.disabled_commands = commands
|
|
||||||
self.modified = True
|
|
||||||
elif choice == 4:
|
|
||||||
self._custom_commands_editor()
|
|
||||||
|
|
||||||
def _custom_commands_editor(self) -> None:
|
|
||||||
"""Edit custom commands."""
|
|
||||||
while True:
|
|
||||||
self._clear()
|
|
||||||
console.print("[bold]Custom Commands[/bold]\n")
|
|
||||||
|
|
||||||
if self.config.commands.custom_commands:
|
|
||||||
for name, data in self.config.commands.custom_commands.items():
|
|
||||||
response = data.get("response", data) if isinstance(data, dict) else data
|
|
||||||
console.print(f" [cyan]{self.config.commands.prefix}{name}[/cyan] → {response[:50]}...")
|
|
||||||
else:
|
|
||||||
console.print(" [dim]No custom commands[/dim]")
|
|
||||||
|
|
||||||
console.print("\n[cyan]a[/cyan] Add command")
|
|
||||||
console.print("[cyan]r[/cyan] Remove command")
|
|
||||||
console.print("[cyan]0[/cyan] Back")
|
|
||||||
console.print()
|
|
||||||
|
|
||||||
choice = Prompt.ask("Select", default="0")
|
|
||||||
|
|
||||||
if choice == "0":
|
|
||||||
return
|
|
||||||
elif choice.lower() == "a":
|
|
||||||
name = Prompt.ask("Command name (without prefix)")
|
|
||||||
if name:
|
|
||||||
response = Prompt.ask("Response text")
|
|
||||||
if response:
|
|
||||||
self.config.commands.custom_commands[name] = {"response": response}
|
|
||||||
self.modified = True
|
|
||||||
elif choice.lower() == "r":
|
|
||||||
if self.config.commands.custom_commands:
|
|
||||||
name = Prompt.ask("Command name to remove")
|
|
||||||
if name in self.config.commands.custom_commands:
|
|
||||||
del self.config.commands.custom_commands[name]
|
|
||||||
self.modified = True
|
|
||||||
|
|
||||||
def _personality_settings(self) -> None:
|
|
||||||
"""Personality settings submenu."""
|
|
||||||
while True:
|
|
||||||
self._clear()
|
|
||||||
console.print("[bold]Personality[/bold]\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")
|
|
||||||
|
|
||||||
prompt_preview = self.config.personality.system_prompt[:40] + "..." if self.config.personality.system_prompt else "[dim]using LLM default[/dim]"
|
|
||||||
table.add_row("1", "System Prompt Override", prompt_preview)
|
|
||||||
table.add_row("2", "Context Injection", self.config.personality.context_injection[:30] + "..." if self.config.personality.context_injection else "[dim]none[/dim]")
|
|
||||||
table.add_row("3", "Personas", f"{len(self.config.personality.personas)} defined")
|
|
||||||
table.add_row("0", "Back", "")
|
|
||||||
|
|
||||||
console.print(table)
|
|
||||||
console.print()
|
|
||||||
|
|
||||||
choice = IntPrompt.ask("Select option", default=0)
|
|
||||||
|
|
||||||
if choice == 0:
|
|
||||||
return
|
|
||||||
elif choice == 1:
|
|
||||||
console.print("\n[dim]Current:[/dim]")
|
|
||||||
console.print(self.config.personality.system_prompt or "(none)")
|
|
||||||
if Confirm.ask("\nEdit system prompt?", default=False):
|
|
||||||
value = Prompt.ask("New system prompt (empty to clear)")
|
|
||||||
if value != self.config.personality.system_prompt:
|
|
||||||
self.config.personality.system_prompt = value
|
|
||||||
self.modified = True
|
|
||||||
elif choice == 2:
|
|
||||||
console.print("\n[dim]Variables: {time}, {sender_name}, {channel}[/dim]")
|
|
||||||
value = Prompt.ask("Context injection template", default=self.config.personality.context_injection)
|
|
||||||
if value != self.config.personality.context_injection:
|
|
||||||
self.config.personality.context_injection = value
|
|
||||||
self.modified = True
|
|
||||||
elif choice == 3:
|
|
||||||
self._personas_editor()
|
|
||||||
|
|
||||||
def _personas_editor(self) -> None:
|
|
||||||
"""Edit personas."""
|
|
||||||
while True:
|
|
||||||
self._clear()
|
|
||||||
console.print("[bold]Personas[/bold]\n")
|
|
||||||
|
|
||||||
if self.config.personality.personas:
|
|
||||||
for name, data in self.config.personality.personas.items():
|
|
||||||
trigger = data.get("trigger", f"!{name}") if isinstance(data, dict) else f"!{name}"
|
|
||||||
console.print(f" [cyan]{name}[/cyan] (trigger: {trigger})")
|
|
||||||
else:
|
|
||||||
console.print(" [dim]No personas defined[/dim]")
|
|
||||||
|
|
||||||
console.print("\n[cyan]a[/cyan] Add persona")
|
|
||||||
console.print("[cyan]r[/cyan] Remove persona")
|
|
||||||
console.print("[cyan]0[/cyan] Back")
|
|
||||||
console.print()
|
|
||||||
|
|
||||||
choice = Prompt.ask("Select", default="0")
|
|
||||||
|
|
||||||
if choice == "0":
|
|
||||||
return
|
|
||||||
elif choice.lower() == "a":
|
|
||||||
name = Prompt.ask("Persona name")
|
|
||||||
if name:
|
|
||||||
trigger = Prompt.ask("Trigger command", default=f"!{name}")
|
|
||||||
prompt = Prompt.ask("System prompt for this persona")
|
|
||||||
if prompt:
|
|
||||||
self.config.personality.personas[name] = {"trigger": trigger, "prompt": prompt}
|
|
||||||
self.modified = True
|
|
||||||
elif choice.lower() == "r":
|
|
||||||
if self.config.personality.personas:
|
|
||||||
name = Prompt.ask("Persona name to remove")
|
|
||||||
if name in self.config.personality.personas:
|
|
||||||
del self.config.personality.personas[name]
|
|
||||||
self.modified = True
|
|
||||||
|
|
||||||
def _logging_settings(self) -> None:
|
|
||||||
"""Logging settings submenu."""
|
|
||||||
while True:
|
|
||||||
self._clear()
|
|
||||||
console.print("[bold]Logging[/bold]\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")
|
|
||||||
|
|
||||||
table.add_row("1", "Log Level", self.config.logging.level)
|
|
||||||
table.add_row("2", "Log File", self.config.logging.file or "[dim]console only[/dim]")
|
|
||||||
table.add_row("3", "Max File Size (MB)", str(self.config.logging.max_size_mb))
|
|
||||||
table.add_row("4", "Backup Count", str(self.config.logging.backup_count))
|
|
||||||
table.add_row("5", "Log Messages", self._status_icon(self.config.logging.log_messages))
|
|
||||||
table.add_row("6", "Log Responses", self._status_icon(self.config.logging.log_responses))
|
|
||||||
table.add_row("7", "Log API Calls", self._status_icon(self.config.logging.log_api_calls))
|
|
||||||
table.add_row("0", "Back", "")
|
|
||||||
|
|
||||||
console.print(table)
|
|
||||||
console.print()
|
|
||||||
|
|
||||||
choice = IntPrompt.ask("Select option", default=0)
|
|
||||||
|
|
||||||
if choice == 0:
|
|
||||||
return
|
|
||||||
elif choice == 1:
|
|
||||||
console.print("\n[cyan]1.[/cyan] DEBUG")
|
|
||||||
console.print("[cyan]2.[/cyan] INFO")
|
|
||||||
console.print("[cyan]3.[/cyan] WARNING")
|
|
||||||
console.print("[cyan]4.[/cyan] ERROR")
|
|
||||||
sel = IntPrompt.ask("Select", default=2)
|
|
||||||
levels = {1: "DEBUG", 2: "INFO", 3: "WARNING", 4: "ERROR"}
|
|
||||||
value = levels.get(sel, "INFO")
|
|
||||||
if value != self.config.logging.level:
|
|
||||||
self.config.logging.level = value
|
|
||||||
self.modified = True
|
|
||||||
elif choice == 2:
|
|
||||||
value = Prompt.ask("Log file path (empty for console only)", default=self.config.logging.file)
|
|
||||||
if value != self.config.logging.file:
|
|
||||||
self.config.logging.file = value
|
|
||||||
self.modified = True
|
|
||||||
elif choice == 3:
|
|
||||||
value = IntPrompt.ask("Max file size (MB)", default=self.config.logging.max_size_mb)
|
|
||||||
if value != self.config.logging.max_size_mb:
|
|
||||||
self.config.logging.max_size_mb = value
|
|
||||||
self.modified = True
|
|
||||||
elif choice == 4:
|
|
||||||
value = IntPrompt.ask("Backup count", default=self.config.logging.backup_count)
|
|
||||||
if value != self.config.logging.backup_count:
|
|
||||||
self.config.logging.backup_count = value
|
|
||||||
self.modified = True
|
|
||||||
elif choice == 5:
|
|
||||||
value = Confirm.ask("Log messages?", default=self.config.logging.log_messages)
|
|
||||||
if value != self.config.logging.log_messages:
|
|
||||||
self.config.logging.log_messages = value
|
|
||||||
self.modified = True
|
|
||||||
elif choice == 6:
|
|
||||||
value = Confirm.ask("Log responses?", default=self.config.logging.log_responses)
|
|
||||||
if value != self.config.logging.log_responses:
|
|
||||||
self.config.logging.log_responses = value
|
|
||||||
self.modified = True
|
|
||||||
elif choice == 7:
|
|
||||||
value = Confirm.ask("Log API calls?", default=self.config.logging.log_api_calls)
|
|
||||||
if value != self.config.logging.log_api_calls:
|
|
||||||
self.config.logging.log_api_calls = value
|
|
||||||
self.modified = True
|
|
||||||
|
|
||||||
def _web_status_settings(self) -> None:
|
def _web_status_settings(self) -> None:
|
||||||
"""Web status page settings submenu."""
|
"""Web status page settings submenu."""
|
||||||
while True:
|
while True:
|
||||||
|
|
@ -1112,47 +716,6 @@ class Configurator:
|
||||||
self.config.announcements.messages.pop(idx - 1)
|
self.config.announcements.messages.pop(idx - 1)
|
||||||
self.modified = True
|
self.modified = True
|
||||||
|
|
||||||
def _webhook_settings(self) -> None:
|
|
||||||
"""Webhook settings submenu."""
|
|
||||||
while True:
|
|
||||||
self._clear()
|
|
||||||
console.print("[bold]Webhooks[/bold]\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")
|
|
||||||
|
|
||||||
table.add_row("1", "Enabled", self._status_icon(self.config.integrations.webhook.enabled))
|
|
||||||
table.add_row("2", "URL", self.config.integrations.webhook.url or "[dim]not set[/dim]")
|
|
||||||
table.add_row("3", "Events", ", ".join(self.config.integrations.webhook.events))
|
|
||||||
table.add_row("0", "Back", "")
|
|
||||||
|
|
||||||
console.print(table)
|
|
||||||
console.print()
|
|
||||||
|
|
||||||
choice = IntPrompt.ask("Select option", default=0)
|
|
||||||
|
|
||||||
if choice == 0:
|
|
||||||
return
|
|
||||||
elif choice == 1:
|
|
||||||
value = Confirm.ask("Enable webhooks?", default=self.config.integrations.webhook.enabled)
|
|
||||||
if value != self.config.integrations.webhook.enabled:
|
|
||||||
self.config.integrations.webhook.enabled = value
|
|
||||||
self.modified = True
|
|
||||||
elif choice == 2:
|
|
||||||
value = Prompt.ask("Webhook URL", default=self.config.integrations.webhook.url)
|
|
||||||
if value != self.config.integrations.webhook.url:
|
|
||||||
self.config.integrations.webhook.url = value
|
|
||||||
self.modified = True
|
|
||||||
elif choice == 3:
|
|
||||||
console.print("\n[dim]Available: message_received, response_sent, error, startup, shutdown[/dim]")
|
|
||||||
value = Prompt.ask("Events (comma-separated)", default=",".join(self.config.integrations.webhook.events))
|
|
||||||
events = [e.strip() for e in value.split(",") if e.strip()]
|
|
||||||
if events != self.config.integrations.webhook.events:
|
|
||||||
self.config.integrations.webhook.events = events
|
|
||||||
self.modified = True
|
|
||||||
|
|
||||||
def _setup_wizard(self) -> None:
|
def _setup_wizard(self) -> None:
|
||||||
"""First-time setup wizard."""
|
"""First-time setup wizard."""
|
||||||
self._clear()
|
self._clear()
|
||||||
|
|
@ -1215,75 +778,50 @@ class Configurator:
|
||||||
console.print("Press Enter to return to main menu...")
|
console.print("Press Enter to return to main menu...")
|
||||||
input()
|
input()
|
||||||
|
|
||||||
def _handle_exit(self) -> None:
|
def _save_only(self) -> None:
|
||||||
"""Handle exit with save prompt."""
|
"""Save config and stay in menu."""
|
||||||
if self.modified:
|
|
||||||
if Confirm.ask("\n[yellow]Save changes before exit?[/yellow]", default=True):
|
|
||||||
self._save_and_restart()
|
|
||||||
console.print("\nGoodbye!")
|
|
||||||
|
|
||||||
def _save_and_restart(self) -> None:
|
|
||||||
"""Save config and optionally restart the bot."""
|
|
||||||
save_config(self.config, self.config_path)
|
save_config(self.config, self.config_path)
|
||||||
console.print(f"[green]Configuration saved to {self.config_path}[/green]")
|
console.print(f"[green]Configuration saved to {self.config_path}[/green]")
|
||||||
self.modified = False
|
self.modified = False
|
||||||
|
input("Press Enter to continue...")
|
||||||
|
|
||||||
# Check if bot is running and offer restart
|
def _save_and_restart(self) -> None:
|
||||||
if self._is_bot_running():
|
"""Save config and signal bot to restart, stay in menu."""
|
||||||
if Confirm.ask("Restart bot with new config?", default=True):
|
self._clear()
|
||||||
self._restart_bot()
|
console.print("[cyan]Saving configuration...[/cyan]")
|
||||||
|
save_config(self.config, self.config_path)
|
||||||
|
console.print("[green]Configuration saved![/green]")
|
||||||
|
self.modified = False
|
||||||
|
console.print()
|
||||||
|
|
||||||
def _is_bot_running(self) -> bool:
|
# Write restart signal file (docker-entrypoint watches for this)
|
||||||
"""Check if meshai bot is running."""
|
restart_file = Path("/tmp/meshai_restart")
|
||||||
pid_file = Path("/tmp/meshai.pid")
|
|
||||||
if pid_file.exists():
|
|
||||||
try:
|
|
||||||
pid = int(pid_file.read_text().strip())
|
|
||||||
os.kill(pid, 0) # Check if process exists
|
|
||||||
return True
|
|
||||||
except (ValueError, OSError):
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Also check systemd
|
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
restart_file.touch()
|
||||||
["systemctl", "is-active", "meshai"],
|
console.print("[cyan]Bot restart signal sent.[/cyan]")
|
||||||
capture_output=True,
|
console.print()
|
||||||
text=True,
|
console.print("The bot will restart momentarily to apply changes.")
|
||||||
)
|
except Exception as e:
|
||||||
return result.stdout.strip() == "active"
|
console.print(f"[yellow]Could not signal restart: {e}[/yellow]")
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
return False
|
input("\nPress Enter to continue...")
|
||||||
|
|
||||||
def _restart_bot(self) -> None:
|
def _save_restart_exit(self) -> None:
|
||||||
"""Restart the bot."""
|
"""Save config, signal bot restart, and exit config tool."""
|
||||||
# Try systemd first
|
console.print("[cyan]Saving configuration...[/cyan]")
|
||||||
|
save_config(self.config, self.config_path)
|
||||||
|
console.print("[green]Configuration saved![/green]")
|
||||||
|
self.modified = False
|
||||||
|
|
||||||
|
# Write restart signal file
|
||||||
|
restart_file = Path("/tmp/meshai_restart")
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
restart_file.touch()
|
||||||
["systemctl", "restart", "meshai"],
|
console.print("[cyan]Bot restart signal sent.[/cyan]")
|
||||||
capture_output=True,
|
except Exception as e:
|
||||||
text=True,
|
console.print(f"[yellow]Could not signal restart: {e}[/yellow]")
|
||||||
)
|
|
||||||
if result.returncode == 0:
|
|
||||||
console.print("[green]Bot restarted via systemd[/green]")
|
|
||||||
return
|
|
||||||
except FileNotFoundError:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Try SIGHUP to running process
|
console.print("\nGoodbye!")
|
||||||
pid_file = Path("/tmp/meshai.pid")
|
|
||||||
if pid_file.exists():
|
|
||||||
try:
|
|
||||||
pid = int(pid_file.read_text().strip())
|
|
||||||
os.kill(pid, signal.SIGHUP)
|
|
||||||
console.print("[green]Sent reload signal to bot[/green]")
|
|
||||||
return
|
|
||||||
except (ValueError, OSError) as e:
|
|
||||||
console.print(f"[yellow]Could not signal bot: {e}[/yellow]")
|
|
||||||
|
|
||||||
console.print("[yellow]Could not restart bot automatically. Please restart manually.[/yellow]")
|
|
||||||
|
|
||||||
|
|
||||||
def run_configurator(config_path: Optional[Path] = None) -> None:
|
def run_configurator(config_path: Optional[Path] = None) -> None:
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue