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:
Edwin Jose 2025-01-22 13:30:01 -05:00 • committed by GitHub
commit 229ba19a66
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 252 additions and 136 deletions

View file

@ -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",

View 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)

View file

@ -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)

View file

@ -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