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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/enmanuel/agents/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewCurrentTime creates a current_time tool that returns the current date and time.
|
// NewCurrentTime creates a current_time tool that returns the current date and time.
|
||||||
// Useful for agents that need temporal awareness.
|
// Useful for agents that need temporal awareness.
|
||||||
func NewCurrentTime() Tool {
|
func NewCurrentTime() tools.Tool {
|
||||||
return Tool{
|
return tools.Tool{
|
||||||
Def: Def{
|
Def: tools.Def{
|
||||||
Name: "current_time",
|
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.",
|
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},
|
{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 {
|
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||||
layout := getString(args, "format")
|
layout := tools.GetString(args, "format")
|
||||||
if layout == "" {
|
if layout == "" {
|
||||||
layout = time.RFC3339
|
layout = time.RFC3339
|
||||||
}
|
}
|
||||||
|
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
output := fmt.Sprintf("Current time: %s\nTimezone: %s", now.Format(layout), now.Location().String())
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -8,37 +8,38 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/enmanuel/agents/internal/config"
|
"github.com/enmanuel/agents/internal/config"
|
||||||
|
"github.com/enmanuel/agents/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewReadFile creates a read_file tool that reads local files.
|
// NewReadFile creates a read_file tool that reads local files.
|
||||||
// Validates paths against cfg.AllowedPaths when non-empty.
|
// Validates paths against cfg.AllowedPaths when non-empty.
|
||||||
func NewReadFile(cfg config.FileOpsCfg) Tool {
|
func NewReadFile(cfg config.FileOpsCfg) tools.Tool {
|
||||||
return Tool{
|
return tools.Tool{
|
||||||
Def: Def{
|
Def: tools.Def{
|
||||||
Name: "read_file",
|
Name: "read_file",
|
||||||
Description: "Read the contents of a local 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},
|
{Name: "path", Type: "string", Description: "Absolute path to the file to read", Required: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Exec: func(ctx context.Context, args map[string]any) Result {
|
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||||
path := getString(args, "path")
|
path := tools.GetString(args, "path")
|
||||||
if 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)
|
absPath, err := filepath.Abs(path)
|
||||||
if err != nil {
|
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 {
|
if err := validatePath(absPath, cfg.AllowedPaths); err != nil {
|
||||||
return Result{Err: err}
|
return tools.Result{Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
data, err := os.ReadFile(absPath)
|
data, err := os.ReadFile(absPath)
|
||||||
if err != nil {
|
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
|
// Limit output to 64 KB
|
||||||
@@ -47,7 +48,7 @@ func NewReadFile(cfg config.FileOpsCfg) Tool {
|
|||||||
content = content[:64*1024] + "\n... (truncated)"
|
content = content[:64*1024] + "\n... (truncated)"
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result{Output: content}
|
return tools.Result{Output: content}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package tools
|
package http
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -10,104 +10,105 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/enmanuel/agents/internal/config"
|
"github.com/enmanuel/agents/internal/config"
|
||||||
|
"github.com/enmanuel/agents/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewHTTPGet creates an http_get tool that performs GET requests.
|
// NewHTTPGet creates an http_get tool that performs GET requests.
|
||||||
// Validates URLs against cfg.AllowedDomains when non-empty.
|
// Validates URLs against cfg.AllowedDomains when non-empty.
|
||||||
func NewHTTPGet(cfg config.HTTPToolCfg) Tool {
|
func NewHTTPGet(cfg config.HTTPToolCfg) tools.Tool {
|
||||||
timeout := cfg.Timeout
|
timeout := cfg.Timeout
|
||||||
if timeout == 0 {
|
if timeout == 0 {
|
||||||
timeout = 30 * time.Second
|
timeout = 30 * time.Second
|
||||||
}
|
}
|
||||||
client := &http.Client{Timeout: timeout}
|
client := &http.Client{Timeout: timeout}
|
||||||
|
|
||||||
return Tool{
|
return tools.Tool{
|
||||||
Def: Def{
|
Def: tools.Def{
|
||||||
Name: "http_get",
|
Name: "http_get",
|
||||||
Description: "Perform an HTTP GET request to a URL and return the response body.",
|
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},
|
{Name: "url", Type: "string", Description: "The URL to request", Required: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Exec: func(ctx context.Context, args map[string]any) Result {
|
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||||
rawURL := getString(args, "url")
|
rawURL := tools.GetString(args, "url")
|
||||||
if rawURL == "" {
|
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 {
|
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)
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, rawURL, nil)
|
||||||
if err != 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)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
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()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) // 64 KB limit
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024)) // 64 KB limit
|
||||||
if err != nil {
|
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.
|
// NewHTTPPost creates an http_post tool that performs POST requests with a JSON body.
|
||||||
// Validates URLs against cfg.AllowedDomains when non-empty.
|
// Validates URLs against cfg.AllowedDomains when non-empty.
|
||||||
func NewHTTPPost(cfg config.HTTPToolCfg) Tool {
|
func NewHTTPPost(cfg config.HTTPToolCfg) tools.Tool {
|
||||||
timeout := cfg.Timeout
|
timeout := cfg.Timeout
|
||||||
if timeout == 0 {
|
if timeout == 0 {
|
||||||
timeout = 30 * time.Second
|
timeout = 30 * time.Second
|
||||||
}
|
}
|
||||||
client := &http.Client{Timeout: timeout}
|
client := &http.Client{Timeout: timeout}
|
||||||
|
|
||||||
return Tool{
|
return tools.Tool{
|
||||||
Def: Def{
|
Def: tools.Def{
|
||||||
Name: "http_post",
|
Name: "http_post",
|
||||||
Description: "Perform an HTTP POST request with a JSON body and return the response.",
|
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: "url", Type: "string", Description: "The URL to request", Required: true},
|
||||||
{Name: "body", Type: "string", Description: "The JSON body to send", Required: true},
|
{Name: "body", Type: "string", Description: "The JSON body to send", Required: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Exec: func(ctx context.Context, args map[string]any) Result {
|
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||||
rawURL := getString(args, "url")
|
rawURL := tools.GetString(args, "url")
|
||||||
if rawURL == "" {
|
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 == "" {
|
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 {
|
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))
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, rawURL, strings.NewReader(bodyStr))
|
||||||
if err != nil {
|
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")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
|
|
||||||
resp, err := client.Do(req)
|
resp, err := client.Do(req)
|
||||||
if err != nil {
|
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()
|
defer resp.Body.Close()
|
||||||
|
|
||||||
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
|
body, err := io.ReadAll(io.LimitReader(resp.Body, 64*1024))
|
||||||
if err != nil {
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/enmanuel/agents/pkg/knowledge"
|
"github.com/enmanuel/agents/pkg/knowledge"
|
||||||
|
"github.com/enmanuel/agents/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// KnowledgeStore is the subset of knowledge.Store needed by knowledge 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.
|
// NewKnowledgeSearch creates a tool that searches the knowledge base.
|
||||||
func NewKnowledgeSearch(store KnowledgeStore) Tool {
|
func NewKnowledgeSearch(store KnowledgeStore) tools.Tool {
|
||||||
return Tool{
|
return tools.Tool{
|
||||||
Def: Def{
|
Def: tools.Def{
|
||||||
Name: "knowledge_search",
|
Name: "knowledge_search",
|
||||||
Description: "Search your knowledge base for relevant documents. Returns matching snippets ranked by relevance.",
|
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: "query", Type: "string", Description: "Search terms or phrase", Required: true},
|
||||||
{Name: "limit", Type: "integer", Description: "Max results (default 5)", Required: false},
|
{Name: "limit", Type: "integer", Description: "Max results (default 5)", Required: false},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Exec: func(ctx context.Context, args map[string]any) Result {
|
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||||
query := getString(args, "query")
|
query := tools.GetString(args, "query")
|
||||||
if 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 {
|
if limit <= 0 {
|
||||||
limit = 5
|
limit = 5
|
||||||
}
|
}
|
||||||
|
|
||||||
results, err := store.Search(ctx, query, limit)
|
results, err := store.Search(ctx, query, limit)
|
||||||
if err != nil {
|
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 {
|
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
|
var sb strings.Builder
|
||||||
for i, r := range results {
|
for i, r := range results {
|
||||||
fmt.Fprintf(&sb, "%d. **%s** (`%s`)\n %s\n", i+1, r.Title, r.Slug, r.Snippet)
|
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.
|
// NewKnowledgeRead creates a tool that reads a knowledge document.
|
||||||
func NewKnowledgeRead(store KnowledgeStore) Tool {
|
func NewKnowledgeRead(store KnowledgeStore) tools.Tool {
|
||||||
return Tool{
|
return tools.Tool{
|
||||||
Def: Def{
|
Def: tools.Def{
|
||||||
Name: "knowledge_read",
|
Name: "knowledge_read",
|
||||||
Description: "Read the full content of a knowledge document by its slug.",
|
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},
|
{Name: "slug", Type: "string", Description: "Document slug (e.g. \"go-patterns\")", Required: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Exec: func(ctx context.Context, args map[string]any) Result {
|
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||||
slug := getString(args, "slug")
|
slug := tools.GetString(args, "slug")
|
||||||
if 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)
|
doc, err := store.Get(ctx, slug)
|
||||||
if err != nil {
|
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.
|
// NewKnowledgeWrite creates a tool that writes a knowledge document.
|
||||||
func NewKnowledgeWrite(store KnowledgeStore) Tool {
|
func NewKnowledgeWrite(store KnowledgeStore) tools.Tool {
|
||||||
return Tool{
|
return tools.Tool{
|
||||||
Def: Def{
|
Def: tools.Def{
|
||||||
Name: "knowledge_write",
|
Name: "knowledge_write",
|
||||||
Description: "Create or update a knowledge document. Use this to save new knowledge or improve existing documents.",
|
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: "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},
|
{Name: "content", Type: "string", Description: "Full markdown content of the document", Required: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Exec: func(ctx context.Context, args map[string]any) Result {
|
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||||
slug := getString(args, "slug")
|
slug := tools.GetString(args, "slug")
|
||||||
content := getString(args, "content")
|
content := tools.GetString(args, "content")
|
||||||
if slug == "" || 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{
|
err := store.Put(ctx, knowledge.Document{
|
||||||
@@ -102,28 +103,28 @@ func NewKnowledgeWrite(store KnowledgeStore) Tool {
|
|||||||
Content: content,
|
Content: content,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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.
|
// NewKnowledgeList creates a tool that lists all knowledge documents.
|
||||||
func NewKnowledgeList(store KnowledgeStore) Tool {
|
func NewKnowledgeList(store KnowledgeStore) tools.Tool {
|
||||||
return Tool{
|
return tools.Tool{
|
||||||
Def: Def{
|
Def: tools.Def{
|
||||||
Name: "knowledge_list",
|
Name: "knowledge_list",
|
||||||
Description: "List all documents in your knowledge base with their titles.",
|
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)
|
docs, err := store.List(ctx)
|
||||||
if err != nil {
|
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 {
|
if len(docs) == 0 {
|
||||||
return Result{Output: "knowledge base is empty"}
|
return tools.Result{Output: "knowledge base is empty"}
|
||||||
}
|
}
|
||||||
|
|
||||||
var sb strings.Builder
|
var sb strings.Builder
|
||||||
@@ -131,23 +132,7 @@ func NewKnowledgeList(store KnowledgeStore) Tool {
|
|||||||
fmt.Fprintf(&sb, "- `%s`: %s (updated %s)\n",
|
fmt.Fprintf(&sb, "- `%s`: %s (updated %s)\n",
|
||||||
d.Slug, d.Title, d.UpdatedAt.Format("2006-01-02"))
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/enmanuel/agents/pkg/knowledge"
|
"github.com/enmanuel/agents/pkg/knowledge"
|
||||||
|
"github.com/enmanuel/agents/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// mockKnowledgeStore implements KnowledgeStore for testing.
|
// mockKnowledgeStore implements KnowledgeStore for testing.
|
||||||
@@ -173,16 +174,9 @@ func TestGetInt(t *testing.T) {
|
|||||||
{map[string]any{}, "n", 0},
|
{map[string]any{}, "n", 0},
|
||||||
}
|
}
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
got := getInt(tt.args, tt.key)
|
got := tools.GetInt(tt.args, tt.key)
|
||||||
if got != tt.want {
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/enmanuel/agents/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MatrixSender is the interface for sending Matrix messages.
|
// 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.
|
// NewMatrixSend creates a matrix_send tool that sends a message to a Matrix room.
|
||||||
func NewMatrixSend(sender MatrixSender) Tool {
|
func NewMatrixSend(sender MatrixSender) tools.Tool {
|
||||||
return Tool{
|
return tools.Tool{
|
||||||
Def: Def{
|
Def: tools.Def{
|
||||||
Name: "matrix_send",
|
Name: "matrix_send",
|
||||||
Description: "Send a text message to a Matrix room.",
|
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: "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},
|
{Name: "message", Type: "string", Description: "The text message to send", Required: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Exec: func(ctx context.Context, args map[string]any) Result {
|
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||||
roomID := getString(args, "room_id")
|
roomID := tools.GetString(args, "room_id")
|
||||||
message := getString(args, "message")
|
message := tools.GetString(args, "message")
|
||||||
if roomID == "" || 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 {
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -8,6 +8,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/enmanuel/agents/pkg/memory"
|
"github.com/enmanuel/agents/pkg/memory"
|
||||||
|
"github.com/enmanuel/agents/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// MemoryStore is the subset of memory.Store needed by memory 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.
|
// NewMemorySave creates a tool that saves a fact to long-term memory.
|
||||||
func NewMemorySave(agentID string, store MemoryStore) Tool {
|
func NewMemorySave(agentID string, store MemoryStore) tools.Tool {
|
||||||
return Tool{
|
return tools.Tool{
|
||||||
Def: Def{
|
Def: tools.Def{
|
||||||
Name: "memory_save",
|
Name: "memory_save",
|
||||||
Description: "Save a fact to long-term memory. Use this to remember important information about users, topics, or preferences.",
|
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: "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: "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},
|
{Name: "value", Type: "string", Description: "The fact value to store", Required: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Exec: func(ctx context.Context, args map[string]any) Result {
|
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||||
subject := getString(args, "subject")
|
subject := tools.GetString(args, "subject")
|
||||||
key := getString(args, "key")
|
key := tools.GetString(args, "key")
|
||||||
value := getString(args, "value")
|
value := tools.GetString(args, "value")
|
||||||
if subject == "" || key == "" || 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{
|
err := store.SaveFact(ctx, memory.Fact{
|
||||||
AgentID: agentID,
|
AgentID: agentID,
|
||||||
@@ -69,119 +70,119 @@ func NewMemorySave(agentID string, store MemoryStore) Tool {
|
|||||||
Value: value,
|
Value: value,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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.
|
// NewMemoryRecall creates a tool that retrieves facts from long-term memory.
|
||||||
func NewMemoryRecall(agentID string, store MemoryStore) Tool {
|
func NewMemoryRecall(agentID string, store MemoryStore) tools.Tool {
|
||||||
return Tool{
|
return tools.Tool{
|
||||||
Def: Def{
|
Def: tools.Def{
|
||||||
Name: "memory_recall",
|
Name: "memory_recall",
|
||||||
Description: "Recall facts from long-term memory about a subject. Omit key to get all facts for the subject.",
|
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: "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},
|
{Name: "key", Type: "string", Description: "Optional specific fact key to recall", Required: false},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Exec: func(ctx context.Context, args map[string]any) Result {
|
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||||
subject := getString(args, "subject")
|
subject := tools.GetString(args, "subject")
|
||||||
if 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
|
var keyPtr *string
|
||||||
if k := getString(args, "key"); k != "" {
|
if k := tools.GetString(args, "key"); k != "" {
|
||||||
keyPtr = &k
|
keyPtr = &k
|
||||||
}
|
}
|
||||||
facts, err := store.RecallFacts(ctx, agentID, subject, keyPtr)
|
facts, err := store.RecallFacts(ctx, agentID, subject, keyPtr)
|
||||||
if err != nil {
|
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 {
|
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
|
var sb strings.Builder
|
||||||
for _, f := range facts {
|
for _, f := range facts {
|
||||||
fmt.Fprintf(&sb, "%s.%s = %s\n", f.Subject, f.Key, f.Value)
|
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.
|
// NewMemoryForget creates a tool that deletes facts from long-term memory.
|
||||||
func NewMemoryForget(agentID string, store MemoryStore) Tool {
|
func NewMemoryForget(agentID string, store MemoryStore) tools.Tool {
|
||||||
return Tool{
|
return tools.Tool{
|
||||||
Def: Def{
|
Def: tools.Def{
|
||||||
Name: "memory_forget",
|
Name: "memory_forget",
|
||||||
Description: "Delete facts from long-term memory. Omit key to delete all facts for the subject.",
|
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: "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},
|
{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 {
|
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||||
subject := getString(args, "subject")
|
subject := tools.GetString(args, "subject")
|
||||||
if 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
|
var keyPtr *string
|
||||||
if k := getString(args, "key"); k != "" {
|
if k := tools.GetString(args, "key"); k != "" {
|
||||||
keyPtr = &k
|
keyPtr = &k
|
||||||
}
|
}
|
||||||
err := store.DeleteFacts(ctx, agentID, subject, keyPtr)
|
err := store.DeleteFacts(ctx, agentID, subject, keyPtr)
|
||||||
if err != nil {
|
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 {
|
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.
|
// NewMemoryClearContext creates a tool that clears the conversation window.
|
||||||
func NewMemoryClearContext(clearer WindowClearer, roomCtx *RoomContext) Tool {
|
func NewMemoryClearContext(clearer WindowClearer, roomCtx *RoomContext) tools.Tool {
|
||||||
return Tool{
|
return tools.Tool{
|
||||||
Def: Def{
|
Def: tools.Def{
|
||||||
Name: "memory_clear_context",
|
Name: "memory_clear_context",
|
||||||
Description: "Clear the conversation context window. Useful to start fresh. Omit room_id to clear the current room.",
|
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},
|
{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 {
|
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||||
roomID := getString(args, "room_id")
|
roomID := tools.GetString(args, "room_id")
|
||||||
if roomID == "" {
|
if roomID == "" {
|
||||||
roomID = roomCtx.Get()
|
roomID = roomCtx.Get()
|
||||||
}
|
}
|
||||||
if roomID == "" {
|
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)
|
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.
|
// NewMemorySummary creates a tool that saves an important summary to long-term memory.
|
||||||
func NewMemorySummary(agentID string, store MemoryStore) Tool {
|
func NewMemorySummary(agentID string, store MemoryStore) tools.Tool {
|
||||||
return Tool{
|
return tools.Tool{
|
||||||
Def: Def{
|
Def: tools.Def{
|
||||||
Name: "memory_summary",
|
Name: "memory_summary",
|
||||||
Description: "Save an important summary or takeaway from the current conversation to long-term memory.",
|
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},
|
{Name: "text", Type: "string", Description: "The summary text to save", Required: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Exec: func(ctx context.Context, args map[string]any) Result {
|
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||||
text := getString(args, "text")
|
text := tools.GetString(args, "text")
|
||||||
if 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")
|
key := time.Now().UTC().Format("2006-01-02T15:04:05")
|
||||||
err := store.SaveFact(ctx, memory.Fact{
|
err := store.SaveFact(ctx, memory.Fact{
|
||||||
@@ -191,9 +192,9 @@ func NewMemorySummary(agentID string, store MemoryStore) Tool {
|
|||||||
Value: text,
|
Value: text,
|
||||||
})
|
})
|
||||||
if err != nil {
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -7,33 +7,34 @@ import (
|
|||||||
|
|
||||||
"github.com/enmanuel/agents/internal/config"
|
"github.com/enmanuel/agents/internal/config"
|
||||||
corespecs "github.com/enmanuel/agents/pkg/tools"
|
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.
|
// NewSSHCommand creates an ssh_command tool that executes remote commands via SSH.
|
||||||
// Validates targets against cfg.AllowedTargets and commands against cfg.ForbiddenCommands.
|
// Validates targets against cfg.AllowedTargets and commands against cfg.ForbiddenCommands.
|
||||||
func NewSSHCommand(cfg config.SSHToolCfg, exec *ssh.Executor) Tool {
|
func NewSSHCommand(cfg config.SSHToolCfg, exec *shellssh.Executor) tools.Tool {
|
||||||
return Tool{
|
return tools.Tool{
|
||||||
Def: Def{
|
Def: tools.Def{
|
||||||
Name: "ssh_command",
|
Name: "ssh_command",
|
||||||
Description: "Execute a command on a remote server via SSH.",
|
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: "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},
|
{Name: "command", Type: "string", Description: "The shell command to execute", Required: true},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Exec: func(ctx context.Context, args map[string]any) Result {
|
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||||
target := getString(args, "target")
|
target := tools.GetString(args, "target")
|
||||||
command := getString(args, "command")
|
command := tools.GetString(args, "command")
|
||||||
if target == "" || 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 {
|
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 {
|
if err := validateCommand(command, cfg.ForbiddenCommands); err != nil {
|
||||||
return Result{Err: err}
|
return tools.Result{Err: err}
|
||||||
}
|
}
|
||||||
|
|
||||||
timeout := "30s"
|
timeout := "30s"
|
||||||
@@ -48,14 +49,14 @@ func NewSSHCommand(cfg config.SSHToolCfg, exec *ssh.Executor) Tool {
|
|||||||
})
|
})
|
||||||
|
|
||||||
if res.Err != nil {
|
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
|
output := res.Stdout
|
||||||
if res.Stderr != "" {
|
if res.Stderr != "" {
|
||||||
output += "\nstderr: " + 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
|
Exec ToolFunc
|
||||||
}
|
}
|
||||||
|
|
||||||
// getString extracts a string argument by name, returning "" if missing or wrong type.
|
// GetString extracts a string argument by name, returning "" if missing or wrong type.
|
||||||
func getString(args map[string]any, key string) string {
|
func GetString(args map[string]any, key string) string {
|
||||||
v, ok := args[key]
|
v, ok := args[key]
|
||||||
if !ok {
|
if !ok {
|
||||||
return ""
|
return ""
|
||||||
@@ -47,3 +47,19 @@ func getString(args map[string]any, key string) string {
|
|||||||
}
|
}
|
||||||
return s
|
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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@@ -9,42 +9,44 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/enmanuel/agents/tools"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewWeather creates a get_weather tool that fetches current weather and forecast
|
// 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).
|
// 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}
|
client := &http.Client{Timeout: 15 * time.Second}
|
||||||
|
|
||||||
return Tool{
|
return tools.Tool{
|
||||||
Def: Def{
|
Def: tools.Def{
|
||||||
Name: "get_weather",
|
Name: "get_weather",
|
||||||
Description: "Get current weather conditions and 3-day forecast for a city. Returns temperature, humidity, wind speed, and weather description.",
|
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},
|
{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 {
|
Exec: func(ctx context.Context, args map[string]any) tools.Result {
|
||||||
city := getString(args, "city")
|
city := tools.GetString(args, "city")
|
||||||
if 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
|
// Step 1: Geocode city name to coordinates
|
||||||
lat, lon, resolvedName, country, err := geocodeCity(ctx, client, city)
|
lat, lon, resolvedName, country, err := geocodeCity(ctx, client, city)
|
||||||
if err != nil {
|
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
|
// Step 2: Fetch weather data
|
||||||
weather, err := fetchWeather(ctx, client, lat, lon)
|
weather, err := fetchWeather(ctx, client, lat, lon)
|
||||||
if err != nil {
|
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
|
// Step 3: Format output
|
||||||
output := formatWeather(resolvedName, country, weather)
|
output := formatWeather(resolvedName, country, weather)
|
||||||
return Result{Output: output}
|
return tools.Result{Output: output}
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user