Refactor Neo API message handling and enhance assistant interaction

- Updated the Answer method to utilize the new withHistory function, improving message retrieval and context management.
- Refactored the chat method to accept a slice of message.Message instead of a map, enhancing type safety and clarity.
- Introduced a new withHistory function to streamline the process of retrieving chat history and user messages, improving maintainability.
- Enhanced the HookInit method to accept a gin.Context, allowing for better context handling during assistant initialization.
- Updated the assistant API interface to reflect changes in message handling, ensuring consistency across the codebase.

These changes improve the robustness and maintainability of the Neo API, paving the way for future enhancements in assistant functionalities and message management.
This commit is contained in:
Max 2025-01-13 14:47:17 +08:00
parent b01f27d70f
commit ffb9ede647
5 changed files with 133 additions and 38 deletions

View file

@ -7,6 +7,7 @@ import (
"strings" "strings"
"github.com/yaoapp/gou/fs" "github.com/yaoapp/gou/fs"
"github.com/yaoapp/yao/neo/message"
chatMessage "github.com/yaoapp/yao/neo/message" chatMessage "github.com/yaoapp/yao/neo/message"
) )
@ -41,7 +42,7 @@ func GetByConnector(connector string, name string) (*Assistant, error) {
} }
// Chat implements the chat functionality // Chat implements the chat functionality
func (ast *Assistant) Chat(ctx context.Context, messages []map[string]interface{}, option map[string]interface{}, cb func(data []byte) int) error { func (ast *Assistant) Chat(ctx context.Context, messages []message.Message, option map[string]interface{}, cb func(data []byte) int) error {
if ast.openai == nil { if ast.openai == nil {
return fmt.Errorf("openai is not initialized") return fmt.Errorf("openai is not initialized")
} }
@ -59,13 +60,12 @@ func (ast *Assistant) Chat(ctx context.Context, messages []map[string]interface{
return nil return nil
} }
func (ast *Assistant) requestMessages(ctx context.Context, messages []map[string]interface{}) ([]map[string]interface{}, error) { func (ast *Assistant) requestMessages(ctx context.Context, messages []message.Message) ([]map[string]interface{}, error) {
newMessages := []map[string]interface{}{} newMessages := []map[string]interface{}{}
// With Prompts // With Prompts
if ast.Prompts != nil { if ast.Prompts != nil {
for _, prompt := range ast.Prompts { for _, prompt := range ast.Prompts {
message := map[string]interface{}{ msg := map[string]interface{}{
"role": prompt.Role, "role": prompt.Role,
"content": prompt.Content, "content": prompt.Content,
} }
@ -75,20 +75,20 @@ func (ast *Assistant) requestMessages(ctx context.Context, messages []map[string
name = prompt.Name name = prompt.Name
} }
message["name"] = name msg["name"] = name
newMessages = append(newMessages, message) newMessages = append(newMessages, msg)
} }
} }
length := len(messages) length := len(messages)
for index, message := range messages { for index, message := range messages {
role, ok := message["role"].(string) role := message.Role
if !ok { if role == "" {
return nil, fmt.Errorf("role must be string") return nil, fmt.Errorf("role must be string")
} }
content, ok := message["content"].(string) content := message.Text
if !ok { if content == "" {
return nil, fmt.Errorf("content must be string") return nil, fmt.Errorf("content must be string")
} }
@ -97,7 +97,7 @@ func (ast *Assistant) requestMessages(ctx context.Context, messages []map[string
"content": content, "content": content,
} }
if name, ok := message["name"].(string); ok { if name := message.Name; name != "" {
newMessage["name"] = name newMessage["name"] = name
} }

View file

@ -1,8 +1,11 @@
package assistant package assistant
import ( import (
"context"
"fmt" "fmt"
"time"
"github.com/gin-gonic/gin"
chatctx "github.com/yaoapp/yao/neo/context" chatctx "github.com/yaoapp/yao/neo/context"
"github.com/yaoapp/yao/neo/message" "github.com/yaoapp/yao/neo/message"
) )
@ -19,8 +22,12 @@ type ResHookInit struct {
} }
// HookInit initialize the assistant // HookInit initialize the assistant
func (ast *Assistant) HookInit(context chatctx.Context, messages []message.Message) (*ResHookInit, error) { func (ast *Assistant) HookInit(c *gin.Context, context chatctx.Context, messages []message.Message) (*ResHookInit, error) {
v, err := ast.call("Init", context, messages) // Create timeout context
ctx, cancel := ast.createTimeoutContext(c)
defer cancel()
v, err := ast.call(ctx, "Init", context, messages, c.Writer)
if err != nil { if err != nil {
if err.Error() == HookErrorMethodNotFound { if err.Error() == HookErrorMethodNotFound {
return nil, nil return nil, nil
@ -50,25 +57,47 @@ func (ast *Assistant) HookInit(context chatctx.Context, messages []message.Messa
return response, nil return response, nil
} }
// Call the script method // createTimeoutContext creates a timeout context with 5 seconds timeout
func (ast *Assistant) call(method string, context chatctx.Context, args ...any) (interface{}, error) { func (ast *Assistant) createTimeoutContext(c *gin.Context) (context.Context, context.CancelFunc) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
return ctx, cancel
}
// Call the script method
func (ast *Assistant) call(ctx context.Context, method string, context chatctx.Context, args ...any) (interface{}, error) {
if ast.Script == nil { if ast.Script == nil {
return nil, nil return nil, nil
} }
ctx, err := ast.Script.NewContext(context.Sid, nil) scriptCtx, err := ast.Script.NewContext(context.Sid, nil)
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer ctx.Close() defer scriptCtx.Close()
// Check if the method exists // Check if the method exists
if !ctx.Global().Has(method) { if !scriptCtx.Global().Has(method) {
return nil, fmt.Errorf(HookErrorMethodNotFound) return nil, fmt.Errorf(HookErrorMethodNotFound)
} }
// Call the method // Create done channel for handling cancellation
args = append([]interface{}{context.Map()}, args...) done := make(chan struct{})
return ctx.Call(method, args...) var result interface{}
var callErr error
go func() {
defer close(done)
// Call the method
args = append([]interface{}{context.Map()}, args...)
result, callErr = scriptCtx.Call(method, args...)
}()
// Wait for either context cancellation or method completion
select {
case <-ctx.Done():
scriptCtx.Close() // Force close the script context
return nil, ctx.Err()
case <-done:
return result, callErr
}
} }

View file

@ -5,6 +5,7 @@ import (
"io" "io"
"mime/multipart" "mime/multipart"
"github.com/gin-gonic/gin"
"github.com/yaoapp/gou/rag/driver" "github.com/yaoapp/gou/rag/driver"
v8 "github.com/yaoapp/gou/runtime/v8" v8 "github.com/yaoapp/gou/runtime/v8"
chatctx "github.com/yaoapp/yao/neo/context" chatctx "github.com/yaoapp/yao/neo/context"
@ -14,11 +15,11 @@ import (
// API the assistant API interface // API the assistant API interface
type API interface { type API interface {
Chat(ctx context.Context, messages []map[string]interface{}, option map[string]interface{}, cb func(data []byte) int) error Chat(ctx context.Context, messages []message.Message, option map[string]interface{}, cb func(data []byte) int) error
Upload(ctx context.Context, file *multipart.FileHeader, reader io.Reader, option map[string]interface{}) (*File, error) Upload(ctx context.Context, file *multipart.FileHeader, reader io.Reader, option map[string]interface{}) (*File, error)
Download(ctx context.Context, fileID string) (*FileResponse, error) Download(ctx context.Context, fileID string) (*FileResponse, error)
ReadBase64(ctx context.Context, fileID string) (string, error) ReadBase64(ctx context.Context, fileID string) (string, error)
HookInit(ctx chatctx.Context, messages []message.Message) (*ResHookInit, error) HookInit(c *gin.Context, ctx chatctx.Context, messages []message.Message) (*ResHookInit, error)
} }
// RAG the RAG interface // RAG the RAG interface

View file

@ -22,6 +22,8 @@ type Message struct {
IsDone bool `json:"done,omitempty"` IsDone bool `json:"done,omitempty"`
Actions []Action `json:"actions,omitempty"` // Conversation Actions for frontend Actions []Action `json:"actions,omitempty"` // Conversation Actions for frontend
Attachments []Attachment `json:"attachments,omitempty"` // File attachments Attachments []Attachment `json:"attachments,omitempty"` // File attachments
Role string `json:"role,omitempty"` // user, assistant, system ...
Name string `json:"name,omitempty"` // name for the message
Data map[string]interface{} `json:"-"` Data map[string]interface{} `json:"-"`
} }
@ -134,12 +136,74 @@ func (m *Message) Error(message interface{}) *Message {
return m return m
} }
// SetContent set the content
func (m *Message) SetContent(content string) *Message {
if strings.HasPrefix(content, "{") && strings.HasSuffix(content, "}") {
var msg Message
if err := jsoniter.UnmarshalFromString(content, &msg); err != nil {
m.Text = err.Error() + "\n" + content
return m
}
*m = msg
} else {
m.Text = content
m.Type = "text"
}
return m
}
// Content get the content
func (m *Message) Content() string {
content := map[string]interface{}{"text": m.Text}
if m.Attachments != nil {
content["attachments"] = m.Attachments
}
if m.Type != "" {
content["type"] = m.Type
}
contentRaw, _ := jsoniter.MarshalToString(content)
return contentRaw
}
// ToMap convert to map
func (m *Message) ToMap() map[string]interface{} {
return map[string]interface{}{
"content": m.Content(),
"role": m.Role,
"name": m.Name,
}
}
// Map set from map // Map set from map
func (m *Message) Map(msg map[string]interface{}) *Message { func (m *Message) Map(msg map[string]interface{}) *Message {
if msg == nil { if msg == nil {
return m return m
} }
// Content {"text": "xxxx", "attachments": ... }
if content, ok := msg["content"].(string); ok {
if strings.HasPrefix(content, "{") && strings.HasSuffix(content, "}") {
var msg Message
if err := jsoniter.UnmarshalFromString(content, &msg); err != nil {
m.Text = err.Error() + "\n" + content
return m
}
*m = msg
} else {
m.Text = content
m.Type = "text"
}
}
if role, ok := msg["role"].(string); ok {
m.Role = role
}
if name, ok := msg["name"].(string); ok {
m.Name = name
}
if text, ok := msg["text"].(string); ok { if text, ok := msg["text"].(string); ok {
m.Text = text m.Text = text
} }

View file

@ -18,7 +18,7 @@ var lock sync.Mutex = sync.Mutex{}
// Answer reply the message // Answer reply the message
func (neo *DSL) Answer(ctx chatctx.Context, question string, c *gin.Context) error { func (neo *DSL) Answer(ctx chatctx.Context, question string, c *gin.Context) error {
messages, err := neo.chatMessages(ctx, question) messages, err := neo.withHistory(ctx, question)
if err != nil { if err != nil {
msg := message.New().Error(err).Done() msg := message.New().Error(err).Done()
msg.Write(c.Writer) msg.Write(c.Writer)
@ -36,7 +36,7 @@ func (neo *DSL) Answer(ctx chatctx.Context, question string, c *gin.Context) err
} }
// Init the assistant // Init the assistant
res, err = ast.HookInit(ctx, []message.Message{{Text: question}}) res, err = ast.HookInit(c, ctx, messages)
if err != nil { if err != nil {
return err return err
} }
@ -131,7 +131,11 @@ func (neo *DSL) GenerateWithAI(ctx chatctx.Context, input string, messageType st
// Chat with AI in background // Chat with AI in background
go func() { go func() {
err := ast.Chat(c.Request.Context(), messages, neo.Option, func(data []byte) int { msgList := make([]message.Message, len(messages))
for i, msg := range messages {
msgList[i] = *message.New().Map(msg)
}
err := ast.Chat(c.Request.Context(), msgList, neo.Option, func(data []byte) int {
select { select {
case <-clientBreak: case <-clientBreak:
return 0 // break return 0 // break
@ -271,7 +275,7 @@ func (neo *DSL) Download(ctx chatctx.Context, c *gin.Context) (*assistant.FileRe
} }
// chat chat with AI // chat chat with AI
func (neo *DSL) chat(ast assistant.API, ctx chatctx.Context, messages []map[string]interface{}, c *gin.Context) error { func (neo *DSL) chat(ast assistant.API, ctx chatctx.Context, messages []message.Message, c *gin.Context) error {
if ast == nil { if ast == nil {
msg := message.New().Error("assistant is not initialized").Done() msg := message.New().Error("assistant is not initialized").Done()
msg.Write(c.Writer) msg.Write(c.Writer)
@ -350,33 +354,30 @@ func (neo *DSL) chat(ast assistant.API, ctx chatctx.Context, messages []map[stri
} }
} }
// chatMessages get the chat messages func (neo *DSL) withHistory(ctx chatctx.Context, question string) ([]message.Message, error) {
func (neo *DSL) chatMessages(ctx chatctx.Context, content ...string) ([]map[string]interface{}, error) {
history, err := neo.Store.GetHistory(ctx.Sid, ctx.ChatID) history, err := neo.Store.GetHistory(ctx.Sid, ctx.ChatID)
if err != nil { if err != nil {
return nil, err return nil, err
} }
messages := []map[string]interface{}{} // Add history messages
messages = append(messages, history...) messages := []message.Message{}
if len(content) == 0 { for _, h := range history {
return messages, nil messages = append(messages, *message.New().Map(h))
} }
// Add user message // Add user message
messages = append(messages, map[string]interface{}{"role": "user", "content": content[0], "name": ctx.Sid}) messages = append(messages, *message.New().Map(map[string]interface{}{"role": "user", "content": question, "name": ctx.Sid}))
return messages, nil return messages, nil
} }
// saveHistory save the history // saveHistory save the history
func (neo *DSL) saveHistory(sid string, chatID string, content []byte, messages []map[string]interface{}) { func (neo *DSL) saveHistory(sid string, chatID string, content []byte, messages []message.Message) {
if len(content) > 0 && sid != "" && len(messages) > 0 { if len(content) > 0 && sid != "" && len(messages) > 0 {
err := neo.Store.SaveHistory( err := neo.Store.SaveHistory(
sid, sid,
[]map[string]interface{}{ []map[string]interface{}{
{"role": "user", "content": messages[len(messages)-1]["content"], "name": sid}, {"role": "user", "content": messages[len(messages)-1].Content(), "name": sid},
{"role": "assistant", "content": string(content), "name": sid}, {"role": "assistant", "content": string(content), "name": sid},
}, },
chatID, chatID,