diff --git a/src/backend/base/langflow/components/tools/__init__.py b/src/backend/base/langflow/components/tools/__init__.py index deda86293..8be4d8833 100644 --- a/src/backend/base/langflow/components/tools/__init__.py +++ b/src/backend/base/langflow/components/tools/__init__.py @@ -16,6 +16,7 @@ from .mcp_stdio import MCPStdio from .python_code_structured_tool import PythonCodeStructuredTool from .python_repl import PythonREPLToolComponent from .python_repl_core import PythonREPLComponent +from .search import SearchComponent from .search_api import SearchAPIComponent from .searxng import SearXNGToolComponent from .serp import SerpComponent @@ -53,6 +54,7 @@ __all__ = [ "PythonREPLToolComponent", "SearXNGToolComponent", "SearchAPIComponent", + "SearchComponent", "SerpAPIComponent", "SerpComponent", "TavilySearchComponent", diff --git a/src/backend/base/langflow/components/tools/search.py b/src/backend/base/langflow/components/tools/search.py new file mode 100644 index 000000000..362cbdf21 --- /dev/null +++ b/src/backend/base/langflow/components/tools/search.py @@ -0,0 +1,79 @@ +from typing import Any + +from langchain_community.utilities.searchapi import SearchApiAPIWrapper + +from langflow.custom import Component +from langflow.inputs import DictInput, DropdownInput, IntInput, MultilineInput, SecretStrInput +from langflow.io import Output +from langflow.schema import Data +from langflow.schema.message import Message + + +class SearchComponent(Component): + display_name: str = "Search API" + description: str = "Call the searchapi.io API with result limiting" + documentation: str = "https://www.searchapi.io/docs/google" + icon = "SearchAPI" + + inputs = [ + DropdownInput(name="engine", display_name="Engine", value="google", options=["google", "bing", "duckduckgo"]), + SecretStrInput(name="api_key", display_name="SearchAPI API Key", required=True), + MultilineInput( + name="input_value", + display_name="Input", + tool_mode=True, + ), + DictInput(name="search_params", display_name="Search parameters", advanced=True, is_list=True), + IntInput(name="max_results", display_name="Max Results", value=5, advanced=True), + IntInput(name="max_snippet_length", display_name="Max Snippet Length", value=100, advanced=True), + ] + + outputs = [ + Output(display_name="Data", name="data", method="fetch_content"), + Output(display_name="Text", name="text", method="fetch_content_text"), + ] + + def _build_wrapper(self): + return SearchApiAPIWrapper(engine=self.engine, searchapi_api_key=self.api_key) + + def run_model(self) -> list[Data]: + return self.fetch_content() + + def fetch_content(self) -> list[Data]: + wrapper = self._build_wrapper() + + def search_func( + query: str, params: dict[str, Any] | None = None, max_results: int = 5, max_snippet_length: int = 100 + ) -> list[Data]: + params = params or {} + full_results = wrapper.results(query=query, **params) + organic_results = full_results.get("organic_results", [])[:max_results] + + return [ + Data( + text=result.get("snippet", ""), + data={ + "title": result.get("title", "")[:max_snippet_length], + "link": result.get("link", ""), + "snippet": result.get("snippet", "")[:max_snippet_length], + }, + ) + for result in organic_results + ] + + results = search_func( + self.input_value, + self.search_params or {}, + self.max_results, + self.max_snippet_length, + ) + self.status = results + return results + + def fetch_content_text(self) -> Message: + data = self.fetch_content() + result_string = "" + for item in data: + result_string += item.text + "\n" + self.status = result_string + return Message(text=result_string) diff --git a/src/backend/base/langflow/components/tools/search_api.py b/src/backend/base/langflow/components/tools/search_api.py index fdb0ecbc9..80f8cc61c 100644 --- a/src/backend/base/langflow/components/tools/search_api.py +++ b/src/backend/base/langflow/components/tools/search_api.py @@ -11,11 +11,12 @@ from langflow.schema import Data class SearchAPIComponent(LCToolComponent): - display_name: str = "Search API" + display_name: str = "Search API [DEPRECATED]" description: str = "Call the searchapi.io API with result limiting" name = "SearchAPI" documentation: str = "https://www.searchapi.io/docs/google" icon = "SearchAPI" + legacy = True inputs = [ MessageTextInput(name="engine", display_name="Engine", value="google"), diff --git a/src/frontend/tests/extended/regression/generalBugs-shard-11.spec.ts b/src/frontend/tests/extended/regression/generalBugs-shard-11.spec.ts index 107b17a55..696402eed 100644 --- a/src/frontend/tests/extended/regression/generalBugs-shard-11.spec.ts +++ b/src/frontend/tests/extended/regression/generalBugs-shard-11.spec.ts @@ -1,5 +1,6 @@ import { expect, test } from "@playwright/test"; import { awaitBootstrapTest } from "../../utils/await-bootstrap-test"; +import { zoomOut } from "../../utils/zoom-out"; test( "user should be able to use ComposIO without getting api_key error", @@ -42,53 +43,49 @@ test( async ({ page }) => { await awaitBootstrapTest(page); - await page.waitForSelector('[data-testid="blank-flow"]', { - timeout: 30000, - }); await page.getByTestId("blank-flow").click(); + + //first component + await page.getByTestId("sidebar-search-input").click(); await page.getByTestId("sidebar-search-input").fill("search api"); - await page.waitForSelector('[data-testid="toolsSearch API"]', { - timeout: 3000, + timeout: 1000, }); - let modelElement = await page.getByTestId("toolsSearch API"); - let targetElement = await page.locator('//*[@id="react-flow-id"]'); - await modelElement.dragTo(targetElement); + await zoomOut(page, 3); - await page.mouse.up(); - await page.mouse.down(); - - await page.getByTestId("fit_view").click(); - await page.getByTestId("zoom_out").click(); - await page.getByTestId("zoom_out").click(); + await page + .getByTestId("toolsSearch API") + .dragTo(page.locator('//*[@id="react-flow-id"]'), { + targetPosition: { x: 100, y: 100 }, + }); await page.getByTestId("sidebar-search-input").click(); await page.getByTestId("sidebar-search-input").fill("tool calling agent"); - await page.waitForSelector( '[data-testid="langchain_utilitiesTool Calling Agent"]', { - timeout: 3000, + timeout: 1000, }, ); - modelElement = page.getByTestId("langchain_utilitiesTool Calling Agent"); - targetElement = await page.locator('//*[@id="react-flow-id"]'); - await modelElement.dragTo(targetElement); - - await page.mouse.up(); - await page.mouse.down(); + await page + .getByTestId("langchain_utilitiesTool Calling Agent") + .dragTo(page.locator('//*[@id="react-flow-id"]'), { + targetPosition: { x: 300, y: 300 }, + }); await page.getByTestId("fit_view").click(); - await page.getByTestId("zoom_out").click(); - await page.getByTestId("zoom_out").click(); + + await page.getByTestId("title-Search API").first().click(); + await page.getByTestId("tool-mode-button").click(); //connection const searchApiOutput = await page - .getByTestId("handle-searchapi-shownode-tool-right") - .nth(0); + .getByTestId("handle-searchcomponent-shownode-toolset-right") + .first(); + await searchApiOutput.hover(); await page.mouse.down(); const toolCallingAgentInput = await page