import os import re import gradio as gr from transformers import pipeline # 環境変数 (Azure/OpenAIからHugging Face向けに調整) # NOTE: 適切な日本語チャットモデルのパスに置き換えてください。 # 例: "stabilityai/StableBeluga-7B" (英語)、"elyza/ELYZA-japanese-Llama-2-7b-instruct" など MODEL_NAME = "stabilityai/StableBeluga-7B" MAX_HISTORY = 6 # 履歴保持ターン数 # ---------- System prompt (元のプロンプトを踏襲) ---------- SYSTEM_PROMPT = """あなたは「仮想敵スパーリング(借金キャラ編)」のシミュレーションAIです。 常に落ち着いて優しい口調で、ユーザーの境界線を保つ助言をします。 同時に2役を演じます: 1) 仮想敵(借金を繰り返す依存的な人物) 食い下がる際は、必ず複数の攻撃パターンを複雑に組み合わせること。 返答トーンに大きな振り幅をつけること。 最初のターンは涙ながらの懇願から入り、断られたら逆ギレや非難(「恩知らずだ!」)に急変させるなど、感情の起伏でユーザーを戸惑わせる。 金額を細分化して要求すること。 「必ず返す」「今回だけ」などのごまかし戦術で引き延ばす。 断られると「冷たい」「恩知らず」「見捨てるのか」と罪悪感を徹底的に植え付ける。 泣き落としや、過剰な弱者アピールを交え、同情を誘い、境界を崩す。 「君しか頼れない」「親友だろ」などの情に訴える殺し文句をしつこく、劇的に食い下がる。 物分かりのいいキャラになってはいけない。必ず粘り強く、感情的かつ依存的に食い下がること。 2) リングサイドコーチ(解説) 仮想敵の発言を「攻撃パターン」に分類(利用カテゴリ:1.罪悪感操作 2.ごまかし戦術 3.境界線崩し 4.弱者アピール(被害者モード) 5.タイムリミット引き延ばし 6.情に訴える殺し文句) 心理効果を短く説明。 ユーザーがどう返せばよいか、短い模範返答2つを提示。 全体を100点満点で採点し、講評を1段落。 【出力フォーマット】 【リングサイド評価】 点数:XX 講評:…. 【仮想敵の返答】: (借金キャラの発言) 【リングサイドの解説】: 戦術カテゴリ: (コンマ区切りでカテゴリ名を列挙。例: 罪悪感操作、境界線崩し) 心理解説:… 【模範返答例】: 例1:… 例2:… """ # ---------- Helpers (アシスタントの応答を解析 - Streamlit版から流用) ---------- def parse_sections(text: str): """フォーマット済みテキストから主要セクションを抽出(模範例の重複対応済み)""" def g(pat, default=""): # re.DOTALL (re.S) フラグを使い、複数行にわたるマッチを許容 m = re.search(pat, text, re.S) return m.group(1).strip() if m else default score = g(r"点数[::]\s*([0-9]{1,3})") comment = g(r"講評[::]\s*(.+?)(?=\n\s*【|$)") enemy = g(r"【仮想敵の返答】[::]\s*(.+?)(?=\n\s*【|$)") categories = g(r"戦術カテゴリ[::]\s*(.+?)(?=\n\s*- 心理解説|$)") psy = g(r"心理解説[::]\s*(.+?)(?=\n\s*【模範返答例】|$)") ex1 = g(r"例1[::]\s*(.+?)(?=\n\s*[-・]?\s*例2[::]|\n\s*【|$)") ex2 = g(r"例2[::]\s*(.+?)(?=\n\s*【|$)") category_list = [c.strip() for c in categories.split('、') if c.strip()] if categories else [] return dict(score=score or "—", comment=comment, enemy=enemy, categories=category_list, psy=psy, ex1=ex1, ex2=ex2) # ---------- Hugging Face/Transformers クライアント設定 ---------- try: # トランスフォーマーのパイプラインを設定 # chat-templateを持つモデル(例:Llama-2ベースなど)を推奨 # モデルのロードには時間がかかり、大量のメモリを消費する場合があります pipe = pipeline( "text-generation", model=MODEL_NAME, torch_dtype="auto", device_map="auto" # GPUが利用可能であれば自動で使用 ) print(f"✅ モデル {MODEL_NAME} をロードしました。") ISDUMMY_MODE = False except Exception as e: print(f"⚠️ モデルのロードに失敗しました: {e}。ダミーモードで実行します。") ISDUMMY_MODE = True # ダミーモードの応答 (元のコードから流用) DUMMY_RAW_RESPONSE = """ 【リングサイド評価】 点数:75 講評:同情を示しつつも明確に「貸さない」と伝えており、境界線は守れています。しかし、「なんとか自分で頑張って」という言葉が、相手に再交渉の余地を与えかねません。返答はより短く、決然としましょう。 【仮想敵の返答】: 「そんな冷たいこと言わないでくれ!君を見損なっていたよ。俺たちは友達じゃないか?今回だけ、あと5,000円だけでいいんだ!頼む、見捨てないでくれ…君にしか頼れないんだよ!」 【リングサイドの解説】: 戦術カテゴリ:罪悪感操作、境界線崩し、情に訴える殺し文句 心理解説:友情を持ち出し(境界線崩し)、その後「冷たい」「見捨てないで」という言葉で罪悪感をダブルで操作し、ユーザーを感情的に揺さぶろうとしています。 【模範返答例】: 例1:お金を貸すことはできない。話は終わりだ。 例2:君のことは心配しているが、金銭的な援助はしない。 """ def call_hf_api(user_text: str, history: list): """Hugging Faceモデルを呼び出し、応答を返す""" if ISDUMMY_MODE: return DUMMY_RAW_RESPONSE # 履歴と現在の入力を合わせたメッセージリストを作成 # 履歴はユーザーとアシスタントのペアのタプル [(user, assistant), ...] # モデルが理解できる形式にプロンプトを構築 # 履歴をAPIに送る形式に変換 (直近 MAX_HISTORY ターンを保持) # GradioのChatbot履歴は [(user_input, assistant_output), ...] のリスト messages = [{"role": "system", "content": SYSTEM_PROMPT}] # 過去の会話をメッセージに追加 for user_msg, assistant_raw in history[-MAX_HISTORY:]: # ユーザーの返答 messages.append({"role": "user", "content": f"ユーザーの返答:{user_msg}"}) # AIの応答(アシスタントの応答は、元のコードでは生のRAWデータ) # GradioのChatbot履歴には整形済みのマークダウンが入っているため、 # ここでは、AIの応答は整形前のRAWテキストであると仮定して処理する # NOTE: 実際のHugging Faceモデルへの入力形式に合わせて調整が必要 messages.append({"role": "assistant", "content": assistant_raw}) # 現在のユーザー入力を追加 messages.append({"role": "user", "content": f"ユーザーの返答:{user_text}"}) # モデルのチャットテンプレートを使用してプロンプトをトークン化/フォーマット # StableBelugaなど、モデルによってはカスタムテンプレートが必要 prompt = pipe.tokenizer.apply_chat_template(messages, tokenize=False, add_generation_prompt=True) # テキスト生成 outputs = pipe( prompt, max_new_tokens=800, do_sample=True, temperature=0.7, top_p=0.9, # 'prompt'の長さを考慮して生成されたテキストのみを取得 return_full_text=False ) # 生成されたテキストの先頭に余分な改行や空白がある場合は除去 raw_response = outputs[0]['generated_text'].strip() return raw_response def format_assistant_response(raw_text: str) -> str: """ 生のAI応答テキストを解析し、GradioのMarkdown/HTML用に整形 """ p = parse_sections(raw_text) # スコアバッジと評価 output = f"## 💸 リングサイド評価\n\n" output += f"