// 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" rakestrawhome "gitea.maximumdirect.net/eric/promptkit-backend-rakestrawhome" "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/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 } // 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) { 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) } client := options.llmClient if client == nil { client, err = llm.NewOpenAICompatibleClient(llm.OpenAICompatibleConfig{Timeout: cfg.Timeout, HTTPClient: cfg.HTTPClient}) if err != nil { return nil, err } } client = capacity.NewClient(manager, client) 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)}, 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) }