Add PromptKit engine adapters
This commit is contained in:
165
integrations/ckprompt/adapters.go
Normal file
165
integrations/ckprompt/adapters.go
Normal file
@@ -0,0 +1,165 @@
|
||||
package ckprompt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||
"gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig"
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
promptkit "gitea.maximumdirect.net/eric/promptkit"
|
||||
)
|
||||
|
||||
// PromptRequest selects one named prompt for inspection.
|
||||
type PromptRequest struct {
|
||||
Selection ckconfig.Selection
|
||||
ID string
|
||||
Version commandkit.Supplied[string]
|
||||
}
|
||||
|
||||
// ProfileRequest selects one named profile for inspection.
|
||||
type ProfileRequest struct {
|
||||
Selection ckconfig.Selection
|
||||
ID string
|
||||
}
|
||||
|
||||
// Suite describes application-owned input, shared state, and named inspection
|
||||
// capabilities. Its private issues retain adapter construction failures.
|
||||
type Suite[I, S any] struct {
|
||||
NewInput func() I
|
||||
NewState func() S
|
||||
Selectors []commandkit.Flag[I]
|
||||
CheckInput func(ckconfig.Selection, I) error
|
||||
InspectPrompt func(context.Context, PromptRequest, I, S) (commandkit.Acquired[*promptkit.PromptInspection], error)
|
||||
InspectProfile func(context.Context, ProfileRequest, I, S) (commandkit.Acquired[*promptkit.ProfileInspection], error)
|
||||
|
||||
issues []error
|
||||
}
|
||||
|
||||
type engineLoader[I, S any] func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error)
|
||||
|
||||
// ConfigScope adapts PromptKit's captured configuration inspection to the
|
||||
// standard configuration commands. The engine is acquired only when an
|
||||
// operation selects this scope.
|
||||
func ConfigScope[I, S any](load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error)) ckconfig.Scope[I, S] {
|
||||
scope := ckconfig.Scope[I, S]{
|
||||
Name: "promptkit",
|
||||
Summary: "PromptKit engine configuration.",
|
||||
Operations: []ckconfig.Operation{ckconfig.Validate, ckconfig.Show, ckconfig.Sources, ckconfig.Explain},
|
||||
}
|
||||
if load == nil {
|
||||
return ckconfig.WithScopeError(scope, errors.New("PromptKit configuration loader is nil"))
|
||||
}
|
||||
|
||||
scope.Acquire = func(ctx context.Context, request ckconfig.Request, input I, state S) (commandkit.Acquired[ckconfig.View], error) {
|
||||
_, inspection, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, load)
|
||||
if err != nil {
|
||||
return commandkit.Acquired[ckconfig.View]{Diagnostics: diagnostics}, err
|
||||
}
|
||||
return commandkit.Acquired[ckconfig.View]{
|
||||
Value: ckconfig.View{
|
||||
Effective: inspection,
|
||||
Sources: inspection,
|
||||
Explanation: inspection,
|
||||
},
|
||||
Diagnostics: diagnostics,
|
||||
}, nil
|
||||
}
|
||||
return scope
|
||||
}
|
||||
|
||||
// FromEngine creates named prompt and profile inspection callbacks backed by a
|
||||
// PromptKit engine loader. A nil loader is retained as an adapter issue.
|
||||
func FromEngine[I, S any](load func(context.Context, ckconfig.Selection, I, S) (*promptkit.Engine, error)) Suite[I, S] {
|
||||
if load == nil {
|
||||
return Suite[I, S]{issues: []error{errors.New("PromptKit engine loader is nil")}}
|
||||
}
|
||||
|
||||
return Suite[I, S]{
|
||||
InspectPrompt: func(ctx context.Context, request PromptRequest, input I, state S) (commandkit.Acquired[*promptkit.PromptInspection], error) {
|
||||
engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, load)
|
||||
if err != nil {
|
||||
return commandkit.Acquired[*promptkit.PromptInspection]{Diagnostics: diagnostics}, err
|
||||
}
|
||||
inspection, err := engine.InspectPrompt(ctx, request.ID, request.Version.Value)
|
||||
return commandkit.Acquired[*promptkit.PromptInspection]{Value: inspection, Diagnostics: diagnostics}, err
|
||||
},
|
||||
InspectProfile: func(ctx context.Context, request ProfileRequest, input I, state S) (commandkit.Acquired[*promptkit.ProfileInspection], error) {
|
||||
engine, _, diagnostics, err := loadPromptKitEngine(ctx, request.Selection, input, state, load)
|
||||
if err != nil {
|
||||
return commandkit.Acquired[*promptkit.ProfileInspection]{Diagnostics: diagnostics}, err
|
||||
}
|
||||
inspection, err := engine.InspectProfile(ctx, request.ID)
|
||||
return commandkit.Acquired[*promptkit.ProfileInspection]{Value: inspection, Diagnostics: diagnostics}, err
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func loadPromptKitEngine[I, S any](
|
||||
ctx context.Context,
|
||||
selection ckconfig.Selection,
|
||||
input I,
|
||||
state S,
|
||||
load engineLoader[I, S],
|
||||
) (*promptkit.Engine, promptkit.ConfigurationInspection, []commandkit.Diagnostic, error) {
|
||||
engine, err := load(ctx, selection, input, state)
|
||||
if err != nil {
|
||||
var loadErr *configkit.LoadError
|
||||
if errors.As(err, &loadErr) {
|
||||
return nil, promptkit.ConfigurationInspection{}, promptKitDiagnostics("promptkit", loadErr.Sources(), loadErr.Diagnostics()), err
|
||||
}
|
||||
return nil, promptkit.ConfigurationInspection{}, nil, err
|
||||
}
|
||||
if engine == nil {
|
||||
return nil, promptkit.ConfigurationInspection{}, nil, errors.New("PromptKit configuration loader returned a nil engine")
|
||||
}
|
||||
if ctx != nil {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, promptkit.ConfigurationInspection{}, nil, err
|
||||
}
|
||||
}
|
||||
inspection, err := engine.InspectConfiguration()
|
||||
if err != nil {
|
||||
return nil, promptkit.ConfigurationInspection{}, nil, err
|
||||
}
|
||||
diagnostics := promptKitDiagnostics("promptkit", inspection.Sources(), inspection.Diagnostics())
|
||||
return engine, inspection, diagnostics, nil
|
||||
}
|
||||
|
||||
func promptKitDiagnostics(scope string, sources []configkit.SourceInfo, records []configkit.Diagnostic) []commandkit.Diagnostic {
|
||||
if len(records) == 0 {
|
||||
return nil
|
||||
}
|
||||
severity := commandkit.SeverityWarning
|
||||
for _, record := range records {
|
||||
if record.Severity == configkit.SeverityError {
|
||||
severity = commandkit.SeverityError
|
||||
break
|
||||
}
|
||||
}
|
||||
// Snapshot provider-owned slices before deferring their report to invocation.
|
||||
sources = append([]configkit.SourceInfo(nil), sources...)
|
||||
records = append([]configkit.Diagnostic(nil), records...)
|
||||
return []commandkit.Diagnostic{{
|
||||
Severity: severity,
|
||||
Write: func(writer io.Writer) error {
|
||||
if _, err := fmt.Fprintf(writer, "Configuration scope %s:\n", strconv.Quote(scope)); err != nil {
|
||||
return err
|
||||
}
|
||||
if len(sources) > 0 {
|
||||
if _, err := io.WriteString(writer, "Source IDs:\n"); err != nil {
|
||||
return err
|
||||
}
|
||||
for _, source := range sources {
|
||||
if _, err := fmt.Fprintf(writer, " %s: label=%s, path=%s\n", strconv.Quote(source.ID), strconv.Quote(source.Label), strconv.Quote(source.Path)); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return configkit.WriteDiagnostics(writer, records)
|
||||
},
|
||||
}}
|
||||
}
|
||||
197
integrations/ckprompt/adapters_test.go
Normal file
197
integrations/ckprompt/adapters_test.go
Normal file
@@ -0,0 +1,197 @@
|
||||
package ckprompt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
|
||||
commandkit "gitea.maximumdirect.net/eric/CommandKit"
|
||||
"gitea.maximumdirect.net/eric/CommandKit/integrations/ckconfig"
|
||||
configkit "gitea.maximumdirect.net/eric/ConfigKit"
|
||||
promptkit "gitea.maximumdirect.net/eric/promptkit"
|
||||
)
|
||||
|
||||
const adapterPrompt = `prompts:
|
||||
briefing:
|
||||
"1":
|
||||
messages:
|
||||
- role: user
|
||||
content: "Hello"
|
||||
output:
|
||||
format: text
|
||||
validation_mode: none
|
||||
backends:
|
||||
local:
|
||||
endpoint: "https://example.invalid/v1"
|
||||
profiles:
|
||||
default:
|
||||
backend: local
|
||||
model: example
|
||||
`
|
||||
|
||||
func adapterEngine(t *testing.T) *promptkit.Engine {
|
||||
t.Helper()
|
||||
inputs, err := promptkit.FilesystemInputs("adapter fixture", fstest.MapFS{
|
||||
"config/prompt.yml": &fstest.MapFile{Data: []byte(adapterPrompt)},
|
||||
}, "config")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{
|
||||
Underlays: []configkit.Underlay{{Inputs: inputs}},
|
||||
}})
|
||||
if err != nil {
|
||||
t.Fatalf("construct PromptKit fixture: %v", err)
|
||||
}
|
||||
return engine
|
||||
}
|
||||
|
||||
func TestConfigScopeUsesNativeInspectionFacetsAndInvocationValues(t *testing.T) {
|
||||
type input struct{ token string }
|
||||
type state struct{ token string }
|
||||
type contextKey struct{}
|
||||
|
||||
engine := adapterEngine(t)
|
||||
wantContext := context.WithValue(context.Background(), contextKey{}, "invocation")
|
||||
wantInput := input{token: "input"}
|
||||
wantState := state{token: "state"}
|
||||
loaderCalls := 0
|
||||
scope := ConfigScope(func(ctx context.Context, selection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
||||
loaderCalls++
|
||||
if ctx.Value(contextKey{}) != "invocation" || !selection.Config.Set || selection.Config.Value != "prompt-config.yml" || gotInput != wantInput || gotState != wantState {
|
||||
t.Errorf("loader args = ctx=%v selection=%#v input=%#v state=%#v", ctx.Value(contextKey{}), selection, gotInput, gotState)
|
||||
}
|
||||
return engine, nil
|
||||
})
|
||||
if scope.Name != "promptkit" || len(scope.Operations) != 4 || scope.Acquire == nil {
|
||||
t.Fatalf("configuration scope = %#v", scope)
|
||||
}
|
||||
acquired, err := scope.Acquire(wantContext, ckconfig.Request{
|
||||
Operation: ckconfig.Show,
|
||||
Selection: ckconfig.Selection{Config: commandkit.Supplied[string]{Value: "prompt-config.yml", Set: true}},
|
||||
}, wantInput, wantState)
|
||||
if err != nil || loaderCalls != 1 {
|
||||
t.Fatalf("scope acquisition = (%#v, %v), calls=%d", acquired, err, loaderCalls)
|
||||
}
|
||||
var effective bytes.Buffer
|
||||
if err := acquired.Value.Effective.WriteEffective(&effective); err != nil || !strings.Contains(effective.String(), "briefing:") {
|
||||
t.Fatalf("native effective report = %q, %v", effective.String(), err)
|
||||
}
|
||||
if sources := acquired.Value.Sources.Sources(); len(sources) == 0 || sources[0].ID == "" {
|
||||
t.Fatalf("native sources = %#v", sources)
|
||||
}
|
||||
if explanation, ok := acquired.Value.Explanation.Explain(configkit.Path{}); !ok || !explanation.Effective.Present {
|
||||
t.Fatalf("native root explanation = %#v, found=%t", explanation, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestConfigScopeRetainsNilLoaderIssueAfterCapabilitiesAreCleared(t *testing.T) {
|
||||
scope := ConfigScope[int, int](nil)
|
||||
if scope.Acquire != nil {
|
||||
t.Fatal("nil loader installed an acquisition callback")
|
||||
}
|
||||
scope.Operations = nil
|
||||
app, err := commandkit.New(commandkit.Identity{Name: "example"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
declaration := ckconfig.Command(ckconfig.Suite[int, int]{Scopes: []ckconfig.Scope[int, int]{scope}})
|
||||
if err := app.Register(declaration); err == nil || !strings.Contains(err.Error(), "PromptKit configuration loader is nil") {
|
||||
t.Fatalf("narrowed nil-loader scope registered: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestFromEnginePassesRequestsAndRetainsNilLoaderIssue(t *testing.T) {
|
||||
type input struct{ value string }
|
||||
type state struct{ value string }
|
||||
type contextKey struct{}
|
||||
|
||||
engine := adapterEngine(t)
|
||||
wantContext := context.WithValue(context.Background(), contextKey{}, "prompt invocation")
|
||||
wantInput := input{value: "typed input"}
|
||||
wantState := state{value: "shared state"}
|
||||
loaderCalls := 0
|
||||
suite := FromEngine(func(ctx context.Context, selection ckconfig.Selection, gotInput input, gotState state) (*promptkit.Engine, error) {
|
||||
loaderCalls++
|
||||
if ctx.Value(contextKey{}) != "prompt invocation" || !selection.Overlay.Set || selection.Overlay.Value != "overlay.yml" || gotInput != wantInput || gotState != wantState {
|
||||
t.Errorf("loader args = ctx=%v selection=%#v input=%#v state=%#v", ctx.Value(contextKey{}), selection, gotInput, gotState)
|
||||
}
|
||||
return engine, nil
|
||||
})
|
||||
selection := ckconfig.Selection{Overlay: commandkit.Supplied[string]{Value: "overlay.yml", Set: true}}
|
||||
version := commandkit.Supplied[string]{Value: "1", Set: true}
|
||||
prompt, err := suite.InspectPrompt(wantContext, PromptRequest{Selection: selection, ID: "briefing", Version: version}, wantInput, wantState)
|
||||
if err != nil || prompt.Value == nil || prompt.Value.PromptID != "briefing" || prompt.Value.PromptVersion != "1" || loaderCalls != 1 {
|
||||
t.Fatalf("prompt inspection = (%#v, %v), calls=%d", prompt, err, loaderCalls)
|
||||
}
|
||||
defaultPrompt, err := suite.InspectPrompt(wantContext, PromptRequest{Selection: selection, ID: "briefing"}, wantInput, wantState)
|
||||
if err != nil || defaultPrompt.Value == nil || defaultPrompt.Value.PromptVersion != "1" || loaderCalls != 2 {
|
||||
t.Fatalf("empty version default = (%#v, %v), calls=%d", defaultPrompt, err, loaderCalls)
|
||||
}
|
||||
profile, err := suite.InspectProfile(wantContext, ProfileRequest{Selection: selection, ID: "default"}, wantInput, wantState)
|
||||
if err != nil || profile.Value == nil || profile.Value.ProfileID != "default" || loaderCalls != 3 {
|
||||
t.Fatalf("profile inspection = (%#v, %v), calls=%d", profile, err, loaderCalls)
|
||||
}
|
||||
if version.Set != true || version.Value != "1" {
|
||||
t.Fatalf("custom callback request lost version presence: %#v", version)
|
||||
}
|
||||
|
||||
empty := FromEngine[int, int](nil)
|
||||
empty.InspectPrompt = nil
|
||||
empty.InspectProfile = nil
|
||||
if len(empty.issues) != 1 || !strings.Contains(empty.issues[0].Error(), "PromptKit engine loader is nil") {
|
||||
t.Fatalf("nil-loader issue was lost when callbacks were cleared: %#v", empty.issues)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdapterAcquisitionPreservesLoadAndContextErrors(t *testing.T) {
|
||||
loadErr := errors.New("loader failure")
|
||||
suite := FromEngine[int, int](func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) {
|
||||
return nil, loadErr
|
||||
})
|
||||
acquired, err := suite.InspectProfile(context.Background(), ProfileRequest{ID: "default"}, 0, 0)
|
||||
if err != loadErr || len(acquired.Diagnostics) != 0 {
|
||||
t.Fatalf("loader failure = (%#v, %v), want original error", acquired, err)
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
engine := adapterEngine(t)
|
||||
called := false
|
||||
suite = FromEngine[int, int](func(context.Context, ckconfig.Selection, int, int) (*promptkit.Engine, error) {
|
||||
called = true
|
||||
return engine, nil
|
||||
})
|
||||
acquired, err = suite.InspectProfile(ctx, ProfileRequest{ID: "default"}, 0, 0)
|
||||
if !errors.Is(err, context.Canceled) || acquired.Value != nil || !called {
|
||||
t.Fatalf("cancelled profile acquisition = (%#v, %v), loader called=%t", acquired, err, called)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPromptKitDiagnosticsRemainDeferredAndPropagateWriterErrors(t *testing.T) {
|
||||
writeErr := errors.New("diagnostic writer failed")
|
||||
diagnostics := promptKitDiagnostics("promptkit", []configkit.SourceInfo{{ID: "source"}}, []configkit.Diagnostic{{
|
||||
Severity: configkit.SeverityWarning,
|
||||
Message: "safe warning",
|
||||
}})
|
||||
if len(diagnostics) != 1 || diagnostics[0].Severity != commandkit.SeverityWarning {
|
||||
t.Fatalf("diagnostics = %#v", diagnostics)
|
||||
}
|
||||
if err := diagnostics[0].Write(failingDiagnosticWriter{err: writeErr}); !errors.Is(err, writeErr) {
|
||||
t.Fatalf("diagnostic write error = %v", err)
|
||||
}
|
||||
var output bytes.Buffer
|
||||
if err := diagnostics[0].Write(&output); err != nil || !strings.Contains(output.String(), "safe warning") || !strings.Contains(output.String(), `"source"`) {
|
||||
t.Fatalf("diagnostic text = %q, %v", output.String(), err)
|
||||
}
|
||||
}
|
||||
|
||||
type failingDiagnosticWriter struct{ err error }
|
||||
|
||||
func (writer failingDiagnosticWriter) Write([]byte) (int, error) { return 0, writer.err }
|
||||
|
||||
var _ io.Writer = failingDiagnosticWriter{}
|
||||
@@ -1,41 +0,0 @@
|
||||
package ckprompt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
"gitea.maximumdirect.net/eric/promptkit"
|
||||
)
|
||||
|
||||
// NewPromptKitScope adapts PromptKit's captured configuration inspection to
|
||||
// CommandKit's configuration commands. The loader runs only when a selected
|
||||
// configuration operation acquires this scope. Successful construction and
|
||||
// inspection supply validation; the adapter does not execute prompts, resolve
|
||||
// credentials, or probe model availability.
|
||||
func NewPromptKitScope[C any](
|
||||
load func(context.Context, Selection, C) (*promptkit.Engine, error),
|
||||
) (ConfigScope[C], error) {
|
||||
if load == nil {
|
||||
return ConfigScope[C]{}, fmt.Errorf("PromptKit configuration loader is nil")
|
||||
}
|
||||
|
||||
return ConfigScope[C]{
|
||||
Name: "promptkit",
|
||||
Summary: "PromptKit engine configuration.",
|
||||
Operations: []ConfigOperation{ConfigValidate, ConfigShow, ConfigSources, ConfigExplain},
|
||||
Acquire: func(ctx context.Context, request ConfigRequest, appContext C) (Acquired[ConfigView], error) {
|
||||
_, inspection, diagnostics, err := loadPromptKitInspection(ctx, request.Selection, appContext, load)
|
||||
if err != nil {
|
||||
return Acquired[ConfigView]{Diagnostics: diagnostics}, err
|
||||
}
|
||||
return Acquired[ConfigView]{
|
||||
Value: ConfigView{
|
||||
Effective: inspection,
|
||||
Sources: inspection,
|
||||
Explanation: inspection,
|
||||
},
|
||||
Diagnostics: diagnostics,
|
||||
}, nil
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
Reference in New Issue
Block a user