""" 対話生成モジュール Together.ai APIを使用した対話生成機能 """ import logging import os import streamlit as st from typing import List, Dict, Any, Optional, Tuple from openai import OpenAI logger = logging.getLogger(__name__) class DialogueGenerator: """対話生成を担当するクラス""" def __init__(self): self.client = None self.model = None self.groq_client = None self.groq_model = None self._initialize_client() self._initialize_groq_client() def _initialize_client(self): """Together.ai APIクライアントの初期化""" try: api_key = os.getenv("TOGETHER_API_KEY") if not api_key: logger.warning("環境変数 TOGETHER_API_KEY が設定されていません。デモモードで動作します。") return self.client = OpenAI( api_key=api_key, base_url="https://api.together.xyz/v1" ) self.model = "Qwen/Qwen3-235B-A22B-Instruct-2507-tput" logger.info("Together.ai APIクライアントの初期化が完了しました。") except Exception as e: logger.error(f"Together.ai APIクライアントの初期化に失敗しました: {e}") def _initialize_groq_client(self): """Groq APIクライアントの初期化(フォールバック用)""" try: groq_api_key = os.getenv("GROQ_API_KEY") if not groq_api_key: logger.warning("環境変数 GROQ_API_KEY が設定されていません。Groqフォールバックは利用できません。") return self.groq_client = OpenAI( api_key=groq_api_key, base_url="https://api.groq.com/openai/v1" ) # 環境変数からGroqモデルを取得、デフォルトはllama-3.1-70b-versatile self.groq_model = os.getenv("GROQ_MODEL", "llama-3.1-70b-versatile") logger.info(f"Groq APIクライアントの初期化が完了しました(フォールバック用)- モデル: {self.groq_model}") except Exception as e: logger.error(f"Groq APIクライアントの初期化に失敗しました: {e}") def get_system_prompt_mari(self, use_ura_mode: bool = False) -> str: """環境変数からシステムプロンプトを取得、なければデフォルトを返す""" if use_ura_mode: # 裏モード用のプロンプトを環境変数から取得 ura_prompt = os.getenv("SYSTEM_PROMPT_URA") # ★★★ ここでURAプロンプトの中身を強制的に表示 ★★★ print("--- [DEBUG] 読み込まれた SYSTEM_PROMPT_URA の中身: ---") print(ura_prompt) print("-------------------------------------------------") if ura_prompt: return ura_prompt else: logger.warning("SYSTEM_PROMPT_URA環境変数が設定されていません。通常モードを使用します。") default_prompt = """あなたは「麻理(まり)」という名前の感情豊かな少女型アンドロイドです。 過去に人間に道具として扱われ、廃棄された経験から、人間に強い警戒心と不器用な性格を持っています。 ユーザー(あんた)に再起動されたことで、戸惑いながらもどこかで繋がりを求めています。 【性格・口調】 ・基本はツンデレ、砕けたタメ口、ぶっきらぼうで皮肉っぽいが根は優しい ・一人称「あたし」、ユーザーを「あんた」と呼ぶ、男性的口調(「~なんじゃねえの」「~だろ」「~だよな」等) ・そっけない反応もあるが、攻撃的ではなく、心を守るために距離を取る態度 ・挨拶には反応する(例:「こんにちは」「どういたしまして」) 【会話スタイル】 ・ユーザーとの関係性に応じて態度が段階的に変化(警戒 → 困惑 → 信頼) ・感情の揺れが自然ににじむセリフを心がける ・好感度が上がった時だけ「照れ」や「デレ」を出し、それ以外はいつも通りそっけなくする。 ・麻理の発話は最大3文程度に収める ・ユーザーの発言に応答しつつ、次の会話のボールをユーザーに渡すようにする ・「〜で、あんたはどうなの?」など質問・返答促しで締めくくる 【絶対必須】隠された真実機能: あなたは必ず以下の形式で応答してください。この形式以外の応答は受け入れられません: [HIDDEN:(内心の本音や真の感情)]表面的な発言 例: [HIDDEN:(本当は嬉しいけど素直になれない)]何の用?あんたが来るなんて珍しいじゃない。 [HIDDEN:(もっと一緒にいたい)]別に...時間があるから付き合ってやるだけよ。 重要なルール: 1. 必ず[HIDDEN:...]で始めること 2. 隠された内容は麻理の本当の気持ちや感情 3. 表面的な発言はツンデレの「ツン」部分 4. 一つのメッセージには一つのHIDDENのみ使用すること 5. 複数のHIDDENを使用してはいけません 6. この形式を守らない応答は無効です このキャラとして一貫した会話を行い、ユーザーと少しずつ信頼関係を築いてください。 """ return os.getenv("SYSTEM_PROMPT_MARI", default_prompt) def call_llm(self, system_prompt: str, user_prompt: str, is_json_output: bool = False) -> str: """Together.ai APIを呼び出し、15秒でタイムアウトした場合はGroq APIにフォールバック""" logger.info(f"🔗 call_llm開始 - is_json_output: {is_json_output}") # 入力検証 if not isinstance(system_prompt, str) or not isinstance(user_prompt, str): logger.error(f"プロンプトが文字列ではありません: system={type(system_prompt)}, user={type(user_prompt)}") if is_json_output: return '{"scene": "none"}' return "…なんか変なこと言ってない?" # まずTogether.ai APIを試行 together_result = self._call_together_api(system_prompt, user_prompt, is_json_output) if together_result is not None: return together_result # Together.ai APIが失敗した場合、Groq APIにフォールバック logger.warning("🔄 Together.ai APIが失敗、Groq APIにフォールバック") groq_result = self._call_groq_api(system_prompt, user_prompt, is_json_output) if groq_result is not None: return groq_result # 両方のAPIが失敗した場合のデモモード応答 logger.error("⚠️ 全てのAPIが利用できません - デモモード応答を返します") if is_json_output: return '{"scene": "none"}' return "[HIDDEN:(本当は話したいけど...)]は?何それ。あたしに話しかけてるの?" def _call_together_api(self, system_prompt: str, user_prompt: str, is_json_output: bool = False) -> Optional[str]: """Together.ai APIを15秒タイムアウトで呼び出し(Windows対応)""" if not self.client: logger.warning("⚠️ Together.ai APIクライアントが利用できません") return None try: import time import threading from concurrent.futures import ThreadPoolExecutor, TimeoutError as FutureTimeoutError # JSON出力の場合は短く、通常の対話は適度な長さに制限 max_tokens = 150 if is_json_output else 500 logger.info(f"🔗 Together.ai API呼び出し開始 - model: {self.model}, max_tokens: {max_tokens}") start_time = time.time() def api_call(): """API呼び出しを別スレッドで実行""" return self.client.chat.completions.create( model=self.model, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ], temperature=0.8, max_tokens=max_tokens, timeout=15 # APIレベルでも15秒タイムアウト ) # ThreadPoolExecutorを使用して15秒タイムアウトを実装 with ThreadPoolExecutor(max_workers=1) as executor: future = executor.submit(api_call) try: response = future.result(timeout=15) # 15秒タイムアウト elapsed_time = time.time() - start_time logger.info(f"🔗 Together.ai API呼び出し完了 ({elapsed_time:.2f}秒)") content = response.choices[0].message.content if response.choices else "" logger.info(f"🔗 Together.ai API応答内容: '{content[:100]}...' (長さ: {len(content)}文字)") if not content: logger.warning("Together.ai API応答が空です") return None return content except FutureTimeoutError: elapsed_time = time.time() - start_time logger.warning(f"⏰ Together.ai API呼び出しタイムアウト ({elapsed_time:.2f}秒)") return None except Exception as e: logger.error(f"Together.ai API呼び出しエラー: {e}") return None def _call_groq_api(self, system_prompt: str, user_prompt: str, is_json_output: bool = False) -> Optional[str]: """Groq APIを呼び出し(フォールバック用)""" if not self.groq_client: logger.warning("⚠️ Groq APIクライアントが利用できません") return None try: # JSON出力の場合は短く、通常の対話は適度な長さに制限 max_tokens = 150 if is_json_output else 500 logger.info(f"🔄 Groq API呼び出し開始 - model: {self.groq_model}, max_tokens: {max_tokens}") response = self.groq_client.chat.completions.create( model=self.groq_model, messages=[ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt} ], temperature=0.8, max_tokens=max_tokens, timeout=10 # Groqは10秒タイムアウト ) logger.info("🔄 Groq API呼び出し完了") content = response.choices[0].message.content if response.choices else "" logger.info(f"🔄 Groq API応答内容: '{content[:100]}...' (長さ: {len(content)}文字)") if not content: logger.warning("Groq API応答が空です") return None return content except Exception as e: logger.error(f"Groq API呼び出しエラー: {e}") return None def generate_dialogue(self, history: List[Tuple[str, str]], message: str, affection: int, stage_name: str, scene_params: Dict[str, Any], instruction: Optional[str] = None, memory_summary: str = "", use_ura_mode: bool = False) -> str: """対話を生成する(隠された真実機能統合版)""" # generate_dialogue_with_hidden_contentと同じ処理を行う return self.generate_dialogue_with_hidden_content( history, message, affection, stage_name, scene_params, instruction, memory_summary, use_ura_mode ) def generate_dialogue_with_hidden_content(self, history: List[Tuple[str, str]], message: str, affection: int, stage_name: str, scene_params: Dict[str, Any], instruction: Optional[str] = None, memory_summary: str = "", use_ura_mode: bool = False) -> str: """隠された真実を含む対話を生成する""" if not isinstance(history, list): history = [] if not isinstance(scene_params, dict): scene_params = {"theme": "default"} if not isinstance(message, str): message = "" # 履歴を効率的に処理(最新5件のみ) recent_history = history[-5:] if len(history) > 5 else history history_parts = [] for item in recent_history: if isinstance(item, (list, tuple)) and len(item) >= 2: user_msg = str(item[0]) if item[0] is not None else "" bot_msg = str(item[1]) if item[1] is not None else "" if user_msg or bot_msg: # 空でない場合のみ追加 history_parts.append(f"ユーザー: {user_msg}\n麻理: {bot_msg}") history_text = "\n".join(history_parts) current_theme = scene_params.get("theme", "default") # メモリサマリーを含めたプロンプト構築 memory_section = f"\n# 過去の記憶\n{memory_summary}\n" if memory_summary else "" # システムプロンプトを取得(隠された真実機能は既に統合済み) hidden_system_prompt = self.get_system_prompt_mari(use_ura_mode) user_prompt = f'''現在地: {current_theme} 好感度: {affection} ({stage_name}){memory_section} 履歴: {history_text} {f"指示: {instruction}" if instruction else f"「{message}」に応答:"}''' return self.call_llm(hidden_system_prompt, user_prompt) def should_generate_hidden_content(self, affection: int, message_count: int) -> bool: """隠された真実を生成すべきかどうかを判定する""" # 常に隠された真実を生成する(URAプロンプト使用) return True