feat: add duckduckgo search component (#3798)
* 🔧 (pyproject.toml): add duckduckgo-search dependency to the project ✨ (DuckDuckGoSearchRun.py): introduce DuckDuckGoSearchComponent for performing web searches using DuckDuckGo search engine ✨ (DuckDuckGo.jsx): add SVG icon for DuckDuckGo ✨ (index.tsx): create DuckDuckGoIcon component for displaying DuckDuckGo icon 🔧 (styleUtils.ts): import DuckDuckGoIcon for nodeIconsLucide in styleUtils * 📝 (DuckDuckGoSearchRun.py): remove unnecessary whitespace to improve code readability and consistency * ✨ (DuckDuckGoSearchRun.py): Add retry logic to DuckDuckGo search component for rate-limited requests 📝 (DuckDuckGoSearchRun.py): Update component description to reflect the addition of retry logic 📝 (DuckDuckGoSearchRun.py): Add new inputs for max_retries and initial_delay to configure retry behavior 📝 (DuckDuckGoSearchRun.py): Update search_response method to use search_with_retry method with retry logic 📝 (DuckDuckGoSearchRun.py): Update format_results method to handle formatted results 📝 (DuckDuckGoSearchRun.py): Add search_with_retry method to handle search with retry logic 📝 (DuckDuckGoSearchRun.py): Update search_response method to use search_with_retry method 📝 (DuckDuckGoSearchRun.py): Update search_response method to set status messages 📝 (DuckDuckGoSearchRun.py): Handle exceptions and set appropriate status messages in search_response method ✨ (duckduckgo.spec.ts): Add integration test for DuckDuckGo search component in frontend * 📝 (DuckDuckGoSearchRun.py): add newline at the end of the file to follow best practices and avoid potential issues with some tools that expect it * [autofix.ci] apply automated fixes * updating duckudckgo * [autofix.ci] apply automated fixes * ✨ (DuckDuckGoSearchRun.py): Refactor DuckDuckGoSearchComponent to use pydantic BaseModel for schema definition and improve code structure for better readability and maintainability. Add support for result limiting in search functionality. * 🔧 (DuckDuckGoSearchRun.py): Remove unnecessary import and update status message for DuckDuckGo Search Tool to improve clarity * [autofix.ci] apply automated fixes * ✨ (duckduckgo.spec.ts): update test selectors for duckduckgo search component to match changes in the frontend code and improve test reliability * 🐛 (linkComponent.spec.ts): fix an issue where the key combination for selecting all text was not working correctly on Mac devices. Updated the key combination to use the correct modifier key based on the user's operating system. * 📝 (frontend): mark is-unicode-supported package as extraneous in package-lock.json * rollback lock file --------- Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
parent
50f9f72f5f
commit
8c8be151e5
9 changed files with 253 additions and 2 deletions
41
poetry.lock
generated
41
poetry.lock
generated
|
|
@ -2079,6 +2079,25 @@ files = [
|
|||
{file = "duckdb-1.1.0.tar.gz", hash = "sha256:b4d4c12b1f98732151bd31377753e0da1a20f6423016d2d097d2e31953ec7c23"},
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "duckduckgo-search"
|
||||
version = "6.2.11"
|
||||
description = "Search for words, documents, images, news, maps and text translation using the DuckDuckGo.com search engine."
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "duckduckgo_search-6.2.11-py3-none-any.whl", hash = "sha256:6fb7069b79e8928f487001de6859034ade19201bdcd257ec198802430e374bfe"},
|
||||
{file = "duckduckgo_search-6.2.11.tar.gz", hash = "sha256:6b6ef1b552c5e67f23e252025d2504caf6f9fc14f70e86c6dd512200f386c673"},
|
||||
]
|
||||
|
||||
[package.dependencies]
|
||||
click = ">=8.1.7"
|
||||
primp = ">=0.6.1"
|
||||
|
||||
[package.extras]
|
||||
dev = ["mypy (>=1.11.1)", "pytest (>=8.3.1)", "pytest-asyncio (>=0.23.8)", "ruff (>=0.6.1)"]
|
||||
lxml = ["lxml (>=5.2.2)"]
|
||||
|
||||
[[package]]
|
||||
name = "e2b"
|
||||
version = "0.17.1"
|
||||
|
|
@ -7248,6 +7267,26 @@ nodeenv = ">=0.11.1"
|
|||
pyyaml = ">=5.1"
|
||||
virtualenv = ">=20.10.0"
|
||||
|
||||
[[package]]
|
||||
name = "primp"
|
||||
version = "0.6.1"
|
||||
description = "HTTP client that can impersonate web browsers, mimicking their headers and `TLS/JA3/JA4/HTTP2` fingerprints"
|
||||
optional = false
|
||||
python-versions = ">=3.8"
|
||||
files = [
|
||||
{file = "primp-0.6.1-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:60cfe95e0bdf154b0f9036d38acaddc9aef02d6723ed125839b01449672d3946"},
|
||||
{file = "primp-0.6.1-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:e1e92433ecf32639f9e800bc3a5d58b03792bdec99421b7fb06500e2fae63c85"},
|
||||
{file = "primp-0.6.1-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6e02353f13f07fb5a6f91df9e2f4d8ec9f41312de95088744dce1c9729a3865d"},
|
||||
{file = "primp-0.6.1-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:c5a2ccfdf488b17be225a529a31e2b22724b2e22fba8e1ae168a222f857c2dc0"},
|
||||
{file = "primp-0.6.1-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:f335c2ace907800a23bbb7bc6e15acc7fff659b86a2d5858817f6ed79cea07cf"},
|
||||
{file = "primp-0.6.1-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:5dc15bd9d47ded7bc356fcb5d8321972dcbeba18e7d3b7250e12bb7365447b2b"},
|
||||
{file = "primp-0.6.1-cp38-abi3-win_amd64.whl", hash = "sha256:eebf0412ebba4089547b16b97b765d83f69f1433d811bb02b02cdcdbca20f672"},
|
||||
{file = "primp-0.6.1.tar.gz", hash = "sha256:64b3c12e3d463a887518811c46f3ec37cca02e6af1ddf1287e548342de436301"},
|
||||
]
|
||||
|
||||
[package.extras]
|
||||
dev = ["certifi", "pytest (>=8.1.1)"]
|
||||
|
||||
[[package]]
|
||||
name = "prometheus-client"
|
||||
version = "0.20.0"
|
||||
|
|
@ -12114,4 +12153,4 @@ local = ["ctransformers", "llama-cpp-python", "sentence-transformers"]
|
|||
[metadata]
|
||||
lock-version = "2.0"
|
||||
python-versions = ">=3.10,<3.13"
|
||||
content-hash = "64cce31a5fc6e0dbb35aebf34f34bf437469f213f9059be6ac31f7877a02cbea"
|
||||
content-hash = "304221b658b52d3de30e52ebbc45b4730832fb087ce4f15b68c858d0a5af5efb"
|
||||
|
|
|
|||
|
|
@ -112,6 +112,7 @@ clickhouse-connect = {version = "0.7.19", optional = true, extras = ["clickhouse
|
|||
langchain-unstructured = "^0.1.2"
|
||||
pydantic-settings = "2.4.0"
|
||||
ragstack-ai-knowledge-store = "^0.2.1"
|
||||
duckduckgo-search = "^6.2.11"
|
||||
|
||||
|
||||
[tool.poetry.group.dev.dependencies]
|
||||
|
|
|
|||
|
|
@ -0,0 +1,69 @@
|
|||
from typing import Dict, Any, List
|
||||
from pydantic import BaseModel, Field
|
||||
from langchain_community.tools import DuckDuckGoSearchRun
|
||||
from langflow.base.langchain_utilities.model import LCToolComponent
|
||||
from langflow.inputs import MessageTextInput, IntInput
|
||||
from langflow.schema import Data
|
||||
from langflow.field_typing import Tool
|
||||
from langchain.tools import StructuredTool
|
||||
|
||||
|
||||
class DuckDuckGoSearchComponent(LCToolComponent):
|
||||
display_name: str = "DuckDuckGo Search"
|
||||
description: str = "Perform web searches using the DuckDuckGo search engine with result limiting"
|
||||
name = "DuckDuckGoSearch"
|
||||
documentation: str = "https://python.langchain.com/docs/integrations/tools/ddg"
|
||||
icon: str = "DuckDuckGo"
|
||||
inputs = [
|
||||
MessageTextInput(
|
||||
name="input_value",
|
||||
display_name="Search Query",
|
||||
required=True,
|
||||
),
|
||||
IntInput(name="max_results", display_name="Max Results", value=5, advanced=True),
|
||||
IntInput(name="max_snippet_length", display_name="Max Snippet Length", value=100, advanced=True),
|
||||
]
|
||||
|
||||
class DuckDuckGoSearchSchema(BaseModel):
|
||||
query: str = Field(..., description="The search query")
|
||||
max_results: int = Field(5, description="Maximum number of results to return")
|
||||
max_snippet_length: int = Field(100, description="Maximum length of each result snippet")
|
||||
|
||||
def _build_wrapper(self):
|
||||
return DuckDuckGoSearchRun()
|
||||
|
||||
def build_tool(self) -> Tool:
|
||||
wrapper = self._build_wrapper()
|
||||
|
||||
def search_func(query: str, max_results: int = 5, max_snippet_length: int = 100) -> List[Dict[str, Any]]:
|
||||
full_results = wrapper.run(f"{query} (site:*)")
|
||||
result_list = full_results.split("\n")[:max_results]
|
||||
limited_results = []
|
||||
for result in result_list:
|
||||
limited_result = {
|
||||
"snippet": result[:max_snippet_length],
|
||||
}
|
||||
limited_results.append(limited_result)
|
||||
return limited_results
|
||||
|
||||
tool = StructuredTool.from_function(
|
||||
name="duckduckgo_search",
|
||||
description="Search for recent results using DuckDuckGo with result limiting",
|
||||
func=search_func,
|
||||
args_schema=self.DuckDuckGoSearchSchema,
|
||||
)
|
||||
self.status = "DuckDuckGo Search Tool created"
|
||||
return tool
|
||||
|
||||
def run_model(self) -> List[Data]:
|
||||
tool = self.build_tool()
|
||||
results = tool.run(
|
||||
{
|
||||
"query": self.input_value,
|
||||
"max_results": self.max_results,
|
||||
"max_snippet_length": self.max_snippet_length,
|
||||
}
|
||||
)
|
||||
data_list = [Data(data=result, text=result.get("snippet", "")) for result in results]
|
||||
self.status = data_list
|
||||
return data_list
|
||||
59
src/frontend/src/icons/DuckDuckGo/DuckDuckGo.jsx
Normal file
59
src/frontend/src/icons/DuckDuckGo/DuckDuckGo.jsx
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
const SvgDuckDuckGo = ({ ...props }) => (
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 122.88 122.88"
|
||||
{...props}
|
||||
height="1rem"
|
||||
width="1rem"
|
||||
>
|
||||
<defs>
|
||||
<style>{".b{fill:#fff}"}</style>
|
||||
</defs>
|
||||
<title>{"duckduckgo"}</title>
|
||||
<path
|
||||
d="M122.88 61.44a61.44 61.44 0 1 0-61.44 61.44 61.44 61.44 0 0 0 61.44-61.44Z"
|
||||
style={{
|
||||
fill: "#d53",
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M114.37 61.44a52.92 52.92 0 1 0-15.5 37.43 52.76 52.76 0 0 0 15.5-37.43Zm-13.12-39.8A56.29 56.29 0 1 1 61.44 5.15a56.12 56.12 0 0 1 39.81 16.49Z"
|
||||
className="b"
|
||||
/>
|
||||
<path
|
||||
d="M43.24 30.15C26.17 34.13 32.43 58 32.43 58l10.81 52.9 4 1.71-4-82.49Zm-4-10.24H34.7l6.3 2.28s-6.26 0-6.26 4C48.36 25.6 54.61 29 54.61 29l-15.36-9.1Zm0 0Z"
|
||||
style={{
|
||||
fill: "#ddd",
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M75.66 115.48S62 93.87 62 79.64c0-26.73 17.63-4 17.63-25S62 28.44 62 28.44c-8.53-10.8-25-8.53-25-8.53l4 2.28s-4 1.13-5.12 2.27 10.81-1.7 15.93 2.85C30.72 29 34.13 46.08 34.13 46.08l11.95 68.27 29.58 1.13Zm0 0Z"
|
||||
className="b"
|
||||
/>
|
||||
<path
|
||||
d="m75.66 60.87 21.62-5.69c19.34 2.82-16.5 13.66-18.77 13.09-17.07-2.85-12 11.37 8.53 6.82s5.12 11.38-13.65 5.12c-26.74-7.39-12.52-20.48 2.27-19.34Z"
|
||||
style={{
|
||||
fill: "#fc0",
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="m70 105.81 1.14-1.7c12.52 4.55 13.09 6.25 12.52-5.12s0-11.38-13.09-1.71c0-2.84-7.39-1.71-8.53 0-11.95-5.12-13.09-6.83-12.52 1.14 1.14 16.5.57 13.65 11.95 8l8.53-.57Zm0 0Z"
|
||||
style={{
|
||||
fill: "#6b5",
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M60.87 99.56v6.82c.57 1.14 9.67 1.14 9.67-1.14s-4.55 1.71-7.39.57S62 98.42 62 98.42l-1.14 1.14Zm0 0Z"
|
||||
style={{
|
||||
fill: "#4a4",
|
||||
}}
|
||||
/>
|
||||
<path
|
||||
d="M48.36 43.24c-2.85-3.42-10.24-.57-8.54 4 .57-2.28 4.55-5.69 8.54-4Zm18.2 0c.57-3.42 6.26-4 8-.57a8 8 0 0 0-8 .57Zm-18.77 9.1a1.14 1.14 0 1 1 0 .57v-.57Zm-4.55 2.27a4 4 0 1 0 0-.57v.57Zm29.58-4a1.14 1.14 0 1 1 0 .57v-.57Zm-3.42 2.3a3.42 3.42 0 1 0 0-.57v.57Zm0 0Z"
|
||||
style={{
|
||||
fill: "#148",
|
||||
}}
|
||||
/>
|
||||
</svg>
|
||||
);
|
||||
export default SvgDuckDuckGo;
|
||||
1
src/frontend/src/icons/DuckDuckGo/duckduckgo-icon.svg
Normal file
1
src/frontend/src/icons/DuckDuckGo/duckduckgo-icon.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 122.88 122.88"><defs><style>.a{fill:#d53;}.b{fill:#fff;}.c{fill:#ddd;}.d{fill:#fc0;}.e{fill:#6b5;}.f{fill:#4a4;}.g{fill:#148;}</style></defs><title>duckduckgo</title><path class="a" d="M122.88,61.44a61.44,61.44,0,1,0-61.44,61.44,61.44,61.44,0,0,0,61.44-61.44Z"/><path class="b" d="M114.37,61.44a52.92,52.92,0,1,0-15.5,37.43,52.76,52.76,0,0,0,15.5-37.43Zm-13.12-39.8A56.29,56.29,0,1,1,61.44,5.15a56.12,56.12,0,0,1,39.81,16.49Z"/><path class="c" d="M43.24,30.15C26.17,34.13,32.43,58,32.43,58l10.81,52.9,4,1.71-4-82.49Zm-4-10.24H34.7L41,22.19s-6.26,0-6.26,4C48.36,25.6,54.61,29,54.61,29l-15.36-9.1Zm0,0Z"/><path class="b" d="M75.66,115.48S62,93.87,62,79.64c0-26.73,17.63-4,17.63-25S62,28.44,62,28.44c-8.53-10.8-25-8.53-25-8.53l4,2.28s-4,1.13-5.12,2.27,10.81-1.7,15.93,2.85C30.72,29,34.13,46.08,34.13,46.08l11.95,68.27,29.58,1.13Zm0,0Z"/><path class="d" d="M75.66,60.87l21.62-5.69C116.62,58,80.78,68.84,78.51,68.27c-17.07-2.85-12,11.37,8.53,6.82s5.12,11.38-13.65,5.12c-26.74-7.39-12.52-20.48,2.27-19.34Z"/><path class="e" d="M70,105.81l1.14-1.7c12.52,4.55,13.09,6.25,12.52-5.12s0-11.38-13.09-1.71c0-2.84-7.39-1.71-8.53,0-11.95-5.12-13.09-6.83-12.52,1.14,1.14,16.5.57,13.65,11.95,8l8.53-.57Zm0,0Z"/><path class="f" d="M60.87,99.56v6.82c.57,1.14,9.67,1.14,9.67-1.14s-4.55,1.71-7.39.57S62,98.42,62,98.42l-1.14,1.14Zm0,0Z"/><path class="g" d="M48.36,43.24c-2.85-3.42-10.24-.57-8.54,4,.57-2.28,4.55-5.69,8.54-4Zm18.2,0c.57-3.42,6.26-4,8-.57a8,8,0,0,0-8,.57Zm-18.77,9.1a1.14,1.14,0,1,1,0,.57v-.57Zm-4.55,2.27a4,4,0,1,0,0-.57v.57Zm29.58-4a1.14,1.14,0,1,1,0,.57v-.57ZM69.4,52.91a3.42,3.42,0,1,0,0-.57v.57Zm0,0Z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.6 KiB |
9
src/frontend/src/icons/DuckDuckGo/index.tsx
Normal file
9
src/frontend/src/icons/DuckDuckGo/index.tsx
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
import React, { forwardRef } from "react";
|
||||
import SvgDuckDuckGo from "./DuckDuckGo";
|
||||
|
||||
export const DuckDuckGoIcon = forwardRef<
|
||||
SVGSVGElement,
|
||||
React.PropsWithChildren<{ color?: string }>
|
||||
>((props, ref) => {
|
||||
return <SvgDuckDuckGo ref={ref} {...props} />;
|
||||
});
|
||||
|
|
@ -1,4 +1,5 @@
|
|||
import { AIMLIcon } from "@/icons/AIML";
|
||||
import { DuckDuckGoIcon } from "@/icons/DuckDuckGo";
|
||||
import Perplexity from "@/icons/Perplexity/Perplexity";
|
||||
import { UnstructuredIcon } from "@/icons/Unstructured";
|
||||
import { AthenaIcon } from "@/icons/athena/index";
|
||||
|
|
@ -627,4 +628,5 @@ export const nodeIconsLucide: iconsType = {
|
|||
OptionIcon: OptionIcon,
|
||||
Option: OptionIcon,
|
||||
Perplexity,
|
||||
DuckDuckGo: DuckDuckGoIcon,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -31,6 +31,10 @@ test("user should interact with link component", async ({ context, page }) => {
|
|||
const userAgentInfo = uaParser(getUA);
|
||||
let control = "Control";
|
||||
|
||||
if (userAgentInfo.os.name.includes("Mac")) {
|
||||
control = "Meta";
|
||||
}
|
||||
|
||||
await page.waitForSelector('[data-testid="blank-flow"]', {
|
||||
timeout: 30000,
|
||||
});
|
||||
|
|
@ -80,7 +84,7 @@ test("user should interact with link component", async ({ context, page }) => {
|
|||
LinkInput(name="link", display_name="BUTTON", value="https://www.datastax.com", text="Click me"),`,
|
||||
);
|
||||
|
||||
await page.locator("textarea").last().press(`Meta+a`);
|
||||
await page.locator("textarea").last().press(`${control}+a`);
|
||||
await page.keyboard.press("Backspace");
|
||||
await page.locator("textarea").last().fill(cleanCode);
|
||||
await page.locator('//*[@id="checkAndSaveBtn"]').click();
|
||||
|
|
|
|||
67
src/frontend/tests/extended/integrations/duckduckgo.spec.ts
Normal file
67
src/frontend/tests/extended/integrations/duckduckgo.spec.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import { expect, test } from "@playwright/test";
|
||||
|
||||
test("user should be able to use duckduckgo search component", async ({
|
||||
page,
|
||||
}) => {
|
||||
await page.goto("/");
|
||||
await page.waitForSelector('[data-testid="mainpage_title"]', {
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
await page.waitForSelector('[id="new-project-btn"]', {
|
||||
timeout: 30000,
|
||||
});
|
||||
|
||||
let modalCount = 0;
|
||||
try {
|
||||
const modalTitleElement = await page?.getByTestId("modal-title");
|
||||
if (modalTitleElement) {
|
||||
modalCount = await modalTitleElement.count();
|
||||
}
|
||||
} catch (error) {
|
||||
modalCount = 0;
|
||||
}
|
||||
|
||||
while (modalCount === 0) {
|
||||
await page.getByText("New Project", { exact: true }).click();
|
||||
await page.waitForTimeout(3000);
|
||||
modalCount = await page.getByTestId("modal-title")?.count();
|
||||
}
|
||||
|
||||
await page.getByTestId("blank-flow").click();
|
||||
await page.waitForSelector('[data-testid="extended-disclosure"]', {
|
||||
timeout: 30000,
|
||||
});
|
||||
await page.getByTestId("extended-disclosure").click();
|
||||
await page.getByPlaceholder("Search").click();
|
||||
await page.getByPlaceholder("Search").fill("duck");
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page
|
||||
.locator('//*[@id="toolsDuckDuckGo Search"]')
|
||||
.dragTo(page.locator('//*[@id="react-flow-id"]'));
|
||||
await page.mouse.up();
|
||||
await page.mouse.down();
|
||||
await page.getByTitle("fit view").click();
|
||||
|
||||
await page
|
||||
.getByTestId("popover-anchor-input-input_value")
|
||||
.fill("what is langflow?");
|
||||
|
||||
await page.getByTestId("button_run_duckduckgo search").click();
|
||||
|
||||
await page.getByTitle("fit view").click();
|
||||
|
||||
await page.waitForSelector("text=built successfully", { timeout: 30000 });
|
||||
|
||||
await page.waitForTimeout(1000);
|
||||
|
||||
await page.getByTestId("output-inspection-data").first().click();
|
||||
|
||||
await page.getByRole("gridcell").first().click();
|
||||
|
||||
const searchResults = await page.getByPlaceholder("Empty").inputValue();
|
||||
expect(searchResults.length).toBeGreaterThan(10);
|
||||
expect(searchResults.toLowerCase()).toContain("langflow");
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue