diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml new file mode 100644 index 0000000..3ba5f53 --- /dev/null +++ b/.github/workflows/ci.yaml @@ -0,0 +1,51 @@ +name: ci + +on: + push: + branches: + - main + pull_request: + branches: + - main + +jobs: + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v4 + with: + go-version: stable + # Cache is managed by golangci-lint + # https://github.com/actions/setup-go#caching-dependency-files-and-build-outputs + cache: false + - uses: actions/checkout@v3 + - name: golangci-lint + uses: golangci/golangci-lint-action@v3.7.0 + with: + args: --timeout=4m + version: v1.57.1 + build: + runs-on: ubuntu-latest + steps: + - uses: actions/setup-go@v4 + with: + go-version: stable + - uses: actions/checkout@v3 + - name: Build + run: go build -v ./... + test: + runs-on: ubuntu-latest + environment: ci + steps: + - uses: actions/setup-go@v4 + with: + go-version: stable + - uses: actions/checkout@v3 + - name: Run tests with coverage + env: + OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }} + run: go test -race -coverprofile=coverage.txt -covermode=atomic ./... + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4-beta + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2d83068 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +coverage.out diff --git a/.golangci.yaml b/.golangci.yaml new file mode 100644 index 0000000..d14e2ea --- /dev/null +++ b/.golangci.yaml @@ -0,0 +1,49 @@ +linters: + presets: + - bugs + - comment + - complexity + - error + - format + - import + - metalinter + - module + - performance + - sql + - style + - test + - unused + disable: + - godox # We allow TODO lines. + - tagliatelle # As we're dealing with third parties we must accept snake case. + - wsl # We don't agree with wsl's style rules + - exhaustruct + - lll + - varnamelen + - nlreturn + - gomnd + - goerr113 + - wrapcheck # TODO: we should probably enable this one (at least for new code). + - testpackage + - nolintlint # see https://github.com/golangci/golangci-lint/issues/3228. + - depguard # disabling temporarily + - ireturn # disabling temporarily + +linters-settings: + cyclop: + max-complexity: 12 + funlen: + lines: 120 + depguard: + list-type: denylist + packages: + - github.com/samber/lo # Use exp packages or internal utilities instead. + additional-guards: + - list-type: denylist + include-go-root: false + packages: + - github.com/stretchr/testify + # Specify rules by which the linter ignores certain files for consideration. + ignore-file-rules: + - "**/*_test.go" + - "**/mock/**/*.go" diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..b874118 --- /dev/null +++ b/Makefile @@ -0,0 +1,31 @@ +# This file contains convenience targets for the project. +# It is not intended to be used as a build system. +# See the README for more information. + +.PHONY: test +test: + go test ./... + +.PHONY: lint +lint: lint-deps + golangci-lint run --color=always --sort-results ./... + +.PHONY: lint-fix +lint-fix: + golangci-lint run --fix --skip-dirs=./exp ./... + +.PHONY: test-race +test-race: + go run test -race ./... + +.PHONY: test-cover +test-cover: + go run test -cover ./... + +.PHONY: lint-deps +lint-deps: + @command -v golangci-lint >/dev/null 2>&1 || { \ + echo >&2 "golangci-lint not found. Installing..."; \ + go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.1; \ + } + diff --git a/go.mod b/go.mod index 857a2be..a644a6e 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module github.com/tmc/langgraphgo -go 1.21.5 +go 1.22 require github.com/tmc/langchaingo v0.1.7 diff --git a/graph/graph_test.go b/graph/graph_test.go index 6a04527..1e04d8a 100644 --- a/graph/graph_test.go +++ b/graph/graph_test.go @@ -28,9 +28,8 @@ func ExampleMessageGraph() { return append(state, llms.TextParts(schema.ChatMessageTypeAI, r.Choices[0].Content), ), nil - }) - g.AddNode(graph.END, func(ctx context.Context, state []llms.MessageContent) ([]llms.MessageContent, error) { + g.AddNode(graph.END, func(_ context.Context, state []llms.MessageContent) ([]llms.MessageContent, error) { return state, nil }) @@ -58,6 +57,7 @@ func ExampleMessageGraph() { } func TestMessageGraph(t *testing.T) { + t.Parallel() testCases := []struct { name string buildGraph func() *graph.MessageGraph @@ -69,10 +69,10 @@ func TestMessageGraph(t *testing.T) { name: "Simple graph", buildGraph: func() *graph.MessageGraph { g := graph.NewMessageGraph() - g.AddNode("node1", func(ctx context.Context, state []llms.MessageContent) ([]llms.MessageContent, error) { + g.AddNode("node1", func(_ context.Context, state []llms.MessageContent) ([]llms.MessageContent, error) { return append(state, llms.TextParts(schema.ChatMessageTypeAI, "Node 1")), nil }) - g.AddNode("node2", func(ctx context.Context, state []llms.MessageContent) ([]llms.MessageContent, error) { + g.AddNode("node2", func(_ context.Context, state []llms.MessageContent) ([]llms.MessageContent, error) { return append(state, llms.TextParts(schema.ChatMessageTypeAI, "Node 2")), nil }) g.AddEdge("node1", "node2") @@ -92,7 +92,7 @@ func TestMessageGraph(t *testing.T) { name: "Entry point not set", buildGraph: func() *graph.MessageGraph { g := graph.NewMessageGraph() - g.AddNode("node1", func(ctx context.Context, state []llms.MessageContent) ([]llms.MessageContent, error) { + g.AddNode("node1", func(_ context.Context, state []llms.MessageContent) ([]llms.MessageContent, error) { return state, nil }) return g @@ -103,7 +103,7 @@ func TestMessageGraph(t *testing.T) { name: "Node not found", buildGraph: func() *graph.MessageGraph { g := graph.NewMessageGraph() - g.AddNode("node1", func(ctx context.Context, state []llms.MessageContent) ([]llms.MessageContent, error) { + g.AddNode("node1", func(_ context.Context, state []llms.MessageContent) ([]llms.MessageContent, error) { return state, nil }) g.AddEdge("node1", "node2") @@ -116,7 +116,7 @@ func TestMessageGraph(t *testing.T) { name: "No outgoing edge", buildGraph: func() *graph.MessageGraph { g := graph.NewMessageGraph() - g.AddNode("node1", func(ctx context.Context, state []llms.MessageContent) ([]llms.MessageContent, error) { + g.AddNode("node1", func(_ context.Context, state []llms.MessageContent) ([]llms.MessageContent, error) { return state, nil }) g.SetEntryPoint("node1") @@ -128,19 +128,20 @@ func TestMessageGraph(t *testing.T) { name: "Error in node function", buildGraph: func() *graph.MessageGraph { g := graph.NewMessageGraph() - g.AddNode("node1", func(ctx context.Context, state []llms.MessageContent) ([]llms.MessageContent, error) { + g.AddNode("node1", func(_ context.Context, _ []llms.MessageContent) ([]llms.MessageContent, error) { return nil, errors.New("node error") }) g.AddEdge("node1", graph.END) g.SetEntryPoint("node1") return g }, - expectedError: fmt.Errorf("error in node node1: node error"), + expectedError: errors.New("error in node node1: node error"), }, } for _, tc := range testCases { t.Run(tc.name, func(t *testing.T) { + t.Parallel() g := tc.buildGraph() runnable, err := g.Compile() if err != nil { @@ -152,7 +153,7 @@ func TestMessageGraph(t *testing.T) { output, err := runnable.Invoke(context.Background(), tc.inputMessages) if err != nil { - if tc.expectedError == nil || fmt.Sprint(err) != fmt.Sprint(tc.expectedError) { + if tc.expectedError == nil || err.Error() != tc.expectedError.Error() { t.Fatalf("unexpected invoke error: '%v', expected '%v'", err, tc.expectedError) } return