diff --git a/components/tool/sougousearch/README.md b/components/tool/sougousearch/README.md new file mode 100644 index 000000000..9ae0e15c6 --- /dev/null +++ b/components/tool/sougousearch/README.md @@ -0,0 +1,137 @@ +# Sougou Search Tool + +This is a custom search tool implemented for [Eino](https://github.com/cloudwego/eino) (powered by Sougou Search). The tool implements the `InvokableTool` interface and seamlessly integrates with Eino's ChatModel interaction system and `ToolsNode` using TencentCloud's Web Search API (WSA) JSON API to provide enhanced search capabilities. + +## Features + +- Implements `github.com/cloudwego/eino/components/tool.InvokableTool` interface +- Easy integration with Eino tool system +- Configurable search parameters (query, result count, pagination, search mode) +- Simplified search results containing titles, links, passages, and contents +- Supports custom base URL endpoint configuration + +## Installation + +```bash +go get github.com/cloudwego/eino-ext/components/tool/sougousearch +go get github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common +go get github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/wsa +``` + +## Prerequisites + +Before using this tool, you need to: + +1. **Get Tencent Cloud Web Search API Keys**: + - Reference documentation: + - Enable Tencent Cloud "Web Search (WSA)" service + - Obtain your `SecretKey` and `SecretID` + +## Configuration (Config) + +```go +type Config struct { + SecretID string `json:"secret_id"` // Required: Tencent Cloud SecretID + SecretKey string `json:"secret_key"` // Required: Tencent Cloud SecretKey + Endpoint string `json:"endpoint"` // default: "wsa.tencentcloudapi.com" + Mode int64 `json:"mode"` // default: 0 + Cnt uint64 `json:"cnt"` // default: 10 + + ToolName string `json:"tool_name"` + ToolDesc string `json:"tool_desc"` +} +``` + +- `SecretID`: Tencent Cloud API SecretID. +- `SecretKey`: Tencent Cloud API SecretKey. +- `Endpoint`: API endpoint (default: `wsa.tencentcloudapi.com`). +- `Mode`: Result type, 0-natural, 1-VR, 2-mixed (default: `0`). +- `Cnt`: Default number of results to fetch per request (default: `10`). +- `ToolName`: Override default tool name (default: `sougou_search`). +- `ToolDesc`: Override default tool description. + +## Examples + +### Example 1: Basic Search + +```go +searchTool, _ := sougousearch.NewTool(ctx, &sougousearch.Config{ + SecretID: os.Getenv("TENCENTCLOUD_SECRET_ID"), + SecretKey: os.Getenv("TENCENTCLOUD_SECRET_KEY"), +}) + +req := sougousearch.SearchRequest{ + Query: "Artificial Intelligence", +} +args, _ := json.Marshal(req) +resp, _ := searchTool.InvokableRun(ctx, string(args)) +``` + +### Example 2: Search with Limits + +```go +searchTool, _ := sougousearch.NewTool(ctx, &sougousearch.Config{ + SecretID: os.Getenv("TENCENTCLOUD_SECRET_ID"), + SecretKey: os.Getenv("TENCENTCLOUD_SECRET_KEY"), + Cnt: 10, // Supports returning 10/20/30/40/50 results per request +}) + +cnt := uint64(3) +req := sougousearch.SearchRequest{ + Query: "Go concurrent programming", + Cnt: &cnt, +} +args, _ := json.Marshal(req) +resp, _ := searchTool.InvokableRun(ctx, string(args)) +``` + +### Example 3: Integrate with Eino ToolsNode + +```go +import ( + "github.com/cloudwego/eino/components/tool" +) + +searchTool, _ := sougousearch.NewTool(ctx, &sougousearch.Config{ + SecretID: os.Getenv("TENCENTCLOUD_SECRET_ID"), + SecretKey: os.Getenv("TENCENTCLOUD_SECRET_KEY"), +}) + +tools := []tool.BaseTool{searchTool} +// Use with Eino's ToolsNode in your workflow +``` + +### Full Example + +See [examples/main.go](examples/main.go) for a complete working example. + +Run the example: +```bash +export TENCENTCLOUD_SECRET_KEY="your-api-key" +export TENCENTCLOUD_SECRET_ID="your-secret-id" +cd examples && go run main.go +``` + +## How it Works + +1. **Tool Creation**: Initialize the tool using your TencentCloud API credentials and configuration. + +2. **Request Processing**: When invoked, the tool receives a JSON-formatted `SearchRequest` with query parameters. + +3. **API Call**: The tool calls the TencentCloud Web Search API using the specified parameters. + +4. **Response Simplification**: The raw TencentCloud API response is simplified to contain only essential fields (title, link, passage, content, etc.). + +5. **JSON Response**: The simplified results are returned as a JSON string for easy consumption. + +## API Limitations + +Please note the limitations of the TencentCloud Web Search API: +- Paid tiers: 30 RMB/1000 requests, 46 RMB/1000 requests +- Each query can return 10/20/30/40/50 results + +## More Details + +- [Tencent Cloud Web Search API Documentation](https://cloud.tencent.com/document/api/1806/121812) +- [Eino Documentation](https://www.cloudwego.io/docs/eino/) +- [Example Code](examples/main.go) \ No newline at end of file diff --git a/components/tool/sougousearch/README_zh.md b/components/tool/sougousearch/README_zh.md new file mode 100644 index 000000000..1a26fe11a --- /dev/null +++ b/components/tool/sougousearch/README_zh.md @@ -0,0 +1,176 @@ +# 搜狗搜索 Tool + +这是一个为 [Eino](https://github.com/cloudwego/eino) 实现的 TencentCloud 自定义搜索工具 (底层为搜索搜索)。该工具实现了 `InvokableTool` 接口,可以使用 TencentCloud 的联网搜索 JSON API 与 Eino 的 ChatModel 交互系统和 `ToolsNode` 无缝集成,提供增强的搜索功能。 + +## 特性 + +- 实现了 `github.com/cloudwego/eino/components/tool.InvokableTool` 接口 +- 易于与 Eino 工具系统集成 +- 可配置的搜索参数(搜索关键词、结果数量、分页参数、搜索模式) +- 简化的搜索结果,包含标题、链接、摘要和描述 +- 支持自定义基础 URL 配置 + +## 安装 + +```bash +go get github.com/cloudwego/eino-ext/components/tool/sougousearch +``` + +## 前置条件 + +使用此工具之前,您需要: + +1. **获取腾讯云联网搜索密钥**: + - 参考文档: + - 开通腾讯云“联网搜索”功能 + - 获取 SecretKey,SecretID + +## 使用示例 + +```go +package main + +import ( + "context" + "fmt" + "os" + + "github.com/cloudwego/eino-ext/components/tool/sougousearch" +) + +func main() { + ctx := context.Background() + + // 初始化配置,建议从环境变量中读取密钥 + conf := &sougousearch.Config{ + SecretID: os.Getenv("TENCENTCLOUD_SECRET_ID"), + SecretKey: os.Getenv("TENCENTCLOUD_SECRET_KEY"), + Endpoint: "wsa.tencentcloudapi.com", // 默认 + Mode: 0, // 0-自然检索,1-多模态VR,2-混合 + Cnt: 10, // 返回结果条数 + } + + // 创建 Tool + sougouTool, err := sougousearch.NewTool(ctx, conf) + if err != nil { + panic(err) + } + + // 使用 Tool 进行搜索 + input := `{"query": "eino framework"}` + resp, err := sougouTool.InvokableRun(ctx, input) + if err != nil { + panic(err) + } + + fmt.Println(resp) +} +``` + +## 配置项 (Config) + +```go +type Config struct { + SecretID string `json:"secret_id"` // 必需:腾讯云 SecretID + SecretKey string `json:"secret_key"` // 必需:腾讯云 SecretKey + Endpoint string `json:"endpoint"` // default: "wsa.tencentcloudapi.com" + Mode int64 `json:"mode"` // default: 0 + Cnt uint64 `json:"cnt"` // default: 10 + + ToolName string `json:"tool_name"` + ToolDesc string `json:"tool_desc"` +} +``` + +- `SecretID`: 腾讯云 API SecretID。 +- `SecretKey`: 腾讯云 API SecretKey。 +- `Endpoint`: 请求节点 (默认: `wsa.tencentcloudapi.com`)。 +- `Mode`: 返回结果类型,0-自然检索,1-多模态VR,2-混合 (默认: `0`)。 +- `Cnt`: 每次请求默认返回的搜索结果条数 (默认: `10`)。 +- `ToolName`: Tool 名称 (默认: `sougou_search`)。 +- `ToolDesc`: Tool 的描述信息。 + +## 示例 + +### 示例 1:基本搜索 + +```go +searchTool, _ := sougousearch.NewTool(ctx, &sougousearch.Config{ + SecretID: os.Getenv("TENCENTCLOUD_SECRET_ID"), + SecretKey: os.Getenv("TENCENTCLOUD_SECRET_KEY"), +}) + +req := sougousearch.SearchRequest{ + Query: "人工智能", +} +args, _ := json.Marshal(req) +resp, _ := searchTool.InvokableRun(ctx, string(args)) +``` + +### 示例 2:带分页限制的搜索 + +```go +searchTool, _ := sougousearch.NewTool(ctx, &sougousearch.Config{ + SecretID: os.Getenv("TENCENTCLOUD_SECRET_ID"), + SecretKey: os.Getenv("TENCENTCLOUD_SECRET_KEY"), + Cnt: 10, // 支持每次返回 10/20/30/40/50 个结果 +}) + +cnt := uint64(3) +req := sougousearch.SearchRequest{ + Query: "Go并发编程", + Cnt: &cnt, +} +args, _ := json.Marshal(req) +resp, _ := searchTool.InvokableRun(ctx, string(args)) +``` + +### 示例 3:与 Eino ToolsNode 集成 + +```go +import ( + "github.com/cloudwego/eino/components/tool" +) + +searchTool, _ := sougousearch.NewTool(ctx, &sougousearch.Config{ + SecretID: os.Getenv("TENCENTCLOUD_SECRET_ID"), + SecretKey: os.Getenv("TENCENTCLOUD_SECRET_KEY"), +}) + +tools := []tool.BaseTool{searchTool} +// 在您的工作流中与 Eino 的 ToolsNode 一起使用 +``` + +### 完整示例 + +完整的工作示例请参见 [examples/main.go](examples/main.go) + +运行示例: + +```bash +export TENCENTCLOUD_SECRET_KEY="your-api-key" +export TENCENTCLOUD_SECRET_ID="your-secret-id" +cd examples && go run main.go +``` + +## 工作原理 + +1. **工具创建**:使用您的 TencentCloud API 凭据和配置初始化工具。 +2. **请求处理**:调用时,工具接收带有查询参数的 JSON 格式 `SearchRequest`。 +3. **API 调用**:工具使用指定的参数调用 Sougou 的自定义搜索 JSON API。 +4. **响应简化**:原始 TencentCloud API 响应被简化为仅包含基本字段(标题、链接、摘要、描述)。 +5. **JSON 响应**:简化的结果作为 JSON 字符串返回,便于使用。 + +## API 限制 + +请注意 TencentCloud 联网搜索 API 的限制: + +- 付费层级:30 元/千次,46 元/千次 +- 每次查询可返回 10/20/30/40/50 个结果 + +## 更多详情 + +- [Tencent Cloud 联网搜索](https://cloud.tencent.com/document/api/1806/121812) +- [Eino 文档](https://www.cloudwego.io/zh/docs/eino/) +- [示例代码](examples/main.go) + diff --git a/components/tool/sougousearch/examples/main.go b/components/tool/sougousearch/examples/main.go new file mode 100644 index 000000000..ec4806eba --- /dev/null +++ b/components/tool/sougousearch/examples/main.go @@ -0,0 +1,96 @@ +/* + * 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" + "encoding/json" + "fmt" + "log" + "os" + + "github.com/cloudwego/eino-ext/components/tool/sougousearch" +) + +func main() { + ctx := context.Background() + + tencentCloudSecretID := os.Getenv("TENCENTCLOUD_SECRET_ID") + tencentCloudSecretKey := os.Getenv("TENCENTCLOUD_SECRET_KEY") + + if tencentCloudSecretID == "" || tencentCloudSecretKey == "" { + log.Fatal("[TENCENTCLOUD_SECRET_ID] and [TENCENTCLOUD_SECRET_KEY] must set") + } + + // create tool + searchTool, err := sougousearch.NewTool(ctx, &sougousearch.Config{ + SecretID: tencentCloudSecretID, + SecretKey: tencentCloudSecretKey, + Cnt: 5, + Mode: 0, // natural search + }) + if err != nil { + log.Fatal(err) + } + + // prepare params + cnt := uint64(3) + req := sougousearch.SearchRequest{ + Query: "Golang concurrent programming", + Cnt: &cnt, + } + + args, err := json.Marshal(req) + if err != nil { + log.Fatal(err) + } + + // do search + resp, err := searchTool.InvokableRun(ctx, string(args)) + if err != nil { + log.Fatal(err) + } + + var searchResp sougousearch.SearchResult + if err := json.Unmarshal([]byte(resp), &searchResp); err != nil { + log.Fatal(err) + } + + // Print results + fmt.Println("Search Results:") + fmt.Println("==============") + for i, result := range searchResp.Items { + fmt.Printf("\n%d. Title: %s\n", i+1, result.Title) + fmt.Printf(" Link: %s\n", result.URL) + fmt.Printf(" Desc: %s\n", result.Passage) + } + fmt.Println("") + fmt.Println("==============") + + // seems like: + // Search Results: + // ============== + // 1. Title: Go Concurrency Patterns - The Go Programming Language + // Link: https://go.dev/blog/pipelines + // Desc: Go's concurrency primitives make it easy to construct streaming data pipelines that make efficient use of I/O and multiple CPUs... + // + // 2. Title: A Tour of Go - Concurrency + // Link: https://go.dev/tour/concurrency/1 + // Desc: Go provides concurrency constructions as part of the core language... + // ... + // ============== +} diff --git a/components/tool/sougousearch/go.mod b/components/tool/sougousearch/go.mod new file mode 100644 index 000000000..283a21024 --- /dev/null +++ b/components/tool/sougousearch/go.mod @@ -0,0 +1,39 @@ +module github.com/cloudwego/eino-ext/components/tool/sougousearch + +go 1.24.11 + +require ( + github.com/cloudwego/eino v0.8.3 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.58 + github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/wsa v1.3.34 +) + +require ( + github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/buger/jsonparser v1.1.1 // indirect + github.com/bytedance/gopkg v0.1.3 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/dustin/go-humanize v1.0.1 // indirect + github.com/eino-contrib/jsonschema v1.0.3 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/goph/emperror v0.17.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.9 // indirect + github.com/mailru/easyjson v0.7.7 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/nikolalohinski/gonja v1.5.3 // indirect + github.com/pelletier/go-toml/v2 v2.0.9 // indirect + github.com/pkg/errors v0.9.1 // indirect + github.com/sirupsen/logrus v1.9.3 // indirect + github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect + github.com/yargevad/filepathx v1.0.0 // indirect + golang.org/x/arch v0.11.0 // indirect + golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect + golang.org/x/sys v0.26.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/components/tool/sougousearch/go.sum b/components/tool/sougousearch/go.sum new file mode 100644 index 000000000..a7f288e79 --- /dev/null +++ b/components/tool/sougousearch/go.sum @@ -0,0 +1,144 @@ +github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= +github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= +github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= +github.com/bmizerany/assert v0.0.0-20160611221934-b7ed37b82869/go.mod h1:Ekp36dRnpXw/yCqJaO+ZrUyxD+3VXMFFr56k5XYrpB4= +github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= +github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bugsnag/bugsnag-go v1.4.0/go.mod h1:2oa8nejYd4cQ/b0hMIopN0lCRxU0bueqREvZLWFrtK8= +github.com/bugsnag/panicwrap v1.2.0/go.mod h1:D/8v3kj0zr8ZAKg1AQ6crr+5VwKN5eIywRkfhyM/+dE= +github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= +github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/certifi/gocertifi v0.0.0-20190105021004-abcd57078448/go.mod h1:GJKEexRPVJrBSOjoqN5VNOIKJ5Q3RViH6eu3puDRwx4= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/cloudwego/eino v0.8.3 h1:LZ23dmR9PLNEt1FS5GCEDeNQ+XQpVgxBTfzUb5r8Y8E= +github.com/cloudwego/eino v0.8.3/go.mod h1:+2N4nsMPxA6kGBHpH+75JuTfEcGprAMTdsZESrShKpU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/eino-contrib/jsonschema v1.0.3 h1:2Kfsm1xlMV0ssY2nuxshS4AwbLFuqmPmzIjLVJ1Fsp0= +github.com/eino-contrib/jsonschema v1.0.3/go.mod h1:cpnX4SyKjWjGC7iN2EbhxaTdLqGjCi0e9DxpLYxddD4= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127 h1:0gkP6mzaMqkmpcJYCFOLkIBwI7xFExG03bbkOkCvUPI= +github.com/go-check/check v0.0.0-20180628173108-788fd7840127/go.mod h1:9ES+weclKsC9YodN5RgxqK/VD9HM9JsCSh7rNhMZE98= +github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= +github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18= +github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= +github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= +github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= +github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= +github.com/kardianos/osext v0.0.0-20190222173326-2bc1f35cddc0/go.mod h1:1NbS8ALrpOvjt0rHPNLyCIeMtbizbir8U//inJ+zuB8= +github.com/klauspost/cpuid/v2 v2.2.9 h1:66ze0taIn2H33fBvCkXuv9BmCwDfafmiIVpKV9kKGuY= +github.com/klauspost/cpuid/v2 v2.2.9/go.mod h1:rqkxqrZ1EhYM9G+hXH7YdowN5R5RGN6NK4QwQ3WMXF8= +github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= +github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI= +github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= +github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= +github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= +github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= +github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mattn/go-colorable v0.1.2 h1:/bC9yWikZXAL9uJdulbSfyVNIR3n3trXl+v8+1sx8mU= +github.com/mattn/go-colorable v0.1.2/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= +github.com/mattn/go-isatty v0.0.8 h1:HLtExJ+uU2HOZ+wI0Tt5DtUDrx8yhUqDcp7fYERX4CE= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b h1:j7+1HpAFS1zy5+Q4qx1fWh90gTKwiN4QCGoY9TWyyO4= +github.com/mgutz/ansi v0.0.0-20170206155736-9520e82c474b/go.mod h1:01TrycV0kFyexm33Z7vhZRXopbI8J3TDReVlkTgMUxE= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/nikolalohinski/gonja v1.5.3 h1:GsA+EEaZDZPGJ8JtpeGN78jidhOlxeJROpqMT9fTj9c= +github.com/nikolalohinski/gonja v1.5.3/go.mod h1:RmjwxNiXAEqcq1HeK5SSMmqFJvKOfTfXhkJv6YBtPa4= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.8.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.5.0/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/pelletier/go-toml/v2 v2.0.9 h1:uH2qQXheeefCCkuBBSLi7jCiSmj3VRh2+Goq2N7Xxu0= +github.com/pelletier/go-toml/v2 v2.0.9/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= +github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/rollbar/rollbar-go v1.0.2/go.mod h1:AcFs5f0I+c71bpHlXNNDbOWJiKwjFDtISeXco0L5PKQ= +github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f h1:Z2cODYsUxQPofhpYRMQVwWz4yUVpHF+vPi+eUdruUYI= +github.com/slongfield/pyfmt v0.0.0-20220222012616-ea85ff4c361f/go.mod h1:JqzWyvTuI2X4+9wOHmKSQCYxybB/8j6Ko43qVmXDuZg= +github.com/smarty/assertions v1.15.0 h1:cR//PqUBUiQRakZWqBiFFQ9wb8emQGDb0HeGdqGByCY= +github.com/smarty/assertions v1.15.0/go.mod h1:yABtdzeQs6l1brC900WlRNwj6ZR55d7B+E8C6HtKdec= +github.com/smartystreets/goconvey v1.8.1 h1:qGjIddxOk4grTu9JPOU31tVfq3cNdBlNa5sSznIX1xY= +github.com/smartystreets/goconvey v1.8.1/go.mod h1:+/u4qLyY6x1jReYOp7GOM2FSt8aP9CzCZL03bI28W60= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.34/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.58 h1:vLowU1ND9bMmdO525NtYejec5yZxRMSO6PkgOhrCayg= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common v1.3.58/go.mod h1:r5r4xbfxSaeR04b166HGsBa/R4U3SueirEUpXGuw+Q0= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/wsa v1.3.34 h1:po5xb/RXqesU/5tb5EunZ2cf6aAGAg75vMnAyiQ8Scc= +github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/wsa v1.3.34/go.mod h1:gyHOOktxKFyeRJS2vQADINBuMBSTyxzGkCnAVkJ9lTQ= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= +github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= +github.com/x-cray/logrus-prefixed-formatter v0.5.2 h1:00txxvfBM9muc0jiLIEAkAcIMJzfthRT6usrui8uGmg= +github.com/x-cray/logrus-prefixed-formatter v0.5.2/go.mod h1:2duySbKsL6M18s5GU7VPsoEPHyzalCE06qoARUCeBBE= +github.com/yargevad/filepathx v1.0.0 h1:SYcT+N3tYGi+NvazubCNlvgIPbzAk7i7y2dwg3I5FYc= +github.com/yargevad/filepathx v1.0.0/go.mod h1:BprfX/gpYNJHJfc35GjRRpVcwWXS89gGulUIU5tK3tA= +go.uber.org/mock v0.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU= +go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= +golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4= +golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= +golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.11.0 h1:6Ewdq3tDic1mg5xRO4milcWCfMVQhI4NkqWWvqejpuA= +golang.org/x/crypto v0.11.0/go.mod h1:xgJhtzW8F9jGdVFWZESrid1U1bjeNy4zgy5cRr/CIio= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 h1:MGwJjxBy0HJshjDNfLsYO8xppfqWlA5ZT9OhtUUhTNw= +golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo= +golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.10.0 h1:3R7pNqamzBraeqj/Tj8qt1aQ2HpmlC+Cx/qL/7hn4/c= +golang.org/x/term v0.10.0/go.mod h1:lpqdcUyK/oCiQxvxVrppt5ggO2KCZ5QblwqPnfZ6d5o= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY= +gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/components/tool/sougousearch/sougou_search.go b/components/tool/sougousearch/sougou_search.go new file mode 100644 index 000000000..b7793a29c --- /dev/null +++ b/components/tool/sougousearch/sougou_search.go @@ -0,0 +1,186 @@ +/* + * Copyright 2024 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 sougousearch + +import ( + "context" + "encoding/json" + "fmt" + + "github.com/cloudwego/eino/components/tool" + "github.com/cloudwego/eino/components/tool/utils" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common" + "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/common/profile" + wsa "github.com/tencentcloud/tencentcloud-sdk-go/tencentcloud/wsa/v20250508" +) + +type Config struct { + SecretID string `json:"secret_id"` + SecretKey string `json:"secret_key"` + Endpoint string `json:"endpoint"` // default: "wsa.tencentcloudapi.com" + Mode int64 `json:"mode"` // default: 0 + Cnt uint64 `json:"cnt"` // default: 10 + + ToolName string `json:"tool_name"` + ToolDesc string `json:"tool_desc"` +} + +type SearchRequest struct { + Query string `json:"query" jsonschema_description:"queried string to the search engine"` + Mode *int64 `json:"mode,omitempty" jsonschema_description:"0-natural, 1-VR, 2-mixed"` + Site *string `json:"site,omitempty" jsonschema_description:"site domain to search within"` + FromTime *int64 `json:"from_time,omitempty" jsonschema_description:"start time timestamp in seconds"` + ToTime *int64 `json:"to_time,omitempty" jsonschema_description:"end time timestamp in seconds"` + Cnt *uint64 `json:"cnt,omitempty" jsonschema_description:"number of search results to return, 10/20/30/40/50"` +} + +type SimplifiedSearchItem struct { + Title string `json:"title,omitempty"` + Date string `json:"date,omitempty"` + URL string `json:"url,omitempty"` + Passage string `json:"passage,omitempty"` + Content string `json:"content,omitempty"` + Site string `json:"site,omitempty"` + Score float64 `json:"score,omitempty"` + Images []string `json:"images,omitempty"` + Favicon string `json:"favicon,omitempty"` +} + +type SearchResult struct { + Query string `json:"query,omitempty"` + Items []*SimplifiedSearchItem `json:"items"` + Version string `json:"version,omitempty"` + Msg string `json:"msg,omitempty"` + RequestId string `json:"request_id,omitempty"` +} + +type sougouSearch struct { + conf *Config + client *wsa.Client +} + +func NewTool(ctx context.Context, conf *Config) (tool.InvokableTool, error) { + if conf == nil { + conf = &Config{} + } + + toolName := "sougou_search" + toolDesc := "Search using Sougou search engine via Tencent Cloud API" + if conf.ToolName != "" { + toolName = conf.ToolName + } + if conf.ToolDesc != "" { + toolDesc = conf.ToolDesc + } + + endpoint := "wsa.tencentcloudapi.com" + if conf.Endpoint != "" { + endpoint = conf.Endpoint + } + + credential := common.NewCredential(conf.SecretID, conf.SecretKey) + cpf := profile.NewClientProfile() + cpf.HttpProfile.Endpoint = endpoint + + // Set Scheme to HTTP if testing with local mock server + if endpoint != "wsa.tencentcloudapi.com" { + cpf.HttpProfile.Scheme = "HTTP" + } + + client, err := wsa.NewClient(credential, "", cpf) + if err != nil { + return nil, fmt.Errorf("create wsa client failed: %w", err) + } + + ss := &sougouSearch{ + conf: conf, + client: client, + } + + return utils.InferTool(toolName, toolDesc, ss.search) +} + +func (s *sougouSearch) search(ctx context.Context, req *SearchRequest) (*SearchResult, error) { + wsaReq := wsa.NewSearchProRequest() + wsaReq.Query = common.StringPtr(req.Query) + + if req.Mode != nil { + wsaReq.Mode = req.Mode + } else if s.conf.Mode > 0 { + wsaReq.Mode = common.Int64Ptr(s.conf.Mode) + } else { + wsaReq.Mode = common.Int64Ptr(0) + } + + if req.Cnt != nil { + wsaReq.Cnt = req.Cnt + } else if s.conf.Cnt > 0 { + wsaReq.Cnt = common.Uint64Ptr(s.conf.Cnt) + } + + if req.Site != nil { + wsaReq.Site = req.Site + } + if req.FromTime != nil { + wsaReq.FromTime = req.FromTime + } + if req.ToTime != nil { + wsaReq.ToTime = req.ToTime + } + + wsaResp, err := s.client.SearchProWithContext(ctx, wsaReq) + if err != nil { + return nil, fmt.Errorf("wsa SearchPro failed: %w", err) + } + + if wsaResp == nil || wsaResp.Response == nil { + return nil, fmt.Errorf("empty response from wsa") + } + + res := &SearchResult{ + Items: make([]*SimplifiedSearchItem, 0, len(wsaResp.Response.Pages)), + } + + if wsaResp.Response.Query != nil { + res.Query = *wsaResp.Response.Query + } else { + res.Query = req.Query + } + if wsaResp.Response.Version != nil { + res.Version = *wsaResp.Response.Version + } + if wsaResp.Response.Msg != nil { + res.Msg = *wsaResp.Response.Msg + } + if wsaResp.Response.RequestId != nil { + res.RequestId = *wsaResp.Response.RequestId + } + + for _, pageStr := range wsaResp.Response.Pages { + if pageStr == nil { + continue + } + var item SimplifiedSearchItem + if err := json.Unmarshal([]byte(*pageStr), &item); err != nil { + // skip invalid json + continue + } + res.Items = append(res.Items, &item) + } + + return res, nil +} diff --git a/components/tool/sougousearch/sougou_search_test.go b/components/tool/sougousearch/sougou_search_test.go new file mode 100644 index 000000000..94dd4550b --- /dev/null +++ b/components/tool/sougousearch/sougou_search_test.go @@ -0,0 +1,182 @@ +/* + * Copyright 2024 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 sougousearch + +import ( + "context" + "encoding/json" + "net/http" + "net/http/httptest" + "strings" + "testing" +) + +func TestNewTool(t *testing.T) { + ctx := context.Background() + + t.Run("default config", func(t *testing.T) { + tl, err := NewTool(ctx, nil) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if tl == nil { + t.Fatal("expected tool, got nil") + } + + info, err := tl.Info(ctx) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if info.Name != "sougou_search" { + t.Errorf("expected name sougou_search, got %s", info.Name) + } + }) + + t.Run("custom config", func(t *testing.T) { + conf := &Config{ + ToolName: "custom_sougou", + ToolDesc: "Custom description", + } + tl, err := NewTool(ctx, conf) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + info, err := tl.Info(ctx) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + if info.Name != "custom_sougou" { + t.Errorf("expected name custom_sougou, got %s", info.Name) + } + if info.Desc != "Custom description" { + t.Errorf("expected custom description, got %s", info.Desc) + } + }) +} + +func TestSougouSearch(t *testing.T) { + ctx := context.Background() + + // Mock server for Tencent Cloud WSA API + mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Verify method and headers + if r.Method != http.MethodPost { + w.WriteHeader(http.StatusMethodNotAllowed) + return + } + + // Read request body + var reqBody map[string]interface{} + if err := json.NewDecoder(r.Body).Decode(&reqBody); err != nil { + w.WriteHeader(http.StatusBadRequest) + return + } + + query, ok := reqBody["Query"].(string) + if !ok { + w.WriteHeader(http.StatusBadRequest) + return + } + + if query == "error" { + // return error format + errResp := map[string]interface{}{ + "Response": map[string]interface{}{ + "Error": map[string]interface{}{ + "Code": "InternalError", + "Message": "mock error", + }, + "RequestId": "mock-req-id-err", + }, + } + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode(errResp) + return + } + + // Success format + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + w.Write([]byte(`{"Response":{"Query":"` + query + `","Pages":["{\"title\":\"Result 1\",\"url\":\"https://example.com/1\",\"passage\":\"Snippet 1\"}"],"RequestId":"mock-req-id"}}`)) + })) + defer mockServer.Close() + + // Parse out the host from mockServer.URL (remove http://) + endpoint := strings.TrimPrefix(mockServer.URL, "http://") + + t.Run("success search", func(t *testing.T) { + conf := &Config{ + SecretID: "test_id", + SecretKey: "test_key", + Endpoint: endpoint, // point SDK to our mock server + } + + tl, err := NewTool(ctx, conf) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + req := &SearchRequest{ + Query: "eino framework", + } + reqJSON, _ := json.Marshal(req) + + resp, err := tl.InvokableRun(ctx, string(reqJSON)) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + var result SearchResult + if err := json.Unmarshal([]byte(resp), &result); err != nil { + t.Fatalf("expected valid json, got %v", err) + } + + if result.Query != "eino framework" { + t.Errorf("expected query 'eino framework', got %s", result.Query) + } + if len(result.Items) != 1 { + t.Errorf("expected 1 item, got %d", len(result.Items)) + } + if result.Items[0].Title != "Result 1" { + t.Errorf("expected title 'Result 1', got %s", result.Items[0].Title) + } + }) + + t.Run("http error", func(t *testing.T) { + conf := &Config{ + SecretID: "test_id", + SecretKey: "test_key", + Endpoint: endpoint, + } + + tl, err := NewTool(ctx, conf) + if err != nil { + t.Fatalf("expected no error, got %v", err) + } + + req := &SearchRequest{ + Query: "error", + } + reqJSON, _ := json.Marshal(req) + + _, err = tl.InvokableRun(ctx, string(reqJSON)) + if err == nil { + t.Fatal("expected error, got nil") + } + }) +}