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
16 changes: 14 additions & 2 deletions src/lang2sql/adapters/storage/sqlite_store.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,15 +130,27 @@ def kv_delete(self, scope: str, key: str) -> None:
)
self._conn.commit()

@staticmethod
def _escape_like(s: str) -> str:
return s.replace("!", "!!").replace("%", "!%").replace("_", "!_")

def kv_delete_prefix(self, scope: str, prefix: str) -> int:
"""Delete all keys under scope that start with prefix. Returns count deleted."""
cur = self._conn.execute(
"DELETE FROM kv WHERE scope = ? AND key LIKE ?",
(scope, prefix + "%"),
"DELETE FROM kv WHERE scope = ? AND key LIKE ? ESCAPE '!'",
(scope, self._escape_like(prefix) + "%"),
)
self._conn.commit()
return cur.rowcount

def kv_list_prefix(self, scope: str, prefix: str) -> list[tuple[str, str]]:
"""Return (key, value) pairs for all keys under scope that start with prefix."""
rows = self._conn.execute(
"SELECT key, value FROM kv WHERE scope = ? AND key LIKE ? ESCAPE '!' ORDER BY key",
(scope, self._escape_like(prefix) + "%"),
).fetchall()
return [(r["key"], r["value"]) for r in rows]


# -- Session (de)serialization ------------------------------------------

Expand Down
27 changes: 27 additions & 0 deletions src/lang2sql/frontends/discord/bot.py
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,33 @@ async def enrich(interaction: discord.Interaction, table: str = "", clear: bool
handlers.enrich(to_identity(_interaction_context(interaction)), table=table, clear=clear),
)

@tree.command(name="term_custom", description="비즈니스 용어 등록·조회·삭제 (조직/팀/개인 범위)")
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nit/term_custom remove에 layer 인자가 없어서 기본값 member만 삭제되는 것 같아요. wizard로 guild/channel 용어를 만들 수 있는데 슬래시로는 그 레이어를 못 지우는 비대칭이 있습니다.

async def term_custom(
interaction: discord.Interaction,
remove: str = "",
list_all: bool = False,
) -> None:
if list_all:
await self._run(
interaction,
handlers.term_custom(to_identity(_interaction_context(interaction)), list_all=True),
)
elif remove:
await self._run(
interaction,
handlers.term_custom(to_identity(_interaction_context(interaction)), term=remove, remove=True),
)
else:
from .term_wizard import start_term_add_flow
await start_term_add_flow(interaction, handlers, _interaction_context)

@tree.command(name="org_setup", description="조직 등록 + DB 스캔으로 팀별 용어 자동 추출")
async def org_setup(interaction: discord.Interaction, org: str, clear: bool = False) -> None:
await self._run(
interaction,
handlers.org_setup(to_identity(_interaction_context(interaction)), org=org, clear=clear),
)

@tree.command(name="semantic_show", description="Show definitions in effect here")
async def semantic_show(interaction: discord.Interaction) -> None:
await self._run(interaction, handlers.semantic_show(to_identity(_interaction_context(interaction))))
Expand Down
34 changes: 34 additions & 0 deletions src/lang2sql/frontends/discord/commands.py
Original file line number Diff line number Diff line change
Expand Up @@ -188,6 +188,40 @@ async def enrich(self, identity: Identity, table: str = "", clear: bool = False)
)
return OutboundMessage(text=result.content)

async def org_setup(self, identity: Identity, org: str, clear: bool = False) -> OutboundMessage:
"""조직 등록 + DB 스캔으로 팀별 용어 자동 추출."""
ctx = await self._concierge.build_context(identity)
result = await ctx.tools.dispatch(
"org_setup", {"org": org, "clear": clear}, ctx, "cmd:org_setup"
)
return OutboundMessage(text=result.content)

async def term_custom(
self,
identity: Identity,
term: str = "",
definition: str = "",
layer: str = "member",
synonyms: str = "",
inferred: bool = False,
scan: bool = False,
remove: bool = False,
list_all: bool = False,
) -> OutboundMessage:
"""채널(팀)/전사/개인 계층 비즈니스 용어 사전 관리."""
ctx = await self._concierge.build_context(identity)
result = await ctx.tools.dispatch(
"term_custom",
{
"term": term, "definition": definition, "layer": layer,
"synonyms": synonyms, "inferred": inferred, "scan": scan,
"remove": remove, "list": list_all,
},
ctx,
"cmd:term_custom",
)
return OutboundMessage(text=result.content)

async def connect(self, identity: Identity, dsn: str) -> OutboundMessage:
"""V1 stub: stash a DB DSN keyed by guild/DM in the concierge kv store.

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

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

채널이 팀 경계 역할을 하므로 entity 직접 입력 불필요.
setup_wizard.py 패턴 동일: Select 선택 → Modal 응답.
"""

from __future__ import annotations

from typing import TYPE_CHECKING

import discord
from discord import ui

from .session_router import to_identity

if TYPE_CHECKING:
from .commands import CommandHandlers


_LAYER_OPTIONS = [
discord.SelectOption(
label="전사 (Guild) — 회사 공통 정의",
value="guild",
description="모든 채널에서 기본값으로 사용",
),
discord.SelectOption(
label="채널 (팀) — 이 채널 전용 정의",
value="channel",
description="다른 채널과 충돌 없이 이 채널에서만 유효",
),
discord.SelectOption(
label="개인 — 나만 사용하는 정의",
value="member",
description="전사·채널 정의를 조용히 덮어씀",
),
]


class _TermModal(ui.Modal, title="비즈니스 용어 등록"):
term = ui.TextInput(
label="용어명",
placeholder="예: 활성고객",
required=True,
max_length=100,
)
definition = ui.TextInput(
label="정의",
placeholder="예: 최근 30일 내 로그인한 users",
required=True,
style=discord.TextStyle.paragraph,
max_length=500,
)
synonyms = ui.TextInput(
label="동의어 (쉼표 구분, 선택)",
placeholder="예: active user, 활성화고객",
required=False,
max_length=200,
)

def __init__(self, layer: str, handlers: "CommandHandlers", ctx_factory) -> None:
super().__init__()
self._layer = layer
self._handlers = handlers
self._ctx_factory = ctx_factory

async def on_submit(self, interaction: discord.Interaction) -> None:
await interaction.response.defer(ephemeral=True, thinking=True)
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 nitdefer()가 try 밖에 있어서, defer 자체나 except 안의 followup이 실패하면(webhook 만료 등) 여전히 처리가 안 됩니다. 일반 케이스 무한로딩은 잘 막았어요 👍 — 엣지만 참고로요.

try:
identity = to_identity(self._ctx_factory(interaction))
result = await self._handlers.term_custom(
identity,
term=self.term.value.strip(),
definition=self.definition.value.strip(),
layer=self._layer,
synonyms=self.synonyms.value.strip(),
)
await interaction.followup.send(result.text, ephemeral=True)
except Exception as exc:
await interaction.followup.send(f"❌ 오류: {exc}", ephemeral=True)


class _LayerSelect(ui.Select):
def __init__(self, handlers: "CommandHandlers", ctx_factory) -> None:
super().__init__(
placeholder="적용 범위를 선택하세요…",
options=_LAYER_OPTIONS,
min_values=1,
max_values=1,
)
self._handlers = handlers
self._ctx_factory = ctx_factory

async def callback(self, interaction: discord.Interaction) -> None:
await interaction.response.send_modal(
_TermModal(self.values[0], self._handlers, self._ctx_factory)
)


class _LayerSelectView(ui.View):
def __init__(self, handlers: "CommandHandlers", ctx_factory) -> None:
super().__init__(timeout=120.0)
self.add_item(_LayerSelect(handlers, ctx_factory))


async def start_term_add_flow(
interaction: discord.Interaction,
handlers: "CommandHandlers",
ctx_factory,
) -> None:
"""bot.py의 /term_custom 커맨드에서 호출 — 범위 선택 → 용어 등록 모달."""
await interaction.response.send_message(
"용어를 등록할 **범위**를 선택하세요.\n"
"- **전사**: 모든 채널에서 기본값\n"
"- **채널(팀)**: 이 채널에서만 유효 (다른 채널과 충돌 없음)\n"
"- **개인**: 나만 사용하는 정의 (전사·채널 정의를 덮어씀)",
view=_LayerSelectView(handlers, ctx_factory),
ephemeral=True,
)
7 changes: 7 additions & 0 deletions src/lang2sql/harness/system_prompt.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,4 +72,11 @@ async def build_system_prompt(ctx: HarnessContext) -> str:
except (ValueError, TypeError):
pass

from ..tools.semantic_federation import build_prompt_section
user_id = ctx.identity.user_id or "unknown"
channel_id = ctx.identity.thread_id or ctx.identity.channel_id or ""
semfed_section = build_prompt_section(ctx.store, scope, channel_id, user_id)
if semfed_section:
parts.append(semfed_section)

return "\n\n".join(parts)
7 changes: 6 additions & 1 deletion src/lang2sql/tools/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,15 @@
from .enrich_schema import EnrichSchema
from .explore_schema import ExploreSchema
from .ingest_doc import IngestDoc
from .org_setup import OrgSetupTool
from .remember import Remember
from .run_sql import RunSQL
from .semantic_federation import SemanticFederationTool

__all__ = [
"build_default_tools",
"RunSQL", "ExploreSchema", "EnrichSchema", "DefineMetric", "Remember", "AskUser", "IngestDoc",
"RunSQL", "ExploreSchema", "EnrichSchema", "DefineMetric", "SemanticFederationTool",
"OrgSetupTool", "Remember", "AskUser", "IngestDoc",
]


Expand All @@ -39,6 +42,8 @@ def build_default_tools(
ExploreSchema(),
EnrichSchema(),
DefineMetric(),
SemanticFederationTool(),
OrgSetupTool(),
AskUser(),
Remember(memory),
IngestDoc(ingestion, source, extractor),
Expand Down
Loading