- Added support for parsing and handling pending and error messages in stream processing - Improved error message extraction and writing for better error visibility - Updated message type handling to capture and process raw error messages - Introduced more flexible input parsing in withHistory and execute methods - Added environment variable support for connector configuration in assistant loading This change improves the robustness of error handling and message processing in the Neo API assistant, providing more comprehensive error reporting and input flexibility.
594 lines
15 KiB
Go
594 lines
15 KiB
Go
package message
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/fatih/color"
|
|
"github.com/gin-gonic/gin"
|
|
jsoniter "github.com/json-iterator/go"
|
|
"github.com/yaoapp/gou/helper"
|
|
"github.com/yaoapp/kun/exception"
|
|
"github.com/yaoapp/kun/log"
|
|
"github.com/yaoapp/kun/maps"
|
|
"github.com/yaoapp/yao/openai"
|
|
)
|
|
|
|
// Message the message
|
|
type Message struct {
|
|
Text string `json:"text,omitempty"` // text content
|
|
Type string `json:"type,omitempty"` // error, text, plan, table, form, page, file, video, audio, image, markdown, json ...
|
|
Props map[string]interface{} `json:"props,omitempty"` // props for the types
|
|
IsDone bool `json:"done,omitempty"` // Mark as a done message from neo
|
|
IsNew bool `json:"new,omitempty"` // Mark as a new message from neo
|
|
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
|
|
AssistantID string `json:"assistant_id,omitempty"` // assistant_id (for assistant role = assistant )
|
|
AssistantName string `json:"assistant_name,omitempty"` // assistant_name (for assistant role = assistant )
|
|
AssistantAvatar string `json:"assistant_avatar,omitempty"` // assistant_avatar (for assistant role = assistant )
|
|
Mentions []Mention `json:"menions,omitempty"` // Mentions for the message ( for user role = user )
|
|
Data map[string]interface{} `json:"-"` // data for the message
|
|
Pending bool `json:"-"` // pending for the message
|
|
}
|
|
|
|
// Mention represents a mention
|
|
type Mention struct {
|
|
ID string `json:"assistant_id"` // assistant_id
|
|
Name string `json:"name"` // name
|
|
Avatar string `json:"avatar,omitempty"` // avatar
|
|
}
|
|
|
|
// Attachment represents a file attachment
|
|
type Attachment struct {
|
|
Name string `json:"name,omitempty"`
|
|
URL string `json:"url,omitempty"`
|
|
Description string `json:"description,omitempty"`
|
|
Type string `json:"type,omitempty"`
|
|
ContentType string `json:"content_type,omitempty"`
|
|
Bytes int64 `json:"bytes,omitempty"`
|
|
CreatedAt int64 `json:"created_at,omitempty"`
|
|
FileID string `json:"file_id,omitempty"`
|
|
ChatID string `json:"chat_id,omitempty"`
|
|
AssistantID string `json:"assistant_id,omitempty"`
|
|
}
|
|
|
|
// Action the action
|
|
type Action struct {
|
|
Name string `json:"name,omitempty"`
|
|
Type string `json:"type"`
|
|
Payload interface{} `json:"payload,omitempty"`
|
|
}
|
|
|
|
// New create a new message
|
|
func New() *Message {
|
|
return &Message{Actions: []Action{}, Props: map[string]interface{}{}}
|
|
}
|
|
|
|
// NewHistory create a new message from history
|
|
func NewHistory(history map[string]interface{}) ([]Message, error) {
|
|
if history == nil {
|
|
return []Message{}, nil
|
|
}
|
|
|
|
var copy map[string]interface{} = map[string]interface{}{}
|
|
for key, value := range history {
|
|
if key != "content" {
|
|
copy[key] = value
|
|
}
|
|
}
|
|
|
|
globalMessage := New().Map(copy)
|
|
messages := []Message{}
|
|
if content, ok := history["content"].(string); ok {
|
|
if strings.HasPrefix(content, "{") && strings.HasSuffix(content, "}") {
|
|
var msg Message = *globalMessage
|
|
if err := jsoniter.UnmarshalFromString(content, &msg); err != nil {
|
|
return nil, err
|
|
}
|
|
messages = append(messages, msg)
|
|
} else if strings.HasPrefix(content, "[") && strings.HasSuffix(content, "]") {
|
|
var msgs []Message
|
|
if err := jsoniter.UnmarshalFromString(content, &msgs); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, msg := range msgs {
|
|
msg.AssistantID = globalMessage.AssistantID
|
|
msg.AssistantName = globalMessage.AssistantName
|
|
msg.AssistantAvatar = globalMessage.AssistantAvatar
|
|
msg.Role = globalMessage.Role
|
|
msg.Name = globalMessage.Name
|
|
msg.Mentions = globalMessage.Mentions
|
|
messages = append(messages, msg)
|
|
}
|
|
} else {
|
|
messages = append(messages, Message{Text: content})
|
|
}
|
|
}
|
|
|
|
return messages, nil
|
|
}
|
|
|
|
// NewContent create a new message from content
|
|
func NewContent(content string) ([]Message, error) {
|
|
messages := []Message{}
|
|
if strings.HasPrefix(content, "{") && strings.HasSuffix(content, "}") {
|
|
var msg Message
|
|
if err := jsoniter.UnmarshalFromString(content, &msg); err != nil {
|
|
return nil, err
|
|
}
|
|
messages = append(messages, msg)
|
|
} else if strings.HasPrefix(content, "[") && strings.HasSuffix(content, "]") {
|
|
var msgs []Message
|
|
if err := jsoniter.UnmarshalFromString(content, &msgs); err != nil {
|
|
return nil, err
|
|
}
|
|
for _, msg := range msgs {
|
|
messages = append(messages, msg)
|
|
}
|
|
} else {
|
|
messages = append(messages, Message{Text: content})
|
|
}
|
|
return messages, nil
|
|
}
|
|
|
|
// NewString create a new message from string
|
|
func NewString(content string) (*Message, error) {
|
|
if strings.HasPrefix(content, "{") && strings.HasSuffix(content, "}") {
|
|
var msg Message
|
|
if err := jsoniter.UnmarshalFromString(content, &msg); err != nil {
|
|
return nil, err
|
|
}
|
|
return &msg, nil
|
|
}
|
|
return &Message{Text: content}, nil
|
|
}
|
|
|
|
// NewStringError create a new message from string error
|
|
func NewStringError(content string) (*Message, error) {
|
|
if strings.HasPrefix(content, "{") && strings.HasSuffix(content, "}") {
|
|
var msg = New()
|
|
var errorMessage openai.ErrorMessage
|
|
if err := jsoniter.UnmarshalFromString(content, &errorMessage); err != nil {
|
|
msg.Text = err.Error() + "\n" + content
|
|
return msg, nil
|
|
}
|
|
msg.Type = "error"
|
|
msg.Text = errorMessage.Error.Message
|
|
return msg, nil
|
|
}
|
|
return &Message{Text: content}, nil
|
|
}
|
|
|
|
// NewMap create a new message from map
|
|
func NewMap(content map[string]interface{}) (*Message, error) {
|
|
return New().Map(content), nil
|
|
}
|
|
|
|
// NewAny create a new message from any content
|
|
func NewAny(content interface{}) (*Message, error) {
|
|
switch v := content.(type) {
|
|
case string:
|
|
return NewString(v)
|
|
case map[string]interface{}:
|
|
return NewMap(v)
|
|
}
|
|
return nil, fmt.Errorf("unknown content type: %T", content)
|
|
}
|
|
|
|
// NewOpenAI create a new message from OpenAI response
|
|
func NewOpenAI(data []byte) *Message {
|
|
if data == nil || len(data) == 0 {
|
|
return nil
|
|
}
|
|
|
|
msg := New()
|
|
text := string(data)
|
|
data = []byte(strings.TrimPrefix(text, "data: "))
|
|
switch {
|
|
|
|
case strings.Contains(text, `"delta":{`) && strings.Contains(text, `"tool_calls"`):
|
|
var toolCalls openai.ToolCalls
|
|
if err := jsoniter.Unmarshal(data, &toolCalls); err != nil {
|
|
msg.Text = err.Error() + "\n" + string(data)
|
|
return msg
|
|
}
|
|
|
|
msg.Type = "tool_calls"
|
|
if len(toolCalls.Choices) > 0 && len(toolCalls.Choices[0].Delta.ToolCalls) > 0 {
|
|
msg.Props["id"] = toolCalls.Choices[0].Delta.ToolCalls[0].ID
|
|
msg.Props["function"] = toolCalls.Choices[0].Delta.ToolCalls[0].Function.Name
|
|
msg.Text = toolCalls.Choices[0].Delta.ToolCalls[0].Function.Arguments
|
|
}
|
|
|
|
case strings.Contains(text, `"delta":{`) && strings.Contains(text, `"content":`):
|
|
var message openai.Message
|
|
if err := jsoniter.Unmarshal(data, &message); err != nil {
|
|
msg.Text = err.Error() + "\n" + string(data)
|
|
return msg
|
|
}
|
|
|
|
msg.Type = "text"
|
|
if len(message.Choices) > 0 {
|
|
msg.Text = message.Choices[0].Delta.Content
|
|
}
|
|
|
|
case strings.Contains(text, `{"error":{`):
|
|
var errorMessage openai.ErrorMessage
|
|
if err := jsoniter.Unmarshal(data, &errorMessage); err != nil {
|
|
msg.Text = err.Error() + "\n" + string(data)
|
|
return msg
|
|
}
|
|
msg.Type = "error"
|
|
msg.Text = errorMessage.Error.Message
|
|
|
|
case strings.Contains(text, `[DONE]`):
|
|
msg.IsDone = true
|
|
|
|
case strings.Contains(text, `"finish_reason":"stop"`):
|
|
msg.IsDone = true
|
|
|
|
case strings.Contains(text, `"finish_reason":"tool_calls"`):
|
|
msg.IsDone = true
|
|
|
|
// Not a data message
|
|
case !strings.Contains(text, `data: `):
|
|
msg.Pending = true
|
|
msg.Text = text
|
|
|
|
default:
|
|
str := strings.TrimPrefix(strings.Trim(string(data), "\""), "data: ")
|
|
msg.Type = "error"
|
|
msg.Text = str
|
|
}
|
|
|
|
return msg
|
|
}
|
|
|
|
// String returns the string representation
|
|
func (m *Message) String() string {
|
|
typ := m.Type
|
|
if typ == "" {
|
|
typ = "text"
|
|
}
|
|
|
|
switch typ {
|
|
case "text":
|
|
return m.Text
|
|
|
|
case "error":
|
|
return m.Text
|
|
|
|
default:
|
|
raw, _ := jsoniter.MarshalToString(map[string]interface{}{"type": m.Type, "props": m.Props})
|
|
return raw
|
|
}
|
|
}
|
|
|
|
// SetText set the text
|
|
func (m *Message) SetText(text string) *Message {
|
|
m.Text = text
|
|
if m.Data != nil {
|
|
if replaced := helper.Bind(text, m.Data); replaced != nil {
|
|
if replacedText, ok := replaced.(string); ok {
|
|
m.Text = replacedText
|
|
}
|
|
}
|
|
}
|
|
return m
|
|
}
|
|
|
|
// Error set the error
|
|
func (m *Message) Error(message interface{}) *Message {
|
|
m.Type = "error"
|
|
switch v := message.(type) {
|
|
case error:
|
|
m.Text = v.Error()
|
|
case string:
|
|
m.Text = v
|
|
default:
|
|
m.Text = fmt.Sprintf("%v", 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
|
|
}
|
|
|
|
// AppendTo append the contents
|
|
func (m *Message) AppendTo(contents *Contents) *Message {
|
|
|
|
// Set type
|
|
if m.Type == "" {
|
|
m.Type = "text"
|
|
}
|
|
|
|
switch m.Type {
|
|
case "text":
|
|
if m.Text != "" {
|
|
if m.IsNew {
|
|
contents.NewText([]byte(m.Text))
|
|
return m
|
|
}
|
|
contents.AppendText([]byte(m.Text))
|
|
return m
|
|
}
|
|
return m
|
|
|
|
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
|
|
|
|
case "loading", "error", "action": // Ignore loading, action and error messages
|
|
return m
|
|
|
|
default:
|
|
if m.IsNew {
|
|
contents.NewType(m.Type, m.Props)
|
|
return m
|
|
}
|
|
contents.UpdateType(m.Type, m.Props)
|
|
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
|
|
}
|
|
if typ, ok := msg["type"].(string); ok {
|
|
m.Type = typ
|
|
}
|
|
if done, ok := msg["done"].(bool); ok {
|
|
m.IsDone = done
|
|
}
|
|
if props, ok := msg["props"].(map[string]interface{}); ok {
|
|
m.Props = props
|
|
}
|
|
|
|
if isNew, ok := msg["new"].(bool); ok {
|
|
m.IsNew = isNew
|
|
}
|
|
|
|
if assistantID, ok := msg["assistant_id"].(string); ok {
|
|
m.AssistantID = assistantID
|
|
|
|
// Set name
|
|
if m.Role == "assistant" {
|
|
m.Name = m.AssistantID
|
|
}
|
|
}
|
|
|
|
if assistantName, ok := msg["assistant_name"].(string); ok {
|
|
m.AssistantName = assistantName
|
|
}
|
|
|
|
if assistantAvatar, ok := msg["assistant_avatar"].(string); ok {
|
|
m.AssistantAvatar = assistantAvatar
|
|
}
|
|
|
|
if actions, ok := msg["actions"].([]interface{}); ok {
|
|
for _, action := range actions {
|
|
if v, ok := action.(map[string]interface{}); ok {
|
|
action := Action{}
|
|
if name, ok := v["name"].(string); ok {
|
|
action.Name = name
|
|
}
|
|
if t, ok := v["type"].(string); ok {
|
|
action.Type = t
|
|
}
|
|
if payload, ok := v["payload"].(map[string]interface{}); ok {
|
|
action.Payload = payload
|
|
}
|
|
m.Actions = append(m.Actions, action)
|
|
}
|
|
}
|
|
}
|
|
if data, ok := msg["data"].(map[string]interface{}); ok {
|
|
m.Data = data
|
|
}
|
|
return m
|
|
}
|
|
|
|
// Done set the done flag
|
|
func (m *Message) Done() *Message {
|
|
m.IsDone = true
|
|
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
|
|
func (m *Message) Action(name string, t string, payload interface{}, next string) *Message {
|
|
if m.Data != nil {
|
|
payload = helper.Bind(payload, m.Data)
|
|
}
|
|
m.Actions = append(m.Actions, Action{
|
|
Name: name,
|
|
Type: t,
|
|
Payload: payload,
|
|
})
|
|
return m
|
|
}
|
|
|
|
// Bind replace with data
|
|
func (m *Message) Bind(data map[string]interface{}) *Message {
|
|
if data == nil {
|
|
return m
|
|
}
|
|
m.Data = maps.Of(data).Dot()
|
|
return m
|
|
}
|
|
|
|
// Write writes the message to response writer
|
|
func (m *Message) Write(w gin.ResponseWriter) bool {
|
|
defer func() {
|
|
if r := recover(); r != nil {
|
|
message := "Write Response Exception: (if client close the connection, it's normal) \n %s\n\n"
|
|
color.Red(message, r)
|
|
}
|
|
}()
|
|
|
|
data, err := jsoniter.Marshal(m)
|
|
if err != nil {
|
|
log.Error("%s", err.Error())
|
|
return false
|
|
}
|
|
|
|
data = append([]byte("data: "), data...)
|
|
data = append(data, []byte("\n\n")...)
|
|
|
|
if _, err := w.Write(data); err != nil {
|
|
color.Red("Write JSON Message Error: %s", err.Error())
|
|
return false
|
|
}
|
|
w.Flush()
|
|
return true
|
|
}
|
|
|
|
// WriteError writes an error message to response writer
|
|
func (m *Message) WriteError(w gin.ResponseWriter, message string) {
|
|
errMsg := strings.Trim(exception.New(message, 500).Message, "\"")
|
|
data := []byte(fmt.Sprintf(`{"text":"%s","type":"error"`, errMsg))
|
|
if m.IsDone {
|
|
data = []byte(fmt.Sprintf(`{"text":"%s","type":"error","done":true`, errMsg))
|
|
}
|
|
data = append([]byte("data: "), data...)
|
|
data = append(data, []byte("}\n\n")...)
|
|
|
|
if _, err := w.Write(data); err != nil {
|
|
color.Red("Write JSON Message Error: %s", message)
|
|
}
|
|
w.Flush()
|
|
}
|
|
|
|
// MarshalJSON implements json.Marshaler interface
|
|
func (m *Message) MarshalJSON() ([]byte, error) {
|
|
type Alias Message
|
|
return jsoniter.Marshal(&struct {
|
|
*Alias
|
|
}{
|
|
Alias: (*Alias)(m),
|
|
})
|
|
}
|
|
|
|
// UnmarshalJSON implements json.Unmarshaler interface
|
|
func (m *Message) UnmarshalJSON(data []byte) error {
|
|
type Alias Message
|
|
aux := &struct {
|
|
*Alias
|
|
}{
|
|
Alias: (*Alias)(m),
|
|
}
|
|
if err := jsoniter.Unmarshal(data, &aux); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MarshalJSON implements json.Marshaler interface
|
|
func (a *Action) MarshalJSON() ([]byte, error) {
|
|
type Alias Action
|
|
return jsoniter.Marshal(&struct {
|
|
*Alias
|
|
}{
|
|
Alias: (*Alias)(a),
|
|
})
|
|
}
|
|
|
|
// UnmarshalJSON implements json.Unmarshaler interface
|
|
func (a *Action) UnmarshalJSON(data []byte) error {
|
|
type Alias Action
|
|
aux := &struct {
|
|
*Alias
|
|
}{
|
|
Alias: (*Alias)(a),
|
|
}
|
|
if err := jsoniter.Unmarshal(data, &aux); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|