Fix bugs: MeshMessage position field, summary loading, Google system prompt, import placement, cleanup timer

- 1a: Declare _position as proper dataclass field with field(default=None, init=False)
  so hasattr() check isn't needed and the attribute always exists
- 1b: Load persisted conversation summaries from DB into memory cache on startup
  via new _load_summaries() method called after backend creation
- 1c: Use Gemini's system_instruction parameter on GenerativeModel instead of
  only prepending to first message, so system prompt persists across all turns
- 1d: Move 'import os' from line 198 to top of main.py with other imports
- 1e: Replace unreliable modulo-based cleanup timer with _last_cleanup timestamp
  comparison that won't miss hours due to async sleep jitter

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Ubuntu 2026-02-23 20:11:46 +00:00
commit c1f2c48494
3 changed files with 53 additions and 13 deletions

View file

@ -162,6 +162,12 @@ class GoogleBackend(LLMBackend):
) )
try: try:
# Create model with system instruction for persistent system prompt
model = genai.GenerativeModel(
self.config.model,
system_instruction=enhanced_system if enhanced_system else None,
)
# Convert messages to Gemini format # Convert messages to Gemini format
# Gemini uses "user" and "model" roles # Gemini uses "user" and "model" roles
history = [] history = []
@ -170,15 +176,11 @@ class GoogleBackend(LLMBackend):
history.append({"role": role, "parts": [msg["content"]]}) history.append({"role": role, "parts": [msg["content"]]})
# Start chat with history # Start chat with history
chat = self._model.start_chat(history=history) chat = model.start_chat(history=history)
# Get the last user message # Get the last user message
last_message = final_messages[-1]["content"] if final_messages else "" last_message = final_messages[-1]["content"] if final_messages else ""
# Prepend system prompt to first message if needed
if enhanced_system and not history:
last_message = f"{enhanced_system}\n\n{last_message}"
# Generate response # Generate response
response = await chat.send_message_async( response = await chat.send_message_async(
last_message, last_message,

View file

@ -2,7 +2,7 @@
import asyncio import asyncio
import logging import logging
from dataclasses import dataclass from dataclasses import dataclass, field
from typing import Callable, Optional from typing import Callable, Optional
import meshtastic import meshtastic
@ -26,13 +26,12 @@ class MeshMessage:
channel: int # Channel index channel: int # Channel index
is_dm: bool # True if direct message to us is_dm: bool # True if direct message to us
packet: dict # Raw packet for additional data packet: dict # Raw packet for additional data
_position: Optional[tuple[float, float]] = field(default=None, repr=False, init=False)
@property @property
def sender_position(self) -> Optional[tuple[float, float]]: def sender_position(self) -> Optional[tuple[float, float]]:
"""Get sender's GPS position if available (lat, lon).""" """Get sender's GPS position if available (lat, lon)."""
# Position comes from node info, not the message itself return self._position
# This will be populated by the connector if available
return self._position if hasattr(self, "_position") else None
class MeshConnector: class MeshConnector:

View file

@ -3,6 +3,7 @@
import argparse import argparse
import asyncio import asyncio
import logging import logging
import os
import signal import signal
import sys import sys
import time import time
@ -18,6 +19,7 @@ from .commands.status import set_start_time
from .config import Config, load_config from .config import Config, load_config
from .connector import MeshConnector, MeshMessage from .connector import MeshConnector, MeshMessage
from .history import ConversationHistory from .history import ConversationHistory
from .memory import ConversationSummary
from .responder import Responder from .responder import Responder
from .router import MessageRouter, RouteType from .router import MessageRouter, RouteType
@ -37,6 +39,7 @@ class MeshAI:
self.responder: Optional[Responder] = None self.responder: Optional[Responder] = None
self._running = False self._running = False
self._loop: Optional[asyncio.AbstractEventLoop] = None self._loop: Optional[asyncio.AbstractEventLoop] = None
self._last_cleanup: float = 0.0
async def start(self) -> None: async def start(self) -> None:
"""Start the bot.""" """Start the bot."""
@ -52,6 +55,7 @@ class MeshAI:
self._running = True self._running = True
self._loop = asyncio.get_event_loop() self._loop = asyncio.get_event_loop()
self._last_cleanup = time.time()
# Write PID file # Write PID file
self._write_pid() self._write_pid()
@ -63,8 +67,9 @@ class MeshAI:
await asyncio.sleep(1) await asyncio.sleep(1)
# Periodic cleanup # Periodic cleanup
if int(time.time()) % 3600 == 0: # Every hour if time.time() - self._last_cleanup >= 3600:
await self.history.cleanup_expired() await self.history.cleanup_expired()
self._last_cleanup = time.time()
async def stop(self) -> None: async def stop(self) -> None:
"""Stop the bot.""" """Stop the bot."""
@ -121,6 +126,9 @@ class MeshAI:
self.config.llm, api_key, window_size, summarize_threshold self.config.llm, api_key, window_size, summarize_threshold
) )
# Load persisted summaries into memory cache
await self._load_summaries()
# Meshtastic connector # Meshtastic connector
self.connector = MeshConnector(self.config.connection) self.connector = MeshConnector(self.config.connection)
@ -183,6 +191,40 @@ class MeshAI:
except Exception as e: except Exception as e:
logger.error(f"Error handling message: {e}", exc_info=True) logger.error(f"Error handling message: {e}", exc_info=True)
async def _load_summaries(self) -> None:
"""Load persisted summaries from database into memory cache."""
memory = self.llm.get_memory()
if not memory:
return
if not self.history or not self.history._db:
return
try:
async with self.history._lock:
cursor = await self.history._db.execute(
"SELECT user_id, summary, message_count, updated_at "
"FROM conversation_summaries"
)
rows = await cursor.fetchall()
loaded = 0
for row in rows:
user_id, summary_text, message_count, updated_at = row
summary = ConversationSummary(
summary=summary_text,
last_updated=updated_at,
message_count=message_count,
)
memory.load_summary(user_id, summary)
loaded += 1
if loaded:
logger.info(f"Loaded {loaded} conversation summaries from database")
except Exception as e:
logger.warning(f"Failed to load summaries from database: {e}")
def _write_pid(self) -> None: def _write_pid(self) -> None:
"""Write PID file.""" """Write PID file."""
pid_file = Path("/tmp/meshai.pid") pid_file = Path("/tmp/meshai.pid")
@ -195,9 +237,6 @@ class MeshAI:
pid_file.unlink() pid_file.unlink()
import os
def setup_logging(verbose: bool = False) -> None: def setup_logging(verbose: bool = False) -> None:
"""Configure logging.""" """Configure logging."""
level = logging.DEBUG if verbose else logging.INFO level = logging.DEBUG if verbose else logging.INFO