139 lines
3.9 KiB
Go
139 lines
3.9 KiB
Go
package scriptorium
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
artifactadapter "gitea.maximumdirect.net/eric/scriptorium/internal/artifact"
|
|
"gitea.maximumdirect.net/eric/scriptorium/internal/defaults"
|
|
"gitea.maximumdirect.net/eric/scriptorium/internal/llm"
|
|
"gitea.maximumdirect.net/eric/scriptorium/internal/profile/builtin"
|
|
"gitea.maximumdirect.net/eric/scriptorium/internal/prompt"
|
|
"gitea.maximumdirect.net/eric/scriptorium/internal/promptdef"
|
|
"gitea.maximumdirect.net/eric/scriptorium/internal/usecase"
|
|
"gitea.maximumdirect.net/eric/scriptorium/internal/validate"
|
|
)
|
|
|
|
// ErrInvalidConfig indicates invalid public engine configuration.
|
|
var ErrInvalidConfig = errors.New("invalid engine configuration")
|
|
|
|
var (
|
|
ErrInvalidRequest = errors.New("invalid run request")
|
|
ErrPromptNotFound = errors.New("prompt not found")
|
|
ErrProfileNotFound = errors.New("profile not found")
|
|
ErrPromptLoad = errors.New("failed to load prompt definition")
|
|
ErrProfileLoad = errors.New("failed to load execution profile")
|
|
ErrArtifactLoad = errors.New("failed to load artifact")
|
|
ErrPromptRender = errors.New("failed to render prompt")
|
|
ErrLLMGenerate = errors.New("failed to generate output")
|
|
ErrValidation = errors.New("failed to validate output")
|
|
)
|
|
|
|
// Engine prepares and runs Scriptorium prompt requests.
|
|
type Engine struct {
|
|
runner *usecase.Runner
|
|
}
|
|
|
|
// Config configures a public Scriptorium engine.
|
|
type Config struct {
|
|
PromptDir string
|
|
ProfileDir string
|
|
SchemaDir string
|
|
Timeout time.Duration
|
|
HTTPClient *http.Client
|
|
}
|
|
|
|
// Option customizes engine construction.
|
|
type Option func(*engineOptions) error
|
|
|
|
type engineOptions struct {
|
|
llmClient llm.Client
|
|
}
|
|
|
|
// WithLLMClient injects a custom LLM client for execution.
|
|
func WithLLMClient(client LLMClient) Option {
|
|
return func(options *engineOptions) error {
|
|
if client == nil {
|
|
return ErrInvalidConfig
|
|
}
|
|
options.llmClient = publicLLMClientAdapter{client: client}
|
|
return nil
|
|
}
|
|
}
|
|
|
|
// NewEngine constructs an Engine using the same default internal components as
|
|
// the CLI and HTTP adapters.
|
|
func NewEngine(cfg Config, opts ...Option) (*Engine, error) {
|
|
if strings.TrimSpace(cfg.PromptDir) == "" {
|
|
return nil, fmt.Errorf("%w: prompt directory is required", ErrInvalidConfig)
|
|
}
|
|
|
|
var options engineOptions
|
|
for _, opt := range opts {
|
|
if opt == nil {
|
|
continue
|
|
}
|
|
if err := opt(&options); err != nil {
|
|
return nil, fmt.Errorf("%w: %v", ErrInvalidConfig, err)
|
|
}
|
|
}
|
|
|
|
schemaDir := cfg.SchemaDir
|
|
if strings.TrimSpace(schemaDir) == "" {
|
|
schemaDir = defaults.SchemaDirDefault
|
|
}
|
|
|
|
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)
|
|
}
|
|
}
|
|
|
|
return &Engine{
|
|
runner: usecase.NewRunner(
|
|
promptdef.NewFilesystemRepository(cfg.PromptDir),
|
|
builtin.NewRepositoryWithDirectory(cfg.ProfileDir),
|
|
artifactadapter.NewCompositeReader(),
|
|
prompt.NewGoRenderer(),
|
|
llmClient,
|
|
validate.NewStandardValidator(schemaDir),
|
|
),
|
|
}, nil
|
|
}
|
|
|
|
// Prepare resolves a prompt request without calling an LLM.
|
|
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)
|
|
}
|
|
|
|
prepared, err := e.runner.Prepare(ctx, toDomainRunRequest(req))
|
|
if err != nil {
|
|
return nil, mapPublicError(err)
|
|
}
|
|
return fromDomainPreparedRun(prepared), nil
|
|
}
|
|
|
|
// Run executes a prompt request and returns the generated artifact and metadata.
|
|
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)
|
|
}
|
|
|
|
result, err := e.runner.Run(ctx, toDomainRunRequest(req))
|
|
if err != nil {
|
|
return nil, mapPublicError(err)
|
|
}
|
|
return fromDomainRunResult(result), nil
|
|
}
|