Track the operation owning internal errors
This commit is contained in:
38
internal/usecase/operation_error.go
Normal file
38
internal/usecase/operation_error.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"go.maximumdirect.net/promptkit/internal/profile"
|
||||
"go.maximumdirect.net/promptkit/internal/promptdef"
|
||||
)
|
||||
|
||||
// OperationError identifies the operation that owns a failure while retaining
|
||||
// the original cause for inspection.
|
||||
type OperationError struct {
|
||||
Category error
|
||||
Cause error
|
||||
}
|
||||
|
||||
func (e *OperationError) Error() string { return fmt.Sprintf("%v: %v", e.Category, e.Cause) }
|
||||
func (e *OperationError) Unwrap() error { return e.Cause }
|
||||
func (e *OperationError) Is(target error) bool { return target == e.Category }
|
||||
|
||||
func promptLookupError(cause error) error {
|
||||
category := ErrPromptLoad
|
||||
if errors.Is(cause, promptdef.ErrPromptDefinitionNotFound) {
|
||||
category = promptdef.ErrPromptDefinitionNotFound
|
||||
cause = fmt.Errorf("%w: %w", ErrPromptLoad, cause)
|
||||
}
|
||||
return &OperationError{Category: category, Cause: cause}
|
||||
}
|
||||
|
||||
func profileLookupError(cause error) error {
|
||||
category := ErrProfileLoad
|
||||
if errors.Is(cause, profile.ErrProfileNotFound) {
|
||||
category = profile.ErrProfileNotFound
|
||||
cause = fmt.Errorf("%w: %w", ErrProfileLoad, cause)
|
||||
}
|
||||
return &OperationError{Category: category, Cause: cause}
|
||||
}
|
||||
34
internal/usecase/operation_error_test.go
Normal file
34
internal/usecase/operation_error_test.go
Normal file
@@ -0,0 +1,34 @@
|
||||
package usecase
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"testing"
|
||||
|
||||
"go.maximumdirect.net/promptkit/internal/profile"
|
||||
"go.maximumdirect.net/promptkit/internal/promptdef"
|
||||
)
|
||||
|
||||
func TestOperationErrorRetainsOwnerAndCause(t *testing.T) {
|
||||
cause := fmt.Errorf("nested: %w", promptdef.ErrPromptDefinitionNotFound)
|
||||
outer := &OperationError{Category: ErrLLMGenerate, Cause: cause}
|
||||
if !errors.Is(outer, ErrLLMGenerate) || !errors.Is(outer, promptdef.ErrPromptDefinitionNotFound) || errors.Unwrap(outer) != cause {
|
||||
t.Fatalf("operation error = %v", outer)
|
||||
}
|
||||
for _, test := range []struct {
|
||||
err error
|
||||
category error
|
||||
}{
|
||||
{promptLookupError(cause), promptdef.ErrPromptDefinitionNotFound},
|
||||
{profileLookupError(profile.ErrProfileNotFound), profile.ErrProfileNotFound},
|
||||
{promptLookupError(contextFailure), ErrPromptLoad},
|
||||
{profileLookupError(contextFailure), ErrProfileLoad},
|
||||
} {
|
||||
var operation *OperationError
|
||||
if !errors.As(test.err, &operation) || operation.Category != test.category {
|
||||
t.Errorf("operation = %+v", operation)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var contextFailure = errors.New("lookup failed")
|
||||
@@ -39,7 +39,7 @@ func (r *Runner) resolveProfileSelection(
|
||||
}
|
||||
selectedProfile, err := r.profiles.GetProfile(ctx, lookupID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrProfileLoad, err)
|
||||
return nil, profileLookupError(err)
|
||||
}
|
||||
if selectedProfile == nil {
|
||||
return nil, fmt.Errorf("%w: profile repository returned nil profile", ErrProfileLoad)
|
||||
@@ -64,7 +64,7 @@ func (r *Runner) resolveProfileIdentity(ctx context.Context, profileID string) (
|
||||
if indexed, ok := r.profiles.(profileIdentityRepository); ok {
|
||||
identity, err := indexed.GetProfileIdentity(ctx, lookupID)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("%w: %w", ErrProfileLoad, err)
|
||||
return "", profileLookupError(err)
|
||||
}
|
||||
return identity, nil
|
||||
}
|
||||
@@ -108,7 +108,7 @@ func (r *Runner) InspectProfile(
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("%w: %w", ErrProfileLoad, ctx.Err())
|
||||
return nil, &OperationError{Category: ErrProfileLoad, Cause: ctx.Err()}
|
||||
default:
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ func (r *Runner) InspectProfile(
|
||||
target.APIKey = ""
|
||||
params, err := jsonvalue.CopyMap(target.ExtraParams)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: profile extra parameters: %w", ErrProfileLoad, err)
|
||||
return nil, &OperationError{Category: ErrProfileLoad, Cause: fmt.Errorf("profile extra parameters: %w", err)}
|
||||
}
|
||||
target.ExtraParams = params
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ func (r *Runner) resolvePromptDefinition(ctx context.Context, promptID string) (
|
||||
definition, err = r.promptDefs.GetPromptDefinition(ctx, promptID)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, err)
|
||||
return nil, promptLookupError(err)
|
||||
}
|
||||
if definition == nil {
|
||||
return nil, fmt.Errorf("%w: prompt repository returned nil definition", ErrPromptLoad)
|
||||
@@ -61,7 +61,7 @@ func (r *Runner) resolvePromptInspection(ctx context.Context, promptID string) (
|
||||
if indexed, ok := r.promptDefs.(promptInspectionRepository); ok {
|
||||
inspection, err := indexed.GetPromptInspection(ctx, promptID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, err)
|
||||
return nil, promptLookupError(err)
|
||||
}
|
||||
if inspection == nil {
|
||||
return nil, fmt.Errorf("%w: prompt repository returned nil inspection", ErrPromptLoad)
|
||||
@@ -90,7 +90,7 @@ func (r *Runner) InspectPrompt(ctx context.Context, promptID string) (*domain.Pr
|
||||
}
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, ctx.Err())
|
||||
return nil, &OperationError{Category: ErrPromptLoad, Cause: ctx.Err()}
|
||||
default:
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ func (r *Runner) InspectRequirements(
|
||||
return nil, fmt.Errorf("%w: session_id: %v", ErrInvalidRequest, err)
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, err)
|
||||
return nil, &OperationError{Category: ErrPromptLoad, Cause: err}
|
||||
}
|
||||
|
||||
selection, err := r.resolvePromptInspection(ctx, req.PromptID)
|
||||
@@ -44,7 +44,7 @@ func (r *Runner) InspectRequirements(
|
||||
return nil, err
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrProfileLoad, err)
|
||||
return nil, &OperationError{Category: ErrProfileLoad, Cause: err}
|
||||
}
|
||||
selectedProfileID := req.ProfileID
|
||||
if strings.TrimSpace(selectedProfileID) == "" {
|
||||
@@ -58,7 +58,7 @@ func (r *Runner) InspectRequirements(
|
||||
return nil, err
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, err)
|
||||
return nil, &OperationError{Category: ErrPromptLoad, Cause: err}
|
||||
}
|
||||
|
||||
repository, ok := r.promptDefs.(requirementSnapshotRepository)
|
||||
@@ -72,7 +72,7 @@ func (r *Runner) InspectRequirements(
|
||||
definition := &domain.PromptDefinition{ID: selection.PromptID}
|
||||
inspection, err := prompt.AnalyzeRequirements(ctx, definition, snapshot, req.Vars, directSessionID != "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrPromptLoad, err)
|
||||
return nil, &OperationError{Category: ErrPromptLoad, Cause: err}
|
||||
}
|
||||
inspection.ProfileID = profileIdentity
|
||||
return &inspection, nil
|
||||
|
||||
@@ -204,7 +204,7 @@ func (r *Runner) executePreparedRun(
|
||||
outputArtifact := buildOutputArtifact(genResp.Content, prepared.OutputContract.Format)
|
||||
validationResult, err := validateArtifact(ctx, &outputArtifact, 0)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrValidation, err)
|
||||
return nil, &OperationError{Category: ErrValidation, Cause: err}
|
||||
}
|
||||
|
||||
if r.shouldAttemptRepair(prepared.OutputContract, validationResult) {
|
||||
@@ -238,7 +238,7 @@ func (r *Runner) executePreparedRun(
|
||||
|
||||
validationResult, err = validateArtifact(ctx, &outputArtifact, attemptsUsed)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrValidation, err)
|
||||
return nil, &OperationError{Category: ErrValidation, Cause: err}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -269,10 +269,7 @@ func (r *Runner) executePreparedRun(
|
||||
}
|
||||
|
||||
func wrapGenerationError(err error) error {
|
||||
if errors.Is(err, llm.ErrInvalidRequest) {
|
||||
return fmt.Errorf("%w: %w", ErrInvalidRequest, err)
|
||||
}
|
||||
return fmt.Errorf("%w: %w", ErrLLMGenerate, err)
|
||||
return &OperationError{Category: ErrLLMGenerate, Cause: err}
|
||||
}
|
||||
|
||||
func addTokenUsage(total, next domain.TokenUsage) domain.TokenUsage {
|
||||
@@ -408,7 +405,7 @@ func (r *Runner) prepareValidation(
|
||||
}
|
||||
plan, err := preparer.PrepareValidation(ctx, contract)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrValidation, err)
|
||||
return nil, &OperationError{Category: ErrValidation, Cause: err}
|
||||
}
|
||||
if plan == nil {
|
||||
return nil, fmt.Errorf("%w: validator returned nil prepared validation", ErrValidation)
|
||||
@@ -450,7 +447,7 @@ func (r *Runner) completePreparationWithStructuredOutput(
|
||||
for name, ref := range req.Inputs {
|
||||
art, readErr := r.artifacts.Read(ctx, ref)
|
||||
if readErr != nil {
|
||||
return nil, fmt.Errorf("%w: input %q: %w", ErrArtifactLoad, name, readErr)
|
||||
return nil, &OperationError{Category: ErrArtifactLoad, Cause: fmt.Errorf("input %q: %w", name, readErr)}
|
||||
}
|
||||
if art.Name == "" {
|
||||
art.Name = name
|
||||
@@ -467,7 +464,7 @@ func (r *Runner) completePreparationWithStructuredOutput(
|
||||
}
|
||||
renderedPrompt, err := r.renderer.Render(ctx, definitionToRender, resolvedInputs, req.Vars)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("%w: %w", ErrPromptRender, err)
|
||||
return nil, &OperationError{Category: ErrPromptRender, Cause: err}
|
||||
}
|
||||
effectivePrompt := *renderedPrompt
|
||||
if state.directSessionID != "" {
|
||||
|
||||
@@ -144,7 +144,7 @@ func (r *Runner) resolveSmokeBackend(id string) (domain.Backend, error) {
|
||||
}
|
||||
backend, err := lookup.GetBackend(id)
|
||||
if err != nil {
|
||||
return domain.Backend{}, fmt.Errorf("%w: backend %q: %v", ErrProfileLoad, id, err)
|
||||
return domain.Backend{}, &OperationError{Category: ErrProfileLoad, Cause: fmt.Errorf("backend %q: %w", id, err)}
|
||||
}
|
||||
return backend, nil
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user