Implement the initial framework skeleton
This commit is contained in:
71
internal/prompt/go_renderer.go
Normal file
71
internal/prompt/go_renderer.go
Normal file
@@ -0,0 +1,71 @@
|
||||
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")
|
||||
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) {
|
||||
// 1. Verify required inputs
|
||||
for _, req := range profile.ExpectedInputs {
|
||||
if _, ok := inputs[req]; !ok {
|
||||
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 {
|
||||
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).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("execution failed for message %d: %v", i, err)
|
||||
}
|
||||
|
||||
renderedMessages = append(renderedMessages, domain.RenderedMessage{
|
||||
Role: tmplMsg.Role,
|
||||
Content: buf.String(),
|
||||
})
|
||||
}
|
||||
|
||||
return &domain.RenderedPrompt{
|
||||
Messages: renderedMessages,
|
||||
}, nil
|
||||
}
|
||||
11
internal/prompt/renderer.go
Normal file
11
internal/prompt/renderer.go
Normal file
@@ -0,0 +1,11 @@
|
||||
package prompt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"gitea.maximumdirect.net/eric/scriptorium/internal/domain"
|
||||
)
|
||||
|
||||
// Renderer renders prompt templates using named artifacts and variables.
|
||||
type Renderer interface {
|
||||
Render(ctx context.Context, profile *domain.PromptProfile, inputs map[string]*domain.Artifact, vars map[string]string) (*domain.RenderedPrompt, error)
|
||||
}
|
||||
90
internal/prompt/renderer_test.go
Normal file
90
internal/prompt/renderer_test.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package prompt
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"gitea.maximumdirect.net/eric/scriptorium/internal/domain"
|
||||
)
|
||||
|
||||
func TestGoRenderer_Render(t *testing.T) {
|
||||
renderer := NewGoRenderer()
|
||||
ctx := context.Background()
|
||||
|
||||
profile := &domain.PromptProfile{
|
||||
ID: "test-profile",
|
||||
ExpectedInputs: []string{"transcript"},
|
||||
Templates: []domain.PromptMessageTemplate{
|
||||
{Role: "system", Content: "You are a {{.role}}."},
|
||||
{Role: "user", Content: "Analyze this: {{input \"transcript\"}}"},
|
||||
},
|
||||
}
|
||||
|
||||
inputs := map[string]*domain.Artifact{
|
||||
"transcript": {Body: []byte("The quick brown fox.")},
|
||||
}
|
||||
|
||||
vars := map[string]string{
|
||||
"role": "helpful assistant",
|
||||
}
|
||||
|
||||
t.Run("successful render", func(t *testing.T) {
|
||||
res, err := renderer.Render(ctx, profile, inputs, vars)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(res.Messages) != 2 {
|
||||
t.Errorf("expected 2 messages, got %d", len(res.Messages))
|
||||
}
|
||||
if res.Messages[0].Content != "You are a helpful assistant." {
|
||||
t.Errorf("unexpected system message: %s", res.Messages[0].Content)
|
||||
}
|
||||
if res.Messages[1].Content != "Analyze this: The quick brown fox." {
|
||||
t.Errorf("unexpected user message: %s", res.Messages[1].Content)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("missing required input", func(t *testing.T) {
|
||||
emptyInputs := map[string]*domain.Artifact{}
|
||||
_, err := renderer.Render(ctx, profile, emptyInputs, vars)
|
||||
if err == nil || (err != ErrMissingRequiredInput && err.Error() != "missing required input artifact: transcript") {
|
||||
t.Errorf("expected ErrMissingRequiredInput, got %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unknown input in template", func(t *testing.T) {
|
||||
profileUnknown := &domain.PromptProfile{
|
||||
Templates: []domain.PromptMessageTemplate{
|
||||
{Role: "user", Content: "Hello {{input \"ghost\"}}"},
|
||||
},
|
||||
}
|
||||
_, err := renderer.Render(ctx, profileUnknown, inputs, vars)
|
||||
if err == nil {
|
||||
t.Error("expected error for unknown input")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid template syntax", func(t *testing.T) {
|
||||
profileInvalid := &domain.PromptProfile{
|
||||
Templates: []domain.PromptMessageTemplate{
|
||||
{Role: "user", Content: "Hello {{.unclosed"},
|
||||
},
|
||||
}
|
||||
_, err := renderer.Render(ctx, profileInvalid, inputs, vars)
|
||||
if err == nil {
|
||||
t.Error("expected error for invalid template syntax")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty message role", func(t *testing.T) {
|
||||
profileNoRole := &domain.PromptProfile{
|
||||
Templates: []domain.PromptMessageTemplate{
|
||||
{Role: "", Content: "Hello"},
|
||||
},
|
||||
}
|
||||
_, err := renderer.Render(ctx, profileNoRole, inputs, vars)
|
||||
if err == nil {
|
||||
t.Error("expected error for empty message role")
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user