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"
"unicode/utf8"
"go.maximumdirect.net/promptkit/internal/configuration"
"go.maximumdirect.net/promptkit/internal/domain"
"go.maximumdirect.net/promptkit/internal/jsonvalue"
)
@@ -228,37 +229,58 @@ func fromDomainExecutionTargetWithOwnedParams(target domain.ExecutionTarget, ext
}
}
func fromDomainProfileInspection(inspection *domain.ProfileInspection) *ProfileInspection {
if inspection == nil {
return nil
}
return &ProfileInspection{
ProfileID: inspection.ProfileID,
EffectiveModelParams: fromDomainExecutionTarget(inspection.EffectiveModelParams),
APIKeyRequired: inspection.APIKeyRequired,
func fromProfileResource(v *configuration.ProfileResource) *ProfileInspection {
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)}
if v.ExtraParams != nil {
data := copyAnyMap(v.ExtraParams)
out.ExtraParams = &data
}
return out
}
func fromDomainPromptInspection(inspection *domain.PromptInspection) *PromptInspection {
if inspection == nil {
return nil
func fromBackendResource(v *configuration.BackendResource) *BackendInspection {
out := &BackendInspection{BackendID: v.ID, Endpoint: v.Endpoint, APIKeyEnv: copyStringPtr(v.APIKeyEnv), ConcurrencyLimit: copyIntPtr(v.ConcurrencyLimit), QueueCapacity: copyIntPtr(v.QueueCapacity)}
if v.ExtraParams != nil {
data := copyAnyMap(v.ExtraParams)
out.ExtraParams = &data
}
inputs := make([]PromptInputDefinition, len(inspection.Inputs))
for i, input := range inspection.Inputs {
inputs[i] = PromptInputDefinition{
Name: input.Name,
Required: input.Required,
ContentType: input.ContentType,
Description: input.Description,
if v.SmokeTests != nil {
out.SmokeTests = &BackendSmokeTestConfig{DefaultProfile: copyStringPtr(v.SmokeTests.DefaultProfile)}
if v.SmokeTests.Reachability != nil {
out.SmokeTests.Reachability = &ReachabilityProbeConfig{Endpoint: copyStringPtr(v.SmokeTests.Reachability.Endpoint)}
}
if v.SmokeTests.Authentication != nil {
out.SmokeTests.Authentication = &AuthenticationProbeConfig{Kind: copyStringPtr(v.SmokeTests.Authentication.Kind), Endpoint: copyStringPtr(v.SmokeTests.Authentication.Endpoint)}
}
}
return &PromptInspection{
PromptID: inspection.PromptID,
PromptHash: inspection.PromptHash,
DefaultProfileID: inspection.DefaultProfileID,
Inputs: inputs,
OutputContract: fromDomainOutputContract(inspection.OutputContract),
return out
}
func fromPromptResource(v *configuration.PromptResource) *PromptInspection {
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)}}
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 {

View File

@@ -343,6 +343,10 @@ the resource report or trigger a second read.
## 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.
1. Add the three-method resource-inspection interface and captured collaborator

View File

@@ -9,6 +9,7 @@ import (
"time"
"go.maximumdirect.net/configkit"
"go.maximumdirect.net/promptkit/internal/backend"
"go.maximumdirect.net/promptkit/internal/domain"
internalengine "go.maximumdirect.net/promptkit/internal/engine"
"go.maximumdirect.net/promptkit/internal/observation"
@@ -30,6 +31,8 @@ var (
// ErrProfileNotFound identifies a selected profile ID that is absent from
// every configured profile source. It does not also match ErrProfileLoad.
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
// nor the selected prompt's default profile is present. Such an error also
// matches ErrInvalidRequest.
@@ -81,12 +84,12 @@ var (
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.
//
// An Engine is safe for concurrent calls to [Engine.WithNamespaces],
// [Engine.InspectConfiguration],
// [Engine.InspectPrompt],
// [Engine.InspectPrompt], [Engine.InspectBackend],
// [Engine.InspectRequirements], [Engine.InspectProfile], [Engine.Prepare], [Engine.PrepareExecution],
// [Engine.Run], [Engine.RunPrepared], [Engine.SmokeTest], [Engine.ListProfiles],
// [Engine.ListPrompts], [Engine.ListBackends], [Engine.ListNamespaces], and
@@ -267,8 +270,9 @@ func NewEngine(cfg Config, opts ...Option) (*Engine, error) {
// qualified IDs pass through.
//
// A successful result proves that the selected definition and any referenced
// message content files were structurally loaded. Inputs are returned in
// definition order. DefaultProfileID is declared metadata only and is not
// message content files were structurally loaded. Inputs and captured,
// 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
// Schema path when declared but without loading or compiling that schema.
// PromptHash is the same opaque equality value as PreparedRun.PromptHash for
@@ -299,7 +303,7 @@ func (e *Engine) InspectPrompt(
if err != nil {
return nil, mapPublicError(err)
}
return fromDomainPromptInspection(inspection), nil
return fromPromptResource(inspection), nil
}
// 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.
//
// APIKeyEnv in the returned resource is an environment-variable name, never its
// value. APIKeyRequired reports that a credential is required; an explicit
// profile environment source can also be present. InspectProfile neither derives
// value. APIKeyRequired, when present and true, reports that a credential is
// 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
// 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 {
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.

View File

@@ -87,6 +87,7 @@ type Engine struct {
runner *usecase.Runner
smokeTester *usecase.SmokeTester
inspection configuration.Inspection
resources resourceInspection
listing identityInventory
namespaces resourceNamespaces
}
@@ -105,6 +106,12 @@ type identityInventory interface {
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.
type ListingFilterMode uint8
@@ -173,6 +180,7 @@ func New(cfg Config, options Options) (*Engine, error) {
runner: runner,
smokeTester: usecase.NewSmokeTester(runner, diagnosticHTTPClient),
inspection: snapshot.Inspection(),
resources: snapshot,
listing: snapshot,
}, nil
}
@@ -224,13 +232,16 @@ func fileSource(name string) (fs.FS, string, error) {
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
id, err = e.qualifyResourceID(id, e.namespaces.prompt, "prompt")
if err != nil {
return nil, err
}
return e.runner.InspectPrompt(ctx, id)
return e.resources.InspectPrompt(ctx, id)
}
func (e *Engine) InspectRequirements(
ctx context.Context,
@@ -247,13 +258,27 @@ func (e *Engine) InspectRequirements(
}
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
id, err = e.qualifyResourceID(id, e.namespaces.profile, "profile")
if err != nil {
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) {

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)
}
}