From a25e0c442664725aeafbf4ecb2ac694e28d05a20 Mon Sep 17 00:00:00 2001 From: seyeong Date: Sat, 30 May 2026 13:33:38 +0900 Subject: [PATCH 1/3] chore: wipe deprecated v0.3 era files + add SSOT (PROJECT.md) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 삭제: - poetry.lock (uv로 이전), pgvector.sh, guideline.md (옛 LangGraph 아키텍처) - docker/ (Streamlit Dockerfile + vector compose), scripts/ (v2 quickstart) - dev/create_faiss.py, dev/create_pgvector.py (옛 vector retrieval) - docs/Base{Component,Flow}_ko.md, Core_concept_ko.md, Hook_and_exception_ko.md - docs/production_test_guide.md - docs/tutorials/01~05*.md (옛 RAG 튜토리얼) - docs/discord_first_redesign{,_v2,_v3_minimal,_v4}.md (중간 초안, v4_1/v4_2 final 유지) 추가: - docs/PROJECT.md — 프로젝트 단일 SSOT 문서 (정체성 / 문제 / 4기둥 / 현황 / 로드맵 / 빠른 시작 / 아키텍처 포인터 / 핵심 설계 결정 / 변천) 유지: .pre-commit-config.yaml, DEPLOY.md, v4_1/v4_2 final design docs, process guidelines (PR/branch) 🤖 Generated with [Claude Code](https://claude.com/claude-code) --- dev/create_faiss.py | 53 - dev/create_pgvector.py | 54 - docker/Dockerfile | 32 - docker/Dockerfile.dockerignore | 10 - docker/docker-compose-pgvector.yml | 23 - docker/docker-compose-postgres.yml | 23 - docker/docker-compose.yml | 34 - docker/pgvector/init/001_create_database.sql | 2 - .../init/002_create_user_and_grant.sql | 5 - docker/postgres/init/001_create_database.sql | 2 - .../init/002_create_user_and_grant.sql | 5 - docs/BaseComponent_ko.md | 248 - docs/BaseFlow_ko.md | 191 - docs/Core_concept_ko.md | 99 - docs/Hook_and_exception_ko.md | 311 - docs/PROJECT.md | 152 + docs/discord_first_redesign.md | 611 -- docs/discord_first_redesign_v2.md | 911 --- docs/discord_first_redesign_v3_minimal.md | 578 -- docs/discord_first_redesign_v4.md | 639 -- docs/production_test_guide.md | 1003 --- docs/tutorials/01-quickstart.md | 99 - docs/tutorials/02-baseline.md | 300 - docs/tutorials/03-vector-search.md | 299 - docs/tutorials/04-hybrid.md | 184 - docs/tutorials/05-advanced.md | 397 - guideline.md | 99 - pgvector.sh | 5 - poetry.lock | 6591 ----------------- scripts/setup_sample_db.py | 267 - scripts/setup_sample_docs.py | 88 - 31 files changed, 152 insertions(+), 13163 deletions(-) delete mode 100644 dev/create_faiss.py delete mode 100644 dev/create_pgvector.py delete mode 100644 docker/Dockerfile delete mode 100644 docker/Dockerfile.dockerignore delete mode 100644 docker/docker-compose-pgvector.yml delete mode 100644 docker/docker-compose-postgres.yml delete mode 100644 docker/docker-compose.yml delete mode 100644 docker/pgvector/init/001_create_database.sql delete mode 100644 docker/pgvector/init/002_create_user_and_grant.sql delete mode 100644 docker/postgres/init/001_create_database.sql delete mode 100644 docker/postgres/init/002_create_user_and_grant.sql delete mode 100644 docs/BaseComponent_ko.md delete mode 100644 docs/BaseFlow_ko.md delete mode 100644 docs/Core_concept_ko.md delete mode 100644 docs/Hook_and_exception_ko.md create mode 100644 docs/PROJECT.md delete mode 100644 docs/discord_first_redesign.md delete mode 100644 docs/discord_first_redesign_v2.md delete mode 100644 docs/discord_first_redesign_v3_minimal.md delete mode 100644 docs/discord_first_redesign_v4.md delete mode 100644 docs/production_test_guide.md delete mode 100644 docs/tutorials/01-quickstart.md delete mode 100644 docs/tutorials/02-baseline.md delete mode 100644 docs/tutorials/03-vector-search.md delete mode 100644 docs/tutorials/04-hybrid.md delete mode 100644 docs/tutorials/05-advanced.md delete mode 100644 guideline.md delete mode 100644 pgvector.sh delete mode 100644 poetry.lock delete mode 100644 scripts/setup_sample_db.py delete mode 100644 scripts/setup_sample_docs.py diff --git a/dev/create_faiss.py b/dev/create_faiss.py deleted file mode 100644 index 6547d41..0000000 --- a/dev/create_faiss.py +++ /dev/null @@ -1,53 +0,0 @@ -""" -dev/create_faiss.py - -CSV 파일에서 테이블과 컬럼 정보를 불러와 OpenAI 임베딩으로 벡터화한 뒤, -FAISS 인덱스를 생성하고 로컬 디렉토리에 저장한다. - -환경 변수: - OPEN_AI_KEY: OpenAI API 키 - OPEN_AI_EMBEDDING_MODEL: 사용할 임베딩 모델 이름 - -출력: - 지정된 OUTPUT_DIR 경로에 FAISS 인덱스 저장 -""" - -import csv -import os -from collections import defaultdict - -from dotenv import load_dotenv -from langchain_community.vectorstores import FAISS -from langchain_openai import OpenAIEmbeddings - -load_dotenv() -# CSV 파일 경로 -CSV_PATH = "./dev/table_catalog.csv" -# .env의 VECTORDB_LOCATION과 동일하게 맞추세요 -OUTPUT_DIR = "./dev/table_info_db" - -tables = 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"].strip() - 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"]]) - page = f"{t}: {info['desc']}\nColumns:\n {cols}" - from langchain.schema import Document - - docs.append(Document(page_content=page)) - -emb = OpenAIEmbeddings( - model=os.getenv("OPEN_AI_EMBEDDING_MODEL"), openai_api_key=os.getenv("OPEN_AI_KEY") -) -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}") diff --git a/dev/create_pgvector.py b/dev/create_pgvector.py deleted file mode 100644 index 77edd9f..0000000 --- a/dev/create_pgvector.py +++ /dev/null @@ -1,54 +0,0 @@ -""" -dev/create_pgvector.py - -CSV 파일에서 테이블과 컬럼 정보를 불러와 OpenAI 임베딩으로 벡터화한 뒤, -pgvector에 적재한다. - -환경 변수: - OPEN_AI_KEY: OpenAI API 키 - OPEN_AI_EMBEDDING_MODEL: 사용할 임베딩 모델 이름 - VECTORDB_LOCATION: pgvector 연결 문자열 - PGVECTOR_COLLECTION: pgvector 컬렉션 이름 -""" - -import csv -import os -from collections import defaultdict - -from dotenv import load_dotenv -from langchain.schema import Document -from langchain_openai import OpenAIEmbeddings -from langchain_postgres.vectorstores import PGVector - -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") - -tables = 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 -) -print(f"pgvector collection populated: {COLLECTION}") diff --git a/docker/Dockerfile b/docker/Dockerfile deleted file mode 100644 index 4b3dd4a..0000000 --- a/docker/Dockerfile +++ /dev/null @@ -1,32 +0,0 @@ -# 1. Base image -FROM python:3.12-slim-bullseye - -# 2. 시스템 라이브러리 설치 -RUN apt-get update && apt-get install -y \ - build-essential \ - curl \ - git \ - libpq-dev \ - && rm -rf /var/lib/apt/lists/* - -# 3. uv 설치 -RUN pip install --no-cache-dir uv - -# 4. 작업 디렉토리 설정 -WORKDIR /app - -# 5. 소스 코드 복사 및 의존성 설치 -COPY pyproject.toml ./ -COPY . . -RUN uv pip install --system --upgrade pip setuptools wheel \ - && uv pip install --system . - -# 6. 환경 변수 설정 -ENV PYTHONPATH=/app -ENV PYTHONUNBUFFERED=1 - -# 7. 포트 설정 -ENV STREAMLIT_SERVER_PORT=8501 - -# 8. 실행 명령 -CMD ["lang2sql", "run-streamlit"] diff --git a/docker/Dockerfile.dockerignore b/docker/Dockerfile.dockerignore deleted file mode 100644 index 44447fb..0000000 --- a/docker/Dockerfile.dockerignore +++ /dev/null @@ -1,10 +0,0 @@ -.git -__pycache__/ -*.pyc -*.pyo -*.pyd -*.db -*.log -venv/ -.env -docker/ diff --git a/docker/docker-compose-pgvector.yml b/docker/docker-compose-pgvector.yml deleted file mode 100644 index 8ad5e16..0000000 --- a/docker/docker-compose-pgvector.yml +++ /dev/null @@ -1,23 +0,0 @@ -# docker compose -f docker-compose-pgvector.yml up -# docker compose -f docker-compose-pgvector.yml down - -services: - pgvector: - image: pgvector/pgvector:pg17 - hostname: pgvector - container_name: pgvector - restart: always - ports: - - "5432:5432" - environment: - POSTGRES_USER: pgvector - POSTGRES_PASSWORD: pgvector - POSTGRES_DB: pgvector - TZ: Asia/Seoul - LANG: en_US.utf8 - volumes: - - pgvector_data:/var/lib/postgresql/data - - ./pgvector/init:/docker-entrypoint-initdb.d - -volumes: - pgvector_data: diff --git a/docker/docker-compose-postgres.yml b/docker/docker-compose-postgres.yml deleted file mode 100644 index 696f7e1..0000000 --- a/docker/docker-compose-postgres.yml +++ /dev/null @@ -1,23 +0,0 @@ -# docker compose -f docker-compose-postgres.yml up -# docker compose -f docker-compose-postgres.yml down - -services: - postgres: - image: postgres:15 - hostname: postgres - container_name: postgres - restart: always - ports: - - "5432:5432" - environment: - POSTGRES_USER: postgres - POSTGRES_PASSWORD: postgres - POSTGRES_DB: postgres - TZ: Asia/Seoul - LANG: en_US.utf8 - volumes: - - postgres_data:/var/lib/postgresql/data - - ./postgres/init:/docker-entrypoint-initdb.d - -volumes: - postgres_data: diff --git a/docker/docker-compose.yml b/docker/docker-compose.yml deleted file mode 100644 index 115575a..0000000 --- a/docker/docker-compose.yml +++ /dev/null @@ -1,34 +0,0 @@ -services: - streamlit: - hostname: streamlit - container_name: streamlit - build: - context: .. - dockerfile: docker/Dockerfile - ports: - - "8501:8501" - volumes: - - ../:/app - env_file: - - ../.env - environment: - - STREAMLIT_SERVER_PORT=8501 - - DATABASE_URL=postgresql://pgvector:pgvector@localhost:5432/streamlit - depends_on: - - pgvector - - pgvector: - image: pgvector/pgvector:pg17 - hostname: pgvector - container_name: pgvector - environment: - POSTGRES_USER: pgvector - POSTGRES_PASSWORD: pgvector - POSTGRES_DB: streamlit - ports: - - "5432:5432" - volumes: - - pgdata:/var/lib/postgresql/data - -volumes: - pgdata: diff --git a/docker/pgvector/init/001_create_database.sql b/docker/pgvector/init/001_create_database.sql deleted file mode 100644 index 2173146..0000000 --- a/docker/pgvector/init/001_create_database.sql +++ /dev/null @@ -1,2 +0,0 @@ -CREATE DATABASE lang2sql; -CREATE DATABASE test; diff --git a/docker/pgvector/init/002_create_user_and_grant.sql b/docker/pgvector/init/002_create_user_and_grant.sql deleted file mode 100644 index 8da26fb..0000000 --- a/docker/pgvector/init/002_create_user_and_grant.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE USER lang2sql WITH PASSWORD 'lang2sqlpassword'; -GRANT ALL PRIVILEGES ON DATABASE lang2sql TO lang2sql; - -CREATE USER test WITH PASSWORD 'testpassword'; -GRANT ALL PRIVILEGES ON DATABASE test TO test; diff --git a/docker/postgres/init/001_create_database.sql b/docker/postgres/init/001_create_database.sql deleted file mode 100644 index 2173146..0000000 --- a/docker/postgres/init/001_create_database.sql +++ /dev/null @@ -1,2 +0,0 @@ -CREATE DATABASE lang2sql; -CREATE DATABASE test; diff --git a/docker/postgres/init/002_create_user_and_grant.sql b/docker/postgres/init/002_create_user_and_grant.sql deleted file mode 100644 index 8da26fb..0000000 --- a/docker/postgres/init/002_create_user_and_grant.sql +++ /dev/null @@ -1,5 +0,0 @@ -CREATE USER lang2sql WITH PASSWORD 'lang2sqlpassword'; -GRANT ALL PRIVILEGES ON DATABASE lang2sql TO lang2sql; - -CREATE USER test WITH PASSWORD 'testpassword'; -GRANT ALL PRIVILEGES ON DATABASE test TO test; diff --git a/docs/BaseComponent_ko.md b/docs/BaseComponent_ko.md deleted file mode 100644 index 534be30..0000000 --- a/docs/BaseComponent_ko.md +++ /dev/null @@ -1,248 +0,0 @@ -# BaseComponent - -`BaseComponent`는 **define-by-run(순수 파이썬 제어)** 철학을 유지하면서도, 컴포넌트 실행을 **관측 가능(observable)** 하게 만들기 위한 **선택적(opt-in) 표준 레이어**입니다. - -* 파이프라인은 그냥 함수/콜러블만으로도 충분히 동작합니다. -* `BaseComponent`는 그 위에 **추적(hooks), 에러 표준화, 이름/형식 통일**을 얹어주는 역할을 합니다. - -즉, **필수는 아니지만**, 라이브러리/팀 단위 개발에서 "운영 가능한 형태"로 만들고 싶을 때 유용합니다. - ---- - -## 왜 필요한가? - -### 1) 관측성(Tracing)을 "그래프 엔진 없이" 얻기 위해 - -Lang2SQL은 그래프 엔진을 강제하지 않습니다. 대신: - -* 사용자는 Python `if/for/while`로 제어한다. -* 라이브러리는 관측성은 **hook 이벤트**로 제공한다. - -`BaseComponent`는 각 컴포넌트 실행의 `start/end/error`를 이벤트로 남깁니다. - -### 2) 에러를 "도메인 친화적으로" 정리하기 위해 - -현실에서는 `ValueError`, `KeyError`, 외부 라이브러리 예외 등이 섞여서 올라옵니다. - -`BaseComponent`는: - -* `Lang2SQLError`(ValidationError, IntegrationMissingError 등)는 **그대로 유지** -* 그 외 예외는 `ComponentError`로 **표준 래핑**(+ 원인 예외를 `cause`로 보존) - -→ 사용자/운영자 관점에서 "어디서 터졌는지"가 분명해집니다. - -### 3) "컴포넌트 단위 표준"을 만들기 위해 - -라이브러리 제공 컴포넌트를 모두 BaseComponent 기반으로 만들면: - -* 로그/트레이스의 포맷이 통일 -* 테스트/디버깅 경험이 일정 -* 문서/타입 힌트가 일관 - ---- - -## BaseComponent가 제공하는 API - -### 생성자 - -```python -BaseComponent(name: str | None = None, hook: TraceHook | None = None) -``` - -* `name`: 이벤트에 찍힐 컴포넌트 이름 (기본값: 클래스명) -* `hook`: 이벤트 수신자. 기본값은 `NullHook()` (아무것도 하지 않음) - -### 구현해야 하는 것: `_run()` - -서브클래스는 `_run()`을 구현합니다. 인자 타입과 반환 타입은 각 컴포넌트에 맞게 자유롭게 정의합니다. - -```python -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"]] -``` - -### 호출: `run()` / `__call__` - -`comp.run(query)` 또는 `comp(query)`를 호출하면 내부적으로 아래를 자동 수행합니다. - -* `component.run start 이벤트 발행` -* `self._run(...)` 실행 -* 성공 시 `end 이벤트` + `duration_ms` -* 실패 시 `error 이벤트` - - * 도메인 예외(`Lang2SQLError`)는 그대로 raise - * 그 외 예외는 `ComponentError`로 래핑해서 raise - ---- - -## 타입 인자 패턴 - -Lang2SQL의 컴포넌트는 **명시적 타입 인자**를 받고, **명시적 타입 결과**를 반환합니다. - -```python -# 라이브러리 내장 컴포넌트 시그니처 예시 -KeywordRetriever._run(query: str) -> list[CatalogEntry] -SQLGenerator._run(query: str, schemas: list[CatalogEntry], context: str = "") -> str -SQLExecutor._run(sql: str) -> list[dict] -``` - -### 구성(config)은 `__init__`에, 요청별 데이터는 `_run()` 인자에 - -```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를 쓰는 게 좋은 경우 - -* 라이브러리 기본 제공 컴포넌트(retriever/generator/executor) -* 팀/제품 환경에서 **관측성(트레이싱)이 필요한 경우** -* 예외 표준화가 중요한 경우(운영/테스트/디버깅) - -### BaseComponent 없이 함수로 두는 게 좋은 경우 - -* `policy`, `eval`, metric 계산처럼 **순수 함수 성격**이 강한 로직 -* "유저가 빠르게 붙여 넣어 쓰는" 초경량 커스텀 로직 -* 실행 단위가 너무 작아 이벤트가 과도해지는 경우 - -즉, **핵심 파이프라인 축**은 BaseComponent로 잡고, -그 외의 작은 로직은 함수로 두는 혼합형이 가장 자연스럽습니다. - ---- - -## 커스텀 컴포넌트 예시 - -```python -from lang2sql.core.base import BaseComponent - -class UpperCaseSQL(BaseComponent): - """SQL을 대문자로 변환하는 후처리 컴포넌트.""" - def _run(self, sql: str) -> str: - return sql.upper() - -upper = UpperCaseSQL() -print(upper.run("select 1")) # SELECT 1 -``` - -hook을 주입하면 실행 추적도 자동으로 됩니다: - -```python -from lang2sql import MemoryHook - -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) 시스템 - -### Hook이란? - -컴포넌트/플로우 실행 시점에 **이벤트(Event)** 를 받는 인터페이스입니다. - -* `start/end/error` 시점 기록 -* 소요 시간(duration_ms) -* 입력/출력 요약(input_summary/output_summary) - -### 어디서 확인하나? - -가장 쉬운 건 `MemoryHook`입니다. - -```python -from lang2sql import MemoryHook, HybridNL2SQL - -hook = MemoryHook() -pipeline = HybridNL2SQL(catalog=catalog, llm=llm, db=db, embedding=embedding, hook=hook) -pipeline.run("지난달 매출") - -for e in hook.snapshot(): - print(e.phase, e.component, e.duration_ms, e.error) -``` - -### 운영용 관측성은 어디서 제어하나? - -운영에서는 `MemoryHook` 대신 다음이 일반적입니다. - -* 로그로 흘리는 Hook (stdout / JSON log) -* APM/Tracing으로 보내는 Hook (OpenTelemetry span 등) -* 필터링 Hook (특정 컴포넌트만 샘플링) - -핵심은: **관측성은 hook 구현체에서 제어**하고, 파이프라인/컴포넌트 코드는 최대한 "비즈니스 로직"만 갖도록 분리합니다. - ---- - -## 중첩(서브플로우/래핑)하면 트레이싱이 깨지나? - -"깨진다"기보다는 **이벤트가 더 많이 찍힙니다.** - -* `flow_b` 안에 `flow_a`를 step으로 넣으면 - - * `flow_b` 이벤트 2개(시작/끝) - * `flow_a` 이벤트 2개(시작/끝) - * `a1/a2` 컴포넌트 이벤트도 각각 찍힘(컴포넌트가 BaseComponent라면) - -이게 싫다면 두 가지 선택지가 있습니다. - -1. **상위 레벨(Flow)만 트레이싱하고 내부는 함수로 둔다** -2. **Hook에서 필터링/샘플링한다** (예: component 이름 prefix로 제외) - -추가 문법 없이 해결하려면 2번이 가장 현실적입니다. - ---- - -## 베스트 프랙티스 - -### 1) 구성(config)은 `__init__`에, 요청별 데이터는 `_run()` 인자에 - -고정 설정(모델, 카탈로그, DB 연결 등)은 생성자에서 받고, -요청마다 달라지는 값(쿼리, 스키마 목록 등)은 `_run()` 인자로 전달합니다. - -### 2) `_run()`의 반환값은 명시적으로 - -반환 타입을 명확히 정의하면 Flow에서 컴포넌트를 조합할 때 안전합니다. - -### 3) "작은 로직(policy/eval)은 그냥 함수" - -* BaseComponent로 감싸는 건 선택 -* 운영에서 꼭 추적이 필요할 때만 감싼다 - ---- - -## FAQ - -### Q. "그냥 함수만 써도 되는데 왜 굳이 BaseComponent?" - -A. **운영/디버깅/협업에서** 차이가 큽니다. -문제 났을 때 "어디서, 어떤 입력으로, 얼마나 걸리다, 어떤 에러로" 터졌는지 자동으로 남는 게 핵심 가치입니다. - -### Q. "BaseComponent를 유저가 직접 써야 하나?" - -A. 필수 아닙니다. -초급 유저는 **프리셋 Flow + 프리셋 컴포넌트**만으로 충분히 쓰게 하고, -고급/운영 유저에게 BaseComponent/Hook을 제공하는 구성이 가장 자연스럽습니다. - ---- diff --git a/docs/BaseFlow_ko.md b/docs/BaseFlow_ko.md deleted file mode 100644 index 9ed9120..0000000 --- a/docs/BaseFlow_ko.md +++ /dev/null @@ -1,191 +0,0 @@ -# BaseFlow - -`BaseFlow`는 Lang2SQL에서 **define-by-run(순수 파이썬 제어)** 철학을 구현하기 위한 "플로우의 최소 추상화(minimal abstraction)"입니다. - -* 파이프라인의 **제어권(control-flow)** 을 프레임워크 DSL이 아니라 **사용자 코드(Python)** 가 갖습니다. -* 그래프 엔진을 강제하지 않습니다. -* 대신, 실행 단위를 `Flow`로 묶고 **관측성(hooks)** 과 **에러 규약**을 통일합니다. - ---- - -## 왜 필요한가? - -### 1) "제어는 파이썬으로"를 지키기 위해 - -Text2SQL은 현실적으로 다음 제어가 자주 필요합니다. - -* 재시도 루프 (`while`, `for`) -* 조건 분기 (`if`, `match`) -* 부분 파이프라인(서브플로우) 호출 -* 정책(policy) 기반 행동 결정 - -`BaseFlow`는 이런 제어를 **사용자가 Python으로 직접 작성**하게 두고, 라이브러리는 "실행 컨테이너 + 관측성"만 제공합니다. - -### 2) 요청 단위 관측성(Flow-level tracing) - -운영/디버깅에서는 "이 요청 전체가 언제 시작했고, 어디서 실패했고, 얼마나 걸렸는지"가 먼저 중요합니다. - -`BaseFlow`는 다음 이벤트를 발행합니다. - -* `flow.run` start / end / error -* 실행 시간(`duration_ms`) - -→ 요청 1건을 **Flow 단위로 빠르게 파악**할 수 있습니다. - ---- - -## BaseFlow가 제공하는 API - -### 1) 구현해야 하는 것: `_run()` - -```python -class MyFlow(BaseFlow): - def _run(self, query: str) -> list[dict]: - ... - return result -``` - -* Flow의 본체 로직은 여기에 작성합니다. -* 제어는 Python으로 직접 작성합니다. (`if/for/while`) -* 입출력 타입은 자유롭게 정의합니다. - -### 2) 호출: `run()` / `__call__` - -```python -out = flow.run("지난달 매출") -# 또는 -out = flow("지난달 매출") -``` - -* 내부적으로 `_run(...)`을 호출합니다. -* hook 이벤트를 `start/end/error`로 기록합니다. - ---- - -## 사용 패턴 - -### 1) 초급: 프리셋 Flow로 바로 실행 - -초급 사용자는 보통 "구성만 하고 실행"하면 됩니다. - -```python -pipeline = BaselineNL2SQL(catalog=catalog, llm=llm, db=db) -rows = pipeline.run("지난달 매출") -``` - -### 2) 고급: CustomFlow로 제어(while/if/policy) - -정책/루프/재시도 같은 제어가 들어오면 `BaseFlow`를 직접 상속해 작성하는 것이 가장 깔끔합니다. - -```python -class RetryFlow(BaseFlow): - def _run(self, query: str) -> str: - for _ in range(3): - schemas = retriever(query) - sql = generator(query, schemas) - if validator(sql): - return sql - return sql -``` - ---- - -## Hook(Tracing)은 어디서 확인하나? - -Flow도 hook을 받을 수 있습니다. - -```python -from lang2sql import MemoryHook, BaselineNL2SQL - -hook = MemoryHook() -pipeline = BaselineNL2SQL(catalog=catalog, llm=llm, db=db, hook=hook) - -rows = pipeline.run("지난달 매출") - -for e in hook.snapshot(): - print(e.name, e.phase, e.component, e.duration_ms, e.error) -``` - -운영에서는 `MemoryHook` 대신 로그/OTel/필터링 훅을 사용합니다. -관측성 제어는 **hook 구현체에서** 담당하고, Flow 코드는 비즈니스 로직에 집중하도록 분리합니다. - ---- - -## (관련 개념) BaseFlow와 BaseComponent의 관계 - -* `BaseFlow`는 "어떻게 실행할지(제어/조립)"를 담당합니다. -* `BaseComponent`는 "한 단계에서 무엇을 할지(작업 단위)"를 담당합니다. - -일반적으로: - -* **Flow는 여러 Component를 호출**합니다. -* **전용 Flow(BaselineNL2SQL 등)는 Component 간 와이어링을 내부에서 처리**합니다. - -즉, **Flow가 상위 레벨 오케스트레이션**, Component가 **재사용 가능한 부품**입니다. - ---- - -## 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가 필수인가? - -A. Flow라는 개념은 사실상 필요하지만, **모든 사용자가 BaseFlow를 직접 상속할 필요는 없습니다.** - -* 초급: 프리셋 Flow(`BaselineNL2SQL`, `HybridNL2SQL`, `EnrichedNL2SQL`)만 사용 -* 고급: `BaseFlow`를 상속해서 제어를 직접 작성 - -### Q. Flow의 반환 타입은? - -A. `_run()`의 입출력 타입은 자유롭습니다. 컴포넌트끼리 합의한 타입을 그대로 사용하면 됩니다. diff --git a/docs/Core_concept_ko.md b/docs/Core_concept_ko.md deleted file mode 100644 index 13b9f00..0000000 --- a/docs/Core_concept_ko.md +++ /dev/null @@ -1,99 +0,0 @@ -# Core Concepts - -Lang2SQL은 "그래프 엔진/DSL"을 강제하지 않고, **순수 Python 코드로 파이프라인을 제어**하는 define-by-run 철학을 따릅니다. -각 컴포넌트는 **명시적 타입 인자**를 받고, 명시적 타입 결과를 반환합니다. - ---- - -## 1) Define-by-run: 제어는 Python으로 - -Lang2SQL에서 파이프라인 제어는 프레임워크가 아니라 **사용자 코드가 가집니다.** - -* 분기: `if / match` -* 반복/재시도: `for / while` -* 조건부 실행: policy 기반 action -* 서브플로우: flow를 step처럼 호출 - -예시: - -```python -retriever = KeywordRetriever(catalog=catalog) -generator = SQLGenerator(llm=llm, db_dialect="sqlite") - -while True: - schemas = retriever.run(query) - sql = generator.run(query, schemas) - if validator(sql): - break - -rows = executor.run(sql) -``` - -**핵심:** Lang2SQL은 위 패턴을 "프레임워크 문법"으로 바꾸지 않습니다. -그냥 Python으로 쓰되, 각 컴포넌트의 입출력이 **타입으로 명확히 정의**되어 있어 안전하게 조합할 수 있습니다. - ---- - -## 2) 타입 인자 패턴 - -Text2SQL 파이프라인은 현실적으로 단계가 늘어납니다. - -* retriever 1개가 아니라 10개, 100개가 될 수 있음 -* 중간 산출물(선택된 테이블, 컨텍스트, 후보 SQL, 검증 결과, 점수/메트릭)이 늘어남 -* loop/branch가 들어가면서 "어떤 단계에서 무엇이 생성되었는지" 추적이 어려워짐 - -Lang2SQL은 각 컴포넌트의 `_run()` 메서드가 **명시적 타입 인자를 받고 타입 결과를 반환**하도록 설계합니다. - -``` -KeywordRetriever._run(query: str) -> list[CatalogEntry] -SQLGenerator._run(query: str, schemas: list[CatalogEntry], context: str) -> str -SQLExecutor._run(sql: str) -> list[dict] -``` - -이 방식의 장점: - -* 각 컴포넌트의 입출력이 코드에 명확히 드러남 -* IDE 자동완성과 타입 체크를 활용할 수 있음 -* 컴포넌트를 독립적으로 테스트하기 쉬움 - -### 컴포넌트 간 데이터 전달 - -컴포넌트 간 와이어링은 **전용 Flow가 내부에서 처리**합니다. - -```python -# BaselineNL2SQL._run() 내부 구현 -def _run(self, query: str) -> list[dict]: - schemas = self._retriever(query) # list[CatalogEntry] - sql = self._generator(query, schemas) # str - return self._executor(sql) # list[dict] -``` - -사용자 관점에서는 Flow의 `run()` 하나만 호출하면 됩니다: - -```python -rows = pipeline.run("지난달 매출") -``` - ---- - -## 3) 컴포넌트 vs 플로우 - -| | BaseComponent | BaseFlow | -|---|---|---| -| 역할 | 단일 작업 단위 (검색, 생성, 실행) | 여러 컴포넌트의 조합/제어 | -| 구현 | `_run()` 메서드 | `_run()` 메서드 | -| 관측성 | `component.run` 이벤트 | `flow.run` 이벤트 | -| 예시 | `KeywordRetriever`, `SQLGenerator` | `BaselineNL2SQL`, `HybridNL2SQL` | - -둘 다 **`_run()`에 비즈니스 로직**을 작성하고, `run()` / `__call__()` 호출 시 자동으로 hook 이벤트를 발행합니다. - ---- - -## 권장 규약 요약 - -* **제어는 Python으로 한다** (define-by-run) -* **컴포넌트의 입출력은 명시적 타입 인자로 정의한다** (`_run(query: str) -> list[CatalogEntry]`) -* **구성(config)은 `__init__`에, 요청별 데이터는 `_run()` 인자에** -* policy/eval처럼 관측성이 불필요한 로직은 **순수 함수로 둬도 된다** - ---- diff --git a/docs/Hook_and_exception_ko.md b/docs/Hook_and_exception_ko.md deleted file mode 100644 index ccc9c14..0000000 --- a/docs/Hook_and_exception_ko.md +++ /dev/null @@ -1,311 +0,0 @@ -# Hooks (Tracing) - -Lang2SQL의 hooks 시스템은 **그래프 엔진 없이도 관측성(observability)을 제공**하기 위한 최소 레이어입니다. -Flow/Component 실행 과정에서 이벤트를 발행하고, 사용자는 hook 구현체로 이를 수집/출력/전송할 수 있습니다. - -핵심 컨셉은 단 하나입니다: - -> **“실행 중 무슨 일이 일어났는지(Event)를 hook이 받는다.”** - ---- - -## Event - -`Event`는 Flow/Component 실행 중 발생한 “관측 단위”입니다. - -```py -@dataclass -class Event: - name: str # e.g., "component.run" / "flow.run" - component: str # e.g., "KeywordTableRetriever" / "SequentialFlow" - phase: Literal["start", "end", "error"] - ts: float # unix timestamp - duration_ms: Optional[float] = None - - input_summary: Optional[str] = None - output_summary: Optional[str] = None - error: Optional[str] = None - - data: dict[str, Any] = field(default_factory=dict) -``` - -### 필드 의미 - -* `name` - - * 이벤트 종류를 나타내는 문자열 - * 예: `"component.run"`, `"flow.run"` -* `component` - - * 이벤트를 발생시킨 실행 단위 이름 - * 예: `"KeywordTableRetriever"`, `"SequentialFlow"` -* `phase` - - * `"start" | "end" | "error"` -* `ts` - - * 이벤트 발생 시간(Unix timestamp) -* `duration_ms` - - * `end/error`에서만 주로 채움(실행 시간) -* `input_summary`, `output_summary` - - * 디버깅을 위한 “사람이 읽기 쉬운” 요약 문자열 -* `error` - - * 실패 시 오류 요약 문자열 -* `data` - - * UI/필터링/테스트/추가 메타를 위한 구조화 payload - * 기본은 빈 dict이며, 필요할 때만 채우는 것을 권장합니다. - ---- - -## TraceHook - -`TraceHook`은 이벤트를 받는 인터페이스입니다. - -```py -class TraceHook(Protocol): - def on_event(self, event: Event) -> None: ... -``` - -* Lang2SQL의 Flow/Component는 실행 시점에 `hook.on_event(Event(...))` 형태로 이벤트를 발행합니다. -* hook은 **옵션**이며, 없으면 `NullHook`이 사용됩니다. - ---- - -## 기본 Hook 구현체 - -### NullHook - -```py -class NullHook: - def on_event(self, event: Event) -> None: - return -``` - -* 기본값 -* 아무 것도 하지 않습니다. -* hook 비용을 없애고 싶을 때 항상 안전한 기본 구현입니다. - -### MemoryHook - -```py -class MemoryHook: - def __init__(self) -> None: - self.events: list[Event] = [] - - def on_event(self, event: Event) -> None: - self.events.append(event) - - def clear(self) -> None: - self.events.clear() - - def snapshot(self) -> list[Event]: - return list(self.events) -``` - -* 이벤트를 메모리에 누적합니다. -* 테스트/디버깅에 가장 유용합니다. - -#### MemoryHook 사용 예시 - -```python -from lang2sql.core.hooks import MemoryHook -from lang2sql.flows.baseline import SequentialFlow - -hook = MemoryHook() -flow = SequentialFlow(steps=[...], hook=hook) - -out = flow.run("지난달 매출") - -for e in hook.snapshot(): - print(e.name, e.phase, e.component, e.duration_ms, e.error) -``` - -#### clear()를 유저가 직접 호출해야 하나? - -* 보통은 **테스트에서만** `clear()`가 필요합니다. (케이스 간 이벤트 섞임 방지) -* 일반 사용자는 보통 “요청 1회 → hook 1개 생성” 패턴으로 충분합니다. - -예: - -```py -hook = MemoryHook() -out = flow.run_query("q") # 여기서만 쓰고 끝 -events = hook.snapshot() -``` - ---- - -## 유틸 함수 - -### now() - -```py -def now() -> float: - return time.time() -``` - -* timestamp 생성에 사용됩니다. - -### ms() - -```py -def ms(start: float, end: float) -> float: - return (end - start) * 1000.0 -``` - -* duration(ms) 계산에 사용됩니다. - -### summarize() - -```py -def summarize(x: Any, max_len: int = 240) -> str: - ... -``` - -* repr(x)를 기반으로 요약 문자열을 만들고 길이를 제한합니다. -* 이벤트의 `input_summary/output_summary`에 사용됩니다. - ---- - -## 운영(Production)에서는 어떻게 쓰나? - -MemoryHook은 테스트용입니다. 운영에서는 보통 다음 형태로 확장합니다. - -* `LoggingHook`: JSON 로그로 남기기 -* `OTelHook`: OpenTelemetry span으로 전송 -* `FilteringHook`: 특정 component만 샘플링/필터링 - -관측성 제어는 **hook 구현체에서** 하고, Flow/Component 로직은 비즈니스에 집중하는 것이 기본 철학입니다. - ---- - -# Exceptions - -Lang2SQL 예외 시스템은 두 목표를 가집니다. - -1. **도메인 에러는 도메인 타입으로 유지**한다. -2. 외부/일반 예외는 “어디서 터졌는지”가 보이도록 **표준 래핑**한다. - ---- - -## Lang2SQLError (Base) - -```py -class Lang2SQLError(Exception): - """Base error for lang2sql.""" -``` - -* Lang2SQL에서 발생하는 모든 도메인 예외의 베이스입니다. -* `BaseComponent` / `BaseFlow`는 일반적으로 **Lang2SQLError는 그대로 다시 raise**합니다. - ---- - -## IntegrationMissingError - -```py -class IntegrationMissingError(Lang2SQLError): - def __init__(self, integration: str, extra: str | None = None, hint: str | None = None): - ... -``` - -### 언제 발생? - -* 선택적 의존성(optional integration)이 필요한데 설치되어 있지 않을 때 - -예: - -* `faiss` retriever를 쓰는데 `faiss`가 설치되어 있지 않음 - -### 메시지 특징 - -* `extra`가 있으면 설치 힌트를 포함합니다. - -예 메시지: - -* `Missing optional integration: faiss. Install with: pip install 'lang2sql[faiss]'` - ---- - -## ValidationError - -```py -class ValidationError(Lang2SQLError): - pass -``` - -### 언제 발생? - -* SQL 검증 실패, 정책상 금지 쿼리, 스키마 불일치 등 -* “유저 입력/생성 결과가 유효하지 않다”에 해당하는 에러를 담는 대표 도메인 예외 - ---- - -## ContractError - -```py -class ContractError(Lang2SQLError): - """Raised when a component violates a required call/return contract.""" - pass -``` - -### 언제 발생? - -* Lang2SQL이 요구하는 호출/반환 계약을 위반했을 때 -* 예: `_run()`이 반드시 반환해야 하는 타입과 다른 값을 반환 - -이 에러는 “사용자 코드 버그를 빨리 발견(fail-fast)”하기 위한 타입입니다. - ---- - -## ComponentError - -```py -class ComponentError(Lang2SQLError): - def __init__(self, component: str, message: str, *, cause: Exception | None = None): - self.component = component - self.cause = cause - super().__init__(f"[{component}] {message}") -``` - -### 목적 - -* “일반 예외(ValueError, KeyError 등)”를 도메인 레이어로 끌어올 때 사용합니다. -* 어떤 컴포넌트에서 터졌는지 식별 가능하게 만듭니다. - -### cause - -* 원본 예외를 보존합니다. -* 테스트/디버깅에서 error chain을 확인할 수 있습니다. - ---- - -## 예외가 Flow/Component에서 어떻게 처리되나? - -(현재 BaseComponent 설계 기준) - -* `Lang2SQLError` 계열 - - * 그대로 이벤트에 기록하고 그대로 raise -* 그 외 모든 예외 - - * 이벤트에 기록하고 `ComponentError(..., cause=e)`로 래핑하여 raise - -즉: - -* **도메인 예외는 “정상적인 실패”로 취급** -* **일반 예외는 “버그/예상 밖 실패”로 표준화** - ---- - -## 권장 사용 가이드 - -* “사용자 입력/정책/검증 실패”는 `ValidationError` -* “의존성 설치 문제”는 `IntegrationMissingError` -* “계약 위반(반환 타입/호출 규약)”은 `ContractError` -* “외부 라이브러리/예상 밖 예외”는 `ComponentError`로 래핑되어 올라오는 것을 기본으로 합니다. - ---- diff --git a/docs/PROJECT.md b/docs/PROJECT.md new file mode 100644 index 0000000..7b71dd8 --- /dev/null +++ b/docs/PROJECT.md @@ -0,0 +1,152 @@ +# Lang2SQL — 프로젝트 SSOT + +> *"질문하면 SQL 짜주는 봇이 아니라, 현실의 messy함에 견디는 분석 에이전트."* + +이 문서는 이 프로젝트가 *무엇이고, 왜 존재하며, 지금 어디까지 와 있는지*를 **단일하게** 설명합니다. 다른 모든 문서·README·디자인노트는 이 문서를 참조하거나 보충합니다. + +--- + +## 1. 한 줄 정체성 + +**Lang2SQL**은 *문서로 비즈니스 맥락을 학습하고, 팀별로 시멘틱이 분기되고, 불완전한 DB에서도 답하고, 모든 정의·대화를 기억하는* 오픈소스 분석 에이전트입니다. Phase 1 인터페이스는 **Discord**. + +--- + +## 2. 왜 존재하는가 + +Vanna AI(~20k★), Wren AI(~12k★), SQLCoder 같은 Text-to-SQL 오픈소스들은 *질문→SQL 파이프라인* 자체는 이미 잘 풉니다. "더 좋은 SQL 생성"은 모델 fine-tuning 싸움이고, 그 영역엔 들어가지 않습니다. + +대신 *실무에 넣어보면 진짜 막히는* **현실의 지저분함 4가지**를 다룹니다: + +| 약점 | 기존 처리 | 우리 해결 | +|---|---|---| +| DB 메타데이터가 비어 있다 | Vanna: 학습 데이터 의존 | ★① **DB 강건성**: safety pipeline + 자동 보강 (V1.5) | +| 봇이 어제 한 얘기를 못 기억한다 | 대부분 stateless | ★② **Hermes 기억**: 3축 분리(Store/Recall/Extractor) | +| 비즈니스 정의를 사람이 일일이 입력 | Wren: MDL 수동 | ★③ **Ingestion 매트릭스**: 문서 → 시멘틱 후보 | +| 같은 *"활성 사용자"*가 팀마다 다르다 | Wren: 단일 MDL → 충돌 | ★④ **Semantic federation**: git-like 분기, 가장 구체적 scope 승리 | + +이 4가지는 *비즈니스마다 다르기 때문에 벤치마크가 안 나오는 영역* → 그래서 오픈소스가 안 건드림 → **그래서 기회**. + +--- + +## 3. 무엇을 다르게 하는가 — 4기둥 + +| 기둥 | 한 줄 | 자세히 | +|---|---|---| +| **★① Safety pipeline** | 모든 SQL이 통과해야 하는 *공항 보안 검색대* | layer를 줄 세우는 패턴 — 새 검사(예: AST 검증, 함수 차단)는 한 칸 끼우기 | +| **★② Memory 3축** | Store/Recall/Extractor 각각 독립 진화 | V1엔 in-memory/inject-all/manual, V1.5엔 SQLite/keyword/auto | +| **★③ Ingestion matrix** | Source × Extractor 자유 조합 | 파일×LLM이 V1, URL/Notion/DDL은 V1.5+ | +| **★④ Semantic federation** | git처럼 팀별 정의 분기, *가장 구체적이 승리* | 충돌이 사라짐. Wren의 "한 회사 한 MDL"이 못 푸는 영역 | + +**핵심 메타원칙**: 모든 외부 시스템 의존성을 *포트(Protocol)*로 추상화. *어댑터*는 가장자리에만. 그래서 새 LLM / 새 DB / 새 frontend 추가가 *기존 코드 안 건드리고 끼우기*로 끝남. + +--- + +## 4. 지금 어디까지 와 있는가 — 정직한 현황 + +### ✅ V1 완료 (master에서 동작) +- **core 포트 11종** — 모든 외부 의존을 Protocol로 추상화 +- **harness** — agent_loop(LLM → tool → 다음 턴), Session, HarnessContext +- **★①~★④ 4기둥** 최소 구현 — safety 12 회귀, memory 3축, ingestion 매트릭스, federation 3-scope +- **도구 6종** — run_sql · explore_schema · define_metric · remember · ask_user · ingest_doc +- **Discord 프론트엔드** — 6개 슬래시 명령 + `/setup` 위저드 (비개발자 DSN-free flow) + bot.py +- **영속화** — SQLite 시멘틱 store + Fernet 실암호화 secrets +- **DB 어댑터** — `SqlAlchemyExplorer` 1개로 Postgres/MySQL/Snowflake/BigQuery/DuckDB 커버 + Cloudflare D1 HTTP 어댑터 + `build_explorer(DSN)` 자동 라우팅 +- **106개 자동화 테스트** (safety 회귀 12 포함) +- **bench 데모** — federation + safety 라이브 시연 (`bench/ecommerce_demo.py`) + +### ⚠️ Stub / 미검증 +| 항목 | 상태 | +|---|---| +| PostgreSQL 실 연결 | psycopg 어댑터는 있음. 실 PG 테스트 미수행 | +| 메타데이터 자동 보강 (★①의 핵심 차별점) | V1.5 | +| 키워드/벡터 recall | V1.5/V2 | +| LLM 자동 fact 추출 | V1.5 | +| `/semantic diff`, `/semantic promote` | V1.5 | +| URL/Notion 문서 입력 | V1.5/V2 | +| Slack/Web frontend | Phase 2/3 | +| Audit hash chain | V2 | + +--- + +## 5. 로드맵 + +``` +V1 ✅ 골격 + 4기둥 최소 + Discord 어댑터 + 영속화 ← 지금 +V1.5 → 메타데이터 자동 보강(★①) + 키워드 recall + + LLM 자동 fact 추출 + /semantic diff·promote + + URL/DDL ingestion + 회귀 강화 +V2 → 벡터 recall + 비용 게이트(EXPLAIN) + Notion MCP + + 외부 git semantic 동기화 + Slack frontend +V2.5 → PostgreSQL 멀티인스턴스 + branch fork/merge UI + + Web frontend +``` + +각 단계의 디테일은 [`docs/discord_first_redesign_v4_1.md`](./discord_first_redesign_v4_1.md) §3. + +--- + +## 6. 빠른 시작 + +```bash +git clone https://github.com/CausalInferenceLab/Lang2SQL.git +cd Lang2SQL +uv sync # 기본 deps +.venv/bin/pytest -q # 106 테스트 +.venv/bin/python bench/ecommerce_demo.py # federation + safety 데모 +``` + +Discord 봇 운영: [`docs/DEPLOY.md`](./DEPLOY.md) + +--- + +## 7. 아키텍처 & 기여 + +- **아키텍처 한눈 가이드 + 어디 손대면 좋은지**: [`docs/ARCHITECTURE.md`](./ARCHITECTURE.md) +- **PR 작성 형식**: [`docs/pull_request_guidelines.md`](./pull_request_guidelines.md) +- **브랜치 전략**: [`docs/branch_guidelines.md`](./branch_guidelines.md) + +기여 PR을 가장 받기 쉬운 지점들 (자세한 위치/방법은 ARCHITECTURE.md §5): +- 새 LLM 어댑터 (`adapters/llm/.py`) +- 새 safety layer (`safety/layers/.py`) +- 새 memory recall 전략 (`memory/recall/.py`) +- 새 ingestion source (`ingestion/sources/.py`) +- 새 frontend (`frontends//`) +- 새 도구 (`tools/.py`) + +--- + +## 8. 핵심 설계 결정 (왜 이 길을 택했나) + +| 결정 | 이유 | +|---|---| +| **백지 재작성** (LangGraph/Streamlit 파이프라인 → ports & adapters 에이전트) | 파이프라인 위에 4기둥을 얹는 것보다, 4기둥을 *전제*로 새로 짓는 게 깔끔함 | +| **포트 & 어댑터** (콘센트와 가전) | V1엔 단순 구현 1개씩, 어댑터 추가는 기존 코드 안 건드림. *연구실 코드가 곧 제품 layer* | +| **Discord 1급 frontend, 나머지는 추상** | "오픈소스 분석 봇"의 자연스러운 거주지. Slack/Web은 어댑터 추가 | +| **"강건성"을 두 축으로 분리** (DB ★① + 시멘틱 ★④) | 실무에선 후자가 더 자주 터진다는 발견. 학계엔 ★①, 거버넌스엔 ★④ | +| **federation = git-like 분기** | "한 회사 한 정의"는 조직 현실과 충돌. 각자 scope에서 살게 함 | +| **Read-only를 fail-closed로 강제** | safety pipeline의 whitelist는 SELECT/WITH 외 BLOCK. DROP/INSERT가 모델 환각으로 새는 사고 방지 | +| **stdlib → 필요 시 lean dep** | 초기엔 의존성 0(urllib OpenAI 어댑터), V1.5에서 cryptography/discord.py만 핀 | + +--- + +## 9. 프로젝트 메타 + +- **License**: [MIT](https://opensource.org/licenses/MIT) +- **운영**: [가짜연구소](https://pseudo-lab.com/) 인과추론팀 +- **커뮤니티**: [Discord](https://discord.gg/EPurkHVtp2) +- **이슈/기능 요청**: [GitHub Issues](https://github.com/CausalInferenceLab/Lang2SQL/issues) +- **백업**: 옛 v0.3 아키텍처는 `archive/pre-v4.1-rebuild` 태그로 복원 가능 + +--- + +## 10. 변천 (간단) + +| 시기 | 사건 | +|---|---| +| ~v0.3 | LangGraph + Streamlit 파이프라인 (질문→retrieval→gate→generation→execution) | +| 2026 봄 | **방향 전환**: Vanna/Wren도 이미 잘 푸는 영역에서 경쟁 그만, "현실 robustness"로 이동 | +| 2026-05 | v4.1 plan 확정 → ports & adapters로 백지 재작성 (PR #227–#230) | +| (지금) | V1 master 안착. 다음은 V1.5 — ★①의 *진짜 차별점*인 메타데이터 자동 보강 | + +— *"더 똑똑한 SQL 생성기가 아니라, 현실의 messy함에 견디는 도구."* diff --git a/docs/discord_first_redesign.md b/docs/discord_first_redesign.md deleted file mode 100644 index 0363975..0000000 --- a/docs/discord_first_redesign.md +++ /dev/null @@ -1,611 +0,0 @@ -# Lang2SQL — Discord-First 재설계 명세 - -> **작성일**: 2026-05-18 -> **결정자**: ryan@brain-crew.com -> **상태**: 골격 승인. §10 잔여 결정 항목 보완 후 Week 1 착수. -> **범위**: 현 `lang2sql/` 트리를 비우고 본 문서대로 새로 채운다. - ---- - -## 0. 제품 한 줄과 타이브레이커 - -> **"Discord에서 쓰는 read-only · audit-by-default SQL 에이전트. -> DB는 절대 변하지 않고, 모든 쿼리는 예산/시간/행 한도 안에서 돈다. -> 대화 맥락은 끊겨도 영속된다."** - -설계 충돌 시 우선순위: - -1. **Discord UX가 단순한가?** -2. **DB는 안 깨지는가?** -3. **맥락은 보존되는가?** - -이 3가지에 부정으로 답하는 어떤 추상화도 도입하지 않는다. - ---- - -## 1. 확정된 기술 결정 - -| # | 항목 | 선택 | 근거 | -|---|---|---|---| -| 1 | Discord lib | **`discord.py` 2.x** | 가장 성숙·안정. 문서/예제 풍부. 비동기 native. | -| 2 | DB 엔진 타깃 (V1) | **PostgreSQL** | EXPLAIN·statement_timeout·RO role·sslmode 등 모든 safety 기법이 정직하게 동작. MySQL/SQLite/BigQuery는 V2 이후. | -| 3 | 세션 영속화 | **Hermes-style write-through** | 매 메시지·매 도구 호출/결과·매 pause·매 audit 항목을 즉시 디스크에 flush. 봇 재시작/배포/네트워크 단절 후 다시 붙어도 동일 맥락 복구. | -| 4 | 세션 + 자격증명 스토어 | **AES-GCM 암호화된 SQLite 단일 파일** | 트라이얼 단계 무료·무의존. JSON1으로 conversation 저장. 외부 secret manager는 V2 옵션. | -| 5 | LLM 백엔드 | **OpenAI + NVIDIA NIM 듀얼 어댑터** | NIM endpoint가 OpenAI 호환(`/v1/chat/completions`) → 어댑터 거의 그대로 재사용. 슬래시 명령 `/model` 로 유저 전환. | -| 6 | Go TUI | **유지 (개발 도구)** | Discord 불가 환경 fallback. `serve.py` NDJSON 인터페이스 유지. | -| 7 | 차트 출력 | **PNG 첨부 + 자동 페이지네이션** | matplotlib + Pillow. Discord 첨부 한도(무료 8MB/메시지, 임베드 10개)에 맞춰 잘라 보냄. | -| 8 | 호스팅 (트라이얼) | **소형 VPS** | §1.1 참조. Cloudflare Workers 부적합. | - -### 1.1 호스팅 — "Cloudflare 5천원" 건에 대한 솔직한 답 - -`discord.py` 는 Discord WebSocket gateway에 **계속 붙어 있어야 하는 장기 실행 Python 프로세스**입니다. Cloudflare Workers의 edge runtime은 짧은 HTTP 요청-응답 모델이라 봇 본체를 못 올립니다. (D1/KV/R2 같은 backing store로는 활용 가능하지만 V1 범위 밖.) - -**저렴·트라이얼 권장 옵션** - -| 옵션 | 비용 | 메모 | -|---|---|---| -| **Oracle Cloud Always Free** | $0 (무기한) | ARM Ampere 4코어/24GB. 가성비 최강. 가입 시 카드 검증. | -| **fly.io shared-cpu-1x** | $0 ~ $5/월 | 256MB로 시작 가능. 한국에서 가까운 region(NRT/SIN). | -| **Hetzner CX11** | €4.5/월 (~6,500원) | 안정·빠름. 유럽 region. | -| **본인 PC + 24h on** | $0 | 데모 단계 충분. 동적 IP면 이주 권장. | - -**비추**: Cloudflare Workers(런타임 비호환), Heroku 무료(폐지), Render 무료(60s sleep으로 봇 끊김). - -### 1.2 "타깃 = 가짜연구소 디스코드?" 정리 - -질문하신 *"이 타깃이 가짜연구소 디스코드?"* 에 대해: 문서에서 쓴 "타깃"은 **지원할 DB 엔진 종류**(PG vs MySQL vs BQ)를 의미한 것이고, **봇이 초대될 디스코드 커뮤니티**와는 별개입니다. - -- **DB 엔진 타깃**: PostgreSQL (확정) -- **배포 커뮤니티**: 가짜연구소 디스코드라면 그쪽 길드에 봇을 초대하는 별개 결정. V1 코드는 어떤 길드에든 동작하도록 작성. - -### 1.3 "자격증명 저장 위치" 풀이 - -`/connect` 로 입력받는 **DB 사용자명·비밀번호를 어디에 저장하느냐**의 질문입니다. - -| 옵션 | 비용 | 강도 | 추천 시점 | -|---|---|---|---| -| **로컬 AES-GCM SQLite** | $0 | 마스터키(env) + per-row IV | **V1 — 채택** | -| AWS Secrets Manager | ~$0.40/secret/월 | 강함 | 다중 인스턴스·SOC2 | -| GCP Secret Manager | ~$0.06/secret/월 | 강함 | GCP 환경일 때 | -| HashiCorp Vault | self-host | 강함 | 사내 운영 | - -V1은 **로컬 암호화 SQLite**. 마스터키는 `LANG2SQL_MASTER_KEY` env로 주입. 키 분실 시 secrets만 무효화되고 conversation/audit은 그대로 (분리 저장). - ---- - -## 2. 상위 아키텍처 - -``` - ┌──────────────────────────────┐ - │ DISCORD GATEWAY │ - │ - DM (private analytics) │ - │ - Guild channel / thread │ - │ - Slash commands / modals │ - └───────────────┬──────────────┘ - │ events - ▼ -┌──────────────────────────────────────────────────────────────────────┐ -│ DISCORD ADAPTER (1급) │ -│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────────┐ │ -│ │ Onboarding │ │ SessionRouter│ │ Streaming Renderer │ │ -│ │ /connect │ │ key 결정 │ │ 토큰 → message edits │ │ -│ │ modal+test │ │ │ │ rows → PNG (페이지) │ │ -│ └──────────────┘ └──────────────┘ └───────────────────────────┘ │ -│ ┌──────────────┐ ┌──────────────┐ ┌───────────────────────────┐ │ -│ │ Interactive │ │ Permissions │ │ DiscordRateLimit │ │ -│ │ ask_user / │ │ guild-admin/ │ │ message edit throttle │ │ -│ │ show_plan │ │ owner/user │ │ per-user / per-guild │ │ -│ │ → buttons │ │ │ │ │ │ -│ └──────────────┘ └──────────────┘ └───────────────────────────┘ │ -└───────────────────────────┬──────────────────────────────────────────┘ - │ ctx = ContextConcierge.build(session_key, principal) - ▼ -┌──────────────────────────────────────────────────────────────────────┐ -│ TENANCY LAYER │ -│ ┌──────────────┐ ┌──────────────┐ ┌──────────────────────────────┐ │ -│ │TenantRegistry│ │ExplorerCache │ │ EncryptedStore (AES-GCM) │ │ -│ │key→DBSpec │ │LRU engines │ │ secrets + conversation + │ │ -│ │ │ │ │ │ audit (단일 SQLite) │ │ -│ └──────────────┘ └──────────────┘ └──────────────────────────────┘ │ -│ ┌──────────────────────────────────────────────────────────────┐ │ -│ │ ContextConcierge — session_key + principal → HarnessContext │ │ -│ └──────────────────────────────────────────────────────────────┘ │ -└───────────────────────────┬──────────────────────────────────────────┘ - ▼ ctx -┌──────────────────────────────────────────────────────────────────────┐ -│ HARNESS KERNEL │ -│ agent_loop · system_prompt(per turn) · ToolRegistry(ctx 주입) │ -│ Session(live layer + pending_call + write-through) · Hooks(pub/sub) │ -└──────┬─────────────────────┬──────────────────┬──────────────────────┘ - │ ports │ │ - ▼ ▼ ▼ -┌──────────────────────────────────────────────────────────────────────┐ -│ SAFETY LAYER (제품의 moat) │ -│ L0 connect (RO role, TLS) → L1 stmt gate (sqlglot AST) │ -│ L2 cost gate (EXPLAIN) → L3 runtime (timeout/row/byte) │ -│ L4 audit (append-only) → L5 rate limit (token bucket) │ -└──────┬─────────────────────┬──────────────────┬──────────────────────┘ - ▼ ▼ ▼ -┌──────────────────────────────────────────────────────────────────────┐ -│ OUTBOUND ADAPTERS │ -│ LLM (openai / nvidia-nim) · DB (sqlalchemy-postgres) │ -│ Secrets (encrypted-sqlite) · SessionStore (encrypted-sqlite) │ -│ AuditSink (sqlite, file) │ -└──────────────────────────────────────────────────────────────────────┘ -``` - -규칙: 화살표는 **안쪽으로만**. Kernel/Safety는 어댑터를 import하지 않는다. 새 frontend·새 LLM·새 DB·새 session store는 어댑터 한 장 추가로 끝. - ---- - -## 3. 세션 전략 — Hermes-style 영속화 - -### 3.1 session_key 정책 - -| Discord 컨텍스트 | session_key | DB credential 출처 | 멤버 가시성 | 용도 | -|---|---|---|---|---| -| **봇과의 DM** | `dm:{user_id}` | 유저 본인의 비밀저장소 항목 | 본인만 | **개인 분석 (기본 경로)** | -| **길드 채널(메인)** | `chan:{guild_id}:{channel_id}` | 길드 어드민이 등록한 공용 DB | 채널 멤버 전원 | 팀 공용 대시보드 질의 | -| **길드 스레드** | `thr:{guild_id}:{channel_id}:{thread_id}` | 상위 채널과 동일 | 스레드 참여자 | 병렬 조사 (1조사 = 1스레드) | - -**원칙**: -- 민감 DB 자격증명은 **DM에서만 받음**. 채널 메시지는 영구히 history에 남고 다른 멤버에게 보임. -- 위 3가지 외의 session_key는 사용 금지. 변형이 늘면 "누가 무엇을 보는지" 추적 불가. -- **principal** (= 누가 보낸 메시지인가) 은 session_key와 별개로 매 요청 기록. 공용 채널에서도 audit log에 *"이 쿼리는 누가 시켰는지"* 가 남음. - -### 3.2 영속화 메커니즘 (write-through) - -``` -User msg ──▶ Discord adapter ──▶ session.append_message(msg) - │ - ├─▶ in-memory state update - └─▶ store.write(session_id, msg) ← 동기, 즉시 - │ - ▼ - AES-GCM SQLite (append-only segment) - -Tool call/result ──▶ same path, immediate flush -Pause(ask_user) ──▶ pending_call snapshot, immediate flush -LLM stream delta ──▶ buffered in-mem, flush at chunk boundary -``` - -**보장**: 봇 프로세스가 임의 시점에 죽어도 *디스크에 마지막으로 flush된 상태까지는* 안전. 재시작 시: - -1. `SessionStore.iter_active(since=now-30d)` 로 미완 세션 로드 -2. `pending_call` 이 있는 세션은 *사용자 답을 기다리는 상태*로 복원 -3. Discord 어댑터가 DM·채널·스레드별로 reattach -4. 사용자가 다음 메시지를 보내면 **끊김 없이 이어짐** — 이게 Hermes-style 핵심 - -**저장 모델** (SQLite 스키마): - -```sql --- 세션 메타 -CREATE TABLE sessions ( - id TEXT PRIMARY KEY, -- session_key - principal TEXT NOT NULL, -- 마지막 발화 유저 - kind TEXT NOT NULL, -- 'dm' | 'channel' | 'thread' - db_spec_id TEXT, -- secrets.id 참조 - created_at TIMESTAMP NOT NULL, - updated_at TIMESTAMP NOT NULL, - pending_call TEXT, -- JSON | NULL - closed_at TIMESTAMP -- NULL = active -); - --- 메시지 append-only 로그 -CREATE TABLE messages ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - session_id TEXT REFERENCES sessions(id), - ts TIMESTAMP NOT NULL, - role TEXT NOT NULL, -- system|user|assistant|tool_result - content TEXT, -- JSON or text - tool_calls TEXT, -- JSON | NULL - tool_call_id TEXT, - principal TEXT -); -CREATE INDEX ON messages(session_id, id); - --- 암호화 자격증명 -CREATE TABLE secrets ( - id TEXT PRIMARY KEY, -- e.g. "user:123:default" - owner TEXT NOT NULL, -- discord user id - label TEXT, -- "prod-pg" 같은 표시명 - ciphertext BLOB NOT NULL, -- AES-GCM - iv BLOB NOT NULL, - tag BLOB NOT NULL, - created_at TIMESTAMP NOT NULL, - last_used_at TIMESTAMP -); - --- 감사 로그 (append-only) -CREATE TABLE audit ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - ts TIMESTAMP NOT NULL, - session_id TEXT, - principal TEXT, - kind TEXT, -- 'sql' | 'tool' | 'pause' | 'safety_block' - payload TEXT, -- JSON - duration_ms INTEGER, - status TEXT -- 'ok' | 'error' | 'blocked' -); -``` - -### 3.3 동시성 - -``` -전역 concurrency = 무제한 (asyncio) -per-session = FIFO 1 (같은 conversation에 동시 LLM 호출 금지) -per-user rate limit = token bucket (LLM 20/min, DB 60/min) -per-guild rate limit = token bucket (DB 200/min) -LLM stream = 세션마다 독립 (한 사용자가 폭주해도 다른 사용자 영향 X) -``` - -이 모델이면 **사용자 1000명도 단일 프로세스 처리 가능**. 한 유저가 폭주해도 다른 유저는 영향 없음. - ---- - -## 4. DB 연결 온보딩 (Discord-native) - -``` -사용자 Bot (DM) Tenancy Layer - │ │ │ - │── /connect ──────────▶│ │ - │ │ Modal 출력: │ - │ │ ┌──────────────────┐ │ - │ │ │ label: "prod-pg" │ │ - │ │ │ host: │ │ - │ │ │ port: 5432 │ │ - │ │ │ database: │ │ - │ │ │ user: │ │ - │ │ │ password: [hide] │ │ - │ │ │ schema: (opt) │ │ - │ │ │ sslmode: require │ │ - │ │ │ ☑ RO 계정인가요? │ │ - │ │ └──────────────────┘ │ - │── submit ────────────▶│ │ - │ │── build url ────────────────│ - │ │── encrypt (AES-GCM) ───────▶│ secrets.put - │ │── test SELECT 1 ───────────▶│ Explorer 임시 - │ │── probe RO (BEGIN; CREATE TABLE __probe; ROLLBACK) - │ │ - 실패하면 ✅ RO 확정 │ - │ │ - 성공하면 ⚠️ 경고 + 명시적 confirm - │ │── SHOW TABLES ──────────────│ - │ │◀── ok, 17 tables ───────────│ - │◀── ephemeral embed ───│ │ - │ ✅ Connected to │ │ - │ "prod-pg" (17 tbls) │ │ - │ Try: "tables 보여줘"│ │ - │ │ │ - │── "tables 보여줘" ───▶│ │ - │ │── agent_loop(ctx) ─────────▶│ kernel -``` - -**핵심 결정**: -- **Modal로만** 자격증명 입력 (슬래시 명령 인자로 URL 받지 않음) → 비밀번호가 채널 로그에 안 남음. -- **자동 RO 검증**: `BEGIN; CREATE TABLE __probe(x int); ROLLBACK;` 시도 → 실패하면 RO 확정, 성공하면 경고. -- **`db_url`은 절대 평문 저장 안 함**. AES-GCM 암호화 후 SQLite secrets 테이블에 저장. -- **여러 DB 등록 가능**: `/connections` 로 목록, `/use