Spaces:
Running
Running
reset
Browse files- app/app.py +826 -18
- app/static/js/app.js +621 -43
- app/templates/index.html +0 -47
app/app.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
RAG ๊ฒ์ ์ฑ๋ด ์น ์ ํ๋ฆฌ์ผ์ด์
(
|
| 3 |
"""
|
| 4 |
|
| 5 |
import os
|
|
@@ -7,19 +7,16 @@ import json
|
|
| 7 |
import logging
|
| 8 |
import tempfile
|
| 9 |
import threading
|
| 10 |
-
import
|
| 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,13 +26,11 @@ load_dotenv()
|
|
| 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,10 +40,10 @@ 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,6 +52,8 @@ try:
|
|
| 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,15 +62,20 @@ except ImportError as e:
|
|
| 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 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 77 |
# --- ์ธ์
์ฟ ํค ์ค์ ๋ ---
|
| 78 |
|
| 79 |
# ์ต๋ ํ์ผ ํฌ๊ธฐ ์ค์ (10MB)
|
|
@@ -108,21 +110,827 @@ app_ready = False # ์ฑ ์ด๊ธฐํ ์ํ ํ๋๊ทธ
|
|
| 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 |
-
|
|
|
|
| 124 |
|
| 125 |
logger.info(f"์ธ์ฆ ์ฑ๊ณต: {session.get('username', 'unknown')} ์ฌ์ฉ์๊ฐ {request.path} ์ ๊ทผ")
|
| 126 |
return f(*args, **kwargs)
|
| 127 |
return decorated_function
|
| 128 |
# --- ์ธ์ฆ ๋ฐ์ฝ๋ ์ดํฐ ๋ ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
RAG ๊ฒ์ ์ฑ๋ด ์น ์ ํ๋ฆฌ์ผ์ด์
(์ธ์
์ค์ ์์ ์ ์ฉ)
|
| 3 |
"""
|
| 4 |
|
| 5 |
import os
|
|
|
|
| 7 |
import logging
|
| 8 |
import tempfile
|
| 9 |
import threading
|
| 10 |
+
import datetime
|
|
|
|
| 11 |
from flask import Flask, request, jsonify, render_template, send_from_directory, session, redirect, url_for
|
| 12 |
from werkzeug.utils import secure_filename
|
| 13 |
from dotenv import load_dotenv
|
| 14 |
from functools import wraps
|
|
|
|
|
|
|
| 15 |
|
| 16 |
# ๋ก๊ฑฐ ์ค์
|
| 17 |
logging.basicConfig(
|
| 18 |
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
| 19 |
+
level=logging.DEBUG # INFO์์ DEBUG๋ก ๋ณ๊ฒฝํ์ฌ ๋ ์์ธํ ๋ก๊ทธ ํ์ธ
|
| 20 |
)
|
| 21 |
logger = logging.getLogger(__name__)
|
| 22 |
|
|
|
|
| 26 |
# ํ๊ฒฝ ๋ณ์ ๋ก๋ ์ํ ํ์ธ ๋ฐ ๋ก๊น
|
| 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 |
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 |
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 |
app = Flask(__name__)
|
| 63 |
|
| 64 |
# ์ธ์
์ค์ - ๊ณ ์ ๋ ์ํฌ๋ฆฟ ํค ์ฌ์ฉ (์ค์ ๋ฐฐํฌ ์ ํ๊ฒฝ ๋ณ์ ๋ฑ์ผ๋ก ๊ด๋ฆฌ ๊ถ์ฅ)
|
| 65 |
+
app.secret_key = os.getenv('FLASK_SECRET_KEY', 'rag_chatbot_fixed_secret_key_12345') # ํ๊ฒฝ ๋ณ์ ์ฐ์ ์ฌ์ฉ
|
| 66 |
|
| 67 |
+
# --- ์ธ์
์ฟ ํค ์ค์ ์์ (ํ๊น
ํ์ด์ค ํ๊ฒฝ ๊ณ ๋ ค) ---
|
| 68 |
+
# ํ๊น
ํ์ด์ค ์คํ์ด์ค๋ ์ผ๋ฐ์ ์ผ๋ก HTTPS๋ก ์๋น์ค๋๋ฏ๋ก Secure=True ์ค์
|
| 69 |
app.config['SESSION_COOKIE_SECURE'] = True
|
| 70 |
+
app.config['SESSION_COOKIE_HTTPONLY'] = True # JavaScript์์ ์ฟ ํค ์ ๊ทผ ๋ฐฉ์ง (๋ณด์ ๊ฐํ)
|
| 71 |
+
# SameSite='Lax'๊ฐ ๋๋ถ๋ถ์ ๊ฒฝ์ฐ์ ๋ ์์ ํ๊ณ ํธํ์ฑ์ด ์ข์.
|
| 72 |
+
# ๋ง์ฝ ์ฑ์ด ๋ค๋ฅธ ๋๋ฉ์ธ์ iframe ๋ด์์ ์คํ๋์ด์ผ ํ๋ค๋ฉด 'None'์ผ๋ก ์ค์ ํด์ผ ํจ.
|
| 73 |
+
# (๋จ, 'None'์ผ๋ก ์ค์ ์ ๋ฐ๋์ Secure=True์ฌ์ผ ํจ)
|
| 74 |
+
# ๋ก๊ทธ ๋ถ์ ๊ฒฐ๊ณผ iframe ํ๊ฒฝ์ผ๋ก ํ์ธ๋์ด 'None'์ผ๋ก ๋ณ๊ฒฝ
|
| 75 |
+
app.config['SESSION_COOKIE_SAMESITE'] = 'None' # <--- ์ด๋ ๊ฒ ๋ณ๊ฒฝํฉ๋๋ค.
|
| 76 |
+
app.config['SESSION_COOKIE_DOMAIN'] = None # ํน์ ๋๋ฉ์ธ ์ ํ ์์
|
| 77 |
+
app.config['SESSION_COOKIE_PATH'] = '/' # ์ฑ ์ ์ฒด ๊ฒฝ๋ก์ ์ฟ ํค ์ ์ฉ
|
| 78 |
+
app.config['PERMANENT_SESSION_LIFETIME'] = datetime.timedelta(days=1) # ์ธ์
์ ํจ ์๊ฐ ์ฆ๊ฐ
|
| 79 |
# --- ์ธ์
์ฟ ํค ์ค์ ๋ ---
|
| 80 |
|
| 81 |
# ์ต๋ ํ์ผ ํฌ๊ธฐ ์ค์ (10MB)
|
|
|
|
| 110 |
# --- ์ ์ญ ๊ฐ์ฒด ์ด๊ธฐํ ๋ ---
|
| 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)
|
app/static/js/app.js
CHANGED
|
@@ -1,58 +1,636 @@
|
|
| 1 |
/**
|
| 2 |
-
* RAG ๊ฒ์ ์ฑ๋ด UI JavaScript
|
| 3 |
-
* ์ด ํ์ผ์ ๋ชจ๋ํ๋ JS ํ์ผ๋ค์ ๋ก๋ํ๋ ์ญํ ์ ํฉ๋๋ค.
|
| 4 |
*/
|
| 5 |
|
| 6 |
-
//
|
| 7 |
-
|
| 8 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 9 |
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 18 |
|
| 19 |
-
//
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
};
|
| 23 |
|
| 24 |
-
|
| 25 |
-
document.head.appendChild(script);
|
| 26 |
}
|
| 27 |
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
-
//
|
| 33 |
-
const
|
| 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 |
-
|
| 40 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
//
|
| 43 |
-
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
-
//
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 56 |
});
|
| 57 |
-
|
| 58 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
/**
|
| 2 |
+
* RAG ๊ฒ์ ์ฑ๋ด UI JavaScript
|
|
|
|
| 3 |
*/
|
| 4 |
|
| 5 |
+
// DOM ์์
|
| 6 |
+
const chatTab = document.getElementById('chatTab');
|
| 7 |
+
const docsTab = document.getElementById('docsTab');
|
| 8 |
+
const chatSection = document.getElementById('chatSection');
|
| 9 |
+
const docsSection = document.getElementById('docsSection');
|
| 10 |
+
const chatMessages = document.getElementById('chatMessages');
|
| 11 |
+
const userInput = document.getElementById('userInput');
|
| 12 |
+
const sendButton = document.getElementById('sendButton');
|
| 13 |
+
const micButton = document.getElementById('micButton');
|
| 14 |
+
const stopRecordingButton = document.getElementById('stopRecordingButton');
|
| 15 |
+
const recordingStatus = document.getElementById('recordingStatus');
|
| 16 |
+
const uploadForm = document.getElementById('uploadForm');
|
| 17 |
+
const documentFile = document.getElementById('documentFile');
|
| 18 |
+
const fileName = document.getElementById('fileName');
|
| 19 |
+
const uploadButton = document.getElementById('uploadButton');
|
| 20 |
+
const uploadStatus = document.getElementById('uploadStatus');
|
| 21 |
+
const refreshDocsButton = document.getElementById('refreshDocsButton');
|
| 22 |
+
const docsList = document.getElementById('docsList');
|
| 23 |
+
const docsLoading = document.getElementById('docsLoading');
|
| 24 |
+
const noDocsMessage = document.getElementById('noDocsMessage');
|
| 25 |
+
const llmSelect = document.getElementById('llmSelect');
|
| 26 |
+
const currentLLMInfo = document.getElementById('currentLLMInfo');
|
| 27 |
+
|
| 28 |
+
// LLM ๊ด๋ จ ๋ณ์
|
| 29 |
+
let currentLLM = 'openai';
|
| 30 |
+
let supportedLLMs = [];
|
| 31 |
+
|
| 32 |
+
// ๋
น์ ๊ด๋ จ ๋ณ์
|
| 33 |
+
let mediaRecorder = null;
|
| 34 |
+
let audioChunks = [];
|
| 35 |
+
let isRecording = false;
|
| 36 |
+
|
| 37 |
+
// ์ฑ ์ด๊ธฐํ ์ํ ํ์ธ ํจ์
|
| 38 |
+
async function checkAppStatus() {
|
| 39 |
+
try {
|
| 40 |
+
const response = await fetch('/api/status');
|
| 41 |
+
if (!response.ok) {
|
| 42 |
+
return false;
|
| 43 |
+
}
|
| 44 |
+
const data = await response.json();
|
| 45 |
+
return data.ready;
|
| 46 |
+
} catch (error) {
|
| 47 |
+
console.error('Status check failed:', error);
|
| 48 |
+
return false;
|
| 49 |
+
}
|
| 50 |
+
}
|
| 51 |
+
|
| 52 |
+
/**
|
| 53 |
+
* LLM ๋ชฉ๋ก ๋ก๋ ํจ์
|
| 54 |
+
*/
|
| 55 |
+
async function loadLLMs() {
|
| 56 |
+
try {
|
| 57 |
+
// API ์์ฒญ
|
| 58 |
+
const response = await fetch('/api/llm');
|
| 59 |
+
|
| 60 |
+
if (!response.ok) {
|
| 61 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
const data = await response.json();
|
| 65 |
+
supportedLLMs = data.supported_llms;
|
| 66 |
+
currentLLM = data.current_llm.id;
|
| 67 |
+
|
| 68 |
+
// LLM ์ ํ ๋๋กญ๋ค์ด ์
๋ฐ์ดํธ
|
| 69 |
+
llmSelect.innerHTML = '';
|
| 70 |
+
supportedLLMs.forEach(llm => {
|
| 71 |
+
const option = document.createElement('option');
|
| 72 |
+
option.value = llm.id;
|
| 73 |
+
option.textContent = llm.name;
|
| 74 |
+
option.selected = llm.current;
|
| 75 |
+
llmSelect.appendChild(option);
|
| 76 |
+
});
|
| 77 |
+
|
| 78 |
+
// ํ์ฌ LLM ํ์
|
| 79 |
+
updateCurrentLLMInfo(data.current_llm);
|
| 80 |
+
} catch (error) {
|
| 81 |
+
console.error('LLM ๋ชฉ๋ก ๋ก๋ ์คํจ:', error);
|
| 82 |
+
}
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
/**
|
| 86 |
+
* LLM ๋ณ๊ฒฝ ํจ์
|
| 87 |
+
* @param {string} llmId - ๋ณ๊ฒฝํ LLM ID
|
| 88 |
+
*/
|
| 89 |
+
async function changeLLM(llmId) {
|
| 90 |
+
try {
|
| 91 |
+
// API ์์ฒญ
|
| 92 |
+
const response = await fetch('/api/llm', {
|
| 93 |
+
method: 'POST',
|
| 94 |
+
headers: {
|
| 95 |
+
'Content-Type': 'application/json'
|
| 96 |
+
},
|
| 97 |
+
body: JSON.stringify({ llm_id: llmId })
|
| 98 |
+
});
|
| 99 |
+
|
| 100 |
+
if (!response.ok) {
|
| 101 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 102 |
+
}
|
| 103 |
+
|
| 104 |
+
const data = await response.json();
|
| 105 |
+
|
| 106 |
+
if (data.success) {
|
| 107 |
+
currentLLM = llmId;
|
| 108 |
+
updateCurrentLLMInfo(data.current_llm);
|
| 109 |
+
console.log(`LLM์ด ${data.current_llm.name}(์ผ)๋ก ๋ณ๊ฒฝ๋์์ต๋๋ค.`);
|
| 110 |
+
|
| 111 |
+
// ์์คํ
๋ฉ์์ง ์ถ๊ฐ
|
| 112 |
+
const systemMessage = `LLM์ด ${data.current_llm.name}(์ผ)๋ก ๋ณ๊ฒฝ๋์์ต๋๋ค. ๋ชจ๋ธ: ${data.current_llm.model}`;
|
| 113 |
+
addSystemNotification(systemMessage);
|
| 114 |
+
} else if (data.error) {
|
| 115 |
+
console.error('LLM ๋ณ๊ฒฝ ์ค๋ฅ:', data.error);
|
| 116 |
+
alert(`LLM ๋ณ๊ฒฝ ์ค๋ฅ: ${data.error}`);
|
| 117 |
+
}
|
| 118 |
+
} catch (error) {
|
| 119 |
+
console.error('LLM ๋ณ๊ฒฝ ์คํจ:', error);
|
| 120 |
+
alert('LLM ๋ณ๊ฒฝ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.');
|
| 121 |
+
}
|
| 122 |
+
}
|
| 123 |
+
|
| 124 |
+
/**
|
| 125 |
+
* ํ์ฌ LLM ์ ๋ณด ํ์ ์
๋ฐ์ดํธ
|
| 126 |
+
* @param {Object} llmInfo - LLM ์ ๋ณด ๊ฐ์ฒด
|
| 127 |
+
*/
|
| 128 |
+
function updateCurrentLLMInfo(llmInfo) {
|
| 129 |
+
if (currentLLMInfo) {
|
| 130 |
+
currentLLMInfo.textContent = `${llmInfo.name} (${llmInfo.model})`;
|
| 131 |
+
}
|
| 132 |
+
}
|
| 133 |
+
|
| 134 |
+
/**
|
| 135 |
+
* ์์คํ
์๋ฆผ ๋ฉ์์ง ์ถ๊ฐ
|
| 136 |
+
* @param {string} message - ์๋ฆผ ๋ฉ์์ง
|
| 137 |
+
*/
|
| 138 |
+
function addSystemNotification(message) {
|
| 139 |
+
const messageDiv = document.createElement('div');
|
| 140 |
+
messageDiv.classList.add('message', 'system');
|
| 141 |
+
|
| 142 |
+
const contentDiv = document.createElement('div');
|
| 143 |
+
contentDiv.classList.add('message-content');
|
| 144 |
+
|
| 145 |
+
const messageP = document.createElement('p');
|
| 146 |
+
messageP.innerHTML = `<i class="fas fa-info-circle"></i> ${message}`;
|
| 147 |
+
contentDiv.appendChild(messageP);
|
| 148 |
+
|
| 149 |
+
messageDiv.appendChild(contentDiv);
|
| 150 |
+
chatMessages.appendChild(messageDiv);
|
| 151 |
+
|
| 152 |
+
// ์คํฌ๋กค์ ๊ฐ์ฅ ์๋๋ก ์ด๋
|
| 153 |
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
// ํ์ด์ง ๋ก๋ ์ ์ด๊ธฐํ
|
| 157 |
+
document.addEventListener('DOMContentLoaded', () => {
|
| 158 |
+
// ์ฑ ์ํ ํ์ธ (๋ก๋ฉ ํ์ด์ง๊ฐ ์๋ ๊ฒฝ์ฐ์๋ง)
|
| 159 |
+
if (window.location.pathname === '/' && !document.getElementById('app-loading-indicator')) {
|
| 160 |
+
// ์ฑ ์ํ ์ฃผ๊ธฐ์ ์ผ๋ก ํ์ธ
|
| 161 |
+
const statusInterval = setInterval(async () => {
|
| 162 |
+
const isReady = await checkAppStatus();
|
| 163 |
+
if (isReady) {
|
| 164 |
+
clearInterval(statusInterval);
|
| 165 |
+
console.log('์ฑ์ด ์ค๋น๋์์ต๋๋ค.');
|
| 166 |
+
|
| 167 |
+
// ์ฑ์ด ์ค๋น๋๋ฉด LLM ๋ชฉ๋ก ๋ก๋
|
| 168 |
+
loadLLMs();
|
| 169 |
+
}
|
| 170 |
+
}, 5000);
|
| 171 |
+
}
|
| 172 |
+
|
| 173 |
+
// ํญ ์ ํ ์ด๋ฒคํธ ๋ฆฌ์ค๋
|
| 174 |
+
chatTab.addEventListener('click', () => {
|
| 175 |
+
switchTab('chat');
|
| 176 |
+
});
|
| 177 |
+
|
| 178 |
+
docsTab.addEventListener('click', () => {
|
| 179 |
+
switchTab('docs');
|
| 180 |
+
loadDocuments();
|
| 181 |
+
});
|
| 182 |
+
|
| 183 |
+
// LLM ์ ํ ์ด๋ฒคํธ ๋ฆฌ์ค๋
|
| 184 |
+
llmSelect.addEventListener('change', (event) => {
|
| 185 |
+
changeLLM(event.target.value);
|
| 186 |
+
});
|
| 187 |
+
|
| 188 |
+
// ๋ฉ์์ง ์ ์ก ์ด๋ฒคํธ ๋ฆฌ์ค๋
|
| 189 |
+
sendButton.addEventListener('click', sendMessage);
|
| 190 |
+
userInput.addEventListener('keydown', (event) => {
|
| 191 |
+
if (event.key === 'Enter' && !event.shiftKey) {
|
| 192 |
+
event.preventDefault();
|
| 193 |
+
sendMessage();
|
| 194 |
+
}
|
| 195 |
+
});
|
| 196 |
+
|
| 197 |
+
// ์์ฑ ์ธ์ ์ด๋ฒคํธ ๋ฆฌ์ค๋
|
| 198 |
+
micButton.addEventListener('click', startRecording);
|
| 199 |
+
stopRecordingButton.addEventListener('click', stopRecording);
|
| 200 |
+
|
| 201 |
+
// ๋ฌธ์ ์
๋ก๋ ์ด๋ฒคํธ ๋ฆฌ์ค๋
|
| 202 |
+
documentFile.addEventListener('change', (event) => {
|
| 203 |
+
if (event.target.files.length > 0) {
|
| 204 |
+
fileName.textContent = event.target.files[0].name;
|
| 205 |
+
} else {
|
| 206 |
+
fileName.textContent = '์ ํ๋ ํ์ผ ์์';
|
| 207 |
+
}
|
| 208 |
+
});
|
| 209 |
+
|
| 210 |
+
uploadForm.addEventListener('submit', (event) => {
|
| 211 |
+
event.preventDefault();
|
| 212 |
+
uploadDocument();
|
| 213 |
+
});
|
| 214 |
+
|
| 215 |
+
// ๋ฌธ์ ๋ชฉ๋ก ์๋ก๊ณ ์นจ ์ด๋ฒคํธ ๋ฆฌ์ค๋
|
| 216 |
+
refreshDocsButton.addEventListener('click', loadDocuments);
|
| 217 |
+
|
| 218 |
+
// ์๋ ์
๋ ฅ ํ๋ ํฌ๊ธฐ ์กฐ์
|
| 219 |
+
userInput.addEventListener('input', adjustTextareaHeight);
|
| 220 |
+
|
| 221 |
+
// ์ด๊ธฐ ๋ฌธ์ ๋ชฉ๋ก ๋ก๋
|
| 222 |
+
if (docsSection.classList.contains('active')) {
|
| 223 |
+
loadDocuments();
|
| 224 |
+
}
|
| 225 |
+
});
|
| 226 |
+
|
| 227 |
+
/**
|
| 228 |
+
* ํญ ์ ํ ํจ์
|
| 229 |
+
* @param {string} tabName - ํ์ฑํํ ํญ ์ด๋ฆ ('chat' ๋๋ 'docs')
|
| 230 |
+
*/
|
| 231 |
+
function switchTab(tabName) {
|
| 232 |
+
if (tabName === 'chat') {
|
| 233 |
+
chatTab.classList.add('active');
|
| 234 |
+
docsTab.classList.remove('active');
|
| 235 |
+
chatSection.classList.add('active');
|
| 236 |
+
docsSection.classList.remove('active');
|
| 237 |
+
} else if (tabName === 'docs') {
|
| 238 |
+
chatTab.classList.remove('active');
|
| 239 |
+
docsTab.classList.add('active');
|
| 240 |
+
chatSection.classList.remove('active');
|
| 241 |
+
docsSection.classList.add('active');
|
| 242 |
+
}
|
| 243 |
+
}
|
| 244 |
+
|
| 245 |
+
/**
|
| 246 |
+
* ์ฑํ
๋ฉ์์ง ์ ์ก ํจ์
|
| 247 |
+
*/
|
| 248 |
+
async function sendMessage() {
|
| 249 |
+
const message = userInput.value.trim();
|
| 250 |
+
if (!message) return;
|
| 251 |
+
|
| 252 |
+
// UI ์
๋ฐ์ดํธ
|
| 253 |
+
addMessage(message, 'user');
|
| 254 |
+
userInput.value = '';
|
| 255 |
+
adjustTextareaHeight();
|
| 256 |
+
|
| 257 |
+
// ๋ก๋ฉ ๋ฉ์์ง ์ถ๊ฐ
|
| 258 |
+
const loadingMessageId = addLoadingMessage();
|
| 259 |
|
| 260 |
+
try {
|
| 261 |
+
// API ์์ฒญ
|
| 262 |
+
const response = await fetch('/api/chat', {
|
| 263 |
+
method: 'POST',
|
| 264 |
+
headers: {
|
| 265 |
+
'Content-Type': 'application/json'
|
| 266 |
+
},
|
| 267 |
+
body: JSON.stringify({
|
| 268 |
+
query: message,
|
| 269 |
+
llm_id: currentLLM // ํ์ฌ ์ ํ๋ LLM ์ ์ก
|
| 270 |
+
})
|
| 271 |
+
});
|
| 272 |
+
|
| 273 |
+
if (!response.ok) {
|
| 274 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 275 |
+
}
|
| 276 |
+
|
| 277 |
+
const data = await response.json();
|
| 278 |
+
|
| 279 |
+
// ๋ก๋ฉ ๋ฉ์์ง ์ ๊ฑฐ
|
| 280 |
+
removeLoadingMessage(loadingMessageId);
|
| 281 |
+
|
| 282 |
+
// ์๋ต ํ์
|
| 283 |
+
if (data.error) {
|
| 284 |
+
addErrorMessage(data.error);
|
| 285 |
+
} else {
|
| 286 |
+
// LLM ์ ๋ณด ์
๋ฐ์ดํธ
|
| 287 |
+
if (data.llm) {
|
| 288 |
+
updateCurrentLLMInfo(data.llm);
|
| 289 |
+
}
|
| 290 |
+
addMessage(data.answer, 'bot', null, data.sources);
|
| 291 |
+
}
|
| 292 |
+
} catch (error) {
|
| 293 |
+
console.error('Error:', error);
|
| 294 |
+
removeLoadingMessage(loadingMessageId);
|
| 295 |
+
addErrorMessage('์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค. ๋ค์ ์๋ํด ์ฃผ์ธ์.');
|
| 296 |
+
}
|
| 297 |
+
}
|
| 298 |
+
|
| 299 |
+
/**
|
| 300 |
+
* ์์ฑ ๋
น์ ์์ ํจ์
|
| 301 |
+
*/
|
| 302 |
+
async function startRecording() {
|
| 303 |
+
if (isRecording) return;
|
| 304 |
|
| 305 |
+
try {
|
| 306 |
+
const stream = await navigator.mediaDevices.getUserMedia({ audio: true });
|
| 307 |
+
isRecording = true;
|
| 308 |
+
audioChunks = [];
|
| 309 |
+
|
| 310 |
+
mediaRecorder = new MediaRecorder(stream);
|
| 311 |
+
|
| 312 |
+
mediaRecorder.addEventListener('dataavailable', (event) => {
|
| 313 |
+
if (event.data.size > 0) audioChunks.push(event.data);
|
| 314 |
+
});
|
| 315 |
+
|
| 316 |
+
mediaRecorder.addEventListener('stop', sendAudioMessage);
|
| 317 |
+
|
| 318 |
+
// ๋
น์ ์์
|
| 319 |
+
mediaRecorder.start();
|
| 320 |
+
|
| 321 |
+
// UI ์
๋ฐ์ดํธ
|
| 322 |
+
micButton.style.display = 'none';
|
| 323 |
+
recordingStatus.classList.remove('hidden');
|
| 324 |
+
|
| 325 |
+
console.log('๋
น์ ์์๋จ');
|
| 326 |
+
} catch (error) {
|
| 327 |
+
console.error('์์ฑ ๋
น์ ๊ถํ์ ์ป์ ์ ์์ต๋๋ค:', error);
|
| 328 |
+
alert('๋ง์ดํฌ ์ ๊ทผ ๊ถํ์ด ํ์ํฉ๋๋ค.');
|
| 329 |
}
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
/**
|
| 333 |
+
* ์์ฑ ๋
น์ ์ค์ง ํจ์
|
| 334 |
+
*/
|
| 335 |
+
function stopRecording() {
|
| 336 |
+
if (!isRecording || !mediaRecorder) return;
|
| 337 |
+
|
| 338 |
+
mediaRecorder.stop();
|
| 339 |
+
isRecording = false;
|
| 340 |
|
| 341 |
+
// UI ์
๋ฐ์ดํธ
|
| 342 |
+
micButton.style.display = 'flex';
|
| 343 |
+
recordingStatus.classList.add('hidden');
|
|
|
|
| 344 |
|
| 345 |
+
console.log('๋
น์ ์ค์ง๋จ');
|
|
|
|
| 346 |
}
|
| 347 |
|
| 348 |
+
/**
|
| 349 |
+
* ๋
น์๋ ์ค๋์ค ๋ฉ์์ง ์ ์ก ํจ์
|
| 350 |
+
*/
|
| 351 |
+
async function sendAudioMessage() {
|
| 352 |
+
if (audioChunks.length === 0) return;
|
| 353 |
+
|
| 354 |
+
// ์ค๋์ค Blob ์์ฑ
|
| 355 |
+
const audioBlob = new Blob(audioChunks, { type: 'audio/wav' });
|
| 356 |
|
| 357 |
+
// ๋ก๋ฉ ๋ฉ์์ง ์ถ๊ฐ
|
| 358 |
+
const loadingMessageId = addLoadingMessage();
|
|
|
|
|
|
|
|
|
|
| 359 |
|
| 360 |
+
try {
|
| 361 |
+
// FormData์ ์ค๋์ค ์ถ๊ฐ
|
| 362 |
+
const formData = new FormData();
|
| 363 |
+
formData.append('audio', audioBlob, 'recording.wav');
|
| 364 |
+
// ํ์ฌ ์ ํ๋ LLM ์ถ๊ฐ
|
| 365 |
+
formData.append('llm_id', currentLLM);
|
| 366 |
+
|
| 367 |
+
// API ์์ฒญ
|
| 368 |
+
const response = await fetch('/api/voice', {
|
| 369 |
+
method: 'POST',
|
| 370 |
+
body: formData
|
| 371 |
+
});
|
| 372 |
+
|
| 373 |
+
if (!response.ok) {
|
| 374 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 375 |
+
}
|
| 376 |
+
|
| 377 |
+
const data = await response.json();
|
| 378 |
+
|
| 379 |
+
// ๋ก๋ฉ ๋ฉ์์ง ์ ๊ฑฐ
|
| 380 |
+
removeLoadingMessage(loadingMessageId);
|
| 381 |
|
| 382 |
+
// ์๋ต ํ์
|
| 383 |
+
if (data.error) {
|
| 384 |
+
addErrorMessage(data.error);
|
| 385 |
+
} else {
|
| 386 |
+
// LLM ์ ๋ณด ์
๋ฐ์ดํธ
|
| 387 |
+
if (data.llm) {
|
| 388 |
+
updateCurrentLLMInfo(data.llm);
|
| 389 |
+
}
|
| 390 |
|
| 391 |
+
// ์ฌ์ฉ์ ๋ฉ์์ง(์์ฑ ํ
์คํธ) ์ถ๊ฐ
|
| 392 |
+
if (data.transcription) {
|
| 393 |
+
addMessage(data.transcription, 'user');
|
| 394 |
+
}
|
| 395 |
+
|
| 396 |
+
// ๋ด ์๋ต ์ถ๊ฐ
|
| 397 |
+
addMessage(data.answer, 'bot', data.transcription, data.sources);
|
| 398 |
+
}
|
| 399 |
+
} catch (error) {
|
| 400 |
+
console.error('Error:', error);
|
| 401 |
+
removeLoadingMessage(loadingMessageId);
|
| 402 |
+
addErrorMessage('์ค๋์ค ์ฒ๋ฆฌ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค. ๋ค์ ์๋ํด ์ฃผ์ธ์.');
|
| 403 |
+
}
|
| 404 |
+
}
|
| 405 |
+
|
| 406 |
+
/**
|
| 407 |
+
* ๋ฌธ์ ์
๋ก๋ ํจ์
|
| 408 |
+
*/
|
| 409 |
+
async function uploadDocument() {
|
| 410 |
+
if (documentFile.files.length === 0) {
|
| 411 |
+
alert('ํ์ผ์ ์ ํํด ์ฃผ์ธ์.');
|
| 412 |
+
return;
|
| 413 |
+
}
|
| 414 |
+
|
| 415 |
+
// UI ์
๋ฐ์ดํธ
|
| 416 |
+
uploadStatus.classList.remove('hidden');
|
| 417 |
+
uploadStatus.className = 'upload-status';
|
| 418 |
+
uploadStatus.innerHTML = '<div class="spinner"></div><p>์
๋ก๋ ์ค...</p>';
|
| 419 |
+
uploadButton.disabled = true;
|
| 420 |
+
|
| 421 |
+
try {
|
| 422 |
+
const formData = new FormData();
|
| 423 |
+
formData.append('document', documentFile.files[0]);
|
| 424 |
+
|
| 425 |
+
// API ์์ฒญ
|
| 426 |
+
const response = await fetch('/api/upload', {
|
| 427 |
+
method: 'POST',
|
| 428 |
+
body: formData
|
| 429 |
});
|
| 430 |
+
|
| 431 |
+
const data = await response.json();
|
| 432 |
+
|
| 433 |
+
// ์๋ต ์ฒ๋ฆฌ
|
| 434 |
+
if (data.error) {
|
| 435 |
+
uploadStatus.className = 'upload-status error';
|
| 436 |
+
uploadStatus.textContent = `์ค๋ฅ: ${data.error}`;
|
| 437 |
+
} else if (data.warning) {
|
| 438 |
+
uploadStatus.className = 'upload-status warning';
|
| 439 |
+
uploadStatus.textContent = data.message;
|
| 440 |
+
} else {
|
| 441 |
+
uploadStatus.className = 'upload-status success';
|
| 442 |
+
uploadStatus.textContent = data.message;
|
| 443 |
+
|
| 444 |
+
// ๋ฌธ์ ๋ชฉ๋ก ์๋ก๊ณ ์นจ
|
| 445 |
+
loadDocuments();
|
| 446 |
+
|
| 447 |
+
// ์
๋ ฅ ํ๋ ์ด๊ธฐํ
|
| 448 |
+
documentFile.value = '';
|
| 449 |
+
fileName.textContent = '์ ํ๋ ํ์ผ ์์';
|
| 450 |
+
}
|
| 451 |
+
} catch (error) {
|
| 452 |
+
console.error('Error:', error);
|
| 453 |
+
uploadStatus.className = 'upload-status error';
|
| 454 |
+
uploadStatus.textContent = '์
๋ก๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค. ๋ค์ ์๋ํด ์ฃผ์ธ์.';
|
| 455 |
+
} finally {
|
| 456 |
+
uploadButton.disabled = false;
|
| 457 |
+
}
|
| 458 |
+
}
|
| 459 |
+
|
| 460 |
+
/**
|
| 461 |
+
* ๋ฌธ์ ๋ชฉ๋ก ๋ก๋ ํจ์
|
| 462 |
+
*/
|
| 463 |
+
async function loadDocuments() {
|
| 464 |
+
// UI ์
๋ฐ์ดํธ
|
| 465 |
+
docsList.querySelector('tbody').innerHTML = '';
|
| 466 |
+
docsLoading.classList.remove('hidden');
|
| 467 |
+
noDocsMessage.classList.add('hidden');
|
| 468 |
+
|
| 469 |
+
try {
|
| 470 |
+
// API ์์ฒญ
|
| 471 |
+
const response = await fetch('/api/documents');
|
| 472 |
+
|
| 473 |
+
if (!response.ok) {
|
| 474 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 475 |
+
}
|
| 476 |
+
|
| 477 |
+
const data = await response.json();
|
| 478 |
+
|
| 479 |
+
// ์๋ต ์ฒ๋ฆฌ
|
| 480 |
+
docsLoading.classList.add('hidden');
|
| 481 |
+
|
| 482 |
+
if (!data.documents || data.documents.length === 0) {
|
| 483 |
+
noDocsMessage.classList.remove('hidden');
|
| 484 |
+
return;
|
| 485 |
+
}
|
| 486 |
+
|
| 487 |
+
// ๋ฌธ์ ๋ชฉ๋ก ์
๋ฐ์ดํธ
|
| 488 |
+
const tbody = docsList.querySelector('tbody');
|
| 489 |
+
data.documents.forEach(doc => {
|
| 490 |
+
const row = document.createElement('tr');
|
| 491 |
+
|
| 492 |
+
const fileNameCell = document.createElement('td');
|
| 493 |
+
fileNameCell.textContent = doc.filename || doc.source;
|
| 494 |
+
row.appendChild(fileNameCell);
|
| 495 |
+
|
| 496 |
+
const chunksCell = document.createElement('td');
|
| 497 |
+
chunksCell.textContent = doc.chunks;
|
| 498 |
+
row.appendChild(chunksCell);
|
| 499 |
+
|
| 500 |
+
const typeCell = document.createElement('td');
|
| 501 |
+
typeCell.textContent = doc.filetype || '-';
|
| 502 |
+
row.appendChild(typeCell);
|
| 503 |
+
|
| 504 |
+
tbody.appendChild(row);
|
| 505 |
+
});
|
| 506 |
+
} catch (error) {
|
| 507 |
+
console.error('Error:', error);
|
| 508 |
+
docsLoading.classList.add('hidden');
|
| 509 |
+
noDocsMessage.classList.remove('hidden');
|
| 510 |
+
noDocsMessage.querySelector('p').textContent = '๋ฌธ์ ๋ชฉ๋ก์ ๋ถ๋ฌ์ค๋ ์ค ์ค๋ฅ๊ฐ ๋ฐ์ํ์ต๋๋ค.';
|
| 511 |
+
}
|
| 512 |
+
}
|
| 513 |
+
|
| 514 |
+
/**
|
| 515 |
+
* ๋ฉ์์ง ์ถ๊ฐ ํจ์
|
| 516 |
+
* @param {string} text - ๋ฉ์์ง ๋ด์ฉ
|
| 517 |
+
* @param {string} sender - ๋ฉ์์ง ๋ฐ์ ์ ('user' ๋๋ 'bot' ๋๋ 'system')
|
| 518 |
+
* @param {string|null} transcription - ์์ฑ ์ธ์ ํ
์คํธ (์ ํ ์ฌํญ)
|
| 519 |
+
* @param {Array|null} sources - ์์ค ์ ๋ณด ๋ฐฐ์ด (์ ํ ์ฌํญ)
|
| 520 |
+
*/
|
| 521 |
+
function addMessage(text, sender, transcription = null, sources = null) {
|
| 522 |
+
const messageDiv = document.createElement('div');
|
| 523 |
+
messageDiv.classList.add('message', sender);
|
| 524 |
+
|
| 525 |
+
const contentDiv = document.createElement('div');
|
| 526 |
+
contentDiv.classList.add('message-content');
|
| 527 |
+
|
| 528 |
+
// ์์ฑ ์ธ์ ํ
์คํธ ์ถ๊ฐ (์๋ ๊ฒฝ์ฐ)
|
| 529 |
+
if (transcription && sender === 'bot') {
|
| 530 |
+
const transcriptionP = document.createElement('p');
|
| 531 |
+
transcriptionP.classList.add('transcription');
|
| 532 |
+
transcriptionP.textContent = `"${transcription}"`;
|
| 533 |
+
contentDiv.appendChild(transcriptionP);
|
| 534 |
+
}
|
| 535 |
+
|
| 536 |
+
// ๋ฉ์์ง ํ
์คํธ ์ถ๊ฐ
|
| 537 |
+
const textP = document.createElement('p');
|
| 538 |
+
textP.textContent = text;
|
| 539 |
+
contentDiv.appendChild(textP);
|
| 540 |
+
|
| 541 |
+
// ์์ค ์ ๋ณด ์ถ๊ฐ (์๋ ๊ฒฝ์ฐ)
|
| 542 |
+
if (sources && sources.length > 0 && sender === 'bot') {
|
| 543 |
+
const sourcesDiv = document.createElement('div');
|
| 544 |
+
sourcesDiv.classList.add('sources');
|
| 545 |
+
|
| 546 |
+
const sourcesTitle = document.createElement('strong');
|
| 547 |
+
sourcesTitle.textContent = '์ถ์ฒ: ';
|
| 548 |
+
sourcesDiv.appendChild(sourcesTitle);
|
| 549 |
+
|
| 550 |
+
sources.forEach((source, index) => {
|
| 551 |
+
if (index < 3) { // ์ต๋ 3๊ฐ๊น์ง๋ง ํ์
|
| 552 |
+
const sourceSpan = document.createElement('span');
|
| 553 |
+
sourceSpan.classList.add('source-item');
|
| 554 |
+
sourceSpan.textContent = source.source;
|
| 555 |
+
sourcesDiv.appendChild(sourceSpan);
|
| 556 |
+
}
|
| 557 |
+
});
|
| 558 |
+
|
| 559 |
+
contentDiv.appendChild(sourcesDiv);
|
| 560 |
+
}
|
| 561 |
+
|
| 562 |
+
messageDiv.appendChild(contentDiv);
|
| 563 |
+
chatMessages.appendChild(messageDiv);
|
| 564 |
+
|
| 565 |
+
// ์คํฌ๋กค์ ๏ฟฝ๏ฟฝ๏ฟฝ์ฅ ์๋๋ก ์ด๋
|
| 566 |
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
| 567 |
+
}
|
| 568 |
+
|
| 569 |
+
/**
|
| 570 |
+
* ๋ก๋ฉ ๋ฉ์์ง ์ถ๊ฐ ํจ์
|
| 571 |
+
* @returns {string} ๋ก๋ฉ ๋ฉ์์ง ID
|
| 572 |
+
*/
|
| 573 |
+
function addLoadingMessage() {
|
| 574 |
+
const id = 'loading-' + Date.now();
|
| 575 |
+
const messageDiv = document.createElement('div');
|
| 576 |
+
messageDiv.classList.add('message', 'bot');
|
| 577 |
+
messageDiv.id = id;
|
| 578 |
+
|
| 579 |
+
const contentDiv = document.createElement('div');
|
| 580 |
+
contentDiv.classList.add('message-content');
|
| 581 |
+
|
| 582 |
+
const loadingP = document.createElement('p');
|
| 583 |
+
loadingP.innerHTML = '<div class="spinner" style="width: 20px; height: 20px; display: inline-block; margin-right: 10px;"></div> ์๊ฐ ์ค...';
|
| 584 |
+
contentDiv.appendChild(loadingP);
|
| 585 |
+
|
| 586 |
+
messageDiv.appendChild(contentDiv);
|
| 587 |
+
chatMessages.appendChild(messageDiv);
|
| 588 |
+
|
| 589 |
+
// ์คํฌ๋กค์ ๊ฐ์ฅ ์๋๋ก ์ด๋
|
| 590 |
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
| 591 |
+
|
| 592 |
+
return id;
|
| 593 |
+
}
|
| 594 |
+
|
| 595 |
+
/**
|
| 596 |
+
* ๋ก๋ฉ ๋ฉ์์ง ์ ๊ฑฐ ํจ์
|
| 597 |
+
* @param {string} id - ๋ก๋ฉ ๋ฉ์์ง ID
|
| 598 |
+
*/
|
| 599 |
+
function removeLoadingMessage(id) {
|
| 600 |
+
const loadingMessage = document.getElementById(id);
|
| 601 |
+
if (loadingMessage) {
|
| 602 |
+
loadingMessage.remove();
|
| 603 |
+
}
|
| 604 |
+
}
|
| 605 |
+
|
| 606 |
+
/**
|
| 607 |
+
* ์ค๋ฅ ๋ฉ์์ง ์ถ๊ฐ ํจ์
|
| 608 |
+
* @param {string} errorText - ์ค๋ฅ ๋ฉ์์ง ๋ด์ฉ
|
| 609 |
+
*/
|
| 610 |
+
function addErrorMessage(errorText) {
|
| 611 |
+
const messageDiv = document.createElement('div');
|
| 612 |
+
messageDiv.classList.add('message', 'system');
|
| 613 |
+
|
| 614 |
+
const contentDiv = document.createElement('div');
|
| 615 |
+
contentDiv.classList.add('message-content');
|
| 616 |
+
contentDiv.style.backgroundColor = 'rgba(239, 68, 68, 0.1)';
|
| 617 |
+
contentDiv.style.color = 'var(--error-color)';
|
| 618 |
+
|
| 619 |
+
const errorP = document.createElement('p');
|
| 620 |
+
errorP.innerHTML = `<i class="fas fa-exclamation-circle"></i> ${errorText}`;
|
| 621 |
+
contentDiv.appendChild(errorP);
|
| 622 |
+
|
| 623 |
+
messageDiv.appendChild(contentDiv);
|
| 624 |
+
chatMessages.appendChild(messageDiv);
|
| 625 |
+
|
| 626 |
+
// ์คํฌ๋กค์ ๊ฐ์ฅ ์๋๋ก ์ด๋
|
| 627 |
+
chatMessages.scrollTop = chatMessages.scrollHeight;
|
| 628 |
+
}
|
| 629 |
+
|
| 630 |
+
/**
|
| 631 |
+
* textarea ๋์ด ์๋ ์กฐ์ ํจ์
|
| 632 |
+
*/
|
| 633 |
+
function adjustTextareaHeight() {
|
| 634 |
+
userInput.style.height = 'auto';
|
| 635 |
+
userInput.style.height = Math.min(userInput.scrollHeight, 100) + 'px';
|
| 636 |
+
}
|
app/templates/index.html
CHANGED
|
@@ -6,7 +6,6 @@
|
|
| 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,7 +28,6 @@
|
|
| 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,51 +119,6 @@
|
|
| 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>
|
|
|
|
| 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 |
<div class="tabs">
|
| 29 |
<button id="chatTab" class="tab active">๋ํ</button>
|
| 30 |
<button id="docsTab" class="tab">๋ฌธ์๊ด๋ฆฌ</button>
|
|
|
|
| 31 |
</div>
|
| 32 |
</header>
|
| 33 |
|
|
|
|
| 119 |
</div>
|
| 120 |
</div>
|
| 121 |
</section>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 122 |
</main>
|
| 123 |
|
| 124 |
<footer>
|