60 lines
3.3 KiB
Python
60 lines
3.3 KiB
Python
"""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}
|