Metadata-Version: 2.4
Name: rag-lab-bsg
Version: 0.1.3
Summary: Bronze / Silver / Gold 텍스트 처리 실습 라이브러리
Author-email: Park Un Woo <uwpark@simplatform.com>
License: MIT
Keywords: bronze-silver-gold,langchain,rag,text-splitter
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.10
Requires-Dist: langchain-community>=0.3.0
Requires-Dist: langchain-text-splitters>=0.3.0
Requires-Dist: pypdf>=4.0.0
Provides-Extra: notebook
Requires-Dist: ipykernel>=6.0; extra == 'notebook'
Requires-Dist: jupyterlab>=4.0; extra == 'notebook'
Description-Content-Type: text/markdown

# rag_lab

Bronze / Silver / Gold 3단계로 텍스트를 처리하는 RAG 실습용 라이브러리.

## 설치

```bash
pip install rag-lab-bsg
```

설치 패키지명은 `rag-lab-bsg` 이지만, 임포트 이름은 `rag_lab` 입니다.

노트북 실습용으로 로컬에서 개발 모드로 설치하려면 프로젝트 루트(`code/`)에서 다음을 한 번만 실행합니다.

```bash
uv pip install -e ".[notebook]"
```

`-e` 로 설치하면 `rag_lab/` 코드를 고칠 때마다 다시 설치할 필요가 없습니다.

## 사용 예시

```python
from rag_lab import bronze, silver, gold

# 1) Bronze — 원본 문서를 .txt 로 추출
txt_path = bronze.to_text("data/input/manual.pdf")

# 2) Silver — 글자 수 기준 단순 분할 (.jsonl)
silver_path = silver.split(txt_path, chunk_size=300, chunk_overlap=30)

# 3) Gold — 단락/문장 경계를 살리는 재귀 분할 (.jsonl)
gold_path = gold.split(silver_path, chunk_size=300, chunk_overlap=30)
```

각 단계의 함수는

- 다음 단계 입력으로 쓸 산출물(.txt / .jsonl) 경로를 반환하고
- 호출 결과를 화면에도 짧게 출력합니다.

## 단계 설명

| 단계 | 함수 | 입력 | 출력 | 핵심 동작 |
|---|---|---|---|---|
| Bronze | `bronze.to_text(input_path)` | PDF / TXT / MD | `data/dummy/bronze.txt` | 원본 보존, 페이지 사이는 빈 줄로만 구분 |
| Silver | `silver.split(input_path)` | bronze.txt | `data/dummy/silver.jsonl` | `CharacterTextSplitter` — 의미 무시, 글자 수로 분할 |
| Gold   | `gold.split(input_path)`   | silver.jsonl | `data/dummy/gold.jsonl` | `RecursiveCharacterTextSplitter` — 단락 → 문장 → 어절 |

## 요구 사항

- Python 3.10+
- `langchain-community`, `langchain-text-splitters`, `pypdf`

---

# 실습 가이드 — Bronze → Silver → Gold

`practice.ipynb` 의 실행 흐름을 그대로 옮긴 단계별 실습 가이드입니다. `rag_lab` 라이브러리를 import 해서 세 단계를 직접 실행해 봅니다.

## 사전 준비

```python
from pathlib import Path

from rag_lab import bronze, silver, gold
```

## 1. Bronze — 원본 그대로 .txt 로 저장

`data/input/` 에 PDF 를 넣어 두면 그 PDF 를 쓰고, 없으면 아래 코드에서 샘플 텍스트 파일을 만들어 그것을 입력으로 씁니다.

```python
INPUT_DIR = Path("data/input")
pdfs = sorted(INPUT_DIR.glob("*.pdf")) if INPUT_DIR.exists() else []

if pdfs:
    input_path = pdfs[0]
else:
    input_path = Path("data/dummy/_sample.txt")
    input_path.parent.mkdir(parents=True, exist_ok=True)
    input_path.write_text(
        "임플란트 시술 매뉴얼 (샘플)\n\n"
        "1. 개요\n임플란트는 치아가 결손된 부위에 인공 치근을 식립하여 자연치와 유사한 기능을 회복하는 치료법이다. "
        "시술은 진단, 식립, 보철 단계로 구분되며 각 단계마다 정밀한 계획이 필요하다.\n\n"
        "2. 적응증\n- 단일 치아 결손\n- 부분 무치악\n- 완전 무치악\n"
        "환자의 골량과 골질, 전신 건강 상태를 종합적으로 평가한 후 시술 여부를 결정한다.\n\n"
        "3. 시술 절차\n3.1 진단 단계\n구강 검사, 파노라마 방사선 촬영, CBCT 영상을 통해 식립 위치를 결정한다. "
        "보철 계획을 먼저 수립한 뒤 역으로 식립 위치를 정하는 보철 주도형 진단이 권장된다.\n"
        "3.2 식립 단계\n국소마취 후 점막을 절개하고 드릴링하여 픽스처를 식립한다. 일차 안정성이 확보되어야 한다.\n"
        "3.3 보철 단계\n골 유착 기간(일반적으로 3~6개월) 이후 인상 채득, 지대주 연결, 최종 보철물 장착으로 마무리된다.\n",
        encoding="utf-8",
    )

bronze_txt = bronze.to_text(input_path)
bronze_txt
```

**실행 결과 예시:**

```
[Bronze] 누비슨 플랫폼 활용 부정 방지 전략.pdf → data/dummy/bronze.txt  (1,848자, PDF, 3페이지 합본)

PosixPath('data/dummy/bronze.txt')
```

생성된 텍스트는 다음과 같이 확인합니다.

```python
print(bronze_txt.read_text(encoding="utf-8"))
```

## 2. Silver — 의미와 무관하게 글자 수로 단순 분할

`chunk_size` 를 바꿔 가며 청크 수/평균 길이를 비교해 봅니다.

```python
for size in [100, 300, 800]:
    silver.split(
        bronze_txt,
        output_path=f"data/dummy/_silver_cs{size}.jsonl",
        chunk_size=size,
        chunk_overlap=30,
    )
```

**실행 결과 예시:**

```
Created a chunk of size 956, which is longer than the specified 100
Created a chunk of size 682, which is longer than the specified 100
Created a chunk of size 956, which is longer than the specified 300
Created a chunk of size 682, which is longer than the specified 300
Created a chunk of size 956, which is longer than the specified 800

[Silver] bronze.txt → data/dummy/_silver_cs100.jsonl  (3청크, 평균 609자, chunk_size=100, overlap=30)
[Silver] bronze.txt → data/dummy/_silver_cs300.jsonl  (3청크, 평균 609자, chunk_size=300, overlap=30)
[Silver] bronze.txt → data/dummy/_silver_cs800.jsonl  (3청크, 평균 609자, chunk_size=800, overlap=30)
```

> `CharacterTextSplitter` 는 우선 separator(기본 `"\n\n"`) 로 잘라 낸 뒤에야 `chunk_size` 를 적용하므로, separator 사이의 텍스트가 길면 청크가 `chunk_size` 보다 커질 수 있습니다. 위 경고 메시지가 바로 그 상황입니다.

다음 단계(Gold)에 넘길 기준 산출물을 생성합니다.

```python
silver_jsonl = silver.split(bronze_txt, chunk_size=300, chunk_overlap=30)
silver_chunks = silver.load_chunks(silver_jsonl)

for c in silver_chunks[:3]:
    print(f"#{c['chunk_id']} ({c['char_len']}자) {c['text']}...")
    print("========================================================================================")
```

**실행 결과 예시:**

```
[Silver] bronze.txt → data/dummy/silver.jsonl  (3청크, 평균 609자, chunk_size=300, overlap=30)
#0 (956자) 누비슨 플랫폼 활용한 부정 방지 전략 및 인프라 제안 ...
#1 (668자) 경주중발생하는초당수백개의센서데이터를누비슨엣지서버에서즉시처리하여 ...
#2 (204자) 베팅패턴연동      실시간배당률변화와선수주행 ...
```

## 3. Gold — Silver 결과를 받아 의미 단위로 재귀 분할

`gold.split` 은 silver.jsonl 의 청크 텍스트를 다시 단락 구분자로 이어붙인 뒤, `separators` 우선순위(단락 → 줄 → 종결 → 어절) 대로 잘라 냅니다.

```python
gold_jsonl = gold.split(silver_jsonl, chunk_size=300, chunk_overlap=30)
gold_chunks = gold.load_chunks(gold_jsonl)

for c in gold_chunks[:3]:
    print(f"#{c['chunk_id']} ({c['char_len']}자) {c['text']}...")
    print("========================================================================================")
```

**실행 결과 예시:**

```
[Gold] silver.jsonl → data/dummy/gold.jsonl  (8청크, 평균 234자, chunk_size=300, overlap=30)
#0 (281자) 누비슨 플랫폼 활용한 부정 방지 전략 및 인프라 제안 ...
#1 (287자) ●   생체데이터모니터링 :  선수의심박수, ...
#2 (273자) 2. sLLM  및RAG    기반의행태분석 (AI) ...
```

### Silver vs Gold 비교

청크 개수, 평균/최소/최대 길이를 한눈에 비교해 봅니다.

```python
def stats(chunks):
    lens = [c["char_len"] for c in chunks] or [0]
    return {
        "개수": len(chunks),
        "평균": round(sum(lens) / len(lens), 1),
        "최소": min(lens),
        "최대": max(lens),
    }

print("Silver:", stats(silver_chunks))
print("Gold  :", stats(gold_chunks))

print("\n— Silver #0 ——————————————")
print(silver_chunks[0]["text"])
print("\n— Gold #0 ————————————————")
print(gold_chunks[0]["text"])
```

**실행 결과 예시:**

```
Silver: {'개수': 3, '평균': 609.3, '최소': 204, '최대': 956}
Gold  : {'개수': 8, '평균': 233.6, '최소': 129, '최대': 294}
```

같은 `chunk_size=300` 을 줘도 Silver 는 separator 단위에 막혀 큰 덩어리(평균 609자)로 남는 반면, Gold 는 재귀적으로 더 작은 의미 단위까지 내려가서 청크 크기가 평균 234자로 안정적으로 분포합니다.

## 4. 직접 바꿔 보기

다음을 바꿔 가며 결과 차이를 관찰해 봅니다.

- Silver `separator` 를 `"\n"` / `""` 로 바꾸면 한국어 문장 중간이 잘리는 빈도가 어떻게 변하는지
- Gold `separators` 리스트에서 `"다. "` 를 맨 앞에 두면 종결 경계가 얼마나 잘 살아나는지
- `chunk_size=100` 처럼 작게 두면 Silver vs Gold 차이가 더 두드러집니다

`separators` 순서를 바꿔서 Gold 결과를 비교하는 예시입니다.

```python
gold.split(
    silver_jsonl,
    output_path="data/dummy/_gold_custom.jsonl",
    chunk_size=300,
    chunk_overlap=30,
    separators=["다. ", "\n\n", "\n", ". ", " ", ""],
)
```

**실행 결과 예시:**

```
[Gold] silver.jsonl → data/dummy/_gold_custom.jsonl  (9청크, 평균 207자, chunk_size=300, overlap=30)

PosixPath('data/dummy/_gold_custom.jsonl')
```
