mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-03 02:23:51 +00:00
feat: add web search support to channels
This commit is contained in:
parent
d0b17f0569
commit
0b66a8e12a
4 changed files with 167 additions and 25 deletions
|
|
@ -51,6 +51,8 @@ from open_webui.utils.models import (
|
|||
get_all_models,
|
||||
get_filtered_models,
|
||||
)
|
||||
from open_webui.routers.tasks import generate_queries
|
||||
from open_webui.routers.retrieval import process_web_search, SearchForm
|
||||
from open_webui.utils.webhook import post_webhook
|
||||
from pydantic import BaseModel, field_validator
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
|
@ -978,9 +980,67 @@ async def model_response_handler(request, channel, message, user, db=None):
|
|||
)
|
||||
|
||||
tool_ids = _resolve_model_tool_ids(request.app, model_id)
|
||||
features = _resolve_model_features(request.app, model_id)
|
||||
model_features = _resolve_model_features(request.app, model_id)
|
||||
filter_ids = _resolve_model_filter_ids(request.app, model_id)
|
||||
|
||||
message_features = (message.data or {}).get('features', {})
|
||||
features = {**(model_features or {}), **message_features}
|
||||
|
||||
# Process web search before pipeline to avoid duplication
|
||||
web_search_sources = []
|
||||
if message_features.get('web_search'):
|
||||
user_message = content if isinstance(content, str) else content[0].get('text', '')
|
||||
queries = [user_message]
|
||||
|
||||
try:
|
||||
queries_res = await generate_queries(
|
||||
request,
|
||||
{
|
||||
'model': model_id,
|
||||
'messages': [{'role': 'user', 'content': user_message}],
|
||||
'prompt': user_message,
|
||||
'type': 'web_search',
|
||||
},
|
||||
user,
|
||||
)
|
||||
if isinstance(queries_res, dict) and queries_res.get('choices'):
|
||||
response = queries_res['choices'][0]['message']['content']
|
||||
bracket_start = response.rfind('{')
|
||||
bracket_end = response.rfind('}') + 1
|
||||
if bracket_start != -1 and bracket_end > bracket_start:
|
||||
parsed = json.loads(response[bracket_start:bracket_end])
|
||||
queries = parsed.get('queries', [user_message])
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
if queries:
|
||||
try:
|
||||
results = await process_web_search(
|
||||
request,
|
||||
SearchForm(queries=queries),
|
||||
user=user,
|
||||
)
|
||||
if results and results.get('docs'):
|
||||
web_search_sources = results.get('filenames', [])[:5]
|
||||
web_context = '\n\n### Web Search Results:\n'
|
||||
for i, doc in enumerate(results['docs'][:5], 1):
|
||||
source_url = web_search_sources[i-1] if i <= len(web_search_sources) else ''
|
||||
doc_content = doc.get('content', str(doc)) if isinstance(doc, dict) else str(doc)
|
||||
web_context += f'\n[{i}] {source_url}\n{doc_content[:2000]}\n'
|
||||
|
||||
if isinstance(content, str):
|
||||
content = web_context + '\n\n### User Question:\n' + content
|
||||
elif isinstance(content, list):
|
||||
for part in content:
|
||||
if part.get('type') == 'text':
|
||||
part['text'] = web_context + '\n\n### User Question:\n' + part['text']
|
||||
break
|
||||
except Exception as e:
|
||||
log.exception(e)
|
||||
|
||||
# Remove web_search from features so pipeline doesn't duplicate
|
||||
features = {k: v for k, v in features.items() if k != 'web_search'}
|
||||
|
||||
# Build full form_data — same shape as frontend POST.
|
||||
# The channel: prefix routes pipeline events to the
|
||||
# channel emitter in socket/main.py instead of the
|
||||
|
|
@ -1006,6 +1066,14 @@ async def model_response_handler(request, channel, message, user, db=None):
|
|||
if filter_ids:
|
||||
form_data['filter_ids'] = filter_ids
|
||||
|
||||
# Store sources for emitter to append when done
|
||||
if web_search_sources:
|
||||
await Messages.update_message_by_id(
|
||||
response_message.id,
|
||||
MessageForm(content='', data={'web_search_sources': web_search_sources}),
|
||||
db=db,
|
||||
)
|
||||
|
||||
# Call the full chat completion pipeline — streaming,
|
||||
# tools, filters, RAG — everything. The pipeline runs as
|
||||
# an async task; the channel emitter handles progressive
|
||||
|
|
|
|||
|
|
@ -844,9 +844,18 @@ async def _make_channel_emitter(request_info):
|
|||
|
||||
update_form = MessageForm(content=content)
|
||||
if done:
|
||||
# Merge done flag into existing meta (preserve model_id etc.)
|
||||
msg = await Messages.get_message_by_id(message_id)
|
||||
existing_meta = (msg.meta or {}) if msg else {}
|
||||
existing_data = (msg.data or {}) if msg else {}
|
||||
|
||||
# Append web search sources if present
|
||||
web_search_sources = existing_data.get('web_search_sources', [])
|
||||
if web_search_sources and content:
|
||||
sources_text = '\n\n---\n**Sources:**\n'
|
||||
for i, url in enumerate(web_search_sources, 1):
|
||||
sources_text += f'- [{i}] {url}\n'
|
||||
content = content + sources_text
|
||||
|
||||
update_form = MessageForm(
|
||||
content=content,
|
||||
meta={**existing_meta, 'done': True},
|
||||
|
|
|
|||
|
|
@ -30,7 +30,9 @@
|
|||
import CommandSuggestionList from '../chat/MessageInput/CommandSuggestionList.svelte';
|
||||
|
||||
import InputMenu from './MessageInput/InputMenu.svelte';
|
||||
import IntegrationsMenu from '../chat/MessageInput/IntegrationsMenu.svelte';
|
||||
import Tooltip from '../common/Tooltip.svelte';
|
||||
import Component from '../icons/Component.svelte';
|
||||
import RichTextInput from '../common/RichTextInput.svelte';
|
||||
import VoiceRecording from '../chat/MessageInput/VoiceRecording.svelte';
|
||||
import FileItem from '../common/FileItem.svelte';
|
||||
|
|
@ -40,6 +42,7 @@
|
|||
import MentionList from './MessageInput/MentionList.svelte';
|
||||
import Skeleton from '../chat/Messages/Skeleton.svelte';
|
||||
import XMark from '../icons/XMark.svelte';
|
||||
import GlobeAlt from '../icons/GlobeAlt.svelte';
|
||||
|
||||
export let placeholder = $i18n.t('Type here...');
|
||||
export let chatInputElement;
|
||||
|
|
@ -75,6 +78,11 @@
|
|||
let content = '';
|
||||
let files = [];
|
||||
|
||||
let webSearchEnabled = false;
|
||||
$: showWebSearchButton =
|
||||
$config?.features?.enable_web_search &&
|
||||
($user?.role === 'admin' || $user?.permissions?.features?.web_search);
|
||||
|
||||
let filesInputElement;
|
||||
let inputFiles;
|
||||
|
||||
|
|
@ -547,7 +555,14 @@
|
|||
onSubmit({
|
||||
content,
|
||||
data: {
|
||||
files: files
|
||||
files: files,
|
||||
features: {
|
||||
web_search:
|
||||
$config?.features?.enable_web_search &&
|
||||
($user?.role === 'admin' || $user?.permissions?.features?.web_search)
|
||||
? webSearchEnabled
|
||||
: false
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
@ -1009,6 +1024,53 @@
|
|||
</button>
|
||||
</InputMenu>
|
||||
{/if}
|
||||
|
||||
{#if showWebSearchButton}
|
||||
<div
|
||||
class="flex self-center w-[1px] h-4 mx-1 bg-gray-200/50 dark:bg-gray-800/50"
|
||||
/>
|
||||
|
||||
<IntegrationsMenu
|
||||
selectedModels={[]}
|
||||
fileUploadCapableModels={[]}
|
||||
toggleFilters={[]}
|
||||
showToolsButton={false}
|
||||
{showWebSearchButton}
|
||||
bind:webSearchEnabled
|
||||
showImageGenerationButton={false}
|
||||
imageGenerationEnabled={false}
|
||||
showCodeInterpreterButton={false}
|
||||
codeInterpreterEnabled={false}
|
||||
selectedToolIds={[]}
|
||||
selectedFilterIds={[]}
|
||||
onShowValves={() => {}}
|
||||
onClose={async () => {
|
||||
await tick();
|
||||
chatInputElement?.focus();
|
||||
}}
|
||||
>
|
||||
<div
|
||||
class="bg-transparent hover:bg-gray-100 text-gray-700 dark:text-white dark:hover:bg-gray-800 rounded-full size-8 flex justify-center items-center outline-hidden focus:outline-hidden"
|
||||
>
|
||||
<Component className="size-4.5" strokeWidth="1.5" />
|
||||
</div>
|
||||
</IntegrationsMenu>
|
||||
|
||||
{#if webSearchEnabled}
|
||||
<Tooltip content={$i18n.t('Web Search')} placement="top">
|
||||
<button
|
||||
on:click|preventDefault={() => (webSearchEnabled = !webSearchEnabled)}
|
||||
type="button"
|
||||
class="group p-[7px] flex gap-1.5 items-center text-sm rounded-full transition-colors duration-300 focus:outline-hidden max-w-full overflow-hidden text-sky-500 dark:text-sky-300 bg-sky-50 hover:bg-sky-100 dark:bg-sky-400/10 dark:hover:bg-sky-600/10 border border-sky-200/40 dark:border-sky-500/20"
|
||||
>
|
||||
<GlobeAlt className="size-4" strokeWidth="1.75" />
|
||||
<div class="hidden group-hover:block">
|
||||
<XMark className="size-4" strokeWidth="1.75" />
|
||||
</div>
|
||||
</button>
|
||||
</Tooltip>
|
||||
{/if}
|
||||
{/if}
|
||||
</slot>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
|
|
@ -43,6 +43,7 @@
|
|||
[];
|
||||
export let selectedFilterIds: string[] = [];
|
||||
|
||||
export let showToolsButton = true;
|
||||
export let showWebSearchButton = false;
|
||||
export let webSearchEnabled = false;
|
||||
export let showImageGenerationButton = false;
|
||||
|
|
@ -119,32 +120,34 @@
|
|||
>
|
||||
{#if tab === ''}
|
||||
<div in:fly={{ x: -20, duration: 150 }}>
|
||||
{#if tools}
|
||||
{#if Object.keys(tools).length > 0}
|
||||
<button
|
||||
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||
on:click={() => {
|
||||
tab = 'tools';
|
||||
}}
|
||||
>
|
||||
<Wrench />
|
||||
{#if showToolsButton}
|
||||
{#if tools}
|
||||
{#if Object.keys(tools).length > 0}
|
||||
<button
|
||||
class="flex w-full justify-between gap-2 items-center px-3 py-1.5 text-sm cursor-pointer rounded-xl hover:bg-gray-50 dark:hover:bg-gray-800/50"
|
||||
on:click={() => {
|
||||
tab = 'tools';
|
||||
}}
|
||||
>
|
||||
<Wrench />
|
||||
|
||||
<div class="flex items-center w-full justify-between">
|
||||
<div class=" line-clamp-1">
|
||||
{$i18n.t('Tools')}
|
||||
<span class="ml-0.5 text-gray-500">{Object.keys(tools).length}</span>
|
||||
</div>
|
||||
<div class="flex items-center w-full justify-between">
|
||||
<div class=" line-clamp-1">
|
||||
{$i18n.t('Tools')}
|
||||
<span class="ml-0.5 text-gray-500">{Object.keys(tools).length}</span>
|
||||
</div>
|
||||
|
||||
<div class="text-gray-500">
|
||||
<ChevronRight />
|
||||
<div class="text-gray-500">
|
||||
<ChevronRight />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</button>
|
||||
</button>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="py-4">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
{:else}
|
||||
<div class="py-4">
|
||||
<Spinner />
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if toggleFilters && toggleFilters.length > 0}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue