From 29f104fbc0c565f04146dcc38c93008098d497e5 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 17 Oct 2026 06:29:57 +0000 Subject: [PATCH] Add collection migration example and guide --- README.md | 8 +- docs/consumers/configuration-migration.md | 165 ++++++++++++++++++ docs/consumers/pkg-promptkit.md | 6 + docs/formats.md | 2 +- docs/roadmap/implementation.md | 12 ++ .../collections/review/collection.yaml | 17 ++ .../collections/support/collection.yaml | 5 + .../support/fragments/resources.yaml} | 17 +- .../collections/support/messages/note.md | 1 + .../support/operator-overrides.yaml | 4 + examples/go-library/prepare/main.go | 121 +++++++++++-- scripts/check-examples.py | 24 ++- 12 files changed, 356 insertions(+), 26 deletions(-) create mode 100644 docs/consumers/configuration-migration.md create mode 100644 examples/go-library/prepare/collections/review/collection.yaml create mode 100644 examples/go-library/prepare/collections/support/collection.yaml rename examples/go-library/prepare/{prompt.yaml => collections/support/fragments/resources.yaml} (54%) create mode 100644 examples/go-library/prepare/collections/support/messages/note.md create mode 100644 examples/go-library/prepare/collections/support/operator-overrides.yaml diff --git a/README.md b/README.md index 0f53544..75016c6 100644 --- a/README.md +++ b/README.md @@ -21,9 +21,11 @@ Run the maintained offline preparation example from the repository root: go run ./examples/go-library/prepare ``` -It loads a repository-local prompt, supplies a programmatic profile and inline -input, inspects the captured configuration, and prints deterministic -preparation metadata without contacting a provider or requiring credentials. +It composes two explicitly selected synthetic collections, imports +source-relative prompt content, supplies an inline input, and prepares through +a scoped view. The example also demonstrates collection inventories and +namespace-filtered discovery, without contacting a provider or requiring +credentials. The [consumer guide](docs/consumers/pkg-promptkit.md) also shows a local backend, ordered file/embedded sources, programmatic mappings, and how to replace an engine when source files change. Read the diff --git a/docs/consumers/configuration-migration.md b/docs/consumers/configuration-migration.md new file mode 100644 index 0000000..f1ae20d --- /dev/null +++ b/docs/consumers/configuration-migration.md @@ -0,0 +1,165 @@ +# Configuration And API Migration + +Promptkit no longer installs a provider catalog or selects prompt definitions +by descriptive version labels. Applications now choose configuration inputs +explicitly, and execution selects one prompt by its exact ID. This guide +explains the shape changes and decisions an application must make; it is not an +automatic migration tool. The [framework format reference](../formats.md) owns +the current configuration contract, and the root package GoDoc owns exact API +signatures and errors. + +## Select And Compose The Configuration You Need + +Supply each backend, profile, prompt, schema, and collection source that the +application intends to use. Promptkit does not discover directories or install +provider definitions implicitly. An application may select an explicit +collection input with `CollectionInput`: + +```go +input, err := promptkit.CollectionInput( + "openrouter", + configkit.File("config/openrouter.yaml"), +) +if err != nil { + return err +} +engine, err := promptkit.NewEngine(promptkit.Config{ + Sources: promptkit.ConfigurationSources{ + Underlays: []configkit.Underlay{{Inputs: []configkit.Input{input}}}, + }, +}) +``` + +Collection resource keys are local keys, while their runtime identities are +qualified. For example, `collections.openrouter.profiles.gemini-flash-latest` +is selected as `openrouter/gemini-flash-latest`. Stored backend, prompt-default, +and other resource references must use their exact intended IDs; Promptkit does +not rewrite references when a source is mounted. Profile `extends` remains a +sibling local key in the same collection, not a cross-collection reference. +Review collisions and all references when moving resources into collections. + +The root `version` is an optional ConfigKit source header. A collection's +`release_version` and a prompt's `version` are optional descriptive labels; +neither selects a resource or changes execution. Preserve a label only when it +is useful to your application. A formerly version-keyed prompt mapping must be +reduced to one deliberately chosen definition per prompt ID, with its old label +retained as prompt metadata if desired. Promptkit cannot choose a winner from a +consumer's version history. + +For example, change a version-keyed definition: + +```yaml +prompts: + report: + "2.0": + messages: + - role: user + content: Review this report. + output: + format: text + validation_mode: none +``` + +to one direct definition. Its optional `version` is only a label: + +```yaml +prompts: + report: + version: "2.0" + messages: + - role: user + content: Review this report. + output: + format: text + validation_mode: none +``` + +## Identity Mapping + +The reviewed catalog identities have slash-free profile keys. Their documented +mapping is: + +| Previous identity | Namespaced identity or action | +| --- | --- | +| Backend `openrouter` | `openrouter/default` | +| Backend `rakestrawhome` | `rakestrawhome/default` | +| Each OpenRouter catalog profile `p` | `openrouter/p`, retaining the exact local key `p` | +| Profile `rakestrawhome-gemma-4-31b` | `rakestrawhome/rakestrawhome-gemma-4-31b` | +| Built-in `promptkit-openrouter-standard` | No automatic replacement; optionally define caller profile `openrouter/standard` | +| Built-in `promptkit-openrouter-auto` | No automatic replacement; optionally define caller profile `openrouter/auto` | + +These are conversion examples, not aliases or bundled resources. Mounting old +`/v3` assets does not update backend references, profile inheritance, prompt +defaults, or diagnostic defaults inside them. Edit those values to the exact +qualified IDs you chose. + +For example, an optional caller-owned standard profile may extend a selected +Gemini profile in the same `openrouter` collection: + +```yaml +collections: + openrouter: + profiles: + standard: + extends: gemini-flash-latest +``` + +An optional auto profile may instead select the exact backend and model values: + +```yaml +collections: + openrouter: + profiles: + auto: + backend: openrouter/default + model: openrouter/auto +``` + +Neither optional profile name is a Promptkit contract. Keep provider model +strings and settings as selected by the application, including any meaningful +prefixes. If a backend declares diagnostic defaults, update them to exact +qualified IDs; an unselected diagnostic default is not automatically made +resolvable by migration. + +## Prompt Selection API + +Prompt selection is ID-only. Remove version selectors from calls to `Run`, +`Prepare`, `PrepareExecution`, `ValidateRequest`, `InspectRequirements`, and +`SmokeTest`. Remove the `PromptVersion` field from `RunRequest`, +`RequirementInspectionRequest`, and `SmokeTestRequest`; `InspectPrompt` now +has the form `InspectPrompt(ctx, promptID)`. For example, select the prompt +explicitly in the request rather than setting a version selector: + +```go +request := promptkit.RunRequest{ + PromptID: "support/report", + ProfileID: "support/standard", +} +_, err = engine.Run(ctx, request) +if err != nil { + return err +} +``` + +If the former format contained multiple versions under one ID, choose the one +definition the application should install, and optionally keep its label in +the prompt's `version` field. That label remains descriptive metadata and is +reported by inspection or results where applicable; it never chooses the +definition. + +The public inventory APIs can help an operator inspect a captured effective +configuration: `ListProfiles`, `ListPrompts`, and `ListBackends` return exact +identities, while `ListNamespaces` and `ListCollections` expose resource-bearing +namespaces and effective collection declarations. Inventory does not select +which configuration version the application should deploy. + +## Deployment Selection Remains With The Application + +Applications decide which checked-in, embedded, operator-supplied, or +programmatic inputs to compose for each deployment. They also decide when to +construct a replacement engine after changing those sources. Promptkit does +not fetch catalog updates, select provider availability, validate upstream +catalog release status, or automatically migrate arbitrary consumer files. +The maintained [offline collection example](../../examples/go-library/prepare/main.go) +shows explicit source selection, local imports, layering, a scoped view, and +operator inventory without a provider call. diff --git a/docs/consumers/pkg-promptkit.md b/docs/consumers/pkg-promptkit.md index caa9f16..7d5c862 100644 --- a/docs/consumers/pkg-promptkit.md +++ b/docs/consumers/pkg-promptkit.md @@ -15,6 +15,12 @@ Import the package as: import "gitea.maximumdirect.net/eric/promptkit" ``` +The maintained [offline preparation example](../../examples/go-library/prepare/main.go) +demonstrates explicit collection composition and inventory. Applications +adapting older provider or versioned-prompt configuration can use the +[configuration migration guide](configuration-migration.md) for the required +caller-owned choices. + The following Go fragments are illustrative and omit surrounding package, import, and error-handling code. Use the maintained examples for complete programs. diff --git a/docs/formats.md b/docs/formats.md index 3748e8c..97cf387 100644 --- a/docs/formats.md +++ b/docs/formats.md @@ -52,7 +52,7 @@ Schema and request artifact reads still happen during preparation. Use `InspectConfiguration` for an explicit effective-configuration or provenance report, `InspectProfile` for backend-completed profile settings, and preparation for request-specific rendered details. For identity discovery before inspection, -see the consumer guide's [profile and prompt listing](consumers/pkg-promptkit.md#discover-available-profiles-and-prompts). +see the consumer guide's [resource and collection listing](consumers/pkg-promptkit.md#discover-available-resources-and-collections). ## Prompt Definitions diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index e36daf4..2e37734 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -745,6 +745,18 @@ Validation: `GOWORK=off python3 scripts/check-examples.py`. Assemble the maintai collection example through real ConfigKit and PromptKit. Keep copied material's actual attribution obligations aligned if any is introduced. +Completion: the offline preparation example now explicitly composes `support` +and `review` collections, including an imported source-relative message file, +sibling profile inheritance, and a later partial profile override. It prepares +an explicitly selected prompt/profile through a scoped view and reports global +namespace/collection inventories plus a namespace-filtered prompt listing. The +synthetic entries demonstrate present and absent source/release metadata; the +run and observe examples remain focused on their existing workflows. Added a +consumer migration guide with the format/API changes, identity mappings, +caller-owned optional profiles, and deployment-selection responsibility. +Validation passed: `GOWORK=off go test ./...`, `GOWORK=off go vet ./...`, +`GOWORK=off python3 scripts/check-examples.py`, and `git diff --check`. + ## Stage 17: Finalize Canonical Contracts And Migration Documentation Scope: documentation consistency after the APIs and examples exist. diff --git a/examples/go-library/prepare/collections/review/collection.yaml b/examples/go-library/prepare/collections/review/collection.yaml new file mode 100644 index 0000000..cd7da05 --- /dev/null +++ b/examples/go-library/prepare/collections/review/collection.yaml @@ -0,0 +1,17 @@ +backends: + offline: + endpoint: https://review.example.invalid/v1 +profiles: + audit: + backend: review/offline + model: review-model +prompts: + check: + default_profile: review/audit + description: Inspect a review item without contacting a model provider. + messages: + - role: user + content: Review the supplied synthetic example. + output: + format: text + validation_mode: none diff --git a/examples/go-library/prepare/collections/support/collection.yaml b/examples/go-library/prepare/collections/support/collection.yaml new file mode 100644 index 0000000..0b4d672 --- /dev/null +++ b/examples/go-library/prepare/collections/support/collection.yaml @@ -0,0 +1,5 @@ +version: "1" +configkit: + imports: + - path: fragments/resources.yaml +release_version: "1.4.0" diff --git a/examples/go-library/prepare/prompt.yaml b/examples/go-library/prepare/collections/support/fragments/resources.yaml similarity index 54% rename from examples/go-library/prepare/prompt.yaml rename to examples/go-library/prepare/collections/support/fragments/resources.yaml index 0dea6f4..d4f36fb 100644 --- a/examples/go-library/prepare/prompt.yaml +++ b/examples/go-library/prepare/collections/support/fragments/resources.yaml @@ -1,7 +1,18 @@ +backends: + offline: + endpoint: https://example.invalid/v1 +profiles: + base: + backend: support/offline + model: offline-model + timeout_seconds: 90 + fast: + extends: base + timeout_seconds: 60 prompts: - example.prepare: + prepare: version: "1.0.0" - default_profile: offline-example + default_profile: support/fast description: Prepare a prompt without contacting a model provider. inputs: - name: note @@ -11,7 +22,7 @@ prompts: - role: system content: Summarize the note in one sentence. - role: user - content: '{{input "note"}}' + content_file: ../messages/note.md output: format: text validation_mode: basic diff --git a/examples/go-library/prepare/collections/support/messages/note.md b/examples/go-library/prepare/collections/support/messages/note.md new file mode 100644 index 0000000..d836856 --- /dev/null +++ b/examples/go-library/prepare/collections/support/messages/note.md @@ -0,0 +1 @@ +{{input "note"}} diff --git a/examples/go-library/prepare/collections/support/operator-overrides.yaml b/examples/go-library/prepare/collections/support/operator-overrides.yaml new file mode 100644 index 0000000..acac9fc --- /dev/null +++ b/examples/go-library/prepare/collections/support/operator-overrides.yaml @@ -0,0 +1,4 @@ +# A later input intentionally replaces only the inherited profile timeout. +profiles: + fast: + timeout_seconds: 30 diff --git a/examples/go-library/prepare/main.go b/examples/go-library/prepare/main.go index 1e81c8c..69dfa65 100644 --- a/examples/go-library/prepare/main.go +++ b/examples/go-library/prepare/main.go @@ -11,33 +11,72 @@ import ( ) type summary struct { - PromptID string `json:"prompt_id"` - PromptVersion *string `json:"prompt_version,omitempty"` - SelectedProfile string `json:"selected_profile"` - Model string `json:"model"` - MessageCount int `json:"message_count"` + PromptID string `json:"prompt_id"` + PromptVersion *string `json:"prompt_version,omitempty"` + SelectedProfile string `json:"selected_profile"` + Model string `json:"model"` + MessageCount int `json:"message_count"` + ProfileTimeout int `json:"profile_timeout_seconds"` + Namespaces []string `json:"namespaces"` + Collections []collectionSummary `json:"collections"` + ScopedPrompts []string `json:"scoped_prompts"` + ReviewPrompts []string `json:"review_prompts"` +} + +type collectionSummary struct { + Namespace string `json:"namespace"` + ReleaseVersion *string `json:"release_version"` } func main() { - endpoint, model := "https://example.invalid/v1", "offline-model" + support, err := promptkit.CollectionInput( + "support", + configkit.File("examples/go-library/prepare/collections/support/collection.yaml"), + ) + if err != nil { + exit(err) + } + review, err := promptkit.CollectionInput( + "review", + configkit.File("examples/go-library/prepare/collections/review/collection.yaml"), + ) + if err != nil { + exit(err) + } + override, err := promptkit.CollectionInput( + "support", + configkit.File("examples/go-library/prepare/collections/support/operator-overrides.yaml"), + ) + if err != nil { + exit(err) + } engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{ - Underlays: []configkit.Underlay{{Inputs: []configkit.Input{{Source: promptkit.ProfileSource("example profile", map[string]promptkit.ProfileConfig{ - "offline-example": {Endpoint: &endpoint, Model: &model}, - })}}}}, - Main: &configkit.Input{Source: configkit.File("examples/go-library/prepare/prompt.yaml")}, + Underlays: []configkit.Underlay{ + {Inputs: []configkit.Input{support, review}}, + {Inputs: []configkit.Input{override}}, + }, }}) if err != nil { exit(err) } + ctx := context.Background() + scoped, err := engine.WithNamespaces(promptkit.Namespaces{ + Prompt: "support", Profile: "support", Backend: "support", + }) + if err != nil { + exit(err) + } view, err := engine.InspectConfiguration() if err != nil { exit(err) } - if _, found := view.Explain(configkit.Keys("profiles", "offline-example", "model")); !found { - exit(fmt.Errorf("configured profile model was not found")) + if _, found := view.Explain(configkit.Keys( + "collections", "support", "profiles", "fast", "timeout_seconds", + )); !found { + exit(fmt.Errorf("collection profile override was not found")) } - requirements, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{ - PromptID: "example.prepare", + requirements, err := scoped.InspectRequirements(ctx, promptkit.RequirementInspectionRequest{ + PromptID: "prepare", }) if err != nil { exit(err) @@ -49,18 +88,61 @@ func main() { } request := promptkit.RunRequest{ - PromptID: "example.prepare", + PromptID: "prepare", + ProfileID: "fast", Inputs: map[string]promptkit.ArtifactRef{ "note": promptkit.Inline("Ada finished the migration review."), }, } - if err := engine.ValidateRequest(context.Background(), request); err != nil { + if err := scoped.ValidateRequest(ctx, request); err != nil { exit(err) } - prepared, err := engine.Prepare(context.Background(), request) + prepared, err := scoped.Prepare(ctx, request) if err != nil { exit(err) } + profile, err := scoped.InspectProfile(ctx, "fast") + if err != nil { + exit(err) + } + if profile.EffectiveModelParams.TimeoutSeconds != 30 { + exit(fmt.Errorf( + "later profile override was not applied: timeout is %d seconds", + profile.EffectiveModelParams.TimeoutSeconds, + )) + } + namespaces, err := scoped.ListNamespaces(ctx) + if err != nil { + exit(err) + } + collections, err := scoped.ListCollections(ctx) + if err != nil { + exit(err) + } + scopedPrompts, err := scoped.ListPrompts(ctx) + if err != nil { + exit(err) + } + reviewPrompts, err := scoped.ListPrompts(ctx, promptkit.NamespaceFilter{ + Mode: promptkit.NamespaceNamed, Name: "review", + }) + if err != nil { + exit(err) + } + scopedPromptIDs := make([]string, len(scopedPrompts)) + for index := range scopedPrompts { + scopedPromptIDs[index] = scopedPrompts[index].ID + } + reviewPromptIDs := make([]string, len(reviewPrompts)) + for index := range reviewPrompts { + reviewPromptIDs[index] = reviewPrompts[index].ID + } + collectionSummaries := make([]collectionSummary, len(collections)) + for index := range collections { + collectionSummaries[index] = collectionSummary{ + Namespace: collections[index].Namespace, ReleaseVersion: collections[index].ReleaseVersion, + } + } encoder := json.NewEncoder(os.Stdout) encoder.SetIndent("", " ") @@ -70,6 +152,11 @@ func main() { SelectedProfile: prepared.SelectedProfileID, Model: prepared.EffectiveModelParams.Model, MessageCount: len(prepared.Messages), + ProfileTimeout: profile.EffectiveModelParams.TimeoutSeconds, + Namespaces: namespaces, + Collections: collectionSummaries, + ScopedPrompts: scopedPromptIDs, + ReviewPrompts: reviewPromptIDs, }); err != nil { exit(err) } diff --git a/scripts/check-examples.py b/scripts/check-examples.py index 901726c..841bc31 100644 --- a/scripts/check-examples.py +++ b/scripts/check-examples.py @@ -115,12 +115,32 @@ def main(): observed = run_example("observe", environment, failures) if prepared is not None: - expect_equal("prepare", prepared, "prompt_id", "example.prepare", failures) + expect_equal("prepare", prepared, "prompt_id", "support/prepare", failures) expect_equal( - "prepare", prepared, "selected_profile", "offline-example", failures + "prepare", prepared, "selected_profile", "support/fast", failures ) expect_equal("prepare", prepared, "model", "offline-model", failures) expect_positive("prepare", prepared, "message_count", failures) + expect_equal("prepare", prepared, "profile_timeout_seconds", 30, failures) + expect_equal( + "prepare", prepared, "namespaces", ["review", "support"], failures + ) + expect_equal( + "prepare", + prepared, + "collections", + [ + {"namespace": "review", "release_version": None}, + {"namespace": "support", "release_version": "1.4.0"}, + ], + failures, + ) + expect_equal( + "prepare", prepared, "scoped_prompts", ["support/prepare"], failures + ) + expect_equal( + "prepare", prepared, "review_prompts", ["review/check"], failures + ) if result is not None: expect_equal(