Skip to content

Feat/add ruff linting - #214

Merged
Ahmath-Gadji merged 2 commits into
devfrom
feat/add-ruff-linting
Jan 16, 2026
Merged

Feat/add ruff linting#214
Ahmath-Gadji merged 2 commits into
devfrom
feat/add-ruff-linting

Conversation

@Ahmath-Gadji

@Ahmath-Gadji Ahmath-Gadji commented Jan 16, 2026

Copy link
Copy Markdown
Collaborator

🎨 Add Ruff Linting, Modernize Type Annotations, and Improve Code Quality

This PR introduces automated code quality tooling and modernizes the codebase to align with contemporary Python standards and best practices.

🚀 What's New

Automated Linting & CI Integration

  • GitHub Actions Workflow: Added .github/workflows/lint.yml to automatically run Ruff linting and formatting checks on every push to main and dev branches, ensuring consistent code quality across all contributions
  • Developer Documentation: Created comprehensive LINTING.md guide covering local setup, pre-commit hooks, CI integration, and troubleshooting tips

Type Annotation Modernization

Migrated type hints to modern Python 3.10+ syntax throughout the codebase:

  • Replaced Union[str, List[str]] with str | list[str]
  • Replaced Optional[List] with list | None
  • Updated type annotations in .github/workflows/api_tests/mock_vllm.py for improved readability and maintainability

✅ Testing

All existing tests continue to pass. The new linting workflow validates code quality on every push.

Summary by CodeRabbit

  • New Features

    • Added linting documentation and CI/CD workflow for code quality enforcement.
  • Refactor

    • Modernized type hints across the codebase to use Python 3.10+ syntax for improved code clarity.
    • Consolidated formatting and code style for consistency.
    • Enhanced text sanitization in document chunking process.

✏️ Tip: You can customize this high-level summary in your review settings.

- Apply ruff code linting (PEP 8, Pyflakes, isort, etc.)
- Auto-format code to 120 character line length
- Sort imports with isort rules
- Modernize Python code with pyupgrade rules
formatting checks on all pull requests and pushes to main/dev branches.
@coderabbitai

coderabbitai Bot commented Jan 16, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

This PR modernizes type hints across the codebase from typing module constructs (Optional, List, Dict, Union) to Python 3.10+ union syntax (| for unions, lowercase built-in types). It adds Ruff linting configuration, improves code formatting consistency, and includes minor functional enhancements like text sanitization in the chunker.

Changes

Cohort / File(s) Summary
Linting Infrastructure
.github/workflows/lint.yml, LINTING.md, pyproject.toml
New Ruff linting setup: GitHub Actions workflow for CI linting, documentation guide, and comprehensive Ruff configuration with Python 3.12 target and rule selections (E, W, F, I, C4, UP, PIE).
Type Hint Modernization: Models & Schemas
.github/workflows/api_tests/mock_vllm.py, openrag/models/indexer.py, openrag/models/openai.py
Updated public request/response models from typing constructs to PEP 604 union syntax. Mock VLLM includes changes to EmbeddingRequest/EmbeddingData/EmbeddingResponse, ChatCompletionRequest/Response, TextCompletionRequest/Response. OpenAI models significantly refactored with expanded field surface.
Type Hint Modernization: Core API & Components
openrag/api.py, openrag/chainlit_api.py, openrag/app_front.py, openrag/components/llm.py
Updated environment variable type hints (AUTH_TOKEN, INDEXERUI_PORT/URL, feature flags) and version handling. Minor formatting and import reorganization.
Type Hint Modernization: Chunker & Indexing
openrag/components/indexer/chunker/chunker.py, openrag/components/indexer/chunker/utils.py, openrag/components/indexer/chunker/test_chunking.py
Updated BaseChunker signatures with `dict
Type Hint Modernization: Loaders
openrag/components/indexer/loaders/base.py, openrag/components/indexer/loaders/txt_loader.py, openrag/components/indexer/loaders/pdf_loaders/marker.py, openrag/components/indexer/loaders/pdf_loaders/openai.py, openrag/components/indexer/loaders/* (8+ files)
Standardized aload_document signatures across all loaders to use str | Path and dict | None. Removed min_width_pixels/min_height_pixels from BaseLoader. pdf_to_images converted to standalone function in OpenAI loader.
Type Hint Modernization: Indexer & VectorDB
openrag/components/indexer/indexer.py, openrag/components/indexer/vectordb/vectordb.py, openrag/components/indexer/vectordb/utils.py
Indexer public methods updated to modern types: chunk/serialize_file/add_file/asearch return types and parameters modernized. TaskInfo and TaskStateManager internal types updated. VectorDB list_partition_files and async_search signatures updated.
Type Hint Modernization: Routers
openrag/routers/indexer.py, openrag/routers/openai.py, openrag/routers/search.py, openrag/routers/users.py, openrag/routers/utils.py, openrag/routers/partition.py, openrag/routers/queue.py, openrag/routers/extract.py, openrag/routers/actors.py, openrag/routers/tools.py
Router parameter and return types modernized. Added new validation helper is_file_id_valid(). Optional imports removed in favor of union syntax. Minor error formatting adjustments.
Type Hint Modernization: Utilities & Scripts
openrag/components/utils.py, openrag/components/pipeline.py, openrag/components/retriever.py, openrag/components/reranker.py, openrag/components/map_reduce.py, openrag/scripts/backup.py, openrag/scripts/embed.py, openrag/scripts/restore.py, openrag/scripts/filter-logs.py
ClassVar annotations added, function return types updated to modern syntax. In filter-logs.py, parse_text_timestamp enhanced with full parsing logic. Backup/restore scripts modernized with dict/set/tuple syntax.
Type Hint Modernization: Migrations
openrag/scripts/migrations/alembic/versions/4add4d260575_initial_migration.py, openrag/scripts/migrations/alembic/versions/cd642e4502d8_create_users_memberships_tables.py
Alembic migration metadata annotations updated from Union to pipe syntax; Sequence now imported from collections.abc.
Code Formatting & Cleanup
openrag/components/indexer/loaders/media_loader.py, openrag/components/indexer/loaders/pdf_loaders/docling.py, openrag/components/indexer/loaders/pdf_loaders/docling2.py, openrag/config/config.py, openrag/consts.py, openrag/components/ray_utils.py, openrag/utils/logger.py, openrag/utils/external_resource_errors.py, utility/data_indexer.py
Multi-line expressions collapsed to single lines for improved readability. Added error handling/try-except blocks (logger.py). Removed import-time config loading. Minor formatting normalizations across logging and initialization.
Functional Enhancements: Loaders & Utilities
openrag/components/indexer/loaders/media_loader.py, openrag/components/indexer/utils/files.py, openrag/components/indexer/loaders/serializer.py, openrag/components/indexer/embeddings/openai.py
Added _transcribe_chunk() helper in AudioTranscriber for error handling. Metadata initialization pattern updated to nullable dict with defensive assignment. Error message formatting updated to use !s conversion.
Test Files
tests/api_tests/test_actors.py, tests/api_tests/test_extract.py, tests/api_tests/test_indexer.py, tests/api_tests/test_partition.py, tests/api_tests/test_queue.py, tests/api_tests/test_search.py, tests/api_tests/test_users.py, tests/test_vectordb.py, tests/api_tests/conftest.py, openrag/components/indexer/loaders/test_media_loader.py
Removed pytest imports where not explicitly needed. Reformatted method signatures from multi-line to single-line. Updated test assertions and dict literals formatting.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

The large number of affected files (~95) with heterogeneous changes across models, loaders, routers, and scripts requires careful verification of type correctness throughout. While most changes follow a consistent pattern (type hint modernization), the functional enhancements (sanitize_text, transcribe helper, validation logic) and configuration additions add complexity. The scope demands attention to ensure no runtime behavioral regressions despite the primarily cosmetic nature of most edits.

Possibly related PRs

  • Feat/add ruff linting #209 — Both apply PEP 604 typing modernizations across the same files (mock_vllm.py) and add Ruff linting infrastructure.
  • Feat/chunking #165 — Overlaps in chunker.py modifications, including BaseChunker signature updates and the addition of sanitize_text functionality.
  • chore: Simple log filter script #208 — Directly modifies openrag/scripts/filter-logs.py (parse_text_timestamp signature and parsing logic) that is touched by both PRs.

Suggested labels

chore

Suggested reviewers

  • paultranvan
  • dodekapod

Poem

🐰 Whisker-twitching type migration complete,
Python 3.10's union syntax so neat,
Ruff lints the way with rules so bright,
Multi-line expressions collapsed just right,
Sanitized chunks and helpers galore—
The codebase gleams like never before!

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 29.21% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Feat/add ruff linting' clearly and concisely summarizes the main change: adding Ruff linting automation to the codebase.

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

✨ Finishing touches
  • 📝 Generate docstrings

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.

@coderabbitai coderabbitai Bot added the chore No production code impact, typically improve tooling, code quality, etc label Jan 16, 2026
@Ahmath-Gadji
Ahmath-Gadji merged commit cb2ab57 into dev Jan 16, 2026
2 of 3 checks passed

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

Caution

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

⚠️ Outside diff range comments (21)
openrag/components/indexer/loaders/pdf_loaders/pymupdf.py (2)

14-27: Type annotations are incomplete and inconsistent with base class.

Given that this PR modernizes type hints, this method signature should align with the base class and sibling implementations. Currently missing:

  • file_path: str | Path
  • save_markdown: bool
  • Return type -> Document
  • metadata should be dict | None = None (not dict = None)
Proposed fix
-    async def aload_document(self, file_path, metadata: dict = None, save_markdown=False):
+    async def aload_document(self, file_path: str | Path, metadata: dict | None = None, save_markdown: bool = False) -> Document:

34-44: Same type annotation gaps as above.

This method has identical type hint inconsistencies with the base class signature.

Proposed fix
-    async def aload_document(self, file_path, metadata: dict = None, save_markdown=False):
+    async def aload_document(self, file_path: str | Path, metadata: dict | None = None, save_markdown: bool = False) -> Document:
openrag/routers/users.py (1)

164-188: Add authorization check for token regeneration endpoint.

The endpoint lacks the admin_user=Depends(require_admin) dependency that all other sensitive user operations have (list_users, create_user, get_user, delete_user). The docstring claims "Requires admin role (or user can regenerate their own token)" but the code enforces neither—any authenticated user can regenerate any other user's token. The vectordb implementation checks only that the user exists, not authorization or ownership.

Either add admin_user=Depends(require_admin) if only admins should regenerate tokens, or implement user self-service logic that validates the requesting user matches the target user_id.

automatic-evaluation-pipeline/benchmark.py (3)

88-97: Missing environment variables will silently pass None to ChatOpenAI.

Using os.environ.get() without defaults means MODEL, BASE_URL, and API_KEY could be None if not set. Since llm_completion_judge and llm_precision_judge are initialized at module level (lines 171-172), this will cause failures when the judges are invoked rather than at startup.

Consider validating required environment variables early or providing sensible defaults.

Suggested improvement
+def get_required_env(key: str) -> str:
+    value = os.environ.get(key)
+    if not value:
+        raise ValueError(f"Required environment variable '{key}' is not set")
+    return value
+
 llm_judge_settings = {
-    "model": os.environ.get("MODEL"),
-    "base_url": os.environ.get("BASE_URL"),
-    "api_key": os.environ.get("API_KEY"),
+    "model": get_required_env("MODEL"),
+    "base_url": get_required_env("BASE_URL"),
+    "api_key": get_required_env("API_KEY"),
     "temperature": 0.2,

201-204: Type inconsistency: returning "error" strings where int scores are expected.

The function returns ("error", "error") on exceptions, but the success path returns (int, int). This mixed-type return complicates downstream handling and could cause runtime errors if the results are used numerically before filtering.

Suggested improvement using `None`
         except Exception as e:
             logger.debug(f"Error evaluating response: {e}")
-            return "error", "error"
+            return None, None

Then update the filter on line 279:

-    valid_scores = [(comp, prec) for comp, prec in llm_judge_scores if comp != "error"]
+    valid_scores = [(comp, prec) for comp, prec in llm_judge_scores if comp is not None]

278-290: Data alignment bug: slicing doesn't preserve index correspondence after filtering.

The filtering on line 279 removes errors from arbitrary positions, but line 280 and 288 use simple slicing ([:len(valid_scores)]). This causes misalignment between valid_scores, valid_ndcg_scores, and chunks_count.

Example:

  • llm_judge_scores = [(5, 5), ("error", "error"), (7, 7)]
  • nDCG_scores = [0.8, 0.6, 0.9]
  • valid_scores = [(5, 5), (7, 7)] (removes index 1)
  • valid_ndcg_scores = nDCG_scores[:2] = [0.8, 0.6] ❌ (should be [0.8, 0.9])
Suggested fix using index tracking
     # Filter out error responses
-    valid_scores = [(comp, prec) for comp, prec in llm_judge_scores if comp != "error"]
-    valid_ndcg_scores = nDCG_scores[: len(valid_scores)]  # Match the filtered scores
+    valid_indices = [i for i, (comp, prec) in enumerate(llm_judge_scores) if comp != "error"]
+    valid_scores = [llm_judge_scores[i] for i in valid_indices]
+    valid_ndcg_scores = [nDCG_scores[i] for i in valid_indices]

     eval_results = pd.DataFrame(
         valid_scores,
         columns=["completion_evaluation", "precision_evaluation"],
     )
     eval_results["nDCG"] = valid_ndcg_scores
     chunks_count = [
-        len(input_reference["chunks"]) for input_reference in list_response_answer_reference[: len(valid_scores)]
+        len(list_response_answer_reference[i]["chunks"]) for i in valid_indices
     ]
openrag/components/indexer/loaders/media_loader.py (1)

116-129: Ensure temp language-detection files are always removed.

If _transcribe_chunk returns an empty string, the early return skips the finally, leaving *_langdetect.wav on disk.

🐛 Suggested fix
-        text = await self._transcribe_chunk(tmp_path)
-        if not text:
-            return fallback_language  # Fallback to English
-        try:
-            return langdetect.detect(text)
-        except Exception as e:
-            logger.exception("Language detection failed", error=str(e))
-            return fallback_language
-        finally:
-            await asyncio.to_thread(os.remove, tmp_path)
+        try:
+            text = await self._transcribe_chunk(tmp_path)
+            if not text:
+                return fallback_language  # Fallback to English
+            return langdetect.detect(text)
+        except Exception as e:
+            logger.exception("Language detection failed", error=str(e))
+            return fallback_language
+        finally:
+            await asyncio.to_thread(os.remove, tmp_path)
automatic-evaluation-pipeline/generate_questions.py (2)

127-136: Guard against clusters smaller than n_min.

If a cluster has fewer items than n_min, random.randint(n_min, min(n_max, len(chunks))) will raise a ValueError because the lower bound exceeds the upper bound. Add a check to skip such clusters or clamp the bounds.

🛠️ Proposed fix
-    for cluster_label, chunks in clusters.items():  # cluster loop
-        for _ in range(n_questions_per_cluster):
-            n = random.randint(n_min, min(n_max, len(chunks)))
-            sampled_chunks = random.sample(chunks, n)
-            task = question_answer(chunks=sampled_chunks)
-            tasks.append(task)
+    for cluster_label, chunks in clusters.items():  # cluster loop
+        max_n = min(n_max, len(chunks))
+        if max_n < n_min:
+            continue
+        for _ in range(n_questions_per_cluster):
+            n = random.randint(n_min, max_n)
+            sampled_chunks = random.sample(chunks, n)
+            task = question_answer(chunks=sampled_chunks)
+            tasks.append(task)

57-67: Avoid creating the semaphore at definition time.

While Python 3.12+ has improved asyncio synchronization primitives to avoid strict event loop binding issues, creating asyncio.Semaphore(10) as a default parameter is still a poor practice. Define it as an optional parameter instead, and instantiate it inside the coroutine when needed. Apply the same fix to both summarize and question_answer:

🛠️ Proposed fix
-async def summarize(chunk: str, semaphore: asyncio.Semaphore = asyncio.Semaphore(10)) -> str:
+async def summarize(chunk: str, semaphore: asyncio.Semaphore | None = None) -> str:
+    if semaphore is None:
+        semaphore = asyncio.Semaphore(10)
     async with semaphore:
-async def question_answer(chunks: list[dict], semaphore=asyncio.Semaphore(10)):
+async def question_answer(chunks: list[dict], semaphore: asyncio.Semaphore | None = None):
+    if semaphore is None:
+        semaphore = asyncio.Semaphore(10)
     async with semaphore:
openrag/components/indexer/loaders/CustomDocLoader.py (1)

35-37: Pre-existing bug: Only the last page content is retained.

The loop uses assignment (s = ...) instead of concatenation (s += ...), so each iteration overwrites the previous content. Only the last page will be included in the final document.

🐛 Proposed fix
 s = ""
 for page_num, p in enumerate(pages, start=1):
-    s = p.page_content.strip() + f"\n[PAGE_{page_num}]\n"
+    s += p.page_content.strip() + f"\n[PAGE_{page_num}]\n"
automatic-evaluation-pipeline/upload_files.py (1)

10-13: Validate APP_URL/APP_PORT before building base_url.
Current code raises a KeyError (APP_URL) or builds ...:None (APP_PORT) without a clear message.

Proposed fix
-num_port = os.environ.get("APP_PORT")
-app_url = os.environ["APP_URL"]
-base_url = f"http://{app_url}:{num_port}"  # the base url of your running app for instance: 'http://localhost:8080'
+num_port = os.environ.get("APP_PORT")
+app_url = os.environ.get("APP_URL")
+if not app_url:
+    raise RuntimeError("APP_URL is required (e.g., localhost)")
+if not num_port:
+    raise RuntimeError("APP_PORT is required (e.g., 8080)")
+base_url = f"http://{app_url}:{num_port}"  # the base url of your running app for instance: 'http://localhost:8080'
pyproject.toml (1)

50-60: Remove Ruff from runtime dependencies.

ruff>=0.14.1 is listed in [project].dependencies (line 50) and duplicated in [dependency-groups].lint (line 60). As a linting tool, it should only be in the lint group to avoid installing it in production environments.

Proposed change
 dependencies = [
     "fast-langdetect>=1.0.0",
-    "ruff>=0.14.1",
     "librosa>=0.11.0",
 ]
openrag/components/indexer/chunker/utils.py (1)

194-210: Unconditional dereference of optional length_function parameter.

The length_function parameter is typed as Callable[[str], int] | None with default None, but is called unconditionally on lines 209-210 (and again on line 221) without a None check. While all current call sites provide this parameter, the type signature and default suggest the function should handle the None case.

Either make length_function required by removing the | None union and default value, or add a fallback implementation (e.g., len) when None is provided.

openrag/components/indexer/chunker/chunker.py (1)

141-162: Guard against llm_config=None before ChatOpenAI and ChunkContextualizer initialization.

Line 153 (ChatOpenAI(**llm_config)) and line 161 (ChunkContextualizer(llm_config)) will raise TypeError if callers instantiate BaseChunker directly with the default llm_config=None. While ChunkerFactory.create_chunker() always provides config.vlm, the signature allows None, yet the implementation doesn't guard against it.

🛠️ Proposed fix to handle None safely
-        self.llm = ChatOpenAI(**llm_config)
+        llm_config = dict(llm_config or {})
+        self.llm = ChatOpenAI(**llm_config)

Apply the same guard before line 161 when passing to ChunkContextualizer.

openrag/api.py (1)

217-226: Prevent duplicate mounting of openai_router.

The router is currently mounted twice when both WITH_OPENAI_API and WITH_CHAINLIT_UI are enabled (the default configuration). This duplicates routes and OpenAPI schema entries.

Suggested fix
-if WITH_OPENAI_API:
-    # Mount the openai router
-    app.include_router(openai_router, prefix="/v1", tags=[Tags.OPENAI])
-
-if WITH_CHAINLIT_UI:
+if WITH_OPENAI_API or WITH_CHAINLIT_UI:
+    app.include_router(openai_router, prefix="/v1", tags=[Tags.OPENAI])
+
+if WITH_CHAINLIT_UI:
     # Mount the default front
     from chainlit.utils import mount_chainlit
openrag/components/indexer/vectordb/vectordb.py (1)

998-1002: Incorrect type annotation for CONNECTORS dictionary.

The type hint dict[BaseVectorDB] is syntactically incorrect. A dictionary requires two type parameters (key and value types). This should be dict[str, type[BaseVectorDB]] to properly annotate a mapping from string names to VectorDB classes.

Proposed fix
 class ConnectorFactory:
-    CONNECTORS: dict[BaseVectorDB] = {
+    CONNECTORS: dict[str, type[BaseVectorDB]] = {
         "milvus": MilvusDB,
         # "qdrant": QdrantDB,
     }
tests/test_vectordb.py (1)

4-24: The _build_expr_template_and_params() static method does not exist in the MilvusDB class and must be implemented.

These tests call MilvusDB._build_expr_template_and_params(partition, filter) on lines 7 and 20, but this method is not defined in openrag/components/indexer/vectordb/vectordb.py. The tests will fail with an AttributeError when run. Implement this static method in the MilvusDB class to return a templated expression and parameter dictionary as expected by the assertions.

openrag/components/indexer/loaders/txt_loader.py (1)

51-54: Incorrect docstring: Class is MarkdownLoader, not TextLoader.

The docstring incorrectly states "Loader for plain text files (.txt)" but this is the MarkdownLoader class which handles Markdown files.

📝 Suggested fix
 class MarkdownLoader(BaseLoader):
     """
-    Loader for plain text files (.txt).
+    Loader for Markdown files (.md).
     """
openrag/components/indexer/indexer.py (3)

57-66: Mutable default argument metadata: dict = {} is dangerous.

Using a mutable default argument (empty dict) is a well-known Python pitfall. The same dict instance is reused across all calls, which can cause unexpected mutations to persist. The utility function in openrag/components/indexer/utils/files.py correctly uses metadata: dict | None = None with metadata = metadata or {} inside.

Additionally, task_id: str = None should be str | None = None to match the modernized type annotations.

Suggested fix
 `@ray.method`(concurrency_group="serialize")
 async def serialize_file(
     self,
     path: str,
-    metadata: dict = {},
-    task_id: str = None,
+    metadata: dict | None = None,
+    task_id: str | None = None,
 ):
     # Serialize
+    metadata = metadata or {}
     doc = await serialize_file(task_id, path, metadata=metadata)
     return doc

68-92: Potential AttributeError when user is None.

The parameter user: dict | None = None allows None, but line 91 calls user.get("id") without a null check. If user is None, this will raise an AttributeError.

Suggested fix
         await task_state_manager.set_details.remote(
             task_id,
             file_id=metadata.get("file_id"),
             partition=partition,
             metadata=user_metadata,
-            user_id=user.get("id"),
+            user_id=user.get("id") if user else None,
         )

191-203: Copy-paste error in log message.

The error message on line 202 references update_file_metadata but this is the copy_file method. This will be misleading when debugging.

Suggested fix
-            log.error("Vector database is not enabled, but update_file_metadata was called.")
+            log.error("Vector database is not enabled, but copy_file was called.")
🤖 Fix all issues with AI agents
In `@automatic-evaluation-pipeline/benchmark.py`:
- Around line 219-221: The code inconsistently reads environment variables:
change the access for APP_PORT to match APP_URL (or vice versa) so both are
validated consistently; either use os.environ["APP_PORT"] and
os.environ["APP_URL"] to raise clear KeyError when missing, or introduce a small
helper (e.g., get_env_or_raise) and call it for both keys, then use the
validated values (num_port, num_host) to build openrag_api_base_url, ensuring
the error message mentions the missing variable name for easier debugging.

In `@LINTING.md`:
- Around line 86-111: The pre-commit hook currently collects staged Python files
into the unquoted FILES variable which breaks on paths with spaces; change the
collection to a bash array (e.g., use mapfile -t FILES < <(git diff --cached
--name-only --diff-filter=ACM | grep '\.py$')) and then use the array safely
everywhere by expanding as "${FILES[@]}" when invoking commands (uv run ruff
check/format, git add) and when testing emptiness use: if [ ${`#FILES`[@]} -eq 0
]; then exit 0; fi; update usages of FILES in the hook to the array form to
ensure filenames with spaces are handled correctly.

In `@openrag/api.py`:
- Around line 17-22: The module currently calls
ray.init(dashboard_host="0.0.0.0") unconditionally at import time; remove that
top-level init and instead initialize Ray only when needed by wrapping the call
with a guard such as if not ray.is_initialized(): ray.init(...), and/or move
initialization into your application startup hook (e.g., a function decorated
with app.on_event("startup") or a dedicated init_ray() function invoked from
when the server actually starts). Update references to ray.init in openrag.api
to use ray.is_initialized() before calling and ensure no unconditional
import-time side effects remain.

In `@openrag/app_front.py`:
- Around line 64-65: Remove the hard-coded fallback for
os.environ["CHAINLIT_AUTH_SECRET"] and instead fail fast when authentication is
enabled: check the auth toggle (e.g., AUTH_TOKEN or similar env/config flag) and
if it is set but CHAINLIT_AUTH_SECRET is missing/empty, raise an exception (or
call sys.exit) with a clear message; optionally allow an explicit dev override
(e.g., OPENRAG_DEV=true) to set a non-production default, but do not silently
set a known secret. Ensure you update the code that referenced
os.environ["CHAINLIT_AUTH_SECRET"] to rely on the validated environment value.

In `@openrag/components/indexer/indexer.py`:
- Around line 53-55: The task_id parameter in the chunk method has a default of
None but is annotated as str; update the type annotation to allow None (use
modern union syntax) so it reads task_id: str | None = None in the async def
chunk(self, doc: Document, file_path: str, task_id: str | None = None) signature
(the method uses chunker.split_document and is defined as chunk in indexer.py).

In `@openrag/components/indexer/loaders/pdf_loaders/openai.py`:
- Around line 18-21: The pdf_to_images function opens pdfium.PdfDocument without
closing it; modify it to use a context manager so the PdfDocument is
automatically closed (e.g., run a synchronous helper inside asyncio.to_thread
that does "with pdfium.PdfDocument(pdf_path) as pdf: return
[p.render(scale=scale).to_pil() for p in pdf]"), preserving the async signature
and returning the same list of PIL images; update references in pdf_to_images to
use that context-managed call to avoid file-handle leaks.
🧹 Nitpick comments (11)
openrag/components/ray_utils.py (1)

39-39: LGTM! Formatting change looks good.

The consolidation to a single line is consistent with the Ruff formatting applied across the codebase.

As an optional nitpick: the pending variable from ray.wait is unpacked but never used. Consider using _ to make this explicit:

-        ready, pending = await asyncio.to_thread(ray.wait, [future], num_returns=1, timeout=timeout)
+        ready, _ = await asyncio.to_thread(ray.wait, [future], num_returns=1, timeout=timeout)
automatic-evaluation-pipeline/benchmark.py (1)

54-66: Remove ChatGPT link from code comments.

External ChatGPT conversation links are ephemeral and not suitable as documentation references. They may become inaccessible and don't provide reliable context for future maintainers.

Suggested change
 def compute_inverted_ranks(true_chunk_id, all_retrieved_chunks):
-    # see link: https://chatgpt.com/share/6813f998-2e88-8002-a472-6af2e9a64b61
+    # Computes reciprocal rank: 1/position if found, 0 otherwise (for MRR calculation)
     key = False
openrag/components/indexer/loaders/media_loader.py (2)

53-78: Use Optional typing for language parameters.

These params accept None but are annotated as str. Align with the PR’s 3.10+ typing style for accuracy.

♻️ Suggested tweak
-    async def _process_chunk(self, index: int, segment: AudioSegment, wav_path: Path, language: str = None) -> str:
+    async def _process_chunk(
+        self,
+        index: int,
+        segment: AudioSegment,
+        wav_path: Path,
+        language: str | None = None,
+    ) -> str:
@@
-    async def _transcribe_chunk(self, wav_path: Path, language: str = None) -> str:
+    async def _transcribe_chunk(self, wav_path: Path, language: str | None = None) -> str:

194-209: Align override signature with BaseLoader for typing.

The override drops type hints and uses dict = None. Keeping the same typed signature improves consistency and type checking.

♻️ Suggested tweak
-    async def aload_document(self, file_path, metadata: dict = None, save_markdown=False):
+    async def aload_document(
+        self,
+        file_path: str | Path,
+        metadata: dict | None = None,
+        save_markdown: bool = False,
+    ):
.github/workflows/lint.yml (2)

7-9: Consider re‑enabling PR linting gates.

Keeping pull_request disabled means lint issues land in main/dev before being caught. If this is temporary, please add a tracking item and re-enable before release.


28-31: Avoid curl | sh for uv installation in CI.

For supply‑chain hardening, prefer the official setup action (pinned version) or checksum verification rather than executing a remote script directly. Line 28–31.

💡 Example (verify action name/version in uv docs)
-      - name: Install uv
-        run: |
-          curl -LsSf https://astral.sh/uv/install.sh | sh
-          echo "$HOME/.local/bin" >> $GITHUB_PATH
+      - name: Set up uv
+        uses: astral-sh/setup-uv@v3
+        with:
+          version: "0.x.y"
openrag/app_front.py (1)

69-69: Simplify the timeout construction.

httpx.Timeout(timeout=httpx.Timeout(...)) is redundant and may behave unexpectedly depending on httpx parsing rules. Prefer a single Timeout instance or a scalar timeout. Line 69 and Line 134.

💡 Suggested simplification
-            async with httpx.AsyncClient(timeout=httpx.Timeout(timeout=httpx.Timeout(4 * 60.0))) as client:
+            async with httpx.AsyncClient(timeout=httpx.Timeout(4 * 60.0)) as client:
-        async with httpx.AsyncClient(timeout=httpx.Timeout(timeout=httpx.Timeout(4 * 60.0))) as client:
+        async with httpx.AsyncClient(timeout=httpx.Timeout(4 * 60.0)) as client:

Also applies to: 134-134

openrag/components/indexer/vectordb/vectordb.py (1)

434-442: Parameter name filter shadows built-in.

The parameter name filter shadows Python's built-in filter() function. While this is a common pattern in database/query contexts and the type hint change itself is fine, consider renaming to filter_expr or query_filter in a future refactor to avoid shadowing.

openrag/routers/utils.py (1)

181-193: Good refactor: Centralized file ID validation.

The new is_file_id_valid helper improves maintainability. However, consider reordering the checks in validate_file_id to check for empty/whitespace-only strings first, as checking forbidden characters on an effectively empty ID is unnecessary work.

♻️ Suggested reordering
 async def validate_file_id(file_id: str):
+    if not file_id.strip():
+        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="File ID cannot be empty.")
     if not is_file_id_valid(file_id):
         raise HTTPException(
             status_code=status.HTTP_400_BAD_REQUEST,
             detail=f"File ID contains forbidden characters: {', '.join(FORBIDDEN_CHARS_IN_FILE_ID)}",
         )
-    if not file_id.strip():
-        raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail="File ID cannot be empty.")
     return file_id
openrag/routers/indexer.py (1)

313-313: Type annotation mismatch with runtime behavior.

The parameter is annotated as Any | None but validate_metadata dependency returns a dict. While this works, the annotation is overly permissive and could be dict to match the actual runtime type.

♻️ Consider tightening the type
 async def patch_file(
     partition: str,
     file_id: str = Depends(validate_file_id),
-    metadata: Any | None = Depends(validate_metadata),
+    metadata: dict = Depends(validate_metadata),
     indexer=Depends(get_indexer),

Same applies to copy_file_between_partitions on line 358.

openrag/components/indexer/indexer.py (1)

223-241: Parameter filter shadows built-in.

The parameter name filter shadows Python's built-in filter function. While this is common in search APIs, consider using filters or query_filter to avoid shadowing.

This is a nitpick and can be deferred if the naming convention is intentional across the codebase.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f309ba1 and 424fd2d.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (86)
  • .github/workflows/api_tests/mock_vllm.py
  • .github/workflows/lint.yml
  • LINTING.md
  • automatic-evaluation-pipeline/benchmark.py
  • automatic-evaluation-pipeline/generate_questions.py
  • automatic-evaluation-pipeline/upload_files.py
  • openrag/api.py
  • openrag/app_front.py
  • openrag/chainlit_api.py
  • openrag/components/indexer/chunker/chunker.py
  • openrag/components/indexer/chunker/test_chunking.py
  • openrag/components/indexer/chunker/utils.py
  • openrag/components/indexer/embeddings/__init__.py
  • openrag/components/indexer/embeddings/openai.py
  • openrag/components/indexer/indexer.py
  • openrag/components/indexer/loaders/CustomDocLoader.py
  • openrag/components/indexer/loaders/__init__.py
  • openrag/components/indexer/loaders/base.py
  • openrag/components/indexer/loaders/doc.py
  • openrag/components/indexer/loaders/docx.py
  • openrag/components/indexer/loaders/eml_loader.py
  • openrag/components/indexer/loaders/image.py
  • openrag/components/indexer/loaders/media_loader.py
  • openrag/components/indexer/loaders/pdf_loaders/docling.py
  • openrag/components/indexer/loaders/pdf_loaders/docling2.py
  • openrag/components/indexer/loaders/pdf_loaders/dotsocr.py
  • openrag/components/indexer/loaders/pdf_loaders/marker.py
  • openrag/components/indexer/loaders/pdf_loaders/openai.py
  • openrag/components/indexer/loaders/pdf_loaders/pymupdf.py
  • openrag/components/indexer/loaders/pptx_loader.py
  • openrag/components/indexer/loaders/serializer.py
  • openrag/components/indexer/loaders/test_media_loader.py
  • openrag/components/indexer/loaders/txt_loader.py
  • openrag/components/indexer/utils/files.py
  • openrag/components/indexer/utils/test_text_sanitizer.py
  • openrag/components/indexer/vectordb/utils.py
  • openrag/components/indexer/vectordb/vectordb.py
  • openrag/components/llm.py
  • openrag/components/map_reduce.py
  • openrag/components/pipeline.py
  • openrag/components/prompts/prompts.py
  • openrag/components/ray_utils.py
  • openrag/components/reranker.py
  • openrag/components/retriever.py
  • openrag/components/utils.py
  • openrag/config/config.py
  • openrag/consts.py
  • openrag/models/indexer.py
  • openrag/models/openai.py
  • openrag/routers/actors.py
  • openrag/routers/extract.py
  • openrag/routers/indexer.py
  • openrag/routers/openai.py
  • openrag/routers/partition.py
  • openrag/routers/queue.py
  • openrag/routers/search.py
  • openrag/routers/tools.py
  • openrag/routers/users.py
  • openrag/routers/utils.py
  • openrag/scripts/backup.py
  • openrag/scripts/embed.py
  • openrag/scripts/filter-logs.py
  • openrag/scripts/migrations/alembic/env.py
  • openrag/scripts/migrations/alembic/versions/4add4d260575_initial_migration.py
  • openrag/scripts/migrations/alembic/versions/cd642e4502d8_create_users_memberships_tables.py
  • openrag/scripts/restore.py
  • openrag/utils/dependencies.py
  • openrag/utils/exceptions/base.py
  • openrag/utils/exceptions/embeddings.py
  • openrag/utils/exceptions/vectordb.py
  • openrag/utils/external_resource_errors.py
  • openrag/utils/logger.py
  • openrag/utils/test_external_resource_errors.py
  • openrag/utils/test_logger.py
  • pyproject.toml
  • tests/api_tests/conftest.py
  • tests/api_tests/test_actors.py
  • tests/api_tests/test_extract.py
  • tests/api_tests/test_indexer.py
  • tests/api_tests/test_openai_compat.py
  • tests/api_tests/test_partition.py
  • tests/api_tests/test_queue.py
  • tests/api_tests/test_search.py
  • tests/api_tests/test_users.py
  • tests/test_vectordb.py
  • utility/data_indexer.py
💤 Files with no reviewable changes (7)
  • tests/api_tests/test_actors.py
  • tests/api_tests/test_partition.py
  • tests/api_tests/test_users.py
  • tests/api_tests/test_queue.py
  • tests/api_tests/test_extract.py
  • openrag/utils/test_logger.py
  • openrag/utils/test_external_resource_errors.py
🧰 Additional context used
🧬 Code graph analysis (32)
openrag/scripts/migrations/alembic/versions/cd642e4502d8_create_users_memberships_tables.py (1)
openrag/scripts/migrations/alembic/versions/4add4d260575_initial_migration.py (2)
  • table_exists (16-20)
  • index_exists (23-28)
openrag/components/indexer/loaders/image.py (1)
openrag/components/indexer/loaders/base.py (1)
  • BaseLoader (19-151)
openrag/routers/openai.py (3)
openrag/routers/utils.py (1)
  • get_partition_name (261-287)
openrag/components/llm.py (2)
  • chat_completion (45-87)
  • completions (23-43)
openrag/components/pipeline.py (2)
  • chat_completion (192-202)
  • completions (180-190)
openrag/utils/dependencies.py (1)
openrag/components/indexer/indexer.py (1)
  • TaskStateManager (271-382)
openrag/components/indexer/loaders/pdf_loaders/pymupdf.py (2)
openrag/components/indexer/loaders/base.py (1)
  • aload_document (37-43)
openrag/components/indexer/loaders/pdf_loaders/openai.py (1)
  • aload_document (42-69)
openrag/components/indexer/loaders/doc.py (1)
openrag/components/indexer/loaders/base.py (1)
  • aload_document (37-43)
openrag/components/indexer/loaders/media_loader.py (1)
openrag/components/indexer/loaders/base.py (1)
  • aload_document (37-43)
openrag/components/indexer/utils/test_text_sanitizer.py (2)
openrag/components/indexer/chunker/test_chunking.py (1)
  • test_multiline_spacing (178-183)
openrag/components/indexer/utils/text_sanitizer.py (1)
  • clean_markdown_table_spacing (98-127)
tests/api_tests/test_search.py (1)
tests/api_tests/conftest.py (1)
  • api_client (16-19)
automatic-evaluation-pipeline/generate_questions.py (1)
openrag/components/indexer/indexer.py (1)
  • chunk (53-55)
openrag/routers/search.py (1)
openrag/components/indexer/indexer.py (1)
  • asearch (224-241)
openrag/routers/users.py (4)
openrag/components/indexer/vectordb/vectordb.py (2)
  • get_user (871-873)
  • delete_user (875-882)
openrag/utils/dependencies.py (1)
  • get_vectordb (45-47)
openrag/routers/utils.py (1)
  • require_admin (171-178)
openrag/components/indexer/vectordb/utils.py (1)
  • delete_user (388-395)
openrag/components/indexer/loaders/pdf_loaders/docling2.py (3)
openrag/components/indexer/loaders/base.py (1)
  • aload_document (37-43)
openrag/components/indexer/loaders/pdf_loaders/docling.py (1)
  • aload_document (61-84)
openrag/components/indexer/loaders/pdf_loaders/openai.py (1)
  • aload_document (42-69)
openrag/scripts/backup.py (3)
openrag/components/indexer/vectordb/utils.py (2)
  • list_partition_files (165-188)
  • list_partitions (265-269)
openrag/components/indexer/vectordb/vectordb.py (4)
  • list_partition_files (57-58)
  • list_partition_files (718-735)
  • list_partitions (45-46)
  • list_partitions (737-742)
openrag/scripts/embed.py (1)
  • open_output_file (246-272)
openrag/components/indexer/loaders/pdf_loaders/openai.py (1)
openrag/components/indexer/loaders/base.py (1)
  • aload_document (37-43)
openrag/routers/indexer.py (2)
openrag/components/indexer/indexer.py (3)
  • add_file (68-141)
  • set_state (284-287)
  • copy_file (192-221)
openrag/routers/utils.py (2)
  • validate_metadata (196-202)
  • require_task_owner (156-168)
openrag/components/map_reduce.py (1)
openrag/components/indexer/indexer.py (1)
  • chunk (53-55)
openrag/routers/queue.py (3)
openrag/routers/utils.py (1)
  • require_admin (171-178)
openrag/utils/dependencies.py (1)
  • get_task_state_manager (24-25)
openrag/components/indexer/indexer.py (1)
  • get_all_user_info (363-374)
openrag/scripts/migrations/alembic/versions/4add4d260575_initial_migration.py (1)
openrag/scripts/migrations/alembic/versions/cd642e4502d8_create_users_memberships_tables.py (2)
  • table_exists (16-20)
  • index_exists (23-28)
openrag/components/indexer/loaders/base.py (1)
openrag/utils/logger.py (1)
  • get_logger (14-46)
openrag/components/indexer/loaders/txt_loader.py (2)
openrag/components/indexer/loaders/base.py (2)
  • BaseLoader (19-151)
  • aload_document (37-43)
openrag/utils/logger.py (1)
  • get_logger (14-46)
openrag/utils/exceptions/vectordb.py (1)
openrag/utils/exceptions/base.py (1)
  • VDBError (35-39)
tests/api_tests/test_openai_compat.py (1)
tests/api_tests/conftest.py (1)
  • api_client (16-19)
openrag/components/indexer/loaders/__init__.py (1)
openrag/components/indexer/loaders/base.py (1)
  • BaseLoader (19-151)
openrag/components/indexer/utils/files.py (2)
openrag/components/indexer/indexer.py (1)
  • serialize_file (58-66)
openrag/components/ray_utils.py (1)
  • call_ray_actor_with_timeout (11-55)
openrag/components/indexer/chunker/chunker.py (2)
openrag/components/indexer/chunker/utils.py (3)
  • MDElement (20-34)
  • split_md_elements (56-110)
  • get_chunk_page_number (113-148)
openrag/components/indexer/embeddings/base.py (1)
  • BaseEmbedding (5-20)
openrag/components/indexer/vectordb/utils.py (2)
openrag/components/indexer/vectordb/vectordb.py (6)
  • list_partition_files (57-58)
  • list_partition_files (718-735)
  • create_user (863-869)
  • get_user_by_token (887-888)
  • remove_partition_member (918-921)
  • update_partition_member_role (902-905)
openrag/routers/users.py (1)
  • create_user (82-98)
openrag/components/indexer/indexer.py (3)
openrag/components/indexer/chunker/chunker.py (1)
  • split_document (288-314)
openrag/components/indexer/utils/files.py (1)
  • serialize_file (73-86)
tests/api_tests/test_indexer.py (1)
  • get_task_id (43-49)
openrag/scripts/restore.py (1)
openrag/scripts/backup.py (1)
  • load_openrag_config (181-201)
automatic-evaluation-pipeline/benchmark.py (2)
openrag/components/indexer/vectordb/utils.py (2)
  • to_dict (55-58)
  • to_dict (76-81)
openrag/utils/exceptions/base.py (1)
  • to_dict (20-24)
openrag/scripts/embed.py (2)
openrag/scripts/restore.py (1)
  • read_vdb_section (122-167)
openrag/scripts/backup.py (1)
  • open_output_file (136-162)
openrag/components/indexer/vectordb/vectordb.py (5)
openrag/components/indexer/vectordb/utils.py (6)
  • list_partition_files (165-188)
  • file_exists_in_partition (288-294)
  • remove_file_from_partition (233-250)
  • create_user (298-325)
  • list_partition_members (415-428)
  • update_partition_member_role (451-458)
openrag/components/indexer/embeddings/base.py (1)
  • BaseEmbedding (5-20)
openrag/components/indexer/embeddings/__init__.py (2)
  • EmbeddingFactory (9-18)
  • get_embedder (11-18)
openrag/utils/exceptions/vectordb.py (3)
  • VDBConnectionError (4-13)
  • UnexpectedVDBError (109-118)
  • VDBDeleteError (37-46)
openrag/routers/users.py (1)
  • create_user (82-98)
🪛 LanguageTool
LINTING.md

[uncategorized] ~68-~68: The official name of this software platform is spelled with a capital “H”.
Context: ...Integration A GitHub Actions workflow (.github/workflows/lint.yml) automatically runs...

(GITHUB)

⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: api-tests

✏️ Tip: You can disable this entire section by setting review_details to false in your review settings.

Comment on lines 219 to 221
num_port = os.environ.get("APP_PORT")
num_host = os.environ["APP_URL"]
openrag_api_base_url = f"http://{num_host}:{num_port}"

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

Inconsistent environment variable access patterns.

Line 219 uses .get() (returns None if unset), while line 220 uses direct access (raises KeyError if unset). If APP_PORT is not set, the URL becomes "http://host:None" which will fail silently with confusing errors.

Suggested fix for consistency
-    num_port = os.environ.get("APP_PORT")
-    num_host = os.environ["APP_URL"]
+    num_port = os.environ["APP_PORT"]
+    num_host = os.environ["APP_URL"]

Or use a helper function with clear error messages for both.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
num_port = os.environ.get("APP_PORT")
num_host = os.environ["APP_URL"]
openrag_api_base_url = f"http://{num_host}:{num_port}"
num_port = os.environ["APP_PORT"]
num_host = os.environ["APP_URL"]
openrag_api_base_url = f"http://{num_host}:{num_port}"
🤖 Prompt for AI Agents
In `@automatic-evaluation-pipeline/benchmark.py` around lines 219 - 221, The code
inconsistently reads environment variables: change the access for APP_PORT to
match APP_URL (or vice versa) so both are validated consistently; either use
os.environ["APP_PORT"] and os.environ["APP_URL"] to raise clear KeyError when
missing, or introduce a small helper (e.g., get_env_or_raise) and call it for
both keys, then use the validated values (num_port, num_host) to build
openrag_api_base_url, ensuring the error message mentions the missing variable
name for easier debugging.

Comment thread LINTING.md
Comment on lines +86 to +111
```bash
cat > .git/hooks/pre-commit << 'EOF'
#!/bin/bash

# Get list of staged Python files
FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')

if [ -z "$FILES" ]; then
exit 0
fi

# Run ruff check (will auto-fix if possible)
echo "Running ruff check..."
uv run ruff check $FILES --fix

# Run ruff format
echo "Running ruff format..."
uv run ruff format $FILES

# Re-add the modified files
git add $FILES

# Run ruff check again without --fix to catch remaining errors
echo "Checking for remaining issues..."
uv run ruff check $FILES

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

Pre-commit snippet should handle filenames with spaces safely.
Unquoted $FILES will break on paths with spaces. Consider using a bash array via mapfile.

Suggested doc snippet update
-FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
+mapfile -t FILES < <(git diff --cached --name-only --diff-filter=ACM | grep -E '\.py$')

-if [ -z "$FILES" ]; then
+if [ ${`#FILES`[@]} -eq 0 ]; then
   exit 0
 fi

-uv run ruff check $FILES --fix
+uv run ruff check --fix "${FILES[@]}"

-uv run ruff format $FILES
+uv run ruff format "${FILES[@]}"

-git add $FILES
+git add "${FILES[@]}"

-uv run ruff check $FILES
+uv run ruff check "${FILES[@]}"
🤖 Prompt for AI Agents
In `@LINTING.md` around lines 86 - 111, The pre-commit hook currently collects
staged Python files into the unquoted FILES variable which breaks on paths with
spaces; change the collection to a bash array (e.g., use mapfile -t FILES <
<(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')) and then use
the array safely everywhere by expanding as "${FILES[@]}" when invoking commands
(uv run ruff check/format, git add) and when testing emptiness use: if [
${`#FILES`[@]} -eq 0 ]; then exit 0; fi; update usages of FILES in the hook to the
array form to ensure filenames with spaces are handled correctly.

Comment thread openrag/api.py
Comment on lines +17 to +22
ray.init(dashboard_host="0.0.0.0")

# Apply noqa: E402 to ignore "module level import not at top of file" cause ray.init has to be called first

# flake8: noqa: E402

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 | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

head -25 openrag/api.py | cat -n

Repository: linagora/openrag

Length of output: 1006


🏁 Script executed:

rg -n --type=py '\bray\.init\b' -C2

Repository: linagora/openrag

Length of output: 396


🏁 Script executed:

fd -e py -x grep -l "uvicorn\|FastAPI" | head -10

Repository: linagora/openrag

Length of output: 102


Avoid unconditional ray.init at module import time.
This causes ray.init to run on every uvicorn reload, which fails or hangs without initialization guard.

🛠️ Guard against re-initialization
-ray.init(dashboard_host="0.0.0.0")
+if not ray.is_initialized():
+    ray.init(dashboard_host="0.0.0.0", ignore_reinit_error=True)
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
ray.init(dashboard_host="0.0.0.0")
# Apply noqa: E402 to ignore "module level import not at top of file" cause ray.init has to be called first
# flake8: noqa: E402
if not ray.is_initialized():
ray.init(dashboard_host="0.0.0.0", ignore_reinit_error=True)
# Apply noqa: E402 to ignore "module level import not at top of file" cause ray.init has to be called first
# flake8: noqa: E402
🤖 Prompt for AI Agents
In `@openrag/api.py` around lines 17 - 22, The module currently calls
ray.init(dashboard_host="0.0.0.0") unconditionally at import time; remove that
top-level init and instead initialize Ray only when needed by wrapping the call
with a guard such as if not ray.is_initialized(): ray.init(...), and/or move
initialization into your application startup hook (e.g., a function decorated
with app.on_event("startup") or a dedicated init_ray() function invoked from
when the server actually starts). Update references to ray.init in openrag.api
to use ray.is_initialized() before calling and ensure no unconditional
import-time side effects remain.

Comment thread openrag/app_front.py
Comment on lines +64 to 65
os.environ["CHAINLIT_AUTH_SECRET"] = "default_secret_for_openrag_ui" # Set default value

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 | 🟠 Major

Default auth secret is an insecure fallback.

Hard‑coding a known CHAINLIT_AUTH_SECRET undermines session integrity if it reaches production. Prefer failing fast (or gating via an explicit dev‑only flag) when AUTH_TOKEN is enabled and the secret is missing. Line 64–65.

💡 Safer default
 if AUTH_TOKEN:
     if not CHAINLIT_AUTH_SECRET:
-        os.environ["CHAINLIT_AUTH_SECRET"] = "default_secret_for_openrag_ui"  # Set default value
+        logger.error("CHAINLIT_AUTH_SECRET must be set when AUTH_TOKEN is enabled.")
+        raise RuntimeError("Missing CHAINLIT_AUTH_SECRET")
🤖 Prompt for AI Agents
In `@openrag/app_front.py` around lines 64 - 65, Remove the hard-coded fallback
for os.environ["CHAINLIT_AUTH_SECRET"] and instead fail fast when authentication
is enabled: check the auth toggle (e.g., AUTH_TOKEN or similar env/config flag)
and if it is set but CHAINLIT_AUTH_SECRET is missing/empty, raise an exception
(or call sys.exit) with a clear message; optionally allow an explicit dev
override (e.g., OPENRAG_DEV=true) to set a non-production default, but do not
silently set a known secret. Ensure you update the code that referenced
os.environ["CHAINLIT_AUTH_SECRET"] to rely on the validated environment value.

Comment on lines +53 to 55
async def chunk(self, doc: Document, file_path: str, task_id: str = None) -> list[Document]:
chunks = await self.chunker.split_document(doc, task_id)
return chunks

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

Inconsistent type annotation: task_id allows None but is typed as str.

The default value is None, but the type hint is str. This should be str | None = None to be consistent with the other modernized type annotations in this file (e.g., lines 71-73).

Suggested fix
-    async def chunk(self, doc: Document, file_path: str, task_id: str = None) -> list[Document]:
+    async def chunk(self, doc: Document, file_path: str, task_id: str | None = None) -> list[Document]:
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def chunk(self, doc: Document, file_path: str, task_id: str = None) -> list[Document]:
chunks = await self.chunker.split_document(doc, task_id)
return chunks
async def chunk(self, doc: Document, file_path: str, task_id: str | None = None) -> list[Document]:
chunks = await self.chunker.split_document(doc, task_id)
return chunks
🤖 Prompt for AI Agents
In `@openrag/components/indexer/indexer.py` around lines 53 - 55, The task_id
parameter in the chunk method has a default of None but is annotated as str;
update the type annotation to allow None (use modern union syntax) so it reads
task_id: str | None = None in the async def chunk(self, doc: Document,
file_path: str, task_id: str | None = None) signature (the method uses
chunker.split_document and is defined as chunk in indexer.py).

Comment on lines +18 to 21
async def pdf_to_images(pdf_path: str, scale: float = 1.0) -> list[Image.Image]:
pdf: pdfium.PdfDocument = await asyncio.to_thread(pdfium.PdfDocument, pdf_path)
return [p.render(scale=scale).to_pil() for p in pdf]

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion | 🟠 Major

🧩 Analysis chain

🌐 Web query:

pypdfium2 PdfDocument context manager close cleanup

💡 Result:

Short answer: use the PdfDocument as a context manager — it calls close() on exit and frees PDFium resources (children/pages are closed automatically). If you opened the PDF from a file-like/byte-stream, pass autoclose=True (default for some constructors) or ensure you close the stream yourself so the underlying file handle/buffer is released. Re-closing is a no-op.

Examples and notes

  • With statement (recommended): with pdfium.PdfDocument("file.pdf") as pdf: ... — document is closed on exit. [1][2]
  • If you supplied a byte/stream, PdfDocument can hold a data-holder and optionally autoclose the stream on finalization; use autoclose or manually close the stream before/during PdfDocument.close(). [2]
  • Closing a parent document automatically closes derived children (pages, form envs). Attempting to close an already-closed object is ignored. [1][2]
  • For custom file-access callbacks or buffers, keep the buffer/callback objects alive until you call close() to avoid use-after-free (see PDFium notes). [3]

Sources

  • pypdfium2 Python API docs. [1]
  • pypdfium2 _helpers.document source (context manager, autoclose behavior). [2]
  • pypdfium2 GitHub / PDFium lifetime notes (custom file access / buffers). [3]

Use context manager for PdfDocument to prevent file-handle leaks.
PdfDocument requires explicit cleanup; use a context manager (with statement) to automatically close the document and release resources.

♻️ Suggested fix
 async def pdf_to_images(pdf_path: str, scale: float = 1.0) -> list[Image.Image]:
-    pdf: pdfium.PdfDocument = await asyncio.to_thread(pdfium.PdfDocument, pdf_path)
-    return [p.render(scale=scale).to_pil() for p in pdf]
+    def _load_pdf():
+        with pdfium.PdfDocument(pdf_path) as pdf:
+            return [p.render(scale=scale).to_pil() for p in pdf]
+    return await asyncio.to_thread(_load_pdf)
🤖 Prompt for AI Agents
In `@openrag/components/indexer/loaders/pdf_loaders/openai.py` around lines 18 -
21, The pdf_to_images function opens pdfium.PdfDocument without closing it;
modify it to use a context manager so the PdfDocument is automatically closed
(e.g., run a synchronous helper inside asyncio.to_thread that does "with
pdfium.PdfDocument(pdf_path) as pdf: return [p.render(scale=scale).to_pil() for
p in pdf]"), preserving the async signature and returning the same list of PIL
images; update references in pdf_to_images to use that context-managed call to
avoid file-handle leaks.

@Ahmath-Gadji
Ahmath-Gadji deleted the feat/add-ruff-linting branch January 22, 2026 15:48
This was referenced Feb 4, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Feb 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

chore No production code impact, typically improve tooling, code quality, etc

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant