87 lines
2.9 KiB
Python
87 lines
2.9 KiB
Python
"""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()
|