Merge pull request #549 from Kingtous/main

feat: add Baidu, DuckDuckGo search tool and optional config
This commit is contained in:
mannaandpoem 2025-03-13 19:39:13 +08:00 committed by GitHub
commit c4d628cc4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
11 changed files with 103 additions and 12 deletions

View File

@ -7,8 +7,9 @@ from app.prompt.manus import NEXT_STEP_PROMPT, SYSTEM_PROMPT
from app.tool import Terminate, ToolCollection from app.tool import Terminate, ToolCollection
from app.tool.browser_use_tool import BrowserUseTool from app.tool.browser_use_tool import BrowserUseTool
from app.tool.file_saver import FileSaver 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.tool.python_execute import PythonExecute
from app.config import config
class Manus(ToolCallAgent): class Manus(ToolCallAgent):
@ -34,7 +35,7 @@ class Manus(ToolCallAgent):
# Add general-purpose tools to the tool collection # Add general-purpose tools to the tool collection
available_tools: ToolCollection = Field( available_tools: ToolCollection = Field(
default_factory=lambda: ToolCollection( default_factory=lambda: ToolCollection(
PythonExecute(), GoogleSearch(), BrowserUseTool(), FileSaver(), Terminate() PythonExecute(), WebSearch(), BrowserUseTool(), FileSaver(), Terminate()
) )
) )

View File

@ -30,6 +30,8 @@ class ProxySettings(BaseModel):
username: Optional[str] = Field(None, description="Proxy username") username: Optional[str] = Field(None, description="Proxy username")
password: Optional[str] = Field(None, description="Proxy password") 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): class BrowserSettings(BaseModel):
headless: bool = Field(False, description="Whether to run browser in headless mode") headless: bool = Field(False, description="Whether to run browser in headless mode")
@ -58,6 +60,9 @@ class AppConfig(BaseModel):
browser_config: Optional[BrowserSettings] = Field( browser_config: Optional[BrowserSettings] = Field(
None, description="Browser configuration" None, description="Browser configuration"
) )
search_config: Optional[SearchSettings] = Field(
None, description="Search configuration"
)
class Config: class Config:
arbitrary_types_allowed = True arbitrary_types_allowed = True
@ -149,6 +154,11 @@ class Config:
if valid_browser_params: if valid_browser_params:
browser_settings = BrowserSettings(**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 = { config_dict = {
"llm": { "llm": {
"default": default_settings, "default": default_settings,
@ -158,6 +168,7 @@ class Config:
}, },
}, },
"browser_config": browser_settings, "browser_config": browser_settings,
"search_config": search_settings,
} }
self._config = AppConfig(**config_dict) self._config = AppConfig(**config_dict)
@ -170,5 +181,9 @@ class Config:
def browser_config(self) -> Optional[BrowserSettings]: def browser_config(self) -> Optional[BrowserSettings]:
return self._config.browser_config return self._config.browser_config
@property
def search_config(self) -> Optional[SearchSettings]:
return self._config.search_config
config = Config() config = Config()

View File

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

View File

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

View File

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

15
app/tool/search/base.py Normal file
View File

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

View File

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

View File

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

View File

@ -1,14 +1,14 @@
import asyncio import asyncio
from typing import List from typing import List
from googlesearch import search
from app.tool.base import BaseTool from app.tool.base import BaseTool
from app.config import config
from app.tool.search import WebSearchEngine, BaiduSearchEngine, GoogleSearchEngine, DuckDuckGoSearchEngine
class GoogleSearch(BaseTool): class WebSearch(BaseTool):
name: str = "google_search" name: str = "web_search"
description: str = """Perform a Google search and return a list of relevant links. 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. 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. 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": { "properties": {
"query": { "query": {
"type": "string", "type": "string",
"description": "(required) The search query to submit to Google.", "description": "(required) The search query to submit to the search engine.",
}, },
"num_results": { "num_results": {
"type": "integer", "type": "integer",
@ -27,13 +27,18 @@ The tool returns a list of URLs that match the search query.
}, },
"required": ["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]: 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: 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. num_results (int, optional): The number of search results to return. Default is 10.
Returns: 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 # Run the search in a thread pool to prevent blocking
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
search_engine = self.get_search_engine()
links = await loop.run_in_executor( 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 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)

View File

@ -42,3 +42,8 @@ api_key = "sk-..."
# server = "http://proxy-server:port" # server = "http://proxy-server:port"
# username = "proxy-username" # username = "proxy-username"
# password = "proxy-password" # 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"

View File

@ -15,6 +15,8 @@ uvicorn~=0.34.0
unidiff~=0.7.5 unidiff~=0.7.5
browser-use~=0.1.40 browser-use~=0.1.40
googlesearch-python~=1.3.0 googlesearch-python~=1.3.0
baidusearch~=1.0.3
duckduckgo_search~=7.5.1
aiofiles~=24.1.0 aiofiles~=24.1.0
pydantic_core~=2.27.2 pydantic_core~=2.27.2