Expose prompt requirement inspection
This commit is contained in:
17
doc.go
17
doc.go
@@ -4,7 +4,7 @@
|
|||||||
// Applications construct an [Engine] with [NewEngine], select ConfigKit
|
// Applications construct an [Engine] with [NewEngine], select ConfigKit
|
||||||
// sources for prompts, profiles, and backends, and
|
// sources for prompts, profiles, and backends, and
|
||||||
// call [Engine.InspectConfiguration], [Engine.InspectPrompt],
|
// call [Engine.InspectConfiguration], [Engine.InspectPrompt],
|
||||||
// [Engine.InspectProfile], [Engine.Prepare],
|
// [Engine.InspectRequirements], [Engine.InspectProfile], [Engine.Prepare],
|
||||||
// [Engine.PrepareExecution], [Engine.Run], or [Engine.RunPrepared]. Concrete
|
// [Engine.PrepareExecution], [Engine.Run], or [Engine.RunPrepared]. Concrete
|
||||||
// registries, repositories, validators, and the built-in OpenAI-compatible
|
// registries, repositories, validators, and the built-in OpenAI-compatible
|
||||||
// client remain internal implementation details.
|
// client remain internal implementation details.
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
// # Concurrency and ownership
|
// # Concurrency and ownership
|
||||||
//
|
//
|
||||||
// An Engine supports concurrent InspectConfiguration, InspectPrompt,
|
// An Engine supports concurrent InspectConfiguration, InspectPrompt,
|
||||||
// InspectProfile, Prepare,
|
// InspectRequirements, InspectProfile, Prepare,
|
||||||
// PrepareExecution, Run, and RunPrepared calls. Engine-local backend policies
|
// PrepareExecution, Run, and RunPrepared calls. Engine-local backend policies
|
||||||
// bound admitted Run and RunPrepared calls and model generations where
|
// bound admitted Run and RunPrepared calls and model generations where
|
||||||
// configured, while different backend pools and unlimited backends continue
|
// configured, while different backend pools and unlimited backends continue
|
||||||
@@ -20,11 +20,11 @@
|
|||||||
// still receive concurrent calls and must be safe for that use.
|
// still receive concurrent calls and must be safe for that use.
|
||||||
//
|
//
|
||||||
// ConfigKit captures the selected configuration when NewEngine constructs an
|
// ConfigKit captures the selected configuration when NewEngine constructs an
|
||||||
// Engine. Prepare,
|
// Engine. Prepare, PrepareExecution, and Run copy request maps, slices, pointer
|
||||||
// PrepareExecution, and Run copy request maps, slices, pointer values, and
|
// values, and JSON-compatible extra parameters before using them. InspectPrompt,
|
||||||
// JSON-compatible extra parameters before using them. InspectPrompt and
|
// InspectRequirements, and InspectProfile return copied inspection values.
|
||||||
// InspectProfile return copied inspection values. Returned values and values
|
// Returned values and values passed to extension interfaces are likewise
|
||||||
// passed to extension interfaces are likewise isolated from engine state.
|
// isolated from engine state.
|
||||||
// Callers own those copies and may mutate them after the call that supplied or
|
// Callers own those copies and may mutate them after the call that supplied or
|
||||||
// returned them. [CapacityError] values are caller-owned and may be mutated
|
// returned them. [CapacityError] values are caller-owned and may be mutated
|
||||||
// without affecting engine state or another error. Immutable [GenerationError]
|
// without affecting engine state or another error. Immutable [GenerationError]
|
||||||
@@ -58,7 +58,8 @@
|
|||||||
// [ConfigurationSources], [ConfigurationInspection], [BackendConfig],
|
// [ConfigurationSources], [ConfigurationInspection], [BackendConfig],
|
||||||
// [ProfileConfig], [RunRequest],
|
// [ProfileConfig], [RunRequest],
|
||||||
// [ArtifactRef], [ExecutionTargetOverride], [ProfileInspection],
|
// [ArtifactRef], [ExecutionTargetOverride], [ProfileInspection],
|
||||||
// [PromptInputDefinition], [PromptInspection], [PreparedExecution],
|
// [PromptInputDefinition], [PromptInspection], [RequirementInspectionRequest],
|
||||||
|
// [RequirementInspection], [PreparedExecution],
|
||||||
// [CapacityError], [GenerationError], [RetryExhaustedError], and
|
// [CapacityError], [GenerationError], [RetryExhaustedError], and
|
||||||
// [ExecutionEvent], do not have stable JSON
|
// [ExecutionEvent], do not have stable JSON
|
||||||
// representations. Direct API keys are nevertheless excluded from JSON for
|
// representations. Direct API keys are nevertheless excluded from JSON for
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ contributor workflow and validation.
|
|||||||
|
|
||||||
| Component | Implemented responsibility | References |
|
| Component | Implemented responsibility | References |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Root `promptkit` package | Provides the supported engine facade, ConfigKit source helpers, profile and backend mapping conveniences, injection options, public requests and results, explicit inspection, observation values, opaque prepared-execution handles, value conversion, redacted formatting, and typed capacity and generation error mapping. | [Package GoDoc](../../doc.go), [observations](../../observations.go), [prepared execution](../../prepared_execution.go), [backend API](../../backends.go), [engine facade](../../engine.go) |
|
| Root `promptkit` package | Provides the supported engine facade, ConfigKit source helpers, profile and backend mapping conveniences, injection options, public requests and results, prompt/profile/requirement inspection, observation values, opaque prepared-execution handles, value conversion, redacted formatting, and typed capacity and generation error mapping. | [Package GoDoc](../../doc.go), [observations](../../observations.go), [prepared execution](../../prepared_execution.go), [backend API](../../backends.go), [engine facade](../../engine.go), [requirement facade](../../requirements.go) |
|
||||||
| `examples/go-library/prepare` | Demonstrates offline preparation and explicit configuration inspection with a prompt file, programmatic profile, and inline input. It is not a public library package. | [Example program](../../examples/go-library/prepare/main.go) |
|
| `examples/go-library/prepare` | Demonstrates offline preparation and explicit configuration inspection with a prompt file, programmatic profile, and inline input. It is not a public library package. | [Example program](../../examples/go-library/prepare/main.go) |
|
||||||
| `examples/go-library/run` | Demonstrates an offline downstream consumer using a prompt file, programmatic profile, inline input, an injected deterministic model client, and `Run`. It is not a public library package. | [Example program](../../examples/go-library/run/main.go) |
|
| `examples/go-library/run` | Demonstrates an offline downstream consumer using a prompt file, programmatic profile, inline input, an injected deterministic model client, and `Run`. It is not a public library package. | [Example program](../../examples/go-library/run/main.go) |
|
||||||
| `examples/go-library/observe` | Demonstrates offline built-in HTTP retry and synchronous metadata observation across structured proposal and assessment runs using embedded sources, two profiles on one finite backend, and an application decision. It is not a public library package. | [Example program](../../examples/go-library/observe/main.go) |
|
| `examples/go-library/observe` | Demonstrates offline built-in HTTP retry and synchronous metadata observation across structured proposal and assessment runs using embedded sources, two profiles on one finite backend, and an application decision. It is not a public library package. | [Example program](../../examples/go-library/observe/main.go) |
|
||||||
@@ -32,7 +32,7 @@ contributor workflow and validation.
|
|||||||
| `internal/validate` | Validates basic, JSON, and JSON Schema output using operating-system filesystem or `fs.FS` schema sources and creates operation-local validation plans with canonical contained schema resources. | [Framework formats](../formats.md#schemas), [internal sources and validation](sources.md) |
|
| `internal/validate` | Validates basic, JSON, and JSON Schema output using operating-system filesystem or `fs.FS` schema sources and creates operation-local validation plans with canonical contained schema resources. | [Framework formats](../formats.md#schemas), [internal sources and validation](sources.md) |
|
||||||
| `internal/llm` | Defines the internal generation boundary and implements outbound OpenAI-compatible chat requests from resolved execution targets, including bounded structured non-success response decoding, successful-response decoding, authentication, deadline handling, and ownership of the OpenAI-compatible reserved request-field policy. | [Internal model client](llm.md) |
|
| `internal/llm` | Defines the internal generation boundary and implements outbound OpenAI-compatible chat requests from resolved execution targets, including bounded structured non-success response decoding, successful-response decoding, authentication, deadline handling, and ownership of the OpenAI-compatible reserved request-field policy. | [Internal model client](llm.md) |
|
||||||
| `internal/observation` | Owns invocation-local run IDs, presence-aware usage and phase accounting, and synchronous metadata event delivery for ordinary and claimed prepared runs and generation attempts. | [Internal execution observation](observation.md) |
|
| `internal/observation` | Owns invocation-local run IDs, presence-aware usage and phase accounting, and synchronous metadata event delivery for ordinary and claimed prepared runs and generation attempts. | [Internal execution observation](observation.md) |
|
||||||
| `internal/usecase` | Selects captured prompts and profiles, applies request overrides and completion checks, and coordinates ordinary and single-claim prepared execution across rendering, artifact loading, operation-local validation plans, generation, capacity, and bounded repair. | [Internal runner](runner.md), [prepared-execution implementation](../../internal/usecase/prepared_execution.go) |
|
| `internal/usecase` | Selects captured prompts and profiles, enumerates requirements from captured prompt syntax, applies request overrides and completion checks, and coordinates ordinary and single-claim prepared execution across rendering, artifact loading, operation-local validation plans, generation, capacity, and bounded repair. | [Internal runner](runner.md), [requirement inspection](../../internal/usecase/requirements_inspection.go), [prepared-execution implementation](../../internal/usecase/prepared_execution.go) |
|
||||||
|
|
||||||
The internal engine assembles these internal components without exposing their
|
The internal engine assembles these internal components without exposing their
|
||||||
representations. Consumers depend only on the root facade.
|
representations. Consumers depend only on the root facade.
|
||||||
|
|||||||
@@ -43,6 +43,15 @@ touch profile, artifact, schema, renderer, validator, admission, or model
|
|||||||
collaborators. The root [`Engine.InspectPrompt`](../../engine.go) GoDoc owns
|
collaborators. The root [`Engine.InspectPrompt`](../../engine.go) GoDoc owns
|
||||||
the public operation's exact contract.
|
the public operation's exact contract.
|
||||||
|
|
||||||
|
`InspectRequirements` uses the same prompt selection boundary, then selects the
|
||||||
|
request profile or prompt default for identity and obtains the exact captured
|
||||||
|
requirement snapshot. It analyzes that snapshot with the supplied caller
|
||||||
|
variables and normalized direct-session bypass; it does not resolve target
|
||||||
|
readiness or enter the preparation pipeline. Missing snapshot capability is a
|
||||||
|
prompt-load failure rather than a source fallback. The root
|
||||||
|
[`Engine.InspectRequirements`](../../requirements.go) GoDoc owns the public
|
||||||
|
operation's exact contract.
|
||||||
|
|
||||||
## Shared Profile Selection
|
## Shared Profile Selection
|
||||||
|
|
||||||
The runner uses one profile-selection and target-resolution boundary for
|
The runner uses one profile-selection and target-resolution boundary for
|
||||||
|
|||||||
@@ -340,6 +340,10 @@ in results/explanations. Run `go test ./internal/prompt`; commit.
|
|||||||
|
|
||||||
## Stage 5 — Wire Selection And The Public Facade
|
## Stage 5 — Wire Selection And The Public Facade
|
||||||
|
|
||||||
|
**Status: Complete (2026-10-08).** The root API now selects captured prompt and
|
||||||
|
profile identities, analyzes the exact captured snapshot, and returns
|
||||||
|
caller-owned requirement values without entering preparation or execution.
|
||||||
|
|
||||||
**Outcome:** Consumers can inspect a selected captured prompt before artifacts
|
**Outcome:** Consumers can inspect a selected captured prompt before artifacts
|
||||||
exist, without execution collaborators or readiness checks.
|
exist, without execution collaborators or readiness checks.
|
||||||
|
|
||||||
|
|||||||
@@ -82,12 +82,12 @@ var (
|
|||||||
ErrValidation = errors.New("failed to validate output")
|
ErrValidation = errors.New("failed to validate output")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Engine inspects prompts and profiles and prepares and runs Promptkit prompt
|
// Engine inspects prompts and profiles, enumerates prompt requirements, and
|
||||||
// requests.
|
// prepares and runs Promptkit prompt requests.
|
||||||
//
|
//
|
||||||
// An Engine is safe for concurrent calls to [Engine.InspectConfiguration],
|
// An Engine is safe for concurrent calls to [Engine.InspectConfiguration],
|
||||||
// [Engine.InspectPrompt],
|
// [Engine.InspectPrompt],
|
||||||
// [Engine.InspectProfile], [Engine.Prepare], [Engine.PrepareExecution],
|
// [Engine.InspectRequirements], [Engine.InspectProfile], [Engine.Prepare], [Engine.PrepareExecution],
|
||||||
// [Engine.Run], and [Engine.RunPrepared]. Each Engine owns independent
|
// [Engine.Run], and [Engine.RunPrepared]. Each Engine owns independent
|
||||||
// backend-capacity pools that coordinate Run and RunPrepared admission and
|
// backend-capacity pools that coordinate Run and RunPrepared admission and
|
||||||
// model generation. Injected collaborators may still be invoked concurrently
|
// model generation. Injected collaborators may still be invoked concurrently
|
||||||
|
|||||||
@@ -178,6 +178,12 @@ func fileSource(name string) (fs.FS, string, string, error) {
|
|||||||
func (e *Engine) InspectPrompt(ctx context.Context, id, version string) (*domain.PromptInspection, error) {
|
func (e *Engine) InspectPrompt(ctx context.Context, id, version string) (*domain.PromptInspection, error) {
|
||||||
return e.runner.InspectPrompt(ctx, id, version)
|
return e.runner.InspectPrompt(ctx, id, version)
|
||||||
}
|
}
|
||||||
|
func (e *Engine) InspectRequirements(
|
||||||
|
ctx context.Context,
|
||||||
|
request usecase.RequirementInspectionRequest,
|
||||||
|
) (*domain.RequirementInspection, error) {
|
||||||
|
return e.runner.InspectRequirements(ctx, request)
|
||||||
|
}
|
||||||
func (e *Engine) InspectProfile(ctx context.Context, id string) (*domain.ProfileInspection, error) {
|
func (e *Engine) InspectProfile(ctx context.Context, id string) (*domain.ProfileInspection, error) {
|
||||||
return e.runner.InspectProfile(ctx, id)
|
return e.runner.InspectProfile(ctx, id)
|
||||||
}
|
}
|
||||||
|
|||||||
79
internal/usecase/requirements_inspection.go
Normal file
79
internal/usecase/requirements_inspection.go
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package usecase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
||||||
|
"gitea.maximumdirect.net/eric/promptkit/internal/prompt"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RequirementInspectionRequest selects a captured prompt and supplies the
|
||||||
|
// context used for dependency classification.
|
||||||
|
type RequirementInspectionRequest struct {
|
||||||
|
PromptID string
|
||||||
|
PromptVersion string
|
||||||
|
ProfileID string
|
||||||
|
Vars map[string]string
|
||||||
|
SessionID string
|
||||||
|
}
|
||||||
|
|
||||||
|
type requirementSnapshotRepository interface {
|
||||||
|
GetRequirementSnapshot(id, version string) (prompt.RequirementSnapshot, bool)
|
||||||
|
}
|
||||||
|
|
||||||
|
// InspectRequirements discovers prompt inputs and caller variables without
|
||||||
|
// loading artifacts or performing execution preparation.
|
||||||
|
func (r *Runner) InspectRequirements(
|
||||||
|
ctx context.Context,
|
||||||
|
req RequirementInspectionRequest,
|
||||||
|
) (*domain.RequirementInspection, error) {
|
||||||
|
if strings.TrimSpace(req.PromptID) == "" {
|
||||||
|
return nil, fmt.Errorf("%w: prompt id is required", ErrInvalidRequest)
|
||||||
|
}
|
||||||
|
directSessionID, err := domain.NormalizeSessionID(req.SessionID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: session_id: %v", ErrInvalidRequest, err)
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
selection, err := r.resolvePromptDefinition(ctx, req.PromptID, req.PromptVersion)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %w", ErrProfileLoad, err)
|
||||||
|
}
|
||||||
|
selectedProfileID := req.ProfileID
|
||||||
|
if strings.TrimSpace(selectedProfileID) == "" {
|
||||||
|
selectedProfileID = selection.definition.DefaultProfile
|
||||||
|
}
|
||||||
|
if strings.TrimSpace(selectedProfileID) == "" {
|
||||||
|
return nil, fmt.Errorf("%w: %w: profile id is required either in request or prompt default_profile", ErrInvalidRequest, ErrProfileRequired)
|
||||||
|
}
|
||||||
|
profileSelection, err := r.resolveProfileSelection(ctx, selectedProfileID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := ctx.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
repository, ok := r.promptDefs.(requirementSnapshotRepository)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%w: prompt repository does not provide captured requirement snapshots", ErrPromptLoad)
|
||||||
|
}
|
||||||
|
snapshot, ok := repository.GetRequirementSnapshot(selection.definition.ID, selection.definition.Version)
|
||||||
|
if !ok {
|
||||||
|
return nil, fmt.Errorf("%w: captured requirement snapshot is unavailable", ErrPromptLoad)
|
||||||
|
}
|
||||||
|
inspection, err := prompt.AnalyzeRequirements(ctx, selection.definition, snapshot, req.Vars, directSessionID != "")
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, err)
|
||||||
|
}
|
||||||
|
inspection.ProfileID = profileSelection.id
|
||||||
|
return &inspection, nil
|
||||||
|
}
|
||||||
230
internal/usecase/requirements_inspection_test.go
Normal file
230
internal/usecase/requirements_inspection_test.go
Normal file
@@ -0,0 +1,230 @@
|
|||||||
|
package usecase
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
"text/template"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
||||||
|
"gitea.maximumdirect.net/eric/promptkit/internal/prompt"
|
||||||
|
"gitea.maximumdirect.net/eric/promptkit/internal/promptdef"
|
||||||
|
)
|
||||||
|
|
||||||
|
type requirementInspectionPromptRepository struct {
|
||||||
|
definition *domain.PromptDefinition
|
||||||
|
snapshot prompt.RequirementSnapshot
|
||||||
|
lookupErr error
|
||||||
|
snapshotFound bool
|
||||||
|
lookupCalls int
|
||||||
|
snapshotCalls int
|
||||||
|
lookupID string
|
||||||
|
lookupVersion string
|
||||||
|
snapshotID string
|
||||||
|
snapshotVersion string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repository *requirementInspectionPromptRepository) GetPromptDefinition(
|
||||||
|
context.Context,
|
||||||
|
string,
|
||||||
|
string,
|
||||||
|
) (*domain.PromptDefinition, error) {
|
||||||
|
return repository.definition, repository.lookupErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repository *requirementInspectionPromptRepository) GetPromptSnapshot(
|
||||||
|
_ context.Context,
|
||||||
|
id string,
|
||||||
|
version string,
|
||||||
|
) (*domain.PromptDefinition, string, error) {
|
||||||
|
repository.lookupCalls++
|
||||||
|
repository.lookupID = id
|
||||||
|
repository.lookupVersion = version
|
||||||
|
return repository.definition, "captured-hash", repository.lookupErr
|
||||||
|
}
|
||||||
|
|
||||||
|
func (repository *requirementInspectionPromptRepository) GetRequirementSnapshot(
|
||||||
|
id string,
|
||||||
|
version string,
|
||||||
|
) (prompt.RequirementSnapshot, bool) {
|
||||||
|
repository.snapshotCalls++
|
||||||
|
repository.snapshotID = id
|
||||||
|
repository.snapshotVersion = version
|
||||||
|
return repository.snapshot, repository.snapshotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunnerInspectRequirementsUsesCapturedSelectionWithoutExecutionWork(t *testing.T) {
|
||||||
|
message, err := prompt.CompileTemplate("message", `{{input "message-input"}} {{.caller}}`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
session, err := prompt.CompileTemplate("session", `{{input "session-only"}}`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
definition := &domain.PromptDefinition{ID: "selected", Version: "v2", DefaultProfile: "default-profile"}
|
||||||
|
repository := &requirementInspectionPromptRepository{
|
||||||
|
definition: definition,
|
||||||
|
snapshotFound: true,
|
||||||
|
snapshot: prompt.RequirementSnapshot{Templates: prompt.CompiledTemplates{
|
||||||
|
Messages: []*template.Template{message}, Session: session,
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
profiles := &inspectionProfileRepository{profile: &domain.ExecutionProfile{
|
||||||
|
ID: "partial-profile", ResolvedTarget: &domain.ExecutionTarget{Endpoint: ""},
|
||||||
|
}}
|
||||||
|
runner := &Runner{promptDefs: repository, profiles: profiles}
|
||||||
|
inspection, err := runner.InspectRequirements(context.Background(), RequirementInspectionRequest{
|
||||||
|
PromptID: " requested ", PromptVersion: " exact-version ", ProfileID: "partial-profile",
|
||||||
|
Vars: map[string]string{"caller": ""}, SessionID: " direct-session ",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("inspect requirements: %v", err)
|
||||||
|
}
|
||||||
|
if repository.lookupCalls != 1 || repository.lookupID != " requested " || repository.lookupVersion != " exact-version " ||
|
||||||
|
repository.snapshotCalls != 1 || repository.snapshotID != definition.ID || repository.snapshotVersion != definition.Version {
|
||||||
|
t.Fatalf("selection lookup=(%d,%q,%q), snapshot=(%d,%q,%q)", repository.lookupCalls, repository.lookupID,
|
||||||
|
repository.lookupVersion, repository.snapshotCalls, repository.snapshotID, repository.snapshotVersion)
|
||||||
|
}
|
||||||
|
if profiles.calls != 1 || profiles.id != "partial-profile" || inspection.PromptID != definition.ID ||
|
||||||
|
inspection.PromptVersion != definition.Version || inspection.ProfileID != "partial-profile" || !inspection.Complete ||
|
||||||
|
len(inspection.Inputs) != 1 || inspection.Inputs[0].Name != "message-input" || len(inspection.Variables) != 1 ||
|
||||||
|
inspection.Variables[0].Name != "caller" || !inspection.Variables[0].Supplied {
|
||||||
|
t.Fatalf("unexpected requirement inspection: %+v", inspection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunnerInspectRequirementsUsesPromptDefaultAndIncludesSessionWhenNotBypassed(t *testing.T) {
|
||||||
|
message, err := prompt.CompileTemplate("message", `{{input "message-input"}}`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
session, err := prompt.CompileTemplate("session", `{{input "session-input"}}`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
definition := &domain.PromptDefinition{ID: "selected", Version: "v1", DefaultProfile: "default-profile"}
|
||||||
|
repository := &requirementInspectionPromptRepository{
|
||||||
|
definition: definition, snapshotFound: true,
|
||||||
|
snapshot: prompt.RequirementSnapshot{Templates: prompt.CompiledTemplates{Messages: []*template.Template{message}, Session: session}},
|
||||||
|
}
|
||||||
|
profiles := &inspectionProfileRepository{profile: &domain.ExecutionProfile{
|
||||||
|
ID: "default-profile", ResolvedTarget: &domain.ExecutionTarget{},
|
||||||
|
}}
|
||||||
|
inspection, err := (&Runner{promptDefs: repository, profiles: profiles}).InspectRequirements(
|
||||||
|
context.Background(), RequirementInspectionRequest{PromptID: "selected"},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("inspect requirements: %v", err)
|
||||||
|
}
|
||||||
|
if profiles.id != "default-profile" || inspection.ProfileID != "default-profile" || len(inspection.Inputs) != 2 ||
|
||||||
|
inspection.Inputs[0].Name != "message-input" || inspection.Inputs[1].Name != "session-input" {
|
||||||
|
t.Fatalf("prompt-default profile or session template was not selected: %+v", inspection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunnerInspectRequirementsReturnsIncompleteDiscoveryAsSuccess(t *testing.T) {
|
||||||
|
compiled, err := prompt.CompileTemplate("message", `{{if .missing}}{{input "left"}}{{else}}{{input "right"}}{{end}}`)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
repository := &requirementInspectionPromptRepository{
|
||||||
|
definition: &domain.PromptDefinition{ID: "selected", Version: "v1", DefaultProfile: "profile"},
|
||||||
|
snapshotFound: true,
|
||||||
|
snapshot: prompt.RequirementSnapshot{Templates: prompt.CompiledTemplates{Messages: []*template.Template{compiled}}},
|
||||||
|
}
|
||||||
|
profiles := &inspectionProfileRepository{profile: &domain.ExecutionProfile{ID: "profile", ResolvedTarget: &domain.ExecutionTarget{}}}
|
||||||
|
inspection, err := (&Runner{promptDefs: repository, profiles: profiles}).InspectRequirements(
|
||||||
|
context.Background(), RequirementInspectionRequest{PromptID: "selected"},
|
||||||
|
)
|
||||||
|
if err != nil || inspection == nil || inspection.Complete || len(inspection.Issues) != 1 ||
|
||||||
|
inspection.Issues[0].Code != domain.RequirementIssueMissingVariable {
|
||||||
|
t.Fatalf("incomplete discovery returned (%+v, %v), want successful partial result", inspection, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestRunnerInspectRequirementsClassifiesSelectionAndSnapshotFailures(t *testing.T) {
|
||||||
|
t.Run("blank prompt", func(t *testing.T) {
|
||||||
|
repository := &requirementInspectionPromptRepository{}
|
||||||
|
_, err := (&Runner{promptDefs: repository}).InspectRequirements(context.Background(), RequirementInspectionRequest{PromptID: " \t "})
|
||||||
|
if !errors.Is(err, ErrInvalidRequest) || repository.lookupCalls != 0 {
|
||||||
|
t.Fatalf("blank prompt error=%v lookups=%d", err, repository.lookupCalls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("invalid session", func(t *testing.T) {
|
||||||
|
repository := &requirementInspectionPromptRepository{}
|
||||||
|
_, err := (&Runner{promptDefs: repository}).InspectRequirements(context.Background(), RequirementInspectionRequest{
|
||||||
|
PromptID: "selected", SessionID: string([]byte{0xff}),
|
||||||
|
})
|
||||||
|
if !errors.Is(err, ErrInvalidRequest) || repository.lookupCalls != 0 {
|
||||||
|
t.Fatalf("invalid session error=%v lookups=%d", err, repository.lookupCalls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("cancellation before lookup", func(t *testing.T) {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
cancel()
|
||||||
|
repository := &requirementInspectionPromptRepository{}
|
||||||
|
_, err := (&Runner{promptDefs: repository}).InspectRequirements(ctx, RequirementInspectionRequest{PromptID: "selected"})
|
||||||
|
if !errors.Is(err, ErrPromptLoad) || !errors.Is(err, context.Canceled) || repository.lookupCalls != 0 {
|
||||||
|
t.Fatalf("canceled inspection=%v lookups=%d", err, repository.lookupCalls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("no profile selection", func(t *testing.T) {
|
||||||
|
repository := &requirementInspectionPromptRepository{definition: &domain.PromptDefinition{ID: "selected"}}
|
||||||
|
_, err := (&Runner{promptDefs: repository}).InspectRequirements(context.Background(), RequirementInspectionRequest{PromptID: "selected"})
|
||||||
|
if !errors.Is(err, ErrInvalidRequest) || !errors.Is(err, ErrProfileRequired) {
|
||||||
|
t.Fatalf("missing profile error=%v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("ambiguous prompt selection", func(t *testing.T) {
|
||||||
|
ambiguous := errors.New("ambiguous omitted version")
|
||||||
|
repository := &requirementInspectionPromptRepository{lookupErr: ambiguous}
|
||||||
|
_, err := (&Runner{promptDefs: repository}).InspectRequirements(context.Background(), RequirementInspectionRequest{PromptID: "selected"})
|
||||||
|
if !errors.Is(err, ErrPromptLoad) || !errors.Is(err, ambiguous) || repository.lookupVersion != "" {
|
||||||
|
t.Fatalf("ambiguous selection error=%v version=%q", err, repository.lookupVersion)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("captured snapshot unavailable", func(t *testing.T) {
|
||||||
|
repository := &requirementInspectionPromptRepository{definition: &domain.PromptDefinition{ID: "selected", DefaultProfile: "profile"}}
|
||||||
|
profiles := &inspectionProfileRepository{profile: &domain.ExecutionProfile{ID: "profile", ResolvedTarget: &domain.ExecutionTarget{}}}
|
||||||
|
_, err := (&Runner{promptDefs: repository, profiles: profiles}).InspectRequirements(context.Background(), RequirementInspectionRequest{PromptID: "selected"})
|
||||||
|
if !errors.Is(err, ErrPromptLoad) || repository.snapshotCalls != 1 {
|
||||||
|
t.Fatalf("missing snapshot error=%v calls=%d", err, repository.snapshotCalls)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("repository lacks snapshot capability", func(t *testing.T) {
|
||||||
|
_, err := (&Runner{promptDefs: &inspectionPromptRepository{definition: &domain.PromptDefinition{ID: "selected", DefaultProfile: "profile"}},
|
||||||
|
profiles: &inspectionProfileRepository{profile: &domain.ExecutionProfile{ID: "profile", ResolvedTarget: &domain.ExecutionTarget{}}},
|
||||||
|
}).InspectRequirements(context.Background(), RequirementInspectionRequest{PromptID: "selected"})
|
||||||
|
if !errors.Is(err, ErrPromptLoad) {
|
||||||
|
t.Fatalf("missing capability error=%v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if _, err := (*Runner)(nil).InspectRequirements(context.Background(), RequirementInspectionRequest{PromptID: "selected"}); !errors.Is(err, ErrPromptLoad) {
|
||||||
|
t.Fatalf("nil runner error=%v", err)
|
||||||
|
}
|
||||||
|
if _, err := (&Runner{}).InspectRequirements(context.Background(), RequirementInspectionRequest{PromptID: "selected"}); !errors.Is(err, ErrPromptLoad) {
|
||||||
|
t.Fatalf("nil repository error=%v", err)
|
||||||
|
}
|
||||||
|
if _, err := (&Runner{promptDefs: &requirementInspectionPromptRepository{definition: &domain.PromptDefinition{ID: "selected"}}}).InspectRequirements(
|
||||||
|
context.Background(), RequirementInspectionRequest{PromptID: "selected", ProfileID: "explicit"},
|
||||||
|
); !errors.Is(err, ErrProfileLoad) {
|
||||||
|
t.Fatalf("missing profile error=%v", err)
|
||||||
|
}
|
||||||
|
if _, err := (&Runner{promptDefs: &requirementInspectionPromptRepository{definition: &domain.PromptDefinition{ID: "selected", DefaultProfile: "profile"}},
|
||||||
|
profiles: &inspectionProfileRepository{profile: &domain.ExecutionProfile{ID: "profile"}},
|
||||||
|
}).InspectRequirements(context.Background(), RequirementInspectionRequest{PromptID: "selected"}); !errors.Is(err, ErrProfileLoad) {
|
||||||
|
t.Fatalf("unresolved profile error=%v", err)
|
||||||
|
}
|
||||||
|
if _, err := (&Runner{promptDefs: &requirementInspectionPromptRepository{lookupErr: promptdef.ErrPromptDefinitionNotFound}}).InspectRequirements(
|
||||||
|
context.Background(), RequirementInspectionRequest{PromptID: "missing"},
|
||||||
|
); !errors.Is(err, ErrPromptLoad) || !errors.Is(err, promptdef.ErrPromptDefinitionNotFound) {
|
||||||
|
t.Fatalf("missing prompt error=%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
158
requirements.go
Normal file
158
requirements.go
Normal file
@@ -0,0 +1,158 @@
|
|||||||
|
package promptkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
||||||
|
"gitea.maximumdirect.net/eric/promptkit/internal/usecase"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RequirementInspectionRequest selects a prompt and supplies caller context
|
||||||
|
// for requirement discovery. It requires no artifact bindings or credentials.
|
||||||
|
type RequirementInspectionRequest struct {
|
||||||
|
PromptID string
|
||||||
|
PromptVersion string
|
||||||
|
ProfileID string
|
||||||
|
Vars map[string]string
|
||||||
|
SessionID string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequirementKind describes how a selected prompt may depend on an input or
|
||||||
|
// caller variable for the supplied context.
|
||||||
|
type RequirementKind string
|
||||||
|
|
||||||
|
const (
|
||||||
|
// RequirementRequired identifies a declaration or definite access.
|
||||||
|
RequirementRequired RequirementKind = "required"
|
||||||
|
// RequirementConditional identifies a dependency whose reachability is
|
||||||
|
// unresolved for the supplied context.
|
||||||
|
RequirementConditional RequirementKind = "conditional"
|
||||||
|
// RequirementOptional identifies a dependency known not to be required,
|
||||||
|
// including a reference used only on a known skipped path.
|
||||||
|
RequirementOptional RequirementKind = "optional"
|
||||||
|
)
|
||||||
|
|
||||||
|
// InputRequirement describes a declared or template-referenced prompt input.
|
||||||
|
type InputRequirement struct {
|
||||||
|
Name string
|
||||||
|
Kind RequirementKind
|
||||||
|
Declared bool
|
||||||
|
ContentType *string
|
||||||
|
Description *string
|
||||||
|
Explanation string
|
||||||
|
}
|
||||||
|
|
||||||
|
// VariableRequirement describes a caller-map key referenced by a prompt.
|
||||||
|
type VariableRequirement struct {
|
||||||
|
Name string
|
||||||
|
Kind RequirementKind
|
||||||
|
Supplied bool
|
||||||
|
Explanation string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequirementIssueCode identifies a limitation that can affect discovery.
|
||||||
|
type RequirementIssueCode string
|
||||||
|
|
||||||
|
const (
|
||||||
|
RequirementIssueMissingVariable RequirementIssueCode = "missing_variable"
|
||||||
|
RequirementIssueInputContent RequirementIssueCode = "input_content"
|
||||||
|
RequirementIssueDynamicInputName RequirementIssueCode = "dynamic_input_name"
|
||||||
|
RequirementIssueUnsupportedConstruct RequirementIssueCode = "unsupported_construct"
|
||||||
|
)
|
||||||
|
|
||||||
|
// RequirementIssue describes one discovery limitation using safe summary
|
||||||
|
// text. Descriptions are not intended as machine-readable details.
|
||||||
|
type RequirementIssue struct {
|
||||||
|
Code RequirementIssueCode
|
||||||
|
InputName string
|
||||||
|
VariableName string
|
||||||
|
Description string
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequirementInspection contains discovered dependencies for one selected
|
||||||
|
// prompt context. Complete describes discovery, not renderability or readiness.
|
||||||
|
type RequirementInspection struct {
|
||||||
|
PromptID string
|
||||||
|
PromptVersion string
|
||||||
|
ProfileID string
|
||||||
|
Inputs []InputRequirement
|
||||||
|
Variables []VariableRequirement
|
||||||
|
Complete bool
|
||||||
|
Issues []RequirementIssue
|
||||||
|
}
|
||||||
|
|
||||||
|
// InspectRequirements discovers inputs and caller variables for one selected
|
||||||
|
// prompt before its input artifacts exist. It uses exact, case-sensitive
|
||||||
|
// prompt selection and requires a profile ID either in the request or the
|
||||||
|
// selected prompt's default. The selected profile establishes identity only;
|
||||||
|
// it need not be execution-ready. A normalized nonblank SessionID bypasses the
|
||||||
|
// captured session template. Vars are copied before analysis, and returned
|
||||||
|
// slices and metadata pointers belong to the caller.
|
||||||
|
//
|
||||||
|
// This method does not perform artifact, schema, credential, rendering,
|
||||||
|
// validation, capacity, observation, provider, or execution work. Complete
|
||||||
|
// describes dependency discovery, not whether every dependency is supplied or
|
||||||
|
// the prompt is ready to execute. Discovery limitations return a partial
|
||||||
|
// inspection and no error; selection and operational failures return no
|
||||||
|
// inspection. Blank prompt IDs, invalid session IDs, and absent profile
|
||||||
|
// selection match ErrInvalidRequest; an absent profile selection also matches
|
||||||
|
// ErrProfileRequired. Missing prompt/profile IDs match ErrPromptNotFound or
|
||||||
|
// ErrProfileNotFound. Other selection/snapshot failures match ErrPromptLoad or
|
||||||
|
// ErrProfileLoad. A nil Engine matches ErrInvalidConfig.
|
||||||
|
func (e *Engine) InspectRequirements(
|
||||||
|
ctx context.Context,
|
||||||
|
req RequirementInspectionRequest,
|
||||||
|
) (*RequirementInspection, error) {
|
||||||
|
if e == nil || e.internal == nil {
|
||||||
|
return nil, ErrInvalidConfig
|
||||||
|
}
|
||||||
|
inspection, err := e.internal.InspectRequirements(ctx, usecase.RequirementInspectionRequest{
|
||||||
|
PromptID: req.PromptID, PromptVersion: req.PromptVersion, ProfileID: req.ProfileID,
|
||||||
|
Vars: copyStringMap(req.Vars), SessionID: req.SessionID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, mapPublicError(err)
|
||||||
|
}
|
||||||
|
return fromDomainRequirementInspection(inspection), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func fromDomainRequirementInspection(inspection *domain.RequirementInspection) *RequirementInspection {
|
||||||
|
if inspection == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
result := &RequirementInspection{
|
||||||
|
PromptID: inspection.PromptID, PromptVersion: inspection.PromptVersion,
|
||||||
|
ProfileID: inspection.ProfileID, Complete: inspection.Complete,
|
||||||
|
Inputs: make([]InputRequirement, len(inspection.Inputs)),
|
||||||
|
Variables: make([]VariableRequirement, len(inspection.Variables)),
|
||||||
|
Issues: make([]RequirementIssue, len(inspection.Issues)),
|
||||||
|
}
|
||||||
|
for index, input := range inspection.Inputs {
|
||||||
|
result.Inputs[index] = InputRequirement{
|
||||||
|
Name: input.Name, Kind: RequirementKind(input.Kind), Declared: input.Declared,
|
||||||
|
ContentType: copyRequirementString(input.ContentType), Description: copyRequirementString(input.Description),
|
||||||
|
Explanation: input.Explanation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for index, variable := range inspection.Variables {
|
||||||
|
result.Variables[index] = VariableRequirement{
|
||||||
|
Name: variable.Name, Kind: RequirementKind(variable.Kind), Supplied: variable.Supplied,
|
||||||
|
Explanation: variable.Explanation,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for index, issue := range inspection.Issues {
|
||||||
|
result.Issues[index] = RequirementIssue{
|
||||||
|
Code: RequirementIssueCode(issue.Code), InputName: issue.InputName,
|
||||||
|
VariableName: issue.VariableName, Description: issue.Description,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func copyRequirementString(value *string) *string {
|
||||||
|
if value == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
copy := *value
|
||||||
|
return ©
|
||||||
|
}
|
||||||
15
requirements_test.go
Normal file
15
requirements_test.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package promptkit
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestInspectRequirementsRejectsNilEngine(t *testing.T) {
|
||||||
|
var engine *Engine
|
||||||
|
inspection, err := engine.InspectRequirements(context.Background(), RequirementInspectionRequest{PromptID: "prompt"})
|
||||||
|
if inspection != nil || !errors.Is(err, ErrInvalidConfig) {
|
||||||
|
t.Fatalf("nil engine returned (%+v, %v), want ErrInvalidConfig", inspection, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user