Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ff9aa3efd | |||
| 78b78ea00b | |||
| 63e9f0b731 | |||
| 20b0f78af2 | |||
| 101a2840d4 | |||
| f083ad5309 | |||
| 19a0604874 | |||
| ea31e71280 | |||
| 27966c1686 | |||
| ea63a7b0ff | |||
| b5b9b1f209 | |||
| 129959b24c | |||
| dfe9d6566f | |||
| bf3d2e19bd | |||
| 4cbd4e2da0 | |||
| b0a16ccce8 | |||
| 8add7c49e6 | |||
| 8563379216 | |||
| 951739eda0 | |||
| a0f696181f | |||
| 3951dcf1f3 | |||
| 363920deb2 | |||
| 06315062b3 | |||
| c669f14610 | |||
| 52287ab2d1 | |||
| a43e0f17ee | |||
| ffaaf552c8 | |||
| d7007eb072 |
@@ -45,6 +45,14 @@ boundary and constraints that framework work must preserve.
|
||||
|
||||
## Release Guidance
|
||||
|
||||
The [v0.20.0 release notes](docs/releases/v0.20.0.md) cover resolved resource
|
||||
inspection, generic JSON/YAML reports, typed prompt sources, and backend
|
||||
inheritance. Upgrading consumers should review the changed inspection result
|
||||
types and profile null-fallback behavior.
|
||||
|
||||
The [v0.19.0 release notes](docs/releases/v0.19.0.md) cover audit remediation
|
||||
and compatibility changes to artifact ownership, programmatic sources, and schemas.
|
||||
|
||||
The [v0.18.0 release notes](docs/releases/v0.18.0.md) cover the default-on
|
||||
OpenRouter backend and profiles, recorded configuration normalization, and
|
||||
ConfigKit v1.1. The notes explain inventory changes and the bundle opt-out;
|
||||
|
||||
20
backends.go
20
backends.go
@@ -19,10 +19,13 @@ const BackendLocal = "local"
|
||||
// explicit zero or blank values. A nil ExtraParams map is omitted; a non-nil
|
||||
// empty map supplies an empty mapping. A nil SmokeTests pointer is omitted; a
|
||||
// non-nil pointer supplies a smoke_tests mapping. Use raw ConfigKit Mapping
|
||||
// inputs for explicit null. BackendConfig has no stable JSON representation.
|
||||
// inputs for explicit null. Extends names a sibling backend in the same list.
|
||||
// BackendConfig has no stable JSON representation.
|
||||
type BackendConfig struct {
|
||||
// ID is the local backend identity within its resource list.
|
||||
ID string
|
||||
// Extends names a sibling backend to inherit through ConfigKit.
|
||||
Extends *string
|
||||
// Endpoint is the required absolute HTTP(S) base URL after all layers merge.
|
||||
Endpoint *string
|
||||
// APIKeyEnv optionally names an environment variable, not a credential.
|
||||
@@ -44,13 +47,13 @@ type BackendSmokeTestConfig struct {
|
||||
// DefaultProfile is the exact profile ID selected as the backend's
|
||||
// diagnostic inference default. A supplied value must not be blank; the ID
|
||||
// is resolved only when that default is selected.
|
||||
DefaultProfile *string
|
||||
DefaultProfile *string `json:"default_profile,omitempty" yaml:"default_profile,omitempty"`
|
||||
// Reachability optionally selects a full HTTP destination for reachability
|
||||
// diagnostics. A nil endpoint uses the effective backend endpoint.
|
||||
Reachability *ReachabilityProbeConfig
|
||||
Reachability *ReachabilityProbeConfig `json:"reachability,omitempty" yaml:"reachability,omitempty"`
|
||||
// Authentication optionally declares a named authentication diagnostic.
|
||||
// A supplied object requires Kind.
|
||||
Authentication *AuthenticationProbeConfig
|
||||
Authentication *AuthenticationProbeConfig `json:"authentication,omitempty" yaml:"authentication,omitempty"`
|
||||
}
|
||||
|
||||
// ReachabilityProbeConfig configures an optional reachability destination.
|
||||
@@ -58,7 +61,7 @@ type ReachabilityProbeConfig struct {
|
||||
// Endpoint is an absolute HTTP(S) URL, including any desired path or query.
|
||||
// Nil selects the effective backend endpoint; an explicit blank value is
|
||||
// invalid.
|
||||
Endpoint *string
|
||||
Endpoint *string `json:"endpoint,omitempty" yaml:"endpoint,omitempty"`
|
||||
}
|
||||
|
||||
// AuthenticationProbeConfig configures an optional named authentication
|
||||
@@ -66,11 +69,11 @@ type ReachabilityProbeConfig struct {
|
||||
type AuthenticationProbeConfig struct {
|
||||
// Kind selects the authentication adapter. Supported values are "none" and
|
||||
// "openrouter_key"; surrounding whitespace and case are normalized.
|
||||
Kind *string
|
||||
Kind *string `json:"kind,omitempty" yaml:"kind,omitempty"`
|
||||
// Endpoint is an absolute HTTP(S) URL, including any desired path or query.
|
||||
// Nil selects the adapter's backend-relative destination; an explicit blank
|
||||
// value is invalid.
|
||||
Endpoint *string
|
||||
Endpoint *string `json:"endpoint,omitempty" yaml:"endpoint,omitempty"`
|
||||
}
|
||||
|
||||
// BackendSource creates a labeled ConfigKit mapping containing backend records
|
||||
@@ -84,6 +87,9 @@ func BackendSource(label string, backends []BackendConfig) configkit.Source {
|
||||
definitions := make([]any, 0, len(backends))
|
||||
for _, config := range backends {
|
||||
fields := map[string]any{"id": config.ID}
|
||||
if config.Extends != nil {
|
||||
fields["extends"] = map[string]any{"id": *config.Extends}
|
||||
}
|
||||
if config.Endpoint != nil {
|
||||
fields["endpoint"] = *config.Endpoint
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ func TestBundledCompositionReplacementAndInheritance(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
underlayProfile, err := underlayEngine.InspectProfile(context.Background(), "gpt-sol-latest")
|
||||
if err != nil || underlayProfile.EffectiveModelParams.Model != "underlay-model" || underlayProfile.EffectiveModelParams.BackendID != "" {
|
||||
if err != nil || underlayProfile.Model == nil || *underlayProfile.Model != "underlay-model" || underlayProfile.Backend != nil {
|
||||
t.Fatalf("underlay replacement = %+v, %v", underlayProfile, err)
|
||||
}
|
||||
main := configkit.Input{Source: configkit.Mapping("caller main", map[string]any{
|
||||
@@ -38,29 +38,29 @@ func TestBundledCompositionReplacementAndInheritance(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inspect := func(id string) promptkit.ExecutionTarget {
|
||||
inspect := func(id string) *promptkit.ProfileInspection {
|
||||
t.Helper()
|
||||
profile, err := engine.InspectProfile(context.Background(), id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return profile.EffectiveModelParams
|
||||
return profile
|
||||
}
|
||||
if target := inspect("gpt-sol-latest"); target.Model != "main-model" || target.BackendID != "" {
|
||||
if target := inspect("gpt-sol-latest"); target.Model == nil || *target.Model != "main-model" || target.Backend != nil {
|
||||
t.Fatalf("replacement = %+v", target)
|
||||
}
|
||||
if target := inspect("custom"); target.Model != "custom-model" || target.BackendID != "openrouter" || target.Endpoint != "https://local.invalid/v1" {
|
||||
if target := inspect("custom"); target.Model == nil || *target.Model != "custom-model" || target.Backend == nil || *target.Backend != "openrouter" || target.Endpoint != nil {
|
||||
t.Fatalf("inherited = %+v", target)
|
||||
}
|
||||
if target := inspect("gpt-luna-latest"); target.Endpoint != "https://local.invalid/v1" || target.APIKeyEnv != "" {
|
||||
t.Fatalf("backend replacement = %+v", target)
|
||||
if backend, err := engine.InspectBackend(context.Background(), "openrouter"); err != nil || backend.Endpoint != "https://local.invalid/v1" || backend.APIKeyEnv != nil {
|
||||
t.Fatalf("backend replacement = %+v, %v", backend, err)
|
||||
}
|
||||
if target := inspect("openrouter/profile"); target.BackendID != "openrouter/local" || target.Model != "collection-model" {
|
||||
if target := inspect("openrouter/profile"); target.Backend == nil || *target.Backend != "openrouter/local" || target.Model == nil || *target.Model != "collection-model" {
|
||||
t.Fatalf("collection = %+v", target)
|
||||
}
|
||||
target := inspect("auto-low")
|
||||
wantPlugins := map[string]any{"plugins": []any{map[string]any{"id": "auto-router", "cost_tier": "low"}}}
|
||||
if target.BackendID != "other" || target.Model != "other-model" || !reflect.DeepEqual(target.ExtraParams, wantPlugins) {
|
||||
if target.Backend == nil || *target.Backend != "other" || target.Model == nil || *target.Model != "other-model" || target.ExtraParams == nil || !reflect.DeepEqual(*target.ExtraParams, wantPlugins) {
|
||||
t.Errorf("inherited tier = %+v", target)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -59,7 +59,7 @@ func TestBundledOpenRouterOptionsAndInventory(t *testing.T) {
|
||||
t.Fatalf("bundle inventory: %v %v", profiles, backends)
|
||||
}
|
||||
profile, err := engine.InspectProfile(context.Background(), "auto")
|
||||
if err != nil || profile.EffectiveModelParams.BackendID != "openrouter" {
|
||||
if err != nil || profile.Backend == nil || *profile.Backend != "openrouter" {
|
||||
t.Fatalf("bundle profile = %+v, %v", profile, err)
|
||||
}
|
||||
view, err := engine.InspectConfiguration()
|
||||
|
||||
@@ -58,7 +58,7 @@ func TestCollectionInputLoadsNativeImportsAndLaterOverride(t *testing.T) {
|
||||
t.Fatalf("construct engine with native collection import: %v", err)
|
||||
}
|
||||
inspection, err := engine.InspectPrompt(context.Background(), "docs/greeting")
|
||||
if err != nil || inspection.PromptID != "docs/greeting" || len(inspection.Inputs) != 1 || inspection.Inputs[0].Name != "overridden" {
|
||||
if err != nil || inspection.PromptID != "docs/greeting" || inspection.Inputs == nil || len(*inspection.Inputs) != 1 || (*inspection.Inputs)[0].Name != "overridden" {
|
||||
t.Fatalf("inspect imported and overlaid prompt = (%+v, %v)", inspection, err)
|
||||
}
|
||||
}
|
||||
@@ -85,7 +85,7 @@ func TestCollectionInputLoadsEmbeddedImportsAndLaterOverride(t *testing.T) {
|
||||
t.Fatalf("construct engine with embedded collection import: %v", err)
|
||||
}
|
||||
inspection, err := engine.InspectPrompt(context.Background(), "docs/greeting")
|
||||
if err != nil || inspection.PromptID != "docs/greeting" || len(inspection.Inputs) != 1 || inspection.Inputs[0].Name != "overridden" {
|
||||
if err != nil || inspection.PromptID != "docs/greeting" || inspection.Inputs == nil || len(*inspection.Inputs) != 1 || (*inspection.Inputs)[0].Name != "overridden" {
|
||||
t.Fatalf("inspect imported and overlaid prompt = (%+v, %v)", inspection, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,7 +134,7 @@ func TestFormerCatalogBackendIDsWorkWhenConfigured(t *testing.T) {
|
||||
"rakestrawhome-profile": rakestrawhomeID,
|
||||
} {
|
||||
inspection, err := engine.InspectProfile(context.Background(), profileID)
|
||||
if err != nil || inspection.EffectiveModelParams.BackendID != backendID {
|
||||
if err != nil || inspection.Backend == nil || *inspection.Backend != backendID {
|
||||
t.Fatalf("inspect %q = (%+v, %v), want backend %q", profileID, inspection, err, backendID)
|
||||
}
|
||||
}
|
||||
@@ -242,8 +242,9 @@ func TestPublicConstructionUsesProgrammaticBackendAndProfileSources(t *testing.T
|
||||
if err != nil {
|
||||
t.Fatalf("inspect configured profile: %v", err)
|
||||
}
|
||||
if inspection.EffectiveModelParams.Endpoint != endpoint {
|
||||
t.Fatalf("effective endpoint = %q, want %q", inspection.EffectiveModelParams.Endpoint, endpoint)
|
||||
backendInspection, err := engine.InspectBackend(context.Background(), backendID)
|
||||
if err != nil || inspection.Endpoint != nil || backendInspection.Endpoint != endpoint {
|
||||
t.Fatalf("profile/backend = %+v / %+v, %v", inspection, backendInspection, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
70
convert.go
70
convert.go
@@ -4,6 +4,7 @@ import (
|
||||
"fmt"
|
||||
"unicode/utf8"
|
||||
|
||||
"go.maximumdirect.net/promptkit/internal/configuration"
|
||||
"go.maximumdirect.net/promptkit/internal/domain"
|
||||
"go.maximumdirect.net/promptkit/internal/jsonvalue"
|
||||
)
|
||||
@@ -228,37 +229,58 @@ func fromDomainExecutionTargetWithOwnedParams(target domain.ExecutionTarget, ext
|
||||
}
|
||||
}
|
||||
|
||||
func fromDomainProfileInspection(inspection *domain.ProfileInspection) *ProfileInspection {
|
||||
if inspection == nil {
|
||||
return nil
|
||||
}
|
||||
return &ProfileInspection{
|
||||
ProfileID: inspection.ProfileID,
|
||||
EffectiveModelParams: fromDomainExecutionTarget(inspection.EffectiveModelParams),
|
||||
APIKeyRequired: inspection.APIKeyRequired,
|
||||
func fromProfileResource(v *configuration.ProfileResource) *ProfileInspection {
|
||||
out := &ProfileInspection{ProfileID: v.ID, Backend: copyStringPtr(v.Backend), Endpoint: copyStringPtr(v.Endpoint), Model: copyStringPtr(v.Model), Temperature: copyFloat64Ptr(v.Temperature), TopP: copyFloat64Ptr(v.TopP), MaxTokens: copyIntPtr(v.MaxTokens), TimeoutSeconds: copyIntPtr(v.TimeoutSeconds), ServiceTier: copyStringPtr(v.ServiceTier), ReasoningEffort: copyStringPtr(v.ReasoningEffort), APIKeyEnv: copyStringPtr(v.APIKeyEnv), APIKeyRequired: copyBoolPtr(v.APIKeyRequired)}
|
||||
if v.ExtraParams != nil {
|
||||
data := copyAnyMap(v.ExtraParams)
|
||||
out.ExtraParams = &data
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func fromDomainPromptInspection(inspection *domain.PromptInspection) *PromptInspection {
|
||||
if inspection == nil {
|
||||
return nil
|
||||
func fromBackendResource(v *configuration.BackendResource) *BackendInspection {
|
||||
out := &BackendInspection{BackendID: v.ID, Endpoint: v.Endpoint, APIKeyEnv: copyStringPtr(v.APIKeyEnv), ConcurrencyLimit: copyIntPtr(v.ConcurrencyLimit), QueueCapacity: copyIntPtr(v.QueueCapacity)}
|
||||
if v.ExtraParams != nil {
|
||||
data := copyAnyMap(v.ExtraParams)
|
||||
out.ExtraParams = &data
|
||||
}
|
||||
inputs := make([]PromptInputDefinition, len(inspection.Inputs))
|
||||
for i, input := range inspection.Inputs {
|
||||
inputs[i] = PromptInputDefinition{
|
||||
Name: input.Name,
|
||||
Required: input.Required,
|
||||
ContentType: input.ContentType,
|
||||
Description: input.Description,
|
||||
if v.SmokeTests != nil {
|
||||
out.SmokeTests = &BackendSmokeTestConfig{DefaultProfile: copyStringPtr(v.SmokeTests.DefaultProfile)}
|
||||
if v.SmokeTests.Reachability != nil {
|
||||
out.SmokeTests.Reachability = &ReachabilityProbeConfig{Endpoint: copyStringPtr(v.SmokeTests.Reachability.Endpoint)}
|
||||
}
|
||||
if v.SmokeTests.Authentication != nil {
|
||||
out.SmokeTests.Authentication = &AuthenticationProbeConfig{Kind: copyStringPtr(v.SmokeTests.Authentication.Kind), Endpoint: copyStringPtr(v.SmokeTests.Authentication.Endpoint)}
|
||||
}
|
||||
}
|
||||
return &PromptInspection{
|
||||
PromptID: inspection.PromptID,
|
||||
PromptHash: inspection.PromptHash,
|
||||
DefaultProfileID: inspection.DefaultProfileID,
|
||||
Inputs: inputs,
|
||||
OutputContract: fromDomainOutputContract(inspection.OutputContract),
|
||||
return out
|
||||
}
|
||||
|
||||
func fromPromptResource(v *configuration.PromptResource) *PromptInspection {
|
||||
out := &PromptInspection{PromptID: v.ID, PromptHash: v.Hash, DefaultProfileID: copyStringPtr(v.DefaultProfile), Description: copyStringPtr(v.Description), SessionID: copyStringPtr(v.SessionID), OutputContract: PromptOutputInspection{Format: OutputFormat(v.Output.Format), ValidationMode: ValidationMode(v.Output.ValidationMode), SchemaPath: copyStringPtr(v.Output.SchemaPath), RepairAttempts: copyIntPtr(v.Output.RepairAttempts)}}
|
||||
if v.Inputs != nil {
|
||||
inputs := make([]PromptInputInspection, len(*v.Inputs))
|
||||
for i, input := range *v.Inputs {
|
||||
inputs[i] = PromptInputInspection{Name: input.Name, Required: copyBoolPtr(input.Required), ContentType: copyStringPtr(input.ContentType), Description: copyStringPtr(input.Description)}
|
||||
}
|
||||
out.Inputs = &inputs
|
||||
}
|
||||
out.Messages = make([]PromptMessageInspection, len(v.Messages))
|
||||
for i, message := range v.Messages {
|
||||
out.Messages[i] = PromptMessageInspection{Role: message.Role, Content: message.Content}
|
||||
if message.CacheControl != nil {
|
||||
out.Messages[i].CacheControl = &PromptCacheControlInspection{Type: CacheControlType(message.CacheControl.Type), TTL: copyStringPtr(message.CacheControl.TTL)}
|
||||
}
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func copyBoolPtr(src *bool) *bool {
|
||||
if src == nil {
|
||||
return nil
|
||||
}
|
||||
value := *src
|
||||
return &value
|
||||
}
|
||||
|
||||
func fromDomainExecutionTargetPresence(presence domain.ExecutionTargetPresence) ExecutionTargetPresence {
|
||||
|
||||
26
doc.go
26
doc.go
@@ -4,17 +4,17 @@
|
||||
// Applications construct an [Engine] with [NewEngine], select ConfigKit
|
||||
// sources for prompts, profiles, and backends, and
|
||||
// call [Engine.InspectConfiguration], [Engine.InspectPrompt],
|
||||
// [Engine.InspectRequirements], [Engine.InspectProfile], [Engine.ListProfiles],
|
||||
// [Engine.ListPrompts], [Engine.Prepare], [Engine.PrepareExecution],
|
||||
// [Engine.Run], or [Engine.RunPrepared]. Concrete registries, repositories,
|
||||
// [Engine.InspectRequirements], [Engine.InspectProfile], [Engine.InspectBackend],
|
||||
// [Engine.ListProfiles], [Engine.ListPrompts], [Engine.Prepare],
|
||||
// [Engine.PrepareExecution], [Engine.Run], or [Engine.RunPrepared]. Concrete registries, repositories,
|
||||
// validators, and the built-in OpenAI-compatible client remain internal
|
||||
// implementation details.
|
||||
//
|
||||
// # Concurrency and ownership
|
||||
//
|
||||
// An Engine supports concurrent InspectConfiguration, InspectPrompt,
|
||||
// InspectRequirements, InspectProfile, ListProfiles, ListPrompts, Prepare,
|
||||
// PrepareExecution, Run, and RunPrepared calls. Engine-local backend policies
|
||||
// InspectRequirements, InspectProfile, InspectBackend, ListProfiles, ListPrompts,
|
||||
// Prepare, PrepareExecution, Run, and RunPrepared calls. Engine-local backend policies
|
||||
// bound admitted Run and RunPrepared calls and model generations where
|
||||
// configured, while different backend pools and unlimited backends continue
|
||||
// independently. An injected [LLMClient] or [ArtifactReader] can therefore
|
||||
@@ -23,7 +23,8 @@
|
||||
// ConfigKit captures the selected configuration when NewEngine constructs an
|
||||
// Engine. Prepare, PrepareExecution, and Run copy request maps, slices, pointer
|
||||
// values, and JSON-compatible extra parameters before using them. InspectPrompt,
|
||||
// InspectRequirements, and InspectProfile return copied inspection values.
|
||||
// InspectRequirements, InspectProfile, and InspectBackend return copied
|
||||
// inspection values.
|
||||
// ListProfiles and ListPrompts return caller-owned identity slices.
|
||||
// Returned values and values passed to extension interfaces are likewise
|
||||
// isolated from engine state.
|
||||
@@ -56,17 +57,22 @@
|
||||
// [GenerateResponse], [ExecutionTargetPresence], and the string value types
|
||||
// used by those values.
|
||||
//
|
||||
// Construction, inspection, handle, and error values, including [Config],
|
||||
// Resource inspection results support direct JSON and YAML encoding.
|
||||
// [PromptInspection], [ProfileInspection], [BackendInspection], and their nested
|
||||
// values define field names, nesting, and omission of absent optional fields.
|
||||
// Formatting belongs to the selected encoder.
|
||||
//
|
||||
// Other construction, inspection, handle, and error values, including [Config],
|
||||
// [ConfigurationSources], [ConfigurationInspection], [BackendConfig],
|
||||
// [ProfileConfig], [RunRequest],
|
||||
// [ArtifactRef], [ExecutionTargetOverride], [ProfileInspection],
|
||||
// [PromptIdentity], [PromptInputDefinition], [PromptInspection],
|
||||
// [ArtifactRef], [ExecutionTargetOverride],
|
||||
// [PromptIdentity], [PromptInputDefinition],
|
||||
// [RequirementInspectionRequest], [RequirementInspection], [PreparedExecution],
|
||||
// [CapacityError], [GenerationError], [RetryExhaustedError], and
|
||||
// [ExecutionEvent], do not have stable JSON
|
||||
// representations. Direct API keys are nevertheless excluded from JSON for
|
||||
// every public value.
|
||||
|
||||
//
|
||||
// Provider-derived [GenerationError] accessor values are untrusted and can
|
||||
// contain sensitive request or schema fragments. Applications must apply their
|
||||
// own disclosure policy before logging, displaying, or returning them.
|
||||
|
||||
@@ -135,8 +135,10 @@ Replace former maps passed to `BackendSource` in the same way, setting each
|
||||
inspection paths now address list indexes, for example
|
||||
`configkit.Keys("profiles").Index(0).Key("model")`. Configuration inspection
|
||||
and diffs expose resolved values, recorded normalization and provenance.
|
||||
`InspectProfile` additionally applies backend completion; request preparation
|
||||
can apply further overrides, so the views are not promised to be identical.
|
||||
`InspectProfile` reports the selected resolved profile without backend completion.
|
||||
Use `Prepare` or `PreparedExecution.Details` for completed request settings,
|
||||
including backend defaults and request overrides; see the
|
||||
[resource inspection guidance](pkg-promptkit.md#inspect-a-profile-before-prompt-work).
|
||||
|
||||
Remove `PromptVersion` and `PromptIdentity.Version` uses and the corresponding
|
||||
`prompt_version` JSON members. Prompt definition hashes are opaque equality
|
||||
|
||||
@@ -94,7 +94,7 @@ engine, err := promptkit.NewEngine(promptkit.Config{
|
||||
|
||||
For a single highest-priority document, set `Sources.Main` to a
|
||||
`*configkit.Input`; omit it for ordinary layered construction. A single file
|
||||
can use `configkit.File` directly. `ProfileSource`, `BackendSource`, and raw
|
||||
can use `configkit.File` directly. `PromptSource`, `ProfileSource`, `BackendSource`, and raw
|
||||
`configkit.Mapping` supply programmatic definitions. Schema and runtime
|
||||
collaborator options remain available. Construction validates the final
|
||||
effective configuration and captures referenced prompt content; construct a
|
||||
@@ -177,8 +177,12 @@ input := configkit.Input{Source: document}
|
||||
// Add input to a later underlay, or set Sources.Main = &input.
|
||||
```
|
||||
|
||||
`ProfileSource` and `BackendSource` are smaller typed conveniences when null
|
||||
is not needed. Their provider maps use ConfigKit Mapping input forms: built-in
|
||||
`PromptSource`, `ProfileSource`, and `BackendSource` are typed conveniences when
|
||||
null is not needed. Prompt and backend helpers, like profiles, support sibling
|
||||
inheritance via `Extends`; partial records can be completed by other layers.
|
||||
For relative prompt `content_file` references, supply the ordinary ConfigKit
|
||||
`Input.Filesystem` context for that source. The helpers do not read files or
|
||||
validate final definitions. Profile and backend provider maps use ConfigKit Mapping input forms: built-in
|
||||
scalars, `[]any`, and `map[string]any` recursively. Convert `json.Number` or
|
||||
typed containers explicitly to an appropriate supported value, choosing numeric
|
||||
precision for your application. Mapping captures supported values immediately;
|
||||
@@ -264,9 +268,10 @@ is not necessarily safe to share. Inspection uses the engine's captured result
|
||||
and does not reopen sources or resolve credentials. It retains effective values,
|
||||
including recorded field normalization and queue defaulting, alongside source
|
||||
provenance.
|
||||
`InspectProfile` instead shows backend-completed execution settings for one profile, and `Prepare` or
|
||||
`PreparedExecution.Details` shows request-specific rendered messages, inputs,
|
||||
and validation resources. None of those views is a substitute for the others.
|
||||
`InspectPrompt`, `InspectProfile`, and `InspectBackend` instead show individual
|
||||
resolved resources without backend-to-profile execution completion or provenance.
|
||||
`Prepare` or `PreparedExecution.Details` shows completed request-specific settings,
|
||||
rendered messages, inputs, and validation resources. None substitutes for the others.
|
||||
|
||||
To review changes between two independently captured configurations, compare
|
||||
their inspection views:
|
||||
@@ -380,8 +385,10 @@ if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, input := range inspection.Inputs {
|
||||
// Compare the declared input with application configuration.
|
||||
if inspection.Inputs != nil {
|
||||
for _, input := range *inspection.Inputs {
|
||||
// Compare the declared input with application configuration.
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@@ -707,10 +714,9 @@ composition rules.
|
||||
|
||||
### Inspect A Profile Before Prompt Work
|
||||
|
||||
Use [`Engine.InspectProfile`](../../engine.go) to validate one configured
|
||||
profile without constructing a synthetic prompt or placeholder inputs. It
|
||||
resolves the profile's effective target but does not prepare or execute a
|
||||
prompt:
|
||||
Use [`Engine.InspectProfile`](../../engine.go) to inspect a resolved profile
|
||||
without constructing a synthetic prompt or placeholder inputs. It reports the
|
||||
profile's own fields, not a backend-completed target:
|
||||
|
||||
```go
|
||||
inspection, err := engine.InspectProfile(ctx, profileID)
|
||||
@@ -718,24 +724,48 @@ if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
target := inspection.EffectiveModelParams
|
||||
if inspection.APIKeyRequired {
|
||||
if inspection.APIKeyRequired != nil && *inspection.APIKeyRequired {
|
||||
// Arrange a usable direct or configured environment credential.
|
||||
}
|
||||
if target.APIKeyEnv != "" {
|
||||
if inspection.APIKeyEnv != nil {
|
||||
// This is the configured environment lookup source, not its value.
|
||||
}
|
||||
```
|
||||
|
||||
Use this configuration-time boundary when only the profile and its target need
|
||||
checking. Use `Prepare` when the application also needs prompt, input, schema,
|
||||
or rendering work; use prepared execution when that work must remain tied to a
|
||||
later execution. A reported `APIKeyEnv` identifies a configured source, while
|
||||
`APIKeyRequired` is the explicit requirement; both may be present. The
|
||||
Use [`InspectBackend`](../../engine.go) to inspect the referenced backend
|
||||
separately. Its diagnostics are configured metadata, not live checks or derived
|
||||
destinations; `ErrBackendNotFound` identifies an absent backend. References in
|
||||
all three resource reports remain as authored. Use `Prepare` for completed
|
||||
request settings and rendering, or prepared execution when that work must remain
|
||||
tied to later execution. A reported `APIKeyEnv` identifies a configured source,
|
||||
not its value; `APIKeyRequired` can also be present. The
|
||||
[credential format reference](../formats.md#credentials) and the method's
|
||||
[GoDoc](../../engine.go) own the exact precedence, timing, result, and error
|
||||
contracts.
|
||||
|
||||
Resource inspection pointers represent optional fields: nil is absent, while a
|
||||
non-nil pointer preserves zero, false, or an empty string. Pointer-to-collection
|
||||
fields distinguish omission from an empty map or list. Results and nested mutable
|
||||
values belong to the caller. Ordinary generic encoders need no per-field adapter:
|
||||
|
||||
```go
|
||||
jsonBytes, err := json.Marshal(inspection)
|
||||
if err != nil { return err }
|
||||
yamlBytes, err := yaml.Marshal(inspection) // go.yaml.in/yaml/v4
|
||||
if err != nil { return err }
|
||||
_, _ = jsonBytes, yamlBytes
|
||||
```
|
||||
|
||||
For migration, replace `ProfileInspection.EffectiveModelParams` accesses with
|
||||
the optional resource fields, and use `Prepare` or `PreparedExecution.Details`
|
||||
where backend completion and request overrides are needed. Prompt inspection's
|
||||
`DefaultProfileID`, `Inputs`, nested input metadata, and output fields now use
|
||||
optional pointers where presence matters; check for nil before dereferencing.
|
||||
Consumers such as CommandKit can render the results directly instead of copying
|
||||
each field into a parallel report. See the result types' [GoDoc](../../resource_inspection.go)
|
||||
for keys and ownership; resource inspection does not replace `InspectConfiguration`
|
||||
provenance, `InspectRequirements`, or concrete-request `ValidateRequest`.
|
||||
|
||||
### Set A Per-Run Session And Reasoning
|
||||
|
||||
Supply a direct session ID when one prompt should be correlated with a
|
||||
|
||||
@@ -110,8 +110,8 @@ and static template syntax at construction. It does not reread configuration or
|
||||
message files on ordinary runs; construct a new engine to observe edits.
|
||||
Schema and request artifact reads still happen during preparation. Use
|
||||
`InspectConfiguration` for an explicit effective-configuration or provenance
|
||||
report, `InspectProfile` for backend-completed profile settings, and preparation
|
||||
for request-specific rendered details. For identity discovery before inspection,
|
||||
report, resource inspection for resolved prompt, profile, and backend fields,
|
||||
and preparation for completed request-specific details. For identity discovery before inspection,
|
||||
see the consumer guide's [resource and collection listing](consumers/pkg-promptkit.md#discover-available-resources-and-collections).
|
||||
|
||||
## Prompt Definitions
|
||||
@@ -315,17 +315,33 @@ only a model or backend does not clear unrelated explicitly inherited fields.
|
||||
| Profile field | Absent | Explicit value | Explicit null |
|
||||
| --- | --- | --- | --- |
|
||||
| `backend` | No backend | Exact backend ID; blank means none | No backend |
|
||||
| `endpoint` | Selected backend endpoint | Valid nonblank URL; empty clears | Clear, without backend fallback |
|
||||
| `endpoint` | Selected backend endpoint | Valid nonblank URL; empty clears | Selected backend endpoint |
|
||||
| `model` | Unspecified | Nonblank model; empty clears | Unspecified |
|
||||
| `temperature`, `top_p`, `max_tokens` | Unspecified | Valid value, including outbound zero | Suppress outbound control |
|
||||
| `service_tier`, `reasoning_effort` | Unspecified | Normalized value; blank suppresses | Suppress outbound control |
|
||||
| `timeout_seconds` | 600 seconds | Nonnegative; zero disables generation deadline | Reset to 600 seconds |
|
||||
| `api_key_env` | Backend lookup name, if any | Trimmed name; blank disables | Disable backend lookup |
|
||||
| `api_key_env` | Backend lookup name, if any | Trimmed name; blank disables | Backend lookup name, if any |
|
||||
| `api_key_required` | False | Boolean, including false | False |
|
||||
| `extra_params` | Backend defaults | Recursively overlay profile map on backend map | Suppress configured provider map |
|
||||
| `extra_params` | Backend defaults | Recursively overlay profile map on backend map | Backend defaults |
|
||||
|
||||
An empty profile mapping does not clear backend map entries. After ConfigKit
|
||||
composition and inheritance, a whole-map null and omission both permit backend
|
||||
defaults. Nested maps merge recursively, while scalar and array values replace;
|
||||
nulls within provider maps or arrays remain literal data.
|
||||
When `api_key_required` is true and the profile source is absent or null,
|
||||
execution suppresses the backend credential source; an explicitly configured
|
||||
profile source remains in use.
|
||||
|
||||
For example, if the backend provides an endpoint, API-key environment name,
|
||||
and provider map, a profile containing `endpoint: null`, `api_key_env: null`,
|
||||
and `extra_params: null` now inherits those backend values after handoff (unless
|
||||
the required-key rule suppresses the credential source). Earlier versions used
|
||||
those nulls to suppress the corresponding fallback. Use `endpoint: ""` or
|
||||
`api_key_env: ""` to clear those individual strings. There is no profile-level
|
||||
whole-map suppression replacement; change the backend map through ordinary
|
||||
ConfigKit composition when different provider defaults are needed. ConfigKit's
|
||||
retained configuration and provenance continue to show the nulls.
|
||||
|
||||
An empty mapping does not clear inherited map entries; null clears the whole
|
||||
map. Nested maps merge recursively, while scalar and array values replace.
|
||||
Runtime nonempty `ExtraParams` overrides replace the entire configured map.
|
||||
Request overrides otherwise follow the [public value GoDoc](../types.go).
|
||||
For example, a runtime `extra_params["extends"]` key is ordinary provider
|
||||
|
||||
@@ -11,15 +11,15 @@ contributor workflow and validation.
|
||||
|
||||
| Component | Implemented responsibility | References |
|
||||
| --- | --- | --- |
|
||||
| Root `promptkit` package | Provides the supported engine facade, ConfigKit source helpers, collection and resource mapping conveniences, injection options, public requests and results, prompt/profile/requirement inspection and identity inventories, concrete-request validation, explicit smoke diagnostics, observation values, opaque prepared-execution handles, value conversion, redacted formatting, and typed capacity and generation error mapping. | [Package GoDoc](../../doc.go), [observations](../../observations.go), [prepared execution](../../prepared_execution.go), [backend API](../../backends.go), [engine facade](../../engine.go), [smoke diagnostics](../../smoke_diagnostics.go), [identity listing](../../listing.go), [requirement facade](../../requirements.go) |
|
||||
| Root `promptkit` package | Provides the supported engine facade, ConfigKit source helpers, collection and resource mapping conveniences, injection options, public requests and results, resource and requirement inspection and identity inventories, concrete-request validation, explicit smoke diagnostics, observation values, opaque prepared-execution handles, value conversion, redacted formatting, and typed capacity and generation error mapping. | [Package GoDoc](../../doc.go), [observations](../../observations.go), [prepared execution](../../prepared_execution.go), [backend API](../../backends.go), [engine facade](../../engine.go), [smoke diagnostics](../../smoke_diagnostics.go), [identity listing](../../listing.go), [requirement facade](../../requirements.go) |
|
||||
| `examples/go-library/prepare` | Demonstrates offline preparation from two explicitly selected synthetic collections, source-relative imported content, a scoped view, filtered listings, and operator inventory. It is not a public library package. | [Example program](../../examples/go-library/prepare/main.go), [collection inputs](../../examples/go-library/prepare/collections/support/collection.yaml) |
|
||||
| `examples/go-library/run` | Demonstrates an offline downstream consumer using a prompt file, programmatic 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/observe` | Demonstrates offline built-in HTTP retry and synchronous metadata observation across structured proposal and assessment runs using embedded sources, two profiles on one finite backend, and an application decision. It is not a public library package. | [Example program](../../examples/go-library/observe/main.go) |
|
||||
| `internal/backend` | Holds a defensive, immutable lookup of final backend definitions, including diagnostic metadata, plus capacity policies validated by the configuration adapter. | [Backend registry](../../internal/backend/registry.go) |
|
||||
| `internal/configuration` | Loads root and namespaced collection resources through ConfigKit; records effective field presence, validates final values including exact references and optional diagnostic metadata, captures indexed prompt definitions and source-relative message content, compiled templates, input metadata snapshots, sorted backend/profile/prompt identities, resource namespace sets, effective collection declarations, backend and presence-aware profile snapshots, and the unchanged read-only inspection view. | [Typed loader](../../internal/configuration/load.go), [identity listing](../../internal/configuration/listing.go), [internal sources](sources.md#typed-configuration-loading) |
|
||||
| `internal/configuration` | Loads root and namespaced resources through ConfigKit; validates final values, captures indexed resource views and source-relative prompt content, compiled templates, requirement metadata, identity inventories, backend and presence-aware execution profiles, and the read-only provenance inspection view. | [Typed loader](../../internal/configuration/load.go), [identity listing](../../internal/configuration/listing.go), [internal sources](sources.md#typed-configuration-loading) |
|
||||
| `internal/capacity` | Owns engine-local bounded execution admission and FIFO model-generation permits for limited backend IDs, including cancellation-safe waiter removal and client wrapping. | [Internal capacity management](capacity.md) |
|
||||
| `internal/domain` | Defines internal framework values for requests, artifacts, prompt definitions, requirement inspection, profiles, execution targets, rendering, generation, validation, and source-neutral smoke diagnostics, and owns source-neutral invariants for shared execution settings, OpenAI-compatible base endpoints, session identifiers, output contracts, and resource-identity syntax and qualification. Source parsing, required fields, other source-specific normalization, defaulting, and boundary-specific error classification remain with their callers. | [Domain declarations](../../internal/domain/domain.go), [requirement values](../../internal/domain/requirements.go), [endpoint invariant](../../internal/domain/endpoint.go) |
|
||||
| `internal/engine` | Assembles caller-selected configuration into one snapshot, then constructs shared capacity, rendering, validation, generation, repair, the ordinary runner, and the smoke tester for each engine. Immutable namespace views share those collaborators and qualify caller-supplied execution, inspection, and diagnostic selections without rewriting captured references. Built-in engines provide the diagnostic HTTP collaborator; injected-client engines do not. It delegates identity listing directly to the captured configuration inventory rather than the runner. | [Engine assembly](../../internal/engine/engine.go), [internal sources](sources.md#typed-configuration-loading), [internal runner](runner.md) |
|
||||
| `internal/engine` | Assembles caller-selected configuration into one snapshot, then constructs shared capacity, rendering, validation, generation, repair, the ordinary runner, and the smoke tester for each engine. Immutable namespace views share those collaborators and qualify caller-supplied execution, inspection, and diagnostic selections without rewriting captured references. Built-in engines provide the diagnostic HTTP collaborator; injected-client engines do not. It delegates resource inspection and identity listing directly to the captured configuration rather than the runner. | [Engine assembly](../../internal/engine/engine.go), [internal sources](sources.md#typed-configuration-loading), [internal runner](runner.md) |
|
||||
| `internal/generation` | Coordinates frozen built-in HTTP attempts and one-shot injected calls through active backend permits, including bounded status retries, cancellable backoff, ordinary attempt observations, and invocation-local evidence for diagnostics. | [Internal generation coordination](generation.md) |
|
||||
| `internal/defaults` | Defines application-neutral framework constants and constructs the default execution target. It contains no CLI, server, or inbound HTTP limits. | [Framework defaults](../../internal/defaults/defaults.go) |
|
||||
| `internal/filecatalog` | Provides deterministic YAML discovery and path helpers for operating-system filesystems and `fs.FS` sources. | [File catalog](../../internal/filecatalog/catalog.go) |
|
||||
|
||||
@@ -45,12 +45,10 @@ captured when the engine was built.
|
||||
Rendering uses those captured parses, cloning them per call to bind request
|
||||
input helpers; it does not parse templates during execution.
|
||||
|
||||
`InspectPrompt` selects captured metadata directly from the repository. It
|
||||
returns owned metadata without
|
||||
rendering templates or touching profile, artifact, schema, renderer, validator,
|
||||
admission, or model collaborators. The root
|
||||
[`Engine.InspectPrompt`](../../engine.go) GoDoc owns the public operation's
|
||||
exact contract.
|
||||
Resource inspection bypasses the runner: the internal engine selects a captured
|
||||
configuration resource, and the facade detaches its values for the caller.
|
||||
Prompt resource inspection includes captured message text without rendering or
|
||||
touching profile, artifact, schema, admission, or model collaborators.
|
||||
|
||||
`InspectRequirements` uses the same metadata selection to resolve the
|
||||
request-profile/default identity, then obtains the captured requirement
|
||||
@@ -64,17 +62,12 @@ operation's exact contract.
|
||||
|
||||
## Shared Profile Selection
|
||||
|
||||
The runner uses one profile-selection and target-resolution boundary for
|
||||
ordinary preparation and exact profile inspection. Preparation first selects a
|
||||
request profile or a prompt default; inspection begins with its required
|
||||
explicit profile ID. Both use the captured, presence-aware target constructed
|
||||
from the final effective profile and backend configuration. Request overrides
|
||||
apply afterward; preparation checks endpoint and model completeness only after
|
||||
those overrides. Inspection can report a partial profile without requiring a
|
||||
runnable target. Neither path checks credential availability or performs prompt,
|
||||
artifact, schema, rendering, admission, or model-client work. The root
|
||||
[`Engine.InspectProfile`](../../engine.go) GoDoc owns the public operation's
|
||||
exact contract.
|
||||
The runner selects a request profile or a prompt default and uses its captured,
|
||||
presence-aware target constructed from final profile and backend configuration.
|
||||
Request overrides apply afterward; preparation checks endpoint and model
|
||||
completeness only after those overrides. Resource profile inspection bypasses
|
||||
this path and reports the profile before backend completion. Credential
|
||||
availability is checked when execution preparation requires it.
|
||||
|
||||
## Smoke-Test Diagnostics
|
||||
|
||||
|
||||
@@ -11,8 +11,8 @@ source semantics. Internal packages are not consumer APIs.
|
||||
## Typed Configuration Loading
|
||||
|
||||
`internal/configuration` is the single adapter from ConfigKit's final
|
||||
effective configuration to execution-owned definitions. It performs one
|
||||
layered load, captures effective presence (including explicit null), records
|
||||
effective configuration to execution-owned definitions and resource views. It performs one
|
||||
layered load, captures the needed effective presence, records
|
||||
field-level normalization and derived defaults during resolution, and validates all effective
|
||||
root and collection backend, profile, and prompt values. Replaced lower-layer
|
||||
domain values are not separately validated. It indexes root resources under
|
||||
@@ -94,13 +94,13 @@ presence, validation, sensitivity, and resource context; full IDs select
|
||||
captured runtime indexes. Unchanged content-file fields retain their origins.
|
||||
Backend-to-profile and request-time completion remain separate runtime work.
|
||||
|
||||
The runner selects full definitions from this index for preparation and uses
|
||||
captured prompt metadata for `InspectPrompt` and `InspectRequirements`; the
|
||||
internal requirement snapshot lookup uses the selected prompt ID alone.
|
||||
These paths do not rescan configuration or reopen message files. `InspectPrompt`
|
||||
returns declared metadata without rendering or resolving profile, input, or
|
||||
schema resources, and its public shape and prompt hashes do not include the
|
||||
captured presence state. Runtime rendering still checks request variables and
|
||||
The runner selects full definitions from this index for preparation and retains
|
||||
lightweight prompt metadata for `InspectRequirements`; the requirement snapshot
|
||||
lookup uses the selected prompt ID alone. Separate configuration-owned resource
|
||||
inspection projects the resolved record and captured messages without rendering
|
||||
or resolving profile, input, or schema resources. These paths do not rescan
|
||||
configuration or reopen message files. Presence metadata does not change the
|
||||
opaque definition hash. Runtime rendering still checks request variables and
|
||||
input helpers. The [prompt adapter tests](../../internal/configuration/prompts_test.go)
|
||||
cover resource origins, context containment, capture, and selection.
|
||||
|
||||
@@ -149,10 +149,12 @@ ConfigKit resolves profile inheritance before the handoff. Within a collection,
|
||||
`extends: {id: parent}` names a sibling local ID; it is not a cross-collection reference.
|
||||
The configuration adapter validates final effective resource references and
|
||||
field values, then captures backend definitions and profile target snapshots
|
||||
with backend defaults and presence/suppression state. A profile can
|
||||
remain partial until runtime overrides complete it. `InspectProfile` selects
|
||||
that captured target without prompt, artifact, schema, credential lookup, or
|
||||
provider work. Preparation applies runtime overrides and then checks target
|
||||
with backend defaults and outbound-control presence. Null profile endpoint,
|
||||
credential source, and whole provider map permit backend fallback after
|
||||
composition, subject to the required-credential rule. A profile can remain
|
||||
partial until runtime overrides complete it. Resource inspection selects indexed
|
||||
resolved backend and profile records without execution completion or prompt,
|
||||
artifact, schema, credential lookup, or provider work. Preparation applies runtime overrides and then checks target
|
||||
completeness. Identity-only profile selection can identify a partial captured
|
||||
profile without copying its target or requiring readiness. The
|
||||
[target tests](../../internal/configuration/targets_test.go) cover effective
|
||||
|
||||
55
docs/releases/v0.20.0.md
Normal file
55
docs/releases/v0.20.0.md
Normal file
@@ -0,0 +1,55 @@
|
||||
# Promptkit v0.20.0
|
||||
|
||||
This minor release adds generically renderable resource inspection and typed
|
||||
prompt source construction. The Go consumer minimum remains 1.27.0 and
|
||||
ConfigKit remains at v1.1.0.
|
||||
|
||||
## Compatibility And Migration
|
||||
|
||||
`InspectProfile` now returns the resolved profile resource instead of the
|
||||
backend-completed `EffectiveModelParams`. Replace accesses to that field with
|
||||
the optional profile fields; use `Prepare` or `PreparedExecution.Details` for
|
||||
completed request settings. `InspectPrompt` now includes captured messages and
|
||||
uses optional pointers for presence-sensitive fields, including default profile,
|
||||
inputs, input metadata, and output settings. Check pointers before dereferencing.
|
||||
All three resource reports support ordinary JSON and YAML rendering without
|
||||
per-field adapters. See the [consumer guidance](../consumers/pkg-promptkit.md#inspect-a-profile-before-prompt-work).
|
||||
|
||||
After configuration composition and inheritance, profile null endpoint,
|
||||
API-key source, and whole provider map now permit backend defaults during
|
||||
execution as well as appearing absent in resource inspection. Use explicit
|
||||
empty strings to clear endpoint or API-key source. There is no replacement for
|
||||
profile-level suppression of an entire backend provider map; edit backend
|
||||
defaults through composition instead. The required-key rule still applies.
|
||||
See [defaults and overrides](../formats.md#defaults-and-overrides) for the
|
||||
migration example and retained ConfigKit null provenance.
|
||||
|
||||
## Resource Inspection And Sources
|
||||
|
||||
`InspectBackend` reports the resolved backend and configured diagnostics without
|
||||
contacting a provider or deriving diagnostic destinations. Prompt and profile
|
||||
inspection likewise report selected resources independently of request readiness;
|
||||
configuration provenance, requirement analysis, and preparation remain separate.
|
||||
`PromptSource` adds typed programmatic prompt definitions, and `BackendConfig`
|
||||
supports sibling inheritance. ConfigKit continues to own source composition,
|
||||
snapshotting, and relative message-file contexts.
|
||||
|
||||
The source helpers' [GoDoc](../../prompts.go) and
|
||||
[backend configuration API](../../backends.go) describe programmatic construction.
|
||||
|
||||
## Refinements
|
||||
|
||||
Package GoDoc and migration guidance now consistently describe the resource
|
||||
inspection contract, and the full package overview is visible in generated
|
||||
documentation. Prompt inspection avoids repeating the same metadata lookup for
|
||||
each declared input.
|
||||
|
||||
## Upgrade
|
||||
|
||||
After adapting affected inspection consumers and reviewing profile null usage:
|
||||
|
||||
```sh
|
||||
go get go.maximumdirect.net/promptkit@v0.20.0
|
||||
```
|
||||
|
||||
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/promptkit/src/tag/v0.20.0/COPYRIGHT) | [LICENSE](https://forge.maximumdirect.net/base-layer/promptkit/src/tag/v0.20.0/LICENSE) | [ATTRIBUTION](https://forge.maximumdirect.net/base-layer/promptkit/src/tag/v0.20.0/docs/oss/attribution.md)
|
||||
55
engine.go
55
engine.go
@@ -9,6 +9,7 @@ import (
|
||||
"time"
|
||||
|
||||
"go.maximumdirect.net/configkit"
|
||||
"go.maximumdirect.net/promptkit/internal/backend"
|
||||
"go.maximumdirect.net/promptkit/internal/domain"
|
||||
internalengine "go.maximumdirect.net/promptkit/internal/engine"
|
||||
"go.maximumdirect.net/promptkit/internal/observation"
|
||||
@@ -30,6 +31,8 @@ var (
|
||||
// ErrProfileNotFound identifies a selected profile ID that is absent from
|
||||
// every configured profile source. It does not also match ErrProfileLoad.
|
||||
ErrProfileNotFound = errors.New("profile not found")
|
||||
// ErrBackendNotFound identifies a backend absent from resource inspection.
|
||||
ErrBackendNotFound = errors.New("backend not found")
|
||||
// ErrProfileRequired identifies a request for which neither RunRequest.ProfileID
|
||||
// nor the selected prompt's default profile is present. Such an error also
|
||||
// matches ErrInvalidRequest.
|
||||
@@ -81,12 +84,12 @@ var (
|
||||
ErrValidation = errors.New("failed to validate output")
|
||||
)
|
||||
|
||||
// Engine inspects prompts and profiles, enumerates prompt requirements, and
|
||||
// Engine inspects resources, enumerates prompt requirements, and
|
||||
// prepares and runs Promptkit prompt requests.
|
||||
//
|
||||
// An Engine is safe for concurrent calls to [Engine.WithNamespaces],
|
||||
// [Engine.InspectConfiguration],
|
||||
// [Engine.InspectPrompt],
|
||||
// [Engine.InspectPrompt], [Engine.InspectBackend],
|
||||
// [Engine.InspectRequirements], [Engine.InspectProfile], [Engine.Prepare], [Engine.PrepareExecution],
|
||||
// [Engine.Run], [Engine.RunPrepared], [Engine.SmokeTest], [Engine.ListProfiles],
|
||||
// [Engine.ListPrompts], [Engine.ListBackends], [Engine.ListNamespaces], and
|
||||
@@ -267,15 +270,16 @@ func NewEngine(cfg Config, opts ...Option) (*Engine, error) {
|
||||
// qualified IDs pass through.
|
||||
//
|
||||
// A successful result proves that the selected definition and any referenced
|
||||
// message content files were structurally loaded. Inputs are returned in
|
||||
// definition order. DefaultProfileID is declared metadata only and is not
|
||||
// message content files were structurally loaded. Inputs and captured,
|
||||
// unrendered messages are returned in definition order. DefaultProfileID is
|
||||
// declared metadata only and is not
|
||||
// resolved. OutputContract is the normalized declared contract, with a JSON
|
||||
// Schema path when declared but without loading or compiling that schema.
|
||||
// PromptHash is the same opaque equality value as PreparedRun.PromptHash for
|
||||
// the selected captured definition; its spelling, length,
|
||||
// encoding, algorithm, and security properties are not contracts.
|
||||
//
|
||||
// This method does not return prompt bodies, templates, source paths, schemas,
|
||||
// This method does not return parsed templates, source paths, schemas,
|
||||
// rendered messages, or execution settings. It does not resolve a profile or
|
||||
// credential, read artifacts or schemas, render, validate, admit capacity,
|
||||
// contact a provider, or generate model output. The returned PromptInspection
|
||||
@@ -299,21 +303,19 @@ func (e *Engine) InspectPrompt(
|
||||
if err != nil {
|
||||
return nil, mapPublicError(err)
|
||||
}
|
||||
return fromDomainPromptInspection(inspection), nil
|
||||
return fromPromptResource(inspection), nil
|
||||
}
|
||||
|
||||
// InspectProfile resolves one explicit profile without selecting a prompt or
|
||||
// starting execution work.
|
||||
//
|
||||
// InspectProfile looks up a nonblank profileID exactly and case-sensitively
|
||||
// in the captured effective configuration. It applies the framework timeout
|
||||
// baseline, selected
|
||||
// backend, and then selected profile to EffectiveModelParams without a request
|
||||
// override. BackendID is empty for an endpoint-only profile.
|
||||
// in the captured effective configuration. It reports the resolved profile
|
||||
// resource without backend completion, execution defaults, or request overrides.
|
||||
//
|
||||
// APIKeyEnv in the returned target is an environment-variable name, never its
|
||||
// value. APIKeyRequired reports that a credential is required; an explicit
|
||||
// profile environment source can also be present. InspectProfile neither derives
|
||||
// APIKeyEnv in the returned resource is an environment-variable name, never its
|
||||
// value. APIKeyRequired, when present and true, reports that a credential is
|
||||
// required; an explicit profile environment source can also be present. InspectProfile neither derives
|
||||
// an ID from a prompt default_profile nor checks credential availability, so an
|
||||
// absent or blank named environment variable is not an error.
|
||||
//
|
||||
@@ -337,7 +339,32 @@ func (e *Engine) InspectProfile(ctx context.Context, profileID string) (*Profile
|
||||
if err != nil {
|
||||
return nil, mapPublicError(err)
|
||||
}
|
||||
return fromDomainProfileInspection(inspection), nil
|
||||
return fromProfileResource(inspection), nil
|
||||
}
|
||||
|
||||
// InspectBackend selects a resolved backend by exact ID from the captured
|
||||
// configuration, qualifying a slashless ID with the backend namespace. It
|
||||
// reports configured resource fields and diagnostics, not credential values,
|
||||
// derived diagnostic destinations, or runtime capacity occupancy. References
|
||||
// are not resolved and no network request is made. The result and its mutable
|
||||
// values are caller-owned. Source changes require a new engine.
|
||||
//
|
||||
// An invalid engine matches ErrInvalidConfig, an invalid ID matches
|
||||
// ErrInvalidRequest, and an absent backend matches ErrBackendNotFound (not
|
||||
// ErrInvalidRequest). Cancellation remains detectable via errors.Is. No
|
||||
// partial result is returned on error.
|
||||
func (e *Engine) InspectBackend(ctx context.Context, backendID string) (*BackendInspection, error) {
|
||||
if e == nil || e.internal == nil {
|
||||
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
||||
}
|
||||
inspection, err := e.internal.InspectBackend(ctx, backendID)
|
||||
if err != nil {
|
||||
if errors.Is(err, backend.ErrBackendNotFound) {
|
||||
return nil, fmt.Errorf("%w: %w", ErrBackendNotFound, err)
|
||||
}
|
||||
return nil, mapPublicError(err)
|
||||
}
|
||||
return fromBackendResource(inspection), nil
|
||||
}
|
||||
|
||||
// Prepare resolves and renders a prompt request without calling an LLM.
|
||||
|
||||
@@ -110,10 +110,10 @@ func run(ctx context.Context, exampleDir string, out io.Writer) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if profile.EffectiveModelParams.TimeoutSeconds != 30 {
|
||||
if profile.TimeoutSeconds == nil || *profile.TimeoutSeconds != 30 {
|
||||
return fmt.Errorf(
|
||||
"later profile override was not applied: timeout is %d seconds",
|
||||
profile.EffectiveModelParams.TimeoutSeconds,
|
||||
"later profile override was not applied: timeout is %v",
|
||||
profile.TimeoutSeconds,
|
||||
)
|
||||
}
|
||||
namespaces, err := scoped.ListNamespaces(ctx)
|
||||
@@ -156,7 +156,7 @@ func run(ctx context.Context, exampleDir string, out io.Writer) error {
|
||||
SelectedProfile: prepared.SelectedProfileID,
|
||||
Model: prepared.EffectiveModelParams.Model,
|
||||
MessageCount: len(prepared.Messages),
|
||||
ProfileTimeout: profile.EffectiveModelParams.TimeoutSeconds,
|
||||
ProfileTimeout: *profile.TimeoutSeconds,
|
||||
Namespaces: namespaces,
|
||||
Collections: collectionSummaries,
|
||||
ScopedPrompts: scopedPromptIDs,
|
||||
|
||||
2
go.mod
2
go.mod
@@ -5,12 +5,12 @@ go 1.27.0
|
||||
require (
|
||||
github.com/santhosh-tekuri/jsonschema/v6 v6.0.2
|
||||
go.maximumdirect.net/configkit v1.1.0
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/yuin/goldmark/v2 v2.1.6 // indirect
|
||||
go.maximumdirect.net/releasekit v0.9.0 // indirect
|
||||
go.yaml.in/yaml/v4 v4.0.0-rc.6 // indirect
|
||||
golang.org/x/mod v0.40.0 // indirect
|
||||
golang.org/x/text v0.14.0 // indirect
|
||||
)
|
||||
|
||||
@@ -119,10 +119,12 @@ const (
|
||||
|
||||
type loaded struct {
|
||||
result *configkit.Result[document]
|
||||
profiles map[string]map[string]fieldState
|
||||
promptMetadata map[string][]promptInputPresence
|
||||
promptRepair map[string]bool
|
||||
prompts map[string]*promptEntry
|
||||
registry *backend.Registry
|
||||
backendRecords map[string]backendConfig
|
||||
profileRecords map[string]profileConfig
|
||||
indexedProfiles map[string]domain.ExecutionProfile
|
||||
backendIDs []string
|
||||
profileIDs []string
|
||||
@@ -211,8 +213,6 @@ func indexedProfiles(config *document) map[string]profileConfig {
|
||||
|
||||
var envName = regexp.MustCompile(`^[A-Za-z_][A-Za-z0-9_]*$`)
|
||||
|
||||
var profileFields = []string{"backend", "endpoint", "model", "temperature", "top_p", "max_tokens", "timeout_seconds", "service_tier", "reasoning_effort", "api_key_env", "api_key_required", "extra_params"}
|
||||
|
||||
// Load captures one immutable effective configuration for internal consumers.
|
||||
func Load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error) {
|
||||
return load(main, underlays)
|
||||
@@ -235,8 +235,7 @@ func configurationDefinition(state *loaded) configkit.Definition[document] {
|
||||
Sensitive: secretProviderPath,
|
||||
Resolve: func(config *document, resolution *configkit.Resolution) error {
|
||||
for _, resources := range resourceSets(config) {
|
||||
captureFields(resources.profiles, func(v profileConfig) string { return v.ID }, resources.bases.profiles, resources.namespace, profileFields, resolution, state.profiles)
|
||||
capturePromptMetadata(resources.prompts, resources.bases.prompts, resources.namespace, resolution, state.promptMetadata)
|
||||
capturePromptMetadata(resources.prompts, resources.bases.prompts, resources.namespace, resolution, state.promptMetadata, state.promptRepair)
|
||||
}
|
||||
for _, resources := range resourceSets(config) {
|
||||
resolveBackends(resources.backends, resources.bases.backends, resolution)
|
||||
@@ -254,8 +253,8 @@ func configurationDefinition(state *loaded) configkit.Definition[document] {
|
||||
// load performs one ConfigKit lifecycle over the selected source tiers.
|
||||
func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error) {
|
||||
state := &loaded{
|
||||
profiles: make(map[string]map[string]fieldState),
|
||||
promptMetadata: make(map[string][]promptInputPresence),
|
||||
promptRepair: make(map[string]bool),
|
||||
}
|
||||
result, err := configkit.LoadLayered(main, configkit.LayeredOptions[document]{
|
||||
Underlays: underlays,
|
||||
@@ -320,11 +319,12 @@ func checkConfigurationVersion(version configkit.Version, validation *configkit.
|
||||
}
|
||||
|
||||
type promptInputPresence struct {
|
||||
required fieldState
|
||||
contentType fieldState
|
||||
description fieldState
|
||||
}
|
||||
|
||||
func capturePromptMetadata(prompts []promptConfig, basePath configkit.Path, namespace string, resolution *configkit.Resolution, destination map[string][]promptInputPresence) {
|
||||
func capturePromptMetadata(prompts []promptConfig, basePath configkit.Path, namespace string, resolution *configkit.Resolution, destination map[string][]promptInputPresence, repair map[string]bool) {
|
||||
for promptIndex, prompt := range prompts {
|
||||
inputs := prompt.Inputs
|
||||
states := make([]promptInputPresence, len(inputs))
|
||||
@@ -332,11 +332,13 @@ func capturePromptMetadata(prompts []promptConfig, basePath configkit.Path, name
|
||||
for index := range inputs {
|
||||
path := base.Index(index)
|
||||
states[index] = promptInputPresence{
|
||||
required: captureFieldState(path.Key("required"), resolution),
|
||||
contentType: captureFieldState(path.Key("content_type"), resolution),
|
||||
description: captureFieldState(path.Key("description"), resolution),
|
||||
}
|
||||
}
|
||||
destination[indexedResourceID(namespace, prompt.ID)] = states
|
||||
repair[indexedResourceID(namespace, prompt.ID)] = captureFieldState(basePath.Index(promptIndex).Key("output").Key("repair_attempts"), resolution) == fieldValue
|
||||
}
|
||||
}
|
||||
|
||||
@@ -352,24 +354,6 @@ func captureFieldState(path configkit.Path, resolution *configkit.Resolution) fi
|
||||
}
|
||||
}
|
||||
|
||||
func captureFields[T any](entries []T, identity func(T) string, basePath configkit.Path, namespace string, fields []string, resolution *configkit.Resolution, destination map[string]map[string]fieldState) {
|
||||
for index, entry := range entries {
|
||||
states := make(map[string]fieldState, len(fields))
|
||||
for _, field := range fields {
|
||||
info, exists := resolution.EntryInfo(basePath.Index(index).Key(field))
|
||||
switch {
|
||||
case !exists || !info.Explicit:
|
||||
states[field] = fieldAbsent
|
||||
case info.Null:
|
||||
states[field] = fieldNull
|
||||
default:
|
||||
states[field] = fieldValue
|
||||
}
|
||||
}
|
||||
destination[indexedResourceID(namespace, identity(entry))] = states
|
||||
}
|
||||
}
|
||||
|
||||
// eligibleResourceList leaves invalid identities to the existing validation
|
||||
// boundaries, without mutating a list that cannot be recorded by identity.
|
||||
func eligibleResourceList[T any](entries []T, identity func(T) string) bool {
|
||||
|
||||
@@ -488,9 +488,6 @@ func TestLoadComposesLiteralKeysAndEffectivePresence(t *testing.T) {
|
||||
if profile.Temperature == nil || *profile.Temperature != 0 || profile.TopP != nil || profile.APIKeyRequired == nil || *profile.APIKeyRequired {
|
||||
t.Fatalf("effective values were not retained: %+v", profile)
|
||||
}
|
||||
if loaded.profiles["base/child.v1"]["temperature"] != fieldValue || loaded.profiles["base/child.v1"]["top_p"] != fieldNull || loaded.profiles["base/child.v1"]["api_key_required"] != fieldValue {
|
||||
t.Fatalf("effective presence was lost: %+v", loaded.profiles["base/child.v1"])
|
||||
}
|
||||
provider := profile.ExtraParams["provider"].(map[string]any)
|
||||
if provider["first"] != int64(1) || provider["second"] != int64(3) {
|
||||
t.Fatalf("recursive overlay failed: %+v", provider)
|
||||
@@ -776,9 +773,8 @@ func TestLoadCapturesProfilePresence(t *testing.T) {
|
||||
if profile.Temperature == nil || *profile.Temperature != 0 || profile.TopP != nil || profile.APIKeyRequired == nil || *profile.APIKeyRequired {
|
||||
t.Fatalf("profile values were not preserved: %+v", profile)
|
||||
}
|
||||
fields := loaded.profiles["local"]
|
||||
if fields["temperature"] != fieldValue || fields["top_p"] != fieldNull || fields["api_key_required"] != fieldValue || fields["model"] != fieldAbsent {
|
||||
t.Fatalf("profile presence was not preserved: %+v", fields)
|
||||
if profile.Model != nil {
|
||||
t.Fatalf("omitted model was not preserved: %+v", profile)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ var (
|
||||
)
|
||||
|
||||
type promptEntry struct {
|
||||
resource promptConfig
|
||||
definition domain.PromptDefinition
|
||||
inspection domain.PromptInspection
|
||||
requirementInputs []prompt.InputMetadata
|
||||
@@ -86,7 +87,7 @@ func (state *loaded) materializePrompt(id string, raw promptConfig, base configk
|
||||
}
|
||||
definition.Inputs = inputs
|
||||
definition.Templates = messages
|
||||
entry := &promptEntry{requirementInputs: inputMetadata, messages: compiledMessages}
|
||||
entry := &promptEntry{resource: raw, requirementInputs: inputMetadata, messages: compiledMessages}
|
||||
definition.Validation = contract
|
||||
if raw.SessionID != nil {
|
||||
var err error
|
||||
@@ -316,8 +317,8 @@ func (state *loaded) GetPromptSnapshot(ctx context.Context, id string) (*domain.
|
||||
return ownPromptDefinition(entry.definition), entry.hash, nil
|
||||
}
|
||||
|
||||
// GetPromptInspection returns an independently owned inspection of one
|
||||
// captured prompt without copying its execution templates.
|
||||
// GetPromptInspection returns lightweight metadata used for requirement
|
||||
// selection, independently of the full resource inspection view.
|
||||
func (state *loaded) GetPromptInspection(ctx context.Context, id string) (*domain.PromptInspection, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -84,6 +84,10 @@ func TestLoadCapturesInheritedImportedContent(t *testing.T) {
|
||||
files["catalog/body.md"].Data = []byte("Changed")
|
||||
files["app/body.md"].Data = []byte("Changed replacement")
|
||||
files["other/body.md"].Data = []byte("Changed other collection")
|
||||
resource, err := state.InspectPrompt(context.Background(), "docs/child")
|
||||
if err != nil || resource.Messages[0].Content != "Captured {{.name}}" || resource.Messages[0].CacheControl == nil || resource.Messages[0].CacheControl.TTL == nil || *resource.Messages[0].CacheControl.TTL != "1h" || resource.Inputs == nil || (*resource.Inputs)[0].Required != nil {
|
||||
t.Fatalf("captured resource = %+v, %v", resource, err)
|
||||
}
|
||||
again, againInspection, err := state.prompt("docs/child")
|
||||
if err != nil || again.Templates[0].Content != "Captured {{.name}}" || againInspection.PromptHash != inspection.PromptHash {
|
||||
t.Fatalf("captured definition changed after source mutation: %+v, %+v, %v", again, againInspection, err)
|
||||
@@ -99,6 +103,25 @@ func TestLoadCapturesInheritedImportedContent(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectPromptResourcePresence(t *testing.T) {
|
||||
state, err := load(mapping("prompt resources", map[string]any{"prompts": []any{
|
||||
map[string]any{"id": "base", "default_profile": "missing", "inputs": []any{map[string]any{"name": "x", "required": false}, map[string]any{"name": "y"}},
|
||||
"messages": []any{map[string]any{"role": "user", "content": "", "cache_control": map[string]any{"type": "ephemeral", "ttl": ""}}, map[string]any{"role": "assistant", "content": "{{.missing}}", "cache_control": map[string]any{"type": "ephemeral"}}},
|
||||
"output": map[string]any{"format": "text", "validation_mode": "basic", "repair_attempts": 0}},
|
||||
map[string]any{"id": "child", "extends": map[string]any{"id": "base"}},
|
||||
}}), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := state.InspectPrompt(context.Background(), "child")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got.DefaultProfile == nil || *got.DefaultProfile != "missing" || got.Inputs == nil || (*got.Inputs)[0].Required == nil || *(*got.Inputs)[0].Required || (*got.Inputs)[1].Required != nil || got.Output.RepairAttempts == nil || *got.Output.RepairAttempts != 0 || got.Messages[0].Content != "" || got.Messages[0].CacheControl.TTL == nil || *got.Messages[0].CacheControl.TTL != "" || got.Messages[1].CacheControl.TTL != nil {
|
||||
t.Fatalf("prompt resource = %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadHeaderlessImportedItem(t *testing.T) {
|
||||
files := fstest.MapFS{
|
||||
"main.yaml": {Data: []byte("version: 2\nconfig:\n imports:\n - path: item.yaml\n destination: [prompts]\n placement: sequence-item\n required: true\n")},
|
||||
|
||||
140
internal/configuration/resources.go
Normal file
140
internal/configuration/resources.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package configuration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"go.maximumdirect.net/promptkit/internal/backend"
|
||||
"go.maximumdirect.net/promptkit/internal/domain"
|
||||
"go.maximumdirect.net/promptkit/internal/profile"
|
||||
)
|
||||
|
||||
// ProfileResource is the resolved profile before execution completion.
|
||||
// Its fields borrow immutable configuration captured by the adapter.
|
||||
type ProfileResource struct {
|
||||
ID string
|
||||
Backend, Endpoint, Model *string
|
||||
Temperature, TopP *float64
|
||||
MaxTokens, TimeoutSeconds *int
|
||||
ServiceTier, ReasoningEffort *string
|
||||
APIKeyEnv *string
|
||||
APIKeyRequired *bool
|
||||
ExtraParams map[string]any
|
||||
}
|
||||
|
||||
// BackendResource is the resolved backend and its configured diagnostics.
|
||||
// Its fields borrow immutable configuration captured by the adapter.
|
||||
type BackendResource struct {
|
||||
ID, Endpoint string
|
||||
APIKeyEnv *string
|
||||
ConcurrencyLimit, QueueCapacity *int
|
||||
ExtraParams map[string]any
|
||||
SmokeTests *domain.BackendSmokeTestConfig
|
||||
}
|
||||
|
||||
// PromptResource is the resolved prompt with already captured message text.
|
||||
type PromptResource struct {
|
||||
ID, Hash string
|
||||
DefaultProfile, Description, SessionID *string
|
||||
Inputs *[]PromptInputResource
|
||||
Messages []PromptMessageResource
|
||||
Output PromptOutputResource
|
||||
}
|
||||
|
||||
type PromptInputResource struct {
|
||||
Name string
|
||||
Required *bool
|
||||
ContentType, Description *string
|
||||
}
|
||||
|
||||
type PromptMessageResource struct {
|
||||
Role, Content string
|
||||
CacheControl *PromptCacheControlResource
|
||||
}
|
||||
|
||||
type PromptCacheControlResource struct {
|
||||
Type domain.CacheControlType
|
||||
TTL *string
|
||||
}
|
||||
|
||||
type PromptOutputResource struct {
|
||||
Format domain.OutputFormat
|
||||
ValidationMode domain.ValidationMode
|
||||
SchemaPath *string
|
||||
RepairAttempts *int
|
||||
}
|
||||
|
||||
func (state *loaded) InspectPrompt(ctx context.Context, id string) (*PromptResource, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
entry, err := state.lookupPromptEntry(id)
|
||||
if err != nil {
|
||||
return nil, promptLookupError(err)
|
||||
}
|
||||
raw := entry.resource
|
||||
result := &PromptResource{ID: id, Hash: entry.hash, DefaultProfile: raw.DefaultProfile, Description: raw.Description, SessionID: raw.SessionID}
|
||||
if raw.Inputs != nil {
|
||||
inputs := make([]PromptInputResource, len(raw.Inputs))
|
||||
presence := state.promptMetadata[id]
|
||||
for i, input := range raw.Inputs {
|
||||
inputs[i] = PromptInputResource{Name: input.Name, ContentType: input.ContentType, Description: input.Description}
|
||||
if presence[i].required == fieldValue {
|
||||
required := input.Required
|
||||
inputs[i].Required = &required
|
||||
}
|
||||
}
|
||||
result.Inputs = &inputs
|
||||
}
|
||||
result.Messages = make([]PromptMessageResource, len(entry.definition.Templates))
|
||||
for i, message := range entry.definition.Templates {
|
||||
result.Messages[i] = PromptMessageResource{Role: message.Role, Content: message.Content}
|
||||
if message.CacheControl != nil {
|
||||
result.Messages[i].CacheControl = &PromptCacheControlResource{Type: message.CacheControl.Type, TTL: raw.Messages[i].CacheControl.TTL}
|
||||
}
|
||||
}
|
||||
result.Output = PromptOutputResource{Format: entry.definition.Validation.Format, ValidationMode: entry.definition.Validation.ValidationMode, SchemaPath: raw.Output.SchemaPath}
|
||||
if state.promptRepair[id] {
|
||||
value := raw.Output.RepairAttempts
|
||||
result.Output.RepairAttempts = &value
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (state *loaded) InspectProfile(ctx context.Context, id string) (*ProfileResource, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if state == nil {
|
||||
return nil, fmt.Errorf("%w: %q", profile.ErrProfileNotFound, id)
|
||||
}
|
||||
config, ok := state.profileRecords[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: %q", profile.ErrProfileNotFound, id)
|
||||
}
|
||||
return &ProfileResource{
|
||||
ID: id, Backend: config.Backend, Endpoint: config.Endpoint, Model: config.Model,
|
||||
Temperature: config.Temperature, TopP: config.TopP, MaxTokens: config.MaxTokens,
|
||||
TimeoutSeconds: config.TimeoutSeconds, ServiceTier: config.ServiceTier,
|
||||
ReasoningEffort: config.ReasoningEffort, APIKeyEnv: config.APIKeyEnv,
|
||||
APIKeyRequired: config.APIKeyRequired, ExtraParams: config.ExtraParams,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (state *loaded) InspectBackend(ctx context.Context, id string) (*BackendResource, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if state == nil {
|
||||
return nil, fmt.Errorf("%w: %q", backend.ErrBackendNotFound, id)
|
||||
}
|
||||
config, ok := state.backendRecords[id]
|
||||
if !ok {
|
||||
return nil, fmt.Errorf("%w: %q", backend.ErrBackendNotFound, id)
|
||||
}
|
||||
return &BackendResource{
|
||||
ID: id, Endpoint: valueOrZero(config.Endpoint), APIKeyEnv: config.APIKeyEnv,
|
||||
ConcurrencyLimit: config.ConcurrencyLimit, QueueCapacity: config.QueueCapacity,
|
||||
ExtraParams: config.ExtraParams, SmokeTests: captureBackend(id, config).SmokeTests,
|
||||
}, nil
|
||||
}
|
||||
67
internal/configuration/resources_test.go
Normal file
67
internal/configuration/resources_test.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package configuration
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"go.maximumdirect.net/promptkit/internal/backend"
|
||||
"go.maximumdirect.net/promptkit/internal/profile"
|
||||
)
|
||||
|
||||
func TestResolvedBackendAndProfileResources(t *testing.T) {
|
||||
state, err := load(mapping("resources", map[string]any{
|
||||
"backends": []any{map[string]any{"id": "local", "endpoint": "https://example.test/v1", "concurrency_limit": 2,
|
||||
"extra_params": map[string]any{"backend": true}, "smoke_tests": map[string]any{"default_profile": "missing", "reachability": map[string]any{}}}},
|
||||
"profiles": []any{
|
||||
map[string]any{"id": "parent", "backend": "local", "temperature": 0.4},
|
||||
map[string]any{"id": "child", "extends": map[string]any{"id": "parent"}, "temperature": 0, "endpoint": "", "extra_params": map[string]any{}},
|
||||
},
|
||||
}), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resource, err := state.InspectProfile(context.Background(), "child")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resource.ID != "child" || resource.Backend == nil || *resource.Backend != "local" || resource.Endpoint == nil || *resource.Endpoint != "" || resource.Temperature == nil || *resource.Temperature != 0 || resource.Model != nil || resource.ExtraParams == nil || len(resource.ExtraParams) != 0 {
|
||||
t.Fatalf("profile resource = %+v", resource)
|
||||
}
|
||||
target, err := state.GetProfile(context.Background(), "child")
|
||||
if err != nil || target.ResolvedTarget.ExtraParams["backend"] != true {
|
||||
t.Fatalf("execution target = %+v, %v", target, err)
|
||||
}
|
||||
resourceBackend, err := state.InspectBackend(context.Background(), "local")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if resourceBackend.Endpoint != "https://example.test/v1" || resourceBackend.QueueCapacity == nil || *resourceBackend.QueueCapacity != 1024 || resourceBackend.SmokeTests == nil || resourceBackend.SmokeTests.DefaultProfile == nil || *resourceBackend.SmokeTests.DefaultProfile != "missing" || resourceBackend.SmokeTests.Reachability == nil || resourceBackend.SmokeTests.Reachability.Endpoint != nil {
|
||||
t.Fatalf("backend resource = %+v", resourceBackend)
|
||||
}
|
||||
if _, err := state.InspectBackend(context.Background(), "missing"); !errors.Is(err, backend.ErrBackendNotFound) {
|
||||
t.Fatalf("backend error = %v", err)
|
||||
}
|
||||
if _, err := state.InspectProfile(context.Background(), "missing"); !errors.Is(err, profile.ErrProfileNotFound) {
|
||||
t.Fatalf("profile error = %v", err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
if _, err := state.InspectBackend(ctx, "local"); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("cancellation = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackendResourcePreservesInheritedDiagnostics(t *testing.T) {
|
||||
state, err := load(mapping("backend diagnostics", map[string]any{"backends": []any{
|
||||
map[string]any{"id": "parent", "endpoint": "https://provider.test/v1", "smoke_tests": map[string]any{"reachability": map[string]any{"endpoint": "https://probe.test/ready"}, "authentication": map[string]any{"kind": "none"}}},
|
||||
map[string]any{"id": "child", "extends": map[string]any{"id": "parent"}},
|
||||
}}), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := state.InspectBackend(context.Background(), "child")
|
||||
if err != nil || got.ID != "child" || got.ConcurrencyLimit != nil || got.QueueCapacity != nil || got.SmokeTests == nil || got.SmokeTests.Reachability == nil || got.SmokeTests.Reachability.Endpoint == nil || *got.SmokeTests.Reachability.Endpoint != "https://probe.test/ready" || got.SmokeTests.Authentication == nil || got.SmokeTests.Authentication.Kind == nil || *got.SmokeTests.Authentication.Kind != "none" || got.SmokeTests.Authentication.Endpoint != nil {
|
||||
t.Fatalf("inherited diagnostics = %+v, %v", got, err)
|
||||
}
|
||||
}
|
||||
@@ -14,6 +14,8 @@ import (
|
||||
func (state *loaded) buildExecutionProfiles() error {
|
||||
backendConfigs := indexedBackends(&state.result.Config)
|
||||
profileConfigs := indexedProfiles(&state.result.Config)
|
||||
state.backendRecords = backendConfigs
|
||||
state.profileRecords = profileConfigs
|
||||
definitions := make(map[string]domain.Backend, len(backendConfigs))
|
||||
for _, id := range sortedKeys(backendConfigs) {
|
||||
config := backendConfigs[id]
|
||||
@@ -23,7 +25,7 @@ func (state *loaded) buildExecutionProfiles() error {
|
||||
state.indexedProfiles = make(map[string]domain.ExecutionProfile, len(profileConfigs))
|
||||
for _, id := range sortedKeys(profileConfigs) {
|
||||
config := profileConfigs[id]
|
||||
profile, err := captureExecutionProfile(id, config, state.profiles[id], state.registry)
|
||||
profile, err := captureExecutionProfile(id, config, state.registry)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -62,7 +64,7 @@ func captureBackend(id string, config backendConfig) domain.Backend {
|
||||
return definition
|
||||
}
|
||||
|
||||
func captureExecutionProfile(id string, config profileConfig, fields map[string]fieldState, registry *backend.Registry) (domain.ExecutionProfile, error) {
|
||||
func captureExecutionProfile(id string, config profileConfig, registry *backend.Registry) (domain.ExecutionProfile, error) {
|
||||
target := defaults.ExecutionTargetDefault()
|
||||
if config.Backend != nil && *config.Backend != "" {
|
||||
selected, err := registry.GetBackend(*config.Backend)
|
||||
@@ -75,47 +77,44 @@ func captureExecutionProfile(id string, config profileConfig, fields map[string]
|
||||
target.ExtraParams = selected.ExtraParams
|
||||
}
|
||||
presence := domain.ExecutionTargetPresence{}
|
||||
if fields["endpoint"] != fieldAbsent {
|
||||
if config.Endpoint != nil {
|
||||
target.Endpoint = valueOrZero(config.Endpoint)
|
||||
}
|
||||
if fields["model"] != fieldAbsent {
|
||||
if config.Model != nil {
|
||||
target.Model = valueOrZero(config.Model)
|
||||
}
|
||||
if fields["temperature"] != fieldAbsent {
|
||||
if config.Temperature != nil {
|
||||
target.Temperature = valueOrZero(config.Temperature)
|
||||
presence.Temperature = fields["temperature"] == fieldValue
|
||||
presence.Temperature = true
|
||||
}
|
||||
if fields["top_p"] != fieldAbsent {
|
||||
if config.TopP != nil {
|
||||
target.TopP = valueOrZero(config.TopP)
|
||||
presence.TopP = fields["top_p"] == fieldValue
|
||||
presence.TopP = true
|
||||
}
|
||||
if fields["max_tokens"] != fieldAbsent {
|
||||
if config.MaxTokens != nil {
|
||||
target.MaxTokens = valueOrZero(config.MaxTokens)
|
||||
presence.MaxTokens = fields["max_tokens"] == fieldValue
|
||||
presence.MaxTokens = true
|
||||
}
|
||||
if fields["timeout_seconds"] == fieldValue {
|
||||
if config.TimeoutSeconds != nil {
|
||||
target.TimeoutSeconds = valueOrZero(config.TimeoutSeconds)
|
||||
presence.TimeoutSeconds = true
|
||||
}
|
||||
if fields["service_tier"] != fieldAbsent {
|
||||
if config.ServiceTier != nil {
|
||||
target.ServiceTier = valueOrZero(config.ServiceTier)
|
||||
}
|
||||
if fields["reasoning_effort"] != fieldAbsent {
|
||||
if config.ReasoningEffort != nil {
|
||||
target.ReasoningEffort = valueOrZero(config.ReasoningEffort)
|
||||
}
|
||||
if fields["api_key_env"] != fieldAbsent {
|
||||
if config.APIKeyEnv != nil {
|
||||
target.APIKeyEnv = valueOrZero(config.APIKeyEnv)
|
||||
}
|
||||
if fields["api_key_required"] != fieldAbsent {
|
||||
if config.APIKeyRequired != nil {
|
||||
target.APIKeyRequired = valueOrZero(config.APIKeyRequired)
|
||||
}
|
||||
if target.APIKeyRequired && fields["api_key_env"] == fieldAbsent {
|
||||
if target.APIKeyRequired && config.APIKeyEnv == nil {
|
||||
target.APIKeyEnv = ""
|
||||
}
|
||||
switch fields["extra_params"] {
|
||||
case fieldNull:
|
||||
target.ExtraParams = nil
|
||||
case fieldValue:
|
||||
if config.ExtraParams != nil {
|
||||
merged, err := overlayProviderMap(target.ExtraParams, config.ExtraParams)
|
||||
if err != nil {
|
||||
return domain.ExecutionProfile{}, fmt.Errorf("profile %q extra parameters: %w", id, err)
|
||||
|
||||
@@ -132,7 +132,7 @@ func TestLoadBuildsEffectiveTargetsAfterInheritance(t *testing.T) {
|
||||
target := child.ResolvedTarget
|
||||
if target.BackendID != "second" || target.Endpoint != "https://second.example/v1" || target.Model != "model" ||
|
||||
target.Temperature != 0 || target.TopP != 0 || target.MaxTokens != 0 ||
|
||||
target.TimeoutSeconds != defaults.ExecutionDefaultTimeoutSeconds || target.APIKeyEnv != "" || target.APIKeyRequired {
|
||||
target.TimeoutSeconds != defaults.ExecutionDefaultTimeoutSeconds || target.APIKeyEnv != "SECOND_KEY" || target.APIKeyRequired {
|
||||
t.Fatalf("effective child target is wrong: %+v", target)
|
||||
}
|
||||
if !child.TargetPresence.Temperature || !child.TargetPresence.MaxTokens || child.TargetPresence.TopP || child.TargetPresence.TimeoutSeconds {
|
||||
@@ -154,8 +154,8 @@ func TestLoadBuildsEffectiveTargetsAfterInheritance(t *testing.T) {
|
||||
}
|
||||
}
|
||||
noMap, _ := state.GetProfile(context.Background(), "no-provider-map")
|
||||
if noMap.ResolvedTarget.ExtraParams != nil {
|
||||
t.Fatalf("explicit null provider map did not suppress backend defaults: %+v", noMap.ResolvedTarget.ExtraParams)
|
||||
if !reflect.DeepEqual(noMap.ResolvedTarget.ExtraParams, selected.ExtraParams) {
|
||||
t.Fatalf("explicit null provider map did not inherit backend defaults: %+v", noMap.ResolvedTarget.ExtraParams)
|
||||
}
|
||||
noTimeout, _ := state.GetProfile(context.Background(), "no-timeout")
|
||||
if noTimeout.ResolvedTarget.TimeoutSeconds != 0 || !noTimeout.TargetPresence.TimeoutSeconds {
|
||||
@@ -183,8 +183,8 @@ func TestLoadBuildsEffectiveTargetsAfterInheritance(t *testing.T) {
|
||||
t.Fatalf("endpoint-only profile was not retained: %+v", endpointOnly.ResolvedTarget)
|
||||
}
|
||||
clearedEndpoint, _ := state.GetProfile(context.Background(), "cleared-endpoint")
|
||||
if clearedEndpoint.ResolvedTarget.Endpoint != "" || clearedEndpoint.ResolvedTarget.BackendID != "first" {
|
||||
t.Fatalf("explicit null recovered backend endpoint: %+v", clearedEndpoint.ResolvedTarget)
|
||||
if clearedEndpoint.ResolvedTarget.Endpoint != "https://first.example/v1" || clearedEndpoint.ResolvedTarget.BackendID != "first" {
|
||||
t.Fatalf("explicit null did not recover backend endpoint: %+v", clearedEndpoint.ResolvedTarget)
|
||||
}
|
||||
child.ResolvedTarget.ExtraParams["source"] = "mutated"
|
||||
again, _ := state.GetProfile(context.Background(), "child")
|
||||
@@ -193,6 +193,48 @@ func TestLoadBuildsEffectiveTargetsAfterInheritance(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestProfileNullFallbackAndExplicitValues(t *testing.T) {
|
||||
state, err := load(mapping("profile fallback", map[string]any{
|
||||
"backends": []any{map[string]any{"id": "backend", "endpoint": "https://backend.example/v1", "api_key_env": "BACKEND_KEY", "extra_params": map[string]any{"backend": true}}},
|
||||
"profiles": []any{
|
||||
map[string]any{"id": "parent", "backend": "backend", "endpoint": "https://parent.example/v1", "extra_params": map[string]any{"parent": true}},
|
||||
map[string]any{"id": "inherited-null", "extends": map[string]any{"id": "parent"}, "endpoint": nil, "extra_params": nil, "api_key_env": nil},
|
||||
map[string]any{"id": "omitted", "backend": "backend"},
|
||||
map[string]any{"id": "null", "backend": "backend", "endpoint": nil, "api_key_env": nil, "extra_params": nil},
|
||||
map[string]any{"id": "empty", "backend": "backend", "endpoint": "", "api_key_env": "", "extra_params": map[string]any{}},
|
||||
map[string]any{"id": "required-null", "backend": "backend", "api_key_required": true, "api_key_env": nil},
|
||||
map[string]any{"id": "required-omitted", "backend": "backend", "api_key_required": true},
|
||||
map[string]any{"id": "required-explicit", "backend": "backend", "api_key_required": true, "api_key_env": "PROFILE_KEY"},
|
||||
},
|
||||
}), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, id := range []string{"inherited-null", "omitted", "null", "empty", "required-null", "required-omitted", "required-explicit"} {
|
||||
profile, err := state.GetProfile(context.Background(), id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
target := profile.ResolvedTarget
|
||||
wantEndpoint, wantKey := "https://backend.example/v1", "BACKEND_KEY"
|
||||
switch id {
|
||||
case "empty":
|
||||
wantEndpoint, wantKey = "", ""
|
||||
case "required-null", "required-omitted":
|
||||
wantKey = ""
|
||||
case "required-explicit":
|
||||
wantKey = "PROFILE_KEY"
|
||||
}
|
||||
if target.Endpoint != wantEndpoint || target.APIKeyEnv != wantKey || !reflect.DeepEqual(target.ExtraParams, map[string]any{"backend": true}) {
|
||||
t.Errorf("%s target = %+v", id, target)
|
||||
}
|
||||
}
|
||||
child := state.result.Config.Profiles[1]
|
||||
if child.Endpoint != nil || child.ExtraParams != nil || child.APIKeyEnv != nil {
|
||||
t.Fatalf("inherited nulls were not retained in resolved values: %+v", child)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadedZeroControlsReachBuiltInRequest(t *testing.T) {
|
||||
payloads := make(chan map[string]any, 1)
|
||||
server := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
||||
|
||||
@@ -148,7 +148,7 @@ type PromptDefinition struct {
|
||||
Validation OutputContract `yaml:"validation"`
|
||||
}
|
||||
|
||||
// PromptInspection is the resolved result of exact prompt inspection.
|
||||
// PromptInspection is lightweight metadata for prompt requirement selection.
|
||||
type PromptInspection struct {
|
||||
PromptID string
|
||||
PromptHash string
|
||||
@@ -239,13 +239,6 @@ type ExecutionTarget struct {
|
||||
ExtraParams map[string]any `yaml:"extra_params" json:"extra_params"`
|
||||
}
|
||||
|
||||
// ProfileInspection is the resolved result of exact profile inspection.
|
||||
type ProfileInspection struct {
|
||||
ProfileID string
|
||||
EffectiveModelParams ExecutionTarget
|
||||
APIKeyRequired bool
|
||||
}
|
||||
|
||||
// OutputContract defines the requirements for the output artifact.
|
||||
type OutputContract struct {
|
||||
Format OutputFormat `yaml:"format"`
|
||||
|
||||
@@ -87,6 +87,7 @@ type Engine struct {
|
||||
runner *usecase.Runner
|
||||
smokeTester *usecase.SmokeTester
|
||||
inspection configuration.Inspection
|
||||
resources resourceInspection
|
||||
listing identityInventory
|
||||
namespaces resourceNamespaces
|
||||
}
|
||||
@@ -105,6 +106,12 @@ type identityInventory interface {
|
||||
ListCollections(context.Context) ([]configuration.CollectionIdentity, error)
|
||||
}
|
||||
|
||||
type resourceInspection interface {
|
||||
InspectPrompt(context.Context, string) (*configuration.PromptResource, error)
|
||||
InspectProfile(context.Context, string) (*configuration.ProfileResource, error)
|
||||
InspectBackend(context.Context, string) (*configuration.BackendResource, error)
|
||||
}
|
||||
|
||||
// ListingFilterMode selects namespace membership for one identity listing.
|
||||
type ListingFilterMode uint8
|
||||
|
||||
@@ -173,6 +180,7 @@ func New(cfg Config, options Options) (*Engine, error) {
|
||||
runner: runner,
|
||||
smokeTester: usecase.NewSmokeTester(runner, diagnosticHTTPClient),
|
||||
inspection: snapshot.Inspection(),
|
||||
resources: snapshot,
|
||||
listing: snapshot,
|
||||
}, nil
|
||||
}
|
||||
@@ -224,13 +232,16 @@ func fileSource(name string) (fs.FS, string, error) {
|
||||
return os.DirFS(directory), filepath.ToSlash(base), nil
|
||||
}
|
||||
|
||||
func (e *Engine) InspectPrompt(ctx context.Context, id string) (*domain.PromptInspection, error) {
|
||||
func (e *Engine) InspectPrompt(ctx context.Context, id string) (*configuration.PromptResource, error) {
|
||||
if strings.TrimSpace(id) == "" {
|
||||
return nil, fmt.Errorf("%w: prompt id is required", usecase.ErrInvalidRequest)
|
||||
}
|
||||
var err error
|
||||
id, err = e.qualifyResourceID(id, e.namespaces.prompt, "prompt")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.runner.InspectPrompt(ctx, id)
|
||||
return e.resources.InspectPrompt(ctx, id)
|
||||
}
|
||||
func (e *Engine) InspectRequirements(
|
||||
ctx context.Context,
|
||||
@@ -247,13 +258,27 @@ func (e *Engine) InspectRequirements(
|
||||
}
|
||||
return e.runner.InspectRequirements(ctx, request)
|
||||
}
|
||||
func (e *Engine) InspectProfile(ctx context.Context, id string) (*domain.ProfileInspection, error) {
|
||||
func (e *Engine) InspectProfile(ctx context.Context, id string) (*configuration.ProfileResource, error) {
|
||||
if strings.TrimSpace(id) == "" {
|
||||
return nil, fmt.Errorf("%w: profile id is required", usecase.ErrInvalidRequest)
|
||||
}
|
||||
var err error
|
||||
id, err = e.qualifyResourceID(id, e.namespaces.profile, "profile")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.runner.InspectProfile(ctx, id)
|
||||
return e.resources.InspectProfile(ctx, id)
|
||||
}
|
||||
|
||||
func (e *Engine) InspectBackend(ctx context.Context, id string) (*configuration.BackendResource, error) {
|
||||
if strings.TrimSpace(id) == "" {
|
||||
return nil, fmt.Errorf("%w: backend id is required", usecase.ErrInvalidRequest)
|
||||
}
|
||||
id, err := e.qualifyResourceID(id, e.namespaces.backend, "backend")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.resources.InspectBackend(ctx, id)
|
||||
}
|
||||
|
||||
func (e *Engine) ListBackends(ctx context.Context, filters ...ListingFilter) ([]string, error) {
|
||||
|
||||
@@ -14,6 +14,7 @@ var ErrPromptDefinitionNotFound = errors.New("prompt definition not found")
|
||||
// Repository selects prompt definitions from a captured index.
|
||||
type Repository interface {
|
||||
GetPromptSnapshot(ctx context.Context, id string) (*domain.PromptDefinition, string, error)
|
||||
// GetPromptInspection supplies lightweight requirement-selection metadata.
|
||||
GetPromptInspection(ctx context.Context, id string) (*domain.PromptInspection, error)
|
||||
GetRequirementSnapshot(id string) (prompt.RequirementSnapshot, bool)
|
||||
}
|
||||
|
||||
38
internal/usecase/profile_fixture_test.go
Normal file
38
internal/usecase/profile_fixture_test.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"go.maximumdirect.net/promptkit/internal/domain"
|
||||
"go.maximumdirect.net/promptkit/internal/jsonvalue"
|
||||
)
|
||||
|
||||
type inspectionProfileRepository struct {
|
||||
profile *domain.ExecutionProfile
|
||||
err error
|
||||
calls int
|
||||
id string
|
||||
}
|
||||
|
||||
func (r *inspectionProfileRepository) GetProfile(_ context.Context, id string) (*domain.ExecutionProfile, error) {
|
||||
r.calls++
|
||||
r.id = id
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.profile == nil {
|
||||
return nil, nil
|
||||
}
|
||||
profile := *r.profile
|
||||
if profile.ResolvedTarget != nil {
|
||||
target := *profile.ResolvedTarget
|
||||
target.ExtraParams = jsonvalue.CloneTrusted(target.ExtraParams).(map[string]any)
|
||||
profile.ResolvedTarget = &target
|
||||
}
|
||||
return &profile, nil
|
||||
}
|
||||
|
||||
func (r *inspectionProfileRepository) GetProfileIdentity(ctx context.Context, id string) (string, error) {
|
||||
_, err := r.GetProfile(ctx, id)
|
||||
return id, err
|
||||
}
|
||||
@@ -85,30 +85,3 @@ func normalizeResolvedExecutionSettings(target domain.ExecutionTarget) (domain.E
|
||||
}
|
||||
return target, nil
|
||||
}
|
||||
|
||||
// InspectProfile resolves one explicit profile without prompt or execution work.
|
||||
func (r *Runner) InspectProfile(
|
||||
ctx context.Context,
|
||||
profileID string,
|
||||
) (*domain.ProfileInspection, error) {
|
||||
if strings.TrimSpace(profileID) == "" {
|
||||
return nil, fmt.Errorf("%w: profile id is required", ErrInvalidRequest)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, &OperationError{Category: ErrProfileLoad, Cause: ctx.Err()}
|
||||
default:
|
||||
}
|
||||
|
||||
selection, err := r.resolveProfileSelection(ctx, profileID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
target, _ := resolveExecutionTarget(selection.profile, nil)
|
||||
target.APIKey = ""
|
||||
return &domain.ProfileInspection{
|
||||
ProfileID: selection.id,
|
||||
EffectiveModelParams: target,
|
||||
APIKeyRequired: target.APIKeyRequired,
|
||||
}, nil
|
||||
}
|
||||
|
||||
@@ -1,105 +0,0 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"go.maximumdirect.net/promptkit/internal/domain"
|
||||
"go.maximumdirect.net/promptkit/internal/jsonvalue"
|
||||
"go.maximumdirect.net/promptkit/internal/profile"
|
||||
)
|
||||
|
||||
type inspectionProfileRepository struct {
|
||||
profile *domain.ExecutionProfile
|
||||
err error
|
||||
calls int
|
||||
id string
|
||||
}
|
||||
|
||||
func (r *inspectionProfileRepository) GetProfile(_ context.Context, id string) (*domain.ExecutionProfile, error) {
|
||||
r.calls++
|
||||
r.id = id
|
||||
if r.err != nil {
|
||||
return nil, r.err
|
||||
}
|
||||
if r.profile == nil {
|
||||
return nil, nil
|
||||
}
|
||||
profile := *r.profile
|
||||
if profile.ResolvedTarget != nil {
|
||||
target := *profile.ResolvedTarget
|
||||
target.ExtraParams = jsonvalue.CloneTrusted(target.ExtraParams).(map[string]any)
|
||||
profile.ResolvedTarget = &target
|
||||
}
|
||||
return &profile, nil
|
||||
}
|
||||
|
||||
func (r *inspectionProfileRepository) GetProfileIdentity(ctx context.Context, id string) (string, error) {
|
||||
_, err := r.GetProfile(ctx, id)
|
||||
return id, err
|
||||
}
|
||||
|
||||
func TestRunnerInspectProfileUsesExactSnapshotAndCopiesTarget(t *testing.T) {
|
||||
configured := domain.ExecutionTarget{
|
||||
BackendID: "backend", Endpoint: "https://backend.example/v1", Model: "model",
|
||||
Temperature: 0, APIKeyEnv: "OPTIONAL_KEY", ExtraParams: map[string]any{"nested": map[string]any{"value": "original"}},
|
||||
}
|
||||
profiles := &inspectionProfileRepository{profile: &domain.ExecutionProfile{
|
||||
ID: " literal ", ResolvedTarget: &configured,
|
||||
TargetPresence: domain.ExecutionTargetPresence{Temperature: true},
|
||||
}}
|
||||
runner := &Runner{profiles: profiles}
|
||||
first, err := runner.InspectProfile(context.Background(), " literal ")
|
||||
if err != nil {
|
||||
t.Fatalf("inspect exact ID: %v", err)
|
||||
}
|
||||
if profiles.calls != 1 || profiles.id != " literal " || first.ProfileID != profiles.id || !reflect.DeepEqual(first.EffectiveModelParams, configured) {
|
||||
t.Fatalf("inspection=%+v lookup=(%d,%q), want exact snapshot", first, profiles.calls, profiles.id)
|
||||
}
|
||||
first.EffectiveModelParams.ExtraParams["nested"].(map[string]any)["value"] = "changed"
|
||||
second, err := runner.InspectProfile(context.Background(), " literal ")
|
||||
if err != nil || second.EffectiveModelParams.ExtraParams["nested"].(map[string]any)["value"] != "original" {
|
||||
t.Fatalf("inspection mutated snapshot: (%+v, %v)", second, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerInspectProfileDoesNotNeedExecutionCollaboratorsOrCredentials(t *testing.T) {
|
||||
t.Setenv("PROMPTKIT_INSPECTION_TEST_KEY", "")
|
||||
profiles := &inspectionProfileRepository{profile: &domain.ExecutionProfile{
|
||||
ID: "endpoint-only", ResolvedTarget: &domain.ExecutionTarget{
|
||||
Endpoint: "https://profile.example/v1", Model: "profile-model", APIKeyEnv: "PROMPTKIT_INSPECTION_TEST_KEY",
|
||||
},
|
||||
}}
|
||||
inspection, err := (&Runner{profiles: profiles}).InspectProfile(context.Background(), "endpoint-only")
|
||||
if err != nil || inspection.EffectiveModelParams.APIKeyEnv != "PROMPTKIT_INSPECTION_TEST_KEY" || inspection.APIKeyRequired {
|
||||
t.Fatalf("unexpected endpoint-only inspection: (%+v, %v)", inspection, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerInspectProfileClassifiesLookupFailures(t *testing.T) {
|
||||
profiles := &inspectionProfileRepository{}
|
||||
runner := &Runner{profiles: profiles}
|
||||
if _, err := runner.InspectProfile(context.Background(), " \t "); !errors.Is(err, ErrInvalidRequest) || profiles.calls != 0 {
|
||||
t.Fatalf("blank inspection=(%v, calls=%d)", err, profiles.calls)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
if _, err := runner.InspectProfile(ctx, "profile"); !errors.Is(err, ErrProfileLoad) || !errors.Is(err, context.Canceled) || profiles.calls != 0 {
|
||||
t.Fatalf("canceled inspection=(%v, calls=%d)", err, profiles.calls)
|
||||
}
|
||||
profiles.err = profile.ErrProfileNotFound
|
||||
if _, err := runner.InspectProfile(context.Background(), "missing"); !errors.Is(err, ErrProfileLoad) || !errors.Is(err, profile.ErrProfileNotFound) {
|
||||
t.Fatalf("missing profile error=%v", err)
|
||||
}
|
||||
for _, invalid := range []*Runner{
|
||||
{},
|
||||
{profiles: &inspectionProfileRepository{}},
|
||||
{profiles: &inspectionProfileRepository{profile: &domain.ExecutionProfile{ID: "profile"}}},
|
||||
} {
|
||||
if _, err := invalid.InspectProfile(context.Background(), "profile"); !errors.Is(err, ErrProfileLoad) {
|
||||
t.Fatalf("invalid dependency error=%v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -48,17 +48,3 @@ func (r *Runner) resolvePromptInspection(ctx context.Context, promptID string) (
|
||||
}
|
||||
return inspection, nil
|
||||
}
|
||||
|
||||
// InspectPrompt resolves one explicit prompt without execution work.
|
||||
func (r *Runner) InspectPrompt(ctx context.Context, promptID string) (*domain.PromptInspection, error) {
|
||||
if strings.TrimSpace(promptID) == "" {
|
||||
return nil, fmt.Errorf("%w: prompt id is required", ErrInvalidRequest)
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, &OperationError{Category: ErrPromptLoad, Cause: ctx.Err()}
|
||||
default:
|
||||
}
|
||||
|
||||
return r.resolvePromptInspection(ctx, promptID)
|
||||
}
|
||||
|
||||
@@ -1,218 +0,0 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"go.maximumdirect.net/promptkit/internal/domain"
|
||||
"go.maximumdirect.net/promptkit/internal/prompt"
|
||||
"go.maximumdirect.net/promptkit/internal/promptdef"
|
||||
)
|
||||
|
||||
type inspectionPromptRepository struct {
|
||||
definition *domain.PromptDefinition
|
||||
err error
|
||||
calls int
|
||||
id string
|
||||
}
|
||||
|
||||
type promptMetadataRepository struct {
|
||||
inspection domain.PromptInspection
|
||||
inspectionErr error
|
||||
lookupID string
|
||||
}
|
||||
|
||||
func (r *inspectionPromptRepository) GetPromptSnapshot(
|
||||
_ context.Context,
|
||||
id string,
|
||||
) (*domain.PromptDefinition, string, error) {
|
||||
r.calls++
|
||||
r.id = id
|
||||
if r.err != nil {
|
||||
return nil, "", r.err
|
||||
}
|
||||
return r.definition, "captured-hash", nil
|
||||
}
|
||||
|
||||
func (r *inspectionPromptRepository) GetPromptInspection(ctx context.Context, id string) (*domain.PromptInspection, error) {
|
||||
definition, hash, err := r.GetPromptSnapshot(ctx, id)
|
||||
return fixturePromptInspection(definition, hash), err
|
||||
}
|
||||
|
||||
func (*inspectionPromptRepository) GetRequirementSnapshot(string) (prompt.RequirementSnapshot, bool) {
|
||||
return prompt.RequirementSnapshot{}, false
|
||||
}
|
||||
|
||||
func (r *promptMetadataRepository) GetPromptSnapshot(context.Context, string) (*domain.PromptDefinition, string, error) {
|
||||
return nil, "", errors.New("full prompt definition should not be requested")
|
||||
}
|
||||
|
||||
func (*promptMetadataRepository) GetRequirementSnapshot(string) (prompt.RequirementSnapshot, bool) {
|
||||
return prompt.RequirementSnapshot{}, false
|
||||
}
|
||||
|
||||
func (r *promptMetadataRepository) GetPromptInspection(
|
||||
_ context.Context,
|
||||
id string,
|
||||
) (*domain.PromptInspection, error) {
|
||||
r.lookupID = id
|
||||
inspection := r.inspection
|
||||
inspection.Inputs = append([]domain.PromptInput(nil), inspection.Inputs...)
|
||||
return &inspection, r.inspectionErr
|
||||
}
|
||||
|
||||
func TestRunnerInspectPromptResolvesOneDefinitionWithoutExecutionCollaborators(t *testing.T) {
|
||||
definition := &domain.PromptDefinition{
|
||||
ID: "normalized.prompt",
|
||||
DefaultProfile: "not-resolved",
|
||||
Inputs: []domain.PromptInput{
|
||||
{Name: "document", Required: true, ContentType: "text/plain", Description: "Source document."},
|
||||
{Name: "audience", ContentType: "text/plain", Description: "Intended reader."},
|
||||
},
|
||||
Validation: domain.OutputContract{
|
||||
Format: domain.FormatJSON,
|
||||
ValidationMode: domain.ValidationJSONSchema,
|
||||
SchemaPath: "schemas/result.json",
|
||||
RepairAttempts: 2,
|
||||
},
|
||||
}
|
||||
repository := &inspectionPromptRepository{definition: definition}
|
||||
runner := &Runner{promptDefs: repository}
|
||||
|
||||
inspection, err := runner.InspectPrompt(context.Background(), " prompt-id ")
|
||||
if err != nil {
|
||||
t.Fatalf("inspect prompt: %v", err)
|
||||
}
|
||||
if repository.calls != 1 || repository.id != " prompt-id " {
|
||||
t.Fatalf("prompt lookup=(calls=%d id=%q), want one unchanged ID lookup", repository.calls, repository.id)
|
||||
}
|
||||
if inspection.PromptID != definition.ID ||
|
||||
inspection.PromptHash != "captured-hash" ||
|
||||
inspection.DefaultProfileID != definition.DefaultProfile ||
|
||||
!reflect.DeepEqual(inspection.Inputs, definition.Inputs) ||
|
||||
inspection.OutputContract != definition.Validation {
|
||||
t.Fatalf("inspection=%#v, want definition metadata", inspection)
|
||||
}
|
||||
|
||||
inspection.Inputs[0].Name = "changed"
|
||||
second, err := runner.InspectPrompt(context.Background(), " prompt-id ")
|
||||
if err != nil {
|
||||
t.Fatalf("inspect prompt again: %v", err)
|
||||
}
|
||||
if definition.Inputs[0].Name != "document" || second.Inputs[0].Name != "document" {
|
||||
t.Fatalf("inspection input mutation escaped caller result: definition=%#v next=%#v", definition.Inputs, second.Inputs)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerInspectPromptUsesOwnedCapturedMetadata(t *testing.T) {
|
||||
repository := &promptMetadataRepository{inspection: domain.PromptInspection{
|
||||
PromptID: "captured.prompt", PromptHash: "cached-prompt-hash",
|
||||
DefaultProfileID: "partial-profile",
|
||||
Inputs: []domain.PromptInput{{Name: "document", Required: true, ContentType: "text/plain", Description: "Captured."}},
|
||||
OutputContract: domain.OutputContract{Format: domain.FormatJSON, ValidationMode: domain.ValidationJSON},
|
||||
}}
|
||||
runner := &Runner{promptDefs: repository}
|
||||
inspection, err := runner.InspectPrompt(context.Background(), " requested-id ")
|
||||
if err != nil || repository.lookupID != " requested-id " ||
|
||||
inspection.PromptID != "captured.prompt" ||
|
||||
inspection.PromptHash != "cached-prompt-hash" || inspection.DefaultProfileID != "partial-profile" ||
|
||||
!reflect.DeepEqual(inspection.Inputs, repository.inspection.Inputs) || inspection.OutputContract != repository.inspection.OutputContract {
|
||||
t.Fatalf("metadata inspection=(%+v, %v), lookup=%q", inspection, err, repository.lookupID)
|
||||
}
|
||||
inspection.Inputs[0].Name = "caller mutation"
|
||||
inspection.OutputContract.SchemaPath = "caller mutation"
|
||||
second, err := runner.InspectPrompt(context.Background(), "requested-id")
|
||||
if err != nil || second.Inputs[0].Name != "document" || second.OutputContract.SchemaPath != "" {
|
||||
t.Fatalf("metadata inspection was not caller-owned: (%+v, %v)", second, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerInspectPromptUsesCapturedHashAndMetadata(t *testing.T) {
|
||||
repository := &requirementInspectionPromptRepository{
|
||||
definition: &domain.PromptDefinition{
|
||||
ID: "captured.prompt", Inputs: []domain.PromptInput{{Name: "document"}},
|
||||
},
|
||||
}
|
||||
inspection, err := (&Runner{promptDefs: repository}).InspectPrompt(context.Background(), " requested-id ")
|
||||
if err != nil || inspection.PromptID != "captured.prompt" ||
|
||||
inspection.PromptHash != "captured-hash" || inspection.Inputs[0].Name != "document" {
|
||||
t.Fatalf("captured inspection=(%+v, %v)", inspection, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunnerInspectPromptClassifiesFailuresWithoutRepositoryWorkAfterCancellation(t *testing.T) {
|
||||
t.Run("blank ID", func(t *testing.T) {
|
||||
repository := &inspectionPromptRepository{}
|
||||
_, err := (&Runner{promptDefs: repository}).InspectPrompt(context.Background(), " \t ")
|
||||
if !errors.Is(err, ErrInvalidRequest) || repository.calls != 0 {
|
||||
t.Fatalf("blank inspection=(%v, calls=%d), want invalid request without lookup", err, repository.calls)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("canceled context", func(t *testing.T) {
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
repository := &inspectionPromptRepository{}
|
||||
_, err := (&Runner{promptDefs: repository}).InspectPrompt(ctx, "prompt")
|
||||
if !errors.Is(err, ErrPromptLoad) || !errors.Is(err, context.Canceled) || repository.calls != 0 {
|
||||
t.Fatalf("canceled inspection=(%v, calls=%d), want prompt load and context identities without lookup", err, repository.calls)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing prompt", func(t *testing.T) {
|
||||
repository := &inspectionPromptRepository{err: promptdef.ErrPromptDefinitionNotFound}
|
||||
_, err := (&Runner{promptDefs: repository}).InspectPrompt(context.Background(), "missing")
|
||||
if !errors.Is(err, ErrPromptLoad) || !errors.Is(err, promptdef.ErrPromptDefinitionNotFound) {
|
||||
t.Fatalf("missing prompt error=%v, want prompt load and not-found identities", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("defensive prompt dependencies", func(t *testing.T) {
|
||||
var nilRunner *Runner
|
||||
cases := []struct {
|
||||
name string
|
||||
runner *Runner
|
||||
}{
|
||||
{name: "nil runner", runner: nilRunner},
|
||||
{name: "nil repository", runner: &Runner{}},
|
||||
{name: "nil definition", runner: &Runner{promptDefs: &inspectionPromptRepository{}}},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
_, err := tc.runner.InspectPrompt(context.Background(), "prompt")
|
||||
if !errors.Is(err, ErrPromptLoad) {
|
||||
t.Fatalf("inspection error=%v, want prompt load", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestRunnerPrepareUsesThePromptInspectionSelectionAndHash(t *testing.T) {
|
||||
definition := promptDef(domain.FormatMarkdown, domain.ValidationBasic, 0)
|
||||
repository := &fakePromptRepo{def: definition}
|
||||
runner := NewRunner(
|
||||
repository,
|
||||
&fakeExecutionProfileRepo{profiles: map[string]*domain.ExecutionProfile{"exec": defaultExecutionProfile()}},
|
||||
|
||||
&fakeArtifactReader{},
|
||||
&fakeRenderer{rendered: &domain.RenderedPrompt{Messages: []domain.RenderedMessage{{Role: "user", Content: "hello"}}}},
|
||||
nil,
|
||||
nil,
|
||||
nil,
|
||||
)
|
||||
|
||||
inspection, err := runner.InspectPrompt(context.Background(), definition.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("inspect prompt: %v", err)
|
||||
}
|
||||
prepared, err := runner.Prepare(context.Background(), domain.RunRequest{PromptID: definition.ID, ProfileID: "exec"})
|
||||
if err != nil {
|
||||
t.Fatalf("prepare prompt: %v", err)
|
||||
}
|
||||
if inspection.PromptHash != prepared.PromptHash {
|
||||
t.Fatalf("inspection hash=%q, preparation hash=%q", inspection.PromptHash, prepared.PromptHash)
|
||||
}
|
||||
}
|
||||
@@ -9,20 +9,6 @@ import (
|
||||
"go.maximumdirect.net/promptkit/internal/domain"
|
||||
)
|
||||
|
||||
func TestInspectProfileAllowsIncompleteResolvedTarget(t *testing.T) {
|
||||
profile := &domain.ExecutionProfile{ID: "partial", ResolvedTarget: &domain.ExecutionTarget{
|
||||
TimeoutSeconds: defaults.ExecutionDefaultTimeoutSeconds,
|
||||
}}
|
||||
runner := &Runner{profiles: staticExecutionProfileRepo{profile: profile}}
|
||||
inspection, err := runner.InspectProfile(context.Background(), "partial")
|
||||
if err != nil {
|
||||
t.Fatalf("partial profile inspection failed: %v", err)
|
||||
}
|
||||
if inspection.EffectiveModelParams.Endpoint != "" || inspection.EffectiveModelParams.Model != "" {
|
||||
t.Fatalf("inspection invented a runnable target: %+v", inspection)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPreparationCompletesResolvedTargetThroughRequestOverrides(t *testing.T) {
|
||||
profile := &domain.ExecutionProfile{ID: "partial", ResolvedTarget: &domain.ExecutionTarget{
|
||||
TimeoutSeconds: defaults.ExecutionDefaultTimeoutSeconds,
|
||||
|
||||
@@ -438,11 +438,11 @@ func TestListIdentitiesUseEffectiveLayeredMembership(t *testing.T) {
|
||||
}
|
||||
|
||||
overlay, err := engine.InspectProfile(context.Background(), "shared/overlay")
|
||||
if err != nil || overlay == nil || overlay.EffectiveModelParams.Model != "effective-model" {
|
||||
if err != nil || overlay == nil || overlay.Model == nil || *overlay.Model != "effective-model" {
|
||||
t.Fatalf("overlay profile did not resolve to its effective definition: (%+v, %v)", overlay, err)
|
||||
}
|
||||
child, err := engine.InspectProfile(context.Background(), "shared/child")
|
||||
if err != nil || child == nil || child.EffectiveModelParams.Model != "base-model" {
|
||||
if err != nil || child == nil || child.Model == nil || *child.Model != "base-model" {
|
||||
t.Fatalf("inherited child profile did not resolve its effective definition: (%+v, %v)", child, err)
|
||||
}
|
||||
|
||||
|
||||
@@ -87,6 +87,26 @@ func TestWithNamespacesRoutesOrdinarySelectionsAndPreservesViews(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestInspectBackendNamespaceSelection(t *testing.T) {
|
||||
engine := newNamespacedContractEngine(t, &fakeLLMClient{})
|
||||
scoped, err := engine.WithNamespaces(promptkit.Namespaces{Backend: "alpha"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for id, want := range map[string]string{"backend": "alpha/backend", "beta/backend": "beta/backend"} {
|
||||
got, err := scoped.InspectBackend(context.Background(), id)
|
||||
if err != nil || got.BackendID != want {
|
||||
t.Errorf("backend %q = %+v, %v", id, got, err)
|
||||
}
|
||||
}
|
||||
if _, err := scoped.InspectBackend(context.Background(), "openrouter"); !errors.Is(err, promptkit.ErrBackendNotFound) {
|
||||
t.Fatalf("unexpected fallback: %v", err)
|
||||
}
|
||||
if got, err := engine.InspectBackend(context.Background(), "openrouter"); err != nil || got.BackendID != "openrouter" {
|
||||
t.Fatalf("bundled backend = %+v, %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestWithNamespacesHasNoFallbackAndValidatesAtTheCorrectBoundary(t *testing.T) {
|
||||
engine := newNamespacedContractEngine(t, &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "reply"}})
|
||||
scoped, err := engine.WithNamespaces(promptkit.Namespaces{Prompt: "alpha", Profile: "alpha"})
|
||||
|
||||
@@ -41,9 +41,8 @@ func TestProfileInheritanceConfiguredBaseAliasWorkflow(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatalf("inspect alias: %v", err)
|
||||
}
|
||||
if child.ProfileID != "child" || child.EffectiveModelParams.BackendID != base.EffectiveModelParams.BackendID ||
|
||||
child.EffectiveModelParams.Model != base.EffectiveModelParams.Model ||
|
||||
child.EffectiveModelParams.ReasoningEffort != reasoning || child.EffectiveModelParams.TimeoutSeconds != timeout {
|
||||
if child.ProfileID != "child" || child.Backend != nil || base.Backend != nil || child.Model == nil || base.Model == nil || *child.Model != *base.Model ||
|
||||
child.ReasoningEffort == nil || *child.ReasoningEffort != reasoning || child.TimeoutSeconds == nil || *child.TimeoutSeconds != timeout {
|
||||
t.Fatalf("alias inspection = %+v, base = %+v", child, base)
|
||||
}
|
||||
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: "prepared"})
|
||||
@@ -71,7 +70,7 @@ func TestProfileInheritanceYAMLAliasOfConfiguredProfile(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inspection, err := engine.InspectProfile(context.Background(), "yaml-alias")
|
||||
if err != nil || inspection.ProfileID != "yaml-alias" || inspection.EffectiveModelParams.Model == "" {
|
||||
if err != nil || inspection.ProfileID != "yaml-alias" || inspection.Model == nil || *inspection.Model == "" {
|
||||
t.Fatalf("YAML alias inspection = (%+v, %v)", inspection, err)
|
||||
}
|
||||
}
|
||||
@@ -92,7 +91,7 @@ func TestProfileInheritanceRetainsRequiredCredentialBehavior(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
inspection, err := engine.InspectProfile(context.Background(), "credential-child")
|
||||
if err != nil || !inspection.APIKeyRequired || inspection.EffectiveModelParams.APIKeyEnv != "" {
|
||||
if err != nil || inspection.APIKeyRequired == nil || !*inspection.APIKeyRequired || inspection.APIKeyEnv != nil {
|
||||
t.Fatalf("credential inspection = (%+v, %v)", inspection, err)
|
||||
}
|
||||
}
|
||||
@@ -144,7 +143,7 @@ func TestProfileInheritanceFreezesPreparedExecution(t *testing.T) {
|
||||
t.Fatalf("prepared execution = (%+v, %v), model %q", result, err, client.model)
|
||||
}
|
||||
inspection, err := engine.InspectProfile(context.Background(), "child")
|
||||
if err != nil || inspection.EffectiveModelParams.Model != "first-model" {
|
||||
if err != nil || inspection.Model == nil || *inspection.Model != "first-model" {
|
||||
t.Fatalf("existing engine inspection = (%+v, %v)", inspection, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestInspectProfileUsesLiteralFileKeyForPreparation(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if inspected.ProfileID != " literal " || prepared.SelectedProfileID != inspected.ProfileID || prepared.EffectiveModelParams.Model != inspected.EffectiveModelParams.Model {
|
||||
if inspected.ProfileID != " literal " || inspected.Model == nil || prepared.SelectedProfileID != inspected.ProfileID || prepared.EffectiveModelParams.Model != *inspected.Model {
|
||||
t.Fatalf("inspection=%+v preparation=%+v", inspected, prepared)
|
||||
}
|
||||
if _, err := engine.InspectProfile(context.Background(), "literal"); !errors.Is(err, promptkit.ErrProfileNotFound) {
|
||||
@@ -61,12 +61,12 @@ func TestInspectProfileIsIndependentAndDoesNotInvokeProvider(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
first, err := engine.InspectProfile(context.Background(), "profile")
|
||||
if err != nil || first == nil || !first.APIKeyRequired {
|
||||
if err != nil || first == nil || first.APIKeyRequired == nil || !*first.APIKeyRequired || first.ExtraParams == nil {
|
||||
t.Fatalf("first inspection=(%+v, %v)", first, err)
|
||||
}
|
||||
first.EffectiveModelParams.ExtraParams["nested"].(map[string]any)["value"] = "changed"
|
||||
(*first.ExtraParams)["nested"].(map[string]any)["value"] = "changed"
|
||||
second, err := engine.InspectProfile(context.Background(), "profile")
|
||||
if err != nil || second == nil || second.EffectiveModelParams.ExtraParams["nested"].(map[string]any)["value"] != "original" || len(client.requests) != 0 {
|
||||
if err != nil || second == nil || second.ExtraParams == nil || (*second.ExtraParams)["nested"].(map[string]any)["value"] != "original" || len(client.requests) != 0 {
|
||||
t.Fatalf("inspection changed snapshot or invoked provider: (%+v, %v), calls=%d", second, err, len(client.requests))
|
||||
}
|
||||
}
|
||||
@@ -94,7 +94,7 @@ func TestInspectProfilePublicErrorsAndPartialTarget(t *testing.T) {
|
||||
t.Fatalf("canceled inspection: %v", err)
|
||||
}
|
||||
partial, err := engine.InspectProfile(context.Background(), "profile")
|
||||
if err != nil || partial == nil || partial.EffectiveModelParams.Endpoint != "" || partial.EffectiveModelParams.Model != "model" {
|
||||
if err != nil || partial == nil || partial.Endpoint != nil || partial.Model == nil || *partial.Model != "model" {
|
||||
t.Fatalf("partial inspection=(%+v, %v)", partial, err)
|
||||
}
|
||||
}
|
||||
|
||||
106
prompt_sources_contract_test.go
Normal file
106
prompt_sources_contract_test.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package promptkit_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
"go.maximumdirect.net/configkit"
|
||||
"go.maximumdirect.net/promptkit"
|
||||
)
|
||||
|
||||
func TestTypedSourcesComposeAndCaptureValues(t *testing.T) {
|
||||
endpoint, childEndpoint, base := "https://base.test/v1", "https://child.test/v1", "base"
|
||||
limit, queue := 2, 0
|
||||
backendSource := promptkit.BackendSource("backends", []promptkit.BackendConfig{{ID: "base", Endpoint: &endpoint, ConcurrencyLimit: &limit}, {ID: "child", Extends: &base, Endpoint: &childEndpoint, QueueCapacity: &queue}})
|
||||
role, content, secondContent, empty, cacheType := "user", "hello {{.name}}", "second", "", "ephemeral"
|
||||
format, validation, description, profileID := "text", "none", "description", "local"
|
||||
required, repairs := false, 0
|
||||
prompts := []promptkit.PromptConfig{
|
||||
{ID: "base", DefaultProfile: &profileID, Description: &description, Inputs: []promptkit.PromptInputConfig{{Name: "input", Required: &required, Description: &empty}}, Messages: []promptkit.PromptMessageConfig{{Role: &role, Content: &content, CacheControl: &promptkit.PromptCacheControlConfig{Type: &cacheType, TTL: &empty}}, {Role: &role, Content: &secondContent}}, Output: &promptkit.PromptOutputConfig{Format: &format, ValidationMode: &validation, RepairAttempts: &repairs}},
|
||||
{ID: "child", Extends: &base},
|
||||
{ID: "layered"},
|
||||
{ID: "empty-inputs", Inputs: []promptkit.PromptInputConfig{}, Messages: []promptkit.PromptMessageConfig{{Role: &role, Content: &content}}, Output: &promptkit.PromptOutputConfig{Format: &format, ValidationMode: &validation}},
|
||||
{ID: "omitted-inputs", Messages: []promptkit.PromptMessageConfig{{Role: &role, Content: &content}}, Output: &promptkit.PromptOutputConfig{Format: &format, ValidationMode: &validation}},
|
||||
}
|
||||
promptSource := promptkit.PromptSource("typed prompts", prompts)
|
||||
endpoint, childEndpoint, role, content, description = "changed", "changed", "changed", "changed", "changed"
|
||||
prompts[0].Inputs[0].Name = "changed"
|
||||
prompts[0].Messages[1].Content = &empty
|
||||
prompts[1].ID = "changed"
|
||||
profileEndpoint, model := "https://profile.test/v1", "model"
|
||||
main := configkit.Input{Source: configkit.Mapping("completion", map[string]any{"prompts": []any{map[string]any{"id": "layered", "messages": []any{map[string]any{"role": "user", "content": "layer"}}, "output": map[string]any{"format": "text", "validation_mode": "none"}}}})}
|
||||
profileSource := promptkit.ProfileSource("profiles", []promptkit.ProfileConfig{{ID: "local", Endpoint: &profileEndpoint, Model: &model}})
|
||||
underlays := []configkit.Underlay{{Inputs: []configkit.Input{{Source: backendSource}, {Source: promptSource}, {Source: profileSource}}}}
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Underlays: underlays, Main: &main}}, promptkit.WithBundledOpenRouter(false))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
backend, err := engine.InspectBackend(ctx, "child")
|
||||
if err != nil || backend.BackendID != "child" || backend.Endpoint != "https://child.test/v1" || backend.ConcurrencyLimit == nil || *backend.ConcurrencyLimit != 2 || backend.QueueCapacity == nil || *backend.QueueCapacity != 0 {
|
||||
t.Fatalf("backend = %+v, %v", backend, err)
|
||||
}
|
||||
prompt, err := engine.InspectPrompt(ctx, "child")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if prompt.DefaultProfileID == nil || *prompt.DefaultProfileID != "local" || prompt.Description == nil || *prompt.Description != "description" || prompt.Inputs == nil || len(*prompt.Inputs) != 1 || (*prompt.Inputs)[0].Name != "input" || (*prompt.Inputs)[0].Required == nil || *(*prompt.Inputs)[0].Required || (*prompt.Inputs)[0].Description == nil || *(*prompt.Inputs)[0].Description != "" || len(prompt.Messages) != 2 || prompt.Messages[0].Content != "hello {{.name}}" || prompt.Messages[1].Content != "second" || prompt.Messages[0].CacheControl.TTL == nil || *prompt.Messages[0].CacheControl.TTL != "" || prompt.OutputContract.RepairAttempts == nil || *prompt.OutputContract.RepairAttempts != 0 {
|
||||
t.Fatalf("prompt = %+v", prompt)
|
||||
}
|
||||
prepared, err := engine.Prepare(ctx, promptkit.RunRequest{PromptID: "child", Vars: map[string]string{"name": "reader"}})
|
||||
if err != nil || prepared.Messages[0].Content != "hello reader" {
|
||||
t.Fatalf("preparation = %+v, %v", prepared, err)
|
||||
}
|
||||
for _, id := range []string{"layered", "empty-inputs", "omitted-inputs"} {
|
||||
result, err := engine.InspectPrompt(ctx, id)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if id == "empty-inputs" && (result.Inputs == nil || len(*result.Inputs) != 0) {
|
||||
t.Fatalf("empty inputs = %+v", result.Inputs)
|
||||
}
|
||||
if id == "omitted-inputs" && result.Inputs != nil {
|
||||
t.Fatalf("omitted inputs = %+v", result.Inputs)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypedPromptSourceUsesExplicitFilesystemContext(t *testing.T) {
|
||||
role, path, format, validation, parent := "user", "body.md", "text", "none", "base"
|
||||
source := promptkit.PromptSource("file prompts", []promptkit.PromptConfig{{ID: "base", Messages: []promptkit.PromptMessageConfig{{Role: &role, ContentFile: &path}}, Output: &promptkit.PromptOutputConfig{Format: &format, ValidationMode: &validation}}, {ID: "child", Extends: &parent}})
|
||||
files := fstest.MapFS{"templates/body.md": {Data: []byte("captured {{.value}}")}}
|
||||
filesystem := configkit.NewFilesystem("fixture", files)
|
||||
input := configkit.Input{Source: source, Filesystem: &configkit.FilesystemContext{FS: &filesystem, Root: ".", BaseDir: "templates"}}
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
files["templates/body.md"].Data = []byte("changed")
|
||||
prompt, err := engine.InspectPrompt(context.Background(), "child")
|
||||
if err != nil || prompt.Messages[0].Content != "captured {{.value}}" {
|
||||
t.Fatalf("file prompt = %+v, %v", prompt, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTypedPromptSourceDefersValidation(t *testing.T) {
|
||||
role, body, path, format, validation := "user", "body", "also-file", "text", "none"
|
||||
source := promptkit.PromptSource("invalid", []promptkit.PromptConfig{{ID: "bad", Messages: []promptkit.PromptMessageConfig{{Role: &role, Content: &body, ContentFile: &path}}, Output: &promptkit.PromptOutputConfig{Format: &format, ValidationMode: &validation}}})
|
||||
_, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{Source: source}}})
|
||||
if !errors.Is(err, promptkit.ErrInvalidConfig) {
|
||||
t.Fatalf("invalid final source: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNilTypedPromptSourceSuppliesEmptySequence(t *testing.T) {
|
||||
source := promptkit.PromptSource("empty", nil)
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{Source: source}}}, promptkit.WithBundledOpenRouter(false))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prompts, err := engine.ListPrompts(context.Background())
|
||||
if err != nil || len(prompts) != 0 {
|
||||
t.Fatalf("empty source = %v, %v", prompts, err)
|
||||
}
|
||||
}
|
||||
129
prompts.go
Normal file
129
prompts.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package promptkit
|
||||
|
||||
import "go.maximumdirect.net/configkit"
|
||||
|
||||
// PromptConfig is a presence-aware source input for one prompt. Extends names
|
||||
// a sibling prompt. Nil optional pointers and slices are omitted; non-nil
|
||||
// pointers and slices preserve explicit zero, blank, and empty values. These
|
||||
// inputs are mapping constructors, not serialized inspection values. Raw
|
||||
// ConfigKit mappings can express explicit null and other composition controls.
|
||||
type PromptConfig struct {
|
||||
ID string
|
||||
Extends, DefaultProfile, Description, SessionID *string
|
||||
Inputs []PromptInputConfig
|
||||
Messages []PromptMessageConfig
|
||||
Output *PromptOutputConfig
|
||||
}
|
||||
|
||||
// PromptInputConfig supplies an input declaration. Name is always supplied;
|
||||
// nil optional pointers omit their fields.
|
||||
type PromptInputConfig struct {
|
||||
Name string
|
||||
Required *bool
|
||||
ContentType, Description *string
|
||||
}
|
||||
|
||||
// PromptMessageConfig supplies message fields, including inline content or a
|
||||
// content-file reference. Final content selection is validated by NewEngine.
|
||||
type PromptMessageConfig struct {
|
||||
Role, Content, ContentFile *string
|
||||
CacheControl *PromptCacheControlConfig
|
||||
}
|
||||
|
||||
// PromptCacheControlConfig supplies optional cache-control source fields.
|
||||
type PromptCacheControlConfig struct{ Type, TTL *string }
|
||||
|
||||
// PromptOutputConfig supplies optional output contract source fields.
|
||||
type PromptOutputConfig struct {
|
||||
Format, ValidationMode, SchemaPath *string
|
||||
RepairAttempts *int
|
||||
}
|
||||
|
||||
// PromptSource creates a labeled ConfigKit mapping in caller order. Nil
|
||||
// top-level input supplies an empty prompts sequence. Records may be partial:
|
||||
// sibling inheritance and layered sources can complete them before NewEngine
|
||||
// validates the final configuration. Mapping snapshots supported values when
|
||||
// constructed; the helper neither resolves inheritance nor reads files.
|
||||
// Relative content files use the ConfigKit Input filesystem context of the
|
||||
// contributing field. Raw mappings remain available for explicit null and
|
||||
// generic ConfigKit operations.
|
||||
func PromptSource(label string, prompts []PromptConfig) configkit.Source {
|
||||
definitions := make([]any, 0, len(prompts))
|
||||
for _, config := range prompts {
|
||||
fields := map[string]any{"id": config.ID}
|
||||
if config.Extends != nil {
|
||||
fields["extends"] = map[string]any{"id": *config.Extends}
|
||||
}
|
||||
if config.DefaultProfile != nil {
|
||||
fields["default_profile"] = *config.DefaultProfile
|
||||
}
|
||||
if config.Description != nil {
|
||||
fields["description"] = *config.Description
|
||||
}
|
||||
if config.SessionID != nil {
|
||||
fields["session_id"] = *config.SessionID
|
||||
}
|
||||
if config.Inputs != nil {
|
||||
inputs := make([]any, 0, len(config.Inputs))
|
||||
for _, input := range config.Inputs {
|
||||
item := map[string]any{"name": input.Name}
|
||||
if input.Required != nil {
|
||||
item["required"] = *input.Required
|
||||
}
|
||||
if input.ContentType != nil {
|
||||
item["content_type"] = *input.ContentType
|
||||
}
|
||||
if input.Description != nil {
|
||||
item["description"] = *input.Description
|
||||
}
|
||||
inputs = append(inputs, item)
|
||||
}
|
||||
fields["inputs"] = inputs
|
||||
}
|
||||
if config.Messages != nil {
|
||||
messages := make([]any, 0, len(config.Messages))
|
||||
for _, message := range config.Messages {
|
||||
item := make(map[string]any)
|
||||
if message.Role != nil {
|
||||
item["role"] = *message.Role
|
||||
}
|
||||
if message.Content != nil {
|
||||
item["content"] = *message.Content
|
||||
}
|
||||
if message.ContentFile != nil {
|
||||
item["content_file"] = *message.ContentFile
|
||||
}
|
||||
if message.CacheControl != nil {
|
||||
cache := make(map[string]any)
|
||||
if message.CacheControl.Type != nil {
|
||||
cache["type"] = *message.CacheControl.Type
|
||||
}
|
||||
if message.CacheControl.TTL != nil {
|
||||
cache["ttl"] = *message.CacheControl.TTL
|
||||
}
|
||||
item["cache_control"] = cache
|
||||
}
|
||||
messages = append(messages, item)
|
||||
}
|
||||
fields["messages"] = messages
|
||||
}
|
||||
if config.Output != nil {
|
||||
output := make(map[string]any)
|
||||
if config.Output.Format != nil {
|
||||
output["format"] = *config.Output.Format
|
||||
}
|
||||
if config.Output.ValidationMode != nil {
|
||||
output["validation_mode"] = *config.Output.ValidationMode
|
||||
}
|
||||
if config.Output.SchemaPath != nil {
|
||||
output["schema_path"] = *config.Output.SchemaPath
|
||||
}
|
||||
if config.Output.RepairAttempts != nil {
|
||||
output["repair_attempts"] = *config.Output.RepairAttempts
|
||||
}
|
||||
fields["output"] = output
|
||||
}
|
||||
definitions = append(definitions, fields)
|
||||
}
|
||||
return configkit.Mapping(label, map[string]any{"prompts": definitions})
|
||||
}
|
||||
85
resource_inspection.go
Normal file
85
resource_inspection.go
Normal file
@@ -0,0 +1,85 @@
|
||||
package promptkit
|
||||
|
||||
// ProfileInspection describes a resolved profile resource, before backend
|
||||
// completion or request overrides. References are not resolved. APIKeyEnv is
|
||||
// an environment-variable name, never a credential value. The result and its
|
||||
// mutable values are caller-owned. Nil pointers denote absent fields; non-nil
|
||||
// pointers preserve explicit zero values and empty collections. JSON and YAML
|
||||
// use the indicated field names and omit absent optional fields.
|
||||
type ProfileInspection struct {
|
||||
ProfileID string `json:"id" yaml:"id"`
|
||||
Backend *string `json:"backend,omitempty" yaml:"backend,omitempty"`
|
||||
Endpoint *string `json:"endpoint,omitempty" yaml:"endpoint,omitempty"`
|
||||
Model *string `json:"model,omitempty" yaml:"model,omitempty"`
|
||||
Temperature *float64 `json:"temperature,omitempty" yaml:"temperature,omitempty"`
|
||||
TopP *float64 `json:"top_p,omitempty" yaml:"top_p,omitempty"`
|
||||
MaxTokens *int `json:"max_tokens,omitempty" yaml:"max_tokens,omitempty"`
|
||||
TimeoutSeconds *int `json:"timeout_seconds,omitempty" yaml:"timeout_seconds,omitempty"`
|
||||
ServiceTier *string `json:"service_tier,omitempty" yaml:"service_tier,omitempty"`
|
||||
ReasoningEffort *string `json:"reasoning_effort,omitempty" yaml:"reasoning_effort,omitempty"`
|
||||
APIKeyEnv *string `json:"api_key_env,omitempty" yaml:"api_key_env,omitempty"`
|
||||
APIKeyRequired *bool `json:"api_key_required,omitempty" yaml:"api_key_required,omitempty"`
|
||||
ExtraParams *map[string]any `json:"extra_params,omitempty" yaml:"extra_params,omitempty"`
|
||||
}
|
||||
|
||||
// BackendInspection describes a resolved backend resource, not runtime capacity
|
||||
// or diagnostic destinations derived during execution. APIKeyEnv names an
|
||||
// environment variable, not its value. The result and its mutable values are
|
||||
// caller-owned. Nil pointers denote absent fields; non-nil pointers preserve
|
||||
// explicit zero values and empty collections in JSON and YAML.
|
||||
type BackendInspection struct {
|
||||
BackendID string `json:"id" yaml:"id"`
|
||||
Endpoint string `json:"endpoint" yaml:"endpoint"`
|
||||
APIKeyEnv *string `json:"api_key_env,omitempty" yaml:"api_key_env,omitempty"`
|
||||
ConcurrencyLimit *int `json:"concurrency_limit,omitempty" yaml:"concurrency_limit,omitempty"`
|
||||
QueueCapacity *int `json:"queue_capacity,omitempty" yaml:"queue_capacity,omitempty"`
|
||||
ExtraParams *map[string]any `json:"extra_params,omitempty" yaml:"extra_params,omitempty"`
|
||||
SmokeTests *BackendSmokeTestConfig `json:"smoke_tests,omitempty" yaml:"smoke_tests,omitempty"`
|
||||
}
|
||||
|
||||
// PromptInspection describes a resolved prompt resource. Messages contain
|
||||
// captured inline or imported text, not rendered content. References are not
|
||||
// resolved by inspection; PromptHash is opaque. The result and its mutable
|
||||
// values are caller-owned. Nil pointers denote absent fields; non-nil pointers
|
||||
// preserve explicit zero values and empty collections in JSON and YAML.
|
||||
type PromptInspection struct {
|
||||
PromptID string `json:"id" yaml:"id"`
|
||||
PromptHash string `json:"prompt_hash" yaml:"prompt_hash"`
|
||||
DefaultProfileID *string `json:"default_profile,omitempty" yaml:"default_profile,omitempty"`
|
||||
Description *string `json:"description,omitempty" yaml:"description,omitempty"`
|
||||
SessionID *string `json:"session_id,omitempty" yaml:"session_id,omitempty"`
|
||||
Inputs *[]PromptInputInspection `json:"inputs,omitempty" yaml:"inputs,omitempty"`
|
||||
Messages []PromptMessageInspection `json:"messages" yaml:"messages"`
|
||||
OutputContract PromptOutputInspection `json:"output" yaml:"output"`
|
||||
}
|
||||
|
||||
// PromptInputInspection describes a declared input and its optional metadata.
|
||||
type PromptInputInspection struct {
|
||||
Name string `json:"name" yaml:"name"`
|
||||
Required *bool `json:"required,omitempty" yaml:"required,omitempty"`
|
||||
ContentType *string `json:"content_type,omitempty" yaml:"content_type,omitempty"`
|
||||
Description *string `json:"description,omitempty" yaml:"description,omitempty"`
|
||||
}
|
||||
|
||||
// PromptMessageInspection describes a captured, unrendered message.
|
||||
type PromptMessageInspection struct {
|
||||
Role string `json:"role" yaml:"role"`
|
||||
Content string `json:"content" yaml:"content"`
|
||||
CacheControl *PromptCacheControlInspection `json:"cache_control,omitempty" yaml:"cache_control,omitempty"`
|
||||
}
|
||||
|
||||
// PromptCacheControlInspection describes configured message cache metadata.
|
||||
// A non-nil TTL preserves an explicitly empty value.
|
||||
type PromptCacheControlInspection struct {
|
||||
Type CacheControlType `json:"type" yaml:"type"`
|
||||
TTL *string `json:"ttl,omitempty" yaml:"ttl,omitempty"`
|
||||
}
|
||||
|
||||
// PromptOutputInspection describes a prompt's output contract. SchemaPath is
|
||||
// a reference and is not loaded by inspection.
|
||||
type PromptOutputInspection struct {
|
||||
Format OutputFormat `json:"format" yaml:"format"`
|
||||
ValidationMode ValidationMode `json:"validation_mode" yaml:"validation_mode"`
|
||||
SchemaPath *string `json:"schema_path,omitempty" yaml:"schema_path,omitempty"`
|
||||
RepairAttempts *int `json:"repair_attempts,omitempty" yaml:"repair_attempts,omitempty"`
|
||||
}
|
||||
51
resource_inspection_api_test.go
Normal file
51
resource_inspection_api_test.go
Normal file
@@ -0,0 +1,51 @@
|
||||
package promptkit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"go.maximumdirect.net/configkit"
|
||||
)
|
||||
|
||||
func TestResourceInspectionSelections(t *testing.T) {
|
||||
engine, err := NewEngine(Config{Sources: ConfigurationSources{Main: &configkit.Input{Source: configkit.Mapping("resources", map[string]any{
|
||||
"backends": []any{map[string]any{"id": "local", "endpoint": "https://example.test/v1"}},
|
||||
"profiles": []any{map[string]any{"id": "partial", "backend": "local"}},
|
||||
"prompts": []any{map[string]any{"id": "prompt", "messages": []any{map[string]any{"role": "user", "content": "hello"}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}}},
|
||||
})}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
if got, err := engine.InspectBackend(ctx, "local"); err != nil || got.BackendID != "local" {
|
||||
t.Fatalf("backend = %+v, %v", got, err)
|
||||
}
|
||||
if got, err := engine.InspectProfile(ctx, "partial"); err != nil || got.Backend == nil || *got.Backend != "local" || got.Endpoint != nil {
|
||||
t.Fatalf("profile = %+v, %v", got, err)
|
||||
}
|
||||
if got, err := engine.InspectPrompt(ctx, "prompt"); err != nil || got.Messages[0].Content != "hello" {
|
||||
t.Fatalf("prompt = %+v, %v", got, err)
|
||||
}
|
||||
if got, err := engine.InspectBackend(ctx, "missing"); got != nil || !errors.Is(err, ErrBackendNotFound) || errors.Is(err, ErrInvalidRequest) {
|
||||
t.Fatalf("missing = %+v, %v", got, err)
|
||||
}
|
||||
if _, err := engine.InspectBackend(ctx, "bad/id/extra"); !errors.Is(err, ErrInvalidRequest) {
|
||||
t.Fatalf("invalid ID = %v", err)
|
||||
}
|
||||
var nilEngine *Engine
|
||||
if _, err := nilEngine.InspectBackend(ctx, "local"); !errors.Is(err, ErrInvalidConfig) {
|
||||
t.Fatalf("nil engine = %v", err)
|
||||
}
|
||||
canceled, cancel := context.WithCancel(ctx)
|
||||
cancel()
|
||||
if _, err := engine.InspectBackend(canceled, "local"); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("canceled = %v", err)
|
||||
}
|
||||
if _, err := engine.InspectPrompt(canceled, "prompt"); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("canceled prompt = %v", err)
|
||||
}
|
||||
if _, err := engine.InspectProfile(canceled, "partial"); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("canceled profile = %v", err)
|
||||
}
|
||||
}
|
||||
101
resource_isolation_test.go
Normal file
101
resource_isolation_test.go
Normal file
@@ -0,0 +1,101 @@
|
||||
package promptkit_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"go.maximumdirect.net/configkit"
|
||||
"go.maximumdirect.net/promptkit"
|
||||
)
|
||||
|
||||
func TestResourceInspectionIsolationAndNullInheritance(t *testing.T) {
|
||||
client := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "unexpected"}}
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{Source: configkit.Mapping("resources", map[string]any{
|
||||
"backends": []any{map[string]any{"id": "local", "endpoint": "https://backend.test/v1", "api_key_env": "UNSET_INSPECTION_KEY", "extra_params": map[string]any{"nested": map[string]any{"array": []any{"captured"}}}, "smoke_tests": map[string]any{"default_profile": "missing", "reachability": map[string]any{}}}},
|
||||
"profiles": []any{map[string]any{"id": "parent", "backend": "local", "endpoint": "https://parent.test/v1", "extra_params": map[string]any{"parent": true}, "model": "model"}, map[string]any{"id": "child", "extends": map[string]any{"id": "parent"}, "endpoint": nil, "extra_params": nil, "api_key_env": nil}, map[string]any{"id": "partial"}},
|
||||
"prompts": []any{map[string]any{"id": "reference", "default_profile": "missing", "messages": []any{map[string]any{"role": "user", "content": "{{.missing}}"}}, "output": map[string]any{"format": "json", "validation_mode": "json_schema", "schema_path": "missing.json"}}, map[string]any{"id": "ready", "inputs": []any{map[string]any{"name": "x", "required": false}}, "messages": []any{map[string]any{"role": "user", "content": "hello", "cache_control": map[string]any{"type": "ephemeral", "ttl": ""}}}, "output": map[string]any{"format": "text", "validation_mode": "none"}}},
|
||||
})}}}, promptkit.WithLLMClient(client), promptkit.WithBundledOpenRouter(false))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
if got, err := engine.InspectPrompt(ctx, "reference"); err != nil || got.DefaultProfileID == nil || *got.DefaultProfileID != "missing" || got.OutputContract.SchemaPath == nil || *got.OutputContract.SchemaPath != "missing.json" || got.Messages[0].Content != "{{.missing}}" {
|
||||
t.Fatalf("reference = %+v, %v", got, err)
|
||||
}
|
||||
if got, err := engine.InspectProfile(ctx, "partial"); err != nil || got.Endpoint != nil || got.Model != nil {
|
||||
t.Fatalf("partial = %+v, %v", got, err)
|
||||
}
|
||||
profile, err := engine.InspectProfile(ctx, "child")
|
||||
if err != nil || profile.Endpoint != nil || profile.ExtraParams != nil || profile.APIKeyEnv != nil {
|
||||
t.Fatalf("child = %+v, %v", profile, err)
|
||||
}
|
||||
view, err := engine.InspectConfiguration()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, field := range []string{"endpoint", "extra_params", "api_key_env"} {
|
||||
path := configkit.Keys("profiles").Index(1).Key(field)
|
||||
explanation, ok := view.Explain(path)
|
||||
if !ok || !strings.Contains(explanation.Effective.YAML, "null") || len(explanation.History) == 0 {
|
||||
t.Errorf("%s provenance = %+v, %t", field, explanation, ok)
|
||||
}
|
||||
}
|
||||
backend, err := engine.InspectBackend(ctx, "local")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if backend.SmokeTests == nil || backend.SmokeTests.DefaultProfile == nil || *backend.SmokeTests.DefaultProfile != "missing" {
|
||||
t.Fatalf("diagnostics = %+v", backend)
|
||||
}
|
||||
*backend.APIKeyEnv = "MUTATED"
|
||||
*backend.SmokeTests.DefaultProfile = "mutated"
|
||||
(*backend.ExtraParams)["nested"].(map[string]any)["array"].([]any)[0] = "mutated"
|
||||
*profile.Model = "mutated"
|
||||
prompt, err := engine.InspectPrompt(ctx, "ready")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
(*prompt.Inputs)[0].Name = "changed"
|
||||
*(*prompt.Inputs)[0].Required = true
|
||||
prompt.Messages[0].Content = "changed"
|
||||
*prompt.Messages[0].CacheControl.TTL = "changed"
|
||||
again, err := engine.InspectBackend(ctx, "local")
|
||||
if err != nil || *again.APIKeyEnv != "UNSET_INSPECTION_KEY" || *again.SmokeTests.DefaultProfile != "missing" || (*again.ExtraParams)["nested"].(map[string]any)["array"].([]any)[0] != "captured" {
|
||||
t.Fatalf("backend mutation leaked: %+v, %v", again, err)
|
||||
}
|
||||
againPrompt, err := engine.InspectPrompt(ctx, "ready")
|
||||
if err != nil || (*againPrompt.Inputs)[0].Name != "x" || *(*againPrompt.Inputs)[0].Required || againPrompt.Messages[0].Content != "hello" || *againPrompt.Messages[0].CacheControl.TTL != "" {
|
||||
t.Fatalf("prompt mutation leaked: %+v, %v", againPrompt, err)
|
||||
}
|
||||
prepared, err := engine.Prepare(ctx, promptkit.RunRequest{PromptID: "ready", ProfileID: "child"})
|
||||
if err != nil || prepared.EffectiveModelParams.Endpoint != "https://backend.test/v1" || prepared.EffectiveModelParams.APIKeyEnv != "UNSET_INSPECTION_KEY" || prepared.EffectiveModelParams.Model != "model" || prepared.EffectiveModelParams.ExtraParams["nested"].(map[string]any)["array"].([]any)[0] != "captured" {
|
||||
t.Fatalf("prepared = %+v, %v", prepared, err)
|
||||
}
|
||||
if len(client.requests) != 0 {
|
||||
t.Fatal("inspection invoked provider")
|
||||
}
|
||||
var wg sync.WaitGroup
|
||||
for i := 0; i < 12; i++ {
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
for j := 0; j < 20; j++ {
|
||||
b, err := engine.InspectBackend(ctx, "local")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
(*b.ExtraParams)["nested"].(map[string]any)["array"].([]any)[0] = "local"
|
||||
p, err := engine.InspectPrompt(ctx, "ready")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
p.Messages[0].Content = "local"
|
||||
}
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
}
|
||||
124
resource_serialization_test.go
Normal file
124
resource_serialization_test.go
Normal file
@@ -0,0 +1,124 @@
|
||||
package promptkit_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"go.maximumdirect.net/configkit"
|
||||
"go.maximumdirect.net/promptkit"
|
||||
"go.yaml.in/yaml/v4"
|
||||
)
|
||||
|
||||
func TestResourceInspectionGenericEncoding(t *testing.T) {
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{Source: configkit.Mapping("resources", map[string]any{
|
||||
"backends": []any{map[string]any{"id": "local", "endpoint": "https://example.test/v1", "api_key_env": "BACKEND_KEY", "concurrency_limit": 2, "extra_params": map[string]any{"null": nil, "array": []any{nil}}, "smoke_tests": map[string]any{"reachability": map[string]any{}, "authentication": map[string]any{"kind": "none"}}}, map[string]any{"id": "plain", "endpoint": "https://plain.test/v1"}},
|
||||
"profiles": []any{map[string]any{"id": "parent", "backend": "local", "model": "model"}, map[string]any{"id": "profile", "extends": map[string]any{"id": "parent"}, "temperature": 0, "top_p": 0, "max_tokens": 0, "timeout_seconds": 0, "service_tier": "", "reasoning_effort": "", "api_key_env": "", "api_key_required": false, "endpoint": "", "extra_params": map[string]any{}}, map[string]any{"id": "unset", "extra_params": nil}},
|
||||
"prompts": []any{map[string]any{"id": "prompt", "inputs": []any{map[string]any{"name": "a", "required": false, "description": ""}, map[string]any{"name": "b"}}, "messages": []any{map[string]any{"role": "user", "content": "", "cache_control": map[string]any{"type": "ephemeral", "ttl": ""}}}, "output": map[string]any{"format": "text", "validation_mode": "basic", "repair_attempts": 0}}, map[string]any{"id": "empty-inputs", "inputs": []any{}, "messages": []any{map[string]any{"role": "user", "content": "x"}}, "output": map[string]any{"format": "text", "validation_mode": "none"}}, map[string]any{"id": "omitted-inputs", "messages": []any{map[string]any{"role": "user", "content": "x"}}, "output": map[string]any{"format": "text", "validation_mode": "none"}}},
|
||||
})}}})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ctx := context.Background()
|
||||
backend, err := engine.InspectBackend(ctx, "local")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
plain, err := engine.InspectBackend(ctx, "plain")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
profile, err := engine.InspectProfile(ctx, "profile")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
unset, err := engine.InspectProfile(ctx, "unset")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prompt, err := engine.InspectPrompt(ctx, "prompt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
empty, err := engine.InspectPrompt(ctx, "empty-inputs")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
omitted, err := engine.InspectPrompt(ctx, "omitted-inputs")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for name, codec := range map[string]struct {
|
||||
marshal func(any) ([]byte, error)
|
||||
unmarshal func([]byte, any) error
|
||||
}{
|
||||
"json": {json.Marshal, json.Unmarshal}, "yaml": {yaml.Marshal, yaml.Unmarshal},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
decode := func(v any) map[string]any {
|
||||
t.Helper()
|
||||
data, err := codec.marshal(v)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var m map[string]any
|
||||
if err := codec.unmarshal(data, &m); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return m
|
||||
}
|
||||
absent := func(m map[string]any, keys ...string) {
|
||||
t.Helper()
|
||||
for _, key := range keys {
|
||||
if _, ok := m[key]; ok {
|
||||
t.Errorf("unexpected %s in %#v", key, m)
|
||||
}
|
||||
}
|
||||
}
|
||||
b := decode(backend)
|
||||
if b["id"] != "local" || b["api_key_env"] != "BACKEND_KEY" || fmt.Sprint(b["concurrency_limit"]) != "2" || fmt.Sprint(b["queue_capacity"]) != "1024" {
|
||||
t.Errorf("backend = %#v", b)
|
||||
}
|
||||
params := b["extra_params"].(map[string]any)
|
||||
if v, ok := params["null"]; !ok || v != nil || params["array"].([]any)[0] != nil {
|
||||
t.Errorf("provider nulls = %#v", params)
|
||||
}
|
||||
smoke := b["smoke_tests"].(map[string]any)
|
||||
if smoke["authentication"].(map[string]any)["kind"] != "none" {
|
||||
t.Errorf("authentication = %#v", smoke)
|
||||
}
|
||||
if reach := smoke["reachability"].(map[string]any); len(reach) != 0 {
|
||||
t.Errorf("reachability = %#v", reach)
|
||||
}
|
||||
absent(b, "extends")
|
||||
absent(decode(plain), "smoke_tests", "queue_capacity", "concurrency_limit")
|
||||
p := decode(profile)
|
||||
if p["id"] != "profile" || p["backend"] != "local" || p["model"] != "model" || p["endpoint"] != "" || p["api_key_env"] != "" || p["service_tier"] != "" || p["reasoning_effort"] != "" || p["api_key_required"] != false || fmt.Sprint(p["temperature"]) != "0" || fmt.Sprint(p["top_p"]) != "0" || fmt.Sprint(p["max_tokens"]) != "0" || fmt.Sprint(p["timeout_seconds"]) != "0" || len(p["extra_params"].(map[string]any)) != 0 {
|
||||
t.Errorf("profile = %#v", p)
|
||||
}
|
||||
absent(p, "extends", "effective_model_params")
|
||||
absent(decode(unset), "extra_params", "endpoint")
|
||||
q := decode(prompt)
|
||||
inputs := q["inputs"].([]any)
|
||||
a := inputs[0].(map[string]any)
|
||||
bInput := inputs[1].(map[string]any)
|
||||
if a["required"] != false || a["description"] != "" {
|
||||
t.Errorf("input = %#v", a)
|
||||
}
|
||||
absent(bInput, "required", "description")
|
||||
message := q["messages"].([]any)[0].(map[string]any)
|
||||
if message["content"] != "" || message["cache_control"].(map[string]any)["ttl"] != "" {
|
||||
t.Errorf("message = %#v", message)
|
||||
}
|
||||
absent(message, "content_file", "template")
|
||||
if fmt.Sprint(q["output"].(map[string]any)["repair_attempts"]) != "0" {
|
||||
t.Errorf("output = %#v", q["output"])
|
||||
}
|
||||
if inputs := decode(empty)["inputs"].([]any); len(inputs) != 0 {
|
||||
t.Errorf("empty inputs = %#v", inputs)
|
||||
}
|
||||
absent(decode(omitted), "inputs")
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -422,7 +422,7 @@ func TestSmokeTestUsesPrivateInferenceWithoutApplicationResourcesOrListingChange
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !reflect.DeepEqual(profilesBefore, profilesAfter) || !reflect.DeepEqual(promptsBefore, promptsAfter) ||
|
||||
inspectionBefore.PromptHash != inspectionAfter.PromptHash || inspectionAfter.OutputContract.SchemaPath != schemaMarker {
|
||||
inspectionBefore.PromptHash != inspectionAfter.PromptHash || inspectionAfter.OutputContract.SchemaPath == nil || *inspectionAfter.OutputContract.SchemaPath != schemaMarker {
|
||||
t.Fatalf("diagnostic changed captured listings or application inspection: profiles %v -> %v, prompts %v -> %v, inspection %+v -> %+v",
|
||||
profilesBefore, profilesAfter, promptsBefore, promptsAfter, inspectionBefore, inspectionAfter)
|
||||
}
|
||||
|
||||
@@ -70,7 +70,7 @@ func TestProgrammaticProfileVariantInherits(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
profile, err := engine.InspectProfile(context.Background(), "child")
|
||||
if err != nil || profile.EffectiveModelParams.Model != model || profile.EffectiveModelParams.Endpoint != endpoint || profile.EffectiveModelParams.TimeoutSeconds != timeout {
|
||||
if err != nil || profile.Model == nil || *profile.Model != model || profile.Endpoint == nil || *profile.Endpoint != endpoint || profile.TimeoutSeconds == nil || *profile.TimeoutSeconds != timeout {
|
||||
t.Fatalf("inherited profile = %+v, %v", profile, err)
|
||||
}
|
||||
}
|
||||
|
||||
41
types.go
41
types.go
@@ -356,27 +356,6 @@ type ExecutionTarget struct {
|
||||
ExtraParams map[string]any `json:"extra_params"`
|
||||
}
|
||||
|
||||
// ProfileInspection is the caller-owned result of [Engine.InspectProfile].
|
||||
// It has no stable JSON representation.
|
||||
//
|
||||
// EffectiveModelParams contains a copied effective target. APIKeyRequired is
|
||||
// separate from that target to preserve ExecutionTarget's general execution
|
||||
// and stable JSON contracts.
|
||||
type ProfileInspection struct {
|
||||
// ProfileID is the exact profile ID inspected by the engine.
|
||||
ProfileID string
|
||||
// EffectiveModelParams contains settings resolved from the framework timeout
|
||||
// baseline, selected backend, and then profile, without a request override.
|
||||
// Unset optional provider controls remain zero rather than reporting a
|
||||
// provider default. APIKeyEnv is an environment-variable name, never its
|
||||
// credential value.
|
||||
EffectiveModelParams ExecutionTarget
|
||||
// APIKeyRequired reports that a later execution must supply a usable direct
|
||||
// key or environment credential. An explicit profile environment source can
|
||||
// coexist with this requirement.
|
||||
APIKeyRequired bool
|
||||
}
|
||||
|
||||
// PromptInputDefinition describes one declared prompt input.
|
||||
// It has no stable JSON representation.
|
||||
type PromptInputDefinition struct {
|
||||
@@ -392,26 +371,6 @@ type PromptInputDefinition struct {
|
||||
Description string
|
||||
}
|
||||
|
||||
// PromptInspection is the caller-owned result of [Engine.InspectPrompt].
|
||||
// It has no stable JSON representation.
|
||||
//
|
||||
// Inputs contains copied declared input metadata in definition order.
|
||||
// OutputContract is the normalized contract declared by the prompt definition,
|
||||
// rather than a request-level effective override. PromptHash is opaque.
|
||||
type PromptInspection struct {
|
||||
// PromptID is the exact selected prompt ID after any namespace-view
|
||||
// qualification.
|
||||
PromptID string
|
||||
// PromptHash is the opaque equality value for the selected definition.
|
||||
PromptHash string
|
||||
// DefaultProfileID is declared metadata and is not resolved by inspection.
|
||||
DefaultProfileID string
|
||||
// Inputs contains caller-owned declared input metadata in definition order.
|
||||
Inputs []PromptInputDefinition
|
||||
// OutputContract is the normalized contract declared by the definition.
|
||||
OutputContract OutputContract
|
||||
}
|
||||
|
||||
// ExecutionTargetOverride represents per-request runtime setting overrides and
|
||||
// has no stable JSON representation.
|
||||
//
|
||||
|
||||
Reference in New Issue
Block a user