From bbaff4f095b402fae3ffc659e8dfb7eb5d9b4c39 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 13 Mar 2025 00:27:48 +0800 Subject: [PATCH 1/6] feat: add baidu search tool and optional config --- app/agent/manus.py | 17 +++++++++++++- app/config.py | 16 +++++++++++++ app/tool/baidu_search.py | 48 ++++++++++++++++++++++++++++++++++++++ config/config.example.toml | 5 ++++ requirements.txt | 1 + 5 files changed, 86 insertions(+), 1 deletion(-) create mode 100644 app/tool/baidu_search.py diff --git a/app/agent/manus.py b/app/agent/manus.py index e11ca45..7cd012c 100644 --- a/app/agent/manus.py +++ b/app/agent/manus.py @@ -8,7 +8,9 @@ from app.tool import Terminate, ToolCollection from app.tool.browser_use_tool import BrowserUseTool from app.tool.file_saver import FileSaver from app.tool.google_search import GoogleSearch +from app.tool.baidu_search import BaiduSearch from app.tool.python_execute import PythonExecute +from app.config import config class Manus(ToolCallAgent): @@ -34,9 +36,22 @@ class Manus(ToolCallAgent): # Add general-purpose tools to the tool collection available_tools: ToolCollection = Field( default_factory=lambda: ToolCollection( - PythonExecute(), GoogleSearch(), BrowserUseTool(), FileSaver(), Terminate() + PythonExecute(), Manus.get_search_tool(), BrowserUseTool(), FileSaver(), Terminate() ) ) + + @staticmethod + def get_search_tool(): + """Determines the search tool to use based on the configuration.""" + print(config.search_config) + if config.search_config is None: + return GoogleSearch() + else: + # Check search engine + engine = config.search_config.engine.lower() + if engine == "baidu": + return BaiduSearch() + return GoogleSearch() async def _handle_special_tool(self, name: str, result: Any, **kwargs): await self.available_tools.get_tool(BrowserUseTool().name).cleanup() diff --git a/app/config.py b/app/config.py index 64f478d..81e1e81 100644 --- a/app/config.py +++ b/app/config.py @@ -30,6 +30,8 @@ class ProxySettings(BaseModel): username: Optional[str] = Field(None, description="Proxy username") password: Optional[str] = Field(None, description="Proxy password") +class SearchSettings(BaseModel): + engine: str = Field(default='Google', description="Search engine the llm to use") class BrowserSettings(BaseModel): headless: bool = Field(False, description="Whether to run browser in headless mode") @@ -58,6 +60,9 @@ class AppConfig(BaseModel): browser_config: Optional[BrowserSettings] = Field( None, description="Browser configuration" ) + search_config: Optional[SearchSettings] = Field( + None, description="Search configuration" + ) class Config: arbitrary_types_allowed = True @@ -149,6 +154,12 @@ class Config: if valid_browser_params: browser_settings = BrowserSettings(**valid_browser_params) + search_config = raw_config.get("search", {}) + search_settings = None + if search_config: + search_settings = SearchSettings(**search_config) + print("search setting", search_settings) + config_dict = { "llm": { "default": default_settings, @@ -158,6 +169,7 @@ class Config: }, }, "browser_config": browser_settings, + "search_config": search_settings, } self._config = AppConfig(**config_dict) @@ -169,6 +181,10 @@ class Config: @property def browser_config(self) -> Optional[BrowserSettings]: return self._config.browser_config + + @property + def search_config(self) -> Optional[SearchSettings]: + return self._config.search_config config = Config() diff --git a/app/tool/baidu_search.py b/app/tool/baidu_search.py new file mode 100644 index 0000000..93ba50f --- /dev/null +++ b/app/tool/baidu_search.py @@ -0,0 +1,48 @@ +import asyncio +from typing import List + +from baidusearch.baidusearch import search + +from app.tool.base import BaseTool + + +class BaiduSearch(BaseTool): + name: str = "baidu_search" + description: str = """Perform a Baidu search and return a list of relevant links. +Use this tool when you need to find information on the web, get up-to-date data, or research specific topics. +The tool returns a list of URLs that match the search query. +""" + parameters: dict = { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "(required) The search query to submit to Baidu.", + }, + "num_results": { + "type": "integer", + "description": "(optional) The number of search results to return. Default is 10.", + "default": 10, + }, + }, + "required": ["query"], + } + + async def execute(self, query: str, num_results: int = 10) -> List[str]: + """ + Execute a Baidu search and return a list of URLs. + + Args: + query (str): The search query to submit to Baidu. + num_results (int, optional): The number of search results to return. Default is 10. + + Returns: + List[str]: A list of URLs matching the search query. + """ + # Run the search in a thread pool to prevent blocking + loop = asyncio.get_event_loop() + links = await loop.run_in_executor( + None, lambda: list(search(query, num_results=num_results)) + ) + + return links diff --git a/config/config.example.toml b/config/config.example.toml index 13648dd..ac8af62 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -42,3 +42,8 @@ api_key = "sk-..." # server = "http://proxy-server:port" # username = "proxy-username" # password = "proxy-password" + +# Optional configuration, Search settings. +# [search] +# Search engine for agent to use. Default is "Google", can be set to "Baidu". +#engine = "Google" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7ce4b52..c275e65 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,7 @@ uvicorn~=0.34.0 unidiff~=0.7.5 browser-use~=0.1.40 googlesearch-python~=1.3.0 +baidusearch~=1.0.3 aiofiles~=24.1.0 pydantic_core~=2.27.2 From f9ce06adb8349af5d1c7d4126ea0e5dea1ac1876 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 13 Mar 2025 00:50:30 +0800 Subject: [PATCH 2/6] opt: remove unnessary print --- app/agent/manus.py | 1 - 1 file changed, 1 deletion(-) diff --git a/app/agent/manus.py b/app/agent/manus.py index 7cd012c..daac10e 100644 --- a/app/agent/manus.py +++ b/app/agent/manus.py @@ -43,7 +43,6 @@ class Manus(ToolCallAgent): @staticmethod def get_search_tool(): """Determines the search tool to use based on the configuration.""" - print(config.search_config) if config.search_config is None: return GoogleSearch() else: From b7774b18ef9db28fe578f224c9956740017041ac Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 13 Mar 2025 08:31:40 +0800 Subject: [PATCH 3/6] opt: abstract web search interface, code cleanup --- app/agent/manus.py | 17 +------- app/config.py | 1 - app/tool/google_search.py | 48 --------------------- app/tool/{baidu_search.py => web_search.py} | 31 +++++++++---- 4 files changed, 25 insertions(+), 72 deletions(-) delete mode 100644 app/tool/google_search.py rename app/tool/{baidu_search.py => web_search.py} (54%) diff --git a/app/agent/manus.py b/app/agent/manus.py index daac10e..fdf0a10 100644 --- a/app/agent/manus.py +++ b/app/agent/manus.py @@ -7,8 +7,7 @@ from app.prompt.manus import NEXT_STEP_PROMPT, SYSTEM_PROMPT from app.tool import Terminate, ToolCollection from app.tool.browser_use_tool import BrowserUseTool from app.tool.file_saver import FileSaver -from app.tool.google_search import GoogleSearch -from app.tool.baidu_search import BaiduSearch +from app.tool.web_search import WebSearch from app.tool.python_execute import PythonExecute from app.config import config @@ -36,21 +35,9 @@ class Manus(ToolCallAgent): # Add general-purpose tools to the tool collection available_tools: ToolCollection = Field( default_factory=lambda: ToolCollection( - PythonExecute(), Manus.get_search_tool(), BrowserUseTool(), FileSaver(), Terminate() + PythonExecute(), WebSearch(), BrowserUseTool(), FileSaver(), Terminate() ) ) - - @staticmethod - def get_search_tool(): - """Determines the search tool to use based on the configuration.""" - if config.search_config is None: - return GoogleSearch() - else: - # Check search engine - engine = config.search_config.engine.lower() - if engine == "baidu": - return BaiduSearch() - return GoogleSearch() async def _handle_special_tool(self, name: str, result: Any, **kwargs): await self.available_tools.get_tool(BrowserUseTool().name).cleanup() diff --git a/app/config.py b/app/config.py index 81e1e81..8fd8bd7 100644 --- a/app/config.py +++ b/app/config.py @@ -158,7 +158,6 @@ class Config: search_settings = None if search_config: search_settings = SearchSettings(**search_config) - print("search setting", search_settings) config_dict = { "llm": { diff --git a/app/tool/google_search.py b/app/tool/google_search.py deleted file mode 100644 index ed5d7d5..0000000 --- a/app/tool/google_search.py +++ /dev/null @@ -1,48 +0,0 @@ -import asyncio -from typing import List - -from googlesearch import search - -from app.tool.base import BaseTool - - -class GoogleSearch(BaseTool): - name: str = "google_search" - description: str = """Perform a Google search and return a list of relevant links. -Use this tool when you need to find information on the web, get up-to-date data, or research specific topics. -The tool returns a list of URLs that match the search query. -""" - parameters: dict = { - "type": "object", - "properties": { - "query": { - "type": "string", - "description": "(required) The search query to submit to Google.", - }, - "num_results": { - "type": "integer", - "description": "(optional) The number of search results to return. Default is 10.", - "default": 10, - }, - }, - "required": ["query"], - } - - async def execute(self, query: str, num_results: int = 10) -> List[str]: - """ - Execute a Google search and return a list of URLs. - - Args: - query (str): The search query to submit to Google. - num_results (int, optional): The number of search results to return. Default is 10. - - Returns: - List[str]: A list of URLs matching the search query. - """ - # Run the search in a thread pool to prevent blocking - loop = asyncio.get_event_loop() - links = await loop.run_in_executor( - None, lambda: list(search(query, num_results=num_results)) - ) - - return links diff --git a/app/tool/baidu_search.py b/app/tool/web_search.py similarity index 54% rename from app/tool/baidu_search.py rename to app/tool/web_search.py index 93ba50f..3beb4c4 100644 --- a/app/tool/baidu_search.py +++ b/app/tool/web_search.py @@ -1,14 +1,16 @@ import asyncio from typing import List -from baidusearch.baidusearch import search +from googlesearch import search as google_search +from baidusearch.baidusearch import search as baidu_search from app.tool.base import BaseTool +from app.config import config -class BaiduSearch(BaseTool): - name: str = "baidu_search" - description: str = """Perform a Baidu search and return a list of relevant links. +class WebSearch(BaseTool): + name: str = "web_search" + description: str = """Perform a web search and return a list of relevant links. Use this tool when you need to find information on the web, get up-to-date data, or research specific topics. The tool returns a list of URLs that match the search query. """ @@ -17,7 +19,7 @@ The tool returns a list of URLs that match the search query. "properties": { "query": { "type": "string", - "description": "(required) The search query to submit to Baidu.", + "description": "(required) The search query to submit to the search engine.", }, "num_results": { "type": "integer", @@ -27,13 +29,17 @@ The tool returns a list of URLs that match the search query. }, "required": ["query"], } + _search_engine: dict = { + "google": google_search, + "baidu": baidu_search, + } async def execute(self, query: str, num_results: int = 10) -> List[str]: """ - Execute a Baidu search and return a list of URLs. + Execute a Web search and return a list of URLs. Args: - query (str): The search query to submit to Baidu. + query (str): The search query to submit to the search engine. num_results (int, optional): The number of search results to return. Default is 10. Returns: @@ -41,8 +47,17 @@ The tool returns a list of URLs that match the search query. """ # Run the search in a thread pool to prevent blocking loop = asyncio.get_event_loop() + search_engine = self.get_search_engine() links = await loop.run_in_executor( - None, lambda: list(search(query, num_results=num_results)) + None, lambda: list(search_engine(query, num_results=num_results)) ) return links + + def get_search_engine(self): + """Determines the search engine to use based on the configuration.""" + if config.search_config is None: + return google_search + else: + engine = config.search_config.engine.lower() + return self._search_engine.get(engine, google_search) From 86d2a7d6bf921be089b2314e53db8c8569d3eab3 Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 13 Mar 2025 09:05:14 +0800 Subject: [PATCH 4/6] feat: implement duckduckgo search, abstract further --- app/prompt/manus.py | 2 +- app/tool/search/__init__.py | 12 ++++++++++++ app/tool/search/baidu_search.py | 9 +++++++++ app/tool/search/base.py | 15 +++++++++++++++ app/tool/search/duckduckgo_search.py | 9 +++++++++ app/tool/search/google_search.py | 8 ++++++++ app/tool/web_search.py | 20 ++++++++++---------- requirements.txt | 1 + 8 files changed, 65 insertions(+), 11 deletions(-) create mode 100644 app/tool/search/__init__.py create mode 100644 app/tool/search/baidu_search.py create mode 100644 app/tool/search/base.py create mode 100644 app/tool/search/duckduckgo_search.py create mode 100644 app/tool/search/google_search.py diff --git a/app/prompt/manus.py b/app/prompt/manus.py index e46c793..6dcca8a 100644 --- a/app/prompt/manus.py +++ b/app/prompt/manus.py @@ -8,7 +8,7 @@ FileSaver: Save files locally, such as txt, py, html, etc. BrowserUseTool: Open, browse, and use web browsers.If you open a local HTML file, you must provide the absolute path to the file. -GoogleSearch: Perform web information retrieval +WebSearch: Perform web information retrieval Terminate: End the current interaction when the task is complete or when you need additional information from the user. Use this tool to signal that you've finished addressing the user's request or need clarification before proceeding further. diff --git a/app/tool/search/__init__.py b/app/tool/search/__init__.py new file mode 100644 index 0000000..509d16d --- /dev/null +++ b/app/tool/search/__init__.py @@ -0,0 +1,12 @@ +from app.tool.search.base import WebSearchEngine +from app.tool.search.baidu_search import BaiduSearchEngine +from app.tool.search.duckduckgo_search import DuckDuckGoSearchEngine +from app.tool.search.google_search import GoogleSearchEngine + + +__all__ = [ + "WebSearchEngine", + "BaiduSearchEngine", + "DuckDuckGoSearchEngine", + "GoogleSearchEngine", +] \ No newline at end of file diff --git a/app/tool/search/baidu_search.py b/app/tool/search/baidu_search.py new file mode 100644 index 0000000..a398899 --- /dev/null +++ b/app/tool/search/baidu_search.py @@ -0,0 +1,9 @@ +from baidusearch.baidusearch import search +from app.tool.search.base import WebSearchEngine + + +class BaiduSearchEngine(WebSearchEngine): + + def perform_search(self, query, num_results = 10, *args, **kwargs): + """Baidu search engine.""" + return search(query, num_results=num_results) diff --git a/app/tool/search/base.py b/app/tool/search/base.py new file mode 100644 index 0000000..095c0b1 --- /dev/null +++ b/app/tool/search/base.py @@ -0,0 +1,15 @@ +class WebSearchEngine(object): + def perform_search(self, query: str, num_results: int = 10, *args, **kwargs) -> list[dict]: + """ + Perform a web search and return a list of URLs. + + Args: + query (str): The search query to submit to the search engine. + num_results (int, optional): The number of search results to return. Default is 10. + args: Additional arguments. + kwargs: Additional keyword arguments. + + Returns: + List: A list of dict matching the search query. + """ + raise NotImplementedError \ No newline at end of file diff --git a/app/tool/search/duckduckgo_search.py b/app/tool/search/duckduckgo_search.py new file mode 100644 index 0000000..738ecf5 --- /dev/null +++ b/app/tool/search/duckduckgo_search.py @@ -0,0 +1,9 @@ +from duckduckgo_search import DDGS +from app.tool.search.base import WebSearchEngine + + +class DuckDuckGoSearchEngine(WebSearchEngine): + + async def perform_search(self, query, num_results = 10, *args, **kwargs): + """DuckDuckGo search engine.""" + return DDGS.text(query, num_results=num_results) diff --git a/app/tool/search/google_search.py b/app/tool/search/google_search.py new file mode 100644 index 0000000..606f107 --- /dev/null +++ b/app/tool/search/google_search.py @@ -0,0 +1,8 @@ +from app.tool.search.base import WebSearchEngine +from googlesearch import search + +class GoogleSearchEngine(WebSearchEngine): + + def perform_search(self, query, num_results = 10, *args, **kwargs): + """Google search engine.""" + return search(query, num_results=num_results) diff --git a/app/tool/web_search.py b/app/tool/web_search.py index 3beb4c4..9f55199 100644 --- a/app/tool/web_search.py +++ b/app/tool/web_search.py @@ -1,11 +1,9 @@ import asyncio from typing import List -from googlesearch import search as google_search -from baidusearch.baidusearch import search as baidu_search - from app.tool.base import BaseTool from app.config import config +from app.tool.search import WebSearchEngine, BaiduSearchEngine, GoogleSearchEngine, DuckDuckGoSearchEngine class WebSearch(BaseTool): @@ -29,9 +27,10 @@ The tool returns a list of URLs that match the search query. }, "required": ["query"], } - _search_engine: dict = { - "google": google_search, - "baidu": baidu_search, + _search_engine: dict[str, WebSearchEngine] = { + "google": GoogleSearchEngine(), + "baidu": BaiduSearchEngine(), + "duckduckgo": DuckDuckGoSearchEngine(), } async def execute(self, query: str, num_results: int = 10) -> List[str]: @@ -53,11 +52,12 @@ The tool returns a list of URLs that match the search query. ) return links - - def get_search_engine(self): + + def get_search_engine(self) -> WebSearchEngine: """Determines the search engine to use based on the configuration.""" + default_engine = self._search_engine.get("google") if config.search_config is None: - return google_search + return default_engine else: engine = config.search_config.engine.lower() - return self._search_engine.get(engine, google_search) + return self._search_engine.get(engine, default_engine) diff --git a/requirements.txt b/requirements.txt index c275e65..60ad38e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,6 +16,7 @@ unidiff~=0.7.5 browser-use~=0.1.40 googlesearch-python~=1.3.0 baidusearch~=1.0.3 +duckduckgo_search~=7.5.1 aiofiles~=24.1.0 pydantic_core~=2.27.2 From 2b9ef4ea08db083e906549cef9b314ddcd923f5e Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 13 Mar 2025 09:10:14 +0800 Subject: [PATCH 5/6] fix: perform search on query --- app/tool/web_search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app/tool/web_search.py b/app/tool/web_search.py index 9f55199..c661f3b 100644 --- a/app/tool/web_search.py +++ b/app/tool/web_search.py @@ -48,7 +48,7 @@ The tool returns a list of URLs that match the search query. loop = asyncio.get_event_loop() search_engine = self.get_search_engine() links = await loop.run_in_executor( - None, lambda: list(search_engine(query, num_results=num_results)) + None, lambda: list(search_engine.perform_search(query, num_results=num_results)) ) return links From 198f70d5246930657f7e35649c98654e2d1328ad Mon Sep 17 00:00:00 2001 From: Kingtous Date: Thu, 13 Mar 2025 09:11:20 +0800 Subject: [PATCH 6/6] opt: update config.example.json --- config/config.example.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/config.example.toml b/config/config.example.toml index ac8af62..d6c193a 100644 --- a/config/config.example.toml +++ b/config/config.example.toml @@ -45,5 +45,5 @@ api_key = "sk-..." # Optional configuration, Search settings. # [search] -# Search engine for agent to use. Default is "Google", can be set to "Baidu". +# Search engine for agent to use. Default is "Google", can be set to "Baidu" or "DuckDuckGo". #engine = "Google" \ No newline at end of file