Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -137,13 +137,19 @@ lang2sql query "질문" --vectordb-type faiss --vectordb-location ./dev/table_in
lang2sql query "질문" --vectordb-type pgvector --vectordb-location "postgresql://user:pass@host:5432/db"
```

참고: DataHub 없이도 미리 준비된 VectorDB(FAISS 디렉토리 혹은 pgvector 컬렉션)를 바로 사용할 수 있습니다. 자세한 준비 방법은 [DataHub 없이 시작하기](docs/tutorials/getting-started-without-datahub.md) 참고하세요.
참고: DataHub 없이도 미리 준비된 VectorDB(FAISS 파일 혹은 pgvector 컬렉션)를 바로 사용할 수 있습니다. 자세한 준비 방법은 [벡터 검색 튜토리얼](docs/tutorials/03-vector-search.md) 참고하세요.

### 처음 시작하기 (DataHub 없이)
### 처음 시작하기

튜토리얼 본문이 길어져 별도 문서로 분리되었습니다. 아래 문서를 참고하세요.
튜토리얼은 난이도 순서로 구성되어 있습니다.

- [DataHub 없이 시작하기 튜토리얼](docs/tutorials/getting-started-without-datahub.md)
| 번호 | 문서 | 내용 |
|------|------|------|
| 01 | [빠른 시작](docs/tutorials/01-quickstart.md) | 5분 안에 NL2SQL 실행 |
| 02 | [Baseline 파이프라인](docs/tutorials/02-baseline.md) | 실제 DB 연결, DB Explorer |
| 03 | [벡터 검색](docs/tutorials/03-vector-search.md) | FAISS/pgvector 인덱싱 |
| 04 | [하이브리드 검색](docs/tutorials/04-hybrid.md) | BM25 + Vector, EnrichedNL2SQL |
| 05 | [고급](docs/tutorials/05-advanced.md) | 수동 조합, 커스텀 어댑터, Hook |

### 자연어 쿼리 실행

Expand Down
197 changes: 81 additions & 116 deletions docs/BaseComponent_ko.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,25 @@

`BaseComponent`는 **define-by-run(순수 파이썬 제어)** 철학을 유지하면서도, 컴포넌트 실행을 **관측 가능(observable)** 하게 만들기 위한 **선택적(opt-in) 표준 레이어**입니다.

* 파이프라인은 `step(run: RunContext) -> RunContext` 형태의 **그냥 함수/콜러블**만으로도 충분히 동작합니다.
* 파이프라인은 그냥 함수/콜러블만으로도 충분히 동작합니다.
* `BaseComponent`는 그 위에 **추적(hooks), 에러 표준화, 이름/형식 통일**을 얹어주는 역할을 합니다.

즉, **필수는 아니지만**, 라이브러리/팀 단위 개발에서 운영 가능한 형태로 만들고 싶을 때 유용합니다.
즉, **필수는 아니지만**, 라이브러리/팀 단위 개발에서 "운영 가능한 형태"로 만들고 싶을 때 유용합니다.

---

## 왜 필요한가?

### 1) 관측성(Tracing)을 그래프 엔진 없이 얻기 위해
### 1) 관측성(Tracing)을 "그래프 엔진 없이" 얻기 위해

Lang2SQL은 LangGraph 같은 그래프 엔진을 강제하지 않습니다. 대신:
Lang2SQL은 그래프 엔진을 강제하지 않습니다. 대신:

* 사용자는 Python `if/for/while`로 제어한다.
* 라이브러리는 관측성은 **hook 이벤트**로 제공한다.

`BaseComponent`는 각 컴포넌트 실행의 `start/end/error`를 이벤트로 남깁니다.

### 2) 에러를 도메인 친화적으로 정리하기 위해
### 2) 에러를 "도메인 친화적으로" 정리하기 위해

현실에서는 `ValueError`, `KeyError`, 외부 라이브러리 예외 등이 섞여서 올라옵니다.

Expand All @@ -29,9 +29,9 @@ Lang2SQL은 LangGraph 같은 그래프 엔진을 강제하지 않습니다. 대
* `Lang2SQLError`(ValidationError, IntegrationMissingError 등)는 **그대로 유지**
* 그 외 예외는 `ComponentError`로 **표준 래핑**(+ 원인 예외를 `cause`로 보존)

→ 사용자/운영자 관점에서 어디서 터졌는지가 분명해집니다.
→ 사용자/운영자 관점에서 "어디서 터졌는지"가 분명해집니다.

### 3) 컴포넌트 단위 표준을 만들기 위해
### 3) "컴포넌트 단위 표준"을 만들기 위해

라이브러리 제공 컴포넌트를 모두 BaseComponent 기반으로 만들면:

Expand All @@ -41,21 +41,6 @@ Lang2SQL은 LangGraph 같은 그래프 엔진을 강제하지 않습니다. 대

---

## 철학: Define-by-run + Minimal core

Lang2SQL의 기본 철학은 아래 2개입니다.

1. **제어는 파이썬으로**
루프/분기/재시도/서브플로우 호출은 “프레임워크 DSL”이 아니라 Python으로 표현합니다.

2. **상태는 RunContext 하나로**
파이프라인이 커져도, step 간 연결이 깨지지 않도록 `RunContext`를 I/O로 둡니다.

`BaseComponent`는 이 철학을 해치지 않습니다.
컴포넌트의 실행을 감싸서 이벤트만 남길 뿐, 그래프/스키마/실행 모델을 강제하지 않습니다.

---

## BaseComponent가 제공하는 API

### 생성자
Expand All @@ -67,21 +52,27 @@ BaseComponent(name: str | None = None, hook: TraceHook | None = None)
* `name`: 이벤트에 찍힐 컴포넌트 이름 (기본값: 클래스명)
* `hook`: 이벤트 수신자. 기본값은 `NullHook()` (아무것도 하지 않음)

### 구현해야 하는 것: `run()`
### 구현해야 하는 것: `_run()`

서브클래스는 `_run()`을 구현합니다. 인자 타입과 반환 타입은 각 컴포넌트에 맞게 자유롭게 정의합니다.

```python
class MyComp(BaseComponent):
def run(self, run: RunContext) -> RunContext:
...
return run
class MyRetriever(BaseComponent):
def __init__(self, catalog: list, **kwargs):
super().__init__(**kwargs)
self._catalog = catalog

def _run(self, query: str) -> list[dict]:
# 비즈니스 로직
return [t for t in self._catalog if query in t["description"]]
```

### 실행: `__call__`
### 호출: `run()` / `__call__`

`comp(run)`을 호출하면 내부적으로 아래를 자동 수행합니다.
`comp.run(query)` 또는 `comp(query)`를 호출하면 내부적으로 아래를 자동 수행합니다.

* `component.run start 이벤트 발행`
* `self.run(...)` 실행
* `self._run(...)` 실행
* 성공 시 `end 이벤트` + `duration_ms`
* 실패 시 `error 이벤트`

Expand All @@ -90,90 +81,84 @@ class MyComp(BaseComponent):

---

## 권장 규약: RunContext in → RunContext out
## 타입 인자 패턴

Lang2SQL의 기본 step 규약은 단순합니다.
Lang2SQL의 컴포넌트는 **명시적 타입 인자**를 받고, **명시적 타입 결과**를 반환합니다.

> **RunContext를 받으면 RunContext를 반환한다.**
> (`return run`을 습관처럼)

왜냐하면 “None 반환”은 인간이 보기엔 자연스럽지만, 팀/사용자 관점에서는 실수를 만들기 쉽습니다.
```python
# 라이브러리 내장 컴포넌트 시그니처 예시
KeywordRetriever._run(query: str) -> list[CatalogEntry]
SQLGenerator._run(query: str, schemas: list[CatalogEntry], context: str = "") -> str
SQLExecutor._run(sql: str) -> list[dict]
```

* `return None`은 “의도적”인지 “실수(반환 누락)”인지 구분이 안 됨
* Flow/컴포넌트 조합에서 결과가 조용히 깨지기 쉬움
### 구성(config)은 `__init__`에, 요청별 데이터는 `_run()` 인자에

그래서 Lang2SQL은 **fail-fast** 스타일을 권장합니다.
```python
class SQLGenerator(BaseComponent):
def __init__(self, llm: LLMPort, db_dialect: str = "default", **kwargs):
super().__init__(**kwargs)
self._llm = llm # 고정 설정
self._dialect = db_dialect

def _run(self, query: str, schemas: list[CatalogEntry], context: str = "") -> str:
# 요청마다 달라지는 값은 _run() 인자로 받는다
...
```

---

## 언제 BaseComponent를 쓰는가?

### BaseComponent를 쓰는 게 좋은 경우
### BaseComponent를 쓰는 게 좋은 경우

* 라이브러리 기본 제공 컴포넌트( retriever/builder/generator/validator )
* 라이브러리 기본 제공 컴포넌트(retriever/generator/executor)
* 팀/제품 환경에서 **관측성(트레이싱)이 필요한 경우**
* 예외 표준화가 중요한 경우(운영/테스트/디버깅)

### BaseComponent 없이 함수로 두는 게 좋은 경우
### BaseComponent 없이 함수로 두는 게 좋은 경우

* `policy`, `eval`, metric 계산처럼 **순수 함수 성격**이 강한 로직
* 유저가 빠르게 붙여 넣어 쓰는 초경량 커스텀 로직
* "유저가 빠르게 붙여 넣어 쓰는" 초경량 커스텀 로직
* 실행 단위가 너무 작아 이벤트가 과도해지는 경우

즉, **핵심 파이프라인 축**은 BaseComponent로 잡고,
그 외의 작은 로직은 함수로 두는 혼합형이 가장 자연스럽습니다.

---

## FunctionalComponent: “함수도 트레이싱하고 싶다”
## 커스텀 컴포넌트 예시

유저에게 “클래스 상속 + run 메서드 작성”이 부담인 경우가 많습니다.
그래서 **함수/콜러블을 그대로 유지하면서**도 트레이싱을 얻고 싶다면 래퍼를 제공합니다.
```python
from lang2sql.core.base import BaseComponent

### 예시: FunctionalComponent
class UpperCaseSQL(BaseComponent):
"""SQL을 대문자로 변환하는 후처리 컴포넌트."""
def _run(self, sql: str) -> str:
return sql.upper()

```python
from __future__ import annotations
from typing import Callable, Any, Optional

from .base import BaseComponent
from .context import RunContext

class FunctionalComponent(BaseComponent):
"""
Wrap a callable(run: RunContext) -> RunContext into a BaseComponent,
so it becomes traceable and error-normalized.
"""

def __init__(
self,
fn: Callable[[RunContext], RunContext],
*,
name: str | None = None,
hook=None,
) -> None:
super().__init__(name=name or getattr(fn, "__name__", "FunctionalComponent"), hook=hook)
self._fn = fn

def run(self, run: RunContext) -> RunContext:
return self._fn(run)
upper = UpperCaseSQL()
print(upper.run("select 1")) # SELECT 1
```

### 사용 예
hook을 주입하면 실행 추적도 자동으로 됩니다:

```python
def my_retriever(run: RunContext) -> RunContext:
run.schema_selected = ...
return run
from lang2sql import MemoryHook

retriever = FunctionalComponent(my_retriever, name="MyRetriever", hook=hook)
```
hook = MemoryHook()
upper = UpperCaseSQL(hook=hook)
upper.run("select 1")

> 이 방식의 장점: 유저는 “함수 스타일” 그대로 유지하면서, 운영/디버깅을 위한 트레이싱을 얻게 됩니다.
for e in hook.snapshot():
print(e.component, e.phase, e.duration_ms)
# UpperCaseSQL start 0.0
# UpperCaseSQL end 0.1
```

---

## 훅(Tracing) 시스템이 뭐고, 왜 필요한가?
## 훅(Tracing) 시스템

### Hook이란?

Expand All @@ -182,22 +167,18 @@ retriever = FunctionalComponent(my_retriever, name="MyRetriever", hook=hook)
* `start/end/error` 시점 기록
* 소요 시간(duration_ms)
* 입력/출력 요약(input_summary/output_summary)
* 필요하면 `data`에 구조화된 값을 추가

### 어디서 확인하나?

가장 쉬운 건 `MemoryHook`입니다.

```python
from lang2sql.core.hooks import MemoryHook
from lang2sql.flows.baseline import SequentialFlow
from lang2sql import MemoryHook, HybridNL2SQL

hook = MemoryHook()
pipeline = HybridNL2SQL(catalog=catalog, llm=llm, db=db, embedding=embedding, hook=hook)
pipeline.run("지난달 매출")

flow = SequentialFlow(steps=[...], hook=hook) # 또는 컴포넌트마다 hook 주입
out = flow.run("지난달 매출")

# 이벤트 확인
for e in hook.snapshot():
print(e.phase, e.component, e.duration_ms, e.error)
```
Expand All @@ -210,13 +191,13 @@ for e in hook.snapshot():
* APM/Tracing으로 보내는 Hook (OpenTelemetry span 등)
* 필터링 Hook (특정 컴포넌트만 샘플링)

핵심은: **관측성은 hook 구현체에서 제어**하고, 파이프라인/컴포넌트 코드는 최대한 비즈니스 로직만 갖도록 분리합니다.
핵심은: **관측성은 hook 구현체에서 제어**하고, 파이프라인/컴포넌트 코드는 최대한 "비즈니스 로직"만 갖도록 분리합니다.

---

## 중첩(서브플로우/래핑)하면 트레이싱이 깨지나?

깨진다기보다는 **이벤트가 더 많이 찍힙니다.**
"깨진다"기보다는 **이벤트가 더 많이 찍힙니다.**

* `flow_b` 안에 `flow_a`를 step으로 넣으면

Expand All @@ -235,49 +216,33 @@ for e in hook.snapshot():

## 베스트 프랙티스

### 1) 구성(config)은 `__init__`에, 요청별 상태는 `RunContext`에
### 1) 구성(config)은 `__init__`에, 요청별 데이터는 `_run()` 인자에

```python
class Retriever(BaseComponent):
def __init__(self, catalog, top_k=8, ...):
self.catalog = catalog # 고정 설정
self.top_k = top_k
고정 설정(모델, 카탈로그, DB 연결 등)은 생성자에서 받고,
요청마다 달라지는 값(쿼리, 스키마 목록 등)은 `_run()` 인자로 전달합니다.

def run(self, run: RunContext) -> RunContext:
# 요청마다 달라지는 값은 run에서 읽고 run에 쓴다
...
return run
```
### 2) `_run()`의 반환값은 명시적으로

### 2) RunContext가 들어오면 무조건 `return run`
반환 타입을 명확히 정의하면 Flow에서 컴포넌트를 조합할 때 안전합니다.

* 가독성(계약이 분명)
* 실수 방지(fail-fast)
* flow 합성 시 안정

### 3) “작은 로직(policy/eval)은 그냥 함수”
### 3) "작은 로직(policy/eval)은 그냥 함수"

* BaseComponent로 감싸는 건 선택
* 운영에서 꼭 추적이 필요할 때만 FunctionalComponent로 감싼다
* 운영에서 꼭 추적이 필요할 때만 감싼다

---

## FAQ

### Q. 그냥 함수만 써도 되는데 왜 굳이 BaseComponent?
### Q. "그냥 함수만 써도 되는데 왜 굳이 BaseComponent?"

A. **운영/디버깅/협업에서** 차이가 큽니다.
문제 났을 때 어디서, 어떤 입력으로, 얼마나 걸리다, 어떤 에러로 터졌는지 자동으로 남는 게 핵심 가치입니다.
문제 났을 때 "어디서, 어떤 입력으로, 얼마나 걸리다, 어떤 에러로" 터졌는지 자동으로 남는 게 핵심 가치입니다.

### Q. BaseComponent를 유저가 직접 써야 하나?
### Q. "BaseComponent를 유저가 직접 써야 하나?"

A. 필수 아닙니다.
초급 유저는 **SequentialFlow + 프리셋 컴포넌트**만으로 충분히 쓰게 하고,
초급 유저는 **프리셋 Flow + 프리셋 컴포넌트**만으로 충분히 쓰게 하고,
고급/운영 유저에게 BaseComponent/Hook을 제공하는 구성이 가장 자연스럽습니다.

### Q. “policy는 RunContext를 몰라도 되는데?”

A. 맞습니다. `policy(metrics) -> action` 같은 건 순수 함수로 두는 걸 권장합니다.
필요하면 `FunctionalComponent(policy_fn)`처럼 감싸서 추적만 추가할 수 있습니다.

---
Loading
Loading