Expose captured resource inspection through the engine

This commit is contained in:
2026-11-06 15:21:00 +00:00
parent bf3d2e19bd
commit dfe9d6566f
5 changed files with 161 additions and 36 deletions

View File

@@ -4,6 +4,7 @@ import (
"fmt" "fmt"
"unicode/utf8" "unicode/utf8"
"go.maximumdirect.net/promptkit/internal/configuration"
"go.maximumdirect.net/promptkit/internal/domain" "go.maximumdirect.net/promptkit/internal/domain"
"go.maximumdirect.net/promptkit/internal/jsonvalue" "go.maximumdirect.net/promptkit/internal/jsonvalue"
) )
@@ -228,37 +229,58 @@ func fromDomainExecutionTargetWithOwnedParams(target domain.ExecutionTarget, ext
} }
} }
func fromDomainProfileInspection(inspection *domain.ProfileInspection) *ProfileInspection { func fromProfileResource(v *configuration.ProfileResource) *ProfileInspection {
if inspection == nil { out := &ProfileInspection{ProfileID: v.ID, Backend: copyStringPtr(v.Backend), Endpoint: copyStringPtr(v.Endpoint), Model: copyStringPtr(v.Model), Temperature: copyFloat64Ptr(v.Temperature), TopP: copyFloat64Ptr(v.TopP), MaxTokens: copyIntPtr(v.MaxTokens), TimeoutSeconds: copyIntPtr(v.TimeoutSeconds), ServiceTier: copyStringPtr(v.ServiceTier), ReasoningEffort: copyStringPtr(v.ReasoningEffort), APIKeyEnv: copyStringPtr(v.APIKeyEnv), APIKeyRequired: copyBoolPtr(v.APIKeyRequired)}
return nil if v.ExtraParams != nil {
} data := copyAnyMap(v.ExtraParams)
return &ProfileInspection{ out.ExtraParams = &data
ProfileID: inspection.ProfileID,
EffectiveModelParams: fromDomainExecutionTarget(inspection.EffectiveModelParams),
APIKeyRequired: inspection.APIKeyRequired,
} }
return out
} }
func fromDomainPromptInspection(inspection *domain.PromptInspection) *PromptInspection { func fromBackendResource(v *configuration.BackendResource) *BackendInspection {
if inspection == nil { out := &BackendInspection{BackendID: v.ID, Endpoint: v.Endpoint, APIKeyEnv: copyStringPtr(v.APIKeyEnv), ConcurrencyLimit: copyIntPtr(v.ConcurrencyLimit), QueueCapacity: copyIntPtr(v.QueueCapacity)}
return nil if v.ExtraParams != nil {
data := copyAnyMap(v.ExtraParams)
out.ExtraParams = &data
} }
inputs := make([]PromptInputDefinition, len(inspection.Inputs)) if v.SmokeTests != nil {
for i, input := range inspection.Inputs { out.SmokeTests = &BackendSmokeTestConfig{DefaultProfile: copyStringPtr(v.SmokeTests.DefaultProfile)}
inputs[i] = PromptInputDefinition{ if v.SmokeTests.Reachability != nil {
Name: input.Name, out.SmokeTests.Reachability = &ReachabilityProbeConfig{Endpoint: copyStringPtr(v.SmokeTests.Reachability.Endpoint)}
Required: input.Required, }
ContentType: input.ContentType, if v.SmokeTests.Authentication != nil {
Description: input.Description, out.SmokeTests.Authentication = &AuthenticationProbeConfig{Kind: copyStringPtr(v.SmokeTests.Authentication.Kind), Endpoint: copyStringPtr(v.SmokeTests.Authentication.Endpoint)}
} }
} }
return &PromptInspection{ return out
PromptID: inspection.PromptID, }
PromptHash: inspection.PromptHash,
DefaultProfileID: inspection.DefaultProfileID, func fromPromptResource(v *configuration.PromptResource) *PromptInspection {
Inputs: inputs, out := &PromptInspection{PromptID: v.ID, PromptHash: v.Hash, DefaultProfileID: copyStringPtr(v.DefaultProfile), Description: copyStringPtr(v.Description), SessionID: copyStringPtr(v.SessionID), OutputContract: PromptOutputInspection{Format: OutputFormat(v.Output.Format), ValidationMode: ValidationMode(v.Output.ValidationMode), SchemaPath: copyStringPtr(v.Output.SchemaPath), RepairAttempts: copyIntPtr(v.Output.RepairAttempts)}}
OutputContract: fromDomainOutputContract(inspection.OutputContract), if v.Inputs != nil {
inputs := make([]PromptInputInspection, len(*v.Inputs))
for i, input := range *v.Inputs {
inputs[i] = PromptInputInspection{Name: input.Name, Required: copyBoolPtr(input.Required), ContentType: copyStringPtr(input.ContentType), Description: copyStringPtr(input.Description)}
}
out.Inputs = &inputs
} }
out.Messages = make([]PromptMessageInspection, len(v.Messages))
for i, message := range v.Messages {
out.Messages[i] = PromptMessageInspection{Role: message.Role, Content: message.Content}
if message.CacheControl != nil {
out.Messages[i].CacheControl = &PromptCacheControlInspection{Type: CacheControlType(message.CacheControl.Type), TTL: copyStringPtr(message.CacheControl.TTL)}
}
}
return out
}
func copyBoolPtr(src *bool) *bool {
if src == nil {
return nil
}
value := *src
return &value
} }
func fromDomainExecutionTargetPresence(presence domain.ExecutionTargetPresence) ExecutionTargetPresence { func fromDomainExecutionTargetPresence(presence domain.ExecutionTargetPresence) ExecutionTargetPresence {

View File

@@ -343,6 +343,10 @@ the resource report or trigger a second read.
## Stage 5: Wire Resource Inspection Through The Engine And Facade ## Stage 5: Wire Resource Inspection Through The Engine And Facade
**Completed:** Captured resource views are wired through namespace selection
and detached public projections. An isolated assembled public-API test passes;
existing consumers and examples are migrated in the next stage.
**Scope:** Selection, public conversion, backend entry point, and errors. **Scope:** Selection, public conversion, backend entry point, and errors.
1. Add the three-method resource-inspection interface and captured collaborator 1. Add the three-method resource-inspection interface and captured collaborator

View File

@@ -9,6 +9,7 @@ import (
"time" "time"
"go.maximumdirect.net/configkit" "go.maximumdirect.net/configkit"
"go.maximumdirect.net/promptkit/internal/backend"
"go.maximumdirect.net/promptkit/internal/domain" "go.maximumdirect.net/promptkit/internal/domain"
internalengine "go.maximumdirect.net/promptkit/internal/engine" internalengine "go.maximumdirect.net/promptkit/internal/engine"
"go.maximumdirect.net/promptkit/internal/observation" "go.maximumdirect.net/promptkit/internal/observation"
@@ -30,6 +31,8 @@ var (
// ErrProfileNotFound identifies a selected profile ID that is absent from // ErrProfileNotFound identifies a selected profile ID that is absent from
// every configured profile source. It does not also match ErrProfileLoad. // every configured profile source. It does not also match ErrProfileLoad.
ErrProfileNotFound = errors.New("profile not found") ErrProfileNotFound = errors.New("profile not found")
// ErrBackendNotFound identifies a backend absent from resource inspection.
ErrBackendNotFound = errors.New("backend not found")
// ErrProfileRequired identifies a request for which neither RunRequest.ProfileID // ErrProfileRequired identifies a request for which neither RunRequest.ProfileID
// nor the selected prompt's default profile is present. Such an error also // nor the selected prompt's default profile is present. Such an error also
// matches ErrInvalidRequest. // matches ErrInvalidRequest.
@@ -81,12 +84,12 @@ var (
ErrValidation = errors.New("failed to validate output") ErrValidation = errors.New("failed to validate output")
) )
// Engine inspects prompts and profiles, enumerates prompt requirements, and // Engine inspects resources, enumerates prompt requirements, and
// prepares and runs Promptkit prompt requests. // prepares and runs Promptkit prompt requests.
// //
// An Engine is safe for concurrent calls to [Engine.WithNamespaces], // An Engine is safe for concurrent calls to [Engine.WithNamespaces],
// [Engine.InspectConfiguration], // [Engine.InspectConfiguration],
// [Engine.InspectPrompt], // [Engine.InspectPrompt], [Engine.InspectBackend],
// [Engine.InspectRequirements], [Engine.InspectProfile], [Engine.Prepare], [Engine.PrepareExecution], // [Engine.InspectRequirements], [Engine.InspectProfile], [Engine.Prepare], [Engine.PrepareExecution],
// [Engine.Run], [Engine.RunPrepared], [Engine.SmokeTest], [Engine.ListProfiles], // [Engine.Run], [Engine.RunPrepared], [Engine.SmokeTest], [Engine.ListProfiles],
// [Engine.ListPrompts], [Engine.ListBackends], [Engine.ListNamespaces], and // [Engine.ListPrompts], [Engine.ListBackends], [Engine.ListNamespaces], and
@@ -267,8 +270,9 @@ func NewEngine(cfg Config, opts ...Option) (*Engine, error) {
// qualified IDs pass through. // qualified IDs pass through.
// //
// A successful result proves that the selected definition and any referenced // A successful result proves that the selected definition and any referenced
// message content files were structurally loaded. Inputs are returned in // message content files were structurally loaded. Inputs and captured,
// definition order. DefaultProfileID is declared metadata only and is not // unrendered messages are returned in definition order. DefaultProfileID is
// declared metadata only and is not
// resolved. OutputContract is the normalized declared contract, with a JSON // resolved. OutputContract is the normalized declared contract, with a JSON
// Schema path when declared but without loading or compiling that schema. // Schema path when declared but without loading or compiling that schema.
// PromptHash is the same opaque equality value as PreparedRun.PromptHash for // PromptHash is the same opaque equality value as PreparedRun.PromptHash for
@@ -299,7 +303,7 @@ func (e *Engine) InspectPrompt(
if err != nil { if err != nil {
return nil, mapPublicError(err) return nil, mapPublicError(err)
} }
return fromDomainPromptInspection(inspection), nil return fromPromptResource(inspection), nil
} }
// InspectProfile resolves one explicit profile without selecting a prompt or // InspectProfile resolves one explicit profile without selecting a prompt or
@@ -310,8 +314,8 @@ func (e *Engine) InspectPrompt(
// resource without backend completion, execution defaults, or request overrides. // resource without backend completion, execution defaults, or request overrides.
// //
// APIKeyEnv in the returned resource is an environment-variable name, never its // APIKeyEnv in the returned resource is an environment-variable name, never its
// value. APIKeyRequired reports that a credential is required; an explicit // value. APIKeyRequired, when present and true, reports that a credential is
// profile environment source can also be present. InspectProfile neither derives // required; an explicit profile environment source can also be present. InspectProfile neither derives
// an ID from a prompt default_profile nor checks credential availability, so an // an ID from a prompt default_profile nor checks credential availability, so an
// absent or blank named environment variable is not an error. // absent or blank named environment variable is not an error.
// //
@@ -335,7 +339,32 @@ func (e *Engine) InspectProfile(ctx context.Context, profileID string) (*Profile
if err != nil { if err != nil {
return nil, mapPublicError(err) return nil, mapPublicError(err)
} }
return fromDomainProfileInspection(inspection), nil return fromProfileResource(inspection), nil
}
// InspectBackend selects a resolved backend by exact ID from the captured
// configuration, qualifying a slashless ID with the backend namespace. It
// reports configured resource fields and diagnostics, not credential values,
// derived diagnostic destinations, or runtime capacity occupancy. References
// are not resolved and no network request is made. The result and its mutable
// values are caller-owned. Source changes require a new engine.
//
// An invalid engine matches ErrInvalidConfig, an invalid ID matches
// ErrInvalidRequest, and an absent backend matches ErrBackendNotFound (not
// ErrInvalidRequest). Cancellation remains detectable via errors.Is. No
// partial result is returned on error.
func (e *Engine) InspectBackend(ctx context.Context, backendID string) (*BackendInspection, error) {
if e == nil || e.internal == nil {
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
}
inspection, err := e.internal.InspectBackend(ctx, backendID)
if err != nil {
if errors.Is(err, backend.ErrBackendNotFound) {
return nil, fmt.Errorf("%w: %w", ErrBackendNotFound, err)
}
return nil, mapPublicError(err)
}
return fromBackendResource(inspection), nil
} }
// Prepare resolves and renders a prompt request without calling an LLM. // Prepare resolves and renders a prompt request without calling an LLM.

View File

@@ -87,6 +87,7 @@ type Engine struct {
runner *usecase.Runner runner *usecase.Runner
smokeTester *usecase.SmokeTester smokeTester *usecase.SmokeTester
inspection configuration.Inspection inspection configuration.Inspection
resources resourceInspection
listing identityInventory listing identityInventory
namespaces resourceNamespaces namespaces resourceNamespaces
} }
@@ -105,6 +106,12 @@ type identityInventory interface {
ListCollections(context.Context) ([]configuration.CollectionIdentity, error) ListCollections(context.Context) ([]configuration.CollectionIdentity, error)
} }
type resourceInspection interface {
InspectPrompt(context.Context, string) (*configuration.PromptResource, error)
InspectProfile(context.Context, string) (*configuration.ProfileResource, error)
InspectBackend(context.Context, string) (*configuration.BackendResource, error)
}
// ListingFilterMode selects namespace membership for one identity listing. // ListingFilterMode selects namespace membership for one identity listing.
type ListingFilterMode uint8 type ListingFilterMode uint8
@@ -173,6 +180,7 @@ func New(cfg Config, options Options) (*Engine, error) {
runner: runner, runner: runner,
smokeTester: usecase.NewSmokeTester(runner, diagnosticHTTPClient), smokeTester: usecase.NewSmokeTester(runner, diagnosticHTTPClient),
inspection: snapshot.Inspection(), inspection: snapshot.Inspection(),
resources: snapshot,
listing: snapshot, listing: snapshot,
}, nil }, nil
} }
@@ -224,13 +232,16 @@ func fileSource(name string) (fs.FS, string, error) {
return os.DirFS(directory), filepath.ToSlash(base), nil return os.DirFS(directory), filepath.ToSlash(base), nil
} }
func (e *Engine) InspectPrompt(ctx context.Context, id string) (*domain.PromptInspection, error) { func (e *Engine) InspectPrompt(ctx context.Context, id string) (*configuration.PromptResource, error) {
if strings.TrimSpace(id) == "" {
return nil, fmt.Errorf("%w: prompt id is required", usecase.ErrInvalidRequest)
}
var err error var err error
id, err = e.qualifyResourceID(id, e.namespaces.prompt, "prompt") id, err = e.qualifyResourceID(id, e.namespaces.prompt, "prompt")
if err != nil { if err != nil {
return nil, err return nil, err
} }
return e.runner.InspectPrompt(ctx, id) return e.resources.InspectPrompt(ctx, id)
} }
func (e *Engine) InspectRequirements( func (e *Engine) InspectRequirements(
ctx context.Context, ctx context.Context,
@@ -247,13 +258,27 @@ func (e *Engine) InspectRequirements(
} }
return e.runner.InspectRequirements(ctx, request) return e.runner.InspectRequirements(ctx, request)
} }
func (e *Engine) InspectProfile(ctx context.Context, id string) (*domain.ProfileInspection, error) { func (e *Engine) InspectProfile(ctx context.Context, id string) (*configuration.ProfileResource, error) {
if strings.TrimSpace(id) == "" {
return nil, fmt.Errorf("%w: profile id is required", usecase.ErrInvalidRequest)
}
var err error var err error
id, err = e.qualifyResourceID(id, e.namespaces.profile, "profile") id, err = e.qualifyResourceID(id, e.namespaces.profile, "profile")
if err != nil { if err != nil {
return nil, err return nil, err
} }
return e.runner.InspectProfile(ctx, id) return e.resources.InspectProfile(ctx, id)
}
func (e *Engine) InspectBackend(ctx context.Context, id string) (*configuration.BackendResource, error) {
if strings.TrimSpace(id) == "" {
return nil, fmt.Errorf("%w: backend id is required", usecase.ErrInvalidRequest)
}
id, err := e.qualifyResourceID(id, e.namespaces.backend, "backend")
if err != nil {
return nil, err
}
return e.resources.InspectBackend(ctx, id)
} }
func (e *Engine) ListBackends(ctx context.Context, filters ...ListingFilter) ([]string, error) { func (e *Engine) ListBackends(ctx context.Context, filters ...ListingFilter) ([]string, error) {

View File

@@ -0,0 +1,45 @@
package promptkit
import (
"context"
"errors"
"testing"
"go.maximumdirect.net/configkit"
)
func TestResourceInspectionSelections(t *testing.T) {
engine, err := NewEngine(Config{Sources: ConfigurationSources{Main: &configkit.Input{Source: configkit.Mapping("resources", map[string]any{
"backends": []any{map[string]any{"id": "local", "endpoint": "https://example.test/v1"}},
"profiles": []any{map[string]any{"id": "partial", "backend": "local"}},
"prompts": []any{map[string]any{"id": "prompt", "messages": []any{map[string]any{"role": "user", "content": "hello"}}, "output": map[string]any{"format": "text", "validation_mode": "basic"}}},
})}}})
if err != nil {
t.Fatal(err)
}
ctx := context.Background()
if got, err := engine.InspectBackend(ctx, "local"); err != nil || got.BackendID != "local" {
t.Fatalf("backend = %+v, %v", got, err)
}
if got, err := engine.InspectProfile(ctx, "partial"); err != nil || got.Backend == nil || *got.Backend != "local" || got.Endpoint != nil {
t.Fatalf("profile = %+v, %v", got, err)
}
if got, err := engine.InspectPrompt(ctx, "prompt"); err != nil || got.Messages[0].Content != "hello" {
t.Fatalf("prompt = %+v, %v", got, err)
}
if got, err := engine.InspectBackend(ctx, "missing"); got != nil || !errors.Is(err, ErrBackendNotFound) || errors.Is(err, ErrInvalidRequest) {
t.Fatalf("missing = %+v, %v", got, err)
}
if _, err := engine.InspectBackend(ctx, "bad/id/extra"); !errors.Is(err, ErrInvalidRequest) {
t.Fatalf("invalid ID = %v", err)
}
var nilEngine *Engine
if _, err := nilEngine.InspectBackend(ctx, "local"); !errors.Is(err, ErrInvalidConfig) {
t.Fatalf("nil engine = %v", err)
}
canceled, cancel := context.WithCancel(ctx)
cancel()
if _, err := engine.InspectBackend(canceled, "local"); !errors.Is(err, context.Canceled) {
t.Fatalf("canceled = %v", err)
}
}