mirror of
https://github.com/open-webui/open-webui.git
synced 2026-10-11 03:38:02 +00:00
refac
This commit is contained in:
parent
3e0e760ce3
commit
82f142d376
6 changed files with 5434 additions and 45 deletions
|
|
@ -9,6 +9,7 @@ import time
|
|||
import weakref
|
||||
from contextlib import suppress
|
||||
from typing import Any
|
||||
from uuid import uuid4
|
||||
|
||||
import pycrdt as Y
|
||||
import socketio
|
||||
|
|
@ -1470,6 +1471,21 @@ async def get_event_call(request_info):
|
|||
log.warning(f'Event caller: session {session_id} not owned by requesting user or disconnected')
|
||||
return {'error': 'Client session disconnected.'}
|
||||
|
||||
interaction_id = None
|
||||
timeout = WEBSOCKET_EVENT_CALLER_TIMEOUT
|
||||
if event_data.get('type') == 'request:user_input' or (
|
||||
event_data.get('type') == 'confirmation' and (event_data.get('data') or {}).get('tool_call')
|
||||
):
|
||||
interaction_id = str(uuid4())
|
||||
data = dict(event_data.get('data') or {})
|
||||
timeout_ms = data.get('timeout_ms', 120_000)
|
||||
if isinstance(timeout_ms, bool) or not isinstance(timeout_ms, int):
|
||||
timeout_ms = 120_000
|
||||
timeout = min(max(timeout_ms / 1000, 60), 240)
|
||||
if WEBSOCKET_EVENT_CALLER_TIMEOUT is not None and WEBSOCKET_EVENT_CALLER_TIMEOUT > 0:
|
||||
timeout = min(timeout, WEBSOCKET_EVENT_CALLER_TIMEOUT)
|
||||
event_data = {**event_data, 'data': {**data, 'interaction_id': interaction_id}}
|
||||
|
||||
try:
|
||||
return await sio.call(
|
||||
'events',
|
||||
|
|
@ -1479,11 +1495,22 @@ async def get_event_call(request_info):
|
|||
'data': event_data,
|
||||
},
|
||||
to=session_id,
|
||||
timeout=WEBSOCKET_EVENT_CALLER_TIMEOUT,
|
||||
timeout=timeout,
|
||||
)
|
||||
except (TimeoutError, socketio.exceptions.TimeoutError):
|
||||
log.warning(f'Event caller timed out for session {session_id}')
|
||||
return {'error': 'Event call timed out. The browser tab may be inactive or closed.'}
|
||||
finally:
|
||||
if interaction_id:
|
||||
await sio.emit(
|
||||
'events',
|
||||
{
|
||||
'chat_id': request_info.get('chat_id'),
|
||||
'message_id': request_info.get('message_id'),
|
||||
'data': {'type': 'request:interaction:done', 'data': {'interaction_id': interaction_id}},
|
||||
},
|
||||
to=session_id,
|
||||
)
|
||||
|
||||
if 'session_id' in request_info and 'chat_id' in request_info and 'message_id' in request_info:
|
||||
return __event_caller__
|
||||
|
|
|
|||
|
|
@ -3431,6 +3431,23 @@ async def execute_tool_call(form_data, metadata, event_caller, tool_call):
|
|||
params = {key: value for key, value in params.items() if key in allowed_params}
|
||||
|
||||
try:
|
||||
if (
|
||||
not is_saved_chat_id(metadata.get('chat_id'))
|
||||
and metadata.get('params', {}).get('tool_approval_mode') == 'ask'
|
||||
and not (name == 'ask_user' and tool_type == 'builtin')
|
||||
and (
|
||||
not event_caller
|
||||
or await event_caller(
|
||||
{
|
||||
'type': 'confirmation',
|
||||
'data': {'tool_call': {'id': tool_call.get('id'), 'name': name, 'arguments': params}},
|
||||
}
|
||||
)
|
||||
is not True # Disconnection and timeout replies are error objects, not approval.
|
||||
)
|
||||
):
|
||||
return params, 'Error: tool call was not approved.', tool, tool_type, direct_tool
|
||||
|
||||
if direct_tool:
|
||||
if not event_caller:
|
||||
result = 'Error: Browser session is not connected for this direct tool.'
|
||||
|
|
@ -5927,7 +5944,7 @@ async def streaming_chat_response_handler(response, ctx):
|
|||
for tool_call in response_tool_calls
|
||||
if tool_call.get('function', {}).get('name') != 'ask_user'
|
||||
]
|
||||
elif ask_user_staged:
|
||||
elif ask_user_staged and save_to_chat:
|
||||
if is_saved_chat_id(metadata.get('chat_id')) and metadata.get('message_id'):
|
||||
await pause_for_tool_approval(
|
||||
metadata['chat_id'],
|
||||
|
|
@ -5938,6 +5955,11 @@ async def streaming_chat_response_handler(response, ctx):
|
|||
)
|
||||
await event_emitter({'type': 'chat:completion', 'data': {'output': full_output()}})
|
||||
return
|
||||
elif ask_user_staged:
|
||||
# Live questions await the socket callback, not the saved-chat resolver.
|
||||
for item in output:
|
||||
if item.get('name') == 'ask_user' and item.get('status') == 'pending':
|
||||
item['status'] = 'in_progress'
|
||||
|
||||
# Append function_call items for each tool call
|
||||
# (Responses API already has them from streaming, so skip duplicates)
|
||||
|
|
|
|||
|
|
@ -177,10 +177,29 @@
|
|||
let eventConfirmationInputType = '';
|
||||
let eventConfirmationInputOptions: ({ label?: string; value: string } | string)[] = [];
|
||||
let eventCallback: (value: any) => void = () => {};
|
||||
let showAskUserDialog = false;
|
||||
let askUserQuestions: any[] = [];
|
||||
let askUserAllowOther = true;
|
||||
let askUserTimeoutMs: number | null = null;
|
||||
type BrowserInteraction = {
|
||||
id: string;
|
||||
messageId: string;
|
||||
type: string;
|
||||
data: any;
|
||||
callback: (value: any) => void;
|
||||
};
|
||||
let browserInteractions: BrowserInteraction[] = [];
|
||||
let browserApprovalMode: string | null = null;
|
||||
$: browserInteraction = browserInteractions[0];
|
||||
|
||||
const resolveBrowserInteraction = (interaction: BrowserInteraction, value: any) => {
|
||||
if (!browserInteractions.includes(interaction)) return;
|
||||
browserInteractions = browserInteractions.filter((item) => item !== interaction);
|
||||
interaction.callback(value);
|
||||
};
|
||||
|
||||
const cancelBrowserInteractions = () => {
|
||||
browserApprovalMode = null;
|
||||
for (const interaction of browserInteractions) {
|
||||
resolveBrowserInteraction(interaction, { status: 'cancelled', answers: {} });
|
||||
}
|
||||
};
|
||||
|
||||
let selectedModels = [''];
|
||||
let selectedModelIdx = 0;
|
||||
|
|
@ -461,6 +480,7 @@
|
|||
|
||||
const handleToolApprovalModeChange = async (mode: string) => {
|
||||
const tool_approval_mode = mode === 'ask' ? 'ask' : 'full';
|
||||
browserApprovalMode = tool_approval_mode;
|
||||
params = {
|
||||
...params,
|
||||
tool_approval_mode
|
||||
|
|
@ -488,6 +508,13 @@
|
|||
}
|
||||
|
||||
if (tool_approval_mode === 'full') {
|
||||
if (isTemporaryChatId($chatId)) {
|
||||
if (browserApprovalMode !== 'full') return;
|
||||
for (const interaction of browserInteractions) {
|
||||
if (interaction.data?.tool_call) resolveBrowserInteraction(interaction, true);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const messages = [...Object.values(history?.messages ?? {})].reverse() as any[];
|
||||
for (const message of messages) {
|
||||
if ((message.user_id ?? chat?.user_id ?? $user?.id) !== $user?.id) continue;
|
||||
|
|
@ -649,20 +676,16 @@
|
|||
}
|
||||
: null;
|
||||
|
||||
$: socketAskUserPrompt = {
|
||||
show: showAskUserDialog,
|
||||
questions: askUserQuestions,
|
||||
allowOther: askUserAllowOther,
|
||||
timeoutMs: askUserTimeoutMs,
|
||||
onConfirm: (value) => {
|
||||
showAskUserDialog = false;
|
||||
eventCallback(value);
|
||||
},
|
||||
onCancel: () => {
|
||||
showAskUserDialog = false;
|
||||
eventCallback({ status: 'cancelled', answers: {} });
|
||||
}
|
||||
};
|
||||
$: socketAskUserPrompt = ((interaction: BrowserInteraction | undefined) => ({
|
||||
id: interaction?.id,
|
||||
show: interaction?.type === 'request:user_input',
|
||||
questions: interaction?.data?.questions ?? [],
|
||||
allowOther: interaction?.data?.allow_other ?? true,
|
||||
timeoutMs: interaction?.data?.timeout_ms ?? null,
|
||||
onConfirm: (value: any) => interaction && resolveBrowserInteraction(interaction, value),
|
||||
onCancel: () =>
|
||||
interaction && resolveBrowserInteraction(interaction, { status: 'cancelled', answers: {} })
|
||||
}))(browserInteraction);
|
||||
|
||||
const mergeChatVariableSchemas = (modelIds = [], availableModels = []) => {
|
||||
const byKey: Record<string, any> = {};
|
||||
|
|
@ -840,6 +863,7 @@
|
|||
}
|
||||
|
||||
const navigateHandler = async () => {
|
||||
cancelBrowserInteractions();
|
||||
bridge?.end();
|
||||
noteChatDebug('navigateHandler start');
|
||||
// Mark the outgoing chat as read before loading the new one.
|
||||
|
|
@ -1382,6 +1406,43 @@
|
|||
const chatEventHandler = async (event, cb) => {
|
||||
console.log(event);
|
||||
if (event.shared && event.user_id === $user?.id && event.data?.type !== 'chat:messages') return;
|
||||
const interactionType = event?.data?.type;
|
||||
const interactionData = event?.data?.data;
|
||||
if (interactionType === 'request:interaction:done') {
|
||||
browserInteractions = browserInteractions.filter(
|
||||
(item) => item.id !== interactionData?.interaction_id
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
interactionType === 'request:user_input' ||
|
||||
(interactionType === 'confirmation' && interactionData?.tool_call)
|
||||
) {
|
||||
if (!cb) return;
|
||||
if (event.chat_id !== $chatId || !history.messages[event.message_id]) {
|
||||
cb({ status: 'cancelled', answers: {} });
|
||||
return;
|
||||
}
|
||||
if (
|
||||
interactionData?.tool_call &&
|
||||
isTemporaryChatId(event.chat_id) &&
|
||||
browserApprovalMode === 'full'
|
||||
) {
|
||||
cb(true);
|
||||
return;
|
||||
}
|
||||
browserInteractions = [
|
||||
...browserInteractions,
|
||||
{
|
||||
id: interactionData?.interaction_id ?? uuidv4(),
|
||||
messageId: event.message_id,
|
||||
type: interactionType,
|
||||
data: interactionData,
|
||||
callback: cb
|
||||
}
|
||||
];
|
||||
return;
|
||||
}
|
||||
|
||||
// A new chat's title can arrive before its id; the response message already exists.
|
||||
if (
|
||||
|
|
@ -1466,6 +1527,11 @@
|
|||
}
|
||||
autoScrollToBottom();
|
||||
} else if (type === 'chat:tasks:cancel') {
|
||||
for (const interaction of browserInteractions) {
|
||||
if (interaction.messageId === event.message_id) {
|
||||
resolveBrowserInteraction(interaction, { status: 'cancelled', answers: {} });
|
||||
}
|
||||
}
|
||||
message.bridgeCancelled = true;
|
||||
bridgeCancellations.get(event.message_id)?.();
|
||||
if (!event.shared) dismissContextCompactionToast();
|
||||
|
|
@ -1629,13 +1695,6 @@
|
|||
eventConfirmationInputValue = data?.value ?? '';
|
||||
eventConfirmationInputType = data?.input?.type ?? data?.type ?? '';
|
||||
eventConfirmationInputOptions = data?.input?.options ?? data?.options ?? [];
|
||||
} else if (type === 'request:user_input') {
|
||||
eventCallback = cb;
|
||||
askUserQuestions = data?.questions ?? [];
|
||||
askUserAllowOther = data?.allow_other ?? true;
|
||||
askUserTimeoutMs =
|
||||
typeof data?.timeout_ms === 'number' && data.timeout_ms > 0 ? data.timeout_ms : null;
|
||||
showAskUserDialog = true;
|
||||
} else if (type.startsWith('terminal:')) {
|
||||
terminalEventHandler(type, data);
|
||||
} else {
|
||||
|
|
@ -1761,6 +1820,7 @@
|
|||
$socket?.on('events', chatEventHandler);
|
||||
$socket?.on('connect', handleSocketConnect);
|
||||
$socket?.on('disconnect', clearTyping);
|
||||
$socket?.on('disconnect', cancelBrowserInteractions);
|
||||
|
||||
$audioQueue?.destroy();
|
||||
|
||||
|
|
@ -1842,6 +1902,7 @@
|
|||
|
||||
return () => {
|
||||
try {
|
||||
cancelBrowserInteractions();
|
||||
clearTimeout(saveControlsTimer);
|
||||
saveControls();
|
||||
if (chatIdProp && !$temporaryChatEnabled) {
|
||||
|
|
@ -1866,6 +1927,7 @@
|
|||
$socket?.off('events', chatEventHandler);
|
||||
$socket?.off('connect', handleSocketConnect);
|
||||
$socket?.off('disconnect', clearTyping);
|
||||
$socket?.off('disconnect', cancelBrowserInteractions);
|
||||
dismissContextCompactionToast();
|
||||
audioQueueInstance?.destroy();
|
||||
audioQueue.set(null);
|
||||
|
|
@ -2402,6 +2464,7 @@
|
|||
};
|
||||
|
||||
const initNewChat = async () => {
|
||||
cancelBrowserInteractions();
|
||||
bridge?.end();
|
||||
console.log('initNewChat');
|
||||
resetWebSearchConfirmation();
|
||||
|
|
@ -4199,6 +4262,7 @@
|
|||
const responseMessage = messageId ? history.messages[messageId] : null;
|
||||
if (responseMessage && (responseMessage.user_id ?? chat?.user_id ?? $user?.id) !== $user?.id)
|
||||
return;
|
||||
cancelBrowserInteractions();
|
||||
if (bridge?.connected && responseMessage) responseMessage.bridgeStopping = true;
|
||||
const hasTaskIds = (taskIds?.length ?? 0) > 0;
|
||||
const hasPendingAssistantResponse =
|
||||
|
|
@ -4741,6 +4805,29 @@
|
|||
}}
|
||||
/>
|
||||
|
||||
{#if browserInteraction?.data?.tool_call}
|
||||
{#key browserInteraction.id}
|
||||
{@const interaction = browserInteraction}
|
||||
<EventConfirmDialog
|
||||
show={true}
|
||||
title={$i18n.t('Tool Permissions')}
|
||||
confirmLabel={$i18n.t('Allow')}
|
||||
cancelLabel={$i18n.t('Deny')}
|
||||
on:confirm={() => resolveBrowserInteraction(interaction, true)}
|
||||
on:cancel={() => resolveBrowserInteraction(interaction, false)}
|
||||
>
|
||||
<div class="text-sm text-gray-500">
|
||||
<div class="font-medium break-all">{interaction.data.tool_call.name}</div>
|
||||
<pre class="mt-2 max-h-64 overflow-auto whitespace-pre-wrap break-all">{JSON.stringify(
|
||||
interaction.data.tool_call.arguments,
|
||||
null,
|
||||
2
|
||||
)}</pre>
|
||||
</div>
|
||||
</EventConfirmDialog>
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
<div
|
||||
class="{embedded
|
||||
? 'h-full'
|
||||
|
|
|
|||
|
|
@ -125,6 +125,7 @@
|
|||
const i18n: any = getContext('i18n');
|
||||
|
||||
type AskUserPrompt = {
|
||||
id?: string;
|
||||
show: boolean;
|
||||
questions: any[];
|
||||
allowOther: boolean;
|
||||
|
|
@ -1813,18 +1814,21 @@
|
|||
|
||||
{#if askUser?.show}
|
||||
<div class="mx-1">
|
||||
<AskUserCard
|
||||
show={askUser.show}
|
||||
questions={askUser.questions}
|
||||
allowOther={askUser.allowOther}
|
||||
timeoutMs={askUser.timeoutMs}
|
||||
on:confirm={(e) => {
|
||||
askUser.onConfirm(e.detail);
|
||||
}}
|
||||
on:cancel={() => {
|
||||
askUser.onCancel();
|
||||
}}
|
||||
/>
|
||||
{#key askUser.id}
|
||||
{@const activeAskUser = askUser}
|
||||
<AskUserCard
|
||||
show={askUser.show}
|
||||
questions={askUser.questions}
|
||||
allowOther={askUser.allowOther}
|
||||
timeoutMs={askUser.timeoutMs}
|
||||
on:confirm={(e) => {
|
||||
activeAskUser.onConfirm(e.detail);
|
||||
}}
|
||||
on:cancel={() => {
|
||||
activeAskUser.onCancel();
|
||||
}}
|
||||
/>
|
||||
{/key}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
|
|
|
|||
|
|
@ -1425,11 +1425,9 @@
|
|||
<AdvancedParams admin={true} custom={true} layout="grid" bind:params />
|
||||
</fieldset>
|
||||
{/if}
|
||||
{#if admin}
|
||||
<fieldset disabled={readOnly} class="min-w-0">
|
||||
<ModelControls bind:controls={params.model_controls} />
|
||||
</fieldset>
|
||||
{/if}
|
||||
<fieldset disabled={readOnly} class="min-w-0">
|
||||
<ModelControls bind:controls={params.model_controls} />
|
||||
</fieldset>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
Loading…
Add table
Reference in a new issue