Reorganize repo: backend/, deploy/, docs/ layout + AGENTS.md
Restructure the monorepo into clear top-level directories: - backend/: Go module root (cmd, internal, pkg, configs, migrations, docs/swagger, scripts, tests, go.mod, Makefile, .air.toml) - deploy/: Docker (Dockerfile, docker-compose*), quickstart, fluentd - docs/: project documentation + reports/ (moved from repo root) - AGENTS.md: new AI coding-agent guide at repo root Update all references to the new layout: - Dockerfile: COPY backend/go.mod, COPY backend/ (context = repo root) - docker-compose files: context ../.., dockerfile deploy/docker/Dockerfile, env_file ../../.env, volume mounts ../../backend:/app - deploy/quickstart/compose.yaml: dockerfile deploy/docker/Dockerfile - CI: working-directory: backend for go commands, file deploy/docker/Dockerfile, coverage path backend/coverage.out, health_check backend/scripts/ - backend/Makefile: docker target uses -f ../deploy/docker/Dockerfile ../ - README: architecture tree, quickstart, config paths updated Move root stray scripts (rename_models.*, run_m11_tests.sh, verify_build.sh, gorm_bool_main.go) to backend/scripts/legacy/. All moves via git mv to preserve history. Build, vet, SQLite tests, and docker compose config verified.
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
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"`
|
||||
}
|
||||
Reference in New Issue
Block a user