jeongsoo commited on
Commit
53a0ebf
ยท
1 Parent(s): 61e46cb
Files changed (1) hide show
  1. app/app_routes.py +49 -173
app/app_routes.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ - API ๋ผ์šฐํŠธ ์ •์˜ (์ˆ˜์ • ์ œ์•ˆ ํฌํ•จ)
3
  """
4
 
5
  import os
@@ -8,6 +8,7 @@ import logging
8
  import tempfile
9
  import requests
10
  import time # ์•ฑ ์‹œ์ž‘ ์‹œ๊ฐ„ ๊ธฐ๋ก ์œ„ํ•ด ์ถ”๊ฐ€
 
11
  from flask import request, jsonify, render_template, send_from_directory, session, redirect, url_for
12
  from datetime import datetime
13
  from werkzeug.utils import secure_filename
@@ -32,7 +33,7 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
32
  ALLOWED_DOC_EXTENSIONS = {'txt', 'md', 'pdf', 'docx', 'csv'}
33
  return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_DOC_EXTENSIONS
34
 
35
- # --- ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ ๋ผ์šฐํŠธ (ํฐ ๋ฌธ์ œ ์—†์Œ, ๋กœ๊น… ๊ฐ•ํ™”) ---
36
  @app.route('/login', methods=['GET', 'POST'])
37
  def login():
38
  error = None
@@ -45,37 +46,28 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
45
  username = request.form.get('username', '')
46
  password = request.form.get('password', '')
47
  logger.info(f"์ž…๋ ฅ๋œ ์‚ฌ์šฉ์ž๋ช…: {username}")
48
- # logger.info(f"๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ์—ฌ๋ถ€: {len(password) > 0}") # ์‹ค์ œ ๋น„๋ฐ€๋ฒˆํ˜ธ ๋กœ๊น…์€ ๋ณด์•ˆ์ƒ ์ข‹์ง€ ์•Š์Œ
49
 
50
  valid_username = ADMIN_USERNAME
51
  valid_password = ADMIN_PASSWORD
52
  logger.info(f"๊ฒ€์ฆ์šฉ ์‚ฌ์šฉ์ž๋ช…: {valid_username}")
53
- # logger.info(f"๊ฒ€์ฆ์šฉ ๋น„๋ฐ€๋ฒˆํ˜ธ ์กด์žฌ ์—ฌ๋ถ€: {valid_password is not None and len(valid_password) > 0}")
54
 
55
  if username == valid_username and password == valid_password:
56
  logger.info(f"๋กœ๊ทธ์ธ ์„ฑ๊ณต: {username}")
57
- # logger.debug(f"์„ธ์…˜ ์„ค์ • ์ „: {session}") # ๋””๋ฒ„๊ทธ ๋ ˆ๋ฒจ๋กœ ๋ณ€๊ฒฝ
58
-
59
  session.permanent = True
60
  session['logged_in'] = True
61
  session['username'] = username
62
- # session.modified = True # Flask๋Š” ์„ธ์…˜ ๋ณ€๊ฒฝ ์‹œ ์ž๋™์œผ๋กœ modified ํ”Œ๋ž˜๊ทธ๋ฅผ ์„ค์ •ํ•˜๋ฏ€๋กœ ๋ช…์‹œ์  ํ˜ธ์ถœ ๋ถˆํ•„์š”
63
-
64
  logger.info(f"์„ธ์…˜ ์„ค์ • ์™„๋ฃŒ: {session}")
65
-
66
  redirect_to = next_url or url_for('index')
67
  logger.info(f"๋ฆฌ๋””๋ ‰์…˜ ๋Œ€์ƒ: {redirect_to}")
68
  response = redirect(redirect_to)
69
- # ์„ธ์…˜ ์ฟ ํ‚ค๊ฐ€ ์ œ๋Œ€๋กœ ์„ค์ •๋˜๋„๋ก ์‘๋‹ต ๋ฐ˜ํ™˜ ์ „ ํ™•์ธ (๋””๋ฒ„๊น…์šฉ)
70
  logger.debug(f"๋กœ๊ทธ์ธ ์‘๋‹ต ํ—ค๋” (Set-Cookie ํ™•์ธ): {response.headers.getlist('Set-Cookie')}")
71
  return response
72
  else:
73
  logger.warning("๋กœ๊ทธ์ธ ์‹คํŒจ: ์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜")
74
- # ์‹คํŒจ ์›์ธ ์ƒ์„ธ ๋กœ๊น…์€ ๋ณด์•ˆ ์œ„ํ—˜ ์†Œ์ง€๊ฐ€ ์žˆ์œผ๋ฏ€๋กœ ์ฃผ์˜
75
  error = '์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'
76
  else: # GET ์š”์ฒญ
77
  logger.info("๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ GET ์š”์ฒญ")
78
- if session.get('logged_in'): # .get() ์‚ฌ์šฉ์ด ๋” ์•ˆ์ „
79
  logger.info("์ด๋ฏธ ๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž, ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜")
80
  return redirect(url_for('index'))
81
 
@@ -86,48 +78,31 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
86
  @app.route('/logout')
87
  def logout():
88
  """๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ"""
89
- username = session.get('username', 'unknown') # ๋จผ์ € ์‚ฌ์šฉ์ž ์ด๋ฆ„ ๊ฐ€์ ธ์˜ค๊ธฐ
90
- if session.pop('logged_in', None): # pop์œผ๋กœ ์ œ๊ฑฐ ์‹œ๋„ ๋ฐ ์„ฑ๊ณต ์—ฌ๋ถ€ ํ™•์ธ
91
  session.pop('username', None)
92
- # session.modified = True # pop ์‚ฌ์šฉ ์‹œ ์ž๋™ ์ฒ˜๋ฆฌ๋จ
93
  logger.info(f"์‚ฌ์šฉ์ž {username} ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ ์™„๋ฃŒ. ํ˜„์žฌ ์„ธ์…˜: {session}")
94
  else:
95
  logger.warning("๋กœ๊ทธ์ธ๋˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ ๋กœ๊ทธ์•„์›ƒ ์‹œ๋„")
96
 
97
  logger.info("๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜")
98
  response = redirect(url_for('login'))
99
- # ๋กœ๊ทธ์•„์›ƒ ์‹œ ์ฟ ํ‚ค ์‚ญ์ œ ํ™•์ธ (๋””๋ฒ„๊น…์šฉ)
100
  logger.debug(f"๋กœ๊ทธ์•„์›ƒ ์‘๋‹ต ํ—ค๋” (Set-Cookie ํ™•์ธ): {response.headers.getlist('Set-Cookie')}")
101
  return response
102
 
103
- # --- ๋ฉ”์ธ ํŽ˜์ด์ง€ ๋ฐ ์ƒํƒœ ํ™•์ธ ---
104
  @app.route('/')
105
  @login_required
106
  def index():
107
  """๋ฉ”์ธ ํŽ˜์ด์ง€"""
108
- # app_ready_flag๋Š” register_routes ํ˜ธ์ถœ ์‹œ์ ์˜ ๊ฐ’์œผ๋กœ ๊ณ ์ •๋จ.
109
- # ์‹ค์‹œ๊ฐ„ ์ƒํƒœ๋ฅผ ๋ฐ˜์˜ํ•˜๋ ค๋ฉด app.py์˜ ์ „์—ญ ๋ณ€์ˆ˜๋ฅผ ์ง์ ‘ ์ฐธ์กฐํ•˜๊ฑฐ๋‚˜ ๋‹ค๋ฅธ ๋ฐฉ๋ฒ• ํ•„์š”.
110
- # ์—ฌ๊ธฐ์„œ๋Š” ์ „๋‹ฌ๋ฐ›์€ ํ”Œ๋ž˜๊ทธ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๊ณ  ๊ฐ€์ •.
111
-
112
- # !! ์ค‘์š”: app_ready_flag๋Š” register_routes ์‹œ์ ์˜ ๊ฐ’์ž…๋‹ˆ๏ฟฝ๏ฟฝ.
113
- # ์‹ค์‹œ๊ฐ„ ์ƒํƒœ๋ฅผ ๋ณด๋ ค๋ฉด app.py์˜ app_ready ๋ณ€์ˆ˜๋ฅผ ์ง์ ‘ ์ฐธ์กฐํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
114
- # ์˜ˆ: from app import app_ready (์ˆœํ™˜ ์ฐธ์กฐ ๋ฌธ์ œ ์—†์„ ๊ฒฝ์šฐ)
115
- # ์—ฌ๊ธฐ์„œ๋Š” ์ผ๋‹จ ์ „๋‹ฌ๋œ ๊ฐ’ ์‚ฌ์šฉ
116
- is_ready = app_ready_flag.is_set() if isinstance(app_ready_flag, threading.Event) else app_ready_flag # Event ๊ฐ์ฒด ๋˜๋Š” bool ๊ฐ€์ •
117
 
118
- # ์•ฑ ์‹œ์ž‘ ํ›„ ๊ฒฝ๊ณผ ์‹œ๊ฐ„ ๊ณ„์‚ฐ (ํŒŒ์ผ ์ˆ˜์ • ์‹œ๊ฐ„ ๋Œ€์‹  ์‹ค์ œ ์‹œ์ž‘ ์‹œ๊ฐ„ ์‚ฌ์šฉ)
119
  time_elapsed = time.time() - APP_START_TIME
120
 
121
- # 30์ดˆ ๊ฐ•์ œ Ready ๋กœ์ง ์ œ๊ฑฐ ๋˜๋Š” ์ˆ˜์ • ๊ถŒ์žฅ
122
- # if not is_ready and time_elapsed > 30:
123
- # logger.warning(f"์•ฑ์ด {time_elapsed:.1f}์ดˆ ์ด์ƒ ์ดˆ๊ธฐํ™” ์ค‘ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. (๊ฐ•์ œ Ready ๋กœ์ง ๋น„ํ™œ์„ฑํ™”๋จ)")
124
- # app_ready = True # ์ „์—ญ ๋ณ€์ˆ˜๋ฅผ ์ง์ ‘ ์ˆ˜์ •ํ•ด์•ผ ํ•จ
125
-
126
  if not is_ready:
127
  logger.info(f"์•ฑ์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•„ ๋กœ๋”ฉ ํŽ˜์ด์ง€ ํ‘œ์‹œ (๊ฒฝ๊ณผ ์‹œ๊ฐ„: {time_elapsed:.1f}์ดˆ)")
128
- # 503 ๋Œ€์‹  ๋กœ๋”ฉ ํŽ˜์ด์ง€๋ฅผ ์ •์ƒ์ ์œผ๋กœ ๋ณด์—ฌ์ฃผ๋Š” ๊ฒƒ์ด ์‚ฌ์šฉ์ž ๊ฒฝํ—˜์— ๋” ์ข‹์„ ์ˆ˜ ์žˆ์Œ
129
- return render_template('loading.html') # 503 ๋Œ€์‹  200 OK์™€ ๋กœ๋”ฉ ํŽ˜์ด์ง€
130
- # return render_template('loading.html'), 503 # ๊ธฐ์กด ๋กœ์ง
131
 
132
  logger.info("๋ฉ”์ธ ํŽ˜์ด์ง€ ์š”์ฒญ")
133
  return render_template('index.html')
@@ -141,21 +116,19 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
141
  logger.info(f"์•ฑ ์ƒํƒœ ํ™•์ธ ์š”์ฒญ: {'Ready' if is_ready else 'Not Ready'}")
142
  return jsonify({"ready": is_ready})
143
 
144
- # --- LLM API (ํฐ ๋ฌธ์ œ ์—†์–ด ๋ณด์ž„, ๋ฐฉ์–ด ์ฝ”๋“œ ์ถ”๊ฐ€) ---
145
  @app.route('/api/llm', methods=['GET', 'POST'])
146
  @login_required
147
  def llm_api():
148
  """์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ LLM ๋ชฉ๋ก ๋ฐ ์„ ํƒ API"""
149
  is_ready = app_ready_flag.is_set() if isinstance(app_ready_flag, threading.Event) else app_ready_flag
150
- if not is_ready:
151
- # LLM API๋Š” ์ดˆ๊ธฐํ™” ์ค‘์ด์–ด๋„ ๋ชฉ๋ก ์กฐํšŒ๋Š” ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ•  ์ˆ˜ ์žˆ์Œ (์„ ํƒ์ )
152
- # return jsonify({"error": "์•ฑ์ด ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."}), 503
153
- pass # ์ผ๋‹จ ์ง„ํ–‰ ํ—ˆ์šฉ
154
 
155
  if request.method == 'GET':
156
  logger.info("LLM ๋ชฉ๋ก ์š”์ฒญ")
157
  try:
158
- # llm_interface ๊ฐ์ฒด ์กด์žฌ ๋ฐ ์†์„ฑ ํ™•์ธ ๊ฐ•ํ™”
159
  if llm_interface is None or not hasattr(llm_interface, 'get_current_llm_details') or not hasattr(llm_interface, 'SUPPORTED_LLMS'):
160
  logger.error("LLM ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ํ•„์š”ํ•œ ์†์„ฑ์ด ์—†์Šต๋‹ˆ๋‹ค.")
161
  return jsonify({"error": "LLM ์ธํ„ฐํŽ˜์ด์Šค ์˜ค๋ฅ˜"}), 500
@@ -171,7 +144,7 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
171
  "current_llm": current_details
172
  })
173
  except Exception as e:
174
- logger.error(f"LLM ์ •๋ณด ์กฐํšŒ ์˜ค๋ฅ˜: {e}", exc_info=True) # exc_info ์ถ”๊ฐ€
175
  return jsonify({"error": "LLM ์ •๋ณด ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ"}), 500
176
 
177
  elif request.method == 'POST':
@@ -183,7 +156,6 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
183
  logger.info(f"LLM ๋ณ€๊ฒฝ ์š”์ฒญ: {llm_id}")
184
 
185
  try:
186
- # llm_interface ๊ฐ์ฒด ์กด์žฌ ๋ฐ ์†์„ฑ ํ™•์ธ ๊ฐ•ํ™”
187
  if llm_interface is None or not hasattr(llm_interface, 'set_llm') or not hasattr(llm_interface, 'llm_clients') or not hasattr(llm_interface, 'get_current_llm_details'):
188
  logger.error("LLM ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ํ•„์š”ํ•œ ์†์„ฑ/๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
189
  return jsonify({"error": "LLM ์ธํ„ฐํŽ˜์ด์Šค ์˜ค๋ฅ˜"}), 500
@@ -207,20 +179,18 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
207
  logger.error(f"LLM ๋ณ€๊ฒฝ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {e}", exc_info=True)
208
  return jsonify({"error": f"LLM ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"}), 500
209
 
210
- # --- Chat API (retriever None ์ฒดํฌ ์ˆ˜์ •) ---
211
  @app.route('/api/chat', methods=['POST'])
212
  @login_required
213
  def chat():
214
  """ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ์ฑ„๋ด‡ API"""
215
- # retriever ๊ฐ์ฒด๊ฐ€ None์ธ์ง€, ๊ทธ๋ฆฌ๊ณ  search ๋ฉ”์†Œ๋“œ๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธ
216
  if retriever is None or not hasattr(retriever, 'search'):
217
  logger.warning("์ฑ„ํŒ… API ์š”์ฒญ ์‹œ retriever๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ search ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
218
- # 503 ๋Œ€์‹  ์‚ฌ์šฉ์ž ์นœํ™”์ ์ธ ๋ฉ”์‹œ์ง€ ๋ฐ˜ํ™˜
219
  return jsonify({
220
  "answer": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰ ์—”์ง„์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.",
221
  "sources": [],
222
- "error": "Retriever not ready" # ํด๋ผ์ด์–ธํŠธ์—์„œ ๊ตฌ๋ถ„ํ•  ์ˆ˜ ์žˆ๋„๋ก ์ถ”๊ฐ€
223
- }), 200 # ๋˜๋Š” 503
224
 
225
  try:
226
  data = request.get_json()
@@ -230,22 +200,17 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
230
  query = data['query']
231
  logger.info(f"ํ…์ŠคํŠธ ์ฟผ๋ฆฌ ์ˆ˜์‹ : {query[:100]}...")
232
 
233
- # RAG ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰
234
- search_results = retriever.search(query, top_k=5, first_stage_k=6) # first_stage_k๋Š” base_retriever์— ์ „๋‹ฌ๋  ์ˆ˜ ์žˆ์Œ
235
 
236
- # ์ปจํ…์ŠคํŠธ ์ค€๋น„
237
  if DocumentProcessor is None or not hasattr(DocumentProcessor, 'prepare_rag_context'):
238
  logger.error("DocumentProcessor๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ prepare_rag_context ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
239
  return jsonify({"error": "๋ฌธ์„œ ์ฒ˜๋ฆฌ๊ธฐ ์˜ค๋ฅ˜"}), 500
240
- context = DocumentProcessor.prepare_rag_context(search_results, field="text") # 'text' ํ•„๋“œ๊ฐ€ ์žˆ๋‹ค๊ณ  ๊ฐ€์ •
241
 
242
  if not context:
243
  logger.warning(f"์ฟผ๋ฆฌ '{query[:50]}...'์— ๋Œ€ํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ.")
244
- # ์ปจํ…์ŠคํŠธ ์—†์ด LLM ํ˜ธ์ถœ ์‹œ๋„ ๋˜๋Š” ๊ธฐ๋ณธ ์‘๋‹ต ๋ฐ˜ํ™˜ ๊ฒฐ์ • ํ•„์š”
245
- # ์—ฌ๊ธฐ์„œ๋Š” LLM ํ˜ธ์ถœ ๋กœ์ง์—์„œ ์ฒ˜๋ฆฌํ•˜๋„๋ก ํ•จ
246
 
247
- # LLM์— ์งˆ์˜
248
- llm_id = data.get('llm_id', None) # ์š”์ฒญ์—์„œ llm_id ๊ฐ€์ ธ์˜ค๊ธฐ
249
  if llm_interface is None or not hasattr(llm_interface, 'rag_generate'):
250
  logger.error("LLM ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ rag_generate ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
251
  return jsonify({"error": "LLM ์ธํ„ฐํŽ˜์ด์Šค ์˜ค๋ฅ˜"}), 500
@@ -257,28 +222,22 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
257
  answer = llm_interface.rag_generate(query, context, llm_id=llm_id)
258
  logger.info(f"LLM ์‘๋‹ต ์ƒ์„ฑ ์™„๋ฃŒ (๊ธธ์ด: {len(answer)})")
259
 
260
- # ์†Œ์Šค ์ •๋ณด ์ถ”์ถœ (๊ธฐ์กด ๋กœ์ง ์œ ์ง€, ๋ฐฉ์–ด ์ฝ”๋“œ ๊ฐ•ํ™”)
261
  sources = []
262
  if search_results:
263
  for result in search_results:
264
  if not isinstance(result, dict):
265
  logger.warning(f"์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ˜•์‹: {type(result)}")
266
  continue
267
-
268
  source_info = {}
269
- source_key = result.get("source") # Langchain Document ํ˜ธํ™˜์„ฑ ์œ„ํ•ด metadata๋„ ํ™•์ธ
270
  if not source_key and "metadata" in result and isinstance(result["metadata"], dict):
271
  source_key = result["metadata"].get("source")
272
-
273
  if source_key:
274
  source_info["source"] = source_key
275
  source_info["score"] = result.get("rerank_score", result.get("score", 0))
276
-
277
- # CSV ID ์ถ”์ถœ ๋กœ์ง
278
  filetype = result.get("filetype")
279
  if not filetype and "metadata" in result and isinstance(result["metadata"], dict):
280
  filetype = result["metadata"].get("filetype")
281
-
282
  if "text" in result and filetype == "csv":
283
  try:
284
  text_lines = result["text"].strip().split('\n')
@@ -287,13 +246,10 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
287
  if ',' in first_line:
288
  first_column = first_line.split(',')[0].strip()
289
  source_info["id"] = first_column
290
- # logger.debug(f"CSV ์†Œ์Šค ID ์ถ”์ถœ: {first_column} from {source_info['source']}")
291
  except Exception as e:
292
  logger.warning(f"CSV ์†Œ์Šค ID ์ถ”์ถœ ์‹คํŒจ ({source_info.get('source')}): {e}")
293
-
294
  sources.append(source_info)
295
 
296
- # ์ตœ์ข… ์‘๋‹ต
297
  response_data = {
298
  "answer": answer,
299
  "sources": sources,
@@ -305,12 +261,11 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
305
  logger.error(f"์ฑ„ํŒ… ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
306
  return jsonify({"error": f"์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"}), 500
307
 
308
- # --- Voice Chat API (retriever, stt_client None ์ฒดํฌ ๊ฐ•ํ™”) ---
309
  @app.route('/api/voice', methods=['POST'])
310
  @login_required
311
  def voice_chat():
312
  """์Œ์„ฑ ์ฑ— API ์—”๋“œํฌ์ธํŠธ"""
313
- # ํ•„์ˆ˜ ์ปดํฌ๋„ŒํŠธ ํ™•์ธ
314
  if retriever is None or not hasattr(retriever, 'search'):
315
  logger.error("์Œ์„ฑ API ์š”์ฒญ ์‹œ retriever๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์Œ")
316
  return jsonify({"error": "๊ฒ€์ƒ‰ ์—”์ง„ ์ค€๋น„ ์•ˆ๋จ"}), 503
@@ -334,16 +289,12 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
334
  logger.info(f"์ˆ˜์‹ ๋œ ์˜ค๋””์˜ค ํŒŒ์ผ: {audio_file.filename} ({audio_file.content_type})")
335
 
336
  try:
337
- # ์˜ค๋””์˜ค ํŒŒ์ผ ์ž„์‹œ ์ €์žฅ ๋ฐ ์ฒ˜๋ฆฌ
338
  with tempfile.NamedTemporaryFile(delete=True, suffix=os.path.splitext(audio_file.filename)[1]) as temp_audio:
339
  audio_file.save(temp_audio.name)
340
  logger.info(f"์˜ค๋””์˜ค ํŒŒ์ผ์„ ์ž„์‹œ ์ €์žฅ: {temp_audio.name}")
341
-
342
- # STT ์ˆ˜ํ–‰ (๋ฐ”์ดํŠธ ๋˜๋Š” ๊ฒฝ๋กœ ์ „๋‹ฌ)
343
- # ์˜ˆ: ๋ฐ”์ดํŠธ ์ „๋‹ฌ
344
  with open(temp_audio.name, 'rb') as f_bytes:
345
  audio_bytes = f_bytes.read()
346
- stt_result = stt_client.transcribe_audio(audio_bytes, language="ko") # VitoSTT๊ฐ€ ๋ฐ”์ดํŠธ๋ฅผ ๋ฐ›๋Š”๋‹ค๊ณ  ๊ฐ€์ •
347
 
348
  if not isinstance(stt_result, dict) or not stt_result.get("success"):
349
  error_msg = stt_result.get("error", "์•Œ ์ˆ˜ ์—†๋Š” STT ์˜ค๋ฅ˜") if isinstance(stt_result, dict) else "STT ๊ฒฐ๊ณผ ํ˜•์‹ ์˜ค๋ฅ˜"
@@ -353,21 +304,19 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
353
  transcription = stt_result.get("text", "")
354
  if not transcription:
355
  logger.warning("์Œ์„ฑ์ธ์‹ ๊ฒฐ๊ณผ๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค.")
356
- # ๋นˆ ํ…์ŠคํŠธ๋ผ๋„ ์‘๋‹ต ๊ตฌ์กฐ๋Š” ์œ ์ง€
357
  return jsonify({
358
  "transcription": "",
359
  "answer": "์Œ์„ฑ์—์„œ ํ…์ŠคํŠธ๋ฅผ ์ธ์‹ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.",
360
  "sources": [],
361
  "llm": llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {}
362
- }), 200 # 400 ๋Œ€์‹  200 OK์™€ ๋ฉ”์‹œ์ง€
363
 
364
  logger.info(f"์Œ์„ฑ์ธ์‹ ์„ฑ๊ณต: {transcription[:50]}...")
365
 
366
- # --- ์ดํ›„ ๋กœ์ง์€ /api/chat๊ณผ ๋™์ผ ---
367
  search_results = retriever.search(transcription, top_k=5, first_stage_k=6)
368
  context = DocumentProcessor.prepare_rag_context(search_results, field="text")
369
 
370
- llm_id = request.form.get('llm_id', None) # form ๋ฐ์ดํ„ฐ์—์„œ llm_id ๊ฐ€์ ธ์˜ค๊ธฐ
371
  if not context:
372
  answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
373
  logger.info("์ปจํ…์ŠคํŠธ ์—†์ด ๊ธฐ๋ณธ ์‘๋‹ต ์ƒ์„ฑ")
@@ -375,7 +324,6 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
375
  answer = llm_interface.rag_generate(transcription, context, llm_id=llm_id)
376
  logger.info(f"LLM ์‘๋‹ต ์ƒ์„ฑ ์™„๋ฃŒ (๊ธธ์ด: {len(answer)})")
377
 
378
- # ์†Œ์Šค ์ •๋ณด ์ถ”์ถœ (chat API์™€ ๋™์ผ ๋กœ์ง ์‚ฌ์šฉ)
379
  sources = []
380
  if search_results:
381
  for result in search_results:
@@ -402,7 +350,6 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
402
  logger.warning(f"[์Œ์„ฑ์ฑ—] CSV ์†Œ์Šค ID ์ถ”์ถœ ์‹คํŒจ ({source_info.get('source')}): {e}")
403
  sources.append(source_info)
404
 
405
- # ์ตœ์ข… ์‘๋‹ต
406
  response_data = {
407
  "transcription": transcription,
408
  "answer": answer,
@@ -415,12 +362,11 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
415
  logger.error(f"์Œ์„ฑ ์ฑ— ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
416
  return jsonify({"error": "์Œ์„ฑ ์ฒ˜๋ฆฌ ์ค‘ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ ๋ฐœ์ƒ", "details": str(e)}), 500
417
 
418
- # --- Document Upload API (base_retriever None ์ฒดํฌ ๊ฐ•ํ™”) ---
419
  @app.route('/api/upload', methods=['POST'])
420
  @login_required
421
  def upload_document():
422
  """์ง€์‹๋ฒ ์ด์Šค ๋ฌธ์„œ ์—…๋กœ๋“œ API"""
423
- # base_retriever ๊ฐ์ฒด ๋ฐ ํ•„์ˆ˜ ๋ฉ”์†Œ๋“œ ํ™•์ธ
424
  if base_retriever is None or not hasattr(base_retriever, 'add_documents') or not hasattr(base_retriever, 'save'):
425
  logger.error("๋ฌธ์„œ ์—…๋กœ๋“œ API ์š”์ฒญ ์‹œ base_retriever๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ํ•„์ˆ˜ ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
426
  return jsonify({"error": "๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 503
@@ -429,28 +375,25 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
429
  return jsonify({"error": "๋ฌธ์„œ ํŒŒ์ผ์ด ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
430
 
431
  doc_file = request.files['document']
432
- if not doc_file or not doc_file.filename: # ํŒŒ์ผ ์กด์žฌ ๋ฐ ํŒŒ์ผ๋ช… ํ™•์ธ
433
  return jsonify({"error": "์„ ํƒ๋œ ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค."}), 400
434
 
435
  if not allowed_doc_file(doc_file.filename):
436
- ALLOWED_DOC_EXTENSIONS = {'txt', 'md', 'pdf', 'docx', 'csv'} # ์—ฌ๊ธฐ์„œ ๋‹ค์‹œ ์ •์˜ ํ•„์š”
437
  logger.warning(f"ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹: {doc_file.filename}")
438
  return jsonify({"error": f"ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค. ํ—ˆ์šฉ: {', '.join(ALLOWED_DOC_EXTENSIONS)}"}), 400
439
 
440
  try:
441
  filename = secure_filename(doc_file.filename)
442
- # DATA_FOLDER๊ฐ€ app.config์— ์„ค์ •๋˜์–ด ์žˆ๋‹ค๊ณ  ๊ฐ€์ •
443
- data_folder = app.config.get('DATA_FOLDER', os.path.join(os.path.dirname(__file__), '..', 'data')) # ๊ธฐ๋ณธ๊ฐ’ ์„ค์ •
444
- os.makedirs(data_folder, exist_ok=True) # ํด๋” ์—†์œผ๋ฉด ์ƒ์„ฑ
445
  filepath = os.path.join(data_folder, filename)
446
 
447
  doc_file.save(filepath)
448
  logger.info(f"๋ฌธ์„œ ์ €์žฅ ์™„๋ฃŒ: {filepath}")
449
 
450
- # ๋ฌธ์„œ ์ฒ˜๋ฆฌ (DocumentProcessor ๊ฐ์ฒด ๋ฐ ๋ฉ”์†Œ๋“œ ํ™•์ธ)
451
  if DocumentProcessor is None or not hasattr(DocumentProcessor, 'csv_to_documents') or not hasattr(DocumentProcessor, 'text_to_documents'):
452
  logger.error("DocumentProcessor๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ํ•„์š”ํ•œ ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
453
- # ์ด๋ฏธ ์ €์žฅ๋œ ํŒŒ์ผ ์‚ญ์ œ ๊ณ ๋ ค
454
  try: os.remove(filepath)
455
  except OSError: pass
456
  return jsonify({"error": "๋ฌธ์„œ ์ฒ˜๋ฆฌ๊ธฐ ์˜ค๋ฅ˜"}), 500
@@ -460,7 +403,6 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
460
  metadata = {"source": filename, "filename": filename, "filetype": file_ext, "filepath": filepath}
461
  docs = []
462
 
463
- # ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ํŒŒ์ผ ์ฝ๊ธฐ (์ธ์ฝ”๋”ฉ ์ฒ˜๋ฆฌ ํฌํ•จ)
464
  if file_ext in ['txt', 'md', 'csv']:
465
  try:
466
  with open(filepath, 'r', encoding='utf-8') as f:
@@ -476,79 +418,63 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
476
  except Exception as e_read:
477
  logger.error(f"ํŒŒ์ผ ์ฝ๊ธฐ ์˜ค๋ฅ˜ ({filename}): {e_read}")
478
  return jsonify({"error": f"ํŒŒ์ผ ์ฝ๊ธฐ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e_read)}"}), 500
479
- # PDF/DOCX ์ฒ˜๋ฆฌ ๋กœ์ง (๋ณ„๋„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ ํ•„์š”)
480
  elif file_ext == 'pdf':
481
  logger.warning("PDF ์ฒ˜๋ฆฌ๋Š” ๊ตฌํ˜„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
482
- # content = extract_text_from_pdf(filepath) # ์˜ˆ์‹œ
483
  elif file_ext == 'docx':
484
  logger.warning("DOCX ์ฒ˜๋ฆฌ๋Š” ๊ตฌํ˜„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
485
- # content = extract_text_from_docx(filepath) # ์˜ˆ์‹œ
486
 
487
- # ๋ฌธ์„œ ๋ถ„ํ• /์ฒ˜๋ฆฌ
488
- if content is not None: # ๋‚ด์šฉ์ด ์„ฑ๊ณต์ ์œผ๋กœ ์ฝํ˜”์„ ๋•Œ๋งŒ
489
  if file_ext == 'csv':
490
  logger.info(f"CSV ํŒŒ์ผ ์ฒ˜๋ฆฌ ์‹œ์ž‘: {filename}")
491
  docs = DocumentProcessor.csv_to_documents(content, metadata)
492
- elif file_ext in ['txt', 'md']: # ๊ธฐํƒ€ ํ…์ŠคํŠธ
493
  logger.info(f"ํ…์ŠคํŠธ ๋ฌธ์„œ ์ฒ˜๋ฆฌ ์‹œ์ž‘: {filename}")
494
  docs = DocumentProcessor.text_to_documents(
495
  content, metadata=metadata,
496
- chunk_size=512, chunk_overlap=50 # ์„ค์ •๊ฐ’ ์‚ฌ์šฉ
497
  )
498
- # PDF/DOCX์—์„œ ์ถ”์ถœ๋œ content ์ฒ˜๋ฆฌ ๋กœ์ง ์ถ”๊ฐ€ ๊ฐ€๋Šฅ
499
 
500
- # ๊ฒ€์ƒ‰๊ธฐ์— ์ถ”๊ฐ€ ๋ฐ ์ €์žฅ
501
  if docs:
502
  logger.info(f"{len(docs)}๊ฐœ ๋ฌธ์„œ ์ฒญํฌ๋ฅผ ๊ฒ€์ƒ‰๊ธฐ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค...")
503
  base_retriever.add_documents(docs)
504
 
505
  logger.info(f"๊ฒ€์ƒ‰๊ธฐ ์ƒํƒœ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค...")
506
- index_path = app.config.get('INDEX_PATH', os.path.join(data_folder, 'index')) # ๊ธฐ๋ณธ๊ฐ’ ์„ค์ •
507
- os.makedirs(os.path.dirname(index_path), exist_ok=True) # ์ธ๋ฑ์Šค ํด๋” ์—†์œผ๋ฉด ์ƒ์„ฑ
508
  try:
509
  base_retriever.save(index_path)
510
  logger.info("์ธ๋ฑ์Šค ์ €์žฅ ์™„๋ฃŒ")
511
- # TODO: ์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ(retriever) ์—…๋ฐ์ดํŠธ ๋กœ์ง ํ•„์š” ์‹œ ์ถ”๊ฐ€
512
  return jsonify({
513
  "success": True,
514
  "message": f"ํŒŒ์ผ '{filename}' ์—…๋กœ๋“œ ๋ฐ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ({len(docs)}๊ฐœ ์ฒญํฌ ์ถ”๊ฐ€)."
515
  })
516
  except Exception as e_save:
517
  logger.error(f"์ธ๋ฑ์Šค ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e_save}", exc_info=True)
518
- # ์ €์žฅ ์‹คํŒจ ์‹œ ์ถ”๊ฐ€๋œ ๋ฌธ์„œ ๋กค๋ฐฑ ๊ณ ๋ ค?
519
  return jsonify({"error": f"์ธ๋ฑ์Šค ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜: {str(e_save)}"}), 500
520
  else:
521
  logger.warning(f"ํŒŒ์ผ '{filename}'์—์„œ ์ฒ˜๋ฆฌํ•  ๋‚ด์šฉ์ด ์—†๊ฑฐ๋‚˜ ์ง€์›๋˜์ง€ ์•Š๋Š” ํ˜•์‹์ž…๋‹ˆ๋‹ค.")
522
- # ํŒŒ์ผ์€ ์ €์žฅ๋˜์—ˆ์œผ๋ฏ€๋กœ warning ๋ฐ˜ํ™˜
523
  return jsonify({
524
- "warning": True, # 'success' ๋Œ€์‹  'warning' ์‚ฌ์šฉ
525
  "message": f"ํŒŒ์ผ '{filename}'์ด ์ €์žฅ๋˜์—ˆ์ง€๋งŒ ์ฒ˜๋ฆฌํ•  ๋‚ด์šฉ์ด ์—†๊ฑฐ๋‚˜ ์ง€์›๋˜์ง€ ์•Š๋Š” ํ˜•์‹์ž…๋‹ˆ๋‹ค."
526
  })
527
 
528
  except Exception as e:
529
  logger.error(f"ํŒŒ์ผ ์—…๋กœ๋“œ ๋˜๋Š” ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
530
- # ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ ์ €์žฅ๋œ ํŒŒ์ผ ์‚ญ์ œ ๊ณ ๋ ค
531
  if 'filepath' in locals() and os.path.exists(filepath):
532
  try: os.remove(filepath)
533
  except OSError: pass
534
  return jsonify({"error": f"ํŒŒ์ผ ์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜: {str(e)}"}), 500
535
 
536
- # --- Document List API (์˜ค๋ฅ˜ ์›์ธ ๋ถ„์„ ํ•„์š”) ---
537
  @app.route('/api/documents', methods=['GET'])
538
  @login_required
539
  def list_documents():
540
  """์ง€์‹๋ฒ ์ด์Šค ๋ฌธ์„œ ๋ชฉ๋ก API"""
541
- # !! ์ค‘์š”: ์ด API๊ฐ€ 503์„ ๋ฐ˜ํ™˜ํ•˜๋Š” ์›์ธ์„ ์ฐพ์•„์•ผ ํ•จ !!
542
- # ํ˜„์žฌ ์ฝ”๋“œ ์ƒ์œผ๋กœ๋Š” base_retriever๊ฐ€ None์ผ ๋•Œ 503์ด ์•„๋‹Œ ๋นˆ ๋ชฉ๋ก์„ ๋ฐ˜ํ™˜ํ•จ.
543
- # 503 ์˜ค๋ฅ˜๋Š” ์ด ํ•จ์ˆ˜ ์‹คํ–‰ *์ „* ๋‹จ๊ณ„(์˜ˆ: ๋‹ค๋ฅธ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ, ๋ฏธ๋“ค์›จ์–ด, Flask ๋‚ด๋ถ€ ์˜ค๋ฅ˜)
544
- # ๋˜๋Š” base_retriever ์ ‘๊ทผ ์‹œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ ์ฒ˜๋ฆฌ ๊ณผ์ •์—์„œ ๋‚˜์˜ฌ ๊ฐ€๋Šฅ์„ฑ ์žˆ์Œ.
545
-
546
- logger.info("๋ฌธ์„œ ๋ชฉ๋ก API ์š”์ฒญ ์‹œ์ž‘") # ๋กœ๊ทธ ์ถ”๊ฐ€
547
 
548
- # base_retriever ์ƒํƒœ ์ƒ์„ธ ๋กœ๊น…
549
  if base_retriever is None:
550
  logger.warning("๋ฌธ์„œ API ์š”์ฒญ ์‹œ base_retriever๊ฐ€ None์ž…๋‹ˆ๋‹ค.")
551
- # 503 ๋Œ€์‹  ๋นˆ ๋ชฉ๋ก ๋ฐ˜ํ™˜ (์˜๋„๋œ ๋™์ž‘)
552
  return jsonify({"documents": [], "total_documents": 0, "total_chunks": 0})
553
  elif not hasattr(base_retriever, 'documents'):
554
  logger.warning("๋ฌธ์„œ API ์š”์ฒญ ์‹œ base_retriever์— 'documents' ์†์„ฑ์ด ์—†์Šต๋‹ˆ๋‹ค.")
@@ -560,42 +486,34 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
560
  logger.info(f"base_retriever.documents ํƒ€์ž…: {type(base_retriever.documents)}")
561
  logger.info(f"base_retriever.documents ๊ธธ์ด: {len(base_retriever.documents) if isinstance(base_retriever.documents, list) else 'N/A'}")
562
 
563
-
564
  try:
565
  sources = {}
566
  total_chunks = 0
567
- doc_list = base_retriever.documents # ์†์„ฑ์ด ์žˆ๋‹ค๊ณ  ๊ฐ€์ •ํ•˜๊ณ  ์ ‘๊ทผ
568
 
569
  if not isinstance(doc_list, list):
570
  logger.error(f"base_retriever.documents๊ฐ€ ๋ฆฌ์ŠคํŠธ๊ฐ€ ์•„๋‹˜: {type(doc_list)}")
571
- # ์ด ๊ฒฝ์šฐ 500 ์˜ค๋ฅ˜๋ฅผ ๋ฐ˜ํ™˜ํ•˜๊ฑฐ๋‚˜ ๋นˆ ๋ชฉ๋ก ๋ฐ˜ํ™˜
572
  return jsonify({"error": "๋‚ด๋ถ€ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ์˜ค๋ฅ˜"}), 500
573
 
574
  logger.info(f"์ด {len(doc_list)}๊ฐœ ๋ฌธ์„œ ์ฒญํฌ์—์„œ ์†Œ์Šค ๋ชฉ๋ก ์ƒ์„ฑ ์ค‘...")
575
  for i, doc in enumerate(doc_list):
576
- # ๊ฐ ๋ฌธ์„œ ์ฒญํฌ ์ฒ˜๋ฆฌ ๋กœ๊น… ์ถ”๊ฐ€
577
- # logger.debug(f"์ฒ˜๋ฆฌ ์ค‘์ธ ์ฒญํฌ {i}: {doc}") # ๋„ˆ๋ฌด ์ƒ์„ธํ•˜๋ฉด ์ฃผ์„ ์ฒ˜๋ฆฌ
578
-
579
  if not isinstance(doc, dict):
580
  logger.warning(f"์ฒญํฌ {i}๊ฐ€ ๋”•์…”๋„ˆ๋ฆฌ ํƒ€์ž…์ด ์•„๋‹˜: {type(doc)}")
581
- continue # ๋‹ค์Œ ์ฒญํฌ๋กœ ๋„˜์–ด๊ฐ
582
 
583
- # ์†Œ์Šค ์ •๋ณด ์ถ”์ถœ (๊ธฐ์กด ๋กœ์ง ๊ฐœ์„ )
584
  source = "unknown"
585
- metadata = doc.get("metadata") # metadata ๋จผ์ € ํ™•์ธ (Langchain Document ๊ตฌ์กฐ)
586
  if isinstance(metadata, dict):
587
  source = metadata.get("source", "unknown")
588
- if source == "unknown": # metadata์— ์—†์œผ๋ฉด doc ์ž์ฒด์—์„œ ์ฐพ๊ธฐ
589
  source = doc.get("source", "unknown")
590
 
591
  if source != "unknown":
592
  if source in sources:
593
  sources[source]["chunks"] += 1
594
  else:
595
- # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์šฐ์„  ์‚ฌ์šฉ
596
  filename = metadata.get("filename", source) if isinstance(metadata, dict) else source
597
  filetype = metadata.get("filetype", "unknown") if isinstance(metadata, dict) else "unknown"
598
- # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ์—†์œผ๋ฉด doc ์ž์ฒด์—์„œ ์ฐพ๊ธฐ
599
  if filename == source and "filename" in doc: filename = doc["filename"]
600
  if filetype == "unknown" and "filetype" in doc: filetype = doc["filetype"]
601
 
@@ -608,10 +526,8 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
608
  else:
609
  logger.warning(f"์ฒญํฌ {i}์—์„œ ์†Œ์Šค ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ: {doc}")
610
 
611
-
612
- # ๋ชฉ๋ก ํ˜•์‹ ๋ณ€ํ™˜ ๋ฐ ์ •๋ ฌ (๋ณ€๊ฒฝ ์—†์Œ)
613
  documents = [{"source": src, **info} for src, info in sources.items()]
614
- documents.sort(key=lambda x: x.get("filename", ""), reverse=False) # ํŒŒ์ผ๋ช… ๊ธฐ์ค€ ์ •๋ ฌ
615
 
616
  logger.info(f"๋ฌธ์„œ ๋ชฉ๋ก ์กฐํšŒ ์™„๋ฃŒ: {len(documents)}๊ฐœ ์†Œ์Šค ํŒŒ์ผ, {total_chunks}๊ฐœ ์ฒญํฌ")
617
  return jsonify({
@@ -621,51 +537,11 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
621
  })
622
 
623
  except Exception as e:
624
- # !! ์ค‘์š”: ์—ฌ๊ธฐ์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๊ฐ€ 503์œผ๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธ !!
625
  logger.error(f"๋ฌธ์„œ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์‹ฌ๊ฐํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
626
- # ์ผ๋ฐ˜์ ์ธ ๋‚ด๋ถ€ ์˜ค๋ฅ˜๋Š” 500 ๋ฐ˜ํ™˜
627
  return jsonify({"error": f"๋ฌธ์„œ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜: {str(e)}"}), 500
628
-
629
  ```
630
 
631
- **์ฃผ์š” ๋ฌธ์ œ์  ๋ฐ ์ˆ˜์ • ์ œ์•ˆ:**
632
-
633
- 1. **`/api/documents` 503 ์˜ค๋ฅ˜์˜ ๋ฏธ์Šคํ„ฐ๋ฆฌ:**
634
- * ์ œ๊ณต๋œ `list_documents` ํ•จ์ˆ˜ ์ฝ”๋“œ ์ž์ฒด์—๋Š” `app_ready` ์ƒํƒœ๋‚˜ `base_retriever`๊ฐ€ `None`์ธ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜์—ฌ 503 ์˜ค๋ฅ˜๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๋กœ์ง์ด **์—†์Šต๋‹ˆ๋‹ค.** ๋กœ๊ทธ์—์„œ 503์ด ๋ฐœ์ƒํ–ˆ๋‹ค๋ฉด, ์›์ธ์€ ๋‹ค์Œ ์ค‘ ํ•˜๋‚˜์ผ ๊ฐ€๋Šฅ์„ฑ์ด ๋†’์Šต๋‹ˆ๋‹ค:
635
- * **์‹ค์ œ ์‹คํ–‰ ์ค‘์ธ ์ฝ”๋“œ ๋ถˆ์ผ์น˜:** ํ˜„์žฌ ์„œ๋ฒ„์—์„œ ์‹คํ–‰ ์ค‘์ธ ์ฝ”๋“œ๊ฐ€ ์ œ๊ณตํ•ด์ฃผ์‹  ์ฝ”๋“œ์™€ ๋‹ค๋ฅผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (์˜ˆ: ์ด์ „ ๋ฒ„์ „์— `if not app_ready: return ..., 503` ์ฝ”๋“œ๊ฐ€ ๋‚จ์•„์žˆ์Œ)
636
- * **`base_retriever` ์ ‘๊ทผ ์˜ค๋ฅ˜:** `base_retriever.documents` ์†์„ฑ์— ์ ‘๊ทผํ•˜๋Š” ๊ณผ์ •์—์„œ ์˜ˆ๊ธฐ์น˜ ์•Š์€ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ•˜๊ณ , Flask์˜ ์ „์—ญ ์˜ค๋ฅ˜ ํ•ธ๋“ค๋Ÿฌ๋‚˜ ํŠน์ • ๋ฏธ๋“ค์›จ์–ด๊ฐ€ ์ด๋ฅผ 503์œผ๋กœ ์ฒ˜๋ฆฌํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. (์ผ๋ฐ˜์ ์œผ๋กœ๋Š” 500 Internal Server Error๊ฐ€ ๋ฐ˜ํ™˜๋ฉ๋‹ˆ๋‹ค.)
637
- * **์™ธ๋ถ€ ์š”์ธ:** ์›น ์„œ๋ฒ„(Nginx ๋“ฑ) ์„ค์ •์ด๋‚˜ ๋กœ๋“œ ๋ฐธ๋Ÿฐ์„œ ๋“ฑ Flask ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ์•ž๋‹จ์˜ ๋‹ค๋ฅธ ์‹œ์Šคํ…œ์—์„œ 503 ์˜ค๋ฅ˜๋ฅผ ๋ฐ˜ํ™˜ํ•  ์ˆ˜๋„ ์žˆ์Šต๋‹ˆ๋‹ค.
638
- * **์ˆ˜์ • ์ œ์•ˆ:**
639
- * `list_documents` ํ•จ์ˆ˜ ์‹œ์ž‘ ๋ถ€๋ถ„๊ณผ `try...except` ๋ธ”๋ก ๋‚ด๋ถ€์— **๋” ์ƒ์„ธํ•œ ๋กœ๊ทธ**๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ํ•จ์ˆ˜ ์‹คํ–‰ ํ๋ฆ„๊ณผ `base_retriever` ๊ฐ์ฒด ์ƒํƒœ๋ฅผ ๋ช…ํ™•ํžˆ ํŒŒ์•…ํ•ฉ๋‹ˆ๋‹ค. (์œ„ ์ฝ”๋“œ์— ๋กœ๊น… ์ถ”๊ฐ€๋จ)
640
- * ์‹คํ–‰ ์ค‘์ธ ์ฝ”๋“œ๊ฐ€ ์ตœ์‹  ๋ฒ„์ „์ธ์ง€ ๋‹ค์‹œ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
641
- * `base_retriever` ๊ฐ์ฒด ์ž์ฒด (`VectorRetriever` ํด๋ž˜์Šค)์˜ `documents` ์†์„ฑ ๊ตฌํ˜„์„ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค.
642
-
643
- 2. **`app_ready` ์ƒํƒœ ๊ด€๋ฆฌ ๋ฐ ์‚ฌ์šฉ:**
644
- * `register_routes` ํ•จ์ˆ˜๋Š” ์•ฑ ์‹œ์ž‘ ์‹œ ํ•œ ๋ฒˆ๋งŒ ํ˜ธ์ถœ๋˜๋ฏ€๋กœ, ์ธ์ž๋กœ ์ „๋‹ฌ๋œ `app_ready` ๊ฐ’์€ **ํ˜ธ์ถœ ์‹œ์ ์˜ ์Šค๋ƒ…์ƒท**์ž…๋‹ˆ๋‹ค. ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ๊ฐ€ ๋‚˜์ค‘์— `app.py`์˜ ์ „์—ญ `app_ready` ๊ฐ’์„ ๋ณ€๊ฒฝํ•ด๋„ `register_routes` ๋‚ด๋ถ€์˜ ์ง€์—ญ ๋ณ€์ˆ˜ `app_ready` (์ฝ”๋“œ์—์„œ๋Š” `app_ready_flag`๋กœ ๋ช…์นญ ๋ณ€๊ฒฝ ์ œ์•ˆ)๋Š” ์—…๋ฐ์ดํŠธ๋˜์ง€ ์•Š์Šต๋‹ˆ๋‹ค.
645
- * `index` ํ•จ์ˆ˜ ๋‚ด์—์„œ `nonlocal app_ready` ์‚ฌ์šฉ์€ ์ž˜๋ชป๋˜์—ˆ์Šต๋‹ˆ๋‹ค. `app_ready`๋Š” ์ „์—ญ ๋ณ€์ˆ˜์ด๋ฏ€๋กœ `global app_ready`๋ฅผ ์‚ฌ์šฉํ•˜๊ฑฐ๋‚˜, ๋” ์ข‹์€ ๋ฐฉ๋ฒ•์€ Flask์˜ `app.before_request` ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ๋‚˜ `g` ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์š”์ฒญ ์ปจํ…์ŠคํŠธ ๋‚ด์—์„œ ์ƒํƒœ๋ฅผ ํ™•์ธํ•˜๋Š” ๊ฒƒ์ž…๋‹ˆ๋‹ค. ํ˜น์€ `threading.Event` ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์Šค๋ ˆ๋“œ ๊ฐ„ ์ƒํƒœ๋ฅผ ์•ˆ์ „ํ•˜๊ฒŒ ๊ณต์œ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
646
- * `index` ํ•จ์ˆ˜์˜ 30์ดˆ ๊ฐ•์ œ Ready ๋กœ์ง์€ `os.path.getmtime(__file__)`์„ ์‚ฌ์šฉํ•˜๋Š”๋ฐ, ์ด๋Š” ํŒŒ์ผ ์ˆ˜์ • ์‹œ๊ฐ„์„ ๊ธฐ์ค€์œผ๋กœ ํ•˜๋ฏ€๋กœ ์•ฑ์˜ ์‹ค์ œ ์‹œ์ž‘ ์‹œ๊ฐ„๊ณผ ๋‹ฌ๋ผ ๋ถ€์ •ํ™•ํ•ฉ๋‹ˆ๋‹ค.
647
- * **์ˆ˜์ • ์ œ์•ˆ:**
648
- * `app.py`์—์„œ `app_ready`๋ฅผ `threading.Event` ๊ฐ์ฒด๋กœ ๊ด€๋ฆฌํ•˜๊ณ , ์ด๋ฅผ `register_routes`์— ์ „๋‹ฌํ•ฉ๋‹ˆ๋‹ค. ๊ฐ ๋ผ์šฐํŠธ ํ•ธ๋“ค๋Ÿฌ์—์„œ๋Š” `app_ready_event.is_set()`์œผ๋กœ ์ƒํƒœ๋ฅผ ํ™•์ธํ•ฉ๋‹ˆ๋‹ค. (์œ„ ์ฝ”๋“œ์— ๋ฐ˜์˜๋จ)
649
- * ์•ฑ ์‹œ์ž‘ ์‹œ๊ฐ„์„ ๋ชจ๋“ˆ ๋กœ๋“œ ์‹œ์ ์— `time.time()`์œผ๋กœ ๊ธฐ๋กํ•˜๊ณ , `index` ํ•จ์ˆ˜์—์„œ ์ด๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ๊ฒฝ๊ณผ ์‹œ๊ฐ„์„ ๊ณ„์‚ฐํ•ฉ๋‹ˆ๋‹ค. (์œ„ ์ฝ”๋“œ์— ๋ฐ˜์˜๋จ)
650
- * 30์ดˆ ๊ฐ•์ œ Ready ๋กœ์ง์€ ์ฃผ์„ ์ฒ˜๋ฆฌํ•˜๊ฑฐ๋‚˜ ์ œ๊ฑฐํ•˜๋Š” ๊ฒƒ์„ ๊ถŒ์žฅํ•ฉ๋‹ˆ๋‹ค. ์ดˆ๊ธฐํ™”๊ฐ€ ์˜ค๋ž˜ ๊ฑธ๋ฆฌ๋Š” ๊ทผ๋ณธ ์›์ธ์„ ํ•ด๊ฒฐํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.
651
-
652
- 3. **๊ฐ์ฒด ๋ฐ ์†์„ฑ ์กด์žฌ ์—ฌ๋ถ€ ํ™•์ธ (๋ฐฉ์–ด ์ฝ”๋“œ):**
653
- * `llm_interface`, `retriever`, `stt_client`, `DocumentProcessor`, `base_retriever` ๋“ฑ์˜ ๊ฐ์ฒด๊ฐ€ None์ด๊ฑฐ๋‚˜ ํ•„์š”ํ•œ ๋ฉ”์†Œ๋“œ/์†์„ฑ(`search`, `transcribe_audio`, `add_documents`, `documents` ๋“ฑ)์ด ์—†์„ ๊ฒฝ์šฐ `AttributeError`๋‚˜ `TypeError`๊ฐ€ ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
654
- * **์ˆ˜์ • ์ œ์•ˆ:** ๊ฐ API ํ•ธ๋“ค๋Ÿฌ ์‹œ์ž‘ ๋ถ€๋ถ„์ด๋‚˜ ๊ฐ์ฒด ์‚ฌ์šฉ ์ง์ „์— ํ•ด๋‹น ๊ฐ์ฒด์™€ ํ•„์š”ํ•œ ์†์„ฑ/๋ฉ”์†Œ๋“œ๊ฐ€ ์กด์žฌํ•˜๋Š”์ง€ ํ™•์ธํ•˜๋Š” ๋ฐฉ์–ด ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. (์œ„ ์ฝ”๋“œ์— ์ผ๋ถ€ ๋ฐ˜์˜๋จ)
655
-
656
- 4. **์˜ค๋ฅ˜ ๋กœ๊น…:**
657
- * `except Exception as e:` ๋ธ”๋ก์—์„œ `logger.error(f"...", exc_info=True)`๋ฅผ ์‚ฌ์šฉํ•˜์—ฌ ์Šคํƒ ํŠธ๋ ˆ์ด์Šค ์ „์ฒด๋ฅผ ๋กœ๊น…ํ•˜๋ฉด ๋””๋ฒ„๊น…์— ๋” ์œ ์šฉํ•ฉ๋‹ˆ๋‹ค.
658
- * **์ˆ˜์ • ์ œ์•ˆ:** ์ฃผ์š” `except` ๋ธ”๋ก์— `exc_info=True`๋ฅผ ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค. (์œ„ ์ฝ”๋“œ์— ๋ฐ˜์˜๋จ)
659
-
660
- 5. **`/api/documents` ๋กœ์ง ๊ฐœ์„ :**
661
- * `base_retriever.documents`๊ฐ€ Langchain์˜ `Document` ๊ฐ์ฒด ๋ฆฌ์ŠคํŠธ์ผ ๊ฒฝ์šฐ, `source` ๋“ฑ์˜ ์ •๋ณด๋Š” `doc.metadata['source']` ์™€ ๊ฐ™์ด ์ ‘๊ทผํ•ด์•ผ ํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค. ํ˜„์žฌ ์ฝ”๋“œ๋Š” ๋”•์…”๋„ˆ๋ฆฌ์™€ Langchain `Document` ๊ตฌ์กฐ๋ฅผ ํ˜ผ์šฉํ•˜์—ฌ ์ฒ˜๋ฆฌํ•˜๋ ค๊ณ  ์‹œ๋„ํ•˜๊ณ  ์žˆ์Šต๋‹ˆ๋‹ค. `base_retriever.documents`์˜ ์‹ค์ œ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ๋ฅผ ๋ช…ํ™•ํžˆ ํ•˜๊ณ  ๊ทธ์— ๋งž๊ฒŒ ์ฝ”๋“œ๋ฅผ ์ˆ˜์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
662
- * ๋ฌธ์„œ ๋ชฉ๋ก ์ •๋ ฌ ๊ธฐ์ค€์„ ํŒŒ์ผ๋ช…(`filename`)์œผ๋กœ ๋ณ€๊ฒฝํ•˜๋Š” ๊ฒƒ์ด ๋” ์ง๊ด€์ ์ผ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.
663
- * **์ˆ˜์ • ์ œ์•ˆ:** `base_retriever.documents`์˜ ๊ตฌ์กฐ๋ฅผ ํ™•์ธํ•˜๊ณ  `source`, `filename`, `filetype` ์ถ”์ถœ ๋กœ์ง์„ ๋ช…ํ™•ํžˆ ํ•ฉ๋‹ˆ๋‹ค. ์ •๋ ฌ ๊ธฐ์ค€์„ `filename`์œผ๋กœ ๋ณ€๊ฒฝํ–ˆ์Šต๋‹ˆ๋‹ค. (์œ„ ์ฝ”๋“œ ์ฐธ์กฐ)
664
-
665
- **์š”์•ฝ ๋ฐ ๋‹ค์Œ ๋‹จ๊ณ„:**
666
-
667
- * `/api/documents`์˜ 503 ์˜ค๋ฅ˜๋Š” ํ˜„์žฌ ์ฝ”๋“œ๋งŒ์œผ๋กœ๋Š” ์„ค๋ช…ํ•˜๊ธฐ ์–ด๋ ต์Šต๋‹ˆ๋‹ค. **์‹คํ–‰ ํ™˜๊ฒฝ์˜ ์ฝ”๋“œ ๋ฒ„์ „ ํ™•์ธ** ๋ฐ **์ƒ์„ธ ๋กœ๊น… ์ถ”๊ฐ€**๋ฅผ ํ†ตํ•ด ์›์ธ์„ ์ถ”์ ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
668
- * `app_ready` ์ƒํƒœ ๊ด€๋ฆฌ ๋ฐฉ์‹์„ `threading.Event` ๋“ฑ์œผ๋กœ ๊ฐœ์„ ํ•˜๊ณ , `index` ํ•จ์ˆ˜์˜ ์‹œ๊ฐ„ ๊ณ„์‚ฐ ๋กœ์ง์„ ์ˆ˜์ •ํ•˜๋Š” ๊ฒƒ์ด ์ข‹์Šต๋‹ˆ๋‹ค.
669
- * ์ฝ”๋“œ ์ „๋ฐ˜์— ๊ฑธ์ณ ๊ฐ์ฒด ๋ฐ ์†์„ฑ ์กด์žฌ ์—ฌ๋ถ€๋ฅผ ํ™•์ธํ•˜๋Š” ๋ฐฉ์–ด ์ฝ”๋“œ๋ฅผ ์ถ”๊ฐ€ํ•˜๊ณ , ์˜ค๋ฅ˜ ๋กœ๊น…์„ ๊ฐ•ํ™”ํ•ฉ๋‹ˆ๋‹ค.
670
-
671
- **๊ฐ€์žฅ ๋จผ์ € ๋ธŒ๋ผ์šฐ์ € ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์˜ 'Network' ํƒญ์—์„œ `/api/documents` ์š”์ฒญ์˜ ์‘๋‹ต(Response) ๋ณธ๋ฌธ์— ํ˜น์‹œ ๋” ์ž์„ธํ•œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€๊ฐ€ ์žˆ๋Š”์ง€ ํ™•์ธํ•ด ๋ณด์„ธ์š”
 
1
  """
2
+ RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ - API ๋ผ์šฐํŠธ ์ •์˜ (SyntaxError ์ˆ˜์ •)
3
  """
4
 
5
  import os
 
8
  import tempfile
9
  import requests
10
  import time # ์•ฑ ์‹œ์ž‘ ์‹œ๊ฐ„ ๊ธฐ๋ก ์œ„ํ•ด ์ถ”๊ฐ€
11
+ import threading # threading.Event ์‚ฌ์šฉ ์œ„ํ•ด ์ถ”๊ฐ€
12
  from flask import request, jsonify, render_template, send_from_directory, session, redirect, url_for
13
  from datetime import datetime
14
  from werkzeug.utils import secure_filename
 
33
  ALLOWED_DOC_EXTENSIONS = {'txt', 'md', 'pdf', 'docx', 'csv'}
34
  return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_DOC_EXTENSIONS
35
 
36
+ # --- ๋กœ๊ทธ์ธ/๋กœ๊ทธ์•„์›ƒ ๋ผ์šฐํŠธ (๋ณ€๊ฒฝ ์—†์Œ) ---
37
  @app.route('/login', methods=['GET', 'POST'])
38
  def login():
39
  error = None
 
46
  username = request.form.get('username', '')
47
  password = request.form.get('password', '')
48
  logger.info(f"์ž…๋ ฅ๋œ ์‚ฌ์šฉ์ž๋ช…: {username}")
 
49
 
50
  valid_username = ADMIN_USERNAME
51
  valid_password = ADMIN_PASSWORD
52
  logger.info(f"๊ฒ€์ฆ์šฉ ์‚ฌ์šฉ์ž๋ช…: {valid_username}")
 
53
 
54
  if username == valid_username and password == valid_password:
55
  logger.info(f"๋กœ๊ทธ์ธ ์„ฑ๊ณต: {username}")
 
 
56
  session.permanent = True
57
  session['logged_in'] = True
58
  session['username'] = username
 
 
59
  logger.info(f"์„ธ์…˜ ์„ค์ • ์™„๋ฃŒ: {session}")
 
60
  redirect_to = next_url or url_for('index')
61
  logger.info(f"๋ฆฌ๋””๋ ‰์…˜ ๋Œ€์ƒ: {redirect_to}")
62
  response = redirect(redirect_to)
 
63
  logger.debug(f"๋กœ๊ทธ์ธ ์‘๋‹ต ํ—ค๋” (Set-Cookie ํ™•์ธ): {response.headers.getlist('Set-Cookie')}")
64
  return response
65
  else:
66
  logger.warning("๋กœ๊ทธ์ธ ์‹คํŒจ: ์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜")
 
67
  error = '์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'
68
  else: # GET ์š”์ฒญ
69
  logger.info("๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ GET ์š”์ฒญ")
70
+ if session.get('logged_in'):
71
  logger.info("์ด๋ฏธ ๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž, ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜")
72
  return redirect(url_for('index'))
73
 
 
78
  @app.route('/logout')
79
  def logout():
80
  """๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ"""
81
+ username = session.get('username', 'unknown')
82
+ if session.pop('logged_in', None):
83
  session.pop('username', None)
 
84
  logger.info(f"์‚ฌ์šฉ์ž {username} ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ ์™„๋ฃŒ. ํ˜„์žฌ ์„ธ์…˜: {session}")
85
  else:
86
  logger.warning("๋กœ๊ทธ์ธ๋˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ ๋กœ๊ทธ์•„์›ƒ ์‹œ๋„")
87
 
88
  logger.info("๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜")
89
  response = redirect(url_for('login'))
 
90
  logger.debug(f"๋กœ๊ทธ์•„์›ƒ ์‘๋‹ต ํ—ค๋” (Set-Cookie ํ™•์ธ): {response.headers.getlist('Set-Cookie')}")
91
  return response
92
 
93
+ # --- ๋ฉ”์ธ ํŽ˜์ด์ง€ ๋ฐ ์ƒํƒœ ํ™•์ธ (app_ready_flag ์‚ฌ์šฉ) ---
94
  @app.route('/')
95
  @login_required
96
  def index():
97
  """๋ฉ”์ธ ํŽ˜์ด์ง€"""
98
+ # app_ready_flag๊ฐ€ Event ๊ฐ์ฒด์ธ์ง€ bool์ธ์ง€ ํ™•์ธํ•˜๊ณ  ์ƒํƒœ ๊ฐ€์ ธ์˜ค๊ธฐ
99
+ is_ready = app_ready_flag.is_set() if isinstance(app_ready_flag, threading.Event) else app_ready_flag
 
 
 
 
 
 
 
100
 
 
101
  time_elapsed = time.time() - APP_START_TIME
102
 
 
 
 
 
 
103
  if not is_ready:
104
  logger.info(f"์•ฑ์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•„ ๋กœ๋”ฉ ํŽ˜์ด์ง€ ํ‘œ์‹œ (๊ฒฝ๊ณผ ์‹œ๊ฐ„: {time_elapsed:.1f}์ดˆ)")
105
+ return render_template('loading.html') # 200 OK์™€ ๋กœ๋”ฉ ํŽ˜์ด์ง€
 
 
106
 
107
  logger.info("๋ฉ”์ธ ํŽ˜์ด์ง€ ์š”์ฒญ")
108
  return render_template('index.html')
 
116
  logger.info(f"์•ฑ ์ƒํƒœ ํ™•์ธ ์š”์ฒญ: {'Ready' if is_ready else 'Not Ready'}")
117
  return jsonify({"ready": is_ready})
118
 
119
+ # --- LLM API (๋ณ€๊ฒฝ ์—†์Œ) ---
120
  @app.route('/api/llm', methods=['GET', 'POST'])
121
  @login_required
122
  def llm_api():
123
  """์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ LLM ๋ชฉ๋ก ๋ฐ ์„ ํƒ API"""
124
  is_ready = app_ready_flag.is_set() if isinstance(app_ready_flag, threading.Event) else app_ready_flag
125
+ # ์ดˆ๊ธฐํ™” ์ค‘์—๋„ LLM ๋ชฉ๋ก ์กฐํšŒ๋Š” ๊ฐ€๋Šฅํ•˜๋„๋ก ํ—ˆ์šฉ
126
+ # if not is_ready:
127
+ # return jsonify({"error": "์•ฑ ์ดˆ๊ธฐํ™” ์ค‘..."}), 503
 
128
 
129
  if request.method == 'GET':
130
  logger.info("LLM ๋ชฉ๋ก ์š”์ฒญ")
131
  try:
 
132
  if llm_interface is None or not hasattr(llm_interface, 'get_current_llm_details') or not hasattr(llm_interface, 'SUPPORTED_LLMS'):
133
  logger.error("LLM ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ํ•„์š”ํ•œ ์†์„ฑ์ด ์—†์Šต๋‹ˆ๋‹ค.")
134
  return jsonify({"error": "LLM ์ธํ„ฐํŽ˜์ด์Šค ์˜ค๋ฅ˜"}), 500
 
144
  "current_llm": current_details
145
  })
146
  except Exception as e:
147
+ logger.error(f"LLM ์ •๋ณด ์กฐํšŒ ์˜ค๋ฅ˜: {e}", exc_info=True)
148
  return jsonify({"error": "LLM ์ •๋ณด ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ"}), 500
149
 
150
  elif request.method == 'POST':
 
156
  logger.info(f"LLM ๋ณ€๊ฒฝ ์š”์ฒญ: {llm_id}")
157
 
158
  try:
 
159
  if llm_interface is None or not hasattr(llm_interface, 'set_llm') or not hasattr(llm_interface, 'llm_clients') or not hasattr(llm_interface, 'get_current_llm_details'):
160
  logger.error("LLM ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ํ•„์š”ํ•œ ์†์„ฑ/๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
161
  return jsonify({"error": "LLM ์ธํ„ฐํŽ˜์ด์Šค ์˜ค๋ฅ˜"}), 500
 
179
  logger.error(f"LLM ๋ณ€๊ฒฝ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {e}", exc_info=True)
180
  return jsonify({"error": f"LLM ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"}), 500
181
 
182
+ # --- Chat API (๋ณ€๊ฒฝ ์—†์Œ) ---
183
  @app.route('/api/chat', methods=['POST'])
184
  @login_required
185
  def chat():
186
  """ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ์ฑ„๋ด‡ API"""
 
187
  if retriever is None or not hasattr(retriever, 'search'):
188
  logger.warning("์ฑ„ํŒ… API ์š”์ฒญ ์‹œ retriever๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ search ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
 
189
  return jsonify({
190
  "answer": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰ ์—”์ง„์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.",
191
  "sources": [],
192
+ "error": "Retriever not ready"
193
+ }), 200 # 503 ๋Œ€์‹  200 OK
194
 
195
  try:
196
  data = request.get_json()
 
200
  query = data['query']
201
  logger.info(f"ํ…์ŠคํŠธ ์ฟผ๋ฆฌ ์ˆ˜์‹ : {query[:100]}...")
202
 
203
+ search_results = retriever.search(query, top_k=5, first_stage_k=6)
 
204
 
 
205
  if DocumentProcessor is None or not hasattr(DocumentProcessor, 'prepare_rag_context'):
206
  logger.error("DocumentProcessor๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ prepare_rag_context ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
207
  return jsonify({"error": "๋ฌธ์„œ ์ฒ˜๋ฆฌ๊ธฐ ์˜ค๋ฅ˜"}), 500
208
+ context = DocumentProcessor.prepare_rag_context(search_results, field="text")
209
 
210
  if not context:
211
  logger.warning(f"์ฟผ๋ฆฌ '{query[:50]}...'์— ๋Œ€ํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ.")
 
 
212
 
213
+ llm_id = data.get('llm_id', None)
 
214
  if llm_interface is None or not hasattr(llm_interface, 'rag_generate'):
215
  logger.error("LLM ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ rag_generate ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
216
  return jsonify({"error": "LLM ์ธํ„ฐํŽ˜์ด์Šค ์˜ค๋ฅ˜"}), 500
 
222
  answer = llm_interface.rag_generate(query, context, llm_id=llm_id)
223
  logger.info(f"LLM ์‘๋‹ต ์ƒ์„ฑ ์™„๋ฃŒ (๊ธธ์ด: {len(answer)})")
224
 
 
225
  sources = []
226
  if search_results:
227
  for result in search_results:
228
  if not isinstance(result, dict):
229
  logger.warning(f"์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ˜•์‹: {type(result)}")
230
  continue
 
231
  source_info = {}
232
+ source_key = result.get("source")
233
  if not source_key and "metadata" in result and isinstance(result["metadata"], dict):
234
  source_key = result["metadata"].get("source")
 
235
  if source_key:
236
  source_info["source"] = source_key
237
  source_info["score"] = result.get("rerank_score", result.get("score", 0))
 
 
238
  filetype = result.get("filetype")
239
  if not filetype and "metadata" in result and isinstance(result["metadata"], dict):
240
  filetype = result["metadata"].get("filetype")
 
241
  if "text" in result and filetype == "csv":
242
  try:
243
  text_lines = result["text"].strip().split('\n')
 
246
  if ',' in first_line:
247
  first_column = first_line.split(',')[0].strip()
248
  source_info["id"] = first_column
 
249
  except Exception as e:
250
  logger.warning(f"CSV ์†Œ์Šค ID ์ถ”์ถœ ์‹คํŒจ ({source_info.get('source')}): {e}")
 
251
  sources.append(source_info)
252
 
 
253
  response_data = {
254
  "answer": answer,
255
  "sources": sources,
 
261
  logger.error(f"์ฑ„ํŒ… ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
262
  return jsonify({"error": f"์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"}), 500
263
 
264
+ # --- Voice Chat API (๋ณ€๊ฒฝ ์—†์Œ) ---
265
  @app.route('/api/voice', methods=['POST'])
266
  @login_required
267
  def voice_chat():
268
  """์Œ์„ฑ ์ฑ— API ์—”๋“œํฌ์ธํŠธ"""
 
269
  if retriever is None or not hasattr(retriever, 'search'):
270
  logger.error("์Œ์„ฑ API ์š”์ฒญ ์‹œ retriever๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์Œ")
271
  return jsonify({"error": "๊ฒ€์ƒ‰ ์—”์ง„ ์ค€๋น„ ์•ˆ๋จ"}), 503
 
289
  logger.info(f"์ˆ˜์‹ ๋œ ์˜ค๋””์˜ค ํŒŒ์ผ: {audio_file.filename} ({audio_file.content_type})")
290
 
291
  try:
 
292
  with tempfile.NamedTemporaryFile(delete=True, suffix=os.path.splitext(audio_file.filename)[1]) as temp_audio:
293
  audio_file.save(temp_audio.name)
294
  logger.info(f"์˜ค๋””์˜ค ํŒŒ์ผ์„ ์ž„์‹œ ์ €์žฅ: {temp_audio.name}")
 
 
 
295
  with open(temp_audio.name, 'rb') as f_bytes:
296
  audio_bytes = f_bytes.read()
297
+ stt_result = stt_client.transcribe_audio(audio_bytes, language="ko")
298
 
299
  if not isinstance(stt_result, dict) or not stt_result.get("success"):
300
  error_msg = stt_result.get("error", "์•Œ ์ˆ˜ ์—†๋Š” STT ์˜ค๋ฅ˜") if isinstance(stt_result, dict) else "STT ๊ฒฐ๊ณผ ํ˜•์‹ ์˜ค๋ฅ˜"
 
304
  transcription = stt_result.get("text", "")
305
  if not transcription:
306
  logger.warning("์Œ์„ฑ์ธ์‹ ๊ฒฐ๊ณผ๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค.")
 
307
  return jsonify({
308
  "transcription": "",
309
  "answer": "์Œ์„ฑ์—์„œ ํ…์ŠคํŠธ๋ฅผ ์ธ์‹ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.",
310
  "sources": [],
311
  "llm": llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {}
312
+ }), 200
313
 
314
  logger.info(f"์Œ์„ฑ์ธ์‹ ์„ฑ๊ณต: {transcription[:50]}...")
315
 
 
316
  search_results = retriever.search(transcription, top_k=5, first_stage_k=6)
317
  context = DocumentProcessor.prepare_rag_context(search_results, field="text")
318
 
319
+ llm_id = request.form.get('llm_id', None)
320
  if not context:
321
  answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
322
  logger.info("์ปจํ…์ŠคํŠธ ์—†์ด ๊ธฐ๋ณธ ์‘๋‹ต ์ƒ์„ฑ")
 
324
  answer = llm_interface.rag_generate(transcription, context, llm_id=llm_id)
325
  logger.info(f"LLM ์‘๋‹ต ์ƒ์„ฑ ์™„๋ฃŒ (๊ธธ์ด: {len(answer)})")
326
 
 
327
  sources = []
328
  if search_results:
329
  for result in search_results:
 
350
  logger.warning(f"[์Œ์„ฑ์ฑ—] CSV ์†Œ์Šค ID ์ถ”์ถœ ์‹คํŒจ ({source_info.get('source')}): {e}")
351
  sources.append(source_info)
352
 
 
353
  response_data = {
354
  "transcription": transcription,
355
  "answer": answer,
 
362
  logger.error(f"์Œ์„ฑ ์ฑ— ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
363
  return jsonify({"error": "์Œ์„ฑ ์ฒ˜๋ฆฌ ์ค‘ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ ๋ฐœ์ƒ", "details": str(e)}), 500
364
 
365
+ # --- Document Upload API (๋ณ€๊ฒฝ ์—†์Œ) ---
366
  @app.route('/api/upload', methods=['POST'])
367
  @login_required
368
  def upload_document():
369
  """์ง€์‹๋ฒ ์ด์Šค ๋ฌธ์„œ ์—…๋กœ๋“œ API"""
 
370
  if base_retriever is None or not hasattr(base_retriever, 'add_documents') or not hasattr(base_retriever, 'save'):
371
  logger.error("๋ฌธ์„œ ์—…๋กœ๋“œ API ์š”์ฒญ ์‹œ base_retriever๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ํ•„์ˆ˜ ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
372
  return jsonify({"error": "๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 503
 
375
  return jsonify({"error": "๋ฌธ์„œ ํŒŒ์ผ์ด ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
376
 
377
  doc_file = request.files['document']
378
+ if not doc_file or not doc_file.filename:
379
  return jsonify({"error": "์„ ํƒ๋œ ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค."}), 400
380
 
381
  if not allowed_doc_file(doc_file.filename):
382
+ ALLOWED_DOC_EXTENSIONS = {'txt', 'md', 'pdf', 'docx', 'csv'}
383
  logger.warning(f"ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹: {doc_file.filename}")
384
  return jsonify({"error": f"ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค. ํ—ˆ์šฉ: {', '.join(ALLOWED_DOC_EXTENSIONS)}"}), 400
385
 
386
  try:
387
  filename = secure_filename(doc_file.filename)
388
+ data_folder = app.config.get('DATA_FOLDER', os.path.join(os.path.dirname(__file__), '..', 'data'))
389
+ os.makedirs(data_folder, exist_ok=True)
 
390
  filepath = os.path.join(data_folder, filename)
391
 
392
  doc_file.save(filepath)
393
  logger.info(f"๋ฌธ์„œ ์ €์žฅ ์™„๋ฃŒ: {filepath}")
394
 
 
395
  if DocumentProcessor is None or not hasattr(DocumentProcessor, 'csv_to_documents') or not hasattr(DocumentProcessor, 'text_to_documents'):
396
  logger.error("DocumentProcessor๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ ํ•„์š”ํ•œ ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
 
397
  try: os.remove(filepath)
398
  except OSError: pass
399
  return jsonify({"error": "๋ฌธ์„œ ์ฒ˜๋ฆฌ๊ธฐ ์˜ค๋ฅ˜"}), 500
 
403
  metadata = {"source": filename, "filename": filename, "filetype": file_ext, "filepath": filepath}
404
  docs = []
405
 
 
406
  if file_ext in ['txt', 'md', 'csv']:
407
  try:
408
  with open(filepath, 'r', encoding='utf-8') as f:
 
418
  except Exception as e_read:
419
  logger.error(f"ํŒŒ์ผ ์ฝ๊ธฐ ์˜ค๋ฅ˜ ({filename}): {e_read}")
420
  return jsonify({"error": f"ํŒŒ์ผ ์ฝ๊ธฐ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e_read)}"}), 500
 
421
  elif file_ext == 'pdf':
422
  logger.warning("PDF ์ฒ˜๋ฆฌ๋Š” ๊ตฌํ˜„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
 
423
  elif file_ext == 'docx':
424
  logger.warning("DOCX ์ฒ˜๋ฆฌ๋Š” ๊ตฌํ˜„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
 
425
 
426
+ if content is not None:
 
427
  if file_ext == 'csv':
428
  logger.info(f"CSV ํŒŒ์ผ ์ฒ˜๋ฆฌ ์‹œ์ž‘: {filename}")
429
  docs = DocumentProcessor.csv_to_documents(content, metadata)
430
+ elif file_ext in ['txt', 'md']:
431
  logger.info(f"ํ…์ŠคํŠธ ๋ฌธ์„œ ์ฒ˜๋ฆฌ ์‹œ์ž‘: {filename}")
432
  docs = DocumentProcessor.text_to_documents(
433
  content, metadata=metadata,
434
+ chunk_size=512, chunk_overlap=50
435
  )
 
436
 
 
437
  if docs:
438
  logger.info(f"{len(docs)}๊ฐœ ๋ฌธ์„œ ์ฒญํฌ๋ฅผ ๊ฒ€์ƒ‰๊ธฐ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค...")
439
  base_retriever.add_documents(docs)
440
 
441
  logger.info(f"๊ฒ€์ƒ‰๊ธฐ ์ƒํƒœ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค...")
442
+ index_path = app.config.get('INDEX_PATH', os.path.join(data_folder, 'index'))
443
+ os.makedirs(os.path.dirname(index_path), exist_ok=True)
444
  try:
445
  base_retriever.save(index_path)
446
  logger.info("์ธ๋ฑ์Šค ์ €์žฅ ์™„๋ฃŒ")
 
447
  return jsonify({
448
  "success": True,
449
  "message": f"ํŒŒ์ผ '{filename}' ์—…๋กœ๋“œ ๋ฐ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ({len(docs)}๊ฐœ ์ฒญํฌ ์ถ”๊ฐ€)."
450
  })
451
  except Exception as e_save:
452
  logger.error(f"์ธ๋ฑ์Šค ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e_save}", exc_info=True)
 
453
  return jsonify({"error": f"์ธ๋ฑ์Šค ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜: {str(e_save)}"}), 500
454
  else:
455
  logger.warning(f"ํŒŒ์ผ '{filename}'์—์„œ ์ฒ˜๋ฆฌํ•  ๋‚ด์šฉ์ด ์—†๊ฑฐ๋‚˜ ์ง€์›๋˜์ง€ ์•Š๋Š” ํ˜•์‹์ž…๋‹ˆ๋‹ค.")
 
456
  return jsonify({
457
+ "warning": True,
458
  "message": f"ํŒŒ์ผ '{filename}'์ด ์ €์žฅ๋˜์—ˆ์ง€๋งŒ ์ฒ˜๋ฆฌํ•  ๋‚ด์šฉ์ด ์—†๊ฑฐ๋‚˜ ์ง€์›๋˜์ง€ ์•Š๋Š” ํ˜•์‹์ž…๋‹ˆ๋‹ค."
459
  })
460
 
461
  except Exception as e:
462
  logger.error(f"ํŒŒ์ผ ์—…๋กœ๋“œ ๋˜๋Š” ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
 
463
  if 'filepath' in locals() and os.path.exists(filepath):
464
  try: os.remove(filepath)
465
  except OSError: pass
466
  return jsonify({"error": f"ํŒŒ์ผ ์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜: {str(e)}"}), 500
467
 
468
+ # --- Document List API (SyntaxError ์œ ๋ฐœ ์ฃผ์„ ์ œ๊ฑฐ) ---
469
  @app.route('/api/documents', methods=['GET'])
470
  @login_required
471
  def list_documents():
472
  """์ง€์‹๋ฒ ์ด์Šค ๋ฌธ์„œ ๋ชฉ๋ก API"""
473
+ # SyntaxError๋ฅผ ์œ ๋ฐœํ–ˆ๋˜ ์„ค๋ช… ์ฃผ์„๋“ค์„ ์ œ๊ฑฐํ–ˆ์Šต๋‹ˆ๋‹ค.
474
+ logger.info("๋ฌธ์„œ ๋ชฉ๋ก API ์š”์ฒญ ์‹œ์ž‘")
 
 
 
 
475
 
 
476
  if base_retriever is None:
477
  logger.warning("๋ฌธ์„œ API ์š”์ฒญ ์‹œ base_retriever๊ฐ€ None์ž…๋‹ˆ๋‹ค.")
 
478
  return jsonify({"documents": [], "total_documents": 0, "total_chunks": 0})
479
  elif not hasattr(base_retriever, 'documents'):
480
  logger.warning("๋ฌธ์„œ API ์š”์ฒญ ์‹œ base_retriever์— 'documents' ์†์„ฑ์ด ์—†์Šต๋‹ˆ๋‹ค.")
 
486
  logger.info(f"base_retriever.documents ํƒ€์ž…: {type(base_retriever.documents)}")
487
  logger.info(f"base_retriever.documents ๊ธธ์ด: {len(base_retriever.documents) if isinstance(base_retriever.documents, list) else 'N/A'}")
488
 
 
489
  try:
490
  sources = {}
491
  total_chunks = 0
492
+ doc_list = base_retriever.documents
493
 
494
  if not isinstance(doc_list, list):
495
  logger.error(f"base_retriever.documents๊ฐ€ ๋ฆฌ์ŠคํŠธ๊ฐ€ ์•„๋‹˜: {type(doc_list)}")
 
496
  return jsonify({"error": "๋‚ด๋ถ€ ๋ฐ์ดํ„ฐ ๊ตฌ์กฐ ์˜ค๋ฅ˜"}), 500
497
 
498
  logger.info(f"์ด {len(doc_list)}๊ฐœ ๋ฌธ์„œ ์ฒญํฌ์—์„œ ์†Œ์Šค ๋ชฉ๋ก ์ƒ์„ฑ ์ค‘...")
499
  for i, doc in enumerate(doc_list):
 
 
 
500
  if not isinstance(doc, dict):
501
  logger.warning(f"์ฒญํฌ {i}๊ฐ€ ๋”•์…”๋„ˆ๋ฆฌ ํƒ€์ž…์ด ์•„๋‹˜: {type(doc)}")
502
+ continue
503
 
 
504
  source = "unknown"
505
+ metadata = doc.get("metadata")
506
  if isinstance(metadata, dict):
507
  source = metadata.get("source", "unknown")
508
+ if source == "unknown":
509
  source = doc.get("source", "unknown")
510
 
511
  if source != "unknown":
512
  if source in sources:
513
  sources[source]["chunks"] += 1
514
  else:
 
515
  filename = metadata.get("filename", source) if isinstance(metadata, dict) else source
516
  filetype = metadata.get("filetype", "unknown") if isinstance(metadata, dict) else "unknown"
 
517
  if filename == source and "filename" in doc: filename = doc["filename"]
518
  if filetype == "unknown" and "filetype" in doc: filetype = doc["filetype"]
519
 
 
526
  else:
527
  logger.warning(f"์ฒญํฌ {i}์—์„œ ์†Œ์Šค ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ: {doc}")
528
 
 
 
529
  documents = [{"source": src, **info} for src, info in sources.items()]
530
+ documents.sort(key=lambda x: x.get("filename", ""), reverse=False)
531
 
532
  logger.info(f"๋ฌธ์„œ ๋ชฉ๋ก ์กฐํšŒ ์™„๋ฃŒ: {len(documents)}๊ฐœ ์†Œ์Šค ํŒŒ์ผ, {total_chunks}๊ฐœ ์ฒญํฌ")
533
  return jsonify({
 
537
  })
538
 
539
  except Exception as e:
540
+ # ์—ฌ๊ธฐ์„œ ๋ฐœ์ƒํ•˜๋Š” ์˜ˆ์™ธ๊ฐ€ 503์œผ๋กœ ์ด์–ด์งˆ ์ˆ˜ ์žˆ๋Š”์ง€ ํ™•์ธ
541
  logger.error(f"๋ฌธ์„œ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์‹ฌ๊ฐํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
 
542
  return jsonify({"error": f"๋ฌธ์„œ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜: {str(e)}"}), 500
 
543
  ```
544
 
545
+ ์ด ์ฝ”๋“œ๋กœ ์—…๋ฐ์ดํŠธํ•˜๋ฉด `SyntaxError` ์—†์ด ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์ด ์ •์ƒ์ ์œผ๋กœ ์‹œ์ž‘๋  ๊ฒƒ์ž…๋‹ˆ๋‹ค. ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์„ ๋‹ค์‹œ ์‹œ์ž‘ํ•˜๊ณ  ๋ฌธ์ œ๊ฐ€ ํ•ด๊ฒฐ๋˜์—ˆ๋Š”์ง€ ํ™•์ธํ•ด ๋ณด์„ธ์š”.
546
+
547
+ ๋งŒ์•ฝ ์ด ์ˆ˜์ • ํ›„์—๋„ '๋ฌธ์„œ๊ด€๋ฆฌ' ๋˜๋Š” '์žฅ์น˜๊ด€๋ฆฌ' ํƒญ ๋กœ๋”ฉ ๋ฌธ์ œ๊ฐ€ ๊ณ„์†๋œ๋‹ค๋ฉด, ์ด์ „์— ๋ง์”€๋“œ๋ฆฐ ๋Œ€๋กœ **๋ธŒ๋ผ์šฐ์ € ๊ฐœ๋ฐœ์ž ๋„๊ตฌ์˜ ์ฝ˜์†” ๋ฐ ๋„คํŠธ์›Œํฌ ํƒญ ์ •๋ณด**๋ฅผ ํ™•์ธํ•˜์—ฌ ์ •ํ™•ํ•œ ์›์ธ์„ ํŒŒ์•…ํ•ด์•ผ ํ•ฉ