"""Incremental evidence analysis and input-bound reuse; no page operations.""" from __future__ import annotations import hashlib import json from typing import Any, Callable from backend.browser.llm_factory import get_llm_config from backend.prospecting.evidence_context import related_context def fingerprint(value: Any) -> str: return hashlib.sha256(json.dumps(value, ensure_ascii=False, sort_keys=True, default=str).encode()).hexdigest() def analysis_inputs(task: dict, brief) -> tuple[list[dict], dict[str, str]]: artifacts = task.get("artifacts") or {} corrections = {row.get("source_id"): row.get("text") for row in artifacts.get("corrections") or []} sources = [{**row, **({"user_correction": corrections[row["source_id"]]} if corrections.get(row.get("source_id")) else {})} for row in artifacts.get("sources") or []] criteria = brief.model_dump(exclude={"target_candidates", "discovery_scope", "max_notes", "max_comments_per_note", "use_web_search"}) cfg = get_llm_config() signatures = {} for source in sources: context = related_context([source], sources) signatures[source["source_id"]] = fingerprint([1, cfg["provider"], cfg["model"], criteria, source, context]) return sources, signatures def analysis_is_current(task: dict, brief) -> bool: sources, signatures = analysis_inputs(task, brief) artifacts = task.get("artifacts") or {} saved = (artifacts.get("analysis_cache") or {}).get("source_signatures") or {} decisions = {row.get("source_id") for row in artifacts.get("decisions") or []} return bool(sources) and signatures == saved and decisions == set(signatures) async def analyze_evidence(task: dict, brief, analyzer: Callable, *, force: bool = False) -> dict: sources, signatures = analysis_inputs(task, brief) if not sources: raise ValueError("当前没有已保存资料可供分析") artifacts = task.get("artifacts") or {} saved = (artifacts.get("analysis_cache") or {}).get("source_signatures") or {} old = {row["source_id"]: row for row in artifacts.get("decisions") or [] if row.get("source_id")} pending = [source for source in sources if force or saved.get(source["source_id"]) != signatures[source["source_id"]]] pending_ids = {source["source_id"] for source in pending} pending.extend(source for source in sources if source["source_id"] not in old and source["source_id"] not in pending_ids) result = await analyzer(brief, pending, context_sources=sources) if pending else {} merged = {**old, **{row["source_id"]: row for row in result.get("decisions") or []}} if set(signatures) - set(merged): raise ValueError("已保存资料尚未全部形成有效判断,原结果保留") decisions = [merged[source["source_id"]] for source in sources] leads = [{**decision, "id": decision["source_id"], "status": "pending"} for decision in decisions if decision.get("decision") == "keep"] return {**result, "decisions": decisions, "leads": leads, "analysis_goal_version": task.get("inputs", {}).get("_goal_version", 1), "analysis_cache": {"version": 1, "source_signatures": signatures}, "analysis_reused_count": len(sources) - len(pending), "analysis_new_count": len(pending), "analysis_cached": not pending}