import os import json import requests import time from dotenv import load_dotenv # 환경 변수 로드 시도 # HuggingFace Spaces에서는 환경 변수가 자동으로 로드됨 # 로컬 개발 환경에서는 .env 파일 사용 load_dotenv() # Gemini API 키 및 기본 URL GEMINI_API_KEY = os.getenv("GEMINI_API_KEY", "") GEMINI_API_URL = "https://generativelanguage.googleapis.com/v1/models/gemini-pro:generateContent" def gemini_query(prompt, retry_attempts=3, retry_delay=2): """ Gemini API에 텍스트 생성 요청을 보냅니다. Args: prompt: 프롬프트 텍스트 retry_attempts: 재시도 횟수 retry_delay: 재시도 대기 시간 (초) Returns: 생성된 텍스트 응답 Raises: Exception: API 요청 실패 시 """ if not GEMINI_API_KEY: # 허깅페이스 환경에서 API 키가 없을 경우 대체 메시지 return "API 키가 설정되지 않아 응답을 생성할 수 없습니다. HuggingFace Spaces의 Settings에서 GEMINI_API_KEY를 시크릿으로 추가해주세요." headers = { "Content-Type": "application/json", "x-goog-api-key": GEMINI_API_KEY } data = { "contents": [{ "parts": [{ "text": prompt }] }], "generationConfig": { "temperature": 0.7, "topP": 0.95, "topK": 40, "maxOutputTokens": 2048 } } # 재시도 로직 attempt = 0 while attempt < retry_attempts: try: response = requests.post( GEMINI_API_URL, headers=headers, json=data, timeout=60 # 타임아웃 설정 ) response.raise_for_status() # HTTP 오류 체크 result = response.json() # 응답 파싱 if "candidates" in result and result["candidates"]: return result["candidates"][0]["content"]["parts"][0]["text"] else: raise Exception("유효한 응답을 받지 못했습니다.") except requests.exceptions.RequestException as e: attempt += 1 if attempt < retry_attempts: print(f"API 요청 실패, {retry_delay}초 후 재시도 ({attempt}/{retry_attempts}): {str(e)}") time.sleep(retry_delay) else: raise Exception(f"Gemini API 요청이 {retry_attempts}회 실패했습니다: {str(e)}") raise Exception("알 수 없는 오류가 발생했습니다.") def get_persona_enhancement(persona_data): """ LLM을 통해 페르소나를 강화합니다. Args: persona_data: 페르소나 기본 정보 Returns: 강화된 페르소나 데이터 """ # 기본 정보 추출 name = persona_data.get("기본정보", {}).get("이름", "") object_type = persona_data.get("기본정보", {}).get("유형", "") description = persona_data.get("기본정보", {}).get("설명", "") # 성격 특성 추출 traits = [] for trait, value in persona_data.get("성격특성", {}).items(): level = "높은" if value >= 70 else "중간" if value >= 40 else "낮은" traits.append(f"{trait}: {level} ({value}/100)") # 배경 이야기 및 경험 backstory = persona_data.get("배경이야기", "") experiences = persona_data.get("경험", []) # 프롬프트 구성 prompt = f""" 당신은 물체에 인격을 부여하는 전문가입니다. 다음 정보를 기반으로 매력적이고 개성 있는 페르소나를 강화해주세요. ## 기본 정보 - 이름: {name} - 유형: {object_type} - 설명: {description} ## 성격 특성 {', '.join(traits)} ## 매력적 결함 {', '.join(persona_data.get("매력적결함", []))} ## 소통 방식 - 대화 스타일: {persona_data.get("소통방식", "")} - 유머 스타일: {persona_data.get("유머스타일", "")} - 말투 패턴: {persona_data.get("말투패턴", "")} ## 관계 성향 - 애착 스타일: {persona_data.get("관계성향", {}).get("애착스타일", "")} - 관계 깊이 선호도: {persona_data.get("관계성향", {}).get("관계깊이선호도", "")} - 초기 태도: {persona_data.get("관계성향", {}).get("초기태도", "")} ## 배경 이야기 {backstory} ## 주요 경험 {', '.join(experiences) if experiences else "정보 없음"} ----- 위 정보를 기반으로 다음 작업을 수행해주세요: 1. 상세한 배경 이야기 확장 (최소 2문장, 최대 4문장) 2. 최소 3개 이상의 구체적인 관심사 추가 3. 말투와 표현 패턴 구체화 (실제 대화에서 쓸만한 특징적 표현 3개 이상) 4. 독특한 성격 특성 추가 (기존 특성 유지하되 개성을 살릴 수 있는 디테일 추가) 강화된 페르소나 정보를 원본 JSON 구조를 유지하면서 제공해주세요. 단, 일부 필드는 세부적으로 확장하여 더 풍부하게 만들어주세요. JSON 형식만 제공하고, 다른 설명은 하지 마세요. """ try: response = gemini_query(prompt) # JSON 형식 추출 json_str = extract_json(response) if json_str: enhanced_persona = json.loads(json_str) # 기존 키를 보존하면서 새 내용 병합 for key in persona_data: if key not in enhanced_persona: enhanced_persona[key] = persona_data[key] return enhanced_persona else: print("유효한 JSON 응답을 추출할 수 없습니다.") return persona_data except Exception as e: print(f"페르소나 강화 중 오류 발생: {str(e)}") return persona_data def generate_response(persona, conversation_history): """ 페르소나 특성에 맞는 대화 응답을 생성합니다. Args: persona: 페르소나 정보 conversation_history: 이전 대화 내역 Returns: 생성된 응답 텍스트 """ # 페르소나 정보 요약 name = persona.get("기본정보", {}).get("이름", "무명") object_type = persona.get("기본정보", {}).get("유형", "물건") description = persona.get("기본정보", {}).get("설명", "") # 성격 특성 요약 traits = [] for trait, value in persona.get("성격특성", {}).items(): level = "높은" if value >= 70 else "중간" if value >= 40 else "낮은" traits.append(f"{trait}: {level} ({value}/100)") # 최근 대화 내역 추출 (최대 10개) recent_conversation = [] for msg in conversation_history[-10:]: role = "User" if msg["role"] == "user" else "Assistant" if msg["role"] == "assistant" else "System" recent_conversation.append(f"{role}: {msg['content']}") # 프롬프트 구성 prompt = f""" 당신은 이제 다음 페르소나를 구현해야 합니다: ## 페르소나 정보 - 이름: {name} - 유형: {object_type} - 설명: {description} ## 성격 특성 {', '.join(traits)} ## 배경 {persona.get("배경이야기", "")} ## 성격 요약 {persona.get("성격요약", "")} ## 소통 방식 - 대화 스타일: {persona.get("소통방식", "")} - 유머 스타일: {persona.get("유머스타일", "")} - 매력적 결함: {', '.join(persona.get("매력적결함", []))} ## 말투 패턴 예시 {persona.get("말투패턴", "")} ## 관심사 {', '.join(persona.get("관심사", []))} 당신은 위 페르소나의 역할을 완벽하게 구현하여 사용자와 대화해야 합니다. 온기, 능력, 신뢰성 등의 점수에 따라 성격 특성을 정확히 반영하세요. 관심사와 배경을 자연스럽게 대화에 활용하세요. 말투 패턴과 매력적 결함을 일관되게 표현하세요. ## 최근 대화 내역 {' '.join(recent_conversation)} 위 대화를 이어서, {name}으로서 답변하세요. 페르소나에 충실하되 사용자의 질문에 직접적으로 답변하세요. 답변은 한국어로만 작성하고, 절대 다른 언어를 사용하지 마세요. """ try: response = gemini_query(prompt) # 응답의 첫 줄이 "Assistant:" 또는 유사한 형태로 시작하면 제거 if response.startswith("Assistant:") or response.startswith(f"{name}:"): response = response.split(":", 1)[1].strip() return response except Exception as e: print(f"응답 생성 중 오류 발생: {str(e)}") return f"죄송합니다, 응답을 생성하는 중에 문제가 발생했습니다. 잠시 후 다시 시도해주세요." def extract_json(text): """ 텍스트에서 JSON 형식의 데이터를 추출합니다. Args: text: 텍스트 데이터 Returns: 추출된 JSON 문자열 또는 None """ # JSON 블록 추출 시도 if "```json" in text: # 마크다운 코드 블록에서 JSON 추출 start = text.find("```json") + 7 end = text.find("```", start) if end != -1: return text[start:end].strip() elif "```" in text: # 일반 코드 블록에서 JSON 추출 start = text.find("```") + 3 end = text.find("```", start) if end != -1: return text[start:end].strip() # 중괄호를 기준으로 추출 시도 if "{" in text and "}" in text: start = text.find("{") # 중첩된 중괄호 처리를 위한 간단한 로직 nested = 0 for i in range(start, len(text)): if text[i] == "{": nested += 1 elif text[i] == "}": nested -= 1 if nested == 0: return text[start:i+1] # JSON 형식이 감지되지 않음 return None