Celery workers are sync by design. To call an async def function from a Celery task, bridge it with asyncio.run() if no loop exists, or grab the existing loop if you’re already in one. The pattern lets you reuse the same async RAG/DB code without rewriting it sync.
ingest_pdf_task is a sync Celery function; inside it await service.ingest_pdf(file_path) once the loop is bridged.
send_email_task reuses the same bridge for any future async-only task.
Keep the bridge tiny and named (run_until_complete) so it’s a one-line audit.
Celery broker -> worker -> run_until_complete -> RAGService.ingest_pdf()
Sync Celery and async business logic don’t fight — bridge the loop in one place and you’re done.