mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-21 23:41:39 +08:00
feat(pipeline): S1 策略稿与第九章对齐(report_strategy_excerpt)
新增 load_report_strategy_excerpt:优先 strategy_opportunities_llm.json 的 markdown,否则从 competitor_analysis.md 截取第九章;runner 成功时落盘 markdown 字段。STRATEGY_SYSTEM 收紧与 report_strategy_excerpt 一致及业务冲突标注。策略稿 API 返回节选来源与字符数;补充单测与 OpenAPI、规划文档。 Made-with: Cursor
This commit is contained in:
parent
224e7d0c16
commit
6e113bbdfe
@ -751,6 +751,8 @@ def write_competitor_analysis_for_run_dir(
|
||||
strategy_opp_llm_rec["prior_chapter_narrative_keys"] = sorted(
|
||||
_strategy_narratives.keys()
|
||||
)
|
||||
if (llm_strategy_opp_md or "").strip():
|
||||
strategy_opp_llm_rec["markdown"] = llm_strategy_opp_md
|
||||
except Exception as e:
|
||||
strategy_opp_llm_rec["ok"] = False
|
||||
strategy_opp_llm_rec["error"] = str(e)
|
||||
|
||||
@ -12,7 +12,9 @@ from .llm_client import call_llm, estimate_chat_input_tokens, llm_context_window
|
||||
STRATEGY_SYSTEM = """你是市场策略顾问,根据**结构化监测摘要**与业务侧填写的**决策字段**,把「规则底稿」润色为可读的策略 Markdown。
|
||||
|
||||
**规则**:
|
||||
- 输入 JSON 含 `rules_draft_markdown`(规则引擎生成的底稿,与同任务数据一致)、`structured_brief`(摘要子集)、`strategy_decisions`、`business_notes` 等;
|
||||
- 输入 JSON 含 `rules_draft_markdown`(规则引擎生成的底稿,与同任务数据一致)、`structured_brief`(摘要子集)、`strategy_decisions`、`business_notes` 等;可选 `report_strategy_excerpt`(与同任务宿主报告**第九章**「策略与机会」正文同源,来自已落盘产物)。
|
||||
- **与报告第九章对齐(硬性,当 `report_strategy_excerpt` 非空时)**:润色后的全文在**战略方向与主要判断**上须与该节选**一致**,不得写出与之**明显矛盾**的结论(如定价档位、差异化主线、主要风险判断);若 `business_notes` 或 `strategy_decisions` 与节选冲突,须在正文适当位置简短说明「与报告第九章策略归纳不一致之处见业务备注」类表述,且**不得**把节选与 `structured_brief` 中均未出现的数字当作既定事实。
|
||||
- **当 `report_strategy_excerpt` 为空或仅为占位说明**:表示报告侧未生成或未重跑第九章大模型正文,仅依据 `rules_draft_markdown` 与 `structured_brief` 润色,**不得编造**与可能存在的报告第九章结论。
|
||||
- **不得编造**输入中不存在的销量、占比、价格数字;若底稿与摘要中有数字,须保持一致;表述集中度时用「第一大……份额」「前三家合计」等中文,**不要用** CR1、CR3 等缩写;
|
||||
- 若 `structured_brief` 含 `matrix_overview_for_llm` 或矩阵相关字段,策略中应**呼应**细分类目分组与竞品矩阵结论,不得无故删光矩阵相关建议;
|
||||
- 可调整段落衔接、标题层级、列表与表格呈现,使更易读;可补充「建议」「待业务确认」类表述,但不虚构竞品名称或数据;
|
||||
@ -29,7 +31,12 @@ def generate_strategy_draft_markdown_llm(
|
||||
business_notes: str,
|
||||
generated_at_iso: str,
|
||||
strategy_decisions: dict[str, Any],
|
||||
report_strategy_excerpt: str | None = None,
|
||||
) -> str:
|
||||
"""
|
||||
``report_strategy_excerpt``:与同任务宿主报告第九章「策略与机会」正文对齐的节选(见
|
||||
``reporting.report_strategy_excerpt.load_report_strategy_excerpt``);空字符串表示未生成或未重跑第九章大模型。
|
||||
"""
|
||||
rules_md = build_strategy_draft_markdown(
|
||||
job_id=job_id,
|
||||
keyword=keyword,
|
||||
@ -39,6 +46,7 @@ def generate_strategy_draft_markdown_llm(
|
||||
strategy_decisions=strategy_decisions,
|
||||
)
|
||||
compact = compact_brief_for_llm(brief, max_chars=80_000)
|
||||
excerpt = (report_strategy_excerpt or "").strip()
|
||||
payload = {
|
||||
"job_id": job_id,
|
||||
"keyword": keyword,
|
||||
@ -47,6 +55,7 @@ def generate_strategy_draft_markdown_llm(
|
||||
"business_notes": business_notes,
|
||||
"structured_brief": compact,
|
||||
"rules_draft_markdown": rules_md,
|
||||
"report_strategy_excerpt": excerpt,
|
||||
}
|
||||
raw = json.dumps(payload, ensure_ascii=False)
|
||||
if len(raw) > 500_000:
|
||||
|
||||
72
backend/pipeline/reporting/report_strategy_excerpt.py
Normal file
72
backend/pipeline/reporting/report_strategy_excerpt.py
Normal file
@ -0,0 +1,72 @@
|
||||
"""
|
||||
从任务 run 目录加载「报告第九章 · 策略与机会」正文,供策略稿 LLM 与宿主报告对齐(阶段 S1)。
|
||||
|
||||
优先顺序:
|
||||
|
||||
1. ``strategy_opportunities_llm.json`` 中的 ``markdown``(与 runner 落盘一致);
|
||||
2. 否则从 ``competitor_analysis.md`` 截取 ``## 九、策略与机会提示`` 至 ``## 附录`` 之前。
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
from typing import Literal
|
||||
|
||||
CHAPTER_NINE_HEADING = "## 九、策略与机会提示"
|
||||
|
||||
|
||||
def extract_chapter_nine_strategy_markdown(full_md: str) -> str:
|
||||
"""
|
||||
提取宿主报告中第九章策略块(含章节标题行),不含「附录」及之后内容。
|
||||
|
||||
若未找到标题,返回空字符串。
|
||||
"""
|
||||
t = full_md or ""
|
||||
if not t.strip():
|
||||
return ""
|
||||
key = CHAPTER_NINE_HEADING
|
||||
i = t.find(key)
|
||||
if i == -1:
|
||||
return ""
|
||||
chunk = t[i:]
|
||||
j = chunk.find("\n## 附录")
|
||||
if j != -1:
|
||||
chunk = chunk[:j]
|
||||
return chunk.rstrip()
|
||||
|
||||
|
||||
def load_report_strategy_excerpt(
|
||||
run_dir: Path | str,
|
||||
*,
|
||||
max_chars: int = 24_000,
|
||||
) -> tuple[str, Literal["json_markdown", "competitor_analysis_md", "none"]]:
|
||||
"""
|
||||
返回 ``(节选正文, 来源标签)``。节选可能为空(未生成第九章或未找到标题)。
|
||||
"""
|
||||
root = Path(run_dir)
|
||||
cap = max(512, int(max_chars))
|
||||
|
||||
json_path = root / "strategy_opportunities_llm.json"
|
||||
if json_path.is_file():
|
||||
try:
|
||||
data = json.loads(json_path.read_text(encoding="utf-8"))
|
||||
except (json.JSONDecodeError, OSError):
|
||||
data = None
|
||||
if isinstance(data, dict):
|
||||
md = (data.get("markdown") or "").strip()
|
||||
if md:
|
||||
s = md if len(md) <= cap else md[: cap - 80].rstrip() + "\n\n…(已截断)\n"
|
||||
return s, "json_markdown"
|
||||
|
||||
md_path = root / "competitor_analysis.md"
|
||||
if md_path.is_file():
|
||||
try:
|
||||
full = md_path.read_text(encoding="utf-8")
|
||||
except OSError:
|
||||
return "", "none"
|
||||
block = extract_chapter_nine_strategy_markdown(full)
|
||||
if block.strip():
|
||||
s = block if len(block) <= cap else block[: cap - 80].rstrip() + "\n\n…(已截断)\n"
|
||||
return s, "competitor_analysis_md"
|
||||
|
||||
return "", "none"
|
||||
68
backend/pipeline/tests/test_report_strategy_excerpt.py
Normal file
68
backend/pipeline/tests/test_report_strategy_excerpt.py
Normal file
@ -0,0 +1,68 @@
|
||||
"""``report_strategy_excerpt``:第九章正文加载与截取。"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
from pipeline.reporting.report_strategy_excerpt import (
|
||||
CHAPTER_NINE_HEADING,
|
||||
extract_chapter_nine_strategy_markdown,
|
||||
load_report_strategy_excerpt,
|
||||
)
|
||||
|
||||
|
||||
def test_extract_chapter_nine_stops_before_appendix() -> None:
|
||||
md = f"""# x
|
||||
|
||||
{CHAPTER_NINE_HEADING}(假设清单,待验证)
|
||||
|
||||
正文一段。
|
||||
|
||||
---
|
||||
|
||||
## 附录 A:数据留存说明
|
||||
|
||||
尾部。
|
||||
"""
|
||||
out = extract_chapter_nine_strategy_markdown(md)
|
||||
assert CHAPTER_NINE_HEADING in out
|
||||
assert "正文一段" in out
|
||||
assert "附录" not in out
|
||||
assert "尾部" not in out
|
||||
|
||||
|
||||
def test_load_prefers_json_markdown(tmp_path: Path) -> None:
|
||||
(tmp_path / "strategy_opportunities_llm.json").write_text(
|
||||
json.dumps(
|
||||
{"schema_version": 1, "ok": True, "markdown": "JSON 内第九章正文"},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(tmp_path / "competitor_analysis.md").write_text(
|
||||
f"{CHAPTER_NINE_HEADING}\n\n从 MD 来\n\n## 附录 A\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
text, src = load_report_strategy_excerpt(tmp_path)
|
||||
assert src == "json_markdown"
|
||||
assert "JSON 内第九章" in text
|
||||
|
||||
|
||||
def test_load_falls_back_to_competitor_md(tmp_path: Path) -> None:
|
||||
(tmp_path / "strategy_opportunities_llm.json").write_text(
|
||||
json.dumps({"schema_version": 1, "ok": True}, ensure_ascii=False),
|
||||
encoding="utf-8",
|
||||
)
|
||||
(tmp_path / "competitor_analysis.md").write_text(
|
||||
f"{CHAPTER_NINE_HEADING}(假设清单,待验证)\n\n从 MD 截取。\n\n## 附录 A\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
text, src = load_report_strategy_excerpt(tmp_path)
|
||||
assert src == "competitor_analysis_md"
|
||||
assert "从 MD 截取" in text
|
||||
|
||||
|
||||
def test_load_none_when_missing(tmp_path: Path) -> None:
|
||||
text, src = load_report_strategy_excerpt(tmp_path)
|
||||
assert src == "none"
|
||||
assert text == ""
|
||||
@ -22,6 +22,7 @@ from ..llm.generate import generate_strategy_draft_markdown_llm
|
||||
from ..models import JobStatus, PipelineJob
|
||||
from ..reporting.brief_pack import build_brief_pack_zip_bytes
|
||||
from ..reporting.md_document_export import markdown_to_docx_bytes, markdown_to_pdf_bytes
|
||||
from ..reporting.report_strategy_excerpt import load_report_strategy_excerpt
|
||||
from ..reporting.strategy_draft import build_strategy_draft_markdown
|
||||
from ..serializers import PipelineJobSerializer, StrategyDraftRequestSerializer
|
||||
from .common import job_run_dir_usable
|
||||
@ -151,6 +152,12 @@ class JobStrategyDraftView(APIView):
|
||||
|
||||
gen_at = timezone.now().isoformat()
|
||||
generator = (vd.get("generator") or "rules").strip()
|
||||
excerpt_src = "none"
|
||||
report_excerpt = ""
|
||||
try:
|
||||
report_excerpt, excerpt_src = load_report_strategy_excerpt(job.run_dir)
|
||||
except OSError:
|
||||
report_excerpt, excerpt_src = "", "none"
|
||||
try:
|
||||
if generator == "llm":
|
||||
md = generate_strategy_draft_markdown_llm(
|
||||
@ -160,6 +167,7 @@ class JobStrategyDraftView(APIView):
|
||||
business_notes=notes,
|
||||
generated_at_iso=gen_at,
|
||||
strategy_decisions=strategy_decisions,
|
||||
report_strategy_excerpt=report_excerpt,
|
||||
)
|
||||
src = "llm_text_ai_crawler_v1"
|
||||
else:
|
||||
@ -179,16 +187,17 @@ class JobStrategyDraftView(APIView):
|
||||
{"detail": f"大模型网关错误:{e}"},
|
||||
status=status.HTTP_502_BAD_GATEWAY,
|
||||
)
|
||||
return Response(
|
||||
{
|
||||
"schema_version": 1,
|
||||
"job_id": job.id,
|
||||
"keyword": job.keyword,
|
||||
"generated_at": gen_at,
|
||||
"source": src,
|
||||
"markdown": md,
|
||||
}
|
||||
)
|
||||
body: dict[str, object] = {
|
||||
"schema_version": 1,
|
||||
"job_id": job.id,
|
||||
"keyword": job.keyword,
|
||||
"generated_at": gen_at,
|
||||
"source": src,
|
||||
"markdown": md,
|
||||
"report_strategy_excerpt_source": excerpt_src,
|
||||
"report_strategy_excerpt_chars": len(report_excerpt or ""),
|
||||
}
|
||||
return Response(body)
|
||||
|
||||
|
||||
@method_decorator(csrf_exempt, name="dispatch")
|
||||
|
||||
@ -585,3 +585,10 @@ components:
|
||||
example: structured_summary_rules_v1
|
||||
markdown:
|
||||
type: string
|
||||
report_strategy_excerpt_source:
|
||||
type: string
|
||||
description: none=未找到第九章正文;json_markdown=strategy_opportunities_llm.json 的 markdown;competitor_analysis_md=从 competitor_analysis.md 截取
|
||||
enum: [none, json_markdown, competitor_analysis_md]
|
||||
report_strategy_excerpt_chars:
|
||||
type: integer
|
||||
description: 参与对齐的第九章节选字符数(供策略稿 LLM 的 report_strategy_excerpt;generator=rules 时仅元数据)
|
||||
|
||||
@ -7,7 +7,8 @@
|
||||
| 能力 | 位置 |
|
||||
|------|------|
|
||||
| 规则策略底稿 | `pipeline/reporting/strategy_draft.py` → `build_strategy_draft_markdown` |
|
||||
| 策略稿 LLM 润色 | `pipeline/llm/generate_strategy.py` → `generate_strategy_draft_markdown_llm` |
|
||||
| 策略稿 LLM 润色 | `pipeline/llm/generate_strategy.py` → `generate_strategy_draft_markdown_llm`(payload 含 `report_strategy_excerpt`) |
|
||||
| 第九章节选加载(S1) | `pipeline/reporting/report_strategy_excerpt.py` → `load_report_strategy_excerpt` |
|
||||
| 报告第九章策略归纳 | `generate_strategy_opportunities_llm`(与 `competitor_brief` + 各章节选对齐) |
|
||||
| 策略稿 API / 导出 | `pipeline/views/job_report_views.py` → `JobStrategyDraftView` |
|
||||
| 简报与压缩 | `pipeline/reporting/brief_compact.py` |
|
||||
@ -26,15 +27,16 @@
|
||||
|
||||
---
|
||||
|
||||
## 2. 策略稿:与报告内「策略建议」对齐(阶段 S1)
|
||||
## 2. 策略稿:与报告内「策略建议」对齐(阶段 S1)— **已落地**
|
||||
|
||||
**目标**:独立下载的策略稿与宿主报告第九章**方向一致**,避免与已发布报告矛盾。
|
||||
|
||||
**拟议实现要点**:
|
||||
**实现要点(与代码一致)**:
|
||||
|
||||
1. 策略稿生成/润色前,加载本任务 **`strategy_opportunities_llm.json`**(若存在),或从 **`competitor_analysis.md`** 抽取第九章相关正文片段,作为 **`report_strategy_excerpt`** 并入 LLM 的 JSON payload。
|
||||
2. 在 `STRATEGY_SYSTEM` 中明确:**须与 `report_strategy_excerpt` 策略方向一致**;若业务备注与报告冲突,文中标注「与报告不一致处见业务备注」。
|
||||
3. 产品侧:`generator: rules | llm` 区分展示;**规则版**可作为审计与对外发送前的对照底稿。
|
||||
1. **`load_report_strategy_excerpt(run_dir)`**:优先读 `strategy_opportunities_llm.json` 的 **`markdown`**(runner 在第九章大模型成功时写入);否则从 **`competitor_analysis.md`** 截取 `## 九、策略与机会提示` 至 `## 附录` 之前。
|
||||
2. **`STRATEGY_SYSTEM`**:当 `report_strategy_excerpt` 非空时,润色稿须与节选**战略方向一致**;若 `business_notes` / `strategy_decisions` 与节选冲突,须标注「与报告第九章策略归纳不一致之处见业务备注」类表述。节选为空时不得编造第九章结论。
|
||||
3. **API**:`POST /api/jobs/{id}/strategy-draft/` 响应增加 `report_strategy_excerpt_source`、`report_strategy_excerpt_chars`(`generator=rules` 时亦返回,便于核对是否加载到节选)。
|
||||
4. 产品侧:`generator: rules | llm` 仍为既有行为;规则版作审计底稿。
|
||||
|
||||
**验收**:抽样任务核对「第九章要点 ↔ 策略稿 bullet」可对应;数字仅来自 brief/底稿。
|
||||
|
||||
@ -73,7 +75,7 @@
|
||||
|
||||
| 阶段 | 内容 | 产出 |
|
||||
|------|------|------|
|
||||
| **S1** | 策略稿 payload 增加 `report_strategy_excerpt`,收紧 `STRATEGY_SYSTEM` | 策略稿与第九章可对照 |
|
||||
| **S1** | 策略稿 payload 增加 `report_strategy_excerpt`,收紧 `STRATEGY_SYSTEM` | ✅ 已合并:`report_strategy_excerpt.py`、runner 落盘 `markdown`、OpenAPI 响应字段 |
|
||||
| **S2** | 营销模块 v1:单模板 + 强约束提示词 + 落盘 + 复用 Word/PDF 导出 | 可演示的营销初稿 |
|
||||
| **S3** | 前端:任务页入口、受众与渠道、生成历史 | 产品闭环 |
|
||||
| **S4**(可选) | 轻量校验:输出中数字与 brief 同源性启发式检查 | 降低明显幻觉 |
|
||||
@ -92,5 +94,6 @@
|
||||
| 日期 | 说明 |
|
||||
|------|------|
|
||||
| 2026-04-17 | 首版:对齐原则、S1~S4、代码锚点、API 示意。 |
|
||||
| 2026-04-18 | S1 落地:`load_report_strategy_excerpt`、`STRATEGY_SYSTEM` 对齐条款、`strategy_opportunities_llm.json.markdown`、策略稿 API 响应字段。 |
|
||||
|
||||
后续变更请在本表追加一行,并在正文相应章节修改。
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user