135 lines
5.3 KiB
Go
135 lines
5.3 KiB
Go
package consumers
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
|
)
|
|
|
|
// SequenceCatalogConfig is a source-backed prompt catalog with an application
|
|
// supplied run label and a value derived from that label during resolution.
|
|
type SequenceCatalogConfig struct {
|
|
RunLabel string `yaml:"run_label"`
|
|
ResolvedLabel string `yaml:"resolved_label"`
|
|
Prompts []SequenceCatalogEntry `yaml:"prompts"`
|
|
}
|
|
|
|
// SequenceCatalogEntry keeps prompt meaning, resources, and credentials in the
|
|
// consuming application while ConfigKit handles collection assembly.
|
|
type SequenceCatalogEntry struct {
|
|
ID string `yaml:"id"`
|
|
Name string `yaml:"name"`
|
|
Description string `yaml:"description"`
|
|
Messages []string `yaml:"messages"`
|
|
Resource string `yaml:"resource"`
|
|
Credential configkit.Secret `yaml:"credential"`
|
|
}
|
|
|
|
// LoadSequenceCatalog assembles imported main fragments over a file-backed
|
|
// underlay, applies one selected collection overlay, then resolves and validates
|
|
// the resulting application configuration.
|
|
func LoadSequenceCatalog(mainPath, overlayName, runLabel string) (*configkit.Result[SequenceCatalogConfig], error) {
|
|
root := filepath.Dir(mainPath)
|
|
filesystem := configkit.NewFilesystem("sequence catalog", os.DirFS(root))
|
|
filesystemContext := &configkit.FilesystemContext{BaseDir: ".", Root: ".", FS: &filesystem}
|
|
definition := configkit.Definition[SequenceCatalogConfig]{
|
|
SequenceIdentities: []configkit.SequenceIdentity{
|
|
configkit.SequenceIdentityAt(configkit.Keys("prompts"), "id"),
|
|
},
|
|
Resolve: func(configuration *SequenceCatalogConfig, resolution *configkit.Resolution) error {
|
|
configuration.ResolvedLabel = "for:" + configuration.RunLabel
|
|
resolution.Record(configkit.Keys("resolved_label"), configkit.ChangeNormalization,
|
|
"derive the resolved label from the selected run label", configkit.Keys("run_label"))
|
|
return nil
|
|
},
|
|
Validate: validateSequenceCatalog,
|
|
}
|
|
return configkit.LoadLayered(configkit.Input{
|
|
Source: filesystem.File(filepath.Base(mainPath)),
|
|
Imports: true,
|
|
}, configkit.LayeredOptions[SequenceCatalogConfig]{
|
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{
|
|
Source: filesystem.File("underlay.yml"),
|
|
}}}},
|
|
Overlay: overlayName,
|
|
OverlayFilesystem: filesystemContext,
|
|
Overrides: []configkit.Override{{
|
|
Path: configkit.Keys("run_label"), Value: runLabel, Flag: "--run-label",
|
|
}},
|
|
Definition: definition,
|
|
})
|
|
}
|
|
|
|
// ReadSequenceCatalogResource follows the final prompt field's source context,
|
|
// including when record inheritance retained a resource from an underlay.
|
|
func ReadSequenceCatalogResource(result *configkit.Result[SequenceCatalogConfig], promptID string) (string, error) {
|
|
if result == nil {
|
|
return "", errors.New("sequence catalog result is nil")
|
|
}
|
|
for index, prompt := range result.Config.Prompts {
|
|
if prompt.ID != promptID {
|
|
continue
|
|
}
|
|
resourcePath := configkit.Keys("prompts").Index(index).Key("resource")
|
|
info, found := result.Metadata().Lookup(resourcePath)
|
|
if !found || len(info.Origin.Locations) != 1 {
|
|
return "", errors.New("prompt resource has no single declaring source")
|
|
}
|
|
context, found := result.SourceContext(info.Origin.Locations[0].SourceID)
|
|
if !found {
|
|
return "", errors.New("prompt resource source context is unavailable")
|
|
}
|
|
data, err := fs.ReadFile(context.FS, path.Join(context.BaseDir, prompt.Resource))
|
|
if err != nil {
|
|
return "", fmt.Errorf("read prompt resource: %w", err)
|
|
}
|
|
return string(data), nil
|
|
}
|
|
return "", fmt.Errorf("prompt %q is unavailable", promptID)
|
|
}
|
|
|
|
func validateSequenceCatalog(configuration *SequenceCatalogConfig, validation *configkit.Validation) {
|
|
for index, prompt := range configuration.Prompts {
|
|
if strings.TrimSpace(prompt.Name) == "" {
|
|
validation.Error(configkit.Keys("prompts").Index(index).Key("name"), "must not be empty")
|
|
}
|
|
if len(prompt.Messages) == 0 {
|
|
validation.Error(configkit.Keys("prompts").Index(index).Key("messages"), "must contain at least one message")
|
|
}
|
|
}
|
|
if len(configuration.Prompts) == 0 {
|
|
validation.Error(configkit.Keys("prompts"), "must contain at least one prompt")
|
|
}
|
|
}
|
|
|
|
// NamespacedMapCatalogConfig demonstrates typed application maps whose
|
|
// independent prompt sequences use root and dynamic namespace registrations.
|
|
type NamespacedMapCatalogConfig struct {
|
|
Prompts []map[string]any `yaml:"prompts"`
|
|
Collections map[string]map[string]any `yaml:"collections"`
|
|
}
|
|
|
|
// LoadNamespacedMapCatalog applies the same scalar identity constraint to one
|
|
// root collection and each namespace's independently scoped collection.
|
|
func LoadNamespacedMapCatalog(source configkit.Source) (*configkit.Result[NamespacedMapCatalogConfig], error) {
|
|
selector := configkit.SequenceSelector{
|
|
configkit.SequenceLiteralKey("collections"),
|
|
configkit.SequenceAnyMappingKey(),
|
|
configkit.SequenceLiteralKey("prompts"),
|
|
}
|
|
return configkit.Load(source, configkit.Options[NamespacedMapCatalogConfig]{
|
|
Definition: configkit.Definition[NamespacedMapCatalogConfig]{
|
|
SequenceIdentities: []configkit.SequenceIdentity{
|
|
configkit.SequenceIdentityAt(configkit.Keys("prompts"), "id"),
|
|
configkit.SequenceIdentityMatching(selector, "id"),
|
|
},
|
|
},
|
|
})
|
|
}
|