184 lines
5.9 KiB
Go
184 lines
5.9 KiB
Go
// 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/catalog"
|
|
"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
|
|
}
|
|
|
|
// 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, err := catalog.Underlays()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("select maintained configuration: %w", err)
|
|
}
|
|
underlays = append(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
|
|
if options.llmClient == nil {
|
|
builtIn, newErr := llm.NewOpenAICompatibleClient(llm.OpenAICompatibleConfig{Timeout: cfg.Timeout, HTTPClient: cfg.HTTPClient})
|
|
if newErr != nil {
|
|
return nil, newErr
|
|
}
|
|
client = generation.NewBuiltIn(manager, builtIn)
|
|
} else {
|
|
client = generation.NewInjected(manager, options.llmClient)
|
|
}
|
|
reader := options.artifactReader
|
|
if !options.artifactSource {
|
|
reader = artifact.NewCompositeReader()
|
|
}
|
|
|
|
return &Engine{runner: usecase.NewRunnerWithRepairer(snapshot, snapshot, snapshot, reader, prompt.NewGoRenderer(), client, validator, usecase.NewDefaultOutputRepairer(client), manager, defaultRetries)}, nil
|
|
}
|
|
|
|
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, version string) (*domain.PromptInspection, error) {
|
|
return e.runner.InspectPrompt(ctx, id, version)
|
|
}
|
|
func (e *Engine) InspectProfile(ctx context.Context, id string) (*domain.ProfileInspection, error) {
|
|
return e.runner.InspectProfile(ctx, id)
|
|
}
|
|
func (e *Engine) Prepare(ctx context.Context, request domain.RunRequest) (*domain.PreparedRun, error) {
|
|
return e.runner.Prepare(ctx, request)
|
|
}
|
|
func (e *Engine) PrepareExecution(ctx context.Context, request domain.RunRequest) (*usecase.PreparedExecution, error) {
|
|
return e.runner.PrepareExecution(ctx, request)
|
|
}
|
|
func (e *Engine) Run(ctx context.Context, request domain.RunRequest) (*domain.RunResult, error) {
|
|
return e.runner.Run(ctx, request)
|
|
}
|
|
func (e *Engine) RunPrepared(ctx context.Context, prepared *usecase.PreparedExecution) (*domain.RunResult, error) {
|
|
return e.runner.RunPrepared(ctx, prepared)
|
|
}
|