package models import ( "database/sql" "encoding/json" "time" ) // ── Base ──────────────────────────────────── type BaseModel struct { ID string `json:"id" db:"id"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } // ── Scope Constants ───────────────────────── const ( ScopeGlobal = "global" ScopeTeam = "team" ScopePersonal = "personal" ) // ── Visibility Constants ──────────────────── const ( VisibilityVisible = "visible" VisibilityHidden = "hidden" ) // ── Role Constants ────────────────────────── const ( UserRoleUser = "user" UserRoleAdmin = "admin" TeamRoleAdmin = "admin" TeamRoleMember = "member" ) // ── Grant Type Constants ──────────────────── const ( GrantTypeTool = "tool" GrantTypeKnowledgeBase = "knowledge_base" GrantTypeAPIEndpoint = "api_endpoint" ) // ── Channel Type Constants ────────────────── const ( ChannelTypeDirect = "direct" ChannelTypeGroup = "group" ChannelTypeChannel = "channel" ) // ── Message Role Constants ────────────────── const ( MessageRoleUser = "user" MessageRoleAssistant = "assistant" MessageRoleSystem = "system" MessageRoleTool = "tool" ) // ========================================= // USERS // ========================================= type User struct { BaseModel Username string `json:"username" db:"username"` Email string `json:"email" db:"email"` PasswordHash string `json:"-" db:"password_hash"` DisplayName string `json:"display_name,omitempty" db:"display_name"` AvatarURL string `json:"avatar_url,omitempty" db:"avatar_url"` Role string `json:"role" db:"role"` IsActive bool `json:"is_active" db:"is_active"` Settings JSONMap `json:"settings,omitempty" db:"settings"` LastLoginAt *time.Time `json:"last_login_at,omitempty" db:"last_login_at"` } // ========================================= // TEAMS // ========================================= type Team struct { BaseModel Name string `json:"name" db:"name"` Description string `json:"description,omitempty" db:"description"` CreatedBy string `json:"created_by" db:"created_by"` IsActive bool `json:"is_active" db:"is_active"` Settings JSONMap `json:"settings,omitempty" db:"settings"` MemberCount int `json:"member_count,omitempty"` // computed } type TeamMember struct { ID string `json:"id" db:"id"` TeamID string `json:"team_id" db:"team_id"` UserID string `json:"user_id" db:"user_id"` Role string `json:"role" db:"role"` JoinedAt string `json:"joined_at" db:"joined_at"` // Joined fields from users table Email string `json:"email,omitempty"` DisplayName string `json:"display_name,omitempty"` Username string `json:"username,omitempty"` UserRole string `json:"user_role,omitempty"` } // ========================================= // PROVIDER CONFIGS (replaces APIConfig) // ========================================= type ProviderConfig struct { BaseModel Scope string `json:"scope" db:"scope"` OwnerID *string `json:"owner_id,omitempty" db:"owner_id"` Name string `json:"name" db:"name"` Provider string `json:"provider" db:"provider"` Endpoint string `json:"endpoint" db:"endpoint"` APIKeyEnc []byte `json:"-" db:"api_key_enc"` KeyNonce []byte `json:"-" db:"key_nonce"` KeyScope string `json:"-" db:"key_scope"` ModelDefault string `json:"model_default,omitempty" db:"model_default"` Config JSONMap `json:"config,omitempty" db:"config"` Headers JSONMap `json:"headers,omitempty" db:"headers"` Settings JSONMap `json:"settings,omitempty" db:"settings"` IsActive bool `json:"is_active" db:"is_active"` IsPrivate bool `json:"is_private" db:"is_private"` } // HasKey returns true if an encrypted API key is stored. func (p *ProviderConfig) HasKey() bool { return len(p.APIKeyEnc) > 0 } type ProviderConfigPatch struct { Name *string `json:"name,omitempty"` Endpoint *string `json:"endpoint,omitempty"` APIKeyEnc []byte `json:"-"` KeyNonce []byte `json:"-"` ModelDefault *string `json:"model_default,omitempty"` Config JSONMap `json:"config,omitempty"` Headers JSONMap `json:"headers,omitempty"` Settings JSONMap `json:"settings,omitempty"` IsActive *bool `json:"is_active,omitempty"` IsPrivate *bool `json:"is_private,omitempty"` } // ========================================= // MODEL CATALOG (replaces model_configs) // ========================================= type CatalogEntry struct { BaseModel ProviderConfigID string `json:"provider_config_id" db:"provider_config_id"` ModelID string `json:"model_id" db:"model_id"` DisplayName string `json:"display_name,omitempty" db:"display_name"` ModelType string `json:"model_type" db:"model_type"` // "chat", "embedding", "image", etc. Capabilities ModelCapabilities `json:"capabilities" db:"capabilities"` Pricing *ModelPricing `json:"pricing,omitempty" db:"pricing"` Visibility string `json:"visibility" db:"visibility"` LastSyncedAt *time.Time `json:"last_synced_at,omitempty" db:"last_synced_at"` } type ModelCapabilities struct { Streaming bool `json:"streaming"` ToolCalling bool `json:"tool_calling"` Vision bool `json:"vision"` Thinking bool `json:"thinking"` Reasoning bool `json:"reasoning"` CodeOptimized bool `json:"code_optimized"` WebSearch bool `json:"web_search"` MaxContext int `json:"max_context"` MaxOutputTokens int `json:"max_output_tokens"` } func (c ModelCapabilities) HasProviderData() bool { return c.ToolCalling || c.Vision || c.Thinking || c.Reasoning || c.CodeOptimized || c.WebSearch || c.MaxContext > 0 || c.MaxOutputTokens > 0 } type ModelPricing struct { InputPerM float64 `json:"input_per_m,omitempty"` OutputPerM float64 `json:"output_per_m,omitempty"` Currency string `json:"currency,omitempty"` } // ========================================= // PERSONAS (replaces ModelPreset) // ========================================= type Persona struct { BaseModel Name string `json:"name" db:"name"` Description string `json:"description,omitempty" db:"description"` Icon string `json:"icon,omitempty" db:"icon"` Avatar string `json:"avatar,omitempty" db:"avatar"` BaseModelID string `json:"base_model_id" db:"base_model_id"` ProviderConfigID *string `json:"provider_config_id,omitempty" db:"provider_config_id"` SystemPrompt string `json:"system_prompt,omitempty" db:"system_prompt"` Temperature *float64 `json:"temperature,omitempty" db:"temperature"` MaxTokens *int `json:"max_tokens,omitempty" db:"max_tokens"` ThinkingBudget *int `json:"thinking_budget,omitempty" db:"thinking_budget"` TopP *float64 `json:"top_p,omitempty" db:"top_p"` Scope string `json:"scope" db:"scope"` OwnerID *string `json:"owner_id,omitempty" db:"owner_id"` CreatedBy string `json:"created_by" db:"created_by"` IsActive bool `json:"is_active" db:"is_active"` IsShared bool `json:"is_shared" db:"is_shared"` // Loaded from persona_grants, not stored in personas table Grants []Grant `json:"grants,omitempty" db:"-"` // Loaded from persona_knowledge_bases, not stored in personas table KBIDs []string `json:"kb_ids,omitempty" db:"-"` // Memory configuration (v0.18.0) MemoryEnabled bool `json:"memory_enabled" db:"memory_enabled"` MemoryExtractionPrompt *string `json:"memory_extraction_prompt,omitempty" db:"memory_extraction_prompt"` } type PersonaPatch struct { Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` Icon *string `json:"icon,omitempty"` Avatar *string `json:"avatar,omitempty"` BaseModelID *string `json:"base_model_id,omitempty"` ProviderConfigID *string `json:"provider_config_id,omitempty"` SystemPrompt *string `json:"system_prompt,omitempty"` Temperature *float64 `json:"temperature,omitempty"` MaxTokens *int `json:"max_tokens,omitempty"` ThinkingBudget *int `json:"thinking_budget,omitempty"` TopP *float64 `json:"top_p,omitempty"` IsActive *bool `json:"is_active,omitempty"` IsShared *bool `json:"is_shared,omitempty"` MemoryEnabled *bool `json:"memory_enabled,omitempty"` MemoryExtractionPrompt *string `json:"memory_extraction_prompt,omitempty"` } // ========================================= // GRANTS // ========================================= type Grant struct { ID string `json:"id" db:"id"` PersonaID string `json:"persona_id" db:"persona_id"` GrantType string `json:"grant_type" db:"grant_type"` GrantRef string `json:"grant_ref" db:"grant_ref"` Config JSONMap `json:"config,omitempty" db:"config"` CreatedAt time.Time `json:"created_at" db:"created_at"` } // ========================================= // PLATFORM POLICIES // ========================================= var PolicyDefaults = map[string]string{ "allow_user_byok": "false", "allow_user_personas": "false", "allow_raw_model_access": "false", "allow_registration": "true", "default_user_active": "false", "allow_team_providers": "true", "default_model": "", } // ========================================= // USER MODEL SETTINGS // ========================================= type UserModelSetting struct { BaseModel UserID string `json:"user_id" db:"user_id"` ModelID string `json:"model_id" db:"model_id"` Hidden bool `json:"hidden" db:"hidden"` PreferredTemperature *float64 `json:"preferred_temperature,omitempty" db:"preferred_temperature"` PreferredMaxTokens *int `json:"preferred_max_tokens,omitempty" db:"preferred_max_tokens"` SortOrder int `json:"sort_order" db:"sort_order"` } type UserModelSettingPatch struct { Hidden *bool `json:"hidden,omitempty"` PreferredTemperature *float64 `json:"preferred_temperature,omitempty"` PreferredMaxTokens *int `json:"preferred_max_tokens,omitempty"` SortOrder *int `json:"sort_order,omitempty"` } // ========================================= // CHANNELS // ========================================= type Channel struct { BaseModel UserID string `json:"user_id" db:"user_id"` Title string `json:"title" db:"title"` Description string `json:"description,omitempty" db:"description"` Type string `json:"type" db:"type"` Model string `json:"model,omitempty" db:"model"` SystemPrompt string `json:"system_prompt,omitempty" db:"system_prompt"` ProviderConfigID *string `json:"provider_config_id,omitempty" db:"provider_config_id"` IsArchived bool `json:"is_archived" db:"is_archived"` IsPinned bool `json:"is_pinned" db:"is_pinned"` FolderID *string `json:"folder_id,omitempty" db:"folder_id"` TeamID *string `json:"team_id,omitempty" db:"team_id"` ProjectID *string `json:"project_id,omitempty" db:"project_id"` WorkspaceID *string `json:"workspace_id,omitempty" db:"workspace_id"` Settings JSONMap `json:"settings,omitempty" db:"settings"` } // ========================================= // MESSAGES // ========================================= type Message struct { BaseModel ChannelID string `json:"channel_id" db:"channel_id"` Role string `json:"role" db:"role"` Content string `json:"content" db:"content"` Model string `json:"model,omitempty" db:"model"` TokensUsed int `json:"tokens_used,omitempty" db:"tokens_used"` ToolCalls JSONMap `json:"tool_calls,omitempty" db:"tool_calls"` Metadata JSONMap `json:"metadata,omitempty" db:"metadata"` ParentID *string `json:"parent_id,omitempty" db:"parent_id"` SiblingIndex int `json:"sibling_index" db:"sibling_index"` ParticipantType string `json:"participant_type,omitempty" db:"participant_type"` ParticipantID string `json:"participant_id,omitempty" db:"participant_id"` DeletedAt *time.Time `json:"deleted_at,omitempty" db:"deleted_at"` } // ========================================= // CHANNEL MEMBERS, MODELS, CURSORS // ========================================= type ChannelMember struct { ID string `json:"id" db:"id"` ChannelID string `json:"channel_id" db:"channel_id"` UserID string `json:"user_id" db:"user_id"` Role string `json:"role" db:"role"` JoinedAt time.Time `json:"joined_at" db:"joined_at"` LastReadAt time.Time `json:"last_read_at" db:"last_read_at"` } type ChannelModel struct { ID string `json:"id" db:"id"` ChannelID string `json:"channel_id" db:"channel_id"` ModelID string `json:"model_id" db:"model_id"` ProviderConfigID string `json:"provider_config_id,omitempty" db:"provider_config_id"` DisplayName string `json:"display_name,omitempty" db:"display_name"` SystemPrompt string `json:"system_prompt,omitempty" db:"system_prompt"` IsDefault bool `json:"is_default" db:"is_default"` } type ChannelCursor struct { ID string `json:"id" db:"id"` ChannelID string `json:"channel_id" db:"channel_id"` UserID string `json:"user_id" db:"user_id"` ActiveLeafID *string `json:"active_leaf_id,omitempty" db:"active_leaf_id"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } // ========================================= // ORGANIZATION // ========================================= type Folder struct { BaseModel UserID string `json:"user_id" db:"user_id"` Name string `json:"name" db:"name"` ParentID *string `json:"parent_id,omitempty" db:"parent_id"` SortOrder int `json:"sort_order" db:"sort_order"` } type Project struct { BaseModel Name string `json:"name" db:"name"` Description string `json:"description,omitempty" db:"description"` Color *string `json:"color,omitempty" db:"color"` Icon *string `json:"icon,omitempty" db:"icon"` Scope string `json:"scope" db:"scope"` // personal, team, global OwnerID string `json:"owner_id" db:"owner_id"` TeamID *string `json:"team_id,omitempty" db:"team_id"` IsArchived bool `json:"is_archived" db:"is_archived"` WorkspaceID *string `json:"workspace_id,omitempty" db:"workspace_id"` Settings JSONMap `json:"settings,omitempty" db:"settings"` // Computed fields (not DB columns) ChannelCount int `json:"channel_count,omitempty"` KBCount int `json:"kb_count,omitempty"` NoteCount int `json:"note_count,omitempty"` } // ProjectPatch holds optional fields for updating a project. type ProjectPatch struct { Name *string `json:"name,omitempty"` Description *string `json:"description,omitempty"` Color *string `json:"color,omitempty"` Icon *string `json:"icon,omitempty"` IsArchived *bool `json:"is_archived,omitempty"` WorkspaceID *string `json:"workspace_id,omitempty"` Settings JSONMap `json:"settings,omitempty"` } // ProjectChannel represents a channel's membership in a project. type ProjectChannel struct { ProjectID string `json:"project_id" db:"project_id"` ChannelID string `json:"channel_id" db:"channel_id"` Position int `json:"position" db:"position"` Folder string `json:"folder,omitempty" db:"folder"` AddedAt string `json:"added_at" db:"added_at"` } // ProjectKB represents a KB's association with a project. type ProjectKB struct { ProjectID string `json:"project_id" db:"project_id"` KBID string `json:"kb_id" db:"kb_id"` AutoSearch bool `json:"auto_search" db:"auto_search"` AddedAt string `json:"added_at" db:"added_at"` Name string `json:"name,omitempty" db:"name"` // enriched via JOIN } // ProjectNote represents a note's association with a project. type ProjectNote struct { ProjectID string `json:"project_id" db:"project_id"` NoteID string `json:"note_id" db:"note_id"` AddedAt string `json:"added_at" db:"added_at"` Title string `json:"title,omitempty" db:"title"` // enriched via JOIN } // ========================================= // NOTES // ========================================= type Note struct { BaseModel UserID string `json:"user_id" db:"user_id"` Title string `json:"title" db:"title"` Content string `json:"content" db:"content"` FolderPath string `json:"folder_path" db:"folder_path"` Tags []string `json:"tags,omitempty" db:"tags"` Metadata JSONMap `json:"metadata,omitempty" db:"metadata"` SourceChannelID *string `json:"source_channel_id,omitempty" db:"source_channel_id"` SourceMessageID *string `json:"source_message_id,omitempty" db:"source_message_id"` TeamID *string `json:"team_id,omitempty" db:"team_id"` } // NoteLink represents a directed link extracted from [[wikilink]] syntax. type NoteLink struct { TargetNoteID *string `json:"target_note_id,omitempty"` TargetTitle string `json:"target_title"` DisplayText string `json:"display_text,omitempty"` IsTransclusion bool `json:"is_transclusion"` } // NoteLinkResult represents a backlink — a note that links to a given note. type NoteLinkResult struct { SourceNoteID string `json:"id"` Title string `json:"title"` FolderPath string `json:"folder_path"` UpdatedAt time.Time `json:"updated_at"` DisplayText string `json:"display_text,omitempty"` } // NoteGraphNode is a lightweight note representation for graph display. type NoteGraphNode struct { ID string `json:"id"` Title string `json:"title"` FolderPath string `json:"folder_path"` Tags []string `json:"tags"` UpdatedAt string `json:"updated_at"` LinkCount int `json:"link_count"` } // NoteGraphEdge is a resolved link between two notes. type NoteGraphEdge struct { Source string `json:"source"` Target string `json:"target"` Title string `json:"title"` IsTransclusion bool `json:"is_transclusion"` } // NoteGraphDangling is an unresolved [[link]] reference. type NoteGraphDangling struct { Source string `json:"source"` Title string `json:"title"` } // NoteGraph is the full graph topology for a user's notes. type NoteGraph struct { Nodes []NoteGraphNode `json:"nodes"` Edges []NoteGraphEdge `json:"edges"` Unresolved []NoteGraphDangling `json:"unresolved"` } // ========================================= // ATTACHMENTS // ========================================= type Attachment struct { ID string `json:"id" db:"id"` ChannelID string `json:"channel_id" db:"channel_id"` UserID string `json:"user_id" db:"user_id"` MessageID *string `json:"message_id,omitempty" db:"message_id"` Filename string `json:"filename" db:"filename"` ContentType string `json:"content_type" db:"content_type"` SizeBytes int64 `json:"size_bytes" db:"size_bytes"` StorageKey string `json:"-" db:"storage_key"` // never expose filesystem path ExtractedText *string `json:"extracted_text,omitempty" db:"extracted_text"` Metadata JSONMap `json:"metadata,omitempty" db:"metadata"` CreatedAt time.Time `json:"created_at" db:"created_at"` } // ========================================= // AUDIT LOG // ========================================= type AuditEntry struct { ID string `json:"id" db:"id"` ActorID *string `json:"actor_id,omitempty" db:"actor_id"` Action string `json:"action" db:"action"` ResourceType string `json:"resource_type" db:"resource_type"` ResourceID string `json:"resource_id,omitempty" db:"resource_id"` Metadata JSONMap `json:"metadata,omitempty" db:"metadata"` IPAddress string `json:"ip_address,omitempty" db:"ip_address"` UserAgent string `json:"user_agent,omitempty" db:"user_agent"` CreatedAt time.Time `json:"created_at" db:"created_at"` } // ========================================= // USAGE TRACKING // ========================================= type UsageEntry struct { ID string `json:"id" db:"id"` ChannelID *string `json:"channel_id,omitempty" db:"channel_id"` UserID string `json:"user_id" db:"user_id"` ProviderConfigID *string `json:"provider_config_id,omitempty" db:"provider_config_id"` ProviderScope string `json:"provider_scope" db:"provider_scope"` ModelID string `json:"model_id" db:"model_id"` Role *string `json:"role,omitempty" db:"role"` // null=chat, "utility", "embedding" PromptTokens int `json:"prompt_tokens" db:"prompt_tokens"` CompletionTokens int `json:"completion_tokens" db:"completion_tokens"` CacheCreationTokens int `json:"cache_creation_tokens" db:"cache_creation_tokens"` CacheReadTokens int `json:"cache_read_tokens" db:"cache_read_tokens"` CostInput *float64 `json:"cost_input,omitempty" db:"cost_input"` CostOutput *float64 `json:"cost_output,omitempty" db:"cost_output"` RoutingDecision JSONMap `json:"routing_decision,omitempty" db:"routing_decision"` CreatedAt time.Time `json:"created_at" db:"created_at"` } type UsageAggregate struct { GroupKey string `json:"group_key"` Label string `json:"label"` Requests int `json:"requests"` InputTokens int `json:"input_tokens"` OutputTokens int `json:"output_tokens"` TotalCost float64 `json:"total_cost"` } type UsageTotals struct { Requests int `json:"requests"` InputTokens int `json:"input_tokens"` OutputTokens int `json:"output_tokens"` TotalCost float64 `json:"total_cost"` Period string `json:"period"` } // ========================================= // MODEL PRICING // ========================================= type PricingEntry struct { ID string `json:"id" db:"id"` ProviderConfigID string `json:"provider_config_id" db:"provider_config_id"` ModelID string `json:"model_id" db:"model_id"` InputPerM *float64 `json:"input_per_m" db:"input_per_m"` OutputPerM *float64 `json:"output_per_m" db:"output_per_m"` CacheCreatePerM *float64 `json:"cache_create_per_m" db:"cache_create_per_m"` CacheReadPerM *float64 `json:"cache_read_per_m" db:"cache_read_per_m"` Currency string `json:"currency" db:"currency"` Source string `json:"source" db:"source"` // "catalog" | "manual" UpdatedAt time.Time `json:"updated_at" db:"updated_at"` UpdatedBy *string `json:"updated_by,omitempty" db:"updated_by"` } // ========================================= // VIEW MODELS (computed, not stored) // ========================================= // UserModel is the view model returned by the capability resolver. // Combines catalog entries + Personas for the frontend. type UserModel struct { ID string `json:"id"` DisplayName string `json:"display_name"` ModelID string `json:"model_id"` ModelType string `json:"model_type"` // "chat", "embedding", "image", etc. Source string `json:"source"` // "catalog", "persona", "live" ProviderConfigID string `json:"provider_config_id"` ConfigID string `json:"config_id"` // Alias of ProviderConfigID for frontend compat ProviderName string `json:"provider_name"` ProviderType string `json:"provider_type"` Capabilities ModelCapabilities `json:"capabilities"` // Preset fields — always emitted so frontend can branch on is_preset. IsPreset bool `json:"is_preset"` PresetID string `json:"preset_id,omitempty"` PresetScope string `json:"preset_scope,omitempty"` PresetAvatar string `json:"preset_avatar,omitempty"` PresetTeamName string `json:"preset_team_name,omitempty"` PersonaID string `json:"persona_id,omitempty"` Description string `json:"description,omitempty"` Icon string `json:"icon,omitempty"` Avatar string `json:"avatar,omitempty"` SystemPrompt string `json:"system_prompt,omitempty"` Temperature *float64 `json:"temperature,omitempty"` MaxTokens *int `json:"max_tokens,omitempty"` ToolGrants []string `json:"tool_grants,omitempty"` Pricing *ModelPricing `json:"pricing,omitempty"` Scope string `json:"scope"` OwnerID *string `json:"owner_id,omitempty"` TeamName string `json:"team_name,omitempty"` Hidden bool `json:"hidden"` SortOrder int `json:"sort_order"` ProviderStatus ProviderStatus `json:"provider_status,omitempty"` // health status from provider health (v0.22.3) } // ========================================= // JSON HELPERS // ========================================= // JSONMap scans from/to JSONB columns. type JSONMap map[string]interface{} func (m *JSONMap) Scan(src interface{}) error { if src == nil { *m = nil return nil } var source []byte switch v := src.(type) { case []byte: source = v case string: source = []byte(v) default: return nil } result := make(JSONMap) if err := json.Unmarshal(source, &result); err != nil { return err } *m = result return nil } // ── Extensions ────────────────────────────── // Extension tier constants const ( ExtTierBrowser = "browser" ExtTierStarlark = "starlark" ExtTierSidecar = "sidecar" ) // Extension represents an installed extension in the registry. type Extension struct { ID string `json:"id" db:"id"` ExtID string `json:"ext_id" db:"ext_id"` // manifest id Name string `json:"name" db:"name"` Version string `json:"version" db:"version"` Tier string `json:"tier" db:"tier"` Description string `json:"description" db:"description"` Author string `json:"author" db:"author"` Manifest json.RawMessage `json:"manifest" db:"manifest"` IsSystem bool `json:"is_system" db:"is_system"` IsEnabled bool `json:"is_enabled" db:"is_enabled"` Scope string `json:"scope" db:"scope"` TeamID *string `json:"team_id,omitempty" db:"team_id"` InstalledBy *string `json:"installed_by,omitempty" db:"installed_by"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } // ExtensionUserSettings stores per-user overrides for an extension. type ExtensionUserSettings struct { ExtensionID string `json:"extension_id" db:"extension_id"` UserID string `json:"user_id" db:"user_id"` Settings json.RawMessage `json:"settings" db:"settings"` IsEnabled bool `json:"is_enabled" db:"is_enabled"` } // UserExtension combines extension info with user-specific settings for API responses. type UserExtension struct { Extension UserEnabled *bool `json:"user_enabled,omitempty"` UserSettings *json.RawMessage `json:"user_settings,omitempty"` } func NullString(s *string) sql.NullString { if s == nil { return sql.NullString{} } return sql.NullString{String: *s, Valid: true} } func NullFloat(f *float64) sql.NullFloat64 { if f == nil { return sql.NullFloat64{} } return sql.NullFloat64{Float64: *f, Valid: true} } func NullInt(i *int) sql.NullInt64 { if i == nil { return sql.NullInt64{} } return sql.NullInt64{Int64: int64(*i), Valid: true} } func StringPtr(s string) *string { return &s } func Float64Ptr(f float64) *float64 { return &f } func IntPtr(i int) *int { return &i } func BoolPtr(b bool) *bool { return &b } // ========================================= // GROUPS (v0.16.0) // ========================================= // Group scope constants (reuses ScopeGlobal, ScopeTeam from above) // ResourceType constants for resource_grants const ( ResourceTypePersona = "persona" ResourceTypeKnowledgeBase = "knowledge_base" ) // GrantScope constants for resource_grants const ( GrantScopeTeamOnly = "team_only" GrantScopeGlobal = "global" GrantScopeGroups = "groups" ) // Group is an access-control list. Decouples resource visibility from teams. type Group struct { BaseModel Name string `json:"name" db:"name"` Description string `json:"description" db:"description"` Scope string `json:"scope" db:"scope"` // global, team TeamID *string `json:"team_id,omitempty" db:"team_id"` CreatedBy string `json:"created_by" db:"created_by"` MemberCount int `json:"member_count,omitempty"` // computed, not a DB column } // GroupMember links a user to a group. type GroupMember struct { ID string `json:"id" db:"id"` GroupID string `json:"group_id" db:"group_id"` UserID string `json:"user_id" db:"user_id"` AddedBy string `json:"added_by" db:"added_by"` AddedAt time.Time `json:"added_at" db:"added_at"` // Joined fields from users table (for list responses) Username string `json:"username,omitempty"` Email string `json:"email,omitempty"` DisplayName string `json:"display_name,omitempty"` } // ResourceGrant controls who can USE a resource (personas, KBs) via groups. // One row per resource. Not to be confused with persona_grants (what a Persona can DO). type ResourceGrant struct { BaseModel ResourceType string `json:"resource_type" db:"resource_type"` ResourceID string `json:"resource_id" db:"resource_id"` GrantScope string `json:"grant_scope" db:"grant_scope"` GrantedGroups []string `json:"granted_groups" db:"granted_groups"` // UUID array CreatedBy string `json:"created_by" db:"created_by"` } // ── Knowledge Bases ──────────────────────────── // KnowledgeBase is a named collection of documents with vector embeddings. type KnowledgeBase struct { ID string `json:"id" db:"id"` Name string `json:"name" db:"name"` Description string `json:"description" db:"description"` Scope string `json:"scope" db:"scope"` // global, team, personal OwnerID *string `json:"owner_id,omitempty" db:"owner_id"` TeamID *string `json:"team_id,omitempty" db:"team_id"` EmbeddingConfig JSONMap `json:"embedding_config" db:"embedding_config"` DocumentCount int `json:"document_count" db:"document_count"` ChunkCount int `json:"chunk_count" db:"chunk_count"` TotalBytes int64 `json:"total_bytes" db:"total_bytes"` Discoverable bool `json:"discoverable" db:"discoverable"` Status string `json:"status" db:"status"` // active, processing, error CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } // PersonaKB binds a knowledge base to a persona. type PersonaKB struct { PersonaID string `json:"persona_id" db:"persona_id"` KBID string `json:"kb_id" db:"kb_id"` AutoSearch bool `json:"auto_search" db:"auto_search"` AddedAt time.Time `json:"added_at" db:"added_at"` // Joined fields (not in persona_knowledge_bases table) KBName string `json:"kb_name,omitempty" db:"kb_name"` DocumentCount int `json:"document_count,omitempty" db:"document_count"` ChunkCount int `json:"chunk_count,omitempty" db:"chunk_count"` } // KBDocument is a single uploaded file within a knowledge base. type KBDocument struct { ID string `json:"id" db:"id"` KBID string `json:"kb_id" db:"kb_id"` Filename string `json:"filename" db:"filename"` ContentType string `json:"content_type" db:"content_type"` SizeBytes int64 `json:"size_bytes" db:"size_bytes"` StorageKey string `json:"storage_key" db:"storage_key"` ExtractedText *string `json:"-" db:"extracted_text"` ChunkCount int `json:"chunk_count" db:"chunk_count"` Status string `json:"status" db:"status"` // pending, chunking, embedding, ready, error Error *string `json:"error,omitempty" db:"error"` UploadedBy string `json:"uploaded_by" db:"uploaded_by"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` } // KBChunk is a text segment from a document with its embedding vector. type KBChunk struct { ID string `json:"id" db:"id"` KBID string `json:"kb_id" db:"kb_id"` DocumentID string `json:"document_id" db:"document_id"` ChunkIndex int `json:"chunk_index" db:"chunk_index"` Content string `json:"content" db:"content"` TokenCount int `json:"token_count" db:"token_count"` Embedding []float64 `json:"-" db:"embedding"` // never serialized to API Metadata JSONMap `json:"metadata" db:"metadata"` CreatedAt time.Time `json:"created_at" db:"created_at"` } // KBSearchResult is a single result from similarity search. type KBSearchResult struct { Content string `json:"content"` Filename string `json:"source"` KBName string `json:"kb"` Similarity float64 `json:"similarity"` Metadata JSONMap `json:"metadata,omitempty"` } // ChannelKB represents a knowledge base linked to a channel. type ChannelKB struct { KBID string `json:"kb_id" db:"kb_id"` KBName string `json:"kb_name" db:"name"` Enabled bool `json:"enabled" db:"enabled"` DocumentCount int `json:"document_count" db:"document_count"` } // ========================================= // PROVIDER HEALTH (v0.22.0) // ========================================= // ProviderStatus represents the derived health state. type ProviderStatus string const ( StatusHealthy ProviderStatus = "healthy" StatusDegraded ProviderStatus = "degraded" StatusDown ProviderStatus = "down" StatusUnknown ProviderStatus = "unknown" ) // ProviderHealthWindow is one hourly bucket of health metrics. type ProviderHealthWindow struct { ID string `json:"id" db:"id"` ProviderConfigID string `json:"provider_config_id" db:"provider_config_id"` WindowStart time.Time `json:"window_start" db:"window_start"` RequestCount int `json:"request_count" db:"request_count"` ErrorCount int `json:"error_count" db:"error_count"` TimeoutCount int `json:"timeout_count" db:"timeout_count"` TotalLatencyMs int64 `json:"total_latency_ms" db:"total_latency_ms"` MaxLatencyMs int `json:"max_latency_ms" db:"max_latency_ms"` LastError *string `json:"last_error,omitempty" db:"last_error"` LastErrorAt *string `json:"last_error_at,omitempty" db:"last_error_at"` } // AvgLatencyMs returns the average latency, or 0 if no requests. func (w *ProviderHealthWindow) AvgLatencyMs() int { if w.RequestCount == 0 { return 0 } return int(w.TotalLatencyMs / int64(w.RequestCount)) } // ErrorRate returns the error fraction, or 0 if no requests. func (w *ProviderHealthWindow) ErrorRate() float64 { if w.RequestCount == 0 { return 0 } return float64(w.ErrorCount) / float64(w.RequestCount) } // ProviderHealthSummary is the API response for a provider's current health. type ProviderHealthSummary struct { ProviderConfigID string `json:"provider_config_id"` ProviderName string `json:"provider_name,omitempty"` Status ProviderStatus `json:"status"` RequestCount int `json:"request_count"` // last hour ErrorRate float64 `json:"error_rate"` // last hour AvgLatencyMs int `json:"avg_latency_ms"` // last hour MaxLatencyMs int `json:"max_latency_ms"` // last hour LastError *string `json:"last_error,omitempty"` LastErrorAt *string `json:"last_error_at,omitempty"` } // ========================================= // CAPABILITY OVERRIDES (v0.22.0) // ========================================= // CapabilityOverride is an admin correction for a model's capabilities. type CapabilityOverride struct { ID string `json:"id" db:"id"` ProviderConfigID *string `json:"provider_config_id,omitempty" db:"provider_config_id"` ModelID string `json:"model_id" db:"model_id"` Field string `json:"field" db:"field"` Value string `json:"value" db:"value"` SetBy *string `json:"set_by,omitempty" db:"set_by"` CreatedAt string `json:"created_at" db:"created_at"` } // ========================================= // ROUTING POLICIES (v0.22.2) // ========================================= // RoutingPolicy is one routing rule that controls how requests are // dispatched to provider configs. Stored in the routing_policies table. type RoutingPolicy struct { ID string `json:"id" db:"id"` Name string `json:"name" db:"name"` Scope string `json:"scope" db:"scope"` // "global" or "team" TeamID *string `json:"team_id,omitempty" db:"team_id"` Priority int `json:"priority" db:"priority"` // lower = first Type string `json:"policy_type" db:"policy_type"` // provider_prefer, team_route, cost_limit, model_alias Config JSONMap `json:"config" db:"config"` // type-specific JSONB IsActive bool `json:"is_active" db:"is_active"` CreatedAt time.Time `json:"created_at" db:"created_at"` UpdatedAt time.Time `json:"updated_at" db:"updated_at"` }