"""Main run loop: connect to the MeshCore device, route DMs through the LLM, reply.""" from __future__ import annotations import asyncio import logging from collections import defaultdict from meshcore import EventType, MeshCore from . import db from .config import Settings from .llm import LLMClient from .messages import trim_to_bytes log = logging.getLogger("meshbot") async def run() -> None: cfg = Settings() db_conn = db.connect(cfg.storage.sqlite_path) llm = LLMClient( base_url=cfg.llm.base_url, api_key=cfg.llm.api_key, model=cfg.llm.model, system_prompt=cfg.llm.system_prompt, temperature=cfg.llm.temperature, timeout=cfg.llm.request_timeout_seconds, ) log.info("connecting to MeshCore on %s @ %d baud", cfg.meshcore.serial_port, cfg.meshcore.baud_rate) mc = await MeshCore.create_serial(cfg.meshcore.serial_port, cfg.meshcore.baud_rate) await mc.ensure_contacts() # One lock per sender so a burst of messages from the same peer is processed # serially while different peers stay independent. locks: dict[str, asyncio.Lock] = defaultdict(asyncio.Lock) async def on_dm(event) -> None: data = event.payload or {} prefix = data.get("pubkey_prefix") text = (data.get("text") or "").strip() if not prefix or not text: return contact = mc.get_contact_by_key_prefix(prefix) if contact is None: log.info("ignoring DM from unknown sender %s", prefix) return public_key = contact["public_key"] contact_name = contact.get("adv_name", "") log.info("DM from %s (%s): %s", contact_name, public_key[:12], text) async with locks[public_key]: db.upsert_conversation(db_conn, public_key, contact_name) db.add_message(db_conn, public_key, "user", text) history = db.get_history(db_conn, public_key) try: reply = await llm.reply(history) except Exception: log.exception("LLM call failed for %s", public_key[:12]) return db.add_message(db_conn, public_key, "assistant", reply) outgoing = trim_to_bytes(reply, cfg.message.max_bytes) log.info("reply to %s (%d bytes): %s", public_key[:12], len(outgoing.encode("utf-8")), outgoing) result = await mc.commands.send_msg(contact, outgoing) if result.type == EventType.ERROR: log.error("send_msg failed for %s: %s", public_key[:12], result.payload) sub = mc.subscribe(EventType.CONTACT_MSG_RECV, on_dm) await mc.start_auto_message_fetching() log.info("meshbot listening on %s", cfg.meshcore.serial_port) try: await asyncio.Event().wait() finally: mc.unsubscribe(sub) await mc.stop_auto_message_fetching() await mc.disconnect() await llm.aclose() db_conn.close()