Compare commits
7 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 31f2ce3a09 | |||
| fd06e4ca6b | |||
| e63b8de1e9 | |||
| 9354d2b373 | |||
| 01ca5430bd | |||
| ae2179d103 | |||
| a248433d0f |
@@ -33,6 +33,9 @@ boundary and constraints that framework work must preserve.
|
|||||||
|
|
||||||
## Release Guidance
|
## Release Guidance
|
||||||
|
|
||||||
|
Consumers upgrading from `v0.4.0` to `v0.5.0` should read the
|
||||||
|
[v0.5.0 changelog and migration guide](docs/releases/v0.5.0.md).
|
||||||
|
|
||||||
Consumers upgrading from `v0.3.0` to `v0.4.0` should read the
|
Consumers upgrading from `v0.3.0` to `v0.4.0` should read the
|
||||||
[v0.4.0 changelog and adoption guide](docs/releases/v0.4.0.md).
|
[v0.4.0 changelog and adoption guide](docs/releases/v0.4.0.md).
|
||||||
|
|
||||||
|
|||||||
@@ -40,6 +40,36 @@ validation, and default transport behavior. Source discovery, format
|
|||||||
validation, and profile precedence are defined by the
|
validation, and profile precedence are defined by the
|
||||||
[framework format reference](../formats.md).
|
[framework format reference](../formats.md).
|
||||||
|
|
||||||
|
## Supply Embedded Application Defaults
|
||||||
|
|
||||||
|
Use `WithFallbackProfileFS` when an application packages profile definitions
|
||||||
|
that should apply unless an operator provides an ordinary configured profile
|
||||||
|
with the same ID. For example, an application can embed its defaults while
|
||||||
|
continuing to use `ProfileDir` for operator overrides:
|
||||||
|
|
||||||
|
```go
|
||||||
|
//go:embed profiles/*.yaml
|
||||||
|
var applicationProfiles embed.FS
|
||||||
|
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{
|
||||||
|
PromptDir: "prompts",
|
||||||
|
ProfileDir: operatorProfileDir,
|
||||||
|
},
|
||||||
|
promptkit.WithFallbackProfileFS(applicationProfiles, "profiles"),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Keep application-owned profile IDs and definitions in the embedded source.
|
||||||
|
Use the ordinary configured profile source for operator overrides. Leave
|
||||||
|
`operatorProfileDir` empty when the operator did not configure an override
|
||||||
|
directory; a non-empty path names an authoritative higher-precedence source,
|
||||||
|
so an unavailable or unreadable directory is an error rather than a reason to
|
||||||
|
fall back. The
|
||||||
|
[framework format reference](../formats.md#source-and-profile-precedence)
|
||||||
|
owns the exact profile format and lookup order; the
|
||||||
|
[`WithFallbackProfileFS` GoDoc](../../engine.go) owns its option contract and
|
||||||
|
validation rules.
|
||||||
|
|
||||||
## Inspect A Prompt Before Preparation
|
## Inspect A Prompt Before Preparation
|
||||||
|
|
||||||
Use [`Engine.InspectPrompt`](../../engine.go) to check one configured prompt's
|
Use [`Engine.InspectPrompt`](../../engine.go) to check one configured prompt's
|
||||||
@@ -152,8 +182,8 @@ replace execution settings or the complete output contract.
|
|||||||
The [public value GoDoc](../../types.go) defines nil, empty, zero, replacement,
|
The [public value GoDoc](../../types.go) defines nil, empty, zero, replacement,
|
||||||
copy, and credential behavior. The
|
copy, and credential behavior. The
|
||||||
[framework format reference](../formats.md) defines how those request values
|
[framework format reference](../formats.md) defines how those request values
|
||||||
interact with prompt definitions, file-backed profiles, built-ins, schemas,
|
interact with prompt definitions, file-backed and application fallback
|
||||||
and framework defaults.
|
profiles, built-ins, schemas, and framework defaults.
|
||||||
|
|
||||||
For programmatic profiles,
|
For programmatic profiles,
|
||||||
[`OpenAICompatibleProfile`](../../profiles.go) converts ordinary
|
[`OpenAICompatibleProfile`](../../profiles.go) converts ordinary
|
||||||
|
|||||||
@@ -44,98 +44,3 @@ Start with:
|
|||||||
For cross-cutting changes, follow every applicable row. Do not create
|
For cross-cutting changes, follow every applicable row. Do not create
|
||||||
placeholder documents for packages, APIs, or integrations that do not yet
|
placeholder documents for packages, APIs, or integrations that do not yet
|
||||||
exist.
|
exist.
|
||||||
|
|
||||||
## Maintainer-Run Validation
|
|
||||||
|
|
||||||
Promptkit does not currently use hosted CI. Maintainers are responsible for
|
|
||||||
running the documented checks before accepting changes. Run the default Go
|
|
||||||
validation from the Promptkit repository root:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
go test ./...
|
|
||||||
go test -race ./...
|
|
||||||
go vet ./...
|
|
||||||
go build ./...
|
|
||||||
go run ./examples/go-library/prepare
|
|
||||||
```
|
|
||||||
|
|
||||||
Check formatting across every tracked Go file:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
gofmt -l $(git ls-files '*.go')
|
|
||||||
```
|
|
||||||
|
|
||||||
The formatting command must produce no paths. Follow every added or changed
|
|
||||||
Markdown link and confirm its target exists. Finally, check whitespace:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
git diff --check
|
|
||||||
```
|
|
||||||
|
|
||||||
Documentation-only work does not require unrelated new tests, but it still
|
|
||||||
requires link validation and `git diff --check`. Run the Go validation whenever
|
|
||||||
documentation changes commands, examples, generated output, or another
|
|
||||||
behavior checked by the module.
|
|
||||||
|
|
||||||
## Focused Validation
|
|
||||||
|
|
||||||
Use focused checks while iterating, then run the complete validation sequence
|
|
||||||
before accepting the change. The root package supports:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
go test .
|
|
||||||
go vet .
|
|
||||||
go build .
|
|
||||||
```
|
|
||||||
|
|
||||||
Filter tests by name without assuming a fixed internal package layout:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
go test ./... -run 'TestName'
|
|
||||||
```
|
|
||||||
|
|
||||||
Replace `TestName` with a useful regular expression. Target only paths that
|
|
||||||
exist, and consult the internal component overview for their owning
|
|
||||||
documentation. A filtered or package-specific run does not replace the
|
|
||||||
complete repository validation.
|
|
||||||
|
|
||||||
## Coordinated Work With Scriptorium
|
|
||||||
|
|
||||||
Promptkit and Scriptorium must remain independently valid. For temporary local
|
|
||||||
integration, use either a Go workspace outside both repositories or an
|
|
||||||
uncommitted replacement in the consuming module.
|
|
||||||
|
|
||||||
If the repositories are sibling directories, run the workspace commands from
|
|
||||||
their parent directory:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
go work init ./promptkit ./scriptorium
|
|
||||||
go work sync
|
|
||||||
```
|
|
||||||
|
|
||||||
Use the workspace only for coordinated local checks. From the same parent
|
|
||||||
directory, remove it when finished:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
rm -f go.work go.work.sum
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively, from the Scriptorium repository root, temporarily point its
|
|
||||||
Promptkit dependency at the sibling checkout:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
go mod edit -replace gitea.maximumdirect.net/eric/promptkit=../promptkit
|
|
||||||
```
|
|
||||||
|
|
||||||
After coordinated checks, remove the replacement and reconcile module
|
|
||||||
metadata:
|
|
||||||
|
|
||||||
```sh
|
|
||||||
go mod edit -dropreplace gitea.maximumdirect.net/eric/promptkit
|
|
||||||
go mod tidy
|
|
||||||
```
|
|
||||||
|
|
||||||
Never commit `go.work`, `go.work.sum`, or a local filesystem `replace`
|
|
||||||
directive. Before committing in either repository, inspect its module files and
|
|
||||||
working tree independently. Published consumer versions must depend on a tagged
|
|
||||||
Promptkit version, not a workspace, local replacement, or unpublished commit.
|
|
||||||
|
|||||||
@@ -188,25 +188,27 @@ they also cannot collide with the standard fields listed in the
|
|||||||
|
|
||||||
Execution settings resolve in this order:
|
Execution settings resolve in this order:
|
||||||
|
|
||||||
1. framework defaults;
|
1. the framework timeout baseline;
|
||||||
2. the selected backend, when the profile names one;
|
2. the selected backend, when the profile names one;
|
||||||
3. the selected profile; and
|
3. the selected profile; and
|
||||||
4. request `ExecutionTargetOverride` values.
|
4. request `ExecutionTargetOverride` values.
|
||||||
|
|
||||||
The framework defaults are:
|
The framework baseline is:
|
||||||
|
|
||||||
| Setting | Default |
|
| Setting | Default |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `temperature` | `0` |
|
| `temperature` | Unspecified and omitted from compatible provider requests unless a profile or runtime override selects it. |
|
||||||
| `max_tokens` | `0` |
|
| `max_tokens` | Unspecified and omitted from compatible provider requests unless a profile or runtime override selects it. |
|
||||||
| `top_p` | `1` |
|
| `top_p` | Unspecified and omitted from compatible provider requests unless a profile or runtime override selects it. |
|
||||||
| `timeout_seconds` | `600` |
|
| `timeout_seconds` | `600` |
|
||||||
|
|
||||||
Numeric zero in a file or in-memory profile means that the profile does not
|
Numeric zero in a file or in-memory profile does not select a numeric value.
|
||||||
replace the framework default. Numeric request overrides use pointers, so an
|
For `temperature`, `max_tokens`, and `top_p`, it leaves the provider control
|
||||||
explicit zero is preserved. In particular, an explicit request
|
unspecified. For `timeout_seconds`, it retains the framework deadline. Numeric
|
||||||
`timeout_seconds` of zero disables the per-generation deadline while leaving
|
request overrides use pointers, so an explicit zero is retained and sent to
|
||||||
the caller context and transport timeout intact.
|
compatible providers. In particular, an explicit request `timeout_seconds` of
|
||||||
|
zero disables the per-generation deadline while leaving the caller context and
|
||||||
|
transport timeout intact.
|
||||||
|
|
||||||
Non-empty profile strings replace backend defaults, and non-empty request
|
Non-empty profile strings replace backend defaults, and non-empty request
|
||||||
strings replace both. Request reasoning is the exception: a nil
|
strings replace both. Request reasoning is the exception: a nil
|
||||||
@@ -231,14 +233,18 @@ default.
|
|||||||
Profile sources resolve matching IDs in this order:
|
Profile sources resolve matching IDs in this order:
|
||||||
|
|
||||||
1. in-memory profiles supplied with `WithProfiles`;
|
1. in-memory profiles supplied with `WithProfiles`;
|
||||||
2. a profile file, `fs.FS`, or configured profile directory; and
|
2. the ordinary configured source selected by a profile file, `fs.FS`, or
|
||||||
3. embedded built-in profiles.
|
configured profile directory;
|
||||||
|
3. application fallback profiles supplied with `WithFallbackProfileFS`; and
|
||||||
|
4. embedded built-in profiles.
|
||||||
|
|
||||||
A higher-precedence source falls back only when the profile is absent. An
|
A profile source supplies a complete definition; definitions and their fields
|
||||||
invalid matching profile is an error and does not fall back. In-memory
|
are not merged across sources. A higher-precedence source falls back only when
|
||||||
`Profile` values follow the same ranges as YAML profiles. They use
|
the requested profile ID is absent. An invalid matching profile is an error and
|
||||||
`APIKeyRequired` for request-scoped credentials instead of `api_key_env`.
|
does not fall back. In-memory `Profile` values follow the same ranges as YAML
|
||||||
Preparation and exact profile inspection use this same source precedence.
|
profiles. They use `APIKeyRequired` for request-scoped credentials instead of
|
||||||
|
`api_key_env`. Preparation and exact profile inspection use this same source
|
||||||
|
precedence.
|
||||||
|
|
||||||
## Built-In Profile Catalog
|
## Built-In Profile Catalog
|
||||||
|
|
||||||
@@ -246,8 +252,8 @@ Every built-in selects the `openrouter` backend. The engine's built-in backend
|
|||||||
registry supplies `https://openrouter.ai/api/v1` and the environment-variable
|
registry supplies `https://openrouter.ai/api/v1` and the environment-variable
|
||||||
name `OPENROUTER_API_KEY`, so individual profiles contain only model and
|
name `OPENROUTER_API_KEY`, so individual profiles contain only model and
|
||||||
generation settings. Built-in profile files do not repeat those connection
|
generation settings. Built-in profile files do not repeat those connection
|
||||||
values. A custom or in-memory profile with the same profile ID takes
|
values. A configured, application fallback, or in-memory profile with the same
|
||||||
precedence.
|
profile ID takes precedence.
|
||||||
|
|
||||||
| Provider | ID | Model |
|
| Provider | ID | Model |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
|
|||||||
@@ -53,8 +53,9 @@ never also sent as a session header.
|
|||||||
|
|
||||||
The client conditionally includes:
|
The client conditionally includes:
|
||||||
|
|
||||||
- `temperature`, `max_tokens`, and `top_p` when non-zero or explicitly
|
- `temperature`, `max_tokens`, and `top_p` only when selected by a profile or
|
||||||
present;
|
runtime override, including an explicit runtime zero; they are absent when
|
||||||
|
unspecified;
|
||||||
- non-empty `service_tier` and effective `reasoning_effort`; an explicitly
|
- non-empty `service_tier` and effective `reasoning_effort`; an explicitly
|
||||||
disabled reasoning setting is empty and therefore omitted; and
|
disabled reasoning setting is empty and therefore omitted; and
|
||||||
- `response_format` for JSON Schema structured output, including its name,
|
- `response_format` for JSON Schema structured output, including its name,
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ contributor workflow and validation.
|
|||||||
|
|
||||||
| Component | Implemented responsibility | References |
|
| Component | Implemented responsibility | References |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Root `promptkit` package | Provides the supported engine facade, source, backend-registration, and injection options, public request, result, prompt-inspection, and profile-inspection values, opaque prepared-execution handles, profile construction, extension interfaces, value conversion, redacted formatting, typed capacity errors, public error mapping, and engine-local assembly. | [Package GoDoc](../../doc.go), [prepared execution](../../prepared_execution.go), [backend API](../../backends.go), [engine assembly](../../engine.go) |
|
| Root `promptkit` package | Provides the supported engine facade, source, backend-registration, and injection options, public request, result, prompt-inspection, and profile-inspection values, opaque prepared-execution handles, profile construction, extension interfaces, value conversion, redacted formatting, typed capacity errors, public error mapping, and engine-local profile-source assembly including application fallbacks. | [Package GoDoc](../../doc.go), [prepared execution](../../prepared_execution.go), [backend API](../../backends.go), [engine assembly](../../engine.go) |
|
||||||
| `examples/go-library/prepare` | Demonstrates an offline downstream consumer using a prompt file, in-memory profile, inline input, and `Prepare`. It is not a public library package. | [Example program](../../examples/go-library/prepare/main.go) |
|
| `examples/go-library/prepare` | Demonstrates an offline downstream consumer using a prompt file, in-memory profile, inline input, and `Prepare`. It is not a public library package. | [Example program](../../examples/go-library/prepare/main.go) |
|
||||||
| `examples/go-library/run` | Demonstrates an offline downstream consumer using a prompt file, in-memory profile, inline input, an injected deterministic model client, and `Run`. It is not a public library package. | [Example program](../../examples/go-library/run/main.go) |
|
| `examples/go-library/run` | Demonstrates an offline downstream consumer using a prompt file, in-memory profile, inline input, an injected deterministic model client, and `Run`. It is not a public library package. | [Example program](../../examples/go-library/run/main.go) |
|
||||||
| `internal/backend` | Constructs each engine's immutable registry from the built-in OpenRouter definition and consumer additions, validates and defensively copies definitions through the shared JSON-value package, and consumes the LLM-owned OpenAI-compatible reserved request-field rule. | [Backend registry](../../internal/backend/registry.go) |
|
| `internal/backend` | Constructs each engine's immutable registry from the built-in OpenRouter definition and consumer additions, validates and defensively copies definitions through the shared JSON-value package, and consumes the LLM-owned OpenAI-compatible reserved request-field rule. | [Backend registry](../../internal/backend/registry.go) |
|
||||||
@@ -22,7 +22,7 @@ contributor workflow and validation.
|
|||||||
| `internal/jsonvalue` | Validates and deeply copies JSON-compatible extra-parameter and prepared-schema trees while preserving supported concrete value types. | [JSON values](../../internal/jsonvalue/jsonvalue.go) |
|
| `internal/jsonvalue` | Validates and deeply copies JSON-compatible extra-parameter and prepared-schema trees while preserving supported concrete value types. | [JSON values](../../internal/jsonvalue/jsonvalue.go) |
|
||||||
| `internal/promptdef` | Loads strictly decoded, validated prompt definitions from filesystem and `fs.FS` sources, including version selection and contained file-backed message content. | [Framework formats](../formats.md), [prompt-definition repository](../../internal/promptdef/filesystem_repository.go) |
|
| `internal/promptdef` | Loads strictly decoded, validated prompt definitions from filesystem and `fs.FS` sources, including version selection and contained file-backed message content. | [Framework formats](../formats.md), [prompt-definition repository](../../internal/promptdef/filesystem_repository.go) |
|
||||||
| `internal/profile` | Loads strictly decoded, validated execution profiles, including backend selection, from filesystem and `fs.FS` sources and composes repositories with error-preserving fallback. | [Framework formats](../formats.md), [profile repositories](../../internal/profile/filesystem_repository.go) |
|
| `internal/profile` | Loads strictly decoded, validated execution profiles, including backend selection, from filesystem and `fs.FS` sources and composes repositories with error-preserving fallback. | [Framework formats](../formats.md), [profile repositories](../../internal/profile/filesystem_repository.go) |
|
||||||
| `internal/profile/builtin` | Embeds the built-in profile catalog, whose entries select OpenRouter, and combines it with an optional primary repository. | [Built-in catalog](../formats.md#built-in-profile-catalog), [repository](../../internal/profile/builtin/repository.go) |
|
| `internal/profile/builtin` | Embeds the built-in profile catalog, whose entries select OpenRouter. | [Built-in catalog](../formats.md#built-in-profile-catalog), [repository](../../internal/profile/builtin/repository.go) |
|
||||||
| `internal/prompt` | Renders prompt messages from Go templates with artifact, variable, session, and cache-control data. | [Go-template renderer](../../internal/prompt/go_renderer.go) |
|
| `internal/prompt` | Renders prompt messages from Go templates with artifact, variable, session, and cache-control data. | [Go-template renderer](../../internal/prompt/go_renderer.go) |
|
||||||
| `internal/artifact` | Resolves ordinary inline and unrestricted caller-selected file references into copied artifacts with metadata and hashes. | [Internal sources and validation](sources.md) |
|
| `internal/artifact` | Resolves ordinary inline and unrestricted caller-selected file references into copied artifacts with metadata and hashes. | [Internal sources and validation](sources.md) |
|
||||||
| `internal/validate` | Validates basic, JSON, and JSON Schema output using operating-system filesystem or `fs.FS` schema sources and creates frozen validation plans for prepared execution. | [Framework formats](../formats.md#schemas), [internal sources and validation](sources.md) |
|
| `internal/validate` | Validates basic, JSON, and JSON Schema output using operating-system filesystem or `fs.FS` schema sources and creates frozen validation plans for prepared execution. | [Framework formats](../formats.md#schemas), [internal sources and validation](sources.md) |
|
||||||
|
|||||||
@@ -28,26 +28,30 @@ duplicate detection, and source containment:
|
|||||||
## Profiles And Built-Ins
|
## Profiles And Built-Ins
|
||||||
|
|
||||||
`internal/profile` loads and validates execution profiles from an
|
`internal/profile` loads and validates execution profiles from an
|
||||||
operating-system filesystem or an `fs.FS`. It supports a primary repository
|
operating-system filesystem or an `fs.FS`. Its overlay repository consults the
|
||||||
with fallback only when the primary reports that a profile is absent. Strict
|
next repository only when the higher-precedence repository reports that a
|
||||||
YAML decoding recognizes the optional `backend` field, trims its value, and
|
profile is absent. Strict YAML decoding recognizes the optional `backend`
|
||||||
requires a model plus at least one non-blank backend or endpoint. Loading does
|
field, trims its value, and requires a model plus at least one non-blank
|
||||||
not check registry membership because the available registry belongs to the
|
backend or endpoint. Loading does not check registry membership because the
|
||||||
assembled engine; the runner checks membership during preparation and exact
|
available registry belongs to the assembled engine; the runner checks
|
||||||
profile inspection.
|
membership during preparation and exact profile inspection.
|
||||||
|
|
||||||
|
The root engine assembles profile repositories in precedence order: in-memory
|
||||||
|
profiles, one ordinary configured source, an application fallback source, then
|
||||||
|
the embedded built-in catalog. An explicit file or `fs.FS` profile source
|
||||||
|
replaces `Config.ProfileDir` within the ordinary configured-source category.
|
||||||
|
|
||||||
Exact profile inspection performs one point-in-time lookup through those
|
Exact profile inspection performs one point-in-time lookup through those
|
||||||
profile sources and checks the resolved target without reading prompt, input,
|
profile sources and checks the resolved target without reading prompt, input,
|
||||||
or schema sources. It does not retain that lookup for a later execution.
|
or schema sources. It does not retain that lookup for a later execution.
|
||||||
|
|
||||||
`internal/profile/builtin` embeds the maintained built-in profile catalog and
|
`internal/profile/builtin` embeds the maintained built-in profile catalog.
|
||||||
can place a caller-selected repository ahead of that catalog. Every embedded
|
Every embedded profile selects `openrouter` and inherits its endpoint and
|
||||||
profile selects `openrouter` and inherits its endpoint and credential
|
credential environment-variable name from the built-in backend registry rather
|
||||||
environment-variable name from the built-in backend registry rather than
|
than repeating those values. Profile loading and overlay behavior are owned by
|
||||||
repeating those values. Profile behavior is owned by the
|
the [profile repository tests](../../internal/profile/repository_test.go),
|
||||||
[profile repository tests](../../internal/profile/repository_test.go), while
|
while catalog completeness, the backend-selection invariant, and duplicate IDs
|
||||||
catalog completeness, the backend-selection invariant, duplicate IDs, and
|
are owned by the
|
||||||
overlay behavior are owned by the
|
|
||||||
[built-in repository tests](../../internal/profile/builtin/repository_test.go).
|
[built-in repository tests](../../internal/profile/builtin/repository_test.go).
|
||||||
|
|
||||||
## Ordinary Artifacts
|
## Ordinary Artifacts
|
||||||
|
|||||||
@@ -126,9 +126,9 @@ later request must provide a direct credential. Environment-variable names may
|
|||||||
be reported, but inspection does not read credential values or require the
|
be reported, but inspection does not read credential values or require the
|
||||||
named variable to be populated.
|
named variable to be populated.
|
||||||
|
|
||||||
Inspection applies the ordinary configured and built-in profile precedence and
|
Inspection applies the engine's profile source precedence and resolves any
|
||||||
resolves any selected backend. It does not load a prompt, render content,
|
selected backend. It does not load a prompt, render content, reserve capacity,
|
||||||
reserve capacity, or contact a model.
|
or contact a model.
|
||||||
|
|
||||||
See the
|
See the
|
||||||
[profile-inspection consumer guide](../consumers/pkg-promptkit.md#inspect-a-profile-before-prompt-work)
|
[profile-inspection consumer guide](../consumers/pkg-promptkit.md#inspect-a-profile-before-prompt-work)
|
||||||
|
|||||||
125
docs/releases/v0.5.0.md
Normal file
125
docs/releases/v0.5.0.md
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
# Promptkit v0.5.0
|
||||||
|
|
||||||
|
This supplemental changelog and migration guide summarizes the consumer-facing
|
||||||
|
changes from `v0.4.0` to `v0.5.0`. The annotated `v0.5.0` tag is the
|
||||||
|
authoritative release record. Exact current contracts belong to the linked
|
||||||
|
GoDoc and durable documentation.
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
`v0.5.0` makes provider requests less prescriptive and adds an application
|
||||||
|
fallback layer for profile definitions:
|
||||||
|
|
||||||
|
- unset optional provider controls are omitted from OpenAI-compatible request
|
||||||
|
bodies instead of being populated with framework values; and
|
||||||
|
- `WithFallbackProfileFS` lets an application package profile defaults that
|
||||||
|
operators can override through the existing ordinary profile sources.
|
||||||
|
|
||||||
|
These changes let compatible providers apply their own model defaults while
|
||||||
|
giving applications stable embedded profile IDs without weakening operator
|
||||||
|
configuration precedence.
|
||||||
|
|
||||||
|
## Compatibility
|
||||||
|
|
||||||
|
The release adds one public function and removes no public declaration.
|
||||||
|
Existing source code should continue to compile.
|
||||||
|
|
||||||
|
There is one intentional behavior change: when no profile or runtime override
|
||||||
|
selects `top_p`, Promptkit no longer sends the former framework value of `1`.
|
||||||
|
It omits `top_p` and lets the provider choose its behavior. Unset
|
||||||
|
`temperature` and `max_tokens` are likewise omitted. Explicit nonzero profile
|
||||||
|
values and runtime values—including explicit runtime zero values—retain their
|
||||||
|
precedence and wire effect.
|
||||||
|
|
||||||
|
Consumers that relied on Promptkit always sending `top_p: 1` should add that
|
||||||
|
value to the relevant profile or runtime override before upgrading. Consumers
|
||||||
|
that did not rely on the implicit sampling value require no migration.
|
||||||
|
|
||||||
|
Application fallback profiles are opt-in. Engines that do not call
|
||||||
|
`WithFallbackProfileFS` retain the previous profile-source behavior.
|
||||||
|
|
||||||
|
## Upgrade
|
||||||
|
|
||||||
|
Update the module dependency with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
go get gitea.maximumdirect.net/eric/promptkit@v0.5.0
|
||||||
|
go mod tidy
|
||||||
|
```
|
||||||
|
|
||||||
|
Run the consuming project's ordinary and race-enabled tests after upgrading.
|
||||||
|
If request payloads or model behavior are asserted in fixtures, review them for
|
||||||
|
the optional-parameter omission described below.
|
||||||
|
|
||||||
|
## Omitted Optional Provider Controls
|
||||||
|
|
||||||
|
The built-in OpenAI-compatible client now includes `temperature`,
|
||||||
|
`max_tokens`, and `top_p` only when a profile or runtime override selects the
|
||||||
|
value. An explicit runtime zero remains present because runtime override
|
||||||
|
pointers distinguish zero from an unspecified value.
|
||||||
|
|
||||||
|
Promptkit's positive generation deadline remains a framework concern and is
|
||||||
|
not a provider request-body default. Required request fields, session IDs,
|
||||||
|
structured output, reasoning selection, credentials, and explicit extra
|
||||||
|
parameters retain their existing behavior.
|
||||||
|
|
||||||
|
See the [framework default and precedence reference](../formats.md#defaults-and-overrides),
|
||||||
|
the [`ExecutionTargetOverride` GoDoc](../../types.go), and the
|
||||||
|
[OpenAI-compatible request-body contract](../integrations/openai-compatible-chat.md#request-body)
|
||||||
|
for current details.
|
||||||
|
|
||||||
|
## Embedded Application Fallback Profiles
|
||||||
|
|
||||||
|
Applications can package ordinary profile YAML in an `fs.FS` and register it
|
||||||
|
as a fallback source:
|
||||||
|
|
||||||
|
```go
|
||||||
|
//go:embed profiles/*.yaml
|
||||||
|
var applicationProfiles embed.FS
|
||||||
|
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{
|
||||||
|
PromptDir: "prompts",
|
||||||
|
ProfileDir: operatorProfileDir,
|
||||||
|
},
|
||||||
|
promptkit.WithFallbackProfileFS(applicationProfiles, "profiles"),
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Leave `operatorProfileDir` empty when no operator source is configured. A
|
||||||
|
configured ordinary source is authoritative: a matching definition overrides
|
||||||
|
the application fallback, while a read or validation failure remains an error
|
||||||
|
instead of silently reaching a lower layer.
|
||||||
|
|
||||||
|
Profile definitions resolve in this order:
|
||||||
|
|
||||||
|
1. in-memory profiles supplied with `WithProfiles`;
|
||||||
|
2. the ordinary configured source selected by `WithProfileFile`,
|
||||||
|
`WithProfileFS`, or `Config.ProfileDir`;
|
||||||
|
3. the application source supplied with `WithFallbackProfileFS`; and
|
||||||
|
4. Promptkit's embedded built-in profiles.
|
||||||
|
|
||||||
|
Only an absent profile ID falls through. Sources provide complete profiles and
|
||||||
|
do not merge fields. Loading remains lazy, and the new source uses the existing
|
||||||
|
strict profile YAML and credential rules.
|
||||||
|
|
||||||
|
See the
|
||||||
|
[embedded-default consumer guidance](../consumers/pkg-promptkit.md#supply-embedded-application-defaults),
|
||||||
|
the [`WithFallbackProfileFS` GoDoc](../../engine.go), and the
|
||||||
|
[profile source reference](../formats.md#source-and-profile-precedence) for
|
||||||
|
current details.
|
||||||
|
|
||||||
|
## Public API Changes
|
||||||
|
|
||||||
|
The release adds:
|
||||||
|
|
||||||
|
- `WithFallbackProfileFS`.
|
||||||
|
|
||||||
|
No public declaration was removed or changed.
|
||||||
|
|
||||||
|
## Consumer Action
|
||||||
|
|
||||||
|
- Review any workflow that depended on Promptkit's implicit `top_p: 1` and
|
||||||
|
configure the value explicitly when required.
|
||||||
|
- Optionally adopt `WithFallbackProfileFS` when an application should package
|
||||||
|
overridable profile defaults.
|
||||||
|
- Run consumer tests after updating the module dependency.
|
||||||
82
docs/roadmap/deferred.md
Normal file
82
docs/roadmap/deferred.md
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
# Deferred Feature Ideas
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
This document catalogs feature ideas that remain potentially useful but have
|
||||||
|
been deliberately postponed. These ideas are not awaiting ordinary selection
|
||||||
|
from the [future feature catalog](future.md); each has a stated reason to wait
|
||||||
|
and should be reconsidered only when its trigger becomes relevant.
|
||||||
|
|
||||||
|
Deferred entries are not commitments, schedules, active implementation plans,
|
||||||
|
or descriptions of current behavior. When an entry is reactivated, move it to
|
||||||
|
`future.md` for evaluation or directly into a focused roadmap after its open
|
||||||
|
design dependencies have been resolved.
|
||||||
|
|
||||||
|
## Deferred Ideas
|
||||||
|
|
||||||
|
### Semantic Execution-Target Fingerprints
|
||||||
|
|
||||||
|
**Reason for deferral:** A stable digest requires a deliberate semantic-
|
||||||
|
equality and versioning design. Notarius can safely use conservative source
|
||||||
|
hashes and a Promptkit release marker today, while Weatherreporter does not
|
||||||
|
currently reuse LLM-dependent checkpoints.
|
||||||
|
|
||||||
|
Promptkit could expose an opaque equality value for a resolved profile and its
|
||||||
|
effective generation target. This would let checkpointing consumers detect
|
||||||
|
generation-affecting configuration changes without hashing YAML presentation
|
||||||
|
or depending on Promptkit's built-in catalog layout.
|
||||||
|
|
||||||
|
The digest should change with semantically relevant state such as the resolved
|
||||||
|
model, endpoint, backend routing identity, request defaults, extra parameters,
|
||||||
|
profile generation settings, and selected built-in profile semantics. It
|
||||||
|
should exclude credential values, concurrency and queue policy, source paths,
|
||||||
|
comments, formatting, and other representation-only changes. Whether a
|
||||||
|
credential environment-variable name affects equality must be decided
|
||||||
|
explicitly. The encoding should remain opaque and internally versioned so
|
||||||
|
Promptkit can deliberately invalidate earlier digests when its resolution
|
||||||
|
semantics change.
|
||||||
|
|
||||||
|
Reconsider this idea when a downstream consumer needs Promptkit-owned
|
||||||
|
checkpoint equality or when a broader semantic identity design is selected.
|
||||||
|
|
||||||
|
### Eager Source Validation
|
||||||
|
|
||||||
|
**Reason for deferral:** Exact prompt and profile inspection may already
|
||||||
|
provide a sufficiently small validation surface. Experience from downstream
|
||||||
|
adoption should establish whether an engine-wide operation would add enough
|
||||||
|
value to justify its broader contract.
|
||||||
|
|
||||||
|
Promptkit could provide an explicit offline operation that discovers and
|
||||||
|
structurally validates configured prompt, profile, and schema sources without
|
||||||
|
model generation. The normal `NewEngine` path would remain lazy.
|
||||||
|
|
||||||
|
An eager operation would need coherent handling for duplicate prompt IDs and
|
||||||
|
versions, strict YAML decoding, referenced content files, profile/backend
|
||||||
|
membership, schema syntax and transitive references, context cancellation,
|
||||||
|
and source-specific public errors. Credential declarations must remain
|
||||||
|
separate from credential values; checking current environment availability,
|
||||||
|
if supported at all, should be an explicit option and must not expose secrets.
|
||||||
|
|
||||||
|
Reconsider this idea after downstream use of `InspectPrompt`,
|
||||||
|
`InspectProfile`, and fixture-based preparation demonstrates a concrete gap.
|
||||||
|
|
||||||
|
### Structured Generation Errors
|
||||||
|
|
||||||
|
**Reason for deferral:** Existing `ErrLLMGenerate` classification, preserved
|
||||||
|
injected-client errors, and prepared execution details currently provide the
|
||||||
|
necessary failure boundary. A typed error should wait for stronger downstream
|
||||||
|
demand and a transport-neutral field design.
|
||||||
|
|
||||||
|
Promptkit could expose safe structured generation context through
|
||||||
|
`errors.As` while preserving `errors.Is(err, ErrLLMGenerate)`. Potential
|
||||||
|
fields include the selected backend ID and model plus an optional HTTP status
|
||||||
|
when the built-in OpenAI-compatible transport supplies one.
|
||||||
|
|
||||||
|
The design must not expose provider response bodies, endpoints, credential
|
||||||
|
environment names or values, request content, or generated content. It should
|
||||||
|
not duplicate prompt and profile provenance already available from a prepared
|
||||||
|
execution, and it must preserve the identity of errors returned by injected
|
||||||
|
clients. Retry and backoff policy remains a consumer responsibility.
|
||||||
|
|
||||||
|
Reconsider this idea when consumers need structured generation diagnostics
|
||||||
|
beyond the existing sentinel, wrapped client error, and preparation record.
|
||||||
@@ -12,6 +12,9 @@ consumer value, and important scope boundaries. Defer API design,
|
|||||||
implementation details, sequencing, and acceptance criteria until an idea is
|
implementation details, sequencing, and acceptance criteria until an idea is
|
||||||
selected.
|
selected.
|
||||||
|
|
||||||
|
Ideas that have been deliberately postponed rather than left available for
|
||||||
|
ordinary selection belong in the [deferred catalog](deferred.md).
|
||||||
|
|
||||||
## Using This Catalog
|
## Using This Catalog
|
||||||
|
|
||||||
- Add an idea when its purpose and likely value can be stated clearly.
|
- Add an idea when its purpose and likely value can be stated clearly.
|
||||||
@@ -23,6 +26,8 @@ selected.
|
|||||||
- When an idea is selected, move its active planning to a focused roadmap or,
|
- When an idea is selected, move its active planning to a focused roadmap or,
|
||||||
when it requires a durable architectural decision, an ADR. Update
|
when it requires a durable architectural decision, an ADR. Update
|
||||||
current-state documentation only when implementation lands.
|
current-state documentation only when implementation lands.
|
||||||
|
- Move an idea to `deferred.md` when maintainers decide to retain it but wait
|
||||||
|
for a stated design dependency, demand signal, or reconsideration trigger.
|
||||||
- Remove ideas that are no longer relevant. Retain a rejected idea only when
|
- Remove ideas that are no longer relevant. Retain a rejected idea only when
|
||||||
its rationale is likely to prevent repeated reconsideration.
|
its rationale is likely to prevent repeated reconsideration.
|
||||||
|
|
||||||
|
|||||||
@@ -1,245 +0,0 @@
|
|||||||
# Notarius PromptKit Wishlist
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
This document records features and interface changes that would be useful
|
|
||||||
additions to PromptKit from the perspective of the maintainers of Notarius, a
|
|
||||||
downstream application that consumes PromptKit.
|
|
||||||
|
|
||||||
PromptKit now provides the capabilities Notarius currently needs. The
|
|
||||||
remaining deferred ideas are optional opportunities to improve checkpointing
|
|
||||||
and operational observability.
|
|
||||||
|
|
||||||
The examples are API sketches intended to communicate the desired capability,
|
|
||||||
not prescriptive names or finalized Go contracts.
|
|
||||||
|
|
||||||
## Priority 1: Atomic Execution With Prepared Details
|
|
||||||
|
|
||||||
**Disposition:** Implemented through [`Engine.PrepareExecution` and
|
|
||||||
`Engine.RunPrepared`](../../engine.go). See the
|
|
||||||
[consumer guidance](../consumers/pkg-promptkit.md#prepare-now-and-execute-the-same-snapshot-later).
|
|
||||||
A separate `RunDetailed` method is not cataloged.
|
|
||||||
|
|
||||||
### Downstream need
|
|
||||||
|
|
||||||
Notarius needs both:
|
|
||||||
|
|
||||||
- the completed `RunResult`; and
|
|
||||||
- the rendered messages, effective output contract, hashes, and other
|
|
||||||
preparation details exposed by `PreparedRun`.
|
|
||||||
|
|
||||||
Notarius uses the prepared details to construct redaction-aware debug bundles
|
|
||||||
and retain enough information to diagnose model behavior.
|
|
||||||
|
|
||||||
### Implemented behavior
|
|
||||||
|
|
||||||
Notarius can prepare one frozen execution snapshot, retain a caller-owned and
|
|
||||||
credential-redacted `Details` value for its debug bundle, and execute the same
|
|
||||||
snapshot through `RunPrepared`. The opaque handle is engine-bound and
|
|
||||||
single-use; an unused handle can be released with `Discard`. The consumer
|
|
||||||
guide and exported GoDoc own the exact lifecycle and failure contracts.
|
|
||||||
|
|
||||||
### Value to Notarius
|
|
||||||
|
|
||||||
This removes duplicate work from PromptKit-backed calls and ensures that
|
|
||||||
retained debug material corresponds atomically to the actual execution.
|
|
||||||
|
|
||||||
## Priority 2: Prompt-Independent Profile Inspection
|
|
||||||
|
|
||||||
**Disposition:** Implemented as
|
|
||||||
[`Engine.InspectProfile`](../../engine.go). See the
|
|
||||||
[consumer guidance](../consumers/pkg-promptkit.md#inspect-a-profile-before-prompt-work).
|
|
||||||
|
|
||||||
### Downstream need
|
|
||||||
|
|
||||||
Notarius validates configured pipeline profile IDs before beginning a run. It
|
|
||||||
needs to determine whether:
|
|
||||||
|
|
||||||
- a profile exists;
|
|
||||||
- its referenced backend is registered;
|
|
||||||
- its execution target can be resolved; and
|
|
||||||
- it declares a credential requirement that the application may need to
|
|
||||||
enforce.
|
|
||||||
|
|
||||||
This validation should not require model generation.
|
|
||||||
|
|
||||||
### Previous integration
|
|
||||||
|
|
||||||
Before profile inspection was available, Notarius constructed a synthetic
|
|
||||||
prompt using `testing/fstest.MapFS`, supplied a dummy transcript, and called
|
|
||||||
`Engine.Prepare` solely to exercise profile and backend resolution.
|
|
||||||
|
|
||||||
### Value to Notarius
|
|
||||||
|
|
||||||
The implemented interface eliminates a synthetic production-only prompt
|
|
||||||
fixture and establishes a direct, supported contract for configuration-time
|
|
||||||
profile and backend validation.
|
|
||||||
|
|
||||||
## Priority 3: Semantic Execution-Target Fingerprints
|
|
||||||
|
|
||||||
**Disposition:** Deferred pending a separate semantic-equality design for
|
|
||||||
resolved execution targets.
|
|
||||||
|
|
||||||
### Downstream need
|
|
||||||
|
|
||||||
Notarius checkpoints model-backed pipeline stages. A checkpoint must not be
|
|
||||||
reused when generation-affecting PromptKit configuration changes.
|
|
||||||
|
|
||||||
Notarius therefore needs a stable equality signal for the effective profile
|
|
||||||
and backend target used by a pipeline.
|
|
||||||
|
|
||||||
### Current integration
|
|
||||||
|
|
||||||
Notarius currently constructs this identity itself from:
|
|
||||||
|
|
||||||
- a manually maintained marker for the PromptKit release and built-in profile
|
|
||||||
catalog;
|
|
||||||
- raw hashes of configured profile files; and
|
|
||||||
- a separate hash of the configured conventional local-backend endpoint.
|
|
||||||
|
|
||||||
This is safe but conservative and coupled to PromptKit details. Raw file
|
|
||||||
hashing also invalidates checkpoints for semantically irrelevant YAML changes,
|
|
||||||
such as comments or formatting.
|
|
||||||
|
|
||||||
### Requested capability
|
|
||||||
|
|
||||||
Expose an opaque semantic digest for a resolved profile and its effective
|
|
||||||
generation target. It could be returned by the proposed profile-resolution
|
|
||||||
API:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type ResolvedProfile struct {
|
|
||||||
ProfileID string
|
|
||||||
BackendID string
|
|
||||||
EffectiveTarget ExecutionTarget
|
|
||||||
ExecutionDigest string
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
Alternatively, PromptKit could expose a dedicated method such as
|
|
||||||
`ProfileExecutionDigest(profileID)`.
|
|
||||||
|
|
||||||
### Desired equality semantics
|
|
||||||
|
|
||||||
The digest should change when generation-affecting state changes, including:
|
|
||||||
|
|
||||||
- resolved model and endpoint;
|
|
||||||
- backend routing identity;
|
|
||||||
- backend request defaults and extra parameters;
|
|
||||||
- profile generation parameters; and
|
|
||||||
- the semantic identity of any selected built-in profile.
|
|
||||||
|
|
||||||
The digest should not incorporate:
|
|
||||||
|
|
||||||
- credential values;
|
|
||||||
- concurrency or queue capacity;
|
|
||||||
- filesystem source paths;
|
|
||||||
- YAML comments or formatting; or
|
|
||||||
- other settings that affect scheduling or source representation without
|
|
||||||
changing the generation target.
|
|
||||||
|
|
||||||
The credential environment-variable name may need to participate if changing
|
|
||||||
it can select a materially different provider account or target. PromptKit
|
|
||||||
should define this deliberately while continuing to exclude the resolved
|
|
||||||
secret value.
|
|
||||||
|
|
||||||
### Design considerations
|
|
||||||
|
|
||||||
- Treat the digest as an opaque equality value rather than a public encoding
|
|
||||||
of internal structures.
|
|
||||||
- Document which categories of change affect equality.
|
|
||||||
- Include a versioned semantic marker internally so PromptKit can deliberately
|
|
||||||
invalidate old digests when its resolution semantics change.
|
|
||||||
- Prefer a per-profile digest over a digest of every profile known to an
|
|
||||||
engine. Notarius generally knows which profiles a resolved pipeline uses.
|
|
||||||
- Do not require consumers to know PromptKit's built-in catalog version.
|
|
||||||
|
|
||||||
### Value to Notarius
|
|
||||||
|
|
||||||
This would let Notarius remove its PromptKit release marker and raw
|
|
||||||
profile-source fingerprinting, reduce unnecessary checkpoint invalidation, and
|
|
||||||
delegate execution-target equality to the component that owns target
|
|
||||||
resolution.
|
|
||||||
|
|
||||||
## Priority 4: Structured Capacity Errors
|
|
||||||
|
|
||||||
**Disposition:** Implemented behavior. See the consumer guide's
|
|
||||||
[Handle Errors](../consumers/pkg-promptkit.md#handle-errors) section.
|
|
||||||
|
|
||||||
### Downstream need
|
|
||||||
|
|
||||||
Notarius translates PromptKit backend-capacity rejection into a
|
|
||||||
provider-neutral application error. When multiple backends are active,
|
|
||||||
operators would benefit from knowing which backend rejected admission without
|
|
||||||
parsing an error string or exposing endpoint details.
|
|
||||||
|
|
||||||
### Implemented behavior
|
|
||||||
|
|
||||||
PromptKit now retains the broad capacity classification while allowing
|
|
||||||
Notarius to obtain the selected backend ID without parsing diagnostic text.
|
|
||||||
The consumer guide owns the application workflow, including retry and backoff
|
|
||||||
policy.
|
|
||||||
|
|
||||||
### Value to Notarius
|
|
||||||
|
|
||||||
This improves operational diagnostics and future metrics while preserving
|
|
||||||
the provider-neutral error boundary used by Notarius.
|
|
||||||
|
|
||||||
## Capabilities PromptKit Already Provides Well
|
|
||||||
|
|
||||||
The current PromptKit boundary is sufficient for Notarius's implemented
|
|
||||||
behavior. In particular, PromptKit already provides:
|
|
||||||
|
|
||||||
- filesystem, `fs.FS`, and programmatic prompt, profile, and schema sources;
|
|
||||||
- offline preparation without model execution;
|
|
||||||
- structured output and content validation;
|
|
||||||
- direct session propagation;
|
|
||||||
- tri-state per-run reasoning overrides;
|
|
||||||
- selected profile, backend, model, endpoint, effective parameters, hashes, and
|
|
||||||
token-usage provenance;
|
|
||||||
- endpoint-only profiles;
|
|
||||||
- the conventional `local` backend helper;
|
|
||||||
- arbitrary engine-scoped `Backend` registrations;
|
|
||||||
- backend authentication environment names, extra parameters, concurrency
|
|
||||||
limits, and queue-capacity policies;
|
|
||||||
- provider-client and artifact-reader extension interfaces;
|
|
||||||
- context cancellation; and
|
|
||||||
- useful public error sentinels, including profile absence and capacity
|
|
||||||
exhaustion.
|
|
||||||
|
|
||||||
The wishlist does not imply that Notarius needs PromptKit to broaden its core
|
|
||||||
responsibilities. It primarily asks for more direct access to information and
|
|
||||||
operations that PromptKit already computes internally.
|
|
||||||
|
|
||||||
## Responsibilities That Should Remain In Notarius
|
|
||||||
|
|
||||||
The following concerns belong to the downstream application and should not
|
|
||||||
move into PromptKit for the sake of Notarius:
|
|
||||||
|
|
||||||
- pipeline staging, dependencies, and generated references;
|
|
||||||
- application-wide scheduling across providers and backends;
|
|
||||||
- module and validation retry policy;
|
|
||||||
- checkpoints, resume, and recomputation;
|
|
||||||
- durable run artifacts and manifests;
|
|
||||||
- D&D prompts, schemas, extractors, validators, and normalizers;
|
|
||||||
- Notarius configuration-file parsing and precedence;
|
|
||||||
- domain-specific prompt-cache prefix policy; and
|
|
||||||
- application-specific redaction, retention, and debug-bundle policy.
|
|
||||||
|
|
||||||
PromptKit's complete `Backend` API already supports custom IDs, multiple local
|
|
||||||
endpoints, authentication, extra parameters, and explicit queue policies.
|
|
||||||
Whether Notarius exposes those capabilities in its own configuration is an
|
|
||||||
application-policy decision, not an upstream PromptKit gap.
|
|
||||||
|
|
||||||
## Suggested Upstream Sequence
|
|
||||||
|
|
||||||
For downstream adoption and any remaining upstream work, the useful order is:
|
|
||||||
|
|
||||||
1. Adopt prepared execution for atomic details and results.
|
|
||||||
2. Add a semantic execution-target digest, preferably alongside profile
|
|
||||||
inspection.
|
|
||||||
3. Use the implemented typed capacity error where backend admission diagnostics
|
|
||||||
are needed.
|
|
||||||
|
|
||||||
The first removes the concrete execution workaround. The second would improve
|
|
||||||
checkpoint correctness and reduce coupling. The third is operational polish.
|
|
||||||
@@ -1,331 +0,0 @@
|
|||||||
# Weatherreporter PromptKit Wishlist
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
This document records features and interface changes that would be useful
|
|
||||||
additions to PromptKit from the perspective of the maintainers of
|
|
||||||
Weatherreporter, a downstream application planning to replace its Scriptorium
|
|
||||||
CLI integration with PromptKit.
|
|
||||||
|
|
||||||
PromptKit now provides the capabilities Weatherreporter needs for the
|
|
||||||
migration. The remaining deferred ideas are optional opportunities to validate
|
|
||||||
configuration earlier and improve durable failure diagnostics.
|
|
||||||
|
|
||||||
The examples are API sketches intended to communicate the desired capability,
|
|
||||||
not prescriptive names or finalized Go contracts. The related
|
|
||||||
[Notarius PromptKit wishlist](notarius-promptkit-wishlist.md) proposes several
|
|
||||||
overlapping features from another downstream consumer's perspective.
|
|
||||||
|
|
||||||
## Priority 1: Executable Preparation Handles
|
|
||||||
|
|
||||||
**Disposition:** Implemented as [`Engine.PrepareExecution` and
|
|
||||||
`Engine.RunPrepared`](../../engine.go). See the
|
|
||||||
[consumer guidance](../consumers/pkg-promptkit.md#prepare-now-and-execute-the-same-snapshot-later).
|
|
||||||
|
|
||||||
### Downstream need
|
|
||||||
|
|
||||||
Weatherreporter treats prompt preparation as a durable preflight boundary. It
|
|
||||||
needs to:
|
|
||||||
|
|
||||||
1. prepare the exact request that will be executed;
|
|
||||||
2. persist a safe preparation record before starting the provider call; and
|
|
||||||
3. execute without reloading or rerendering prompt, profile, schema, or input
|
|
||||||
sources.
|
|
||||||
|
|
||||||
Persisting preflight before generation leaves useful evidence when a provider
|
|
||||||
call fails or the process is interrupted during generation.
|
|
||||||
|
|
||||||
### Implemented behavior
|
|
||||||
|
|
||||||
Weatherreporter can prepare one frozen execution snapshot, persist a
|
|
||||||
caller-owned and credential-redacted `Details` value, and execute that same
|
|
||||||
snapshot through `RunPrepared`. The opaque handle is engine-bound and
|
|
||||||
single-use; an unused handle can be released with `Discard`. The consumer
|
|
||||||
guide and exported GoDoc own the exact lifecycle, credential, cancellation,
|
|
||||||
and capacity contracts.
|
|
||||||
|
|
||||||
### Value to Weatherreporter
|
|
||||||
|
|
||||||
This preserves Weatherreporter's durable preflight behavior, removes duplicate
|
|
||||||
work, eliminates the source-consistency window, and ensures that persisted
|
|
||||||
provenance describes the actual execution.
|
|
||||||
|
|
||||||
## Priority 2: Prompt-Definition Inspection
|
|
||||||
|
|
||||||
**Disposition:** Implemented as
|
|
||||||
[`Engine.InspectPrompt`](../../engine.go). See the
|
|
||||||
[consumer guidance](../consumers/pkg-promptkit.md#inspect-a-prompt-before-preparation).
|
|
||||||
|
|
||||||
### Downstream need
|
|
||||||
|
|
||||||
Weatherreporter has a fixed registry of seven report definitions. Each report
|
|
||||||
selects a prompt ID and one of two output workflows:
|
|
||||||
|
|
||||||
- direct Markdown; or
|
|
||||||
- structured generated text followed by application-owned domain validation
|
|
||||||
and Markdown template rendering.
|
|
||||||
|
|
||||||
Weatherreporter will embed the PromptKit prompt definitions and private
|
|
||||||
response schemas that implement those reports. It needs to validate that the
|
|
||||||
report registry and embedded prompt corpus agree before weather collection or
|
|
||||||
provider execution.
|
|
||||||
|
|
||||||
### Previous integration option
|
|
||||||
|
|
||||||
Before prompt inspection was available, Weatherreporter could maintain
|
|
||||||
synthetic data-package fixtures and call `Engine.Prepare` for every report
|
|
||||||
prompt during tests. Runtime validation could also occur through the ordinary
|
|
||||||
per-report preparation stage.
|
|
||||||
|
|
||||||
This required complete placeholder inputs and profile resolution when the
|
|
||||||
application primarily wanted to inspect prompt identity and declared contracts.
|
|
||||||
|
|
||||||
### Value to Weatherreporter
|
|
||||||
|
|
||||||
The implemented interface lets Weatherreporter directly verify that every
|
|
||||||
report prompt exists, requires the curated `data_package` input, and declares
|
|
||||||
the expected Markdown or JSON Schema output contract. It reduces synthetic
|
|
||||||
test setup and moves failures ahead of weather collection.
|
|
||||||
|
|
||||||
## Priority 3: Prompt-Independent Profile Inspection
|
|
||||||
|
|
||||||
**Disposition:** Implemented as
|
|
||||||
[`Engine.InspectProfile`](../../engine.go). See the
|
|
||||||
[consumer guidance](../consumers/pkg-promptkit.md#inspect-a-profile-before-prompt-work).
|
|
||||||
|
|
||||||
### Downstream need
|
|
||||||
|
|
||||||
Weatherreporter will allow operators to select an external PromptKit profile
|
|
||||||
source and may allow an explicit profile override. It needs to reject a missing
|
|
||||||
profile, unknown backend, or malformed execution target before collecting
|
|
||||||
weather data or writing report artifacts, and to apply its own policy to a
|
|
||||||
reported credential requirement.
|
|
||||||
|
|
||||||
### Value to Weatherreporter
|
|
||||||
|
|
||||||
The implemented interface improves fail-fast configuration validation and gives
|
|
||||||
operator-facing errors direct profile and backend context. It remains optional
|
|
||||||
for the initial migration.
|
|
||||||
|
|
||||||
## Priority 4: Eager Source Validation
|
|
||||||
|
|
||||||
**Disposition:** Deferred until prompt and profile inspection have been used
|
|
||||||
to determine whether a broader engine-wide validation operation is still
|
|
||||||
needed.
|
|
||||||
|
|
||||||
### Downstream need
|
|
||||||
|
|
||||||
PromptKit deliberately defers reading and validating filesystem and `fs.FS`
|
|
||||||
prompt, profile, and schema content until a request needs it. Weatherreporter
|
|
||||||
has a small fixed embedded prompt corpus and one optional external profile
|
|
||||||
source. It would benefit from an explicit offline validation operation for
|
|
||||||
tests, startup diagnostics, and configuration checks.
|
|
||||||
|
|
||||||
### Current integration option
|
|
||||||
|
|
||||||
Weatherreporter can prepare every report prompt with fixture inputs and inspect
|
|
||||||
any explicit profiles individually. That provides strong coverage but requires
|
|
||||||
consumer-maintained traversal and synthetic material.
|
|
||||||
|
|
||||||
### Requested capability
|
|
||||||
|
|
||||||
Consider an opt-in source-validation operation:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type SourceValidationOptions struct {
|
|
||||||
RequireCredentials bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Engine) ValidateSources(
|
|
||||||
ctx context.Context,
|
|
||||||
opts SourceValidationOptions,
|
|
||||||
) error
|
|
||||||
```
|
|
||||||
|
|
||||||
The operation should eagerly discover and structurally validate the configured
|
|
||||||
prompt, profile, and schema sources without model generation.
|
|
||||||
|
|
||||||
### Design considerations
|
|
||||||
|
|
||||||
- Keep deferred validation as the normal `NewEngine` behavior.
|
|
||||||
- Make eager validation an explicit consumer choice.
|
|
||||||
- Validate duplicate IDs and versions, strict YAML decoding, referenced content
|
|
||||||
files, profile/backend membership, schema syntax, and schema references.
|
|
||||||
- Distinguish structural credential declarations from current environment
|
|
||||||
availability.
|
|
||||||
- Do not read or expose credential values when credential availability is not
|
|
||||||
requested.
|
|
||||||
- Preserve source-specific public error identities and useful path context.
|
|
||||||
- Respect context cancellation during filesystem discovery and schema work.
|
|
||||||
- Consider whether exact prompt and profile inspection APIs already provide a
|
|
||||||
smaller sufficient surface before adding an engine-wide operation.
|
|
||||||
|
|
||||||
### Value to Weatherreporter
|
|
||||||
|
|
||||||
This would simplify offline corpus checks and catch malformed operator profile
|
|
||||||
sources before report work begins. It is helpful but lower priority than exact
|
|
||||||
prompt and profile inspection.
|
|
||||||
|
|
||||||
## Priority 5: Structured Generation Errors
|
|
||||||
|
|
||||||
**Disposition:** Deferred pending stronger downstream demand and a narrower
|
|
||||||
design that does not duplicate prepared provenance or impose HTTP-specific
|
|
||||||
fields on injected model clients.
|
|
||||||
|
|
||||||
### Downstream need
|
|
||||||
|
|
||||||
Weatherreporter preserves redacted, inspectable failure receipts for report
|
|
||||||
runs. When model generation fails operationally, it needs to classify the
|
|
||||||
failure and retain safe execution context without parsing error prose.
|
|
||||||
|
|
||||||
Prompt preparation already supplies selected profile, backend, and model
|
|
||||||
identity. Provider status classification would add useful operator context,
|
|
||||||
especially when the built-in OpenAI-compatible client receives a non-success
|
|
||||||
HTTP status.
|
|
||||||
|
|
||||||
### Current integration option
|
|
||||||
|
|
||||||
PromptKit exposes `ErrLLMGenerate` and preserves injected client errors through
|
|
||||||
`errors.Is`. Weatherreporter can reliably classify generation failure and use
|
|
||||||
its preparation record for profile, backend, and model provenance. Any further
|
|
||||||
diagnostic detail remains a redacted error string.
|
|
||||||
|
|
||||||
### Requested capability
|
|
||||||
|
|
||||||
Consider a typed generation error that continues to match `ErrLLMGenerate`:
|
|
||||||
|
|
||||||
```go
|
|
||||||
type GenerationError struct {
|
|
||||||
BackendID string
|
|
||||||
Model string
|
|
||||||
StatusCode int
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
The exact fields may differ. The useful contract is safe structured context
|
|
||||||
available through `errors.As`, while `errors.Is(err, ErrLLMGenerate)` remains
|
|
||||||
compatible.
|
|
||||||
|
|
||||||
### Design considerations
|
|
||||||
|
|
||||||
- Include only fields that PromptKit knows reliably and can expose safely.
|
|
||||||
- Treat an HTTP status as optional because injected model clients may not use
|
|
||||||
HTTP.
|
|
||||||
- Do not expose provider response bodies, endpoints, credential environment
|
|
||||||
names, credential values, request content, or generated content.
|
|
||||||
- Do not make a structured error a second source of prompt/profile provenance
|
|
||||||
already present in a prepared execution.
|
|
||||||
- Preserve injected client error identity.
|
|
||||||
- Keep retry and backoff policy with the consuming application.
|
|
||||||
|
|
||||||
### Value to Weatherreporter
|
|
||||||
|
|
||||||
This would improve durable failure receipts and troubleshooting, particularly
|
|
||||||
for built-in transport failures. It is not required if preparation details and
|
|
||||||
the existing sentinel remain available.
|
|
||||||
|
|
||||||
## Lower-Priority Shared Wishlist Items
|
|
||||||
|
|
||||||
### Structured Capacity Errors
|
|
||||||
|
|
||||||
**Disposition:** Implemented behavior. See the consumer guide's
|
|
||||||
[Handle Errors](../consumers/pkg-promptkit.md#handle-errors) section.
|
|
||||||
|
|
||||||
PromptKit now exposes the stable backend ID for capacity rejection without
|
|
||||||
requiring Weatherreporter to parse error text.
|
|
||||||
|
|
||||||
Weatherreporter currently generates batch reports sequentially and constructs
|
|
||||||
one engine per invocation, so engine-local capacity exhaustion is unlikely in
|
|
||||||
the initial design. The typed error becomes more valuable if report generation
|
|
||||||
later becomes concurrent or PromptKit engines become longer-lived.
|
|
||||||
It should not block adoption.
|
|
||||||
|
|
||||||
### Semantic Execution-Target Fingerprints
|
|
||||||
|
|
||||||
**Disposition:** Deferred pending a separate semantic-equality design for
|
|
||||||
resolved execution targets.
|
|
||||||
|
|
||||||
The semantic target digest proposed by the
|
|
||||||
[Notarius wishlist](notarius-promptkit-wishlist.md#priority-3-semantic-execution-target-fingerprints)
|
|
||||||
would provide a compact equality signal for audit metadata.
|
|
||||||
|
|
||||||
Weatherreporter does not currently reuse LLM-dependent checkpoints. Its Recent
|
|
||||||
Changes behavior compares deterministic module snapshots rather than generated
|
|
||||||
reports, so the digest has no immediate cache-correctness role. Existing
|
|
||||||
PromptKit result metadata is sufficient for the initial integration. A digest
|
|
||||||
would still be useful provenance and future-proofing, but it is not a
|
|
||||||
migration priority.
|
|
||||||
|
|
||||||
## Capabilities PromptKit Already Provides Well
|
|
||||||
|
|
||||||
PromptKit already provides the essential Weatherreporter integration surface:
|
|
||||||
|
|
||||||
- importable in-process engine construction;
|
|
||||||
- filesystem, `fs.FS`, and programmatic prompt, profile, and schema sources;
|
|
||||||
- offline preparation without model execution;
|
|
||||||
- prepared execution handles for a durable preflight boundary;
|
|
||||||
- exact prompt and profile inspection;
|
|
||||||
- versioned prompt selection;
|
|
||||||
- text, Markdown, JSON, and JSON Schema output contracts;
|
|
||||||
- single-pass output validation with raw output retained after completed
|
|
||||||
validation failure;
|
|
||||||
- inline input artifacts with provenance URIs and input hashes;
|
|
||||||
- selected profile, backend, model, effective target, prompt hashes, timing,
|
|
||||||
and token-usage provenance;
|
|
||||||
- endpoint-only profiles and engine-scoped backend registration;
|
|
||||||
- injected model-client and artifact-reader interfaces;
|
|
||||||
- caller cancellation, generation timeout, and transport timeout behavior; and
|
|
||||||
- public error sentinels for configuration, prompt, profile, artifact,
|
|
||||||
validation, capacity, and generation failures; and
|
|
||||||
- structured backend identity for capacity rejection.
|
|
||||||
|
|
||||||
These capabilities are sufficient for Weatherreporter to adopt PromptKit
|
|
||||||
without waiting for new upstream work.
|
|
||||||
|
|
||||||
## Responsibilities That Should Remain In Weatherreporter
|
|
||||||
|
|
||||||
The following concerns belong to Weatherreporter and should not move into
|
|
||||||
PromptKit:
|
|
||||||
|
|
||||||
- report definitions, valid periods, batches, and output naming;
|
|
||||||
- application prompt content and private report response schemas;
|
|
||||||
- deterministic weather facts, modules, and Recent Changes;
|
|
||||||
- curated `data_package` construction and persistence;
|
|
||||||
- generated-text domain validation and Markdown template rendering;
|
|
||||||
- managed artifact paths, atomic writes, metadata, and inspection commands;
|
|
||||||
- preparation, execution, raw-output, and failure-receipt schemas;
|
|
||||||
- CLI configuration loading and precedence;
|
|
||||||
- debug enablement, redaction, placement, sensitivity, and retention;
|
|
||||||
- distributor notification;
|
|
||||||
- batch continuation and any future retry policy; and
|
|
||||||
- application-level compatibility and migration policy.
|
|
||||||
|
|
||||||
## Suggested Upstream Sequence
|
|
||||||
|
|
||||||
For downstream adoption and any remaining upstream work, the useful order is:
|
|
||||||
|
|
||||||
1. Adopt the implemented executable preparation handles.
|
|
||||||
2. Consider eager source validation after evaluating whether the two exact
|
|
||||||
inspection APIs are sufficient.
|
|
||||||
3. Add structured generation errors.
|
|
||||||
4. Use structured capacity errors and consider semantic execution-target
|
|
||||||
fingerprints as lower-priority operational improvements.
|
|
||||||
|
|
||||||
The first item removes the material integration workaround. Prompt and profile
|
|
||||||
inspection improve fail-fast validation. The remaining items are optional
|
|
||||||
ergonomic and diagnostic improvements.
|
|
||||||
|
|
||||||
## Adoption Sequencing
|
|
||||||
|
|
||||||
Weatherreporter should not wait for the deferred wishlist items. The current
|
|
||||||
PromptKit interface is sufficient when Weatherreporter:
|
|
||||||
|
|
||||||
- embeds immutable prompt and schema assets;
|
|
||||||
- supplies immutable inline data-package bytes;
|
|
||||||
- constructs one engine per CLI invocation;
|
|
||||||
- prepares an execution, persists selected `Details`, and calls
|
|
||||||
`RunPrepared`; and
|
|
||||||
- keeps PromptKit behind a weatherreporter-owned adapter contract.
|
|
||||||
|
|
||||||
Prompt inspection, profile inspection, source validation, structured errors,
|
|
||||||
capacity details, and semantic fingerprints should not gate adoption.
|
|
||||||
139
engine.go
139
engine.go
@@ -98,9 +98,10 @@ type Config struct {
|
|||||||
// It is required unless a WithPromptFS or WithPromptFile option supplies the
|
// It is required unless a WithPromptFS or WithPromptFile option supplies the
|
||||||
// prompt source.
|
// prompt source.
|
||||||
PromptDir string
|
PromptDir string
|
||||||
// ProfileDir is an optional directory whose profiles take precedence over
|
// ProfileDir is an optional ordinary configured source whose profiles take
|
||||||
// embedded built-in profiles. An empty value selects only built-ins unless
|
// precedence over application fallback and embedded built-in profiles. An
|
||||||
// profile options are also supplied.
|
// empty value selects the lower-precedence sources unless a profile-source
|
||||||
|
// option supplies the ordinary source.
|
||||||
ProfileDir string
|
ProfileDir string
|
||||||
// SchemaDir is the root for JSON Schema files. An empty value uses the
|
// SchemaDir is the root for JSON Schema files. An empty value uses the
|
||||||
// current directory. WithSchemaFS or WithSchemaFile replaces this source.
|
// current directory. WithSchemaFS or WithSchemaFile replaces this source.
|
||||||
@@ -119,12 +120,12 @@ type Config struct {
|
|||||||
// Option customizes engine construction.
|
// Option customizes engine construction.
|
||||||
//
|
//
|
||||||
// NewEngine applies options in argument order and ignores nil options. Within
|
// NewEngine applies options in argument order and ignores nil options. Within
|
||||||
// each prompt-source, profile-source, in-memory-profile, schema-source,
|
// each prompt-source, ordinary-profile-source, fallback-profile-source,
|
||||||
// model-client, and artifact-reader category, the last non-nil valid option
|
// in-memory-profile, schema-source, model-client, and artifact-reader
|
||||||
// replaces earlier options in that category. WithBackend is the additive
|
// category, the last non-nil valid option replaces earlier options in that
|
||||||
// exception: unique registrations accumulate, and a repeated backend ID is an
|
// category. WithBackend is the additive exception: unique registrations
|
||||||
// error rather than a replacement. An invalid option fails construction even
|
// accumulate, and a repeated backend ID is an error rather than a replacement.
|
||||||
// if a later option would replace it.
|
// An invalid option fails construction even if a later option would replace it.
|
||||||
type Option interface {
|
type Option interface {
|
||||||
apply(*engineOptions) error
|
apply(*engineOptions) error
|
||||||
}
|
}
|
||||||
@@ -136,18 +137,20 @@ func (f optionFunc) apply(options *engineOptions) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type engineOptions struct {
|
type engineOptions struct {
|
||||||
llmClient llm.Client
|
llmClient llm.Client
|
||||||
artifactReader artifactadapter.Reader
|
artifactReader artifactadapter.Reader
|
||||||
promptDefs promptdef.Repository
|
promptDefs promptdef.Repository
|
||||||
profiles profile.Repository
|
profiles profile.Repository
|
||||||
memoryProfiles profile.Repository
|
fallbackProfiles profile.Repository
|
||||||
backends []domain.Backend
|
memoryProfiles profile.Repository
|
||||||
validator validate.Validator
|
backends []domain.Backend
|
||||||
promptSource bool
|
validator validate.Validator
|
||||||
profileSource bool
|
promptSource bool
|
||||||
memorySource bool
|
profileSource bool
|
||||||
validatorSource bool
|
fallbackProfileSource bool
|
||||||
artifactSource bool
|
memorySource bool
|
||||||
|
validatorSource bool
|
||||||
|
artifactSource bool
|
||||||
}
|
}
|
||||||
|
|
||||||
// WithLLMClient replaces the built-in model client used by [Engine.Run] and
|
// WithLLMClient replaces the built-in model client used by [Engine.Run] and
|
||||||
@@ -224,12 +227,12 @@ func WithPromptFile(path string) Option {
|
|||||||
|
|
||||||
// WithProfileFS loads execution profiles from fsys under root.
|
// WithProfileFS loads execution profiles from fsys under root.
|
||||||
//
|
//
|
||||||
// Profiles from this source overlay built-in profiles. Profile YAML must use
|
// Profiles from this ordinary configured source take precedence over
|
||||||
// api_key_env for environment-based credentials; raw API keys are rejected.
|
// application fallback and built-in profiles. Profile YAML must use api_key_env
|
||||||
// fsys must be non-nil and root must be non-empty; otherwise NewEngine fails
|
// for environment-based credentials; raw API keys are rejected. fsys must be
|
||||||
// with ErrInvalidConfig. This option replaces Config.ProfileDir and earlier
|
// non-nil and root must be non-empty; otherwise NewEngine fails with
|
||||||
// file or FS profile-source options, but remains below WithProfiles in
|
// ErrInvalidConfig. This option replaces Config.ProfileDir and earlier file or
|
||||||
// precedence.
|
// FS profile-source options, but remains below WithProfiles in precedence.
|
||||||
func WithProfileFS(fsys fs.FS, root string) Option {
|
func WithProfileFS(fsys fs.FS, root string) Option {
|
||||||
return optionFunc(func(options *engineOptions) error {
|
return optionFunc(func(options *engineOptions) error {
|
||||||
if fsys == nil {
|
if fsys == nil {
|
||||||
@@ -246,11 +249,12 @@ func WithProfileFS(fsys fs.FS, root string) Option {
|
|||||||
|
|
||||||
// WithProfileFile loads execution profiles from the single profile file at path.
|
// WithProfileFile loads execution profiles from the single profile file at path.
|
||||||
//
|
//
|
||||||
// The profile overlays built-in profiles. Profile YAML must use api_key_env for
|
// The profile takes precedence over application fallback and built-in profiles.
|
||||||
// environment-based credentials; raw API keys are rejected. path must name an
|
// Profile YAML must use api_key_env for environment-based credentials; raw API
|
||||||
// existing non-directory file when NewEngine applies the option. This option
|
// keys are rejected. path must name an existing non-directory file when
|
||||||
// replaces Config.ProfileDir and earlier file or FS profile-source options,
|
// NewEngine applies the option. This option replaces Config.ProfileDir and
|
||||||
// but remains below WithProfiles in precedence.
|
// earlier file or FS profile-source options, but remains below WithProfiles in
|
||||||
|
// precedence.
|
||||||
func WithProfileFile(path string) Option {
|
func WithProfileFile(path string) Option {
|
||||||
return optionFunc(func(options *engineOptions) error {
|
return optionFunc(func(options *engineOptions) error {
|
||||||
fsys, root, err := fileSource(path)
|
fsys, root, err := fileSource(path)
|
||||||
@@ -263,8 +267,41 @@ func WithProfileFile(path string) Option {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WithFallbackProfileFS supplies application-owned fallback profile
|
||||||
|
// definitions from fsys under root.
|
||||||
|
//
|
||||||
|
// Profile lookup checks, in order, profiles supplied by WithProfiles; the
|
||||||
|
// ordinary configured source selected by WithProfileFile, WithProfileFS, or
|
||||||
|
// Config.ProfileDir; this fallback source; and Promptkit's embedded built-in
|
||||||
|
// profiles. Each source supplies a complete profile definition; profile fields
|
||||||
|
// are not merged between sources. Only an absent profile ID proceeds to the
|
||||||
|
// next source. A matching read, parse, duplicate, validation, or credential
|
||||||
|
// format failure stops resolution.
|
||||||
|
//
|
||||||
|
// Files use the ordinary strict profile YAML and api_key_env credential rules.
|
||||||
|
// Loading and validation are lazy: NewEngine validates this option's arguments
|
||||||
|
// but does not read profile files. fsys must be non-nil and root must be
|
||||||
|
// nonblank; otherwise NewEngine returns an error matching ErrInvalidConfig.
|
||||||
|
// Repeating this option replaces the earlier valid fallback source.
|
||||||
|
//
|
||||||
|
// This option controls profile-definition lookup, not provider or generation
|
||||||
|
// failover.
|
||||||
|
func WithFallbackProfileFS(fsys fs.FS, root string) Option {
|
||||||
|
return optionFunc(func(options *engineOptions) error {
|
||||||
|
if fsys == nil {
|
||||||
|
return ErrInvalidConfig
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(root) == "" {
|
||||||
|
return ErrInvalidConfig
|
||||||
|
}
|
||||||
|
options.fallbackProfiles = profile.NewFSRepository(fsys, root)
|
||||||
|
options.fallbackProfileSource = true
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
// WithProfiles configures in-memory profiles that take precedence over
|
// WithProfiles configures in-memory profiles that take precedence over
|
||||||
// configured profile files and built-in profiles.
|
// ordinary configured, application fallback, and built-in profiles.
|
||||||
//
|
//
|
||||||
// NewEngine validates and copies every profile. IDs must be unique within one
|
// NewEngine validates and copies every profile. IDs must be unique within one
|
||||||
// call. An invalid profile, duplicate ID, or unsupported ExtraParams value
|
// call. An invalid profile, duplicate ID, or unsupported ExtraParams value
|
||||||
@@ -350,13 +387,7 @@ func NewEngine(cfg Config, opts ...Option) (*Engine, error) {
|
|||||||
promptDefs = promptdef.NewFilesystemRepository(cfg.PromptDir)
|
promptDefs = promptdef.NewFilesystemRepository(cfg.PromptDir)
|
||||||
}
|
}
|
||||||
|
|
||||||
profiles := builtin.NewRepositoryWithDirectory(cfg.ProfileDir)
|
profiles := newProfileRepository(cfg.ProfileDir, options)
|
||||||
if options.profileSource {
|
|
||||||
profiles = builtin.NewRepositoryWithPrimary(options.profiles)
|
|
||||||
}
|
|
||||||
if options.memorySource {
|
|
||||||
profiles = profile.NewOverlayRepository(options.memoryProfiles, profiles)
|
|
||||||
}
|
|
||||||
|
|
||||||
backendRegistry, err := backend.NewRegistry(options.backends)
|
backendRegistry, err := backend.NewRegistry(options.backends)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -409,6 +440,26 @@ func NewEngine(cfg Config, opts ...Option) (*Engine, error) {
|
|||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newProfileRepository(profileDir string, options engineOptions) profile.Repository {
|
||||||
|
repository := builtin.NewRepository()
|
||||||
|
|
||||||
|
if options.fallbackProfileSource {
|
||||||
|
repository = profile.NewOverlayRepository(options.fallbackProfiles, repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.profileSource {
|
||||||
|
repository = profile.NewOverlayRepository(options.profiles, repository)
|
||||||
|
} else if strings.TrimSpace(profileDir) != "" {
|
||||||
|
repository = profile.NewOverlayRepository(profile.NewFilesystemRepository(profileDir), repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
if options.memorySource {
|
||||||
|
repository = profile.NewOverlayRepository(options.memoryProfiles, repository)
|
||||||
|
}
|
||||||
|
|
||||||
|
return repository
|
||||||
|
}
|
||||||
|
|
||||||
func fileSource(name string) (fs.FS, string, error) {
|
func fileSource(name string) (fs.FS, string, error) {
|
||||||
cleanName := strings.TrimSpace(name)
|
cleanName := strings.TrimSpace(name)
|
||||||
if cleanName == "" {
|
if cleanName == "" {
|
||||||
@@ -481,10 +532,10 @@ func (e *Engine) InspectPrompt(
|
|||||||
//
|
//
|
||||||
// InspectProfile trims surrounding whitespace from profileID and looks up the
|
// InspectProfile trims surrounding whitespace from profileID and looks up the
|
||||||
// resulting nonblank ID exactly and case-sensitively through the engine's
|
// resulting nonblank ID exactly and case-sensitively through the engine's
|
||||||
// ordinary in-memory, configured-source, and built-in profile precedence. It
|
// in-memory, ordinary configured-source, application fallback, and built-in
|
||||||
// applies framework defaults, the selected backend, and then the selected
|
// profile precedence. It applies the framework timeout baseline, selected
|
||||||
// profile to EffectiveModelParams without a request override. BackendID is
|
// backend, and then selected profile to EffectiveModelParams without a request
|
||||||
// empty for an endpoint-only profile.
|
// override. BackendID is empty for an endpoint-only profile.
|
||||||
//
|
//
|
||||||
// APIKeyEnv in the returned target is an environment-variable name, never its
|
// APIKeyEnv in the returned target is an environment-variable name, never its
|
||||||
// value. APIKeyRequired instead reports a direct credential requirement and is
|
// value. APIKeyRequired instead reports a direct credential requirement and is
|
||||||
|
|||||||
@@ -360,14 +360,14 @@ func TestEngineExecutionSettingPrecedence(t *testing.T) {
|
|||||||
wantPresence promptkit.ExecutionTargetPresence
|
wantPresence promptkit.ExecutionTargetPresence
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
name: "framework defaults fill zero-valued profile settings",
|
name: "unspecified provider controls retain framework timeout",
|
||||||
profile: defaultsProfile,
|
profile: defaultsProfile,
|
||||||
want: promptkit.ExecutionTarget{
|
want: promptkit.ExecutionTarget{
|
||||||
Endpoint: defaultsProfile.endpoint,
|
Endpoint: defaultsProfile.endpoint,
|
||||||
Model: defaultsProfile.model,
|
Model: defaultsProfile.model,
|
||||||
Temperature: 0,
|
Temperature: 0,
|
||||||
MaxTokens: 0,
|
MaxTokens: 0,
|
||||||
TopP: 1,
|
TopP: 0,
|
||||||
TimeoutSeconds: 600,
|
TimeoutSeconds: 600,
|
||||||
ServiceTier: defaultsProfile.serviceTier,
|
ServiceTier: defaultsProfile.serviceTier,
|
||||||
ReasoningEffort: defaultsProfile.reasoningEffort,
|
ReasoningEffort: defaultsProfile.reasoningEffort,
|
||||||
@@ -2301,6 +2301,8 @@ func TestSourceOptionsRejectInvalidInputs(t *testing.T) {
|
|||||||
{name: "profile fs nil", opt: promptkit.WithProfileFS(nil, "profiles")},
|
{name: "profile fs nil", opt: promptkit.WithProfileFS(nil, "profiles")},
|
||||||
{name: "profile fs empty root", opt: promptkit.WithProfileFS(fstest.MapFS{}, "")},
|
{name: "profile fs empty root", opt: promptkit.WithProfileFS(fstest.MapFS{}, "")},
|
||||||
{name: "profile file empty", opt: promptkit.WithProfileFile("")},
|
{name: "profile file empty", opt: promptkit.WithProfileFile("")},
|
||||||
|
{name: "fallback profile fs nil", opt: promptkit.WithFallbackProfileFS(nil, "profiles")},
|
||||||
|
{name: "fallback profile fs empty root", opt: promptkit.WithFallbackProfileFS(fstest.MapFS{}, "")},
|
||||||
{name: "schema fs nil", opt: promptkit.WithSchemaFS(nil, "schemas")},
|
{name: "schema fs nil", opt: promptkit.WithSchemaFS(nil, "schemas")},
|
||||||
{name: "schema fs empty root", opt: promptkit.WithSchemaFS(fstest.MapFS{}, "")},
|
{name: "schema fs empty root", opt: promptkit.WithSchemaFS(fstest.MapFS{}, "")},
|
||||||
{name: "schema file empty", opt: promptkit.WithSchemaFile("")},
|
{name: "schema file empty", opt: promptkit.WithSchemaFile("")},
|
||||||
|
|||||||
@@ -14,9 +14,6 @@ const (
|
|||||||
ContentTypeApplicationJSON = "application/json"
|
ContentTypeApplicationJSON = "application/json"
|
||||||
OpenAIChatCompletionsPath = "/chat/completions"
|
OpenAIChatCompletionsPath = "/chat/completions"
|
||||||
|
|
||||||
ExecutionDefaultTemperature = 0.0
|
|
||||||
ExecutionDefaultMaxTokens = 0
|
|
||||||
ExecutionDefaultTopP = 1.0
|
|
||||||
ExecutionDefaultTimeoutSeconds = 600
|
ExecutionDefaultTimeoutSeconds = 600
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -26,9 +23,6 @@ var (
|
|||||||
|
|
||||||
func ExecutionTargetDefault() domain.ExecutionTarget {
|
func ExecutionTargetDefault() domain.ExecutionTarget {
|
||||||
return domain.ExecutionTarget{
|
return domain.ExecutionTarget{
|
||||||
Temperature: ExecutionDefaultTemperature,
|
|
||||||
MaxTokens: ExecutionDefaultMaxTokens,
|
|
||||||
TopP: ExecutionDefaultTopP,
|
|
||||||
TimeoutSeconds: ExecutionDefaultTimeoutSeconds,
|
TimeoutSeconds: ExecutionDefaultTimeoutSeconds,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package builtin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"embed"
|
"embed"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/profile"
|
"gitea.maximumdirect.net/eric/promptkit/internal/profile"
|
||||||
)
|
)
|
||||||
@@ -15,17 +14,3 @@ var assets embed.FS
|
|||||||
func NewRepository() profile.Repository {
|
func NewRepository() profile.Repository {
|
||||||
return profile.NewFSRepository(assets, assetRoot)
|
return profile.NewFSRepository(assets, assetRoot)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewRepositoryWithPrimary(primary profile.Repository) profile.Repository {
|
|
||||||
if primary == nil {
|
|
||||||
return NewRepository()
|
|
||||||
}
|
|
||||||
return profile.NewOverlayRepository(primary, NewRepository())
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRepositoryWithDirectory(dir string) profile.Repository {
|
|
||||||
if strings.TrimSpace(dir) == "" {
|
|
||||||
return NewRepository()
|
|
||||||
}
|
|
||||||
return NewRepositoryWithPrimary(profile.NewFilesystemRepository(dir))
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,14 +2,11 @@ package builtin
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"errors"
|
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/backend"
|
"gitea.maximumdirect.net/eric/promptkit/internal/backend"
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
|
||||||
"gitea.maximumdirect.net/eric/promptkit/internal/profile"
|
|
||||||
"gopkg.in/yaml.v3"
|
"gopkg.in/yaml.v3"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -91,53 +88,3 @@ func loadBuiltInProfileIDs(t *testing.T) map[string]string {
|
|||||||
}
|
}
|
||||||
return ids
|
return ids
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRepositoryWithPrimaryUsesPrimaryBeforeBuiltIns(t *testing.T) {
|
|
||||||
repo := NewRepositoryWithPrimary(staticProfileRepo{
|
|
||||||
profiles: map[string]string{"mistral-small-3": "custom-model"},
|
|
||||||
})
|
|
||||||
|
|
||||||
p, err := repo.GetProfile(context.Background(), "mistral-small-3")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected profile to load, got %v", err)
|
|
||||||
}
|
|
||||||
if p.Model != "custom-model" {
|
|
||||||
t.Fatalf("expected primary profile to override built-in, got %+v", p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRepositoryWithPrimaryFallsBackToBuiltIns(t *testing.T) {
|
|
||||||
repo := NewRepositoryWithPrimary(staticProfileRepo{})
|
|
||||||
|
|
||||||
p, err := repo.GetProfile(context.Background(), "mistral-small-3")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected built-in profile to load, got %v", err)
|
|
||||||
}
|
|
||||||
if p.ID != "mistral-small-3" {
|
|
||||||
t.Fatalf("unexpected profile: %+v", p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRepositoryWithPrimaryDoesNotFallBackAfterPrimaryError(t *testing.T) {
|
|
||||||
repo := NewRepositoryWithPrimary(staticProfileRepo{err: profile.ErrInvalidProfile})
|
|
||||||
|
|
||||||
_, err := repo.GetProfile(context.Background(), "mistral-small-3")
|
|
||||||
if !errors.Is(err, profile.ErrInvalidProfile) {
|
|
||||||
t.Fatalf("expected primary error, got %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type staticProfileRepo struct {
|
|
||||||
profiles map[string]string
|
|
||||||
err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r staticProfileRepo) GetProfile(_ context.Context, id string) (*domain.ExecutionProfile, error) {
|
|
||||||
if r.err != nil {
|
|
||||||
return nil, r.err
|
|
||||||
}
|
|
||||||
if model, ok := r.profiles[id]; ok {
|
|
||||||
return &domain.ExecutionProfile{ID: id, Endpoint: "http://primary/v1", Model: model}, nil
|
|
||||||
}
|
|
||||||
return nil, profile.ErrProfileNotFound
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1690,36 +1690,6 @@ func TestRunnerRunSelectedProfileBeatsBuiltInDefault(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRunnerRunBuiltInDefaultsUsedWhenProfileOmitsOptionalFields(t *testing.T) {
|
|
||||||
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
|
||||||
execRepo := &fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{
|
|
||||||
"exec": {ID: "exec", Endpoint: "http://profile/v1", Model: "profile-model"},
|
|
||||||
}}
|
|
||||||
llmClient := &fakeLLM{resp: &domain.GenerateResponse{Content: "ok"}}
|
|
||||||
runner := NewRunner(promptRepo, execRepo, nil, defaultArtifactReader(), defaultRenderer(), llmClient, nil, nil)
|
|
||||||
|
|
||||||
res, err := runner.Run(context.Background(), domain.RunRequest{
|
|
||||||
PromptID: "p",
|
|
||||||
ProfileID: "exec",
|
|
||||||
Inputs: singleInputRef(),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("expected no error, got %v", err)
|
|
||||||
}
|
|
||||||
if res.EffectiveModelParams.Temperature != defaults.ExecutionDefaultTemperature {
|
|
||||||
t.Fatalf("expected default temperature %v, got %v", defaults.ExecutionDefaultTemperature, res.EffectiveModelParams.Temperature)
|
|
||||||
}
|
|
||||||
if res.EffectiveModelParams.TopP != defaults.ExecutionDefaultTopP {
|
|
||||||
t.Fatalf("expected default top_p %v, got %v", defaults.ExecutionDefaultTopP, res.EffectiveModelParams.TopP)
|
|
||||||
}
|
|
||||||
if res.EffectiveModelParams.MaxTokens != defaults.ExecutionDefaultMaxTokens {
|
|
||||||
t.Fatalf("expected default max_tokens %d, got %d", defaults.ExecutionDefaultMaxTokens, res.EffectiveModelParams.MaxTokens)
|
|
||||||
}
|
|
||||||
if res.EffectiveModelParams.TimeoutSeconds != defaults.ExecutionDefaultTimeoutSeconds {
|
|
||||||
t.Fatalf("expected default timeout_seconds %d, got %d", defaults.ExecutionDefaultTimeoutSeconds, res.EffectiveModelParams.TimeoutSeconds)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestRunnerRunAPIKeyEnvResolvesFromEnvironment(t *testing.T) {
|
func TestRunnerRunAPIKeyEnvResolvesFromEnvironment(t *testing.T) {
|
||||||
t.Setenv("PROMPTKIT_TEST_API_KEY", "secret")
|
t.Setenv("PROMPTKIT_TEST_API_KEY", "secret")
|
||||||
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
promptRepo := &fakePromptRepo{def: promptDef(domain.FormatText, domain.ValidationNone, 0)}
|
||||||
|
|||||||
@@ -973,6 +973,24 @@ func TestRepeatedOptionsUseLastValueInEachCategory(t *testing.T) {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
t.Run("fallback profile source", func(t *testing.T) {
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{},
|
||||||
|
promptkit.WithPromptFS(contractPromptFS("prompt", "profile", "message"), "."),
|
||||||
|
promptkit.WithFallbackProfileFS(contractProfileFS("profile", "first-model"), "."),
|
||||||
|
promptkit.WithFallbackProfileFS(contractProfileFS("profile", "second-model"), "."),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine: %v", err)
|
||||||
|
}
|
||||||
|
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: "prompt"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("prepare from last fallback profile source: %v", err)
|
||||||
|
}
|
||||||
|
if prepared.EffectiveModelParams.Model != "second-model" {
|
||||||
|
t.Fatalf("expected last fallback profile source, got %q", prepared.EffectiveModelParams.Model)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
t.Run("in-memory profiles", func(t *testing.T) {
|
t.Run("in-memory profiles", func(t *testing.T) {
|
||||||
first := profile
|
first := profile
|
||||||
first.Model = "first-model"
|
first.Model = "first-model"
|
||||||
@@ -1057,6 +1075,212 @@ func TestRepeatedOptionsUseLastValueInEachCategory(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestFallbackProfileSourcePrecedence(t *testing.T) {
|
||||||
|
const profileID = "application-profile"
|
||||||
|
|
||||||
|
prepareModel := func(t *testing.T, engine *promptkit.Engine, promptID string) string {
|
||||||
|
t.Helper()
|
||||||
|
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: promptID})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("prepare: %v", err)
|
||||||
|
}
|
||||||
|
return prepared.EffectiveModelParams.Model
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("in-memory profiles override ordinary and fallback profiles", func(t *testing.T) {
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{},
|
||||||
|
promptkit.WithPromptFS(contractPromptFS("prompt", profileID, "message"), "."),
|
||||||
|
promptkit.WithFallbackProfileFS(contractProfileFS(profileID, "fallback-model"), "."),
|
||||||
|
promptkit.WithProfileFS(contractProfileFS(profileID, "ordinary-model"), "."),
|
||||||
|
promptkit.WithProfiles(promptkit.Profile{ID: profileID, Endpoint: "http://example.test/v1", Model: "memory-model"}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine: %v", err)
|
||||||
|
}
|
||||||
|
if model := prepareModel(t, engine, "prompt"); model != "memory-model" {
|
||||||
|
t.Fatalf("expected in-memory profile, got %q", model)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ordinary filesystem source overrides fallback profile", func(t *testing.T) {
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{},
|
||||||
|
promptkit.WithPromptFS(contractPromptFS("prompt", profileID, "message"), "."),
|
||||||
|
promptkit.WithFallbackProfileFS(contractProfileFS(profileID, "fallback-model"), "."),
|
||||||
|
promptkit.WithProfileFS(contractProfileFS(profileID, "ordinary-model"), "."),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine: %v", err)
|
||||||
|
}
|
||||||
|
if model := prepareModel(t, engine, "prompt"); model != "ordinary-model" {
|
||||||
|
t.Fatalf("expected ordinary profile, got %q", model)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("configured directory overrides fallback profile", func(t *testing.T) {
|
||||||
|
profileDir := t.TempDir()
|
||||||
|
writePublicProfileFile(t, profileDir, profileID, "http://example.test/v1", "directory-model")
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{ProfileDir: profileDir},
|
||||||
|
promptkit.WithPromptFS(contractPromptFS("prompt", profileID, "message"), "."),
|
||||||
|
promptkit.WithFallbackProfileFS(contractProfileFS(profileID, "fallback-model"), "."),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine: %v", err)
|
||||||
|
}
|
||||||
|
if model := prepareModel(t, engine, "prompt"); model != "directory-model" {
|
||||||
|
t.Fatalf("expected configured directory profile, got %q", model)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("fallback profile overrides built-in profile", func(t *testing.T) {
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{},
|
||||||
|
promptkit.WithPromptFS(contractPromptFS("prompt", "mistral-small-3", "message"), "."),
|
||||||
|
promptkit.WithFallbackProfileFS(contractProfileFS("mistral-small-3", "fallback-model"), "."),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine: %v", err)
|
||||||
|
}
|
||||||
|
if model := prepareModel(t, engine, "prompt"); model != "fallback-model" {
|
||||||
|
t.Fatalf("expected fallback profile, got %q", model)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("missing fallback profile uses built-in profile", func(t *testing.T) {
|
||||||
|
t.Setenv("OPENROUTER_API_KEY", "test-key")
|
||||||
|
baseline, err := promptkit.NewEngine(promptkit.Config{},
|
||||||
|
promptkit.WithPromptFS(contractPromptFS("prompt", "mistral-small-3", "message"), "."),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct baseline engine: %v", err)
|
||||||
|
}
|
||||||
|
want := prepareModel(t, baseline, "prompt")
|
||||||
|
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{},
|
||||||
|
promptkit.WithPromptFS(contractPromptFS("prompt", "mistral-small-3", "message"), "."),
|
||||||
|
promptkit.WithFallbackProfileFS(contractProfileFS(profileID, "fallback-model"), "."),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine: %v", err)
|
||||||
|
}
|
||||||
|
if model := prepareModel(t, engine, "prompt"); model != want {
|
||||||
|
t.Fatalf("expected built-in profile model %q, got %q", want, model)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFallbackProfileSourcePreservesLazyLoadingAndErrors(t *testing.T) {
|
||||||
|
const profileID = "application-profile"
|
||||||
|
|
||||||
|
t.Run("construction defers malformed fallback profiles", func(t *testing.T) {
|
||||||
|
_, err := promptkit.NewEngine(promptkit.Config{},
|
||||||
|
promptkit.WithPromptFS(fstest.MapFS{}, "."),
|
||||||
|
promptkit.WithFallbackProfileFS(fstest.MapFS{
|
||||||
|
"broken.yaml": &fstest.MapFile{Data: []byte("id: broken\nunknown: value\n")},
|
||||||
|
}, "."),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine with malformed fallback profile: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("unrelated malformed fallback profile does not block matching definition", func(t *testing.T) {
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{},
|
||||||
|
promptkit.WithPromptFS(contractPromptFS("prompt", profileID, "message"), "."),
|
||||||
|
promptkit.WithFallbackProfileFS(fstest.MapFS{
|
||||||
|
"broken.yaml": &fstest.MapFile{Data: []byte("id: unrelated\nunknown: value\n")},
|
||||||
|
"valid.yaml": &fstest.MapFile{Data: []byte("id: application-profile\nendpoint: http://example.test/v1\nmodel: fallback-model\n")},
|
||||||
|
}, "."),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine: %v", err)
|
||||||
|
}
|
||||||
|
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: "prompt"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("prepare from valid fallback profile: %v", err)
|
||||||
|
}
|
||||||
|
if prepared.EffectiveModelParams.Model != "fallback-model" {
|
||||||
|
t.Fatalf("unexpected fallback profile model: %q", prepared.EffectiveModelParams.Model)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("matching malformed fallback profile does not reach built-in profile", func(t *testing.T) {
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{},
|
||||||
|
promptkit.WithPromptFS(contractPromptFS("prompt", "mistral-small-3", "message"), "."),
|
||||||
|
promptkit.WithFallbackProfileFS(fstest.MapFS{
|
||||||
|
"mistral-small-3.yaml": &fstest.MapFile{Data: []byte("id: mistral-small-3\nendpoint: http://example.test/v1\nmodel: fallback-model\nunknown: value\n")},
|
||||||
|
}, "."),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: "prompt"}); !errors.Is(err, promptkit.ErrProfileLoad) {
|
||||||
|
t.Fatalf("expected ErrProfileLoad, got %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("matching malformed ordinary profile does not reach fallback profile", func(t *testing.T) {
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{},
|
||||||
|
promptkit.WithPromptFS(contractPromptFS("prompt", profileID, "message"), "."),
|
||||||
|
promptkit.WithProfileFS(fstest.MapFS{
|
||||||
|
"application-profile.yaml": &fstest.MapFile{Data: []byte("id: application-profile\nendpoint: http://example.test/v1\nmodel: ordinary-model\nunknown: value\n")},
|
||||||
|
}, "."),
|
||||||
|
promptkit.WithFallbackProfileFS(contractProfileFS(profileID, "fallback-model"), "."),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine: %v", err)
|
||||||
|
}
|
||||||
|
if _, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: "prompt"}); !errors.Is(err, promptkit.ErrProfileLoad) {
|
||||||
|
t.Fatalf("expected ErrProfileLoad, got %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestFallbackProfileSourceWorksAcrossWorkflows(t *testing.T) {
|
||||||
|
const profileID = "application-profile"
|
||||||
|
client := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "ok"}}
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{},
|
||||||
|
promptkit.WithPromptFS(contractPromptFS("prompt", profileID, "message"), "."),
|
||||||
|
promptkit.WithFallbackProfileFS(contractProfileFS(profileID, "fallback-model"), "."),
|
||||||
|
promptkit.WithLLMClient(client),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct engine: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
inspection, err := engine.InspectProfile(context.Background(), profileID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("inspect fallback profile: %v", err)
|
||||||
|
}
|
||||||
|
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: "prompt"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("prepare fallback profile: %v", err)
|
||||||
|
}
|
||||||
|
preparedExecution, err := engine.PrepareExecution(context.Background(), promptkit.RunRequest{PromptID: "prompt"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("prepare execution with fallback profile: %v", err)
|
||||||
|
}
|
||||||
|
preparedDetails := preparedExecution.Details()
|
||||||
|
preparedResult, err := engine.RunPrepared(context.Background(), preparedExecution)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("run prepared fallback profile: %v", err)
|
||||||
|
}
|
||||||
|
runResult, err := engine.Run(context.Background(), promptkit.RunRequest{PromptID: "prompt"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("run fallback profile: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for name, model := range map[string]string{
|
||||||
|
"inspection": inspection.EffectiveModelParams.Model,
|
||||||
|
"preparation": prepared.EffectiveModelParams.Model,
|
||||||
|
"prepared execution": preparedDetails.EffectiveModelParams.Model,
|
||||||
|
"prepared result": preparedResult.EffectiveModelParams.Model,
|
||||||
|
"run result": runResult.EffectiveModelParams.Model,
|
||||||
|
} {
|
||||||
|
if model != "fallback-model" {
|
||||||
|
t.Fatalf("%s model=%q, want fallback-model", name, model)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestEngineSupportsConcurrentPrepareAndRun(t *testing.T) {
|
func TestEngineSupportsConcurrentPrepareAndRun(t *testing.T) {
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{},
|
engine, err := promptkit.NewEngine(promptkit.Config{},
|
||||||
promptkit.WithPromptFS(contractPromptFS("prompt", "profile", "message"), "."),
|
promptkit.WithPromptFS(contractPromptFS("prompt", "profile", "message"), "."),
|
||||||
|
|||||||
74
types.go
74
types.go
@@ -116,7 +116,8 @@ type RunRequest struct {
|
|||||||
// empty maps are equivalent.
|
// empty maps are equivalent.
|
||||||
Vars map[string]string
|
Vars map[string]string
|
||||||
// Execution optionally overrides individual execution settings. Nil uses
|
// Execution optionally overrides individual execution settings. Nil uses
|
||||||
// the selected profile over its backend, when any, and framework defaults.
|
// the selected profile over its backend, when any, and the framework
|
||||||
|
// baseline.
|
||||||
Execution *ExecutionTargetOverride
|
Execution *ExecutionTargetOverride
|
||||||
// Validation optionally replaces the prompt's complete output contract. It
|
// Validation optionally replaces the prompt's complete output contract. It
|
||||||
// does not merge individual fields. Nil uses the prompt contract.
|
// does not merge individual fields. Nil uses the prompt contract.
|
||||||
@@ -144,9 +145,10 @@ type PreparedRun struct {
|
|||||||
// SelectedBackendID equals EffectiveModelParams.BackendID. It is empty for
|
// SelectedBackendID equals EffectiveModelParams.BackendID. It is empty for
|
||||||
// an endpoint-only profile.
|
// an endpoint-only profile.
|
||||||
SelectedBackendID string `json:"selected_backend_id,omitempty"`
|
SelectedBackendID string `json:"selected_backend_id,omitempty"`
|
||||||
// EffectiveModelParams contains framework defaults overlaid by the selected
|
// EffectiveModelParams contains settings resolved from the framework timeout
|
||||||
// backend, profile, and then request overrides. It excludes resolved API-key
|
// baseline, selected backend, profile, and then request overrides. Unset
|
||||||
// values.
|
// optional provider controls remain zero rather than reporting a provider
|
||||||
|
// default. It excludes resolved API-key values.
|
||||||
EffectiveModelParams ExecutionTarget `json:"effective_model_params"`
|
EffectiveModelParams ExecutionTarget `json:"effective_model_params"`
|
||||||
// OutputContract is the complete effective output contract.
|
// OutputContract is the complete effective output contract.
|
||||||
OutputContract OutputContract `json:"output_contract"`
|
OutputContract OutputContract `json:"output_contract"`
|
||||||
@@ -298,13 +300,17 @@ type ExecutionTarget struct {
|
|||||||
Endpoint string `json:"endpoint"`
|
Endpoint string `json:"endpoint"`
|
||||||
// Model is the provider model identifier.
|
// Model is the provider model identifier.
|
||||||
Model string `json:"model"`
|
Model string `json:"model"`
|
||||||
// Temperature is the effective sampling temperature from 0 through 2.
|
// Temperature is the resolved sampling temperature from 0 through 2. Zero
|
||||||
|
// leaves the field unspecified to compatible providers unless the
|
||||||
|
// corresponding ExecutionTargetPresence bit is true.
|
||||||
Temperature float64 `json:"temperature"`
|
Temperature float64 `json:"temperature"`
|
||||||
// MaxTokens is the non-negative effective output-token limit. Zero leaves
|
// MaxTokens is the non-negative resolved output-token limit. Zero leaves
|
||||||
// the limit unspecified to compatible providers unless it was an explicit
|
// the limit unspecified to compatible providers unless the corresponding
|
||||||
// request override.
|
// ExecutionTargetPresence bit is true.
|
||||||
MaxTokens int `json:"max_tokens"`
|
MaxTokens int `json:"max_tokens"`
|
||||||
// TopP is the effective nucleus-sampling value from 0 through 1.
|
// TopP is the resolved nucleus-sampling value from 0 through 1. Zero leaves
|
||||||
|
// the field unspecified to compatible providers unless the corresponding
|
||||||
|
// ExecutionTargetPresence bit is true.
|
||||||
TopP float64 `json:"top_p"`
|
TopP float64 `json:"top_p"`
|
||||||
// TimeoutSeconds is the non-negative per-generation deadline. Zero disables
|
// TimeoutSeconds is the non-negative per-generation deadline. Zero disables
|
||||||
// this deadline without disabling caller cancellation or the transport cap.
|
// this deadline without disabling caller cancellation or the transport cap.
|
||||||
@@ -329,9 +335,11 @@ type ExecutionTarget struct {
|
|||||||
type ProfileInspection struct {
|
type ProfileInspection struct {
|
||||||
// ProfileID is the trimmed, exact profile ID inspected by the engine.
|
// ProfileID is the trimmed, exact profile ID inspected by the engine.
|
||||||
ProfileID string
|
ProfileID string
|
||||||
// EffectiveModelParams contains framework defaults overlaid by the selected
|
// EffectiveModelParams contains settings resolved from the framework timeout
|
||||||
// backend and then the profile, without a request override. APIKeyEnv is an
|
// baseline, selected backend, and then profile, without a request override.
|
||||||
// environment-variable name, never its credential value.
|
// Unset optional provider controls remain zero rather than reporting a
|
||||||
|
// provider default. APIKeyEnv is an environment-variable name, never its
|
||||||
|
// credential value.
|
||||||
EffectiveModelParams ExecutionTarget
|
EffectiveModelParams ExecutionTarget
|
||||||
// APIKeyRequired reports that a later execution must supply a direct API
|
// APIKeyRequired reports that a later execution must supply a direct API
|
||||||
// key or an explicit request environment override. It is mutually exclusive
|
// key or an explicit request environment override. It is mutually exclusive
|
||||||
@@ -382,19 +390,26 @@ type PromptInspection struct {
|
|||||||
// fields replace profile values and preserve explicit zero or empty values. A
|
// fields replace profile values and preserve explicit zero or empty values. A
|
||||||
// non-empty ExtraParams map replaces the complete profile or backend map
|
// non-empty ExtraParams map replaces the complete profile or backend map
|
||||||
// rather than merging keys. Empty string fields, nil pointers, and a nil or
|
// rather than merging keys. Empty string fields, nil pointers, and a nil or
|
||||||
// empty ExtraParams map inherit the selected profile over its backend, when
|
// empty ExtraParams map inherit lower-precedence values. An optional provider
|
||||||
// any, and framework defaults.
|
// control that remains zero is unspecified; TimeoutSeconds retains its
|
||||||
|
// framework deadline when no higher-precedence value is present.
|
||||||
type ExecutionTargetOverride struct {
|
type ExecutionTargetOverride struct {
|
||||||
// Endpoint replaces the profile or backend endpoint when non-empty without
|
// Endpoint replaces the profile or backend endpoint when non-empty without
|
||||||
// changing the effective BackendID.
|
// changing the effective BackendID.
|
||||||
Endpoint string
|
Endpoint string
|
||||||
// Model replaces the profile model when non-empty.
|
// Model replaces the profile model when non-empty.
|
||||||
Model string
|
Model string
|
||||||
// Temperature, when non-nil, must point to a value from 0 through 2.
|
// Temperature, when non-nil, must point to a value from 0 through 2. A
|
||||||
|
// pointed-to zero is explicitly present; nil inherits a lower-precedence
|
||||||
|
// value and otherwise leaves the provider control unspecified.
|
||||||
Temperature *float64
|
Temperature *float64
|
||||||
// MaxTokens, when non-nil, must point to a non-negative value.
|
// MaxTokens, when non-nil, must point to a non-negative value. A pointed-to
|
||||||
|
// zero is explicitly present; nil inherits a lower-precedence value and
|
||||||
|
// otherwise leaves the provider control unspecified.
|
||||||
MaxTokens *int
|
MaxTokens *int
|
||||||
// TopP, when non-nil, must point to a value from 0 through 1.
|
// TopP, when non-nil, must point to a value from 0 through 1. A pointed-to
|
||||||
|
// zero is explicitly present; nil inherits a lower-precedence value and
|
||||||
|
// otherwise leaves the provider control unspecified.
|
||||||
TopP *float64
|
TopP *float64
|
||||||
// TimeoutSeconds, when non-nil, must point to a non-negative value. A
|
// TimeoutSeconds, when non-nil, must point to a non-negative value. A
|
||||||
// pointed-to zero disables the per-generation deadline.
|
// pointed-to zero disables the per-generation deadline.
|
||||||
@@ -425,9 +440,12 @@ type ExecutionTargetOverride struct {
|
|||||||
// use profile YAML api_key_env with file and FS profile sources. Profile has no
|
// use profile YAML api_key_env with file and FS profile sources. Profile has no
|
||||||
// stable JSON representation.
|
// stable JSON representation.
|
||||||
//
|
//
|
||||||
// WithProfiles validates and copies Profile values during NewEngine. Numeric
|
// WithProfiles validates and copies Profile values during NewEngine. Zero
|
||||||
// zero, blank strings, and an empty ExtraParams map inherit framework defaults;
|
// Temperature, MaxTokens, and TopP values and blank ServiceTier and
|
||||||
// use ExecutionTargetOverride pointer fields to request explicit numeric zero.
|
// ReasoningEffort values leave those provider controls unspecified. A zero
|
||||||
|
// TimeoutSeconds retains the framework deadline, while an empty ExtraParams map
|
||||||
|
// inherits backend request defaults. Use ExecutionTargetOverride pointer fields
|
||||||
|
// to request an explicit numeric zero.
|
||||||
type Profile struct {
|
type Profile struct {
|
||||||
// ID is the required non-blank profile identifier. WithProfiles trims it.
|
// ID is the required non-blank profile identifier. WithProfiles trims it.
|
||||||
ID string
|
ID string
|
||||||
@@ -441,19 +459,19 @@ type Profile struct {
|
|||||||
Endpoint string
|
Endpoint string
|
||||||
// Model is the required non-blank provider model identifier.
|
// Model is the required non-blank provider model identifier.
|
||||||
Model string
|
Model string
|
||||||
// Temperature is from 0 through 2. Zero inherits the framework default.
|
// Temperature is from 0 through 2. Zero leaves the provider control
|
||||||
|
// unspecified.
|
||||||
Temperature float64
|
Temperature float64
|
||||||
// MaxTokens is non-negative. Zero inherits the framework default.
|
// MaxTokens is non-negative. Zero leaves the provider control unspecified.
|
||||||
MaxTokens int
|
MaxTokens int
|
||||||
// TopP is from 0 through 1. Zero inherits the framework default rather than
|
// TopP is from 0 through 1. Zero leaves the provider control unspecified
|
||||||
// selecting an explicit zero.
|
// rather than selecting an explicit zero.
|
||||||
TopP float64
|
TopP float64
|
||||||
// TimeoutSeconds is non-negative. Zero inherits the framework default.
|
// TimeoutSeconds is non-negative. Zero retains the framework deadline.
|
||||||
TimeoutSeconds int
|
TimeoutSeconds int
|
||||||
// ServiceTier is optional; a blank value inherits the framework default.
|
// ServiceTier is optional; a blank value leaves it unspecified.
|
||||||
ServiceTier string
|
ServiceTier string
|
||||||
// ReasoningEffort is optional; a blank value inherits the framework
|
// ReasoningEffort is optional; a blank value leaves it unspecified.
|
||||||
// default.
|
|
||||||
ReasoningEffort string
|
ReasoningEffort string
|
||||||
// APIKeyRequired clears a backend's inherited API-key environment name and
|
// APIKeyRequired clears a backend's inherited API-key environment name and
|
||||||
// requires a non-blank RunRequest.APIKey unless the request explicitly
|
// requires a non-blank RunRequest.APIKey unless the request explicitly
|
||||||
|
|||||||
Reference in New Issue
Block a user