topic_table.py 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639
  1. """Application service for deriving Query candidates from one topic-table snapshot."""
  2. from __future__ import annotations
  3. import json
  4. import re
  5. from copy import deepcopy
  6. from dataclasses import dataclass
  7. from typing import Any, Callable
  8. from core.prompts import load_prompt
  9. from query_planning.domain import GenerationRequest, GenerationResult, GeneratorKind
  10. from query_planning.service import UnifiedQueryGenerationService
  11. PROMPT_NAME = "topic_table_query_generation"
  12. PROMPT_VERSION = "topic_table_query_v4"
  13. ROUTES: dict[str, dict[str, Any]] = {
  14. "unique_hook": {
  15. "label": "灵感点",
  16. "source_description": "来自选题的灵感点字段",
  17. "priority": 0,
  18. },
  19. "goal_execution": {
  20. "label": "创作目标",
  21. "source_description": "来自目的点字段",
  22. "priority": 0,
  23. },
  24. "key_execution": {
  25. "label": "关键镜头和动作",
  26. "source_description": "来自关键点字段",
  27. "priority": 0,
  28. },
  29. "cross_dimension": {
  30. "label": "实质、形式、意图及元素关系",
  31. "source_description": "来自组成元素、最终选题描述和元素关系字段",
  32. "priority": 0,
  33. },
  34. "account_fit": {
  35. "label": "账号内容偏好",
  36. "source_description": "来自账号的实质偏好、形式偏好和意图偏好字段",
  37. "priority": 0,
  38. },
  39. "evidence_gap": {
  40. "label": "参考案例和选取理由",
  41. "source_description": "来自每个元素引用的参考来源说明字段",
  42. "priority": 0,
  43. },
  44. }
  45. class TopicTableGenerationError(ValueError):
  46. """Raised when a topic snapshot or LLM result cannot become a Query plan."""
  47. @dataclass(frozen=True)
  48. class TopicTablePreview:
  49. result: GenerationResult
  50. source: dict[str, Any]
  51. topic: dict[str, Any]
  52. llm_output: dict[str, Any]
  53. def topic_table_prompt() -> str:
  54. return load_prompt(PROMPT_NAME)
  55. def _select_topic(payload: dict[str, Any], topic_id: int | None) -> dict[str, Any]:
  56. topics = [topic for topic in payload.get("topics") or [] if isinstance(topic, dict)]
  57. if topic_id is not None:
  58. for topic in topics:
  59. if int(topic.get("id") or 0) == topic_id:
  60. return topic
  61. raise TopicTableGenerationError(f"选题构建中找不到 topic_id={topic_id}")
  62. mature = next((topic for topic in topics if topic.get("status") == "mature"), None)
  63. if mature is not None:
  64. return mature
  65. if topics:
  66. return topics[0]
  67. raise TopicTableGenerationError("选题构建中没有可用 topic")
  68. def _source_summary(source: dict[str, Any]) -> dict[str, Any]:
  69. return {
  70. "derivation_type": source.get("derivation_type"),
  71. "source_type": source.get("source_type"),
  72. "source_reference_id": source.get("source_reference_id"),
  73. "reason": source.get("reason"),
  74. "dataset_from": source.get("dataset_from"),
  75. }
  76. def _compact_snapshot(
  77. payload: dict[str, Any],
  78. topic: dict[str, Any],
  79. ) -> dict[str, Any]:
  80. build = payload.get("build") or {}
  81. items = []
  82. for item in topic.get("composition_items") or []:
  83. if not isinstance(item, dict) or item.get("is_active") is False:
  84. continue
  85. items.append(
  86. {
  87. "id": item.get("id"),
  88. "item_level": item.get("item_level"),
  89. "dimension": item.get("dimension"),
  90. "point_type": item.get("point_type"),
  91. "element_name": item.get("element_name"),
  92. "category_path": item.get("category_path"),
  93. "reason": item.get("reason"),
  94. "is_active": item.get("is_active"),
  95. "sources": [
  96. _source_summary(source)
  97. for source in item.get("sources") or []
  98. if isinstance(source, dict)
  99. ],
  100. }
  101. )
  102. return {
  103. "topic_build_id": build.get("id"),
  104. "topic_id": topic.get("id"),
  105. "build": {
  106. "demand": build.get("demand"),
  107. "demand_constraints": build.get("demand_constraints"),
  108. "strategies_config": build.get("strategies_config"),
  109. "status": build.get("status"),
  110. },
  111. "topic": {
  112. "status": topic.get("status"),
  113. "result": topic.get("result"),
  114. "topic_direction": topic.get("topic_direction"),
  115. "points": [
  116. deepcopy(point)
  117. for point in topic.get("points") or []
  118. if isinstance(point, dict) and point.get("is_active") is not False
  119. ],
  120. "composition_items": items,
  121. "item_relations": deepcopy(topic.get("item_relations") or []),
  122. },
  123. }
  124. def build_topic_table_user_prompt(
  125. snapshot: dict[str, Any],
  126. field_catalog: list[dict[str, Any]],
  127. *,
  128. target_query_count: int,
  129. ) -> str:
  130. return (
  131. "请从 field_catalog 中选择真正能产生创作方法搜索词的具体字段。"
  132. "field_catalog 已经按照六种数据来源标明 source_group_key 和 source_group_label。"
  133. "每一项 field_mapping 只能引用一个真实 field_key;不能合并多个字段,也不能自己编字段。"
  134. "六种数据来源必须全部覆盖,每种来源至少选择一个字段。"
  135. f"所有字段合计生成约 {target_query_count} 条具体搜索词,每个字段生成 1~3 条。"
  136. "judgement 必须是一句不超过 45 个汉字的大白话,只说明大模型针对该字段要判断什么。"
  137. "不要提前编造答案、参数、比例或效果数据,不要启动搜索。\n\n"
  138. + json.dumps(
  139. {
  140. "topic_build_id": snapshot["topic_build_id"],
  141. "topic_id": snapshot["topic_id"],
  142. "field_catalog": field_catalog,
  143. },
  144. ensure_ascii=False,
  145. indent=2,
  146. default=str,
  147. )
  148. )
  149. def _field_catalog(snapshot: dict[str, Any]) -> list[dict[str, Any]]:
  150. topic = snapshot["topic"]
  151. topic_id = snapshot["topic_id"]
  152. fields: list[dict[str, Any]] = []
  153. def add(
  154. *,
  155. field_key: str,
  156. field_path: str,
  157. field_label: str,
  158. field_value: Any,
  159. route: str,
  160. point_id: Any = None,
  161. item_ids: list[Any] | None = None,
  162. ) -> None:
  163. value = str(field_value or "").strip()
  164. if not value:
  165. return
  166. fields.append(
  167. {
  168. "field_key": field_key,
  169. "field_path": field_path,
  170. "field_label": field_label,
  171. "field_value": value,
  172. "source_group_key": route,
  173. "source_group_label": ROUTES[route]["label"],
  174. "point_id": point_id,
  175. "item_ids": list(item_ids or []),
  176. }
  177. )
  178. add(
  179. field_key="topic.result",
  180. field_path=f"topics[id={topic_id}].result",
  181. field_label="最终选题描述",
  182. field_value=topic.get("result"),
  183. route="cross_dimension",
  184. )
  185. direction = topic.get("topic_direction") or {}
  186. for preference, label in (
  187. ("实质偏好", "账号实质偏好"),
  188. ("形式偏好", "账号形式偏好"),
  189. ("意图偏好", "账号意图偏好"),
  190. ):
  191. add(
  192. field_key=f"topic_direction.persona_dimensions.{preference}",
  193. field_path=f"topics[id={topic_id}].topic_direction.persona_dimensions.{preference}",
  194. field_label=label,
  195. field_value=(direction.get("persona_dimensions") or {}).get(preference),
  196. route="account_fit",
  197. )
  198. point_routes = {
  199. "灵感点": "unique_hook",
  200. "目的点": "goal_execution",
  201. "关键点": "key_execution",
  202. }
  203. for point in topic.get("points") or []:
  204. point_id = point.get("id")
  205. point_type = str(point.get("point_type") or "选题点")
  206. add(
  207. field_key=f"point:{point_id}:point_result",
  208. field_path=f"topics[id={topic_id}].points[id={point_id}].point_result",
  209. field_label=f"{point_type}字段",
  210. field_value=point.get("point_result"),
  211. route=point_routes.get(point_type, "cross_dimension"),
  212. point_id=point_id,
  213. item_ids=list(point.get("item_ids") or []),
  214. )
  215. for item in topic.get("composition_items") or []:
  216. item_id = item.get("id")
  217. dimension = str(item.get("dimension") or "元素")
  218. add(
  219. field_key=f"item:{item_id}:element_name",
  220. field_path=(
  221. f"topics[id={topic_id}].composition_items[id={item_id}].element_name"
  222. ),
  223. field_label=f"{dimension}字段",
  224. field_value=item.get("element_name"),
  225. route="cross_dimension",
  226. item_ids=[item_id],
  227. )
  228. for source_index, source in enumerate(item.get("sources") or []):
  229. add(
  230. field_key=f"item:{item_id}:source:{source_index}:reason",
  231. field_path=(
  232. f"topics[id={topic_id}].composition_items[id={item_id}]"
  233. f".sources[{source_index}].reason"
  234. ),
  235. field_label="参考来源说明",
  236. field_value=source.get("reason"),
  237. route="evidence_gap",
  238. item_ids=[item_id],
  239. )
  240. for relation_index, relation in enumerate(topic.get("item_relations") or []):
  241. add(
  242. field_key=f"relation:{relation_index}:reason",
  243. field_path=f"topics[id={topic_id}].item_relations[{relation_index}].reason",
  244. field_label="元素关系说明",
  245. field_value=relation.get("reason"),
  246. route="cross_dimension",
  247. item_ids=[
  248. value
  249. for value in (
  250. relation.get("source_item_id"),
  251. relation.get("target_item_id"),
  252. )
  253. if value is not None
  254. ],
  255. )
  256. return fields
  257. def _safe_need_key(value: Any, route: str, index: int) -> str:
  258. raw = re.sub(r"[^a-zA-Z0-9_-]+", "_", str(value or "").strip()).strip("_")
  259. return raw[:80] or f"{route}_{index + 1}"
  260. def _planning_payload(
  261. llm_output: dict[str, Any],
  262. *,
  263. snapshot: dict[str, Any],
  264. field_catalog: list[dict[str, Any]],
  265. ) -> dict[str, Any]:
  266. raw_mappings = llm_output.get("field_mappings")
  267. if not isinstance(raw_mappings, list) or not raw_mappings:
  268. raise TopicTableGenerationError("大模型没有返回字段与搜索词的对应关系")
  269. topic_build_id = snapshot["topic_build_id"]
  270. topic_id = snapshot["topic_id"]
  271. catalog_by_key = {field["field_key"]: field for field in field_catalog}
  272. needs: list[dict[str, Any]] = []
  273. route_mapping_candidates: dict[str, list[list[dict[str, Any]]]] = {
  274. route: [] for route in ROUTES
  275. }
  276. used_field_keys: set[str] = set()
  277. used_need_keys: set[str] = set()
  278. route_mapping_counts: dict[str, int] = {route: 0 for route in ROUTES}
  279. selected_labels_by_route: dict[str, set[str]] = {
  280. route: set() for route in ROUTES
  281. }
  282. for index, raw in enumerate(raw_mappings):
  283. if not isinstance(raw, dict):
  284. continue
  285. field_key = str(raw.get("field_key") or "").strip()
  286. field = catalog_by_key.get(field_key)
  287. if field is None:
  288. raise TopicTableGenerationError(f"大模型引用了不存在的选题表字段:{field_key}")
  289. if field_key in used_field_keys:
  290. raise TopicTableGenerationError(f"大模型重复引用了同一个选题表字段:{field_key}")
  291. used_field_keys.add(field_key)
  292. judgement = str(raw.get("judgement") or "").strip()
  293. if not judgement:
  294. continue
  295. raw_queries = raw.get("queries") or []
  296. if isinstance(raw_queries, str):
  297. raw_queries = [raw_queries]
  298. query_texts = [
  299. str(query or "").strip()
  300. for query in raw_queries
  301. if str(query or "").strip()
  302. ]
  303. if not query_texts:
  304. continue
  305. route = field["source_group_key"]
  306. route_mapping_counts[route] += 1
  307. selected_labels_by_route[route].add(field["field_label"])
  308. priority = ROUTES[route]["priority"]
  309. need_key = _safe_need_key(field_key, route, index)
  310. if need_key in used_need_keys:
  311. need_key = f"{need_key}_{index + 1}"
  312. used_need_keys.add(need_key)
  313. source_ref = {
  314. "generator_kind": GeneratorKind.TOPIC_TABLE.value,
  315. "topic_build_id": topic_build_id,
  316. "topic_id": topic_id,
  317. "route": route,
  318. "field_key": field["field_key"],
  319. "field_path": field["field_path"],
  320. "field_label": field["field_label"],
  321. "field_value": field["field_value"],
  322. "point_id": field.get("point_id"),
  323. "item_ids": list(field.get("item_ids") or []),
  324. }
  325. needs.append(
  326. {
  327. "need_key": need_key,
  328. "decision_context": judgement,
  329. "unknown_information": judgement,
  330. "source_ref": source_ref,
  331. "priority": priority,
  332. "metadata": {
  333. "route": route,
  334. "route_label": ROUTES[route]["label"],
  335. "source_field": field,
  336. },
  337. }
  338. )
  339. mapping_candidates: list[dict[str, Any]] = []
  340. for query_text in query_texts:
  341. mapping_candidates.append(
  342. {
  343. "query_text": query_text,
  344. "axes": {
  345. "选题表字段": field["field_path"],
  346. "字段值": field["field_value"],
  347. "大模型判断": judgement,
  348. },
  349. "priority": priority,
  350. "original_position": 0,
  351. "source_refs": [source_ref],
  352. "knowledge_need_keys": [need_key],
  353. "metadata": {
  354. "family_key": GeneratorKind.TOPIC_TABLE.value,
  355. "route": route,
  356. "route_label": ROUTES[route]["label"],
  357. "need_key": need_key,
  358. },
  359. }
  360. )
  361. route_mapping_candidates[route].append(mapping_candidates)
  362. missing_routes = [
  363. ROUTES[route]["label"]
  364. for route, count in route_mapping_counts.items()
  365. if count == 0
  366. ]
  367. if missing_routes:
  368. raise TopicTableGenerationError(
  369. "大模型没有覆盖全部六种数据来源:" + "、".join(missing_routes)
  370. )
  371. required_dimension_labels = {
  372. field["field_label"]
  373. for field in field_catalog
  374. if field["source_group_key"] == "cross_dimension"
  375. and field["field_label"] in {"实质字段", "形式字段", "意图字段"}
  376. }
  377. missing_dimension_labels = sorted(
  378. required_dimension_labels - selected_labels_by_route["cross_dimension"]
  379. )
  380. if missing_dimension_labels:
  381. raise TopicTableGenerationError(
  382. "组成元素没有同时覆盖实质、形式、意图字段:"
  383. + "、".join(missing_dimension_labels)
  384. )
  385. required_account_labels = {
  386. field["field_label"]
  387. for field in field_catalog
  388. if field["source_group_key"] == "account_fit"
  389. }
  390. missing_account_labels = sorted(
  391. required_account_labels - selected_labels_by_route["account_fit"]
  392. )
  393. if missing_account_labels:
  394. raise TopicTableGenerationError(
  395. "账号内容偏好没有覆盖完整:" + "、".join(missing_account_labels)
  396. )
  397. route_candidates: dict[str, list[dict[str, Any]]] = {
  398. route: [] for route in ROUTES
  399. }
  400. for route, mapping_lists in route_mapping_candidates.items():
  401. max_length = max((len(items) for items in mapping_lists), default=0)
  402. for query_index in range(max_length):
  403. for mapping_candidates in mapping_lists:
  404. if query_index < len(mapping_candidates):
  405. route_candidates[route].append(mapping_candidates[query_index])
  406. candidates: list[dict[str, Any]] = []
  407. position = 0
  408. while any(route_candidates.values()):
  409. for route in ROUTES:
  410. if not route_candidates[route]:
  411. continue
  412. candidate = route_candidates[route].pop(0)
  413. candidate["original_position"] = position
  414. candidates.append(candidate)
  415. position += 1
  416. if not needs or not candidates:
  417. raise TopicTableGenerationError("大模型返回内容无法形成字段与搜索词的对应关系")
  418. return {
  419. "input_snapshot": snapshot,
  420. "knowledge_needs": needs,
  421. "candidates": candidates,
  422. "generator_config": {
  423. "prompt_name": PROMPT_NAME,
  424. "prompt_version": PROMPT_VERSION,
  425. "field_count": len(field_catalog),
  426. },
  427. }
  428. def generate_topic_table_preview(
  429. source_payload: dict[str, Any],
  430. *,
  431. topic_id: int | None,
  432. max_queries: int | None,
  433. chat_fn: Callable[[str, str], dict[str, Any]],
  434. ) -> TopicTablePreview:
  435. topic = _select_topic(source_payload, topic_id)
  436. snapshot = _compact_snapshot(source_payload, topic)
  437. field_catalog = _field_catalog(snapshot)
  438. system_prompt = topic_table_prompt()
  439. user_prompt = build_topic_table_user_prompt(
  440. snapshot,
  441. field_catalog,
  442. target_query_count=max_queries or 18,
  443. )
  444. llm_output: dict[str, Any] = {}
  445. payload: dict[str, Any] | None = None
  446. validation_error: TopicTableGenerationError | None = None
  447. for attempt in range(2):
  448. current_user_prompt = user_prompt
  449. if attempt and validation_error is not None:
  450. current_user_prompt += (
  451. "\n\n上一次输出没有满足字段覆盖要求。"
  452. f"具体问题:{validation_error}。"
  453. "请重新检查六种数据来源、实质/形式/意图字段和三种账号偏好后,"
  454. "重新输出完整 JSON。"
  455. )
  456. candidate_output = chat_fn(system_prompt, current_user_prompt)
  457. if not isinstance(candidate_output, dict):
  458. validation_error = TopicTableGenerationError("大模型返回格式不正确")
  459. continue
  460. llm_output = candidate_output
  461. try:
  462. payload = _planning_payload(
  463. llm_output,
  464. snapshot=snapshot,
  465. field_catalog=field_catalog,
  466. )
  467. break
  468. except TopicTableGenerationError as exc:
  469. validation_error = exc
  470. if payload is None:
  471. raise validation_error or TopicTableGenerationError("大模型返回格式不正确")
  472. request = GenerationRequest(
  473. generator_kind=GeneratorKind.TOPIC_TABLE,
  474. payload=payload,
  475. name=f"topic-table-{snapshot['topic_build_id']}-{snapshot['topic_id']}",
  476. max_queries=max_queries,
  477. metadata={
  478. "source": "pattern_topic_build_api",
  479. "prompt_version": PROMPT_VERSION,
  480. },
  481. )
  482. result = UnifiedQueryGenerationService().generate(request)
  483. return TopicTablePreview(
  484. result=result,
  485. source={
  486. "topic_build_id": snapshot["topic_build_id"],
  487. "topic_id": snapshot["topic_id"],
  488. "source_url": source_payload.get("source_url"),
  489. },
  490. topic=snapshot["topic"],
  491. llm_output=llm_output,
  492. )
  493. def preview_to_dict(preview: TopicTablePreview) -> dict[str, Any]:
  494. result = preview.result
  495. route_counts: dict[str, int] = {route: 0 for route in ROUTES}
  496. for need in result.knowledge_needs:
  497. route = str(need.metadata.get("route") or "")
  498. if route in route_counts:
  499. route_counts[route] += 1
  500. dimensions: dict[str, list[str]] = {"实质": [], "形式": [], "意图": []}
  501. reference_notes: list[dict[str, str]] = []
  502. for item in preview.topic.get("composition_items") or []:
  503. dimension = item.get("dimension")
  504. name = item.get("element_name")
  505. if dimension in dimensions and name and item.get("is_active"):
  506. dimensions[dimension].append(name)
  507. for source in item.get("sources") or []:
  508. note = str(source.get("reason") or item.get("reason") or "").strip()
  509. if name and note:
  510. reference_notes.append({"element": str(name), "note": note})
  511. point_groups: dict[str, list[str]] = {"灵感点": [], "目的点": [], "关键点": []}
  512. for point in preview.topic.get("points") or []:
  513. point_type = str(point.get("point_type") or "")
  514. point_result = str(point.get("point_result") or "").strip()
  515. if point_type in point_groups and point_result:
  516. point_groups[point_type].append(point_result)
  517. queries_by_need: dict[str, list[str]] = {}
  518. for query in result.selected_queries:
  519. need_keys = query.knowledge_need_keys or [str(query.metadata.get("need_key") or "")]
  520. for need_key in need_keys:
  521. if need_key:
  522. queries_by_need.setdefault(need_key, []).append(query.query_text)
  523. return {
  524. "generator_kind": GeneratorKind.TOPIC_TABLE.value,
  525. "source": preview.source,
  526. "topic": {
  527. "status": preview.topic.get("status"),
  528. "result": preview.topic.get("result"),
  529. "topic_direction": preview.topic.get("topic_direction"),
  530. "dimensions": dimensions,
  531. "points": preview.topic.get("points") or [],
  532. "input_groups": {
  533. "points": point_groups,
  534. "reference_notes": reference_notes[:8],
  535. },
  536. },
  537. "knowledge_needs": [
  538. {
  539. "need_key": need.need_key,
  540. "route": need.metadata.get("route"),
  541. "route_label": need.metadata.get("route_label"),
  542. "decision_context": need.decision_context,
  543. "unknown_information": need.unknown_information,
  544. "priority": need.priority,
  545. "source_ref": need.source_ref,
  546. }
  547. for need in result.knowledge_needs
  548. ],
  549. "field_mappings": [
  550. {
  551. "field_key": need.source_ref.get("field_key"),
  552. "field_path": need.source_ref.get("field_path"),
  553. "field_label": need.source_ref.get("field_label"),
  554. "field_value": need.source_ref.get("field_value"),
  555. "judgement": need.decision_context,
  556. "source_group_key": need.metadata.get("route"),
  557. "source_group_label": need.metadata.get("route_label"),
  558. "queries": queries_by_need.get(need.need_key, []),
  559. }
  560. for need in result.knowledge_needs
  561. if queries_by_need.get(need.need_key)
  562. ],
  563. "source_groups": [
  564. {
  565. "key": route,
  566. "label": config["label"],
  567. "source_description": config["source_description"],
  568. "field_mappings": [
  569. {
  570. "field_key": need.source_ref.get("field_key"),
  571. "field_path": need.source_ref.get("field_path"),
  572. "field_label": need.source_ref.get("field_label"),
  573. "field_value": need.source_ref.get("field_value"),
  574. "judgement": need.decision_context,
  575. "queries": queries_by_need.get(need.need_key, []),
  576. }
  577. for need in result.knowledge_needs
  578. if need.metadata.get("route") == route
  579. and queries_by_need.get(need.need_key)
  580. ],
  581. }
  582. for route, config in ROUTES.items()
  583. ],
  584. "queries": [
  585. {
  586. "query": query.query_text,
  587. "priority": query.priority,
  588. "route": query.metadata.get("route"),
  589. "route_label": query.metadata.get("route_label"),
  590. "need_key": query.metadata.get("need_key"),
  591. "axes": query.axes,
  592. "source_refs": query.source_refs,
  593. }
  594. for query in result.selected_queries
  595. ],
  596. "summary": {
  597. "generated_count": result.stats.generated_count,
  598. "unique_count": result.stats.unique_count,
  599. "selected_count": result.stats.selected_count,
  600. "dropped_count": result.stats.dropped_count,
  601. "route_counts": route_counts,
  602. "persisted": False,
  603. "search_started": False,
  604. },
  605. "prompt": {"name": PROMPT_NAME, "version": PROMPT_VERSION},
  606. }