jeongsoo commited on
Commit
460bd69
ยท
1 Parent(s): 5542876

add function

Browse files
app/app.py CHANGED
@@ -1,5 +1,5 @@
1
  """
2
- RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ (์„ธ์…˜ ์„ค์ • ์ˆ˜์ • ์ ์šฉ)
3
  """
4
 
5
  import os
@@ -7,16 +7,19 @@ import json
7
  import logging
8
  import tempfile
9
  import threading
10
- import datetime
 
11
  from flask import Flask, request, jsonify, render_template, send_from_directory, session, redirect, url_for
12
  from werkzeug.utils import secure_filename
13
  from dotenv import load_dotenv
14
  from functools import wraps
 
 
15
 
16
  # ๋กœ๊ฑฐ ์„ค์ •
17
  logging.basicConfig(
18
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
19
- level=logging.DEBUG # INFO์—์„œ DEBUG๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ๋” ์ƒ์„ธํ•œ ๋กœ๊ทธ ํ™•์ธ
20
  )
21
  logger = logging.getLogger(__name__)
22
 
@@ -26,11 +29,13 @@ load_dotenv()
26
  # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ ์ƒํƒœ ํ™•์ธ ๋ฐ ๋กœ๊น…
27
  ADMIN_USERNAME = os.getenv('ADMIN_USERNAME')
28
  ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD')
 
29
 
30
  logger.info(f"==== ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ ์ƒํƒœ ====")
31
  logger.info(f"ADMIN_USERNAME ์„ค์ • ์—ฌ๋ถ€: {ADMIN_USERNAME is not None}")
32
  # ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ๋กœ๋“œ ์—ฌ๋ถ€๋งŒ ๊ธฐ๋ก (๋ณด์•ˆ)
33
  logger.info(f"ADMIN_PASSWORD ์„ค์ • ์—ฌ๋ถ€: {ADMIN_PASSWORD is not None}")
 
34
 
35
  # ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • (๊ฐœ๋ฐœ์šฉ, ๋ฐฐํฌ ์‹œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ๊ถŒ์žฅ)
36
  if not ADMIN_USERNAME:
@@ -40,10 +45,10 @@ if not ADMIN_USERNAME:
40
  if not ADMIN_PASSWORD:
41
  ADMIN_PASSWORD = 'rag12345'
42
  logger.warning("ADMIN_PASSWORD ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์—†์–ด ๊ธฐ๋ณธ๊ฐ’ 'rag12345'๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.")
 
43
  class MockComponent: pass
44
 
45
  # --- ๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ---
46
- # ์‹ค์ œ ๊ฒฝ๋กœ์— ๋งž๊ฒŒ utils, retrieval ํด๋”๊ฐ€ ์กด์žฌํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
47
  try:
48
  from utils.vito_stt import VitoSTT
49
  from utils.llm_interface import LLMInterface
@@ -52,8 +57,6 @@ try:
52
  from retrieval.reranker import ReRanker
53
  except ImportError as e:
54
  logger.error(f"๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ์‹คํŒจ: {e}. utils ๋ฐ retrieval ํŒจํ‚ค์ง€๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ๊ฒฝ๋กœ์— ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.")
55
- # ๊ฐœ๋ฐœ/ํ…Œ์ŠคํŠธ๋ฅผ ์œ„ํ•ด ์ž„์‹œ ํด๋ž˜์Šค ์ •์˜ (์‹ค์ œ ์‚ฌ์šฉ ์‹œ ์ œ๊ฑฐ)
56
-
57
  VitoSTT = LLMInterface = DocumentProcessor = VectorRetriever = ReRanker = MockComponent
58
  # --- ๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ๋ ---
59
 
@@ -62,20 +65,15 @@ except ImportError as e:
62
  app = Flask(__name__)
63
 
64
  # ์„ธ์…˜ ์„ค์ • - ๊ณ ์ •๋œ ์‹œํฌ๋ฆฟ ํ‚ค ์‚ฌ์šฉ (์‹ค์ œ ๋ฐฐํฌ ์‹œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋“ฑ์œผ๋กœ ๊ด€๋ฆฌ ๊ถŒ์žฅ)
65
- app.secret_key = os.getenv('FLASK_SECRET_KEY', 'rag_chatbot_fixed_secret_key_12345') # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์šฐ์„  ์‚ฌ์šฉ
66
 
67
- # --- ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ • ์ˆ˜์ • (ํ—ˆ๊น…ํŽ˜์ด์Šค ํ™˜๊ฒฝ ๊ณ ๋ ค) ---
68
- # ํ—ˆ๊น…ํŽ˜์ด์Šค ์ŠคํŽ˜์ด์Šค๋Š” ์ผ๋ฐ˜์ ์œผ๋กœ HTTPS๋กœ ์„œ๋น„์Šค๋˜๋ฏ€๋กœ Secure=True ์„ค์ •
69
  app.config['SESSION_COOKIE_SECURE'] = True
70
- app.config['SESSION_COOKIE_HTTPONLY'] = True # JavaScript์—์„œ ์ฟ ํ‚ค ์ ‘๊ทผ ๋ฐฉ์ง€ (๋ณด์•ˆ ๊ฐ•ํ™”)
71
- # SameSite='Lax'๊ฐ€ ๋Œ€๋ถ€๋ถ„์˜ ๊ฒฝ์šฐ์— ๋” ์•ˆ์ „ํ•˜๊ณ  ํ˜ธํ™˜์„ฑ์ด ์ข‹์Œ.
72
- # ๋งŒ์•ฝ ์•ฑ์ด ๋‹ค๋ฅธ ๋„๋ฉ”์ธ์˜ iframe ๋‚ด์—์„œ ์‹คํ–‰๋˜์–ด์•ผ ํ•œ๋‹ค๋ฉด 'None'์œผ๋กœ ์„ค์ •ํ•ด์•ผ ํ•จ.
73
- # (๋‹จ, 'None'์œผ๋กœ ์„ค์ • ์‹œ ๋ฐ˜๋“œ์‹œ Secure=True์—ฌ์•ผ ํ•จ)
74
- # ๋กœ๊ทธ ๋ถ„์„ ๊ฒฐ๊ณผ iframe ํ™˜๊ฒฝ์œผ๋กœ ํ™•์ธ๋˜์–ด 'None'์œผ๋กœ ๋ณ€๊ฒฝ
75
- app.config['SESSION_COOKIE_SAMESITE'] = 'None' # <--- ์ด๋ ‡๊ฒŒ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.
76
- app.config['SESSION_COOKIE_DOMAIN'] = None # ํŠน์ • ๋„๋ฉ”์ธ ์ œํ•œ ์—†์Œ
77
- app.config['SESSION_COOKIE_PATH'] = '/' # ์•ฑ ์ „์ฒด ๊ฒฝ๋กœ์— ์ฟ ํ‚ค ์ ์šฉ
78
- app.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(days=1) # ์„ธ์…˜ ์œ ํšจ ์‹œ๊ฐ„ ์ฆ๊ฐ€
79
  # --- ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ • ๋ ---
80
 
81
  # ์ตœ๋Œ€ ํŒŒ์ผ ํฌ๊ธฐ ์„ค์ • (10MB)
@@ -110,827 +108,21 @@ app_ready = False # ์•ฑ ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ”Œ๋ž˜๊ทธ
110
  # --- ์ „์—ญ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ๋ ---
111
 
112
 
113
- # --- ์ธ์ฆ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ (์ˆ˜์ •๋จ) ---
114
  def login_required(f):
115
  @wraps(f)
116
  def decorated_function(*args, **kwargs):
117
  logger.info(f"----------- ์ธ์ฆ ํ•„์š” ํŽ˜์ด์ง€ ์ ‘๊ทผ ์‹œ๋„: {request.path} -----------")
118
  logger.info(f"ํ˜„์žฌ ํ”Œ๋ผ์Šคํฌ ์„ธ์…˜ ๊ฐ์ฒด: {session}")
119
  logger.info(f"ํ˜„์žฌ ์„ธ์…˜ ์ƒํƒœ: logged_in={session.get('logged_in', False)}, username={session.get('username', 'None')}")
120
- # ๋ธŒ๋ผ์šฐ์ €๊ฐ€ ๋ณด๋‚ธ ์‹ค์ œ ์ฟ ํ‚ค ํ™•์ธ (๋””๋ฒ„๊น…์šฉ)
121
  logger.info(f"์š”์ฒญ์˜ ์„ธ์…˜ ์ฟ ํ‚ค ๊ฐ’: {request.cookies.get('session', 'None')}")
122
 
123
  # Flask ์„ธ์…˜์— 'logged_in' ํ‚ค๊ฐ€ ์žˆ๋Š”์ง€ ์ง์ ‘ ํ™•์ธ
124
  if 'logged_in' not in session:
125
  logger.warning(f"ํ”Œ๋ผ์Šคํฌ ์„ธ์…˜์— 'logged_in' ์—†์Œ. ๋กœ๊ทธ์ธ ํŽ˜๏ฟฝ๏ฟฝ๏ฟฝ์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜.")
126
- # ์ˆ˜๋™ ์ฟ ํ‚ค ํ™•์ธ ๋กœ์ง ์ œ๊ฑฐ๋จ
127
- return redirect(url_for('login', next=request.url)) # ๋กœ๊ทธ์ธ ํ›„ ์›๋ž˜ ํŽ˜์ด์ง€๋กœ ๋Œ์•„๊ฐ€๋„๋ก next ํŒŒ๋ผ๋ฏธํ„ฐ ์ถ”๊ฐ€
128
 
129
  logger.info(f"์ธ์ฆ ์„ฑ๊ณต: {session.get('username', 'unknown')} ์‚ฌ์šฉ์ž๊ฐ€ {request.path} ์ ‘๊ทผ")
130
  return f(*args, **kwargs)
131
  return decorated_function
132
  # --- ์ธ์ฆ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ๋ ---
133
-
134
-
135
- # --- ํ—ฌํผ ํ•จ์ˆ˜ ---
136
- def allowed_audio_file(filename):
137
- """ํŒŒ์ผ์ด ํ—ˆ์šฉ๋œ ์˜ค๋””์˜ค ํ™•์žฅ์ž๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธ"""
138
- return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_AUDIO_EXTENSIONS
139
-
140
- def allowed_doc_file(filename):
141
- """ํŒŒ์ผ์ด ํ—ˆ์šฉ๋œ ๋ฌธ์„œ ํ™•์žฅ์ž๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธ"""
142
- return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_DOC_EXTENSIONS
143
- # --- ํ—ฌํผ ํ•จ์ˆ˜ ๋ ---
144
-
145
-
146
- # init_retriever ํ•จ์ˆ˜ ๋‚ด๋ถ€์— ๋กœ๊น… ์ถ”๊ฐ€ ์˜ˆ์‹œ
147
- # --- ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ๊ด€๋ จ ํ•จ์ˆ˜ ---
148
- def init_retriever():
149
- """๊ฒ€์ƒ‰๊ธฐ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ๋˜๋Š” ๋กœ๋“œ"""
150
- global base_retriever, retriever
151
-
152
- index_path = app.config['INDEX_PATH']
153
- data_path = app.config['DATA_FOLDER'] # data_path ์ •์˜ ํ™•์ธ
154
- logger.info("--- init_retriever ์‹œ์ž‘ ---")
155
-
156
- # 1. ๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ ๋กœ๋“œ ๋˜๋Š” ์ดˆ๊ธฐํ™”
157
- # ... (VectorRetriever ๋กœ๋“œ ๋˜๋Š” ์ดˆ๊ธฐํ™” ๋กœ์ง์€ ์ด์ „๊ณผ ๋™์ผํ•˜๊ฒŒ ์œ ์ง€) ...
158
- # VectorRetriever ์ดˆ๊ธฐํ™”/๋กœ๋“œ ์‹คํŒจ ์‹œ base_retriever = None ๋ฐ return None ์ฒ˜๋ฆฌ ํฌํ•จ
159
- if os.path.exists(os.path.join(index_path, "documents.json")):
160
- try:
161
- logger.info(f"์ธ๋ฑ์Šค ๋กœ๋“œ ์‹œ๋„: {index_path}")
162
- base_retriever = VectorRetriever.load(index_path)
163
- logger.info(f"์ธ๋ฑ์Šค ๋กœ๋“œ ์„ฑ๊ณต. ๋ฌธ์„œ {len(getattr(base_retriever, 'documents', []))}๊ฐœ")
164
- except Exception as e:
165
- logger.error(f"์ธ๋ฑ์Šค ๋กœ๋“œ ์‹คํŒจ: {e}", exc_info=True)
166
- logger.info("์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์‹œ๋„...")
167
- try:
168
- base_retriever = VectorRetriever()
169
- logger.info("์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์„ฑ๊ณต.")
170
- except Exception as e_init:
171
- logger.error(f"์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์‹คํŒจ: {e_init}", exc_info=True)
172
- base_retriever = None
173
- else:
174
- logger.info("์ธ๋ฑ์Šค ํŒŒ์ผ ์—†์Œ. ์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์‹œ๋„...")
175
- try:
176
- base_retriever = VectorRetriever()
177
- logger.info("์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์„ฑ๊ณต.")
178
- except Exception as e_init:
179
- logger.error(f"์ƒˆ VectorRetriever ์ดˆ๊ธฐํ™” ์‹คํŒจ: {e_init}", exc_info=True)
180
- base_retriever = None
181
-
182
- if base_retriever is None:
183
- logger.error("base_retriever ์ดˆ๊ธฐํ™”/๋กœ๋“œ์— ์‹คํŒจํ•˜์—ฌ init_retriever ์ค‘๋‹จ.")
184
- return None
185
-
186
- # 2. ๋ฐ์ดํ„ฐ ํด๋” ๋ฌธ์„œ ๋กœ๋“œ (๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ๋น„์–ด์žˆ์„ ๋•Œ)
187
- needs_loading = (not hasattr(base_retriever, 'documents') or not getattr(base_retriever, 'documents', None)) # None ์ฒดํฌ ์ถ”๊ฐ€
188
- if needs_loading and os.path.exists(data_path):
189
- logger.info(f"๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ๋น„์–ด์žˆ์–ด {data_path}์—์„œ ๋ฌธ์„œ ๋กœ๋“œ ์‹œ๋„...")
190
- try:
191
- # ================== ์ˆ˜์ •๋œ ๋ถ€๋ถ„ 1 ์‹œ์ž‘ ==================
192
- # DocumentProcessor.load_documents_from_directory ํ˜ธ์ถœ ์‹œ ์˜ฌ๋ฐ”๋ฅธ ์ธ์ž ์ „๋‹ฌ
193
- docs = DocumentProcessor.load_documents_from_directory(
194
- directory=data_path, # <-- ๊ฒฝ๋กœ ๋ณ€์ˆ˜ ์‚ฌ์šฉ
195
- extensions=[".txt", ".md", ".csv"], # <-- ํ•„์š”ํ•œ ํ™•์žฅ์ž ์ „๋‹ฌ
196
- recursive=True # <-- ์žฌ๊ท€ ํƒ์ƒ‰ ์—ฌ๋ถ€ ์ „๋‹ฌ
197
- )
198
- # ================== ์ˆ˜์ •๋œ ๋ถ€๋ถ„ 1 ๋ ====================
199
- logger.info(f"{len(docs)}๊ฐœ ๋ฌธ์„œ ๋กœ๋“œ ์„ฑ๊ณต.")
200
- if docs and hasattr(base_retriever, 'add_documents'):
201
- logger.info("๊ฒ€์ƒ‰๊ธฐ์— ๋ฌธ์„œ ์ถ”๊ฐ€ ์‹œ๋„...")
202
- base_retriever.add_documents(docs)
203
- logger.info("๋ฌธ์„œ ์ถ”๊ฐ€ ์™„๋ฃŒ.")
204
-
205
- if hasattr(base_retriever, 'save'):
206
- logger.info(f"๊ฒ€์ƒ‰๊ธฐ ์ƒํƒœ ์ €์žฅ ์‹œ๋„: {index_path}")
207
- try:
208
- base_retriever.save(index_path)
209
- logger.info("์ธ๋ฑ์Šค ์ €์žฅ ์™„๋ฃŒ.")
210
- except Exception as e_save:
211
- logger.error(f"์ธ๋ฑ์Šค ์ €์žฅ ์‹คํŒจ: {e_save}", exc_info=True)
212
- except Exception as e_load_add:
213
- # load_documents_from_directory ์ž์ฒด์—์„œ ์˜ค๋ฅ˜๊ฐ€ ๋‚  ์ˆ˜๋„ ์žˆ์Œ (๊ถŒํ•œ ๋“ฑ)
214
- logger.error(f"DATA_FOLDER ๋ฌธ์„œ ๋กœ๋“œ/์ถ”๊ฐ€ ์ค‘ ์˜ค๋ฅ˜: {e_load_add}", exc_info=True)
215
-
216
- # 3. ์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”
217
- logger.info("์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์‹œ๋„...")
218
- try:
219
- # ================== ์ˆ˜์ •๋œ ๋ถ€๋ถ„ 2 ์‹œ์ž‘ ==================
220
- # custom_rerank_fn ํ•จ์ˆ˜๋ฅผ ReRanker ์ดˆ๊ธฐ๏ฟฝ๏ฟฝ ์ „์— ์ •์˜
221
- def custom_rerank_fn(query, results):
222
- query_terms = set(query.lower().split())
223
- for result in results:
224
- if isinstance(result, dict) and "text" in result:
225
- text = result["text"].lower()
226
- term_freq = sum(1 for term in query_terms if term in text)
227
- normalized_score = term_freq / (len(text.split()) + 1) * 10
228
- result["rerank_score"] = result.get("score", 0) * 0.7 + normalized_score * 0.3
229
- elif isinstance(result, dict):
230
- result["rerank_score"] = result.get("score", 0)
231
- results.sort(key=lambda x: x.get("rerank_score", 0) if isinstance(x, dict) else 0, reverse=True)
232
- return results
233
- # ================== ์ˆ˜์ •๋œ ๋ถ€๋ถ„ 2 ๋ ====================
234
-
235
- # ReRanker ํด๋ž˜์Šค ์‚ฌ์šฉ
236
- retriever = ReRanker(
237
- base_retriever=base_retriever,
238
- rerank_fn=custom_rerank_fn, # ์ด์ œ ํ•จ์ˆ˜๊ฐ€ ์ •์˜๋˜์—ˆ์œผ๋ฏ€๋กœ ์‚ฌ์šฉ ๊ฐ€๋Šฅ
239
- rerank_field="text"
240
- )
241
- logger.info("์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ.")
242
- except Exception as e_rerank:
243
- logger.error(f"์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์‹คํŒจ: {e_rerank}", exc_info=True)
244
- logger.warning("์žฌ์ˆœ์œ„ํ™” ์‹คํŒจ, ๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๋ฅผ retriever๋กœ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.")
245
- retriever = base_retriever # fallback
246
-
247
- logger.info("--- init_retriever ์ข…๋ฃŒ ---")
248
- return retriever
249
-
250
- def background_init():
251
- """๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์ˆ˜ํ–‰"""
252
- global app_ready, retriever, base_retriever, llm_interface, stt_client
253
-
254
- temp_app_ready = False # ์ž„์‹œ ์ƒํƒœ ํ”Œ๋ž˜๊ทธ
255
- try:
256
- logger.info("๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ์‹œ์ž‘...")
257
-
258
- # 1. LLM, STT ์ธํ„ฐํŽ˜์ด์Šค ์ดˆ๊ธฐํ™” (ํ•„์š” ์‹œ)
259
- if llm_interface is None or isinstance(llm_interface, MockComponent):
260
- if 'LLMInterface' in globals() and LLMInterface != MockComponent:
261
- llm_interface = LLMInterface(default_llm="openai")
262
- logger.info("LLM ์ธํ„ฐํŽ˜์ด์Šค ์ดˆ๊ธฐํ™” ์™„๋ฃŒ.")
263
- else:
264
- logger.warning("LLMInterface ํด๋ž˜์Šค ์—†์Œ. Mock ์‚ฌ์šฉ.")
265
- llm_interface = MockComponent() # Mock ๊ฐ์ฒด ๋ณด์žฅ
266
- if stt_client is None or isinstance(stt_client, MockComponent):
267
- if 'VitoSTT' in globals() and VitoSTT != MockComponent:
268
- stt_client = VitoSTT()
269
- logger.info("STT ํด๋ผ์ด์–ธํŠธ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ.")
270
- else:
271
- logger.warning("VitoSTT ํด๋ž˜์Šค ์—†์Œ. Mock ์‚ฌ์šฉ.")
272
- stt_client = MockComponent() # Mock ๊ฐ์ฒด ๋ณด์žฅ
273
-
274
-
275
- # 2. ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”
276
- if 'VectorRetriever' in globals() and VectorRetriever != MockComponent:
277
- logger.info("์‹ค์ œ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์‹œ๋„...")
278
- # init_retriever๊ฐ€ base_retriever์™€ retriever๋ฅผ ๋ชจ๋‘ ์„ค์ •ํ•œ๋‹ค๊ณ  ๊ฐ€์ •
279
- retriever = init_retriever()
280
- # init_retriever ๋‚ด๋ถ€์—์„œ base_retriever๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์•˜๋‹ค๋ฉด ์—ฌ๊ธฐ์„œ ์„ค์ •
281
- if hasattr(retriever, 'base_retriever') and base_retriever is None:
282
- base_retriever = retriever.base_retriever
283
- elif base_retriever is None:
284
- # retriever๊ฐ€ base_retriever๋ฅผ ํฌํ•จํ•˜์ง€ ์•Š๋Š” ๊ฒฝ์šฐ ๋˜๋Š” ReRanker๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ
285
- # init_retriever์—์„œ base_retriever๋ฅผ ์ง์ ‘ ์„ค์ •ํ•˜๋„๋ก ํ•˜๊ฑฐ๋‚˜, ์—ฌ๊ธฐ์„œ ๋ณ„๋„ ๋กœ์ง ํ•„์š”
286
- # ์˜ˆ์‹œ: base_retriever = VectorRetriever.load(...) ๋˜๋Š” VectorRetriever()
287
- logger.warning("init_retriever ํ›„ base_retriever๊ฐ€ ์„ค์ •๋˜์ง€ ์•Š์Œ. ํ™•์ธ ํ•„์š”.")
288
- # ์ž„์‹œ๋กœ retriever ์ž์ฒด๋ฅผ base_retriever๋กœ ์„ค์ • (๋™์ผ ๊ฐ์ฒด์ผ ๊ฒฝ์šฐ)
289
- if isinstance(retriever, VectorRetriever):
290
- base_retriever = retriever
291
-
292
- # ์„ฑ๊ณต์ ์œผ๋กœ ์ดˆ๊ธฐํ™” ๋˜์—ˆ๋Š”์ง€ ํ™•์ธ (None์ด ์•„๋‹Œ์ง€)
293
- if retriever is not None and base_retriever is not None:
294
- logger.info("๊ฒ€์ƒ‰๊ธฐ (Retriever, Base Retriever) ์ดˆ๊ธฐํ™” ์„ฑ๊ณต")
295
- temp_app_ready = True # ์ดˆ๊ธฐํ™” ์„ฑ๊ณต ์‹œ์—๋งŒ True ์„ค์ •
296
- else:
297
- logger.error("๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ํ›„์—๋„ retriever ๋˜๋Š” base_retriever๊ฐ€ None์ž…๋‹ˆ๋‹ค.")
298
- # ์‹คํŒจ ์‹œ Mock ๊ฐ์ฒด ํ• ๋‹น (์ตœ์†Œํ•œ์˜ ๋™์ž‘ ๋ณด์žฅ)
299
- if base_retriever is None: base_retriever = MockComponent()
300
- if retriever is None: retriever = MockComponent()
301
- if not hasattr(retriever, 'search'): retriever.search = lambda query, **kwargs: []
302
- if not hasattr(base_retriever, 'documents'): base_retriever.documents = []
303
- # temp_app_ready = False ๋˜๋Š” True (์ •์ฑ…์— ๋”ฐ๋ผ ๊ฒฐ์ •)
304
- temp_app_ready = True # ์ผ๋‹จ ์•ฑ์€ ์‹คํ–‰๋˜๋„๋ก ์„ค์ •
305
-
306
- else:
307
- logger.warning("VectorRetriever ํด๏ฟฝ๏ฟฝ๏ฟฝ์Šค ์—†์Œ. Mock ๊ฒ€์ƒ‰๊ธฐ ์‚ฌ์šฉ.")
308
- base_retriever = MockComponent()
309
- retriever = MockComponent()
310
- if not hasattr(retriever, 'search'): retriever.search = lambda query, **kwargs: []
311
- if not hasattr(base_retriever, 'documents'): base_retriever.documents = []
312
- temp_app_ready = True # Mock์ด๋ผ๋„ ์ค€๋น„๋Š” ๋œ ๊ฒƒ์œผ๋กœ ๊ฐ„์ฃผ
313
-
314
- logger.info(f"๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ. ์ตœ์ข… ์ƒํƒœ: {'Ready' if temp_app_ready else 'Not Ready (Error during init)'}")
315
-
316
- except Exception as e:
317
- logger.error(f"์•ฑ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ์ค‘ ์‹ฌ๊ฐํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
318
- # ์˜ค๋ฅ˜ ๋ฐœ์ƒ ์‹œ์—๋„ Mock ๊ฐ์ฒด ํ• ๋‹น ์‹œ๋„
319
- if base_retriever is None: base_retriever = MockComponent()
320
- if retriever is None: retriever = MockComponent()
321
- if not hasattr(retriever, 'search'): retriever.search = lambda query, **kwargs: []
322
- if not hasattr(base_retriever, 'documents'): base_retriever.documents = []
323
- temp_app_ready = True # ์˜ค๋ฅ˜ ๋ฐœ์ƒํ•ด๋„ ์•ฑ์€ ์‘๋‹ตํ•˜๋„๋ก ์„ค์ • (์ •์ฑ…์— ๋”ฐ๋ผ False ๊ฐ€๋Šฅ)
324
- logger.warning("์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์ง€๋งŒ Mock ๊ฐ์ฒด๋กœ ๋Œ€์ฒด ํ›„ ์•ฑ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์ƒํƒœ๋กœ ์„ค์ •.")
325
-
326
- finally:
327
- # ์ตœ์ข…์ ์œผ๋กœ app_ready ์ƒํƒœ ์—…๋ฐ์ดํŠธ
328
- app_ready = temp_app_ready
329
-
330
- # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ ์‹œ์ž‘ ๋ถ€๋ถ„์€ ๊ทธ๋Œ€๋กœ ์œ ์ง€
331
- init_thread = threading.Thread(target=background_init)
332
- init_thread.daemon = True
333
- init_thread.start()
334
-
335
-
336
- # --- Flask ๋ผ์šฐํŠธ ์ •์˜ ---
337
-
338
- @app.route('/login', methods=['GET', 'POST'])
339
- def login():
340
- error = None
341
- next_url = request.args.get('next') # ๋ฆฌ๋””๋ ‰์…˜ํ•  URL ๊ฐ€์ ธ์˜ค๊ธฐ
342
- logger.info(f"-------------- ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ์ ‘์† (Next: {next_url}) --------------")
343
- logger.info(f"Method: {request.method}")
344
-
345
- # ํ—ค๋” ๋กœ๊น… (๋””๋ฒ„๊น…์šฉ)
346
- # logger.debug("Request Headers:")
347
- # for header, value in request.headers.items():
348
- # logger.debug(f" {header}: {value}")
349
-
350
- if request.method == 'POST':
351
- logger.info("๋กœ๊ทธ์ธ ์‹œ๋„ ๋ฐ›์Œ")
352
- username = request.form.get('username', '')
353
- password = request.form.get('password', '')
354
- logger.info(f"์ž…๋ ฅ๋œ ์‚ฌ์šฉ์ž๋ช…: {username}")
355
- logger.info(f"๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ์—ฌ๋ถ€: {len(password) > 0}")
356
-
357
- # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋˜๋Š” ๊ธฐ๋ณธ๊ฐ’๊ณผ ๋น„๊ต
358
- valid_username = ADMIN_USERNAME
359
- valid_password = ADMIN_PASSWORD
360
- logger.info(f"๊ฒ€์ฆ์šฉ ์‚ฌ์šฉ์ž๋ช…: {valid_username}")
361
- logger.info(f"๊ฒ€์ฆ์šฉ ๋น„๋ฐ€๋ฒˆํ˜ธ ์กด์žฌ ์—ฌ๋ถ€: {valid_password is not None and len(valid_password) > 0}")
362
-
363
-
364
- if username == valid_username and password == valid_password:
365
- logger.info(f"๋กœ๊ทธ์ธ ์„ฑ๊ณต: {username}")
366
- # ์„ธ์…˜ ์„ค์ • ์ „ ํ˜„์žฌ ์„ธ์…˜ ์ƒํƒœ ๋กœ๊น…
367
- logger.debug(f"์„ธ์…˜ ์„ค์ • ์ „: {session}")
368
-
369
- # ์„ธ์…˜์— ๋กœ๊ทธ์ธ ์ •๋ณด ์ €์žฅ
370
- session.permanent = True # PERMANENT_SESSION_LIFETIME ์„ค์ • ์‚ฌ์šฉ
371
- session['logged_in'] = True
372
- session['username'] = username
373
- session.modified = True # ์„ธ์…˜์ด ๋ณ€๊ฒฝ๋˜์—ˆ์Œ์„ ๋ช…์‹œ (ํ•„์ˆ˜๋Š” ์•„๋‹ ์ˆ˜ ์žˆ์Œ)
374
-
375
- logger.info(f"์„ธ์…˜ ์„ค์ • ํ›„: {session}")
376
- logger.info("์„ธ์…˜ ์„ค์ • ์™„๋ฃŒ, ๋ฆฌ๋””๋ ‰์…˜ ์‹œ๋„")
377
-
378
- # ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„ ๋ฆฌ๋””๋ ‰์…˜
379
- # 'next' ํŒŒ๋ผ๋ฏธํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ํ•ด๋‹น URL๋กœ, ์—†์œผ๋ฉด ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ
380
- redirect_to = next_url or url_for('index')
381
- logger.info(f"๋ฆฌ๋””๋ ‰์…˜ ๋Œ€์ƒ: {redirect_to}")
382
- response = redirect(redirect_to)
383
-
384
- # ์‘๋‹ต ํ—ค๋” ๋กœ๊น… (Set-Cookie ํ™•์ธ์šฉ)
385
- # logger.debug(f"๋กœ๊ทธ์ธ ์„ฑ๊ณต ์‘๋‹ต ํ—ค๋”: {response.headers}")
386
- return response
387
- else:
388
- logger.warning("๋กœ๊ทธ์ธ ์‹คํŒจ: ์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜")
389
- if username != valid_username: logger.warning("์‚ฌ์šฉ์ž๋ช… ๋ถˆ์ผ์น˜")
390
- if password != valid_password: logger.warning("๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜")
391
- error = '์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'
392
- else: # GET ์š”์ฒญ
393
- logger.info("๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ GET ์š”์ฒญ")
394
- if 'logged_in' in session:
395
- logger.info("์ด๋ฏธ ๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž, ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜")
396
- return redirect(url_for('index'))
397
-
398
- logger.info("---------- ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ๋ Œ๋”๋ง ----------")
399
- return render_template('login.html', error=error, next=next_url)
400
-
401
-
402
- @app.route('/logout')
403
- def logout():
404
- logger.info("-------------- ๋กœ๊ทธ์•„์›ƒ ์š”์ฒญ --------------")
405
- logger.info(f"๋กœ๊ทธ์•„์›ƒ ์ „ ์„ธ์…˜ ์ƒํƒœ: {session}")
406
-
407
- if 'logged_in' in session:
408
- username = session.get('username', 'unknown')
409
- logger.info(f"์‚ฌ์šฉ์ž {username} ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ ์‹œ์ž‘")
410
- session.pop('logged_in', None)
411
- session.pop('username', None)
412
- session.modified = True # ์„ธ์…˜ ๏ฟฝ๏ฟฝ๏ฟฝ๊ฒฝ ๋ช…์‹œ
413
- logger.info(f"์„ธ์…˜ ์ •๋ณด ์‚ญ์ œ ์™„๋ฃŒ. ํ˜„์žฌ ์„ธ์…˜: {session}")
414
- else:
415
- logger.warning("๋กœ๊ทธ์ธ๋˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ ๋กœ๊ทธ์•„์›ƒ ์‹œ๋„")
416
-
417
- logger.info("๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜")
418
- response = redirect(url_for('login'))
419
- # logger.debug(f"๋กœ๊ทธ์•„์›ƒ ์‘๋‹ต ํ—ค๋”: {response.headers}") # ์ฟ ํ‚ค ์‚ญ์ œ ํ™•์ธ์šฉ
420
- return response
421
-
422
-
423
- @app.route('/')
424
- @login_required
425
- def index():
426
- """๋ฉ”์ธ ํŽ˜์ด์ง€"""
427
- global app_ready
428
-
429
- # ์•ฑ ์ค€๋น„ ์ƒํƒœ ํ™•์ธ - 30์ดˆ ์ด์ƒ ์ง€๋‚ฌ์œผ๋ฉด ๊ฐ•์ œ๋กœ ready ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ
430
- current_time = datetime.datetime.now()
431
- start_time = datetime.datetime.fromtimestamp(os.path.getmtime(__file__))
432
- time_diff = (current_time - start_time).total_seconds()
433
-
434
- if not app_ready and time_diff > 30:
435
- logger.warning(f"์•ฑ์ด 30์ดˆ ์ด์ƒ ์ดˆ๊ธฐํ™” ์ค‘ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ๊ฐ•์ œ๋กœ ready ์ƒํƒœ๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.")
436
- app_ready = True
437
-
438
- if not app_ready:
439
- logger.info("์•ฑ์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•„ ๋กœ๋”ฉ ํŽ˜์ด์ง€ ํ‘œ์‹œ")
440
- return render_template('loading.html'), 503 # ์„œ๋น„์Šค ์ค€๋น„ ์•ˆ๋จ ์ƒํƒœ ์ฝ”๋“œ
441
-
442
- logger.info("๋ฉ”์ธ ํŽ˜์ด์ง€ ์š”์ฒญ")
443
- return render_template('index.html')
444
-
445
-
446
- @app.route('/api/status')
447
- @login_required
448
- def app_status():
449
- """์•ฑ ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ™•์ธ API"""
450
- logger.info(f"์•ฑ ์ƒํƒœ ํ™•์ธ ์š”์ฒญ: {'Ready' if app_ready else 'Not Ready'}")
451
- return jsonify({"ready": app_ready})
452
-
453
-
454
- @app.route('/api/llm', methods=['GET', 'POST'])
455
- @login_required
456
- def llm_api():
457
- """์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ LLM ๋ชฉ๋ก ๋ฐ ์„ ํƒ API"""
458
- global llm_interface
459
-
460
- if not app_ready:
461
- return jsonify({"error": "์•ฑ์ด ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."}), 503
462
-
463
- if request.method == 'GET':
464
- logger.info("LLM ๋ชฉ๋ก ์š”์ฒญ")
465
- try:
466
- current_details = llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {"id": "unknown", "name": "Unknown"}
467
- supported_llms_dict = llm_interface.SUPPORTED_LLMS if hasattr(llm_interface, 'SUPPORTED_LLMS') else {}
468
- supported_list = [{
469
- "name": name, "id": id, "current": id == current_details.get("id")
470
- } for name, id in supported_llms_dict.items()]
471
-
472
- return jsonify({
473
- "supported_llms": supported_list,
474
- "current_llm": current_details
475
- })
476
- except Exception as e:
477
- logger.error(f"LLM ์ •๋ณด ์กฐํšŒ ์˜ค๋ฅ˜: {e}")
478
- return jsonify({"error": "LLM ์ •๋ณด ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ"}), 500
479
-
480
- elif request.method == 'POST':
481
- data = request.get_json()
482
- if not data or 'llm_id' not in data:
483
- return jsonify({"error": "LLM ID๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
484
-
485
- llm_id = data['llm_id']
486
- logger.info(f"LLM ๋ณ€๊ฒฝ ์š”์ฒญ: {llm_id}")
487
-
488
- try:
489
- if not hasattr(llm_interface, 'set_llm') or not hasattr(llm_interface, 'llm_clients'):
490
- raise NotImplementedError("LLM ์ธํ„ฐํŽ˜์ด์Šค์— ํ•„์š”ํ•œ ๋ฉ”์†Œ๋“œ/์†์„ฑ ์—†์Œ")
491
-
492
- if llm_id not in llm_interface.llm_clients:
493
- return jsonify({"error": f"์ง€์›๋˜์ง€ ์•Š๋Š” LLM ID: {llm_id}"}), 400
494
-
495
- success = llm_interface.set_llm(llm_id)
496
- if success:
497
- new_details = llm_interface.get_current_llm_details()
498
- logger.info(f"LLM์ด '{new_details.get('name', llm_id)}'๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
499
- return jsonify({
500
- "success": True,
501
- "message": f"LLM์ด '{new_details.get('name', llm_id)}'๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
502
- "current_llm": new_details
503
- })
504
- else:
505
- # set_llm ์ด False๋ฅผ ๋ฐ˜ํ™˜ํ•˜๋Š” ๊ฒฝ์šฐ (๊ฐ€๋Šฅ์„ฑ์€ ๋‚ฎ์ง€๋งŒ)
506
- logger.error(f"LLM ๋ณ€๊ฒฝ ์‹คํŒจ (ID: {llm_id})")
507
- return jsonify({"error": "LLM ๋ณ€๊ฒฝ ์ค‘ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ ๋ฐœ์ƒ"}), 500
508
- except Exception as e:
509
- logger.error(f"LLM ๋ณ€๊ฒฝ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {e}", exc_info=True)
510
- return jsonify({"error": f"LLM ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"}), 500
511
-
512
-
513
- @app.route('/api/chat', methods=['POST'])
514
- @login_required
515
- def chat():
516
- """ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ์ฑ—๋ด‡ API"""
517
- global retriever
518
-
519
- if not app_ready or retriever is None:
520
- return jsonify({"error": "์•ฑ/๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."}), 503
521
-
522
- try:
523
- data = request.get_json()
524
- if not data or 'query' not in data:
525
- return jsonify({"error": "์ฟผ๋ฆฌ๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
526
-
527
- query = data['query']
528
- logger.info(f"ํ…์ŠคํŠธ ์ฟผ๋ฆฌ ์ˆ˜์‹ : {query[:100]}...") # ๋„ˆ๋ฌด ๊ธด ์ฟผ๋ฆฌ ๋กœ๊ทธ๋Š” ์ž˜๋ผ์„œ ํ‘œ์‹œ
529
-
530
- # RAG ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰
531
- if not hasattr(retriever, 'search'):
532
- raise NotImplementedError("Retriever์— search ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
533
- search_results = retriever.search(query, top_k=5, first_stage_k=6) # ์žฌ์ˆœ์œ„ํ™” ๊ณ ๋ ค
534
-
535
- # ์ปจํ…์ŠคํŠธ ์ค€๋น„
536
- if not hasattr(DocumentProcessor, 'prepare_rag_context'):
537
- raise NotImplementedError("DocumentProcessor์— prepare_rag_context ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
538
- context = DocumentProcessor.prepare_rag_context(search_results, field="text")
539
-
540
- if not context:
541
- logger.warning("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์–ด ์ปจํ…์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•˜์ง€ ๋ชปํ•จ.")
542
- # LLM ํ˜ธ์ถœ ์—†์ด ๊ธฐ๋ณธ ์‘๋‹ต ๋ฐ˜ํ™˜ ๋˜๋Š” ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ์„ ์•Œ๋ฆฌ๋Š” ์‘๋‹ต ์ƒ์„ฑ
543
- # answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." (์•„๋ž˜ LLM ํ˜ธ์ถœ ๋กœ์ง์—์„œ ์ฒ˜๋ฆฌ)
544
- pass
545
-
546
- # LLM์— ์งˆ์˜
547
- llm_id = data.get('llm_id', None) # ํด๋ผ์ด์–ธํŠธ์—์„œ ํŠน์ • LLM ์ง€์ • ๊ฐ€๋Šฅ
548
- if not hasattr(llm_interface, 'rag_generate'):
549
- raise NotImplementedError("LLMInterface์— rag_generate ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
550
-
551
- if not context:
552
- answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
553
- logger.info("์ปจํ…์ŠคํŠธ ์—†์ด ๊ธฐ๋ณธ ์‘๋‹ต ์ƒ์„ฑ")
554
- else:
555
- answer = llm_interface.rag_generate(query, context, llm_id=llm_id)
556
- logger.info(f"LLM ์‘๋‹ต ์ƒ์„ฑ ์™„๋ฃŒ (๊ธธ์ด: {len(answer)})")
557
-
558
-
559
- # ์†Œ์Šค ์ •๋ณด ์ถ”์ถœ (CSV ID ์ถ”์ถœ ๋กœ์ง ํฌํ•จ)
560
- sources = []
561
- if search_results: # ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์žˆ์„ ๋•Œ๋งŒ ์†Œ์Šค ์ฒ˜๋ฆฌ
562
- for result in search_results:
563
- # ๊ฒฐ๊ณผ๊ฐ€ ๋”•์…”๋„ˆ๋ฆฌ ํ˜•ํƒœ์ธ์ง€ ํ™•์ธ
564
- if not isinstance(result, dict):
565
- logger.warning(f"์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ˜•์‹: {type(result)}")
566
- continue
567
-
568
- if "source" in result:
569
- source_info = {
570
- "source": result.get("source", "Unknown"),
571
- # ์žฌ์ˆœ์œ„ํ™” ์ ์ˆ˜๊ฐ€ ์žˆ์œผ๋ฉด ์‚ฌ์šฉ, ์—†์œผ๋ฉด ์›๋ž˜ ์ ์ˆ˜ ์‚ฌ์šฉ
572
- "score": result.get("rerank_score", result.get("score", 0))
573
- }
574
-
575
- # CSV ํŒŒ์ผ ํŠน์ • ์ฒ˜๋ฆฌ
576
- if "text" in result and result.get("filetype") == "csv":
577
- try:
578
- text_lines = result["text"].strip().split('\n')
579
- if text_lines:
580
- first_line = text_lines[0].strip()
581
- if ',' in first_line:
582
- first_column = first_line.split(',')[0].strip()
583
- source_info["id"] = first_column # ์˜ˆ: CSV์˜ ์ฒซ ์ปฌ๋Ÿผ ๊ฐ’์„ ID๋กœ ์ถ”๊ฐ€
584
- logger.debug(f"CSV ์†Œ์Šค ID ์ถ”์ถœ: {first_column} from {source_info['source']}")
585
- except Exception as e:
586
- logger.warning(f"CSV ์†Œ์Šค ID ์ถ”์ถœ ์‹คํŒจ ({result.get('source')}): {e}")
587
-
588
- sources.append(source_info)
589
-
590
- # ์ตœ์ข… ์‘๋‹ต
591
- response_data = {
592
- "answer": answer,
593
- "sources": sources,
594
- "llm": llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {}
595
- }
596
- # logger.debug(f"์ตœ์ข… API ์‘๋‹ต: {response_data}") # ๋„ˆ๋ฌด ๊ธธ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ํ•„์š”ํ•œ ๊ฒฝ์šฐ์—๋งŒ ํ™œ์„ฑํ™”
597
- return jsonify(response_data)
598
-
599
- except Exception as e:
600
- logger.error(f"์ฑ„ํŒ… ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
601
- return jsonify({"error": f"์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"}), 500
602
-
603
-
604
- @app.route('/api/voice', methods=['POST'])
605
- @login_required
606
- def voice_chat():
607
- """์Œ์„ฑ ์ฑ— API ์—”๋“œํฌ์ธํŠธ"""
608
- global retriever, stt_client
609
-
610
-
611
- if not app_ready:
612
- logger.warning("์•ฑ ์ดˆ๊ธฐํ™”๊ฐ€ ์™„๋ฃŒ๋˜์ง€ ์•Š์•˜์ง€๋งŒ ์Œ์„ฑ API ์š”์ฒญ ์ฒ˜๋ฆฌ ์‹œ๋„")
613
- # ์—ฌ๊ธฐ์„œ ๋ฐ”๋กœ ๋ฆฌํ„ดํ•˜์ง€ ์•Š๊ณ  ๊ณ„์† ์ง„ํ–‰
614
- # ์‚ฌ์ „ ๊ฒ€์‚ฌ: retriever์™€ stt_client๊ฐ€ ์ œ๋Œ€๋กœ ์ดˆ๊ธฐํ™”๋˜์—ˆ๋Š”์ง€ ํ™•์ธ
615
-
616
- if retriever is None:
617
- logger.error("retriever๊ฐ€ ์•„์ง ์ดˆ๊ธฐํ™”๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค")
618
- return jsonify({
619
- "transcription": "(์Œ์„ฑ์„ ํ…์ŠคํŠธ๋กœ ๋ณ€ํ™˜ํ–ˆ์ง€๋งŒ ๊ฒ€์ƒ‰ ์—”์ง„์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค)",
620
- "answer": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ฒ€์ƒ‰ ์—”์ง„์ด ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.",
621
- "sources": []
622
- })
623
- # ๋˜๋Š” ํ•„์ˆ˜ ์ปดํฌ๋„ŒํŠธ๊ฐ€ ์—†์„ ๋•Œ๋งŒ ํŠน๋ณ„ ์‘๋‹ต ๋ฐ˜ํ™˜
624
- if stt_client is None:
625
- return jsonify({
626
- "transcription": "(์Œ์„ฑ ์ธ์‹ ๊ธฐ๋Šฅ์ด ์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค)",
627
- "answer": "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ํ˜„์žฌ ์Œ์„ฑ ์ธ์‹ ์„œ๋น„์Šค๊ฐ€ ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.",
628
- "sources": []
629
- })
630
-
631
- logger.info("์Œ์„ฑ ์ฑ— ์š”์ฒญ ์ˆ˜์‹ ")
632
-
633
- if 'audio' not in request.files:
634
- logger.error("์˜ค๋””์˜ค ํŒŒ์ผ์ด ์ œ๊ณต๋˜์ง€ ์•Š์Œ")
635
- return jsonify({"error": "์˜ค๋””์˜ค ํŒŒ์ผ์ด ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
636
-
637
- audio_file = request.files['audio']
638
- logger.info(f"์ˆ˜์‹ ๋œ ์˜ค๋””์˜ค ํŒŒ์ผ: {audio_file.filename} ({audio_file.content_type})")
639
-
640
- try:
641
- # ์˜ค๋””์˜ค ํŒŒ์ผ ์ฒ˜๋ฆฌ
642
- # ์ž„์‹œ ํŒŒ์ผ ์‚ฌ์šฉ ๊ณ ๋ ค (๋ฉ”๋ชจ๋ฆฌ ๋ถ€๋‹ด ์ค„์ด๊ธฐ ์œ„ํ•ด)
643
- with tempfile.NamedTemporaryFile(delete=True) as temp_audio:
644
- audio_file.save(temp_audio.name)
645
- logger.info(f"์˜ค๋””์˜ค ํŒŒ์ผ์„ ์ž„์‹œ ์ €์žฅ: {temp_audio.name}")
646
- # VitoSTT.transcribe_audio ๊ฐ€ ํŒŒ์ผ ๊ฒฝ๋กœ ๋˜๋Š” ๋ฐ”์ดํŠธ๋ฅผ ๋ฐ›์„ ์ˆ˜ ์žˆ๋„๋ก ๊ตฌํ˜„๋˜์–ด์•ผ ํ•จ
647
- # ์—ฌ๊ธฐ์„œ๋Š” ํŒŒ์ผ ๊ฒฝ๋กœ๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค๊ณ  ๊ฐ€์ •
648
- if not hasattr(stt_client, 'transcribe_audio'):
649
- raise NotImplementedError("STT ํด๋ผ์ด์–ธํŠธ์— transcribe_audio ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
650
-
651
- # ํŒŒ์ผ ๊ฒฝ๋กœ๋กœ ์ „๋‹ฌ ์‹œ
652
- # stt_result = stt_client.transcribe_audio(temp_audio.name, language="ko")
653
- # ๋ฐ”์ดํŠธ๋กœ ์ „๋‹ฌ ์‹œ
654
- with open(temp_audio.name, 'rb') as f_bytes:
655
- audio_bytes = f_bytes.read()
656
- stt_result = stt_client.transcribe_audio(audio_bytes, language="ko")
657
-
658
-
659
- if not isinstance(stt_result, dict) or not stt_result.get("success"):
660
- error_msg = stt_result.get("error", "์•Œ ์ˆ˜ ์—†๋Š” STT ์˜ค๋ฅ˜") if isinstance(stt_result, dict) else "STT ๊ฒฐ๊ณผ ํ˜•์‹ ์˜ค๋ฅ˜"
661
- logger.error(f"์Œ์„ฑ์ธ์‹ ์‹คํŒจ: {error_msg}")
662
- return jsonify({
663
- "error": "์Œ์„ฑ์ธ์‹ ์‹คํŒจ",
664
- "details": error_msg
665
- }), 500
666
-
667
- transcription = stt_result.get("text", "")
668
- if not transcription:
669
- logger.warning("์Œ์„ฑ์ธ์‹ ๊ฒฐ๊ณผ๊ฐ€ ๋น„์–ด์žˆ์Šต๋‹ˆ๋‹ค.")
670
- return jsonify({"error": "์Œ์„ฑ์—์„œ ํ…์ŠคํŠธ๋ฅผ ์ธ์‹ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.", "transcription": ""}), 400
671
-
672
- logger.info(f"์Œ์„ฑ์ธ์‹ ์„ฑ๊ณต: {transcription[:50]}...")
673
- if retriever is None:
674
- logger.error("STT ์„ฑ๊ณต ํ›„ ๊ฒ€์ƒ‰ ์‹œ๋„ ์ค‘ retriever๊ฐ€ None์ž„")
675
- return jsonify({
676
- "transcription": transcription,
677
- "answer": "์Œ์„ฑ์„ ์ธ์‹ํ–ˆ์ง€๋งŒ, ํ˜„์žฌ ๊ฒ€์ƒ‰ ์‹œ์Šคํ…œ์ด ์ค€๋น„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”.",
678
- "sources": []
679
- })
680
- # --- ์ดํ›„ ๋กœ์ง์€ /api/chat๊ณผ ๊ฑฐ์˜ ๋™์ผ ---
681
- # RAG ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰
682
- search_results = retriever.search(transcription, top_k=5, first_stage_k=6)
683
- context = DocumentProcessor.prepare_rag_context(search_results, field="text")
684
-
685
- if not context:
686
- logger.warning("์Œ์„ฑ ์ฟผ๋ฆฌ์— ๋Œ€ํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ์—†์Œ.")
687
- # answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค." (์•„๋ž˜ LLM ํ˜ธ์ถœ ๋กœ์ง์—์„œ ์ฒ˜๋ฆฌ)
688
- pass
689
-
690
- # LLM ํ˜ธ์ถœ
691
- llm_id = request.form.get('llm_id', None) # ์Œ์„ฑ ์š”์ฒญ์€ form ๋ฐ์ดํ„ฐ๋กœ LLM ID ๋ฐ›์„ ์ˆ˜ ์žˆ์Œ
692
- if not context:
693
- answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
694
- logger.info("์ปจํ…์ŠคํŠธ ์—†์ด ๊ธฐ๋ณธ ์‘๋‹ต ์ƒ์„ฑ")
695
- else:
696
- answer = llm_interface.rag_generate(transcription, context, llm_id=llm_id)
697
- logger.info(f"LLM ์‘๋‹ต ์ƒ์„ฑ ์™„๋ฃŒ (๊ธธ์ด: {len(answer)})")
698
-
699
-
700
- # ์†Œ์Šค ์ •๋ณด ์ถ”์ถœ
701
- enhanced_sources = []
702
- if search_results:
703
- for doc in search_results:
704
- if not isinstance(doc, dict): continue # ํ˜•์‹ ์ฒดํฌ
705
- if "source" in doc:
706
- source_info = {
707
- "source": doc.get("source", "Unknown"),
708
- "score": doc.get("rerank_score", doc.get("score", 0))
709
- }
710
- if "text" in doc and doc.get("filetype") == "csv":
711
- try:
712
- text_lines = doc["text"].strip().split('\n')
713
- if text_lines:
714
- first_line = text_lines[0].strip()
715
- if ',' in first_line:
716
- first_column = first_line.split(',')[0].strip()
717
- source_info["id"] = first_column
718
- except Exception as e:
719
- logger.warning(f"[์Œ์„ฑ์ฑ—] CSV ์†Œ์Šค ID ์ถ”์ถœ ์‹คํŒจ ({doc.get('source')}): {e}")
720
- enhanced_sources.append(source_info)
721
-
722
- # ์ตœ์ข… ์‘๋‹ต
723
- response_data = {
724
- "transcription": transcription,
725
- "answer": answer,
726
- "sources": enhanced_sources,
727
- "llm": llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {}
728
- }
729
- return jsonify(response_data)
730
-
731
- except Exception as e:
732
- logger.error(f"์Œ์„ฑ ์ฑ— ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
733
- return jsonify({
734
- "error": "์Œ์„ฑ ์ฒ˜๋ฆฌ ์ค‘ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ ๋ฐœ์ƒ",
735
- "details": str(e)
736
- }), 500
737
-
738
-
739
- @app.route('/api/upload', methods=['POST'])
740
- @login_required
741
- def upload_document():
742
- """์ง€์‹๋ฒ ์ด์Šค ๋ฌธ์„œ ์—…๋กœ๋“œ API"""
743
- global base_retriever, retriever
744
-
745
- if not app_ready or base_retriever is None:
746
- return jsonify({"error": "์•ฑ/๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค."}), 503
747
-
748
- if 'document' not in request.files:
749
- return jsonify({"error": "๋ฌธ์„œ ํŒŒ์ผ์ด ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
750
-
751
- doc_file = request.files['document']
752
- if doc_file.filename == '':
753
- return jsonify({"error": "์„ ํƒ๋œ ํŒŒ์ผ์ด ์—†์Šต๋‹ˆ๋‹ค."}), 400
754
-
755
- if not allowed_doc_file(doc_file.filename):
756
- logger.error(f"ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹: {doc_file.filename}")
757
- return jsonify({"error": f"ํ—ˆ์šฉ๋˜์ง€ ์•Š๋Š” ํŒŒ์ผ ํ˜•์‹์ž…๋‹ˆ๋‹ค. ํ—ˆ์šฉ: {', '.join(ALLOWED_DOC_EXTENSIONS)}"}), 400
758
-
759
- try:
760
- filename = secure_filename(doc_file.filename)
761
- filepath = os.path.join(app.config['DATA_FOLDER'], filename)
762
- doc_file.save(filepath)
763
- logger.info(f"๋ฌธ์„œ ์ €์žฅ ์™„๋ฃŒ: {filepath}")
764
-
765
- # ๋ฌธ์„œ ์ฒ˜๋ฆฌ (์ธ์ฝ”๋”ฉ ์ฒ˜๋ฆฌ ํฌํ•จ)
766
- try:
767
- with open(filepath, 'r', encoding='utf-8') as f:
768
- content = f.read()
769
- except UnicodeDecodeError:
770
- logger.info(f"UTF-8 ๋””์ฝ”๋”ฉ ์‹คํŒจ, CP949๋กœ ์‹œ๋„: {filename}")
771
- try:
772
- with open(filepath, 'r', encoding='cp949') as f:
773
- content = f.read()
774
- except Exception as e_cp949:
775
- logger.error(f"CP949 ๋””์ฝ”๋”ฉ ์‹คํŒจ ({filename}): {e_cp949}")
776
- return jsonify({"error": "ํŒŒ์ผ ์ธ์ฝ”๋”ฉ์„ ์ฝ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค (UTF-8, CP949 ์‹œ๋„ ์‹คํŒจ)."}), 400
777
- except Exception as e_read:
778
- logger.error(f"ํŒŒ์ผ ์ฝ๊ธฐ ์˜ค๋ฅ˜ ({filename}): {e_read}")
779
- return jsonify({"error": f"ํŒŒ์ผ ์ฝ๊ธฐ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e_read)}"}), 500
780
-
781
-
782
- # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ ๋ฐ ๋ฌธ์„œ ๋ถ„ํ• /์ฒ˜๋ฆฌ
783
- metadata = {
784
- "source": filename, "filename": filename,
785
- "filetype": filename.rsplit('.', 1)[1].lower(),
786
- "filepath": filepath
787
- }
788
- file_ext = metadata["filetype"]
789
- docs = []
790
-
791
- if not hasattr(DocumentProcessor, 'csv_to_documents') or not hasattr(DocumentProcessor, 'text_to_documents'):
792
- raise NotImplementedError("DocumentProcessor์— ํ•„์š”ํ•œ ๋ฉ”์†Œ๋“œ ์—†์Œ")
793
-
794
- if file_ext == 'csv':
795
- logger.info(f"CSV ํŒŒ์ผ ์ฒ˜๋ฆฌ ์‹œ์ž‘: {filename}")
796
- docs = DocumentProcessor.csv_to_documents(content, metadata) # ํ–‰ ๋‹จ์œ„ ์ฒ˜๋ฆฌ ๊ฐ€์ •
797
- else: # ๊ธฐํƒ€ ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ๋ฌธ์„œ
798
- logger.info(f"์ผ๋ฐ˜ ํ…์ŠคํŠธ ๋ฌธ์„œ ์ฒ˜๋ฆฌ ์‹œ์ž‘: {filename}")
799
- # PDF, DOCX ๋“ฑ์€ ๋ณ„๋„ ๋ผ์ด๋ธŒ๋Ÿฌ๋ฆฌ(pypdf, python-docx) ํ•„์š”
800
- if file_ext in ['pdf', 'docx']:
801
- logger.warning(f".{file_ext} ํŒŒ์ผ ์ฒ˜๋ฆฌ๋Š” ํ˜„์žฌ ๊ตฌํ˜„๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค. ํ…์ŠคํŠธ ์ถ”์ถœ ๋กœ์ง ์ถ”๊ฐ€ ํ•„์š”.")
802
- # ์—ฌ๊ธฐ์— pdf/docx ํ…์ŠคํŠธ ์ถ”์ถœ ๋กœ์ง ์ถ”๊ฐ€
803
- # ์˜ˆ: content = extract_text_from_pdf(filepath)
804
- # content = extract_text_from_docx(filepath)
805
- # ์ž„์‹œ๋กœ ๋น„์›Œ๋‘ 
806
- content = ""
807
-
808
- if content: # ํ…์ŠคํŠธ ๋‚ด์šฉ์ด ์žˆ์„ ๋•Œ๋งŒ ์ฒ˜๋ฆฌ
809
- docs = DocumentProcessor.text_to_documents(
810
- content, metadata=metadata,
811
- chunk_size=512, chunk_overlap=50
812
- )
813
-
814
- # ๊ฒ€์ƒ‰๊ธฐ์— ๋ฌธ์„œ ์ถ”๊ฐ€ ๋ฐ ์ธ๋ฑ์Šค ์ €์žฅ
815
- if docs:
816
- if not hasattr(base_retriever, 'add_documents') or not hasattr(base_retriever, 'save'):
817
- raise NotImplementedError("๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ์— add_documents ๋˜๋Š” save ๋ฉ”์†Œ๋“œ ์—†์Œ")
818
-
819
- logger.info(f"{len(docs)}๊ฐœ ๋ฌธ์„œ ์ฒญํฌ๋ฅผ ๊ฒ€์ƒ‰๊ธฐ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค...")
820
- base_retriever.add_documents(docs)
821
-
822
- # ์ธ๋ฑ์Šค ์ €์žฅ (์—…๋กœ๋“œ๋งˆ๋‹ค ์ €์žฅ - ๋น„ํšจ์œจ์ ์ผ ์ˆ˜ ์žˆ์Œ)
823
- logger.info(f"๊ฒ€์ƒ‰๊ธฐ ์ƒํƒœ๋ฅผ ์ €์žฅํ•ฉ๋‹ˆ๋‹ค...")
824
- index_path = app.config['INDEX_PATH']
825
- try:
826
- base_retriever.save(index_path)
827
- logger.info("์ธ๋ฑ์Šค ์ €์žฅ ์™„๋ฃŒ")
828
- # ์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ๋„ ์—…๋ฐ์ดํŠธ ํ•„์š” ์‹œ ๋กœ์ง ์ถ”๊ฐ€
829
- # ์˜ˆ: retriever.update_base_retriever(base_retriever)
830
- return jsonify({
831
- "success": True,
832
- "message": f"ํŒŒ์ผ '{filename}' ์—…๋กœ๋“œ ๋ฐ ์ฒ˜๋ฆฌ ์™„๋ฃŒ ({len(docs)}๊ฐœ ์ฒญํฌ ์ถ”๊ฐ€)."
833
- })
834
- except Exception as e_save:
835
- logger.error(f"์ธ๋ฑ์Šค ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e_save}")
836
- return jsonify({"error": f"์ธ๋ฑ์Šค ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜: {str(e_save)}"}), 500
837
- else:
838
- logger.warning(f"ํŒŒ์ผ '{filename}'์—์„œ ์ฒ˜๋ฆฌ๏ฟฝ๏ฟฝ๏ฟฝ ๋‚ด์šฉ์ด ์—†๊ฑฐ๋‚˜ ์ง€์›๋˜์ง€ ์•Š๋Š” ํ˜•์‹์ž…๋‹ˆ๋‹ค.")
839
- # ํŒŒ์ผ์€ ์ €์žฅ๋˜์—ˆ์œผ๋ฏ€๋กœ ์„ฑ๊ณต์œผ๋กœ ๊ฐ„์ฃผํ• ์ง€ ๊ฒฐ์ • ํ•„์š”
840
- return jsonify({
841
- "warning": True,
842
- "message": f"ํŒŒ์ผ '{filename}'์ด ์ €์žฅ๋˜์—ˆ์ง€๋งŒ ์ฒ˜๋ฆฌํ•  ๋‚ด์šฉ์ด ์—†์Šต๋‹ˆ๋‹ค."
843
- })
844
-
845
- except Exception as e:
846
- logger.error(f"ํŒŒ์ผ ์—…๋กœ๋“œ ๋˜๋Š” ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
847
- return jsonify({"error": f"ํŒŒ์ผ ์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜: {str(e)}"}), 500
848
-
849
-
850
- @app.route('/api/documents', methods=['GET'])
851
- @login_required
852
- def list_documents():
853
- """์ง€์‹๋ฒ ์ด์Šค ๋ฌธ์„œ ๋ชฉ๋ก API"""
854
- global base_retriever
855
-
856
- if not app_ready or base_retriever is None:
857
- return jsonify({"error": "์•ฑ/๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค."}), 503
858
-
859
- try:
860
- sources = {}
861
- total_chunks = 0
862
- # base_retriever.documents ์™€ ๊ฐ™์€ ์†์„ฑ์ด ์‹ค์ œ ํด๋ž˜์Šค์— ์žˆ๋‹ค๊ณ  ๊ฐ€์ •
863
- if hasattr(base_retriever, 'documents') and base_retriever.documents:
864
- logger.info(f"์ด {len(base_retriever.documents)}๊ฐœ ๋ฌธ์„œ ์ฒญํฌ์—์„œ ์†Œ์Šค ๋ชฉ๋ก ์ƒ์„ฑ ์ค‘...")
865
- for doc in base_retriever.documents:
866
- # ๋ฌธ์„œ ์ฒญํฌ๊ฐ€ ๋”•์…”๋„ˆ๋ฆฌ ํ˜•ํƒœ๋ผ๊ณ  ๊ฐ€์ •
867
- if not isinstance(doc, dict): continue
868
-
869
- source = doc.get("source", "unknown") # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์—์„œ source ๊ฐ€์ ธ์˜ค๊ธฐ
870
- if source == "unknown" and "metadata" in doc and isinstance(doc["metadata"], dict):
871
- source = doc["metadata"].get("source", "unknown") # Langchain Document ๊ตฌ์กฐ ๊ณ ๋ ค
872
-
873
- if source != "unknown":
874
- if source in sources:
875
- sources[source]["chunks"] += 1
876
- else:
877
- # ๋ฉ”ํƒ€๋ฐ์ดํ„ฐ์—์„œ ์ถ”๊ฐ€ ์ •๋ณด ๊ฐ€์ ธ์˜ค๊ธฐ
878
- filename = doc.get("filename", source)
879
- filetype = doc.get("filetype", "unknown")
880
- if "metadata" in doc and isinstance(doc["metadata"], dict):
881
- filename = doc["metadata"].get("filename", filename)
882
- filetype = doc["metadata"].get("filetype", filetype)
883
-
884
- sources[source] = {
885
- "filename": filename,
886
- "chunks": 1,
887
- "filetype": filetype
888
- }
889
- total_chunks += 1
890
- else:
891
- logger.info("๊ฒ€์ƒ‰๊ธฐ์— ๋ฌธ์„œ๊ฐ€ ์—†๊ฑฐ๋‚˜ documents ์†์„ฑ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
892
-
893
- # ๋ชฉ๋ก ํ˜•์‹ ๋ณ€ํ™˜ ๋ฐ ์ •๋ ฌ
894
- documents = [{"source": src, **info} for src, info in sources.items()]
895
- documents.sort(key=lambda x: x["chunks"], reverse=True)
896
-
897
- logger.info(f"๋ฌธ์„œ ๋ชฉ๋ก ์กฐํšŒ ์™„๋ฃŒ: {len(documents)}๊ฐœ ์†Œ์Šค ํŒŒ์ผ, {total_chunks}๊ฐœ ์ฒญํฌ")
898
- return jsonify({
899
- "documents": documents,
900
- "total_documents": len(documents),
901
- "total_chunks": total_chunks # sum(doc["chunks"] for doc in documents) ์™€ ๋™์ผ
902
- })
903
-
904
- except Exception as e:
905
- logger.error(f"๋ฌธ์„œ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
906
- return jsonify({"error": f"๋ฌธ์„œ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜: {str(e)}"}), 500
907
-
908
-
909
- # ์ •์  ํŒŒ์ผ ์„œ๋น™
910
- @app.route('/static/<path:path>')
911
- def send_static(path):
912
- return send_from_directory('static', path)
913
-
914
-
915
- # --- ์š”์ฒญ ์ฒ˜๋ฆฌ ํ›… ---
916
-
917
- # @app.before_request - ์ œ๊ฑฐ๋จ (์ˆ˜๋™ ์ฟ ํ‚ค ์ฒ˜๋ฆฌ ๋กœ์ง ์‚ญ์ œ)
918
- # def process_cookies(): ...
919
-
920
- @app.after_request
921
- def after_request_func(response):
922
- """๋ชจ๋“  ์‘๋‹ต์— ๋Œ€ํ•ด ํ›„์ฒ˜๋ฆฌ ์ˆ˜ํ–‰"""
923
- # ์„ธ์…˜์ด ์ˆ˜์ •๋˜์—ˆ๋Š”์ง€ ํ™•์ธ ํ›„ ๋กœ๊น… (๋””๋ฒ„๊น…์šฉ)
924
- # if session.modified: # session.modified ๋Š” ํ•ญ์ƒ ์ •ํ™•ํ•˜์ง€ ์•Š์„ ์ˆ˜ ์žˆ์Œ
925
- # logger.debug(f"[After Request] ์„ธ์…˜ ์ˆ˜์ • ๊ฐ์ง€๋จ. ์‘๋‹ต์— Set-Cookie ํฌํ•จ ์—ฌ๋ถ€ ํ™•์ธ ํ•„์š”.")
926
- # logger.debug(f"[After Request] ์‘๋‹ต ํ—ค๋”: {response.headers}") # ๋””๋ฒ„๊น… ์‹œ Set-Cookie ํ™•์ธ
927
- return response # ์‘๋‹ต ๊ฐ์ฒด๋ฅผ ๋ฐ˜๋“œ์‹œ ๋ฐ˜ํ™˜ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
928
-
929
- # ์•ฑ ์‹คํ–‰ (๋กœ์ปฌ ํ…Œ์ŠคํŠธ์šฉ)
930
- if __name__ == '__main__':
931
- logger.info("Flask ์•ฑ์„ ์ง์ ‘ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค (๊ฐœ๋ฐœ์šฉ ์„œ๋ฒ„).")
932
- # ๋””๋ฒ„๊ทธ ๋ชจ๋“œ๋Š” ์‹ค์ œ ๋ฐฐํฌ ์‹œ False๋กœ ์„ค์ •ํ•ด์•ผ ํ•ฉ๋‹ˆ๋‹ค.
933
- # port ๋ฒˆํ˜ธ๋Š” ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋˜๋Š” ๊ธฐ๋ณธ๊ฐ’์„ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.
934
- port = int(os.environ.get("PORT", 7860))
935
- logger.info(f"์„œ๋ฒ„๋ฅผ http://0.0.0.0:{port} ์—์„œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.")
936
- app.run(debug=True, host='0.0.0.0', port=port)
 
1
  """
2
+ RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ (์žฅ์น˜ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ํ†ตํ•ฉ)
3
  """
4
 
5
  import os
 
7
  import logging
8
  import tempfile
9
  import threading
10
+ import requests
11
+ from datetime import datetime, timedelta
12
  from flask import Flask, request, jsonify, render_template, send_from_directory, session, redirect, url_for
13
  from werkzeug.utils import secure_filename
14
  from dotenv import load_dotenv
15
  from functools import wraps
16
+ import pickle
17
+ import gzip
18
 
19
  # ๋กœ๊ฑฐ ์„ค์ •
20
  logging.basicConfig(
21
  format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
22
+ level=logging.DEBUG
23
  )
24
  logger = logging.getLogger(__name__)
25
 
 
29
  # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ ์ƒํƒœ ํ™•์ธ ๋ฐ ๋กœ๊น…
30
  ADMIN_USERNAME = os.getenv('ADMIN_USERNAME')
31
  ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD')
32
+ DEVICE_SERVER_URL = os.getenv('DEVICE_SERVER_URL', 'http://localhost:5050')
33
 
34
  logger.info(f"==== ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ ์ƒํƒœ ====")
35
  logger.info(f"ADMIN_USERNAME ์„ค์ • ์—ฌ๋ถ€: {ADMIN_USERNAME is not None}")
36
  # ๋น„๋ฐ€๋ฒˆํ˜ธ๋Š” ๋กœ๋“œ ์—ฌ๋ถ€๋งŒ ๊ธฐ๋ก (๋ณด์•ˆ)
37
  logger.info(f"ADMIN_PASSWORD ์„ค์ • ์—ฌ๋ถ€: {ADMIN_PASSWORD is not None}")
38
+ logger.info(f"DEVICE_SERVER_URL: {DEVICE_SERVER_URL}")
39
 
40
  # ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์„ค์ • (๊ฐœ๋ฐœ์šฉ, ๋ฐฐํฌ ์‹œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ์„ค์ • ๊ถŒ์žฅ)
41
  if not ADMIN_USERNAME:
 
45
  if not ADMIN_PASSWORD:
46
  ADMIN_PASSWORD = 'rag12345'
47
  logger.warning("ADMIN_PASSWORD ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์—†์–ด ๊ธฐ๋ณธ๊ฐ’ 'rag12345'๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.")
48
+
49
  class MockComponent: pass
50
 
51
  # --- ๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ---
 
52
  try:
53
  from utils.vito_stt import VitoSTT
54
  from utils.llm_interface import LLMInterface
 
57
  from retrieval.reranker import ReRanker
58
  except ImportError as e:
59
  logger.error(f"๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ์‹คํŒจ: {e}. utils ๋ฐ retrieval ํŒจํ‚ค์ง€๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ๊ฒฝ๋กœ์— ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.")
 
 
60
  VitoSTT = LLMInterface = DocumentProcessor = VectorRetriever = ReRanker = MockComponent
61
  # --- ๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ๋ ---
62
 
 
65
  app = Flask(__name__)
66
 
67
  # ์„ธ์…˜ ์„ค์ • - ๊ณ ์ •๋œ ์‹œํฌ๋ฆฟ ํ‚ค ์‚ฌ์šฉ (์‹ค์ œ ๋ฐฐํฌ ์‹œ ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋“ฑ์œผ๋กœ ๊ด€๋ฆฌ ๊ถŒ์žฅ)
68
+ app.secret_key = os.getenv('FLASK_SECRET_KEY', 'rag_chatbot_fixed_secret_key_12345')
69
 
70
+ # --- ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ • ---
 
71
  app.config['SESSION_COOKIE_SECURE'] = True
72
+ app.config['SESSION_COOKIE_HTTPONLY'] = True
73
+ app.config['SESSION_COOKIE_SAMESITE'] = 'None'
74
+ app.config['SESSION_COOKIE_DOMAIN'] = None
75
+ app.config['SESSION_COOKIE_PATH'] = '/'
76
+ app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=1)
 
 
 
 
77
  # --- ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ • ๋ ---
78
 
79
  # ์ตœ๋Œ€ ํŒŒ์ผ ํฌ๊ธฐ ์„ค์ • (10MB)
 
108
  # --- ์ „์—ญ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ๋ ---
109
 
110
 
111
+ # --- ์ธ์ฆ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ---
112
  def login_required(f):
113
  @wraps(f)
114
  def decorated_function(*args, **kwargs):
115
  logger.info(f"----------- ์ธ์ฆ ํ•„์š” ํŽ˜์ด์ง€ ์ ‘๊ทผ ์‹œ๋„: {request.path} -----------")
116
  logger.info(f"ํ˜„์žฌ ํ”Œ๋ผ์Šคํฌ ์„ธ์…˜ ๊ฐ์ฒด: {session}")
117
  logger.info(f"ํ˜„์žฌ ์„ธ์…˜ ์ƒํƒœ: logged_in={session.get('logged_in', False)}, username={session.get('username', 'None')}")
 
118
  logger.info(f"์š”์ฒญ์˜ ์„ธ์…˜ ์ฟ ํ‚ค ๊ฐ’: {request.cookies.get('session', 'None')}")
119
 
120
  # Flask ์„ธ์…˜์— 'logged_in' ํ‚ค๊ฐ€ ์žˆ๋Š”์ง€ ์ง์ ‘ ํ™•์ธ
121
  if 'logged_in' not in session:
122
  logger.warning(f"ํ”Œ๋ผ์Šคํฌ ์„ธ์…˜์— 'logged_in' ์—†์Œ. ๋กœ๊ทธ์ธ ํŽ˜๏ฟฝ๏ฟฝ๏ฟฝ์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜.")
123
+ return redirect(url_for('login', next=request.url))
 
124
 
125
  logger.info(f"์ธ์ฆ ์„ฑ๊ณต: {session.get('username', 'unknown')} ์‚ฌ์šฉ์ž๊ฐ€ {request.path} ์ ‘๊ทผ")
126
  return f(*args, **kwargs)
127
  return decorated_function
128
  # --- ์ธ์ฆ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ๋ ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/app_device_routes.py ADDED
@@ -0,0 +1,201 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ - ์žฅ์น˜ ๊ด€๋ฆฌ API ๋ผ์šฐํŠธ ์ •์˜
3
+ """
4
+
5
+ import logging
6
+ import requests
7
+ from flask import request, jsonify
8
+
9
+ # ๋กœ๊ฑฐ ๊ฐ€์ ธ์˜ค๊ธฐ
10
+ logger = logging.getLogger(__name__)
11
+
12
+ def register_device_routes(app, login_required, DEVICE_SERVER_URL):
13
+ """Flask ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ์žฅ์น˜ ๊ด€๋ฆฌ ๊ด€๋ จ ๋ผ์šฐํŠธ ๋“ฑ๋ก"""
14
+
15
+ @app.route('/api/device/status', methods=['GET'])
16
+ @login_required
17
+ def device_status():
18
+ """์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ API"""
19
+ logger.info("์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ ์š”์ฒญ")
20
+
21
+ try:
22
+ # ์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ
23
+ response = requests.get(f"{DEVICE_SERVER_URL}/api/status", timeout=5)
24
+
25
+ if response.status_code == 200:
26
+ data = response.json()
27
+ logger.info(f"์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์ƒํƒœ: {data.get('status', 'unknown')}")
28
+ return jsonify({
29
+ "success": True,
30
+ "server_status": data.get("status", "unknown")
31
+ })
32
+ else:
33
+ logger.warning(f"์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์‘๋‹ต ์ฝ”๋“œ: {response.status_code}")
34
+ return jsonify({
35
+ "success": False,
36
+ "error": f"์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„๊ฐ€ ๋น„์ •์ƒ ์‘๋‹ต ์ฝ”๋“œ๋ฅผ ๋ฐ˜ํ™˜ํ–ˆ์Šต๋‹ˆ๋‹ค: {response.status_code}"
37
+ }), 502
38
+
39
+ except requests.exceptions.Timeout:
40
+ logger.error("์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹œ๊ฐ„ ์ดˆ๊ณผ")
41
+ return jsonify({
42
+ "success": False,
43
+ "error": "์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."
44
+ }), 504
45
+
46
+ except requests.exceptions.ConnectionError:
47
+ logger.error("์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ")
48
+ return jsonify({
49
+ "success": False,
50
+ "error": "์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”."
51
+ }), 503
52
+
53
+ except Exception as e:
54
+ logger.error(f"์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
55
+ return jsonify({
56
+ "success": False,
57
+ "error": f"์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"
58
+ }), 500
59
+
60
+
61
+ @app.route('/api/device/list', methods=['GET'])
62
+ @login_required
63
+ def device_list():
64
+ """์žฅ์น˜ ๋ชฉ๋ก ์กฐํšŒ API"""
65
+ logger.info("์žฅ์น˜ ๋ชฉ๋ก ์กฐํšŒ ์š”์ฒญ")
66
+
67
+ try:
68
+ # ์žฅ์น˜ ๋ชฉ๋ก ์กฐํšŒ
69
+ response = requests.get(f"{DEVICE_SERVER_URL}/api/devices", timeout=5)
70
+
71
+ if response.status_code == 200:
72
+ data = response.json()
73
+ devices = data.get("devices", [])
74
+ logger.info(f"์žฅ์น˜ ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต: {len(devices)}๊ฐœ ์žฅ์น˜")
75
+ return jsonify({
76
+ "success": True,
77
+ "devices": devices
78
+ })
79
+ else:
80
+ logger.warning(f"์žฅ์น˜ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ: {response.status_code}")
81
+ return jsonify({
82
+ "success": False,
83
+ "error": f"์žฅ์น˜ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ: {response.status_code}"
84
+ }), 502
85
+
86
+ except requests.exceptions.Timeout:
87
+ logger.error("์žฅ์น˜ ๋ชฉ๋ก ์กฐํšŒ ์‹œ๊ฐ„ ์ดˆ๊ณผ")
88
+ return jsonify({
89
+ "success": False,
90
+ "error": "์žฅ์น˜ ๋ชฉ๋ก ์กฐํšŒ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."
91
+ }), 504
92
+
93
+ except requests.exceptions.ConnectionError:
94
+ logger.error("์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ")
95
+ return jsonify({
96
+ "success": False,
97
+ "error": "์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”."
98
+ }), 503
99
+
100
+ except Exception as e:
101
+ logger.error(f"์žฅ์น˜ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
102
+ return jsonify({
103
+ "success": False,
104
+ "error": f"์žฅ์น˜ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"
105
+ }), 500
106
+
107
+
108
+ @app.route('/api/device/programs', methods=['GET'])
109
+ @login_required
110
+ def device_programs():
111
+ """์‹คํ–‰ ๊ฐ€๋Šฅํ•œ ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ API"""
112
+ logger.info("ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์š”์ฒญ")
113
+
114
+ try:
115
+ # ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ
116
+ response = requests.get(f"{DEVICE_SERVER_URL}/api/programs", timeout=5)
117
+
118
+ if response.status_code == 200:
119
+ data = response.json()
120
+ programs = data.get("programs", [])
121
+ logger.info(f"ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์„ฑ๊ณต: {len(programs)}๊ฐœ ํ”„๋กœ๊ทธ๋žจ")
122
+ return jsonify({
123
+ "success": True,
124
+ "programs": programs
125
+ })
126
+ else:
127
+ logger.warning(f"ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ: {response.status_code}")
128
+ return jsonify({
129
+ "success": False,
130
+ "error": f"ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์‹คํŒจ: {response.status_code}"
131
+ }), 502
132
+
133
+ except requests.exceptions.Timeout:
134
+ logger.error("ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์‹œ๊ฐ„ ์ดˆ๊ณผ")
135
+ return jsonify({
136
+ "success": False,
137
+ "error": "ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."
138
+ }), 504
139
+
140
+ except requests.exceptions.ConnectionError:
141
+ logger.error("์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ")
142
+ return jsonify({
143
+ "success": False,
144
+ "error": "์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”."
145
+ }), 503
146
+
147
+ except Exception as e:
148
+ logger.error(f"ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
149
+ return jsonify({
150
+ "success": False,
151
+ "error": f"ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"
152
+ }), 500
153
+
154
+
155
+ @app.route('/api/device/programs/<program_id>/execute', methods=['POST'])
156
+ @login_required
157
+ def execute_program(program_id):
158
+ """ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ API"""
159
+ logger.info(f"ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์š”์ฒญ: {program_id}")
160
+
161
+ try:
162
+ # ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰
163
+ response = requests.post(
164
+ f"{DEVICE_SERVER_URL}/api/programs/{program_id}/execute",
165
+ json={},
166
+ timeout=10 # ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰์—๋Š” ๋” ๊ธด ์‹œ๊ฐ„ ๋ถ€์—ฌ
167
+ )
168
+
169
+ if response.status_code == 200:
170
+ data = response.json()
171
+ success = data.get("success", False)
172
+ message = data.get("message", "")
173
+ logger.info(f"ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์‘๋‹ต: {success}, {message}")
174
+ return jsonify(data)
175
+ else:
176
+ logger.warning(f"ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์‹คํŒจ: {response.status_code}")
177
+ return jsonify({
178
+ "success": False,
179
+ "error": f"ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์š”์ฒญ ์‹คํŒจ: {response.status_code}"
180
+ }), 502
181
+
182
+ except requests.exceptions.Timeout:
183
+ logger.error("ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์š”์ฒญ ์‹œ๊ฐ„ ์ดˆ๊ณผ")
184
+ return jsonify({
185
+ "success": False,
186
+ "error": "ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์š”์ฒญ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค."
187
+ }), 504
188
+
189
+ except requests.exceptions.ConnectionError:
190
+ logger.error("์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์—ฐ๊ฒฐ ์‹คํŒจ")
191
+ return jsonify({
192
+ "success": False,
193
+ "error": "์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”."
194
+ }), 503
195
+
196
+ except Exception as e:
197
+ logger.error(f"ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
198
+ return jsonify({
199
+ "success": False,
200
+ "error": f"ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"
201
+ }), 500
app/app_part2.py ADDED
@@ -0,0 +1,209 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- ์ž„๋ฒ ๋”ฉ ๊ด€๋ จ ํ—ฌํผ ํ•จ์ˆ˜ ---
2
+ def save_embeddings(base_retriever, file_path):
3
+ """์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ์••์ถ•ํ•˜์—ฌ ํŒŒ์ผ์— ์ €์žฅ"""
4
+ try:
5
+ # ์ €์žฅ ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ์—†์œผ๋ฉด ์ƒ์„ฑ
6
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
7
+
8
+ # ํƒ€์ž„์Šคํƒฌํ”„ ์ถ”๊ฐ€
9
+ save_data = {
10
+ 'timestamp': datetime.now().isoformat(),
11
+ 'retriever': base_retriever
12
+ }
13
+
14
+ # ์••์ถ•ํ•˜์—ฌ ์ €์žฅ (์šฉ๋Ÿ‰ ์ค„์ด๊ธฐ)
15
+ with gzip.open(file_path, 'wb') as f:
16
+ pickle.dump(save_data, f)
17
+
18
+ logger.info(f"์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ {file_path}์— ์••์ถ•ํ•˜์—ฌ ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.")
19
+ return True
20
+ except Exception as e:
21
+ logger.error(f"์ž„๋ฒ ๋”ฉ ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
22
+ return False
23
+
24
+ def load_embeddings(file_path, max_age_days=30):
25
+ """์ €์žฅ๋œ ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ํŒŒ์ผ์—์„œ ๋กœ๋“œ"""
26
+ try:
27
+ if not os.path.exists(file_path):
28
+ logger.info(f"์ €์žฅ๋œ ์ž„๋ฒ ๋”ฉ ํŒŒ์ผ({file_path})์ด ์—†์Šต๋‹ˆ๋‹ค.")
29
+ return None
30
+
31
+ # ์••์ถ• ํŒŒ์ผ ๋กœ๋“œ
32
+ with gzip.open(file_path, 'rb') as f:
33
+ data = pickle.load(f)
34
+
35
+ # ํƒ€์ž„์Šคํƒฌํ”„ ํ™•์ธ (๋„ˆ๋ฌด ์˜ค๋ž˜๋œ ๋ฐ์ดํ„ฐ๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ)
36
+ saved_time = datetime.fromisoformat(data['timestamp'])
37
+ age = (datetime.now() - saved_time).days
38
+
39
+ if age > max_age_days:
40
+ logger.info(f"์ €์žฅ๋œ ์ž„๋ฒ ๋”ฉ์ด {age}์ผ๋กœ ๋„ˆ๋ฌด ์˜ค๋ž˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ƒˆ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.")
41
+ return None
42
+
43
+ logger.info(f"{file_path}์—์„œ ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ–ˆ์Šต๋‹ˆ๋‹ค. (์ƒ์„ฑ์ผ: {saved_time})")
44
+ return data['retriever']
45
+ except Exception as e:
46
+ logger.error(f"์ž„๋ฒ ๋”ฉ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
47
+ return None
48
+
49
+ def init_retriever():
50
+ """๊ฒ€์ƒ‰๊ธฐ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ๋˜๋Š” ๋กœ๋“œ"""
51
+ global base_retriever, retriever
52
+
53
+ # ์ž„๋ฒ ๋”ฉ ์บ์‹œ ํŒŒ์ผ ๊ฒฝ๋กœ
54
+ cache_path = os.path.join(app.config['INDEX_PATH'], "cached_embeddings.gz")
55
+
56
+ # ๋จผ์ € ์ €์žฅ๋œ ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹œ๋„
57
+ cached_retriever = load_embeddings(cache_path)
58
+
59
+ if cached_retriever:
60
+ logger.info("์บ์‹œ๋œ ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ๋กœ๋“œํ–ˆ์Šต๋‹ˆ๋‹ค.")
61
+ base_retriever = cached_retriever
62
+ else:
63
+ # ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ์กด ๋ฐฉ์‹์œผ๋กœ ์ดˆ๊ธฐํ™”
64
+ index_path = app.config['INDEX_PATH']
65
+
66
+ # VectorRetriever ๋กœ๋“œ ๋˜๋Š” ์ดˆ๊ธฐํ™”
67
+ if os.path.exists(os.path.join(index_path, "documents.json")):
68
+ try:
69
+ logger.info(f"๊ธฐ์กด ๋ฒกํ„ฐ ์ธ๋ฑ์Šค๋ฅผ '{index_path}'์—์„œ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค...")
70
+ base_retriever = VectorRetriever.load(index_path)
71
+ logger.info(f"{len(base_retriever.documents) if hasattr(base_retriever, 'documents') else 0}๊ฐœ ๋ฌธ์„œ๊ฐ€ ๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
72
+ except Exception as e:
73
+ logger.error(f"์ธ๋ฑ์Šค ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}. ์ƒˆ ๊ฒ€์ƒ‰๊ธฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.")
74
+ base_retriever = VectorRetriever()
75
+ else:
76
+ logger.info("๊ธฐ์กด ์ธ๋ฑ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์–ด ์ƒˆ ๊ฒ€์ƒ‰๊ธฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค...")
77
+ base_retriever = VectorRetriever()
78
+
79
+ # ๋ฐ์ดํ„ฐ ํด๋”์˜ ๋ฌธ์„œ ๋กœ๋“œ
80
+ data_path = app.config['DATA_FOLDER']
81
+ if (not hasattr(base_retriever, 'documents') or not base_retriever.documents) and os.path.exists(data_path):
82
+ logger.info(f"{data_path}์—์„œ ๋ฌธ์„œ๋ฅผ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค...")
83
+ try:
84
+ docs = DocumentProcessor.load_documents_from_directory(
85
+ data_path,
86
+ extensions=[".txt", ".md", ".csv"],
87
+ recursive=True
88
+ )
89
+ if docs and hasattr(base_retriever, 'add_documents'):
90
+ logger.info(f"{len(docs)}๊ฐœ ๋ฌธ์„œ๋ฅผ ๊ฒ€์ƒ‰๊ธฐ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค...")
91
+ base_retriever.add_documents(docs)
92
+
93
+ if hasattr(base_retriever, 'save'):
94
+ logger.info(f"๊ฒ€์ƒ‰๊ธฐ ์ƒํƒœ๋ฅผ '{index_path}'์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค...")
95
+ try:
96
+ base_retriever.save(index_path)
97
+ logger.info("์ธ๋ฑ์Šค ์ €์žฅ ์™„๋ฃŒ")
98
+
99
+ # ์ƒˆ๋กœ ์ƒ์„ฑ๋œ ๊ฒ€์ƒ‰๊ธฐ ์บ์‹ฑ
100
+ if hasattr(base_retriever, 'documents') and base_retriever.documents:
101
+ save_embeddings(base_retriever, cache_path)
102
+ logger.info(f"๊ฒ€์ƒ‰๊ธฐ๋ฅผ ์บ์‹œ ํŒŒ์ผ {cache_path}์— ์ €์žฅ ์™„๋ฃŒ")
103
+ except Exception as e:
104
+ logger.error(f"์ธ๋ฑ์Šค ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
105
+ except Exception as e:
106
+ logger.error(f"DATA_FOLDER์—์„œ ๋ฌธ์„œ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜: {e}")
107
+
108
+ # ์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”
109
+ logger.info("์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค...")
110
+ try:
111
+ # ์ž์ฒด ๊ตฌํ˜„๋œ ์žฌ์ˆœ์œ„ํ™” ํ•จ์ˆ˜
112
+ def custom_rerank_fn(query, results):
113
+ query_terms = set(query.lower().split())
114
+ for result in results:
115
+ if isinstance(result, dict) and "text" in result:
116
+ text = result["text"].lower()
117
+ term_freq = sum(1 for term in query_terms if term in text)
118
+ normalized_score = term_freq / (len(text.split()) + 1) * 10
119
+ result["rerank_score"] = result.get("score", 0) * 0.7 + normalized_score * 0.3
120
+ elif isinstance(result, dict):
121
+ result["rerank_score"] = result.get("score", 0)
122
+ results.sort(key=lambda x: x.get("rerank_score", 0) if isinstance(x, dict) else 0, reverse=True)
123
+ return results
124
+
125
+ # ReRanker ํด๋ž˜์Šค ์‚ฌ์šฉ
126
+ retriever = ReRanker(
127
+ base_retriever=base_retriever,
128
+ rerank_fn=custom_rerank_fn,
129
+ rerank_field="text"
130
+ )
131
+ logger.info("์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ")
132
+ except Exception as e:
133
+ logger.error(f"์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์‹คํŒจ: {e}")
134
+ retriever = base_retriever # ์‹คํŒจ ์‹œ ๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ ์‚ฌ์šฉ
135
+
136
+ return retriever
137
+
138
+ def background_init():
139
+ """๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์ˆ˜ํ–‰"""
140
+ global app_ready, retriever, base_retriever
141
+
142
+ # ์ฆ‰์‹œ ์•ฑ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์ƒํƒœ๋กœ ์„ค์ •
143
+ app_ready = True
144
+ logger.info("์•ฑ์„ ์ฆ‰์‹œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์ƒํƒœ๋กœ ์„ค์ • (app_ready=True)")
145
+
146
+ try:
147
+ # ๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” (๋ณดํ—˜)
148
+ if base_retriever is None:
149
+ base_retriever = MockComponent()
150
+ if hasattr(base_retriever, 'documents'):
151
+ base_retriever.documents = []
152
+
153
+ # ์ž„์‹œ retriever ์„ค์ •
154
+ if retriever is None:
155
+ retriever = MockComponent()
156
+ if not hasattr(retriever, 'search'):
157
+ retriever.search = lambda query, **kwargs: []
158
+
159
+ # ์บ์‹œ๋œ ์ž„๋ฒ ๋”ฉ ๋กœ๋“œ ์‹œ๋„
160
+ cache_path = os.path.join(app.config['INDEX_PATH'], "cached_embeddings.gz")
161
+ cached_retriever = load_embeddings(cache_path)
162
+
163
+ if cached_retriever:
164
+ # ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐ”๋กœ ์‚ฌ์šฉ
165
+ base_retriever = cached_retriever
166
+
167
+ # ๊ฐ„๋‹จํ•œ ์žฌ์ˆœ์œ„ํ™” ํ•จ์ˆ˜
168
+ def simple_rerank(query, results):
169
+ if results:
170
+ for result in results:
171
+ if isinstance(result, dict):
172
+ result["rerank_score"] = result.get("score", 0)
173
+ results.sort(key=lambda x: x.get("rerank_score", 0) if isinstance(x, dict) else 0, reverse=True)
174
+ return results
175
+
176
+ # ์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”
177
+ retriever = ReRanker(
178
+ base_retriever=base_retriever,
179
+ rerank_fn=simple_rerank,
180
+ rerank_field="text"
181
+ )
182
+
183
+ logger.info("์บ์‹œ๋œ ์ž„๋ฒ ๋”ฉ์œผ๋กœ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ (๋น ๋ฅธ ์‹œ์ž‘)")
184
+ else:
185
+ # ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ์ „์ฒด ์ดˆ๊ธฐํ™” ์ง„ํ–‰
186
+ logger.info("์บ์‹œ๋œ ์ž„๋ฒ ๋”ฉ์ด ์—†์–ด ์ „์ฒด ์ดˆ๊ธฐํ™” ์‹œ์ž‘")
187
+ retriever = init_retriever()
188
+ logger.info("์ „์ฒด ์ดˆ๊ธฐํ™” ์™„๋ฃŒ")
189
+
190
+ logger.info("์•ฑ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ (๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ ์ค€๋น„๋จ)")
191
+ except Exception as e:
192
+ logger.error(f"์•ฑ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ์ค‘ ์‹ฌ๊ฐํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
193
+ # ์ดˆ๊ธฐํ™” ์‹คํŒจ ์‹œ ๊ธฐ๋ณธ ๊ฐ์ฒด ์ƒ์„ฑ
194
+ if base_retriever is None:
195
+ base_retriever = MockComponent()
196
+ if hasattr(base_retriever, 'documents'):
197
+ base_retriever.documents = []
198
+ if retriever is None:
199
+ retriever = MockComponent()
200
+ if not hasattr(retriever, 'search'):
201
+ retriever.search = lambda query, **kwargs: []
202
+
203
+ logger.warning("์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ์žˆ์ง€๋งŒ ์•ฑ์€ ๊ณ„์† ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.")
204
+
205
+ # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์Šค๋ ˆ๋“œ ์‹œ์ž‘
206
+ init_thread = threading.Thread(target=background_init)
207
+ init_thread.daemon = True
208
+ init_thread.start()
209
+
app/app_part3.py ADDED
@@ -0,0 +1,163 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # --- Flask ๋ผ์šฐํŠธ ์ •์˜ ---
2
+
3
+ @app.route('/login', methods=['GET', 'POST'])
4
+ def login():
5
+ error = None
6
+ next_url = request.args.get('next')
7
+ logger.info(f"-------------- ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ์ ‘์† (Next: {next_url}) --------------")
8
+ logger.info(f"Method: {request.method}")
9
+
10
+ if request.method == 'POST':
11
+ logger.info("๋กœ๊ทธ์ธ ์‹œ๋„ ๋ฐ›์Œ")
12
+ username = request.form.get('username', '')
13
+ password = request.form.get('password', '')
14
+ logger.info(f"์ž…๋ ฅ๋œ ์‚ฌ์šฉ์ž๋ช…: {username}")
15
+ logger.info(f"๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ์—ฌ๋ถ€: {len(password) > 0}")
16
+
17
+ # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋˜๋Š” ๊ธฐ๋ณธ๊ฐ’๊ณผ ๋น„๊ต
18
+ valid_username = ADMIN_USERNAME
19
+ valid_password = ADMIN_PASSWORD
20
+ logger.info(f"๊ฒ€์ฆ์šฉ ์‚ฌ์šฉ์ž๋ช…: {valid_username}")
21
+ logger.info(f"๊ฒ€์ฆ์šฉ ๋น„๋ฐ€๋ฒˆํ˜ธ ์กด์žฌ ์—ฌ๋ถ€: {valid_password is not None and len(valid_password) > 0}")
22
+
23
+ if username == valid_username and password == valid_password:
24
+ logger.info(f"๋กœ๊ทธ์ธ ์„ฑ๊ณต: {username}")
25
+ # ์„ธ์…˜ ์„ค์ • ์ „ ํ˜„์žฌ ์„ธ์…˜ ์ƒํƒœ ๋กœ๊น…
26
+ logger.debug(f"์„ธ์…˜ ์„ค์ • ์ „: {session}")
27
+
28
+ # ์„ธ์…˜์— ๋กœ๊ทธ์ธ ์ •๋ณด ์ €์žฅ
29
+ session.permanent = True
30
+ session['logged_in'] = True
31
+ session['username'] = username
32
+ session.modified = True
33
+
34
+ logger.info(f"์„ธ์…˜ ์„ค์ • ํ›„: {session}")
35
+ logger.info("์„ธ์…˜ ์„ค์ • ์™„๋ฃŒ, ๋ฆฌ๋””๋ ‰์…˜ ์‹œ๋„")
36
+
37
+ # ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„ ๋ฆฌ๋””๋ ‰์…˜
38
+ redirect_to = next_url or url_for('index')
39
+ logger.info(f"๋ฆฌ๋””๋ ‰์…˜ ๋Œ€์ƒ: {redirect_to}")
40
+ response = redirect(redirect_to)
41
+ return response
42
+ else:
43
+ logger.warning("๋กœ๊ทธ์ธ ์‹คํŒจ: ์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜")
44
+ if username != valid_username: logger.warning("์‚ฌ์šฉ์ž๋ช… ๋ถˆ์ผ์น˜")
45
+ if password != valid_password: logger.warning("๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜")
46
+ error = '์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'
47
+ else:
48
+ logger.info("๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ GET ์š”์ฒญ")
49
+ if 'logged_in' in session:
50
+ logger.info("์ด๋ฏธ ๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž, ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜")
51
+ return redirect(url_for('index'))
52
+
53
+ logger.info("---------- ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ๋ Œ๋”๋ง ----------")
54
+ return render_template('login.html', error=error, next=next_url)
55
+
56
+
57
+ @app.route('/logout')
58
+ def logout():
59
+ logger.info("-------------- ๋กœ๊ทธ์•„์›ƒ ์š”์ฒญ --------------")
60
+ logger.info(f"๋กœ๊ทธ์•„์›ƒ ์ „ ์„ธ์…˜ ์ƒํƒœ: {session}")
61
+
62
+ if 'logged_in' in session:
63
+ username = session.get('username', 'unknown')
64
+ logger.info(f"์‚ฌ์šฉ์ž {username} ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ ์‹œ์ž‘")
65
+ session.pop('logged_in', None)
66
+ session.pop('username', None)
67
+ session.modified = True
68
+ logger.info(f"์„ธ์…˜ ์ •๋ณด ์‚ญ์ œ ์™„๋ฃŒ. ํ˜„์žฌ ์„ธ์…˜: {session}")
69
+ else:
70
+ logger.warning("๋กœ๊ทธ์ธ๋˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ ๋กœ๊ทธ์•„์›ƒ ์‹œ๋„")
71
+
72
+ logger.info("๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜")
73
+ response = redirect(url_for('login'))
74
+ return response
75
+
76
+
77
+ @app.route('/')
78
+ @login_required
79
+ def index():
80
+ """๋ฉ”์ธ ํŽ˜์ด์ง€"""
81
+ global app_ready
82
+
83
+ # ์•ฑ ์ค€๋น„ ์ƒํƒœ ํ™•์ธ - 30์ดˆ ์ด์ƒ ์ง€๋‚ฌ์œผ๋ฉด ๊ฐ•์ œ๋กœ ready ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ
84
+ current_time = datetime.now()
85
+ start_time = datetime.fromtimestamp(os.path.getmtime(__file__))
86
+ time_diff = (current_time - start_time).total_seconds()
87
+
88
+ if not app_ready and time_diff > 30:
89
+ logger.warning(f"์•ฑ์ด 30์ดˆ ์ด์ƒ ์ดˆ๊ธฐํ™” ์ค‘ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ๊ฐ•์ œ๋กœ ready ์ƒํƒœ๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.")
90
+ app_ready = True
91
+
92
+ if not app_ready:
93
+ logger.info("์•ฑ์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•„ ๋กœ๋”ฉ ํŽ˜์ด์ง€ ํ‘œ์‹œ")
94
+ return render_template('loading.html'), 503 # ์„œ๋น„์Šค ์ค€๋น„ ์•ˆ๋จ ์ƒํƒœ ์ฝ”๋“œ
95
+
96
+ logger.info("๋ฉ”์ธ ํŽ˜์ด์ง€ ์š”์ฒญ")
97
+ return render_template('index.html')
98
+
99
+
100
+ @app.route('/api/status')
101
+ @login_required
102
+ def app_status():
103
+ """์•ฑ ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ™•์ธ API"""
104
+ logger.info(f"์•ฑ ์ƒํƒœ ํ™•์ธ ์š”์ฒญ: {'Ready' if app_ready else 'Not Ready'}")
105
+ return jsonify({"ready": app_ready})
106
+
107
+
108
+ @app.route('/api/llm', methods=['GET', 'POST'])
109
+ @login_required
110
+ def llm_api():
111
+ """์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ LLM ๋ชฉ๋ก ๋ฐ ์„ ํƒ API"""
112
+ global llm_interface
113
+
114
+ if not app_ready:
115
+ return jsonify({"error": "์•ฑ์ด ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."}), 503
116
+
117
+ if request.method == 'GET':
118
+ logger.info("LLM ๋ชฉ๋ก ์š”์ฒญ")
119
+ try:
120
+ current_details = llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {"id": "unknown", "name": "Unknown"}
121
+ supported_llms_dict = llm_interface.SUPPORTED_LLMS if hasattr(llm_interface, 'SUPPORTED_LLMS') else {}
122
+ supported_list = [{
123
+ "name": name, "id": id, "current": id == current_details.get("id")
124
+ } for name, id in supported_llms_dict.items()]
125
+
126
+ return jsonify({
127
+ "supported_llms": supported_list,
128
+ "current_llm": current_details
129
+ })
130
+ except Exception as e:
131
+ logger.error(f"LLM ์ •๋ณด ์กฐํšŒ ์˜ค๋ฅ˜: {e}")
132
+ return jsonify({"error": "LLM ์ •๋ณด ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ"}), 500
133
+
134
+ elif request.method == 'POST':
135
+ data = request.get_json()
136
+ if not data or 'llm_id' not in data:
137
+ return jsonify({"error": "LLM ID๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
138
+
139
+ llm_id = data['llm_id']
140
+ logger.info(f"LLM ๋ณ€๊ฒฝ ์š”์ฒญ: {llm_id}")
141
+
142
+ try:
143
+ if not hasattr(llm_interface, 'set_llm') or not hasattr(llm_interface, 'llm_clients'):
144
+ raise NotImplementedError("LLM ์ธํ„ฐํŽ˜์ด์Šค์— ํ•„์š”ํ•œ ๋ฉ”์†Œ๋“œ/์†์„ฑ ์—†์Œ")
145
+
146
+ if llm_id not in llm_interface.llm_clients:
147
+ return jsonify({"error": f"์ง€์›๋˜์ง€ ์•Š๋Š” LLM ID: {llm_id}"}), 400
148
+
149
+ success = llm_interface.set_llm(llm_id)
150
+ if success:
151
+ new_details = llm_interface.get_current_llm_details()
152
+ logger.info(f"LLM์ด '{new_details.get('name', llm_id)}'๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
153
+ return jsonify({
154
+ "success": True,
155
+ "message": f"LLM์ด '{new_details.get('name', llm_id)}'๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
156
+ "current_llm": new_details
157
+ })
158
+ else:
159
+ logger.error(f"LLM ๋ณ€๊ฒฝ ์‹คํŒจ (ID: {llm_id})")
160
+ return jsonify({"error": "LLM ๋ณ€๊ฒฝ ์ค‘ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ ๋ฐœ์ƒ"}), 500
161
+ except Exception as e:
162
+ logger.error(f"LLM ๋ณ€๊ฒฝ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {e}", exc_info=True)
163
+ return jsonify({"error": f"LLM ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"}), 500
app/app_revised.py ADDED
@@ -0,0 +1,241 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ (์žฅ์น˜ ๊ด€๋ฆฌ ๊ธฐ๋Šฅ ํ†ตํ•ฉ)
3
+ """
4
+
5
+ import os
6
+ import logging
7
+ import threading
8
+ from datetime import datetime, timedelta
9
+ from flask import Flask, send_from_directory
10
+ from dotenv import load_dotenv
11
+ from functools import wraps
12
+
13
+ # ๋กœ๊ฑฐ ์„ค์ •
14
+ logging.basicConfig(
15
+ format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
16
+ level=logging.DEBUG
17
+ )
18
+ logger = logging.getLogger(__name__)
19
+
20
+ # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ
21
+ load_dotenv()
22
+
23
+ # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ ์ƒํƒœ ํ™•์ธ ๋ฐ ๋กœ๊น…
24
+ ADMIN_USERNAME = os.getenv('ADMIN_USERNAME')
25
+ ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD')
26
+ DEVICE_SERVER_URL = os.getenv('DEVICE_SERVER_URL', 'http://localhost:5050')
27
+
28
+ logger.info(f"==== ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋กœ๋“œ ์ƒํƒœ ====")
29
+ logger.info(f"ADMIN_USERNAME ์„ค์ • ์—ฌ๋ถ€: {ADMIN_USERNAME is not None}")
30
+ logger.info(f"ADMIN_PASSWORD ์„ค์ • ์—ฌ๋ถ€: {ADMIN_PASSWORD is not None}")
31
+ logger.info(f"DEVICE_SERVER_URL: {DEVICE_SERVER_URL}")
32
+
33
+ # ํ™˜๊ฒฝ ๋ณ€์ˆ˜๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ๋ณธ๊ฐ’ ์„ค์ •
34
+ if not ADMIN_USERNAME:
35
+ ADMIN_USERNAME = 'admin'
36
+ logger.warning("ADMIN_USERNAME ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์—†์–ด ๊ธฐ๋ณธ๊ฐ’ 'admin'์œผ๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.")
37
+
38
+ if not ADMIN_PASSWORD:
39
+ ADMIN_PASSWORD = 'rag12345'
40
+ logger.warning("ADMIN_PASSWORD ํ™˜๊ฒฝ๋ณ€์ˆ˜๊ฐ€ ์—†์–ด ๊ธฐ๋ณธ๊ฐ’ 'rag12345'๋กœ ์„ค์ •ํ•ฉ๋‹ˆ๋‹ค.")
41
+
42
+ class MockComponent: pass
43
+
44
+ # --- ๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ---
45
+ try:
46
+ from utils.vito_stt import VitoSTT
47
+ from utils.llm_interface import LLMInterface
48
+ from utils.document_processor import DocumentProcessor
49
+ from retrieval.vector_retriever import VectorRetriever
50
+ from retrieval.reranker import ReRanker
51
+ except ImportError as e:
52
+ logger.error(f"๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ์‹คํŒจ: {e}. utils ๋ฐ retrieval ํŒจํ‚ค์ง€๊ฐ€ ์˜ฌ๋ฐ”๋ฅธ ๊ฒฝ๋กœ์— ์žˆ๋Š”์ง€ ํ™•์ธํ•˜์„ธ์š”.")
53
+ VitoSTT = LLMInterface = DocumentProcessor = VectorRetriever = ReRanker = MockComponent
54
+ # --- ๋กœ์ปฌ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ๋ ---
55
+
56
+
57
+ # Flask ์•ฑ ์ดˆ๊ธฐํ™”
58
+ app = Flask(__name__)
59
+
60
+ # ์„ธ์…˜ ์„ค์ •
61
+ app.secret_key = os.getenv('FLASK_SECRET_KEY', 'rag_chatbot_fixed_secret_key_12345')
62
+
63
+ # --- ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ • ---
64
+ app.config['SESSION_COOKIE_SECURE'] = True
65
+ app.config['SESSION_COOKIE_HTTPONLY'] = True
66
+ app.config['SESSION_COOKIE_SAMESITE'] = 'None'
67
+ app.config['SESSION_COOKIE_DOMAIN'] = None
68
+ app.config['SESSION_COOKIE_PATH'] = '/'
69
+ app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=1)
70
+ # --- ์„ธ์…˜ ์ฟ ํ‚ค ์„ค์ • ๋ ---
71
+
72
+ # ์ตœ๋Œ€ ํŒŒ์ผ ํฌ๊ธฐ ์„ค์ • (10MB)
73
+ app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024
74
+ # ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ ํŒŒ์ผ ๊ธฐ์ค€ ์ƒ๋Œ€ ๊ฒฝ๋กœ ์„ค์ •
75
+ APP_ROOT = os.path.dirname(os.path.abspath(__file__))
76
+ app.config['UPLOAD_FOLDER'] = os.path.join(APP_ROOT, 'uploads')
77
+ app.config['DATA_FOLDER'] = os.path.join(APP_ROOT, '..', 'data')
78
+ app.config['INDEX_PATH'] = os.path.join(APP_ROOT, '..', 'data', 'index')
79
+
80
+ # ํ•„์š”ํ•œ ํด๋” ์ƒ์„ฑ
81
+ os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True)
82
+ os.makedirs(app.config['DATA_FOLDER'], exist_ok=True)
83
+ os.makedirs(app.config['INDEX_PATH'], exist_ok=True)
84
+
85
+ # --- ์ „์—ญ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ---
86
+ try:
87
+ llm_interface = LLMInterface(default_llm="openai")
88
+ stt_client = VitoSTT()
89
+ except NameError:
90
+ logger.warning("LLM ๋˜๋Š” STT ์ธํ„ฐํŽ˜์ด์Šค ์ดˆ๊ธฐํ™” ์‹คํŒจ. Mock ๊ฐ์ฒด๋ฅผ ์‚ฌ์šฉํ•ฉ๋‹ˆ๋‹ค.")
91
+ llm_interface = MockComponent()
92
+ stt_client = MockComponent()
93
+
94
+ base_retriever = None
95
+ retriever = None
96
+ app_ready = False # ์•ฑ ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ”Œ๋ž˜๊ทธ
97
+ # --- ์ „์—ญ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ๋ ---
98
+
99
+
100
+ # --- ์ธ์ฆ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ---
101
+ def login_required(f):
102
+ @wraps(f)
103
+ def decorated_function(*args, **kwargs):
104
+ from flask import request, session, redirect, url_for
105
+
106
+ logger.info(f"----------- ์ธ์ฆ ํ•„์š” ํŽ˜์ด์ง€ ์ ‘๊ทผ ์‹œ๋„: {request.path} -----------")
107
+ logger.info(f"ํ˜„์žฌ ํ”Œ๋ผ์Šคํฌ ์„ธ์…˜ ๊ฐ์ฒด: {session}")
108
+ logger.info(f"ํ˜„์žฌ ์„ธ์…˜ ์ƒํƒœ: logged_in={session.get('logged_in', False)}, username={session.get('username', 'None')}")
109
+ logger.info(f"์š”์ฒญ์˜ ์„ธ์…˜ ์ฟ ํ‚ค ๊ฐ’: {request.cookies.get('session', 'None')}")
110
+
111
+ # Flask ์„ธ์…˜์— 'logged_in' ํ‚ค๊ฐ€ ์žˆ๋Š”์ง€ ์ง์ ‘ ํ™•์ธ
112
+ if 'logged_in' not in session:
113
+ logger.warning(f"ํ”Œ๋ผ์Šคํฌ ์„ธ์…˜์— 'logged_in' ์—†์Œ. ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜.")
114
+ return redirect(url_for('login', next=request.url))
115
+
116
+ logger.info(f"์ธ์ฆ ์„ฑ๊ณต: {session.get('username', 'unknown')} ์‚ฌ์šฉ์ž๊ฐ€ {request.path} ์ ‘๊ทผ")
117
+ return f(*args, **kwargs)
118
+ return decorated_function
119
+ # --- ์ธ์ฆ ๋ฐ์ฝ”๋ ˆ์ดํ„ฐ ๋ ---
120
+
121
+
122
+ # --- ์ •์  ํŒŒ์ผ ์„œ๋น™ ---
123
+ @app.route('/static/<path:path>')
124
+ def send_static(path):
125
+ return send_from_directory('static', path)
126
+
127
+
128
+ # --- ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜ ---
129
+ def background_init():
130
+ """๋ฐฑ๊ทธ๋ผ์šด๋“œ์—์„œ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์ˆ˜ํ–‰"""
131
+ global app_ready, retriever, base_retriever
132
+
133
+ # ์ฆ‰์‹œ ์•ฑ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์ƒํƒœ๋กœ ์„ค์ •
134
+ app_ready = True
135
+ logger.info("์•ฑ์„ ์ฆ‰์‹œ ์‚ฌ์šฉ ๊ฐ€๋Šฅ ์ƒํƒœ๋กœ ์„ค์ • (app_ready=True)")
136
+
137
+ try:
138
+ from app.init_retriever import init_retriever
139
+
140
+ # ๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” (๋ณดํ—˜)
141
+ if base_retriever is None:
142
+ base_retriever = MockComponent()
143
+ if hasattr(base_retriever, 'documents'):
144
+ base_retriever.documents = []
145
+
146
+ # ์ž„์‹œ retriever ์„ค์ •
147
+ if retriever is None:
148
+ retriever = MockComponent()
149
+ if not hasattr(retriever, 'search'):
150
+ retriever.search = lambda query, **kwargs: []
151
+
152
+ # ์ž„๋ฒ ๋”ฉ ์บ์‹œ ํŒŒ์ผ ๊ฒฝ๋กœ
153
+ cache_path = os.path.join(app.config['INDEX_PATH'], "cached_embeddings.gz")
154
+
155
+ # ์บ์‹œ๋œ ์ž„๋ฒ ๋”ฉ ๋กœ๋“œ ์‹œ๋„
156
+ try:
157
+ from app.init_retriever import load_embeddings
158
+ cached_retriever = load_embeddings(cache_path)
159
+
160
+ if cached_retriever:
161
+ # ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์žˆ์œผ๋ฉด ๋ฐ”๋กœ ์‚ฌ์šฉ
162
+ base_retriever = cached_retriever
163
+
164
+ # ์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”
165
+ retriever = ReRanker(
166
+ base_retriever=base_retriever,
167
+ rerank_fn=lambda query, results: results,
168
+ rerank_field="text"
169
+ )
170
+
171
+ logger.info("์บ์‹œ๋œ ์ž„๋ฒ ๋”ฉ์œผ๋กœ ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ (๋น ๋ฅธ ์‹œ์ž‘)")
172
+ else:
173
+ # ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ์ „์ฒด ์ดˆ๊ธฐํ™” ์ง„ํ–‰
174
+ logger.info("์บ์‹œ๋œ ์ž„๋ฒ ๋”ฉ์ด ์—†์–ด ์ „์ฒด ์ดˆ๊ธฐํ™” ์‹œ์ž‘")
175
+ retriever = init_retriever(app, base_retriever, retriever, ReRanker)
176
+ logger.info("์ „์ฒด ์ดˆ๊ธฐํ™” ์™„๋ฃŒ")
177
+ except ImportError:
178
+ logger.warning("์ž„๋ฒ ๋”ฉ ์บ์‹œ ๋ชจ๋“ˆ์„ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์ „์ฒด ์ดˆ๊ธฐํ™”๋ฅผ ์ง„ํ–‰ํ•ฉ๋‹ˆ๋‹ค.")
179
+ retriever = init_retriever(app, base_retriever, retriever, ReRanker)
180
+
181
+ logger.info("์•ฑ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ (๋ชจ๋“  ์ปดํฌ๋„ŒํŠธ ์ค€๋น„๋จ)")
182
+ except Exception as e:
183
+ logger.error(f"์•ฑ ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ์ค‘ ์‹ฌ๊ฐํ•œ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
184
+ # ์ดˆ๊ธฐํ™” ์‹คํŒจ ์‹œ ๊ธฐ๋ณธ ๊ฐ์ฒด ์ƒ์„ฑ
185
+ if base_retriever is None:
186
+ base_retriever = MockComponent()
187
+ if hasattr(base_retriever, 'documents'):
188
+ base_retriever.documents = []
189
+ if retriever is None:
190
+ retriever = MockComponent()
191
+ if not hasattr(retriever, 'search'):
192
+ retriever.search = lambda query, **kwargs: []
193
+
194
+ logger.warning("์ดˆ๊ธฐํ™” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ์žˆ์ง€๋งŒ ์•ฑ์€ ๊ณ„์† ์‚ฌ์šฉ ๊ฐ€๋Šฅํ•ฉ๋‹ˆ๋‹ค.")
195
+
196
+
197
+ # --- ๋ผ์šฐํŠธ ๋“ฑ๋ก ---
198
+ def register_all_routes():
199
+ try:
200
+ # ๊ธฐ๋ณธ ๋ผ์šฐํŠธ ๋“ฑ๋ก
201
+ from app.app_routes import register_routes
202
+ register_routes(
203
+ app, login_required, llm_interface, retriever, stt_client,
204
+ DocumentProcessor, base_retriever, app_ready,
205
+ ADMIN_USERNAME, ADMIN_PASSWORD, DEVICE_SERVER_URL
206
+ )
207
+
208
+ # ์žฅ์น˜ ๊ด€๋ฆฌ ๋ผ์šฐํŠธ ๋“ฑ๋ก
209
+ from app.app_device_routes import register_device_routes
210
+ register_device_routes(app, login_required, DEVICE_SERVER_URL)
211
+
212
+ logger.info("๋ชจ๋“  ๋ผ์šฐํŠธ ๋“ฑ๋ก ์™„๋ฃŒ")
213
+ except ImportError as e:
214
+ logger.error(f"๋ผ์šฐํŠธ ๋ชจ๋“ˆ ์ž„ํฌํŠธ ์‹คํŒจ: {e}")
215
+ except Exception as e:
216
+ logger.error(f"๋ผ์šฐํŠธ ๋“ฑ๋ก ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
217
+
218
+
219
+ # --- ์•ฑ ์ดˆ๊ธฐํ™” ๋ฐ ์‹คํ–‰ ---
220
+ def initialize_app():
221
+ # ๋ฐฑ๊ทธ๋ผ์šด๋“œ ์ดˆ๊ธฐํ™” ์Šค๋ ˆ๋“œ ์‹œ์ž‘
222
+ init_thread = threading.Thread(target=background_init)
223
+ init_thread.daemon = True
224
+ init_thread.start()
225
+
226
+ # ๋ผ์šฐํŠธ ๋“ฑ๋ก
227
+ register_all_routes()
228
+
229
+ logger.info("์•ฑ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ")
230
+
231
+
232
+ # ์•ฑ ์ดˆ๊ธฐํ™” ์‹คํ–‰
233
+ initialize_app()
234
+
235
+
236
+ # --- ์•ฑ ์‹คํ–‰ (์ง์ ‘ ์‹คํ–‰ ์‹œ) ---
237
+ if __name__ == '__main__':
238
+ logger.info("Flask ์•ฑ์„ ์ง์ ‘ ์‹คํ–‰ํ•ฉ๋‹ˆ๋‹ค (๊ฐœ๋ฐœ์šฉ ์„œ๋ฒ„).")
239
+ port = int(os.environ.get("PORT", 7860))
240
+ logger.info(f"์„œ๋ฒ„๋ฅผ http://0.0.0.0:{port} ์—์„œ ์‹œ์ž‘ํ•ฉ๋‹ˆ๋‹ค.")
241
+ app.run(debug=True, host='0.0.0.0', port=port)
app/app_routes.py ADDED
@@ -0,0 +1,297 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ์›น ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜ - API ๋ผ์šฐํŠธ ์ •์˜
3
+ """
4
+
5
+ import os
6
+ import json
7
+ import logging
8
+ import tempfile
9
+ import requests
10
+ from flask import request, jsonify, render_template, send_from_directory, session, redirect, url_for
11
+ from datetime import datetime
12
+ from werkzeug.utils import secure_filename
13
+
14
+ # ๋กœ๊ฑฐ ๊ฐ€์ ธ์˜ค๊ธฐ
15
+ logger = logging.getLogger(__name__)
16
+
17
+ def register_routes(app, login_required, llm_interface, retriever, stt_client, DocumentProcessor, base_retriever, app_ready, ADMIN_USERNAME, ADMIN_PASSWORD, DEVICE_SERVER_URL):
18
+ """Flask ์• ํ”Œ๋ฆฌ์ผ€์ด์…˜์— ๋ผ์šฐํŠธ ๋“ฑ๋ก"""
19
+
20
+ # ํ—ฌํผ ํ•จ์ˆ˜
21
+ def allowed_audio_file(filename):
22
+ """ํŒŒ์ผ์ด ํ—ˆ์šฉ๋œ ์˜ค๋””์˜ค ํ™•์žฅ์ž๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธ"""
23
+ ALLOWED_AUDIO_EXTENSIONS = {'mp3', 'wav', 'ogg', 'm4a'}
24
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_AUDIO_EXTENSIONS
25
+
26
+ def allowed_doc_file(filename):
27
+ """ํŒŒ์ผ์ด ํ—ˆ์šฉ๋œ ๋ฌธ์„œ ํ™•์žฅ์ž๋ฅผ ๊ฐ€์ง€๋Š”์ง€ ํ™•์ธ"""
28
+ ALLOWED_DOC_EXTENSIONS = {'txt', 'md', 'pdf', 'docx', 'csv'}
29
+ return '.' in filename and filename.rsplit('.', 1)[1].lower() in ALLOWED_DOC_EXTENSIONS
30
+
31
+ # ์ž„๋ฒ ๋”ฉ ์ €์žฅ ํ•จ์ˆ˜
32
+ def save_embeddings(base_retriever, file_path):
33
+ """์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ์••์ถ•ํ•˜์—ฌ ํŒŒ์ผ์— ์ €์žฅ"""
34
+ import pickle
35
+ import gzip
36
+
37
+ try:
38
+ # ์ €์žฅ ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ์—†์œผ๋ฉด ์ƒ์„ฑ
39
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
40
+
41
+ # ํƒ€์ž„์Šคํƒฌํ”„ ์ถ”๊ฐ€
42
+ save_data = {
43
+ 'timestamp': datetime.now().isoformat(),
44
+ 'retriever': base_retriever
45
+ }
46
+
47
+ # ์••์ถ•ํ•˜์—ฌ ์ €์žฅ (์šฉ๋Ÿ‰ ์ค„์ด๊ธฐ)
48
+ with gzip.open(file_path, 'wb') as f:
49
+ pickle.dump(save_data, f)
50
+
51
+ logger.info(f"์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ {file_path}์— ์••์ถ•ํ•˜์—ฌ ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.")
52
+ return True
53
+ except Exception as e:
54
+ logger.error(f"์ž„๋ฒ ๋”ฉ ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
55
+ return False
56
+
57
+ @app.route('/login', methods=['GET', 'POST'])
58
+ def login():
59
+ error = None
60
+ next_url = request.args.get('next')
61
+ logger.info(f"-------------- ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ์ ‘์† (Next: {next_url}) --------------")
62
+ logger.info(f"Method: {request.method}")
63
+
64
+ if request.method == 'POST':
65
+ logger.info("๋กœ๊ทธ์ธ ์‹œ๋„ ๋ฐ›์Œ")
66
+ username = request.form.get('username', '')
67
+ password = request.form.get('password', '')
68
+ logger.info(f"์ž…๋ ฅ๋œ ์‚ฌ์šฉ์ž๋ช…: {username}")
69
+ logger.info(f"๋น„๋ฐ€๋ฒˆํ˜ธ ์ž…๋ ฅ ์—ฌ๋ถ€: {len(password) > 0}")
70
+
71
+ # ํ™˜๊ฒฝ ๋ณ€์ˆ˜ ๋˜๋Š” ๊ธฐ๋ณธ๊ฐ’๊ณผ ๋น„๊ต
72
+ valid_username = ADMIN_USERNAME
73
+ valid_password = ADMIN_PASSWORD
74
+ logger.info(f"๊ฒ€์ฆ์šฉ ์‚ฌ์šฉ์ž๋ช…: {valid_username}")
75
+ logger.info(f"๊ฒ€์ฆ์šฉ ๋น„๋ฐ€๋ฒˆํ˜ธ ์กด์žฌ ์—ฌ๋ถ€: {valid_password is not None and len(valid_password) > 0}")
76
+
77
+ if username == valid_username and password == valid_password:
78
+ logger.info(f"๋กœ๊ทธ์ธ ์„ฑ๊ณต: {username}")
79
+ # ์„ธ์…˜ ์„ค์ • ์ „ ํ˜„์žฌ ์„ธ์…˜ ์ƒํƒœ ๋กœ๊น…
80
+ logger.debug(f"์„ธ์…˜ ์„ค์ • ์ „: {session}")
81
+
82
+ # ์„ธ์…˜์— ๋กœ๊ทธ์ธ ์ •๋ณด ์ €์žฅ
83
+ session.permanent = True
84
+ session['logged_in'] = True
85
+ session['username'] = username
86
+ session.modified = True
87
+
88
+ logger.info(f"์„ธ์…˜ ์„ค์ • ํ›„: {session}")
89
+ logger.info("์„ธ์…˜ ์„ค์ • ์™„๋ฃŒ, ๋ฆฌ๋””๋ ‰์…˜ ์‹œ๋„")
90
+
91
+ # ๋กœ๊ทธ์ธ ์„ฑ๊ณต ํ›„ ๋ฆฌ๋””๋ ‰์…˜
92
+ redirect_to = next_url or url_for('index')
93
+ logger.info(f"๋ฆฌ๋””๋ ‰์…˜ ๋Œ€์ƒ: {redirect_to}")
94
+ response = redirect(redirect_to)
95
+ return response
96
+ else:
97
+ logger.warning("๋กœ๊ทธ์ธ ์‹คํŒจ: ์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜")
98
+ if username != valid_username: logger.warning("์‚ฌ์šฉ์ž๋ช… ๋ถˆ์ผ์น˜")
99
+ if password != valid_password: logger.warning("๋น„๋ฐ€๋ฒˆํ˜ธ ๋ถˆ์ผ์น˜")
100
+ error = '์•„์ด๋”” ๋˜๋Š” ๋น„๋ฐ€๋ฒˆํ˜ธ๊ฐ€ ์˜ฌ๋ฐ”๋ฅด์ง€ ์•Š์Šต๋‹ˆ๋‹ค.'
101
+ else:
102
+ logger.info("๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ GET ์š”์ฒญ")
103
+ if 'logged_in' in session:
104
+ logger.info("์ด๋ฏธ ๋กœ๊ทธ์ธ๋œ ์‚ฌ์šฉ์ž, ๋ฉ”์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜")
105
+ return redirect(url_for('index'))
106
+
107
+ logger.info("---------- ๋กœ๊ทธ์ธ ํŽ˜์ด์ง€ ๋ Œ๋”๋ง ----------")
108
+ return render_template('login.html', error=error, next=next_url)
109
+
110
+
111
+ @app.route('/logout')
112
+ def logout():
113
+ logger.info("-------------- ๋กœ๊ทธ์•„์›ƒ ์š”์ฒญ --------------")
114
+ logger.info(f"๋กœ๊ทธ์•„์›ƒ ์ „ ์„ธ์…˜ ์ƒํƒœ: {session}")
115
+
116
+ if 'logged_in' in session:
117
+ username = session.get('username', 'unknown')
118
+ logger.info(f"์‚ฌ์šฉ์ž {username} ๋กœ๊ทธ์•„์›ƒ ์ฒ˜๋ฆฌ ์‹œ์ž‘")
119
+ session.pop('logged_in', None)
120
+ session.pop('username', None)
121
+ session.modified = True
122
+ logger.info(f"์„ธ์…˜ ์ •๋ณด ์‚ญ์ œ ์™„๋ฃŒ. ํ˜„์žฌ ์„ธ์…˜: {session}")
123
+ else:
124
+ logger.warning("๋กœ๊ทธ์ธ๋˜์ง€ ์•Š์€ ์ƒํƒœ์—์„œ ๋กœ๊ทธ์•„์›ƒ ์‹œ๋„")
125
+
126
+ logger.info("๋กœ๊ทธ์ธ ํŽ˜์ด์ง€๋กœ ๋ฆฌ๋””๋ ‰์…˜")
127
+ response = redirect(url_for('login'))
128
+ return response
129
+
130
+
131
+ @app.route('/')
132
+ @login_required
133
+ def index():
134
+ """๋ฉ”์ธ ํŽ˜์ด์ง€"""
135
+ nonlocal app_ready
136
+
137
+ # ์•ฑ ์ค€๋น„ ์ƒํƒœ ํ™•์ธ - 30์ดˆ ์ด์ƒ ์ง€๋‚ฌ์œผ๋ฉด ๊ฐ•์ œ๋กœ ready ์ƒํƒœ๋กœ ๋ณ€๊ฒฝ
138
+ current_time = datetime.now()
139
+ start_time = datetime.fromtimestamp(os.path.getmtime(__file__))
140
+ time_diff = (current_time - start_time).total_seconds()
141
+
142
+ if not app_ready and time_diff > 30:
143
+ logger.warning(f"์•ฑ์ด 30์ดˆ ์ด์ƒ ์ดˆ๊ธฐํ™” ์ค‘ ์ƒํƒœ์ž…๋‹ˆ๋‹ค. ๊ฐ•์ œ๋กœ ready ์ƒํƒœ๋กœ ๋ณ€๊ฒฝํ•ฉ๋‹ˆ๋‹ค.")
144
+ app_ready = True
145
+
146
+ if not app_ready:
147
+ logger.info("์•ฑ์ด ์•„์ง ์ค€๋น„๋˜์ง€ ์•Š์•„ ๋กœ๋”ฉ ํŽ˜์ด์ง€ ํ‘œ์‹œ")
148
+ return render_template('loading.html'), 503 # ์„œ๋น„์Šค ์ค€๋น„ ์•ˆ๋จ ์ƒํƒœ ์ฝ”๋“œ
149
+
150
+ logger.info("๋ฉ”์ธ ํŽ˜์ด์ง€ ์š”์ฒญ")
151
+ return render_template('index.html')
152
+
153
+
154
+ @app.route('/api/status')
155
+ @login_required
156
+ def app_status():
157
+ """์•ฑ ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ™•์ธ API"""
158
+ logger.info(f"์•ฑ ์ƒํƒœ ํ™•์ธ ์š”์ฒญ: {'Ready' if app_ready else 'Not Ready'}")
159
+ return jsonify({"ready": app_ready})
160
+
161
+
162
+ @app.route('/api/llm', methods=['GET', 'POST'])
163
+ @login_required
164
+ def llm_api():
165
+ """์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ LLM ๋ชฉ๋ก ๋ฐ ์„ ํƒ API"""
166
+ if not app_ready:
167
+ return jsonify({"error": "์•ฑ์ด ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."}), 503
168
+
169
+ if request.method == 'GET':
170
+ logger.info("LLM ๋ชฉ๋ก ์š”์ฒญ")
171
+ try:
172
+ current_details = llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {"id": "unknown", "name": "Unknown"}
173
+ supported_llms_dict = llm_interface.SUPPORTED_LLMS if hasattr(llm_interface, 'SUPPORTED_LLMS') else {}
174
+ supported_list = [{
175
+ "name": name, "id": id, "current": id == current_details.get("id")
176
+ } for name, id in supported_llms_dict.items()]
177
+
178
+ return jsonify({
179
+ "supported_llms": supported_list,
180
+ "current_llm": current_details
181
+ })
182
+ except Exception as e:
183
+ logger.error(f"LLM ์ •๋ณด ์กฐํšŒ ์˜ค๋ฅ˜: {e}")
184
+ return jsonify({"error": "LLM ์ •๋ณด ์กฐํšŒ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ"}), 500
185
+
186
+ elif request.method == 'POST':
187
+ data = request.get_json()
188
+ if not data or 'llm_id' not in data:
189
+ return jsonify({"error": "LLM ID๊ฐ€ ์ œ๊ณต๋˜์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
190
+
191
+ llm_id = data['llm_id']
192
+ logger.info(f"LLM ๋ณ€๊ฒฝ ์š”์ฒญ: {llm_id}")
193
+
194
+ try:
195
+ if not hasattr(llm_interface, 'set_llm') or not hasattr(llm_interface, 'llm_clients'):
196
+ raise NotImplementedError("LLM ์ธํ„ฐํŽ˜์ด์Šค์— ํ•„์š”ํ•œ ๋ฉ”์†Œ๋“œ/์†์„ฑ ์—†์Œ")
197
+
198
+ if llm_id not in llm_interface.llm_clients:
199
+ return jsonify({"error": f"์ง€์›๋˜์ง€ ์•Š๋Š” LLM ID: {llm_id}"}), 400
200
+
201
+ success = llm_interface.set_llm(llm_id)
202
+ if success:
203
+ new_details = llm_interface.get_current_llm_details()
204
+ logger.info(f"LLM์ด '{new_details.get('name', llm_id)}'๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
205
+ return jsonify({
206
+ "success": True,
207
+ "message": f"LLM์ด '{new_details.get('name', llm_id)}'๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.",
208
+ "current_llm": new_details
209
+ })
210
+ else:
211
+ logger.error(f"LLM ๋ณ€๊ฒฝ ์‹คํŒจ (ID: {llm_id})")
212
+ return jsonify({"error": "LLM ๋ณ€๊ฒฝ ์ค‘ ๋‚ด๋ถ€ ์˜ค๋ฅ˜ ๋ฐœ์ƒ"}), 500
213
+ except Exception as e:
214
+ logger.error(f"LLM ๋ณ€๊ฒฝ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜: {e}", exc_info=True)
215
+ return jsonify({"error": f"LLM ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {str(e)}"}), 500
216
+
217
+
218
+ @app.route('/api/chat', methods=['POST'])
219
+ @login_required
220
+ def chat():
221
+ """ํ…์ŠคํŠธ ๊ธฐ๋ฐ˜ ์ฑ—๋ด‡ API"""
222
+ if not app_ready or retriever is None:
223
+ return jsonify({"error": "์•ฑ/๊ฒ€์ƒ‰๊ธฐ๊ฐ€ ์•„์ง ์ดˆ๊ธฐํ™” ์ค‘์ž…๋‹ˆ๋‹ค. ์ž ์‹œ ํ›„ ๋‹ค์‹œ ์‹œ๋„ํ•ด์ฃผ์„ธ์š”."}), 503
224
+
225
+ try:
226
+ data = request.get_json()
227
+ if not data or 'query' not in data:
228
+ return jsonify({"error": "์ฟผ๋ฆฌ๊ฐ€ ์ œ๊ณต๏ฟฝ๏ฟฝ๏ฟฝ์ง€ ์•Š์•˜์Šต๋‹ˆ๋‹ค."}), 400
229
+
230
+ query = data['query']
231
+ logger.info(f"ํ…์ŠคํŠธ ์ฟผ๋ฆฌ ์ˆ˜์‹ : {query[:100]}...")
232
+
233
+ # RAG ๊ฒ€์ƒ‰ ์ˆ˜ํ–‰
234
+ if not hasattr(retriever, 'search'):
235
+ raise NotImplementedError("Retriever์— search ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
236
+ search_results = retriever.search(query, top_k=5, first_stage_k=6)
237
+
238
+ # ์ปจํ…์ŠคํŠธ ์ค€๋น„
239
+ if not hasattr(DocumentProcessor, 'prepare_rag_context'):
240
+ raise NotImplementedError("DocumentProcessor์— prepare_rag_context ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
241
+ context = DocumentProcessor.prepare_rag_context(search_results, field="text")
242
+
243
+ if not context:
244
+ logger.warning("๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ๊ฐ€ ์—†์–ด ์ปจํ…์ŠคํŠธ๋ฅผ ์ƒ์„ฑํ•˜์ง€ ๋ชปํ•จ.")
245
+
246
+ # LLM์— ์งˆ์˜
247
+ llm_id = data.get('llm_id', None)
248
+ if not hasattr(llm_interface, 'rag_generate'):
249
+ raise NotImplementedError("LLMInterface์— rag_generate ๋ฉ”์†Œ๋“œ๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.")
250
+
251
+ if not context:
252
+ answer = "์ฃ„์†กํ•ฉ๋‹ˆ๋‹ค. ๊ด€๋ จ ์ •๋ณด๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค."
253
+ logger.info("์ปจํ…์ŠคํŠธ ์—†์ด ๊ธฐ๋ณธ ์‘๋‹ต ์ƒ์„ฑ")
254
+ else:
255
+ answer = llm_interface.rag_generate(query, context, llm_id=llm_id)
256
+ logger.info(f"LLM ์‘๋‹ต ์ƒ์„ฑ ์™„๋ฃŒ (๊ธธ์ด: {len(answer)})")
257
+
258
+ # ์†Œ์Šค ์ •๋ณด ์ถ”์ถœ (CSV ID ์ถ”์ถœ ๋กœ์ง ํฌํ•จ)
259
+ sources = []
260
+ if search_results:
261
+ for result in search_results:
262
+ if not isinstance(result, dict):
263
+ logger.warning(f"์˜ˆ์ƒ์น˜ ๋ชปํ•œ ๊ฒ€์ƒ‰ ๊ฒฐ๊ณผ ํ˜•์‹: {type(result)}")
264
+ continue
265
+
266
+ if "source" in result:
267
+ source_info = {
268
+ "source": result.get("source", "Unknown"),
269
+ "score": result.get("rerank_score", result.get("score", 0))
270
+ }
271
+
272
+ # CSV ํŒŒ์ผ ํŠน์ • ์ฒ˜๋ฆฌ
273
+ if "text" in result and result.get("filetype") == "csv":
274
+ try:
275
+ text_lines = result["text"].strip().split('\n')
276
+ if text_lines:
277
+ first_line = text_lines[0].strip()
278
+ if ',' in first_line:
279
+ first_column = first_line.split(',')[0].strip()
280
+ source_info["id"] = first_column
281
+ logger.debug(f"CSV ์†Œ์Šค ID ์ถ”์ถœ: {first_column} from {source_info['source']}")
282
+ except Exception as e:
283
+ logger.warning(f"CSV ์†Œ์Šค ID ์ถ”์ถœ ์‹คํŒจ ({result.get('source')}): {e}")
284
+
285
+ sources.append(source_info)
286
+
287
+ # ์ตœ์ข… ์‘๋‹ต
288
+ response_data = {
289
+ "answer": answer,
290
+ "sources": sources,
291
+ "llm": llm_interface.get_current_llm_details() if hasattr(llm_interface, 'get_current_llm_details') else {}
292
+ }
293
+ return jsonify(response_data)
294
+
295
+ except Exception as e:
296
+ logger.error(f"์ฑ„ํŒ… ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}", exc_info=True)
297
+ return jsonify({"error": f"์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค: {str(e)}"}), 500
app/init_retriever.py ADDED
@@ -0,0 +1,149 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ - ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ๋ชจ๋“ˆ
3
+ """
4
+
5
+ import os
6
+ import logging
7
+ import pickle
8
+ import gzip
9
+ from datetime import datetime
10
+
11
+ # ๋กœ๊ฑฐ ๊ฐ€์ ธ์˜ค๊ธฐ
12
+ logger = logging.getLogger(__name__)
13
+
14
+ def save_embeddings(base_retriever, file_path):
15
+ """์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ์••์ถ•ํ•˜์—ฌ ํŒŒ์ผ์— ์ €์žฅ"""
16
+ try:
17
+ # ์ €์žฅ ๋””๋ ‰ํ† ๋ฆฌ๊ฐ€ ์—†์œผ๋ฉด ์ƒ์„ฑ
18
+ os.makedirs(os.path.dirname(file_path), exist_ok=True)
19
+
20
+ # ํƒ€์ž„์Šคํƒฌํ”„ ์ถ”๊ฐ€
21
+ save_data = {
22
+ 'timestamp': datetime.now().isoformat(),
23
+ 'retriever': base_retriever
24
+ }
25
+
26
+ # ์••์ถ•ํ•˜์—ฌ ์ €์žฅ (์šฉ๋Ÿ‰ ์ค„์ด๊ธฐ)
27
+ with gzip.open(file_path, 'wb') as f:
28
+ pickle.dump(save_data, f)
29
+
30
+ logger.info(f"์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ {file_path}์— ์••์ถ•ํ•˜์—ฌ ์ €์žฅํ–ˆ์Šต๋‹ˆ๋‹ค.")
31
+ return True
32
+ except Exception as e:
33
+ logger.error(f"์ž„๋ฒ ๋”ฉ ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
34
+ return False
35
+
36
+ def load_embeddings(file_path, max_age_days=30):
37
+ """์ €์žฅ๋œ ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ํŒŒ์ผ์—์„œ ๋กœ๋“œ"""
38
+ try:
39
+ if not os.path.exists(file_path):
40
+ logger.info(f"์ €์žฅ๋œ ์ž„๋ฒ ๋”ฉ ํŒŒ์ผ({file_path})์ด ์—†์Šต๋‹ˆ๋‹ค.")
41
+ return None
42
+
43
+ # ์••์ถ• ํŒŒ์ผ ๋กœ๋“œ
44
+ with gzip.open(file_path, 'rb') as f:
45
+ data = pickle.load(f)
46
+
47
+ # ํƒ€์ž„์Šคํƒฌํ”„ ํ™•์ธ (๋„ˆ๋ฌด ์˜ค๋ž˜๋œ ๋ฐ์ดํ„ฐ๋Š” ์‚ฌ์šฉํ•˜์ง€ ์•Š์Œ)
48
+ saved_time = datetime.fromisoformat(data['timestamp'])
49
+ age = (datetime.now() - saved_time).days
50
+
51
+ if age > max_age_days:
52
+ logger.info(f"์ €์žฅ๋œ ์ž„๋ฒ ๋”ฉ์ด {age}์ผ๋กœ ๋„ˆ๋ฌด ์˜ค๋ž˜๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์ƒˆ๋กœ ์ƒ์„ฑํ•ฉ๋‹ˆ๋‹ค.")
53
+ return None
54
+
55
+ logger.info(f"{file_path}์—์„œ ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ๋กœ๋“œํ–ˆ์Šต๋‹ˆ๋‹ค. (์ƒ์„ฑ์ผ: {saved_time})")
56
+ return data['retriever']
57
+ except Exception as e:
58
+ logger.error(f"์ž„๋ฒ ๋”ฉ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
59
+ return None
60
+
61
+ def init_retriever(app, base_retriever, retriever, ReRanker):
62
+ """๊ฒ€์ƒ‰๊ธฐ ๊ฐ์ฒด ์ดˆ๊ธฐํ™” ๋˜๋Š” ๋กœ๋“œ"""
63
+ from utils.document_processor import DocumentProcessor
64
+ from retrieval.vector_retriever import VectorRetriever
65
+
66
+ # ์ž„๋ฒ ๋”ฉ ์บ์‹œ ํŒŒ์ผ ๊ฒฝ๋กœ
67
+ cache_path = os.path.join(app.config['INDEX_PATH'], "cached_embeddings.gz")
68
+
69
+ # ๋จผ์ € ์ €์žฅ๋œ ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ ๋กœ๋“œ ์‹œ๋„
70
+ cached_retriever = load_embeddings(cache_path)
71
+
72
+ if cached_retriever:
73
+ logger.info("์บ์‹œ๋œ ์ž„๋ฒ ๋”ฉ ๋ฐ์ดํ„ฐ๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ๋กœ๋“œํ–ˆ์Šต๋‹ˆ๋‹ค.")
74
+ base_retriever = cached_retriever
75
+ else:
76
+ # ์บ์‹œ๋œ ๋ฐ์ดํ„ฐ๊ฐ€ ์—†์œผ๋ฉด ๊ธฐ์กด ๋ฐฉ์‹์œผ๋กœ ์ดˆ๊ธฐํ™”
77
+ index_path = app.config['INDEX_PATH']
78
+
79
+ # VectorRetriever ๋กœ๋“œ ๋˜๋Š” ์ดˆ๊ธฐํ™”
80
+ if os.path.exists(os.path.join(index_path, "documents.json")):
81
+ try:
82
+ logger.info(f"๊ธฐ์กด ๋ฒกํ„ฐ ์ธ๋ฑ์Šค๋ฅผ '{index_path}'์—์„œ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค...")
83
+ base_retriever = VectorRetriever.load(index_path)
84
+ logger.info(f"{len(base_retriever.documents) if hasattr(base_retriever, 'documents') else 0}๊ฐœ ๋ฌธ์„œ๊ฐ€ ๋กœ๋“œ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.")
85
+ except Exception as e:
86
+ logger.error(f"์ธ๋ฑ์Šค ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}. ์ƒˆ ๊ฒ€์ƒ‰๊ธฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค.")
87
+ base_retriever = VectorRetriever()
88
+ else:
89
+ logger.info("๊ธฐ์กด ์ธ๋ฑ์Šค๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์–ด ์ƒˆ ๊ฒ€์ƒ‰๊ธฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค...")
90
+ base_retriever = VectorRetriever()
91
+
92
+ # ๋ฐ์ดํ„ฐ ํด๋”์˜ ๋ฌธ์„œ ๋กœ๋“œ
93
+ data_path = app.config['DATA_FOLDER']
94
+ if (not hasattr(base_retriever, 'documents') or not base_retriever.documents) and os.path.exists(data_path):
95
+ logger.info(f"{data_path}์—์„œ ๋ฌธ์„œ๋ฅผ ๋กœ๋“œํ•ฉ๋‹ˆ๋‹ค...")
96
+ try:
97
+ docs = DocumentProcessor.load_documents_from_directory(
98
+ data_path,
99
+ extensions=[".txt", ".md", ".csv"],
100
+ recursive=True
101
+ )
102
+ if docs and hasattr(base_retriever, 'add_documents'):
103
+ logger.info(f"{len(docs)}๊ฐœ ๋ฌธ์„œ๋ฅผ ๊ฒ€์ƒ‰๊ธฐ์— ์ถ”๊ฐ€ํ•ฉ๋‹ˆ๋‹ค...")
104
+ base_retriever.add_documents(docs)
105
+
106
+ if hasattr(base_retriever, 'save'):
107
+ logger.info(f"๊ฒ€์ƒ‰๊ธฐ ์ƒํƒœ๋ฅผ '{index_path}'์— ์ €์žฅํ•ฉ๋‹ˆ๋‹ค...")
108
+ try:
109
+ base_retriever.save(index_path)
110
+ logger.info("์ธ๋ฑ์Šค ์ €์žฅ ์™„๋ฃŒ")
111
+
112
+ # ์ƒˆ๋กœ ์ƒ์„ฑ๋œ ๊ฒ€์ƒ‰๊ธฐ ์บ์‹ฑ
113
+ if hasattr(base_retriever, 'documents') and base_retriever.documents:
114
+ save_embeddings(base_retriever, cache_path)
115
+ logger.info(f"๊ฒ€์ƒ‰๊ธฐ๋ฅผ ์บ์‹œ ํŒŒ์ผ {cache_path}์— ์ €์žฅ ์™„๋ฃŒ")
116
+ except Exception as e:
117
+ logger.error(f"์ธ๋ฑ์Šค ์ €์žฅ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: {e}")
118
+ except Exception as e:
119
+ logger.error(f"DATA_FOLDER์—์„œ ๋ฌธ์„œ ๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜: {e}")
120
+
121
+ # ์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™”
122
+ logger.info("์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ๋ฅผ ์ดˆ๊ธฐํ™”ํ•ฉ๋‹ˆ๋‹ค...")
123
+ try:
124
+ # ์ž์ฒด ๊ตฌํ˜„๋œ ์žฌ์ˆœ์œ„ํ™” ํ•จ์ˆ˜
125
+ def custom_rerank_fn(query, results):
126
+ query_terms = set(query.lower().split())
127
+ for result in results:
128
+ if isinstance(result, dict) and "text" in result:
129
+ text = result["text"].lower()
130
+ term_freq = sum(1 for term in query_terms if term in text)
131
+ normalized_score = term_freq / (len(text.split()) + 1) * 10
132
+ result["rerank_score"] = result.get("score", 0) * 0.7 + normalized_score * 0.3
133
+ elif isinstance(result, dict):
134
+ result["rerank_score"] = result.get("score", 0)
135
+ results.sort(key=lambda x: x.get("rerank_score", 0) if isinstance(x, dict) else 0, reverse=True)
136
+ return results
137
+
138
+ # ReRanker ํด๋ž˜์Šค ์‚ฌ์šฉ
139
+ retriever = ReRanker(
140
+ base_retriever=base_retriever,
141
+ rerank_fn=custom_rerank_fn,
142
+ rerank_field="text"
143
+ )
144
+ logger.info("์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ")
145
+ except Exception as e:
146
+ logger.error(f"์žฌ์ˆœ์œ„ํ™” ๊ฒ€์ƒ‰๊ธฐ ์ดˆ๊ธฐํ™” ์‹คํŒจ: {e}")
147
+ retriever = base_retriever # ์‹คํŒจ ์‹œ ๊ธฐ๋ณธ ๊ฒ€์ƒ‰๊ธฐ ์‚ฌ์šฉ
148
+
149
+ return retriever
app/static/css/device-style.css ADDED
@@ -0,0 +1,306 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * ์žฅ์น˜ ๊ด€๋ฆฌ ์ „์šฉ CSS ์Šคํƒ€์ผ
3
+ */
4
+
5
+ /* ์žฅ์น˜ ๊ด€๋ฆฌ ์„น์…˜ */
6
+ .device-container {
7
+ background-color: var(--card-bg);
8
+ border-radius: 8px;
9
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
10
+ overflow: hidden;
11
+ padding: 20px;
12
+ }
13
+
14
+ /* ์„œ๋ฒ„ ์ƒํƒœ ํ‘œ์‹œ */
15
+ .server-status {
16
+ padding: 12px 15px;
17
+ border-radius: 6px;
18
+ margin-bottom: 15px;
19
+ display: flex;
20
+ align-items: center;
21
+ }
22
+
23
+ .server-status i {
24
+ margin-right: 10px;
25
+ font-size: 18px;
26
+ }
27
+
28
+ .server-status.success {
29
+ background-color: rgba(16, 185, 129, 0.1);
30
+ color: var(--success-color);
31
+ border-left: 4px solid var(--success-color);
32
+ }
33
+
34
+ .server-status.error {
35
+ background-color: rgba(239, 68, 68, 0.1);
36
+ color: var(--error-color);
37
+ border-left: 4px solid var(--error-color);
38
+ }
39
+
40
+ .server-status.warning {
41
+ background-color: rgba(245, 158, 11, 0.1);
42
+ color: var(--secondary-color);
43
+ border-left: 4px solid var(--secondary-color);
44
+ }
45
+
46
+ /* ์„œ๋ฒ„ ์‹œ์ž‘ ์•ˆ๋‚ด */
47
+ .server-guide {
48
+ background-color: #f8f9fa;
49
+ border-radius: 6px;
50
+ padding: 15px;
51
+ margin: 15px 0;
52
+ font-size: 14px;
53
+ }
54
+
55
+ .server-guide code {
56
+ background-color: #e9ecef;
57
+ padding: 2px 6px;
58
+ border-radius: 4px;
59
+ font-family: monospace;
60
+ }
61
+
62
+ .server-guide ol {
63
+ margin-left: 20px;
64
+ margin-top: 10px;
65
+ margin-bottom: 0;
66
+ }
67
+
68
+ .server-guide li {
69
+ margin-bottom: 5px;
70
+ }
71
+
72
+ /* ์žฌ์‹œ๋„ ๋ฒ„ํŠผ */
73
+ .retry-button {
74
+ background-color: var(--primary-color);
75
+ color: white;
76
+ border: none;
77
+ border-radius: 4px;
78
+ padding: 8px 15px;
79
+ margin-top: 10px;
80
+ cursor: pointer;
81
+ display: flex;
82
+ align-items: center;
83
+ justify-content: center;
84
+ font-size: 14px;
85
+ transition: var(--transition);
86
+ }
87
+
88
+ .retry-button:hover {
89
+ background-color: var(--primary-dark);
90
+ }
91
+
92
+ .retry-button i {
93
+ margin-right: 5px;
94
+ }
95
+
96
+ /* ์žฅ์น˜ ๋ชฉ๋ก */
97
+ .device-section, .programs-section {
98
+ margin-top: 25px;
99
+ }
100
+
101
+ .device-section h2, .programs-section h2 {
102
+ margin-bottom: 15px;
103
+ color: var(--primary-color);
104
+ display: flex;
105
+ justify-content: space-between;
106
+ align-items: center;
107
+ }
108
+
109
+ .device-count {
110
+ font-size: 14px;
111
+ color: var(--light-text);
112
+ margin-bottom: 10px;
113
+ }
114
+
115
+ .device-item {
116
+ background-color: #f8f9fa;
117
+ border-radius: 8px;
118
+ padding: 15px;
119
+ margin-bottom: 10px;
120
+ border: 1px solid var(--border-color);
121
+ transition: var(--transition);
122
+ }
123
+
124
+ .device-item:hover {
125
+ box-shadow: 0 2px 5px rgba(0, 0, 0, 0.08);
126
+ }
127
+
128
+ .device-item h3 {
129
+ margin-bottom: 10px;
130
+ color: var(--primary-color);
131
+ font-size: 16px;
132
+ }
133
+
134
+ .device-details {
135
+ display: grid;
136
+ grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
137
+ gap: 10px;
138
+ font-size: 14px;
139
+ }
140
+
141
+ .device-details p {
142
+ margin: 0;
143
+ }
144
+
145
+ .status-connected, .status-online {
146
+ color: var(--success-color);
147
+ font-weight: 500;
148
+ }
149
+
150
+ .status-disconnected, .status-offline {
151
+ color: var(--error-color);
152
+ font-weight: 500;
153
+ }
154
+
155
+ .status-idle {
156
+ color: var(--secondary-color);
157
+ font-weight: 500;
158
+ }
159
+
160
+ .no-devices, .no-programs {
161
+ padding: 15px;
162
+ text-align: center;
163
+ background-color: #f8f9fa;
164
+ border-radius: 8px;
165
+ color: var(--light-text);
166
+ border: 1px dashed var(--border-color);
167
+ }
168
+
169
+ .no-devices i, .no-programs i {
170
+ margin-right: 5px;
171
+ }
172
+
173
+ /* ๋กœ๋”ฉ ํ‘œ์‹œ */
174
+ .loading-device, .loading-device-list, .loading-programs {
175
+ display: flex;
176
+ flex-direction: column;
177
+ align-items: center;
178
+ justify-content: center;
179
+ padding: 20px;
180
+ text-align: center;
181
+ }
182
+
183
+ .spinner.small {
184
+ width: 20px;
185
+ height: 20px;
186
+ margin-right: 8px;
187
+ }
188
+
189
+ /* ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์„น์…˜ */
190
+ .programs-container {
191
+ background-color: #f8f9fa;
192
+ border-radius: 8px;
193
+ padding: 15px;
194
+ margin-top: 20px;
195
+ border: 1px solid var(--border-color);
196
+ }
197
+
198
+ .program-item {
199
+ background-color: white;
200
+ border-radius: 6px;
201
+ padding: 15px;
202
+ margin-bottom: 10px;
203
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
204
+ position: relative;
205
+ }
206
+
207
+ .program-item h3 {
208
+ margin-bottom: 8px;
209
+ color: var(--primary-color);
210
+ font-size: 16px;
211
+ }
212
+
213
+ .program-description {
214
+ font-size: 14px;
215
+ color: var(--light-text);
216
+ margin-bottom: 15px;
217
+ }
218
+
219
+ .execute-btn {
220
+ background-color: var(--primary-color);
221
+ color: white;
222
+ border: none;
223
+ border-radius: 4px;
224
+ padding: 8px 15px;
225
+ cursor: pointer;
226
+ display: flex;
227
+ align-items: center;
228
+ font-size: 14px;
229
+ transition: var(--transition);
230
+ }
231
+
232
+ .execute-btn:hover {
233
+ background-color: var(--primary-dark);
234
+ }
235
+
236
+ .execute-btn i {
237
+ margin-right: 5px;
238
+ }
239
+
240
+ .execute-loading, .execute-success, .execute-error {
241
+ display: flex;
242
+ align-items: center;
243
+ padding: 5px 10px;
244
+ border-radius: 4px;
245
+ margin-top: 10px;
246
+ font-size: 14px;
247
+ }
248
+
249
+ .execute-loading {
250
+ background-color: #f1f5f9;
251
+ color: var(--light-text);
252
+ }
253
+
254
+ .execute-success {
255
+ background-color: rgba(16, 185, 129, 0.1);
256
+ color: var(--success-color);
257
+ }
258
+
259
+ .execute-error {
260
+ background-color: rgba(239, 68, 68, 0.1);
261
+ color: var(--error-color);
262
+ }
263
+
264
+ .execute-loading span, .execute-success span, .execute-error span {
265
+ margin-left: 5px;
266
+ }
267
+
268
+ /* ์žฅ์น˜ ๊ด€๋ฆฌ ์„น์…˜ ์ƒ๋‹จ ๋ฒ„ํŠผ */
269
+ .device-toolbar {
270
+ display: flex;
271
+ justify-content: space-between;
272
+ margin-bottom: 15px;
273
+ }
274
+
275
+ .load-programs-btn, .refresh-device-btn {
276
+ padding: 8px 15px;
277
+ background-color: var(--primary-color);
278
+ color: white;
279
+ border: none;
280
+ border-radius: 4px;
281
+ cursor: pointer;
282
+ display: flex;
283
+ align-items: center;
284
+ font-size: 14px;
285
+ transition: var(--transition);
286
+ }
287
+
288
+ .load-programs-btn:hover, .refresh-device-btn:hover {
289
+ background-color: var(--primary-dark);
290
+ }
291
+
292
+ .load-programs-btn i, .refresh-device-btn i {
293
+ margin-right: 5px;
294
+ }
295
+
296
+ /* ๋ฐ˜์‘ํ˜• */
297
+ @media (max-width: 768px) {
298
+ .device-details {
299
+ grid-template-columns: 1fr;
300
+ }
301
+
302
+ .device-toolbar {
303
+ flex-direction: column;
304
+ gap: 10px;
305
+ }
306
+ }
app/static/js/app-core.js ADDED
@@ -0,0 +1,326 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ UI ์ฝ”์–ด JavaScript
3
+ */
4
+
5
+ // ์ „์—ญ ๋ณ€์ˆ˜
6
+ let currentLLM = 'openai';
7
+ let supportedLLMs = [];
8
+
9
+ // DOM ๋ณ€์ˆ˜ ๋ฏธ๋ฆฌ ์„ ์–ธ
10
+ let chatTab, docsTab, deviceTab, chatSection, docsSection, deviceSection;
11
+ let chatMessages, userInput, sendButton;
12
+ let micButton, stopRecordingButton, recordingStatus;
13
+ let llmSelect, currentLLMInfo;
14
+
15
+ // ๋…น์Œ ๊ด€๋ จ ๋ณ€์ˆ˜
16
+ let mediaRecorder = null;
17
+ let audioChunks = [];
18
+ let isRecording = false;
19
+
20
+ /**
21
+ * ์•ฑ ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ™•์ธ ํ•จ์ˆ˜
22
+ */
23
+ async function checkAppStatus() {
24
+ try {
25
+ const response = await fetch('/api/status');
26
+ if (!response.ok) {
27
+ return false;
28
+ }
29
+ const data = await response.json();
30
+ return data.ready;
31
+ } catch (error) {
32
+ console.error('์ƒํƒœ ํ™•์ธ ์‹คํŒจ:', error);
33
+ return false;
34
+ }
35
+ }
36
+
37
+ /**
38
+ * DOM ์š”์†Œ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜
39
+ */
40
+ function initDomElements() {
41
+ console.log('DOM ์š”์†Œ ์ดˆ๊ธฐํ™” ์ค‘...');
42
+
43
+ // ํƒญ ๊ด€๋ จ ์š”์†Œ
44
+ chatTab = document.getElementById('chatTab');
45
+ docsTab = document.getElementById('docsTab');
46
+ deviceTab = document.getElementById('deviceTab');
47
+ chatSection = document.getElementById('chatSection');
48
+ docsSection = document.getElementById('docsSection');
49
+ deviceSection = document.getElementById('deviceSection');
50
+
51
+ // ์ฑ„ํŒ… ๊ด€๋ จ ์š”์†Œ
52
+ chatMessages = document.getElementById('chatMessages');
53
+ userInput = document.getElementById('userInput');
54
+ sendButton = document.getElementById('sendButton');
55
+
56
+ // ์Œ์„ฑ ๋…น์Œ ๊ด€๋ จ ์š”์†Œ
57
+ micButton = document.getElementById('micButton');
58
+ stopRecordingButton = document.getElementById('stopRecordingButton');
59
+ recordingStatus = document.getElementById('recordingStatus');
60
+
61
+ // LLM ๊ด€๋ จ ์š”์†Œ
62
+ llmSelect = document.getElementById('llmSelect');
63
+ currentLLMInfo = document.getElementById('currentLLMInfo');
64
+
65
+ console.log('DOM ์š”์†Œ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ');
66
+ }
67
+
68
+ /**
69
+ * ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™” ํ•จ์ˆ˜
70
+ */
71
+ function initEventListeners() {
72
+ console.log('์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™” ์ค‘...');
73
+
74
+ // ํƒญ ์ „ํ™˜ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
75
+ chatTab.addEventListener('click', () => {
76
+ switchTab('chat');
77
+ });
78
+
79
+ docsTab.addEventListener('click', () => {
80
+ switchTab('docs');
81
+ loadDocuments();
82
+ });
83
+
84
+ deviceTab.addEventListener('click', () => {
85
+ switchTab('device');
86
+ loadDeviceStatus();
87
+ });
88
+
89
+ // LLM ์„ ํƒ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
90
+ llmSelect.addEventListener('change', (event) => {
91
+ changeLLM(event.target.value);
92
+ });
93
+
94
+ // ๋ฉ”์‹œ์ง€ ์ „์†ก ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
95
+ sendButton.addEventListener('click', sendMessage);
96
+ userInput.addEventListener('keydown', (event) => {
97
+ if (event.key === 'Enter' && !event.shiftKey) {
98
+ event.preventDefault();
99
+ sendMessage();
100
+ }
101
+ });
102
+
103
+ // ์Œ์„ฑ ์ธ์‹ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
104
+ micButton.addEventListener('click', startRecording);
105
+ stopRecordingButton.addEventListener('click', stopRecording);
106
+
107
+ // ์ž๋™ ์ž…๋ ฅ ํ•„๋“œ ํฌ๊ธฐ ์กฐ์ •
108
+ userInput.addEventListener('input', adjustTextareaHeight);
109
+
110
+ console.log('์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ');
111
+ }
112
+
113
+ /**
114
+ * ํƒญ ์ „ํ™˜ ํ•จ์ˆ˜
115
+ * @param {string} tabName - ํ™œ์„ฑํ™”ํ•  ํƒญ ์ด๋ฆ„ ('chat', 'docs', ๋˜๋Š” 'device')
116
+ */
117
+ function switchTab(tabName) {
118
+ console.log(`ํƒญ ์ „ํ™˜: ${tabName}`);
119
+
120
+ // ๋ชจ๋“  ํƒญ์„ ๋น„ํ™œ์„ฑํ™”
121
+ [chatTab, docsTab, deviceTab].forEach(tab => tab.classList.remove('active'));
122
+ [chatSection, docsSection, deviceSection].forEach(section => section.classList.remove('active'));
123
+
124
+ // ์„ ํƒํ•œ ํƒญ ํ™œ์„ฑํ™”
125
+ if (tabName === 'chat') {
126
+ chatTab.classList.add('active');
127
+ chatSection.classList.add('active');
128
+ } else if (tabName === 'docs') {
129
+ docsTab.classList.add('active');
130
+ docsSection.classList.add('active');
131
+ } else if (tabName === 'device') {
132
+ deviceTab.classList.add('active');
133
+ deviceSection.classList.add('active');
134
+ }
135
+ }
136
+
137
+ /**
138
+ * ์‹œ์Šคํ…œ ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
139
+ * @param {string} message - ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€
140
+ */
141
+ function addSystemNotification(message) {
142
+ console.log(`์‹œ์Šคํ…œ ์•Œ๋ฆผ ์ถ”๊ฐ€: ${message}`);
143
+
144
+ const messageDiv = document.createElement('div');
145
+ messageDiv.classList.add('message', 'system');
146
+
147
+ const contentDiv = document.createElement('div');
148
+ contentDiv.classList.add('message-content');
149
+
150
+ const messageP = document.createElement('p');
151
+ messageP.innerHTML = `<i class="fas fa-info-circle"></i> ${message}`;
152
+ contentDiv.appendChild(messageP);
153
+
154
+ messageDiv.appendChild(contentDiv);
155
+ chatMessages.appendChild(messageDiv);
156
+
157
+ // ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๋กœ ์ด๋™
158
+ chatMessages.scrollTop = chatMessages.scrollHeight;
159
+ }
160
+
161
+ /**
162
+ * ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ ํ•จ์ˆ˜
163
+ * @param {string} text - ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ
164
+ * @param {string} sender - ๋ฉ”์‹œ์ง€ ๋ฐœ์‹ ๏ฟฝ๏ฟฝ๏ฟฝ ('user' ๋˜๋Š” 'bot' ๋˜๋Š” 'system')
165
+ * @param {string|null} transcription - ์Œ์„ฑ ์ธ์‹ ํ…์ŠคํŠธ (์„ ํƒ ์‚ฌํ•ญ)
166
+ * @param {Array|null} sources - ์†Œ์Šค ์ •๋ณด ๋ฐฐ์—ด (์„ ํƒ ์‚ฌํ•ญ)
167
+ */
168
+ function addMessage(text, sender, transcription = null, sources = null) {
169
+ console.log(`๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€: ${sender}`);
170
+
171
+ const messageDiv = document.createElement('div');
172
+ messageDiv.classList.add('message', sender);
173
+
174
+ const contentDiv = document.createElement('div');
175
+ contentDiv.classList.add('message-content');
176
+
177
+ // ์Œ์„ฑ ์ธ์‹ ํ…์ŠคํŠธ ์ถ”๊ฐ€ (์žˆ๋Š” ๊ฒฝ์šฐ)
178
+ if (transcription && sender === 'bot') {
179
+ const transcriptionP = document.createElement('p');
180
+ transcriptionP.classList.add('transcription');
181
+ transcriptionP.textContent = `"${transcription}"`;
182
+ contentDiv.appendChild(transcriptionP);
183
+ }
184
+
185
+ // ๋ฉ”์‹œ์ง€ ํ…์ŠคํŠธ ์ถ”๊ฐ€
186
+ const textP = document.createElement('p');
187
+ textP.textContent = text;
188
+ contentDiv.appendChild(textP);
189
+
190
+ // ์†Œ์Šค ์ •๋ณด ์ถ”๊ฐ€ (์žˆ๋Š” ๊ฒฝ์šฐ)
191
+ if (sources && sources.length > 0 && sender === 'bot') {
192
+ const sourcesDiv = document.createElement('div');
193
+ sourcesDiv.classList.add('sources');
194
+
195
+ const sourcesTitle = document.createElement('strong');
196
+ sourcesTitle.textContent = '์ถœ์ฒ˜: ';
197
+ sourcesDiv.appendChild(sourcesTitle);
198
+
199
+ sources.forEach((source, index) => {
200
+ if (index < 3) { // ์ตœ๋Œ€ 3๊ฐœ๊นŒ์ง€๋งŒ ํ‘œ์‹œ
201
+ const sourceSpan = document.createElement('span');
202
+ sourceSpan.classList.add('source-item');
203
+ sourceSpan.textContent = source.source;
204
+ sourcesDiv.appendChild(sourceSpan);
205
+ }
206
+ });
207
+
208
+ contentDiv.appendChild(sourcesDiv);
209
+ }
210
+
211
+ messageDiv.appendChild(contentDiv);
212
+ chatMessages.appendChild(messageDiv);
213
+
214
+ // ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๋กœ ์ด๋™
215
+ chatMessages.scrollTop = chatMessages.scrollHeight;
216
+ }
217
+
218
+ /**
219
+ * ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ ํ•จ์ˆ˜
220
+ * @returns {string} ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ID
221
+ */
222
+ function addLoadingMessage() {
223
+ console.log('๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€');
224
+
225
+ const id = 'loading-' + Date.now();
226
+ const messageDiv = document.createElement('div');
227
+ messageDiv.classList.add('message', 'bot');
228
+ messageDiv.id = id;
229
+
230
+ const contentDiv = document.createElement('div');
231
+ contentDiv.classList.add('message-content');
232
+
233
+ const loadingP = document.createElement('p');
234
+ loadingP.innerHTML = '<div class="spinner" style="width: 20px; height: 20px; display: inline-block; margin-right: 10px;"></div> ์ƒ๊ฐ ์ค‘...';
235
+ contentDiv.appendChild(loadingP);
236
+
237
+ messageDiv.appendChild(contentDiv);
238
+ chatMessages.appendChild(messageDiv);
239
+
240
+ // ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๋กœ ์ด๋™
241
+ chatMessages.scrollTop = chatMessages.scrollHeight;
242
+
243
+ return id;
244
+ }
245
+
246
+ /**
247
+ * ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ ํ•จ์ˆ˜
248
+ * @param {string} id - ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ID
249
+ */
250
+ function removeLoadingMessage(id) {
251
+ console.log(`๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ: ${id}`);
252
+
253
+ const loadingMessage = document.getElementById(id);
254
+ if (loadingMessage) {
255
+ loadingMessage.remove();
256
+ }
257
+ }
258
+
259
+ /**
260
+ * ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ ํ•จ์ˆ˜
261
+ * @param {string} errorText - ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ
262
+ */
263
+ function addErrorMessage(errorText) {
264
+ console.log(`์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€: ${errorText}`);
265
+
266
+ const messageDiv = document.createElement('div');
267
+ messageDiv.classList.add('message', 'system');
268
+
269
+ const contentDiv = document.createElement('div');
270
+ contentDiv.classList.add('message-content');
271
+ contentDiv.style.backgroundColor = 'rgba(239, 68, 68, 0.1)';
272
+ contentDiv.style.color = 'var(--error-color)';
273
+
274
+ const errorP = document.createElement('p');
275
+ errorP.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${errorText}`;
276
+ contentDiv.appendChild(errorP);
277
+
278
+ messageDiv.appendChild(contentDiv);
279
+ chatMessages.appendChild(messageDiv);
280
+
281
+ // ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๋กœ ์ด๋™
282
+ chatMessages.scrollTop = chatMessages.scrollHeight;
283
+ }
284
+
285
+ /**
286
+ * textarea ๋†’์ด ์ž๋™ ์กฐ์ • ํ•จ์ˆ˜
287
+ */
288
+ function adjustTextareaHeight() {
289
+ userInput.style.height = 'auto';
290
+ userInput.style.height = Math.min(userInput.scrollHeight, 100) + 'px';
291
+ }
292
+
293
+ // ์•ฑ ์ดˆ๊ธฐํ™” (ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ)
294
+ document.addEventListener('DOMContentLoaded', function() {
295
+ console.log('ํŽ˜์ด์ง€ ๋กœ๋“œ ์™„๋ฃŒ, ์•ฑ ์ดˆ๊ธฐํ™” ์‹œ์ž‘');
296
+
297
+ // DOM ์š”์†Œ ์ดˆ๊ธฐํ™”
298
+ initDomElements();
299
+
300
+ // ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™”
301
+ initEventListeners();
302
+
303
+ // ์•ฑ ์ƒํƒœ ํ™•์ธ (๋กœ๋”ฉ ํŽ˜์ด์ง€๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ์—๋งŒ)
304
+ if (window.location.pathname === '/' && !document.getElementById('app-loading-indicator')) {
305
+ // ์•ฑ ์ƒํƒœ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ™•์ธ
306
+ const statusInterval = setInterval(async () => {
307
+ const isReady = await checkAppStatus();
308
+ if (isReady) {
309
+ clearInterval(statusInterval);
310
+ console.log('์•ฑ์ด ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
311
+
312
+ // ์•ฑ์ด ์ค€๋น„๋˜๋ฉด LLM ๋ชฉ๋ก ๋กœ๋“œ
313
+ loadLLMs();
314
+
315
+ // ํ™œ์„ฑ ํƒญ ํ™•์ธ ๋ฐ ๋ฐ์ดํ„ฐ ๋กœ๋“œ
316
+ if (docsSection.classList.contains('active')) {
317
+ loadDocuments();
318
+ } else if (deviceSection.classList.contains('active')) {
319
+ loadDeviceStatus();
320
+ }
321
+ }
322
+ }, 5000);
323
+ }
324
+
325
+ console.log('์•ฑ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ');
326
+ });
app/static/js/app-device.js ADDED
@@ -0,0 +1,375 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ์žฅ์น˜ ๊ด€๋ฆฌ JavaScript
3
+ */
4
+
5
+ // ์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ๊ธฐ๋ณธ URL (ํฌํŠธ ๋ณ€๊ฒฝ: 5050)
6
+ const DEVICE_SERVER_URL = 'http://localhost:5050';
7
+
8
+ // DOM ์š”์†Œ ๋ฏธ๋ฆฌ ์„ ์–ธ
9
+ let deviceStatus, deviceList, programsList, deviceRefreshButton;
10
+ let programsContainer, loadProgramsButton;
11
+
12
+ /**
13
+ * ์žฅ์น˜ ๊ด€๋ฆฌ DOM ์š”์†Œ ์ดˆ๊ธฐํ™”
14
+ */
15
+ function initDeviceElements() {
16
+ console.log('์žฅ์น˜ ๊ด€๋ฆฌ DOM ์š”์†Œ ์ดˆ๊ธฐํ™” ์ค‘...');
17
+
18
+ deviceStatus = document.getElementById('deviceStatus');
19
+ deviceList = document.getElementById('deviceList');
20
+ programsList = document.getElementById('programsList');
21
+ deviceRefreshButton = document.getElementById('deviceRefreshButton');
22
+ programsContainer = document.getElementById('programsContainer');
23
+ loadProgramsButton = document.getElementById('loadProgramsButton');
24
+
25
+ console.log('์žฅ์น˜ ๊ด€๋ฆฌ DOM ์š”์†Œ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ');
26
+
27
+ // ์žฅ์น˜ ๊ด€๋ฆฌ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™”
28
+ initDeviceEventListeners();
29
+ }
30
+
31
+ /**
32
+ * ์žฅ์น˜ ๊ด€๋ฆฌ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™”
33
+ */
34
+ function initDeviceEventListeners() {
35
+ console.log('์žฅ์น˜ ๊ด€๋ฆฌ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™” ์ค‘...');
36
+
37
+ // ์žฅ์น˜ ์ƒํƒœ ์ƒˆ๋กœ๊ณ ์นจ ๋ฒ„ํŠผ
38
+ deviceRefreshButton.addEventListener('click', () => {
39
+ console.log('์žฅ์น˜ ์ƒํƒœ ์ƒˆ๋กœ๊ณ ์นจ ์š”์ฒญ');
40
+ loadDeviceStatus();
41
+ });
42
+
43
+ // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ ๋ฒ„ํŠผ
44
+ loadProgramsButton.addEventListener('click', () => {
45
+ console.log('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ ์š”์ฒญ');
46
+ loadProgramsList();
47
+ });
48
+
49
+ console.log('์žฅ์น˜ ๊ด€๋ฆฌ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ');
50
+ }
51
+
52
+ /**
53
+ * ํƒ€์ž„์•„์›ƒ ๊ธฐ๋Šฅ์ด ์žˆ๋Š” fetch
54
+ * @param {string} url - ์š”์ฒญ URL
55
+ * @param {Object} options - fetch ์˜ต์…˜
56
+ * @param {number} timeout - ํƒ€์ž„์•„์›ƒ ์‹œ๊ฐ„(ms)
57
+ * @returns {Promise} - fetch ์‘๋‹ต Promise
58
+ */
59
+ async function fetchWithTimeout(url, options = {}, timeout = 5000) {
60
+ console.log(`API ์š”์ฒญ: ${options.method || 'GET'} ${url}`);
61
+
62
+ const controller = new AbortController();
63
+ const id = setTimeout(() => controller.abort(), timeout);
64
+
65
+ try {
66
+ const response = await fetch(url, {
67
+ ...options,
68
+ signal: controller.signal
69
+ });
70
+ clearTimeout(id);
71
+ console.log(`API ์‘๋‹ต ์ƒํƒœ: ${response.status}`);
72
+ return response;
73
+ } catch (error) {
74
+ clearTimeout(id);
75
+ if (error.name === 'AbortError') {
76
+ console.error(`API ์š”์ฒญ ํƒ€์ž„์•„์›ƒ: ${url}`);
77
+ throw new Error('์š”์ฒญ ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค');
78
+ }
79
+ console.error(`API ์š”์ฒญ ์‹คํŒจ: ${url}`, error);
80
+ throw error;
81
+ }
82
+ }
83
+
84
+ /**
85
+ * ์žฅ์น˜ ์ƒํƒœ ๋กœ๋“œ ํ•จ์ˆ˜
86
+ */
87
+ async function loadDeviceStatus() {
88
+ console.log('์žฅ์น˜ ์ƒํƒœ ๋กœ๋“œ ์‹œ์ž‘');
89
+
90
+ // ์žฅ์น˜ ์ƒํƒœ ์ดˆ๊ธฐํ™”
91
+ deviceStatus.innerHTML = '<div class="loading-device"><div class="spinner"></div><p>์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์ค‘...</p></div>';
92
+ deviceList.innerHTML = '';
93
+
94
+ try {
95
+ console.log('์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ ์ค‘...');
96
+ // ๋จผ์ € ์„œ๋ฒ„ ์ƒํƒœ ํ™•์ธ
97
+ const statusResponse = await fetchWithTimeout(`${DEVICE_SERVER_URL}/api/status`, {}, 3000);
98
+
99
+ if (!statusResponse.ok) {
100
+ throw new Error(`์„œ๋ฒ„ ์‘๋‹ต ์ฝ”๋“œ: ${statusResponse.status}`);
101
+ }
102
+
103
+ const statusData = await statusResponse.json();
104
+ console.log(`์„œ๋ฒ„ ์ƒํƒœ: ${statusData.status}`);
105
+
106
+ // ์žฅ์น˜ ๋ชฉ๋ก ๊ฐ€์ ธ์˜ค๊ธฐ
107
+ console.log('์žฅ์น˜ ๋ชฉ๋ก ์š”์ฒญ ์ค‘...');
108
+ const devicesResponse = await fetchWithTimeout(`${DEVICE_SERVER_URL}/api/devices`);
109
+
110
+ if (!devicesResponse.ok) {
111
+ throw new Error(`์žฅ์น˜ ๋ชฉ๋ก ์š”์ฒญ ์‹คํŒจ: ${devicesResponse.status}`);
112
+ }
113
+
114
+ const devicesData = await devicesResponse.json();
115
+ console.log(`์žฅ์น˜ ๋ชฉ๋ก ์‘๋‹ต: ${devicesData.devices ? devicesData.devices.length : 0}๊ฐœ ์žฅ์น˜`);
116
+
117
+ // ์ƒํƒœ ์—…๋ฐ์ดํŠธ
118
+ deviceStatus.innerHTML = '';
119
+
120
+ // ์„œ๋ฒ„ ์ƒํƒœ ํ‘œ์‹œ
121
+ const serverStatusDiv = document.createElement('div');
122
+ serverStatusDiv.className = 'server-status success';
123
+ serverStatusDiv.innerHTML = `<i class="fas fa-check-circle"></i> ์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์ƒํƒœ: ${statusData.status}`;
124
+ deviceStatus.appendChild(serverStatusDiv);
125
+
126
+ // ์žฅ์น˜ ๋ชฉ๋ก ํ‘œ์‹œ
127
+ if (devicesData.devices && devicesData.devices.length > 0) {
128
+ console.log('์žฅ์น˜ ๋ชฉ๋ก ํ™”๋ฉด์— ํ‘œ์‹œ');
129
+
130
+ const deviceCountDiv = document.createElement('div');
131
+ deviceCountDiv.className = 'device-count';
132
+ deviceCountDiv.textContent = `์ด ${devicesData.devices.length}๊ฐœ ์žฅ์น˜ ์—ฐ๊ฒฐ๋จ`;
133
+ deviceStatus.appendChild(deviceCountDiv);
134
+
135
+ devicesData.devices.forEach(device => {
136
+ const deviceItem = document.createElement('div');
137
+ deviceItem.className = 'device-item';
138
+
139
+ const deviceName = document.createElement('h3');
140
+ deviceName.textContent = device.name;
141
+ deviceItem.appendChild(deviceName);
142
+
143
+ const deviceDetails = document.createElement('div');
144
+ deviceDetails.className = 'device-details';
145
+ deviceDetails.innerHTML = `
146
+ <p><strong>์œ ํ˜•:</strong> ${device.type}</p>
147
+ <p><strong>์ƒํƒœ:</strong> <span class="status-${device.status.toLowerCase()}">${device.status}</span></p>
148
+ ${device.id ? `<p><strong>ID:</strong> ${device.id}</p>` : ''}
149
+ `;
150
+ deviceItem.appendChild(deviceDetails);
151
+
152
+ deviceList.appendChild(deviceItem);
153
+ });
154
+ } else {
155
+ console.log('์—ฐ๊ฒฐ๋œ ์žฅ์น˜ ์—†์Œ');
156
+
157
+ const noDeviceMsg = document.createElement('div');
158
+ noDeviceMsg.className = 'no-devices';
159
+ noDeviceMsg.innerHTML = '<i class="fas fa-info-circle"></i> ํ˜„์žฌ ์—ฐ๊ฒฐ๋œ ์žฅ์น˜๊ฐ€ ์—†์Šต๋‹ˆ๋‹ค.';
160
+ deviceList.appendChild(noDeviceMsg);
161
+ }
162
+ } catch (error) {
163
+ console.error('์žฅ์น˜ ์ƒํƒœ ๋กœ๋“œ ์˜ค๋ฅ˜:', error);
164
+
165
+ deviceStatus.innerHTML = '';
166
+ const errorDiv = document.createElement('div');
167
+ errorDiv.className = 'server-status error';
168
+
169
+ if (error.message.includes('์‹œ๊ฐ„์ด ์ดˆ๊ณผ')) {
170
+ errorDiv.innerHTML = '<i class="fas fa-exclamation-circle"></i> ์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์‘๋‹ต ์‹œ๊ฐ„์ด ์ดˆ๊ณผ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”.';
171
+ } else if (error.message.includes('Failed to fetch')) {
172
+ errorDiv.innerHTML = '<i class="fas fa-exclamation-circle"></i> ์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”.';
173
+ } else {
174
+ errorDiv.innerHTML = `<i class="fas fa-exclamation-circle"></i> ์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„ ์˜ค๋ฅ˜: ${error.message}`;
175
+ }
176
+
177
+ deviceStatus.appendChild(errorDiv);
178
+
179
+ // ์žฌ์‹œ๋„ ๋ฒ„ํŠผ
180
+ const retryButton = document.createElement('button');
181
+ retryButton.className = 'retry-button';
182
+ retryButton.innerHTML = '<i class="fas fa-sync"></i> ๋‹ค์‹œ ์‹œ๋„';
183
+ retryButton.addEventListener('click', loadDeviceStatus);
184
+ deviceStatus.appendChild(retryButton);
185
+
186
+ // ์žฅ์น˜ ๋ชฉ๋ก ์ดˆ๊ธฐํ™”
187
+ deviceList.innerHTML = '';
188
+ }
189
+ }
190
+
191
+ /**
192
+ * ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ ํ•จ์ˆ˜
193
+ */
194
+ async function loadProgramsList() {
195
+ console.log('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ ์‹œ์ž‘');
196
+
197
+ // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์ดˆ๊ธฐํ™”
198
+ programsList.innerHTML = '<div class="loading-programs"><div class="spinner"></div><p>ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ ์ค‘...</p></div>';
199
+
200
+ try {
201
+ console.log('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก API ์š”์ฒญ ์ค‘...');
202
+ const response = await fetchWithTimeout(`${DEVICE_SERVER_URL}/api/programs`);
203
+
204
+ if (!response.ok) {
205
+ throw new Error(`ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์š”์ฒญ ์‹คํŒจ: ${response.status}`);
206
+ }
207
+
208
+ const data = await response.json();
209
+ console.log(`ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ์‘๋‹ต: ${data.programs ? data.programs.length : 0}๊ฐœ ํ”„๋กœ๊ทธ๋žจ`);
210
+
211
+ // ๋ชฉ๋ก ์ดˆ๊ธฐํ™”
212
+ programsList.innerHTML = '';
213
+
214
+ // ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ํ‘œ์‹œ
215
+ if (data.programs && data.programs.length > 0) {
216
+ programsContainer.style.display = 'block';
217
+
218
+ data.programs.forEach(program => {
219
+ const programItem = document.createElement('div');
220
+ programItem.className = 'program-item';
221
+
222
+ const programName = document.createElement('h3');
223
+ programName.textContent = program.name;
224
+ programItem.appendChild(programName);
225
+
226
+ if (program.description) {
227
+ const programDesc = document.createElement('p');
228
+ programDesc.className = 'program-description';
229
+ programDesc.textContent = program.description;
230
+ programItem.appendChild(programDesc);
231
+ }
232
+
233
+ const executeButton = document.createElement('button');
234
+ executeButton.className = 'execute-btn';
235
+ executeButton.innerHTML = '<i class="fas fa-play"></i> ์‹คํ–‰';
236
+ executeButton.addEventListener('click', () => {
237
+ executeProgram(program.id, program.name);
238
+ });
239
+ programItem.appendChild(executeButton);
240
+
241
+ programsList.appendChild(programItem);
242
+ });
243
+ } else {
244
+ programsContainer.style.display = 'block';
245
+
246
+ const noProgramsMsg = document.createElement('div');
247
+ noProgramsMsg.className = 'no-programs';
248
+ noProgramsMsg.innerHTML = '<i class="fas fa-info-circle"></i> ๋“ฑ๋ก๋œ ํ”„๋กœ๊ทธ๋žจ์ด ์—†์Šต๋‹ˆ๋‹ค.';
249
+ programsList.appendChild(noProgramsMsg);
250
+ }
251
+ } catch (error) {
252
+ console.error('ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ ์˜ค๋ฅ˜:', error);
253
+
254
+ programsList.innerHTML = '';
255
+ const errorDiv = document.createElement('div');
256
+ errorDiv.className = 'error-message';
257
+
258
+ if (error.message.includes('Failed to fetch')) {
259
+ errorDiv.innerHTML = '<i class="fas fa-exclamation-circle"></i> ์žฅ์น˜ ๊ด€๋ฆฌ ์„œ๋ฒ„์— ์—ฐ๊ฒฐํ•  ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค. ์„œ๋ฒ„๊ฐ€ ์‹คํ–‰ ์ค‘์ธ์ง€ ํ™•์ธํ•ด์ฃผ์„ธ์š”.';
260
+ } else {
261
+ errorDiv.innerHTML = `<i class="fas fa-exclamation-circle"></i> ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ ์˜ค๋ฅ˜: ${error.message}`;
262
+ }
263
+
264
+ programsList.appendChild(errorDiv);
265
+
266
+ // ์žฌ์‹œ๋„ ๋ฒ„ํŠผ
267
+ const retryButton = document.createElement('button');
268
+ retryButton.className = 'retry-button';
269
+ retryButton.innerHTML = '<i class="fas fa-sync"></i> ๋‹ค์‹œ ์‹œ๋„';
270
+ retryButton.addEventListener('click', loadProgramsList);
271
+ programsList.appendChild(retryButton);
272
+ }
273
+ }
274
+
275
+ /**
276
+ * ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ํ•จ์ˆ˜
277
+ * @param {string} programId - ํ”„๋กœ๊ทธ๋žจ ID
278
+ * @param {string} programName - ํ”„๋กœ๊ทธ๋žจ ์ด๋ฆ„
279
+ */
280
+ async function executeProgram(programId, programName) {
281
+ console.log(`ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์š”์ฒญ: ${programName} (ID: ${programId})`);
282
+
283
+ // ํ”„๋กœ๊ทธ๋žจ ์š”์†Œ ์ฐพ๊ธฐ
284
+ const programItems = document.querySelectorAll('.program-item');
285
+ let programItem = null;
286
+
287
+ for (const item of programItems) {
288
+ if (item.querySelector('h3').textContent === programName) {
289
+ programItem = item;
290
+ break;
291
+ }
292
+ }
293
+
294
+ if (!programItem) {
295
+ console.error(`ํ”„๋กœ๊ทธ๋žจ ์š”์†Œ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Œ: ${programName}`);
296
+ return;
297
+ }
298
+
299
+ // ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ์ถ”๊ฐ€
300
+ const loadingIndicator = document.createElement('div');
301
+ loadingIndicator.className = 'execute-loading';
302
+ loadingIndicator.innerHTML = '<div class="spinner small"></div><span>์‹คํ–‰ ์ค‘...</span>';
303
+ programItem.appendChild(loadingIndicator);
304
+
305
+ try {
306
+ console.log('ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ API ์š”์ฒญ ์ค‘...');
307
+ const response = await fetchWithTimeout(`${DEVICE_SERVER_URL}/api/programs/${programId}/execute`, {
308
+ method: 'POST',
309
+ headers: {
310
+ 'Content-Type': 'application/json'
311
+ },
312
+ body: JSON.stringify({})
313
+ });
314
+
315
+ // ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ ์ œ๊ฑฐ
316
+ programItem.removeChild(loadingIndicator);
317
+
318
+ const data = await response.json();
319
+
320
+ if (!response.ok || !data.success) {
321
+ throw new Error(data.message || `์‹คํ–‰ ์‹คํŒจ (${response.status})`);
322
+ }
323
+
324
+ console.log(`ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์„ฑ๊ณต: ${programName}`);
325
+
326
+ // ์„ฑ๊ณต ํ‘œ์‹œ
327
+ const successIndicator = document.createElement('div');
328
+ successIndicator.className = 'execute-success';
329
+ successIndicator.innerHTML = '<i class="fas fa-check-circle"></i><span>์‹คํ–‰ ์™„๋ฃŒ</span>';
330
+ programItem.appendChild(successIndicator);
331
+
332
+ // 3์ดˆ ํ›„ ์„ฑ๊ณต ํ‘œ์‹œ ์ œ๊ฑฐ
333
+ setTimeout(() => {
334
+ if (programItem.contains(successIndicator)) {
335
+ programItem.removeChild(successIndicator);
336
+ }
337
+ }, 3000);
338
+
339
+ // ์‹œ์Šคํ…œ ์•Œ๋ฆผ ์ถ”๊ฐ€
340
+ addSystemNotification(`ํ”„๋กœ๊ทธ๋žจ '${programName}' ์‹คํ–‰ ์„ฑ๊ณต: ${data.message}`);
341
+ } catch (error) {
342
+ console.error(`ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰ ์˜ค๋ฅ˜ (${programName}):`, error);
343
+
344
+ // ๋กœ๋”ฉ ์ธ๋””์ผ€์ดํ„ฐ๊ฐ€ ์•„์ง ์žˆ์œผ๋ฉด ์ œ๊ฑฐ
345
+ if (programItem.contains(loadingIndicator)) {
346
+ programItem.removeChild(loadingIndicator);
347
+ }
348
+
349
+ // ์˜ค๋ฅ˜ ํ‘œ์‹œ
350
+ const errorIndicator = document.createElement('div');
351
+ errorIndicator.className = 'execute-error';
352
+ errorIndicator.innerHTML = `<i class="fas fa-exclamation-circle"></i><span>์‹คํ–‰ ์‹คํŒจ</span>`;
353
+ programItem.appendChild(errorIndicator);
354
+
355
+ // 3์ดˆ ํ›„ ์˜ค๋ฅ˜ ํ‘œ์‹œ ์ œ๊ฑฐ
356
+ setTimeout(() => {
357
+ if (programItem.contains(errorIndicator)) {
358
+ programItem.removeChild(errorIndicator);
359
+ }
360
+ }, 3000);
361
+
362
+ // ์‹œ์Šคํ…œ ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
363
+ addErrorMessage(`ํ”„๋กœ๊ทธ๋žจ '${programName}' ์‹คํ–‰ ์‹คํŒจ: ${error.message}`);
364
+ }
365
+ }
366
+
367
+ // ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™”
368
+ document.addEventListener('DOMContentLoaded', function() {
369
+ console.log('์žฅ์น˜ ๊ด€๋ฆฌ ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™”');
370
+
371
+ // ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ดˆ๊ธฐํ™” (DOM ์š”์†Œ๊ฐ€ ์ค€๋น„๋œ ํ›„)
372
+ setTimeout(() => {
373
+ initDeviceElements();
374
+ }, 100);
375
+ });
app/static/js/app-docs.js ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ ๋ฌธ์„œ ๊ด€๋ฆฌ JavaScript
3
+ */
4
+
5
+ // DOM ์š”์†Œ ๋ฏธ๋ฆฌ ์„ ์–ธ
6
+ let uploadForm, documentFile, fileName, uploadButton, uploadStatus;
7
+ let refreshDocsButton, docsList, docsLoading, noDocsMessage;
8
+
9
+ /**
10
+ * ๋ฌธ์„œ ๊ด€๋ฆฌ DOM ์š”์†Œ ์ดˆ๊ธฐํ™”
11
+ */
12
+ function initDocsElements() {
13
+ console.log('๋ฌธ์„œ ๊ด€๋ฆฌ DOM ์š”์†Œ ์ดˆ๊ธฐํ™” ์ค‘...');
14
+
15
+ uploadForm = document.getElementById('uploadForm');
16
+ documentFile = document.getElementById('documentFile');
17
+ fileName = document.getElementById('fileName');
18
+ uploadButton = document.getElementById('uploadButton');
19
+ uploadStatus = document.getElementById('uploadStatus');
20
+ refreshDocsButton = document.getElementById('refreshDocsButton');
21
+ docsList = document.getElementById('docsList');
22
+ docsLoading = document.getElementById('docsLoading');
23
+ noDocsMessage = document.getElementById('noDocsMessage');
24
+
25
+ console.log('๋ฌธ์„œ ๊ด€๋ฆฌ DOM ์š”์†Œ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ');
26
+
27
+ // ๋ฌธ์„œ ๊ด€๋ฆฌ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™”
28
+ initDocsEventListeners();
29
+ }
30
+
31
+ /**
32
+ * ๋ฌธ์„œ ๊ด€๋ฆฌ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™”
33
+ */
34
+ function initDocsEventListeners() {
35
+ console.log('๋ฌธ์„œ ๊ด€๋ฆฌ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™” ์ค‘...');
36
+
37
+ // ๋ฌธ์„œ ์—…๋กœ๋“œ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
38
+ documentFile.addEventListener('change', (event) => {
39
+ if (event.target.files.length > 0) {
40
+ fileName.textContent = event.target.files[0].name;
41
+ console.log(`ํŒŒ์ผ ์„ ํƒ๋จ: ${event.target.files[0].name}`);
42
+ } else {
43
+ fileName.textContent = '์„ ํƒ๋œ ํŒŒ์ผ ์—†์Œ';
44
+ console.log('ํŒŒ์ผ ์„ ํƒ ์ทจ์†Œ๋จ');
45
+ }
46
+ });
47
+
48
+ uploadForm.addEventListener('submit', (event) => {
49
+ event.preventDefault();
50
+ console.log('์—…๋กœ๋“œ ํผ ์ œ์ถœ๋จ');
51
+ uploadDocument();
52
+ });
53
+
54
+ // ๋ฌธ์„œ ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
55
+ refreshDocsButton.addEventListener('click', () => {
56
+ console.log('๋ฌธ์„œ ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ ์š”์ฒญ');
57
+ loadDocuments();
58
+ });
59
+
60
+ console.log('๋ฌธ์„œ ๊ด€๋ฆฌ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ ์ดˆ๊ธฐํ™” ์™„๋ฃŒ');
61
+ }
62
+
63
+ /**
64
+ * ๋ฌธ์„œ ์—…๋กœ๋“œ ํ•จ์ˆ˜
65
+ */
66
+ async function uploadDocument() {
67
+ if (documentFile.files.length === 0) {
68
+ console.log('์—…๋กœ๋“œํ•  ํŒŒ์ผ์ด ์„ ํƒ๋˜์ง€ ์•Š์Œ');
69
+ alert('ํŒŒ์ผ์„ ์„ ํƒํ•ด ์ฃผ์„ธ์š”.');
70
+ return;
71
+ }
72
+
73
+ console.log(`ํŒŒ์ผ ์—…๋กœ๋“œ ์‹œ์ž‘: ${documentFile.files[0].name}`);
74
+
75
+ // UI ์—…๋ฐ์ดํŠธ
76
+ uploadStatus.classList.remove('hidden');
77
+ uploadStatus.className = 'upload-status';
78
+ uploadStatus.innerHTML = '<div class="spinner"></div><p>์—…๋กœ๋“œ ์ค‘...</p>';
79
+ uploadButton.disabled = true;
80
+
81
+ try {
82
+ const formData = new FormData();
83
+ formData.append('document', documentFile.files[0]);
84
+
85
+ console.log('๋ฌธ์„œ ์—…๋กœ๋“œ API ์š”์ฒญ ์ „์†ก');
86
+ // API ์š”์ฒญ
87
+ const response = await fetch('/api/upload', {
88
+ method: 'POST',
89
+ body: formData
90
+ });
91
+
92
+ const data = await response.json();
93
+ console.log('๋ฌธ์„œ ์—…๋กœ๋“œ API ์‘๋‹ต ์ˆ˜์‹ ');
94
+
95
+ // ์‘๋‹ต ์ฒ˜๋ฆฌ
96
+ if (data.error) {
97
+ console.error('์—…๋กœ๋“œ ์˜ค๋ฅ˜:', data.error);
98
+ uploadStatus.className = 'upload-status error';
99
+ uploadStatus.textContent = `์˜ค๋ฅ˜: ${data.error}`;
100
+ } else if (data.warning) {
101
+ console.warn('์—…๋กœ๋“œ ๊ฒฝ๊ณ :', data.message);
102
+ uploadStatus.className = 'upload-status warning';
103
+ uploadStatus.textContent = data.message;
104
+ } else {
105
+ console.log('์—…๋กœ๋“œ ์„ฑ๊ณต:', data.message);
106
+ uploadStatus.className = 'upload-status success';
107
+ uploadStatus.textContent = data.message;
108
+
109
+ // ๋ฌธ์„œ ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ
110
+ loadDocuments();
111
+
112
+ // ์ž…๋ ฅ ํ•„๋“œ ์ดˆ๊ธฐํ™”
113
+ documentFile.value = '';
114
+ fileName.textContent = '์„ ํƒ๋œ ํŒŒ์ผ ์—†์Œ';
115
+ }
116
+ } catch (error) {
117
+ console.error('์—…๋กœ๋“œ ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜:', error);
118
+ uploadStatus.className = 'upload-status error';
119
+ uploadStatus.textContent = '์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.';
120
+ } finally {
121
+ uploadButton.disabled = false;
122
+ }
123
+ }
124
+
125
+ /**
126
+ * ๋ฌธ์„œ ๋ชฉ๋ก ๋กœ๋“œ ํ•จ์ˆ˜
127
+ */
128
+ async function loadDocuments() {
129
+ console.log('๋ฌธ์„œ ๋ชฉ๋ก ๋กœ๋“œ ์‹œ์ž‘');
130
+
131
+ // UI ์—…๋ฐ์ดํŠธ
132
+ docsList.querySelector('tbody').innerHTML = '';
133
+ docsLoading.classList.remove('hidden');
134
+ noDocsMessage.classList.add('hidden');
135
+
136
+ try {
137
+ console.log('๋ฌธ์„œ ๋ชฉ๋ก API ์š”์ฒญ ์ „์†ก');
138
+ // API ์š”์ฒญ
139
+ const response = await fetch('/api/documents');
140
+
141
+ if (!response.ok) {
142
+ throw new Error(`HTTP error! status: ${response.status}`);
143
+ }
144
+
145
+ const data = await response.json();
146
+ console.log(`๋ฌธ์„œ ๋ชฉ๋ก API ์‘๋‹ต ์ˆ˜์‹ : ${data.documents ? data.documents.length : 0}๊ฐœ ๋ฌธ์„œ`);
147
+
148
+ // ์‘๋‹ต ์ฒ˜๋ฆฌ
149
+ docsLoading.classList.add('hidden');
150
+
151
+ if (!data.documents || data.documents.length === 0) {
152
+ console.log('๋กœ๋“œ๋œ ๋ฌธ์„œ๊ฐ€ ์—†์Œ');
153
+ noDocsMessage.classList.remove('hidden');
154
+ return;
155
+ }
156
+
157
+ // ๋ฌธ์„œ ๋ชฉ๋ก ์—…๋ฐ์ดํŠธ
158
+ const tbody = docsList.querySelector('tbody');
159
+ data.documents.forEach(doc => {
160
+ console.log(`๋ฌธ์„œ ํ‘œ์‹œ: ${doc.filename || doc.source}`);
161
+
162
+ const row = document.createElement('tr');
163
+
164
+ const fileNameCell = document.createElement('td');
165
+ fileNameCell.textContent = doc.filename || doc.source;
166
+ row.appendChild(fileNameCell);
167
+
168
+ const chunksCell = document.createElement('td');
169
+ chunksCell.textContent = doc.chunks;
170
+ row.appendChild(chunksCell);
171
+
172
+ const typeCell = document.createElement('td');
173
+ typeCell.textContent = doc.filetype || '-';
174
+ row.appendChild(typeCell);
175
+
176
+ tbody.appendChild(row);
177
+ });
178
+
179
+ console.log('๋ฌธ์„œ ๋ชฉ๋ก ์—…๋ฐ์ดํŠธ ์™„๋ฃŒ');
180
+ } catch (error) {
181
+ console.error('๋ฌธ์„œ ๋ชฉ๋ก ๋กœ๋“œ ์˜ค๋ฅ˜:', error);
182
+ docsLoading.classList.add('hidden');
183
+ noDocsMessage.classList.remove('hidden');
184
+ noDocsMessage.querySelector('p').textContent = '๋ฌธ์„œ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.';
185
+ }
186
+ }
187
+
188
+ // ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™”
189
+ document.addEventListener('DOMContentLoaded', function() {
190
+ console.log('๋ฌธ์„œ ๊ด€๋ฆฌ ๋ชจ๋“ˆ ์ดˆ๊ธฐํ™”');
191
+
192
+ // ๋น„๋™๊ธฐ์ ์œผ๋กœ ์ดˆ๊ธฐํ™” (DOM ์š”์†Œ๊ฐ€ ์ค€๋น„๋œ ํ›„)
193
+ setTimeout(() => {
194
+ initDocsElements();
195
+ }, 100);
196
+ });
app/static/js/app-llm.js ADDED
@@ -0,0 +1,273 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /**
2
+ * RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ LLM ๊ด€๋ จ JavaScript
3
+ */
4
+
5
+ /**
6
+ * LLM ๋ชฉ๋ก ๋กœ๋“œ ํ•จ์ˆ˜
7
+ */
8
+ async function loadLLMs() {
9
+ try {
10
+ console.log('LLM ๋ชฉ๋ก ๋กœ๋“œ ์‹œ์ž‘');
11
+
12
+ // API ์š”์ฒญ
13
+ const response = await fetch('/api/llm');
14
+
15
+ if (!response.ok) {
16
+ throw new Error(`HTTP error! status: ${response.status}`);
17
+ }
18
+
19
+ const data = await response.json();
20
+ supportedLLMs = data.supported_llms;
21
+ currentLLM = data.current_llm.id;
22
+
23
+ console.log(`๋กœ๋“œ๋œ LLM ์ˆ˜: ${supportedLLMs.length}, ํ˜„์žฌ LLM: ${currentLLM}`);
24
+
25
+ // LLM ์„ ํƒ ๋“œ๋กญ๋‹ค์šด ์—…๋ฐ์ดํŠธ
26
+ llmSelect.innerHTML = '';
27
+ supportedLLMs.forEach(llm => {
28
+ const option = document.createElement('option');
29
+ option.value = llm.id;
30
+ option.textContent = llm.name;
31
+ option.selected = llm.current;
32
+ llmSelect.appendChild(option);
33
+ });
34
+
35
+ // ํ˜„์žฌ LLM ํ‘œ์‹œ
36
+ updateCurrentLLMInfo(data.current_llm);
37
+ console.log('LLM ๋ชฉ๋ก ๋กœ๋“œ ์™„๋ฃŒ');
38
+ } catch (error) {
39
+ console.error('LLM ๋ชฉ๋ก ๋กœ๋“œ ์‹คํŒจ:', error);
40
+ }
41
+ }
42
+
43
+ /**
44
+ * LLM ๋ณ€๊ฒฝ ํ•จ์ˆ˜
45
+ * @param {string} llmId - ๋ณ€๊ฒฝํ•  LLM ID
46
+ */
47
+ async function changeLLM(llmId) {
48
+ try {
49
+ console.log(`LLM ๋ณ€๊ฒฝ ์‹œ์ž‘: ${llmId}`);
50
+
51
+ // API ์š”์ฒญ
52
+ const response = await fetch('/api/llm', {
53
+ method: 'POST',
54
+ headers: {
55
+ 'Content-Type': 'application/json'
56
+ },
57
+ body: JSON.stringify({ llm_id: llmId })
58
+ });
59
+
60
+ if (!response.ok) {
61
+ throw new Error(`HTTP error! status: ${response.status}`);
62
+ }
63
+
64
+ const data = await response.json();
65
+
66
+ if (data.success) {
67
+ currentLLM = llmId;
68
+ updateCurrentLLMInfo(data.current_llm);
69
+ console.log(`LLM ๋ณ€๊ฒฝ ์„ฑ๊ณต: ${data.current_llm.name}`);
70
+
71
+ // ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
72
+ const systemMessage = `LLM์ด ${data.current_llm.name}(์œผ)๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋ธ: ${data.current_llm.model}`;
73
+ addSystemNotification(systemMessage);
74
+ } else if (data.error) {
75
+ console.error('LLM ๋ณ€๊ฒฝ ์˜ค๋ฅ˜:', data.error);
76
+ alert(`LLM ๋ณ€๊ฒฝ ์˜ค๋ฅ˜: ${data.error}`);
77
+ }
78
+ } catch (error) {
79
+ console.error('LLM ๋ณ€๊ฒฝ ์‹คํŒจ:', error);
80
+ alert('LLM ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
81
+ }
82
+ }
83
+
84
+ /**
85
+ * ํ˜„์žฌ LLM ์ •๋ณด ํ‘œ์‹œ ์—…๋ฐ์ดํŠธ
86
+ * @param {Object} llmInfo - LLM ์ •๋ณด ๊ฐ์ฒด
87
+ */
88
+ function updateCurrentLLMInfo(llmInfo) {
89
+ console.log(`ํ˜„์žฌ LLM ์ •๋ณด ์—…๋ฐ์ดํŠธ: ${llmInfo.name} (${llmInfo.model})`);
90
+
91
+ if (currentLLMInfo) {
92
+ currentLLMInfo.textContent = `${llmInfo.name} (${llmInfo.model})`;
93
+ }
94
+ }
95
+
96
+ /**
97
+ * ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ์ „์†ก ํ•จ์ˆ˜
98
+ */
99
+ async function sendMessage() {
100
+ const message = userInput.value.trim();
101
+ if (!message) return;
102
+
103
+ console.log(`๋ฉ”์‹œ์ง€ ์ „์†ก: "${message}"`);
104
+
105
+ // UI ์—…๋ฐ์ดํŠธ
106
+ addMessage(message, 'user');
107
+ userInput.value = '';
108
+ adjustTextareaHeight();
109
+
110
+ // ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
111
+ const loadingMessageId = addLoadingMessage();
112
+
113
+ try {
114
+ console.log('์ฑ„ํŒ… API ์š”์ฒญ ์‹œ์ž‘');
115
+
116
+ // API ์š”์ฒญ
117
+ const response = await fetch('/api/chat', {
118
+ method: 'POST',
119
+ headers: {
120
+ 'Content-Type': 'application/json'
121
+ },
122
+ body: JSON.stringify({
123
+ query: message,
124
+ llm_id: currentLLM // ํ˜„์žฌ ์„ ํƒ๋œ LLM ์ „์†ก
125
+ })
126
+ });
127
+
128
+ if (!response.ok) {
129
+ throw new Error(`HTTP error! status: ${response.status}`);
130
+ }
131
+
132
+ const data = await response.json();
133
+ console.log('์ฑ„ํŒ… API ์‘๋‹ต ์ˆ˜์‹  ์™„๋ฃŒ');
134
+
135
+ // ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ
136
+ removeLoadingMessage(loadingMessageId);
137
+
138
+ // ์‘๋‹ต ํ‘œ์‹œ
139
+ if (data.error) {
140
+ console.error('์ฑ„ํŒ… ์‘๋‹ต ์˜ค๋ฅ˜:', data.error);
141
+ addErrorMessage(data.error);
142
+ } else {
143
+ // LLM ์ •๋ณด ์—…๋ฐ์ดํŠธ
144
+ if (data.llm) {
145
+ updateCurrentLLMInfo(data.llm);
146
+ }
147
+ console.log('๋ด‡ ์‘๋‹ต ํ‘œ์‹œ');
148
+ addMessage(data.answer, 'bot', null, data.sources);
149
+ }
150
+ } catch (error) {
151
+ console.error('์ฑ„ํŒ… ์š”์ฒญ ์˜ค๋ฅ˜:', error);
152
+ removeLoadingMessage(loadingMessageId);
153
+ addErrorMessage('์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.');
154
+ }
155
+ }
156
+
157
+ /**
158
+ * ์Œ์„ฑ ๋…น์Œ ์‹œ์ž‘ ํ•จ์ˆ˜
159
+ */
160
+ async function startRecording() {
161
+ if (isRecording) return;
162
+
163
+ console.log('์Œ์„ฑ ๋…น์Œ ์‹œ์ž‘ ์š”์ฒญ');
164
+
165
+ try {
166
+ const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
167
+ isRecording = true;
168
+ audioChunks = [];
169
+
170
+ mediaRecorder = new MediaRecorder(stream);
171
+
172
+ mediaRecorder.addEventListener('dataavailable', (event) => {
173
+ if (event.data.size > 0) audioChunks.push(event.data);
174
+ console.log('์˜ค๋””์˜ค ๋ฐ์ดํ„ฐ ์ฒญํฌ ์ˆ˜์‹ ๋จ');
175
+ });
176
+
177
+ mediaRecorder.addEventListener('stop', sendAudioMessage);
178
+
179
+ // ๋…น์Œ ์‹œ์ž‘
180
+ mediaRecorder.start();
181
+ console.log('MediaRecorder ์‹œ์ž‘๋จ');
182
+
183
+ // UI ์—…๋ฐ์ดํŠธ
184
+ micButton.style.display = 'none';
185
+ recordingStatus.classList.remove('hidden');
186
+ } catch (error) {
187
+ console.error('์Œ์„ฑ ๋…น์Œ ๊ถŒํ•œ์„ ์–ป์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค:', error);
188
+ alert('๋งˆ์ดํฌ ์ ‘๊ทผ ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.');
189
+ }
190
+ }
191
+
192
+ /**
193
+ * ์Œ์„ฑ ๋…น์Œ ์ค‘์ง€ ํ•จ์ˆ˜
194
+ */
195
+ function stopRecording() {
196
+ if (!isRecording || !mediaRecorder) return;
197
+
198
+ console.log('์Œ์„ฑ ๋…น์Œ ์ค‘์ง€ ์š”์ฒญ');
199
+
200
+ mediaRecorder.stop();
201
+ isRecording = false;
202
+
203
+ // UI ์—…๋ฐ์ดํŠธ
204
+ micButton.style.display = 'flex';
205
+ recordingStatus.classList.add('hidden');
206
+
207
+ console.log('MediaRecorder ์ค‘์ง€๋จ');
208
+ }
209
+
210
+ /**
211
+ * ๋…น์Œ๋œ ์˜ค๋””์˜ค ๋ฉ”์‹œ์ง€ ์ „์†ก ํ•จ์ˆ˜
212
+ */
213
+ async function sendAudioMessage() {
214
+ if (audioChunks.length === 0) return;
215
+
216
+ console.log(`์˜ค๋””์˜ค ๋ฉ”์‹œ์ง€ ์ „์†ก ์ค€๋น„, ${audioChunks.length}๊ฐœ ์ฒญํฌ`);
217
+
218
+ // ์˜ค๋””์˜ค Blob ์ƒ์„ฑ
219
+ const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
220
+
221
+ // ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
222
+ const loadingMessageId = addLoadingMessage();
223
+
224
+ try {
225
+ // FormData์— ์˜ค๋””์˜ค ์ถ”๊ฐ€
226
+ const formData = new FormData();
227
+ formData.append('audio', audioBlob, 'recording.wav');
228
+ // ํ˜„์žฌ ์„ ํƒ๋œ LLM ์ถ”๊ฐ€
229
+ formData.append('llm_id', currentLLM);
230
+
231
+ console.log('์Œ์„ฑ API ์š”์ฒญ ์‹œ์ž‘');
232
+ // API ์š”์ฒญ
233
+ const response = await fetch('/api/voice', {
234
+ method: 'POST',
235
+ body: formData
236
+ });
237
+
238
+ if (!response.ok) {
239
+ throw new Error(`HTTP error! status: ${response.status}`);
240
+ }
241
+
242
+ const data = await response.json();
243
+ console.log('์Œ์„ฑ API ์‘๋‹ต ์ˆ˜์‹  ์™„๋ฃŒ');
244
+
245
+ // ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ
246
+ removeLoadingMessage(loadingMessageId);
247
+
248
+ // ์‘๋‹ต ํ‘œ์‹œ
249
+ if (data.error) {
250
+ console.error('์Œ์„ฑ ์‘๋‹ต ์˜ค๋ฅ˜:', data.error);
251
+ addErrorMessage(data.error);
252
+ } else {
253
+ // LLM ์ •๋ณด ์—…๋ฐ์ดํŠธ
254
+ if (data.llm) {
255
+ updateCurrentLLMInfo(data.llm);
256
+ }
257
+
258
+ // ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€(์Œ์„ฑ ํ…์ŠคํŠธ) ์ถ”๊ฐ€
259
+ if (data.transcription) {
260
+ console.log(`์Œ์„ฑ ์ธ์‹ ๊ฒฐ๊ณผ: "${data.transcription}"`);
261
+ addMessage(data.transcription, 'user');
262
+ }
263
+
264
+ // ๋ด‡ ์‘๋‹ต ์ถ”๊ฐ€
265
+ console.log('๋ด‡ ์‘๋‹ต ํ‘œ์‹œ');
266
+ addMessage(data.answer, 'bot', data.transcription, data.sources);
267
+ }
268
+ } catch (error) {
269
+ console.error('์Œ์„ฑ ์š”์ฒญ ์˜ค๋ฅ˜:', error);
270
+ removeLoadingMessage(loadingMessageId);
271
+ addErrorMessage('์˜ค๋””์˜ค ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.');
272
+ }
273
+ }
app/static/js/app.js CHANGED
@@ -1,636 +1,58 @@
1
  /**
2
- * RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ UI JavaScript
 
3
  */
4
 
5
- // DOM ์š”์†Œ
6
- const chatTab = document.getElementById('chatTab');
7
- const docsTab = document.getElementById('docsTab');
8
- const chatSection = document.getElementById('chatSection');
9
- const docsSection = document.getElementById('docsSection');
10
- const chatMessages = document.getElementById('chatMessages');
11
- const userInput = document.getElementById('userInput');
12
- const sendButton = document.getElementById('sendButton');
13
- const micButton = document.getElementById('micButton');
14
- const stopRecordingButton = document.getElementById('stopRecordingButton');
15
- const recordingStatus = document.getElementById('recordingStatus');
16
- const uploadForm = document.getElementById('uploadForm');
17
- const documentFile = document.getElementById('documentFile');
18
- const fileName = document.getElementById('fileName');
19
- const uploadButton = document.getElementById('uploadButton');
20
- const uploadStatus = document.getElementById('uploadStatus');
21
- const refreshDocsButton = document.getElementById('refreshDocsButton');
22
- const docsList = document.getElementById('docsList');
23
- const docsLoading = document.getElementById('docsLoading');
24
- const noDocsMessage = document.getElementById('noDocsMessage');
25
- const llmSelect = document.getElementById('llmSelect');
26
- const currentLLMInfo = document.getElementById('currentLLMInfo');
27
-
28
- // LLM ๊ด€๋ จ ๋ณ€์ˆ˜
29
- let currentLLM = 'openai';
30
- let supportedLLMs = [];
31
-
32
- // ๋…น์Œ ๊ด€๋ จ ๋ณ€์ˆ˜
33
- let mediaRecorder = null;
34
- let audioChunks = [];
35
- let isRecording = false;
36
-
37
- // ์•ฑ ์ดˆ๊ธฐํ™” ์ƒํƒœ ํ™•์ธ ํ•จ์ˆ˜
38
- async function checkAppStatus() {
39
- try {
40
- const response = await fetch('/api/status');
41
- if (!response.ok) {
42
- return false;
43
- }
44
- const data = await response.json();
45
- return data.ready;
46
- } catch (error) {
47
- console.error('Status check failed:', error);
48
- return false;
49
- }
50
- }
51
-
52
- /**
53
- * LLM ๋ชฉ๋ก ๋กœ๋“œ ํ•จ์ˆ˜
54
- */
55
- async function loadLLMs() {
56
- try {
57
- // API ์š”์ฒญ
58
- const response = await fetch('/api/llm');
59
-
60
- if (!response.ok) {
61
- throw new Error(`HTTP error! status: ${response.status}`);
62
- }
63
-
64
- const data = await response.json();
65
- supportedLLMs = data.supported_llms;
66
- currentLLM = data.current_llm.id;
67
-
68
- // LLM ์„ ํƒ ๋“œ๋กญ๋‹ค์šด ์—…๋ฐ์ดํŠธ
69
- llmSelect.innerHTML = '';
70
- supportedLLMs.forEach(llm => {
71
- const option = document.createElement('option');
72
- option.value = llm.id;
73
- option.textContent = llm.name;
74
- option.selected = llm.current;
75
- llmSelect.appendChild(option);
76
- });
77
-
78
- // ํ˜„์žฌ LLM ํ‘œ์‹œ
79
- updateCurrentLLMInfo(data.current_llm);
80
- } catch (error) {
81
- console.error('LLM ๋ชฉ๋ก ๋กœ๋“œ ์‹คํŒจ:', error);
82
- }
83
- }
84
-
85
- /**
86
- * LLM ๋ณ€๊ฒฝ ํ•จ์ˆ˜
87
- * @param {string} llmId - ๋ณ€๊ฒฝํ•  LLM ID
88
- */
89
- async function changeLLM(llmId) {
90
- try {
91
- // API ์š”์ฒญ
92
- const response = await fetch('/api/llm', {
93
- method: 'POST',
94
- headers: {
95
- 'Content-Type': 'application/json'
96
- },
97
- body: JSON.stringify({ llm_id: llmId })
98
- });
99
-
100
- if (!response.ok) {
101
- throw new Error(`HTTP error! status: ${response.status}`);
102
- }
103
-
104
- const data = await response.json();
105
-
106
- if (data.success) {
107
- currentLLM = llmId;
108
- updateCurrentLLMInfo(data.current_llm);
109
- console.log(`LLM์ด ${data.current_llm.name}(์œผ)๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค.`);
110
-
111
- // ์‹œ์Šคํ…œ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
112
- const systemMessage = `LLM์ด ${data.current_llm.name}(์œผ)๋กœ ๋ณ€๊ฒฝ๋˜์—ˆ์Šต๋‹ˆ๋‹ค. ๋ชจ๋ธ: ${data.current_llm.model}`;
113
- addSystemNotification(systemMessage);
114
- } else if (data.error) {
115
- console.error('LLM ๋ณ€๊ฒฝ ์˜ค๋ฅ˜:', data.error);
116
- alert(`LLM ๋ณ€๊ฒฝ ์˜ค๋ฅ˜: ${data.error}`);
117
- }
118
- } catch (error) {
119
- console.error('LLM ๋ณ€๊ฒฝ ์‹คํŒจ:', error);
120
- alert('LLM ๋ณ€๊ฒฝ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.');
121
- }
122
- }
123
-
124
- /**
125
- * ํ˜„์žฌ LLM ์ •๋ณด ํ‘œ์‹œ ์—…๋ฐ์ดํŠธ
126
- * @param {Object} llmInfo - LLM ์ •๋ณด ๊ฐ์ฒด
127
- */
128
- function updateCurrentLLMInfo(llmInfo) {
129
- if (currentLLMInfo) {
130
- currentLLMInfo.textContent = `${llmInfo.name} (${llmInfo.model})`;
131
- }
132
- }
133
-
134
- /**
135
- * ์‹œ์Šคํ…œ ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
136
- * @param {string} message - ์•Œ๋ฆผ ๋ฉ”์‹œ์ง€
137
- */
138
- function addSystemNotification(message) {
139
- const messageDiv = document.createElement('div');
140
- messageDiv.classList.add('message', 'system');
141
-
142
- const contentDiv = document.createElement('div');
143
- contentDiv.classList.add('message-content');
144
-
145
- const messageP = document.createElement('p');
146
- messageP.innerHTML = `<i class="fas fa-info-circle"></i> ${message}`;
147
- contentDiv.appendChild(messageP);
148
-
149
- messageDiv.appendChild(contentDiv);
150
- chatMessages.appendChild(messageDiv);
151
-
152
- // ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๋กœ ์ด๋™
153
- chatMessages.scrollTop = chatMessages.scrollHeight;
154
- }
155
-
156
- // ํŽ˜์ด์ง€ ๋กœ๋“œ ์‹œ ์ดˆ๊ธฐํ™”
157
- document.addEventListener('DOMContentLoaded', () => {
158
- // ์•ฑ ์ƒํƒœ ํ™•์ธ (๋กœ๋”ฉ ํŽ˜์ด์ง€๊ฐ€ ์•„๋‹Œ ๊ฒฝ์šฐ์—๋งŒ)
159
- if (window.location.pathname === '/' && !document.getElementById('app-loading-indicator')) {
160
- // ์•ฑ ์ƒํƒœ ์ฃผ๊ธฐ์ ์œผ๋กœ ํ™•์ธ
161
- const statusInterval = setInterval(async () => {
162
- const isReady = await checkAppStatus();
163
- if (isReady) {
164
- clearInterval(statusInterval);
165
- console.log('์•ฑ์ด ์ค€๋น„๋˜์—ˆ์Šต๋‹ˆ๋‹ค.');
166
-
167
- // ์•ฑ์ด ์ค€๋น„๋˜๋ฉด LLM ๋ชฉ๋ก ๋กœ๋“œ
168
- loadLLMs();
169
- }
170
- }, 5000);
171
- }
172
-
173
- // ํƒญ ์ „ํ™˜ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
174
- chatTab.addEventListener('click', () => {
175
- switchTab('chat');
176
- });
177
-
178
- docsTab.addEventListener('click', () => {
179
- switchTab('docs');
180
- loadDocuments();
181
- });
182
-
183
- // LLM ์„ ํƒ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
184
- llmSelect.addEventListener('change', (event) => {
185
- changeLLM(event.target.value);
186
- });
187
-
188
- // ๋ฉ”์‹œ์ง€ ์ „์†ก ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
189
- sendButton.addEventListener('click', sendMessage);
190
- userInput.addEventListener('keydown', (event) => {
191
- if (event.key === 'Enter' && !event.shiftKey) {
192
- event.preventDefault();
193
- sendMessage();
194
- }
195
- });
196
-
197
- // ์Œ์„ฑ ์ธ์‹ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
198
- micButton.addEventListener('click', startRecording);
199
- stopRecordingButton.addEventListener('click', stopRecording);
200
-
201
- // ๋ฌธ์„œ ์—…๋กœ๋“œ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
202
- documentFile.addEventListener('change', (event) => {
203
- if (event.target.files.length > 0) {
204
- fileName.textContent = event.target.files[0].name;
205
- } else {
206
- fileName.textContent = '์„ ํƒ๋œ ํŒŒ์ผ ์—†์Œ';
207
- }
208
- });
209
-
210
- uploadForm.addEventListener('submit', (event) => {
211
- event.preventDefault();
212
- uploadDocument();
213
- });
214
-
215
- // ๋ฌธ์„œ ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ ์ด๋ฒคํŠธ ๋ฆฌ์Šค๋„ˆ
216
- refreshDocsButton.addEventListener('click', loadDocuments);
217
-
218
- // ์ž๋™ ์ž…๋ ฅ ํ•„๋“œ ํฌ๊ธฐ ์กฐ์ •
219
- userInput.addEventListener('input', adjustTextareaHeight);
220
-
221
- // ์ดˆ๊ธฐ ๋ฌธ์„œ ๋ชฉ๋ก ๋กœ๋“œ
222
- if (docsSection.classList.contains('active')) {
223
- loadDocuments();
224
- }
225
- });
226
-
227
- /**
228
- * ํƒญ ์ „ํ™˜ ํ•จ์ˆ˜
229
- * @param {string} tabName - ํ™œ์„ฑํ™”ํ•  ํƒญ ์ด๋ฆ„ ('chat' ๋˜๋Š” 'docs')
230
- */
231
- function switchTab(tabName) {
232
- if (tabName === 'chat') {
233
- chatTab.classList.add('active');
234
- docsTab.classList.remove('active');
235
- chatSection.classList.add('active');
236
- docsSection.classList.remove('active');
237
- } else if (tabName === 'docs') {
238
- chatTab.classList.remove('active');
239
- docsTab.classList.add('active');
240
- chatSection.classList.remove('active');
241
- docsSection.classList.add('active');
242
- }
243
- }
244
-
245
- /**
246
- * ์ฑ„ํŒ… ๋ฉ”์‹œ์ง€ ์ „์†ก ํ•จ์ˆ˜
247
- */
248
- async function sendMessage() {
249
- const message = userInput.value.trim();
250
- if (!message) return;
251
-
252
- // UI ์—…๋ฐ์ดํŠธ
253
- addMessage(message, 'user');
254
- userInput.value = '';
255
- adjustTextareaHeight();
256
-
257
- // ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
258
- const loadingMessageId = addLoadingMessage();
259
 
260
- try {
261
- // API ์š”์ฒญ
262
- const response = await fetch('/api/chat', {
263
- method: 'POST',
264
- headers: {
265
- 'Content-Type': 'application/json'
266
- },
267
- body: JSON.stringify({
268
- query: message,
269
- llm_id: currentLLM // ํ˜„์žฌ ์„ ํƒ๋œ LLM ์ „์†ก
270
- })
271
- });
272
-
273
- if (!response.ok) {
274
- throw new Error(`HTTP error! status: ${response.status}`);
275
- }
276
-
277
- const data = await response.json();
278
-
279
- // ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ
280
- removeLoadingMessage(loadingMessageId);
281
-
282
- // ์‘๋‹ต ํ‘œ์‹œ
283
- if (data.error) {
284
- addErrorMessage(data.error);
285
- } else {
286
- // LLM ์ •๋ณด ์—…๋ฐ์ดํŠธ
287
- if (data.llm) {
288
- updateCurrentLLMInfo(data.llm);
289
- }
290
- addMessage(data.answer, 'bot', null, data.sources);
291
- }
292
- } catch (error) {
293
- console.error('Error:', error);
294
- removeLoadingMessage(loadingMessageId);
295
- addErrorMessage('์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.');
296
- }
297
- }
298
-
299
- /**
300
- * ์Œ์„ฑ ๋…น์Œ ์‹œ์ž‘ ํ•จ์ˆ˜
301
- */
302
- async function startRecording() {
303
- if (isRecording) return;
304
 
305
- try {
306
- const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
307
- isRecording = true;
308
- audioChunks = [];
309
-
310
- mediaRecorder = new MediaRecorder(stream);
311
-
312
- mediaRecorder.addEventListener('dataavailable', (event) => {
313
- if (event.data.size > 0) audioChunks.push(event.data);
314
- });
315
-
316
- mediaRecorder.addEventListener('stop', sendAudioMessage);
317
-
318
- // ๋…น์Œ ์‹œ์ž‘
319
- mediaRecorder.start();
320
-
321
- // UI ์—…๋ฐ์ดํŠธ
322
- micButton.style.display = 'none';
323
- recordingStatus.classList.remove('hidden');
324
-
325
- console.log('๋…น์Œ ์‹œ์ž‘๋จ');
326
- } catch (error) {
327
- console.error('์Œ์„ฑ ๋…น์Œ ๊ถŒํ•œ์„ ์–ป์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค:', error);
328
- alert('๋งˆ์ดํฌ ์ ‘๊ทผ ๊ถŒํ•œ์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.');
329
  }
330
- }
331
-
332
- /**
333
- * ์Œ์„ฑ ๋…น์Œ ์ค‘์ง€ ํ•จ์ˆ˜
334
- */
335
- function stopRecording() {
336
- if (!isRecording || !mediaRecorder) return;
337
-
338
- mediaRecorder.stop();
339
- isRecording = false;
340
 
341
- // UI ์—…๋ฐ์ดํŠธ
342
- micButton.style.display = 'flex';
343
- recordingStatus.classList.add('hidden');
 
344
 
345
- console.log('๋…น์Œ ์ค‘์ง€๋จ');
346
- }
347
-
348
- /**
349
- * ๋…น์Œ๋œ ์˜ค๋””์˜ค ๋ฉ”์‹œ์ง€ ์ „์†ก ํ•จ์ˆ˜
350
- */
351
- async function sendAudioMessage() {
352
- if (audioChunks.length === 0) return;
353
-
354
- // ์˜ค๋””์˜ค Blob ์ƒ์„ฑ
355
- const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
356
-
357
- // ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€
358
- const loadingMessageId = addLoadingMessage();
359
-
360
- try {
361
- // FormData์— ์˜ค๋””์˜ค ์ถ”๊ฐ€
362
- const formData = new FormData();
363
- formData.append('audio', audioBlob, 'recording.wav');
364
- // ํ˜„์žฌ ์„ ํƒ๋œ LLM ์ถ”๊ฐ€
365
- formData.append('llm_id', currentLLM);
366
-
367
- // API ์š”์ฒญ
368
- const response = await fetch('/api/voice', {
369
- method: 'POST',
370
- body: formData
371
- });
372
-
373
- if (!response.ok) {
374
- throw new Error(`HTTP error! status: ${response.status}`);
375
- }
376
-
377
- const data = await response.json();
378
-
379
- // ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ
380
- removeLoadingMessage(loadingMessageId);
381
-
382
- // ์‘๋‹ต ํ‘œ์‹œ
383
- if (data.error) {
384
- addErrorMessage(data.error);
385
- } else {
386
- // LLM ์ •๋ณด ์—…๋ฐ์ดํŠธ
387
- if (data.llm) {
388
- updateCurrentLLMInfo(data.llm);
389
- }
390
-
391
- // ์‚ฌ์šฉ์ž ๋ฉ”์‹œ์ง€(์Œ์„ฑ ํ…์ŠคํŠธ) ์ถ”๊ฐ€
392
- if (data.transcription) {
393
- addMessage(data.transcription, 'user');
394
- }
395
-
396
- // ๋ด‡ ์‘๋‹ต ์ถ”๊ฐ€
397
- addMessage(data.answer, 'bot', data.transcription, data.sources);
398
- }
399
- } catch (error) {
400
- console.error('Error:', error);
401
- removeLoadingMessage(loadingMessageId);
402
- addErrorMessage('์˜ค๋””์˜ค ์ฒ˜๋ฆฌ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.');
403
- }
404
  }
405
 
406
- /**
407
- * ๋ฌธ์„œ ์—…๋กœ๋“œ ํ•จ์ˆ˜
408
- */
409
- async function uploadDocument() {
410
- if (documentFile.files.length === 0) {
411
- alert('ํŒŒ์ผ์„ ์„ ํƒํ•ด ์ฃผ์„ธ์š”.');
412
- return;
413
- }
414
 
415
- // UI ์—…๋ฐ์ดํŠธ
416
- uploadStatus.classList.remove('hidden');
417
- uploadStatus.className = 'upload-status';
418
- uploadStatus.innerHTML = '<div class="spinner"></div><p>์—…๋กœ๋“œ ์ค‘...</p>';
419
- uploadButton.disabled = true;
420
 
421
- try {
422
- const formData = new FormData();
423
- formData.append('document', documentFile.files[0]);
424
 
425
- // API ์š”์ฒญ
426
- const response = await fetch('/api/upload', {
427
- method: 'POST',
428
- body: formData
429
- });
430
-
431
- const data = await response.json();
432
-
433
- // ์‘๋‹ต ์ฒ˜๋ฆฌ
434
- if (data.error) {
435
- uploadStatus.className = 'upload-status error';
436
- uploadStatus.textContent = `์˜ค๋ฅ˜: ${data.error}`;
437
- } else if (data.warning) {
438
- uploadStatus.className = 'upload-status warning';
439
- uploadStatus.textContent = data.message;
440
- } else {
441
- uploadStatus.className = 'upload-status success';
442
- uploadStatus.textContent = data.message;
443
-
444
- // ๋ฌธ์„œ ๋ชฉ๋ก ์ƒˆ๋กœ๊ณ ์นจ
445
- loadDocuments();
446
-
447
- // ์ž…๋ ฅ ํ•„๋“œ ์ดˆ๊ธฐํ™”
448
- documentFile.value = '';
449
- fileName.textContent = '์„ ํƒ๋œ ํŒŒ์ผ ์—†์Œ';
450
- }
451
- } catch (error) {
452
- console.error('Error:', error);
453
- uploadStatus.className = 'upload-status error';
454
- uploadStatus.textContent = '์—…๋กœ๋“œ ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค. ๋‹ค์‹œ ์‹œ๋„ํ•ด ์ฃผ์„ธ์š”.';
455
- } finally {
456
- uploadButton.disabled = false;
457
- }
458
- }
459
-
460
- /**
461
- * ๋ฌธ์„œ ๋ชฉ๋ก ๋กœ๋“œ ํ•จ์ˆ˜
462
- */
463
- async function loadDocuments() {
464
- // UI ์—…๋ฐ์ดํŠธ
465
- docsList.querySelector('tbody').innerHTML = '';
466
- docsLoading.classList.remove('hidden');
467
- noDocsMessage.classList.add('hidden');
468
-
469
- try {
470
- // API ์š”์ฒญ
471
- const response = await fetch('/api/documents');
472
-
473
- if (!response.ok) {
474
- throw new Error(`HTTP error! status: ${response.status}`);
475
- }
476
-
477
- const data = await response.json();
478
-
479
- // ์‘๋‹ต ์ฒ˜๋ฆฌ
480
- docsLoading.classList.add('hidden');
481
-
482
- if (!data.documents || data.documents.length === 0) {
483
- noDocsMessage.classList.remove('hidden');
484
- return;
485
- }
486
-
487
- // ๋ฌธ์„œ ๋ชฉ๋ก ์—…๋ฐ์ดํŠธ
488
- const tbody = docsList.querySelector('tbody');
489
- data.documents.forEach(doc => {
490
- const row = document.createElement('tr');
491
-
492
- const fileNameCell = document.createElement('td');
493
- fileNameCell.textContent = doc.filename || doc.source;
494
- row.appendChild(fileNameCell);
495
 
496
- const chunksCell = document.createElement('td');
497
- chunksCell.textContent = doc.chunks;
498
- row.appendChild(chunksCell);
499
-
500
- const typeCell = document.createElement('td');
501
- typeCell.textContent = doc.filetype || '-';
502
- row.appendChild(typeCell);
503
-
504
- tbody.appendChild(row);
505
- });
506
- } catch (error) {
507
- console.error('Error:', error);
508
- docsLoading.classList.add('hidden');
509
- noDocsMessage.classList.remove('hidden');
510
- noDocsMessage.querySelector('p').textContent = '๋ฌธ์„œ ๋ชฉ๋ก์„ ๋ถˆ๋Ÿฌ์˜ค๋Š” ์ค‘ ์˜ค๋ฅ˜๊ฐ€ ๋ฐœ์ƒํ–ˆ์Šต๋‹ˆ๋‹ค.';
511
- }
512
- }
513
-
514
- /**
515
- * ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ ํ•จ์ˆ˜
516
- * @param {string} text - ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ
517
- * @param {string} sender - ๋ฉ”์‹œ์ง€ ๋ฐœ์‹ ์ž ('user' ๋˜๋Š” 'bot' ๋˜๋Š” 'system')
518
- * @param {string|null} transcription - ์Œ์„ฑ ์ธ์‹ ํ…์ŠคํŠธ (์„ ํƒ ์‚ฌํ•ญ)
519
- * @param {Array|null} sources - ์†Œ์Šค ์ •๋ณด ๋ฐฐ์—ด (์„ ํƒ ์‚ฌํ•ญ)
520
- */
521
- function addMessage(text, sender, transcription = null, sources = null) {
522
- const messageDiv = document.createElement('div');
523
- messageDiv.classList.add('message', sender);
524
-
525
- const contentDiv = document.createElement('div');
526
- contentDiv.classList.add('message-content');
527
-
528
- // ์Œ์„ฑ ์ธ์‹ ํ…์ŠคํŠธ ์ถ”๊ฐ€ (์žˆ๋Š” ๊ฒฝ์šฐ)
529
- if (transcription && sender === 'bot') {
530
- const transcriptionP = document.createElement('p');
531
- transcriptionP.classList.add('transcription');
532
- transcriptionP.textContent = `"${transcription}"`;
533
- contentDiv.appendChild(transcriptionP);
534
- }
535
-
536
- // ๋ฉ”์‹œ์ง€ ํ…์ŠคํŠธ ์ถ”๊ฐ€
537
- const textP = document.createElement('p');
538
- textP.textContent = text;
539
- contentDiv.appendChild(textP);
540
-
541
- // ์†Œ์Šค ์ •๋ณด ์ถ”๊ฐ€ (์žˆ๋Š” ๊ฒฝ์šฐ)
542
- if (sources && sources.length > 0 && sender === 'bot') {
543
- const sourcesDiv = document.createElement('div');
544
- sourcesDiv.classList.add('sources');
545
-
546
- const sourcesTitle = document.createElement('strong');
547
- sourcesTitle.textContent = '์ถœ์ฒ˜: ';
548
- sourcesDiv.appendChild(sourcesTitle);
549
-
550
- sources.forEach((source, index) => {
551
- if (index < 3) { // ์ตœ๋Œ€ 3๊ฐœ๊นŒ์ง€๋งŒ ํ‘œ์‹œ
552
- const sourceSpan = document.createElement('span');
553
- sourceSpan.classList.add('source-item');
554
- sourceSpan.textContent = source.source;
555
- sourcesDiv.appendChild(sourceSpan);
556
- }
557
  });
558
-
559
- contentDiv.appendChild(sourcesDiv);
560
- }
561
-
562
- messageDiv.appendChild(contentDiv);
563
- chatMessages.appendChild(messageDiv);
564
-
565
- // ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๏ฟฝ๏ฟฝ ์ด๋™
566
- chatMessages.scrollTop = chatMessages.scrollHeight;
567
- }
568
-
569
- /**
570
- * ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ ํ•จ์ˆ˜
571
- * @returns {string} ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ID
572
- */
573
- function addLoadingMessage() {
574
- const id = 'loading-' + Date.now();
575
- const messageDiv = document.createElement('div');
576
- messageDiv.classList.add('message', 'bot');
577
- messageDiv.id = id;
578
-
579
- const contentDiv = document.createElement('div');
580
- contentDiv.classList.add('message-content');
581
-
582
- const loadingP = document.createElement('p');
583
- loadingP.innerHTML = '<div class="spinner" style="width: 20px; height: 20px; display: inline-block; margin-right: 10px;"></div> ์ƒ๊ฐ ์ค‘...';
584
- contentDiv.appendChild(loadingP);
585
-
586
- messageDiv.appendChild(contentDiv);
587
- chatMessages.appendChild(messageDiv);
588
-
589
- // ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๋กœ ์ด๋™
590
- chatMessages.scrollTop = chatMessages.scrollHeight;
591
-
592
- return id;
593
- }
594
-
595
- /**
596
- * ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ์ œ๊ฑฐ ํ•จ์ˆ˜
597
- * @param {string} id - ๋กœ๋”ฉ ๋ฉ”์‹œ์ง€ ID
598
- */
599
- function removeLoadingMessage(id) {
600
- const loadingMessage = document.getElementById(id);
601
- if (loadingMessage) {
602
- loadingMessage.remove();
603
- }
604
- }
605
-
606
- /**
607
- * ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ์ถ”๊ฐ€ ํ•จ์ˆ˜
608
- * @param {string} errorText - ์˜ค๋ฅ˜ ๋ฉ”์‹œ์ง€ ๋‚ด์šฉ
609
- */
610
- function addErrorMessage(errorText) {
611
- const messageDiv = document.createElement('div');
612
- messageDiv.classList.add('message', 'system');
613
-
614
- const contentDiv = document.createElement('div');
615
- contentDiv.classList.add('message-content');
616
- contentDiv.style.backgroundColor = 'rgba(239, 68, 68, 0.1)';
617
- contentDiv.style.color = 'var(--error-color)';
618
-
619
- const errorP = document.createElement('p');
620
- errorP.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${errorText}`;
621
- contentDiv.appendChild(errorP);
622
-
623
- messageDiv.appendChild(contentDiv);
624
- chatMessages.appendChild(messageDiv);
625
-
626
- // ์Šคํฌ๋กค์„ ๊ฐ€์žฅ ์•„๋ž˜๋กœ ์ด๋™
627
- chatMessages.scrollTop = chatMessages.scrollHeight;
628
- }
629
-
630
- /**
631
- * textarea ๋†’์ด ์ž๋™ ์กฐ์ • ํ•จ์ˆ˜
632
- */
633
- function adjustTextareaHeight() {
634
- userInput.style.height = 'auto';
635
- userInput.style.height = Math.min(userInput.scrollHeight, 100) + 'px';
636
- }
 
1
  /**
2
+ * RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡ UI JavaScript ๋ฉ”์ธ ํŒŒ์ผ
3
+ * ์ด ํŒŒ์ผ์€ ๋ชจ๋“ˆํ™”๋œ JS ํŒŒ์ผ๋“ค์„ ๋กœ๋“œํ•˜๋Š” ์—ญํ• ์„ ํ•ฉ๋‹ˆ๋‹ค.
4
  */
5
 
6
+ // ์Šคํฌ๋ฆฝํŠธ ๋™์  ๋กœ๋“œ ํ•จ์ˆ˜
7
+ function loadScript(url, callback = null) {
8
+ console.log(`์Šคํฌ๋ฆฝํŠธ ๋กœ๋“œ ์ค‘: ${url}`);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
9
 
10
+ const script = document.createElement('script');
11
+ script.type = 'text/javascript';
12
+ script.src = url;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
13
 
14
+ // ์ฝœ๋ฐฑ ํ•จ์ˆ˜ ์„ค์ • (์žˆ๋Š” ๊ฒฝ์šฐ)
15
+ if (callback) {
16
+ script.onload = callback;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  }
 
 
 
 
 
 
 
 
 
 
18
 
19
+ // ์—๋Ÿฌ ํ•ธ๋“ค๋ง
20
+ script.onerror = function() {
21
+ console.error(`์Šคํฌ๋ฆฝํŠธ ๋กœ๋“œ ์‹คํŒจ: ${url}`);
22
+ };
23
 
24
+ // ํŽ˜์ด์ง€์— ์Šคํฌ๋ฆฝํŠธ ์ถ”๊ฐ€
25
+ document.head.appendChild(script);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
26
  }
27
 
28
+ // ๋ชจ๋“ˆ ๋กœ๋“œ ์ˆœ์„œ ๊ด€๋ฆฌ
29
+ document.addEventListener('DOMContentLoaded', function() {
30
+ console.log('์•ฑ ์ดˆ๊ธฐํ™” ์‹œ์ž‘: ๋ชจ๋“ˆ ๋กœ๋“œ');
 
 
 
 
 
31
 
32
+ // ๋ชจ๋“ˆ ํŒŒ์ผ ๊ฒฝ๋กœ
33
+ const coreScriptPath = '/static/js/app-core.js';
34
+ const llmScriptPath = '/static/js/app-llm.js';
35
+ const docsScriptPath = '/static/js/app-docs.js';
36
+ const deviceScriptPath = '/static/js/app-device.js';
37
 
38
+ // ๋ชจ๋“ˆ ์ˆœ์ฐจ์  ๋กœ๋“œ
39
+ loadScript(coreScriptPath, function() {
40
+ console.log('์ฝ”์–ด ๋ชจ๋“ˆ ๋กœ๋“œ ์™„๋ฃŒ');
41
 
42
+ // LLM ๋ชจ๋“ˆ ๋กœ๋“œ
43
+ loadScript(llmScriptPath, function() {
44
+ console.log('LLM ๋ชจ๋“ˆ ๋กœ๋“œ ์™„๋ฃŒ');
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
45
 
46
+ // ๋ฌธ์„œ ๊ด€๋ฆฌ ๋ชจ๋“ˆ ๋กœ๋“œ
47
+ loadScript(docsScriptPath, function() {
48
+ console.log('๋ฌธ์„œ ๊ด€๋ฆฌ ๋ชจ๋“ˆ ๋กœ๋“œ ์™„๋ฃŒ');
49
+
50
+ // ์žฅ์น˜ ๊ด€๋ฆฌ ๋ชจ๋“ˆ ๋กœ๋“œ
51
+ loadScript(deviceScriptPath, function() {
52
+ console.log('์žฅ์น˜ ๊ด€๋ฆฌ ๋ชจ๋“ˆ ๋กœ๋“œ ์™„๋ฃŒ');
53
+ console.log('๋ชจ๋“  ๋ชจ๋“ˆ ๋กœ๋“œ ์™„๋ฃŒ');
54
+ });
55
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  });
57
+ });
58
+ });
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
app/templates/index.html CHANGED
@@ -6,6 +6,7 @@
6
  <title>RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡</title>
7
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
8
  <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
 
9
  </head>
10
  <body>
11
  <div class="container">
@@ -28,6 +29,7 @@
28
  <div class="tabs">
29
  <button id="chatTab" class="tab active">๋Œ€ํ™”</button>
30
  <button id="docsTab" class="tab">๋ฌธ์„œ๊ด€๋ฆฌ</button>
 
31
  </div>
32
  </header>
33
 
@@ -119,6 +121,51 @@
119
  </div>
120
  </div>
121
  </section>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  </main>
123
 
124
  <footer>
 
6
  <title>RAG ๊ฒ€์ƒ‰ ์ฑ—๋ด‡</title>
7
  <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0-beta3/css/all.min.css">
8
  <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
9
+ <link rel="stylesheet" href="{{ url_for('static', filename='css/device-style.css') }}">
10
  </head>
11
  <body>
12
  <div class="container">
 
29
  <div class="tabs">
30
  <button id="chatTab" class="tab active">๋Œ€ํ™”</button>
31
  <button id="docsTab" class="tab">๋ฌธ์„œ๊ด€๋ฆฌ</button>
32
+ <button id="deviceTab" class="tab">์žฅ์น˜๊ด€๋ฆฌ</button>
33
  </div>
34
  </header>
35
 
 
121
  </div>
122
  </div>
123
  </section>
124
+
125
+ <!-- ์žฅ์น˜๊ด€๋ฆฌ ํƒญ -->
126
+ <section id="deviceSection" class="tab-content">
127
+ <div class="device-container">
128
+ <div class="device-section">
129
+ <div class="device-toolbar">
130
+ <h2>์žฅ์น˜ ์ƒํƒœ</h2>
131
+ <button id="deviceRefreshButton" class="refresh-device-btn">
132
+ <i class="fas fa-sync-alt"></i> ์ƒˆ๋กœ๊ณ ์นจ
133
+ </button>
134
+ </div>
135
+
136
+ <div id="deviceStatus">
137
+ <!-- ์žฅ์น˜ ์„œ๋ฒ„ ์ƒํƒœ๊ฐ€ ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค -->
138
+ <div class="loading-device">
139
+ <div class="spinner"></div>
140
+ <p>์žฅ์น˜ ์ƒํƒœ ํ™•์ธ ์ค‘...</p>
141
+ </div>
142
+ </div>
143
+
144
+ <div id="deviceList">
145
+ <!-- ์žฅ์น˜ ๋ชฉ๋ก์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค -->
146
+ </div>
147
+ </div>
148
+
149
+ <div class="programs-section">
150
+ <div class="device-toolbar">
151
+ <h2>ํ”„๋กœ๊ทธ๋žจ ์‹คํ–‰</h2>
152
+ <button id="loadProgramsButton" class="load-programs-btn">
153
+ <i class="fas fa-list"></i> ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ
154
+ </button>
155
+ </div>
156
+
157
+ <div id="programsContainer" class="programs-container">
158
+ <div id="programsList">
159
+ <!-- ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก์ด ์—ฌ๊ธฐ์— ํ‘œ์‹œ๋ฉ๋‹ˆ๋‹ค -->
160
+ <div class="loading-programs">
161
+ <div class="spinner"></div>
162
+ <p>ํ”„๋กœ๊ทธ๋žจ ๋ชฉ๋ก ๋กœ๋“œ ์ค‘...</p>
163
+ </div>
164
+ </div>
165
+ </div>
166
+ </div>
167
+ </div>
168
+ </section>
169
  </main>
170
 
171
  <footer>