Commit f5184a0

mo khan <mo@mokhan.ca>
2025-06-22 02:27:06
feat: try to hook up tools
1 parent 84d8c20
Changed files (2)
internal/del/assistant.go
@@ -5,90 +5,159 @@ import (
 	"context"
 	"encoding/json"
 	"fmt"
+	"io/ioutil"
 	"os"
+	"os/exec"
+	"regexp"
 	"strings"
 )
 
+// Tool represents a tool Del can run
+type Tool struct {
+	Name        string
+	Description string
+	Handler     func(args map[string]interface{}) (interface{}, error)
+}
+
+// Del represents the assistant instance
 type Del struct {
 	aiProvider AIProvider
 	tools      map[string]*Tool
+	mcpServers map[string]string
 }
 
-func NewDel(ai AIProvider) *Del {
-	d := &Del{
-		aiProvider: ai,
-		tools:      map[string]*Tool{},
+// NewDel creates a new assistant
+func NewDel(provider AIProvider) *Del {
+	tools := map[string]*Tool{
+		"run_command": {
+			Name:        "run_command",
+			Description: "Execute a shell command and return the output",
+			Handler: func(args map[string]interface{}) (interface{}, error) {
+				cmd, ok := args["cmd"].(string)
+				if !ok {
+					return nil, fmt.Errorf("missing 'cmd' string argument")
+				}
+				out, err := exec.Command("bash", "-c", cmd).CombinedOutput()
+				if err != nil {
+					return nil, fmt.Errorf("command failed: %v\n%s", err, string(out))
+				}
+				return string(out), nil
+			},
+		},
+		"read_file": {
+			Name:        "read_file",
+			Description: "Reads the contents of a file",
+			Handler: func(args map[string]interface{}) (interface{}, error) {
+				path, ok := args["path"].(string)
+				if !ok {
+					return nil, fmt.Errorf("missing 'path' string argument")
+				}
+				data, err := ioutil.ReadFile(path)
+				if err != nil {
+					return nil, err
+				}
+				return string(data), nil
+			},
+		},
+		"write_file": {
+			Name:        "write_file",
+			Description: "Writes content to a file",
+			Handler: func(args map[string]interface{}) (interface{}, error) {
+				path, ok1 := args["path"].(string)
+				content, ok2 := args["content"].(string)
+				if !ok1 || !ok2 {
+					return nil, fmt.Errorf("missing 'path' or 'content' argument")
+				}
+				if err := ioutil.WriteFile(path, []byte(content), 0644); err != nil {
+					return nil, err
+				}
+				return fmt.Sprintf("wrote %d bytes to %s", len(content), path), nil
+			},
+		},
+		"list_dir": {
+			Name:        "list_dir",
+			Description: "Lists files in a directory",
+			Handler: func(args map[string]interface{}) (interface{}, error) {
+				dir, ok := args["path"].(string)
+				if !ok {
+					dir = "."
+				}
+				entries, err := os.ReadDir(dir)
+				if err != nil {
+					return nil, err
+				}
+				var names []string
+				for _, entry := range entries {
+					names = append(names, entry.Name())
+				}
+				return names, nil
+			},
+		},
+	}
+
+	return &Del{
+		aiProvider: provider,
+		tools:      tools,
+		mcpServers: make(map[string]string),
 	}
-	d.registerTools()
-	return d
 }
 
+// StartREPL launches an interactive session
 func (d *Del) StartREPL(ctx context.Context) {
-	fmt.Println("๐ŸŽค Del is ready with", d.aiProvider.Name())
+	fmt.Printf("๐ŸŽค Del is ready with %s\n", d.aiProvider.Name())
 	scanner := bufio.NewScanner(os.Stdin)
-
 	for {
 		fmt.Print("๐ŸŽค You: ")
 		if !scanner.Scan() {
 			break
 		}
-		line := scanner.Text()
-		if strings.HasPrefix(line, "/") {
-			d.handleSlash(line)
-			continue
+		input := scanner.Text()
+		if strings.HasPrefix(input, "/quit") {
+			fmt.Println("๐Ÿ‘‹ Goodbye!")
+			return
 		}
 
-		prompt := d.buildPrompt(line)
-		response, err := d.aiProvider.Generate(ctx, prompt)
+		resp, err := d.aiProvider.Generate(ctx, input)
 		if err != nil {
-			fmt.Println("โŒ Error:", err)
+			fmt.Println("[error]", err)
 			continue
 		}
-
-		fmt.Println("๐Ÿค– Del:", response)
-		d.handleToolResponse(response)
+		output := d.handleToolCalls(ctx, resp)
+		fmt.Println(output)
 	}
 }
 
-func (d *Del) handleSlash(line string) {
-	fields := strings.Fields(line)
-	switch fields[0] {
-	case "/quit":
-		fmt.Println("๐Ÿ‘‹ Bye!")
-		os.Exit(0)
-	case "/model":
-		fmt.Println("Current model:", d.aiProvider.Name())
-	default:
-		fmt.Println("โ“ Unknown command:", line)
-	}
-}
+// handleToolCalls parses output for TOOL_USE and runs the associated tool.
+func (d *Del) handleToolCalls(ctx context.Context, response string) string {
+	toolUseRE := regexp.MustCompile(`(?m)^TOOL_USE:\s*(\w+)\s*(\{.*\})`)
+	matches := toolUseRE.FindAllStringSubmatch(response, -1)
+
+	var results []string
+	for _, match := range matches {
+		name, jsonArgs := match[1], match[2]
+		tool, ok := d.tools[name]
+		if !ok {
+			results = append(results, fmt.Sprintf("[error] tool '%s' not found", name))
+			continue
+		}
+
+		var args map[string]interface{}
+		if err := json.Unmarshal([]byte(jsonArgs), &args); err != nil {
+			results = append(results, fmt.Sprintf("[error] invalid JSON args for tool '%s': %v", name, err))
+			continue
+		}
 
-func (d *Del) handleToolResponse(response string) {
-	for _, line := range strings.Split(response, " ") {
-		if strings.HasPrefix(line, "TOOL_USE:") {
-			parts := strings.SplitN(line[len("TOOL_USE:"):], " ", 2)
-			if len(parts) != 2 {
-				fmt.Println("โŒ Malformed tool call:", line)
-				continue
-			}
-			name := strings.TrimSpace(parts[0])
-			argsJSON := strings.TrimSpace(parts[1])
-			tool, ok := d.tools[name]
-			if !ok {
-				fmt.Println("โŒ Unknown tool:", name)
-				continue
-			}
-			var args map[string]interface{}
-			if err := json.Unmarshal([]byte(argsJSON), &args); err != nil {
-				fmt.Println("โŒ JSON error:", err)
-				continue
-			}
-			result, err := tool.Handler(args)
-			if err != nil {
-				fmt.Println("โŒ Tool error:", err)
-			} else {
-				fmt.Printf("โœ… %s result: %+v ", name, result)
-			}
+		result, err := tool.Handler(args)
+		if err != nil {
+			results = append(results, fmt.Sprintf("[error] tool '%s' failed: %v", name, err))
+			continue
 		}
+		results = append(results, fmt.Sprintf("[tool:%s] %v", name, result))
 	}
+
+	if len(results) == 0 {
+		return response
+	}
+
+	return strings.TrimSpace(response) + "\n\n" + strings.Join(results, "\n")
 }
internal/del/tools.go
@@ -1,66 +1,60 @@
 package del
 
 import (
-    "fmt"
-    "os"
-    "os/exec"
+	"fmt"
+	"os"
+	"os/exec"
 )
 
-type Tool struct {
-    Name        string
-    Description string
-    Handler     func(args map[string]interface{}) (interface{}, error)
-}
-
 func (d *Del) registerTools() {
-    d.tools["read_file"] = &Tool{"read_file", "Read a file from disk", d.readFile}
-    d.tools["write_file"] = &Tool{"write_file", "Write content to a file", d.writeFile}
-    d.tools["run_command"] = &Tool{"run_command", "Execute a shell command", d.runCommand}
-    d.tools["analyze_code"] = &Tool{"analyze_code", "Analyze code quality", d.analyzeCode}
-    d.tools["mcp_git_list"] = &Tool{"mcp_git_list", "MCP: List repo files", d.stubMCP}
-    d.tools["mcp_git_read"] = &Tool{"mcp_git_read", "MCP: Read repo file", d.stubMCP}
+	d.tools["read_file"] = &Tool{"read_file", "Read a file from disk", d.readFile}
+	d.tools["write_file"] = &Tool{"write_file", "Write content to a file", d.writeFile}
+	d.tools["run_command"] = &Tool{"run_command", "Execute a shell command", d.runCommand}
+	d.tools["analyze_code"] = &Tool{"analyze_code", "Analyze code quality", d.analyzeCode}
+	d.tools["mcp_git_list"] = &Tool{"mcp_git_list", "MCP: List repo files", d.stubMCP}
+	d.tools["mcp_git_read"] = &Tool{"mcp_git_read", "MCP: Read repo file", d.stubMCP}
 }
 
 func (d *Del) readFile(args map[string]interface{}) (interface{}, error) {
-    path, ok := args["path"].(string)
-    if !ok {
-        return nil, fmt.Errorf("missing 'path'")
-    }
-    data, err := os.ReadFile(path)
-    if err != nil {
-        return nil, err
-    }
-    return string(data), nil
+	path, ok := args["path"].(string)
+	if !ok {
+		return nil, fmt.Errorf("missing 'path'")
+	}
+	data, err := os.ReadFile(path)
+	if err != nil {
+		return nil, err
+	}
+	return string(data), nil
 }
 
 func (d *Del) writeFile(args map[string]interface{}) (interface{}, error) {
-    path, ok := args["path"].(string)
-    content, ok2 := args["content"].(string)
-    if !ok || !ok2 {
-        return nil, fmt.Errorf("missing 'path' or 'content'")
-    }
-    return "written", os.WriteFile(path, []byte(content), 0644)
+	path, ok := args["path"].(string)
+	content, ok2 := args["content"].(string)
+	if !ok || !ok2 {
+		return nil, fmt.Errorf("missing 'path' or 'content'")
+	}
+	return "written", os.WriteFile(path, []byte(content), 0644)
 }
 
 func (d *Del) runCommand(args map[string]interface{}) (interface{}, error) {
-    command, ok := args["command"].(string)
-    if !ok {
-        return nil, fmt.Errorf("missing 'command'")
-    }
-    output, err := exec.Command("sh", "-c", command).CombinedOutput()
-    return string(output), err
+	command, ok := args["command"].(string)
+	if !ok {
+		return nil, fmt.Errorf("missing 'command'")
+	}
+	output, err := exec.Command("sh", "-c", command).CombinedOutput()
+	return string(output), err
 }
 
 func (d *Del) analyzeCode(args map[string]interface{}) (interface{}, error) {
-    return map[string]interface{}{
-        "quality": "good",
-        "suggestions": []string{
-            "Consider breaking large functions into smaller ones.",
-            "Add more comments for readability.",
-        },
-    }, nil
+	return map[string]interface{}{
+		"quality": "good",
+		"suggestions": []string{
+			"Consider breaking large functions into smaller ones.",
+			"Add more comments for readability.",
+		},
+	}, nil
 }
 
 func (d *Del) stubMCP(args map[string]interface{}) (interface{}, error) {
-    return map[string]string{"status": "MCP stub - not connected"}, nil
+	return map[string]string{"status": "MCP stub - not connected"}, nil
 }