package model import ( "encoding/json" "fmt" "gorm.io/gorm" ) // --- Copilot Thread Model --- // Reference: Chatwoot enterprise/app/models/copilot_thread.rb // Table: copilot_threads // // CopilotThread represents a conversation thread between a user // and a Captain Assistant in the Copilot UI (side-panel chat). // Linked to Account, User, and optionally an Assistant. type CopilotThread struct { Base AccountID uint `gorm:"index;not null" json:"account_id"` UserID uint `gorm:"index;not null" json:"user_id"` AssistantID *uint `gorm:"index" json:"assistant_id,omitempty"` Title string `gorm:"size:255;not null" json:"title"` // Relationships Messages []CopilotMessage `gorm:"foreignKey:CopilotThreadID" json:"messages,omitempty"` User User `gorm:"foreignKey:UserID" json:"user,omitempty"` Assistant CaptainAssistant `gorm:"foreignKey:AssistantID" json:"assistant,omitempty"` } func (CopilotThread) TableName() string { return "copilot_threads" } // PreviousHistory returns the conversation history as role/content pairs // for LLM context building. Reference: Chatwoot CopilotThread#previous_history func (t *CopilotThread) PreviousHistory(messages []CopilotMessage) []ChatMessage { var history []ChatMessage for _, m := range messages { if m.MessageType == CopilotMessageTypeUser || m.MessageType == CopilotMessageTypeAssistant { content := m.GetMessageContent() history = append(history, ChatMessage{ Role: string(m.MessageType), Content: content, }) } } return history } // --- Copilot Message Model --- // Reference: Chatwoot enterprise/app/models/copilot_message.rb // Table: copilot_messages // // CopilotMessage represents a single message within a CopilotThread. // Message content is stored as JSONB (supports structured content, // tool results, thinking traces). MessageType: user/assistant/assistant_thinking. type CopilotMessage struct { Base AccountID uint `gorm:"index;not null" json:"account_id"` CopilotThreadID uint `gorm:"index;not null" json:"copilot_thread_id"` MessageType CopilotMessageType `gorm:"size:50;default:user;not null" json:"message_type"` Message json.RawMessage `gorm:"type:jsonb;not null" json:"message"` CopilotThread CopilotThread `gorm:"foreignKey:CopilotThreadID" json:"copilot_thread,omitempty"` } func (CopilotMessage) TableName() string { return "copilot_messages" } // BeforeSave mirrors Chatwoot's CopilotMessage JSON key validation so // reloadable tool-call state stays compatible with the reused frontend. func (m *CopilotMessage) BeforeSave(tx *gorm.DB) error { if len(m.Message) == 0 { return nil } var msgMap map[string]interface{} if err := json.Unmarshal(m.Message, &msgMap); err != nil { return err } allowed := map[string]bool{ "content": true, "reasoning": true, "function_name": true, "reply_suggestion": true, } for key := range msgMap { if !allowed[key] { return fmt.Errorf("message contains invalid attribute: %s", key) } } return nil } // GetMessageContent extracts the "content" field from the JSONB message. func (m *CopilotMessage) GetMessageContent() string { var msgMap map[string]interface{} if err := json.Unmarshal(m.Message, &msgMap); err != nil { return "" } if val, ok := msgMap["content"]; ok { s, _ := val.(string) return s } return "" } // CopilotSuggestionMessage represents a Copilot suggestion/reply tied to a conversation. // These are "side-panel" suggestions the Copilot generates for the agent, // distinct from the thread-based CopilotMessage (which tracks the full chat history). // Reference: Chatwoot enterprise/app/controllers/api/v1/copilot_messages_controller.rb // Table: copilot_suggestion_messages type CopilotSuggestionType string const ( CopilotSuggestionTypeReply CopilotSuggestionType = "reply" CopilotSuggestionTypeSuggestion CopilotSuggestionType = "suggestion" CopilotSuggestionTypeSummary CopilotSuggestionType = "summary" ) type CopilotSuggestionStatus string const ( CopilotSuggestionStatusPending CopilotSuggestionStatus = "pending" CopilotSuggestionStatusAccepted CopilotSuggestionStatus = "accepted" CopilotSuggestionStatusRejected CopilotSuggestionStatus = "rejected" ) type CopilotSuggestionMessage struct { Base AccountID uint `gorm:"index;not null" json:"account_id"` ConversationID uint `gorm:"index;not null" json:"conversation_id"` Content string `gorm:"type:text;not null" json:"content"` SuggestionType CopilotSuggestionType `gorm:"size:50;not null;default:suggestion" json:"suggestion_type"` Status CopilotSuggestionStatus `gorm:"size:50;not null;default:pending" json:"status"` } func (CopilotSuggestionMessage) TableName() string { return "copilot_suggestion_messages" } // ChatMessage is a simplified LLM message format (role + content). // Used for building LLM conversation context. type ChatMessage struct { Role string `json:"role"` Content string `json:"content"` }