From 5e26277abad0063064534b28068333acf88d8ede Mon Sep 17 00:00:00 2001 From: seyeong Date: Sun, 1 Mar 2026 15:40:36 +0900 Subject: [PATCH 1/3] docs: remove RunContext_ko.md MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RunContext는 공개 API(__init__.py)에 포함되지 않으며 v2 컴포넌트/플로우에서 사용되지 않음. 문서 존재 시 사용자가 공개 API로 오인할 수 있어 삭제. --- docs/RunContext_ko.md | 132 ------------------------------------------ 1 file changed, 132 deletions(-) delete mode 100644 docs/RunContext_ko.md diff --git a/docs/RunContext_ko.md b/docs/RunContext_ko.md deleted file mode 100644 index 1b57472..0000000 --- a/docs/RunContext_ko.md +++ /dev/null @@ -1,132 +0,0 @@ -> **레거시 유틸리티**: `RunContext`는 현재 레거시 유틸리티로 유지됩니다. -> 새 코드에서는 명시적 Python 인자를 사용하는 것을 권장합니다. -> 컴포넌트 I/O는 `RunContext` 대신 구체적인 타입(str, list 등)으로 표현하세요. - -## RunContext - -`RunContext`는 define-by-run 파이프라인에서 **상태(state)를 운반하는 State Carrier**입니다. -레거시 파이프라인이나 직접 상태를 조합할 때 유용합니다. - -### 설계 원칙 - -* **최소 루트 필드 5개만 고정**: `inputs / artifacts / outputs / error / metadata` -* 루트는 전부 `dict` 기반(스키마 락인 방지) -* 자주 쓰는 값은 **alias 프로퍼티**로 제공하여 UX 개선 (`run.query`, `run.sql` 등) - ---- - -## 데이터 구조 트리 - -아래는 `RunContext`가 담는 데이터 구조를 "트리 형태"로 나타낸 것입니다. - -``` -RunContext -├─ inputs: dict -│ └─ "query": str -│ -├─ artifacts: dict -│ └─ "schema": dict -│ ├─ "catalog": Any -│ │ (예: list[TableSchema] | provider | None) -│ ├─ "selected": Any -│ │ (예: list[TableCandidate] | None) -│ └─ "context": str -│ (prompt에 넣을 스키마 컨텍스트) -│ -├─ outputs: dict -│ ├─ "sql": str -│ └─ "validation": Any -│ -├─ error: dict | None -│ └─ (구조화된 에러 정보. 형식은 프로젝트 정책에 따라 확장 가능) -│ -└─ metadata: dict - └─ (로그/추적/히스토리/실험용 값. 표준 스키마 강제 없음) - 예) - ├─ "events": list[Event] - ├─ "sql_drafts": list[str] - ├─ "attempt": int - └─ ... -``` - ---- - -## Root fields (고정 5개) - -* `inputs: dict[str, Any]` — 사용자 입력 -* `artifacts: dict[str, Any]` — 중간 산출물 -* `outputs: dict[str, Any]` — 최종 산출물 -* `error: Optional[dict[str, Any]]` — 구조화된 에러(선택) -* `metadata: dict[str, Any]` — 로그/추적/히스토리(선택) - ---- - -## 권장 키 컨벤션 (Minimal Standard) - -### inputs - -* `inputs["query"]`: 자연어 질의 - -### artifacts["schema"] - -* `catalog`: 스키마 카탈로그(테이블/컬럼 목록 등) -* `selected`: 선택된 테이블 후보 -* `context`: 프롬프트에 들어갈 스키마 컨텍스트 문자열 - -### outputs - -* `outputs["sql"]`: 최종 SQL -* `outputs["validation"]`: 검증 결과(구조는 구현체 자유) - ---- - -## Alias (Beginner-friendly API) - -키 문자열 접근을 줄이기 위해 alias를 제공합니다. - -* `run.query` ↔ `inputs["query"]` -* `run.sql` ↔ `outputs["sql"]` -* `run.validation` ↔ `outputs["validation"]` - -스키마 관련 alias: - -* `run.schema` ↔ `artifacts["schema"]` *(항상 dict로 보정)* -* `run.schema_catalog` ↔ `run.schema["catalog"]` -* `run.schema_selected` ↔ `run.schema["selected"]` -* `run.schema_context` ↔ `run.schema["context"]` - ---- - -## 파이프라인 예시 (Text2SQL) — 새 API - -새 API에서는 각 컴포넌트가 명시적 인자를 주고받습니다. - -```python -query = "지난달 매출" - -schemas = retriever(query) # str → list[CatalogEntry] -context = builder(query, schemas) # str, list → str -sql = generator(query, context) # str, str → str -result = validator(sql) # str → ValidationResult -``` - -또는 `SequentialFlow`로 조합: - -```python -flow = SequentialFlow(steps=[retriever, builder, generator, validator]) -result = flow.run(query) -``` - ---- - -## RunContext 직접 사용 (레거시) - -기존 코드나 직접 상태를 조합할 때만 사용합니다. - -```python -from lang2sql.core.context import RunContext - -run = RunContext(query="지난달 매출") -# run을 직접 조작하거나 레거시 컴포넌트에 전달 -run.metadata["session_id"] = "abc123" -``` From b620fd4ecce474004d9335a46873e243a517db82 Mon Sep 17 00:00:00 2001 From: seyeong Date: Sun, 1 Mar 2026 15:40:48 +0900 Subject: [PATCH 2/3] docs(BaseFlow): add SequentialFlow known limitations section MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 다중 인자 컴포넌트 비호환, 컨텍스트 소실 문제와 전용 Flow(BaselineNL2SQL 등) 사용을 권장하는 해결 방법 추가 --- docs/BaseFlow_ko.md | 52 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/docs/BaseFlow_ko.md b/docs/BaseFlow_ko.md index f48dcfe..5e2f0e0 100644 --- a/docs/BaseFlow_ko.md +++ b/docs/BaseFlow_ko.md @@ -129,6 +129,58 @@ for e in hook.events: --- +## SequentialFlow의 알려진 제한 + +`SequentialFlow`는 `value = step(value)` 단일 값 전달 방식으로 동작합니다. +이 설계는 단순한 변환 체인에는 적합하지만, NL2SQL 파이프라인에서 다음 한계가 있습니다. + +### 문제 1: 컨텍스트 소실 + +파이프라인이 진행되면서 초기 입력(`query`)이 중간 단계 출력으로 대체되어 사라집니다. + +```python +flow.run("주문 내역 확인") +↓ +retriever("주문 내역 확인") → list[CatalogEntry] +↓ +generator(list[CatalogEntry]) # ← 여기서 original query가 없음 +↓ +TypeError 또는 잘못된 결과 +``` + +### 문제 2: 다중 인자 컴포넌트와 호환 불가 + +`SQLGenerator.run(query, schemas)`처럼 2개 이상의 인자를 받는 컴포넌트는 +`SequentialFlow`의 단일 값 전달로 연결할 수 없습니다. + +```python +# ❌ 동작하지 않음 — generator는 (query, schemas) 2개 인자가 필요 +flow = SequentialFlow(steps=[retriever, generator, executor]) +flow.run("주문 내역") # TypeError: run() missing 1 required positional argument: 'schemas' +``` + +### 해결 방법 + +NL2SQL 파이프라인은 `SequentialFlow` 대신 **전용 Flow**를 사용하세요. +전용 Flow는 내부에서 다중 인자 와이어링을 올바르게 처리합니다. + +```python +# ✅ KeywordRetriever 기반 +pipeline = BaselineNL2SQL(catalog=catalog, llm=llm, db=db) + +# ✅ Keyword + Vector 기반 +pipeline = HybridNL2SQL(catalog=catalog, llm=llm, db=db, embedding=embedding) + +# ✅ Gate + 프로파일링 + 보강 포함 풀 파이프라인 +pipeline = EnrichedNL2SQL(catalog=catalog, llm=llm, db=db, embedding=embedding) + +rows = pipeline.run("주문 내역") +``` + +`SequentialFlow`는 단일 값 변환 체인(예: 텍스트 전처리, 단계별 필터링)에 적합합니다. + +--- + ## FAQ ### Q. BaseFlow가 필수인가? From 8137ff0ed156d1d0e22856a63d6e22cf3c0d7e2d Mon Sep 17 00:00:00 2001 From: seyeong Date: Sun, 1 Mar 2026 15:41:03 +0900 Subject: [PATCH 3/3] docs(tutorials): update to v2 native API, remove IndexBuilder references MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - vector-retriever: IndexBuilder 제거, BaselineNL2SQL retriever= 오류 수정 (HybridNL2SQL로 교체), add() 시그니처 수정(pre-split 필수), splitter 파라미터명 반영 - v2-usage-guide: EnrichedNL2SQL 추가, embedding 6개/vectorstore 3개로 목록 현행화 - getting-started-without-datahub: langchain import 제거, FAISSVectorStore/PGVectorStore 네이티브 코드로 교체, pgvector 실행 방법 3가지(Docker, 직접 설치, 클라우드)로 확장 --- .../getting-started-without-datahub.md | 168 ++++++++------ docs/tutorials/v2-usage-guide.md | 8 +- docs/tutorials/vector-retriever.md | 214 ++++++++---------- 3 files changed, 201 insertions(+), 189 deletions(-) diff --git a/docs/tutorials/getting-started-without-datahub.md b/docs/tutorials/getting-started-without-datahub.md index b2e4e7c..0792b6a 100644 --- a/docs/tutorials/getting-started-without-datahub.md +++ b/docs/tutorials/getting-started-without-datahub.md @@ -1,6 +1,7 @@ ## DataHub 없이 시작하기 (튜토리얼) -이 문서는 DataHub 없이도 Lang2SQL을 바로 사용하기 위한 최소 절차를 설명합니다. CSV로 테이블/컬럼 설명을 준비해 FAISS 또는 pgvector에 적재한 뒤 Lang2SQL을 실행합니다. +이 문서는 DataHub 없이도 Lang2SQL을 바로 사용하기 위한 최소 절차를 설명합니다. +CSV로 테이블/컬럼 설명을 준비해 FAISS 또는 pgvector에 적재한 뒤 Lang2SQL을 실행합니다. ### 0) 준비 @@ -10,7 +11,6 @@ git clone https://github.com/CausalInferenceLab/lang2sql.git cd lang2sql # (권장) uv 사용 -# uv 설치가 되어 있다면 아래 두 줄로 개발 모드 설치 uv venv --python 3.11 source .venv/bin/activate uv pip install -e . @@ -31,15 +31,6 @@ OPEN_AI_LLM_MODEL=gpt-4o # 또는 gpt-4.1 등 EMBEDDING_PROVIDER=openai OPEN_AI_EMBEDDING_MODEL=text-embedding-3-large # 권장 -# VectorDB (선택: 명시하지 않으면 기본값 동작) -VECTORDB_TYPE=faiss -VECTORDB_LOCATION=dev/table_info_db # FAISS 디렉토리 경로 - -# (pgvector를 쓰는 경우) -# VECTORDB_TYPE=pgvector -# VECTORDB_LOCATION=postgresql://pgvector:pgvector@localhost:5432/postgres -# PGVECTOR_COLLECTION=table_info_db - # DB 타입 DB_TYPE=clickhouse ``` @@ -47,7 +38,9 @@ DB_TYPE=clickhouse 중요: 코드상 OpenAI 키는 `OPEN_AI_KEY` 환경변수를 사용합니다. `.example.env`의 `OPENAI_API_KEY`는 사용되지 않으니 혼동에 주의하세요. ### 2) 테이블/컬럼 메타데이터 준비 (CSV 예시) -- dev/table_catalog.csv 파일을 생성합니다. + +`dev/table_catalog.csv` 파일을 생성합니다. + ```csv table_name,table_description,column_name,column_description customers,고객 정보 테이블,customer_id,고객 고유 ID @@ -60,21 +53,22 @@ orders,주문 정보 테이블,status,주문 상태 ``` ### 3) FAISS 인덱스 생성 (로컬) -- dev/create_faiss.py 파일을 실행합니다. -- `python dev/create_faiss.py` + +`dev/create_faiss.py` 파일을 실행합니다: `python dev/create_faiss.py` + ```python """ dev/create_faiss.py CSV 파일에서 테이블과 컬럼 정보를 불러와 OpenAI 임베딩으로 벡터화한 뒤, -FAISS 인덱스를 생성하고 로컬 디렉토리에 저장한다. +FAISSVectorStore 인덱스를 생성하고 로컬 디렉토리에 저장한다. 환경 변수: OPEN_AI_KEY: OpenAI API 키 OPEN_AI_EMBEDDING_MODEL: 사용할 임베딩 모델 이름 출력: - 지정된 OUTPUT_DIR 경로에 FAISS 인덱스 저장 + OUTPUT_DIR 경로에 FAISS 인덱스 저장 (catalog.faiss) """ import csv @@ -82,16 +76,19 @@ import os from collections import defaultdict from dotenv import load_dotenv -from langchain_community.vectorstores import FAISS -from langchain_openai import OpenAIEmbeddings +from lang2sql import CatalogChunker, VectorRetriever +from lang2sql.integrations.embedding import OpenAIEmbedding +from lang2sql.integrations.vectorstore import FAISSVectorStore load_dotenv() + # CSV 파일 경로 CSV_PATH = "./dev/table_catalog.csv" # .env의 VECTORDB_LOCATION과 동일하게 맞추세요 OUTPUT_DIR = "./dev/table_info_db" -tables = defaultdict(lambda: {"desc": "", "columns": []}) +# CSV → CatalogEntry 변환 +tables: dict = defaultdict(lambda: {"desc": "", "columns": {}}) with open(CSV_PATH, newline="", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: @@ -99,94 +96,137 @@ with open(CSV_PATH, newline="", encoding="utf-8") as f: tables[t]["desc"] = row["table_description"].strip() col = row["column_name"].strip() col_desc = row["column_description"].strip() - tables[t]["columns"].append((col, col_desc)) + tables[t]["columns"][col] = col_desc -docs = [] -for t, info in tables.items(): - cols = "\n".join([f"{c}: {d}" for c, d in info["columns"]]) - page = f"{t}: {info['desc']}\nColumns:\n {cols}" - from langchain.schema import Document +catalog = [ + {"name": t, "description": info["desc"], "columns": info["columns"]} + for t, info in tables.items() +] - docs.append(Document(page_content=page)) +# 청킹 → 임베딩 → 저장 +chunks = CatalogChunker().split(catalog) +store = FAISSVectorStore(index_path=f"{OUTPUT_DIR}/catalog.faiss") +os.makedirs(OUTPUT_DIR, exist_ok=True) -emb = OpenAIEmbeddings( - model=os.getenv("OPEN_AI_EMBEDDING_MODEL"), openai_api_key=os.getenv("OPEN_AI_KEY") +VectorRetriever.from_chunks( + chunks, + embedding=OpenAIEmbedding( + model=os.getenv("OPEN_AI_EMBEDDING_MODEL", "text-embedding-3-large"), + api_key=os.getenv("OPEN_AI_KEY"), + ), + vectorstore=store, ) -db = FAISS.from_documents(docs, emb) -os.makedirs(OUTPUT_DIR, exist_ok=True) -db.save_local(OUTPUT_DIR) -print(f"FAISS index saved to: {OUTPUT_DIR}") +store.save() +print(f"FAISS index saved to: {OUTPUT_DIR}/catalog.faiss") ``` ### 4) 실행 ```bash # Streamlit UI -lang2sql --vectordb-type faiss --vectordb-location ./dev/table_info_db run-streamlit +lang2sql run-streamlit -# CLI 예시 -lang2sql query "주문 수를 집계하는 SQL을 만들어줘" --vectordb-type faiss --vectordb-location ./dev/table_info_db +# CLI 예시 (FAISS 인덱스 사용) +lang2sql query "주문 수를 집계하는 SQL을 만들어줘" \ + --vectordb-type faiss \ + --vectordb-location ./dev/table_info_db # CLI 예시 (pgvector) -lang2sql query "주문 수를 집계하는 SQL을 만들어줘" --vectordb-type pgvector --vectordb-location "postgresql://pgvector:pgvector@localhost:5432/postgres" +lang2sql query "주문 수를 집계하는 SQL을 만들어줘" \ + --vectordb-type pgvector \ + --vectordb-location "postgresql://pgvector:pgvector@localhost:5432/postgres" ``` ### 5) (선택) pgvector로 적재하기 -- dev/create_pgvector.py 파일을 실행합니다. -- `python dev/create_pgvector.py` + +`dev/create_pgvector.py` 파일을 실행합니다: `python dev/create_pgvector.py` + +pgvector를 사용하려면 PostgreSQL에 pgvector 확장이 설치되어 있어야 합니다. +아래 중 하나를 선택하세요: + +**방법 A — Docker (로컬 테스트용, 가장 빠름)** + +```bash +docker run -d \ + -e POSTGRES_USER=pgvector \ + -e POSTGRES_PASSWORD=pgvector \ + -e POSTGRES_DB=postgres \ + -p 5432:5432 \ + pgvector/pgvector:pg16 +``` + +**방법 B — 기존 PostgreSQL 서버에 확장 설치** + +```sql +-- psql 또는 DBeaver 등에서 실행 +CREATE EXTENSION IF NOT EXISTS vector; +``` + +**방법 C — 클라우드 관리형 서비스 (별도 설치 불필요)** + +- [Supabase](https://supabase.com/) — 무료 플랜에서 pgvector 기본 지원 +- AWS RDS PostgreSQL 15+ — 파라미터 그룹에서 `pgvector` 활성화 +- Azure Database for PostgreSQL Flexible Server — 확장 목록에서 활성화 ```python """ dev/create_pgvector.py CSV 파일에서 테이블과 컬럼 정보를 불러와 OpenAI 임베딩으로 벡터화한 뒤, -pgvector에 적재한다. +pgvector에 적재한다. ON CONFLICT upsert를 지원하므로 재실행 시 중복 없음. 환경 변수: OPEN_AI_KEY: OpenAI API 키 OPEN_AI_EMBEDDING_MODEL: 사용할 임베딩 모델 이름 VECTORDB_LOCATION: pgvector 연결 문자열 - PGVECTOR_COLLECTION: pgvector 컬렉션 이름 + PGVECTOR_COLLECTION: pgvector 테이블 이름 """ import csv import os from collections import defaultdict -from langchain.schema import Document -from langchain_openai import OpenAIEmbeddings -from langchain_postgres.vectorstores import PGVector +from dotenv import load_dotenv +from lang2sql import CatalogChunker, VectorRetriever +from lang2sql.integrations.embedding import OpenAIEmbedding +from lang2sql.integrations.vectorstore import PGVectorStore + +load_dotenv() # CSV 파일 경로 CSV_PATH = "./dev/table_catalog.csv" -# .env의 VECTORDB_LOCATION과 동일하게 맞추세요 -CONN = ( - os.getenv("VECTORDB_LOCATION") or "postgresql://pgvector:pgvector@localhost:5432/postgres" -) -COLLECTION = os.getenv("PGVECTOR_COLLECTION", "table_info_db") +CONN = os.getenv("VECTORDB_LOCATION", "postgresql://pgvector:pgvector@localhost:5432/postgres") +TABLE = os.getenv("PGVECTOR_COLLECTION", "table_info_db") -tables = defaultdict(lambda: {"desc": "", "columns": []}) +# CSV → CatalogEntry 변환 +tables: dict = defaultdict(lambda: {"desc": "", "columns": {}}) with open(CSV_PATH, newline="", encoding="utf-8") as f: reader = csv.DictReader(f) for row in reader: t = row["table_name"].strip() tables[t]["desc"] = row["table_description"].strip() col = row["column_name"].strip() - col_desc = row["column_description"] - tables[t]["columns"].append((col, col_desc)) - -docs = [] -for t, info in tables.items(): - cols = "\n".join([f"{c}: {d}" for c, d in info["columns"]]) - docs.append(Document(page_content=f"{t}: {info['desc']}\nColumns:\n {cols}")) - -emb = OpenAIEmbeddings( - model=os.getenv("OPEN_AI_EMBEDDING_MODEL"), openai_api_key=os.getenv("OPEN_AI_KEY") -) -PGVector.from_documents( - documents=docs, embedding=emb, connection=CONN, collection_name=COLLECTION + col_desc = row["column_description"].strip() + tables[t]["columns"][col] = col_desc + +catalog = [ + {"name": t, "description": info["desc"], "columns": info["columns"]} + for t, info in tables.items() +] + +# 청킹 → 임베딩 → pgvector 적재 +chunks = CatalogChunker().split(catalog) +store = PGVectorStore(connection=CONN, table_name=TABLE) + +VectorRetriever.from_chunks( + chunks, + embedding=OpenAIEmbedding( + model=os.getenv("OPEN_AI_EMBEDDING_MODEL", "text-embedding-3-large"), + api_key=os.getenv("OPEN_AI_KEY"), + ), + vectorstore=store, ) -print(f"pgvector collection populated: {COLLECTION}") +print(f"pgvector collection populated: {TABLE}") ``` -주의: FAISS 디렉토리가 없으면 현재 코드는 DataHub에서 메타데이터를 가져와 인덱스를 생성하려고 시도합니다. DataHub를 사용하지 않는 경우 위 절차로 사전에 VectorDB를 만들어 두세요. +주의: FAISS 디렉토리 또는 pgvector 컬렉션이 없으면 현재 코드는 DataHub에서 메타데이터를 가져와 인덱스를 생성하려고 시도합니다. DataHub를 사용하지 않는 경우 위 절차로 사전에 VectorDB를 만들어 두세요. diff --git a/docs/tutorials/v2-usage-guide.md b/docs/tutorials/v2-usage-guide.md index 341041a..99885bd 100644 --- a/docs/tutorials/v2-usage-guide.md +++ b/docs/tutorials/v2-usage-guide.md @@ -26,6 +26,7 @@ python scripts/setup_sample_docs.py ### Flows - `BaselineNL2SQL`: BM25 `KeywordRetriever` 기반 기본 파이프라인 - `HybridNL2SQL`: BM25 + Vector `HybridRetriever` 기반 파이프라인 +- `EnrichedNL2SQL`: Gate + 프로파일링 + 보강 + HybridRetriever 기반 풀 파이프라인 ### Retrievers - `KeywordRetriever` @@ -33,8 +34,8 @@ python scripts/setup_sample_docs.py - `HybridRetriever` ### Vector / Embedding (v2 내장) -- Embedding: `OpenAIEmbedding` (내장 1개) -- Vector store: `InMemoryVectorStore` (내장 1개) +- Embedding: `OpenAIEmbedding`, `AzureOpenAIEmbedding`, `GeminiEmbedding`, `BedrockEmbedding`, `OllamaEmbedding`, `HuggingFaceEmbedding` (6개) +- Vector store: `InMemoryVectorStore`, `FAISSVectorStore`, `PGVectorStore` (3개) ### Chunking / Loading - Chunkers: `CatalogChunker`, `RecursiveCharacterChunker`, `SemanticChunker` @@ -240,8 +241,7 @@ PDF는 페이지 단위로 `TextDocument`를 생성합니다: ## 4) 중요한 현재 제약 -- v2 내장 VectorStore는 현재 `InMemoryVectorStore`만 공식 제공됩니다. -- `BaselineNL2SQL`은 현재 `retriever` 주입 파라미터를 받지 않습니다. +- `BaselineNL2SQL`은 `retriever` 주입 파라미터를 받지 않습니다. - 벡터 기반 파이프라인은 `HybridNL2SQL` 또는 수동 조합을 사용하세요. - `VectorRetriever` 결과의 `context`는 현재 `list[str]`입니다. - 문서 출처 구조화가 필요하면 `metadata`를 별도 조회하거나 커스텀 래퍼를 두세요. diff --git a/docs/tutorials/vector-retriever.md b/docs/tutorials/vector-retriever.md index bef59ac..1d45b26 100644 --- a/docs/tutorials/vector-retriever.md +++ b/docs/tutorials/vector-retriever.md @@ -11,9 +11,9 @@ 2. [설치 — 임베딩 패키지 추가하기](#2-설치--임베딩-패키지-추가하기) 3. [가장 빠른 시작 — from_sources()](#3-가장-빠른-시작--from_sources) 4. [비즈니스 문서를 컨텍스트로 추가하기](#4-비즈니스-문서를-컨텍스트로-추가하기) -5. [파이프라인에 연결하기 — BaselineNL2SQL](#5-파이프라인에-연결하기--baselinenl2sql) +5. [파이프라인에 연결하기](#5-파이프라인에-연결하기) 6. [인덱스 점진적으로 추가하기 — add()](#6-인덱스-점진적으로-추가하기--add) -7. [고급 — IndexBuilder 직접 사용하기](#7-고급--indexbuilder-직접-사용하기) +7. [고급 — 명시적 파이프라인 (from_chunks)](#7-고급--명시적-파이프라인-from_chunks) 8. [고급 — 청커 교체하기](#8-고급--청커-교체하기) 9. [점수 임계값과 top_n 조정](#9-점수-임계값과-top_n-조정) 10. [전체 체크리스트 — API 키 없이 실행](#10-전체-체크리스트--api-키-없이-실행) @@ -93,7 +93,7 @@ print(result.context) `from_sources()`는 내부적으로 다음을 자동으로 처리합니다: - `InMemoryVectorStore` 생성 (외부 DB 불필요) -- `IndexBuilder`로 카탈로그 청킹 → 임베딩 → 저장 +- 카탈로그 청킹 → 임베딩 → 저장 (`from_chunks()` 내부 호출) - 검색 준비 완료된 `VectorRetriever` 반환 --- @@ -140,39 +140,49 @@ print(result.context) # 관련 문서 텍스트 — LLM 프롬프트에 포함 --- -## 5. 파이프라인에 연결하기 — BaselineNL2SQL +## 5. 파이프라인에 연결하기 -`BaselineNL2SQL`의 `retriever=` 파라미터로 `VectorRetriever`를 주입합니다. -기본 `KeywordRetriever`를 대체합니다. +벡터 기반 검색을 사용하려면 `HybridNL2SQL`을 사용합니다. +(`BaselineNL2SQL`은 `KeywordRetriever`만 내부적으로 사용하며, retriever 주입 파라미터를 받지 않습니다.) ```python -from lang2sql import BaselineNL2SQL, VectorRetriever +from lang2sql import HybridNL2SQL from lang2sql.integrations.llm import AnthropicLLM from lang2sql.integrations.db import SQLAlchemyDB from lang2sql.integrations.embedding import OpenAIEmbedding -# 1. VectorRetriever 준비 -retriever = VectorRetriever.from_sources( +pipeline = HybridNL2SQL( catalog=CATALOG, - documents=DOCS, - embedding=OpenAIEmbedding(), -) - -# 2. 파이프라인에 주입 -pipeline = BaselineNL2SQL( - catalog=CATALOG, # KeywordRetriever 기본값용 (retriever 주입 시 무시됨) llm=AnthropicLLM(model="claude-sonnet-4-6"), db=SQLAlchemyDB("sqlite:///sample.db"), + embedding=OpenAIEmbedding(), + documents=DOCS, db_dialect="sqlite", - retriever=retriever, # ← VectorRetriever 주입 ) -result = pipeline.run("취소 제외한 이번 달 매출 합계") -print(result) +rows = pipeline.run("취소 제외한 이번 달 매출 합계") +print(rows) ``` -> `retriever=`가 주어지면 `catalog=`는 내부적으로 사용되지 않습니다. -> 하지만 API 일관성을 위해 `catalog=`를 함께 전달하는 것을 권장합니다. +또는 `VectorRetriever`를 직접 조합해 수동 파이프라인을 구성할 수 있습니다: + +```python +from lang2sql import VectorRetriever, SQLGenerator, SQLExecutor +from lang2sql.integrations.embedding import OpenAIEmbedding +from lang2sql.integrations.llm import AnthropicLLM +from lang2sql.integrations.db import SQLAlchemyDB + +retriever = VectorRetriever.from_sources( + catalog=CATALOG, documents=DOCS, embedding=OpenAIEmbedding(), +) +generator = SQLGenerator(llm=AnthropicLLM(model="claude-sonnet-4-6"), db_dialect="sqlite") +executor = SQLExecutor(db=SQLAlchemyDB("sqlite:///sample.db")) + +query = "취소 제외한 이번 달 매출 합계" +result = retriever(query) +sql = generator(query, result.schemas, context=result.context) +rows = executor(sql) +``` --- @@ -197,48 +207,53 @@ NEW_DOCS: list[TextDocument] = [ }, ] -retriever.add(NEW_DOCS) # 기존 카탈로그 인덱스 유지 + 새 문서 추가 +retriever.add(RecursiveCharacterChunker().split(NEW_DOCS)) # pre-split 후 전달 result = retriever("VIP 고객 할인 금액 계산") print(result.context) # ['할인 정책: VIP 고객(gold 등급)에게는...'] ``` -> `add()`는 `from_sources()`로 만든 retriever에서만 사용할 수 있습니다. -> 직접 생성한 경우엔 `IndexBuilder.run()`을 호출하세요 (섹션 7 참고). +> **주의**: `add()`는 `list[IndexedChunk]`만 받습니다. +> `TextDocument`를 직접 전달하면 오류가 발생합니다. +> +> ```python +> # ❌ 동작 안 함 +> retriever.add(NEW_DOCS) +> +> # ✅ 올바른 방법 +> retriever.add(RecursiveCharacterChunker().split(NEW_DOCS)) +> ``` --- -## 7. 고급 — IndexBuilder 직접 사용하기 +## 7. 고급 — 명시적 파이프라인 (from_chunks) -여러 소스를 단계별로 인덱싱하거나, 커스텀 벡터 저장소를 쓰고 싶을 때 -`IndexBuilder`를 직접 조작합니다. +영속 벡터스토어(FAISS, pgvector)를 사용하거나, +카탈로그와 문서를 따로 스케줄링하고 싶을 때 `from_chunks()`를 직접 사용합니다. ```python -from lang2sql import VectorRetriever, IndexBuilder -from lang2sql.integrations.vectorstore import InMemoryVectorStore +from lang2sql import CatalogChunker, RecursiveCharacterChunker, VectorRetriever from lang2sql.integrations.embedding import OpenAIEmbedding +from lang2sql.integrations.vectorstore import FAISSVectorStore embedding = OpenAIEmbedding() -store = InMemoryVectorStore() -registry: dict = {} # IndexBuilder와 VectorRetriever가 공유하는 저장소 -builder = IndexBuilder( - embedding=embedding, - vectorstore=store, - registry=registry, -) +# (1) 청킹 — 각 소스를 명시적으로 split +catalog_chunks = CatalogChunker().split(CATALOG) +doc_chunks = RecursiveCharacterChunker(chunk_size=500).split(DOCS) +all_chunks = catalog_chunks + doc_chunks -retriever = VectorRetriever( - vectorstore=store, +# (2) 영속 벡터스토어 지정 +store = FAISSVectorStore(index_path="./index/catalog.faiss") + +# (3) Retriever 생성 (embed + store 자동) +retriever = VectorRetriever.from_chunks( + all_chunks, embedding=embedding, - registry=registry, # 같은 registry 공유 + vectorstore=store, ) - -# 단계별 인덱싱 — 기존 데이터 유지됨 -builder.run(CATALOG) # 카탈로그 인덱싱 -builder.run(DOCS) # 문서 인덱싱 (카탈로그 유지) -builder.run(NEW_DOCS) # 추가 문서 (기존 모두 유지) +store.save() # 디스크에 저장 result = retriever("매출 정의") ``` @@ -247,6 +262,7 @@ result = retriever("매출 정의") - 벡터 저장소를 외부 DB(FAISS 파일, pgvector)로 교체할 때 - 인덱스를 디스크에 저장하고 재사용할 때 - 카탈로그와 문서를 따로 스케줄링할 때 +- 청킹 중간 결과를 검사하거나 필터링할 때 --- @@ -277,7 +293,7 @@ retriever = VectorRetriever.from_sources( catalog=CATALOG, documents=DOCS, embedding=embedding, - document_chunker=SemanticChunker(embedding=embedding), # ← 의미 기반 청킹 + splitter=SemanticChunker(embedding=embedding), # ← 의미 기반 청킹 ) ``` @@ -315,7 +331,7 @@ retriever = VectorRetriever.from_sources( catalog=CATALOG, documents=DOCS, embedding=OpenAIEmbedding(), - document_chunker=LangChainChunkerAdapter(lc_splitter), + splitter=LangChainChunkerAdapter(lc_splitter), ) ``` @@ -423,6 +439,8 @@ assert len(result2.context) >= 1 # ── 4. add() — 점진적 인덱싱 ───────────────────────────────────────────────── +from lang2sql import RecursiveCharacterChunker + initial_count = len(retriever._registry) NEW_DOC: list[TextDocument] = [ @@ -434,7 +452,7 @@ NEW_DOC: list[TextDocument] = [ }, ] -retriever.add(NEW_DOC) +retriever.add(RecursiveCharacterChunker().split(NEW_DOC)) # pre-split 필수 print("\n✓ add() — 점진적 인덱싱") print(f" registry 크기: {initial_count} → {len(retriever._registry)}") @@ -443,26 +461,15 @@ assert len(retriever._registry) > initial_count # ── 5. score_threshold 필터링 ───────────────────────────────────────────────── +from lang2sql import CatalogChunker from lang2sql.integrations.vectorstore import InMemoryVectorStore -store = InMemoryVectorStore() -registry: dict = {} - -from lang2sql import IndexBuilder - -builder = IndexBuilder( +catalog_chunks = CatalogChunker().split(CATALOG) +strict_retriever = VectorRetriever.from_chunks( + catalog_chunks, embedding=FakeEmbedding(), - vectorstore=store, - registry=registry, -) -builder.run(CATALOG) - -strict_retriever = VectorRetriever( - vectorstore=store, - embedding=FakeEmbedding(), - registry=registry, # FakeEmbedding은 항상 동일 벡터 반환 → 코사인 유사도 = 1.0 - # threshold=1.0 이면 1.0 <= 1.0 조건 충족 → 전부 필터링됨 + # score_threshold=1.0 이면 1.0 <= 1.0 조건 충족 → 전부 필터링됨 score_threshold=1.0, ) @@ -472,65 +479,29 @@ print(f" schemas: {result3.schemas} (빈 리스트 예상)") assert result3.schemas == [] -# ── 6. IndexBuilder 직접 사용 ───────────────────────────────────────────────── +# ── 6. from_chunks — 카탈로그 + 문서 병합 ──────────────────────────────────── -store2 = InMemoryVectorStore() -registry2: dict = {} -builder2 = IndexBuilder( - embedding=FakeEmbedding(), - vectorstore=store2, - registry=registry2, -) -retriever3 = VectorRetriever( - vectorstore=store2, +catalog_chunks2 = CatalogChunker().split(CATALOG) +doc_chunks2 = RecursiveCharacterChunker().split(DOCS) +all_chunks2 = catalog_chunks2 + doc_chunks2 + +retriever3 = VectorRetriever.from_chunks( + all_chunks2, embedding=FakeEmbedding(), - registry=registry2, ) -builder2.run(CATALOG) -catalog_ids = set(registry2.keys()) - -builder2.run(DOCS) # 카탈로그가 유지되는지 확인 +catalog_ids = {c["chunk_id"] for c in catalog_chunks2} for chunk_id in catalog_ids: - assert chunk_id in registry2, f"카탈로그 청크 '{chunk_id}' 유실!" - -print("\n✓ IndexBuilder — 카탈로그 유지 확인") -print(f" 카탈로그 청크 수: {len(catalog_ids)} (모두 유지됨)") + assert chunk_id in retriever3._registry, f"카탈로그 청크 '{chunk_id}' 유실!" +print("\n✓ from_chunks — 카탈로그 + 문서 병합 확인") +print(f" 카탈로그 청크 수: {len(catalog_ids)} (모두 존재)") -# ── 7. BaselineNL2SQL 파이프라인 주입 ──────────────────────────────────────── -class FakeLLM: - def invoke(self, messages): - return "```sql\nSELECT COUNT(*) FROM orders\n```" - -class FakeDB: - def execute(self, sql): - return [{"cnt": 44}] - -from lang2sql import BaselineNL2SQL - -pipeline = BaselineNL2SQL( - catalog=CATALOG, - llm=FakeLLM(), - db=FakeDB(), - retriever=VectorRetriever.from_sources( - catalog=CATALOG, - embedding=FakeEmbedding(), - ), -) - -result4 = pipeline.run("주문 건수") -print("\n✓ BaselineNL2SQL — VectorRetriever 주입") -print(f" 결과: {result4}") -assert result4 == [{"cnt": 44}] - - -# ── 8. public import 확인 ──────────────────────────────────────────────────── +# ── 7. public import 확인 ──────────────────────────────────────────────────── from lang2sql import ( VectorRetriever, - IndexBuilder, CatalogChunker, RecursiveCharacterChunker, DocumentChunkerPort, @@ -554,17 +525,18 @@ print("=" * 50) ``` [CATALOG / DOCS] │ - ▼ - IndexBuilder.run() - ├── CatalogChunker — 테이블 헤더 + 컬럼 그룹 분할 - └── RecursiveCharacterChunker / SemanticChunker — 문서 분할 - │ - ▼ embed_texts() - EmbeddingPort — OpenAIEmbedding 등 + ▼ chunker.split() + CatalogChunker — 테이블 헤더 + 컬럼 그룹 분할 + RecursiveCharacterChunker — 문서 분할 (또는 SemanticChunker) │ - ▼ upsert() - VectorStorePort — InMemoryVectorStore (기본) - + registry dict 공유 + ▼ list[IndexedChunk] + VectorRetriever.from_chunks() / from_sources() + │ embed_texts() + ▼ + EmbeddingPort — OpenAIEmbedding 등 (6개) + │ upsert() + ▼ + VectorStorePort — InMemoryVectorStore / FAISSVectorStore / PGVectorStore │ ▼ VectorRetriever.__call__(query)