// 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) }