Skip to content

Commit 0037bae

Browse files
committed
enh: channels streaming agent
1 parent c951b4f commit 0037bae

4 files changed

Lines changed: 185 additions & 115 deletions

File tree

backend/open_webui/main.py

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1796,7 +1796,7 @@ async def chat_completion(
17961796

17971797
if metadata.get('chat_id') and user:
17981798
chat_id = metadata['chat_id']
1799-
if not chat_id.startswith('local:'): # temporary chats are not stored
1799+
if not chat_id.startswith('local:') and not chat_id.startswith('channel:'): # temporary/channel chats are not stored
18001800
if is_new_chat:
18011801
# Build the full history upfront with ALL assistant placeholders
18021802
user_message = metadata.get('user_message') or {}
@@ -2012,7 +2012,7 @@ async def emit_cancel_event():
20122012
if metadata.get('chat_id') and metadata.get('message_id'):
20132013
# Update the chat message with the error
20142014
try:
2015-
if not metadata['chat_id'].startswith('local:'):
2015+
if not metadata['chat_id'].startswith('local:') and not metadata['chat_id'].startswith('channel:'):
20162016
await Chats.upsert_message_to_chat_by_id_and_message_id(
20172017
metadata['chat_id'],
20182018
metadata['message_id'],
@@ -2275,7 +2275,7 @@ async def list_tasks_endpoint(request: Request, user=Depends(get_admin_user)):
22752275

22762276
@app.get('/api/tasks/chat/{chat_id:path}')
22772277
async def list_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)):
2278-
if chat_id.startswith('local:'):
2278+
if chat_id.startswith('local:') or chat_id.startswith('channel:'):
22792279
socket_id = chat_id[len('local:') :]
22802280
owner_id = get_user_id_from_session_pool(socket_id)
22812281
if owner_id != user.id and user.role != 'admin':
@@ -2293,7 +2293,7 @@ async def list_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=De
22932293

22942294
@app.post('/api/tasks/chat/{chat_id:path}/stop')
22952295
async def stop_tasks_by_chat_id_endpoint(request: Request, chat_id: str, user=Depends(get_verified_user)):
2296-
if chat_id.startswith('local:'):
2296+
if chat_id.startswith('local:') or chat_id.startswith('channel:'):
22972297
socket_id = chat_id[len('local:') :]
22982298
owner_id = get_user_id_from_session_pool(socket_id)
22992299
if owner_id != user.id and user.role != 'admin':

backend/open_webui/routers/channels.py

Lines changed: 35 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
get_all_models,
5858
get_filtered_models,
5959
)
60-
from open_webui.utils.chat import generate_chat_completion
60+
6161

6262

6363
from open_webui.utils.auth import get_admin_user, get_verified_user
@@ -979,57 +979,50 @@ async def model_response_handler(request, channel, message, user, db=None):
979979
],
980980
]
981981

982+
# Resolve model config (same helpers automations use)
983+
from open_webui.utils.automations import (
984+
_resolve_model_tool_ids,
985+
_resolve_model_features,
986+
_resolve_model_filter_ids,
987+
)
988+
989+
tool_ids = _resolve_model_tool_ids(request.app, model_id)
990+
features = _resolve_model_features(request.app, model_id)
991+
filter_ids = _resolve_model_filter_ids(request.app, model_id)
992+
993+
# Build full form_data — same shape as frontend POST.
994+
# The channel: prefix routes pipeline events to the
995+
# channel emitter in socket/main.py instead of the
996+
# default chat emitter.
982997
form_data = {
983998
'model': model_id,
984999
'messages': [
9851000
system_message,
9861001
{'role': 'user', 'content': content},
9871002
],
988-
'stream': False,
1003+
'stream': True,
1004+
'chat_id': f'channel:{channel.id}',
1005+
'id': response_message.id,
1006+
'session_id': f'channel:{channel.id}',
1007+
'background_tasks': {},
9891008
}
990-
991-
res = await generate_chat_completion(
992-
request,
993-
form_data=form_data,
994-
user=user,
1009+
if tool_ids:
1010+
form_data['tool_ids'] = tool_ids
1011+
if features:
1012+
form_data['features'] = features
1013+
if filter_ids:
1014+
form_data['filter_ids'] = filter_ids
1015+
1016+
# Call the full chat completion pipeline — streaming,
1017+
# tools, filters, RAG — everything. The pipeline runs as
1018+
# an async task; the channel emitter handles progressive
1019+
# message updates via socket events.
1020+
await request.app.state.CHAT_COMPLETION_HANDLER(
1021+
request, form_data, user=user
9951022
)
9961023

997-
if res:
998-
if res.get('choices', []) and len(res['choices']) > 0:
999-
await update_message_by_id(
1000-
request,
1001-
channel.id,
1002-
response_message.id,
1003-
MessageForm(
1004-
**{
1005-
'content': res['choices'][0]['message']['content'],
1006-
'meta': {
1007-
'done': True,
1008-
},
1009-
}
1010-
),
1011-
user,
1012-
db,
1013-
)
1014-
elif res.get('error', None):
1015-
await update_message_by_id(
1016-
request,
1017-
channel.id,
1018-
response_message.id,
1019-
MessageForm(
1020-
**{
1021-
'content': f'Error: {res["error"]}',
1022-
'meta': {
1023-
'done': True,
1024-
},
1025-
}
1026-
),
1027-
user,
1028-
db,
1029-
)
10301024
except Exception as e:
1031-
log.info(e)
1032-
pass
1025+
log.exception(e)
10331026

10341027
return True
10351028

backend/open_webui/socket/main.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -832,7 +832,80 @@ async def disconnect(sid):
832832
# print(f"Unknown session ID {sid} disconnected")
833833

834834

835+
async def _make_channel_emitter(request_info):
836+
"""Event emitter that routes pipeline output to a channel message.
837+
838+
Translates chat:completion events into channel message:update socket
839+
emissions, throttled to avoid flooding with per-token updates.
840+
"""
841+
channel_id = request_info['chat_id'].removeprefix('channel:')
842+
message_id = request_info['message_id']
843+
844+
state = {'last_emit_at': 0.0}
845+
THROTTLE_INTERVAL = 0.15 # ~6 updates/sec
846+
847+
async def _emit_channel_update(content: str, done: bool = False):
848+
from open_webui.models.messages import Messages, MessageForm
849+
850+
update_form = MessageForm(content=content)
851+
if done:
852+
# Merge done flag into existing meta (preserve model_id etc.)
853+
msg = await Messages.get_message_by_id(message_id)
854+
existing_meta = (msg.meta or {}) if msg else {}
855+
update_form = MessageForm(
856+
content=content,
857+
meta={**existing_meta, 'done': True},
858+
)
859+
860+
await Messages.update_message_by_id(message_id, update_form)
861+
message = await Messages.get_message_by_id(message_id)
862+
if message:
863+
await sio.emit(
864+
'events:channel',
865+
{
866+
'channel_id': channel_id,
867+
'message_id': message_id,
868+
'data': {
869+
'type': 'message:update',
870+
'data': message.model_dump(),
871+
},
872+
},
873+
to=f'channel:{channel_id}',
874+
)
875+
876+
async def __channel_emitter__(event_data):
877+
event_type = event_data.get('type')
878+
879+
if event_type == 'chat:completion':
880+
data = event_data.get('data', {})
881+
content = data.get('content', '')
882+
done = data.get('done', False)
883+
884+
if not content and not done:
885+
return
886+
887+
now = __import__('time').time()
888+
if done or (now - state['last_emit_at']) >= THROTTLE_INTERVAL:
889+
state['last_emit_at'] = now
890+
await _emit_channel_update(content, done)
891+
892+
elif event_type == 'chat:message:error':
893+
error = event_data.get('data', {}).get('error', {})
894+
error_content = (
895+
error.get('content', 'An error occurred')
896+
if isinstance(error, dict)
897+
else str(error)
898+
)
899+
await _emit_channel_update(f'Error: {error_content}', done=True)
900+
901+
return __channel_emitter__
902+
903+
835904
async def get_event_emitter(request_info, update_db=True):
905+
# Channel mode: route pipeline output to channel message updates
906+
if request_info.get('chat_id', '').startswith('channel:'):
907+
return await _make_channel_emitter(request_info)
908+
836909
async def __event_emitter__(event_data):
837910
user_id = request_info['user_id']
838911
chat_id = request_info['chat_id']

0 commit comments

Comments
 (0)