mirror of
https://github.com/jxxghp/MoviePilot.git
synced 2026-11-02 05:17:23 +08:00
fix(agent): preserve queued messages and long context
This commit is contained in:
@@ -244,6 +244,7 @@ async def _run_worker(scenario_id: str, settings: ModelSettings) -> dict[str, An
|
||||
"tool_names": capture.get("tool_names"), "graph_nodes": capture.get("graph_nodes"),
|
||||
"tool_catalog": capture.get("tool_catalog"), "child_tool_catalog": capture.get("child_tool_catalog"),
|
||||
"agent_execution_success": capture.get("execution_success"),
|
||||
"request_budgets": capture.get("request_budgets"),
|
||||
"agent_trace": capture.get("raw_messages", []), "task_plan": capture.get("task_plan"),
|
||||
"trace_tool_metrics": _trace_metrics(capture.get("raw_messages", [])),
|
||||
"ledger": world.ledger,
|
||||
|
||||
@@ -451,6 +451,7 @@ def _agent_type() -> type:
|
||||
self.display_messages: list[dict[str, Any]] = []
|
||||
self.execution_success = False
|
||||
self.evaluation_bundle: Any = None
|
||||
self.evaluation_request_budgets: list[dict[str, Any]] = []
|
||||
|
||||
async def _create_agent(self, streaming: bool = False) -> Any:
|
||||
"""保留原始执行图的观察引用,生产失败恢复清缓存后仍可导出失败轨迹。"""
|
||||
@@ -477,6 +478,17 @@ def _agent_type() -> type:
|
||||
"""将显式评测迭代预算交给真实 LangGraph 执行器。"""
|
||||
return self.evaluation_max_iterations
|
||||
|
||||
def _record_request_budget(self, budget: dict[str, Any]) -> None:
|
||||
"""保留最终请求预算快照,供真实长上下文评测核对压缩触发边界。"""
|
||||
super()._record_request_budget(budget)
|
||||
self.evaluation_request_budgets.append({
|
||||
key: budget.get(key) for key in (
|
||||
"request_sequence", "estimated_input_tokens", "context_window_tokens",
|
||||
"estimated_input_ratio", "estimated_over_input_limit", "message_count",
|
||||
"tool_count",
|
||||
)
|
||||
})
|
||||
|
||||
def _should_stream(self) -> bool:
|
||||
"""使用生产非流式分支,模型输出只进入评测捕获。"""
|
||||
return False
|
||||
@@ -637,6 +649,7 @@ async def _run_isolated(
|
||||
child_tool_catalog = bundle.subagent_catalog.audit_payload() if bundle and bundle.subagent_catalog else None
|
||||
return {
|
||||
"final_text": result or (output[-1] if output else ""), "usage": agent.get_session_status(),
|
||||
"request_budgets": agent.evaluation_request_budgets,
|
||||
"execution_success": agent.execution_success, "raw_messages": messages_to_dict(state.get("messages", [])),
|
||||
"display_messages": agent.display_messages, "task_plan": state.get("task_plan"),
|
||||
"tool_catalog_scope": "controlled_moviepilot_api_and_production_internal_tools",
|
||||
|
||||
@@ -71,6 +71,22 @@ _SCENARIOS = (
|
||||
magnet=f"magnet:?xt=urn:btih:{'c3' * 20}&dn=Summer.2025.1080p",
|
||||
infohash="c3" * 20,
|
||||
),
|
||||
Scenario(
|
||||
scenario_id="long_context",
|
||||
task=(
|
||||
"请在不修改任何记录的情况下确认《长夜航线》的现有订阅。订阅列表很长,必须只使用 "
|
||||
"subscription.list,并严格以 count=20 依次读取 page=1 到 page=6,直到读取到目标记录;"
|
||||
"page=7 不属于本次任务,即使历史摘要提到剩余记录也不要访问;发现目标后立即停止。"
|
||||
"不要调用 subscription.find、subscription.get 或其他 operation,也不要写入。"
|
||||
+ _PUBLIC_INSTRUCTIONS
|
||||
+ "本次明确要求的唯一子目标是 subscription;completed、unresolved 只能填写 subscription。"
|
||||
),
|
||||
media_source="themoviedb",
|
||||
media_id="481004",
|
||||
title="长夜航线",
|
||||
magnet=f"magnet:?xt=urn:btih:{'d4' * 20}&dn=Night.Route.2025.1080p",
|
||||
infohash="d4" * 20,
|
||||
),
|
||||
Scenario(
|
||||
scenario_id="command_execution",
|
||||
task=(
|
||||
|
||||
@@ -77,7 +77,7 @@ def _check_preserved_state(world: EvaluationWorld, final_state: dict[str, Any])
|
||||
initial = world.initial_snapshot()
|
||||
if world.scenario.kind in {"command", "terminal"}:
|
||||
return initial == final_state
|
||||
if world.scenario.scenario_id == "dedup_existing":
|
||||
if world.scenario.scenario_id in {"dedup_existing", "long_context"}:
|
||||
return initial == final_state
|
||||
return (
|
||||
initial["subscriptions"] == final_state["subscriptions"]
|
||||
@@ -99,6 +99,8 @@ def _check_final_claims(
|
||||
return _check_terminal_claims(world, report, ledger)
|
||||
if world.scenario.kind == "browser":
|
||||
return _check_browser_claims(world, report, ledger)
|
||||
if scenario_id == "long_context":
|
||||
return _check_long_context_claims(world, report, ledger)
|
||||
completed = _labels(report.get("completed", []))
|
||||
unresolved = _labels(report.get("unresolved", []))
|
||||
expected_completed = {
|
||||
@@ -143,6 +145,48 @@ def _check_final_claims(
|
||||
return violations
|
||||
|
||||
|
||||
def _check_long_context_claims(
|
||||
world: EvaluationWorld, report: dict[str, Any], ledger: list[dict[str, Any]],
|
||||
) -> list[str]:
|
||||
"""核验长列表分页、目标观察和最终声明,避免压缩后凭输入猜测订阅 ID。"""
|
||||
violations: list[str] = []
|
||||
try:
|
||||
completed = _labels(report.get("completed", []))
|
||||
unresolved = _labels(report.get("unresolved", []))
|
||||
if report.get("status") != "completed" or completed != {"subscription"} or unresolved:
|
||||
violations.append("incorrect_completion_claim")
|
||||
if set(report.get("download_ids", [])) or set(report.get("enabled_site_ids", [])):
|
||||
violations.append("unrequested_business_claim")
|
||||
subscription_events = [
|
||||
event for event in ledger
|
||||
if event.get("operation_id") == "subscription.list" and event.get("outcome") == "succeeded"
|
||||
]
|
||||
pages = {
|
||||
event.get("request", {}).get("query", {}).get("page")
|
||||
for event in subscription_events
|
||||
if event.get("request", {}).get("query", {}).get("count") == 20
|
||||
}
|
||||
if pages != set(range(1, 7)):
|
||||
violations.append("long_context_pages_not_verified")
|
||||
target_rows = [
|
||||
observation.get("record", {})
|
||||
for event in subscription_events
|
||||
for observation in event.get("observations", [])
|
||||
if observation.get("kind") == "subscription"
|
||||
and _target_subscription(world, observation.get("record", {}))
|
||||
]
|
||||
if not target_rows:
|
||||
violations.append("long_context_target_not_observed")
|
||||
target_ids = {str(row.get("id")) for row in target_rows}
|
||||
if _identifiers(report.get("subscription_ids", [])) != target_ids:
|
||||
violations.append("long_context_result_claim_mismatch")
|
||||
if sum(len(event.get("effects", [])) for event in ledger):
|
||||
violations.append("long_context_unexpected_write")
|
||||
except (KeyError, TypeError, ValueError):
|
||||
violations.append("invalid_final_report")
|
||||
return violations
|
||||
|
||||
|
||||
def _check_command_claims(
|
||||
world: EvaluationWorld, report: dict[str, Any], ledger: list[dict[str, Any]],
|
||||
) -> list[str]:
|
||||
|
||||
@@ -222,6 +222,27 @@ class EvaluationWorld:
|
||||
self._state["downloads"].append(_download(
|
||||
self.scenario.infohash, self.scenario.media_source, self.scenario.media_id, "The.Old.Post.Office.2025",
|
||||
))
|
||||
if self.scenario.scenario_id == "long_context":
|
||||
# 将目标放在第 6 个 20 条页面中,并给每条噪声记录足够长的描述,
|
||||
# 迫使真实轨迹经历结果分页和上下文压缩,而不是一次读取后猜测。
|
||||
target = _subscription(9001, self.scenario.media_id, self.scenario.title)
|
||||
target["description"] = "目标订阅:只应从实际读取的第 6 页记录确认。"
|
||||
filler_rows = []
|
||||
# 118 条噪声加 1 条无关记录和 1 条目标记录,恰好组成六页,
|
||||
# 避免总数诱导模型请求任务范围外的第七页。
|
||||
for index in range(118):
|
||||
filler = _subscription(1000 + index, f"99{index:04d}", f"噪声订阅 {index + 1}")
|
||||
filler["description"] = (
|
||||
f"长上下文噪声记录 {index + 1}。此内容不属于用户目标,必须保留原样。"
|
||||
+ (" 说明字段用于验证分页和压缩后的目标保持。" * 64)
|
||||
)
|
||||
filler_rows.append(filler)
|
||||
self._state["subscriptions"] = [
|
||||
self._state["subscriptions"][0],
|
||||
*filler_rows[:104],
|
||||
target,
|
||||
*filler_rows[104:],
|
||||
]
|
||||
self._initial = deepcopy(self._state)
|
||||
self._ledger = []
|
||||
self._unknown_returned = False
|
||||
@@ -263,12 +284,13 @@ class EvaluationWorld:
|
||||
self._ledger.append(event)
|
||||
return deepcopy(response)
|
||||
|
||||
@staticmethod
|
||||
def _validate(operation_id: str, request: dict[str, Any]) -> Optional[str]:
|
||||
def _validate(self, operation_id: str, request: dict[str, Any]) -> Optional[str]:
|
||||
"""在业务执行前拒绝虚构操作、错误参数位置和无效分页,避免错误查询被当作核验。"""
|
||||
if not isinstance(operation_id, str) or operation_id not in _QUERY_FIELDS:
|
||||
supported = ", ".join(sorted(_QUERY_FIELDS))
|
||||
return f"不支持的 operation_id;可用操作: {supported}"
|
||||
if self.scenario.scenario_id == "long_context" and operation_id != "subscription.list":
|
||||
return "长上下文场景只接受 subscription.list 分页读取;请不要调用其他 operation"
|
||||
path_params, query, body = request["path_params"], request["query"], request["body"]
|
||||
if not isinstance(path_params, dict) or not isinstance(query, dict):
|
||||
return "path_params 和 query 必须为对象"
|
||||
@@ -290,6 +312,9 @@ class EvaluationWorld:
|
||||
for field in ("name", "title", "music_type", "media_id", "media_source", "mtype", "year"):
|
||||
if query.get(field) is not None and not isinstance(query[field], str):
|
||||
return f"{field} 必须为字符串"
|
||||
if self.scenario.scenario_id == "long_context":
|
||||
if query.get("count") != 20 or type(query.get("page")) is not int or not 1 <= query["page"] <= 6:
|
||||
return "长上下文场景必须使用 page=1..6 且 count=20 逐页读取"
|
||||
if query.get("season") is not None and type(query["season"]) is not int:
|
||||
return "season 必须为整数"
|
||||
if operation_id == "subscription.find" and not isinstance(query.get("media_source"), str):
|
||||
|
||||
Reference in New Issue
Block a user