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
25089
docs/package-lock.json
generated
Normal file
25089
docs/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load diff
921
docs/yarn.lock
921
docs/yarn.lock
File diff suppressed because it is too large
Load diff
|
|
@ -4,7 +4,7 @@ from collections.abc import Sequence
|
||||||
from langflow.custom import Component
|
from langflow.custom import Component
|
||||||
from langflow.field_typing import Tool
|
from langflow.field_typing import Tool
|
||||||
from langflow.io import Output
|
from langflow.io import Output
|
||||||
from langflow.schema import Data
|
from langflow.schema import Data, DataFrame
|
||||||
|
|
||||||
|
|
||||||
class LCToolComponent(Component):
|
class LCToolComponent(Component):
|
||||||
|
|
@ -26,7 +26,7 @@ class LCToolComponent(Component):
|
||||||
raise ValueError(msg)
|
raise ValueError(msg)
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def run_model(self) -> Data | list[Data]:
|
def run_model(self) -> Data | list[Data] | DataFrame:
|
||||||
"""Run model and return the output."""
|
"""Run model and return the output."""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
|
|
|
||||||
29
src/backend/base/langflow/components/search/__init__.py
Normal file
29
src/backend/base/langflow/components/search/__init__.py
Normal 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",
|
||||||
|
]
|
||||||
|
|
@ -5,6 +5,7 @@ from xml.etree.ElementTree import Element
|
||||||
from defusedxml.ElementTree import fromstring
|
from defusedxml.ElementTree import fromstring
|
||||||
|
|
||||||
from langflow.custom import Component
|
from langflow.custom import Component
|
||||||
|
from langflow.helpers.data import data_to_dataframe
|
||||||
from langflow.io import DropdownInput, IntInput, MessageTextInput, Output
|
from langflow.io import DropdownInput, IntInput, MessageTextInput, Output
|
||||||
from langflow.schema import Data, DataFrame
|
from langflow.schema import Data, DataFrame
|
||||||
|
|
||||||
|
|
@ -37,8 +38,7 @@ class ArXivComponent(Component):
|
||||||
]
|
]
|
||||||
|
|
||||||
outputs = [
|
outputs = [
|
||||||
Output(display_name="Data", name="data", method="search_papers"),
|
Output(display_name="DataFrame", name="dataframe", method="search_papers_dataframe"),
|
||||||
Output(display_name="DataFrame", name="dataframe", method="as_dataframe"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def build_query_url(self) -> str:
|
def build_query_url(self) -> str:
|
||||||
|
|
@ -105,6 +105,9 @@ class ArXivComponent(Component):
|
||||||
cat = element.find("arxiv:primary_category", ns)
|
cat = element.find("arxiv:primary_category", ns)
|
||||||
return cat.get("term") if cat is not None else None
|
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]:
|
def search_papers(self) -> list[Data]:
|
||||||
"""Search arXiv and return results."""
|
"""Search arXiv and return results."""
|
||||||
try:
|
try:
|
||||||
|
|
@ -150,13 +153,11 @@ class ArXivComponent(Component):
|
||||||
else:
|
else:
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def as_dataframe(self) -> DataFrame:
|
def search_papers_dataframe(self) -> DataFrame:
|
||||||
"""Convert the Arxiv search results to a DataFrame.
|
"""Convert the Arxiv search results to a DataFrame.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
DataFrame: A DataFrame containing the search results.
|
DataFrame: A DataFrame containing the search results.
|
||||||
"""
|
"""
|
||||||
data = self.search_papers()
|
data = self.search_papers()
|
||||||
if isinstance(data, list):
|
return data_to_dataframe(data)
|
||||||
return DataFrame(data=[d.data for d in data])
|
|
||||||
return DataFrame(data=[data.data])
|
|
||||||
|
|
@ -5,8 +5,10 @@ from langchain_community.utilities import BingSearchAPIWrapper
|
||||||
|
|
||||||
from langflow.base.langchain_utilities.model import LCToolComponent
|
from langflow.base.langchain_utilities.model import LCToolComponent
|
||||||
from langflow.field_typing import Tool
|
from langflow.field_typing import Tool
|
||||||
|
from langflow.helpers.data import data_to_dataframe
|
||||||
from langflow.inputs import IntInput, MessageTextInput, MultilineInput, SecretStrInput
|
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):
|
class BingSearchAPIComponent(LCToolComponent):
|
||||||
|
|
@ -25,7 +27,15 @@ class BingSearchAPIComponent(LCToolComponent):
|
||||||
IntInput(name="k", display_name="Number of results", value=4, required=True),
|
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:
|
if self.bing_search_url:
|
||||||
wrapper = BingSearchAPIWrapper(
|
wrapper = BingSearchAPIWrapper(
|
||||||
bing_search_url=self.bing_search_url, bing_subscription_key=self.bing_subscription_key
|
bing_search_url=self.bing_search_url, bing_subscription_key=self.bing_subscription_key
|
||||||
|
|
@ -37,6 +47,10 @@ class BingSearchAPIComponent(LCToolComponent):
|
||||||
self.status = data
|
self.status = data
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
def fetch_content_dataframe(self) -> DataFrame:
|
||||||
|
data = self.fetch_content()
|
||||||
|
return data_to_dataframe(data)
|
||||||
|
|
||||||
def build_tool(self) -> Tool:
|
def build_tool(self) -> Tool:
|
||||||
if self.bing_search_url:
|
if self.bing_search_url:
|
||||||
wrapper = BingSearchAPIWrapper(
|
wrapper = BingSearchAPIWrapper(
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
from langchain_community.tools import DuckDuckGoSearchRun
|
from langchain_community.tools import DuckDuckGoSearchRun
|
||||||
|
|
||||||
from langflow.custom import Component
|
from langflow.custom import Component
|
||||||
|
from langflow.helpers.data import data_to_dataframe
|
||||||
from langflow.inputs import IntInput, MessageTextInput
|
from langflow.inputs import IntInput, MessageTextInput
|
||||||
from langflow.io import Output
|
from langflow.io import Output
|
||||||
from langflow.schema import Data
|
from langflow.schema import Data, DataFrame
|
||||||
from langflow.schema.message import Message
|
|
||||||
|
|
||||||
|
|
||||||
class DuckDuckGoSearchComponent(Component):
|
class DuckDuckGoSearchComponent(Component):
|
||||||
|
|
@ -42,16 +42,15 @@ class DuckDuckGoSearchComponent(Component):
|
||||||
]
|
]
|
||||||
|
|
||||||
outputs = [
|
outputs = [
|
||||||
Output(display_name="Data", name="data", method="fetch_content"),
|
Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"),
|
||||||
Output(display_name="Text", name="text", method="fetch_content_text"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def _build_wrapper(self) -> DuckDuckGoSearchRun:
|
def _build_wrapper(self) -> DuckDuckGoSearchRun:
|
||||||
"""Build the DuckDuckGo search wrapper."""
|
"""Build the DuckDuckGo search wrapper."""
|
||||||
return DuckDuckGoSearchRun()
|
return DuckDuckGoSearchRun()
|
||||||
|
|
||||||
def run_model(self) -> list[Data]:
|
def run_model(self) -> DataFrame:
|
||||||
return self.fetch_content()
|
return self.fetch_content_dataframe()
|
||||||
|
|
||||||
def fetch_content(self) -> list[Data]:
|
def fetch_content(self) -> list[Data]:
|
||||||
"""Execute the search and return results as Data objects."""
|
"""Execute the search and return results as Data objects."""
|
||||||
|
|
@ -83,9 +82,11 @@ class DuckDuckGoSearchComponent(Component):
|
||||||
self.status = data_results
|
self.status = data_results
|
||||||
return data_results
|
return data_results
|
||||||
|
|
||||||
def fetch_content_text(self) -> Message:
|
def fetch_content_dataframe(self) -> DataFrame:
|
||||||
"""Return search results as a single text message."""
|
"""Convert the search results to a DataFrame.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
DataFrame: A DataFrame containing the search results.
|
||||||
|
"""
|
||||||
data = self.fetch_content()
|
data = self.fetch_content()
|
||||||
result_string = "\n".join(item.text for item in data)
|
return data_to_dataframe(data)
|
||||||
self.status = result_string
|
|
||||||
return Message(text=result_string)
|
|
||||||
|
|
@ -9,6 +9,7 @@ from pydantic.v1 import Field
|
||||||
|
|
||||||
from langflow.base.langchain_utilities.model import LCToolComponent
|
from langflow.base.langchain_utilities.model import LCToolComponent
|
||||||
from langflow.field_typing import Tool
|
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.inputs import IntInput, MultilineInput, NestedDictInput, SecretStrInput, StrInput
|
||||||
from langflow.io import Output
|
from langflow.io import Output
|
||||||
from langflow.schema import Data, DataFrame
|
from langflow.schema import Data, DataFrame
|
||||||
|
|
@ -104,8 +105,7 @@ class GleanSearchAPIComponent(LCToolComponent):
|
||||||
icon: str = "Glean"
|
icon: str = "Glean"
|
||||||
|
|
||||||
outputs = [
|
outputs = [
|
||||||
Output(display_name="Data", name="data", method="run_model"),
|
Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"),
|
||||||
Output(display_name="DataFrame", name="dataframe", method="as_dataframe"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
inputs = [
|
inputs = [
|
||||||
|
|
@ -133,7 +133,10 @@ class GleanSearchAPIComponent(LCToolComponent):
|
||||||
|
|
||||||
return tool
|
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()
|
tool = self.build_tool()
|
||||||
|
|
||||||
results = tool.run(
|
results = tool.run(
|
||||||
|
|
@ -160,13 +163,11 @@ class GleanSearchAPIComponent(LCToolComponent):
|
||||||
glean_access_token=glean_access_token,
|
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.
|
"""Convert the Glean search results to a DataFrame.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
DataFrame: A DataFrame containing the search results.
|
DataFrame: A DataFrame containing the search results.
|
||||||
"""
|
"""
|
||||||
data = self.run_model()
|
data = self.fetch_content()
|
||||||
if isinstance(data, list):
|
return data_to_dataframe(data)
|
||||||
return DataFrame(data=[d.data for d in data])
|
|
||||||
return DataFrame(data=[data.data])
|
|
||||||
|
|
@ -3,10 +3,10 @@ from typing import Any
|
||||||
from langchain_community.utilities.searchapi import SearchApiAPIWrapper
|
from langchain_community.utilities.searchapi import SearchApiAPIWrapper
|
||||||
|
|
||||||
from langflow.custom import Component
|
from langflow.custom import Component
|
||||||
|
from langflow.helpers.data import data_to_dataframe
|
||||||
from langflow.inputs import DictInput, DropdownInput, IntInput, MultilineInput, SecretStrInput
|
from langflow.inputs import DictInput, DropdownInput, IntInput, MultilineInput, SecretStrInput
|
||||||
from langflow.io import Output
|
from langflow.io import Output
|
||||||
from langflow.schema import Data, DataFrame
|
from langflow.schema import Data, DataFrame
|
||||||
from langflow.schema.message import Message
|
|
||||||
|
|
||||||
|
|
||||||
class SearchComponent(Component):
|
class SearchComponent(Component):
|
||||||
|
|
@ -29,16 +29,14 @@ class SearchComponent(Component):
|
||||||
]
|
]
|
||||||
|
|
||||||
outputs = [
|
outputs = [
|
||||||
Output(display_name="Data", name="data", method="fetch_content"),
|
Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"),
|
||||||
Output(display_name="Text", name="text", method="fetch_content_text"),
|
|
||||||
Output(display_name="DataFrame", name="dataframe", method="as_dataframe"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def _build_wrapper(self):
|
def _build_wrapper(self):
|
||||||
return SearchApiAPIWrapper(engine=self.engine, searchapi_api_key=self.api_key)
|
return SearchApiAPIWrapper(engine=self.engine, searchapi_api_key=self.api_key)
|
||||||
|
|
||||||
def run_model(self) -> list[Data]:
|
def run_model(self) -> DataFrame:
|
||||||
return self.fetch_content()
|
return self.fetch_content_dataframe()
|
||||||
|
|
||||||
def fetch_content(self) -> list[Data]:
|
def fetch_content(self) -> list[Data]:
|
||||||
wrapper = self._build_wrapper()
|
wrapper = self._build_wrapper()
|
||||||
|
|
@ -71,19 +69,11 @@ class SearchComponent(Component):
|
||||||
self.status = results
|
self.status = results
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def fetch_content_text(self) -> Message:
|
def fetch_content_dataframe(self) -> DataFrame:
|
||||||
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:
|
|
||||||
"""Convert the search results to a DataFrame.
|
"""Convert the search results to a DataFrame.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
DataFrame: A DataFrame containing the search results.
|
DataFrame: A DataFrame containing the search results.
|
||||||
"""
|
"""
|
||||||
data = self.fetch_content()
|
data = self.fetch_content()
|
||||||
return DataFrame(data)
|
return data_to_dataframe(data)
|
||||||
|
|
@ -3,10 +3,9 @@ from httpx import HTTPError
|
||||||
from langchain_core.tools import ToolException
|
from langchain_core.tools import ToolException
|
||||||
|
|
||||||
from langflow.custom import Component
|
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.io import MultilineInput, Output
|
||||||
from langflow.schema import Data
|
from langflow.schema import Data, DataFrame
|
||||||
from langflow.schema.message import Message
|
|
||||||
|
|
||||||
|
|
||||||
class WikidataComponent(Component):
|
class WikidataComponent(Component):
|
||||||
|
|
@ -25,10 +24,12 @@ class WikidataComponent(Component):
|
||||||
]
|
]
|
||||||
|
|
||||||
outputs = [
|
outputs = [
|
||||||
Output(display_name="Data", name="data", method="fetch_content"),
|
Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"),
|
||||||
Output(display_name="Message", name="text", method="fetch_content_text"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
|
def run_model(self) -> DataFrame:
|
||||||
|
return self.fetch_content_dataframe()
|
||||||
|
|
||||||
def fetch_content(self) -> list[Data]:
|
def fetch_content(self) -> list[Data]:
|
||||||
try:
|
try:
|
||||||
# Define request parameters for Wikidata API
|
# Define request parameters for Wikidata API
|
||||||
|
|
@ -79,8 +80,6 @@ class WikidataComponent(Component):
|
||||||
else:
|
else:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
def fetch_content_text(self) -> Message:
|
def fetch_content_dataframe(self) -> DataFrame:
|
||||||
data = self.fetch_content()
|
data = self.fetch_content()
|
||||||
result_string = data_to_text("{text}", data)
|
return data_to_dataframe(data)
|
||||||
self.status = result_string
|
|
||||||
return Message(text=result_string)
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
from langchain_community.utilities.wikipedia import WikipediaAPIWrapper
|
from langchain_community.utilities.wikipedia import WikipediaAPIWrapper
|
||||||
|
|
||||||
from langflow.custom import Component
|
from langflow.custom import Component
|
||||||
|
from langflow.helpers.data import data_to_dataframe
|
||||||
from langflow.inputs import BoolInput, IntInput, MessageTextInput, MultilineInput
|
from langflow.inputs import BoolInput, IntInput, MessageTextInput, MultilineInput
|
||||||
from langflow.io import Output
|
from langflow.io import Output
|
||||||
from langflow.schema import Data, DataFrame
|
from langflow.schema import Data, DataFrame
|
||||||
from langflow.schema.message import Message
|
|
||||||
|
|
||||||
|
|
||||||
class WikipediaComponent(Component):
|
class WikipediaComponent(Component):
|
||||||
|
|
@ -27,24 +27,11 @@ class WikipediaComponent(Component):
|
||||||
]
|
]
|
||||||
|
|
||||||
outputs = [
|
outputs = [
|
||||||
Output(display_name="Data", name="data", method="fetch_content"),
|
Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"),
|
||||||
Output(display_name="DataFrame", name="dataframe", method="as_dataframe"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def fetch_content(self) -> list[Data]:
|
def run_model(self) -> DataFrame:
|
||||||
wrapper = self._build_wrapper()
|
return self.fetch_content_dataframe()
|
||||||
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 _build_wrapper(self) -> WikipediaAPIWrapper:
|
def _build_wrapper(self) -> WikipediaAPIWrapper:
|
||||||
return WikipediaAPIWrapper(
|
return WikipediaAPIWrapper(
|
||||||
|
|
@ -54,11 +41,13 @@ class WikipediaComponent(Component):
|
||||||
doc_content_chars_max=self.doc_content_chars_max,
|
doc_content_chars_max=self.doc_content_chars_max,
|
||||||
)
|
)
|
||||||
|
|
||||||
def as_dataframe(self) -> DataFrame:
|
def fetch_content(self) -> list[Data]:
|
||||||
"""Convert the Wikipedia results to a DataFrame.
|
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:
|
def fetch_content_dataframe(self) -> DataFrame:
|
||||||
DataFrame: A DataFrame containing the Wikipedia results.
|
|
||||||
"""
|
|
||||||
data = self.fetch_content()
|
data = self.fetch_content()
|
||||||
return DataFrame(data)
|
return data_to_dataframe(data)
|
||||||
|
|
@ -2,6 +2,7 @@ from langchain_community.utilities.wolfram_alpha import WolframAlphaAPIWrapper
|
||||||
|
|
||||||
from langflow.base.langchain_utilities.model import LCToolComponent
|
from langflow.base.langchain_utilities.model import LCToolComponent
|
||||||
from langflow.field_typing import Tool
|
from langflow.field_typing import Tool
|
||||||
|
from langflow.helpers.data import data_to_dataframe
|
||||||
from langflow.inputs import MultilineInput, SecretStrInput
|
from langflow.inputs import MultilineInput, SecretStrInput
|
||||||
from langflow.io import Output
|
from langflow.io import Output
|
||||||
from langflow.schema import Data, DataFrame
|
from langflow.schema import Data, DataFrame
|
||||||
|
|
@ -14,8 +15,7 @@ topics, delivering structured responses."""
|
||||||
name = "WolframAlphaAPI"
|
name = "WolframAlphaAPI"
|
||||||
|
|
||||||
outputs = [
|
outputs = [
|
||||||
Output(display_name="Data", name="data", method="run_model"),
|
Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"),
|
||||||
Output(display_name="DataFrame", name="dataframe", method="as_dataframe"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
inputs = [
|
inputs = [
|
||||||
|
|
@ -27,12 +27,8 @@ topics, delivering structured responses."""
|
||||||
|
|
||||||
icon = "WolframAlphaAPI"
|
icon = "WolframAlphaAPI"
|
||||||
|
|
||||||
def run_model(self) -> list[Data]:
|
def run_model(self) -> DataFrame:
|
||||||
wrapper = self._build_wrapper()
|
return self.fetch_content_dataframe()
|
||||||
result_str = wrapper.run(self.input_value)
|
|
||||||
data = [Data(text=result_str)]
|
|
||||||
self.status = data
|
|
||||||
return data
|
|
||||||
|
|
||||||
def build_tool(self) -> Tool:
|
def build_tool(self) -> Tool:
|
||||||
wrapper = self._build_wrapper()
|
wrapper = self._build_wrapper()
|
||||||
|
|
@ -41,11 +37,18 @@ topics, delivering structured responses."""
|
||||||
def _build_wrapper(self) -> WolframAlphaAPIWrapper:
|
def _build_wrapper(self) -> WolframAlphaAPIWrapper:
|
||||||
return WolframAlphaAPIWrapper(wolfram_alpha_appid=self.app_id)
|
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.
|
"""Convert the Wolfram Alpha results to a DataFrame.
|
||||||
|
|
||||||
Returns:
|
Returns:
|
||||||
DataFrame: A DataFrame containing the query results.
|
DataFrame: A DataFrame containing the query results.
|
||||||
"""
|
"""
|
||||||
data = self.run_model()
|
data = self.fetch_content()
|
||||||
return DataFrame(data)
|
return data_to_dataframe(data)
|
||||||
|
|
@ -8,10 +8,10 @@ from loguru import logger
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from langflow.custom import Component
|
from langflow.custom import Component
|
||||||
|
from langflow.helpers.data import data_to_dataframe
|
||||||
from langflow.inputs import DropdownInput, IntInput, MessageTextInput
|
from langflow.inputs import DropdownInput, IntInput, MessageTextInput
|
||||||
from langflow.io import Output
|
from langflow.io import Output
|
||||||
from langflow.schema import Data, DataFrame
|
from langflow.schema import Data, DataFrame
|
||||||
from langflow.schema.message import Message
|
|
||||||
|
|
||||||
|
|
||||||
class YahooFinanceMethod(Enum):
|
class YahooFinanceMethod(Enum):
|
||||||
|
|
@ -77,21 +77,11 @@ to access financial data and market information from Yahoo Finance."""
|
||||||
]
|
]
|
||||||
|
|
||||||
outputs = [
|
outputs = [
|
||||||
Output(display_name="Data", name="data", method="fetch_content"),
|
Output(display_name="DataFrame", name="dataframe", method="fetch_content_dataframe"),
|
||||||
Output(display_name="Text", name="text", method="fetch_content_text"),
|
|
||||||
Output(display_name="DataFrame", name="dataframe", method="as_dataframe"),
|
|
||||||
]
|
]
|
||||||
|
|
||||||
def run_model(self) -> list[Data]:
|
def run_model(self) -> DataFrame:
|
||||||
return self.fetch_content()
|
return self.fetch_content_dataframe()
|
||||||
|
|
||||||
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 _fetch_yfinance_data(self, ticker: yf.Ticker, method: YahooFinanceMethod, num_news: int | None) -> str:
|
def _fetch_yfinance_data(self, ticker: yf.Ticker, method: YahooFinanceMethod, num_news: int | None) -> str:
|
||||||
try:
|
try:
|
||||||
|
|
@ -142,11 +132,6 @@ to access financial data and market information from Yahoo Finance."""
|
||||||
|
|
||||||
return data_list
|
return data_list
|
||||||
|
|
||||||
def as_dataframe(self) -> DataFrame:
|
def fetch_content_dataframe(self) -> DataFrame:
|
||||||
"""Convert the Yahoo search results to a DataFrame.
|
|
||||||
|
|
||||||
Returns:
|
|
||||||
DataFrame: A DataFrame containing the search results.
|
|
||||||
"""
|
|
||||||
data = self.fetch_content()
|
data = self.fetch_content()
|
||||||
return DataFrame(data)
|
return data_to_dataframe(data)
|
||||||
|
|
@ -2,69 +2,48 @@ import warnings
|
||||||
|
|
||||||
from langchain_core._api.deprecation import LangChainDeprecationWarning
|
from langchain_core._api.deprecation import LangChainDeprecationWarning
|
||||||
|
|
||||||
from .arxiv import ArXivComponent
|
|
||||||
from .bing_search_api import BingSearchAPIComponent
|
|
||||||
from .calculator import CalculatorToolComponent
|
from .calculator import CalculatorToolComponent
|
||||||
from .calculator_core import CalculatorComponent
|
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 import GoogleSearchAPIComponent
|
||||||
from .google_search_api_core import GoogleSearchAPICore
|
|
||||||
from .google_serper_api import GoogleSerperAPIComponent
|
from .google_serper_api import GoogleSerperAPIComponent
|
||||||
from .google_serper_api_core import GoogleSerperAPICore
|
|
||||||
from .mcp_component import MCPToolsComponent
|
from .mcp_component import MCPToolsComponent
|
||||||
from .python_code_structured_tool import PythonCodeStructuredTool
|
from .python_code_structured_tool import PythonCodeStructuredTool
|
||||||
from .python_repl import PythonREPLToolComponent
|
from .python_repl import PythonREPLToolComponent
|
||||||
from .python_repl_core import PythonREPLComponent
|
from .python_repl_core import PythonREPLComponent
|
||||||
from .search import SearchComponent
|
|
||||||
from .search_api import SearchAPIComponent
|
from .search_api import SearchAPIComponent
|
||||||
from .searxng import SearXNGToolComponent
|
from .searxng import SearXNGToolComponent
|
||||||
from .serp import SerpComponent
|
|
||||||
from .serp_api import SerpAPIComponent
|
from .serp_api import SerpAPIComponent
|
||||||
from .tavily_extract import TavilyExtractComponent
|
from .tavily_extract import TavilyExtractComponent
|
||||||
from .tavily_search import TavilySearchComponent
|
from .tavily_search import TavilySearchComponent
|
||||||
from .tavily_search_tool import TavilySearchToolComponent
|
from .tavily_search_tool import TavilySearchToolComponent
|
||||||
from .wikidata import WikidataComponent
|
|
||||||
from .wikidata_api import WikidataAPIComponent
|
from .wikidata_api import WikidataAPIComponent
|
||||||
from .wikipedia import WikipediaComponent
|
|
||||||
from .wikipedia_api import WikipediaAPIComponent
|
from .wikipedia_api import WikipediaAPIComponent
|
||||||
from .wolfram_alpha_api import WolframAlphaAPIComponent
|
|
||||||
from .yahoo import YfinanceComponent
|
|
||||||
from .yahoo_finance import YfinanceToolComponent
|
from .yahoo_finance import YfinanceToolComponent
|
||||||
|
|
||||||
with warnings.catch_warnings():
|
with warnings.catch_warnings():
|
||||||
warnings.simplefilter("ignore", LangChainDeprecationWarning)
|
warnings.simplefilter("ignore", LangChainDeprecationWarning)
|
||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"ArXivComponent",
|
"AstraDBCQLToolComponent",
|
||||||
"BingSearchAPIComponent",
|
"AstraDBToolComponent",
|
||||||
"CalculatorComponent",
|
"CalculatorComponent",
|
||||||
"CalculatorToolComponent",
|
"CalculatorToolComponent",
|
||||||
"DuckDuckGoSearchComponent",
|
"DuckDuckGoSearchComponent",
|
||||||
"ExaSearchToolkit",
|
"ExaSearchToolkit",
|
||||||
"GleanSearchAPIComponent",
|
"GleanSearchAPIComponent",
|
||||||
"GoogleSearchAPIComponent",
|
"GoogleSearchAPIComponent",
|
||||||
"GoogleSearchAPICore",
|
|
||||||
"GoogleSerperAPIComponent",
|
"GoogleSerperAPIComponent",
|
||||||
"GoogleSerperAPICore",
|
|
||||||
"MCPToolsComponent",
|
"MCPToolsComponent",
|
||||||
"PythonCodeStructuredTool",
|
"PythonCodeStructuredTool",
|
||||||
"PythonREPLComponent",
|
"PythonREPLComponent",
|
||||||
"PythonREPLToolComponent",
|
"PythonREPLToolComponent",
|
||||||
"SearXNGToolComponent",
|
"SearXNGToolComponent",
|
||||||
"SearchAPIComponent",
|
"SearchAPIComponent",
|
||||||
"SearchComponent",
|
|
||||||
"SerpAPIComponent",
|
"SerpAPIComponent",
|
||||||
"SerpComponent",
|
|
||||||
"TavilyExtractComponent",
|
"TavilyExtractComponent",
|
||||||
"TavilySearchComponent",
|
"TavilySearchComponent",
|
||||||
"TavilySearchToolComponent",
|
"TavilySearchToolComponent",
|
||||||
"WikidataAPIComponent",
|
"WikidataAPIComponent",
|
||||||
"WikidataComponent",
|
|
||||||
"WikipediaAPIComponent",
|
"WikipediaAPIComponent",
|
||||||
"WikipediaComponent",
|
|
||||||
"WolframAlphaAPIComponent",
|
|
||||||
"YfinanceComponent",
|
|
||||||
"YfinanceToolComponent",
|
"YfinanceToolComponent",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@ from collections import defaultdict
|
||||||
|
|
||||||
from langchain_core.documents import Document
|
from langchain_core.documents import Document
|
||||||
|
|
||||||
from langflow.schema import Data
|
from langflow.schema import Data, DataFrame
|
||||||
from langflow.schema.message import Message
|
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_]
|
formated_messages = [template.format(data=message.model_dump(), **message.model_dump()) for message in messages_]
|
||||||
return "\n".join(formated_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
|
|
@ -1462,7 +1462,7 @@
|
||||||
"show": true,
|
"show": true,
|
||||||
"title_case": false,
|
"title_case": false,
|
||||||
"type": "code",
|
"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": {
|
"engine": {
|
||||||
"_input_type": "DropdownInput",
|
"_input_type": "DropdownInput",
|
||||||
|
|
|
||||||
|
|
@ -50,21 +50,21 @@ class TestNewsSearchComponent(ComponentTestBaseWithoutClient):
|
||||||
component = NewsSearchComponent(query="OpenAI")
|
component = NewsSearchComponent(query="OpenAI")
|
||||||
result = component.search_news()
|
result = component.search_news()
|
||||||
assert isinstance(result, DataFrame)
|
assert isinstance(result, DataFrame)
|
||||||
df = result
|
news_results_df = result
|
||||||
assert len(df) == 2
|
assert len(news_results_df) == 2
|
||||||
assert list(df.columns) == ["title", "link", "published", "summary"]
|
assert list(news_results_df.columns) == ["title", "link", "published", "summary"]
|
||||||
assert df.iloc[0]["title"] == "Test News 1"
|
assert news_results_df.iloc[0]["title"] == "Test News 1"
|
||||||
assert df.iloc[1]["title"] == "Test News 2"
|
assert news_results_df.iloc[1]["title"] == "Test News 2"
|
||||||
|
|
||||||
def test_news_search_error(self):
|
def test_news_search_error(self):
|
||||||
with patch("requests.get", side_effect=requests.RequestException("Network error")):
|
with patch("requests.get", side_effect=requests.RequestException("Network error")):
|
||||||
component = NewsSearchComponent(query="OpenAI")
|
component = NewsSearchComponent(query="OpenAI")
|
||||||
result = component.search_news()
|
result = component.search_news()
|
||||||
assert isinstance(result, DataFrame)
|
assert isinstance(result, DataFrame)
|
||||||
df = result
|
news_results_df = result
|
||||||
assert len(df) == 1
|
assert len(news_results_df) == 1
|
||||||
assert df.iloc[0]["title"] == "Error"
|
assert news_results_df.iloc[0]["title"] == "Error"
|
||||||
assert "Network error" in df.iloc[0]["summary"]
|
assert "Network error" in news_results_df.iloc[0]["summary"]
|
||||||
|
|
||||||
def test_empty_news_results(self):
|
def test_empty_news_results(self):
|
||||||
# Mock empty RSS feed
|
# Mock empty RSS feed
|
||||||
|
|
@ -83,6 +83,6 @@ class TestNewsSearchComponent(ComponentTestBaseWithoutClient):
|
||||||
component = NewsSearchComponent(query="OpenAI")
|
component = NewsSearchComponent(query="OpenAI")
|
||||||
result = component.search_news()
|
result = component.search_news()
|
||||||
assert isinstance(result, DataFrame)
|
assert isinstance(result, DataFrame)
|
||||||
df = result
|
news_results_df = result
|
||||||
assert len(df) == 1
|
assert len(news_results_df) == 1
|
||||||
assert df.iloc[0]["title"] == "No articles found"
|
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):
|
class TestArXivComponent(ComponentTestBaseWithClient):
|
||||||
def test_component_versions(self, default_kwargs, file_names_mapping):
|
def test_component_versions(self, default_kwargs, file_names_mapping):
|
||||||
"""Test component compatibility across versions."""
|
"""Test component compatibility across versions."""
|
||||||
from langflow.components.tools.arxiv import ArXivComponent
|
from langflow.components.search.arxiv import ArXivComponent
|
||||||
|
|
||||||
# Test current version
|
# Test current version
|
||||||
component = ArXivComponent(**default_kwargs)
|
component = ArXivComponent(**default_kwargs)
|
||||||
|
|
@ -31,7 +31,7 @@ class TestArXivComponent(ComponentTestBaseWithClient):
|
||||||
|
|
||||||
@pytest.fixture
|
@pytest.fixture
|
||||||
def component_class(self):
|
def component_class(self):
|
||||||
from langflow.components.tools.arxiv import ArXivComponent
|
from langflow.components.search.arxiv import ArXivComponent
|
||||||
|
|
||||||
return ArXivComponent
|
return ArXivComponent
|
||||||
|
|
||||||
|
|
@ -2,7 +2,7 @@ from unittest.mock import patch
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytest
|
import pytest
|
||||||
from langflow.components.tools import GoogleSearchAPICore
|
from langflow.components.search import GoogleSearchAPICore
|
||||||
from langflow.schema import DataFrame
|
from langflow.schema import DataFrame
|
||||||
|
|
||||||
from tests.base import ComponentTestBaseWithoutClient
|
from tests.base import ComponentTestBaseWithoutClient
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from langflow.components.tools import GoogleSerperAPICore
|
from langflow.components.search import GoogleSerperAPICore
|
||||||
from langflow.schema import DataFrame
|
from langflow.schema import DataFrame
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -3,11 +3,9 @@ from unittest.mock import MagicMock, patch
|
||||||
import httpx
|
import httpx
|
||||||
import pytest
|
import pytest
|
||||||
from langchain_core.tools import ToolException
|
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 import Component
|
||||||
from langflow.custom.utils import build_custom_component_template
|
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
|
# Import the base test class
|
||||||
from tests.base import ComponentTestBaseWithoutClient
|
from tests.base import ComponentTestBaseWithoutClient
|
||||||
|
|
@ -102,18 +100,3 @@ class TestWikidataComponent(ComponentTestBaseWithoutClient):
|
||||||
|
|
||||||
with pytest.raises(ToolException):
|
with pytest.raises(ToolException):
|
||||||
component.fetch_content()
|
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
|
from unittest.mock import MagicMock
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from langflow.components.tools import WikipediaComponent
|
from langflow.components.search import WikipediaComponent
|
||||||
from langflow.custom import Component
|
from langflow.custom import Component
|
||||||
from langflow.custom.utils import build_custom_component_template
|
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
|
# Import the base test class
|
||||||
from tests.base import ComponentTestBaseWithoutClient
|
from tests.base import ComponentTestBaseWithoutClient
|
||||||
|
|
@ -83,15 +81,6 @@ class TestWikipediaComponent(ComponentTestBaseWithoutClient):
|
||||||
assert len(result) == 1
|
assert len(result) == 1
|
||||||
assert result[0].text == "Test content"
|
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):
|
def test_wikipedia_error_handling(self, component_class):
|
||||||
component = component_class()
|
component = component_class()
|
||||||
# Mock _build_wrapper to raise exception
|
# Mock _build_wrapper to raise exception
|
||||||
|
|
@ -2,8 +2,7 @@ from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from langchain_core.tools import ToolException
|
from langchain_core.tools import ToolException
|
||||||
from langflow.components.tools import YfinanceComponent
|
from langflow.components.search.yahoo import YahooFinanceMethod, YfinanceComponent
|
||||||
from langflow.components.tools.yahoo import YahooFinanceMethod
|
|
||||||
from langflow.custom.utils import build_custom_component_template
|
from langflow.custom.utils import build_custom_component_template
|
||||||
from langflow.schema import Data
|
from langflow.schema import Data
|
||||||
|
|
||||||
|
|
@ -38,7 +37,7 @@ class TestYfinanceComponent:
|
||||||
for input_name in expected_inputs:
|
for input_name in expected_inputs:
|
||||||
assert input_name in input_names
|
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):
|
def test_fetch_info(self, mock_ticker, component_class, default_kwargs):
|
||||||
component = component_class(**default_kwargs)
|
component = component_class(**default_kwargs)
|
||||||
|
|
||||||
|
|
@ -53,7 +52,7 @@ class TestYfinanceComponent:
|
||||||
assert len(result) == 1
|
assert len(result) == 1
|
||||||
assert "Apple Inc." in result[0].text
|
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):
|
def test_fetch_news(self, mock_ticker, component_class):
|
||||||
component = component_class(symbol="AAPL", method=YahooFinanceMethod.GET_NEWS, num_news=2)
|
component = component_class(symbol="AAPL", method=YahooFinanceMethod.GET_NEWS, num_news=2)
|
||||||
|
|
||||||
|
|
@ -2,7 +2,7 @@ from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
from langchain_core.tools import ToolException
|
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 import Component
|
||||||
from langflow.custom.utils import build_custom_component_template
|
from langflow.custom.utils import build_custom_component_template
|
||||||
from langflow.schema import Data
|
from langflow.schema import Data
|
||||||
|
|
@ -34,7 +34,7 @@ def test_serpapi_template():
|
||||||
assert input_name in input_names
|
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):
|
def test_fetch_content(mock_serpapi_wrapper):
|
||||||
component = SerpComponent()
|
component = SerpComponent()
|
||||||
component.serpapi_api_key = "test-key"
|
component.serpapi_api_key = "test-key"
|
||||||
|
|
@ -81,7 +81,7 @@ def test_error_handling():
|
||||||
component.serpapi_api_key = "test-key"
|
component.serpapi_api_key = "test-key"
|
||||||
component.input_value = "test query"
|
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_instance = MagicMock()
|
||||||
mock_serpapi.return_value = mock_instance
|
mock_serpapi.return_value = mock_instance
|
||||||
mock_instance.results.side_effect = Exception("API Error")
|
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_total_events = 50 # Limit to prevent infinite loops
|
||||||
self.max_empty_polls = 10 # Maximum number of empty polls before giving up
|
self.max_empty_polls = 10 # Maximum number of empty polls before giving up
|
||||||
self.poll_timeout = 3.0 # Timeout for each polling request
|
self.poll_timeout = 3.0 # Timeout for each polling request
|
||||||
|
self._closed = False
|
||||||
|
|
||||||
async def aiter_lines(self):
|
async def aiter_lines(self):
|
||||||
|
if self._closed:
|
||||||
|
return
|
||||||
|
|
||||||
try:
|
try:
|
||||||
empty_polls = 0
|
empty_polls = 0
|
||||||
total_events = 0
|
total_events = 0
|
||||||
end_event_found = False
|
end_event_found = False
|
||||||
|
|
||||||
while (
|
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
|
# Add Accept header for NDJSON
|
||||||
headers = {**self.headers, "Accept": "application/x-ndjson"}
|
headers = {**self.headers, "Accept": "application/x-ndjson"}
|
||||||
|
|
||||||
# Set a timeout for the request
|
try:
|
||||||
response = await asyncio.wait_for(
|
# Set a timeout for the request
|
||||||
self.client.get(
|
response = await asyncio.wait_for(
|
||||||
f"api/v1/build/{self.job_id}/events?event_delivery=polling",
|
self.client.get(
|
||||||
headers=headers,
|
f"api/v1/build/{self.job_id}/events?event_delivery=polling",
|
||||||
),
|
headers=headers,
|
||||||
timeout=self.poll_timeout,
|
),
|
||||||
)
|
timeout=self.poll_timeout,
|
||||||
|
)
|
||||||
|
|
||||||
assert response.status_code == codes.OK
|
if response.status_code != codes.OK:
|
||||||
|
break
|
||||||
|
|
||||||
# Get the NDJSON response as text
|
# Get the NDJSON response as text
|
||||||
text = response.text
|
text = response.text
|
||||||
|
|
||||||
# Skip if response is empty
|
# Skip if response is empty
|
||||||
if not text.strip():
|
if not text.strip():
|
||||||
empty_polls += 1
|
empty_polls += 1
|
||||||
await asyncio.sleep(0.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():
|
|
||||||
continue
|
continue
|
||||||
|
|
||||||
line_count += 1
|
# Reset empty polls counter since we got data
|
||||||
total_events += 1
|
empty_polls = 0
|
||||||
|
|
||||||
# Check for end event with multiple possible formats
|
# Process each line as an individual JSON object
|
||||||
if '"event":"end"' in line or '"event": "end"' in line:
|
line_count = 0
|
||||||
end_event_found = True
|
for line in text.splitlines():
|
||||||
|
if not line.strip():
|
||||||
|
continue
|
||||||
|
|
||||||
# Validate it's proper JSON before yielding
|
line_count += 1
|
||||||
try:
|
total_events += 1
|
||||||
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
|
# Check for end event with multiple possible formats
|
||||||
if line_count == 0:
|
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
|
empty_polls += 1
|
||||||
|
continue
|
||||||
# Add a small delay to prevent tight polling
|
|
||||||
await asyncio.sleep(0.1)
|
|
||||||
|
|
||||||
# If we hit the limit without finding the end event, log a warning
|
# If we hit the limit without finding the end event, log a warning
|
||||||
if total_events >= self.max_total_events:
|
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"
|
f"WARNING: Reached maximum empty polls ({self.max_empty_polls}) without finding end event"
|
||||||
)
|
)
|
||||||
|
|
||||||
except asyncio.TimeoutError as e:
|
except Exception as e:
|
||||||
logger.debug(f"ERROR: Polling request timed out after {self.poll_timeout}s")
|
logger.debug(f"ERROR: Unexpected error during polling: {e!s}")
|
||||||
msg = "Build event polling timed out."
|
raise
|
||||||
raise TimeoutError(msg) from e
|
finally:
|
||||||
|
self._closed = True
|
||||||
|
|
||||||
|
def close(self):
|
||||||
|
self._closed = True
|
||||||
|
|
||||||
polling_response = PollingResponse(client, job_id, logged_in_headers)
|
polling_response = PollingResponse(client, job_id, logged_in_headers)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -271,6 +271,7 @@ export const SIDEBAR_BUNDLES = [
|
||||||
name: "homeassistant",
|
name: "homeassistant",
|
||||||
icon: "HomeAssistant",
|
icon: "HomeAssistant",
|
||||||
},
|
},
|
||||||
|
{ display_name: "Search", name: "search", icon: "Search" },
|
||||||
];
|
];
|
||||||
|
|
||||||
export const categoryIcons: Record<string, string> = {
|
export const categoryIcons: Record<string, string> = {
|
||||||
|
|
|
||||||
|
|
@ -125,7 +125,6 @@ test(
|
||||||
await expect(page.getByTestId("dataAPI Request")).toBeVisible();
|
await expect(page.getByTestId("dataAPI Request")).toBeVisible();
|
||||||
await expect(page.getByTestId("helpersMessage History")).toBeVisible();
|
await expect(page.getByTestId("helpersMessage History")).toBeVisible();
|
||||||
await expect(page.getByTestId("vectorstoresAstra DB")).toBeVisible();
|
await expect(page.getByTestId("vectorstoresAstra DB")).toBeVisible();
|
||||||
await expect(page.getByTestId("toolsSearch API")).toBeVisible();
|
|
||||||
await expect(page.getByTestId("logicSub Flow [Deprecated]")).toBeVisible();
|
await expect(page.getByTestId("logicSub Flow [Deprecated]")).toBeVisible();
|
||||||
|
|
||||||
await page.getByTestId("sidebar-options-trigger").click();
|
await page.getByTestId("sidebar-options-trigger").click();
|
||||||
|
|
@ -137,19 +136,16 @@ test(
|
||||||
await expect(page.getByTestId("logicSub Flow [Deprecated]")).toBeVisible();
|
await expect(page.getByTestId("logicSub Flow [Deprecated]")).toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByTestId("processingSplit Text")).toBeVisible();
|
await expect(page.getByTestId("processingSplit Text")).toBeVisible();
|
||||||
await expect(page.getByTestId("toolsSearch API")).toBeVisible();
|
|
||||||
|
|
||||||
await page.getByTestId("icon-X").first().click();
|
await page.getByTestId("icon-X").first().click();
|
||||||
|
|
||||||
await expect(page.getByTestId("dataAPI Request")).not.toBeVisible();
|
await expect(page.getByTestId("dataAPI Request")).not.toBeVisible();
|
||||||
await expect(page.getByTestId("helpersMessage History")).not.toBeVisible();
|
await expect(page.getByTestId("helpersMessage History")).not.toBeVisible();
|
||||||
await expect(page.getByTestId("vectorstoresAstra DB")).not.toBeVisible();
|
await expect(page.getByTestId("vectorstoresAstra DB")).not.toBeVisible();
|
||||||
await expect(page.getByTestId("toolsSearch API")).not.toBeVisible();
|
|
||||||
await expect(
|
await expect(
|
||||||
page.getByTestId("logicSub Flow [Deprecated]"),
|
page.getByTestId("logicSub Flow [Deprecated]"),
|
||||||
).not.toBeVisible();
|
).not.toBeVisible();
|
||||||
|
|
||||||
await expect(page.getByTestId("processingSplit Text")).not.toBeVisible();
|
await expect(page.getByTestId("processingSplit Text")).not.toBeVisible();
|
||||||
await expect(page.getByTestId("toolsSearch API")).not.toBeVisible();
|
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -11,12 +11,12 @@ test(
|
||||||
await page.getByTestId("sidebar-search-input").click();
|
await page.getByTestId("sidebar-search-input").click();
|
||||||
await page.getByTestId("sidebar-search-input").fill("duck");
|
await page.getByTestId("sidebar-search-input").fill("duck");
|
||||||
|
|
||||||
await page.waitForSelector('[data-testid="toolsDuckDuckGo Search"]', {
|
await page.waitForSelector('[data-testid="searchDuckDuckGo Search"]', {
|
||||||
timeout: 3000,
|
timeout: 3000,
|
||||||
});
|
});
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByTestId("toolsDuckDuckGo Search")
|
.getByTestId("searchDuckDuckGo Search")
|
||||||
.hover()
|
.hover()
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
await page
|
await page
|
||||||
|
|
@ -44,7 +44,7 @@ test(
|
||||||
) ?? false;
|
) ?? false;
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByTestId("output-inspection-data-duckduckgosearchcomponent")
|
.getByTestId("output-inspection-dataframe-duckduckgosearchcomponent")
|
||||||
.first()
|
.first()
|
||||||
.click();
|
.click();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -49,14 +49,14 @@ test(
|
||||||
|
|
||||||
await page.getByTestId("sidebar-search-input").click();
|
await page.getByTestId("sidebar-search-input").click();
|
||||||
await page.getByTestId("sidebar-search-input").fill("search api");
|
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,
|
timeout: 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
await zoomOut(page, 3);
|
await zoomOut(page, 3);
|
||||||
|
|
||||||
await page
|
await page
|
||||||
.getByTestId("toolsSearch API")
|
.getByTestId("searchSearch API")
|
||||||
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
|
.dragTo(page.locator('//*[@id="react-flow-id"]'), {
|
||||||
targetPosition: { x: 100, y: 100 },
|
targetPosition: { x: 100, y: 100 },
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue