jeongsoo commited on
Commit
b0188f6
ยท
1 Parent(s): f61a84c
Files changed (2) hide show
  1. app/app.py +12 -3
  2. app/app_routes.py +275 -180
app/app.py CHANGED
@@ -46,12 +46,20 @@ if not ADMIN_PASSWORD:
46
  # --- ๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ---
47
  # MockComponent ์ •์˜ (์ž„ํฌํŠธ ์‹คํŒจ ์‹œ ๋Œ€์ฒด)
48
  class MockComponent:
 
 
 
 
 
 
 
 
49
  def __getattr__(self, name):
50
  # Mock ๊ฐ์ฒด์˜ ์–ด๋–ค ์†์„ฑ์ด๋‚˜ ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ ์‹œ ๊ฒฝ๊ณ  ๋กœ๊ทธ ์ถœ๋ ฅ ๋ฐ ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜
51
  logger.warning(f"MockComponent์—์„œ '{name}' ์ ‘๊ทผ ์‹œ๋„๋จ (์‹ค์ œ ๋ชจ๋“ˆ ๋กœ๋“œ ์‹คํŒจ)")
52
  # ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ ์‹œ์—๋Š” ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ํ•˜๋Š” ํ•จ์ˆ˜ ๋ฐ˜ํ™˜
53
- if name in ['search', 'add_documents', 'save', 'transcribe_audio', 'rag_generate', 'set_llm', 'get_current_llm_details', 'prepare_rag_context', 'csv_to_documents', 'text_to_documents', 'load_documents_from_directory']:
54
- return lambda *args, **kwargs: logger.warning(f"Mocked method '{name}' called") or (None if name != 'search' else []) # search๋Š” ๋นˆ ๋ฆฌ์ŠคํŠธ ๋ฐ˜ํ™˜
55
  # ์†์„ฑ ์ ‘๊ทผ ์‹œ์—๋Š” None ๋ฐ˜ํ™˜
56
  return None
57
 
@@ -188,7 +196,8 @@ def init_retriever():
188
  logger.error(f"๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”/๋กœ๋“œ ์‹คํŒจ: {e}", exc_info=True)
189
  base_retriever = MockComponent() # ์‹คํŒจ ์‹œ Mock ์‚ฌ์šฉ
190
  retriever = MockComponent()
191
- return None # ์ดˆ๊ธฐํ™” ์‹คํŒจ
 
192
 
193
  # 2. ๋ฐ์ดํ„ฐ ํด๋” ๋ฌธ์„œ ๋กœ๋“œ (๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ๋น„์–ด์žˆ์„ ๋•Œ)
194
  needs_loading = not hasattr(base_retriever, 'documents') or not getattr(base_retriever, 'documents', [])
 
46
  # --- ๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ---
47
  # MockComponent ์ •์˜ (์ž„ํฌํŠธ ์‹คํŒจ ์‹œ ๋Œ€์ฒด)
48
  class MockComponent:
49
+ def __init__(self):
50
+ self.is_mock = True
51
+
52
+ def search(self, query, top_k=5, first_stage_k=None):
53
+ """๋นˆ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๋ฅผ ๋ฐ˜ํ™˜ํ•ฉ๋‹ˆ๋‹ค."""
54
+ logger.warning(f"MockComponent.search ํ˜ธ์ถœ๋จ (์ฟผ๋ฆฌ: {query[:30]}...)")
55
+ return []
56
+
57
  def __getattr__(self, name):
58
  # Mock ๊ฐ์ฒด์˜ ์–ด๋–ค ์†์„ฑ์ด๋‚˜ ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ ์‹œ ๊ฒฝ๊ณ  ๋กœ๊ทธ ์ถœ๋ ฅ ๋ฐ ๊ธฐ๋ณธ๊ฐ’ ๋ฐ˜ํ™˜
59
  logger.warning(f"MockComponent์—์„œ '{name}' ์ ‘๊ทผ ์‹œ๋„๋จ (์‹ค์ œ ๋ชจ๋“ˆ ๋กœ๋“œ ์‹คํŒจ)")
60
  # ๋ฉ”์†Œ๋“œ ํ˜ธ์ถœ ์‹œ์—๋Š” ์•„๋ฌด๊ฒƒ๋„ ์•ˆ ํ•˜๋Š” ํ•จ์ˆ˜ ๋ฐ˜ํ™˜
61
+ if name in ['add_documents', 'save', 'transcribe_audio', 'rag_generate', 'set_llm', 'get_current_llm_details', 'prepare_rag_context', 'csv_to_documents', 'text_to_documents', 'load_documents_from_directory']:
62
+ return lambda *args, **kwargs: logger.warning(f"Mocked method '{name}' called") or None
63
  # ์†์„ฑ ์ ‘๊ทผ ์‹œ์—๋Š” None ๋ฐ˜ํ™˜
64
  return None
65
 
 
196
  logger.error(f"๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”/๋กœ๋“œ ์‹คํŒจ: {e}", exc_info=True)
197
  base_retriever = MockComponent() # ์‹คํŒจ ์‹œ Mock ์‚ฌ์šฉ
198
  retriever = MockComponent()
199
+ logger.info("Mock ๊ฒ€์ƒ‰๊ธฐ๋ฅผ ๋Œ€์ฒด๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.")
200
+ return retriever # ์ดˆ๊ธฐํ™” ์‹คํŒจํ•ด๋„ Mock ๊ฒ€์ƒ‰๊ธฐ ๋ฐ˜ํ™˜ (None ๋Œ€์‹ )
201
 
202
  # 2. ๋ฐ์ดํ„ฐ ํด๋” ๋ฌธ์„œ ๋กœ๋“œ (๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ๋น„์–ด์žˆ์„ ๋•Œ)
203
  needs_loading = not hasattr(base_retriever, 'documents') or not getattr(base_retriever, 'documents', [])
app/app_routes.py CHANGED
@@ -191,205 +191,300 @@ def register_routes(app, login_required, llm_interface, retriever, stt_client, D
191
  @login_required
192
  def chat():
193
  """ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ์ฑ„๋ด‡ API"""
194
- is_ready = app_ready_event.is_set() if isinstance(app_ready_event, threading.Event) else False
195
- if not is_ready:
196
- return jsonify({"error": "์•ฑ ์ดˆ๊ธฐํ™” ์ค‘...", "answer": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์‹œ์Šคํ…œ์ด ์•„์ง ์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค.", "sources": []}), 503
197
-
198
- # retriever ๊ฐ์ฒด ๋ฐ ํ•„์ˆ˜ ๋ฉ”์†Œ๋“œ ํ™•์ธ
199
- if retriever is None or not hasattr(retriever, 'search'):
200
- logger.warning("์ฑ„ํŒ… API ์š”์ฒญ ์‹œ retriever๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ search ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
201
- return jsonify({
202
- "answer": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰ ์—”์ง„์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.",
203
- "sources": [],
204
- "error": "Retriever not ready"
205
- }), 503 # ์„œ๋น„์Šค ๋ถˆ๊ฐ€ ์ƒํƒœ
206
-
207
  try:
 
 
 
 
 
 
 
 
 
 
208
  data = request.get_json()
209
  if not data or 'query' not in data:
210
  return jsonify({"error": "์ฟผ๋ฆฌ๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
211
 
212
  query = data['query']
213
  logger.info(f"ํ…์ŠคํŠธ ์ฟผ๋ฆฌ ์ˆ˜์‹ : {query[:100]}...")
214
-
215
- # RAG ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰
216
- search_results = retriever.search(query, top_k=5, first_stage_k=6)
217
-
218
- # DocumentProcessor ๊ฐ์ฒด ๋ฐ ๋ฉ”์†Œ๋“œ ํ™•์ธ
219
- if DocumentProcessor is None or not hasattr(DocumentProcessor, 'prepare_rag_context'):
220
- logger.error("DocumentProcessor๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ prepare_rag_context ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
221
- return jsonify({"error": "๋ฌธ์„œ ์ฒ˜๋ฆฌ๊ธฐ ์˜ค๋ฅ˜"}), 500
222
- context = DocumentProcessor.prepare_rag_context(search_results, field="text")
223
-
224
- if not context:
225
- logger.warning(f"์ฟผ๋ฆฌ '{query[:50]}...'์— ๋Œ€ํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ.")
226
-
227
- # LLM ์ธํ„ฐํŽ˜์ด์Šค ๊ฐ์ฒด ๋ฐ ๋ฉ”์†Œ๋“œ ํ™•์ธ
228
- llm_id = data.get('llm_id', None)
229
- if llm_interface is None or not hasattr(llm_interface, 'rag_generate'):
230
- logger.error("LLM ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ rag_generate ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
231
- return jsonify({"error": "LLM ์ธํ„ฐํŽ˜์ด์Šค ์˜ค๋ฅ˜"}), 500
232
-
233
- # LLM ํ˜ธ์ถœ
234
- if not context:
235
- answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
236
- logger.info("์ปจํ…์ŠคํŠธ ์—†์ด ๊ธฐ๋ณธ ์‘๋‹ต ์ƒ์„ฑ")
237
- else:
238
- answer = llm_interface.rag_generate(query, context, llm_id=llm_id)
239
- logger.info(f"LLM ์‘๋‹ต ์ƒ์„ฑ ์™„๋ฃŒ (๊ธธ์ด: {len(answer)})")
240
-
241
- # ์†Œ์Šค ์ •๋ณด ์ถ”์ถœ
242
- sources = []
243
- if search_results:
244
- for result in search_results:
245
- if not isinstance(result, dict):
246
- logger.warning(f"์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ˜•์‹: {type(result)}")
247
- continue
248
- source_info = {}
249
- source_key = result.get("source")
250
- if not source_key and "metadata" in result and isinstance(result["metadata"], dict):
251
- source_key = result["metadata"].get("source")
252
- if source_key:
253
- source_info["source"] = source_key
254
- source_info["score"] = result.get("rerank_score", result.get("score", 0))
255
- filetype = result.get("filetype")
256
- if not filetype and "metadata" in result and isinstance(result["metadata"], dict):
257
- filetype = result["metadata"].get("filetype")
258
- if "text" in result and filetype == "csv":
259
- try:
260
- text_lines = result["text"].strip().split('\n')
261
- if text_lines:
262
- first_line = text_lines[0].strip()
263
- if ',' in first_line:
264
- first_column = first_line.split(',')[0].strip()
265
- source_info["id"] = first_column
266
- except Exception as e:
267
- logger.warning(f"CSV ์†Œ์Šค ID ์ถ”์ถœ ์‹คํŒจ ({source_info.get('source')}): {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
  sources.append(source_info)
269
 
270
- # ์ตœ์ข… ์‘๋‹ต
271
- response_data = {
272
- "answer": answer,
273
- "sources": sources,
274
- "llm": llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {}
275
- }
276
- return jsonify(response_data)
277
-
 
 
 
 
 
 
278
  except Exception as e:
279
- logger.error(f"์ฑ„ํŒ… ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
280
- return jsonify({"error": f"์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"}), 500
 
 
 
 
281
 
282
  # --- Voice Chat API ---
283
  @app.route('/api/voice', methods=['POST'])
284
  @login_required
285
  def voice_chat():
286
  """์Œ์„ฑ ์ฑ— API ์—”๋“œํฌ์ธํŠธ"""
287
- is_ready = app_ready_event.is_set() if isinstance(app_ready_event, threading.Event) else False
288
- if not is_ready:
289
- return jsonify({"error": "์•ฑ ์ดˆ๊ธฐํ™” ์ค‘..."}), 503
290
-
291
- # ํ•„์ˆ˜ ์ปดํฌ๋„ŒํŠธ ํ™•์ธ
292
- if retriever is None or not hasattr(retriever, 'search'):
293
- logger.error("์Œ์„ฑ API ์š”์ฒญ ์‹œ retriever๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์Œ")
294
- return jsonify({"error": "๊ฒ€์ƒ‰ ์—”์ง„ ์ค€๋น„ ์•ˆ๋จ"}), 503
295
- if stt_client is None or not hasattr(stt_client, 'transcribe_audio'):
296
- logger.error("์Œ์„ฑ API ์š”์ฒญ ์‹œ STT ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์Œ")
297
- return jsonify({"error": "์Œ์„ฑ ์ธ์‹ ์„œ๋น„์Šค ์ค€๋น„ ์•ˆ๋จ"}), 503
298
- if llm_interface is None or not hasattr(llm_interface, 'rag_generate'):
299
- logger.error("์Œ์„ฑ API ์š”์ฒญ ์‹œ LLM ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์Œ")
300
- return jsonify({"error": "LLM ์ธํ„ฐํŽ˜์ด์Šค ์˜ค๋ฅ˜"}), 500
301
- if DocumentProcessor is None or not hasattr(DocumentProcessor, 'prepare_rag_context'):
302
- logger.error("์Œ์„ฑ API ์š”์ฒญ ์‹œ DocumentProcessor๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์Œ")
303
- return jsonify({"error": "๋ฌธ์„œ ์ฒ˜๋ฆฌ๊ธฐ ์˜ค๋ฅ˜"}), 500
304
-
305
- logger.info("์Œ์„ฑ ์ฑ— ์š”์ฒญ ์ˆ˜์‹ ")
306
-
307
- if 'audio' not in request.files:
308
- logger.error("์˜ค๋””์˜ค ํŒŒ์ผ์ด ์ œ๊ณต๋˜์ง€ ์•Š์Œ")
309
- return jsonify({"error": "์˜ค๋””์˜ค ํŒŒ์ผ์ด ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
310
-
311
- audio_file = request.files['audio']
312
- logger.info(f"์ˆ˜์‹ ๋œ ์˜ค๋””์˜ค ํŒŒ์ผ: {audio_file.filename} ({audio_file.content_type})")
313
-
314
  try:
315
- # ์˜ค๋””์˜ค ํŒŒ์ผ ์ž„์‹œ ์ €์žฅ ๋ฐ ์ฒ˜๋ฆฌ
316
- with tempfile.NamedTemporaryFile(delete=True, suffix=os.path.splitext(audio_file.filename)[1]) as temp_audio:
317
- audio_file.save(temp_audio.name)
318
- logger.info(f"์˜ค๋””์˜ค ํŒŒ์ผ์„ ์ž„์‹œ ์ €์žฅ: {temp_audio.name}")
319
- # STT ์ˆ˜ํ–‰ (๋ฐ”์ดํŠธ ์ „๋‹ฌ ๊ฐ€์ •)
320
- with open(temp_audio.name, 'rb') as f_bytes:
321
- audio_bytes = f_bytes.read()
322
- stt_result = stt_client.transcribe_audio(audio_bytes, language="ko")
323
-
324
- # STT ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ
325
- if not isinstance(stt_result, dict) or not stt_result.get("success"):
326
- error_msg = stt_result.get("error", "์•Œ ์ˆ˜ ์—†๋Š” STT ์˜ค๋ฅ˜") if isinstance(stt_result, dict) else "STT ๊ฒฐ๊ณผ ํ˜•์‹ ์˜ค๋ฅ˜"
327
- logger.error(f"์Œ์„ฑ์ธ์‹ ์‹คํŒจ: {error_msg}")
328
- return jsonify({"error": "์Œ์„ฑ์ธ์‹ ์‹คํŒจ", "details": error_msg}), 500
329
-
330
- transcription = stt_result.get("text", "")
331
- if not transcription:
332
- logger.warning("์Œ์„ฑ์ธ์‹ ๊ฒฐ๊ณผ๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
333
  return jsonify({
334
- "transcription": "",
335
- "answer": "์Œ์„ฑ์—์„œ ํ…์ŠคํŠธ๋ฅผ ์ธ์‹ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.",
336
- "sources": [],
337
- "llm": llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {}
338
- }), 200 # 200 OK์™€ ๋ฉ”์‹œ์ง€
339
-
340
- logger.info(f"์Œ์„ฑ์ธ์‹ ์„ฑ๊ณต: {transcription[:50]}...")
341
-
342
- # --- RAG ๋ฐ LLM ํ˜ธ์ถœ (Chat API์™€ ๋™์ผ ๋กœ์ง) ---
343
- search_results = retriever.search(transcription, top_k=5, first_stage_k=6)
344
- context = DocumentProcessor.prepare_rag_context(search_results, field="text")
345
-
346
- llm_id = request.form.get('llm_id', None) # form ๋ฐ์ดํ„ฐ์—์„œ llm_id ๊ฐ€์ ธ์˜ค๊ธฐ
347
- if not context:
348
- answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
349
- logger.info("์ปจํ…์ŠคํŠธ ์—†์ด ๊ธฐ๋ณธ ์‘๋‹ต ์ƒ์„ฑ")
350
- else:
351
- answer = llm_interface.rag_generate(transcription, context, llm_id=llm_id)
352
- logger.info(f"LLM ์‘๋‹ต ์ƒ์„ฑ ์™„๋ฃŒ (๊ธธ์ด: {len(answer)})")
353
-
354
- # ์†Œ์Šค ์ •๋ณด ์ถ”์ถœ (Chat API์™€ ๋™์ผ ๋กœ์ง)
355
- sources = []
356
- if search_results:
357
- for result in search_results:
358
- if not isinstance(result, dict): continue
359
- source_info = {}
360
- source_key = result.get("source")
361
- if not source_key and "metadata" in result and isinstance(result["metadata"], dict):
362
- source_key = result["metadata"].get("source")
363
- if source_key:
364
- source_info["source"] = source_key
365
- source_info["score"] = result.get("rerank_score", result.get("score", 0))
366
- filetype = result.get("filetype")
367
- if not filetype and "metadata" in result and isinstance(result["metadata"], dict):
368
- filetype = result["metadata"].get("filetype")
369
- if "text" in result and filetype == "csv":
370
- try:
371
- text_lines = result["text"].strip().split('\n')
372
- if text_lines:
373
- first_line = text_lines[0].strip()
374
- if ',' in first_line:
375
- first_column = first_line.split(',')[0].strip()
376
- source_info["id"] = first_column
377
- except Exception as e:
378
- logger.warning(f"[์Œ์„ฑ์ฑ—] CSV ์†Œ์Šค ID ์ถ”์ถœ ์‹คํŒจ ({source_info.get('source')}): {e}")
379
- sources.append(source_info)
380
-
381
- # ์ตœ์ข… ์‘๋‹ต
382
- response_data = {
383
- "transcription": transcription,
384
- "answer": answer,
385
- "sources": sources,
386
- "llm": llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {}
387
- }
388
- return jsonify(response_data)
389
-
390
  except Exception as e:
391
- logger.error(f"์Œ์„ฑ ์ฑ— ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
392
- return jsonify({"error": "์Œ์„ฑ ์ฒ˜๋ฆฌ ์ค‘ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ ๋ฐœ์ƒ", "details": str(e)}), 500
 
 
 
393
 
394
  # --- Document Upload API ---
395
  @app.route('/api/upload', methods=['POST'])
 
191
  @login_required
192
  def chat():
193
  """ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ์ฑ„๋ด‡ API"""
 
 
 
 
 
 
 
 
 
 
 
 
 
194
  try:
195
+ # ์•ฑ์ด ์ค€๋น„๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
196
+ is_ready = app_ready_event.is_set() if isinstance(app_ready_event, threading.Event) else False
197
+ if not is_ready:
198
+ logger.warning("์•ฑ์ด ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค.")
199
+ return jsonify({
200
+ "error": "์•ฑ ์ดˆ๊ธฐํ™” ์ค‘...",
201
+ "answer": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์‹œ์Šคํ…œ์ด ์•„์ง ์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค.",
202
+ "sources": []
203
+ }), 503
204
+
205
  data = request.get_json()
206
  if not data or 'query' not in data:
207
  return jsonify({"error": "์ฟผ๋ฆฌ๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
208
 
209
  query = data['query']
210
  logger.info(f"ํ…์ŠคํŠธ ์ฟผ๋ฆฌ ์ˆ˜์‹ : {query[:100]}...")
211
+
212
+ # ๊ฒ€์ƒ‰ ์—”์ง„ ์ฒ˜๋ฆฌ ๋ถ€๋ถ„ ์ˆ˜์ •
213
+ search_results = []
214
+ search_warning = None
215
+ try:
216
+ # retriever ์ƒํƒœ ๊ฒ€์ฆ
217
+ if retriever is None:
218
+ logger.warning("Retriever๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
219
+ search_warning = "๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."
220
+ elif hasattr(retriever, 'is_mock') and retriever.is_mock:
221
+ logger.info("Mock Retriever ์‚ฌ์šฉ ์ค‘ - ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ.")
222
+ search_warning = "๊ฒ€์ƒ‰ ์ธ๋ฑ์Šค๊ฐ€ ์•„์ง ๊ตฌ์ถ• ์ค‘์ž…๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ์‘๋‹ต๋งŒ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค."
223
+ elif not hasattr(retriever, 'search'):
224
+ logger.warning("Retriever์— search ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
225
+ search_warning = "๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์ด ํ˜„์žฌ ์ œํ•œ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค."
226
+ else:
227
+ logger.info(f"๊ฒ€์ƒ‰ ์ˆ˜ํ–‰: {query[:50]}...")
228
+ search_results = retriever.search(query, top_k=5, first_stage_k=6)
229
+ if not search_results:
230
+ logger.info("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
231
+ else:
232
+ logger.info(f"๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ: {len(search_results)}๊ฐœ ํ•ญ๋ชฉ")
233
+ except Exception as e:
234
+ logger.error(f"๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}", exc_info=True)
235
+ search_results = []
236
+ search_warning = f"๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"
237
+
238
+ # LLM ์‘๋‹ต ์ƒ์„ฑ
239
+ try:
240
+ # DocumentProcessor ๊ฐ์ฒด ๋ฐ ๋ฉ”์†Œ๋“œ ํ™•์ธ
241
+ context = ""
242
+ if search_results:
243
+ if DocumentProcessor is None or not hasattr(DocumentProcessor, 'prepare_rag_context'):
244
+ logger.warning("DocumentProcessor๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ prepare_rag_context ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
245
+ else:
246
+ context = DocumentProcessor.prepare_rag_context(search_results, field="text")
247
+ logger.info(f"์ปจํ…์ŠคํŠธ ์ค€๋น„ ์™„๋ฃŒ (๊ธธ์ด: {len(context) if context else 0}์ž)")
248
+
249
+ # LLM ์ธํ„ฐํŽ˜์ด์Šค ๊ฐ์ฒด ๋ฐ ๋ฉ”์†Œ๋“œ ํ™•์ธ
250
+ llm_id = data.get('llm_id', None)
251
+
252
+ if not context:
253
+ if search_warning:
254
+ logger.info(f"์ปจํ…์ŠคํŠธ ์—†์Œ, ๊ฒ€์ƒ‰ ๊ฒฝ๊ณ : {search_warning}")
255
+ answer = f"์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์งˆ๋ฌธ์— ๋Œ€ํ•œ ๋‹ต๋ณ€์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ({search_warning})"
256
+ else:
257
+ logger.info("์ปจํ…์ŠคํŠธ ์—†์ด ๊ธฐ๋ณธ ์‘๋‹ต ์ƒ์„ฑ")
258
+ answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
259
+ else:
260
+ if llm_interface is None or not hasattr(llm_interface, 'rag_generate'):
261
+ logger.error("LLM ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ rag_generate ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
262
+ answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ LLM ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
263
+ else:
264
+ # LLM ํ˜ธ์ถœ ์ „์— ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
265
+ if search_warning:
266
+ modified_query = f"{query}\n\n์ฐธ๊ณ : {search_warning}"
267
+ logger.info(f"๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€์™€ ํ•จ๊ป˜ ์ฟผ๋ฆฌ ์ƒ์„ฑ: {modified_query[:100]}...")
268
+ else:
269
+ modified_query = query
270
+
271
+ answer = llm_interface.rag_generate(modified_query, context, llm_id=llm_id)
272
+ logger.info(f"LLM ์‘๋‹ต ์ƒ์„ฑ ์™„๋ฃŒ (๊ธธ์ด: {len(answer)})")
273
+
274
+ # ์†Œ์Šค ์ •๋ณด ์ถ”์ถœ
275
+ sources = []
276
+ if search_results:
277
+ for result in search_results:
278
+ if not isinstance(result, dict):
279
+ logger.warning(f"์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ˜•์‹: {type(result)}")
280
+ continue
281
+ source_info = {}
282
+ source_key = result.get("source")
283
+ if not source_key and "metadata" in result and isinstance(result["metadata"], dict):
284
+ source_key = result["metadata"].get("source")
285
+
286
+ if source_key:
287
+ source_info["name"] = os.path.basename(source_key)
288
+ source_info["path"] = source_key
289
+ else:
290
+ source_info["name"] = "์•Œ ์ˆ˜ ์—†๋Š” ์†Œ์Šค"
291
+
292
+ if "score" in result:
293
+ source_info["score"] = result["score"]
294
+ if "rerank_score" in result:
295
+ source_info["rerank_score"] = result["rerank_score"]
296
+
297
  sources.append(source_info)
298
 
299
+ return jsonify({
300
+ "answer": answer,
301
+ "sources": sources,
302
+ "search_warning": search_warning
303
+ })
304
+
305
+ except Exception as e:
306
+ logger.error(f"LLM ์‘๋‹ต ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}", exc_info=True)
307
+ return jsonify({
308
+ "answer": f"์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์‘๋‹ต ์ƒ์„ฑ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}",
309
+ "sources": [],
310
+ "error": str(e)
311
+ })
312
+
313
  except Exception as e:
314
+ logger.error(f"์ฑ„ํŒ… API์—์„œ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}", exc_info=True)
315
+ return jsonify({
316
+ "error": f"์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}",
317
+ "answer": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.",
318
+ "sources": []
319
+ }), 500
320
 
321
  # --- Voice Chat API ---
322
  @app.route('/api/voice', methods=['POST'])
323
  @login_required
324
  def voice_chat():
325
  """์Œ์„ฑ ์ฑ— API ์—”๋“œํฌ์ธํŠธ"""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  try:
327
+ # ์•ฑ์ด ์ค€๋น„๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
328
+ is_ready = app_ready_event.is_set() if isinstance(app_ready_event, threading.Event) else False
329
+ if not is_ready:
330
+ logger.warning("์•ฑ์ด ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค.")
331
+ return jsonify({"error": "์•ฑ ์ดˆ๊ธฐํ™” ์ค‘...", "answer": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์‹œ์Šคํ…œ์ด ์•„์ง ์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค."}), 503
332
+
333
+ # STT ํด๋ผ์ด์–ธํŠธ ํ™•์ธ
334
+ if stt_client is None or not hasattr(stt_client, 'transcribe_audio'):
335
+ logger.error("์Œ์„ฑ API ์š”์ฒญ ์‹œ STT ํด๋ผ์ด์–ธํŠธ๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์Œ")
336
+ return jsonify({"error": "์Œ์„ฑ ์ธ์‹ ์„œ๋น„์Šค ์ค€๋น„ ์•ˆ๋จ"}), 503
337
+
338
+ logger.info("์Œ์„ฑ ์ฑ— ์š”์ฒญ ์ˆ˜์‹ ")
339
+
340
+ if 'audio' not in request.files:
341
+ logger.error("์˜ค๋””์˜ค ํŒŒ์ผ์ด ์ œ๊ณต๋˜์ง€ ์•Š์Œ")
342
+ return jsonify({"error": "์˜ค๋””์˜ค ํŒŒ์ผ์ด ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
343
+
344
+ audio_file = request.files['audio']
345
+ logger.info(f"์ˆ˜์‹ ๋œ ์˜ค๋””์˜ค ํŒŒ์ผ: {audio_file.filename} ({audio_file.content_type})")
346
+
347
+ try:
348
+ # ์˜ค๋””์˜ค ํŒŒ์ผ ์ž„์‹œ ์ €์žฅ ๋ฐ ์ฒ˜๋ฆฌ
349
+ with tempfile.NamedTemporaryFile(delete=True, suffix=os.path.splitext(audio_file.filename)[1]) as temp_audio:
350
+ audio_file.save(temp_audio.name)
351
+ logger.info(f"์˜ค๋””์˜ค ํŒŒ์ผ์„ ์ž„์‹œ ์ €์žฅ: {temp_audio.name}")
352
+ # STT ์ˆ˜ํ–‰ (๋ฐ”์ดํŠธ ์ „๋‹ฌ ๊ฐ€์ •)
353
+ with open(temp_audio.name, 'rb') as f_bytes:
354
+ audio_bytes = f_bytes.read()
355
+ stt_result = stt_client.transcribe_audio(audio_bytes, language="ko")
356
+
357
+ # STT ๊ฒฐ๊ณผ ์ฒ˜๋ฆฌ
358
+ if not isinstance(stt_result, dict) or not stt_result.get("success"):
359
+ error_msg = stt_result.get("error", "์•Œ ์ˆ˜ ์—†๋Š” STT ์˜ค๋ฅ˜") if isinstance(stt_result, dict) else "STT ๊ฒฐ๊ณผ ํ˜•์‹ ์˜ค๋ฅ˜"
360
+ logger.error(f"์Œ์„ฑ์ธ์‹ ์‹คํŒจ: {error_msg}")
361
+ return jsonify({"error": "์Œ์„ฑ์ธ์‹ ์‹คํŒจ", "details": error_msg}), 500
362
+
363
+ transcription = stt_result.get("text", "")
364
+ if not transcription:
365
+ logger.warning("์Œ์„ฑ์ธ์‹ ๊ฒฐ๊ณผ๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค.")
366
+ return jsonify({
367
+ "transcription": "",
368
+ "answer": "์Œ์„ฑ์—์„œ ํ…์ŠคํŠธ๋ฅผ ์ธ์‹ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.",
369
+ "sources": []
370
+ }), 200 # 200 OK์™€ ๋ฉ”์‹œ์ง€
371
+
372
+ logger.info(f"์Œ์„ฑ์ธ์‹ ์„ฑ๊ณต: {transcription[:50]}...")
373
+
374
+ # --- RAG ๋ฐ LLM ํ˜ธ์ถœ (Chat API์™€ ๋™์ผ ๋กœ์ง) ---
375
+ # ๊ฒ€์ƒ‰ ์—”์ง„ ์ฒ˜๋ฆฌ ๋ถ€๋ถ„
376
+ search_results = []
377
+ search_warning = None
378
+ try:
379
+ # retriever ์ƒํƒœ ๊ฒ€์ฆ
380
+ if retriever is None:
381
+ logger.warning("Retriever๊ฐ€ ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค.")
382
+ search_warning = "๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."
383
+ elif hasattr(retriever, 'is_mock') and retriever.is_mock:
384
+ logger.info("Mock Retriever ์‚ฌ์šฉ ์ค‘ - ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ.")
385
+ search_warning = "๊ฒ€์ƒ‰ ์ธ๋ฑ์Šค๊ฐ€ ์•„์ง ๊ตฌ์ถ• ์ค‘์ž…๋‹ˆ๋‹ค. ๊ธฐ๋ณธ ์‘๋‹ต๋งŒ ์ œ๊ณต๋ฉ๋‹ˆ๋‹ค."
386
+ elif not hasattr(retriever, 'search'):
387
+ logger.warning("Retriever์— search ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
388
+ search_warning = "๊ฒ€์ƒ‰ ๊ธฐ๋Šฅ์ด ํ˜„์žฌ ์ œํ•œ๋˜์–ด ์žˆ์Šต๋‹ˆ๋‹ค."
389
+ else:
390
+ logger.info(f"๊ฒ€์ƒ‰ ์ˆ˜ํ–‰: {transcription[:50]}...")
391
+ search_results = retriever.search(transcription, top_k=5, first_stage_k=6)
392
+ if not search_results:
393
+ logger.info("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
394
+ else:
395
+ logger.info(f"๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ: {len(search_results)}๊ฐœ ํ•ญ๋ชฉ")
396
+ except Exception as e:
397
+ logger.error(f"๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}", exc_info=True)
398
+ search_results = []
399
+ search_warning = f"๊ฒ€์ƒ‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"
400
+
401
+ # LLM ์‘๋‹ต ์ƒ์„ฑ
402
+ context = ""
403
+ if search_results:
404
+ if DocumentProcessor is None or not hasattr(DocumentProcessor, 'prepare_rag_context'):
405
+ logger.warning("DocumentProcessor๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ prepare_rag_context ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
406
+ else:
407
+ context = DocumentProcessor.prepare_rag_context(search_results, field="text")
408
+ logger.info(f"์ปจํ…์ŠคํŠธ ์ค€๋น„ ์™„๋ฃŒ (๊ธธ์ด: {len(context) if context else 0}์ž)")
409
+
410
+ # LLM ์ธํ„ฐํŽ˜์ด์Šค ํ˜ธ์ถœ
411
+ llm_id = request.form.get('llm_id', None) # form ๋ฐ์ดํ„ฐ์—์„œ llm_id ๊ฐ€์ ธ์˜ค๊ธฐ
412
+
413
+ if not context:
414
+ if search_warning:
415
+ logger.info(f"์ปจํ…์ŠคํŠธ ์—†์Œ, ๊ฒ€์ƒ‰ ๊ฒฝ๊ณ : {search_warning}")
416
+ answer = f"์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์งˆ๋ฌธ์— ๋Œ€ํ•œ ๋‹ต๋ณ€์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ({search_warning})"
417
+ else:
418
+ logger.info("์ปจํ…์ŠคํŠธ ์—†๏ฟฝ๏ฟฝ๏ฟฝ ๊ธฐ๋ณธ ์‘๋‹ต ์ƒ์„ฑ")
419
+ answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
420
+ else:
421
+ if llm_interface is None or not hasattr(llm_interface, 'rag_generate'):
422
+ logger.error("LLM ์ธํ„ฐํŽ˜์ด์Šค๊ฐ€ ์ค€๋น„๋˜์ง€ ์•Š์•˜๊ฑฐ๋‚˜ rag_generate ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
423
+ answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ LLM ์„œ๋น„์Šค๋ฅผ ์‚ฌ์šฉํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
424
+ else:
425
+ # LLM ํ˜ธ์ถœ ์ „์— ๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
426
+ if search_warning:
427
+ modified_query = f"{transcription}\n\n์ฐธ๊ณ : {search_warning}"
428
+ logger.info(f"๊ฒฝ๊ณ  ๋ฉ”์‹œ์ง€์™€ ํ•จ๊ป˜ ์ฟผ๋ฆฌ ์ƒ์„ฑ: {modified_query[:100]}...")
429
+ else:
430
+ modified_query = transcription
431
+
432
+ answer = llm_interface.rag_generate(modified_query, context, llm_id=llm_id)
433
+ logger.info(f"LLM ์‘๋‹ต ์ƒ์„ฑ ์™„๋ฃŒ (๊ธธ์ด: {len(answer)})")
434
+
435
+ # ์†Œ์Šค ์ •๋ณด ์ถ”์ถœ
436
+ sources = []
437
+ if search_results:
438
+ for result in search_results:
439
+ if not isinstance(result, dict):
440
+ logger.warning(f"์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ˜•์‹: {type(result)}")
441
+ continue
442
+ source_info = {}
443
+ source_key = result.get("source")
444
+ if not source_key and "metadata" in result and isinstance(result["metadata"], dict):
445
+ source_key = result["metadata"].get("source")
446
+
447
+ if source_key:
448
+ source_info["name"] = os.path.basename(source_key)
449
+ source_info["path"] = source_key
450
+ else:
451
+ source_info["name"] = "์•Œ ์ˆ˜ ์—†๋Š” ์†Œ์Šค"
452
+
453
+ if "score" in result:
454
+ source_info["score"] = result["score"]
455
+ if "rerank_score" in result:
456
+ source_info["rerank_score"] = result["rerank_score"]
457
+
458
+ sources.append(source_info)
459
+
460
+ # ์ตœ์ข… ์‘๋‹ต
461
+ response_data = {
462
+ "transcription": transcription,
463
+ "answer": answer,
464
+ "sources": sources,
465
+ "search_warning": search_warning
466
+ }
467
+
468
+ # LLM ์ •๋ณด ์ถ”๊ฐ€ (์˜ต์…˜)
469
+ if hasattr(llm_interface, 'get_current_llm_details'):
470
+ response_data["llm"] = llm_interface.get_current_llm_details()
471
+
472
+ return jsonify(response_data)
473
+
474
+ except Exception as e:
475
+ logger.error(f"์Œ์„ฑ ์ฑ— ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
476
  return jsonify({
477
+ "error": "์Œ์„ฑ ์ฒ˜๋ฆฌ ์ค‘ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ ๋ฐœ์ƒ",
478
+ "details": str(e),
479
+ "answer": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์˜ค๋””์˜ค ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."
480
+ }), 500
481
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
482
  except Exception as e:
483
+ logger.error(f"์Œ์„ฑ API์—์„œ ์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}", exc_info=True)
484
+ return jsonify({
485
+ "error": f"์˜ˆ์ƒ์น˜ ๋ชปํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}",
486
+ "answer": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ์„œ๋ฒ„์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค."
487
+ }), 500
488
 
489
  # --- Document Upload API ---
490
  @app.route('/api/upload', methods=['POST'])