Spaces:
Running
Running
# tasks.py | |
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 | |
# A dictionary to keep track of active tasks | |
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: | |
# Called everywhere a request is available to check Redis | |
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}") | |
### ------------------------------ | |
### REDIS-ENABLED HANDLERS | |
### ------------------------------ | |
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}") # Remove if empty set | |
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) # Remove the task if it exists | |
# If an ID is provided, remove the task from the chat_tasks dictionary | |
if id and task_id in chat_tasks.get(id, []): | |
chat_tasks[id].remove(task_id) | |
if not chat_tasks[id]: # If no tasks left for this ID, remove the entry | |
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()) # Generate a unique ID for the task | |
task = asyncio.create_task(coroutine) # Create the task | |
# Add a done callback for cleanup | |
task.add_done_callback( | |
lambda t: asyncio.create_task(cleanup_task(request, task_id, id)) | |
) | |
tasks[task_id] = task | |
# If an ID is provided, associate the task with that ID | |
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): | |
# PUBSUB: All instances check if they have this task, and stop if so. | |
await redis_send_command( | |
request.app.state.redis, | |
{ | |
"action": "stop", | |
"task_id": task_id, | |
}, | |
) | |
# Optionally check if task_id still in Redis a few moments later for feedback? | |
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() # Request task cancellation | |
try: | |
await task # Wait for the task to handle the cancellation | |
except asyncio.CancelledError: | |
# Task successfully canceled | |
tasks.pop(task_id, None) # Remove it from the dictionary | |
return {"status": True, "message": f"Task {task_id} successfully stopped."} | |
return {"status": False, "message": f"Failed to stop task {task_id}."} | |