mirror of
https://github.com/0xJacky/nginx-ui.git
synced 2025-05-11 18:35:51 +02:00
enhance: chat with LLM
This commit is contained in:
parent
e84ea98be9
commit
642e21a260
40 changed files with 544 additions and 250 deletions
102
internal/chatbot/context.go
Normal file
102
internal/chatbot/context.go
Normal file
|
@ -0,0 +1,102 @@
|
|||
package chatbot
|
||||
|
||||
import (
|
||||
"github.com/0xJacky/Nginx-UI/internal/helper"
|
||||
"github.com/0xJacky/Nginx-UI/internal/logger"
|
||||
"github.com/0xJacky/Nginx-UI/internal/nginx"
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"os"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type includeContext struct {
|
||||
Paths []string
|
||||
PathsMap map[string]bool
|
||||
}
|
||||
|
||||
func IncludeContext(filename string) (includes []string) {
|
||||
c := &includeContext{
|
||||
Paths: make([]string, 0),
|
||||
PathsMap: make(map[string]bool),
|
||||
}
|
||||
|
||||
c.extractIncludes(filename)
|
||||
|
||||
return c.Paths
|
||||
}
|
||||
|
||||
// extractIncludes extracts all include statements from the given nginx configuration file.
|
||||
func (c *includeContext) extractIncludes(filename string) {
|
||||
if !helper.FileExists(filename) {
|
||||
logger.Error("File does not exist: ", filename)
|
||||
return
|
||||
}
|
||||
|
||||
// Read the file content
|
||||
content, err := os.ReadFile(filename)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
// Find all include statements
|
||||
pattern := regexp.MustCompile(`(?m)^\s*include\s+([^;]+);`)
|
||||
matches := pattern.FindAllStringSubmatch(string(content), -1)
|
||||
for _, match := range matches {
|
||||
if len(match) > 1 {
|
||||
// Resolve the path of the included file
|
||||
includePath := match[1]
|
||||
|
||||
// to avoid infinite loop
|
||||
if c.PathsMap[includePath] {
|
||||
continue
|
||||
}
|
||||
|
||||
c.push(includePath)
|
||||
|
||||
// Recursively extract includes from the included file
|
||||
c.extractIncludes(includePath)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func (c *includeContext) push(path string) {
|
||||
c.Paths = append(c.Paths, path)
|
||||
c.PathsMap[path] = true
|
||||
}
|
||||
|
||||
// getConfigIncludeContext returns the context of the given filename.
|
||||
func getConfigIncludeContext(filename string) (multiContent []openai.ChatMessagePart) {
|
||||
multiContent = make([]openai.ChatMessagePart, 0)
|
||||
|
||||
if !helper.IsUnderDirectory(filename, nginx.GetConfPath()) {
|
||||
return
|
||||
}
|
||||
|
||||
includes := IncludeContext(filename)
|
||||
logger.Debug(includes)
|
||||
var sb strings.Builder
|
||||
for _, include := range includes {
|
||||
text, _ := os.ReadFile(nginx.GetConfPath(include))
|
||||
|
||||
if len(text) == 0 {
|
||||
continue
|
||||
}
|
||||
|
||||
sb.WriteString("The Content of ")
|
||||
sb.WriteString(include)
|
||||
sb.WriteString(",")
|
||||
sb.WriteString(string(text))
|
||||
|
||||
multiContent = append(multiContent, openai.ChatMessagePart{
|
||||
Type: openai.ChatMessagePartTypeText,
|
||||
Text: sb.String(),
|
||||
})
|
||||
|
||||
sb.Reset()
|
||||
}
|
||||
return
|
||||
}
|
23
internal/chatbot/context_test.go
Normal file
23
internal/chatbot/context_test.go
Normal file
|
@ -0,0 +1,23 @@
|
|||
package chatbot
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRegex(t *testing.T) {
|
||||
content := `
|
||||
server {
|
||||
listen 80;
|
||||
listen [::]:80;
|
||||
server_name _;
|
||||
include error_json;
|
||||
}
|
||||
`
|
||||
pattern := regexp.MustCompile(`(?m)^\s*include\s+([^;]+);`)
|
||||
matches := pattern.FindAllStringSubmatch(content, -1)
|
||||
|
||||
assert.Equal(t, 1, len(matches))
|
||||
assert.Equal(t, "error_json", matches[0][1])
|
||||
}
|
22
internal/chatbot/messages.go
Normal file
22
internal/chatbot/messages.go
Normal file
|
@ -0,0 +1,22 @@
|
|||
package chatbot
|
||||
|
||||
import (
|
||||
"github.com/sashabaranov/go-openai"
|
||||
)
|
||||
|
||||
func ChatCompletionWithContext(filename string, messages []openai.ChatCompletionMessage) []openai.ChatCompletionMessage {
|
||||
|
||||
for i := len(messages) - 1; i >= 0; i-- {
|
||||
if messages[i].Role == openai.ChatMessageRoleUser {
|
||||
// openai.ChatCompletionMessage: can't use both Content and MultiContent properties simultaneously
|
||||
multiContent := getConfigIncludeContext(filename)
|
||||
multiContent = append(multiContent, openai.ChatMessagePart{
|
||||
Type: openai.ChatMessagePartTypeText,
|
||||
Text: messages[i].Content,
|
||||
})
|
||||
messages[i].Content = ""
|
||||
messages[i].MultiContent = multiContent
|
||||
}
|
||||
}
|
||||
return messages
|
||||
}
|
26
internal/chatbot/messages_test.go
Normal file
26
internal/chatbot/messages_test.go
Normal file
|
@ -0,0 +1,26 @@
|
|||
package chatbot
|
||||
|
||||
import (
|
||||
"github.com/sashabaranov/go-openai"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestChatCompletionWithContext(t *testing.T) {
|
||||
filename := "test"
|
||||
messages := []openai.ChatCompletionMessage{
|
||||
{
|
||||
Role: openai.ChatMessageRoleSystem,
|
||||
},
|
||||
{
|
||||
Role: openai.ChatMessageRoleUser,
|
||||
},
|
||||
{
|
||||
Role: openai.ChatMessageRoleAssistant,
|
||||
},
|
||||
}
|
||||
|
||||
messages = ChatCompletionWithContext(filename, messages)
|
||||
|
||||
assert.NotNil(t, messages[1].MultiContent)
|
||||
}
|
|
@ -9,7 +9,7 @@ type Config struct {
|
|||
Name string `json:"name"`
|
||||
Content string `json:"content,omitempty"`
|
||||
ChatGPTMessages []openai.ChatCompletionMessage `json:"chatgpt_messages,omitempty"`
|
||||
FilePath string `json:"file_path,omitempty"`
|
||||
FilePath string `json:"filepath,omitempty"`
|
||||
ModifiedAt time.Time `json:"modified_at"`
|
||||
Size int64 `json:"size,omitempty"`
|
||||
IsDir bool `json:"is_dir"`
|
||||
|
|
25
internal/helper/directory.go
Normal file
25
internal/helper/directory.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"github.com/0xJacky/Nginx-UI/internal/logger"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func IsUnderDirectory(path, directory string) bool {
|
||||
absPath, err := filepath.Abs(path)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return false
|
||||
}
|
||||
|
||||
absDirectory, err := filepath.Abs(directory)
|
||||
if err != nil {
|
||||
logger.Error(err)
|
||||
return false
|
||||
}
|
||||
|
||||
absDirectory = filepath.Clean(absDirectory) + string(filepath.Separator)
|
||||
|
||||
return strings.HasPrefix(absPath, absDirectory)
|
||||
}
|
11
internal/helper/directory_test.go
Normal file
11
internal/helper/directory_test.go
Normal file
|
@ -0,0 +1,11 @@
|
|||
package helper
|
||||
|
||||
import (
|
||||
"github.com/stretchr/testify/assert"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestIsUnderDirectory(t *testing.T) {
|
||||
assert.Equal(t, true, IsUnderDirectory("/etc/nginx/nginx.conf", "/etc/nginx"))
|
||||
assert.Equal(t, false, IsUnderDirectory("../../root/nginx.conf", "/etc/nginx"))
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue