2025-01-21 20:52:28 +08:00
#
# Copyright 2025 The InfiniFlow Authors. All Rights Reserved.
#
2024-08-15 09:17:36 +08:00
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
2025-01-21 20:52:28 +08:00
2024-11-14 17:13:48 +08:00
import logging
2025-03-13 18:48:32 +08:00
import re
2025-10-17 18:46:47 +08:00
import os
2025-03-13 18:48:32 +08:00
from functools import reduce
2024-08-15 09:17:36 +08:00
from io import BytesIO
from timeit import default_timer as timer
2025-03-13 18:48:32 +08:00
from docx import Document
2025-08-14 12:14:03 +08:00
from docx . opc . pkgreader import _SerializedRelationships , _SerializedRelationship
2026-01-07 15:08:17 +08:00
from docx . table import Table as DocxTable
from docx . text . paragraph import Paragraph
2025-08-14 12:14:03 +08:00
from docx . opc . oxml import parse_xml
2025-07-15 13:03:01 +08:00
from markdown import markdown
2025-03-13 18:48:32 +08:00
from PIL import Image
2025-11-25 19:54:06 +08:00
from common . token_utils import num_tokens_from_string
2025-03-13 18:48:32 +08:00
2025-11-05 08:01:39 +08:00
from common . constants import LLMType
2025-03-18 14:52:20 +08:00
from api . db . services . llm_service import LLMBundle
2026-03-05 17:27:17 +08:00
from api . db . joint_services . tenant_model_service import get_model_config_by_type_and_name , get_tenant_default_model_by_type
2025-11-03 19:26:09 +08:00
from rag . utils . file_utils import extract_embed_file , extract_links_from_pdf , extract_links_from_docx , extract_html
2026-01-09 17:48:45 +08:00
from deepdoc . parser import DocxParser , ExcelParser , HtmlParser , JsonParser , MarkdownElementExtractor , MarkdownParser , PdfParser , TxtParser
from deepdoc . parser . figure_parser import VisionFigureParser , vision_figure_parser_docx_wrapper_naive , vision_figure_parser_pdf_wrapper
2025-03-20 10:48:38 +08:00
from deepdoc . parser . pdf_parser import PlainParser , VisionParser
2025-10-23 19:44:25 +08:00
from deepdoc . parser . docling_parser import DoclingParser
2025-10-27 15:14:58 +08:00
from deepdoc . parser . tcadp_parser import TCADPParser
2026-01-27 12:43:01 +08:00
from common . float_utils import normalize_overlapped_percent
2025-12-17 19:48:24 +08:00
from common . parser_config_utils import normalize_layout_recognizer
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
from common . text_utils import normalize_arabic_presentation_forms
2026-01-09 17:48:45 +08:00
from rag . nlp import (
concat_img ,
find_codec ,
naive_merge ,
naive_merge_with_images ,
naive_merge_docx ,
rag_tokenizer ,
tokenize_chunks ,
doc_tokenize_chunks_with_images ,
tokenize_table ,
append_context2table_image4pdf ,
tokenize_chunks_with_images ,
) # noqa: F401
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
def _normalize_section_text_for_rtl_presentation_forms ( sections ) :
if not sections :
return sections
normalized_sections = [ ]
for section in sections :
if isinstance ( section , tuple ) :
if not section :
normalized_sections . append ( section )
continue
text = section [ 0 ]
normalized_text = normalize_arabic_presentation_forms ( text )
normalized_sections . append ( ( normalized_text , * section [ 1 : ] ) )
continue
if isinstance ( section , list ) :
if not section :
normalized_sections . append ( section )
continue
text = section [ 0 ]
normalized_text = normalize_arabic_presentation_forms ( text )
normalized_sections . append ( [ normalized_text , * section [ 1 : ] ] )
continue
normalized_sections . append ( normalize_arabic_presentation_forms ( section ) )
return normalized_sections
2026-01-09 17:48:45 +08:00
def by_deepdoc ( filename , binary = None , from_page = 0 , to_page = 100000 , lang = " Chinese " , callback = None , pdf_cls = None , * * kwargs ) :
2025-11-05 13:00:42 +08:00
callback = callback
binary = binary
pdf_parser = pdf_cls ( ) if pdf_cls else Pdf ( )
2026-01-09 17:48:45 +08:00
sections , tables = pdf_parser ( filename if not binary else binary , from_page = from_page , to_page = to_page , callback = callback )
2025-11-06 15:20:35 +08:00
2026-01-05 09:55:43 +08:00
tables = vision_figure_parser_pdf_wrapper (
tbls = tables ,
sections = sections ,
callback = callback ,
* * kwargs ,
)
2025-11-05 13:00:42 +08:00
return sections , tables , pdf_parser
2025-12-17 19:48:24 +08:00
def by_mineru (
2026-01-09 17:48:45 +08:00
filename ,
binary = None ,
from_page = 0 ,
to_page = 100000 ,
lang = " Chinese " ,
callback = None ,
pdf_cls = None ,
parse_method : str = " raw " ,
mineru_llm_name : str | None = None ,
tenant_id : str | None = None ,
* * kwargs ,
2025-12-17 19:48:24 +08:00
) :
2025-12-09 18:54:14 +08:00
pdf_parser = None
if tenant_id :
if not mineru_llm_name :
try :
from api . db . services . tenant_llm_service import TenantLLMService
env_name = TenantLLMService . ensure_mineru_from_env ( tenant_id )
2025-12-11 17:33:12 +08:00
candidates = TenantLLMService . query ( tenant_id = tenant_id , llm_factory = " MinerU " , model_type = LLMType . OCR )
2025-12-09 18:54:14 +08:00
if candidates :
mineru_llm_name = candidates [ 0 ] . llm_name
elif env_name :
mineru_llm_name = env_name
except Exception as e : # best-effort fallback
logging . warning ( f " fallback to env mineru: { e } " )
if mineru_llm_name :
try :
2026-03-05 17:27:17 +08:00
ocr_model_config = get_model_config_by_type_and_name ( tenant_id , LLMType . OCR , mineru_llm_name )
ocr_model = LLMBundle ( tenant_id = tenant_id , model_config = ocr_model_config , lang = lang )
2025-12-09 18:54:14 +08:00
pdf_parser = ocr_model . mdl
sections , tables = pdf_parser . parse_pdf (
filepath = filename ,
binary = binary ,
callback = callback ,
parse_method = parse_method ,
2025-12-16 07:15:25 +02:00
lang = lang ,
2025-12-17 19:48:24 +08:00
* * kwargs ,
2025-12-09 18:54:14 +08:00
)
return sections , tables , pdf_parser
except Exception as e :
logging . error ( f " Failed to parse pdf via LLMBundle MinerU ( { mineru_llm_name } ): { e } " )
2025-11-05 13:00:42 +08:00
2025-12-09 18:54:14 +08:00
if callback :
callback ( - 1 , " MinerU not found. " )
return None , None , None
2025-11-05 13:00:42 +08:00
2025-12-16 07:15:25 +02:00
2026-01-09 17:48:45 +08:00
def by_docling ( filename , binary = None , from_page = 0 , to_page = 100000 , lang = " Chinese " , callback = None , pdf_cls = None , * * kwargs ) :
2025-11-05 13:00:42 +08:00
pdf_parser = DoclingParser ( )
2025-11-20 19:07:17 +08:00
parse_method = kwargs . get ( " parse_method " , " raw " )
2025-11-05 13:00:42 +08:00
if not pdf_parser . check_installation ( ) :
2026-03-12 18:09:03 +09:00
if callback :
callback ( - 1 , " Docling not found. " )
2025-11-10 15:08:24 +08:00
return None , None , pdf_parser
2025-11-05 13:00:42 +08:00
sections , tables = pdf_parser . parse_pdf (
filepath = filename ,
binary = binary ,
callback = callback ,
2026-03-12 18:09:03 +09:00
output_dir = os . environ . get ( " DOCLING_OUTPUT_DIR " , " " ) ,
delete_output = bool ( int ( os . environ . get ( " DOCLING_DELETE_OUTPUT " , 1 ) ) ) ,
docling_server_url = os . environ . get ( " DOCLING_SERVER_URL " , " " ) ,
2026-01-09 17:48:45 +08:00
parse_method = parse_method ,
2025-11-05 13:00:42 +08:00
)
return sections , tables , pdf_parser
2025-12-29 12:01:18 +08:00
def by_tcadp ( filename , binary = None , from_page = 0 , to_page = 100000 , lang = " Chinese " , callback = None , pdf_cls = None , * * kwargs ) :
2025-11-05 13:00:42 +08:00
tcadp_parser = TCADPParser ( )
if not tcadp_parser . check_installation ( ) :
callback ( - 1 , " TCADP parser not available. Please check Tencent Cloud API configuration. " )
2025-11-10 15:08:24 +08:00
return None , None , tcadp_parser
2025-11-05 13:00:42 +08:00
2026-01-09 17:48:45 +08:00
sections , tables = tcadp_parser . parse_pdf ( filepath = filename , binary = binary , callback = callback , output_dir = os . environ . get ( " TCADP_OUTPUT_DIR " , " " ) , file_type = " PDF " )
2025-11-05 13:00:42 +08:00
return sections , tables , tcadp_parser
2026-01-09 17:48:45 +08:00
def by_paddleocr (
filename ,
binary = None ,
from_page = 0 ,
to_page = 100000 ,
lang = " Chinese " ,
callback = None ,
pdf_cls = None ,
parse_method : str = " raw " ,
paddleocr_llm_name : str | None = None ,
tenant_id : str | None = None ,
* * kwargs ,
) :
pdf_parser = None
if tenant_id :
if not paddleocr_llm_name :
try :
from api . db . services . tenant_llm_service import TenantLLMService
env_name = TenantLLMService . ensure_paddleocr_from_env ( tenant_id )
candidates = TenantLLMService . query ( tenant_id = tenant_id , llm_factory = " PaddleOCR " , model_type = LLMType . OCR )
if candidates :
paddleocr_llm_name = candidates [ 0 ] . llm_name
elif env_name :
paddleocr_llm_name = env_name
except Exception as e : # best-effort fallback
logging . warning ( f " fallback to env paddleocr: { e } " )
if paddleocr_llm_name :
try :
2026-03-05 17:27:17 +08:00
ocr_model_config = get_model_config_by_type_and_name ( tenant_id , LLMType . OCR , paddleocr_llm_name )
ocr_model = LLMBundle ( tenant_id = tenant_id , model_config = ocr_model_config , lang = lang )
2026-01-09 17:48:45 +08:00
pdf_parser = ocr_model . mdl
sections , tables = pdf_parser . parse_pdf (
filepath = filename ,
binary = binary ,
callback = callback ,
parse_method = parse_method ,
* * kwargs ,
)
return sections , tables , pdf_parser
except Exception as e :
logging . error ( f " Failed to parse pdf via LLMBundle PaddleOCR ( { paddleocr_llm_name } ): { e } " )
return None , None , None
if callback :
callback ( - 1 , " PaddleOCR not found. " )
return None , None , None
2025-11-06 15:20:35 +08:00
def by_plaintext ( filename , binary = None , from_page = 0 , to_page = 100000 , callback = None , * * kwargs ) :
2025-12-17 19:48:24 +08:00
layout_recognizer = ( kwargs . get ( " layout_recognizer " ) or " " ) . strip ( )
if ( not layout_recognizer ) or ( layout_recognizer == " Plain Text " ) :
2025-11-05 13:00:42 +08:00
pdf_parser = PlainParser ( )
else :
2025-12-17 19:48:24 +08:00
tenant_id = kwargs . get ( " tenant_id " )
if not tenant_id :
raise ValueError ( " tenant_id is required when using vision layout recognizer " )
2026-03-05 17:27:17 +08:00
vision_model_config = get_model_config_by_type_and_name ( tenant_id , LLMType . IMAGE2TEXT , layout_recognizer )
2025-12-17 19:48:24 +08:00
vision_model = LLMBundle (
tenant_id ,
2026-03-05 17:27:17 +08:00
model_config = vision_model_config ,
2025-12-17 19:48:24 +08:00
lang = kwargs . get ( " lang " , " Chinese " ) ,
)
2025-11-05 13:00:42 +08:00
pdf_parser = VisionParser ( vision_model = vision_model , * * kwargs )
2025-11-20 10:08:42 +08:00
2026-01-09 17:48:45 +08:00
sections , tables = pdf_parser ( filename if not binary else binary , from_page = from_page , to_page = to_page , callback = callback )
2025-11-05 13:00:42 +08:00
return sections , tables , pdf_parser
PARSERS = {
2025-12-29 12:01:18 +08:00
" deepdoc " : by_deepdoc ,
" mineru " : by_mineru ,
" docling " : by_docling ,
2026-03-04 11:51:10 +08:00
" tcadp parser " : by_tcadp ,
2026-01-09 17:48:45 +08:00
" paddleocr " : by_paddleocr ,
2025-11-06 15:20:35 +08:00
" plaintext " : by_plaintext , # default
2025-11-05 13:00:42 +08:00
}
2024-09-30 16:59:39 +08:00
2024-08-15 09:17:36 +08:00
class Docx ( DocxParser ) :
def __init__ ( self ) :
pass
def __clean ( self , line ) :
line = re . sub ( r " \ u3000 " , " " , line ) . strip ( )
return line
2025-03-21 18:42:36 +08:00
def __get_nearest_title ( self , table_index , filename ) :
""" Get the hierarchical title structure before the table """
import re
from docx . text . paragraph import Paragraph
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
titles = [ ]
blocks = [ ]
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
# Get document name from filename parameter
doc_name = re . sub ( r " \ .[a-zA-Z]+$ " , " " , filename )
if not doc_name :
doc_name = " Untitled Document "
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
# Collect all document blocks while maintaining document order
try :
# Iterate through all paragraphs and tables in document order
for i , block in enumerate ( self . doc . _element . body ) :
2026-01-09 17:48:45 +08:00
if block . tag . endswith ( " p " ) : # Paragraph
2025-03-21 18:42:36 +08:00
p = Paragraph ( block , self . doc )
2026-01-09 17:48:45 +08:00
blocks . append ( ( " p " , i , p ) )
elif block . tag . endswith ( " tbl " ) : # Table
blocks . append ( ( " t " , i , None ) ) # Table object will be retrieved later
2025-03-21 18:42:36 +08:00
except Exception as e :
logging . error ( f " Error collecting blocks: { e } " )
return " "
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
# Find the target table position
target_table_pos = - 1
table_count = 0
for i , ( block_type , pos , _ ) in enumerate ( blocks ) :
2026-01-09 17:48:45 +08:00
if block_type == " t " :
2025-03-21 18:42:36 +08:00
if table_count == table_index :
target_table_pos = pos
break
table_count + = 1
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
if target_table_pos == - 1 :
return " " # Target table not found
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
# Find the nearest heading paragraph in reverse order
nearest_title = None
2025-12-29 12:01:18 +08:00
for i in range ( len ( blocks ) - 1 , - 1 , - 1 ) :
2025-03-21 18:42:36 +08:00
block_type , pos , block = blocks [ i ]
if pos > = target_table_pos : # Skip blocks after the table
continue
2025-07-15 13:03:01 +08:00
2026-01-09 17:48:45 +08:00
if block_type != " p " :
2025-03-21 18:42:36 +08:00
continue
2025-07-15 13:03:01 +08:00
2025-07-01 17:38:11 +08:00
if block . style and block . style . name and re . search ( r " Heading \ s*( \ d+) " , block . style . name , re . I ) :
2025-03-21 18:42:36 +08:00
try :
level_match = re . search ( r " ( \ d+) " , block . style . name )
if level_match :
level = int ( level_match . group ( 1 ) )
if level < = 7 : # Support up to 7 heading levels
title_text = block . text . strip ( )
if title_text : # Avoid empty titles
nearest_title = ( level , title_text )
break
except Exception as e :
logging . error ( f " Error parsing heading level: { e } " )
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
if nearest_title :
# Add current title
titles . append ( nearest_title )
current_level = nearest_title [ 0 ]
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
# Find all parent headings, allowing cross-level search
while current_level > 1 :
found = False
2025-12-29 12:01:18 +08:00
for i in range ( len ( blocks ) - 1 , - 1 , - 1 ) :
2025-03-21 18:42:36 +08:00
block_type , pos , block = blocks [ i ]
if pos > = target_table_pos : # Skip blocks after the table
continue
2025-07-15 13:03:01 +08:00
2026-01-09 17:48:45 +08:00
if block_type != " p " :
2025-03-21 18:42:36 +08:00
continue
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
if block . style and re . search ( r " Heading \ s*( \ d+) " , block . style . name , re . I ) :
try :
level_match = re . search ( r " ( \ d+) " , block . style . name )
if level_match :
level = int ( level_match . group ( 1 ) )
# Find any heading with a higher level
2025-07-15 13:03:01 +08:00
if level < current_level :
2025-03-21 18:42:36 +08:00
title_text = block . text . strip ( )
if title_text : # Avoid empty titles
titles . append ( ( level , title_text ) )
current_level = level
found = True
break
except Exception as e :
logging . error ( f " Error parsing parent heading: { e } " )
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
if not found : # Break if no parent heading is found
break
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
# Sort by level (ascending, from highest to lowest)
titles . sort ( key = lambda x : x [ 0 ] )
# Organize titles (from highest to lowest)
hierarchy = [ doc_name ] + [ t [ 1 ] for t in titles ]
return " > " . join ( hierarchy )
2025-07-15 13:03:01 +08:00
2025-03-21 18:42:36 +08:00
return " "
2024-08-15 09:17:36 +08:00
def __call__ ( self , filename , binary = None , from_page = 0 , to_page = 100000 ) :
2026-01-09 17:48:45 +08:00
self . doc = Document ( filename ) if not binary else Document ( BytesIO ( binary ) )
2024-08-15 09:17:36 +08:00
pn = 0
lines = [ ]
last_image = None
2026-01-07 15:08:17 +08:00
table_idx = 0
def flush_last_image ( ) :
nonlocal last_image , lines
if last_image is not None :
lines . append ( { " text " : " " , " image " : last_image , " table " : None , " style " : " Image " } )
last_image = None
for block in self . doc . _element . body :
2024-08-15 09:17:36 +08:00
if pn > to_page :
break
2026-01-07 15:08:17 +08:00
2026-01-09 17:48:45 +08:00
if block . tag . endswith ( " p " ) :
2026-01-07 15:08:17 +08:00
p = Paragraph ( block , self . doc )
if from_page < = pn < to_page :
text = p . text . strip ( )
style_name = p . style . name if p . style else " "
if text :
if style_name == " Caption " :
former_image = None
if lines and lines [ - 1 ] . get ( " image " ) and lines [ - 1 ] . get ( " style " ) != " Caption " :
former_image = lines [ - 1 ] . get ( " image " )
lines . pop ( )
elif last_image is not None :
former_image = last_image
last_image = None
lines . append (
{
" text " : self . __clean ( text ) ,
" image " : former_image if former_image else None ,
" table " : None ,
}
)
else :
flush_last_image ( )
lines . append (
{
" text " : self . __clean ( text ) ,
" image " : None ,
" table " : None ,
}
)
current_image = self . get_picture ( self . doc , p )
if current_image is not None :
lines . append (
{
" text " : " " ,
" image " : current_image ,
" table " : None ,
}
)
2024-08-15 09:17:36 +08:00
else :
current_image = self . get_picture ( self . doc , p )
2026-01-07 15:08:17 +08:00
if current_image is not None :
2024-08-15 09:17:36 +08:00
last_image = current_image
2026-01-07 15:08:17 +08:00
for run in p . runs :
xml = run . _element . xml
if " lastRenderedPageBreak " in xml :
pn + = 1
continue
if " w:br " in xml and ' type= " page " ' in xml :
pn + = 1
2026-01-09 17:48:45 +08:00
elif block . tag . endswith ( " tbl " ) :
2026-01-07 15:08:17 +08:00
if pn < from_page or pn > to_page :
table_idx + = 1
2024-08-15 09:17:36 +08:00
continue
2026-01-07 15:08:17 +08:00
flush_last_image ( )
tb = DocxTable ( block , self . doc )
title = self . __get_nearest_title ( table_idx , filename )
html = " <table> "
if title :
html + = f " <caption>Table Location: { title } </caption> "
for r in tb . rows :
html + = " <tr> "
col_idx = 0
try :
while col_idx < len ( r . cells ) :
span = 1
c = r . cells [ col_idx ]
for j in range ( col_idx + 1 , len ( r . cells ) ) :
if c . text == r . cells [ j ] . text :
span + = 1
col_idx = j
else :
break
col_idx + = 1
html + = f " <td> { c . text } </td> " if span == 1 else f " <td colspan= ' { span } ' > { c . text } </td> "
except Exception as e :
logging . warning ( f " Error parsing table, ignore: { e } " )
html + = " </tr> "
html + = " </table> "
lines . append ( { " text " : " " , " image " : None , " table " : html } )
table_idx + = 1
flush_last_image ( )
new_line = [ ( line . get ( " text " ) , line . get ( " image " ) , line . get ( " table " ) ) for line in lines ]
return new_line
2025-10-10 09:39:15 +08:00
def to_markdown ( self , filename = None , binary = None , inline_images : bool = True ) :
"""
This function uses mammoth , licensed under the BSD 2 - Clause License .
"""
import base64
import uuid
import mammoth
from markdownify import markdownify
docx_file = BytesIO ( binary ) if binary else open ( filename , " rb " )
def _convert_image_to_base64 ( image ) :
try :
with image . open ( ) as image_file :
image_bytes = image_file . read ( )
encoded = base64 . b64encode ( image_bytes ) . decode ( " utf-8 " )
base64_url = f " data: { image . content_type } ;base64, { encoded } "
alt_name = " image "
alt_name = f " img_ { uuid . uuid4 ( ) . hex [ : 8 ] } "
return { " src " : base64_url , " alt " : alt_name }
except Exception as e :
logging . warning ( f " Failed to convert image to base64: { e } " )
return { " src " : " " , " alt " : " image " }
try :
if inline_images :
2026-01-09 17:48:45 +08:00
result = mammoth . convert_to_html ( docx_file , convert_image = mammoth . images . img_element ( _convert_image_to_base64 ) )
2025-10-10 09:39:15 +08:00
else :
result = mammoth . convert_to_html ( docx_file )
html = result . value
markdown_text = markdownify ( html )
return markdown_text
finally :
if not binary :
docx_file . close ( )
2024-08-15 09:17:36 +08:00
class Pdf ( PdfParser ) :
2025-03-17 16:49:54 +08:00
def __init__ ( self ) :
super ( ) . __init__ ( )
2025-03-17 11:58:40 +08:00
2026-01-09 17:48:45 +08:00
def __call__ ( self , filename , binary = None , from_page = 0 , to_page = 100000 , zoomin = 3 , callback = None , separate_tables_figures = False ) :
2024-08-15 09:17:36 +08:00
start = timer ( )
2024-11-30 18:48:06 +08:00
first_start = start
callback ( msg = " OCR started " )
2026-01-09 17:48:45 +08:00
self . __images__ ( filename if not binary else binary , zoomin , from_page , to_page , callback )
2024-11-30 18:48:06 +08:00
callback ( msg = " OCR finished ( {:.2f} s) " . format ( timer ( ) - start ) )
logging . info ( " OCR( {} ~ {} ): {:.2f} s " . format ( from_page , to_page , timer ( ) - start ) )
2024-08-15 09:17:36 +08:00
start = timer ( )
self . _layouts_rec ( zoomin )
2024-11-30 18:48:06 +08:00
callback ( 0.63 , " Layout analysis ( {:.2f} s) " . format ( timer ( ) - start ) )
start = timer ( )
2024-08-15 09:17:36 +08:00
self . _table_transformer_job ( zoomin )
2024-11-30 18:48:06 +08:00
callback ( 0.65 , " Table analysis ( {:.2f} s) " . format ( timer ( ) - start ) )
start = timer ( )
2025-10-11 18:46:09 +08:00
self . _text_merge ( zoomin = zoomin )
2024-11-30 18:48:06 +08:00
callback ( 0.67 , " Text merged ( {:.2f} s) " . format ( timer ( ) - start ) )
2024-08-15 09:17:36 +08:00
2025-03-20 09:39:32 +08:00
if separate_tables_figures :
tbls , figures = self . _extract_table_figure ( True , zoomin , True , True , True )
self . _concat_downward ( )
logging . info ( " layouts cost: {} s " . format ( timer ( ) - first_start ) )
return [ ( b [ " text " ] , self . _line_tag ( b , zoomin ) ) for b in self . boxes ] , tbls , figures
else :
tbls = self . _extract_table_figure ( True , zoomin , True , True )
2025-08-20 17:29:15 +08:00
self . _naive_vertical_merge ( )
2025-03-20 09:39:32 +08:00
self . _concat_downward ( )
2025-12-30 20:24:27 +08:00
# self._final_reading_order_merge()
2025-03-20 09:39:32 +08:00
# self._filter_forpages()
logging . info ( " layouts cost: {} s " . format ( timer ( ) - first_start ) )
return [ ( b [ " text " ] , self . _line_tag ( b , zoomin ) ) for b in self . boxes ] , tbls
2024-08-15 09:17:36 +08:00
class Markdown ( MarkdownParser ) :
2025-11-03 09:34:12 +08:00
def md_to_html ( self , sections ) :
2025-04-25 18:35:28 +08:00
if not sections :
return [ ]
if isinstance ( sections , type ( " " ) ) :
text = sections
elif isinstance ( sections [ 0 ] , type ( " " ) ) :
text = sections [ 0 ]
else :
return [ ]
2025-07-15 13:03:01 +08:00
2025-04-25 18:35:28 +08:00
from bs4 import BeautifulSoup
2026-01-09 17:48:45 +08:00
2025-05-12 17:16:55 +08:00
html_content = markdown ( text )
2026-01-09 17:48:45 +08:00
soup = BeautifulSoup ( html_content , " html.parser " )
2025-11-03 09:34:12 +08:00
return soup
def get_hyperlink_urls ( self , soup ) :
if soup :
2026-01-09 17:48:45 +08:00
return set ( [ a . get ( " href " ) for a in soup . find_all ( " a " ) if a . get ( " href " ) ] )
2025-11-03 09:34:12 +08:00
return [ ]
2025-11-25 19:54:06 +08:00
def extract_image_urls_with_lines ( self , text ) :
md_img_re = re . compile ( r " ! \ [[^ \ ]]* \ ] \ (([^) \ s]+) " )
html_img_re = re . compile ( r ' src=[ " \\ \' ]([^ " \\ \' > \\ s]+) ' , re . IGNORECASE )
urls = [ ]
seen = set ( )
lines = text . splitlines ( )
for idx , line in enumerate ( lines ) :
for url in md_img_re . findall ( line ) :
if ( url , idx ) not in seen :
urls . append ( { " url " : url , " line " : idx } )
seen . add ( ( url , idx ) )
for url in html_img_re . findall ( line ) :
if ( url , idx ) not in seen :
urls . append ( { " url " : url , " line " : idx } )
seen . add ( ( url , idx ) )
# cross-line
try :
from bs4 import BeautifulSoup
2026-01-09 17:48:45 +08:00
soup = BeautifulSoup ( text , " html.parser " )
2025-11-25 19:54:06 +08:00
newline_offsets = [ m . start ( ) for m in re . finditer ( r " \ n " , text ) ] + [ len ( text ) ]
2026-01-09 17:48:45 +08:00
for img_tag in soup . find_all ( " img " ) :
src = img_tag . get ( " src " )
2025-11-25 19:54:06 +08:00
if not src :
continue
tag_str = str ( img_tag )
pos = text . find ( tag_str )
if pos == - 1 :
# fallback
pos = max ( text . find ( src ) , 0 )
line_no = 0
for i , off in enumerate ( newline_offsets ) :
if pos < = off :
line_no = i
break
if ( src , line_no ) not in seen :
urls . append ( { " url " : src , " line " : line_no } )
seen . add ( ( src , line_no ) )
2025-12-30 15:04:09 +08:00
except Exception as e :
logging . error ( " Failed to extract image urls: {} " . format ( e ) )
2025-11-25 19:54:06 +08:00
pass
return urls
def load_images_from_urls ( self , urls , cache = None ) :
2025-04-25 18:35:28 +08:00
import requests
2025-11-25 19:54:06 +08:00
from pathlib import Path
cache = cache or { }
2025-04-25 18:35:28 +08:00
images = [ ]
2025-11-25 19:54:06 +08:00
for url in urls :
if url in cache :
if cache [ url ] :
images . append ( cache [ url ] )
2025-11-10 13:28:07 +08:00
continue
2025-11-25 19:54:06 +08:00
img_obj = None
2025-04-25 18:35:28 +08:00
try :
2026-01-09 17:48:45 +08:00
if url . startswith ( ( " http:// " , " https:// " ) ) :
2025-07-18 17:06:58 +08:00
response = requests . get ( url , stream = True , timeout = 30 )
2026-01-09 17:48:45 +08:00
if response . status_code == 200 and response . headers . get ( " Content-Type " , " " ) . startswith ( " image/ " ) :
img_obj = Image . open ( BytesIO ( response . content ) ) . convert ( " RGB " )
2025-07-18 17:06:58 +08:00
else :
local_path = Path ( url )
2025-11-25 19:54:06 +08:00
if local_path . exists ( ) :
2026-01-09 17:48:45 +08:00
img_obj = Image . open ( url ) . convert ( " RGB " )
2025-11-25 19:54:06 +08:00
else :
2025-07-18 17:06:58 +08:00
logging . warning ( f " Local image file not found: { url } " )
2025-04-25 18:35:28 +08:00
except Exception as e :
logging . error ( f " Failed to download/open image from { url } : { e } " )
2025-11-25 19:54:06 +08:00
cache [ url ] = img_obj
if img_obj :
images . append ( img_obj )
return images , cache
2025-07-15 13:03:01 +08:00
2025-11-25 19:54:06 +08:00
def __call__ ( self , filename , binary = None , separate_tables = True , delimiter = None , return_section_images = False ) :
2024-08-15 09:17:36 +08:00
if binary :
encoding = find_codec ( binary )
txt = binary . decode ( encoding , errors = " ignore " )
else :
with open ( filename , " r " ) as f :
txt = f . read ( )
2025-08-21 09:36:18 +08:00
2026-01-09 17:48:45 +08:00
remainder , tables = self . extract_tables_and_remainder ( f " { txt } \n " , separate_tables = separate_tables )
2025-11-05 16:10:21 +08:00
# To eliminate duplicate tables in chunking result, uncomment code below and set separate_tables to True in line 410.
# extractor = MarkdownElementExtractor(remainder)
2025-08-21 09:36:18 +08:00
extractor = MarkdownElementExtractor ( txt )
2025-11-25 19:54:06 +08:00
image_refs = self . extract_image_urls_with_lines ( txt )
element_sections = extractor . extract_elements ( delimiter , include_meta = True )
sections = [ ]
section_images = [ ]
image_cache = { }
for element in element_sections :
content = element [ " content " ]
start_line = element [ " start_line " ]
end_line = element [ " end_line " ]
urls_in_section = [ ref [ " url " ] for ref in image_refs if start_line < = ref [ " line " ] < = end_line ]
imgs = [ ]
if urls_in_section :
imgs , image_cache = self . load_images_from_urls ( urls_in_section , image_cache )
combined_image = None
if imgs :
combined_image = reduce ( concat_img , imgs ) if len ( imgs ) > 1 else imgs [ 0 ]
sections . append ( ( content , " " ) )
section_images . append ( combined_image )
2024-08-15 09:17:36 +08:00
tbls = [ ]
for table in tables :
2026-01-09 17:48:45 +08:00
tbls . append ( ( ( None , markdown ( table , extensions = [ " markdown.extensions.tables " ] ) ) , " " ) )
2025-11-25 19:54:06 +08:00
if return_section_images :
return sections , tbls , section_images
2024-08-15 09:17:36 +08:00
return sections , tbls
2025-12-29 12:01:18 +08:00
2025-08-14 12:14:03 +08:00
def load_from_xml_v2 ( baseURI , rels_item_xml ) :
"""
Return | _SerializedRelationships | instance loaded with the
relationships contained in * rels_item_xml * . Returns an empty
collection if * rels_item_xml * is | None | .
"""
srels = _SerializedRelationships ( )
if rels_item_xml is not None :
rels_elm = parse_xml ( rels_item_xml )
for rel_elm in rels_elm . Relationship_lst :
2026-01-14 06:08:46 -05:00
if rel_elm . target_ref in ( " ../NULL " , " NULL " ) or rel_elm . target_ref . startswith ( " # " ) :
2025-08-14 12:14:03 +08:00
continue
srels . _srels . append ( _SerializedRelationship ( baseURI , rel_elm ) )
return srels
2024-08-15 09:17:36 +08:00
2025-12-29 12:01:18 +08:00
2025-11-28 19:25:32 +08:00
def chunk ( filename , binary = None , from_page = 0 , to_page = 100000 , lang = " Chinese " , callback = None , * * kwargs ) :
2024-08-15 09:17:36 +08:00
"""
2026-01-09 17:48:45 +08:00
Supported file formats are docx , pdf , excel , txt .
This method apply the naive ways to chunk files .
Successive text will be sliced into pieces using ' delimiter ' .
Next , these successive pieces are merge into chunks whose token number is no more than ' Max token number ' .
2024-08-15 09:17:36 +08:00
"""
2025-11-03 09:34:12 +08:00
urls = set ( )
url_res = [ ]
2025-10-27 17:04:13 +08:00
2024-12-01 22:28:00 +08:00
is_english = lang . lower ( ) == " english " # is_english(cks)
2026-01-09 17:48:45 +08:00
parser_config = kwargs . get ( " parser_config " , { " chunk_token_num " : 512 , " delimiter " : " \n !?。;!? " , " layout_recognize " : " DeepDOC " , " analyze_hyperlink " : True } )
2025-11-28 19:25:32 +08:00
2026-01-09 17:48:45 +08:00
child_deli = ( parser_config . get ( " children_delimiter " ) or " " ) . encode ( " utf-8 " ) . decode ( " unicode_escape " ) . encode ( " latin1 " ) . decode ( " utf-8 " )
2025-12-17 16:50:36 +08:00
cust_child_deli = re . findall ( r " `([^`]+)` " , child_deli )
child_deli = " | " . join ( re . sub ( r " `([^`]+)` " , " " , child_deli ) )
if cust_child_deli :
cust_child_deli = sorted ( set ( cust_child_deli ) , key = lambda x : - len ( x ) )
cust_child_deli = " | " . join ( re . escape ( t ) for t in cust_child_deli if t )
child_deli + = cust_child_deli
2025-11-28 19:25:32 +08:00
is_markdown = False
2025-11-27 10:21:44 +08:00
table_context_size = max ( 0 , int ( parser_config . get ( " table_context_size " , 0 ) or 0 ) )
image_context_size = max ( 0 , int ( parser_config . get ( " image_context_size " , 0 ) or 0 ) )
2025-11-28 19:25:32 +08:00
2026-01-09 17:48:45 +08:00
doc = { " docnm_kwd " : filename , " title_tks " : rag_tokenizer . tokenize ( re . sub ( r " \ .[a-zA-Z]+$ " , " " , filename ) ) }
2024-08-15 09:17:36 +08:00
doc [ " title_sm_tks " ] = rag_tokenizer . fine_grained_tokenize ( doc [ " title_tks " ] )
res = [ ]
pdf_parser = None
2025-04-25 18:35:28 +08:00
section_images = None
2025-10-17 18:46:47 +08:00
is_root = kwargs . get ( " is_root " , True )
embed_res = [ ]
if is_root :
# Only extract embedded files at the root call
embeds = [ ]
if binary is not None :
embeds = extract_embed_file ( binary )
else :
raise Exception ( " Embedding extraction from file path is not supported. " )
2025-10-27 17:04:13 +08:00
2025-10-17 18:46:47 +08:00
# Recursively chunk each embedded file and collect results
for embed_filename , embed_bytes in embeds :
try :
2026-01-09 17:48:45 +08:00
sub_res = chunk ( embed_filename , binary = embed_bytes , lang = lang , callback = callback , is_root = False , * * kwargs ) or [ ]
2025-10-17 18:46:47 +08:00
embed_res . extend ( sub_res )
except Exception as e :
2025-12-30 15:04:09 +08:00
error_msg = f " Failed to chunk embed { embed_filename } : { e } "
logging . error ( error_msg )
2025-10-17 18:46:47 +08:00
if callback :
2025-12-30 15:04:09 +08:00
callback ( 0.05 , error_msg )
2025-10-17 18:46:47 +08:00
continue
2024-08-15 09:17:36 +08:00
if re . search ( r " \ .docx$ " , filename , re . IGNORECASE ) :
callback ( 0.1 , " Start to parse. " )
2025-11-03 09:34:12 +08:00
if parser_config . get ( " analyze_hyperlink " , False ) and is_root :
urls = extract_links_from_docx ( binary )
for index , url in enumerate ( urls ) :
html_bytes , metadata = extract_html ( url )
if not html_bytes :
continue
try :
sub_url_res = chunk ( url , html_bytes , callback = callback , lang = lang , is_root = False , * * kwargs )
except Exception as e :
logging . info ( f " Failed to chunk url in registered file type { url } : { e } " )
2026-01-09 17:48:45 +08:00
sub_url_res = chunk ( f " { index } .html " , html_bytes , callback = callback , lang = lang , is_root = False , * * kwargs )
2025-11-03 09:34:12 +08:00
url_res . extend ( sub_url_res )
2025-03-20 11:24:44 +08:00
2025-08-14 12:14:03 +08:00
# fix "There is no item named 'word/NULL' in the archive", referring to https://github.com/python-openxml/python-docx/issues/1105#issuecomment-1298075246
_SerializedRelationships . load_from_xml = load_from_xml_v2
2024-08-15 09:17:36 +08:00
2026-01-07 15:08:17 +08:00
# sections = (text, image, tables)
sections = Docx ( ) ( filename , binary )
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2024-08-15 09:17:36 +08:00
2026-01-07 15:08:17 +08:00
# chunks list[dict]
# images list - index of image chunk in chunks
2026-01-09 17:48:45 +08:00
chunks , images = naive_merge_docx ( sections , int ( parser_config . get ( " chunk_token_num " , 128 ) ) , parser_config . get ( " delimiter " , " \n !?。;!? " ) , table_context_size , image_context_size )
2026-01-07 15:08:17 +08:00
vision_figure_parser_docx_wrapper_naive ( chunks = chunks , idx_lst = images , callback = callback , * * kwargs )
2024-08-15 09:17:36 +08:00
2026-01-07 15:08:17 +08:00
callback ( 0.8 , " Finish parsing. " )
st = timer ( )
res . extend ( doc_tokenize_chunks_with_images ( chunks , doc , is_english , child_delimiters_pattern = child_deli ) )
2024-11-14 17:13:48 +08:00
logging . info ( " naive_merge( {} ): {} " . format ( filename , timer ( ) - st ) )
2025-10-17 18:46:47 +08:00
res . extend ( embed_res )
2025-11-03 09:34:12 +08:00
res . extend ( url_res )
2024-08-15 09:17:36 +08:00
return res
elif re . search ( r " \ .pdf$ " , filename , re . IGNORECASE ) :
2026-01-09 17:48:45 +08:00
layout_recognizer , parser_model_name = normalize_layout_recognizer ( parser_config . get ( " layout_recognize " , " DeepDOC " ) )
2025-12-09 18:54:14 +08:00
2025-11-03 09:34:12 +08:00
if parser_config . get ( " analyze_hyperlink " , False ) and is_root :
urls = extract_links_from_pdf ( binary )
2025-03-20 14:50:59 +08:00
if isinstance ( layout_recognizer , bool ) :
2026-03-04 11:51:10 +08:00
layout_recognizer = " DeepDOC " if layout_recognizer else " PlainText "
2025-03-20 09:39:32 +08:00
2025-11-05 13:00:42 +08:00
name = layout_recognizer . strip ( ) . lower ( )
2025-11-06 15:20:35 +08:00
parser = PARSERS . get ( name , by_plaintext )
2025-11-05 13:00:42 +08:00
callback ( 0.1 , " Start to parse. " )
2025-10-17 18:46:47 +08:00
2025-11-06 15:20:35 +08:00
sections , tables , pdf_parser = parser (
2025-12-29 12:01:18 +08:00
filename = filename ,
binary = binary ,
from_page = from_page ,
to_page = to_page ,
lang = lang ,
callback = callback ,
layout_recognizer = layout_recognizer ,
mineru_llm_name = parser_model_name ,
2026-01-09 17:48:45 +08:00
paddleocr_llm_name = parser_model_name ,
* * kwargs ,
2025-11-05 13:00:42 +08:00
)
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2025-10-23 19:44:25 +08:00
2025-11-05 13:00:42 +08:00
if not sections and not tables :
return [ ]
2025-10-27 15:14:58 +08:00
2025-12-30 20:24:27 +08:00
if table_context_size or image_context_size :
tables = append_context2table_image4pdf ( sections , tables , image_context_size )
2026-01-09 17:48:45 +08:00
if name in [ " tcadp " , " docling " , " mineru " , " paddleocr " ] :
Fix: respect user-configured chunk_token_num for MinerU/docling/paddleocr parsers (#13234)
## Summary
When using MinerU, docling, TCADP, or paddleocr as the PDF parser with
the General (naive) chunk method, the user-configured `chunk_token_num`
is **unconditionally overwritten to 0** at
[rag/app/naive.py#L858-L859](https://github.com/infiniflow/ragflow/blob/main/rag/app/naive.py#L858-L859),
effectively disabling chunk merging regardless of what the user sets in
the UI.
### Problem
A user sets `chunk_token_num = 2048` in the dataset configuration UI,
expecting small parser blocks to be merged into larger chunks. However,
this line:
```python
if name in ["tcadp", "docling", "mineru", "paddleocr"]:
parser_config["chunk_token_num"] = 0
```
silently overrides the user's setting. As a result, every MinerU output
block becomes its own chunk. For short documents (e.g. a 3-page PDF fund
factsheet parsed by MinerU), this produces **47 tiny chunks** — some as
small as 11 characters (`"July 2025"`) or 15 characters (`"CIES
Eligible"`).
This severely degrades retrieval quality: vector embeddings of such
short fragments have minimal semantic value, and keyword search produces
excessive noise.
### Fix
Only apply the `chunk_token_num = 0` override when the user has **not**
explicitly configured a positive value:
```python
if name in ["tcadp", "docling", "mineru", "paddleocr"]:
if int(parser_config.get("chunk_token_num", 0)) <= 0:
parser_config["chunk_token_num"] = 0
```
This preserves the original default behavior (no merging) while
respecting the user's explicit configuration.
### Before / After (MinerU, 3-page PDF, chunk_token_num=2048)
| | Before | After |
|---|---|---|
| Chunks produced | 47 | ~8 (merged by token limit) |
| Smallest chunk | 11 chars | ~500 chars |
| User setting respected | No | Yes |
## Test plan
- [ ] Parse a PDF with MinerU and `chunk_token_num = 2048` → verify
chunks are merged up to token limit
- [ ] Parse a PDF with MinerU and `chunk_token_num = 0` (or default) →
verify original behavior (no merging)
- [ ] Parse a PDF with DeepDOC parser → verify no change in behavior
(not affected by this code path)
- [ ] Repeat with docling/paddleocr if available
2026-03-02 15:31:40 +08:00
if int ( parser_config . get ( " chunk_token_num " , 0 ) ) < = 0 :
parser_config [ " chunk_token_num " ] = 0
2025-11-20 10:08:42 +08:00
2025-11-05 13:00:42 +08:00
res = tokenize_table ( tables , doc , is_english )
callback ( 0.8 , " Finish parsing. " )
2024-08-15 09:17:36 +08:00
2025-03-12 19:20:50 +08:00
elif re . search ( r " \ .(csv|xlsx?)$ " , filename , re . IGNORECASE ) :
2024-08-15 09:17:36 +08:00
callback ( 0.1 , " Start to parse. " )
2025-11-20 10:08:42 +08:00
# Check if tcadp_parser is selected for spreadsheet files
layout_recognizer = parser_config . get ( " layout_recognize " , " DeepDOC " )
if layout_recognizer == " TCADP Parser " :
table_result_type = parser_config . get ( " table_result_type " , " 1 " )
markdown_image_response_type = parser_config . get ( " markdown_image_response_type " , " 1 " )
2026-01-09 17:48:45 +08:00
tcadp_parser = TCADPParser ( table_result_type = table_result_type , markdown_image_response_type = markdown_image_response_type )
2025-11-20 10:08:42 +08:00
if not tcadp_parser . check_installation ( ) :
callback ( - 1 , " TCADP parser not available. Please check Tencent Cloud API configuration. " )
return res
# Determine file type based on extension
file_type = " XLSX " if re . search ( r " \ .xlsx?$ " , filename , re . IGNORECASE ) else " CSV "
2026-01-09 17:48:45 +08:00
sections , tables = tcadp_parser . parse_pdf ( filepath = filename , binary = binary , callback = callback , output_dir = os . environ . get ( " TCADP_OUTPUT_DIR " , " " ) , file_type = file_type )
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2025-11-20 10:08:42 +08:00
parser_config [ " chunk_token_num " ] = 0
res = tokenize_table ( tables , doc , is_english )
callback ( 0.8 , " Finish parsing. " )
2024-09-20 15:33:38 +08:00
else :
2025-11-20 10:08:42 +08:00
# Default DeepDOC parser
excel_parser = ExcelParser ( )
if parser_config . get ( " html4excel " ) :
sections = [ ( _ , " " ) for _ in excel_parser . html ( binary , 12 ) if _ ]
2025-11-25 19:54:20 +08:00
parser_config [ " chunk_token_num " ] = 0
2025-11-20 10:08:42 +08:00
else :
sections = [ ( _ , " " ) for _ in excel_parser ( binary ) if _ ]
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2024-08-15 09:17:36 +08:00
elif re . search ( r " \ .(txt|py|js|java|c|cpp|h|php|go|ts|sh|cs|kt|sql)$ " , filename , re . IGNORECASE ) :
callback ( 0.1 , " Start to parse. " )
2026-01-09 17:48:45 +08:00
sections = TxtParser ( ) ( filename , binary , parser_config . get ( " chunk_token_num " , 128 ) , parser_config . get ( " delimiter " , " \n !?;。;!? " ) )
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2024-08-15 09:17:36 +08:00
callback ( 0.8 , " Finish parsing. " )
2024-10-09 19:37:32 +08:00
2025-12-29 12:54:31 +08:00
elif re . search ( r " \ .(md|markdown|mdx)$ " , filename , re . IGNORECASE ) :
2024-08-15 09:17:36 +08:00
callback ( 0.1 , " Start to parse. " )
2025-04-25 18:35:28 +08:00
markdown_parser = Markdown ( int ( parser_config . get ( " chunk_token_num " , 128 ) ) )
2025-11-25 19:54:06 +08:00
sections , tables , section_images = markdown_parser (
filename ,
binary ,
separate_tables = False ,
delimiter = parser_config . get ( " delimiter " , " \n !?;。;!? " ) ,
return_section_images = True ,
)
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2025-11-25 19:54:06 +08:00
2025-11-28 19:25:32 +08:00
is_markdown = True
2025-07-15 13:03:01 +08:00
2025-09-18 09:44:17 +08:00
try :
2026-03-05 17:27:17 +08:00
vision_model_config = get_tenant_default_model_by_type ( kwargs [ " tenant_id " ] , LLMType . IMAGE2TEXT )
vision_model = LLMBundle ( kwargs [ " tenant_id " ] , vision_model_config )
2025-09-18 09:44:17 +08:00
callback ( 0.2 , " Visual model detected. Attempting to enhance figure extraction... " )
2025-12-30 15:04:09 +08:00
except Exception as e :
logging . warning ( f " Failed to detect figure extraction: { e } " )
2025-09-18 09:44:17 +08:00
vision_model = None
2025-10-10 09:39:15 +08:00
2025-09-18 09:44:17 +08:00
if vision_model :
# Process images for each section
for idx , ( section_text , _ ) in enumerate ( sections ) :
2025-11-25 19:54:06 +08:00
images = [ ]
if section_images and len ( section_images ) > idx and section_images [ idx ] is not None :
images . append ( section_images [ idx ] )
2025-09-18 09:44:17 +08:00
2025-11-25 19:54:06 +08:00
if images and len ( images ) > 0 :
2025-09-18 09:44:17 +08:00
# If multiple images found, combine them using concat_img
combined_image = reduce ( concat_img , images ) if len ( images ) > 1 else images [ 0 ]
2025-11-25 19:54:06 +08:00
if section_images :
section_images [ idx ] = combined_image
else :
section_images = [ None ] * len ( sections )
section_images [ idx ] = combined_image
2026-01-09 17:48:45 +08:00
markdown_vision_parser = VisionFigureParser ( vision_model = vision_model , figures_data = [ ( ( combined_image , [ " markdown image " ] ) , [ ( 0 , 0 , 0 , 0 , 0 ) ] ) ] , * * kwargs )
2025-09-18 09:44:17 +08:00
boosted_figures = markdown_vision_parser ( callback = callback )
2026-01-09 17:48:45 +08:00
sections [ idx ] = ( section_text + " \n \n " + " \n \n " . join ( [ fig [ 0 ] [ 1 ] for fig in boosted_figures ] ) , sections [ idx ] [ 1 ] )
2025-11-03 09:34:12 +08:00
2025-09-18 09:44:17 +08:00
else :
logging . warning ( " No visual model detected. Skipping figure parsing enhancement. " )
2025-07-15 13:03:01 +08:00
2025-11-03 09:34:12 +08:00
if parser_config . get ( " hyperlink_urls " , False ) and is_root :
for idx , ( section_text , _ ) in enumerate ( sections ) :
soup = markdown_parser . md_to_html ( section_text )
hyperlink_urls = markdown_parser . get_hyperlink_urls ( soup )
urls . update ( hyperlink_urls )
2024-12-01 22:28:00 +08:00
res = tokenize_table ( tables , doc , is_english )
2024-08-15 09:17:36 +08:00
callback ( 0.8 , " Finish parsing. " )
elif re . search ( r " \ .(htm|html)$ " , filename , re . IGNORECASE ) :
callback ( 0.1 , " Start to parse. " )
2025-08-27 12:43:55 +08:00
chunk_token_num = int ( parser_config . get ( " chunk_token_num " , 128 ) )
sections = HtmlParser ( ) ( filename , binary , chunk_token_num )
2024-09-29 10:29:56 +08:00
sections = [ ( _ , " " ) for _ in sections if _ ]
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2024-08-15 09:17:36 +08:00
callback ( 0.8 , " Finish parsing. " )
2025-07-30 09:48:20 +08:00
elif re . search ( r " \ .(json|jsonl|ldjson)$ " , filename , re . IGNORECASE ) :
2024-08-15 09:17:36 +08:00
callback ( 0.1 , " Start to parse. " )
2024-12-03 19:02:03 +08:00
chunk_token_num = int ( parser_config . get ( " chunk_token_num " , 128 ) )
sections = JsonParser ( chunk_token_num ) ( binary )
2024-09-29 10:29:56 +08:00
sections = [ ( _ , " " ) for _ in sections if _ ]
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2024-08-15 09:17:36 +08:00
callback ( 0.8 , " Finish parsing. " )
elif re . search ( r " \ .doc$ " , filename , re . IGNORECASE ) :
callback ( 0.1 , " Start to parse. " )
2025-11-03 09:34:12 +08:00
2025-11-07 11:46:10 +08:00
try :
from tika import parser as tika_parser
except Exception as e :
callback ( 0.8 , f " tika not available: { e } . Unsupported .doc parsing. " )
logging . warning ( f " tika not available: { e } . Unsupported .doc parsing for { filename } . " )
return [ ]
2024-08-15 09:17:36 +08:00
binary = BytesIO ( binary )
2025-11-07 11:46:10 +08:00
doc_parsed = tika_parser . from_buffer ( binary )
2026-01-09 17:48:45 +08:00
if doc_parsed . get ( " content " , None ) is not None :
sections = doc_parsed [ " content " ] . split ( " \n " )
2024-11-22 11:05:06 +08:00
sections = [ ( _ , " " ) for _ in sections if _ ]
Feature rtl support (#13118)
### What problem does this PR solve?
This PR adds comprehensive **Right-to-Left (RTL) language support**,
primarily targeting Arabic and other RTL scripts (Hebrew, Persian, Urdu,
etc.).
Previously, RTL content had multiple rendering issues:
- Incorrect sentence splitting for Arabic punctuation in citation logic
- Misaligned text in chat messages and markdown components
- Improper positioning of blockquotes and “think” sections
- Incorrect table alignment
- Citation placement ambiguity in RTL prompts
- UI layout inconsistencies when mixing LTR and RTL text
This PR introduces backend and frontend improvements to properly detect,
render, and style RTL content while preserving existing LTR behavior.
#### Backend
- Updated sentence boundary regex in `rag/nlp/search.py` to include
Arabic punctuation:
- `،` (comma)
- `؛` (semicolon)
- `؟` (question mark)
- `۔` (Arabic full stop)
- Ensures citation insertion works correctly in RTL sentences.
- Updated citation prompt instructions to clarify citation placement
rules for RTL languages.
#### Frontend
- Introduced a new utility: `text-direction.ts`
- Detects text direction based on Unicode ranges.
- Supports Arabic, Hebrew, Syriac, Thaana, and related scripts.
- Provides `getDirAttribute()` for automatic `dir` assignment.
- Applied dynamic `dir` attributes across:
- Markdown rendering
- Chat messages
- Search results
- Tables
- Hover cards and reference popovers
- Added proper RTL styling in LESS:
- Text alignment adjustments
- Blockquote border flipping
- Section indentation correction
- Table direction switching
- Use of `<bdi>` for figure labels to prevent bidirectional conflicts
#### DevOps / Environment
- Added Windows backend launch script with retry handling.
- Updated dependency metadata.
- Adjusted development-only React debugging behavior.
---
### Type of change
- [x] Bug Fix (non-breaking change which fixes RTL rendering and
citation issues)
- [x] New Feature (non-breaking change which adds RTL detection and
dynamic direction handling)
---------
Co-authored-by: 6ba3i <isbaaoui09@gmail.com>
Co-authored-by: Ahmad Intisar <ahmadintisar@Ahmads-MacBook-M4-Pro.local>
Co-authored-by: Ahmad Intisar <168020872+ahmadintisar@users.noreply.github.com>
Co-authored-by: Liu An <asiro@qq.com>
2026-03-02 08:03:44 +03:00
sections = _normalize_section_text_for_rtl_presentation_forms ( sections )
2024-11-22 11:05:06 +08:00
callback ( 0.8 , " Finish parsing. " )
else :
2025-12-30 15:04:09 +08:00
error_msg = f " tika.parser got empty content from { filename } . "
callback ( 0.8 , error_msg )
logging . warning ( error_msg )
2024-11-22 11:05:06 +08:00
return [ ]
2024-08-15 09:17:36 +08:00
else :
2026-01-09 17:48:45 +08:00
raise NotImplementedError ( " file type not supported yet(pdf, xlsx, doc, docx, txt supported) " )
2024-08-15 09:17:36 +08:00
st = timer ( )
2026-01-27 12:43:01 +08:00
overlapped_percent = normalize_overlapped_percent ( parser_config . get ( " overlapped_percent " , 0 ) )
2025-11-28 19:25:32 +08:00
if is_markdown :
2025-11-25 19:54:06 +08:00
merged_chunks = [ ]
merged_images = [ ]
chunk_limit = max ( 0 , int ( parser_config . get ( " chunk_token_num " , 128 ) ) )
current_text = " "
current_tokens = 0
current_image = None
for idx , sec in enumerate ( sections ) :
text = sec [ 0 ] if isinstance ( sec , tuple ) else sec
sec_tokens = num_tokens_from_string ( text )
sec_image = section_images [ idx ] if section_images and idx < len ( section_images ) else None
if current_text and current_tokens + sec_tokens > chunk_limit :
merged_chunks . append ( current_text )
merged_images . append ( current_image )
overlap_part = " "
if overlapped_percent > 0 :
overlap_len = int ( len ( current_text ) * overlapped_percent / 100 )
if overlap_len > 0 :
overlap_part = current_text [ - overlap_len : ]
current_text = overlap_part
current_tokens = num_tokens_from_string ( current_text )
current_image = current_image if overlap_part else None
if current_text :
current_text + = " \n " + text
else :
current_text = text
current_tokens + = sec_tokens
2025-07-15 13:03:01 +08:00
2025-11-25 19:54:06 +08:00
if sec_image :
current_image = concat_img ( current_image , sec_image ) if current_image else sec_image
if current_text :
merged_chunks . append ( current_text )
merged_images . append ( current_image )
chunks = merged_chunks
has_images = merged_images and any ( img is not None for img in merged_images )
2025-11-28 19:25:32 +08:00
2025-11-25 19:54:06 +08:00
if has_images :
2026-01-09 17:48:45 +08:00
res . extend ( tokenize_chunks_with_images ( chunks , doc , is_english , merged_images , child_delimiters_pattern = child_deli ) )
2025-11-25 19:54:06 +08:00
else :
2025-11-28 19:25:32 +08:00
res . extend ( tokenize_chunks ( chunks , doc , is_english , pdf_parser , child_delimiters_pattern = child_deli ) )
2025-11-25 19:54:06 +08:00
else :
if section_images :
if all ( image is None for image in section_images ) :
section_images = None
if section_images :
2026-01-27 12:43:01 +08:00
chunks , images = naive_merge_with_images ( sections , section_images , int ( parser_config . get ( " chunk_token_num " , 128 ) ) , parser_config . get ( " delimiter " , " \n !?。;!? " ) , overlapped_percent )
2026-01-09 17:48:45 +08:00
res . extend ( tokenize_chunks_with_images ( chunks , doc , is_english , images , child_delimiters_pattern = child_deli ) )
2025-11-25 19:54:06 +08:00
else :
2026-01-27 12:43:01 +08:00
chunks = naive_merge ( sections , int ( parser_config . get ( " chunk_token_num " , 128 ) ) , parser_config . get ( " delimiter " , " \n !?。;!? " ) , overlapped_percent )
2025-04-25 18:35:28 +08:00
2025-11-28 19:25:32 +08:00
res . extend ( tokenize_chunks ( chunks , doc , is_english , pdf_parser , child_delimiters_pattern = child_deli ) )
2025-07-15 13:03:01 +08:00
2025-11-03 09:34:12 +08:00
if urls and parser_config . get ( " analyze_hyperlink " , False ) and is_root :
for index , url in enumerate ( urls ) :
html_bytes , metadata = extract_html ( url )
if not html_bytes :
continue
try :
sub_url_res = chunk ( url , html_bytes , callback = callback , lang = lang , is_root = False , * * kwargs )
except Exception as e :
logging . info ( f " Failed to chunk url in registered file type { url } : { e } " )
sub_url_res = chunk ( f " { index } .html " , html_bytes , callback = callback , lang = lang , is_root = False , * * kwargs )
url_res . extend ( sub_url_res )
2025-11-25 19:54:06 +08:00
2024-11-14 17:13:48 +08:00
logging . info ( " naive_merge( {} ): {} " . format ( filename , timer ( ) - st ) )
2025-11-25 19:54:06 +08:00
2025-10-17 18:46:47 +08:00
if embed_res :
res . extend ( embed_res )
2025-11-03 09:34:12 +08:00
if url_res :
res . extend ( url_res )
2026-01-07 15:08:17 +08:00
# if table_context_size or image_context_size:
2025-12-30 20:24:27 +08:00
# attach_media_context(res, table_context_size, image_context_size)
2024-08-15 09:17:36 +08:00
return res
if __name__ == " __main__ " :
import sys
def dummy ( prog = None , msg = " " ) :
pass
chunk ( sys . argv [ 1 ] , from_page = 0 , to_page = 10 , callback = dummy )