""" メモリ管理モジュール 会話履歴から重要単語を抽出し、トークン使用量を最適化する """ import logging import re from typing import List, Dict, Tuple, Any from collections import Counter import json logger = logging.getLogger(__name__) class MemoryManager: """会話履歴のメモリ管理を行うクラス""" def __init__(self, history_threshold: int = 10): """ Args: history_threshold: 履歴圧縮を実行する会話数の閾値 """ self.history_threshold = history_threshold self.important_words_cache = [] self.special_memories = {} # 手紙などの特別な記憶を保存 def extract_important_words(self, messages: List[Dict[str, str]], dialogue_generator=None) -> List[str]: """ 会話履歴から重要単語を抽出する(ルールベースのみ) Args: messages: チャットメッセージのリスト dialogue_generator: 対話生成器(使用しない) Returns: 重要単語のリスト """ try: # メッセージからテキストを結合 text_content = [] for msg in messages: if msg.get("content"): text_content.append(msg["content"]) combined_text = " ".join(text_content) # ルールベースの抽出のみ使用 return self._extract_with_rules(combined_text) except Exception as e: logger.error(f"重要単語抽出エラー: {e}") return self._extract_with_rules(" ".join([msg.get("content", "") for msg in messages])) def _extract_with_rules(self, text: str) -> List[str]: """ ルールベースで重要単語を抽出する(強化版) Args: text: 抽出対象のテキスト Returns: 重要単語のリスト """ try: # 基本的なクリーニング text = re.sub(r'[^\w\s]', ' ', text) words = text.split() # ストップワードを除外 stop_words = { 'の', 'に', 'は', 'を', 'が', 'で', 'と', 'から', 'まで', 'より', 'だ', 'である', 'です', 'ます', 'した', 'する', 'される', 'これ', 'それ', 'あれ', 'この', 'その', 'あの', 'ここ', 'そこ', 'あそこ', 'どこ', 'いつ', 'なに', 'なぜ', 'ちょっと', 'とても', 'すごく', 'かなり', 'もう', 'まだ', 'でも', 'しかし', 'だから', 'そして', 'また', 'さらに', 'あたし', 'お前', 'ユーザー', 'システム', 'アプリ' } # 重要カテゴリのキーワード important_categories = { 'food': ['コーヒー', 'お茶', '紅茶', 'ケーキ', 'パン', '料理', '食べ物', '飲み物'], 'hobby': ['読書', '映画', '音楽', 'ゲーム', 'スポーツ', '散歩', '旅行'], 'emotion': ['嬉しい', '悲しい', '楽しい', '怒り', '不安', '安心', '幸せ'], 'place': ['家', '学校', '会社', '公園', 'カフェ', '図書館', '駅', '街'], 'time': ['朝', '昼', '夜', '今日', '明日', '昨日', '週末', '平日'], 'color': ['赤', '青', '緑', '黄色', '白', '黒', 'ピンク', '紫'], 'weather': ['晴れ', '雨', '曇り', '雪', '暑い', '寒い', '暖かい', '涼しい'] } # 重要そうなパターンを優先 important_patterns = [ r'[A-Za-z]{3,}', # 英単語(3文字以上) r'[ァ-ヶー]{2,}', # カタカナ(2文字以上) r'[一-龯]{2,}', # 漢字(2文字以上) ] important_words = [] # パターンマッチング for pattern in important_patterns: matches = re.findall(pattern, text) important_words.extend(matches) # カテゴリ別重要語句の検出 for category, keywords in important_categories.items(): for keyword in keywords: if keyword in text: important_words.append(keyword) # 頻度でフィルタリング word_counts = Counter(important_words) filtered_words = [] for word, count in word_counts.items(): if (len(word) >= 2 and word not in stop_words and not word.isdigit() and # 数字のみは除外 count >= 1): # 最低1回は出現 filtered_words.append(word) # 重要度でソート(頻度 + カテゴリ重要度) def get_importance_score(word): base_score = word_counts[word] # カテゴリに含まれる語句は重要度アップ for keywords in important_categories.values(): if word in keywords: base_score += 2 # 長い語句は重要度アップ if len(word) >= 4: base_score += 1 return base_score # 重要度順でソートして上位15個を返す sorted_words = sorted(filtered_words, key=get_importance_score, reverse=True) return sorted_words[:15] except Exception as e: logger.error(f"ルールベース抽出エラー: {e}") return [] def should_compress_history(self, messages: List[Dict[str, str]]) -> bool: """ 履歴を圧縮すべきかどうかを判定する Args: messages: チャットメッセージのリスト Returns: 圧縮が必要かどうか """ # ユーザーとアシスタントのペア数をカウント user_messages = [msg for msg in messages if msg.get("role") == "user"] return len(user_messages) >= self.history_threshold def compress_history(self, messages: List[Dict[str, str]], dialogue_generator=None) -> Tuple[List[Dict[str, str]], List[str]]: """ 履歴を圧縮し、重要単語を抽出する Args: messages: チャットメッセージのリスト dialogue_generator: 対話生成器 Returns: (圧縮後のメッセージリスト, 抽出された重要単語のリスト) """ try: if not self.should_compress_history(messages): return messages, self.important_words_cache # 最新の数ターンを保持 keep_recent = 4 # 最新4ターン(ユーザー2回、アシスタント2回)を保持 # 古い履歴から重要単語を抽出 old_messages = messages[:-keep_recent] if len(messages) > keep_recent else [] recent_messages = messages[-keep_recent:] if len(messages) > keep_recent else messages if old_messages: # 重要単語を抽出 new_keywords = self.extract_important_words(old_messages, dialogue_generator) # 既存のキーワードと統合(重複除去) all_keywords = list(set(self.important_words_cache + new_keywords)) self.important_words_cache = all_keywords[:20] # 最大20個のキーワードを保持 logger.info(f"履歴を圧縮しました。抽出されたキーワード: {new_keywords}") return recent_messages, self.important_words_cache except Exception as e: logger.error(f"履歴圧縮エラー: {e}") return messages, self.important_words_cache def get_memory_summary(self) -> str: """ 保存されている重要単語から記憶の要約を生成する Returns: 記憶の要約文字列 """ summary_parts = [] # 通常の重要単語 if self.important_words_cache: keywords_text = "、".join(self.important_words_cache) summary_parts.append(f"過去の会話で言及された重要な要素: {keywords_text}") # 特別な記憶(手紙など) if self.special_memories: for memory_type, memories in self.special_memories.items(): if memories: latest_memory = memories[-1]["content"] if memory_type == "letter_content": summary_parts.append(f"最近の手紙の記憶: {latest_memory}") else: summary_parts.append(f"{memory_type}: {latest_memory}") return "\n".join(summary_parts) if summary_parts else "" def add_important_memory(self, memory_type: str, content: str) -> str: """ 重要な記憶を追加する(手紙の内容など) Args: memory_type: 記憶の種類(例: "letter_content") content: 記憶する内容 Returns: ユーザーに表示する通知メッセージ """ if memory_type not in self.special_memories: self.special_memories[memory_type] = [] self.special_memories[memory_type].append({ "content": content, "timestamp": logging.Formatter().formatTime(logging.LogRecord("", 0, "", 0, "", (), None)) }) # 最大5件まで保持 if len(self.special_memories[memory_type]) > 5: self.special_memories[memory_type] = self.special_memories[memory_type][-5:] logger.info(f"特別な記憶を追加しました: {memory_type}") # 記憶の種類に応じた通知メッセージを生成 if memory_type == "letter_content": return "🧠✨ 麻理の記憶に新しい手紙の内容が刻まれました。今後の会話でこの記憶を参照することがあります。" else: return f"🧠✨ 麻理の記憶に新しい{memory_type}が追加されました。" def get_special_memories(self, memory_type: str = None) -> Dict[str, Any]: """ 特別な記憶を取得する Args: memory_type: 取得する記憶の種類(Noneの場合は全て) Returns: 記憶の辞書 """ if memory_type: return self.special_memories.get(memory_type, []) return self.special_memories def clear_memory(self): """メモリをクリアする""" self.important_words_cache = [] self.special_memories = {} logger.info("メモリをクリアしました") def get_memory_stats(self) -> Dict[str, Any]: """ メモリの統計情報を取得する Returns: 統計情報の辞書 """ return { "cached_keywords_count": len(self.important_words_cache), "cached_keywords": self.important_words_cache, "special_memories_count": sum(len(memories) for memories in self.special_memories.values()), "special_memories": self.special_memories, "history_threshold": self.history_threshold }