- Converted initialization methods from receiver methods to package-level functions - Simplified method calls by directly using the global Neo instance - Removed redundant method receiver references in initialization logic - Improved code readability and reduced unnecessary method complexity This change streamlines the Neo initialization process and makes the code more straightforward and maintainable.
263 lines
6.3 KiB
Go
263 lines
6.3 KiB
Go
package neo
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/yaoapp/kun/log"
|
|
"github.com/yaoapp/yao/neo/assistant"
|
|
chatctx "github.com/yaoapp/yao/neo/context"
|
|
"github.com/yaoapp/yao/neo/message"
|
|
)
|
|
|
|
// Answer reply the message
|
|
func (neo *DSL) Answer(ctx chatctx.Context, question string, c *gin.Context) error {
|
|
var err error
|
|
var ast assistant.API = Neo.Assistant
|
|
if ctx.AssistantID != "" {
|
|
ast, err = neo.Select(ctx.AssistantID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return ast.Execute(c, ctx, question, nil)
|
|
}
|
|
|
|
// Select select an assistant
|
|
func (neo *DSL) Select(id string) (assistant.API, error) {
|
|
if id == "" {
|
|
return Neo.Assistant, nil
|
|
}
|
|
return assistant.Get(id)
|
|
}
|
|
|
|
// GeneratePrompts generate prompts for the AI assistant
|
|
func (neo *DSL) GeneratePrompts(ctx chatctx.Context, input string, c *gin.Context, silent ...bool) (string, error) {
|
|
prompts := `
|
|
Optimize the prompts for the AI assistant
|
|
1. Optimize prompts based on the user's input
|
|
2. The prompts should be clear and specific
|
|
3. The prompts should be in the same language as the input
|
|
4. Keep the prompts concise but comprehensive
|
|
5. DO NOT ASK USER FOR MORE INFORMATION, JUST GENERATE PROMPTS
|
|
6. DO NOT ANSWER THE QUESTION, JUST GENERATE PROMPTS
|
|
`
|
|
isSilent := false
|
|
if len(silent) > 0 {
|
|
isSilent = silent[0]
|
|
}
|
|
return neo.GenerateWithAI(ctx, input, "prompts", prompts, c, isSilent)
|
|
}
|
|
|
|
// GenerateChatTitle generate the chat title
|
|
func (neo *DSL) GenerateChatTitle(ctx chatctx.Context, input string, c *gin.Context, silent ...bool) (string, error) {
|
|
prompts := `
|
|
Help me generate a title for the chat
|
|
1. The title should be a short and concise description of the chat.
|
|
2. The title should be a single sentence.
|
|
3. The title should be in same language as the chat.
|
|
4. The title should be no more than 50 characters.
|
|
5. ANSWER ONLY THE TITLE CONTENT, FOR EXAMPLE: Chat with AI is a valid title, but "Chat with AI" is not a valid title.
|
|
`
|
|
isSilent := false
|
|
if len(silent) > 0 {
|
|
isSilent = silent[0]
|
|
}
|
|
return neo.GenerateWithAI(ctx, input, "title", prompts, c, isSilent)
|
|
}
|
|
|
|
// GenerateWithAI generate content with AI, type can be "title", "prompts", etc.
|
|
func (neo *DSL) GenerateWithAI(ctx chatctx.Context, input string, messageType string, systemPrompt string, c *gin.Context, silent bool) (string, error) {
|
|
messages := []map[string]interface{}{
|
|
{"role": "system", "content": systemPrompt},
|
|
{
|
|
"role": "user",
|
|
"content": input,
|
|
"type": messageType,
|
|
"name": ctx.Sid,
|
|
},
|
|
}
|
|
|
|
res, err := neo.HookCreate(ctx, messages, c)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
// Select Assistant
|
|
ast, err := neo.Select(res.AssistantID)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
if ast == nil {
|
|
msg := message.New().Error("assistant is not initialized").Done()
|
|
msg.Write(c.Writer)
|
|
return "", fmt.Errorf("assistant is not initialized")
|
|
}
|
|
|
|
clientBreak := make(chan bool, 1)
|
|
done := make(chan bool, 1)
|
|
fail := make(chan error, 1)
|
|
contents := message.NewContents()
|
|
|
|
// Chat with AI in background
|
|
go func() {
|
|
msgList := []message.Message{}
|
|
for _, vv := range messages {
|
|
msg := message.New().Map(vv)
|
|
if content, ok := vv["content"].(string); ok {
|
|
msgs, err := message.NewContent(content)
|
|
if err == nil {
|
|
for _, v := range msgs {
|
|
v.AssistantID = msg.AssistantID
|
|
v.AssistantName = msg.AssistantName
|
|
v.AssistantAvatar = msg.AssistantAvatar
|
|
v.Role = msg.Role
|
|
v.Name = msg.Name
|
|
v.Mentions = msg.Mentions
|
|
msgList = append(msgList, v)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
err := ast.Chat(c.Request.Context(), msgList, neo.Option, func(data []byte) int {
|
|
select {
|
|
case <-clientBreak:
|
|
return 0 // break
|
|
|
|
default:
|
|
msg := message.NewOpenAI(data)
|
|
if msg == nil {
|
|
return 1 // continue
|
|
}
|
|
|
|
// Handle error
|
|
if msg.Type == "error" {
|
|
fail <- fmt.Errorf("%s", msg.Text)
|
|
return 0 // break
|
|
}
|
|
|
|
// Append content and send message
|
|
msg.AppendTo(contents)
|
|
if !silent {
|
|
value := msg.String()
|
|
if value != "" {
|
|
message.New().
|
|
Map(map[string]interface{}{
|
|
"text": value,
|
|
"done": msg.IsDone,
|
|
}).
|
|
Write(c.Writer)
|
|
}
|
|
}
|
|
|
|
// Complete the stream
|
|
if msg.IsDone {
|
|
value := msg.String()
|
|
if value == "" {
|
|
msg.Write(c.Writer)
|
|
}
|
|
done <- true
|
|
return 0 // break
|
|
}
|
|
|
|
return 1 // continue
|
|
}
|
|
})
|
|
|
|
if err != nil {
|
|
log.Error("Chat error: %s", err.Error())
|
|
if !silent {
|
|
message.New().Error(err).Done().Write(c.Writer)
|
|
}
|
|
}
|
|
|
|
done <- true
|
|
}()
|
|
|
|
// Wait for completion or client disconnect
|
|
select {
|
|
case <-done:
|
|
return contents.Text(), nil
|
|
case err := <-fail:
|
|
return "", err
|
|
case <-c.Writer.CloseNotify():
|
|
clientBreak <- true
|
|
return "", nil
|
|
}
|
|
}
|
|
|
|
// Upload upload a file
|
|
func (neo *DSL) Upload(ctx chatctx.Context, c *gin.Context) (*assistant.File, error) {
|
|
// Get the file
|
|
tmpfile, err := c.FormFile("file")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
reader, err := tmpfile.Open()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer func() {
|
|
reader.Close()
|
|
os.Remove(tmpfile.Filename)
|
|
}()
|
|
|
|
// Get option from form data option_xxx
|
|
option := map[string]interface{}{}
|
|
for key := range c.Request.Form {
|
|
if strings.HasPrefix(key, "option_") {
|
|
option[strings.TrimPrefix(key, "option_")] = c.PostForm(key)
|
|
}
|
|
}
|
|
|
|
// Get file info
|
|
ctx.Upload = &chatctx.FileUpload{
|
|
Name: tmpfile.Filename,
|
|
Type: tmpfile.Header.Get("Content-Type"),
|
|
Size: tmpfile.Size,
|
|
TempFile: tmpfile.Filename,
|
|
}
|
|
|
|
// Default use the assistant in context
|
|
ast := neo.Assistant
|
|
if ctx.ChatID == "" {
|
|
if ctx.AssistantID == "" {
|
|
return nil, fmt.Errorf("assistant_id is required")
|
|
}
|
|
ast, err = neo.Select(ctx.AssistantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
|
|
return ast.Upload(ctx, tmpfile, reader, option)
|
|
}
|
|
|
|
// Download downloads a file
|
|
func (neo *DSL) Download(ctx chatctx.Context, c *gin.Context) (*assistant.FileResponse, error) {
|
|
// Get file_id from query string
|
|
fileID := c.Query("file_id")
|
|
if fileID == "" {
|
|
return nil, fmt.Errorf("file_id is required")
|
|
}
|
|
|
|
// Get assistant_id from context or query
|
|
res, err := neo.HookCreate(ctx, []map[string]interface{}{}, c)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Select Assistant
|
|
ast, err := neo.Select(res.AssistantID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// Download file using the assistant
|
|
return ast.Download(ctx.Context, fileID)
|
|
}
|