diff --git a/app/agent/manus.py b/app/agent/manus.py index e11ca45..fdf0a10 100644 --- a/app/agent/manus.py +++ b/app/agent/manus.py @@ -7,8 +7,9 @@ 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.web_search import WebSearch from app.tool.python_execute import PythonExecute +from app.config import config class Manus(ToolCallAgent): @@ -34,7 +35,7 @@ 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(), WebSearch(), BrowserUseTool(), FileSaver(), Terminate() ) ) diff --git a/app/config.py b/app/config.py index 64f478d..8fd8bd7 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,11 @@ 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) + config_dict = { "llm": { "default": default_settings, @@ -158,6 +168,7 @@ class Config: }, }, "browser_config": browser_settings, + "search_config": search_settings, } self._config = AppConfig(**config_dict) @@ -169,6 +180,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/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/google_search.py b/app/tool/web_search.py similarity index 50% rename from app/tool/google_search.py rename to app/tool/web_search.py index ed5d7d5..c661f3b 100644 --- a/app/tool/google_search.py +++ b/app/tool/web_search.py @@ -1,14 +1,14 @@ import asyncio from typing import List -from googlesearch import search - from app.tool.base import BaseTool +from app.config import config +from app.tool.search import WebSearchEngine, BaiduSearchEngine, GoogleSearchEngine, DuckDuckGoSearchEngine -class GoogleSearch(BaseTool): - name: str = "google_search" - description: str = """Perform a Google 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 +17,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 Google.", + "description": "(required) The search query to submit to the search engine.", }, "num_results": { "type": "integer", @@ -27,13 +27,18 @@ The tool returns a list of URLs that match the search query. }, "required": ["query"], } + _search_engine: dict[str, WebSearchEngine] = { + "google": GoogleSearchEngine(), + "baidu": BaiduSearchEngine(), + "duckduckgo": DuckDuckGoSearchEngine(), + } async def execute(self, query: str, num_results: int = 10) -> List[str]: """ - Execute a Google 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 Google. + 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 +46,18 @@ 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.perform_search(query, num_results=num_results)) ) return links + + 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 default_engine + else: + engine = config.search_config.engine.lower() + return self._search_engine.get(engine, default_engine) diff --git a/config/config.example.toml b/config/config.example.toml index 13648dd..d6c193a 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" or "DuckDuckGo". +#engine = "Google" \ No newline at end of file diff --git a/requirements.txt b/requirements.txt index 7ce4b52..60ad38e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -15,6 +15,8 @@ uvicorn~=0.34.0 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