common.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. from __future__ import annotations
  2. import re
  3. from collections.abc import Callable, Iterable
  4. from typing import Any
  5. from .schema import RuntimeUnit, field
  6. EventLoader = Callable[[int], dict[str, Any]]
  7. def integer(value: Any) -> int | None:
  8. try:
  9. return int(value)
  10. except (TypeError, ValueError):
  11. return None
  12. def suffix_int(ref: str) -> int | None:
  13. return integer(str(ref).rsplit(":", 1)[-1])
  14. def event_content(side: Any) -> Any:
  15. return side.get("content") if isinstance(side, dict) else None
  16. def event_completeness(event: dict[str, Any]) -> str:
  17. has_input = event_content(event.get("input")) not in (None, "", [], {})
  18. has_output = event_content(event.get("output")) not in (None, "", [], {})
  19. if has_input and has_output:
  20. return "complete"
  21. if has_input or has_output or event:
  22. return "partial"
  23. return "missing"
  24. def event_unit(event: dict[str, Any], *, label: str | None = None) -> RuntimeUnit:
  25. event_id = integer(event.get("id")) or 0
  26. status = str(event.get("status") or "unknown")
  27. event_label = label or str(event.get("businessLabel") or event.get("title") or event.get("event_name") or f"运行事件 {event_id}")
  28. return {
  29. "id": f"runtime:event:{event_id}",
  30. "label": event_label,
  31. "status": status,
  32. "summary": event_summary(event, event_label),
  33. "input": event.get("input"),
  34. "output": event.get("output"),
  35. "durationMs": integer(event.get("duration_ms")),
  36. "eventRef": f"event:{event_id}",
  37. "completeness": event_completeness(event), # type: ignore[typeddict-item]
  38. "startedAt": event.get("started_at"),
  39. "endedAt": event.get("ended_at"),
  40. }
  41. def event_summary(event: dict[str, Any], fallback: str) -> str:
  42. if event.get("error_message"):
  43. return f"技术失败:{event['error_message']}"
  44. output = event_content(event.get("output"))
  45. if isinstance(output, dict):
  46. for key in ("summary", "message", "conclusion"):
  47. if output.get(key):
  48. return str(output[key])
  49. if event.get("output_preview"):
  50. return str(event["output_preview"])
  51. return fallback
  52. def load_events(loader: EventLoader, refs: Iterable[Any]) -> list[dict[str, Any]]:
  53. values: list[dict[str, Any]] = []
  54. seen: set[int] = set()
  55. for ref in refs:
  56. event_id = suffix_int(str(ref)) if isinstance(ref, str) else integer(ref)
  57. if event_id is None or event_id in seen:
  58. continue
  59. seen.add(event_id)
  60. try:
  61. values.append(loader(event_id))
  62. except LookupError:
  63. continue
  64. return values
  65. def event_notices(events: Iterable[dict[str, Any]]) -> list[dict[str, str]]:
  66. notices: list[dict[str, str]] = []
  67. omitted = 0
  68. missing_body = 0
  69. for event in events:
  70. for side_name in ("input", "output"):
  71. side = event.get(side_name)
  72. if isinstance(side, dict) and side.get("truncated"):
  73. omitted += int(side.get("omittedCharacters") or 0)
  74. if event.get("input") is None and event.get("output") is None:
  75. missing_body += 1
  76. if omitted:
  77. notices.append({"level": "warning", "message": f"Event Body 超过读取上限,共省略 {omitted} 个字符。"})
  78. if missing_body:
  79. notices.append({"level": "warning", "message": f"{missing_body} 个运行事件没有保存可用的 Event Body。"})
  80. return notices
  81. def find_by_id(items: Iterable[dict[str, Any]], record_id: int | None) -> dict[str, Any] | None:
  82. return next((item for item in items if integer(item.get("id")) == record_id), None)
  83. def find_round(view: dict[str, Any], round_index: int) -> dict[str, Any] | None:
  84. return next((item for item in view.get("rounds") or [] if integer(item.get("roundIndex")) == round_index), None)
  85. def find_raw_round(bundle: dict[str, Any], round_index: int) -> dict[str, Any] | None:
  86. return next((item for item in bundle.get("rounds") or [] if integer(item.get("round_index")) == round_index), None)
  87. def find_branch(view: dict[str, Any], round_index: int, branch_id: int) -> dict[str, Any] | None:
  88. round_ = find_round(view, round_index)
  89. return next((item for item in (round_ or {}).get("branches") or [] if integer(item.get("branchId")) == branch_id), None)
  90. def find_raw_branch(bundle: dict[str, Any], branch_id: int) -> dict[str, Any] | None:
  91. return next((item for item in bundle.get("branches") or [] if integer(item.get("branch_id")) == branch_id), None)
  92. def find_detail(value: Any, detail_ref: str) -> dict[str, Any] | None:
  93. if isinstance(value, dict):
  94. if value.get("detailRef") == detail_ref:
  95. return value
  96. for child in value.values():
  97. found = find_detail(child, detail_ref)
  98. if found is not None:
  99. return found
  100. elif isinstance(value, list):
  101. for child in value:
  102. found = find_detail(child, detail_ref)
  103. if found is not None:
  104. return found
  105. return None
  106. def decision_context(node: dict[str, Any] | None) -> dict[str, Any]:
  107. if not isinstance(node, dict):
  108. return {}
  109. decision = node.get("decision")
  110. if isinstance(decision, dict):
  111. body = decision.get("body")
  112. if isinstance(body, dict):
  113. return {**decision, **body}
  114. return decision
  115. return node
  116. def context_input_fields(
  117. context: dict[str, Any],
  118. prefix: str,
  119. events: list[dict[str, Any]] | None = None,
  120. ) -> list[dict[str, Any]]:
  121. relation_map = {
  122. "direct-read": "direct-input",
  123. "standing-constraint": "standing-constraint",
  124. "persisted-record": "previous-result",
  125. "explicit-basis": "explicit-basis",
  126. }
  127. fields: list[dict[str, Any]] = []
  128. events_by_ref = {
  129. f"event:{integer(event.get('id'))}": event
  130. for event in events or []
  131. if integer(event.get("id"))
  132. }
  133. for index, item in enumerate(context.get("inputs") or [], 1):
  134. if not isinstance(item, dict):
  135. continue
  136. raw_relation = str(item.get("relation") or "")
  137. relation = relation_map.get(raw_relation, "available-upstream")
  138. detail_ref = str(item.get("detailRef") or "")
  139. label = str(item.get("label") or f"上游输入 {index}")
  140. if not detail_ref:
  141. detail_ref = _implicit_context_ref(prefix, label)
  142. event = events_by_ref.get(detail_ref) or {}
  143. event_name = str(event.get("event_name") or "")
  144. source_kind, source_label, field_path = _context_source(
  145. detail_ref, raw_relation, event_name
  146. )
  147. fields.append(field(
  148. f"{prefix}:input:{index}",
  149. label,
  150. item.get("summary"),
  151. source_kind=source_kind,
  152. source_label=source_label,
  153. source_ref=detail_ref or None,
  154. field_path=field_path,
  155. relation=relation,
  156. ))
  157. return fields
  158. def _context_source(
  159. detail_ref: str,
  160. raw_relation: str,
  161. event_name: str,
  162. ) -> tuple[str, str, str]:
  163. """Classify context sources from their real reference contract."""
  164. event_paths = {
  165. "save_script_direction": "input.content.script_direction",
  166. "begin_round": "input.content.goal",
  167. "record_multipath_plan": "input.content",
  168. }
  169. if event_name or detail_ref.startswith("event:"):
  170. return (
  171. "runtime-event",
  172. "明确读取的运行事件" if raw_relation == "direct-read" else "上游运行事件",
  173. event_paths.get(event_name, ""),
  174. )
  175. database_refs = (
  176. (r"^run:objective$", "script_build_record", "script_direction"),
  177. (r"^round:\d+:goal$", "script_build_round", "goal"),
  178. (r"^multipath-decision:\d+$", "script_build_multipath_decision", "decision"),
  179. (r"^data-decision:\d+$", "script_build_data_decision", "decision"),
  180. (r"^domain-info:\d+$", "script_build_domain_info", "content"),
  181. )
  182. for pattern, table, field_path in database_refs:
  183. if re.match(pattern, detail_ref):
  184. return "database", table, field_path
  185. if detail_ref.startswith("artifact:"):
  186. return "artifact", "脚本或候选产物", ""
  187. return "database", "上游业务结果", ""
  188. def _implicit_context_ref(prefix: str, label: str) -> str:
  189. """Recover only deterministic upstream refs omitted by historical views."""
  190. if prefix == "objective" and label in {"选题", "账号人设"}:
  191. return ""
  192. if re.match(r"^round:\d+:goal$", prefix) and label == "当前保存的创作目标":
  193. return "run:objective"
  194. match = re.match(r"^(round:\d+):plan$", prefix)
  195. if match and label == "本轮目标":
  196. return f"{match.group(1)}:goal"
  197. return ""
  198. def decision_event_refs(context: dict[str, Any]) -> list[str]:
  199. refs: list[str] = []
  200. for key in ("revisionRefs", "technicalRefs", "eventRefs"):
  201. for ref in context.get(key) or []:
  202. if isinstance(ref, str) and ref.startswith("event:") and ref not in refs:
  203. refs.append(ref)
  204. for key in ("currentRevisionRef", "eventRef"):
  205. ref = context.get(key)
  206. if isinstance(ref, str) and ref.startswith("event:") and ref not in refs:
  207. refs.append(ref)
  208. for item in context.get("inputs") or []:
  209. ref = item.get("detailRef") if isinstance(item, dict) else None
  210. if isinstance(ref, str) and ref.startswith("event:") and ref not in refs:
  211. refs.append(ref)
  212. for item in context.get("revisions") or []:
  213. ref = item.get("detailRef") if isinstance(item, dict) else None
  214. if isinstance(ref, str) and ref.startswith("event:") and ref not in refs:
  215. refs.append(ref)
  216. return refs
  217. def output_field_from_event(event: dict[str, Any], prefix: str, label: str, relation: str = "persisted-output") -> dict[str, Any]:
  218. event_id = integer(event.get("id")) or 0
  219. return field(
  220. f"{prefix}:event:{event_id}:output",
  221. label,
  222. event_content(event.get("output")),
  223. source_kind="runtime-event",
  224. source_label="Event Body 输出",
  225. source_ref=f"event:{event_id}",
  226. field_path="output.content",
  227. relation=relation,
  228. completeness=event_completeness(event),
  229. )