Skip to content

Fix LangChain hook tests failing when langchain is not installed#67237

Merged
kaxil merged 2 commits into
apache:mainfrom
astronomer:langchain-fix
May 20, 2026
Merged

Fix LangChain hook tests failing when langchain is not installed#67237
kaxil merged 2 commits into
apache:mainfrom
astronomer:langchain-fix

Conversation

@amoghrajesh
Copy link
Copy Markdown
Contributor


Was generative AI tooling used to co-author this PR?
  • Yes (please specify the tool below)

Fix to errors seen in: https://github.com/apache/airflow/actions/runs/26144521163/job/76898272728?pr=67041

Example:

_________________________________________________________________ TestGetChatModel.test_dispatches_via_init_chat_model_with_api_key ____________________________________________________________________
[gw1] linux -- Python 3.10.20 /usr/local/bin/python3
/usr/python/lib/python3.10/unittest/mock.py:1376: in patched
    with self.decoration_helper(patched,
/usr/python/lib/python3.10/contextlib.py:135: in __enter__
    return next(self.gen)
/usr/python/lib/python3.10/unittest/mock.py:1358: in decoration_helper
    arg = exit_stack.enter_context(patching)
/usr/python/lib/python3.10/contextlib.py:492: in enter_context
    result = _cm_type.__enter__(cm)
/usr/python/lib/python3.10/unittest/mock.py:1431: in __enter__
    self.target = self.getter()
/usr/python/lib/python3.10/unittest/mock.py:1618: in <lambda>
    getter = lambda: _importer(target)
/usr/python/lib/python3.10/unittest/mock.py:1257: in _importer
    thing = __import__(import_path)
E   ModuleNotFoundError: No module named 'langchain'

The LangChain hook tests fail in CI with ModuleNotFoundError: No module named 'langchain'. Langchain is an optional dep not installed in std test envs like Non DB tests.

Add an autouse fixture that pre populates sys.modules with MagicMock stubs for the relevant langchain modules before each test. This lets @patch resolve its targets without langchain installed, while keeping all tests always-running rather than skipping them.

The TestLangChainHookInit, TestGetUiFieldBehaviour, and TestResolveModelId classes have no langchain dependency and they anyways mocked most of it and are unaffected


  • Read the Pull Request Guidelines for more information. Note: commit author/co-author name and email in commits become permanently public when merged.
  • For fundamental code changes, an Airflow Improvement Proposal (AIP) is needed.
  • When adding dependency, check compliance with the ASF 3rd Party License Policy.
  • For significant user-facing changes create newsfragment: {pr_number}.significant.rst, in airflow-core/newsfragments. You can add this file in a follow-up commit after the PR is created so you know the PR number.

Comment thread providers/common/ai/tests/unit/common/ai/hooks/test_langchain.py
Copy link
Copy Markdown
Member

@kaxil kaxil left a comment

Choose a reason for hiding this comment

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

I'll want to find a different solution for this, but merging this to unblock other PRs

@kaxil kaxil merged commit fe1618f into apache:main May 20, 2026
86 of 87 checks passed
@kaxil kaxil deleted the langchain-fix branch May 20, 2026 15:50
kaxil added a commit that referenced this pull request May 21, 2026
…s, cloud URIs

Same playbook as #67192 (LangChain) and #67120 (DocumentLoader) plus
three LlamaIndex-specific architectural fixes:

Critical fixes
- Stop mutating LlamaIndex's global ``Settings`` singleton. The previous
  ``LlamaIndexHook.configure_settings()`` wrote ``Settings.embed_model``
  / ``Settings.llm`` process-wide, which leaks across concurrent tasks
  in the same worker. Replaced with per-call ``embed_model=`` /
  ``llm=`` parameters on ``VectorStoreIndex(...)`` and
  ``load_index_from_storage(...)``.
- Own ``llamaindex`` connection type instead of squatting on
  ``pydanticai``. Mirrors the LangChain / CrewAI fix.
- Remove ``documents`` from ``EmbeddingOperator.template_fields``.
  ``list[dict]`` doesn't survive Jinja stringification, and worse, a
  user document containing literal ``{{ var.value.api_key }}`` would
  leak secrets into the embedding store. Bind via ``loader.output``
  instead.

BYO embedding/LLM for non-OpenAI vendors
- LlamaIndex doesn't ship an ``init_chat_model`` / ``init_embedding_model``
  equivalent (verified in ``llama_index.core.embeddings.utils.resolve_embed_model``
  -- only ``"default"`` / ``"local"`` / ``"clip:"`` dispatch). The hook
  therefore covers OpenAI (matching LlamaIndex's own
  ``resolve_embed_model("default")`` behaviour) and operators accept a
  pre-built ``BaseEmbedding`` / ``LLM`` instance to bypass the hook for
  Cohere / Bedrock / Vertex / HuggingFace / etc.

Cloud-URI persistence
- ``EmbeddingOperator.persist_dir`` and
  ``RetrievalOperator.index_persist_dir`` accept storage URIs
  (``s3://``, ``gs://``, ``azure://``) resolved via
  ``ObjectStoragePath`` and fsspec, matching the merged
  ``DocumentLoaderOperator`` pattern.

Hook plumbing playbook (mirrors LangChain / CrewAI / DocumentLoader)
- ``conn_type = "llamaindex"`` + new ``connection-types`` entry in
  ``provider.yaml`` with ``embed_model`` / ``llm_model`` conn-fields.
- ``default_conn_name`` resolves at runtime via
  ``llm_conn_id: str | None = None``.
- ``_resolve_model`` honours ``conn.extra_dejson`` for parity with the
  sibling hooks (swallows ``JSONDecodeError``, applies secret masking).
- ``get_ui_field_behaviour`` added.
- ``[llamaindex]`` extra in ``pyproject.toml`` pinning
  ``llama-index-core``, ``llama-index-embeddings-openai``,
  ``llama-index-llms-openai`` (enough to back the hook's default
  OpenAI return values). Same in the ``dev`` group.

Misc operator/test fixes
- Wrap lazy ``llama_index`` imports with
  ``AirflowOptionalProviderFeatureException`` so missing extras surface
  cleanly.
- ``RetrievalOperator`` returns ``{"query": ..., "chunks": [...]}``
  (was ``"question"``) and ``chunks[*].node_id`` (was the misleading
  ``"source"`` key).
- ``RetrievalOperator`` raises ``FileNotFoundError`` with a "did you
  run EmbeddingOperator first?" hint when ``index_persist_dir`` is
  missing.
- All three test files get an autouse fixture stubbing
  ``llama_index.*`` in ``sys.modules`` so ``@patch`` resolves without
  ``llama-index-*`` packages installed in CI's non-DB test env
  (mirrors #67237).
- New ``example_llamaindex_hook.py`` with ``[START howto_*]`` markers
  for the docs to ``exampleinclude``.
kaxil added a commit that referenced this pull request May 21, 2026
* Add LlamaIndex operators to common.ai provider

 - Adds LlamaIndexHook to bridge Airflow connections to LlamaIndex's Settings singleton. Reuses the pydanticai connection type, supports separate
  embedding and LLM connections.
  - Adds EmbeddingOperator to chunk documents and produce embedding vectors via LlamaIndex's SentenceSplitter. Input is list[dict(text, metadata)]
  (same shape as DocumentLoaderOperator output), output includes chunks with vectors ready for downstream vector store ingest operators (pgvector,
  Pinecone, Weaviate).
  - Adds RetrievalOperator to load a persisted LlamaIndex index and perform similarity search. Output is scored chunks ready for synthesis via
  LLMOperator.

  Design notes

  All LlamaIndex imports are lazy (inside execute() / method bodies), so modules parse without llama-index installed. The hook currently hardcodes
  OpenAI embedding/LLM providers; a follow-up PR will refactor to use BaseAIHook for provider-agnostic model resolution when it lands.

  What's included

  ┌─────────────────────────────────────────┬──────────────────────────────────────────┐
  │                  File                   │                 Purpose                  │
  ├─────────────────────────────────────────┼──────────────────────────────────────────┤
  │ hooks/llamaindex.py                     │ Hook (~110 lines)                        │
  ├─────────────────────────────────────────┼──────────────────────────────────────────┤
  │ operators/llamaindex_embedding.py       │ EmbeddingOperator (~110 lines)           │
  ├─────────────────────────────────────────┼──────────────────────────────────────────┤
  │ operators/llamaindex_retrieval.py       │ RetrievalOperator (~90 lines)            │
  ├─────────────────────────────────────────┼──────────────────────────────────────────┤
  │ tests/.../test_llamaindex.py            │ 12 hook tests                            │
  ├─────────────────────────────────────────┼──────────────────────────────────────────┤
  │ tests/.../test_llamaindex_embedding.py  │ 10 operator tests                        │
  ├─────────────────────────────────────────┼──────────────────────────────────────────┤
  │ tests/.../test_llamaindex_retrieval.py  │ 8 operator tests                         │
  ├─────────────────────────────────────────┼──────────────────────────────────────────┤
  │ docs/hooks/llamaindex.rst               │ Hook docs                                │
  ├─────────────────────────────────────────┼──────────────────────────────────────────┤
  │ docs/operators/llamaindex_embedding.rst │ EmbeddingOperator docs                   │
  ├─────────────────────────────────────────┼──────────────────────────────────────────┤
  │ docs/operators/llamaindex_retrieval.rst │ RetrievalOperator docs                   │
  ├─────────────────────────────────────────┼──────────────────────────────────────────┤
  │ provider.yaml                           │ Integration, hook, operator registration │
  ├─────────────────────────────────────────┼──────────────────────────────────────────┤
  │ docs/index.rst                          │ LlamaIndex Hook in Guides toctree        │
  ├─────────────────────────────────────────┼──────────────────────────────────────────┤
  │ docs/operators/index.rst                │ Chooser table rows                       │
  └─────────────────────────────────────────┴──────────────────────────────────────────┘

  Test plan

  - uv run --project providers/common/ai pytest providers/common/ai/tests/unit/common/ai/hooks/test_llamaindex.py -xvs (12 tests)
  - uv run --project providers/common/ai pytest providers/common/ai/tests/unit/common/ai/operators/test_llamaindex_embedding.py
  providers/common/ai/tests/unit/common/ai/operators/test_llamaindex_retrieval.py -xvs (18 tests)
  - Hook: init defaults, separate embed_conn_id, connection kwargs extraction, embedding model, LLM, Settings configuration
  - EmbeddingOperator: output shape, chunking, index persistence, vector inclusion/omission, splitter params
  - RetrievalOperator: output shape, chunk keys, top_k forwarding, multiple results, storage context

  ---
  Was generative AI tooling used to co-author this PR?

  - Yes — Claude Code (Opus 4.6)

  Generated-by: Claude Code (Opus 4.6) following
  https://github.com/apache/airflow/blob/main/contributing-docs/05_pull_requests.rst#gen-ai-assisted-contributions

* Refactor LlamaIndex hook + operators: no Settings mutation, BYO models, cloud URIs

Same playbook as #67192 (LangChain) and #67120 (DocumentLoader) plus
three LlamaIndex-specific architectural fixes:

Critical fixes
- Stop mutating LlamaIndex's global ``Settings`` singleton. The previous
  ``LlamaIndexHook.configure_settings()`` wrote ``Settings.embed_model``
  / ``Settings.llm`` process-wide, which leaks across concurrent tasks
  in the same worker. Replaced with per-call ``embed_model=`` /
  ``llm=`` parameters on ``VectorStoreIndex(...)`` and
  ``load_index_from_storage(...)``.
- Own ``llamaindex`` connection type instead of squatting on
  ``pydanticai``. Mirrors the LangChain / CrewAI fix.
- Remove ``documents`` from ``EmbeddingOperator.template_fields``.
  ``list[dict]`` doesn't survive Jinja stringification, and worse, a
  user document containing literal ``{{ var.value.api_key }}`` would
  leak secrets into the embedding store. Bind via ``loader.output``
  instead.

BYO embedding/LLM for non-OpenAI vendors
- LlamaIndex doesn't ship an ``init_chat_model`` / ``init_embedding_model``
  equivalent (verified in ``llama_index.core.embeddings.utils.resolve_embed_model``
  -- only ``"default"`` / ``"local"`` / ``"clip:"`` dispatch). The hook
  therefore covers OpenAI (matching LlamaIndex's own
  ``resolve_embed_model("default")`` behaviour) and operators accept a
  pre-built ``BaseEmbedding`` / ``LLM`` instance to bypass the hook for
  Cohere / Bedrock / Vertex / HuggingFace / etc.

Cloud-URI persistence
- ``EmbeddingOperator.persist_dir`` and
  ``RetrievalOperator.index_persist_dir`` accept storage URIs
  (``s3://``, ``gs://``, ``azure://``) resolved via
  ``ObjectStoragePath`` and fsspec, matching the merged
  ``DocumentLoaderOperator`` pattern.

Hook plumbing playbook (mirrors LangChain / CrewAI / DocumentLoader)
- ``conn_type = "llamaindex"`` + new ``connection-types`` entry in
  ``provider.yaml`` with ``embed_model`` / ``llm_model`` conn-fields.
- ``default_conn_name`` resolves at runtime via
  ``llm_conn_id: str | None = None``.
- ``_resolve_model`` honours ``conn.extra_dejson`` for parity with the
  sibling hooks (swallows ``JSONDecodeError``, applies secret masking).
- ``get_ui_field_behaviour`` added.
- ``[llamaindex]`` extra in ``pyproject.toml`` pinning
  ``llama-index-core``, ``llama-index-embeddings-openai``,
  ``llama-index-llms-openai`` (enough to back the hook's default
  OpenAI return values). Same in the ``dev`` group.

Misc operator/test fixes
- Wrap lazy ``llama_index`` imports with
  ``AirflowOptionalProviderFeatureException`` so missing extras surface
  cleanly.
- ``RetrievalOperator`` returns ``{"query": ..., "chunks": [...]}``
  (was ``"question"``) and ``chunks[*].node_id`` (was the misleading
  ``"source"`` key).
- ``RetrievalOperator`` raises ``FileNotFoundError`` with a "did you
  run EmbeddingOperator first?" hint when ``index_persist_dir`` is
  missing.
- All three test files get an autouse fixture stubbing
  ``llama_index.*`` in ``sys.modules`` so ``@patch`` resolves without
  ``llama-index-*`` packages installed in CI's non-DB test env
  (mirrors #67237).
- New ``example_llamaindex_hook.py`` with ``[START howto_*]`` markers
  for the docs to ``exampleinclude``.

* Rename LlamaIndex operators with framework prefix; fold in #67189 RAG examples

Per Kaxil's review r3267387604: ``RetrievalOperator`` / ``EmbeddingOperator``
are too generic in the common.ai namespace -- they risk colliding when
other frameworks add their own embedding/retrieval operators. Renamed
both with the LlamaIndex prefix:

- ``EmbeddingOperator`` -> ``LlamaIndexEmbeddingOperator``
- ``RetrievalOperator`` -> ``LlamaIndexRetrievalOperator``

Renames applied across the two operator modules, three docs RSTs, the
two test files, both example DAGs, and the cross-refs in
``docs/operators/index.rst``, ``docs/hooks/llamaindex.rst``,
``docs/operators/document_loader.rst``, and ``docs/hooks/index.rst``.

Folds in #67189 (``example_llamaindex_rag.py``) which would otherwise
sit blocked waiting for this PR to merge. Rewritten for the new API:

- Uses the renamed classes
- Drops ``documents="{{ ti.xcom_pull(...) }}"`` Jinja templating
  (template_fields removed; bind via ``loader.output`` direct)
- Switches LlamaIndex operators to ``llamaindex_default`` conn (was
  ``pydanticai_default``); the synthesis-step ``LLMOperator`` keeps
  ``pydanticai_default`` because it's pydantic-ai-backed (different
  framework, intentional split documented in the module docstring)
- Adds explicit ``embed_model="text-embedding-3-small"`` to every
  embedding/retrieval call (new operator validation requires it)
- Fixes the string-reference task chains (``load >> "build_index"`` ->
  ``load >> build_index``) which weren't valid task dependencies

Closes #67189.

* Address code-review findings on LlamaIndex operators

- Fix ObjectStoragePath conn_id mangling: pass raw URI to LlamaIndex
  persist_dir= and supply target.fs separately. str(target) returns
  s3://<conn_id>@<bucket>/..., which fsspec misinterprets.
- Add documents / embed_model / embed_conn_id to template_fields so
  XComArg resolution fires. The previous "list[dict] doesn't survive
  stringification" rationale was wrong; Templater unwraps resolvables
  before Jinja.
- Default llm_conn_id to None on both operators; LlamaIndexHook
  resolves to default_conn_name at runtime. Hard-coding
  "llamaindex_default" undid the hook's careful runtime resolution.
- Add embed_conn_id pass-through for separate embedding credentials.
- Replace isinstance(str) duck-typing with hasattr-based BaseEmbedding
  check; raise TypeError with a clear pointer instead of letting an
  unresolved XComArg or random object explode later.
- Hoist 'import os' and 'from pathlib import Path' to module top.
- Pad RST title underlines and refresh docs/tests to match the new
  surface.

* Fix mypy on LlamaIndex embedding operator

- Pass persist_dir as a typed str arg to _persist so the existing
  None-narrowing # type: ignore comments can go away.
- Cast SentenceSplitter nodes to list[TextNode] for the .text access:
  the splitter only ever returns TextNode, but the base
  get_nodes_from_documents signature is typed as list[BaseNode].

* Install llama-index in tests instead of stubbing sys.modules

llama-index-core / -embeddings-openai / -llms-openai were declared in
the common.ai provider's dev dependency group but missing from uv.lock,
so CI never actually installed them. The tests papered over that by
faking out llama_index.* in sys.modules with MagicMocks.

Refresh uv.lock so the packages get installed, then drop the
sys.modules manipulation:

- test_llamaindex.py: remove the autouse _stub_llama_index_modules
  fixture entirely; @patch resolves against the real modules.
- test_llamaindex_embedding.py / test_llamaindex_retrieval.py: replace
  the _stub_li fixture (sys.modules setitem) with a smaller _li fixture
  that uses monkeypatch.setattr against real llama_index.core symbols.

* Apply ruff lint/format fixes

---------

Co-authored-by: Kaxil Naik <kaxilnaik@gmail.com>
kaxil added a commit that referenced this pull request May 21, 2026
crewai 1.14.x ships tight conservative pins (click~=8.1.7, tomli~=2.0.2,
plus a crewai-cli that pulls textual>=7.5 -> rich>=14.2) that conflict
with Airflow's resolved click 8.3+, tomli 2.4+, and FAB's transitive
rich<14. uv can satisfy the install via override-dependencies, but the
CI image build runs ``pip check`` (see
scripts/docker/install_airflow_when_building_images.sh) which treats
the metadata mismatch as fatal.

The cleanest fix is to keep crewai out of Airflow's resolution entirely:

- Remove the ``[project.optional-dependencies] crewai`` extra from the
  provider pyproject. There's now nothing for uv to resolve against
  crewai's pins -- the root override-dependencies and crewai-* cooldown
  bypass are no longer needed.
- Drop crewai from the provider's dev dependency group.
- Document in docs/hooks/crewai.rst that users ``pip install
  "crewai>=1.14.5"`` themselves after installing common-ai.
- Stub ``crewai`` in ``sys.modules`` for the hook tests (mirrors the
  langchain-stub pattern from #67237 -- justified for crewai because
  the pin conflict makes installation in CI infeasible).

The hook itself is unchanged; users who want it install crewai into
their own venv where they've accepted the pin cascade. The lock no
longer carries crewai / chromadb / onnxruntime entries.
kaxil added a commit that referenced this pull request May 21, 2026
crewai 1.14.x ships tight conservative pins (click~=8.1.7, tomli~=2.0.2,
plus a crewai-cli that pulls textual>=7.5 -> rich>=14.2) that conflict
with Airflow's resolved click 8.3+, tomli 2.4+, and FAB's transitive
rich<14. uv can satisfy the install via override-dependencies, but the
CI image build runs ``pip check`` (see
scripts/docker/install_airflow_when_building_images.sh) which treats
the metadata mismatch as fatal.

The cleanest fix is to keep crewai out of Airflow's resolution entirely:

- Remove the ``[project.optional-dependencies] crewai`` extra from the
  provider pyproject. There's now nothing for uv to resolve against
  crewai's pins -- the root override-dependencies and crewai-* cooldown
  bypass are no longer needed.
- Drop crewai from the provider's dev dependency group.
- Document in docs/hooks/crewai.rst that users ``pip install
  "crewai>=1.14.5"`` themselves after installing common-ai.
- Stub ``crewai`` in ``sys.modules`` for the hook tests (mirrors the
  langchain-stub pattern from #67237 -- justified for crewai because
  the pin conflict makes installation in CI infeasible).

The hook itself is unchanged; users who want it install crewai into
their own venv where they've accepted the pin cascade. The lock no
longer carries crewai / chromadb / onnxruntime entries.
kaxil added a commit that referenced this pull request May 22, 2026
crewai 1.14.x ships tight conservative pins (click~=8.1.7, tomli~=2.0.2,
plus a crewai-cli that pulls textual>=7.5 -> rich>=14.2) that conflict
with Airflow's resolved click 8.3+, tomli 2.4+, and FAB's transitive
rich<14. uv can satisfy the install via override-dependencies, but the
CI image build runs ``pip check`` (see
scripts/docker/install_airflow_when_building_images.sh) which treats
the metadata mismatch as fatal.

The cleanest fix is to keep crewai out of Airflow's resolution entirely:

- Remove the ``[project.optional-dependencies] crewai`` extra from the
  provider pyproject. There's now nothing for uv to resolve against
  crewai's pins -- the root override-dependencies and crewai-* cooldown
  bypass are no longer needed.
- Drop crewai from the provider's dev dependency group.
- Document in docs/hooks/crewai.rst that users ``pip install
  "crewai>=1.14.5"`` themselves after installing common-ai.
- Stub ``crewai`` in ``sys.modules`` for the hook tests (mirrors the
  langchain-stub pattern from #67237 -- justified for crewai because
  the pin conflict makes installation in CI infeasible).

The hook itself is unchanged; users who want it install crewai into
their own venv where they've accepted the pin cascade. The lock no
longer carries crewai / chromadb / onnxruntime entries.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants