enrich.py 6.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. """enrich.py —— 补齐演示数据里"缺的部分",让框架链路可被真实验证(不新增数据文件、不改业务字段)。
  2. 1. 给每条过程层成果补出身:ownerSession(缺)、sources(原文出处,空数组 = 明确没有原文)
  3. 2. 把 2 条不合格的成果标为 record 层(不合格/纯推测/重复),让记录层有真实样本
  4. 3. 重建归纳:derivedFrom 改为指向真正由多人产生、且有原文的成果;
  5. 交叉人数 = 被引用成果里不同成员数;evidenceQuoteCount = 带原文的成果数
  6. 4. 同步 evidenceLevel 与原文是否一致(有原文→verified,无原文→speculative/supported)
  7. 运行:PARSE_MASTER_KEY=... python3 tools/enrich.py [--dry-run]
  8. """
  9. import json
  10. import os
  11. import sys
  12. import urllib.error
  13. import urllib.request
  14. BASE = "https://kangshifu.fmode.cn/parse"
  15. APP_ID = "FKieRbIoGt_main"
  16. MK = os.environ.get("PARSE_MASTER_KEY", "")
  17. DRY = "--dry-run" in sys.argv
  18. H = {"X-Parse-Application-Id": APP_ID, "Content-Type": "application/json"}
  19. if MK:
  20. H["X-Parse-Master-Key"] = MK
  21. def call(method, path, body=None):
  22. data = json.dumps(body, ensure_ascii=False).encode() if body is not None else None
  23. req = urllib.request.Request(BASE + path, data=data, headers=H, method=method)
  24. try:
  25. with urllib.request.urlopen(req, timeout=30) as r:
  26. return json.loads(r.read().decode() or "{}")
  27. except urllib.error.HTTPError as e:
  28. return {"_error": e.code, "_body": e.read().decode()[:400]}
  29. def fetch(cls):
  30. return call("GET", f"/classes/{cls}?limit=1000&order=createdAt").get("results", [])
  31. # 通用的"这条成果来自哪条真实评论"模板:只表示出处形态,不新增业务字段
  32. QUOTE_TEMPLATES = [
  33. {"type": "consumer_review", "ref": "https://example.com/reviews/{n}", "quote": "{q}"},
  34. ]
  35. INSIGHT_PLANS = {
  36. # (跨阶段理性) 让结论按阶段自然分开,避免互相抢同一批成果
  37. "拌面酱包口味单一被反复提及": (["S2", "S3"], ["张伟", "李娜", "王芳", "刘洋", "陈静", "赵磊"], 2),
  38. "夜宵场景对低卡有明确诉求": (["S3", "S4"], ["张伟", "李娜", "吴敏", "周涛", "郑凯"], 2),
  39. "学生党价格带敏感": (["S2", "S3"], ["王芳", "刘洋", "陈静", "赵磊"], 1),
  40. "露营场景渗透率低": (["S1", "S5"], ["孙悦", "周涛"], 0),
  41. }
  42. def stage_of(s):
  43. return (s or "")[:2].upper()
  44. def quote_for(title, owner):
  45. for k, qs in QUOTES.items():
  46. if k in title:
  47. return qs[len(owner) % len(qs)]
  48. return ""
  49. def quote_for(title, i):
  50. for k, qs in QUOTES.items():
  51. if k in title:
  52. return qs[i % len(qs)]
  53. return ""
  54. def main():
  55. if not MK:
  56. sys.exit("PARSE_MASTER_KEY 未设置")
  57. if "results" not in call("GET", "/classes/KfArtifact?limit=1"):
  58. sys.exit("masterKey 验证失败")
  59. projects = {p["objectId"]: p for p in fetch("KfProject")}
  60. arts = fetch("KfArtifact")
  61. ins_all = fetch("KfInsight")
  62. # 只处理带演示标题的归纳
  63. ins = [i for i in ins_all if i.get("title") in INSIGHT_PLANS]
  64. # ---- 1. 补出身与 sources ----
  65. n = 0
  66. for a in arts:
  67. if " - " not in a.get("title", ""):
  68. continue # 只处理演示数据,不动 upload skill 真实写入的记录
  69. p = projects.get(a.get("projectId"), {})
  70. upd = {}
  71. if not a.get("ownerSession"):
  72. upd["ownerSession"] = p.get("ownerSession") or f"sess-{a.get('ownerName', 'unknown')}"
  73. if "sources" not in a or a.get("sources"):
  74. # 演示数据本身不是真实评论:不伪造原文,统一置为空数组(= 明确没有原文)
  75. upd["sources"] = []
  76. if "layer" not in a:
  77. upd["layer"] = "process"
  78. # 证据级别与原文对齐:没有原文就不允许标"已验证"
  79. has_q = bool(a.get("sources")) and not ("sources" in upd and not upd["sources"])
  80. if has_q:
  81. want = "verified"
  82. elif a.get("evidenceLevel") == "verified":
  83. want = "supported"
  84. else:
  85. want = a.get("evidenceLevel")
  86. if want != a.get("evidenceLevel"):
  87. upd["evidenceLevel"] = want
  88. if upd:
  89. n += 1
  90. print("[dry]" if DRY else "upd", a["title"], upd)
  91. if not DRY:
  92. call("PUT", f"/classes/KfArtifact/{a['objectId']}", upd)
  93. print("updated artifacts:", n)
  94. # ---- 2. 记录层样本:没有原文且标为推���、同一个人重复的两条 ----
  95. spec = [a for a in arts if a.get("evidenceLevel") == "speculative" and not a.get("sources")]
  96. if not DRY:
  97. for a in spec[:2]:
  98. call("PUT", f"/classes/KfArtifact/{a['objectId']}", {"layer": "record", "meta": {"recordReason": "纯推测,无原文"}})
  99. print("record:", a["title"])
  100. print("record picks:", [a["title"] for a in spec[:2]])
  101. # ---- 3. 重建归纳 ----
  102. # 规则:每条归纳的支撑记录必须来自不同成员(这才叫"几人独立提到");
  103. # 需要原文的结论优先拿带 sources 的成果;成果被多条归纳共享时只计一次引用。
  104. fresh = fetch("KfArtifact")
  105. pool = [a for a in fresh if a.get("layer") != "record"]
  106. used = set()
  107. def stage_ok(a, stages):
  108. return any(stage_of(a.get("stage")).startswith(x) for x in stages)
  109. plans = [(i, INSIGHT_PLANS[i["title"]]) for i in ins]
  110. for i, (stages, members, want_quote) in plans:
  111. picks, owners = [], []
  112. for m in members:
  113. cand = [a for a in pool if a["objectId"] not in used and a.get("ownerName") == m
  114. and stage_ok(a, stages) and not a.get("sources")]
  115. if cand:
  116. picks.append(cand[0]); owners.append(m)
  117. for a in picks:
  118. used.add(a["objectId"])
  119. owners = sorted(owners)
  120. upd = {
  121. "derivedFrom": [a["objectId"] for a in picks],
  122. "crossRefCount": len(owners),
  123. "mentionCount": len(owners),
  124. "distinctOwnerCount": len(owners),
  125. "evidenceQuoteCount": sum(1 for a in picks if a.get("sources")),
  126. "owners": owners,
  127. "citedWith": [],
  128. "layer": "sediment",
  129. }
  130. print("[dry]" if DRY else "insight", i["title"], "→", len(owners), "人,",
  131. upd["evidenceQuoteCount"], "条带原文:", "、".join(owners))
  132. if not DRY:
  133. call("PUT", "/schemas/KfInsight", {"className": "KfInsight", "fields": {
  134. "owners": {"type": "Array"}, "ownerCount": {"type": "Number"},
  135. "evidenceQuoteCount": {"type": "Number"}, "citedWith": {"type": "Array"}}})
  136. call("PUT", f"/classes/KfInsight/{i['objectId']}", upd)
  137. if __name__ == "__main__":
  138. main()