package types import ( "database/sql/driver" "encoding/json" "strings" "time" "github.com/google/uuid" "gorm.io/gorm" ) // FallbackStrategy represents the fallback strategy type type FallbackStrategy string const ( FallbackStrategyFixed FallbackStrategy = "fixed" // Fixed response FallbackStrategyModel FallbackStrategy = "model" // Model fallback response ) // SummaryConfig represents the summary configuration for a session type SummaryConfig struct { // Max tokens MaxTokens int `json:"max_tokens"` // Repeat penalty RepeatPenalty float64 `json:"repeat_penalty"` // TopK TopK int `json:"top_k"` // TopP TopP float64 `json:"top_p"` // Frequency penalty FrequencyPenalty float64 `json:"frequency_penalty"` // Presence penalty PresencePenalty float64 `json:"presence_penalty"` // Prompt Prompt string `json:"prompt"` // Context template ContextTemplate string `json:"context_template"` // No match prefix NoMatchPrefix string `json:"no_match_prefix"` // Temperature Temperature float64 `json:"temperature"` // Seed Seed int `json:"seed"` // Max completion tokens MaxCompletionTokens int `json:"max_completion_tokens"` // Thinking - whether to enable thinking mode Thinking *bool `json:"thinking"` } // ContextCompressionStrategy represents the strategy for context compression type ContextCompressionStrategy string const ( // ContextCompressionSlidingWindow keeps the most recent N messages ContextCompressionSlidingWindow ContextCompressionStrategy = "sliding_window" // ContextCompressionSmart uses LLM to summarize old messages ContextCompressionSmart ContextCompressionStrategy = "smart" ) // ContextConfig configures LLM context management // This is separate from message storage and manages token limits type ContextConfig struct { // Maximum tokens allowed in LLM context MaxTokens int `json:"max_tokens"` // Compression strategy: "sliding_window" or "smart" CompressionStrategy ContextCompressionStrategy `json:"compression_strategy"` // For sliding_window: number of messages to keep // For smart: number of recent messages to keep uncompressed RecentMessageCount int `json:"recent_message_count"` // Summarize threshold: number of messages before summarization SummarizeThreshold int `json:"summarize_threshold"` } // Session represents the session type Session struct { // ID ID string `json:"id" gorm:"type:varchar(36);primaryKey"` // Title Title string `json:"title"` // Description Description string `json:"description"` // Workspace ID TenantID uint64 `json:"tenant_id" gorm:"index"` // UserID is the owner scope for this session. WeKnora user UUIDs, API // external-user principals, and embed visitor principals all use this column. UserID string `json:"user_id,omitempty" gorm:"type:varchar(512);index"` // IsPinned indicates whether the session is pinned in the list. IsPinned bool `json:"is_pinned" gorm:"default:false"` // PinnedAt records when the session was pinned; nil when not pinned. PinnedAt *time.Time `json:"pinned_at,omitempty"` // LastRequestState records the input-bar state used the last time this // session sent a question (agent, model, KB scope, web search, MCPs). // Persisted on every successful POST to /knowledge-chat or /agent-chat so // that reopening the session can restore the original request context to // the chat UI. Stored in the legacy sessions.agent_config JSONB column to // avoid a new migration; the shape used today is `SessionLastRequestState`. LastRequestState *SessionLastRequestState `json:"last_request_state,omitempty" gorm:"column:agent_config;type:jsonb"` // SandboxConfigID pins which sandbox config this session's CURRENT live // sandbox was created on. Empty means no live sandbox; // SandboxConfigIDGlobalDefault means the deployment-wide default config. // // This is an ephemeral pin that dies with the sandbox, not a permanent // owner: sessions outlive sandboxes by months, so treating it as // permanent would make "no session references this config" never true. SandboxConfigID string `json:"sandbox_config_id,omitempty" gorm:"type:varchar(36)"` // // Strategy configuration // KnowledgeBaseID string `json:"knowledge_base_id"` // 关联的知识库ID // MaxRounds int `json:"max_rounds"` // 多轮保持轮数 // EnableRewrite bool `json:"enable_rewrite"` // 多轮改写开关 // FallbackStrategy FallbackStrategy `json:"fallback_strategy"` // 兜底策略 // FallbackResponse string `json:"fallback_response"` // 固定回复内容 // EmbeddingTopK int `json:"embedding_top_k"` // 向量召回TopK // KeywordThreshold float64 `json:"keyword_threshold"` // 关键词召回阈值 // VectorThreshold float64 `json:"vector_threshold"` // 向量召回阈值 // RerankModelID string `json:"rerank_model_id"` // 排序模型ID // RerankTopK int `json:"rerank_top_k"` // 排序TopK // RerankThreshold float64 `json:"rerank_threshold"` // 排序阈值 // SummaryModelID string `json:"summary_model_id"` // 总结模型ID // SummaryParameters *SummaryConfig `json:"summary_parameters" gorm:"type:json"` // 总结模型参数 // AgentConfig *SessionAgentConfig `json:"agent_config" gorm:"type:jsonb"` // Agent 配置(会话级别,仅存储enabled和knowledge_bases) // ContextConfig *ContextConfig `json:"context_config" gorm:"type:jsonb"` // 上下文管理配置(可选) CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"` // IMPlatform is the originating IM platform (e.g. "feishu", "wecom") when // this session is bound to an IM channel. It is not stored on the sessions // table (it lives in im_channel_sessions) and is populated on read so the // Web console can classify a session's origin folder without a list query. IMPlatform string `json:"im_platform,omitempty" gorm:"-"` // Association relationship, not stored in the database Messages []Message `json:"-" gorm:"foreignKey:SessionID"` } func (s *Session) BeforeCreate(tx *gorm.DB) (err error) { s.ID = uuid.New().String() return nil } // SessionSourceAPI is the source filter that lists every session created via a // tenant API key across the whole tenant. It is an admin-only view: the service // layer requires Admin+ and drops the per-user owner scope so a tenant // Owner/admin can observe API-key traffic that is otherwise isolated per key. const SessionSourceAPI = "api" // SessionSourceWeb is the source filter for a user's own Web-console chats. const SessionSourceWeb = "web" // SessionListSourceRequiresAdmin reports whether a session-list source filter // exposes tenant-wide channel traffic (API / IM / embed) in the Web console. func SessionListSourceRequiresAdmin(source string) bool { src := strings.TrimSpace(source) if src == "" || strings.EqualFold(src, SessionSourceWeb) { return false } return true } // SessionRequiresAdminConsoleRead reports whether a session row is channel- // managed traffic that non-admin web users must not open from the console. func SessionRequiresAdminConsoleRead(s *Session, imPlatform string) bool { if s == nil { return false } if IsAPISessionOwnerID(s.UserID) { return true } if strings.HasPrefix(s.Description, EmbedSessionMarkerPrefix) || strings.HasPrefix(s.UserID, PrincipalEmbedSession+":") { return true } // Defence in depth for skill maintenance transcripts: the listing hides // them, and this keeps a leaked session id from being opened by a // non-admin who happens to own the row. if IsSkillMaintenanceDescription(s.Description) { return true } return strings.TrimSpace(imPlatform) != "" } // IsSkillMaintenanceDescription reports whether description is the reserved // prefix that hides skill-install sessions from the console list. func IsSkillMaintenanceDescription(description string) bool { return strings.HasPrefix(description, SkillMaintenanceSessionMarker) } // SanitizeClientSessionDescription keeps the skill-maintenance marker off // client-writable descriptions. A row that is already a maintenance session // keeps its stored description so a PUT cannot un-hide it; any other row // drops a planted marker rather than accepting it. func SanitizeClientSessionDescription(incoming, existing string) string { if IsSkillMaintenanceDescription(existing) { return existing } if IsSkillMaintenanceDescription(incoming) { return "" } return incoming } // SessionListQuery bundles the parameters for listing sessions. // UserID empty means "tenant-wide" (used by API-key callers / legacy rows). // Keyword matches title ILIKE '%keyword%'. // Source values: "web" (user chats, no IM/embed), "embed" / "embed:{channelID}", // "api" (all API-key sessions, Admin+ only), or an IM platform name // (e.g. "feishu", "wechat"). IM and embed sources are also Admin+ only. // AgentID currently only filters sessions that have an IM channel mapping. type SessionListQuery struct { TenantID uint64 UserID string Keyword string Source string AgentID string Page int PageSize int } // SessionListItem is a session row enriched with its IM origin (when any). // IM-related fields are populated from the im_channel_sessions table via LEFT JOIN // and are empty for Web-created sessions. type SessionListItem struct { Session IMPlatform string `json:"im_platform,omitempty" gorm:"column:im_platform"` IMChatID string `json:"im_chat_id,omitempty" gorm:"column:im_chat_id"` IMThreadID string `json:"im_thread_id,omitempty" gorm:"column:im_thread_id"` IMUserID string `json:"im_user_id,omitempty" gorm:"column:im_user_id"` IMAgentID string `json:"im_agent_id,omitempty" gorm:"column:im_agent_id"` IMChannelID string `json:"im_channel_id,omitempty" gorm:"column:im_channel_id"` } // StringArray represents a list of strings type StringArray []string // Value implements the driver.Valuer interface, used to convert StringArray to database value func (c StringArray) Value() (driver.Value, error) { return json.Marshal(c) } // Scan implements the sql.Scanner interface, used to convert database value to StringArray func (c *StringArray) Scan(value interface{}) error { if value == nil { return nil } b, ok := value.([]byte) if !ok { return nil } return json.Unmarshal(b, c) } // Value implements the driver.Valuer interface, used to convert SummaryConfig to database value func (c *SummaryConfig) Value() (driver.Value, error) { return json.Marshal(c) } // Scan implements the sql.Scanner interface, used to convert database value to SummaryConfig func (c *SummaryConfig) Scan(value interface{}) error { if value == nil { return nil } b, ok := value.([]byte) if !ok { return nil } return json.Unmarshal(b, c) } // SessionLastRequestState captures the user-facing input-bar state at the // time of the most recent QA request on a session. It is purely a UI memory // aid — none of the fields here drive backend behaviour. They are echoed back // to the frontend by GetSession so the chat input can restore the same agent, // model, KB scope, etc. the user had selected last time. type SessionLastRequestState struct { AgentID string `json:"agent_id,omitempty"` AgentEnabled bool `json:"agent_enabled"` ModelID string `json:"model_id,omitempty"` KnowledgeBaseIDs []string `json:"knowledge_base_ids,omitempty"` KnowledgeIDs []string `json:"knowledge_ids,omitempty"` TagIDs []string `json:"tag_ids,omitempty"` MCPServiceIDs []string `json:"mcp_service_ids,omitempty"` SkillNames []string `json:"skill_names,omitempty"` MentionedItems MentionedItems `json:"mentioned_items,omitempty"` WebSearchEnabled bool `json:"web_search_enabled"` } // Value implements driver.Valuer for SessionLastRequestState (JSONB). func (s *SessionLastRequestState) Value() (driver.Value, error) { if s == nil { return nil, nil } return json.Marshal(s) } // Scan implements sql.Scanner for SessionLastRequestState (JSONB). // Tolerates legacy values that may not match the current schema by silently // ignoring unmarshal errors — the stored row predates this struct. func (s *SessionLastRequestState) Scan(value interface{}) error { if value == nil { return nil } var b []byte switch v := value.(type) { case []byte: b = v case string: b = []byte(v) default: return nil } if len(b) == 0 { return nil } if err := json.Unmarshal(b, s); err != nil { // Tolerate legacy shapes from before this column was repurposed. return nil } return nil } // Value implements the driver.Valuer interface, used to convert ContextConfig to database value func (c *ContextConfig) Value() (driver.Value, error) { return json.Marshal(c) } // Scan implements the sql.Scanner interface, used to convert database value to ContextConfig func (c *ContextConfig) Scan(value interface{}) error { if value == nil { return nil } b, ok := value.([]byte) if !ok { return nil } return json.Unmarshal(b, c) }