Skip to content

Phase 11: add parser registration and DI provider bridge - #431

Merged
EnjoyBacon7 merged 13 commits into
refactor/hexagonalfrom
refactor/phase-11-person-b-di-infra
May 28, 2026
Merged

Phase 11: add parser registration and DI provider bridge#431
EnjoyBacon7 merged 13 commits into
refactor/hexagonalfrom
refactor/phase-11-person-b-di-infra

Conversation

@hedhoud

@hedhoud hedhoud commented May 28, 2026

Copy link
Copy Markdown
Collaborator

Context: Phase 11 moves OpenRAG toward a single composition root. This PR covers the low-conflict Person B side first so Person A can continue container work with stable registration and provider boundaries.\n\nProblem: parser implementations were not registered through a DI module, Ollama registration was implicit, and the provider bridge still only worked through request state. That made the next container lifecycle step harder to test and wire cleanly.\n\nExpected behavior: DI registration is explicit, provider access works both from FastAPI request state and controlled test/lifespan overrides, and current Phase 10 request behavior remains unchanged.

Summary by CodeRabbit

  • New Features

    • Added Ollama as an additional inference option.
    • Enabled a broad set of document parsers (audio, docs, email, HTML, images, markdown, PDF, presentations, text).
  • Improvements

    • Runtime configuration is injected per-request (reduces import-time side effects) and exposed from the container.
    • DI can resolve services from request or a process-level singleton; container exposes init state and ensures tracked inference clients are cleaned up on shutdown.
    • Component factories provide thread-safe, cached per-model clients.
    • Worker/bootstrap initialization deferred until explicit startup.
  • Tests

    • New and expanded tests covering parser and inference registration, DI lifecycle/availability, factory caching/concurrency, and worker/bootstrap behavior.

Review Change Stack

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review
📝 Walkthrough

Walkthrough

Adds parser registration and Ollama registration via import side-effects, a thread-safe cached component factory, ServiceContainer lifecycle and tracked shutdown, a process-level DI container bridge with initialization gating (HTTP 503), broad runtime DI migration to Depends(get_config), worker/bootstrap refactors, and tests for these behaviors.

Changes

DI Provider Bridge with Ollama and Parser Support

Layer / File(s) Summary
Parser registration setup and tests
openrag/di/parsers.py, openrag/di/test_inference.py
New module docstring and register_parsers() import multiple parser implementation modules to trigger side-effect registration. Tests verify parser identifiers are registered and that register_parsers() is idempotent.
Ollama support in LLM and embedder registration
openrag/di/embedders.py, openrag/di/llms.py, openrag/di/test_inference.py
register_llms() and register_embedders() now import services.inference.ollama_client to register Ollama alongside vllm; tests and ServiceContainer assertions updated to include "ollama" in registries.
Generic component factory and tests
openrag/di/factories.py, openrag/di/test_factories.py
Adds ModelEndpointConfig protocol and make_component_factory() implementing a thread-safe cached per-name factory with double-checked locking; tests cover caching, config/implementation selection, arg forwarding, extra_kwargs_fn, and concurrency.
ServiceContainer lifecycle and tracked shutdown
openrag/di/container.py, openrag/di/test_container.py
ServiceContainer gains _initialized, is_initialized, config, client tracking via _inference_clients, instance factory methods that _track() created clients, and expanded shutdown() to close tracked clients; tests validate lifecycle and shutdown behavior.
Process-level container bridge and tests
openrag/di/providers.py, openrag/di/test_container.py
Adds a thread-safe process global container (set_container()), makes get_container() and service getters accept optional request and lazily provide a container, introduces _require_initialized() (raises HTTP 503 until ready), adds get_config(), and updates __all__. Tests validate precedence (request over process), set_container() behavior, and 503 conditions.
API runtime DI & routers
openrag/api/*, openrag/api/routers/*, openrag/api/dependencies/*, openrag/api/schemas/*
Migrates many modules from import-time load_config() to Depends(get_config) or per-call defaults (default_factory), updates route/dependency signatures to accept injected config, and updates schema defaults to compute at instantiation.
Workers bootstrap & Ray actor refactor
openrag/services/workers/bootstrap.py, openrag/services/workers/parsers/*, openrag/di/workers.py, openrag/di/test_workers.py
Worker bootstrap now initializes explicitly with settings; Ray actor options/GPU allocations are computed at runtime; replaces decorator-based timeouts/retries with explicit helpers; adds tests to prevent import-time side effects.
Components & utils runtime config
openrag/components/*
Defers configuration to per-instance or per-call load_config(), including BaseLoader, chunker contextualization, prompt loading, token counting, and semaphore creation.
Logger change
openrag/utils/logger.py
Removes import-time config load; get_logger(config=None) now accepts optional config and lazily loads when omitted.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Possibly related PRs

  • linagora/openrag#430: Related auth dependency refactor (DEFAULT_FILE_QUOTA → Depends(get_config)) and test updates.
  • linagora/openrag#396: Prior DI extraction and container/provider wiring changes connected to this provider/container update.
  • linagora/openrag#304: Related work on retry/timeouts and ray_utils helpers used by parser worker refactors.

Suggested reviewers

  • Ahmath-Gadji
  • paultranvan

"A rabbit hops through DI trees,
Parsers wake with papered breeze,
Ollama nudges registries bright,
Containers guard the startup light,
Factories hum — tests clap in delight. 🐇✨"

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 56.57% 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 title directly and clearly summarizes the main changes: adding parser registration (openrag/di/parsers.py) and a DI provider bridge (openrag/di/providers.py enhancements), which are the core objectives of Phase 11 as described in the PR objectives.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

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

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch refactor/phase-11-person-b-di-infra

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 changed the title Phase 11 Person B DI infrastructure Phase 11: add parser registration and DI provider bridge May 28, 2026
@hedhoud

hedhoud commented May 28, 2026

Copy link
Copy Markdown
Collaborator Author

@coderabbitai review

@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown
✅ Actions performed

Review triggered.

Note: CodeRabbit is an incremental review system and does not re-review already reviewed commits. This command is applicable only when automatic reviews are paused.

Add make_component_factory: a generic, thread-safe (double-checked
locking) cached factory that builds inference components by name from a
registry and config section. Returns a (factory, cache) tuple so the
container can close cached httpx clients on shutdown and invalidate
entries on endpoint changes. ModelEndpointConfig documents the unified
per-endpoint config shape the factory consumes.
Expose `config` and `is_initialized` on ServiceContainer so the provider
bridge's get_config and initialization guard resolve against the real
container, not just the test fakes. Track inference clients built via
create_embedder/llm/reranker/vlm and aclose() them in shutdown() so
httpx connection pools are released on teardown.
The lifespan only set request.app.state.container, so callers that
resolve the container without a request (Chainlit, background tasks)
fell back to an empty ServiceContainer with no settings. Mirror the
resolved boot container into di.providers via set_container after
startup and clear it on shutdown, matching the degraded-boot 503 path.
@coderabbitai coderabbitai Bot added breaking-change Change of behavior after upgrade feat Add a new feature refactor labels May 28, 2026

@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: 2

🤖 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 `@openrag/di/container.py`:
- Around line 130-139: The shutdown method currently aborts cleanup if any
client's aclose() raises; update Container.shutdown to make close operations
best-effort by iterating over self._inference_clients and awaiting each aclose()
inside a try/except that logs the error (but continues), then clear
self._inference_clients regardless; likewise wrap await
self._catalog_store.shutdown() in a try/except if _catalog_store is not None so
store shutdown failures don't stop final state reset, and ensure
self._initialized = False always executes (e.g., via finally or by placing it
after the guarded operations).
- Around line 147-149: The config() method is annotated to return Settings but
can return None via self._settings; fix by either (A) making the return type
Optional[Settings] and updating callers to handle None (change def config(self)
-> Optional[Settings]) or (B) enforce settings by asserting/raising when absent
(in container.config check if self._settings is None and raise a
RuntimeError/ValueError with a clear message), so update the config method and
references to use the chosen approach and adjust any callers accordingly; refer
to the config method and the _settings attribute on the container class when
making the change.
🪄 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: 1db2d8e7-27db-436e-aed6-449a1d67722f

📥 Commits

Reviewing files that changed from the base of the PR and between f8a3716 and dd5a1ec.

📒 Files selected for processing (5)
  • openrag/api/main.py
  • openrag/di/container.py
  • openrag/di/factories.py
  • openrag/di/test_container.py
  • openrag/di/test_factories.py

Comment thread openrag/di/container.py Outdated
Comment thread openrag/di/container.py Outdated
Address review feedback on the phase 11 container: close each inference
client inside its own guard so one aclose() failure no longer skips the
remaining clients, the store shutdown, or the state reset (now in a
finally block). Make the config property raise the no-settings error
instead of returning None so its Settings return type holds.
@coderabbitai

coderabbitai Bot commented May 28, 2026

Copy link
Copy Markdown

Actionable comments posted: 0

hedhoud added 2 commits May 28, 2026 14:04
Replace module-level load_config() calls with explicit settings passing
and FastAPI Depends(get_config) so all components source config from the
DI container rather than import-time globals. Bootstrap and parser workers
now receive settings via initialize_worker_bootstrap(settings) instead of
capturing a module-level singleton.

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
openrag/api/routers/user/chat.py (1)

176-181: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Propagate injected settings into max-token ceiling calculation.

check_tokens_limit(..., settings=...) currently ignores settings for max_tokens_allowed because get_max_model_tokens() is called without it. This can apply limits from a different config than the request’s injected config.

💡 Proposed fix
-def get_max_model_tokens() -> int:
+def get_max_model_tokens(settings: "Settings | None" = None) -> int:
     """Return the cached max model token limit (populated at startup)."""
     if _max_model_tokens is not None:
         return _max_model_tokens
-    config = _runtime_config()
+    config = _runtime_config(settings)
     return int(config.llm_context.max_llm_context_size)

 def check_tokens_limit(
     request: OpenAIChatCompletionRequest | OpenAICompletionRequest,
     log,
     settings: "Settings | None" = None,
 ):
     """Validate token limit and raise HTTPException(413) if exceeded."""
     is_valid, error_message = validate_tokens_limit(
         request,
-        max_tokens_allowed=get_max_model_tokens(),
+        max_tokens_allowed=get_max_model_tokens(settings),
         settings=settings,
     )

Also applies to: 228-238, 294-296, 389-390

🤖 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 `@openrag/api/routers/user/chat.py` around lines 176 - 181,
get_max_model_tokens() currently always reads the global runtime config, causing
check_tokens_limit(..., settings=...) to ignore injected per-request settings;
update get_max_model_tokens to accept an optional settings parameter (e.g.,
get_max_model_tokens(settings: Optional[RuntimeSettings] = None)) and have it
prefer values from the passed settings.llm_context.max_llm_context_size when
provided, then update all callers (notably where check_tokens_limit is invoked,
and the sites around the symbols mentioned) to pass the request's injected
settings through so the max-token ceiling is computed from the correct config
source.
openrag/services/workers/parsers/whisper_workers.py (1)

199-204: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Missing call_ray_actor_with_timeout wrapper for pool dispatch.

LocalWhisperLoader.parse calls self.whisper_actor.transcribe.remote(...) directly without a timeout wrapper. While WhisperPool.transcribe has internal retry/backoff, the outer Ray call from the loader has no timeout protection. This is inconsistent with MarkerLoader._convert_pdf and DoclingLoader._dispatch, which both wrap their pool calls with call_ray_actor_with_timeout.

🛠️ Suggested fix
     async def parse(self, document: Document) -> ProcessedDocument:
         if not document.raw_bytes:
             return ProcessedDocument(
                 document_id=document.id,
                 metadata=dict(document.metadata),
             )

         async with document.as_temporary_file() as path:
             try:
-                text = await self.whisper_actor.transcribe.remote(str(path))
+                text = await call_ray_actor_with_timeout(
+                    self.whisper_actor.transcribe.remote(str(path)),
+                    timeout=self.config.loader.local_whisper.whisper_timeout,
+                    task_description=f"LocalWhisperLoader transcribe ({path})",
+                )
             except Exception as e:
                 logger.error("Error transcribing audio", error=str(e))
                 raise
🤖 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 `@openrag/services/workers/parsers/whisper_workers.py` around lines 199 - 204,
LocalWhisperLoader.parse is calling self.whisper_actor.transcribe.remote(...)
directly without the call_ray_actor_with_timeout wrapper, leaving the outer Ray
call without a timeout; update LocalWhisperLoader.parse to dispatch to the
whisper actor using call_ray_actor_with_timeout(self.whisper_actor,
"transcribe", args=(str(path),), ...) (matching the pattern used in
MarkerLoader._convert_pdf and DoclingLoader._dispatch) so the outer call has the
same timeout/retry protection as WhisperPool.transcribe; ensure you preserve the
existing exception handling and pass the same timeout value used elsewhere in
the loaders.
🤖 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 `@openrag/components/prompts/prompts.py`:
- Around line 25-27: The current assignment uses falsy checks (prompts_dir =
prompts_dir or config.paths.prompts_dir and prompt_mapping = prompt_mapping or
config.prompts) which overrides explicit empty values like {} or ""; change
these to explicit None checks so only None triggers defaults: after calling
load_config(), set prompts_dir = config.paths.prompts_dir only if prompts_dir is
None, and set prompt_mapping = config.prompts only if prompt_mapping is None
(referencing the variables prompts_dir, prompt_mapping and
config.paths.prompts_dir / config.prompts to locate the assignments).

In `@openrag/di/test_workers.py`:
- Around line 14-23: The test replaces sys.modules["services.workers.bootstrap"]
but the finally block only pops it, which loses any pre-existing module and can
make tests order-dependent; save the original entry before inserting (e.g., prev
= sys.modules.get("services.workers.bootstrap")), set the test ModuleType and
run ensure_worker_bootstrap(settings), then in the finally restore the original:
if prev is None remove the test entry, otherwise reassign
sys.modules["services.workers.bootstrap"] = prev so the original module is
preserved; apply this change around the ModuleType/Mock setup used with
ensure_worker_bootstrap.

---

Outside diff comments:
In `@openrag/api/routers/user/chat.py`:
- Around line 176-181: get_max_model_tokens() currently always reads the global
runtime config, causing check_tokens_limit(..., settings=...) to ignore injected
per-request settings; update get_max_model_tokens to accept an optional settings
parameter (e.g., get_max_model_tokens(settings: Optional[RuntimeSettings] =
None)) and have it prefer values from the passed
settings.llm_context.max_llm_context_size when provided, then update all callers
(notably where check_tokens_limit is invoked, and the sites around the symbols
mentioned) to pass the request's injected settings through so the max-token
ceiling is computed from the correct config source.

In `@openrag/services/workers/parsers/whisper_workers.py`:
- Around line 199-204: LocalWhisperLoader.parse is calling
self.whisper_actor.transcribe.remote(...) directly without the
call_ray_actor_with_timeout wrapper, leaving the outer Ray call without a
timeout; update LocalWhisperLoader.parse to dispatch to the whisper actor using
call_ray_actor_with_timeout(self.whisper_actor, "transcribe", args=(str(path),),
...) (matching the pattern used in MarkerLoader._convert_pdf and
DoclingLoader._dispatch) so the outer call has the same timeout/retry protection
as WhisperPool.transcribe; ensure you preserve the existing exception handling
and pass the same timeout value used elsewhere in the loaders.
🪄 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: 220e4d93-8737-4493-8a87-bb46ebf0393d

📥 Commits

Reviewing files that changed from the base of the PR and between 4953736 and d0a3a31.

📒 Files selected for processing (21)
  • openrag/api/dependencies/auth.py
  • openrag/api/dependencies/files.py
  • openrag/api/dependencies/llm.py
  • openrag/api/dependencies/test_auth.py
  • openrag/api/main.py
  • openrag/api/routers/admin/indexing.py
  • openrag/api/routers/admin/tools.py
  • openrag/api/routers/user/chat.py
  • openrag/api/schemas/user/chat.py
  • openrag/components/indexer/chunker/chunker.py
  • openrag/components/indexer/loaders/base.py
  • openrag/components/prompts/prompts.py
  • openrag/components/utils.py
  • openrag/di/test_workers.py
  • openrag/di/workers.py
  • openrag/services/workers/bootstrap.py
  • openrag/services/workers/parsers/doc_serializer.py
  • openrag/services/workers/parsers/docling_workers.py
  • openrag/services/workers/parsers/marker_workers.py
  • openrag/services/workers/parsers/whisper_workers.py
  • openrag/utils/logger.py

Comment on lines +25 to +27
config = load_config()
prompts_dir = prompts_dir or config.paths.prompts_dir
prompt_mapping = prompt_mapping or config.prompts

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Preserve explicit empty prompt_mapping overrides.

Using or here treats {} as “not provided” and silently replaces it with config defaults. That breaks intentional caller overrides.

Suggested fix
 def load_prompt(
     prompt_name: str,
     prompts_dir: Path | None = None,
     prompt_mapping=None,
 ) -> str:
     config = load_config()
-    prompts_dir = prompts_dir or config.paths.prompts_dir
-    prompt_mapping = prompt_mapping or config.prompts
+    if prompts_dir is None:
+        prompts_dir = config.paths.prompts_dir
+    if prompt_mapping is None:
+        prompt_mapping = config.prompts
     return load_template_by_key(prompts_dir, prompt_mapping, prompt_name)
🤖 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 `@openrag/components/prompts/prompts.py` around lines 25 - 27, The current
assignment uses falsy checks (prompts_dir = prompts_dir or
config.paths.prompts_dir and prompt_mapping = prompt_mapping or config.prompts)
which overrides explicit empty values like {} or ""; change these to explicit
None checks so only None triggers defaults: after calling load_config(), set
prompts_dir = config.paths.prompts_dir only if prompts_dir is None, and set
prompt_mapping = config.prompts only if prompt_mapping is None (referencing the
variables prompts_dir, prompt_mapping and config.paths.prompts_dir /
config.prompts to locate the assignments).

Comment on lines +14 to +23
module = ModuleType("services.workers.bootstrap")
module.initialize_worker_bootstrap = Mock()
sys.modules["services.workers.bootstrap"] = module
settings = Settings()

try:
ensure_worker_bootstrap(settings)
finally:
sys.modules.pop("services.workers.bootstrap", None)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Restore original sys.modules entry to keep test isolation.

finally: pop(...) removes the module but doesn’t restore a pre-existing entry. That can make later tests order-dependent.

Suggested fix
 def test_ensure_worker_bootstrap_initializes_explicitly() -> None:
     """Startup calls the worker bootstrap function instead of relying on import side effects."""
+    original = sys.modules.get("services.workers.bootstrap")
     module = ModuleType("services.workers.bootstrap")
     module.initialize_worker_bootstrap = Mock()
     sys.modules["services.workers.bootstrap"] = module
     settings = Settings()

     try:
         ensure_worker_bootstrap(settings)
     finally:
-        sys.modules.pop("services.workers.bootstrap", None)
+        if original is None:
+            sys.modules.pop("services.workers.bootstrap", None)
+        else:
+            sys.modules["services.workers.bootstrap"] = original
🤖 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 `@openrag/di/test_workers.py` around lines 14 - 23, The test replaces
sys.modules["services.workers.bootstrap"] but the finally block only pops it,
which loses any pre-existing module and can make tests order-dependent; save the
original entry before inserting (e.g., prev =
sys.modules.get("services.workers.bootstrap")), set the test ModuleType and run
ensure_worker_bootstrap(settings), then in the finally restore the original: if
prev is None remove the test entry, otherwise reassign
sys.modules["services.workers.bootstrap"] = prev so the original module is
preserved; apply this change around the ModuleType/Mock setup used with
ensure_worker_bootstrap.

@EnjoyBacon7
EnjoyBacon7 merged commit 67cd67f into refactor/hexagonal May 28, 2026
10 checks passed
@EnjoyBacon7
EnjoyBacon7 deleted the refactor/phase-11-person-b-di-infra branch May 28, 2026 14:26
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 feat Add a new feature refactor

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants