enh: linkup
This commit is contained in:
@@ -1914,6 +1914,24 @@ YOUCOM_API_KEY = ConfigVar(
|
||||
os.getenv('YOUCOM_API_KEY', ''),
|
||||
)
|
||||
|
||||
LINKUP_API_KEY = ConfigVar(
|
||||
'LINKUP_API_KEY',
|
||||
'rag.web.search.linkup_api_key',
|
||||
os.getenv('LINKUP_API_KEY', ''),
|
||||
)
|
||||
|
||||
linkup_search_params = os.getenv('LINKUP_SEARCH_PARAMS', '')
|
||||
try:
|
||||
linkup_search_params = json.loads(linkup_search_params)
|
||||
except json.JSONDecodeError:
|
||||
linkup_search_params = {}
|
||||
|
||||
LINKUP_SEARCH_PARAMS = ConfigVar(
|
||||
'LINKUP_SEARCH_PARAMS',
|
||||
'rag.web.search.linkup_search_params',
|
||||
linkup_search_params,
|
||||
)
|
||||
|
||||
####################################
|
||||
# Images
|
||||
####################################
|
||||
|
||||
@@ -410,6 +410,8 @@ from open_webui.config import (
|
||||
YANDEX_WEB_SEARCH_CONFIG,
|
||||
YANDEX_WEB_SEARCH_URL,
|
||||
YOUCOM_API_KEY,
|
||||
LINKUP_API_KEY,
|
||||
LINKUP_SEARCH_PARAMS,
|
||||
YOUTUBE_LOADER_LANGUAGE,
|
||||
YOUTUBE_LOADER_PROXY_URL,
|
||||
AppConfig,
|
||||
@@ -1130,6 +1132,8 @@ app.state.config.YANDEX_WEB_SEARCH_URL = YANDEX_WEB_SEARCH_URL
|
||||
app.state.config.YANDEX_WEB_SEARCH_API_KEY = YANDEX_WEB_SEARCH_API_KEY
|
||||
app.state.config.YANDEX_WEB_SEARCH_CONFIG = YANDEX_WEB_SEARCH_CONFIG
|
||||
app.state.config.YOUCOM_API_KEY = YOUCOM_API_KEY
|
||||
app.state.config.LINKUP_API_KEY = LINKUP_API_KEY
|
||||
app.state.config.LINKUP_SEARCH_PARAMS = LINKUP_SEARCH_PARAMS
|
||||
|
||||
|
||||
app.state.config.PLAYWRIGHT_WS_URL = PLAYWRIGHT_WS_URL
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
|
||||
from open_webui.retrieval.web.main import SearchResult, get_filtered_results
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
DEFAULT_LINKUP_PARAMS = {
|
||||
'url': 'https://api.linkup.so/v1/search',
|
||||
'depth': 'standard',
|
||||
'outputType': 'sourcedAnswer',
|
||||
}
|
||||
|
||||
|
||||
def search_linkup(
|
||||
api_key: str,
|
||||
query: str,
|
||||
count: int,
|
||||
filter_list: Optional[list[str]] = None,
|
||||
params: Optional[dict] = None,
|
||||
) -> list[SearchResult]:
|
||||
"""Search using the Linkup Search API.
|
||||
|
||||
``params`` is forwarded almost verbatim as the JSON body; only ``q``
|
||||
and ``maxResults`` are injected automatically. The special key
|
||||
``url`` (default ``https://api.linkup.so/v1/search``) is popped and
|
||||
used as the endpoint.
|
||||
"""
|
||||
if hasattr(api_key, '__str__'):
|
||||
api_key = str(api_key)
|
||||
|
||||
merged = {**DEFAULT_LINKUP_PARAMS, **(params or {})}
|
||||
api_url = str(merged.pop('url', DEFAULT_LINKUP_PARAMS['url']))
|
||||
|
||||
payload = {**merged, 'q': query, 'maxResults': count}
|
||||
|
||||
try:
|
||||
response = requests.post(
|
||||
api_url,
|
||||
headers={
|
||||
'Authorization': f'Bearer {api_key}',
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
json=payload,
|
||||
timeout=30,
|
||||
)
|
||||
response.raise_for_status()
|
||||
json_response = response.json()
|
||||
|
||||
output_type = merged.get('outputType', 'sourcedAnswer')
|
||||
search_results = (
|
||||
json_response.get('sources', [])
|
||||
if output_type == 'sourcedAnswer'
|
||||
else json_response.get('results', [])
|
||||
)
|
||||
|
||||
if filter_list:
|
||||
search_results = get_filtered_results(search_results, filter_list)
|
||||
|
||||
return [
|
||||
SearchResult(
|
||||
link=r.get('url', ''),
|
||||
title=r.get('name') or r.get('title'),
|
||||
snippet=r.get('content') or r.get('text') or r.get('snippet'),
|
||||
)
|
||||
for r in search_results
|
||||
][:count]
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
log.error(f'Linkup API request failed: {e}')
|
||||
raise Exception(f'Linkup search failed: {str(e)}')
|
||||
except Exception as e:
|
||||
log.error(f'Error searching Linkup: {e}')
|
||||
raise Exception(f'Linkup search error: {str(e)}')
|
||||
@@ -107,6 +107,7 @@ from open_webui.retrieval.web.utils import get_web_loader
|
||||
from open_webui.retrieval.web.yacy import search_yacy
|
||||
from open_webui.retrieval.web.yandex import search_yandex
|
||||
from open_webui.retrieval.web.ydc import search_youcom
|
||||
from open_webui.retrieval.web.linkup import search_linkup
|
||||
from open_webui.storage.provider import Storage
|
||||
from open_webui.utils.access_control import has_permission
|
||||
from open_webui.utils.access_control.files import has_access_to_file
|
||||
@@ -553,6 +554,8 @@ async def get_rag_config(request: Request, user=Depends(get_admin_user)):
|
||||
'YANDEX_WEB_SEARCH_API_KEY': request.app.state.config.YANDEX_WEB_SEARCH_API_KEY,
|
||||
'YANDEX_WEB_SEARCH_CONFIG': request.app.state.config.YANDEX_WEB_SEARCH_CONFIG,
|
||||
'YOUCOM_API_KEY': request.app.state.config.YOUCOM_API_KEY,
|
||||
'LINKUP_API_KEY': request.app.state.config.LINKUP_API_KEY,
|
||||
'LINKUP_SEARCH_PARAMS': request.app.state.config.LINKUP_SEARCH_PARAMS,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -622,6 +625,8 @@ class WebConfig(BaseModel):
|
||||
YANDEX_WEB_SEARCH_API_KEY: str | None = None
|
||||
YANDEX_WEB_SEARCH_CONFIG: str | None = None
|
||||
YOUCOM_API_KEY: str | None = None
|
||||
LINKUP_API_KEY: str | None = None
|
||||
LINKUP_SEARCH_PARAMS: dict | None = None
|
||||
|
||||
|
||||
class ConfigForm(BaseModel):
|
||||
@@ -1109,6 +1114,8 @@ async def update_rag_config(request: Request, form_data: ConfigForm, user=Depend
|
||||
request.app.state.config.YANDEX_WEB_SEARCH_API_KEY = form_data.web.YANDEX_WEB_SEARCH_API_KEY
|
||||
request.app.state.config.YANDEX_WEB_SEARCH_CONFIG = form_data.web.YANDEX_WEB_SEARCH_CONFIG
|
||||
request.app.state.config.YOUCOM_API_KEY = form_data.web.YOUCOM_API_KEY
|
||||
request.app.state.config.LINKUP_API_KEY = form_data.web.LINKUP_API_KEY
|
||||
request.app.state.config.LINKUP_SEARCH_PARAMS = form_data.web.LINKUP_SEARCH_PARAMS
|
||||
|
||||
return {
|
||||
'status': True,
|
||||
@@ -1241,6 +1248,8 @@ async def update_rag_config(request: Request, form_data: ConfigForm, user=Depend
|
||||
'YANDEX_WEB_SEARCH_API_KEY': request.app.state.config.YANDEX_WEB_SEARCH_API_KEY,
|
||||
'YANDEX_WEB_SEARCH_CONFIG': request.app.state.config.YANDEX_WEB_SEARCH_CONFIG,
|
||||
'YOUCOM_API_KEY': request.app.state.config.YOUCOM_API_KEY,
|
||||
'LINKUP_API_KEY': request.app.state.config.LINKUP_API_KEY,
|
||||
'LINKUP_SEARCH_PARAMS': request.app.state.config.LINKUP_SEARCH_PARAMS,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1876,6 +1885,7 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea
|
||||
- SOUGOU_API_SID + SOUGOU_API_SK
|
||||
- SEARCHAPI_API_KEY + SEARCHAPI_ENGINE (by default `google`)
|
||||
- SERPAPI_API_KEY + SERPAPI_ENGINE (by default `google`)
|
||||
- LINKUP_API_KEY
|
||||
Args:
|
||||
query (str): The query to search for
|
||||
"""
|
||||
@@ -2166,6 +2176,17 @@ def search_web(request: Request, engine: str, query: str, user=None) -> list[Sea
|
||||
request.app.state.config.WEB_SEARCH_RESULT_COUNT,
|
||||
request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||
)
|
||||
elif engine == 'linkup':
|
||||
if request.app.state.config.LINKUP_API_KEY:
|
||||
return search_linkup(
|
||||
api_key=request.app.state.config.LINKUP_API_KEY,
|
||||
query=query,
|
||||
count=request.app.state.config.WEB_SEARCH_RESULT_COUNT,
|
||||
filter_list=request.app.state.config.WEB_SEARCH_DOMAIN_FILTER_LIST,
|
||||
params=request.app.state.config.LINKUP_SEARCH_PARAMS,
|
||||
)
|
||||
else:
|
||||
raise Exception('No LINKUP_API_KEY found in environment variables')
|
||||
else:
|
||||
raise Exception('No search engine API key found in environment variables')
|
||||
|
||||
|
||||
@@ -39,7 +39,8 @@
|
||||
'firecrawl',
|
||||
'external',
|
||||
'yandex',
|
||||
'youcom'
|
||||
'youcom',
|
||||
'linkup'
|
||||
];
|
||||
let webLoaderEngines = ['playwright', 'firecrawl', 'tavily', 'external'];
|
||||
|
||||
@@ -78,8 +79,14 @@
|
||||
webConfig.PLAYWRIGHT_TIMEOUT = webConfig.PLAYWRIGHT_TIMEOUT.toString();
|
||||
}
|
||||
|
||||
// Convert Linkup params JSON string to object before sending
|
||||
const linkupParams =
|
||||
typeof webConfig.LINKUP_SEARCH_PARAMS === 'string' && webConfig.LINKUP_SEARCH_PARAMS.trim() !== ''
|
||||
? JSON.parse(webConfig.LINKUP_SEARCH_PARAMS)
|
||||
: webConfig.LINKUP_SEARCH_PARAMS ?? {};
|
||||
|
||||
const res = await updateRAGConfig(localStorage.token, {
|
||||
web: webConfig
|
||||
web: { ...webConfig, LINKUP_SEARCH_PARAMS: linkupParams }
|
||||
});
|
||||
|
||||
// Convert arrays back to strings for display
|
||||
@@ -123,6 +130,12 @@
|
||||
webConfig.PLAYWRIGHT_TIMEOUT = parsed;
|
||||
}
|
||||
}
|
||||
|
||||
// Convert Linkup params object to JSON string for textarea display
|
||||
webConfig.LINKUP_SEARCH_PARAMS =
|
||||
typeof webConfig.LINKUP_SEARCH_PARAMS === 'object'
|
||||
? JSON.stringify(webConfig.LINKUP_SEARCH_PARAMS ?? {}, null, 2)
|
||||
: webConfig.LINKUP_SEARCH_PARAMS ?? '';
|
||||
}
|
||||
});
|
||||
</script>
|
||||
@@ -834,6 +847,30 @@
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{:else if webConfig.WEB_SEARCH_ENGINE === 'linkup'}
|
||||
<div class="mb-2.5 flex w-full flex-col">
|
||||
<div>
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Linkup API Key')}
|
||||
</div>
|
||||
|
||||
<SensitiveInput
|
||||
placeholder={$i18n.t('Enter Linkup API Key')}
|
||||
bind:value={webConfig.LINKUP_API_KEY}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="mt-2">
|
||||
<div class=" self-center text-xs font-medium mb-1">
|
||||
{$i18n.t('Parameters')}
|
||||
</div>
|
||||
|
||||
<Textarea
|
||||
bind:value={webConfig.LINKUP_SEARCH_PARAMS}
|
||||
placeholder={`{\n "depth": "standard",\n "outputType": "sourcedAnswer"\n}`}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if webConfig.WEB_SEARCH_ENGINE === 'duckduckgo'}
|
||||
|
||||
Reference in New Issue
Block a user