Files
weatherreporter/internal/app/prompt_inspection.go

107 lines
4.4 KiB
Go

package app
import (
"context"
"os"
"strings"
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
"gitea.maximumdirect.net/eric/weatherreporter/internal/promptexec"
"gitea.maximumdirect.net/eric/weatherreporter/internal/report"
)
// PromptInspectionRequest contains the non-executing inputs required to
// validate one report's configured prompt and profile.
type PromptInspectionRequest struct {
Resolved report.Resolved
Executor promptexec.Executor
Promptkit config.PromptkitConfig
LookupEnv func(string) (string, bool)
}
// PromptInspectionResult contains only safe identity and provenance from a
// prompt/profile inspection.
type PromptInspectionResult struct {
PromptID string
PromptVersion string
PromptHash string
ProfileID string
BackendID string
ModelName string
}
// InspectPromptExecution validates the exact prompt and profile needed for a
// report before collection, execution, or durable writes begin.
func InspectPromptExecution(ctx context.Context, req PromptInspectionRequest) (PromptInspectionResult, error) {
if req.Executor == nil {
return PromptInspectionResult{}, promptexec.NewError(promptexec.InvalidConfiguration, "prompt executor is required", nil)
}
definition := req.Resolved.Definition
if strings.TrimSpace(definition.PromptID) == "" || strings.TrimSpace(definition.PromptVersion) == "" {
return PromptInspectionResult{}, promptexec.NewError(promptexec.InvalidConfiguration, "report prompt identity is incomplete", nil)
}
inspection, err := req.Executor.InspectPrompt(ctx, definition.PromptID, definition.PromptVersion)
if err != nil {
return PromptInspectionResult{}, promptInspectionError("prompt inspection failed", err)
}
if inspection.PromptID != definition.PromptID || inspection.PromptVersion != definition.PromptVersion {
return PromptInspectionResult{}, promptexec.NewError(promptexec.InvalidConfiguration, "prompt inspection did not return the requested prompt version", nil)
}
if !validPromptInput(inspection.Inputs) {
return PromptInspectionResult{}, promptexec.NewError(promptexec.InvalidConfiguration, "prompt must declare exactly one required application/yaml data_package input", nil)
}
if !validPromptOutput(definition, inspection.Output) {
return PromptInspectionResult{}, promptexec.NewError(promptexec.InvalidConfiguration, "prompt must declare the report JSON Schema output contract", nil)
}
profileID := req.Promptkit.Profile
if profileID == "" {
profileID = inspection.DefaultProfileID
}
if strings.TrimSpace(profileID) == "" {
return PromptInspectionResult{}, promptexec.NewError(promptexec.InvalidConfiguration, "prompt has no execution profile", nil)
}
profile, err := req.Executor.InspectProfile(ctx, profileID)
if err != nil {
return PromptInspectionResult{}, promptInspectionError("profile inspection failed", err)
}
if profile.ProfileID != profileID {
return PromptInspectionResult{}, promptexec.NewError(promptexec.InvalidConfiguration, "profile inspection did not return the selected profile", nil)
}
if profile.CredentialRequired {
return PromptInspectionResult{}, promptexec.NewError(promptexec.MissingCredential, "selected profile requires an unsupported direct API key", nil)
}
if strings.TrimSpace(profile.APIKeyEnv) != "" {
lookupEnv := req.LookupEnv
if lookupEnv == nil {
lookupEnv = os.LookupEnv
}
value, present := lookupEnv(profile.APIKeyEnv)
if !present || strings.TrimSpace(value) == "" {
return PromptInspectionResult{}, promptexec.NewError(promptexec.MissingCredential, "selected profile credential is unavailable", nil)
}
}
return PromptInspectionResult{
PromptID: inspection.PromptID,
PromptVersion: inspection.PromptVersion,
PromptHash: inspection.PromptHash,
ProfileID: profile.ProfileID,
BackendID: profile.BackendID,
ModelName: profile.ModelName,
}, nil
}
func validPromptInput(inputs []promptexec.InputDefinition) bool {
return len(inputs) == 1 && inputs[0].Name == "data_package" && inputs[0].Required && inputs[0].ContentType == "application/yaml"
}
func validPromptOutput(definition report.Definition, output promptexec.OutputContract) bool {
return output.Format == "json" && output.ValidationMode == "json_schema" && output.SchemaPath == definition.GeneratedTextSchemaID+".generated_text.schema.json"
}
func promptInspectionError(operation string, err error) error {
if promptexec.CategoryOf(err) != "" {
return err
}
return promptexec.NewError(promptexec.InvalidConfiguration, operation, err)
}