Jelajahi Sumber

feat: add perplexity integration to web search

kurtdami 2 bulan lalu
induk
melakukan
b061775932

+ 6 - 0
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",

+ 2 - 0
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

+ 87 - 0
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 []

+ 14 - 1
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")
 

+ 13 - 1
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}
 								/>
 							</div>
+						{:else if webConfig.search.engine === 'perplexity'}
+							<div>
+								<div class=" self-center text-xs font-medium mb-1">
+									{$i18n.t('Perplexity API Key')}
+								</div>
+
+								<SensitiveInput
+									placeholder={$i18n.t('Enter Perplexity API Key')}
+									bind:value={webConfig.search.perplexity_api_key}
+								/>
+							</div>
 						{:else if webConfig.search.engine === 'bing'}
 							<div>
 								<div class=" self-center text-xs font-medium mb-1">