Expose prompt requirement inspection
This commit is contained in:
@@ -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) {
|
||||
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) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user