diff --git a/go.mod b/go.mod index e22fd45..d2e2126 100644 --- a/go.mod +++ b/go.mod @@ -5,6 +5,7 @@ go 1.24.2 require ( github.com/anthropics/anthropic-sdk-go v1.6.2 github.com/invopop/jsonschema v0.13.0 + github.com/sashabaranov/go-openai v1.30.3 ) require ( diff --git a/go.sum b/go.sum index e9c518d..603d198 100644 --- a/go.sum +++ b/go.sum @@ -13,6 +13,8 @@ github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0 github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sashabaranov/go-openai v1.30.3 h1:TEdRP3otRXX2A7vLoU+kI5XpoSo7VUUlM/rEttUqgek= +github.com/sashabaranov/go-openai v1.30.3/go.mod h1:lj5b/K+zjTSFxVLijLSTDZuP7adOgerWeFyZLUhAKRg= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= diff --git a/gpt_version/README.md b/gpt_version/README.md new file mode 100644 index 0000000..757cb95 --- /dev/null +++ b/gpt_version/README.md @@ -0,0 +1,74 @@ +# GPT Version Agents + +This directory contains GPT-powered versions of the coding agents, converted from the original Anthropic Claude versions. Each file is a standalone program that provides different tool capabilities. + +## Files Overview + +- `chat_gpt.go` - Basic chat functionality without tools +- `read_gpt.go` - Chat with file reading capability +- `list_files_gpt.go` - Chat with file reading and listing capabilities +- `bash_tool_gpt.go` - Chat with file operations and bash command execution +- `code_search_tool_gpt.go` - Chat with file operations, bash, and code search using ripgrep +- `edit_tool_gpt.go` - Chat with file operations, bash, and file editing capabilities + +## Usage + +Each program is standalone. To run any of them: + +```bash +# For basic chat +go run gpt_version/chat_gpt.go + +# For file reading agent +go run gpt_version/read_gpt.go + +# For file listing agent +go run gpt_version/list_files_gpt.go + +# For bash command agent +go run gpt_version/bash_tool_gpt.go + +# For code search agent +go run gpt_version/code_search_tool_gpt.go + +# For file editing agent +go run gpt_version/edit_tool_gpt.go +``` + +## Configuration + +All programs use the ChatAnywhere proxy service for accessing OpenAI's API. You can: + +1. Set the `OPENAI_API_KEY` environment variable with your API key +2. Or modify the default API key in the source code (line ~32 in each file) + +For international users, change the `BaseURL` from `https://api.chatanywhere.tech/v1` to `https://api.chatanywhere.org/v1`. + +## Features + +### Tools Available + +- **read_file**: Read contents of files +- **list_files**: List files and directories +- **bash**: Execute bash commands +- **code_search**: Search code using ripgrep (rg) +- **edit_file**: Edit files by replacing text + +### Verbose Mode + +All programs support verbose logging: + +```bash +go run gpt_version/edit_tool_gpt.go -verbose +``` + +## Dependencies + +- `github.com/sashabaranov/go-openai` - OpenAI Go SDK +- Standard Go libraries + +## Notes + +- These are standalone programs, not meant to be compiled together +- The linter warnings about redeclared identifiers are expected since each file defines the same types independently +- Each program provides a progressively more feature-rich agent, choose based on your needs \ No newline at end of file diff --git a/gpt_version/bash_tool_gpt.go b/gpt_version/bash_tool_gpt.go new file mode 100644 index 0000000..1e55a29 --- /dev/null +++ b/gpt_version/bash_tool_gpt.go @@ -0,0 +1,452 @@ +package main + +import ( + "bufio" + "context" + "encoding/json" + "flag" + "fmt" + "log" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/sashabaranov/go-openai" +) + +func main() { + verbose := flag.Bool("verbose", false, "enable verbose logging") + flag.Parse() + + if *verbose { + log.SetOutput(os.Stderr) + log.SetFlags(log.LstdFlags | log.Lshortfile) + log.Println("Verbose logging enabled") + } else { + log.SetOutput(os.Stdout) + log.SetFlags(0) + log.SetPrefix("") + } + + // Get API Key from environment variable, use default if not set + apiKey := os.Getenv("OPENAI_API_KEY") + if apiKey == "" { + // Set your ChatAnywhere free API Key here + apiKey = "YOUR_API_KEY" + } + + // Use ChatAnywhere proxy service (recommended for China) + config := openai.DefaultConfig(apiKey) + config.BaseURL = "https://api.chatanywhere.tech/v1" + // For international users: config.BaseURL = "https://api.chatanywhere.org/v1" + + client := openai.NewClientWithConfig(config) + if *verbose { + log.Println("OpenAI client initialized with ChatAnywhere") + log.Printf("Base URL: %s", config.BaseURL) + } + + scanner := bufio.NewScanner(os.Stdin) + getUserMessage := func() (string, bool) { + if !scanner.Scan() { + return "", false + } + return scanner.Text(), true + } + + tools := []ToolDefinition{ReadFileDefinition, ListFilesDefinition, BashDefinition} + if *verbose { + log.Printf("Initialized %d tools", len(tools)) + } + agent := NewAgent(client, getUserMessage, tools, *verbose) + err := agent.Run(context.TODO()) + if err != nil { + fmt.Printf("Error: %s\n", err.Error()) + } +} + +func NewAgent( + client *openai.Client, + getUserMessage func() (string, bool), + tools []ToolDefinition, + verbose bool, +) *Agent { + return &Agent{ + client: client, + getUserMessage: getUserMessage, + tools: tools, + verbose: verbose, + } +} + +type Agent struct { + client *openai.Client + getUserMessage func() (string, bool) + tools []ToolDefinition + verbose bool +} + +func (a *Agent) Run(ctx context.Context) error { + conversation := []openai.ChatCompletionMessage{} + + if a.verbose { + log.Println("Starting chat session with tools enabled") + } + fmt.Println("Chat with GPT (use 'ctrl-c' to quit)") + + for { + fmt.Print("\u001b[94mYou\u001b[0m: ") + userInput, ok := a.getUserMessage() + if !ok { + if a.verbose { + log.Println("User input ended, breaking from chat loop") + } + break + } + + // Skip empty messages + if userInput == "" { + if a.verbose { + log.Println("Skipping empty message") + } + continue + } + + if a.verbose { + log.Printf("User input received: %q", userInput) + } + + userMessage := openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, + Content: userInput, + } + conversation = append(conversation, userMessage) + + if a.verbose { + log.Printf("Sending message to GPT, conversation length: %d", len(conversation)) + } + + message, err := a.runInference(ctx, conversation) + if err != nil { + if a.verbose { + log.Printf("Error during inference: %v", err) + } + return err + } + conversation = append(conversation, message) + + // Keep processing until GPT stops using tools + for { + // Check if there are tool calls in the message + if len(message.ToolCalls) == 0 { + // No tool calls, just print the message and break + if message.Content != "" { + fmt.Printf("\u001b[93mGPT\u001b[0m: %s\n", message.Content) + } + break + } + + // Process tool calls + if a.verbose { + log.Printf("Processing %d tool calls from GPT", len(message.ToolCalls)) + } + + var toolMessages []openai.ChatCompletionMessage + for _, toolCall := range message.ToolCalls { + if a.verbose { + log.Printf("Tool call detected: %s with arguments: %s", toolCall.Function.Name, toolCall.Function.Arguments) + } + fmt.Printf("\u001b[96mtool\u001b[0m: %s(%s)\n", toolCall.Function.Name, toolCall.Function.Arguments) + + // Find and execute the tool + var toolResult string + var toolError error + var toolFound bool + for _, tool := range a.tools { + if tool.Name == toolCall.Function.Name { + if a.verbose { + log.Printf("Executing tool: %s", tool.Name) + } + toolResult, toolError = tool.Function(json.RawMessage(toolCall.Function.Arguments)) + fmt.Printf("\u001b[92mresult\u001b[0m: %s\n", toolResult) + if toolError != nil { + fmt.Printf("\u001b[91merror\u001b[0m: %s\n", toolError.Error()) + } + if a.verbose { + if toolError != nil { + log.Printf("Tool execution failed: %v", toolError) + } else { + log.Printf("Tool execution successful, result length: %d chars", len(toolResult)) + } + } + toolFound = true + break + } + } + + if !toolFound { + toolError = fmt.Errorf("tool '%s' not found", toolCall.Function.Name) + fmt.Printf("\u001b[91merror\u001b[0m: %s\n", toolError.Error()) + } + + // Add tool result message + var content string + if toolError != nil { + content = fmt.Sprintf("Error: %s", toolError.Error()) + } else { + content = toolResult + } + + toolMessage := openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleTool, + Content: content, + ToolCallID: toolCall.ID, + } + toolMessages = append(toolMessages, toolMessage) + } + + // Add all tool messages to conversation + conversation = append(conversation, toolMessages...) + + if a.verbose { + log.Printf("Sending %d tool results back to GPT", len(toolMessages)) + } + + // Get GPT's response after tool execution + message, err = a.runInference(ctx, conversation) + if err != nil { + if a.verbose { + log.Printf("Error during followup inference: %v", err) + } + return err + } + conversation = append(conversation, message) + + if a.verbose { + log.Printf("Received followup response from GPT") + } + + // Continue loop to process the new message + } + } + + if a.verbose { + log.Println("Chat session ended") + } + return nil +} + +func (a *Agent) runInference(ctx context.Context, conversation []openai.ChatCompletionMessage) (openai.ChatCompletionMessage, error) { + // Convert tools to OpenAI function definitions + var functions []openai.FunctionDefinition + for _, tool := range a.tools { + functions = append(functions, openai.FunctionDefinition{ + Name: tool.Name, + Description: tool.Description, + Parameters: tool.Parameters, + }) + } + + model := openai.GPT3Dot5Turbo + if a.verbose { + log.Printf("Making API call to GPT with model: %s and %d tools", model, len(functions)) + } + + req := openai.ChatCompletionRequest{ + Model: model, + Messages: conversation, + MaxTokens: 1024, + Temperature: 0.7, + } + + // Add tools if available + if len(functions) > 0 { + var tools []openai.Tool + for _, fn := range functions { + tools = append(tools, openai.Tool{ + Type: openai.ToolTypeFunction, + Function: &fn, + }) + } + req.Tools = tools + req.ToolChoice = "auto" + } + + // Synchronous API call + resp, err := a.client.CreateChatCompletion(ctx, req) + + if a.verbose { + if err != nil { + log.Printf("API call failed: %v", err) + } else { + log.Printf("API call successful, response received") + } + } + + if err != nil { + return openai.ChatCompletionMessage{}, err + } + + if len(resp.Choices) == 0 { + return openai.ChatCompletionMessage{}, fmt.Errorf("no response choices returned") + } + + return resp.Choices[0].Message, nil +} + +type ToolDefinition struct { + Name string `json:"name"` + Description string `json:"description"` + Parameters map[string]interface{} `json:"parameters"` + Function func(input json.RawMessage) (string, error) +} + +var ReadFileDefinition = ToolDefinition{ + Name: "read_file", + Description: "Read the contents of a given relative file path. Use this when you want to see what's inside a file. Do not use this with directory names.", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "path": map[string]interface{}{ + "type": "string", + "description": "The relative path of a file in the working directory.", + }, + }, + "required": []string{"path"}, + }, + Function: ReadFile, +} + +var ListFilesDefinition = ToolDefinition{ + Name: "list_files", + Description: "List files and directories at a given path. If no path is provided, lists files in the current directory.", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "path": map[string]interface{}{ + "type": "string", + "description": "Optional relative path to list files from. Defaults to current directory if not provided.", + }, + }, + "required": []string{}, + }, + Function: ListFiles, +} + +var BashDefinition = ToolDefinition{ + Name: "bash", + Description: "Execute a bash command and return its output. Use this to run shell commands.", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "command": map[string]interface{}{ + "type": "string", + "description": "The bash command to execute.", + }, + }, + "required": []string{"command"}, + }, + Function: Bash, +} + +type ReadFileInput struct { + Path string `json:"path"` +} + +type ListFilesInput struct { + Path string `json:"path,omitempty"` +} + +type BashInput struct { + Command string `json:"command"` +} + +func ReadFile(input json.RawMessage) (string, error) { + readFileInput := ReadFileInput{} + err := json.Unmarshal(input, &readFileInput) + if err != nil { + panic(err) + } + + log.Printf("Reading file: %s", readFileInput.Path) + content, err := os.ReadFile(readFileInput.Path) + if err != nil { + log.Printf("Failed to read file %s: %v", readFileInput.Path, err) + return "", err + } + log.Printf("Successfully read file %s (%d bytes)", readFileInput.Path, len(content)) + return string(content), nil +} + +func ListFiles(input json.RawMessage) (string, error) { + listFilesInput := ListFilesInput{} + err := json.Unmarshal(input, &listFilesInput) + if err != nil { + panic(err) + } + + dir := "." + if listFilesInput.Path != "" { + dir = listFilesInput.Path + } + + log.Printf("Listing files in directory: %s", dir) + var files []string + err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + relPath, err := filepath.Rel(dir, path) + if err != nil { + return err + } + + // Skip .devenv directory and its contents + if info.IsDir() && (relPath == ".devenv" || strings.HasPrefix(relPath, ".devenv/")) { + return filepath.SkipDir + } + + if relPath != "." { + if info.IsDir() { + files = append(files, relPath+"/") + } else { + files = append(files, relPath) + } + } + return nil + }) + + if err != nil { + log.Printf("Failed to list files in %s: %v", dir, err) + return "", err + } + + result, err := json.Marshal(files) + if err != nil { + return "", err + } + + log.Printf("Successfully listed %d files/directories in %s", len(files), dir) + return string(result), nil +} + +func Bash(input json.RawMessage) (string, error) { + bashInput := BashInput{} + err := json.Unmarshal(input, &bashInput) + if err != nil { + return "", err + } + + log.Printf("Executing bash command: %s", bashInput.Command) + cmd := exec.Command("bash", "-c", bashInput.Command) + output, err := cmd.CombinedOutput() + if err != nil { + log.Printf("Bash command failed: %s, error: %v", bashInput.Command, err) + return fmt.Sprintf("Command failed with error: %s\nOutput: %s", err.Error(), string(output)), nil + } + + log.Printf("Bash command succeeded: %s (output: %d bytes)", bashInput.Command, len(output)) + return strings.TrimSpace(string(output)), nil +} diff --git a/gpt_version/chat_gpt.go b/gpt_version/chat_gpt.go new file mode 100644 index 0000000..fe1c1a1 --- /dev/null +++ b/gpt_version/chat_gpt.go @@ -0,0 +1,179 @@ +package main + +import ( + "bufio" + "context" + "encoding/json" + "flag" + "fmt" + "log" + "os" + + "github.com/sashabaranov/go-openai" +) + +type ToolDefinition struct { + Name string `json:"name"` + Description string `json:"description"` + Parameters map[string]interface{} `json:"parameters"` + Function func(input json.RawMessage) (string, error) +} + +func main() { + verbose := flag.Bool("verbose", false, "enable verbose logging") + flag.Parse() + + if *verbose { + log.SetOutput(os.Stderr) + log.SetFlags(log.LstdFlags | log.Lshortfile) + log.Println("Verbose logging enabled") + } else { + log.SetOutput(os.Stdout) + log.SetFlags(0) + log.SetPrefix("") + } + + // Get API Key from environment variable, use default if not set + apiKey := os.Getenv("OPENAI_API_KEY") + if apiKey == "" { + // Set your ChatAnywhere free API Key here + apiKey = "YOUR_API_KEY" + } + + // Use ChatAnywhere proxy service (recommended for China) + config := openai.DefaultConfig(apiKey) + config.BaseURL = "https://api.chatanywhere.tech/v1" + // For international users: config.BaseURL = "https://api.chatanywhere.org/v1" + + client := openai.NewClientWithConfig(config) + if *verbose { + log.Println("OpenAI client initialized with ChatAnywhere") + log.Printf("Base URL: %s", config.BaseURL) + } + + scanner := bufio.NewScanner(os.Stdin) + getUserMessage := func() (string, bool) { + if !scanner.Scan() { + return "", false + } + return scanner.Text(), true + } + + agent := NewAgent(client, getUserMessage, nil, *verbose) + err := agent.Run(context.TODO()) + if err != nil { + fmt.Printf("Error: %s\n", err.Error()) + } +} + +func NewAgent(client *openai.Client, getUserMessage func() (string, bool), tools []ToolDefinition, verbose bool) *Agent { + return &Agent{ + client: client, + getUserMessage: getUserMessage, + tools: tools, + verbose: verbose, + } +} + +type Agent struct { + client *openai.Client + getUserMessage func() (string, bool) + tools []ToolDefinition + verbose bool +} + +func (a *Agent) Run(ctx context.Context) error { + // Conversation history + conversation := []openai.ChatCompletionMessage{} + + if a.verbose { + log.Println("Starting chat session") + } + fmt.Println("Chat with GPT (use 'ctrl-c' to quit)") + + for { + fmt.Print("\u001b[94mYou\u001b[0m: ") + userInput, ok := a.getUserMessage() + if !ok { + if a.verbose { + log.Println("User input ended, breaking from chat loop") + } + break + } + + // Skip empty messages + if userInput == "" { + if a.verbose { + log.Println("Skipping empty message") + } + continue + } + + if a.verbose { + log.Printf("User input received: %q", userInput) + } + + userMessage := openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, + Content: userInput, + } + conversation = append(conversation, userMessage) + + if a.verbose { + log.Printf("Sending message to GPT, conversation length: %d", len(conversation)) + } + + response, err := a.runInference(ctx, conversation) + if err != nil { + if a.verbose { + log.Printf("Error during inference: %v", err) + } + return err + } + conversation = append(conversation, response) + + if a.verbose { + log.Printf("Received response from GPT") + } + + fmt.Printf("\u001b[93mGPT\u001b[0m: %s\n", response.Content) + } + + if a.verbose { + log.Println("Chat session ended") + } + return nil +} + +func (a *Agent) runInference(ctx context.Context, conversation []openai.ChatCompletionMessage) (openai.ChatCompletionMessage, error) { + model := openai.GPT3Dot5Turbo + if a.verbose { + log.Printf("Making API call to GPT with model: %s", model) + } + + // Synchronous API call + resp, err := a.client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{ + Model: model, + Messages: conversation, + MaxTokens: 1024, + Temperature: 0.7, + }) + + if a.verbose { + if err != nil { + log.Printf("API call failed: %v", err) + } else { + log.Printf("API call successful, response received") + } + } + + if err != nil { + return openai.ChatCompletionMessage{}, err + } + + if len(resp.Choices) == 0 { + return openai.ChatCompletionMessage{}, fmt.Errorf("no response choices returned") + } + + return resp.Choices[0].Message, nil +} diff --git a/gpt_version/code_search_tool_gpt.go b/gpt_version/code_search_tool_gpt.go new file mode 100644 index 0000000..f6dd117 --- /dev/null +++ b/gpt_version/code_search_tool_gpt.go @@ -0,0 +1,533 @@ +package main + +import ( + "bufio" + "context" + "encoding/json" + "flag" + "fmt" + "log" + "os" + "os/exec" + "strings" + + "github.com/sashabaranov/go-openai" +) + +func main() { + verbose := flag.Bool("verbose", false, "enable verbose logging") + flag.Parse() + + if *verbose { + log.SetOutput(os.Stderr) + log.SetFlags(log.LstdFlags | log.Lshortfile) + log.Println("Verbose logging enabled") + } else { + log.SetOutput(os.Stdout) + log.SetFlags(0) + log.SetPrefix("") + } + + // Get API Key from environment variable, use default if not set + apiKey := os.Getenv("OPENAI_API_KEY") + if apiKey == "" { + // Set your ChatAnywhere free API Key here + apiKey = "YOUR_API_KEY" + } + + // Use ChatAnywhere proxy service (recommended for China) + config := openai.DefaultConfig(apiKey) + config.BaseURL = "https://api.chatanywhere.tech/v1" + // For international users: config.BaseURL = "https://api.chatanywhere.org/v1" + + client := openai.NewClientWithConfig(config) + if *verbose { + log.Println("OpenAI client initialized with ChatAnywhere") + log.Printf("Base URL: %s", config.BaseURL) + } + + scanner := bufio.NewScanner(os.Stdin) + getUserMessage := func() (string, bool) { + if !scanner.Scan() { + return "", false + } + return scanner.Text(), true + } + + tools := []ToolDefinition{ReadFileDefinition, ListFilesDefinition, BashDefinition, CodeSearchDefinition} + if *verbose { + log.Printf("Initialized %d tools", len(tools)) + } + agent := NewAgent(client, getUserMessage, tools, *verbose) + err := agent.Run(context.TODO()) + if err != nil { + fmt.Printf("Error: %s\n", err.Error()) + } +} + +func NewAgent( + client *openai.Client, + getUserMessage func() (string, bool), + tools []ToolDefinition, + verbose bool, +) *Agent { + return &Agent{ + client: client, + getUserMessage: getUserMessage, + tools: tools, + verbose: verbose, + } +} + +type Agent struct { + client *openai.Client + getUserMessage func() (string, bool) + tools []ToolDefinition + verbose bool +} + +func (a *Agent) Run(ctx context.Context) error { + conversation := []openai.ChatCompletionMessage{} + + if a.verbose { + log.Println("Starting chat session with tools enabled") + } + fmt.Println("Chat with GPT (use 'ctrl-c' to quit)") + + for { + fmt.Print("\u001b[94mYou\u001b[0m: ") + userInput, ok := a.getUserMessage() + if !ok { + if a.verbose { + log.Println("User input ended, breaking from chat loop") + } + break + } + + // Skip empty messages + if userInput == "" { + if a.verbose { + log.Println("Skipping empty message") + } + continue + } + + if a.verbose { + log.Printf("User input received: %q", userInput) + } + + userMessage := openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, + Content: userInput, + } + conversation = append(conversation, userMessage) + + if a.verbose { + log.Printf("Sending message to GPT, conversation length: %d", len(conversation)) + } + + message, err := a.runInference(ctx, conversation) + if err != nil { + if a.verbose { + log.Printf("Error during inference: %v", err) + } + return err + } + conversation = append(conversation, message) + + // Keep processing until GPT stops using tools + for { + // Check if there are tool calls in the message + if len(message.ToolCalls) == 0 { + // No tool calls, just print the message and break + if message.Content != "" { + fmt.Printf("\u001b[93mGPT\u001b[0m: %s\n", message.Content) + } + break + } + + // Process tool calls + if a.verbose { + log.Printf("Processing %d tool calls from GPT", len(message.ToolCalls)) + } + + var toolMessages []openai.ChatCompletionMessage + for _, toolCall := range message.ToolCalls { + if a.verbose { + log.Printf("Tool call detected: %s with arguments: %s", toolCall.Function.Name, toolCall.Function.Arguments) + } + fmt.Printf("\u001b[96mtool\u001b[0m: %s(%s)\n", toolCall.Function.Name, toolCall.Function.Arguments) + + // Find and execute the tool + var toolResult string + var toolError error + var toolFound bool + for _, tool := range a.tools { + if tool.Name == toolCall.Function.Name { + if a.verbose { + log.Printf("Executing tool: %s", tool.Name) + } + toolResult, toolError = tool.Function(json.RawMessage(toolCall.Function.Arguments)) + fmt.Printf("\u001b[92mresult\u001b[0m: %s\n", toolResult) + if toolError != nil { + fmt.Printf("\u001b[91merror\u001b[0m: %s\n", toolError.Error()) + } + if a.verbose { + if toolError != nil { + log.Printf("Tool execution failed: %v", toolError) + } else { + log.Printf("Tool execution successful, result length: %d chars", len(toolResult)) + } + } + toolFound = true + break + } + } + + if !toolFound { + toolError = fmt.Errorf("tool '%s' not found", toolCall.Function.Name) + fmt.Printf("\u001b[91merror\u001b[0m: %s\n", toolError.Error()) + } + + // Add tool result message + var content string + if toolError != nil { + content = fmt.Sprintf("Error: %s", toolError.Error()) + } else { + content = toolResult + } + + toolMessage := openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleTool, + Content: content, + ToolCallID: toolCall.ID, + } + toolMessages = append(toolMessages, toolMessage) + } + + // Add all tool messages to conversation + conversation = append(conversation, toolMessages...) + + if a.verbose { + log.Printf("Sending %d tool results back to GPT", len(toolMessages)) + } + + // Get GPT's response after tool execution + message, err = a.runInference(ctx, conversation) + if err != nil { + if a.verbose { + log.Printf("Error during followup inference: %v", err) + } + return err + } + conversation = append(conversation, message) + + if a.verbose { + log.Printf("Received followup response from GPT") + } + + // Continue loop to process the new message + } + } + + if a.verbose { + log.Println("Chat session ended") + } + return nil +} + +func (a *Agent) runInference(ctx context.Context, conversation []openai.ChatCompletionMessage) (openai.ChatCompletionMessage, error) { + // Convert tools to OpenAI function definitions + var functions []openai.FunctionDefinition + for _, tool := range a.tools { + functions = append(functions, openai.FunctionDefinition{ + Name: tool.Name, + Description: tool.Description, + Parameters: tool.Parameters, + }) + } + + model := openai.GPT3Dot5Turbo + if a.verbose { + log.Printf("Making API call to GPT with model: %s and %d tools", model, len(functions)) + } + + req := openai.ChatCompletionRequest{ + Model: model, + Messages: conversation, + MaxTokens: 1024, + Temperature: 0.7, + } + + // Add tools if available + if len(functions) > 0 { + var tools []openai.Tool + for _, fn := range functions { + tools = append(tools, openai.Tool{ + Type: openai.ToolTypeFunction, + Function: &fn, + }) + } + req.Tools = tools + req.ToolChoice = "auto" + } + + // Synchronous API call + resp, err := a.client.CreateChatCompletion(ctx, req) + + if a.verbose { + if err != nil { + log.Printf("API call failed: %v", err) + } else { + log.Printf("API call successful, response received") + } + } + + if err != nil { + return openai.ChatCompletionMessage{}, err + } + + if len(resp.Choices) == 0 { + return openai.ChatCompletionMessage{}, fmt.Errorf("no response choices returned") + } + + return resp.Choices[0].Message, nil +} + +type ToolDefinition struct { + Name string `json:"name"` + Description string `json:"description"` + Parameters map[string]interface{} `json:"parameters"` + Function func(input json.RawMessage) (string, error) +} + +var ReadFileDefinition = ToolDefinition{ + Name: "read_file", + Description: "Read the contents of a given relative file path. Use this when you want to see what's inside a file. Do not use this with directory names.", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "path": map[string]interface{}{ + "type": "string", + "description": "The relative path of a file in the working directory.", + }, + }, + "required": []string{"path"}, + }, + Function: ReadFile, +} + +var ListFilesDefinition = ToolDefinition{ + Name: "list_files", + Description: "List files and directories at a given path. If no path is provided, lists files in the current directory.", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "path": map[string]interface{}{ + "type": "string", + "description": "Optional relative path to list files from. Defaults to current directory if not provided.", + }, + }, + "required": []string{}, + }, + Function: ListFiles, +} + +var BashDefinition = ToolDefinition{ + Name: "bash", + Description: "Execute a bash command and return its output. Use this to run shell commands.", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "command": map[string]interface{}{ + "type": "string", + "description": "The bash command to execute.", + }, + }, + "required": []string{"command"}, + }, + Function: Bash, +} + +var CodeSearchDefinition = ToolDefinition{ + Name: "code_search", + Description: `Search for code patterns using ripgrep (rg). + +Use this to find code patterns, function definitions, variable usage, or any text in the codebase. +You can search by pattern, file type, or directory.`, + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "pattern": map[string]interface{}{ + "type": "string", + "description": "The search pattern or regex to look for", + }, + "path": map[string]interface{}{ + "type": "string", + "description": "Optional path to search in (file or directory)", + }, + "file_type": map[string]interface{}{ + "type": "string", + "description": "Optional file extension to limit search to (e.g., 'go', 'js', 'py')", + }, + "case_sensitive": map[string]interface{}{ + "type": "boolean", + "description": "Whether the search should be case sensitive (default: false)", + }, + }, + "required": []string{"pattern"}, + }, + Function: CodeSearch, +} + +type ReadFileInput struct { + Path string `json:"path"` +} + +type ListFilesInput struct { + Path string `json:"path,omitempty"` +} + +type BashInput struct { + Command string `json:"command"` +} + +type CodeSearchInput struct { + Pattern string `json:"pattern"` + Path string `json:"path,omitempty"` + FileType string `json:"file_type,omitempty"` + CaseSensitive bool `json:"case_sensitive,omitempty"` +} + +func ReadFile(input json.RawMessage) (string, error) { + readFileInput := ReadFileInput{} + err := json.Unmarshal(input, &readFileInput) + if err != nil { + panic(err) + } + + log.Printf("Reading file: %s", readFileInput.Path) + content, err := os.ReadFile(readFileInput.Path) + if err != nil { + log.Printf("Failed to read file %s: %v", readFileInput.Path, err) + return "", err + } + log.Printf("Successfully read file %s (%d bytes)", readFileInput.Path, len(content)) + return string(content), nil +} + +func ListFiles(input json.RawMessage) (string, error) { + listFilesInput := ListFilesInput{} + err := json.Unmarshal(input, &listFilesInput) + if err != nil { + panic(err) + } + + dir := "." + if listFilesInput.Path != "" { + dir = listFilesInput.Path + } + + log.Printf("Listing files in directory: %s", dir) + cmd := exec.Command("find", dir, "-type", "f", "-not", "-path", "*/.devenv/*", "-not", "-path", "*/.git/*") + output, err := cmd.Output() + if err != nil { + log.Printf("Failed to list files in %s: %v", dir, err) + return "", err + } + + files := strings.Split(strings.TrimSpace(string(output)), "\n") + if len(files) == 1 && files[0] == "" { + files = []string{} + } + + result, err := json.Marshal(files) + if err != nil { + return "", err + } + + log.Printf("Successfully listed %d files in %s", len(files), dir) + return string(result), nil +} + +func Bash(input json.RawMessage) (string, error) { + bashInput := BashInput{} + err := json.Unmarshal(input, &bashInput) + if err != nil { + return "", err + } + + log.Printf("Executing bash command: %s", bashInput.Command) + cmd := exec.Command("bash", "-c", bashInput.Command) + output, err := cmd.CombinedOutput() + if err != nil { + log.Printf("Bash command failed: %v", err) + return fmt.Sprintf("Command failed with error: %s\nOutput: %s", err.Error(), string(output)), nil + } + + log.Printf("Bash command executed successfully, output length: %d chars", len(output)) + return strings.TrimSpace(string(output)), nil +} + +func CodeSearch(input json.RawMessage) (string, error) { + codeSearchInput := CodeSearchInput{} + err := json.Unmarshal(input, &codeSearchInput) + if err != nil { + return "", err + } + + if codeSearchInput.Pattern == "" { + log.Printf("CodeSearch failed: pattern is required") + return "", fmt.Errorf("pattern is required") + } + + log.Printf("Searching for pattern: %s", codeSearchInput.Pattern) + + // Build ripgrep command + args := []string{"rg", "--line-number", "--with-filename", "--color=never"} + + // Add case sensitivity flag + if !codeSearchInput.CaseSensitive { + args = append(args, "--ignore-case") + } + + // Add file type filter if specified + if codeSearchInput.FileType != "" { + args = append(args, "--type", codeSearchInput.FileType) + } + + // Add pattern + args = append(args, codeSearchInput.Pattern) + + // Add path if specified + if codeSearchInput.Path != "" { + args = append(args, codeSearchInput.Path) + } else { + args = append(args, ".") + } + + cmd := exec.Command(args[0], args[1:]...) + output, err := cmd.Output() + + // ripgrep returns exit code 1 when no matches are found, which is not an error + if err != nil { + if exitError, ok := err.(*exec.ExitError); ok && exitError.ExitCode() == 1 { + log.Printf("No matches found for pattern: %s", codeSearchInput.Pattern) + return "No matches found", nil + } + log.Printf("Ripgrep command failed: %v", err) + return "", fmt.Errorf("search failed: %w", err) + } + + result := strings.TrimSpace(string(output)) + lines := strings.Split(result, "\n") + + log.Printf("Found %d matches for pattern: %s", len(lines), codeSearchInput.Pattern) + + // Limit output to prevent overwhelming responses + if len(lines) > 50 { + result = strings.Join(lines[:50], "\n") + fmt.Sprintf("\n... (showing first 50 of %d matches)", len(lines)) + } + + return result, nil +} diff --git a/gpt_version/edit_tool_gpt.go b/gpt_version/edit_tool_gpt.go new file mode 100644 index 0000000..d917473 --- /dev/null +++ b/gpt_version/edit_tool_gpt.go @@ -0,0 +1,563 @@ +package main + +import ( + "bufio" + "context" + "encoding/json" + "flag" + "fmt" + "log" + "os" + "os/exec" + "path" + "path/filepath" + "strings" + + "github.com/sashabaranov/go-openai" +) + +func main() { + verbose := flag.Bool("verbose", false, "enable verbose logging") + flag.Parse() + + if *verbose { + log.SetOutput(os.Stderr) + log.SetFlags(log.LstdFlags | log.Lshortfile) + log.Println("Verbose logging enabled") + } else { + log.SetOutput(os.Stdout) + log.SetFlags(0) + log.SetPrefix("") + } + + // Get API Key from environment variable, use default if not set + apiKey := os.Getenv("OPENAI_API_KEY") + if apiKey == "" { + // Set your ChatAnywhere free API Key here + apiKey = "YOUR_API_KEY" + } + + // Use ChatAnywhere proxy service (recommended for China) + config := openai.DefaultConfig(apiKey) + config.BaseURL = "https://api.chatanywhere.tech/v1" + // For international users: config.BaseURL = "https://api.chatanywhere.org/v1" + + client := openai.NewClientWithConfig(config) + if *verbose { + log.Println("OpenAI client initialized with ChatAnywhere") + log.Printf("Base URL: %s", config.BaseURL) + } + + scanner := bufio.NewScanner(os.Stdin) + getUserMessage := func() (string, bool) { + if !scanner.Scan() { + return "", false + } + return scanner.Text(), true + } + + tools := []ToolDefinition{ReadFileDefinition, ListFilesDefinition, BashDefinition, EditFileDefinition} + if *verbose { + log.Printf("Initialized %d tools", len(tools)) + } + agent := NewAgent(client, getUserMessage, tools, *verbose) + err := agent.Run(context.TODO()) + if err != nil { + fmt.Printf("Error: %s\n", err.Error()) + } +} + +func NewAgent( + client *openai.Client, + getUserMessage func() (string, bool), + tools []ToolDefinition, + verbose bool, +) *Agent { + return &Agent{ + client: client, + getUserMessage: getUserMessage, + tools: tools, + verbose: verbose, + } +} + +type Agent struct { + client *openai.Client + getUserMessage func() (string, bool) + tools []ToolDefinition + verbose bool +} + +func (a *Agent) Run(ctx context.Context) error { + conversation := []openai.ChatCompletionMessage{} + + if a.verbose { + log.Println("Starting chat session with tools enabled") + } + fmt.Println("Chat with GPT (use 'ctrl-c' to quit)") + + for { + fmt.Print("\u001b[94mYou\u001b[0m: ") + userInput, ok := a.getUserMessage() + if !ok { + if a.verbose { + log.Println("User input ended, breaking from chat loop") + } + break + } + + // Skip empty messages + if userInput == "" { + if a.verbose { + log.Println("Skipping empty message") + } + continue + } + + if a.verbose { + log.Printf("User input received: %q", userInput) + } + + userMessage := openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, + Content: userInput, + } + conversation = append(conversation, userMessage) + + if a.verbose { + log.Printf("Sending message to GPT, conversation length: %d", len(conversation)) + } + + message, err := a.runInference(ctx, conversation) + if err != nil { + if a.verbose { + log.Printf("Error during inference: %v", err) + } + return err + } + conversation = append(conversation, message) + + // Keep processing until GPT stops using tools + for { + // Check if there are tool calls in the message + if len(message.ToolCalls) == 0 { + // No tool calls, just print the message and break + if message.Content != "" { + fmt.Printf("\u001b[93mGPT\u001b[0m: %s\n", message.Content) + } + break + } + + // Process tool calls + if a.verbose { + log.Printf("Processing %d tool calls from GPT", len(message.ToolCalls)) + } + + var toolMessages []openai.ChatCompletionMessage + for _, toolCall := range message.ToolCalls { + if a.verbose { + log.Printf("Tool call detected: %s with arguments: %s", toolCall.Function.Name, toolCall.Function.Arguments) + } + fmt.Printf("\u001b[96mtool\u001b[0m: %s(%s)\n", toolCall.Function.Name, toolCall.Function.Arguments) + + // Find and execute the tool + var toolResult string + var toolError error + var toolFound bool + for _, tool := range a.tools { + if tool.Name == toolCall.Function.Name { + if a.verbose { + log.Printf("Executing tool: %s", tool.Name) + } + toolResult, toolError = tool.Function(json.RawMessage(toolCall.Function.Arguments)) + fmt.Printf("\u001b[92mresult\u001b[0m: %s\n", toolResult) + if toolError != nil { + fmt.Printf("\u001b[91merror\u001b[0m: %s\n", toolError.Error()) + } + if a.verbose { + if toolError != nil { + log.Printf("Tool execution failed: %v", toolError) + } else { + log.Printf("Tool execution successful, result length: %d chars", len(toolResult)) + } + } + toolFound = true + break + } + } + + if !toolFound { + toolError = fmt.Errorf("tool '%s' not found", toolCall.Function.Name) + fmt.Printf("\u001b[91merror\u001b[0m: %s\n", toolError.Error()) + } + + // Add tool result message + var content string + if toolError != nil { + content = fmt.Sprintf("Error: %s", toolError.Error()) + } else { + content = toolResult + } + + toolMessage := openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleTool, + Content: content, + ToolCallID: toolCall.ID, + } + toolMessages = append(toolMessages, toolMessage) + } + + // Add all tool messages to conversation + conversation = append(conversation, toolMessages...) + + if a.verbose { + log.Printf("Sending %d tool results back to GPT", len(toolMessages)) + } + + // Get GPT's response after tool execution + message, err = a.runInference(ctx, conversation) + if err != nil { + if a.verbose { + log.Printf("Error during followup inference: %v", err) + } + return err + } + conversation = append(conversation, message) + + if a.verbose { + log.Printf("Received followup response from GPT") + } + + // Continue loop to process the new message + } + } + + if a.verbose { + log.Println("Chat session ended") + } + return nil +} + +func (a *Agent) runInference(ctx context.Context, conversation []openai.ChatCompletionMessage) (openai.ChatCompletionMessage, error) { + // Convert tools to OpenAI function definitions + var functions []openai.FunctionDefinition + for _, tool := range a.tools { + functions = append(functions, openai.FunctionDefinition{ + Name: tool.Name, + Description: tool.Description, + Parameters: tool.Parameters, + }) + } + + model := openai.GPT3Dot5Turbo + if a.verbose { + log.Printf("Making API call to GPT with model: %s and %d tools", model, len(functions)) + } + + req := openai.ChatCompletionRequest{ + Model: model, + Messages: conversation, + MaxTokens: 1024, + Temperature: 0.7, + } + + // Add tools if available + if len(functions) > 0 { + var tools []openai.Tool + for _, fn := range functions { + tools = append(tools, openai.Tool{ + Type: openai.ToolTypeFunction, + Function: &fn, + }) + } + req.Tools = tools + req.ToolChoice = "auto" + } + + // Synchronous API call + resp, err := a.client.CreateChatCompletion(ctx, req) + + if a.verbose { + if err != nil { + log.Printf("API call failed: %v", err) + } else { + log.Printf("API call successful, response received") + } + } + + if err != nil { + return openai.ChatCompletionMessage{}, err + } + + if len(resp.Choices) == 0 { + return openai.ChatCompletionMessage{}, fmt.Errorf("no response choices returned") + } + + return resp.Choices[0].Message, nil +} + +type ToolDefinition struct { + Name string `json:"name"` + Description string `json:"description"` + Parameters map[string]interface{} `json:"parameters"` + Function func(input json.RawMessage) (string, error) +} + +var ReadFileDefinition = ToolDefinition{ + Name: "read_file", + Description: "Read the contents of a given relative file path. Use this when you want to see what's inside a file. Do not use this with directory names.", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "path": map[string]interface{}{ + "type": "string", + "description": "The relative path of a file in the working directory.", + }, + }, + "required": []string{"path"}, + }, + Function: ReadFile, +} + +var ListFilesDefinition = ToolDefinition{ + Name: "list_files", + Description: "List files and directories at a given path. If no path is provided, lists files in the current directory.", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "path": map[string]interface{}{ + "type": "string", + "description": "Optional relative path to list files from. Defaults to current directory if not provided.", + }, + }, + "required": []string{}, + }, + Function: ListFiles, +} + +var BashDefinition = ToolDefinition{ + Name: "bash", + Description: "Execute a bash command and return its output. Use this to run shell commands.", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "command": map[string]interface{}{ + "type": "string", + "description": "The bash command to execute.", + }, + }, + "required": []string{"command"}, + }, + Function: Bash, +} + +var EditFileDefinition = ToolDefinition{ + Name: "edit_file", + Description: `Make edits to a text file. + +Replaces 'old_str' with 'new_str' in the given file. 'old_str' and 'new_str' MUST be different from each other. + +If the file specified with path doesn't exist, it will be created.`, + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "path": map[string]interface{}{ + "type": "string", + "description": "The path to the file", + }, + "old_str": map[string]interface{}{ + "type": "string", + "description": "Text to search for - must match exactly and must only have one match exactly", + }, + "new_str": map[string]interface{}{ + "type": "string", + "description": "Text to replace old_str with", + }, + }, + "required": []string{"path", "old_str", "new_str"}, + }, + Function: EditFile, +} + +type ReadFileInput struct { + Path string `json:"path"` +} + +type ListFilesInput struct { + Path string `json:"path,omitempty"` +} + +type BashInput struct { + Command string `json:"command"` +} + +type EditFileInput struct { + Path string `json:"path"` + OldStr string `json:"old_str"` + NewStr string `json:"new_str"` +} + +func ReadFile(input json.RawMessage) (string, error) { + readFileInput := ReadFileInput{} + err := json.Unmarshal(input, &readFileInput) + if err != nil { + panic(err) + } + + log.Printf("Reading file: %s", readFileInput.Path) + content, err := os.ReadFile(readFileInput.Path) + if err != nil { + log.Printf("Failed to read file %s: %v", readFileInput.Path, err) + return "", err + } + log.Printf("Successfully read file %s (%d bytes)", readFileInput.Path, len(content)) + return string(content), nil +} + +func ListFiles(input json.RawMessage) (string, error) { + listFilesInput := ListFilesInput{} + err := json.Unmarshal(input, &listFilesInput) + if err != nil { + panic(err) + } + + dir := "." + if listFilesInput.Path != "" { + dir = listFilesInput.Path + } + + log.Printf("Listing files in directory: %s", dir) + var files []string + err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + relPath, err := filepath.Rel(dir, path) + if err != nil { + return err + } + + // Skip .devenv directory and its contents + if info.IsDir() && (relPath == ".devenv" || strings.HasPrefix(relPath, ".devenv/")) { + return filepath.SkipDir + } + + if relPath != "." { + if info.IsDir() { + files = append(files, relPath+"/") + } else { + files = append(files, relPath) + } + } + return nil + }) + + if err != nil { + log.Printf("Failed to list files in %s: %v", dir, err) + return "", err + } + + result, err := json.Marshal(files) + if err != nil { + return "", err + } + + log.Printf("Successfully listed %d files in %s", len(files), dir) + return string(result), nil +} + +func Bash(input json.RawMessage) (string, error) { + bashInput := BashInput{} + err := json.Unmarshal(input, &bashInput) + if err != nil { + return "", err + } + + log.Printf("Executing bash command: %s", bashInput.Command) + cmd := exec.Command("bash", "-c", bashInput.Command) + output, err := cmd.CombinedOutput() + if err != nil { + log.Printf("Bash command failed: %v", err) + return fmt.Sprintf("Command failed with error: %s\nOutput: %s", err.Error(), string(output)), nil + } + + log.Printf("Bash command executed successfully, output length: %d chars", len(output)) + return strings.TrimSpace(string(output)), nil +} + +func EditFile(input json.RawMessage) (string, error) { + editFileInput := EditFileInput{} + err := json.Unmarshal(input, &editFileInput) + if err != nil { + return "", err + } + + if editFileInput.Path == "" || editFileInput.OldStr == editFileInput.NewStr { + log.Printf("EditFile failed: invalid input parameters") + return "", fmt.Errorf("invalid input parameters") + } + + log.Printf("Editing file: %s (replacing %d chars with %d chars)", editFileInput.Path, len(editFileInput.OldStr), len(editFileInput.NewStr)) + content, err := os.ReadFile(editFileInput.Path) + if err != nil { + if os.IsNotExist(err) && editFileInput.OldStr == "" { + log.Printf("File does not exist, creating new file: %s", editFileInput.Path) + return createNewFile(editFileInput.Path, editFileInput.NewStr) + } + log.Printf("Failed to read file %s: %v", editFileInput.Path, err) + return "", err + } + + oldContent := string(content) + + // Special case: if old_str is empty, we're appending to the file + var newContent string + if editFileInput.OldStr == "" { + newContent = oldContent + editFileInput.NewStr + } else { + // Count occurrences first to ensure we have exactly one match + count := strings.Count(oldContent, editFileInput.OldStr) + if count == 0 { + log.Printf("EditFile failed: old_str not found in file %s", editFileInput.Path) + return "", fmt.Errorf("old_str not found in file") + } + if count > 1 { + log.Printf("EditFile failed: old_str found %d times in file %s, must be unique", count, editFileInput.Path) + return "", fmt.Errorf("old_str found %d times in file, must be unique", count) + } + + newContent = strings.Replace(oldContent, editFileInput.OldStr, editFileInput.NewStr, 1) + } + + err = os.WriteFile(editFileInput.Path, []byte(newContent), 0644) + if err != nil { + log.Printf("Failed to write file %s: %v", editFileInput.Path, err) + return "", err + } + + log.Printf("Successfully edited file %s", editFileInput.Path) + return "OK", nil +} + +func createNewFile(filePath, content string) (string, error) { + log.Printf("Creating new file: %s (%d bytes)", filePath, len(content)) + dir := path.Dir(filePath) + if dir != "." { + log.Printf("Creating directory: %s", dir) + err := os.MkdirAll(dir, 0755) + if err != nil { + log.Printf("Failed to create directory %s: %v", dir, err) + return "", fmt.Errorf("failed to create directory: %w", err) + } + } + + err := os.WriteFile(filePath, []byte(content), 0644) + if err != nil { + log.Printf("Failed to create file %s: %v", filePath, err) + return "", fmt.Errorf("failed to create file: %w", err) + } + + log.Printf("Successfully created file %s", filePath) + return fmt.Sprintf("Successfully created file %s", filePath), nil +} diff --git a/gpt_version/list_files_gpt.go b/gpt_version/list_files_gpt.go new file mode 100644 index 0000000..f497857 --- /dev/null +++ b/gpt_version/list_files_gpt.go @@ -0,0 +1,411 @@ +package main + +import ( + "bufio" + "context" + "encoding/json" + "flag" + "fmt" + "log" + "os" + "path/filepath" + "strings" + + "github.com/sashabaranov/go-openai" +) + +func main() { + verbose := flag.Bool("verbose", false, "enable verbose logging") + flag.Parse() + + if *verbose { + log.SetOutput(os.Stderr) + log.SetFlags(log.LstdFlags | log.Lshortfile) + log.Println("Verbose logging enabled") + } else { + log.SetOutput(os.Stdout) + log.SetFlags(0) + log.SetPrefix("") + } + + // Get API Key from environment variable, use default if not set + apiKey := os.Getenv("OPENAI_API_KEY") + if apiKey == "" { + // Set your ChatAnywhere free API Key here + apiKey = "YOUR_API_KEY" + } + + // Use ChatAnywhere proxy service (recommended for China) + config := openai.DefaultConfig(apiKey) + config.BaseURL = "https://api.chatanywhere.tech/v1" + // For international users: config.BaseURL = "https://api.chatanywhere.org/v1" + + client := openai.NewClientWithConfig(config) + if *verbose { + log.Println("OpenAI client initialized with ChatAnywhere") + log.Printf("Base URL: %s", config.BaseURL) + } + + scanner := bufio.NewScanner(os.Stdin) + getUserMessage := func() (string, bool) { + if !scanner.Scan() { + return "", false + } + return scanner.Text(), true + } + + tools := []ToolDefinition{ReadFileDefinition, ListFilesDefinition} + if *verbose { + log.Printf("Initialized %d tools", len(tools)) + } + agent := NewAgent(client, getUserMessage, tools, *verbose) + err := agent.Run(context.TODO()) + if err != nil { + fmt.Printf("Error: %s\n", err.Error()) + } +} + +func NewAgent( + client *openai.Client, + getUserMessage func() (string, bool), + tools []ToolDefinition, + verbose bool, +) *Agent { + return &Agent{ + client: client, + getUserMessage: getUserMessage, + tools: tools, + verbose: verbose, + } +} + +type Agent struct { + client *openai.Client + getUserMessage func() (string, bool) + tools []ToolDefinition + verbose bool +} + +func (a *Agent) Run(ctx context.Context) error { + conversation := []openai.ChatCompletionMessage{} + + if a.verbose { + log.Println("Starting chat session with tools enabled") + } + fmt.Println("Chat with GPT (use 'ctrl-c' to quit)") + + for { + fmt.Print("\u001b[94mYou\u001b[0m: ") + userInput, ok := a.getUserMessage() + if !ok { + if a.verbose { + log.Println("User input ended, breaking from chat loop") + } + break + } + + // Skip empty messages + if userInput == "" { + if a.verbose { + log.Println("Skipping empty message") + } + continue + } + + if a.verbose { + log.Printf("User input received: %q", userInput) + } + + userMessage := openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, + Content: userInput, + } + conversation = append(conversation, userMessage) + + if a.verbose { + log.Printf("Sending message to GPT, conversation length: %d", len(conversation)) + } + + message, err := a.runInference(ctx, conversation) + if err != nil { + if a.verbose { + log.Printf("Error during inference: %v", err) + } + return err + } + conversation = append(conversation, message) + + // Keep processing until GPT stops using tools + for { + // Check if there are tool calls in the message + if len(message.ToolCalls) == 0 { + // No tool calls, just print the message and break + if message.Content != "" { + fmt.Printf("\u001b[93mGPT\u001b[0m: %s\n", message.Content) + } + break + } + + // Process tool calls + if a.verbose { + log.Printf("Processing %d tool calls from GPT", len(message.ToolCalls)) + } + + var toolMessages []openai.ChatCompletionMessage + for _, toolCall := range message.ToolCalls { + if a.verbose { + log.Printf("Tool call detected: %s with arguments: %s", toolCall.Function.Name, toolCall.Function.Arguments) + } + fmt.Printf("\u001b[96mtool\u001b[0m: %s(%s)\n", toolCall.Function.Name, toolCall.Function.Arguments) + + // Find and execute the tool + var toolResult string + var toolError error + var toolFound bool + for _, tool := range a.tools { + if tool.Name == toolCall.Function.Name { + if a.verbose { + log.Printf("Executing tool: %s", tool.Name) + } + toolResult, toolError = tool.Function(json.RawMessage(toolCall.Function.Arguments)) + fmt.Printf("\u001b[92mresult\u001b[0m: %s\n", toolResult) + if toolError != nil { + fmt.Printf("\u001b[91merror\u001b[0m: %s\n", toolError.Error()) + } + if a.verbose { + if toolError != nil { + log.Printf("Tool execution failed: %v", toolError) + } else { + log.Printf("Tool execution successful, result length: %d chars", len(toolResult)) + } + } + toolFound = true + break + } + } + + if !toolFound { + toolError = fmt.Errorf("tool '%s' not found", toolCall.Function.Name) + fmt.Printf("\u001b[91merror\u001b[0m: %s\n", toolError.Error()) + } + + // Add tool result message + var content string + if toolError != nil { + content = fmt.Sprintf("Error: %s", toolError.Error()) + } else { + content = toolResult + } + + toolMessage := openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleTool, + Content: content, + ToolCallID: toolCall.ID, + } + toolMessages = append(toolMessages, toolMessage) + } + + // Add all tool messages to conversation + conversation = append(conversation, toolMessages...) + + if a.verbose { + log.Printf("Sending %d tool results back to GPT", len(toolMessages)) + } + + // Get GPT's response after tool execution + message, err = a.runInference(ctx, conversation) + if err != nil { + if a.verbose { + log.Printf("Error during followup inference: %v", err) + } + return err + } + conversation = append(conversation, message) + + if a.verbose { + log.Printf("Received followup response from GPT") + } + + // Continue loop to process the new message + } + } + + if a.verbose { + log.Println("Chat session ended") + } + return nil +} + +func (a *Agent) runInference(ctx context.Context, conversation []openai.ChatCompletionMessage) (openai.ChatCompletionMessage, error) { + // Convert tools to OpenAI function definitions + var functions []openai.FunctionDefinition + for _, tool := range a.tools { + functions = append(functions, openai.FunctionDefinition{ + Name: tool.Name, + Description: tool.Description, + Parameters: tool.Parameters, + }) + } + + model := openai.GPT3Dot5Turbo + if a.verbose { + log.Printf("Making API call to GPT with model: %s and %d tools", model, len(functions)) + } + + req := openai.ChatCompletionRequest{ + Model: model, + Messages: conversation, + MaxTokens: 1024, + Temperature: 0.7, + } + + // Add tools if available + if len(functions) > 0 { + var tools []openai.Tool + for _, fn := range functions { + tools = append(tools, openai.Tool{ + Type: openai.ToolTypeFunction, + Function: &fn, + }) + } + req.Tools = tools + req.ToolChoice = "auto" + } + + // Synchronous API call + resp, err := a.client.CreateChatCompletion(ctx, req) + + if a.verbose { + if err != nil { + log.Printf("API call failed: %v", err) + } else { + log.Printf("API call successful, response received") + } + } + + if err != nil { + return openai.ChatCompletionMessage{}, err + } + + if len(resp.Choices) == 0 { + return openai.ChatCompletionMessage{}, fmt.Errorf("no response choices returned") + } + + return resp.Choices[0].Message, nil +} + +type ToolDefinition struct { + Name string `json:"name"` + Description string `json:"description"` + Parameters map[string]interface{} `json:"parameters"` + Function func(input json.RawMessage) (string, error) +} + +var ReadFileDefinition = ToolDefinition{ + Name: "read_file", + Description: "Read the contents of a given relative file path. Use this when you want to see what's inside a file. Do not use this with directory names.", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "path": map[string]interface{}{ + "type": "string", + "description": "The relative path of a file in the working directory.", + }, + }, + "required": []string{"path"}, + }, + Function: ReadFile, +} + +var ListFilesDefinition = ToolDefinition{ + Name: "list_files", + Description: "List files and directories at a given path. If no path is provided, lists files in the current directory.", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "path": map[string]interface{}{ + "type": "string", + "description": "Optional relative path to list files from. Defaults to current directory if not provided.", + }, + }, + "required": []string{}, + }, + Function: ListFiles, +} + +type ReadFileInput struct { + Path string `json:"path"` +} + +type ListFilesInput struct { + Path string `json:"path,omitempty"` +} + +func ReadFile(input json.RawMessage) (string, error) { + readFileInput := ReadFileInput{} + err := json.Unmarshal(input, &readFileInput) + if err != nil { + return "", err + } + + content, err := os.ReadFile(readFileInput.Path) + if err != nil { + return "", err + } + return string(content), nil +} + +func ListFiles(input json.RawMessage) (string, error) { + listFilesInput := ListFilesInput{} + err := json.Unmarshal(input, &listFilesInput) + if err != nil { + return "", err + } + + dir := "." + if listFilesInput.Path != "" { + dir = listFilesInput.Path + } + + log.Printf("Listing files in directory: %s", dir) + + var files []string + err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + + relPath, err := filepath.Rel(dir, path) + if err != nil { + return err + } + + // Skip .devenv directory and its contents + if info.IsDir() && (relPath == ".devenv" || strings.HasPrefix(relPath, ".devenv/")) { + return filepath.SkipDir + } + + if relPath != "." { + if info.IsDir() { + files = append(files, relPath+"/") + } else { + files = append(files, relPath) + } + } + return nil + }) + + if err != nil { + log.Printf("Failed to list files in %s: %v", dir, err) + return "", err + } + + log.Printf("Successfully listed %d items in %s", len(files), dir) + + result, err := json.Marshal(files) + if err != nil { + return "", err + } + + return string(result), nil +} diff --git a/gpt_version/read_gpt.go b/gpt_version/read_gpt.go new file mode 100644 index 0000000..e723174 --- /dev/null +++ b/gpt_version/read_gpt.go @@ -0,0 +1,337 @@ +package main + +import ( + "bufio" + "context" + "encoding/json" + "flag" + "fmt" + "log" + "os" + + "github.com/sashabaranov/go-openai" +) + +func main() { + verbose := flag.Bool("verbose", false, "enable verbose logging") + flag.Parse() + + if *verbose { + log.SetOutput(os.Stderr) + log.SetFlags(log.LstdFlags | log.Lshortfile) + log.Println("Verbose logging enabled") + } else { + log.SetOutput(os.Stdout) + log.SetFlags(0) + log.SetPrefix("") + } + + // Get API Key from environment variable, use default if not set + apiKey := os.Getenv("OPENAI_API_KEY") + if apiKey == "" { + // Set your ChatAnywhere free API Key here + apiKey = "YOUR_API_KEY" + } + + // Use ChatAnywhere proxy service (recommended for China) + config := openai.DefaultConfig(apiKey) + config.BaseURL = "https://api.chatanywhere.tech/v1" + // For international users: config.BaseURL = "https://api.chatanywhere.org/v1" + + client := openai.NewClientWithConfig(config) + if *verbose { + log.Println("OpenAI client initialized with ChatAnywhere") + log.Printf("Base URL: %s", config.BaseURL) + } + + scanner := bufio.NewScanner(os.Stdin) + getUserMessage := func() (string, bool) { + if !scanner.Scan() { + return "", false + } + return scanner.Text(), true + } + + tools := []ToolDefinition{ReadFileDefinition} + if *verbose { + log.Printf("Initialized %d tools", len(tools)) + } + agent := NewAgent(client, getUserMessage, tools, *verbose) + err := agent.Run(context.TODO()) + if err != nil { + fmt.Printf("Error: %s\n", err.Error()) + } +} + +func NewAgent( + client *openai.Client, + getUserMessage func() (string, bool), + tools []ToolDefinition, + verbose bool, +) *Agent { + return &Agent{ + client: client, + getUserMessage: getUserMessage, + tools: tools, + verbose: verbose, + } +} + +type Agent struct { + client *openai.Client + getUserMessage func() (string, bool) + tools []ToolDefinition + verbose bool +} + +func (a *Agent) Run(ctx context.Context) error { + var conversation []openai.ChatCompletionMessage + + if a.verbose { + log.Println("Starting chat session with tools enabled") + } + fmt.Println("Chat with GPT (use 'ctrl-c' to quit)") + + for { + fmt.Print("\u001b[94mYou\u001b[0m: ") + userInput, ok := a.getUserMessage() + if !ok { + if a.verbose { + log.Println("User input ended, breaking from chat loop") + } + break + } + + // Skip empty messages + if userInput == "" { + if a.verbose { + log.Println("Skipping empty message") + } + continue + } + + if a.verbose { + log.Printf("User input received: %q", userInput) + } + + userMessage := openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleUser, + Content: userInput, + } + conversation = append(conversation, userMessage) + + if a.verbose { + log.Printf("Sending message to GPT, conversation length: %d", len(conversation)) + } + + message, err := a.runInference(ctx, conversation) + if err != nil { + if a.verbose { + log.Printf("Error during inference: %v", err) + } + return err + } + conversation = append(conversation, message) + + // Keep processing until GPT stops using tools + for { + // Check if there are tool calls in the message + if len(message.ToolCalls) == 0 { + // No tool calls, just print the message and break + if message.Content != "" { + fmt.Printf("\u001b[93mGPT\u001b[0m: %s\n", message.Content) + } + break + } + + // Process tool calls + if a.verbose { + log.Printf("Processing %d tool calls from GPT", len(message.ToolCalls)) + } + + var toolMessages []openai.ChatCompletionMessage + for _, toolCall := range message.ToolCalls { + if a.verbose { + log.Printf("Tool call detected: %s with arguments: %s", toolCall.Function.Name, toolCall.Function.Arguments) + } + fmt.Printf("\u001b[96mtool\u001b[0m: %s(%s)\n", toolCall.Function.Name, toolCall.Function.Arguments) + + // Find and execute the tool + var toolResult string + var toolError error + var toolFound bool + for _, tool := range a.tools { + if tool.Name == toolCall.Function.Name { + if a.verbose { + log.Printf("Executing tool: %s", tool.Name) + } + toolResult, toolError = tool.Function(json.RawMessage(toolCall.Function.Arguments)) + fmt.Printf("\u001b[92mresult\u001b[0m: %s\n", toolResult) + if toolError != nil { + fmt.Printf("\u001b[91merror\u001b[0m: %s\n", toolError.Error()) + } + if a.verbose { + if toolError != nil { + log.Printf("Tool execution failed: %v", toolError) + } else { + log.Printf("Tool execution successful, result length: %d chars", len(toolResult)) + } + } + toolFound = true + break + } + } + + if !toolFound { + toolError = fmt.Errorf("tool '%s' not found", toolCall.Function.Name) + fmt.Printf("\u001b[91merror\u001b[0m: %s\n", toolError.Error()) + } + + // Add tool result message + var content string + if toolError != nil { + content = fmt.Sprintf("Error: %s", toolError.Error()) + } else { + content = toolResult + } + + toolMessage := openai.ChatCompletionMessage{ + Role: openai.ChatMessageRoleTool, + Content: content, + ToolCallID: toolCall.ID, + } + toolMessages = append(toolMessages, toolMessage) + } + + // Add all tool messages to conversation + conversation = append(conversation, toolMessages...) + + if a.verbose { + log.Printf("Sending %d tool results back to GPT", len(toolMessages)) + } + + // Get GPT's response after tool execution + message, err = a.runInference(ctx, conversation) + if err != nil { + if a.verbose { + log.Printf("Error during followup inference: %v", err) + } + return err + } + conversation = append(conversation, message) + + if a.verbose { + log.Printf("Received followup response from GPT") + } + + // Continue loop to process the new message + } + } + + if a.verbose { + log.Println("Chat session ended") + } + return nil +} + +func (a *Agent) runInference(ctx context.Context, conversation []openai.ChatCompletionMessage) (openai.ChatCompletionMessage, error) { + // Convert tools to OpenAI function definitions + var functions []openai.FunctionDefinition + for _, tool := range a.tools { + functions = append(functions, openai.FunctionDefinition{ + Name: tool.Name, + Description: tool.Description, + Parameters: tool.Parameters, + }) + } + + model := openai.GPT3Dot5Turbo + if a.verbose { + log.Printf("Making API call to GPT with model: %s and %d tools", model, len(functions)) + } + + req := openai.ChatCompletionRequest{ + Model: model, + Messages: conversation, + MaxTokens: 1024, + Temperature: 0.7, + } + + // Add tools if available + if len(functions) > 0 { + var tools []openai.Tool + for _, fn := range functions { + tools = append(tools, openai.Tool{ + Type: openai.ToolTypeFunction, + Function: &fn, + }) + } + req.Tools = tools + req.ToolChoice = "auto" + } + + // Synchronous API call + resp, err := a.client.CreateChatCompletion(ctx, req) + + if a.verbose { + if err != nil { + log.Printf("API call failed: %v", err) + } else { + log.Printf("API call successful, response received") + } + } + + if err != nil { + return openai.ChatCompletionMessage{}, err + } + + if len(resp.Choices) == 0 { + return openai.ChatCompletionMessage{}, fmt.Errorf("no response choices returned") + } + + return resp.Choices[0].Message, nil +} + +type ToolDefinition struct { + Name string `json:"name"` + Description string `json:"description"` + Parameters map[string]interface{} `json:"parameters"` + Function func(input json.RawMessage) (string, error) +} + +var ReadFileDefinition = ToolDefinition{ + Name: "read_file", + Description: "Read the contents of a given relative file path. Use this when you want to see what's inside a file. Do not use this with directory names.", + Parameters: map[string]interface{}{ + "type": "object", + "properties": map[string]interface{}{ + "path": map[string]interface{}{ + "type": "string", + "description": "The relative path of a file in the working directory.", + }, + }, + "required": []string{"path"}, + }, + Function: ReadFile, +} + +type ReadFileInput struct { + Path string `json:"path"` +} + +func ReadFile(input json.RawMessage) (string, error) { + readFileInput := ReadFileInput{} + err := json.Unmarshal(input, &readFileInput) + if err != nil { + return "", err + } + + log.Printf("Reading file: %s", readFileInput.Path) + content, err := os.ReadFile(readFileInput.Path) + if err != nil { + log.Printf("Failed to read file %s: %v", readFileInput.Path, err) + return "", err + } + log.Printf("Successfully read file %s (%d bytes)", readFileInput.Path, len(content)) + return string(content), nil +}