From ee3ad3be546453e1bd46ca1d08da3c47afd78f15 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Sat, 16 Nov 2024 18:25:14 -0800 Subject: [PATCH 1/3] Start hacking on some code for dealing with long files. --- app/pkg/agent/edit_file_prompt.tmpl | 14 ++++ app/pkg/agent/long_files.go | 62 ++++++++++++++++ app/pkg/agent/long_files_test.go | 111 ++++++++++++++++++++++++++++ 3 files changed, 187 insertions(+) create mode 100644 app/pkg/agent/edit_file_prompt.tmpl create mode 100644 app/pkg/agent/long_files.go create mode 100644 app/pkg/agent/long_files_test.go diff --git a/app/pkg/agent/edit_file_prompt.tmpl b/app/pkg/agent/edit_file_prompt.tmpl new file mode 100644 index 0000000..96e2bc4 --- /dev/null +++ b/app/pkg/agent/edit_file_prompt.tmpl @@ -0,0 +1,14 @@ +# Edit File Snippet Task + +You will be given a snippet of a file. You will be asked to make changes to that snippet. +Output the modified snippet and nothing else. +Do not make any changes that you aren't explicitly asked to make. +Do not add additional markdown formatting such as putting the snippet in a code block. + + +{{.Changes}} + + + +{{.Text}} + diff --git a/app/pkg/agent/long_files.go b/app/pkg/agent/long_files.go new file mode 100644 index 0000000..f8c89cc --- /dev/null +++ b/app/pkg/agent/long_files.go @@ -0,0 +1,62 @@ +package agent + +import ( + "bufio" + _ "embed" + "github.com/jlewi/monogo/helpers" + "os" + "strings" + "text/template" +) + +//go:embed edit_file_prompt.tmpl +var editPrompt string + +var ( + editTemplate = template.Must(template.New("prompt").Parse(editPrompt)) +) + +type editPromptInput struct { + Changes string + Text string +} + +// FileSnippet holds the start and end lines and the actual text of the segment. +type FileSnippet struct { + StartLine int + EndLine int + Text string +} + +// ReadFileSegment reads the file at filePath and returns the lines between startLine and endLine (inclusive) as a FileSegment. +func ReadFileSegment(filePath string, startLine, endLine int) (FileSnippet, error) { + file, err := os.Open(filePath) + if err != nil { + return FileSnippet{}, err + } + defer helpers.DeferIgnoreError(file.Close) + + scanner := bufio.NewScanner(file) + var textBuilder strings.Builder + currentLine := 0 + + for scanner.Scan() { + currentLine++ + if currentLine >= startLine && currentLine <= endLine { + textBuilder.WriteString(scanner.Text() + "\n") + } + if currentLine > endLine { + break + } + } + + if err := scanner.Err(); err != nil { + return FileSnippet{}, err + } + + return FileSnippet{ + StartLine: startLine, + EndLine: endLine, + Text: textBuilder.String(), + }, nil +} diff --git a/app/pkg/agent/long_files_test.go b/app/pkg/agent/long_files_test.go new file mode 100644 index 0000000..5d4fac5 --- /dev/null +++ b/app/pkg/agent/long_files_test.go @@ -0,0 +1,111 @@ +package agent + +import ( + "context" + "fmt" + "github.com/jlewi/foyle/app/pkg/config" + "github.com/jlewi/foyle/app/pkg/oai" + "github.com/pkg/errors" + "github.com/sashabaranov/go-openai" + "github.com/sergi/go-diff/diffmatchpatch" + "go.uber.org/zap" + "os" + "strings" + "testing" +) + +func run() error { + inputFile := "/Users/jlewi/tmp/clusters.tf" + + if err := config.InitViper(nil); err != nil { + return errors.Wrapf(err, "Error initializing viper") + } + + lConfig := zap.NewDevelopmentConfig() + l, err := lConfig.Build() + if err != nil { + return errors.Wrapf(err, "Error creating logger") + } + + zap.ReplaceGlobals(l) + + cfg := config.GetConfig() + + client, err := oai.NewClient(*cfg) + + if err != nil { + return errors.Wrapf(err, "Error creating OpenAI client") + } + + segment, err := ReadFileSegment(inputFile, 5570, 5594) + if err != nil { + return errors.Wrapf(err, "Error reading file segment") + } + + var sb strings.Builder + input := editPromptInput{ + Changes: "Change the cluster u35 to use the region spaincentral. Add the label owner=foo to the cluster", + Text: segment.Text, + } + + if err := editTemplate.Execute(&sb, input); err != nil { + return errors.Wrapf(err, "Failed to execute prompt template") + } + + messages := []openai.ChatCompletionMessage{ + {Role: openai.ChatMessageRoleUser, + Content: sb.String(), + }, + } + request := openai.ChatCompletionRequest{ + Model: openai.GPT4oMini, + Messages: messages, + MaxTokens: 8000, + Temperature: temperature, + } + + resp, err := client.CreateChatCompletion(context.Background(), request) + if err != nil { + return errors.Wrapf(err, "Error calling OpenAI") + } + + if len(resp.Choices) != 1 { + return errors.Errorf("Expected 1 choice but got %v", len(resp.Choices)) + } + + output := resp.Choices[0].Message.Content + fmt.Printf("ChatGPT Output:\n%v\n", output) + + // Compute a diff between the original text and the output + dmp := diffmatchpatch.New() + + //diffs := dmp.DiffMain(segment.Text, output, false) + // + //// Create a patch + //patches := dmp.PatchMake(segment.Text, diffs) + + patches := dmp.PatchMake(segment.Text, output) + + //// Convert the diff to unified format + //patch, err := diff.ToUnified(ud, unifiedDiff) + //if err != nil { + // fmt.Println("Error generating unified diff:", err) + // return + //} + for _, p := range patches { + fmt.Printf("Patch:\n%v\n", p) + } + return nil +} + +func Test_LongFiles(t *testing.T) { + // The purpose of this test is to experiment with different patterns for modifying very long (e.g. 8K-10K lines) + // files. Such as Terraform files + if os.Getenv("GITHUB_ACTIONS") != "" { + t.Skipf("Test is skipped in GitHub actions") + } + + if err := run(); err != nil { + t.Fatalf("Error running test: %v", err) + } +} From 7db119f475be298823c44a000d833cc220a46ccb Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Sat, 16 Nov 2024 18:28:53 -0800 Subject: [PATCH 2/3] Print the patch as a string; the format still isn't very human friendly. --- app/pkg/agent/long_files_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/pkg/agent/long_files_test.go b/app/pkg/agent/long_files_test.go index 5d4fac5..aef8697 100644 --- a/app/pkg/agent/long_files_test.go +++ b/app/pkg/agent/long_files_test.go @@ -93,7 +93,7 @@ func run() error { // return //} for _, p := range patches { - fmt.Printf("Patch:\n%v\n", p) + fmt.Printf("Patch:\n%v\n", p.String()) } return nil } From 1baab366064b9ce1b602322c3bbdcab029962378 Mon Sep 17 00:00:00 2001 From: Jeremy Lewi Date: Wed, 20 Nov 2024 13:23:45 -0800 Subject: [PATCH 3/3] Continue hacking on a tool to modify long files. --- app/pkg/agent/long_files_test.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/app/pkg/agent/long_files_test.go b/app/pkg/agent/long_files_test.go index aef8697..52ac200 100644 --- a/app/pkg/agent/long_files_test.go +++ b/app/pkg/agent/long_files_test.go @@ -95,6 +95,13 @@ func run() error { for _, p := range patches { fmt.Printf("Patch:\n%v\n", p.String()) } + + // TODO(jeremy): How do we adjust the patch to be relative to the original file? + // Maybe we should just replace the original lines with the output lines. + // if we wanted to we could then compute a diff between the modified and aligned file. + // So our patch format should be a start and end line that will be replaced with our fragment. + // This is human readable. + return nil }