Skip to content

Commit 0a5b018

Browse files
thrcleclaude
andcommitted
feat: FedEntry에 kind/applies_to/tags 필드 추가
비즈니스 용어를 Metric/Table/Rule/Dimension으로 분류할 수 있도록 FedEntry에 kind, applies_to, tags 필드를 추가한다. - FedEntry: kind, applies_to, tags 필드 추가 (기존 JSON 하위호환) - term_custom tool spec: 세 파라미터 노출 - _fmt_entry: kind 배지 표시 (`metric` 등) - 신규 테스트 3개: round-trip, 하위호환, kind 배지 렌더링 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent bc4a84f commit 0a5b018

2 files changed

Lines changed: 64 additions & 4 deletions

File tree

src/lang2sql/tools/semantic_federation.py

Lines changed: 29 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -61,17 +61,23 @@ class FedEntry:
6161
definition: str
6262
synonyms: list[str] = field(default_factory=list)
6363
inferred: bool = False
64+
kind: str = "" # metric | table | rule | dimension
65+
applies_to: str = "" # 관련 테이블/컬럼 (예: users, orders.amount)
66+
tags: list[str] = field(default_factory=list)
6467

6568
def __post_init__(self) -> None:
6669
if not isinstance(self.synonyms, list):
6770
self.synonyms = _parse_synonyms(self.synonyms)
71+
if not isinstance(self.tags, list):
72+
self.tags = [t.strip() for t in str(self.tags).split(",") if t.strip()]
6873

6974
def to_json(self) -> str:
7075
return json.dumps(
7176
{
7277
"term": self.term, "layer": self.layer, "entity": self.entity,
7378
"definition": self.definition, "synonyms": self.synonyms,
74-
"inferred": self.inferred,
79+
"inferred": self.inferred, "kind": self.kind,
80+
"applies_to": self.applies_to, "tags": self.tags,
7581
},
7682
ensure_ascii=False,
7783
)
@@ -82,7 +88,8 @@ def from_json(raw: str) -> "FedEntry":
8288
return FedEntry(
8389
term=d["term"], layer=d["layer"], entity=d.get("entity", ""),
8490
definition=d["definition"], synonyms=d.get("synonyms", []),
85-
inferred=d.get("inferred", False),
91+
inferred=d.get("inferred", False), kind=d.get("kind", ""),
92+
applies_to=d.get("applies_to", ""), tags=d.get("tags", []),
8693
)
8794

8895

@@ -117,6 +124,19 @@ def spec(self) -> ToolSpec:
117124
"type": "string",
118125
"description": "쉼표 구분 동의어 (예: active_user,활성화고객)",
119126
},
127+
"kind": {
128+
"type": "string",
129+
"enum": ["metric", "table", "rule", "dimension"],
130+
"description": "용어 종류. metric=지표, table=테이블/엔티티, rule=비즈니스 규칙, dimension=분류 기준.",
131+
},
132+
"applies_to": {
133+
"type": "string",
134+
"description": "관련 테이블 또는 컬럼 (예: users, orders.amount).",
135+
},
136+
"tags": {
137+
"type": "string",
138+
"description": "쉼표 구분 태그 (예: growth,retention).",
139+
},
120140
"inferred": {
121141
"type": "boolean",
122142
"description": "true 시 LLM 추론 임시 정의로 표시. 사용자 확인 후 재등록 권장.",
@@ -215,9 +235,13 @@ async def run(self, args: dict[str, Any], ctx: "HarnessContext") -> ToolResult:
215235

216236
synonyms = _parse_synonyms(args.get("synonyms"))
217237
inferred = bool(args.get("inferred", False))
238+
kind = str(args.get("kind", "")).strip().lower()
239+
applies_to = str(args.get("applies_to", "")).strip()
240+
tags = [t.strip() for t in str(args.get("tags", "")).split(",") if t.strip()]
218241

219242
entry = FedEntry(term=term, layer=layer, entity=entity,
220-
definition=definition, synonyms=synonyms, inferred=inferred)
243+
definition=definition, synonyms=synonyms, inferred=inferred,
244+
kind=kind, applies_to=applies_to, tags=tags)
221245
ctx.store.kv_set(scope, key, entry.to_json())
222246
if ctx.audit is not None:
223247
await ctx.audit.record(
@@ -360,7 +384,8 @@ def _fmt_entry(e: FedEntry, tag: str) -> str:
360384
syns = ", ".join(e.synonyms)
361385
syn_str = f" (= {syns})" if syns else ""
362386
inferred_badge = " 🤖" if e.inferred else ""
363-
return f"- **{e.term}** [{tag}]{syn_str}{inferred_badge}: {e.definition}"
387+
kind_badge = f" `{e.kind}`" if e.kind else ""
388+
return f"- **{e.term}**{kind_badge} [{tag}]{syn_str}{inferred_badge}: {e.definition}"
364389

365390

366391
def _resolve_term(entries: list[FedEntry], channel_id: str, user_id: str) -> str:

tests/test_semantic.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,3 +84,38 @@ def test_build_prompt_section_includes_ambiguous_term_policy() -> None:
8484
store = SqliteStore()
8585
section = build_prompt_section(store, "g1", "c1", "u1")
8686
assert "Ambiguous Term Policy" in section
87+
88+
89+
def test_fed_entry_kind_applies_to_tags_roundtrip() -> None:
90+
entry = FedEntry(
91+
term="활성고객", layer="guild", entity="",
92+
definition="30일 내 로그인한 users",
93+
kind="metric", applies_to="users", tags=["growth", "retention"],
94+
)
95+
restored = FedEntry.from_json(entry.to_json())
96+
assert restored.kind == "metric"
97+
assert restored.applies_to == "users"
98+
assert restored.tags == ["growth", "retention"]
99+
100+
101+
def test_fed_entry_backward_compat_missing_new_fields() -> None:
102+
# kind/applies_to/tags 없는 기존 JSON도 파싱 가능해야 함
103+
import json
104+
old_json = json.dumps({
105+
"term": "revenue", "layer": "guild", "entity": "",
106+
"definition": "net revenue", "synonyms": [], "inferred": False,
107+
})
108+
entry = FedEntry.from_json(old_json)
109+
assert entry.kind == ""
110+
assert entry.applies_to == ""
111+
assert entry.tags == []
112+
113+
114+
def test_fmt_entry_shows_kind_badge() -> None:
115+
from lang2sql.tools.semantic_federation import _fmt_entry
116+
entry = FedEntry(
117+
term="활성고객", layer="guild", entity="",
118+
definition="30일 내 로그인", kind="metric",
119+
)
120+
rendered = _fmt_entry(entry, "전사")
121+
assert "`metric`" in rendered

0 commit comments

Comments
 (0)