Skip to content

Commit 3f0c0e0

Browse files
committed
refac
1 parent 8958b64 commit 3f0c0e0

9 files changed

Lines changed: 690 additions & 11 deletions

File tree

backend/open_webui/config.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1990,6 +1990,12 @@ class BannerModel(BaseModel):
19901990

19911991
TASK_MODEL_EXTERNAL = os.getenv('TASK_MODEL_EXTERNAL', '')
19921992

1993+
ENABLE_CONTEXT_COMPACTION = os.getenv('ENABLE_CONTEXT_COMPACTION', 'False').lower() == 'true'
1994+
1995+
CONTEXT_COMPACTION_TOKEN_THRESHOLD = int(os.getenv('CONTEXT_COMPACTION_TOKEN_THRESHOLD', '80000'))
1996+
1997+
CONTEXT_COMPACTION_PROMPT_TEMPLATE = os.getenv('CONTEXT_COMPACTION_PROMPT_TEMPLATE', '')
1998+
19931999
TITLE_GENERATION_PROMPT_TEMPLATE = os.getenv('TITLE_GENERATION_PROMPT_TEMPLATE', '')
19942000

19952001
DEFAULT_TITLE_GENERATION_PROMPT_TEMPLATE = """### Task:
@@ -2864,6 +2870,9 @@ def feishu_oauth_register(oauth: OAuth):
28642870
'auth.admin.email': ADMIN_EMAIL,
28652871
'task.model.default': TASK_MODEL,
28662872
'task.model.external': TASK_MODEL_EXTERNAL,
2873+
'chat.context_compaction.enable': ENABLE_CONTEXT_COMPACTION,
2874+
'chat.context_compaction.token_threshold': CONTEXT_COMPACTION_TOKEN_THRESHOLD,
2875+
'chat.context_compaction.prompt_template': CONTEXT_COMPACTION_PROMPT_TEMPLATE,
28672876
'task.title.prompt_template': TITLE_GENERATION_PROMPT_TEMPLATE,
28682877
'task.tags.prompt_template': TAGS_GENERATION_PROMPT_TEMPLATE,
28692878
'task.image.prompt_template': IMAGE_PROMPT_GENERATION_PROMPT_TEMPLATE,
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
"""add context summary to chat message
2+
3+
Revision ID: 4c5ce3d2f27f
4+
Revises: 3ff2c63645b8
5+
Create Date: 2026-06-18 23:48:08.310063
6+
7+
"""
8+
9+
from typing import Sequence, Union
10+
11+
from alembic import op
12+
import sqlalchemy as sa
13+
14+
15+
# revision identifiers, used by Alembic.
16+
revision: str = '4c5ce3d2f27f'
17+
down_revision: Union[str, None] = '3ff2c63645b8'
18+
branch_labels: Union[str, Sequence[str], None] = None
19+
depends_on: Union[str, Sequence[str], None] = None
20+
21+
22+
def upgrade() -> None:
23+
conn = op.get_bind()
24+
inspector = sa.inspect(conn)
25+
columns = {column['name'] for column in inspector.get_columns('chat_message')}
26+
27+
if 'context_summary' not in columns:
28+
op.add_column('chat_message', sa.Column('context_summary', sa.Text(), nullable=True))
29+
30+
31+
def downgrade() -> None:
32+
conn = op.get_bind()
33+
inspector = sa.inspect(conn)
34+
columns = {column['name'] for column in inspector.get_columns('chat_message')}
35+
36+
if 'context_summary' in columns:
37+
op.drop_column('chat_message', 'context_summary')

backend/open_webui/models/chat_messages.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -109,6 +109,9 @@ class ChatMessage(Base):
109109
# Usage (tokens, timing, etc.)
110110
usage = Column(JSON, nullable=True)
111111

112+
# Context compaction checkpoint
113+
context_summary = Column(Text, nullable=True)
114+
112115
# Timestamps
113116
created_at = Column(BigInteger, index=True)
114117
updated_at = Column(BigInteger)
@@ -143,6 +146,7 @@ class ChatMessageModel(BaseModel):
143146
status_history: Optional[list] = None
144147
error: Optional[dict | str] = None
145148
usage: Optional[dict] = None
149+
context_summary: Optional[str] = None
146150
created_at: int
147151
updated_at: int
148152

@@ -194,6 +198,8 @@ async def upsert_message(
194198
existing.status_history = data.get('status_history') or data.get('statusHistory')
195199
if 'error' in data:
196200
existing.error = data.get('error')
201+
if 'context_summary' in data or 'contextSummary' in data:
202+
existing.context_summary = data.get('context_summary') or data.get('contextSummary')
197203
# Extract and normalize usage
198204
usage = get_usage(data)
199205
if usage:
@@ -225,6 +231,7 @@ async def upsert_message(
225231
status_history=data.get('status_history') or data.get('statusHistory'),
226232
error=data.get('error'),
227233
usage=usage,
234+
context_summary=data.get('context_summary') or data.get('contextSummary'),
228235
created_at=timestamp,
229236
updated_at=now,
230237
)
@@ -251,6 +258,7 @@ async def get_messages_by_chat_id(self, chat_id: str, db: Optional[AsyncSession]
251258
'parent_id': 'parentId',
252259
'model_id': 'model',
253260
'status_history': 'statusHistory',
261+
'context_summary': 'contextSummary',
254262
'created_at': 'timestamp',
255263
}
256264
# DB-internal columns excluded from the reconstructed message dict.

backend/open_webui/routers/chats.py

Lines changed: 95 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -31,19 +31,46 @@
3131
from open_webui.models.shared_chats import SharedChatResponse, SharedChats
3232
from open_webui.models.tags import TagModel, Tags
3333
from open_webui.socket.main import get_event_emitter
34-
from open_webui.tasks import stop_item_tasks
34+
from open_webui.tasks import has_active_tasks, stop_item_tasks
3535
from open_webui.utils.access_control import filter_allowed_access_grants, has_permission
3636
from open_webui.utils.access_control.folders import has_folder_access
3737
from open_webui.utils.auth import get_admin_user, get_verified_user
38+
from open_webui.utils.context_compaction import compact_chat_branch
3839
from open_webui.utils.middleware import serialize_output
3940
from open_webui.utils.misc import get_message_list
41+
from open_webui.utils.models import get_all_models
4042
from pydantic import BaseModel
4143
from sqlalchemy.ext.asyncio import AsyncSession
4244

4345
log = logging.getLogger(__name__)
4446

4547
router = APIRouter()
4648

49+
CHAT_CONFIG_KEYS = {
50+
'ENABLE_CONTEXT_COMPACTION': 'chat.context_compaction.enable',
51+
'CONTEXT_COMPACTION_TOKEN_THRESHOLD': 'chat.context_compaction.token_threshold',
52+
'CONTEXT_COMPACTION_PROMPT_TEMPLATE': 'chat.context_compaction.prompt_template',
53+
}
54+
55+
56+
class ChatConfigForm(BaseModel):
57+
ENABLE_CONTEXT_COMPACTION: bool
58+
CONTEXT_COMPACTION_TOKEN_THRESHOLD: int
59+
CONTEXT_COMPACTION_PROMPT_TEMPLATE: str
60+
61+
62+
class CompactChatForm(BaseModel):
63+
model: str | None = None
64+
65+
66+
async def get_chat_config_values() -> dict:
67+
values = await Config.get_many(*CHAT_CONFIG_KEYS.values())
68+
return {field: values[storage_key] for field, storage_key in CHAT_CONFIG_KEYS.items() if storage_key in values}
69+
70+
71+
def chat_config_updates(data: dict) -> dict:
72+
return {CHAT_CONFIG_KEYS[field]: value for field, value in data.items() if field in CHAT_CONFIG_KEYS}
73+
4774

4875
async def require_chat_import_permission(request: Request, user, db: AsyncSession):
4976
if user.role != 'admin' and not await has_permission(
@@ -612,6 +639,30 @@ async def import_chats(
612639
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=ERROR_MESSAGES.DEFAULT())
613640

614641

642+
############################
643+
# ChatConfig
644+
############################
645+
646+
647+
@router.get('/config', response_model=ChatConfigForm)
648+
async def get_chat_config(user=Depends(get_admin_user)):
649+
return await get_chat_config_values()
650+
651+
652+
@router.post('/config', response_model=ChatConfigForm)
653+
async def set_chat_config(form_data: ChatConfigForm, user=Depends(get_admin_user)):
654+
threshold = max(1, int(form_data.CONTEXT_COMPACTION_TOKEN_THRESHOLD))
655+
await Config.upsert(
656+
chat_config_updates(
657+
{
658+
**form_data.model_dump(),
659+
'CONTEXT_COMPACTION_TOKEN_THRESHOLD': threshold,
660+
}
661+
)
662+
)
663+
return await get_chat_config_values()
664+
665+
615666
############################
616667
# GetChats
617668
############################
@@ -976,6 +1027,49 @@ async def get_user_chat_list_by_tag_name(
9761027
return chats
9771028

9781029

1030+
############################
1031+
# CompactChat
1032+
############################
1033+
1034+
1035+
@router.post('/{id}/compact')
1036+
async def compact_chat_by_id(
1037+
request: Request,
1038+
id: str,
1039+
form_data: CompactChatForm | None = None,
1040+
user=Depends(get_verified_user),
1041+
db: AsyncSession = Depends(get_async_session),
1042+
):
1043+
chat = await Chats.get_chat_by_id_and_user_id(id, user.id, db=db)
1044+
if not chat:
1045+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ERROR_MESSAGES.NOT_FOUND)
1046+
1047+
if await has_active_tasks(request.app.state.redis, id):
1048+
raise HTTPException(
1049+
status_code=status.HTTP_409_CONFLICT,
1050+
detail='Wait for the current response to finish before compacting.',
1051+
)
1052+
1053+
if not request.app.state.MODELS:
1054+
await get_all_models(request, user=user)
1055+
1056+
history = (chat.chat or {}).get('history') or {}
1057+
messages_map = await Chats.get_messages_map_by_chat_id(id)
1058+
message_list = get_message_list(messages_map or history.get('messages') or {}, history.get('currentId'))
1059+
model_id = (form_data.model if form_data else None) or next(
1060+
(message.get('model') for message in reversed(message_list) if message.get('model')),
1061+
None,
1062+
)
1063+
1064+
if not model_id:
1065+
chat_models = (chat.chat or {}).get('models') or []
1066+
model_id = chat_models[0] if chat_models else None
1067+
if not model_id:
1068+
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail='No model found for context compaction.')
1069+
1070+
return await compact_chat_branch(request, user, chat, model_id, request.app.state.MODELS)
1071+
1072+
9791073
############################
9801074
# GetChatById
9811075
############################

0 commit comments

Comments
 (0)