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) }