package prompt import ( "bytes" "context" "errors" "fmt" "gitea.maximumdirect.net/eric/scriptorium/internal/domain" "text/template" ) var ( ErrMissingRequiredInput = errors.New("missing required input artifact") ErrUnknownInput = errors.New("referenced unknown input artifact") ErrInvalidTemplate = errors.New("invalid prompt template") ErrRenderFailure = errors.New("prompt render failure") ErrInvalidMessageRole = errors.New("invalid or empty message role") ) type goRenderer struct{} func NewGoRenderer() Renderer { return &goRenderer{} } func (r *goRenderer) Render(ctx context.Context, profile *domain.PromptProfile, inputs map[string]*domain.Artifact, vars map[string]string) (*domain.RenderedPrompt, error) { if profile == nil { return nil, fmt.Errorf("%w: nil profile", ErrRenderFailure) } // 1. Verify required inputs for _, req := range profile.ExpectedInputs { art, ok := inputs[req] if !ok || art == nil { return nil, fmt.Errorf("%w: %s", ErrMissingRequiredInput, req) } } // 2. Setup template functions funcs := template.FuncMap{ "input": func(name string) (string, error) { art, ok := inputs[name] if !ok { return "", fmt.Errorf("%w: %s", ErrUnknownInput, name) } return string(art.Body), nil }, } var renderedMessages []domain.RenderedMessage for i, tmplMsg := range profile.Templates { select { case <-ctx.Done(): return nil, ctx.Err() default: } if tmplMsg.Role == "" { return nil, fmt.Errorf("%w: message %d", ErrInvalidMessageRole, i) } // Parse and execute template tmpl, err := template.New(fmt.Sprintf("msg_%d", i)).Funcs(funcs).Option("missingkey=error").Parse(tmplMsg.Content) if err != nil { return nil, fmt.Errorf("%w: message %d: %v", ErrInvalidTemplate, i, err) } var buf bytes.Buffer if err := tmpl.Execute(&buf, vars); err != nil { return nil, fmt.Errorf("%w: message %d: %w", ErrRenderFailure, i, err) } renderedMessages = append(renderedMessages, domain.RenderedMessage{ Role: tmplMsg.Role, Content: buf.String(), }) } return &domain.RenderedPrompt{ Messages: renderedMessages, }, nil }