Add scoped identity listings and backend inventory
This commit is contained in:
@@ -148,9 +148,13 @@ does not watch or reload sources automatically.
|
||||
|
||||
## Discover Available Profiles And Prompts
|
||||
|
||||
Use [`Engine.ListProfiles`](../../listing.go) and
|
||||
[`Engine.ListPrompts`](../../listing.go) to discover exact identities from the
|
||||
engine's final captured configuration, then inspect a selected prompt:
|
||||
Use [`Engine.ListProfiles`](../../listing.go),
|
||||
[`Engine.ListPrompts`](../../listing.go), and
|
||||
[`Engine.ListBackends`](../../listing.go) to discover exact identities from the
|
||||
engine's final captured configuration. A scoped view filters by its respective
|
||||
namespace by default; pass `NamespaceNamed`, `NamespaceUnqualified`, or
|
||||
`NamespaceAll` to override that listing filter without changing execution
|
||||
scope. Returned IDs remain full identities and can be used for exact selection:
|
||||
|
||||
```go
|
||||
profiles, err := engine.ListProfiles(ctx)
|
||||
|
||||
@@ -16,7 +16,7 @@ contributor workflow and validation.
|
||||
| `examples/go-library/run` | Demonstrates an offline downstream consumer using a prompt file, programmatic profile, inline input, an injected deterministic model client, and `Run`. It is not a public library package. | [Example program](../../examples/go-library/run/main.go) |
|
||||
| `examples/go-library/observe` | Demonstrates offline built-in HTTP retry and synchronous metadata observation across structured proposal and assessment runs using embedded sources, two profiles on one finite backend, and an application decision. It is not a public library package. | [Example program](../../examples/go-library/observe/main.go) |
|
||||
| `internal/backend` | Holds a defensive, immutable lookup of final backend definitions, including diagnostic metadata, plus capacity policies validated by the configuration adapter. | [Backend registry](../../internal/backend/registry.go) |
|
||||
| `internal/configuration` | Loads keyed backend, profile, and prompt configuration through ConfigKit; records effective field presence, validates final values including optional diagnostic metadata, captures indexed prompt definitions, referenced message content, compiled templates, input metadata snapshots, sorted identity-only profile and prompt inventories, backend and presence-aware profile snapshots, and an explicit read-only inspection view. | [Typed loader](../../internal/configuration/load.go), [identity listing](../../internal/configuration/listing.go), [internal sources](sources.md#typed-configuration-loading) |
|
||||
| `internal/configuration` | Loads keyed backend, profile, and prompt configuration through ConfigKit; records effective field presence, validates final values including optional diagnostic metadata, captures indexed prompt definitions, referenced message content, compiled templates, input metadata snapshots, sorted identity-only backend, profile, and prompt inventories, backend and presence-aware profile snapshots, and an explicit read-only inspection view. | [Typed loader](../../internal/configuration/load.go), [identity listing](../../internal/configuration/listing.go), [internal sources](sources.md#typed-configuration-loading) |
|
||||
| `internal/capacity` | Owns engine-local bounded execution admission and FIFO model-generation permits for limited backend IDs, including cancellation-safe waiter removal and client wrapping. | [Internal capacity management](capacity.md) |
|
||||
| `internal/domain` | Defines internal framework values for requests, artifacts, prompt definitions, requirement inspection, profiles, execution targets, rendering, generation, validation, and source-neutral smoke diagnostics, and owns source-neutral invariants for shared execution settings, OpenAI-compatible base endpoints, session identifiers, output contracts, and resource-identity syntax and qualification. Source parsing, required fields, other source-specific normalization, defaulting, and boundary-specific error classification remain with their callers. | [Domain declarations](../../internal/domain/domain.go), [requirement values](../../internal/domain/requirements.go), [endpoint invariant](../../internal/domain/endpoint.go) |
|
||||
| `internal/engine` | Assembles caller-selected configuration into one snapshot, then constructs shared capacity, rendering, validation, generation, repair, the ordinary runner, and the smoke tester for each engine. Immutable namespace views share those collaborators and qualify caller-supplied execution, inspection, and diagnostic selections without rewriting captured references. Built-in engines provide the diagnostic HTTP collaborator; injected-client engines do not. It delegates identity listing directly to the captured configuration inventory rather than the runner. | [Engine assembly](../../internal/engine/engine.go), [internal sources](sources.md#typed-configuration-loading), [internal runner](runner.md) |
|
||||
|
||||
@@ -681,6 +681,18 @@ names, full-ID round trips (and unqualified selection through the original
|
||||
engine), invalid filters, ownership, and canceled contexts. Use a compact matrix
|
||||
across resource types, not three copies of all tests. Run configuration/root tests.
|
||||
|
||||
Completion: prompt and profile listings now accept the documented optional
|
||||
namespace filters, and backend identities are captured and listed through the
|
||||
same path. Default filters use the view's per-resource scope, explicit filters
|
||||
replace it, results retain full IDs and detached prompt labels, and ordinary
|
||||
no-filter calls still return the full inventory. Contract tests cover three
|
||||
resource types, exact namespace boundaries, round trips, invalid filters,
|
||||
ownership, and cancellation. Validation passed: `GOWORK=off go test ./...`,
|
||||
focused `GOWORK=off go test -race . -run
|
||||
'TestScopedIdentityListingsApplyNamespaceFilters|TestListIdentitiesAreOwnedAndSafeForConcurrentUse|TestListIdentitiesPreserveContextErrorsWithoutPartialResults' -count=1`,
|
||||
`GOWORK=off go vet ./...`, `GOWORK=off python3 scripts/check-examples.py`,
|
||||
and `git diff --check`.
|
||||
|
||||
## Stage 15: Expose Namespace And Effective Collection Inventories
|
||||
|
||||
Scope: the two discovery APIs and construction-time inventory capture.
|
||||
|
||||
@@ -88,7 +88,8 @@ var (
|
||||
// [Engine.InspectConfiguration],
|
||||
// [Engine.InspectPrompt],
|
||||
// [Engine.InspectRequirements], [Engine.InspectProfile], [Engine.Prepare], [Engine.PrepareExecution],
|
||||
// [Engine.Run], [Engine.RunPrepared], and [Engine.SmokeTest]. Each Engine owns
|
||||
// [Engine.Run], [Engine.RunPrepared], [Engine.SmokeTest], [Engine.ListProfiles],
|
||||
// [Engine.ListPrompts], and [Engine.ListBackends]. Each Engine owns
|
||||
// independent backend-capacity pools that coordinate Run, RunPrepared, and
|
||||
// inference SmokeTest admission and model generation. Injected collaborators
|
||||
// may still be invoked concurrently across different backend pools or for
|
||||
|
||||
@@ -17,6 +17,17 @@ func (state *loaded) ListProfiles(ctx context.Context) ([]string, error) {
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
func (state *loaded) ListBackends(ctx context.Context) ([]string, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
backends := append([]string(nil), state.backendIDs...)
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return backends, nil
|
||||
}
|
||||
|
||||
func (state *loaded) ListPrompts(ctx context.Context) ([]domain.PromptIdentity, error) {
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
|
||||
@@ -25,6 +25,14 @@ func TestLoadedListsSortedEffectiveIdentities(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backends, err := state.ListBackends(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wantBackends := []string{"local"}
|
||||
if !reflect.DeepEqual(backends, wantBackends) {
|
||||
t.Fatalf("backend IDs = %#v, want %#v", backends, wantBackends)
|
||||
}
|
||||
|
||||
profiles, err := state.ListProfiles(context.Background())
|
||||
if err != nil {
|
||||
@@ -51,6 +59,7 @@ func TestLoadedListsSortedEffectiveIdentities(t *testing.T) {
|
||||
|
||||
func TestLoadedListsAreIndependent(t *testing.T) {
|
||||
state, err := Load(mapping("identities", map[string]any{
|
||||
"backends": map[string]any{"backend": map[string]any{"endpoint": "https://example.test/v1"}},
|
||||
"profiles": map[string]any{"profile": map[string]any{}},
|
||||
"prompts": map[string]any{"example": listingPromptDefinition()},
|
||||
}), nil)
|
||||
@@ -64,6 +73,11 @@ func TestLoadedListsAreIndependent(t *testing.T) {
|
||||
}
|
||||
profiles = append(profiles, "caller profile")
|
||||
profiles[0] = "changed profile"
|
||||
backends, err := state.ListBackends(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
backends[0] = "changed backend"
|
||||
|
||||
prompts, err := state.ListPrompts(context.Background())
|
||||
if err != nil {
|
||||
@@ -79,6 +93,10 @@ func TestLoadedListsAreIndependent(t *testing.T) {
|
||||
if !reflect.DeepEqual(profiles, []string{"profile"}) {
|
||||
t.Fatalf("caller mutation changed profile inventory: %#v", profiles)
|
||||
}
|
||||
backends, err = state.ListBackends(context.Background())
|
||||
if err != nil || !reflect.DeepEqual(backends, []string{"backend"}) {
|
||||
t.Fatalf("caller mutation changed backend inventory: %#v, %v", backends, err)
|
||||
}
|
||||
prompts, err = state.ListPrompts(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -98,6 +116,10 @@ func TestLoadedListsEmptyInventory(t *testing.T) {
|
||||
if err != nil || len(profiles) != 0 {
|
||||
t.Fatalf("empty profile listing = %#v, %v", profiles, err)
|
||||
}
|
||||
backends, err := state.ListBackends(context.Background())
|
||||
if err != nil || len(backends) != 0 {
|
||||
t.Fatalf("empty backend listing = %#v, %v", backends, err)
|
||||
}
|
||||
prompts, err := state.ListPrompts(context.Background())
|
||||
if err != nil || len(prompts) != 0 {
|
||||
t.Fatalf("empty prompt listing = %#v, %v", prompts, err)
|
||||
@@ -119,6 +141,10 @@ func TestLoadedListsHonorCanceledContext(t *testing.T) {
|
||||
if !errors.Is(err, context.Canceled) || len(profiles) != 0 {
|
||||
t.Fatalf("canceled profile listing = %#v, %v", profiles, err)
|
||||
}
|
||||
backends, err := state.ListBackends(ctx)
|
||||
if !errors.Is(err, context.Canceled) || len(backends) != 0 {
|
||||
t.Fatalf("canceled backend listing = %#v, %v", backends, err)
|
||||
}
|
||||
prompts, err := state.ListPrompts(ctx)
|
||||
if !errors.Is(err, context.Canceled) || len(prompts) != 0 {
|
||||
t.Fatalf("canceled prompt listing = %#v, %v", prompts, err)
|
||||
|
||||
@@ -124,6 +124,7 @@ type loaded struct {
|
||||
prompts map[string]*promptEntry
|
||||
registry *backend.Registry
|
||||
indexedProfiles map[string]domain.ExecutionProfile
|
||||
backendIDs []string
|
||||
profileIDs []string
|
||||
promptIDs []domain.PromptIdentity
|
||||
}
|
||||
@@ -260,6 +261,7 @@ func load(main configkit.Input, underlays []configkit.Underlay) (*loaded, error)
|
||||
if err := state.buildExecutionProfiles(); err != nil {
|
||||
return nil, fmt.Errorf("load configuration: %w", err)
|
||||
}
|
||||
state.backendIDs = sortedKeys(indexedBackends(&result.Config))
|
||||
state.profileIDs = sortedKeys(state.indexedProfiles)
|
||||
for _, id := range sortedKeys(state.prompts) {
|
||||
state.promptIDs = append(state.promptIDs, domain.PromptIdentity{ID: id, Version: stringPointerValue(state.prompts[id].definition.Version)})
|
||||
|
||||
@@ -94,10 +94,27 @@ type resourceNamespaces struct {
|
||||
}
|
||||
|
||||
type identityInventory interface {
|
||||
ListBackends(context.Context) ([]string, error)
|
||||
ListProfiles(context.Context) ([]string, error)
|
||||
ListPrompts(context.Context) ([]domain.PromptIdentity, error)
|
||||
}
|
||||
|
||||
// ListingFilterMode selects namespace membership for one identity listing.
|
||||
type ListingFilterMode uint8
|
||||
|
||||
const (
|
||||
ListingDefault ListingFilterMode = iota
|
||||
ListingNamed
|
||||
ListingUnqualified
|
||||
ListingAll
|
||||
)
|
||||
|
||||
// ListingFilter optionally overrides the namespace selected by an engine view.
|
||||
type ListingFilter struct {
|
||||
Mode ListingFilterMode
|
||||
Name string
|
||||
}
|
||||
|
||||
// New assembles an engine from facade-selected configuration and collaborators.
|
||||
func New(cfg Config, options Options) (*Engine, error) {
|
||||
defaultRetries, err := domain.ResolveTransientRetries(2, cfg.TransientRetries)
|
||||
@@ -231,12 +248,99 @@ func (e *Engine) InspectProfile(ctx context.Context, id string) (*domain.Profile
|
||||
return e.runner.InspectProfile(ctx, id)
|
||||
}
|
||||
|
||||
func (e *Engine) ListProfiles(ctx context.Context) ([]string, error) {
|
||||
return e.listing.ListProfiles(ctx)
|
||||
func (e *Engine) ListBackends(ctx context.Context, filters ...ListingFilter) ([]string, error) {
|
||||
return filterIdentityList(ctx, filters, e.namespaces.backend, e.listing.ListBackends)
|
||||
}
|
||||
|
||||
func (e *Engine) ListPrompts(ctx context.Context) ([]domain.PromptIdentity, error) {
|
||||
return e.listing.ListPrompts(ctx)
|
||||
func (e *Engine) ListProfiles(ctx context.Context, filters ...ListingFilter) ([]string, error) {
|
||||
return filterIdentityList(ctx, filters, e.namespaces.profile, e.listing.ListProfiles)
|
||||
}
|
||||
|
||||
func (e *Engine) ListPrompts(ctx context.Context, filters ...ListingFilter) ([]domain.PromptIdentity, error) {
|
||||
mode, namespace, err := resolveListingFilter(filters, e.namespaces.prompt)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
prompts, err := e.listing.ListPrompts(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var filtered []domain.PromptIdentity
|
||||
for _, prompt := range prompts {
|
||||
if matchesListingNamespace(prompt.ID, mode, namespace) {
|
||||
filtered = append(filtered, prompt)
|
||||
}
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func filterIdentityList(
|
||||
ctx context.Context,
|
||||
filters []ListingFilter,
|
||||
scope string,
|
||||
list func(context.Context) ([]string, error),
|
||||
) ([]string, error) {
|
||||
mode, namespace, err := resolveListingFilter(filters, scope)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
ids, err := list(ctx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var filtered []string
|
||||
for _, id := range ids {
|
||||
if matchesListingNamespace(id, mode, namespace) {
|
||||
filtered = append(filtered, id)
|
||||
}
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return filtered, nil
|
||||
}
|
||||
|
||||
func resolveListingFilter(filters []ListingFilter, scope string) (ListingFilterMode, string, error) {
|
||||
if len(filters) > 1 {
|
||||
return 0, "", fmt.Errorf("%w: at most one listing filter is allowed", usecase.ErrInvalidRequest)
|
||||
}
|
||||
filter := ListingFilter{}
|
||||
if len(filters) == 1 {
|
||||
filter = filters[0]
|
||||
}
|
||||
switch filter.Mode {
|
||||
case ListingDefault:
|
||||
if scope == "" {
|
||||
return ListingAll, "", nil
|
||||
}
|
||||
return ListingNamed, scope, nil
|
||||
case ListingNamed:
|
||||
if !domain.IsValidResourceNamespace(filter.Name) {
|
||||
return 0, "", fmt.Errorf("%w: invalid listing namespace", usecase.ErrInvalidRequest)
|
||||
}
|
||||
return ListingNamed, filter.Name, nil
|
||||
case ListingUnqualified, ListingAll:
|
||||
return filter.Mode, "", nil
|
||||
default:
|
||||
return 0, "", fmt.Errorf("%w: unknown listing filter mode", usecase.ErrInvalidRequest)
|
||||
}
|
||||
}
|
||||
|
||||
func matchesListingNamespace(id string, mode ListingFilterMode, namespace string) bool {
|
||||
switch mode {
|
||||
case ListingNamed:
|
||||
idNamespace, _, qualified := strings.Cut(id, "/")
|
||||
return qualified && idNamespace == namespace
|
||||
case ListingUnqualified:
|
||||
return !strings.Contains(id, "/")
|
||||
case ListingAll:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
func (e *Engine) Prepare(ctx context.Context, request domain.RunRequest) (*domain.PreparedRun, error) {
|
||||
request, err := e.qualifyRunRequest(request)
|
||||
|
||||
99
listing.go
99
listing.go
@@ -3,8 +3,32 @@ package promptkit
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
internalengine "gitea.maximumdirect.net/eric/promptkit/internal/engine"
|
||||
)
|
||||
|
||||
// NamespaceFilterMode selects namespace membership for an identity listing.
|
||||
type NamespaceFilterMode uint8
|
||||
|
||||
const (
|
||||
// NamespaceDefault uses the engine view's scope, or all IDs if it is unset.
|
||||
NamespaceDefault NamespaceFilterMode = iota
|
||||
// NamespaceNamed selects IDs whose exact namespace matches Name.
|
||||
NamespaceNamed
|
||||
// NamespaceUnqualified selects IDs without a namespace.
|
||||
NamespaceUnqualified
|
||||
// NamespaceAll selects IDs from every namespace, overriding the view scope.
|
||||
NamespaceAll
|
||||
)
|
||||
|
||||
// NamespaceFilter optionally overrides the namespace used for one listing.
|
||||
// NamespaceNamed requires a nonempty valid namespace in Name; other modes
|
||||
// ignore Name.
|
||||
type NamespaceFilter struct {
|
||||
Mode NamespaceFilterMode
|
||||
Name string
|
||||
}
|
||||
|
||||
// PromptIdentity identifies one prompt ID and its optional descriptive version
|
||||
// label in the effective configuration captured when an Engine is constructed.
|
||||
// A nil Version means no label was declared; a non-nil empty string is an
|
||||
@@ -17,43 +41,57 @@ type PromptIdentity struct {
|
||||
// ListProfiles returns profile IDs in the effective configuration captured
|
||||
// when the engine was constructed. Maintained and consumer profiles are
|
||||
// included; an inherited profile retains its own ID. IDs are preserved exactly
|
||||
// and sorted lexically. The returned slice is caller-owned and safe to mutate
|
||||
// while the engine is used concurrently. Empty inventories succeed.
|
||||
// and sorted lexically. An omitted filter or NamespaceDefault uses the view's
|
||||
// profile namespace, or all profiles when unset. NamespaceNamed selects the
|
||||
// exact named namespace, NamespaceUnqualified selects IDs without a slash, and
|
||||
// NamespaceAll selects every profile. An explicit filter replaces the view's
|
||||
// default for this call. At most one filter is accepted.
|
||||
//
|
||||
// The returned slice is caller-owned and safe to mutate while the engine is
|
||||
// used concurrently. Empty inventories succeed.
|
||||
//
|
||||
// Listing reports membership, not execution readiness. It does not reload
|
||||
// configuration or perform resource, credential, provider, or execution work.
|
||||
// Profile IDs are application data and may be sensitive. A nil or zero-value
|
||||
// engine returns an error matching ErrInvalidConfig. Cancellation or deadline
|
||||
// expiration returns the context error and no partial result.
|
||||
func (e *Engine) ListProfiles(ctx context.Context) ([]string, error) {
|
||||
// expiration returns the context error and no partial result. Multiple
|
||||
// filters, an unknown mode, or an invalid named namespace returns an error
|
||||
// matching ErrInvalidRequest.
|
||||
func (e *Engine) ListProfiles(ctx context.Context, filters ...NamespaceFilter) ([]string, error) {
|
||||
if e == nil || e.internal == nil {
|
||||
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
||||
}
|
||||
|
||||
profiles, err := e.internal.ListProfiles(ctx)
|
||||
profiles, err := e.internal.ListProfiles(ctx, internalListingFilters(filters)...)
|
||||
if err != nil {
|
||||
return nil, mapPublicError(err)
|
||||
}
|
||||
return profiles, nil
|
||||
}
|
||||
|
||||
// ListPrompts returns every prompt ID in the effective configuration captured
|
||||
// when the engine was constructed, sorted lexically by ID. Each identity may
|
||||
// include its descriptive version label. The returned slice and each Version
|
||||
// pointer are caller-owned and safe to mutate while the engine is used
|
||||
// concurrently. Empty inventories succeed.
|
||||
// ListPrompts returns prompt IDs in the effective configuration captured when
|
||||
// the engine was constructed, sorted lexically by ID. Each identity may include
|
||||
// its descriptive version label. Its optional filter follows the same
|
||||
// NamespaceDefault, NamespaceNamed, NamespaceUnqualified, and NamespaceAll
|
||||
// behavior as [Engine.ListProfiles], using the prompt namespace by default.
|
||||
// At most one filter is accepted.
|
||||
//
|
||||
// The returned slice and each Version pointer are caller-owned and safe to
|
||||
// mutate while the engine is used concurrently. Empty inventories succeed.
|
||||
//
|
||||
// Listing reports membership, not execution readiness. It does not reload
|
||||
// configuration or perform resource, credential, provider, or execution work.
|
||||
// Prompt identities are application data and may be sensitive. A nil or
|
||||
// zero-value engine returns an error matching ErrInvalidConfig. Cancellation
|
||||
// or deadline expiration returns the context error and no partial result.
|
||||
func (e *Engine) ListPrompts(ctx context.Context) ([]PromptIdentity, error) {
|
||||
// Multiple filters, an unknown mode, or an invalid named namespace returns an
|
||||
// error matching ErrInvalidRequest.
|
||||
func (e *Engine) ListPrompts(ctx context.Context, filters ...NamespaceFilter) ([]PromptIdentity, error) {
|
||||
if e == nil || e.internal == nil {
|
||||
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
||||
}
|
||||
|
||||
identities, err := e.internal.ListPrompts(ctx)
|
||||
identities, err := e.internal.ListPrompts(ctx, internalListingFilters(filters)...)
|
||||
if err != nil {
|
||||
return nil, mapPublicError(err)
|
||||
}
|
||||
@@ -66,3 +104,40 @@ func (e *Engine) ListPrompts(ctx context.Context) ([]PromptIdentity, error) {
|
||||
}
|
||||
return public, nil
|
||||
}
|
||||
|
||||
// ListBackends returns backend IDs in the effective configuration captured
|
||||
// when the engine was constructed, sorted lexically by full ID. Its optional
|
||||
// filter follows the same namespace behavior as [Engine.ListProfiles], using
|
||||
// the backend namespace by default. At most one filter is accepted. The
|
||||
// returned slice is caller-owned and safe to mutate while the engine is used
|
||||
// concurrently. Empty inventories succeed.
|
||||
//
|
||||
// Listing reports membership, not execution readiness, and performs no
|
||||
// provider or execution work. A nil or zero-value engine returns an error
|
||||
// matching ErrInvalidConfig. Cancellation or deadline expiration returns the
|
||||
// context error and no partial result. Multiple filters, an unknown mode, or
|
||||
// an invalid named namespace returns an error matching ErrInvalidRequest.
|
||||
func (e *Engine) ListBackends(ctx context.Context, filters ...NamespaceFilter) ([]string, error) {
|
||||
if e == nil || e.internal == nil {
|
||||
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
||||
}
|
||||
backends, err := e.internal.ListBackends(ctx, internalListingFilters(filters)...)
|
||||
if err != nil {
|
||||
return nil, mapPublicError(err)
|
||||
}
|
||||
return backends, nil
|
||||
}
|
||||
|
||||
func internalListingFilters(filters []NamespaceFilter) []internalengine.ListingFilter {
|
||||
if filters == nil {
|
||||
return nil
|
||||
}
|
||||
internal := make([]internalengine.ListingFilter, len(filters))
|
||||
for index, filter := range filters {
|
||||
internal[index] = internalengine.ListingFilter{
|
||||
Mode: internalengine.ListingFilterMode(filter.Mode),
|
||||
Name: filter.Name,
|
||||
}
|
||||
}
|
||||
return internal
|
||||
}
|
||||
|
||||
@@ -60,6 +60,185 @@ func TestListIdentitiesBasicContract(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestScopedIdentityListingsApplyNamespaceFilters(t *testing.T) {
|
||||
engine := newNamespaceListingEngine(t)
|
||||
scoped, err := engine.WithNamespaces(promptkit.Namespaces{
|
||||
Prompt: "team", Profile: "team", Backend: "team",
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
all := promptkit.NamespaceFilter{Mode: promptkit.NamespaceAll}
|
||||
named := promptkit.NamespaceFilter{Mode: promptkit.NamespaceNamed, Name: "team-blue"}
|
||||
unqualified := promptkit.NamespaceFilter{Mode: promptkit.NamespaceUnqualified}
|
||||
defaultOverride := promptkit.NamespaceFilter{Mode: promptkit.NamespaceDefault, Name: "ignored"}
|
||||
absent := promptkit.NamespaceFilter{Mode: promptkit.NamespaceNamed, Name: "missing"}
|
||||
|
||||
type listing struct {
|
||||
name string
|
||||
list func(*promptkit.Engine, ...promptkit.NamespaceFilter) ([]string, error)
|
||||
all []string
|
||||
team []string
|
||||
blue []string
|
||||
root []string
|
||||
}
|
||||
listings := []listing{
|
||||
{name: "profiles", list: func(e *promptkit.Engine, f ...promptkit.NamespaceFilter) ([]string, error) {
|
||||
return e.ListProfiles(context.Background(), f...)
|
||||
}, all: []string{"root-profile", "team-blue/profile", "team/profile"}, team: []string{"team/profile"}, blue: []string{"team-blue/profile"}, root: []string{"root-profile"}},
|
||||
{name: "prompts", list: func(e *promptkit.Engine, f ...promptkit.NamespaceFilter) ([]string, error) {
|
||||
prompts, err := e.ListPrompts(context.Background(), f...)
|
||||
ids := make([]string, len(prompts))
|
||||
for index := range prompts {
|
||||
ids[index] = prompts[index].ID
|
||||
}
|
||||
return ids, err
|
||||
}, all: []string{"root-prompt", "team-blue/prompt", "team/prompt"}, team: []string{"team/prompt"}, blue: []string{"team-blue/prompt"}, root: []string{"root-prompt"}},
|
||||
{name: "backends", list: func(e *promptkit.Engine, f ...promptkit.NamespaceFilter) ([]string, error) {
|
||||
return e.ListBackends(context.Background(), f...)
|
||||
}, all: []string{"root-backend", "team-blue/backend", "team/backend"}, team: []string{"team/backend"}, blue: []string{"team-blue/backend"}, root: []string{"root-backend"}},
|
||||
}
|
||||
for _, listing := range listings {
|
||||
t.Run(listing.name, func(t *testing.T) {
|
||||
for _, check := range []struct {
|
||||
name string
|
||||
engine *promptkit.Engine
|
||||
filter []promptkit.NamespaceFilter
|
||||
want []string
|
||||
}{
|
||||
{name: "scoped default", engine: scoped, want: listing.team},
|
||||
{name: "explicit default override", engine: scoped, filter: []promptkit.NamespaceFilter{defaultOverride}, want: listing.team},
|
||||
{name: "named override", engine: scoped, filter: []promptkit.NamespaceFilter{named}, want: listing.blue},
|
||||
{name: "unqualified override", engine: scoped, filter: []promptkit.NamespaceFilter{unqualified}, want: listing.root},
|
||||
{name: "all override", engine: scoped, filter: []promptkit.NamespaceFilter{all}, want: listing.all},
|
||||
{name: "absent namespace", engine: scoped, filter: []promptkit.NamespaceFilter{absent}, want: []string{}},
|
||||
{name: "unscoped default", engine: engine, want: listing.all},
|
||||
} {
|
||||
got, err := listing.list(check.engine, check.filter...)
|
||||
if err != nil || (check.name == "absent namespace" && len(got) != 0) ||
|
||||
(check.name != "absent namespace" && !reflect.DeepEqual(got, check.want)) {
|
||||
t.Errorf("%s = %#v, %v; want %#v", check.name, got, err, check.want)
|
||||
}
|
||||
}
|
||||
for _, invalid := range [][]promptkit.NamespaceFilter{
|
||||
{{Mode: promptkit.NamespaceNamed}},
|
||||
{{Mode: promptkit.NamespaceNamed, Name: "team/child"}},
|
||||
{{Mode: promptkit.NamespaceFilterMode(99)}},
|
||||
{{Mode: promptkit.NamespaceAll}, {Mode: promptkit.NamespaceUnqualified}},
|
||||
} {
|
||||
got, err := listing.list(scoped, invalid...)
|
||||
if !errors.Is(err, promptkit.ErrInvalidRequest) || len(got) != 0 {
|
||||
t.Errorf("invalid filter %#v = %#v, %v; want ErrInvalidRequest without results", invalid, got, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
prompts, err := scoped.ListPrompts(context.Background(), promptkit.NamespaceFilter{Mode: promptkit.NamespaceNamed, Name: "team"})
|
||||
if err != nil || len(prompts) != 1 || prompts[0].ID != "team/prompt" || prompts[0].Version == nil || *prompts[0].Version != "team-v1" {
|
||||
t.Fatalf("named prompt identity = %#v, %v", prompts, err)
|
||||
}
|
||||
if _, err := scoped.InspectPrompt(context.Background(), prompts[0].ID); err != nil {
|
||||
t.Fatalf("listed full prompt ID did not round trip: %v", err)
|
||||
}
|
||||
*prompts[0].Version = "caller change"
|
||||
prompts[0].ID = "caller change"
|
||||
prompts, err = scoped.ListPrompts(context.Background(), promptkit.NamespaceFilter{Mode: promptkit.NamespaceNamed, Name: "team"})
|
||||
if err != nil || len(prompts) != 1 || prompts[0].ID != "team/prompt" || prompts[0].Version == nil || *prompts[0].Version != "team-v1" {
|
||||
t.Fatalf("mutating filtered prompt inventory escaped: %#v, %v", prompts, err)
|
||||
}
|
||||
profiles, err := scoped.ListProfiles(context.Background(), promptkit.NamespaceFilter{Mode: promptkit.NamespaceNamed, Name: "team"})
|
||||
if err != nil || len(profiles) != 1 || profiles[0] != "team/profile" {
|
||||
t.Fatalf("named profile inventory = %#v, %v", profiles, err)
|
||||
}
|
||||
if _, err := scoped.InspectProfile(context.Background(), profiles[0]); err != nil {
|
||||
t.Fatalf("listed full profile ID did not round trip: %v", err)
|
||||
}
|
||||
profiles[0] = "caller change"
|
||||
profiles, err = scoped.ListProfiles(context.Background(), promptkit.NamespaceFilter{Mode: promptkit.NamespaceNamed, Name: "team"})
|
||||
if err != nil || len(profiles) != 1 || profiles[0] != "team/profile" {
|
||||
t.Fatalf("mutating filtered profile inventory escaped: %#v, %v", profiles, err)
|
||||
}
|
||||
backends, err := scoped.ListBackends(context.Background(), promptkit.NamespaceFilter{Mode: promptkit.NamespaceNamed, Name: "team"})
|
||||
if err != nil || len(backends) != 1 || backends[0] != "team/backend" {
|
||||
t.Fatalf("named backend inventory = %#v, %v", backends, err)
|
||||
}
|
||||
if report, err := scoped.SmokeTest(context.Background(), promptkit.SmokeTestRequest{
|
||||
Kind: promptkit.SmokeTestReachability, BackendID: backends[0],
|
||||
}); err != nil || report.Target.BackendID != backends[0] {
|
||||
t.Fatalf("listed full backend ID did not round trip: %+v, %v", report, err)
|
||||
}
|
||||
backends[0] = "caller change"
|
||||
backends, err = scoped.ListBackends(context.Background(), promptkit.NamespaceFilter{Mode: promptkit.NamespaceNamed, Name: "team"})
|
||||
if err != nil || len(backends) != 1 || backends[0] != "team/backend" {
|
||||
t.Fatalf("mutating filtered backend inventory escaped: %#v, %v", backends, err)
|
||||
}
|
||||
rootPrompts, err := engine.ListPrompts(context.Background(), unqualified)
|
||||
if err != nil || len(rootPrompts) != 1 || rootPrompts[0].ID != "root-prompt" {
|
||||
t.Fatalf("unqualified prompt listing = %#v, %v", rootPrompts, err)
|
||||
}
|
||||
if _, err := engine.InspectPrompt(context.Background(), rootPrompts[0].ID); err != nil {
|
||||
t.Fatalf("unqualified prompt did not select through original engine: %v", err)
|
||||
}
|
||||
rootProfiles, err := engine.ListProfiles(context.Background(), unqualified)
|
||||
if err != nil || len(rootProfiles) != 1 || rootProfiles[0] != "root-profile" {
|
||||
t.Fatalf("unqualified profile listing = %#v, %v", rootProfiles, err)
|
||||
}
|
||||
if _, err := engine.InspectProfile(context.Background(), rootProfiles[0]); err != nil {
|
||||
t.Fatalf("unqualified profile did not select through original engine: %v", err)
|
||||
}
|
||||
rootBackends, err := engine.ListBackends(context.Background(), unqualified)
|
||||
if err != nil || len(rootBackends) != 1 || rootBackends[0] != "root-backend" {
|
||||
t.Fatalf("unqualified backend listing = %#v, %v", rootBackends, err)
|
||||
}
|
||||
if report, err := engine.SmokeTest(context.Background(), promptkit.SmokeTestRequest{
|
||||
Kind: promptkit.SmokeTestReachability, BackendID: rootBackends[0],
|
||||
}); err != nil || report.Target.BackendID != rootBackends[0] {
|
||||
t.Fatalf("unqualified backend did not select through original engine: %+v, %v", report, err)
|
||||
}
|
||||
|
||||
canceled, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
if got, err := scoped.ListProfiles(canceled); !errors.Is(err, context.Canceled) || len(got) != 0 {
|
||||
t.Errorf("canceled profile list = %#v, %v", got, err)
|
||||
}
|
||||
if got, err := scoped.ListPrompts(canceled); !errors.Is(err, context.Canceled) || len(got) != 0 {
|
||||
t.Errorf("canceled prompt list = %#v, %v", got, err)
|
||||
}
|
||||
if got, err := scoped.ListBackends(canceled); !errors.Is(err, context.Canceled) || len(got) != 0 {
|
||||
t.Errorf("canceled backend list = %#v, %v", got, err)
|
||||
}
|
||||
}
|
||||
|
||||
func newNamespaceListingEngine(t *testing.T) *promptkit.Engine {
|
||||
t.Helper()
|
||||
collection := func(namespace string) map[string]any {
|
||||
return map[string]any{
|
||||
"backends": map[string]any{"backend": map[string]any{"endpoint": "https://example.test/v1"}},
|
||||
"profiles": map[string]any{"profile": map[string]any{"backend": namespace + "/backend", "model": "example-model"}},
|
||||
"prompts": map[string]any{"prompt": promptDefinitionWithVersion("team-v1", listingContractPrompt())},
|
||||
}
|
||||
}
|
||||
input := configkit.Input{Source: configkit.Mapping("namespaced listing", map[string]any{
|
||||
"collections": map[string]any{
|
||||
"team": collection("team"),
|
||||
"team-blue": collection("team-blue"),
|
||||
},
|
||||
"backends": map[string]any{"root-backend": map[string]any{"endpoint": "https://root.example.test/v1"}},
|
||||
"profiles": map[string]any{"root-profile": map[string]any{"backend": "root-backend", "model": "root-model"}},
|
||||
"prompts": map[string]any{"root-prompt": promptDefinitionWithVersion("root-v1", listingContractPrompt())},
|
||||
})}
|
||||
engine, err := promptkit.NewEngine(promptkit.Config{Sources: promptkit.ConfigurationSources{Main: &input}})
|
||||
if err != nil {
|
||||
var loadErr *configkit.LoadError
|
||||
if errors.As(err, &loadErr) {
|
||||
t.Fatalf("construct namespaced listing engine: %v: %+v", err, loadErr.Diagnostics())
|
||||
}
|
||||
t.Fatalf("construct namespaced listing engine: %v", err)
|
||||
}
|
||||
return engine
|
||||
}
|
||||
|
||||
func TestListIdentitiesInvalidEngine(t *testing.T) {
|
||||
var nilEngine *promptkit.Engine
|
||||
var zeroEngine promptkit.Engine
|
||||
@@ -70,6 +249,9 @@ func TestListIdentitiesInvalidEngine(t *testing.T) {
|
||||
if _, err := engine.ListPrompts(context.Background()); !errors.Is(err, promptkit.ErrInvalidConfig) {
|
||||
t.Errorf("ListPrompts on invalid engine: %v", err)
|
||||
}
|
||||
if _, err := engine.ListBackends(context.Background()); !errors.Is(err, promptkit.ErrInvalidConfig) {
|
||||
t.Errorf("ListBackends on invalid engine: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user