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"
"github.com/yaoapp/gou/fs"
"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
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 {
return fmt.Errorf("openai is not initialized")
}
@ -59,13 +60,12 @@ func (ast *Assistant) Chat(ctx context.Context, messages []map[string]interface{
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{}{}
// With Prompts
if ast.Prompts != nil {
for _, prompt := range ast.Prompts {
message := map[string]interface{}{
msg := map[string]interface{}{
"role": prompt.Role,
"content": prompt.Content,
}
@ -75,20 +75,20 @@ func (ast *Assistant) requestMessages(ctx context.Context, messages []map[string
name = prompt.Name
}
message["name"] = name
newMessages = append(newMessages, message)
msg["name"] = name
newMessages = append(newMessages, msg)
}
}
length := len(messages)
for index, message := range messages {
role, ok := message["role"].(string)
if !ok {
role := message.Role
if role == "" {
return nil, fmt.Errorf("role must be string")
}
content, ok := message["content"].(string)
if !ok {
content := message.Text
if content == "" {
return nil, fmt.Errorf("content must be string")
}
@ -97,7 +97,7 @@ func (ast *Assistant) requestMessages(ctx context.Context, messages []map[string
"content": content,
}
if name, ok := message["name"].(string); ok {
if name := message.Name; name != "" {
newMessage["name"] = name
}

View file

@ -1,8 +1,11 @@
package assistant
import (
"context"
"fmt"
"time"
"github.com/gin-gonic/gin"
chatctx "github.com/yaoapp/yao/neo/context"
"github.com/yaoapp/yao/neo/message"
)
@ -19,8 +22,12 @@ type ResHookInit struct {
}
// HookInit initialize the assistant
func (ast *Assistant) HookInit(context chatctx.Context, messages []message.Message) (*ResHookInit, error) {
v, err := ast.call("Init", context, messages)
func (ast *Assistant) HookInit(c *gin.Context, context chatctx.Context, messages []message.Message) (*ResHookInit, error) {
// 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.Error() == HookErrorMethodNotFound {
return nil, nil
@ -50,25 +57,47 @@ func (ast *Assistant) HookInit(context chatctx.Context, messages []message.Messa
return response, nil
}
// Call the script method
func (ast *Assistant) call(method string, context chatctx.Context, args ...any) (interface{}, error) {
// createTimeoutContext creates a timeout context with 5 seconds timeout
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 {
return nil, nil
}
ctx, err := ast.Script.NewContext(context.Sid, nil)
scriptCtx, err := ast.Script.NewContext(context.Sid, nil)
if err != nil {
return nil, err
}
defer ctx.Close()
defer scriptCtx.Close()
// Check if the method exists
if !ctx.Global().Has(method) {
if !scriptCtx.Global().Has(method) {
return nil, fmt.Errorf(HookErrorMethodNotFound)
}
// Call the method
args = append([]interface{}{context.Map()}, args...)
return ctx.Call(method, args...)
// Create done channel for handling cancellation
done := make(chan struct{})
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"
"mime/multipart"
"github.com/gin-gonic/gin"
"github.com/yaoapp/gou/rag/driver"
v8 "github.com/yaoapp/gou/runtime/v8"
chatctx "github.com/yaoapp/yao/neo/context"
@ -14,11 +15,11 @@ import (
// API the assistant 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)
Download(ctx context.Context, fileID string) (*FileResponse, 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

View file

@ -22,6 +22,8 @@ type Message struct {
IsDone bool `json:"done,omitempty"`
Actions []Action `json:"actions,omitempty"` // Conversation Actions for frontend
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:"-"`
}
@ -134,12 +136,74 @@ func (m *Message) Error(message interface{}) *Message {
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
func (m *Message) Map(msg map[string]interface{}) *Message {
if msg == nil {
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 {
m.Text = text
}

View file

@ -18,7 +18,7 @@ var lock sync.Mutex = sync.Mutex{}
// Answer reply the message
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 {
msg := message.New().Error(err).Done()
msg.Write(c.Writer)
@ -36,7 +36,7 @@ func (neo *DSL) Answer(ctx chatctx.Context, question string, c *gin.Context) err
}
// Init the assistant
res, err = ast.HookInit(ctx, []message.Message{{Text: question}})
res, err = ast.HookInit(c, ctx, messages)
if err != nil {
return err
}
@ -131,7 +131,11 @@ func (neo *DSL) GenerateWithAI(ctx chatctx.Context, input string, messageType st
// Chat with AI in background
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 {
case <-clientBreak:
return 0 // break
@ -271,7 +275,7 @@ func (neo *DSL) Download(ctx chatctx.Context, c *gin.Context) (*assistant.FileRe
}
// 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 {
msg := message.New().Error("assistant is not initialized").Done()
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) chatMessages(ctx chatctx.Context, content ...string) ([]map[string]interface{}, error) {
func (neo *DSL) withHistory(ctx chatctx.Context, question string) ([]message.Message, error) {
history, err := neo.Store.GetHistory(ctx.Sid, ctx.ChatID)
if err != nil {
return nil, err
}
messages := []map[string]interface{}{}
messages = append(messages, history...)
if len(content) == 0 {
return messages, nil
// Add history messages
messages := []message.Message{}
for _, h := range history {
messages = append(messages, *message.New().Map(h))
}
// 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
}
// 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 {
err := neo.Store.SaveHistory(
sid,
[]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},
},
chatID,