|
|
|
import asyncio |
|
from typing import Dict |
|
from uuid import uuid4 |
|
import json |
|
from redis.asyncio import Redis |
|
from fastapi import Request |
|
from typing import Dict, List, Optional |
|
|
|
|
|
tasks: Dict[str, asyncio.Task] = {} |
|
chat_tasks = {} |
|
|
|
|
|
REDIS_TASKS_KEY = "open-webui:tasks" |
|
REDIS_CHAT_TASKS_KEY = "open-webui:tasks:chat" |
|
REDIS_PUBSUB_CHANNEL = "open-webui:tasks:commands" |
|
|
|
|
|
def is_redis(request: Request) -> bool: |
|
|
|
return hasattr(request.app.state, "redis") and (request.app.state.redis is not None) |
|
|
|
|
|
async def redis_task_command_listener(app): |
|
redis: Redis = app.state.redis |
|
pubsub = redis.pubsub() |
|
await pubsub.subscribe(REDIS_PUBSUB_CHANNEL) |
|
|
|
async for message in pubsub.listen(): |
|
if message["type"] != "message": |
|
continue |
|
try: |
|
command = json.loads(message["data"]) |
|
if command.get("action") == "stop": |
|
task_id = command.get("task_id") |
|
local_task = tasks.get(task_id) |
|
if local_task: |
|
local_task.cancel() |
|
except Exception as e: |
|
print(f"Error handling distributed task command: {e}") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def redis_save_task(redis: Redis, task_id: str, chat_id: Optional[str]): |
|
pipe = redis.pipeline() |
|
pipe.hset(REDIS_TASKS_KEY, task_id, chat_id or "") |
|
if chat_id: |
|
pipe.sadd(f"{REDIS_CHAT_TASKS_KEY}:{chat_id}", task_id) |
|
await pipe.execute() |
|
|
|
|
|
async def redis_cleanup_task(redis: Redis, task_id: str, chat_id: Optional[str]): |
|
pipe = redis.pipeline() |
|
pipe.hdel(REDIS_TASKS_KEY, task_id) |
|
if chat_id: |
|
pipe.srem(f"{REDIS_CHAT_TASKS_KEY}:{chat_id}", task_id) |
|
if (await pipe.scard(f"{REDIS_CHAT_TASKS_KEY}:{chat_id}").execute())[-1] == 0: |
|
pipe.delete(f"{REDIS_CHAT_TASKS_KEY}:{chat_id}") |
|
await pipe.execute() |
|
|
|
|
|
async def redis_list_tasks(redis: Redis) -> List[str]: |
|
return list(await redis.hkeys(REDIS_TASKS_KEY)) |
|
|
|
|
|
async def redis_list_chat_tasks(redis: Redis, chat_id: str) -> List[str]: |
|
return list(await redis.smembers(f"{REDIS_CHAT_TASKS_KEY}:{chat_id}")) |
|
|
|
|
|
async def redis_send_command(redis: Redis, command: dict): |
|
await redis.publish(REDIS_PUBSUB_CHANNEL, json.dumps(command)) |
|
|
|
|
|
async def cleanup_task(request, task_id: str, id=None): |
|
""" |
|
Remove a completed or canceled task from the global `tasks` dictionary. |
|
""" |
|
if is_redis(request): |
|
await redis_cleanup_task(request.app.state.redis, task_id, id) |
|
|
|
tasks.pop(task_id, None) |
|
|
|
|
|
if id and task_id in chat_tasks.get(id, []): |
|
chat_tasks[id].remove(task_id) |
|
if not chat_tasks[id]: |
|
chat_tasks.pop(id, None) |
|
|
|
|
|
async def create_task(request, coroutine, id=None): |
|
""" |
|
Create a new asyncio task and add it to the global task dictionary. |
|
""" |
|
task_id = str(uuid4()) |
|
task = asyncio.create_task(coroutine) |
|
|
|
|
|
task.add_done_callback( |
|
lambda t: asyncio.create_task(cleanup_task(request, task_id, id)) |
|
) |
|
tasks[task_id] = task |
|
|
|
|
|
if chat_tasks.get(id): |
|
chat_tasks[id].append(task_id) |
|
else: |
|
chat_tasks[id] = [task_id] |
|
|
|
if is_redis(request): |
|
await redis_save_task(request.app.state.redis, task_id, id) |
|
|
|
return task_id, task |
|
|
|
|
|
async def list_tasks(request): |
|
""" |
|
List all currently active task IDs. |
|
""" |
|
if is_redis(request): |
|
return await redis_list_tasks(request.app.state.redis) |
|
return list(tasks.keys()) |
|
|
|
|
|
async def list_task_ids_by_chat_id(request, id): |
|
""" |
|
List all tasks associated with a specific ID. |
|
""" |
|
if is_redis(request): |
|
return await redis_list_chat_tasks(request.app.state.redis, id) |
|
return chat_tasks.get(id, []) |
|
|
|
|
|
async def stop_task(request, task_id: str): |
|
""" |
|
Cancel a running task and remove it from the global task list. |
|
""" |
|
if is_redis(request): |
|
|
|
await redis_send_command( |
|
request.app.state.redis, |
|
{ |
|
"action": "stop", |
|
"task_id": task_id, |
|
}, |
|
) |
|
|
|
return {"status": True, "message": f"Stop signal sent for {task_id}"} |
|
|
|
task = tasks.get(task_id) |
|
if not task: |
|
raise ValueError(f"Task with ID {task_id} not found.") |
|
|
|
task.cancel() |
|
try: |
|
await task |
|
except asyncio.CancelledError: |
|
|
|
tasks.pop(task_id, None) |
|
return {"status": True, "message": f"Task {task_id} successfully stopped."} |
|
|
|
return {"status": False, "message": f"Failed to stop task {task_id}."} |
|
|