""" LLM 핸들러 모듈 ───────────── LLM 답변 생성 및 후처리 """ import re from typing import Optional, List, Dict class LLMHandler: """LLM 답변 생성 핸들러""" def __init__(self, llm_client, config): self.llm_client = llm_client self.config = config def generate_answer( self, system_prompt: str, user_prompt: str, ts: str, max_tokens: Optional[int] = None, temperature: float = 0.3 ) -> str: """LLM 답변 생성 (기존 방식 - 하위 호환성) Args: system_prompt: 시스템 프롬프트 user_prompt: 사용자 프롬프트 ts: 타임스탬프 (로깅용) max_tokens: 최대 토큰 수 (기본값: config에서 가져옴) temperature: 온도 파라미터 Returns: 생성된 답변 텍스트 """ if max_tokens is None: max_tokens = self.config.llm_max_tokens try: print(f"[LLMHandler] {ts} LLM 답변 생성 중... (max_tokens={max_tokens})") response = self.llm_client.chat_completion( messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ], max_tokens=max_tokens, temperature=temperature ) # 태그 제거 formatted = self._remove_think_tags(response) print(f"[LLMHandler] {ts} LLM 답변 생성 완료 (길이: {len(formatted)}자)") return formatted except Exception as e: print(f"[LLMHandler] {ts} ❌ LLM 답변 생성 실패: {e}") raise def generate_answer_from_messages( self, messages: List[Dict[str, str]], ts: str, max_tokens: Optional[int] = None, temperature: float = 0.3 ) -> str: """LLM 답변 생성 (Messages Format - 표준 방식) Args: messages: [{"role": "system|user|assistant", "content": "..."}] ts: 타임스탬프 (로깅용) max_tokens: 최대 토큰 수 temperature: 온도 파라미터 Returns: 생성된 답변 텍스트 """ if max_tokens is None: max_tokens = self.config.llm_max_tokens try: print(f"[LLMHandler] {ts} LLM 답변 생성 중... (messages={len(messages)}개, max_tokens={max_tokens})") response = self.llm_client.chat_completion( messages=messages, max_tokens=max_tokens, temperature=temperature ) # 태그 제거 formatted = self._remove_think_tags(response) print(f"[LLMHandler] {ts} LLM 답변 생성 완료 (길이: {len(formatted)}자)") return formatted except Exception as e: print(f"[LLMHandler] {ts} ❌ LLM 답변 생성 실패: {e}") raise def generate_fallback_answer(self, best_match: dict) -> str: """폴백 답변 생성 (LLM 실패 시) Args: best_match: 최상위 매칭 결과 Returns: 폴백 답변 """ return ( "고객님의 질문은 다음과 같다고 생각됩니다.\n\n" f"{best_match['q']}\n\n" "이에 대한 답변을 드리겠습니다.\n\n" f"{best_match['a']}" ) def generate_default_guidance(self) -> str: """기본 질문 유도 답변 (LLM 실패 시)""" return ( "죄송합니다. 고객님의 질문과 관련된 정보를 찾을 수 없습니다.\n\n" "한국도로공사는 고속도로 이용과 관련된 상담을 제공하고 있습니다.\n" "통행료, Hi-pass, 환불, 소음 민원, 시설물 이용 등에 대해 궁금하신 사항이 있으시다면 질문해 주세요.\n\n" "또는 한국도로공사 콜센터(1588-2504)로 문의하시면 자세한 안내를 받으실 수 있습니다." ) def _remove_think_tags(self, text: str) -> str: """ 태그 제거""" if "" in text and "" in text: return re.sub(r'.*?\s*', '', text, flags=re.DOTALL).strip() return text