mirror of
https://github.com/vxcontrol/langchaingo.git
synced 2026-07-21 08:55:25 -04:00
c544fb77bd
* all: add broad httprr coverage, update dependencies, organize go.mod file, bump to 1.23 update go version to 1.23 add lots of test coverage via httprr recordings update dependencies and organize go.mod add testutil/testctr which helps work around a testcontainers-go+colima bug expand the huggingface implementation and tests expand capabilities of the ollama package
227 lines
7.1 KiB
Go
227 lines
7.1 KiB
Go
package chains
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
"github.com/tmc/langchaingo/memory"
|
|
"github.com/tmc/langchaingo/prompts"
|
|
"github.com/tmc/langchaingo/schema"
|
|
)
|
|
|
|
var errDummy = errors.New("boom")
|
|
|
|
func TestSimpleSequential(t *testing.T) {
|
|
ctx := context.Background()
|
|
t.Parallel()
|
|
|
|
// Build and execute a simple sequential chain with two LLMChains
|
|
testLLM1 := &testLanguageModel{expResult: "the chicken crossed the road"}
|
|
testLLM2 := &testLanguageModel{expResult: "The chicken made it to the other side"}
|
|
|
|
chains := []Chain{
|
|
NewLLMChain(testLLM1, prompts.NewPromptTemplate("{{.input}}", []string{"input"})),
|
|
NewLLMChain(testLLM2, prompts.NewPromptTemplate("What happened after {{.output}}?", []string{"output"})),
|
|
}
|
|
simpleSeqChain, err := NewSimpleSequentialChain(chains)
|
|
require.NoError(t, err)
|
|
|
|
res, err := Run(ctx, simpleSeqChain, "What did the chicken do?")
|
|
require.NoError(t, err)
|
|
|
|
// Assert that the second LLMChain received the output of the first LLMChain
|
|
expPrompt := "What happened after the chicken crossed the road?"
|
|
assert.Equal(t, expPrompt, testLLM2.recordedPrompt[0].String())
|
|
|
|
// Assert that the output of the second LLMChain is the output of the entire chain
|
|
assert.Equal(t, "The chicken made it to the other side", res)
|
|
}
|
|
|
|
func TestSimpleSequentialErrors(t *testing.T) {
|
|
ctx := context.Background()
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
chain Chain
|
|
initErr error
|
|
execErr error
|
|
}{
|
|
{
|
|
name: "multiple inputs",
|
|
chain: &testLLMChain{inputKeys: []string{"input1", "input2"}},
|
|
initErr: ErrInvalidInputNumberInSimpleSeq,
|
|
},
|
|
{
|
|
name: "multiple outputs",
|
|
chain: &testLLMChain{inputKeys: []string{"input"}, outputKeys: []string{"output1", "output2"}},
|
|
initErr: ErrInvalidOutputNumberInSimpleSeq,
|
|
},
|
|
{
|
|
name: "chain execution error",
|
|
chain: &testLLMChain{err: errDummy, inputKeys: []string{"input"}, outputKeys: []string{"output"}},
|
|
execErr: errDummy,
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
c, err := NewSimpleSequentialChain([]Chain{tc.chain})
|
|
if tc.initErr != nil {
|
|
require.ErrorIs(t, err, tc.initErr)
|
|
} else {
|
|
require.NoError(t, err)
|
|
_, err := Run(ctx, c, "Do something")
|
|
require.ErrorIs(t, err, tc.execErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSequentialChain(t *testing.T) {
|
|
ctx := context.Background()
|
|
t.Parallel()
|
|
|
|
// Build and execute a sequential chain with three LLMChains
|
|
testLLM1 := &testLanguageModel{expResult: "In the year 3000, chickens have taken over the world"}
|
|
testLLM2 := &testLanguageModel{expResult: "An egg-citing adventure"}
|
|
testLLM3 := &testLanguageModel{expResult: "Vey legit"}
|
|
|
|
chain1 := NewLLMChain(
|
|
testLLM1,
|
|
prompts.NewPromptTemplate("Write a story titled {{.title}} set in the year {{.year}}", []string{"title", "year"}),
|
|
)
|
|
chain1.OutputKey = "story"
|
|
chain2 := NewLLMChain(testLLM2, prompts.NewPromptTemplate("Review this story: {{.story}}", []string{"story"}))
|
|
chain2.OutputKey = "review"
|
|
chain3 := NewLLMChain(
|
|
testLLM3,
|
|
prompts.NewPromptTemplate("Tell me if this review is legit: {{.review}}", []string{"review"}),
|
|
)
|
|
|
|
chains := []Chain{chain1, chain2, chain3}
|
|
|
|
seqChain, err := NewSequentialChain(chains, []string{"title", "year"}, []string{_llmChainDefaultOutputKey})
|
|
require.NoError(t, err)
|
|
|
|
res, err := Call(ctx, seqChain, map[string]any{"title": "Chicken Takeover", "year": 3000})
|
|
require.NoError(t, err)
|
|
|
|
// Assert that the second LLMChain received the output of the first LLMChain
|
|
expPrompt := "Review this story: In the year 3000, chickens have taken over the world"
|
|
assert.Equal(t, expPrompt, testLLM2.recordedPrompt[0].String())
|
|
|
|
// Assert that the third LLMChain received the output of the second LLMChain
|
|
expPrompt = "Tell me if this review is legit: An egg-citing adventure"
|
|
assert.Equal(t, expPrompt, testLLM3.recordedPrompt[0].String())
|
|
|
|
// Assert that the output of the third LLMChain is the output of the entire chain
|
|
assert.Equal(t, "Vey legit", res[_llmChainDefaultOutputKey])
|
|
}
|
|
|
|
func TestSequentialChainErrors(t *testing.T) {
|
|
ctx := context.Background()
|
|
t.Parallel()
|
|
|
|
testCases := []struct {
|
|
name string
|
|
chains []Chain
|
|
initErr error
|
|
execErr error
|
|
seqChainOpts []SequentialChainOption
|
|
}{
|
|
{
|
|
name: "missing input key",
|
|
chains: []Chain{
|
|
&testLLMChain{inputKeys: []string{"input1", "input2"}, outputKeys: []string{"output"}},
|
|
// 2nd chain's input key does not exist
|
|
&testLLMChain{inputKeys: []string{"non-existent-input"}},
|
|
},
|
|
initErr: ErrChainInitialization,
|
|
},
|
|
{
|
|
name: "overlapping output key",
|
|
chains: []Chain{
|
|
&testLLMChain{inputKeys: []string{"input1", "input2"}, outputKeys: []string{"output"}},
|
|
// 2nd chain's output key overlaps with 1st chain's output key
|
|
&testLLMChain{inputKeys: []string{"output"}, outputKeys: []string{"output"}},
|
|
},
|
|
initErr: ErrChainInitialization,
|
|
},
|
|
{
|
|
name: "missing output key",
|
|
// no chains have 'output' key which is expected by the sequential chain
|
|
chains: []Chain{
|
|
&testLLMChain{inputKeys: []string{"input1", "input2"}, outputKeys: []string{"output1"}},
|
|
&testLLMChain{inputKeys: []string{"output1"}, outputKeys: []string{"output2"}},
|
|
},
|
|
initErr: ErrChainInitialization,
|
|
},
|
|
{
|
|
name: "chain execution error",
|
|
chains: []Chain{
|
|
// chain throws an error
|
|
&testLLMChain{inputKeys: []string{"input1", "input2"}, outputKeys: []string{"output"}, err: errDummy},
|
|
},
|
|
execErr: errDummy,
|
|
},
|
|
{
|
|
name: "memory key collides with input key",
|
|
chains: []Chain{
|
|
&testLLMChain{inputKeys: []string{"input1"}, outputKeys: []string{"output"}},
|
|
},
|
|
initErr: ErrChainInitialization,
|
|
seqChainOpts: []SequentialChainOption{WithSeqChainMemory(
|
|
memory.NewConversationBuffer(memory.WithMemoryKey("input1")),
|
|
)},
|
|
},
|
|
}
|
|
|
|
for _, tc := range testCases {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
c, err := NewSequentialChain(tc.chains, []string{"input1", "input2"}, []string{"output"}, tc.seqChainOpts...)
|
|
if tc.initErr != nil {
|
|
require.ErrorIs(t, err, tc.initErr)
|
|
} else {
|
|
require.NoError(t, err)
|
|
_, err := Call(ctx, c, map[string]any{"input1": "foo", "input2": "bar"})
|
|
require.ErrorIs(t, err, tc.execErr)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// LLMChain for testing purposes.
|
|
type testLLMChain struct {
|
|
err error
|
|
inputKeys []string
|
|
outputKeys []string
|
|
}
|
|
|
|
// Call runs the logic of the chain and returns the output. This method should
|
|
// not be called directly. Use rather the Call, Run or Predict functions that
|
|
// handles the memory and other aspects of the chain.
|
|
func (c *testLLMChain) Call(_ context.Context, _ map[string]any, _ ...ChainCallOption) (map[string]any, error) { //nolint:lll
|
|
return nil, c.err
|
|
}
|
|
|
|
// GetMemory gets the memory of the chain.
|
|
func (c *testLLMChain) GetMemory() schema.Memory {
|
|
return memory.NewSimple()
|
|
}
|
|
|
|
// InputKeys returns the input keys the chain expects.
|
|
func (c *testLLMChain) GetInputKeys() []string {
|
|
return c.inputKeys
|
|
}
|
|
|
|
// OutputKeys returns the output keys the chain returns.
|
|
func (c *testLLMChain) GetOutputKeys() []string {
|
|
return c.outputKeys
|
|
}
|