227 lines
9.5 KiB
Go
227 lines
9.5 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"strings"
|
|
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/comparison"
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/config"
|
|
"gitea.maximumdirect.net/eric/weatherreporter/internal/generatedtext"
|
|
"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
|
|
}
|
|
|
|
// 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
|
|
handler generatedtext.Handler
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// 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
|
|
handler generatedtext.Handler
|
|
}
|
|
|
|
// 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,
|
|
})
|
|
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
|
|
handler, err := generatedtext.LookupDefinition(definition)
|
|
if err != nil {
|
|
return nil, promptexec.NewError(promptexec.InvalidConfiguration, "report generated-text catalog is incompatible", err)
|
|
}
|
|
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)
|
|
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,
|
|
handler: handler,
|
|
}
|
|
}
|
|
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. If a profile fails, the returned
|
|
// partial result retains the prompt identity and successfully inspected prefix.
|
|
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)
|
|
}
|
|
|
|
handler, err := generatedtext.LookupDefinition(req.Resolved.Definition)
|
|
if err != nil {
|
|
return ComparisonInspectionResult{}, comparisonInspectionError("comparison generated-text catalog inspection failed", promptexec.NewError(promptexec.InvalidConfiguration, "report generated-text catalog is incompatible", err))
|
|
}
|
|
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)),
|
|
handler: handler,
|
|
}
|
|
for _, profileID := range req.ProfileIDs {
|
|
profile, err := inspectPromptProfile(ctx, req.Executor, profileID)
|
|
if err != nil {
|
|
return result, 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 strings.TrimSpace(inspection.PromptHash) == "" {
|
|
return promptexec.PromptInspection{}, promptexec.NewError(promptexec.InvalidConfiguration, "prompt inspection did not return a prompt hash", 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) (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.ModelName) == "" {
|
|
return promptexec.ProfileInspection{}, promptexec.NewError(promptexec.InvalidConfiguration, "profile inspection did not return a complete execution identity", 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)
|
|
}
|