Defer AI library imports to reduce process startup cost - #3683
Defer AI library imports to reduce process startup cost#3683blarghmatey wants to merge 1 commit into
Conversation
OpenAPI ChangesNo changes detected Unexpected changes? Ensure your branch is up-to-date with |
There was a problem hiding this comment.
Pull request overview
This PR reduces Django/Celery process startup time and memory footprint by deferring imports of heavyweight AI/OCR/text-splitting dependencies until the specific task/function that needs them is actually executed, avoiding eager module-scope imports on the web request import chain.
Changes:
- Moved
langchain_text_splittersandContentSummarizerimports invector_search/utils.pyinto the specific helper functions that use them. - Deferred
ContentSummarizerimports in Celery task modules (learning_resources/tasks.py,vector_search/tasks.py) to the call sites. - Deferred
OpenDataLoaderLLMConverterimport inlearning_resources/etl/utils.pyto the_pdf_to_markdownconversion function, and updated tests to patch the new import locations.
Reviewed changes
Copilot reviewed 6 out of 6 changed files in this pull request and generated no comments.
Show a summary per file
| File | Description |
|---|---|
| vector_search/utils.py | Lazily imports text splitters and ContentSummarizer within the functions that use them to avoid module-scope heavy imports. |
| vector_search/utils_test.py | Updates mocks/patch targets to match the new lazy-import locations. |
| vector_search/tasks.py | Defers ContentSummarizer import to the _missing_summaries helper. |
| learning_resources/tasks.py | Defers ContentSummarizer import to the summarize_content_files_task Celery task body. |
| learning_resources/tasks_test.py | Updates patch target to the defining module for ContentSummarizer now that the task module no longer exposes it. |
| learning_resources/etl/utils.py | Defers OpenDataLoaderLLMConverter import to _pdf_to_markdown to avoid pulling OCR/LLM stack into the general ETL import chain. |
|
Addressed review feedback: Sentry's bug-prediction finding in |
…ries (#3686) get_unprocessed_content_file_ids(self, overwrite, learning_resource_ids=None, content_file_ids=None) was being called with a queryset passed positionally, which landed in the `overwrite` parameter instead of `learning_resource_ids`. Because a non-empty queryset is truthy, this silently set overwrite=True, skipping the "only unprocessed" filter, and `learning_resource_ids` was never passed at all, so the require_summaries=True scoping was lost entirely. The embeddings_healthcheck Sentry alert this feeds was therefore counting already-summarized content files across every learning resource, not just missing summaries for resources that require them. Pass both arguments as keywords, and short-circuit when no learning resources currently require summaries, since get_unprocessed_content_file_ids treats an empty learning_resource_ids list the same as None (no restriction) rather than "match nothing". Flagged by Sentry's bug-prediction bot on #3683 as a pre-existing issue unrelated to that PR's changes. Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
litellm, langchain_litellm, tiktoken, opendataloader_pdf/cv2, and langchain_text_splitters were imported at module scope in tasks.py, etl/utils.py, and vector_search/utils.py. Those modules sit on the import chain for ordinary web request handling (views.py, learning_resources_search/api.py, vector_search/views.py), so every Django process paid the import cost even though the majority of code-paths never touch summarization, PDF-to-markdown conversion, or embedding chunking. Move each import to its single call site instead. Python's root logger config still captures these libraries' logs once they're actually used, since logger creation doesn't depend on import timing. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
928b424 to
dd010ba
Compare
What are the relevant tickets?
N/A
Description (What does it do?)
litellm,langchain_litellm,tiktoken, theopendataloader_pdf/cv2(OpenCV) stack, andlangchain_text_splitterswere being imported at module scope inlearning_resources/tasks.py,vector_search/tasks.py,learning_resources/etl/utils.py, andvector_search/utils.py.Those modules sit on the import chain for ordinary web request handling:
learning_resources/tasks.pyis imported bylearning_resources/views.pyvector_search/utils.pyis imported bylearning_resources_search/api.py,learning_resources_search/indexing_api.py, andvector_search/views.pylearning_resources/etl/utils.pyis imported bylearning_resources/tasks.py(and every ETL pipeline module)Because Python imports are eager, every Django process (web/ASGI workers, every Celery worker, every management command) paid the full memory and startup-time cost of loading these libraries at process boot — even though they're only actually used by a handful of specific Celery tasks (content summarization, PDF-to-markdown OCR conversion, embedding-time text chunking) that the majority of code-paths never touch.
This PR moves each of those imports from module scope down to the single call site that uses them, so the import only happens when the relevant task/function actually runs. Log capture for these libraries is unaffected — Python's root logger configuration applies regardless of when a module (and its logger) is created, so litellm/langchain output is still captured once that code path is active.
Screenshots (if appropriate):
N/A - no UI changes
How can this be tested?
ruff check --select F401,F821passes on all touched files (no unused imports, no unresolved names introduced by the deferred imports).docker compose run --rm web uv run pytest learning_resources/tasks_test.py learning_resources/content_summarizer_test.py learning_resources/converters/opendataloader_llm_converter_test.py vector_search/tasks_test.py vector_search/utils_test.py learning_resources/etl/utils_test.py— all 545 tests pass.vector_search/utils_test.pythat previously patchedvector_search.utils.RecursiveCharacterTextSplitter(a module attribute that no longer exists once the import is lazy) were updated to patchlangchain_text_splitters.RecursiveCharacterTextSplitterdirectly, and one test inlearning_resources/tasks_test.pywas updated to patchlearning_resources.content_summarizer.ContentSummarizer(the defining module) instead oflearning_resources.tasks.ContentSummarizer.sys.modulesdoes not containlitellm/cv2/opendataloader_pdfimmediately afterdjango.setup(), prior to any request being served.Additional Context
No functional behavior changes — this is purely an import-timing change. The libraries still fully load and function normally the first time their owning task/function runs; they're just no longer paid for by every process that never touches them.