|
@@ -13,7 +13,7 @@ from query_planning.service import UnifiedQueryGenerationService
|
|
|
|
|
|
|
|
|
|
|
|
|
PROMPT_NAME = "topic_table_query_generation"
|
|
PROMPT_NAME = "topic_table_query_generation"
|
|
|
-PROMPT_VERSION = "topic_table_query_v2"
|
|
|
|
|
|
|
+PROMPT_VERSION = "topic_table_query_v3"
|
|
|
|
|
|
|
|
ROUTES: dict[str, dict[str, Any]] = {
|
|
ROUTES: dict[str, dict[str, Any]] = {
|
|
|
"unique_hook": {"label": "灵感点和特殊组合", "priority": 100},
|
|
"unique_hook": {"label": "灵感点和特殊组合", "priority": 100},
|
|
@@ -118,20 +118,143 @@ def _compact_snapshot(
|
|
|
|
|
|
|
|
def build_topic_table_user_prompt(
|
|
def build_topic_table_user_prompt(
|
|
|
snapshot: dict[str, Any],
|
|
snapshot: dict[str, Any],
|
|
|
|
|
+ field_catalog: list[dict[str, Any]],
|
|
|
*,
|
|
*,
|
|
|
target_query_count: int,
|
|
target_query_count: int,
|
|
|
) -> str:
|
|
) -> str:
|
|
|
return (
|
|
return (
|
|
|
- "请根据下面的真实选题表快照,从六类信息中各判断一个需要搜索的问题。"
|
|
|
|
|
- "knowledge_needs 数组必须正好有 6 项,每类信息正好 1 项,不能把搜索词数量误当成问题数量。"
|
|
|
|
|
- f"六个问题合计生成约 {target_query_count} 条具体搜索词;数量允许时,每个问题给 2~3 条。"
|
|
|
|
|
- "decision_context 必须是一句不超过 45 个汉字的大白话,只说大模型要判断什么,不要先解释背景。"
|
|
|
|
|
- "只输出待搜索的问题,不要在 decision_context 或 unknown_information 中提前编造答案、参数、比例或效果数据。"
|
|
|
|
|
- "不要启动搜索。\n\n"
|
|
|
|
|
- + json.dumps(snapshot, ensure_ascii=False, indent=2, default=str)
|
|
|
|
|
|
|
+ "请从 field_catalog 中选择真正能产生创作方法搜索词的具体字段。"
|
|
|
|
|
+ "每一项 field_mapping 只能引用一个真实 field_key;不能合并多个字段,也不能自己编字段。"
|
|
|
|
|
+ f"所有字段合计生成约 {target_query_count} 条具体搜索词,每个字段生成 1~3 条。"
|
|
|
|
|
+ "judgement 必须是一句不超过 45 个汉字的大白话,只说明大模型针对该字段要判断什么。"
|
|
|
|
|
+ "不要提前编造答案、参数、比例或效果数据,不要启动搜索。\n\n"
|
|
|
|
|
+ + json.dumps(
|
|
|
|
|
+ {
|
|
|
|
|
+ "topic_build_id": snapshot["topic_build_id"],
|
|
|
|
|
+ "topic_id": snapshot["topic_id"],
|
|
|
|
|
+ "field_catalog": field_catalog,
|
|
|
|
|
+ },
|
|
|
|
|
+ ensure_ascii=False,
|
|
|
|
|
+ indent=2,
|
|
|
|
|
+ default=str,
|
|
|
|
|
+ )
|
|
|
)
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
+def _field_catalog(snapshot: dict[str, Any]) -> list[dict[str, Any]]:
|
|
|
|
|
+ topic = snapshot["topic"]
|
|
|
|
|
+ topic_id = snapshot["topic_id"]
|
|
|
|
|
+ fields: list[dict[str, Any]] = []
|
|
|
|
|
+
|
|
|
|
|
+ def add(
|
|
|
|
|
+ *,
|
|
|
|
|
+ field_key: str,
|
|
|
|
|
+ field_path: str,
|
|
|
|
|
+ field_label: str,
|
|
|
|
|
+ field_value: Any,
|
|
|
|
|
+ route: str,
|
|
|
|
|
+ point_id: Any = None,
|
|
|
|
|
+ item_ids: list[Any] | None = None,
|
|
|
|
|
+ ) -> None:
|
|
|
|
|
+ value = str(field_value or "").strip()
|
|
|
|
|
+ if not value:
|
|
|
|
|
+ return
|
|
|
|
|
+ fields.append(
|
|
|
|
|
+ {
|
|
|
|
|
+ "field_key": field_key,
|
|
|
|
|
+ "field_path": field_path,
|
|
|
|
|
+ "field_label": field_label,
|
|
|
|
|
+ "field_value": value,
|
|
|
|
|
+ "route": route,
|
|
|
|
|
+ "point_id": point_id,
|
|
|
|
|
+ "item_ids": list(item_ids or []),
|
|
|
|
|
+ }
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ add(
|
|
|
|
|
+ field_key="topic.result",
|
|
|
|
|
+ field_path=f"topics[id={topic_id}].result",
|
|
|
|
|
+ field_label="最终选题描述",
|
|
|
|
|
+ field_value=topic.get("result"),
|
|
|
|
|
+ route="cross_dimension",
|
|
|
|
|
+ )
|
|
|
|
|
+ direction = topic.get("topic_direction") or {}
|
|
|
|
|
+ for preference, label in (
|
|
|
|
|
+ ("实质偏好", "账号实质偏好"),
|
|
|
|
|
+ ("形式偏好", "账号形式偏好"),
|
|
|
|
|
+ ("意图偏好", "账号意图偏好"),
|
|
|
|
|
+ ):
|
|
|
|
|
+ add(
|
|
|
|
|
+ field_key=f"topic_direction.persona_dimensions.{preference}",
|
|
|
|
|
+ field_path=f"topics[id={topic_id}].topic_direction.persona_dimensions.{preference}",
|
|
|
|
|
+ field_label=label,
|
|
|
|
|
+ field_value=(direction.get("persona_dimensions") or {}).get(preference),
|
|
|
|
|
+ route="account_fit",
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ point_routes = {
|
|
|
|
|
+ "灵感点": "unique_hook",
|
|
|
|
|
+ "目的点": "goal_execution",
|
|
|
|
|
+ "关键点": "key_execution",
|
|
|
|
|
+ }
|
|
|
|
|
+ for point in topic.get("points") or []:
|
|
|
|
|
+ point_id = point.get("id")
|
|
|
|
|
+ point_type = str(point.get("point_type") or "选题点")
|
|
|
|
|
+ add(
|
|
|
|
|
+ field_key=f"point:{point_id}:point_result",
|
|
|
|
|
+ field_path=f"topics[id={topic_id}].points[id={point_id}].point_result",
|
|
|
|
|
+ field_label=f"{point_type}字段",
|
|
|
|
|
+ field_value=point.get("point_result"),
|
|
|
|
|
+ route=point_routes.get(point_type, "cross_dimension"),
|
|
|
|
|
+ point_id=point_id,
|
|
|
|
|
+ item_ids=list(point.get("item_ids") or []),
|
|
|
|
|
+ )
|
|
|
|
|
+
|
|
|
|
|
+ for item in topic.get("composition_items") or []:
|
|
|
|
|
+ item_id = item.get("id")
|
|
|
|
|
+ dimension = str(item.get("dimension") or "元素")
|
|
|
|
|
+ point_type = str(item.get("point_type") or "")
|
|
|
|
|
+ add(
|
|
|
|
|
+ field_key=f"item:{item_id}:element_name",
|
|
|
|
|
+ field_path=(
|
|
|
|
|
+ f"topics[id={topic_id}].composition_items[id={item_id}].element_name"
|
|
|
|
|
+ ),
|
|
|
|
|
+ field_label=f"{dimension}字段",
|
|
|
|
|
+ field_value=item.get("element_name"),
|
|
|
|
|
+ route=point_routes.get(point_type, "cross_dimension"),
|
|
|
|
|
+ item_ids=[item_id],
|
|
|
|
|
+ )
|
|
|
|
|
+ for source_index, source in enumerate(item.get("sources") or []):
|
|
|
|
|
+ add(
|
|
|
|
|
+ field_key=f"item:{item_id}:source:{source_index}:reason",
|
|
|
|
|
+ field_path=(
|
|
|
|
|
+ f"topics[id={topic_id}].composition_items[id={item_id}]"
|
|
|
|
|
+ f".sources[{source_index}].reason"
|
|
|
|
|
+ ),
|
|
|
|
|
+ field_label="参考来源说明",
|
|
|
|
|
+ field_value=source.get("reason"),
|
|
|
|
|
+ route="evidence_gap",
|
|
|
|
|
+ item_ids=[item_id],
|
|
|
|
|
+ )
|
|
|
|
|
+ for relation_index, relation in enumerate(topic.get("item_relations") or []):
|
|
|
|
|
+ add(
|
|
|
|
|
+ field_key=f"relation:{relation_index}:reason",
|
|
|
|
|
+ field_path=f"topics[id={topic_id}].item_relations[{relation_index}].reason",
|
|
|
|
|
+ field_label="元素关系说明",
|
|
|
|
|
+ field_value=relation.get("reason"),
|
|
|
|
|
+ route="cross_dimension",
|
|
|
|
|
+ item_ids=[
|
|
|
|
|
+ value
|
|
|
|
|
+ for value in (
|
|
|
|
|
+ relation.get("source_item_id"),
|
|
|
|
|
+ relation.get("target_item_id"),
|
|
|
|
|
+ )
|
|
|
|
|
+ if value is not None
|
|
|
|
|
+ ],
|
|
|
|
|
+ )
|
|
|
|
|
+ return fields
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
def _safe_need_key(value: Any, route: str, index: int) -> str:
|
|
def _safe_need_key(value: Any, route: str, index: int) -> str:
|
|
|
raw = re.sub(r"[^a-zA-Z0-9_-]+", "_", str(value or "").strip()).strip("_")
|
|
raw = re.sub(r"[^a-zA-Z0-9_-]+", "_", str(value or "").strip()).strip("_")
|
|
|
return raw[:80] or f"{route}_{index + 1}"
|
|
return raw[:80] or f"{route}_{index + 1}"
|
|
@@ -141,50 +264,58 @@ def _planning_payload(
|
|
|
llm_output: dict[str, Any],
|
|
llm_output: dict[str, Any],
|
|
|
*,
|
|
*,
|
|
|
snapshot: dict[str, Any],
|
|
snapshot: dict[str, Any],
|
|
|
|
|
+ field_catalog: list[dict[str, Any]],
|
|
|
) -> dict[str, Any]:
|
|
) -> dict[str, Any]:
|
|
|
- raw_needs = llm_output.get("knowledge_needs")
|
|
|
|
|
- if not isinstance(raw_needs, list) or not raw_needs:
|
|
|
|
|
- raise TopicTableGenerationError("大模型没有返回六类搜索问题")
|
|
|
|
|
|
|
+ raw_mappings = llm_output.get("field_mappings")
|
|
|
|
|
+ if not isinstance(raw_mappings, list) or not raw_mappings:
|
|
|
|
|
+ raise TopicTableGenerationError("大模型没有返回字段与搜索词的对应关系")
|
|
|
topic_build_id = snapshot["topic_build_id"]
|
|
topic_build_id = snapshot["topic_build_id"]
|
|
|
topic_id = snapshot["topic_id"]
|
|
topic_id = snapshot["topic_id"]
|
|
|
|
|
+ catalog_by_key = {field["field_key"]: field for field in field_catalog}
|
|
|
needs: list[dict[str, Any]] = []
|
|
needs: list[dict[str, Any]] = []
|
|
|
candidates: list[dict[str, Any]] = []
|
|
candidates: list[dict[str, Any]] = []
|
|
|
- used_keys: set[str] = set()
|
|
|
|
|
- route_counts: dict[str, int] = {route: 0 for route in ROUTES}
|
|
|
|
|
|
|
+ used_field_keys: set[str] = set()
|
|
|
position = 0
|
|
position = 0
|
|
|
- for index, raw in enumerate(raw_needs):
|
|
|
|
|
|
|
+ for index, raw in enumerate(raw_mappings):
|
|
|
if not isinstance(raw, dict):
|
|
if not isinstance(raw, dict):
|
|
|
continue
|
|
continue
|
|
|
- route = str(raw.get("route") or "").strip()
|
|
|
|
|
- if route not in ROUTES:
|
|
|
|
|
|
|
+ field_key = str(raw.get("field_key") or "").strip()
|
|
|
|
|
+ field = catalog_by_key.get(field_key)
|
|
|
|
|
+ if field is None:
|
|
|
|
|
+ raise TopicTableGenerationError(f"大模型引用了不存在的选题表字段:{field_key}")
|
|
|
|
|
+ if field_key in used_field_keys:
|
|
|
|
|
+ raise TopicTableGenerationError(f"大模型重复引用了同一个选题表字段:{field_key}")
|
|
|
|
|
+ used_field_keys.add(field_key)
|
|
|
|
|
+ judgement = str(raw.get("judgement") or "").strip()
|
|
|
|
|
+ if not judgement:
|
|
|
continue
|
|
continue
|
|
|
- route_counts[route] += 1
|
|
|
|
|
- need_key = _safe_need_key(raw.get("need_key"), route, index)
|
|
|
|
|
- if need_key in used_keys:
|
|
|
|
|
- need_key = f"{need_key}_{index + 1}"
|
|
|
|
|
- used_keys.add(need_key)
|
|
|
|
|
- decision_context = str(raw.get("decision_context") or "").strip()
|
|
|
|
|
- unknown_information = str(raw.get("unknown_information") or "").strip()
|
|
|
|
|
- if not decision_context or not unknown_information:
|
|
|
|
|
- continue
|
|
|
|
|
- priority = max(0, min(100, int(raw.get("priority") or ROUTES[route]["priority"])))
|
|
|
|
|
- raw_ref = raw.get("source_ref") if isinstance(raw.get("source_ref"), dict) else {}
|
|
|
|
|
|
|
+ route = field["route"]
|
|
|
|
|
+ priority = ROUTES[route]["priority"]
|
|
|
|
|
+ need_key = _safe_need_key(field_key, route, index)
|
|
|
source_ref = {
|
|
source_ref = {
|
|
|
"generator_kind": GeneratorKind.TOPIC_TABLE.value,
|
|
"generator_kind": GeneratorKind.TOPIC_TABLE.value,
|
|
|
"topic_build_id": topic_build_id,
|
|
"topic_build_id": topic_build_id,
|
|
|
"topic_id": topic_id,
|
|
"topic_id": topic_id,
|
|
|
"route": route,
|
|
"route": route,
|
|
|
- "point_id": raw_ref.get("point_id"),
|
|
|
|
|
- "item_ids": list(raw_ref.get("item_ids") or []),
|
|
|
|
|
|
|
+ "field_key": field["field_key"],
|
|
|
|
|
+ "field_path": field["field_path"],
|
|
|
|
|
+ "field_label": field["field_label"],
|
|
|
|
|
+ "field_value": field["field_value"],
|
|
|
|
|
+ "point_id": field.get("point_id"),
|
|
|
|
|
+ "item_ids": list(field.get("item_ids") or []),
|
|
|
}
|
|
}
|
|
|
needs.append(
|
|
needs.append(
|
|
|
{
|
|
{
|
|
|
"need_key": need_key,
|
|
"need_key": need_key,
|
|
|
- "decision_context": decision_context,
|
|
|
|
|
- "unknown_information": unknown_information,
|
|
|
|
|
|
|
+ "decision_context": judgement,
|
|
|
|
|
+ "unknown_information": judgement,
|
|
|
"source_ref": source_ref,
|
|
"source_ref": source_ref,
|
|
|
"priority": priority,
|
|
"priority": priority,
|
|
|
- "metadata": {"route": route, "route_label": ROUTES[route]["label"]},
|
|
|
|
|
|
|
+ "metadata": {
|
|
|
|
|
+ "route": route,
|
|
|
|
|
+ "route_label": ROUTES[route]["label"],
|
|
|
|
|
+ "source_field": field,
|
|
|
|
|
+ },
|
|
|
}
|
|
}
|
|
|
)
|
|
)
|
|
|
raw_queries = raw.get("queries") or []
|
|
raw_queries = raw.get("queries") or []
|
|
@@ -198,9 +329,9 @@ def _planning_payload(
|
|
|
{
|
|
{
|
|
|
"query_text": query_text,
|
|
"query_text": query_text,
|
|
|
"axes": {
|
|
"axes": {
|
|
|
- "需求路径": ROUTES[route]["label"],
|
|
|
|
|
- "创作决定": decision_context,
|
|
|
|
|
- "未知信息": unknown_information,
|
|
|
|
|
|
|
+ "选题表字段": field["field_path"],
|
|
|
|
|
+ "字段值": field["field_value"],
|
|
|
|
|
+ "大模型判断": judgement,
|
|
|
},
|
|
},
|
|
|
"priority": priority,
|
|
"priority": priority,
|
|
|
"original_position": position,
|
|
"original_position": position,
|
|
@@ -215,17 +346,8 @@ def _planning_payload(
|
|
|
}
|
|
}
|
|
|
)
|
|
)
|
|
|
position += 1
|
|
position += 1
|
|
|
- invalid_routes = [
|
|
|
|
|
- ROUTES[route]["label"]
|
|
|
|
|
- for route, count in route_counts.items()
|
|
|
|
|
- if count != 1
|
|
|
|
|
- ]
|
|
|
|
|
- if invalid_routes:
|
|
|
|
|
- raise TopicTableGenerationError(
|
|
|
|
|
- "大模型返回的六类问题不完整或有重复:" + "、".join(invalid_routes)
|
|
|
|
|
- )
|
|
|
|
|
if not needs or not candidates:
|
|
if not needs or not candidates:
|
|
|
- raise TopicTableGenerationError("大模型返回内容无法形成搜索问题和搜索词")
|
|
|
|
|
|
|
+ raise TopicTableGenerationError("大模型返回内容无法形成字段与搜索词的对应关系")
|
|
|
return {
|
|
return {
|
|
|
"input_snapshot": snapshot,
|
|
"input_snapshot": snapshot,
|
|
|
"knowledge_needs": needs,
|
|
"knowledge_needs": needs,
|
|
@@ -233,7 +355,7 @@ def _planning_payload(
|
|
|
"generator_config": {
|
|
"generator_config": {
|
|
|
"prompt_name": PROMPT_NAME,
|
|
"prompt_name": PROMPT_NAME,
|
|
|
"prompt_version": PROMPT_VERSION,
|
|
"prompt_version": PROMPT_VERSION,
|
|
|
- "routes": list(ROUTES),
|
|
|
|
|
|
|
+ "field_count": len(field_catalog),
|
|
|
},
|
|
},
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -247,16 +369,22 @@ def generate_topic_table_preview(
|
|
|
) -> TopicTablePreview:
|
|
) -> TopicTablePreview:
|
|
|
topic = _select_topic(source_payload, topic_id)
|
|
topic = _select_topic(source_payload, topic_id)
|
|
|
snapshot = _compact_snapshot(source_payload, topic)
|
|
snapshot = _compact_snapshot(source_payload, topic)
|
|
|
|
|
+ field_catalog = _field_catalog(snapshot)
|
|
|
llm_output = chat_fn(
|
|
llm_output = chat_fn(
|
|
|
topic_table_prompt(),
|
|
topic_table_prompt(),
|
|
|
build_topic_table_user_prompt(
|
|
build_topic_table_user_prompt(
|
|
|
snapshot,
|
|
snapshot,
|
|
|
|
|
+ field_catalog,
|
|
|
target_query_count=max_queries or 18,
|
|
target_query_count=max_queries or 18,
|
|
|
),
|
|
),
|
|
|
)
|
|
)
|
|
|
if not isinstance(llm_output, dict):
|
|
if not isinstance(llm_output, dict):
|
|
|
raise TopicTableGenerationError("大模型返回格式不正确")
|
|
raise TopicTableGenerationError("大模型返回格式不正确")
|
|
|
- payload = _planning_payload(llm_output, snapshot=snapshot)
|
|
|
|
|
|
|
+ payload = _planning_payload(
|
|
|
|
|
+ llm_output,
|
|
|
|
|
+ snapshot=snapshot,
|
|
|
|
|
+ field_catalog=field_catalog,
|
|
|
|
|
+ )
|
|
|
request = GenerationRequest(
|
|
request = GenerationRequest(
|
|
|
generator_kind=GeneratorKind.TOPIC_TABLE,
|
|
generator_kind=GeneratorKind.TOPIC_TABLE,
|
|
|
payload=payload,
|
|
payload=payload,
|
|
@@ -304,6 +432,12 @@ def preview_to_dict(preview: TopicTablePreview) -> dict[str, Any]:
|
|
|
point_result = str(point.get("point_result") or "").strip()
|
|
point_result = str(point.get("point_result") or "").strip()
|
|
|
if point_type in point_groups and point_result:
|
|
if point_type in point_groups and point_result:
|
|
|
point_groups[point_type].append(point_result)
|
|
point_groups[point_type].append(point_result)
|
|
|
|
|
+ queries_by_need: dict[str, list[str]] = {}
|
|
|
|
|
+ for query in result.selected_queries:
|
|
|
|
|
+ need_keys = query.knowledge_need_keys or [str(query.metadata.get("need_key") or "")]
|
|
|
|
|
+ for need_key in need_keys:
|
|
|
|
|
+ if need_key:
|
|
|
|
|
+ queries_by_need.setdefault(need_key, []).append(query.query_text)
|
|
|
return {
|
|
return {
|
|
|
"generator_kind": GeneratorKind.TOPIC_TABLE.value,
|
|
"generator_kind": GeneratorKind.TOPIC_TABLE.value,
|
|
|
"source": preview.source,
|
|
"source": preview.source,
|
|
@@ -330,6 +464,18 @@ def preview_to_dict(preview: TopicTablePreview) -> dict[str, Any]:
|
|
|
}
|
|
}
|
|
|
for need in result.knowledge_needs
|
|
for need in result.knowledge_needs
|
|
|
],
|
|
],
|
|
|
|
|
+ "field_mappings": [
|
|
|
|
|
+ {
|
|
|
|
|
+ "field_key": need.source_ref.get("field_key"),
|
|
|
|
|
+ "field_path": need.source_ref.get("field_path"),
|
|
|
|
|
+ "field_label": need.source_ref.get("field_label"),
|
|
|
|
|
+ "field_value": need.source_ref.get("field_value"),
|
|
|
|
|
+ "judgement": need.decision_context,
|
|
|
|
|
+ "queries": queries_by_need.get(need.need_key, []),
|
|
|
|
|
+ }
|
|
|
|
|
+ for need in result.knowledge_needs
|
|
|
|
|
+ if queries_by_need.get(need.need_key)
|
|
|
|
|
+ ],
|
|
|
"queries": [
|
|
"queries": [
|
|
|
{
|
|
{
|
|
|
"query": query.query_text,
|
|
"query": query.query_text,
|