event_store.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332
  1. #!/usr/bin/env python3
  2. """
  3. event_store.py — 事件溯源引擎(Event Sourcing)
  4. 解决多用户 git 共享进化数据的冲突问题:
  5. - 每个用户写入不可变事件日志文件(追加写入,天然无冲突)
  6. - population.json / thresholds.json 从事件日志编译生成(可复现)
  7. - git pull → 获得他人事件 → recompile → 最新状态
  8. 事件文件格式:
  9. evolution/genres/{genre_id}/events/{timestamp}_{user}.json
  10. 内容: [event, event, ...]
  11. 事件类型:
  12. - feedback: 章节反馈(多维评分写入)
  13. - mutate: 变异产生新变体 / 移除旧变体
  14. - extinction: 灭绝重播
  15. - threshold_adjust: 阈值调优
  16. """
  17. import json, os, re, hashlib, subprocess
  18. from pathlib import Path
  19. from datetime import datetime
  20. from typing import Any
  21. import sys
  22. sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
  23. from utils import (
  24. get_evolution_dir, get_genre_evolution_dir,
  25. load_genre_evolution, save_genre_evolution,
  26. load_evolution, save_evolution,
  27. )
  28. def _get_user() -> str:
  29. """获取当前用户标识(git config → 系统用户名 → hostname)"""
  30. try:
  31. result = subprocess.run(
  32. ["git", "config", "user.name"], capture_output=True, text=True
  33. )
  34. if result.returncode == 0 and result.stdout.strip():
  35. return result.stdout.strip()
  36. except Exception:
  37. pass
  38. try:
  39. result = subprocess.run(
  40. ["git", "config", "user.email"], capture_output=True, text=True
  41. )
  42. if result.returncode == 0 and result.stdout.strip():
  43. return result.stdout.strip().split("@")[0]
  44. except Exception:
  45. pass
  46. return os.environ.get("USER", os.environ.get("USERNAME", "unknown"))
  47. def _events_dir(genre_id: str) -> Path:
  48. d = get_genre_evolution_dir(genre_id) / "events"
  49. d.mkdir(parents=True, exist_ok=True)
  50. return d
  51. def _event_filename(user: str | None = None) -> str:
  52. """生成事件文件名: {timestamp}_{user}.json"""
  53. ts = datetime.now().strftime("%Y%m%d_%H%M%S")
  54. u = user or _get_user()
  55. return f"{ts}_{u}.json"
  56. def write_events(genre_id: str, events: list[dict],
  57. user: str | None = None) -> Path:
  58. """追加写入事件文件(一次会话一个文件,追加写入天然无冲突)。
  59. 返回写入的文件路径。
  60. """
  61. if not events:
  62. return None
  63. u = user or _get_user()
  64. fname = _event_filename(u)
  65. fpath = _events_dir(genre_id) / fname
  66. with open(fpath, "w", encoding="utf-8") as f:
  67. json.dump({
  68. "user": u,
  69. "timestamp": datetime.now().isoformat(),
  70. "events": events,
  71. }, f, ensure_ascii=False, indent=2)
  72. return fpath
  73. def read_all_events(genre_id: str) -> list[dict]:
  74. """按时间顺序读取所有事件文件,返回展开后的事件列表。"""
  75. d = _events_dir(genre_id)
  76. if not d.exists():
  77. return []
  78. all_events = []
  79. for fpath in sorted(d.glob("*.json")):
  80. try:
  81. data = json.loads(fpath.read_text(encoding="utf-8"))
  82. for evt in data.get("events", []):
  83. evt["_source_user"] = data.get("user", "unknown")
  84. evt["_source_file"] = fpath.name
  85. all_events.append(evt)
  86. except (json.JSONDecodeError, KeyError):
  87. continue
  88. return all_events
  89. def compile_population(genre_id: str) -> dict:
  90. """从事件日志编译 population.json。
  91. 回放所有事件,重建 5 岛种群状态。
  92. """
  93. from prompt_evolver import PromptVariant, PromptPopulation, IslandModel
  94. events = read_all_events(genre_id)
  95. # 从种子初始化
  96. from seed_extractor import SeedExtractor
  97. extractor = SeedExtractor()
  98. seeds = extractor.extract_seeds(genre_id=genre_id)
  99. model = IslandModel()
  100. model.initialize(seeds)
  101. for cat, pop in model.islands.items():
  102. pop.generation = 0
  103. gen_counters: dict[str, int] = {}
  104. for evt in events:
  105. etype = evt.get("type", "")
  106. if etype == "feedback":
  107. island = evt.get("island", "writing")
  108. variant_id = evt.get("variant_id", "")
  109. scores = evt.get("scores", {})
  110. variant_template = evt.get("variant_template", "")
  111. if island in model.islands:
  112. pop = model.islands[island]
  113. target = None
  114. for v in pop.variants:
  115. if v.id == variant_id:
  116. target = v
  117. break
  118. # 变体不存在 → 从事件携带的模板文本重建
  119. if target is None and variant_template:
  120. from prompt_evolver import PromptVariant
  121. target = PromptVariant(variant_template, source="event:feedback")
  122. target.id = variant_id
  123. pop.add(target)
  124. if target:
  125. for dim, score in scores.items():
  126. dim_key = dim.upper()
  127. count = target.trials_per_dim.get(dim_key, 0)
  128. if count > 0:
  129. target.scores[dim_key] = (
  130. target.scores.get(dim_key, 0) * count + score
  131. ) / (count + 1)
  132. else:
  133. target.scores[dim_key] = float(score)
  134. target.trials_per_dim[dim_key] = count + 1
  135. target.trials += 1
  136. elif etype == "mutate":
  137. island = evt.get("island", "")
  138. new_variants = evt.get("new_variants", [])
  139. removed_ids = set(evt.get("removed_variants", []))
  140. if island in model.islands:
  141. pop = model.islands[island]
  142. # Remove old variants
  143. pop.variants = [v for v in pop.variants if v.id not in removed_ids]
  144. # Add new ones
  145. for vd in new_variants:
  146. v = PromptVariant.from_dict(vd)
  147. pop.add(v)
  148. gen_counters[island] = gen_counters.get(island, 0) + 1
  149. pop.generation = gen_counters[island]
  150. elif etype == "extinction":
  151. island = evt.get("island", "")
  152. removed_ids = set(evt.get("removed_variants", []))
  153. added = evt.get("added_variants", [])
  154. if island in model.islands:
  155. pop = model.islands[island]
  156. pop.variants = [v for v in pop.variants if v.id not in removed_ids]
  157. for vd in added:
  158. v = PromptVariant.from_dict(vd)
  159. pop.add(v)
  160. elif etype == "threshold_adjust":
  161. # Handled separately — see compile_thresholds
  162. pass
  163. return {
  164. cat: {
  165. "generation": pop.generation,
  166. "variants": [v.to_dict() for v in pop.variants],
  167. }
  168. for cat, pop in model.islands.items()
  169. }
  170. def compile_thresholds(genre_id: str) -> dict:
  171. """从事件日志编译当前阈值(应用最后一次 threshold_adjust 事件)。"""
  172. events = read_all_events(genre_id)
  173. # 从默认阈值开始
  174. default = load_genre_evolution(genre_id, "thresholds.json")
  175. if not default:
  176. default = load_evolution("thresholds.json") or {
  177. "pass": 85, "revise_low": 60, "rewrite_low": 40,
  178. "max_revision_rounds": 3,
  179. "weights": {"P1": 0.35, "P2": 0.25, "P3": 0.15, "P4": 0.15, "P5": 0.10},
  180. }
  181. current = dict(default)
  182. for evt in events:
  183. if evt.get("type") == "threshold_adjust":
  184. changes = evt.get("changes", {})
  185. for k, v in changes.items():
  186. if k == "weights" and isinstance(v, dict):
  187. current.setdefault("weights", {}).update(v)
  188. else:
  189. current[k] = v
  190. return current
  191. def compile_all(genre_id: str) -> dict:
  192. """编译题材的全部进化数据并写入文件。
  193. 返回: {"population": ..., "thresholds": ..., "event_count": N}
  194. """
  195. pop = compile_population(genre_id)
  196. thresh = compile_thresholds(genre_id)
  197. save_genre_evolution(genre_id, "prompts/population.json", pop)
  198. save_genre_evolution(genre_id, "thresholds.json", thresh)
  199. # 同步 current_best
  200. best = {}
  201. for cat, pop_data in pop.items():
  202. variants = [v for v in pop_data.get("variants", []) if v.get("scores")]
  203. variants.sort(key=lambda v: sum(v["scores"].values()) / max(len(v["scores"]), 1), reverse=True)
  204. if len(variants) < 3:
  205. unscored = [v for v in pop_data.get("variants", []) if not v.get("scores")]
  206. for v in unscored:
  207. if v not in variants:
  208. variants.append(v)
  209. if len(variants) >= 3:
  210. break
  211. best[cat] = [v["template"] for v in variants[:3]]
  212. save_genre_evolution(genre_id, "prompts/current_best.json", best)
  213. events = read_all_events(genre_id)
  214. print(f" ✓ 编译完成: {len(events)} 条事件 → evolution/genres/{genre_id}/")
  215. print(f" 种群: {sum(len(p['variants']) for p in pop.values())} 变体")
  216. print(f" 阈值: pass={thresh.get('pass')}")
  217. return {
  218. "population": pop,
  219. "thresholds": thresh,
  220. "current_best": best,
  221. "event_count": len(events),
  222. }
  223. def list_event_summary(genre_id: str) -> list[dict]:
  224. """列出事件摘要(不展开所有事件细节)。"""
  225. d = _events_dir(genre_id)
  226. if not d.exists():
  227. return []
  228. summaries = []
  229. for fpath in sorted(d.glob("*.json")):
  230. try:
  231. data = json.loads(fpath.read_text(encoding="utf-8"))
  232. summaries.append({
  233. "file": fpath.name,
  234. "user": data.get("user", "?"),
  235. "timestamp": data.get("timestamp", ""),
  236. "event_types": list(set(
  237. e.get("type", "?") for e in data.get("events", [])
  238. )),
  239. "event_count": len(data.get("events", [])),
  240. })
  241. except Exception:
  242. summaries.append({"file": fpath.name, "error": "parse failed"})
  243. return summaries
  244. # ── CLI ─────────────────────────────────────────────────
  245. if __name__ == "__main__":
  246. import argparse
  247. parser = argparse.ArgumentParser(description="事件溯源引擎")
  248. parser.add_argument("action", choices=["compile", "list", "write"],
  249. help="compile=编译全部 | list=列出事件 | write=写入事件")
  250. parser.add_argument("--genre", "-g", type=str, required=True)
  251. parser.add_argument("--events", "-e", type=str, default=None,
  252. help="JSON 事件列表(write 时使用)")
  253. args = parser.parse_args()
  254. if args.action == "compile":
  255. compile_all(args.genre)
  256. elif args.action == "list":
  257. summaries = list_event_summary(args.genre)
  258. if not summaries:
  259. print(" 暂无事件")
  260. else:
  261. for s in summaries:
  262. print(f" {s['file']} | {s['user']} | {s['event_count']} events: {s['event_types']}")
  263. elif args.action == "write":
  264. if not args.events:
  265. print("错误: 需要 --events JSON")
  266. sys.exit(1)
  267. try:
  268. events = json.loads(args.events)
  269. except json.JSONDecodeError:
  270. print("错误: --events 需要 JSON 数组")
  271. sys.exit(1)
  272. fpath = write_events(args.genre, events)
  273. print(f" ✓ 写入 {len(events)} 条事件 → {fpath}")