Initial commit
This commit is contained in:
@@ -0,0 +1,86 @@
|
||||
"""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()
|
||||
Reference in New Issue
Block a user