yao/neo/neo.go
2023-04-30 19:22:23 +08:00

296 lines
6.9 KiB
Go

package neo
import (
"context"
"fmt"
"io"
"net/http"
"net/url"
"strings"
"github.com/gin-gonic/gin"
"github.com/google/uuid"
jsoniter "github.com/json-iterator/go"
"github.com/yaoapp/gou/api"
"github.com/yaoapp/gou/connector"
"github.com/yaoapp/gou/process"
"github.com/yaoapp/kun/log"
"github.com/yaoapp/yao/helper"
"github.com/yaoapp/yao/neo/conversation"
"github.com/yaoapp/yao/openai"
)
// API is a method on the Neo type
func (neo *DSL) API(router *gin.Engine, path string) error {
prompts := []map[string]interface{}{}
for _, prompt := range neo.Prompts {
message := map[string]interface{}{"role": prompt.Role, "content": prompt.Content}
if prompt.Name != "" {
message["name"] = prompt.Name
}
prompts = append(prompts, message)
}
// set the guard
err := neo.setGuard(router)
if err != nil {
return err
}
// Cross-Domain
neo.crossDomain(router, path)
// api router
router.GET(path, func(c *gin.Context) {
sid := c.GetString("__sid")
if sid == "" {
sid = uuid.New().String()
}
content := c.Query("content")
if content == "" {
c.JSON(400, gin.H{"message": "content is required", "code": 400})
return
}
messages := append([]map[string]interface{}{}, prompts...)
history, err := neo.Conversation.GetHistory(sid)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
}
messages = append(messages, history...)
messages = append(messages, map[string]interface{}{"role": "user", "content": content, "name": sid})
// utils.Dump(messages)
// reply the content
ctx, cancel := context.WithCancel(context.Background())
defer cancel()
err = neo.Answer(ctx, c, messages)
if err != nil {
c.JSON(500, gin.H{"message": err.Error(), "code": 500})
c.Done()
}
})
return nil
}
// Answer the message
func (neo *DSL) Answer(ctx context.Context, c *gin.Context, messages []map[string]interface{}) error {
chanStream := make(chan []byte, 1)
chanError := make(chan error, 1)
go func() {
defer func() {
close(chanStream)
close(chanError)
}()
_, ex := neo.AI.ChatCompletionsWith(ctx, messages, neo.Option, func(data []byte) int {
chanStream <- data
return 1
})
if ex != nil {
chanError <- fmt.Errorf("AI chat error: %s", ex.Message)
}
}()
// save the history
content := []byte{}
defer func() {
sid := c.GetString("__sid")
if len(content) > 0 && sid != "" && len(messages) > 0 {
err := neo.Conversation.SaveHistory(
sid,
[]map[string]interface{}{
{"role": "user", "content": messages[len(messages)-1]["content"], "name": sid},
{"role": "assistant", "content": string(content), "name": sid},
},
)
if err != nil {
log.Error("Save history error: %s", err.Error())
}
}
}()
c.Header("Content-Type", "text/event-stream;charset=utf-8")
ok := c.Stream(func(w io.Writer) bool {
select {
case err := <-chanError:
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"message": err.Error(), "code": 500})
}
return false
case msg := <-chanStream:
if msg != nil && len(msg) > 0 {
if strings.Contains(string(msg), `"delta":{"content"`) {
msg = []byte(strings.TrimPrefix(string(msg), "data: "))
var message openai.Message
err := jsoniter.Unmarshal(msg, &message)
if err != nil {
data, _ := jsoniter.Marshal(map[string]interface{}{"text": err.Error()})
w.Write([]byte(fmt.Sprintf("data: %s\n\n", data)))
return true
}
if len(message.Choices) > 0 {
text := message.Choices[0].Delta.Content
content = append(content, []byte(text)...)
data, _ := jsoniter.Marshal(map[string]interface{}{"text": text})
w.Write([]byte(fmt.Sprintf("data: %s\n\n", data)))
return true
}
} else if strings.Contains(string(msg), `[DONE]`) {
w.Write([]byte(fmt.Sprintf("data: %s\n\n", `{"done":true}`)))
return true
}
}
return true
}
})
if !ok {
c.Status(500)
return nil
}
c.Status(200)
return nil
}
func (neo *DSL) crossDomain(router *gin.Engine, path string) {
if len(neo.Allows) == 0 {
return
}
allowsMap := map[string]bool{}
for _, allow := range neo.Allows {
allow = strings.TrimPrefix(allow, "http://")
allow = strings.TrimPrefix(allow, "https://")
allowsMap[allow] = true
}
router.Use(func(c *gin.Context) {
referer := c.Request.Referer()
if referer != "" {
if !api.IsAllowed(c, allowsMap) {
c.JSON(403, gin.H{"message": referer + " not allowed", "code": 403})
c.Abort()
return
}
url, _ := url.Parse(referer)
referer = fmt.Sprintf("%s://%s", url.Scheme, url.Host)
c.Writer.Header().Set("Access-Control-Allow-Origin", referer)
c.Writer.Header().Set("Access-Control-Allow-Credentials", "true")
c.Writer.Header().Set("Access-Control-Allow-Headers", "Content-Type, Content-Length, Accept-Encoding, X-CSRF-Token, Authorization, accept, origin, Cache-Control, X-Requested-With")
c.Writer.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS, GET, PUT")
c.Next()
}
})
router.OPTIONS(path, func(c *gin.Context) { c.AbortWithStatus(204) })
}
func (neo *DSL) setGuard(router *gin.Engine) error {
if neo.Guard == "" {
router.Use(func(c *gin.Context) {
token := c.Query("token")
if token == "" {
c.JSON(403, gin.H{"message": "token is required", "code": 403})
c.Abort()
return
}
user := helper.JwtValidate(token)
c.Set("__sid", user.SID)
c.Next()
})
return nil
}
// validate the custom guard
_, err := process.Of(neo.Guard)
if err != nil {
return err
}
// custom guard
router.Use(api.ProcessGuard(neo.Guard))
return nil
}
// NewAI create a new AI
func (neo *DSL) newAI() error {
if neo.Connector == "" {
return fmt.Errorf("%s connector is required", neo.ID)
}
conn, err := connector.Select(neo.Connector)
if err != nil {
return err
}
if conn.Is(connector.OPENAI) {
ai, err := openai.New(neo.Connector)
if err != nil {
return err
}
neo.AI = ai
return nil
}
return fmt.Errorf("%s connector %s not support, should be a openai", neo.ID, neo.Connector)
}
// newConversation create a new conversation
func (neo *DSL) newConversation() error {
var err error
if neo.ConversationSetting.Connector == "default" || neo.ConversationSetting.Connector == "" {
neo.Conversation, err = conversation.NewXun(neo.ConversationSetting)
return err
}
// other connector
conn, err := connector.Select(neo.ConversationSetting.Connector)
if err != nil {
return err
}
if conn.Is(connector.DATABASE) {
neo.Conversation, err = conversation.NewXun(neo.ConversationSetting)
return err
} else if conn.Is(connector.REDIS) {
neo.Conversation = conversation.NewRedis()
return nil
} else if conn.Is(connector.MONGO) {
neo.Conversation = conversation.NewMongo()
return nil
} else if conn.Is(connector.WEAVIATE) {
neo.Conversation = conversation.NewWeaviate()
return nil
}
return fmt.Errorf("%s conversation connector %s not support", neo.ID, neo.ConversationSetting.Connector)
}