Repository: tmc/langchaingo
Pull request #34 (2023-04-20 13:24:55)

prompts: introduced PromptTemplate along with the relevant types
Implement partially #31 

Implemented:

 - `PromptTemplater` interface, is a equivalent
 - `PromptTemplate` along with the member methods, one of the implementation of `PromptTemplater`
 - constructor of `PromptTemplate` from options, examples
 - `StringPromptValue` implementation, one of the implementation to `schema.PromptValue`
 - `ExampleSelector` interface

References:

 - https://github.com/hwchase17/langchainjs/blob/main/langchain/src/prompts/index.ts
 - https://github.com/hwchase17/langchainjs/blob/main/langchain/src/prompts/prompt.ts
 - https://github.com/hwchase17/langchainjs/blob/main/langchain/src/prompts/base.ts
 - https://github.com/hwchase17/langchainjs/blob/main/langchain/src/prompts/template.ts
 - https://github.com/hwchase17/langchain/blob/master/langchain/prompts/prompt.py
 - https://github.com/hwchase17/langchain/blob/master/langchain/prompts/base.py

### PR Checklist

- [x] Read the [Contributing documentation](https://github.com/tmc/langchaingo/blob/main/CONTRIBUTING.md).
- [x] Read the [Code of conduct documentation](https://github.com/tmc/langchaingo/blob/main/CODE_OF_CONDUCT.md).
- [x] Name your Pull Request title clearly, concisely, and prefixed with the name of primarily affected package you changed according to [Good commit messages](https://go.dev/doc/contribute#commit_messages) (such as `memory: add interfaces for X, Y` or `util: add whizzbang helpers`).
- [x] Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate.
- [x] Provide a description in this PR that addresses **what** the PR is solving, or reference the issue that it solves (e.g. `fixes #123`).
- [x] Describes source of new concepts.
- [x] References existing implementations as appropriate.
- [x] Contains test coverage for new functions.
- [x] Passes all [`golangci-lint`](https://golangci-lint.run/) checks.


Patch:
diff --git a/.gitignore b/.gitignore
index 42e9f7c3..bac05c87 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,8 @@
 go.work
 go.work.sum
+
+# Test outputs
+coverage.out
+
+# macOS Specific
+.DS_Store
diff --git a/go.mod b/go.mod
index 02f72610..bbd5468f 100644
--- a/go.mod
+++ b/go.mod
@@ -4,6 +4,7 @@ go 1.20
 
 require (
 	github.com/google/uuid v1.3.0
+	github.com/samber/lo v1.38.1
 	github.com/stretchr/testify v1.8.2
 )
 
@@ -12,3 +13,5 @@ require (
 	github.com/pmezard/go-difflib v1.0.0 // indirect
 	gopkg.in/yaml.v3 v3.0.1 // indirect
 )
+
+require golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
diff --git a/go.sum b/go.sum
index 4fe79919..f93c1886 100644
--- a/go.sum
+++ b/go.sum
@@ -5,6 +5,8 @@ github.com/google/uuid v1.3.0 h1:t6JiXgmwXMjEs8VusXIJk2BXHsn+wx8BZdTaoZ5fu7I=
 github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
 github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
 github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
+github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
+github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
 github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
 github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
 github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -12,6 +14,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
 github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
 github.com/stretchr/testify v1.8.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
 github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
+golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 h1:3MTrJm4PyNL9NBqvYDSj3DHl46qQakyfqfWo4jgfaEM=
+golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
 gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
 gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/prompts/doc.go b/prompts/doc.go
new file mode 100644
index 00000000..db4a8a09
--- /dev/null
+++ b/prompts/doc.go
@@ -0,0 +1,3 @@
+// Package prompts contains types, prompt templates, loading utilities, output parsers,
+// example selectors, and other utilities for working with LLM prompts..
+package prompts
diff --git a/prompts/example_selector.go b/prompts/example_selector.go
new file mode 100644
index 00000000..a354d22a
--- /dev/null
+++ b/prompts/example_selector.go
@@ -0,0 +1,8 @@
+package prompts
+
+// ExampleSelector is an interface for example selectors. It is equivalent to
+// BaseExampleSelector in langchain and langchainjs.
+type ExampleSelector interface {
+	AddExample(example map[string]string) string
+	SelectExamples(inputVariables map[string]string) []map[string]string
+}
diff --git a/prompts/prompt.go b/prompts/prompt.go
new file mode 100644
index 00000000..0e00c27b
--- /dev/null
+++ b/prompts/prompt.go
@@ -0,0 +1,27 @@
+package prompts
+
+import (
+	"github.com/tmc/langchaingo/schema"
+)
+
+var _ schema.PromptValue = StringPromptValue{}
+
+// StringPromptValue is a prompt value that is a string.
+type StringPromptValue struct {
+	value string
+}
+
+func (v StringPromptValue) String() string {
+	return v.value
+}
+
+func (v StringPromptValue) Messages() []schema.ChatMessage {
+	return []schema.ChatMessage{
+		schema.HumanChatMessage{Text: v.value},
+	}
+}
+
+// NewStringPromptValue creates a new StringPromptValue.
+func NewStringPromptValue(value string) StringPromptValue {
+	return StringPromptValue{value: value}
+}
diff --git a/prompts/prompt_template.go b/prompts/prompt_template.go
new file mode 100644
index 00000000..061fb9f4
--- /dev/null
+++ b/prompts/prompt_template.go
@@ -0,0 +1,318 @@
+package prompts
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+
+	"github.com/samber/lo"
+	"github.com/tmc/langchaingo/schema"
+)
+
+var (
+	ErrInputVariableReserved      = errors.New("cannot have input variable")
+	ErrPartialVariableReserved    = errors.New("cannot have partial variable")
+	ErrInvalidPartialVariableType = errors.New("invalid partial variable type")
+)
+
+// PromptTemplateBaseOptions are the options that are common to all prompt templates.
+type PromptTemplateBaseOptions struct {
+	// A list of variable names the prompt template expects
+	InputVariables []string
+	// How to parse the output of calling an LLM on this formatted prompt
+	OutputParser schema.OutputParser[any]
+	// Partial variables
+	PartialVariables map[string]any
+}
+
+// checkInputVariables validates the input variable names do not include restricted names.
+func checkInputVariables(inputVariables []string) error {
+	for _, variable := range inputVariables {
+		if variable == "stop" {
+			return ErrInputVariableReserved
+		}
+	}
+
+	return nil
+}
+
+// checkPartialVariables validates the partial variable names do not include restricted names.
+func checkPartialVariables(partialValues map[string]any) error {
+	for k := range partialValues {
+		if k == "stop" {
+			return ErrPartialVariableReserved
+		}
+	}
+
+	return nil
+}
+
+// applyPromptTemplateBaseOptions applies the input options to the default options.
+func applyPromptTemplateBaseOptions(input PromptTemplateBaseOptions) (PromptTemplateBaseOptions, error) {
+	opts := PromptTemplateBaseOptions{
+		InputVariables:   make([]string, 0),
+		OutputParser:     nil,
+		PartialVariables: make(map[string]any),
+	}
+	if len(input.InputVariables) != 0 {
+		if err := checkInputVariables(input.InputVariables); err != nil {
+			return opts, err
+		}
+		opts.InputVariables = append(opts.InputVariables, input.InputVariables...)
+	}
+	if input.OutputParser != nil {
+		opts.OutputParser = input.OutputParser
+	}
+	if len(input.PartialVariables) != 0 {
+		if err := checkPartialVariables(input.PartialVariables); err != nil {
+			return opts, err
+		}
+
+		for k, v := range input.PartialVariables {
+			opts.PartialVariables[k] = v
+		}
+	}
+
+	return opts, nil
+}
+
+// TemplateType is the string type key uniquely identifying this class
+// of prompt template.
+type TemplateType string
+
+const (
+	// TemplateTypePrompt is the type for SerializedPromptTemplate.
+	TemplateTypePrompt TemplateType = "prompt"
+	// TemplateTypeFewShot is the type for SerializedFewShotTemplate.
+	TemplateTypeFewShot TemplateType = "few_shot"
+	// TemplateTypeMessage is the type for SerializedMessagePromptTemplate.
+	TemplateTypeMessage TemplateType = "message"
+	// TemplateTypeChatPrompt is the type for SerializedChatPromptTemplate.
+	TemplateTypeChatPrompt TemplateType = "chat_prompt"
+)
+
+// PromptTemplater is an interface for prompt templates. It is also equivalent to
+// BasePromptTemplate in langchain or BasePromptTemplate in langchainjs.
+type PromptTemplater interface {
+	MergePartialAndUserVariables(userVariables map[string]any) (map[string]any, error)
+	// Format formats the prompt given the input values.
+	Format(values map[string]any) (string, error)
+	// FormatPromptValue formats the prompt given the input values and return a formatted prompt value.
+	FormatPromptValue(values map[string]any) (schema.PromptValue, error)
+	// GetPromptType returns the string type key uniquely identifying this class of prompt template.
+	GetPromptType() TemplateType
+}
+
+// formatPromptValue formats the prompt given the input values and return a formatted prompt value.
+func formatPromptValue(promptTemplater PromptTemplater, values map[string]any) (schema.PromptValue, error) {
+	formattedPrompt, err := promptTemplater.Format(values)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewStringPromptValue(formattedPrompt), nil
+}
+
+// mergePartialAndUserVariables merges the partial variables and user variables. This is the common
+// and default implementation for PromptTemplater.
+func mergePartialAndUserVariables(
+	partialVariables map[string]any,
+	userVariables map[string]any,
+) (map[string]any, error) {
+	if partialVariables == nil {
+		partialVariables = make(map[string]any)
+	}
+
+	partialValues := make(map[string]any)
+	for k, v := range partialVariables {
+		switch val := v.(type) {
+		case string:
+			partialValues[k] = val
+		case func() any:
+			partialValues[k] = val()
+		default:
+			return make(map[string]any), fmt.Errorf("%w: %T of %v", ErrInvalidPartialVariableType, val, val)
+		}
+	}
+
+	allKwargs := make(map[string]any, len(partialValues)+len(userVariables))
+	for k, v := range partialValues {
+		allKwargs[k] = v
+	}
+	for k, v := range userVariables {
+		allKwargs[k] = v
+	}
+
+	return allKwargs, nil
+}
+
+// PromptTemplateOptions is the option to create a PromptTemplate. Is is also equivalent to
+// PromptTemplate.
+type PromptTemplateOptions struct {
+	PromptTemplateBaseOptions
+
+	// The propmt template
+	Template string
+	// The format of the prompt template. Options are 'go-template', 'f-string', 'jinja-2'
+	TemplateFormat TemplateFormat
+	// Whether or not to try validating the template on initialization
+	ValidateTemplate bool
+}
+
+var _ PromptTemplater = PromptTemplate{}
+
+// PromptTemplate is the default implementation of PromptTemplater.
+type PromptTemplate struct {
+	opts PromptTemplateOptions
+}
+
+// NewPromptTemplate creates a new PromptTemplate from the given option.
+func NewPromptTemplate(inputOpts PromptTemplateOptions) (*PromptTemplate, error) {
+	baseOpts, err := applyPromptTemplateBaseOptions(inputOpts.PromptTemplateBaseOptions)
+	if err != nil {
+		return nil, err
+	}
+
+	opts := PromptTemplateOptions{
+		PromptTemplateBaseOptions: baseOpts,
+		TemplateFormat:            TemplateFormatGoTemplate,
+		ValidateTemplate:          true,
+	}
+	if inputOpts.Template != "" {
+		opts.Template = inputOpts.Template
+	}
+	if inputOpts.TemplateFormat != "" && inputOpts.TemplateFormat != TemplateFormatGoTemplate {
+		opts.TemplateFormat = inputOpts.TemplateFormat
+	}
+	if !inputOpts.ValidateTemplate {
+		opts.ValidateTemplate = inputOpts.ValidateTemplate
+	}
+
+	p := &PromptTemplate{opts: opts}
+
+	if p.opts.ValidateTemplate {
+		totalInputVariables := make([]string, 0, len(p.opts.InputVariables)+len(p.opts.PartialVariables))
+		totalInputVariables = append(totalInputVariables, p.opts.InputVariables...)
+		totalInputVariables = append(totalInputVariables, lo.Keys(p.opts.PartialVariables)...)
+		err = CheckValidTemplate(p.opts.Template, p.opts.TemplateFormat, totalInputVariables)
+		if err != nil {
+			return nil, err
+		}
+	}
+
+	return p, nil
+}
+
+func (p PromptTemplate) MergePartialAndUserVariables(userVariables map[string]any) (map[string]any, error) {
+	return mergePartialAndUserVariables(p.opts.PartialVariables, userVariables)
+}
+
+func (p PromptTemplate) GetPromptType() TemplateType {
+	return TemplateTypePrompt
+}
+
+func (p PromptTemplate) Format(values map[string]any) (string, error) {
+	allValues, err := p.MergePartialAndUserVariables(values)
+	if err != nil {
+		return "", err
+	}
+
+	return RenderTemplate(p.opts.Template, p.opts.TemplateFormat, allValues)
+}
+
+func (p PromptTemplate) FormatPromptValue(values map[string]any) (schema.PromptValue, error) {
+	formattedPrompt, err := p.Format(values)
+	if err != nil {
+		return nil, err
+	}
+
+	return NewStringPromptValue(formattedPrompt), nil
+}
+
+// PromptTemplateFromExamplesOption is the option to create a PromptTemplate from examples.
+type PromptTemplateFromExamplesOption struct {
+	// List of examples to use in the prompt.
+	Examples []string
+	// String to go after the list of examples. Should generally set up the user's input.
+	Suffix string
+	// A list of variable names the final prompt template will expect
+	InputVariables []string
+	// The separator to use in between examples
+	ExampleSeparator string
+	// String that should go before any examples. Generally includes examples.
+	Prefix string
+}
+
+// NewPromptTemplateFromExamples takes examples in list format with prefix and suffix
+// to create a prompt. It is equivalent to PromptTemplate.from_examples in langchain and
+// PromptTemplate.fromExamples in langchainjs.
+//
+// Intended to be used a way to dynamically create a prompt from examples.
+func NewPromptTemplateFromExamples(inputOpts PromptTemplateFromExamplesOption) (*PromptTemplate, error) {
+	opts := PromptTemplateFromExamplesOption{
+		Examples:         make([]string, 0),
+		InputVariables:   make([]string, 0),
+		ExampleSeparator: "\n\n",
+	}
+	if inputOpts.Examples != nil {
+		opts.Examples = inputOpts.Examples
+	}
+	if inputOpts.Suffix != "" {
+		opts.Suffix = inputOpts.Suffix
+	}
+	if inputOpts.InputVariables != nil {
+		opts.InputVariables = inputOpts.InputVariables
+	}
+	if inputOpts.ExampleSeparator != "" {
+		opts.ExampleSeparator = inputOpts.ExampleSeparator
+	}
+	if inputOpts.Prefix != "" {
+		opts.Prefix = inputOpts.Prefix
+	}
+
+	templateSlice := make([]string, 0, len(inputOpts.Examples)+1+1)
+	templateSlice = append(templateSlice, inputOpts.Prefix)
+	templateSlice = append(templateSlice, inputOpts.Examples...)
+	templateSlice = append(templateSlice, inputOpts.Suffix)
+	template := strings.Join(templateSlice, inputOpts.ExampleSeparator)
+	return NewPromptTemplate(PromptTemplateOptions{
+		PromptTemplateBaseOptions: PromptTemplateBaseOptions{
+			InputVariables: inputOpts.InputVariables,
+		},
+		Template: template,
+	})
+}
+
+// PromptTemplateFromFStringOption is the option to create a PromptTemplate from an f-string template.
+type PromptTemplateFromFStringOption struct {
+	// The f-string template
+	Template string
+	// Whether or not to try validating the template on initialization
+	ValidateTemplate bool
+	// How to parse the output of calling an LLM on this formatted prompt
+	OutputParser schema.OutputParser[any]
+	// Partial variables
+	PartialVariables map[string]any
+}
+
+// NewPromptTemplateFromFStringTemplate loads prompt template from a template f-string. It is equivalent
+// to PromptTemplate.from_template in langchain and PromptTemplate.fromTemplate in langchainjs.
+func NewPromptTemplateFromFStringTemplate(PromptTemplateFromFStringOption) (*PromptTemplate, error) {
+	// TODO: implement, after f-string template is implemented
+	panic("not implemented")
+}
+
+// PromptTemplateFromFileOption is the option to create a PromptTemplate from a file.
+type PromptTemplateFromFileOption struct {
+	// The path to the file containing the template
+	TemplateFile string
+	// A list of variable names the prompt template expects
+	InputVariables []string
+}
+
+// NewPromptTemplateFromFile loads prompt template from a file. It is equivalent to
+// PromptTemplate.from_file in langchain.
+func NewPromptTemplateFromFile(PromptTemplateFromFileOption) (*PromptTemplate, error) {
+	// TODO: implement
+	panic("not implemented")
+}
diff --git a/prompts/templates.go b/prompts/templates.go
new file mode 100644
index 00000000..4de4db4e
--- /dev/null
+++ b/prompts/templates.go
@@ -0,0 +1,89 @@
+package prompts
+
+import (
+	"errors"
+	"fmt"
+	"strings"
+	"text/template"
+
+	"github.com/samber/lo"
+)
+
+// ErrInvalidTemplateFormat is the error when the template format is invalid and
+// not supported.
+var ErrInvalidTemplateFormat = errors.New("invalid template format")
+
+// TemplateFormat is the format of the template, options: go-template, f-string, jinja2.
+type TemplateFormat string
+
+const (
+	// TemplateFormatGoTemplate is the format for Go Template
+	// TemplateFormatGoTemplate.
+	TemplateFormatGoTemplate TemplateFormat = "go-template"
+	// TemplateFormatFString is the format for f-string
+	// TODO: Add support for f-string.
+	TemplateFormatFString TemplateFormat = "f-string"
+	// TemplateFormatJinja2 is the format for jinja2
+	// TODO: Add support for jinja2.
+	TemplateFormatJinja2 TemplateFormat = "jinja2"
+)
+
+// Interpolator is the function that interpolates the given template with the given values.
+type Interpolator func(string, TemplateFormat, map[string]any) (string, error)
+
+// DefaultFormatterMapping is the default mapping of TemplateFormat to Interpolator.
+var DefaultFormatterMapping = map[TemplateFormat]Interpolator{
+	TemplateFormatGoTemplate: interpolateGoTemplate,
+}
+
+// interpolateGoTemplate interpolates the given template with the given values by using
+// text/template.
+func interpolateGoTemplate(tmpl string, _ TemplateFormat, values map[string]any) (string, error) {
+	parsedTmpl, err := template.New("template").Parse(tmpl)
+	if err != nil {
+		return "", err
+	}
+
+	sb := new(strings.Builder)
+	err = parsedTmpl.Execute(sb, values)
+	if err != nil {
+		return "", err
+	}
+
+	return sb.String(), nil
+}
+
+func newInvalidTemplateError(gotTemplateFormat TemplateFormat) error {
+	return fmt.Errorf("%w, got: %s, should be one of %s",
+		ErrInvalidTemplateFormat,
+		gotTemplateFormat,
+		lo.Keys(DefaultFormatterMapping),
+	)
+}
+
+// CheckValidTemplate checks if the template is valid through checking whether the given
+// TemplateFormat is available and whether the template can be rendered.
+func CheckValidTemplate(template string, templateFormat TemplateFormat, inputVariables []string) error {
+	_, ok := DefaultFormatterMapping[templateFormat]
+	if !ok {
+		return newInvalidTemplateError(templateFormat)
+	}
+
+	dummyInputs := make(map[string]any, len(inputVariables))
+	for _, v := range inputVariables {
+		dummyInputs[v] = "foo"
+	}
+
+	_, err := RenderTemplate(template, templateFormat, dummyInputs)
+	return err
+}
+
+// RenderTemplate renders the template with the given values.
+func RenderTemplate(tmpl string, tmplFormat TemplateFormat, values map[string]any) (string, error) {
+	formatter, ok := DefaultFormatterMapping[tmplFormat]
+	if !ok {
+		return "", newInvalidTemplateError(tmplFormat)
+	}
+
+	return formatter(tmpl, tmplFormat, values)
+}
diff --git a/schema/output_parsers.go b/schema/output_parsers.go
new file mode 100644
index 00000000..834fc417
--- /dev/null
+++ b/schema/output_parsers.go
@@ -0,0 +1,10 @@
+package schema
+
+type OutputParser[T any] interface {
+	// Parse parses the output of an LLM call.
+	Parse(text string) (T, error)
+	// GetFormatInstructions returns a string describing the format of the output.
+	GetFormatInstructions() string
+	// Type returns the string type key uniquely identifying this class of parser
+	Type() string
+}


Test patch:
diff --git a/prompts/prompt_template_test.go b/prompts/prompt_template_test.go
new file mode 100644
index 00000000..98d5e74a
--- /dev/null
+++ b/prompts/prompt_template_test.go
@@ -0,0 +1,457 @@
+package prompts
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/tmc/langchaingo/schema"
+)
+
+func TestCheckInputVariables(t *testing.T) {
+	t.Parallel()
+
+	err := checkInputVariables([]string{"stop"})
+	require.Error(t, err)
+	assert.ErrorIs(t, err, ErrInputVariableReserved)
+
+	err = checkInputVariables([]string{"test"})
+	require.NoError(t, err)
+}
+
+func TestCheckPartialVariables(t *testing.T) {
+	t.Parallel()
+
+	err := checkPartialVariables(map[string]interface{}{"stop": "test"})
+	require.Error(t, err)
+	assert.ErrorIs(t, err, ErrPartialVariableReserved)
+
+	err = checkPartialVariables(map[string]interface{}{"test": "test"})
+	require.NoError(t, err)
+}
+
+var _ schema.OutputParser[any] = testEmptyOutputParser{}
+
+type testEmptyOutputParser struct{}
+
+func (t testEmptyOutputParser) Parse(_ string) (any, error) {
+	return nil, nil
+}
+
+func (t testEmptyOutputParser) GetFormatInstructions() string {
+	return ""
+}
+
+func (t testEmptyOutputParser) Type() string {
+	return ""
+}
+
+func TestApplyPromptTemplateBaseOptions(t *testing.T) {
+	t.Parallel()
+
+	_, err := applyPromptTemplateBaseOptions(PromptTemplateBaseOptions{
+		InputVariables: []string{"stop"},
+	})
+	require.Error(t, err)
+	assert.ErrorIs(t, err, ErrInputVariableReserved)
+
+	_, err = applyPromptTemplateBaseOptions(PromptTemplateBaseOptions{
+		PartialVariables: map[string]any{"stop": "test"},
+	})
+	require.Error(t, err)
+	assert.ErrorIs(t, err, ErrPartialVariableReserved)
+
+	opts, err := applyPromptTemplateBaseOptions(PromptTemplateBaseOptions{
+		InputVariables:   []string{"test"},
+		OutputParser:     testEmptyOutputParser{},
+		PartialVariables: map[string]any{"test": "test"},
+	})
+	require.NoError(t, err)
+	assert.Equal(t, PromptTemplateBaseOptions{
+		InputVariables:   []string{"test"},
+		OutputParser:     testEmptyOutputParser{},
+		PartialVariables: map[string]any{"test": "test"},
+	}, opts)
+}
+
+func TestFormatPromptValue(t *testing.T) {
+	t.Parallel()
+
+	t.Run("Error", func(t *testing.T) {
+		t.Parallel()
+
+		pt, err := NewPromptTemplate(PromptTemplateOptions{
+			PromptTemplateBaseOptions: PromptTemplateBaseOptions{
+				InputVariables: []string{"testFormatPromptValue"},
+			},
+			Template:         "Hello, {{ .test }}",
+			ValidateTemplate: true,
+		})
+		require.NoError(t, err)
+
+		pt.opts.Template = "Hello, {{{ .testFormatPromptValue }}"
+		_, err = formatPromptValue(pt, map[string]any{
+			"testFormatPromptValue": "world",
+		})
+		require.Error(t, err)
+	})
+
+	t.Run("Success", func(t *testing.T) {
+		t.Parallel()
+
+		pt, err := NewPromptTemplate(PromptTemplateOptions{
+			PromptTemplateBaseOptions: PromptTemplateBaseOptions{
+				InputVariables: []string{"testFormatPromptValue"},
+			},
+			Template:         "Hello, {{ .testFormatPromptValue }}",
+			ValidateTemplate: true,
+		})
+		require.NoError(t, err)
+
+		spv, err := formatPromptValue(pt, map[string]any{
+			"testFormatPromptValue": "world",
+		})
+		require.NoError(t, err)
+		assert.Equal(t, "Hello, world", spv.String())
+	})
+}
+
+func TestMergePartialAndUserVariables(t *testing.T) {
+	t.Parallel()
+
+	type test struct {
+		name             string
+		partialVariables map[string]any
+		userVariables    map[string]any
+		mergedVariables  map[string]any
+		err              error
+	}
+
+	testCases := []test{
+		{
+			name:             "NilPartialVariables",
+			partialVariables: nil,
+			userVariables: map[string]any{
+				"test": "world",
+			},
+			mergedVariables: map[string]any{
+				"test": "world",
+			},
+		},
+		{
+			name: "Success",
+			partialVariables: map[string]interface{}{
+				"test":  "test",
+				"test2": "test2",
+				"test3": func() any {
+					return "test3"
+				},
+			},
+			userVariables: map[string]any{
+				"test": "world",
+			},
+			mergedVariables: map[string]any{
+				"test":  "world",
+				"test2": "test2",
+				"test3": "test3",
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			mergedVariables, err := mergePartialAndUserVariables(tc.partialVariables, tc.userVariables)
+			require.NoError(t, err)
+			assert.Equal(t, tc.mergedVariables, mergedVariables)
+		})
+	}
+
+	errTestCases := []test{
+		{
+			name: "InvalidPartialVariableType",
+			partialVariables: map[string]interface{}{
+				"test": 1234,
+			},
+			userVariables: map[string]any{
+				"test": "world",
+			},
+			mergedVariables: make(map[string]any),
+			err:             ErrInvalidPartialVariableType,
+		},
+	}
+
+	for _, tc := range errTestCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			mergedVariables, err := mergePartialAndUserVariables(tc.partialVariables, tc.userVariables)
+			require.Error(t, err)
+			assert.Empty(t, mergedVariables)
+			assert.ErrorIs(t, err, tc.err)
+		})
+	}
+}
+
+func TestNewPromptTemplate(t *testing.T) {
+	t.Parallel()
+
+	_, err := NewPromptTemplate(PromptTemplateOptions{
+		PromptTemplateBaseOptions: PromptTemplateBaseOptions{
+			InputVariables: []string{"stop"},
+		},
+	})
+	require.Error(t, err)
+	assert.ErrorIs(t, err, ErrInputVariableReserved)
+
+	_, err = NewPromptTemplate(PromptTemplateOptions{
+		Template:         "Hello, {{ .test }}",
+		TemplateFormat:   "unknown",
+		ValidateTemplate: true,
+	})
+	require.Error(t, err)
+	assert.ErrorIs(t, err, ErrInvalidTemplateFormat)
+
+	_, err = NewPromptTemplate(PromptTemplateOptions{
+		Template:         "Hello, {{ .test }}",
+		ValidateTemplate: false,
+	})
+	require.NoError(t, err)
+}
+
+func TestPromptTemplateMergePartialAndUserVariables(t *testing.T) {
+	t.Parallel()
+
+	type test struct {
+		name             string
+		partialVariables map[string]any
+		userVariables    map[string]any
+		mergedVariables  map[string]any
+		err              error
+	}
+
+	testCases := []test{
+		{
+			name:             "NilPartialVariables",
+			partialVariables: nil,
+			userVariables:    map[string]any{"test": "world"},
+			mergedVariables:  map[string]any{"test": "world"},
+		},
+		{
+			name: "Success",
+			partialVariables: map[string]interface{}{
+				"test":  "test",
+				"test2": "test2",
+				"test3": func() any {
+					return "test3"
+				},
+			},
+			userVariables: map[string]any{"test": "world"},
+			mergedVariables: map[string]any{
+				"test":  "world",
+				"test2": "test2",
+				"test3": "test3",
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			pt, err := NewPromptTemplate(PromptTemplateOptions{
+				PromptTemplateBaseOptions: PromptTemplateBaseOptions{
+					PartialVariables: tc.partialVariables,
+				},
+			})
+			require.NoError(t, err)
+			require.NotNil(t, pt)
+
+			mergedVariables, err := pt.MergePartialAndUserVariables(tc.userVariables)
+			require.NoError(t, err)
+			assert.Equal(t, tc.mergedVariables, mergedVariables)
+		})
+	}
+
+	errTestCases := []test{
+		{
+			name:             "InvalidPartialVariableType",
+			partialVariables: map[string]interface{}{"test": 1234},
+			userVariables:    map[string]any{"test": "world"},
+			mergedVariables:  make(map[string]any),
+			err:              ErrInvalidPartialVariableType,
+		},
+	}
+
+	for _, tc := range errTestCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			pt, err := NewPromptTemplate(PromptTemplateOptions{
+				PromptTemplateBaseOptions: PromptTemplateBaseOptions{
+					PartialVariables: tc.partialVariables,
+				},
+			})
+			require.NoError(t, err)
+			require.NotNil(t, pt)
+
+			mergedVariables, err := pt.MergePartialAndUserVariables(tc.userVariables)
+			require.Error(t, err)
+			assert.Empty(t, mergedVariables)
+			assert.ErrorIs(t, err, tc.err)
+		})
+	}
+}
+
+func TestPromptTemplateGetPromptType(t *testing.T) {
+	t.Parallel()
+
+	pt, err := NewPromptTemplate(PromptTemplateOptions{})
+	require.NoError(t, err)
+	assert.Equal(t, TemplateTypePrompt, pt.GetPromptType())
+}
+
+func TestPromptTemplateFormat(t *testing.T) {
+	t.Parallel()
+
+	t.Run("Error", func(t *testing.T) {
+		t.Parallel()
+
+		pt, err := NewPromptTemplate(PromptTemplateOptions{
+			PromptTemplateBaseOptions: PromptTemplateBaseOptions{
+				InputVariables: []string{"test"},
+				PartialVariables: map[string]any{
+					"test": 1234,
+				},
+			},
+			Template:         "Hello, {{ .test }}",
+			ValidateTemplate: true,
+		})
+		if err != nil {
+			t.Fatalf("Expected nil, got %v", err)
+		}
+
+		pt.opts.Template = "Hello, {{{ .test }}"
+		_, err = pt.Format(map[string]any{
+			"test": "world",
+		})
+		if err == nil {
+			t.Fatal("Expected error, got nil")
+		}
+	})
+
+	t.Run("Success", func(t *testing.T) {
+		t.Parallel()
+
+		pt, err := NewPromptTemplate(PromptTemplateOptions{
+			PromptTemplateBaseOptions: PromptTemplateBaseOptions{
+				InputVariables: []string{"test"},
+			},
+			Template:         "Hello, {{ .test }}",
+			ValidateTemplate: true,
+		})
+		if err != nil {
+			t.Fatalf("Expected nil, got %v", err)
+		}
+
+		str, err := pt.Format(map[string]any{
+			"test": "world",
+		})
+		if err != nil {
+			t.Fatalf("Expected nil, got %v", err)
+		}
+		if str != "Hello, world" {
+			t.Errorf("Expected 'Hello, world', got %s", str)
+		}
+	})
+}
+
+func TestPromptTemplateFormatPromptValue(t *testing.T) {
+	t.Parallel()
+
+	t.Run("Error", func(t *testing.T) {
+		t.Parallel()
+
+		pt, err := NewPromptTemplate(PromptTemplateOptions{
+			PromptTemplateBaseOptions: PromptTemplateBaseOptions{
+				InputVariables: []string{"test"},
+				PartialVariables: map[string]any{
+					"test": 1234,
+				},
+			},
+			Template:         "Hello, {{ .test }}",
+			ValidateTemplate: true,
+		})
+		if err != nil {
+			t.Fatalf("Expected nil, got %v", err)
+		}
+
+		pt.opts.Template = "Hello, {{{ .test }}"
+		_, err = pt.FormatPromptValue(map[string]any{
+			"test": "world",
+		})
+		if err == nil {
+			t.Fatal("Expected error, got nil")
+		}
+	})
+
+	t.Run("Success", func(t *testing.T) {
+		t.Parallel()
+
+		pt, err := NewPromptTemplate(PromptTemplateOptions{
+			PromptTemplateBaseOptions: PromptTemplateBaseOptions{
+				InputVariables: []string{"test"},
+			},
+			Template:         "Hello, {{ .test }}",
+			ValidateTemplate: true,
+		})
+		if err != nil {
+			t.Fatalf("Expected nil, got %v", err)
+		}
+
+		str, err := pt.FormatPromptValue(map[string]any{
+			"test": "world",
+		})
+		if err != nil {
+			t.Fatalf("Expected nil, got %v", err)
+		}
+		if str.String() != "Hello, world" {
+			t.Errorf("Expected 'Hello, world', got %s", str.String())
+		}
+	})
+}
+
+func TestNewPromptTemplateFromExamples(t *testing.T) {
+	t.Parallel()
+
+	pt, err := NewPromptTemplateFromExamples(PromptTemplateFromExamplesOption{
+		Examples:         []string{"- example 1", "- example 2"},
+		Suffix:           "Thank you!",
+		InputVariables:   []string{"test"},
+		ExampleSeparator: "\n\n",
+		Prefix:           "Hello, {{ .test }}, output as:",
+	})
+	require.NoError(t, err)
+	assert.Equal(t, pt.opts.Template, "Hello, {{ .test }}, output as:\n\n- example 1\n\n- example 2\n\nThank you!")
+}
+
+func TestNewPromptTemplateFromFStringTemplate(t *testing.T) {
+	t.Parallel()
+
+	require.PanicsWithValue(t, "not implemented", func() {
+		_, _ = NewPromptTemplateFromFStringTemplate(PromptTemplateFromFStringOption{})
+	})
+}
+
+func TestNewPromptTemplateFromFile(t *testing.T) {
+	t.Parallel()
+
+	require.PanicsWithValue(t, "not implemented", func() {
+		_, _ = NewPromptTemplateFromFile(PromptTemplateFromFileOption{})
+	})
+}
diff --git a/prompts/prompt_test.go b/prompts/prompt_test.go
new file mode 100644
index 00000000..7ef99b91
--- /dev/null
+++ b/prompts/prompt_test.go
@@ -0,0 +1,33 @@
+package prompts
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestStringPromptValueString(t *testing.T) {
+	t.Parallel()
+
+	spv := NewStringPromptValue("")
+	str := spv.String()
+	assert.Empty(t, str)
+
+	spv = NewStringPromptValue("test")
+	str = spv.String()
+	assert.Equal(t, "test", str)
+}
+
+func TestStringPromptValueMessages(t *testing.T) {
+	t.Parallel()
+
+	spv := NewStringPromptValue("")
+	msgs := spv.Messages()
+	require.Len(t, msgs, 1)
+
+	spv = NewStringPromptValue("test")
+	msgs = spv.Messages()
+	require.Len(t, msgs, 1)
+	assert.Equal(t, "test", msgs[0].GetText())
+}
diff --git a/prompts/templates_test.go b/prompts/templates_test.go
new file mode 100644
index 00000000..784a1bfc
--- /dev/null
+++ b/prompts/templates_test.go
@@ -0,0 +1,147 @@
+package prompts
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+)
+
+func TestInterpolateGoTemplate(t *testing.T) {
+	t.Parallel()
+
+	type tests struct {
+		name           string
+		template       string
+		templateValues map[string]any
+		expected       string
+		errValue       string
+	}
+
+	testCases := []tests{
+		{
+			name:     "Single",
+			template: "Hello {{ .key }}",
+			templateValues: map[string]any{
+				"key": "world",
+			},
+			expected: "Hello world",
+		},
+		{
+			name:     "Multiple",
+			template: "Hello {{ .key1 }} and {{ .key2 }}",
+			templateValues: map[string]any{
+				"key1": "world",
+				"key2": "universe",
+			},
+			expected: "Hello world and universe",
+		},
+		{
+			name:     "Nested",
+			template: "Hello {{ .key1.key2 }}",
+			templateValues: map[string]any{
+				"key1": map[string]any{
+					"key2": "world",
+				},
+			},
+			expected: "Hello world",
+		},
+	}
+
+	for _, tc := range testCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			actual, err := interpolateGoTemplate(tc.template, TemplateFormatGoTemplate, tc.templateValues)
+			require.NoError(t, err)
+			assert.Equal(t, tc.expected, actual)
+		})
+	}
+
+	errTestCases := []tests{
+		{
+			name:     "ParseErrored",
+			template: "Hello {{{ .key1 }}",
+			expected: "",
+			errValue: "template: template:1: unexpected \"{\" in command",
+		},
+		{
+			name:     "ExecuteErrored",
+			template: "Hello {{ .key1 .key2 }}",
+			expected: "",
+			errValue: "template: template:1:9: executing \"template\" at <.key1>: key1 is not a method but has arguments",
+		},
+	}
+
+	for _, tc := range errTestCases {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+
+			_, err := interpolateGoTemplate(tc.template, TemplateFormatGoTemplate, map[string]any{})
+			require.Error(t, err)
+			assert.EqualError(t, err, tc.errValue)
+		})
+	}
+}
+
+func TestCheckValidTemplate(t *testing.T) {
+	t.Parallel()
+
+	t.Run("NoTemplateAvailable", func(t *testing.T) {
+		t.Parallel()
+
+		err := CheckValidTemplate("Hello, {test}", "unknown", []string{"test"})
+		require.Error(t, err)
+		assert.ErrorIs(t, err, ErrInvalidTemplateFormat)
+		assert.EqualError(t, err, "invalid template format, got: unknown, should be one of [go-template]")
+	})
+
+	t.Run("TemplateErrored", func(t *testing.T) {
+		t.Parallel()
+
+		err := CheckValidTemplate("Hello, {{{ test }}", TemplateFormatGoTemplate, []string{"test"})
+		require.Error(t, err)
+		assert.EqualError(t, err, "template: template:1: unexpected \"{\" in command")
+	})
+
+	t.Run("TemplateValid", func(t *testing.T) {
+		t.Parallel()
+
+		err := CheckValidTemplate("Hello, {{ .test }}", TemplateFormatGoTemplate, []string{"test"})
+		require.NoError(t, err)
+	})
+}
+
+func TestRenderTemplate(t *testing.T) {
+	t.Parallel()
+
+	t.Run("TemplateAvailable", func(t *testing.T) {
+		t.Parallel()
+
+		actual, err := RenderTemplate(
+			"Hello {{ .key }}",
+			TemplateFormatGoTemplate,
+			map[string]any{
+				"key": "world",
+			},
+		)
+		require.NoError(t, err)
+		assert.Equal(t, "Hello world", actual)
+	})
+
+	t.Run("TemplateNotAvailable", func(t *testing.T) {
+		t.Parallel()
+
+		_, err := RenderTemplate(
+			"Hello {key}",
+			"unknown",
+			map[string]any{
+				"key": "world",
+			},
+		)
+		require.Error(t, err)
+		assert.ErrorIs(t, err, ErrInvalidTemplateFormat)
+	})
+}