diff --git a/.env.example b/.env.example index 8558e9c4..c5438a2c 100644 --- a/.env.example +++ b/.env.example @@ -46,10 +46,16 @@ MAX_WORKERS=30 # API Keys and External Services # ============================================================================= +# SEARCH_PROVIDER=exa + # Serper API for web search and Google Scholar # Get your key from: https://serper.dev/ SERPER_KEY_ID=your_key +# Exa API for web search +# Get your key from: https://dashboard.exa.ai/api-keys +# EXA_API_KEY=your_key + # Jina API for web page reading # Get your key from: https://jina.ai/ JINA_API_KEYS=your_key diff --git a/README.md b/README.md index 453f5ae2..3397df2c 100644 --- a/README.md +++ b/README.md @@ -100,6 +100,7 @@ cp .env.example .env Edit the `.env` file and provide your actual API keys and configuration values: - **SERPER_KEY_ID**: Get your key from [Serper.dev](https://serper.dev/) for web search and Google Scholar +- **EXA_API_KEY**: Get your key from [Exa](https://dashboard.exa.ai/api-keys) for the `search` tool. Set `SEARCH_PROVIDER=exa` to select it, or leave `SEARCH_PROVIDER` unset and it is used automatically when `SERPER_KEY_ID` is absent - **JINA_API_KEYS**: Get your key from [Jina.ai](https://jina.ai/) for web page reading - **API_KEY/API_BASE**: OpenAI-compatible API for page summarization from [OpenAI](https://platform.openai.com/) - **DASHSCOPE_API_KEY**: Get your key from [Dashscope](https://dashscope.aliyun.com/) for file parsing diff --git a/inference/tool_search.py b/inference/tool_search.py index 1a3f7b53..1adf6193 100644 --- a/inference/tool_search.py +++ b/inference/tool_search.py @@ -13,6 +13,8 @@ SERPER_KEY=os.environ.get('SERPER_KEY_ID') +EXA_API_KEY=os.environ.get('EXA_API_KEY') +SEARCH_PROVIDER=os.environ.get('SEARCH_PROVIDER') or ('exa' if EXA_API_KEY and not SERPER_KEY else 'serper') @register_tool("search", allow_overwrite=True) @@ -106,7 +108,63 @@ def contains_chinese_basic(text: str) -> bool: + def exa_search(self, query: str): + payload = { + "query": query, + "type": "auto", + "numResults": 10, + "contents": {"highlights": True}, + } + headers = { + 'x-api-key': EXA_API_KEY, + 'Content-Type': 'application/json', + 'x-exa-integration': 'Alibaba-NLP/DeepResearch-integration', + } + + results = None + for i in range(5): + try: + response = requests.post( + "https://api.exa.ai/search", + json=payload, + headers=headers, + timeout=30, + ) + response.raise_for_status() + results = response.json() + break + except Exception as e: + print(e) + if i == 4: + return f"Exa search Timeout, return None, Please try again later." + continue + + try: + web_snippets = list() + for idx, page in enumerate(results.get("results", []), start=1): + date_published = "" + if page.get("publishedDate"): + date_published = "\nDate published: " + page["publishedDate"] + + source = "" + if page.get("author"): + source = "\nSource: " + page["author"] + + snippet = "" + if page.get("highlights"): + snippet = "\n" + " ... ".join(page["highlights"]) + + web_snippets.append( + f"{idx}. [{page.get('title') or page.get('url')}]({page.get('url')}){date_published}{source}\n{snippet}" + ) + + return f"An Exa search for '{query}' found {len(web_snippets)} results:\n\n## Web Results\n" + "\n\n".join(web_snippets) + except: + return f"No results found for '{query}'. Try with a more general query." + def search_with_serp(self, query: str): + if SEARCH_PROVIDER == 'exa': + return self.exa_search(query) result = self.google_search_with_serp(query) return result