Files
sm-golem-plugin/controllers/models_admin_controller.go
Jakub Zych c93e2d9142 feat: AI models admin, settings import, Anthropic adapter and the rest of AIService
- golem15_golem_models (encrypted write-only api_key) with its cabana list
  and form under golem15.golem.access_settings, and the Settings lookups
  (Models, DefaultModel with PHP's position-0 quirk, Vision, Image, File,
  ByName), every one Trusted
- golem:import-settings copies the WinterCMS golem_settings repeater once
- AnthropicAdapter; SendStream, SendFile/SendFilePath/SendFileReader,
  SendWithModel, SendToImageModel, SendToVisionModel, GenerateImage, Ask
- PromptFactory; README documents the config keys, the environment
  variable, the command and the admin screen
2026-10-03 23:09:02 +02:00

91 lines
2.8 KiB
Go

package controllers
import (
"context"
"errors"
"git.golem15.com/golem15/sm-golem-plugin/models"
"git.golem15.com/golem15/summercms/modules/cabana"
"git.golem15.com/golem15/summercms/modules/pact"
)
// PermissionAccessSettings guards the AI models screen.
const PermissionAccessSettings = "golem15.golem.access_settings"
// ModelsAdminController is the AI models list and form
// (golem15.golem.models). The API key is write-only: the form shows it
// redacted, and an update that sends it back empty or redacted keeps the
// stored key.
type ModelsAdminController struct{}
var (
_ pact.AdminController = ModelsAdminController{}
_ pact.AdminPermissioned = ModelsAdminController{}
_ pact.AdminRecordSource = ModelsAdminController{}
_ pact.FormBeforeCreate = ModelsAdminController{}
_ pact.FormBeforeUpdate = ModelsAdminController{}
)
// ID is golem15.golem.models.
func (ModelsAdminController) ID() string { return "golem15.golem.models" }
// ModelName is the model's WinterCMS class name.
func (ModelsAdminController) ModelName() string { return `Golem15\Golem\Models\AiModel` }
// ConfigDir holds config_list.yaml and config_form.yaml.
func (ModelsAdminController) ConfigDir() string { return "controllers/aimodels" }
// RequiredPermissions is golem15.golem.access_settings.
func (ModelsAdminController) RequiredPermissions() []string {
return []string{PermissionAccessSettings}
}
// NewRecord returns an empty AI model.
func (ModelsAdminController) NewRecord() any { return &models.AiModel{} }
// redacted is how lagoon.Encrypted serializes, so how the form shows the
// stored key.
const redacted = "[redacted]"
func keyMissing(m *models.AiModel) bool {
k := m.APIKey.Reveal()
return k == "" || k == redacted
}
// FormBeforeCreate requires an API key.
func (ModelsAdminController) FormBeforeCreate(_ context.Context, model any) error {
m, ok := model.(*models.AiModel)
if !ok {
return errors.New("golem: unexpected admin record")
}
if keyMissing(m) {
return &cabana.ValidationError{Details: map[string]any{"api_key": []string{"The api key field is required."}}}
}
return nil
}
// FormBeforeUpdate keeps the stored key when the form sent it back empty
// or redacted.
func (ModelsAdminController) FormBeforeUpdate(ctx context.Context, model any) error {
m, ok := model.(*models.AiModel)
if !ok {
return errors.New("golem: unexpected admin record")
}
if !keyMissing(m) {
return nil
}
tx, ok := cabana.TxFromContext(ctx)
if !ok {
return errors.New("golem: no transaction for the model update")
}
var stored models.AiModel
if err := tx.Select("api_key").Where("id = ?", m.ID).Take(&stored).Error; err != nil {
return err
}
m.APIKey = stored.APIKey
if keyMissing(m) {
return &cabana.ValidationError{Details: map[string]any{"api_key": []string{"The api key field is required."}}}
}
return nil
}