Store one prompt definition per ID
This commit is contained in:
@@ -100,11 +100,12 @@ func newAssembledWorkflowFixture(b *testing.B, name string) assembledWorkflowFix
|
|||||||
|
|
||||||
config := map[string]any{
|
config := map[string]any{
|
||||||
"profiles": map[string]any{"profile": profile},
|
"profiles": map[string]any{"profile": profile},
|
||||||
"prompts": map[string]any{"workflow": map[string]any{"v1": map[string]any{
|
"prompts": map[string]any{"workflow": map[string]any{
|
||||||
|
"version": "v1",
|
||||||
"default_profile": "profile",
|
"default_profile": "profile",
|
||||||
"messages": []any{map[string]any{"role": "user", "content": message}},
|
"messages": []any{map[string]any{"role": "user", "content": message}},
|
||||||
"output": output,
|
"output": output,
|
||||||
}}},
|
}},
|
||||||
}
|
}
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{
|
||||||
Source: configkit.Mapping("assembled workflow benchmark", config),
|
Source: configkit.Mapping("assembled workflow benchmark", config),
|
||||||
|
|||||||
@@ -267,7 +267,7 @@ func newBackendCapacityEngine(
|
|||||||
Source: configkit.Mapping("capacity fixture", map[string]any{
|
Source: configkit.Mapping("capacity fixture", map[string]any{
|
||||||
"backends": map[string]any{"limited": backend},
|
"backends": map[string]any{"limited": backend},
|
||||||
"profiles": map[string]any{"profile": map[string]any{"backend": "limited", "model": "model"}},
|
"profiles": map[string]any{"profile": map[string]any{"backend": "limited", "model": "model"}},
|
||||||
"prompts": map[string]any{promptID: map[string]any{"1": definition}},
|
"prompts": map[string]any{promptID: promptDefinitionWithVersion("1", definition)},
|
||||||
}),
|
}),
|
||||||
}}}, options...)
|
}}}, options...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -199,7 +199,7 @@ func TestConfigurationInspectionUsesCapturedSourcesWithoutProviderCalls(t *testi
|
|||||||
t.Fatalf("captured difference changed between reads: first=%+v next=%+v", expected, differences[0])
|
t.Fatalf("captured difference changed between reads: first=%+v next=%+v", expected, differences[0])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, found := view.Explain(configkit.Keys("prompts", "captured", "1", "messages").Index(0).Key("content_file")); !found {
|
if _, found := view.Explain(configkit.Keys("prompts", "captured", "messages").Index(0).Key("content_file")); !found {
|
||||||
t.Fatal("captured prompt content path was not inspectable")
|
t.Fatal("captured prompt content path was not inspectable")
|
||||||
}
|
}
|
||||||
var report bytes.Buffer
|
var report bytes.Buffer
|
||||||
|
|||||||
@@ -73,11 +73,12 @@ func TestProfileEnvironmentLookupAcceptsNonShellNames(t *testing.T) {
|
|||||||
func TestPublicConstructionWithLocalBackendAndProgrammaticProfile(t *testing.T) {
|
func TestPublicConstructionWithLocalBackendAndProgrammaticProfile(t *testing.T) {
|
||||||
backendID, model := "workstation", "offline-model"
|
backendID, model := "workstation", "offline-model"
|
||||||
prompt := configkit.Input{Source: configkit.Mapping("prompt", map[string]any{
|
prompt := configkit.Input{Source: configkit.Mapping("prompt", map[string]any{
|
||||||
"prompts": map[string]any{"hello": map[string]any{"1": map[string]any{
|
"prompts": map[string]any{"hello": map[string]any{
|
||||||
|
"version": "1",
|
||||||
"default_profile": "offline",
|
"default_profile": "offline",
|
||||||
"messages": []any{map[string]any{"role": "user", "content": "Hello"}},
|
"messages": []any{map[string]any{"role": "user", "content": "Hello"}},
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||||
}}},
|
}},
|
||||||
})}
|
})}
|
||||||
client := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "reply"}}
|
client := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: "reply"}}
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
@@ -115,14 +116,14 @@ profiles:
|
|||||||
model: default-model
|
model: default-model
|
||||||
prompts:
|
prompts:
|
||||||
hello:
|
hello:
|
||||||
"1":
|
version: "1"
|
||||||
default_profile: application
|
default_profile: application
|
||||||
messages:
|
messages:
|
||||||
- role: user
|
- role: user
|
||||||
content: Hello
|
content: Hello
|
||||||
output:
|
output:
|
||||||
format: text
|
format: text
|
||||||
validation_mode: none
|
validation_mode: none
|
||||||
`)},
|
`)},
|
||||||
}, "config")
|
}, "config")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -269,24 +270,10 @@ func TestBackendSourceOwnsSmokeTestValuesAndDefersDefaultLookup(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPreparationRejectsAmbiguousPromptVersion(t *testing.T) {
|
func TestPreparationSelectsSinglePromptDefinitionByID(t *testing.T) {
|
||||||
definition := func(content string) map[string]any {
|
engine := newSimpleEngine(t, "selected")
|
||||||
return map[string]any{
|
prepared, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: "prompt"})
|
||||||
"messages": []any{map[string]any{"role": "user", "content": content}},
|
if err != nil || prepared == nil || len(prepared.Messages) != 1 || prepared.Messages[0].Content != "selected" {
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
t.Fatalf("prepare prompt by ID = (%+v, %v)", prepared, err)
|
||||||
}
|
|
||||||
}
|
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
|
||||||
Main: &configkit.Input{Source: configkit.Mapping("versions", map[string]any{
|
|
||||||
"prompts": map[string]any{"hello": map[string]any{
|
|
||||||
"1": definition("first"), "2": definition("second"),
|
|
||||||
}},
|
|
||||||
})},
|
|
||||||
}})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("construct engine: %v", err)
|
|
||||||
}
|
|
||||||
if result, err := engine.Prepare(context.Background(), promptkit.RunRequest{PromptID: "hello"}); result != nil || !errors.Is(err, promptkit.ErrPromptLoad) {
|
|
||||||
t.Fatalf("ambiguous preparation = (%#v, %v), want ErrPromptLoad", result, err)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,11 +63,12 @@ func simpleConfig(content string) promptkit.Config {
|
|||||||
"profiles": map[string]any{"profile": map[string]any{
|
"profiles": map[string]any{"profile": map[string]any{
|
||||||
"endpoint": "http://example.test/v1", "model": "model",
|
"endpoint": "http://example.test/v1", "model": "model",
|
||||||
}},
|
}},
|
||||||
"prompts": map[string]any{"prompt": map[string]any{"1": map[string]any{
|
"prompts": map[string]any{"prompt": map[string]any{
|
||||||
|
"version": "1",
|
||||||
"default_profile": "profile",
|
"default_profile": "profile",
|
||||||
"messages": []any{map[string]any{"role": "user", "content": content}},
|
"messages": []any{map[string]any{"role": "user", "content": content}},
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||||
}}},
|
}},
|
||||||
})},
|
})},
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,10 @@ This reference describes what Promptkit does with the final effective values.
|
|||||||
|
|
||||||
A configuration document has keyed top-level `backends`, `profiles`, and
|
A configuration document has keyed top-level `backends`, `profiles`, and
|
||||||
`prompts` mappings. Their keys are literal IDs, not filename-derived identities.
|
`prompts` mappings. Their keys are literal IDs, not filename-derived identities.
|
||||||
Prompt versions are a second keyed mapping. Keys and references must be
|
Each prompt ID maps directly to one definition; an optional `version` string is
|
||||||
nonblank and remain exact: Promptkit does not trim or re-key them. Old embedded
|
descriptive metadata, not a selection key. Keys and references must be nonblank
|
||||||
`id`, `version`, and `base_profile` fields are invalid. Unknown fields are
|
and remain exact: Promptkit does not trim or re-key them. Old embedded `id` and
|
||||||
errors.
|
`base_profile` fields are invalid. Unknown fields are errors.
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
backends:
|
backends:
|
||||||
@@ -28,14 +28,14 @@ profiles:
|
|||||||
model: example-model
|
model: example-model
|
||||||
prompts:
|
prompts:
|
||||||
meeting.summary:
|
meeting.summary:
|
||||||
"1.0.0":
|
version: "1.0.0"
|
||||||
default_profile: local-summary
|
default_profile: local-summary
|
||||||
messages:
|
messages:
|
||||||
- role: user
|
- role: user
|
||||||
content: Summarize this note.
|
content: Summarize this note.
|
||||||
output:
|
output:
|
||||||
format: text
|
format: text
|
||||||
validation_mode: none
|
validation_mode: none
|
||||||
```
|
```
|
||||||
|
|
||||||
Every effective backend, profile, and prompt definition is validated when
|
Every effective backend, profile, and prompt definition is validated when
|
||||||
@@ -56,32 +56,32 @@ see the consumer guide's [profile and prompt listing](consumers/pkg-promptkit.md
|
|||||||
|
|
||||||
## Prompt Definitions
|
## Prompt Definitions
|
||||||
|
|
||||||
The `prompts` mapping selects a literal prompt ID, then a string version key:
|
The `prompts` mapping selects a literal prompt ID whose value is the definition:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
prompts:
|
prompts:
|
||||||
meeting.summary:
|
meeting.summary:
|
||||||
"1.0.0":
|
version: "1.0.0"
|
||||||
default_profile: local-summary
|
default_profile: local-summary
|
||||||
description: Summarize a synthetic meeting note.
|
description: Summarize a synthetic meeting note.
|
||||||
session_id: '{{.session}}'
|
session_id: '{{.session}}'
|
||||||
inputs:
|
inputs:
|
||||||
- name: note
|
- name: note
|
||||||
required: true
|
required: true
|
||||||
content_type: text/plain
|
content_type: text/plain
|
||||||
description: Meeting note to summarize.
|
description: Meeting note to summarize.
|
||||||
messages:
|
messages:
|
||||||
- role: system
|
- role: system
|
||||||
content: Return a concise summary.
|
content: Return a concise summary.
|
||||||
cache_control:
|
cache_control:
|
||||||
type: ephemeral
|
type: ephemeral
|
||||||
ttl: 1h
|
ttl: 1h
|
||||||
- role: user
|
- role: user
|
||||||
content_file: ./summary.user.md
|
content_file: ./summary.user.md
|
||||||
output:
|
output:
|
||||||
format: markdown
|
format: markdown
|
||||||
validation_mode: basic
|
validation_mode: basic
|
||||||
repair_attempts: 0
|
repair_attempts: 0
|
||||||
```
|
```
|
||||||
|
|
||||||
`messages` must be nonempty and `output` must be a valid mapping. Optional
|
`messages` must be nonempty and `output` must be a valid mapping. Optional
|
||||||
@@ -89,9 +89,8 @@ prompts:
|
|||||||
fields may be absent or cleared with null where appropriate. A null whole
|
fields may be absent or cleared with null where appropriate. A null whole
|
||||||
definition, required role, output format, or validation mode is invalid.
|
definition, required role, output format, or validation mode is invalid.
|
||||||
`required` and `repair_attempts` default to false and zero, respectively, and
|
`required` and `repair_attempts` default to false and zero, respectively, and
|
||||||
are not nullable. A request that omits a prompt version succeeds only if that
|
are not nullable. `InspectPrompt` selects the same captured definition without
|
||||||
prompt ID has exactly one effective version. `InspectPrompt` selects from the
|
rendering or resolving a profile.
|
||||||
same captured definitions without rendering or resolving a profile.
|
|
||||||
|
|
||||||
### Inputs
|
### Inputs
|
||||||
|
|
||||||
|
|||||||
@@ -390,6 +390,13 @@ Validation: `GOWORK=off go test ./...`, including consumers of migrated fixtures
|
|||||||
Test ID-only selection, source-relative content, and inherited/overlaid prompt
|
Test ID-only selection, source-relative content, and inherited/overlaid prompt
|
||||||
settings. Do not add tests preserving temporary obsolete selector parameters.
|
settings. Do not add tests preserving temporary obsolete selector parameters.
|
||||||
|
|
||||||
|
Completion: prompts now map directly from each ID to one definition, with an
|
||||||
|
optional descriptive version label. Configuration lookup and message/input
|
||||||
|
metadata paths are ID-only; ambiguity handling and version-keyed indexes are
|
||||||
|
removed. Fixtures, examples, and the canonical format reference use the direct
|
||||||
|
shape, including layered overlays. Validation passed: `GOWORK=off go test ./...`,
|
||||||
|
`GOWORK=off python3 scripts/check-examples.py`, and `git diff --check`.
|
||||||
|
|
||||||
## Stage 5: Remove Request-Side Version Selection
|
## Stage 5: Remove Request-Side Version Selection
|
||||||
|
|
||||||
Scope: compile-directed removal of obsolete selector plumbing; no new lookup rules.
|
Scope: compile-directed removal of obsolete selector plumbing; no new lookup rules.
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
prompts:
|
prompts:
|
||||||
example.assessment:
|
example.assessment:
|
||||||
"1.0.0":
|
version: "1.0.0"
|
||||||
default_profile: assessment-profile
|
default_profile: assessment-profile
|
||||||
description: Assess a structured proposal independently of schema validity.
|
description: Assess a structured proposal independently of schema validity.
|
||||||
inputs:
|
inputs:
|
||||||
- name: proposal
|
- name: proposal
|
||||||
required: true
|
required: true
|
||||||
content_type: application/json
|
content_type: application/json
|
||||||
messages:
|
messages:
|
||||||
- role: system
|
- role: system
|
||||||
content: Return a concise assessment as JSON.
|
content: Return a concise assessment as JSON.
|
||||||
- role: user
|
- role: user
|
||||||
content: '{{input "proposal"}}'
|
content: '{{input "proposal"}}'
|
||||||
output:
|
output:
|
||||||
format: json
|
format: json
|
||||||
validation_mode: json_schema
|
validation_mode: json_schema
|
||||||
schema_path: assessment.schema.json
|
schema_path: assessment.schema.json
|
||||||
|
|||||||
@@ -1,18 +1,18 @@
|
|||||||
prompts:
|
prompts:
|
||||||
example.proposal:
|
example.proposal:
|
||||||
"1.0.0":
|
version: "1.0.0"
|
||||||
default_profile: proposal-profile
|
default_profile: proposal-profile
|
||||||
description: Propose a bounded improvement from a synthetic idea.
|
description: Propose a bounded improvement from a synthetic idea.
|
||||||
inputs:
|
inputs:
|
||||||
- name: idea
|
- name: idea
|
||||||
required: true
|
required: true
|
||||||
content_type: text/plain
|
content_type: text/plain
|
||||||
messages:
|
messages:
|
||||||
- role: system
|
- role: system
|
||||||
content: Return a concise proposal as JSON.
|
content: Return a concise proposal as JSON.
|
||||||
- role: user
|
- role: user
|
||||||
content: '{{input "idea"}}'
|
content: '{{input "idea"}}'
|
||||||
output:
|
output:
|
||||||
format: json
|
format: json
|
||||||
validation_mode: json_schema
|
validation_mode: json_schema
|
||||||
schema_path: proposal.schema.json
|
schema_path: proposal.schema.json
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
prompts:
|
prompts:
|
||||||
example.prepare:
|
example.prepare:
|
||||||
"1.0.0":
|
version: "1.0.0"
|
||||||
default_profile: offline-example
|
default_profile: offline-example
|
||||||
description: Prepare a prompt without contacting a model provider.
|
description: Prepare a prompt without contacting a model provider.
|
||||||
inputs:
|
inputs:
|
||||||
- name: note
|
- name: note
|
||||||
required: true
|
required: true
|
||||||
content_type: text/plain
|
content_type: text/plain
|
||||||
messages:
|
messages:
|
||||||
- role: system
|
- role: system
|
||||||
content: Summarize the note in one sentence.
|
content: Summarize the note in one sentence.
|
||||||
- role: user
|
- role: user
|
||||||
content: '{{input "note"}}'
|
content: '{{input "note"}}'
|
||||||
output:
|
output:
|
||||||
format: text
|
format: text
|
||||||
validation_mode: basic
|
validation_mode: basic
|
||||||
|
|||||||
@@ -1,17 +1,17 @@
|
|||||||
prompts:
|
prompts:
|
||||||
example.run:
|
example.run:
|
||||||
"1.0.0":
|
version: "1.0.0"
|
||||||
default_profile: offline-example
|
default_profile: offline-example
|
||||||
description: Run a prompt with a deterministic injected model client.
|
description: Run a prompt with a deterministic injected model client.
|
||||||
inputs:
|
inputs:
|
||||||
- name: note
|
- name: note
|
||||||
required: true
|
required: true
|
||||||
content_type: text/plain
|
content_type: text/plain
|
||||||
messages:
|
messages:
|
||||||
- role: system
|
- role: system
|
||||||
content: Summarize the note in one sentence.
|
content: Summarize the note in one sentence.
|
||||||
- role: user
|
- role: user
|
||||||
content: '{{input "note"}}'
|
content: '{{input "note"}}'
|
||||||
output:
|
output:
|
||||||
format: text
|
format: text
|
||||||
validation_mode: basic
|
validation_mode: basic
|
||||||
|
|||||||
@@ -19,15 +19,15 @@ import (
|
|||||||
const capturedPromptDocument = `
|
const capturedPromptDocument = `
|
||||||
prompts:
|
prompts:
|
||||||
captured:
|
captured:
|
||||||
"1":
|
version: "1"
|
||||||
default_profile: offline
|
default_profile: offline
|
||||||
session_id: '{{.session}}'
|
session_id: '{{.session}}'
|
||||||
messages:
|
messages:
|
||||||
- role: user
|
- role: user
|
||||||
content_file: body.tmpl
|
content_file: body.tmpl
|
||||||
output:
|
output:
|
||||||
format: text
|
format: text
|
||||||
validation_mode: none
|
validation_mode: none
|
||||||
`
|
`
|
||||||
|
|
||||||
func capturedPromptEngine(t *testing.T, source fs.FS) *promptkit.Engine {
|
func capturedPromptEngine(t *testing.T, source fs.FS) *promptkit.Engine {
|
||||||
@@ -149,12 +149,13 @@ func TestPreparationStillReadsRequestArtifactsAndSchemasAfterConstruction(t *tes
|
|||||||
"offline": {Endpoint: &endpoint, Model: &model},
|
"offline": {Endpoint: &endpoint, Model: &model},
|
||||||
})}}}},
|
})}}}},
|
||||||
Main: &configkit.Input{Source: configkit.Mapping("structured prompt", map[string]any{
|
Main: &configkit.Input{Source: configkit.Mapping("structured prompt", map[string]any{
|
||||||
"prompts": map[string]any{"structured": map[string]any{"1": map[string]any{
|
"prompts": map[string]any{"structured": map[string]any{
|
||||||
|
"version": "1",
|
||||||
"default_profile": "offline",
|
"default_profile": "offline",
|
||||||
"inputs": []any{map[string]any{"name": "note", "required": true}},
|
"inputs": []any{map[string]any{"name": "note", "required": true}},
|
||||||
"messages": []any{map[string]any{"role": "user", "content": `{{input "note"}}`}},
|
"messages": []any{map[string]any{"role": "user", "content": `{{input "note"}}`}},
|
||||||
"output": map[string]any{"format": "json", "validation_mode": "json_schema", "schema_path": "schema.json"},
|
"output": map[string]any{"format": "json", "validation_mode": "json_schema", "schema_path": "schema.json"},
|
||||||
}}},
|
}},
|
||||||
})},
|
})},
|
||||||
}}, promptkit.WithSchemaFS(schemas, "."))
|
}}, promptkit.WithSchemaFS(schemas, "."))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ type configurationShape struct {
|
|||||||
|
|
||||||
func benchmarkPrompt(profileID string) map[string]any {
|
func benchmarkPrompt(profileID string) map[string]any {
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
|
"version": "1",
|
||||||
"default_profile": profileID,
|
"default_profile": profileID,
|
||||||
"messages": []any{map[string]any{"role": "user", "content": "Hello {{.name}}"}},
|
"messages": []any{map[string]any{"role": "user", "content": "Hello {{.name}}"}},
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||||
@@ -44,7 +45,7 @@ func independentDefinitions(count int) configurationShape {
|
|||||||
profileID := fmt.Sprintf("profile-%03d", index)
|
profileID := fmt.Sprintf("profile-%03d", index)
|
||||||
promptID := fmt.Sprintf("prompt-%03d", index)
|
promptID := fmt.Sprintf("prompt-%03d", index)
|
||||||
profiles[profileID] = map[string]any{"endpoint": "https://example.test/v1", "model": profileID}
|
profiles[profileID] = map[string]any{"endpoint": "https://example.test/v1", "model": profileID}
|
||||||
prompts[promptID] = map[string]any{"1": benchmarkPrompt(profileID)}
|
prompts[promptID] = benchmarkPrompt(profileID)
|
||||||
}
|
}
|
||||||
return configurationShape{
|
return configurationShape{
|
||||||
main: configkit.Input{Source: configkit.Mapping("independent definitions", map[string]any{"profiles": profiles, "prompts": prompts})},
|
main: configkit.Input{Source: configkit.Mapping("independent definitions", map[string]any{"profiles": profiles, "prompts": prompts})},
|
||||||
@@ -63,7 +64,7 @@ func fileBackedDefinitions(count int) configurationShape {
|
|||||||
profiles[profileID] = map[string]any{"endpoint": "https://example.test/v1", "model": profileID}
|
profiles[profileID] = map[string]any{"endpoint": "https://example.test/v1", "model": profileID}
|
||||||
prompt := benchmarkPrompt(profileID)
|
prompt := benchmarkPrompt(profileID)
|
||||||
prompt["messages"] = []any{map[string]any{"role": "user", "content_file": "body.md"}}
|
prompt["messages"] = []any{map[string]any{"role": "user", "content_file": "body.md"}}
|
||||||
prompts[promptID] = map[string]any{"1": prompt}
|
prompts[promptID] = prompt
|
||||||
}
|
}
|
||||||
namespace := configkit.NewFilesystem("benchmark content", fstest.MapFS{
|
namespace := configkit.NewFilesystem("benchmark content", fstest.MapFS{
|
||||||
"assets/body.md": &fstest.MapFile{Data: []byte("Hello {{.name}}")},
|
"assets/body.md": &fstest.MapFile{Data: []byte("Hello {{.name}}")},
|
||||||
@@ -89,7 +90,7 @@ func sharedBaseDefinitions(count int) configurationShape {
|
|||||||
return configurationShape{
|
return configurationShape{
|
||||||
main: configkit.Input{Source: configkit.Mapping("inherited definitions", map[string]any{
|
main: configkit.Input{Source: configkit.Mapping("inherited definitions", map[string]any{
|
||||||
"profiles": profiles,
|
"profiles": profiles,
|
||||||
"prompts": map[string]any{"shared": map[string]any{"1": benchmarkPrompt(fmt.Sprintf("child-%03d", count-1))}},
|
"prompts": map[string]any{"shared": benchmarkPrompt(fmt.Sprintf("child-%03d", count-1))},
|
||||||
})},
|
})},
|
||||||
inputEntries: count + 2,
|
inputEntries: count + 2,
|
||||||
effectiveEntries: count + 2,
|
effectiveEntries: count + 2,
|
||||||
@@ -117,7 +118,7 @@ func layeredDefinitions(count int) configurationShape {
|
|||||||
},
|
},
|
||||||
main: configkit.Input{Source: configkit.Mapping("main", map[string]any{
|
main: configkit.Input{Source: configkit.Mapping("main", map[string]any{
|
||||||
"profiles": final,
|
"profiles": final,
|
||||||
"prompts": map[string]any{"layered": map[string]any{"1": benchmarkPrompt(fmt.Sprintf("profile-%03d", count-1))}},
|
"prompts": map[string]any{"layered": benchmarkPrompt(fmt.Sprintf("profile-%03d", count-1))},
|
||||||
})},
|
})},
|
||||||
inputEntries: count*4 + 1,
|
inputEntries: count*4 + 1,
|
||||||
effectiveEntries: count + 1,
|
effectiveEntries: count + 1,
|
||||||
@@ -160,7 +161,7 @@ func BenchmarkConfigurationSelection(b *testing.B) {
|
|||||||
b.Run("prompt", func(b *testing.B) {
|
b.Run("prompt", func(b *testing.B) {
|
||||||
b.ReportAllocs()
|
b.ReportAllocs()
|
||||||
for b.Loop() {
|
for b.Loop() {
|
||||||
definition, _, err := result.prompt("prompt-127", "1")
|
definition, _, err := result.prompt("prompt-127")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
b.Fatal(err)
|
b.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -201,7 +202,7 @@ func inspectionDefinitions(extraParams map[string]any) configurationShape {
|
|||||||
prompt := benchmarkPrompt("small")
|
prompt := benchmarkPrompt("small")
|
||||||
prompt["inputs"] = []any{map[string]any{"name": "transcript", "required": true}}
|
prompt["inputs"] = []any{map[string]any{"name": "transcript", "required": true}}
|
||||||
prompts := map[string]any{
|
prompts := map[string]any{
|
||||||
"inspection": map[string]any{"1": prompt},
|
"inspection": prompt,
|
||||||
}
|
}
|
||||||
return configurationShape{
|
return configurationShape{
|
||||||
main: configkit.Input{Source: configkit.Mapping("inspection definitions", map[string]any{
|
main: configkit.Input{Source: configkit.Mapping("inspection definitions", map[string]any{
|
||||||
|
|||||||
@@ -22,12 +22,9 @@ func TestLoadedListsSortedEffectiveIdentities(t *testing.T) {
|
|||||||
" z/profile ": map[string]any{"backend": "local", "model": "spaced-model"},
|
" z/profile ": map[string]any{"backend": "local", "model": "spaced-model"},
|
||||||
},
|
},
|
||||||
"prompts": map[string]any{
|
"prompts": map[string]any{
|
||||||
"z/prompt": map[string]any{"v1": listingPromptDefinition()},
|
"z/prompt": listingPromptDefinition(),
|
||||||
"a/prompt": map[string]any{"v1": listingPromptDefinition()},
|
"a/prompt": listingPromptDefinition(),
|
||||||
" z/prompt ": map[string]any{
|
" z/prompt ": listingPromptDefinition(),
|
||||||
"2": listingPromptDefinition(),
|
|
||||||
"10": listingPromptDefinition(),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}), nil)
|
}), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -48,8 +45,7 @@ func TestLoadedListsSortedEffectiveIdentities(t *testing.T) {
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
wantPrompts := []domain.PromptIdentity{
|
wantPrompts := []domain.PromptIdentity{
|
||||||
{ID: " z/prompt ", Version: "10"},
|
{ID: " z/prompt ", Version: "v1"},
|
||||||
{ID: " z/prompt ", Version: "2"},
|
|
||||||
{ID: "a/prompt", Version: "v1"},
|
{ID: "a/prompt", Version: "v1"},
|
||||||
{ID: "z/prompt", Version: "v1"},
|
{ID: "z/prompt", Version: "v1"},
|
||||||
}
|
}
|
||||||
@@ -61,7 +57,7 @@ func TestLoadedListsSortedEffectiveIdentities(t *testing.T) {
|
|||||||
func TestLoadedListsAreIndependent(t *testing.T) {
|
func TestLoadedListsAreIndependent(t *testing.T) {
|
||||||
state, err := Load(mapping("identities", map[string]any{
|
state, err := Load(mapping("identities", map[string]any{
|
||||||
"profiles": map[string]any{"profile": map[string]any{}},
|
"profiles": map[string]any{"profile": map[string]any{}},
|
||||||
"prompts": map[string]any{"example": map[string]any{"v1": listingPromptDefinition()}},
|
"prompts": map[string]any{"example": listingPromptDefinition()},
|
||||||
}), nil)
|
}), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -116,7 +112,7 @@ func TestLoadedListsEmptyInventory(t *testing.T) {
|
|||||||
func TestLoadedListsHonorCanceledContext(t *testing.T) {
|
func TestLoadedListsHonorCanceledContext(t *testing.T) {
|
||||||
state, err := Load(mapping("identities", map[string]any{
|
state, err := Load(mapping("identities", map[string]any{
|
||||||
"profiles": map[string]any{"partial": map[string]any{}},
|
"profiles": map[string]any{"partial": map[string]any{}},
|
||||||
"prompts": map[string]any{"example": map[string]any{"v1": listingPromptDefinition()}},
|
"prompts": map[string]any{"example": listingPromptDefinition()},
|
||||||
}), nil)
|
}), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -136,6 +132,7 @@ func TestLoadedListsHonorCanceledContext(t *testing.T) {
|
|||||||
|
|
||||||
func listingPromptDefinition() map[string]any {
|
func listingPromptDefinition() map[string]any {
|
||||||
return map[string]any{
|
return map[string]any{
|
||||||
|
"version": "v1",
|
||||||
"messages": []any{map[string]any{"role": "user", "content": "Hello."}},
|
"messages": []any{map[string]any{"role": "user", "content": "Hello."}},
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "basic"},
|
"output": map[string]any{"format": "text", "validation_mode": "basic"},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,9 +17,9 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type document struct {
|
type document struct {
|
||||||
Backends map[string]backendConfig `yaml:"backends"`
|
Backends map[string]backendConfig `yaml:"backends"`
|
||||||
Profiles map[string]profileConfig `yaml:"profiles"`
|
Profiles map[string]profileConfig `yaml:"profiles"`
|
||||||
Prompts map[string]map[string]promptConfig `yaml:"prompts"`
|
Prompts map[string]promptConfig `yaml:"prompts"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type backendConfig struct {
|
type backendConfig struct {
|
||||||
@@ -62,6 +62,7 @@ type profileConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type promptConfig struct {
|
type promptConfig struct {
|
||||||
|
Version *string `yaml:"version"`
|
||||||
DefaultProfile *string `yaml:"default_profile"`
|
DefaultProfile *string `yaml:"default_profile"`
|
||||||
Description *string `yaml:"description"`
|
Description *string `yaml:"description"`
|
||||||
SessionID *string `yaml:"session_id"`
|
SessionID *string `yaml:"session_id"`
|
||||||
@@ -110,9 +111,8 @@ type loaded struct {
|
|||||||
result *configkit.Result[document]
|
result *configkit.Result[document]
|
||||||
backends map[string]map[string]fieldState
|
backends map[string]map[string]fieldState
|
||||||
profiles map[string]map[string]fieldState
|
profiles map[string]map[string]fieldState
|
||||||
promptMetadata map[string]map[string][]promptInputPresence
|
promptMetadata map[string][]promptInputPresence
|
||||||
prompts map[string]map[string]*promptEntry
|
prompts map[string]*promptEntry
|
||||||
ambiguous map[string]bool
|
|
||||||
registry *backend.Registry
|
registry *backend.Registry
|
||||||
indexedProfiles map[string]domain.ExecutionProfile
|
indexedProfiles map[string]domain.ExecutionProfile
|
||||||
profileIDs []string
|
profileIDs []string
|
||||||
@@ -149,7 +149,7 @@ func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error)
|
|||||||
state := &loaded{
|
state := &loaded{
|
||||||
backends: make(map[string]map[string]fieldState),
|
backends: make(map[string]map[string]fieldState),
|
||||||
profiles: make(map[string]map[string]fieldState),
|
profiles: make(map[string]map[string]fieldState),
|
||||||
promptMetadata: make(map[string]map[string][]promptInputPresence),
|
promptMetadata: make(map[string][]promptInputPresence),
|
||||||
}
|
}
|
||||||
result, err := configkit.LoadLayered(main, configkit.LayeredOptions[document]{
|
result, err := configkit.LoadLayered(main, configkit.LayeredOptions[document]{
|
||||||
Underlays: underlays,
|
Underlays: underlays,
|
||||||
@@ -181,9 +181,7 @@ func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error)
|
|||||||
}
|
}
|
||||||
state.profileIDs = sortedKeys(state.indexedProfiles)
|
state.profileIDs = sortedKeys(state.indexedProfiles)
|
||||||
for _, id := range sortedKeys(state.prompts) {
|
for _, id := range sortedKeys(state.prompts) {
|
||||||
for _, version := range sortedKeys(state.prompts[id]) {
|
state.promptIDs = append(state.promptIDs, domain.PromptIdentity{ID: id, Version: state.prompts[id].definition.Version})
|
||||||
state.promptIDs = append(state.promptIDs, domain.PromptIdentity{ID: id, Version: version})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return state, nil
|
return state, nil
|
||||||
}
|
}
|
||||||
@@ -193,24 +191,19 @@ type promptInputPresence struct {
|
|||||||
description fieldState
|
description fieldState
|
||||||
}
|
}
|
||||||
|
|
||||||
func capturePromptMetadata(prompts map[string]map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution, destination map[string]map[string][]promptInputPresence) {
|
func capturePromptMetadata(prompts map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution, destination map[string][]promptInputPresence) {
|
||||||
for _, id := range sortedKeys(prompts) {
|
for _, id := range sortedKeys(prompts) {
|
||||||
if destination[id] == nil {
|
inputs := prompts[id].Inputs
|
||||||
destination[id] = make(map[string][]promptInputPresence, len(prompts[id]))
|
states := make([]promptInputPresence, len(inputs))
|
||||||
}
|
base := basePath.Key(id).Key("inputs")
|
||||||
for _, version := range sortedKeys(prompts[id]) {
|
for index := range inputs {
|
||||||
inputs := prompts[id][version].Inputs
|
path := base.Index(index)
|
||||||
states := make([]promptInputPresence, len(inputs))
|
states[index] = promptInputPresence{
|
||||||
base := basePath.Key(id).Key(version).Key("inputs")
|
contentType: captureFieldState(path.Key("content_type"), resolution),
|
||||||
for index := range inputs {
|
description: captureFieldState(path.Key("description"), resolution),
|
||||||
path := base.Index(index)
|
|
||||||
states[index] = promptInputPresence{
|
|
||||||
contentType: captureFieldState(path.Key("content_type"), resolution),
|
|
||||||
description: captureFieldState(path.Key("description"), resolution),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
destination[id][version] = states
|
|
||||||
}
|
}
|
||||||
|
destination[id] = states
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -21,10 +21,11 @@ func TestLoadComposesLiteralKeysAndEffectivePresence(t *testing.T) {
|
|||||||
"extra_params": map[string]any{"provider": map[string]any{"first": 1, "second": 2}}},
|
"extra_params": map[string]any{"provider": map[string]any{"first": 1, "second": 2}}},
|
||||||
"child.v1": map[string]any{"extends": "base/one"},
|
"child.v1": map[string]any{"extends": "base/one"},
|
||||||
},
|
},
|
||||||
"prompts": map[string]any{"invoice/summary": map[string]any{"v1.0": map[string]any{
|
"prompts": map[string]any{"invoice/summary": map[string]any{
|
||||||
|
"version": "v1.0",
|
||||||
"messages": []any{map[string]any{"role": "user", "content": "Summarize."}},
|
"messages": []any{map[string]any{"role": "user", "content": "Summarize."}},
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "basic"},
|
"output": map[string]any{"format": "text", "validation_mode": "basic"},
|
||||||
}}},
|
}},
|
||||||
})
|
})
|
||||||
overlay := mapping("operator", map[string]any{
|
overlay := mapping("operator", map[string]any{
|
||||||
"backends": map[string]any{"local.v1": map[string]any{"queue_capacity": 0}},
|
"backends": map[string]any{"local.v1": map[string]any{"queue_capacity": 0}},
|
||||||
@@ -54,7 +55,7 @@ func TestLoadComposesLiteralKeysAndEffectivePresence(t *testing.T) {
|
|||||||
if backend.QueueCapacity == nil || *backend.QueueCapacity != 0 || loaded.backends["local.v1"]["queue_capacity"] != fieldValue {
|
if backend.QueueCapacity == nil || *backend.QueueCapacity != 0 || loaded.backends["local.v1"]["queue_capacity"] != fieldValue {
|
||||||
t.Fatalf("explicit zero queue capacity was lost: %+v", backend)
|
t.Fatalf("explicit zero queue capacity was lost: %+v", backend)
|
||||||
}
|
}
|
||||||
if loaded.result.Config.Prompts["invoice/summary"]["v1.0"].Messages[0].Content == nil {
|
if loaded.result.Config.Prompts["invoice/summary"].Messages[0].Content == nil || loaded.result.Config.Prompts["invoice/summary"].Version == nil || *loaded.result.Config.Prompts["invoice/summary"].Version != "v1.0" {
|
||||||
t.Fatal("literal prompt ID and version were not decoded")
|
t.Fatal("literal prompt ID and version were not decoded")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,8 +16,7 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
errPromptNotFound = errors.New("prompt definition not found")
|
errPromptNotFound = errors.New("prompt definition not found")
|
||||||
errPromptAmbiguous = errors.New("prompt version is ambiguous")
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type promptEntry struct {
|
type promptEntry struct {
|
||||||
@@ -29,64 +28,61 @@ type promptEntry struct {
|
|||||||
messages []*template.Template
|
messages []*template.Template
|
||||||
}
|
}
|
||||||
|
|
||||||
func resolvePrompts(prompts map[string]map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution) {
|
func resolvePrompts(prompts map[string]promptConfig, basePath configkit.Path, resolution *configkit.Resolution) {
|
||||||
for _, id := range sortedKeys(prompts) {
|
for _, id := range sortedKeys(prompts) {
|
||||||
for _, version := range sortedKeys(prompts[id]) {
|
definition := prompts[id]
|
||||||
definition := prompts[id][version]
|
base := basePath.Key(id)
|
||||||
base := basePath.Key(id).Key(version)
|
for _, field := range []struct {
|
||||||
|
name string
|
||||||
|
value **string
|
||||||
|
}{
|
||||||
|
{"description", &definition.Description},
|
||||||
|
{"session_id", &definition.SessionID},
|
||||||
|
} {
|
||||||
|
if *field.value != nil {
|
||||||
|
normalizeString(field.value, base.Key(field.name), resolution, strings.TrimSpace(**field.value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inputsChanged := false
|
||||||
|
for index := range definition.Inputs {
|
||||||
|
input := &definition.Inputs[index]
|
||||||
|
inputPath := base.Key("inputs").Index(index)
|
||||||
|
normalizedName := strings.TrimSpace(input.Name)
|
||||||
|
if normalizedName != input.Name {
|
||||||
|
configkit.Normalize(resolution, inputPath.Key("name"), &input.Name, normalizedName, "normalize input metadata")
|
||||||
|
inputsChanged = true
|
||||||
|
}
|
||||||
for _, field := range []struct {
|
for _, field := range []struct {
|
||||||
name string
|
name string
|
||||||
value **string
|
value **string
|
||||||
}{
|
}{
|
||||||
{"description", &definition.Description},
|
{"content_type", &input.ContentType},
|
||||||
{"session_id", &definition.SessionID},
|
{"description", &input.Description},
|
||||||
} {
|
} {
|
||||||
if *field.value != nil {
|
if *field.value != nil {
|
||||||
normalizeString(field.value, base.Key(field.name), resolution, strings.TrimSpace(**field.value))
|
normalized := strings.TrimSpace(**field.value)
|
||||||
}
|
if normalized != **field.value {
|
||||||
}
|
configkit.Normalize(resolution, inputPath.Key(field.name), field.value, &normalized, "normalize input metadata")
|
||||||
inputsChanged := false
|
inputsChanged = true
|
||||||
for index := range definition.Inputs {
|
|
||||||
input := &definition.Inputs[index]
|
|
||||||
inputPath := base.Key("inputs").Index(index)
|
|
||||||
normalizedName := strings.TrimSpace(input.Name)
|
|
||||||
if normalizedName != input.Name {
|
|
||||||
configkit.Normalize(resolution, inputPath.Key("name"), &input.Name, normalizedName, "normalize input metadata")
|
|
||||||
inputsChanged = true
|
|
||||||
}
|
|
||||||
for _, field := range []struct {
|
|
||||||
name string
|
|
||||||
value **string
|
|
||||||
}{
|
|
||||||
{"content_type", &input.ContentType},
|
|
||||||
{"description", &input.Description},
|
|
||||||
} {
|
|
||||||
if *field.value != nil {
|
|
||||||
normalized := strings.TrimSpace(**field.value)
|
|
||||||
if normalized != **field.value {
|
|
||||||
configkit.Normalize(resolution, inputPath.Key(field.name), field.value, &normalized, "normalize input metadata")
|
|
||||||
inputsChanged = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if inputsChanged {
|
|
||||||
resolution.Record(base.Key("inputs"), configkit.ChangeNormalization, "normalize input metadata")
|
|
||||||
}
|
|
||||||
// Normalize message metadata on execution-owned copies below. ConfigKit
|
|
||||||
// records sequence mutations as whole-value replacements, which would
|
|
||||||
// discard the effective source contexts of unchanged content_file fields.
|
|
||||||
if definition.Output != nil && definition.Output.SchemaPath != nil {
|
|
||||||
normalizeString(&definition.Output.SchemaPath, base.Key("output").Key("schema_path"), resolution, strings.TrimSpace(*definition.Output.SchemaPath))
|
|
||||||
}
|
|
||||||
prompts[id][version] = definition
|
|
||||||
}
|
}
|
||||||
|
if inputsChanged {
|
||||||
|
resolution.Record(base.Key("inputs"), configkit.ChangeNormalization, "normalize input metadata")
|
||||||
|
}
|
||||||
|
// Normalize message metadata on execution-owned copies below. ConfigKit
|
||||||
|
// records sequence mutations as whole-value replacements, which would
|
||||||
|
// discard the effective source contexts of unchanged content_file fields.
|
||||||
|
if definition.Output != nil && definition.Output.SchemaPath != nil {
|
||||||
|
normalizeString(&definition.Output.SchemaPath, base.Key("output").Key("schema_path"), resolution, strings.TrimSpace(*definition.Output.SchemaPath))
|
||||||
|
}
|
||||||
|
prompts[id] = definition
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *loaded) materializePrompts(prompts map[string]map[string]promptConfig, basePath configkit.Path) error {
|
func (state *loaded) materializePrompts(prompts map[string]promptConfig, basePath configkit.Path) error {
|
||||||
state.prompts = make(map[string]map[string]*promptEntry, len(prompts))
|
state.prompts = make(map[string]*promptEntry, len(prompts))
|
||||||
state.ambiguous = make(map[string]bool)
|
|
||||||
var metadata *configkit.Metadata
|
var metadata *configkit.Metadata
|
||||||
lookupOrigin := func(path configkit.Path) (configkit.FieldInfo, bool) {
|
lookupOrigin := func(path configkit.Path) (configkit.FieldInfo, bool) {
|
||||||
if metadata == nil {
|
if metadata == nil {
|
||||||
@@ -100,23 +96,15 @@ func (state *loaded) materializePrompts(prompts map[string]map[string]promptConf
|
|||||||
if strings.TrimSpace(id) == "" {
|
if strings.TrimSpace(id) == "" {
|
||||||
return fmt.Errorf("%s: prompt ID must not be blank", promptBase)
|
return fmt.Errorf("%s: prompt ID must not be blank", promptBase)
|
||||||
}
|
}
|
||||||
versions := prompts[id]
|
version := ""
|
||||||
if versions == nil {
|
if prompts[id].Version != nil {
|
||||||
return fmt.Errorf("%s: prompt collection must be a mapping", promptBase)
|
version = *prompts[id].Version
|
||||||
}
|
}
|
||||||
state.prompts[id] = make(map[string]*promptEntry, len(versions))
|
entry, err := state.materializePrompt(id, version, prompts[id], promptBase, lookupOrigin)
|
||||||
state.ambiguous[id] = len(versions) > 1
|
if err != nil {
|
||||||
for _, version := range sortedKeys(versions) {
|
return err
|
||||||
versionBase := promptBase.Key(version)
|
|
||||||
if strings.TrimSpace(version) == "" {
|
|
||||||
return fmt.Errorf("%s: prompt version must not be blank", versionBase)
|
|
||||||
}
|
|
||||||
entry, err := state.materializePrompt(id, version, versions[version], versionBase, lookupOrigin)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
state.prompts[id][version] = entry
|
|
||||||
}
|
}
|
||||||
|
state.prompts[id] = entry
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -164,7 +152,7 @@ func (state *loaded) materializePrompt(id, version string, raw promptConfig, bas
|
|||||||
definition.Inputs = append(definition.Inputs, domain.PromptInput{
|
definition.Inputs = append(definition.Inputs, domain.PromptInput{
|
||||||
Name: input.Name, Required: input.Required, ContentType: contentType, Description: description,
|
Name: input.Name, Required: input.Required, ContentType: contentType, Description: description,
|
||||||
})
|
})
|
||||||
presence := state.promptMetadata[id][version][index]
|
presence := state.promptMetadata[id][index]
|
||||||
metadata := prompt.InputMetadata{Name: input.Name, Required: input.Required}
|
metadata := prompt.InputMetadata{Name: input.Name, Required: input.Required}
|
||||||
if presence.contentType == fieldValue {
|
if presence.contentType == fieldValue {
|
||||||
metadata.ContentType = stringPointer(contentType)
|
metadata.ContentType = stringPointer(contentType)
|
||||||
@@ -299,19 +287,10 @@ func (state *loaded) readContent(fieldPath configkit.Path, name string, lookupOr
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (state *loaded) lookupPromptEntry(id, version string) (*promptEntry, error) {
|
func (state *loaded) lookupPromptEntry(id, version string) (*promptEntry, error) {
|
||||||
if state == nil || strings.TrimSpace(id) == "" || (version != "" && strings.TrimSpace(version) == "") {
|
if state == nil || strings.TrimSpace(id) == "" {
|
||||||
return nil, errPromptNotFound
|
return nil, errPromptNotFound
|
||||||
}
|
}
|
||||||
versions := state.prompts[id]
|
entry := state.prompts[id]
|
||||||
if version == "" {
|
|
||||||
if state.ambiguous[id] {
|
|
||||||
return nil, errPromptAmbiguous
|
|
||||||
}
|
|
||||||
for selected := range versions {
|
|
||||||
version = selected
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entry := versions[version]
|
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
return nil, errPromptNotFound
|
return nil, errPromptNotFound
|
||||||
}
|
}
|
||||||
@@ -342,8 +321,8 @@ func promptLookupError(err error) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (state *loaded) prompt(id, version string) (*domain.PromptDefinition, domain.PromptInspection, error) {
|
func (state *loaded) prompt(id string) (*domain.PromptDefinition, domain.PromptInspection, error) {
|
||||||
entry, err := state.lookupPromptEntry(id, version)
|
entry, err := state.lookupPromptEntry(id, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, domain.PromptInspection{}, err
|
return nil, domain.PromptInspection{}, err
|
||||||
}
|
}
|
||||||
@@ -351,7 +330,8 @@ func (state *loaded) prompt(id, version string) (*domain.PromptDefinition, domai
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetPromptSnapshot returns an independently owned captured definition and its
|
// GetPromptSnapshot returns an independently owned captured definition and its
|
||||||
// construction-time hash. Lookup retains literal ID and version keys.
|
// construction-time hash. The version argument is retained temporarily at the
|
||||||
|
// adapter boundary and does not participate in lookup.
|
||||||
func (state *loaded) GetPromptSnapshot(ctx context.Context, id, version string) (*domain.PromptDefinition, string, error) {
|
func (state *loaded) GetPromptSnapshot(ctx context.Context, id, version string) (*domain.PromptDefinition, string, error) {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return nil, "", err
|
return nil, "", err
|
||||||
@@ -382,12 +362,12 @@ func (state *loaded) GetPromptDefinition(ctx context.Context, id, version string
|
|||||||
return definition, err
|
return definition, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetCompiledTemplates returns construction-time parses for one exact prompt.
|
// GetCompiledTemplates returns construction-time parses for one prompt ID.
|
||||||
func (state *loaded) GetCompiledTemplates(id, version string) (prompt.CompiledTemplates, bool) {
|
func (state *loaded) GetCompiledTemplates(id, version string) (prompt.CompiledTemplates, bool) {
|
||||||
if state == nil {
|
if state == nil {
|
||||||
return prompt.CompiledTemplates{}, false
|
return prompt.CompiledTemplates{}, false
|
||||||
}
|
}
|
||||||
entry := state.prompts[id][version]
|
entry := state.prompts[id]
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
return prompt.CompiledTemplates{}, false
|
return prompt.CompiledTemplates{}, false
|
||||||
}
|
}
|
||||||
@@ -395,13 +375,13 @@ func (state *loaded) GetCompiledTemplates(id, version string) (prompt.CompiledTe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetRequirementSnapshot returns captured templates and input metadata for
|
// GetRequirementSnapshot returns captured templates and input metadata for
|
||||||
// one exact prompt identity. Returned metadata and slices are independently
|
// one prompt ID. Returned metadata and slices are independently
|
||||||
// owned; the compiled templates are shared immutable construction-time parses.
|
// owned; the compiled templates are shared immutable construction-time parses.
|
||||||
func (state *loaded) GetRequirementSnapshot(id, version string) (prompt.RequirementSnapshot, bool) {
|
func (state *loaded) GetRequirementSnapshot(id, version string) (prompt.RequirementSnapshot, bool) {
|
||||||
if state == nil {
|
if state == nil {
|
||||||
return prompt.RequirementSnapshot{}, false
|
return prompt.RequirementSnapshot{}, false
|
||||||
}
|
}
|
||||||
entry := state.prompts[id][version]
|
entry := state.prompts[id]
|
||||||
if entry == nil {
|
if entry == nil {
|
||||||
return prompt.RequirementSnapshot{}, false
|
return prompt.RequirementSnapshot{}, false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import (
|
|||||||
func TestLoadCapturesInheritedImportedContent(t *testing.T) {
|
func TestLoadCapturesInheritedImportedContent(t *testing.T) {
|
||||||
files := fstest.MapFS{
|
files := fstest.MapFS{
|
||||||
"app/main.yaml": {Data: []byte("configkit:\n imports:\n - path: ../catalog/base.yaml\nprompts:\n child:\n extends: base\n")},
|
"app/main.yaml": {Data: []byte("configkit:\n imports:\n - path: ../catalog/base.yaml\nprompts:\n child:\n extends: base\n")},
|
||||||
"catalog/base.yaml": {Data: []byte("prompts:\n base:\n v1:\n messages:\n - role: ' USER '\n cache_control:\n type: ' ephemeral '\n ttl: ' 1h '\n content_file: ./body.md\n output:\n format: text\n validation_mode: basic\n")},
|
"catalog/base.yaml": {Data: []byte("prompts:\n base:\n version: \"v1\"\n messages:\n - role: ' USER '\n cache_control:\n type: ' ephemeral '\n ttl: ' 1h '\n content_file: ./body.md\n output:\n format: text\n validation_mode: basic\n")},
|
||||||
"catalog/body.md": {Data: []byte("Captured {{.name}}")},
|
"catalog/body.md": {Data: []byte("Captured {{.name}}")},
|
||||||
}
|
}
|
||||||
namespace := configkit.NewFilesystem("embedded", files)
|
namespace := configkit.NewFilesystem("embedded", files)
|
||||||
@@ -32,7 +32,7 @@ func TestLoadCapturesInheritedImportedContent(t *testing.T) {
|
|||||||
}
|
}
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
definition, inspection, err := state.prompt("child", "")
|
definition, inspection, err := state.prompt("child")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -43,7 +43,7 @@ func TestLoadCapturesInheritedImportedContent(t *testing.T) {
|
|||||||
if message.Role != "user" || message.CacheControl == nil || message.CacheControl.Type != "ephemeral" || message.CacheControl.TTL != "1h" {
|
if message.Role != "user" || message.CacheControl == nil || message.CacheControl.Type != "ephemeral" || message.CacheControl.TTL != "1h" {
|
||||||
t.Fatalf("message metadata was not normalized: %+v", message)
|
t.Fatalf("message metadata was not normalized: %+v", message)
|
||||||
}
|
}
|
||||||
field, ok := state.result.Metadata().Lookup(configkit.Keys("prompts", "child", "v1", "messages").Index(0).Key("content_file"))
|
field, ok := state.result.Metadata().Lookup(configkit.Keys("prompts", "child", "messages").Index(0).Key("content_file"))
|
||||||
if !ok || len(field.Origin.Locations) != 1 {
|
if !ok || len(field.Origin.Locations) != 1 {
|
||||||
t.Fatalf("content origin was lost during normalization: %+v", field)
|
t.Fatalf("content origin was lost during normalization: %+v", field)
|
||||||
}
|
}
|
||||||
@@ -57,12 +57,12 @@ func TestLoadCapturesInheritedImportedContent(t *testing.T) {
|
|||||||
t.Fatalf("content origin = %q, want catalog/base.yaml", originPath)
|
t.Fatalf("content origin = %q, want catalog/base.yaml", originPath)
|
||||||
}
|
}
|
||||||
files["catalog/body.md"].Data = []byte("Changed")
|
files["catalog/body.md"].Data = []byte("Changed")
|
||||||
again, againInspection, err := state.prompt("child", "v1")
|
again, againInspection, err := state.prompt("child")
|
||||||
if err != nil || again.Templates[0].Content != "Captured {{.name}}" || againInspection.PromptHash != inspection.PromptHash {
|
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)
|
t.Fatalf("captured definition changed after source mutation: %+v, %+v, %v", again, againInspection, err)
|
||||||
}
|
}
|
||||||
definition.Templates[0].Content = "caller mutation"
|
definition.Templates[0].Content = "caller mutation"
|
||||||
third, _, err := state.prompt("child", "v1")
|
third, _, err := state.prompt("child")
|
||||||
if err != nil || third.Templates[0].Content != "Captured {{.name}}" {
|
if err != nil || third.Templates[0].Content != "Captured {{.name}}" {
|
||||||
t.Fatalf("caller mutation escaped into index: %+v, %v", third, err)
|
t.Fatalf("caller mutation escaped into index: %+v, %v", third, err)
|
||||||
}
|
}
|
||||||
@@ -81,12 +81,12 @@ func TestLoadUsesExplicitMappingContextAndClosesContent(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
definition, _, err := state.prompt("example", "v1")
|
definition, _, err := state.prompt("example")
|
||||||
if err != nil || definition.Templates[0].Content != "From mapping" || filesystem.contentOpened != 1 || filesystem.contentClosed != 1 {
|
if err != nil || definition.Templates[0].Content != "From mapping" || filesystem.contentOpened != 1 || filesystem.contentClosed != 1 {
|
||||||
t.Fatalf("mapping context or handle ownership failed: %+v, opens=%d closes=%d, %v", definition, filesystem.contentOpened, filesystem.contentClosed, err)
|
t.Fatalf("mapping context or handle ownership failed: %+v, opens=%d closes=%d, %v", definition, filesystem.contentOpened, filesystem.contentClosed, err)
|
||||||
}
|
}
|
||||||
_, err = load(mapping("contextless", promptWithFile("body.md")), nil)
|
_, err = load(mapping("contextless", promptWithFile("body.md")), nil)
|
||||||
if err == nil || !strings.Contains(err.Error(), "/prompts/example/v1/messages/0/content_file") {
|
if err == nil || !strings.Contains(err.Error(), "/prompts/example/messages/0/content_file") {
|
||||||
t.Fatalf("expected semantic path for contextless resource, got %v", err)
|
t.Fatalf("expected semantic path for contextless resource, got %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -95,7 +95,7 @@ func TestLoadCapturesNativeContentFile(t *testing.T) {
|
|||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
definitionPath := filepath.Join(root, "definition.yaml")
|
definitionPath := filepath.Join(root, "definition.yaml")
|
||||||
contentPath := filepath.Join(root, "body.md")
|
contentPath := filepath.Join(root, "body.md")
|
||||||
if err := os.WriteFile(definitionPath, []byte("prompts:\n example:\n v1:\n messages:\n - role: user\n content_file: body.md\n output:\n format: text\n validation_mode: basic\n"), 0o600); err != nil {
|
if err := os.WriteFile(definitionPath, []byte("prompts:\n example:\n version: \"v1\"\n messages:\n - role: user\n content_file: body.md\n output:\n format: text\n validation_mode: basic\n"), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if err := os.WriteFile(contentPath, []byte("Native body"), 0o600); err != nil {
|
if err := os.WriteFile(contentPath, []byte("Native body"), 0o600); err != nil {
|
||||||
@@ -108,31 +108,31 @@ func TestLoadCapturesNativeContentFile(t *testing.T) {
|
|||||||
if err := os.WriteFile(contentPath, []byte("Modified"), 0o600); err != nil {
|
if err := os.WriteFile(contentPath, []byte("Modified"), 0o600); err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
definition, _, err := state.prompt("example", "v1")
|
definition, _, err := state.prompt("example")
|
||||||
if err != nil || definition.Templates[0].Content != "Native body" {
|
if err != nil || definition.Templates[0].Content != "Native body" {
|
||||||
t.Fatalf("native content was not captured: %+v, %v", definition, err)
|
t.Fatalf("native content was not captured: %+v, %v", definition, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadRejectsMalformedStaticSyntaxAndAmbiguousSelection(t *testing.T) {
|
func TestLoadRejectsMalformedStaticSyntaxAndCapturesOptionalVersion(t *testing.T) {
|
||||||
_, err := load(mapping("bad-template", map[string]any{"prompts": map[string]any{"example": map[string]any{
|
_, err := load(mapping("bad-template", map[string]any{"prompts": map[string]any{"example": map[string]any{
|
||||||
"v1": map[string]any{"messages": []any{map[string]any{"role": "user", "content": "{{if}}"}},
|
"messages": []any{map[string]any{"role": "user", "content": "{{if}}"}},
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "basic"}},
|
"output": map[string]any{"format": "text", "validation_mode": "basic"},
|
||||||
}}}), nil)
|
}}}), nil)
|
||||||
if err == nil || !strings.Contains(err.Error(), "invalid template syntax") {
|
if err == nil || !strings.Contains(err.Error(), "invalid template syntax") {
|
||||||
t.Fatalf("expected construction-time syntax error, got %v", err)
|
t.Fatalf("expected construction-time syntax error, got %v", err)
|
||||||
}
|
}
|
||||||
state, err := load(mapping("versions", map[string]any{"prompts": map[string]any{"example": map[string]any{
|
state, err := load(mapping("version metadata", map[string]any{"prompts": map[string]any{"example": map[string]any{
|
||||||
"v1": map[string]any{"messages": []any{map[string]any{"role": "user", "content": "one"}},
|
"version": "release label",
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "basic"}},
|
"messages": []any{map[string]any{"role": "user", "content": "one"}},
|
||||||
"v2": map[string]any{"messages": []any{map[string]any{"role": "user", "content": "two"}},
|
"output": map[string]any{"format": "text", "validation_mode": "basic"},
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "basic"}},
|
|
||||||
}}}), nil)
|
}}}), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
if _, _, err := state.prompt("example", ""); !errors.Is(err, errPromptAmbiguous) {
|
definition, inspection, err := state.prompt("example")
|
||||||
t.Fatalf("expected ambiguous version selection, got %v", err)
|
if err != nil || definition.Version != "release label" || inspection.PromptVersion != "release label" {
|
||||||
|
t.Fatalf("optional version metadata was not retained: definition=%+v inspection=%+v err=%v", definition, inspection, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -148,30 +148,26 @@ func TestPromptInspectionLookupMatchesSnapshotSelectionAndOwnsInputs(t *testing.
|
|||||||
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
selectedPrompt := makePrompt("selected")
|
||||||
|
selectedPrompt["version"] = "v2"
|
||||||
state, err := load(mapping("prompt inspection lookup", map[string]any{
|
state, err := load(mapping("prompt inspection lookup", map[string]any{
|
||||||
"profiles": map[string]any{"partial": map[string]any{}},
|
"profiles": map[string]any{"partial": map[string]any{}},
|
||||||
"prompts": map[string]any{
|
"prompts": map[string]any{
|
||||||
"multi": map[string]any{"v1": makePrompt("first"), "v2": makePrompt("second")},
|
"selected": selectedPrompt,
|
||||||
"single": map[string]any{"v1": makePrompt("only")},
|
|
||||||
},
|
},
|
||||||
}), nil)
|
}), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, selection := range []struct {
|
{
|
||||||
id, version string
|
inspection, err := state.GetPromptInspection(context.Background(), "selected", "")
|
||||||
}{
|
|
||||||
{id: "multi", version: "v2"},
|
|
||||||
{id: "single"},
|
|
||||||
} {
|
|
||||||
inspection, err := state.GetPromptInspection(context.Background(), selection.id, selection.version)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("inspect %q/%q: %v", selection.id, selection.version, err)
|
t.Fatalf("inspect prompt ID: %v", err)
|
||||||
}
|
}
|
||||||
definition, hash, err := state.GetPromptSnapshot(context.Background(), selection.id, selection.version)
|
definition, hash, err := state.GetPromptSnapshot(context.Background(), "selected", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("snapshot %q/%q: %v", selection.id, selection.version, err)
|
t.Fatalf("snapshot prompt ID: %v", err)
|
||||||
}
|
}
|
||||||
if inspection.PromptID != definition.ID || inspection.PromptVersion != definition.Version ||
|
if inspection.PromptID != definition.ID || inspection.PromptVersion != definition.Version ||
|
||||||
inspection.PromptHash != hash || inspection.DefaultProfileID != "partial" ||
|
inspection.PromptHash != hash || inspection.DefaultProfileID != "partial" ||
|
||||||
@@ -184,53 +180,42 @@ func TestPromptInspectionLookupMatchesSnapshotSelectionAndOwnsInputs(t *testing.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
owned, err := state.GetPromptInspection(context.Background(), "single", "")
|
owned, err := state.GetPromptInspection(context.Background(), "selected", "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
owned.Inputs[0].Name = "caller mutation"
|
owned.Inputs[0].Name = "caller mutation"
|
||||||
owned.Inputs[0].Description = "caller mutation"
|
owned.Inputs[0].Description = "caller mutation"
|
||||||
again, err := state.GetPromptInspection(context.Background(), "single", "v1")
|
again, err := state.GetPromptInspection(context.Background(), "selected", "")
|
||||||
if err != nil || again.Inputs[0].Name != "document" || again.Inputs[0].Description != "source document" {
|
if err != nil || again.Inputs[0].Name != "document" || again.Inputs[0].Description != "source document" {
|
||||||
t.Fatalf("inspection mutation escaped into captured state: inspection=%+v err=%v", again, err)
|
t.Fatalf("inspection mutation escaped into captured state: inspection=%+v err=%v", again, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, selection := range []struct {
|
for _, id := range []string{"missing", " "} {
|
||||||
id, version string
|
inspection, err := state.GetPromptInspection(context.Background(), id, "")
|
||||||
}{
|
|
||||||
{id: "missing", version: "v1"},
|
|
||||||
{id: "multi", version: "V2"},
|
|
||||||
{id: " ", version: "v1"},
|
|
||||||
} {
|
|
||||||
inspection, err := state.GetPromptInspection(context.Background(), selection.id, selection.version)
|
|
||||||
if inspection != nil || !errors.Is(err, promptdef.ErrPromptDefinitionNotFound) {
|
if inspection != nil || !errors.Is(err, promptdef.ErrPromptDefinitionNotFound) {
|
||||||
t.Errorf("inspection selection %q/%q=(%+v, %v), want not found", selection.id, selection.version, inspection, err)
|
t.Errorf("inspection lookup %q=(%+v, %v), want not found", id, inspection, err)
|
||||||
}
|
}
|
||||||
definition, _, err := state.GetPromptSnapshot(context.Background(), selection.id, selection.version)
|
definition, _, err := state.GetPromptSnapshot(context.Background(), id, "")
|
||||||
if definition != nil || !errors.Is(err, promptdef.ErrPromptDefinitionNotFound) {
|
if definition != nil || !errors.Is(err, promptdef.ErrPromptDefinitionNotFound) {
|
||||||
t.Errorf("snapshot selection %q/%q=(%+v, %v), want not found", selection.id, selection.version, definition, err)
|
t.Errorf("snapshot lookup %q=(%+v, %v), want not found", id, definition, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if _, err := state.GetPromptInspection(context.Background(), "multi", ""); !errors.Is(err, errPromptAmbiguous) {
|
|
||||||
t.Errorf("ambiguous inspection selection error=%v", err)
|
|
||||||
}
|
|
||||||
if _, _, err := state.GetPromptSnapshot(context.Background(), "multi", ""); !errors.Is(err, errPromptAmbiguous) {
|
|
||||||
t.Errorf("ambiguous snapshot selection error=%v", err)
|
|
||||||
}
|
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
cancel()
|
cancel()
|
||||||
if _, err := state.GetPromptInspection(ctx, "single", "v1"); !errors.Is(err, context.Canceled) {
|
if _, err := state.GetPromptInspection(ctx, "selected", ""); !errors.Is(err, context.Canceled) {
|
||||||
t.Errorf("canceled inspection selection error=%v", err)
|
t.Errorf("canceled inspection selection error=%v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadAcceptsEmptyInlineContentAndRejectsInvalidPromptFields(t *testing.T) {
|
func TestLoadAcceptsEmptyInlineContentAndRejectsInvalidPromptFields(t *testing.T) {
|
||||||
state, err := load(mapping("empty-content", map[string]any{"prompts": map[string]any{" exact.id ": map[string]any{"v1": map[string]any{
|
state, err := load(mapping("empty-content", map[string]any{"prompts": map[string]any{" exact.id ": map[string]any{
|
||||||
"inputs": []any{map[string]any{"name": " note ", "required": true}},
|
"version": "v1",
|
||||||
|
"inputs": []any{map[string]any{"name": " note ", "required": true}},
|
||||||
"messages": []any{map[string]any{"role": " USER ", "content": "", "content_file": nil,
|
"messages": []any{map[string]any{"role": " USER ", "content": "", "content_file": nil,
|
||||||
"cache_control": map[string]any{"type": " ephemeral ", "ttl": "1h "}}},
|
"cache_control": map[string]any{"type": " ephemeral ", "ttl": "1h "}}},
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "basic"},
|
"output": map[string]any{"format": "text", "validation_mode": "basic"},
|
||||||
}}}}), nil)
|
}}}), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var loadError *configkit.LoadError
|
var loadError *configkit.LoadError
|
||||||
if errors.As(err, &loadError) {
|
if errors.As(err, &loadError) {
|
||||||
@@ -238,18 +223,19 @@ func TestLoadAcceptsEmptyInlineContentAndRejectsInvalidPromptFields(t *testing.T
|
|||||||
}
|
}
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
definition, _, err := state.prompt(" exact.id ", "v1")
|
definition, _, err := state.prompt(" exact.id ")
|
||||||
if err != nil || definition.ID != " exact.id " || definition.Templates[0].Content != "" || definition.Templates[0].Role != "user" ||
|
if err != nil || definition.ID != " exact.id " || definition.Templates[0].Content != "" || definition.Templates[0].Role != "user" ||
|
||||||
definition.Inputs[0].Name != "note" || definition.Templates[0].CacheControl.TTL != "1h" {
|
definition.Inputs[0].Name != "note" || definition.Templates[0].CacheControl.TTL != "1h" {
|
||||||
t.Fatalf("explicit empty content or literal ID was changed: %+v, %v", definition, err)
|
t.Fatalf("explicit empty content or literal ID was changed: %+v, %v", definition, err)
|
||||||
}
|
}
|
||||||
if _, _, err := state.prompt("exact.id", "v1"); !errors.Is(err, errPromptNotFound) {
|
if _, _, err := state.prompt("exact.id"); !errors.Is(err, errPromptNotFound) {
|
||||||
t.Fatalf("trimmed prompt identity unexpectedly matched: %v", err)
|
t.Fatalf("trimmed prompt identity unexpectedly matched: %v", err)
|
||||||
}
|
}
|
||||||
_, err = load(mapping("obsolete-field", map[string]any{"prompts": map[string]any{"example": map[string]any{"v1": map[string]any{
|
_, err = load(mapping("obsolete-field", map[string]any{"prompts": map[string]any{"example": map[string]any{
|
||||||
"id": "example", "messages": []any{map[string]any{"role": "user", "content": "ok"}},
|
"version": "v1",
|
||||||
|
"id": "example", "messages": []any{map[string]any{"role": "user", "content": "ok"}},
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "basic"},
|
"output": map[string]any{"format": "text", "validation_mode": "basic"},
|
||||||
}}}}), nil)
|
}}}), nil)
|
||||||
var loadError *configkit.LoadError
|
var loadError *configkit.LoadError
|
||||||
if !errors.As(err, &loadError) {
|
if !errors.As(err, &loadError) {
|
||||||
t.Fatalf("obsolete identity field should fail strict decoding: %v", err)
|
t.Fatalf("obsolete identity field should fail strict decoding: %v", err)
|
||||||
@@ -262,7 +248,8 @@ func TestLoadAcceptsEmptyInlineContentAndRejectsInvalidPromptFields(t *testing.T
|
|||||||
|
|
||||||
func TestRequirementSnapshotCapturesMetadataPresenceAndNormalizedValues(t *testing.T) {
|
func TestRequirementSnapshotCapturesMetadataPresenceAndNormalizedValues(t *testing.T) {
|
||||||
state, err := load(mapping("requirement-metadata", map[string]any{
|
state, err := load(mapping("requirement-metadata", map[string]any{
|
||||||
"prompts": map[string]any{"example": map[string]any{"v1": map[string]any{
|
"prompts": map[string]any{"example": map[string]any{
|
||||||
|
"version": "v1",
|
||||||
"session_id": "{{.session}}",
|
"session_id": "{{.session}}",
|
||||||
"inputs": []any{
|
"inputs": []any{
|
||||||
map[string]any{"name": "absent"},
|
map[string]any{"name": "absent"},
|
||||||
@@ -272,7 +259,7 @@ func TestRequirementSnapshotCapturesMetadataPresenceAndNormalizedValues(t *testi
|
|||||||
},
|
},
|
||||||
"messages": []any{map[string]any{"role": "user", "content": "{{input \"absent\"}}"}},
|
"messages": []any{map[string]any{"role": "user", "content": "{{input \"absent\"}}"}},
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "basic"},
|
"output": map[string]any{"format": "text", "validation_mode": "basic"},
|
||||||
}}},
|
}},
|
||||||
}), nil)
|
}), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var loadError *configkit.LoadError
|
var loadError *configkit.LoadError
|
||||||
@@ -282,7 +269,7 @@ func TestRequirementSnapshotCapturesMetadataPresenceAndNormalizedValues(t *testi
|
|||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
snapshot, ok := state.GetRequirementSnapshot("example", "v1")
|
snapshot, ok := state.GetRequirementSnapshot("example", "")
|
||||||
if !ok || len(snapshot.Inputs) != 4 || len(snapshot.Templates.Messages) != 1 || snapshot.Templates.Session == nil {
|
if !ok || len(snapshot.Inputs) != 4 || len(snapshot.Templates.Messages) != 1 || snapshot.Templates.Session == nil {
|
||||||
t.Fatalf("requirement snapshot was not captured: %+v, %t", snapshot, ok)
|
t.Fatalf("requirement snapshot was not captured: %+v, %t", snapshot, ok)
|
||||||
}
|
}
|
||||||
@@ -300,7 +287,7 @@ func TestRequirementSnapshotCapturesMetadataPresenceAndNormalizedValues(t *testi
|
|||||||
t.Errorf("snapshot did not use normalized final metadata: %+v", snapshot.Inputs[3])
|
t.Errorf("snapshot did not use normalized final metadata: %+v", snapshot.Inputs[3])
|
||||||
}
|
}
|
||||||
|
|
||||||
definition, inspection, err := state.prompt("example", "v1")
|
definition, inspection, err := state.prompt("example")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -312,13 +299,10 @@ func TestRequirementSnapshotCapturesMetadataPresenceAndNormalizedValues(t *testi
|
|||||||
*snapshot.Inputs[2].ContentType = "caller mutation"
|
*snapshot.Inputs[2].ContentType = "caller mutation"
|
||||||
snapshot.Inputs[3].Name = "caller mutation"
|
snapshot.Inputs[3].Name = "caller mutation"
|
||||||
snapshot.Templates.Messages[0] = nil
|
snapshot.Templates.Messages[0] = nil
|
||||||
again, ok := state.GetRequirementSnapshot("example", "v1")
|
again, ok := state.GetRequirementSnapshot("example", "")
|
||||||
if !ok || *again.Inputs[2].ContentType != "" || again.Inputs[3].Name != "normalized" || again.Templates.Messages[0] == nil {
|
if !ok || *again.Inputs[2].ContentType != "" || again.Inputs[3].Name != "normalized" || again.Templates.Messages[0] == nil {
|
||||||
t.Fatalf("snapshot mutation escaped into captured state: %+v, %t", again, ok)
|
t.Fatalf("snapshot mutation escaped into captured state: %+v, %t", again, ok)
|
||||||
}
|
}
|
||||||
if _, ok := state.GetRequirementSnapshot("example", "V1"); ok {
|
|
||||||
t.Fatal("snapshot lookup unexpectedly changed exact version matching")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInputMetadataPresenceDoesNotChangePromptHashOrInspection(t *testing.T) {
|
func TestInputMetadataPresenceDoesNotChangePromptHashOrInspection(t *testing.T) {
|
||||||
@@ -338,11 +322,12 @@ func TestInputMetadataPresenceDoesNotChangePromptHashOrInspection(t *testing.T)
|
|||||||
input[name] = value
|
input[name] = value
|
||||||
}
|
}
|
||||||
state, err := load(mapping("hash-"+variant.name, map[string]any{
|
state, err := load(mapping("hash-"+variant.name, map[string]any{
|
||||||
"prompts": map[string]any{"same": map[string]any{"v1": map[string]any{
|
"prompts": map[string]any{"same": map[string]any{
|
||||||
|
"version": "v1",
|
||||||
"inputs": []any{input},
|
"inputs": []any{input},
|
||||||
"messages": []any{map[string]any{"role": "user", "content": "hello"}},
|
"messages": []any{map[string]any{"role": "user", "content": "hello"}},
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "basic"},
|
"output": map[string]any{"format": "text", "validation_mode": "basic"},
|
||||||
}}},
|
}},
|
||||||
}), nil)
|
}), nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
var loadError *configkit.LoadError
|
var loadError *configkit.LoadError
|
||||||
@@ -351,7 +336,7 @@ func TestInputMetadataPresenceDoesNotChangePromptHashOrInspection(t *testing.T)
|
|||||||
}
|
}
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
_, inspection, err := state.prompt("same", "v1")
|
_, inspection, err := state.prompt("same")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -368,10 +353,10 @@ func TestInputMetadataPresenceDoesNotChangePromptHashOrInspection(t *testing.T)
|
|||||||
}
|
}
|
||||||
|
|
||||||
func promptWithFile(name string) map[string]any {
|
func promptWithFile(name string) map[string]any {
|
||||||
return map[string]any{"prompts": map[string]any{"example": map[string]any{"v1": map[string]any{
|
return map[string]any{"prompts": map[string]any{"example": map[string]any{
|
||||||
"messages": []any{map[string]any{"role": "user", "content_file": name}},
|
"messages": []any{map[string]any{"role": "user", "content_file": name}},
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "basic"},
|
"output": map[string]any{"format": "text", "validation_mode": "basic"},
|
||||||
}}}}
|
}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
type countingFS struct {
|
type countingFS struct {
|
||||||
|
|||||||
@@ -356,22 +356,20 @@ func smokeSelectionConfig() map[string]any {
|
|||||||
},
|
},
|
||||||
"prompts": map[string]any{
|
"prompts": map[string]any{
|
||||||
"selection.prompt": map[string]any{
|
"selection.prompt": map[string]any{
|
||||||
"v1": map[string]any{
|
"version": "v1",
|
||||||
"default_profile": "prompt-profile",
|
"default_profile": "prompt-profile",
|
||||||
"inputs": []any{map[string]any{"name": "required_input", "required": true}},
|
"inputs": []any{map[string]any{"name": "required_input", "required": true}},
|
||||||
"messages": []any{map[string]any{"role": "user", "content": "Hello {{.required_input}}"}},
|
"messages": []any{map[string]any{"role": "user", "content": "Hello {{.required_input}}"}},
|
||||||
"session_id": "{{.vars.session}}",
|
"session_id": "{{.vars.session}}",
|
||||||
"output": map[string]any{
|
"output": map[string]any{
|
||||||
"format": "json", "validation_mode": "json_schema", "schema_path": "not-read.json",
|
"format": "json", "validation_mode": "json_schema", "schema_path": "not-read.json",
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
"backend.prompt": map[string]any{
|
"backend.prompt": map[string]any{
|
||||||
"v1": map[string]any{
|
"version": "v1",
|
||||||
"messages": []any{map[string]any{"role": "user", "content": "Hello"}},
|
"messages": []any{map[string]any{"role": "user", "content": "Hello"}},
|
||||||
"output": map[string]any{
|
"output": map[string]any{
|
||||||
"format": "json", "validation_mode": "json_schema", "schema_path": "not-read.json",
|
"format": "json", "validation_mode": "json_schema", "schema_path": "not-read.json",
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -18,10 +18,7 @@ func TestListIdentitiesBasicContract(t *testing.T) {
|
|||||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{
|
||||||
Source: configkit.Mapping("listing", map[string]any{
|
Source: configkit.Mapping("listing", map[string]any{
|
||||||
"profiles": map[string]any{"configured-profile": map[string]any{"model": "example-model"}},
|
"profiles": map[string]any{"configured-profile": map[string]any{"model": "example-model"}},
|
||||||
"prompts": map[string]any{"example-prompt": map[string]any{
|
"prompts": map[string]any{"example-prompt": promptDefinitionWithVersion("10", listingContractPrompt())},
|
||||||
"2": listingContractPrompt(),
|
|
||||||
"10": listingContractPrompt(),
|
|
||||||
}},
|
|
||||||
}),
|
}),
|
||||||
}}})
|
}}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -42,7 +39,6 @@ func TestListIdentitiesBasicContract(t *testing.T) {
|
|||||||
}
|
}
|
||||||
want := []promptkit.PromptIdentity{
|
want := []promptkit.PromptIdentity{
|
||||||
{ID: "example-prompt", Version: "10"},
|
{ID: "example-prompt", Version: "10"},
|
||||||
{ID: "example-prompt", Version: "2"},
|
|
||||||
}
|
}
|
||||||
if len(prompts) != len(want) {
|
if len(prompts) != len(want) {
|
||||||
t.Fatalf("prompt listing = %#v, want %#v", prompts, want)
|
t.Fatalf("prompt listing = %#v, want %#v", prompts, want)
|
||||||
@@ -80,7 +76,7 @@ func TestListIdentitiesPreserveContextErrorsWithoutPartialResults(t *testing.T)
|
|||||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{
|
||||||
Source: configkit.Mapping("listing", map[string]any{
|
Source: configkit.Mapping("listing", map[string]any{
|
||||||
"profiles": map[string]any{"profile": map[string]any{}},
|
"profiles": map[string]any{"profile": map[string]any{}},
|
||||||
"prompts": map[string]any{"example": map[string]any{"v1": listingContractPrompt()}},
|
"prompts": map[string]any{"example": promptDefinitionWithVersion("v1", listingContractPrompt())},
|
||||||
}),
|
}),
|
||||||
}}})
|
}}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -120,11 +116,8 @@ func TestListIdentitiesUseEffectiveLayeredMembership(t *testing.T) {
|
|||||||
"overlay/profile": map[string]any{"endpoint": "https://example.test/v1", "model": "old-model"},
|
"overlay/profile": map[string]any{"endpoint": "https://example.test/v1", "model": "old-model"},
|
||||||
},
|
},
|
||||||
"prompts": map[string]any{
|
"prompts": map[string]any{
|
||||||
"shared/prompt": map[string]any{
|
"shared/prompt": promptDefinitionWithVersion("v1", listingContractPrompt()),
|
||||||
"v1": listingContractPrompt(),
|
"underlay-only": promptDefinitionWithVersion("v1", listingContractPrompt()),
|
||||||
"v2": listingContractPrompt(),
|
|
||||||
},
|
|
||||||
"underlay-only": map[string]any{"v1": listingContractPrompt()},
|
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
main := configkit.Input{Source: configkit.Mapping("application overlay", map[string]any{
|
main := configkit.Input{Source: configkit.Mapping("application overlay", map[string]any{
|
||||||
@@ -132,8 +125,8 @@ func TestListIdentitiesUseEffectiveLayeredMembership(t *testing.T) {
|
|||||||
"overlay/profile": map[string]any{"model": "effective-model"},
|
"overlay/profile": map[string]any{"model": "effective-model"},
|
||||||
},
|
},
|
||||||
"prompts": map[string]any{
|
"prompts": map[string]any{
|
||||||
"shared/prompt": map[string]any{"v1": listingContractPrompt()},
|
"shared/prompt": promptDefinitionWithVersion("v2", listingContractPrompt()),
|
||||||
"main-only": map[string]any{"v3": listingContractPrompt()},
|
"main-only": promptDefinitionWithVersion("v3", listingContractPrompt()),
|
||||||
},
|
},
|
||||||
})}
|
})}
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
@@ -177,14 +170,13 @@ func TestListIdentitiesUseEffectiveLayeredMembership(t *testing.T) {
|
|||||||
}
|
}
|
||||||
wantPrompts := []promptkit.PromptIdentity{
|
wantPrompts := []promptkit.PromptIdentity{
|
||||||
{ID: "main-only", Version: "v3"},
|
{ID: "main-only", Version: "v3"},
|
||||||
{ID: "shared/prompt", Version: "v1"},
|
|
||||||
{ID: "shared/prompt", Version: "v2"},
|
{ID: "shared/prompt", Version: "v2"},
|
||||||
{ID: "underlay-only", Version: "v1"},
|
{ID: "underlay-only", Version: "v1"},
|
||||||
}
|
}
|
||||||
if !reflect.DeepEqual(prompts, wantPrompts) {
|
if !reflect.DeepEqual(prompts, wantPrompts) {
|
||||||
t.Fatalf("effective prompt identities = %#v, want %#v", prompts, wantPrompts)
|
t.Fatalf("effective prompt identities = %#v, want %#v", prompts, wantPrompts)
|
||||||
}
|
}
|
||||||
if got := countListingPrompt(prompts, "shared/prompt", "v1"); got != 1 {
|
if got := countListingPrompt(prompts, "shared/prompt", "v2"); got != 1 {
|
||||||
t.Fatalf("overlaid prompt identity appears %d times, want 1", got)
|
t.Fatalf("overlaid prompt identity appears %d times, want 1", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -211,7 +203,7 @@ func TestListIdentitiesEmptyEffectiveConfiguration(t *testing.T) {
|
|||||||
func TestListIdentitiesAreOwnedAndSafeForConcurrentUse(t *testing.T) {
|
func TestListIdentitiesAreOwnedAndSafeForConcurrentUse(t *testing.T) {
|
||||||
engine, err := newListingContractEngine(t, map[string]any{
|
engine, err := newListingContractEngine(t, map[string]any{
|
||||||
"profiles": map[string]any{"profile": map[string]any{"model": "partial-model"}},
|
"profiles": map[string]any{"profile": map[string]any{"model": "partial-model"}},
|
||||||
"prompts": map[string]any{"prompt": map[string]any{"v1": listingContractPrompt()}},
|
"prompts": map[string]any{"prompt": promptDefinitionWithVersion("v1", listingContractPrompt())},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@@ -300,7 +292,7 @@ func TestListIdentitiesAreOwnedAndSafeForConcurrentUse(t *testing.T) {
|
|||||||
|
|
||||||
func TestListIdentitiesUseCapturedSourcesWithoutExecutionWork(t *testing.T) {
|
func TestListIdentitiesUseCapturedSourcesWithoutExecutionWork(t *testing.T) {
|
||||||
files := fstest.MapFS{
|
files := fstest.MapFS{
|
||||||
"config/listing.yaml": &fstest.MapFile{Data: []byte("profiles:\n captured-partial:\n model: partial-model\nprompts:\n captured-prompt:\n v1:\n messages:\n - role: user\n content: Hello.\n output:\n format: text\n validation_mode: basic\n")},
|
"config/listing.yaml": &fstest.MapFile{Data: []byte("profiles:\n captured-partial:\n model: partial-model\nprompts:\n captured-prompt:\n version: v1\n messages:\n - role: user\n content: Hello.\n output:\n format: text\n validation_mode: basic\n")},
|
||||||
}
|
}
|
||||||
inputs, err := promptkit.FilesystemInputs("application listing", files, "config")
|
inputs, err := promptkit.FilesystemInputs("application listing", files, "config")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -312,7 +304,7 @@ func TestListIdentitiesUseCapturedSourcesWithoutExecutionWork(t *testing.T) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
files["config/listing.yaml"].Data = []byte("profiles:\n replacement-profile: {}\nprompts:\n replacement-prompt:\n v2:\n messages:\n - role: user\n content: Changed.\n output:\n format: text\n validation_mode: basic\n")
|
files["config/listing.yaml"].Data = []byte("profiles:\n replacement-profile: {}\nprompts:\n replacement-prompt:\n version: v2\n messages:\n - role: user\n content: Changed.\n output:\n format: text\n validation_mode: basic\n")
|
||||||
|
|
||||||
profiles, err := engine.ListProfiles(context.Background())
|
profiles, err := engine.ListProfiles(context.Background())
|
||||||
if err != nil || !containsListingProfile(profiles, "captured-partial") || containsListingProfile(profiles, "replacement-profile") {
|
if err != nil || !containsListingProfile(profiles, "captured-partial") || containsListingProfile(profiles, "replacement-profile") {
|
||||||
|
|||||||
@@ -27,11 +27,12 @@ func TestDelegatedOperationsIsolateObservations(t *testing.T) {
|
|||||||
child, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{
|
child, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{
|
||||||
Source: configkit.Mapping("child execution", map[string]any{
|
Source: configkit.Mapping("child execution", map[string]any{
|
||||||
"profiles": map[string]any{"profile": map[string]any{"endpoint": "https://example.invalid/v1", "model": "child-model"}},
|
"profiles": map[string]any{"profile": map[string]any{"endpoint": "https://example.invalid/v1", "model": "child-model"}},
|
||||||
"prompts": map[string]any{"child": map[string]any{"1": map[string]any{
|
"prompts": map[string]any{"child": map[string]any{
|
||||||
|
"version": "1",
|
||||||
"default_profile": "profile",
|
"default_profile": "profile",
|
||||||
"messages": []any{map[string]any{"role": "user", "content": "child message"}},
|
"messages": []any{map[string]any{"role": "user", "content": "child message"}},
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||||
}}},
|
}},
|
||||||
}),
|
}),
|
||||||
}}}, promptkit.WithLLMClient(observationClientFunc(func(callCtx context.Context, _ promptkit.GenerateRequest) (*promptkit.GenerateResponse, error) {
|
}}}, promptkit.WithLLMClient(observationClientFunc(func(callCtx context.Context, _ promptkit.GenerateRequest) (*promptkit.GenerateResponse, error) {
|
||||||
gotDeadline, ok := callCtx.Deadline()
|
gotDeadline, ok := callCtx.Deadline()
|
||||||
|
|||||||
@@ -767,11 +767,12 @@ func preparedContractConfig(message string, keyRequired bool) promptkit.Config {
|
|||||||
"profiles": map[string]any{"profile": map[string]any{
|
"profiles": map[string]any{"profile": map[string]any{
|
||||||
"endpoint": "http://example.test/v1", "model": "model", "api_key_required": keyRequired,
|
"endpoint": "http://example.test/v1", "model": "model", "api_key_required": keyRequired,
|
||||||
}},
|
}},
|
||||||
"prompts": map[string]any{"prepared": map[string]any{"1": map[string]any{
|
"prompts": map[string]any{"prepared": map[string]any{
|
||||||
|
"version": "1",
|
||||||
"default_profile": "profile",
|
"default_profile": "profile",
|
||||||
"messages": []any{map[string]any{"role": "user", "content": message}},
|
"messages": []any{map[string]any{"role": "user", "content": message}},
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||||
}}},
|
}},
|
||||||
}),
|
}),
|
||||||
}}}
|
}}}
|
||||||
}
|
}
|
||||||
@@ -780,19 +781,19 @@ func preparedPromptSource(label string) fstest.MapFS {
|
|||||||
return fstest.MapFS{
|
return fstest.MapFS{
|
||||||
"prompt.yaml": &fstest.MapFile{Data: []byte(`prompts:
|
"prompt.yaml": &fstest.MapFile{Data: []byte(`prompts:
|
||||||
prepared:
|
prepared:
|
||||||
"1":
|
version: "1"
|
||||||
default_profile: profile
|
default_profile: profile
|
||||||
inputs:
|
inputs:
|
||||||
- name: input
|
- name: input
|
||||||
required: true
|
required: true
|
||||||
messages:
|
messages:
|
||||||
- role: user
|
- role: user
|
||||||
content: 'Input={{input "input"}} Label={{.label}}'
|
content: 'Input={{input "input"}} Label={{.label}}'
|
||||||
description: ` + label + `
|
description: ` + label + `
|
||||||
output:
|
output:
|
||||||
format: json
|
format: json
|
||||||
validation_mode: json_schema
|
validation_mode: json_schema
|
||||||
schema_path: schema.json
|
schema_path: schema.json
|
||||||
`)},
|
`)},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -137,11 +137,12 @@ func TestPreparedObservationFinishesAfterCredentialAndCapacityFailures(t *testin
|
|||||||
"profiles": map[string]any{"profile": map[string]any{
|
"profiles": map[string]any{"profile": map[string]any{
|
||||||
"endpoint": "https://example.invalid/v1", "model": "model", "api_key_required": true,
|
"endpoint": "https://example.invalid/v1", "model": "model", "api_key_required": true,
|
||||||
}},
|
}},
|
||||||
"prompts": map[string]any{"prepared": map[string]any{"1": map[string]any{
|
"prompts": map[string]any{"prepared": map[string]any{
|
||||||
|
"version": "1",
|
||||||
"default_profile": "profile",
|
"default_profile": "profile",
|
||||||
"messages": []any{map[string]any{"role": "user", "content": "hello"}},
|
"messages": []any{map[string]any{"role": "user", "content": "hello"}},
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||||
}}},
|
}},
|
||||||
}),
|
}),
|
||||||
}}}, promptkit.WithLLMClient(&fakeLLMClient{response: &promptkit.GenerateResponse{Content: "ok"}}))
|
}}}, promptkit.WithLLMClient(&fakeLLMClient{response: &promptkit.GenerateResponse{Content: "ok"}}))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -13,11 +13,11 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func aliasPrompt() map[string]any {
|
func aliasPrompt() map[string]any {
|
||||||
return map[string]any{"prompts": map[string]any{"prepared": map[string]any{"1": map[string]any{
|
return map[string]any{"prompts": map[string]any{"prepared": promptDefinitionWithVersion("1", map[string]any{
|
||||||
"default_profile": "child",
|
"default_profile": "child",
|
||||||
"messages": []any{map[string]any{"role": "user", "content": "hello"}},
|
"messages": []any{map[string]any{"role": "user", "content": "hello"}},
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||||
}}}}
|
})}}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestProfileInheritanceBuiltInAliasWorkflow(t *testing.T) {
|
func TestProfileInheritanceBuiltInAliasWorkflow(t *testing.T) {
|
||||||
|
|||||||
@@ -20,11 +20,12 @@ func TestInspectProfileUsesLiteralFileKeyForPreparation(t *testing.T) {
|
|||||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||||
Underlays: []configkit.Underlay{{Inputs: profiles}},
|
Underlays: []configkit.Underlay{{Inputs: profiles}},
|
||||||
Main: &configkit.Input{Source: configkit.Mapping("prompt", map[string]any{
|
Main: &configkit.Input{Source: configkit.Mapping("prompt", map[string]any{
|
||||||
"prompts": map[string]any{"prompt": map[string]any{"1": map[string]any{
|
"prompts": map[string]any{"prompt": map[string]any{
|
||||||
|
"version": "1",
|
||||||
"default_profile": " literal ",
|
"default_profile": " literal ",
|
||||||
"messages": []any{map[string]any{"role": "user", "content": "hello"}},
|
"messages": []any{map[string]any{"role": "user", "content": "hello"}},
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||||
}}},
|
}},
|
||||||
})},
|
})},
|
||||||
}})
|
}})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -86,11 +86,12 @@ func reliabilityConfig(backends, profiles map[string]any) promptkit.Config {
|
|||||||
return promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{Source: configkit.Mapping("reliable execution", map[string]any{
|
return promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &configkit.Input{Source: configkit.Mapping("reliable execution", map[string]any{
|
||||||
"backends": backends,
|
"backends": backends,
|
||||||
"profiles": profiles,
|
"profiles": profiles,
|
||||||
"prompts": map[string]any{"prompt": map[string]any{"1": map[string]any{
|
"prompts": map[string]any{"prompt": map[string]any{
|
||||||
|
"version": "1",
|
||||||
"default_profile": "profile-a",
|
"default_profile": "profile-a",
|
||||||
"messages": []any{map[string]any{"role": "user", "content": "hello"}},
|
"messages": []any{map[string]any{"role": "user", "content": "hello"}},
|
||||||
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
"output": map[string]any{"format": "text", "validation_mode": "none"},
|
||||||
}}},
|
}},
|
||||||
})}}}
|
})}}}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,14 +253,14 @@ func TestPreparedRetriesFreezeSourceAndCredentialAtExecution(t *testing.T) {
|
|||||||
t.Setenv(envName, "first-key")
|
t.Setenv(envName, "first-key")
|
||||||
source := fstest.MapFS{"prompt.yaml": &fstest.MapFile{Data: []byte(`prompts:
|
source := fstest.MapFS{"prompt.yaml": &fstest.MapFile{Data: []byte(`prompts:
|
||||||
prompt:
|
prompt:
|
||||||
"1":
|
version: "1"
|
||||||
default_profile: profile
|
default_profile: profile
|
||||||
messages:
|
messages:
|
||||||
- role: user
|
- role: user
|
||||||
content: original message
|
content: original message
|
||||||
output:
|
output:
|
||||||
format: text
|
format: text
|
||||||
validation_mode: none
|
validation_mode: none
|
||||||
`)}}
|
`)}}
|
||||||
var calls int
|
var calls int
|
||||||
var auth, content []string
|
var auth, content []string
|
||||||
@@ -306,14 +307,14 @@ func TestPreparedRetriesFreezeSourceAndCredentialAtExecution(t *testing.T) {
|
|||||||
}
|
}
|
||||||
source["prompt.yaml"] = &fstest.MapFile{Data: []byte(`prompts:
|
source["prompt.yaml"] = &fstest.MapFile{Data: []byte(`prompts:
|
||||||
prompt:
|
prompt:
|
||||||
"1":
|
version: "1"
|
||||||
default_profile: profile
|
default_profile: profile
|
||||||
messages:
|
messages:
|
||||||
- role: user
|
- role: user
|
||||||
content: changed message
|
content: changed message
|
||||||
output:
|
output:
|
||||||
format: text
|
format: text
|
||||||
validation_mode: none
|
validation_mode: none
|
||||||
`)}
|
`)}
|
||||||
result, err := engine.RunPrepared(context.Background(), prepared)
|
result, err := engine.RunPrepared(context.Background(), prepared)
|
||||||
if err != nil || result == nil || calls != 2 {
|
if err != nil || result == nil || calls != 2 {
|
||||||
|
|||||||
@@ -52,7 +52,16 @@ func contractPrompt(defaultProfile string, content string, inputs []any, session
|
|||||||
}
|
}
|
||||||
|
|
||||||
func oneVersionPrompt(id, version string, definition map[string]any) map[string]any {
|
func oneVersionPrompt(id, version string, definition map[string]any) map[string]any {
|
||||||
return map[string]any{id: map[string]any{version: definition}}
|
return map[string]any{id: promptDefinitionWithVersion(version, definition)}
|
||||||
|
}
|
||||||
|
|
||||||
|
func promptDefinitionWithVersion(version string, definition map[string]any) map[string]any {
|
||||||
|
result := make(map[string]any, len(definition)+1)
|
||||||
|
result["version"] = version
|
||||||
|
for key, value := range definition {
|
||||||
|
result[key] = value
|
||||||
|
}
|
||||||
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
var noValidationOutput = map[string]any{"format": "text", "validation_mode": "none"}
|
var noValidationOutput = map[string]any{"format": "text", "validation_mode": "none"}
|
||||||
@@ -123,16 +132,12 @@ func TestInspectRequirementsEnumeratesCapturedDependenciesAndInactiveNames(t *te
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestInspectRequirementsSelectionUsesExactVersionsAndProfileIdentity(t *testing.T) {
|
func TestInspectRequirementsUsesPromptIDsAndProfileIdentity(t *testing.T) {
|
||||||
prompts := map[string]any{
|
prompts := map[string]any{
|
||||||
"multi": map[string]any{
|
"first-prompt": promptDefinitionWithVersion("v1", contractPrompt("first", `{{input "one"}}`, nil, "", noValidationOutput)),
|
||||||
"v1": contractPrompt("first", `{{input "one"}}`, nil, "", noValidationOutput),
|
"second-prompt": promptDefinitionWithVersion("v2", contractPrompt("second", `{{input "two"}}`, nil, "", noValidationOutput)),
|
||||||
"v2": contractPrompt("second", `{{input "two"}}`, nil, "", noValidationOutput),
|
"without-default": contractPrompt("", "plain", nil, "", noValidationOutput),
|
||||||
},
|
"direct-required": promptDefinitionWithVersion("v1", contractPrompt("direct", "plain", nil, "", noValidationOutput)),
|
||||||
"without-default": map[string]any{
|
|
||||||
"v1": contractPrompt("", "plain", nil, "", noValidationOutput),
|
|
||||||
},
|
|
||||||
"direct-required": map[string]any{"v1": contractPrompt("direct", "plain", nil, "", noValidationOutput)},
|
|
||||||
}
|
}
|
||||||
profiles := map[string]any{
|
profiles := map[string]any{
|
||||||
"first": map[string]any{"model": "first-model"},
|
"first": map[string]any{"model": "first-model"},
|
||||||
@@ -140,26 +145,20 @@ func TestInspectRequirementsSelectionUsesExactVersionsAndProfileIdentity(t *test
|
|||||||
"direct": map[string]any{"endpoint": "https://example.test/v1", "model": "direct-model", "api_key_required": true},
|
"direct": map[string]any{"endpoint": "https://example.test/v1", "model": "direct-model", "api_key_required": true},
|
||||||
}
|
}
|
||||||
engine := requirementsEngine(t, prompts, profiles)
|
engine := requirementsEngine(t, prompts, profiles)
|
||||||
if _, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "multi"}); !errors.Is(err, promptkit.ErrPromptLoad) {
|
first, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "first-prompt"})
|
||||||
t.Fatalf("ambiguous omitted version error=%v", err)
|
|
||||||
}
|
|
||||||
first, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "multi", PromptVersion: "v1"})
|
|
||||||
if err != nil || first.PromptVersion != "v1" || first.ProfileID != "first" || len(first.Inputs) != 1 || first.Inputs[0].Name != "one" {
|
if err != nil || first.PromptVersion != "v1" || first.ProfileID != "first" || len(first.Inputs) != 1 || first.Inputs[0].Name != "one" {
|
||||||
t.Fatalf("exact first version/default profile=(%+v, %v)", first, err)
|
t.Fatalf("first prompt ID/default profile=(%+v, %v)", first, err)
|
||||||
}
|
}
|
||||||
explicit, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{
|
second, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{
|
||||||
PromptID: "multi", PromptVersion: "v2", ProfileID: "first",
|
PromptID: "second-prompt", ProfileID: "first",
|
||||||
})
|
})
|
||||||
if err != nil || explicit.PromptVersion != "v2" || explicit.ProfileID != "first" || explicit.Inputs[0].Name != "two" {
|
if err != nil || second.PromptVersion != "v2" || second.ProfileID != "first" || second.Inputs[0].Name != "two" {
|
||||||
t.Fatalf("exact version/request profile=(%+v, %v)", explicit, err)
|
t.Fatalf("second prompt ID/request profile=(%+v, %v)", second, err)
|
||||||
}
|
}
|
||||||
if _, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "Multi", PromptVersion: "v1"}); !errors.Is(err, promptkit.ErrPromptNotFound) {
|
if _, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "First-Prompt"}); !errors.Is(err, promptkit.ErrPromptNotFound) {
|
||||||
t.Fatalf("case-folded prompt ID unexpectedly selected: %v", err)
|
t.Fatalf("case-folded prompt ID unexpectedly selected: %v", err)
|
||||||
}
|
}
|
||||||
if _, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "multi", PromptVersion: "V1"}); !errors.Is(err, promptkit.ErrPromptNotFound) {
|
if _, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "first-prompt", ProfileID: "missing"}); !errors.Is(err, promptkit.ErrProfileNotFound) {
|
||||||
t.Fatalf("case-folded version unexpectedly selected: %v", err)
|
|
||||||
}
|
|
||||||
if _, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "multi", PromptVersion: "v1", ProfileID: "missing"}); !errors.Is(err, promptkit.ErrProfileNotFound) {
|
|
||||||
t.Fatalf("missing profile error=%v", err)
|
t.Fatalf("missing profile error=%v", err)
|
||||||
}
|
}
|
||||||
if _, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "without-default"}); !errors.Is(err, promptkit.ErrProfileRequired) || !errors.Is(err, promptkit.ErrInvalidRequest) {
|
if _, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "without-default"}); !errors.Is(err, promptkit.ErrProfileRequired) || !errors.Is(err, promptkit.ErrInvalidRequest) {
|
||||||
@@ -173,7 +172,7 @@ func TestInspectRequirementsSelectionUsesExactVersionsAndProfileIdentity(t *test
|
|||||||
}
|
}
|
||||||
ctx, cancel := context.WithCancel(context.Background())
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
cancel()
|
cancel()
|
||||||
if _, err := engine.InspectRequirements(ctx, promptkit.RequirementInspectionRequest{PromptID: "multi", PromptVersion: "v1"}); !errors.Is(err, context.Canceled) {
|
if _, err := engine.InspectRequirements(ctx, promptkit.RequirementInspectionRequest{PromptID: "first-prompt"}); !errors.Is(err, context.Canceled) {
|
||||||
t.Fatalf("canceled inspection error=%v", err)
|
t.Fatalf("canceled inspection error=%v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -202,11 +201,11 @@ func TestInspectPromptHashMatchesPreparationForCapturedConfiguration(t *testing.
|
|||||||
|
|
||||||
func TestInspectRequirementsReportsConditionalAndCompleteOptionalInventories(t *testing.T) {
|
func TestInspectRequirementsReportsConditionalAndCompleteOptionalInventories(t *testing.T) {
|
||||||
prompts := map[string]any{
|
prompts := map[string]any{
|
||||||
"conditional": map[string]any{"v1": contractPrompt("profile",
|
"conditional": promptDefinitionWithVersion("v1", contractPrompt("profile",
|
||||||
`{{if .mode}}{{input "left"}}{{else}}{{input "right"}}{{end}}`, nil, "", noValidationOutput)},
|
`{{if .mode}}{{input "left"}}{{else}}{{input "right"}}{{end}}`, nil, "", noValidationOutput)),
|
||||||
"optional": map[string]any{"v1": contractPrompt("profile", `{{index . "lookup"}}`,
|
"optional": promptDefinitionWithVersion("v1", contractPrompt("profile", `{{index . "lookup"}}`,
|
||||||
[]any{map[string]any{"name": "declared-optional"}}, "", noValidationOutput)},
|
[]any{map[string]any{"name": "declared-optional"}}, "", noValidationOutput)),
|
||||||
"empty": map[string]any{"v1": contractPrompt("profile", "plain", nil, "", noValidationOutput)},
|
"empty": promptDefinitionWithVersion("v1", contractPrompt("profile", "plain", nil, "", noValidationOutput)),
|
||||||
}
|
}
|
||||||
engine := requirementsEngine(t, prompts, map[string]any{"profile": map[string]any{"model": "partial"}})
|
engine := requirementsEngine(t, prompts, map[string]any{"profile": map[string]any{"model": "partial"}})
|
||||||
conditional, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "conditional"})
|
conditional, err := engine.InspectRequirements(context.Background(), promptkit.RequirementInspectionRequest{PromptID: "conditional"})
|
||||||
|
|||||||
@@ -252,7 +252,8 @@ func TestSmokeTestUsesPrivateInferenceWithoutApplicationResourcesOrListingChange
|
|||||||
client := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: outputMarker}}
|
client := &fakeLLMClient{response: &promptkit.GenerateResponse{Content: outputMarker}}
|
||||||
input := configkit.Input{Source: configkit.Mapping("private diagnostic test", map[string]any{
|
input := configkit.Input{Source: configkit.Mapping("private diagnostic test", map[string]any{
|
||||||
"profiles": map[string]any{"profile": map[string]any{"endpoint": "http://example.test/v1", "model": "model"}},
|
"profiles": map[string]any{"profile": map[string]any{"endpoint": "http://example.test/v1", "model": "model"}},
|
||||||
"prompts": map[string]any{"application-prompt": map[string]any{"1": map[string]any{
|
"prompts": map[string]any{"application-prompt": map[string]any{
|
||||||
|
"version": "1",
|
||||||
"default_profile": "profile",
|
"default_profile": "profile",
|
||||||
"session_id": `application-session {{input "document"}}`,
|
"session_id": `application-session {{input "document"}}`,
|
||||||
"inputs": []any{map[string]any{"name": "document", "required": true}},
|
"inputs": []any{map[string]any{"name": "document", "required": true}},
|
||||||
@@ -263,7 +264,7 @@ func TestSmokeTestUsesPrivateInferenceWithoutApplicationResourcesOrListingChange
|
|||||||
"output": map[string]any{
|
"output": map[string]any{
|
||||||
"format": "json", "validation_mode": "json_schema", "schema_path": schemaMarker,
|
"format": "json", "validation_mode": "json_schema", "schema_path": schemaMarker,
|
||||||
},
|
},
|
||||||
}}},
|
}},
|
||||||
})}
|
})}
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}},
|
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}},
|
||||||
promptkit.WithLLMClient(client), promptkit.WithArtifactReader(reader), promptkit.WithSchemaFS(schemaSource, "schemas"))
|
promptkit.WithLLMClient(client), promptkit.WithArtifactReader(reader), promptkit.WithSchemaFS(schemaSource, "schemas"))
|
||||||
|
|||||||
Reference in New Issue
Block a user