feat: add WikiData Component and depeciates the WikiData API tool component (#5872)
* update * [autofix.ci] apply automated fixes * Update test_wikidata_api.py * [autofix.ci] apply automated fixes * Update wikidata.py --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
dcd68c5f4e
commit
229ba19a66
4 changed files with 252 additions and 136 deletions
|
|
@ -24,6 +24,7 @@ from .serp import SerpComponent
|
||||||
from .serp_api import SerpAPIComponent
|
from .serp_api import SerpAPIComponent
|
||||||
from .tavily import TavilySearchComponent
|
from .tavily import TavilySearchComponent
|
||||||
from .tavily_search import TavilySearchToolComponent
|
from .tavily_search import TavilySearchToolComponent
|
||||||
|
from .wikidata import WikidataComponent
|
||||||
from .wikidata_api import WikidataAPIComponent
|
from .wikidata_api import WikidataAPIComponent
|
||||||
from .wikipedia import WikipediaComponent
|
from .wikipedia import WikipediaComponent
|
||||||
from .wikipedia_api import WikipediaAPIComponent
|
from .wikipedia_api import WikipediaAPIComponent
|
||||||
|
|
@ -62,6 +63,7 @@ __all__ = [
|
||||||
"TavilySearchComponent",
|
"TavilySearchComponent",
|
||||||
"TavilySearchToolComponent",
|
"TavilySearchToolComponent",
|
||||||
"WikidataAPIComponent",
|
"WikidataAPIComponent",
|
||||||
|
"WikidataComponent",
|
||||||
"WikipediaAPIComponent",
|
"WikipediaAPIComponent",
|
||||||
"WikipediaComponent",
|
"WikipediaComponent",
|
||||||
"WolframAlphaAPIComponent",
|
"WolframAlphaAPIComponent",
|
||||||
|
|
|
||||||
86
src/backend/base/langflow/components/tools/wikidata.py
Normal file
86
src/backend/base/langflow/components/tools/wikidata.py
Normal file
|
|
@ -0,0 +1,86 @@
|
||||||
|
import httpx
|
||||||
|
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.io import MultilineInput, Output
|
||||||
|
from langflow.schema import Data
|
||||||
|
from langflow.schema.message import Message
|
||||||
|
|
||||||
|
|
||||||
|
class WikidataComponent(Component):
|
||||||
|
display_name = "Wikidata"
|
||||||
|
description = "Performs a search using the Wikidata API."
|
||||||
|
icon = "Wikipedia"
|
||||||
|
|
||||||
|
inputs = [
|
||||||
|
MultilineInput(
|
||||||
|
name="query",
|
||||||
|
display_name="Query",
|
||||||
|
info="The text query for similarity search on Wikidata.",
|
||||||
|
required=True,
|
||||||
|
tool_mode=True,
|
||||||
|
),
|
||||||
|
]
|
||||||
|
|
||||||
|
outputs = [
|
||||||
|
Output(display_name="Data", name="data", method="fetch_content"),
|
||||||
|
Output(display_name="Message", name="text", method="fetch_content_text"),
|
||||||
|
]
|
||||||
|
|
||||||
|
def fetch_content(self) -> list[Data]:
|
||||||
|
try:
|
||||||
|
# Define request parameters for Wikidata API
|
||||||
|
params = {
|
||||||
|
"action": "wbsearchentities",
|
||||||
|
"format": "json",
|
||||||
|
"search": self.query,
|
||||||
|
"language": "en",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send request to Wikidata API
|
||||||
|
wikidata_api_url = "https://www.wikidata.org/w/api.php"
|
||||||
|
response = httpx.get(wikidata_api_url, params=params)
|
||||||
|
response.raise_for_status()
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
# Extract search results
|
||||||
|
results = response_json.get("search", [])
|
||||||
|
|
||||||
|
if not results:
|
||||||
|
return [Data(data={"error": "No search results found for the given query."})]
|
||||||
|
|
||||||
|
# Transform the API response into Data objects
|
||||||
|
data = [
|
||||||
|
Data(
|
||||||
|
text=f"{result['label']}: {result.get('description', '')}",
|
||||||
|
data={
|
||||||
|
"label": result["label"],
|
||||||
|
"id": result.get("id"),
|
||||||
|
"url": result.get("url"),
|
||||||
|
"description": result.get("description", ""),
|
||||||
|
"concepturi": result.get("concepturi"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
for result in results
|
||||||
|
]
|
||||||
|
|
||||||
|
self.status = data
|
||||||
|
except HTTPError as e:
|
||||||
|
error_message = f"HTTP Error in Wikidata Search API: {e!s}"
|
||||||
|
raise ToolException(error_message) from None
|
||||||
|
except KeyError as e:
|
||||||
|
error_message = f"Data parsing error in Wikidata API response: {e!s}"
|
||||||
|
raise ToolException(error_message) from None
|
||||||
|
except ValueError as e:
|
||||||
|
error_message = f"Value error in Wikidata API: {e!s}"
|
||||||
|
raise ToolException(error_message) from None
|
||||||
|
else:
|
||||||
|
return data
|
||||||
|
|
||||||
|
def fetch_content_text(self) -> Message:
|
||||||
|
data = self.fetch_content()
|
||||||
|
result_string = data_to_text("{text}", data)
|
||||||
|
self.status = result_string
|
||||||
|
return Message(text=result_string)
|
||||||
|
|
@ -1,19 +1,63 @@
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
import httpx
|
import httpx
|
||||||
from httpx import HTTPError
|
from langchain_core.tools import StructuredTool, ToolException
|
||||||
from langchain_core.tools import ToolException
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
from langflow.custom import Component
|
from langflow.base.langchain_utilities.model import LCToolComponent
|
||||||
from langflow.helpers.data import data_to_text
|
from langflow.field_typing import Tool
|
||||||
from langflow.io import MultilineInput, Output
|
from langflow.inputs import MultilineInput
|
||||||
from langflow.schema import Data
|
from langflow.schema import Data
|
||||||
from langflow.schema.message import Message
|
|
||||||
|
|
||||||
|
|
||||||
class WikidataAPIComponent(Component):
|
class WikidataSearchSchema(BaseModel):
|
||||||
display_name = "Wikidata API"
|
query: str = Field(..., description="The search query for Wikidata")
|
||||||
|
|
||||||
|
|
||||||
|
class WikidataAPIWrapper(BaseModel):
|
||||||
|
"""Wrapper around Wikidata API."""
|
||||||
|
|
||||||
|
wikidata_api_url: str = "https://www.wikidata.org/w/api.php"
|
||||||
|
|
||||||
|
def results(self, query: str) -> list[dict[str, Any]]:
|
||||||
|
# Define request parameters for Wikidata API
|
||||||
|
params = {
|
||||||
|
"action": "wbsearchentities",
|
||||||
|
"format": "json",
|
||||||
|
"search": query,
|
||||||
|
"language": "en",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Send request to Wikidata API
|
||||||
|
response = httpx.get(self.wikidata_api_url, params=params)
|
||||||
|
response.raise_for_status()
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
# Extract and return search results
|
||||||
|
return response_json.get("search", [])
|
||||||
|
|
||||||
|
def run(self, query: str) -> list[dict[str, Any]]:
|
||||||
|
try:
|
||||||
|
results = self.results(query)
|
||||||
|
if results:
|
||||||
|
return results
|
||||||
|
|
||||||
|
error_message = "No search results found for the given query."
|
||||||
|
|
||||||
|
raise ToolException(error_message)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
error_message = f"Error in Wikidata Search API: {e!s}"
|
||||||
|
|
||||||
|
raise ToolException(error_message) from e
|
||||||
|
|
||||||
|
|
||||||
|
class WikidataAPIComponent(LCToolComponent):
|
||||||
|
display_name = "Wikidata API [Deprecated]"
|
||||||
description = "Performs a search using the Wikidata API."
|
description = "Performs a search using the Wikidata API."
|
||||||
name = "WikidataAPI"
|
name = "WikidataAPI"
|
||||||
icon = "Wikipedia"
|
icon = "Wikipedia"
|
||||||
|
legacy = True
|
||||||
|
|
||||||
inputs = [
|
inputs = [
|
||||||
MultilineInput(
|
MultilineInput(
|
||||||
|
|
@ -21,67 +65,38 @@ class WikidataAPIComponent(Component):
|
||||||
display_name="Query",
|
display_name="Query",
|
||||||
info="The text query for similarity search on Wikidata.",
|
info="The text query for similarity search on Wikidata.",
|
||||||
required=True,
|
required=True,
|
||||||
tool_mode=True,
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
outputs = [
|
def build_tool(self) -> Tool:
|
||||||
Output(display_name="Data", name="data", method="fetch_content"),
|
wrapper = WikidataAPIWrapper()
|
||||||
Output(display_name="Message", name="text", method="fetch_content_text"),
|
|
||||||
]
|
|
||||||
|
|
||||||
def fetch_content(self) -> list[Data]:
|
# Define the tool using StructuredTool and wrapper's run method
|
||||||
try:
|
tool = StructuredTool.from_function(
|
||||||
# Define request parameters for Wikidata API
|
name="wikidata_search_api",
|
||||||
params = {
|
description="Perform similarity search on Wikidata API",
|
||||||
"action": "wbsearchentities",
|
func=wrapper.run,
|
||||||
"format": "json",
|
args_schema=WikidataSearchSchema,
|
||||||
"search": self.query,
|
)
|
||||||
"language": "en",
|
|
||||||
}
|
|
||||||
|
|
||||||
# Send request to Wikidata API
|
self.status = "Wikidata Search API Tool for Langchain"
|
||||||
wikidata_api_url = "https://www.wikidata.org/w/api.php"
|
|
||||||
response = httpx.get(wikidata_api_url, params=params)
|
|
||||||
response.raise_for_status()
|
|
||||||
response_json = response.json()
|
|
||||||
|
|
||||||
# Extract search results
|
return tool
|
||||||
results = response_json.get("search", [])
|
|
||||||
|
|
||||||
if not results:
|
def run_model(self) -> list[Data]:
|
||||||
return [Data(data={"error": "No search results found for the given query."})]
|
tool = self.build_tool()
|
||||||
|
|
||||||
# Transform the API response into Data objects
|
results = tool.run({"query": self.query})
|
||||||
data = [
|
|
||||||
Data(
|
|
||||||
text=f"{result['label']}: {result.get('description', '')}",
|
|
||||||
data={
|
|
||||||
"label": result["label"],
|
|
||||||
"id": result.get("id"),
|
|
||||||
"url": result.get("url"),
|
|
||||||
"description": result.get("description", ""),
|
|
||||||
"concepturi": result.get("concepturi"),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
for result in results
|
|
||||||
]
|
|
||||||
|
|
||||||
self.status = data
|
# Transform the API response into Data objects
|
||||||
except HTTPError as e:
|
data = [
|
||||||
error_message = f"HTTP Error in Wikidata Search API: {e!s}"
|
Data(
|
||||||
raise ToolException(error_message) from None
|
text=result["label"],
|
||||||
except KeyError as e:
|
metadata=result,
|
||||||
error_message = f"Data parsing error in Wikidata API response: {e!s}"
|
)
|
||||||
raise ToolException(error_message) from None
|
for result in results
|
||||||
except ValueError as e:
|
]
|
||||||
error_message = f"Value error in Wikidata API: {e!s}"
|
|
||||||
raise ToolException(error_message) from None
|
|
||||||
else:
|
|
||||||
return data
|
|
||||||
|
|
||||||
def fetch_content_text(self) -> Message:
|
self.status = data # type: ignore[assignment]
|
||||||
data = self.fetch_content()
|
|
||||||
result_string = data_to_text("{text}", data)
|
return data
|
||||||
self.status = result_string
|
|
||||||
return Message(text=result_string)
|
|
||||||
|
|
|
||||||
|
|
@ -3,104 +3,117 @@ 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 WikidataAPIComponent
|
from langflow.components.tools 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 import Data
|
||||||
from langflow.schema.message import Message
|
from langflow.schema.message import Message
|
||||||
|
|
||||||
|
# Import the base test class
|
||||||
def test_wikidata_initialization():
|
from tests.base import ComponentTestBaseWithoutClient
|
||||||
component = WikidataAPIComponent()
|
|
||||||
assert component.display_name == "Wikidata API"
|
|
||||||
assert component.description == "Performs a search using the Wikidata API."
|
|
||||||
assert component.icon == "Wikipedia"
|
|
||||||
|
|
||||||
|
|
||||||
def test_wikidata_template():
|
class TestWikidataComponent(ComponentTestBaseWithoutClient):
|
||||||
wikidata = WikidataAPIComponent()
|
@pytest.fixture
|
||||||
component = Component(_code=wikidata._code)
|
def component_class(self):
|
||||||
frontend_node, _ = build_custom_component_template(component)
|
"""Fixture to create a WikidataComponent instance."""
|
||||||
|
return WikidataComponent
|
||||||
|
|
||||||
# Verify basic structure
|
@pytest.fixture
|
||||||
assert isinstance(frontend_node, dict)
|
def file_names_mapping(self):
|
||||||
|
"""Return an empty list since this component doesn't have version-specific files."""
|
||||||
|
return []
|
||||||
|
|
||||||
# Verify inputs
|
@pytest.fixture
|
||||||
assert "template" in frontend_node
|
def mock_query(self):
|
||||||
input_names = [input_["name"] for input_ in frontend_node["template"].values() if isinstance(input_, dict)]
|
"""Fixture to provide a default query."""
|
||||||
assert "query" in input_names
|
return "test query"
|
||||||
|
|
||||||
|
def test_wikidata_initialization(self, component_class):
|
||||||
|
component = component_class()
|
||||||
|
assert component.display_name == "Wikidata"
|
||||||
|
assert component.description == "Performs a search using the Wikidata API."
|
||||||
|
assert component.icon == "Wikipedia"
|
||||||
|
|
||||||
@patch("langflow.components.tools.wikidata_api.httpx.get")
|
def test_wikidata_template(self, component_class):
|
||||||
def test_fetch_content_success(mock_httpx):
|
component = component_class()
|
||||||
component = WikidataAPIComponent()
|
frontend_node, _ = build_custom_component_template(Component(_code=component._code))
|
||||||
component.query = "test query"
|
|
||||||
|
|
||||||
# Mock successful API response
|
# Verify basic structure
|
||||||
mock_response = MagicMock()
|
assert isinstance(frontend_node, dict)
|
||||||
mock_response.json.return_value = {
|
|
||||||
"search": [
|
|
||||||
{
|
|
||||||
"label": "Test Label",
|
|
||||||
"id": "Q123",
|
|
||||||
"url": "https://test.com",
|
|
||||||
"description": "Test Description",
|
|
||||||
"concepturi": "https://test.com/concept",
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
mock_httpx.return_value = mock_response
|
|
||||||
|
|
||||||
result = component.fetch_content()
|
# Verify inputs
|
||||||
|
assert "template" in frontend_node
|
||||||
|
input_names = [input_["name"] for input_ in frontend_node["template"].values() if isinstance(input_, dict)]
|
||||||
|
assert "query" in input_names
|
||||||
|
|
||||||
assert isinstance(result, list)
|
@patch("langflow.components.tools.wikidata_api.httpx.get")
|
||||||
assert len(result) == 1
|
def test_fetch_content_success(self, mock_httpx, component_class, mock_query):
|
||||||
assert result[0].text == "Test Label: Test Description"
|
component = component_class()
|
||||||
assert result[0].data["label"] == "Test Label"
|
component.query = mock_query
|
||||||
assert result[0].data["id"] == "Q123"
|
|
||||||
|
|
||||||
|
# Mock successful API response
|
||||||
|
mock_response = MagicMock()
|
||||||
|
mock_response.json.return_value = {
|
||||||
|
"search": [
|
||||||
|
{
|
||||||
|
"label": "Test Label",
|
||||||
|
"id": "Q123",
|
||||||
|
"url": "https://test.com",
|
||||||
|
"description": "Test Description",
|
||||||
|
"concepturi": "https://test.com/concept",
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
mock_httpx.return_value = mock_response
|
||||||
|
|
||||||
@patch("langflow.components.tools.wikidata_api.httpx.get")
|
result = component.fetch_content()
|
||||||
def test_fetch_content_empty_response(mock_httpx):
|
|
||||||
component = WikidataAPIComponent()
|
|
||||||
component.query = "test query"
|
|
||||||
|
|
||||||
# Mock empty API response
|
assert isinstance(result, list)
|
||||||
mock_response = MagicMock()
|
assert len(result) == 1
|
||||||
mock_response.json.return_value = {"search": []}
|
assert result[0].text == "Test Label: Test Description"
|
||||||
mock_httpx.return_value = mock_response
|
assert result[0].data["label"] == "Test Label"
|
||||||
|
assert result[0].data["id"] == "Q123"
|
||||||
|
|
||||||
result = component.fetch_content()
|
@patch("langflow.components.tools.wikidata_api.httpx.get")
|
||||||
|
def test_fetch_content_empty_response(self, mock_httpx, component_class, mock_query):
|
||||||
|
component = component_class()
|
||||||
|
component.query = mock_query
|
||||||
|
|
||||||
assert isinstance(result, list)
|
# Mock empty API response
|
||||||
assert len(result) == 1
|
mock_response = MagicMock()
|
||||||
assert "error" in result[0].data
|
mock_response.json.return_value = {"search": []}
|
||||||
assert "No search results found" in result[0].data["error"]
|
mock_httpx.return_value = mock_response
|
||||||
|
|
||||||
|
result = component.fetch_content()
|
||||||
|
|
||||||
@patch("langflow.components.tools.wikidata_api.httpx.get")
|
assert isinstance(result, list)
|
||||||
def test_fetch_content_error_handling(mock_httpx):
|
assert len(result) == 1
|
||||||
component = WikidataAPIComponent()
|
assert "error" in result[0].data
|
||||||
component.query = "test query"
|
assert "No search results found" in result[0].data["error"]
|
||||||
|
|
||||||
# Mock HTTP error
|
@patch("langflow.components.tools.wikidata_api.httpx.get")
|
||||||
mock_httpx.side_effect = httpx.HTTPError("API Error")
|
def test_fetch_content_error_handling(self, mock_httpx, component_class, mock_query):
|
||||||
|
component = component_class()
|
||||||
|
component.query = mock_query
|
||||||
|
|
||||||
with pytest.raises(ToolException):
|
# Mock HTTP error
|
||||||
component.fetch_content()
|
mock_httpx.side_effect = httpx.HTTPError("API Error")
|
||||||
|
|
||||||
|
with pytest.raises(ToolException):
|
||||||
|
component.fetch_content()
|
||||||
|
|
||||||
def test_fetch_content_text():
|
def test_fetch_content_text(self, component_class):
|
||||||
component = WikidataAPIComponent()
|
component = component_class()
|
||||||
component.fetch_content = MagicMock(
|
component.fetch_content = MagicMock(
|
||||||
return_value=[
|
return_value=[
|
||||||
Data(text="First result", data={"label": "Label 1"}),
|
Data(text="First result", data={"label": "Label 1"}),
|
||||||
Data(text="Second result", data={"label": "Label 2"}),
|
Data(text="Second result", data={"label": "Label 2"}),
|
||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
result = component.fetch_content_text()
|
result = component.fetch_content_text()
|
||||||
|
|
||||||
assert isinstance(result, Message)
|
assert isinstance(result, Message)
|
||||||
assert "First result" in result.text
|
assert "First result" in result.text
|
||||||
assert "Second result" in result.text
|
assert "Second result" in result.text
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue