feat: New Search Bundle (#8146)

* delete data and transfer data to dataframe

* [autofix.ci] apply automated fixes

* create a new bundle for search

* fix type for dataframe

* add data_to_dataframe function

* [autofix.ci] apply automated fixes

* fix test because of files movement

* delete message and text

* json update

* fix search yahoo test

* fix run_model output type

* fix test errors

* fix test errors

* fix test error

* try fix frontend tests

* test fix

* [autofix.ci] apply automated fixes

* move serp search

* fix test

* fix test

* fix test to pass ruff style check

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Edwin Jose <edwin.jose@datastax.com>
Co-authored-by: Mike Fortman <michael.fortman@datastax.com>
This commit is contained in:
Yuqi Tang 2025-05-23 14:51:04 -07:00 committed by GitHub
commit a5ce562299
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
37 changed files with 25863 additions and 645 deletions

25089
docs/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -4,7 +4,7 @@ from collections.abc import Sequence
from langflow.custom import Component
from langflow.field_typing import Tool
from langflow.io import Output
from langflow.schema import Data
from langflow.schema import Data, DataFrame
class LCToolComponent(Component):
@ -26,7 +26,7 @@ class LCToolComponent(Component):
raise ValueError(msg)
@abstractmethod
def run_model(self) -> Data | list[Data]:
def run_model(self) -> Data | list[Data] | DataFrame:
"""Run model and return the output."""
@abstractmethod

View file

@ -0,0 +1,29 @@
from .arxiv import ArXivComponent
from .bing_search_api import BingSearchAPIComponent
from .duck_duck_go_search_run import DuckDuckGoSearchComponent
from .exa_search import ExaSearchToolkit
from .glean_search_api import GleanSearchAPISchema
from .google_search_api_core import GoogleSearchAPICore
from .google_serper_api_core import GoogleSerperAPICore
from .search import SearchComponent
from .serp import SerpComponent
from .wikidata import WikidataComponent
from .wikipedia import WikipediaComponent
from .wolfram_alpha_api import WolframAlphaAPIComponent
from .yahoo import YahooFinanceSchema
__all__ = [
"ArXivComponent",
"BingSearchAPIComponent",
"DuckDuckGoSearchComponent",
"ExaSearchToolkit",
"GleanSearchAPISchema",
"GoogleSearchAPICore",
"GoogleSerperAPICore",
"SearchComponent",
"SerpComponent",
"WikidataComponent",
"WikipediaComponent",
"WolframAlphaAPIComponent",
"YahooFinanceSchema",
]

View file

@ -5,6 +5,7 @@ from xml.etree.ElementTree import Element
from defusedxml.ElementTree import fromstring
from langflow.custom import Component
from langflow.helpers.data import data_to_dataframe
from langflow.io import DropdownInput, IntInput, MessageTextInput, Output
from langflow.schema import Data, DataFrame
@ -37,8 +38,7 @@ class ArXivComponent(Component):
]
outputs = [
Output(display_name="Data", name="data", method="search_papers"),
Output(display_name="DataFrame", name="dataframe", method="as_dataframe"),
Output(display_name="DataFrame", name="dataframe", method="search_papers_dataframe"),
]
def build_query_url(self) -> str:
@ -105,6 +105,9 @@ class ArXivComponent(Component):
cat = element.find("arxiv:primary_category", ns)
return cat.get("term") if cat is not None else None
def run_model(self) -> DataFrame:
return self.search_papers_dataframe()
def search_papers(self) -> list[Data]:
"""Search arXiv and return results."""
try:
@ -150,13 +153,11 @@ class ArXivComponent(Component):
else:
return results
def as_dataframe(self) -> DataFrame:
def search_papers_dataframe(self) -> DataFrame:
"""Convert the Arxiv search results to a DataFrame.
Returns:
DataFrame: A DataFrame containing the search results.
"""
data = self.search_papers()
if isinstance(data, list):
return DataFrame(data=[d.data for d in data])
return DataFrame(data=[data.data])
return data_to_dataframe(data)

View file

@ -5,8 +5,10 @@ from langchain_community.utilities import BingSearchAPIWrapper
from langflow.base.langchain_utilities.model import LCToolComponent
from langflow.field_typing import Tool
from langflow.helpers.data import data_to_dataframe
from langflow.inputs import IntInput, MessageTextInput, MultilineInput, SecretStrInput
from langflow.schema import Data
from langflow.io import Output
from langflow.schema import Data, DataFrame
class BingSearchAPIComponent(LCToolComponent):
@ -25,7 +27,15 @@ class BingSearchAPIComponent(LCToolComponent):
IntInput(name="k", display_name="Number of results", value=4, required=True),
]
def run_model(self) -> list[Data]:
outputs = [
Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"),
Output(display_name="Tool", name="tool", method="build_tool"),
]
def run_model(self) -> DataFrame:
return self.fetch_content_dataframe()
def fetch_content(self) -> list[Data]:
if self.bing_search_url:
wrapper = BingSearchAPIWrapper(
bing_search_url=self.bing_search_url, bing_subscription_key=self.bing_subscription_key
@ -37,6 +47,10 @@ class BingSearchAPIComponent(LCToolComponent):
self.status = data
return data
def fetch_content_dataframe(self) -> DataFrame:
data = self.fetch_content()
return data_to_dataframe(data)
def build_tool(self) -> Tool:
if self.bing_search_url:
wrapper = BingSearchAPIWrapper(

View file

@ -1,10 +1,10 @@
from langchain_community.tools import DuckDuckGoSearchRun
from langflow.custom import Component
from langflow.helpers.data import data_to_dataframe
from langflow.inputs import IntInput, MessageTextInput
from langflow.io import Output
from langflow.schema import Data
from langflow.schema.message import Message
from langflow.schema import Data, DataFrame
class DuckDuckGoSearchComponent(Component):
@ -42,16 +42,15 @@ class DuckDuckGoSearchComponent(Component):
]
outputs = [
Output(display_name="Data", name="data", method="fetch_content"),
Output(display_name="Text", name="text", method="fetch_content_text"),
Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"),
]
def _build_wrapper(self) -> DuckDuckGoSearchRun:
"""Build the DuckDuckGo search wrapper."""
return DuckDuckGoSearchRun()
def run_model(self) -> list[Data]:
return self.fetch_content()
def run_model(self) -> DataFrame:
return self.fetch_content_dataframe()
def fetch_content(self) -> list[Data]:
"""Execute the search and return results as Data objects."""
@ -83,9 +82,11 @@ class DuckDuckGoSearchComponent(Component):
self.status = data_results
return data_results
def fetch_content_text(self) -> Message:
"""Return search results as a single text message."""
def fetch_content_dataframe(self) -> DataFrame:
"""Convert the search results to a DataFrame.
Returns:
DataFrame: A DataFrame containing the search results.
"""
data = self.fetch_content()
result_string = "\n".join(item.text for item in data)
self.status = result_string
return Message(text=result_string)
return data_to_dataframe(data)

View file

@ -9,6 +9,7 @@ from pydantic.v1 import Field
from langflow.base.langchain_utilities.model import LCToolComponent
from langflow.field_typing import Tool
from langflow.helpers.data import data_to_dataframe
from langflow.inputs import IntInput, MultilineInput, NestedDictInput, SecretStrInput, StrInput
from langflow.io import Output
from langflow.schema import Data, DataFrame
@ -104,8 +105,7 @@ class GleanSearchAPIComponent(LCToolComponent):
icon: str = "Glean"
outputs = [
Output(display_name="Data", name="data", method="run_model"),
Output(display_name="DataFrame", name="dataframe", method="as_dataframe"),
Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"),
]
inputs = [
@ -133,7 +133,10 @@ class GleanSearchAPIComponent(LCToolComponent):
return tool
def run_model(self) -> list[Data]:
def run_model(self) -> DataFrame:
return self.fetch_content_dataframe()
def fetch_content(self) -> list[Data]:
tool = self.build_tool()
results = tool.run(
@ -160,13 +163,11 @@ class GleanSearchAPIComponent(LCToolComponent):
glean_access_token=glean_access_token,
)
def as_dataframe(self) -> DataFrame:
def fetch_content_dataframe(self) -> DataFrame:
"""Convert the Glean search results to a DataFrame.
Returns:
DataFrame: A DataFrame containing the search results.
"""
data = self.run_model()
if isinstance(data, list):
return DataFrame(data=[d.data for d in data])
return DataFrame(data=[data.data])
data = self.fetch_content()
return data_to_dataframe(data)

View file

@ -3,10 +3,10 @@ from typing import Any
from langchain_community.utilities.searchapi import SearchApiAPIWrapper
from langflow.custom import Component
from langflow.helpers.data import data_to_dataframe
from langflow.inputs import DictInput, DropdownInput, IntInput, MultilineInput, SecretStrInput
from langflow.io import Output
from langflow.schema import Data, DataFrame
from langflow.schema.message import Message
class SearchComponent(Component):
@ -29,16 +29,14 @@ class SearchComponent(Component):
]
outputs = [
Output(display_name="Data", name="data", method="fetch_content"),
Output(display_name="Text", name="text", method="fetch_content_text"),
Output(display_name="DataFrame", name="dataframe", method="as_dataframe"),
Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"),
]
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 run_model(self) -> DataFrame:
return self.fetch_content_dataframe()
def fetch_content(self) -> list[Data]:
wrapper = self._build_wrapper()
@ -71,19 +69,11 @@ class SearchComponent(Component):
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)
def as_dataframe(self) -> DataFrame:
def fetch_content_dataframe(self) -> DataFrame:
"""Convert the search results to a DataFrame.
Returns:
DataFrame: A DataFrame containing the search results.
"""
data = self.fetch_content()
return DataFrame(data)
return data_to_dataframe(data)

View file

@ -3,10 +3,9 @@ from httpx import HTTPError
from langchain_core.tools import ToolException
from langflow.custom import Component
from langflow.helpers.data import data_to_text
from langflow.helpers.data import data_to_dataframe
from langflow.io import MultilineInput, Output
from langflow.schema import Data
from langflow.schema.message import Message
from langflow.schema import Data, DataFrame
class WikidataComponent(Component):
@ -25,10 +24,12 @@ class WikidataComponent(Component):
]
outputs = [
Output(display_name="Data", name="data", method="fetch_content"),
Output(display_name="Message", name="text", method="fetch_content_text"),
Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"),
]
def run_model(self) -> DataFrame:
return self.fetch_content_dataframe()
def fetch_content(self) -> list[Data]:
try:
# Define request parameters for Wikidata API
@ -79,8 +80,6 @@ class WikidataComponent(Component):
else:
return data
def fetch_content_text(self) -> Message:
def fetch_content_dataframe(self) -> DataFrame:
data = self.fetch_content()
result_string = data_to_text("{text}", data)
self.status = result_string
return Message(text=result_string)
return data_to_dataframe(data)

View file

@ -1,10 +1,10 @@
from langchain_community.utilities.wikipedia import WikipediaAPIWrapper
from langflow.custom import Component
from langflow.helpers.data import data_to_dataframe
from langflow.inputs import BoolInput, IntInput, MessageTextInput, MultilineInput
from langflow.io import Output
from langflow.schema import Data, DataFrame
from langflow.schema.message import Message
class WikipediaComponent(Component):
@ -27,24 +27,11 @@ class WikipediaComponent(Component):
]
outputs = [
Output(display_name="Data", name="data", method="fetch_content"),
Output(display_name="DataFrame", name="dataframe", method="as_dataframe"),
Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"),
]
def fetch_content(self) -> list[Data]:
wrapper = self._build_wrapper()
docs = wrapper.load(self.input_value)
data = [Data.from_document(doc) for doc in docs]
self.status = data
return data
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)
def run_model(self) -> DataFrame:
return self.fetch_content_dataframe()
def _build_wrapper(self) -> WikipediaAPIWrapper:
return WikipediaAPIWrapper(
@ -54,11 +41,13 @@ class WikipediaComponent(Component):
doc_content_chars_max=self.doc_content_chars_max,
)
def as_dataframe(self) -> DataFrame:
"""Convert the Wikipedia results to a DataFrame.
def fetch_content(self) -> list[Data]:
wrapper = self._build_wrapper()
docs = wrapper.load(self.input_value)
data = [Data.from_document(doc) for doc in docs]
self.status = data
return data
Returns:
DataFrame: A DataFrame containing the Wikipedia results.
"""
def fetch_content_dataframe(self) -> DataFrame:
data = self.fetch_content()
return DataFrame(data)
return data_to_dataframe(data)

View file

@ -2,6 +2,7 @@ from langchain_community.utilities.wolfram_alpha import WolframAlphaAPIWrapper
from langflow.base.langchain_utilities.model import LCToolComponent
from langflow.field_typing import Tool
from langflow.helpers.data import data_to_dataframe
from langflow.inputs import MultilineInput, SecretStrInput
from langflow.io import Output
from langflow.schema import Data, DataFrame
@ -14,8 +15,7 @@ topics, delivering structured responses."""
name = "WolframAlphaAPI"
outputs = [
Output(display_name="Data", name="data", method="run_model"),
Output(display_name="DataFrame", name="dataframe", method="as_dataframe"),
Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"),
]
inputs = [
@ -27,12 +27,8 @@ topics, delivering structured responses."""
icon = "WolframAlphaAPI"
def run_model(self) -> list[Data]:
wrapper = self._build_wrapper()
result_str = wrapper.run(self.input_value)
data = [Data(text=result_str)]
self.status = data
return data
def run_model(self) -> DataFrame:
return self.fetch_content_dataframe()
def build_tool(self) -> Tool:
wrapper = self._build_wrapper()
@ -41,11 +37,18 @@ topics, delivering structured responses."""
def _build_wrapper(self) -> WolframAlphaAPIWrapper:
return WolframAlphaAPIWrapper(wolfram_alpha_appid=self.app_id)
def as_dataframe(self) -> DataFrame:
def fetch_content(self) -> list[Data]:
wrapper = self._build_wrapper()
result_str = wrapper.run(self.input_value)
data = [Data(text=result_str)]
self.status = data
return data
def fetch_content_dataframe(self) -> DataFrame:
"""Convert the Wolfram Alpha results to a DataFrame.
Returns:
DataFrame: A DataFrame containing the query results.
"""
data = self.run_model()
return DataFrame(data)
data = self.fetch_content()
return data_to_dataframe(data)

View file

@ -8,10 +8,10 @@ from loguru import logger
from pydantic import BaseModel, Field
from langflow.custom import Component
from langflow.helpers.data import data_to_dataframe
from langflow.inputs import DropdownInput, IntInput, MessageTextInput
from langflow.io import Output
from langflow.schema import Data, DataFrame
from langflow.schema.message import Message
class YahooFinanceMethod(Enum):
@ -77,21 +77,11 @@ to access financial data and market information from Yahoo Finance."""
]
outputs = [
Output(display_name="Data", name="data", method="fetch_content"),
Output(display_name="Text", name="text", method="fetch_content_text"),
Output(display_name="DataFrame", name="dataframe", method="as_dataframe"),
Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"),
]
def run_model(self) -> list[Data]:
return self.fetch_content()
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)
def run_model(self) -> DataFrame:
return self.fetch_content_dataframe()
def _fetch_yfinance_data(self, ticker: yf.Ticker, method: YahooFinanceMethod, num_news: int | None) -> str:
try:
@ -142,11 +132,6 @@ to access financial data and market information from Yahoo Finance."""
return data_list
def as_dataframe(self) -> DataFrame:
"""Convert the Yahoo search results to a DataFrame.
Returns:
DataFrame: A DataFrame containing the search results.
"""
def fetch_content_dataframe(self) -> DataFrame:
data = self.fetch_content()
return DataFrame(data)
return data_to_dataframe(data)

View file

@ -2,69 +2,48 @@ import warnings
from langchain_core._api.deprecation import LangChainDeprecationWarning
from .arxiv import ArXivComponent
from .bing_search_api import BingSearchAPIComponent
from .calculator import CalculatorToolComponent
from .calculator_core import CalculatorComponent
from .duck_duck_go_search_run import DuckDuckGoSearchComponent
from .exa_search import ExaSearchToolkit
from .glean_search_api import GleanSearchAPIComponent
from .google_search_api import GoogleSearchAPIComponent
from .google_search_api_core import GoogleSearchAPICore
from .google_serper_api import GoogleSerperAPIComponent
from .google_serper_api_core import GoogleSerperAPICore
from .mcp_component import MCPToolsComponent
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
from .serp_api import SerpAPIComponent
from .tavily_extract import TavilyExtractComponent
from .tavily_search import TavilySearchComponent
from .tavily_search_tool import TavilySearchToolComponent
from .wikidata import WikidataComponent
from .wikidata_api import WikidataAPIComponent
from .wikipedia import WikipediaComponent
from .wikipedia_api import WikipediaAPIComponent
from .wolfram_alpha_api import WolframAlphaAPIComponent
from .yahoo import YfinanceComponent
from .yahoo_finance import YfinanceToolComponent
with warnings.catch_warnings():
warnings.simplefilter("ignore", LangChainDeprecationWarning)
__all__ = [
"ArXivComponent",
"BingSearchAPIComponent",
"AstraDBCQLToolComponent",
"AstraDBToolComponent",
"CalculatorComponent",
"CalculatorToolComponent",
"DuckDuckGoSearchComponent",
"ExaSearchToolkit",
"GleanSearchAPIComponent",
"GoogleSearchAPIComponent",
"GoogleSearchAPICore",
"GoogleSerperAPIComponent",
"GoogleSerperAPICore",
"MCPToolsComponent",
"PythonCodeStructuredTool",
"PythonREPLComponent",
"PythonREPLToolComponent",
"SearXNGToolComponent",
"SearchAPIComponent",
"SearchComponent",
"SerpAPIComponent",
"SerpComponent",
"TavilyExtractComponent",
"TavilySearchComponent",
"TavilySearchToolComponent",
"WikidataAPIComponent",
"WikidataComponent",
"WikipediaAPIComponent",
"WikipediaComponent",
"WolframAlphaAPIComponent",
"YfinanceComponent",
"YfinanceToolComponent",
]

View file

@ -2,7 +2,7 @@ from collections import defaultdict
from langchain_core.documents import Document
from langflow.schema import Data
from langflow.schema import Data, DataFrame
from langflow.schema.message import Message
@ -139,3 +139,17 @@ def messages_to_text(template: str, messages: Message | list[Message]) -> str:
formated_messages = [template.format(data=message.model_dump(), **message.model_dump()) for message in messages_]
return "\n".join(formated_messages)
def data_to_dataframe(data: Data | list[Data]) -> DataFrame:
"""Converts a Data object or a list of Data objects to a DataFrame.
Args:
data (Data | list[Data]): The Data object or list of Data objects to convert.
Returns:
DataFrame: The converted DataFrame.
"""
if isinstance(data, Data):
return DataFrame([data.data])
return DataFrame(data=[d.data for d in data])

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -1462,7 +1462,7 @@
"show": true,
"title_case": false,
"type": "code",
"value": "from typing import Any\n\nfrom langchain_community.utilities.searchapi import SearchApiAPIWrapper\n\nfrom langflow.custom import Component\nfrom langflow.inputs import DictInput, DropdownInput, IntInput, MultilineInput, SecretStrInput\nfrom langflow.io import Output\nfrom langflow.schema import Data, DataFrame\nfrom langflow.schema.message import Message\n\n\nclass SearchComponent(Component):\n display_name: str = \"Search API\"\n description: str = \"Call the searchapi.io API with result limiting\"\n documentation: str = \"https://www.searchapi.io/docs/google\"\n icon = \"SearchAPI\"\n\n inputs = [\n DropdownInput(name=\"engine\", display_name=\"Engine\", value=\"google\", options=[\"google\", \"bing\", \"duckduckgo\"]),\n SecretStrInput(name=\"api_key\", display_name=\"SearchAPI API Key\", required=True),\n MultilineInput(\n name=\"input_value\",\n display_name=\"Input\",\n tool_mode=True,\n ),\n DictInput(name=\"search_params\", display_name=\"Search parameters\", advanced=True, is_list=True),\n IntInput(name=\"max_results\", display_name=\"Max Results\", value=5, advanced=True),\n IntInput(name=\"max_snippet_length\", display_name=\"Max Snippet Length\", value=100, advanced=True),\n ]\n\n outputs = [\n Output(display_name=\"Data\", name=\"data\", method=\"fetch_content\"),\n Output(display_name=\"Text\", name=\"text\", method=\"fetch_content_text\"),\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"as_dataframe\"),\n ]\n\n def _build_wrapper(self):\n return SearchApiAPIWrapper(engine=self.engine, searchapi_api_key=self.api_key)\n\n def run_model(self) -> list[Data]:\n return self.fetch_content()\n\n def fetch_content(self) -> list[Data]:\n wrapper = self._build_wrapper()\n\n def search_func(\n query: str, params: dict[str, Any] | None = None, max_results: int = 5, max_snippet_length: int = 100\n ) -> list[Data]:\n params = params or {}\n full_results = wrapper.results(query=query, **params)\n organic_results = full_results.get(\"organic_results\", [])[:max_results]\n\n return [\n Data(\n text=result.get(\"snippet\", \"\"),\n data={\n \"title\": result.get(\"title\", \"\")[:max_snippet_length],\n \"link\": result.get(\"link\", \"\"),\n \"snippet\": result.get(\"snippet\", \"\")[:max_snippet_length],\n },\n )\n for result in organic_results\n ]\n\n results = search_func(\n self.input_value,\n self.search_params or {},\n self.max_results,\n self.max_snippet_length,\n )\n self.status = results\n return results\n\n def fetch_content_text(self) -> Message:\n data = self.fetch_content()\n result_string = \"\"\n for item in data:\n result_string += item.text + \"\\n\"\n self.status = result_string\n return Message(text=result_string)\n\n def as_dataframe(self) -> DataFrame:\n \"\"\"Convert the search results to a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the search results.\n \"\"\"\n data = self.fetch_content()\n return DataFrame(data)\n"
"value": "from typing import Any\n\nfrom langchain_community.utilities.searchapi import SearchApiAPIWrapper\n\nfrom langflow.custom import Component\nfrom langflow.helpers.data import data_to_dataframe\nfrom langflow.inputs import DictInput, DropdownInput, IntInput, MultilineInput, SecretStrInput\nfrom langflow.io import Output\nfrom langflow.schema import Data, DataFrame\n\n\nclass SearchComponent(Component):\n display_name: str = \"Search API\"\n description: str = \"Call the searchapi.io API with result limiting\"\n documentation: str = \"https://www.searchapi.io/docs/google\"\n icon = \"SearchAPI\"\n\n inputs = [\n DropdownInput(name=\"engine\", display_name=\"Engine\", value=\"google\", options=[\"google\", \"bing\", \"duckduckgo\"]),\n SecretStrInput(name=\"api_key\", display_name=\"SearchAPI API Key\", required=True),\n MultilineInput(\n name=\"input_value\",\n display_name=\"Input\",\n tool_mode=True,\n ),\n DictInput(name=\"search_params\", display_name=\"Search parameters\", advanced=True, is_list=True),\n IntInput(name=\"max_results\", display_name=\"Max Results\", value=5, advanced=True),\n IntInput(name=\"max_snippet_length\", display_name=\"Max Snippet Length\", value=100, advanced=True),\n ]\n\n outputs = [\n Output(display_name=\"DataFrame\", name=\"dataframe\", method=\"fetch_content_dataframe\"),\n ]\n\n def _build_wrapper(self):\n return SearchApiAPIWrapper(engine=self.engine, searchapi_api_key=self.api_key)\n\n def run_model(self) -> DataFrame:\n return self.fetch_content_dataframe()\n\n def fetch_content(self) -> list[Data]:\n wrapper = self._build_wrapper()\n\n def search_func(\n query: str, params: dict[str, Any] | None = None, max_results: int = 5, max_snippet_length: int = 100\n ) -> list[Data]:\n params = params or {}\n full_results = wrapper.results(query=query, **params)\n organic_results = full_results.get(\"organic_results\", [])[:max_results]\n\n return [\n Data(\n text=result.get(\"snippet\", \"\"),\n data={\n \"title\": result.get(\"title\", \"\")[:max_snippet_length],\n \"link\": result.get(\"link\", \"\"),\n \"snippet\": result.get(\"snippet\", \"\")[:max_snippet_length],\n },\n )\n for result in organic_results\n ]\n\n results = search_func(\n self.input_value,\n self.search_params or {},\n self.max_results,\n self.max_snippet_length,\n )\n self.status = results\n return results\n\n def fetch_content_dataframe(self) -> DataFrame:\n \"\"\"Convert the search results to a DataFrame.\n\n Returns:\n DataFrame: A DataFrame containing the search results.\n \"\"\"\n data = self.fetch_content()\n return data_to_dataframe(data)\n"
},
"engine": {
"_input_type": "DropdownInput",

View file

@ -50,21 +50,21 @@ class TestNewsSearchComponent(ComponentTestBaseWithoutClient):
component = NewsSearchComponent(query="OpenAI")
result = component.search_news()
assert isinstance(result, DataFrame)
df = result
assert len(df) == 2
assert list(df.columns) == ["title", "link", "published", "summary"]
assert df.iloc[0]["title"] == "Test News 1"
assert df.iloc[1]["title"] == "Test News 2"
news_results_df = result
assert len(news_results_df) == 2
assert list(news_results_df.columns) == ["title", "link", "published", "summary"]
assert news_results_df.iloc[0]["title"] == "Test News 1"
assert news_results_df.iloc[1]["title"] == "Test News 2"
def test_news_search_error(self):
with patch("requests.get", side_effect=requests.RequestException("Network error")):
component = NewsSearchComponent(query="OpenAI")
result = component.search_news()
assert isinstance(result, DataFrame)
df = result
assert len(df) == 1
assert df.iloc[0]["title"] == "Error"
assert "Network error" in df.iloc[0]["summary"]
news_results_df = result
assert len(news_results_df) == 1
assert news_results_df.iloc[0]["title"] == "Error"
assert "Network error" in news_results_df.iloc[0]["summary"]
def test_empty_news_results(self):
# Mock empty RSS feed
@ -83,6 +83,6 @@ class TestNewsSearchComponent(ComponentTestBaseWithoutClient):
component = NewsSearchComponent(query="OpenAI")
result = component.search_news()
assert isinstance(result, DataFrame)
df = result
assert len(df) == 1
assert df.iloc[0]["title"] == "No articles found"
news_results_df = result
assert len(news_results_df) == 1
assert news_results_df.iloc[0]["title"] == "No articles found"

View file

@ -8,7 +8,7 @@ from tests.base import ComponentTestBaseWithClient
class TestArXivComponent(ComponentTestBaseWithClient):
def test_component_versions(self, default_kwargs, file_names_mapping):
"""Test component compatibility across versions."""
from langflow.components.tools.arxiv import ArXivComponent
from langflow.components.search.arxiv import ArXivComponent
# Test current version
component = ArXivComponent(**default_kwargs)
@ -31,7 +31,7 @@ class TestArXivComponent(ComponentTestBaseWithClient):
@pytest.fixture
def component_class(self):
from langflow.components.tools.arxiv import ArXivComponent
from langflow.components.search.arxiv import ArXivComponent
return ArXivComponent

View file

@ -2,7 +2,7 @@ from unittest.mock import patch
import pandas as pd
import pytest
from langflow.components.tools import GoogleSearchAPICore
from langflow.components.search import GoogleSearchAPICore
from langflow.schema import DataFrame
from tests.base import ComponentTestBaseWithoutClient

View file

@ -1,7 +1,7 @@
from unittest.mock import MagicMock, patch
import pytest
from langflow.components.tools import GoogleSerperAPICore
from langflow.components.search import GoogleSerperAPICore
from langflow.schema import DataFrame

View file

@ -3,11 +3,9 @@ from unittest.mock import MagicMock, patch
import httpx
import pytest
from langchain_core.tools import ToolException
from langflow.components.tools import WikidataComponent
from langflow.components.search import WikidataComponent
from langflow.custom import Component
from langflow.custom.utils import build_custom_component_template
from langflow.schema import Data
from langflow.schema.message import Message
# Import the base test class
from tests.base import ComponentTestBaseWithoutClient
@ -102,18 +100,3 @@ class TestWikidataComponent(ComponentTestBaseWithoutClient):
with pytest.raises(ToolException):
component.fetch_content()
def test_fetch_content_text(self, component_class):
component = component_class()
component.fetch_content = MagicMock(
return_value=[
Data(text="First result", data={"label": "Label 1"}),
Data(text="Second result", data={"label": "Label 2"}),
]
)
result = component.fetch_content_text()
assert isinstance(result, Message)
assert "First result" in result.text
assert "Second result" in result.text

View file

@ -1,11 +1,9 @@
from unittest.mock import MagicMock
import pytest
from langflow.components.tools import WikipediaComponent
from langflow.components.search import WikipediaComponent
from langflow.custom import Component
from langflow.custom.utils import build_custom_component_template
from langflow.schema import Data
from langflow.schema.message import Message
# Import the base test class
from tests.base import ComponentTestBaseWithoutClient
@ -83,15 +81,6 @@ class TestWikipediaComponent(ComponentTestBaseWithoutClient):
assert len(result) == 1
assert result[0].text == "Test content"
def test_fetch_content_text(self, component_class):
component = component_class()
component.fetch_content = MagicMock(return_value=[Data(text="First result"), Data(text="Second result")])
result = component.fetch_content_text()
assert isinstance(result, Message)
assert result.text == "First result\nSecond result\n"
def test_wikipedia_error_handling(self, component_class):
component = component_class()
# Mock _build_wrapper to raise exception

View file

@ -2,8 +2,7 @@ from unittest.mock import MagicMock, patch
import pytest
from langchain_core.tools import ToolException
from langflow.components.tools import YfinanceComponent
from langflow.components.tools.yahoo import YahooFinanceMethod
from langflow.components.search.yahoo import YahooFinanceMethod, YfinanceComponent
from langflow.custom.utils import build_custom_component_template
from langflow.schema import Data
@ -38,7 +37,7 @@ class TestYfinanceComponent:
for input_name in expected_inputs:
assert input_name in input_names
@patch("langflow.components.tools.yahoo.yf.Ticker")
@patch("langflow.components.search.yahoo.yf.Ticker")
def test_fetch_info(self, mock_ticker, component_class, default_kwargs):
component = component_class(**default_kwargs)
@ -53,7 +52,7 @@ class TestYfinanceComponent:
assert len(result) == 1
assert "Apple Inc." in result[0].text
@patch("langflow.components.tools.yahoo.yf.Ticker")
@patch("langflow.components.search.yahoo.yf.Ticker")
def test_fetch_news(self, mock_ticker, component_class):
component = component_class(symbol="AAPL", method=YahooFinanceMethod.GET_NEWS, num_news=2)

View file

@ -2,7 +2,7 @@ from unittest.mock import MagicMock, patch
import pytest
from langchain_core.tools import ToolException
from langflow.components.tools import SerpComponent
from langflow.components.search import SerpComponent
from langflow.custom import Component
from langflow.custom.utils import build_custom_component_template
from langflow.schema import Data
@ -34,7 +34,7 @@ def test_serpapi_template():
assert input_name in input_names
@patch("langflow.components.tools.serp.SerpAPIWrapper")
@patch("langflow.components.search.serp.SerpAPIWrapper")
def test_fetch_content(mock_serpapi_wrapper):
component = SerpComponent()
component.serpapi_api_key = "test-key"
@ -81,7 +81,7 @@ def test_error_handling():
component.serpapi_api_key = "test-key"
component.input_value = "test query"
with patch("langflow.components.tools.serp.SerpAPIWrapper") as mock_serpapi:
with patch("langflow.components.search.serp.SerpAPIWrapper") as mock_serpapi:
mock_instance = MagicMock()
mock_serpapi.return_value = mock_instance
mock_instance.results.side_effect = Exception("API Error")

View file

@ -165,70 +165,84 @@ async def test_build_flow_polling(client, json_memory_chatbot_no_llm, logged_in_
self.max_total_events = 50 # Limit to prevent infinite loops
self.max_empty_polls = 10 # Maximum number of empty polls before giving up
self.poll_timeout = 3.0 # Timeout for each polling request
self._closed = False
async def aiter_lines(self):
if self._closed:
return
try:
empty_polls = 0
total_events = 0
end_event_found = False
while (
empty_polls < self.max_empty_polls and total_events < self.max_total_events and not end_event_found
empty_polls < self.max_empty_polls
and total_events < self.max_total_events
and not end_event_found
and not self._closed
):
# Add Accept header for NDJSON
headers = {**self.headers, "Accept": "application/x-ndjson"}
# Set a timeout for the request
response = await asyncio.wait_for(
self.client.get(
f"api/v1/build/{self.job_id}/events?event_delivery=polling",
headers=headers,
),
timeout=self.poll_timeout,
)
try:
# Set a timeout for the request
response = await asyncio.wait_for(
self.client.get(
f"api/v1/build/{self.job_id}/events?event_delivery=polling",
headers=headers,
),
timeout=self.poll_timeout,
)
assert response.status_code == codes.OK
if response.status_code != codes.OK:
break
# Get the NDJSON response as text
text = response.text
# Get the NDJSON response as text
text = response.text
# Skip if response is empty
if not text.strip():
empty_polls += 1
await asyncio.sleep(0.1)
continue
# Reset empty polls counter since we got data
empty_polls = 0
# Process each line as an individual JSON object
line_count = 0
for line in text.splitlines():
if not line.strip():
# Skip if response is empty
if not text.strip():
empty_polls += 1
await asyncio.sleep(0.1)
continue
line_count += 1
total_events += 1
# Reset empty polls counter since we got data
empty_polls = 0
# Check for end event with multiple possible formats
if '"event":"end"' in line or '"event": "end"' in line:
end_event_found = True
# Process each line as an individual JSON object
line_count = 0
for line in text.splitlines():
if not line.strip():
continue
# Validate it's proper JSON before yielding
try:
json.loads(line) # Test parse to ensure it's valid JSON
yield line
except json.JSONDecodeError as e:
logger.debug(f"WARNING: Skipping invalid JSON: {line}")
logger.debug(f"Error: {e}")
# Don't yield invalid JSON, but continue processing other lines
line_count += 1
total_events += 1
# If we had no events in this batch, count as empty poll
if line_count == 0:
# Check for end event with multiple possible formats
if '"event":"end"' in line or '"event": "end"' in line:
end_event_found = True
# Validate it's proper JSON before yielding
try:
json.loads(line) # Test parse to ensure it's valid JSON
yield line
except json.JSONDecodeError as e:
logger.debug(f"WARNING: Skipping invalid JSON: {line}")
logger.debug(f"Error: {e}")
# Don't yield invalid JSON, but continue processing other lines
# If we had no events in this batch, count as empty poll
if line_count == 0:
empty_polls += 1
# Add a small delay to prevent tight polling
await asyncio.sleep(0.1)
except asyncio.TimeoutError:
logger.debug(f"WARNING: Polling request timed out after {self.poll_timeout}s")
empty_polls += 1
# Add a small delay to prevent tight polling
await asyncio.sleep(0.1)
continue
# If we hit the limit without finding the end event, log a warning
if total_events >= self.max_total_events:
@ -241,10 +255,14 @@ async def test_build_flow_polling(client, json_memory_chatbot_no_llm, logged_in_
f"WARNING: Reached maximum empty polls ({self.max_empty_polls}) without finding end event"
)
except asyncio.TimeoutError as e:
logger.debug(f"ERROR: Polling request timed out after {self.poll_timeout}s")
msg = "Build event polling timed out."
raise TimeoutError(msg) from e
except Exception as e:
logger.debug(f"ERROR: Unexpected error during polling: {e!s}")
raise
finally:
self._closed = True
def close(self):
self._closed = True
polling_response = PollingResponse(client, job_id, logged_in_headers)

View file

@ -271,6 +271,7 @@ export const SIDEBAR_BUNDLES = [
name: "homeassistant",
icon: "HomeAssistant",
},
{ display_name: "Search", name: "search", icon: "Search" },
];
export const categoryIcons: Record<string, string> = {

View file

@ -125,7 +125,6 @@ test(
await expect(page.getByTestId("dataAPI Request")).toBeVisible();
await expect(page.getByTestId("helpersMessage History")).toBeVisible();
await expect(page.getByTestId("vectorstoresAstra DB")).toBeVisible();
await expect(page.getByTestId("toolsSearch API")).toBeVisible();
await expect(page.getByTestId("logicSub Flow [Deprecated]")).toBeVisible();
await page.getByTestId("sidebar-options-trigger").click();
@ -137,19 +136,16 @@ test(
await expect(page.getByTestId("logicSub Flow [Deprecated]")).toBeVisible();
await expect(page.getByTestId("processingSplit Text")).toBeVisible();
await expect(page.getByTestId("toolsSearch API")).toBeVisible();
await page.getByTestId("icon-X").first().click();
await expect(page.getByTestId("dataAPI Request")).not.toBeVisible();
await expect(page.getByTestId("helpersMessage History")).not.toBeVisible();
await expect(page.getByTestId("vectorstoresAstra DB")).not.toBeVisible();
await expect(page.getByTestId("toolsSearch API")).not.toBeVisible();
await expect(
page.getByTestId("logicSub Flow [Deprecated]"),
).not.toBeVisible();
await expect(page.getByTestId("processingSplit Text")).not.toBeVisible();
await expect(page.getByTestId("toolsSearch API")).not.toBeVisible();
},
);

View file

@ -11,12 +11,12 @@ test(
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("duck");
await page.waitForSelector('[data-testid="toolsDuckDuckGo Search"]', {
await page.waitForSelector('[data-testid="searchDuckDuckGo Search"]', {
timeout: 3000,
});
await page
.getByTestId("toolsDuckDuckGo Search")
.getByTestId("searchDuckDuckGo Search")
.hover()
.then(async () => {
await page
@ -44,7 +44,7 @@ test(
) ?? false;
await page
.getByTestId("output-inspection-data-duckduckgosearchcomponent")
.getByTestId("output-inspection-dataframe-duckduckgosearchcomponent")
.first()
.click();

View file

@ -49,14 +49,14 @@ test(
await page.getByTestId("sidebar-search-input").click();
await page.getByTestId("sidebar-search-input").fill("search api");
await page.waitForSelector('[data-testid="toolsSearch API"]', {
await page.waitForSelector('[data-testid="searchSearch API"]', {
timeout: 1000,
});
await zoomOut(page, 3);
await page
.getByTestId("toolsSearch API")
.getByTestId("searchSearch API")
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
targetPosition: { x: 100, y: 100 },
});