| """ |
| ๋ฌธ์ ์ฒ๋ฆฌ ์ ํธ๋ฆฌํฐ ๋ชจ๋ |
| """ |
|
|
| import os |
| import re |
| import logging |
| from typing import List, Dict, Any, Optional, Tuple, Union |
| import numpy as np |
|
|
| logger = logging.getLogger("DocProcessor") |
| if not logger.hasHandlers(): |
| handler = logging.StreamHandler() |
| formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') |
| handler.setFormatter(formatter) |
| logger.addHandler(handler) |
| logger.setLevel(logging.INFO) |
|
|
| class DocumentProcessor: |
| """๋ฌธ์ ์ฒ๋ฆฌ ์ ํธ๋ฆฌํฐ ํด๋์ค""" |
| |
| @staticmethod |
| def split_text( |
| text: str, |
| chunk_size: int = 512, |
| chunk_overlap: int = 50, |
| separator: str = "\n" |
| ) -> List[str]: |
| """ |
| ํ
์คํธ๋ฅผ ๋ ์์ ์ฒญํฌ๋ก ๋ถํ |
| |
| Args: |
| text: ๋ถํ ํ ํ
์คํธ |
| chunk_size: ๊ฐ ์ฒญํฌ์ ์ต๋ ๋ฌธ์ ์ |
| chunk_overlap: ์ฒญํฌ ๊ฐ ์ค์ฒฉ๋๋ ๋ฌธ์ ์ |
| separator: ๋ถํ ์ ์ฌ์ฉํ ๊ตฌ๋ถ์ |
| |
| Returns: |
| ๋ถํ ๋ ํ
์คํธ ์ฒญํฌ ๋ชฉ๋ก |
| """ |
| if not text or chunk_size <= 0: |
| return [] |
| |
| |
| parts = text.split(separator) |
| chunks = [] |
| current_chunk = [] |
| current_size = 0 |
| |
| for part in parts: |
| part_size = len(part) |
| |
| if current_size + part_size + len(current_chunk) > chunk_size and current_chunk: |
| |
| chunks.append(separator.join(current_chunk)) |
| |
| |
| overlap_tokens = [] |
| overlap_size = 0 |
| for token in reversed(current_chunk): |
| if overlap_size + len(token) <= chunk_overlap: |
| overlap_tokens.insert(0, token) |
| overlap_size += len(token) + 1 |
| else: |
| break |
| |
| current_chunk = overlap_tokens |
| current_size = overlap_size - len(current_chunk) |
| |
| current_chunk.append(part) |
| current_size += part_size |
| |
| |
| if current_chunk: |
| chunks.append(separator.join(current_chunk)) |
| |
| return chunks |
| |
| @staticmethod |
| def clean_text(text: str, remove_urls: bool = True, remove_extra_whitespace: bool = True) -> str: |
| """ |
| ํ
์คํธ ์ ์ |
| |
| Args: |
| text: ์ ์ ํ ํ
์คํธ |
| remove_urls: URL ์ ๊ฑฐ ์ฌ๋ถ |
| remove_extra_whitespace: ์ฌ๋ถ์ ๊ณต๋ฐฑ ์ ๊ฑฐ ์ฌ๋ถ |
| |
| Returns: |
| ์ ์ ๋ ํ
์คํธ |
| """ |
| if not text: |
| return "" |
| |
| |
| if remove_urls: |
| text = re.sub(r'https?://\S+|www\.\S+', '', text) |
| |
| |
| text = re.sub(r'<.*?>', '', text) |
| |
| |
| if remove_extra_whitespace: |
| text = re.sub(r'\s+', ' ', text).strip() |
| |
| return text |
| |
| @staticmethod |
| def text_to_documents( |
| text: str, |
| metadata: Optional[Dict[str, Any]] = None, |
| chunk_size: int = 512, |
| chunk_overlap: int = 50 |
| ) -> List[Dict[str, Any]]: |
| """ |
| ํ
์คํธ๋ฅผ ๋ฌธ์ ๊ฐ์ฒด ๋ชฉ๋ก์ผ๋ก ๋ณํ |
| |
| Args: |
| text: ๋ณํํ ํ
์คํธ |
| metadata: ๋ฌธ์์ ์ถ๊ฐํ ๋ฉํ๋ฐ์ดํฐ |
| chunk_size: ๊ฐ ์ฒญํฌ์ ์ต๋ ๋ฌธ์ ์ |
| chunk_overlap: ์ฒญํฌ ๊ฐ ์ค์ฒฉ๋๋ ๋ฌธ์ ์ |
| |
| Returns: |
| ๋ฌธ์ ๊ฐ์ฒด ๋ชฉ๋ก |
| """ |
| if not text: |
| return [] |
| |
| |
| clean = DocumentProcessor.clean_text(text) |
| |
| |
| chunks = DocumentProcessor.split_text( |
| clean, |
| chunk_size=chunk_size, |
| chunk_overlap=chunk_overlap |
| ) |
| |
| |
| documents = [] |
| for i, chunk in enumerate(chunks): |
| doc = { |
| "text": chunk, |
| "index": i, |
| "chunk_count": len(chunks) |
| } |
| |
| |
| if metadata: |
| doc.update(metadata) |
| |
| documents.append(doc) |
| |
| return documents |
| |
| @staticmethod |
| def load_documents_from_directory( |
| directory: str, |
| extensions: List[str] = [".txt", ".md", ".csv"], |
| recursive: bool = True, |
| chunk_size: int = 512, |
| chunk_overlap: int = 50 |
| ) -> List[Dict[str, Any]]: |
| """ |
| ๋๋ ํ ๋ฆฌ์์ ๋ฌธ์ ๋ก๋ ๋ฐ ์ฒ๋ฆฌ |
| |
| Args: |
| directory: ๋ก๋ํ ๋๋ ํ ๋ฆฌ ๊ฒฝ๋ก |
| extensions: ์ฒ๋ฆฌํ ํ์ผ ํ์ฅ์ ๋ชฉ๋ก |
| recursive: ํ์ ๋๋ ํ ๋ฆฌ ๊ฒ์ ์ฌ๋ถ |
| chunk_size: ๊ฐ ์ฒญํฌ์ ์ต๋ ๋ฌธ์ ์ |
| chunk_overlap: ์ฒญํฌ ๊ฐ ์ค์ฒฉ๋๋ ๋ฌธ์ ์ |
| |
| Returns: |
| ๋ฌธ์ ๊ฐ์ฒด ๋ชฉ๋ก |
| """ |
| if not os.path.isdir(directory): |
| logger.error(f"๋๋ ํ ๋ฆฌ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค: {directory}") |
| return [] |
| |
| documents = [] |
| |
| for root, dirs, files in os.walk(directory): |
| if not recursive and root != directory: |
| continue |
| |
| for file in files: |
| _, ext = os.path.splitext(file) |
| if ext.lower() not in extensions: |
| continue |
| |
| file_path = os.path.join(root, file) |
| rel_path = os.path.relpath(file_path, directory) |
| |
| try: |
| logger.info(f"ํ์ผ ๋ก๋ ์ค: {rel_path}") |
| |
| try: |
| with open(file_path, 'r', encoding='utf-8') as f: |
| content = f.read() |
| except UnicodeDecodeError: |
| |
| logger.info(f"UTF-8 ๋์ฝ๋ฉ ์คํจ, CP949๋ก ์๋: {rel_path}") |
| with open(file_path, 'r', encoding='cp949') as f: |
| content = f.read() |
| |
| |
| metadata = { |
| "source": rel_path, |
| "filename": file, |
| "filetype": ext.lower()[1:], |
| "filepath": file_path |
| } |
| |
| |
| file_docs = DocumentProcessor.text_to_documents( |
| content, |
| metadata=metadata, |
| chunk_size=chunk_size, |
| chunk_overlap=chunk_overlap |
| ) |
| |
| documents.extend(file_docs) |
| logger.info(f"{len(file_docs)}๊ฐ ์ฒญํฌ ์ถ์ถ: {rel_path}") |
| |
| except Exception as e: |
| logger.error(f"ํ์ผ '{rel_path}' ์ฒ๋ฆฌ ์ค ์ค๋ฅ ๋ฐ์: {e}") |
| continue |
| |
| logger.info(f"์ด {len(documents)}๊ฐ ๋ฌธ์ ์ฒญํฌ๋ฅผ ๋ก๋ํ์ต๋๋ค.") |
| return documents |
| |
| @staticmethod |
| def prepare_rag_context(results: List[Dict[str, Any]], field: str = "text") -> List[str]: |
| """ |
| ๊ฒ์ ๊ฒฐ๊ณผ์์ RAG์ ์ฌ์ฉํ ์ปจํ
์คํธ ์ถ์ถ |
| |
| Args: |
| results: ๊ฒ์ ๊ฒฐ๊ณผ ๋ชฉ๋ก |
| field: ํ
์คํธ ๋ด์ฉ์ด ์๋ ํ๋ ์ด๋ฆ |
| |
| Returns: |
| ์ปจํ
์คํธ ํ
์คํธ ๋ชฉ๋ก |
| """ |
| context = [] |
| |
| for result in results: |
| if field in result: |
| context.append(result[field]) |
| |
| return context |
|
|