| import os |
| from typing import List, Optional, Tuple, Type, Union |
|
|
| import aiohttp |
| import requests |
|
|
| from lagent.schema import ActionReturn, ActionStatusCode |
| from .base_action import AsyncActionMixin, BaseAction, tool_api |
| from .parser import BaseParser, JsonParser |
|
|
|
|
| class GoogleSearch(BaseAction): |
| """Wrapper around the Serper.dev Google Search API. |
| |
| To use, you should pass your serper API key to the constructor. |
| |
| Code is modified from lang-chain GoogleSerperAPIWrapper |
| (https://github.com/langchain-ai/langchain/blob/ba5f |
| baba704a2d729a4b8f568ed70d7c53e799bb/libs/langchain/ |
| langchain/utilities/google_serper.py) |
| |
| Args: |
| api_key (str): API KEY to use serper google search API, |
| You can create a free API key at https://serper.dev. |
| timeout (int): Upper bound of waiting time for a serper request. |
| search_type (str): Serper API support ['search', 'images', 'news', |
| 'places'] types of search, currently we only support 'search'. |
| description (dict): The description of the action. Defaults to ``None``. |
| parser (Type[BaseParser]): The parser class to process the |
| action's inputs and outputs. Defaults to :class:`JsonParser`. |
| """ |
| result_key_for_type = { |
| 'news': 'news', |
| 'places': 'places', |
| 'images': 'images', |
| 'search': 'organic', |
| } |
|
|
| def __init__( |
| self, |
| api_key: Optional[str] = None, |
| timeout: int = 5, |
| search_type: str = 'search', |
| description: Optional[dict] = None, |
| parser: Type[BaseParser] = JsonParser, |
| ): |
| super().__init__(description, parser) |
| api_key = os.environ.get('SERPER_API_KEY', api_key) |
| if api_key is None: |
| raise ValueError( |
| 'Please set Serper API key either in the environment ' |
| 'as SERPER_API_KEY or pass it as `api_key` parameter.') |
| self.api_key = api_key |
| self.timeout = timeout |
| self.search_type = search_type |
|
|
| @tool_api |
| def run(self, query: str, k: int = 10) -> ActionReturn: |
| """一个可以从谷歌搜索结果的API。当你需要对于一个特定问题找到简短明了的回答时,可以使用它。输入应该是一个搜索查询。 |
| |
| Args: |
| query (str): the search content |
| k (int): select first k results in the search results as response |
| """ |
| tool_return = ActionReturn(type=self.name) |
| status_code, response = self._search(query, k=k) |
| |
| if status_code == -1: |
| tool_return.errmsg = response |
| tool_return.state = ActionStatusCode.HTTP_ERROR |
| elif status_code == 200: |
| parsed_res = self._parse_results(response, k) |
| tool_return.result = [dict(type='text', content=str(parsed_res))] |
| tool_return.state = ActionStatusCode.SUCCESS |
| else: |
| tool_return.errmsg = str(status_code) |
| tool_return.state = ActionStatusCode.API_ERROR |
| return tool_return |
|
|
| def _parse_results(self, results: dict, k: int) -> Union[str, List[str]]: |
| """Parse the search results from Serper API. |
| |
| Args: |
| results (dict): The search content from Serper API |
| in json format. |
| |
| Returns: |
| List[str]: The parsed search results. |
| """ |
|
|
| snippets = [] |
|
|
| if results.get('answerBox'): |
| answer_box = results.get('answerBox', {}) |
| if answer_box.get('answer'): |
| return [answer_box.get('answer')] |
| elif answer_box.get('snippet'): |
| return [answer_box.get('snippet').replace('\n', ' ')] |
| elif answer_box.get('snippetHighlighted'): |
| return answer_box.get('snippetHighlighted') |
|
|
| if results.get('knowledgeGraph'): |
| kg = results.get('knowledgeGraph', {}) |
| title = kg.get('title') |
| entity_type = kg.get('type') |
| if entity_type: |
| snippets.append(f'{title}: {entity_type}.') |
| description = kg.get('description') |
| if description: |
| snippets.append(description) |
| for attribute, value in kg.get('attributes', {}).items(): |
| snippets.append(f'{title} {attribute}: {value}.') |
|
|
| for result in results[self.result_key_for_type[ |
| self.search_type]][:k]: |
| if 'snippet' in result: |
| snippets.append(result['snippet']) |
| for attribute, value in result.get('attributes', {}).items(): |
| snippets.append(f'{attribute}: {value}.') |
|
|
| if len(snippets) == 0: |
| return ['No good Google Search Result was found'] |
| return snippets |
|
|
| def _search(self, |
| search_term: str, |
| search_type: Optional[str] = None, |
| **kwargs) -> Tuple[int, Union[dict, str]]: |
| """HTTP requests to Serper API. |
| |
| Args: |
| search_term (str): The search query. |
| search_type (str): search type supported by Serper API, |
| default to 'search'. |
| |
| Returns: |
| tuple: the return value is a tuple contains: |
| - status_code (int): HTTP status code from Serper API. |
| - response (dict): response context with json format. |
| """ |
| headers = { |
| 'X-API-KEY': self.api_key or '', |
| 'Content-Type': 'application/json', |
| } |
| params = { |
| 'q': search_term, |
| **{ |
| key: value |
| for key, value in kwargs.items() if value is not None |
| }, |
| } |
| try: |
| response = requests.post( |
| f'https://google.serper.dev/{search_type or self.search_type}', |
| headers=headers, |
| params=params, |
| timeout=self.timeout) |
| except Exception as e: |
| return -1, str(e) |
| return response.status_code, response.json() |
|
|
|
|
| class AsyncGoogleSearch(AsyncActionMixin, GoogleSearch): |
| """Wrapper around the Serper.dev Google Search API. |
| |
| To use, you should pass your serper API key to the constructor. |
| |
| Code is modified from lang-chain GoogleSerperAPIWrapper |
| (https://github.com/langchain-ai/langchain/blob/ba5f |
| baba704a2d729a4b8f568ed70d7c53e799bb/libs/langchain/ |
| langchain/utilities/google_serper.py) |
| |
| Args: |
| api_key (str): API KEY to use serper google search API, |
| You can create a free API key at https://serper.dev. |
| timeout (int): Upper bound of waiting time for a serper request. |
| search_type (str): Serper API support ['search', 'images', 'news', |
| 'places'] types of search, currently we only support 'search'. |
| description (dict): The description of the action. Defaults to ``None``. |
| parser (Type[BaseParser]): The parser class to process the |
| action's inputs and outputs. Defaults to :class:`JsonParser`. |
| """ |
|
|
| @tool_api |
| async def run(self, query: str, k: int = 10) -> ActionReturn: |
| """一个可以从谷歌搜索结果的API。当你需要对于一个特定问题找到简短明了的回答时,可以使用它。输入应该是一个搜索查询。 |
| |
| Args: |
| query (str): the search content |
| k (int): select first k results in the search results as response |
| """ |
| tool_return = ActionReturn(type=self.name) |
| status_code, response = await self._search(query, k=k) |
| |
| if status_code == -1: |
| tool_return.errmsg = response |
| tool_return.state = ActionStatusCode.HTTP_ERROR |
| elif status_code == 200: |
| parsed_res = self._parse_results(response) |
| tool_return.result = [dict(type='text', content=str(parsed_res))] |
| tool_return.state = ActionStatusCode.SUCCESS |
| else: |
| tool_return.errmsg = str(status_code) |
| tool_return.state = ActionStatusCode.API_ERROR |
| return tool_return |
|
|
| async def _search(self, |
| search_term: str, |
| search_type: Optional[str] = None, |
| **kwargs) -> Tuple[int, Union[dict, str]]: |
| """HTTP requests to Serper API. |
| |
| Args: |
| search_term (str): The search query. |
| search_type (str): search type supported by Serper API, |
| default to 'search'. |
| |
| Returns: |
| tuple: the return value is a tuple contains: |
| - status_code (int): HTTP status code from Serper API. |
| - response (dict): response context with json format. |
| """ |
| headers = { |
| 'X-API-KEY': self.api_key or '', |
| 'Content-Type': 'application/json', |
| } |
| params = { |
| 'q': search_term, |
| **{ |
| key: value |
| for key, value in kwargs.items() if value is not None |
| }, |
| } |
| timeout = aiohttp.ClientTimeout(total=self.timeout) |
| async with aiohttp.ClientSession(timeout=timeout) as session: |
| try: |
| async with session.post( |
| f'https://google.serper.dev/{search_type or self.search_type}', |
| headers=headers, |
| params=params) as resp: |
| code, ret = resp.status, await resp.json() |
| except aiohttp.ClientError as e: |
| code, ret = -1, str(e) |
| return code, ret |
|
|