assistant/backend/prospecting/preflight.py

181 lines
12 KiB
Python

"""Pure delivery projection of independently recorded read-only preflight."""
from __future__ import annotations
from datetime import datetime
import re
from backend.browser.preflight_policy import compile_preflight_policy
_CLASSIFICATIONS = ("unique_dom_deep_reply", "needs_native_identity", "ambiguous", "unknown")
_NAV_STATUSES = {"entered_note", "revisited_note", "excluded_note_blocked", "note_limit_blocked", "unknown_click_blocked",
"action_not_allowed", "unexpected_note_rejected", "scope_history_untracked"}
_NAV_ACTIONS = {"navigate", "click", "input", "send_keys", "scroll", "go_back", "wait", "done", "ask_user",
"inspect_reply_relations", "advance_comments", "find_elements", "find_text", "search_page",
"screenshot", "observe", "other"}
def _valid_navigation(row: object) -> bool:
if not isinstance(row, dict) or set(row) != {"id", "version", "run_id", "goal_version", "status", "action", "note_id", "observed_at"}:
return False
if (type(row["version"]) is not int or row["version"] != 1
or not isinstance(row["id"], str) or not re.fullmatch(r"preflight_nav_[a-f0-9]{32}", row["id"])
or not isinstance(row["run_id"], str) or not re.fullmatch(r"[a-f0-9]{32}", row["run_id"])
or type(row["goal_version"]) is not int or not 1 <= row["goal_version"] <= 1_000_000_000
or not isinstance(row["status"], str) or row["status"] not in _NAV_STATUSES
or not isinstance(row["action"], str) or row["action"] not in _NAV_ACTIONS
or not isinstance(row["observed_at"], str) or len(row["observed_at"]) > 40):
return False
note_id = row["note_id"]
if note_id is not None and (not isinstance(note_id, str) or not re.fullmatch(r"[a-f0-9]{24}", note_id)):
return False
if row["status"] in {"entered_note", "revisited_note", "unexpected_note_rejected"} and note_id is None:
return False
try:
datetime.fromisoformat(row["observed_at"])
except ValueError:
return False
return True
def _run_context(task: dict) -> tuple[int, str | None]:
inputs, artifacts = task.get("inputs") or {}, task.get("artifacts") or {}
version = inputs.get("_goal_version", 1)
if type(version) is not int or version < 1:
return 0, None
marker = artifacts.get("preflight_run")
if (not isinstance(marker, dict) or set(marker) != {"run_id", "goal_version"}
or marker.get("goal_version") != version or type(marker.get("goal_version")) is not int
or not isinstance(marker.get("run_id"), str) or not re.fullmatch(r"[a-f0-9]{32}", marker["run_id"])):
return version, None
return version, marker["run_id"]
def summarize_preflight(task: dict) -> dict | None:
"""Count only the current goal/run, without changing or upgrading history."""
from backend.browser.tools import _valid_preflight_telemetry
inputs, artifacts = task.get("inputs") or {}, task.get("artifacts") or {}
policy = compile_preflight_policy(inputs.get("goal", ""), inputs.get("max_notes"))
if not policy.active:
return None
goal_version, run_id = _run_context(task)
def current(row):
return isinstance(row, dict) and run_id is not None and row.get("run_id") == run_id and row.get("goal_version") == goal_version
raw_observations = artifacts.get("preflight_observations")
raw_navigation = artifacts.get("preflight_navigation")
observations = raw_observations if isinstance(raw_observations, list) else []
navigation = raw_navigation if isinstance(raw_navigation, list) else []
valid_observations = [row for row in observations if current(row) and _valid_preflight_telemetry(row)]
valid_navigation = [row for row in navigation if current(row) and _valid_navigation(row)]
prior_navigation = [row for row in navigation if _valid_navigation(row)]
malformed_current = (any(current(row) and not _valid_preflight_telemetry(row) for row in observations)
or any(current(row) and not _valid_navigation(row) for row in navigation))
# Wrapper receipts are append-only. A later unknown observation supersedes
# an earlier visible state; repeated checks never multiply sample counts.
latest = {}
for row in valid_observations:
latest[row["note_id"]] = row
visited = list(dict.fromkeys(row["note_id"] for row in valid_navigation if row["status"] in {"entered_note", "revisited_note"}))
all_observed_notes = set(visited) | set(latest)
order = [*visited, *(note_id for note_id in latest if note_id not in visited)]
counts = dict.fromkeys(_CLASSIFICATIONS, 0)
inspected, not_in_view, unknown, folded, folded_unknown = 0, 0, 0, 0, 0
checked = eligible = 0
notes = []
for ordinal, note_id in enumerate(order, 1):
row = latest.get(note_id)
confirmed = bool(row and row["status"] in {"checked", "no_visible_reply"} and row["discussion_state"] == "visible_comments")
if confirmed:
inspected += 1
checked += row["checked_count"]
eligible += row["eligible_count"]
for key in counts:
counts[key] += row["classification_counts"][key]
elif row and row["discussion_state"] == "comments_not_in_view":
not_in_view += 1
else:
unknown += 1
if row and row["has_unexpanded_replies"] is True:
folded += 1
elif not row or row["has_unexpanded_replies"] is None:
folded_unknown += 1
notes.append({"ordinal": ordinal, "inspected": confirmed,
"status": "inspected" if confirmed else "not_in_view" if row and row["discussion_state"] == "comments_not_in_view" else "unknown",
"checked_count": row["checked_count"] if confirmed else 0,
"eligible_count": row["eligible_count"] if confirmed else 0,
"has_unexpanded_replies": row["has_unexpanded_replies"] if row else None})
actual_violation = (any(row["status"] == "unexpected_note_rejected" for row in prior_navigation)
or bool(all_observed_notes & set(policy.excluded_note_ids))
or policy.max_notes is not None and len(all_observed_notes) > policy.max_notes)
truncated = any(row["history_truncated"] for row in valid_observations)
unknown_scope = (not valid_navigation or not visited or malformed_current or truncated
or bool(set(latest) - set(visited))
or any(row["status"] == "scope_history_untracked" for row in prior_navigation))
visited_known = bool(run_id and valid_navigation and visited and not malformed_current
and not any(row["status"] == "scope_history_untracked" for row in valid_navigation))
unknown_clicks = sum(row["status"] == "unknown_click_blocked" for row in valid_navigation)
scope_passed = False if actual_violation else None if unknown_scope else True
history = artifacts.get("browser_history") or {}
browser_success = artifacts.get("browser_verdict") == "success" and history.get("is_successful") is not False
completed = bool(browser_success and inspected > 0 and not not_in_view and not unknown and scope_passed is True)
if task.get("status") == "running":
headline = "正在预检可见回复关系"
elif completed:
headline = "预检已完成,发现可继续取证的样本" if eligible else "预检已完成,可见范围未发现合格样本"
elif inspected:
headline = "预检阶段结果已保留"
else:
headline = "尚未形成可核对的预检结果"
detail = (f"本段已记录实际访问{len(visited)}篇。" if visited_known else "本段实际访问数量未记录,访问范围无法完整核对。")
detail += (f"可确认预检{inspected}篇;检查当前可见回复{checked}条,其中{eligible}条具备继续取证的依据。"
if valid_observations else "本段尚无独立、可确认的预检记录。")
if not_in_view:
detail += f"{not_in_view}篇讨论尚未进入视口,不计入已预检。"
if unknown:
detail += f"{unknown}篇尚无可确认的当前预检结果。"
if folded:
detail += f"{folded}篇仍有折叠回复未展开。"
if folded_unknown:
detail += f"{folded_unknown}篇的折叠状态尚未确认。"
if unknown_clicks:
detail += f"已拒绝{unknown_clicks}个目标无法事前核对的入口,相关范围仍保留限制。"
if scope_passed is False:
detail += "已观察到超出本次访问范围的页面,本轮不能认定完成。"
elif scope_passed is None:
detail += "访问范围记录仍不完整,不能认定已遵守全部范围。"
detail += "零合格样本只对应本次可见范围,未展开或未加载部分仍未知;本轮未生成候选名单。"
return {"version": 1, "completed": completed, "inspected_notes": inspected, "visited_notes": len(visited),
"run_available": run_id is not None, "recorded": bool(valid_observations), "visited_known": visited_known,
"checked_count": checked, "eligible_count": eligible, "classification_counts": counts,
"not_in_view_notes": not_in_view, "unknown_notes": unknown, "folded_notes": folded,
"folded_unknown_notes": folded_unknown, "scope_passed": scope_passed,
"history_truncated": truncated, "max_notes": policy.max_notes,
"unknown_click_blocked_count": unknown_clicks,
"headline": headline, "detail": detail, "notes": notes}
def delivery_projection(task: dict) -> dict | None:
summary = summarize_preflight(task)
if summary is None:
return None
artifacts = task.get("artifacts") or {}
history = artifacts.get("browser_history") or {}
running = task.get("status") == "running"
status = "in_progress" if running else "fulfilled" if summary["completed"] else "partial" if summary["inspected_notes"] else "no_evidence"
return {"version": 1, "status": status, "headline": summary["headline"], "preflight": summary,
"runtime": {"status": task.get("status") or "unknown", "step": task.get("current_step"),
"stop_reason": artifacts.get("stop_reason"), "stop_detail": artifacts.get("stop_detail") or task.get("pause_reason") or "",
"raw_browser_success": history.get("is_successful"), "browser_verdict": artifacts.get("browser_verdict")},
"fulfillment": {"scope_met": summary["scope_passed"] is True, "requirements_met": summary["inspected_notes"] > 0 and not summary["not_in_view_notes"] and not summary["unknown_notes"],
"complete": summary["completed"], "assessment_current": True, "target": None, "current": 0, "remaining": None},
"artifacts": {"list_available": False, "file_required": False, "file_available": False,
"delivered": summary["inspected_notes"] > 0, "export_count": 0},
"counts": dict.fromkeys(("eligible", "known_identity", "supplemental", "unresolved", "signals", "verified_accounts"), 0),
"conditions": [
{"key": "preflight_visible_discussion", "label": "实际预检当前可见讨论", "required": True,
"met": summary["inspected_notes"] > 0 and not summary["not_in_view_notes"] and not summary["unknown_notes"], "current": summary["inspected_notes"], "target": None},
{"key": "preflight_visit_scope", "label": "实际访问符合本次范围", "required": True,
"met": summary["scope_passed"] is True, "current": summary["visited_notes"], "target": None}],
"people": []}