package services import ( "crypto/sha256" "encoding/hex" "encoding/json" "strings" "time" "code.tczkiot.com/wlw/ai-agent/internal/models" "code.tczkiot.com/wlw/ai-agent/internal/pkg/dto" "code.tczkiot.com/wlw/ai-agent/internal/pkg/enums" "code.tczkiot.com/wlw/ai-agent/internal/pkg/errorsx" "code.tczkiot.com/wlw/ai-agent/internal/pkg/utils" "code.tczkiot.com/wlw/ai-agent/internal/repositories" "github.com/mlogclub/simple/sqls" "gorm.io/gorm" ) var AgentRevisionService = newAgentRevisionService() func newAgentRevisionService() *agentRevisionService { return &agentRevisionService{} } type agentRevisionService struct{} func (s *agentRevisionService) Get(id int64) *models.AgentRevision { if id <= 0 { return nil } return repositories.AgentRevisionRepository.Get(sqls.DB(), id) } func (s *agentRevisionService) FindByAgentID(agentID int64) []models.AgentRevision { return repositories.AgentRevisionRepository.FindByAgentID(sqls.DB(), agentID) } type agentRevisionDefinition struct { Agent agentRevisionAgent `json:"agent"` Model agentRevisionModel `json:"model"` } // agentRevisionModel deliberately excludes APIKey. A revision must capture // reproducible routing/model parameters without duplicating credentials. type agentRevisionModel struct { ConfigID int64 `json:"config_id"` Provider string `json:"provider"` BaseURL string `json:"base_url"` ModelType string `json:"model_type"` ModelName string `json:"model_name"` MaxContextTokens int `json:"max_context_tokens"` MaxOutputTokens int `json:"max_output_tokens"` TimeoutMS int `json:"timeout_ms"` MaxRetryCount int `json:"max_retry_count"` } type agentRevisionAgent struct { Name string `json:"name"` Description string `json:"description"` AIConfigID int64 `json:"ai_config_id"` MaxSteps int `json:"max_steps"` ContextWindow int `json:"context_window"` ToolPolicy string `json:"tool_policy"` KnowledgePolicy string `json:"knowledge_policy"` ServiceMode int `json:"service_mode"` SystemPrompt string `json:"system_prompt"` WelcomeMessage string `json:"welcome_message"` ReplyTimeoutSeconds int `json:"reply_timeout_seconds"` TeamIDs string `json:"team_ids"` HandoffMode int `json:"handoff_mode"` FallbackMode int `json:"fallback_mode"` FallbackMessage string `json:"fallback_message"` KnowledgeIDs string `json:"knowledge_ids"` } // AgentRevisionSnapshot is the immutable runtime configuration restored from // a published revision. Model credentials deliberately remain on the current // AIConfig so credential rotation does not require republishing every Agent. type AgentRevisionSnapshot struct { Revision models.AgentRevision Agent models.AIAgent AIConfig models.AIConfig } // ResolvePublishedSnapshot restores an immutable published Agent revision. func (s *agentRevisionService) ResolvePublishedSnapshot(agent models.AIAgent, config models.AIConfig) (*AgentRevisionSnapshot, error) { if agent.PublishedRevisionID <= 0 { return nil, errorsx.InvalidParam("Agent is not published") } revision := repositories.AgentRevisionRepository.Get(sqls.DB(), agent.PublishedRevisionID) if revision == nil || revision.AgentID != agent.ID || revision.Status != enums.StatusOk { return nil, errorsx.InvalidParam("published Agent revision does not exist") } snapshot := &AgentRevisionSnapshot{Revision: *revision, Agent: agent, AIConfig: config} if strings.TrimSpace(revision.Definition) == "" { return nil, errorsx.InvalidParam("published Agent revision definition is empty") } definition := agentRevisionDefinition{} if err := json.Unmarshal([]byte(revision.Definition), &definition); err != nil { return nil, errorsx.InvalidParam("published Agent revision is invalid") } publishedConfigID := definition.Agent.AIConfigID if publishedConfigID <= 0 { publishedConfigID = definition.Model.ConfigID } if !config.Platform && publishedConfigID > 0 && publishedConfigID != config.ID { publishedConfig := repositories.AIConfigRepository.Get(sqls.DB(), publishedConfigID) if publishedConfig == nil || publishedConfig.Status != enums.StatusOk { return nil, errorsx.InvalidParam("published agent model config is unavailable") } snapshot.AIConfig = *publishedConfig } applyRevisionAgentSnapshot(&snapshot.Agent, definition.Agent) if !config.Platform { applyRevisionModelSnapshot(&snapshot.AIConfig, definition.Model) } return snapshot, nil } func applyRevisionAgentSnapshot(agent *models.AIAgent, definition agentRevisionAgent) { if agent == nil { return } agent.Name = definition.Name agent.Description = definition.Description agent.AIConfigID = definition.AIConfigID agent.MaxSteps = definition.MaxSteps agent.ContextWindow = definition.ContextWindow agent.ToolPolicy = definition.ToolPolicy agent.KnowledgePolicy = definition.KnowledgePolicy agent.ServiceMode = enums.IMConversationServiceMode(definition.ServiceMode) agent.SystemPrompt = definition.SystemPrompt agent.WelcomeMessage = definition.WelcomeMessage agent.ReplyTimeoutSeconds = definition.ReplyTimeoutSeconds agent.TeamIDs = definition.TeamIDs agent.HandoffMode = enums.AIAgentHandoffMode(definition.HandoffMode) agent.FallbackMode = enums.AIAgentFallbackMode(definition.FallbackMode) agent.FallbackMessage = definition.FallbackMessage agent.KnowledgeIDs = definition.KnowledgeIDs } func applyRevisionModelSnapshot(config *models.AIConfig, definition agentRevisionModel) { if config == nil || definition.ConfigID <= 0 { return } config.Provider = enums.AIProvider(definition.Provider) config.BaseURL = definition.BaseURL config.ModelType = enums.AIModelType(definition.ModelType) config.ModelName = definition.ModelName config.MaxContextTokens = definition.MaxContextTokens config.MaxOutputTokens = definition.MaxOutputTokens config.TimeoutMS = definition.TimeoutMS config.MaxRetryCount = definition.MaxRetryCount } func (s *agentRevisionService) PublishSnapshot(db *gorm.DB, agent *models.AIAgent, operator *dto.AuthPrincipal) (*models.AgentRevision, error) { return s.publishSnapshot(db, agent, operator) } func (s *agentRevisionService) publishSnapshot(db *gorm.DB, agent *models.AIAgent, operator *dto.AuthPrincipal) (*models.AgentRevision, error) { model := agentRevisionModel{ConfigID: agent.AIConfigID} if config := repositories.AIConfigRepository.Get(db, agent.AIConfigID); config != nil { model = agentRevisionModel{ ConfigID: config.ID, Provider: string(config.Provider), BaseURL: config.BaseURL, ModelType: string(config.ModelType), ModelName: config.ModelName, MaxContextTokens: config.MaxContextTokens, MaxOutputTokens: config.MaxOutputTokens, TimeoutMS: config.TimeoutMS, MaxRetryCount: config.MaxRetryCount, } } definition := agentRevisionDefinition{ Agent: agentRevisionAgent{ Name: agent.Name, Description: agent.Description, AIConfigID: agent.AIConfigID, MaxSteps: agent.MaxSteps, ContextWindow: agent.ContextWindow, ToolPolicy: agent.ToolPolicy, KnowledgePolicy: agent.KnowledgePolicy, ServiceMode: int(agent.ServiceMode), SystemPrompt: agent.SystemPrompt, WelcomeMessage: agent.WelcomeMessage, ReplyTimeoutSeconds: agent.ReplyTimeoutSeconds, TeamIDs: agent.TeamIDs, HandoffMode: int(agent.HandoffMode), FallbackMode: int(agent.FallbackMode), FallbackMessage: agent.FallbackMessage, KnowledgeIDs: agent.KnowledgeIDs, }, Model: model, } data, err := json.Marshal(definition) if err != nil { return nil, err } now := time.Now() hash := sha256.Sum256(data) item := &models.AgentRevision{ AgentID: agent.ID, Revision: repositories.AgentRevisionRepository.MaxRevisionByAgentID(db, agent.ID) + 1, Status: enums.StatusOk, Definition: string(data), DefinitionHash: hex.EncodeToString(hash[:]), PublishedAt: &now, PublishedByID: operator.UserID, PublishedByName: operator.Username, AuditFields: utils.BuildAuditFields(operator), } if err := repositories.AgentRevisionRepository.Create(db, item); err != nil { return nil, err } return item, nil }