286 lines
9.6 KiB
Go
286 lines
9.6 KiB
Go
// Package engine assembles Promptkit's internal execution components.
|
|
package engine
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
openrouter "gitea.maximumdirect.net/eric/promptkit-backend-openrouter/v2"
|
|
rakestrawhome "gitea.maximumdirect.net/eric/promptkit-backend-rakestrawhome/v2"
|
|
"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/catalog"
|
|
"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/profile"
|
|
"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"
|
|
)
|
|
|
|
// Config carries construction settings selected by the public facade.
|
|
type Config struct {
|
|
PromptDir string
|
|
ProfileDir string
|
|
SchemaDir string
|
|
Timeout time.Duration
|
|
HTTPClient *http.Client
|
|
TransientRetries *int
|
|
}
|
|
|
|
// Options collects construction choices from the public facade.
|
|
type Options struct {
|
|
llmClient llm.Client
|
|
artifactReader artifact.Reader
|
|
promptDefinitions 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
|
|
}
|
|
|
|
// 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
|
|
}
|
|
|
|
// SetPromptFS selects a prompt source rooted in fsys.
|
|
func (o *Options) SetPromptFS(fsys fs.FS, root string) error {
|
|
if err := validateSourceFS(fsys, root); err != nil {
|
|
return err
|
|
}
|
|
o.promptDefinitions = promptdef.NewFSRepository(fsys, root)
|
|
o.promptSource = true
|
|
return nil
|
|
}
|
|
|
|
// SetPromptFile selects a single prompt file.
|
|
func (o *Options) SetPromptFile(path string) error {
|
|
fsys, root, directory, err := fileSource(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.promptDefinitions = promptdef.NewFileRepository(fsys, root, directory)
|
|
o.promptSource = true
|
|
return nil
|
|
}
|
|
|
|
// SetProfileFS selects an ordinary profile source rooted in fsys.
|
|
func (o *Options) SetProfileFS(fsys fs.FS, root string) error {
|
|
if err := validateSourceFS(fsys, root); err != nil {
|
|
return err
|
|
}
|
|
o.profiles = profile.NewFSRepository(fsys, root)
|
|
o.profileSource = true
|
|
return nil
|
|
}
|
|
|
|
// SetProfileFile selects a single ordinary profile file.
|
|
func (o *Options) SetProfileFile(path string) error {
|
|
fsys, root, _, err := fileSource(path)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.profiles = profile.NewFSRepository(fsys, root)
|
|
o.profileSource = true
|
|
return nil
|
|
}
|
|
|
|
// SetFallbackProfileFS selects a fallback profile source rooted in fsys.
|
|
func (o *Options) SetFallbackProfileFS(fsys fs.FS, root string) error {
|
|
if err := validateSourceFS(fsys, root); err != nil {
|
|
return err
|
|
}
|
|
o.fallbackProfiles = profile.NewFSRepository(fsys, root)
|
|
o.fallbackProfileSource = true
|
|
return nil
|
|
}
|
|
|
|
// SetMemoryProfiles validates and stores application-owned profile definitions.
|
|
func (o *Options) SetMemoryProfiles(definitions []domain.ExecutionProfile) error {
|
|
repository, err := profile.NewMemoryRepository(definitions)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
o.memoryProfiles = repository
|
|
o.memorySource = true
|
|
return nil
|
|
}
|
|
|
|
// AddBackend appends one backend registration.
|
|
func (o *Options) AddBackend(backend domain.Backend) { o.backends = append(o.backends, backend) }
|
|
|
|
// 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
|
|
}
|
|
promptDefinitions := options.promptDefinitions
|
|
if !options.promptSource {
|
|
if strings.TrimSpace(cfg.PromptDir) == "" {
|
|
return nil, fmt.Errorf("prompt directory is required")
|
|
}
|
|
promptDefinitions = promptdef.NewFilesystemRepository(cfg.PromptDir)
|
|
}
|
|
|
|
maintained, err := catalog.Load(
|
|
catalog.Source{Name: "OpenRouter", ExpectedBackendID: backend.OpenRouterID, FS: openrouter.FS(), Root: openrouter.Root},
|
|
catalog.Source{Name: "Rakestrawhome", ExpectedBackendID: backend.RakestrawHomeID, FS: rakestrawhome.FS(), Root: rakestrawhome.Root},
|
|
)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to load maintained catalogs: %w", err)
|
|
}
|
|
|
|
maintainedProfiles, err := newMaintainedProfileRepository(maintained.Profiles, maintained.Backends)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to construct Promptkit-maintained profiles: %w", err)
|
|
}
|
|
|
|
profiles := newProfileRepository(cfg.ProfileDir, options, maintainedProfiles)
|
|
registry, err := backend.NewRegistry(maintained.Backends, options.backends)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to construct backend registry: %w", err)
|
|
}
|
|
manager, err := capacity.NewManager(registry.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(promptDefinitions, profiles, registry, reader, prompt.NewGoRenderer(), client, validator, usecase.NewDefaultOutputRepairer(client), manager, defaultRetries)}, nil
|
|
}
|
|
|
|
func newProfileRepository(profileDir string, options Options, maintained profile.Repository) profile.Repository {
|
|
repository := maintained
|
|
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 profile.NewResolvingRepository(repository)
|
|
}
|
|
|
|
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)
|
|
}
|