diff --git a/backend/open_webui/config.py b/backend/open_webui/config.py index ee540f5598..2084fc638a 100644 --- a/backend/open_webui/config.py +++ b/backend/open_webui/config.py @@ -1271,6 +1271,9 @@ AZURE_AI_SEARCH_ENDPOINT = os.getenv('AZURE_AI_SEARCH_ENDPOINT', '') AZURE_AI_SEARCH_INDEX_NAME = os.getenv('AZURE_AI_SEARCH_INDEX_NAME', '') EXA_API_KEY = os.getenv('EXA_API_KEY', '') +EXA_MAX_CONTENT_LENGTH = int(os.environ['EXA_MAX_CONTENT_LENGTH']) if os.getenv('EXA_MAX_CONTENT_LENGTH') else None +if EXA_MAX_CONTENT_LENGTH is not None and EXA_MAX_CONTENT_LENGTH <= 0: + raise ValueError('EXA_MAX_CONTENT_LENGTH must be a positive integer or unset') PERPLEXITY_API_KEY = os.getenv('PERPLEXITY_API_KEY', '') @@ -3001,6 +3004,7 @@ DEFAULT_CONFIG = { 'web.search.azure_ai_search_endpoint': AZURE_AI_SEARCH_ENDPOINT, 'web.search.azure_ai_search_index_name': AZURE_AI_SEARCH_INDEX_NAME, 'web.search.exa_api_key': EXA_API_KEY, + 'web.search.exa_max_content_length': EXA_MAX_CONTENT_LENGTH, 'web.search.perplexity_api_key': PERPLEXITY_API_KEY, 'web.search.perplexity_model': PERPLEXITY_MODEL, 'web.search.perplexity_search_context_usage': PERPLEXITY_SEARCH_CONTEXT_USAGE, diff --git a/backend/open_webui/retrieval/web/exa.py b/backend/open_webui/retrieval/web/exa.py index 068cc380b9..803428a342 100644 --- a/backend/open_webui/retrieval/web/exa.py +++ b/backend/open_webui/retrieval/web/exa.py @@ -1,6 +1,4 @@ import logging -from dataclasses import dataclass -from typing import Optional import requests from open_webui.retrieval.web.main import SearchResult @@ -10,18 +8,12 @@ log = logging.getLogger(__name__) EXA_API_BASE = 'https://api.exa.ai' -@dataclass -class ExaResult: - url: str - title: str - text: str - - def search_exa( api_key: str, query: str, count: int, - filter_list: Optional[list[str]] = None, + filter_list: list[str] | None = None, + max_content_length: int | None = None, ) -> list[SearchResult]: """Search using Exa Search API and return the results as a list of SearchResult objects. @@ -29,7 +21,8 @@ def search_exa( api_key (str): A Exa Search API key query (str): The query to search for count (int): Number of results to return - filter_list (Optional[list[str]]): List of domains to filter results by + filter_list (list[str] | None): List of domains to filter results by + max_content_length (int | None): Maximum characters per result; None leaves text unlimited. """ log.info('Searching with Exa for query: %s', query) @@ -39,7 +32,7 @@ def search_exa( 'query': query, 'numResults': count or 5, 'includeDomains': filter_list, - 'contents': {'text': True, 'highlights': True}, + 'contents': {'text': {'maxCharacters': max_content_length} if max_content_length is not None else True}, 'type': 'auto', # Use the auto search type (keyword or neural) } @@ -48,22 +41,13 @@ def search_exa( response.raise_for_status() data = response.json() - results = [] - for result in data['results']: - results.append( - ExaResult( - url=result['url'], - title=result['title'], - text=result['text'], - ) - ) - + results = data['results'] log.info('Found %s results', len(results)) return [ SearchResult( - link=result.url, - title=result.title, - snippet=result.text, + link=result['url'], + title=result['title'], + snippet=(result.get('text') or '')[:max_content_length], ) for result in results ] diff --git a/backend/open_webui/routers/retrieval.py b/backend/open_webui/routers/retrieval.py index 55813341a2..5c7a323565 100644 --- a/backend/open_webui/routers/retrieval.py +++ b/backend/open_webui/routers/retrieval.py @@ -129,7 +129,7 @@ from open_webui.utils.misc import ( calculate_sha256_string, sanitize_text_for_db, ) -from pydantic import BaseModel +from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession log = logging.getLogger(__name__) @@ -297,6 +297,7 @@ RETRIEVAL_CONFIG_KEYS = { 'ENABLE_WEB_SEARCH_CONFIRMATION': 'web.search.confirmation.enable', 'WEB_SEARCH_CONFIRMATION_CONTENT': 'web.search.confirmation.content', 'EXA_API_KEY': 'web.search.exa_api_key', + 'EXA_MAX_CONTENT_LENGTH': 'web.search.exa_max_content_length', 'EXTERNAL_DOCUMENT_LOADER_API_KEY': 'rag.external_document_loader_api_key', 'EXTERNAL_DOCUMENT_LOADER_HEADERS': 'rag.external_document_loader_headers', 'EXTERNAL_DOCUMENT_LOADER_URL': 'rag.external_document_loader_url', @@ -745,6 +746,7 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)): 'BING_SEARCH_V7_ENDPOINT': config.BING_SEARCH_V7_ENDPOINT, 'BING_SEARCH_V7_SUBSCRIPTION_KEY': config.BING_SEARCH_V7_SUBSCRIPTION_KEY, 'EXA_API_KEY': config.EXA_API_KEY, + 'EXA_MAX_CONTENT_LENGTH': config.EXA_MAX_CONTENT_LENGTH, 'PERPLEXITY_API_KEY': config.PERPLEXITY_API_KEY, 'PERPLEXITY_MODEL': config.PERPLEXITY_MODEL, 'PERPLEXITY_SEARCH_CONTEXT_USAGE': config.PERPLEXITY_SEARCH_CONTEXT_USAGE, @@ -824,6 +826,7 @@ class WebConfig(BaseModel): BING_SEARCH_V7_ENDPOINT: str | None = None BING_SEARCH_V7_SUBSCRIPTION_KEY: str | None = None EXA_API_KEY: str | None = None + EXA_MAX_CONTENT_LENGTH: int | None = Field(default=None, gt=0, strict=True) PERPLEXITY_API_KEY: str | None = None PERPLEXITY_MODEL: str | None = None PERPLEXITY_SEARCH_CONTEXT_USAGE: str | None = None @@ -1342,6 +1345,7 @@ async def update_rag_config(request: Request, form_data: ConfigForm, user=Depend config.BING_SEARCH_V7_ENDPOINT = form_data.web.BING_SEARCH_V7_ENDPOINT config.BING_SEARCH_V7_SUBSCRIPTION_KEY = form_data.web.BING_SEARCH_V7_SUBSCRIPTION_KEY config.EXA_API_KEY = form_data.web.EXA_API_KEY + config.EXA_MAX_CONTENT_LENGTH = form_data.web.EXA_MAX_CONTENT_LENGTH config.PERPLEXITY_API_KEY = form_data.web.PERPLEXITY_API_KEY config.PERPLEXITY_MODEL = form_data.web.PERPLEXITY_MODEL config.PERPLEXITY_SEARCH_CONTEXT_USAGE = form_data.web.PERPLEXITY_SEARCH_CONTEXT_USAGE @@ -1494,6 +1498,7 @@ async def update_rag_config(request: Request, form_data: ConfigForm, user=Depend 'BING_SEARCH_V7_ENDPOINT': config.BING_SEARCH_V7_ENDPOINT, 'BING_SEARCH_V7_SUBSCRIPTION_KEY': config.BING_SEARCH_V7_SUBSCRIPTION_KEY, 'EXA_API_KEY': config.EXA_API_KEY, + 'EXA_MAX_CONTENT_LENGTH': config.EXA_MAX_CONTENT_LENGTH, 'PERPLEXITY_API_KEY': config.PERPLEXITY_API_KEY, 'PERPLEXITY_MODEL': config.PERPLEXITY_MODEL, 'PERPLEXITY_SEARCH_CONTEXT_USAGE': config.PERPLEXITY_SEARCH_CONTEXT_USAGE, @@ -2691,6 +2696,7 @@ async def search_web(request: Request, engine: str, query: str, user=None) -> li query, config.WEB_SEARCH_RESULT_COUNT, config.WEB_SEARCH_DOMAIN_FILTER_LIST, + max_content_length=config.EXA_MAX_CONTENT_LENGTH, ) else: raise Exception('No EXA_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 e77814f3a4..3570a03cde 100644 --- a/src/lib/components/admin/Settings/WebSearch.svelte +++ b/src/lib/components/admin/Settings/WebSearch.svelte @@ -98,7 +98,11 @@ : (webConfig.LINKUP_SEARCH_PARAMS ?? {}); const res = await updateRAGConfig(localStorage.token, { - web: { ...webConfig, LINKUP_SEARCH_PARAMS: linkupParams } + web: { + ...webConfig, + EXA_MAX_CONTENT_LENGTH: webConfig.EXA_MAX_CONTENT_LENGTH ?? null, + LINKUP_SEARCH_PARAMS: linkupParams + } }); // Convert arrays back to strings for display @@ -711,6 +715,24 @@ bind:value={webConfig.EXA_API_KEY} /> + + + {:else if webConfig.WEB_SEARCH_ENGINE === 'perplexity'}