From 8d89a762fbe72090511855324aa78067a7f18a1a Mon Sep 17 00:00:00 2001 From: Enmanuel Date: Sat, 7 Mar 2026 17:16:45 +0000 Subject: [PATCH] refactor: mover tools a subpackages individuales MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cada tool ahora vive en su propio subpackage dentro de tools/ (clock, file, http, knowledgetools, matrix, memorytools, ssh, weather) en lugar de archivos planos en el paquete raíz tools/. Esto mejora la organización, permite imports selectivos y reduce acoplamiento entre tools. El paquete tools/ raíz conserva los tipos base (Def, Param, Result, ToolFunc, Tool, Registry). Co-Authored-By: Claude Opus 4.6 --- tools/{time.go => clock/clock.go} | 18 ++-- tools/{ => file}/file.go | 25 ++--- tools/{ => http}/http.go | 55 +++++----- tools/{ => knowledgetools}/knowledge.go | 95 +++++++---------- tools/{ => knowledgetools}/knowledge_test.go | 14 +-- tools/{ => matrix}/matrix.go | 24 +++-- tools/{ => memorytools}/memory.go | 103 ++++++++++--------- tools/{ => ssh}/ssh.go | 29 +++--- tools/tool.go | 20 +++- tools/{ => weather}/weather.go | 24 +++-- 10 files changed, 206 insertions(+), 201 deletions(-) rename tools/{time.go => clock/clock.go} (68%) rename tools/{ => file}/file.go (67%) rename tools/{ => http}/http.go (64%) rename tools/{ => knowledgetools}/knowledge.go (58%) rename tools/{ => knowledgetools}/knowledge_test.go (96%) rename tools/{ => matrix}/matrix.go (58%) rename tools/{ => memorytools}/memory.go (62%) rename tools/{ => ssh}/ssh.go (71%) rename tools/{ => weather}/weather.go (91%) diff --git a/tools/time.go b/tools/clock/clock.go similarity index 68% rename from tools/time.go rename to tools/clock/clock.go index 9994622..85eff29 100644 --- a/tools/time.go +++ b/tools/clock/clock.go @@ -1,31 +1,33 @@ -package tools +package clock import ( "context" "fmt" "time" + + "github.com/enmanuel/agents/tools" ) // NewCurrentTime creates a current_time tool that returns the current date and time. // Useful for agents that need temporal awareness. -func NewCurrentTime() Tool { - return Tool{ - Def: Def{ +func NewCurrentTime() tools.Tool { + return tools.Tool{ + Def: tools.Def{ Name: "current_time", Description: "Returns the current date and time in the server's timezone. Use this when you need to know the current time or date.", - Parameters: []Param{ + Parameters: []tools.Param{ {Name: "format", Type: "string", Description: "Optional Go time format string. Defaults to RFC3339 if empty.", Required: false}, }, }, - Exec: func(ctx context.Context, args map[string]any) Result { - layout := getString(args, "format") + Exec: func(ctx context.Context, args map[string]any) tools.Result { + layout := tools.GetString(args, "format") if layout == "" { layout = time.RFC3339 } now := time.Now() output := fmt.Sprintf("Current time: %s\nTimezone: %s", now.Format(layout), now.Location().String()) - return Result{Output: output} + return tools.Result{Output: output} }, } } diff --git a/tools/file.go b/tools/file/file.go similarity index 67% rename from tools/file.go rename to tools/file/file.go index 96babee..6a38f0d 100644 --- a/tools/file.go +++ b/tools/file/file.go @@ -1,4 +1,4 @@ -package tools +package file import ( "context" @@ -8,37 +8,38 @@ import ( "strings" "github.com/enmanuel/agents/internal/config" + "github.com/enmanuel/agents/tools" ) // NewReadFile creates a read_file tool that reads local files. // Validates paths against cfg.AllowedPaths when non-empty. -func NewReadFile(cfg config.FileOpsCfg) Tool { - return Tool{ - Def: Def{ +func NewReadFile(cfg config.FileOpsCfg) tools.Tool { + return tools.Tool{ + Def: tools.Def{ Name: "read_file", Description: "Read the contents of a local file.", - Parameters: []Param{ + Parameters: []tools.Param{ {Name: "path", Type: "string", Description: "Absolute path to the file to read", Required: true}, }, }, - Exec: func(ctx context.Context, args map[string]any) Result { - path := getString(args, "path") + Exec: func(ctx context.Context, args map[string]any) tools.Result { + path := tools.GetString(args, "path") if path == "" { - return Result{Err: fmt.Errorf("read_file: path is required")} + return tools.Result{Err: fmt.Errorf("read_file: path is required")} } absPath, err := filepath.Abs(path) if err != nil { - return Result{Err: fmt.Errorf("read_file: %w", err)} + return tools.Result{Err: fmt.Errorf("read_file: %w", err)} } if err := validatePath(absPath, cfg.AllowedPaths); err != nil { - return Result{Err: err} + return tools.Result{Err: err} } data, err := os.ReadFile(absPath) if err != nil { - return Result{Err: fmt.Errorf("read_file: %w", err)} + return tools.Result{Err: fmt.Errorf("read_file: %w", err)} } // Limit output to 64 KB @@ -47,7 +48,7 @@ func NewReadFile(cfg config.FileOpsCfg) Tool { content = content[:64*1024] + "\n... (truncated)" } - return Result{Output: content} + return tools.Result{Output: content} }, } } diff --git a/tools/http.go b/tools/http/http.go similarity index 64% rename from tools/http.go rename to tools/http/http.go index 3c13b24..3964cb0 100644 --- a/tools/http.go +++ b/tools/http/http.go @@ -1,4 +1,4 @@ -package tools +package http import ( "context" @@ -10,104 +10,105 @@ import ( "time" "github.com/enmanuel/agents/internal/config" + "github.com/enmanuel/agents/tools" ) // NewHTTPGet creates an http_get tool that performs GET requests. // Validates URLs against cfg.AllowedDomains when non-empty. -func NewHTTPGet(cfg config.HTTPToolCfg) Tool { +func NewHTTPGet(cfg config.HTTPToolCfg) tools.Tool { timeout := cfg.Timeout if timeout == 0 { timeout = 30 * time.Second } client := &http.Client{Timeout: timeout} - return Tool{ - Def: Def{ + return tools.Tool{ + Def: tools.Def{ Name: "http_get", Description: "Perform an HTTP GET request to a URL and return the response body.", - Parameters: []Param{ + Parameters: []tools.Param{ {Name: "url", Type: "string", Description: "The URL to request", Required: true}, }, }, - Exec: func(ctx context.Context, args map[string]any) Result { - rawURL := getString(args, "url") + Exec: func(ctx context.Context, args map[string]any) tools.Result { + rawURL := tools.GetString(args, "url") if rawURL == "" { - return Result{Err: fmt.Errorf("http_get: url is required")} + return tools.Result{Err: fmt.Errorf("http_get: url is required")} } if err := validateDomain(rawURL, cfg.AllowedDomains); err != nil { - return Result{Err: err} + return tools.Result{Err: err} } req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil) if err != nil { - return Result{Err: fmt.Errorf("http_get: %w", err)} + return tools.Result{Err: fmt.Errorf("http_get: %w", err)} } resp, err := client.Do(req) if err != nil { - return Result{Err: fmt.Errorf("http_get: %w", err)} + return tools.Result{Err: fmt.Errorf("http_get: %w", err)} } defer resp.Body.Close() body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) // 64 KB limit if err != nil { - return Result{Err: fmt.Errorf("http_get read body: %w", err)} + return tools.Result{Err: fmt.Errorf("http_get read body: %w", err)} } - return Result{Output: fmt.Sprintf("HTTP %d\n%s", resp.StatusCode, body)} + return tools.Result{Output: fmt.Sprintf("HTTP %d\n%s", resp.StatusCode, body)} }, } } // NewHTTPPost creates an http_post tool that performs POST requests with a JSON body. // Validates URLs against cfg.AllowedDomains when non-empty. -func NewHTTPPost(cfg config.HTTPToolCfg) Tool { +func NewHTTPPost(cfg config.HTTPToolCfg) tools.Tool { timeout := cfg.Timeout if timeout == 0 { timeout = 30 * time.Second } client := &http.Client{Timeout: timeout} - return Tool{ - Def: Def{ + return tools.Tool{ + Def: tools.Def{ Name: "http_post", Description: "Perform an HTTP POST request with a JSON body and return the response.", - Parameters: []Param{ + Parameters: []tools.Param{ {Name: "url", Type: "string", Description: "The URL to request", Required: true}, {Name: "body", Type: "string", Description: "The JSON body to send", Required: true}, }, }, - Exec: func(ctx context.Context, args map[string]any) Result { - rawURL := getString(args, "url") + Exec: func(ctx context.Context, args map[string]any) tools.Result { + rawURL := tools.GetString(args, "url") if rawURL == "" { - return Result{Err: fmt.Errorf("http_post: url is required")} + return tools.Result{Err: fmt.Errorf("http_post: url is required")} } - bodyStr := getString(args, "body") + bodyStr := tools.GetString(args, "body") if bodyStr == "" { - return Result{Err: fmt.Errorf("http_post: body is required")} + return tools.Result{Err: fmt.Errorf("http_post: body is required")} } if err := validateDomain(rawURL, cfg.AllowedDomains); err != nil { - return Result{Err: err} + return tools.Result{Err: err} } req, err := http.NewRequestWithContext(ctx, http.MethodPost, rawURL, strings.NewReader(bodyStr)) if err != nil { - return Result{Err: fmt.Errorf("http_post: %w", err)} + return tools.Result{Err: fmt.Errorf("http_post: %w", err)} } req.Header.Set("Content-Type", "application/json") resp, err := client.Do(req) if err != nil { - return Result{Err: fmt.Errorf("http_post: %w", err)} + return tools.Result{Err: fmt.Errorf("http_post: %w", err)} } defer resp.Body.Close() body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) if err != nil { - return Result{Err: fmt.Errorf("http_post read body: %w", err)} + return tools.Result{Err: fmt.Errorf("http_post read body: %w", err)} } - return Result{Output: fmt.Sprintf("HTTP %d\n%s", resp.StatusCode, body)} + return tools.Result{Output: fmt.Sprintf("HTTP %d\n%s", resp.StatusCode, body)} }, } } diff --git a/tools/knowledge.go b/tools/knowledgetools/knowledge.go similarity index 58% rename from tools/knowledge.go rename to tools/knowledgetools/knowledge.go index 4643e14..4d98bd2 100644 --- a/tools/knowledge.go +++ b/tools/knowledgetools/knowledge.go @@ -1,4 +1,4 @@ -package tools +package knowledgetools import ( "context" @@ -6,6 +6,7 @@ import ( "strings" "github.com/enmanuel/agents/pkg/knowledge" + "github.com/enmanuel/agents/tools" ) // KnowledgeStore is the subset of knowledge.Store needed by knowledge tools. @@ -17,84 +18,84 @@ type KnowledgeStore interface { } // NewKnowledgeSearch creates a tool that searches the knowledge base. -func NewKnowledgeSearch(store KnowledgeStore) Tool { - return Tool{ - Def: Def{ +func NewKnowledgeSearch(store KnowledgeStore) tools.Tool { + return tools.Tool{ + Def: tools.Def{ Name: "knowledge_search", Description: "Search your knowledge base for relevant documents. Returns matching snippets ranked by relevance.", - Parameters: []Param{ + Parameters: []tools.Param{ {Name: "query", Type: "string", Description: "Search terms or phrase", Required: true}, {Name: "limit", Type: "integer", Description: "Max results (default 5)", Required: false}, }, }, - Exec: func(ctx context.Context, args map[string]any) Result { - query := getString(args, "query") + Exec: func(ctx context.Context, args map[string]any) tools.Result { + query := tools.GetString(args, "query") if query == "" { - return Result{Err: fmt.Errorf("knowledge_search: query is required")} + return tools.Result{Err: fmt.Errorf("knowledge_search: query is required")} } - limit := getInt(args, "limit") + limit := tools.GetInt(args, "limit") if limit <= 0 { limit = 5 } results, err := store.Search(ctx, query, limit) if err != nil { - return Result{Err: fmt.Errorf("knowledge_search: %w", err)} + return tools.Result{Err: fmt.Errorf("knowledge_search: %w", err)} } if len(results) == 0 { - return Result{Output: "no documents found matching your query"} + return tools.Result{Output: "no documents found matching your query"} } var sb strings.Builder for i, r := range results { fmt.Fprintf(&sb, "%d. **%s** (`%s`)\n %s\n", i+1, r.Title, r.Slug, r.Snippet) } - return Result{Output: sb.String()} + return tools.Result{Output: sb.String()} }, } } // NewKnowledgeRead creates a tool that reads a knowledge document. -func NewKnowledgeRead(store KnowledgeStore) Tool { - return Tool{ - Def: Def{ +func NewKnowledgeRead(store KnowledgeStore) tools.Tool { + return tools.Tool{ + Def: tools.Def{ Name: "knowledge_read", Description: "Read the full content of a knowledge document by its slug.", - Parameters: []Param{ + Parameters: []tools.Param{ {Name: "slug", Type: "string", Description: "Document slug (e.g. \"go-patterns\")", Required: true}, }, }, - Exec: func(ctx context.Context, args map[string]any) Result { - slug := getString(args, "slug") + Exec: func(ctx context.Context, args map[string]any) tools.Result { + slug := tools.GetString(args, "slug") if slug == "" { - return Result{Err: fmt.Errorf("knowledge_read: slug is required")} + return tools.Result{Err: fmt.Errorf("knowledge_read: slug is required")} } doc, err := store.Get(ctx, slug) if err != nil { - return Result{Err: fmt.Errorf("knowledge_read: %w", err)} + return tools.Result{Err: fmt.Errorf("knowledge_read: %w", err)} } - return Result{Output: doc.Content} + return tools.Result{Output: doc.Content} }, } } // NewKnowledgeWrite creates a tool that writes a knowledge document. -func NewKnowledgeWrite(store KnowledgeStore) Tool { - return Tool{ - Def: Def{ +func NewKnowledgeWrite(store KnowledgeStore) tools.Tool { + return tools.Tool{ + Def: tools.Def{ Name: "knowledge_write", Description: "Create or update a knowledge document. Use this to save new knowledge or improve existing documents.", - Parameters: []Param{ + Parameters: []tools.Param{ {Name: "slug", Type: "string", Description: "Document slug (lowercase, hyphens, e.g. \"matrix-tips\")", Required: true}, {Name: "content", Type: "string", Description: "Full markdown content of the document", Required: true}, }, }, - Exec: func(ctx context.Context, args map[string]any) Result { - slug := getString(args, "slug") - content := getString(args, "content") + Exec: func(ctx context.Context, args map[string]any) tools.Result { + slug := tools.GetString(args, "slug") + content := tools.GetString(args, "content") if slug == "" || content == "" { - return Result{Err: fmt.Errorf("knowledge_write: slug and content are required")} + return tools.Result{Err: fmt.Errorf("knowledge_write: slug and content are required")} } err := store.Put(ctx, knowledge.Document{ @@ -102,28 +103,28 @@ func NewKnowledgeWrite(store KnowledgeStore) Tool { Content: content, }) if err != nil { - return Result{Err: fmt.Errorf("knowledge_write: %w", err)} + return tools.Result{Err: fmt.Errorf("knowledge_write: %w", err)} } - return Result{Output: fmt.Sprintf("document saved: %s (%d bytes)", slug, len(content))} + return tools.Result{Output: fmt.Sprintf("document saved: %s (%d bytes)", slug, len(content))} }, } } // NewKnowledgeList creates a tool that lists all knowledge documents. -func NewKnowledgeList(store KnowledgeStore) Tool { - return Tool{ - Def: Def{ +func NewKnowledgeList(store KnowledgeStore) tools.Tool { + return tools.Tool{ + Def: tools.Def{ Name: "knowledge_list", Description: "List all documents in your knowledge base with their titles.", - Parameters: []Param{}, + Parameters: []tools.Param{}, }, - Exec: func(ctx context.Context, args map[string]any) Result { + Exec: func(ctx context.Context, args map[string]any) tools.Result { docs, err := store.List(ctx) if err != nil { - return Result{Err: fmt.Errorf("knowledge_list: %w", err)} + return tools.Result{Err: fmt.Errorf("knowledge_list: %w", err)} } if len(docs) == 0 { - return Result{Output: "knowledge base is empty"} + return tools.Result{Output: "knowledge base is empty"} } var sb strings.Builder @@ -131,23 +132,7 @@ func NewKnowledgeList(store KnowledgeStore) Tool { fmt.Fprintf(&sb, "- `%s`: %s (updated %s)\n", d.Slug, d.Title, d.UpdatedAt.Format("2006-01-02")) } - return Result{Output: sb.String()} + return tools.Result{Output: sb.String()} }, } } - -// getInt extracts an integer argument by name, returning 0 if missing or wrong type. -func getInt(args map[string]any, key string) int { - v, ok := args[key] - if !ok { - return 0 - } - switch n := v.(type) { - case float64: - return int(n) - case int: - return n - default: - return 0 - } -} diff --git a/tools/knowledge_test.go b/tools/knowledgetools/knowledge_test.go similarity index 96% rename from tools/knowledge_test.go rename to tools/knowledgetools/knowledge_test.go index 94b553f..050d0c2 100644 --- a/tools/knowledge_test.go +++ b/tools/knowledgetools/knowledge_test.go @@ -1,10 +1,11 @@ -package tools +package knowledgetools import ( "context" "testing" "github.com/enmanuel/agents/pkg/knowledge" + "github.com/enmanuel/agents/tools" ) // mockKnowledgeStore implements KnowledgeStore for testing. @@ -173,16 +174,9 @@ func TestGetInt(t *testing.T) { {map[string]any{}, "n", 0}, } for _, tt := range tests { - got := getInt(tt.args, tt.key) + got := tools.GetInt(tt.args, tt.key) if got != tt.want { - t.Errorf("getInt(%v, %q) = %d, want %d", tt.args, tt.key, got, tt.want) + t.Errorf("GetInt(%v, %q) = %d, want %d", tt.args, tt.key, got, tt.want) } } } - -func min(a, b int) int { - if a < b { - return a - } - return b -} diff --git a/tools/matrix.go b/tools/matrix/matrix.go similarity index 58% rename from tools/matrix.go rename to tools/matrix/matrix.go index 3c92f69..7a15f70 100644 --- a/tools/matrix.go +++ b/tools/matrix/matrix.go @@ -1,8 +1,10 @@ -package tools +package matrix import ( "context" "fmt" + + "github.com/enmanuel/agents/tools" ) // MatrixSender is the interface for sending Matrix messages. @@ -13,28 +15,28 @@ type MatrixSender interface { } // NewMatrixSend creates a matrix_send tool that sends a message to a Matrix room. -func NewMatrixSend(sender MatrixSender) Tool { - return Tool{ - Def: Def{ +func NewMatrixSend(sender MatrixSender) tools.Tool { + return tools.Tool{ + Def: tools.Def{ Name: "matrix_send", Description: "Send a text message to a Matrix room.", - Parameters: []Param{ + Parameters: []tools.Param{ {Name: "room_id", Type: "string", Description: "The Matrix room ID to send to", Required: true}, {Name: "message", Type: "string", Description: "The text message to send", Required: true}, }, }, - Exec: func(ctx context.Context, args map[string]any) Result { - roomID := getString(args, "room_id") - message := getString(args, "message") + Exec: func(ctx context.Context, args map[string]any) tools.Result { + roomID := tools.GetString(args, "room_id") + message := tools.GetString(args, "message") if roomID == "" || message == "" { - return Result{Err: fmt.Errorf("matrix_send: room_id and message are required")} + return tools.Result{Err: fmt.Errorf("matrix_send: room_id and message are required")} } if err := sender.SendMarkdown(ctx, roomID, message); err != nil { - return Result{Err: fmt.Errorf("matrix_send: %w", err)} + return tools.Result{Err: fmt.Errorf("matrix_send: %w", err)} } - return Result{Output: fmt.Sprintf("message sent to %s", roomID)} + return tools.Result{Output: fmt.Sprintf("message sent to %s", roomID)} }, } } diff --git a/tools/memory.go b/tools/memorytools/memory.go similarity index 62% rename from tools/memory.go rename to tools/memorytools/memory.go index f913203..d3ac614 100644 --- a/tools/memory.go +++ b/tools/memorytools/memory.go @@ -1,4 +1,4 @@ -package tools +package memorytools import ( "context" @@ -8,6 +8,7 @@ import ( "time" "github.com/enmanuel/agents/pkg/memory" + "github.com/enmanuel/agents/tools" ) // MemoryStore is the subset of memory.Store needed by memory tools. @@ -44,23 +45,23 @@ func (rc *RoomContext) Get() string { } // NewMemorySave creates a tool that saves a fact to long-term memory. -func NewMemorySave(agentID string, store MemoryStore) Tool { - return Tool{ - Def: Def{ +func NewMemorySave(agentID string, store MemoryStore) tools.Tool { + return tools.Tool{ + Def: tools.Def{ Name: "memory_save", Description: "Save a fact to long-term memory. Use this to remember important information about users, topics, or preferences.", - Parameters: []Param{ + Parameters: []tools.Param{ {Name: "subject", Type: "string", Description: "The subject this fact is about (e.g. a username, a topic)", Required: true}, {Name: "key", Type: "string", Description: "The fact key (e.g. 'favorite_language', 'timezone')", Required: true}, {Name: "value", Type: "string", Description: "The fact value to store", Required: true}, }, }, - Exec: func(ctx context.Context, args map[string]any) Result { - subject := getString(args, "subject") - key := getString(args, "key") - value := getString(args, "value") + Exec: func(ctx context.Context, args map[string]any) tools.Result { + subject := tools.GetString(args, "subject") + key := tools.GetString(args, "key") + value := tools.GetString(args, "value") if subject == "" || key == "" || value == "" { - return Result{Err: fmt.Errorf("memory_save: subject, key, and value are required")} + return tools.Result{Err: fmt.Errorf("memory_save: subject, key, and value are required")} } err := store.SaveFact(ctx, memory.Fact{ AgentID: agentID, @@ -69,119 +70,119 @@ func NewMemorySave(agentID string, store MemoryStore) Tool { Value: value, }) if err != nil { - return Result{Err: fmt.Errorf("memory_save: %w", err)} + return tools.Result{Err: fmt.Errorf("memory_save: %w", err)} } - return Result{Output: fmt.Sprintf("saved: %s.%s = %s", subject, key, value)} + return tools.Result{Output: fmt.Sprintf("saved: %s.%s = %s", subject, key, value)} }, } } // NewMemoryRecall creates a tool that retrieves facts from long-term memory. -func NewMemoryRecall(agentID string, store MemoryStore) Tool { - return Tool{ - Def: Def{ +func NewMemoryRecall(agentID string, store MemoryStore) tools.Tool { + return tools.Tool{ + Def: tools.Def{ Name: "memory_recall", Description: "Recall facts from long-term memory about a subject. Omit key to get all facts for the subject.", - Parameters: []Param{ + Parameters: []tools.Param{ {Name: "subject", Type: "string", Description: "The subject to recall facts about", Required: true}, {Name: "key", Type: "string", Description: "Optional specific fact key to recall", Required: false}, }, }, - Exec: func(ctx context.Context, args map[string]any) Result { - subject := getString(args, "subject") + Exec: func(ctx context.Context, args map[string]any) tools.Result { + subject := tools.GetString(args, "subject") if subject == "" { - return Result{Err: fmt.Errorf("memory_recall: subject is required")} + return tools.Result{Err: fmt.Errorf("memory_recall: subject is required")} } var keyPtr *string - if k := getString(args, "key"); k != "" { + if k := tools.GetString(args, "key"); k != "" { keyPtr = &k } facts, err := store.RecallFacts(ctx, agentID, subject, keyPtr) if err != nil { - return Result{Err: fmt.Errorf("memory_recall: %w", err)} + return tools.Result{Err: fmt.Errorf("memory_recall: %w", err)} } if len(facts) == 0 { - return Result{Output: fmt.Sprintf("no facts found for subject %q", subject)} + return tools.Result{Output: fmt.Sprintf("no facts found for subject %q", subject)} } var sb strings.Builder for _, f := range facts { fmt.Fprintf(&sb, "%s.%s = %s\n", f.Subject, f.Key, f.Value) } - return Result{Output: sb.String()} + return tools.Result{Output: sb.String()} }, } } // NewMemoryForget creates a tool that deletes facts from long-term memory. -func NewMemoryForget(agentID string, store MemoryStore) Tool { - return Tool{ - Def: Def{ +func NewMemoryForget(agentID string, store MemoryStore) tools.Tool { + return tools.Tool{ + Def: tools.Def{ Name: "memory_forget", Description: "Delete facts from long-term memory. Omit key to delete all facts for the subject.", - Parameters: []Param{ + Parameters: []tools.Param{ {Name: "subject", Type: "string", Description: "The subject whose facts to delete", Required: true}, {Name: "key", Type: "string", Description: "Optional specific fact key to delete; omit to delete all", Required: false}, }, }, - Exec: func(ctx context.Context, args map[string]any) Result { - subject := getString(args, "subject") + Exec: func(ctx context.Context, args map[string]any) tools.Result { + subject := tools.GetString(args, "subject") if subject == "" { - return Result{Err: fmt.Errorf("memory_forget: subject is required")} + return tools.Result{Err: fmt.Errorf("memory_forget: subject is required")} } var keyPtr *string - if k := getString(args, "key"); k != "" { + if k := tools.GetString(args, "key"); k != "" { keyPtr = &k } err := store.DeleteFacts(ctx, agentID, subject, keyPtr) if err != nil { - return Result{Err: fmt.Errorf("memory_forget: %w", err)} + return tools.Result{Err: fmt.Errorf("memory_forget: %w", err)} } if keyPtr != nil { - return Result{Output: fmt.Sprintf("forgot %s.%s", subject, *keyPtr)} + return tools.Result{Output: fmt.Sprintf("forgot %s.%s", subject, *keyPtr)} } - return Result{Output: fmt.Sprintf("forgot all facts about %s", subject)} + return tools.Result{Output: fmt.Sprintf("forgot all facts about %s", subject)} }, } } // NewMemoryClearContext creates a tool that clears the conversation window. -func NewMemoryClearContext(clearer WindowClearer, roomCtx *RoomContext) Tool { - return Tool{ - Def: Def{ +func NewMemoryClearContext(clearer WindowClearer, roomCtx *RoomContext) tools.Tool { + return tools.Tool{ + Def: tools.Def{ Name: "memory_clear_context", Description: "Clear the conversation context window. Useful to start fresh. Omit room_id to clear the current room.", - Parameters: []Param{ + Parameters: []tools.Param{ {Name: "room_id", Type: "string", Description: "Optional room ID to clear; defaults to current room", Required: false}, }, }, - Exec: func(ctx context.Context, args map[string]any) Result { - roomID := getString(args, "room_id") + Exec: func(ctx context.Context, args map[string]any) tools.Result { + roomID := tools.GetString(args, "room_id") if roomID == "" { roomID = roomCtx.Get() } if roomID == "" { - return Result{Err: fmt.Errorf("memory_clear_context: no room_id provided and no current room")} + return tools.Result{Err: fmt.Errorf("memory_clear_context: no room_id provided and no current room")} } clearer.ClearWindow(roomID) - return Result{Output: fmt.Sprintf("conversation context cleared for room %s", roomID)} + return tools.Result{Output: fmt.Sprintf("conversation context cleared for room %s", roomID)} }, } } // NewMemorySummary creates a tool that saves an important summary to long-term memory. -func NewMemorySummary(agentID string, store MemoryStore) Tool { - return Tool{ - Def: Def{ +func NewMemorySummary(agentID string, store MemoryStore) tools.Tool { + return tools.Tool{ + Def: tools.Def{ Name: "memory_summary", Description: "Save an important summary or takeaway from the current conversation to long-term memory.", - Parameters: []Param{ + Parameters: []tools.Param{ {Name: "text", Type: "string", Description: "The summary text to save", Required: true}, }, }, - Exec: func(ctx context.Context, args map[string]any) Result { - text := getString(args, "text") + Exec: func(ctx context.Context, args map[string]any) tools.Result { + text := tools.GetString(args, "text") if text == "" { - return Result{Err: fmt.Errorf("memory_summary: text is required")} + return tools.Result{Err: fmt.Errorf("memory_summary: text is required")} } key := time.Now().UTC().Format("2006-01-02T15:04:05") err := store.SaveFact(ctx, memory.Fact{ @@ -191,9 +192,9 @@ func NewMemorySummary(agentID string, store MemoryStore) Tool { Value: text, }) if err != nil { - return Result{Err: fmt.Errorf("memory_summary: %w", err)} + return tools.Result{Err: fmt.Errorf("memory_summary: %w", err)} } - return Result{Output: "summary saved"} + return tools.Result{Output: "summary saved"} }, } } diff --git a/tools/ssh.go b/tools/ssh/ssh.go similarity index 71% rename from tools/ssh.go rename to tools/ssh/ssh.go index 57bf3fb..77b7769 100644 --- a/tools/ssh.go +++ b/tools/ssh/ssh.go @@ -1,4 +1,4 @@ -package tools +package ssh import ( "context" @@ -7,33 +7,34 @@ import ( "github.com/enmanuel/agents/internal/config" corespecs "github.com/enmanuel/agents/pkg/tools" - "github.com/enmanuel/agents/shell/ssh" + shellssh "github.com/enmanuel/agents/shell/ssh" + "github.com/enmanuel/agents/tools" ) // NewSSHCommand creates an ssh_command tool that executes remote commands via SSH. // Validates targets against cfg.AllowedTargets and commands against cfg.ForbiddenCommands. -func NewSSHCommand(cfg config.SSHToolCfg, exec *ssh.Executor) Tool { - return Tool{ - Def: Def{ +func NewSSHCommand(cfg config.SSHToolCfg, exec *shellssh.Executor) tools.Tool { + return tools.Tool{ + Def: tools.Def{ Name: "ssh_command", Description: "Execute a command on a remote server via SSH.", - Parameters: []Param{ + Parameters: []tools.Param{ {Name: "target", Type: "string", Description: "The SSH target name (e.g. production, staging)", Required: true}, {Name: "command", Type: "string", Description: "The shell command to execute", Required: true}, }, }, - Exec: func(ctx context.Context, args map[string]any) Result { - target := getString(args, "target") - command := getString(args, "command") + Exec: func(ctx context.Context, args map[string]any) tools.Result { + target := tools.GetString(args, "target") + command := tools.GetString(args, "command") if target == "" || command == "" { - return Result{Err: fmt.Errorf("ssh_command: target and command are required")} + return tools.Result{Err: fmt.Errorf("ssh_command: target and command are required")} } if err := validateTarget(target, cfg.AllowedTargets); err != nil { - return Result{Err: err} + return tools.Result{Err: err} } if err := validateCommand(command, cfg.ForbiddenCommands); err != nil { - return Result{Err: err} + return tools.Result{Err: err} } timeout := "30s" @@ -48,14 +49,14 @@ func NewSSHCommand(cfg config.SSHToolCfg, exec *ssh.Executor) Tool { }) if res.Err != nil { - return Result{Err: fmt.Errorf("ssh_command: %w", res.Err)} + return tools.Result{Err: fmt.Errorf("ssh_command: %w", res.Err)} } output := res.Stdout if res.Stderr != "" { output += "\nstderr: " + res.Stderr } - return Result{Output: output} + return tools.Result{Output: output} }, } } diff --git a/tools/tool.go b/tools/tool.go index c614e3e..ff6afa8 100644 --- a/tools/tool.go +++ b/tools/tool.go @@ -35,8 +35,8 @@ type Tool struct { Exec ToolFunc } -// getString extracts a string argument by name, returning "" if missing or wrong type. -func getString(args map[string]any, key string) string { +// GetString extracts a string argument by name, returning "" if missing or wrong type. +func GetString(args map[string]any, key string) string { v, ok := args[key] if !ok { return "" @@ -47,3 +47,19 @@ func getString(args map[string]any, key string) string { } return s } + +// GetInt extracts an integer argument by name, returning 0 if missing or wrong type. +func GetInt(args map[string]any, key string) int { + v, ok := args[key] + if !ok { + return 0 + } + switch n := v.(type) { + case float64: + return int(n) + case int: + return n + default: + return 0 + } +} diff --git a/tools/weather.go b/tools/weather/weather.go similarity index 91% rename from tools/weather.go rename to tools/weather/weather.go index 6360fb6..dd9c901 100644 --- a/tools/weather.go +++ b/tools/weather/weather.go @@ -1,4 +1,4 @@ -package tools +package weather import ( "context" @@ -9,42 +9,44 @@ import ( "net/url" "strings" "time" + + "github.com/enmanuel/agents/tools" ) // NewWeather creates a get_weather tool that fetches current weather and forecast // for a city using the Open-Meteo API (free, no API key required). -func NewWeather() Tool { +func NewWeather() tools.Tool { client := &http.Client{Timeout: 15 * time.Second} - return Tool{ - Def: Def{ + return tools.Tool{ + Def: tools.Def{ Name: "get_weather", Description: "Get current weather conditions and 3-day forecast for a city. Returns temperature, humidity, wind speed, and weather description.", - Parameters: []Param{ + Parameters: []tools.Param{ {Name: "city", Type: "string", Description: "City name to look up (e.g. 'Madrid', 'New York', 'Tokyo')", Required: true}, }, }, - Exec: func(ctx context.Context, args map[string]any) Result { - city := getString(args, "city") + Exec: func(ctx context.Context, args map[string]any) tools.Result { + city := tools.GetString(args, "city") if city == "" { - return Result{Err: fmt.Errorf("get_weather: city is required")} + return tools.Result{Err: fmt.Errorf("get_weather: city is required")} } // Step 1: Geocode city name to coordinates lat, lon, resolvedName, country, err := geocodeCity(ctx, client, city) if err != nil { - return Result{Err: fmt.Errorf("get_weather: geocoding failed: %w", err)} + return tools.Result{Err: fmt.Errorf("get_weather: geocoding failed: %w", err)} } // Step 2: Fetch weather data weather, err := fetchWeather(ctx, client, lat, lon) if err != nil { - return Result{Err: fmt.Errorf("get_weather: forecast failed: %w", err)} + return tools.Result{Err: fmt.Errorf("get_weather: forecast failed: %w", err)} } // Step 3: Format output output := formatWeather(resolvedName, country, weather) - return Result{Output: output} + return tools.Result{Output: output} }, } }