Expand prompt catalog discovery example
This commit is contained in:
@@ -185,11 +185,26 @@ import, source-location, and ownership behavior.
|
|||||||
## Identified prompt sequences
|
## Identified prompt sequences
|
||||||
|
|
||||||
[`LoadPromptCatalog`](../../examples/consumers/prompt_catalog.go) uses one
|
[`LoadPromptCatalog`](../../examples/consumers/prompt_catalog.go) uses one
|
||||||
definition registration for the literal `prompts` path and `id` field. Its main
|
definition registration for the literal `prompts` path and `id` field. Its
|
||||||
file imports one prompt by path and others by glob, placing each source as an
|
load-wide filename defaults accept two patterns, an explicit directory
|
||||||
item. A nested ordinary helper import supplies an item's ID. The main tier
|
declaration overrides those patterns, and an exclusion omits one matching
|
||||||
strictly appends the distinct records in contribution order; its completed
|
candidate. A nested ordinary helper import supplies an item's ID. A separate
|
||||||
sequence replaces the lower-priority default collection as one value.
|
empty optional directory search is read from `SourceInfo.ImportSelections`.
|
||||||
|
The executable example prints both the scheduled prompt order and the single
|
||||||
|
empty-search outcome; the behavior test also follows a mounted resource through
|
||||||
|
its effective metadata origin to the declaring source context. The completed
|
||||||
|
main sequence replaces the lower-priority default collection as one value.
|
||||||
|
The maintained [executable example and behavior tests](../../examples/consumers/prompt_catalog_test.go)
|
||||||
|
show the scheduled result, empty-search inspection, excluded candidate, and
|
||||||
|
resource lookup without a consumer-side discovery pass. The corresponding
|
||||||
|
[complex fixture](../../examples/consumers/testdata/prompt-catalog/config.yml)
|
||||||
|
and [simple required-file fixture](../../examples/consumers/testdata/prompt-catalog/simple.yml)
|
||||||
|
are complete local examples.
|
||||||
|
|
||||||
|
[`LoadRequiredPromptCatalog`](../../examples/consumers/prompt_catalog.go) and
|
||||||
|
[`Example_requiredPromptCatalog`](../../examples/consumers/prompt_catalog_test.go)
|
||||||
|
keep the smaller adoption path visible: one explicitly required file import,
|
||||||
|
the same identity registration, and the same application validation.
|
||||||
|
|
||||||
The example's `validatePromptCatalog` remains application-owned. It checks that
|
The example's `validatePromptCatalog` remains application-owned. It checks that
|
||||||
prompt names and message lists satisfy this consumer's requirements after
|
prompt names and message lists satisfy this consumer's requirements after
|
||||||
|
|||||||
@@ -16,9 +16,10 @@ type PromptRecord struct {
|
|||||||
ID string `yaml:"id"`
|
ID string `yaml:"id"`
|
||||||
Name string `yaml:"name"`
|
Name string `yaml:"name"`
|
||||||
Messages []string `yaml:"messages"`
|
Messages []string `yaml:"messages"`
|
||||||
|
Resource string `yaml:"resource"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadPromptCatalog composes explicitly registered prompt records in the main
|
// LoadPromptCatalog discovers explicitly registered prompt records in the main
|
||||||
// tier and replaces the lower-priority default collection as a whole.
|
// tier and replaces the lower-priority default collection as a whole.
|
||||||
func LoadPromptCatalog(mainPath string) (*configkit.Result[PromptCatalogConfig], error) {
|
func LoadPromptCatalog(mainPath string) (*configkit.Result[PromptCatalogConfig], error) {
|
||||||
return configkit.LoadLayered(configkit.Input{
|
return configkit.LoadLayered(configkit.Input{
|
||||||
@@ -28,16 +29,30 @@ func LoadPromptCatalog(mainPath string) (*configkit.Result[PromptCatalogConfig],
|
|||||||
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{
|
Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{
|
||||||
Source: configkit.Bytes("default prompts", []byte("prompts:\n - id: default\n name: Default prompt\n messages: [Default message]\n")),
|
Source: configkit.Bytes("default prompts", []byte("prompts:\n - id: default\n name: Default prompt\n messages: [Default message]\n")),
|
||||||
}}}},
|
}}}},
|
||||||
Definition: configkit.Definition[PromptCatalogConfig]{
|
ImportDefaults: configkit.ImportDefaults{
|
||||||
SequenceCompositions: []configkit.SequenceComposition{{
|
Pattern: []string{"curated*.yml", "generated-*.yml"},
|
||||||
Path: configkit.Keys("prompts"),
|
|
||||||
IdentityField: "id",
|
|
||||||
}},
|
|
||||||
Validate: validatePromptCatalog,
|
|
||||||
},
|
},
|
||||||
|
Definition: promptCatalogDefinition(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// LoadRequiredPromptCatalog keeps the common one-file import shape small.
|
||||||
|
func LoadRequiredPromptCatalog(mainPath string) (*configkit.Result[PromptCatalogConfig], error) {
|
||||||
|
return configkit.Load(configkit.File(mainPath), configkit.Options[PromptCatalogConfig]{
|
||||||
|
Imports: true, Definition: promptCatalogDefinition(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptCatalogDefinition() configkit.Definition[PromptCatalogConfig] {
|
||||||
|
return configkit.Definition[PromptCatalogConfig]{
|
||||||
|
SequenceCompositions: []configkit.SequenceComposition{{
|
||||||
|
Path: configkit.Keys("prompts"),
|
||||||
|
IdentityField: "id",
|
||||||
|
}},
|
||||||
|
Validate: validatePromptCatalog,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func validatePromptCatalog(configuration *PromptCatalogConfig, validation *configkit.Validation) {
|
func validatePromptCatalog(configuration *PromptCatalogConfig, validation *configkit.Validation) {
|
||||||
for index, prompt := range configuration.Prompts {
|
for index, prompt := range configuration.Prompts {
|
||||||
if strings.TrimSpace(prompt.Name) == "" {
|
if strings.TrimSpace(prompt.Name) == "" {
|
||||||
|
|||||||
@@ -3,6 +3,8 @@ package consumers_test
|
|||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io/fs"
|
||||||
|
pathpkg "path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -20,8 +22,26 @@ func Example_identifiedPromptCatalog() {
|
|||||||
for _, prompt := range result.Config.Prompts {
|
for _, prompt := range result.Config.Prompts {
|
||||||
ids = append(ids, prompt.ID)
|
ids = append(ids, prompt.ID)
|
||||||
}
|
}
|
||||||
fmt.Println(ids, err)
|
emptySearches := 0
|
||||||
// Output: [main curated weather welcome] <nil>
|
for _, source := range result.Sources() {
|
||||||
|
for _, selection := range source.ImportSelections {
|
||||||
|
if selection.Selector == configkit.ImportSelectorDirectory && selection.Outcome == configkit.ImportSelectionNone {
|
||||||
|
emptySearches++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fmt.Println(ids, emptySearches, err)
|
||||||
|
// Output: [weather curated welcome main] 1 <nil>
|
||||||
|
}
|
||||||
|
|
||||||
|
func Example_requiredPromptCatalog() {
|
||||||
|
result, err := consumers.LoadRequiredPromptCatalog(fixture("prompt-catalog", "simple.yml"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println(result.Config.Prompts[0].ID, err)
|
||||||
|
// Output: required <nil>
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPromptCatalogImportsRegisteredItemsAndReplacesDefaults(t *testing.T) {
|
func TestPromptCatalogImportsRegisteredItemsAndReplacesDefaults(t *testing.T) {
|
||||||
@@ -36,7 +56,7 @@ func TestPromptCatalogImportsRegisteredItemsAndReplacesDefaults(t *testing.T) {
|
|||||||
if len(result.Config.Prompts) != 4 {
|
if len(result.Config.Prompts) != 4 {
|
||||||
t.Fatalf("prompt records = %#v", result.Config.Prompts)
|
t.Fatalf("prompt records = %#v", result.Config.Prompts)
|
||||||
}
|
}
|
||||||
ids := []string{"main", "curated", "weather", "welcome"}
|
ids := []string{"weather", "curated", "welcome", "main"}
|
||||||
for index, id := range ids {
|
for index, id := range ids {
|
||||||
if result.Config.Prompts[index].ID != id {
|
if result.Config.Prompts[index].ID != id {
|
||||||
t.Fatalf("prompt %d ID = %q, want %q", index, result.Config.Prompts[index].ID, id)
|
t.Fatalf("prompt %d ID = %q, want %q", index, result.Config.Prompts[index].ID, id)
|
||||||
@@ -46,12 +66,15 @@ func TestPromptCatalogImportsRegisteredItemsAndReplacesDefaults(t *testing.T) {
|
|||||||
if prompt.ID == "default" {
|
if prompt.ID == "default" {
|
||||||
t.Fatalf("lower-priority default survived main sequence replacement: %#v", result.Config.Prompts)
|
t.Fatalf("lower-priority default survived main sequence replacement: %#v", result.Config.Prompts)
|
||||||
}
|
}
|
||||||
|
if prompt.ID == "ignored" {
|
||||||
|
t.Fatalf("excluded generated prompt was selected: %#v", result.Config.Prompts)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var itemImports, helperImports int
|
var itemImports, helperImports int
|
||||||
for _, source := range result.Sources() {
|
for _, source := range result.Sources() {
|
||||||
switch filepath.Base(source.Path) {
|
switch filepath.Base(source.Path) {
|
||||||
case "curated.yml", "generated-weather.yml", "generated-welcome.yml":
|
case "curated.yml", "weather-feature.yml", "generated-welcome.yml":
|
||||||
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/prompts" {
|
if source.PlacementMode != configkit.PlacementModeSequenceItem || source.Destination.String() != "/prompts" {
|
||||||
t.Fatalf("item source placement = %#v", source)
|
t.Fatalf("item source placement = %#v", source)
|
||||||
}
|
}
|
||||||
@@ -71,6 +94,36 @@ func TestPromptCatalogImportsRegisteredItemsAndReplacesDefaults(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPromptCatalogMountsResourceFromScheduledPromptOrigin(t *testing.T) {
|
||||||
|
result, err := consumers.LoadPromptCatalog(fixture("prompt-catalog", "config.yml"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if result.Config.Prompts[2].ID != "welcome" || result.Config.Prompts[2].Resource != "welcome.txt" {
|
||||||
|
t.Fatalf("scheduled resource prompt = %#v", result.Config.Prompts)
|
||||||
|
}
|
||||||
|
resourcePath := configkit.Keys("prompts").Index(2).Key("resource")
|
||||||
|
info, ok := result.Metadata().Lookup(resourcePath)
|
||||||
|
if !ok || len(info.Origin.Locations) != 1 {
|
||||||
|
t.Fatalf("resource origin = %#v, %v", info, ok)
|
||||||
|
}
|
||||||
|
context, ok := result.SourceContext(info.Origin.Locations[0].SourceID)
|
||||||
|
if !ok || context.FS == nil || filepath.Base(context.BaseDir) != "prompts" {
|
||||||
|
t.Fatalf("resource source context = %#v, %v", context, ok)
|
||||||
|
}
|
||||||
|
contents, err := fs.ReadFile(context.FS, pathpkg.Join(context.BaseDir, result.Config.Prompts[2].Resource))
|
||||||
|
if err != nil || string(contents) != "Welcome resource text.\n" {
|
||||||
|
t.Fatalf("mounted resource = %q, %v", contents, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRequiredPromptCatalogUsesOneRequiredFile(t *testing.T) {
|
||||||
|
result, err := consumers.LoadRequiredPromptCatalog(fixture("prompt-catalog", "simple.yml"))
|
||||||
|
if err != nil || len(result.Config.Prompts) != 1 || result.Config.Prompts[0].ID != "required" {
|
||||||
|
t.Fatalf("simple required prompt = %#v, %v", result, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPromptCatalogLeavesDomainValidationWithConsumer(t *testing.T) {
|
func TestPromptCatalogLeavesDomainValidationWithConsumer(t *testing.T) {
|
||||||
_, err := consumers.LoadPromptCatalog(fixture("prompt-catalog", "invalid.yml"))
|
_, err := consumers.LoadPromptCatalog(fixture("prompt-catalog", "invalid.yml"))
|
||||||
var loadError *configkit.LoadError
|
var loadError *configkit.LoadError
|
||||||
|
|||||||
@@ -1,11 +1,14 @@
|
|||||||
configkit:
|
configkit:
|
||||||
imports:
|
imports:
|
||||||
- path: prompts/curated.yml
|
- dir: prompts
|
||||||
|
exclude: [generated-ignored.yml]
|
||||||
destination: [prompts]
|
destination: [prompts]
|
||||||
placement: sequence-item
|
placement: sequence-item
|
||||||
- glob: prompts/generated-*.yml
|
- dir: featured
|
||||||
|
pattern: [weather-*.yml]
|
||||||
destination: [prompts]
|
destination: [prompts]
|
||||||
placement: sequence-item
|
placement: sequence-item
|
||||||
|
- dir: optional
|
||||||
prompts:
|
prompts:
|
||||||
- id: main
|
- id: main
|
||||||
name: Main authored prompt
|
name: Main authored prompt
|
||||||
|
|||||||
1
examples/consumers/testdata/prompt-catalog/optional/ignored.txt
vendored
Normal file
1
examples/consumers/testdata/prompt-catalog/optional/ignored.txt
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
This file does not match the application import defaults.
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
configkit:
|
configkit:
|
||||||
imports:
|
imports:
|
||||||
- path: curated-id.yml
|
- path: helpers/curated-id.yml
|
||||||
name: Curated summary
|
name: Curated summary
|
||||||
messages:
|
messages:
|
||||||
- Summarize the supplied content.
|
- Summarize the supplied content.
|
||||||
|
|||||||
3
examples/consumers/testdata/prompt-catalog/prompts/generated-ignored.yml
vendored
Normal file
3
examples/consumers/testdata/prompt-catalog/prompts/generated-ignored.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
id: ignored
|
||||||
|
name: Excluded generated prompt
|
||||||
|
messages: [This record is excluded.]
|
||||||
@@ -2,3 +2,4 @@ id: welcome
|
|||||||
name: Welcome
|
name: Welcome
|
||||||
messages:
|
messages:
|
||||||
- Welcome the user.
|
- Welcome the user.
|
||||||
|
resource: welcome.txt
|
||||||
|
|||||||
3
examples/consumers/testdata/prompt-catalog/prompts/required.yml
vendored
Normal file
3
examples/consumers/testdata/prompt-catalog/prompts/required.yml
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
id: required
|
||||||
|
name: Required prompt
|
||||||
|
messages: [Use the required prompt.]
|
||||||
1
examples/consumers/testdata/prompt-catalog/prompts/welcome.txt
vendored
Normal file
1
examples/consumers/testdata/prompt-catalog/prompts/welcome.txt
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
Welcome resource text.
|
||||||
7
examples/consumers/testdata/prompt-catalog/simple.yml
vendored
Normal file
7
examples/consumers/testdata/prompt-catalog/simple.yml
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
configkit:
|
||||||
|
imports:
|
||||||
|
- path: prompts/required.yml
|
||||||
|
required: true
|
||||||
|
destination: [prompts]
|
||||||
|
placement: sequence-item
|
||||||
|
prompts: []
|
||||||
Reference in New Issue
Block a user