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 } // PromptExecutionsInspectionRequest validates all prompt/profile combinations // needed by a batch before collection begins. type PromptExecutionsInspectionRequest struct { Resolved []report.Resolved Executor promptexec.Executor Promptkit config.PromptkitConfig LookupEnv func(string) (string, bool) } // 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) { results, err := InspectPromptExecutions(ctx, PromptExecutionsInspectionRequest{ Resolved: []report.Resolved{req.Resolved}, Executor: req.Executor, Promptkit: req.Promptkit, LookupEnv: req.LookupEnv, }) if err != nil { return PromptInspectionResult{}, err } return results[req.Resolved.Definition.ID], nil } // InspectPromptExecutions validates exact prompt contracts and their unique // effective profiles. It performs no collection, execution, or durable write. func InspectPromptExecutions(ctx context.Context, req PromptExecutionsInspectionRequest) (map[report.ID]PromptInspectionResult, error) { if req.Executor == nil { return nil, promptexec.NewError(promptexec.InvalidConfiguration, "prompt executor is required", nil) } results := make(map[report.ID]PromptInspectionResult, len(req.Resolved)) profiles := map[string]promptexec.ProfileInspection{} for _, resolved := range req.Resolved { definition := resolved.Definition if strings.TrimSpace(definition.PromptID) == "" || strings.TrimSpace(definition.PromptVersion) == "" { return nil, promptexec.NewError(promptexec.InvalidConfiguration, "report prompt identity is incomplete", nil) } inspection, err := req.Executor.InspectPrompt(ctx, definition.PromptID, definition.PromptVersion) if err != nil { return nil, promptInspectionError("prompt inspection failed", err) } if inspection.PromptID != definition.PromptID || inspection.PromptVersion != definition.PromptVersion { return nil, promptexec.NewError(promptexec.InvalidConfiguration, "prompt inspection did not return the requested prompt version", nil) } if !validPromptInput(inspection.Inputs) { return nil, promptexec.NewError(promptexec.InvalidConfiguration, "prompt must declare exactly one required application/yaml data_package input", nil) } if !validPromptOutput(definition, inspection.Output) { return nil, 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 nil, promptexec.NewError(promptexec.InvalidConfiguration, "prompt has no execution profile", nil) } profile, ok := profiles[profileID] if !ok { profile, err = inspectPromptProfile(ctx, req.Executor, profileID, req.LookupEnv) if err != nil { return nil, err } profiles[profileID] = profile } results[definition.ID] = PromptInspectionResult{ PromptID: inspection.PromptID, PromptVersion: inspection.PromptVersion, PromptHash: inspection.PromptHash, ProfileID: profile.ProfileID, BackendID: profile.BackendID, ModelName: profile.ModelName, } } return results, nil } func inspectPromptProfile(ctx context.Context, executor promptexec.Executor, profileID string, lookupEnv func(string) (string, bool)) (promptexec.ProfileInspection, error) { profile, err := executor.InspectProfile(ctx, profileID) if err != nil { return promptexec.ProfileInspection{}, promptInspectionError("profile inspection failed", err) } if profile.ProfileID != profileID { return promptexec.ProfileInspection{}, promptexec.NewError(promptexec.InvalidConfiguration, "profile inspection did not return the selected profile", nil) } if profile.CredentialRequired { return promptexec.ProfileInspection{}, promptexec.NewError(promptexec.MissingCredential, "selected profile requires an unsupported direct API key", nil) } if strings.TrimSpace(profile.APIKeyEnv) != "" { if lookupEnv == nil { lookupEnv = os.LookupEnv } value, present := lookupEnv(profile.APIKeyEnv) if !present || strings.TrimSpace(value) == "" { return promptexec.ProfileInspection{}, promptexec.NewError(promptexec.MissingCredential, "selected profile credential is unavailable", nil) } } return profile, 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) }