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 小节均以 ``#### `` + 与矩阵一致的细类名为小节标题
|
||||
(见 ``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
|
||||
|
||||
@ -13,6 +15,77 @@ import re
|
||||
from pathlib import Path
|
||||
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]:
|
||||
"""
|
||||
@ -72,14 +145,19 @@ def load_report_matrix_group_evidence_markdown(
|
||||
except OSError:
|
||||
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:
|
||||
return "", "none"
|
||||
|
||||
intro = (
|
||||
f"> **说明**:以下为同任务《竞品分析报告》正文中、细类「**{group_title.strip()}**」下 "
|
||||
"「#### …」小节的**大模型归纳**摘录(按正文出现顺序拼接),"
|
||||
"覆盖矩阵/价盘/促销/评论与场景等块中**已生成**的段落;"
|
||||
"覆盖矩阵/价盘/促销/评论与场景等块中**已生成**的段落,"
|
||||
"并含 **§8.3 评价正/负向主题(按细类 · 大模型)** 内该细类段落(允许嵌套四级小标题);"
|
||||
"若某块未开 LLM 或未产出对应小节,则不会出现在此摘录中。\n\n"
|
||||
)
|
||||
sep = "\n\n---\n\n"
|
||||
@ -93,5 +171,6 @@ def load_report_matrix_group_evidence_markdown(
|
||||
|
||||
__all__ = [
|
||||
"extract_level4_sections_by_group_title",
|
||||
"extract_sentiment_83_level4_body",
|
||||
"load_report_matrix_group_evidence_markdown",
|
||||
]
|
||||
|
||||
@ -1,11 +1,15 @@
|
||||
"""从宿主报告 MD 按细类抽取大模型小节。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
from tempfile import TemporaryDirectory
|
||||
|
||||
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 (
|
||||
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:
|
||||
"""8.3 在 #### 细类 下须用 ##### 子标题,否则抽取在首个 #### 子节处断开。"""
|
||||
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_sentiment_83_nested_level4(self) -> None:
|
||||
md = """## 八、消费者反馈
|
||||
|
||||
def test_demote_known_h4_titles_only(self) -> None:
|
||||
raw = "#### 正向体验主题\nx\n#### 饼干\ny\n"
|
||||
out = demote_sentiment_inner_h4_to_h5_for_matrix_group(raw)
|
||||
self.assertIn("##### 正向体验主题", out)
|
||||
self.assertIn("#### 饼干", out)
|
||||
### 8.3 评价正/负向主题(按细类 · 大模型)
|
||||
|
||||
> 说明
|
||||
|
||||
#### 饼干
|
||||
|
||||
#### 正向体验主题
|
||||
酥脆好评。
|
||||
|
||||
#### 负向评价主题归因
|
||||
略贵。
|
||||
|
||||
#### 西式糕点
|
||||
|
||||
#### 正向体验主题
|
||||
别的细类。
|
||||
|
||||
## 九、策略
|
||||
"""
|
||||
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