Skip to content

Commit

Permalink
feat(tool): add google custom search tool (#34)
Browse files Browse the repository at this point in the history
  • Loading branch information
abughali authored Sep 27, 2024
1 parent 8d47cae commit ef839da
Show file tree
Hide file tree
Showing 7 changed files with 717 additions and 87 deletions.
6 changes: 5 additions & 1 deletion .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -13,4 +13,8 @@ BEE_FRAMEWORK_LOG_SINGLE_LINE="false"
# GROQ_API_KEY=

# Tools
CODE_INTERPRETER_URL=http://127.0.0.1:50051
CODE_INTERPRETER_URL=http://127.0.0.1:50051

# For Google Search Tool
# GOOGLE_API_KEY=your-google-api-key
# GOOGLE_CSE_ID=your-custom-search-engine-id
29 changes: 15 additions & 14 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,20 +95,21 @@ To run this example, be sure that you have installed [ollama](https://ollama.com

### 🛠️ Tools

| Name | Description |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `PythonTool` | Run arbitrary Python code in the remote environment. |
| `WikipediaTool` | Search for data on Wikipedia. |
| `DuckDuckGoTool` | Search for data on DuckDuckGo. |
| `SQLTool` | Executing SQL queries against various databases. [Instructions](./docs/sql-tool.md). |
| `CustomTool` | Runs your own Python function in the remote environment. |
| `LLMTool` | Uses an LLM to process input data. |
| `DynamicTool` | Construct to create dynamic tools. |
| `ArXivTool` | Retrieves research articles published on arXiv. |
| `WebCrawlerTool` | Retrieves content of an arbitrary website. |
| `CustomTool` | Runs your own Python function in the remote environment. |
| `OpenMeteoTool` | Retrieves current, previous, or upcoming weather for a given destination. |
|[Request](https://github.com/i-am-bee/bee-agent-framework/discussions) | |
| Name | Description |
| ------------------------------------------------------------------------- | ------------------------------------------------------------------------------------- |
| `PythonTool` | Run arbitrary Python code in the remote environment. |
| `WikipediaTool` | Search for data on Wikipedia. |
| `GoogleSearchTool` | Search for data on Google using Custom Search Engine. |
| `DuckDuckGoTool` | Search for data on DuckDuckGo. |
| `SQLTool` | Execute SQL queries against relational databases. [Instructions](./docs/sql-tool.md). |
| `CustomTool` | Run your own Python function in the remote environment. |
| `LLMTool` | Use an LLM to process input data. |
| `DynamicTool` | Construct to create dynamic tools. |
| `ArXivTool` | Retrieve research articles published on arXiv. |
| `WebCrawlerTool` | Retrieve content of an arbitrary website. |
| `CustomTool` | Run your own Python function in the remote environment. |
| `OpenMeteoTool` | Retrieve current, previous, or upcoming weather for a given destination. |
|[Request](https://github.com/i-am-bee/bee-agent-framework/discussions) | |

### 🔌️ Adapters (LLM - Inference providers)

Expand Down
2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,7 @@
"zod-to-json-schema": "^3.23.3"
},
"peerDependencies": {
"@googleapis/customsearch": "^3.2.0",
"@ibm-generative-ai/node-sdk": "~3.2.3",
"@langchain/community": "~0.2.28",
"@langchain/core": "~0.2.27",
Expand All @@ -129,6 +130,7 @@
"@commitlint/config-conventional": "^19.4.1",
"@eslint/js": "^9.9.0",
"@eslint/markdown": "^6.0.0",
"@googleapis/customsearch": "^3.2.0",
"@ibm-generative-ai/node-sdk": "~3.2.3",
"@langchain/community": "~0.2.28",
"@langchain/core": "~0.2.27",
Expand Down
184 changes: 184 additions & 0 deletions src/tools/search/googleCustomSearch.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
/**
* Copyright 2024 IBM Corp.
*
* 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.
*/

import { GoogleSearchTool, GoogleSearchToolOutput } from "@/tools/search/googleCustomSearch.js";
import { vi, describe, it, expect, beforeEach } from "vitest";
import { SlidingCache } from "@/cache/slidingCache.js";
import { verifyDeserialization } from "@tests/e2e/utils.js";
import { Task } from "promise-based-task";

vi.mock("@googleapis/customsearch");

describe("GoogleCustomSearch Tool", () => {
let googleSearchTool: GoogleSearchTool;
const mockCustomSearchClient = {
cse: {
list: vi.fn(),
},
};

beforeEach(() => {
vi.clearAllMocks();
googleSearchTool = new GoogleSearchTool({
apiKey: "test-api-key",
cseId: "test-cse-id",
maxResultsPerPage: 10,
});

Object.defineProperty(googleSearchTool, "client", {
get: () => mockCustomSearchClient,
});
});

const generateResults = (count: number) => {
return {
data: {
items: Array(count)
.fill(null)
.map((_, i) => ({
title: `Result ${i + 1}`,
snippet: `Description for result ${i + 1}`,
link: `https://example.com/${i + 1}`,
})),
},
};
};

it("is a valid tool", () => {
expect(googleSearchTool).toBeDefined();
expect(googleSearchTool.name).toBe("GoogleCustomSearch");
expect(googleSearchTool.description).toBeDefined();
});

it("retrieves data with the correct number of results", async () => {
const query = "IBM Research";
const mockResults = generateResults(3);

mockCustomSearchClient.cse.list.mockResolvedValueOnce(mockResults);

const response = await googleSearchTool.run({ query });

expect(response).toBeInstanceOf(GoogleSearchToolOutput);
expect(response.results.length).toBe(3);
expect(mockCustomSearchClient.cse.list).toHaveBeenCalledWith(
{
cx: "test-cse-id",
q: query,
num: 10,
start: 1,
safe: "active",
},
{
signal: undefined,
},
);
});

it("validates maxResultsPerPage range", () => {
expect(
() =>
new GoogleSearchTool({
apiKey: "test-api-key",
cseId: "test-cse-id",
maxResultsPerPage: 0,
}),
).toThrowError("validation failed");
expect(
() =>
new GoogleSearchTool({
apiKey: "test-api-key",
cseId: "test-cse-id",
maxResultsPerPage: 11,
}),
).toThrowError("validation failed");
});

it("paginates correctly", async () => {
const query = "paginated search";
const mockFirstPageResults = generateResults(10);
const mockSecondPageResults = generateResults(10);

mockCustomSearchClient.cse.list
.mockResolvedValueOnce(mockFirstPageResults)
.mockResolvedValueOnce(mockSecondPageResults);

const responsePage1 = await googleSearchTool.run({ query });
const responsePage2 = await googleSearchTool.run({ query, page: 2 });

const combinedResults = [...responsePage1.results, ...responsePage2.results];

expect(combinedResults.length).toBe(20);

expect(mockCustomSearchClient.cse.list).toHaveBeenCalledTimes(2);
expect(mockCustomSearchClient.cse.list).toHaveBeenNthCalledWith(
1,
{
cx: "test-cse-id",
q: query,
num: 10,
start: 1,
safe: "active",
},
{
signal: undefined,
},
);
expect(mockCustomSearchClient.cse.list).toHaveBeenNthCalledWith(
2,
{
cx: "test-cse-id",
q: query,
num: 10,
start: 11,
safe: "active",
},
{
signal: undefined,
},
);
});

it("Serializes", async () => {
const tool = new GoogleSearchTool({
apiKey: "test-api-key",
cseId: "test-cse-id",
maxResultsPerPage: 1,
cache: new SlidingCache({
size: 10,
ttl: 1000,
}),
});

await tool.cache!.set(
"A",
Task.resolve(
new GoogleSearchToolOutput([
{
title: "A",
url: "http://example.com",
description: "A",
},
]),
),
);

await tool.cache!.set("B", Task.resolve(new GoogleSearchToolOutput([])));
const serialized = tool.serialize();
const deserialized = GoogleSearchTool.fromSerialized(serialized);
expect(await tool.cache.get("A")).toStrictEqual(await deserialized.cache.get("A"));
verifyDeserialization(tool, deserialized);
});
});
Loading

0 comments on commit ef839da

Please sign in to comment.