Spaces:
Sleeping
Sleeping
add function
Browse files- app/app.py +18 -826
- app/app_device_routes.py +201 -0
- app/app_part2.py +209 -0
- app/app_part3.py +163 -0
- app/app_revised.py +241 -0
- app/app_routes.py +297 -0
- app/init_retriever.py +149 -0
- app/static/css/device-style.css +306 -0
- app/static/js/app-core.js +326 -0
- app/static/js/app-device.js +375 -0
- app/static/js/app-docs.js +196 -0
- app/static/js/app-llm.js +273 -0
- app/static/js/app.js +43 -621
- app/templates/index.html +47 -0
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
|
|
|
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
|
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
|
71 |
-
|
72 |
-
|
73 |
-
|
74 |
-
|
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 |
-
//
|
6 |
-
|
7 |
-
|
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 |
-
|
261 |
-
|
262 |
-
|
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 |
-
|
306 |
-
|
307 |
-
|
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 |
-
//
|
342 |
-
|
343 |
-
|
|
|
344 |
|
345 |
-
|
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 |
-
//
|
416 |
-
|
417 |
-
|
418 |
-
|
419 |
-
|
420 |
|
421 |
-
|
422 |
-
|
423 |
-
|
424 |
|
425 |
-
//
|
426 |
-
|
427 |
-
|
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 |
-
|
497 |
-
|
498 |
-
|
499 |
-
|
500 |
-
|
501 |
-
|
502 |
-
|
503 |
-
|
504 |
-
|
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 |
-
|
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>
|