mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-21 23:41:39 +08:00
fix(pipeline): 策略节选纳入 8.3 情感归纳嵌套四级标题
load_report_matrix_group_evidence 先剔除 8.3 再按 #### 细类抽取,避免嵌套 #### 正向体验主题等导致正文为空;单独解析 8.3 并拼入摘录。补充单测。 Made-with: Cursor
This commit is contained in:
parent
991dec3c4b
commit
6c37a2def2
@ -4,8 +4,10 @@
|
|||||||
|
|
||||||
报告生成侧约定:矩阵/价盘/促销/评论/场景等 LLM 小节均以 ``#### `` + 与矩阵一致的细类名为小节标题
|
报告生成侧约定:矩阵/价盘/促销/评论/场景等 LLM 小节均以 ``#### `` + 与矩阵一致的细类名为小节标题
|
||||||
(见 ``generate_group_summaries`` 系统提示)。
|
(见 ``generate_group_summaries`` 系统提示)。
|
||||||
**8.3 评价正/负向主题**:细类下内层小节使用 ``#####``(由 ``demote_sentiment_inner_h4_to_h5_for_matrix_group`` 处理),
|
|
||||||
避免与外层 ``#### 细类名`` 同级导致本节正文抽取为空。
|
**第八章 8.3**(``generate_comment_sentiment_analysis_llm``)在每组 ``#### {细类}`` 下还会再嵌套
|
||||||
|
``#### 正向体验主题`` 等四级标题(见 ``generate_sections.SENTIMENT_LLM_SYSTEM``),
|
||||||
|
通用抽取在遇到下一行 ``####`` 时即结束,会把 8.3 正文误判为空;故 8.3 单独解析后再拼回。
|
||||||
"""
|
"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
@ -13,6 +15,77 @@ import re
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Literal
|
from typing import Literal
|
||||||
|
|
||||||
|
# 与 ``jd_report.build_competitor_markdown`` 中 8.3 小节标题一致。
|
||||||
|
_SENTIMENT_83_HEADING = "### 8.3 评价正/负向主题(按细类 · 大模型)"
|
||||||
|
|
||||||
|
# 与 ``generate_sections.SENTIMENT_LLM_SYSTEM``「建议结构」四级标题一致;嵌套于 8.3 每组 ``#### 细类`` 之下。
|
||||||
|
_SENTIMENT_LLM_INNER_LEVEL4 = frozenset(
|
||||||
|
{
|
||||||
|
"正向体验主题",
|
||||||
|
"负向评价主题归因",
|
||||||
|
"混合评价中的典型张力",
|
||||||
|
"使用注意",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _md_splice_out_sentiment_83_section(md: str) -> str:
|
||||||
|
"""去掉 8.3 整节,避免按细类抽取时把嵌套 ``####`` 误判为同级边界;其它章不变。"""
|
||||||
|
i = md.find(_SENTIMENT_83_HEADING)
|
||||||
|
if i < 0:
|
||||||
|
return md
|
||||||
|
tail = md[i + len(_SENTIMENT_83_HEADING) :]
|
||||||
|
m = re.search(r"^##\s+", tail, re.MULTILINE)
|
||||||
|
if not m:
|
||||||
|
return md[:i].rstrip() + "\n\n"
|
||||||
|
cut = i + len(_SENTIMENT_83_HEADING) + m.start()
|
||||||
|
return md[:i].rstrip() + "\n\n" + md[cut:].lstrip("\n")
|
||||||
|
|
||||||
|
|
||||||
|
def extract_sentiment_83_level4_body(md: str, group_title: str) -> str:
|
||||||
|
"""
|
||||||
|
仅在 ``### 8.3 …`` 节内,抽取 ``#### {group_title}`` 下正文(**不含**该标题行)。
|
||||||
|
|
||||||
|
允许正文内出现 ``#### 正向体验主题`` 等情感归纳子标题;遇下一 peer ``####``(另一细类)或 ``###``/``##`` 则结束。
|
||||||
|
"""
|
||||||
|
title = (group_title or "").strip()
|
||||||
|
if not title or not (md or "").strip():
|
||||||
|
return ""
|
||||||
|
|
||||||
|
i = md.find(_SENTIMENT_83_HEADING)
|
||||||
|
if i < 0:
|
||||||
|
return ""
|
||||||
|
tail = md[i + len(_SENTIMENT_83_HEADING) :]
|
||||||
|
m_end = re.search(r"^##\s+", tail, re.MULTILINE)
|
||||||
|
chunk = tail[: m_end.start()] if m_end else tail
|
||||||
|
|
||||||
|
lines = chunk.splitlines()
|
||||||
|
n = len(lines)
|
||||||
|
j = 0
|
||||||
|
while j < n:
|
||||||
|
line = lines[j]
|
||||||
|
m4 = re.match(r"^####\s+(.+?)\s*$", line)
|
||||||
|
if m4 and m4.group(1).strip() == title:
|
||||||
|
j += 1
|
||||||
|
body_lines: list[str] = []
|
||||||
|
while j < n:
|
||||||
|
nxt = lines[j]
|
||||||
|
mpeer = re.match(r"^####\s+(.+?)\s*$", nxt)
|
||||||
|
if mpeer:
|
||||||
|
inner = mpeer.group(1).strip()
|
||||||
|
if inner in _SENTIMENT_LLM_INNER_LEVEL4:
|
||||||
|
body_lines.append(nxt)
|
||||||
|
j += 1
|
||||||
|
continue
|
||||||
|
break
|
||||||
|
if re.match(r"^###\s", nxt) or re.match(r"^##\s", nxt):
|
||||||
|
break
|
||||||
|
body_lines.append(nxt)
|
||||||
|
j += 1
|
||||||
|
return "\n".join(body_lines).strip()
|
||||||
|
j += 1
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
def extract_level4_sections_by_group_title(md: str, group_title: str) -> list[str]:
|
def extract_level4_sections_by_group_title(md: str, group_title: str) -> list[str]:
|
||||||
"""
|
"""
|
||||||
@ -72,14 +145,19 @@ def load_report_matrix_group_evidence_markdown(
|
|||||||
except OSError:
|
except OSError:
|
||||||
return "", "none"
|
return "", "none"
|
||||||
|
|
||||||
parts = extract_level4_sections_by_group_title(full, group_title)
|
without_83 = _md_splice_out_sentiment_83_section(full)
|
||||||
|
parts = extract_level4_sections_by_group_title(without_83, group_title)
|
||||||
|
s83 = extract_sentiment_83_level4_body(full, group_title)
|
||||||
|
if s83:
|
||||||
|
parts.append(s83)
|
||||||
if not parts:
|
if not parts:
|
||||||
return "", "none"
|
return "", "none"
|
||||||
|
|
||||||
intro = (
|
intro = (
|
||||||
f"> **说明**:以下为同任务《竞品分析报告》正文中、细类「**{group_title.strip()}**」下 "
|
f"> **说明**:以下为同任务《竞品分析报告》正文中、细类「**{group_title.strip()}**」下 "
|
||||||
"「#### …」小节的**大模型归纳**摘录(按正文出现顺序拼接),"
|
"「#### …」小节的**大模型归纳**摘录(按正文出现顺序拼接),"
|
||||||
"覆盖矩阵/价盘/促销/评论与场景等块中**已生成**的段落;"
|
"覆盖矩阵/价盘/促销/评论与场景等块中**已生成**的段落,"
|
||||||
|
"并含 **§8.3 评价正/负向主题(按细类 · 大模型)** 内该细类段落(允许嵌套四级小标题);"
|
||||||
"若某块未开 LLM 或未产出对应小节,则不会出现在此摘录中。\n\n"
|
"若某块未开 LLM 或未产出对应小节,则不会出现在此摘录中。\n\n"
|
||||||
)
|
)
|
||||||
sep = "\n\n---\n\n"
|
sep = "\n\n---\n\n"
|
||||||
@ -93,5 +171,6 @@ def load_report_matrix_group_evidence_markdown(
|
|||||||
|
|
||||||
__all__ = [
|
__all__ = [
|
||||||
"extract_level4_sections_by_group_title",
|
"extract_level4_sections_by_group_title",
|
||||||
|
"extract_sentiment_83_level4_body",
|
||||||
"load_report_matrix_group_evidence_markdown",
|
"load_report_matrix_group_evidence_markdown",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -1,11 +1,15 @@
|
|||||||
"""从宿主报告 MD 按细类抽取大模型小节。"""
|
"""从宿主报告 MD 按细类抽取大模型小节。"""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from tempfile import TemporaryDirectory
|
||||||
|
|
||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
|
|
||||||
from pipeline.llm.generate_sections import demote_sentiment_inner_h4_to_h5_for_matrix_group
|
|
||||||
from pipeline.reporting.report_matrix_group_evidence import (
|
from pipeline.reporting.report_matrix_group_evidence import (
|
||||||
extract_level4_sections_by_group_title,
|
extract_level4_sections_by_group_title,
|
||||||
|
extract_sentiment_83_level4_body,
|
||||||
|
load_report_matrix_group_evidence_markdown,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -39,23 +43,51 @@ B 段评论归纳。
|
|||||||
[],
|
[],
|
||||||
)
|
)
|
||||||
|
|
||||||
def test_extract_8_3_sentiment_when_inner_subsections_are_h5(self) -> None:
|
def test_sentiment_83_nested_level4(self) -> None:
|
||||||
"""8.3 在 #### 细类 下须用 ##### 子标题,否则抽取在首个 #### 子节处断开。"""
|
md = """## 八、消费者反馈
|
||||||
inner = demote_sentiment_inner_h4_to_h5_for_matrix_group(
|
|
||||||
"#### 正向体验主题\n\n正文A\n\n#### 负向评价主题归因\n\n正文B\n"
|
|
||||||
)
|
|
||||||
md = (
|
|
||||||
"### 8.3 评价正/负向主题\n\n"
|
|
||||||
"#### 饼干\n\n"
|
|
||||||
f"{inner}\n"
|
|
||||||
)
|
|
||||||
parts = extract_level4_sections_by_group_title(md, "饼干")
|
|
||||||
self.assertEqual(len(parts), 1)
|
|
||||||
self.assertIn("正文A", parts[0])
|
|
||||||
self.assertIn("正文B", parts[0])
|
|
||||||
|
|
||||||
def test_demote_known_h4_titles_only(self) -> None:
|
### 8.3 评价正/负向主题(按细类 · 大模型)
|
||||||
raw = "#### 正向体验主题\nx\n#### 饼干\ny\n"
|
|
||||||
out = demote_sentiment_inner_h4_to_h5_for_matrix_group(raw)
|
> 说明
|
||||||
self.assertIn("##### 正向体验主题", out)
|
|
||||||
self.assertIn("#### 饼干", out)
|
#### 饼干
|
||||||
|
|
||||||
|
#### 正向体验主题
|
||||||
|
酥脆好评。
|
||||||
|
|
||||||
|
#### 负向评价主题归因
|
||||||
|
略贵。
|
||||||
|
|
||||||
|
#### 西式糕点
|
||||||
|
|
||||||
|
#### 正向体验主题
|
||||||
|
别的细类。
|
||||||
|
|
||||||
|
## 九、策略
|
||||||
|
"""
|
||||||
|
body = extract_sentiment_83_level4_body(md, "饼干")
|
||||||
|
self.assertIn("酥脆好评", body)
|
||||||
|
self.assertIn("正向体验主题", body)
|
||||||
|
self.assertIn("略贵", body)
|
||||||
|
self.assertNotIn("别的细类", body)
|
||||||
|
|
||||||
|
def test_load_includes_83_after_splice(self) -> None:
|
||||||
|
md = """#### 饼干
|
||||||
|
矩阵段。
|
||||||
|
|
||||||
|
### 8.3 评价正/负向主题(按细类 · 大模型)
|
||||||
|
|
||||||
|
#### 饼干
|
||||||
|
|
||||||
|
#### 正向体验主题
|
||||||
|
情感段。
|
||||||
|
|
||||||
|
## 九、策略
|
||||||
|
"""
|
||||||
|
with TemporaryDirectory() as td:
|
||||||
|
p = Path(td) / "competitor_analysis.md"
|
||||||
|
p.write_text(md, encoding="utf-8")
|
||||||
|
out, src = load_report_matrix_group_evidence_markdown(td, "饼干")
|
||||||
|
self.assertEqual(src, "competitor_analysis_md")
|
||||||
|
self.assertIn("矩阵段", out)
|
||||||
|
self.assertIn("情感段", out)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user