package app import ( "context" "os" "strings" "gitea.maximumdirect.net/eric/weatherreporter/internal/comparison" "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) } // ComparisonInspectionRequest contains the explicit profile selection for one // resolved prompt comparison. It intentionally has no configured profile field. type ComparisonInspectionRequest struct { Resolved report.Resolved ProfileIDs []string Executor promptexec.Executor LookupEnv func(string) (string, bool) } // ComparisonInspectionResult contains the safe, shared prompt identity and // ordered effective profile identities for a comparison. type ComparisonInspectionResult struct { PromptID string PromptVersion string PromptHash string Profiles []ComparisonProfileInspection } // ComparisonProfileInspection contains one requested profile's safe effective // execution identity. type ComparisonProfileInspection struct { 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) { 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 inspection, err := inspectPromptContract(ctx, req.Executor, definition) if err != nil { return nil, err } 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 } // InspectComparisonExecution validates one exact prompt and every explicitly // requested profile before collection or model execution. Profiles are // inspected sequentially in request order. func InspectComparisonExecution(ctx context.Context, req ComparisonInspectionRequest) (ComparisonInspectionResult, error) { if err := comparison.ValidateProfileIDs(req.ProfileIDs); err != nil { return ComparisonInspectionResult{}, promptexec.NewError(promptexec.InvalidRequest, "comparison profile selection is invalid", err) } if req.Executor == nil { return ComparisonInspectionResult{}, promptexec.NewError(promptexec.InvalidConfiguration, "prompt executor is required", nil) } inspection, err := inspectPromptContract(ctx, req.Executor, req.Resolved.Definition) if err != nil { return ComparisonInspectionResult{}, comparisonInspectionError("comparison prompt inspection failed", err) } result := ComparisonInspectionResult{ PromptID: inspection.PromptID, PromptVersion: inspection.PromptVersion, PromptHash: inspection.PromptHash, Profiles: make([]ComparisonProfileInspection, 0, len(req.ProfileIDs)), } for _, profileID := range req.ProfileIDs { profile, err := inspectPromptProfile(ctx, req.Executor, profileID, req.LookupEnv) if err != nil { return ComparisonInspectionResult{}, comparisonInspectionError("comparison profile inspection failed", err) } result.Profiles = append(result.Profiles, ComparisonProfileInspection{ ProfileID: profile.ProfileID, BackendID: profile.BackendID, ModelName: profile.ModelName, }) } return result, nil } func inspectPromptContract(ctx context.Context, executor promptexec.Executor, definition report.Definition) (promptexec.PromptInspection, error) { if strings.TrimSpace(definition.PromptID) == "" || strings.TrimSpace(definition.PromptVersion) == "" { return promptexec.PromptInspection{}, promptexec.NewError(promptexec.InvalidConfiguration, "report prompt identity is incomplete", nil) } inspection, err := executor.InspectPrompt(ctx, definition.PromptID, definition.PromptVersion) if err != nil { return promptexec.PromptInspection{}, promptInspectionError("prompt inspection failed", err) } if inspection.PromptID != definition.PromptID || inspection.PromptVersion != definition.PromptVersion { return promptexec.PromptInspection{}, promptexec.NewError(promptexec.InvalidConfiguration, "prompt inspection did not return the requested prompt version", nil) } if !validPromptInput(inspection.Inputs) { return promptexec.PromptInspection{}, promptexec.NewError(promptexec.InvalidConfiguration, "prompt must declare exactly one required application/yaml data_package input", nil) } if !validPromptOutput(definition, inspection.Output) { return promptexec.PromptInspection{}, promptexec.NewError(promptexec.InvalidConfiguration, "prompt must declare the report JSON Schema output contract", nil) } return inspection, 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) } func comparisonInspectionError(operation string, err error) error { category := promptexec.CategoryOf(err) if category == "" { category = promptexec.InvalidConfiguration } return promptexec.NewError(category, operation, err) }