Add collection migration example and guide

This commit is contained in:
2026-10-17 06:29:57 +00:00
parent 8669294cf6
commit 29f104fbc0
12 changed files with 356 additions and 26 deletions

View File

@@ -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

View File

@@ -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.

View File

@@ -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.

View File

@@ -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

View File

@@ -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.

View File

@@ -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

View File

@@ -0,0 +1,5 @@
version: "1"
configkit:
imports:
- path: fragments/resources.yaml
release_version: "1.4.0"

View File

@@ -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

View File

@@ -0,0 +1 @@
{{input "note"}}

View File

@@ -0,0 +1,4 @@
# A later input intentionally replaces only the inherited profile timeout.
profiles:
fast:
timeout_seconds: 30

View File

@@ -16,28 +16,67 @@ type summary struct {
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)
}

View File

@@ -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(