H022329's picture
Upload folder using huggingface_hub
6b62834 verified
Raw
History Blame Contribute Delete
5 kB
"""Text modal processor — chunking, entity extraction, and annotation."""
from agentic_rag.services.knowledge.content_list import ContentItem, ContentType
from agentic_rag.services.knowledge.processors.base import BaseModalProcessor
class TextModalProcessor(BaseModalProcessor):
"""Process text content — chunking, cleaning, entity extraction."""
content_type = ContentType.TEXT
description = "Clean and chunk text content."
def __init__(self, chunk_size: int = 512, chunk_overlap: int = 50):
self.chunk_size = chunk_size
self.chunk_overlap = chunk_overlap
async def process(self, item: ContentItem) -> ContentItem:
"""Clean and optionally chunk text content."""
if item.type != ContentType.TEXT:
return item
# Basic cleaning
text = item.text.strip()
text = self._clean_text(text)
if not text:
return item
item.text = text
return item
async def process_batch(self, items: list[ContentItem]) -> list[ContentItem]:
"""Process text items. For texts longer than chunk_size, split into chunks."""
results = []
for item in items:
processed = await self.process(item)
if processed.text and len(processed.text) > self.chunk_size:
# Split long text into overlapping chunks
chunks = self._chunk_text(processed.text)
for i, chunk in enumerate(chunks):
chunk_item = ContentItem(
type=ContentType.TEXT,
text=chunk,
page_idx=processed.page_idx,
metadata={
**processed.metadata,
"chunk_index": i,
"chunk_count": len(chunks),
"parent_item": id(processed),
},
)
results.append(chunk_item)
else:
results.append(processed)
return results
def _clean_text(self, text: str) -> str:
"""Clean up text — normalize whitespace, remove artifacts."""
import re
# Collapse multiple newlines
text = re.sub(r'\n{3,}', '\n\n', text)
# Collapse multiple spaces
text = re.sub(r' {3,}', ' ', text)
# Remove control characters except newlines
text = re.sub(r'[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]', '', text)
return text.strip()
def _chunk_text(self, text: str) -> list[str]:
"""Split text into chunks at Chinese/English sentence boundaries.
Overlap is sentence-aware — the last complete sentence(s) from the
previous chunk are prepended to the next one, so no chunk starts
with a mid-sentence fragment.
"""
import re
# Split on sentence-ending punctuation: Chinese + English
# Also split on 】 (Chinese doc section headers) and ; (Chinese semicolon)
sentence_end = r'(?<=[.!?。!?\n;;】])\s*'
raw_parts = re.split(sentence_end, text)
# Merge tiny fragments back
sentences = []
buf = ""
for part in raw_parts:
if not part.strip():
continue
if buf and len(buf) < 20: # short fragment — merge with previous
buf += part
else:
if buf.strip():
sentences.append(buf.strip())
buf = part
if buf.strip():
sentences.append(buf.strip())
if not sentences:
return [text]
chunks = []
current_sentences = [] # track sentence indices in current chunk
current_len = 0
for i, sent in enumerate(sentences):
if current_len + len(sent) > self.chunk_size and current_sentences:
# Finalize current chunk
chunk_text = "".join(sentences[j] for j in current_sentences)
chunks.append(chunk_text)
# Overlap: carry over the last 1-2 complete sentences
overlap_chars = 0
overlap_sentences = []
for j in reversed(current_sentences):
s = sentences[j]
if overlap_chars + len(s) <= self.chunk_overlap:
overlap_sentences.insert(0, j)
overlap_chars += len(s)
else:
break
# Rebuild current from overlap sentences + new sentence
current_sentences = list(overlap_sentences)
current_len = sum(len(sentences[j]) for j in current_sentences)
current_sentences.append(i)
current_len += len(sent)
if current_sentences:
chunk_text = "".join(sentences[j] for j in current_sentences)
chunks.append(chunk_text)
return chunks if chunks else [text]