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  }