""" RAG 검색 챗봇 웹 애플리케이션 (장치 관리 기능 통합) """ import os import logging import threading from datetime import datetime, timedelta from flask import Flask, send_from_directory, jsonify from dotenv import load_dotenv from functools import wraps from flask_cors import CORS # 로거 설정 logging.basicConfig( format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', level=logging.DEBUG ) logger = logging.getLogger(__name__) # 환경 변수 로드 load_dotenv() # 환경 변수 로드 상태 확인 및 로깅 ADMIN_USERNAME = os.getenv('ADMIN_USERNAME') ADMIN_PASSWORD = os.getenv('ADMIN_PASSWORD') DEVICE_SERVER_URL = os.getenv('DEVICE_SERVER_URL', 'http://localhost:5050') logger.info(f"==== 환경 변수 로드 상태 ====") logger.info(f"ADMIN_USERNAME 설정 여부: {ADMIN_USERNAME is not None}") logger.info(f"ADMIN_PASSWORD 설정 여부: {ADMIN_PASSWORD is not None}") logger.info(f"DEVICE_SERVER_URL: {DEVICE_SERVER_URL}") # 환경 변수가 없으면 기본값 설정 if not ADMIN_USERNAME: ADMIN_USERNAME = 'admin' logger.warning("ADMIN_USERNAME 환경변수가 없어 기본값 'admin'으로 설정합니다.") if not ADMIN_PASSWORD: ADMIN_PASSWORD = 'rag12345' logger.warning("ADMIN_PASSWORD 환경변수가 없어 기본값 'rag12345'로 설정합니다.") class MockComponent: pass # --- 로컬 모듈 임포트 --- try: from utils.vito_stt import VitoSTT from utils.llm_interface import LLMInterface from utils.document_processor import DocumentProcessor from retrieval.vector_retriever import VectorRetriever from retrieval.reranker import ReRanker except ImportError as e: logger.error(f"로컬 모듈 임포트 실패: {e}. utils 및 retrieval 패키지가 올바른 경로에 있는지 확인하세요.") VitoSTT = LLMInterface = DocumentProcessor = VectorRetriever = ReRanker = MockComponent # --- 로컬 모듈 임포트 끝 --- # Flask 앱 초기화 app = Flask(__name__) # CORS 설정 - 모든 도메인에서의 요청 허용 CORS(app, supports_credentials=True) # 세션 설정 app.secret_key = os.getenv('FLASK_SECRET_KEY', 'rag_chatbot_fixed_secret_key_12345') # --- 세션 쿠키 설정 --- app.config['SESSION_COOKIE_SECURE'] = True app.config['SESSION_COOKIE_HTTPONLY'] = True app.config['SESSION_COOKIE_SAMESITE'] = 'None' app.config['SESSION_COOKIE_DOMAIN'] = None app.config['SESSION_COOKIE_PATH'] = '/' app.config['PERMANENT_SESSION_LIFETIME'] = timedelta(days=1) # --- 세션 쿠키 설정 끝 --- # 최대 파일 크기 설정 (10MB) app.config['MAX_CONTENT_LENGTH'] = 10 * 1024 * 1024 # 애플리케이션 파일 기준 상대 경로 설정 APP_ROOT = os.path.dirname(os.path.abspath(__file__)) app.config['UPLOAD_FOLDER'] = os.path.join(APP_ROOT, 'uploads') app.config['DATA_FOLDER'] = os.path.join(APP_ROOT, '..', 'data') app.config['INDEX_PATH'] = os.path.join(APP_ROOT, '..', 'data', 'index') # 필요한 폴더 생성 os.makedirs(app.config['UPLOAD_FOLDER'], exist_ok=True) os.makedirs(app.config['DATA_FOLDER'], exist_ok=True) os.makedirs(app.config['INDEX_PATH'], exist_ok=True) # --- 전역 객체 초기화 --- try: llm_interface = LLMInterface(default_llm="openai") stt_client = VitoSTT() except NameError: logger.warning("LLM 또는 STT 인터페이스 초기화 실패. Mock 객체를 사용합니다.") llm_interface = MockComponent() stt_client = MockComponent() base_retriever = None retriever = None app_ready = False # 앱 초기화 상태 플래그 # --- 전역 객체 초기화 끝 --- # --- 인증 데코레이터 --- def login_required(f): @wraps(f) def decorated_function(*args, **kwargs): from flask import request, session, redirect, url_for logger.info(f"----------- 인증 필요 페이지 접근 시도: {request.path} -----------") logger.info(f"현재 플라스크 세션 객체: {session}") logger.info(f"현재 세션 상태: logged_in={session.get('logged_in', False)}, username={session.get('username', 'None')}") logger.info(f"요청의 세션 쿠키 값: {request.cookies.get('session', 'None')}") # API 요청이고 클라이언트에서 오는 경우 인증 무시 (임시 조치) if request.path.startswith('/api/device/'): logger.info(f"장치 API 요청: {request.path} - 인증 제외") return f(*args, **kwargs) # Flask 세션에 'logged_in' 키가 있는지 직접 확인 if 'logged_in' not in session: logger.warning(f"플라스크 세션에 'logged_in' 없음. 로그인 페이지로 리디렉션.") return redirect(url_for('login', next=request.url)) logger.info(f"인증 성공: {session.get('username', 'unknown')} 사용자가 {request.path} 접근") return f(*args, **kwargs) return decorated_function # --- 인증 데코레이터 끝 --- # --- 오류 핸들러 추가 --- @app.errorhandler(404) def not_found(e): # 클라이언트가 JSON을 기대하는 API 호출인 경우 JSON 응답 if request.path.startswith('/api/'): return jsonify({"success": False, "error": "요청한 API 엔드포인트를 찾을 수 없습니다."}), 404 # 일반 웹 페이지 요청인 경우 HTML 응답 return "페이지를 찾을 수 없습니다.", 404 @app.errorhandler(500) def internal_error(e): # 클라이언트가 JSON을 기대하는 API 호출인 경우 JSON 응답 if request.path.startswith('/api/'): return jsonify({"success": False, "error": "서버 내부 오류가 발생했습니다."}), 500 # 일반 웹 페이지 요청인 경우 HTML 응답 return "서버 오류가 발생했습니다.", 500 # --- 오류 핸들러 끝 --- # --- 정적 파일 서빙 --- @app.route('/static/') def send_static(path): return send_from_directory('static', path) # --- 백그라운드 초기화 함수 --- def background_init(): """백그라운드에서 검색기 초기화 수행""" global app_ready, retriever, base_retriever # 즉시 앱 사용 가능 상태로 설정 app_ready = True logger.info("앱을 즉시 사용 가능 상태로 설정 (app_ready=True)") try: from app.init_retriever import init_retriever # 기본 검색기 초기화 (보험) if base_retriever is None: base_retriever = MockComponent() if hasattr(base_retriever, 'documents'): base_retriever.documents = [] # 임시 retriever 설정 if retriever is None: retriever = MockComponent() if not hasattr(retriever, 'search'): retriever.search = lambda query, **kwargs: [] # 임베딩 캐시 파일 경로 cache_path = os.path.join(app.config['INDEX_PATH'], "cached_embeddings.gz") # 캐시된 임베딩 로드 시도 try: from app.init_retriever import load_embeddings cached_retriever = load_embeddings(cache_path) if cached_retriever: # 캐시된 데이터가 있으면 바로 사용 base_retriever = cached_retriever # 재순위화 검색기 초기화 retriever = ReRanker( base_retriever=base_retriever, rerank_fn=lambda query, results: results, rerank_field="text" ) logger.info("캐시된 임베딩으로 검색기 초기화 완료 (빠른 시작)") else: # 캐시된 데이터가 없으면 전체 초기화 진행 logger.info("캐시된 임베딩이 없어 전체 초기화 시작") retriever = init_retriever(app, base_retriever, retriever, ReRanker) logger.info("전체 초기화 완료") except ImportError: logger.warning("임베딩 캐시 모듈을 찾을 수 없습니다. 전체 초기화를 진행합니다.") retriever = init_retriever(app, base_retriever, retriever, ReRanker) logger.info("앱 초기화 완료 (모든 컴포넌트 준비됨)") except Exception as e: logger.error(f"앱 백그라운드 초기화 중 심각한 오류 발생: {e}", exc_info=True) # 초기화 실패 시 기본 객체 생성 if base_retriever is None: base_retriever = MockComponent() if hasattr(base_retriever, 'documents'): base_retriever.documents = [] if retriever is None: retriever = MockComponent() if not hasattr(retriever, 'search'): retriever.search = lambda query, **kwargs: [] logger.warning("초기화 중 오류가 있지만 앱은 계속 사용 가능합니다.") # --- 라우트 등록 --- def register_all_routes(): try: # 기본 라우트 등록 from app.app_routes import register_routes register_routes( app, login_required, llm_interface, retriever, stt_client, DocumentProcessor, base_retriever, app_ready, ADMIN_USERNAME, ADMIN_PASSWORD, DEVICE_SERVER_URL ) # 장치 관리 라우트 등록 from app.app_device_routes import register_device_routes register_device_routes(app, login_required, DEVICE_SERVER_URL) logger.info("모든 라우트 등록 완료") except ImportError as e: logger.error(f"라우트 모듈 임포트 실패: {e}") except Exception as e: logger.error(f"라우트 등록 중 오류 발생: {e}", exc_info=True) # --- 앱 초기화 및 실행 --- def initialize_app(): # 백그라운드 초기화 스레드 시작 init_thread = threading.Thread(target=background_init) init_thread.daemon = True init_thread.start() # 라우트 등록 register_all_routes() logger.info("앱 초기화 완료") # 앱 초기화 실행 initialize_app() # --- 앱 실행 (직접 실행 시) --- if __name__ == '__main__': logger.info("Flask 앱을 직접 실행합니다 (개발용 서버).") port = int(os.environ.get("PORT", 7860)) logger.info(f"서버를 http://0.0.0.0:{port} 에서 시작합니다.") app.run(debug=True, host='0.0.0.0', port=port)