// Package engine assembles Promptkit's internal execution components. package engine import ( "context" "fmt" "io/fs" "net/http" "os" "path/filepath" "strings" "time" "gitea.maximumdirect.net/eric/ConfigKit" "gitea.maximumdirect.net/eric/promptkit/internal/artifact" "gitea.maximumdirect.net/eric/promptkit/internal/capacity" "gitea.maximumdirect.net/eric/promptkit/internal/configuration" "gitea.maximumdirect.net/eric/promptkit/internal/defaults" "gitea.maximumdirect.net/eric/promptkit/internal/domain" "gitea.maximumdirect.net/eric/promptkit/internal/generation" "gitea.maximumdirect.net/eric/promptkit/internal/llm" "gitea.maximumdirect.net/eric/promptkit/internal/prompt" "gitea.maximumdirect.net/eric/promptkit/internal/usecase" "gitea.maximumdirect.net/eric/promptkit/internal/validate" ) // Config carries construction settings selected by the public facade. type Config struct { Sources ConfigurationSources SchemaDir string Timeout time.Duration HTTPClient *http.Client TransientRetries *int } // ConfigurationSources identifies caller-selected ConfigKit layers. type ConfigurationSources struct { Underlays []configkit.Underlay Main *configkit.Input } // Options collects construction choices from the public facade. type Options struct { llmClient llm.Client artifactReader artifact.Reader validator validate.Validator validatorSource bool artifactSource bool } // SetLLMClient selects the client collaborator for one engine. func (o *Options) SetLLMClient(client llm.Client) { o.llmClient = client } // SetArtifactReader selects the artifact collaborator for one engine. func (o *Options) SetArtifactReader(reader artifact.Reader) { o.artifactReader = reader o.artifactSource = true } // SetSchemaFS selects a schema source rooted in fsys. func (o *Options) SetSchemaFS(fsys fs.FS, root string) error { if err := validateSourceFS(fsys, root); err != nil { return err } o.validator = validate.NewFSValidator(fsys, root) o.validatorSource = true return nil } // SetSchemaFile selects a single schema file. func (o *Options) SetSchemaFile(path string) error { fsys, root, _, err := fileSource(path) if err != nil { return err } o.validator = validate.NewFSValidator(fsys, root) o.validatorSource = true return nil } // Engine forwards domain operations to the single assembled use-case runner. type Engine struct { runner *usecase.Runner smokeTester *usecase.SmokeTester inspection configuration.Inspection listing identityInventory namespaces resourceNamespaces } type resourceNamespaces struct { prompt string profile string backend string } type identityInventory interface { ListProfiles(context.Context) ([]string, error) ListPrompts(context.Context) ([]domain.PromptIdentity, error) } // 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) if err != nil { return nil, err } underlays := cfg.Sources.Underlays main := configkit.Input{Source: configkit.NoSource()} if cfg.Sources.Main != nil { main = *cfg.Sources.Main } snapshot, err := configuration.Load(main, underlays) if err != nil { return nil, err } manager, err := capacity.NewManager(snapshot.CapacityPolicies()) if err != nil { return nil, fmt.Errorf("failed to construct backend capacity manager: %w", err) } validator := options.validator if !options.validatorSource { schemaDir := cfg.SchemaDir if strings.TrimSpace(schemaDir) == "" { schemaDir = defaults.SchemaDirDefault } validator = validate.NewStandardValidator(schemaDir) } var client llm.Client var diagnosticHTTPClient *llm.OpenAICompatibleClient if options.llmClient == nil { builtIn, newErr := llm.NewOpenAICompatibleClient(llm.OpenAICompatibleConfig{Timeout: cfg.Timeout, HTTPClient: cfg.HTTPClient}) if newErr != nil { return nil, newErr } diagnosticHTTPClient = builtIn client = generation.NewBuiltIn(manager, builtIn) } else { client = generation.NewInjected(manager, options.llmClient) } reader := options.artifactReader if !options.artifactSource { reader = artifact.NewCompositeReader() } runner := usecase.NewRunnerWithRepairer(snapshot, snapshot, reader, prompt.NewCompiledRenderer(snapshot), client, validator, usecase.NewDefaultOutputRepairer(client), manager, defaultRetries) return &Engine{ runner: runner, smokeTester: usecase.NewSmokeTester(runner, diagnosticHTTPClient), inspection: snapshot.Inspection(), listing: snapshot, }, nil } // WithNamespaces returns an immutable view sharing this engine's assembled // collaborators and configuration snapshot. Caller-supplied selection IDs are // qualified at each operation boundary; captured configuration references are // not rewritten. func (e *Engine) WithNamespaces(prompt, profile, backend string) *Engine { view := *e view.namespaces = resourceNamespaces{prompt: prompt, profile: profile, backend: backend} return &view } // InspectConfiguration returns read-only operations over the captured result. func (e *Engine) InspectConfiguration() configuration.Inspection { if e == nil { return nil } return e.inspection } func validateSourceFS(fsys fs.FS, root string) error { if fsys == nil { return fmt.Errorf("source filesystem is nil") } if strings.TrimSpace(root) == "" { return fmt.Errorf("source root is required") } return nil } func fileSource(name string) (fs.FS, string, string, error) { if strings.TrimSpace(name) == "" { return nil, "", "", fmt.Errorf("source path is required") } directory := filepath.Dir(name) base := filepath.Base(name) if base == "." || base == string(filepath.Separator) { return nil, "", "", fmt.Errorf("source path must name a file") } info, err := os.Stat(name) if err != nil { return nil, "", "", fmt.Errorf("failed to access source file %q: %w", name, err) } if info.IsDir() { return nil, "", "", fmt.Errorf("source path %q must be a file", name) } return os.DirFS(directory), filepath.ToSlash(base), directory, nil } func (e *Engine) InspectPrompt(ctx context.Context, id string) (*domain.PromptInspection, error) { var err error id, err = e.qualifyResourceID(id, e.namespaces.prompt, "prompt") if err != nil { return nil, err } return e.runner.InspectPrompt(ctx, id) } func (e *Engine) InspectRequirements( ctx context.Context, request usecase.RequirementInspectionRequest, ) (*domain.RequirementInspection, error) { var err error request.PromptID, err = e.qualifyResourceID(request.PromptID, e.namespaces.prompt, "prompt") if err != nil { return nil, err } request.ProfileID, err = e.qualifyResourceID(request.ProfileID, e.namespaces.profile, "profile") if err != nil { return nil, err } return e.runner.InspectRequirements(ctx, request) } func (e *Engine) InspectProfile(ctx context.Context, id string) (*domain.ProfileInspection, error) { var err error id, err = e.qualifyResourceID(id, e.namespaces.profile, "profile") if err != nil { return nil, err } return e.runner.InspectProfile(ctx, id) } func (e *Engine) ListProfiles(ctx context.Context) ([]string, error) { return e.listing.ListProfiles(ctx) } func (e *Engine) ListPrompts(ctx context.Context) ([]domain.PromptIdentity, error) { return e.listing.ListPrompts(ctx) } func (e *Engine) Prepare(ctx context.Context, request domain.RunRequest) (*domain.PreparedRun, error) { request, err := e.qualifyRunRequest(request) if err != nil { return nil, err } return e.runner.Prepare(ctx, request) } func (e *Engine) PrepareExecution(ctx context.Context, request domain.RunRequest) (*usecase.PreparedExecution, error) { request, err := e.qualifyRunRequest(request) if err != nil { return nil, err } return e.runner.PrepareExecution(ctx, request) } func (e *Engine) Run(ctx context.Context, request domain.RunRequest) (*domain.RunResult, error) { request, err := e.qualifyRunRequest(request) if err != nil { return nil, err } return e.runner.Run(ctx, request) } func (e *Engine) qualifyRunRequest(request domain.RunRequest) (domain.RunRequest, error) { var err error request.PromptID, err = e.qualifyResourceID(request.PromptID, e.namespaces.prompt, "prompt") if err != nil { return domain.RunRequest{}, err } request.ProfileID, err = e.qualifyResourceID(request.ProfileID, e.namespaces.profile, "profile") if err != nil { return domain.RunRequest{}, err } return request, nil } func (e *Engine) qualifyResourceID(id, namespace, kind string) (string, error) { if id == "" { return "", nil } qualified, ok := domain.QualifyResourceID(id, namespace) if !ok { return "", fmt.Errorf("%w: invalid %s ID", usecase.ErrInvalidRequest, kind) } return qualified, nil } func (e *Engine) RunPrepared(ctx context.Context, prepared *usecase.PreparedExecution) (*domain.RunResult, error) { return e.runner.RunPrepared(ctx, prepared) } func (e *Engine) SmokeTest(ctx context.Context, request domain.SmokeTestRequest) (*domain.SmokeTestReport, error) { if e == nil || e.smokeTester == nil { return nil, fmt.Errorf("%w: smoke tester is not configured", usecase.ErrInvalidRequest) } var err error request.PromptID, err = e.qualifyResourceID(request.PromptID, e.namespaces.prompt, "prompt") if err != nil { return nil, err } request.ProfileID, err = e.qualifyResourceID(request.ProfileID, e.namespaces.profile, "profile") if err != nil { return nil, err } request.BackendID, err = e.qualifyResourceID(request.BackendID, e.namespaces.backend, "backend") if err != nil { return nil, err } return e.smokeTester.Run(ctx, request) }