mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
feat(fmt): Run formatters
This commit is contained in:
+5
-5
@@ -6,8 +6,8 @@ import "context"
|
||||
type Tool interface {
|
||||
Name() string
|
||||
Description() string
|
||||
Parameters() map[string]interface{}
|
||||
Execute(ctx context.Context, args map[string]interface{}) *ToolResult
|
||||
Parameters() map[string]any
|
||||
Execute(ctx context.Context, args map[string]any) *ToolResult
|
||||
}
|
||||
|
||||
// ContextualTool is an optional interface that tools can implement
|
||||
@@ -69,10 +69,10 @@ type AsyncTool interface {
|
||||
SetCallback(cb AsyncCallback)
|
||||
}
|
||||
|
||||
func ToolToSchema(tool Tool) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func ToolToSchema(tool Tool) map[string]any {
|
||||
return map[string]any{
|
||||
"type": "function",
|
||||
"function": map[string]interface{}{
|
||||
"function": map[string]any{
|
||||
"name": tool.Name(),
|
||||
"description": tool.Description(),
|
||||
"parameters": tool.Parameters(),
|
||||
|
||||
+20
-18
@@ -30,7 +30,10 @@ type CronTool struct {
|
||||
|
||||
// NewCronTool creates a new CronTool
|
||||
// execTimeout: 0 means no timeout, >0 sets the timeout duration
|
||||
func NewCronTool(cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool, execTimeout time.Duration, config *config.Config) *CronTool {
|
||||
func NewCronTool(
|
||||
cronService *cron.CronService, executor JobExecutor, msgBus *bus.MessageBus, workspace string, restrict bool,
|
||||
execTimeout time.Duration, config *config.Config,
|
||||
) *CronTool {
|
||||
execTool := NewExecToolWithConfig(workspace, restrict, config)
|
||||
execTool.SetTimeout(execTimeout)
|
||||
return &CronTool{
|
||||
@@ -52,40 +55,40 @@ func (t *CronTool) Description() string {
|
||||
}
|
||||
|
||||
// Parameters returns the tool parameters schema
|
||||
func (t *CronTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func (t *CronTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"action": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"action": map[string]any{
|
||||
"type": "string",
|
||||
"enum": []string{"add", "list", "remove", "enable", "disable"},
|
||||
"description": "Action to perform. Use 'add' when user wants to schedule a reminder or task.",
|
||||
},
|
||||
"message": map[string]interface{}{
|
||||
"message": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The reminder/task message to display when triggered. If 'command' is used, this describes what the command does.",
|
||||
},
|
||||
"command": map[string]interface{}{
|
||||
"command": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Optional: Shell command to execute directly (e.g., 'df -h'). If set, the agent will run this command and report output instead of just showing the message. 'deliver' will be forced to false for commands.",
|
||||
},
|
||||
"at_seconds": map[string]interface{}{
|
||||
"at_seconds": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "One-time reminder: seconds from now when to trigger (e.g., 600 for 10 minutes later). Use this for one-time reminders like 'remind me in 10 minutes'.",
|
||||
},
|
||||
"every_seconds": map[string]interface{}{
|
||||
"every_seconds": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "Recurring interval in seconds (e.g., 3600 for every hour). Use this ONLY for recurring tasks like 'every 2 hours' or 'daily reminder'.",
|
||||
},
|
||||
"cron_expr": map[string]interface{}{
|
||||
"cron_expr": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Cron expression for complex recurring schedules (e.g., '0 9 * * *' for daily at 9am). Use this for complex recurring schedules.",
|
||||
},
|
||||
"job_id": map[string]interface{}{
|
||||
"job_id": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Job ID (for remove/enable/disable)",
|
||||
},
|
||||
"deliver": map[string]interface{}{
|
||||
"deliver": map[string]any{
|
||||
"type": "boolean",
|
||||
"description": "If true, send message directly to channel. If false, let agent process message (for complex tasks). Default: true",
|
||||
},
|
||||
@@ -103,7 +106,7 @@ func (t *CronTool) SetContext(channel, chatID string) {
|
||||
}
|
||||
|
||||
// Execute runs the tool with the given arguments
|
||||
func (t *CronTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
||||
func (t *CronTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||
action, ok := args["action"].(string)
|
||||
if !ok {
|
||||
return ErrorResult("action is required")
|
||||
@@ -125,7 +128,7 @@ func (t *CronTool) Execute(ctx context.Context, args map[string]interface{}) *To
|
||||
}
|
||||
}
|
||||
|
||||
func (t *CronTool) addJob(args map[string]interface{}) *ToolResult {
|
||||
func (t *CronTool) addJob(args map[string]any) *ToolResult {
|
||||
t.mu.RLock()
|
||||
channel := t.channel
|
||||
chatID := t.chatID
|
||||
@@ -233,7 +236,7 @@ func (t *CronTool) listJobs() *ToolResult {
|
||||
return SilentResult(result)
|
||||
}
|
||||
|
||||
func (t *CronTool) removeJob(args map[string]interface{}) *ToolResult {
|
||||
func (t *CronTool) removeJob(args map[string]any) *ToolResult {
|
||||
jobID, ok := args["job_id"].(string)
|
||||
if !ok || jobID == "" {
|
||||
return ErrorResult("job_id is required for remove")
|
||||
@@ -245,7 +248,7 @@ func (t *CronTool) removeJob(args map[string]interface{}) *ToolResult {
|
||||
return ErrorResult(fmt.Sprintf("Job %s not found", jobID))
|
||||
}
|
||||
|
||||
func (t *CronTool) enableJob(args map[string]interface{}, enable bool) *ToolResult {
|
||||
func (t *CronTool) enableJob(args map[string]any, enable bool) *ToolResult {
|
||||
jobID, ok := args["job_id"].(string)
|
||||
if !ok || jobID == "" {
|
||||
return ErrorResult("job_id is required for enable/disable")
|
||||
@@ -279,7 +282,7 @@ func (t *CronTool) ExecuteJob(ctx context.Context, job *cron.CronJob) string {
|
||||
|
||||
// Execute command if present
|
||||
if job.Payload.Command != "" {
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"command": job.Payload.Command,
|
||||
}
|
||||
|
||||
@@ -320,7 +323,6 @@ func (t *CronTool) ExecuteJob(ctx context.Context, job *cron.CronJob) string {
|
||||
channel,
|
||||
chatID,
|
||||
)
|
||||
|
||||
if err != nil {
|
||||
return fmt.Sprintf("Error: %v", err)
|
||||
}
|
||||
|
||||
+18
-16
@@ -30,19 +30,19 @@ func (t *EditFileTool) Description() string {
|
||||
return "Edit a file by replacing old_text with new_text. The old_text must exist exactly in the file."
|
||||
}
|
||||
|
||||
func (t *EditFileTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func (t *EditFileTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"path": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"path": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The file path to edit",
|
||||
},
|
||||
"old_text": map[string]interface{}{
|
||||
"old_text": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The exact text to find and replace",
|
||||
},
|
||||
"new_text": map[string]interface{}{
|
||||
"new_text": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The text to replace with",
|
||||
},
|
||||
@@ -51,7 +51,7 @@ func (t *EditFileTool) Parameters() map[string]interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *EditFileTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
||||
func (t *EditFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||
path, ok := args["path"].(string)
|
||||
if !ok {
|
||||
return ErrorResult("path is required")
|
||||
@@ -89,12 +89,14 @@ func (t *EditFileTool) Execute(ctx context.Context, args map[string]interface{})
|
||||
|
||||
count := strings.Count(contentStr, oldText)
|
||||
if count > 1 {
|
||||
return ErrorResult(fmt.Sprintf("old_text appears %d times. Please provide more context to make it unique", count))
|
||||
return ErrorResult(
|
||||
fmt.Sprintf("old_text appears %d times. Please provide more context to make it unique", count),
|
||||
)
|
||||
}
|
||||
|
||||
newContent := strings.Replace(contentStr, oldText, newText, 1)
|
||||
|
||||
if err := os.WriteFile(resolvedPath, []byte(newContent), 0644); err != nil {
|
||||
if err := os.WriteFile(resolvedPath, []byte(newContent), 0o644); err != nil {
|
||||
return ErrorResult(fmt.Sprintf("failed to write file: %v", err))
|
||||
}
|
||||
|
||||
@@ -118,15 +120,15 @@ func (t *AppendFileTool) Description() string {
|
||||
return "Append content to the end of a file"
|
||||
}
|
||||
|
||||
func (t *AppendFileTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func (t *AppendFileTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"path": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"path": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The file path to append to",
|
||||
},
|
||||
"content": map[string]interface{}{
|
||||
"content": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The content to append",
|
||||
},
|
||||
@@ -135,7 +137,7 @@ func (t *AppendFileTool) Parameters() map[string]interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *AppendFileTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
||||
func (t *AppendFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||
path, ok := args["path"].(string)
|
||||
if !ok {
|
||||
return ErrorResult("path is required")
|
||||
@@ -151,7 +153,7 @@ func (t *AppendFileTool) Execute(ctx context.Context, args map[string]interface{
|
||||
return ErrorResult(err.Error())
|
||||
}
|
||||
|
||||
f, err := os.OpenFile(resolvedPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
|
||||
f, err := os.OpenFile(resolvedPath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("failed to open file: %v", err))
|
||||
}
|
||||
|
||||
+16
-16
@@ -12,11 +12,11 @@ import (
|
||||
func TestEditTool_EditFile_Success(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.txt")
|
||||
os.WriteFile(testFile, []byte("Hello World\nThis is a test"), 0644)
|
||||
os.WriteFile(testFile, []byte("Hello World\nThis is a test"), 0o644)
|
||||
|
||||
tool := NewEditFileTool(tmpDir, true)
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"path": testFile,
|
||||
"old_text": "World",
|
||||
"new_text": "Universe",
|
||||
@@ -60,7 +60,7 @@ func TestEditTool_EditFile_NotFound(t *testing.T) {
|
||||
|
||||
tool := NewEditFileTool(tmpDir, true)
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"path": testFile,
|
||||
"old_text": "old",
|
||||
"new_text": "new",
|
||||
@@ -83,11 +83,11 @@ func TestEditTool_EditFile_NotFound(t *testing.T) {
|
||||
func TestEditTool_EditFile_OldTextNotFound(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.txt")
|
||||
os.WriteFile(testFile, []byte("Hello World"), 0644)
|
||||
os.WriteFile(testFile, []byte("Hello World"), 0o644)
|
||||
|
||||
tool := NewEditFileTool(tmpDir, true)
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"path": testFile,
|
||||
"old_text": "Goodbye",
|
||||
"new_text": "Hello",
|
||||
@@ -110,11 +110,11 @@ func TestEditTool_EditFile_OldTextNotFound(t *testing.T) {
|
||||
func TestEditTool_EditFile_MultipleMatches(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.txt")
|
||||
os.WriteFile(testFile, []byte("test test test"), 0644)
|
||||
os.WriteFile(testFile, []byte("test test test"), 0o644)
|
||||
|
||||
tool := NewEditFileTool(tmpDir, true)
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"path": testFile,
|
||||
"old_text": "test",
|
||||
"new_text": "done",
|
||||
@@ -138,11 +138,11 @@ func TestEditTool_EditFile_OutsideAllowedDir(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
otherDir := t.TempDir()
|
||||
testFile := filepath.Join(otherDir, "test.txt")
|
||||
os.WriteFile(testFile, []byte("content"), 0644)
|
||||
os.WriteFile(testFile, []byte("content"), 0o644)
|
||||
|
||||
tool := NewEditFileTool(tmpDir, true) // Restrict to tmpDir
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"path": testFile,
|
||||
"old_text": "content",
|
||||
"new_text": "new",
|
||||
@@ -165,7 +165,7 @@ func TestEditTool_EditFile_OutsideAllowedDir(t *testing.T) {
|
||||
func TestEditTool_EditFile_MissingPath(t *testing.T) {
|
||||
tool := NewEditFileTool("", false)
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"old_text": "old",
|
||||
"new_text": "new",
|
||||
}
|
||||
@@ -182,7 +182,7 @@ func TestEditTool_EditFile_MissingPath(t *testing.T) {
|
||||
func TestEditTool_EditFile_MissingOldText(t *testing.T) {
|
||||
tool := NewEditFileTool("", false)
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"path": "/tmp/test.txt",
|
||||
"new_text": "new",
|
||||
}
|
||||
@@ -199,7 +199,7 @@ func TestEditTool_EditFile_MissingOldText(t *testing.T) {
|
||||
func TestEditTool_EditFile_MissingNewText(t *testing.T) {
|
||||
tool := NewEditFileTool("", false)
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"path": "/tmp/test.txt",
|
||||
"old_text": "old",
|
||||
}
|
||||
@@ -216,11 +216,11 @@ func TestEditTool_EditFile_MissingNewText(t *testing.T) {
|
||||
func TestEditTool_AppendFile_Success(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.txt")
|
||||
os.WriteFile(testFile, []byte("Initial content"), 0644)
|
||||
os.WriteFile(testFile, []byte("Initial content"), 0o644)
|
||||
|
||||
tool := NewAppendFileTool("", false)
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"path": testFile,
|
||||
"content": "\nAppended content",
|
||||
}
|
||||
@@ -260,7 +260,7 @@ func TestEditTool_AppendFile_Success(t *testing.T) {
|
||||
func TestEditTool_AppendFile_MissingPath(t *testing.T) {
|
||||
tool := NewAppendFileTool("", false)
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"content": "test",
|
||||
}
|
||||
|
||||
@@ -276,7 +276,7 @@ func TestEditTool_AppendFile_MissingPath(t *testing.T) {
|
||||
func TestEditTool_AppendFile_MissingContent(t *testing.T) {
|
||||
tool := NewAppendFileTool("", false)
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"path": "/tmp/test.txt",
|
||||
}
|
||||
|
||||
|
||||
+18
-18
@@ -94,11 +94,11 @@ func (t *ReadFileTool) Description() string {
|
||||
return "Read the contents of a file"
|
||||
}
|
||||
|
||||
func (t *ReadFileTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func (t *ReadFileTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"path": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"path": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Path to the file to read",
|
||||
},
|
||||
@@ -107,7 +107,7 @@ func (t *ReadFileTool) Parameters() map[string]interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ReadFileTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
||||
func (t *ReadFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||
path, ok := args["path"].(string)
|
||||
if !ok {
|
||||
return ErrorResult("path is required")
|
||||
@@ -143,15 +143,15 @@ func (t *WriteFileTool) Description() string {
|
||||
return "Write content to a file"
|
||||
}
|
||||
|
||||
func (t *WriteFileTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func (t *WriteFileTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"path": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"path": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Path to the file to write",
|
||||
},
|
||||
"content": map[string]interface{}{
|
||||
"content": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Content to write to the file",
|
||||
},
|
||||
@@ -160,7 +160,7 @@ func (t *WriteFileTool) Parameters() map[string]interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *WriteFileTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
||||
func (t *WriteFileTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||
path, ok := args["path"].(string)
|
||||
if !ok {
|
||||
return ErrorResult("path is required")
|
||||
@@ -177,11 +177,11 @@ func (t *WriteFileTool) Execute(ctx context.Context, args map[string]interface{}
|
||||
}
|
||||
|
||||
dir := filepath.Dir(resolvedPath)
|
||||
if err := os.MkdirAll(dir, 0755); err != nil {
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
return ErrorResult(fmt.Sprintf("failed to create directory: %v", err))
|
||||
}
|
||||
|
||||
if err := os.WriteFile(resolvedPath, []byte(content), 0644); err != nil {
|
||||
if err := os.WriteFile(resolvedPath, []byte(content), 0o644); err != nil {
|
||||
return ErrorResult(fmt.Sprintf("failed to write file: %v", err))
|
||||
}
|
||||
|
||||
@@ -205,11 +205,11 @@ func (t *ListDirTool) Description() string {
|
||||
return "List files and directories in a path"
|
||||
}
|
||||
|
||||
func (t *ListDirTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func (t *ListDirTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"path": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"path": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Path to list",
|
||||
},
|
||||
@@ -218,7 +218,7 @@ func (t *ListDirTool) Parameters() map[string]interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ListDirTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
||||
func (t *ListDirTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||
path, ok := args["path"].(string)
|
||||
if !ok {
|
||||
path = "."
|
||||
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
func TestFilesystemTool_ReadFile_Success(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.txt")
|
||||
os.WriteFile(testFile, []byte("test content"), 0644)
|
||||
os.WriteFile(testFile, []byte("test content"), 0o644)
|
||||
|
||||
tool := &ReadFileTool{}
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"path": testFile,
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ func TestFilesystemTool_ReadFile_Success(t *testing.T) {
|
||||
func TestFilesystemTool_ReadFile_NotFound(t *testing.T) {
|
||||
tool := &ReadFileTool{}
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"path": "/nonexistent_file_12345.txt",
|
||||
}
|
||||
|
||||
@@ -64,7 +64,7 @@ func TestFilesystemTool_ReadFile_NotFound(t *testing.T) {
|
||||
func TestFilesystemTool_ReadFile_MissingPath(t *testing.T) {
|
||||
tool := &ReadFileTool{}
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{}
|
||||
args := map[string]any{}
|
||||
|
||||
result := tool.Execute(ctx, args)
|
||||
|
||||
@@ -86,7 +86,7 @@ func TestFilesystemTool_WriteFile_Success(t *testing.T) {
|
||||
|
||||
tool := &WriteFileTool{}
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"path": testFile,
|
||||
"content": "hello world",
|
||||
}
|
||||
@@ -125,7 +125,7 @@ func TestFilesystemTool_WriteFile_CreateDir(t *testing.T) {
|
||||
|
||||
tool := &WriteFileTool{}
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"path": testFile,
|
||||
"content": "test",
|
||||
}
|
||||
@@ -151,7 +151,7 @@ func TestFilesystemTool_WriteFile_CreateDir(t *testing.T) {
|
||||
func TestFilesystemTool_WriteFile_MissingPath(t *testing.T) {
|
||||
tool := &WriteFileTool{}
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"content": "test",
|
||||
}
|
||||
|
||||
@@ -167,7 +167,7 @@ func TestFilesystemTool_WriteFile_MissingPath(t *testing.T) {
|
||||
func TestFilesystemTool_WriteFile_MissingContent(t *testing.T) {
|
||||
tool := &WriteFileTool{}
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"path": "/tmp/test.txt",
|
||||
}
|
||||
|
||||
@@ -179,7 +179,8 @@ func TestFilesystemTool_WriteFile_MissingContent(t *testing.T) {
|
||||
}
|
||||
|
||||
// Should mention required parameter
|
||||
if !strings.Contains(result.ForLLM, "content is required") && !strings.Contains(result.ForUser, "content is required") {
|
||||
if !strings.Contains(result.ForLLM, "content is required") &&
|
||||
!strings.Contains(result.ForUser, "content is required") {
|
||||
t.Errorf("Expected 'content is required' message, got ForLLM: %s", result.ForLLM)
|
||||
}
|
||||
}
|
||||
@@ -187,13 +188,13 @@ func TestFilesystemTool_WriteFile_MissingContent(t *testing.T) {
|
||||
// TestFilesystemTool_ListDir_Success verifies successful directory listing
|
||||
func TestFilesystemTool_ListDir_Success(t *testing.T) {
|
||||
tmpDir := t.TempDir()
|
||||
os.WriteFile(filepath.Join(tmpDir, "file1.txt"), []byte("content"), 0644)
|
||||
os.WriteFile(filepath.Join(tmpDir, "file2.txt"), []byte("content"), 0644)
|
||||
os.Mkdir(filepath.Join(tmpDir, "subdir"), 0755)
|
||||
os.WriteFile(filepath.Join(tmpDir, "file1.txt"), []byte("content"), 0o644)
|
||||
os.WriteFile(filepath.Join(tmpDir, "file2.txt"), []byte("content"), 0o644)
|
||||
os.Mkdir(filepath.Join(tmpDir, "subdir"), 0o755)
|
||||
|
||||
tool := &ListDirTool{}
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"path": tmpDir,
|
||||
}
|
||||
|
||||
@@ -217,7 +218,7 @@ func TestFilesystemTool_ListDir_Success(t *testing.T) {
|
||||
func TestFilesystemTool_ListDir_NotFound(t *testing.T) {
|
||||
tool := &ListDirTool{}
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"path": "/nonexistent_directory_12345",
|
||||
}
|
||||
|
||||
@@ -238,7 +239,7 @@ func TestFilesystemTool_ListDir_NotFound(t *testing.T) {
|
||||
func TestFilesystemTool_ListDir_DefaultPath(t *testing.T) {
|
||||
tool := &ListDirTool{}
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{}
|
||||
args := map[string]any{}
|
||||
|
||||
result := tool.Execute(ctx, args)
|
||||
|
||||
@@ -250,15 +251,14 @@ func TestFilesystemTool_ListDir_DefaultPath(t *testing.T) {
|
||||
|
||||
// Block paths that look inside workspace but point outside via symlink.
|
||||
func TestFilesystemTool_ReadFile_RejectsSymlinkEscape(t *testing.T) {
|
||||
|
||||
root := t.TempDir()
|
||||
workspace := filepath.Join(root, "workspace")
|
||||
if err := os.MkdirAll(workspace, 0755); err != nil {
|
||||
if err := os.MkdirAll(workspace, 0o755); err != nil {
|
||||
t.Fatalf("failed to create workspace: %v", err)
|
||||
}
|
||||
|
||||
secret := filepath.Join(root, "secret.txt")
|
||||
if err := os.WriteFile(secret, []byte("top secret"), 0644); err != nil {
|
||||
if err := os.WriteFile(secret, []byte("top secret"), 0o644); err != nil {
|
||||
t.Fatalf("failed to write secret file: %v", err)
|
||||
}
|
||||
|
||||
@@ -268,7 +268,7 @@ func TestFilesystemTool_ReadFile_RejectsSymlinkEscape(t *testing.T) {
|
||||
}
|
||||
|
||||
tool := NewReadFileTool(workspace, true)
|
||||
result := tool.Execute(context.Background(), map[string]interface{}{
|
||||
result := tool.Execute(context.Background(), map[string]any{
|
||||
"path": link,
|
||||
})
|
||||
|
||||
|
||||
+17
-15
@@ -24,37 +24,37 @@ func (t *I2CTool) Description() string {
|
||||
return "Interact with I2C bus devices for reading sensors and controlling peripherals. Actions: detect (list buses), scan (find devices on a bus), read (read bytes from device), write (send bytes to device). Linux only."
|
||||
}
|
||||
|
||||
func (t *I2CTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func (t *I2CTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"action": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"action": map[string]any{
|
||||
"type": "string",
|
||||
"enum": []string{"detect", "scan", "read", "write"},
|
||||
"description": "Action to perform: detect (list available I2C buses), scan (find devices on a bus), read (read bytes from a device), write (send bytes to a device)",
|
||||
},
|
||||
"bus": map[string]interface{}{
|
||||
"bus": map[string]any{
|
||||
"type": "string",
|
||||
"description": "I2C bus number (e.g. \"1\" for /dev/i2c-1). Required for scan/read/write.",
|
||||
},
|
||||
"address": map[string]interface{}{
|
||||
"address": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "7-bit I2C device address (0x03-0x77). Required for read/write.",
|
||||
},
|
||||
"register": map[string]interface{}{
|
||||
"register": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "Register address to read from or write to. If set, sends register byte before read/write.",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"data": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]interface{}{"type": "integer"},
|
||||
"items": map[string]any{"type": "integer"},
|
||||
"description": "Bytes to write (0-255 each). Required for write action.",
|
||||
},
|
||||
"length": map[string]interface{}{
|
||||
"length": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "Number of bytes to read (1-256). Default: 1. Used with read action.",
|
||||
},
|
||||
"confirm": map[string]interface{}{
|
||||
"confirm": map[string]any{
|
||||
"type": "boolean",
|
||||
"description": "Must be true for write operations. Safety guard to prevent accidental writes.",
|
||||
},
|
||||
@@ -63,7 +63,7 @@ func (t *I2CTool) Parameters() map[string]interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *I2CTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
||||
func (t *I2CTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||
if runtime.GOOS != "linux" {
|
||||
return ErrorResult("I2C is only supported on Linux. This tool requires /dev/i2c-* device files.")
|
||||
}
|
||||
@@ -95,7 +95,9 @@ func (t *I2CTool) detect() *ToolResult {
|
||||
}
|
||||
|
||||
if len(matches) == 0 {
|
||||
return SilentResult("No I2C buses found. You may need to:\n1. Load the i2c-dev module: modprobe i2c-dev\n2. Check that I2C is enabled in device tree\n3. Configure pinmux for your board (see hardware skill)")
|
||||
return SilentResult(
|
||||
"No I2C buses found. You may need to:\n1. Load the i2c-dev module: modprobe i2c-dev\n2. Check that I2C is enabled in device tree\n3. Configure pinmux for your board (see hardware skill)",
|
||||
)
|
||||
}
|
||||
|
||||
type busInfo struct {
|
||||
@@ -122,7 +124,7 @@ func isValidBusID(id string) bool {
|
||||
}
|
||||
|
||||
// parseI2CAddress extracts and validates an I2C address from args
|
||||
func parseI2CAddress(args map[string]interface{}) (int, *ToolResult) {
|
||||
func parseI2CAddress(args map[string]any) (int, *ToolResult) {
|
||||
addrFloat, ok := args["address"].(float64)
|
||||
if !ok {
|
||||
return 0, ErrorResult("address is required (e.g. 0x38 for AHT20)")
|
||||
@@ -135,7 +137,7 @@ func parseI2CAddress(args map[string]interface{}) (int, *ToolResult) {
|
||||
}
|
||||
|
||||
// parseI2CBus extracts and validates an I2C bus from args
|
||||
func parseI2CBus(args map[string]interface{}) (string, *ToolResult) {
|
||||
func parseI2CBus(args map[string]any) (string, *ToolResult) {
|
||||
bus, ok := args["bus"].(string)
|
||||
if !ok || bus == "" {
|
||||
return "", ErrorResult("bus is required (e.g. \"1\" for /dev/i2c-1)")
|
||||
|
||||
+12
-8
@@ -74,7 +74,7 @@ func smbusProbe(fd int, addr int, hasQuick bool) bool {
|
||||
// scan probes valid 7-bit addresses on a bus for connected devices.
|
||||
// Uses the same hybrid probe strategy as i2cdetect's MODE_AUTO:
|
||||
// SMBus Quick Write for most addresses, SMBus Read Byte for EEPROM ranges.
|
||||
func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
|
||||
func (t *I2CTool) scan(args map[string]any) *ToolResult {
|
||||
bus, errResult := parseI2CBus(args)
|
||||
if errResult != nil {
|
||||
return errResult
|
||||
@@ -99,7 +99,9 @@ func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
|
||||
hasReadByte := funcs&i2cFuncSmbusReadByte != 0
|
||||
|
||||
if !hasQuick && !hasReadByte {
|
||||
return ErrorResult(fmt.Sprintf("I2C adapter %s supports neither SMBus Quick nor Read Byte — cannot probe safely", devPath))
|
||||
return ErrorResult(
|
||||
fmt.Sprintf("I2C adapter %s supports neither SMBus Quick nor Read Byte — cannot probe safely", devPath),
|
||||
)
|
||||
}
|
||||
|
||||
type deviceEntry struct {
|
||||
@@ -133,7 +135,7 @@ func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
|
||||
return SilentResult(fmt.Sprintf("No devices found on %s. Check wiring and pull-up resistors.", devPath))
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(map[string]interface{}{
|
||||
result, _ := json.MarshalIndent(map[string]any{
|
||||
"bus": devPath,
|
||||
"devices": found,
|
||||
"count": len(found),
|
||||
@@ -142,7 +144,7 @@ func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
|
||||
}
|
||||
|
||||
// readDevice reads bytes from an I2C device, optionally at a specific register
|
||||
func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
|
||||
func (t *I2CTool) readDevice(args map[string]any) *ToolResult {
|
||||
bus, errResult := parseI2CBus(args)
|
||||
if errResult != nil {
|
||||
return errResult
|
||||
@@ -201,7 +203,7 @@ func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
|
||||
intBytes[i] = int(buf[i])
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(map[string]interface{}{
|
||||
result, _ := json.MarshalIndent(map[string]any{
|
||||
"bus": devPath,
|
||||
"address": fmt.Sprintf("0x%02x", addr),
|
||||
"bytes": intBytes,
|
||||
@@ -212,10 +214,12 @@ func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
|
||||
}
|
||||
|
||||
// writeDevice writes bytes to an I2C device, optionally at a specific register
|
||||
func (t *I2CTool) writeDevice(args map[string]interface{}) *ToolResult {
|
||||
func (t *I2CTool) writeDevice(args map[string]any) *ToolResult {
|
||||
confirm, _ := args["confirm"].(bool)
|
||||
if !confirm {
|
||||
return ErrorResult("write operations require confirm: true. Please confirm with the user before writing to I2C devices, as incorrect writes can misconfigure hardware.")
|
||||
return ErrorResult(
|
||||
"write operations require confirm: true. Please confirm with the user before writing to I2C devices, as incorrect writes can misconfigure hardware.",
|
||||
)
|
||||
}
|
||||
|
||||
bus, errResult := parseI2CBus(args)
|
||||
@@ -228,7 +232,7 @@ func (t *I2CTool) writeDevice(args map[string]interface{}) *ToolResult {
|
||||
return errResult
|
||||
}
|
||||
|
||||
dataRaw, ok := args["data"].([]interface{})
|
||||
dataRaw, ok := args["data"].([]any)
|
||||
if !ok || len(dataRaw) == 0 {
|
||||
return ErrorResult("data is required for write (array of byte values 0-255)")
|
||||
}
|
||||
|
||||
@@ -3,16 +3,16 @@
|
||||
package tools
|
||||
|
||||
// scan is a stub for non-Linux platforms.
|
||||
func (t *I2CTool) scan(args map[string]interface{}) *ToolResult {
|
||||
func (t *I2CTool) scan(args map[string]any) *ToolResult {
|
||||
return ErrorResult("I2C is only supported on Linux")
|
||||
}
|
||||
|
||||
// readDevice is a stub for non-Linux platforms.
|
||||
func (t *I2CTool) readDevice(args map[string]interface{}) *ToolResult {
|
||||
func (t *I2CTool) readDevice(args map[string]any) *ToolResult {
|
||||
return ErrorResult("I2C is only supported on Linux")
|
||||
}
|
||||
|
||||
// writeDevice is a stub for non-Linux platforms.
|
||||
func (t *I2CTool) writeDevice(args map[string]interface{}) *ToolResult {
|
||||
func (t *I2CTool) writeDevice(args map[string]any) *ToolResult {
|
||||
return ErrorResult("I2C is only supported on Linux")
|
||||
}
|
||||
|
||||
@@ -26,19 +26,19 @@ func (t *MessageTool) Description() string {
|
||||
return "Send a message to user on a chat channel. Use this when you want to communicate something."
|
||||
}
|
||||
|
||||
func (t *MessageTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func (t *MessageTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"content": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"content": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The message content to send",
|
||||
},
|
||||
"channel": map[string]interface{}{
|
||||
"channel": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Optional: target channel (telegram, whatsapp, etc.)",
|
||||
},
|
||||
"chat_id": map[string]interface{}{
|
||||
"chat_id": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Optional: target chat/user ID",
|
||||
},
|
||||
@@ -62,7 +62,7 @@ func (t *MessageTool) SetSendCallback(callback SendCallback) {
|
||||
t.sendCallback = callback
|
||||
}
|
||||
|
||||
func (t *MessageTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
||||
func (t *MessageTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||
content, ok := args["content"].(string)
|
||||
if !ok {
|
||||
return &ToolResult{ForLLM: "content is required", IsError: true}
|
||||
|
||||
+10
-10
@@ -19,7 +19,7 @@ func TestMessageTool_Execute_Success(t *testing.T) {
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"content": "Hello, world!",
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestMessageTool_Execute_WithCustomChannel(t *testing.T) {
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"content": "Test message",
|
||||
"channel": "custom-channel",
|
||||
"chat_id": "custom-chat-id",
|
||||
@@ -104,7 +104,7 @@ func TestMessageTool_Execute_SendFailure(t *testing.T) {
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"content": "Test message",
|
||||
}
|
||||
|
||||
@@ -136,7 +136,7 @@ func TestMessageTool_Execute_MissingContent(t *testing.T) {
|
||||
tool.SetContext("test-channel", "test-chat-id")
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{} // content missing
|
||||
args := map[string]any{} // content missing
|
||||
|
||||
result := tool.Execute(ctx, args)
|
||||
|
||||
@@ -158,7 +158,7 @@ func TestMessageTool_Execute_NoTargetChannel(t *testing.T) {
|
||||
})
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"content": "Test message",
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ func TestMessageTool_Execute_NotConfigured(t *testing.T) {
|
||||
// No SetSendCallback called
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"content": "Test message",
|
||||
}
|
||||
|
||||
@@ -219,7 +219,7 @@ func TestMessageTool_Parameters(t *testing.T) {
|
||||
t.Error("Expected type 'object'")
|
||||
}
|
||||
|
||||
props, ok := params["properties"].(map[string]interface{})
|
||||
props, ok := params["properties"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("Expected properties to be a map")
|
||||
}
|
||||
@@ -231,7 +231,7 @@ func TestMessageTool_Parameters(t *testing.T) {
|
||||
}
|
||||
|
||||
// Check content property
|
||||
contentProp, ok := props["content"].(map[string]interface{})
|
||||
contentProp, ok := props["content"].(map[string]any)
|
||||
if !ok {
|
||||
t.Error("Expected 'content' property")
|
||||
}
|
||||
@@ -240,7 +240,7 @@ func TestMessageTool_Parameters(t *testing.T) {
|
||||
}
|
||||
|
||||
// Check channel property (optional)
|
||||
channelProp, ok := props["channel"].(map[string]interface{})
|
||||
channelProp, ok := props["channel"].(map[string]any)
|
||||
if !ok {
|
||||
t.Error("Expected 'channel' property")
|
||||
}
|
||||
@@ -249,7 +249,7 @@ func TestMessageTool_Parameters(t *testing.T) {
|
||||
}
|
||||
|
||||
// Check chat_id property (optional)
|
||||
chatIDProp, ok := props["chat_id"].(map[string]interface{})
|
||||
chatIDProp, ok := props["chat_id"].(map[string]any)
|
||||
if !ok {
|
||||
t.Error("Expected 'chat_id' property")
|
||||
}
|
||||
|
||||
+18
-12
@@ -34,16 +34,22 @@ func (r *ToolRegistry) Get(name string) (Tool, bool) {
|
||||
return tool, ok
|
||||
}
|
||||
|
||||
func (r *ToolRegistry) Execute(ctx context.Context, name string, args map[string]interface{}) *ToolResult {
|
||||
func (r *ToolRegistry) Execute(ctx context.Context, name string, args map[string]any) *ToolResult {
|
||||
return r.ExecuteWithContext(ctx, name, args, "", "", nil)
|
||||
}
|
||||
|
||||
// ExecuteWithContext executes a tool with channel/chatID context and optional async callback.
|
||||
// If the tool implements AsyncTool and a non-nil callback is provided,
|
||||
// the callback will be set on the tool before execution.
|
||||
func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args map[string]interface{}, channel, chatID string, asyncCallback AsyncCallback) *ToolResult {
|
||||
func (r *ToolRegistry) ExecuteWithContext(
|
||||
ctx context.Context,
|
||||
name string,
|
||||
args map[string]any,
|
||||
channel, chatID string,
|
||||
asyncCallback AsyncCallback,
|
||||
) *ToolResult {
|
||||
logger.InfoCF("tool", "Tool execution started",
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"tool": name,
|
||||
"args": args,
|
||||
})
|
||||
@@ -51,7 +57,7 @@ func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args
|
||||
tool, ok := r.Get(name)
|
||||
if !ok {
|
||||
logger.ErrorCF("tool", "Tool not found",
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"tool": name,
|
||||
})
|
||||
return ErrorResult(fmt.Sprintf("tool %q not found", name)).WithError(fmt.Errorf("tool not found"))
|
||||
@@ -66,7 +72,7 @@ func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args
|
||||
if asyncTool, ok := tool.(AsyncTool); ok && asyncCallback != nil {
|
||||
asyncTool.SetCallback(asyncCallback)
|
||||
logger.DebugCF("tool", "Async callback injected",
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"tool": name,
|
||||
})
|
||||
}
|
||||
@@ -78,20 +84,20 @@ func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args
|
||||
// Log based on result type
|
||||
if result.IsError {
|
||||
logger.ErrorCF("tool", "Tool execution failed",
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"tool": name,
|
||||
"duration": duration.Milliseconds(),
|
||||
"error": result.ForLLM,
|
||||
})
|
||||
} else if result.Async {
|
||||
logger.InfoCF("tool", "Tool started (async)",
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"tool": name,
|
||||
"duration": duration.Milliseconds(),
|
||||
})
|
||||
} else {
|
||||
logger.InfoCF("tool", "Tool execution completed",
|
||||
map[string]interface{}{
|
||||
map[string]any{
|
||||
"tool": name,
|
||||
"duration_ms": duration.Milliseconds(),
|
||||
"result_length": len(result.ForLLM),
|
||||
@@ -101,11 +107,11 @@ func (r *ToolRegistry) ExecuteWithContext(ctx context.Context, name string, args
|
||||
return result
|
||||
}
|
||||
|
||||
func (r *ToolRegistry) GetDefinitions() []map[string]interface{} {
|
||||
func (r *ToolRegistry) GetDefinitions() []map[string]any {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
definitions := make([]map[string]interface{}, 0, len(r.tools))
|
||||
definitions := make([]map[string]any, 0, len(r.tools))
|
||||
for _, tool := range r.tools {
|
||||
definitions = append(definitions, ToolToSchema(tool))
|
||||
}
|
||||
@@ -123,14 +129,14 @@ func (r *ToolRegistry) ToProviderDefs() []providers.ToolDefinition {
|
||||
schema := ToolToSchema(tool)
|
||||
|
||||
// Safely extract nested values with type checks
|
||||
fn, ok := schema["function"].(map[string]interface{})
|
||||
fn, ok := schema["function"].(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
name, _ := fn["name"].(string)
|
||||
desc, _ := fn["description"].(string)
|
||||
params, _ := fn["parameters"].(map[string]interface{})
|
||||
params, _ := fn["parameters"].(map[string]any)
|
||||
|
||||
definitions = append(definitions, providers.ToolDefinition{
|
||||
Type: "function",
|
||||
|
||||
@@ -192,7 +192,7 @@ func TestToolResultJSONStructure(t *testing.T) {
|
||||
}
|
||||
|
||||
// Verify JSON structure
|
||||
var parsed map[string]interface{}
|
||||
var parsed map[string]any
|
||||
if err := json.Unmarshal(data, &parsed); err != nil {
|
||||
t.Fatalf("Failed to parse JSON: %v", err)
|
||||
}
|
||||
|
||||
+6
-6
@@ -118,15 +118,15 @@ func (t *ExecTool) Description() string {
|
||||
return "Execute a shell command and return its output. Use with caution."
|
||||
}
|
||||
|
||||
func (t *ExecTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func (t *ExecTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"command": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"command": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The shell command to execute",
|
||||
},
|
||||
"working_dir": map[string]interface{}{
|
||||
"working_dir": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Optional working directory for the command",
|
||||
},
|
||||
@@ -135,7 +135,7 @@ func (t *ExecTool) Parameters() map[string]interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ExecTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
||||
func (t *ExecTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||
command, ok := args["command"].(string)
|
||||
if !ok {
|
||||
return ErrorResult("command is required")
|
||||
|
||||
+15
-11
@@ -14,7 +14,7 @@ func TestShellTool_Success(t *testing.T) {
|
||||
tool := NewExecTool("", false)
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"command": "echo 'hello world'",
|
||||
}
|
||||
|
||||
@@ -41,7 +41,7 @@ func TestShellTool_Failure(t *testing.T) {
|
||||
tool := NewExecTool("", false)
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"command": "ls /nonexistent_directory_12345",
|
||||
}
|
||||
|
||||
@@ -69,7 +69,7 @@ func TestShellTool_Timeout(t *testing.T) {
|
||||
tool.SetTimeout(100 * time.Millisecond)
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"command": "sleep 10",
|
||||
}
|
||||
|
||||
@@ -91,12 +91,12 @@ func TestShellTool_WorkingDir(t *testing.T) {
|
||||
// Create temp directory
|
||||
tmpDir := t.TempDir()
|
||||
testFile := filepath.Join(tmpDir, "test.txt")
|
||||
os.WriteFile(testFile, []byte("test content"), 0644)
|
||||
os.WriteFile(testFile, []byte("test content"), 0o644)
|
||||
|
||||
tool := NewExecTool("", false)
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"command": "cat test.txt",
|
||||
"working_dir": tmpDir,
|
||||
}
|
||||
@@ -117,7 +117,7 @@ func TestShellTool_DangerousCommand(t *testing.T) {
|
||||
tool := NewExecTool("", false)
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"command": "rm -rf /",
|
||||
}
|
||||
|
||||
@@ -138,7 +138,7 @@ func TestShellTool_MissingCommand(t *testing.T) {
|
||||
tool := NewExecTool("", false)
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{}
|
||||
args := map[string]any{}
|
||||
|
||||
result := tool.Execute(ctx, args)
|
||||
|
||||
@@ -153,7 +153,7 @@ func TestShellTool_StderrCapture(t *testing.T) {
|
||||
tool := NewExecTool("", false)
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"command": "sh -c 'echo stdout; echo stderr >&2'",
|
||||
}
|
||||
|
||||
@@ -174,7 +174,7 @@ func TestShellTool_OutputTruncation(t *testing.T) {
|
||||
|
||||
ctx := context.Background()
|
||||
// Generate long output (>10000 chars)
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"command": "python3 -c \"print('x' * 20000)\" || echo " + strings.Repeat("x", 20000),
|
||||
}
|
||||
|
||||
@@ -193,7 +193,7 @@ func TestShellTool_RestrictToWorkspace(t *testing.T) {
|
||||
tool.SetRestrictToWorkspace(true)
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"command": "cat ../../etc/passwd",
|
||||
}
|
||||
|
||||
@@ -205,6 +205,10 @@ func TestShellTool_RestrictToWorkspace(t *testing.T) {
|
||||
}
|
||||
|
||||
if !strings.Contains(result.ForLLM, "blocked") && !strings.Contains(result.ForUser, "blocked") {
|
||||
t.Errorf("Expected 'blocked' message for path traversal, got ForLLM: %s, ForUser: %s", result.ForLLM, result.ForUser)
|
||||
t.Errorf(
|
||||
"Expected 'blocked' message for path traversal, got ForLLM: %s, ForUser: %s",
|
||||
result.ForLLM,
|
||||
result.ForUser,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
+7
-7
@@ -34,19 +34,19 @@ func (t *SpawnTool) Description() string {
|
||||
return "Spawn a subagent to handle a task in the background. Use this for complex or time-consuming tasks that can run independently. The subagent will complete the task and report back when done."
|
||||
}
|
||||
|
||||
func (t *SpawnTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func (t *SpawnTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"task": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"task": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The task for subagent to complete",
|
||||
},
|
||||
"label": map[string]interface{}{
|
||||
"label": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Optional short label for the task (for display)",
|
||||
},
|
||||
"agent_id": map[string]interface{}{
|
||||
"agent_id": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Optional target agent ID to delegate the task to",
|
||||
},
|
||||
@@ -64,7 +64,7 @@ func (t *SpawnTool) SetAllowlistChecker(check func(targetAgentID string) bool) {
|
||||
t.allowlistCheck = check
|
||||
}
|
||||
|
||||
func (t *SpawnTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
||||
func (t *SpawnTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||
task, ok := args["task"].(string)
|
||||
if !ok {
|
||||
return ErrorResult("task is required")
|
||||
|
||||
+17
-15
@@ -24,41 +24,41 @@ func (t *SPITool) Description() string {
|
||||
return "Interact with SPI bus devices for high-speed peripheral communication. Actions: list (find SPI devices), transfer (full-duplex send/receive), read (receive bytes). Linux only."
|
||||
}
|
||||
|
||||
func (t *SPITool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func (t *SPITool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"action": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"action": map[string]any{
|
||||
"type": "string",
|
||||
"enum": []string{"list", "transfer", "read"},
|
||||
"description": "Action to perform: list (find available SPI devices), transfer (full-duplex send/receive), read (receive bytes by sending zeros)",
|
||||
},
|
||||
"device": map[string]interface{}{
|
||||
"device": map[string]any{
|
||||
"type": "string",
|
||||
"description": "SPI device identifier (e.g. \"2.0\" for /dev/spidev2.0). Required for transfer/read.",
|
||||
},
|
||||
"speed": map[string]interface{}{
|
||||
"speed": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "SPI clock speed in Hz. Default: 1000000 (1 MHz).",
|
||||
},
|
||||
"mode": map[string]interface{}{
|
||||
"mode": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "SPI mode (0-3). Default: 0. Mode sets CPOL and CPHA: 0=0,0 1=0,1 2=1,0 3=1,1.",
|
||||
},
|
||||
"bits": map[string]interface{}{
|
||||
"bits": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "Bits per word. Default: 8.",
|
||||
},
|
||||
"data": map[string]interface{}{
|
||||
"data": map[string]any{
|
||||
"type": "array",
|
||||
"items": map[string]interface{}{"type": "integer"},
|
||||
"items": map[string]any{"type": "integer"},
|
||||
"description": "Bytes to send (0-255 each). Required for transfer action.",
|
||||
},
|
||||
"length": map[string]interface{}{
|
||||
"length": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "Number of bytes to read (1-4096). Required for read action.",
|
||||
},
|
||||
"confirm": map[string]interface{}{
|
||||
"confirm": map[string]any{
|
||||
"type": "boolean",
|
||||
"description": "Must be true for transfer operations. Safety guard to prevent accidental writes.",
|
||||
},
|
||||
@@ -67,7 +67,7 @@ func (t *SPITool) Parameters() map[string]interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *SPITool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
||||
func (t *SPITool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||
if runtime.GOOS != "linux" {
|
||||
return ErrorResult("SPI is only supported on Linux. This tool requires /dev/spidev* device files.")
|
||||
}
|
||||
@@ -97,7 +97,9 @@ func (t *SPITool) list() *ToolResult {
|
||||
}
|
||||
|
||||
if len(matches) == 0 {
|
||||
return SilentResult("No SPI devices found. You may need to:\n1. Enable SPI in device tree\n2. Configure pinmux for your board (see hardware skill)\n3. Check that spidev module is loaded")
|
||||
return SilentResult(
|
||||
"No SPI devices found. You may need to:\n1. Enable SPI in device tree\n2. Configure pinmux for your board (see hardware skill)\n3. Check that spidev module is loaded",
|
||||
)
|
||||
}
|
||||
|
||||
type devInfo struct {
|
||||
@@ -118,7 +120,7 @@ func (t *SPITool) list() *ToolResult {
|
||||
}
|
||||
|
||||
// parseSPIArgs extracts and validates common SPI parameters
|
||||
func parseSPIArgs(args map[string]interface{}) (device string, speed uint32, mode uint8, bits uint8, errMsg string) {
|
||||
func parseSPIArgs(args map[string]any) (device string, speed uint32, mode uint8, bits uint8, errMsg string) {
|
||||
dev, ok := args["device"].(string)
|
||||
if !ok || dev == "" {
|
||||
return "", 0, 0, 0, "device is required (e.g. \"2.0\" for /dev/spidev2.0)"
|
||||
|
||||
@@ -66,10 +66,12 @@ func configureSPI(devPath string, mode uint8, bits uint8, speed uint32) (int, *T
|
||||
}
|
||||
|
||||
// transfer performs a full-duplex SPI transfer
|
||||
func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
|
||||
func (t *SPITool) transfer(args map[string]any) *ToolResult {
|
||||
confirm, _ := args["confirm"].(bool)
|
||||
if !confirm {
|
||||
return ErrorResult("transfer operations require confirm: true. Please confirm with the user before sending data to SPI devices.")
|
||||
return ErrorResult(
|
||||
"transfer operations require confirm: true. Please confirm with the user before sending data to SPI devices.",
|
||||
)
|
||||
}
|
||||
|
||||
dev, speed, mode, bits, errMsg := parseSPIArgs(args)
|
||||
@@ -77,7 +79,7 @@ func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
|
||||
return ErrorResult(errMsg)
|
||||
}
|
||||
|
||||
dataRaw, ok := args["data"].([]interface{})
|
||||
dataRaw, ok := args["data"].([]any)
|
||||
if !ok || len(dataRaw) == 0 {
|
||||
return ErrorResult("data is required for transfer (array of byte values 0-255)")
|
||||
}
|
||||
@@ -130,7 +132,7 @@ func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
|
||||
intBytes[i] = int(b)
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(map[string]interface{}{
|
||||
result, _ := json.MarshalIndent(map[string]any{
|
||||
"device": devPath,
|
||||
"sent": len(txBuf),
|
||||
"received": intBytes,
|
||||
@@ -140,7 +142,7 @@ func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
|
||||
}
|
||||
|
||||
// readDevice reads bytes from SPI by sending zeros (read-only, no confirm needed)
|
||||
func (t *SPITool) readDevice(args map[string]interface{}) *ToolResult {
|
||||
func (t *SPITool) readDevice(args map[string]any) *ToolResult {
|
||||
dev, speed, mode, bits, errMsg := parseSPIArgs(args)
|
||||
if errMsg != "" {
|
||||
return ErrorResult(errMsg)
|
||||
@@ -186,7 +188,7 @@ func (t *SPITool) readDevice(args map[string]interface{}) *ToolResult {
|
||||
intBytes[i] = int(b)
|
||||
}
|
||||
|
||||
result, _ := json.MarshalIndent(map[string]interface{}{
|
||||
result, _ := json.MarshalIndent(map[string]any{
|
||||
"device": devPath,
|
||||
"bytes": intBytes,
|
||||
"hex": hexBytes,
|
||||
|
||||
@@ -3,11 +3,11 @@
|
||||
package tools
|
||||
|
||||
// transfer is a stub for non-Linux platforms.
|
||||
func (t *SPITool) transfer(args map[string]interface{}) *ToolResult {
|
||||
func (t *SPITool) transfer(args map[string]any) *ToolResult {
|
||||
return ErrorResult("SPI is only supported on Linux")
|
||||
}
|
||||
|
||||
// readDevice is a stub for non-Linux platforms.
|
||||
func (t *SPITool) readDevice(args map[string]interface{}) *ToolResult {
|
||||
func (t *SPITool) readDevice(args map[string]any) *ToolResult {
|
||||
return ErrorResult("SPI is only supported on Linux")
|
||||
}
|
||||
|
||||
+22
-10
@@ -34,7 +34,11 @@ type SubagentManager struct {
|
||||
nextID int
|
||||
}
|
||||
|
||||
func NewSubagentManager(provider providers.LLMProvider, defaultModel, workspace string, bus *bus.MessageBus) *SubagentManager {
|
||||
func NewSubagentManager(
|
||||
provider providers.LLMProvider,
|
||||
defaultModel, workspace string,
|
||||
bus *bus.MessageBus,
|
||||
) *SubagentManager {
|
||||
return &SubagentManager{
|
||||
tasks: make(map[string]*SubagentTask),
|
||||
provider: provider,
|
||||
@@ -62,7 +66,11 @@ func (sm *SubagentManager) RegisterTool(tool Tool) {
|
||||
sm.tools.Register(tool)
|
||||
}
|
||||
|
||||
func (sm *SubagentManager) Spawn(ctx context.Context, task, label, agentID, originChannel, originChatID string, callback AsyncCallback) (string, error) {
|
||||
func (sm *SubagentManager) Spawn(
|
||||
ctx context.Context,
|
||||
task, label, agentID, originChannel, originChatID string,
|
||||
callback AsyncCallback,
|
||||
) (string, error) {
|
||||
sm.mu.Lock()
|
||||
defer sm.mu.Unlock()
|
||||
|
||||
@@ -168,7 +176,12 @@ After completing the task, provide a clear summary of what was done.`
|
||||
task.Status = "completed"
|
||||
task.Result = loopResult.Content
|
||||
result = &ToolResult{
|
||||
ForLLM: fmt.Sprintf("Subagent '%s' completed (iterations: %d): %s", task.Label, loopResult.Iterations, loopResult.Content),
|
||||
ForLLM: fmt.Sprintf(
|
||||
"Subagent '%s' completed (iterations: %d): %s",
|
||||
task.Label,
|
||||
loopResult.Iterations,
|
||||
loopResult.Content,
|
||||
),
|
||||
ForUser: loopResult.Content,
|
||||
Silent: false,
|
||||
IsError: false,
|
||||
@@ -232,15 +245,15 @@ func (t *SubagentTool) Description() string {
|
||||
return "Execute a subagent task synchronously and return the result. Use this for delegating specific tasks to an independent agent instance. Returns execution summary to user and full details to LLM."
|
||||
}
|
||||
|
||||
func (t *SubagentTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func (t *SubagentTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"task": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"task": map[string]any{
|
||||
"type": "string",
|
||||
"description": "The task for subagent to complete",
|
||||
},
|
||||
"label": map[string]interface{}{
|
||||
"label": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Optional short label for the task (for display)",
|
||||
},
|
||||
@@ -254,7 +267,7 @@ func (t *SubagentTool) SetContext(channel, chatID string) {
|
||||
t.originChatID = chatID
|
||||
}
|
||||
|
||||
func (t *SubagentTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
||||
func (t *SubagentTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||
task, ok := args["task"].(string)
|
||||
if !ok {
|
||||
return ErrorResult("task is required").WithError(fmt.Errorf("task parameter is required"))
|
||||
@@ -295,7 +308,6 @@ func (t *SubagentTool) Execute(ctx context.Context, args map[string]interface{})
|
||||
"temperature": 0.7,
|
||||
},
|
||||
}, messages, t.originChannel, t.originChatID)
|
||||
|
||||
if err != nil {
|
||||
return ErrorResult(fmt.Sprintf("Subagent execution failed: %v", err)).WithError(err)
|
||||
}
|
||||
|
||||
@@ -12,7 +12,13 @@ import (
|
||||
// MockLLMProvider is a test implementation of LLMProvider
|
||||
type MockLLMProvider struct{}
|
||||
|
||||
func (m *MockLLMProvider) Chat(ctx context.Context, messages []providers.Message, tools []providers.ToolDefinition, model string, options map[string]interface{}) (*providers.LLMResponse, error) {
|
||||
func (m *MockLLMProvider) Chat(
|
||||
ctx context.Context,
|
||||
messages []providers.Message,
|
||||
tools []providers.ToolDefinition,
|
||||
model string,
|
||||
options map[string]any,
|
||||
) (*providers.LLMResponse, error) {
|
||||
// Find the last user message to generate a response
|
||||
for i := len(messages) - 1; i >= 0; i-- {
|
||||
if messages[i].Role == "user" {
|
||||
@@ -79,13 +85,13 @@ func TestSubagentTool_Parameters(t *testing.T) {
|
||||
}
|
||||
|
||||
// Check properties
|
||||
props, ok := params["properties"].(map[string]interface{})
|
||||
props, ok := params["properties"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("Properties should be a map")
|
||||
}
|
||||
|
||||
// Verify task parameter
|
||||
task, ok := props["task"].(map[string]interface{})
|
||||
task, ok := props["task"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("Task parameter should exist")
|
||||
}
|
||||
@@ -94,7 +100,7 @@ func TestSubagentTool_Parameters(t *testing.T) {
|
||||
}
|
||||
|
||||
// Verify label parameter
|
||||
label, ok := props["label"].(map[string]interface{})
|
||||
label, ok := props["label"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatal("Label parameter should exist")
|
||||
}
|
||||
@@ -134,7 +140,7 @@ func TestSubagentTool_Execute_Success(t *testing.T) {
|
||||
tool.SetContext("telegram", "chat-123")
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"task": "Write a haiku about coding",
|
||||
"label": "haiku-task",
|
||||
}
|
||||
@@ -189,7 +195,7 @@ func TestSubagentTool_Execute_NoLabel(t *testing.T) {
|
||||
tool := NewSubagentTool(manager)
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"task": "Test task without label",
|
||||
}
|
||||
|
||||
@@ -212,7 +218,7 @@ func TestSubagentTool_Execute_MissingTask(t *testing.T) {
|
||||
tool := NewSubagentTool(manager)
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"label": "test",
|
||||
}
|
||||
|
||||
@@ -239,7 +245,7 @@ func TestSubagentTool_Execute_NilManager(t *testing.T) {
|
||||
tool := NewSubagentTool(nil)
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"task": "test task",
|
||||
}
|
||||
|
||||
@@ -268,7 +274,7 @@ func TestSubagentTool_Execute_ContextPassing(t *testing.T) {
|
||||
tool.SetContext(channel, chatID)
|
||||
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"task": "Test context passing",
|
||||
}
|
||||
|
||||
@@ -295,7 +301,7 @@ func TestSubagentTool_ForUserTruncation(t *testing.T) {
|
||||
|
||||
// Create a task that will generate long response
|
||||
longTask := strings.Repeat("This is a very long task description. ", 100)
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"task": longTask,
|
||||
"label": "long-test",
|
||||
}
|
||||
|
||||
@@ -33,7 +33,12 @@ type ToolLoopResult struct {
|
||||
|
||||
// RunToolLoop executes the LLM + tool call iteration loop.
|
||||
// This is the core agent logic that can be reused by both main agent and subagents.
|
||||
func RunToolLoop(ctx context.Context, config ToolLoopConfig, messages []providers.Message, channel, chatID string) (*ToolLoopResult, error) {
|
||||
func RunToolLoop(
|
||||
ctx context.Context,
|
||||
config ToolLoopConfig,
|
||||
messages []providers.Message,
|
||||
channel, chatID string,
|
||||
) (*ToolLoopResult, error) {
|
||||
iteration := 0
|
||||
var finalContent string
|
||||
|
||||
|
||||
+15
-9
@@ -10,11 +10,11 @@ type Message struct {
|
||||
}
|
||||
|
||||
type ToolCall struct {
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Function *FunctionCall `json:"function,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Arguments map[string]interface{} `json:"arguments,omitempty"`
|
||||
ID string `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Function *FunctionCall `json:"function,omitempty"`
|
||||
Name string `json:"name,omitempty"`
|
||||
Arguments map[string]any `json:"arguments,omitempty"`
|
||||
}
|
||||
|
||||
type FunctionCall struct {
|
||||
@@ -36,7 +36,13 @@ type UsageInfo struct {
|
||||
}
|
||||
|
||||
type LLMProvider interface {
|
||||
Chat(ctx context.Context, messages []Message, tools []ToolDefinition, model string, options map[string]interface{}) (*LLMResponse, error)
|
||||
Chat(
|
||||
ctx context.Context,
|
||||
messages []Message,
|
||||
tools []ToolDefinition,
|
||||
model string,
|
||||
options map[string]any,
|
||||
) (*LLMResponse, error)
|
||||
GetDefaultModel() string
|
||||
}
|
||||
|
||||
@@ -46,7 +52,7 @@ type ToolDefinition struct {
|
||||
}
|
||||
|
||||
type ToolFunctionDefinition struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters map[string]interface{} `json:"parameters"`
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
Parameters map[string]any `json:"parameters"`
|
||||
}
|
||||
|
||||
+30
-18
@@ -183,11 +183,17 @@ type PerplexitySearchProvider struct {
|
||||
func (p *PerplexitySearchProvider) Search(ctx context.Context, query string, count int) (string, error) {
|
||||
searchURL := "https://api.perplexity.ai/chat/completions"
|
||||
|
||||
payload := map[string]interface{}{
|
||||
payload := map[string]any{
|
||||
"model": "sonar",
|
||||
"messages": []map[string]string{
|
||||
{"role": "system", "content": "You are a search assistant. Provide concise search results with titles, URLs, and brief descriptions in the following format:\n1. Title\n URL\n Description\n\nDo not add extra commentary."},
|
||||
{"role": "user", "content": fmt.Sprintf("Search for: %s. Provide up to %d relevant results.", query, count)},
|
||||
{
|
||||
"role": "system",
|
||||
"content": "You are a search assistant. Provide concise search results with titles, URLs, and brief descriptions in the following format:\n1. Title\n URL\n Description\n\nDo not add extra commentary.",
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": fmt.Sprintf("Search for: %s. Provide up to %d relevant results.", query, count),
|
||||
},
|
||||
},
|
||||
"max_tokens": 1000,
|
||||
}
|
||||
@@ -295,15 +301,15 @@ func (t *WebSearchTool) Description() string {
|
||||
return "Search the web for current information. Returns titles, URLs, and snippets from search results."
|
||||
}
|
||||
|
||||
func (t *WebSearchTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func (t *WebSearchTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"query": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"query": map[string]any{
|
||||
"type": "string",
|
||||
"description": "Search query",
|
||||
},
|
||||
"count": map[string]interface{}{
|
||||
"count": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "Number of results (1-10)",
|
||||
"minimum": 1.0,
|
||||
@@ -314,7 +320,7 @@ func (t *WebSearchTool) Parameters() map[string]interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *WebSearchTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
||||
func (t *WebSearchTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||
query, ok := args["query"].(string)
|
||||
if !ok {
|
||||
return ErrorResult("query is required")
|
||||
@@ -359,15 +365,15 @@ func (t *WebFetchTool) Description() string {
|
||||
return "Fetch a URL and extract readable content (HTML to text). Use this to get weather info, news, articles, or any web content."
|
||||
}
|
||||
|
||||
func (t *WebFetchTool) Parameters() map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
func (t *WebFetchTool) Parameters() map[string]any {
|
||||
return map[string]any{
|
||||
"type": "object",
|
||||
"properties": map[string]interface{}{
|
||||
"url": map[string]interface{}{
|
||||
"properties": map[string]any{
|
||||
"url": map[string]any{
|
||||
"type": "string",
|
||||
"description": "URL to fetch",
|
||||
},
|
||||
"maxChars": map[string]interface{}{
|
||||
"maxChars": map[string]any{
|
||||
"type": "integer",
|
||||
"description": "Maximum characters to extract",
|
||||
"minimum": 100.0,
|
||||
@@ -377,7 +383,7 @@ func (t *WebFetchTool) Parameters() map[string]interface{} {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *WebFetchTool) Execute(ctx context.Context, args map[string]interface{}) *ToolResult {
|
||||
func (t *WebFetchTool) Execute(ctx context.Context, args map[string]any) *ToolResult {
|
||||
urlStr, ok := args["url"].(string)
|
||||
if !ok {
|
||||
return ErrorResult("url is required")
|
||||
@@ -442,7 +448,7 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]interface{})
|
||||
var text, extractor string
|
||||
|
||||
if strings.Contains(contentType, "application/json") {
|
||||
var jsonData interface{}
|
||||
var jsonData any
|
||||
if err := json.Unmarshal(body, &jsonData); err == nil {
|
||||
formatted, _ := json.MarshalIndent(jsonData, "", " ")
|
||||
text = string(formatted)
|
||||
@@ -465,7 +471,7 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]interface{})
|
||||
text = text[:maxChars]
|
||||
}
|
||||
|
||||
result := map[string]interface{}{
|
||||
result := map[string]any{
|
||||
"url": urlStr,
|
||||
"status": resp.StatusCode,
|
||||
"extractor": extractor,
|
||||
@@ -477,7 +483,13 @@ func (t *WebFetchTool) Execute(ctx context.Context, args map[string]interface{})
|
||||
resultJSON, _ := json.MarshalIndent(result, "", " ")
|
||||
|
||||
return &ToolResult{
|
||||
ForLLM: fmt.Sprintf("Fetched %d bytes from %s (extractor: %s, truncated: %v)", len(text), urlStr, extractor, truncated),
|
||||
ForLLM: fmt.Sprintf(
|
||||
"Fetched %d bytes from %s (extractor: %s, truncated: %v)",
|
||||
len(text),
|
||||
urlStr,
|
||||
extractor,
|
||||
truncated,
|
||||
),
|
||||
ForUser: string(resultJSON),
|
||||
}
|
||||
}
|
||||
|
||||
+15
-11
@@ -20,7 +20,7 @@ func TestWebTool_WebFetch_Success(t *testing.T) {
|
||||
|
||||
tool := NewWebFetchTool(50000)
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"url": server.URL,
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func TestWebTool_WebFetch_JSON(t *testing.T) {
|
||||
|
||||
tool := NewWebFetchTool(50000)
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"url": server.URL,
|
||||
}
|
||||
|
||||
@@ -77,7 +77,7 @@ func TestWebTool_WebFetch_JSON(t *testing.T) {
|
||||
func TestWebTool_WebFetch_InvalidURL(t *testing.T) {
|
||||
tool := NewWebFetchTool(50000)
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"url": "not-a-valid-url",
|
||||
}
|
||||
|
||||
@@ -98,7 +98,7 @@ func TestWebTool_WebFetch_InvalidURL(t *testing.T) {
|
||||
func TestWebTool_WebFetch_UnsupportedScheme(t *testing.T) {
|
||||
tool := NewWebFetchTool(50000)
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"url": "ftp://example.com/file.txt",
|
||||
}
|
||||
|
||||
@@ -119,7 +119,7 @@ func TestWebTool_WebFetch_UnsupportedScheme(t *testing.T) {
|
||||
func TestWebTool_WebFetch_MissingURL(t *testing.T) {
|
||||
tool := NewWebFetchTool(50000)
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{}
|
||||
args := map[string]any{}
|
||||
|
||||
result := tool.Execute(ctx, args)
|
||||
|
||||
@@ -147,7 +147,7 @@ func TestWebTool_WebFetch_Truncation(t *testing.T) {
|
||||
|
||||
tool := NewWebFetchTool(1000) // Limit to 1000 chars
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"url": server.URL,
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ func TestWebTool_WebFetch_Truncation(t *testing.T) {
|
||||
}
|
||||
|
||||
// ForUser should contain truncated content (not the full 20000 chars)
|
||||
resultMap := make(map[string]interface{})
|
||||
resultMap := make(map[string]any)
|
||||
json.Unmarshal([]byte(result.ForUser), &resultMap)
|
||||
if text, ok := resultMap["text"].(string); ok {
|
||||
if len(text) > 1100 { // Allow some margin
|
||||
@@ -191,7 +191,7 @@ func TestWebTool_WebSearch_NoApiKey(t *testing.T) {
|
||||
func TestWebTool_WebSearch_MissingQuery(t *testing.T) {
|
||||
tool := NewWebSearchTool(WebSearchToolOptions{BraveEnabled: true, BraveAPIKey: "test-key", BraveMaxResults: 5})
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{}
|
||||
args := map[string]any{}
|
||||
|
||||
result := tool.Execute(ctx, args)
|
||||
|
||||
@@ -206,13 +206,17 @@ func TestWebTool_WebFetch_HTMLExtraction(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html")
|
||||
w.WriteHeader(http.StatusOK)
|
||||
w.Write([]byte(`<html><body><script>alert('test');</script><style>body{color:red;}</style><h1>Title</h1><p>Content</p></body></html>`))
|
||||
w.Write(
|
||||
[]byte(
|
||||
`<html><body><script>alert('test');</script><style>body{color:red;}</style><h1>Title</h1><p>Content</p></body></html>`,
|
||||
),
|
||||
)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
tool := NewWebFetchTool(50000)
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"url": server.URL,
|
||||
}
|
||||
|
||||
@@ -238,7 +242,7 @@ func TestWebTool_WebFetch_HTMLExtraction(t *testing.T) {
|
||||
func TestWebTool_WebFetch_MissingDomain(t *testing.T) {
|
||||
tool := NewWebFetchTool(50000)
|
||||
ctx := context.Background()
|
||||
args := map[string]interface{}{
|
||||
args := map[string]any{
|
||||
"url": "https://",
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user