""" Context 压缩 基于 Goal 状态进行增量压缩: - 当 Goal 完成或放弃时,将相关的详细 messages 替换为 summary """ from typing import List, Dict, Any, Optional from .goal_models import GoalTree, Goal def compress_messages_for_goal( messages: List[Dict[str, Any]], goal_id: str, summary: str, ) -> List[Dict[str, Any]]: """ 压缩指定 goal 关联的 messages 将 goal_id 关联的所有详细 messages 替换为一条 summary message。 Args: messages: 原始消息列表 goal_id: 要压缩的 goal ID summary: 压缩后的摘要 Returns: 压缩后的消息列表 """ # 分离:关联的 messages vs 其他 messages related = [] other = [] for msg in messages: if msg.get("goal_id") == goal_id: related.append(msg) else: other.append(msg) # 如果没有关联的消息,直接返回 if not related: return messages # 找到第一条关联消息的位置(用于插入 summary) first_related_index = None for i, msg in enumerate(messages): if msg.get("goal_id") == goal_id: first_related_index = i break # 创建 summary message summary_message = { "role": "assistant", "content": f"[Goal {goal_id} Summary] {summary}", "goal_id": goal_id, "is_summary": True, } # 构建新的消息列表 result = [] summary_inserted = False for i, msg in enumerate(messages): if msg.get("goal_id") == goal_id: # 跳过关联的详细消息,在第一个位置插入 summary if not summary_inserted: result.append(summary_message) summary_inserted = True else: result.append(msg) return result def should_compress(goal: Goal) -> bool: """判断 goal 是否需要压缩""" return goal.status in ("completed", "abandoned") and goal.summary is not None def compress_all_completed( messages: List[Dict[str, Any]], tree: GoalTree, ) -> List[Dict[str, Any]]: """ 压缩所有已完成/已放弃的 goals 遍历 GoalTree,对所有需要压缩的 goal 执行压缩。 Args: messages: 原始消息列表 tree: GoalTree 实例 Returns: 压缩后的消息列表 """ result = messages def process_goal(goal: Goal): nonlocal result if should_compress(goal): # 检查是否已经压缩过(避免重复压缩) already_compressed = any( msg.get("goal_id") == goal.id and msg.get("is_summary") for msg in result ) if not already_compressed: result = compress_messages_for_goal(result, goal.id, goal.summary) # 递归处理子目标 for child in goal.children: process_goal(child) for goal in tree.goals: process_goal(goal) return result def get_messages_for_goal( messages: List[Dict[str, Any]], goal_id: str, ) -> List[Dict[str, Any]]: """获取指定 goal 关联的所有 messages""" return [msg for msg in messages if msg.get("goal_id") == goal_id] def count_tokens_estimate(messages: List[Dict[str, Any]]) -> int: """ 估算消息的 token 数量(简单估算) 实际使用时应该用 tiktoken 或 API 返回的 token 数。 这里用简单的字符数 / 4 来估算。 """ total_chars = 0 for msg in messages: content = msg.get("content", "") if isinstance(content, str): total_chars += len(content) elif isinstance(content, list): # 多模态消息 for part in content: if isinstance(part, dict) and part.get("type") == "text": total_chars += len(part.get("text", "")) return total_chars // 4