Repository: tmc/langchaingo
Pull request #194 (2023-07-14 18:57:43)

feat(outputparser): Add Regex Dict Parser
References:
- [langchain (python) regex dict output parser](https://github.com/hwchase17/langchain/blob/master/langchain/output_parsers/regex_dict.py).

Changes:
- Adds functionality `outputparser` `RegexDict`.

### 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/docs/parity_matrix.md b/docs/parity_matrix.md
index f209e533..fcfe67fb 100644
--- a/docs/parity_matrix.md
+++ b/docs/parity_matrix.md
@@ -83,7 +83,7 @@ Please note that this page lists the current state of the LangChain Go project,
 | Boolean                     | ❌     |
 | Combining Parsers           | ❌     |
 | Regex                       | ✅     |
-| Regex Dictionary            | ❌     |
+| Regex Dictionary            | ✅     |
 | Comma separated list        | ✅     |
 | Parser fixer                | ❌     |
 | Retry                       | ❌     |
diff --git a/outputparser/doc.go b/outputparser/doc.go
index 4590c323..32e394ca 100644
--- a/outputparser/doc.go
+++ b/outputparser/doc.go
@@ -11,5 +11,7 @@ The outputparser package includes the following parsers:
     and returns them as a string slice.
   - RegexParser: a parser that takes a string, compiles it into a regular expression,
     and returns map[string]string of the regex groups.
+  - RegexDict: a parser that searches a string for values in a dictionary format,
+    and returns a map[string]string of the keys and their associated value.
 */
 package outputparser
diff --git a/outputparser/regex_dict.go b/outputparser/regex_dict.go
new file mode 100644
index 00000000..12be6844
--- /dev/null
+++ b/outputparser/regex_dict.go
@@ -0,0 +1,91 @@
+package outputparser
+
+import (
+	"fmt"
+	"regexp"
+
+	"github.com/tmc/langchaingo/schema"
+)
+
+// RegexDict is an output parser used to parse the output of an llm as a map.
+type RegexDict struct {
+	// OutputKeyToFormat is a map which has a key that represents an identifier for the regex,
+	// and a value to search for as a key in the parsed text.
+	OutputKeyToFormat map[string]string
+
+	// NoUpdateValue is a string that prevents assignment to parsed outputs map when matched.
+	NoUpdateValue string
+}
+
+const (
+	regexDictPattern = `(?:%s):\s?(?P<value>(?:[^.'\n']*)\.?)`
+)
+
+// NewRegexDict returns a new RegexDict.
+func NewRegexDict(outputKeyToFormat map[string]string, noUpdateValue string) RegexDict {
+	return RegexDict{
+		OutputKeyToFormat: outputKeyToFormat,
+		NoUpdateValue:     noUpdateValue,
+	}
+}
+
+// Statically assert that RegexDict implements the OutputParser interface.
+var _ schema.OutputParser[any] = RegexDict{}
+
+// GetFormatInstructions returns instructions on the expected output format.
+func (p RegexDict) 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 RegexDict) parse(text string) (map[string]string, error) {
+	results := make(map[string]string, len(p.OutputKeyToFormat))
+	// We only expect to get a single matched value pair for each output key.
+	expectedMatches := 2
+
+	for key, format := range p.OutputKeyToFormat {
+		expression := regexp.MustCompile(fmt.Sprintf(regexDictPattern, format))
+		matches := expression.FindStringSubmatch(text)
+
+		if len(matches) < expectedMatches {
+			return nil, ParseError{
+				Text:   text,
+				Reason: fmt.Sprintf("No match found for expression %s", expression),
+			}
+		}
+
+		if len(matches) > expectedMatches {
+			return nil, ParseError{
+				Text:   text,
+				Reason: fmt.Sprintf("Multiple matches found for expression %s", expression),
+			}
+		}
+
+		match := matches[1]
+
+		if match == p.NoUpdateValue {
+			continue
+		}
+
+		results[key] = match
+	}
+
+	return results, nil
+}
+
+// Parse parses the output of an llm into a map of strings.
+func (p RegexDict) Parse(text string) (any, error) {
+	return p.parse(text)
+}
+
+// ParseWithPrompt does the same as Parse.
+func (p RegexDict) ParseWithPrompt(text string, _ schema.PromptValue) (any, error) {
+	return p.parse(text)
+}
+
+// Type returns the type of the parser.
+func (p RegexDict) Type() string {
+	return "regex_dict_parser"
+}


Test patch:
diff --git a/outputparser/regex_dict_test.go b/outputparser/regex_dict_test.go
new file mode 100644
index 00000000..8d8b71b9
--- /dev/null
+++ b/outputparser/regex_dict_test.go
@@ -0,0 +1,74 @@
+package outputparser_test
+
+import (
+	"reflect"
+	"testing"
+
+	"github.com/tmc/langchaingo/outputparser"
+)
+
+func TestRegexDict(t *testing.T) {
+	t.Parallel()
+
+	testText := `We have just received a new result from the LLM, and our next step is
+to filter and read its format using regular expressions to identify specific fields,
+such as:
+
+- Action: Search
+- Action Input: How to use this class?
+- Additional Fields: "N/A"
+
+To assist us in this task, we use the regex_dict class. This class allows us to send a
+dictionary containing an output key and the expected format, which in turn enables us to
+retrieve the result of the matching formats and extract specific information from it.
+
+To exclude irrelevant information from our return dictionary, we can instruct the LLM to
+use a specific command that notifies us when it doesn't know the answer. We call this
+variable the "no_update_value", and for our current case, we set it to "N/A". Therefore,
+we expect the result to only contain the following fields:
+{
+ {key = action, value = search}
+ {key = action_input, value = "How to use this class?"}.
+}`
+
+	testCases := []struct {
+		noUpdateValue string
+		outputKeys    map[string]string
+		expected      map[string]string
+	}{
+		{
+			noUpdateValue: "",
+			outputKeys: map[string]string{
+				"action":       "Action",
+				"action_input": "Action Input",
+			},
+			expected: map[string]string{
+				"action":       "Search",
+				"action_input": "How to use this class?",
+			},
+		},
+		{
+			noUpdateValue: "Search",
+			outputKeys: map[string]string{
+				"action":       "Action",
+				"action_input": "Action Input",
+			},
+			expected: map[string]string{
+				"action_input": "How to use this class?",
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		parser := outputparser.NewRegexDict(tc.outputKeys, tc.noUpdateValue)
+
+		actual, err := parser.Parse(testText)
+		if err != nil {
+			t.Errorf("Unexpected error: %v", err)
+		}
+
+		if !reflect.DeepEqual(actual, tc.expected) {
+			t.Errorf("Expected %v, got %v", tc.expected, actual)
+		}
+	}
+}