refactor: mover tools a subpackages individuales
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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)}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
@@ -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)}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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"}
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -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}
|
||||
},
|
||||
}
|
||||
}
|
||||
+18
-2
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}
|
||||
},
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user