Add collection migration example and guide
This commit is contained in:
@@ -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
|
||||
|
||||
165
docs/consumers/configuration-migration.md
Normal file
165
docs/consumers/configuration-migration.md
Normal 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.
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,5 @@
|
||||
version: "1"
|
||||
configkit:
|
||||
imports:
|
||||
- path: fragments/resources.yaml
|
||||
release_version: "1.4.0"
|
||||
@@ -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
|
||||
@@ -0,0 +1 @@
|
||||
{{input "note"}}
|
||||
@@ -0,0 +1,4 @@
|
||||
# A later input intentionally replaces only the inherited profile timeout.
|
||||
profiles:
|
||||
fast:
|
||||
timeout_seconds: 30
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user