feat: sistema de prompts externos y mejoras en runtime de agentes
Añade pkg/command/prompts.go para cargar prompts desde archivos .md externos en el directorio prompts/. Mejoras en agents/runtime.go para el manejo de herramientas y flujo de ejecución. Nuevos comandos en agents/commands.go. Ajustes menores en pkg/command/builtins.go para integrar las nuevas capacidades. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -19,6 +19,7 @@ func (a *Agent) registerBuiltinCommands() {
|
|||||||
a.commands["status"] = a.cmdStatus
|
a.commands["status"] = a.cmdStatus
|
||||||
a.commands["info"] = a.cmdInfo
|
a.commands["info"] = a.cmdInfo
|
||||||
a.commands["clear"] = a.cmdClear
|
a.commands["clear"] = a.cmdClear
|
||||||
|
a.commands["prompts"] = a.cmdPrompts
|
||||||
a.commands["version"] = a.cmdVersion
|
a.commands["version"] = a.cmdVersion
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -155,6 +156,21 @@ func (a *Agent) cmdInfo(_ context.Context, _ decision.MessageContext) string {
|
|||||||
return b.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: `!<nombre> [detalles adicionales...]`")
|
||||||
|
return b.String()
|
||||||
|
}
|
||||||
|
|
||||||
// cmdClear clears the conversation window for the current room.
|
// cmdClear clears the conversation window for the current room.
|
||||||
func (a *Agent) cmdClear(_ context.Context, msgCtx decision.MessageContext) string {
|
func (a *Agent) cmdClear(_ context.Context, msgCtx decision.MessageContext) string {
|
||||||
a.ClearWindow(msgCtx.RoomID)
|
a.ClearWindow(msgCtx.RoomID)
|
||||||
|
|||||||
+68
-25
@@ -29,6 +29,14 @@ import (
|
|||||||
shellmem "github.com/enmanuel/agents/shell/memory"
|
shellmem "github.com/enmanuel/agents/shell/memory"
|
||||||
"github.com/enmanuel/agents/shell/ssh"
|
"github.com/enmanuel/agents/shell/ssh"
|
||||||
"github.com/enmanuel/agents/tools"
|
"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 (
|
const (
|
||||||
@@ -63,7 +71,10 @@ type Agent struct {
|
|||||||
windowsMu sync.RWMutex
|
windowsMu sync.RWMutex
|
||||||
memStore memory.Store // nil when memory is disabled
|
memStore memory.Store // nil when memory is disabled
|
||||||
windowSize int
|
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
|
// Knowledge store — non-nil when knowledge is enabled
|
||||||
knowledgeStore *shellknowledge.FileStore
|
knowledgeStore *shellknowledge.FileStore
|
||||||
@@ -73,7 +84,7 @@ type Agent struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ClearWindow resets the conversation window for a room and deletes persisted
|
// 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) {
|
func (a *Agent) ClearWindow(roomID string) {
|
||||||
a.windowsMu.Lock()
|
a.windowsMu.Lock()
|
||||||
a.windows[roomID] = memory.NewWindow(a.windowSize)
|
a.windows[roomID] = memory.NewWindow(a.windowSize)
|
||||||
@@ -159,7 +170,7 @@ func New(cfg *config.AgentConfig, rules []decision.Rule, logger *slog.Logger) (*
|
|||||||
// Memory subsystem
|
// Memory subsystem
|
||||||
var memStore memory.Store
|
var memStore memory.Store
|
||||||
windowSize := defaultWindowSize
|
windowSize := defaultWindowSize
|
||||||
roomCtx := &tools.RoomContext{}
|
roomCtx := &toolmemory.RoomContext{}
|
||||||
|
|
||||||
if cfg.Memory.Enabled {
|
if cfg.Memory.Enabled {
|
||||||
windowSize = cfg.Memory.WindowSize
|
windowSize = cfg.Memory.WindowSize
|
||||||
@@ -223,9 +234,12 @@ func New(cfg *config.AgentConfig, rules []decision.Rule, logger *slog.Logger) (*
|
|||||||
// Register built-in command handlers
|
// Register built-in command handlers
|
||||||
a.registerBuiltinCommands()
|
a.registerBuiltinCommands()
|
||||||
|
|
||||||
|
// Load prompt-commands from prompts/ directory
|
||||||
|
a.loadPromptCommands()
|
||||||
|
|
||||||
// Register memory_clear_context with self as WindowClearer (after a is created)
|
// Register memory_clear_context with self as WindowClearer (after a is created)
|
||||||
if cfg.Tools.Memory.Enabled && memStore != nil {
|
if cfg.Tools.Memory.Enabled && memStore != nil {
|
||||||
toolReg.Register(tools.NewMemoryClearContext(a, roomCtx))
|
toolReg.Register(toolmemory.NewMemoryClearContext(a, roomCtx))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Matrix event listener
|
// 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)
|
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.
|
// SetBus attaches the agent to the inter-agent bus for orchestration.
|
||||||
// Must be called before Run().
|
// Must be called before Run().
|
||||||
func (a *Agent) SetBus(b *bus.Bus) {
|
func (a *Agent) SetBus(b *bus.Bus) {
|
||||||
@@ -469,11 +503,20 @@ func (a *Agent) handleEvent(ctx context.Context, msgCtx decision.MessageContext,
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Unknown command — never falls through to rules or LLM
|
// Prompt-command: expand .md content and pass to LLM
|
||||||
a.logger.Info("command_unknown", "command", msgCtx.Command)
|
if content, ok := a.promptCmds[cmdName]; ok {
|
||||||
_ = a.matrix.SendReplyMarkdown(ctx, roomID, msgCtx.EventID,
|
a.logger.Info("prompt_command_expanded", "command", cmdName)
|
||||||
fmt.Sprintf("Comando desconocido: `!%s`. Usa `!help` para ver comandos disponibles.", msgCtx.Command))
|
msgCtx.Content = command.ExpandPrompt(content, msgCtx.Args)
|
||||||
return
|
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 ─────────────────────────────────────────────
|
// ── Non-command flow ─────────────────────────────────────────────
|
||||||
@@ -729,54 +772,54 @@ func buildToolRegistry(
|
|||||||
matrixClient *matrix.Client,
|
matrixClient *matrix.Client,
|
||||||
memStore memory.Store,
|
memStore memory.Store,
|
||||||
kStore *shellknowledge.FileStore,
|
kStore *shellknowledge.FileStore,
|
||||||
roomCtx *tools.RoomContext,
|
roomCtx *toolmemory.RoomContext,
|
||||||
logger *slog.Logger,
|
logger *slog.Logger,
|
||||||
) *tools.Registry {
|
) *tools.Registry {
|
||||||
reg := tools.NewRegistry(logger)
|
reg := tools.NewRegistry(logger)
|
||||||
|
|
||||||
if cfg.Tools.HTTP.Enabled {
|
if cfg.Tools.HTTP.Enabled {
|
||||||
reg.Register(tools.NewHTTPGet(cfg.Tools.HTTP))
|
reg.Register(toolhttp.NewHTTPGet(cfg.Tools.HTTP))
|
||||||
reg.Register(tools.NewHTTPPost(cfg.Tools.HTTP))
|
reg.Register(toolhttp.NewHTTPPost(cfg.Tools.HTTP))
|
||||||
logger.Debug("registered http tools")
|
logger.Debug("registered http tools")
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Tools.SSH.Enabled {
|
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")
|
logger.Debug("registered ssh tool")
|
||||||
}
|
}
|
||||||
|
|
||||||
if cfg.Tools.FileOps.Enabled {
|
if cfg.Tools.FileOps.Enabled {
|
||||||
reg.Register(tools.NewReadFile(cfg.Tools.FileOps))
|
reg.Register(toolfile.NewReadFile(cfg.Tools.FileOps))
|
||||||
logger.Debug("registered file tool")
|
logger.Debug("registered file tool")
|
||||||
}
|
}
|
||||||
|
|
||||||
// current_time is always available
|
// current_time is always available
|
||||||
reg.Register(tools.NewCurrentTime())
|
reg.Register(toolclock.NewCurrentTime())
|
||||||
logger.Debug("registered current_time tool")
|
logger.Debug("registered current_time tool")
|
||||||
|
|
||||||
// weather tool is always available
|
// weather tool is always available
|
||||||
reg.Register(tools.NewWeather())
|
reg.Register(toolweather.NewWeather())
|
||||||
logger.Debug("registered weather tool")
|
logger.Debug("registered weather tool")
|
||||||
|
|
||||||
// matrix_send is always available
|
// matrix_send is always available
|
||||||
reg.Register(tools.NewMatrixSend(matrixClient))
|
reg.Register(toolmatrix.NewMatrixSend(matrixClient))
|
||||||
logger.Debug("registered matrix tool")
|
logger.Debug("registered matrix tool")
|
||||||
|
|
||||||
// Memory tools (memory_clear_context registered later since it needs the Agent)
|
// Memory tools (memory_clear_context registered later since it needs the Agent)
|
||||||
if cfg.Tools.Memory.Enabled && memStore != nil {
|
if cfg.Tools.Memory.Enabled && memStore != nil {
|
||||||
reg.Register(tools.NewMemorySave(cfg.Agent.ID, memStore))
|
reg.Register(toolmemory.NewMemorySave(cfg.Agent.ID, memStore))
|
||||||
reg.Register(tools.NewMemoryRecall(cfg.Agent.ID, memStore))
|
reg.Register(toolmemory.NewMemoryRecall(cfg.Agent.ID, memStore))
|
||||||
reg.Register(tools.NewMemoryForget(cfg.Agent.ID, memStore))
|
reg.Register(toolmemory.NewMemoryForget(cfg.Agent.ID, memStore))
|
||||||
reg.Register(tools.NewMemorySummary(cfg.Agent.ID, memStore))
|
reg.Register(toolmemory.NewMemorySummary(cfg.Agent.ID, memStore))
|
||||||
logger.Debug("registered memory tools")
|
logger.Debug("registered memory tools")
|
||||||
}
|
}
|
||||||
|
|
||||||
// Knowledge tools
|
// Knowledge tools
|
||||||
if cfg.Tools.Knowledge.Enabled && kStore != nil {
|
if cfg.Tools.Knowledge.Enabled && kStore != nil {
|
||||||
reg.Register(tools.NewKnowledgeSearch(kStore))
|
reg.Register(toolknowledge.NewKnowledgeSearch(kStore))
|
||||||
reg.Register(tools.NewKnowledgeRead(kStore))
|
reg.Register(toolknowledge.NewKnowledgeRead(kStore))
|
||||||
reg.Register(tools.NewKnowledgeWrite(kStore))
|
reg.Register(toolknowledge.NewKnowledgeWrite(kStore))
|
||||||
reg.Register(tools.NewKnowledgeList(kStore))
|
reg.Register(toolknowledge.NewKnowledgeList(kStore))
|
||||||
logger.Debug("registered knowledge tools")
|
logger.Debug("registered knowledge tools")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,6 +39,11 @@ func Builtins() []Spec {
|
|||||||
Description: "Limpia ventana de conversacion del room actual",
|
Description: "Limpia ventana de conversacion del room actual",
|
||||||
Usage: "!clear",
|
Usage: "!clear",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
Name: "prompts",
|
||||||
|
Description: "Lista prompt-commands disponibles (archivos .md en prompts/)",
|
||||||
|
Usage: "!prompts",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
Name: "version",
|
Name: "version",
|
||||||
Aliases: []string{"v"},
|
Aliases: []string{"v"},
|
||||||
|
|||||||
@@ -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, " ")
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
Hola!
|
||||||
Reference in New Issue
Block a user