mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-25 06:54:49 -06:00
refac
This commit is contained in:
@@ -1948,6 +1948,7 @@ async def get_app_config(request: Request):
|
||||
'calendar.enable',
|
||||
'automations.enable',
|
||||
'notes.enable',
|
||||
'chat.context_compaction.enable',
|
||||
'web.search.enable',
|
||||
'web.search.confirmation.enable',
|
||||
'web.search.confirmation.content',
|
||||
@@ -2017,6 +2018,7 @@ async def get_app_config(request: Request):
|
||||
'enable_calendar': config.get('calendar.enable'),
|
||||
'enable_automations': config.get('automations.enable'),
|
||||
'enable_notes': config.get('notes.enable'),
|
||||
'enable_context_compaction': config.get('chat.context_compaction.enable'),
|
||||
'enable_web_search': config.get('web.search.enable'),
|
||||
'enable_web_search_confirmation': config.get('web.search.confirmation.enable'),
|
||||
'web_search_confirmation_content': config.get('web.search.confirmation.content'),
|
||||
|
||||
@@ -145,6 +145,8 @@ async def compact_messages_for_request(
|
||||
|
||||
async def compact_chat_branch(request, user, chat: Any, model_id: str, models: dict) -> dict:
|
||||
config = await _load_config()
|
||||
if not config['enable']:
|
||||
return {'ok': True, 'compacted': False, 'reason': 'disabled'}
|
||||
|
||||
history = (chat.chat or {}).get('history') or {}
|
||||
current_id = history.get('currentId')
|
||||
@@ -225,6 +227,9 @@ async def get_chat_context_usage(chat: Any, model_id: str | None = None) -> dict
|
||||
return None
|
||||
|
||||
config = await _load_config()
|
||||
if not config['enable']:
|
||||
return None
|
||||
|
||||
params = ((chat.chat or {}).get('params') or {}).copy()
|
||||
if model_id:
|
||||
params['model'] = model_id
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
import AdminSettingField from './AdminSettingField.svelte';
|
||||
import AdminSettingRow from './AdminSettingRow.svelte';
|
||||
import AdminSettingSection from './AdminSettingSection.svelte';
|
||||
import { config as appConfig } from '$lib/stores';
|
||||
|
||||
const dispatch = createEventDispatcher();
|
||||
|
||||
@@ -51,6 +52,17 @@
|
||||
updateTaskConfig(localStorage.token, taskConfig),
|
||||
updateChatConfig(localStorage.token, chatConfig)
|
||||
]);
|
||||
appConfig.update((current) =>
|
||||
current
|
||||
? {
|
||||
...current,
|
||||
features: {
|
||||
...current.features,
|
||||
enable_context_compaction: chatConfig.ENABLE_CONTEXT_COMPACTION
|
||||
}
|
||||
}
|
||||
: current
|
||||
);
|
||||
};
|
||||
|
||||
let workspaceModels: any[] = [];
|
||||
|
||||
@@ -220,6 +220,8 @@
|
||||
return next;
|
||||
}, 0);
|
||||
|
||||
$: contextCompactionEnabled = Boolean($config?.features?.enable_context_compaction);
|
||||
|
||||
const getContextThreshold = () => {
|
||||
const chatThreshold = Number(params?.compact_token_threshold);
|
||||
if (Number.isFinite(chatThreshold) && chatThreshold > 0) {
|
||||
@@ -229,7 +231,7 @@
|
||||
const modelId = atSelectedModel?.id ?? selectedModels.find((id) => id);
|
||||
const model = $models.find((item) => item.id === modelId);
|
||||
const threshold = Number(model?.info?.params?.compact_token_threshold);
|
||||
return Number.isFinite(threshold) && threshold > 0 ? threshold : 80000;
|
||||
return Number.isFinite(threshold) && threshold > 0 ? threshold : null;
|
||||
};
|
||||
|
||||
const getContextUsage = () => {
|
||||
@@ -238,7 +240,9 @@
|
||||
}
|
||||
|
||||
const messages = createMessagesList(history, history.currentId);
|
||||
const threshold = getContextThreshold();
|
||||
const threshold = contextCompactionEnabled
|
||||
? (getContextThreshold() ?? serverContextUsage?.threshold ?? null)
|
||||
: null;
|
||||
const systemTokens = estimateTokens($settings?.system ?? '');
|
||||
let estimatedTokens = systemTokens;
|
||||
let hasUsageCheckpoint = false;
|
||||
@@ -276,12 +280,12 @@
|
||||
tokens: estimatedTokens,
|
||||
estimated_tokens: estimatedTokens,
|
||||
threshold,
|
||||
percent: threshold > 0 ? Math.max(0, Math.round((estimatedTokens / threshold) * 100)) : 0,
|
||||
percent: threshold > 0 ? Math.max(0, Math.round((estimatedTokens / threshold) * 100)) : null,
|
||||
source: 'estimated'
|
||||
};
|
||||
};
|
||||
|
||||
$: contextUsage = getContextUsage() ?? serverContextUsage;
|
||||
$: contextUsage = getContextUsage() ?? (contextCompactionEnabled ? serverContextUsage : null);
|
||||
$: embeddedHeaderTitle = embeddedTitle || $chatTitle || $i18n.t('Chat');
|
||||
|
||||
let selectedToolIds = [];
|
||||
@@ -2400,6 +2404,11 @@
|
||||
};
|
||||
|
||||
const handleManualCompact = async () => {
|
||||
if (!contextCompactionEnabled) {
|
||||
toast.message($i18n.t('Context compaction is disabled'));
|
||||
return;
|
||||
}
|
||||
|
||||
if (!$chatId || !history?.currentId) {
|
||||
toast.message($i18n.t('No chat to compact'));
|
||||
return;
|
||||
@@ -2430,7 +2439,9 @@
|
||||
? $i18n.t('Chat is too short to compact')
|
||||
: result?.reason === 'empty'
|
||||
? $i18n.t('No chat to compact')
|
||||
: $i18n.t('Nothing to compact');
|
||||
: result?.reason === 'disabled'
|
||||
? $i18n.t('Context compaction is disabled')
|
||||
: $i18n.t('Nothing to compact');
|
||||
toast.message(skippedReason, { id: toastId });
|
||||
}
|
||||
|
||||
@@ -3751,6 +3762,7 @@
|
||||
dropzoneId={messageInputDropzoneId}
|
||||
chatId={$chatId}
|
||||
{contextUsage}
|
||||
{contextCompactionEnabled}
|
||||
compactHandler={handleManualCompact}
|
||||
statusHandler={handleStatusCommand}
|
||||
forkHandler={handleForkChat}
|
||||
@@ -3866,6 +3878,7 @@
|
||||
dropzoneId={messageInputDropzoneId}
|
||||
chatId={$chatId}
|
||||
{contextUsage}
|
||||
{contextCompactionEnabled}
|
||||
compactHandler={handleManualCompact}
|
||||
statusHandler={handleStatusCommand}
|
||||
forkHandler={handleForkChat}
|
||||
|
||||
@@ -121,6 +121,7 @@
|
||||
export let forkHandler: Function = () => {};
|
||||
export let chatId = '';
|
||||
export let contextUsage = null;
|
||||
export let contextCompactionEnabled = false;
|
||||
|
||||
export let autoScroll = false;
|
||||
export let generating = false;
|
||||
@@ -389,6 +390,79 @@
|
||||
const trimNumber = (value: number) =>
|
||||
value >= 10 ? String(Math.round(value)) : value.toFixed(1).replace(/\.0$/, '');
|
||||
|
||||
const estimateTokens = (value) => {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return 0;
|
||||
}
|
||||
if (typeof value !== 'string') {
|
||||
try {
|
||||
value = JSON.stringify(value);
|
||||
} catch {
|
||||
value = String(value);
|
||||
}
|
||||
}
|
||||
return Math.max(1, Math.floor(value.length / 4));
|
||||
};
|
||||
|
||||
const estimateMessagesTokens = (messages) =>
|
||||
messages.reduce((total, message) => {
|
||||
let next = total + 4 + estimateTokens(message.content);
|
||||
next += estimateTokens(message.output);
|
||||
next += estimateTokens(message.tool_calls);
|
||||
next += estimateTokens(message.files);
|
||||
return next;
|
||||
}, 0);
|
||||
|
||||
const getLocalContextUsage = () => {
|
||||
if (!history?.currentId) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const messages = createMessagesList(history, history.currentId);
|
||||
if (!messages.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
let summary = '';
|
||||
let startIdx = 0;
|
||||
for (let idx = 0; idx < messages.length; idx += 1) {
|
||||
const value = messages[idx]?.contextSummary ?? messages[idx]?.context_summary;
|
||||
if (typeof value === 'string' && value.trim()) {
|
||||
summary = value;
|
||||
startIdx = idx;
|
||||
}
|
||||
}
|
||||
|
||||
const activeMessages = messages.slice(startIdx);
|
||||
let estimatedTokens = estimateTokens($settings?.system ?? '');
|
||||
let hasUsageCheckpoint = false;
|
||||
|
||||
for (let idx = activeMessages.length - 1; idx >= 0; idx -= 1) {
|
||||
const usage = activeMessages[idx]?.usage ?? activeMessages[idx]?.info?.usage;
|
||||
const inputTokens = usage?.input_tokens ?? usage?.prompt_tokens;
|
||||
if (inputTokens) {
|
||||
hasUsageCheckpoint = true;
|
||||
estimatedTokens =
|
||||
Number(inputTokens || 0) +
|
||||
Number(usage.output_tokens ?? usage.completion_tokens ?? 0) +
|
||||
estimateMessagesTokens(activeMessages.slice(idx + 1));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!hasUsageCheckpoint) {
|
||||
estimatedTokens += estimateTokens(summary) + estimateMessagesTokens(activeMessages);
|
||||
}
|
||||
|
||||
return {
|
||||
tokens: estimatedTokens,
|
||||
estimated_tokens: estimatedTokens,
|
||||
threshold: null,
|
||||
percent: null,
|
||||
source: 'estimated'
|
||||
};
|
||||
};
|
||||
|
||||
const copyStatusChatId = async () => {
|
||||
if (!chatId) return;
|
||||
await navigator.clipboard.writeText(chatId);
|
||||
@@ -398,11 +472,18 @@
|
||||
}, 1600);
|
||||
};
|
||||
|
||||
$: contextPercent = Math.max(0, Math.round(contextUsage?.percent ?? 0));
|
||||
$: contextValue = contextUsage
|
||||
? `${contextPercent}% ${formatTokenCount(contextUsage.estimated_tokens || contextUsage.tokens)}/${formatTokenCount(contextUsage.threshold)}`
|
||||
$: statusContextUsage = contextUsage ?? getLocalContextUsage();
|
||||
$: contextHasThreshold = Number(statusContextUsage?.threshold) > 0;
|
||||
$: contextPercent = contextHasThreshold
|
||||
? Math.max(0, Math.round(statusContextUsage?.percent ?? 0))
|
||||
: null;
|
||||
$: contextTokens = formatTokenCount(statusContextUsage?.estimated_tokens || statusContextUsage?.tokens || 0);
|
||||
$: contextValue = statusContextUsage
|
||||
? contextHasThreshold
|
||||
? `${contextPercent}% ${contextTokens}/${formatTokenCount(statusContextUsage.threshold)}`
|
||||
: `${contextTokens} ${$i18n.t('tokens')}`
|
||||
: $i18n.t('unknown');
|
||||
$: contextBarPercent = Math.min(contextPercent, 100);
|
||||
$: contextBarPercent = contextHasThreshold ? Math.min(contextPercent, 100) : 0;
|
||||
|
||||
const getCommand = () => {
|
||||
const chatInput = document.getElementById('chat-input');
|
||||
@@ -1086,12 +1167,12 @@
|
||||
char: '/',
|
||||
render: getSuggestionRenderer(CommandSuggestionList, {
|
||||
i18n,
|
||||
canCompact: () => !!history?.currentId,
|
||||
canCompact: () => !!history?.currentId && contextCompactionEnabled,
|
||||
compactDisabled: () => isActive,
|
||||
canStatus: () => !!history?.currentId,
|
||||
canFork: () => !!history?.currentId,
|
||||
forkDisabled: () => isActive,
|
||||
contextUsage: () => contextUsage,
|
||||
contextUsage: () => statusContextUsage,
|
||||
onCompact: compactHandler,
|
||||
onStatus: statusHandler,
|
||||
onFork: forkHandler,
|
||||
@@ -1436,14 +1517,16 @@
|
||||
{contextValue}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="mt-1.5 h-0.5 overflow-hidden rounded-full bg-gray-100 dark:bg-white/8"
|
||||
>
|
||||
{#if contextHasThreshold}
|
||||
<div
|
||||
class="h-full rounded-full bg-gray-300 dark:bg-white/20"
|
||||
style={`width: ${contextBarPercent}%`}
|
||||
></div>
|
||||
</div>
|
||||
class="mt-1.5 h-0.5 overflow-hidden rounded-full bg-gray-100 dark:bg-white/8"
|
||||
>
|
||||
<div
|
||||
class="h-full rounded-full bg-gray-300 dark:bg-white/20"
|
||||
style={`width: ${contextBarPercent}%`}
|
||||
></div>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if messageQueue.length}
|
||||
|
||||
@@ -30,7 +30,10 @@
|
||||
$: forkAvailable = typeof canFork === 'function' ? canFork() : canFork;
|
||||
$: isForkDisabled = typeof forkDisabled === 'function' ? forkDisabled() : forkDisabled;
|
||||
$: resolvedContextUsage = typeof contextUsage === 'function' ? contextUsage() : contextUsage;
|
||||
$: contextPercent = Math.max(0, Math.round(resolvedContextUsage?.percent ?? 0));
|
||||
$: contextHasThreshold = Number(resolvedContextUsage?.threshold) > 0;
|
||||
$: contextPercent = contextHasThreshold
|
||||
? Math.max(0, Math.round(resolvedContextUsage?.percent ?? 0))
|
||||
: null;
|
||||
|
||||
let suggestionElement: any = null;
|
||||
let filteredItems: any[] = [];
|
||||
@@ -85,6 +88,7 @@
|
||||
canFork={forkAvailable}
|
||||
forkDisabled={isForkDisabled}
|
||||
{contextPercent}
|
||||
{contextHasThreshold}
|
||||
onSelect={(e) => {
|
||||
const { type, data } = e;
|
||||
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
export let canFork = false;
|
||||
export let forkDisabled = false;
|
||||
export let contextPercent = 0;
|
||||
export let contextHasThreshold = false;
|
||||
|
||||
let selectedIdx = 0;
|
||||
export let filteredItems = [];
|
||||
@@ -23,7 +24,9 @@
|
||||
let skills = [];
|
||||
let searchDebounceTimer: ReturnType<typeof setTimeout>;
|
||||
|
||||
$: contextCirclePercent = Math.min(Math.max(0, Math.round(contextPercent)), 100);
|
||||
$: contextCirclePercent = contextHasThreshold
|
||||
? Math.min(Math.max(0, Math.round(contextPercent)), 100)
|
||||
: 0;
|
||||
$: contextCircleOffset = 50.27 * (1 - contextCirclePercent / 100);
|
||||
|
||||
$: commandItems = [
|
||||
@@ -144,34 +147,38 @@
|
||||
data-selected={commandIdx === selectedIdx}
|
||||
>
|
||||
<span class="app-icon-muted flex items-center justify-center w-4 shrink-0">
|
||||
<svg class="size-3.5 -rotate-90" viewBox="0 0 20 20" aria-hidden="true">
|
||||
<circle
|
||||
cx="10"
|
||||
cy="10"
|
||||
r="8"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
class="opacity-20"
|
||||
/>
|
||||
<circle
|
||||
cx="10"
|
||||
cy="10"
|
||||
r="8"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="50.27"
|
||||
style={`stroke-dashoffset: ${contextCircleOffset};`}
|
||||
/>
|
||||
</svg>
|
||||
{#if contextHasThreshold}
|
||||
<svg class="size-3.5 -rotate-90" viewBox="0 0 20 20" aria-hidden="true">
|
||||
<circle
|
||||
cx="10"
|
||||
cy="10"
|
||||
r="8"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
class="opacity-20"
|
||||
/>
|
||||
<circle
|
||||
cx="10"
|
||||
cy="10"
|
||||
r="8"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
stroke-linecap="round"
|
||||
stroke-dasharray="50.27"
|
||||
style={`stroke-dashoffset: ${contextCircleOffset};`}
|
||||
/>
|
||||
</svg>
|
||||
{/if}
|
||||
</span>
|
||||
<span class="flex-1 min-w-0 flex items-baseline gap-1.5 overflow-hidden">
|
||||
<span class="truncate">Compact</span>
|
||||
<span class="app-muted text-[0.625rem] truncate shrink-0">
|
||||
{contextCirclePercent}% full
|
||||
</span>
|
||||
{#if contextHasThreshold}
|
||||
<span class="app-muted text-[0.625rem] truncate shrink-0">
|
||||
{contextCirclePercent}% full
|
||||
</span>
|
||||
{/if}
|
||||
</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
|
||||
@@ -665,6 +665,21 @@
|
||||
title: 'Sub-agents',
|
||||
keywords: ['sub-agents', 'subagents', 'delegation', 'background', 'agents']
|
||||
},
|
||||
{
|
||||
id: 'admin:interface',
|
||||
title: 'Interface',
|
||||
keywords: ['interface', 'ui', 'appearance', 'banners', 'tasks', 'prompt suggestions', 'tags']
|
||||
},
|
||||
{
|
||||
id: 'admin:audio',
|
||||
title: 'Audio',
|
||||
keywords: ['audio', 'voice', 'speech', 'tts', 'stt', 'whisper', 'deepgram', 'azure']
|
||||
},
|
||||
{
|
||||
id: 'admin:images',
|
||||
title: 'Images',
|
||||
keywords: ['images', 'generation', 'dalle', 'stable diffusion', 'comfyui', 'automatic1111']
|
||||
},
|
||||
{
|
||||
id: 'admin:evaluations',
|
||||
title: 'Evaluations',
|
||||
@@ -700,21 +715,7 @@
|
||||
title: 'Pipelines',
|
||||
keywords: ['pipelines', 'workflows', 'filters', 'valves', 'middleware']
|
||||
},
|
||||
{
|
||||
id: 'admin:interface',
|
||||
title: 'Interface',
|
||||
keywords: ['interface', 'ui', 'appearance', 'banners', 'tasks', 'prompt suggestions', 'tags']
|
||||
},
|
||||
{
|
||||
id: 'admin:audio',
|
||||
title: 'Audio',
|
||||
keywords: ['audio', 'voice', 'speech', 'tts', 'stt', 'whisper', 'deepgram', 'azure']
|
||||
},
|
||||
{
|
||||
id: 'admin:images',
|
||||
title: 'Images',
|
||||
keywords: ['images', 'generation', 'dalle', 'stable diffusion', 'comfyui', 'automatic1111']
|
||||
},
|
||||
|
||||
{
|
||||
id: 'admin:db',
|
||||
title: 'Database',
|
||||
|
||||
@@ -322,6 +322,7 @@ type Config = {
|
||||
enable_admin_export: boolean;
|
||||
enable_admin_chat_access: boolean;
|
||||
enable_admin_analytics: boolean;
|
||||
enable_context_compaction?: boolean;
|
||||
enable_community_sharing: boolean;
|
||||
enable_memories: boolean;
|
||||
enable_plugins?: boolean;
|
||||
|
||||
Reference in New Issue
Block a user