Files
gochat/backend/internal/model/captain_models.go
T
Rogeeandrogee ef8931099c HH-469: close WebSocket fanout contract gaps (#109)
* HH-469: close websocket fanout contract gaps

* fix: unify message sender contracts

---------

Co-authored-by: Rogee <rogee@ipao.vip>
2026-08-23 00:22:01 +08:00

338 lines
15 KiB
Go

package model
import (
"encoding/json"
"fmt"
"os"
"strings"
"github.com/pgvector/pgvector-go"
)
// --- Captain Enums (M10 P10) ---
type AssistantStatus string
const (
AssistantStatusActive AssistantStatus = "active"
AssistantStatusDraft AssistantStatus = "draft"
AssistantStatusArchived AssistantStatus = "archived"
)
type DocumentStatus string
const (
DocumentStatusPending DocumentStatus = "pending"
DocumentStatusInProgress DocumentStatus = "in_progress"
DocumentStatusCompleted DocumentStatus = "completed"
DocumentStatusFailed DocumentStatus = "failed"
)
type DocumentSyncStatus string
const (
DocumentSyncStatusPending DocumentSyncStatus = "pending"
DocumentSyncStatusSynced DocumentSyncStatus = "synced"
DocumentSyncStatusStale DocumentSyncStatus = "stale"
DocumentSyncStatusFailed DocumentSyncStatus = "failed"
)
type ResponseStatus string
const (
ResponseStatusApproved ResponseStatus = "approved"
ResponseStatusPending ResponseStatus = "pending"
ResponseStatusRejected ResponseStatus = "rejected"
)
type CopilotMessageType string
const (
CopilotMessageTypeUser CopilotMessageType = "user"
CopilotMessageTypeAssistant CopilotMessageType = "assistant"
CopilotMessageTypeAssistantThinking CopilotMessageType = "assistant_thinking"
)
type ToolAuthType string
const (
ToolAuthTypeNone ToolAuthType = "none"
ToolAuthTypeBasic ToolAuthType = "basic"
ToolAuthTypeBearer ToolAuthType = "bearer"
ToolAuthTypeApiKey ToolAuthType = "api_key"
)
// --- Captain Assistant Model ---
// Reference: Chatwoot enterprise/app/models/captain/assistant.rb
// Table: captain_assistants
//
// Captain Assistant is the core orchestrator entity for AI features.
// It holds configuration (temperature, feature flags, product name),
// guardrails, and response_guidelines as JSONB.
// Associated with Account, has Documents, Responses, Scenarios, Inboxes (via CaptainInbox).
type CaptainAssistant struct {
Base
AccountID uint `gorm:"index;not null" json:"account_id"`
Name string `gorm:"size:255;not null" json:"name"`
Description string `gorm:"size:1024" json:"description"`
Config json.RawMessage `gorm:"type:jsonb;not null;default:'{}';serializer:json" json:"config"`
Guardrails json.RawMessage `gorm:"type:jsonb;serializer:json" json:"guardrails,omitempty"`
ResponseGuidelines json.RawMessage `gorm:"type:jsonb;serializer:json" json:"response_guidelines,omitempty"`
Status AssistantStatus `gorm:"size:50;default:active" json:"status"`
// Relationships (loaded via service layer or GORM preload)
Documents []CaptainDocument `gorm:"foreignKey:AssistantID" json:"documents,omitempty"`
Responses []CaptainAssistantResponse `gorm:"foreignKey:AssistantID" json:"responses,omitempty"`
Scenarios []CaptainScenario `gorm:"foreignKey:AssistantID" json:"scenarios,omitempty"`
CaptainInboxes []CaptainInbox `gorm:"foreignKey:AssistantID" json:"captain_inboxes,omitempty"`
CopilotThreads []CopilotThread `gorm:"foreignKey:AssistantID" json:"copilot_threads,omitempty"`
}
func (CaptainAssistant) TableName() string { return "captain_assistants" }
// PushEventData matches Chatwoot's public message-sender contract.
func (a *CaptainAssistant) PushEventData() map[string]any {
avatarURL := strings.TrimRight(os.Getenv("FRONTEND_URL"), "/") + "/assets/images/dashboard/captain/logo.svg"
return map[string]any{
"id": a.ID,
"name": a.Name,
"avatar_url": avatarURL,
"description": a.Description,
"created_at": a.CreatedAt,
"type": "captain_assistant",
}
}
// DefaultAssistantConfig returns the default configuration for a new Captain Assistant.
// Reference: Chatwoot Captain::Assistant default config values
func DefaultAssistantConfig() map[string]interface{} {
return map[string]interface{}{
"temperature": 0.7,
"product_name": "",
"feature_flags": map[string]bool{},
"response_guidelines": "",
}
}
// AssistantConfig provides typed access to JSONB config fields.
// Reference: Chatwoot store_accessor :config, :temperature, :feature_faq, etc.
type AssistantConfig struct {
Temperature float64 `json:"temperature,omitempty"`
TemperatureConfigured bool `json:"-"`
FeatureFAQ bool `json:"feature_faq,omitempty"`
FeatureMemory bool `json:"feature_memory,omitempty"`
FeatureCitation bool `json:"feature_citation,omitempty"`
FeatureContactAttributes bool `json:"feature_contact_attributes,omitempty"`
ProductName string `json:"product_name,omitempty"`
Instructions string `json:"instructions,omitempty"`
Model string `json:"model,omitempty"`
}
func (a *CaptainAssistant) GetConfig() (*AssistantConfig, error) {
var cfg AssistantConfig
if len(a.Config) == 0 || string(a.Config) == "{}" || string(a.Config) == "null" {
return &cfg, nil
}
if err := json.Unmarshal(a.Config, &cfg); err != nil {
// Return an empty config rather than nil so callers that ignore the
// error do not dereference a nil pointer. The raw JSON may contain
// type mismatches (e.g. temperature as string) from older data.
return &cfg, fmt.Errorf("parse assistant config: %w", err)
}
var fields map[string]json.RawMessage
if err := json.Unmarshal(a.Config, &fields); err == nil {
_, cfg.TemperatureConfigured = fields["temperature"]
}
return &cfg, nil
}
func (a *CaptainAssistant) SetConfig(cfg *AssistantConfig) error {
data, err := json.Marshal(cfg)
if err != nil {
return err
}
a.Config = data
return nil
}
// GetResponseGuidelines unmarshals the ResponseGuidelines JSON field.
// Returns nil and no error if the field is empty/null.
func (a *CaptainAssistant) GetResponseGuidelines() (string, error) {
if len(a.ResponseGuidelines) == 0 || string(a.ResponseGuidelines) == "{}" || string(a.ResponseGuidelines) == "null" {
return "", nil
}
// ResponseGuidelines may be a plain string or a JSON object
// Try unmarshaling as string first
var s string
if err := json.Unmarshal(a.ResponseGuidelines, &s); err == nil {
return s, nil
}
// Otherwise return the raw JSON as a string
return string(a.ResponseGuidelines), nil
}
// GetGuardrails unmarshals the Guardrails JSON field.
// Returns an empty string and no error if the field is empty/null.
func (a *CaptainAssistant) GetGuardrails() (string, error) {
if len(a.Guardrails) == 0 || string(a.Guardrails) == "{}" || string(a.Guardrails) == "null" {
return "", nil
}
var s string
if err := json.Unmarshal(a.Guardrails, &s); err == nil {
return s, nil
}
return string(a.Guardrails), nil
}
// --- Captain Document Model ---
// Reference: Chatwoot enterprise/app/models/captain/document.rb
// Table: captain_documents
//
// Captain Document represents a knowledge base entry (web page or PDF)
// linked to an Assistant. Contains content, sync tracking, metadata,
// and content_fingerprint for deduplication.
type CaptainDocument struct {
Base
AccountID uint `gorm:"index;not null" json:"account_id"`
AssistantID uint `gorm:"index;not null" json:"assistant_id"`
Name string `gorm:"size:255" json:"name"`
ExternalLink string `gorm:"size:2048" json:"external_link"`
Content string `gorm:"type:text" json:"content,omitempty"`
ContentFingerprint string `gorm:"size:64" json:"content_fingerprint,omitempty"`
Status DocumentStatus `gorm:"size:50;default:in_progress;not null" json:"status"`
SyncStatus DocumentSyncStatus `gorm:"size:50" json:"sync_status,omitempty"`
LastSyncedAt *int64 `json:"last_synced_at,omitempty"`
LastSyncAttemptedAt *int64 `json:"last_sync_attempted_at,omitempty"`
LastSyncErrorCode string `gorm:"size:50" json:"last_sync_error_code,omitempty"`
Metadata json.RawMessage `gorm:"type:jsonb;serializer:json" json:"metadata,omitempty"`
// File upload metadata (PDF support)
FileSize int64 `gorm:"default:0" json:"file_size,omitempty"`
ContentType string `gorm:"size:100" json:"content_type,omitempty"`
FileURL string `gorm:"size:2048" json:"file_url,omitempty"`
// Relationships
Assistant CaptainAssistant `gorm:"foreignKey:AssistantID" json:"assistant,omitempty"`
Responses []CaptainAssistantResponse `gorm:"foreignKey:DocumentableID" json:"responses,omitempty"`
}
func (CaptainDocument) TableName() string { return "captain_documents" }
// --- Captain AssistantResponse Model ---
// Reference: Chatwoot enterprise/app/models/captain/assistant_response.rb
// Table: captain_assistant_responses
//
// FAQ-style knowledge entry with question/answer pair.
// Has pgvector embedding (1536 dims) for similarity search.
// Polymorphic documentable association (can belong to Assistant or Document).
// Status workflow: approved/pending/rejected.
type CaptainAssistantResponse struct {
Base
AccountID uint `gorm:"index;not null" json:"account_id"`
AssistantID uint `gorm:"index;not null" json:"assistant_id"`
DocumentableID *uint `gorm:"index" json:"documentable_id,omitempty"`
DocumentableType string `gorm:"size:50" json:"documentable_type,omitempty"` // "CaptainAssistant" or "CaptainDocument"
Question string `gorm:"size:1024;not null" json:"question"`
Answer string `gorm:"type:text;not null" json:"answer"`
Status ResponseStatus `gorm:"size:50;default:approved;not null" json:"status"`
Edited bool `gorm:"default:false;not null" json:"edited"`
// pgvector-go Vector type for embeddings (dimension follows the configured embedding model)
Embedding pgvector.Vector `gorm:"type:vector" json:"embedding,omitempty"`
SourceURL string `gorm:"column:source_url;->;-:migration" json:"-"`
Assistant CaptainAssistant `gorm:"foreignKey:AssistantID" json:"assistant,omitempty"`
}
func (CaptainAssistantResponse) TableName() string { return "captain_assistant_responses" }
// --- Captain Scenario Model ---
// Reference: Chatwoot enterprise/app/models/captain/scenario.rb
// Table: captain_scenarios
//
// Scenario is a specialized agent configuration within an Assistant.
// It can be enabled/disabled, has its own instruction text,
// and declares which tools it uses (as JSONB array).
// Scenario acts as a "handoff" target in the multi-agent system.
type CaptainScenario struct {
Base
AccountID uint `gorm:"index;not null" json:"account_id"`
AssistantID uint `gorm:"index;not null" json:"assistant_id"`
Title string `gorm:"size:255" json:"title"`
Description string `gorm:"type:text" json:"description,omitempty"`
Instruction string `gorm:"type:text" json:"instruction,omitempty"`
Enabled bool `gorm:"default:true;not null;index" json:"enabled"`
Tools json.RawMessage `gorm:"type:jsonb;serializer:json" json:"tools,omitempty"` // array of tool names
Assistant CaptainAssistant `gorm:"foreignKey:AssistantID" json:"assistant,omitempty"`
}
func (CaptainScenario) TableName() string { return "captain_scenarios" }
// --- Captain CustomTool Model ---
// Reference: Chatwoot enterprise/app/models/captain/custom_tool.rb
// Table: captain_custom_tools
//
// User-defined HTTP tool with configurable endpoint, method,
// auth, request/response templates (Go template equivalent of Liquid),
// and parameter schema. Max 15 per account. Slug is unique per account.
type CaptainCustomTool struct {
Base
AccountID uint `gorm:"index;uniqueIndex:idx_captain_custom_tools_account_slug,priority:1;not null" json:"account_id"`
Title string `gorm:"size:255;not null" json:"title"`
Slug string `gorm:"size:64;not null;uniqueIndex:idx_captain_custom_tools_account_slug,priority:2" json:"slug"`
Description string `gorm:"type:text" json:"description,omitempty"`
EndpointURL string `gorm:"type:text;not null" json:"endpoint_url"`
HTTPMethod string `gorm:"size:10;default:GET;not null" json:"http_method"`
AuthType ToolAuthType `gorm:"size:20;default:none" json:"auth_type"`
AuthConfig json.RawMessage `gorm:"type:jsonb;serializer:json" json:"auth_config,omitempty"`
ParamSchema json.RawMessage `gorm:"type:jsonb;serializer:json" json:"param_schema,omitempty"`
RequestTemplate string `gorm:"type:text" json:"request_template,omitempty"`
ResponseTemplate string `gorm:"type:text" json:"response_template,omitempty"`
Enabled bool `gorm:"default:true;not null" json:"enabled"`
}
func (CaptainCustomTool) TableName() string { return "captain_custom_tools" }
// --- Captain Inbox (join table) ---
// Reference: Chatwoot CaptainInbox model
// Table: captain_inboxes
//
// Links a Captain Assistant to an Inbox, enabling the assistant
// to operate on conversations in that inbox.
type CaptainInbox struct {
Base
AssistantID uint `gorm:"column:captain_assistant_id;index;not null" json:"captain_assistant_id"`
InboxID uint `gorm:"index;not null;uniqueIndex:idx_captain_inboxes_active_inbox,where:deleted_at IS NULL" json:"inbox_id"`
AccountID uint `gorm:"index;not null" json:"account_id"`
}
func (CaptainInbox) TableName() string { return "captain_inboxes" }
// --- Captain Preference ---
// Reference: Chatwoot enterprise/app/models/captain/preference.rb
// Table: captain_preferences
//
// Per-account AI configuration: tone, language, response guidelines,
// and feature toggles. Each account has at most one preference record.
type CaptainPreference struct {
Base
AccountID uint `gorm:"uniqueIndex;not null" json:"account_id"`
Tone string `gorm:"size:50;default:professional" json:"tone"` // professional, friendly, casual, formal
Language string `gorm:"size:10;default:en" json:"language"` // ISO 639-1 language code
ResponseGuidelines string `gorm:"type:text" json:"response_guidelines,omitempty"` // free-form instructions for AI responses
AutoLabelEnabled bool `gorm:"default:false" json:"auto_label_enabled"` // auto-apply label suggestions
AutoFollowUpEnabled bool `gorm:"default:false" json:"auto_follow_up_enabled"` // auto-create follow-up tasks
AutoReplyEnabled bool `gorm:"default:false" json:"auto_reply_enabled"` // auto-send assistant responses
MaxResponseLength int `gorm:"default:500" json:"max_response_length"` // max characters in AI response
CustomPromptSuffix string `gorm:"type:text" json:"custom_prompt_suffix,omitempty"` // appended to system prompts
}
func (CaptainPreference) TableName() string { return "captain_preferences" }