Skip to content

fix(indexer): wire contextualization into hexagonal pool - #524

Merged
hedhoud merged 4 commits into
refactor/hexagonalfrom
fix/contextualization-indexer-wiring
Jun 19, 2026
Merged

fix(indexer): wire contextualization into hexagonal pool#524
hedhoud merged 4 commits into
refactor/hexagonalfrom
fix/contextualization-indexer-wiring

Conversation

@hedhoud

@hedhoud hedhoud commented Jun 19, 2026

Copy link
Copy Markdown
Collaborator

Context
Contextual retrieval was implemented in the refactored pipeline, but the production Ray indexer never provided the contextualizer dependency. That made contextualization silently no-op on refactor/hexagonal even when a partition preset enabled it.

Problem
This restores the missing composition link so the indexer can build contextualizers from named LLM endpoints, with a fallback to the legacy/global LLM config. It also handles OpenAI-compatible chat response payloads correctly before prepending context to chunks.

Validation

  • uv run ruff format --check openrag/ tests/
  • uv run ruff check openrag/ tests/
  • uv run pytest tests/unit/services/workers/test_pipeline_builder.py tests/unit/services/workers/test_indexer_worker.py tests/unit/services/orchestrators/test_indexing_service.py tests/unit/core/indexing/test_contextualize.py tests/unit/services/workers/test_indexer_pool.py
  • API_KEY=llm-key EMBEDDER_API_KEY=embed-key VLM_API_KEY=vlm-key RERANKER_API_KEY=rerank-key uv run --no-env-file pytest tests/unit

Closes #522

Summary by CodeRabbit

Release Notes

  • New Features

    • Enhanced support for multiple LLM response formats during contextualization, with robust fallback handling for malformed responses.
    • Improved concurrency management for LLM operations with configurable batch sizing.
  • Configuration

    • Contextualization settings now intelligently derive from global configuration when preset-specific values are not defined.

@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c3002cfb-33b8-44bf-a7a2-8ceb6be331a4

📥 Commits

Reviewing files that changed from the base of the PR and between 8aa7249 and 77769ab.

📒 Files selected for processing (2)
  • openrag/services/orchestrators/preset_service.py
  • tests/unit/services/orchestrators/test_preset_service.py
💤 Files with no reviewable changes (1)
  • openrag/services/orchestrators/preset_service.py

📝 Walkthrough

Walkthrough

ChunkContextualizer replaces max_concurrent/local Semaphore with batch_size plus an injected llm_semaphore context manager, and adds _chat_response_text to normalize LLM response shapes. IndexerPool gains _build_contextualizer_factory (named/global LLM endpoint resolution, cached, semaphore-gated) wired into build_indexing_pipeline. PresetService._finalize_seed now conditionally injects enable_contextualization from the env toggle only for the default indexation preset instead of hard-coding it.

Changes

Contextualization wiring, concurrency refactor, and preset-driven configuration

Layer / File(s) Summary
ChunkContextualizer batch concurrency and LLM response normalization
openrag/core/indexing/contextualize.py, tests/unit/core/indexing/test_contextualize.py
DEFAULT_MAX_CONCURRENT replaced by DEFAULT_BATCH_SIZE = 4; constructor now accepts batch_size and optional llm_semaphore (falls back to nullcontext()). _chat_response_text helper normalizes str, OpenAI-style dict, or falls back to "". Tests verify OpenAI-style response parsing and that the injected gate is held during chat() then released.
Contextualizer factory construction and IndexerPool wiring
openrag/services/workers/indexer_pool.py, tests/unit/services/workers/test_indexer_pool.py
_build_contextualizer_factory(cfg) loads the prompt template, resolves LLM endpoints from models.llm with "default" fallback to _global_llm_endpoint_config(cfg), wraps in a thread-safe cached factory with DistributedSemaphore, and returns None when no LLM config exists. IndexerPool.__init__ creates the factory and forwards it to build_indexing_pipeline. Four tests cover None-without-config, global fallback, named endpoint, and end-to-end wiring.
Preset-driven contextualization flag injection via _finalize_seed
openrag/services/orchestrators/preset_service.py, tests/unit/services/orchestrators/test_preset_service.py
Hard-coded enable_contextualization removed from _DEFAULT_SEEDS. _finalize_seed now accepts the preset name and injects enable_contextualization from chunker.contextual_retrieval only for the default indexation preset; named presets retain their explicit values. seed_defaults passes the name into _finalize_seed. Tests verify env propagation and existing-row preservation.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐇 A semaphore gone, a batch takes its place,
The contextualizer runs at a friendlier pace.
Named endpoints resolve, or the global one falls through,
Presets read the env flag — no hard-coding the queue.
Hop, hop — the pipeline is finally wired through! 🎉

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.56% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The PR title 'fix(indexer): wire contextualization into hexagonal pool' directly describes the main change: wiring the contextualization feature into the indexer_pool, restoring the missing connection.
Linked Issues check ✅ Passed The PR fully addresses #522: builds contextualizer_factory in indexer_pool.py [#522], wires it into build_indexing_pipeline [#522], aligns contextualization gates via preset/env flags [#522], and validates chunks carry [CONTEXT] blocks with proper testing [#522].
Out of Scope Changes check ✅ Passed All changes are scoped to implementing contextualization wiring and supporting infrastructure (ChunkContextualizer refactoring, semaphore injection, preset synchronization) directly required by #522.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/contextualization-indexer-wiring

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@hedhoud
hedhoud force-pushed the fix/contextualization-indexer-wiring branch from cc4f62a to a0b3df3 Compare June 19, 2026 07:55

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
tests/unit/core/indexing/test_contextualize.py (1)

13-20: ⚡ Quick win

Add branch coverage for all normalized chat payload shapes.

The new test validates only choices[0].message.content. Adding parametrized cases for choices[0].text, top-level content, and unsupported payload fallback would lock in the new helper behavior and prevent silent regressions.

Proposed test extension
 `@pytest.mark.asyncio`
 async def test_contextualizer_accepts_openai_style_chat_response():
@@
     assert result[0].context == "document-level context"
     assert result[0].content == "chunk body"
+
+
+@pytest.mark.parametrize(
+    ("response", "expected_context"),
+    [
+        ({"choices": [{"message": {"content": "document-level context"}}]}, "document-level context"),
+        ({"choices": [{"text": "legacy completion context"}]}, "legacy completion context"),
+        ({"content": "top-level context"}, "top-level context"),
+        ({"unexpected": True}, ""),
+    ],
+)
+@pytest.mark.asyncio
+async def test_contextualizer_normalizes_chat_response_shapes(response, expected_context):
+    class ShapeLLM:
+        async def chat(self, messages, **kwargs):
+            return response
+
+    contextualizer = ChunkContextualizer(ShapeLLM(), "System prompt")
+    result = await contextualizer.contextualize([Chunk(id="c1", text="chunk body", partition="p")])
+    assert result[0].context == expected_context
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/core/indexing/test_contextualize.py` around lines 13 - 20, The
test_contextualizer_accepts_openai_style_chat_response function currently only
covers one chat response payload format (choices[0].message.content).
Parametrize this test using pytest.mark.parametrize to add test cases for
alternative normalized payload shapes including choices[0].text, top-level
content, and an unsupported payload fallback case. Each parametrized case should
test the ChunkContextualizer with DictLLM configured to return the respective
payload shape and verify that contextualize correctly extracts the context from
each supported format while handling the unsupported format appropriately.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@tests/unit/core/indexing/test_contextualize.py`:
- Around line 13-20: The test_contextualizer_accepts_openai_style_chat_response
function currently only covers one chat response payload format
(choices[0].message.content). Parametrize this test using
pytest.mark.parametrize to add test cases for alternative normalized payload
shapes including choices[0].text, top-level content, and an unsupported payload
fallback case. Each parametrized case should test the ChunkContextualizer with
DictLLM configured to return the respective payload shape and verify that
contextualize correctly extracts the context from each supported format while
handling the unsupported format appropriately.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9a9e0eef-fc3e-460b-8cf6-c44f5c4ec0fb

📥 Commits

Reviewing files that changed from the base of the PR and between 14301a4 and cc4f62a.

📒 Files selected for processing (4)
  • openrag/core/indexing/contextualize.py
  • openrag/services/workers/indexer_pool.py
  • tests/unit/core/indexing/test_contextualize.py
  • tests/unit/services/workers/test_indexer_pool.py

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
tests/unit/services/workers/test_indexer_pool.py (1)

141-185: ⚡ Quick win

Consider adding test coverage for unknown LLM name.

The _build_contextualizer_factory should raise a KeyError when an unknown LLM name is requested (and no fallback is available), but this error path is not currently tested.

📝 Suggested test case
def test_build_contextualizer_factory_raises_on_unknown_name(tmp_path) -> None:
    from core.config.model_endpoints import ModelEndpointConfig
    from services.workers.indexer_pool import _build_contextualizer_factory

    (tmp_path / "chunk_contextualizer_tmpl.txt").write_text("Context prompt", encoding="utf-8")
    cfg = SimpleNamespace(
        models=SimpleNamespace(
            llm={
                "known": ModelEndpointConfig(
                    endpoint="http://llm.example/v1",
                    model_name="model",
                    timeout=30,
                    extra={"implementation": "vllm", "api_key": "key"},
                )
            }
        ),
        llm=SimpleNamespace(base_url="", model="", api_key=""),  # No fallback
        chunker=SimpleNamespace(contextualization_timeout=12, max_concurrent_contextualization=3),
        paths=SimpleNamespace(prompts_dir=str(tmp_path)),
        prompts=SimpleNamespace(chunk_contextualizer="chunk_contextualizer_tmpl.txt"),
    )

    factory = _build_contextualizer_factory(cfg)

    with pytest.raises(KeyError, match="Unknown llm 'unknown'"):
        factory("unknown")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@tests/unit/services/workers/test_indexer_pool.py` around lines 141 - 185, Add
a new test function to cover the error case for _build_contextualizer_factory
when an unknown LLM name is requested. Create a test function (e.g.,
test_build_contextualizer_factory_raises_on_unknown_name) that sets up a
configuration with a known LLM endpoint and no fallback settings, then verifies
that calling the factory with an unknown LLM name raises a KeyError with an
appropriate error message. Use pytest.raises to assert the exception is raised
with the expected error message pattern.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@tests/unit/services/workers/test_indexer_pool.py`:
- Line 156: The llm_registry registration of FakeLLM with key
"test-contextualizer-llm" persists after the test completes, causing potential
test pollution in the shared process. Add cleanup logic to remove the
"test-contextualizer-llm" registration from the llm_registry after the test
finishes, either by using a pytest fixture with an appropriate teardown/cleanup
method, or by wrapping the registration in a try/finally block that unregisters
the entry. Ensure the cleanup mechanism properly removes the registration to
prevent interference with other tests.

---

Nitpick comments:
In `@tests/unit/services/workers/test_indexer_pool.py`:
- Around line 141-185: Add a new test function to cover the error case for
_build_contextualizer_factory when an unknown LLM name is requested. Create a
test function (e.g., test_build_contextualizer_factory_raises_on_unknown_name)
that sets up a configuration with a known LLM endpoint and no fallback settings,
then verifies that calling the factory with an unknown LLM name raises a
KeyError with an appropriate error message. Use pytest.raises to assert the
exception is raised with the expected error message pattern.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 40cc105d-dba2-4d7e-a9f4-6c025e2ed682

📥 Commits

Reviewing files that changed from the base of the PR and between cc4f62a and a0b3df3.

📒 Files selected for processing (4)
  • openrag/core/indexing/contextualize.py
  • openrag/services/workers/indexer_pool.py
  • tests/unit/core/indexing/test_contextualize.py
  • tests/unit/services/workers/test_indexer_pool.py
🚧 Files skipped from review as they are similar to previous changes (3)
  • tests/unit/core/indexing/test_contextualize.py
  • openrag/core/indexing/contextualize.py
  • openrag/services/workers/indexer_pool.py

Comment thread tests/unit/services/workers/test_indexer_pool.py
… LLM semaphore

The hexagonal contextualizer called the LLM without sharing the cluster-wide
"llmSemaphore", so a large indexing job could flood the vLLM endpoint and run
uncapped against query-time calls. Inject the distributed semaphore into
ChunkContextualizer through an optional llm_semaphore gate that wraps _llm.chat,
built in the services-layer factory so core stays free of any Ray/services import.

Also harden the contextualizer factory test: unregister the FakeLLM from the
shared llm_registry in a finally block and drop the unused `instances` attribute.
In the refactored pipeline, contextualization is gated only by the per-partition
preset's enable_contextualization (default false); the global CONTEXTUAL_RETRIEVAL
(chunker.contextual_retrieval) flag was orphaned and never took effect. Wire it
back via two complementary paths:

- seed the default indexation preset's enable_contextualization from the flag,
  mirroring the existing reranker.enabled kill-switch in _finalize_seed; and
- re-sync the default preset from the flag on every boot (sync_env_toggles),
  so an existing deployment honours a changed env value after a restart.

Named presets (legal/finance) keep their explicit values.
@Ahmath-Gadji

Copy link
Copy Markdown
Collaborator

Pushed two follow-up commits on top of the contextualization wiring:

37649d74 — feat(indexer): rate-limit chunk contextualization via the distributed LLM semaphore
The hexagonal contextualizer was calling the LLM without sharing the cluster-wide llmSemaphore, so a large indexing job could flood the vLLM endpoint and run uncapped against query-time calls. _llm.chat is now wrapped by an optional llm_semaphore gate injected from the services-layer factory (DistributedSemaphore("llmSemaphore")), keeping core free of any Ray/services import (layer guard stays green).

8aa7249c — fix(presets): drive default contextualization from CONTEXTUAL_RETRIEVAL
Contextualization was gated only by the per-partition preset's enable_contextualization (default false); the global CONTEXTUAL_RETRIEVAL (chunker.contextual_retrieval) flag was orphaned and never took effect. Now (a) the default indexation preset inherits the flag at seed time (mirrors the reranker.enabled kill-switch), and (b) PresetService.sync_env_toggles re-applies it to the default preset on every boot, so existing deployments honour a changed env value after restart. Named presets (legal/finance) keep their explicit values.

Verified end-to-end on a live stack: forcing the preset to false + restart → boot re-sync flips it back to true; indexing then runs contextualization. Full unit suite: 1268 passed, 1 skipped; ruff + layer-import-guard green.

@coderabbitai coderabbitai Bot added the breaking-change Change of behavior after upgrade label Jun 19, 2026

@hedhoud hedhoud left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

Left one concern about the default preset ownership model.

Comment thread openrag/services/orchestrators/preset_service.py Outdated

@Ahmath-Gadji Ahmath-Gadji left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

LGTM. Tested end to end

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking-change Change of behavior after upgrade fix Fix issue

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants