162 lines
11 KiB
Python
162 lines
11 KiB
Python
"""Program-owned operation facts and knowledge receipts; never trust model success."""
|
|
from __future__ import annotations
|
|
|
|
import hashlib
|
|
import json
|
|
from copy import deepcopy
|
|
|
|
PAGE_VARIANT = "xhs_visible_dom_v1"
|
|
|
|
|
|
def context_for(task: dict, *, stage: str | None = None) -> dict:
|
|
intent = (task.get("inputs") or {}).get("intent", "discover")
|
|
default_stage = "delivery" if intent == "export" else "outreach" if intent in {"follow", "message"} else "search_read"
|
|
return {"platform": "xiaohongshu", "stage": stage or default_stage,
|
|
"page_variant": PAGE_VARIANT, "authorized_capabilities": []}
|
|
|
|
|
|
def method_spec(capability: str, source_kind: str = "note") -> dict:
|
|
names = {"save_source": "保存独立来源", "expand_replies": "展开当前讨论回复", "inspect_profile": "核对来源主页号", "export": "生成当前名单文件"}
|
|
stage = "delivery" if capability == "export" else "search_read"
|
|
expected = {"save_source": "独立容器原话与层级核对通过并持久保存", "expand_replies": "当前父讨论中可见回复增加且归属可核对",
|
|
"inspect_profile": "来源作者入口匹配当前主页并取得带标签的可见小红书号", "export": "当前结果文件存在且清单与结果版本一致"}
|
|
return {"key": f"xhs_operation_{capability}_{source_kind}_v1", "version": 1,
|
|
"title": names[capability] + (f" · {source_kind}" if capability == "save_source" else ""),
|
|
"platform": "xiaohongshu", "stage": stage, "capability": capability, "source_kind": source_kind,
|
|
"page_variant": PAGE_VARIANT, "kind": "operation", "origin": "program_template",
|
|
"preconditions": ["当前页面实际可见且入口唯一", "按本次目标和授权执行"],
|
|
"parameters": ["current_observed_target"], "steps": ["检查当前实际入口", "执行限定操作", "核对变化并持久保存依据"],
|
|
"postcondition": expected[capability], "recovery": "无新增、定位失效或结果未知时停止重复操作,保留实际进度并重新观察。", "dependencies": []}
|
|
|
|
|
|
def operation_fact(capability: str, source_kind: str, object_key: str, pre: str, post: str,
|
|
*, outcome: str = "success", verifier: str, at: str, **details) -> dict:
|
|
# Only the program calls this after its tool checks. The model cannot supply
|
|
# these fields through save_learning/report_learning_outcome.
|
|
identity = hashlib.sha256(json.dumps([capability, source_kind, object_key, pre, post, outcome], ensure_ascii=False).encode()).hexdigest()
|
|
return {"id": identity, "capability": capability, "source_kind": source_kind,
|
|
"object_key": hashlib.sha256(object_key.encode()).hexdigest(), "outcome": outcome,
|
|
"evidence_refs": {"pre": pre, "post": post}, "verifier": verifier,
|
|
"observed_at": at, "page_variant": PAGE_VARIANT, "details": deepcopy(details)}
|
|
|
|
|
|
def _fact_trace(fact: dict) -> dict:
|
|
"""Use only the trace captured at execution; missing history stays missing."""
|
|
trace = (fact.get("details") or {}).get("trace")
|
|
if not isinstance(trace, dict):
|
|
return {}
|
|
result = {key: trace[key] for key in ("run_id", "action_id") if isinstance(trace.get(key), str) and trace[key]}
|
|
if type(trace.get("goal_version")) is int and trace["goal_version"] > 0:
|
|
result["goal_version"] = trace["goal_version"]
|
|
return result
|
|
|
|
|
|
def synchronize(task: dict, artifacts: dict) -> dict:
|
|
"""Register new executed facts only; old source snapshots are not backfilled."""
|
|
from backend.prospecting import knowledge
|
|
facts = artifacts.get("operation_evidence") or []
|
|
candidates = [row for row in artifacts.get("learning_candidates") or [] if isinstance(row, dict)
|
|
and row.get("evidence") and row.get("observed_at") and row.get("value")]
|
|
if not facts and not artifacts.get("knowledge_methods") and not candidates:
|
|
return artifacts
|
|
task_id = task["id"]
|
|
selected = {row["id"]: row for row in artifacts.get("knowledge_methods") or []}
|
|
registered = {}
|
|
events = []
|
|
fact_events = {}
|
|
valid_facts = []
|
|
for candidate in candidates:
|
|
digest = hashlib.sha256(json.dumps([candidate.get("key"), candidate["value"]], ensure_ascii=False).encode()).hexdigest()[:24]
|
|
spec = {"key": "suggested_" + digest, "version": 1, "title": str(candidate.get("key", "方法建议")),
|
|
"platform": "xiaohongshu", "stage": "search_read", "capability": "suggested_method", "source_kind": "any",
|
|
"page_variant": PAGE_VARIANT, "kind": "operation", "origin": "model",
|
|
"preconditions": ["当前实际页面必须重新观察"], "parameters": ["current_observed_target"],
|
|
"steps": [candidate["value"]], "postcondition": "需要匹配的程序验收器提供动作结果证明", "recovery": "没有独立动作验收时保留探索候选。", "dependencies": []}
|
|
try:
|
|
method = knowledge.register_method(spec, task_id)
|
|
registered[method["id"]] = method
|
|
except ValueError:
|
|
# Invalid task-specific text stays in the task candidate ledger,
|
|
# never in generic knowledge and never as a paid retry.
|
|
continue
|
|
for fact in facts:
|
|
if (not isinstance(fact, dict) or fact.get("capability") not in {"save_source", "expand_replies", "inspect_profile", "export"}
|
|
or not all(fact.get(key) for key in ("id", "object_key", "outcome", "verifier"))):
|
|
continue
|
|
spec = method_spec(fact["capability"], fact.get("source_kind", "note"))
|
|
method = knowledge.register_method(spec, task_id)
|
|
registered[method["id"]] = method
|
|
event = knowledge.record_event(method["id"], task_id, fact["object_key"], fact["outcome"],
|
|
evidence_refs=fact["evidence_refs"], context={**context_for(task, stage=spec["stage"]), "source_kind": spec["source_kind"]},
|
|
provenance="program", event_key="operation:" + fact["id"], verified=True,
|
|
verifier=fact["verifier"], **_fact_trace(fact))
|
|
events.append(event)
|
|
fact_events[fact["id"]] = event["id"]
|
|
if event.get("outcome") == "success":
|
|
valid_facts.append(fact)
|
|
# Merely injecting another method with the same capability is not
|
|
# proof it was adopted. It receives no automatic success credit.
|
|
# Agent reports remain reported/unknown, with no positive reliability count.
|
|
for report in artifacts.get("learning_outcomes") or []:
|
|
mid = str(report.get("entry_id", report.get("id", "")))
|
|
if mid not in selected:
|
|
continue
|
|
rid = hashlib.sha256(json.dumps(report, sort_keys=True, ensure_ascii=False).encode()).hexdigest()
|
|
events.append(knowledge.record_event(mid, task_id, "reported:" + task_id,
|
|
"unused" if report.get("outcome") == "unused" else "unknown", provenance="model",
|
|
event_key="report:" + rid, reason="模型回执;未作为程序动作验收", evidence_refs={}))
|
|
# Stage success is scoped to this actual bounded branch, not a claim that
|
|
# the full search/profile/outreach chain or all comments were completed.
|
|
by_object = {}
|
|
for fact in valid_facts:
|
|
if fact.get("capability") == "save_source" and fact.get("outcome") == "success":
|
|
by_object.setdefault(fact["object_key"], {})[fact["source_kind"]] = fact
|
|
for object_key, kinds in by_object.items():
|
|
discussion = kinds.get("reply") or kinds.get("comment")
|
|
if "note" not in kinds or discussion is None:
|
|
continue
|
|
note = kinds["note"]
|
|
deps = [knowledge.register_method(method_spec("save_source", kind), task_id)["id"] for kind in ("note", discussion["source_kind"])]
|
|
spec = {"key": "xhs_stage_save_discussion_" + discussion["source_kind"] + "_v1", "version": 1,
|
|
"title": "保存一篇正文与独立讨论", "platform": "xiaohongshu", "stage": "search_read", "capability": "read_discussion",
|
|
"source_kind": discussion["source_kind"], "page_variant": PAGE_VARIANT, "kind": "pipeline", "origin": "program_template",
|
|
"preconditions": ["同一笔记已有实际可见正文和讨论"], "parameters": ["current_note"],
|
|
"steps": ["保存正文", "保存独立发言人的讨论"], "postcondition": "同笔记正文与一条独立讨论均保存通过",
|
|
"recovery": "保留已保存部分,未展开与未读取范围继续未知。", "dependencies": deps,
|
|
"branches": [discussion["source_kind"]], "terminal": "saved_note_and_attributed_discussion"}
|
|
method = knowledge.register_method(spec, task_id)
|
|
registered[method["id"]] = method
|
|
events.append(knowledge.record_event(method["id"], task_id, object_key, "success",
|
|
evidence_refs={"pre": note["evidence_refs"]["pre"], "post": discussion["evidence_refs"]["post"]},
|
|
context={**context_for(task, stage=spec["stage"]), "source_kind": discussion["source_kind"]}, provenance="program",
|
|
event_key="stage:" + note["id"] + ":" + discussion["id"], verified=True,
|
|
verifier="same_note_saved_sources", terminal_complete=True, executed_branch=discussion["source_kind"],
|
|
dependency_event_ids=[fact_events[note["id"]], fact_events[discussion["id"]]], **_fact_trace(discussion)))
|
|
acted = {row["method_id"] for row in events if row.get("provenance") == "program" and row.get("outcome") != "unused"}
|
|
reported = {str(row.get("entry_id", row.get("id", ""))) for row in artifacts.get("learning_outcomes") or []}
|
|
for mid in selected:
|
|
if mid not in reported and mid not in acted:
|
|
events.append(knowledge.record_event(mid, task_id, "task:" + task_id, "unused", provenance="program",
|
|
event_key="unused:" + task_id + ":" + mid, reason_code="not_observed_in_operation_evidence"))
|
|
return {**artifacts, "knowledge_summary": knowledge.task_summary(task_id),
|
|
"knowledge_method_ids": list(registered), "knowledge_event_ids": [row["id"] for row in events]}
|
|
|
|
|
|
def export_fact(task: dict, entry: dict) -> dict:
|
|
from backend.prospecting.workflows import export_path, result_version
|
|
path, metadata = export_path(task, entry["id"])
|
|
if not path.is_file() or metadata["result_version"] != result_version(task):
|
|
raise ValueError("文件与当前结果版本不一致,不能记录交付成功")
|
|
fact = operation_fact("export", "table", metadata["result_version"],
|
|
task["id"] + ":result:" + metadata["result_version"], task["id"] + ":export:" + entry["id"],
|
|
verifier="file_manifest_and_current_result", at=entry["generated_at"],
|
|
trace={"goal_version": metadata["goal_version"], "action_id": entry["id"]},
|
|
pre_snapshot={"result_version": metadata["result_version"], "goal_version": metadata["goal_version"]},
|
|
post_snapshot={"export_id": entry["id"], "exists": True, "current_version": True, "row_count": metadata["count"], "format": "csv"})
|
|
artifacts = dict(task.get("artifacts") or {})
|
|
facts = list(artifacts.get("operation_evidence") or [])
|
|
if not any(row.get("id") == fact["id"] for row in facts):
|
|
facts.append(fact)
|
|
artifacts["operation_evidence"] = facts
|
|
return artifacts
|