mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-21 23:41:39 +08:00
feat(strategy): 无表单决策时由大模型推断草案并净化对外成稿约束
新增 strategy_decisions_substantive 与 JSON 载荷字段;STRATEGY_SYSTEM/USER_PREFIX 要求实质成稿、禁止技术泄露;build_strategy_draft_markdown(for_llm_input=True) 供归纳用弱化路径与字段名。规则下载稿默认行为不变。 Made-with: Cursor
This commit is contained in:
parent
805e9b4de4
commit
4cf73e7602
@ -21,8 +21,6 @@ from .generate_group_summaries import (
|
|||||||
PRICE_GROUPS_USER_PREFIX,
|
PRICE_GROUPS_USER_PREFIX,
|
||||||
PROMO_GROUPS_SYSTEM,
|
PROMO_GROUPS_SYSTEM,
|
||||||
PROMO_GROUPS_USER_PREFIX,
|
PROMO_GROUPS_USER_PREFIX,
|
||||||
SCENARIO_GROUPS_SYSTEM,
|
|
||||||
SCENARIO_GROUPS_USER_PREFIX,
|
|
||||||
_join_chunked_group_markdown,
|
_join_chunked_group_markdown,
|
||||||
generate_comment_group_summaries_llm,
|
generate_comment_group_summaries_llm,
|
||||||
generate_comment_group_summaries_llm_chunked,
|
generate_comment_group_summaries_llm_chunked,
|
||||||
@ -32,8 +30,6 @@ from .generate_group_summaries import (
|
|||||||
generate_price_group_summaries_llm_chunked,
|
generate_price_group_summaries_llm_chunked,
|
||||||
generate_promo_group_summaries_llm,
|
generate_promo_group_summaries_llm,
|
||||||
generate_promo_group_summaries_llm_chunked,
|
generate_promo_group_summaries_llm_chunked,
|
||||||
generate_scenario_group_summaries_llm,
|
|
||||||
generate_scenario_group_summaries_llm_chunked,
|
|
||||||
)
|
)
|
||||||
from .generate_sections import (
|
from .generate_sections import (
|
||||||
BRIDGE_SECTIONS_SYSTEM,
|
BRIDGE_SECTIONS_SYSTEM,
|
||||||
@ -49,6 +45,7 @@ from .generate_strategy import (
|
|||||||
STRATEGY_USER_PREFIX,
|
STRATEGY_USER_PREFIX,
|
||||||
generate_strategy_draft_markdown_llm,
|
generate_strategy_draft_markdown_llm,
|
||||||
generate_strategy_opportunities_llm,
|
generate_strategy_opportunities_llm,
|
||||||
|
strategy_decisions_substantive,
|
||||||
)
|
)
|
||||||
from .llm_client import call_llm as _call_llm
|
from .llm_client import call_llm as _call_llm
|
||||||
|
|
||||||
@ -64,8 +61,6 @@ __all__ = [
|
|||||||
"PROMO_GROUPS_USER_PREFIX",
|
"PROMO_GROUPS_USER_PREFIX",
|
||||||
"REPORT_SYSTEM",
|
"REPORT_SYSTEM",
|
||||||
"REPORT_USER_PREFIX",
|
"REPORT_USER_PREFIX",
|
||||||
"SCENARIO_GROUPS_SYSTEM",
|
|
||||||
"SCENARIO_GROUPS_USER_PREFIX",
|
|
||||||
"SENTIMENT_LLM_SYSTEM",
|
"SENTIMENT_LLM_SYSTEM",
|
||||||
"STRATEGY_OPPORTUNITIES_SYSTEM",
|
"STRATEGY_OPPORTUNITIES_SYSTEM",
|
||||||
"STRATEGY_OPPORTUNITIES_USER_PREFIX",
|
"STRATEGY_OPPORTUNITIES_USER_PREFIX",
|
||||||
@ -83,10 +78,9 @@ __all__ = [
|
|||||||
"generate_price_group_summaries_llm_chunked",
|
"generate_price_group_summaries_llm_chunked",
|
||||||
"generate_promo_group_summaries_llm",
|
"generate_promo_group_summaries_llm",
|
||||||
"generate_promo_group_summaries_llm_chunked",
|
"generate_promo_group_summaries_llm_chunked",
|
||||||
"generate_scenario_group_summaries_llm",
|
|
||||||
"generate_scenario_group_summaries_llm_chunked",
|
|
||||||
"generate_section_bridges_llm",
|
"generate_section_bridges_llm",
|
||||||
"generate_strategy_draft_markdown_llm",
|
"generate_strategy_draft_markdown_llm",
|
||||||
"generate_strategy_opportunities_llm",
|
"generate_strategy_opportunities_llm",
|
||||||
"split_competitor_report_for_bridges",
|
"split_competitor_report_for_bridges",
|
||||||
|
"strategy_decisions_substantive",
|
||||||
]
|
]
|
||||||
|
|||||||
@ -12,6 +12,37 @@ from ..reporting.strategy_draft import (
|
|||||||
)
|
)
|
||||||
from .llm_client import call_llm, estimate_chat_input_tokens, llm_context_window_size
|
from .llm_client import call_llm, estimate_chat_input_tokens, llm_context_window_size
|
||||||
|
|
||||||
|
# 与策略生成表单 POST 字段一致:任一则视为业务已提供「实质决策」,否则由模型基于数据推断草案。
|
||||||
|
_STRATEGY_DECISION_SUBSTANTIVE_KEYS: tuple[str, ...] = (
|
||||||
|
"product_role",
|
||||||
|
"battlefield_one_line",
|
||||||
|
"audience_segment",
|
||||||
|
"time_horizon",
|
||||||
|
"success_criteria",
|
||||||
|
"non_goals",
|
||||||
|
"positioning_choice",
|
||||||
|
"competitive_stance",
|
||||||
|
"pillar_product",
|
||||||
|
"pillar_price",
|
||||||
|
"pillar_channel",
|
||||||
|
"pillar_comm",
|
||||||
|
"marketing_strategy",
|
||||||
|
"general_strategy",
|
||||||
|
"competitor_reference",
|
||||||
|
"resource_notes",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def strategy_decisions_substantive(strategy_decisions: dict[str, Any] | None) -> bool:
|
||||||
|
"""是否填写了至少一项策略表单文本字段(用于 LLM 是否「自动推断」全稿)。"""
|
||||||
|
if not isinstance(strategy_decisions, dict):
|
||||||
|
return False
|
||||||
|
for k in _STRATEGY_DECISION_SUBSTANTIVE_KEYS:
|
||||||
|
v = strategy_decisions.get(k)
|
||||||
|
if isinstance(v, str) and v.strip():
|
||||||
|
return True
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def _omit_ch8_probe_wordchart_fields(compact: dict[str, Any]) -> None:
|
def _omit_ch8_probe_wordchart_fields(compact: dict[str, Any]) -> None:
|
||||||
"""
|
"""
|
||||||
@ -89,10 +120,21 @@ STRATEGY_SYSTEM = f"""你是市场策略顾问,根据**结构化监测摘要**
|
|||||||
|
|
||||||
**落实范围**:上文「全局禁止编造」适用于**摘要、一至十、附录**的每一句话与表格每一格;**不得**因章节不同而放宽。
|
**落实范围**:上文「全局禁止编造」适用于**摘要、一至十、附录**的每一句话与表格每一格;**不得**因章节不同而放宽。
|
||||||
|
|
||||||
|
**对外成稿与禁止技术泄露(硬性)**:
|
||||||
|
- 正文须为**可直接对业务或合作方阅读**的正式策略文档(对外前仍须按需脱敏)。**禁止**出现:反引号代码体、JSON 键名、英文字段名、内部数据结构名、源码或仓库路径、类文件名、「任务 ID」「工作台」「规则骨架」等系统痕迹;**禁止**照抄底稿中以 *成稿:*、*回答:*、*占位*、*骨架* 开头的**元说明句**,须改写为正式业务表述。
|
||||||
|
- **一级标题**用文书式,例如 `# 「{{keyword}}」市场策略建议书(草案)`,其中 `keyword` 取自本消息 JSON 的同名字段;**勿**使用「草稿」「底稿」「归纳用」等对内用词。
|
||||||
|
- **附录**中的采集范围等信息用**中文短句**(如「列表页约采集第 3~10 页」),**禁止** `page_start=` 等键值对或英文键名。
|
||||||
|
|
||||||
|
**业务决策未填写时的成稿义务(当 JSON 中 `strategy_decisions_substantive` 为 false 时)**:
|
||||||
|
- 视为未提交表单决策:须基于监测摘要、细类报告节选、底稿数据表及 `business_notes`(若有)**主动推断**一套连贯的**假设性**策略;在「策略范围与前提」写清推断前提(「假设:」「待业务确认:」),对阶段目标类型给出 **A~E 类选项及你从数据中归纳的推荐倾向**(不得只列选项而无立场)。
|
||||||
|
- **§2.1 针对痛点要怎么做**须含**多行实质内容**,覆盖监测已支撑的主要细类与痛点,**禁止**整表留白或满篇 *(待填)*。
|
||||||
|
- 仍须遵守全局禁止编造:数字、品牌、店铺、用户原话、活动规则仅可来自输入依据;无依据处用假设语气。
|
||||||
|
|
||||||
**决策边界(硬性)**:
|
**决策边界(硬性)**:
|
||||||
- **业务已在 `strategy_decisions` 中填写的项**(角色、时间、成功标准、战场一句话、定位勾选、竞争倾向、四柱、目标客群/对标/资源备注、**营销策略**与**总体策略**等)视为**已定决策**:成稿须**落实为具体执行句**,**不得**改写成相反结论或再要求用户「请选择」。
|
- **当 `strategy_decisions_substantive` 为 true 时**:业务已在 `strategy_decisions` 中填写的项(角色、时间、成功标准、战场一句话、定位勾选、竞争倾向、四柱、目标客群/对标/资源备注、**营销策略**与**总体策略**等)视为**已定决策**:成稿须**落实为具体执行句**,**不得**改写成相反结论或再要求用户「请选择」。
|
||||||
- **表单中为空或占位(如 *待填*、*骨架占位*)的项**:结合 `structured_brief`、节选与数据摘录**补全为可执行表述**;补全须与数据方向一致。**例外**:「**策略范围与前提**」中若仍无依据,**允许**保留「待确认」并**列出** A~E 类目标选项,**禁止**用虚构本品角色或目标补满。
|
- **当 `strategy_decisions_substantive` 为 true** 而部分表单项仍为空或占位:结合监测摘要与节选**补全为可执行表述**,与数据方向一致。
|
||||||
- **成稿阶段避免**:反复「请业务决策」「待确认后再定」;**不确定时**须在 §2.1 表中用「类目/细类」列 +「待业务定类」「假设:」**分类**写清,**禁止**只写泛化一句带过。
|
- **当 `strategy_decisions_substantive` 为 false 时**:适用上文「业务决策未填写时的成稿义务」,**禁止**以「请先填表」类表述搪塞全篇。
|
||||||
|
- **成稿阶段避免**:反复「请业务决策」;不确定时在 §2.1 用「类目/细类」+「假设:」「待业务确认:」**写清**,**禁止**只写泛化一句。
|
||||||
|
|
||||||
**输出结构与阅读顺序(须与 `rules_draft_markdown` 章节一致,勿另起目录)**:
|
**输出结构与阅读顺序(须与 `rules_draft_markdown` 章节一致,勿另起目录)**:
|
||||||
**策略范围与前提(生成前先对齐)** → **摘要** → **一、顾客是谁**(含人群与路径、细类讨论、本品聚焦)→ **二、产品价值与用户痛点**(**仅 §2.1 针对痛点要怎么做** 表,**勿**再设独立「痛点表/价值对表/负向归因」子节)→ **三、为什么要买「这款产品」**(**仅 §3.1 品类与时机**;**无 §3.2**,转化与价带应对已在 §2 表内则**勿重复**)→ **四、为什么要选「这个品牌」** → **五、与其它品牌有何不同** → **六、阶段目标与路径** → **七、品牌四线**(建设·打造·运营·体验)→ **八、战术支柱**(产品/定价/促销/渠道与传播)→ **九、风险、假设与待验证** → **十、下一步与节奏**(含业务备注)→ **附录**。
|
**策略范围与前提(生成前先对齐)** → **摘要** → **一、顾客是谁**(含人群与路径、细类讨论、本品聚焦)→ **二、产品价值与用户痛点**(**仅 §2.1 针对痛点要怎么做** 表,**勿**再设独立「痛点表/价值对表/负向归因」子节)→ **三、为什么要买「这款产品」**(**仅 §3.1 品类与时机**;**无 §3.2**,转化与价带应对已在 §2 表内则**勿重复**)→ **四、为什么要选「这个品牌」** → **五、与其它品牌有何不同** → **六、阶段目标与路径** → **七、品牌四线**(建设·打造·运营·体验)→ **八、战术支柱**(产品/定价/促销/渠道与传播)→ **九、风险、假设与待验证** → **十、下一步与节奏**(含业务备注)→ **附录**。
|
||||||
@ -105,7 +147,7 @@ STRATEGY_SYSTEM = f"""你是市场策略顾问,根据**结构化监测摘要**
|
|||||||
- **§2.1 针对痛点要怎么做**(若底稿已有表头)须**填写实质内容**;全稿**动作总锚**为 §2.1。若无表,须在 **§二** 或 **§八** 用等价分条写清「痛点—动作—落地—验证」。
|
- **§2.1 针对痛点要怎么做**(若底稿已有表头)须**填写实质内容**;全稿**动作总锚**为 §2.1。若无表,须在 **§二** 或 **§八** 用等价分条写清「痛点—动作—落地—验证」。
|
||||||
|
|
||||||
**分节要求(与底稿章节一一对应,勿省略)**:
|
**分节要求(与底稿章节一一对应,勿省略)**:
|
||||||
- **策略范围与前提**:回答「**这份策略是针对什么做的**」(监测任务、本品角色、战场、主推类目、**本阶段目标类型**、时间、成功标准)。与 `strategy_decisions`、`business_notes` 对齐;表格占位未填时**禁止**编造决策,应保留「待确认」并**列出**可选目标类型(见 `STRATEGY_USER_PREFIX` 与规划文档「启动前」);可写「建议选项:…」供业务勾选。**禁止**与后文 §2.1、§六 自相矛盾。
|
- **策略范围与前提**:回答「**这份策略是针对什么做的**」(监测任务、本品角色、战场、主推类目、**本阶段目标类型**、时间、成功标准)。与业务表单及备注对齐;`strategy_decisions_substantive` 为 false 时须写清假设前提与推荐目标类型(可含 A~E 选项),为 true 时未填项不得与已填决策矛盾。**禁止**与后文 §2.1、§六 自相矛盾。
|
||||||
- **摘要**:除范围样本外,**阶段重点**须含 1~2 条**可执行动作**,且能回扣 §2 中的优先痛点(非空泛「加强运营」);须**承接**上文「策略范围与前提」已定的边界。
|
- **摘要**:除范围样本外,**阶段重点**须含 1~2 条**可执行动作**,且能回扣 §2 中的优先痛点(非空泛「加强运营」);须**承接**上文「策略范围与前提」已定的边界。
|
||||||
- **一、顾客是谁**:**禁止**重复报告中的细类词频、分品类样本量展开、文本挖掘方法;用 **少量结论句**(谁搜、关心什么)即可,**1.3 本品聚焦**须写清「本期主攻人群/场景」与 **§2.1** 的对应关系。
|
- **一、顾客是谁**:**禁止**重复报告中的细类词频、分品类样本量展开、文本挖掘方法;用 **少量结论句**(谁搜、关心什么)即可,**1.3 本品聚焦**须写清「本期主攻人群/场景」与 **§2.1** 的对应关系。
|
||||||
- **二**:**仅 §2.1** 一张表:「类目/细类(本决策适用)| 用户痛点(简述)| 策略动作 | 具体怎么做 | 如何验证」。须覆盖监测已支撑的主要维度(**按类目分行**,口感/质地分线、分量/规格、信任与价格等,依数据取舍);**类目列 + 痛点简述列**遵守「§2 表」条款。**禁止**再写独立「痛点与证据表」「价值对表」「负向归因」子节(与 §二 重复的内容一律并入本表或删去)。
|
- **二**:**仅 §2.1** 一张表:「类目/细类(本决策适用)| 用户痛点(简述)| 策略动作 | 具体怎么做 | 如何验证」。须覆盖监测已支撑的主要维度(**按类目分行**,口感/质地分线、分量/规格、信任与价格等,依数据取舍);**类目列 + 痛点简述列**遵守「§2 表」条款。**禁止**再写独立「痛点与证据表」「价值对表」「负向归因」子节(与 §二 重复的内容一律并入本表或删去)。
|
||||||
@ -138,9 +180,11 @@ STRATEGY_SYSTEM = f"""你是市场策略顾问,根据**结构化监测摘要**
|
|||||||
**输出**:仅 Markdown 正文(不要 ``` 围栏);须收束各小节与全文,勿中途截断。"""
|
**输出**:仅 Markdown 正文(不要 ``` 围栏);须收束各小节与全文,勿中途截断。"""
|
||||||
|
|
||||||
STRATEGY_USER_PREFIX = (
|
STRATEGY_USER_PREFIX = (
|
||||||
"请基于以下 JSON 输出最终策略稿(Markdown)。"
|
"请基于以下 JSON 输出最终策略稿(Markdown),正文须为对外可读正式文档,不得泄露 JSON 键名、字段名、源码路径或底稿中的编写提示语。\n"
|
||||||
"输出前自检:全文不得包含输入 JSON 中未出现的具体数字、品牌/店铺名、用户引语与活动规则;不确定处须写「假设」「输入未体现」或「待核对」。"
|
"输出前自检:全文不得包含输入中未出现的具体数字、品牌/店铺名、用户引语与活动规则;不确定处须写「假设」「监测未体现」或「待业务核对」。\n"
|
||||||
"「策略范围与前提」一节:若 `strategy_decisions` 中本品角色、战场、成功标准、主推类目等大量为空,**不得**编造;须列「待确认」并给出**目标类型选项**供业务选择,例如:**A** 上市/首发前验证 **B** 份额追赶 **C** 利润与价盘防守 **D** 新场景/新人群拓展 **E** 其它(简述)——具体措辞见项目规划文档「策略生成-框架确定」之「启动前」。\n\n"
|
"若 JSON 中 `strategy_decisions_substantive` 为 false:你须基于监测摘要与细类报告节选**主动推断**完整策略草案(含 §2.1 多行实质内容),"
|
||||||
|
"在「策略范围与前提」标明假设前提,并对阶段目标给出 A~E 类型选项及**推荐倾向**;禁止全文停留在待填占位。\n"
|
||||||
|
"若 `strategy_decisions_substantive` 为 true:已填表单项视为已定须落实;空项结合数据补全,并与后文一致。\n\n"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -171,6 +215,7 @@ def generate_strategy_draft_markdown_llm(
|
|||||||
generated_at_iso=generated_at_iso,
|
generated_at_iso=generated_at_iso,
|
||||||
strategy_decisions=strategy_decisions,
|
strategy_decisions=strategy_decisions,
|
||||||
report_config=report_config,
|
report_config=report_config,
|
||||||
|
for_llm_input=True,
|
||||||
)
|
)
|
||||||
excerpt_raw = (report_strategy_excerpt or "").strip()
|
excerpt_raw = (report_strategy_excerpt or "").strip()
|
||||||
group_evidence_raw = (report_matrix_group_evidence_md or "").strip()
|
group_evidence_raw = (report_matrix_group_evidence_md or "").strip()
|
||||||
@ -208,6 +253,9 @@ def generate_strategy_draft_markdown_llm(
|
|||||||
"keyword": keyword,
|
"keyword": keyword,
|
||||||
"generated_at_iso": generated_at_iso,
|
"generated_at_iso": generated_at_iso,
|
||||||
"strategy_decisions": strategy_decisions,
|
"strategy_decisions": strategy_decisions,
|
||||||
|
"strategy_decisions_substantive": strategy_decisions_substantive(
|
||||||
|
strategy_decisions
|
||||||
|
),
|
||||||
"business_notes": business_notes,
|
"business_notes": business_notes,
|
||||||
"structured_brief": compact,
|
"structured_brief": compact,
|
||||||
"rules_draft_markdown": rd,
|
"rules_draft_markdown": rd,
|
||||||
|
|||||||
@ -162,22 +162,37 @@ def build_strategy_draft_markdown(
|
|||||||
generated_at_iso: str = "",
|
generated_at_iso: str = "",
|
||||||
strategy_decisions: dict[str, Any] | None = None,
|
strategy_decisions: dict[str, Any] | None = None,
|
||||||
report_config: dict[str, Any] | None = None,
|
report_config: dict[str, Any] | None = None,
|
||||||
|
for_llm_input: bool = False,
|
||||||
) -> str:
|
) -> str:
|
||||||
"""生成可下载的 Markdown:与「六主轴 + 品牌四线」示例稿同构的规则骨架,附录为数据速览。"""
|
"""生成可下载的 Markdown:与「六主轴 + 品牌四线」示例稿同构的规则骨架,附录为数据速览。
|
||||||
|
|
||||||
|
``for_llm_input=True`` 时供大模型归纳用:弱化源码/路径/任务 ID 等痕迹,减少对外成稿误复述。
|
||||||
|
"""
|
||||||
use_ch8_probe = report_uses_chapter8_text_mining_probe(report_config)
|
use_ch8_probe = report_uses_chapter8_text_mining_probe(report_config)
|
||||||
d = strategy_decisions or {}
|
d = strategy_decisions or {}
|
||||||
pos = _esc(d.get("positioning_choice") or "").strip()
|
pos = _esc(d.get("positioning_choice") or "").strip()
|
||||||
kw = _esc(brief.get("keyword")) or _esc(keyword) or "—"
|
kw = _esc(brief.get("keyword")) or _esc(keyword) or "—"
|
||||||
batch = _esc(brief.get("batch_label")) or "—"
|
batch = _esc(brief.get("batch_label")) or "—"
|
||||||
lines: list[str] = [
|
if for_llm_input:
|
||||||
f"# 市场策略制定草稿 · 「{kw}」",
|
lines: list[str] = [
|
||||||
"",
|
f"# 「{kw}」",
|
||||||
"> **骨架说明**:本页为规则骨架;结构与 [`docs/demo`](docs/demo) 示例一致。**全局禁止编造**见 `generate_strategy.py` 中 `STRATEGY_DATA_RULES`。",
|
"",
|
||||||
"",
|
]
|
||||||
]
|
if generated_at_iso:
|
||||||
if generated_at_iso:
|
lines.append(
|
||||||
lines.append(f"> **生成时间**:{_esc(generated_at_iso)} · **任务 ID**:{job_id}")
|
f"> **生成时间**:{_esc(generated_at_iso)}(归纳用,**勿写入对外成稿正文**)"
|
||||||
lines.append("")
|
)
|
||||||
|
lines.append("")
|
||||||
|
else:
|
||||||
|
lines = [
|
||||||
|
f"# 市场策略制定草稿 · 「{kw}」",
|
||||||
|
"",
|
||||||
|
"> **骨架说明**:本页为规则骨架;结构与 [`docs/demo`](docs/demo) 示例一致。**全局禁止编造**见 `generate_strategy.py` 中 `STRATEGY_DATA_RULES`。",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
if generated_at_iso:
|
||||||
|
lines.append(f"> **生成时间**:{_esc(generated_at_iso)} · **任务 ID**:{job_id}")
|
||||||
|
lines.append("")
|
||||||
|
|
||||||
scope = brief.get("scope") or {}
|
scope = brief.get("scope") or {}
|
||||||
merged_n = scope.get("merged_sku_count")
|
merged_n = scope.get("merged_sku_count")
|
||||||
@ -192,13 +207,24 @@ def build_strategy_draft_markdown(
|
|||||||
def _scope_cell(val: str, placeholder: str) -> str:
|
def _scope_cell(val: str, placeholder: str) -> str:
|
||||||
return val if val else f"*({placeholder})*"
|
return val if val else f"*({placeholder})*"
|
||||||
|
|
||||||
|
scope_prelude = (
|
||||||
|
[
|
||||||
|
"*回答:**这份策略是针对什么、在什么边界里做的**——属「立项靶心」,不是执行摘要。表单已填则写成短句;未填保留占位,**勿**编造。*",
|
||||||
|
"*业务侧在动策略稿之前,应先对齐本节;可选目标类型见 `docs/planning/策略生成-框架确定.md`「启动前:须先明确的项」。*",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
if not for_llm_input
|
||||||
|
else [
|
||||||
|
"*监测任务边界与业务角色:表单已填则写成短句;未填时由归纳稿基于监测摘要提出**假设性**表述。*",
|
||||||
|
"*阶段目标类型可列 A~E 备选(上市验证 / 份额追赶 / 价盘防守 / 新场景或新人群 / 其它),并写明推荐倾向。*",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"## 策略范围与前提(生成前先对齐)",
|
"## 策略范围与前提(生成前先对齐)",
|
||||||
"",
|
"",
|
||||||
"*回答:**这份策略是针对什么、在什么边界里做的**——属「立项靶心」,不是执行摘要。表单已填则写成短句;未填保留占位,**勿**编造。*",
|
*scope_prelude,
|
||||||
"*业务侧在动策略稿之前,应先对齐本节;可选目标类型见 `docs/planning/策略生成-框架确定.md`「启动前:须先明确的项」。*",
|
|
||||||
"",
|
|
||||||
"| 须明确项 | 填写或待确认 |",
|
"| 须明确项 | 填写或待确认 |",
|
||||||
"|----------|----------------|",
|
"|----------|----------------|",
|
||||||
f"| **监测任务(数据同源)** | 关键词「{kw}」;批次 **{batch}**;与同任务《竞品分析报告》一致 |",
|
f"| **监测任务(数据同源)** | 关键词「{kw}」;批次 **{batch}**;与同任务《竞品分析报告》一致 |",
|
||||||
@ -296,7 +322,11 @@ def build_strategy_draft_markdown(
|
|||||||
"",
|
"",
|
||||||
"*本节**仅**用下表写清**针对痛点要怎么做**(**类目** + 痛点简述 + 动作 + 落地 + 验证)。**不再**单设「痛点表 / 价值对表 / 负向归因」子节,避免与 §三、§八重复。*",
|
"*本节**仅**用下表写清**针对痛点要怎么做**(**类目** + 痛点简述 + 动作 + 落地 + 验证)。**不再**单设「痛点表 / 价值对表 / 负向归因」子节,避免与 §三、§八重复。*",
|
||||||
"",
|
"",
|
||||||
"*「用户痛点(简述)」须与 `structured_brief` / 策略线索 / 报告节选**可核对**;**禁止**编造「用户反馈『……』」式引语,除非原句已出现在上述输入中。*",
|
(
|
||||||
|
"*「用户痛点(简述)」须与监测摘要 / 策略线索 / 报告节选**可核对**;**禁止**编造「用户反馈『……』」式引语,除非原句已出现在上述输入中。*"
|
||||||
|
if for_llm_input
|
||||||
|
else "*「用户痛点(简述)」须与 `structured_brief` / 策略线索 / 报告节选**可核对**;**禁止**编造「用户反馈『……』」式引语,除非原句已出现在上述输入中。*"
|
||||||
|
),
|
||||||
"",
|
"",
|
||||||
"*「类目/细类」列:写明本行决策**适用于哪一类**(如饼干/面包/全检索池);多细类须**分行**,**禁止**用一句「全站」覆盖彼此冲突的策略;类目未定可写「待业务定类」并附分类假设。*",
|
"*「类目/细类」列:写明本行决策**适用于哪一类**(如饼干/面包/全检索池);多细类须**分行**,**禁止**用一句「全站」覆盖彼此冲突的策略;类目未定可写「待业务定类」并附分类假设。*",
|
||||||
"",
|
"",
|
||||||
@ -317,7 +347,9 @@ def build_strategy_draft_markdown(
|
|||||||
else (list(raw_hints) if isinstance(raw_hints, list) else [])
|
else (list(raw_hints) if isinstance(raw_hints, list) else [])
|
||||||
)
|
)
|
||||||
if hints:
|
if hints:
|
||||||
lines.append("**摘要自动线索(`strategy_hints`)**")
|
lines.append(
|
||||||
|
"**监测摘要自动线索**" if for_llm_input else "**摘要自动线索(`strategy_hints`)**"
|
||||||
|
)
|
||||||
lines.append("")
|
lines.append("")
|
||||||
for h in hints:
|
for h in hints:
|
||||||
lines.append(f"- {_esc(h)}")
|
lines.append(f"- {_esc(h)}")
|
||||||
@ -335,7 +367,9 @@ def build_strategy_draft_markdown(
|
|||||||
raw = brief.get("pc_search_raw") or {}
|
raw = brief.get("pc_search_raw") or {}
|
||||||
if raw.get("result_count_consensus") is not None:
|
if raw.get("result_count_consensus") is not None:
|
||||||
lines.append(
|
lines.append(
|
||||||
f"- **列表申报规模(resultCount)**:{_num(raw.get('result_count_consensus'))}(非销售额)"
|
f"- **平台申报检索规模**:{_num(raw.get('result_count_consensus'))}(站内匹配条数量级,非销售额)"
|
||||||
|
if for_llm_input
|
||||||
|
else f"- **列表申报规模(resultCount)**:{_num(raw.get('result_count_consensus'))}(非销售额)"
|
||||||
)
|
)
|
||||||
elif merged_n is not None:
|
elif merged_n is not None:
|
||||||
lines.append(f"- **深入样本 SKU 数**:{_num(merged_n)}")
|
lines.append(f"- **深入样本 SKU 数**:{_num(merged_n)}")
|
||||||
@ -344,9 +378,10 @@ def build_strategy_draft_markdown(
|
|||||||
lines.append("")
|
lines.append("")
|
||||||
if pst.get("n"):
|
if pst.get("n"):
|
||||||
src = _esc(brief.get("price_stats_source")) or "—"
|
src = _esc(brief.get("price_stats_source")) or "—"
|
||||||
|
src_disp = "本监测样本" if for_llm_input and src == "strategy_scope_matrix_group_skus" else src
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
f"- **价格摘录(可支撑购买理由与锚点)**:来源 {src},n = {_num(pst.get('n'))};"
|
f"- **价格摘录(可支撑购买理由与锚点)**:来源 {src_disp},n = {_num(pst.get('n'))};"
|
||||||
f"区间 {_num(pst.get('min'))}~{_num(pst.get('max'))};中位数 {_num(pst.get('median'))}。",
|
f"区间 {_num(pst.get('min'))}~{_num(pst.get('max'))};中位数 {_num(pst.get('median'))}。",
|
||||||
"",
|
"",
|
||||||
]
|
]
|
||||||
@ -517,10 +552,15 @@ def build_strategy_draft_markdown(
|
|||||||
if use_ch8_probe:
|
if use_ch8_probe:
|
||||||
pst_sig = brief.get("price_promotion_signals") or {}
|
pst_sig = brief.get("price_promotion_signals") or {}
|
||||||
has_promo = isinstance(pst_sig, dict) and bool(pst_sig)
|
has_promo = isinstance(pst_sig, dict) and bool(pst_sig)
|
||||||
|
promo_hint = (
|
||||||
|
"*促销与活动线索:须与摘要中的价差与活动信号及报告第六章已有归纳一致;无则勿编造具体满减门槛。*"
|
||||||
|
if for_llm_input
|
||||||
|
else "*促销与活动线索:须与摘要 `price_promotion_signals` 及第六章/第九章已有归纳一致;无则勿编造具体满减门槛。*"
|
||||||
|
)
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"",
|
"",
|
||||||
"*促销与活动线索:须与摘要 `price_promotion_signals` 及第六章/第九章已有归纳一致;无则勿编造具体满减门槛。*"
|
promo_hint
|
||||||
if has_promo
|
if has_promo
|
||||||
else "*促销与价差:若摘要或价格信号有归纳则承接;无则勿编造。*",
|
else "*促销与价差:若摘要或价格信号有归纳则承接;无则勿编造。*",
|
||||||
"",
|
"",
|
||||||
@ -544,7 +584,11 @@ def build_strategy_draft_markdown(
|
|||||||
"### 8.3 促销与活动策略",
|
"### 8.3 促销与活动策略",
|
||||||
"",
|
"",
|
||||||
"*须写促销**原则**(券/到手价/跟价节奏);**满减、满折、跨店**等:能引用的写清来源;监测未捕获具体门槛时写「待与运营/后台对齐」,**勿**整节留空,**勿**编造门槛数字。*",
|
"*须写促销**原则**(券/到手价/跟价节奏);**满减、满折、跨店**等:能引用的写清来源;监测未捕获具体门槛时写「待与运营/后台对齐」,**勿**整节留空,**勿**编造门槛数字。*",
|
||||||
"*与 `price_promotion_signals`、报告第六章一致;勿虚构活动。*",
|
(
|
||||||
|
"*与监测摘要中的促销与价差信号、报告第六章一致;勿虚构活动。*"
|
||||||
|
if for_llm_input
|
||||||
|
else "*与 `price_promotion_signals`、报告第六章一致;勿虚构活动。*"
|
||||||
|
),
|
||||||
"",
|
"",
|
||||||
"### 8.4 渠道与传播",
|
"### 8.4 渠道与传播",
|
||||||
"",
|
"",
|
||||||
@ -602,27 +646,54 @@ def build_strategy_draft_markdown(
|
|||||||
"scenario_filter_enabled": "场景筛选",
|
"scenario_filter_enabled": "场景筛选",
|
||||||
}
|
}
|
||||||
if isinstance(meta, dict) and meta:
|
if isinstance(meta, dict) and meta:
|
||||||
bits = []
|
if for_llm_input:
|
||||||
for k in ("page_start", "page_to", "max_skus_config", "scenario_filter_enabled"):
|
bits_llm: list[str] = []
|
||||||
if k in meta:
|
ps, pt = meta.get("page_start"), meta.get("page_to")
|
||||||
label = meta_labels.get(k, k)
|
if ps is not None and pt is not None:
|
||||||
bits.append(f"{label}={_esc(meta.get(k))}")
|
bits_llm.append(f"列表页约第 {_esc(ps)}~{_esc(pt)} 页")
|
||||||
if bits:
|
elif ps is not None:
|
||||||
lines.append(f"- **采集参数快照**:{'; '.join(bits)}")
|
bits_llm.append(f"列表自第 {_esc(ps)} 页起采集")
|
||||||
|
if meta.get("max_skus_config") is not None:
|
||||||
|
bits_llm.append(f"深入样本上限约 {_num(meta.get('max_skus_config'))} 个 SKU")
|
||||||
|
if meta.get("scenario_filter_enabled"):
|
||||||
|
bits_llm.append("已启用场景筛选")
|
||||||
|
if bits_llm:
|
||||||
|
lines.append(f"- **采集范围**:{';'.join(bits_llm)}")
|
||||||
|
else:
|
||||||
|
bits = []
|
||||||
|
for k in ("page_start", "page_to", "max_skus_config", "scenario_filter_enabled"):
|
||||||
|
if k in meta:
|
||||||
|
label = meta_labels.get(k, k)
|
||||||
|
bits.append(f"{label}={_esc(meta.get(k))}")
|
||||||
|
if bits:
|
||||||
|
lines.append(f"- **采集参数快照**:{'; '.join(bits)}")
|
||||||
raw = brief.get("pc_search_raw") or {}
|
raw = brief.get("pc_search_raw") or {}
|
||||||
if raw.get("result_count_consensus") is not None:
|
if raw.get("result_count_consensus") is not None:
|
||||||
lines.append(
|
lines.append(
|
||||||
f"- **列表申报规模(resultCount)**:{_num(raw.get('result_count_consensus'))}"
|
f"- **平台申报检索规模**:{_num(raw.get('result_count_consensus'))}"
|
||||||
|
if for_llm_input
|
||||||
|
else f"- **列表申报规模(resultCount)**:{_num(raw.get('result_count_consensus'))}"
|
||||||
|
)
|
||||||
|
if for_llm_input:
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"*可与同任务《竞品分析报告》及本批次数据表对照核验。*",
|
||||||
|
"",
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
"*同目录含本批次 CSV 与分析产出,可对照使用。*",
|
||||||
|
"",
|
||||||
|
"---",
|
||||||
|
"",
|
||||||
|
"*本稿由工作台「市场策略制定」生成;与同任务结构化分析数据一致。*",
|
||||||
|
"",
|
||||||
|
]
|
||||||
)
|
)
|
||||||
lines.extend(
|
|
||||||
[
|
|
||||||
"",
|
|
||||||
"*同目录含本批次 CSV 与分析产出,可对照使用。*",
|
|
||||||
"",
|
|
||||||
"---",
|
|
||||||
"",
|
|
||||||
"*本稿由工作台「市场策略制定」生成;与同任务结构化分析数据一致。*",
|
|
||||||
"",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
return "\n".join(lines)
|
return "\n".join(lines)
|
||||||
|
|||||||
@ -4,10 +4,45 @@ from __future__ import annotations
|
|||||||
from django.test import SimpleTestCase
|
from django.test import SimpleTestCase
|
||||||
|
|
||||||
from pipeline.llm.generate_strategy import _omit_ch8_probe_wordchart_fields
|
from pipeline.llm.generate_strategy import _omit_ch8_probe_wordchart_fields
|
||||||
|
from pipeline.llm.generate_strategy import strategy_decisions_substantive
|
||||||
from pipeline.reporting.strategy_draft import build_strategy_draft_markdown
|
from pipeline.reporting.strategy_draft import build_strategy_draft_markdown
|
||||||
|
|
||||||
|
|
||||||
class StrategyDraftTests(SimpleTestCase):
|
class StrategyDraftTests(SimpleTestCase):
|
||||||
|
def test_strategy_decisions_substantive(self) -> None:
|
||||||
|
self.assertFalse(strategy_decisions_substantive(None))
|
||||||
|
self.assertFalse(strategy_decisions_substantive({}))
|
||||||
|
self.assertFalse(
|
||||||
|
strategy_decisions_substantive(
|
||||||
|
{"ack_risk_keywords": True, "product_role": " "}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
self.assertTrue(
|
||||||
|
strategy_decisions_substantive({"product_role": "新品"})
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_for_llm_input_omits_dev_traces(self) -> None:
|
||||||
|
brief = {
|
||||||
|
"schema_version": 1,
|
||||||
|
"keyword": "K",
|
||||||
|
"batch_label": "b1",
|
||||||
|
"scope": {"merged_sku_count": 2},
|
||||||
|
"strategy_hints": ["线索1"],
|
||||||
|
"meta": {"page_start": 1, "page_to": 3, "max_skus_config": 100},
|
||||||
|
}
|
||||||
|
md = build_strategy_draft_markdown(
|
||||||
|
job_id=7,
|
||||||
|
keyword="K",
|
||||||
|
brief=brief,
|
||||||
|
generated_at_iso="2026-01-01",
|
||||||
|
for_llm_input=True,
|
||||||
|
)
|
||||||
|
self.assertNotIn("任务 ID", md)
|
||||||
|
self.assertNotIn("generate_strategy.py", md)
|
||||||
|
self.assertNotIn("strategy_hints", md)
|
||||||
|
self.assertIn("监测摘要自动线索", md)
|
||||||
|
self.assertIn("列表页约第 1~3 页", md)
|
||||||
|
|
||||||
def test_build_contains_sections_and_notes(self) -> None:
|
def test_build_contains_sections_and_notes(self) -> None:
|
||||||
brief = {
|
brief = {
|
||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
@ -65,7 +100,7 @@ class StrategyDraftTests(SimpleTestCase):
|
|||||||
self.assertIn("侧翼切入", md)
|
self.assertIn("侧翼切入", md)
|
||||||
self.assertIn("做低糖配方", md)
|
self.assertIn("做低糖配方", md)
|
||||||
self.assertIn("### 7.1 品牌建设", md)
|
self.assertIn("### 7.1 品牌建设", md)
|
||||||
self.assertIn("- [x] 关注词/场景统计是否以偏概全", md)
|
self.assertIn("- [x] 评论侧归纳是否以偏概全", md)
|
||||||
self.assertIn("- [ ] 价格带是否含大促", md)
|
self.assertIn("- [ ] 价格带是否含大促", md)
|
||||||
self.assertIn("- [x] 列表集中度与深入样本品牌是否不一致", md)
|
self.assertIn("- [x] 列表集中度与深入样本品牌是否不一致", md)
|
||||||
|
|
||||||
@ -92,7 +127,7 @@ class StrategyDraftTests(SimpleTestCase):
|
|||||||
self.assertNotIn("子串统计命中约 **501**", md)
|
self.assertNotIn("子串统计命中约 **501**", md)
|
||||||
self.assertNotIn("场景「控糖", md)
|
self.assertNotIn("场景「控糖", md)
|
||||||
|
|
||||||
def test_legacy_report_shows_focus_scenario_bullets(self) -> None:
|
def test_non_probe_path_no_preset_focus_enumeration(self) -> None:
|
||||||
brief = {
|
brief = {
|
||||||
"schema_version": 1,
|
"schema_version": 1,
|
||||||
"keyword": "低GI",
|
"keyword": "低GI",
|
||||||
@ -111,8 +146,10 @@ class StrategyDraftTests(SimpleTestCase):
|
|||||||
brief=brief,
|
brief=brief,
|
||||||
report_config={"chapter8_text_mining_probe": False},
|
report_config={"chapter8_text_mining_probe": False},
|
||||||
)
|
)
|
||||||
self.assertIn("「口感」:子串统计命中约 **501** 次", md)
|
self.assertIn("不再", md)
|
||||||
self.assertIn("场景「控糖/血糖相关」:约 **305** 条", md)
|
self.assertIn("预设关注词", md)
|
||||||
|
self.assertNotIn("子串统计命中约 **501**", md)
|
||||||
|
self.assertNotIn("场景「控糖", md)
|
||||||
|
|
||||||
def test_shops_unique_sku_basis_rendered(self) -> None:
|
def test_shops_unique_sku_basis_rendered(self) -> None:
|
||||||
brief = {
|
brief = {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user