This commit is contained in:
Timothy Jaeryang Baek 2026-09-08 23:18:48 -04:00
parent 31b272d3c9
commit 12b14124b9
4 changed files with 43 additions and 27 deletions

View file

@ -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,

View file

@ -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
]

View file

@ -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')

View file

@ -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}
/>
</div>
<AdminSettingField
className="mt-1.5"
label={$i18n.t('Max Content Length')}
forId="exa-max-content-length"
description={$i18n.t(
'Maximum characters per Exa search result. Leave empty for no limit.'
)}
>
<input
id="exa-max-content-length"
class={inputClass}
type="number"
min="1"
step="1"
placeholder={$i18n.t('No limit')}
bind:value={webConfig.EXA_MAX_CONTENT_LENGTH}
/>
</AdminSettingField>
</div>
{:else if webConfig.WEB_SEARCH_ENGINE === 'perplexity'}
<div class="mb-2.5 flex w-full flex-col">