From 092956d555cbe293fb79c35c3664a4aa20361c65 Mon Sep 17 00:00:00 2001 From: Mahmoud Abughali Date: Mon, 4 Nov 2024 11:07:48 -0500 Subject: [PATCH 1/8] feat(tool): add elasticsearch tool Signed-off-by: Mahmoud Abughali --- .env.template | 4 +- docs/tools.md | 29 ++-- examples/agents/elasticsearch.ts | 60 +++++++ package.json | 2 + src/tools/database/elasticsearch.test.ts | 128 +++++++++++++++ src/tools/database/elasticsearch.ts | 198 +++++++++++++++++++++++ tests/e2e/utils.ts | 2 + yarn.lock | 43 ++++- 8 files changed, 450 insertions(+), 16 deletions(-) create mode 100644 examples/agents/elasticsearch.ts create mode 100644 src/tools/database/elasticsearch.test.ts create mode 100644 src/tools/database/elasticsearch.ts diff --git a/.env.template b/.env.template index 517b011d..cecb763b 100644 --- a/.env.template +++ b/.env.template @@ -25,4 +25,6 @@ BEE_FRAMEWORK_LOG_SINGLE_LINE="false" # GOOGLE_API_KEY=your-google-api-key # GOOGLE_CSE_ID=your-custom-search-engine-id - +# For Elasticsearch Tool +# ELASTICSEARCH_NODE= +# ELASTICSEARCH_API_KEY= diff --git a/docs/tools.md b/docs/tools.md index d27ffc32..79b9fcde 100644 --- a/docs/tools.md +++ b/docs/tools.md @@ -10,20 +10,21 @@ These tools extend the agent's abilities, allowing it to interact with external ## Built-in tools -| 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`](./sql-tool.md) | Execute SQL queries against relational databases. Instructions can be found [here](./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. | -| `OpenMeteoTool` | Retrieve 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`](./sql-tool.md) | Execute SQL queries against relational databases. | +| `ElasticSearchTool` | Perform search or aggregation queries against an ElasticSearch database. | +| `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. | +| `OpenMeteoTool` | Retrieve current, previous, or upcoming weather for a given destination. | +| ➕ [Request](https://github.com/i-am-bee/bee-agent-framework/discussions) | | All examples can be found [here](/examples/tools). diff --git a/examples/agents/elasticsearch.ts b/examples/agents/elasticsearch.ts new file mode 100644 index 00000000..617d6599 --- /dev/null +++ b/examples/agents/elasticsearch.ts @@ -0,0 +1,60 @@ +import "dotenv/config.js"; +import { BeeAgent } from "bee-agent-framework/agents/bee/agent"; +import { OpenAIChatLLM } from "bee-agent-framework/adapters/openai/chat"; +import { ElasticSearchTool } from "bee-agent-framework/tools/database/elasticsearch"; +import { FrameworkError } from "bee-agent-framework/errors"; +import { UnconstrainedMemory } from "bee-agent-framework/memory/unconstrainedMemory"; + +const llm = new OpenAIChatLLM({ + parameters: { + temperature: 0, + }, +}); + +const elasticSearchTool = new ElasticSearchTool({ + connection: { + node: process.env.ELASTICSEARCH_NODE, + auth: { + apiKey: process.env.ELASTICSEARCH_API_KEY || "", + }, + }, +}); + +const agent = new BeeAgent({ + llm, + memory: new UnconstrainedMemory(), + tools: [elasticSearchTool], +}); + +const question = "what is the average ticket price of all flights from Cape Town to Venice"; + +try { + const response = await agent + .run( + { prompt: `${question}` }, + { + execution: { + maxRetriesPerStep: 5, + totalMaxRetries: 10, + maxIterations: 15, + }, + }, + ) + .observe((emitter) => { + emitter.on("error", ({ error }) => { + console.log(`Agent 🤖 : `, FrameworkError.ensure(error).dump()); + }); + emitter.on("retry", () => { + console.log(`Agent 🤖 : `, "retrying the action..."); + }); + emitter.on("update", async ({ data, update, meta }) => { + console.log(`Agent (${update.key}) 🤖 : `, update.value); + }); + }); + + console.log(`Agent 🤖 : `, response.result.text); +} catch (error) { + console.error(FrameworkError.ensure(error).dump()); +} finally { + process.exit(0); +} diff --git a/package.json b/package.json index 5d1e31d7..af0be6f1 100644 --- a/package.json +++ b/package.json @@ -173,6 +173,7 @@ "zod-to-json-schema": "^3.23.3" }, "peerDependencies": { + "@elastic/elasticsearch": "^8.15.1", "@googleapis/customsearch": "^3.2.0", "@grpc/grpc-js": "^1.11.3", "@grpc/proto-loader": "^0.7.13", @@ -188,6 +189,7 @@ "devDependencies": { "@commitlint/cli": "^19.5.0", "@commitlint/config-conventional": "^19.5.0", + "@elastic/elasticsearch": "^8.15.1", "@eslint/js": "^9.13.0", "@eslint/markdown": "^6.2.1", "@googleapis/customsearch": "^3.2.0", diff --git a/src/tools/database/elasticsearch.test.ts b/src/tools/database/elasticsearch.test.ts new file mode 100644 index 00000000..03aa5d80 --- /dev/null +++ b/src/tools/database/elasticsearch.test.ts @@ -0,0 +1,128 @@ +/** + * 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 { describe, it, expect, beforeEach, vi } from "vitest"; +import { ElasticSearchTool, ElasticSearchToolOptions } from "@/tools/database/elasticsearch.js"; +import { verifyDeserialization } from "@tests/e2e/utils.js"; +import { JSONToolOutput } from "@/tools/base.js"; +import { SlidingCache } from "@/cache/slidingCache.js"; +import { Task } from "promise-based-task"; + +vi.mock("@elastic/elasticsearch"); + +describe("ElasticSearchTool", () => { + let elasticSearchTool: ElasticSearchTool; + const mockClient = { + cat: { indices: vi.fn() }, + indices: { getMapping: vi.fn() }, + search: vi.fn(), + }; + + beforeEach(() => { + vi.clearAllMocks(); + elasticSearchTool = new ElasticSearchTool({ + connection: { node: "http://localhost:9200" }, + } as ElasticSearchToolOptions); + + Object.defineProperty(elasticSearchTool, "client", { + get: () => mockClient, + }); + }); + + it("lists indices correctly", async () => { + const mockIndices = [{ index: "index1" }, { index: "index2" }]; + mockClient.cat.indices.mockResolvedValueOnce(mockIndices); + + const response = await elasticSearchTool.run({ action: "LIST_INDICES" }); + expect(response.result).toEqual([{ index: "index1" }, { index: "index2" }]); + }); + + it("gets index details", async () => { + const indexName = "index1"; + const mockIndexDetails = { + [indexName]: { mappings: { properties: { field1: { type: "text" } } } }, + }; + mockClient.indices.getMapping.mockResolvedValueOnce(mockIndexDetails); + + const response = await elasticSearchTool.run({ action: "GET_INDEX_DETAILS", indexName }); + expect(response.result).toEqual(mockIndexDetails); + expect(mockClient.indices.getMapping).toHaveBeenCalledWith( + { index: indexName }, + { signal: undefined }, + ); + }); + + it("performs a search", async () => { + const indexName = "index1"; + const query = JSON.stringify({ query: { match_all: {} } }); + const mockSearchResponse = { hits: { hits: [{ _source: { field1: "value1" } }] } }; + mockClient.search.mockResolvedValueOnce(mockSearchResponse); + + const response = await elasticSearchTool.run({ + action: "SEARCH", + indexName, + query, + start: 0, + size: 1, + }); + expect(response.result).toEqual([{ field1: "value1" }]); + }); + + it("throws invalid JSON format error", async () => { + await expect(async () => { + await elasticSearchTool.run({ action: "SEARCH", indexName: "index1", query: "invalid" }); + }).rejects.toThrowError( + expect.objectContaining({ + message: expect.stringContaining("Invalid JSON format for query"), + }), + ); + }); + + it("throws missing index name error", async () => { + await expect(elasticSearchTool.run({ action: "GET_INDEX_DETAILS" })).rejects.toThrow( + "Index name is required for GET_INDEX_DETAILS action.", + ); + }); + + it("throws missing index and query error", async () => { + await expect(elasticSearchTool.run({ action: "SEARCH" })).rejects.toThrow( + "Both index name and query are required for SEARCH action.", + ); + }); + + it("serializes", async () => { + const elasticSearchTool = new ElasticSearchTool({ + connection: { node: "http://localhost:9200" }, + cache: new SlidingCache({ + size: 10, + ttl: 1000, + }), + }); + + await elasticSearchTool.cache!.set( + "connection", + Task.resolve(new JSONToolOutput([{ index: "index1", detail: "sample" }])), + ); + + const serialized = elasticSearchTool.serialize(); + const deserializedTool = ElasticSearchTool.fromSerialized(serialized); + + expect(await deserializedTool.cache.get("connection")).toStrictEqual( + await elasticSearchTool.cache.get("connection"), + ); + verifyDeserialization(elasticSearchTool, deserializedTool); + }); +}); diff --git a/src/tools/database/elasticsearch.ts b/src/tools/database/elasticsearch.ts new file mode 100644 index 00000000..54421b2c --- /dev/null +++ b/src/tools/database/elasticsearch.ts @@ -0,0 +1,198 @@ +/** + * 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 { + Tool, + ToolInput, + ToolError, + BaseToolOptions, + BaseToolRunOptions, + JSONToolOutput, +} from "@/tools/base.js"; +import { Cache } from "@/cache/decoratorCache.js"; +import { z } from "zod"; +import { Client, ClientOptions } from "@elastic/elasticsearch"; +import { + CatIndicesResponse, + IndicesGetMappingResponse, + SearchRequest, + SearchResponse, + SearchHit, +} from "@elastic/elasticsearch/lib/api/types.js"; +import { ValidationError } from "ajv"; + +type ToolRunOptions = BaseToolRunOptions; + +export interface ElasticSearchToolOptions extends BaseToolOptions { + connection: ClientOptions; +} + +export class ElasticSearchTool extends Tool< + JSONToolOutput, + ElasticSearchToolOptions, + ToolRunOptions +> { + name = "ElasticSearchTool"; + + description = `Can query data from an ElasticSearch database. IMPORTANT: strictly follow this order of actions: + 1. LIST_INDICES - retrieve a list of available indices + 2. GET_INDEX_DETAILS - get details of index fields + 3. SEARCH - perform search or aggregation query on a specific index or pass the original user query without modifications if it's a valid JSON ElasticSearch query`; + + inputSchema() { + return z.object({ + action: z + .enum(["LIST_INDICES", "GET_INDEX_DETAILS", "SEARCH"]) + .describe( + "The action to perform. LIST_INDICES lists all indices, GET_INDEX_DETAILS fetches details for a specified index, and SEARCH executes a search or aggregation query", + ), + indexName: z + .string() + .optional() + .describe("The name of the index to query, required for GET_INDEX_DETAILS and SEARCH"), + query: z + .string() + .optional() + .describe("Valid ElasticSearch JSON search or aggregation query for SEARCH action"), + start: z.coerce + .number() + .int() + .min(0) + .default(0) + .optional() + .describe( + "The record index from which the query will start. Increase by the size of the query to get the next page of results", + ), + size: z.coerce + .number() + .int() + .min(0) + .max(10) + .default(10) + .optional() + .describe("How many records will be retrieved from the ElasticSearch query. Maximum is 10"), + }); + } + + static { + this.register(); + } + + public constructor(options: ElasticSearchToolOptions) { + super(options); + if (!options.connection.cloud && !options.connection.node && !options.connection.nodes) { + throw new ValidationError([ + { + message: "At least one of the properties must be provided", + propertyName: "connection.cloud, connection.node, connection.nodes", + }, + ]); + } + } + + @Cache() + protected get client(): Client { + try { + return new Client(this.options.connection); + } catch (error) { + throw new ToolError(`Unable to connect to ElasticSearch: ${error}`, [], { + isRetryable: false, + isFatal: true, + }); + } + } + + protected async _run( + input: ToolInput, + _options?: ToolRunOptions, + ): Promise> { + if (input.action === "LIST_INDICES") { + const indices = await this.listIndices(_options?.signal); + return new JSONToolOutput(indices); + } else if (input.action === "GET_INDEX_DETAILS") { + const indexDetails = await this.getIndexDetails(input, _options?.signal); + return new JSONToolOutput(indexDetails); + } else if (input.action === "SEARCH") { + const response = await this.search(input, _options?.signal); + if (response.aggregations) { + return new JSONToolOutput(response.aggregations); + } else { + return new JSONToolOutput(response.hits.hits.map((hit: SearchHit) => hit._source)); + } + } else { + throw new ToolError("Invalid action specified."); + } + } + + private async listIndices(signal?: AbortSignal): Promise { + const response = await this.client.cat.indices( + { + expand_wildcards: "open", + h: "index", + format: "json", + }, + { signal: signal }, + ); + return response + .filter((record) => record.index && !record.index.startsWith(".")) // Exclude system indices + .map((record) => ({ index: record.index })); + } + + private async getIndexDetails( + input: ToolInput, + signal?: AbortSignal, + ): Promise { + if (!input.indexName) { + throw new ToolError("Index name is required for GET_INDEX_DETAILS action."); + } + return await this.client.indices.getMapping( + { + index: input.indexName, + }, + { signal: signal }, + ); + } + + private async search(input: ToolInput, signal?: AbortSignal): Promise { + if (!input.indexName || !input.query) { + throw new ToolError("Both index name and query are required for SEARCH action."); + } + let parsedQuery; + try { + parsedQuery = JSON.parse(input.query); + } catch { + throw new ToolError(`Invalid JSON format for query`); + } + + const searchBody: SearchRequest = { + ...parsedQuery, + from: parsedQuery.from || input.start, + size: parsedQuery.size || input.size, + }; + + return await this.client.search( + { + index: input.indexName, + body: searchBody, + }, + { signal: signal }, + ); + } + + loadSnapshot({ ...snapshot }: ReturnType): void { + super.loadSnapshot(snapshot); + } +} diff --git a/tests/e2e/utils.ts b/tests/e2e/utils.ts index 192129ec..4a03f7b6 100644 --- a/tests/e2e/utils.ts +++ b/tests/e2e/utils.ts @@ -31,6 +31,7 @@ import { OpenAI } from "openai"; import { Groq } from "groq-sdk"; import { customsearch_v1 } from "@googleapis/customsearch"; import { LangChainTool } from "@/adapters/langchain/tools.js"; +import { Client as esClient } from "@elastic/elasticsearch"; interface CallbackOptions { required?: boolean; @@ -127,6 +128,7 @@ verifyDeserialization.ignoredClasses = [ LCBaseLLM, RunContext, Emitter, + esClient, ] as ClassConstructor[]; verifyDeserialization.isIgnored = (key: string, value: unknown, parent?: any) => { if (verifyDeserialization.ignoredKeys.has(key)) { diff --git a/yarn.lock b/yarn.lock index 5d22fa4c..ac99300b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -327,6 +327,31 @@ __metadata: languageName: node linkType: hard +"@elastic/elasticsearch@npm:^8.15.1": + version: 8.15.1 + resolution: "@elastic/elasticsearch@npm:8.15.1" + dependencies: + "@elastic/transport": "npm:^8.8.1" + tslib: "npm:^2.4.0" + checksum: 10c0/6fed56487e0bd5c2e8a54e794cd1ebe58e0ff40319b4b8d10ef3cb45534a572fd4d6d5aff5ca63707aaf4be7abbc073ba5bf414cea129f86facc5b1c576cb815 + languageName: node + linkType: hard + +"@elastic/transport@npm:^8.8.1": + version: 8.9.1 + resolution: "@elastic/transport@npm:8.9.1" + dependencies: + "@opentelemetry/api": "npm:1.x" + debug: "npm:^4.3.4" + hpagent: "npm:^1.0.0" + ms: "npm:^2.1.3" + secure-json-parse: "npm:^2.4.0" + tslib: "npm:^2.4.0" + undici: "npm:^6.12.0" + checksum: 10c0/a79fee3091dd9b9cfa70af5835ac8b362f43e783cbe28112312a3759944066613305764ce19c22941dfaf6e7157c4398eaadd1ac65b22ed8cdcf165a92f553c4 + languageName: node + linkType: hard + "@esbuild/aix-ppc64@npm:0.21.5": version: 0.21.5 resolution: "@esbuild/aix-ppc64@npm:0.21.5" @@ -1573,7 +1598,7 @@ __metadata: languageName: node linkType: hard -"@opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.9.0": +"@opentelemetry/api@npm:1.x, @opentelemetry/api@npm:^1.3.0, @opentelemetry/api@npm:^1.9.0": version: 1.9.0 resolution: "@opentelemetry/api@npm:1.9.0" checksum: 10c0/9aae2fe6e8a3a3eeb6c1fdef78e1939cf05a0f37f8a4fae4d6bf2e09eb1e06f966ece85805626e01ba5fab48072b94f19b835449e58b6d26720ee19a58298add @@ -3232,6 +3257,7 @@ __metadata: "@commitlint/config-conventional": "npm:^19.5.0" "@connectrpc/connect": "npm:^1.6.1" "@connectrpc/connect-node": "npm:^1.6.1" + "@elastic/elasticsearch": "npm:^8.15.1" "@eslint/js": "npm:^9.13.0" "@eslint/markdown": "npm:^6.2.1" "@googleapis/customsearch": "npm:^3.2.0" @@ -3321,6 +3347,7 @@ __metadata: zod: "npm:^3.23.8" zod-to-json-schema: "npm:^3.23.3" peerDependencies: + "@elastic/elasticsearch": ^8.15.1 "@googleapis/customsearch": ^3.2.0 "@grpc/grpc-js": ^1.11.3 "@grpc/proto-loader": ^0.7.13 @@ -6364,6 +6391,13 @@ __metadata: languageName: node linkType: hard +"hpagent@npm:^1.0.0": + version: 1.2.0 + resolution: "hpagent@npm:1.2.0" + checksum: 10c0/505ef42e5e067dba701ea21e7df9fa73f6f5080e59d53680829827d34cd7040f1ecf7c3c8391abe9df4eb4682ef4a4321608836b5b70a61b88c1b3a03d77510b + languageName: node + linkType: hard + "html-entities@npm:^2.3.3, html-entities@npm:^2.5.2": version: 2.5.2 resolution: "html-entities@npm:2.5.2" @@ -11777,6 +11811,13 @@ __metadata: languageName: node linkType: hard +"undici@npm:^6.12.0": + version: 6.20.1 + resolution: "undici@npm:6.20.1" + checksum: 10c0/b2c8d5adcd226c53d02f9270e4cac277256a7147cf310af319369ec6f87651ca46b2960366cb1339a6dac84d937e01e8cdbec5cb468f1f1ce5e9490e438d7222 + languageName: node + linkType: hard + "unicorn-magic@npm:^0.1.0": version: 0.1.0 resolution: "unicorn-magic@npm:0.1.0" From 62cec54876784b2b327ae7569792b5b86f457cdc Mon Sep 17 00:00:00 2001 From: Mahmoud Abughali Date: Mon, 4 Nov 2024 21:30:34 -0500 Subject: [PATCH 2/8] fix: apply suggested changes and fixes Signed-off-by: Mahmoud Abughali --- examples/agents/elasticsearch.ts | 2 - src/tools/database/elasticsearch.test.ts | 24 ++++----- src/tools/database/elasticsearch.ts | 66 +++++++++++++----------- 3 files changed, 47 insertions(+), 45 deletions(-) diff --git a/examples/agents/elasticsearch.ts b/examples/agents/elasticsearch.ts index 617d6599..b5b8ebbd 100644 --- a/examples/agents/elasticsearch.ts +++ b/examples/agents/elasticsearch.ts @@ -55,6 +55,4 @@ try { console.log(`Agent 🤖 : `, response.result.text); } catch (error) { console.error(FrameworkError.ensure(error).dump()); -} finally { - process.exit(0); } diff --git a/src/tools/database/elasticsearch.test.ts b/src/tools/database/elasticsearch.test.ts index 03aa5d80..9a11af42 100644 --- a/src/tools/database/elasticsearch.test.ts +++ b/src/tools/database/elasticsearch.test.ts @@ -21,25 +21,25 @@ import { JSONToolOutput } from "@/tools/base.js"; import { SlidingCache } from "@/cache/slidingCache.js"; import { Task } from "promise-based-task"; -vi.mock("@elastic/elasticsearch"); +const mockClient = { + cat: { indices: vi.fn() }, + indices: { getMapping: vi.fn() }, + search: vi.fn(), + info: vi.fn(), +}; + +vi.mock("@elastic/elasticsearch", () => ({ + Client: vi.fn(() => mockClient), +})); describe("ElasticSearchTool", () => { let elasticSearchTool: ElasticSearchTool; - const mockClient = { - cat: { indices: vi.fn() }, - indices: { getMapping: vi.fn() }, - search: vi.fn(), - }; beforeEach(() => { vi.clearAllMocks(); elasticSearchTool = new ElasticSearchTool({ connection: { node: "http://localhost:9200" }, } as ElasticSearchToolOptions); - - Object.defineProperty(elasticSearchTool, "client", { - get: () => mockClient, - }); }); it("lists indices correctly", async () => { @@ -59,10 +59,6 @@ describe("ElasticSearchTool", () => { const response = await elasticSearchTool.run({ action: "GET_INDEX_DETAILS", indexName }); expect(response.result).toEqual(mockIndexDetails); - expect(mockClient.indices.getMapping).toHaveBeenCalledWith( - { index: indexName }, - { signal: undefined }, - ); }); it("performs a search", async () => { diff --git a/src/tools/database/elasticsearch.ts b/src/tools/database/elasticsearch.ts index 54421b2c..de6f8a53 100644 --- a/src/tools/database/elasticsearch.ts +++ b/src/tools/database/elasticsearch.ts @@ -23,6 +23,7 @@ import { JSONToolOutput, } from "@/tools/base.js"; import { Cache } from "@/cache/decoratorCache.js"; +import { RunContext } from "@/context.js"; import { z } from "zod"; import { Client, ClientOptions } from "@elastic/elasticsearch"; import { @@ -34,28 +35,34 @@ import { } from "@elastic/elasticsearch/lib/api/types.js"; import { ValidationError } from "ajv"; -type ToolRunOptions = BaseToolRunOptions; - export interface ElasticSearchToolOptions extends BaseToolOptions { connection: ClientOptions; } +export type ElasticSearchToolResult = CatIndicesResponse | IndicesGetMappingResponse | SearchResponse; + +export const ElasticSearchAction = { + ListIndices: "LIST_INDICES", + getIndexDetails: "GET_INDEX_DETAILS", + search: "SEARCH", +}; + export class ElasticSearchTool extends Tool< - JSONToolOutput, + JSONToolOutput, ElasticSearchToolOptions, - ToolRunOptions + BaseToolRunOptions > { name = "ElasticSearchTool"; description = `Can query data from an ElasticSearch database. IMPORTANT: strictly follow this order of actions: - 1. LIST_INDICES - retrieve a list of available indices - 2. GET_INDEX_DETAILS - get details of index fields - 3. SEARCH - perform search or aggregation query on a specific index or pass the original user query without modifications if it's a valid JSON ElasticSearch query`; + 1. ${ElasticSearchAction.ListIndices} - retrieve a list of available indices + 2. ${ElasticSearchAction.getIndexDetails} - get details of index fields + 3. ${ElasticSearchAction.search} - perform search or aggregation query on a specific index or pass the original user query without modifications if it's a valid JSON ElasticSearch query`; inputSchema() { return z.object({ action: z - .enum(["LIST_INDICES", "GET_INDEX_DETAILS", "SEARCH"]) + .nativeEnum(ElasticSearchAction) .describe( "The action to perform. LIST_INDICES lists all indices, GET_INDEX_DETAILS fetches details for a specified index, and SEARCH executes a search or aggregation query", ), @@ -104,9 +111,11 @@ export class ElasticSearchTool extends Tool< } @Cache() - protected get client(): Client { + protected async client(): Promise { try { - return new Client(this.options.connection); + const client = new Client(this.options.connection); + await client.info(); + return client; } catch (error) { throw new ToolError(`Unable to connect to ElasticSearch: ${error}`, [], { isRetryable: false, @@ -117,28 +126,30 @@ export class ElasticSearchTool extends Tool< protected async _run( input: ToolInput, - _options?: ToolRunOptions, + _options: BaseToolRunOptions | undefined, + run: RunContext, ): Promise> { - if (input.action === "LIST_INDICES") { - const indices = await this.listIndices(_options?.signal); + if (input.action === ElasticSearchAction.ListIndices) { + const indices = await this.listIndices(run.signal); return new JSONToolOutput(indices); - } else if (input.action === "GET_INDEX_DETAILS") { - const indexDetails = await this.getIndexDetails(input, _options?.signal); + } else if (input.action === ElasticSearchAction.getIndexDetails) { + const indexDetails = await this.getIndexDetails(input, run.signal); return new JSONToolOutput(indexDetails); - } else if (input.action === "SEARCH") { - const response = await this.search(input, _options?.signal); + } else if (input.action === ElasticSearchAction.search) { + const response = await this.search(input, run.signal); if (response.aggregations) { return new JSONToolOutput(response.aggregations); } else { return new JSONToolOutput(response.hits.hits.map((hit: SearchHit) => hit._source)); } } else { - throw new ToolError("Invalid action specified."); + throw new ToolError(`Invalid action specified: ${input.action}`); } } private async listIndices(signal?: AbortSignal): Promise { - const response = await this.client.cat.indices( + const client = await this.client(); + const response = await client.cat.indices( { expand_wildcards: "open", h: "index", @@ -151,14 +162,15 @@ export class ElasticSearchTool extends Tool< .map((record) => ({ index: record.index })); } - private async getIndexDetails( + protected async getIndexDetails( input: ToolInput, - signal?: AbortSignal, + signal: AbortSignal, ): Promise { if (!input.indexName) { throw new ToolError("Index name is required for GET_INDEX_DETAILS action."); } - return await this.client.indices.getMapping( + const client = await this.client(); + return await client.indices.getMapping( { index: input.indexName, }, @@ -166,7 +178,7 @@ export class ElasticSearchTool extends Tool< ); } - private async search(input: ToolInput, signal?: AbortSignal): Promise { + protected async search(input: ToolInput, signal: AbortSignal): Promise { if (!input.indexName || !input.query) { throw new ToolError("Both index name and query are required for SEARCH action."); } @@ -182,8 +194,8 @@ export class ElasticSearchTool extends Tool< from: parsedQuery.from || input.start, size: parsedQuery.size || input.size, }; - - return await this.client.search( + const client = await this.client(); + return await client.search( { index: input.indexName, body: searchBody, @@ -191,8 +203,4 @@ export class ElasticSearchTool extends Tool< { signal: signal }, ); } - - loadSnapshot({ ...snapshot }: ReturnType): void { - super.loadSnapshot(snapshot); - } } From bb64d4ac786f119d466f6d13ac130752dbb0c11c Mon Sep 17 00:00:00 2001 From: Mahmoud Abughali Date: Mon, 4 Nov 2024 21:33:11 -0500 Subject: [PATCH 3/8] fix: code style Signed-off-by: Mahmoud Abughali --- src/tools/database/elasticsearch.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/tools/database/elasticsearch.ts b/src/tools/database/elasticsearch.ts index de6f8a53..4608d111 100644 --- a/src/tools/database/elasticsearch.ts +++ b/src/tools/database/elasticsearch.ts @@ -39,7 +39,10 @@ export interface ElasticSearchToolOptions extends BaseToolOptions { connection: ClientOptions; } -export type ElasticSearchToolResult = CatIndicesResponse | IndicesGetMappingResponse | SearchResponse; +export type ElasticSearchToolResult = + | CatIndicesResponse + | IndicesGetMappingResponse + | SearchResponse; export const ElasticSearchAction = { ListIndices: "LIST_INDICES", From 0de0783494b9aff1d662d454e8dcf6cb19468c57 Mon Sep 17 00:00:00 2001 From: Mahmoud Abughali Date: Wed, 6 Nov 2024 08:16:02 -0500 Subject: [PATCH 4/8] chore: improve input validation for elastic search tool Signed-off-by: Mahmoud Abughali --- examples/agents/elasticsearch.ts | 6 +----- src/tools/database/elasticsearch.ts | 29 +++++++++++++++++++++-------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/examples/agents/elasticsearch.ts b/examples/agents/elasticsearch.ts index b5b8ebbd..0b7a3a25 100644 --- a/examples/agents/elasticsearch.ts +++ b/examples/agents/elasticsearch.ts @@ -5,11 +5,7 @@ import { ElasticSearchTool } from "bee-agent-framework/tools/database/elasticsea import { FrameworkError } from "bee-agent-framework/errors"; import { UnconstrainedMemory } from "bee-agent-framework/memory/unconstrainedMemory"; -const llm = new OpenAIChatLLM({ - parameters: { - temperature: 0, - }, -}); +const llm = new OpenAIChatLLM(); const elasticSearchTool = new ElasticSearchTool({ connection: { diff --git a/src/tools/database/elasticsearch.ts b/src/tools/database/elasticsearch.ts index 4608d111..06c1ebb3 100644 --- a/src/tools/database/elasticsearch.ts +++ b/src/tools/database/elasticsearch.ts @@ -21,10 +21,13 @@ import { BaseToolOptions, BaseToolRunOptions, JSONToolOutput, + ToolInputValidationError, } from "@/tools/base.js"; import { Cache } from "@/cache/decoratorCache.js"; import { RunContext } from "@/context.js"; import { z } from "zod"; +import { ValidationError } from "ajv"; +import { AnyToolSchemaLike } from "@/internals/helpers/schema.js"; import { Client, ClientOptions } from "@elastic/elasticsearch"; import { CatIndicesResponse, @@ -33,7 +36,6 @@ import { SearchResponse, SearchHit, } from "@elastic/elasticsearch/lib/api/types.js"; -import { ValidationError } from "ajv"; export interface ElasticSearchToolOptions extends BaseToolOptions { connection: ClientOptions; @@ -97,6 +99,23 @@ export class ElasticSearchTool extends Tool< }); } + protected validateInput( + schema: AnyToolSchemaLike, + input: unknown, + ): asserts input is ToolInput { + super.validateInput(schema, input); + if (input.action === ElasticSearchAction.getIndexDetails && !input.indexName) { + throw new ToolInputValidationError( + `Index name is required for ${ElasticSearchAction.getIndexDetails} action.`, + ); + } + if (input.action === ElasticSearchAction.search && (!input.indexName || !input.query)) { + throw new ToolInputValidationError( + `Both index name and query are required for ${ElasticSearchAction.search} action.`, + ); + } + } + static { this.register(); } @@ -169,9 +188,6 @@ export class ElasticSearchTool extends Tool< input: ToolInput, signal: AbortSignal, ): Promise { - if (!input.indexName) { - throw new ToolError("Index name is required for GET_INDEX_DETAILS action."); - } const client = await this.client(); return await client.indices.getMapping( { @@ -182,12 +198,9 @@ export class ElasticSearchTool extends Tool< } protected async search(input: ToolInput, signal: AbortSignal): Promise { - if (!input.indexName || !input.query) { - throw new ToolError("Both index name and query are required for SEARCH action."); - } let parsedQuery; try { - parsedQuery = JSON.parse(input.query); + parsedQuery = JSON.parse(input.query!); } catch { throw new ToolError(`Invalid JSON format for query`); } From 6b950654c0c52b0290e5d25be63d4993b1ef4eab Mon Sep 17 00:00:00 2001 From: Mahmoud Abughali Date: Wed, 6 Nov 2024 15:40:44 -0500 Subject: [PATCH 5/8] chore: enhance json query validation Signed-off-by: Mahmoud Abughali --- src/tools/database/elasticsearch.ts | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/tools/database/elasticsearch.ts b/src/tools/database/elasticsearch.ts index 06c1ebb3..70c74060 100644 --- a/src/tools/database/elasticsearch.ts +++ b/src/tools/database/elasticsearch.ts @@ -114,6 +114,13 @@ export class ElasticSearchTool extends Tool< `Both index name and query are required for ${ElasticSearchAction.search} action.`, ); } + if (input.action === ElasticSearchAction.search && input.query) { + try { + JSON.parse(input.query); + } catch (error) { + throw new ToolInputValidationError(`Invalid JSON format for query ${error}`); + } + } } static { @@ -169,7 +176,7 @@ export class ElasticSearchTool extends Tool< } } - private async listIndices(signal?: AbortSignal): Promise { + protected async listIndices(signal?: AbortSignal): Promise { const client = await this.client(); const response = await client.cat.indices( { @@ -198,13 +205,7 @@ export class ElasticSearchTool extends Tool< } protected async search(input: ToolInput, signal: AbortSignal): Promise { - let parsedQuery; - try { - parsedQuery = JSON.parse(input.query!); - } catch { - throw new ToolError(`Invalid JSON format for query`); - } - + const parsedQuery = JSON.parse(input.query!); const searchBody: SearchRequest = { ...parsedQuery, from: parsedQuery.from || input.start, From 60ac5128581257fe9418677eec70a3a61561794a Mon Sep 17 00:00:00 2001 From: Mahmoud Abughali Date: Fri, 8 Nov 2024 10:22:17 -0500 Subject: [PATCH 6/8] chore: apply suggested changes Signed-off-by: Mahmoud Abughali --- src/tools/database/elasticsearch.ts | 36 ++++++++++++++++------------- 1 file changed, 20 insertions(+), 16 deletions(-) diff --git a/src/tools/database/elasticsearch.ts b/src/tools/database/elasticsearch.ts index 70c74060..de65d7a3 100644 --- a/src/tools/database/elasticsearch.ts +++ b/src/tools/database/elasticsearch.ts @@ -48,9 +48,9 @@ export type ElasticSearchToolResult = export const ElasticSearchAction = { ListIndices: "LIST_INDICES", - getIndexDetails: "GET_INDEX_DETAILS", - search: "SEARCH", -}; + GetIndexDetails: "GET_INDEX_DETAILS", + Search: "SEARCH", +} as const; export class ElasticSearchTool extends Tool< JSONToolOutput, @@ -61,24 +61,28 @@ export class ElasticSearchTool extends Tool< description = `Can query data from an ElasticSearch database. IMPORTANT: strictly follow this order of actions: 1. ${ElasticSearchAction.ListIndices} - retrieve a list of available indices - 2. ${ElasticSearchAction.getIndexDetails} - get details of index fields - 3. ${ElasticSearchAction.search} - perform search or aggregation query on a specific index or pass the original user query without modifications if it's a valid JSON ElasticSearch query`; + 2. ${ElasticSearchAction.GetIndexDetails} - get details of index fields + 3. ${ElasticSearchAction.Search} - perform search or aggregation query on a specific index or pass the original user query without modifications if it's a valid JSON ElasticSearch query after identifying the index`; inputSchema() { return z.object({ action: z .nativeEnum(ElasticSearchAction) .describe( - "The action to perform. LIST_INDICES lists all indices, GET_INDEX_DETAILS fetches details for a specified index, and SEARCH executes a search or aggregation query", + `The action to perform. ${ElasticSearchAction.ListIndices} lists all indices, ${ElasticSearchAction.GetIndexDetails} fetches details for a specified index, and ${ElasticSearchAction.Search} executes a search or aggregation query`, ), indexName: z .string() .optional() - .describe("The name of the index to query, required for GET_INDEX_DETAILS and SEARCH"), + .describe( + `The name of the index to query, required for ${ElasticSearchAction.GetIndexDetails} and ${ElasticSearchAction.Search}`, + ), query: z .string() .optional() - .describe("Valid ElasticSearch JSON search or aggregation query for SEARCH action"), + .describe( + `Valid ElasticSearch JSON search or aggregation query for ${ElasticSearchAction.Search} action`, + ), start: z.coerce .number() .int() @@ -104,21 +108,21 @@ export class ElasticSearchTool extends Tool< input: unknown, ): asserts input is ToolInput { super.validateInput(schema, input); - if (input.action === ElasticSearchAction.getIndexDetails && !input.indexName) { + if (input.action === ElasticSearchAction.GetIndexDetails && !input.indexName) { throw new ToolInputValidationError( - `Index name is required for ${ElasticSearchAction.getIndexDetails} action.`, + `Index name is required for ${ElasticSearchAction.GetIndexDetails} action.`, ); } - if (input.action === ElasticSearchAction.search && (!input.indexName || !input.query)) { + if (input.action === ElasticSearchAction.Search && (!input.indexName || !input.query)) { throw new ToolInputValidationError( - `Both index name and query are required for ${ElasticSearchAction.search} action.`, + `Both index name and query are required for ${ElasticSearchAction.Search} action.`, ); } - if (input.action === ElasticSearchAction.search && input.query) { + if (input.action === ElasticSearchAction.Search && input.query) { try { JSON.parse(input.query); } catch (error) { - throw new ToolInputValidationError(`Invalid JSON format for query ${error}`); + throw new ToolInputValidationError(`Invalid JSON format for query.`, [error]); } } } @@ -161,10 +165,10 @@ export class ElasticSearchTool extends Tool< if (input.action === ElasticSearchAction.ListIndices) { const indices = await this.listIndices(run.signal); return new JSONToolOutput(indices); - } else if (input.action === ElasticSearchAction.getIndexDetails) { + } else if (input.action === ElasticSearchAction.GetIndexDetails) { const indexDetails = await this.getIndexDetails(input, run.signal); return new JSONToolOutput(indexDetails); - } else if (input.action === ElasticSearchAction.search) { + } else if (input.action === ElasticSearchAction.Search) { const response = await this.search(input, run.signal); if (response.aggregations) { return new JSONToolOutput(response.aggregations); From 2cf5b203a95c0d7c11808b33ab2f576a3c353120 Mon Sep 17 00:00:00 2001 From: Mahmoud Abughali Date: Fri, 8 Nov 2024 21:06:26 -0500 Subject: [PATCH 7/8] chore: use parseBrokenJson to fix input query Signed-off-by: Mahmoud Abughali --- src/tools/database/elasticsearch.test.ts | 10 ---------- src/tools/database/elasticsearch.ts | 10 ++-------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/src/tools/database/elasticsearch.test.ts b/src/tools/database/elasticsearch.test.ts index 9a11af42..18bee4ea 100644 --- a/src/tools/database/elasticsearch.test.ts +++ b/src/tools/database/elasticsearch.test.ts @@ -77,16 +77,6 @@ describe("ElasticSearchTool", () => { expect(response.result).toEqual([{ field1: "value1" }]); }); - it("throws invalid JSON format error", async () => { - await expect(async () => { - await elasticSearchTool.run({ action: "SEARCH", indexName: "index1", query: "invalid" }); - }).rejects.toThrowError( - expect.objectContaining({ - message: expect.stringContaining("Invalid JSON format for query"), - }), - ); - }); - it("throws missing index name error", async () => { await expect(elasticSearchTool.run({ action: "GET_INDEX_DETAILS" })).rejects.toThrow( "Index name is required for GET_INDEX_DETAILS action.", diff --git a/src/tools/database/elasticsearch.ts b/src/tools/database/elasticsearch.ts index de65d7a3..04e356ea 100644 --- a/src/tools/database/elasticsearch.ts +++ b/src/tools/database/elasticsearch.ts @@ -28,6 +28,7 @@ import { RunContext } from "@/context.js"; import { z } from "zod"; import { ValidationError } from "ajv"; import { AnyToolSchemaLike } from "@/internals/helpers/schema.js"; +import { parseBrokenJson } from "@/internals/helpers/schema.js"; import { Client, ClientOptions } from "@elastic/elasticsearch"; import { CatIndicesResponse, @@ -118,13 +119,6 @@ export class ElasticSearchTool extends Tool< `Both index name and query are required for ${ElasticSearchAction.Search} action.`, ); } - if (input.action === ElasticSearchAction.Search && input.query) { - try { - JSON.parse(input.query); - } catch (error) { - throw new ToolInputValidationError(`Invalid JSON format for query.`, [error]); - } - } } static { @@ -209,7 +203,7 @@ export class ElasticSearchTool extends Tool< } protected async search(input: ToolInput, signal: AbortSignal): Promise { - const parsedQuery = JSON.parse(input.query!); + const parsedQuery = parseBrokenJson(input.query); const searchBody: SearchRequest = { ...parsedQuery, from: parsedQuery.from || input.start, From f34fe23bd099280b3cde38044696c7c4c954f752 Mon Sep 17 00:00:00 2001 From: Tomas Dvorak Date: Tue, 12 Nov 2024 17:03:45 +0100 Subject: [PATCH 8/8] feat(tool): elasticsearch improvements Signed-off-by: Tomas Dvorak --- examples/agents/elasticsearch.ts | 6 ++++-- package.json | 4 ++-- src/tools/database/elasticsearch.ts | 7 +++---- tests/examples/examples.test.ts | 1 + yarn.lock | 12 ++++++------ 5 files changed, 16 insertions(+), 14 deletions(-) diff --git a/examples/agents/elasticsearch.ts b/examples/agents/elasticsearch.ts index 0b7a3a25..1a488ac4 100644 --- a/examples/agents/elasticsearch.ts +++ b/examples/agents/elasticsearch.ts @@ -4,6 +4,7 @@ import { OpenAIChatLLM } from "bee-agent-framework/adapters/openai/chat"; import { ElasticSearchTool } from "bee-agent-framework/tools/database/elasticsearch"; import { FrameworkError } from "bee-agent-framework/errors"; import { UnconstrainedMemory } from "bee-agent-framework/memory/unconstrainedMemory"; +import { createConsoleReader } from "../helpers/io.js"; const llm = new OpenAIChatLLM(); @@ -22,12 +23,13 @@ const agent = new BeeAgent({ tools: [elasticSearchTool], }); -const question = "what is the average ticket price of all flights from Cape Town to Venice"; +const reader = createConsoleReader(); +const prompt = await reader.prompt(); try { const response = await agent .run( - { prompt: `${question}` }, + { prompt }, { execution: { maxRetriesPerStep: 5, diff --git a/package.json b/package.json index af0be6f1..2bc1c1b0 100644 --- a/package.json +++ b/package.json @@ -173,7 +173,7 @@ "zod-to-json-schema": "^3.23.3" }, "peerDependencies": { - "@elastic/elasticsearch": "^8.15.1", + "@elastic/elasticsearch": "^8.0.0", "@googleapis/customsearch": "^3.2.0", "@grpc/grpc-js": "^1.11.3", "@grpc/proto-loader": "^0.7.13", @@ -189,7 +189,7 @@ "devDependencies": { "@commitlint/cli": "^19.5.0", "@commitlint/config-conventional": "^19.5.0", - "@elastic/elasticsearch": "^8.15.1", + "@elastic/elasticsearch": "^8.0.0", "@eslint/js": "^9.13.0", "@eslint/markdown": "^6.2.1", "@googleapis/customsearch": "^3.2.0", diff --git a/src/tools/database/elasticsearch.ts b/src/tools/database/elasticsearch.ts index 04e356ea..162d7126 100644 --- a/src/tools/database/elasticsearch.ts +++ b/src/tools/database/elasticsearch.ts @@ -30,7 +30,7 @@ import { ValidationError } from "ajv"; import { AnyToolSchemaLike } from "@/internals/helpers/schema.js"; import { parseBrokenJson } from "@/internals/helpers/schema.js"; import { Client, ClientOptions } from "@elastic/elasticsearch"; -import { +import type { CatIndicesResponse, IndicesGetMappingResponse, SearchRequest, @@ -55,8 +55,7 @@ export const ElasticSearchAction = { export class ElasticSearchTool extends Tool< JSONToolOutput, - ElasticSearchToolOptions, - BaseToolRunOptions + ElasticSearchToolOptions > { name = "ElasticSearchTool"; @@ -144,7 +143,7 @@ export class ElasticSearchTool extends Tool< await client.info(); return client; } catch (error) { - throw new ToolError(`Unable to connect to ElasticSearch: ${error}`, [], { + throw new ToolError(`Unable to connect to ElasticSearch.`, [error], { isRetryable: false, isFatal: true, }); diff --git a/tests/examples/examples.test.ts b/tests/examples/examples.test.ts index c5ab2d59..b44da204 100644 --- a/tests/examples/examples.test.ts +++ b/tests/examples/examples.test.ts @@ -51,6 +51,7 @@ const exclude: string[] = [ !hasEnv("COHERE_API_KEY") && ["examples/llms/providers/langchain.ts"], !hasEnv("CODE_INTERPRETER_URL") && ["examples/tools/custom/python.ts"], ["examples/llms/providers/bam.ts", "examples/llms/providers/bam_verbose.ts"], + !hasEnv("ELASTICSEARCH_NODE") && ["examples/agents/elasticsearch.ts"], ] .filter(isTruthy) .flat(); // list of examples that are excluded diff --git a/yarn.lock b/yarn.lock index ac99300b..763329ce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -327,13 +327,13 @@ __metadata: languageName: node linkType: hard -"@elastic/elasticsearch@npm:^8.15.1": - version: 8.15.1 - resolution: "@elastic/elasticsearch@npm:8.15.1" +"@elastic/elasticsearch@npm:^8.0.0": + version: 8.15.2 + resolution: "@elastic/elasticsearch@npm:8.15.2" dependencies: "@elastic/transport": "npm:^8.8.1" tslib: "npm:^2.4.0" - checksum: 10c0/6fed56487e0bd5c2e8a54e794cd1ebe58e0ff40319b4b8d10ef3cb45534a572fd4d6d5aff5ca63707aaf4be7abbc073ba5bf414cea129f86facc5b1c576cb815 + checksum: 10c0/c37775d17b14c640204c28be5113f8db6361f30b41df05bad997e49a2f3436433d5ca5748ae172cd4c774c7ae182e38790ec735eee2afa607fa1b0449048c2a0 languageName: node linkType: hard @@ -3257,7 +3257,7 @@ __metadata: "@commitlint/config-conventional": "npm:^19.5.0" "@connectrpc/connect": "npm:^1.6.1" "@connectrpc/connect-node": "npm:^1.6.1" - "@elastic/elasticsearch": "npm:^8.15.1" + "@elastic/elasticsearch": "npm:^8.0.0" "@eslint/js": "npm:^9.13.0" "@eslint/markdown": "npm:^6.2.1" "@googleapis/customsearch": "npm:^3.2.0" @@ -3347,7 +3347,7 @@ __metadata: zod: "npm:^3.23.8" zod-to-json-schema: "npm:^3.23.3" peerDependencies: - "@elastic/elasticsearch": ^8.15.1 + "@elastic/elasticsearch": ^8.0.0 "@googleapis/customsearch": ^3.2.0 "@grpc/grpc-js": ^1.11.3 "@grpc/proto-loader": ^0.7.13