fc644ecd6e
Reemplaza el scaffold del echobot por la plataforma completa de bots traida desde ~/DataProyects/Github/agents_and_robots tras la operacion Matrix-out: los bots ya no hablan por Matrix sino por el bus unibus (modelo todo-rooms + E2E via shell/transportunibus sobre github.com/enmanuel/unibus/pkg/client). - go.mod: replace de unibus -> ../unibus y de fn-registry -> ../../../.. (paths relativos reajustados a la nueva ubicacion dentro de fn_registry). - app.md: bump a 0.2.0, descripcion + arquitectura + comandos + gotchas reales. - modulo Go conservado como github.com/enmanuel/agents (sin reescribir imports). agents_and_robots queda archivado como museo de la era Matrix.
244 lines
7.3 KiB
Go
244 lines
7.3 KiB
Go
package agents
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"log/slog"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/enmanuel/agents/internal/config"
|
|
"github.com/enmanuel/agents/pkg/command"
|
|
"github.com/enmanuel/agents/pkg/decision"
|
|
"github.com/enmanuel/agents/pkg/transport"
|
|
"github.com/enmanuel/agents/shell/effects"
|
|
"github.com/enmanuel/agents/shell/transportunibus"
|
|
)
|
|
|
|
// Robot is a lightweight runtime for command-only bots.
|
|
// Unlike Agent, it has no LLM, rules, memory, knowledge, skills, or tools.
|
|
// It connects to the bus and dispatches commands; non-command messages are ignored.
|
|
type Robot struct {
|
|
cfg *config.AgentConfig
|
|
transport *transportunibus.Transport
|
|
sender effects.Sender
|
|
logger *slog.Logger
|
|
|
|
// Lifecycle
|
|
cancel context.CancelFunc
|
|
done chan struct{}
|
|
|
|
// Commands — handlers keyed by canonical name; aliases maps alias → canonical.
|
|
commands map[string]CommandHandler
|
|
cmdAliases map[string]string
|
|
customSpecs []command.Spec
|
|
startTime time.Time
|
|
|
|
// Personality prefix for replies
|
|
prefix string
|
|
}
|
|
|
|
// NewRobot creates a lightweight command-only bot from its config and logger.
|
|
// It initializes the unibus transport and built-in commands.
|
|
func NewRobot(cfg *config.AgentConfig, logger *slog.Logger) (*Robot, error) {
|
|
tr, err := transportunibus.New(cfg.Bus, logger)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("unibus transport: %w", err)
|
|
}
|
|
|
|
r := &Robot{
|
|
cfg: cfg,
|
|
transport: tr,
|
|
sender: tr.Sender(),
|
|
logger: logger,
|
|
done: make(chan struct{}),
|
|
commands: make(map[string]CommandHandler),
|
|
cmdAliases: command.BuiltinNames(),
|
|
startTime: time.Now(),
|
|
prefix: cfg.Personality.Prefix,
|
|
}
|
|
|
|
// Register built-in commands (robot-appropriate subset).
|
|
r.registerBuiltinCommands()
|
|
|
|
return r, nil
|
|
}
|
|
|
|
// registerBuiltinCommands registers command handlers appropriate for a robot.
|
|
// Robots support: help, ping, status, info, version.
|
|
// They do NOT support: tools, tool, clear, prompts (no LLM, no memory, no tools).
|
|
func (r *Robot) registerBuiltinCommands() {
|
|
r.commands["help"] = r.cmdHelp
|
|
r.commands["ping"] = r.cmdPing
|
|
r.commands["status"] = r.cmdStatus
|
|
r.commands["info"] = r.cmdInfo
|
|
r.commands["version"] = r.cmdVersion
|
|
}
|
|
|
|
// RegisterCommand adds a custom command handler for this robot.
|
|
func (r *Robot) RegisterCommand(spec command.Spec, handler CommandHandler) {
|
|
r.commands[spec.Name] = handler
|
|
r.cmdAliases[spec.Name] = spec.Name
|
|
for _, alias := range spec.Aliases {
|
|
r.cmdAliases[alias] = spec.Name
|
|
}
|
|
r.customSpecs = append(r.customSpecs, spec)
|
|
r.logger.Info("command_registered", "command", spec.Name, "aliases", spec.Aliases)
|
|
}
|
|
|
|
// Run starts the robot's transport loop. Blocks until ctx is cancelled.
|
|
func (r *Robot) Run(ctx context.Context) error {
|
|
ctx, r.cancel = context.WithCancel(ctx)
|
|
defer close(r.done)
|
|
|
|
if r.transport != nil {
|
|
defer r.transport.Close()
|
|
}
|
|
|
|
r.logger.Info("robot starting",
|
|
"id", r.cfg.Agent.ID,
|
|
"name", r.cfg.Agent.Name,
|
|
"type", "robot",
|
|
"endpoint", r.transport.Endpoint(),
|
|
)
|
|
|
|
return r.transport.Run(ctx, r.handleInbound)
|
|
}
|
|
|
|
// Stop cancels this robot's individual context, causing Run to return.
|
|
func (r *Robot) Stop() {
|
|
if r.cancel != nil {
|
|
r.cancel()
|
|
}
|
|
}
|
|
|
|
// Done returns a channel that is closed when Run has returned.
|
|
func (r *Robot) Done() <-chan struct{} {
|
|
return r.done
|
|
}
|
|
|
|
// handleInbound is called for each filtered incoming message. It carries no
|
|
// mautrix types, so the robot core is transport-neutral. For a robot, only
|
|
// commands are processed; all other messages are silently ignored.
|
|
func (r *Robot) handleInbound(ctx context.Context, in transport.InboundMessage) {
|
|
msgCtx := inboundToMsgCtx(in)
|
|
roomID := in.RoomID
|
|
|
|
// Only process commands. Non-command messages are silently ignored.
|
|
if msgCtx.Command == "" {
|
|
r.logger.Debug("non-command message, ignoring (robot)",
|
|
"sender", msgCtx.SenderID,
|
|
"room", roomID,
|
|
)
|
|
return
|
|
}
|
|
|
|
r.logger.Info("command_received",
|
|
"command", msgCtx.Command,
|
|
"sender", msgCtx.SenderID,
|
|
"room", roomID,
|
|
"args", msgCtx.Args,
|
|
)
|
|
|
|
// Resolve aliases
|
|
cmdName := msgCtx.Command
|
|
if canonical, ok := r.cmdAliases[cmdName]; ok {
|
|
cmdName = canonical
|
|
}
|
|
|
|
if handler, ok := r.commands[cmdName]; ok {
|
|
r.logger.Info("command_executed", "command", cmdName)
|
|
reply := handler(ctx, msgCtx)
|
|
_ = r.sendReply(ctx, roomID, msgCtx.EventID, msgCtx.ThreadID, reply)
|
|
return
|
|
}
|
|
|
|
// Unknown command
|
|
r.logger.Info("command_unknown", "command", msgCtx.Command)
|
|
_ = r.sendReply(ctx, roomID, msgCtx.EventID, msgCtx.ThreadID,
|
|
fmt.Sprintf("Comando desconocido: `!%s`. Usa `!help` para ver comandos disponibles.", msgCtx.Command))
|
|
}
|
|
|
|
// sendReply sends a markdown reply that respects thread context.
|
|
func (r *Robot) sendReply(ctx context.Context, roomID, eventID, threadID, markdown string) error {
|
|
if threadID != "" {
|
|
return r.sender.SendThreadMarkdown(ctx, roomID, threadID, eventID, markdown)
|
|
}
|
|
return r.sender.SendReplyMarkdown(ctx, roomID, eventID, markdown)
|
|
}
|
|
|
|
// ── Built-in command handlers (robot subset) ─────────────────────────────
|
|
|
|
func (r *Robot) cmdHelp(_ context.Context, _ decision.MessageContext) string {
|
|
var b strings.Builder
|
|
b.WriteString("**Comandos disponibles:**\n\n")
|
|
|
|
// Built-in commands appropriate for robots
|
|
robotBuiltins := []command.Spec{
|
|
{Name: "help", Aliases: []string{"h"}, Description: "Lista comandos disponibles", Usage: "!help"},
|
|
{Name: "ping", Description: "Alive check", Usage: "!ping"},
|
|
{Name: "status", Description: "Info del robot: uptime", Usage: "!status"},
|
|
{Name: "info", Description: "Nombre, version y descripcion", Usage: "!info"},
|
|
{Name: "version", Aliases: []string{"v"}, Description: "Version del robot", Usage: "!version"},
|
|
}
|
|
for _, spec := range robotBuiltins {
|
|
writeSpec(&b, spec)
|
|
}
|
|
|
|
// Agent-specific commands (registered via RegisterCommand)
|
|
if len(r.customSpecs) > 0 {
|
|
b.WriteString("\n**Comandos del robot:**\n\n")
|
|
for _, spec := range r.customSpecs {
|
|
if spec.Hidden {
|
|
continue
|
|
}
|
|
writeSpec(&b, spec)
|
|
}
|
|
}
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func (r *Robot) cmdPing(_ context.Context, _ decision.MessageContext) string {
|
|
return fmt.Sprintf("pong — %s", time.Now().Format(time.RFC3339))
|
|
}
|
|
|
|
func (r *Robot) cmdStatus(_ context.Context, _ decision.MessageContext) string {
|
|
uptime := time.Since(r.startTime).Truncate(time.Second)
|
|
|
|
var b strings.Builder
|
|
fmt.Fprintf(&b, "**Estado de %s:**\n\n", r.cfg.Agent.Name)
|
|
fmt.Fprintf(&b, "- **Tipo:** robot\n")
|
|
fmt.Fprintf(&b, "- **Uptime:** %s\n", uptime)
|
|
fmt.Fprintf(&b, "- **Comandos custom:** %d\n", len(r.customSpecs))
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func (r *Robot) cmdInfo(_ context.Context, _ decision.MessageContext) string {
|
|
var b strings.Builder
|
|
|
|
b.WriteString("## Identidad\n\n")
|
|
fmt.Fprintf(&b, "- **Nombre:** %s\n", r.cfg.Agent.Name)
|
|
fmt.Fprintf(&b, "- **ID:** `%s`\n", r.cfg.Agent.ID)
|
|
fmt.Fprintf(&b, "- **Tipo:** robot\n")
|
|
if r.cfg.Agent.Version != "" {
|
|
fmt.Fprintf(&b, "- **Version:** %s\n", r.cfg.Agent.Version)
|
|
}
|
|
fmt.Fprintf(&b, "- **Descripcion:** %s\n", r.cfg.Agent.Description)
|
|
|
|
uptime := time.Since(r.startTime).Round(time.Second)
|
|
b.WriteString("\n## Uptime\n\n")
|
|
fmt.Fprintf(&b, "- **Activo desde:** %s\n", uptime)
|
|
|
|
return b.String()
|
|
}
|
|
|
|
func (r *Robot) cmdVersion(_ context.Context, _ decision.MessageContext) string {
|
|
v := r.cfg.Agent.Version
|
|
if v == "" {
|
|
v = "sin version"
|
|
}
|
|
return fmt.Sprintf("%s %s", r.cfg.Agent.Name, v)
|
|
}
|