Commit fbda0e5
Changed files (2)
cmd
del
cmd/del/main.go
@@ -3,7 +3,6 @@ package main
import (
"bufio"
"context"
- "encoding/json"
"flag"
"fmt"
"os"
@@ -75,15 +74,11 @@ Available tools:
- analyze_code: Analyze code structure (auto-detects files if no path provided)
- search_code: Search for patterns in code
-Use tools by generating calls in this format:
-function<|tool▁sep|>tool_name
-{"arg": "value"}
-
EXAMPLES:
-- When user says "analyze the code" or "analyze this project": use analyze_code with {} (auto-detects files)
-- When user says "list files": use list_dir with {}
-- When user says "read main.go": use read_file with {"path": "main.go"}
-- When user says "check git status": use git_status with {}
+- When user says "analyze the code" or "analyze this project": use analyze_code (auto-detects files)
+- When user says "list files": use list_dir
+- When user says "read main.go": use read_file with path "main.go"
+- When user says "check git status": use git_status
FORMATTING: Always format your responses using markdown. Use:
- ## Headers for main sections
@@ -668,84 +663,6 @@ func (d *Del) searchCode(ctx context.Context, args map[string]interface{}, progr
return result, nil
}
-// Parse universal tool calls - supports multiple formats
-func (d *Del) parseToolCalls(response string) []ToolCall {
- var calls []ToolCall
-
- // Format 1: Chinese models format
- re1 := regexp.MustCompile(`function<|tool▁sep|>(\w+)`)
- matches1 := re1.FindAllStringSubmatch(response, -1)
-
- for _, match := range matches1 {
- if len(match) >= 2 {
- toolName := match[1]
-
- startPos := strings.Index(response, match[0]) + len(match[0])
- remaining := response[startPos:]
-
- var args map[string]interface{}
- jsonStart := strings.Index(remaining, "{")
- if jsonStart != -1 {
- braceCount := 0
- jsonEnd := -1
- for i, char := range remaining[jsonStart:] {
- if char == '{' {
- braceCount++
- } else if char == '}' {
- braceCount--
- if braceCount == 0 {
- jsonEnd = jsonStart + i + 1
- break
- }
- }
- }
-
- if jsonEnd != -1 {
- jsonStr := remaining[jsonStart:jsonEnd]
- json.Unmarshal([]byte(jsonStr), &args)
- }
- }
-
- if args == nil {
- args = make(map[string]interface{})
- }
-
- calls = append(calls, ToolCall{Name: toolName, Args: args})
- }
- }
-
- // Format 2: JSON format - { "function": "tool_name", "args": {...} }
- re2 := regexp.MustCompile(`\{\s*"function"\s*:\s*"(\w+)"\s*,\s*"args"\s*:\s*(\{[^}]*\})\s*\}`)
- matches2 := re2.FindAllStringSubmatch(response, -1)
-
- for _, match := range matches2 {
- if len(match) >= 3 {
- toolName := match[1]
- argsStr := match[2]
-
- var args map[string]interface{}
- if err := json.Unmarshal([]byte(argsStr), &args); err == nil {
- calls = append(calls, ToolCall{Name: toolName, Args: args})
- }
- }
- }
-
- // Format 3: Simple JSON format - { "function": "tool_name" } without separate args
- if len(calls) == 0 {
- re3 := regexp.MustCompile(`\{\s*"function"\s*:\s*"(\w+)"\s*\}`)
- matches3 := re3.FindAllStringSubmatch(response, -1)
-
- for _, match := range matches3 {
- if len(match) >= 2 {
- toolName := match[1]
- args := make(map[string]interface{})
- calls = append(calls, ToolCall{Name: toolName, Args: args})
- }
- }
- }
-
- return calls
-}
func (d *Del) executeTool(ctx context.Context, call ToolCall) string {
d.emit(StreamMessage{
@@ -825,6 +742,212 @@ func (d *Del) streamResponseChunks(ctx context.Context, text string) {
})
}
+func (d *Del) buildOllamaTools() []api.Tool {
+ var tools []api.Tool
+
+ // Helper function to create property
+ makeProperty := func(propType string, description string) struct {
+ Type api.PropertyType `json:"type"`
+ Items any `json:"items,omitempty"`
+ Description string `json:"description"`
+ Enum []any `json:"enum,omitempty"`
+ } {
+ return struct {
+ Type api.PropertyType `json:"type"`
+ Items any `json:"items,omitempty"`
+ Description string `json:"description"`
+ Enum []any `json:"enum,omitempty"`
+ }{
+ Type: api.PropertyType{propType},
+ Description: description,
+ }
+ }
+
+ // read_file tool
+ readFileFunc := api.ToolFunction{
+ Name: "read_file",
+ Description: "Read file contents",
+ }
+ readFileFunc.Parameters.Type = "object"
+ readFileFunc.Parameters.Required = []string{"path"}
+ readFileFunc.Parameters.Properties = make(map[string]struct {
+ Type api.PropertyType `json:"type"`
+ Items any `json:"items,omitempty"`
+ Description string `json:"description"`
+ Enum []any `json:"enum,omitempty"`
+ })
+ readFileFunc.Parameters.Properties["path"] = makeProperty("string", "Path to the file to read")
+
+ tools = append(tools, api.Tool{
+ Type: "function",
+ Function: readFileFunc,
+ })
+
+ // list_dir tool
+ listDirFunc := api.ToolFunction{
+ Name: "list_dir",
+ Description: "List directory contents",
+ }
+ listDirFunc.Parameters.Type = "object"
+ listDirFunc.Parameters.Required = []string{}
+ listDirFunc.Parameters.Properties = make(map[string]struct {
+ Type api.PropertyType `json:"type"`
+ Items any `json:"items,omitempty"`
+ Description string `json:"description"`
+ Enum []any `json:"enum,omitempty"`
+ })
+ listDirFunc.Parameters.Properties["path"] = makeProperty("string", "Path to the directory to list (defaults to current directory)")
+
+ tools = append(tools, api.Tool{
+ Type: "function",
+ Function: listDirFunc,
+ })
+
+ // run_command tool
+ runCommandFunc := api.ToolFunction{
+ Name: "run_command",
+ Description: "Execute shell commands",
+ }
+ runCommandFunc.Parameters.Type = "object"
+ runCommandFunc.Parameters.Required = []string{"command"}
+ runCommandFunc.Parameters.Properties = make(map[string]struct {
+ Type api.PropertyType `json:"type"`
+ Items any `json:"items,omitempty"`
+ Description string `json:"description"`
+ Enum []any `json:"enum,omitempty"`
+ })
+ runCommandFunc.Parameters.Properties["command"] = makeProperty("string", "Shell command to execute")
+
+ tools = append(tools, api.Tool{
+ Type: "function",
+ Function: runCommandFunc,
+ })
+
+ // git_status tool
+ gitStatusFunc := api.ToolFunction{
+ Name: "git_status",
+ Description: "Check git repository status",
+ }
+ gitStatusFunc.Parameters.Type = "object"
+ gitStatusFunc.Parameters.Required = []string{}
+ gitStatusFunc.Parameters.Properties = make(map[string]struct {
+ Type api.PropertyType `json:"type"`
+ Items any `json:"items,omitempty"`
+ Description string `json:"description"`
+ Enum []any `json:"enum,omitempty"`
+ })
+
+ tools = append(tools, api.Tool{
+ Type: "function",
+ Function: gitStatusFunc,
+ })
+
+ // write_file tool
+ writeFileFunc := api.ToolFunction{
+ Name: "write_file",
+ Description: "Write content to files",
+ }
+ writeFileFunc.Parameters.Type = "object"
+ writeFileFunc.Parameters.Required = []string{"path", "content"}
+ writeFileFunc.Parameters.Properties = make(map[string]struct {
+ Type api.PropertyType `json:"type"`
+ Items any `json:"items,omitempty"`
+ Description string `json:"description"`
+ Enum []any `json:"enum,omitempty"`
+ })
+ writeFileFunc.Parameters.Properties["path"] = makeProperty("string", "Path to the file to write")
+ writeFileFunc.Parameters.Properties["content"] = makeProperty("string", "Content to write to the file")
+
+ tools = append(tools, api.Tool{
+ Type: "function",
+ Function: writeFileFunc,
+ })
+
+ // analyze_code tool
+ analyzeCodeFunc := api.ToolFunction{
+ Name: "analyze_code",
+ Description: "Analyze code structure (auto-detects files if no path provided)",
+ }
+ analyzeCodeFunc.Parameters.Type = "object"
+ analyzeCodeFunc.Parameters.Required = []string{}
+ analyzeCodeFunc.Parameters.Properties = make(map[string]struct {
+ Type api.PropertyType `json:"type"`
+ Items any `json:"items,omitempty"`
+ Description string `json:"description"`
+ Enum []any `json:"enum,omitempty"`
+ })
+ analyzeCodeFunc.Parameters.Properties["path"] = makeProperty("string", "Path to the file to analyze (optional, auto-detects if not provided)")
+ analyzeCodeFunc.Parameters.Properties["content"] = makeProperty("string", "Code content to analyze (optional)")
+ analyzeCodeFunc.Parameters.Properties["language"] = makeProperty("string", "Programming language (optional, auto-detected from file extension)")
+
+ tools = append(tools, api.Tool{
+ Type: "function",
+ Function: analyzeCodeFunc,
+ })
+
+ // search_code tool
+ searchCodeFunc := api.ToolFunction{
+ Name: "search_code",
+ Description: "Search for patterns in code",
+ }
+ searchCodeFunc.Parameters.Type = "object"
+ searchCodeFunc.Parameters.Required = []string{"pattern"}
+ searchCodeFunc.Parameters.Properties = make(map[string]struct {
+ Type api.PropertyType `json:"type"`
+ Items any `json:"items,omitempty"`
+ Description string `json:"description"`
+ Enum []any `json:"enum,omitempty"`
+ })
+ searchCodeFunc.Parameters.Properties["pattern"] = makeProperty("string", "Pattern to search for")
+ searchCodeFunc.Parameters.Properties["path"] = makeProperty("string", "Path to search in (defaults to current directory)")
+
+ tools = append(tools, api.Tool{
+ Type: "function",
+ Function: searchCodeFunc,
+ })
+
+ return tools
+}
+
+// Fallback text parsing for models without native tool support
+func (d *Del) parseTextToolCalls(input string) []ToolCall {
+ var calls []ToolCall
+
+ // Simple command detection based on user input
+ input = strings.ToLower(strings.TrimSpace(input))
+
+ if input == "list files" || input == "list the files" || input == "ls" {
+ calls = append(calls, ToolCall{Name: "list_dir", Args: map[string]interface{}{}})
+ } else if strings.HasPrefix(input, "read ") {
+ // Extract filename from input
+ filename := strings.TrimPrefix(input, "read ")
+ filename = strings.TrimSpace(filename)
+ if filename != "" {
+ calls = append(calls, ToolCall{Name: "read_file", Args: map[string]interface{}{"path": filename}})
+ }
+ } else if input == "git status" || input == "check git status" || input == "check git" {
+ calls = append(calls, ToolCall{Name: "git_status", Args: map[string]interface{}{}})
+ } else if input == "analyze the code" || input == "analyze code" || input == "analyze this project" {
+ calls = append(calls, ToolCall{Name: "analyze_code", Args: map[string]interface{}{}})
+ } else if strings.HasPrefix(input, "search ") {
+ // Extract search pattern
+ pattern := strings.TrimPrefix(input, "search ")
+ pattern = strings.TrimSpace(pattern)
+ if pattern != "" {
+ calls = append(calls, ToolCall{Name: "search_code", Args: map[string]interface{}{"pattern": pattern}})
+ }
+ } else if strings.HasPrefix(input, "run ") {
+ // Extract command
+ command := strings.TrimPrefix(input, "run ")
+ command = strings.TrimSpace(command)
+ if command != "" {
+ calls = append(calls, ToolCall{Name: "run_command", Args: map[string]interface{}{"command": command}})
+ }
+ }
+
+ return calls
+}
+
func (d *Del) processMessage(ctx context.Context, userInput string) {
d.emit(StreamMessage{
Type: MessageTypeUser,
@@ -833,16 +956,38 @@ func (d *Del) processMessage(ctx context.Context, userInput string) {
d.chatHistory = append(d.chatHistory, api.Message{Role: "user", Content: userInput})
- // Get response from model
+ // Define tools for Ollama native tool calling
+ tools := d.buildOllamaTools()
+
+ // Try with native tools first, fall back if not supported
var fullResponse string
+ var assistantMessage api.Message
+ var toolsSupported = true
+
err := d.client.Chat(ctx, &api.ChatRequest{
Model: d.model,
Messages: d.chatHistory,
+ Tools: tools,
}, func(resp api.ChatResponse) error {
fullResponse += resp.Message.Content
+ assistantMessage = resp.Message
return nil
})
+ // If tools aren't supported, try without tools
+ if err != nil && strings.Contains(err.Error(), "does not support tools") {
+ toolsSupported = false
+ fullResponse = ""
+ err = d.client.Chat(ctx, &api.ChatRequest{
+ Model: d.model,
+ Messages: d.chatHistory,
+ }, func(resp api.ChatResponse) error {
+ fullResponse += resp.Message.Content
+ assistantMessage = resp.Message
+ return nil
+ })
+ }
+
if err != nil {
d.emit(StreamMessage{
Type: MessageTypeSystem,
@@ -851,28 +996,35 @@ func (d *Del) processMessage(ctx context.Context, userInput string) {
return
}
- d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: fullResponse})
-
- // Check for tool calls
- toolCalls := d.parseToolCalls(fullResponse)
-
- if len(toolCalls) > 0 {
- // Execute tools
- var toolResults []string
- for _, call := range toolCalls {
+ // Add assistant message to history
+ d.chatHistory = append(d.chatHistory, assistantMessage)
+
+ // Check for native tool calls from Ollama (only if tools are supported)
+ if toolsSupported && len(assistantMessage.ToolCalls) > 0 {
+ // Execute tools using native Ollama tool calls
+ for _, apiCall := range assistantMessage.ToolCalls {
+ // Convert api.ToolCall to our internal format
+ call := ToolCall{
+ Name: apiCall.Function.Name,
+ Args: apiCall.Function.Arguments,
+ }
+
+ // Execute the tool
result := d.executeTool(ctx, call)
- toolResults = append(toolResults, fmt.Sprintf("%s: %s", call.Name, result))
+
+ // Add tool result as a 'tool' role message
+ d.chatHistory = append(d.chatHistory, api.Message{
+ Role: "tool",
+ Content: result,
+ })
}
// Get final response with tool results
- toolResultsMsg := fmt.Sprintf("Tool results:\n%s\n\nProvide a natural response based on these results.",
- strings.Join(toolResults, "\n"))
- d.chatHistory = append(d.chatHistory, api.Message{Role: "user", Content: toolResultsMsg})
-
var finalResponse string
err = d.client.Chat(ctx, &api.ChatRequest{
Model: d.model,
Messages: d.chatHistory,
+ Tools: tools,
}, func(resp api.ChatResponse) error {
finalResponse += resp.Message.Content
return nil
@@ -883,8 +1035,38 @@ func (d *Del) processMessage(ctx context.Context, userInput string) {
d.streamResponseChunks(ctx, finalResponse)
}
} else {
- // No tools, just stream the response
- d.streamResponseChunks(ctx, fullResponse)
+ // Fallback: Parse user input for tool calls (for models without native support)
+ toolCalls := d.parseTextToolCalls(userInput)
+ if len(toolCalls) > 0 {
+ // Execute any tools found in user input
+ for _, call := range toolCalls {
+ result := d.executeTool(ctx, call)
+
+ // Add tool result as context for next message
+ d.chatHistory = append(d.chatHistory, api.Message{
+ Role: "user",
+ Content: fmt.Sprintf("Tool result for %s: %s", call.Name, result),
+ })
+ }
+
+ // Get final response with tool results
+ var finalResponse string
+ err = d.client.Chat(ctx, &api.ChatRequest{
+ Model: d.model,
+ Messages: d.chatHistory,
+ }, func(resp api.ChatResponse) error {
+ finalResponse += resp.Message.Content
+ return nil
+ })
+
+ if err == nil {
+ d.chatHistory = append(d.chatHistory, api.Message{Role: "assistant", Content: finalResponse})
+ d.streamResponseChunks(ctx, finalResponse)
+ }
+ } else {
+ // No tools, just stream the response
+ d.streamResponseChunks(ctx, fullResponse)
+ }
}
}
.gitignore
@@ -1,1 +1,2 @@
/del
+/bin