From 2e523c30304f256bddc7c4e991ac962de809528e Mon Sep 17 00:00:00 2001 From: conneroisu Date: Fri, 25 Oct 2024 10:04:51 -0400 Subject: [PATCH] optimize readability in composio and make sure e2b is concurrent safe --- examples/composio-github-star/README.md | 18 ++- examples/composio-github-star/main.go | 5 +- extensions/composio/auth.go | 14 +- extensions/composio/composio | 1 - extensions/composio/composio.go | 2 +- extensions/composio/execute_test.go | 1 - extensions/composio/options.go | 22 ++- extensions/e2b/doc.go | 2 + extensions/e2b/model.go | 10 +- extensions/e2b/options.go | 41 ++++++ extensions/e2b/sandbox.go | 187 ++++++++++-------------- extensions/e2b/sandbox_test.go | 38 +++-- extensions/e2b/tools.go | 7 +- extensions/e2b/unit_test.go | 5 +- go.work.sum | 2 + 15 files changed, 186 insertions(+), 169 deletions(-) delete mode 160000 extensions/composio/composio create mode 100644 extensions/e2b/doc.go create mode 100644 extensions/e2b/options.go diff --git a/examples/composio-github-star/README.md b/examples/composio-github-star/README.md index 10f0571..898ae8d 100644 --- a/examples/composio-github-star/README.md +++ b/examples/composio-github-star/README.md @@ -1,13 +1,21 @@ # composio-github-star +Adapted from the [quickstart](https://docs.composio.dev/introduction/intro/quickstart) guide. + +Install the `composio` CLI and login to your account (also add github to your account if you haven't already) + ```bash +pip install -U composio_core composio_openai -pip install composio-langchain -pip install langchain-groq +composio login #Connect your Github so agents can use it composio add github - -#Check all different apps which you can connect with -composio apps ``` + +Congratulations! You’ve just: + + 🔐 Authenticated your GitHub account with Composio + 🛠 Fetched GitHub tools for the llm + ⭐ Instructed the AI to star the conneroisu/groq-go repository + ✅ Successfully executed the action on GitHub diff --git a/examples/composio-github-star/main.go b/examples/composio-github-star/main.go index 6e55094..19a230a 100644 --- a/examples/composio-github-star/main.go +++ b/examples/composio-github-star/main.go @@ -54,9 +54,8 @@ func run( { Role: groq.ChatMessageRoleUser, Content: ` -You are a github star bot. -You will be given a repo name and you will star it. -Star a repo conneroisu/groq-go on GitHub +You are a github star bot. You will be given a repo name and you will star it. +Star the repo conneroisu/groq-go on GitHub. `, }, }, diff --git a/extensions/composio/auth.go b/extensions/composio/auth.go index aa9c019..ce1f079 100644 --- a/extensions/composio/auth.go +++ b/extensions/composio/auth.go @@ -1,7 +1,5 @@ package composio -// https://backend.composio.dev/api/v1/connectedAccounts?user_uuid=default&showActiveOnly=true - import ( "context" "fmt" @@ -18,6 +16,8 @@ type ( GetConnectedAccounts(ctx context.Context, opts ...AuthOption) (ConnectedAccounts, error) } // ConnectedAccounts represents a composio connected account. + // + // Gotten from similar url to: https://backend.composio.dev/api/v1/connectedAccounts?user_uuid=default&showActiveOnly=true ConnectedAccounts struct { Items []struct { IntegrationID string `json:"integrationId"` @@ -72,13 +72,13 @@ func (c *Composio) GetConnectedAccounts(ctx context.Context, opts ...AuthOption) if err != nil { return ca, err } - ps := url.Values{} - ps.Add("user_uuid", "default") - ps.Add("showActiveOnly", "true") + urlValues := u.Query() + urlValues.Add("user_uuid", "default") + urlValues.Add("showActiveOnly", "true") for _, opt := range opts { - opt(u) + opt(&urlValues) } - u.RawQuery = ps.Encode() + u.RawQuery = urlValues.Encode() uri = u.String() c.logger.Debug("auth", "url", uri) req, err := builders.NewRequest( diff --git a/extensions/composio/composio b/extensions/composio/composio deleted file mode 160000 index f26383d..0000000 --- a/extensions/composio/composio +++ /dev/null @@ -1 +0,0 @@ -Subproject commit f26383d2953cd0f73453b90be388261335e9e2b2 diff --git a/extensions/composio/composio.go b/extensions/composio/composio.go index d9a4996..451451b 100644 --- a/extensions/composio/composio.go +++ b/extensions/composio/composio.go @@ -37,7 +37,7 @@ type ( ) // NewComposer creates a new composio client. -func NewComposer(apiKey string, opts ...ComposerOption) (*Composio, error) { +func NewComposer(apiKey string, opts ...Option) (*Composio, error) { c := &Composio{ apiKey: apiKey, header: builders.Header{SetCommonHeaders: func(r *http.Request) { diff --git a/extensions/composio/execute_test.go b/extensions/composio/execute_test.go index 88c5d03..98b7309 100644 --- a/extensions/composio/execute_test.go +++ b/extensions/composio/execute_test.go @@ -25,7 +25,6 @@ func TestRun(t *testing.T) { a.NoError(err) ts, err := client.GetTools( ctx, WithApp("GITHUB"), WithUseCase("StarRepo")) - t.Logf("%+v\n", len(ts)) a.NoError(err) a.NotEmpty(ts) groqClient, err := groq.NewClient( diff --git a/extensions/composio/options.go b/extensions/composio/options.go index 633eb89..2bb06f5 100644 --- a/extensions/composio/options.go +++ b/extensions/composio/options.go @@ -8,26 +8,26 @@ import ( ) type ( - // ComposerOption is an option for the composio client. + // Option is an option for the composio client. // // WithLogger sets the logger for the composio client. - ComposerOption func(*Composio) + Option func(*Composio) // ToolsOption is an option for the tools request. ToolsOption func(*url.Values) // AuthOption is an option for the auth request. - AuthOption func(*url.URL) + AuthOption func(*url.Values) ) // Composer Options // WithLogger sets the logger for the composio client. -func WithLogger(logger *slog.Logger) ComposerOption { +func WithLogger(logger *slog.Logger) Option { return func(c *Composio) { c.logger = logger } } -// Tool Options +// Get Tool Options // WithTags sets the tags for the tools request. func WithTags(tags ...string) ToolsOption { @@ -53,18 +53,14 @@ func WithUseCase(useCase string) ToolsOption { // WithShowActiveOnly sets the show active only for the auth request. func WithShowActiveOnly(showActiveOnly bool) AuthOption { - return func(u *url.URL) { - ps := u.Query() - ps.Add("showActiveOnly", fmt.Sprintf("%t", showActiveOnly)) - u.RawQuery = ps.Encode() + return func(u *url.Values) { + u.Set("showActiveOnly", fmt.Sprintf("%t", showActiveOnly)) } } // WithUserUUID sets the user uuid for the auth request. func WithUserUUID(userUUID string) AuthOption { - return func(u *url.URL) { - ps := u.Query() - ps.Add("user_uuid", userUUID) - u.RawQuery = ps.Encode() + return func(u *url.Values) { + u.Set("user_uuid", userUUID) } } diff --git a/extensions/e2b/doc.go b/extensions/e2b/doc.go new file mode 100644 index 0000000..8a2f191 --- /dev/null +++ b/extensions/e2b/doc.go @@ -0,0 +1,2 @@ +// Package e2b provides an e2b client for groq-go. +package e2b diff --git a/extensions/e2b/model.go b/extensions/e2b/model.go index 90311cc..acecbed 100644 --- a/extensions/e2b/model.go +++ b/extensions/e2b/model.go @@ -14,7 +14,8 @@ type ( Read(ctx context.Context) error io.Closer } - // Identifier is an interface for a constantly running process to identify new request ids. + // Identifier is an interface for a constantly running process to + // identify new request ids. Identifier interface { Identify(ctx context.Context) } @@ -50,11 +51,8 @@ type ( cmd string, timeout time.Duration, ) - Subscribe( - ctx context.Context, - event ProcessEvents, - eCh chan<- Event, - ) + SubscribeStdout() (events chan Event, err error) + SubscribeStderr() (events chan Event, err error) } // Watcher is an interface for a instance that can watch a filesystem. Watcher interface { diff --git a/extensions/e2b/options.go b/extensions/e2b/options.go new file mode 100644 index 0000000..21543a8 --- /dev/null +++ b/extensions/e2b/options.go @@ -0,0 +1,41 @@ +package e2b + +import ( + "log/slog" + "net/http" +) + +// WithBaseURL sets the base URL for the e2b sandbox. +func WithBaseURL(baseURL string) Option { + return func(s *Sandbox) { s.baseURL = baseURL } +} + +// WithClient sets the client for the e2b sandbox. +func WithClient(client *http.Client) Option { + return func(s *Sandbox) { s.client = client } +} + +// WithLogger sets the logger for the e2b sandbox. +func WithLogger(logger *slog.Logger) Option { + return func(s *Sandbox) { s.logger = logger } +} + +// WithTemplate sets the template for the e2b sandbox. +func WithTemplate(template SandboxTemplate) Option { + return func(s *Sandbox) { s.Template = template } +} + +// WithMetaData sets the meta data for the e2b sandbox. +func WithMetaData(metaData map[string]string) Option { + return func(s *Sandbox) { s.Metadata = metaData } +} + +// WithCwd sets the current working directory. +func WithCwd(cwd string) Option { + return func(s *Sandbox) { s.Cwd = cwd } +} + +// WithWsURL sets the websocket url for the e2b sandbox. +func WithWsURL(wsURL func(s *Sandbox) string) Option { + return func(s *Sandbox) { s.wsURL = wsURL } +} diff --git a/extensions/e2b/sandbox.go b/extensions/e2b/sandbox.go index 0bd5a31..480204e 100644 --- a/extensions/e2b/sandbox.go +++ b/extensions/e2b/sandbox.go @@ -444,7 +444,13 @@ func (p *Process) Start(ctx context.Context) (err error) { p.Env = map[string]string{"PYTHONUNBUFFERED": "1"} } respCh := make(chan []byte) - if err = p.sb.writeRequest(ctx, processStart, []any{p.id, p.cmd, p.Env, p.Cwd}, respCh); err != nil { + err = p.sb.writeRequest( + ctx, + processStart, + []any{p.id, p.cmd, p.Env, p.Cwd}, + respCh, + ) + if err != nil { return err } p.ctx = ctx @@ -479,83 +485,77 @@ func (p *Process) Done() <-chan struct{} { } // SubscribeStdout subscribes to the process's stdout. -func (p *Process) SubscribeStdout() (events chan Event, err error) { +func (p *Process) SubscribeStdout(events chan Event) (err error) { err = p.subscribe(p.ctx, OnStdout, events) return } // SubscribeStderr subscribes to the process's stderr. -func (p *Process) SubscribeStderr() (events chan Event, err error) { +func (p *Process) SubscribeStderr(events chan Event) (err error) { err = p.subscribe(p.ctx, OnStderr, events) return } // SubscribeExit subscribes to the process's exit. -func (p *Process) SubscribeExit() (events chan Event, err error) { +func (p *Process) SubscribeExit(events chan Event) (err error) { err = p.subscribe(p.ctx, OnExit, events) return } // Subscribe subscribes to a process event. // -// It creates a go routine to read the process events. +// It creates a go routine to read the process events into the provided channel. func (p *Process) subscribe( ctx context.Context, event ProcessEvents, eCh chan<- Event, ) error { - respCh := make(chan []byte) - err := p.sb.writeRequest(ctx, processSubscribe, []any{event, p.id}, respCh) - if err != nil { - return err - } - res, err := decodeResponse[string, APIError](<-respCh) - if err != nil { - return err - } - if res.Error.Code != 0 { - return fmt.Errorf("process subscribe failed(%d): %s", res.Error.Code, res.Error.Message) - } - eventByCh := make(chan []byte) - p.sb.Map.Store(res.Result, eventByCh) - for { - select { - case eventBd := <-eventByCh: - var event Event - err = json.Unmarshal(eventBd, &event) - if err != nil { - return err - } - if event.Error != "" { - return fmt.Errorf("failed to read event: %s", event.Error) - } - if event.Params.Subscription != res.Result { - return fmt.Errorf("subscription id mismatch") - } - eCh <- event - case <-ctx.Done(): - close(eventByCh) - p.sb.Map.Delete(res.Result) - finishCtx, cancel := context.WithCancel(context.Background()) - defer cancel() - p.sb.logger.Debug("unsubscribing from process", "event", event, "id", res.Result) - err = p.sb.writeRequest(finishCtx, processUnsubscribe, []any{res.Result}, respCh) - if err != nil { - return err - } - unsubRes, err := decodeResponse[bool, string](<-respCh) - if err != nil { - return err - } - if unsubRes.Error != "" || !unsubRes.Result { - return fmt.Errorf("failed to unsubscribe from process: %s", unsubRes.Error) + errCh := make(chan error) + go func(errCh chan error) { + respCh := make(chan []byte) + defer close(respCh) + err := p.sb.writeRequest(ctx, processSubscribe, []any{event, p.id}, respCh) + if err != nil || respCh == nil { + errCh <- err + } + res, err := decodeResponse[string, any](<-respCh) + errCh <- err + if err != nil { + return + } + p.sb.Map.Store(res.Result, respCh) + for { + select { + case eventBd := <-respCh: + p.sb.logger.Debug("eventByCh", "event", string(eventBd)) + var event Event + _ = json.Unmarshal(eventBd, &event) + if event.Error != "" { + p.sb.logger.Debug("failed to read event", "error", event.Error) + continue + } + if event.Params.Subscription != res.Result { + p.sb.logger.Debug("subscription id mismatch", "expected", res.Result, "got", event.Params.Subscription) + continue + } + eCh <- event + case <-ctx.Done(): + p.sb.Map.Delete(res.Result) + finishCtx, cancel := context.WithCancel(context.Background()) + defer cancel() + p.sb.logger.Debug("unsubscribing from process", "event", event, "id", res.Result) + _ = p.sb.writeRequest(finishCtx, processUnsubscribe, []any{res.Result}, respCh) + unsubRes, _ := decodeResponse[bool, string](<-respCh) + if unsubRes.Error != "" || !unsubRes.Result { + p.sb.logger.Debug("failed to unsubscribe from process", "error", unsubRes.Error) + } + return + case <-p.Done(): + return } - return nil - // TODO: make this a timeout that comes from a function param. - case <-p.Done(): - return nil } - } + }(errCh) + return <-errCh } func (s *Sandbox) sendRequest(req *http.Request, v interface{}) error { req.Header.Set("Accept", "application/json") @@ -587,42 +587,6 @@ func (s *Sandbox) sendRequest(req *http.Request, v interface{}) error { return json.NewDecoder(res.Body).Decode(v) } } - -// WithBaseURL sets the base URL for the e2b sandbox. -func WithBaseURL(baseURL string) Option { - return func(s *Sandbox) { s.baseURL = baseURL } -} - -// WithClient sets the client for the e2b sandbox. -func WithClient(client *http.Client) Option { - return func(s *Sandbox) { s.client = client } -} - -// WithLogger sets the logger for the e2b sandbox. -func WithLogger(logger *slog.Logger) Option { - return func(s *Sandbox) { s.logger = logger } -} - -// WithTemplate sets the template for the e2b sandbox. -func WithTemplate(template SandboxTemplate) Option { - return func(s *Sandbox) { s.Template = template } -} - -// WithMetaData sets the meta data for the e2b sandbox. -func WithMetaData(metaData map[string]string) Option { - return func(s *Sandbox) { s.Metadata = metaData } -} - -// WithCwd sets the current working directory. -func WithCwd(cwd string) Option { - return func(s *Sandbox) { s.Cwd = cwd } -} - -// WithWsURL sets the websocket url for the e2b sandbox. -func WithWsURL(wsURL func(s *Sandbox) string) Option { - return func(s *Sandbox) { s.wsURL = wsURL } -} - func decodeResponse[T any, Q any](body []byte) (*Response[T, Q], error) { decResp := new(Response[T, Q]) err := json.Unmarshal(body, decResp) @@ -644,44 +608,55 @@ func (s *Sandbox) identify(ctx context.Context) { } } func (s *Sandbox) read(ctx context.Context) (err error) { - var key any + var body []byte defer func() { err = s.ws.Close() }() msgCh := make(chan []byte, 10) for { select { - case body := <-msgCh: + case body = <-msgCh: var decResp decResp err = json.Unmarshal(body, &decResp) if err != nil { return err } - s.logger.Debug("read", - "id", decResp.ID, - "body", body, - "sandbox", s.ID, - ) if decResp.Params.Subscription != "" { - key = decResp.Params.Subscription + toR, ok := s.Map.Load(decResp.Params.Subscription) + if !ok { + msgCh <- body + continue + } + toRCh, ok := toR.(chan []byte) + if !ok { + msgCh <- body + continue + } + s.logger.Debug("read", + "subscription", decResp.Params.Subscription, + "body", body, + "sandbox", s.ID, + ) + toRCh <- body } if decResp.ID != 0 { - key = decResp.ID - } - if key != nil { - // response has an id - toR, ok := s.Map.Load(key) + toR, ok := s.Map.Load(decResp.ID) if !ok { + msgCh <- body continue } toRCh, ok := toR.(chan []byte) if !ok { + msgCh <- body continue } + s.logger.Debug("read", + "id", decResp.ID, + "body", body, + "sandbox", s.ID, + ) toRCh <- body - continue } - msgCh <- body case <-ctx.Done(): return ctx.Err() default: diff --git a/extensions/e2b/sandbox_test.go b/extensions/e2b/sandbox_test.go index 72ee8e4..385ebf1 100644 --- a/extensions/e2b/sandbox_test.go +++ b/extensions/e2b/sandbox_test.go @@ -16,6 +16,8 @@ import ( var upgrader = websocket.Upgrader{} +const subID = "test-sub-id" + func echo(a *assert.Assertions) func(w http.ResponseWriter, r *http.Request) { mu := sync.Mutex{} return func(w http.ResponseWriter, r *http.Request) { @@ -29,7 +31,7 @@ func echo(a *assert.Assertions) func(w http.ResponseWriter, r *http.Request) { for { mt, message, err := c.ReadMessage() a.NoError(err) - defaultLogger.Debug("server read message", "msg", message) + test.DefaultLogger.Debug("server read message", "msg", message) req := decode(message) switch req.Method { case filesystemList: @@ -69,16 +71,12 @@ func echo(a *assert.Assertions) func(w http.ResponseWriter, r *http.Request) { err = c.WriteMessage(mt, encode(Response[string, APIError]{ ID: req.ID, Error: APIError{}, - Result: "test-proc-id", + Result: subID, })) a.NoError(err) - err = c.WriteMessage(mt, encode(Response[ - EventParams, APIError, - ]{ - ID: req.ID, - Error: APIError{}, - Result: EventParams{ - Subscription: "test-proc-id", + err = c.WriteMessage(mt, encode(Event{ + Params: EventParams{ + Subscription: subID, Result: EventResult{ Type: "Stdout", Line: "hello", @@ -95,8 +93,6 @@ func echo(a *assert.Assertions) func(w http.ResponseWriter, r *http.Request) { Error: APIError{}, Result: "", })) - default: - err = c.WriteMessage(mt, message) a.NoError(err) } } @@ -139,7 +135,7 @@ func TestNewSandbox(t *testing.T) { sb, err := NewSandbox( ctx, test.GetTestToken(), - WithLogger(defaultLogger), + WithLogger(test.DefaultLogger), WithBaseURL(ts.URL), WithWsURL(func(_ *Sandbox) string { return u + "/ws" @@ -148,30 +144,30 @@ func TestNewSandbox(t *testing.T) { a.NoError(err, "NewSandbox error") a.NotNil(sb, "NewSandbox returned nil") a.Equal(sb.ID, id) - // Call ls on the sandbox. + lsRes, err := sb.Ls(ctx, ".") a.NoError(err) a.NotEmpty(lsRes) - // Call mkdir on the sandbox. + err = sb.Mkdir(ctx, "hello") a.NoError(err) - // Call write on the sandbox. + err = sb.Write(ctx, "hello.txt", []byte("hello")) a.NoError(err) - // Call read on the sandbox. + readRes, err := sb.Read(ctx, "hello.txt") a.NoError(err) a.Equal("hello", readRes) - // create a process + proc, err := sb.NewProcess("sleep 5 && echo 'hello world!'", Process{}) a.NoError(err) - // start the process + err = proc.Start(ctx) a.NoError(err) - // subscribe to the process's stdout - events, err := proc.SubscribeStdout() + e := make(chan Event) + err = proc.SubscribeStdout(e) a.NoError(err) - event := <-events + event := <-e jsnBytes, err := json.MarshalIndent(&event, "", " ") a.NoError(err) t.Logf("test got event: %s", string(jsnBytes)) diff --git a/extensions/e2b/tools.go b/extensions/e2b/tools.go index 9e55910..a3811cf 100644 --- a/extensions/e2b/tools.go +++ b/extensions/e2b/tools.go @@ -111,11 +111,12 @@ var ( if err != nil { return groq.ChatCompletionMessage{}, err } - stdevents, err := proc.SubscribeStdout() + e := make(chan Event, 10) + err = proc.SubscribeStdout(e) if err != nil { return groq.ChatCompletionMessage{}, err } - _, err = proc.SubscribeStderr() + err = proc.SubscribeStderr(e) if err != nil { return groq.ChatCompletionMessage{}, err } @@ -129,7 +130,7 @@ var ( select { case <-ctx.Done(): return - case event := <-stdevents: + case event := <-e: buf.Write([]byte(event.Params.Result.Line)) case <-proc.Done(): break diff --git a/extensions/e2b/unit_test.go b/extensions/e2b/unit_test.go index e9d41c7..9da5758 100644 --- a/extensions/e2b/unit_test.go +++ b/extensions/e2b/unit_test.go @@ -104,9 +104,10 @@ func TestCreateProcess(t *testing.T) { a.NoError(err) ctx, cancel := context.WithTimeout(ctx, time.Second*6) defer cancel() - stdoutEvents, err := proc.SubscribeStdout() + stdOutEvents := make(chan e2b.Event) + err = proc.SubscribeStdout(stdOutEvents) a.NoError(err) - event := <-stdoutEvents + event := <-stdOutEvents jsonBytes, err := json.MarshalIndent(&event, "", " ") if err != nil { a.Error(err) diff --git a/go.work.sum b/go.work.sum index 665e91f..593da36 100644 --- a/go.work.sum +++ b/go.work.sum @@ -126,6 +126,7 @@ golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQz golang.org/x/mod v0.17.0 h1:zY54UmvipHiNd+pm+m0x9KhZ9hl1/7QNMyxXbc6ICqA= golang.org/x/mod v0.17.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.18.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= +golang.org/x/mod v0.21.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.25.0/go.mod h1:JkAGAh7GEvH74S6FOH42FLoXpXbE/aqXSrIQjXgsiwM= golang.org/x/net v0.26.0/go.mod h1:5YKkiSynbBIh3p6iOc/vibscux0x38BZDkn8sCUPxHE= @@ -150,6 +151,7 @@ golang.org/x/tools v0.13.0/go.mod h1:HvlwmtVNQAhOuCjW7xxvovg8wbNq7LwfXh/k7wXUl58 golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d h1:vU5i/LfpvrRCpgM/VPfJLg5KjxD3E+hfT1SH+d9zLwg= golang.org/x/tools v0.21.1-0.20240508182429-e35e4ccd0d2d/go.mod h1:aiJjzUbINMkxbQROHiO6hDPo2LHcIPhhQsa9DLh0yGk= golang.org/x/tools v0.22.0/go.mod h1:aCwcsjqvq7Yqt6TNyX7QMU2enbQ/Gt0bo6krSeEri+c= +golang.org/x/tools v0.26.0/go.mod h1:TPVVj70c7JJ3WCazhD8OdXcZg/og+b9+tH/KxylGwH0= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20230920204549-e6e6cdab5c13/go.mod h1:CCviP9RmpZ1mxVr8MUjCnSiY09IbAXZxhLE6EhHIdPU= gopkg.in/inf.v0 v0.9.1/go.mod h1:cWUDdTG/fYaXco+Dcufb5Vnc6Gp2YChqWtbxRZE0mXw=