Repository: tmc/langchaingo
Pull request #722 (2024-03-26 20:28:50)

tooling: Update minimum go version to 1.22, update golangci-lint
This updates the Go minimum version in go.mod to 1.22 and updates golangci-lint to 1.57.1 

See https://go.dev/doc/modules/gomod-ref and https://go.dev/doc/toolchain for more info.


Patch:
diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml
index 96e35ce2..4dd493df 100644
--- a/.github/workflows/ci.yaml
+++ b/.github/workflows/ci.yaml
@@ -23,7 +23,7 @@ jobs:
         uses: golangci/golangci-lint-action@v3.7.0
         with:
           args: --timeout=4m
-          version: v1.55.1
+          version: v1.57.1
   build-examples:
     runs-on: ubuntu-latest
     steps:
diff --git a/.golangci.yaml b/.golangci.yaml
index 860de57f..f47e374d 100644
--- a/.golangci.yaml
+++ b/.golangci.yaml
@@ -28,6 +28,8 @@ linters:
     - nolintlint # see https://github.com/golangci/golangci-lint/issues/3228.
     - depguard # disabling temporarily
     - ireturn # disabling temporarily
+    - perfsprint
+    - musttag
 
 linters-settings:
   cyclop:
@@ -48,5 +50,5 @@ linters-settings:
           - "**/*_test.go"
           - "**/mock/**/*.go"
 run:
-  skip-dirs:
+  exclude-dirs:
     - 'exp'
diff --git a/Makefile b/Makefile
index 94d15076..868b9f28 100644
--- a/Makefile
+++ b/Makefile
@@ -26,7 +26,7 @@ lint-all:
 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.55.1; \
+		go install github.com/golangci/golangci-lint/cmd/golangci-lint@v1.57.1; \
 	}
 
 .PHONY: docs
diff --git a/go.mod b/go.mod
index 66dcaa76..a89054ad 100644
--- a/go.mod
+++ b/go.mod
@@ -1,8 +1,6 @@
 module github.com/tmc/langchaingo
 
-go 1.21
-
-toolchain go1.21.4
+go 1.22
 
 require (
 	github.com/google/uuid v1.6.0
diff --git a/llms/cache/cache.go b/llms/cache/cache.go
index 0e1ddc95..9a47bd80 100644
--- a/llms/cache/cache.go
+++ b/llms/cache/cache.go
@@ -81,19 +81,16 @@ func (c *Cacher) GenerateContent(ctx context.Context, messages []llms.MessageCon
 	return response, nil
 }
 
-// hashKeyForCache implements a hair-brained hashing scheme for the parameters to `GenerateContent`.
-// It simply marshals all parameters as JSON and hashes the result.
+// hashKeyForCache is a helper function that generates a unique key for a given
+// set of messages and call options.
 func hashKeyForCache(messages []llms.MessageContent, opts llms.CallOptions) (string, error) {
 	hash := sha256.New()
 	enc := json.NewEncoder(hash)
-
 	if err := enc.Encode(messages); err != nil {
 		return "", err
 	}
-
 	if err := enc.Encode(opts); err != nil {
 		return "", err
 	}
-
 	return hex.EncodeToString(hash.Sum(nil)), nil
 }
diff --git a/llms/cloudflare/cloudflarellm.go b/llms/cloudflare/cloudflarellm.go
index aa6faeeb..0ecc270c 100644
--- a/llms/cloudflare/cloudflarellm.go
+++ b/llms/cloudflare/cloudflarellm.go
@@ -87,7 +87,7 @@ func (o *LLM) GenerateContent(ctx context.Context, messages []llms.MessageConten
 		// Look at all the parts in mc; expect to find a single Text part and
 		// any number of binary parts.
 		var text string
-		foundText := false
+		var foundText bool
 
 		for _, p := range mc.Parts {
 			switch pt := p.(type) {
diff --git a/textsplitter/token_splitter.go b/textsplitter/token_splitter.go
index 04b54c3c..fc150385 100644
--- a/textsplitter/token_splitter.go
+++ b/textsplitter/token_splitter.go
@@ -62,20 +62,20 @@ func (s TokenSplitter) SplitText(text string) ([]string, error) {
 
 func (s TokenSplitter) splitText(text string, tk *tiktoken.Tiktoken) []string {
 	splits := make([]string, 0)
-	inputIds := tk.Encode(text, s.AllowedSpecial, s.DisallowedSpecial)
+	inputIDs := tk.Encode(text, s.AllowedSpecial, s.DisallowedSpecial)
 
 	startIdx := 0
-	curIdx := len(inputIds)
+	curIdx := len(inputIDs)
 	if startIdx+s.ChunkSize < curIdx {
 		curIdx = startIdx + s.ChunkSize
 	}
-	for startIdx < len(inputIds) {
-		chunkIds := inputIds[startIdx:curIdx]
-		splits = append(splits, tk.Decode(chunkIds))
+	for startIdx < len(inputIDs) {
+		chunkIDs := inputIDs[startIdx:curIdx]
+		splits = append(splits, tk.Decode(chunkIDs))
 		startIdx += s.ChunkSize - s.ChunkOverlap
 		curIdx = startIdx + s.ChunkSize
-		if curIdx > len(inputIds) {
-			curIdx = len(inputIds)
+		if curIdx > len(inputIDs) {
+			curIdx = len(inputIDs)
 		}
 	}
 	return splits
diff --git a/vectorstores/qdrant/rest.go b/vectorstores/qdrant/rest.go
index e94f3562..e4fee5d2 100644
--- a/vectorstores/qdrant/rest.go
+++ b/vectorstores/qdrant/rest.go
@@ -27,7 +27,7 @@ func (s Store) upsertPoints(
 
 	payload := upsertBody{
 		Batch: upsertBatch{
-			Ids:      ids,
+			IDs:      ids,
 			Vectors:  vectors,
 			Payloads: payloads,
 		},
diff --git a/vectorstores/qdrant/schema.go b/vectorstores/qdrant/schema.go
index 07a77eb3..a918b236 100644
--- a/vectorstores/qdrant/schema.go
+++ b/vectorstores/qdrant/schema.go
@@ -6,7 +6,7 @@
 package qdrant
 
 type upsertBatch struct {
-	Ids      []string                 `json:"ids"`
+	IDs      []string                 `json:"ids"`
 	Payloads []map[string]interface{} `json:"payloads"`
 	Vectors  [][]float32              `json:"vectors"`
 }


Test patch:
diff --git a/chains/chains_test.go b/chains/chains_test.go
index 409b4195..4b1296fd 100644
--- a/chains/chains_test.go
+++ b/chains/chains_test.go
@@ -101,12 +101,16 @@ func TestApplyWithCanceledContext(t *testing.T) {
 	wg.Add(1)
 	c := NewLLMChain(&testLanguageModel{simulateWork: time.Second}, prompts.NewPromptTemplate("test", nil))
 
+	var applyErr error
 	go func() {
 		defer wg.Done()
-		_, err := Apply(ctx, c, inputs, maxWorkers)
-		require.Error(t, err)
+		_, applyErr = Apply(ctx, c, inputs, maxWorkers)
 	}()
 
 	cancelFunc()
 	wg.Wait()
+
+	if applyErr == nil || applyErr.Error() != "context canceled" {
+		t.Fatal("expected context canceled error, got:", applyErr)
+	}
 }
diff --git a/chains/sequential_test.go b/chains/sequential_test.go
index 9e1d0fb9..146dcbfa 100644
--- a/chains/sequential_test.go
+++ b/chains/sequential_test.go
@@ -66,7 +66,6 @@ func TestSimpleSequentialErrors(t *testing.T) {
 	}
 
 	for _, tc := range testCases {
-		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
 			c, err := NewSimpleSequentialChain([]Chain{tc.chain})
@@ -179,7 +178,6 @@ func TestSequentialChainErrors(t *testing.T) {
 	}
 
 	for _, tc := range testCases {
-		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
 			c, err := NewSequentialChain(tc.chains, []string{"input1", "input2"}, []string{"output"}, tc.seqChainOpts...)
diff --git a/chains/stuff_documents_test.go b/chains/stuff_documents_test.go
index 25e1a75e..756a02c3 100644
--- a/chains/stuff_documents_test.go
+++ b/chains/stuff_documents_test.go
@@ -86,7 +86,6 @@ func TestStuffDocuments_joinDocs(t *testing.T) {
 	chain := NewStuffDocuments(&LLMChain{})
 
 	for _, tc := range testcases {
-		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
 			got := chain.joinDocuments(tc.docs)
diff --git a/jsonschema/json_test.go b/jsonschema/json_test.go
index ee4de608..8f4d0bf6 100644
--- a/jsonschema/json_test.go
+++ b/jsonschema/json_test.go
@@ -172,7 +172,6 @@ func TestDefinition_MarshalJSON(t *testing.T) { //nolint:funlen
 	}
 
 	for _, tt := range tests {
-		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
 			wantBytes := []byte(tt.want)
@@ -184,7 +183,7 @@ func TestDefinition_MarshalJSON(t *testing.T) { //nolint:funlen
 			}
 
 			got := structToMap(t, tt.def)
-			gotPtr := structToMap(t, &tt.def)
+			gotPtr := structToMap(t, &tt.def) //#nosec G601 -- false positive now that we're on go 1.22+
 
 			if !reflect.DeepEqual(got, want) {
 				t.Errorf("MarshalJSON() got = %v, want %v", got, want)
diff --git a/llms/cache/cache_test.go b/llms/cache/cache_test.go
index 03662455..4ca47af1 100644
--- a/llms/cache/cache_test.go
+++ b/llms/cache/cache_test.go
@@ -11,8 +11,33 @@ import (
 func TestCache_hashKeyForCache(t *testing.T) {
 	t.Parallel()
 
-	rq := require.New(t)
-
+	cases := []struct {
+		name        string
+		v1          []llms.MessageContent
+		v1opt       []llms.CallOption
+		v2          []llms.MessageContent
+		shouldMatch bool
+	}{
+		{
+			name:        "empty",
+			v1:          []llms.MessageContent{},
+			v2:          []llms.MessageContent{},
+			shouldMatch: true,
+		},
+		{
+			name:        "empty vs non-empty",
+			v1:          []llms.MessageContent{},
+			v2:          []llms.MessageContent{{}},
+			shouldMatch: false,
+		},
+		{
+			name:        "different options",
+			v1:          []llms.MessageContent{{}},
+			v1opt:       []llms.CallOption{llms.WithCandidateCount(1)},
+			v2:          []llms.MessageContent{{}},
+			shouldMatch: false,
+		},
+	}
 	mustHashKeyForCache := func(messages []llms.MessageContent, options ...llms.CallOption) string {
 		var opts llms.CallOptions
 		for _, opt := range options {
@@ -26,26 +51,16 @@ func TestCache_hashKeyForCache(t *testing.T) {
 
 		return key
 	}
-
-	rq.Equal(
-		mustHashKeyForCache([]llms.MessageContent{}),
-		mustHashKeyForCache([]llms.MessageContent{}),
-	)
-
-	rq.Equal(
-		mustHashKeyForCache([]llms.MessageContent{}, llms.WithCandidateCount(1)),
-		mustHashKeyForCache([]llms.MessageContent{}, llms.WithCandidateCount(1)),
-	)
-
-	rq.NotEqual(
-		mustHashKeyForCache([]llms.MessageContent{{}}),
-		mustHashKeyForCache([]llms.MessageContent{}),
-	)
-
-	rq.NotEqual(
-		mustHashKeyForCache([]llms.MessageContent{}, llms.WithCandidateCount(1)),
-		mustHashKeyForCache([]llms.MessageContent{}),
-	)
+	for _, tc := range cases {
+		t.Run(tc.name, func(t *testing.T) {
+			t.Parallel()
+			v1hash := mustHashKeyForCache(tc.v1, tc.v1opt...)
+			v2hash := mustHashKeyForCache(tc.v2)
+			if (v1hash == v2hash) != tc.shouldMatch {
+				t.Fatalf("expected %v, got %v", tc.shouldMatch, v1hash == v2hash)
+			}
+		})
+	}
 }
 
 func TestCache_Call(t *testing.T) {
diff --git a/llms/cloudflare/internal/cloudflareclient/api_test.go b/llms/cloudflare/internal/cloudflareclient/api_test.go
index d2385cc2..0d918cac 100644
--- a/llms/cloudflare/internal/cloudflareclient/api_test.go
+++ b/llms/cloudflare/internal/cloudflareclient/api_test.go
@@ -97,7 +97,7 @@ func TestClient_GenerateContent(t *testing.T) { // nolint:funlen
 						{Role: "user", Content: "userPrompt"},
 					},
 					Stream: true,
-					StreamingFunc: func(ctx context.Context, chunk []byte) error {
+					StreamingFunc: func(_ context.Context, chunk []byte) error {
 						if string(chunk) != `{"result": {"response": "response"}}` {
 							return io.EOF
 						}
diff --git a/llms/ernie/internal/ernieclient/ernieclient_test.go b/llms/ernie/internal/ernieclient/ernieclient_test.go
index 6cf9fda6..6fe619a3 100644
--- a/llms/ernie/internal/ernieclient/ernieclient_test.go
+++ b/llms/ernie/internal/ernieclient/ernieclient_test.go
@@ -32,7 +32,6 @@ func TestClient_buildURL(t *testing.T) {
 	}
 
 	for _, tt := range tests {
-		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
 			c := &Client{
diff --git a/llms/generatecontent_test.go b/llms/generatecontent_test.go
index 611f50d2..fd4e2618 100644
--- a/llms/generatecontent_test.go
+++ b/llms/generatecontent_test.go
@@ -28,7 +28,6 @@ func TestTextParts(t *testing.T) {
 		}},
 	}
 	for _, tt := range tests {
-		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
 			if got := TextParts(tt.args.role, tt.args.parts...); !reflect.DeepEqual(got, tt.want) {
diff --git a/llms/huggingface/internal/huggingfaceclient/huggingfaceclient_test.go b/llms/huggingface/internal/huggingfaceclient/huggingfaceclient_test.go
index 05cb1091..d36e6620 100644
--- a/llms/huggingface/internal/huggingfaceclient/huggingfaceclient_test.go
+++ b/llms/huggingface/internal/huggingfaceclient/huggingfaceclient_test.go
@@ -35,7 +35,6 @@ func TestRunInference(t *testing.T) {
 	t.Cleanup(server.Close)
 
 	for _, tc := range tests {
-		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
 			client, err := New("token", "model", server.URL)
diff --git a/llms/llamafile/llamafilellm_test.go b/llms/llamafile/llamafilellm_test.go
index 969c8f64..4f20037d 100644
--- a/llms/llamafile/llamafilellm_test.go
+++ b/llms/llamafile/llamafilellm_test.go
@@ -62,7 +62,7 @@ func TestWithStreaming(t *testing.T) {
 
 	var sb strings.Builder
 	rsp, err := llm.GenerateContent(context.Background(), content,
-		llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error {
+		llms.WithStreamingFunc(func(_ context.Context, chunk []byte) error {
 			sb.Write(chunk)
 			return nil
 		}))
diff --git a/llms/ollama/ollama_test.go b/llms/ollama/ollama_test.go
index 47c79f15..74813f1d 100644
--- a/llms/ollama/ollama_test.go
+++ b/llms/ollama/ollama_test.go
@@ -94,7 +94,7 @@ func TestWithStreaming(t *testing.T) {
 
 	var sb strings.Builder
 	rsp, err := llm.GenerateContent(context.Background(), content,
-		llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error {
+		llms.WithStreamingFunc(func(_ context.Context, chunk []byte) error {
 			sb.Write(chunk)
 			return nil
 		}))
diff --git a/llms/openai/internal/openaiclient/chat_test.go b/llms/openai/internal/openaiclient/chat_test.go
index 1f254e4d..383d90c3 100644
--- a/llms/openai/internal/openaiclient/chat_test.go
+++ b/llms/openai/internal/openaiclient/chat_test.go
@@ -21,7 +21,7 @@ func TestParseStreamingChatResponse_FinishReason(t *testing.T) {
 	}
 
 	req := &ChatRequest{
-		StreamingFunc: func(ctx context.Context, chunk []byte) error {
+		StreamingFunc: func(_ context.Context, _ []byte) error {
 			return nil
 		},
 	}
diff --git a/llms/openai/multicontent_test.go b/llms/openai/multicontent_test.go
index 7dccb94c..d39e0abd 100644
--- a/llms/openai/multicontent_test.go
+++ b/llms/openai/multicontent_test.go
@@ -116,7 +116,7 @@ func TestWithStreaming(t *testing.T) {
 
 	var sb strings.Builder
 	rsp, err := llm.GenerateContent(context.Background(), content,
-		llms.WithStreamingFunc(func(ctx context.Context, chunk []byte) error {
+		llms.WithStreamingFunc(func(_ context.Context, chunk []byte) error {
 			sb.Write(chunk)
 			return nil
 		}))
diff --git a/memory/window_buffer_test.go b/memory/window_buffer_test.go
index fcd8935f..5cb1e3fc 100644
--- a/memory/window_buffer_test.go
+++ b/memory/window_buffer_test.go
@@ -185,7 +185,6 @@ func TestConversationWindowBuffer_cutMessages(t *testing.T) { // nolint:funlen
 		},
 	}
 	for _, tt := range tests {
-		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
 			wb := &ConversationWindowBuffer{
diff --git a/outputparser/structured_test.go b/outputparser/structured_test.go
index 4a089a86..73dddfda 100644
--- a/outputparser/structured_test.go
+++ b/outputparser/structured_test.go
@@ -54,7 +54,6 @@ func TestStructured(t *testing.T) {
 	}
 
 	for _, tc := range testCases {
-		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
 			parser := NewStructured(tc.responseSchema)
diff --git a/prompts/few_shot_test.go b/prompts/few_shot_test.go
index 43fda5f0..b3704606 100644
--- a/prompts/few_shot_test.go
+++ b/prompts/few_shot_test.go
@@ -121,7 +121,6 @@ func TestFewShotPrompt_Format(t *testing.T) {
 		},
 	}
 	for _, tc := range testCases {
-		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
 			t.Helper()
diff --git a/prompts/internal/fstring/fstring_test.go b/prompts/internal/fstring/fstring_test.go
index 9f00f379..5238c346 100644
--- a/prompts/internal/fstring/fstring_test.go
+++ b/prompts/internal/fstring/fstring_test.go
@@ -29,7 +29,6 @@ func TestFormat(t *testing.T) {
 		{"4", args{"a= { val }", map[string]any{"val": 1}}, "a= 1", ""},
 	}
 	for _, tt := range tests {
-		tt := tt
 		t.Run(tt.name, func(t *testing.T) {
 			t.Parallel()
 			got, err := Format(tt.args.format, tt.args.values)
diff --git a/prompts/prompt_template_test.go b/prompts/prompt_template_test.go
index 638cb5b6..92d11b86 100644
--- a/prompts/prompt_template_test.go
+++ b/prompts/prompt_template_test.go
@@ -51,7 +51,6 @@ func TestPromptTemplateFormatPrompt(t *testing.T) {
 	}
 
 	for _, tc := range cases {
-		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
 			p := PromptTemplate{
diff --git a/prompts/templates_test.go b/prompts/templates_test.go
index f1a6b4fb..ba10dc31 100644
--- a/prompts/templates_test.go
+++ b/prompts/templates_test.go
@@ -50,7 +50,6 @@ func TestInterpolateGoTemplate(t *testing.T) {
 	}
 
 	for _, tc := range testCases {
-		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			t.Run("go/template", func(t *testing.T) {
 				t.Parallel()
@@ -85,7 +84,6 @@ func TestInterpolateGoTemplate(t *testing.T) {
 	}
 
 	for _, tc := range errTestCases {
-		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
 
diff --git a/schema/chat_messages_test.go b/schema/chat_messages_test.go
index 4b93b77d..a752f8de 100644
--- a/schema/chat_messages_test.go
+++ b/schema/chat_messages_test.go
@@ -50,7 +50,6 @@ func TestGetBufferString(t *testing.T) {
 	}
 
 	for _, tc := range testCases {
-		tc := tc
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
 			result, err := schema.GetBufferString(tc.messages, tc.humanPrefix, tc.aiPrefix)
diff --git a/textsplitter/markdown_splitter_test.go b/textsplitter/markdown_splitter_test.go
index df27fd5e..ccfbb5cb 100644
--- a/textsplitter/markdown_splitter_test.go
+++ b/textsplitter/markdown_splitter_test.go
@@ -335,8 +335,6 @@ more text`,
 	}
 
 	for _, tc := range tt {
-		tc := tc // pin
-
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
 
@@ -441,8 +439,6 @@ func TestMarkdownHeaderTextSplitter_SplitInline(t *testing.T) {
 	}
 
 	for _, tc := range tt {
-		tc := tc // pin
-
 		t.Run(tc.name, func(t *testing.T) {
 			t.Parallel()
 
diff --git a/vectorstores/pgvector/pgvector_test.go b/vectorstores/pgvector/pgvector_test.go
index 74221b54..f9f3c23f 100644
--- a/vectorstores/pgvector/pgvector_test.go
+++ b/vectorstores/pgvector/pgvector_test.go
@@ -558,7 +558,7 @@ func TestDeduplicater(t *testing.T) {
 			"type": "vegetable",
 		}},
 	}, vectorstores.WithDeduplicater(
-		func(ctx context.Context, doc schema.Document) bool {
+		func(_ context.Context, doc schema.Document) bool {
 			return doc.PageContent == "tokyo"
 		},
 	))
diff --git a/vectorstores/weaviate/weaviate_test.go b/vectorstores/weaviate/weaviate_test.go
index ee7059ee..96405fc2 100644
--- a/vectorstores/weaviate/weaviate_test.go
+++ b/vectorstores/weaviate/weaviate_test.go
@@ -236,7 +236,7 @@ func TestDeduplicater(t *testing.T) {
 			"type": "vegetable",
 		}},
 	}, vectorstores.WithDeduplicater(
-		func(ctx context.Context, doc schema.Document) bool {
+		func(_ context.Context, doc schema.Document) bool {
 			return doc.PageContent == "tokyo"
 		},
 	))