mirror of
https://github.com/open-webui/open-webui.git
synced 2026-08-26 11:40:56 -05:00
refac
This commit is contained in:
@@ -1389,6 +1389,15 @@ async def chat_completion(
|
||||
'content_preview': user_message.get('content', '')[:300],
|
||||
},
|
||||
)
|
||||
if not getattr(request.state, 'internal', False) and not (
|
||||
user_message.get('meta') or {}
|
||||
).get('internal'):
|
||||
try:
|
||||
from open_webui.utils.timers import cancel_timers_for_chat
|
||||
|
||||
await cancel_timers_for_chat(chat_id, 'chat.user_message')
|
||||
except Exception:
|
||||
log.exception('Failed to cancel chat.user_message timers for chat %s', chat_id)
|
||||
|
||||
# Link grandparent → user message (childrenIds)
|
||||
grandparent_id = user_message.get('parentId')
|
||||
@@ -1607,9 +1616,9 @@ async def chat_completion(
|
||||
and getattr(request.state, 'internal', False) is not True
|
||||
and not await has_active_tasks(request.app.state.redis, chat_id)
|
||||
):
|
||||
from open_webui.utils.subagents import process_pending_subagent_results
|
||||
from open_webui.utils.subagents import process_pending_internal_messages
|
||||
|
||||
await process_pending_subagent_results(
|
||||
await process_pending_internal_messages(
|
||||
request,
|
||||
chat_id,
|
||||
user.id,
|
||||
@@ -1626,7 +1635,7 @@ async def chat_completion(
|
||||
},
|
||||
)
|
||||
except Exception:
|
||||
log.exception('Failed to process pending sub-agent results for chat %s', metadata.get('chat_id'))
|
||||
log.exception('Failed to process pending internal messages for chat %s', metadata.get('chat_id'))
|
||||
|
||||
# Fan out: one task per model
|
||||
if metadata.get('session_id') and metadata.get('chat_id'):
|
||||
|
||||
@@ -529,6 +529,12 @@ async def chat_events(sid, data):
|
||||
|
||||
if event_type == 'last_read_at':
|
||||
await Chats.update_chat_last_read_at_by_id(data['chat_id'], user['id'])
|
||||
try:
|
||||
from open_webui.utils.timers import cancel_timers_for_chat
|
||||
|
||||
await cancel_timers_for_chat(data['chat_id'], 'chat.read')
|
||||
except Exception:
|
||||
log.exception('Failed to cancel chat.read timers for chat %s', data.get('chat_id'))
|
||||
|
||||
|
||||
def normalize_document_id(document_id: str) -> str:
|
||||
|
||||
@@ -12,7 +12,7 @@ import asyncio
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
from typing import Literal, Optional
|
||||
|
||||
from fastapi import Request
|
||||
|
||||
@@ -1519,6 +1519,43 @@ async def delegate_task(
|
||||
)
|
||||
|
||||
|
||||
async def timer(
|
||||
prompt: str,
|
||||
at: str,
|
||||
cancel_on: list[Literal['chat.read', 'chat.user_message']] | None = None,
|
||||
__request__: Request = None,
|
||||
__user__: dict = None,
|
||||
__metadata__: dict = None,
|
||||
__chat_id__: str = None,
|
||||
__message_id__: str = None,
|
||||
) -> str:
|
||||
"""
|
||||
Set a one-shot timer for this chat.
|
||||
|
||||
:param prompt: The prompt to send back into this chat when the timer fires
|
||||
:param at: Relative time like 10s, 5m, 1h, 2d, or a timezone-aware RFC 3339 timestamp
|
||||
:param cancel_on: Optional events that cancel the timer before it fires
|
||||
:return: JSON status with the scheduled time, or an error string
|
||||
"""
|
||||
if __request__ is None:
|
||||
return 'Error: request context not available.'
|
||||
if getattr(__request__.state, 'internal', False) is True:
|
||||
return 'Error: timers cannot be set from internal chats.'
|
||||
|
||||
from open_webui.utils.timers import create_timer
|
||||
|
||||
return await create_timer(
|
||||
prompt=prompt,
|
||||
at=at,
|
||||
cancel_on=cancel_on,
|
||||
request=__request__,
|
||||
user_data=__user__ or {},
|
||||
metadata=__metadata__ or {},
|
||||
parent_chat_id=__chat_id__ or '',
|
||||
parent_message_id=__message_id__,
|
||||
)
|
||||
|
||||
|
||||
# =============================================================================
|
||||
# CHANNELS TOOLS
|
||||
# =============================================================================
|
||||
|
||||
@@ -7,6 +7,7 @@ Follows the utils/<feature>.py pattern (cf. utils/channels.py, utils/task.py).
|
||||
The scheduler_worker_loop handles all time-based background work:
|
||||
- Automation execution (claim_due → execute)
|
||||
- Calendar event alerts (upcoming events → socket + webhook notifications)
|
||||
- One-shot chat timers
|
||||
|
||||
Environment:
|
||||
SCHEDULER_POLL_INTERVAL – seconds between polls (default: 10)
|
||||
@@ -190,8 +191,18 @@ async def scheduler_worker_loop(app) -> None:
|
||||
SCHEDULER_POLL_INTERVAL env var (default: 10 seconds).
|
||||
"""
|
||||
log.info(f'Scheduler worker started (poll interval: {SCHEDULER_POLL_INTERVAL}s)')
|
||||
|
||||
while True:
|
||||
try:
|
||||
# ── Timers ──
|
||||
try:
|
||||
from open_webui.utils.timers import claim_due_timers, execute_due_timer
|
||||
|
||||
for timer_id, claim_id in await claim_due_timers(int(time.time_ns()), limit=10):
|
||||
asyncio.create_task(execute_due_timer(app, timer_id, claim_id))
|
||||
except Exception:
|
||||
log.exception('Scheduler: timer error')
|
||||
|
||||
# ── Automations ──
|
||||
if await Config.get('automations.enable'):
|
||||
try:
|
||||
|
||||
@@ -67,7 +67,7 @@ def _build_request(source: Request, user_id: str, *, internal: bool) -> Request:
|
||||
return request
|
||||
|
||||
|
||||
async def process_pending_subagent_results(
|
||||
async def process_pending_internal_messages(
|
||||
source_request: Request,
|
||||
parent_chat_id: str,
|
||||
user_id: str,
|
||||
@@ -92,40 +92,74 @@ async def process_pending_subagent_results(
|
||||
message
|
||||
for message in messages.values()
|
||||
if message.get('role') == 'user'
|
||||
and (message.get('meta') or {}).get('async_subagent_result') is True
|
||||
and not message.get('childrenIds')
|
||||
and (
|
||||
(message.get('meta') or {}).get('async_subagent_result') is True
|
||||
or (
|
||||
(message.get('meta') or {}).get('internal') is True
|
||||
and (message.get('meta') or {}).get('type') == 'timer'
|
||||
)
|
||||
)
|
||||
]
|
||||
if not pending:
|
||||
return
|
||||
|
||||
first = pending[0]
|
||||
first_meta = first.get('meta') or {}
|
||||
kind = 'subagent' if first_meta.get('async_subagent_result') is True else 'timer'
|
||||
parent_id = first.get('parentId')
|
||||
if kind == 'timer' and first_meta.get('timer_id'):
|
||||
timer = await Chats.get_chat_by_id(first_meta['timer_id'])
|
||||
run = {**run, **(((timer.meta or {}).get('run') if timer else None) or {})}
|
||||
model_id = first.get('model') or run['model_id']
|
||||
batch = [
|
||||
message
|
||||
for message in pending
|
||||
if message.get('parentId') == parent_id and (message.get('model') or model_id) == model_id
|
||||
]
|
||||
if kind == 'timer':
|
||||
batch = [
|
||||
message
|
||||
for message in pending
|
||||
if message.get('parentId') == parent_id
|
||||
and (message.get('model') or model_id) == model_id
|
||||
and (message.get('meta') or {}).get('internal') is True
|
||||
and (message.get('meta') or {}).get('type') == 'timer'
|
||||
]
|
||||
else:
|
||||
batch = [
|
||||
message
|
||||
for message in pending
|
||||
if message.get('parentId') == parent_id
|
||||
and (message.get('model') or model_id) == model_id
|
||||
and (message.get('meta') or {}).get('async_subagent_result') is True
|
||||
]
|
||||
combined_content = '\n\n'.join(message.get('content', '') for message in batch if message.get('content'))
|
||||
delegation_ids = [
|
||||
message['meta']['delegation_id'] for message in batch if (message.get('meta') or {}).get('delegation_id')
|
||||
]
|
||||
subagent_chat_ids = [
|
||||
message['meta']['subagent_chat_id']
|
||||
for message in batch
|
||||
if (message.get('meta') or {}).get('subagent_chat_id')
|
||||
]
|
||||
combined_meta = {'async_subagent_result': True}
|
||||
if len(delegation_ids) == 1:
|
||||
combined_meta['delegation_id'] = delegation_ids[0]
|
||||
elif delegation_ids:
|
||||
combined_meta['delegation_ids'] = delegation_ids
|
||||
if len(subagent_chat_ids) == 1:
|
||||
combined_meta['subagent_chat_id'] = subagent_chat_ids[0]
|
||||
elif subagent_chat_ids:
|
||||
combined_meta['subagent_chat_ids'] = subagent_chat_ids
|
||||
if kind == 'timer':
|
||||
timer_ids = [message['meta']['timer_id'] for message in batch if (message.get('meta') or {}).get('timer_id')]
|
||||
combined_meta = {'internal': True, 'type': 'timer'}
|
||||
if len(timer_ids) == 1:
|
||||
combined_meta['timer_id'] = timer_ids[0]
|
||||
elif timer_ids:
|
||||
combined_meta['timer_ids'] = timer_ids
|
||||
else:
|
||||
delegation_ids = [
|
||||
message['meta']['delegation_id']
|
||||
for message in batch
|
||||
if (message.get('meta') or {}).get('delegation_id')
|
||||
]
|
||||
subagent_chat_ids = [
|
||||
message['meta']['subagent_chat_id']
|
||||
for message in batch
|
||||
if (message.get('meta') or {}).get('subagent_chat_id')
|
||||
]
|
||||
combined_meta = {'async_subagent_result': True}
|
||||
if len(delegation_ids) == 1:
|
||||
combined_meta['delegation_id'] = delegation_ids[0]
|
||||
elif delegation_ids:
|
||||
combined_meta['delegation_ids'] = delegation_ids
|
||||
if len(subagent_chat_ids) == 1:
|
||||
combined_meta['subagent_chat_id'] = subagent_chat_ids[0]
|
||||
elif subagent_chat_ids:
|
||||
combined_meta['subagent_chat_ids'] = subagent_chat_ids
|
||||
|
||||
reuse_message = len(batch) == 1 and not (first.get('meta') or {}).get('async_subagent_pending')
|
||||
pending_flag = 'timer_pending' if kind == 'timer' else 'async_subagent_pending'
|
||||
reuse_message = len(batch) == 1 and not (first.get('meta') or {}).get(pending_flag)
|
||||
user_message_id = first['id'] if reuse_message else str(uuid4())
|
||||
if not reuse_message:
|
||||
removed_ids = {message['id'] for message in batch}
|
||||
@@ -211,12 +245,13 @@ async def process_pending_subagent_results(
|
||||
'id': assistant_message_id,
|
||||
'parent_id': parent_id,
|
||||
'user_message': user_message,
|
||||
'session_id': run.get('session_id') or f'subagent-result:{parent_chat_id}',
|
||||
'session_id': run.get('session_id') or f'{kind}-result:{parent_chat_id}',
|
||||
'background_tasks': {},
|
||||
'tool_ids': run.get('tool_ids') or [],
|
||||
'skill_ids': run.get('skill_ids') or [],
|
||||
'filter_ids': run.get('filter_ids') or [],
|
||||
'features': run.get('features') or {},
|
||||
'files': run.get('files') or [],
|
||||
'variables': run.get('variables') or {},
|
||||
}
|
||||
if run.get('terminal_id'):
|
||||
@@ -582,7 +617,7 @@ async def delegate(
|
||||
room=f'user:{user.id}',
|
||||
)
|
||||
if not await has_active_tasks(request.app.state.redis, parent_chat_id):
|
||||
await process_pending_subagent_results(request, parent_chat_id, user.id, run)
|
||||
await process_pending_internal_messages(request, parent_chat_id, user.id, run)
|
||||
if cancelled:
|
||||
raise asyncio.CancelledError
|
||||
return result
|
||||
|
||||
@@ -0,0 +1,355 @@
|
||||
"""Durable one-shot timers backed by internal child chats."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import copy
|
||||
import json
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from datetime import datetime, timezone
|
||||
from typing import Literal
|
||||
from uuid import uuid4
|
||||
|
||||
from fastapi import Request
|
||||
from sqlalchemy import select
|
||||
from starlette.datastructures import Headers
|
||||
|
||||
from open_webui.internal.db import get_async_db
|
||||
from open_webui.models.chat_messages import ChatMessages
|
||||
from open_webui.models.chats import Chat, ChatForm, Chats
|
||||
from open_webui.models.users import UserModel, Users
|
||||
from open_webui.tasks import has_active_tasks
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
_RELATIVE_TIME = re.compile(r"^(?:\+|in\s+)?(\d+)\s*(s|sec(?:onds?)?|m|min(?:utes?)?|h|hours?|d|days?)$")
|
||||
_RFC3339_TIME = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?(?:Z|[+-]\d{2}:\d{2})$")
|
||||
_TIME_UNITS_NS = {
|
||||
's': 1_000_000_000,
|
||||
'm': 60 * 1_000_000_000,
|
||||
'h': 60 * 60 * 1_000_000_000,
|
||||
'd': 24 * 60 * 60 * 1_000_000_000,
|
||||
}
|
||||
_timer_locks: dict[str, asyncio.Lock] = {}
|
||||
|
||||
|
||||
def parse_timer_at(value: str) -> int:
|
||||
"""Normalize a relative offset or timezone-aware RFC 3339 timestamp."""
|
||||
raw = value.strip()
|
||||
now = time.time_ns()
|
||||
relative = _RELATIVE_TIME.fullmatch(raw.lower())
|
||||
if relative:
|
||||
count = int(relative.group(1))
|
||||
if count <= 0:
|
||||
raise ValueError('at must be in the future.')
|
||||
return now + count * _TIME_UNITS_NS[relative.group(2)[0]]
|
||||
|
||||
if not _RFC3339_TIME.fullmatch(raw):
|
||||
raise ValueError(
|
||||
'at must be a relative time such as 10s or in 10 seconds, '
|
||||
'or an RFC 3339 timestamp with a timezone.'
|
||||
)
|
||||
try:
|
||||
parsed = datetime.fromisoformat(raw.replace('Z', '+00:00'))
|
||||
except ValueError as exc:
|
||||
raise ValueError(
|
||||
'at must be a relative time such as 10s or in 10 seconds, '
|
||||
'or an RFC 3339 timestamp with a timezone.'
|
||||
) from exc
|
||||
if parsed.tzinfo is None or parsed.utcoffset() is None:
|
||||
raise ValueError('absolute at values must include an explicit timezone.')
|
||||
|
||||
due_at = int(parsed.timestamp() * 1_000_000_000)
|
||||
if due_at <= now:
|
||||
raise ValueError('at must be in the future.')
|
||||
return due_at
|
||||
|
||||
|
||||
async def create_timer(
|
||||
*,
|
||||
prompt: str,
|
||||
at: str,
|
||||
cancel_on: list[Literal['chat.read', 'chat.user_message']] | None,
|
||||
request: Request,
|
||||
user_data: dict,
|
||||
metadata: dict,
|
||||
parent_chat_id: str,
|
||||
parent_message_id: str | None,
|
||||
) -> str:
|
||||
prompt = prompt.strip()
|
||||
if not prompt:
|
||||
return 'Error: prompt must not be empty.'
|
||||
if not parent_chat_id or not user_data.get('id'):
|
||||
return 'Error: chat and user context are required.'
|
||||
|
||||
try:
|
||||
due_at = parse_timer_at(at)
|
||||
except ValueError as exc:
|
||||
return f'Error: {exc}'
|
||||
|
||||
selected_events = cancel_on or []
|
||||
allowed_events = {'chat.read', 'chat.user_message'}
|
||||
if any(event not in allowed_events for event in selected_events):
|
||||
return 'Error: cancel_on accepts only chat.read and chat.user_message.'
|
||||
selected_events = list(dict.fromkeys(selected_events))
|
||||
|
||||
model_id = metadata.get('model_id') or (metadata.get('model') or {}).get('id')
|
||||
if not model_id:
|
||||
return 'Error: model context is required.'
|
||||
if metadata.get('direct'):
|
||||
return 'Error: timers are unavailable for direct connections.'
|
||||
|
||||
chat_id = str(uuid4())
|
||||
user_message_id = str(uuid4())
|
||||
user = UserModel(**user_data)
|
||||
run = {
|
||||
'model_id': model_id,
|
||||
'session_id': metadata.get('session_id'),
|
||||
'tool_ids': copy.deepcopy(metadata.get('tool_ids') or []),
|
||||
'skill_ids': copy.deepcopy(metadata.get('skill_ids') or []),
|
||||
'system_prompt': metadata.get('system_prompt'),
|
||||
'filter_ids': copy.deepcopy(metadata.get('filter_ids') or []),
|
||||
'terminal_id': metadata.get('terminal_id'),
|
||||
'features': copy.deepcopy(metadata.get('features') or {}),
|
||||
'files': copy.deepcopy(metadata.get('files') or []),
|
||||
'variables': copy.deepcopy(metadata.get('variables') or {}),
|
||||
}
|
||||
|
||||
chat = await Chats.insert_new_chat(
|
||||
chat_id,
|
||||
user.id,
|
||||
ChatForm(
|
||||
chat={
|
||||
'id': chat_id,
|
||||
'title': f'Timer: {prompt[:60]}',
|
||||
'models': [model_id],
|
||||
'history': {
|
||||
'currentId': user_message_id,
|
||||
'messages': {
|
||||
user_message_id: {
|
||||
'id': user_message_id,
|
||||
'parentId': None,
|
||||
'childrenIds': [],
|
||||
'role': 'user',
|
||||
'content': prompt,
|
||||
'timestamp': int(time.time()),
|
||||
'models': [model_id],
|
||||
},
|
||||
},
|
||||
},
|
||||
'messages': [{'role': 'user', 'content': prompt}],
|
||||
}
|
||||
),
|
||||
internal_meta={
|
||||
'internal': True,
|
||||
'type': 'timer',
|
||||
'parent_chat_id': parent_chat_id,
|
||||
'parent_message_id': parent_message_id,
|
||||
'timer_at': due_at,
|
||||
'timer_status': 'pending',
|
||||
'timer_model_id': model_id,
|
||||
'timer_task_message_id': user_message_id,
|
||||
'cancel_on': selected_events,
|
||||
'run': run,
|
||||
},
|
||||
)
|
||||
if not chat:
|
||||
return 'Error: failed to create timer.'
|
||||
|
||||
return json.dumps(
|
||||
{
|
||||
'status': 'set',
|
||||
'at': datetime.fromtimestamp(due_at / 1_000_000_000, timezone.utc).isoformat().replace('+00:00', 'Z'),
|
||||
'cancel_on': selected_events,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
|
||||
|
||||
async def claim_due_timers(now_ns: int, limit: int = 10) -> list[tuple[str, str]]:
|
||||
"""Claim due timers by moving them from pending to running."""
|
||||
async with get_async_db() as db:
|
||||
stmt = (
|
||||
select(Chat)
|
||||
.where(Chat.meta['internal'].as_boolean().is_(True))
|
||||
.where(Chat.meta['type'].as_string() == 'timer')
|
||||
.where(Chat.meta['timer_status'].as_string() == 'pending')
|
||||
)
|
||||
if db.bind.dialect.name == 'postgresql':
|
||||
stmt = stmt.with_for_update(skip_locked=True)
|
||||
|
||||
result = await db.execute(stmt)
|
||||
rows = [
|
||||
row
|
||||
for row in result.scalars().all()
|
||||
if int((row.meta or {}).get('timer_at') or 0) <= now_ns
|
||||
]
|
||||
rows.sort(key=lambda row: int((row.meta or {}).get('timer_at') or 0))
|
||||
rows = rows[:limit]
|
||||
|
||||
claimed = []
|
||||
for row in rows:
|
||||
claim_id = str(uuid4())
|
||||
row.meta = {
|
||||
**(row.meta or {}),
|
||||
'timer_status': 'running',
|
||||
'timer_started_at': now_ns,
|
||||
'timer_claim_id': claim_id,
|
||||
}
|
||||
row.updated_at = int(time.time())
|
||||
claimed.append((row.id, claim_id))
|
||||
await db.commit()
|
||||
return claimed
|
||||
|
||||
|
||||
async def cancel_timers_for_chat(parent_chat_id: str, event: Literal['chat.read', 'chat.user_message']) -> None:
|
||||
async with get_async_db() as db:
|
||||
result = await db.execute(
|
||||
select(Chat)
|
||||
.where(Chat.meta['internal'].as_boolean().is_(True))
|
||||
.where(Chat.meta['type'].as_string() == 'timer')
|
||||
.where(Chat.meta['parent_chat_id'].as_string() == parent_chat_id)
|
||||
.where(Chat.meta['timer_status'].as_string() == 'pending')
|
||||
)
|
||||
now_ns = int(time.time_ns())
|
||||
for row in result.scalars().all():
|
||||
meta = row.meta or {}
|
||||
if event not in (meta.get('cancel_on') or []):
|
||||
continue
|
||||
row.meta = {
|
||||
**meta,
|
||||
'timer_status': 'cancelled',
|
||||
'timer_cancelled_at': now_ns,
|
||||
'timer_cancelled_by': event,
|
||||
}
|
||||
row.updated_at = int(time.time())
|
||||
await db.commit()
|
||||
|
||||
|
||||
async def execute_due_timer(app, timer_id: str, claim_id: str | None = None) -> None:
|
||||
lock = _timer_locks.setdefault(timer_id, asyncio.Lock())
|
||||
async with lock:
|
||||
from open_webui.socket.main import sio
|
||||
from open_webui.utils.subagents import _parent_locks, process_pending_internal_messages
|
||||
|
||||
timer = await Chats.get_chat_by_id(timer_id)
|
||||
if not timer:
|
||||
return
|
||||
meta = timer.meta or {}
|
||||
if meta.get('timer_status') != 'running':
|
||||
return
|
||||
if claim_id is not None and meta.get('timer_claim_id') != claim_id:
|
||||
return
|
||||
|
||||
parent_chat_id = meta.get('parent_chat_id') or ''
|
||||
parent = await Chats.get_chat_by_id_and_user_id(parent_chat_id, timer.user_id)
|
||||
if not parent:
|
||||
await _set_timer_status(timer_id, 'error', timer_error='parent chat no longer exists')
|
||||
return
|
||||
|
||||
prompt_message_id = meta.get('timer_task_message_id')
|
||||
prompt_message = await Chats.get_message_by_id_and_message_id(timer_id, prompt_message_id)
|
||||
if not prompt_message:
|
||||
await _set_timer_status(timer_id, 'error', timer_error='timer task message is missing')
|
||||
return
|
||||
|
||||
user = await Users.get_user_by_id(timer.user_id)
|
||||
if not user:
|
||||
await _set_timer_status(timer_id, 'error', timer_error='timer user no longer exists')
|
||||
return
|
||||
|
||||
run = meta.get('run') or {}
|
||||
model_id = run.get('model_id') or meta.get('timer_model_id')
|
||||
if not model_id:
|
||||
await _set_timer_status(timer_id, 'error', timer_error='model context is missing')
|
||||
return
|
||||
|
||||
prompt = prompt_message.get('content') or ''
|
||||
if isinstance(prompt, list):
|
||||
prompt = ''.join(
|
||||
str(part.get('text', '')) for part in prompt if isinstance(part, dict) and part.get('type') == 'text'
|
||||
)
|
||||
|
||||
user_message_id = str(uuid4())
|
||||
parent_lock = _parent_locks.setdefault(parent_chat_id, asyncio.Lock())
|
||||
async with parent_lock:
|
||||
parent = await Chats.get_chat_by_id_and_user_id(parent_chat_id, timer.user_id)
|
||||
if not parent:
|
||||
await _set_timer_status(timer_id, 'error', timer_error='parent chat no longer exists')
|
||||
return
|
||||
parent_chat = copy.deepcopy(parent.chat or {})
|
||||
history = parent_chat.setdefault('history', {})
|
||||
messages = history.setdefault('messages', {})
|
||||
done_assistants = [
|
||||
message
|
||||
for message in messages.values()
|
||||
if message.get('role') == 'assistant' and message.get('done') is not False
|
||||
]
|
||||
parent_id = (
|
||||
max(done_assistants, key=lambda message: message.get('timestamp', 0)).get('id')
|
||||
if done_assistants
|
||||
else meta.get('parent_message_id')
|
||||
)
|
||||
pending_meta = {'internal': True, 'type': 'timer', 'timer_id': timer_id}
|
||||
if await has_active_tasks(app.state.redis, parent_chat_id):
|
||||
pending_meta['timer_pending'] = True
|
||||
|
||||
user_message = {
|
||||
'id': user_message_id,
|
||||
'parentId': parent_id,
|
||||
'childrenIds': [],
|
||||
'role': 'user',
|
||||
'content': prompt,
|
||||
'model': model_id,
|
||||
'meta': pending_meta,
|
||||
'timestamp': int(time.time()),
|
||||
}
|
||||
messages[user_message_id] = user_message
|
||||
if parent_id and parent_id in messages:
|
||||
children = messages[parent_id].setdefault('childrenIds', [])
|
||||
if user_message_id not in children:
|
||||
children.append(user_message_id)
|
||||
await Chats.update_chat_by_id(parent_chat_id, parent_chat)
|
||||
await ChatMessages.upsert_message(user_message_id, parent_chat_id, timer.user_id, user_message)
|
||||
await _set_timer_status(timer_id, 'dispatched', timer_dispatched_at=int(time.time_ns()))
|
||||
|
||||
if user_message['meta'].get('timer_pending') is True:
|
||||
await sio.emit(
|
||||
'events',
|
||||
{
|
||||
'chat_id': parent_chat_id,
|
||||
'message_id': user_message_id,
|
||||
'data': {'type': 'chat:reload'},
|
||||
},
|
||||
room=f'user:{timer.user_id}',
|
||||
)
|
||||
elif not await has_active_tasks(app.state.redis, parent_chat_id):
|
||||
request = Request(
|
||||
{
|
||||
'type': 'http',
|
||||
'asgi': {'version': '3.0', 'spec_version': '2.0'},
|
||||
'method': 'POST',
|
||||
'path': '/api/v1/timers/internal',
|
||||
'query_string': b'',
|
||||
'headers': Headers({}).raw,
|
||||
'client': ('127.0.0.1', 0),
|
||||
'server': ('127.0.0.1', 80),
|
||||
'scheme': 'http',
|
||||
'app': app,
|
||||
}
|
||||
)
|
||||
request.state.token = None
|
||||
request.state.enable_api_keys = False
|
||||
await process_pending_internal_messages(request, parent_chat_id, user.id, run)
|
||||
|
||||
|
||||
async def _set_timer_status(timer_id: str, status: str, **fields) -> None:
|
||||
async with get_async_db() as db:
|
||||
row = await db.get(Chat, timer_id)
|
||||
if not row:
|
||||
return
|
||||
row.meta = {**(row.meta or {}), 'timer_status': status, **fields}
|
||||
row.updated_at = int(time.time())
|
||||
await db.commit()
|
||||
@@ -82,6 +82,7 @@ from open_webui.tools.builtin import (
|
||||
search_memories,
|
||||
search_notes,
|
||||
search_web,
|
||||
timer,
|
||||
toggle_automation,
|
||||
update_automation,
|
||||
update_calendar_event,
|
||||
@@ -574,7 +575,7 @@ async def get_builtin_tools(
|
||||
and getattr(request.state, 'internal', False) is not True
|
||||
and getattr(request.state, 'direct', False) is not True
|
||||
):
|
||||
builtin_functions.append(delegate_task)
|
||||
builtin_functions.extend([delegate_task, timer])
|
||||
|
||||
# Add memory tools when memory is enabled and the model allows this builtin category.
|
||||
if (
|
||||
|
||||
@@ -22,14 +22,6 @@
|
||||
import SubagentResultRow from './SubagentResultRow.svelte';
|
||||
|
||||
const i18n = getContext('i18n');
|
||||
type SubagentResult = {
|
||||
async_subagent_result: true;
|
||||
delegation_id?: string;
|
||||
delegation_ids?: string[];
|
||||
subagent_chat_id?: string;
|
||||
subagent_chat_ids?: string[];
|
||||
};
|
||||
|
||||
export let user;
|
||||
|
||||
export let chatId;
|
||||
@@ -62,7 +54,7 @@
|
||||
let editScrollContainer: HTMLDivElement;
|
||||
|
||||
let message = structuredClone(history.messages[messageId]);
|
||||
let subagentResult: SubagentResult | undefined;
|
||||
let timerExpanded = false;
|
||||
$: if (history.messages) {
|
||||
const source = history.messages[messageId];
|
||||
if (source) {
|
||||
@@ -73,8 +65,6 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
$: subagentResult = message?.meta?.async_subagent_result ? message.meta : undefined;
|
||||
|
||||
const copyToClipboard = async (text) => {
|
||||
const res = await _copyToClipboard(text);
|
||||
if (res) {
|
||||
@@ -143,7 +133,7 @@
|
||||
id="message-{message.id}"
|
||||
style="scroll-margin-top: 3rem;"
|
||||
>
|
||||
{#if !($settings?.chatBubble ?? true) && !subagentResult}
|
||||
{#if !($settings?.chatBubble ?? true) && !message?.meta?.async_subagent_result && !(message?.meta?.internal === true && message?.meta?.type === 'timer')}
|
||||
<div class={`shrink-0 ltr:mr-2 rtl:ml-2 hidden @lg:flex mt-0.5`}>
|
||||
<ProfileImage
|
||||
src={user?.id
|
||||
@@ -153,8 +143,13 @@
|
||||
/>
|
||||
</div>
|
||||
{/if}
|
||||
<div class="flex-auto w-0 max-w-full {subagentResult ? '' : 'pl-1'}">
|
||||
{#if !($settings?.chatBubble ?? true) && !subagentResult}
|
||||
<div
|
||||
class="flex-auto w-0 max-w-full {message?.meta?.async_subagent_result ||
|
||||
(message?.meta?.internal === true && message?.meta?.type === 'timer')
|
||||
? ''
|
||||
: 'pl-1'}"
|
||||
>
|
||||
{#if !($settings?.chatBubble ?? true) && !message?.meta?.async_subagent_result && !(message?.meta?.internal === true && message?.meta?.type === 'timer')}
|
||||
<div>
|
||||
<Name>
|
||||
{#if message.user}
|
||||
@@ -335,8 +330,53 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{:else if subagentResult}
|
||||
<SubagentResultRow content={message.content} result={subagentResult} />
|
||||
{:else if message?.meta?.internal === true && message?.meta?.type === 'timer'}
|
||||
<div class="flex justify-start">
|
||||
<div
|
||||
class="w-full max-w-3xl rounded-xl border border-gray-100 bg-gray-50/70 px-3 py-2 text-gray-700 dark:border-gray-800 dark:bg-gray-900/60 dark:text-gray-300"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full items-center justify-between gap-3 text-left"
|
||||
aria-expanded={timerExpanded}
|
||||
on:click={() => {
|
||||
timerExpanded = !timerExpanded;
|
||||
}}
|
||||
>
|
||||
<span class="text-xs font-medium text-gray-500 dark:text-gray-400">{$i18n.t('Timer')}</span>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
fill="none"
|
||||
viewBox="0 0 24 24"
|
||||
stroke-width="2"
|
||||
stroke="currentColor"
|
||||
class="size-3.5 shrink-0 text-gray-400 transition-transform dark:text-gray-600 {timerExpanded
|
||||
? 'rotate-180'
|
||||
: ''}"
|
||||
>
|
||||
<path stroke-linecap="round" stroke-linejoin="round" d="m19.5 8.25-7.5 7.5-7.5-7.5" />
|
||||
</svg>
|
||||
</button>
|
||||
{#if timerExpanded}
|
||||
<div class="mt-2 text-sm">
|
||||
{#if $settings?.renderMarkdownInUserMessages ?? true}
|
||||
<Markdown
|
||||
id={`${chatId}-${message.id}`}
|
||||
content={message.content}
|
||||
{editCodeBlock}
|
||||
{topPadding}
|
||||
/>
|
||||
{:else}
|
||||
<div class="whitespace-pre-wrap" dir={$settings?.chatDirection ?? 'auto'}>
|
||||
{message.content}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
{:else if message?.meta?.async_subagent_result}
|
||||
<SubagentResultRow content={message.content} result={message.meta} />
|
||||
{:else if message.content !== ''}
|
||||
<div class="w-full">
|
||||
<div class="flex {($settings?.chatBubble ?? true) ? 'justify-end pb-1' : 'w-full'}">
|
||||
@@ -371,7 +411,7 @@
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if edit !== true && !subagentResult}
|
||||
{#if edit !== true && !message?.meta?.async_subagent_result && !(message?.meta?.internal === true && message?.meta?.type === 'timer')}
|
||||
<div
|
||||
class=" flex {($settings?.chatBubble ?? true)
|
||||
? 'justify-end'
|
||||
|
||||
Reference in New Issue
Block a user