이 문서는 처음 보는 사람도 10분 안에 어디 무엇이 있는지 / 어디를 손대면 좋은지 알 수 있도록 쓰여졌습니다. 상세 설계 의도는 docs/discord_first_redesign_v4_1.md에 있습니다.
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를 기존 코드 안 건드리고 끼울 수 있습니다.
| ★ | 이름 | 풀려는 현실 문제 | 핵심 파일 |
|---|---|---|---|
| ① | Safety pipeline | SQL이 실수로/악의로 DB를 망치는 일 | src/lang2sql/safety/ |
| ② | Memory 3축 | 봇이 어제 한 얘기·정의를 기억 못 함 | src/lang2sql/memory/ |
| ③ | Ingestion 매트릭스 | 비즈니스 정의를 사람이 일일이 입력해야 함 | src/lang2sql/ingestion/ |
| ④ | Semantic federation | 같은 "활성 사용자" 가 팀마다 의미 다름 | src/lang2sql/semantic/ |
자세한 배경은 redesign 문서 §3을 참고.
의존 방향:
frontends → tenancy → harness → semantic/safety/memory/ingestion/tools → core ← adapterscore/는 누구도 의존하지 않는 순수 영역(타입+포트). 새 모듈 추가 시 이 방향을 깨지 않게.
시스템 전체의 어휘가 모여 있습니다. 외부 의존 0, I/O 0.
types.py—Message,ToolCall,ToolResult,Completion,Roleidentity.py—Identity,Scope, federation의scope_chain()순서 (narrow→wide)ports/— 11개 Protocol:LLMPort,ExplorerPort,ToolPort,SafetyLayerPort,SafetyPipelinePort,StorePort,RecallPort,ExtractorPort(memory),SourcePort,DocExtractorPort,ScopeResolverPort,FrontendPort,SecretsPort,SessionStorePort,AuditPort
context.py—HarnessContext(llm + tools + safety + explorer + scope_resolver + session 한 다발)session.py— 대화 transcriptloop.py—agent_loop: system prompt → LLM → tool 호출 → 다음 턴tool_registry.py— 이름→도구 dispatchsystem_prompt.py— 시멘틱 + 스키마 주입
types.py—SemanticEntry(METRIC/DIMENSION/RELATIONSHIP/RULE)layer.py—SemanticLayer.render()(시스템 프롬프트로 들어감)scoped_layer.py— 가장 구체적 scope가 승리하는 mergestore.py— in-memory storesql_composer.py— metric 이름 → 정의 펼치기 (V1 최소)
pipeline.py— layer를 순서대로 통과, 첫 비-PASS에서 차단layers/whitelist.py— SELECT/WITH만 통과, DML 키워드 fail-closedlayers/timeout.py— 실행 timeout configtests/test_safety.py— 12개 회귀 케이스 (머지 게이트)
stores/in_memory.py— Whererecall/inject_all.py— Whatextractors/manual.py— How newservice.py— 셋을 묶음
sources/file_source.py— 어디서extractors/llm_extractor.py— 어떻게 추출pipeline.py— Source × Extractor matrix
6개 도구 (모두 ctx-aware, async):
run_sql.py— safety 통과 후 explorer로 실행explore_schema.py— 테이블/컬럼 introspectiondefine_metric.py— scope-aware 정의 쓰기remember.py— fact 저장ask_user.py— 모호하면 사용자에게 질문ingest_doc.py— 문서 → 후보 제안__init__.py: build_default_tools— 어셈블리
concierge.py— 유일하게 구체 클래스를 import 하는 곳. 요청마다HarnessContext만듦.scope_resolver.py—ScopeResolverPort구현 (semantic 위)encrypted_secrets.py—cryptography.Fernet실 암호화
llm/openai_.py— urllib 기반 OpenAI tool-callingllm/fake.py— 오프라인 테스트용 결정적 LLMdb/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+ kvstorage/sqlite_semantic.py— 시멘틱 정의 영속화
discord/bot.py— 유일하게discord.py를 importdiscord/commands.py— 순수 핸들러 (discord 비의존, 테스트 가능)discord/setup_wizard.py—/setupModal/Selectdiscord/session_router.py— discord ID →Identitydiscord/render.py— >50행이면 CSV 첨부cli/app.py— 개발용 CLI
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에 답
기여 PR을 받기 가장 쉬운 지점들. 전부 기존 코드 안 건드리고 추가만 하면 됩니다.
src/lang2sql/adapters/llm/<provider>_.py새로 작성,LLMPort구현tenancy/concierge.py: _default_llm()에 분기 추가- tests/ 에
test_<provider>_adapter.py
SQLAlchemy 지원 DB라면:
pyproject.toml의[project.optional-dependencies]에 extra 추가- 끝.
SqlAlchemyExplorer가 DSN으로 알아서 처리
SQLAlchemy 미지원 (예: 자체 HTTP API):
adapters/db/<db>_explorer.py에ExplorerPort구현adapters/db/factory.py의build_explorer에 scheme 분기adapters/db/dsn_builder.py에build_<db>()+FIELD_SCHEMA[<db>]- tests/
safety/layers/<name>.py에SafetyLayerPort구현safety/pipeline.py의SafetyPipeline기본 layers 목록에 끼우거나, 옵셔널로 노출- tests/test_safety.py에 회귀 케이스 추가
memory/recall/<name>.py에RecallPort구현- concierge에서 옵션으로 선택 가능하게
- tests/
ingestion/sources/<name>.py에SourcePort구현- ingestion 도구 흐름이 자동 매트릭스이므로 추가 코드 거의 없음
frontends/<platform>/디렉토리에 transport 작성commands.py는 그대로 재사용 (discord 비의존이라)core/ports/frontend.py의FrontendPort인터페이스 따르기
tools/<name>.py에ToolPort구현 (spec + run)tools/__init__.py: build_default_tools()에 추가- tests/
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.
| 규칙 | 이유 |
|---|---|
포트는 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 |
변경은 모든 어댑터/구현에 영향 — 정말 필요한지 한 번 더 고민 |
docs/discord_first_redesign_v4_1.md— 왜 이렇게 만들었나 (장문)docs/discord_first_redesign_v4_2.md— 확정 컨셉 요약 (단문)docs/DEPLOY.md— Discord 봇 운영bench/ecommerce_demo.py— federation/safety 라이브 데모- 테스트가 사실상 사양서 —
tests/test_*.py를 모듈별 가이드로 활용
질문/제안은 Discord 또는 GitHub Issues 환영.