Allow prompt version selection without inputs
This commit is contained in:
14
docs/api.md
14
docs/api.md
@@ -32,14 +32,11 @@ returns `413 response_too_large`.
|
|||||||
### Request Body
|
### Request Body
|
||||||
|
|
||||||
The maintained [request example](../examples/http-run.json) is a complete
|
The maintained [request example](../examples/http-run.json) is a complete
|
||||||
copyable shape. The smallest valid shape is:
|
copyable shape. At the HTTP adapter boundary, the smallest valid shape is:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
"prompt_id": "generic.markdown_summary",
|
"prompt_id": "generic.markdown_summary"
|
||||||
"inputs": {
|
|
||||||
"transcript": {"type": "inline", "body": "Source text"}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -48,7 +45,7 @@ copyable shape. The smallest valid shape is:
|
|||||||
| `prompt_id` | yes | Non-blank prompt ID. |
|
| `prompt_id` | yes | Non-blank prompt ID. |
|
||||||
| `prompt_version` | no | Prompt version filter. |
|
| `prompt_version` | no | Prompt version filter. |
|
||||||
| `profile_id` | no | Execution-profile ID; otherwise the prompt must set `default_profile`. |
|
| `profile_id` | no | Execution-profile ID; otherwise the prompt must set `default_profile`. |
|
||||||
| `inputs` | yes | Non-empty object mapping input names to references. |
|
| `inputs` | no | Optional object mapping input names to references. Promptkit decides whether the selected definition needs them. |
|
||||||
| `vars` | no | Object mapping template-variable names to strings. |
|
| `vars` | no | Object mapping template-variable names to strings. |
|
||||||
| `model` | no | Runtime model-override object. |
|
| `model` | no | Runtime model-override object. |
|
||||||
| `include_raw_output` | no | Include `raw_model_output` when true. |
|
| `include_raw_output` | no | Include `raw_model_output` when true. |
|
||||||
@@ -83,8 +80,9 @@ empty string explicitly clears it. JSON `null` is treated as omission.
|
|||||||
### Strict JSON
|
### Strict JSON
|
||||||
|
|
||||||
Request decoding rejects malformed JSON, unknown fields at every request level,
|
Request decoding rejects malformed JSON, unknown fields at every request level,
|
||||||
and trailing JSON tokens with `400 invalid_json`. A blank `prompt_id` or
|
and trailing JSON tokens with `400 invalid_json`. A blank `prompt_id` returns
|
||||||
empty `inputs` object returns `400 invalid_request`.
|
`400 invalid_request`. Omitted or empty `inputs` are passed to Promptkit, which
|
||||||
|
reports any definition-required or template-referenced inputs.
|
||||||
|
|
||||||
### Success Response
|
### Success Response
|
||||||
|
|
||||||
|
|||||||
14
docs/cli.md
14
docs/cli.md
@@ -39,7 +39,6 @@ Required flags:
|
|||||||
| Flag | Meaning |
|
| Flag | Meaning |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `--prompt <id>` | Prompt ID to execute. |
|
| `--prompt <id>` | Prompt ID to execute. |
|
||||||
| `--input name=path` | Input file mapping; repeat or use comma-separated mappings. |
|
|
||||||
|
|
||||||
Optional flags:
|
Optional flags:
|
||||||
|
|
||||||
@@ -49,7 +48,9 @@ Optional flags:
|
|||||||
| `--prompt-dir <dir>` | Prompt-definition directory override. |
|
| `--prompt-dir <dir>` | Prompt-definition directory override. |
|
||||||
| `--profile-dir <dir>` | Custom profile-directory override. |
|
| `--profile-dir <dir>` | Custom profile-directory override. |
|
||||||
| `--schema-dir <dir>` | Schema base-directory override. |
|
| `--schema-dir <dir>` | Schema base-directory override. |
|
||||||
|
| `--prompt-version <version>` | Optional prompt-definition version selector. |
|
||||||
| `--profile <id>` | Execution-profile override. |
|
| `--profile <id>` | Execution-profile override. |
|
||||||
|
| `--input name=path` | Optional input file mapping; repeat or use comma-separated mappings. |
|
||||||
| `--var name=value` | Template-variable mapping; repeat or use comma-separated mappings. |
|
| `--var name=value` | Template-variable mapping; repeat or use comma-separated mappings. |
|
||||||
| `--out <path>` | Write generated content to this file instead of stdout. |
|
| `--out <path>` | Write generated content to this file instead of stdout. |
|
||||||
| `--llm-base-url <url>` | Runtime endpoint override. |
|
| `--llm-base-url <url>` | Runtime endpoint override. |
|
||||||
@@ -73,15 +74,20 @@ zero-second override. The timeout layers are defined in the
|
|||||||
|
|
||||||
There is no raw API-key flag. Use `--api-key-env`.
|
There is no raw API-key flag. Use `--api-key-env`.
|
||||||
|
|
||||||
|
`--prompt-version` is passed directly to Promptkit. When it is omitted, the
|
||||||
|
selected prompt ID must have exactly one available version. `--input` is
|
||||||
|
optional at the CLI boundary: Promptkit decides whether the selected definition
|
||||||
|
requires declared inputs or template-referenced values.
|
||||||
|
|
||||||
## `scriptorium render`
|
## `scriptorium render`
|
||||||
|
|
||||||
```text
|
```text
|
||||||
scriptorium render [flags]
|
scriptorium render [flags]
|
||||||
```
|
```
|
||||||
|
|
||||||
`--prompt <id>` and at least one `--input name=path` are required. The
|
`--prompt <id>` is required. The following optional flags are supported:
|
||||||
following optional flags are supported: `--config`, `--prompt-dir`,
|
`--config`, `--prompt-dir`, `--profile-dir`, `--prompt-version`, `--profile`,
|
||||||
`--profile-dir`, `--profile`, `--var`, `--out`, `--llm-base-url`,
|
`--input`, `--var`, `--out`, `--llm-base-url`,
|
||||||
`--model`, `--api-key-env`, `--temperature`, `--max-tokens`, `--top-p`,
|
`--model`, `--api-key-env`, `--temperature`, `--max-tokens`, `--top-p`,
|
||||||
`--timeout`, and `--format text|json`. Their meanings match the corresponding
|
`--timeout`, and `--format text|json`. Their meanings match the corresponding
|
||||||
`run` flags; `--format` selects prepared-run output and otherwise uses
|
`run` flags; `--format` selects prepared-run output and otherwise uses
|
||||||
|
|||||||
@@ -29,10 +29,10 @@ described by its tagged
|
|||||||
### CLI
|
### CLI
|
||||||
|
|
||||||
`run` calls `promptkit.Engine.Run`; `render` calls
|
`run` calls `promptkit.Engine.Run`; `render` calls
|
||||||
`promptkit.Engine.Prepare`. Both share request mapping for prompt/profile
|
`promptkit.Engine.Prepare`. Both share request mapping for prompt ID/version
|
||||||
selection, file inputs, variables, and presence-aware execution overrides.
|
and profile selection, optional file inputs, variables, and presence-aware
|
||||||
Omitted framework settings remain zero values so Promptkit resolves its own
|
execution overrides. Omitted framework settings remain zero values so Promptkit
|
||||||
defaults.
|
resolves its own defaults and definition-required inputs.
|
||||||
|
|
||||||
`serve` constructs Scriptorium's restricted HTTP artifact reader, injects it
|
`serve` constructs Scriptorium's restricted HTTP artifact reader, injects it
|
||||||
with `promptkit.WithArtifactReader`, passes the engine through the HTTP
|
with `promptkit.WithArtifactReader`, passes the engine through the HTTP
|
||||||
|
|||||||
@@ -95,6 +95,8 @@ private Promptkit integration mechanism has been introduced.
|
|||||||
|
|
||||||
## Stage 2: Remove Definition-Compatibility Restrictions In Run And Render
|
## Stage 2: Remove Definition-Compatibility Restrictions In Run And Render
|
||||||
|
|
||||||
|
**Completion: Complete.**
|
||||||
|
|
||||||
Allow the executable adapters to address every valid directory-backed prompt
|
Allow the executable adapters to address every valid directory-backed prompt
|
||||||
definition without imposing input requirements of their own.
|
definition without imposing input requirements of their own.
|
||||||
|
|
||||||
|
|||||||
@@ -32,21 +32,22 @@ const (
|
|||||||
type runConfig struct {
|
type runConfig struct {
|
||||||
configPath string
|
configPath string
|
||||||
|
|
||||||
promptDir string
|
promptDir string
|
||||||
profileDir string
|
profileDir string
|
||||||
promptID string
|
promptID string
|
||||||
profileID string
|
promptVersion string
|
||||||
inputRaw listFlag
|
profileID string
|
||||||
varRaw listFlag
|
inputRaw listFlag
|
||||||
outputPath string
|
varRaw listFlag
|
||||||
llmBaseURL string
|
outputPath string
|
||||||
apiKeyEnv string
|
llmBaseURL string
|
||||||
model string
|
apiKeyEnv string
|
||||||
temperature float64
|
model string
|
||||||
maxTokens int
|
temperature float64
|
||||||
topP float64
|
maxTokens int
|
||||||
schemaDir string
|
topP float64
|
||||||
timeout time.Duration
|
schemaDir string
|
||||||
|
timeout time.Duration
|
||||||
|
|
||||||
defaultRenderFormat renderformat.PreparedRunOutputFormat
|
defaultRenderFormat renderformat.PreparedRunOutputFormat
|
||||||
|
|
||||||
@@ -349,6 +350,7 @@ func registerExecutionRequestFlags(fs *flag.FlagSet, cfg *runConfig) {
|
|||||||
fs.StringVar(&cfg.promptDir, "prompt-dir", "", "directory containing prompt definition YAML files")
|
fs.StringVar(&cfg.promptDir, "prompt-dir", "", "directory containing prompt definition YAML files")
|
||||||
fs.StringVar(&cfg.profileDir, "profile-dir", "", "directory containing execution profile YAML files")
|
fs.StringVar(&cfg.profileDir, "profile-dir", "", "directory containing execution profile YAML files")
|
||||||
fs.StringVar(&cfg.promptID, "prompt", "", "prompt ID to run")
|
fs.StringVar(&cfg.promptID, "prompt", "", "prompt ID to run")
|
||||||
|
fs.StringVar(&cfg.promptVersion, "prompt-version", "", "optional prompt definition version")
|
||||||
fs.StringVar(&cfg.profileID, "profile", "", "optional execution profile ID; if omitted, prompt default_profile is used")
|
fs.StringVar(&cfg.profileID, "profile", "", "optional execution profile ID; if omitted, prompt default_profile is used")
|
||||||
fs.Var(&cfg.inputRaw, "input", "input mapping(s): name=path (repeatable, comma-separated)")
|
fs.Var(&cfg.inputRaw, "input", "input mapping(s): name=path (repeatable, comma-separated)")
|
||||||
fs.Var(&cfg.varRaw, "var", "variable mapping(s): name=value (repeatable, comma-separated)")
|
fs.Var(&cfg.varRaw, "var", "variable mapping(s): name=value (repeatable, comma-separated)")
|
||||||
@@ -389,9 +391,6 @@ func finalizeExecutionRequestConfig(fs *flag.FlagSet, cfg *runConfig) error {
|
|||||||
if strings.TrimSpace(cfg.promptID) == "" {
|
if strings.TrimSpace(cfg.promptID) == "" {
|
||||||
return errors.New("--prompt is required")
|
return errors.New("--prompt is required")
|
||||||
}
|
}
|
||||||
if len(cfg.inputRaw) == 0 {
|
|
||||||
return errors.New("at least one --input is required")
|
|
||||||
}
|
|
||||||
cfg.promptDir = filepath.Clean(cfg.promptDir)
|
cfg.promptDir = filepath.Clean(cfg.promptDir)
|
||||||
if strings.TrimSpace(cfg.profileDir) != "" {
|
if strings.TrimSpace(cfg.profileDir) != "" {
|
||||||
cfg.profileDir = filepath.Clean(cfg.profileDir)
|
cfg.profileDir = filepath.Clean(cfg.profileDir)
|
||||||
@@ -547,9 +546,13 @@ func newEngine(cfg *runConfig, options ...promptkit.Option) (*promptkit.Engine,
|
|||||||
}
|
}
|
||||||
|
|
||||||
func buildRunRequestFromConfig(cfg *runConfig) (promptkit.RunRequest, error) {
|
func buildRunRequestFromConfig(cfg *runConfig) (promptkit.RunRequest, error) {
|
||||||
inputMappings, err := parseMappings(cfg.inputRaw, false)
|
var inputMappings map[string]string
|
||||||
if err != nil {
|
var err error
|
||||||
return promptkit.RunRequest{}, fmt.Errorf("input parse error: %w", err)
|
if len(cfg.inputRaw) > 0 {
|
||||||
|
inputMappings, err = parseMappings(cfg.inputRaw, false)
|
||||||
|
if err != nil {
|
||||||
|
return promptkit.RunRequest{}, fmt.Errorf("input parse error: %w", err)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
varMappings := map[string]string{}
|
varMappings := map[string]string{}
|
||||||
@@ -560,9 +563,12 @@ func buildRunRequestFromConfig(cfg *runConfig) (promptkit.RunRequest, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
inputs := make(map[string]promptkit.ArtifactRef, len(inputMappings))
|
var inputs map[string]promptkit.ArtifactRef
|
||||||
for name, path := range inputMappings {
|
if len(inputMappings) > 0 {
|
||||||
inputs[name] = promptkit.File(path)
|
inputs = make(map[string]promptkit.ArtifactRef, len(inputMappings))
|
||||||
|
for name, path := range inputMappings {
|
||||||
|
inputs[name] = promptkit.File(path)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var modelOverride *promptkit.ExecutionTargetOverride
|
var modelOverride *promptkit.ExecutionTargetOverride
|
||||||
@@ -588,11 +594,12 @@ func buildRunRequestFromConfig(cfg *runConfig) (promptkit.RunRequest, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return promptkit.RunRequest{
|
return promptkit.RunRequest{
|
||||||
PromptID: cfg.promptID,
|
PromptID: cfg.promptID,
|
||||||
ProfileID: cfg.profileID,
|
PromptVersion: cfg.promptVersion,
|
||||||
Inputs: inputs,
|
ProfileID: cfg.profileID,
|
||||||
Vars: varMappings,
|
Inputs: inputs,
|
||||||
Execution: modelOverride,
|
Vars: varMappings,
|
||||||
|
Execution: modelOverride,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -687,7 +694,7 @@ func printSummary(stderr io.Writer, res *promptkit.RunResult) {
|
|||||||
|
|
||||||
func printUsage(w io.Writer) {
|
func printUsage(w io.Writer) {
|
||||||
fmt.Fprintln(w, "usage: scriptorium <run|render|serve> ...")
|
fmt.Fprintln(w, "usage: scriptorium <run|render|serve> ...")
|
||||||
fmt.Fprintln(w, " run: scriptorium run [--config PATH] [--prompt-dir DIR] [--profile-dir DIR] --prompt ID --input name=path [--input ...] [--profile ID] [--llm-base-url URL] [--model NAME] [--api-key-env ENV] [--temperature N] [--max-tokens N] [--top-p N] [--var k=v] [--out path] [--timeout 10m]")
|
fmt.Fprintln(w, " run: scriptorium run [--config PATH] [--prompt-dir DIR] [--profile-dir DIR] --prompt ID [--prompt-version VERSION] [--input name=path] [--profile ID] [--llm-base-url URL] [--model NAME] [--api-key-env ENV] [--temperature N] [--max-tokens N] [--top-p N] [--var k=v] [--out path] [--timeout 10m]")
|
||||||
fmt.Fprintln(w, " render: scriptorium render [--config PATH] [--prompt-dir DIR] [--profile-dir DIR] --prompt ID --input name=path [--input ...] [--profile ID] [--llm-base-url URL] [--model NAME] [--api-key-env ENV] [--temperature N] [--max-tokens N] [--top-p N] [--var k=v] [--format text|json] [--out path] [--timeout 10m]")
|
fmt.Fprintln(w, " render: scriptorium render [--config PATH] [--prompt-dir DIR] [--profile-dir DIR] --prompt ID [--prompt-version VERSION] [--input name=path] [--profile ID] [--llm-base-url URL] [--model NAME] [--api-key-env ENV] [--temperature N] [--max-tokens N] [--top-p N] [--var k=v] [--format text|json] [--out path] [--timeout 10m]")
|
||||||
fmt.Fprintf(w, " serve: scriptorium serve [--config PATH] [--addr %s] [--prompt-dir DIR] [--profile-dir DIR] [--schema-dir DIR] [--artifact-root DIR] [--max-request-bytes N] [--max-artifact-bytes N] [--max-response-bytes N]\n", defaults.HTTPAddrDefault)
|
fmt.Fprintf(w, " serve: scriptorium serve [--config PATH] [--addr %s] [--prompt-dir DIR] [--profile-dir DIR] [--schema-dir DIR] [--artifact-root DIR] [--max-request-bytes N] [--max-artifact-bytes N] [--max-response-bytes N]\n", defaults.HTTPAddrDefault)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -87,9 +87,12 @@ func TestParseRunArgsRequiredFlags(t *testing.T) {
|
|||||||
t.Fatal("expected missing --prompt error")
|
t.Fatal("expected missing --prompt error")
|
||||||
}
|
}
|
||||||
|
|
||||||
_, err = parseRunArgs([]string{"--config", configPath, "--prompt-dir", "./prompts", "--profile-dir", "./profiles", "--prompt", "p"})
|
cfg, err = parseRunArgs([]string{"--config", configPath, "--prompt-dir", "./prompts", "--profile-dir", "./profiles", "--prompt", "p"})
|
||||||
if err == nil {
|
if err != nil {
|
||||||
t.Fatal("expected missing --input error")
|
t.Fatalf("expected omitted --input to be accepted, got %v", err)
|
||||||
|
}
|
||||||
|
if len(cfg.inputRaw) != 0 {
|
||||||
|
t.Fatalf("expected no input mappings, got %#v", cfg.inputRaw)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,6 +101,7 @@ func TestParseRunArgsFlagMapping(t *testing.T) {
|
|||||||
"--prompt-dir", "./prompts",
|
"--prompt-dir", "./prompts",
|
||||||
"--profile-dir", "./profiles",
|
"--profile-dir", "./profiles",
|
||||||
"--prompt", "prompt.a",
|
"--prompt", "prompt.a",
|
||||||
|
"--prompt-version", "2",
|
||||||
"--profile", "profile.a",
|
"--profile", "profile.a",
|
||||||
"--input", "a=b",
|
"--input", "a=b",
|
||||||
"--llm-base-url", "http://x/v1",
|
"--llm-base-url", "http://x/v1",
|
||||||
@@ -114,8 +118,8 @@ func TestParseRunArgsFlagMapping(t *testing.T) {
|
|||||||
if cfg.promptDir != filepath.Clean("./prompts") || cfg.profileDir != filepath.Clean("./profiles") {
|
if cfg.promptDir != filepath.Clean("./prompts") || cfg.profileDir != filepath.Clean("./profiles") {
|
||||||
t.Fatalf("unexpected dirs: prompt=%q profile=%q", cfg.promptDir, cfg.profileDir)
|
t.Fatalf("unexpected dirs: prompt=%q profile=%q", cfg.promptDir, cfg.profileDir)
|
||||||
}
|
}
|
||||||
if cfg.promptID != "prompt.a" || cfg.profileID != "profile.a" {
|
if cfg.promptID != "prompt.a" || cfg.promptVersion != "2" || cfg.profileID != "profile.a" {
|
||||||
t.Fatalf("unexpected prompt/profile ids: %q %q", cfg.promptID, cfg.profileID)
|
t.Fatalf("unexpected prompt/version/profile ids: %q %q %q", cfg.promptID, cfg.promptVersion, cfg.profileID)
|
||||||
}
|
}
|
||||||
if !cfg.llmBaseURLSet || !cfg.modelSet || !cfg.temperatureSet || !cfg.maxTokensSet || !cfg.topPSet || !cfg.timeoutSet || !cfg.apiKeyEnvSet {
|
if !cfg.llmBaseURLSet || !cfg.modelSet || !cfg.temperatureSet || !cfg.maxTokensSet || !cfg.topPSet || !cfg.timeoutSet || !cfg.apiKeyEnvSet {
|
||||||
t.Fatalf("expected override flags set, got %+v", cfg)
|
t.Fatalf("expected override flags set, got %+v", cfg)
|
||||||
@@ -192,7 +196,7 @@ func TestParseServeArgsRejectsRuntimeOverrideFlags(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestUsageIncludesServeFileAndSizeLimitFlags(t *testing.T) {
|
func TestUsageIncludesExecutionAndServeFlags(t *testing.T) {
|
||||||
var stderr bytes.Buffer
|
var stderr bytes.Buffer
|
||||||
code := Run(nil, io.Discard, &stderr)
|
code := Run(nil, io.Discard, &stderr)
|
||||||
if code != ExitRuntimeError {
|
if code != ExitRuntimeError {
|
||||||
@@ -201,6 +205,7 @@ func TestUsageIncludesServeFileAndSizeLimitFlags(t *testing.T) {
|
|||||||
|
|
||||||
usage := stderr.String()
|
usage := stderr.String()
|
||||||
for _, want := range []string{
|
for _, want := range []string{
|
||||||
|
"--prompt-version VERSION",
|
||||||
"--artifact-root",
|
"--artifact-root",
|
||||||
"--max-request-bytes",
|
"--max-request-bytes",
|
||||||
"--max-artifact-bytes",
|
"--max-artifact-bytes",
|
||||||
@@ -689,6 +694,22 @@ func TestBuildRunRequestPreservesNumericOverridePresence(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildRunRequestAllowsOmittedInputsAndMapsPromptVersion(t *testing.T) {
|
||||||
|
req, err := buildRunRequestFromConfig(&runConfig{
|
||||||
|
promptID: "prompt-1",
|
||||||
|
promptVersion: "2",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("expected request without inputs to build, got %v", err)
|
||||||
|
}
|
||||||
|
if req.PromptVersion != "2" {
|
||||||
|
t.Fatalf("expected prompt version to be mapped, got %q", req.PromptVersion)
|
||||||
|
}
|
||||||
|
if req.Inputs != nil {
|
||||||
|
t.Fatalf("expected omitted inputs to remain nil, got %#v", req.Inputs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestParseRunArgsFailsClearlyWhenNoEffectivePromptDir(t *testing.T) {
|
func TestParseRunArgsFailsClearlyWhenNoEffectivePromptDir(t *testing.T) {
|
||||||
configPath := writeAppConfigFile(t, `
|
configPath := writeAppConfigFile(t, `
|
||||||
profile_dir: ./profiles
|
profile_dir: ./profiles
|
||||||
@@ -1122,6 +1143,104 @@ func TestRenderCommandExplicitProfileOverridesPromptDefault(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestRenderCommandUsesDefinitionInputRulesAndPromptVersions(t *testing.T) {
|
||||||
|
lib := newCLITestLibrary(t)
|
||||||
|
writeProfileFile(t, lib.profileDir, "local", "http://127.0.0.1:1/v1", "model")
|
||||||
|
|
||||||
|
writePromptDefinition(t, lib.promptDir, "sole.yaml", `id: sole
|
||||||
|
version: "1"
|
||||||
|
default_profile: local
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content: "hello"
|
||||||
|
output:
|
||||||
|
format: text
|
||||||
|
validation_mode: none
|
||||||
|
`)
|
||||||
|
writePromptDefinition(t, lib.promptDir, "versioned-one.yaml", `id: versioned
|
||||||
|
version: "1"
|
||||||
|
default_profile: local
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content: "one"
|
||||||
|
output:
|
||||||
|
format: text
|
||||||
|
validation_mode: none
|
||||||
|
`)
|
||||||
|
writePromptDefinition(t, lib.promptDir, "versioned-two.yaml", `id: versioned
|
||||||
|
version: "2"
|
||||||
|
default_profile: local
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content: "two"
|
||||||
|
output:
|
||||||
|
format: text
|
||||||
|
validation_mode: none
|
||||||
|
`)
|
||||||
|
writePromptDefinition(t, lib.promptDir, "optional.yaml", `id: optional
|
||||||
|
version: "1"
|
||||||
|
default_profile: local
|
||||||
|
inputs:
|
||||||
|
- name: note
|
||||||
|
required: false
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content: "hello"
|
||||||
|
output:
|
||||||
|
format: text
|
||||||
|
validation_mode: none
|
||||||
|
`)
|
||||||
|
writePromptDefinition(t, lib.promptDir, "required.yaml", `id: required
|
||||||
|
version: "1"
|
||||||
|
default_profile: local
|
||||||
|
inputs:
|
||||||
|
- name: note
|
||||||
|
required: true
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content: "hello"
|
||||||
|
output:
|
||||||
|
format: text
|
||||||
|
validation_mode: none
|
||||||
|
`)
|
||||||
|
writePromptDefinition(t, lib.promptDir, "template.yaml", `id: template
|
||||||
|
version: "1"
|
||||||
|
default_profile: local
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content: '{{input "note"}}'
|
||||||
|
output:
|
||||||
|
format: text
|
||||||
|
validation_mode: none
|
||||||
|
`)
|
||||||
|
|
||||||
|
baseArgs := []string{"--prompt-dir", lib.promptDir, "--profile-dir", lib.profileDir}
|
||||||
|
for _, tc := range []struct {
|
||||||
|
name string
|
||||||
|
args []string
|
||||||
|
wantCode int
|
||||||
|
wantText string
|
||||||
|
}{
|
||||||
|
{name: "sole version selected when omitted", args: []string{"--prompt", "sole"}, wantCode: ExitOK, wantText: "prompt_version: 1"},
|
||||||
|
{name: "explicit version selected", args: []string{"--prompt", "versioned", "--prompt-version", "2"}, wantCode: ExitOK, wantText: "prompt_version: 2"},
|
||||||
|
{name: "multiple versions require selection", args: []string{"--prompt", "versioned"}, wantCode: ExitRuntimeError, wantText: "duplicate prompt definition id"},
|
||||||
|
{name: "no declared inputs", args: []string{"--prompt", "sole"}, wantCode: ExitOK, wantText: "prompt: sole"},
|
||||||
|
{name: "optional input omitted", args: []string{"--prompt", "optional"}, wantCode: ExitOK, wantText: "prompt: optional"},
|
||||||
|
{name: "required input omitted", args: []string{"--prompt", "required"}, wantCode: ExitRuntimeError, wantText: "required"},
|
||||||
|
{name: "template input omitted", args: []string{"--prompt", "template"}, wantCode: ExitRuntimeError, wantText: "note"},
|
||||||
|
} {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
code, stdout, stderr := runCLICommand(t, renderCommand, append(append([]string{}, baseArgs...), tc.args...))
|
||||||
|
if code != tc.wantCode {
|
||||||
|
t.Fatalf("expected exit %d, got %d stderr=%q", tc.wantCode, code, stderr)
|
||||||
|
}
|
||||||
|
if !strings.Contains(stdout+stderr, tc.wantText) {
|
||||||
|
t.Fatalf("expected output to contain %q, stdout=%q stderr=%q", tc.wantText, stdout, stderr)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRunCommandPromptDefaultProfileWorksThroughCLIPath(t *testing.T) {
|
func TestRunCommandPromptDefaultProfileWorksThroughCLIPath(t *testing.T) {
|
||||||
lib := newCLITestLibrary(t)
|
lib := newCLITestLibrary(t)
|
||||||
inputPath := lib.writeInputFile(t, "transcript.md", "hello")
|
inputPath := lib.writeInputFile(t, "transcript.md", "hello")
|
||||||
@@ -1340,6 +1459,13 @@ func writePromptFile(t *testing.T, dir, id, defaultProfile string) {
|
|||||||
writePromptFileWithTemplate(t, dir, id, defaultProfile, "Summarize: {{input \"transcript\"}}")
|
writePromptFileWithTemplate(t, dir, id, defaultProfile, "Summarize: {{input \"transcript\"}}")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func writePromptDefinition(t *testing.T, dir, name, definition string) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.WriteFile(filepath.Join(dir, name), []byte(definition), 0o644); err != nil {
|
||||||
|
t.Fatalf("write prompt definition: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func writePromptFileWithTemplate(t *testing.T, dir, id, defaultProfile, templateContent string) {
|
func writePromptFileWithTemplate(t *testing.T, dir, id, defaultProfile, templateContent string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
data := fmt.Sprintf(`id: %s
|
data := fmt.Sprintf(`id: %s
|
||||||
|
|||||||
@@ -76,17 +76,15 @@ func (h *Handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||||||
writeError(w, http.StatusBadRequest, "invalid_request", "prompt_id is required")
|
writeError(w, http.StatusBadRequest, "invalid_request", "prompt_id is required")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if len(req.Inputs) == 0 {
|
var mappedInputs map[string]promptkit.ArtifactRef
|
||||||
writeError(w, http.StatusBadRequest, "invalid_request", "inputs is required")
|
if len(req.Inputs) > 0 {
|
||||||
return
|
mappedInputs = make(map[string]promptkit.ArtifactRef, len(req.Inputs))
|
||||||
}
|
for name, in := range req.Inputs {
|
||||||
|
mappedInputs[name] = promptkit.ArtifactRef{
|
||||||
mappedInputs := make(map[string]promptkit.ArtifactRef, len(req.Inputs))
|
Type: promptkit.ArtifactRefType(in.Type),
|
||||||
for name, in := range req.Inputs {
|
URI: in.URI,
|
||||||
mappedInputs[name] = promptkit.ArtifactRef{
|
Body: in.Body,
|
||||||
Type: promptkit.ArtifactRefType(in.Type),
|
}
|
||||||
URI: in.URI,
|
|
||||||
Body: in.Body,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -192,6 +192,124 @@ func TestHandlerPostRunsSuccessWithExplicitProfileID(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestHandlerAllowsOmittedInputsAndMapsPromptVersion(t *testing.T) {
|
||||||
|
r := &fakeRunner{result: &promptkit.RunResult{
|
||||||
|
Artifact: promptkit.Artifact{Body: []byte("ok")},
|
||||||
|
Validation: promptkit.ValidationResult{Status: promptkit.ValidationPassed, Mode: promptkit.ValidationNone, IsValid: true},
|
||||||
|
EffectiveModelParams: promptkit.ExecutionTarget{Endpoint: "http://llm/v1", Model: "m1"},
|
||||||
|
}}
|
||||||
|
h := NewHandler(r)
|
||||||
|
|
||||||
|
for _, body := range []string{
|
||||||
|
`{"prompt_id":"prompt-1","prompt_version":"2"}`,
|
||||||
|
`{"prompt_id":"prompt-1","prompt_version":"2","inputs":{}}`,
|
||||||
|
} {
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/v1/runs", bytes.NewBufferString(body))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != http.StatusOK {
|
||||||
|
t.Fatalf("expected 200, got %d body=%s", w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
if r.last.PromptVersion != "2" {
|
||||||
|
t.Fatalf("expected prompt version to be mapped, got %q", r.last.PromptVersion)
|
||||||
|
}
|
||||||
|
if r.last.Inputs != nil {
|
||||||
|
t.Fatalf("expected omitted or empty inputs to remain nil, got %#v", r.last.Inputs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestHandlerDelegatesDefinitionInputRequirements(t *testing.T) {
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
definition string
|
||||||
|
wantStatus int
|
||||||
|
wantCode string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no declared inputs",
|
||||||
|
definition: `id: p
|
||||||
|
version: "1"
|
||||||
|
default_profile: exec
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content: "hello"
|
||||||
|
output:
|
||||||
|
format: text
|
||||||
|
validation_mode: none
|
||||||
|
`,
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "optional input omitted",
|
||||||
|
definition: `id: p
|
||||||
|
version: "1"
|
||||||
|
default_profile: exec
|
||||||
|
inputs:
|
||||||
|
- name: note
|
||||||
|
required: false
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content: "hello"
|
||||||
|
output:
|
||||||
|
format: text
|
||||||
|
validation_mode: none
|
||||||
|
`,
|
||||||
|
wantStatus: http.StatusOK,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "required input omitted",
|
||||||
|
definition: `id: p
|
||||||
|
version: "1"
|
||||||
|
default_profile: exec
|
||||||
|
inputs:
|
||||||
|
- name: note
|
||||||
|
required: true
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content: "hello"
|
||||||
|
output:
|
||||||
|
format: text
|
||||||
|
validation_mode: none
|
||||||
|
`,
|
||||||
|
wantStatus: http.StatusBadRequest,
|
||||||
|
wantCode: "prompt_render_failed",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "template input omitted",
|
||||||
|
definition: `id: p
|
||||||
|
version: "1"
|
||||||
|
default_profile: exec
|
||||||
|
messages:
|
||||||
|
- role: user
|
||||||
|
content: '{{input "note"}}'
|
||||||
|
output:
|
||||||
|
format: text
|
||||||
|
validation_mode: none
|
||||||
|
`,
|
||||||
|
wantStatus: http.StatusBadRequest,
|
||||||
|
wantCode: "prompt_render_failed",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range tests {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
h := newDefinitionHandler(t, tc.definition)
|
||||||
|
req := httptest.NewRequest(http.MethodPost, "/v1/runs", bytes.NewBufferString(`{"prompt_id":"p"}`))
|
||||||
|
w := httptest.NewRecorder()
|
||||||
|
h.ServeHTTP(w, req)
|
||||||
|
|
||||||
|
if w.Code != tc.wantStatus {
|
||||||
|
t.Fatalf("expected %d, got %d body=%s", tc.wantStatus, w.Code, w.Body.String())
|
||||||
|
}
|
||||||
|
if tc.wantCode != "" {
|
||||||
|
assertHTTPErrorCode(t, w, tc.wantStatus, tc.wantCode)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestHandlerInlineRefsWorkWithoutArtifactRoot(t *testing.T) {
|
func TestHandlerInlineRefsWorkWithoutArtifactRoot(t *testing.T) {
|
||||||
h := newArtifactRootHandler(t, "")
|
h := newArtifactRootHandler(t, "")
|
||||||
|
|
||||||
@@ -931,6 +1049,31 @@ model: model
|
|||||||
return engine
|
return engine
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func newDefinitionHandler(t *testing.T, definition string) *Handler {
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
|
promptDir := t.TempDir()
|
||||||
|
profileDir := t.TempDir()
|
||||||
|
if err := os.WriteFile(filepath.Join(promptDir, "prompt.yaml"), []byte(definition), 0o644); err != nil {
|
||||||
|
t.Fatalf("write prompt fixture: %v", err)
|
||||||
|
}
|
||||||
|
if err := os.WriteFile(filepath.Join(profileDir, "profile.yaml"), []byte(`id: exec
|
||||||
|
endpoint: http://example.invalid/v1
|
||||||
|
model: model
|
||||||
|
`), 0o644); err != nil {
|
||||||
|
t.Fatalf("write profile fixture: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
engine, err := promptkit.NewEngine(promptkit.Config{
|
||||||
|
PromptDir: promptDir,
|
||||||
|
ProfileDir: profileDir,
|
||||||
|
}, promptkit.WithLLMClient(handlerLLMClient{}))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("construct public engine: %v", err)
|
||||||
|
}
|
||||||
|
return NewHandler(engine)
|
||||||
|
}
|
||||||
|
|
||||||
func assertHTTPErrorCode(t *testing.T, w *httptest.ResponseRecorder, status int, code string) {
|
func assertHTTPErrorCode(t *testing.T, w *httptest.ResponseRecorder, status int, code string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user