package llm
import (
"context"
"regexp"
"strconv"
"strings"
"sync"
"github.com/0xJacky/Nginx-UI/settings"
"github.com/sashabaranov/go-openai"
"github.com/uozi-tech/cosy/logger"
)
const (
MaxTokens = 2000
Temperature = 1
// SystemPrompt Build system prompt and user prompt
SystemPrompt = "You are a code completion assistant. " +
"Complete the provided code snippet based on the context and instruction." +
"[IMPORTANT] Keep the original code indentation."
)
// Position the cursor position
type Position struct {
Row int `json:"row"`
Column int `json:"column"`
}
// CodeCompletionRequest the code completion request
type CodeCompletionRequest struct {
RequestID string `json:"request_id"`
UserID uint64 `json:"user_id"`
Context string `json:"context"`
Code string `json:"code"`
Suffix string `json:"suffix"`
Language string `json:"language"`
Position Position `json:"position"`
CurrentIndent string `json:"current_indent"`
}
var (
requestContext = make(map[uint64]context.CancelFunc)
mutex sync.Mutex
)
func (c *CodeCompletionRequest) Send() (completedCode string, err error) {
if cancel, ok := requestContext[c.UserID]; ok {
logger.Infof("Code completion request cancelled for user %d", c.UserID)
cancel()
}
mutex.Lock()
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
requestContext[c.UserID] = cancel
mutex.Unlock()
defer func() {
mutex.Lock()
delete(requestContext, c.UserID)
mutex.Unlock()
}()
openaiClient, err := GetClient()
if err != nil {
return
}
// Build user prompt with code and instruction
userPrompt := "Here is a file written in " + c.Language + ":\n```\n" + c.Context + "\n```\n"
userPrompt += "I'm editing at row " + strconv.Itoa(c.Position.Row) + ", column " + strconv.Itoa(c.Position.Column) + ".\n"
userPrompt += "Code before cursor:\n```\n" + c.Code + "\n```\n"
if c.Suffix != "" {
userPrompt += "Code after cursor:\n```\n" + c.Suffix + "\n```\n"
}
userPrompt += "Instruction: Only provide the completed code that should be inserted at the cursor position without explanations. " +
"The code should be syntactically correct and follow best practices for " + c.Language + ". " +
"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. " +
"For multi-line completions, use proper indentation - the current line uses '" + c.CurrentIndent + "' as base indentation. " +
"Each new line should maintain consistent indentation levels appropriate for the code structure."
messages := []openai.ChatCompletionMessage{
{
Role: openai.ChatMessageRoleSystem,
Content: SystemPrompt,
},
{
Role: openai.ChatMessageRoleUser,
Content: userPrompt,
},
}
req := openai.ChatCompletionRequest{
Model: settings.OpenAISettings.GetCodeCompletionModel(),
Messages: messages,
MaxCompletionTokens: MaxTokens,
Temperature: Temperature,
}
// Make a direct (non-streaming) call to the API
response, err := openaiClient.CreateChatCompletion(ctx, req)
if err != nil {
return
}
completedCode = response.Choices[0].Message.Content
// extract the last word of the code
lastWord := extractLastWord(c.Code)
completedCode = cleanCompletionResponse(completedCode, lastWord, c.CurrentIndent)
logger.Infof("Code completion response: %s", completedCode)
return
}
// extractLastWord extract the last word of the code
func extractLastWord(code string) string {
if code == "" {
return ""
}
// define a regex to match word characters (letters, numbers, underscores)
re := regexp.MustCompile(`[a-zA-Z0-9_]+$`)
// find the last word of the code
match := re.FindString(code)
return match
}
// cleanCompletionResponse removes any tags and their content from the completion response
// and strips the already entered code from the completion while preserving formatting
func cleanCompletionResponse(response string, lastWord string, currentIndent string) (cleanResp string) {
// remove tags and their content using regex
re := regexp.MustCompile(`[\s\S]*?`)
cleanResp = re.ReplaceAllString(response, "")
// remove markdown code block tags
codeBlockRegex := regexp.MustCompile("```(?:[a-zA-Z]+)?\n((?:.|\n)*?)\n```")
matches := codeBlockRegex.FindStringSubmatch(cleanResp)
if len(matches) > 1 {
// extract the code block content, preserve leading newlines
cleanResp = matches[1]
} else {
// if no code block is found, only trim trailing whitespace
cleanResp = strings.TrimRight(cleanResp, " \t")
}
// remove markdown backticks but preserve newlines
cleanResp = strings.Trim(cleanResp, "`")
// if there is a last word, and the completion result starts with the last word, remove the already entered part
if lastWord != "" && strings.HasPrefix(strings.TrimLeft(cleanResp, " \t\n"), lastWord) {
// Find the position after the last word, preserving leading whitespace
trimmed := strings.TrimLeft(cleanResp, " \t\n")
leadingWhitespace := cleanResp[:len(cleanResp)-len(trimmed)]
cleanResp = leadingWhitespace + trimmed[len(lastWord):]
}
// Fix indentation for multi-line completions
cleanResp = fixCompletionIndentation(cleanResp, currentIndent)
return
}
// fixCompletionIndentation ensures proper indentation for multi-line completions
func fixCompletionIndentation(completion string, baseIndent string) string {
lines := strings.Split(completion, "\n")
if len(lines) <= 1 {
return completion
}
result := []string{lines[0]} // First line stays as-is
for i := 1; i < len(lines); i++ {
line := lines[i]
// Skip empty lines
if strings.TrimSpace(line) == "" {
result = append(result, "")
continue
}
// Remove any existing indentation and apply base indentation
trimmedLine := strings.TrimLeft(line, " \t")
// For Nginx config, determine appropriate indentation level
indentLevel := getIndentLevel(trimmedLine, baseIndent)
result = append(result, baseIndent + indentLevel + trimmedLine)
}
return strings.Join(result, "\n")
}
// getIndentLevel determines the appropriate indentation for a line based on content
func getIndentLevel(line string, baseIndent string) string {
// If line starts with a closing brace, use base indent (no extra)
if strings.HasPrefix(strings.TrimSpace(line), "}") {
return ""
}
// For regular directives inside blocks, add one level of indentation
return " " // 4 spaces for one indent level
}