""" BaseAI - すべてのAIモデルアダプターの基底クラス """ from abc import ABC, abstractmethod from typing import List, Tuple, Optional class BaseAI(ABC): """ すべてのAIモデルアダプターが実装すべき基底クラス 各モデルは以下のメソッドを実装する必要があります: - get_token_probabilities: トークン確率の取得 - build_chat_prompt: モデル固有のプロンプト形式への変換 """ @abstractmethod def get_token_probabilities(self, text: str, k: int = 5) -> List[Tuple[str, float]]: """ テキストから次のトークン候補と確率を取得 Args: text: 入力テキスト(プロンプト) k: 取得するトークン候補数 Returns: List[Tuple[str, float]]: (トークン, 確率)のリスト(確率順) """ raise NotImplementedError @abstractmethod def build_chat_prompt( self, user_content: str, system_content: str = "", assistant_content: Optional[str] = None ) -> str: """ モデル固有のチャットプロンプト形式に変換 注意: モデルによってuser/assistantの分離方法が異なります - OpenAI, Claude: user/assistantを明確に分離することを推奨 - Gemini: user/assistantを分離しない方が良い場合もある - Transformers: モデルによって異なる(Llamaは分離推奨) Args: user_content: ユーザーのメッセージ system_content: システムプロンプト(オプション) assistant_content: アシスタントの既存応答(会話履歴用、オプション) Returns: str: モデル固有のプロンプト形式 """ raise NotImplementedError def _clean_text(self, text: str) -> str: """ 制御文字・不可視文字・置換文字を厳密に取り除く(共通処理) Args: text: クリーンアップするテキスト Returns: str: クリーンアップされたテキスト """ if not text: return "" # 制御文字(0x00-0x1F、0x7F-0x9F)を除去 # ただし、改行・タブ・復帰は許可 cleaned = [] for ch in text: code = ord(ch) # 許可する制御文字: 改行(0x0A), タブ(0x09), 復帰(0x0D) if code in [0x09, 0x0A, 0x0D]: cleaned.append(ch) # 通常の印刷可能文字 elif ch.isprintable(): # 置換文字(U+FFFD)を除去 if ch != "\uFFFD": cleaned.append(ch) # その他の制御文字や不可視文字は除去 result = "".join(cleaned) # ゼロ幅文字を除去 result = result.replace("\u200B", "") # Zero-width space result = result.replace("\u200C", "") # Zero-width non-joiner result = result.replace("\u200D", "") # Zero-width joiner result = result.replace("\uFEFF", "") # Zero-width no-break space # その他の不可視文字(結合文字など)を除去 result = result.replace("\u200E", "") # Left-to-right mark result = result.replace("\u200F", "") # Right-to-left mark result = result.replace("\u202A", "") # Left-to-right embedding result = result.replace("\u202B", "") # Right-to-left embedding result = result.replace("\u202C", "") # Pop directional formatting result = result.replace("\u202D", "") # Left-to-right override result = result.replace("\u202E", "") # Right-to-left override return result.strip()