From 77bd95ba7183b8e73dd6a320c7613b0c08728d3f Mon Sep 17 00:00:00 2001 From: "shentong.martin" Date: Mon, 15 Dec 2025 19:37:00 +0800 Subject: [PATCH] feat(react): demonstrate short-term memory Change-Id: Ib2b796a7f66ee33b9115c9331489c8d86e42b8c5 --- flow/agent/react/memory_example/README.md | 97 ++++++++++++++++++ flow/agent/react/memory_example/main.go | 98 +++++++++++++++++++ .../react/memory_example/memory/inmem.go | 81 +++++++++++++++ .../react/memory_example/memory/redis.go | 91 +++++++++++++++++ .../react/memory_example/memory/store.go | 58 +++++++++++ go.mod | 5 + go.sum | 11 +++ 7 files changed, 441 insertions(+) create mode 100644 flow/agent/react/memory_example/README.md create mode 100644 flow/agent/react/memory_example/main.go create mode 100644 flow/agent/react/memory_example/memory/inmem.go create mode 100644 flow/agent/react/memory_example/memory/redis.go create mode 100644 flow/agent/react/memory_example/memory/store.go diff --git a/flow/agent/react/memory_example/README.md b/flow/agent/react/memory_example/README.md new file mode 100644 index 0000000..4a89a1b --- /dev/null +++ b/flow/agent/react/memory_example/README.md @@ -0,0 +1,97 @@ +# Short-Term Memory for ReAct Agent + +This example demonstrates a minimal short-term memory for a `flow/react` agent: + +1. Run the agent with a new input message list, get the assistant output. +2. Serialize and persist the original input messages plus the assistant output. +3. On the next run, restore the stored messages, append the new input, and continue the conversation. +4. Do not persist the system message; inject it at runtime via `MessageModifier`. +5. Storage options include an in-memory map and Redis (with optional in-memory `miniredis`). + +## Where to Look + +- `main.go` — minimal demo: two turns that share memory and a system prompt injected at runtime. +- `memory/store.go` — `MemoryStore` interface and Gob encode/decode helpers. +- `memory/inmem.go` — in-memory store. +- `memory/redis.go` — Redis-backed store and `NewMiniRedisClient()` for an embedded Redis server. + +## System Prompt Handling + +- Do not persist system messages. +- Use the agent hook `react.AgentConfig.MessageModifier` to prepend the system prompt at execution time: + +```go +agent, _ := react.NewAgent(ctx, &react.AgentConfig{ + Model: model, + MessageModifier: func(_ context.Context, input []*schema.Message) []*schema.Message { + return append([]*schema.Message{schema.SystemMessage(sys)}, input...) + }, +}) +``` + +## Serialization + +- Messages are serialized using `encoding/gob`. +- Eino registers the necessary types, so no manual `gob.Register` is required here. + +## Quick Start (OpenAI) + +Environment variables: + +- `OPENAI_API_KEY` +- `OPENAI_MODEL` (e.g., `gpt-4o-mini`) +- `OPENAI_BASE_URL` (optional for proxy endpoints) +- `OPENAI_BY_AZURE` + +Build and run: + +```bash +cd flow/agent/react/memory_example +go build -o memory_example main.go +./memory_example +``` + +Expected output: + +- First run: prints assistant response; memory stores the turn. +- Second run: restores prior messages, appends the new input, and maintains context. + +## Switch Storage Implementations + +Use the in-memory store (default): + +```go +store := memory.NewInMemoryStore() +``` + +Use Redis with in-memory `miniredis`: + +```go +cli, closer, _ := memory.NewMiniRedisClient() +defer closer() +store := memory.NewRedisStore(cli) +``` + +Use Redis with a real server: + +```go +cli := redis.NewClient(&redis.Options{Addr: "localhost:6379"}) +store := memory.NewRedisStore(cli) +``` + +## Minimal Flow + +```go +sessionID := "session:demo" +prev, _ := store.Read(ctx, sessionID) +effective := append(prev, schema.UserMessage(userInput)) +resp, _ := agent.Generate(ctx, effective) +_ = store.Write(ctx, sessionID, append(effective, resp)) + +hits, _ := store.Query(ctx, sessionID, "CloudWeGo", 3) +``` + +## Notes + +- The example uses `Generate`. You can use `Stream` similarly and persist on `io.EOF`. +- Keep the memory window small to cap serialization size; this can be enforced by your store implementation. diff --git a/flow/agent/react/memory_example/main.go b/flow/agent/react/memory_example/main.go new file mode 100644 index 0000000..90b0607 --- /dev/null +++ b/flow/agent/react/memory_example/main.go @@ -0,0 +1,98 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package main + +import ( + "context" + "fmt" + "os" + + "github.com/cloudwego/eino-ext/components/model/openai" + "github.com/cloudwego/eino/flow/agent/react" + "github.com/cloudwego/eino/schema" + + "github.com/cloudwego/eino-examples/flow/agent/react/memory_example/memory" +) + +func main() { + ctx := context.Background() + + apiKey := os.Getenv("OPENAI_API_KEY") + modelName := os.Getenv("OPENAI_MODEL") + baseURL := os.Getenv("OPENAI_BASE_URL") + isAzure := os.Getenv("OPENAI_BY_AZURE") == "true" + + model, err := openai.NewChatModel(ctx, &openai.ChatModelConfig{APIKey: apiKey, Model: modelName, BaseURL: baseURL, ByAzure: isAzure}) + if err != nil { + panic(err) + } + + // System prompt is injected at runtime and not persisted. + sys := "You are a concise assistant. Maintain context across turns." + + agent, err := react.NewAgent(ctx, &react.AgentConfig{ + Model: model, + MessageModifier: func(_ context.Context, input []*schema.Message) []*schema.Message { + return append([]*schema.Message{schema.SystemMessage(sys)}, input...) + }, + }) + if err != nil { + panic(err) + } + + // Choose your store: InMemoryStore (default) or RedisStore (see README). + store := memory.NewInMemoryStore() + sessionID := "session:demo" + + verifyGobRoundTrip() + + run := func(turn string) { + // 1) restore prior messages, 2) append new input, 3) call agent, 4) persist with output + prev, _ := store.Read(ctx, sessionID) + eff := append(prev, schema.UserMessage(turn)) + msg, err := agent.Generate(ctx, eff) + if err != nil { + panic(err) + } + fmt.Printf("history_before=%d after=%d\n", len(prev), len(eff)+1) + fmt.Println(msg.Content) + _ = store.Write(ctx, sessionID, append(eff, msg)) + + hits, _ := store.Query(ctx, sessionID, "AI", 3) + fmt.Printf("query_hits=%d\n", len(hits)) + } + + run("Hello, summarize AI briefly.") + run("Add two more details.") +} + +func verifyGobRoundTrip() { + msgs := []*schema.Message{ + schema.UserMessage("a"), + schema.AssistantMessage("b", nil), + } + // Round-trip serialize/deserialize to validate gob setup. + b, err := memory.EncodeMessages(msgs) + if err != nil { + panic(err) + } + out, err := memory.DecodeMessages(b) + if err != nil { + panic(err) + } + fmt.Printf("gob_round_trip=%d\n", len(out)) +} diff --git a/flow/agent/react/memory_example/memory/inmem.go b/flow/agent/react/memory_example/memory/inmem.go new file mode 100644 index 0000000..6258c2e --- /dev/null +++ b/flow/agent/react/memory_example/memory/inmem.go @@ -0,0 +1,81 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package memory + +import ( + "context" + "strings" + "sync" + + "github.com/cloudwego/eino/schema" +) + +// InMemoryStore keeps serialized messages in a process-local map. +// Suitable for demos/tests; not shared across processes. +type InMemoryStore struct { + mu sync.RWMutex + data map[string][]byte +} + +func NewInMemoryStore() *InMemoryStore { + return &InMemoryStore{data: make(map[string][]byte)} +} + +// Write encodes and stores messages for the given key. +func (s *InMemoryStore) Write(ctx context.Context, sessionID string, msgs []*schema.Message) error { + b, err := EncodeMessages(msgs) + if err != nil { + return err + } + s.mu.Lock() + s.data[sessionID] = b + s.mu.Unlock() + return nil +} + +// Read returns decoded messages for the given session; returns nil if absent. +func (s *InMemoryStore) Read(ctx context.Context, sessionID string) ([]*schema.Message, error) { + s.mu.RLock() + b := s.data[sessionID] + s.mu.RUnlock() + return DecodeMessages(b) +} + +// Query performs a simple substring search on message contents for the session. +func (s *InMemoryStore) Query(ctx context.Context, sessionID string, text string, limit int) ([]*schema.Message, error) { + msgs, err := s.Read(ctx, sessionID) + if err != nil { + return nil, err + } + if len(msgs) == 0 || text == "" { + return nil, nil + } + q := strings.ToLower(text) + out := make([]*schema.Message, 0, limit) + for _, m := range msgs { + if m == nil { + continue + } + if strings.Contains(strings.ToLower(m.Content), q) { + out = append(out, m) + if limit > 0 && len(out) >= limit { + break + } + } + } + return out, nil +} diff --git a/flow/agent/react/memory_example/memory/redis.go b/flow/agent/react/memory_example/memory/redis.go new file mode 100644 index 0000000..9c2c674 --- /dev/null +++ b/flow/agent/react/memory_example/memory/redis.go @@ -0,0 +1,91 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package memory + +import ( + "context" + "strings" + + miniredis "github.com/alicebob/miniredis/v2" + "github.com/cloudwego/eino/schema" + "github.com/redis/go-redis/v9" +) + +// RedisStore persists serialized messages in Redis under the provided session key. +type RedisStore struct { + cli *redis.Client +} + +func NewRedisStore(cli *redis.Client) *RedisStore { + return &RedisStore{cli: cli} +} + +// Write encodes and stores messages using Redis SET. +func (s *RedisStore) Write(ctx context.Context, sessionID string, msgs []*schema.Message) error { + b, err := EncodeMessages(msgs) + if err != nil { + return err + } + return s.cli.Set(ctx, sessionID, b, 0).Err() +} + +// Read returns decoded messages from Redis GET; returns nil if not found. +func (s *RedisStore) Read(ctx context.Context, sessionID string) ([]*schema.Message, error) { + res, err := s.cli.Get(ctx, sessionID).Bytes() + if err == redis.Nil { + return nil, nil + } + if err != nil { + return nil, err + } + return DecodeMessages(res) +} + +func (s *RedisStore) Query(ctx context.Context, sessionID string, text string, limit int) ([]*schema.Message, error) { + msgs, err := s.Read(ctx, sessionID) + if err != nil { + return nil, err + } + if len(msgs) == 0 || text == "" { + return nil, nil + } + out := make([]*schema.Message, 0, limit) + q := strings.ToLower(text) + for _, m := range msgs { + if m == nil { + continue + } + if strings.Contains(strings.ToLower(m.Content), q) { + out = append(out, m) + if limit > 0 && len(out) >= limit { + break + } + } + } + return out, nil +} + +// NewMiniRedisClient starts an embedded Redis server for local demos/tests. +func NewMiniRedisClient() (*redis.Client, func(), error) { + srv, err := miniredis.Run() + if err != nil { + return nil, nil, err + } + cli := redis.NewClient(&redis.Options{Addr: srv.Addr()}) + closer := func() { srv.Close() } + return cli, closer, nil +} diff --git a/flow/agent/react/memory_example/memory/store.go b/flow/agent/react/memory_example/memory/store.go new file mode 100644 index 0000000..6f1946f --- /dev/null +++ b/flow/agent/react/memory_example/memory/store.go @@ -0,0 +1,58 @@ +/* + * Copyright 2025 CloudWeGo Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package memory + +import ( + "bytes" + "context" + "encoding/gob" + + "github.com/cloudwego/eino/schema" +) + +// MemoryStore persists and restores short-term conversation history. +// Implementations are responsible for storing a slice of messages under a session key. +type MemoryStore interface { + Write(ctx context.Context, sessionID string, msgs []*schema.Message) error + Read(ctx context.Context, sessionID string) ([]*schema.Message, error) + Query(ctx context.Context, sessionID string, text string, limit int) ([]*schema.Message, error) +} + +// Gob registrations for eino message types are provided by the framework; no manual registration needed here. + +// EncodeMessages serializes messages using Gob. +func EncodeMessages(msgs []*schema.Message) ([]byte, error) { + var buf bytes.Buffer + enc := gob.NewEncoder(&buf) + if err := enc.Encode(msgs); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// DecodeMessages deserializes messages previously encoded by EncodeMessages. +func DecodeMessages(b []byte) ([]*schema.Message, error) { + if len(b) == 0 { + return nil, nil + } + dec := gob.NewDecoder(bytes.NewReader(b)) + var msgs []*schema.Message + if err := dec.Decode(&msgs); err != nil { + return nil, err + } + return msgs, nil +} diff --git a/go.mod b/go.mod index 80b6b89..7a14ac6 100644 --- a/go.mod +++ b/go.mod @@ -35,6 +35,7 @@ require ( require ( github.com/PuerkitoBio/goquery v1.10.3 // indirect + github.com/alicebob/miniredis/v2 v2.35.0 // indirect github.com/andybalholm/cascadia v1.3.3 // indirect github.com/aymerick/douceur v0.2.0 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect @@ -43,6 +44,7 @@ require ( github.com/bytedance/gopkg v0.1.3 // indirect github.com/bytedance/sonic/loader v0.4.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732 // indirect github.com/chromedp/sysutil v1.0.0 // indirect github.com/cloudwego/base64x v0.1.6 // indirect @@ -50,6 +52,7 @@ require ( github.com/cohesion-org/deepseek-go v1.3.2 // indirect github.com/corpix/uarand v0.2.0 // indirect github.com/coze-dev/cozeloop-go/spec v0.1.5 // indirect + github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect github.com/dlclark/regexp2 v1.11.4 // indirect github.com/dslipak/pdf v0.0.2 // indirect github.com/dustin/go-humanize v1.0.1 // indirect @@ -80,6 +83,7 @@ require ( github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/pkg/errors v0.9.1 // indirect github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect + github.com/redis/go-redis/v9 v9.17.2 // indirect github.com/richardlehane/mscfb v1.0.4 // indirect github.com/richardlehane/msoleps v1.0.4 // indirect github.com/sirupsen/logrus v1.9.3 // indirect @@ -96,6 +100,7 @@ require ( github.com/xuri/efp v0.0.1 // indirect github.com/xuri/nfp v0.0.2-0.20250530014748-2ddeb826f9a9 // indirect github.com/yargevad/filepathx v1.0.0 // indirect + github.com/yuin/gopher-lua v1.1.1 // indirect golang.org/x/arch v0.19.0 // indirect golang.org/x/crypto v0.43.0 // indirect golang.org/x/exp v0.0.0-20250718183923-645b1fa84792 // indirect diff --git a/go.sum b/go.sum index ffc9381..4f0e966 100644 --- a/go.sum +++ b/go.sum @@ -56,6 +56,8 @@ github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuy github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= +github.com/alicebob/miniredis/v2 v2.35.0 h1:QwLphYqCEAo1eu1TqPRN2jgVMPBweeQcR21jeqDCONI= +github.com/alicebob/miniredis/v2 v2.35.0/go.mod h1:TcL7YfarKPGDAthEtl5NBeHZfeUQj6OXMm/+iu5cLMM= github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= @@ -101,9 +103,12 @@ github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK3 github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= +github.com/cespare/xxhash v1.1.0 h1:a6HrQnmkObjyL+Gs60czilIUGqrzKutQD6XZog3p+ko= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732 h1:XYUCaZrW8ckGWlCRJKCSoh/iFwlpX316a8yY9IFEzv8= github.com/chromedp/cdproto v0.0.0-20240202021202-6d0b6a386732/go.mod h1:GKljq0VrfU4D5yc+2qA6OVr8pmO/MBbPEWqWQ/oqGEs= github.com/chromedp/chromedp v0.9.5 h1:viASzruPJOiThk7c5bueOUY91jGLJVximoEMGoH93rg= @@ -164,6 +169,8 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78= +github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc= github.com/dlclark/regexp2 v1.11.4 h1:rPYF9/LECdNymJufQKmri9gV604RvvABwgOA8un7yAo= github.com/dlclark/regexp2 v1.11.4/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8= github.com/dslipak/pdf v0.0.2 h1:djAvcM5neg9Ush+zR6QXB+VMJzR6TdnX766HPIg1JmI= @@ -529,6 +536,8 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= github.com/rcrowley/go-metrics v0.0.0-20201227073835-cf1acfcdf475/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/redis/go-redis/v9 v9.17.2 h1:P2EGsA4qVIM3Pp+aPocCJ7DguDHhqrXNhVcEp4ViluI= +github.com/redis/go-redis/v9 v9.17.2/go.mod h1:u410H11HMLoB+TP67dz8rL9s6QW2j76l0//kSOd3370= github.com/richardlehane/mscfb v1.0.4 h1:WULscsljNPConisD5hR0+OyZjwK46Pfyr6mPu5ZawpM= github.com/richardlehane/mscfb v1.0.4/go.mod h1:YzVpcZg9czvAuhk9T+a3avCpcFPMUWm7gK3DypaEsUk= github.com/richardlehane/msoleps v1.0.1/go.mod h1:BWev5JBpU9Ko2WAgmZEuiz4/u3ZYTKbjLycmwiWUfWg= @@ -626,6 +635,8 @@ github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yuin/gopher-lua v1.1.1 h1:kYKnWBjvbNP4XLT3+bPEwAXJx262OhaHDWDVOPjL46M= +github.com/yuin/gopher-lua v1.1.1/go.mod h1:GBR0iDaNXjAgGg9zfCvksxSRnQx76gclCIb7kdAd1Pw= go.etcd.io/etcd/api/v3 v3.5.0/go.mod h1:cbVKeC6lCfl7j/8jBhAK6aIYO9XOjdptoxU/nLQcPvs= go.etcd.io/etcd/client/pkg/v3 v3.5.0/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.0/go.mod h1:h9puh54ZTgAKtEbut2oe9P4L/oqKCVB6xsXlzd7alYQ=