diff --git a/agents/commands.go b/agents/commands.go index a7c3e8c..ff5dbc5 100644 --- a/agents/commands.go +++ b/agents/commands.go @@ -19,6 +19,7 @@ func (a *Agent) registerBuiltinCommands() { 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 } @@ -155,6 +156,21 @@ func (a *Agent) cmdInfo(_ context.Context, _ decision.MessageContext) string { 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) diff --git a/agents/runtime.go b/agents/runtime.go index 83da64f..be4d746 100644 --- a/agents/runtime.go +++ b/agents/runtime.go @@ -29,6 +29,14 @@ import ( shellmem "github.com/enmanuel/agents/shell/memory" "github.com/enmanuel/agents/shell/ssh" "github.com/enmanuel/agents/tools" + toolclock "github.com/enmanuel/agents/tools/clock" + toolfile "github.com/enmanuel/agents/tools/file" + toolhttp "github.com/enmanuel/agents/tools/http" + toolknowledge "github.com/enmanuel/agents/tools/knowledgetools" + toolmatrix "github.com/enmanuel/agents/tools/matrix" + toolmemory "github.com/enmanuel/agents/tools/memorytools" + toolssh "github.com/enmanuel/agents/tools/ssh" + toolweather "github.com/enmanuel/agents/tools/weather" ) const ( @@ -63,7 +71,10 @@ type Agent struct { windowsMu sync.RWMutex memStore memory.Store // nil when memory is disabled windowSize int - roomCtx *tools.RoomContext + roomCtx *toolmemory.RoomContext + + // Prompt-commands — loaded from prompts/*.md at startup + promptCmds map[string]string // name → prompt content // Knowledge store — non-nil when knowledge is enabled knowledgeStore *shellknowledge.FileStore @@ -73,7 +84,7 @@ type Agent struct { } // ClearWindow resets the conversation window for a room and deletes persisted -// messages from SQLite so the agent starts fresh. Implements tools.WindowClearer. +// messages from SQLite so the agent starts fresh. Implements toolmemory.WindowClearer. func (a *Agent) ClearWindow(roomID string) { a.windowsMu.Lock() a.windows[roomID] = memory.NewWindow(a.windowSize) @@ -159,7 +170,7 @@ func New(cfg *config.AgentConfig, rules []decision.Rule, logger *slog.Logger) (* // Memory subsystem var memStore memory.Store windowSize := defaultWindowSize - roomCtx := &tools.RoomContext{} + roomCtx := &toolmemory.RoomContext{} if cfg.Memory.Enabled { windowSize = cfg.Memory.WindowSize @@ -223,9 +234,12 @@ func New(cfg *config.AgentConfig, rules []decision.Rule, logger *slog.Logger) (* // Register built-in command handlers a.registerBuiltinCommands() + // Load prompt-commands from prompts/ directory + a.loadPromptCommands() + // Register memory_clear_context with self as WindowClearer (after a is created) if cfg.Tools.Memory.Enabled && memStore != nil { - toolReg.Register(tools.NewMemoryClearContext(a, roomCtx)) + toolReg.Register(toolmemory.NewMemoryClearContext(a, roomCtx)) } // Matrix event listener @@ -247,6 +261,26 @@ func (a *Agent) RegisterCommand(spec command.Spec, handler CommandHandler) { a.logger.Info("command_registered", "command", spec.Name, "aliases", spec.Aliases) } +// loadPromptCommands scans the project-root prompts/ directory and loads all .md files. +func (a *Agent) loadPromptCommands() { + prompts, err := command.LoadPromptCommands("prompts") + if err != nil { + a.logger.Warn("failed to load prompt-commands", "err", err) + return + } + a.promptCmds = make(map[string]string, len(prompts)) + for _, p := range prompts { + a.promptCmds[p.Name] = p.Content + } + if len(a.promptCmds) > 0 { + names := make([]string, 0, len(a.promptCmds)) + for n := range a.promptCmds { + names = append(names, n) + } + a.logger.Info("prompt-commands loaded", "count", len(a.promptCmds), "names", names) + } +} + // SetBus attaches the agent to the inter-agent bus for orchestration. // Must be called before Run(). func (a *Agent) SetBus(b *bus.Bus) { @@ -469,11 +503,20 @@ func (a *Agent) handleEvent(ctx context.Context, msgCtx decision.MessageContext, return } - // Unknown command — never falls through to rules or LLM - a.logger.Info("command_unknown", "command", msgCtx.Command) - _ = a.matrix.SendReplyMarkdown(ctx, roomID, msgCtx.EventID, - fmt.Sprintf("Comando desconocido: `!%s`. Usa `!help` para ver comandos disponibles.", msgCtx.Command)) - return + // Prompt-command: expand .md content and pass to LLM + if content, ok := a.promptCmds[cmdName]; ok { + a.logger.Info("prompt_command_expanded", "command", cmdName) + msgCtx.Content = command.ExpandPrompt(content, msgCtx.Args) + msgCtx.Command = "" + msgCtx.Args = nil + // Fall through to rules/LLM flow below + } else { + // Unknown command — never falls through to rules or LLM + a.logger.Info("command_unknown", "command", msgCtx.Command) + _ = a.matrix.SendReplyMarkdown(ctx, roomID, msgCtx.EventID, + fmt.Sprintf("Comando desconocido: `!%s`. Usa `!help` para ver comandos disponibles.", msgCtx.Command)) + return + } } // ── Non-command flow ───────────────────────────────────────────── @@ -729,54 +772,54 @@ func buildToolRegistry( matrixClient *matrix.Client, memStore memory.Store, kStore *shellknowledge.FileStore, - roomCtx *tools.RoomContext, + roomCtx *toolmemory.RoomContext, logger *slog.Logger, ) *tools.Registry { reg := tools.NewRegistry(logger) if cfg.Tools.HTTP.Enabled { - reg.Register(tools.NewHTTPGet(cfg.Tools.HTTP)) - reg.Register(tools.NewHTTPPost(cfg.Tools.HTTP)) + reg.Register(toolhttp.NewHTTPGet(cfg.Tools.HTTP)) + reg.Register(toolhttp.NewHTTPPost(cfg.Tools.HTTP)) logger.Debug("registered http tools") } if cfg.Tools.SSH.Enabled { - reg.Register(tools.NewSSHCommand(cfg.Tools.SSH, sshExec)) + reg.Register(toolssh.NewSSHCommand(cfg.Tools.SSH, sshExec)) logger.Debug("registered ssh tool") } if cfg.Tools.FileOps.Enabled { - reg.Register(tools.NewReadFile(cfg.Tools.FileOps)) + reg.Register(toolfile.NewReadFile(cfg.Tools.FileOps)) logger.Debug("registered file tool") } // current_time is always available - reg.Register(tools.NewCurrentTime()) + reg.Register(toolclock.NewCurrentTime()) logger.Debug("registered current_time tool") // weather tool is always available - reg.Register(tools.NewWeather()) + reg.Register(toolweather.NewWeather()) logger.Debug("registered weather tool") // matrix_send is always available - reg.Register(tools.NewMatrixSend(matrixClient)) + reg.Register(toolmatrix.NewMatrixSend(matrixClient)) logger.Debug("registered matrix tool") // Memory tools (memory_clear_context registered later since it needs the Agent) if cfg.Tools.Memory.Enabled && memStore != nil { - reg.Register(tools.NewMemorySave(cfg.Agent.ID, memStore)) - reg.Register(tools.NewMemoryRecall(cfg.Agent.ID, memStore)) - reg.Register(tools.NewMemoryForget(cfg.Agent.ID, memStore)) - reg.Register(tools.NewMemorySummary(cfg.Agent.ID, memStore)) + reg.Register(toolmemory.NewMemorySave(cfg.Agent.ID, memStore)) + reg.Register(toolmemory.NewMemoryRecall(cfg.Agent.ID, memStore)) + reg.Register(toolmemory.NewMemoryForget(cfg.Agent.ID, memStore)) + reg.Register(toolmemory.NewMemorySummary(cfg.Agent.ID, memStore)) logger.Debug("registered memory tools") } // Knowledge tools if cfg.Tools.Knowledge.Enabled && kStore != nil { - reg.Register(tools.NewKnowledgeSearch(kStore)) - reg.Register(tools.NewKnowledgeRead(kStore)) - reg.Register(tools.NewKnowledgeWrite(kStore)) - reg.Register(tools.NewKnowledgeList(kStore)) + reg.Register(toolknowledge.NewKnowledgeSearch(kStore)) + reg.Register(toolknowledge.NewKnowledgeRead(kStore)) + reg.Register(toolknowledge.NewKnowledgeWrite(kStore)) + reg.Register(toolknowledge.NewKnowledgeList(kStore)) logger.Debug("registered knowledge tools") } diff --git a/pkg/command/builtins.go b/pkg/command/builtins.go index a5e9f77..d38c4fd 100644 --- a/pkg/command/builtins.go +++ b/pkg/command/builtins.go @@ -39,6 +39,11 @@ func Builtins() []Spec { Description: "Limpia ventana de conversacion del room actual", Usage: "!clear", }, + { + Name: "prompts", + Description: "Lista prompt-commands disponibles (archivos .md en prompts/)", + Usage: "!prompts", + }, { Name: "version", Aliases: []string{"v"}, diff --git a/pkg/command/prompts.go b/pkg/command/prompts.go new file mode 100644 index 0000000..4614715 --- /dev/null +++ b/pkg/command/prompts.go @@ -0,0 +1,51 @@ +package command + +import ( + "os" + "path/filepath" + "strings" +) + +// PromptCommand maps a command name to its prompt content loaded from a .md file. +type PromptCommand struct { + Name string // filename without .md extension + Content string // file content (the prompt text) +} + +// LoadPromptCommands scans dir for .md files and returns one PromptCommand per file. +// Returns nil (no error) if the directory does not exist. +func LoadPromptCommands(dir string) ([]PromptCommand, error) { + entries, err := os.ReadDir(dir) + if err != nil { + if os.IsNotExist(err) { + return nil, nil + } + return nil, err + } + + var prompts []PromptCommand + for _, e := range entries { + if e.IsDir() || !strings.HasSuffix(e.Name(), ".md") { + continue + } + data, err := os.ReadFile(filepath.Join(dir, e.Name())) + if err != nil { + continue + } + name := strings.TrimSuffix(e.Name(), ".md") + prompts = append(prompts, PromptCommand{ + Name: name, + Content: strings.TrimSpace(string(data)), + }) + } + return prompts, nil +} + +// ExpandPrompt builds the final message by concatenating the prompt content +// with any extra arguments the user provided after the command. +func ExpandPrompt(content string, args []string) string { + if len(args) == 0 { + return content + } + return content + "\n\n" + strings.Join(args, " ") +} diff --git a/prompts/hola.md b/prompts/hola.md new file mode 100644 index 0000000..22875c8 --- /dev/null +++ b/prompts/hola.md @@ -0,0 +1 @@ +Hola! \ No newline at end of file