Skip to content

Feat/add ruff linting - #209

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

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

Conversation

@Ahmath-Gadji

@Ahmath-Gadji Ahmath-Gadji commented Jan 15, 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 document reranking to improve result relevance.
    • Enhanced evaluation pipeline with judge-based answer assessment and scoring.
  • Bug Fixes

    • Improved error message formatting across APIs for clearer diagnostics.
  • Chores

    • Added Ruff linting and CI lint workflow.
    • Modernized codebase to Python 3.10+ typing style.
  • Documentation

    • Added linting setup and best-practice guide.

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

@Ahmath-Gadji
Ahmath-Gadji marked this pull request as draft January 15, 2026 13:41
@coderabbitai

coderabbitai Bot commented Jan 15, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

Modernized type hints across the codebase to Python 3.10+ syntax (built-in generics and | unions), added Ruff linting config and CI workflow, and applied formatting/exception-message consistency changes across many modules; also added minor runtime wiring (Reranker client) and evaluation pipeline enhancements.

Changes

Cohort / File(s) Summary
Type Modernization: Mock API & Tests
.github/workflows/api_tests/mock_vllm.py, tests/api_tests/*
Replaced List/Optional/Union with list[...] and `type
Type Modernization: Core Loaders
openrag/components/indexer/loaders/base.py, openrag/components/indexer/loaders/eml_loader.py, openrag/components/indexer/loaders/txt_loader.py, openrag/components/indexer/loaders/serializer.py, openrag/components/indexer/loaders/pdf_loaders/*, openrag/components/indexer/loaders/image.py
Updated loader signatures to `file_path: str
Chunking & Chunk Utils
openrag/components/indexer/chunker/chunker.py, openrag/components/indexer/chunker/utils.py
Migrated to `
Indexer, Vector DB & Vectordb Utils
openrag/components/indexer/indexer.py, openrag/components/indexer/vectordb/vectordb.py, openrag/components/indexer/vectordb/utils.py
Modernized public signatures (list/dict generics and `
Embeddings & Embedding Helpers
openrag/components/indexer/embeddings/__init__.py, openrag/components/indexer/embeddings/openai.py
Switched to list[...] and dict[...] types, reordered __all__, and updated some error-message formatting; generate_fake_embedding/response helpers updated.
Routing, API & OpenAPI
openrag/api.py, openrag/chainlit_api.py, openrag/routers/*
Updated endpoint signatures to new typing style, reformatted decorators to multi-line in many routers, added FastAPI app version wiring, and standardized exception message {e!s} formatting.
Models & OpenAI Compatibility
openrag/models/indexer.py, openrag/models/openai.py
Replaced many legacy OpenAI response classes with a smaller set; modernized model field types to `
LLM, Reranker, Retriever, Utils
openrag/components/llm.py, openrag/components/reranker.py, openrag/components/retriever.py, openrag/components/utils.py, openrag/components/map_reduce.py, openrag/components/pipeline.py
Minor runtime wiring (Reranker client + semaphore), ClassVar annotations, public attribute additions, and concise exception/message formatting; types modernized.
Scripts & Utilities
openrag/scripts/*, openrag/utils/files.py, openrag/routers/utils.py, openrag/components/prompts/prompts.py
Updated public function signatures to built-in generics, normalized file open calls (removed explicit mode), and applied stylistic/formatting cleanups.
Evaluation Pipeline
automatic-evaluation-pipeline/benchmark.py, automatic-evaluation-pipeline/generate_questions.py, automatic-evaluation-pipeline/upload_files.py
Reorganized imports, added HTTP/cluster dependencies, introduced judge/OpenRAG task concurrency and enhanced evaluation aggregation and reporting.
Linting & CI
.github/workflows/lint.yml, LINTING.md, pyproject.toml
Added Ruff configuration and CI workflow, documentation for linting, and ruff dependency in pyproject.
Tests & Test Cleanup
tests/*, tests/api_tests/*
Formatting and minor test cleanup (removed unused imports, consolidated assertions, adjusted payload formatting).

Sequence Diagram(s)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Suggested labels

feat

Suggested reviewers

  • dodekapod

"🐰
Types now gleam in morning light,
Pipes and unions hopping right,
Ruff patrols the tidy trail,
I nibble bugs and leave no trail. 🥕"

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.51% 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' accurately reflects the main change: adding Ruff linting automation and configuration to the codebase via a new GitHub Actions workflow and linting guide.

✏️ 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 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: 15

Caution

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

⚠️ Outside diff range comments (10)
automatic-evaluation-pipeline/generate_questions.py (2)

154-158: Missing null check before unpacking all_chunks_list.

get_all_chunks() returns None on failure (line 126), but the code proceeds to unpack all_chunks_list without validation. This will cause a TypeError when iterating over None.

Proposed fix
     all_chunks_list = await get_all_chunks(url)
+    if not all_chunks_list:
+        logger.error("Failed to retrieve chunks. Exiting.")
+        return
     pause = time.time()

179-186: HDBSCAN computation is dead code.

Lines 180-181 compute HDBSCAN labels, but lines 184-186 immediately overwrite the labels variable with DBSCAN results. The HDBSCAN computation is wasted.

Either remove the unused HDBSCAN code, or make the clustering algorithm selectable via configuration.

Option 1: Remove dead HDBSCAN code
-    # Here you have the choice up clustering using HDBSCAN
-    hdb = hdbscan.HDBSCAN(min_cluster_size=5, metric="euclidean")
-    labels = hdb.fit_predict(embeddings)
-
-    # or dbscan
+    # Clustering using DBSCAN
     db = DBSCAN(eps=0.1, min_samples=3, metric="cosine")
     db_labels = db.fit(embeddings)
     labels = db_labels.labels_

This would also allow removing the hdbscan import on line 8.

openrag/components/indexer/loaders/pdf_loaders/docling2.py (1)

71-77: Bug: config should be self.config on line 77.

Line 76 creates self.config via a local load_config() call, but line 77 references the module-level config variable instead. This inconsistency makes the instance config partially unused and could cause subtle issues if configurations differ.

🐛 Proposed fix
     def __init__(self):
         from config import load_config
         from utils.logger import get_logger

         self.logger = get_logger()
         self.config = load_config()
-        self.pool_size = config.loader.get("docling_pool_size", 1)
+        self.pool_size = self.config.loader.get("docling_pool_size", 1)
openrag/components/reranker.py (1)

37-40: Add timeout to external rerank API call.

The async call to the external rerank service has no timeout. If the service is slow or unresponsive, this will block indefinitely, potentially exhausting the semaphore slots and causing request pile-up.

Consider wrapping the call with asyncio.wait_for():

Proposed fix
             try:
-                rerank_result: ReRankResult = await rerank.asyncio(
-                    client=self.client, body=rerank_input
+                rerank_result: ReRankResult = await asyncio.wait_for(
+                    rerank.asyncio(client=self.client, body=rerank_input),
+                    timeout=30.0,  # Configure as needed
                 )
automatic-evaluation-pipeline/benchmark.py (1)

294-306: Logic bug: nDCG scores may misalign with valid scores when errors occur mid-list.

The current slicing nDCG_scores[:len(valid_scores)] assumes errors only occur at the end. If an error occurs at any other position, the nDCG scores won't correspond to the correct questions.

For example, if question 2 fails:

  • llm_judge_scores = [score1, "error", score3, score4, score5]
  • valid_scores = [score1, score3, score4, score5]
  • valid_ndcg_scores = nDCG_scores[:4] = [ndcg1, ndcg2, ndcg3, ndcg4]

This incorrectly pairs score3 with ndcg2.

🐛 Proposed fix: Filter nDCG scores alongside judge scores
-    # 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
+    # Filter out error responses and keep corresponding nDCG scores aligned
+    valid_scores = []
+    valid_ndcg_scores = []
+    for (comp, prec), ndcg in zip(llm_judge_scores, nDCG_scores):
+        if comp != "error":
+            valid_scores.append((comp, prec))
+            valid_ndcg_scores.append(ndcg)
openrag/routers/tools.py (1)

54-58: Bug: Generator expression is always truthy, validation never fails.

The condition if not (t.name == name for t in AVAILABLE_TOOLS) creates a generator object, which is always truthy regardless of what it yields. This means the tool name validation is effectively bypassed — any tool name will pass this check.

Use any() to properly evaluate the generator:

🐛 Proposed fix
-    if not (t.name == name for t in AVAILABLE_TOOLS):
+    if not any(t.name == name for t in AVAILABLE_TOOLS):
openrag/api.py (1)

211-220: Duplicate openai_router inclusion when Chainlit UI is enabled.

When WITH_OPENAI_API is True AND WITH_CHAINLIT_UI is True, the openai_router will be included twice (lines 213 and 220), which will cause route conflicts or duplicate endpoint registrations.

Proposed fix
 if WITH_CHAINLIT_UI:
     # Mount the default front
     from chainlit.utils import mount_chainlit

     mount_chainlit(app, "./app_front.py", path="/chainlit")
-    app.include_router(openai_router, prefix="/v1", tags=[Tags.OPENAI])  # cause chainlit uses openai api endpoints
+    # Include openai_router only if not already included
+    if not WITH_OPENAI_API:
+        app.include_router(openai_router, prefix="/v1", tags=[Tags.OPENAI])  # cause chainlit uses openai api endpoints
openrag/components/indexer/loaders/serializer.py (1)

39-50: Mutable default argument and None handling issue.

Two issues here:

  1. Mutable default argument: Using = {} as a default value is a Python antipattern because the same dict instance is shared across all calls.

  2. Type/default mismatch: The type annotation dict | None suggests None is a valid argument, but if a caller explicitly passes None, line 47's metadata.get("file_id") will raise AttributeError: 'NoneType' object has no attribute 'get'.

Other loaders in this PR use = None with an explicit guard, which is the safer pattern.

🐛 Proposed fix
     async def serialize_document(
         self,
         task_id: str,
         path: str | Path,
-        metadata: dict | None = {},
+        metadata: dict | None = None,
     ) -> Document:
+        if metadata is None:
+            metadata = {}
         # Set task state
         log = self.logger.bind(
openrag/components/indexer/chunker/chunker.py (1)

112-132: Potential TypeError when llm_config is None.

The parameter llm_config accepts None as a default, but line 124 calls ChatOpenAI(**llm_config) which will raise TypeError: 'NoneType' object is not a mapping if llm_config is None.

🐛 Proposed fix - add guard or change default

Option 1: Add a guard before using:

     def __init__(
         self,
         chunk_size: int = 200,
         chunk_overlap_rate: float = 0.2,
         llm_config: dict | None = None,
         contextual_retrieval: bool = False,
         **kwargs,
     ):
         self.chunk_size = chunk_size
         self.chunk_overlap_rate = chunk_overlap_rate
         self.chunk_overlap = int(self.chunk_size * self.chunk_overlap_rate)

+        if llm_config is None:
+            raise ValueError("llm_config is required")
         self.llm = ChatOpenAI(**llm_config)

Option 2: Change signature to require llm_config (remove None default):

     def __init__(
         self,
         chunk_size: int = 200,
         chunk_overlap_rate: float = 0.2,
-        llm_config: dict | None = None,
+        llm_config: dict,
         contextual_retrieval: bool = False,
         **kwargs,
     ):
openrag/components/indexer/indexer.py (1)

219-236: Mutable default argument filter: dict | None = {}.

Same issue as above - using {} as a default value shares the same dictionary instance across calls.

🐛 Proposed fix
     `@ray.method`(concurrency_group="search")
     async def asearch(
         self,
         query: str,
         top_k: int = 5,
         similarity_threshold: float = 0.80,
         partition: str | list[str] | None = None,
-        filter: dict | None = {},
+        filter: dict | None = None,
     ) -> list[Document]:
         partition_list = self._check_partition_list(partition)
         vectordb = ray.get_actor("Vectordb", namespace="openrag")
+        if filter is None:
+            filter = {}
         return await vectordb.async_search.remote(
🤖 Fix all issues with AI agents
In @.github/workflows/lint.yml:
- Around line 26-29: Replace the manual curl installer block that adds uv (the
"Install uv" step) with the official astral-sh/setup-uv action (use
astral-sh/setup-uv@v7) to pin the version and enable automatic caching; remove
the manual $HOME/.local/bin append and any separate caching steps since setup-uv
manages them, and pass a pinned version via the action's "version" input to
ensure reproducible installs.

In `@LINTING.md`:
- Line 107: Fix the typo in the LINTING.md text by replacing the misspelled word
"unecessary" with the correct spelling "unnecessary" in the sentence "Sometimes,
you will have to ignore a particular check if deemed unecessary".
- Around line 88-95: The pre-commit hook uses an incorrect command and
auto-stages all files; replace the line "uv run git add ." with a plain git
invocation that only updates tracked files (e.g., "git add -u") or remove the
auto-staging entirely so developers control what gets committed; update the hook
script block where the commands are defined (the here-doc creating
.git/hooks/pre-commit) to call "git add -u" (or delete that line) and ensure the
other commands remain "uv run ruff check . --fix" and "uv run ruff format .",
then keep the chmod +x step.

In `@openrag/api.py`:
- Around line 68-72: The type hints for WITH_CHAINLIT_UI and WITH_OPENAI_API are
incorrect: change their annotations from "bool | None" to "bool" because the
expressions os.getenv(..., "true").lower() == "true" always produce a boolean;
update the variable declarations (WITH_CHAINLIT_UI and WITH_OPENAI_API) to use
type hint bool and leave the runtime expression as-is.

In `@openrag/components/indexer/indexer.py`:
- Around line 58-64: The add_file function uses a mutable default for metadata
(metadata: dict | None = {}), which can be shared across calls; change the
signature to use None as the default (metadata: dict | None = None or
Optional[dict]) and inside add_file initialize a fresh dict when metadata is
None (e.g., metadata = {} if metadata is None) before any modifications; update
any type hints or callers if needed and ensure the same pattern for other
mutable defaults like path if applicable.

In `@openrag/components/indexer/loaders/base.py`:
- Around line 37-43: The abstract async method aload_document is missing the
instance parameter; update its signature to include self as the first parameter
(async def aload_document(self, file_path: str | Path, metadata: dict | None =
None, save_markdown: bool = False):) and ensure any subclasses implement the
matching signature so the abstractmethod contract is satisfied.

In `@openrag/components/indexer/loaders/eml_loader.py`:
- Line 30: The type annotation for function aload_document is inconsistent
because it declares metadata: dict = None; update the signature to use a proper
PEP 604 union so the default None matches the type—e.g., change metadata to dict
| None = None (or Optional[dict] if using typing) while leaving save_markdown:
bool = False unchanged; update any related type hints or callers if needed to
reflect the new annotation for the aload_document method.
- Around line 232-238: The call to self.get_image_description uses the wrong
keyword argument name; change the call in eml_loader.py (where image =
Image.open... and caption = await self.get_image_description(image=image)) to
use the expected parameter name image_data (i.e., caption = await
self.get_image_description(image_data=image)) so it matches the base loader's
get_image_description signature.
- Around line 193-194: The call uses the wrong keyword for
get_image_description; change the invocation to pass the image under the correct
parameter name (e.g., await self.get_image_description(image_data=image)) or
pass the image positionally (await self.get_image_description(image)) so the
base method parameter image_data matches and avoids the TypeError; update the
call near where Image.open(...) is assigned before caption to use image_data.
- Around line 204-206: The condition referencing fallback_success can raise
NameError for non-PDF files; initialize fallback_success to False before any
loader attempts (e.g., at the start of the relevant function or before the PDF
handling block) or change the conditional in the branch that contains 'elif
file_ext in [".txt", ".docx", ".doc"] or (file_ext == ".pdf" and not
fallback_success)' so that fallback_success is only evaluated when file_ext ==
".pdf" (for example, split into two separate conditions), ensuring
fallback_success is always defined before use and avoiding referencing it for
non-PDF extensions.

In `@openrag/components/indexer/loaders/pdf_loaders/openai.py`:
- Around line 18-20: The module-level function pdf_to_images incorrectly
declares a self parameter; remove self from its signature so it becomes async
def pdf_to_images(pdf_path: str, scale: float = 1.0) -> list[Image.Image],
adjust the body accordingly (no use of self), and update any call sites that
currently pass an instance as the first argument to instead call
pdf_to_images(pdf_path, scale) so arguments line up correctly.

In `@openrag/components/indexer/utils/files.py`:
- Line 70: The function serialize_file uses a mutable default for metadata
(metadata: dict | None = {}), which can cause shared-state bugs; change the
signature to default metadata to None (e.g., metadata: dict | None = None) and
inside the function initialize a fresh dict when needed (e.g., if metadata is
None: metadata = {}), ensuring serialize_file and any callers (by name) no
longer rely on a shared mutable default.

In `@openrag/models/openai.py`:
- Around line 55-59: The ChoiceLogprobs model has a type mismatch: the tokens
field is annotated as list[str] but given a default of None; update the tokens
declaration in class ChoiceLogprobs so its type matches the default (make it
list[str] | None) or provide a non-None default consistent with list[str];
adjust the tokens field signature to be tokens: list[str] | None = Field(None)
(or set tokens: list[str] = Field(default_factory=list)) so Pydantic validation
and typing are consistent.

In `@openrag/routers/indexer.py`:
- Line 282: The put_file endpoint is calling indexer.add_file.remote without
passing the current user, but add_file (in
openrag/components/indexer/indexer.py) accepts user: dict | None; update the
call in routers/indexer.py (the put_file handler) to include user=user (e.g.,
task = indexer.add_file.remote(path=file_path, metadata=metadata,
partition=partition, user=user)); ensure the handler uses the same user variable
used by the add_file endpoint (or obtains it from the request/auth dependency)
so the user dict is in scope when invoking indexer.add_file.remote.

In `@openrag/scripts/restore.py`:
- Around line 268-270: The --user-id argument is defined without type=int
causing args.user_id to be a string; update the parser.add_argument call for
"-u/--user-id" to include type=int so args.user_id is an integer before it's
passed to add_file_to_partition (and any other functions expecting an int),
ensuring downstream calls like add_file_to_partition receive the correct type.
🧹 Nitpick comments (12)
utility/data_indexer.py (1)

65-65: Consider removing or converting this debug print statement.

This appears to be leftover debug code. Since the rest of the file uses logger for output, consider either removing this line or converting it to a logger call for consistency.

Suggested fix
-print(dir_path.is_dir())
+logger.debug(f"Directory check: {dir_path.is_dir()}")

Or simply remove if not needed for debugging.

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

143-148: Minor inconsistency with test_chunking.py.

This formatting change is fine, but note that test_chunking.py (lines 183-190) has the same test_multiline_spacing test with a multi-line assertion format. Consider aligning the assertion styles across both test files for consistency, or consolidating the duplicate test.

openrag/components/indexer/loaders/pdf_loaders/docling2.py (1)

92-98: Type annotation is misleading for Ray actor handle.

actor is actually a ray.actor.ActorHandle, not a DoclingWorker instance. The type hint is functionally harmless but could confuse readers or cause type-checker warnings.

♻️ Suggested fix
     async def process_pdf(self, file_path: str) -> ConversionResult:
-        actor: DoclingWorker = await self._queue.get()
+        actor: ray.actor.ActorHandle = await self._queue.get()
openrag/components/reranker.py (2)

41-46: Mutating input documents may cause unintended side effects.

The method modifies the metadata of the input documents directly. Callers may not expect their original documents to be altered. Consider creating shallow copies to avoid side effects:

Proposed fix
                 output = []
                 for rerank_res in rerank_result.results:
-                    doc = documents[rerank_res.index]
-                    doc.metadata["relevance_score"] = rerank_res.relevance_score
+                    original_doc = documents[rerank_res.index]
+                    doc = Document(
+                        page_content=original_doc.page_content,
+                        metadata={**original_doc.metadata, "relevance_score": rerank_res.relevance_score},
+                    )
                     output.append(doc)

48-55: Use bare raise to preserve full traceback.

Using raise e instead of raise can subtly alter the traceback in some edge cases. The idiomatic Python pattern is to use bare raise:

Proposed fix
             except Exception as e:
                 self.logger.error(
                     "Reranking failed",
                     error=str(e),
                     model_name=self.model_name,
                     documents_count=len(documents),
                 )
-                raise e
+                raise
automatic-evaluation-pipeline/benchmark.py (2)

58-70: Consider simplifying the control flow and removing the external link.

  1. The ChatGPT link in the comment (line 59) is not ideal - external links can break and don't provide context for future maintainers.
  2. The key boolean flag adds unnecessary complexity when you can return directly.
♻️ Suggested simplification
 def compute_inverted_ranks(true_chunk_id, all_retrieved_chunks):
-    # see link: https://chatgpt.com/share/6813f998-2e88-8002-a472-6af2e9a64b61
-    key = False
     try:
         rank = all_retrieved_chunks.index(true_chunk_id) + 1
-        key = True
+        return 1 / rank
     except ValueError:
         logger.debug(f"ValueError: {true_chunk_id} not found in retrieved_ids")
-
-    if key:
-        return 1 / rank
-    else:
         return 0

227-230: Consider making partition configurable via environment variable.

Other configuration values (APP_PORT, APP_URL) are sourced from environment variables, but partition is hardcoded. This inconsistency could lead to errors when running benchmarks against different partitions.

♻️ 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}"
-    partition = "pdftest"  # To replace with your wanted partition's name
+    partition = os.environ.get("PARTITION", "pdftest")
openrag/components/pipeline.py (1)

206-208: LGTM on the formatting change.

The {e!s} conversion is idiomatic and equivalent to str(e).

Minor pre-existing nit: the log message says "chat completion" but this is in the completions method. Consider aligning the message with the method name for clarity.

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

25-27: Consider specifying explicit encoding for cross-platform consistency.

While removing the redundant "r" mode is fine, consider adding an explicit encoding="utf-8" parameter. This ensures consistent behavior across different platforms and locales, and is a common best practice (PEP 597).

♻️ Suggested improvement
-    with open(file_path) as f:
+    with open(file_path, encoding="utf-8") as f:
         sys_msg = f.read()
         return sys_msg
openrag/scripts/embed.py (1)

246-272: Consider extracting duplicate open_output_file function to a shared utility.

This function is duplicated in openrag/scripts/backup.py (lines 139-167). Consider extracting it to a shared utility module to avoid code duplication and ensure consistency.

openrag/api.py (1)

35-41: ray.init() at module import time may cause side effects.

Calling ray.init() at module level means Ray initializes whenever this module is imported, including during tests or when importing for introspection. This can cause unexpected behavior or resource allocation.

Consider deferring initialization to an explicit startup point or guarding it.

Suggested approach
-ray.init(dashboard_host="0.0.0.0")
+# Defer ray.init to avoid side effects on import
+if not ray.is_initialized():
+    ray.init(dashboard_host="0.0.0.0")

Or move initialization inside the if __name__ == "__main__": block.

tests/api_tests/test_indexer.py (1)

73-99: Fixed sleep delay may cause test flakiness.

The time.sleep(2) is a hardcoded delay that may not be sufficient in slow CI environments, or may unnecessarily slow down tests in fast environments. Consider polling for the upload status instead of a fixed sleep.

Alternative approach using polling
# Poll for task completion instead of fixed sleep
for _ in range(10):  # Max 10 attempts
    time.sleep(0.5)
    # Check if file is registered (e.g., via list files endpoint)
    status_resp = api_client.get(f"/partition/{created_partition}/files")
    if status_resp.status_code == 200 and file_id in str(status_resp.json()):
        break

However, if the current 2-second sleep is sufficient for your CI environment and test reliability is acceptable, this can be deferred.

📜 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 773a5af and 66a1897.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (66)
  • .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/utils.py
  • openrag/components/indexer/embeddings/__init__.py
  • openrag/components/indexer/embeddings/openai.py
  • openrag/components/indexer/indexer.py
  • openrag/components/indexer/loaders/__init__.py
  • openrag/components/indexer/loaders/base.py
  • openrag/components/indexer/loaders/eml_loader.py
  • openrag/components/indexer/loaders/image.py
  • openrag/components/indexer/loaders/pdf_loaders/docling2.py
  • openrag/components/indexer/loaders/pdf_loaders/marker.py
  • openrag/components/indexer/loaders/pdf_loaders/openai.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/reranker.py
  • openrag/components/retriever.py
  • openrag/components/utils.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/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
  • 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_tools.py
  • tests/api_tests/test_users.py
  • tests/test_vectordb.py
  • utility/data_indexer.py
💤 Files with no reviewable changes (6)
  • tests/api_tests/test_users.py
  • tests/api_tests/test_queue.py
  • tests/api_tests/test_tools.py
  • tests/api_tests/test_partition.py
  • tests/api_tests/test_extract.py
  • tests/api_tests/test_actors.py
🧰 Additional context used
🧬 Code graph analysis (17)
openrag/components/indexer/loaders/image.py (1)
openrag/components/indexer/loaders/base.py (1)
  • BaseLoader (18-152)
openrag/components/indexer/vectordb/utils.py (1)
openrag/components/indexer/vectordb/vectordb.py (3)
  • list_partition_files (57-58)
  • list_partition_files (718-735)
  • get_user_by_token (887-888)
openrag/components/map_reduce.py (1)
openrag/components/indexer/indexer.py (1)
  • chunk (52-56)
openrag/components/indexer/loaders/__init__.py (1)
openrag/components/indexer/loaders/base.py (1)
  • BaseLoader (18-152)
openrag/components/indexer/utils/test_text_sanitizer.py (2)
openrag/components/indexer/chunker/test_chunking.py (1)
  • test_multiline_spacing (184-191)
openrag/components/indexer/utils/text_sanitizer.py (1)
  • clean_markdown_table_spacing (98-127)
openrag/scripts/embed.py (1)
openrag/scripts/backup.py (1)
  • open_output_file (140-168)
openrag/routers/users.py (2)
openrag/components/indexer/vectordb/utils.py (1)
  • delete_user (424-431)
openrag/components/indexer/vectordb/vectordb.py (1)
  • delete_user (875-882)
openrag/routers/openai.py (1)
openrag/models/openai.py (2)
  • OpenAIChatCompletionRequest (14-30)
  • OpenAICompletionRequest (75-92)
openrag/components/indexer/loaders/pdf_loaders/docling2.py (1)
openrag/config/config.py (1)
  • load_config (12-29)
openrag/api.py (4)
openrag/config/config.py (1)
  • load_config (12-29)
openrag/utils/dependencies.py (1)
  • get_vectordb (47-49)
openrag/utils/exceptions/base.py (1)
  • OpenRAGError (4-24)
openrag/utils/logger.py (1)
  • get_logger (10-50)
automatic-evaluation-pipeline/generate_questions.py (1)
openrag/components/indexer/indexer.py (1)
  • chunk (52-56)
openrag/scripts/backup.py (3)
openrag/components/indexer/vectordb/utils.py (2)
  • list_partition_files (181-204)
  • list_partitions (295-299)
openrag/utils/logger.py (1)
  • get_logger (10-50)
openrag/scripts/embed.py (1)
  • open_output_file (246-272)
tests/api_tests/test_indexer.py (1)
tests/api_tests/conftest.py (4)
  • api_client (16-19)
  • created_partition (87-98)
  • sample_markdown_file (57-77)
  • sample_text_file (39-53)
openrag/components/indexer/indexer.py (2)
openrag/components/indexer/chunker/chunker.py (1)
  • split_document (259-285)
openrag/routers/indexer.py (1)
  • add_file (115-172)
openrag/components/indexer/loaders/eml_loader.py (4)
openrag/components/indexer/loaders/__init__.py (1)
  • get_loader_classes (18-51)
openrag/components/indexer/loaders/base.py (3)
  • BaseLoader (18-152)
  • aload_document (38-43)
  • get_image_description (66-152)
openrag/components/indexer/loaders/image.py (1)
  • aload_document (13-20)
openrag/components/indexer/loaders/pdf_loaders/openai.py (1)
  • aload_document (44-71)
openrag/routers/indexer.py (2)
openrag/components/indexer/indexer.py (3)
  • add_file (58-131)
  • set_state (281-284)
  • copy_file (186-217)
openrag/routers/utils.py (2)
  • validate_metadata (200-208)
  • require_task_owner (156-170)
openrag/components/indexer/chunker/chunker.py (1)
openrag/components/indexer/chunker/utils.py (3)
  • MDElement (20-34)
  • split_md_elements (56-110)
  • get_chunk_page_number (113-148)
🪛 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)


[grammar] ~107-~107: Ensure spelling is correct
Context: ... to ignore a particular check if deemed unecessary python # Example: C417 rule suggest that map here is unnecessary docs_with_tokens = list(map(lambda d: (_length_function(d.page_content), d), docs)) # noqa: C417

(QB_NEW_EN_ORTHOGRAPHY_ERROR_IDS_1)

⏰ 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: index-backup-restore

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

Comment on lines +26 to +29
- name: Install uv
run: |
curl -LsSf https://astral.sh/uv/install.sh | sh
echo "$HOME/.local/bin" >> $GITHUB_PATH

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:

# First, let's examine the actual workflow file to confirm the current implementation
find . -type f -name "lint.yml" -path "*/.github/workflows/*"

Repository: linagora/openrag

Length of output: 88


🌐 Web query:

astral-sh/setup-uv GitHub Action documentation

💡 Result:

I found the official documentation and README for the astral-sh/setup-uv GitHub Action:

  • Repository README (usage, inputs, outputs, advanced config, FAQ). Example install: uses: astral-sh/setup-uv@v7. Includes inputs like version, version-file, python-version, activate-environment, working-directory, checksum; outputs like uv-version, uv-path, venv, cache-hit. [1]

  • Astral docs guide for using uv in GitHub Actions (recommendation and example, showing pinning uv via the action). [2]

  • GitHub Marketplace entry for the action. [3]

If you want, I can paste the README sections (usage, inputs, outputs) or produce a ready-to-drop workflow snippet tailored to your OS and Python matrix.


🌐 Web query:

uv package manager latest version 2025 2026

💡 Result:

As of Jan 15, 2026:

  • Latest uv release: 0.9.25 — released Jan 13, 2026. [1][2]
  • Latest release in 2025: 0.9.21 — released Dec 30, 2025. [3]

Sources: GitHub Releases (uv 0.9.25) [1], PyPI (uv 0.9.25) [2], GitHub release list showing 2025 tags (0.9.21 on Dec 30, 2025) [3].


🏁 Script executed:

# Let's examine the actual lint.yml workflow file
cat -n ./.github/workflows/lint.yml

Repository: linagora/openrag

Length of output: 1568


Use the official astral-sh/setup-uv action for better reproducibility and maintenance.

The curl | sh approach installs the latest uv version, which lacks reproducibility. Instead, use the astral-sh/setup-uv action (v7), which provides version pinning, automatic caching, and is maintained by Astral.

♻️ Suggested replacement
-      - name: Install uv
-        run: |
-          curl -LsSf https://astral.sh/uv/install.sh | sh
-          echo "$HOME/.local/bin" >> $GITHUB_PATH
-
-      - name: Cache uv
-        uses: actions/cache@v4
-        with:
-          path: |
-            ~/.cache/uv
-          key: uv-${{ runner.os }}-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml', '**/uv.lock') }}
-          restore-keys: |
-            uv-${{ runner.os }}-${{ matrix.python-version }}-
+      - name: Install uv
+        uses: astral-sh/setup-uv@v7
+        with:
+          version: "0.9.25"

This eliminates the manual cache step since setup-uv handles caching automatically.

🤖 Prompt for AI Agents
In @.github/workflows/lint.yml around lines 26 - 29, Replace the manual curl
installer block that adds uv (the "Install uv" step) with the official
astral-sh/setup-uv action (use astral-sh/setup-uv@v7) to pin the version and
enable automatic caching; remove the manual $HOME/.local/bin append and any
separate caching steps since setup-uv manages them, and pass a pinned version
via the action's "version" input to ensure reproducible installs.

Comment thread LINTING.md
Comment thread LINTING.md Outdated
Comment thread openrag/api.py Outdated
Comment thread openrag/components/indexer/indexer.py
Comment thread openrag/components/indexer/loaders/pdf_loaders/openai.py Outdated
Comment thread openrag/components/indexer/utils/files.py Outdated
Comment thread openrag/models/openai.py Outdated
Comment thread openrag/routers/indexer.py Outdated
Comment thread openrag/scripts/restore.py Outdated
@Ahmath-Gadji
Ahmath-Gadji force-pushed the feat/add-ruff-linting branch 6 times, most recently from ac5502b to a0838a2 Compare January 16, 2026 10:22
@Ahmath-Gadji Ahmath-Gadji added the chore No production code impact, typically improve tooling, code quality, etc label Jan 16, 2026
@Ahmath-Gadji
Ahmath-Gadji marked this pull request as ready for review January 16, 2026 14:29

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

Caution

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

⚠️ Outside diff range comments (8)
automatic-evaluation-pipeline/benchmark.py (1)

294-307: Fix metric misalignment after filtering judge errors.

valid_ndcg_scores = nDCG_scores[: len(valid_scores)] and the chunks_count slice can misalign metrics if errors are interspersed. Filter nDCG and chunks with the same mask used for valid_scores.
Line 294-307.

✅ Suggested fix (aligned filtering)
-    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
-
-    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)]
-    ]
-    eval_results["n_chunks"] = chunks_count
+    valid_rows = [
+        (comp, prec, ndcg, len(ref["chunks"]))
+        for (comp, prec), ndcg, ref in zip(
+            llm_judge_scores, nDCG_scores, list_response_answer_reference
+        )
+        if comp != "error"
+    ]
+    eval_results = pd.DataFrame(
+        valid_rows,
+        columns=["completion_evaluation", "precision_evaluation", "nDCG", "n_chunks"],
+    )
openrag/components/reranker.py (2)

41-46: Input documents are mutated in place.

Line 44 modifies the original Document objects by setting metadata["relevance_score"]. This side effect could surprise callers who don't expect their input to be modified. Consider creating shallow copies of the documents before modifying metadata.

Proposed fix
                 output = []
                 for rerank_res in rerank_result.results:
                     doc = documents[rerank_res.index]
-                    doc.metadata["relevance_score"] = rerank_res.relevance_score
-                    output.append(doc)
+                    # Create a copy to avoid mutating the input
+                    reranked_doc = Document(
+                        page_content=doc.page_content,
+                        metadata={**doc.metadata, "relevance_score": rerank_res.relevance_score},
+                    )
+                    output.append(reranked_doc)
                 return output

37-40: External API call lacks a timeout.

The Client initialization on line 12 does not configure a timeout. Without it, rerank.asyncio() calls can block indefinitely if the rerank service becomes unresponsive, exhausting the 5-slot semaphore and stalling the entire system. Configure a timeout via httpx_args when creating the Client:

self.client = Client(
    base_url=config.reranker["base_url"],
    httpx_args={"timeout": httpx.Timeout(30.0)}  # Adjust value as needed
)

This pattern is already used elsewhere in the codebase for external API calls (llm.py, indexer loaders).

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

51-54: Incorrect docstring: states "plain text files (.txt)" for MarkdownLoader.

The docstring appears to be copied from TextLoader. It should describe Markdown files instead.

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

194-210: Potential TypeError when length_function is None.

The length_function parameter is typed as Callable[[str], int] | None with default None, but lines 209–210 call it without a null check:

  • header_ntoks = length_function(header_text)
  • groups_ntoks = [length_function(g) for g in group_texts]

This will raise TypeError: 'NoneType' object is not callable if the function is called without providing a length function. While all current call sites in the codebase explicitly provide length_function, the function signature permits None, creating a type safety contract violation.

Fix: Either remove the | None and default from the signature, or add a null check with a sensible default:

 def chunk_table(
     table_element: MDElement,
     chunk_size: int = 512,
     length_function: Callable[[str], int] | None = None,
 ) -> list[MDElement]:
+    if length_function is None:
+        length_function = len
     txt = clean_markdown_table_spacing(table_element.content)
openrag/api.py (1)

217-226: Duplicate router registration when both flags are enabled.

When WITH_OPENAI_API=True and WITH_CHAINLIT_UI=True, the openai_router is mounted twice to /v1. This causes duplicate route definitions and may lead to unexpected behavior.

Proposed fix
 if WITH_OPENAI_API:
     # Mount the openai router
     app.include_router(openai_router, prefix="/v1", tags=[Tags.OPENAI])

 if WITH_CHAINLIT_UI:
     # Mount the default front
     from chainlit.utils import mount_chainlit

     mount_chainlit(app, "./app_front.py", path="/chainlit")
-    app.include_router(openai_router, prefix="/v1", tags=[Tags.OPENAI])  # cause chainlit uses openai api endpoints
+    if not WITH_OPENAI_API:
+        # Mount openai router for chainlit if not already mounted
+        app.include_router(openai_router, prefix="/v1", tags=[Tags.OPENAI])
openrag/components/indexer/chunker/chunker.py (1)

141-161: llm_config default of None will cause TypeError at runtime.

The parameter llm_config: dict | None = None suggests it's optional, but line 153 unconditionally unpacks it with ChatOpenAI(**llm_config). If llm_config is None, this raises TypeError: argument after ** must be a mapping, not NoneType.

Proposed fix — add validation or remove None default

Option 1: Add validation

     def __init__(
         self,
         chunk_size: int = 200,
         chunk_overlap_rate: float = 0.2,
         llm_config: dict | None = None,
         contextual_retrieval: bool = False,
         **kwargs,
     ):
+        if llm_config is None:
+            raise ValueError("llm_config is required")
+
         self.chunk_size = chunk_size

Option 2: Remove None default (preferred if always required)

     def __init__(
         self,
         chunk_size: int = 200,
         chunk_overlap_rate: float = 0.2,
-        llm_config: dict | None = None,
+        llm_config: dict,
         contextual_retrieval: bool = False,
         **kwargs,
     ):
openrag/components/indexer/indexer.py (1)

61-80: Guard user before dereference on line 79, or declare it as required.

Line 79 calls user.get("id") unconditionally, but user is typed as dict | None = None. When user is None, this raises AttributeError. Either add a guard before line 79 and handle the None case, or make the user parameter required (remove the default None). Note that line 96 also passes user to insert_documents.remote(), so ensure consistency across both call sites.

🤖 Fix all issues with AI agents
In `@automatic-evaluation-pipeline/benchmark.py`:
- Around line 265-267: The recall computation can raise ZeroDivisionError when
chunk_id_reference is empty; before calculating recall in the block that uses
chunk_id_reference and openrag_chunk_ids (where recall =
len(list(set(chunk_id_reference) & set(openrag_chunk_ids))) /
len(chunk_id_reference)), add a defensive guard that checks if
chunk_id_reference is empty and sets recall to 0.0 (or an appropriate sentinel)
instead of performing the division; keep the rest of the logic unchanged and
ensure references to chunk_id_reference, openrag_chunk_ids, and the recall
variable are used so the check is colocated with the original computation.

In `@LINTING.md`:
- Around line 140-143: Fix the minor grammar in the troubleshooting example
sentence by changing "suggest" to "suggests" in the line referencing the C417
rule (the example comment starting with "# Example: C417 rule suggest that map
here is unnecessary"); update that sentence so it reads "...C417 rule suggests
that map here is unnecessary".

In `@openrag/components/indexer/indexer.py`:
- Around line 329-331: get_all_states currently declares a return type of
dict[str, str] but iterates TaskInfo.state which is typed str | None, so change
the return annotation to dict[str, str | None] and keep the implementation the
same; update the get_all_states signature to reflect dict[str, str | None] and
ensure callers expect optional values from TaskInfo.state (refer to
get_all_states, TaskInfo.state, self.tasks and self.lock to locate the change).
- Around line 213-216: The asearch() method currently uses a mutable default for
the filter parameter (filter: dict | None = {}), which can cause state leakage
across calls; change the signature to accept filter: dict | None = None and
inside asearch (near where partition_list is obtained via _check_partition_list)
initialize a local filters variable with filter or {} (e.g., filters = filter or
{}) before using it so downstream mutations do not affect future calls; update
any references in asearch that expect the old parameter name accordingly.

In `@openrag/components/indexer/loaders/base.py`:
- Around line 94-96: The image-size check currently logs "Image too small..."
but falls through; after the logger.debug inside the conditional that checks
width/height against self.min_width_pixels/self.min_height_pixels, add an early
return (e.g., return None) to stop further description processing for small
images so the method actually skips them; update the containing method (the
function where this if appears) to consistently return the same sentinel (None
or empty string) when skipping.

In `@openrag/components/indexer/loaders/serializer.py`:
- Around line 42-43: The parameter metadata currently uses a mutable default {}
but is typed as dict | None; change the signature to use metadata: dict | None =
None and inside the function (e.g., at start of the function that declares path:
str | Path, metadata: ...) normalize it with something like metadata = metadata
or {} so subsequent calls to metadata.get(...) are safe and you avoid shared
mutable defaults; update the type hint and ensure all uses of metadata assume a
dict after normalization.

In `@openrag/scripts/filter-logs.py`:
- Around line 71-76: The code uses the name UTC (in the assume_tz assignment and
the else branch checking args.tz) which requires Python 3.11; change those uses
to the stdlib timezone.utc and ensure datetime.timezone is imported (replace
references to UTC with timezone.utc wherever assume_tz or the tz fallback is
set, and update the import to include from datetime import timezone or use
datetime.timezone). Target symbols: assume_tz, args.tz, UTC -> timezone.utc,
datetime.now().astimezone().tzinfo.
♻️ Duplicate comments (2)
openrag/components/indexer/loaders/base.py (1)

38-45: Type hints updated correctly; self parameter now present.

The abstract method signature is correct with self included and modernized type hints.

openrag/routers/indexer.py (1)

282-282: The user parameter is now correctly passed to add_file.remote().

This addresses the previous review feedback about the missing user parameter in the put_file endpoint.

🧹 Nitpick comments (9)
openrag/scripts/backup.py (1)

268-270: Consider Pythonic empty-check idiom.

The Yoda-style comparison 0 == len(partitions) works but is non-idiomatic in Python. Consider using not partitions or len(partitions) == 0 for readability.

Suggested change
-    if 0 == len(partitions):
+    if not partitions:
         logger.error("No partitions meet given conditions.")
         return 1
.github/workflows/api_tests/mock_vllm.py (1)

124-128: Minor type hint inconsistency.

The function signature declares text: str, but the implementation includes a fallback branch for non-string content (line 128). While this defensive coding is reasonable given that msg.content can be a list for vision models, the type hint could be more accurate.

Optional: Align type hint with implementation
-def count_tokens(text: str) -> int:
+def count_tokens(text: str | Any) -> int:
     """Approximate token count (roughly 4 chars per token)."""
     if isinstance(text, str):
         return max(1, len(text) // 4)
     return 10  # Default for non-string content

Alternatively, since callers already use str(msg.content), the non-string branch may be dead code and could be removed.

openrag/components/reranker.py (3)

10-17: Consider adding type hints to __init__ parameters.

Given this PR modernizes type annotations, the logger and config parameters could benefit from type hints for consistency.

Suggested improvement
-    def __init__(self, logger, config):
+    def __init__(self, logger: "structlog.BoundLogger", config: "Config"):

Alternatively, if the concrete types aren't easily importable, use a protocol or typing.Any with a docstring.


26-26: Consider early return for empty document list.

If documents is empty, the code still makes an external API call. An early return would avoid unnecessary network overhead.

Suggested improvement
+            if not documents:
+                return []
             top_k = min(top_k, len(documents))

55-55: Use bare raise to preserve the full traceback.

Using raise e resets the traceback origin to this line. A bare raise preserves the original exception context.

Proposed fix
-                raise e
+                raise
LINTING.md (1)

86-107: Handle staged filenames with spaces safely in the pre-commit hook.
Unquoted $FILES will break on paths with spaces/newlines. Consider a null-delimited list and array expansion.

♻️ Suggested doc update
-FILES=$(git diff --cached --name-only --diff-filter=ACM | grep '\.py$')
+mapfile -d '' FILES < <(git diff --cached --name-only --diff-filter=ACM -z | grep -z '\.py$')

-if [ -z "$FILES" ]; then
+if [ ${`#FILES`[@]} -eq 0 ]; then
   exit 0
 fi
 ...
-uv run ruff check $FILES --fix
+uv run ruff check "${FILES[@]}" --fix
 ...
-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[@]}"
openrag/routers/indexer.py (1)

310-317: Type hints updated correctly; consider using dict instead of Any.

The modernization from Optional[Any] to Any | None is correct. However, since validate_metadata returns a parsed JSON object (dict), using dict | None would provide better type safety.

♻️ Optional: More precise type hint
 async def patch_file(
     partition: str,
     file_id: str = Depends(validate_file_id),
-    metadata: Any | None = Depends(validate_metadata),
+    metadata: dict | None = Depends(validate_metadata),

Apply similar change to copy_file_between_partitions.

Also applies to: 355-364

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

52-52: Type task_id as optional to match the default.

task_id defaults to None but is typed as str. Use str | None to align with runtime behavior and the chunker signature.

♻️ Proposed change
-    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]:

58-66: Prefer None check over metadata = metadata or {}.

Using or {} replaces a caller-supplied empty dict, so mutations won’t reflect back to the caller. Use an explicit None check.

♻️ Proposed change
-        metadata = metadata or {}
+        if metadata is None:
+            metadata = {}
📜 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 66a1897 and a0838a2.

⛔ Files ignored due to path filters (1)
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (67)
  • .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/utils.py
  • openrag/components/indexer/embeddings/__init__.py
  • openrag/components/indexer/embeddings/openai.py
  • openrag/components/indexer/indexer.py
  • openrag/components/indexer/loaders/__init__.py
  • openrag/components/indexer/loaders/base.py
  • openrag/components/indexer/loaders/eml_loader.py
  • openrag/components/indexer/loaders/image.py
  • openrag/components/indexer/loaders/pdf_loaders/docling2.py
  • openrag/components/indexer/loaders/pdf_loaders/marker.py
  • openrag/components/indexer/loaders/pdf_loaders/openai.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/reranker.py
  • openrag/components/retriever.py
  • openrag/components/utils.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/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/test_external_resource_errors.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 (6)
  • openrag/utils/test_external_resource_errors.py
  • tests/api_tests/test_actors.py
  • tests/api_tests/test_users.py
  • tests/api_tests/test_extract.py
  • tests/api_tests/test_partition.py
  • tests/api_tests/test_queue.py
✅ Files skipped from review due to trivial changes (4)
  • openrag/routers/extract.py
  • utility/data_indexer.py
  • openrag/components/indexer/loaders/test_media_loader.py
  • openrag/scripts/embed.py
🚧 Files skipped from review as they are similar to previous changes (27)
  • openrag/routers/utils.py
  • openrag/components/pipeline.py
  • tests/test_vectordb.py
  • openrag/routers/tools.py
  • openrag/routers/partition.py
  • automatic-evaluation-pipeline/upload_files.py
  • pyproject.toml
  • openrag/components/indexer/loaders/pdf_loaders/docling2.py
  • openrag/components/indexer/loaders/image.py
  • tests/api_tests/test_openai_compat.py
  • openrag/app_front.py
  • openrag/components/map_reduce.py
  • openrag/components/llm.py
  • openrag/models/indexer.py
  • automatic-evaluation-pipeline/generate_questions.py
  • openrag/components/retriever.py
  • tests/api_tests/test_search.py
  • openrag/components/utils.py
  • openrag/chainlit_api.py
  • openrag/scripts/restore.py
  • openrag/utils/dependencies.py
  • openrag/components/indexer/loaders/init.py
  • tests/api_tests/test_indexer.py
  • openrag/routers/queue.py
  • openrag/routers/search.py
  • .github/workflows/lint.yml
  • openrag/components/indexer/loaders/pdf_loaders/openai.py
🧰 Additional context used
🧬 Code graph analysis (11)
openrag/components/indexer/utils/test_text_sanitizer.py (2)
openrag/components/indexer/chunker/test_chunking.py (1)
  • test_multiline_spacing (184-191)
openrag/components/indexer/utils/text_sanitizer.py (1)
  • clean_markdown_table_spacing (98-127)
openrag/components/indexer/utils/files.py (1)
openrag/components/ray_utils.py (1)
  • call_ray_actor_with_timeout (11-57)
openrag/routers/users.py (2)
openrag/components/indexer/vectordb/utils.py (1)
  • delete_user (424-431)
openrag/components/indexer/vectordb/vectordb.py (1)
  • delete_user (875-882)
openrag/routers/indexer.py (2)
openrag/components/indexer/indexer.py (4)
  • add_file (56-125)
  • set_state (267-270)
  • set_object_ref (299-302)
  • copy_file (176-205)
openrag/routers/utils.py (2)
  • validate_metadata (200-208)
  • require_task_owner (156-170)
openrag/scripts/backup.py (4)
openrag/components/indexer/vectordb/utils.py (2)
  • list_partition_files (181-204)
  • list_partitions (295-299)
openrag/utils/logger.py (1)
  • get_logger (10-50)
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/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/loaders/txt_loader.py (2)
openrag/components/indexer/loaders/base.py (1)
  • BaseLoader (19-159)
openrag/utils/logger.py (1)
  • get_logger (10-50)
openrag/components/indexer/loaders/base.py (1)
openrag/utils/logger.py (1)
  • get_logger (10-50)
openrag/components/indexer/loaders/eml_loader.py (14)
openrag/components/indexer/loaders/__init__.py (1)
  • get_loader_classes (18-51)
openrag/components/indexer/loaders/base.py (3)
  • BaseLoader (19-159)
  • aload_document (39-45)
  • get_image_description (68-159)
openrag/components/indexer/loaders/image.py (1)
  • aload_document (13-20)
openrag/components/indexer/loaders/pdf_loaders/docling2.py (1)
  • aload_document (108-133)
openrag/components/indexer/loaders/pdf_loaders/marker.py (1)
  • aload_document (217-269)
openrag/components/indexer/loaders/pdf_loaders/openai.py (1)
  • aload_document (42-69)
openrag/components/indexer/loaders/txt_loader.py (2)
  • aload_document (26-48)
  • aload_document (65-119)
openrag/components/indexer/loaders/pptx_loader.py (1)
  • aload_document (151-167)
openrag/components/indexer/loaders/media_loader.py (1)
  • aload_document (149-175)
openrag/components/indexer/loaders/doc.py (1)
  • aload_document (17-31)
openrag/components/indexer/loaders/pdf_loaders/pymupdf.py (2)
  • aload_document (14-29)
  • aload_document (36-48)
openrag/components/indexer/loaders/pdf_loaders/docling.py (1)
  • aload_document (65-90)
openrag/components/indexer/loaders/CustomDocLoader.py (1)
  • aload_document (22-41)
openrag/components/indexer/loaders/CustomHTMLLoader.py (1)
  • aload_document (13-22)
openrag/components/indexer/vectordb/utils.py (1)
openrag/components/indexer/vectordb/vectordb.py (3)
  • list_partition_files (57-58)
  • list_partition_files (718-735)
  • get_user_by_token (887-888)
openrag/components/indexer/indexer.py (3)
openrag/components/indexer/chunker/chunker.py (1)
  • split_document (288-314)
openrag/routers/indexer.py (1)
  • add_file (115-172)
tests/api_tests/test_indexer.py (1)
  • get_task_id (43-49)
🪛 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)

🔇 Additional comments (58)
openrag/components/indexer/embeddings/__init__.py (1)

21-21: LGTM — export list reorder is safe.
No functional impact; the module’s public surface remains the same.

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

44-69: LGTM!

The conversion from {str(e)} to {e!s} is the idiomatic Python f-string syntax and aligns with Ruff's formatting preferences. Both changes are consistent and maintain identical behavior.

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

73-78: LGTM — safer default for metadata.
Switching the default to None and normalizing to a dict removes the mutable-default pitfall while keeping downstream usage consistent.

openrag/scripts/backup.py (6)

3-10: LGTM!

Imports are properly organized following PEP 8 conventions (standard library → third-party → local), and the typing imports (IO, Any) are correctly included for the modernized type hints.


13-69: LGTM!

Type hints are correctly modernized to Python 3.10+ syntax (dict[str, dict[str, Any]]). The logic for iterating partitions, fetching files, and writing JSON output is sound with appropriate error handling.


72-137: LGTM!

Type hints correctly modernized. The iterator pattern for batch processing Milvus data is well-implemented with proper error handling. The verbose counter (cnt) is correctly scoped within the verbose condition blocks.


140-168: LGTM!

Implementation is consistent with openrag/scripts/embed.py (same pattern for stdout handling, file existence check, and xz compression support). The function signature with modern type hints is clean.


291-309: LGTM!

Proper use of context manager for file handling ensures resources are cleaned up correctly. The explicit flush() before exit and comprehensive exception handling with logger.exception are good practices.


314-315: LGTM!

Standard entry point pattern with proper exit code propagation.

openrag/scripts/filter-logs.py (3)

13-13: LGTM!

The type hint modernization from Optional[datetime] to datetime | None correctly follows Python 3.10+ union syntax and aligns with the PR objectives.


87-88: Minor: Removal of explicit read mode is fine.

Removing "r" from open() has no functional impact as read mode is the default. This is acceptable.


5-5: No compatibility issue exists. The project requires Python 3.12+ (requires-python = ">=3.12"), which is higher than Python 3.11 when datetime.UTC was introduced. The datetime.UTC import is fully compatible with the project's minimum Python version.

Likely an incorrect or invalid review comment.

openrag/models/openai.py (3)

1-1: LGTM on reduced typing imports.

The import statement correctly retains only Any and Literal from the typing module, as the other types (Optional, List, Dict, Union) are now replaced with Python 3.10+ built-in syntax.


14-30: LGTM on type modernization.

The class fields are correctly updated to Python 3.10+ union syntax. The mutable default dict for metadata is properly handled by Pydantic's Field() which creates a copy for each instance.


33-50: LGTM on type modernization.

The legacy completion request fields are correctly updated. The dict | None for logit_bias is acceptable, though you could optionally add type parameters (dict[str, float] | None) to match the OpenAI API specification more precisely.

.github/workflows/api_tests/mock_vllm.py (6)

23-39: LGTM on embedding models.

The type hints correctly reflect the OpenAI embeddings API patterns: accepting single or batch string inputs, returning float vectors, and wrapping in a list response.


50-79: LGTM on chat completion models.

The modernized type hints accurately reflect the OpenAI chat completions API. The stop: str | list[str] | None correctly allows both single and multiple stop sequences.


85-108: LGTM on text completion models.

Type hints correctly reflect the legacy OpenAI completions API with support for both single and batch prompts.


114-121: LGTM on helper function.

Using MD5 for deterministic fake embedding generation is appropriate for testing purposes. The function correctly produces consistent embeddings based on text hash.


131-156: LGTM on mock response generation.

The function correctly handles both string and vision model (list) content formats, generating contextual mock responses for different query types.


179-247: LGTM on API endpoints.

All endpoints correctly use the updated Pydantic models with modernized type hints. The implementation properly handles both single and batch inputs for embeddings and completions.

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

4-12: Import additions look fine.


105-110: Formatting-only change; no issues spotted.


118-123: Formatting-only change; no issues spotted.


222-230: No issues in dataset load/partition tweak.


256-258: Refactor-only change; looks OK.


322-330: Output formatting tweak looks fine.

tests/api_tests/conftest.py (2)

4-4: Spacing after the module docstring is fine.
Improves readability without changing behavior.


90-92: Assertion formatting looks good.
Multiline message is clearer, no behavior change.

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

25-26: LGTM — default read mode is acceptable here.
No behavior change from explicit "r".

LINTING.md (1)

1-82: Doc structure and commands are clear.
Nice, concise onboarding steps for Ruff.

openrag/routers/openai.py (1)

189-192: Consistent exception string formatting — looks good.
No behavioral change; just standardized formatting.

Also applies to: 240-243, 318-320, 333-335

openrag/routers/actors.py (1)

33-36: Formatting cleanup is fine.
Multiline decorators and {e!s} details are consistent with the rest of the codebase.

Also applies to: 74-77, 125-126, 145-146

openrag/components/indexer/utils/test_text_sanitizer.py (2)

127-127: LGTM - Assertion formatting consolidated.

The multi-line assertions are correctly collapsed to single lines for linting compliance without changing the expected values.

Also applies to: 141-141, 148-148


187-187: LGTM - noqa comment preserves intentional test artifact.

The # noqa: W293 comment appropriately suppresses the trailing whitespace warning since this test string intentionally simulates PDF extraction artifacts with whitespace issues.

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

88-88: LGTM - Bare raise preserved.

The bare raise correctly re-raises the caught exception after logging.

openrag/components/indexer/loaders/pdf_loaders/marker.py (1)

217-226: LGTM - Type hints correctly modernized.

The metadata: dict | None = None pattern with explicit None-handling at lines 225-226 is the correct approach. This is the pattern that should be followed in serializer.py as well.

openrag/routers/users.py (2)

13-14: LGTM - Route decorator formatting improved.

Placing the path argument on a separate line improves readability when combined with multi-line description strings.

Also applies to: 37-38, 60-61, 102-103, 133-134, 169-170


83-89: LGTM - Type hints modernized for Form parameters.

The str | None = Form(None) syntax is correct for optional form fields in FastAPI.

openrag/scripts/migrations/alembic/versions/4add4d260575_initial_migration.py (1)

9-9: LGTM - Import and type hints modernized.

Using collections.abc.Sequence instead of typing.Sequence is the preferred approach in Python 3.9+, and the union syntax is correctly updated for Python 3.10+.

Also applies to: 33-35

openrag/scripts/migrations/alembic/versions/cd642e4502d8_create_users_memberships_tables.py (1)

9-9: LGTM! Type hint modernization looks correct.

The migration from typing.Sequence and typing.Union to collections.abc.Sequence and Python 3.10+ union syntax (str | Sequence[str] | None) is appropriate and aligns with modern Python practices.

Also applies to: 33-35

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

68-71: Type hint modernization looks good.

The parameter type Image.Image | str correctly uses Python 3.10+ union syntax.

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

26-31: LGTM! Type hints correctly modernized and consistent with base class.

Both TextLoader and MarkdownLoader implementations use the updated Python 3.10+ union syntax matching BaseLoader.aload_document.

Also applies to: 65-70

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

2-3: LGTM! Type hint modernization is correct.

Using Callable from collections.abc and Literal from typing is appropriate, and the union syntax for page_number: int | None is valid.

Also applies to: 23-28

openrag/components/indexer/vectordb/utils.py (2)

181-211: LGTM! Type hints modernized consistently.

The type annotation updates to Python 3.10+ union syntax (int | None, dict | None) are correctly applied and align with the corresponding interface in vectordb.py.


334-401: Type hint modernization applied correctly to user-related methods.

The return type annotations (dict | None) and parameter types (str | None) are consistent with the function behavior that returns None when entities are not found.

openrag/components/indexer/loaders/eml_loader.py (4)

30-30: Type annotation corrected.

The signature now correctly uses dict | None = None, fixing the previous inconsistency where the type was dict but the default was None.


193-194: Keyword argument corrected.

The call now correctly uses image_data=image matching the base class get_image_description parameter name.


204-206: Short-circuit evaluation makes this safe.

The condition file_ext == ".pdf" and not fallback_success is safe because Python's short-circuit evaluation ensures fallback_success is only evaluated when file_ext == ".pdf", which is precisely when fallback_success is defined (line 153).


237-238: Keyword argument corrected.

The call now correctly uses image_data=image matching the base class method signature.

openrag/api.py (1)

76-77: Type annotations corrected.

The type hints are now correctly bool instead of bool | None, matching the actual return type of the comparison expression.

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

215-215: Type hint modernization applied correctly.


288-288: Type hint modernization applied correctly.


345-348: Type hint modernization applied correctly.

The embedder: BaseEmbedding | None = None parameter correctly indicates it's optional.

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

56-97: Abstract method signatures modernized consistently.

The type hint updates (int | None, dict | None, list[Document]) in BaseVectorDB align correctly with the concrete implementations in MilvusDB.


332-333: Return type annotation modernized.

list[str] correctly uses Python 3.10+ built-in generic syntax.


434-441: Type hints modernized in async_search.

The filter: dict | None = None parameter type is consistent with the abstract method signature.


718-721: Implementation aligns with abstract method signature.

The limit: int | None = None parameter correctly matches the updated BaseVectorDB.list_partition_files signature.

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

Comment thread automatic-evaluation-pipeline/benchmark.py
Comment thread LINTING.md
Comment thread openrag/components/indexer/indexer.py
Comment on lines +329 to 331
async def get_all_states(self) -> dict[str, str]:
async with self.lock:
return {tid: info.state for tid, info in self.tasks.items()}

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

🧩 Analysis chain

🏁 Script executed:

# First, find the TaskInfo class definition and examine its state attribute
rg -n "class TaskInfo" openrag/components/indexer/

Repository: linagora/openrag

Length of output: 117


🏁 Script executed:

# Also search for any type hints related to TaskInfo.state
rg -n "state.*:" openrag/components/indexer/indexer.py | head -20

Repository: linagora/openrag

Length of output: 375


🏁 Script executed:

# Get a broader view of the indexer.py file structure
wc -l openrag/components/indexer/indexer.py

Repository: linagora/openrag

Length of output: 101


🏁 Script executed:

# View the TaskInfo class definition to confirm state type annotation
sed -n '246,260p' openrag/components/indexer/indexer.py

Repository: linagora/openrag

Length of output: 555


Return type should allow None states.

TaskInfo.state is typed as str | None, so get_all_states can return None values in the dictionary. The return type must be dict[str, str | None].

♻️ Proposed change (type fix)
-    async def get_all_states(self) -> dict[str, str]:
+    async def get_all_states(self) -> dict[str, str | None]:
🤖 Prompt for AI Agents
In `@openrag/components/indexer/indexer.py` around lines 329 - 331, get_all_states
currently declares a return type of dict[str, str] but iterates TaskInfo.state
which is typed str | None, so change the return annotation to dict[str, str |
None] and keep the implementation the same; update the get_all_states signature
to reflect dict[str, str | None] and ensure callers expect optional values from
TaskInfo.state (refer to get_all_states, TaskInfo.state, self.tasks and
self.lock to locate the change).

Comment thread openrag/components/indexer/loaders/base.py Outdated
Comment thread openrag/components/indexer/loaders/serializer.py Outdated
Comment thread openrag/scripts/filter-logs.py
- 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.
@Ahmath-Gadji
Ahmath-Gadji force-pushed the feat/add-ruff-linting branch from a0838a2 to 04beee4 Compare January 16, 2026 16:19
@coderabbitai coderabbitai Bot added the feat Add a new feature label Jan 16, 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: 4

Caution

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

⚠️ Outside diff range comments (3)
automatic-evaluation-pipeline/generate_questions.py (1)

57-65: Avoid creating asyncio.Semaphore in default args (event-loop binding risk).

Creating the semaphore at import time can bind it to the wrong event loop and trigger RuntimeError: <Semaphore> is bound to a different event loop under asyncio.run. Prefer constructing it inside a running loop (and ideally pass it in).

✅ Safer pattern (create in running loop)
-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:
         message = [
             {
                 "role": "user",
                 "content": f"Voici le document:\n{chunk}. Donnez-moi un résumé qui précise quel type d'informations et de contenu contient le passage, mais sans entrer dans des détails trop précis.",
             }
         ]

Please mirror the same change in question_answer to avoid the same pitfall there.

openrag/api.py (1)

217-226: Avoid double-registering the OpenAI router when Chainlit is enabled.

When both WITH_OPENAI_API and WITH_CHAINLIT_UI flags are true, the router is registered twice with identical prefix and tags, creating duplicate routes in the FastAPI application and OpenAPI schema. Combine the conditions to ensure registration happens only once.

Proposed fix
-if WITH_OPENAI_API:
-    # Mount the openai router
-    app.include_router(openai_router, prefix="/v1", tags=[Tags.OPENAI])
+if WITH_OPENAI_API or WITH_CHAINLIT_UI:
+    # Mount the openai router (needed for OpenAI API and Chainlit)
+    app.include_router(openai_router, prefix="/v1", tags=[Tags.OPENAI])
@@
 if WITH_CHAINLIT_UI:
     # Mount the default front
     from chainlit.utils import mount_chainlit
 
     mount_chainlit(app, "./app_front.py", path="/chainlit")
-    app.include_router(openai_router, prefix="/v1", tags=[Tags.OPENAI])  # cause chainlit uses openai api endpoints
+    # openai_router already included above for Chainlit use
openrag/components/indexer/indexer.py (1)

58-66: Fix mutable default for metadata in serialize_file.

The method uses a shared {} default which violates Python best practices and is inconsistent with the pattern already used in add_file (line 71) and the imported serialize_file utility function. Change to metadata: dict | None = None with metadata = metadata or {} inside the method.

Suggested fix
     async def serialize_file(
         self,
         path: str,
-        metadata: dict = {},
+        metadata: dict | None = None,
         task_id: str = None,
     ):
+        metadata = metadata or {}
         # Serialize
         doc = await serialize_file(task_id, path, metadata=metadata)
🤖 Fix all issues with AI agents
In `@automatic-evaluation-pipeline/benchmark.py`:
- Around line 256-263: Guard against empty input_reference["chunks"] before
indexing chunk_id_reference[0]: if chunk_id_reference is empty, append fallback
values (e.g., 0) to hit_rates and MRRs instead of calling
compute_hits/compute_inverted_ranks; otherwise call
compute_hits(chunk_id_reference[0], openrag_chunk_ids) and
compute_inverted_ranks(chunk_id_reference[0], openrag_chunk_ids) as before.
Ensure this check updates the same lists hit_rates and MRRs and uses the
variable names chunk_id_reference and openrag_chunk_ids so the logic is
localized to this block.

In `@automatic-evaluation-pipeline/generate_questions.py`:
- Around line 1-15: The project is missing runtime dependencies for the imports
"numpy" and "tqdm" used in generate_questions.py; open the project's
pyproject.toml and add entries for numpy (e.g., numpy>=1.24.0) and tqdm (e.g.,
tqdm>=4.66.0) to the top-level [project.dependencies] (or equivalent
dependencies list) alongside the existing hdbscan and httpx entries so the
imports in generate_questions.py (numpy, tqdm.asyncio.tqdm) resolve at runtime.

In `@openrag/components/indexer/utils/files.py`:
- Around line 73-77: The serialize_file function in indexer.py currently uses a
mutable default (metadata: dict = {}); change the parameter to accept None
(e.g., metadata: dict | None = None), then inside serialize_file normalize with
metadata = metadata or {} (or explicit if metadata is None: metadata = {});
update any type hints/usages accordingly to avoid shared mutable state and
mirror the fix applied in files.py.

In `@openrag/scripts/backup.py`:
- Around line 51-69: PartitionFileManager.list_partition_files() may return an
empty dict causing files["files"] KeyError; change the handling after calling
pfm.list_partition_files(part_name) to default files_list = files.get("files",
[]) (or similar) and use files_list for sorting, popping "partition", writing
JSON lines to out_fh, writing the separator, and logging the count (use
len(files_list)); keep the rest of the try/except and variable names
(pfm.list_partition_files, files, out_fh, verbose, logger) intact.
♻️ Duplicate comments (4)
LINTING.md (2)

140-140: Fix typo: "unecessary" → "unnecessary".

This typo was flagged in a previous review but hasn't been corrected yet.


143-143: Fix grammar: "suggest" → "suggests".

This grammar issue was flagged in a previous review but hasn't been corrected yet.

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

265-267: Guard against empty reference list to avoid ZeroDivisionError.

This is the same issue previously flagged: len(chunk_id_reference) can be zero. Please add a defensive guard here.

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

346-348: Return type should allow None states.
TaskInfo.state is str | None, so get_all_states should reflect that.

♻️ Suggested fix
-    async def get_all_states(self) -> dict[str, str]:
+    async def get_all_states(self) -> dict[str, str | None]:
🧹 Nitpick comments (6)
openrag/components/prompts/prompts.py (1)

25-27: LGTM — Consider adding explicit encoding for cross-platform consistency.

The removal of the explicit "r" mode is a valid stylistic simplification since open() defaults to read mode. However, consider specifying encoding="utf-8" to ensure consistent behavior across platforms, as prompt files may contain non-ASCII characters.

♻️ Optional improvement
-    with open(file_path) as f:
+    with open(file_path, encoding="utf-8") as f:
LINTING.md (1)

57-57: Clarify or remove the RUF rules entry.

The documentation lists "RUF: Ruff-specific rules" under "Key Rules Enabled" but notes they are "not applied currently". This is contradictory and may confuse developers. Either remove RUF from the list if it's not enabled, or explain why it's documented but not applied.

openrag/components/reranker.py (1)

11-17: Make the concurrency limit configurable (avoid magic number).

Different deployments will want different parallelism. Consider reading a max_concurrency from config with a sane default and guard against non-positive values.

♻️ Proposed refactor
-        self.semaphore = asyncio.Semaphore(
-            5
-        )  # Only allow 5 reranking operation at a time
+        max_concurrency = max(1, int(config.reranker.get("max_concurrency", 5)))
+        self.semaphore = asyncio.Semaphore(
+            max_concurrency
+        )  # Only allow N reranking operations at a time
openrag/components/map_reduce.py (1)

52-52: Consider updating the type annotation.

The type hint ChatOpenAI is technically inaccurate since .with_structured_output() returns a Runnable (specifically a RunnableSerializable that outputs SummarizedChunk), not a ChatOpenAI instance. This could be misleading for IDE type checking and future maintainers.

🔧 Suggested fix
-        self.slm: ChatOpenAI = ChatOpenAI(**config.llm).with_structured_output(SummarizedChunk)
+        self.slm = ChatOpenAI(**config.llm).with_structured_output(SummarizedChunk)

Or use a more accurate type if needed:

from langchain_core.runnables import Runnable
self.slm: Runnable[list, SummarizedChunk] = ChatOpenAI(**config.llm).with_structured_output(SummarizedChunk)
openrag/scripts/restore.py (1)

193-206: Consider adding explicit return 0 for clarity.

The docstring states the function returns int, but success paths return None implicitly. While sys.exit(None) works correctly (exits with code 0), adding an explicit return 0 at the end of main() would align with the documented contract.

Proposed addition at end of main()
    finally:
        client.close()
    
    return 0
automatic-evaluation-pipeline/generate_questions.py (1)

146-151: Consider making the partition configurable instead of hard-coded.

This avoids editing code for different runs and makes CI/local usage more flexible.

♻️ Example using an env var with fallback
-    partition = "pdftest"  # To replace with your wanted partition's name
+    partition = os.environ.get("PARTITION_NAME", "pdftest")  # Override via env
📜 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 a0838a2 and 04beee4.

📒 Files selected for processing (67)
  • .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/utils.py
  • openrag/components/indexer/embeddings/__init__.py
  • openrag/components/indexer/embeddings/openai.py
  • openrag/components/indexer/indexer.py
  • openrag/components/indexer/loaders/__init__.py
  • openrag/components/indexer/loaders/base.py
  • openrag/components/indexer/loaders/eml_loader.py
  • openrag/components/indexer/loaders/image.py
  • openrag/components/indexer/loaders/pdf_loaders/docling2.py
  • openrag/components/indexer/loaders/pdf_loaders/marker.py
  • openrag/components/indexer/loaders/pdf_loaders/openai.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/reranker.py
  • openrag/components/retriever.py
  • openrag/components/utils.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/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/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
💤 Files with no reviewable changes (8)
  • tests/api_tests/test_users.py
  • openrag/utils/test_external_resource_errors.py
  • openrag/utils/test_logger.py
  • tests/api_tests/test_actors.py
  • tests/api_tests/test_extract.py
  • tests/api_tests/test_queue.py
  • tests/api_tests/test_partition.py
  • openrag/utils/logger.py
✅ Files skipped from review due to trivial changes (1)
  • openrag/components/indexer/loaders/test_media_loader.py
🚧 Files skipped from review as they are similar to previous changes (15)
  • openrag/app_front.py
  • openrag/routers/tools.py
  • openrag/scripts/filter-logs.py
  • openrag/routers/utils.py
  • .github/workflows/lint.yml
  • openrag/utils/dependencies.py
  • openrag/routers/queue.py
  • openrag/components/pipeline.py
  • openrag/components/utils.py
  • openrag/scripts/embed.py
  • pyproject.toml
  • openrag/components/indexer/loaders/serializer.py
  • tests/api_tests/test_openai_compat.py
  • openrag/routers/extract.py
  • openrag/components/indexer/chunker/utils.py
🧰 Additional context used
🧬 Code graph analysis (19)
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-57)
openrag/components/indexer/utils/test_text_sanitizer.py (2)
openrag/components/indexer/chunker/test_chunking.py (1)
  • test_multiline_spacing (184-191)
openrag/components/indexer/utils/text_sanitizer.py (1)
  • clean_markdown_table_spacing (98-127)
openrag/components/indexer/loaders/eml_loader.py (3)
openrag/components/indexer/loaders/__init__.py (1)
  • get_loader_classes (18-51)
openrag/components/indexer/loaders/base.py (3)
  • BaseLoader (19-151)
  • aload_document (37-43)
  • get_image_description (66-151)
openrag/components/indexer/loaders/image.py (1)
  • aload_document (13-20)
tests/api_tests/test_indexer.py (2)
tests/api_tests/conftest.py (3)
  • api_client (16-19)
  • created_partition (87-98)
  • sample_text_file (39-53)
tests/api_tests/test_tools.py (1)
  • pdf_file_path (13-17)
openrag/api.py (4)
openrag/config/config.py (1)
  • load_config (12-29)
openrag/utils/dependencies.py (1)
  • get_vectordb (47-49)
openrag/utils/exceptions/base.py (1)
  • OpenRAGError (4-24)
openrag/utils/logger.py (1)
  • get_logger (13-57)
openrag/components/indexer/loaders/image.py (1)
openrag/components/indexer/loaders/base.py (1)
  • BaseLoader (19-151)
openrag/components/indexer/loaders/__init__.py (1)
openrag/components/indexer/loaders/base.py (1)
  • BaseLoader (19-151)
openrag/routers/users.py (2)
openrag/components/indexer/vectordb/utils.py (1)
  • delete_user (424-431)
openrag/components/indexer/vectordb/vectordb.py (1)
  • delete_user (875-882)
openrag/components/indexer/loaders/txt_loader.py (1)
openrag/components/indexer/loaders/base.py (1)
  • BaseLoader (19-151)
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/scripts/backup.py (3)
openrag/components/indexer/vectordb/utils.py (3)
  • PartitionFileManager (140-566)
  • list_partition_files (181-204)
  • list_partitions (295-299)
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/vectordb/utils.py (1)
openrag/components/indexer/vectordb/vectordb.py (3)
  • list_partition_files (57-58)
  • list_partition_files (718-735)
  • get_user_by_token (887-888)
openrag/components/indexer/loaders/base.py (1)
openrag/utils/logger.py (1)
  • get_logger (13-57)
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 (200-208)
  • require_task_owner (156-170)
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/components/indexer/loaders/pdf_loaders/openai.py (2)
openrag/components/indexer/loaders/base.py (1)
  • aload_document (37-43)
openrag/components/indexer/loaders/image.py (1)
  • aload_document (13-20)
openrag/components/indexer/vectordb/vectordb.py (4)
openrag/components/indexer/vectordb/utils.py (3)
  • list_partition_files (181-204)
  • file_exists_in_partition (322-330)
  • create_user (334-361)
openrag/utils/exceptions/base.py (2)
  • VDBError (37-43)
  • EmbeddingError (28-34)
openrag/utils/exceptions/vectordb.py (4)
  • VDBConnectionError (4-13)
  • UnexpectedVDBError (117-126)
  • VDBSearchError (57-66)
  • VDBDeleteError (43-54)
openrag/routers/users.py (1)
  • create_user (83-99)
openrag/components/indexer/loaders/pdf_loaders/docling2.py (1)
openrag/config/config.py (1)
  • load_config (12-29)
openrag/scripts/restore.py (3)
openrag/components/indexer/indexer.py (1)
  • chunk (53-55)
openrag/scripts/backup.py (1)
  • load_openrag_config (187-207)
openrag/components/indexer/vectordb/vectordb.py (2)
  • list_partitions (45-46)
  • list_partitions (737-742)
🪛 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). (2)
  • GitHub Check: api-tests
  • GitHub Check: tests (3.12)
🔇 Additional comments (111)
openrag/components/indexer/utils/test_text_sanitizer.py (1)

122-128: Formatting-only test tidy-up looks good.

These are readability/linting adjustments without behavior changes.

Also applies to: 136-142, 143-149, 171-188

tests/api_tests/conftest.py (2)

1-5: LGTM!

Adding a blank line after the module docstring aligns with standard Python formatting conventions.


90-92: LGTM!

The multi-line assertion using parentheses for implicit line continuation is a clean way to comply with line length limits while maintaining readability.

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

21-21: LGTM!

The __all__ reordering to alphabetical order is a reasonable style improvement with no functional impact.

openrag/components/indexer/embeddings/openai.py (2)

44-51: LGTM!

Using {e!s} instead of {str(e)} is the more idiomatic f-string syntax and produces identical output. Good consistency improvement.


62-69: LGTM!

Consistent use of {e!s} conversion specifier for exception formatting.

automatic-evaluation-pipeline/upload_files.py (2)

1-6: LGTM! Import reorganization follows PEP 8 conventions.

The imports are now correctly grouped: standard library (os, pathlib) separated from third-party packages (httpx, dotenv, loguru) with a blank line, which aligns with Ruff/isort formatting rules.


17-17: Minor formatting change approved.

Extra blank line is acceptable for readability and likely auto-formatted by Ruff.

openrag/components/map_reduce.py (4)

32-37: LGTM!

Formatting change to consolidate the relevancy field declaration onto a single line improves readability while maintaining the same behavior.


64-84: LGTM!

Formatting refinement to the content field construction. The error handling and fallback return are appropriate.


86-114: LGTM!

Formatting improvements throughout the method. The termination condition logic (Line 101) correctly determines early exit when the last expansion_batch_size chunks are all irrelevant. Debug logging behavior is preserved.


116-157: LGTM!

Formatting refinements throughout the map method. The batch processing logic, termination conditions, and control flow remain functionally equivalent. The modernized list[Document] type hints (Line 127) align with the PR's Python 3.10+ migration objectives.

openrag/scripts/restore.py (7)

15-25: LGTM! Type annotations modernized correctly.

The function signature now uses Python 3.10+ built-in generics (list[str] | None, dict[str, set[str]]) which aligns with the PR's type hint modernization objective.


59-75: LGTM! Formatting improvements.

The single-line conditionals and condensed logging statements improve readability while preserving the original logic.


122-167: LGTM! Type annotation and formatting updates.

The added_documents type annotation is consistent with read_rdb_section, and the condensed skip-logic preserves correct filtering behavior.


186-188: LGTM! Simplified file open call.

Removing the explicit "rt" mode is correct—open() defaults to read mode, and specifying encoding implies text mode.


208-228: LGTM! Return type annotation added.

The tuple[dict[str, Any], dict[str, Any]] return type accurately reflects the function's behavior and is consistent with the similar function in backup.py.


250-251: Good fix! type=int added for --user-id.

This correctly ensures args.user_id is an integer when passed to add_file_to_partition, addressing the previously identified issue.


259-286: LGTM! Formatting improvements in main().

The condensed dict comprehension (line 283) and single-line formatting for MilvusDB.options and error messages improve readability without changing functionality.

.github/workflows/api_tests/mock_vllm.py (6)

9-9: LGTM!

Clean import trimming—only Any is retained since it's still needed for ChatMessage.content. The Union, List, and Optional imports are no longer required with Python 3.10+ built-in generics and union syntax.


50-79: LGTM!

The ChatCompletionRequest and ChatCompletionResponse models are correctly updated with Python 3.10+ type syntax. The optional field pattern type | None = default is appropriate for fields that have defaults but can also explicitly accept None.


85-108: LGTM!

Consistent modernization of TextCompletionRequest and TextCompletionResponse models, mirroring the pattern used in the chat completion models.


114-131: LGTM!

Function signatures correctly updated to use Python 3.10+ type syntax. The implementations remain compatible with these type annotations.


183-196: LGTM!

The formatting changes inline the list comprehension and generator expression, improving readability while maintaining identical functionality. These align with Ruff's formatting conventions introduced in this PR.


23-39: Type hints are correct for Python 3.12+.

The updates to EmbeddingRequest, EmbeddingData, and EmbeddingResponse correctly use Python 3.10+ union and generic syntax. Since the project targets Python 3.12+ (requires-python = ">=3.12" in pyproject.toml), these type hints are natively supported at runtime without needing from __future__ import annotations. Pydantic is available as a transitive dependency and supports this syntax.

openrag/chainlit_api.py (1)

1-6: Formatting-only import/order change looks good.

No functional impact detected.

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

1-6: Import reordering is fine.

No behavior changes introduced.

openrag/routers/partition.py (1)

26-27: Decorator formatting-only changes look good.

No functional impact observed.

Also applies to: 53-54, 80-81, 131-132, 179-180, 223-224, 255-256, 293-294, 334-335, 371-372

tests/api_tests/test_search.py (1)

2-2: Formatting tweaks look good.

No behavioral change to tests.

Also applies to: 20-21, 51-53, 60-62, 70-72, 80-82, 89-91

openrag/components/indexer/loaders/pdf_loaders/docling2.py (1)

1-5: Import reordering only—looks good.

No functional changes spotted in this hunk.

openrag/components/llm.py (3)

39-43: LGTM!

The error message formatting consolidation is clean and consistent. The single-line f-string approach improves readability while preserving the same error information (status code and detail).


61-64: LGTM!

Consistent formatting with the other error handling paths in this file.


83-85: LGTM!

Error formatting is now consistent across all three API error paths in this class.

openrag/routers/users.py (2)

13-14: LGTM!

The multi-line decorator formatting with the path on a separate line improves readability and is consistent with Ruff's formatting preferences.

Also applies to: 37-38, 60-61, 102-103, 133-134, 169-170


83-89: LGTM!

The type hints are correctly modernized to Python 3.10+ union syntax (str | None). The Form(None) default values work correctly with these types.

openrag/components/retriever.py (3)

3-3: LGTM!

Good use of ClassVar[dict] annotation for the class-level RETRIEVERS mapping. This explicitly documents the intent that this is a class variable, not an instance variable.

Also applies to: 141-145


36-40: LGTM!

The with_surrounding_chunks parameter is now explicitly declared in the signature rather than being passed through **kwargs. This improves API clarity and IDE support.


78-79: LGTM!

The prompt construction is functionally equivalent and the single-line formatting is cleaner.

openrag/scripts/migrations/alembic/versions/4add4d260575_initial_migration.py (1)

9-9: LGTM!

Good modernization: using collections.abc.Sequence instead of the deprecated typing.Sequence, and adopting Python 3.10+ union syntax for the Alembic revision identifiers. The migration logic remains unchanged.

Also applies to: 33-35

openrag/routers/search.py (1)

18-19: LGTM! Type annotations and decorator formatting modernized.

The changes correctly update:

  • Type hint from Optional[List[str]] to list[str] | None (Python 3.10+ syntax)
  • Decorator formatting to multiline for improved readability

These are consistent with the PR's linting and modernization objectives.

Also applies to: 49-51, 86-87, 137-138

openrag/scripts/migrations/alembic/versions/cd642e4502d8_create_users_memberships_tables.py (1)

9-9: LGTM! Migration metadata type annotations modernized.

The changes correctly:

  • Import Sequence from collections.abc instead of typing (preferred since Python 3.9+)
  • Update union types to Python 3.10+ syntax (str | Sequence[str] | None)

No functional impact on migration behavior.

Also applies to: 33-35

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

145-145: LGTM! Type annotations modernized throughout the chunker module.

Type hints properly updated to Python 3.10+ syntax:

  • dict | None for optional dictionary parameters
  • str | None for optional string parameters
  • BaseEmbedding | None for optional embedder

These changes align with the broader PR objective of type modernization.

Also applies to: 161-161, 215-215, 288-288, 347-347


25-28: LGTM! Constants and formatting consolidated.

The single-line formatting for MAX_CONCURRENT_CONTEXTUALIZATION and BASE_CHUNK_FORMAT improves readability and consistency with the linting objectives.

tests/api_tests/test_indexer.py (1)

106-106: LGTM! Test method signatures reformatted.

The single-line formatting for test method signatures is consistent with the PR's linting and formatting objectives. No functional changes to test behavior.

Also applies to: 136-136

openrag/routers/actors.py (2)

33-35: LGTM! Decorator formatting improved.

The multiline decorator formatting with the path on a separate line improves readability, especially for decorators with long descriptions.

Also applies to: 74-76


125-125: LGTM! Error message formatting modernized.

Using {e!s} instead of {str(e)} is more concise and idiomatic Python. The !s format specifier calls str() on the value, producing equivalent output with cleaner syntax.

Also applies to: 145-145

openrag/routers/openai.py (1)

194-198: LGTM — consistent exception string formatting.

Also applies to: 244-249, 323-326, 338-340

openrag/components/indexer/loaders/pdf_loaders/marker.py (1)

217-221: LGTM — signature aligns with BaseLoader’s 3.10+ typing.

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

18-25: LGTM — typing modernization only.

Also applies to: 32-40, 54-54

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

5-13: LGTM — typing updates are consistent with the loader base API.

Also applies to: 26-30, 65-69

openrag/models/indexer.py (1)

5-7: Code syntax is compatible. The project explicitly enforces requires-python = ">=3.12" in pyproject.toml and uv.lock, which exceeds the Python 3.10+ requirement for int | None union syntax. No action needed.

openrag/components/indexer/loaders/base.py (5)

13-13: Logger import update looks fine.


36-41: Abstract loader signature is consistent.


66-69: Image description input handling looks good.


103-113: Invalid image data returns a clear sentinel.


133-149: Error logging path is clear.

openrag/routers/indexer.py (6)

4-4: No concerns in these small declaration tweaks.

Also applies to: 313-313, 358-358


107-107: Docstring tweak noted.


166-172: User context is now propagated to indexing tasks.


282-289: Replacement uploads now preserve user context.


375-375: Copy task now carries the user context.


477-503: Task log retrieval cleanup looks good.

openrag/components/indexer/vectordb/utils.py (2)

153-158: Connection error reporting looks good.


181-181: Public method signatures remain consistent.

Also applies to: 206-212, 334-339, 377-377, 401-401

openrag/components/indexer/loaders/eml_loader.py (5)

1-19: Imports and helper setup look good.


30-56: Email metadata extraction path is clear.


61-103: Body and attachment parsing flow looks solid.


107-260: Attachment processing and fallbacks look robust.


266-301: Metadata enrichment and markdown output look good.

openrag/api.py (7)

4-4: No concerns in these import adjustments.

Also applies to: 7-7, 10-10, 31-31


17-22: Verify module-level initialization is safe.

This runs on import; please confirm it won’t reinitialize in reloads/tests. If needed, guard initialization.

🛠️ Possible guard
-ray.init(dashboard_host="0.0.0.0")
+if not ray.is_initialized():
+    ray.init(dashboard_host="0.0.0.0")

37-46: No issues noted here.


73-77: No concerns with these flag declarations.


97-97: No issues noted here.


123-124: No issues noted in these exposure/mounting tweaks.

Also applies to: 186-197


237-243: No issues noted in runtime start wiring.

Also applies to: 246-246

openrag/components/indexer/loaders/pdf_loaders/openai.py (5)

18-20: LGTM: module-level helper signature is clean.


31-40: Semaphore initialization looks fine.


42-47: Type-hint modernization is consistent.


71-75: Helper signatures updated consistently.


100-107: Verify image_url payload shape against the target OpenAI‑compatible endpoint.
Some providers accept a string URL, others require an object with url (and optionally detail). Please confirm the expected schema for your OCR model before release.

openrag/models/openai.py (3)

1-1: No review comment for this change.


36-50: LGTM: completion request fields look consistent.


17-30: No action required; Pydantic v2 handles mutable defaults safely.

In Pydantic v2 (currently v2.11.7), Field({...}) with dict literals automatically deep-copies the default for each instance. This prevents shared mutable state across requests, so the current code is safe from cross-request leakage.

Using default_factory remains a best practice for clarity and explicit intent, but is not necessary for correctness in this case.

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

56-98: LGTM: abstract method signature updates are consistent.


149-183: Initializer updates look good.


185-243: Collection load/logging/error formatting looks fine.


245-309: Schema/index formatting changes are OK.


335-402: Add-documents flow remains intact.


434-527: Search path updates look consistent.


528-571: Surrounding-chunks dedup logic looks unchanged.


573-602: Delete-file path updates are OK.


603-660: File-chunk retrieval changes look good.


661-702: Chunk-by-id metadata filtering looks fine.


703-736: File existence and partition list wrappers are OK.


750-780: Partition deletion error formatting is consistent.


842-862: List-all-chunk error handling looks consistent.


863-906: User/partition management updates look good.


952-959: File existence check is unchanged in behavior.

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

7-7: No review comment for this change.


52-55: LGTM: chunk return typing is consistent.


68-118: Add-file flow updates look good.


149-159: Delete-file logging update looks fine.


164-189: Metadata update path is consistent.


192-221: Copy-file changes look good.


223-241: Search signature/default handling looks good.


243-259: Partition validation helpers are OK.


262-268: TaskInfo typing updates look good.


270-275: TaskStateManager state containers are OK.


321-344: Getter return types look consistent.


351-382: Queue info helpers look good.

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

Comment thread automatic-evaluation-pipeline/benchmark.py
Comment on lines +1 to +15
import ast
import asyncio
import os
import httpx
import json
import os
import random
import time
import numpy as np
import ast

import hdbscan
import httpx
import numpy as np
from dotenv import load_dotenv
from loguru import logger
from langchain_openai import ChatOpenAI
from tqdm.asyncio import tqdm
from loguru import logger
from sklearn.cluster import DBSCAN
import umap.umap_ as umap
import hdbscan
import pickle
from tqdm.asyncio import tqdm

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:

#!/bin/bash
set -euo pipefail

# Find dependency manifest files
echo "=== Locating dependency manifests ==="
manifest_files=$(fd -a -t f \( -name 'pyproject.toml' -o -name 'requirements*.txt' -o -name 'setup.cfg' -o -name 'setup.py' -o -name 'Pipfile' -o -name 'poetry.lock' \) 2>/dev/null || echo "")

if [ -z "$manifest_files" ]; then
  echo "No dependency manifest files found."
  exit 0
fi

echo "$manifest_files"
echo

# Check each manifest for the new dependencies
echo "=== Checking for new dependencies: hdbscan, httpx, numpy, tqdm ==="
echo

for file in $manifest_files; do
  echo "--- File: $file ---"
  if grep -E '(hdbscan|httpx|numpy|tqdm)' "$file" 2>/dev/null || echo "Not found in this file"; then
    echo
  fi
done

# Also check if there's an automatic-evaluation-pipeline directory structure
echo "=== Directory structure check ==="
if [ -d "automatic-evaluation-pipeline" ]; then
  echo "Found automatic-evaluation-pipeline directory"
  ls -la automatic-evaluation-pipeline/ | head -20
fi

Repository: linagora/openrag

Length of output: 133


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Repository root structure ==="
ls -la | head -30

echo
echo "=== All Python dependency-related files in the entire repo ==="
find . -type f \( -name '*requirements*' -o -name 'pyproject.toml' -o -name 'setup.py' -o -name 'setup.cfg' -o -name 'Pipfile' -o -name 'poetry.lock' -o -name 'Pipfile.lock' \) 2>/dev/null | head -20

echo
echo "=== Locate automatic-evaluation-pipeline directory ==="
find . -type d -name 'automatic-evaluation-pipeline' 2>/dev/null

echo
echo "=== Check for generate_questions.py file ==="
find . -name 'generate_questions.py' 2>/dev/null

echo
echo "=== List Python files in automatic-evaluation-pipeline (if exists) ==="
if [ -d "automatic-evaluation-pipeline" ]; then
  ls -la automatic-evaluation-pipeline/
fi

Repository: linagora/openrag

Length of output: 2786


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Content of pyproject.toml ==="
cat ./pyproject.toml

echo
echo "=== Content of utility/requirements.txt ==="
cat ./utility/requirements.txt

echo
echo "=== Searching for new dependencies in all files ==="
echo "Looking for: hdbscan, httpx, numpy, tqdm"
rg -i "(hdbscan|httpx|numpy|tqdm)" ./pyproject.toml ./utility/requirements.txt || echo "Pattern not found"

Repository: linagora/openrag

Length of output: 2675


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== Checking imports in generate_questions.py ==="
head -20 ./automatic-evaluation-pipeline/generate_questions.py

echo
echo "=== Searching for numpy and tqdm usage in the file ==="
rg -i "numpy|tqdm" ./automatic-evaluation-pipeline/generate_questions.py | head -20

Repository: linagora/openrag

Length of output: 659


Add missing runtime dependencies numpy and tqdm to pyproject.toml.

The imports numpy and tqdm are not declared in the project's dependency manifest. Code will fail at runtime with ModuleNotFoundError. Add them to the dependencies list in pyproject.toml with version constraints (e.g., numpy>=1.24.0, tqdm>=4.66.0). Note: hdbscan>=0.8.40 and httpx are already declared.

🤖 Prompt for AI Agents
In `@automatic-evaluation-pipeline/generate_questions.py` around lines 1 - 15, The
project is missing runtime dependencies for the imports "numpy" and "tqdm" used
in generate_questions.py; open the project's pyproject.toml and add entries for
numpy (e.g., numpy>=1.24.0) and tqdm (e.g., tqdm>=4.66.0) to the top-level
[project.dependencies] (or equivalent dependencies list) alongside the existing
hdbscan and httpx entries so the imports in generate_questions.py (numpy,
tqdm.asyncio.tqdm) resolve at runtime.

Comment on lines +73 to +77
async def serialize_file(task_id: str, path: str, metadata: dict | None = None):
import ray

from components.ray_utils import call_ray_actor_with_timeout

metadata = metadata or {}

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:

# Check the specific file and lines mentioned
cat -n openrag/components/indexer/utils/files.py | sed -n '73,80p'

Repository: linagora/openrag

Length of output: 461


🏁 Script executed:

# Check if indexer.py exists and has the mentioned serialize_file method
if [ -f openrag/components/indexer/indexer.py ]; then
  cat -n openrag/components/indexer/indexer.py | sed -n '57,70p'
else
  echo "File not found"
fi

Repository: linagora/openrag

Length of output: 498


🏁 Script executed:

# Search for mutable default arguments pattern with metadata
rg -n --type=py 'def.*metadata.*=\s*\{\}' openrag/

Repository: linagora/openrag

Length of output: 42


🏁 Script executed:

# More general search for all mutable default arguments in python files
rg -n --type=py 'def [^(]*\([^)]*=\s*\{\}' openrag/ | head -20

Repository: linagora/openrag

Length of output: 42


Apply the same mutable default argument fix to indexer.py.

The fix in files.py is correct: changing metadata: dict | None = {} to metadata: dict | None = None with normalization metadata = metadata or {} properly addresses the mutable default argument anti-pattern.

However, the serialize_file method in openrag/components/indexer/indexer.py at line 61 still has metadata: dict = {}, which exhibits the same mutable default issue. Apply the same fix pattern there for consistency and to prevent shared state bugs.

🤖 Prompt for AI Agents
In `@openrag/components/indexer/utils/files.py` around lines 73 - 77, The
serialize_file function in indexer.py currently uses a mutable default
(metadata: dict = {}); change the parameter to accept None (e.g., metadata: dict
| None = None), then inside serialize_file normalize with metadata = metadata or
{} (or explicit if metadata is None: metadata = {}); update any type
hints/usages accordingly to avoid shared mutable state and mirror the fix
applied in files.py.

Comment thread openrag/scripts/backup.py
Comment on lines 51 to +69
try:
files = pfm.list_partition_files(part_name)
except Exception as e:
logger.error(f'Failed while requesting the list of files in partition \'{part_name}\'\n{e}')
logger.error(
f"Failed while requesting the list of files in partition '{part_name}'\n{e}"
)
raise

files['files'].sort(key=lambda v: v['file_id'])
files["files"].sort(key=lambda v: v["file_id"])

for f in files['files']:
f.pop('partition', None)
out_fh.write(json.dumps(f, ensure_ascii=False, sort_keys=True) + '\n')
for f in files["files"]:
f.pop("partition", None)
out_fh.write(json.dumps(f, ensure_ascii=False, sort_keys=True) + "\n")

# Separator
out_fh.write('\n')
out_fh.write("\n")

if verbose:
logger.info(f'Partition \'{part_name}\' - {len(files["files"])} files')
logger.info(f"Partition '{part_name}' - {len(files['files'])} 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 | 🟠 Major

Handle empty partition file lists to avoid KeyError.

PartitionFileManager.list_partition_files() returns {} when a partition has no files; files["files"] will crash the backup. Default to an empty list and use that consistently.

🐛 Proposed fix
-        files["files"].sort(key=lambda v: v["file_id"])
-
-        for f in files["files"]:
+        file_list = files.get("files", [])
+        file_list.sort(key=lambda v: v["file_id"])
+
+        for f in file_list:
             f.pop("partition", None)
             out_fh.write(json.dumps(f, ensure_ascii=False, sort_keys=True) + "\n")
@@
-            logger.info(f"Partition '{part_name}' - {len(files['files'])} files")
+            logger.info(f"Partition '{part_name}' - {len(file_list)} files")
🤖 Prompt for AI Agents
In `@openrag/scripts/backup.py` around lines 51 - 69,
PartitionFileManager.list_partition_files() may return an empty dict causing
files["files"] KeyError; change the handling after calling
pfm.list_partition_files(part_name) to default files_list = files.get("files",
[]) (or similar) and use files_list for sorting, popping "partition", writing
JSON lines to out_fh, writing the separator, and logging the count (use
len(files_list)); keep the rest of the try/except and variable names
(pfm.list_partition_files, files, out_fh, verbose, logger) intact.

@Ahmath-Gadji
Ahmath-Gadji merged commit 5fd0354 into dev Jan 16, 2026
3 checks passed
@coderabbitai coderabbitai Bot mentioned this pull request Jan 16, 2026
@coderabbitai coderabbitai Bot mentioned this pull request Jan 30, 2026
This was referenced Feb 8, 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 feat Add a new feature

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants