""" 벡터 스토어, 임베딩 모델, LLM 등 구성 요소 설정 환경 변수 및 .env 파일 활용 개선 버전 - HuggingFace 환경 지원 추가 """ import os import logging import sys import re import requests import json from pathlib import Path from typing import Dict, Any from dotenv import load_dotenv # 로깅 설정 logger = logging.getLogger("Config") # 현재 실행 위치 확인 (디버깅용) script_dir = os.path.dirname(os.path.abspath(__file__)) logger.info(f"스크립트 디렉토리: {script_dir}") logger.info(f"현재 작업 디렉토리: {os.getcwd()}") logger.info(f"운영 체제: {os.name}") # 환경 감지 - HuggingFace Space 환경인지 확인 IS_HUGGINGFACE = False if os.getenv('SPACE_ID') is not None or os.getenv('SYSTEM') == 'spaces': IS_HUGGINGFACE = True logger.info("HuggingFace Spaces 환경이 감지되었습니다.") else: # 로컬 환경인 경우 .env 파일 로드 # .env 파일 위치 후보들 env_paths = [ ".env", # 현재 디렉토리 os.path.join(script_dir, ".env"), # 스크립트 디렉토리 os.path.join(script_dir, "config", ".env"), # config 하위 디렉토리 os.path.join(os.path.dirname(script_dir), ".env"), # 상위 디렉토리 ] # .env 파일 찾아서 로드 env_loaded = False for env_path in env_paths: if os.path.isfile(env_path): logger.info(f".env 파일 발견: {env_path}") env_loaded = load_dotenv(env_path, verbose=True) if env_loaded: logger.info(f".env 파일 로드 성공: {env_path}") break if not env_loaded: logger.warning(".env 파일을 찾을 수 없습니다. 기본값 또는 시스템 환경 변수를 사용합니다.") logger.info(f"로컬 환경에서 실행 중입니다. (OS: {'Windows' if os.name == 'nt' else 'Unix/Linux/MacOS'})") # Windows 환경 감지 IS_WINDOWS = os.name == 'nt' # 유틸리티 함수: 환경 변수 가져오기 (HuggingFace 환경과 로컬 환경 구분) def get_env(key: str, default: Any = None, required: bool = False) -> Any: """ 환경 변수를 가져오는 유틸리티 함수 (HuggingFace 환경 지원) Args: key: 환경 변수 키 default: 환경 변수가 없을 경우 기본값 required: 환경 변수가 필수적인지 여부 Returns: 환경 변수 값 또는 기본값 """ # HuggingFace Spaces 환경에서는 내부 환경변수 활용 if IS_HUGGINGFACE: # HuggingFace Spaces에서는 시크릿 값을 직접 사용 # HF_SECRET_ 형식으로 저장된 시크릿 확인 hf_secret_key = f"HF_SECRET_{key.upper()}" value = os.getenv(hf_secret_key) # 시크릿이 없으면 일반 환경변수 확인 if value is None: value = os.getenv(key, default) else: # 로컬 환경에서는 일반적인 방식으로 환경변수 가져오기 value = os.getenv(key, default) if required and value is None: if IS_HUGGINGFACE: error_msg = f"필수 환경 변수 {key}가 설정되지 않았습니다. HuggingFace Space에서 시크릿을 설정해주세요." logger.error(error_msg) raise ValueError(error_msg) else: error_msg = f"필수 환경 변수 {key}가 설정되지 않았습니다. .env 파일에 추가해주세요." logger.error(error_msg) raise ValueError(error_msg) return value # 경로 생성 유틸리티 함수 def ensure_absolute_path(path_str: str) -> str: """ 상대 경로를 절대 경로로 변환 (Windows 경로 지원) Args: path_str: 변환할 경로 문자열 Returns: 절대 경로 """ # Windows 드라이브 문자(C:\ 등)로 시작하는 경로 확인 if IS_WINDOWS and re.match(r'^[a-zA-Z]:\\', path_str): logger.info(f"Windows 절대 경로 감지: {path_str}") # Windows 절대 경로는 그대로 사용 return path_str path = Path(path_str) if path.is_absolute(): return str(path) # 스크립트 디렉토리 기준 경로 script_based_path = Path(script_dir) / path # 현재 작업 디렉토리 기준 경로 cwd_based_path = Path.cwd() / path # 두 경로 중 존재하는 경로 우선 사용 if script_based_path.exists(): return str(script_based_path) elif cwd_based_path.exists(): return str(cwd_based_path) else: # 기본적으로 현재 작업 디렉토리 기준 경로 반환 return str(cwd_based_path) # Windows 경로 처리를 위한 유틸리티 함수 def normalize_path(path_str: str) -> str: """ 경로 문자열을 정규화하여 OS에 맞게 변환 Args: path_str: 변환할 경로 문자열 Returns: 정규화된 경로 """ # Windows 경로 형식('\')을 OS에 맞게 변환 return os.path.normpath(path_str) # 기본 디렉토리 설정 (절대 경로로 변환) PDF_DIRECTORY_RAW = get_env("PDF_DIRECTORY", "documents") # Windows 백슬래시 이중 처리를 위해 정규화 PDF_DIRECTORY_RAW = normalize_path(PDF_DIRECTORY_RAW) PDF_DIRECTORY = ensure_absolute_path(PDF_DIRECTORY_RAW) CACHE_DIRECTORY_RAW = get_env("CACHE_DIRECTORY", "cached_data") CACHE_DIRECTORY_RAW = normalize_path(CACHE_DIRECTORY_RAW) CACHE_DIRECTORY = ensure_absolute_path(CACHE_DIRECTORY_RAW) logger.info(f"PDF 디렉토리 (원본): {PDF_DIRECTORY_RAW}") logger.info(f"PDF 디렉토리 (절대): {PDF_DIRECTORY}") logger.info(f"캐시 디렉토리 (원본): {CACHE_DIRECTORY_RAW}") logger.info(f"캐시 디렉토리 (절대): {CACHE_DIRECTORY}") # 청킹 설정 CHUNK_SIZE = int(get_env("CHUNK_SIZE", "1000")) CHUNK_OVERLAP = int(get_env("CHUNK_OVERLAP", "200")) # API 키 및 환경 설정 OPENAI_API_KEY = get_env("OPENAI_API_KEY", "") LANGFUSE_PUBLIC_KEY = get_env("LANGFUSE_PUBLIC_KEY", "") LANGFUSE_SECRET_KEY = get_env("LANGFUSE_SECRET_KEY", "") LANGFUSE_HOST = get_env("LANGFUSE_HOST", "https://cloud.langfuse.com") # DeepSeek 관련 설정 추가 DEEPSEEK_API_KEY = get_env("DEEPSEEK_API_KEY", "") DEEPSEEK_ENDPOINT = get_env("DEEPSEEK_ENDPOINT", "https://api.deepseek.com/v1/chat/completions") DEEPSEEK_MODEL = get_env("DEEPSEEK_MODEL", "deepseek-chat") # 허깅페이스 환경에서 API 키 확인 및 로그 출력 if IS_HUGGINGFACE: logger.info(f"허깅페이스 환경에서 DeepSeek API 키 존재 여부: {bool(DEEPSEEK_API_KEY)}") # 보안을 위해 API 키 첫 4자리와 마지막 4자리만 표시 (키가 존재하는 경우) if DEEPSEEK_API_KEY: masked_key = DEEPSEEK_API_KEY[:4] + "****" + DEEPSEEK_API_KEY[-4:] if len(DEEPSEEK_API_KEY) > 8 else "****" logger.info(f"DeepSeek API 키: {masked_key}") logger.info(f"DeepSeek 모델: {DEEPSEEK_MODEL}") logger.info(f"DeepSeek 엔드포인트: {DEEPSEEK_ENDPOINT}") # Milvus 벡터 DB 설정 MILVUS_HOST = get_env("MILVUS_HOST", "localhost") MILVUS_PORT = get_env("MILVUS_PORT", "19530") MILVUS_COLLECTION = get_env("MILVUS_COLLECTION", "pdf_documents") # 임베딩 모델 설정 EMBEDDING_MODEL = get_env("EMBEDDING_MODEL", "Alibaba-NLP/gte-multilingual-base") # 다국어 지원 모델 RERANKER_MODEL = get_env("RERANKER_MODEL", "Alibaba-NLP/gte-multilingual-reranker-base") # 다국어 지원 리랭커 # LLM 모델 설정 (환경에 따라 자동 선택) USE_OPENAI = get_env("USE_OPENAI", "False").lower() == "true" USE_DEEPSEEK = get_env("USE_DEEPSEEK", "False").lower() == "true" # 허깅페이스 환경에서는 DeepSeek 우선 사용 if IS_HUGGINGFACE: # 허깅페이스 환경에서 DeepSeek API 키가 있는지 확인 if DEEPSEEK_API_KEY: USE_DEEPSEEK = True USE_OPENAI = False LLM_MODEL = DEEPSEEK_MODEL logger.info("HuggingFace Spaces 환경: DeepSeek 모델 사용") else: logger.warning("HuggingFace Spaces 환경에서 DeepSeek API 키가 설정되지 않았습니다.") USE_DEEPSEEK = False USE_OPENAI = False # 기본적으로 API 키가 없으면 비활성화 LLM_MODEL = get_env("LLM_MODEL", "gemma3:latest") # 대체 모델 설정 logger.info(f"HuggingFace Spaces 환경: DeepSeek API 키 없음, LLM 모델: {LLM_MODEL}") else: # 로컬 환경에서는 설정에 따라 LLM 선택 if USE_DEEPSEEK: LLM_MODEL = DEEPSEEK_MODEL logger.info(f"로컬 환경: DeepSeek 모델 사용 ({DEEPSEEK_MODEL})") elif USE_OPENAI: LLM_MODEL = get_env("LLM_MODEL", "gpt-3.5-turbo") logger.info(f"로컬 환경: OpenAI 모델 사용 ({LLM_MODEL})") else: LLM_MODEL = get_env("LLM_MODEL", "gemma3:latest") OLLAMA_HOST = get_env("OLLAMA_HOST", "http://localhost:11434") logger.info(f"로컬 환경: Ollama 모델 사용 ({LLM_MODEL})") # API 키 검증 (로컬 환경만) if not IS_HUGGINGFACE: if USE_DEEPSEEK and not DEEPSEEK_API_KEY: logger.warning("DeepSeek 모델이 선택되었지만 API 키가 설정되지 않았습니다.") USE_DEEPSEEK = False USE_OPENAI = False LLM_MODEL = get_env("LLM_MODEL", "gemma3:latest") logger.info("DeepSeek API 키가 없어 Ollama로 폴백합니다.") elif USE_OPENAI and not OPENAI_API_KEY: logger.warning("OpenAI 모델이 선택되었지만 API 키가 설정되지 않았습니다.") logger.warning("OpenAI API 키가 없어 Ollama로 폴백합니다.") USE_OPENAI = False LLM_MODEL = get_env("LLM_MODEL", "gemma3:latest") # DeepSeek API 테스트 함수 def test_deepseek_connection(): """ DeepSeek API 연결 테스트 Returns: 테스트 결과 딕셔너리 (성공 여부 및 메시지) """ if not DEEPSEEK_API_KEY: logger.warning("DeepSeek API 키가 설정되지 않아 테스트를 건너뜁니다.") return { "success": False, "message": "API 키가 설정되지 않았습니다.", "status_code": None } try: logger.info(f"DeepSeek API 연결 테스트 시작: {DEEPSEEK_ENDPOINT}, 모델: {DEEPSEEK_MODEL}") # 테스트용 간단한 프롬프트 test_prompt = "Hello, please respond with a short greeting." # API 요청 헤더 및 데이터 headers = { "Content-Type": "application/json", "Authorization": f"Bearer {DEEPSEEK_API_KEY}" } payload = { "model": DEEPSEEK_MODEL, "messages": [{"role": "user", "content": test_prompt}], "temperature": 0.7, "max_tokens": 50 } # API 요청 전송 response = requests.post( DEEPSEEK_ENDPOINT, headers=headers, json=payload, timeout=10 # 10초 타임아웃 ) # 응답 확인 if response.status_code == 200: logger.info("DeepSeek API 연결 성공") return { "success": True, "message": "API 연결 성공", "status_code": response.status_code } else: logger.error(f"DeepSeek API 오류: 상태 코드 {response.status_code}") error_message = "" try: error_data = response.json() error_message = error_data.get("error", {}).get("message", str(error_data)) except: error_message = response.text return { "success": False, "message": f"API 오류: {error_message}", "status_code": response.status_code } except requests.exceptions.Timeout: logger.error("DeepSeek API 요청 시간 초과") return { "success": False, "message": "API 요청 시간 초과", "status_code": None } except requests.exceptions.ConnectionError: logger.error("DeepSeek API 연결 실패") return { "success": False, "message": "API 서버 연결 실패", "status_code": None } except Exception as e: logger.error(f"DeepSeek API 테스트 중 예상치 못한 오류: {e}", exc_info=True) return { "success": False, "message": f"예상치 못한 오류: {str(e)}", "status_code": None } # 벡터 검색 설정 TOP_K_RETRIEVAL = int(get_env("TOP_K_RETRIEVAL", "5")) # 벡터 검색 결과 수 TOP_K_RERANK = int(get_env("TOP_K_RERANK", "3")) # 리랭킹 후 선택할 결과 수 # 로깅 설정 LOG_LEVEL = get_env("LOG_LEVEL", "INFO") LOG_FILE = get_env("LOG_FILE", "autorag.log") # 설정 정보 출력 (디버깅용) def print_config(): """현재 설정 정보를 로그에 출력""" logger.info("===== 현재 설정 정보 =====") logger.info(f"실행 환경: {'HuggingFace Spaces' if IS_HUGGINGFACE else '로컬'}") logger.info(f"문서 디렉토리: {PDF_DIRECTORY}") logger.info(f"캐시 디렉토리: {CACHE_DIRECTORY}") logger.info(f"청크 크기: {CHUNK_SIZE}, 오버랩: {CHUNK_OVERLAP}") logger.info(f"OpenAI 사용: {USE_OPENAI}") logger.info(f"DeepSeek 사용: {USE_DEEPSEEK}") logger.info(f"LLM 모델: {LLM_MODEL}") if not USE_OPENAI and not USE_DEEPSEEK and not IS_HUGGINGFACE: logger.info(f"Ollama 호스트: {OLLAMA_HOST}") logger.info(f"임베딩 모델: {EMBEDDING_MODEL}") logger.info(f"리랭커 모델: {RERANKER_MODEL}") logger.info(f"TOP_K 검색: {TOP_K_RETRIEVAL}, 리랭킹: {TOP_K_RERANK}") logger.info("=========================") # 설정 유효성 검사 def validate_config() -> Dict[str, Any]: """ 현재 설정의 유효성을 검사하고 경고나 오류를 로그에 기록 Returns: 검증 결과 (status: 상태, warnings: 경고 목록) """ warnings = [] # 디렉토리 확인 if not os.path.exists(PDF_DIRECTORY): warnings.append(f"PDF 디렉토리({PDF_DIRECTORY})가 존재하지 않습니다.") # API 키 확인 (허깅페이스와 로컬 환경 구분) if IS_HUGGINGFACE: if USE_DEEPSEEK and not DEEPSEEK_API_KEY: warnings.append("허깅페이스 환경에서 DeepSeek 사용이 설정되었지만 API 키가 제공되지 않았습니다.") else: if USE_OPENAI and not OPENAI_API_KEY: warnings.append("OpenAI 사용이 설정되었지만 API 키가 제공되지 않았습니다.") if USE_DEEPSEEK and not DEEPSEEK_API_KEY: warnings.append("DeepSeek 사용이 설정되었지만 API 키가 제공되지 않았습니다.") # 모델 및 설정 값 확인 if CHUNK_SIZE <= CHUNK_OVERLAP: warnings.append(f"청크 크기({CHUNK_SIZE})가 오버랩({CHUNK_OVERLAP})보다 작거나 같습니다.") # DeepSeek API 연결 확인 (설정된 경우) if USE_DEEPSEEK and DEEPSEEK_API_KEY: deepseek_test_result = test_deepseek_connection() if not deepseek_test_result["success"]: warnings.append(f"DeepSeek API 연결 테스트 실패: {deepseek_test_result['message']}") # 결과 기록 if warnings: for warning in warnings: logger.warning(warning) return { "status": "valid" if not warnings else "warnings", "warnings": warnings } # 설정 로드 시 실행 print_config() config_status = validate_config()