Spaces:
Runtime error
Runtime error
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 |