code_completion.go 6.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. package llm
  2. import (
  3. "context"
  4. "regexp"
  5. "strconv"
  6. "strings"
  7. "sync"
  8. "github.com/0xJacky/Nginx-UI/settings"
  9. "github.com/sashabaranov/go-openai"
  10. "github.com/uozi-tech/cosy/logger"
  11. )
  12. const (
  13. MaxTokens = 2000
  14. Temperature = 1
  15. // SystemPrompt Build system prompt and user prompt
  16. SystemPrompt = "You are a code completion assistant. " +
  17. "Complete the provided code snippet based on the context and instruction." +
  18. "[IMPORTANT] Keep the original code indentation."
  19. )
  20. // Position the cursor position
  21. type Position struct {
  22. Row int `json:"row"`
  23. Column int `json:"column"`
  24. }
  25. // CodeCompletionRequest the code completion request
  26. type CodeCompletionRequest struct {
  27. RequestID string `json:"request_id"`
  28. UserID uint64 `json:"user_id"`
  29. Context string `json:"context"`
  30. Code string `json:"code"`
  31. Suffix string `json:"suffix"`
  32. Language string `json:"language"`
  33. Position Position `json:"position"`
  34. CurrentIndent string `json:"current_indent"`
  35. }
  36. var (
  37. requestContext = make(map[uint64]context.CancelFunc)
  38. mutex sync.Mutex
  39. )
  40. func (c *CodeCompletionRequest) Send() (completedCode string, err error) {
  41. if cancel, ok := requestContext[c.UserID]; ok {
  42. logger.Infof("Code completion request cancelled for user %d", c.UserID)
  43. cancel()
  44. }
  45. mutex.Lock()
  46. ctx, cancel := context.WithCancel(context.Background())
  47. defer cancel()
  48. requestContext[c.UserID] = cancel
  49. mutex.Unlock()
  50. defer func() {
  51. mutex.Lock()
  52. delete(requestContext, c.UserID)
  53. mutex.Unlock()
  54. }()
  55. openaiClient, err := GetClient()
  56. if err != nil {
  57. return
  58. }
  59. // Build user prompt with code and instruction
  60. userPrompt := "Here is a file written in " + c.Language + ":\n```\n" + c.Context + "\n```\n"
  61. userPrompt += "I'm editing at row " + strconv.Itoa(c.Position.Row) + ", column " + strconv.Itoa(c.Position.Column) + ".\n"
  62. userPrompt += "Code before cursor:\n```\n" + c.Code + "\n```\n"
  63. if c.Suffix != "" {
  64. userPrompt += "Code after cursor:\n```\n" + c.Suffix + "\n```\n"
  65. }
  66. userPrompt += "Instruction: Only provide the completed code that should be inserted at the cursor position without explanations. " +
  67. "The code should be syntactically correct and follow best practices for " + c.Language + ". " +
  68. "IMPORTANT: If the cursor is at the end of a line and the completion should start on a new line, begin with a newline character. " +
  69. "For multi-line completions, use proper indentation - the current line uses '" + c.CurrentIndent + "' as base indentation. " +
  70. "Each new line should maintain consistent indentation levels appropriate for the code structure."
  71. messages := []openai.ChatCompletionMessage{
  72. {
  73. Role: openai.ChatMessageRoleSystem,
  74. Content: SystemPrompt,
  75. },
  76. {
  77. Role: openai.ChatMessageRoleUser,
  78. Content: userPrompt,
  79. },
  80. }
  81. req := openai.ChatCompletionRequest{
  82. Model: settings.OpenAISettings.GetCodeCompletionModel(),
  83. Messages: messages,
  84. MaxCompletionTokens: MaxTokens,
  85. Temperature: Temperature,
  86. }
  87. // Make a direct (non-streaming) call to the API
  88. response, err := openaiClient.CreateChatCompletion(ctx, req)
  89. if err != nil {
  90. return
  91. }
  92. completedCode = response.Choices[0].Message.Content
  93. // extract the last word of the code
  94. lastWord := extractLastWord(c.Code)
  95. completedCode = cleanCompletionResponse(completedCode, lastWord, c.CurrentIndent)
  96. logger.Infof("Code completion response: %s", completedCode)
  97. return
  98. }
  99. // extractLastWord extract the last word of the code
  100. func extractLastWord(code string) string {
  101. if code == "" {
  102. return ""
  103. }
  104. // define a regex to match word characters (letters, numbers, underscores)
  105. re := regexp.MustCompile(`[a-zA-Z0-9_]+$`)
  106. // find the last word of the code
  107. match := re.FindString(code)
  108. return match
  109. }
  110. // cleanCompletionResponse removes any <think></think> tags and their content from the completion response
  111. // and strips the already entered code from the completion while preserving formatting
  112. func cleanCompletionResponse(response string, lastWord string, currentIndent string) (cleanResp string) {
  113. // remove <think></think> tags and their content using regex
  114. re := regexp.MustCompile(`<think>[\s\S]*?</think>`)
  115. cleanResp = re.ReplaceAllString(response, "")
  116. // remove markdown code block tags
  117. codeBlockRegex := regexp.MustCompile("```(?:[a-zA-Z]+)?\n((?:.|\n)*?)\n```")
  118. matches := codeBlockRegex.FindStringSubmatch(cleanResp)
  119. if len(matches) > 1 {
  120. // extract the code block content, preserve leading newlines
  121. cleanResp = matches[1]
  122. } else {
  123. // if no code block is found, only trim trailing whitespace
  124. cleanResp = strings.TrimRight(cleanResp, " \t")
  125. }
  126. // remove markdown backticks but preserve newlines
  127. cleanResp = strings.Trim(cleanResp, "`")
  128. // if there is a last word, and the completion result starts with the last word, remove the already entered part
  129. if lastWord != "" && strings.HasPrefix(strings.TrimLeft(cleanResp, " \t\n"), lastWord) {
  130. // Find the position after the last word, preserving leading whitespace
  131. trimmed := strings.TrimLeft(cleanResp, " \t\n")
  132. leadingWhitespace := cleanResp[:len(cleanResp)-len(trimmed)]
  133. cleanResp = leadingWhitespace + trimmed[len(lastWord):]
  134. }
  135. // Fix indentation for multi-line completions
  136. cleanResp = fixCompletionIndentation(cleanResp, currentIndent)
  137. return
  138. }
  139. // fixCompletionIndentation ensures proper indentation for multi-line completions
  140. func fixCompletionIndentation(completion string, baseIndent string) string {
  141. lines := strings.Split(completion, "\n")
  142. if len(lines) <= 1 {
  143. return completion
  144. }
  145. result := []string{lines[0]} // First line stays as-is
  146. for i := 1; i < len(lines); i++ {
  147. line := lines[i]
  148. // Skip empty lines
  149. if strings.TrimSpace(line) == "" {
  150. result = append(result, "")
  151. continue
  152. }
  153. // Remove any existing indentation and apply base indentation
  154. trimmedLine := strings.TrimLeft(line, " \t")
  155. // For Nginx config, determine appropriate indentation level
  156. indentLevel := getIndentLevel(trimmedLine, baseIndent)
  157. result = append(result, baseIndent + indentLevel + trimmedLine)
  158. }
  159. return strings.Join(result, "\n")
  160. }
  161. // getIndentLevel determines the appropriate indentation for a line based on content
  162. func getIndentLevel(line string, baseIndent string) string {
  163. // If line starts with a closing brace, use base indent (no extra)
  164. if strings.HasPrefix(strings.TrimSpace(line), "}") {
  165. return ""
  166. }
  167. // For regular directives inside blocks, add one level of indentation
  168. return " " // 4 spaces for one indent level
  169. }