Files
agents_and_robots/devagents/registry_build.go
T
egutierrez f2718aa17c feat: añadir tools wikipedia_search y exchange_rate
- tools/wikipedia/wikipedia.go: tool wikipedia_search que consulta la
  API pública de Wikipedia (sin auth). Devuelve resumen del artículo.
- tools/exchange/exchange.go: 4 tools de tipo de cambio usando
  exchangerate-api.com: exchange_rate_get, exchange_rate_convert,
  exchange_rate_list, exchange_rate_historical.
- internal/config/schema.go: añadir ExchangeRateToolCfg con Enabled,
  APIKey, APIKeyEnv y Timeout.
- devagents/registry_build.go: registrar ambas tool families.
  wikipedia_search siempre disponible; exchange rate tools requieren
  APIKey configurado (deny-by-default con WARN si falta).
- devagents/registry_build_test.go: actualizar test de registry build.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-11 00:25:10 +00:00

308 lines
10 KiB
Go

package devagents
import (
"context"
"log/slog"
"os"
"path/filepath"
"time"
"github.com/enmanuel/agents/internal/config"
"github.com/enmanuel/agents/pkg/memory"
shellknowledge "github.com/enmanuel/agents/shell/knowledge"
shellmcp "github.com/enmanuel/agents/shell/mcp"
shellskills "github.com/enmanuel/agents/shell/skills"
"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"
toolimdb "github.com/enmanuel/agents/tools/imdb"
toolknowledge "github.com/enmanuel/agents/tools/knowledgetools"
toolmatrix "github.com/enmanuel/agents/tools/matrix"
toolmcp "github.com/enmanuel/agents/tools/mcptools"
toolmemory "github.com/enmanuel/agents/tools/memorytools"
toolskills "github.com/enmanuel/agents/tools/skilltools"
toolssh "github.com/enmanuel/agents/tools/ssh"
toolweather "github.com/enmanuel/agents/tools/weather"
toolwikipedia "github.com/enmanuel/agents/tools/wikipedia"
toolexchange "github.com/enmanuel/agents/tools/exchange"
"github.com/enmanuel/agents/shell/matrix"
)
// toolDeps holds external subsystem instances needed by the tool registry.
type toolDeps struct {
kStore *shellknowledge.FileStore
sharedKStore *shellknowledge.FileStore
mcpManager *shellmcp.Manager
skillLoader *shellskills.Loader
skillExecutor *shellskills.Executor
}
// initToolDeps initializes knowledge stores, MCP manager, and skills loader
// based on the agent config. All results are optional (nil when disabled).
func initToolDeps(cfg *config.AgentConfig, dataBase string, logger *slog.Logger) toolDeps {
var deps toolDeps
// Knowledge store
if cfg.Tools.Knowledge.Enabled {
knowledgeDir := cfg.Tools.Knowledge.Dir
if knowledgeDir == "" {
knowledgeDir = filepath.Join("agents", cfg.Agent.ID, "knowledge")
}
knowledgeDBPath := filepath.Join(dataBase, "knowledge.db")
kStore, kErr := shellknowledge.New(knowledgeDir, knowledgeDBPath, logger)
if kErr != nil {
logger.Error("knowledge_store_init_failed", "err", kErr)
} else {
if syncErr := kStore.Sync(context.Background()); syncErr != nil {
logger.Error("knowledge_sync_failed", "err", syncErr)
}
deps.kStore = kStore
}
}
// Shared knowledge store
if cfg.Tools.SharedKnowledge.Enabled {
sharedDir := cfg.Tools.SharedKnowledge.Dir
if sharedDir == "" {
sharedDir = "knowledges"
}
sharedDBPath := cfg.Tools.SharedKnowledge.DBPath
if sharedDBPath == "" {
sharedDBPath = "knowledges/data/knowledge.db"
}
sharedKStore, skErr := shellknowledge.New(sharedDir, sharedDBPath, logger)
if skErr != nil {
logger.Error("shared_knowledge_store_init_failed", "err", skErr)
} else {
if syncErr := sharedKStore.Sync(context.Background()); syncErr != nil {
logger.Error("shared_knowledge_sync_failed", "err", syncErr)
}
logger.Info("shared knowledge enabled", "dir", sharedDir, "db", sharedDBPath)
deps.sharedKStore = sharedKStore
}
}
// MCP client manager — connects to external MCP servers
if cfg.Tools.MCP.Enabled && len(cfg.Tools.MCP.Servers) > 0 {
mcpManager, mcpErr := shellmcp.NewManager(context.Background(), cfg.Tools.MCP.Servers, logger)
if mcpErr != nil {
logger.Error("mcp_manager_init_failed", "err", mcpErr)
} else {
logger.Info("mcp manager initialized", "servers", len(cfg.Tools.MCP.Servers))
deps.mcpManager = mcpManager
}
}
// Skills loader
if cfg.Skills.Enabled {
skillsPath := cfg.Skills.SkillsPath
if skillsPath == "" {
skillsPath = "skills/"
}
deps.skillLoader = shellskills.NewLoader(skillsPath)
// Skills executor for scripts
allowedInterpreters := cfg.Tools.Skills.AllowedInterpreters
timeout := cfg.Skills.Timeout
if timeout == 0 {
timeout = 60 * time.Second
}
deps.skillExecutor = shellskills.NewExecutor(allowedInterpreters, timeout)
logger.Info("skills enabled", "path", skillsPath, "categories", cfg.Skills.Categories)
}
return deps
}
// initRateLimiter configures the rate limiter on the tool registry if enabled.
func initRateLimiter(cfg *config.AgentConfig, toolReg *tools.Registry, logger *slog.Logger) {
if !cfg.Security.ToolRateLimit.Enabled {
return
}
maxCalls := cfg.Security.ToolRateLimit.MaxCallsPerMin
if maxCalls <= 0 {
maxCalls = 10
}
rl := tools.NewRateLimiter(maxCalls, time.Minute)
toolReg.SetRateLimiter(rl)
cleanupInterval := cfg.Security.ToolRateLimit.CleanupIntervalS
if cleanupInterval <= 0 {
cleanupInterval = 60
}
go func() {
ticker := time.NewTicker(time.Duration(cleanupInterval) * time.Second)
defer ticker.Stop()
for range ticker.C {
rl.Cleanup()
}
}()
logger.Info("tool rate limiting enabled", "max_calls_per_min", maxCalls)
}
// buildToolRegistry creates a Registry with tools enabled in the agent's config.
func buildToolRegistry(
cfg *config.AgentConfig,
sshExec *ssh.Executor,
matrixClient *matrix.Client,
memStore memory.Store,
kStore *shellknowledge.FileStore,
sharedKStore *shellknowledge.FileStore,
mcpManager *shellmcp.Manager,
skillLoader *shellskills.Loader,
skillExecutor *shellskills.Executor,
roomCtx *toolmemory.RoomContext,
logger *slog.Logger,
) *tools.Registry {
reg := tools.NewRegistry(logger)
if cfg.Tools.HTTP.Enabled {
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(toolssh.NewSSHCommand(cfg.Tools.SSH, sshExec))
logger.Debug("registered ssh tool")
}
if cfg.Tools.FileOps.Enabled {
reg.Register(toolfile.NewReadFile(cfg.Tools.FileOps))
reg.Register(toolfile.NewListDirectory(cfg.Tools.FileOps))
if !cfg.Tools.FileOps.ReadOnly {
reg.Register(toolfile.NewWriteFile(cfg.Tools.FileOps))
reg.Register(toolfile.NewAppendFile(cfg.Tools.FileOps))
reg.Register(toolfile.NewDeleteFile(cfg.Tools.FileOps))
}
logger.Debug("registered file tools")
}
// current_time is always available
reg.Register(toolclock.NewCurrentTime())
logger.Debug("registered current_time tool")
// weather tool is always available
reg.Register(toolweather.NewWeather())
logger.Debug("registered weather tool")
// wikipedia_search tool is always available
reg.Register(toolwikipedia.NewWikipediaSearch())
logger.Debug("registered wikipedia_search tool")
// imdb tool (enabled via config)
if cfg.Tools.IMDb.Enabled {
reg.Register(toolimdb.NewIMDbSearch(cfg.Tools.IMDb))
logger.Debug("registered imdb tool")
}
// exchange rate tools (enabled via config)
if cfg.Tools.ExchangeRate.Enabled {
if t, err := toolexchange.NewExchangeRateGet(cfg.Tools.ExchangeRate); err != nil {
logger.Warn("exchange_rate_get disabled: API key not configured", "err", err)
} else {
reg.Register(t)
}
if t, err := toolexchange.NewExchangeRateConvert(cfg.Tools.ExchangeRate); err != nil {
logger.Warn("exchange_rate_convert disabled: API key not configured", "err", err)
} else {
reg.Register(t)
}
if t, err := toolexchange.NewExchangeRateList(cfg.Tools.ExchangeRate); err != nil {
logger.Warn("exchange_rate_list disabled: API key not configured", "err", err)
} else {
reg.Register(t)
}
if t, err := toolexchange.NewExchangeRateHistorical(cfg.Tools.ExchangeRate); err != nil {
logger.Warn("exchange_rate_historical disabled: API key not configured", "err", err)
} else {
reg.Register(t)
}
logger.Debug("registered exchange rate tools")
}
// matrix_send is always available
reg.Register(toolmatrix.NewMatrixSend(matrixClient, cfg.Tools.Matrix))
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(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(toolknowledge.NewKnowledgeSearch(kStore))
reg.Register(toolknowledge.NewKnowledgeRead(kStore))
reg.Register(toolknowledge.NewKnowledgeWrite(kStore))
reg.Register(toolknowledge.NewKnowledgeList(kStore))
logger.Debug("registered knowledge tools")
}
// Shared knowledge tools
if cfg.Tools.SharedKnowledge.Enabled && sharedKStore != nil {
sharedTools := toolknowledge.NewSharedKnowledgeTools(sharedKStore)
for _, tool := range sharedTools {
reg.Register(tool)
}
logger.Debug("registered shared knowledge tools", "count", len(sharedTools))
}
// MCP tools — register tools from all connected MCP servers
if mcpManager != nil {
for serverName, mcpClient := range mcpManager.AllClients() {
// Find the config for this server to get prefix, filter, timeout
var serverCfg *config.MCPServerCfg
for i := range cfg.Tools.MCP.Servers {
if cfg.Tools.MCP.Servers[i].Name == serverName {
serverCfg = &cfg.Tools.MCP.Servers[i]
break
}
}
if serverCfg == nil {
logger.Warn("no config found for MCP server", "name", serverName)
continue
}
// Convert and register MCP tools
mcpTools := toolmcp.FromMCPServer(mcpClient, serverCfg.Prefix, serverCfg.Tools, serverCfg.Timeout, logger)
for _, tool := range mcpTools {
reg.Register(tool)
}
logger.Debug("registered MCP tools", "server", serverName, "count", len(mcpTools))
}
}
// Skills tools — register skill search, load, read, and run tools
if skillLoader != nil {
reg.Register(toolskills.NewSkillSearch(skillLoader, cfg.Skills.Categories))
reg.Register(toolskills.NewSkillLoad(skillLoader))
reg.Register(toolskills.NewSkillReadResource(skillLoader))
if skillExecutor != nil {
reg.Register(toolskills.NewSkillRunScript(skillLoader, skillExecutor))
}
logger.Debug("registered skills tools")
}
return reg
}
// resolveDataBase returns the base directory for agent runtime data.
// Priority: config storage.base_path > $AGENTS_DATA_DIR/<id> > agents/<id>/data
func resolveDataBase(cfg *config.AgentConfig) string {
if cfg.Storage.BasePath != "" {
return cfg.Storage.BasePath
}
if envDir := os.Getenv("AGENTS_DATA_DIR"); envDir != "" {
return filepath.Join(envDir, cfg.Agent.ID)
}
return filepath.Join("agents", cfg.Agent.ID, "data")
}