From 341249843848f93b52880fccad4a0f2ac4fbcffb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 23 Jan 2026 23:57:27 +0000 Subject: [PATCH 1/5] Initial plan From f4157b52eac03a369f9b2b7c0f96a44473272d32 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 00:02:47 +0000 Subject: [PATCH 2/5] Fix agent tool_calls integrity validation Co-authored-by: jxxghp <51039935+jxxghp@users.noreply.github.com> --- app/agent/__init__.py | 76 +++++++++++++++++++++++++++++++++++++++---- 1 file changed, 69 insertions(+), 7 deletions(-) diff --git a/app/agent/__init__.py b/app/agent/__init__.py index e9e849ae..34a491cf 100644 --- a/app/agent/__init__.py +++ b/app/agent/__init__.py @@ -98,14 +98,15 @@ class MoviePilotAgent: user_id=self.user_id ) if messages: + loaded_messages = [] for msg in messages: if msg.get("role") == "user": - chat_history.add_message(HumanMessage(content=msg.get("content", ""))) + loaded_messages.append(HumanMessage(content=msg.get("content", ""))) elif msg.get("role") == "agent": - chat_history.add_message(AIMessage(content=msg.get("content", ""))) + loaded_messages.append(AIMessage(content=msg.get("content", ""))) elif msg.get("role") == "tool_call": metadata = msg.get("metadata", {}) - chat_history.add_message( + loaded_messages.append( AIMessage( content=msg.get("content", ""), tool_calls=[ @@ -119,12 +120,18 @@ class MoviePilotAgent: ) elif msg.get("role") == "tool_result": metadata = msg.get("metadata", {}) - chat_history.add_message(ToolMessage( + loaded_messages.append(ToolMessage( content=msg.get("content", ""), tool_call_id=metadata.get("call_id", "unknown") )) elif msg.get("role") == "system": - chat_history.add_message(SystemMessage(content=msg.get("content", ""))) + loaded_messages.append(SystemMessage(content=msg.get("content", ""))) + + # 验证并修复工具调用的完整性 + validated_messages = self._ensure_tool_call_integrity(loaded_messages) + for msg in validated_messages: + chat_history.add_message(msg) + return chat_history @staticmethod @@ -192,13 +199,62 @@ class MoviePilotAgent: # 发生错误时返回一个保守的估算值 return len(str(messages)) // 4 + def _ensure_tool_call_integrity(self, messages: List[Union[HumanMessage, AIMessage, ToolMessage, SystemMessage]]) \ + -> List[Union[HumanMessage, AIMessage, ToolMessage, SystemMessage]]: + """ + 确保工具调用的完整性: + 1. 如果AIMessage包含tool_calls,必须后跟相应的ToolMessage + 2. 移除孤立的AIMessage(有tool_calls但没有对应的ToolMessage) + """ + if not messages: + return messages + + validated_messages = [] + i = 0 + + while i < len(messages): + msg = messages[i] + + # 检查是否是包含tool_calls的AIMessage + if isinstance(msg, AIMessage) and getattr(msg, 'tool_calls', None): + tool_call_ids = {tc.get('id') if isinstance(tc, dict) else tc.id + for tc in msg.tool_calls} + + # 查找后续的ToolMessage + j = i + 1 + found_tool_messages = [] + while j < len(messages) and isinstance(messages[j], ToolMessage): + found_tool_messages.append(messages[j]) + j += 1 + + # 检查是否所有tool_call都有对应的ToolMessage + found_tool_call_ids = {tm.tool_call_id for tm in found_tool_messages} + + if not tool_call_ids.issubset(found_tool_call_ids): + # 如果缺少某些tool_call的响应,移除这个AIMessage + logger.warning(f"移除不完整的tool_call AIMessage: 缺少tool_call响应") + i += 1 + continue + else: + # 添加AIMessage和所有对应的ToolMessage + validated_messages.append(msg) + validated_messages.extend(found_tool_messages) + i = j + continue + else: + validated_messages.append(msg) + + i += 1 + + return validated_messages + def _create_agent_executor(self) -> RunnableWithMessageHistory: """ 创建Agent执行器 """ try: # 消息裁剪器,防止上下文超出限制 - trimmer = trim_messages( + base_trimmer = trim_messages( max_tokens=settings.LLM_MAX_CONTEXT_TOKENS * 1000 * 0.8, strategy="last", token_counter=self._token_counter, @@ -206,6 +262,12 @@ class MoviePilotAgent: allow_partial=False, start_on="human", ) + + # 包装trimmer,在裁剪后验证工具调用的完整性 + def validated_trimmer(messages): + trimmed = base_trimmer(messages) + return self._ensure_tool_call_integrity(trimmed) + # 创建Agent执行链 agent = ( RunnablePassthrough.assign( @@ -214,7 +276,7 @@ class MoviePilotAgent: ) ) | self.prompt - | trimmer + | validated_trimmer | self.llm.bind_tools(self.tools) | OpenAIToolsAgentOutputParser() ) From 179cc61f65cf6b041d13a5174400cf96e68c833d Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 00:05:21 +0000 Subject: [PATCH 3/5] Fix tool call integrity validation to skip orphaned ToolMessages Co-authored-by: jxxghp <51039935+jxxghp@users.noreply.github.com> --- app/agent/__init__.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/app/agent/__init__.py b/app/agent/__init__.py index 34a491cf..83691f6e 100644 --- a/app/agent/__init__.py +++ b/app/agent/__init__.py @@ -204,7 +204,7 @@ class MoviePilotAgent: """ 确保工具调用的完整性: 1. 如果AIMessage包含tool_calls,必须后跟相应的ToolMessage - 2. 移除孤立的AIMessage(有tool_calls但没有对应的ToolMessage) + 2. 移除孤立的AIMessage(有tool_calls但没有对应的ToolMessage)及其关联的ToolMessage """ if not messages: return messages @@ -231,9 +231,9 @@ class MoviePilotAgent: found_tool_call_ids = {tm.tool_call_id for tm in found_tool_messages} if not tool_call_ids.issubset(found_tool_call_ids): - # 如果缺少某些tool_call的响应,移除这个AIMessage - logger.warning(f"移除不完整的tool_call AIMessage: 缺少tool_call响应") - i += 1 + # 如果缺少某些tool_call的响应,移除这个AIMessage及其相关的ToolMessage + logger.warning(f"移除不完整的tool_call AIMessage及其部分ToolMessage: 缺少tool_call响应") + i = j # 跳过AIMessage和所有相关的ToolMessage continue else: # 添加AIMessage和所有对应的ToolMessage From 712995dcf339a2d3d3e5bb436d73a15de648b6df Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 00:08:25 +0000 Subject: [PATCH 4/5] Address code review feedback: fix ToolCall handling and add orphaned message filtering Co-authored-by: jxxghp <51039935+jxxghp@users.noreply.github.com> --- app/agent/__init__.py | 36 ++++++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 14 deletions(-) diff --git a/app/agent/__init__.py b/app/agent/__init__.py index 83691f6e..2144d2c8 100644 --- a/app/agent/__init__.py +++ b/app/agent/__init__.py @@ -127,7 +127,7 @@ class MoviePilotAgent: elif msg.get("role") == "system": loaded_messages.append(SystemMessage(content=msg.get("content", ""))) - # 验证并修复工具调用的完整性 + # Validate and fix tool call integrity validated_messages = self._ensure_tool_call_integrity(loaded_messages) for msg in validated_messages: chat_history.add_message(msg) @@ -202,9 +202,10 @@ class MoviePilotAgent: def _ensure_tool_call_integrity(self, messages: List[Union[HumanMessage, AIMessage, ToolMessage, SystemMessage]]) \ -> List[Union[HumanMessage, AIMessage, ToolMessage, SystemMessage]]: """ - 确保工具调用的完整性: - 1. 如果AIMessage包含tool_calls,必须后跟相应的ToolMessage - 2. 移除孤立的AIMessage(有tool_calls但没有对应的ToolMessage)及其关联的ToolMessage + Ensure tool call integrity: + 1. AIMessage with tool_calls must be followed by corresponding ToolMessages + 2. Remove incomplete AIMessages (with tool_calls but missing ToolMessage responses) and their partial ToolMessages + 3. Filter out orphaned ToolMessages that don't correspond to any tool_call """ if not messages: return messages @@ -215,32 +216,39 @@ class MoviePilotAgent: while i < len(messages): msg = messages[i] - # 检查是否是包含tool_calls的AIMessage + # Check if this is an AIMessage with tool_calls if isinstance(msg, AIMessage) and getattr(msg, 'tool_calls', None): - tool_call_ids = {tc.get('id') if isinstance(tc, dict) else tc.id - for tc in msg.tool_calls} + # Extract tool_call IDs (ToolCall is a TypedDict, so it's a dict at runtime) + tool_call_ids = {tc.get('id') or tc.get('name') for tc in msg.tool_calls} - # 查找后续的ToolMessage + # Find subsequent ToolMessages j = i + 1 found_tool_messages = [] while j < len(messages) and isinstance(messages[j], ToolMessage): found_tool_messages.append(messages[j]) j += 1 - # 检查是否所有tool_call都有对应的ToolMessage + # Check if all tool_calls have corresponding ToolMessages found_tool_call_ids = {tm.tool_call_id for tm in found_tool_messages} if not tool_call_ids.issubset(found_tool_call_ids): - # 如果缺少某些tool_call的响应,移除这个AIMessage及其相关的ToolMessage - logger.warning(f"移除不完整的tool_call AIMessage及其部分ToolMessage: 缺少tool_call响应") - i = j # 跳过AIMessage和所有相关的ToolMessage + # Missing some tool_call responses, skip this AIMessage and all its ToolMessages + logger.warning("Removing incomplete tool_call AIMessage and its partial ToolMessages: missing tool_call responses") + i = j # Skip the AIMessage and all related ToolMessages continue else: - # 添加AIMessage和所有对应的ToolMessage + # Add the AIMessage and only the ToolMessages that correspond to its tool_calls validated_messages.append(msg) - validated_messages.extend(found_tool_messages) + for tm in found_tool_messages: + if tm.tool_call_id in tool_call_ids: + validated_messages.append(tm) i = j continue + # Skip orphaned ToolMessages (not preceded by an AIMessage with tool_calls) + elif isinstance(msg, ToolMessage): + logger.warning(f"Skipping orphaned ToolMessage with tool_call_id: {msg.tool_call_id}") + i += 1 + continue else: validated_messages.append(msg) From 54422b5181c32baa7227f5ec3edffed4bbe37028 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Sat, 24 Jan 2026 00:10:00 +0000 Subject: [PATCH 5/5] Final refinements: fix falsy value handling and add warning for extra ToolMessages Co-authored-by: jxxghp <51039935+jxxghp@users.noreply.github.com> --- app/agent/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/app/agent/__init__.py b/app/agent/__init__.py index 2144d2c8..7c1f31f2 100644 --- a/app/agent/__init__.py +++ b/app/agent/__init__.py @@ -219,7 +219,10 @@ class MoviePilotAgent: # Check if this is an AIMessage with tool_calls if isinstance(msg, AIMessage) and getattr(msg, 'tool_calls', None): # Extract tool_call IDs (ToolCall is a TypedDict, so it's a dict at runtime) - tool_call_ids = {tc.get('id') or tc.get('name') for tc in msg.tool_calls} + tool_call_ids = { + tc.get('id') if tc.get('id') is not None else tc.get('name') + for tc in msg.tool_calls + } # Find subsequent ToolMessages j = i + 1 @@ -231,6 +234,11 @@ class MoviePilotAgent: # Check if all tool_calls have corresponding ToolMessages found_tool_call_ids = {tm.tool_call_id for tm in found_tool_messages} + # Warn if there are extra ToolMessages that don't correspond to any tool_call + extra_tool_messages = found_tool_call_ids - tool_call_ids + if extra_tool_messages: + logger.warning(f"Found extra ToolMessages that don't correspond to any tool_call: {extra_tool_messages}") + if not tool_call_ids.issubset(found_tool_call_ids): # Missing some tool_call responses, skip this AIMessage and all its ToolMessages logger.warning("Removing incomplete tool_call AIMessage and its partial ToolMessages: missing tool_call responses")