143 lines
5.8 KiB
Go
143 lines
5.8 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
|
|
}
|
|
|
|
// 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)
|
|
}
|