Repository: tmc/langchaingo (go)

Issue:
[Feature Request] Introducing MessagePromptTemplate
First of all, please correct me if I am mistaken about your roadmap plan.

### Your proposal is related to a problem

Currently, I couldn't find a proper way to invoke `schema.ChatMessage` with the package `prompts`. The only related one I found is `prompts.StringPromptValue` which Format the message to `schema.HumanChatMessage`.

The original Langchain provides different types of MessagePromptTemplate such as `AIMessagePromptTemplate`, `HumanMessagePromptTemplate`, and `SystemMessagePromptTemplate`.

FYI: https://langchain-langchain.vercel.app/docs/modules/model_io/prompts/prompt_templates/msg_prompt_templates

This is more convenient instead of using schema.ChatMessage directly.

### Describe the solution you'd like

So let's introduce the same thing into this lib.

To declare an interface BaseMessagePromptTemplate inside `prompts`:
```go
type BaseMessagePromptTemplate interface {
	IntputVariables() []string
	FormatPrompt(values map[string]interface{}) (schema.PromptValue, error)
	FormatMessages(values map[string]interface{}) ([]schema.ChatMessage, error)
}
```
Then we implement `AIMessagePromptTemplate`, `HumanMessagePromptTemplate`, and `SystemMessagePromptTemplate` based on `prompts.PromptTemplate` for the interface. That tree prompt templates should produce their respective `schema.PromptValue` (`AIPromptValue`, `HumanPromptValue`, and `SystemPromptValue`) so that can be easily used by Chat Models in such way:
```go
func TestChatWithTemplate(t *testing.T) {
	template := prompts.NewChatPromptTemplate([]prompts.BaseMessagePromptTemplate{
		chat.NewSystemMessagePromptTemplate(
			`You are a translation engine that can only translate text and cannot interpret it.`,
			nil,
		),
		chat.NewHumanMessagePromptTemplate(
			`translate this text from {{.input_lang}} to {{.output_lang}}:\n{{.input}}`,
			[]string{"input_lang", "output_lang", "input"},
		),
	})
	value, err := template.FormatPrompt(map[string]interface{}{
		"input_lang":  "English",
		"output_lang": "Chinese",
		"input":       INPUT,
	})
	if err != nil {
		t.Fatalf("%s", err)
	}
	completion, err := llm.Chat(context.Background(), value.Messages())
	if err != nil {
		t.Fatalf("%s", err)
	}
	fmt.Println(completion.Message.GetText())
}
```

### Additional context

I would like to create a **PR** and make a contribution to this feature request if you let me know.:)


Pull request description:
prompts: add chat prompt template
This adds chat prompt templates and prompt templates for each of the message types. All message type prompt templates implement the new message formatter interface. GetInputVariables is added to the format prompter because the llm chain needs to know what input values the prompt needs. The checks for "stop" is removed because stop values are now given in the call options. A prompt value for chat messages is added.

Fixes #134. 

### 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.
- [x]  I am an existing contributor.


Patch:
diff --git a/prompts/chat_prompt.go b/prompts/chat_prompt.go
new file mode 100644
index 00000000..d00118ff
--- /dev/null
+++ b/prompts/chat_prompt.go
@@ -0,0 +1,26 @@
+package prompts
+
+import (
+	"fmt"
+
+	"github.com/tmc/langchaingo/schema"
+)
+
+var _ schema.PromptValue = ChatPromptValue{}
+
+// ChatPromptValue is a prompt value that is a list of chat messages.
+type ChatPromptValue []schema.ChatMessage
+
+// String returns the chat message slice as a buffer string.
+func (v ChatPromptValue) String() string {
+	s, err := schema.GetBufferString(v, "Human", "AI")
+	if err == nil {
+		return s
+	}
+	return fmt.Sprintf("%v", []schema.ChatMessage(v))
+}
+
+// Messages returns the ChatMessage slice.
+func (v ChatPromptValue) Messages() []schema.ChatMessage {
+	return []schema.ChatMessage(v)
+}
diff --git a/prompts/chat_prompt_template.go b/prompts/chat_prompt_template.go
new file mode 100644
index 00000000..58efe31e
--- /dev/null
+++ b/prompts/chat_prompt_template.go
@@ -0,0 +1,74 @@
+package prompts
+
+import "github.com/tmc/langchaingo/schema"
+
+// ChatPromptTemplate is a prompt template for chat messages.
+type ChatPromptTemplate struct {
+	// Messages is the list of the messages to be formatted.
+	Messages []MessageFormatter
+
+	// PartialVariables represents a map of variable names to values or functions that return values.
+	// If the value is a function, it will be called when the prompt template is rendered.
+	PartialVariables map[string]any
+}
+
+var (
+	_ Formatter        = ChatPromptTemplate{}
+	_ MessageFormatter = ChatPromptTemplate{}
+	_ FormatPrompter   = ChatPromptTemplate{}
+)
+
+// FormatPrompt formats the messages into a chat prompt value.
+func (p ChatPromptTemplate) FormatPrompt(values map[string]any) (schema.PromptValue, error) { //nolint:ireturn
+	resolvedValues, err := resolvePartialValues(p.PartialVariables, values)
+	if err != nil {
+		return nil, err
+	}
+
+	formattedMessages := make([]schema.ChatMessage, 0, len(p.Messages))
+	for _, m := range p.Messages {
+		curFormattedMessages, err := m.FormatMessages(resolvedValues)
+		if err != nil {
+			return nil, err
+		}
+
+		formattedMessages = append(formattedMessages, curFormattedMessages...)
+	}
+
+	return ChatPromptValue(formattedMessages), nil
+}
+
+// Format formats the messages with values given and returns the messages as a string.
+func (p ChatPromptTemplate) Format(values map[string]any) (string, error) {
+	promptValue, err := p.FormatPrompt(values)
+	return promptValue.String(), err
+}
+
+// FormatMessages formats the messages with the values and returns the formatted messages.
+func (p ChatPromptTemplate) FormatMessages(values map[string]any) ([]schema.ChatMessage, error) {
+	promptValue, err := p.FormatPrompt(values)
+	return promptValue.Messages(), err
+}
+
+// GetInputVariables returns the input variables the prompt expect.
+func (p ChatPromptTemplate) GetInputVariables() []string {
+	inputVariablesMap := make(map[string]bool, 0)
+	for _, msg := range p.Messages {
+		for _, variable := range msg.GetInputVariables() {
+			inputVariablesMap[variable] = true
+		}
+	}
+
+	inputVariables := make([]string, 0, len(inputVariablesMap))
+	for variable := range inputVariablesMap {
+		inputVariables = append(inputVariables, variable)
+	}
+	return inputVariables
+}
+
+// NewChatPromptTemplate creates a new chat prompt template from a list of message formatters.
+func NewChatPromptTemplate(messages []MessageFormatter) ChatPromptTemplate {
+	return ChatPromptTemplate{
+		Messages: messages,
+	}
+}
diff --git a/prompts/message_promt_templage.go b/prompts/message_promt_templage.go
new file mode 100644
index 00000000..9d550033
--- /dev/null
+++ b/prompts/message_promt_templage.go
@@ -0,0 +1,105 @@
+package prompts
+
+import "github.com/tmc/langchaingo/schema"
+
+// SystemMessagePromptTemplate is a message formatter that returns a system message.
+type SystemMessagePromptTemplate struct {
+	Prompt PromptTemplate
+}
+
+var _ MessageFormatter = SystemMessagePromptTemplate{}
+
+// FormatMessages formats the message with the values given.
+func (p SystemMessagePromptTemplate) FormatMessages(values map[string]any) ([]schema.ChatMessage, error) {
+	text, err := p.Prompt.Format(values)
+	return []schema.ChatMessage{schema.SystemChatMessage{Text: text}}, err
+}
+
+// GetInputVariables returns the input variables the prompt expects.
+func (p SystemMessagePromptTemplate) GetInputVariables() []string {
+	return p.Prompt.InputVariables
+}
+
+// NewSystemMessagePromptTemplate creates a new system message prompt template.
+func NewSystemMessagePromptTemplate(template string, inputVariables []string) SystemMessagePromptTemplate {
+	return SystemMessagePromptTemplate{
+		Prompt: NewPromptTemplate(template, inputVariables),
+	}
+}
+
+// AIMessagePromptTemplate is a message formatter that returns a AI message.
+type AIMessagePromptTemplate struct {
+	Prompt PromptTemplate
+}
+
+var _ MessageFormatter = AIMessagePromptTemplate{}
+
+// FormatMessages formats the message with the values given.
+func (p AIMessagePromptTemplate) FormatMessages(values map[string]any) ([]schema.ChatMessage, error) {
+	text, err := p.Prompt.Format(values)
+	return []schema.ChatMessage{schema.AIChatMessage{Text: text}}, err
+}
+
+// GetInputVariables returns the input variables the prompt expects.
+func (p AIMessagePromptTemplate) GetInputVariables() []string {
+	return p.Prompt.InputVariables
+}
+
+// NewAIMessagePromptTemplate creates a new AI message prompt template.
+func NewAIMessagePromptTemplate(template string, inputVariables []string) AIMessagePromptTemplate {
+	return AIMessagePromptTemplate{
+		Prompt: NewPromptTemplate(template, inputVariables),
+	}
+}
+
+// HumanMessagePromptTemplate is a message formatter that returns a human message.
+type HumanMessagePromptTemplate struct {
+	Prompt PromptTemplate
+}
+
+var _ MessageFormatter = HumanMessagePromptTemplate{}
+
+// FormatMessages formats the message with the values given.
+func (p HumanMessagePromptTemplate) FormatMessages(values map[string]any) ([]schema.ChatMessage, error) {
+	text, err := p.Prompt.Format(values)
+	return []schema.ChatMessage{schema.HumanChatMessage{Text: text}}, err
+}
+
+// GetInputVariables returns the input variables the prompt expects.
+func (p HumanMessagePromptTemplate) GetInputVariables() []string {
+	return p.Prompt.InputVariables
+}
+
+// NewHumanMessagePromptTemplate creates a new human message prompt template.
+func NewHumanMessagePromptTemplate(template string, inputVariables []string) HumanMessagePromptTemplate {
+	return HumanMessagePromptTemplate{
+		Prompt: NewPromptTemplate(template, inputVariables),
+	}
+}
+
+// GenericMessagePromptTemplate is a message formatter that returns message with the specified speaker.
+type GenericMessagePromptTemplate struct {
+	Prompt PromptTemplate
+	Role   string
+}
+
+var _ MessageFormatter = GenericMessagePromptTemplate{}
+
+// FormatMessages formats the message with the values given.
+func (p GenericMessagePromptTemplate) FormatMessages(values map[string]any) ([]schema.ChatMessage, error) {
+	text, err := p.Prompt.Format(values)
+	return []schema.ChatMessage{schema.GenericChatMessage{Text: text, Role: p.Role}}, err
+}
+
+// GetInputVariables returns the input variables the prompt expects.
+func (p GenericMessagePromptTemplate) GetInputVariables() []string {
+	return p.Prompt.InputVariables
+}
+
+// NewGenericMessagePromptTemplate creates a new generic message prompt template.
+func NewGenericMessagePromptTemplate(role, template string, inputVariables []string) GenericMessagePromptTemplate {
+	return GenericMessagePromptTemplate{
+		Prompt: NewPromptTemplate(template, inputVariables),
+		Role:   role,
+	}
+}
diff --git a/prompts/prompt_template.go b/prompts/prompt_template.go
index ac0c08d0..f29f9103 100644
--- a/prompts/prompt_template.go
+++ b/prompts/prompt_template.go
@@ -5,7 +5,6 @@ import (
 	"fmt"
 
 	"github.com/tmc/langchaingo/schema"
-	"golang.org/x/exp/maps"
 )
 
 var (
@@ -50,16 +49,11 @@ var (
 
 // Format formats the prompt template and returns a string value.
 func (p PromptTemplate) Format(values map[string]any) (string, error) {
-	if err := checkInputVariables(p.InputVariables); err != nil {
-		return "", err
-	}
-	if err := checkPartialVariables(p.PartialVariables); err != nil {
-		return "", err
-	}
 	resolvedValues, err := resolvePartialValues(p.PartialVariables, values)
 	if err != nil {
 		return "", err
 	}
+
 	return RenderTemplate(p.Template, p.TemplateFormat, resolvedValues)
 }
 
@@ -70,7 +64,12 @@ func (p PromptTemplate) FormatPrompt(values map[string]any) (schema.PromptValue,
 		return nil, err
 	}
 
-	return StringPromptValue(f), nil
+	return StringPromptValue(f), nil //nolint:ireturn
+}
+
+// GetInputVariables returns the input variables the prompt expect.
+func (p PromptTemplate) GetInputVariables() []string {
+	return p.InputVariables
 }
 
 func resolvePartialValues(partialValues map[string]any, values map[string]any) (map[string]any, error) {
@@ -90,18 +89,3 @@ func resolvePartialValues(partialValues map[string]any, values map[string]any) (
 	}
 	return resolvedValues, nil
 }
-
-// checkInputVariables validates the input variable names do not include restricted names.
-func checkInputVariables(inputVariables []string) error {
-	for _, variable := range inputVariables {
-		if variable == "stop" {
-			return fmt.Errorf("%w: %v", ErrInputVariableReserved, variable)
-		}
-	}
-	return nil
-}
-
-// checkPartialVariables validates the partial variable names do not include restricted names.
-func checkPartialVariables(partialValues map[string]any) error {
-	return checkInputVariables(maps.Keys(partialValues))
-}
diff --git a/prompts/prompts.go b/prompts/prompts.go
index a7132d8d..7ee65356 100644
--- a/prompts/prompts.go
+++ b/prompts/prompts.go
@@ -7,7 +7,14 @@ type Formatter interface {
 	Format(values map[string]any) (string, error)
 }
 
+// Formatter is an interface for formatting a map of values into a list of messages.
+type MessageFormatter interface {
+	FormatMessages(values map[string]any) ([]schema.ChatMessage, error)
+	GetInputVariables() []string
+}
+
 // FormatPrompter is an interface for formatting a map of values into a prompt.
 type FormatPrompter interface {
 	FormatPrompt(values map[string]any) (schema.PromptValue, error)
+	GetInputVariables() []string
 }
diff --git a/prompts/templates.go b/prompts/templates.go
index 7f86057c..11ea24b3 100644
--- a/prompts/templates.go
+++ b/prompts/templates.go
@@ -34,6 +34,7 @@ var defaultformatterMapping = map[TemplateFormat]interpolator{ //nolint:gocheckn
 // text/template.
 func interpolateGoTemplate(tmpl string, values map[string]any) (string, error) {
 	parsedTmpl, err := template.New("template").
+		Option("missingkey=error").
 		Funcs(sprig.FuncMap()).
 		Parse(tmpl)
 	if err != nil {


Test patch:
diff --git a/prompts/chat_prompt_template_test.go b/prompts/chat_prompt_template_test.go
new file mode 100644
index 00000000..d981159c
--- /dev/null
+++ b/prompts/chat_prompt_template_test.go
@@ -0,0 +1,45 @@
+package prompts
+
+import (
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"github.com/tmc/langchaingo/schema"
+)
+
+func TestChatPromptTemplate(t *testing.T) {
+	t.Parallel()
+
+	template := NewChatPromptTemplate([]MessageFormatter{
+		NewSystemMessagePromptTemplate(
+			"You are a translation engine that can only translate text and cannot interpret it.",
+			nil,
+		),
+		NewHumanMessagePromptTemplate(
+			`translate this text from {{.inputLang}} to {{.outputLang}}:\n{{.input}}`,
+			[]string{"inputLang", "outputLang", "input"},
+		),
+	})
+	value, err := template.FormatPrompt(map[string]interface{}{
+		"inputLang":  "English",
+		"outputLang": "Chinese",
+		"input":      "I love programming",
+	})
+	assert.NoError(t, err)
+	expectedMessages := []schema.ChatMessage{
+		schema.SystemChatMessage{
+			Text: "You are a translation engine that can only translate text and cannot interpret it.",
+		},
+		schema.HumanChatMessage{
+			Text: `translate this text from English to Chinese:\nI love programming`,
+		},
+	}
+	require.Equal(t, expectedMessages, value.Messages())
+
+	_, err = template.FormatPrompt(map[string]interface{}{
+		"inputLang":  "English",
+		"outputLang": "Chinese",
+	})
+	assert.Error(t, err)
+}
diff --git a/prompts/prompt_template_test.go b/prompts/prompt_template_test.go
index 32521912..638cb5b6 100644
--- a/prompts/prompt_template_test.go
+++ b/prompts/prompt_template_test.go
@@ -20,7 +20,7 @@ func TestPromptTemplateFormatPrompt(t *testing.T) {
 	}{
 		{"empty", "", nil, nil, nil, "", false},
 		{"ok-input-var", "", []string{"foobar"}, nil, nil, "", false},
-		{"bad-input-var", "", []string{"stop"}, nil, nil, "", true}, // expect a error.
+		{"missing-input-var", "{{.name}}", []string{"job"}, nil, nil, "", true}, // expect a error.
 		{"hello world", "hello world", nil, nil, nil, "hello world", false},
 		{"basic", "hello {{.name}}", nil, nil, map[string]any{
 			"name": "richard",
@@ -39,10 +39,9 @@ func TestPromptTemplateFormatPrompt(t *testing.T) {
 			"hello richard", false,
 		},
 		{
-			"partials w err", "{{.greeting}} {{.name}}", nil,
+			"partials w err", "{{.greeting}} {{.name}} {{.message}}", nil,
 			map[string]any{
 				"name": func() string { return "richard" },
-				"stop": "foobar",
 			},
 			map[string]any{
 				"greeting": "hello",