File size: 14,674 Bytes
a73fa4e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90babcf
 
a73fa4e
90babcf
a73fa4e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90babcf
 
 
 
 
 
 
 
 
 
 
 
379221a
 
 
90babcf
 
 
a73fa4e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90babcf
65d030c
 
a73fa4e
 
 
 
 
 
 
90babcf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a73fa4e
90babcf
 
 
 
a73fa4e
 
65d030c
a73fa4e
90babcf
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
a73fa4e
 
 
 
 
 
90babcf
a73fa4e
 
90babcf
65d030c
a73fa4e
90babcf
65d030c
a73fa4e
90babcf
 
a73fa4e
 
 
 
90babcf
 
a73fa4e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
"""
対話生成モジュール
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