Skip to content
Closed
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
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,8 @@ for the full architecture write-up.

## 🤝 기여하기

**처음 보시는 분은 [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md)** — 디렉토리·레이어 책임, 한 메시지의 lifecycle, *어디를 수정하면 좋을지* 가 한곳에 정리돼 있습니다.

```bash
git clone https://github.com/CausalInferenceLab/lang2sql.git
cd lang2sql
Expand Down
245 changes: 245 additions & 0 deletions docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,245 @@
# Architecture — 기여자용 한눈 가이드

이 문서는 *처음 보는 사람도 10분 안에 어디 무엇이 있는지 / 어디를 손대면 좋은지* 알 수 있도록 쓰여졌습니다. 상세 설계 의도는 [`docs/discord_first_redesign_v4_1.md`](./discord_first_redesign_v4_1.md)에 있습니다.

---

## 1. 한 눈에 보는 아키텍처

```
USER (Discord / CLI / 향후 Slack·Web)
┌─────────────────────────────────────────────────┐
│ frontends/ ← 입력 받고 출력 보내기 (transport) │
│ discord/ cli/ slack/(빈) web/(빈) │
└──────────────────┬──────────────────────────────┘
▼ (인터랙션 → Identity)
┌─────────────────────────────────────────────────┐
│ tenancy/ ContextConcierge ← *조립점* │
│ 요청마다 HarnessContext를 하나 만들어 넘김 │
└──────────────────┬──────────────────────────────┘
▼ (ctx = LLM+tools+session+...)
┌─────────────────────────────────────────────────┐
│ harness/ agent_loop │
│ system prompt → LLM → tool 호출 → 다음 턴/종료 │
└──────────────────┬──────────────────────────────┘
▼ (도구가 ctx의 포트를 호출)
┌──────────────┬───────────┬───────────┬─────────┐
│ semantic/(★④)│safety/(★①)│memory/(★②)│ingest/(★③)│ ← 4기둥
└──────────────┴───────────┴───────────┴─────────┘
▼ (모두 포트(Protocol)로 외부와 분리)
┌─────────────────────────────────────────────────┐
│ adapters/ 외부 시스템과의 마지막 한 줄 │
│ llm/openai_ · llm/fake │
│ db/sqlalchemy_explorer · db/d1_explorer · db/postgres_explorer │
│ storage/sqlite_store · storage/sqlite_semantic │
└─────────────────────────────────────────────────┘
```

핵심 원칙: **로직은 포트(추상)에만 의존, 어댑터(구체)는 가장자리에만**. 그래서 새 LLM·새 DB·새 frontend를 *기존 코드 안 건드리고* 끼울 수 있습니다.

---

## 2. 왜 이런 구조? — 4기둥 (해결하려는 문제)

| ★ | 이름 | 풀려는 현실 문제 | 핵심 파일 |
|---|---|---|---|
| ① | **Safety pipeline** | SQL이 *실수로/악의로* DB를 망치는 일 | [`src/lang2sql/safety/`](../src/lang2sql/safety/) |
| ② | **Memory 3축** | 봇이 어제 한 얘기·정의를 *기억 못 함* | [`src/lang2sql/memory/`](../src/lang2sql/memory/) |
| ③ | **Ingestion 매트릭스** | 비즈니스 정의를 *사람이 일일이* 입력해야 함 | [`src/lang2sql/ingestion/`](../src/lang2sql/ingestion/) |
| ④ | **Semantic federation** | 같은 *"활성 사용자"* 가 팀마다 의미 다름 | [`src/lang2sql/semantic/`](../src/lang2sql/semantic/) |

자세한 배경은 redesign 문서 §3을 참고.

---

## 3. 디렉토리·레이어 가이드

> 의존 방향: `frontends → tenancy → harness → semantic/safety/memory/ingestion/tools → core ← adapters`
> `core/`는 누구도 의존하지 않는 *순수* 영역(타입+포트). 새 모듈 추가 시 이 방향을 깨지 않게.

### `src/lang2sql/core/` — 순수 타입 + 포트 (★ 손대지 마세요)
시스템 전체의 *어휘*가 모여 있습니다. 외부 의존 0, I/O 0.
- [`types.py`](../src/lang2sql/core/types.py) — `Message`, `ToolCall`, `ToolResult`, `Completion`, `Role`
- [`identity.py`](../src/lang2sql/core/identity.py) — `Identity`, `Scope`, federation의 `scope_chain()` 순서 (narrow→wide)
- [`ports/`](../src/lang2sql/core/ports/) — 11개 Protocol: `LLMPort`, `ExplorerPort`, `ToolPort`, `SafetyLayerPort`, `SafetyPipelinePort`, `StorePort`, `RecallPort`, `ExtractorPort` (memory), `SourcePort`, `DocExtractorPort`, `ScopeResolverPort`, `FrontendPort`, `SecretsPort`, `SessionStorePort`, `AuditPort`

### `src/lang2sql/harness/` — 에이전트 한 턴의 엔진
- [`context.py`](../src/lang2sql/harness/context.py) — `HarnessContext` (llm + tools + safety + explorer + scope_resolver + session 한 다발)
- [`session.py`](../src/lang2sql/harness/session.py) — 대화 transcript
- [`loop.py`](../src/lang2sql/harness/loop.py) — `agent_loop`: system prompt → LLM → tool 호출 → 다음 턴
- [`tool_registry.py`](../src/lang2sql/harness/tool_registry.py) — 이름→도구 dispatch
- [`system_prompt.py`](../src/lang2sql/harness/system_prompt.py) — 시멘틱 + 스키마 주입

### `src/lang2sql/semantic/` — 시멘틱 레이어 + federation (★④)
- [`types.py`](../src/lang2sql/semantic/types.py) — `SemanticEntry` (METRIC/DIMENSION/RELATIONSHIP/RULE)
- [`layer.py`](../src/lang2sql/semantic/layer.py) — `SemanticLayer.render()` (시스템 프롬프트로 들어감)
- [`scoped_layer.py`](../src/lang2sql/semantic/scoped_layer.py) — *가장 구체적 scope가 승리*하는 merge
- [`store.py`](../src/lang2sql/semantic/store.py) — in-memory store
- [`sql_composer.py`](../src/lang2sql/semantic/sql_composer.py) — metric 이름 → 정의 펼치기 (V1 최소)

### `src/lang2sql/safety/` — Read-only 게이트 (★①)
- [`pipeline.py`](../src/lang2sql/safety/pipeline.py) — layer를 순서대로 통과, *첫 비-PASS에서 차단*
- [`layers/whitelist.py`](../src/lang2sql/safety/layers/whitelist.py) — SELECT/WITH만 통과, DML 키워드 fail-closed
- [`layers/timeout.py`](../src/lang2sql/safety/layers/timeout.py) — 실행 timeout config
- [`tests/test_safety.py`](../tests/test_safety.py) — **12개 회귀 케이스** (머지 게이트)

### `src/lang2sql/memory/` — Hermes 3축 (★②)
- [`stores/in_memory.py`](../src/lang2sql/memory/stores/in_memory.py) — Where
- [`recall/inject_all.py`](../src/lang2sql/memory/recall/inject_all.py) — What
- [`extractors/manual.py`](../src/lang2sql/memory/extractors/manual.py) — How new
- [`service.py`](../src/lang2sql/memory/service.py) — 셋을 묶음

### `src/lang2sql/ingestion/` — 문서 → 시멘틱 후보 (★③)
- [`sources/file_source.py`](../src/lang2sql/ingestion/sources/file_source.py) — 어디서
- [`extractors/llm_extractor.py`](../src/lang2sql/ingestion/extractors/llm_extractor.py) — 어떻게 추출
- [`pipeline.py`](../src/lang2sql/ingestion/pipeline.py) — Source × Extractor matrix

### `src/lang2sql/tools/` — 에이전트가 부르는 capability
6개 도구 (모두 ctx-aware, async):
- [`run_sql.py`](../src/lang2sql/tools/run_sql.py) — safety 통과 후 explorer로 실행
- [`explore_schema.py`](../src/lang2sql/tools/explore_schema.py) — 테이블/컬럼 introspection
- [`define_metric.py`](../src/lang2sql/tools/define_metric.py) — scope-aware 정의 쓰기
- [`remember.py`](../src/lang2sql/tools/remember.py) — fact 저장
- [`ask_user.py`](../src/lang2sql/tools/ask_user.py) — 모호하면 사용자에게 질문
- [`ingest_doc.py`](../src/lang2sql/tools/ingest_doc.py) — 문서 → 후보 제안
- [`__init__.py: build_default_tools`](../src/lang2sql/tools/__init__.py) — 어셈블리

### `src/lang2sql/tenancy/` — 조립점
- [`concierge.py`](../src/lang2sql/tenancy/concierge.py) — *유일하게* 구체 클래스를 import 하는 곳. 요청마다 `HarnessContext` 만듦.
- [`scope_resolver.py`](../src/lang2sql/tenancy/scope_resolver.py) — `ScopeResolverPort` 구현 (semantic 위)
- [`encrypted_secrets.py`](../src/lang2sql/tenancy/encrypted_secrets.py) — `cryptography.Fernet` 실 암호화

### `src/lang2sql/adapters/` — 외부 시스템과의 마지막 줄
- `llm/openai_.py` — urllib 기반 OpenAI tool-calling
- `llm/fake.py` — 오프라인 테스트용 결정적 LLM
- `db/sqlalchemy_explorer.py` — **DSN만 바꾸면 Postgres/MySQL/Snowflake/BigQuery/DuckDB 다 커버**
- `db/d1_explorer.py` — Cloudflare D1 (HTTP API, urllib)
- `db/factory.py` — `build_explorer(connection)` scheme 라우팅
- `db/postgres_explorer.py` — V1 stub (psycopg 미설치 환경용)
- `storage/sqlite_store.py` — `AuditPort` + `SessionStorePort` + kv
- `storage/sqlite_semantic.py` — 시멘틱 정의 영속화

### `src/lang2sql/frontends/` — 사용자 인터페이스
- [`discord/bot.py`](../src/lang2sql/frontends/discord/bot.py) — **유일하게** `discord.py`를 import
- [`discord/commands.py`](../src/lang2sql/frontends/discord/commands.py) — 순수 핸들러 (discord 비의존, 테스트 가능)
- [`discord/setup_wizard.py`](../src/lang2sql/frontends/discord/setup_wizard.py) — `/setup` Modal/Select
- [`discord/session_router.py`](../src/lang2sql/frontends/discord/session_router.py) — discord ID → `Identity`
- [`discord/render.py`](../src/lang2sql/frontends/discord/render.py) — >50행이면 CSV 첨부
- [`cli/app.py`](../src/lang2sql/frontends/cli/app.py) — 개발용 CLI

---

## 4. 한 메시지의 lifecycle (디스코드 멘션 한 번 따라가기)

```
1. 사용자: "@lang2sql-test 이번 달 매출 알려줘"
2. discord/bot.py: on_message → _message_context()로 (guild_id, channel_id, user_id) 뽑음
3. session_router.to_identity() → Identity(...)
4. CommandHandlers.query(identity, "이번 달 매출 알려줘")
5. ContextConcierge.build_context(identity)
- secrets에서 길드별 db_dsn 있나? → 있으면 build_explorer로 그 DB 사용 (캐시)
- SqliteStore에서 세션 로드 (없으면 새로)
- build_default_tools()로 ToolRegistry 채움
- HarnessContext 반환
6. agent_loop(ctx, "이번 달 매출 알려줘")
- system_prompt: 시멘틱 effective_layer + 스키마 주입
- LLM(GPT-4.1-mini): "run_sql 도구를 부르세요" 응답
- tools.dispatch("run_sql", {sql: "SELECT ..."}, ctx)
→ safety.evaluate(sql) → PASS
→ explorer.execute(sql) → 행들 반환
- 결과 messages에 추가, LLM 다시 호출 → 최종 답변
7. concierge.store.save(session_key, ctx.session) ← 세션 영속화
8. render_answer(answer) → OutboundMessage
9. interaction.followup.send(...) → Discord에 답
```

---

## 5. 어디를 수정하면 좋을까 — Extension Points

기여 PR을 받기 가장 쉬운 지점들. 전부 *기존 코드 안 건드리고 추가만 하면 됩니다*.

### LLM 추가 (예: Anthropic Claude, NIM)
1. `src/lang2sql/adapters/llm/<provider>_.py` 새로 작성, `LLMPort` 구현
2. `tenancy/concierge.py: _default_llm()`에 분기 추가
3. tests/ 에 `test_<provider>_adapter.py`

### 새 DB 지원
SQLAlchemy 지원 DB라면:
1. `pyproject.toml`의 `[project.optional-dependencies]`에 extra 추가
2. 끝. `SqlAlchemyExplorer`가 DSN으로 알아서 처리

SQLAlchemy 미지원 (예: 자체 HTTP API):
1. `adapters/db/<db>_explorer.py`에 `ExplorerPort` 구현
2. `adapters/db/factory.py`의 `build_explorer`에 scheme 분기
3. `adapters/db/dsn_builder.py`에 `build_<db>()` + `FIELD_SCHEMA[<db>]`
4. tests/

### 새 safety layer (예: AST 정밀 검증, 함수 차단, EXPLAIN 비용)
1. `safety/layers/<name>.py`에 `SafetyLayerPort` 구현
2. `safety/pipeline.py`의 `SafetyPipeline` 기본 layers 목록에 끼우거나, 옵셔널로 노출
3. tests/test_safety.py에 회귀 케이스 추가

### 더 똑똑한 memory recall (예: 키워드, 벡터)
1. `memory/recall/<name>.py`에 `RecallPort` 구현
2. concierge에서 옵션으로 선택 가능하게
3. tests/

### 새 ingestion source (예: URL, Notion MCP)
1. `ingestion/sources/<name>.py`에 `SourcePort` 구현
2. ingestion 도구 흐름이 자동 매트릭스이므로 추가 코드 거의 없음

### 새 frontend (예: Slack, Web)
1. `frontends/<platform>/` 디렉토리에 transport 작성
2. `commands.py`는 그대로 재사용 (discord 비의존이라)
3. `core/ports/frontend.py`의 `FrontendPort` 인터페이스 따르기

### 새 도구 (예: visualize, write_code)
1. `tools/<name>.py`에 `ToolPort` 구현 (spec + run)
2. `tools/__init__.py: build_default_tools()`에 추가
3. tests/

---

## 6. 빠른 기여 시작 (5분)

```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 로컬 데모
```

브랜치 → 코드 + 테스트 → PR. CI는 따로 없으니 *로컬에서 pytest 확인 후 PR*.

---

## 7. 코드 컨벤션 (작은 약속)

| 규칙 | 이유 |
|---|---|
| **포트는 `typing.Protocol`** (`runtime_checkable` 권장) | 덕타이핑 + isinstance 가능 |
| **어댑터의 engine/connection은 lazy** | 라우팅 단계에서 드라이버 미설치여도 OK |
| **blocking 호출은 `asyncio.to_thread`** | discord 이벤트 루프 막지 않기 |
| **frontends/discord에서 `discord.py` import는 `bot.py`·`setup_wizard.py`만** | 로직층은 유닛테스트 가능해야 함 |
| **새 환경변수는 `.env.example`에도 문서화** | 신규 컨트리뷰터 친화 |
| **테스트는 토큰/네트워크 없이도 통과해야 함** | `FakeLLM` / mock transport 활용 |
| **포트(`core/ports/`)는 거의 frozen** | 변경은 모든 어댑터/구현에 영향 — 정말 필요한지 한 번 더 고민 |

---

## 8. 더 깊이 보고 싶다면

- [`docs/discord_first_redesign_v4_1.md`](./discord_first_redesign_v4_1.md) — *왜 이렇게 만들었나* (장문)
- [`docs/discord_first_redesign_v4_2.md`](./discord_first_redesign_v4_2.md) — 확정 컨셉 요약 (단문)
- [`docs/DEPLOY.md`](./DEPLOY.md) — Discord 봇 운영
- [`bench/ecommerce_demo.py`](../bench/ecommerce_demo.py) — federation/safety 라이브 데모
- 테스트가 사실상 사양서 — `tests/test_*.py`를 *모듈별 가이드*로 활용

---

질문/제안은 [Discord](https://discord.gg/EPurkHVtp2) 또는 GitHub Issues 환영.
Loading