Files

100 lines
3.7 KiB
Go

// Command echobot is the first bot of the unibots platform: a bot WITHOUT an
// LLM that demonstrates the two conversation patterns of the unibus bus.
//
// - Chat mode (bot<->human): the bot joins a cleartext room (room.ModeNATS)
// on a shared subject and echoes back every message it sees, prefixed with
// "echo: ". It never echoes its own messages (anti-loop guard), so two
// echobots on the same subject do not spin forever.
// - RPC mode (bot<->process): the bot registers a NATS request/reply
// responder on an rpc.* subject that returns "echo: " + the request body.
//
// echobot is application code that consumes the unibus client library; it is
// not a reusable registry function. The bus is the neighbouring `unibus` app.
package main
import (
"flag"
"log"
"os"
"os/signal"
"syscall"
"github.com/enmanuel/unibus/pkg/client"
"github.com/enmanuel/unibus/pkg/frame"
"github.com/enmanuel/unibus/pkg/room"
)
func main() {
var (
natsURL = flag.String("nats-url", "nats://127.0.0.1:4250", "NATS data-plane URL of the unibus bus")
ctrlURL = flag.String("ctrl-url", "http://127.0.0.1:8470", "membershipd control-plane HTTP URL")
roomSubject = flag.String("room-subject", "room.echo", "cleartext chat subject the bot listens on (bot<->human)")
rpcSubject = flag.String("rpc-subject", "rpc.echo", "request/reply subject the bot responds on (bot<->process)")
idFile = flag.String("id-file", "./local_files/echobot.id", "path to the bot's long-term identity file")
)
flag.Parse()
logger := log.New(os.Stderr, "[echobot] ", log.LstdFlags|log.Lmsgprefix)
id, err := client.LoadOrCreateIdentity(*idFile)
if err != nil {
logger.Fatalf("load/create identity %q: %v", *idFile, err)
}
c, err := client.New(*natsURL, *ctrlURL, id)
if err != nil {
logger.Fatalf("connect to bus (nats=%s ctrl=%s): %v", *natsURL, *ctrlURL, err)
}
defer c.Close()
self := c.Endpoint()
// --- Chat mode (bot<->human) --------------------------------------------
// A cleartext room mapped to the shared subject. NATS fans out by subject,
// so the bot shares the conversation with any peer on the same subject even
// if their room ids differ (same pattern as unibus worker/chat).
chatRoom, err := c.CreateRoom(*roomSubject, room.ModeNATS)
if err != nil {
logger.Fatalf("create chat room on subject %q: %v", *roomSubject, err)
}
chatSub, err := c.Subscribe(chatRoom, func(f frame.Frame, plaintext []byte) {
// Anti-loop guard: never echo our own messages, or two echobots (or a
// single bot seeing its own publish) would loop forever.
if f.Sender == self.ID {
return
}
reply := "echo: " + string(plaintext)
if err := c.Publish(chatRoom, []byte(reply)); err != nil {
logger.Printf("chat: publish echo failed: %v", err)
return
}
logger.Printf("chat: echoed %q -> %q (from %s)", string(plaintext), reply, f.Sender)
})
if err != nil {
logger.Fatalf("subscribe to chat room: %v", err)
}
defer chatSub.Unsubscribe()
// --- RPC mode (bot<->process) -------------------------------------------
// NATS request/reply: a responder on the rpc subject returns "echo: " + body.
rpcSub, err := c.Reply(*rpcSubject, func(body []byte) []byte {
reply := "echo: " + string(body)
logger.Printf("rpc: %q -> %q", string(body), reply)
return []byte(reply)
})
if err != nil {
logger.Fatalf("register rpc responder on %q: %v", *rpcSubject, err)
}
defer rpcSub.Unsubscribe()
logger.Printf("echobot up: endpoint=%s bus(nats=%s ctrl=%s) chat-subject=%q rpc-subject=%q",
self.ID, *natsURL, *ctrlURL, *roomSubject, *rpcSubject)
// --- Loop until SIGINT/SIGTERM, then shut down cleanly ------------------
sig := make(chan os.Signal, 1)
signal.Notify(sig, syscall.SIGINT, syscall.SIGTERM)
s := <-sig
logger.Printf("received %v, shutting down", s)
}