Skip to content

Commit 5f4a009

Browse files
authored
Feat/e2e pipeline (#210)
* chore : internal spec added * chore: remove local-only ignore rule * feat(core): add CatalogEntry TypedDict as explicit schema contract * refactor(core): remove RunContext contract check and run_query() from BaseComponent/BaseFlow * refactor(flows): replace RunContext-based SequentialFlow with explicit value pipeline - Remove RunComponent Protocol, _apply(), _run_steps() - SequentialFlow.run() now takes and returns Any (plain value pipe) - BaselineFlow demoted to deprecated alias with DeprecationWarning * refactor(components/retrieval): change KeywordRetriever to explicit str → list[CatalogEntry] API - run(run: RunContext) → run(query: str) -> list[CatalogEntry] - Re-export CatalogEntry from retrieval __init__ * test: update tests to RunContext-free API - Remove RunContext contract tests from test_core_base - Update SequentialFlow tests to new value-pipe signature - Add BaselineFlow deprecation warning assertion - Update KeywordRetriever tests to str → list[CatalogEntry] * docs: update BaseFlow and RunContext docs to reflect RunContext-free API - Replace run_query()/RunContext examples with SequentialFlow.run(value) - Mark RunContext as legacy utility in RunContext_ko.md * refactor(core): make run() the public hook-bearing entry point, _run() the abstract impl BaseComponent and BaseFlow now expose run() as the public API (with hook events), while _run() becomes the abstract method subclasses implement. __call__() is a convenience alias that delegates to run(). This fixes the design gap where direct .run() calls bypassed the hook system. * feat(core): add LLMPort and DBPort protocols to ports.py * feat(components): add SQLGenerator component Implements run(query, schemas) -> str with BM25-retrieved schema context building, LLMPort.invoke() call, block extraction, and ComponentError on missing block. * feat(components): add SQLExecutor component Implements run(sql) -> list[dict] with empty-sql guard and DBPort.execute() delegation. * feat(flows): add BaselineNL2SQL end-to-end pipeline Orchestrates KeywordRetriever → SQLGenerator → SQLExecutor with shared hook injection. * feat(integrations): add AnthropicLLM, OpenAILLM, and SQLAlchemyDB integrations AnthropicLLM: system message extraction, IntegrationMissingError on missing package. OpenAILLM: raw openai SDK (not langchain-openai). SQLAlchemyDB: SQLAlchemy 2.x text() wrapping and row._mapping conversion. * feat: expose public API in __init__.py and add optional extras to pyproject.toml Exports CatalogEntry, LLMPort, DBPort, KeywordRetriever, SQLGenerator, SQLExecutor, BaselineNL2SQL, TraceHook, MemoryHook, NullHook, and exception classes. Adds [anthropic] and [sqlalchemy] optional dependency groups. * test: add tests for SQLGenerator, SQLExecutor, and BaselineNL2SQL e2e pipeline FakeLLM and FakeDB defined inline (no external mock libraries). E2E test verifies 3 component start events via MemoryHook. * feat(integrations/llm): add api_key parameter and fix IntegrationMissingError message Allow callers to pass an explicit API key to OpenAILLM and AnthropicLLM. Remove the optional-extra hint from IntegrationMissingError since both packages are now core dependencies. * refactor(pyproject): promote anthropic and sqlalchemy to core dependencies Remove [project.optional-dependencies] section. Both packages are required for the e2e pipeline and should not be optional extras. * feat(generation): add db_dialect parameter with per-dialect prompt files SQLGenerator now accepts db_dialect (sqlite, postgresql, mysql, bigquery, duckdb) and loads the matching prompt from components/generation/prompts/{dialect}.md. system_prompt takes precedence when both are provided. BaselineNL2SQL forwards db_dialect to SQLGenerator. * test(generation): add db_dialect tests for SQLGenerator Covers: sqlite/postgresql prompt loading, unsupported dialect raises ValueError, system_prompt overrides db_dialect. * feat(scripts): add setup_sample_db.py for tutorial sample data Creates customers/products/orders/order_items tables with Korean sample data for SQLite (default) and PostgreSQL. Fixes postgres default URL to match docker-compose-postgres.yml credentials (postgres:postgres). * docs: add quickstart.md tutorial for first-time users Covers installation, API key setup, sample DB creation, SQLAlchemyDB connection, catalog definition, BaselineNL2SQL with db_dialect, Hook tracing, customization, error handling, and a full feature checklist runnable without real API keys using FakeLLM/FakeDB. * feat(integrations/llm): add api_key param and fix IntegrationMissingError message Add api_key: str | None = None to AnthropicLLM. Remove extra= from IntegrationMissingError since anthropic is now a core dependency. * style: apply black formatting * chore: upgrade black pre-commit hook to 26.1.0
1 parent d54d451 commit 5f4a009

45 files changed

Lines changed: 2474 additions & 559 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.pre-commit-config.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
repos:
22
- repo: https://github.com/psf/black
3-
rev: 25.1.0
3+
rev: 26.1.0
44
hooks:
55
- id: black

docs/BaseFlow_ko.md

Lines changed: 26 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# BaseFlow
22

3-
`BaseFlow`는 Lang2SQL에서 **define-by-run(순수 파이썬 제어)** 철학을 구현하기 위한 플로우의 최소 추상화(minimal abstraction)입니다.
3+
`BaseFlow`는 Lang2SQL에서 **define-by-run(순수 파이썬 제어)** 철학을 구현하기 위한 "플로우의 최소 추상화(minimal abstraction)"입니다.
44

55
* 파이프라인의 **제어권(control-flow)** 을 프레임워크 DSL이 아니라 **사용자 코드(Python)** 가 갖습니다.
66
* LangGraph 같은 그래프 엔진을 강제하지 않습니다.
@@ -10,7 +10,7 @@
1010

1111
## 왜 필요한가?
1212

13-
### 1) 제어는 파이썬으로를 지키기 위해
13+
### 1) "제어는 파이썬으로"를 지키기 위해
1414

1515
Text2SQL은 현실적으로 다음 제어가 자주 필요합니다.
1616

@@ -19,11 +19,11 @@ Text2SQL은 현실적으로 다음 제어가 자주 필요합니다.
1919
* 부분 파이프라인(서브플로우) 호출
2020
* 정책(policy) 기반 행동 결정
2121

22-
`BaseFlow`는 이런 제어를 **사용자가 Python으로 직접 작성**하게 두고, 라이브러리는 실행 컨테이너 + 관측성만 제공합니다.
22+
`BaseFlow`는 이런 제어를 **사용자가 Python으로 직접 작성**하게 두고, 라이브러리는 "실행 컨테이너 + 관측성"만 제공합니다.
2323

2424
### 2) 요청 단위 관측성(Flow-level tracing)
2525

26-
운영/디버깅에서는 이 요청 전체가 언제 시작했고, 어디서 실패했고, 얼마나 걸렸는지가 먼저 중요합니다.
26+
운영/디버깅에서는 "이 요청 전체가 언제 시작했고, 어디서 실패했고, 얼마나 걸렸는지"가 먼저 중요합니다.
2727

2828
`BaseFlow`는 다음 이벤트를 발행합니다.
2929

@@ -32,15 +32,6 @@ Text2SQL은 현실적으로 다음 제어가 자주 필요합니다.
3232

3333
→ 요청 1건을 **Flow 단위로 빠르게 파악**할 수 있습니다.
3434

35-
### 3) 공통 엔트리포인트(run_query) 제공
36-
37-
Text2SQL은 대부분 “문장(query)”이 시작점입니다.
38-
39-
`run_query("...")`를 제공하면:
40-
41-
* 초급 사용자는 `RunContext`를 몰라도 “바로 실행” 가능
42-
* 고급 사용자는 `run(RunContext)`로 제어를 확장 가능
43-
4435
---
4536

4637
## BaseFlow가 제공하는 API
@@ -49,84 +40,57 @@ Text2SQL은 대부분 “문장(query)”이 시작점입니다.
4940

5041
```python
5142
class MyFlow(BaseFlow):
52-
def run(self, run: RunContext) -> RunContext:
43+
def run(self, value):
5344
...
54-
return run
45+
return result
5546
```
5647

5748
* Flow의 본체 로직은 여기에 작성합니다.
5849
* 제어는 Python으로 직접 작성합니다. (`if/for/while`)
50+
* 입출력 타입은 자유롭게 정의합니다. 공유 상태 백(RunContext)을 강제하지 않습니다.
5951

6052
### 2) 호출: `__call__`
6153

6254
```python
63-
out = flow(run)
55+
out = flow(value)
6456
```
6557

6658
* 내부적으로 `flow.run(...)`을 호출합니다.
6759
* hook 이벤트를 `start/end/error`로 기록합니다.
6860

69-
### 3) 편의 엔트리포인트: `run_query()`
70-
71-
```python
72-
out = flow.run_query("지난달 매출")
73-
```
74-
75-
* 내부에서 `RunContext(query=...)`를 만들고 `run()`을 호출합니다.
76-
* Quickstart / demo / 초급 UX용 엔트리포인트입니다.
77-
78-
> 권장: **BaseFlow에 run_query를 둬서 “모든 Flow는 run_query가 된다”는 직관을 유지**합니다.
79-
80-
---
81-
82-
## run(runcontext) vs run_query(query)
83-
84-
둘은 기능적으로 **같은 동작**을 하도록 설계합니다.
85-
86-
```python
87-
out1 = flow.run_query("지난달 매출")
88-
out2 = flow.run(RunContext(query="지난달 매출"))
89-
```
90-
91-
* `run_query(query)` : 문자열 query에서 시작하는 편의 API
92-
* `run(runcontext)` : 고급 사용자를 위한 명시적 API
93-
9461
---
9562

9663
## 사용 패턴
9764

98-
### 1) 초급: SequentialFlow로 구성하고 run_query로 실행
65+
### 1) 초급: SequentialFlow로 구성하고 run으로 실행
9966

100-
초급 사용자는 보통 구성만 하고 실행하면 됩니다.
67+
초급 사용자는 보통 "구성만 하고 실행"하면 됩니다.
10168

10269
```python
10370
flow = SequentialFlow(steps=[retriever, builder, generator, validator])
104-
out = flow.run_query("지난달 매출")
71+
result = flow.run("지난달 매출")
10572
```
10673

74+
각 step은 이전 step의 출력을 입력으로 받습니다.
75+
10776
### 2) 고급: CustomFlow로 제어(while/if/policy)
10877

10978
정책/루프/재시도 같은 제어가 들어오면 `BaseFlow`를 직접 상속해 작성하는 것이 가장 깔끔합니다.
11079

11180
```python
11281
class RetryFlow(BaseFlow):
113-
def run(self, run: RunContext) -> RunContext:
114-
while True:
115-
run = retriever(run)
116-
metrics = eval_retrieval(run) # 순수 함수 가능
117-
action = policy(metrics) # 순수 함수 가능
118-
if action == "retry":
119-
continue
120-
break
121-
122-
run = generator(run)
123-
run = validator(run)
124-
return run
82+
def run(self, query: str) -> str:
83+
for _ in range(3):
84+
schemas = retriever(query)
85+
sql = generator(query, schemas)
86+
if validator(sql):
87+
return sql
88+
return sql
12589
```
12690

12791
### 3) Sequential을 유지하면서 동적 파라미터가 필요하면 closure/partial
12892

129-
이건 “필수”가 아니라, **steps 배열을 유지하고 싶은 사람을 위한 옵션**입니다.
93+
이건 "필수"가 아니라, **steps 배열을 유지하고 싶은 사람을 위한 옵션**입니다.
13094

13195
---
13296

@@ -140,7 +104,7 @@ from lang2sql.core.hooks import MemoryHook
140104
hook = MemoryHook()
141105
flow = SequentialFlow(steps=[...], hook=hook)
142106

143-
out = flow.run_query("지난달 매출")
107+
result = flow.run("지난달 매출")
144108

145109
for e in hook.events:
146110
print(e.name, e.phase, e.component, e.duration_ms, e.error)
@@ -153,8 +117,8 @@ for e in hook.events:
153117

154118
## (관련 개념) BaseFlow와 BaseComponent의 관계
155119

156-
* `BaseFlow`어떻게 실행할지(제어/조립)를 담당합니다.
157-
* `BaseComponent`한 단계에서 무엇을 할지(작업 단위)를 담당합니다.
120+
* `BaseFlow`"어떻게 실행할지(제어/조립)"를 담당합니다.
121+
* `BaseComponent`"한 단계에서 무엇을 할지(작업 단위)"를 담당합니다.
158122

159123
일반적으로:
160124

@@ -176,8 +140,5 @@ A. Flow라는 개념은 사실상 필요하지만, **모든 사용자가 BaseFlo
176140

177141
### Q. Flow의 반환 타입은?
178142

179-
A. `run()`**반드시 `RunContext`를 반환**하는 것을 권장합니다.
180-
(합성/디버깅/타입 안정성 측면에서 이득이 큽니다.)
181-
182-
---
183-
143+
A. `run()`의 입출력 타입은 자유롭습니다. 컴포넌트끼리 합의한 타입을 그대로 사용하면 됩니다.
144+
`SequentialFlow`는 각 step의 출력을 다음 step의 입력으로 전달하는 파이프입니다.

docs/RunContext_ko.md

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
1+
> **레거시 유틸리티**: `RunContext`는 현재 레거시 유틸리티로 유지됩니다.
2+
> 새 코드에서는 명시적 Python 인자를 사용하는 것을 권장합니다.
3+
> 컴포넌트 I/O는 `RunContext` 대신 구체적인 타입(str, list 등)으로 표현하세요.
4+
15
## RunContext
26

3-
`RunContext`는 define-by-run 파이프라인에서 **상태(state)를 운반하는 최소 State Carrier**입니다.
4-
컴포넌트는 기본적으로 `RunContext -> RunContext` 계약을 따르며, 필요한 값을 읽고/쓰면서 파이프라인을 구성합니다.
7+
`RunContext`는 define-by-run 파이프라인에서 **상태(state)를 운반하는 State Carrier**입니다.
8+
레거시 파이프라인이나 직접 상태를 조합할 때 유용합니다.
59

610
### 설계 원칙
711

@@ -13,7 +17,7 @@
1317

1418
## 데이터 구조 트리
1519

16-
아래는 `RunContext`가 담는 데이터 구조를 트리 형태로 나타낸 것입니다.
20+
아래는 `RunContext`가 담는 데이터 구조를 "트리 형태"로 나타낸 것입니다.
1721

1822
```
1923
RunContext
@@ -93,21 +97,36 @@ RunContext
9397

9498
---
9599

96-
## 파이프라인 예시 (Text2SQL)
100+
## 파이프라인 예시 (Text2SQL) — 새 API
101+
102+
새 API에서는 각 컴포넌트가 명시적 인자를 주고받습니다.
97103

98-
개념:
104+
```python
105+
query = "지난달 매출"
99106

100-
* retriever: `(query, catalog) -> selected`
101-
* builder: `(query, selected) -> context`
102-
* generator: `(query, context) -> sql`
103-
* validator: `(sql) -> validation`
107+
schemas = retriever(query) # str → list[CatalogEntry]
108+
context = builder(query, schemas) # str, list → str
109+
sql = generator(query, context) # str, str → str
110+
result = validator(sql) # str → ValidationResult
111+
```
104112

105-
RunContext에서의 읽기/쓰기:
113+
또는 `SequentialFlow`로 조합:
106114

107-
* retriever: `run.query`, `run.schema_catalog` 읽고 → `run.schema_selected` 작성
108-
* builder: `run.query`, `run.schema_selected` 읽고 → `run.schema_context` 작성
109-
* generator: `run.query`, `run.schema_context` 읽고 → `run.sql` 작성
110-
* validator: `run.sql` 읽고 → `run.validation` 작성
115+
```python
116+
flow = SequentialFlow(steps=[retriever, builder, generator, validator])
117+
result = flow.run(query)
118+
```
111119

112120
---
113121

122+
## RunContext 직접 사용 (레거시)
123+
124+
기존 코드나 직접 상태를 조합할 때만 사용합니다.
125+
126+
```python
127+
from lang2sql.core.context import RunContext
128+
129+
run = RunContext(query="지난달 매출")
130+
# run을 직접 조작하거나 레거시 컴포넌트에 전달
131+
run.metadata["session_id"] = "abc123"
132+
```

interface/app_pages/chatbot.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,13 @@ def initialize_session_state():
8282
# 페이지 제목
8383
st.title("🤖 AI ChatBot")
8484

85-
st.markdown(
86-
"""
85+
st.markdown("""
8786
LangGraph 기반 AI ChatBot과 대화를 나눌 수 있습니다.
8887
- 데이터베이스 테이블 정보 검색
8988
- 용어집 조회
9089
- 쿼리 예제 조회
9190
- 대화를 통해 질문 구체화
92-
"""
93-
)
91+
""")
9492

9593
# 설정 로드
9694
config = load_config()

interface/app_pages/home.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,7 @@
88

99
st.title("🏠 홈")
1010

11-
st.markdown(
12-
"""
11+
st.markdown("""
1312
### Lang2SQL 데이터 분석 도구에 오신 것을 환영합니다 🎉
1413
1514
이 도구는 자연어로 작성한 질문을 SQL 쿼리로 변환하고,
@@ -21,7 +20,6 @@
2120
2. **🔍 Lang2SQL**: 자연어 → SQL 변환 및 결과 분석
2221
3. **📊 그래프 빌더**: LangGraph 실행 순서를 프리셋/커스텀으로 구성하고 세션에 적용
2322
4. **⚙️ 설정**: 데이터 소스, LLM, DB 연결 등 환경 설정
24-
"""
25-
)
23+
""")
2624

2725
st.info("왼쪽 메뉴에서 기능 페이지를 선택해 시작하세요 🚀")

interface/app_pages/lang2sql.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,6 @@
2828
render_sidebar_db_selector,
2929
)
3030

31-
3231
TITLE = "Lang2SQL"
3332
DEFAULT_QUERY = "고객 데이터를 기반으로 유니크한 유저 수를 카운트하는 쿼리"
3433
SIDEBAR_OPTIONS = {

interface/app_pages/settings.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,6 @@
1111
from interface.app_pages.settings_sections.llm_section import render_llm_section
1212
from interface.app_pages.settings_sections.db_section import render_db_section
1313

14-
1514
st.title("⚙️ 설정")
1615

1716
config = load_config()

interface/app_pages/settings_sections/llm_section.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
get_embedding_registry,
1313
)
1414

15-
1615
LLM_PROVIDERS = [
1716
"openai",
1817
"azure",

interface/core/result_renderer.py

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -76,13 +76,11 @@ def should_show(_key: str) -> bool:
7676
st.markdown("---")
7777
token_summary = TokenUtils.get_token_usage_summary(data=res["messages"])
7878
st.write("**토큰 사용량:**")
79-
st.markdown(
80-
f"""
79+
st.markdown(f"""
8180
- Input tokens: `{token_summary['input_tokens']}`
8281
- Output tokens: `{token_summary['output_tokens']}`
8382
- Total tokens: `{token_summary['total_tokens']}`
84-
"""
85-
)
83+
""")
8684

8785
if show_sql_section:
8886
st.markdown("---")

pyproject.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ dependencies = [
4646
"pgvector==0.3.6",
4747
"langchain-postgres==0.0.15",
4848
"trino>=0.329.0,<1.0.0",
49+
"anthropic>=0.20.0",
50+
"sqlalchemy>=2.0",
4951
]
5052

5153
[project.scripts]

0 commit comments

Comments
 (0)