From b0617759322e4d5c03b740a20a07a868c3e3e331 Mon Sep 17 00:00:00 2001 From: kurtdami Date: Thu, 27 Feb 2025 00:12:41 -0800 Subject: [PATCH] feat: add perplexity integration to web search --- backend/open_webui/config.py | 6 ++ backend/open_webui/main.py | 2 + .../open_webui/retrieval/web/perplexity.py | 87 +++++++++++++++++++ backend/open_webui/routers/retrieval.py | 15 +++- .../admin/Settings/WebSearch.svelte | 14 ++- 5 files changed, 122 insertions(+), 2 deletions(-) create mode 100644 backend/open_webui/retrieval/web/perplexity.py diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index 5e0e4f0a1..b086778ba 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1936,6 +1936,12 @@ EXA_API_KEY = PersistentConfig( os.getenv("EXA_API_KEY", ""), ) +PERPLEXITY_API_KEY = PersistentConfig( + "PERPLEXITY_API_KEY", + "rag.web.search.perplexity_api_key", + os.getenv("PERPLEXITY_API_KEY", ""), +) + RAG_WEB_SEARCH_RESULT_COUNT = PersistentConfig( "RAG_WEB_SEARCH_RESULT_COUNT", "rag.web.search.result_count", diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index 346d28d6c..801e1a0d4 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -208,6 +208,7 @@ from open_webui.config import ( BING_SEARCH_V7_SUBSCRIPTION_KEY, BRAVE_SEARCH_API_KEY, EXA_API_KEY, + PERPLEXITY_API_KEY, KAGI_SEARCH_API_KEY, MOJEEK_SEARCH_API_KEY, BOCHA_SEARCH_API_KEY, @@ -584,6 +585,7 @@ app.state.config.JINA_API_KEY = JINA_API_KEY app.state.config.BING_SEARCH_V7_ENDPOINT = BING_SEARCH_V7_ENDPOINT app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY = BING_SEARCH_V7_SUBSCRIPTION_KEY app.state.config.EXA_API_KEY = EXA_API_KEY +app.state.config.PERPLEXITY_API_KEY = PERPLEXITY_API_KEY app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = RAG_WEB_SEARCH_RESULT_COUNT app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS = RAG_WEB_SEARCH_CONCURRENT_REQUESTS diff --git a/backend/open_webui/retrieval/web/perplexity.py b/backend/open_webui/retrieval/web/perplexity.py new file mode 100644 index 000000000..e5314eb1f --- /dev/null +++ b/backend/open_webui/retrieval/web/perplexity.py @@ -0,0 +1,87 @@ +import logging +from typing import Optional, List +import requests + +from open_webui.retrieval.web.main import SearchResult, get_filtered_results +from open_webui.env import SRC_LOG_LEVELS + +log = logging.getLogger(__name__) +log.setLevel(SRC_LOG_LEVELS["RAG"]) + + +def search_perplexity( + api_key: str, + query: str, + count: int, + filter_list: Optional[list[str]] = None, +) -> list[SearchResult]: + """Search using Perplexity API and return the results as a list of SearchResult objects. + + Args: + api_key (str): A Perplexity API key + query (str): The query to search for + count (int): Maximum number of results to return + + """ + + # Handle PersistentConfig object + if hasattr(api_key, "__str__"): + api_key = str(api_key) + + try: + url = "https://api.perplexity.ai/chat/completions" + + # Create payload for the API call + payload = { + "model": "sonar", + "messages": [ + { + "role": "system", + "content": "You are a search assistant. Provide factual information with citations.", + }, + {"role": "user", "content": query}, + ], + "temperature": 0.2, # Lower temperature for more factual responses + "stream": False, + } + + headers = { + "Authorization": f"Bearer {api_key}", + "Content-Type": "application/json", + } + + # Make the API request + response = requests.request("POST", url, json=payload, headers=headers) + + # Parse the JSON response + json_response = response.json() + + # Extract citations from the response + citations = json_response.get("citations", []) + + # Create search results from citations + results = [] + for i, citation in enumerate(citations[:count]): + # Extract content from the response to use as snippet + content = "" + if "choices" in json_response and json_response["choices"]: + if i == 0: + content = json_response["choices"][0]["message"]["content"] + + result = {"link": citation, "title": f"Source {i+1}", "snippet": content} + results.append(result) + + if filter_list: + + results = get_filtered_results(results, filter_list) + + return [ + SearchResult( + link=result["link"], title=result["title"], snippet=result["snippet"] + ) + for result in results[:count] + ] + + except Exception as e: + log.error(f"Error searching with Perplexity API: {e}") + return [] diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index e69d2ce96..d09445c37 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -59,7 +59,7 @@ from open_webui.retrieval.web.serpstack import search_serpstack from open_webui.retrieval.web.tavily import search_tavily from open_webui.retrieval.web.bing import search_bing from open_webui.retrieval.web.exa import search_exa - +from open_webui.retrieval.web.perplexity import search_perplexity from open_webui.retrieval.utils import ( get_embedding_function, @@ -398,6 +398,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): "bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT, "bing_search_v7_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY, "exa_api_key": request.app.state.config.EXA_API_KEY, + "perplexity_api_key": request.app.state.config.PERPLEXITY_API_KEY, "result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, "concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS, "domain_filter_list": request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, @@ -451,6 +452,7 @@ class WebSearchConfig(BaseModel): bing_search_v7_endpoint: Optional[str] = None bing_search_v7_subscription_key: Optional[str] = None exa_api_key: Optional[str] = None + perplexity_api_key: Optional[str] = None result_count: Optional[int] = None concurrent_requests: Optional[int] = None trust_env: Optional[bool] = None @@ -580,6 +582,8 @@ async def update_rag_config( request.app.state.config.EXA_API_KEY = form_data.web.search.exa_api_key + request.app.state.config.PERPLEXITY_API_KEY = form_data.web.search.perplexity_api_key + request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT = ( form_data.web.search.result_count ) @@ -641,6 +645,7 @@ async def update_rag_config( "bing_search_v7_endpoint": request.app.state.config.BING_SEARCH_V7_ENDPOINT, "bing_search_v7_subscription_key": request.app.state.config.BING_SEARCH_V7_SUBSCRIPTION_KEY, "exa_api_key": request.app.state.config.EXA_API_KEY, + "perplexity_api_key": request.app.state.config.PERPLEXITY_API_KEY, "result_count": request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, "concurrent_requests": request.app.state.config.RAG_WEB_SEARCH_CONCURRENT_REQUESTS, "trust_env": request.app.state.config.RAG_WEB_SEARCH_TRUST_ENV, @@ -1163,6 +1168,7 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]: - SERPLY_API_KEY - TAVILY_API_KEY - EXA_API_KEY + - PERPLEXITY_API_KEY - SEARCHAPI_API_KEY + SEARCHAPI_ENGINE (by default `google`) - SERPAPI_API_KEY + SERPAPI_ENGINE (by default `google`) Args: @@ -1327,6 +1333,13 @@ def search_web(request: Request, engine: str, query: str) -> list[SearchResult]: request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, ) + elif engine == "perplexity": + return search_perplexity( + request.app.state.config.PERPLEXITY_API_KEY, + query, + request.app.state.config.RAG_WEB_SEARCH_RESULT_COUNT, + request.app.state.config.RAG_WEB_SEARCH_DOMAIN_FILTER_LIST, + ) else: raise Exception("No search engine API key found in environment variables") diff --git a/src/lib/components/admin/Settings/WebSearch.svelte b/src/lib/components/admin/Settings/WebSearch.svelte index 84e9d0e5a..df41bdbc9 100644 --- a/src/lib/components/admin/Settings/WebSearch.svelte +++ b/src/lib/components/admin/Settings/WebSearch.svelte @@ -29,7 +29,8 @@ 'tavily', 'jina', 'bing', - 'exa' + 'exa', + 'perplexity' ]; let youtubeLanguage = 'en'; @@ -344,6 +345,17 @@ bind:value={webConfig.search.exa_api_key} /> + {:else if webConfig.search.engine === 'perplexity'} +
+
+ {$i18n.t('Perplexity API Key')} +
+ + +
{:else if webConfig.search.engine === 'bing'}