Add fallback PromptKit profile assets
This commit is contained in:
@@ -62,21 +62,25 @@ completion responses and recorded profile manifests identify the adapter
|
|||||||
provider as `promptkit`.
|
provider as `promptkit`.
|
||||||
|
|
||||||
The CLI's profile-inspection engine and the production adapter use the same
|
The CLI's profile-inspection engine and the production adapter use the same
|
||||||
profile-source construction to apply the configured profile directory or file
|
profile-source construction to apply the configured profile directory or file,
|
||||||
and register the optional conventional `local` backend. Preflight therefore
|
the optional registered fallback profile assets, and the optional conventional
|
||||||
resolves the same ordinary profile source and backend membership as runtime
|
`local` backend. Preflight therefore resolves the same profile sources and
|
||||||
without performing generation. When the registration is absent, a profile
|
backend membership as runtime without performing generation. Fallback assets
|
||||||
selecting `backend: local` fails inspection instead of falling back to a
|
are mounted only when at least one source is registered. PromptKit owns source
|
||||||
built-in or endpoint-only target.
|
precedence and profile parsing: an operator-provided matching profile takes
|
||||||
|
precedence over a fallback profile without Notarius merging either document.
|
||||||
|
When the registration is absent, a profile selecting `backend: local` fails
|
||||||
|
inspection instead of falling back to a built-in or endpoint-only target.
|
||||||
|
|
||||||
Before execution, the adapter also contributes a non-secret checkpoint
|
Before execution, the adapter also contributes a non-secret checkpoint
|
||||||
fingerprint for the effective PromptKit profile source. It combines the
|
fingerprint for the effective PromptKit profile source. It combines the
|
||||||
identity of PromptKit's compiled-in profile catalog with a deterministic digest
|
identity of PromptKit's compiled-in profile catalog with a deterministic digest
|
||||||
of every YAML profile in the configured profile directory, or of the configured
|
of every YAML profile in the configured profile directory, or of the configured
|
||||||
profile file. The fingerprint contains neither profile content nor source
|
profile file, and a deterministic digest of the flattened fallback profile
|
||||||
paths. It covers both explicit binding profiles and prompt-selected defaults,
|
assets. The fingerprint contains neither profile content nor source paths. It
|
||||||
so changing a model or other profile setting cannot reuse checkpoints created
|
covers both explicit binding profiles and prompt-selected defaults, so changing
|
||||||
under the prior profile source. This cache identity is independent of durable
|
a model or other profile setting cannot reuse checkpoints created under the
|
||||||
|
prior profile source. This cache identity is independent of durable
|
||||||
profile provenance: run manifests continue to list only profiles actually
|
profile provenance: run manifests continue to list only profiles actually
|
||||||
observed during LLM calls. When the local backend is registered, a second
|
observed during LLM calls. When the local backend is registered, a second
|
||||||
fingerprint hashes its trimmed endpoint behind a stable marker. Changing that
|
fingerprint hashes its trimmed endpoint behind a stable marker. Changing that
|
||||||
@@ -109,10 +113,12 @@ only by the Notarius scheduler.
|
|||||||
|
|
||||||
## Prompt And Schema Assets
|
## Prompt And Schema Assets
|
||||||
|
|
||||||
An `AssetRegistry` collects prompt and schema filesystems from production module
|
An `AssetRegistry` collects prompt, schema, and optional fallback-profile
|
||||||
families. It flattens registered roots into the PromptKit filesystems and
|
filesystems from production module families. It flattens registered roots into
|
||||||
rejects invalid roots, unreadable assets, duplicate paths, and missing prompt
|
the corresponding PromptKit filesystems and rejects invalid roots, unreadable
|
||||||
or schema files during preparation. The framework’s `promptfs` helper combines
|
assets, duplicate paths, and missing prompt or schema files during preparation.
|
||||||
|
Fallback assets receive a safe content digest for checkpoint identity; raw
|
||||||
|
paths and bytes are never included. The framework’s `promptfs` helper combines
|
||||||
module-owned prompt files with reusable domain fragments without making the
|
module-owned prompt files with reusable domain fragments without making the
|
||||||
framework depend on D&D content.
|
framework depend on D&D content.
|
||||||
|
|
||||||
|
|||||||
@@ -8,11 +8,14 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func validateExplicitPromptKitProfiles(ctx context.Context, cfg config.Config, profileIDs []string) error {
|
func validateExplicitPromptKitProfiles(ctx context.Context, cfg config.Config, profileIDs []string, assets *llm.AssetRegistry) error {
|
||||||
if len(profileIDs) == 0 {
|
if len(profileIDs) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
inspector, err := llm.NewPromptKitProfileInspector(promptKitProfileSourceConfig(cfg))
|
inspector, err := llm.NewPromptKitProfileInspector(llm.PromptKitProfileInspectorConfig{
|
||||||
|
Source: promptKitProfileSourceConfig(cfg),
|
||||||
|
Assets: assets,
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("load PromptKit profiles: %w", err)
|
return fmt.Errorf("load PromptKit profiles: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,8 +10,10 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
|
"testing/fstest"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/notarius/internal/core/config"
|
"gitea.maximumdirect.net/eric/notarius/internal/core/config"
|
||||||
|
"gitea.maximumdirect.net/eric/notarius/internal/framework/llm"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestExplicitPromptKitProfileValidationInspectsProfilesWithoutGeneration(t *testing.T) {
|
func TestExplicitPromptKitProfileValidationInspectsProfilesWithoutGeneration(t *testing.T) {
|
||||||
@@ -112,7 +114,7 @@ api_key_env: NOTARIUS_PROMPTKIT_PROFILE_INSPECTION_TEST_KEY
|
|||||||
ctx, cancel = context.WithCancel(ctx)
|
ctx, cancel = context.WithCancel(ctx)
|
||||||
cancel()
|
cancel()
|
||||||
}
|
}
|
||||||
err := validateExplicitPromptKitProfiles(ctx, cfg, []string{tt.profileID})
|
err := validateExplicitPromptKitProfiles(ctx, cfg, []string{tt.profileID}, nil)
|
||||||
if len(tt.wantErr) == 0 {
|
if len(tt.wantErr) == 0 {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("validateExplicitPromptKitProfiles() error = %v, want nil", err)
|
t.Fatalf("validateExplicitPromptKitProfiles() error = %v, want nil", err)
|
||||||
@@ -136,3 +138,15 @@ api_key_env: NOTARIUS_PROMPTKIT_PROFILE_INSPECTION_TEST_KEY
|
|||||||
t.Fatalf("provider calls during profile inspection = %d, want 0", providerCalls.Load())
|
t.Fatalf("provider calls during profile inspection = %d, want 0", providerCalls.Load())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExplicitPromptKitProfileValidationUsesFallbackAssets(t *testing.T) {
|
||||||
|
assets := llm.NewAssetRegistry()
|
||||||
|
if err := assets.RegisterFallbackProfileFS(fstest.MapFS{
|
||||||
|
"profiles/fallback.yaml": {Data: []byte("id: fallback-profile\nendpoint: http://promptkit.test/v1\nmodel: fallback-model\n")},
|
||||||
|
}, "profiles"); err != nil {
|
||||||
|
t.Fatalf("RegisterFallbackProfileFS() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
if err := validateExplicitPromptKitProfiles(context.Background(), config.Default(), []string{"fallback-profile"}, assets); err != nil {
|
||||||
|
t.Fatalf("validateExplicitPromptKitProfiles() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -46,6 +46,7 @@ type Options struct {
|
|||||||
ChunkPlanStoreFactory pipeline.ChunkPlanStoreFactory
|
ChunkPlanStoreFactory pipeline.ChunkPlanStoreFactory
|
||||||
DebugRecorderFactory func(string) (pipeline.DebugRecorder, error)
|
DebugRecorderFactory func(string) (pipeline.DebugRecorder, error)
|
||||||
DebugTerminalFactory func(*debugbundle.SummaryWriter) DebugTerminalWriter
|
DebugTerminalFactory func(*debugbundle.SummaryWriter) DebugTerminalWriter
|
||||||
|
promptKitAssets *frameworkllm.AssetRegistry
|
||||||
}
|
}
|
||||||
|
|
||||||
type LLMRuntimeOverrides struct {
|
type LLMRuntimeOverrides struct {
|
||||||
@@ -121,6 +122,7 @@ func normalizeOptions(opts Options) (Options, error) {
|
|||||||
}
|
}
|
||||||
opts.Registries = components.registries
|
opts.Registries = components.registries
|
||||||
opts.Catalog = catalogFromRegistries(components.registries)
|
opts.Catalog = catalogFromRegistries(components.registries)
|
||||||
|
opts.promptKitAssets = components.assets
|
||||||
if opts.LLMClientFactory == nil {
|
if opts.LLMClientFactory == nil {
|
||||||
opts.LLMClientFactory = productionLLMClientFactoryWithAssets(components.assets)
|
opts.LLMClientFactory = productionLLMClientFactoryWithAssets(components.assets)
|
||||||
}
|
}
|
||||||
@@ -340,7 +342,7 @@ func runPipelineCommand(args []string, stdout, stderr io.Writer, opts Options) i
|
|||||||
return failPipelineCommand(stderr, commandState, terminalWriter, err)
|
return failPipelineCommand(stderr, commandState, terminalWriter, err)
|
||||||
}
|
}
|
||||||
profileIDs := effectiveLLMProfileIDs(effective.ResolvedPipeline)
|
profileIDs := effectiveLLMProfileIDs(effective.ResolvedPipeline)
|
||||||
if err := validateExplicitPromptKitProfiles(context.Background(), effective.Config, profileIDs); err != nil {
|
if err := validateExplicitPromptKitProfiles(context.Background(), effective.Config, profileIDs, opts.promptKitAssets); err != nil {
|
||||||
return failPipelineCommand(stderr, commandState, terminalWriter, err)
|
return failPipelineCommand(stderr, commandState, terminalWriter, err)
|
||||||
}
|
}
|
||||||
workingDir, err := os.Getwd()
|
workingDir, err := os.Getwd()
|
||||||
@@ -1048,7 +1050,7 @@ func runConfigValidate(args []string, stdout, stderr io.Writer, opts Options) in
|
|||||||
fmt.Fprintf(stderr, "notarius: %v\n", err)
|
fmt.Fprintf(stderr, "notarius: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
if err := validateExplicitPromptKitProfiles(context.Background(), effective.Config, effectiveLLMProfileIDs(effective.ResolvedPipeline)); err != nil {
|
if err := validateExplicitPromptKitProfiles(context.Background(), effective.Config, effectiveLLMProfileIDs(effective.ResolvedPipeline), opts.promptKitAssets); err != nil {
|
||||||
fmt.Fprintf(stderr, "notarius: %v\n", err)
|
fmt.Fprintf(stderr, "notarius: %v\n", err)
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,8 +21,9 @@ type AssetSource struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type AssetRegistry struct {
|
type AssetRegistry struct {
|
||||||
prompts []AssetSource
|
prompts []AssetSource
|
||||||
schemas []AssetSource
|
schemas []AssetSource
|
||||||
|
fallbackProfiles []AssetSource
|
||||||
}
|
}
|
||||||
|
|
||||||
type AssetHashPart struct {
|
type AssetHashPart struct {
|
||||||
@@ -58,6 +59,20 @@ func (r *AssetRegistry) RegisterSchemaFS(fsys fs.FS, root string) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RegisterFallbackProfileFS registers profile assets that PromptKit uses only
|
||||||
|
// when an operator-configured source does not provide a matching profile.
|
||||||
|
func (r *AssetRegistry) RegisterFallbackProfileFS(fsys fs.FS, root string) error {
|
||||||
|
if r == nil {
|
||||||
|
return fmt.Errorf("asset registry must not be nil")
|
||||||
|
}
|
||||||
|
source, err := newAssetSource(fsys, root)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("register fallback profile assets: %w", err)
|
||||||
|
}
|
||||||
|
r.fallbackProfiles = append(r.fallbackProfiles, source)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *AssetRegistry) PromptFS() (fs.FS, error) {
|
func (r *AssetRegistry) PromptFS() (fs.FS, error) {
|
||||||
if r == nil {
|
if r == nil {
|
||||||
return nil, fmt.Errorf("asset registry must not be nil")
|
return nil, fmt.Errorf("asset registry must not be nil")
|
||||||
@@ -72,19 +87,76 @@ func (r *AssetRegistry) SchemaFS() (fs.FS, error) {
|
|||||||
return flattenAssetSources(r.schemas)
|
return flattenAssetSources(r.schemas)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *AssetRegistry) FallbackProfileFS() (fs.FS, error) {
|
||||||
|
if r == nil {
|
||||||
|
return nil, fmt.Errorf("asset registry must not be nil")
|
||||||
|
}
|
||||||
|
return flattenAssetSources(r.fallbackProfiles)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FallbackProfileDigest returns a deterministic, non-secret identity for the
|
||||||
|
// flattened fallback profile assets.
|
||||||
|
func (r *AssetRegistry) FallbackProfileDigest() (string, error) {
|
||||||
|
_, digest, _, err := r.fallbackProfileAssets()
|
||||||
|
return digest, err
|
||||||
|
}
|
||||||
|
|
||||||
func (r *AssetRegistry) PromptKitOptions() ([]promptkit.Option, error) {
|
func (r *AssetRegistry) PromptKitOptions() ([]promptkit.Option, error) {
|
||||||
|
options, _, err := r.promptKitOptions()
|
||||||
|
return options, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AssetRegistry) promptKitOptions() ([]promptkit.Option, string, error) {
|
||||||
promptFS, err := r.PromptFS()
|
promptFS, err := r.PromptFS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("prepare prompt assets: %w", err)
|
return nil, "", fmt.Errorf("prepare prompt assets: %w", err)
|
||||||
}
|
}
|
||||||
schemaFS, err := r.SchemaFS()
|
schemaFS, err := r.SchemaFS()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("prepare schema assets: %w", err)
|
return nil, "", fmt.Errorf("prepare schema assets: %w", err)
|
||||||
}
|
}
|
||||||
return []promptkit.Option{
|
options := []promptkit.Option{
|
||||||
promptkit.WithPromptFS(promptFS, "."),
|
promptkit.WithPromptFS(promptFS, "."),
|
||||||
promptkit.WithSchemaFS(schemaFS, "."),
|
promptkit.WithSchemaFS(schemaFS, "."),
|
||||||
}, nil
|
}
|
||||||
|
fallbackFS, fallbackDigest, hasFallback, err := r.fallbackProfileAssets()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
if hasFallback {
|
||||||
|
options = append(options, promptkit.WithFallbackProfileFS(fallbackFS, "."))
|
||||||
|
}
|
||||||
|
return options, fallbackDigest, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AssetRegistry) promptKitFallbackProfileOption() (promptkit.Option, bool, error) {
|
||||||
|
fallbackFS, _, hasFallback, err := r.fallbackProfileAssets()
|
||||||
|
if err != nil {
|
||||||
|
return nil, false, err
|
||||||
|
}
|
||||||
|
if !hasFallback {
|
||||||
|
return nil, false, nil
|
||||||
|
}
|
||||||
|
return promptkit.WithFallbackProfileFS(fallbackFS, "."), true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *AssetRegistry) fallbackProfileAssets() (fs.FS, string, bool, error) {
|
||||||
|
if r == nil {
|
||||||
|
return nil, "", false, fmt.Errorf("asset registry must not be nil")
|
||||||
|
}
|
||||||
|
if len(r.fallbackProfiles) == 0 {
|
||||||
|
empty := sha256.Sum256([]byte("notarius:fallback-profile-assets:empty"))
|
||||||
|
return nil, "sha256:" + hex.EncodeToString(empty[:]), false, nil
|
||||||
|
}
|
||||||
|
fallbackFS, err := r.FallbackProfileFS()
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", false, fmt.Errorf("prepare fallback profile assets: %w", err)
|
||||||
|
}
|
||||||
|
digest, err := hashAssetFilesystem(fallbackFS)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", false, err
|
||||||
|
}
|
||||||
|
return fallbackFS, digest, true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func HashAssets(parts []AssetHashPart) (string, error) {
|
func HashAssets(parts []AssetHashPart) (string, error) {
|
||||||
@@ -117,6 +189,28 @@ func HashAssets(parts []AssetHashPart) (string, error) {
|
|||||||
return "sha256:" + hex.EncodeToString(hash.Sum(nil)), nil
|
return "sha256:" + hex.EncodeToString(hash.Sum(nil)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func hashAssetFilesystem(fsys fs.FS) (string, error) {
|
||||||
|
var parts []AssetHashPart
|
||||||
|
err := fs.WalkDir(fsys, ".", func(name string, entry fs.DirEntry, walkErr error) error {
|
||||||
|
if walkErr != nil {
|
||||||
|
return walkErr
|
||||||
|
}
|
||||||
|
if entry.IsDir() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
parts = append(parts, AssetHashPart{FS: fsys, Path: name})
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", fmt.Errorf("walk assets for digest: %w", err)
|
||||||
|
}
|
||||||
|
if len(parts) > 0 {
|
||||||
|
return HashAssets(parts)
|
||||||
|
}
|
||||||
|
empty := sha256.Sum256([]byte("notarius:fallback-profile-assets:empty"))
|
||||||
|
return "sha256:" + hex.EncodeToString(empty[:]), nil
|
||||||
|
}
|
||||||
|
|
||||||
func newAssetSource(fsys fs.FS, root string) (AssetSource, error) {
|
func newAssetSource(fsys fs.FS, root string) (AssetSource, error) {
|
||||||
if fsys == nil {
|
if fsys == nil {
|
||||||
return AssetSource{}, fmt.Errorf("filesystem must not be nil")
|
return AssetSource{}, fmt.Errorf("filesystem must not be nil")
|
||||||
@@ -248,6 +342,9 @@ func (m assetMapFS) dirEntries(dir string) []fs.DirEntry {
|
|||||||
children[childName] = entry
|
children[childName] = entry
|
||||||
}
|
}
|
||||||
if len(children) == 0 {
|
if len(children) == 0 {
|
||||||
|
if dir == "." {
|
||||||
|
return []fs.DirEntry{}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
names := make([]string, 0, len(children))
|
names := make([]string, 0, len(children))
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package llm
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io/fs"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
"testing/fstest"
|
"testing/fstest"
|
||||||
@@ -98,6 +99,89 @@ func TestAssetRegistryRejectsDuplicateAssetPaths(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAssetRegistryCombinesFallbackProfileSources(t *testing.T) {
|
||||||
|
registry := NewAssetRegistry()
|
||||||
|
if err := registry.RegisterFallbackProfileFS(fstest.MapFS{
|
||||||
|
"first/profiles/one.yaml": {Data: []byte("id: one\nmodel: first\n")},
|
||||||
|
}, "first/profiles"); err != nil {
|
||||||
|
t.Fatalf("RegisterFallbackProfileFS() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
if err := registry.RegisterFallbackProfileFS(fstest.MapFS{
|
||||||
|
"second/two.yaml": {Data: []byte("id: two\nmodel: second\n")},
|
||||||
|
}, "second"); err != nil {
|
||||||
|
t.Fatalf("RegisterFallbackProfileFS() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
fallbackFS, err := registry.FallbackProfileFS()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("FallbackProfileFS() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
for _, name := range []string{"one.yaml", "two.yaml"} {
|
||||||
|
if _, err := fs.ReadFile(fallbackFS, name); err != nil {
|
||||||
|
t.Fatalf("FallbackProfileFS().ReadFile(%q) error = %v, want nil", name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAssetRegistryRejectsInvalidFallbackProfileRoot(t *testing.T) {
|
||||||
|
registry := NewAssetRegistry()
|
||||||
|
err := registry.RegisterFallbackProfileFS(fstest.MapFS{}, "../profiles")
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "invalid path") {
|
||||||
|
t.Fatalf("RegisterFallbackProfileFS() error = %v, want invalid root error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAssetRegistryRejectsUnreadableFallbackProfileAssets(t *testing.T) {
|
||||||
|
registry := NewAssetRegistry()
|
||||||
|
if err := registry.RegisterFallbackProfileFS(unreadableAssetFS{}, "."); err != nil {
|
||||||
|
t.Fatalf("RegisterFallbackProfileFS() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
_, err := registry.FallbackProfileFS()
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "permission denied") {
|
||||||
|
t.Fatalf("FallbackProfileFS() error = %v, want unreadable asset error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAssetRegistryRejectsDuplicateFallbackProfilePaths(t *testing.T) {
|
||||||
|
registry := NewAssetRegistry()
|
||||||
|
if err := registry.RegisterFallbackProfileFS(fstest.MapFS{"first/profile.yaml": {Data: []byte("id: first\n")}}, "first"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if err := registry.RegisterFallbackProfileFS(fstest.MapFS{"second/profile.yaml": {Data: []byte("id: second\n")}}, "second"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
_, err := registry.FallbackProfileFS()
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "duplicate asset path") {
|
||||||
|
t.Fatalf("FallbackProfileFS() error = %v, want duplicate path error", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAssetRegistryFallbackProfileDigestTracksContentWithoutLeakingIt(t *testing.T) {
|
||||||
|
digestFor := func(content string) string {
|
||||||
|
t.Helper()
|
||||||
|
registry := NewAssetRegistry()
|
||||||
|
if err := registry.RegisterFallbackProfileFS(fstest.MapFS{
|
||||||
|
"profiles/profile.yaml": {Data: []byte(content)},
|
||||||
|
}, "profiles"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
digest, err := registry.FallbackProfileDigest()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
return digest
|
||||||
|
}
|
||||||
|
|
||||||
|
first := digestFor("id: fallback\nmodel: model-one\n")
|
||||||
|
second := digestFor("id: fallback\nmodel: model-two\n")
|
||||||
|
if first == second {
|
||||||
|
t.Fatalf("fallback profile digests = %q and %q, want content change", first, second)
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(first, "sha256:") || strings.Contains(first, "model-one") || strings.Contains(first, "profile.yaml") {
|
||||||
|
t.Fatalf("fallback profile digest leaked source details: %q", first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestAssetRegistryCombinesNamespacedPromptSources(t *testing.T) {
|
func TestAssetRegistryCombinesNamespacedPromptSources(t *testing.T) {
|
||||||
registry := NewAssetRegistry()
|
registry := NewAssetRegistry()
|
||||||
mustRegisterPromptFS(t, registry, fstest.MapFS{
|
mustRegisterPromptFS(t, registry, fstest.MapFS{
|
||||||
@@ -196,3 +280,9 @@ output:
|
|||||||
repair_attempts: 0
|
repair_attempts: 0
|
||||||
`
|
`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type unreadableAssetFS struct{}
|
||||||
|
|
||||||
|
func (unreadableAssetFS) Open(name string) (fs.File, error) {
|
||||||
|
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrPermission}
|
||||||
|
}
|
||||||
|
|||||||
@@ -37,12 +37,13 @@ type PromptKitClientConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type PromptKitClient struct {
|
type PromptKitClient struct {
|
||||||
engine *promptkit.Engine
|
engine *promptkit.Engine
|
||||||
recorder *LLMProfileRecorder
|
recorder *LLMProfileRecorder
|
||||||
profileDir string
|
profileDir string
|
||||||
profileFile string
|
profileFile string
|
||||||
localEndpoint string
|
localEndpoint string
|
||||||
reasoningEffort *string
|
fallbackProfileDigest string
|
||||||
|
reasoningEffort *string
|
||||||
}
|
}
|
||||||
|
|
||||||
type LLMProfileRecorder struct {
|
type LLMProfileRecorder struct {
|
||||||
@@ -69,7 +70,7 @@ func NewPromptKitClient(cfg PromptKitClientConfig) (*PromptKitClient, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
options, err := cfg.Assets.PromptKitOptions()
|
options, fallbackProfileDigest, err := cfg.Assets.promptKitOptions()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -94,12 +95,13 @@ func NewPromptKitClient(cfg PromptKitClientConfig) (*PromptKitClient, error) {
|
|||||||
reasoningEffort = &value
|
reasoningEffort = &value
|
||||||
}
|
}
|
||||||
return &PromptKitClient{
|
return &PromptKitClient{
|
||||||
engine: engine,
|
engine: engine,
|
||||||
recorder: recorder,
|
recorder: recorder,
|
||||||
profileDir: profileSource.ProfileDir,
|
profileDir: profileSource.ProfileDir,
|
||||||
profileFile: profileSource.ProfileFile,
|
profileFile: profileSource.ProfileFile,
|
||||||
localEndpoint: profileSource.localEndpoint(),
|
localEndpoint: profileSource.localEndpoint(),
|
||||||
reasoningEffort: reasoningEffort,
|
fallbackProfileDigest: fallbackProfileDigest,
|
||||||
|
reasoningEffort: reasoningEffort,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -325,7 +327,7 @@ func (c *PromptKitClient) LLMCheckpointFingerprints() ([]CheckpointFingerprint,
|
|||||||
if c == nil {
|
if c == nil {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
fingerprint, err := promptKitProfileFingerprint(c.profileDir, c.profileFile)
|
fingerprint, err := promptKitProfileFingerprint(c.profileDir, c.profileFile, c.fallbackProfileDigest)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -431,6 +431,84 @@ func TestPromptKitClientCheckpointFingerprintTracksProfileSource(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestPromptKitClientUsesFallbackProfilesForExecutionAndInspection(t *testing.T) {
|
||||||
|
assets := newTestPromptKitAssets(t)
|
||||||
|
const profileID = "fallback-profile"
|
||||||
|
if err := assets.RegisterFallbackProfileFS(fstest.MapFS{
|
||||||
|
"profiles/fallback.yaml": {Data: []byte("id: " + profileID + "\nendpoint: http://promptkit.test/v1\nmodel: fallback-model\n")},
|
||||||
|
}, "profiles"); err != nil {
|
||||||
|
t.Fatalf("RegisterFallbackProfileFS() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
fake := &fakePromptKitLLM{content: `{"ok":true}`}
|
||||||
|
client, err := NewPromptKitClient(PromptKitClientConfig{
|
||||||
|
Assets: assets,
|
||||||
|
EngineOptions: []promptkit.Option{promptkit.WithLLMClient(fake)},
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewPromptKitClient() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
var out map[string]any
|
||||||
|
response, err := client.CompleteStructured(context.Background(), contracts.StructuredCompletionRequest{
|
||||||
|
PromptID: "adapter.test",
|
||||||
|
ProfileID: profileID,
|
||||||
|
SessionID: "fallback-profile-test",
|
||||||
|
Inputs: contracts.LLMInputSet{
|
||||||
|
"transcript": contracts.NewLLMInputMaterial("transcript", "application/json", []byte(`{"source":true}`), "", ""),
|
||||||
|
},
|
||||||
|
}, &out)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("CompleteStructured() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
if response.ProfileID != profileID || response.Model != "fallback-model" {
|
||||||
|
t.Fatalf("completion response = %#v, want fallback profile", response)
|
||||||
|
}
|
||||||
|
|
||||||
|
inspector, err := NewPromptKitProfileInspector(PromptKitProfileInspectorConfig{Assets: assets})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("NewPromptKitProfileInspector() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
inspection, err := inspector.InspectProfile(context.Background(), profileID)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("InspectProfile() error = %v, want nil", err)
|
||||||
|
}
|
||||||
|
if inspection.ProfileID != profileID || inspection.Model != "fallback-model" {
|
||||||
|
t.Fatalf("profile inspection = %#v, want fallback profile", inspection)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPromptKitClientCheckpointFingerprintTracksFallbackProfileAssets(t *testing.T) {
|
||||||
|
fingerprintFor := func(content string) CheckpointFingerprint {
|
||||||
|
t.Helper()
|
||||||
|
assets := newTestPromptKitAssets(t)
|
||||||
|
if err := assets.RegisterFallbackProfileFS(fstest.MapFS{
|
||||||
|
"profiles/fallback.yaml": {Data: []byte(content)},
|
||||||
|
}, "profiles"); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
client, err := NewPromptKitClient(PromptKitClientConfig{Assets: assets})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
fingerprints, err := client.LLMCheckpointFingerprints()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if len(fingerprints) != 1 || fingerprints[0].Name != promptKitProfileFingerprintName {
|
||||||
|
t.Fatalf("checkpoint fingerprints = %#v, want profile source identity", fingerprints)
|
||||||
|
}
|
||||||
|
return fingerprints[0]
|
||||||
|
}
|
||||||
|
|
||||||
|
first := fingerprintFor("id: fallback\nendpoint: http://promptkit.test/v1\nmodel: model-one\n")
|
||||||
|
second := fingerprintFor("id: fallback\nendpoint: http://promptkit.test/v1\nmodel: model-two\n")
|
||||||
|
if first == second {
|
||||||
|
t.Fatalf("checkpoint fingerprints = %#v and %#v, want fallback asset change", first, second)
|
||||||
|
}
|
||||||
|
if strings.Contains(first.Value, "model-one") || strings.Contains(first.Value, "fallback.yaml") {
|
||||||
|
t.Fatalf("checkpoint fingerprint leaked fallback source details: %#v", first)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestPromptKitClientCheckpointFingerprintTracksLocalBackendTarget(t *testing.T) {
|
func TestPromptKitClientCheckpointFingerprintTracksLocalBackendTarget(t *testing.T) {
|
||||||
const (
|
const (
|
||||||
firstEndpoint = "http://localhost:8000/v1"
|
firstEndpoint = "http://localhost:8000/v1"
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ const (
|
|||||||
promptKitBuiltinProfileCatalogID = "promptkit:v0.5.0:builtin-profiles"
|
promptKitBuiltinProfileCatalogID = "promptkit:v0.5.0:builtin-profiles"
|
||||||
)
|
)
|
||||||
|
|
||||||
func promptKitProfileFingerprint(profileDir, profileFile string) (CheckpointFingerprint, error) {
|
func promptKitProfileFingerprint(profileDir, profileFile, fallbackProfileDigest string) (CheckpointFingerprint, error) {
|
||||||
hasher := sha256.New()
|
hasher := sha256.New()
|
||||||
writeFingerprintPart(hasher, []byte(promptKitBuiltinProfileCatalogID))
|
writeFingerprintPart(hasher, []byte(promptKitBuiltinProfileCatalogID))
|
||||||
|
writeFingerprintPart(hasher, []byte(strings.TrimSpace(fallbackProfileDigest)))
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
case strings.TrimSpace(profileFile) != "":
|
case strings.TrimSpace(profileFile) != "":
|
||||||
|
|||||||
@@ -15,6 +15,11 @@ type PromptKitProfileSourceConfig struct {
|
|||||||
LocalBackend *PromptKitLocalBackendConfig
|
LocalBackend *PromptKitLocalBackendConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type PromptKitProfileInspectorConfig struct {
|
||||||
|
Source PromptKitProfileSourceConfig
|
||||||
|
Assets *AssetRegistry
|
||||||
|
}
|
||||||
|
|
||||||
func (c PromptKitProfileSourceConfig) localEndpoint() string {
|
func (c PromptKitProfileSourceConfig) localEndpoint() string {
|
||||||
if c.LocalBackend == nil {
|
if c.LocalBackend == nil {
|
||||||
return ""
|
return ""
|
||||||
@@ -50,11 +55,20 @@ func (e *PromptKitProfileInspectionError) Unwrap() error {
|
|||||||
return e.err
|
return e.err
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPromptKitProfileInspector(cfg PromptKitProfileSourceConfig) (*PromptKitProfileInspector, error) {
|
func NewPromptKitProfileInspector(cfg PromptKitProfileInspectorConfig) (*PromptKitProfileInspector, error) {
|
||||||
source, options, err := promptKitProfileSourceEngineOptions(cfg)
|
source, options, err := promptKitProfileSourceEngineOptions(cfg.Source)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
if cfg.Assets != nil {
|
||||||
|
fallbackOption, hasFallback, err := cfg.Assets.promptKitFallbackProfileOption()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if hasFallback {
|
||||||
|
options = append(options, fallbackOption)
|
||||||
|
}
|
||||||
|
}
|
||||||
engine, err := promptkit.NewEngine(promptkit.Config{
|
engine, err := promptkit.NewEngine(promptkit.Config{
|
||||||
PromptDir: ".",
|
PromptDir: ".",
|
||||||
ProfileDir: source.ProfileDir,
|
ProfileDir: source.ProfileDir,
|
||||||
|
|||||||
Reference in New Issue
Block a user