package devagents import ( "context" "encoding/json" "fmt" "strings" "time" "github.com/enmanuel/agents/pkg/command" "github.com/enmanuel/agents/pkg/decision" "github.com/enmanuel/agents/shell/logger" ) // 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["prompts"] = a.cmdPrompts a.commands["version"] = a.cmdVersion a.commands["metrics"] = a.cmdMetrics } // cmdHelp lists all available commands (built-in + agent-specific). 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 } writeSpec(&b, spec) } // Agent-specific commands (registered via RegisterCommand) if len(a.customSpecs) > 0 { b.WriteString("\n**Comandos del agente:**\n\n") for _, spec := range a.customSpecs { if spec.Hidden { continue } writeSpec(&b, spec) } } return b.String() } // writeSpec formats a single command spec for the help output. func writeSpec(b *strings.Builder, spec command.Spec) { aliases := "" if len(spec.Aliases) > 0 { aliases = " (" + strings.Join(prefixAll(spec.Aliases, "!"), ", ") + ")" } usage := spec.Usage if usage == "" { usage = "!" + spec.Name } fmt.Fprintf(b, "- `%s`%s — %s\n", usage, aliases, spec.Description) } // 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 [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 [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.ExecuteForRoom(ctx, toolName, argsJSON, msgCtx.RoomID) 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\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 metadata, personality, capabilities, and configuration. func (a *Agent) cmdInfo(_ context.Context, _ decision.MessageContext) string { var b strings.Builder // === Identidad === b.WriteString("## Identidad\n\n") 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) if len(a.cfg.Agent.Tags) > 0 { fmt.Fprintf(&b, "- **Tags:** %v\n", a.cfg.Agent.Tags) } // === Personalidad === if a.personality.Role != "" || a.personality.Communication.Personality != "" { b.WriteString("\n## Personalidad\n\n") if a.personality.Role != "" { fmt.Fprintf(&b, "- **Rol:** %s\n", a.personality.Role) } if a.personality.Tone != "" { fmt.Fprintf(&b, "- **Tono:** %s\n", a.personality.Tone) } if a.personality.Communication.Formality != "" { fmt.Fprintf(&b, "- **Formalidad:** %s\n", a.personality.Communication.Formality) } if a.personality.Communication.Personality != "" { fmt.Fprintf(&b, "- **Tipo:** %s\n", a.personality.Communication.Personality) } if a.personality.Communication.Humor != "" && a.personality.Communication.Humor != "none" { fmt.Fprintf(&b, "- **Humor:** %s\n", a.personality.Communication.Humor) } } // === LLM === if a.cfg.LLM.Primary.Provider != "" { b.WriteString("\n## LLM\n\n") fmt.Fprintf(&b, "- **Provider:** %s\n", a.cfg.LLM.Primary.Provider) fmt.Fprintf(&b, "- **Modelo:** %s\n", a.cfg.LLM.Primary.Model) if a.cfg.LLM.ToolUse.Enabled { fmt.Fprintf(&b, "- **Tools:** habilitadas (max %d iteraciones)\n", a.cfg.LLM.ToolUse.MaxIterations) } } // === Tools === toolCount := a.toolReg.Len() if toolCount > 0 { b.WriteString("\n## Tools disponibles\n\n") fmt.Fprintf(&b, "- **Total:** %d tools\n", toolCount) // Lista de tools (nombres) toolNames := a.toolReg.Names() if len(toolNames) > 0 && len(toolNames) <= 20 { b.WriteString("- **Lista:** ") for i, name := range toolNames { if i > 0 { b.WriteString(", ") } fmt.Fprintf(&b, "`%s`", name) } b.WriteString("\n") } } // === Skills === if a.cfg.Skills.Enabled { b.WriteString("\n## Skills\n\n") b.WriteString("- **Habilitadas:** si\n") if len(a.cfg.Skills.Categories) > 0 { fmt.Fprintf(&b, "- **Categorias:** %v\n", a.cfg.Skills.Categories) } if a.skillLoader != nil { if metas, err := a.skillLoader.LoadMeta(); err == nil { fmt.Fprintf(&b, "- **Cantidad:** %d skills\n", len(metas)) } } } // === Knowledge === hasPrivate := a.cfg.Tools.Knowledge.Enabled hasShared := a.cfg.Tools.SharedKnowledge.Enabled if hasPrivate || hasShared { b.WriteString("\n## Knowledge\n\n") if hasPrivate { b.WriteString("- **Privado:** habilitado\n") } if hasShared { b.WriteString("- **Compartido:** habilitado\n") } } // === Memoria === if a.cfg.Memory.Enabled { b.WriteString("\n## Memoria\n\n") fmt.Fprintf(&b, "- **Habilitada:** si\n") fmt.Fprintf(&b, "- **Window size:** %d mensajes\n", a.windowSize) } // === Schedules === if len(a.cfg.Schedules) > 0 { b.WriteString("\n## Schedules\n\n") fmt.Fprintf(&b, "- **Cron jobs:** %d configurados\n", len(a.cfg.Schedules)) } // === Uptime === uptime := time.Since(a.startTime).Round(time.Second) b.WriteString("\n## Uptime\n\n") fmt.Fprintf(&b, "- **Activo desde:** %s\n", uptime) return b.String() } // cmdPrompts lists available prompt-commands. func (a *Agent) cmdPrompts(_ context.Context, _ decision.MessageContext) string { if len(a.promptCmds) == 0 { return "No hay prompt-commands disponibles." } var b strings.Builder fmt.Fprintf(&b, "**Prompt-commands disponibles (%d):**\n\n", len(a.promptCmds)) for name := range a.promptCmds { fmt.Fprintf(&b, "- `!%s`\n", name) } b.WriteString("\nUso: `! [detalles adicionales...]`") 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) } // cmdMetrics aggregates today's log data and returns a summary table. func (a *Agent) cmdMetrics(_ context.Context, _ decision.MessageContext) string { if a.logDir == "" { return "Metricas no disponibles: directorio de logs no configurado." } entries, err := logger.ReadDayLogs(a.logDir, a.cfg.Agent.ID, time.Now().UTC()) if err != nil || len(entries) == 0 { return "No hay logs disponibles para hoy." } var ( totalMessages int totalCommands int totalLLMCalls int totalLLMTokens int64 totalLLMLatency int64 totalToolCalls int totalToolErrors int totalErrors int ) for _, raw := range entries { var m map[string]any if json.Unmarshal(raw, &m) != nil { continue } msg, _ := m["msg"].(string) level, _ := m["level"].(string) switch msg { case "handling event": totalMessages++ case "command_received": totalCommands++ case "tool_exec_end": totalToolCalls++ if d, ok := m["duration_ms"]; ok { totalLLMLatency += toInt64(d) // reused for tool latency aggregation if needed } case "tool_exec_error": totalToolCalls++ totalToolErrors++ case "LLM responded": totalLLMCalls++ } // Count tokens from any log line that has the field if t, ok := m["tokens_used"]; ok { totalLLMTokens += toInt64(t) } // Count LLM latency from duration_ms on LLM-related entries if msg == "LLM responded" || msg == "LLM call failed" { if d, ok := m["duration_ms"]; ok { totalLLMLatency += toInt64(d) } } if level == "ERROR" { totalErrors++ } } var avgLLMLatency string if totalLLMCalls > 0 { avgLLMLatency = fmt.Sprintf("%d ms", totalLLMLatency/int64(totalLLMCalls)) } else { avgLLMLatency = "n/a" } var b strings.Builder fmt.Fprintf(&b, "**Metricas de %s — %s:**\n\n", a.cfg.Agent.Name, time.Now().UTC().Format("2006-01-02")) b.WriteString("| Metrica | Valor |\n") b.WriteString("|---------|-------|\n") fmt.Fprintf(&b, "| Mensajes recibidos | %d |\n", totalMessages) fmt.Fprintf(&b, "| Comandos ejecutados | %d |\n", totalCommands) fmt.Fprintf(&b, "| Llamadas LLM | %d |\n", totalLLMCalls) fmt.Fprintf(&b, "| Tokens LLM (total) | %d |\n", totalLLMTokens) fmt.Fprintf(&b, "| Latencia LLM (media) | %s |\n", avgLLMLatency) fmt.Fprintf(&b, "| Llamadas a tools | %d |\n", totalToolCalls) fmt.Fprintf(&b, "| Errores de tools | %d |\n", totalToolErrors) fmt.Fprintf(&b, "| Errores totales | %d |\n", totalErrors) fmt.Fprintf(&b, "| Entradas de log | %d |\n", len(entries)) return b.String() } // toInt64 converts a JSON number (float64) to int64. func toInt64(v any) int64 { switch n := v.(type) { case float64: return int64(n) case int64: return n case json.Number: i, _ := n.Int64() return i default: return 0 } } // 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 }