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

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)