Files
agents_and_robots/agents/commands.go
T
egutierrez 33b11a63c8 feat: añadir sistema de comandos directos (!command)
Implementa pkg/command/ como core puro: tipos Spec/ParsedArgs, parser de
args key=value con soporte de comillas, specs de 8 comandos built-in
(help, tools, tool, ping, status, info, clear, version) y BuiltinNames()
para aliases.

En agents/runtime.go: nuevo flujo handleEvent que prioriza comandos sobre
LLM — custom rules del agente → built-in handlers → comando desconocido →
LLM fallback. Handlers en agents/commands.go. El comando !tool ejecuta
tools directamente via Registry con args key=value parseados.

LLM ahora es opcional: si no hay provider configurado, el agente corre
como simple_bot respondiendo solo a comandos.

Se extrae executeActions() como helper reutilizable para ambos flujos
(comando y no-comando).
2026-03-07 01:11:26 +00:00

181 lines
5.1 KiB
Go

package agents
import (
"context"
"fmt"
"strings"
"time"
"github.com/enmanuel/agents/pkg/command"
"github.com/enmanuel/agents/pkg/decision"
)
// registerBuiltinCommands registers all built-in command handlers.
func (a *Agent) registerBuiltinCommands() {
a.commands["help"] = a.cmdHelp
a.commands["tools"] = a.cmdTools
a.commands["tool"] = a.cmdTool
a.commands["ping"] = a.cmdPing
a.commands["status"] = a.cmdStatus
a.commands["info"] = a.cmdInfo
a.commands["clear"] = a.cmdClear
a.commands["version"] = a.cmdVersion
}
// cmdHelp lists all available commands (built-in + custom rules with MatchCommand).
func (a *Agent) cmdHelp(_ context.Context, _ decision.MessageContext) string {
var b strings.Builder
b.WriteString("Comandos disponibles:\n\n")
// Built-in commands
for _, spec := range command.Builtins() {
if spec.Hidden {
continue
}
aliases := ""
if len(spec.Aliases) > 0 {
aliases = " (" + strings.Join(prefixAll(spec.Aliases, "!"), ", ") + ")"
}
fmt.Fprintf(&b, " %s%s — %s\n", spec.Usage, aliases, spec.Description)
}
// Custom commands from agent rules (rules named with MatchCommand pattern)
customRules := a.customCommandRules()
if len(customRules) > 0 {
b.WriteString("\nComandos del agente:\n")
for _, name := range customRules {
fmt.Fprintf(&b, " !%s\n", name)
}
}
return b.String()
}
// cmdTools lists all tools registered in the agent's tool registry.
func (a *Agent) cmdTools(_ context.Context, _ decision.MessageContext) string {
names := a.toolReg.Names()
if len(names) == 0 {
return "No hay tools registradas."
}
var b strings.Builder
fmt.Fprintf(&b, "Tools disponibles (%d):\n\n", len(names))
for _, name := range names {
t, _ := a.toolReg.Get(name)
fmt.Fprintf(&b, " %s — %s\n", t.Def.Name, t.Def.Description)
for _, p := range t.Def.Parameters {
req := ""
if p.Required {
req = " (requerido)"
}
fmt.Fprintf(&b, " %s: %s%s\n", p.Name, p.Description, req)
}
}
b.WriteString("\nUso: !tool <nombre> [key=value ...]")
return b.String()
}
// cmdTool executes a tool directly with key=value args.
func (a *Agent) cmdTool(ctx context.Context, msgCtx decision.MessageContext) string {
if len(msgCtx.Args) == 0 {
return "Uso: !tool <nombre> [key=value ...]\nUsa !tools para ver tools disponibles."
}
toolName := msgCtx.Args[0]
if _, ok := a.toolReg.Get(toolName); !ok {
return fmt.Sprintf("Tool %q no encontrada. Usa !tools para ver tools disponibles.", toolName)
}
// Parse remaining args as key=value
parsed := command.ParseArgs(msgCtx.Args[1:])
argsJSON := command.ArgsToJSON(parsed.Named)
a.logger.Info("executing tool via command",
"tool", toolName,
"args", argsJSON,
)
result := a.toolReg.Execute(ctx, toolName, argsJSON)
if result.Err != nil {
return fmt.Sprintf("Error ejecutando %s: %s", toolName, result.Err)
}
return fmt.Sprintf("%s:\n%s", toolName, result.Output)
}
// cmdPing responds with pong and timestamp.
func (a *Agent) cmdPing(_ context.Context, _ decision.MessageContext) string {
return fmt.Sprintf("pong — %s", time.Now().Format(time.RFC3339))
}
// cmdStatus shows agent uptime and active rooms.
func (a *Agent) cmdStatus(_ context.Context, _ decision.MessageContext) string {
uptime := time.Since(a.startTime).Truncate(time.Second)
a.windowsMu.RLock()
roomCount := len(a.windows)
a.windowsMu.RUnlock()
var b strings.Builder
fmt.Fprintf(&b, "Estado de %s:\n", a.cfg.Agent.Name)
fmt.Fprintf(&b, " Uptime: %s\n", uptime)
fmt.Fprintf(&b, " Rooms activos: %d\n", roomCount)
fmt.Fprintf(&b, " Window size: %d\n", a.windowSize)
fmt.Fprintf(&b, " Tools: %d\n", a.toolReg.Len())
if a.llm != nil {
fmt.Fprintf(&b, " LLM: %s/%s\n", a.cfg.LLM.Primary.Provider, a.cfg.LLM.Primary.Model)
} else {
b.WriteString(" LLM: no configurado\n")
}
return b.String()
}
// cmdInfo shows agent name, version, and description.
func (a *Agent) cmdInfo(_ context.Context, _ decision.MessageContext) string {
var b strings.Builder
fmt.Fprintf(&b, "Nombre: %s\n", a.cfg.Agent.Name)
fmt.Fprintf(&b, "ID: %s\n", a.cfg.Agent.ID)
if a.cfg.Agent.Version != "" {
fmt.Fprintf(&b, "Version: %s\n", a.cfg.Agent.Version)
}
fmt.Fprintf(&b, "Descripcion: %s\n", a.cfg.Agent.Description)
return b.String()
}
// cmdClear clears the conversation window for the current room.
func (a *Agent) cmdClear(_ context.Context, msgCtx decision.MessageContext) string {
a.ClearWindow(msgCtx.RoomID)
return "Ventana de conversacion limpiada."
}
// cmdVersion shows the agent version.
func (a *Agent) cmdVersion(_ context.Context, _ decision.MessageContext) string {
v := a.cfg.Agent.Version
if v == "" {
v = "sin version"
}
return fmt.Sprintf("%s %s", a.cfg.Agent.Name, v)
}
// customCommandRules extracts rule names that look like command handlers.
func (a *Agent) customCommandRules() []string {
var names []string
for _, r := range a.rules {
if r.Name != "" {
names = append(names, r.Name)
}
}
return names
}
// prefixAll adds a prefix to each string in a slice.
func prefixAll(ss []string, prefix string) []string {
out := make([]string, len(ss))
for i, s := range ss {
out[i] = prefix + s
}
return out
}