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..52ac200
--- /dev/null
+++ b/app/pkg/agent/long_files_test.go
@@ -0,0 +1,118 @@
+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.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
+}
+
+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)
+ }
+}