mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-24 17:21:40 +08:00
feat(pipeline): remove llm section bridges from report generation
Made-with: Cursor
This commit is contained in:
parent
c244a2902e
commit
f994555f9f
@ -33,6 +33,3 @@ CSRF_TRUSTED_ORIGINS=http://localhost:5173,http://127.0.0.1:5173
|
|||||||
# MA_SKIP_LLM_KEYWORD_SUGGEST=1
|
# MA_SKIP_LLM_KEYWORD_SUGGEST=1
|
||||||
# MA_ENABLE_LLM_COMMENT_SENTIMENT=1
|
# MA_ENABLE_LLM_COMMENT_SENTIMENT=1
|
||||||
# MA_SKIP_LLM_COMMENT_SENTIMENT=1
|
# MA_SKIP_LLM_COMMENT_SENTIMENT=1
|
||||||
# 各章标题后插入大模型「衔接分析」(任务 report_config.llm_section_bridges 或本项设为 1)
|
|
||||||
# MA_ENABLE_LLM_SECTION_BRIDGES=1
|
|
||||||
# MA_SKIP_LLM_SECTION_BRIDGES=1
|
|
||||||
|
|||||||
@ -1787,7 +1787,6 @@ def build_competitor_markdown(
|
|||||||
"- **用途/场景**:对每条评价独立判断是否命中预设场景词;一条可计入多个场景,统计的是「提及该场景的评价条数」而非用户数。",
|
"- **用途/场景**:对每条评价独立判断是否命中预设场景词;一条可计入多个场景,统计的是「提及该场景的评价条数」而非用户数。",
|
||||||
"- **用户画像(第八章)**:正负面粗判含**口语短语**级摘录;关注词与场景**仅按细类**以条形图展示(场景图为**占该细类有效文本比例 %**);见 §8.3~8.4。",
|
"- **用户画像(第八章)**:正负面粗判含**口语短语**级摘录;关注词与场景**仅按细类**以条形图展示(场景图为**占该细类有效文本比例 %**);见 §8.3~8.4。",
|
||||||
"- **细类划分(§5~§8)**:**仅**依据合并表 ``detail_category_path``;该列为空或无法解析出可读细类段的 SKU **不参与**竞品矩阵与按细类评价统计(相关评价条亦**不进入**按细类图表)。",
|
"- **细类划分(§5~§8)**:**仅**依据合并表 ``detail_category_path``;该列为空或无法解析出可读细类段的 SKU **不参与**竞品矩阵与按细类评价统计(相关评价条亦**不进入**按细类图表)。",
|
||||||
"- **各章衔接(可选)**:若任务配置 ``llm_section_bridges``(或部署侧环境变量启用),则在「## 一」至「## 九」各章二级标题后插入大模型撰写的**衔接分析**段落,便于阅读过渡;**定量结论仍以正文表格与摘要 JSON 为准**。",
|
|
||||||
"- **检索结果规模**:来自京东 PC 搜索返回的「结果条数」类指标,表示平台侧申报的匹配数量级,**不等于**动销、库存或独立 SKU 数。",
|
"- **检索结果规模**:来自京东 PC 搜索返回的「结果条数」类指标,表示平台侧申报的匹配数量级,**不等于**动销、库存或独立 SKU 数。",
|
||||||
"",
|
"",
|
||||||
"### 1.4 主要局限",
|
"### 1.4 主要局限",
|
||||||
|
|||||||
@ -6,7 +6,6 @@ from __future__ import annotations
|
|||||||
|
|
||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import re
|
|
||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
@ -71,25 +70,6 @@ def merge_llm_report_with_rules_charts(llm_md: str, rules_md: str) -> str:
|
|||||||
return merge_llm_supplement_with_rules_report(llm_md, rules_md)
|
return merge_llm_supplement_with_rules_report(llm_md, rules_md)
|
||||||
|
|
||||||
|
|
||||||
def inject_section_bridges_into_markdown(md: str, bridges: dict[str, str]) -> str:
|
|
||||||
"""
|
|
||||||
在「## 一、」…「## 九、」各章标题行之后插入 ``#### 衔接分析(大模型)`` 段落。
|
|
||||||
自第九章向前替换,避免多次插入导致偏移错位。
|
|
||||||
"""
|
|
||||||
out = md
|
|
||||||
for key in "九八七六五四三二一":
|
|
||||||
content = (bridges.get(key) or "").strip()
|
|
||||||
if not content:
|
|
||||||
continue
|
|
||||||
pat = re.compile(rf"^(## {key}、[^\n]*)\n", re.MULTILINE)
|
|
||||||
|
|
||||||
def _repl(m: re.Match[str], _c: str = content) -> str:
|
|
||||||
return m.group(0) + "\n#### 衔接分析(大模型)\n\n" + _c + "\n\n"
|
|
||||||
|
|
||||||
out = pat.sub(_repl, out, count=1)
|
|
||||||
return out
|
|
||||||
|
|
||||||
|
|
||||||
def _flat_comment_texts(comment_rows: list[dict[str, str]]) -> list[str]:
|
def _flat_comment_texts(comment_rows: list[dict[str, str]]) -> list[str]:
|
||||||
"""全部非空评价正文(与报告统计同源)。"""
|
"""全部非空评价正文(与报告统计同源)。"""
|
||||||
out: list[str] = []
|
out: list[str] = []
|
||||||
@ -169,7 +149,6 @@ def get_default_report_config() -> dict[str, Any]:
|
|||||||
jcr, _ = _jd_crawler_modules()
|
jcr, _ = _jd_crawler_modules()
|
||||||
return {
|
return {
|
||||||
"llm_comment_sentiment": True,
|
"llm_comment_sentiment": True,
|
||||||
"llm_section_bridges": True,
|
|
||||||
"llm_matrix_group_summaries": True,
|
"llm_matrix_group_summaries": True,
|
||||||
"llm_comment_group_summaries": True,
|
"llm_comment_group_summaries": True,
|
||||||
"llm_price_group_summaries": True,
|
"llm_price_group_summaries": True,
|
||||||
@ -528,48 +507,6 @@ def write_competitor_analysis_for_run_dir(
|
|||||||
llm_comment_groups_section_md=llm_comment_gr_md or None,
|
llm_comment_groups_section_md=llm_comment_gr_md or None,
|
||||||
)
|
)
|
||||||
|
|
||||||
bridge_record: dict[str, Any] = {
|
|
||||||
"schema_version": 1,
|
|
||||||
"attempted": False,
|
|
||||||
}
|
|
||||||
skip_bridge = os.environ.get(
|
|
||||||
"MA_SKIP_LLM_SECTION_BRIDGES", ""
|
|
||||||
).strip().lower() in ("1", "true", "yes")
|
|
||||||
env_bridge = os.environ.get(
|
|
||||||
"MA_ENABLE_LLM_SECTION_BRIDGES", ""
|
|
||||||
).strip().lower() in ("1", "true", "yes")
|
|
||||||
want_bridge = bool(eff_rc.get("llm_section_bridges")) or env_bridge
|
|
||||||
if want_bridge and not skip_bridge:
|
|
||||||
from .llm_generate import (
|
|
||||||
generate_section_bridges_llm,
|
|
||||||
split_competitor_report_for_bridges,
|
|
||||||
)
|
|
||||||
|
|
||||||
parts = split_competitor_report_for_bridges(md)
|
|
||||||
if parts:
|
|
||||||
bridge_record["attempted"] = True
|
|
||||||
try:
|
|
||||||
bridges = generate_section_bridges_llm(
|
|
||||||
keyword=kw, brief=brief_final, sections=parts
|
|
||||||
)
|
|
||||||
bridge_record["keys_received"] = sorted(bridges.keys())
|
|
||||||
md = inject_section_bridges_into_markdown(md, bridges)
|
|
||||||
bridge_record["ok"] = True
|
|
||||||
except Exception as e:
|
|
||||||
bridge_record["ok"] = False
|
|
||||||
bridge_record["error"] = str(e)
|
|
||||||
else:
|
|
||||||
bridge_record["skipped"] = "no_h2_sections_matched"
|
|
||||||
elif skip_bridge:
|
|
||||||
bridge_record["skipped"] = "MA_SKIP_LLM_SECTION_BRIDGES"
|
|
||||||
elif not want_bridge:
|
|
||||||
bridge_record["skipped"] = "not_enabled"
|
|
||||||
|
|
||||||
(run_dir / "section_bridge_llm.json").write_text(
|
|
||||||
json.dumps(bridge_record, ensure_ascii=False, indent=2),
|
|
||||||
encoding="utf-8",
|
|
||||||
)
|
|
||||||
|
|
||||||
out_md = run_dir / "competitor_analysis.md"
|
out_md = run_dir / "competitor_analysis.md"
|
||||||
out_md.write_text(md, encoding="utf-8")
|
out_md.write_text(md, encoding="utf-8")
|
||||||
return run_dir
|
return run_dir
|
||||||
|
|||||||
@ -5,7 +5,6 @@
|
|||||||
- §6 后:``generate_price_group_summaries_llm``
|
- §6 后:``generate_price_group_summaries_llm``
|
||||||
- §8.2:``generate_comment_sentiment_analysis_llm``
|
- §8.2:``generate_comment_sentiment_analysis_llm``
|
||||||
- §8末细类评价:``generate_comment_group_summaries_llm``
|
- §8末细类评价:``generate_comment_group_summaries_llm``
|
||||||
- 各章衔接(一~九):``generate_section_bridges_llm``(基于无 LLM 插入的规则稿切分)
|
|
||||||
- §8.5 类全文补充(独立长文):``generate_competitor_report_markdown_llm``
|
- §8.5 类全文补充(独立长文):``generate_competitor_report_markdown_llm``
|
||||||
|
|
||||||
cd backend
|
cd backend
|
||||||
@ -124,7 +123,7 @@ def main() -> None:
|
|||||||
"--only",
|
"--only",
|
||||||
type=str,
|
type=str,
|
||||||
default="",
|
default="",
|
||||||
help="逗号分隔子集:sentiment,matrix,price,comment_groups,bridges,report_supplement",
|
help="逗号分隔子集:sentiment,matrix,price,comment_groups,report_supplement",
|
||||||
)
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--preview-chars",
|
"--preview-chars",
|
||||||
@ -144,7 +143,6 @@ def main() -> None:
|
|||||||
"matrix",
|
"matrix",
|
||||||
"price",
|
"price",
|
||||||
"comment_groups",
|
"comment_groups",
|
||||||
"bridges",
|
|
||||||
"report_supplement",
|
"report_supplement",
|
||||||
}
|
}
|
||||||
if not only:
|
if not only:
|
||||||
@ -166,8 +164,6 @@ def main() -> None:
|
|||||||
generate_competitor_report_markdown_llm,
|
generate_competitor_report_markdown_llm,
|
||||||
generate_matrix_group_summaries_llm,
|
generate_matrix_group_summaries_llm,
|
||||||
generate_price_group_summaries_llm,
|
generate_price_group_summaries_llm,
|
||||||
generate_section_bridges_llm,
|
|
||||||
split_competitor_report_for_bridges,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if "sentiment" in only:
|
if "sentiment" in only:
|
||||||
@ -256,18 +252,8 @@ def main() -> None:
|
|||||||
if not args.live:
|
if not args.live:
|
||||||
print(f" payload groups={len(pl_cg)}", flush=True)
|
print(f" payload groups={len(pl_cg)}", flush=True)
|
||||||
|
|
||||||
brief = jcr.build_competitor_brief(
|
if "report_supplement" in only:
|
||||||
run_dir=run_dir,
|
brief = jcr.build_competitor_brief(
|
||||||
keyword=keyword,
|
|
||||||
merged_rows=merged,
|
|
||||||
search_export_rows=search_rows,
|
|
||||||
comment_rows=comment_rows,
|
|
||||||
meta=meta,
|
|
||||||
report_config=eff_rc,
|
|
||||||
)
|
|
||||||
|
|
||||||
if "bridges" in only:
|
|
||||||
md_base = jcr.build_competitor_markdown(
|
|
||||||
run_dir=run_dir,
|
run_dir=run_dir,
|
||||||
keyword=keyword,
|
keyword=keyword,
|
||||||
merged_rows=merged,
|
merged_rows=merged,
|
||||||
@ -275,29 +261,7 @@ def main() -> None:
|
|||||||
comment_rows=comment_rows,
|
comment_rows=comment_rows,
|
||||||
meta=meta,
|
meta=meta,
|
||||||
report_config=eff_rc,
|
report_config=eff_rc,
|
||||||
llm_sentiment_section_md=None,
|
|
||||||
llm_matrix_section_md=None,
|
|
||||||
llm_price_groups_section_md=None,
|
|
||||||
llm_comment_groups_section_md=None,
|
|
||||||
)
|
)
|
||||||
parts = split_competitor_report_for_bridges(md_base)
|
|
||||||
|
|
||||||
def _br() -> str:
|
|
||||||
m = generate_section_bridges_llm(
|
|
||||||
keyword=keyword, brief=brief, sections=parts
|
|
||||||
)
|
|
||||||
return json.dumps(m, ensure_ascii=False, indent=2)
|
|
||||||
|
|
||||||
_run_one(
|
|
||||||
"各章衔接 bridges(JSON 键一~九)",
|
|
||||||
_br,
|
|
||||||
live=args.live,
|
|
||||||
preview_chars=min(args.preview_chars, 1200),
|
|
||||||
)
|
|
||||||
if not args.live:
|
|
||||||
print(f" sections keys={sorted(parts.keys())}", flush=True)
|
|
||||||
|
|
||||||
if "report_supplement" in only:
|
|
||||||
|
|
||||||
def _rp() -> str:
|
def _rp() -> str:
|
||||||
return generate_competitor_report_markdown_llm(brief, keyword)
|
return generate_competitor_report_markdown_llm(brief, keyword)
|
||||||
|
|||||||
@ -17,7 +17,6 @@ from .models import (
|
|||||||
_REPORT_CONFIG_ALLOWED_KEYS = frozenset(
|
_REPORT_CONFIG_ALLOWED_KEYS = frozenset(
|
||||||
{
|
{
|
||||||
"llm_comment_sentiment",
|
"llm_comment_sentiment",
|
||||||
"llm_section_bridges",
|
|
||||||
"llm_matrix_group_summaries",
|
"llm_matrix_group_summaries",
|
||||||
"llm_price_group_summaries",
|
"llm_price_group_summaries",
|
||||||
"llm_comment_group_summaries",
|
"llm_comment_group_summaries",
|
||||||
@ -31,6 +30,8 @@ _REPORT_CONFIG_ALLOWED_KEYS = frozenset(
|
|||||||
def validate_report_config_body(value: dict) -> dict:
|
def validate_report_config_body(value: dict) -> dict:
|
||||||
if not isinstance(value, dict):
|
if not isinstance(value, dict):
|
||||||
raise serializers.ValidationError("须为 JSON 对象")
|
raise serializers.ValidationError("须为 JSON 对象")
|
||||||
|
value = dict(value)
|
||||||
|
value.pop("llm_section_bridges", None)
|
||||||
extra = set(value.keys()) - _REPORT_CONFIG_ALLOWED_KEYS
|
extra = set(value.keys()) - _REPORT_CONFIG_ALLOWED_KEYS
|
||||||
if extra:
|
if extra:
|
||||||
raise serializers.ValidationError(
|
raise serializers.ValidationError(
|
||||||
@ -39,9 +40,6 @@ def validate_report_config_body(value: dict) -> dict:
|
|||||||
if "llm_comment_sentiment" in value and value["llm_comment_sentiment"] is not None:
|
if "llm_comment_sentiment" in value and value["llm_comment_sentiment"] is not None:
|
||||||
if not isinstance(value["llm_comment_sentiment"], bool):
|
if not isinstance(value["llm_comment_sentiment"], bool):
|
||||||
raise serializers.ValidationError("llm_comment_sentiment 须为 true 或 false")
|
raise serializers.ValidationError("llm_comment_sentiment 须为 true 或 false")
|
||||||
if "llm_section_bridges" in value and value["llm_section_bridges"] is not None:
|
|
||||||
if not isinstance(value["llm_section_bridges"], bool):
|
|
||||||
raise serializers.ValidationError("llm_section_bridges 须为 true 或 false")
|
|
||||||
for k in (
|
for k in (
|
||||||
"llm_matrix_group_summaries",
|
"llm_matrix_group_summaries",
|
||||||
"llm_price_group_summaries",
|
"llm_price_group_summaries",
|
||||||
@ -61,7 +59,6 @@ _ARTIFACT_FILES: tuple[tuple[str, str], ...] = (
|
|||||||
("comments", "comments_flat.csv"),
|
("comments", "comments_flat.csv"),
|
||||||
("detail_ware", "detail_ware_export.csv"),
|
("detail_ware", "detail_ware_export.csv"),
|
||||||
("report", "competitor_analysis.md"),
|
("report", "competitor_analysis.md"),
|
||||||
("section_bridge_llm", "section_bridge_llm.json"),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@ -17,7 +17,6 @@ function splitTriggers(text) {
|
|||||||
*/
|
*/
|
||||||
const REPORT_CONFIG_PASSTHROUGH_BOOL_KEYS = [
|
const REPORT_CONFIG_PASSTHROUGH_BOOL_KEYS = [
|
||||||
'llm_comment_sentiment',
|
'llm_comment_sentiment',
|
||||||
'llm_section_bridges',
|
|
||||||
'llm_matrix_group_summaries',
|
'llm_matrix_group_summaries',
|
||||||
'llm_price_group_summaries',
|
'llm_price_group_summaries',
|
||||||
'llm_comment_group_summaries',
|
'llm_comment_group_summaries',
|
||||||
|
|||||||
@ -278,7 +278,7 @@ watch(
|
|||||||
<h3 class="report-config-title">报告里的评价统计怎么算</h3>
|
<h3 class="report-config-title">报告里的评价统计怎么算</h3>
|
||||||
<p class="hint-top report-config-hint">
|
<p class="hint-top report-config-hint">
|
||||||
关注词、场景词组、外部市场表等<strong>可以不改</strong>:留空并保存即沿用内置规则。大模型相关布尔项(如
|
关注词、场景词组、外部市场表等<strong>可以不改</strong>:留空并保存即沿用内置规则。大模型相关布尔项(如
|
||||||
<code>llm_comment_sentiment</code>、<code>llm_section_bridges</code>)不再单独占勾选框:若任务里已有,会在保存时保留;要改请展开「高级 JSON」。
|
<code>llm_comment_sentiment</code>)不再单独占勾选框:若任务里已有,会在保存时保留;要改请展开「高级 JSON」。
|
||||||
</p>
|
</p>
|
||||||
<div class="report-config-actions">
|
<div class="report-config-actions">
|
||||||
<button
|
<button
|
||||||
@ -315,7 +315,7 @@ watch(
|
|||||||
<summary>高级:用 JSON 编辑(一般不需要)</summary>
|
<summary>高级:用 JSON 编辑(一般不需要)</summary>
|
||||||
<p class="rc-help">
|
<p class="rc-help">
|
||||||
打开时会根据上面表单生成内容;改完后点「写回表单」再保存。可在此加入
|
打开时会根据上面表单生成内容;改完后点「写回表单」再保存。可在此加入
|
||||||
<code>llm_comment_sentiment</code>、<code>llm_section_bridges</code>、<code>llm_matrix_group_summaries</code>
|
<code>llm_comment_sentiment</code>、<code>llm_matrix_group_summaries</code>
|
||||||
等布尔字段(须为 <code>true</code>/<code>false</code>)。页顶「重新生成报告」默认已使用
|
等布尔字段(须为 <code>true</code>/<code>false</code>)。页顶「重新生成报告」默认已使用
|
||||||
<code>generator:"llm"</code>;若只要规则稿请勾选「本次仅用规则引擎」。
|
<code>generator:"llm"</code>;若只要规则稿请勾选「本次仅用规则引擎」。
|
||||||
</p>
|
</p>
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user