evidence_pack_builder.py 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632
  1. """Build DB-validated evidence packs for DemandAgent outputs."""
  2. from __future__ import annotations
  3. import json
  4. import re
  5. from collections import defaultdict
  6. from collections.abc import Iterable, Mapping
  7. from typing import Any
  8. from examples.demand.db_manager import (
  9. query_case_ids_by_post_ids,
  10. query_element_bindings_for_items,
  11. query_execution_for_evidence,
  12. query_itemset_evidence,
  13. query_itemset_items_with_categories,
  14. )
  15. SOURCE_KIND_PATTERN_ITEMSET = "pattern_itemset"
  16. PATTERN_SOURCE_SYSTEM = "pg_pattern_v2"
  17. PATTERN_ITEMSET_SCOPE = "topic"
  18. def build_evidence_pack(
  19. execution_id: int,
  20. demand_item: Any,
  21. trace_id: str,
  22. demand_task_id: int | None,
  23. demand_content_id: int | None,
  24. ) -> dict[str, Any]:
  25. """Return a DB-validated evidence pack or a reject result.
  26. This function is intentionally read-only. It never writes Demand rows or
  27. reject rows; callers can route the returned result into their own sink.
  28. """
  29. try:
  30. return _build_evidence_pack(
  31. execution_id=execution_id,
  32. demand_item=demand_item,
  33. trace_id=trace_id,
  34. demand_task_id=demand_task_id,
  35. demand_content_id=demand_content_id,
  36. )
  37. except Exception as exc:
  38. return _reject(f"db evidence validation failed: {exc}")
  39. def _build_evidence_pack(
  40. execution_id: int,
  41. demand_item: Any,
  42. trace_id: str,
  43. demand_task_id: int | None,
  44. demand_content_id: int | None,
  45. ) -> dict[str, Any]:
  46. evidence_refs = _extract_evidence_refs(demand_item)
  47. itemset_ids, invalid_itemset_ids = _normalize_int_list(
  48. _first_present(
  49. evidence_refs.get("itemset_ids"),
  50. evidence_refs.get("itemset_id"),
  51. _read_field(demand_item, "itemset_ids"),
  52. _read_field(demand_item, "itemset_id"),
  53. )
  54. )
  55. if invalid_itemset_ids:
  56. return _reject(f"invalid itemset_ids: {invalid_itemset_ids}")
  57. source_kind = _resolve_source_kind(evidence_refs, demand_item, itemset_ids)
  58. if source_kind and source_kind != SOURCE_KIND_PATTERN_ITEMSET:
  59. return _reject(f"unsupported source_kind={source_kind}; only pattern_itemset is supported")
  60. if not source_kind:
  61. return _reject("missing source_kind=pattern_itemset")
  62. if not itemset_ids:
  63. return _reject("missing itemset_ids for source_kind=pattern_itemset")
  64. execution = query_execution_for_evidence(int(execution_id))
  65. if not execution:
  66. return _reject(f"execution_id {execution_id} not found")
  67. execution_status = _clean_str(execution.get("status")).lower()
  68. if execution_status != "success":
  69. return _reject(
  70. f"execution_id {execution_id} status is {execution.get('status')}, not success"
  71. )
  72. itemsets = query_itemset_evidence(execution_id=int(execution_id), itemset_ids=itemset_ids)
  73. found_itemset_ids = {int(itemset["id"]) for itemset in itemsets}
  74. missing_itemset_ids = [itemset_id for itemset_id in itemset_ids if itemset_id not in found_itemset_ids]
  75. if missing_itemset_ids:
  76. return _reject(
  77. f"itemset_id {missing_itemset_ids[0]} not found under execution_id {execution_id}"
  78. )
  79. invalid_itemset_reason = _validate_itemset_facts(itemsets)
  80. if invalid_itemset_reason:
  81. return _reject(invalid_itemset_reason)
  82. mining_config_ids = _unique_ints(itemset.get("mining_config_id") for itemset in itemsets)
  83. if len(mining_config_ids) != 1:
  84. return _reject(
  85. "itemset_ids must resolve to exactly one mining_config_id; "
  86. f"got {mining_config_ids}"
  87. )
  88. itemset_items = query_itemset_items_with_categories(
  89. execution_id=int(execution_id),
  90. itemset_ids=itemset_ids,
  91. )
  92. items_by_itemset: dict[int, list[dict[str, Any]]] = defaultdict(list)
  93. for item in itemset_items:
  94. items_by_itemset[int(item["itemset_id"])].append(item)
  95. item_reason = _validate_itemset_items(
  96. execution_id=int(execution_id),
  97. itemsets=itemsets,
  98. items_by_itemset=items_by_itemset,
  99. )
  100. if item_reason:
  101. return _reject(item_reason)
  102. matched_post_ids = _merge_post_ids(itemset.get("matched_post_ids") for itemset in itemsets)
  103. requested_source_post_id = _choose_source_post_id(evidence_refs, demand_item, matched_post_ids)
  104. if not requested_source_post_id:
  105. return _reject("source_post_id cannot be resolved from DB-validated matched_post_ids")
  106. if requested_source_post_id not in set(matched_post_ids):
  107. return _reject(
  108. f"source_post_id {requested_source_post_id} is not in matched_post_ids for itemset_ids={itemset_ids}"
  109. )
  110. element_bindings = query_element_bindings_for_items(
  111. execution_id=int(execution_id),
  112. itemset_items=itemset_items,
  113. post_ids=[requested_source_post_id],
  114. )
  115. binding_reason = _validate_element_bindings(itemset_items, element_bindings)
  116. if binding_reason:
  117. resolved_source_post_id, element_bindings, binding_reason = _resolve_source_post_with_bindings(
  118. execution_id=int(execution_id),
  119. itemset_items=itemset_items,
  120. matched_post_ids=matched_post_ids,
  121. preferred_post_id=requested_source_post_id,
  122. )
  123. if binding_reason:
  124. return _reject(binding_reason)
  125. source_post_id = resolved_source_post_id
  126. else:
  127. source_post_id = requested_source_post_id
  128. seed_terms = _resolve_seed_terms(evidence_refs, itemset_items, element_bindings)
  129. seed_reason = _validate_seed_terms(seed_terms, itemset_items, element_bindings)
  130. if seed_reason:
  131. return _reject(seed_reason)
  132. case_rows = query_case_ids_by_post_ids(matched_post_ids)
  133. decode_case_ids = _unique_strings(row.get("case_id") for row in case_rows)
  134. evidence_pack = {
  135. "pattern_source_system": PATTERN_SOURCE_SYSTEM,
  136. "pattern_execution_id": int(execution_id),
  137. "mining_config_id": mining_config_ids[0],
  138. "source_kind": SOURCE_KIND_PATTERN_ITEMSET,
  139. "case_id_type": "post_id",
  140. "source_post_id": source_post_id,
  141. "category_bindings": _build_category_bindings(itemset_items),
  142. "element_bindings": _build_element_bindings(element_bindings),
  143. "itemset_ids": itemset_ids,
  144. "itemset_items": _build_itemset_items(itemset_items),
  145. "support": min(float(itemset["support"]) for itemset in itemsets),
  146. "absolute_support": min(int(itemset["absolute_support"]) for itemset in itemsets),
  147. "matched_post_ids": matched_post_ids,
  148. "video_ids": matched_post_ids,
  149. "case_ids": matched_post_ids,
  150. "decode_case_ids": decode_case_ids,
  151. "seed_terms": seed_terms,
  152. "trace_id": trace_id,
  153. "demand_task_id": demand_task_id,
  154. "demand_content_id": demand_content_id,
  155. "source_certainty": "db_validated",
  156. "validation_status": "passed",
  157. }
  158. return {"success": True, "evidence_pack": evidence_pack}
  159. def _reject(reason: str) -> dict[str, Any]:
  160. return {"success": False, "reject_reason": reason}
  161. def _resolve_source_kind(
  162. evidence_refs: Mapping[str, Any],
  163. demand_item: Any,
  164. itemset_ids: list[int],
  165. ) -> str:
  166. explicit = _clean_str(
  167. _first_present(
  168. evidence_refs.get("source_kind"),
  169. _read_field(demand_item, "source_kind"),
  170. )
  171. )
  172. if explicit:
  173. return explicit
  174. source_tool = _clean_str(evidence_refs.get("source_tool"))
  175. if itemset_ids or source_tool == "get_itemset_detail":
  176. return SOURCE_KIND_PATTERN_ITEMSET
  177. return ""
  178. def _extract_evidence_refs(demand_item: Any) -> dict[str, Any]:
  179. value = _read_field(demand_item, "evidence_refs", {})
  180. if isinstance(value, str):
  181. raw = value.strip()
  182. if not raw:
  183. return {}
  184. try:
  185. parsed = json.loads(raw)
  186. except json.JSONDecodeError:
  187. return {}
  188. return dict(parsed) if isinstance(parsed, Mapping) else {}
  189. return dict(value) if isinstance(value, Mapping) else {}
  190. def _read_field(source: Any, field: str, default: Any = None) -> Any:
  191. if source is None:
  192. return default
  193. if isinstance(source, Mapping):
  194. return source.get(field, default)
  195. if hasattr(source, field):
  196. return getattr(source, field)
  197. if hasattr(source, "model_dump"):
  198. dumped = source.model_dump()
  199. if isinstance(dumped, Mapping):
  200. return dumped.get(field, default)
  201. if hasattr(source, "dict"):
  202. dumped = source.dict()
  203. if isinstance(dumped, Mapping):
  204. return dumped.get(field, default)
  205. return default
  206. def _first_present(*values: Any) -> Any:
  207. for value in values:
  208. if value is None:
  209. continue
  210. if isinstance(value, str) and not value.strip():
  211. continue
  212. if isinstance(value, (list, tuple, set, dict)) and len(value) == 0:
  213. continue
  214. return value
  215. return None
  216. def _clean_str(value: Any) -> str:
  217. return str(value).strip() if value is not None else ""
  218. def _normalize_list(value: Any) -> list[Any]:
  219. if value is None:
  220. return []
  221. if isinstance(value, list):
  222. return value
  223. if isinstance(value, (tuple, set)):
  224. return list(value)
  225. if isinstance(value, str):
  226. raw = value.strip()
  227. if not raw:
  228. return []
  229. try:
  230. parsed = json.loads(raw)
  231. except json.JSONDecodeError:
  232. return [part.strip() for part in raw.split(",") if part.strip()]
  233. if isinstance(parsed, list):
  234. return parsed
  235. if parsed is None:
  236. return []
  237. return [parsed]
  238. return [value]
  239. def _normalize_int_list(value: Any) -> tuple[list[int], list[Any]]:
  240. result: list[int] = []
  241. invalid: list[Any] = []
  242. seen: set[int] = set()
  243. for raw in _normalize_list(value):
  244. try:
  245. parsed = int(raw)
  246. except (TypeError, ValueError):
  247. invalid.append(raw)
  248. continue
  249. if parsed not in seen:
  250. seen.add(parsed)
  251. result.append(parsed)
  252. return result, invalid
  253. def _unique_ints(values: Iterable[Any]) -> list[int]:
  254. result: list[int] = []
  255. seen: set[int] = set()
  256. for value in values:
  257. if value is None:
  258. continue
  259. parsed = int(value)
  260. if parsed not in seen:
  261. seen.add(parsed)
  262. result.append(parsed)
  263. return result
  264. def _unique_strings(values: Iterable[Any]) -> list[str]:
  265. result: list[str] = []
  266. seen: set[str] = set()
  267. for value in values:
  268. text = _clean_str(value)
  269. if text and text not in seen:
  270. seen.add(text)
  271. result.append(text)
  272. return result
  273. def _validate_itemset_facts(itemsets: list[dict[str, Any]]) -> str | None:
  274. for itemset in itemsets:
  275. itemset_id = itemset.get("id")
  276. execution_id = itemset.get("execution_id")
  277. if itemset.get("mining_config_id") is None:
  278. return f"itemset_id {itemset_id} missing mining_config_id"
  279. if int(itemset.get("mining_config_execution_id") or -1) != int(execution_id):
  280. return (
  281. f"itemset_id {itemset_id} mining_config_id {itemset.get('mining_config_id')} "
  282. f"does not belong to execution_id {execution_id}"
  283. )
  284. if _clean_str(itemset.get("scope")) != PATTERN_ITEMSET_SCOPE:
  285. return (
  286. f"itemset_id {itemset_id} scope={itemset.get('scope')} is not "
  287. f"{PATTERN_ITEMSET_SCOPE}"
  288. )
  289. if _clean_str(itemset.get("mining_config_scope")) != PATTERN_ITEMSET_SCOPE:
  290. return (
  291. f"itemset_id {itemset_id} mining_config_id {itemset.get('mining_config_id')} "
  292. f"scope={itemset.get('mining_config_scope')} is not {PATTERN_ITEMSET_SCOPE}"
  293. )
  294. if itemset.get("support") is None:
  295. return f"itemset_id {itemset_id} missing support"
  296. if itemset.get("absolute_support") is None:
  297. return f"itemset_id {itemset_id} missing absolute_support"
  298. matched_post_ids = itemset.get("matched_post_ids") or []
  299. if not matched_post_ids:
  300. return f"itemset_id {itemset_id} missing matched_post_ids"
  301. if len(matched_post_ids) != int(itemset["absolute_support"]):
  302. return (
  303. f"itemset_id {itemset_id} matched_post_ids count {len(matched_post_ids)} "
  304. f"does not equal absolute_support {itemset['absolute_support']}"
  305. )
  306. return None
  307. def _validate_itemset_items(
  308. execution_id: int,
  309. itemsets: list[dict[str, Any]],
  310. items_by_itemset: dict[int, list[dict[str, Any]]],
  311. ) -> str | None:
  312. for itemset in itemsets:
  313. itemset_id = int(itemset["id"])
  314. items = items_by_itemset.get(itemset_id, [])
  315. if not items:
  316. return f"itemset_id {itemset_id} has no itemset_items"
  317. expected_count = itemset.get("item_count")
  318. if expected_count is not None and int(expected_count) != len(items):
  319. return (
  320. f"itemset_id {itemset_id} item_count mismatch: "
  321. f"expected {expected_count}, got {len(items)}"
  322. )
  323. for item in items:
  324. category_id = item.get("category_id")
  325. if category_id is None:
  326. return f"itemset_id {itemset_id} has item without category_id"
  327. if not item.get("category_found"):
  328. return (
  329. f"category_id {category_id} for itemset_id {itemset_id} "
  330. f"not found under execution_id {execution_id}"
  331. )
  332. if int(item.get("category_execution_id")) != int(execution_id):
  333. return (
  334. f"category_id {category_id} belongs to execution_id "
  335. f"{item.get('category_execution_id')}, not {execution_id}"
  336. )
  337. return None
  338. def _merge_post_ids(post_id_groups: Iterable[Any]) -> list[str]:
  339. merged: list[str] = []
  340. seen: set[str] = set()
  341. for group in post_id_groups:
  342. for raw_post_id in _normalize_list(group):
  343. post_id = _clean_str(raw_post_id)
  344. if post_id and post_id not in seen:
  345. seen.add(post_id)
  346. merged.append(post_id)
  347. return merged
  348. def _choose_source_post_id(
  349. evidence_refs: Mapping[str, Any],
  350. demand_item: Any,
  351. matched_post_ids: list[str],
  352. ) -> str:
  353. candidate = _clean_str(
  354. _first_present(
  355. evidence_refs.get("source_post_id"),
  356. evidence_refs.get("post_id"),
  357. _read_field(demand_item, "source_post_id"),
  358. _read_field(demand_item, "post_id"),
  359. )
  360. )
  361. if candidate:
  362. return candidate
  363. candidate_posts = _normalize_list(
  364. _first_present(
  365. evidence_refs.get("video_ids"),
  366. evidence_refs.get("matched_post_ids"),
  367. _read_field(demand_item, "video_ids"),
  368. )
  369. )
  370. matched_set = set(matched_post_ids)
  371. for raw_post_id in candidate_posts:
  372. post_id = _clean_str(raw_post_id)
  373. if post_id in matched_set:
  374. return post_id
  375. return matched_post_ids[0] if matched_post_ids else ""
  376. def _resolve_source_post_with_bindings(
  377. execution_id: int,
  378. itemset_items: list[dict[str, Any]],
  379. matched_post_ids: list[str],
  380. preferred_post_id: str,
  381. ) -> tuple[str, list[dict[str, Any]], str | None]:
  382. broad_bindings = query_element_bindings_for_items(
  383. execution_id=int(execution_id),
  384. itemset_items=itemset_items,
  385. post_ids=matched_post_ids,
  386. limit_per_item=max(len(matched_post_ids), 200),
  387. )
  388. by_item_id = {
  389. int(binding["itemset_item_id"]): set(_normalize_list(binding.get("matched_post_ids")))
  390. for binding in broad_bindings
  391. if binding.get("itemset_item_id") is not None
  392. }
  393. candidate_posts: set[str] | None = set(matched_post_ids)
  394. for item in itemset_items:
  395. item_id = int(item["itemset_item_id"])
  396. item_posts = {str(post_id) for post_id in by_item_id.get(item_id, set())}
  397. if not item_posts:
  398. return "", broad_bindings, f"itemset_item_id {item_id} has no exact PG topic element binding"
  399. candidate_posts = candidate_posts & item_posts if candidate_posts is not None else item_posts
  400. ordered_candidates = [post_id for post_id in matched_post_ids if post_id in (candidate_posts or set())]
  401. if not ordered_candidates:
  402. return "", broad_bindings, "no source_post_id can close all PG topic element bindings"
  403. source_post_id = preferred_post_id if preferred_post_id in ordered_candidates else ordered_candidates[0]
  404. exact_bindings = query_element_bindings_for_items(
  405. execution_id=int(execution_id),
  406. itemset_items=itemset_items,
  407. post_ids=[source_post_id],
  408. )
  409. binding_reason = _validate_element_bindings(itemset_items, exact_bindings)
  410. if binding_reason:
  411. return "", exact_bindings, binding_reason
  412. return source_post_id, exact_bindings, None
  413. def _validate_element_bindings(
  414. itemset_items: list[dict[str, Any]],
  415. element_bindings: list[dict[str, Any]],
  416. ) -> str | None:
  417. by_item_id = {
  418. int(binding["itemset_item_id"]): binding
  419. for binding in element_bindings
  420. if binding.get("itemset_item_id") is not None
  421. }
  422. for item in itemset_items:
  423. item_id = int(item["itemset_item_id"])
  424. binding = by_item_id.get(item_id)
  425. if not binding:
  426. return f"itemset_item_id {item_id} has no element binding"
  427. if int(binding.get("matched_element_count") or 0) <= 0:
  428. return f"itemset_item_id {item_id} has no exact PG topic element binding"
  429. if int(binding.get("matched_post_count") or 0) <= 0:
  430. return f"itemset_item_id {item_id} has no matched post binding"
  431. return None
  432. def _resolve_seed_terms(
  433. evidence_refs: Mapping[str, Any],
  434. itemset_items: list[dict[str, Any]],
  435. element_bindings: list[dict[str, Any]],
  436. ) -> list[str]:
  437. provided = _unique_strings(_normalize_list(evidence_refs.get("seed_terms")))
  438. if provided:
  439. covered_terms = _build_covered_terms(itemset_items, element_bindings)
  440. verified = [term for term in provided if _normalize_term(term) in covered_terms]
  441. if verified:
  442. return verified
  443. derived: list[str] = []
  444. for item in itemset_items:
  445. for value in (
  446. item.get("element_name"),
  447. item.get("category_name"),
  448. _last_path_part(item.get("category_path")),
  449. _last_path_part(item.get("category_full_path")),
  450. ):
  451. text = _clean_str(value)
  452. if text and text not in derived:
  453. derived.append(text)
  454. return derived
  455. def _validate_seed_terms(
  456. seed_terms: list[str],
  457. itemset_items: list[dict[str, Any]],
  458. element_bindings: list[dict[str, Any]],
  459. ) -> str | None:
  460. if not seed_terms:
  461. return "seed_terms cannot be resolved from itemset evidence"
  462. covered_terms = _build_covered_terms(itemset_items, element_bindings)
  463. uncovered = [term for term in seed_terms if _normalize_term(term) not in covered_terms]
  464. if uncovered:
  465. return f"seed_terms not covered by DB evidence: {uncovered}"
  466. return None
  467. def _build_covered_terms(
  468. itemset_items: list[dict[str, Any]],
  469. element_bindings: list[dict[str, Any]],
  470. ) -> set[str]:
  471. terms: set[str] = set()
  472. for item in itemset_items:
  473. for field in ("element_name", "category_name", "category_path", "category_full_path"):
  474. _add_term_variants(terms, item.get(field))
  475. for binding in element_bindings:
  476. _add_term_variants(terms, binding.get("element_name"))
  477. for sample in binding.get("sample_elements") or []:
  478. _add_term_variants(terms, sample.get("name"))
  479. _add_term_variants(terms, sample.get("category_path"))
  480. return terms
  481. def _add_term_variants(terms: set[str], value: Any) -> None:
  482. text = _clean_str(value)
  483. if not text:
  484. return
  485. terms.add(_normalize_term(text))
  486. for part in _split_pathish(text):
  487. terms.add(_normalize_term(part))
  488. def _normalize_term(value: Any) -> str:
  489. return re.sub(r"\s+", "", _clean_str(value))
  490. def _split_pathish(value: Any) -> list[str]:
  491. text = _clean_str(value)
  492. if not text:
  493. return []
  494. return [part.strip() for part in re.split(r"[>/|,,]+", text) if part.strip()]
  495. def _last_path_part(value: Any) -> str:
  496. parts = _split_pathish(value)
  497. return parts[-1] if parts else ""
  498. def _build_category_bindings(itemset_items: list[dict[str, Any]]) -> list[dict[str, Any]]:
  499. bindings: list[dict[str, Any]] = []
  500. seen: set[tuple[int, int | None]] = set()
  501. for item in itemset_items:
  502. key = (int(item["category_id"]), item.get("itemset_id"))
  503. if key in seen:
  504. continue
  505. seen.add(key)
  506. bindings.append(
  507. {
  508. "itemset_id": item.get("itemset_id"),
  509. "itemset_item_id": item.get("itemset_item_id"),
  510. "category_id": item.get("category_id"),
  511. "category_name": item.get("category_name"),
  512. "category_path": item.get("category_path") or item.get("category_full_path"),
  513. "category_full_path": item.get("category_full_path"),
  514. "category_source_type": item.get("category_source_type"),
  515. "category_level": item.get("category_level"),
  516. "point_type": item.get("point_type"),
  517. "dimension": item.get("dimension"),
  518. "element_name": item.get("element_name"),
  519. }
  520. )
  521. return bindings
  522. def _build_element_bindings(element_bindings: list[dict[str, Any]]) -> list[dict[str, Any]]:
  523. return [
  524. {
  525. "itemset_id": binding.get("itemset_id"),
  526. "itemset_item_id": binding.get("itemset_item_id"),
  527. "category_id": binding.get("category_id"),
  528. "point_type": binding.get("point_type"),
  529. "dimension": binding.get("dimension"),
  530. "element_name": binding.get("element_name"),
  531. "matched_element_count": binding.get("matched_element_count"),
  532. "matched_post_count": binding.get("matched_post_count"),
  533. "matched_post_ids": binding.get("matched_post_ids") or [],
  534. "sample_elements": binding.get("sample_elements") or [],
  535. }
  536. for binding in element_bindings
  537. ]
  538. def _build_itemset_items(itemset_items: list[dict[str, Any]]) -> list[dict[str, Any]]:
  539. return [
  540. {
  541. "itemset_id": item.get("itemset_id"),
  542. "category_id": item.get("category_id"),
  543. "category_path": item.get("category_path") or item.get("category_full_path"),
  544. "point_type": item.get("point_type"),
  545. "dimension": item.get("dimension"),
  546. "element_name": item.get("element_name"),
  547. }
  548. for item in itemset_items
  549. ]