github.com/xyproto/orbiton/v2@v2.65.12-0.20240516144430-e10a419274ec/chatgpt.go (about) 1 package main 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "strings" 8 "sync" 9 10 "github.com/PullRequestInc/go-gpt3" 11 "github.com/xyproto/env/v2" 12 "github.com/xyproto/mode" 13 "github.com/xyproto/vt100" 14 ) 15 16 const ( 17 codePrompt = "Write it in %s and include comments where it makes sense. The code should be concise, correct and expertly created. Comments above functions should start with the function name." 18 continuePrompt = "Write the next 10 lines of this %s program:\n" 19 textPrompt = "Write it in %s. It should be expertly written, concise and correct." 20 ) 21 22 var fixLineMut sync.Mutex 23 24 // ProgrammingLanguage returns true if the current mode appears to be a programming language (and not a markup language etc) 25 // The main question is "can it be compiled or built to something?". Dockerfiles are borderline config files. 26 func (e *Editor) ProgrammingLanguage() bool { 27 switch e.mode { 28 case mode.AIDL, mode.ASCIIDoc, mode.Amber, mode.Bazel, mode.Blank, mode.Config, mode.Email, mode.Git, mode.HIDL, mode.HTML, mode.JSON, mode.Log, mode.M4, mode.ManPage, mode.Markdown, mode.Nroff, mode.PolicyLanguage, mode.ReStructured, mode.SCDoc, mode.SQL, mode.Shader, mode.Text, mode.XML: 29 return false 30 } 31 return true 32 } 33 34 // AddSpaceAfterComments adds a space after single-line comments 35 func (e *Editor) AddSpaceAfterComments(generatedLine string) string { 36 var ( 37 singleLineComment = e.SingleLineCommentMarker() 38 trimmedLine = strings.TrimSpace(generatedLine) 39 ) 40 if len(trimmedLine) > 2 && e.ProgrammingLanguage() && strings.HasPrefix(trimmedLine, singleLineComment) && !strings.HasPrefix(trimmedLine, singleLineComment+" ") && !strings.HasPrefix(generatedLine, "#!") { 41 return strings.Replace(generatedLine, singleLineComment, singleLineComment+" ", 1) 42 } 43 return generatedLine 44 } 45 46 // GenerateTokens uses the ChatGTP API to generate text. n is the maximum number of tokens. 47 // The global atomic Bool "ContinueGeneratingTokens" controls when the text generation should stop. 48 func (e *Editor) GenerateTokens(keyHolder *KeyHolder, prompt string, n int, temperature float32, model string, newToken func(string)) error { 49 if keyHolder == nil { 50 return errors.New("no API key") 51 } 52 client := gpt3.NewClient(keyHolder.Key) 53 chatContext, cancelFunction := context.WithCancel(context.Background()) 54 defer cancelFunction() 55 err := client.CompletionStreamWithEngine( 56 chatContext, 57 model, 58 gpt3.CompletionRequest{ 59 Prompt: []string{prompt}, 60 MaxTokens: gpt3.IntPtr(n), 61 Temperature: gpt3.Float32Ptr(temperature), 62 }, func(resp *gpt3.CompletionResponse) { 63 newToken(resp.Choices[0].Text) 64 if !e.generatingTokens { 65 cancelFunction() 66 } 67 }) 68 return err 69 } 70 71 // TODO: Find an exact way to find the number of tokens in the prompt, from a ChatGPT point of view 72 func countTokens(s string) int { 73 // Multiplying with 1.1 and adding 100, until the OpenAI API for counting tokens is used 74 return int(float64(len(strings.Fields(s)))*1.1 + 100) 75 } 76 77 // FixLine will try to correct the line at the given lineIndex in the editor, using ChatGPT 78 func (e *Editor) FixLine(c *vt100.Canvas, status *StatusBar, lineIndex LineIndex, disableFixAsYouTypeOnError bool) { 79 80 line := e.Line(lineIndex) 81 if strings.TrimSpace(line) == "" { 82 // Nothing to do 83 return 84 } 85 86 var temperature float32 // Low temperature for fixing grammar and issues 87 88 // Select a model 89 gptModel, gptModelTokens := gpt3.TextDavinci003Engine, 4000 90 // gptModel, gptModelTokens := "gpt-3.5-turbo", 4000 // only for chat 91 // gptModel, gptModelTokens := "text-curie-001", 2048 // simpler and faster 92 // gptModel, gptModelTokens := "text-ada-001", 2048 // even simpler and even faster 93 94 prompt := "Make as few changes as possible to this line in order to correct any typos or obvious grammatical errors, but only output EITHER the exact same line OR the corrected line! Here it is: " + line 95 if e.ProgrammingLanguage() { // fix a line of code or a line of text? 96 prompt = "Make as few changes as possible to this line of " + e.mode.String() + " code in order to correct any typos or obvious grammatical errors, but only output EITHER the exact same line OR the corrected line! Here it is: " + line 97 } 98 99 // Find the maxTokens value that will be sent to the OpenAI API 100 amountOfPromptTokens := countTokens(prompt) 101 maxTokens := gptModelTokens - amountOfPromptTokens // The user can press Esc when there are enough tokens 102 if maxTokens < 1 { 103 status.SetErrorMessage("ChatGPT API request is too long") 104 status.Show(c, e) 105 // Don't disable "fix as you type" if this happens 106 return 107 } 108 109 // Start generating the code/text while inserting words into the editor as it happens 110 e.generatingTokens = true // global 111 var ( 112 currentLeadingWhitespace = e.LeadingWhitespaceAt(lineIndex) 113 generatedLine string 114 newContents string 115 newTrimmedContents string 116 ) 117 118 fixLineMut.Lock() 119 if err := e.GenerateTokens(openAIKeyHolder, prompt, maxTokens, temperature, gptModel, func(word string) { 120 generatedLine = strings.TrimSpace(generatedLine) + word 121 newTrimmedContents = e.AddSpaceAfterComments(generatedLine) 122 newContents = currentLeadingWhitespace + newTrimmedContents 123 }); err != nil { 124 e.redrawCursor = true 125 errorMessage := err.Error() 126 if !strings.Contains(errorMessage, "context") { 127 128 e.End(c) 129 status.SetError(err) 130 status.Show(c, e) 131 132 if disableFixAsYouTypeOnError { 133 e.fixAsYouType = false 134 } 135 136 return 137 } 138 } 139 140 if e.TrimmedLineAt(lineIndex) != newTrimmedContents { 141 e.SetLine(lineIndex, newContents) 142 } 143 144 fixLineMut.Unlock() 145 } 146 147 // FixCodeOrText tries to fix the current line 148 func (e *Editor) FixCodeOrText(c *vt100.Canvas, status *StatusBar, disableFixAsYouTypeOnError bool) { 149 if openAIKeyHolder == nil { 150 status.SetErrorMessage("ChatGPT API key is empty") 151 status.Show(c, e) 152 if disableFixAsYouTypeOnError { 153 e.fixAsYouType = false 154 } 155 return 156 } 157 go e.FixLine(c, status, e.DataY(), disableFixAsYouTypeOnError) 158 } 159 160 // GenerateCodeOrText will try to generate and insert text at the corrent position in the editor, given a ChatGPT prompt 161 func (e *Editor) GenerateCodeOrText(c *vt100.Canvas, status *StatusBar, bookmark *Position) { 162 if openAIKeyHolder == nil { 163 status.SetErrorMessage("ChatGPT API key is empty") 164 status.Show(c, e) 165 return 166 } 167 168 trimmedLine := e.TrimmedLine() 169 170 go func() { 171 172 // Strip away any comment markers or leading exclamation marks, 173 // and trim away spaces at the end. 174 prompt := strings.TrimPrefix(trimmedLine, e.SingleLineCommentMarker()) 175 prompt = strings.TrimPrefix(prompt, "!") 176 prompt = strings.TrimSpace(prompt) 177 178 const ( 179 generateText = iota 180 generateCode 181 continueCode 182 ) 183 184 generationType := generateText // generateCode // continueCode 185 if e.ProgrammingLanguage() { 186 generationType = generateCode 187 if prompt == "" { 188 generationType = continueCode 189 } 190 } 191 192 // Determine the temperature 193 var defaultTemperature float32 194 switch generationType { 195 case generateText: 196 defaultTemperature = 0.8 197 } 198 temperature := env.Float32("CHATGPT_TEMPERATURE", defaultTemperature) 199 200 // Select a model 201 gptModel, gptModelTokens := gpt3.TextDavinci003Engine, 4000 202 // gptModel, gptModelTokens := "gpt-3.5-turbo", 4000 // only for chat 203 // gptModel, gptModelTokens := "text-curie-001", 2048 // simpler and faster 204 // gptModel, gptModelTokens := "text-ada-001", 2048 // even simpler and even faster 205 206 switch generationType { 207 case continueCode: 208 gptModel, gptModelTokens = "code-davinci-002", 8000 209 // gptModel, gptModelTokens = "code-cushman-001", 2048 // slightly simpler and slightly faster 210 } 211 212 // Prefix the prompt 213 switch generationType { 214 case generateCode: 215 prompt += ". " + fmt.Sprintf(codePrompt, e.mode.String()) 216 case continueCode: 217 prompt += ". " + fmt.Sprintf(continuePrompt, e.mode.String()) + "\n" 218 // gather about 2000 tokens/fields from the current file and use that as the prompt 219 startTokens := strings.Fields(e.String()) 220 gatherNTokens := gptModelTokens - countTokens(prompt) 221 if len(startTokens) > gatherNTokens { 222 startTokens = startTokens[len(startTokens)-gatherNTokens:] 223 } 224 prompt += strings.Join(startTokens, " ") 225 case generateText: 226 prompt += ". " + fmt.Sprintf(textPrompt, e.mode.String()) 227 } 228 229 // Set a suitable status bar text 230 status.ClearAll(c) 231 switch generationType { 232 case generateText: 233 status.SetMessage("Generating text...") 234 case generateCode: 235 status.SetMessage("Generating code...") 236 case continueCode: 237 status.SetMessage("Continuing code...") 238 } 239 status.Show(c, e) 240 241 // Find the maxTokens value that will be sent to the OpenAI API 242 amountOfPromptTokens := countTokens(prompt) 243 maxTokens := gptModelTokens - amountOfPromptTokens // The user can press Esc when there are enough tokens 244 if maxTokens < 1 { 245 status.SetErrorMessage("ChatGPT API request is too long") 246 status.Show(c, e) 247 return 248 } 249 250 // Start generating the code/text while inserting words into the editor as it happens 251 currentLeadingWhitespace := e.LeadingWhitespace() 252 e.generatingTokens = true // global 253 first := true 254 var generatedLine string 255 if err := e.GenerateTokens(openAIKeyHolder, prompt, maxTokens, temperature, gptModel, func(word string) { 256 generatedLine += word 257 if strings.HasSuffix(generatedLine, "\n") { 258 newContents := currentLeadingWhitespace + e.AddSpaceAfterComments(generatedLine) 259 e.SetCurrentLine(newContents) 260 if !first { 261 if !e.EmptyTrimmedLine() { 262 e.InsertLineBelow() 263 e.pos.sy++ 264 } 265 } else { 266 e.DeleteCurrentLineMoveBookmark(bookmark) 267 first = false 268 } 269 generatedLine = "" 270 } else { 271 e.SetCurrentLine(currentLeadingWhitespace + e.AddSpaceAfterComments(generatedLine)) 272 } 273 // "refresh" 274 e.MakeConsistent() 275 e.DrawLines(c, true, false) 276 e.redrawCursor = true 277 }); err != nil { 278 e.redrawCursor = true 279 errorMessage := err.Error() 280 if !strings.Contains(errorMessage, "context") { 281 e.End(c) 282 status.SetError(err) 283 status.Show(c, e) 284 return 285 } 286 } 287 e.End(c) 288 289 if e.generatingTokens { // global 290 if first { // Nothing was generated 291 status.SetMessageAfterRedraw("Nothing was generated") 292 } else { 293 status.SetMessageAfterRedraw("Done") 294 } 295 } else { 296 status.SetMessageAfterRedraw("Stopped") 297 } 298 299 e.RedrawAtEndOfKeyLoop(c, status) 300 301 }() 302 }