"""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