Files
Travis Cline c544fb77bd all: add broad httprr coverage, update dependencies, organize go.mod file, bump to 1.23 (#1299)
* 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
2025-06-04 11:41:45 -07:00

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
}