scipious commited on
Commit
326f795
·
verified ·
1 Parent(s): 1ae3185

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +119 -221
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
- import reg_embedding_system
 
 
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 설정 (SDK 대신 API 호출에 사용) ---
66
  TOGETHER_API_KEY = os.getenv("TOGETHER_API_KEY")
67
  if not TOGETHER_API_KEY:
68
- raise EnvironmentError("TOGETHER_API_KEY가 설정되지 않았습니다. Hugging Face Secrets에 추가하세요.")
69
- # client = Together(api_key=TOGETHER_API_KEY) # <--- Together SDK 클라이언트 제거
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
- # NOTE: reg_embedding_system 모듈이 현재 환경에 설치/존재하는지 확인해야 합니다.
100
- ensemble_retriever, vectorstore, sqlite_conn = reg_embedding_system.load_embedding_from_faiss(path)
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
- "ensemble_retriever": ensemble_retriever,
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
- # [수정]: 상세한 에러 추적을 위해 traceback 추가
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
- logger.info("선택된 지역: %s", regions)
156
- logger.info("선택된 법규: %s", selected_regulations)
157
- logger.info("검색 모드: %s", "각각 검색" if Search_each_all_mode else "통합 검색")
 
 
 
 
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
- #RegType는 법규 유형, RegName은 법규 명칭
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
- Rag_Results = search_DB_from_multiple_regions(Translated_query, regions, region_rag_objects, current_filters, False) #마지막 False값은 유사한 값에 대한 검색을 하지 않겠다는 의미
 
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
- Rag_Results = search_DB_from_multiple_regions(Translated_query, regions, region_rag_objects, combined_filters, True)
 
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
- Rag_Results = search_DB_from_multiple_regions(Translated_query, regions, region_rag_objects, None, True)
 
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 # 해당 지역 RAG가 없으면 건너뜀
319
 
320
  try:
321
- # 이미 로드된 SQLite 연결 재사용
322
  sqlite_conn = rag["sqlite_conn"]
323
- reg_list_part = get_unique_metadata_values(sqlite_conn, "regulation_part")
324
- reg_list_section = get_unique_metadata_values(sqlite_conn, "regulation_section")
325
- reg_list_chapter = get_unique_metadata_values(sqlite_conn, "chapter_section")
326
- reg_list_jo = get_unique_metadata_values(sqlite_conn, "jo")
327
-
328
- # 문자열이면 리스트로 변환
329
- if isinstance(reg_list_part, str):
330
- reg_list_part = [reg_list_part]
331
-
332
- if isinstance(reg_list_section, str):
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 분석 (SDK -> requests 직접 호출로 변경) ---
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
- #model="meta-llama/Llama-4-Scout-17B-16E-Instruct", #비용 효율 측면 최고
422
- model="moonshotai/Kimi-K2-Instruct-0905", #오픈소스 최고 성능
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() # traceback.logger.info_exc() 대신 일반 print_exc()를 사용하거나, logging 모듈 설정을 확인하세요.
440
  return f"AI 분석 중 오류가 발생했습니다: {e}"
441
 
442
- # --- Together AI 번역 (SDK -> requests 직접 호출로 변경) ---
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
- #model="meta-llama/Llama-4-Scout-17B-16E-Instruct", #비용 효율 측면 최고
450
- model="moonshotai/Kimi-K2-Instruct-0905", #오픈소스 최고 성능
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
- def search_DB_from_multiple_regions(query, selected_regions, region_rag_objects, custom_filters=None, failsafe_mode=True):
 
 
 
478
  if not selected_regions:
479
  selected_regions = list(region_rag_objects.keys())
480
 
481
  print(f"Translated Query : {query}")
482
 
483
- # None 안전하게 처리
484
- if custom_filters is None:
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 = rag["ensemble_retriever"]
 
501
  vectorstore = rag["vectorstore"]
502
  sqlite_conn = rag["sqlite_conn"]
503
 
504
- if ensemble_retriever:
505
  if has_filters:
 
506
  results = reg_embedding_system.search_with_metadata_filter(
507
- ensemble_retriever=ensemble_retriever,
508
  vectorstore=vectorstore,
509
  query=query,
510
  k=search_document_number,
511
- metadata_filter=custom_filters,
512
- sqlite_conn=sqlite_conn,
513
- failsafe_search=failsafe_mode
514
  )
515
  else:
 
516
  results = reg_embedding_system.smart_search_vectorstore(
517
- retriever=ensemble_retriever,
 
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
- "regulation_part": [],
555
- "regulation_section": [],
556
- "chapter_section": [],
557
- "jo": []
558
  }
559
 
560
- # 타입별 매핑
561
-
562
- # 전체 키를 지원하는 매핑 (입력으로 'regulation_section' 등을 받는 경우)
563
  type_mapping = {
564
- "regulation_part": "regulation_part",
565
- "regulation_section": "regulation_section",
566
- "chapter_section": "chapter_section",
567
- "jo": "jo",
568
- # 혹시 짧은 형태로 들어오는 경우도 함께 지원
569
- "part": "regulation_part",
570
- "section": "regulation_section",
571
- "chapter": "chapter_section",
572
  }
573
-
574
 
575
- filter_key = type_mapping.get(regulation_type, "regulation_part")
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
- "regulation_part": grouped_regulations["part"],
604
- "regulation_section": grouped_regulations["section"],
605
- "chapter_section": grouped_regulations["chapter"],
606
- "jo": grouped_regulations["jo"]
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"[에러] 고유 값 검색 수 없는 오류 발생: {e}")
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()