Refactor Neo API assistant message handling and content structure

- Replaced the Content struct with a new Contents struct to improve message handling and organization.
- Updated methods across the Assistant and Message structs to utilize the new Contents structure, enhancing the clarity and efficiency of message processing.
- Improved error handling by ensuring proper message appending and content management during chat streaming.
- Removed the deprecated Content struct, streamlining the codebase and reducing complexity.

These changes enhance the maintainability and robustness of the Neo API assistant, paving the way for improved message handling and assistant functionalities.
This commit is contained in:
Max 2025-01-19 12:23:23 +08:00
parent 5b30ef5fde
commit 5d5fa4c270
7 changed files with 230 additions and 141 deletions

View file

@ -56,6 +56,11 @@ func (ast *Assistant) Execute(c *gin.Context, ctx chatctx.Context, input string,
// Run init hook // Run init hook
res, err := ast.HookInit(c, ctx, messages, options) res, err := ast.HookInit(c, ctx, messages, options)
if err != nil { if err != nil {
chatMessage.New().
Assistant(ast.ID, ast.Name, ast.Avatar).
Error(err).
Done().
Write(c.Writer)
return err return err
} }
@ -63,6 +68,11 @@ func (ast *Assistant) Execute(c *gin.Context, ctx chatctx.Context, input string,
if res != nil && res.AssistantID != ctx.AssistantID { if res != nil && res.AssistantID != ctx.AssistantID {
newAst, err := Get(res.AssistantID) newAst, err := Get(res.AssistantID)
if err != nil { if err != nil {
chatMessage.New().
Assistant(ast.ID, ast.Name, ast.Avatar).
Error(err).
Done().
Write(c.Writer)
return err return err
} }
*ast = *newAst *ast = *newAst
@ -163,16 +173,16 @@ func (next *NextAction) Execute(c *gin.Context, ctx chatctx.Context) error {
func (ast *Assistant) handleChatStream(c *gin.Context, ctx chatctx.Context, messages []chatMessage.Message, options map[string]interface{}) error { func (ast *Assistant) handleChatStream(c *gin.Context, ctx chatctx.Context, messages []chatMessage.Message, options map[string]interface{}) error {
clientBreak := make(chan bool, 1) clientBreak := make(chan bool, 1)
done := make(chan bool, 1) done := make(chan bool, 1)
content := chatMessage.NewContent("text") contents := chatMessage.NewContents()
// Chat with AI in background // Chat with AI in background
go func() { go func() {
err := ast.streamChat(c, ctx, messages, options, clientBreak, done, content) err := ast.streamChat(c, ctx, messages, options, clientBreak, done, contents)
if err != nil { if err != nil {
chatMessage.New().Error(err).Done().Write(c.Writer) chatMessage.New().Error(err).Done().Write(c.Writer)
} }
ast.saveChatHistory(ctx, messages, content) ast.saveChatHistory(ctx, messages, contents)
done <- true done <- true
}() }()
@ -194,7 +204,7 @@ func (ast *Assistant) streamChat(
options map[string]interface{}, options map[string]interface{},
clientBreak chan bool, clientBreak chan bool,
done chan bool, done chan bool,
content *chatMessage.Content) error { contents *chatMessage.Contents) error {
return ast.Chat(c.Request.Context(), messages, options, func(data []byte) int { return ast.Chat(c.Request.Context(), messages, options, func(data []byte) int {
select { select {
@ -210,7 +220,7 @@ func (ast *Assistant) streamChat(
// Handle error // Handle error
if msg.Type == "error" { if msg.Type == "error" {
value := msg.String() value := msg.String()
res, hookErr := ast.HookFail(c, ctx, messages, content.String(), fmt.Errorf("%s", value)) res, hookErr := ast.HookFail(c, ctx, messages, contents.JSON(), fmt.Errorf("%s", value))
if hookErr == nil && res != nil && (res.Output != "" || res.Error != "") { if hookErr == nil && res != nil && (res.Output != "" || res.Error != "") {
value = res.Output value = res.Output
if res.Error != "" { if res.Error != "" {
@ -221,26 +231,12 @@ func (ast *Assistant) streamChat(
return 0 // break return 0 // break
} }
// Handle tool call
if msg.Type == "tool_calls" {
content.SetType("function") // Set type to function
// Set id
if id, ok := msg.Props["id"].(string); ok && id != "" {
content.SetID(id)
}
// Set name
if name, ok := msg.Props["name"].(string); ok && name != "" {
content.SetName(name)
}
}
// Append content and send message // Append content and send message
msg.Append(contents)
value := msg.String() value := msg.String()
content.Append(value)
if value != "" { if value != "" {
// Handle stream // Handle stream
res, err := ast.HookStream(c, ctx, messages, content.String(), content.Type == "function") res, err := ast.HookStream(c, ctx, messages, contents.JSON())
if err == nil && res != nil { if err == nil && res != nil {
if res.Output != "" { if res.Output != "" {
value = res.Output value = res.Output
@ -278,9 +274,7 @@ func (ast *Assistant) streamChat(
msg.Write(c.Writer) msg.Write(c.Writer)
} }
// Call HookDone res, hookErr := ast.HookDone(c, ctx, messages, contents.JSON())
content.SetStatus(chatMessage.ContentStatusDone)
res, hookErr := ast.HookDone(c, ctx, messages, content.String(), content.Type == "function")
if hookErr == nil && res != nil { if hookErr == nil && res != nil {
if res.Output != "" { if res.Output != "" {
chatMessage.New(). chatMessage.New().
@ -322,8 +316,8 @@ func (ast *Assistant) streamChat(
} }
// saveChatHistory saves the chat history if storage is available // saveChatHistory saves the chat history if storage is available
func (ast *Assistant) saveChatHistory(ctx chatctx.Context, messages []chatMessage.Message, content *chatMessage.Content) { func (ast *Assistant) saveChatHistory(ctx chatctx.Context, messages []chatMessage.Message, contents *chatMessage.Contents) {
if len(content.Bytes) > 0 && ctx.Sid != "" && len(messages) > 0 { if len(contents.Data) > 0 && ctx.Sid != "" && len(messages) > 0 {
userMessage := messages[len(messages)-1] userMessage := messages[len(messages)-1]
data := []map[string]interface{}{ data := []map[string]interface{}{
{ {
@ -333,7 +327,7 @@ func (ast *Assistant) saveChatHistory(ctx chatctx.Context, messages []chatMessag
}, },
{ {
"role": "assistant", "role": "assistant",
"content": content.String(), "content": contents.JSON(),
"name": ctx.Sid, "name": ctx.Sid,
"assistant_id": ast.ID, "assistant_id": ast.ID,
"assistant_name": ast.Name, "assistant_name": ast.Name,

View file

@ -6,6 +6,7 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
jsoniter "github.com/json-iterator/go"
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"
) )
@ -34,6 +35,17 @@ func (ast *Assistant) HookInit(c *gin.Context, context chatctx.Context, input []
response.ChatID = res response.ChatID = res
} }
// input
if input, has := v["input"]; has {
raw, _ := jsoniter.MarshalToString(input)
vv := []message.Message{}
err := jsoniter.UnmarshalFromString(raw, &vv)
if err != nil {
return nil, err
}
response.Input = vv
}
if res, ok := v["next"].(map[string]interface{}); ok { if res, ok := v["next"].(map[string]interface{}); ok {
response.Next = &NextAction{} response.Next = &NextAction{}
if name, ok := res["action"].(string); ok { if name, ok := res["action"].(string); ok {
@ -57,13 +69,13 @@ func (ast *Assistant) HookInit(c *gin.Context, context chatctx.Context, input []
} }
// HookStream Handle streaming response from LLM // HookStream Handle streaming response from LLM
func (ast *Assistant) HookStream(c *gin.Context, context chatctx.Context, input []message.Message, output string, toolcall bool) (*ResHookStream, error) { func (ast *Assistant) HookStream(c *gin.Context, context chatctx.Context, input []message.Message, output string) (*ResHookStream, error) {
// Create timeout context // Create timeout context
ctx, cancel := ast.createTimeoutContext(c) ctx, cancel := ast.createTimeoutContext(c)
defer cancel() defer cancel()
v, err := ast.call(ctx, "Stream", context, input, output, toolcall, c.Writer) v, err := ast.call(ctx, "Stream", context, input, output, c.Writer)
if err != nil { if err != nil {
if err.Error() == HookErrorMethodNotFound { if err.Error() == HookErrorMethodNotFound {
return nil, nil return nil, nil
@ -100,12 +112,12 @@ func (ast *Assistant) HookStream(c *gin.Context, context chatctx.Context, input
} }
// HookDone Handle completion of assistant response // HookDone Handle completion of assistant response
func (ast *Assistant) HookDone(c *gin.Context, context chatctx.Context, input []message.Message, output string, toolcall bool) (*ResHookDone, error) { func (ast *Assistant) HookDone(c *gin.Context, context chatctx.Context, input []message.Message, output string) (*ResHookDone, error) {
// Create timeout context // Create timeout context
ctx, cancel := ast.createTimeoutContext(c) ctx, cancel := ast.createTimeoutContext(c)
defer cancel() defer cancel()
v, err := ast.call(ctx, "Done", context, input, output, toolcall, c.Writer) v, err := ast.call(ctx, "Done", context, input, output, c.Writer)
if err != nil { if err != nil {
if err.Error() == HookErrorMethodNotFound { if err.Error() == HookErrorMethodNotFound {
return nil, nil return nil, nil
@ -185,7 +197,7 @@ func (ast *Assistant) HookFail(c *gin.Context, context chatctx.Context, input []
// createTimeoutContext creates a timeout context with 5 seconds timeout // createTimeoutContext creates a timeout context with 5 seconds timeout
func (ast *Assistant) createTimeoutContext(c *gin.Context) (context.Context, context.CancelFunc) { func (ast *Assistant) createTimeoutContext(c *gin.Context) (context.Context, context.CancelFunc) {
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second) ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
return ctx, cancel return ctx, cancel
} }

View file

@ -28,6 +28,10 @@ var defaultConnector string = "" // default connector
// LoadBuiltIn load the built-in assistants // LoadBuiltIn load the built-in assistants
func LoadBuiltIn() error { func LoadBuiltIn() error {
// Clear the cache
loaded.Clear()
root := `/assistants` root := `/assistants`
app, err := fs.Get("app") app, err := fs.Get("app")
if err != nil { if err != nil {

View file

@ -1,100 +0,0 @@
package message
import (
jsoniter "github.com/json-iterator/go"
)
const (
// ContentStatusPending the content status pending
ContentStatusPending = iota
// ContentStatusDone the content status done
ContentStatusDone
// ContentStatusError the content status error
ContentStatusError
)
// Content the content
type Content struct {
ID string `json:"id"`
Name string `json:"name"`
Bytes []byte `json:"bytes"`
Type string `json:"type"` // text, function, error
Status uint8 `json:"status"` // 0: pending, 1: done
}
// NewContent create a new content
func NewContent(typ string) *Content {
if typ == "" {
typ = "text"
}
return &Content{
Bytes: []byte{},
Type: typ,
Status: ContentStatusPending,
}
}
// String the content string
func (c *Content) String() string {
if c.Type == "function" {
var arguments interface{} = string(c.Bytes)
if c.Status == ContentStatusDone {
var vv interface{} = nil
err := jsoniter.Unmarshal(c.Bytes, &vv)
if err != nil {
return ""
}
arguments = vv
}
data := map[string]interface{}{
"id": c.ID,
"type": "function",
"text": c.Name,
"props": map[string]interface{}{
"id": c.ID,
"name": c.Name,
"arguments": arguments,
},
"function": map[string]interface{}{
"name": c.Name,
"arguments": arguments,
},
}
raw, err := jsoniter.MarshalToString(data)
if err != nil {
return ""
}
return raw
}
return string(c.Bytes)
}
// SetID set the content id
func (c *Content) SetID(id string) {
c.ID = id
}
// SetName set the content name
func (c *Content) SetName(name string) {
c.Name = name
}
// SetType set the content type
func (c *Content) SetType(typ string) {
c.Type = typ
}
// Append append the content
func (c *Content) Append(data string) {
c.Bytes = append(c.Bytes, []byte(data)...)
}
// SetStatus set the content status
func (c *Content) SetStatus(status uint8) {
c.Status = status
}

150
neo/message/contents.go Normal file
View file

@ -0,0 +1,150 @@
package message
import (
jsoniter "github.com/json-iterator/go"
)
const (
// ContentStatusPending the content status pending
ContentStatusPending = iota
// ContentStatusDone the content status done
ContentStatusDone
// ContentStatusError the content status error
ContentStatusError
)
// Contents the contents
type Contents struct {
Current int `json:"current"` // the current content index
Data []Data `json:"data"` // the data
}
// Data the data of the content
type Data struct {
Type string `json:"type"` // text, function, error, ...
ID string `json:"id"` // the id of the content
Function string `json:"function"` // the function name
Bytes []byte `json:"bytes"` // the content bytes
Arguments []byte `json:"arguments"` // the function arguments
}
// NewContents create a new contents
func NewContents() *Contents {
return &Contents{
Current: -1,
Data: []Data{},
}
}
// NewText create a new text data and append to the contents
func (c *Contents) NewText(bytes []byte) *Contents {
c.Data = append(c.Data, Data{
Type: "text",
Bytes: bytes,
})
c.Current++
return c
}
// NewFunction create a new function data and append to the contents
func (c *Contents) NewFunction(function string, arguments []byte) *Contents {
c.Data = append(c.Data, Data{
Type: "function",
Function: function,
Arguments: arguments,
})
c.Current++
return c
}
// SetFunctionID set the id of the current function content
func (c *Contents) SetFunctionID(id string) *Contents {
if c.Current == -1 {
c.NewFunction("", []byte{})
}
c.Data[c.Current].ID = id
return c
}
// NewError create a new error data and append to the contents
func (c *Contents) NewError(err []byte) *Contents {
c.Data = append(c.Data, Data{
Type: "error",
Bytes: err,
})
c.Current++
return c
}
// AppendText append the text to the current content
func (c *Contents) AppendText(bytes []byte) *Contents {
if c.Current == -1 {
c.NewText(bytes)
return c
}
c.Data[c.Current].Bytes = append(c.Data[c.Current].Bytes, bytes...)
return c
}
// AppendFunction append the function to the current content
func (c *Contents) AppendFunction(arguments []byte) *Contents {
if c.Current == -1 {
c.NewFunction("", arguments)
return c
}
c.Data[c.Current].Arguments = append(c.Data[c.Current].Arguments, arguments...)
return c
}
// AppendError append the error to the current content
func (c *Contents) AppendError(err []byte) *Contents {
if c.Current == -1 {
c.NewError(err)
return c
}
c.Data[c.Current].Bytes = append(c.Data[c.Current].Bytes, err...)
return c
}
// JSON returns the json representation
func (c *Contents) JSON() string {
raw, _ := jsoniter.MarshalToString(c.Data)
return raw
}
// Text returns the text of the current content
func (c *Contents) Text() string {
if c.Current == -1 {
return ""
}
return string(c.Data[c.Current].Bytes)
}
// MarshalJSON returns the json representation
func (data *Data) MarshalJSON() ([]byte, error) {
v := map[string]interface{}{"type": data.Type}
if data.ID != "" {
v["id"] = data.ID
}
if data.Bytes != nil {
v["bytes"] = string(data.Bytes)
}
if data.Arguments != nil {
var vv interface{} = nil
err := jsoniter.Unmarshal(data.Arguments, &vv)
if err != nil {
return nil, err
}
v["arguments"] = vv
}
if data.Function != "" {
v["function"] = data.Function
}
return jsoniter.Marshal(v)
}

View file

@ -99,7 +99,7 @@ func NewOpenAI(data []byte) *Message {
msg.Type = "tool_calls" msg.Type = "tool_calls"
if len(toolCalls.Choices) > 0 && len(toolCalls.Choices[0].Delta.ToolCalls) > 0 { if len(toolCalls.Choices) > 0 && len(toolCalls.Choices[0].Delta.ToolCalls) > 0 {
msg.Props["id"] = toolCalls.Choices[0].Delta.ToolCalls[0].ID msg.Props["id"] = toolCalls.Choices[0].Delta.ToolCalls[0].ID
msg.Props["name"] = toolCalls.Choices[0].Delta.ToolCalls[0].Function.Name msg.Props["function"] = toolCalls.Choices[0].Delta.ToolCalls[0].Function.Name
msg.Text = toolCalls.Choices[0].Delta.ToolCalls[0].Function.Arguments msg.Text = toolCalls.Choices[0].Delta.ToolCalls[0].Function.Arguments
} }
@ -184,6 +184,32 @@ func (m *Message) SetContent(content string) *Message {
return m return m
} }
// Append append the contents
func (m *Message) Append(contents *Contents) *Message {
switch m.Type {
case "text":
if m.Text != "" {
contents.AppendText([]byte(m.Text))
}
case "tool_calls":
// Set function name
if name, ok := m.Props["function"].(string); ok && name != "" {
contents.NewFunction(name, []byte(m.Text))
}
// Set id
if id, ok := m.Props["id"].(string); ok && id != "" {
contents.SetFunctionID(id)
}
contents.AppendFunction([]byte(m.Text))
}
return m
}
// Content get the content // Content get the content
func (m *Message) Content() string { func (m *Message) Content() string {
content := map[string]interface{}{"text": m.Text} content := map[string]interface{}{"text": m.Text}
@ -291,6 +317,14 @@ func (m *Message) Done() *Message {
return m return m
} }
// Assistant set the assistant
func (m *Message) Assistant(id string, name string, avatar string) *Message {
m.AssistantID = id
m.AssistantName = name
m.AssistantAvatar = avatar
return m
}
// Action add an action // Action add an action
func (m *Message) Action(name string, t string, payload interface{}, next string) *Message { func (m *Message) Action(name string, t string, payload interface{}, next string) *Message {
if m.Data != nil { if m.Data != nil {
@ -339,11 +373,6 @@ func (m *Message) Write(w gin.ResponseWriter) bool {
return true return true
} }
// Append appends content to the byte slice
func (m *Message) Append(content []byte) []byte {
return append(content, []byte(m.Text)...)
}
// WriteError writes an error message to response writer // WriteError writes an error message to response writer
func (m *Message) WriteError(w gin.ResponseWriter, message string) { func (m *Message) WriteError(w gin.ResponseWriter, message string) {
errMsg := strings.Trim(exception.New(message, 500).Message, "\"") errMsg := strings.Trim(exception.New(message, 500).Message, "\"")

View file

@ -100,7 +100,7 @@ func (neo *DSL) GenerateWithAI(ctx chatctx.Context, input string, messageType st
clientBreak := make(chan bool, 1) clientBreak := make(chan bool, 1)
done := make(chan bool, 1) done := make(chan bool, 1)
fail := make(chan error, 1) fail := make(chan error, 1)
content := []byte{} contents := message.NewContents()
// Chat with AI in background // Chat with AI in background
go func() { go func() {
@ -126,7 +126,7 @@ func (neo *DSL) GenerateWithAI(ctx chatctx.Context, input string, messageType st
} }
// Append content and send message // Append content and send message
content = msg.Append(content) msg.Append(contents)
if !silent { if !silent {
value := msg.String() value := msg.String()
if value != "" { if value != "" {
@ -166,7 +166,7 @@ func (neo *DSL) GenerateWithAI(ctx chatctx.Context, input string, messageType st
// Wait for completion or client disconnect // Wait for completion or client disconnect
select { select {
case <-done: case <-done:
return string(content), nil return contents.Text(), nil
case err := <-fail: case err := <-fail:
return "", err return "", err
case <-c.Writer.CloseNotify(): case <-c.Writer.CloseNotify():