mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-22 08:01:34 +08:00
feat(pipeline): add LLM summaries for usage scenarios (§8.4)
Made-with: Cursor
This commit is contained in:
parent
d2135e9a6e
commit
5fe4ac4ffe
@ -869,6 +869,109 @@ def build_comment_groups_llm_payload(
|
||||
return out
|
||||
|
||||
|
||||
def build_scenario_groups_llm_payload(
|
||||
*,
|
||||
feedback_groups: list[tuple[str, list[dict[str, str]], list[str]]],
|
||||
scenario_groups: tuple[tuple[str, tuple[str, ...]], ...],
|
||||
merged_rows: list[dict[str, str]],
|
||||
sku_header: str,
|
||||
title_h: str,
|
||||
) -> dict[str, Any]:
|
||||
"""供 ``generate_scenario_group_summaries_llm``;计数与 §8.4 场景条形图一致。"""
|
||||
if not feedback_groups:
|
||||
return {}
|
||||
sku_meta: dict[str, tuple[str, str, str]] = {}
|
||||
for row in merged_rows:
|
||||
sku = _cell(row, sku_header).strip()
|
||||
if not sku:
|
||||
continue
|
||||
gk = _competitor_matrix_group_key(row)
|
||||
if not gk:
|
||||
continue
|
||||
sku_meta[sku] = (
|
||||
gk,
|
||||
_cell(row, title_h),
|
||||
_cell(row, "detail_shop_name") or _cell(row, "店铺名(shopName)"),
|
||||
)
|
||||
lexicon = [
|
||||
{"label": lbl, "trigger_examples": list(trigs[:12])}
|
||||
for lbl, trigs in scenario_groups
|
||||
]
|
||||
groups_out: list[dict[str, Any]] = []
|
||||
for gname, cr, tu in feedback_groups:
|
||||
if not tu and not cr:
|
||||
continue
|
||||
scen_g, scen_ng = _comment_scenario_counts(tu, scenario_groups)
|
||||
dist: list[dict[str, Any]] = []
|
||||
for lbl, n in scen_g.most_common():
|
||||
if n <= 0:
|
||||
continue
|
||||
dist.append(
|
||||
{
|
||||
"scenario": lbl,
|
||||
"mention_rows": int(n),
|
||||
"share_of_effective_texts": round(
|
||||
float(n) / float(scen_ng), 4
|
||||
)
|
||||
if scen_ng > 0
|
||||
else 0.0,
|
||||
}
|
||||
)
|
||||
snippets: list[str] = []
|
||||
for row in cr:
|
||||
txt = (row.get("tagCommentContent") or "").strip()
|
||||
if not txt:
|
||||
continue
|
||||
if not _text_hits_scenario_triggers(txt, scenario_groups):
|
||||
continue
|
||||
sku = _cell(row, "sku").strip()
|
||||
meta = sku_meta.get(sku)
|
||||
if meta:
|
||||
sg, tit, shop = meta
|
||||
prefix = (
|
||||
f"【细类:{sg}\uff5cSKU:{sku}\uff5c品名:{_md_cell(tit, 60)}\uff5c"
|
||||
f"店铺:{_md_cell(shop, 28)}】"
|
||||
)
|
||||
snippets.append(prefix + txt[:300])
|
||||
else:
|
||||
snippets.append(
|
||||
f"【细类:{gname}\uff5cSKU:{sku or '—'}】" + txt[:320]
|
||||
)
|
||||
if len(snippets) >= 16:
|
||||
break
|
||||
if len(snippets) < 5:
|
||||
for row in cr:
|
||||
txt = (row.get("tagCommentContent") or "").strip()
|
||||
if not txt:
|
||||
continue
|
||||
sku = _cell(row, "sku").strip()
|
||||
meta = sku_meta.get(sku)
|
||||
if meta:
|
||||
sg, tit, shop = meta
|
||||
prefix = (
|
||||
f"【细类:{sg}\uff5cSKU:{sku}\uff5c品名:{_md_cell(tit, 60)}\uff5c"
|
||||
f"店铺:{_md_cell(shop, 28)}】"
|
||||
)
|
||||
snippets.append(prefix + txt[:260])
|
||||
else:
|
||||
snippets.append(
|
||||
f"【细类:{gname}\uff5cSKU:{sku or '—'}】" + txt[:280]
|
||||
)
|
||||
if len(snippets) >= 10:
|
||||
break
|
||||
groups_out.append(
|
||||
{
|
||||
"group": gname,
|
||||
"effective_text_count": int(scen_ng),
|
||||
"scenario_distribution": dist[:18],
|
||||
"sample_text_snippets": snippets,
|
||||
}
|
||||
)
|
||||
if not groups_out:
|
||||
return {}
|
||||
return {"scenario_lexicon": lexicon, "groups": groups_out}
|
||||
|
||||
|
||||
def build_comment_sentiment_llm_payload(
|
||||
texts: list[str],
|
||||
*,
|
||||
@ -974,6 +1077,17 @@ def _comment_scenario_counts(
|
||||
return c, n
|
||||
|
||||
|
||||
def _text_hits_scenario_triggers(
|
||||
text: str,
|
||||
scenario_groups: tuple[tuple[str, tuple[str, ...]], ...],
|
||||
) -> bool:
|
||||
blob = text or ""
|
||||
for _lbl, triggers in scenario_groups:
|
||||
if any(t in blob for t in triggers):
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _scenario_summary_bullets(counter: Counter[str], n_texts: int, top_k: int = 5) -> list[str]:
|
||||
if n_texts <= 0 or not counter:
|
||||
return []
|
||||
@ -1657,6 +1771,7 @@ def build_competitor_markdown(
|
||||
llm_sentiment_section_md: str | None = None,
|
||||
llm_matrix_section_md: str | None = None,
|
||||
llm_price_groups_section_md: str | None = None,
|
||||
llm_scenario_groups_section_md: str | None = None,
|
||||
llm_comment_groups_section_md: str | None = None,
|
||||
) -> str:
|
||||
focus_words, scenario_groups, external_rows = resolve_report_tuning(report_config)
|
||||
@ -2373,14 +2488,28 @@ def build_competitor_markdown(
|
||||
lines.append("*未命中预设场景词组。*")
|
||||
lines.append("")
|
||||
|
||||
_llm_sg = (llm_scenario_groups_section_md or "").strip()
|
||||
if _llm_sg:
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"#### 使用场景要点归纳(大模型,与 §8.4 图表互补)",
|
||||
"",
|
||||
"> **说明**:与 §8.4 **相同**的预设场景词组与子串命中规则;**各场景条数与占比以正文条形图为准**。",
|
||||
"",
|
||||
_llm_sg,
|
||||
"",
|
||||
]
|
||||
)
|
||||
|
||||
_llm_cg = (llm_comment_groups_section_md or "").strip()
|
||||
if _llm_cg:
|
||||
lines.extend(
|
||||
[
|
||||
"",
|
||||
"#### 细类评价与关注词要点归纳(大模型,与 §8.3~8.4 图表互补)",
|
||||
"#### 细类评价与关注词要点归纳(大模型,与 §8.3 图表互补)",
|
||||
"",
|
||||
"> **说明**:归纳各细类反馈主题;**命中次数与条形图以正文为准**。",
|
||||
"> **说明**:归纳各细类反馈主题与配置关注词命中;**次数与 §8.3 条形图以正文为准**。",
|
||||
"",
|
||||
_llm_cg,
|
||||
"",
|
||||
|
||||
@ -151,6 +151,7 @@ def get_default_report_config() -> dict[str, Any]:
|
||||
"llm_comment_sentiment": True,
|
||||
"llm_matrix_group_summaries": True,
|
||||
"llm_comment_group_summaries": True,
|
||||
"llm_scenario_group_summaries": True,
|
||||
"llm_price_group_summaries": True,
|
||||
"comment_focus_words": list(jcr.COMMENT_FOCUS_WORDS),
|
||||
"comment_scenario_groups": [
|
||||
@ -390,9 +391,11 @@ def write_competitor_analysis_for_run_dir(
|
||||
|
||||
llm_matrix_md = ""
|
||||
llm_price_md = ""
|
||||
llm_scenario_gr_md = ""
|
||||
llm_comment_gr_md = ""
|
||||
matrix_llm_rec: dict[str, Any] = {"schema_version": 1, "attempted": False}
|
||||
price_llm_rec: dict[str, Any] = {"schema_version": 1, "attempted": False}
|
||||
scenario_gr_llm_rec: dict[str, Any] = {"schema_version": 1, "attempted": False}
|
||||
comment_gr_llm_rec: dict[str, Any] = {"schema_version": 1, "attempted": False}
|
||||
sku_h = "SKU(skuId)"
|
||||
title_h = "标题(wareName)"
|
||||
@ -402,6 +405,7 @@ def write_competitor_analysis_for_run_dir(
|
||||
|
||||
skip_mx = _env_on("MA_SKIP_LLM_MATRIX_GROUP_SUMMARIES")
|
||||
skip_pr = _env_on("MA_SKIP_LLM_PRICE_GROUP_SUMMARIES")
|
||||
skip_sg = _env_on("MA_SKIP_LLM_SCENARIO_GROUP_SUMMARIES")
|
||||
skip_cg = _env_on("MA_SKIP_LLM_COMMENT_GROUP_SUMMARIES")
|
||||
want_mx = bool(eff_rc.get("llm_matrix_group_summaries")) or _env_on(
|
||||
"MA_ENABLE_LLM_MATRIX_GROUP_SUMMARIES"
|
||||
@ -409,6 +413,9 @@ def write_competitor_analysis_for_run_dir(
|
||||
want_pr = bool(eff_rc.get("llm_price_group_summaries")) or _env_on(
|
||||
"MA_ENABLE_LLM_PRICE_GROUP_SUMMARIES"
|
||||
)
|
||||
want_sg = bool(eff_rc.get("llm_scenario_group_summaries")) or _env_on(
|
||||
"MA_ENABLE_LLM_SCENARIO_GROUP_SUMMARIES"
|
||||
)
|
||||
want_cg = bool(eff_rc.get("llm_comment_group_summaries")) or _env_on(
|
||||
"MA_ENABLE_LLM_COMMENT_GROUP_SUMMARIES"
|
||||
)
|
||||
@ -443,6 +450,40 @@ def write_competitor_analysis_for_run_dir(
|
||||
elif not want_pr:
|
||||
price_llm_rec["skipped"] = "not_enabled"
|
||||
|
||||
if want_sg and not skip_sg and merged_rows:
|
||||
_, scenario_tuple, _ = jcr.resolve_report_tuning(eff_rc)
|
||||
fb_sg = jcr._consumer_feedback_by_matrix_group(
|
||||
merged_rows=merged_rows,
|
||||
comment_rows=comment_rows,
|
||||
sku_header=sku_h,
|
||||
)
|
||||
pl_sg = jcr.build_scenario_groups_llm_payload(
|
||||
feedback_groups=fb_sg,
|
||||
scenario_groups=scenario_tuple,
|
||||
merged_rows=merged_rows,
|
||||
sku_header=sku_h,
|
||||
title_h=title_h,
|
||||
)
|
||||
if pl_sg:
|
||||
scenario_gr_llm_rec["attempted"] = True
|
||||
try:
|
||||
from .llm_generate import generate_scenario_group_summaries_llm
|
||||
|
||||
llm_scenario_gr_md = generate_scenario_group_summaries_llm(
|
||||
pl_sg, keyword=kw
|
||||
)
|
||||
scenario_gr_llm_rec["ok"] = True
|
||||
scenario_gr_llm_rec["chars"] = len(llm_scenario_gr_md)
|
||||
except Exception as e:
|
||||
scenario_gr_llm_rec["ok"] = False
|
||||
scenario_gr_llm_rec["error"] = str(e)
|
||||
else:
|
||||
scenario_gr_llm_rec["skipped"] = "empty_scenario_groups_payload"
|
||||
elif skip_sg:
|
||||
scenario_gr_llm_rec["skipped"] = "MA_SKIP_LLM_SCENARIO_GROUP_SUMMARIES"
|
||||
elif not want_sg:
|
||||
scenario_gr_llm_rec["skipped"] = "not_enabled"
|
||||
|
||||
if want_cg and not skip_cg and merged_rows:
|
||||
fb_cg = jcr._consumer_feedback_by_matrix_group(
|
||||
merged_rows=merged_rows,
|
||||
@ -492,6 +533,10 @@ def write_competitor_analysis_for_run_dir(
|
||||
json.dumps(comment_gr_llm_rec, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(run_dir / "scenario_groups_llm.json").write_text(
|
||||
json.dumps(scenario_gr_llm_rec, ensure_ascii=False, indent=2),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
md = jcr.build_competitor_markdown(
|
||||
run_dir=run_dir,
|
||||
@ -504,6 +549,7 @@ def write_competitor_analysis_for_run_dir(
|
||||
llm_sentiment_section_md=llm_sentiment_md or None,
|
||||
llm_matrix_section_md=llm_matrix_md or None,
|
||||
llm_price_groups_section_md=llm_price_md or None,
|
||||
llm_scenario_groups_section_md=llm_scenario_gr_md or None,
|
||||
llm_comment_groups_section_md=llm_comment_gr_md or None,
|
||||
)
|
||||
|
||||
|
||||
@ -462,6 +462,158 @@ def generate_comment_group_summaries_llm(
|
||||
return _call_llm(COMMENT_GROUPS_SYSTEM, user)
|
||||
|
||||
|
||||
SCENARIO_GROUPS_SYSTEM = """你是用户研究与品类顾问。输入为 JSON:``keyword``、``scenario_lexicon``、``groups``。
|
||||
``scenario_lexicon`` 列出各场景标签及示例触发子串(与报告 **§8.4** 统计规则一致)。
|
||||
``groups`` 每项含 ``group``(与 §5 矩阵一致的细分类目名)、``effective_text_count``(有效评价文本条数)、
|
||||
``scenario_distribution``(各预设场景的 ``mention_rows`` 与 ``share_of_effective_texts``;**一条评价可计入多场景**;与 §8.4 条形图同源)、
|
||||
``sample_text_snippets``(摘录行常含细类、SKU、品名、店铺等前缀的短引文,已截断)。
|
||||
统计为**子串命中**,不是语义主题模型。
|
||||
|
||||
请**为每个细类**输出一小段 Markdown(全部 groups 都要写,顺序与输入一致):
|
||||
- 以 ``#### `` + 与该条 ``group`` 字段**完全一致**的细类名作为小节标题;
|
||||
- 每段约 **100~220 字**:归纳该细类用户**自述的使用场景/用途**结构(哪些场景标签相对突出、多场景叠加是否常见),可点到与其他细类的差异;**所有条数与占比须与 ``scenario_distribution``、``effective_text_count`` 一致**,禁止编造;
|
||||
- 引用原话时须保留或复述摘录中的店铺/SKU/品名信息,勿虚构;
|
||||
- **禁止** Markdown 表格、禁止复述全部摘录;若 ``effective_text_count`` 很小,写明「样本较少,归纳供启发」。
|
||||
|
||||
总输出约 **600~3200 字**。仅输出正文 Markdown,不要用代码围栏包裹全文。"""
|
||||
|
||||
|
||||
SCENARIO_GROUPS_USER_PREFIX = (
|
||||
"请根据以下 JSON 撰写竞品报告 §8.4 用途与使用场景之后的「使用场景要点归纳」正文(Markdown)。\n\n"
|
||||
)
|
||||
|
||||
|
||||
def generate_scenario_group_summaries_llm(
|
||||
payload: dict[str, Any], *, keyword: str
|
||||
) -> str:
|
||||
"""与 ``generate_comment_group_summaries_llm`` 类似:细类多时长 JSON 按档压缩。"""
|
||||
|
||||
def _compact_group(
|
||||
g: dict[str, Any],
|
||||
*,
|
||||
dist_n: int,
|
||||
sn_n: int,
|
||||
sn_max: int,
|
||||
) -> dict[str, Any]:
|
||||
g2: dict[str, Any] = {
|
||||
"group": g.get("group"),
|
||||
"effective_text_count": g.get("effective_text_count"),
|
||||
}
|
||||
dist = g.get("scenario_distribution")
|
||||
if isinstance(dist, list):
|
||||
g2["scenario_distribution"] = []
|
||||
for x in dist[:dist_n]:
|
||||
if not isinstance(x, dict):
|
||||
continue
|
||||
g2["scenario_distribution"].append(
|
||||
{
|
||||
"scenario": x.get("scenario"),
|
||||
"mention_rows": x.get("mention_rows"),
|
||||
"share_of_effective_texts": x.get(
|
||||
"share_of_effective_texts"
|
||||
),
|
||||
}
|
||||
)
|
||||
else:
|
||||
g2["scenario_distribution"] = []
|
||||
sn = g.get("sample_text_snippets")
|
||||
if isinstance(sn, list):
|
||||
g2["sample_text_snippets"] = [
|
||||
str(x)[:sn_max] for x in sn[:sn_n]
|
||||
]
|
||||
else:
|
||||
g2["sample_text_snippets"] = []
|
||||
return g2
|
||||
|
||||
def _compact_lex(raw: Any, *, max_items: int, trig_n: int) -> list[dict[str, Any]]:
|
||||
if not isinstance(raw, list):
|
||||
return []
|
||||
out: list[dict[str, Any]] = []
|
||||
for item in raw[:max_items]:
|
||||
if not isinstance(item, dict):
|
||||
continue
|
||||
tr = item.get("trigger_examples")
|
||||
te = (
|
||||
[str(x)[:48] for x in tr[:trig_n]]
|
||||
if isinstance(tr, list)
|
||||
else []
|
||||
)
|
||||
out.append({"label": item.get("label"), "trigger_examples": te})
|
||||
return out
|
||||
|
||||
groups_in = [g for g in (payload.get("groups") or []) if isinstance(g, dict)]
|
||||
ctx = _llm_context_window_size()
|
||||
budget = ctx - 512 - 256
|
||||
|
||||
def _input_ok(system: str, user_p: str) -> bool:
|
||||
est = _estimated_chat_input_tokens(system, user_p)
|
||||
return est < 15_500
|
||||
|
||||
levels: list[tuple[int, int, int, int, int]] = [
|
||||
(16, 14, 260, 10, 12),
|
||||
(14, 12, 220, 8, 10),
|
||||
(12, 10, 180, 8, 8),
|
||||
(10, 8, 150, 6, 6),
|
||||
(8, 6, 120, 5, 5),
|
||||
(6, 5, 100, 4, 4),
|
||||
(5, 4, 80, 3, 3),
|
||||
]
|
||||
user = ""
|
||||
chosen = levels[-1]
|
||||
for level in levels:
|
||||
chosen = level
|
||||
dist_n, sn_n, sn_max, lex_n, trig_n = level
|
||||
trimmed_g = [
|
||||
_compact_group(g, dist_n=dist_n, sn_n=sn_n, sn_max=sn_max)
|
||||
for g in groups_in
|
||||
]
|
||||
lex_c = _compact_lex(
|
||||
payload.get("scenario_lexicon"),
|
||||
max_items=lex_n,
|
||||
trig_n=trig_n,
|
||||
)
|
||||
body = {
|
||||
"keyword": keyword,
|
||||
"scenario_lexicon": lex_c,
|
||||
"groups": trimmed_g,
|
||||
}
|
||||
raw = json.dumps(body, ensure_ascii=False)
|
||||
if len(raw) > 48_000:
|
||||
raw = raw[:44_000] + "\n…\n"
|
||||
user = SCENARIO_GROUPS_USER_PREFIX + raw
|
||||
if _input_ok(SCENARIO_GROUPS_SYSTEM, user):
|
||||
break
|
||||
else:
|
||||
tail = "\n\n…(JSON 已截断以适配上下文;仅依据可见字段撰写。)\n"
|
||||
dist_n, sn_n, sn_max, lex_n, trig_n = chosen
|
||||
trimmed_g = [
|
||||
_compact_group(g, dist_n=dist_n, sn_n=sn_n, sn_max=sn_max)
|
||||
for g in groups_in
|
||||
]
|
||||
lex_c = _compact_lex(
|
||||
payload.get("scenario_lexicon"),
|
||||
max_items=lex_n,
|
||||
trig_n=trig_n,
|
||||
)
|
||||
raw = json.dumps(
|
||||
{
|
||||
"keyword": keyword,
|
||||
"scenario_lexicon": lex_c,
|
||||
"groups": trimmed_g,
|
||||
},
|
||||
ensure_ascii=False,
|
||||
)
|
||||
room = max(
|
||||
2000,
|
||||
int((budget - 800) / 0.55)
|
||||
- len(SCENARIO_GROUPS_SYSTEM)
|
||||
- len(SCENARIO_GROUPS_USER_PREFIX)
|
||||
- len(tail),
|
||||
)
|
||||
user = SCENARIO_GROUPS_USER_PREFIX + raw[: max(1500, room)] + tail
|
||||
return _call_llm(SCENARIO_GROUPS_SYSTEM, user)
|
||||
|
||||
|
||||
PRICE_GROUPS_SYSTEM = """你是定价与渠道顾问。输入为 JSON:``keyword`` 与 ``groups``。
|
||||
每个 group 含 ``group``(细分类目名,与 §5 矩阵、§6「按细类价盘」小节一致)、``sku_count``、``price_stats``(该细类可解析展示价的 min/max/median/mean/n,与 §6 各细类 Markdown 分位数表同源)、
|
||||
``listing_snippets``(若干「标题|标价|券后|详情价」摘录,来自合并表字段,已截断)。
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
- §6 后:``generate_price_group_summaries_llm``
|
||||
- §8.2:``generate_comment_sentiment_analysis_llm``
|
||||
- §8末细类评价:``generate_comment_group_summaries_llm``
|
||||
- §8.4 后使用场景:``generate_scenario_group_summaries_llm``
|
||||
- §8.5 类全文补充(独立长文):``generate_competitor_report_markdown_llm``
|
||||
|
||||
cd backend
|
||||
@ -123,7 +124,7 @@ def main() -> None:
|
||||
"--only",
|
||||
type=str,
|
||||
default="",
|
||||
help="逗号分隔子集:sentiment,matrix,price,comment_groups,report_supplement",
|
||||
help="逗号分隔子集:sentiment,matrix,price,scenario_groups,comment_groups,report_supplement",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--preview-chars",
|
||||
@ -142,6 +143,7 @@ def main() -> None:
|
||||
"sentiment",
|
||||
"matrix",
|
||||
"price",
|
||||
"scenario_groups",
|
||||
"comment_groups",
|
||||
"report_supplement",
|
||||
}
|
||||
@ -164,6 +166,7 @@ def main() -> None:
|
||||
generate_competitor_report_markdown_llm,
|
||||
generate_matrix_group_summaries_llm,
|
||||
generate_price_group_summaries_llm,
|
||||
generate_scenario_group_summaries_llm,
|
||||
)
|
||||
|
||||
if "sentiment" in only:
|
||||
@ -222,6 +225,34 @@ def main() -> None:
|
||||
if not args.live:
|
||||
print(f" payload groups={len(pl_pr)}", flush=True)
|
||||
|
||||
if "scenario_groups" in only:
|
||||
_, scen_tuple, _ = jcr.resolve_report_tuning(eff_rc)
|
||||
fb_s = jcr._consumer_feedback_by_matrix_group(
|
||||
merged_rows=merged,
|
||||
comment_rows=comment_rows,
|
||||
sku_header=sku_h,
|
||||
)
|
||||
pl_sg = jcr.build_scenario_groups_llm_payload(
|
||||
feedback_groups=fb_s,
|
||||
scenario_groups=scen_tuple,
|
||||
merged_rows=merged,
|
||||
sku_header=sku_h,
|
||||
title_h=title_h,
|
||||
)
|
||||
|
||||
def _sg() -> str:
|
||||
return generate_scenario_group_summaries_llm(pl_sg, keyword=keyword)
|
||||
|
||||
_run_one(
|
||||
"§8.4 使用场景归纳(scenario_groups)",
|
||||
_sg,
|
||||
live=args.live,
|
||||
preview_chars=args.preview_chars,
|
||||
)
|
||||
if not args.live:
|
||||
n = len((pl_sg or {}).get("groups") or [])
|
||||
print(f" payload groups={n}", flush=True)
|
||||
|
||||
if "comment_groups" in only:
|
||||
fb = jcr._consumer_feedback_by_matrix_group(
|
||||
merged_rows=merged,
|
||||
|
||||
@ -20,6 +20,7 @@ _REPORT_CONFIG_ALLOWED_KEYS = frozenset(
|
||||
"llm_matrix_group_summaries",
|
||||
"llm_price_group_summaries",
|
||||
"llm_comment_group_summaries",
|
||||
"llm_scenario_group_summaries",
|
||||
"comment_focus_words",
|
||||
"comment_scenario_groups",
|
||||
"external_market_table_rows",
|
||||
@ -44,6 +45,7 @@ def validate_report_config_body(value: dict) -> dict:
|
||||
"llm_matrix_group_summaries",
|
||||
"llm_price_group_summaries",
|
||||
"llm_comment_group_summaries",
|
||||
"llm_scenario_group_summaries",
|
||||
):
|
||||
if k in value and value[k] is not None and not isinstance(value[k], bool):
|
||||
raise serializers.ValidationError(f"{k} 须为 true 或 false")
|
||||
|
||||
@ -20,6 +20,7 @@ const REPORT_CONFIG_PASSTHROUGH_BOOL_KEYS = [
|
||||
'llm_matrix_group_summaries',
|
||||
'llm_price_group_summaries',
|
||||
'llm_comment_group_summaries',
|
||||
'llm_scenario_group_summaries',
|
||||
]
|
||||
|
||||
/**
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user