mari-chat-3 / core_dialogue.py
sirochild's picture
Upload core_dialogue.py
379221a verified
"""
対話生成モジュール
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