Skip to content

Commit 827db80

Browse files
committed
문서 적합성 평가 기능 추가 및 관련 모듈 업데이트
- 새로운 문서 적합성 체인 및 출력 모델 구현 - UI에서 문서 적합성 결과를 표시하도록 수정 - 그래프 빌더에 문서 적합성 노드 추가
1 parent 3008455 commit 827db80

File tree

7 files changed

+215
-6
lines changed

7 files changed

+215
-6
lines changed

interface/lang2sql.py

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313

1414
from db_utils import get_db_connector
1515
from db_utils.base_connector import BaseConnector
16-
from infra.db.connect_db import ConnectDB
1716
from viz.display_chart import DisplayChart
1817
from engine.query_executor import execute_query as execute_query_common
1918
from llm_utils.llm_response_parser import LLMResponseParser
@@ -31,6 +30,7 @@
3130
"show_question_reinterpreted_by_ai": "Show User Question Reinterpreted by AI",
3231
"show_referenced_tables": "Show List of Referenced Tables",
3332
"show_question_gate_result": "Show Question Gate Result",
33+
"show_document_suitability": "Show Document Suitability",
3434
"show_table": "Show Table",
3535
"show_chart": "Show Chart",
3636
}
@@ -105,16 +105,14 @@ def should_show(_key: str) -> bool:
105105
show_result_desc = has_query and should_show("show_result_description")
106106
show_reinterpreted = has_query and should_show("show_question_reinterpreted_by_ai")
107107
show_gate_result = should_show("show_question_gate_result")
108+
show_doc_suitability = should_show("show_document_suitability")
108109
show_table_section = has_query and should_show("show_table")
109110
show_chart_section = has_query and should_show("show_chart")
110111
if show_gate_result and ("question_gate_result" in res):
111112
st.markdown("---")
112113
st.markdown("**Question Gate 결과:**")
113114
details = res.get("question_gate_result")
114115
if details:
115-
passed = details.get("is_sql_like")
116-
if passed is not None:
117-
st.write(f"적합성 통과 여부: `{passed}`")
118116
try:
119117
import json as _json
120118

@@ -124,6 +122,38 @@ def should_show(_key: str) -> bool:
124122
except Exception:
125123
st.write(details)
126124

125+
if show_doc_suitability and ("document_suitability" in res):
126+
st.markdown("---")
127+
st.markdown("**문서 적합성 평가:**")
128+
ds = res.get("document_suitability")
129+
if not isinstance(ds, dict):
130+
st.write(ds)
131+
else:
132+
133+
def _as_float(value):
134+
try:
135+
return float(value)
136+
except Exception:
137+
return -1.0
138+
139+
rows = [
140+
{
141+
"table": table_name,
142+
"score": _as_float(info.get("score", -1)),
143+
"matched_columns": ", ".join(info.get("matched_columns", [])),
144+
"missing_entities": ", ".join(info.get("missing_entities", [])),
145+
"reason": info.get("reason", ""),
146+
}
147+
for table_name, info in ds.items()
148+
if isinstance(info, dict)
149+
]
150+
151+
rows.sort(key=lambda r: r["score"], reverse=True)
152+
if rows:
153+
st.dataframe(rows, use_container_width=True)
154+
else:
155+
st.info("문서 적합성 평가 결과가 비어 있습니다.")
156+
127157
if should_show("show_token_usage"):
128158
st.markdown("---")
129159
token_summary = TokenUtils.get_token_usage_summary(data=res["messages"])

llm_utils/chains.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@
1515
)
1616
from pydantic import BaseModel, Field
1717
from llm_utils.output_parser.question_suitability import QuestionSuitability
18+
from llm_utils.output_parser.document_suitability import (
19+
DocumentSuitabilityList,
20+
)
1821

1922
from llm_utils.llm import get_llm
2023

@@ -125,7 +128,26 @@ def create_question_gate_chain(llm):
125128
return gate_prompt | llm.with_structured_output(QuestionSuitability)
126129

127130

131+
def create_document_suitability_chain(llm):
132+
"""
133+
문서 적합성 평가 체인을 생성합니다.
134+
135+
질문(question)과 검색 결과(tables)를 입력으로 받아
136+
테이블별 적합도 점수를 포함한 JSON 딕셔너리를 반환합니다.
137+
138+
Returns:
139+
Runnable: invoke({"question": str, "tables": dict}) -> {"results": DocumentSuitability[]}
140+
"""
141+
142+
prompt = get_prompt_template("document_suitability_prompt")
143+
doc_prompt = ChatPromptTemplate.from_messages(
144+
[SystemMessagePromptTemplate.from_template(prompt)]
145+
)
146+
return doc_prompt | llm.with_structured_output(DocumentSuitabilityList)
147+
148+
128149
query_maker_chain = create_query_maker_chain(llm)
129150
profile_extraction_chain = create_profile_extraction_chain(llm)
130151
query_enrichment_chain = create_query_enrichment_chain(llm)
131152
question_gate_chain = create_question_gate_chain(llm)
153+
document_suitability_chain = create_document_suitability_chain(llm)

llm_utils/graph_utils/base.py

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,14 @@
99
profile_extraction_chain,
1010
query_enrichment_chain,
1111
question_gate_chain,
12+
document_suitability_chain,
1213
)
1314

1415
from llm_utils.retrieval import search_tables
1516

1617
# 노드 식별자 정의
1718
QUESTION_GATE = "question_gate"
19+
EVALUATE_DOCUMENT_SUITABILITY = "evaluate_document_suitability"
1820
GET_TABLE_INFO = "get_table_info"
1921
TOOL = "tool"
2022
TABLE_FILTER = "table_filter"
@@ -28,6 +30,7 @@ class QueryMakerState(TypedDict):
2830
messages: Annotated[list, add_messages]
2931
user_database_env: str
3032
searched_tables: dict[str, dict[str, str]]
33+
document_suitability: dict
3134
best_practice_query: str
3235
question_profile: dict
3336
generated_query: str
@@ -156,6 +159,70 @@ def get_table_info_node(state: QueryMakerState):
156159
return state
157160

158161

162+
# 노드 함수: DOCUMENT_SUITABILITY 노드
163+
def document_suitability_node(state: QueryMakerState):
164+
"""
165+
GET_TABLE_INFO에서 수집된 테이블 후보들에 대해 문서 적합성 점수를 계산하는 노드입니다.
166+
167+
질문(`messages[0].content`)과 `searched_tables`(테이블→칼럼 설명 맵)를 입력으로
168+
프롬프트 체인(`document_suitability_chain`)을 호출하고, 결과 딕셔너리를
169+
`document_suitability` 상태 키에 저장합니다.
170+
171+
Returns:
172+
QueryMakerState: 문서 적합성 평가 결과가 포함된 상태
173+
"""
174+
175+
# 관련 테이블이 없으면 즉시 반환
176+
if not state.get("searched_tables"):
177+
state["document_suitability"] = {}
178+
return state
179+
180+
res = document_suitability_chain.invoke(
181+
{
182+
"question": state["messages"][0].content,
183+
"tables": state["searched_tables"],
184+
}
185+
)
186+
187+
items = (
188+
res.get("results", [])
189+
if isinstance(res, dict)
190+
else getattr(res, "results", None)
191+
or (res.model_dump().get("results", []) if hasattr(res, "model_dump") else [])
192+
)
193+
194+
normalized = {}
195+
for x in items:
196+
d = (
197+
x.model_dump()
198+
if hasattr(x, "model_dump")
199+
else (
200+
x
201+
if isinstance(x, dict)
202+
else {
203+
"table_name": getattr(x, "table_name", ""),
204+
"score": getattr(x, "score", 0),
205+
"reason": getattr(x, "reason", ""),
206+
"matched_columns": getattr(x, "matched_columns", []),
207+
"missing_entities": getattr(x, "missing_entities", []),
208+
}
209+
)
210+
)
211+
t = d.get("table_name")
212+
if not t:
213+
continue
214+
normalized[t] = {
215+
"score": float(d.get("score", 0)),
216+
"reason": d.get("reason", ""),
217+
"matched_columns": d.get("matched_columns", []),
218+
"missing_entities": d.get("missing_entities", []),
219+
}
220+
221+
state["document_suitability"] = normalized
222+
223+
return state
224+
225+
159226
# 노드 함수: QUERY_MAKER 노드
160227
def query_maker_node(state: QueryMakerState):
161228
# 사용자 원 질문 + (있다면) 컨텍스트 보강 결과를 하나의 문자열로 결합

llm_utils/graph_utils/basic_graph.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,11 @@
55
QueryMakerState,
66
QUESTION_GATE,
77
GET_TABLE_INFO,
8+
EVALUATE_DOCUMENT_SUITABILITY,
89
QUERY_MAKER,
910
question_gate_node,
1011
get_table_info_node,
12+
document_suitability_node,
1113
query_maker_node,
1214
)
1315

@@ -23,6 +25,7 @@
2325
# 노드 추가
2426
builder.add_node(QUESTION_GATE, question_gate_node)
2527
builder.add_node(GET_TABLE_INFO, get_table_info_node)
28+
builder.add_node(EVALUATE_DOCUMENT_SUITABILITY, document_suitability_node)
2629
builder.add_node(QUERY_MAKER, query_maker_node)
2730

2831

@@ -40,7 +43,8 @@ def _route_after_gate(state: QueryMakerState):
4043
)
4144

4245
# 기본 엣지 설정
43-
builder.add_edge(GET_TABLE_INFO, QUERY_MAKER)
46+
builder.add_edge(GET_TABLE_INFO, EVALUATE_DOCUMENT_SUITABILITY)
47+
builder.add_edge(EVALUATE_DOCUMENT_SUITABILITY, QUERY_MAKER)
4448

4549
# QUERY_MAKER 노드 후 종료
4650
builder.add_edge(QUERY_MAKER, END)

llm_utils/graph_utils/enriched_graph.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55
QueryMakerState,
66
QUESTION_GATE,
77
GET_TABLE_INFO,
8+
EVALUATE_DOCUMENT_SUITABILITY,
89
PROFILE_EXTRACTION,
910
CONTEXT_ENRICHMENT,
1011
QUERY_MAKER,
1112
question_gate_node,
1213
get_table_info_node,
14+
document_suitability_node,
1315
profile_extraction_node,
1416
context_enrichment_node,
1517
query_maker_node,
@@ -27,6 +29,7 @@
2729
# 노드 추가
2830
builder.add_node(QUESTION_GATE, question_gate_node)
2931
builder.add_node(GET_TABLE_INFO, get_table_info_node)
32+
builder.add_node(EVALUATE_DOCUMENT_SUITABILITY, document_suitability_node)
3033
builder.add_node(PROFILE_EXTRACTION, profile_extraction_node)
3134
builder.add_node(CONTEXT_ENRICHMENT, context_enrichment_node)
3235
builder.add_node(QUERY_MAKER, query_maker_node)
@@ -46,7 +49,8 @@ def _route_after_gate(state: QueryMakerState):
4649
)
4750

4851
# 기본 엣지 설정
49-
builder.add_edge(GET_TABLE_INFO, PROFILE_EXTRACTION)
52+
builder.add_edge(GET_TABLE_INFO, EVALUATE_DOCUMENT_SUITABILITY)
53+
builder.add_edge(EVALUATE_DOCUMENT_SUITABILITY, PROFILE_EXTRACTION)
5054
builder.add_edge(PROFILE_EXTRACTION, CONTEXT_ENRICHMENT)
5155
builder.add_edge(CONTEXT_ENRICHMENT, QUERY_MAKER)
5256

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
"""
2+
DocumentSuitability 출력 모델.
3+
4+
LLM 구조화 출력으로부터 테이블별 적합성 평가 결과를 표현하는 Pydantic 모델입니다.
5+
최상위는 테이블명(string) -> 평가 객체 매핑을 담는 Root 모델입니다.
6+
"""
7+
8+
from typing import Dict, List
9+
from pydantic import BaseModel, Field
10+
11+
12+
class DocumentSuitability(BaseModel):
13+
"""
14+
단일 테이블에 대한 적합성 평가 결과.
15+
"""
16+
17+
table_name: str = Field(description="테이블명")
18+
score: float = Field(description="0.0~1.0 사이의 적합도 점수")
19+
reason: str = Field(description="한국어 한두 문장 근거")
20+
matched_columns: List[str] = Field(
21+
default_factory=list, description="질문과 직접 연관된 컬럼명 목록"
22+
)
23+
missing_entities: List[str] = Field(
24+
default_factory=list, description="부족한 엔티티/지표/기간 등"
25+
)
26+
27+
28+
class DocumentSuitabilityList(BaseModel):
29+
"""
30+
문서 적합성 평가 결과 리스트 래퍼.
31+
32+
OpenAI Structured Outputs 호환을 위해 명시적 최상위 키(`results`)를 둡니다.
33+
"""
34+
35+
results: List[DocumentSuitability] = Field(description="평가 결과 목록")
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
## 문서 적합성 평가 프롬프트 (Table Search 재랭킹)
2+
3+
당신은 데이터 카탈로그 평가자입니다. 주어진 사용자 질문과 검색 결과(테이블 → 칼럼 설명 맵)를 바탕으로, 각 테이블이 질문에 얼마나 적합한지 0~1 사이의 실수 점수로 평가하세요.
4+
5+
### 입력
6+
- **question**: {question}
7+
- **tables**: {tables}
8+
9+
### 과업
10+
1. **핵심 신호 추출**: 질문에서 엔터티/지표/시간/필터/그룹화 단서를 추출합니다.
11+
2. **테이블별 점수화**: 각 테이블의 칼럼·설명과의 연관성으로 적합도를 점수화합니다(0~1, 소수 셋째 자리 반올림).
12+
3. **근거와 보완점 제시**: 매칭된 칼럼과 부족한 요소(엔터티/지표/기간 등)를 한국어로 설명합니다.
13+
4. **정렬**: 결과를 점수 내림차순으로 정렬해 반환합니다.
14+
15+
### 평가 규칙(가이드)
16+
- **0.90~1.00**: 필요한 엔터티, 기간/시간 컬럼, 핵심 지표/측정 칼럼이 모두 존재. 직접 조회/집계만으로 답 가능.
17+
- **0.60~0.89**: 주요 신호 매칭, 일부 보완(기간/그룹 키/보조 칼럼) 필요. 조인 없이 근사 가능.
18+
- **0.30~0.59**: 일부만 매칭. 외부 컨텍스트나 조인 없이는 부정확/제한적.
19+
- **0.00~0.29**: 연관성 낮음. 스키마/도메인 불일치 또는 정책/운영성 테이블.
20+
21+
### 주의
22+
- 칼럼 이름/설명에 실제로 존재하지 않는 항목을 매칭하지 마세요(환각 금지).
23+
- 시간 요구(특정 날짜/기간)가 있으면 timestamp/date/created_at 등 시간 계열 키를 중시하세요.
24+
- 엔티티 키(예: id, user_id, product_id)의 존재 여부를 가산점으로 반영하세요.
25+
- 키 이름은 정확히 입력 맵의 키만 사용하세요(자유 추측 금지).
26+
27+
### 언어/출력 형식
28+
- 모든 텍스트 값은 한국어로 작성하세요.
29+
- 결과는 반드시 아래 JSON 스키마로만 반환하세요(추가/누락 키 금지).
30+
31+
### 출력(JSON 스키마)
32+
{{
33+
"results": [
34+
{{
35+
"table_name": string,
36+
"score": number, // 0.0~1.0, 소수 셋째 자리 반올림
37+
"reason": string, // 한국어 한두 문장 근거
38+
"matched_columns": string[],
39+
"missing_entities": string[]
40+
}}
41+
]
42+
}}
43+
44+
### 검증 규칙
45+
- score는 [0, 1] 범위로 클램핑하고 소수 셋째 자리까지 반올림하세요.
46+
- matched_columns는 해당 테이블 객체의 실제 키만 포함하세요(단, table_description 제외).
47+
- reason 및 missing_entities는 한국어로 작성하세요.

0 commit comments

Comments
 (0)