Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -1,4 +1,8 @@
|
|
| 1 |
import os
|
|
|
|
|
|
|
|
|
|
|
|
|
| 2 |
from flask import Flask, render_template, jsonify, request, Response
|
| 3 |
from flask_socketio import SocketIO, emit
|
| 4 |
import uuid
|
|
@@ -34,12 +38,16 @@ logging.basicConfig(
|
|
| 34 |
logger = logging.getLogger(__name__)
|
| 35 |
|
| 36 |
# --- 외부 모듈 임포트 ---
|
| 37 |
-
|
|
|
|
|
|
|
| 38 |
import leximind_prompts
|
| 39 |
|
| 40 |
# --- 전역 변수 ---
|
| 41 |
connected_clients = 0
|
| 42 |
search_document_number = 30
|
|
|
|
|
|
|
| 43 |
|
| 44 |
# --- 경로 설정 ---
|
| 45 |
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
@@ -62,18 +70,19 @@ active_sessions = {}
|
|
| 62 |
# --- RAG 객체 ---
|
| 63 |
region_rag_objects = {}
|
| 64 |
|
| 65 |
-
# --- Together AI 설정
|
| 66 |
TOGETHER_API_KEY = os.getenv("TOGETHER_API_KEY")
|
| 67 |
if not TOGETHER_API_KEY:
|
| 68 |
-
raise
|
| 69 |
-
|
| 70 |
|
| 71 |
try:
|
| 72 |
-
# TOGETHER_API_KEY를 사용해 클라이언트 초기화 (TOGETHER_API_KEY가 코드 내에 정의되어 있다고 가정)
|
| 73 |
client = Together(api_key=TOGETHER_API_KEY)
|
| 74 |
except NameError:
|
| 75 |
-
# TOGETHER_API_KEY가 정의되지 않은 경우 환경 변수 사용을 시도
|
| 76 |
client = Together()
|
|
|
|
|
|
|
|
|
|
| 77 |
|
| 78 |
rag_connection_status_info = ""
|
| 79 |
|
|
@@ -82,7 +91,6 @@ def load_rag_objects():
|
|
| 82 |
global region_rag_objects
|
| 83 |
global rag_connection_status_info
|
| 84 |
|
| 85 |
-
# 로딩 스레드 시작 로그를 추가하여 Gunicorn 로그에서 확인 가능하게 함
|
| 86 |
logger.info(">>> [RAG_LOADER] RAG 로딩 스레드 시작 <<<")
|
| 87 |
|
| 88 |
for region, path in region_paths.items():
|
|
@@ -96,14 +104,16 @@ def load_rag_objects():
|
|
| 96 |
socketio.emit('message', {'message': f"[{region}] RAG 로딩 중..."})
|
| 97 |
rag_connection_status_info = f"[{region}] RAG 로딩 중..."
|
| 98 |
|
| 99 |
-
#
|
| 100 |
-
|
| 101 |
sqlite_conn.close()
|
|
|
|
| 102 |
db_path = os.path.join(path, "metadata_mapping.db")
|
| 103 |
new_conn = sqlite3.connect(db_path, check_same_thread=False)
|
| 104 |
|
|
|
|
| 105 |
region_rag_objects[region] = {
|
| 106 |
-
"
|
| 107 |
"vectorstore": vectorstore,
|
| 108 |
"sqlite_conn": new_conn
|
| 109 |
}
|
|
@@ -114,8 +124,7 @@ def load_rag_objects():
|
|
| 114 |
except Exception as e:
|
| 115 |
error_msg = f"[{region}] 로딩 실패: {str(e)}"
|
| 116 |
logger.info(error_msg)
|
| 117 |
-
|
| 118 |
-
traceback.logger.info_exc()
|
| 119 |
socketio.emit('message', {'message': error_msg})
|
| 120 |
|
| 121 |
socketio.emit('message', {'message': "Ready to Search"})
|
|
@@ -128,65 +137,57 @@ def index():
|
|
| 128 |
return render_template('chat_v03.html')
|
| 129 |
|
| 130 |
# 전역 변수에 기본값 추가
|
| 131 |
-
Search_each_all_mode = True
|
| 132 |
|
| 133 |
@socketio.on('search_query')
|
| 134 |
def handle_search_query(data):
|
|
|
|
|
|
|
| 135 |
global Search_each_all_mode
|
| 136 |
-
global current_dir
|
| 137 |
|
| 138 |
-
# 세션 ID 생성
|
| 139 |
session_id = str(uuid.uuid4())
|
| 140 |
active_sessions[session_id] = True
|
| 141 |
|
| 142 |
-
# 클라이언트에 session_id 전달
|
| 143 |
emit('search_started', {'session_id': session_id})
|
| 144 |
|
| 145 |
try:
|
| 146 |
-
# 클라이언트에서 전송된 검색 모드 사용
|
| 147 |
Search_each_all_mode = data.get('searchEachMode', True)
|
| 148 |
-
|
| 149 |
query = data.get('query', '')
|
| 150 |
regions = data.get('regions', [])
|
| 151 |
selected_regulations = data.get('selectedRegulations', [])
|
| 152 |
|
| 153 |
emit('search_status', {'status': 'processing', 'message': '검색 요청을 처리하는 중입니다...'})
|
| 154 |
|
| 155 |
-
|
| 156 |
-
|
| 157 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 158 |
|
| 159 |
-
# 번역 진행 상황 알림
|
| 160 |
emit('search_status', {'status': 'translating', 'message': '질문에 대해 생각 중입니다...'})
|
| 161 |
|
| 162 |
if session_id not in active_sessions:
|
| 163 |
-
emit('search_cancelled', {'message': '검색이 취소되었습니다.'})
|
| 164 |
-
emit('search_status', {'status': 'processing', 'message': 'Ready to search'})
|
| 165 |
return
|
| 166 |
|
| 167 |
Translated_query = Gemma3_AI_Translate(query)
|
| 168 |
emit('search_status', {'status': 'translated', 'message': f'번역 완료: {Translated_query}'})
|
| 169 |
-
logger.info(f"Query: Original query : {query}")
|
| 170 |
-
logger.info(f"Query: Translated_query : {Translated_query}")
|
| 171 |
|
| 172 |
if selected_regulations:
|
|
|
|
| 173 |
cont_selected_num = 0
|
| 174 |
|
| 175 |
-
# 파일로 저장
|
| 176 |
output_path = os.path.join(current_dir, "merged_ai_messages.txt")
|
| 177 |
-
|
| 178 |
if os.path.exists(output_path):
|
| 179 |
os.remove(output_path)
|
| 180 |
-
logger.info(f"기존 파일 삭제 완료: {output_path}")
|
| 181 |
|
| 182 |
-
|
| 183 |
grouped_regulations = group_regulations_by_type(selected_regulations)
|
| 184 |
emit('search_status', {'status': 'searching', 'message': f'선택된 {len(selected_regulations)}개 법규를 타입별로 통합하여 검색 중...'})
|
| 185 |
|
| 186 |
# 타입별로 필터 생성
|
| 187 |
combined_filters = create_combined_filters(grouped_regulations)
|
| 188 |
-
logger.info(f"통합 필터: {combined_filters}")
|
| 189 |
-
|
| 190 |
combined_cleaned_filter = {k: v for k, v in combined_filters.items() if v}
|
| 191 |
|
| 192 |
if Search_each_all_mode:
|
|
@@ -196,14 +197,12 @@ def handle_search_query(data):
|
|
| 196 |
total_search_num = sum(len(v) for v in combined_cleaned_filter.values())
|
| 197 |
i = 0
|
| 198 |
for RegType, RegNames in combined_cleaned_filter.items():
|
| 199 |
-
if RegNames:
|
| 200 |
for RegName in RegNames:
|
| 201 |
i = i + 1
|
| 202 |
-
|
| 203 |
-
|
| 204 |
if session_id not in active_sessions:
|
| 205 |
emit('search_cancelled', {'message': '검색이 취소되었습니다.'})
|
| 206 |
-
emit('search_status', {'status': 'processing', 'message': 'Ready to search'})
|
| 207 |
return
|
| 208 |
|
| 209 |
emit('search_status', {
|
|
@@ -214,15 +213,12 @@ def handle_search_query(data):
|
|
| 214 |
|
| 215 |
# 법규 타입별 필터 생성
|
| 216 |
current_filters = create_filter_by_type(RegType, RegName)
|
| 217 |
-
logger.info(f"생성된 필터: {current_filters}")
|
| 218 |
|
| 219 |
-
|
|
|
|
| 220 |
|
| 221 |
if Rag_Results:
|
| 222 |
-
if session_id not in active_sessions:
|
| 223 |
-
emit('search_cancelled', {'message': '검색이 취소되었습니다.'})
|
| 224 |
-
emit('search_status', {'status': 'processing', 'message': 'Ready to search'})
|
| 225 |
-
return
|
| 226 |
|
| 227 |
emit('search_status', {
|
| 228 |
'status': 'ai_processing',
|
|
@@ -230,13 +226,9 @@ def handle_search_query(data):
|
|
| 230 |
})
|
| 231 |
|
| 232 |
AImessage = RegAI(query, Rag_Results, ResultFile_FolderAddress)
|
| 233 |
-
logger.info(f"Answer: {AImessage}")
|
| 234 |
|
| 235 |
-
if session_id not in active_sessions:
|
| 236 |
-
emit('search_cancelled', {'message': '검색이 취소되었습니다.'})
|
| 237 |
-
return
|
| 238 |
|
| 239 |
-
# 각 법규별 결과를 실시간으로 전송 (타입 정보 포함)
|
| 240 |
emit('regulation_result', {
|
| 241 |
'regulation_title': f"[{RegName}]",
|
| 242 |
'regulation_index': i,
|
|
@@ -244,7 +236,6 @@ def handle_search_query(data):
|
|
| 244 |
'result': AImessage
|
| 245 |
})
|
| 246 |
|
| 247 |
-
# 파일에 저장
|
| 248 |
if isinstance(AImessage, str) and AImessage.strip():
|
| 249 |
with open(output_path, "a", encoding="utf-8") as f:
|
| 250 |
cont_selected_num += 1
|
|
@@ -254,27 +245,28 @@ def handle_search_query(data):
|
|
| 254 |
|
| 255 |
emit('search_complete', {'status': 'completed', 'message': '모든 법규 검색이 완료되었습니다.'})
|
| 256 |
else:
|
| 257 |
-
|
|
|
|
| 258 |
|
| 259 |
if session_id in active_sessions:
|
| 260 |
emit('search_status', {'status': 'ai_processing', 'message': 'AI가 통합 답변을 생성 중...'})
|
| 261 |
AImessage = RegAI(query, Rag_Results, ResultFile_FolderAddress)
|
| 262 |
-
logger.info(f"Answer: {AImessage}")
|
| 263 |
|
| 264 |
if session_id in active_sessions:
|
| 265 |
emit('search_result', {'result': AImessage})
|
| 266 |
emit('search_complete', {'status': 'completed', 'message': '통합 검색이 완료되었습니다.'})
|
| 267 |
|
| 268 |
else:
|
|
|
|
| 269 |
emit('search_status', {'status': 'searching_all', 'message': '전체 법규에서 검색 중...'})
|
| 270 |
|
| 271 |
# 필터 없이 검색
|
| 272 |
-
|
|
|
|
| 273 |
|
| 274 |
if session_id in active_sessions:
|
| 275 |
emit('search_status', {'status': 'ai_processing', 'message': 'AI가 답변을 생성 중...'})
|
| 276 |
AImessage = RegAI(query, Rag_Results, ResultFile_FolderAddress)
|
| 277 |
-
logger.info(f"Answer: {AImessage}")
|
| 278 |
|
| 279 |
if session_id in active_sessions:
|
| 280 |
emit('search_result', {'result': AImessage})
|
|
@@ -282,9 +274,9 @@ def handle_search_query(data):
|
|
| 282 |
|
| 283 |
except Exception as e:
|
| 284 |
print(f"검색 오류: {e}")
|
|
|
|
| 285 |
emit('search_error', {'error': str(e), 'message': '검색 중 오류가 발생했습니다.'})
|
| 286 |
finally:
|
| 287 |
-
# 세션 정리
|
| 288 |
if session_id in active_sessions:
|
| 289 |
del active_sessions[session_id]
|
| 290 |
|
|
@@ -303,7 +295,6 @@ def get_reg_list():
|
|
| 303 |
data = request.get_json()
|
| 304 |
selected_regions = data.get('regions', [])
|
| 305 |
|
| 306 |
-
# 지역이 선택되지 않았으면 전체 지역으로 설정
|
| 307 |
if not selected_regions:
|
| 308 |
selected_regions = ["국내", "북미", "유럽"]
|
| 309 |
|
|
@@ -315,28 +306,20 @@ def get_reg_list():
|
|
| 315 |
for region in selected_regions:
|
| 316 |
rag = region_rag_objects.get(region)
|
| 317 |
if not rag:
|
| 318 |
-
continue
|
| 319 |
|
| 320 |
try:
|
| 321 |
-
# 이미 로드된 SQLite 연결 재사용
|
| 322 |
sqlite_conn = rag["sqlite_conn"]
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
if isinstance(reg_list_part, str):
|
| 330 |
-
|
| 331 |
-
|
| 332 |
-
if isinstance(
|
| 333 |
-
reg_list_section = [reg_list_section]
|
| 334 |
-
|
| 335 |
-
if isinstance(reg_list_chapter, str):
|
| 336 |
-
reg_list_chapter = [reg_list_chapter]
|
| 337 |
-
|
| 338 |
-
if isinstance(reg_list_jo, str):
|
| 339 |
-
reg_list_jo = [reg_list_jo]
|
| 340 |
|
| 341 |
all_reg_list_part.extend(reg_list_part)
|
| 342 |
all_reg_list_section.extend(reg_list_section)
|
|
@@ -345,19 +328,14 @@ def get_reg_list():
|
|
| 345 |
except Exception as e:
|
| 346 |
print(f"[{region}] DB 연결 오류: {e}")
|
| 347 |
|
| 348 |
-
# 중복 제거
|
| 349 |
-
#unique_reg_list_part = list(set(all_reg_list_part))
|
| 350 |
unique_reg_list_part = sorted(set(all_reg_list_part), key=reg_embedding_system.natural_sort_key)
|
| 351 |
-
|
| 352 |
-
#unique_reg_list_section = list(set(all_reg_list_section))
|
| 353 |
unique_reg_list_section = sorted(set(all_reg_list_section), key=reg_embedding_system.natural_sort_key)
|
| 354 |
-
|
| 355 |
-
#unique_reg_list_chapter = list(set(all_reg_list_chapter))
|
| 356 |
unique_reg_list_chapter = sorted(set(all_reg_list_chapter), key=reg_embedding_system.natural_sort_key)
|
| 357 |
-
|
| 358 |
-
#unique_reg_list_jo = list(set(all_reg_list_jo))
|
| 359 |
unique_reg_list_jo = sorted(set(all_reg_list_jo), key=reg_embedding_system.natural_sort_key)
|
| 360 |
|
|
|
|
|
|
|
| 361 |
text_result_part = "\n".join(str(item) for item in unique_reg_list_part)
|
| 362 |
text_result_section = "\n".join(str(item) for item in unique_reg_list_section)
|
| 363 |
text_result_chapter = "\n".join(str(item) for item in unique_reg_list_chapter)
|
|
@@ -374,16 +352,11 @@ def handle_connect():
|
|
| 374 |
global connected_clients
|
| 375 |
connected_clients += 1
|
| 376 |
|
| 377 |
-
# 클라이언트 IP 가져오기
|
| 378 |
client_ip = request.remote_addr
|
| 379 |
-
|
| 380 |
-
# 프록시(Nginx, Cloudflare 등)를 거치는 경우 실제 IP는 헤더에 들어있을 수 있음
|
| 381 |
if request.headers.get('X-Forwarded-For'):
|
| 382 |
-
# X-Forwarded-For 는 "client, proxy1, proxy2" 형태로 여러 IP가 있을 수 있음
|
| 383 |
client_ip = request.headers.get('X-Forwarded-For').split(',')[0].strip()
|
| 384 |
elif request.headers.get('X-Real-IP'):
|
| 385 |
client_ip = request.headers.get('X-Real-IP')
|
| 386 |
-
# Cloudflare의 경우
|
| 387 |
elif request.headers.get('CF-Connecting-IP'):
|
| 388 |
client_ip = request.headers.get('CF-Connecting-IP')
|
| 389 |
|
|
@@ -397,10 +370,6 @@ def handle_disconnect():
|
|
| 397 |
global connected_clients
|
| 398 |
connected_clients -= 1
|
| 399 |
logger.info(f"클라이언트 연결: {connected_clients}명")
|
| 400 |
-
#if connected_clients <= 0:
|
| 401 |
-
# cleanup_connections()
|
| 402 |
-
# logger.info("서버 종료")
|
| 403 |
-
# os._exit(0)
|
| 404 |
|
| 405 |
def cleanup_connections():
|
| 406 |
for region, rag in region_rag_objects.items():
|
|
@@ -410,85 +379,62 @@ def cleanup_connections():
|
|
| 410 |
except:
|
| 411 |
pass
|
| 412 |
|
| 413 |
-
# --- Together AI 분석
|
| 414 |
def Gemma3_AI_analysis(query_txt, content_txt):
|
| 415 |
content_txt = "\n".join(doc.page_content for doc in content_txt) if isinstance(content_txt, list) else str(content_txt)
|
| 416 |
query_txt = str(query_txt)
|
| 417 |
prompt = lexi_prompts.use_prompt(lexi_prompts.AI_system_prompt, query_txt=query_txt, content_txt=content_txt)
|
| 418 |
|
|
|
|
|
|
|
|
|
|
| 419 |
try:
|
| 420 |
response = client.chat.completions.create(
|
| 421 |
-
|
| 422 |
-
|
| 423 |
-
messages=[
|
| 424 |
-
{
|
| 425 |
-
"role": "user",
|
| 426 |
-
"content": prompt,
|
| 427 |
-
}
|
| 428 |
-
],
|
| 429 |
)
|
| 430 |
-
|
| 431 |
-
# 응답에서 결과 텍스트를 추출
|
| 432 |
AI_Result = response.choices[0].message.content
|
| 433 |
return AI_Result
|
| 434 |
-
|
| 435 |
except Exception as e:
|
| 436 |
-
# Together SDK의 오류는 requests.exceptions.RequestException이 아닌 다른 종류의 예외로 발생합니다.
|
| 437 |
-
# 따라서 일반적인 Exception으로 처리하는 것이 안전합니다.
|
| 438 |
logger.info(f"Together AI 분석 API 호출 실패: {e}")
|
| 439 |
-
traceback.print_exc()
|
| 440 |
return f"AI 분석 중 오류가 발생했습니다: {e}"
|
| 441 |
|
| 442 |
-
# --- Together AI 번역
|
| 443 |
def Gemma3_AI_Translate(query_txt):
|
| 444 |
query_txt = str(query_txt)
|
| 445 |
prompt = lexi_prompts.use_prompt(lexi_prompts.query_translator, query_txt=query_txt)
|
| 446 |
|
|
|
|
|
|
|
|
|
|
| 447 |
try:
|
| 448 |
response = client.chat.completions.create(
|
| 449 |
-
|
| 450 |
-
|
| 451 |
-
messages=[
|
| 452 |
-
{
|
| 453 |
-
"role": "user",
|
| 454 |
-
"content": prompt,
|
| 455 |
-
}
|
| 456 |
-
],
|
| 457 |
)
|
| 458 |
-
|
| 459 |
-
# 응답에서 결과 텍스트를 추출
|
| 460 |
AI_Result = response.choices[0].message.content
|
| 461 |
return AI_Result
|
| 462 |
-
|
| 463 |
except Exception as e:
|
| 464 |
-
# API 호출 실패 시 처리 (SDK 사용 시 일반 Exception으로 처리)
|
| 465 |
logger.info(f"Together AI 번역 API 호출 실패: {e}")
|
| 466 |
-
|
| 467 |
-
# traceback.logger.info_exc() 대신 traceback.print_exc() 사용 (권장)
|
| 468 |
-
# 만약 기존 로깅 시스템에서 해당 함수를 정의해 사용하고 있다면 그대로 두셔도 됩니다.
|
| 469 |
-
# 여기서는 표준 traceback 모듈을 사용합니다.
|
| 470 |
traceback.print_exc()
|
| 471 |
-
|
| 472 |
-
# 번역 실패 시 query_txt 변수를 반환 (기존 코드 로직 반영)
|
| 473 |
return query_txt
|
| 474 |
|
| 475 |
-
# --- 검색 ---
|
| 476 |
-
|
| 477 |
-
|
|
|
|
|
|
|
|
|
|
| 478 |
if not selected_regions:
|
| 479 |
selected_regions = list(region_rag_objects.keys())
|
| 480 |
|
| 481 |
print(f"Translated Query : {query}")
|
| 482 |
|
| 483 |
-
|
| 484 |
-
|
| 485 |
-
custom_filters = {} # 빈 딕셔너리로 대체 (필터 없음 = 전체 검색)
|
| 486 |
|
| 487 |
-
|
| 488 |
-
has_filters = any(custom_filters.get(key, []) for key in custom_filters.keys())
|
| 489 |
-
|
| 490 |
-
print(f"사용된 검색 필터: {custom_filters}")
|
| 491 |
-
print(f"필터 사용 여부: {has_filters}")
|
| 492 |
|
| 493 |
combined_results = []
|
| 494 |
|
|
@@ -497,27 +443,29 @@ def search_DB_from_multiple_regions(query, selected_regions, region_rag_objects,
|
|
| 497 |
if not rag:
|
| 498 |
continue
|
| 499 |
|
| 500 |
-
ensemble_retriever
|
|
|
|
| 501 |
vectorstore = rag["vectorstore"]
|
| 502 |
sqlite_conn = rag["sqlite_conn"]
|
| 503 |
|
| 504 |
-
if
|
| 505 |
if has_filters:
|
|
|
|
| 506 |
results = reg_embedding_system.search_with_metadata_filter(
|
| 507 |
-
|
| 508 |
vectorstore=vectorstore,
|
| 509 |
query=query,
|
| 510 |
k=search_document_number,
|
| 511 |
-
metadata_filter=
|
| 512 |
-
sqlite_conn=sqlite_conn
|
| 513 |
-
failsafe_search=failsafe_mode
|
| 514 |
)
|
| 515 |
else:
|
|
|
|
| 516 |
results = reg_embedding_system.smart_search_vectorstore(
|
| 517 |
-
|
|
|
|
| 518 |
query=query,
|
| 519 |
k=search_document_number,
|
| 520 |
-
vectorstore=vectorstore,
|
| 521 |
sqlite_conn=sqlite_conn,
|
| 522 |
enable_detailed_search=True
|
| 523 |
)
|
|
@@ -531,55 +479,40 @@ def search_DB_from_multiple_regions(query, selected_regions, region_rag_objects,
|
|
| 531 |
def RegAI(query, Rag_Results, ResultFile_FolderAddress):
|
| 532 |
gc.collect()
|
| 533 |
AI_Result = "검색 결과가 없습니다." if not Rag_Results else Gemma3_AI_analysis(query, Rag_Results)
|
| 534 |
-
|
| 535 |
-
#with open(ResultFile_FolderAddress, 'w', encoding='utf-8') as f:
|
| 536 |
-
# print("검색된 문서:", file=f)
|
| 537 |
-
# logger.info("검색된 문서:")
|
| 538 |
-
# for i, doc in enumerate(Rag_Results):
|
| 539 |
-
# print(f"문서 {i+1}: {doc.page_content[:200]}... (메타: {doc.metadata})", file=f)
|
| 540 |
-
# logger.info(f"문서 {i+1}: {doc.page_content[:200]}... (메타: {doc.metadata})")
|
| 541 |
-
|
| 542 |
-
# print("\n답변:", file=f)
|
| 543 |
-
# logger.info("\n답변:")
|
| 544 |
-
|
| 545 |
-
# print(AI_Result, file=f)
|
| 546 |
-
# logger.info(AI_Result)
|
| 547 |
-
|
| 548 |
return AI_Result
|
| 549 |
|
| 550 |
-
# 법규 타입별 필터 생성 함수
|
| 551 |
def create_filter_by_type(regulation_type, regulation_title):
|
| 552 |
-
"""
|
|
|
|
|
|
|
|
|
|
| 553 |
filter_dict = {
|
| 554 |
-
"
|
| 555 |
-
"
|
| 556 |
-
"
|
| 557 |
-
"
|
| 558 |
}
|
| 559 |
|
| 560 |
-
#
|
| 561 |
-
|
| 562 |
-
# 전체 키를 지원하는 매핑 (입력으로 'regulation_section' 등을 받는 경우)
|
| 563 |
type_mapping = {
|
| 564 |
-
"regulation_part": "
|
| 565 |
-
"regulation_section": "
|
| 566 |
-
"chapter_section": "
|
| 567 |
-
"jo": "
|
| 568 |
-
#
|
| 569 |
-
"part": "
|
| 570 |
-
"section": "
|
| 571 |
-
"chapter": "
|
| 572 |
}
|
| 573 |
-
|
| 574 |
|
| 575 |
-
filter_key = type_mapping.get(regulation_type, "
|
| 576 |
filter_dict[filter_key].append(regulation_title)
|
| 577 |
|
| 578 |
return filter_dict
|
| 579 |
|
| 580 |
# 법규들을 타입별로 그룹화하는 함수
|
| 581 |
def group_regulations_by_type(selected_regulations):
|
| 582 |
-
"""선택된 법규들을 타입별로 그룹화"""
|
| 583 |
grouped = {
|
| 584 |
"part": [],
|
| 585 |
"section": [],
|
|
@@ -596,87 +529,52 @@ def group_regulations_by_type(selected_regulations):
|
|
| 596 |
|
| 597 |
return grouped
|
| 598 |
|
| 599 |
-
# 통합 필터 생성 함수
|
| 600 |
def create_combined_filters(grouped_regulations):
|
| 601 |
-
"""그룹화된 법규들로부터 통합 필터 생성"""
|
| 602 |
filters = {
|
| 603 |
-
"
|
| 604 |
-
"
|
| 605 |
-
"
|
| 606 |
-
"
|
| 607 |
}
|
| 608 |
-
|
| 609 |
return filters
|
| 610 |
|
| 611 |
-
def natural_sort_key(text):
|
| 612 |
-
"""숫자가 포함된 문자열을 자연스럽게 정렬 (예: item1, item2, item10)"""
|
| 613 |
-
return [int(c) if c.isdigit() else c.lower() for c in re.split('([0-9]+)', str(text))]
|
| 614 |
-
|
| 615 |
def get_unique_metadata_values(
|
| 616 |
sqlite_conn: sqlite3.Connection,
|
| 617 |
key_name: str,
|
| 618 |
partial_match: Optional[str] = None
|
| 619 |
) -> List[str]:
|
| 620 |
-
"""
|
| 621 |
-
SQLite 'documents' 테이블에서 특정 컬럼(key_name)의 중복되지 않은
|
| 622 |
-
모든 고유 값 리스트를 반환합니다.
|
| 623 |
-
|
| 624 |
-
Args:
|
| 625 |
-
sqlite_conn: SQLite 데이터베이스 연결 객체.
|
| 626 |
-
key_name: 고유한 값을 가져올 컬럼 이름 (예: 'regulation_name', 'part_name').
|
| 627 |
-
partial_match: (선택 사항) 해당 문자열을 포함하는 값만 검색할 때 사용.
|
| 628 |
-
|
| 629 |
-
Returns:
|
| 630 |
-
중복이 제거된 고유한 값들의 리스트.
|
| 631 |
-
"""
|
| 632 |
-
|
| 633 |
text_result = ""
|
| 634 |
if not sqlite_conn:
|
| 635 |
-
print("[경고] SQLite 연결이 없어 고유 값 검색을 수행할 수 없습니다.")
|
| 636 |
return text_result
|
| 637 |
|
| 638 |
cursor = sqlite_conn.cursor()
|
| 639 |
-
|
| 640 |
-
# SQL 쿼리 구성
|
| 641 |
-
# 1. 컬럼 이름에 백틱(`)을 사용하여 안전성 확보
|
| 642 |
-
# 2. DISTINCT를 사용하여 중복 제거
|
| 643 |
-
|
| 644 |
sql_query = f"SELECT DISTINCT `{key_name}` FROM documents"
|
| 645 |
params = []
|
| 646 |
|
| 647 |
-
# 부분 문자열 검색 (LIKE) 조건 추가
|
| 648 |
if partial_match:
|
| 649 |
sql_query += f" WHERE `{key_name}` LIKE ?"
|
| 650 |
params.append(f"%{partial_match}%")
|
| 651 |
|
| 652 |
try:
|
| 653 |
cursor.execute(sql_query, params)
|
| 654 |
-
|
| 655 |
-
# 쿼리 결과에서 첫 번째 항목 (값)만 추출
|
| 656 |
unique_values = [row[0] for row in cursor.fetchall() if row[0] is not None]
|
| 657 |
-
unique_values.sort(key=natural_sort_key)
|
| 658 |
text_result = "\n".join(str(value) for value in unique_values)
|
| 659 |
-
|
| 660 |
-
return text_result
|
| 661 |
-
|
| 662 |
-
except sqlite3.OperationalError as e:
|
| 663 |
-
# 컬럼 이름이 DB에 없을 때 발생하는 에러 처리
|
| 664 |
-
print(f"[에러] SQLite 쿼리 실행 실패 (컬럼 '{key_name}' 이름 오류 가능): {e}")
|
| 665 |
return text_result
|
| 666 |
except Exception as e:
|
| 667 |
-
print(f"[에러] 고유 값 검색
|
| 668 |
return text_result
|
| 669 |
|
| 670 |
-
|
| 671 |
# --- 실행 ---
|
| 672 |
if __name__ == '__main__':
|
| 673 |
-
# 로컬 개발용
|
| 674 |
threading.Thread(target=load_rag_objects, daemon=True).start()
|
| 675 |
time.sleep(2)
|
| 676 |
socketio.emit('message', {'message': '데이터 로딩 시작...'})
|
| 677 |
socketio.run(app, host='0.0.0.0', port=7860, debug=False)
|
| 678 |
else:
|
| 679 |
-
# Gunicorn용: 워커 시작 후 로딩
|
| 680 |
import atexit
|
| 681 |
loading_thread = threading.Thread(target=load_rag_objects, daemon=True)
|
| 682 |
loading_thread.start()
|
|
|
|
| 1 |
import os
|
| 2 |
+
#os.environ["PYDANTIC_V1_STYLE"] = "1"
|
| 3 |
+
#os.environ["PYDANTIC_SKIP_VALIDATING_CORE_SCHEMAS"] = "1"
|
| 4 |
+
# --------------------------------------------------------------------------
|
| 5 |
+
|
| 6 |
from flask import Flask, render_template, jsonify, request, Response
|
| 7 |
from flask_socketio import SocketIO, emit
|
| 8 |
import uuid
|
|
|
|
| 38 |
logger = logging.getLogger(__name__)
|
| 39 |
|
| 40 |
# --- 외부 모듈 임포트 ---
|
| 41 |
+
# [수정됨] v02 파일명에 맞춰 임포트 (파일명이 reg_embedding_system_v02.py라면 아래와 같이 수정)
|
| 42 |
+
# 여기서는 편의상 reg_embedding_system으로 사용하되 내용은 v02라고 가정합니다.
|
| 43 |
+
import reg_embedding_system_v02 as reg_embedding_system
|
| 44 |
import leximind_prompts
|
| 45 |
|
| 46 |
# --- 전역 변수 ---
|
| 47 |
connected_clients = 0
|
| 48 |
search_document_number = 30
|
| 49 |
+
Filtered_search = False
|
| 50 |
+
filters = {"regulation": []} # [수정됨] 기본 필터 키 변경
|
| 51 |
|
| 52 |
# --- 경로 설정 ---
|
| 53 |
current_dir = os.path.dirname(os.path.abspath(__file__))
|
|
|
|
| 70 |
# --- RAG 객체 ---
|
| 71 |
region_rag_objects = {}
|
| 72 |
|
| 73 |
+
# --- Together AI 설정 ---
|
| 74 |
TOGETHER_API_KEY = os.getenv("TOGETHER_API_KEY")
|
| 75 |
if not TOGETHER_API_KEY:
|
| 76 |
+
# 로컬 테스트용 예외 처리 등을 위해 raise 대신 경고 로그만 남길 수도 있음
|
| 77 |
+
logger.warning("TOGETHER_API_KEY가 설정되지 않았습니다.")
|
| 78 |
|
| 79 |
try:
|
|
|
|
| 80 |
client = Together(api_key=TOGETHER_API_KEY)
|
| 81 |
except NameError:
|
|
|
|
| 82 |
client = Together()
|
| 83 |
+
except Exception as e:
|
| 84 |
+
logger.warning(f"Together Client 초기화 실패 (API 키 확인 필요): {e}")
|
| 85 |
+
client = None
|
| 86 |
|
| 87 |
rag_connection_status_info = ""
|
| 88 |
|
|
|
|
| 91 |
global region_rag_objects
|
| 92 |
global rag_connection_status_info
|
| 93 |
|
|
|
|
| 94 |
logger.info(">>> [RAG_LOADER] RAG 로딩 스레드 시작 <<<")
|
| 95 |
|
| 96 |
for region, path in region_paths.items():
|
|
|
|
| 104 |
socketio.emit('message', {'message': f"[{region}] RAG 로딩 중..."})
|
| 105 |
rag_connection_status_info = f"[{region}] RAG 로딩 중..."
|
| 106 |
|
| 107 |
+
# [수정됨] load_embedding_from_faiss 반환값 변경 (Ensemble -> BM25)
|
| 108 |
+
bm25_retriever, vectorstore, sqlite_conn = reg_embedding_system.load_embedding_from_faiss(path)
|
| 109 |
sqlite_conn.close()
|
| 110 |
+
|
| 111 |
db_path = os.path.join(path, "metadata_mapping.db")
|
| 112 |
new_conn = sqlite3.connect(db_path, check_same_thread=False)
|
| 113 |
|
| 114 |
+
# [수정됨] 딕셔너리 키 변경 (ensemble_retriever -> bm25_retriever)
|
| 115 |
region_rag_objects[region] = {
|
| 116 |
+
"bm25_retriever": bm25_retriever,
|
| 117 |
"vectorstore": vectorstore,
|
| 118 |
"sqlite_conn": new_conn
|
| 119 |
}
|
|
|
|
| 124 |
except Exception as e:
|
| 125 |
error_msg = f"[{region}] 로딩 실패: {str(e)}"
|
| 126 |
logger.info(error_msg)
|
| 127 |
+
traceback.print_exc()
|
|
|
|
| 128 |
socketio.emit('message', {'message': error_msg})
|
| 129 |
|
| 130 |
socketio.emit('message', {'message': "Ready to Search"})
|
|
|
|
| 137 |
return render_template('chat_v03.html')
|
| 138 |
|
| 139 |
# 전역 변수에 기본값 추가
|
| 140 |
+
Search_each_all_mode = True
|
| 141 |
|
| 142 |
@socketio.on('search_query')
|
| 143 |
def handle_search_query(data):
|
| 144 |
+
global Filtered_search
|
| 145 |
+
global filters
|
| 146 |
global Search_each_all_mode
|
|
|
|
| 147 |
|
|
|
|
| 148 |
session_id = str(uuid.uuid4())
|
| 149 |
active_sessions[session_id] = True
|
| 150 |
|
|
|
|
| 151 |
emit('search_started', {'session_id': session_id})
|
| 152 |
|
| 153 |
try:
|
|
|
|
| 154 |
Search_each_all_mode = data.get('searchEachMode', True)
|
|
|
|
| 155 |
query = data.get('query', '')
|
| 156 |
regions = data.get('regions', [])
|
| 157 |
selected_regulations = data.get('selectedRegulations', [])
|
| 158 |
|
| 159 |
emit('search_status', {'status': 'processing', 'message': '검색 요청을 처리하는 중입니다...'})
|
| 160 |
|
| 161 |
+
# [수정됨] 초기 필터 구조 변경 (새로운 DB 스키마 반영)
|
| 162 |
+
filters = {
|
| 163 |
+
"regulation": [], # 구 regulation_part
|
| 164 |
+
"section": [], # 구 regulation_section
|
| 165 |
+
"chapter": [], # 구 chapter_section
|
| 166 |
+
"standard": [] # 구 jo
|
| 167 |
+
}
|
| 168 |
|
|
|
|
| 169 |
emit('search_status', {'status': 'translating', 'message': '질문에 대해 생각 중입니다...'})
|
| 170 |
|
| 171 |
if session_id not in active_sessions:
|
|
|
|
|
|
|
| 172 |
return
|
| 173 |
|
| 174 |
Translated_query = Gemma3_AI_Translate(query)
|
| 175 |
emit('search_status', {'status': 'translated', 'message': f'번역 완료: {Translated_query}'})
|
|
|
|
|
|
|
| 176 |
|
| 177 |
if selected_regulations:
|
| 178 |
+
Filtered_search = True
|
| 179 |
cont_selected_num = 0
|
| 180 |
|
|
|
|
| 181 |
output_path = os.path.join(current_dir, "merged_ai_messages.txt")
|
|
|
|
| 182 |
if os.path.exists(output_path):
|
| 183 |
os.remove(output_path)
|
|
|
|
| 184 |
|
| 185 |
+
# 통합 검색 모드 - 타입별로 그룹화
|
| 186 |
grouped_regulations = group_regulations_by_type(selected_regulations)
|
| 187 |
emit('search_status', {'status': 'searching', 'message': f'선택된 {len(selected_regulations)}개 법규를 타입별로 통합하여 검색 중...'})
|
| 188 |
|
| 189 |
# 타입별로 필터 생성
|
| 190 |
combined_filters = create_combined_filters(grouped_regulations)
|
|
|
|
|
|
|
| 191 |
combined_cleaned_filter = {k: v for k, v in combined_filters.items() if v}
|
| 192 |
|
| 193 |
if Search_each_all_mode:
|
|
|
|
| 197 |
total_search_num = sum(len(v) for v in combined_cleaned_filter.values())
|
| 198 |
i = 0
|
| 199 |
for RegType, RegNames in combined_cleaned_filter.items():
|
| 200 |
+
if RegNames:
|
| 201 |
for RegName in RegNames:
|
| 202 |
i = i + 1
|
| 203 |
+
|
|
|
|
| 204 |
if session_id not in active_sessions:
|
| 205 |
emit('search_cancelled', {'message': '검색이 취소되었습니다.'})
|
|
|
|
| 206 |
return
|
| 207 |
|
| 208 |
emit('search_status', {
|
|
|
|
| 213 |
|
| 214 |
# 법규 타입별 필터 생성
|
| 215 |
current_filters = create_filter_by_type(RegType, RegName)
|
|
|
|
| 216 |
|
| 217 |
+
# [수정됨] failsafe_mode 인자 제거 (v02 함수 정의에 없음)
|
| 218 |
+
Rag_Results = search_DB_from_multiple_regions(Translated_query, regions, region_rag_objects, current_filters)
|
| 219 |
|
| 220 |
if Rag_Results:
|
| 221 |
+
if session_id not in active_sessions: return
|
|
|
|
|
|
|
|
|
|
| 222 |
|
| 223 |
emit('search_status', {
|
| 224 |
'status': 'ai_processing',
|
|
|
|
| 226 |
})
|
| 227 |
|
| 228 |
AImessage = RegAI(query, Rag_Results, ResultFile_FolderAddress)
|
|
|
|
| 229 |
|
| 230 |
+
if session_id not in active_sessions: return
|
|
|
|
|
|
|
| 231 |
|
|
|
|
| 232 |
emit('regulation_result', {
|
| 233 |
'regulation_title': f"[{RegName}]",
|
| 234 |
'regulation_index': i,
|
|
|
|
| 236 |
'result': AImessage
|
| 237 |
})
|
| 238 |
|
|
|
|
| 239 |
if isinstance(AImessage, str) and AImessage.strip():
|
| 240 |
with open(output_path, "a", encoding="utf-8") as f:
|
| 241 |
cont_selected_num += 1
|
|
|
|
| 245 |
|
| 246 |
emit('search_complete', {'status': 'completed', 'message': '모든 법규 검색이 완료되었습니다.'})
|
| 247 |
else:
|
| 248 |
+
# [수정됨] failsafe_mode 인자 제거
|
| 249 |
+
Rag_Results = search_DB_from_multiple_regions(Translated_query, regions, region_rag_objects, combined_filters)
|
| 250 |
|
| 251 |
if session_id in active_sessions:
|
| 252 |
emit('search_status', {'status': 'ai_processing', 'message': 'AI가 통합 답변을 생성 중...'})
|
| 253 |
AImessage = RegAI(query, Rag_Results, ResultFile_FolderAddress)
|
|
|
|
| 254 |
|
| 255 |
if session_id in active_sessions:
|
| 256 |
emit('search_result', {'result': AImessage})
|
| 257 |
emit('search_complete', {'status': 'completed', 'message': '통합 검색이 완료되었습니다.'})
|
| 258 |
|
| 259 |
else:
|
| 260 |
+
Filtered_search = False
|
| 261 |
emit('search_status', {'status': 'searching_all', 'message': '전체 법규에서 검색 중...'})
|
| 262 |
|
| 263 |
# 필터 없이 검색
|
| 264 |
+
# [수정됨] failsafe_mode 인자 제거
|
| 265 |
+
Rag_Results = search_DB_from_multiple_regions(Translated_query, regions, region_rag_objects, None)
|
| 266 |
|
| 267 |
if session_id in active_sessions:
|
| 268 |
emit('search_status', {'status': 'ai_processing', 'message': 'AI가 답변을 생성 중...'})
|
| 269 |
AImessage = RegAI(query, Rag_Results, ResultFile_FolderAddress)
|
|
|
|
| 270 |
|
| 271 |
if session_id in active_sessions:
|
| 272 |
emit('search_result', {'result': AImessage})
|
|
|
|
| 274 |
|
| 275 |
except Exception as e:
|
| 276 |
print(f"검색 오류: {e}")
|
| 277 |
+
traceback.print_exc()
|
| 278 |
emit('search_error', {'error': str(e), 'message': '검색 중 오류가 발생했습니다.'})
|
| 279 |
finally:
|
|
|
|
| 280 |
if session_id in active_sessions:
|
| 281 |
del active_sessions[session_id]
|
| 282 |
|
|
|
|
| 295 |
data = request.get_json()
|
| 296 |
selected_regions = data.get('regions', [])
|
| 297 |
|
|
|
|
| 298 |
if not selected_regions:
|
| 299 |
selected_regions = ["국내", "북미", "유럽"]
|
| 300 |
|
|
|
|
| 306 |
for region in selected_regions:
|
| 307 |
rag = region_rag_objects.get(region)
|
| 308 |
if not rag:
|
| 309 |
+
continue
|
| 310 |
|
| 311 |
try:
|
|
|
|
| 312 |
sqlite_conn = rag["sqlite_conn"]
|
| 313 |
+
# [수정됨] v02 스키마(regulation, section, chapter, standard)에 맞춰 쿼리
|
| 314 |
+
reg_list_part = get_unique_metadata_values(sqlite_conn, "regulation") # 구 regulation_part
|
| 315 |
+
reg_list_section = get_unique_metadata_values(sqlite_conn, "section") # 구 regulation_section
|
| 316 |
+
reg_list_chapter = get_unique_metadata_values(sqlite_conn, "chapter") # 구 chapter_section
|
| 317 |
+
reg_list_jo = get_unique_metadata_values(sqlite_conn, "standard") # 구 jo
|
| 318 |
+
|
| 319 |
+
if isinstance(reg_list_part, str): reg_list_part = [reg_list_part]
|
| 320 |
+
if isinstance(reg_list_section, str): reg_list_section = [reg_list_section]
|
| 321 |
+
if isinstance(reg_list_chapter, str): reg_list_chapter = [reg_list_chapter]
|
| 322 |
+
if isinstance(reg_list_jo, str): reg_list_jo = [reg_list_jo]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 323 |
|
| 324 |
all_reg_list_part.extend(reg_list_part)
|
| 325 |
all_reg_list_section.extend(reg_list_section)
|
|
|
|
| 328 |
except Exception as e:
|
| 329 |
print(f"[{region}] DB 연결 오류: {e}")
|
| 330 |
|
| 331 |
+
# 자연 정렬 및 중복 제거
|
|
|
|
| 332 |
unique_reg_list_part = sorted(set(all_reg_list_part), key=reg_embedding_system.natural_sort_key)
|
|
|
|
|
|
|
| 333 |
unique_reg_list_section = sorted(set(all_reg_list_section), key=reg_embedding_system.natural_sort_key)
|
|
|
|
|
|
|
| 334 |
unique_reg_list_chapter = sorted(set(all_reg_list_chapter), key=reg_embedding_system.natural_sort_key)
|
|
|
|
|
|
|
| 335 |
unique_reg_list_jo = sorted(set(all_reg_list_jo), key=reg_embedding_system.natural_sort_key)
|
| 336 |
|
| 337 |
+
# Frontend(HTML)에서는 기존 key(reg_list_part 등)를 그대로 사용할 가능성이 높으므로
|
| 338 |
+
# 반환 변수명은 유지하되 내용은 새로운 DB 컬럼에서 가져온 것을 넣습니다.
|
| 339 |
text_result_part = "\n".join(str(item) for item in unique_reg_list_part)
|
| 340 |
text_result_section = "\n".join(str(item) for item in unique_reg_list_section)
|
| 341 |
text_result_chapter = "\n".join(str(item) for item in unique_reg_list_chapter)
|
|
|
|
| 352 |
global connected_clients
|
| 353 |
connected_clients += 1
|
| 354 |
|
|
|
|
| 355 |
client_ip = request.remote_addr
|
|
|
|
|
|
|
| 356 |
if request.headers.get('X-Forwarded-For'):
|
|
|
|
| 357 |
client_ip = request.headers.get('X-Forwarded-For').split(',')[0].strip()
|
| 358 |
elif request.headers.get('X-Real-IP'):
|
| 359 |
client_ip = request.headers.get('X-Real-IP')
|
|
|
|
| 360 |
elif request.headers.get('CF-Connecting-IP'):
|
| 361 |
client_ip = request.headers.get('CF-Connecting-IP')
|
| 362 |
|
|
|
|
| 370 |
global connected_clients
|
| 371 |
connected_clients -= 1
|
| 372 |
logger.info(f"클라이언트 연결: {connected_clients}명")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 373 |
|
| 374 |
def cleanup_connections():
|
| 375 |
for region, rag in region_rag_objects.items():
|
|
|
|
| 379 |
except:
|
| 380 |
pass
|
| 381 |
|
| 382 |
+
# --- Together AI 분석 ---
|
| 383 |
def Gemma3_AI_analysis(query_txt, content_txt):
|
| 384 |
content_txt = "\n".join(doc.page_content for doc in content_txt) if isinstance(content_txt, list) else str(content_txt)
|
| 385 |
query_txt = str(query_txt)
|
| 386 |
prompt = lexi_prompts.use_prompt(lexi_prompts.AI_system_prompt, query_txt=query_txt, content_txt=content_txt)
|
| 387 |
|
| 388 |
+
if not client:
|
| 389 |
+
return "AI Client가 초기화되지 않았습니다."
|
| 390 |
+
|
| 391 |
try:
|
| 392 |
response = client.chat.completions.create(
|
| 393 |
+
model="moonshotai/Kimi-K2-Instruct-0905",
|
| 394 |
+
messages=[{"role": "user", "content": prompt}],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
)
|
|
|
|
|
|
|
| 396 |
AI_Result = response.choices[0].message.content
|
| 397 |
return AI_Result
|
|
|
|
| 398 |
except Exception as e:
|
|
|
|
|
|
|
| 399 |
logger.info(f"Together AI 분석 API 호출 실패: {e}")
|
| 400 |
+
traceback.print_exc()
|
| 401 |
return f"AI 분석 중 오류가 발생했습니다: {e}"
|
| 402 |
|
| 403 |
+
# --- Together AI 번역 ---
|
| 404 |
def Gemma3_AI_Translate(query_txt):
|
| 405 |
query_txt = str(query_txt)
|
| 406 |
prompt = lexi_prompts.use_prompt(lexi_prompts.query_translator, query_txt=query_txt)
|
| 407 |
|
| 408 |
+
if not client:
|
| 409 |
+
return query_txt
|
| 410 |
+
|
| 411 |
try:
|
| 412 |
response = client.chat.completions.create(
|
| 413 |
+
model="moonshotai/Kimi-K2-Instruct-0905",
|
| 414 |
+
messages=[{"role": "user", "content": prompt}],
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 415 |
)
|
|
|
|
|
|
|
| 416 |
AI_Result = response.choices[0].message.content
|
| 417 |
return AI_Result
|
|
|
|
| 418 |
except Exception as e:
|
|
|
|
| 419 |
logger.info(f"Together AI 번역 API 호출 실패: {e}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 420 |
traceback.print_exc()
|
|
|
|
|
|
|
| 421 |
return query_txt
|
| 422 |
|
| 423 |
+
# --- 검색 (수정됨) ---
|
| 424 |
+
def search_DB_from_multiple_regions(query, selected_regions, region_rag_objects, custom_filters=None):
|
| 425 |
+
# [수정됨] failsafe_mode 인자 제거 (v02 함수 정의와 일치시킴)
|
| 426 |
+
global Filtered_search
|
| 427 |
+
global filters
|
| 428 |
+
|
| 429 |
if not selected_regions:
|
| 430 |
selected_regions = list(region_rag_objects.keys())
|
| 431 |
|
| 432 |
print(f"Translated Query : {query}")
|
| 433 |
|
| 434 |
+
search_filters = custom_filters if custom_filters is not None else filters
|
| 435 |
+
has_filters = any(search_filters.get(key, []) for key in search_filters.keys())
|
|
|
|
| 436 |
|
| 437 |
+
print(f"사용된 검색 필터: {search_filters}")
|
|
|
|
|
|
|
|
|
|
|
|
|
| 438 |
|
| 439 |
combined_results = []
|
| 440 |
|
|
|
|
| 443 |
if not rag:
|
| 444 |
continue
|
| 445 |
|
| 446 |
+
# [수정됨] 키 변경 (ensemble_retriever -> bm25_retriever)
|
| 447 |
+
bm25_retriever = rag["bm25_retriever"]
|
| 448 |
vectorstore = rag["vectorstore"]
|
| 449 |
sqlite_conn = rag["sqlite_conn"]
|
| 450 |
|
| 451 |
+
if bm25_retriever:
|
| 452 |
if has_filters:
|
| 453 |
+
# [수정됨] v02 시그니처 반영 (ensemble->bm25, failsafe 제거)
|
| 454 |
results = reg_embedding_system.search_with_metadata_filter(
|
| 455 |
+
bm25_retriever=bm25_retriever,
|
| 456 |
vectorstore=vectorstore,
|
| 457 |
query=query,
|
| 458 |
k=search_document_number,
|
| 459 |
+
metadata_filter=search_filters,
|
| 460 |
+
sqlite_conn=sqlite_conn
|
|
|
|
| 461 |
)
|
| 462 |
else:
|
| 463 |
+
# [수정됨] v02 시그니처 반영 (retriever->bm25, failsafe 제거)
|
| 464 |
results = reg_embedding_system.smart_search_vectorstore(
|
| 465 |
+
bm25_retriever=bm25_retriever,
|
| 466 |
+
vectorstore=vectorstore,
|
| 467 |
query=query,
|
| 468 |
k=search_document_number,
|
|
|
|
| 469 |
sqlite_conn=sqlite_conn,
|
| 470 |
enable_detailed_search=True
|
| 471 |
)
|
|
|
|
| 479 |
def RegAI(query, Rag_Results, ResultFile_FolderAddress):
|
| 480 |
gc.collect()
|
| 481 |
AI_Result = "검색 결과가 없습니다." if not Rag_Results else Gemma3_AI_analysis(query, Rag_Results)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 482 |
return AI_Result
|
| 483 |
|
| 484 |
+
# [수정됨] 법규 타입별 필터 생성 함수 - DB 스키마 변경 반영
|
| 485 |
def create_filter_by_type(regulation_type, regulation_title):
|
| 486 |
+
"""
|
| 487 |
+
법규 타입에 따라 적절한 필터 딕셔너리 생성
|
| 488 |
+
v02 DB 컬럼: regulation, section, chapter, standard
|
| 489 |
+
"""
|
| 490 |
filter_dict = {
|
| 491 |
+
"regulation": [],
|
| 492 |
+
"section": [],
|
| 493 |
+
"chapter": [],
|
| 494 |
+
"standard": []
|
| 495 |
}
|
| 496 |
|
| 497 |
+
# [수정됨] 기존 Frontend 타입 -> v02 DB 컬럼 매핑
|
|
|
|
|
|
|
| 498 |
type_mapping = {
|
| 499 |
+
"regulation_part": "regulation",
|
| 500 |
+
"regulation_section": "section",
|
| 501 |
+
"chapter_section": "chapter",
|
| 502 |
+
"jo": "standard",
|
| 503 |
+
# 축약형 지원
|
| 504 |
+
"part": "regulation",
|
| 505 |
+
"section": "section",
|
| 506 |
+
"chapter": "chapter",
|
| 507 |
}
|
|
|
|
| 508 |
|
| 509 |
+
filter_key = type_mapping.get(regulation_type, "regulation")
|
| 510 |
filter_dict[filter_key].append(regulation_title)
|
| 511 |
|
| 512 |
return filter_dict
|
| 513 |
|
| 514 |
# 법규들을 타입별로 그룹화하는 함수
|
| 515 |
def group_regulations_by_type(selected_regulations):
|
|
|
|
| 516 |
grouped = {
|
| 517 |
"part": [],
|
| 518 |
"section": [],
|
|
|
|
| 529 |
|
| 530 |
return grouped
|
| 531 |
|
| 532 |
+
# [수정됨] 통합 필터 생성 함수 - DB 키 변경 반영
|
| 533 |
def create_combined_filters(grouped_regulations):
|
| 534 |
+
"""그룹화된 법규들로부터 통합 필터 생성 (v02 DB 키 사용)"""
|
| 535 |
filters = {
|
| 536 |
+
"regulation": grouped_regulations["part"], # regulation_part -> regulation
|
| 537 |
+
"section": grouped_regulations["section"], # regulation_section -> section
|
| 538 |
+
"chapter": grouped_regulations["chapter"], # chapter_section -> chapter
|
| 539 |
+
"standard": grouped_regulations["jo"] # jo -> standard
|
| 540 |
}
|
|
|
|
| 541 |
return filters
|
| 542 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 543 |
def get_unique_metadata_values(
|
| 544 |
sqlite_conn: sqlite3.Connection,
|
| 545 |
key_name: str,
|
| 546 |
partial_match: Optional[str] = None
|
| 547 |
) -> List[str]:
|
| 548 |
+
"""SQLite 고유 값 반환"""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 549 |
text_result = ""
|
| 550 |
if not sqlite_conn:
|
|
|
|
| 551 |
return text_result
|
| 552 |
|
| 553 |
cursor = sqlite_conn.cursor()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 554 |
sql_query = f"SELECT DISTINCT `{key_name}` FROM documents"
|
| 555 |
params = []
|
| 556 |
|
|
|
|
| 557 |
if partial_match:
|
| 558 |
sql_query += f" WHERE `{key_name}` LIKE ?"
|
| 559 |
params.append(f"%{partial_match}%")
|
| 560 |
|
| 561 |
try:
|
| 562 |
cursor.execute(sql_query, params)
|
|
|
|
|
|
|
| 563 |
unique_values = [row[0] for row in cursor.fetchall() if row[0] is not None]
|
| 564 |
+
unique_values.sort(key=reg_embedding_system.natural_sort_key)
|
| 565 |
text_result = "\n".join(str(value) for value in unique_values)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 566 |
return text_result
|
| 567 |
except Exception as e:
|
| 568 |
+
print(f"[에러] 고유 값 검색 실패 ({key_name}): {e}")
|
| 569 |
return text_result
|
| 570 |
|
|
|
|
| 571 |
# --- 실행 ---
|
| 572 |
if __name__ == '__main__':
|
|
|
|
| 573 |
threading.Thread(target=load_rag_objects, daemon=True).start()
|
| 574 |
time.sleep(2)
|
| 575 |
socketio.emit('message', {'message': '데이터 로딩 시작...'})
|
| 576 |
socketio.run(app, host='0.0.0.0', port=7860, debug=False)
|
| 577 |
else:
|
|
|
|
| 578 |
import atexit
|
| 579 |
loading_thread = threading.Thread(target=load_rag_objects, daemon=True)
|
| 580 |
loading_thread.start()
|