Skip to content
Open
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: 1 addition & 1 deletion docs/ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@
- [`run_sql.py`](../src/lang2sql/tools/run_sql.py) — safety 통과 후 explorer로 실행
- [`explore_schema.py`](../src/lang2sql/tools/explore_schema.py) — 테이블/컬럼 introspection
- [`enrich_schema.py`](../src/lang2sql/tools/enrich_schema.py) — LLM으로 컬럼 메타데이터 자동 보강
- [`semantic_federation.py`](../src/lang2sql/tools/semantic_federation.py) — `term_custom`: guild/channel/member 계층 용어 사전 (KV 기반, narrow→wide lookup)
- [`semantic_federation.py`](../src/lang2sql/tools/semantic_federation.py) — `term_custom`: org/team/user 계층 용어 사전 (KV 기반, narrow→wide lookup)
- [`org_setup.py`](../src/lang2sql/tools/org_setup.py) — 전사/팀 단위 용어 일괄 등록
- [`remember.py`](../src/lang2sql/tools/remember.py) — fact 저장
- [`ask_user.py`](../src/lang2sql/tools/ask_user.py) — 모호하면 사용자에게 질문
Expand Down
11 changes: 5 additions & 6 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@ DB가 연결되지 않으면 SQL을 실행할 수 없습니다.

DB 종류(PostgreSQL, MySQL, SQLite 등)를 선택하는 안내가 나타납니다. 접속 정보를 입력하면 연결됩니다. DSN을 직접 알고 있다면 `/connect dsn:...`으로 바로 입력할 수도 있습니다.

> 관리자 권한이 필요합니다.

---

Expand Down Expand Up @@ -87,7 +86,7 @@ DB 스키마를 분석해 비즈니스 용어 후보를 자동으로 뽑습니
같은 용어가 여러 레이어에 등록된 경우 **좁은 범위가 우선** 적용됩니다.

```
개인(member) > 채널(channel) > 전사(guild)
개인(member) > 채널(channel) > 전사(org)
```

예를 들어 "활성고객"을 전사에서는 "30일 내 로그인"으로 정의했더라도, 마케팅 채널에서 "14일 내 로그인"으로 따로 등록하면 마케팅 채널 안에서만 그 정의가 우선 적용됩니다. 다른 채널에는 영향이 없습니다.
Expand All @@ -98,10 +97,9 @@ DB 스키마를 분석해 비즈니스 용어 후보를 자동으로 뽑습니

| 커맨드 | 설명 |
|---|---|
| `/setup` | DB 연결 마법사 (관리자) |
| `/setup` | DB 연결 마법사 |
| `/connect dsn:...` | DSN으로 직접 DB 연결 |
| `/ingest content:...` | 텍스트에서 용어 후보 추출 |
| `/ingest ref:파일명` | 서버 파일에서 용어 후보 추출 |
| `/confirm_ingest ref:... accept:... layer:...` | 추출된 후보 검토 후 등록 |
| `/term_custom` | 용어 직접 등록 (위저드) |
| `/term_custom action:show` | 등록된 용어 전체 조회 |
Expand All @@ -111,6 +109,7 @@ DB 스키마를 분석해 비즈니스 용어 후보를 자동으로 뽑습니
| `/enrich` | DB 컬럼 메타데이터 자동 보강 |
| `/remember text:...` | 사실 기억 저장 |
| `/audit_me` | 내 활동 이력 조회 |
| `/help` | 사용 방법 안내 |

---

Expand All @@ -119,8 +118,8 @@ DB 스키마를 분석해 비즈니스 용어 후보를 자동으로 뽑습니
**Q. 질문했는데 엉뚱한 SQL이 나와요.**
등록된 용어가 없거나 DB 메타데이터가 부족한 경우입니다. `/enrich`로 컬럼 설명을 보강하거나 `/term_custom`으로 관련 용어를 등록해보세요.

**Q. "guild 용어는 관리자만 등록 가능" 오류가 나요.**
`layer:guild`는 관리자 권한이 필요합니다. `layer:channel`로 채널 범위로 등록하거나 관리자에게 요청하세요.
**Q. "org 용어는 관리자만 등록 가능" 오류가 나요.**
`layer:org`는 관리자 권한이 필요합니다. `layer:team`으로 채널 범위로 등록하거나 관리자에게 요청하세요.

**Q. 이전 대화 내용을 기억하나요?**
같은 채널(또는 DM 스레드)에서 이어지는 대화는 맥락이 유지됩니다. `/remember`로 중요한 사실을 명시적으로 저장할 수도 있습니다.
4 changes: 2 additions & 2 deletions src/lang2sql/adapters/storage/okf_bundle.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,7 @@ def import_(self, store: "SqliteStore", kv_scope: str) -> int:
# ------------------------------------------------------------------

def _scope_dir(self, entry: FedEntry) -> Path:
label = "guild" if entry.layer == "guild" else f"{entry.layer}:{entry.entity}"
label = "org" if entry.layer == "org" else f"{entry.layer}:{entry.entity}"
return self.base_dir / label

def _concept_path(self, entry: FedEntry) -> Path:
Expand Down Expand Up @@ -172,7 +172,7 @@ def _md_to_entry(path: Path) -> FedEntry | None:

return FedEntry(
term=str(fm.get("title", path.stem)),
layer=str(fm.get("layer", "guild")),
layer=str(fm.get("layer", "org")),
entity=str(fm.get("entity", "")),
definition=str(fm.get("description", "")),
synonyms=fm.get("synonyms") or [],
Expand Down
4 changes: 2 additions & 2 deletions src/lang2sql/core/identity.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ class ScopeLevel(str, Enum):
"""

THREAD = "thread"
CHANNEL = "channel"
GUILD = "guild"
CHANNEL = "team"
GUILD = "org"
BUILTIN = "builtin"


Expand Down
4 changes: 2 additions & 2 deletions src/lang2sql/frontends/discord/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ async def confirm_ingest(
interaction: discord.Interaction,
ref: str,
accept: str = "all",
layer: str = "channel",
layer: str = "team",
) -> None:
await self._run(
interaction,
Expand Down Expand Up @@ -219,7 +219,7 @@ async def term_custom(
interaction: discord.Interaction,
action: str = "",
term: str = "",
layer: str = "member",
layer: str = "team",
) -> None:
ident = to_identity(_interaction_context(interaction))
if action == "show":
Expand Down
12 changes: 6 additions & 6 deletions src/lang2sql/frontends/discord/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -179,7 +179,7 @@ async def term_custom(
identity: Identity,
term: str = "",
definition: str = "",
layer: str = "member",
layer: str = "team",
synonyms: str = "",
inferred: bool = False,
scan: bool = False,
Expand Down Expand Up @@ -252,7 +252,7 @@ async def confirm_ingest(
identity: Identity,
ref: str,
accept: str = "all",
layer: str = "channel",
layer: str = "team",
) -> OutboundMessage:
"""ingest_doc로 추출한 후보를 검토 후 시멘틱 레이어에 등록."""
ctx = await self._concierge.build_context(identity)
Expand All @@ -270,24 +270,24 @@ async def help(self) -> OutboundMessage:
**Lang2SQL 사용 가이드**

**📊 질문하기**
봇을 멘션하거나 채널에서 자연어로 질문하세요.
봇을 멘션하거나 스레드에서 자연어로 질문하세요.
> @Lang2SQL 이번 달 매출 상위 고객 10명 알려줘

**🗄️ DB 연결** (관리자)
**🗄️ DB 연결**
`/setup` — 안내에 따라 DB 접속 정보 입력
`/connect dsn:...` — DSN 직접 입력

**📖 비즈니스 용어 등록**
`/ingest content:월매출은 SUM(orders.amount), 활성고객은 30일 내 로그인`
→ 후보 추출 후 아래 커맨드로 확정
`/confirm_ingest ref:inline:xxxx accept:all layer:channel`
`/confirm_ingest ref:inline:xxxx accept:all layer:team`

`/term_custom` — 용어 직접 등록 (위저드)
`/term_custom action:show` — 등록된 용어 조회
`/org_setup org:회사명` — DB 스캔으로 용어 자동 추출

**🏷️ 용어 우선순위**
개인(member) > 채널(channel) > 전사(guild)
개인(user) > 팀(team) > 전사(org)
같은 채널 안에서 등록한 정의가 전사 정의보다 우선 적용됩니다.

**🔧 기타**
Expand Down
8 changes: 4 additions & 4 deletions src/lang2sql/frontends/discord/term_wizard.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""term_wizard.py — /term_custom 등록 폼 (2단계 UI).

Step 1: Select — 전사(guild) / 채널·팀(channel) / 개인(member) 선택
Step 1: Select — 전사(org) / 채널·팀(channel) / 개인(member) 선택
Step 2: Modal — 용어명·정의·동의어 입력

채널이 팀 경계 역할을 하므로 entity 직접 입력 불필요.
Expand All @@ -23,17 +23,17 @@
_LAYER_OPTIONS = [
discord.SelectOption(
label="전사 (Guild) — 회사 공통 정의",
value="guild",
value="org",
description="모든 채널에서 기본값으로 사용",
),
discord.SelectOption(
label="채널 (팀) — 이 채널 전용 정의",
value="channel",
value="team",
description="다른 채널과 충돌 없이 이 채널에서만 유효",
),
discord.SelectOption(
label="개인 — 나만 사용하는 정의",
value="member",
value="user",
description="전사·채널 정의를 조용히 덮어씀",
),
]
Expand Down
4 changes: 4 additions & 0 deletions src/lang2sql/harness/system_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,10 @@
- Discover schema with explore_schema before guessing table or column names.
- Prefer definitions from the semantic layer below over your own assumptions.
- Answer concisely. Show only the final successful SQL you ran, not intermediate attempts.

Clarification rule:
- If the user's query contains business terms (e.g. '활성고객', '월매출', '신규유저') that are NOT defined in the semantic layer and whose meaning is ambiguous, ask ONE concise clarifying question BEFORE running any SQL. Do not guess.
- After answering with SQL results, if you had to infer a term's meaning yourself, suggest the user save it: "이 정의를 저장하려면 `/term_custom`으로 등록해두세요. 다음 질문부터 자동 적용됩니다."
"""


Expand Down
3 changes: 2 additions & 1 deletion src/lang2sql/tenancy/concierge.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,12 +150,13 @@ def _default_llm() -> LLMPort:
model = os.environ.get("LANG2SQL_LLM_MODEL", "default")
# Local servers (vLLM, Ollama) speak OpenAI-compatible API; dummy key satisfies the header.
api_key = os.environ.get("OPENAI_API_KEY") or "local"
timeout = float(os.environ.get("LANG2SQL_LLM_TIMEOUT", "120"))
url = base_url.rstrip("/")
if not url.endswith("/chat/completions"):
if not url.endswith("/v1"):
url = url + "/v1"
url = url + "/chat/completions"
return OpenAILLM(model=model, api_key=api_key, base_url=url)
return OpenAILLM(model=model, api_key=api_key, base_url=url, timeout=timeout)
if os.environ.get("OPENAI_API_KEY"):
return OpenAILLM()
return FakeLLM()
10 changes: 5 additions & 5 deletions src/lang2sql/tools/confirm_ingest.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,9 +58,9 @@ def spec(self) -> ToolSpec:
},
"layer": {
"type": "string",
"enum": ["guild", "channel", "member"],
"enum": ["org", "team", "user"],
"description": "scope to register under (default: channel)",
"default": "channel",
"default": "team",
},
},
"required": ["ref"],
Expand All @@ -70,7 +70,7 @@ def spec(self) -> ToolSpec:
async def run(self, args: dict[str, Any], ctx: "HarnessContext") -> ToolResult:
ref = (args.get("ref") or "").strip()
accept = (args.get("accept") or "all").strip()
layer_raw = (args.get("layer") or "channel").strip()
layer_raw = (args.get("layer") or "team").strip()

if not ref:
return ToolResult(call_id="", content="'ref' is required.", is_error=True)
Expand Down Expand Up @@ -113,8 +113,8 @@ async def run(self, args: dict[str, Any], ctx: "HarnessContext") -> ToolResult:

entity = (
""
if layer == "guild"
else (channel_id if layer == "channel" else ctx.identity.user_id)
if layer == "org"
else (channel_id if layer == "team" else ctx.identity.user_id)
)
registered: list[str] = []
for cand in selected:
Expand Down
22 changes: 11 additions & 11 deletions src/lang2sql/tools/org_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,8 @@
KV 저장:
org:{org_lower} → {"name", "domain", "registered_at"}
team:{team_lower}:{channel_id} → {"name", "domain", "registered_at"} (팀 등록 시)
cterm:{term_lower}:guild → FedEntry JSON (org 전용, guild 레이어)
cterm:{term_lower}:channel:{channel_id} → FedEntry JSON (team 등록 시, channel 레이어)
cterm:{term_lower}:org → FedEntry JSON (org 전용, guild 레이어)
cterm:{term_lower}:channel:{channel_id} → FedEntry JSON (team 등록 시, team 레이어)
"""

from __future__ import annotations
Expand Down Expand Up @@ -79,7 +79,7 @@ def spec(self) -> ToolSpec:
name="org_setup",
description=(
"조직(전사) 또는 팀(채널) 등록 및 DB 테이블 스캔으로 비즈니스 용어를 자동 추출한다. "
"org만 지정 시 guild 레이어(전사 공통), team 지정 시 channel 레이어(팀 전용)에 저장. "
"org만 지정 시 guild 레이어(전사 공통), team 지정 시 team 레이어(팀 전용)에 저장. "
"DB 연결(/setup) 후 실행."
),
parameters={
Expand All @@ -91,7 +91,7 @@ def spec(self) -> ToolSpec:
},
"team": {
"type": "string",
"description": "팀 이름 (예: 마케팅팀). 현재 채널에 팀 전용 용어를 channel 레이어에 저장. org 없이 단독 사용 가능.",
"description": "팀 이름 (예: 마케팅팀). 현재 채널에 팀 전용 용어를 team 레이어에 저장. org 없이 단독 사용 가능.",
},
"clear": {
"type": "boolean",
Expand All @@ -118,18 +118,18 @@ async def run(self, args: dict[str, Any], ctx: "HarnessContext") -> ToolResult:
scope = ctx.identity.kv_scope
channel_id = ctx.identity.effective_channel_id

# team이 있으면 channel 레이어, org만 있으면 guild 레이어
# team이 있으면 team 레이어, org만 있으면 guild 레이어
use_team = bool(team_name)
layer = "channel" if use_team else "guild"
layer = "team" if use_team else "org"

if layer == "guild" and not ctx.identity.is_admin:
if layer == "org" and not ctx.identity.is_admin:
return ToolResult(
call_id="",
content="❌ 전사(guild) 용어 등록·초기화는 관리자만 가능합니다.",
content="❌ 전사(org) 용어 등록·초기화는 관리자만 가능합니다.",
is_error=True,
)

if layer == "channel" and not channel_id:
if layer == "team" and not channel_id:
return ToolResult(
call_id="",
content="❌ 채널 컨텍스트 없이 팀(channel) 레이어에 등록할 수 없습니다.",
Expand Down Expand Up @@ -161,7 +161,7 @@ async def run(self, args: dict[str, Any], ctx: "HarnessContext") -> ToolResult:
ctx.store.kv_delete(scope, key)
deleted += 1
ctx.store.kv_delete(scope, meta_key)
layer_label = "팀(채널)" if use_team else "전사(guild)"
layer_label = "팀(채널)" if use_team else "전사(org)"
return ToolResult(
call_id="",
content=f"🗑️ **{display_name}** [{layer_label}] 자동 추출 용어 {deleted}개 초기화 완료 (수동 등록 용어 보존)",
Expand Down Expand Up @@ -247,7 +247,7 @@ async def run(self, args: dict[str, Any], ctx: "HarnessContext") -> ToolResult:
syn_str = f" (= {', '.join(synonyms)})" if synonyms else ""
saved_terms.append(f"- **{term}**{syn_str}: {definition} 🤖")

layer_label = "팀(채널)" if use_team else "전사(guild)"
layer_label = "팀(채널)" if use_team else "전사(org)"
domain_line = f"📌 도메인: {domain}\n\n" if domain else ""
term_block = "\n".join(saved_terms)
return ToolResult(
Expand Down
Loading
Loading