package handlers import ( "context" "net/http" "github.com/gin-gonic/gin" "git.gobha.me/xcaliber/chat-switchboard/models" "git.gobha.me/xcaliber/chat-switchboard/store" ) // PersonaHandler handles persona (formerly preset) endpoints. type PersonaHandler struct { stores store.Stores } func NewPersonaHandler(s store.Stores) *PersonaHandler { return &PersonaHandler{stores: s} } // ── User Personas (personal scope) ────────── func (h *PersonaHandler) ListUserPersonas(c *gin.Context) { userID := getUserID(c) personas, err := h.stores.Personas.ListForUser(c.Request.Context(), userID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list personas"}) return } c.JSON(http.StatusOK, gin.H{"personas": personas, "presets": personas}) } func (h *PersonaHandler) CreateUserPersona(c *gin.Context) { userID := getUserID(c) // Check policy allowed, _ := h.stores.Policies.GetBool(c.Request.Context(), "allow_user_personas") if !allowed { c.JSON(http.StatusForbidden, gin.H{"error": "custom personas not allowed"}) return } var req personaRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } persona := req.toPersona() persona.Scope = models.ScopePersonal persona.OwnerID = &userID persona.CreatedBy = userID if err := h.stores.Personas.Create(c.Request.Context(), persona); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create persona"}) return } c.JSON(http.StatusCreated, persona) } func (h *PersonaHandler) UpdateUserPersona(c *gin.Context) { userID := getUserID(c) id := c.Param("id") existing, err := h.stores.Personas.GetByID(c.Request.Context(), id) if err != nil { c.JSON(http.StatusNotFound, gin.H{"error": "persona not found"}) return } // Users can only edit their own personal personas if existing.Scope != models.ScopePersonal || existing.OwnerID == nil || *existing.OwnerID != userID { c.JSON(http.StatusForbidden, gin.H{"error": "cannot edit this persona"}) return } var patch models.PersonaPatch if err := c.ShouldBindJSON(&patch); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.stores.Personas.Update(c.Request.Context(), id, patch); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update persona"}) return } c.JSON(http.StatusOK, gin.H{"message": "persona updated"}) } func (h *PersonaHandler) DeleteUserPersona(c *gin.Context) { userID := getUserID(c) id := c.Param("id") existing, err := h.stores.Personas.GetByID(c.Request.Context(), id) if err != nil || existing.Scope != models.ScopePersonal || existing.OwnerID == nil || *existing.OwnerID != userID { c.JSON(http.StatusForbidden, gin.H{"error": "cannot delete this persona"}) return } if err := h.stores.Personas.Delete(c.Request.Context(), id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete persona"}) return } c.JSON(http.StatusOK, gin.H{"message": "persona deleted"}) } // ── Team Personas ─────────────────────────── func (h *PersonaHandler) ListTeamPersonas(c *gin.Context) { teamID := c.Param("teamId") personas, err := h.stores.Personas.ListForTeam(c.Request.Context(), teamID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list team personas"}) return } c.JSON(http.StatusOK, gin.H{"personas": personas, "presets": personas}) } func (h *PersonaHandler) CreateTeamPersona(c *gin.Context) { userID := getUserID(c) teamID := c.Param("teamId") var req personaRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } persona := req.toPersona() persona.Scope = models.ScopeTeam persona.OwnerID = &teamID persona.CreatedBy = userID if err := h.stores.Personas.Create(c.Request.Context(), persona); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create team persona"}) return } c.JSON(http.StatusCreated, persona) } func (h *PersonaHandler) DeleteTeamPersona(c *gin.Context) { id := c.Param("id") if err := h.stores.Personas.Delete(c.Request.Context(), id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete persona"}) return } c.JSON(http.StatusOK, gin.H{"message": "persona deleted"}) } // ── Admin Personas (global scope) ─────────── func (h *PersonaHandler) ListAdminPersonas(c *gin.Context) { personas, err := h.stores.Personas.ListGlobal(c.Request.Context()) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to list personas"}) return } c.JSON(http.StatusOK, gin.H{"personas": personas, "presets": personas}) } func (h *PersonaHandler) CreateAdminPersona(c *gin.Context) { userID := getUserID(c) var req personaRequest if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } persona := req.toPersona() persona.Scope = models.ScopeGlobal persona.CreatedBy = userID if err := h.stores.Personas.Create(c.Request.Context(), persona); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to create persona"}) return } c.JSON(http.StatusCreated, persona) } func (h *PersonaHandler) UpdateAdminPersona(c *gin.Context) { id := c.Param("id") var patch models.PersonaPatch if err := c.ShouldBindJSON(&patch); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.stores.Personas.Update(c.Request.Context(), id, patch); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update persona"}) return } c.JSON(http.StatusOK, gin.H{"message": "persona updated"}) } func (h *PersonaHandler) DeleteAdminPersona(c *gin.Context) { id := c.Param("id") if err := h.stores.Personas.Delete(c.Request.Context(), id); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to delete persona"}) return } c.JSON(http.StatusOK, gin.H{"message": "persona deleted"}) } // ── Request Types ─────────────────────────── type personaRequest struct { Name string `json:"name" binding:"required"` Description string `json:"description,omitempty"` Icon string `json:"icon,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"` IsShared bool `json:"is_shared,omitempty"` } func (r *personaRequest) toPersona() *models.Persona { p := &models.Persona{ Name: r.Name, Description: r.Description, Icon: r.Icon, BaseModelID: r.BaseModelID, ProviderConfigID: r.ProviderConfigID, SystemPrompt: r.SystemPrompt, Temperature: r.Temperature, MaxTokens: r.MaxTokens, ThinkingBudget: r.ThinkingBudget, TopP: r.TopP, IsActive: true, IsShared: r.IsShared, } return p } // ResolvePreset loads a persona by ID and returns it if the user has access. // Returns nil if not found, inactive, or not accessible. // Used by completion.go and messages.go for preset unwrapping. func ResolvePreset(stores store.Stores, presetID, userID string) *models.Persona { ctx := context.Background() p, err := stores.Personas.GetByID(ctx, presetID) if err != nil || !p.IsActive { return nil } ok, err := stores.Personas.UserCanAccess(ctx, userID, presetID) if err != nil || !ok { return nil } return p } // ── Persona-KB Binding Endpoints (v0.17.0) ────── // GetPersonaKBs returns the knowledge bases bound to a persona. func (h *PersonaHandler) GetPersonaKBs(c *gin.Context) { personaID := c.Param("id") kbs, err := h.stores.Personas.GetKBs(c.Request.Context(), personaID) if err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to load persona KBs"}) return } c.JSON(http.StatusOK, gin.H{"data": kbs}) } // SetPersonaKBs replaces the knowledge bases bound to a persona. func (h *PersonaHandler) SetPersonaKBs(c *gin.Context) { personaID := c.Param("id") var req struct { KBIDs []string `json:"kb_ids"` AutoSearch map[string]bool `json:"auto_search"` // kb_id → true/false } if err := c.ShouldBindJSON(&req); err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) return } if err := h.stores.Personas.SetKBs(c.Request.Context(), personaID, req.KBIDs, req.AutoSearch); err != nil { c.JSON(http.StatusInternalServerError, gin.H{"error": "failed to update persona KBs"}) return } c.JSON(http.StatusOK, gin.H{"status": "ok"}) }