Skip to content

Defer AI library imports to reduce process startup cost - #3683

Open
blarghmatey wants to merge 1 commit into
mainfrom
defer-ai-library-imports
Open

Defer AI library imports to reduce process startup cost#3683
blarghmatey wants to merge 1 commit into
mainfrom
defer-ai-library-imports

Conversation

@blarghmatey

Copy link
Copy Markdown
Member

What are the relevant tickets?

N/A

Description (What does it do?)

litellm, langchain_litellm, tiktoken, the opendataloader_pdf/cv2 (OpenCV) stack, and langchain_text_splitters were being imported at module scope in learning_resources/tasks.py, vector_search/tasks.py, learning_resources/etl/utils.py, and vector_search/utils.py.

Those modules sit on the import chain for ordinary web request handling:

  • learning_resources/tasks.py is imported by learning_resources/views.py
  • vector_search/utils.py is imported by learning_resources_search/api.py, learning_resources_search/indexing_api.py, and vector_search/views.py
  • learning_resources/etl/utils.py is imported by learning_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,F821 passes on all touched files (no unused imports, no unresolved names introduced by the deferred imports).
  • Ran the full affected test suite via 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.
  • Two tests in vector_search/utils_test.py that previously patched vector_search.utils.RecursiveCharacterTextSplitter (a module attribute that no longer exists once the import is lazy) were updated to patch langchain_text_splitters.RecursiveCharacterTextSplitter directly, and one test in learning_resources/tasks_test.py was updated to patch learning_resources.content_summarizer.ContentSummarizer (the defining module) instead of learning_resources.tasks.ContentSummarizer.
  • A reviewer can confirm the startup-cost reduction by checking that sys.modules does not contain litellm/cv2/opendataloader_pdf immediately after django.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.

Copilot AI review requested due to automatic review settings July 27, 2026 15:59
@github-actions

github-actions Bot commented Jul 27, 2026

Copy link
Copy Markdown

OpenAPI Changes

No changes detected

View full changelog

Unexpected changes? Ensure your branch is up-to-date with main (consider rebasing).

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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_splitters and ContentSummarizer imports in vector_search/utils.py into the specific helper functions that use them.
  • Deferred ContentSummarizer imports in Celery task modules (learning_resources/tasks.py, vector_search/tasks.py) to the call sites.
  • Deferred OpenDataLoaderLLMConverter import in learning_resources/etl/utils.py to the _pdf_to_markdown conversion 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.

Comment thread vector_search/tasks.py Outdated
@blarghmatey

Copy link
Copy Markdown
Member Author

Addressed review feedback: Sentry's bug-prediction finding in _missing_summaries() was verified as a real, pre-existing bug unrelated to this PR's import-timing changes — split out and fixed with regression tests in #3686. No other review comments outstanding. All checks (pytest, lint, CodeQL, GitGuardian, pre-commit.ci) are green.

blarghmatey added a commit that referenced this pull request Jul 27, 2026
…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>
@blarghmatey
blarghmatey force-pushed the defer-ai-library-imports branch from 928b424 to dd010ba Compare July 27, 2026 20:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants