438 lines
18 KiB
Go
438 lines
18 KiB
Go
package commandkit
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"strconv"
|
|
"strings"
|
|
|
|
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
|
"gitea.maximumdirect.net/eric/promptkit"
|
|
)
|
|
|
|
// PromptKitSuite declares optional PromptKit prompt and profile inspection
|
|
// commands for an App.
|
|
type PromptKitSuite[C any] struct {
|
|
// NewContext creates command-local application context when an inspection runs.
|
|
NewContext func() C
|
|
// Selectors declares context flags accepted by the installed commands. Each
|
|
// explicit scope list must include "promptkit"; an empty list applies here.
|
|
Selectors []Selector[C]
|
|
// CheckInput validates standard configuration selections and selector values.
|
|
CheckInput func(Selection, C) error
|
|
// InspectPrompt inspects an ID and exact or omitted version.
|
|
InspectPrompt func(context.Context, Selection, C, string, string) (Acquired[*promptkit.PromptInspection], error)
|
|
// InspectProfile inspects one profile ID.
|
|
InspectProfile func(context.Context, Selection, C, string) (Acquired[*promptkit.ProfileInspection], error)
|
|
}
|
|
|
|
// NewPromptKitSuite creates prompt and profile inspection callbacks backed by
|
|
// a PromptKit Engine loader. The loader runs once per selected command. Each
|
|
// callback captures configuration diagnostics before calling the corresponding
|
|
// PromptKit inspection method, and returns those diagnostics with lookup errors.
|
|
func NewPromptKitSuite[C any](
|
|
load func(context.Context, Selection, C) (*promptkit.Engine, error),
|
|
) (PromptKitSuite[C], error) {
|
|
if load == nil {
|
|
return PromptKitSuite[C]{}, fmt.Errorf("PromptKit engine loader is nil")
|
|
}
|
|
return PromptKitSuite[C]{
|
|
InspectPrompt: func(ctx context.Context, selection Selection, appContext C, id, version string) (Acquired[*promptkit.PromptInspection], error) {
|
|
engine, _, diagnostics, err := loadPromptKitInspection(ctx, selection, appContext, load)
|
|
if err != nil {
|
|
return Acquired[*promptkit.PromptInspection]{Diagnostics: diagnostics}, err
|
|
}
|
|
inspection, err := engine.InspectPrompt(ctx, id, version)
|
|
return Acquired[*promptkit.PromptInspection]{Value: inspection, Diagnostics: diagnostics}, err
|
|
},
|
|
InspectProfile: func(ctx context.Context, selection Selection, appContext C, id string) (Acquired[*promptkit.ProfileInspection], error) {
|
|
engine, _, diagnostics, err := loadPromptKitInspection(ctx, selection, appContext, load)
|
|
if err != nil {
|
|
return Acquired[*promptkit.ProfileInspection]{Diagnostics: diagnostics}, err
|
|
}
|
|
inspection, err := engine.InspectProfile(ctx, id)
|
|
return Acquired[*promptkit.ProfileInspection]{Value: inspection, Diagnostics: diagnostics}, err
|
|
},
|
|
}, nil
|
|
}
|
|
|
|
func loadPromptKitInspection[C any](ctx context.Context, selection Selection, appContext C, load func(context.Context, Selection, C) (*promptkit.Engine, error)) (*promptkit.Engine, promptkit.ConfigurationInspection, []Diagnostic, error) {
|
|
engine, err := load(ctx, selection, appContext)
|
|
if err != nil {
|
|
var loadErr *configkit.LoadError
|
|
if errors.As(err, &loadErr) {
|
|
return nil, promptkit.ConfigurationInspection{}, ConfigKitDiagnostics("promptkit", loadErr.Sources(), loadErr.Diagnostics()), err
|
|
}
|
|
return nil, promptkit.ConfigurationInspection{}, nil, err
|
|
}
|
|
if engine == nil {
|
|
return nil, promptkit.ConfigurationInspection{}, nil, fmt.Errorf("PromptKit configuration loader returned a nil engine")
|
|
}
|
|
inspection, err := engine.InspectConfiguration()
|
|
if err != nil {
|
|
return nil, promptkit.ConfigurationInspection{}, nil, err
|
|
}
|
|
sources := inspection.Sources()
|
|
diagnostics := ConfigKitDiagnostics("promptkit", sources, inspection.Diagnostics())
|
|
return engine, inspection, diagnostics, nil
|
|
}
|
|
|
|
// RegisterPromptKit installs the supplied PromptKit inspection commands under
|
|
// promptkit show. Each nonnil callback installs only its matching command. A
|
|
// suite with no inspection callbacks installs no PromptKit namespace.
|
|
func RegisterPromptKit[C any](app *App, suite PromptKitSuite[C]) error {
|
|
if suite.InspectPrompt == nil && suite.InspectProfile == nil {
|
|
return nil
|
|
}
|
|
if app == nil {
|
|
return fmt.Errorf("cannot register PromptKit commands on a nil app")
|
|
}
|
|
normalized, err := normalizePromptKitSuite(suite)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
showCommands := make([]Command, 0, 2)
|
|
if normalized.InspectPrompt != nil {
|
|
command, err := newPromptKitPromptCommand(normalized)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
showCommands = append(showCommands, command)
|
|
}
|
|
if normalized.InspectProfile != nil {
|
|
command, err := newPromptKitProfileCommand(normalized)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
showCommands = append(showCommands, command)
|
|
}
|
|
show, err := Group("show", "Inspect PromptKit definitions.", showCommands...)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
root, err := Group("promptkit", "Inspect PromptKit definitions.", show)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return app.Register(root)
|
|
}
|
|
|
|
func normalizePromptKitSuite[C any](suite PromptKitSuite[C]) (PromptKitSuite[C], error) {
|
|
normalized := suite
|
|
normalized.Selectors = append([]Selector[C](nil), suite.Selectors...)
|
|
seenNames := make(map[string]struct{}, len(normalized.Selectors))
|
|
seenShorts := make(map[string]struct{}, len(normalized.Selectors))
|
|
for i, selector := range normalized.Selectors {
|
|
binding := selector.Binding.binding
|
|
meta := binding.meta
|
|
if !binding.valid || binding.clear == nil || meta.Name == "" {
|
|
return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector %d has an invalid flag binding", i)
|
|
}
|
|
if !validFlagName(meta.Name) {
|
|
return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector flag name %q is invalid", meta.Name)
|
|
}
|
|
switch meta.Name {
|
|
case "config", "overlay", "scope", "format", "version", "help":
|
|
return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector flag --%s conflicts with a standard flag", meta.Name)
|
|
}
|
|
if meta.Short == "h" {
|
|
return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector short flag -%s conflicts with help", meta.Short)
|
|
}
|
|
if meta.Short != "" && (len(meta.Short) != 1 || meta.Short == "-") {
|
|
return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector short spelling %q must be one byte", meta.Short)
|
|
}
|
|
if _, exists := seenNames[meta.Name]; exists {
|
|
return PromptKitSuite[C]{}, fmt.Errorf("duplicate PromptKit selector flag --%s", meta.Name)
|
|
}
|
|
seenNames[meta.Name] = struct{}{}
|
|
if meta.Short != "" {
|
|
if _, exists := seenShorts[meta.Short]; exists {
|
|
return PromptKitSuite[C]{}, fmt.Errorf("duplicate PromptKit selector short flag -%s", meta.Short)
|
|
}
|
|
seenShorts[meta.Short] = struct{}{}
|
|
}
|
|
applies := len(selector.Scopes) == 0
|
|
for _, scope := range selector.Scopes {
|
|
if scope == "promptkit" {
|
|
applies = true
|
|
break
|
|
}
|
|
}
|
|
if !applies {
|
|
return PromptKitSuite[C]{}, fmt.Errorf("PromptKit selector --%s must apply to scope %q", meta.Name, "promptkit")
|
|
}
|
|
normalized.Selectors[i].Scopes = append([]string(nil), selector.Scopes...)
|
|
}
|
|
return normalized, nil
|
|
}
|
|
|
|
type promptKitInput[C any] struct {
|
|
Selection Selection
|
|
Format string
|
|
ID string
|
|
Version string
|
|
SelectorContext C
|
|
selectors []selectorState[C]
|
|
}
|
|
|
|
func promptKitInputFlags[C any](suite PromptKitSuite[C], prompt bool) []Flag[promptKitInput[C]] {
|
|
formatDefault := "text"
|
|
flags := []Flag[promptKitInput[C]]{
|
|
ValueFlag[promptKitInput[C]](FlagMeta{Name: "format", Usage: "Output format."}, &formatDefault, parseString, func(input *promptKitInput[C], value Supplied[string]) {
|
|
input.Format = value.Value
|
|
}),
|
|
}
|
|
flags = append(flags, configurationSelectionFlags(
|
|
func(input *promptKitInput[C], value Supplied[string]) { input.Selection.Config = value },
|
|
func(input *promptKitInput[C], value Supplied[string]) { input.Selection.Overlay = value },
|
|
)...)
|
|
if prompt {
|
|
versionDefault := ""
|
|
flags = append(flags, ValueFlag[promptKitInput[C]](FlagMeta{Name: "version", Usage: "Exact prompt version; empty selects the owning default."}, &versionDefault, parseString, func(input *promptKitInput[C], value Supplied[string]) {
|
|
input.Version = value.Value
|
|
}))
|
|
}
|
|
for _, selector := range suite.Selectors {
|
|
flags = append(flags, adaptContextSelector(selector,
|
|
func(input *promptKitInput[C]) *C { return &input.SelectorContext },
|
|
func(input *promptKitInput[C], state selectorState[C]) {
|
|
input.selectors = append(input.selectors, state)
|
|
},
|
|
))
|
|
}
|
|
return flags
|
|
}
|
|
|
|
func checkPromptKitInput[C any](suite PromptKitSuite[C], input promptKitInput[C]) error {
|
|
if _, err := parseConfigFormat(input.Format); err != nil {
|
|
return err
|
|
}
|
|
if suite.CheckInput != nil {
|
|
return suite.CheckInput(input.Selection, input.SelectorContext)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func promptKitContext[C any](suite PromptKitSuite[C], input promptKitInput[C]) C {
|
|
var result C
|
|
if suite.NewContext != nil {
|
|
result = suite.NewContext()
|
|
}
|
|
for _, selector := range input.selectors {
|
|
if selectorApplies(selector.scopes, "promptkit") {
|
|
selector.apply(&result)
|
|
} else {
|
|
selector.clear(&result)
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func newPromptKitPromptCommand[C any](suite PromptKitSuite[C]) (Command, error) {
|
|
inputSpec := CommandSpec[promptKitInput[C]]{
|
|
Name: "prompt",
|
|
Summary: "Inspect one PromptKit prompt definition.",
|
|
Flags: promptKitInputFlags(suite, true),
|
|
Arguments: []Argument[promptKitInput[C]]{ValueArgument[promptKitInput[C]](ArgumentMeta{Name: "ID"}, true, parseString, func(input *promptKitInput[C], value Supplied[string]) {
|
|
input.ID = value.Value
|
|
})},
|
|
CheckInput: func(input promptKitInput[C]) error { return checkPromptKitInput(suite, input) },
|
|
Run: func(invocation Invocation, input promptKitInput[C]) Outcome {
|
|
return runPromptKitPrompt(invocation, suite, input)
|
|
},
|
|
}
|
|
return NewCommand(inputSpec)
|
|
}
|
|
|
|
func newPromptKitProfileCommand[C any](suite PromptKitSuite[C]) (Command, error) {
|
|
inputSpec := CommandSpec[promptKitInput[C]]{
|
|
Name: "profile",
|
|
Summary: "Inspect one PromptKit profile definition.",
|
|
Flags: promptKitInputFlags(suite, false),
|
|
Arguments: []Argument[promptKitInput[C]]{ValueArgument[promptKitInput[C]](ArgumentMeta{Name: "ID"}, true, parseString, func(input *promptKitInput[C], value Supplied[string]) {
|
|
input.ID = value.Value
|
|
})},
|
|
CheckInput: func(input promptKitInput[C]) error { return checkPromptKitInput(suite, input) },
|
|
Run: func(invocation Invocation, input promptKitInput[C]) Outcome {
|
|
return runPromptKitProfile(invocation, suite, input)
|
|
},
|
|
}
|
|
return NewCommand(inputSpec)
|
|
}
|
|
|
|
func runPromptKitPrompt[C any](invocation Invocation, suite PromptKitSuite[C], input promptKitInput[C]) Outcome {
|
|
contextValue := promptKitContext(suite, input)
|
|
acquired, err := suite.InspectPrompt(invocation.Context, input.Selection, contextValue, input.ID, input.Version)
|
|
if err != nil {
|
|
return promptKitFailure(err, acquired.Diagnostics)
|
|
}
|
|
if acquired.Value == nil {
|
|
return promptKitFailure(fmt.Errorf("PromptKit prompt inspection returned no result"), acquired.Diagnostics)
|
|
}
|
|
report := promptReportFromInspection(acquired.Value)
|
|
return writePromptKitReport(invocation, input.Format, report, formatPromptText(report), acquired.Diagnostics)
|
|
}
|
|
|
|
func runPromptKitProfile[C any](invocation Invocation, suite PromptKitSuite[C], input promptKitInput[C]) Outcome {
|
|
contextValue := promptKitContext(suite, input)
|
|
acquired, err := suite.InspectProfile(invocation.Context, input.Selection, contextValue, input.ID)
|
|
if err != nil {
|
|
return promptKitFailure(err, acquired.Diagnostics)
|
|
}
|
|
if acquired.Value == nil {
|
|
return promptKitFailure(fmt.Errorf("PromptKit profile inspection returned no result"), acquired.Diagnostics)
|
|
}
|
|
report := profileReportFromInspection(acquired.Value)
|
|
textReport, err := formatProfileText(report)
|
|
if err != nil {
|
|
return promptKitFailure(err, acquired.Diagnostics)
|
|
}
|
|
return writePromptKitReport(invocation, input.Format, report, textReport, acquired.Diagnostics)
|
|
}
|
|
|
|
func promptKitFailure(err error, diagnostics []Diagnostic) Outcome {
|
|
outcome := Failure(err)
|
|
outcome.Diagnostics = diagnostics
|
|
return outcome
|
|
}
|
|
|
|
func writePromptKitReport(invocation Invocation, format string, report any, textReport string, diagnostics []Diagnostic) Outcome {
|
|
var output strings.Builder
|
|
if format == "yaml" {
|
|
if err := encodeYAML(&output, report); err != nil {
|
|
return promptKitFailure(err, diagnostics)
|
|
}
|
|
} else if _, err := io.WriteString(&output, textReport); err != nil {
|
|
return promptKitFailure(err, diagnostics)
|
|
}
|
|
if err := writeReport(invocation.Streams.Out, []byte(output.String())); err != nil {
|
|
outcome := incompleteReport(err)
|
|
outcome.Diagnostics = diagnostics
|
|
return outcome
|
|
}
|
|
outcome := Success()
|
|
outcome.Diagnostics = diagnostics
|
|
return outcome
|
|
}
|
|
|
|
type promptInputReport struct {
|
|
Name string `yaml:"name"`
|
|
Required bool `yaml:"required"`
|
|
ContentType string `yaml:"content_type"`
|
|
Description string `yaml:"description"`
|
|
}
|
|
|
|
type promptOutputContractReport struct {
|
|
Format string `yaml:"format"`
|
|
ValidationMode string `yaml:"validation_mode"`
|
|
SchemaPath string `yaml:"schema_path"`
|
|
RepairAttempts int `yaml:"repair_attempts"`
|
|
}
|
|
|
|
type promptInspectionReport struct {
|
|
PromptID string `yaml:"prompt_id"`
|
|
PromptVersion string `yaml:"prompt_version"`
|
|
PromptHash string `yaml:"prompt_hash"`
|
|
DefaultProfileID string `yaml:"default_profile_id"`
|
|
Inputs []promptInputReport `yaml:"inputs"`
|
|
OutputContract promptOutputContractReport `yaml:"output_contract"`
|
|
}
|
|
|
|
func promptReportFromInspection(inspection *promptkit.PromptInspection) promptInspectionReport {
|
|
report := promptInspectionReport{
|
|
PromptID: inspection.PromptID,
|
|
PromptVersion: inspection.PromptVersion,
|
|
PromptHash: inspection.PromptHash,
|
|
DefaultProfileID: inspection.DefaultProfileID,
|
|
Inputs: make([]promptInputReport, 0, len(inspection.Inputs)),
|
|
OutputContract: promptOutputContractReport{
|
|
Format: string(inspection.OutputContract.Format),
|
|
ValidationMode: string(inspection.OutputContract.ValidationMode),
|
|
SchemaPath: inspection.OutputContract.SchemaPath,
|
|
RepairAttempts: inspection.OutputContract.RepairAttempts,
|
|
},
|
|
}
|
|
for _, input := range inspection.Inputs {
|
|
report.Inputs = append(report.Inputs, promptInputReport{Name: input.Name, Required: input.Required, ContentType: input.ContentType, Description: input.Description})
|
|
}
|
|
return report
|
|
}
|
|
|
|
type modelParamsReport struct {
|
|
BackendID string `yaml:"backend_id"`
|
|
Endpoint string `yaml:"endpoint"`
|
|
Model string `yaml:"model"`
|
|
Temperature float64 `yaml:"temperature"`
|
|
MaxTokens int `yaml:"max_tokens"`
|
|
TopP float64 `yaml:"top_p"`
|
|
TimeoutSeconds int `yaml:"timeout_seconds"`
|
|
ServiceTier string `yaml:"service_tier"`
|
|
ReasoningEffort string `yaml:"reasoning_effort"`
|
|
APIKeyEnv string `yaml:"api_key_env"`
|
|
ExtraParams map[string]any `yaml:"extra_params"`
|
|
}
|
|
|
|
type profileInspectionReport struct {
|
|
ProfileID string `yaml:"profile_id"`
|
|
EffectiveModelParams modelParamsReport `yaml:"effective_model_params"`
|
|
APIKeyRequired bool `yaml:"api_key_required"`
|
|
}
|
|
|
|
func profileReportFromInspection(inspection *promptkit.ProfileInspection) profileInspectionReport {
|
|
params := inspection.EffectiveModelParams
|
|
extra := params.ExtraParams
|
|
if extra == nil {
|
|
extra = map[string]any{}
|
|
}
|
|
return profileInspectionReport{
|
|
ProfileID: inspection.ProfileID,
|
|
EffectiveModelParams: modelParamsReport{
|
|
BackendID: params.BackendID, Endpoint: params.Endpoint, Model: params.Model,
|
|
Temperature: params.Temperature, MaxTokens: params.MaxTokens, TopP: params.TopP,
|
|
TimeoutSeconds: params.TimeoutSeconds, ServiceTier: params.ServiceTier,
|
|
ReasoningEffort: params.ReasoningEffort, APIKeyEnv: params.APIKeyEnv, ExtraParams: extra,
|
|
},
|
|
APIKeyRequired: inspection.APIKeyRequired,
|
|
}
|
|
}
|
|
|
|
func formatPromptText(report promptInspectionReport) string {
|
|
var output strings.Builder
|
|
fmt.Fprintf(&output, "Prompt ID: %s\n", strconv.Quote(report.PromptID))
|
|
fmt.Fprintf(&output, "Prompt version: %s\n", strconv.Quote(report.PromptVersion))
|
|
fmt.Fprintf(&output, "Prompt hash: %s\n", strconv.Quote(report.PromptHash))
|
|
fmt.Fprintf(&output, "Default profile ID: %s\n", strconv.Quote(report.DefaultProfileID))
|
|
output.WriteString("Inputs:\n")
|
|
for _, input := range report.Inputs {
|
|
fmt.Fprintf(&output, " - name: %s\n required: %t\n content_type: %s\n description: %s\n", strconv.Quote(input.Name), input.Required, strconv.Quote(input.ContentType), strconv.Quote(input.Description))
|
|
}
|
|
if len(report.Inputs) == 0 {
|
|
output.WriteString(" []\n")
|
|
}
|
|
fmt.Fprintf(&output, "Output contract:\n format: %s\n validation_mode: %s\n schema_path: %s\n repair_attempts: %d\n", strconv.Quote(report.OutputContract.Format), strconv.Quote(report.OutputContract.ValidationMode), strconv.Quote(report.OutputContract.SchemaPath), report.OutputContract.RepairAttempts)
|
|
return output.String()
|
|
}
|
|
|
|
func formatProfileText(report profileInspectionReport) (string, error) {
|
|
params := report.EffectiveModelParams
|
|
var output strings.Builder
|
|
fmt.Fprintf(&output, "Profile ID: %s\n", strconv.Quote(report.ProfileID))
|
|
output.WriteString("Effective model params:\n")
|
|
fmt.Fprintf(&output, " backend_id: %s\n endpoint: %s\n model: %s\n temperature: %s\n max_tokens: %d\n top_p: %s\n timeout_seconds: %d\n service_tier: %s\n reasoning_effort: %s\n api_key_env: %s\n", strconv.Quote(params.BackendID), strconv.Quote(params.Endpoint), strconv.Quote(params.Model), strconv.FormatFloat(params.Temperature, 'g', -1, 64), params.MaxTokens, strconv.FormatFloat(params.TopP, 'g', -1, 64), params.TimeoutSeconds, strconv.Quote(params.ServiceTier), strconv.Quote(params.ReasoningEffort), strconv.Quote(params.APIKeyEnv))
|
|
output.WriteString(" extra_params:\n")
|
|
var nested strings.Builder
|
|
if err := encodeYAML(&nested, params.ExtraParams); err != nil {
|
|
return "", err
|
|
}
|
|
for _, line := range strings.Split(strings.TrimSuffix(nested.String(), "\n"), "\n") {
|
|
output.WriteString(" ")
|
|
output.WriteString(line)
|
|
output.WriteByte('\n')
|
|
}
|
|
fmt.Fprintf(&output, "API key required: %t\n", report.APIKeyRequired)
|
|
return output.String(), nil
|
|
}
|