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:
parent
9d5c75bce3
commit
a5ce562299
37 changed files with 25863 additions and 645 deletions
|
|
@ -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"
|
||||
|
|
|
|||
0
src/backend/tests/unit/components/search/__init__.py
Normal file
0
src/backend/tests/unit/components/search/__init__.py
Normal 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
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue