Repository: tmc/langchaingo
Pull request #180 (2023-07-08 17:16:23)

feat: regex parser
This is a necessary component for use in the map rerank chain.

Changes:
- Adds regex parser with tests.
- Minor nits on docs.

### 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 the 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 the 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/chains/chains.go b/chains/chains.go
index 709cac3d..64644c65 100644
--- a/chains/chains.go
+++ b/chains/chains.go
@@ -3,7 +3,6 @@ package chains
 import (
 	"context"
 	"fmt"
-	"sync"
 
 	"github.com/tmc/langchaingo/schema"
 )
@@ -124,82 +123,46 @@ func Predict(ctx context.Context, c Chain, inputValues map[string]any, options .
 
 const _defaultApplyMaxNumberWorkers = 5
 
-type applyInputJob struct {
-	input map[string]any
-	i     int
-}
-
-type applyResult struct {
-	result map[string]any
-	err    error
-	i      int
-}
-
 // Apply executes the chain for each of the inputs asynchronously.
 func Apply(ctx context.Context, c Chain, inputValues []map[string]any, maxWorkers int, options ...ChainCallOption) ([]map[string]any, error) { // nolint:lll
 	if maxWorkers <= 0 {
 		maxWorkers = _defaultApplyMaxNumberWorkers
 	}
 
-	inputJobs := make(chan applyInputJob, len(inputValues))
-	resultsChan := make(chan applyResult, len(inputValues))
-
-	var wg sync.WaitGroup
-	wg.Add(maxWorkers)
+	inputJobs := make(chan map[string]any, len(inputValues))
+	resultsChan := make(chan struct {
+		result map[string]any
+		err    error
+	}, len(inputValues))
+	defer close(inputJobs)
+	defer close(resultsChan)
 
 	for w := 0; w < maxWorkers; w++ {
 		go func() {
-			defer wg.Done()
-			for {
-				select {
-				case <-ctx.Done():
-					return
-				case input, ok := <-inputJobs:
-					if !ok {
-						return
-					}
-					res, err := Call(ctx, c, input.input, options...)
-					resultsChan <- applyResult{
-						result: res,
-						err:    err,
-						i:      input.i,
-					}
+			for input := range inputJobs {
+				res, err := Call(ctx, c, input, options...)
+				resultsChan <- struct {
+					result map[string]any
+					err    error
+				}{
+					result: res,
+					err:    err,
 				}
 			}
 		}()
 	}
 
-	go func() {
-		wg.Wait()
-		close(resultsChan)
-	}()
-
-	sendApplyInputJobs(inputJobs, inputValues)
-	return getApplyResults(ctx, resultsChan, inputValues)
-}
-
-func sendApplyInputJobs(inputJobs chan applyInputJob, inputValues []map[string]any) {
-	for i, input := range inputValues {
-		inputJobs <- applyInputJob{
-			input: input,
-			i:     i,
-		}
+	for _, input := range inputValues {
+		inputJobs <- input
 	}
-	close(inputJobs)
-}
 
-func getApplyResults(ctx context.Context, resultsChan chan applyResult, inputValues []map[string]any) ([]map[string]any, error) { //nolint:lll
 	results := make([]map[string]any, len(inputValues))
-	for range results {
-		select {
-		case <-ctx.Done():
-			return nil, ctx.Err()
-		case r := <-resultsChan:
-			if r.err != nil {
-				return nil, r.err
-			}
-			results[r.i] = r.result
+	for i := range inputValues {
+		r := <-resultsChan
+		if r.err != nil {
+			return nil, r.err
 		}
+		results[i] = r.result
 	}
 
 	return results, nil
diff --git a/chains/map_reduce.go b/chains/map_reduce.go
index 5da0d801..1cbd3563 100644
--- a/chains/map_reduce.go
+++ b/chains/map_reduce.go
@@ -50,7 +50,7 @@ func NewMapReduceDocuments(llmChain *LLMChain, reduceChain Chain) MapReduceDocum
 		LLMChain:                   llmChain,
 		ReduceChain:                reduceChain,
 		Memory:                     memory.NewSimple(),
-		ReduceDocumentVariableName: _combineDocumentsDefaultInputKey,
+		ReduceDocumentVariableName: _combineDocumentsDefaultDocumentVariableName,
 		LLMChainInputVariableName:  _combineDocumentsDefaultDocumentVariableName,
 		MaxNumberOfConcurrent:      _defaultApplyMaxNumberWorkers,
 		InputKey:                   _combineDocumentsDefaultInputKey,
@@ -72,12 +72,12 @@ func (c MapReduceDocuments) Call(ctx context.Context, values map[string]any, opt
 	}
 
 	// Create a document for each of map results and create input values for each of the document.
-	reduceInputs, err := c.mapResultsToReduceInputs(docs, mapResults, values)
+	llmChainInputs, err := c.mapResultsToLLMChainInputs(docs, mapResults, values)
 	if err != nil {
 		return nil, err
 	}
 
-	result, err := Call(ctx, c.ReduceChain, reduceInputs, options...)
+	result, err := Call(ctx, c.ReduceChain, llmChainInputs, options...)
 	return c.maybeAddIntermediateSteps(result, mapResults), err
 }
 
@@ -112,7 +112,7 @@ func (c MapReduceDocuments) getApplyInputs(values map[string]any, docs []schema.
 	return inputs
 }
 
-func (c MapReduceDocuments) mapResultsToReduceInputs(
+func (c MapReduceDocuments) mapResultsToLLMChainInputs(
 	docs []schema.Document,
 	mapResults []map[string]any,
 	inputValues map[string]any,
@@ -130,7 +130,7 @@ func (c MapReduceDocuments) mapResultsToReduceInputs(
 		})
 	}
 
-	documentInputVariable := c.getInputVariable(c.ReduceDocumentVariableName, c.ReduceChain.GetInputKeys())
+	documentInputVariable := c.getInputVariable(c.LLMChainInputVariableName, c.LLMChain.GetInputKeys())
 	reduceInputs := c.copyInputValuesWithoutInputKey(inputValues)
 	reduceInputs[documentInputVariable] = resultDocs
 
@@ -145,31 +145,30 @@ func (c MapReduceDocuments) copyInputValuesWithoutInputKey(inputValues map[strin
 }
 
 func (c MapReduceDocuments) GetInputKeys() []string {
-	inputKeys := map[string]bool{c.InputKey: true}
+	inputKeys := []string{c.InputKey}
 	for _, key := range c.LLMChain.GetInputKeys() {
 		if key == c.LLMChainInputVariableName {
 			continue
 		}
-		inputKeys[key] = true
+		inputKeys = append(inputKeys, key)
 	}
 
 	for _, key := range c.ReduceChain.GetInputKeys() {
 		if key == c.ReduceDocumentVariableName {
 			continue
 		}
-		inputKeys[key] = true
+		inputKeys = append(inputKeys, key)
 	}
 
-	return maps.Keys(inputKeys)
-}
-
-func (c MapReduceDocuments) GetOutputKeys() []string {
-	outputKeys := c.ReduceChain.GetOutputKeys()
 	if c.ReturnIntermediateSteps {
-		outputKeys = append(outputKeys, _intermediateStepsOutputKey)
+		inputKeys = append(inputKeys, _intermediateStepsOutputKey)
 	}
 
-	return outputKeys
+	return inputKeys
+}
+
+func (c MapReduceDocuments) GetOutputKeys() []string {
+	return c.ReduceChain.GetOutputKeys()
 }
 
 func (c MapReduceDocuments) GetMemory() schema.Memory { //nolint:ireturn
diff --git a/chains/question_answering.go b/chains/question_answering.go
index d5b1457e..735cb51e 100644
--- a/chains/question_answering.go
+++ b/chains/question_answering.go
@@ -13,7 +13,7 @@ const _defaultStuffQATemplate = `Use the following pieces of context to answer t
 Question: {{.question}}
 Helpful Answer:`
 
-const _defaultRefineQATemplate = `The original question is as follows: {{.question}}
+const _defaultRefineTemplate = `The original question is as follows: {{.question}}
 We have provided an existing answer: {{.existing_answer}}
 We have the opportunity to refine the existing answer
 (only if needed) with some more context below.
@@ -23,23 +23,6 @@ We have the opportunity to refine the existing answer
 Given the new context, refine the original answer to better answer the question. 
 If the context isn't useful, return the original answer.`
 
-//nolint:lll
-const _defaultMapReduceGetInformationQATemplate = `Use the following portion of a long document to see if any of the text is relevant to answer the question. 
-Return any relevant text verbatim.
-{{.context}}
-Question: {{.question}}
-Relevant text, if any:`
-
-//nolint:lll
-const _defaultMapReduceCombineQATemplate = `Given the following extracted parts of a long document and a question, create a final answer. 
-If you don't know the answer, just say that you don't know. Don't try to make up an answer.
-
-QUESTION: {{.question}}
-=========
-{{.context}}
-=========
-FINAL ANSWER:`
-
 // LoadStuffQA loads a StuffDocuments chain with default prompts for the llm chain.
 func LoadStuffQA(llm llms.LanguageModel) StuffDocuments {
 	defaultQAPromptTemplate := prompts.NewPromptTemplate(
@@ -64,7 +47,7 @@ func LoadRefineQA(llm llms.LanguageModel) RefineDocuments {
 		[]string{"context", "question"},
 	)
 	refinePrompt := prompts.NewPromptTemplate(
-		_defaultRefineQATemplate,
+		_defaultRefineTemplate,
 		[]string{"question", "existing_answer", "context"},
 	)
 
@@ -73,23 +56,3 @@ func LoadRefineQA(llm llms.LanguageModel) RefineDocuments {
 		NewLLMChain(llm, refinePrompt),
 	)
 }
-
-// LoadRefineQA loads a refine documents chain for question answering. Inputs are
-// "question" and "input_documents".
-func LoadMapReduceQA(llm llms.LanguageModel) MapReduceDocuments {
-	getInfoPrompt := prompts.NewPromptTemplate(
-		_defaultMapReduceGetInformationQATemplate,
-		[]string{"question", "context"},
-	)
-	combinePrompt := prompts.NewPromptTemplate(
-		_defaultMapReduceCombineQATemplate,
-		[]string{"question", "context"},
-	)
-
-	mapChain := NewLLMChain(llm, getInfoPrompt)
-	reduceChain := NewStuffDocuments(
-		NewLLMChain(llm, combinePrompt),
-	)
-
-	return NewMapReduceDocuments(mapChain, reduceChain)
-}
diff --git a/chains/summarization.go b/chains/summarization.go
index 2c73405a..bca5a28b 100644
--- a/chains/summarization.go
+++ b/chains/summarization.go
@@ -48,14 +48,3 @@ func LoadRefineSummarization(llm llms.LanguageModel) RefineDocuments {
 
 	return NewRefineDocuments(llmChain, refineLLMChain)
 }
-
-// LoadMapReduceSummarization loads a map reduce documents chain for
-// summarization of documents.
-func LoadMapReduceSummarization(llm llms.LanguageModel) MapReduceDocuments {
-	mapChain := NewLLMChain(llm, prompts.NewPromptTemplate(
-		_stuffSummarizationTemplate, []string{"context"},
-	))
-	combineChain := LoadStuffSummarization(llm)
-
-	return NewMapReduceDocuments(mapChain, combineChain)
-}
diff --git a/docs/parity_matrix.md b/docs/parity_matrix.md
index 4e84f3ea..4b3dd1fb 100644
--- a/docs/parity_matrix.md
+++ b/docs/parity_matrix.md
@@ -28,7 +28,7 @@ Please note that this page lists the current state of the LangChain Go project,
 | LLM Chain                              | ✅     |
 | Stuff Combine Documents Chain          | ✅     |
 | Retrieval QA Chain                     | ✅     |
-| Map Reduce Combine Documents Chain     | ✅     |
+| Map Reduce Combine Documents Chain     | ❌     |
 | Refine Combine Documents Chain         | ✅     |
 | Map Rerank Combine Documents Chain     | ❌ [#68](https://github.com/tmc/langchaingo/issues/68) |
 | Chat Vector DB Chain                   | ❌     |
@@ -37,8 +37,8 @@ Please note that this page lists the current state of the LangChain Go project,
 | Question Answering Chains              | ✅     |
 | Summarization Chains                   | ✅     |
 | Question Answering With Sources Chains | ❌     |
-| SQL Database Chain                     | ✅     |
-| API Chain                              | ✅     |
+| SQL Database Chain                     | ❌     |
+| API Chain                              | ❌     |
 | Transformation Chain                   | ❌     |
 | Constitutional Chain                   | ❌     |
 | Conversational Chain                   | ❌     |
@@ -65,10 +65,10 @@ Please note that this page lists the current state of the LangChain Go project,
 
 ## Memory
 
-| Feature                      | Status |
+| Feature                     | Status |
 | ----------------------------| ------ |
 | Buffer Memory               | ✅     |
-| Buffer Window Memory         | ❌     |
+| Buffer Window Memory        | ❌     |
 | Summary Memory              | ❌     |
 | Entity Memory               | ❌     |
 | Summary Buffer Memory       | ❌     |
@@ -82,7 +82,7 @@ Please note that this page lists the current state of the LangChain Go project,
 | Structured                  | ✅     |
 | Boolean                     | ❌     |
 | Combining Parsers           | ❌     |
-| Regex                       | ❌     |
+| Regex                       | ✅     |
 | Regex Dictionary            | ❌     |
 | Comma separated list        | ✅     |
 | Parser fixer                | ❌     |
diff --git a/outputparser/regex_parser.go b/outputparser/regex_parser.go
new file mode 100644
index 00000000..61467518
--- /dev/null
+++ b/outputparser/regex_parser.go
@@ -0,0 +1,73 @@
+package outputparser
+
+import (
+	"fmt"
+	"regexp"
+
+	"github.com/tmc/langchaingo/schema"
+)
+
+// RegexParser is an output parser used to parse the output of an llm as a map.
+type RegexParser struct {
+	Expression *regexp.Regexp
+	OutputKeys []string
+}
+
+// NewRegexParser returns a new RegexParser.
+func NewRegexParser(expressionStr string) RegexParser {
+	expression := regexp.MustCompile(expressionStr)
+	outputKeys := expression.SubexpNames()[1:]
+
+	return RegexParser{
+		Expression: expression,
+		OutputKeys: outputKeys,
+	}
+}
+
+// Statically assert that RegexParser implements the OutputParser interface.
+var _ schema.OutputParser[map[string]string] = RegexParser{}
+
+// GetFormatInstructions returns instructions on the expected output format.
+func (p RegexParser) GetFormatInstructions() string {
+	instructions := "Your output should be a map of strings. e.g.:\n"
+	instructions += "map[string]string{\"key1\": \"value1\", \"key2\": \"value2\"}\n"
+
+	return instructions
+}
+
+func (p RegexParser) parse(text string) (map[string]string, error) {
+	match := p.Expression.FindStringSubmatch(text)
+
+	if len(match) == 0 {
+		return nil, ParseError{
+			Text:   text,
+			Reason: fmt.Sprintf("No match found for expression %s", p.Expression),
+		}
+	}
+
+	// remove the first match (entire string) for parity with the output keys.
+	match = match[1:]
+
+	matches := make(map[string]string, len(match))
+
+	for i, name := range p.OutputKeys {
+		matches[name] = match[i]
+	}
+
+	return matches, nil
+}
+
+// Parse parses the output of an llm into a map of strings.
+func (p RegexParser) Parse(text string) (map[string]string, error) {
+	return p.parse(text)
+}
+
+// ParseWithPrompt does the same as Parse.
+func (p RegexParser) ParseWithPrompt(text string, _ schema.PromptValue) (map[string]string, error) {
+	return p.parse(text)
+}
+
+// Type returns the type of the parser.
+func (p RegexParser) Type() string {
+	return "regex_parser"
+}
diff --git a/tools/duckduckgo/ddg.go b/tools/duckduckgo/ddg.go
index e8bc7b08..2d6f39b3 100644
--- a/tools/duckduckgo/ddg.go
+++ b/tools/duckduckgo/ddg.go
@@ -8,30 +8,22 @@ import (
 	"github.com/tmc/langchaingo/tools/duckduckgo/internal"
 )
 
-// DefaultUserAgent defines a default value for user-agent header.
-const DefaultUserAgent = "github.com/tmc/langchaingo/tools/duckduckgo"
-
-// Tool defines a tool implementation for the DuckDuckGo Search.
 type Tool struct {
 	client *internal.Client
 }
 
 var _ tools.Tool = Tool{}
 
-// New initializes a new DuckDuckGo Search tool with arguments for setting a
-// max results per search query and a value for the user agent header.
 func New(maxResults int, userAgent string) (*Tool, error) {
 	return &Tool{
 		client: internal.New(maxResults, userAgent),
 	}, nil
 }
 
-// Name returns a name for the tool.
 func (t Tool) Name() string {
 	return "DuckDuckGo Search"
 }
 
-// Description returns a description for the tool.
 func (t Tool) Description() string {
 	return `
 	"A wrapper around DuckDuckGo Search."
@@ -39,7 +31,6 @@ func (t Tool) Description() string {
 	"Input should be a search query."`
 }
 
-// Call performs the search and return the result.
 func (t Tool) Call(ctx context.Context, input string) (string, error) {
 	result, err := t.client.Search(ctx, input)
 	if err != nil {
diff --git a/tools/duckduckgo/internal/client.go b/tools/duckduckgo/internal/client.go
index f11312d7..7a4d6892 100644
--- a/tools/duckduckgo/internal/client.go
+++ b/tools/duckduckgo/internal/client.go
@@ -4,6 +4,7 @@ import (
 	"context"
 	"errors"
 	"fmt"
+	"log"
 	"net/http"
 	"net/url"
 	"strings"
@@ -11,27 +12,28 @@ import (
 	"github.com/PuerkitoBio/goquery"
 )
 
+const _defaultUserAgent = "github.com/tmc/langchaingo/tools/duckduckgo"
+
 var (
 	ErrNoGoodResult = errors.New("no good search results found")
 	ErrAPIResponse  = errors.New("duckduckgo api responded with error")
 )
 
-// Client defines an HTTP client for communicating with duckduckgo.
 type Client struct {
 	maxResults int
 	userAgent  string
 }
 
-// Result defines a search query result type.
 type Result struct {
 	Title string
 	Info  string
 	Ref   string
 }
 
-// New initializes a Client with arguments for setting a max
-// results per search query and a value for the user agent header.
 func New(maxResults int, userAgent string) *Client {
+	if userAgent == "" {
+		userAgent = _defaultUserAgent
+	}
 	if maxResults == 0 {
 		maxResults = 1
 	}
@@ -42,29 +44,17 @@ func New(maxResults int, userAgent string) *Client {
 	}
 }
 
-func (client *Client) newRequest(ctx context.Context, queryURL string) (*http.Request, error) {
-	request, err := http.NewRequestWithContext(ctx, http.MethodGet, queryURL, nil)
-	if err != nil {
-		return nil, fmt.Errorf("creating duckduckgo request: %w", err)
-	}
-
-	if client.userAgent != "" {
-		request.Header.Add("User-Agent", client.userAgent)
-	}
-
-	return request, nil
-}
-
-// Search performs a search query and returns
-// the result as string and an error if any.
 func (client *Client) Search(ctx context.Context, query string) (string, error) {
+	results := []Result{}
 	queryURL := fmt.Sprintf("https://html.duckduckgo.com/html/?q=%s", url.QueryEscape(query))
 
-	request, err := client.newRequest(ctx, queryURL)
+	request, err := http.NewRequestWithContext(ctx, http.MethodGet, queryURL, nil)
 	if err != nil {
-		return "", err
+		return "", fmt.Errorf("creating duckduckgo request: %w", err)
 	}
 
+	request.Header.Add("User-Agent", client.userAgent)
+
 	response, err := http.DefaultClient.Do(request)
 	if err != nil {
 		return "", fmt.Errorf("get %s error: %w", queryURL, err)
@@ -80,7 +70,6 @@ func (client *Client) Search(ctx context.Context, query string) (string, error)
 		return "", fmt.Errorf("new document error: %w", err)
 	}
 
-	results := []Result{}
 	sel := doc.Find(".web-result")
 
 	for i := range sel.Nodes {
@@ -103,6 +92,7 @@ func (client *Client) Search(ctx context.Context, query string) (string, error)
 				),
 			)
 			if err != nil {
+				log.Printf("Error: %s", err)
 				return "", err
 			}
 		}


Test patch:
diff --git a/chains/chains_test.go b/chains/chains_test.go
index 59ed2bf8..141fa1b3 100644
--- a/chains/chains_test.go
+++ b/chains/chains_test.go
@@ -2,9 +2,7 @@ package chains
 
 import (
 	"context"
-	"sync"
 	"testing"
-	"time"
 
 	"github.com/stretchr/testify/require"
 	"github.com/tmc/langchaingo/llms"
@@ -15,7 +13,6 @@ import (
 type testLanguageModel struct{}
 
 func (l testLanguageModel) GeneratePrompt(_ context.Context, _ []schema.PromptValue, _ ...llms.CallOption) (llms.LLMResult, error) { //nolint:lll
-	time.Sleep(time.Second)
 	return llms.LLMResult{
 		Generations: [][]*llms.Generation{{&llms.Generation{
 			Text: "result",
@@ -41,24 +38,3 @@ func TestApply(t *testing.T) {
 	require.NoError(t, err)
 	require.Equal(t, numInputs, len(results), "number of inputs and results not equal")
 }
-
-func TestApplyWithCanceledContext(t *testing.T) {
-	t.Parallel()
-
-	numInputs := 10
-	maxWorkers := 5
-	inputs := make([]map[string]any, numInputs)
-	ctx, cancelFunc := context.WithCancel(context.Background())
-	wg := sync.WaitGroup{}
-	wg.Add(1)
-	c := NewLLMChain(testLanguageModel{}, prompts.NewPromptTemplate("test", nil))
-
-	go func() {
-		defer wg.Done()
-		_, err := Apply(ctx, c, inputs, maxWorkers)
-		require.Error(t, err)
-	}()
-
-	cancelFunc()
-	wg.Wait()
-}
diff --git a/chains/llm_test.go b/chains/llm_test.go
index 59bcab43..682c3112 100644
--- a/chains/llm_test.go
+++ b/chains/llm_test.go
@@ -6,6 +6,7 @@ import (
 	"strings"
 	"testing"
 
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"github.com/tmc/langchaingo/llms/openai"
 	"github.com/tmc/langchaingo/prompts"
@@ -27,11 +28,17 @@ func TestLLMChain(t *testing.T) {
 
 	chain := NewLLMChain(model, prompt)
 
-	result, err := Predict(context.Background(), chain,
+	resultChainValue, err := Call(context.Background(), chain,
 		map[string]any{
 			"country": "France",
 		},
 	)
 	require.NoError(t, err)
-	require.True(t, strings.Contains(result, "Paris"))
+
+	resultAny, ok := resultChainValue["text"]
+	require.True(t, ok)
+
+	result, _ := resultAny.(string)
+	result = strings.TrimSpace(result)
+	assert.Equal(t, "Paris.", result)
 }
diff --git a/chains/map_reduce_test.go b/chains/map_reduce_test.go
index 013b77bf..6008bf41 100644
--- a/chains/map_reduce_test.go
+++ b/chains/map_reduce_test.go
@@ -39,11 +39,9 @@ func TestMapReduce(t *testing.T) {
 			testLanguageModel{},
 			prompts.NewPromptTemplate("{{.context}}", []string{"context"}),
 		),
-		NewStuffDocuments(
-			NewLLMChain(
-				testLanguageModel{},
-				prompts.NewPromptTemplate("{{.context}}", []string{"context"}),
-			),
+		NewLLMChain(
+			testLanguageModel{},
+			prompts.NewPromptTemplate("{{.context}}", []string{"context"}),
 		),
 	)
 
diff --git a/chains/question_answering_test.go b/chains/question_answering_test.go
index 488b8c22..c5921cb9 100644
--- a/chains/question_answering_test.go
+++ b/chains/question_answering_test.go
@@ -3,7 +3,6 @@ package chains
 import (
 	"context"
 	"os"
-	"strings"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -35,27 +34,3 @@ func TestRefineQA(t *testing.T) {
 	_, ok := results["text"].(string)
 	require.True(t, ok, "result does not contain text key")
 }
-
-func TestMapReduceQA(t *testing.T) {
-	t.Parallel()
-
-	if openaiKey := os.Getenv("OPENAI_API_KEY"); openaiKey == "" {
-		t.Skip("OPENAI_API_KEY not set")
-	}
-	llm, err := openai.New()
-	require.NoError(t, err)
-
-	docs := loadTestData(t)
-	qaChain := LoadMapReduceQA(llm)
-
-	result, err := Predict(
-		context.Background(),
-		qaChain,
-		map[string]any{
-			"input_documents": docs,
-			"question":        "What is the name of the lion?",
-		},
-	)
-	require.NoError(t, err)
-	require.True(t, strings.Contains(result, "Leo"), "result does not contain correct answer Leo")
-}
diff --git a/chains/summarization_test.go b/chains/summarization_test.go
index bf087907..a849095f 100644
--- a/chains/summarization_test.go
+++ b/chains/summarization_test.go
@@ -67,23 +67,3 @@ func TestRefineSummarization(t *testing.T) {
 	)
 	require.NoError(t, err)
 }
-
-func TestMapReduceSummarization(t *testing.T) {
-	t.Parallel()
-
-	if openaiKey := os.Getenv("OPENAI_API_KEY"); openaiKey == "" {
-		t.Skip("OPENAI_API_KEY not set")
-	}
-	llm, err := openai.New()
-	require.NoError(t, err)
-
-	docs := loadTestData(t)
-
-	chain := LoadMapReduceSummarization(llm)
-	_, err = Run(
-		context.Background(),
-		chain,
-		docs,
-	)
-	require.NoError(t, err)
-}
diff --git a/outputparser/regex_parser_test.go b/outputparser/regex_parser_test.go
new file mode 100644
index 00000000..40a28e4c
--- /dev/null
+++ b/outputparser/regex_parser_test.go
@@ -0,0 +1,56 @@
+package outputparser_test
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/tmc/langchaingo/outputparser"
+)
+
+func TestRegexParser(t *testing.T) {
+	t.Parallel()
+
+	testCases := []struct {
+		input      string
+		expression string
+		expected   map[string]string
+	}{
+		{
+			input:      "testing_foo, testing_bar, testing_baz",
+			expression: `(?P<foo>\w+), (?P<bar>\w+), (?P<baz>\w+)`,
+			expected: map[string]string{
+				"foo": "testing_foo",
+				"bar": "testing_bar",
+				"baz": "testing_baz",
+			},
+		},
+		{
+			input:      "Score: 100",
+			expression: `Score: (?P<score>\d+)`,
+			expected: map[string]string{
+				"score": "100",
+			},
+		},
+		{
+			input:      "Score: 100",
+			expression: `Score: (?P<score>\d+)(?:\s(?P<test>\d+))*`,
+			expected: map[string]string{
+				"score": "100",
+				"test":  "",
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		parser := outputparser.NewRegexParser(tc.expression)
+
+		actual, err := parser.Parse(tc.input)
+		if err != nil {
+			t.Errorf("Unexpected error: %v", err)
+		}
+
+		if !reflect.DeepEqual(actual, tc.expected) {
+			t.Errorf("Expected %v, got %v", tc.expected, actual)
+		}
+	}
+}
diff --git a/tools/zapier/zapier_test.go b/tools/zapier/zapier_test.go
index cd6728b5..29fff461 100644
--- a/tools/zapier/zapier_test.go
+++ b/tools/zapier/zapier_test.go
@@ -21,6 +21,5 @@ func TestCreateDescription(t *testing.T) {
 
 	desc := tool.Description()
 	assert.Contains(t, desc, "Test Tool")
-	assert.Contains(t, desc, "Param2")
-	assert.Contains(t, desc, "Param1")
+	assert.Contains(t, desc, "['Param1', 'Param2']")
 }