""" OpenAI API 클라이언트 모듈 """ import os import json import logging from typing import List, Dict, Any, Optional, Union from dotenv import load_dotenv from openai import OpenAI # 환경 변수 로드 load_dotenv() # 로거 설정 logger = logging.getLogger("OpenAILLM") if not logger.hasHandlers(): handler = logging.StreamHandler() formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s') handler.setFormatter(formatter) logger.addHandler(handler) logger.setLevel(logging.INFO) class OpenAILLM: """OpenAI API 래퍼 클래스""" def __init__(self): """OpenAI LLM 클래스 초기화""" self.api_key = os.getenv("OPENAI_API_KEY") self.model = os.getenv("OPENAI_MODEL", "gpt-3.5-turbo") if not self.api_key: logger.warning("OpenAI API 키가 .env 파일에 설정되지 않았습니다.") logger.warning("OPENAI_API_KEY를 확인하세요.") else: # OpenAI 클라이언트 초기화 self.client = OpenAI(api_key=self.api_key) logger.info("OpenAI API 키 로드 완료.") def chat_completion( self, messages: List[Dict[str, str]], temperature: float = 0.7, max_tokens: int = 1000, **kwargs ) -> Dict[str, Any]: """ OpenAI 채팅 완성 API 호출 Args: messages: 채팅 메시지 목록 temperature: 생성 온도 (낮을수록 결정적) max_tokens: 생성할 최대 토큰 수 **kwargs: 추가 API 매개변수 Returns: API 응답 (딕셔너리) """ if not self.api_key: logger.error("API 키가 설정되지 않아 OpenAI API를 호출할 수 없습니다.") raise ValueError("OpenAI API 키가 설정되지 않았습니다.") try: logger.info(f"OpenAI API 요청 전송 중 (모델: {self.model})") # 새로운 OpenAI SDK를 사용하여 API 호출 response = self.client.chat.completions.create( model=self.model, messages=messages, temperature=temperature, max_tokens=max_tokens, **kwargs ) return response except Exception as e: logger.error(f"OpenAI API 요청 실패: {e}") raise Exception(f"OpenAI API 요청 실패: {e}") def generate( self, prompt: str, system_prompt: Optional[str] = None, temperature: float = 0.7, max_tokens: int = 1000, **kwargs ) -> str: """ 간단한 텍스트 생성 인터페이스 Args: prompt: 사용자 프롬프트 system_prompt: 시스템 프롬프트 (선택 사항) temperature: 생성 온도 max_tokens: 생성할 최대 토큰 수 **kwargs: 추가 API 매개변수 Returns: 생성된 텍스트 """ messages = [] if system_prompt: messages.append({"role": "system", "content": system_prompt}) messages.append({"role": "user", "content": prompt}) try: response = self.chat_completion( messages=messages, temperature=temperature, max_tokens=max_tokens, **kwargs ) # 새로운 OpenAI SDK 응답 구조에 맞게 처리 if not response or not hasattr(response, 'choices') or not response.choices: logger.error("OpenAI API 응답에서 생성된 텍스트를 찾을 수 없습니다.") return "" return response.choices[0].message.content.strip() except Exception as e: logger.error(f"텍스트 생성 중 오류 발생: {e}") return f"오류: {str(e)}" def rag_generate( self, query: str, context: List[str], system_prompt: Optional[str] = None, temperature: float = 0.3, max_tokens: int = 1000, **kwargs ) -> str: """ RAG 검색 결과를 활용한 텍스트 생성 Args: query: 사용자 질의 context: 검색된 문맥 목록 system_prompt: 시스템 프롬프트 (선택 사항) temperature: 생성 온도 max_tokens: 생성할 최대 토큰 수 **kwargs: 추가 API 매개변수 Returns: 생성된 텍스트 """ if not system_prompt: system_prompt = """당신은 검색 결과를 기반으로 질문에 답변하는 도우미입니다. - 검색 결과는 태그 안에 제공됩니다. - 검색 결과에 답변이 있으면 해당 정보를 사용하여 명확하게 답변하세요. - 검색 결과에 답변이 없으면 "검색 결과에 관련 정보가 없습니다"라고 말하세요. - 검색 내용을 그대로 복사하지 말고, 자연스러운 한국어로 답변을 작성하세요. - 답변은 간결하고 정확하게 제공하세요.""" # 중요: 컨텍스트 길이 제한 # gpt-4o-mini에 맞게 제한 완화 max_context = 10 # 3개에서 10개로 증가 if len(context) > max_context: logger.warning(f"컨텍스트가 너무 길어 처음 {max_context}개만 사용합니다.") context = context[:max_context] # 각 컨텍스트 액세스 limited_context = [] for i, doc in enumerate(context): # 각 문서를 1000자로 제한 (이전 500자에서 업그레이드) if len(doc) > 1000: logger.warning(f"문서 {i+1}의 길이가 제한되었습니다 ({len(doc)} -> 1000)") doc = doc[:1000] + "...(생략)" limited_context.append(doc) context_text = "\n\n".join([f"문서 {i+1}: {doc}" for i, doc in enumerate(limited_context)]) prompt = f"""질문: {query} {context_text} 위 검색 결과를 참고하여 질문에 답변해 주세요.""" try: return self.generate( prompt=prompt, system_prompt=system_prompt, temperature=temperature, max_tokens=max_tokens, **kwargs ) except Exception as e: logger.error(f"RAG 텍스트 생성 중 오류 발생: {e}") return f"오류: {str(e)}"