710 lines
29 KiB
Go
710 lines
29 KiB
Go
package promptkit
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
artifactadapter "gitea.maximumdirect.net/eric/promptkit/internal/artifact"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/backend"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/capacity"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/defaults"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/domain"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/llm"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/profile"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/profile/builtin"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/prompt"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/promptdef"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/usecase"
|
|
"gitea.maximumdirect.net/eric/promptkit/internal/validate"
|
|
)
|
|
|
|
// ErrInvalidConfig identifies invalid engine construction, including missing
|
|
// required configuration, invalid options or backend registrations, and a nil
|
|
// Engine receiver.
|
|
var ErrInvalidConfig = errors.New("invalid engine configuration")
|
|
|
|
var (
|
|
// ErrInvalidRequest identifies a request whose required values, overrides,
|
|
// credentials, or effective settings are invalid.
|
|
ErrInvalidRequest = errors.New("invalid run request")
|
|
// ErrPromptNotFound identifies a requested prompt ID or version that is not
|
|
// present in the selected prompt source. It does not also match
|
|
// ErrPromptLoad.
|
|
ErrPromptNotFound = errors.New("prompt not found")
|
|
// 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")
|
|
// ErrProfileRequired identifies a request for which neither RunRequest.ProfileID
|
|
// nor the selected prompt's default profile is present. Such an error also
|
|
// matches ErrInvalidRequest.
|
|
ErrProfileRequired = errors.New("profile selection is required")
|
|
// ErrPromptLoad identifies a failure to read, decode, validate, select, or
|
|
// hash a prompt definition, except for the not-found case represented by
|
|
// ErrPromptNotFound.
|
|
ErrPromptLoad = errors.New("failed to load prompt definition")
|
|
// ErrProfileLoad identifies a failure to read, decode, validate, or select
|
|
// an execution profile or resolve its backend, except for the profile
|
|
// not-found case represented by ErrProfileNotFound.
|
|
ErrProfileLoad = errors.New("failed to load execution profile")
|
|
// ErrAPIKeyEnvMissing identifies an APIKeyEnv whose environment variable is
|
|
// unset or empty when no direct RunRequest.APIKey takes precedence. Such an
|
|
// error also matches ErrInvalidRequest.
|
|
ErrAPIKeyEnvMissing = errors.New("api_key_env points to an unset environment variable")
|
|
// ErrArtifactLoad identifies a failure to resolve an input artifact. Errors
|
|
// returned by an injected ArtifactReader remain available through errors.Is.
|
|
ErrArtifactLoad = errors.New("failed to load artifact")
|
|
// ErrPromptRender identifies a failure to render prompt messages or the
|
|
// session ID from the resolved inputs and variables.
|
|
ErrPromptRender = errors.New("failed to render prompt")
|
|
// ErrCapacityExceeded identifies a Run or RunPrepared rejected because the
|
|
// selected backend already admitted ConcurrencyLimit + QueueCapacity calls.
|
|
// A [CapacityError] reports the selected backend ID. It is not an invalid
|
|
// request, an LLM or provider rate-limit response, or ErrLLMGenerate.
|
|
ErrCapacityExceeded = errors.New("backend capacity exceeded")
|
|
// ErrLLMGenerate identifies a model-client failure or a nil successful
|
|
// response. Errors returned by an injected LLMClient remain available
|
|
// through errors.Is.
|
|
ErrLLMGenerate = errors.New("failed to generate output")
|
|
// ErrValidation identifies an operational failure to load or compile a
|
|
// schema or validate output. A completed validation whose Status is
|
|
// ValidationFailed is returned in RunResult without this error.
|
|
ErrValidation = errors.New("failed to validate output")
|
|
)
|
|
|
|
// Engine inspects prompts and profiles and prepares and runs Promptkit prompt
|
|
// requests.
|
|
//
|
|
// An Engine is safe for concurrent calls to [Engine.InspectPrompt],
|
|
// [Engine.InspectProfile], [Engine.Prepare], [Engine.PrepareExecution],
|
|
// [Engine.Run], and [Engine.RunPrepared]. Each Engine owns independent
|
|
// backend-capacity pools that coordinate Run and RunPrepared admission and
|
|
// model generation. Injected collaborators may still be invoked concurrently
|
|
// across different backend pools or for unlimited backends.
|
|
type Engine struct {
|
|
runner *usecase.Runner
|
|
}
|
|
|
|
// Config selects the directory-backed sources and built-in model-client
|
|
// transport used by [NewEngine]. Config has no stable JSON representation.
|
|
type Config struct {
|
|
// PromptDir is the directory searched recursively for prompt definitions.
|
|
// It is required unless a WithPromptFS or WithPromptFile option supplies the
|
|
// prompt source.
|
|
PromptDir string
|
|
// ProfileDir is an optional ordinary configured source whose profiles take
|
|
// precedence over application fallback and embedded built-in profiles. An
|
|
// empty value selects the lower-precedence sources unless a profile-source
|
|
// option supplies the ordinary source.
|
|
ProfileDir string
|
|
// SchemaDir is the root for JSON Schema files. An empty value uses the
|
|
// current directory. WithSchemaFS or WithSchemaFile replaces this source.
|
|
SchemaDir string
|
|
// Timeout is the transport-wide safety cap for the built-in LLM client
|
|
// when HTTPClient is absent or has a non-positive timeout. A zero or negative
|
|
// value selects the 10-minute default.
|
|
Timeout time.Duration
|
|
// HTTPClient is cloned for the built-in LLM client. Its positive Timeout
|
|
// takes precedence over Timeout. A zero or negative client Timeout inherits
|
|
// Timeout or the 10-minute default. The supplied client is not mutated. This
|
|
// field is ignored when WithLLMClient is used.
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
// Option customizes engine construction.
|
|
//
|
|
// NewEngine applies options in argument order and ignores nil options. Within
|
|
// each prompt-source, ordinary-profile-source, fallback-profile-source,
|
|
// in-memory-profile, schema-source, model-client, and artifact-reader
|
|
// category, the last non-nil valid option replaces earlier options in that
|
|
// category. WithBackend is the additive exception: unique registrations
|
|
// accumulate, and a repeated backend ID is an error rather than a replacement.
|
|
// An invalid option fails construction even if a later option would replace it.
|
|
type Option interface {
|
|
apply(*engineOptions) error
|
|
}
|
|
|
|
type optionFunc func(*engineOptions) error
|
|
|
|
func (f optionFunc) apply(options *engineOptions) error {
|
|
return f(options)
|
|
}
|
|
|
|
type engineOptions struct {
|
|
llmClient llm.Client
|
|
artifactReader artifactadapter.Reader
|
|
promptDefs promptdef.Repository
|
|
profiles profile.Repository
|
|
fallbackProfiles profile.Repository
|
|
memoryProfiles profile.Repository
|
|
backends []domain.Backend
|
|
validator validate.Validator
|
|
promptSource bool
|
|
profileSource bool
|
|
fallbackProfileSource bool
|
|
memorySource bool
|
|
validatorSource bool
|
|
artifactSource bool
|
|
}
|
|
|
|
// WithLLMClient replaces the built-in model client used by [Engine.Run] and
|
|
// [Engine.RunPrepared].
|
|
//
|
|
// A nil client makes NewEngine fail with ErrInvalidConfig. The Engine schedules
|
|
// Generate calls according to the selected backend's capacity policy, but the
|
|
// client may still be called concurrently across different backend pools or for
|
|
// unlimited backends. The client is not used by [Engine.Prepare] or
|
|
// [Engine.PrepareExecution].
|
|
func WithLLMClient(client LLMClient) Option {
|
|
return optionFunc(func(options *engineOptions) error {
|
|
if client == nil {
|
|
return ErrInvalidConfig
|
|
}
|
|
options.llmClient = publicLLMClientAdapter{client: client}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// WithArtifactReader replaces the default reader for every input artifact
|
|
// reference, regardless of its ArtifactRef.Type.
|
|
//
|
|
// A nil reader makes NewEngine fail with ErrInvalidConfig. The reader may be
|
|
// called concurrently.
|
|
func WithArtifactReader(reader ArtifactReader) Option {
|
|
return optionFunc(func(options *engineOptions) error {
|
|
if reader == nil {
|
|
return ErrInvalidConfig
|
|
}
|
|
options.artifactReader = publicArtifactReaderAdapter{reader: reader}
|
|
options.artifactSource = true
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// WithPromptFS loads prompt definitions from fsys under root.
|
|
//
|
|
// The source uses the same strict prompt YAML rules as configured prompt
|
|
// directories, and prompt content_file paths resolve within this source.
|
|
// fsys must be non-nil and root must be non-empty; otherwise NewEngine fails
|
|
// with ErrInvalidConfig. This option replaces Config.PromptDir and earlier
|
|
// prompt-source options.
|
|
func WithPromptFS(fsys fs.FS, root string) Option {
|
|
return optionFunc(func(options *engineOptions) error {
|
|
if fsys == nil {
|
|
return ErrInvalidConfig
|
|
}
|
|
if strings.TrimSpace(root) == "" {
|
|
return ErrInvalidConfig
|
|
}
|
|
options.promptDefs = promptdef.NewFSRepository(fsys, root)
|
|
options.promptSource = true
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// WithPromptFile loads prompt definitions from the single prompt file at path.
|
|
//
|
|
// Relative prompt content_file paths resolve from the file's directory. path
|
|
// must name an existing non-directory file when NewEngine applies the option.
|
|
// This option replaces Config.PromptDir and earlier prompt-source options.
|
|
func WithPromptFile(path string) Option {
|
|
return optionFunc(func(options *engineOptions) error {
|
|
fsys, root, err := fileSource(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
options.promptDefs = promptdef.NewFSRepository(fsys, root)
|
|
options.promptSource = true
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// WithProfileFS loads execution profiles from fsys under root.
|
|
//
|
|
// Profiles from this ordinary configured source take precedence over
|
|
// application fallback and built-in profiles. Profile YAML must use api_key_env
|
|
// for environment-based credentials; raw API keys are rejected. fsys must be
|
|
// non-nil and root must be non-empty; otherwise NewEngine fails with
|
|
// ErrInvalidConfig. This option replaces Config.ProfileDir and earlier file or
|
|
// FS profile-source options, but remains below WithProfiles in precedence.
|
|
func WithProfileFS(fsys fs.FS, root string) Option {
|
|
return optionFunc(func(options *engineOptions) error {
|
|
if fsys == nil {
|
|
return ErrInvalidConfig
|
|
}
|
|
if strings.TrimSpace(root) == "" {
|
|
return ErrInvalidConfig
|
|
}
|
|
options.profiles = profile.NewFSRepository(fsys, root)
|
|
options.profileSource = true
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// WithProfileFile loads execution profiles from the single profile file at path.
|
|
//
|
|
// The profile takes precedence over application fallback and built-in profiles.
|
|
// Profile YAML must use api_key_env for environment-based credentials; raw API
|
|
// keys are rejected. path must name an existing non-directory file when
|
|
// NewEngine applies the option. This option replaces Config.ProfileDir and
|
|
// earlier file or FS profile-source options, but remains below WithProfiles in
|
|
// precedence.
|
|
func WithProfileFile(path string) Option {
|
|
return optionFunc(func(options *engineOptions) error {
|
|
fsys, root, err := fileSource(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
options.profiles = profile.NewFSRepository(fsys, root)
|
|
options.profileSource = true
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// WithFallbackProfileFS supplies application-owned fallback profile
|
|
// definitions from fsys under root.
|
|
//
|
|
// Profile lookup checks, in order, profiles supplied by WithProfiles; the
|
|
// ordinary configured source selected by WithProfileFile, WithProfileFS, or
|
|
// Config.ProfileDir; this fallback source; and Promptkit's embedded built-in
|
|
// profiles. Each source supplies a complete profile definition; profile fields
|
|
// are not merged between sources. Only an absent profile ID proceeds to the
|
|
// next source. A matching read, parse, duplicate, validation, or credential
|
|
// format failure stops resolution.
|
|
//
|
|
// Files use the ordinary strict profile YAML and api_key_env credential rules.
|
|
// Loading and validation are lazy: NewEngine validates this option's arguments
|
|
// but does not read profile files. fsys must be non-nil and root must be
|
|
// nonblank; otherwise NewEngine returns an error matching ErrInvalidConfig.
|
|
// Repeating this option replaces the earlier valid fallback source.
|
|
//
|
|
// This option controls profile-definition lookup, not provider or generation
|
|
// failover.
|
|
func WithFallbackProfileFS(fsys fs.FS, root string) Option {
|
|
return optionFunc(func(options *engineOptions) error {
|
|
if fsys == nil {
|
|
return ErrInvalidConfig
|
|
}
|
|
if strings.TrimSpace(root) == "" {
|
|
return ErrInvalidConfig
|
|
}
|
|
options.fallbackProfiles = profile.NewFSRepository(fsys, root)
|
|
options.fallbackProfileSource = true
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// WithProfiles configures in-memory profiles that take precedence over
|
|
// ordinary configured, application fallback, and built-in profiles.
|
|
//
|
|
// NewEngine validates and copies every profile. IDs must be unique within one
|
|
// call. An invalid profile, duplicate ID, or unsupported ExtraParams value
|
|
// makes construction fail with ErrInvalidConfig. Repeating WithProfiles
|
|
// replaces the complete earlier in-memory set rather than merging it.
|
|
func WithProfiles(profiles ...Profile) Option {
|
|
return optionFunc(func(options *engineOptions) error {
|
|
repo, err := newMemoryProfileRepository(profiles)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
options.memoryProfiles = repo
|
|
options.memorySource = true
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// WithSchemaFS loads JSON Schema documents from fsys under root.
|
|
//
|
|
// Prompt schema_path values resolve within this source when schema validation
|
|
// or structured output is requested. fsys must be non-nil and root must be
|
|
// non-empty; otherwise NewEngine fails with ErrInvalidConfig. This option
|
|
// replaces Config.SchemaDir and earlier schema-source options.
|
|
func WithSchemaFS(fsys fs.FS, root string) Option {
|
|
return optionFunc(func(options *engineOptions) error {
|
|
if fsys == nil {
|
|
return ErrInvalidConfig
|
|
}
|
|
if strings.TrimSpace(root) == "" {
|
|
return ErrInvalidConfig
|
|
}
|
|
options.validator = validate.NewFSValidator(fsys, root)
|
|
options.validatorSource = true
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// WithSchemaFile loads JSON Schema documents from the single schema file at path.
|
|
//
|
|
// Prompt schema_path values refer to the file's base name. path must name an
|
|
// existing non-directory file when NewEngine applies the option. This option
|
|
// replaces Config.SchemaDir and earlier schema-source options.
|
|
func WithSchemaFile(path string) Option {
|
|
return optionFunc(func(options *engineOptions) error {
|
|
fsys, root, err := fileSource(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
options.validator = validate.NewFSValidator(fsys, root)
|
|
options.validatorSource = true
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// NewEngine constructs an Engine from configuration and options.
|
|
//
|
|
// Options are applied in order according to [Option]. PromptDir is required
|
|
// unless a prompt-source option is present. Construction validates option
|
|
// arguments, in-memory profiles, and backend registrations but defers reading
|
|
// and validating prompt, file-backed profile, and schema contents until Prepare
|
|
// or Run needs them.
|
|
//
|
|
// NewEngine returns an error matching ErrInvalidConfig for invalid
|
|
// configuration, options, or backend-capacity policies. Each constructed
|
|
// Engine has independent backend-capacity pools. Construction does not perform
|
|
// model requests or require credentials.
|
|
func NewEngine(cfg Config, opts ...Option) (*Engine, error) {
|
|
var options engineOptions
|
|
for _, opt := range opts {
|
|
if opt == nil {
|
|
continue
|
|
}
|
|
if err := opt.apply(&options); err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrInvalidConfig, err)
|
|
}
|
|
}
|
|
|
|
promptDefs := options.promptDefs
|
|
if !options.promptSource {
|
|
if strings.TrimSpace(cfg.PromptDir) == "" {
|
|
return nil, fmt.Errorf("%w: prompt directory is required", ErrInvalidConfig)
|
|
}
|
|
promptDefs = promptdef.NewFilesystemRepository(cfg.PromptDir)
|
|
}
|
|
|
|
profiles := newProfileRepository(cfg.ProfileDir, options)
|
|
|
|
backendRegistry, err := backend.NewRegistry(options.backends)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: failed to construct backend registry: %v", ErrInvalidConfig, err)
|
|
}
|
|
|
|
capacityManager, err := capacity.NewManager(backendRegistry.CapacityPolicies())
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: failed to construct backend capacity manager: %v", ErrInvalidConfig, err)
|
|
}
|
|
|
|
validator := options.validator
|
|
if !options.validatorSource {
|
|
schemaDir := cfg.SchemaDir
|
|
if strings.TrimSpace(schemaDir) == "" {
|
|
schemaDir = defaults.SchemaDirDefault
|
|
}
|
|
validator = validate.NewStandardValidator(schemaDir)
|
|
}
|
|
|
|
llmClient := options.llmClient
|
|
if llmClient == nil {
|
|
var err error
|
|
llmClient, err = llm.NewOpenAICompatibleClient(llm.OpenAICompatibleConfig{
|
|
Timeout: cfg.Timeout,
|
|
HTTPClient: cfg.HTTPClient,
|
|
})
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrInvalidConfig, err)
|
|
}
|
|
}
|
|
llmClient = capacity.NewClient(capacityManager, llmClient)
|
|
|
|
artifacts := options.artifactReader
|
|
if !options.artifactSource {
|
|
artifacts = artifactadapter.NewCompositeReader()
|
|
}
|
|
|
|
return &Engine{
|
|
runner: usecase.NewRunner(
|
|
promptDefs,
|
|
profiles,
|
|
backendRegistry,
|
|
artifacts,
|
|
prompt.NewGoRenderer(),
|
|
llmClient,
|
|
validator,
|
|
capacityManager,
|
|
),
|
|
}, nil
|
|
}
|
|
|
|
func newProfileRepository(profileDir string, options engineOptions) profile.Repository {
|
|
repository := builtin.NewRepository()
|
|
|
|
if options.fallbackProfileSource {
|
|
repository = profile.NewOverlayRepository(options.fallbackProfiles, repository)
|
|
}
|
|
|
|
if options.profileSource {
|
|
repository = profile.NewOverlayRepository(options.profiles, repository)
|
|
} else if strings.TrimSpace(profileDir) != "" {
|
|
repository = profile.NewOverlayRepository(profile.NewFilesystemRepository(profileDir), repository)
|
|
}
|
|
|
|
if options.memorySource {
|
|
repository = profile.NewOverlayRepository(options.memoryProfiles, repository)
|
|
}
|
|
|
|
return repository
|
|
}
|
|
|
|
func fileSource(name string) (fs.FS, string, error) {
|
|
cleanName := strings.TrimSpace(name)
|
|
if cleanName == "" {
|
|
return nil, "", ErrInvalidConfig
|
|
}
|
|
dir := filepath.Dir(cleanName)
|
|
base := filepath.Base(cleanName)
|
|
if base == "." || base == string(filepath.Separator) || strings.TrimSpace(base) == "" {
|
|
return nil, "", ErrInvalidConfig
|
|
}
|
|
info, err := os.Stat(cleanName)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("%w: failed to access source file %q: %v", ErrInvalidConfig, cleanName, err)
|
|
}
|
|
if info.IsDir() {
|
|
return nil, "", fmt.Errorf("%w: source path %q must be a file", ErrInvalidConfig, cleanName)
|
|
}
|
|
return os.DirFS(dir), filepath.ToSlash(base), nil
|
|
}
|
|
|
|
// InspectPrompt resolves one explicit prompt definition without selecting a
|
|
// profile or starting execution work.
|
|
//
|
|
// InspectPrompt requires a nonblank promptID. It passes nonblank promptID and
|
|
// promptVersion values unchanged to the engine's ordinary, case-sensitive
|
|
// prompt selection. An empty version succeeds only when that source has one
|
|
// selected ID; a nonempty version selects one exact ID/version pair. The
|
|
// configured prompt source is used without merging, fallback, or enumeration.
|
|
//
|
|
// 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
|
|
// 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
|
|
// the selected definition and observed source state; its spelling, length,
|
|
// encoding, algorithm, and security properties are not contracts.
|
|
//
|
|
// This method does not return prompt bodies, templates, source paths, schemas,
|
|
// rendered messages, or execution settings. It does not resolve a profile or
|
|
// credential, read artifacts or schemas, render, validate, admit capacity,
|
|
// contact a provider, or generate model output. The returned PromptInspection
|
|
// and its input slice are caller-owned. Filesystem-backed inspection is a
|
|
// point-in-time lookup and does not freeze a definition for later execution.
|
|
//
|
|
// A nil Engine returns an error matching ErrInvalidConfig. A blank prompt ID
|
|
// matches ErrInvalidRequest. An absent exact ID or version matches
|
|
// ErrPromptNotFound and not ErrPromptLoad. Malformed, unreadable, duplicate,
|
|
// ambiguous, referenced-content, or hashing failures match ErrPromptLoad.
|
|
// Cancellation during lookup matches ErrPromptLoad while preserving the
|
|
// context error. InspectPrompt returns no partial result on error.
|
|
func (e *Engine) InspectPrompt(
|
|
ctx context.Context,
|
|
promptID string,
|
|
promptVersion string,
|
|
) (*PromptInspection, error) {
|
|
if e == nil || e.runner == nil {
|
|
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
|
}
|
|
|
|
inspection, err := e.runner.InspectPrompt(ctx, promptID, promptVersion)
|
|
if err != nil {
|
|
return nil, mapPublicError(err)
|
|
}
|
|
return fromDomainPromptInspection(inspection), nil
|
|
}
|
|
|
|
// InspectProfile resolves one explicit profile without selecting a prompt or
|
|
// starting execution work.
|
|
//
|
|
// InspectProfile trims surrounding whitespace from profileID and looks up the
|
|
// resulting nonblank ID exactly and case-sensitively through the engine's
|
|
// in-memory, ordinary configured-source, application fallback, and built-in
|
|
// profile precedence. It applies the framework timeout baseline, selected
|
|
// backend, and then selected profile to EffectiveModelParams without a request
|
|
// override. BackendID is empty for an endpoint-only profile.
|
|
//
|
|
// APIKeyEnv in the returned target is an environment-variable name, never its
|
|
// value. APIKeyRequired instead reports a direct credential requirement and is
|
|
// mutually exclusive with a nonblank APIKeyEnv. 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.
|
|
//
|
|
// The returned ProfileInspection and all nested mutable values are
|
|
// caller-owned. Filesystem-backed inspection is a point-in-time lookup and
|
|
// does not freeze the profile for a later execution. This method does not load
|
|
// a prompt, render, read artifacts or schemas, admit backend capacity, contact
|
|
// a provider, or generate model output.
|
|
//
|
|
// A nil Engine returns an error matching ErrInvalidConfig. A blank profile ID
|
|
// matches ErrInvalidRequest. An absent exact ID matches ErrProfileNotFound and
|
|
// not ErrProfileLoad. Malformed or unreadable profile data, an unknown backend,
|
|
// or an invalid resolved target matches ErrProfileLoad. Cancellation during
|
|
// profile loading matches ErrProfileLoad while preserving the context error.
|
|
// InspectProfile returns no partial result on error.
|
|
func (e *Engine) InspectProfile(ctx context.Context, profileID string) (*ProfileInspection, error) {
|
|
if e == nil || e.runner == nil {
|
|
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
|
}
|
|
|
|
inspection, err := e.runner.InspectProfile(ctx, profileID)
|
|
if err != nil {
|
|
return nil, mapPublicError(err)
|
|
}
|
|
return fromDomainProfileInspection(inspection), nil
|
|
}
|
|
|
|
// Prepare resolves and renders a prompt request without calling an LLM.
|
|
//
|
|
// Prepare selects the prompt and profile, resolves any selected backend and
|
|
// effective execution settings, resolves the output contract, loads and hashes
|
|
// inputs, loads structured-output schema metadata when required, and renders
|
|
// the session ID and messages. The returned PreparedRun is owned by the caller
|
|
// and never contains a resolved API-key value, model output, or validation
|
|
// result.
|
|
//
|
|
// A nil Engine returns an error matching ErrInvalidConfig. Request and
|
|
// preparation failures may match ErrInvalidRequest, ErrPromptNotFound,
|
|
// ErrPromptLoad, ErrProfileNotFound, ErrProfileLoad, ErrProfileRequired,
|
|
// ErrAPIKeyEnvMissing, ErrArtifactLoad, ErrPromptRender, or ErrValidation as
|
|
// applicable. Cancellation is passed to the active collaborator and is
|
|
// reported in the applicable operation category; no general errors.Is
|
|
// relationship to ctx.Err is promised. Prepare returns no partial result on
|
|
// error.
|
|
func (e *Engine) Prepare(ctx context.Context, req RunRequest) (*PreparedRun, error) {
|
|
if e == nil || e.runner == nil {
|
|
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
|
}
|
|
|
|
domainReq, err := toDomainRunRequest(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
|
|
}
|
|
|
|
prepared, err := e.runner.Prepare(ctx, domainReq)
|
|
if err != nil {
|
|
return nil, mapPublicError(err)
|
|
}
|
|
return fromDomainPreparedRun(prepared), nil
|
|
}
|
|
|
|
// PrepareExecution completely prepares a prompt request without calling the
|
|
// configured LLMClient or reserving backend admission capacity.
|
|
//
|
|
// The returned opaque handle is bound to this Engine and permits one
|
|
// [Engine.RunPrepared] invocation. Preparation freezes the selected sources,
|
|
// rendered messages, effective settings, inputs, provider structured-output
|
|
// metadata, and validation resources needed by that invocation. The handle
|
|
// retains a direct RunRequest.APIKey only in private execution state;
|
|
// [PreparedExecution.Details] is credential-redacted.
|
|
//
|
|
// The context governs preparation only. Cancellation after this method
|
|
// returns does not invalidate the handle or propagate to RunPrepared.
|
|
// PrepareExecution returns the same error categories as [Engine.Prepare] and
|
|
// returns no handle on error. A nil Engine returns an error matching
|
|
// ErrInvalidConfig.
|
|
func (e *Engine) PrepareExecution(ctx context.Context, req RunRequest) (*PreparedExecution, error) {
|
|
if e == nil || e.runner == nil {
|
|
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
|
}
|
|
|
|
domainReq, err := toDomainRunRequest(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
|
|
}
|
|
|
|
prepared, err := e.runner.PrepareExecution(ctx, domainReq)
|
|
if err != nil {
|
|
return nil, mapPublicError(err)
|
|
}
|
|
return &PreparedExecution{internal: prepared}, nil
|
|
}
|
|
|
|
// Run prepares a request, invokes the configured LLMClient, and validates the
|
|
// generated output.
|
|
//
|
|
// A content-validation failure is a successful run whose
|
|
// RunResult.Validation has Status ValidationFailed. An inability to perform
|
|
// validation returns an error matching ErrValidation and no partial result.
|
|
// The public Engine does not perform output repair, so validation is
|
|
// single-pass even when OutputContract.RepairAttempts is positive.
|
|
//
|
|
// Run can return every error category documented by [Engine.Prepare], plus
|
|
// ErrCapacityExceeded and ErrLLMGenerate. An engine admission rejection is
|
|
// discoverable as [CapacityError] and still matches ErrCapacityExceeded. It
|
|
// occurs before artifacts, schemas, rendering, or model generation because the
|
|
// selected backend's admission capacity is full; it does not match
|
|
// ErrInvalidRequest or ErrLLMGenerate. Errors from injected clients remain
|
|
// available through errors.Is. Cancellation while waiting for model-generation
|
|
// capacity matches both ErrLLMGenerate and the context error. Cancellation
|
|
// otherwise follows the active collaborator's documented behavior. A nil
|
|
// Engine returns ErrInvalidConfig. Run returns no partial result on error.
|
|
func (e *Engine) Run(ctx context.Context, req RunRequest) (*RunResult, error) {
|
|
if e == nil || e.runner == nil {
|
|
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
|
}
|
|
|
|
domainReq, err := toDomainRunRequest(req)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrInvalidRequest, err)
|
|
}
|
|
|
|
result, err := e.runner.Run(ctx, domainReq)
|
|
if err != nil {
|
|
return nil, mapPublicError(err)
|
|
}
|
|
return fromDomainRunResult(result), nil
|
|
}
|
|
|
|
// RunPrepared atomically claims and executes a handle created by
|
|
// [Engine.PrepareExecution].
|
|
//
|
|
// A valid owning-Engine invocation consumes the handle's one attempt before
|
|
// credential revalidation, backend admission, generation, or validation.
|
|
// Cancellation, capacity rejection, generation failure, operational
|
|
// validation failure, and success all leave the handle unusable. A nil,
|
|
// zero-value, foreign-Engine, discarded, claimed, or used handle returns an
|
|
// error matching ErrInvalidRequest; a nil Engine returns ErrInvalidConfig and
|
|
// does not claim the handle.
|
|
//
|
|
// The supplied context governs this execution attempt independently of the
|
|
// preparation context. It covers credential revalidation, admission,
|
|
// generation, validation, and any internal repair. Result timing begins after
|
|
// the claim and excludes preparation and consumer-held delay.
|
|
//
|
|
// RunPrepared can return ErrInvalidRequest, ErrAPIKeyEnvMissing,
|
|
// ErrCapacityExceeded, ErrLLMGenerate, or ErrValidation as applicable while
|
|
// preserving documented collaborator and context identities. An engine
|
|
// admission rejection is discoverable as [CapacityError] and still matches
|
|
// ErrCapacityExceeded. A completed content-validation rejection is returned
|
|
// in RunResult, not as an operational error. An operational error returns no
|
|
// partial RunResult.
|
|
func (e *Engine) RunPrepared(ctx context.Context, prepared *PreparedExecution) (*RunResult, error) {
|
|
if e == nil || e.runner == nil {
|
|
return nil, fmt.Errorf("%w: engine is nil", ErrInvalidConfig)
|
|
}
|
|
|
|
var internal *usecase.PreparedExecution
|
|
if prepared != nil {
|
|
internal = prepared.internal
|
|
}
|
|
result, err := e.runner.RunPrepared(ctx, internal)
|
|
if err != nil {
|
|
return nil, mapPublicError(err)
|
|
}
|
|
return fromDomainRunResult(result), nil
|
|
}
|