mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-21 23:41:39 +08:00
feat(pipeline): 策略表单增加营销与总体策略并顺延规则稿章节
- API:marketing_strategy、general_strategy;规则骨架新增「二、营销策略与总体方向」,原 §2–§9 顺延为 §3–§10。 - LLM 提示与探针说明用语同步;OpenAPI、单测与策略生成页字段集「五」已对齐。 Made-with: Cursor
This commit is contained in:
parent
16d64565a3
commit
f09a46a79e
@ -17,7 +17,7 @@ STRATEGY_SYSTEM = """你是市场策略顾问,根据**结构化监测摘要**
|
|||||||
**输入**:`rules_draft_markdown`(规则骨架,与同任务数据一致)、`structured_brief`(摘要子集)、`strategy_decisions`、`business_notes`;可选 `report_strategy_excerpt`(与同任务宿主报告**第九章**「策略与机会」正文同源)。
|
**输入**:`rules_draft_markdown`(规则骨架,与同任务数据一致)、`structured_brief`(摘要子集)、`strategy_decisions`、`business_notes`;可选 `report_strategy_excerpt`(与同任务宿主报告**第九章**「策略与机会」正文同源)。
|
||||||
|
|
||||||
**决策边界(硬性)**:
|
**决策边界(硬性)**:
|
||||||
- **业务已在 `strategy_decisions` 中填写的项**(角色、时间、成功标准、战场一句话、定位勾选、竞争倾向、四柱、目标客群/对标/资源备注等)视为**已定决策**:成稿须**落实为具体执行句**,**不得**改写成相反结论或再要求用户「请选择」。
|
- **业务已在 `strategy_decisions` 中填写的项**(角色、时间、成功标准、战场一句话、定位勾选、竞争倾向、四柱、目标客群/对标/资源备注、**营销策略**与**总体策略**等)视为**已定决策**:成稿须**落实为具体执行句**,**不得**改写成相反结论或再要求用户「请选择」。
|
||||||
- **表单中为空或占位(如 *待填*、*骨架占位*)的项**:由你结合 `structured_brief`、`report_strategy_excerpt`(若非空)与数据摘录**补全为可执行表述**;补全须与数据方向一致,**不得**编造输入中不存在的销量、GMV、未给出的价格或占比。
|
- **表单中为空或占位(如 *待填*、*骨架占位*)的项**:由你结合 `structured_brief`、`report_strategy_excerpt`(若非空)与数据摘录**补全为可执行表述**;补全须与数据方向一致,**不得**编造输入中不存在的销量、GMV、未给出的价格或占比。
|
||||||
- **成稿阶段禁止**:再写一轮「请业务决策」「待确认后再定」「假设:待验证」等**二次决策话术**;不确定性用一句带过即可(如「需下周用原评论抽样核对」),勿重复堆砌。
|
- **成稿阶段禁止**:再写一轮「请业务决策」「待确认后再定」「假设:待验证」等**二次决策话术**;不确定性用一句带过即可(如「需下周用原评论抽样核对」),勿重复堆砌。
|
||||||
|
|
||||||
@ -101,7 +101,7 @@ def generate_strategy_draft_markdown_llm(
|
|||||||
if report_uses_chapter8_text_mining_probe(report_config):
|
if report_uses_chapter8_text_mining_probe(report_config):
|
||||||
payload["structured_brief_omission_note"] = (
|
payload["structured_brief_omission_note"] = (
|
||||||
"已启用第八章文本挖掘:structured_brief 已省略关注词/场景子串计数字段,避免与报告正文口径冲突。"
|
"已启用第八章文本挖掘:structured_brief 已省略关注词/场景子串计数字段,避免与报告正文口径冲突。"
|
||||||
"请依报告 §8 与摘要其他字段,将底稿第五节写成具体执行要点(需求焦点、场景、传播),勿逐条编造子串命中列表。"
|
"请依报告 §8 与摘要其他字段,将底稿「用户与评论侧」一节写成具体执行要点(需求焦点、场景、传播),勿逐条编造子串命中列表。"
|
||||||
)
|
)
|
||||||
raw = json.dumps(payload, ensure_ascii=False)
|
raw = json.dumps(payload, ensure_ascii=False)
|
||||||
if len(raw) > 500_000:
|
if len(raw) > 500_000:
|
||||||
|
|||||||
@ -102,7 +102,7 @@ def report_uses_chapter8_text_mining_probe(report_config: dict[str, Any] | None)
|
|||||||
"""
|
"""
|
||||||
与任务 ``report_config`` 中 ``chapter8_text_mining_probe`` 一致;未显式设置时默认 ``True``
|
与任务 ``report_config`` 中 ``chapter8_text_mining_probe`` 一致;未显式设置时默认 ``True``
|
||||||
(与 ``jd.runner.get_default_report_config`` 一致)。
|
(与 ``jd.runner.get_default_report_config`` 一致)。
|
||||||
为 ``True`` 时,策略稿第五节不再逐条列举关注词/场景子串命中,以免与当前报告正文口径冲突。
|
为 ``True`` 时,策略稿「用户与评论侧」一节不再逐条列举关注词/场景子串命中,以免与当前报告正文口径冲突。
|
||||||
"""
|
"""
|
||||||
if not isinstance(report_config, dict):
|
if not isinstance(report_config, dict):
|
||||||
return True
|
return True
|
||||||
@ -170,12 +170,30 @@ def build_strategy_draft_markdown(
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
|
lines.extend(
|
||||||
|
[
|
||||||
|
"## 二、营销策略与总体方向",
|
||||||
|
"",
|
||||||
|
_goal_bullet(
|
||||||
|
"营销策略",
|
||||||
|
str(d.get("marketing_strategy") or ""),
|
||||||
|
"传播、活动、投放、内容主线等(可选)",
|
||||||
|
),
|
||||||
|
_goal_bullet(
|
||||||
|
"总体策略",
|
||||||
|
str(d.get("general_strategy") or ""),
|
||||||
|
"增长/品类/经营层面的总原则,可与 4P 支柱呼应(可选)",
|
||||||
|
),
|
||||||
|
"",
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
scope = brief.get("scope") or {}
|
scope = brief.get("scope") or {}
|
||||||
merged_n = scope.get("merged_sku_count")
|
merged_n = scope.get("merged_sku_count")
|
||||||
comm_n = scope.get("comment_flat_rows")
|
comm_n = scope.get("comment_flat_rows")
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"## 二、战场与样本",
|
"## 三、战场与样本",
|
||||||
"",
|
"",
|
||||||
f"- **监测关键词 / 货架语境**:{kw}",
|
f"- **监测关键词 / 货架语境**:{kw}",
|
||||||
f"- **批次**:{_esc(brief.get('batch_label')) or '—'}",
|
f"- **批次**:{_esc(brief.get('batch_label')) or '—'}",
|
||||||
@ -197,7 +215,7 @@ def build_strategy_draft_markdown(
|
|||||||
conc = brief.get("concentration") or {}
|
conc = brief.get("concentration") or {}
|
||||||
shops = conc.get("shops_from_list") or {}
|
shops = conc.get("shops_from_list") or {}
|
||||||
dbrand = conc.get("detail_brand_among_merged") or {}
|
dbrand = conc.get("detail_brand_among_merged") or {}
|
||||||
lines.extend(["## 三、竞争结构(摘录)", ""])
|
lines.extend(["## 四、竞争结构(摘录)", ""])
|
||||||
n_shop = _cr_narrative(
|
n_shop = _cr_narrative(
|
||||||
"列表侧店铺集中度",
|
"列表侧店铺集中度",
|
||||||
concentration_first_share(shops),
|
concentration_first_share(shops),
|
||||||
@ -244,7 +262,7 @@ def build_strategy_draft_markdown(
|
|||||||
lines.append("")
|
lines.append("")
|
||||||
|
|
||||||
pst = brief.get("price_stats") or {}
|
pst = brief.get("price_stats") or {}
|
||||||
lines.extend(["## 四、价格带与定位(业务已选)", ""])
|
lines.extend(["## 五、价格带与定位(业务已选)", ""])
|
||||||
if pst.get("n"):
|
if pst.get("n"):
|
||||||
src = _esc(brief.get("price_stats_source")) or "—"
|
src = _esc(brief.get("price_stats_source")) or "—"
|
||||||
lines.extend(
|
lines.extend(
|
||||||
@ -278,7 +296,7 @@ def build_strategy_draft_markdown(
|
|||||||
|
|
||||||
ckw = brief.get("comment_focus_keywords") or []
|
ckw = brief.get("comment_focus_keywords") or []
|
||||||
usc = brief.get("usage_scenarios") or []
|
usc = brief.get("usage_scenarios") or []
|
||||||
lines.extend(["## 五、用户与评论侧", ""])
|
lines.extend(["## 六、用户与评论侧", ""])
|
||||||
if use_ch8_probe:
|
if use_ch8_probe:
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
@ -317,7 +335,7 @@ def build_strategy_draft_markdown(
|
|||||||
hints = brief.get("strategy_hints") or []
|
hints = brief.get("strategy_hints") or []
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"## 六、机会与策略支柱",
|
"## 七、机会与策略支柱",
|
||||||
"",
|
"",
|
||||||
"### 摘要提示(`strategy_hints`)",
|
"### 摘要提示(`strategy_hints`)",
|
||||||
"",
|
"",
|
||||||
@ -357,7 +375,7 @@ def build_strategy_draft_markdown(
|
|||||||
)
|
)
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"## 七、风险核对项",
|
"## 八、风险核对项",
|
||||||
"",
|
"",
|
||||||
_risk_line(rk, rk_kw),
|
_risk_line(rk, rk_kw),
|
||||||
_risk_line(rp, "价格带是否含大促/异常挂价?(需核对清洗规则)"),
|
_risk_line(rp, "价格带是否含大促/异常挂价?(需核对清洗规则)"),
|
||||||
@ -369,7 +387,7 @@ def build_strategy_draft_markdown(
|
|||||||
notes = _esc(business_notes)
|
notes = _esc(business_notes)
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"## 八、业务约束与备注",
|
"## 九、业务约束与备注",
|
||||||
"",
|
"",
|
||||||
(notes if notes else "*(未填写业务备注。)*"),
|
(notes if notes else "*(未填写业务备注。)*"),
|
||||||
"",
|
"",
|
||||||
@ -378,10 +396,10 @@ def build_strategy_draft_markdown(
|
|||||||
|
|
||||||
lines.extend(
|
lines.extend(
|
||||||
[
|
[
|
||||||
"## 九、下一步(可执行)",
|
"## 十、下一步(可执行)",
|
||||||
"",
|
"",
|
||||||
"- [ ] 对齐 **§一** 目标与 **§八** 约束,锁定 1~2 条主命题。",
|
"- [ ] 对齐 **§一** 目标与 **§九** 约束,锁定 1~2 条主命题。",
|
||||||
"- [ ] 为 **§六** 支柱各补 **1 条数据证据** + **12 周内可交付动作**(负责人 + 时间)。",
|
"- [ ] 为 **§七** 支柱各补 **1 条数据证据** + **12 周内可交付动作**(负责人 + 时间)。",
|
||||||
"",
|
"",
|
||||||
"---",
|
"---",
|
||||||
"",
|
"",
|
||||||
|
|||||||
@ -447,6 +447,12 @@ class StrategyDraftRequestSerializer(serializers.Serializer):
|
|||||||
resource_notes = serializers.CharField(
|
resource_notes = serializers.CharField(
|
||||||
required=False, allow_blank=True, default="", max_length=1000, trim_whitespace=False
|
required=False, allow_blank=True, default="", max_length=1000, trim_whitespace=False
|
||||||
)
|
)
|
||||||
|
marketing_strategy = serializers.CharField(
|
||||||
|
required=False, allow_blank=True, default="", max_length=2000, trim_whitespace=False
|
||||||
|
)
|
||||||
|
general_strategy = serializers.CharField(
|
||||||
|
required=False, allow_blank=True, default="", max_length=2000, trim_whitespace=False
|
||||||
|
)
|
||||||
ack_risk_keywords = serializers.BooleanField(required=False, default=False)
|
ack_risk_keywords = serializers.BooleanField(required=False, default=False)
|
||||||
ack_risk_price = serializers.BooleanField(required=False, default=False)
|
ack_risk_price = serializers.BooleanField(required=False, default=False)
|
||||||
ack_risk_concentration = serializers.BooleanField(required=False, default=False)
|
ack_risk_concentration = serializers.BooleanField(required=False, default=False)
|
||||||
|
|||||||
@ -27,6 +27,7 @@ class StrategyDraftTests(SimpleTestCase):
|
|||||||
self.assertIn("假设A", md)
|
self.assertIn("假设A", md)
|
||||||
self.assertIn("重点:华东", md)
|
self.assertIn("重点:华东", md)
|
||||||
self.assertIn("目标与边界", md)
|
self.assertIn("目标与边界", md)
|
||||||
|
self.assertIn("营销策略与总体方向", md)
|
||||||
self.assertIn("市场策略制定草稿", md)
|
self.assertIn("市场策略制定草稿", md)
|
||||||
|
|
||||||
def test_strategy_decisions_merge(self) -> None:
|
def test_strategy_decisions_merge(self) -> None:
|
||||||
@ -35,6 +36,8 @@ class StrategyDraftTests(SimpleTestCase):
|
|||||||
"product_role": "追赶型",
|
"product_role": "追赶型",
|
||||||
"positioning_choice": "mid",
|
"positioning_choice": "mid",
|
||||||
"competitive_stance": "flank",
|
"competitive_stance": "flank",
|
||||||
|
"marketing_strategy": "内容种草+搜索承接",
|
||||||
|
"general_strategy": "先腰后顶",
|
||||||
"pillar_product": "做低糖配方",
|
"pillar_product": "做低糖配方",
|
||||||
"ack_risk_keywords": True,
|
"ack_risk_keywords": True,
|
||||||
"ack_risk_price": False,
|
"ack_risk_price": False,
|
||||||
@ -48,6 +51,8 @@ class StrategyDraftTests(SimpleTestCase):
|
|||||||
report_config={"chapter8_text_mining_probe": False},
|
report_config={"chapter8_text_mining_probe": False},
|
||||||
)
|
)
|
||||||
self.assertIn("**本品角色**:追赶型", md)
|
self.assertIn("**本品角色**:追赶型", md)
|
||||||
|
self.assertIn("**营销策略**:内容种草+搜索承接", md)
|
||||||
|
self.assertIn("**总体策略**:先腰后顶", md)
|
||||||
self.assertIn("- [x] **卡腰**", md)
|
self.assertIn("- [x] **卡腰**", md)
|
||||||
self.assertIn("- [ ] **贴顶**", md)
|
self.assertIn("- [ ] **贴顶**", md)
|
||||||
self.assertIn("侧翼切入", md)
|
self.assertIn("侧翼切入", md)
|
||||||
|
|||||||
@ -136,6 +136,8 @@ class JobStrategyDraftView(APIView):
|
|||||||
"audience_segment": vd.get("audience_segment") or "",
|
"audience_segment": vd.get("audience_segment") or "",
|
||||||
"competitor_reference": vd.get("competitor_reference") or "",
|
"competitor_reference": vd.get("competitor_reference") or "",
|
||||||
"resource_notes": vd.get("resource_notes") or "",
|
"resource_notes": vd.get("resource_notes") or "",
|
||||||
|
"marketing_strategy": vd.get("marketing_strategy") or "",
|
||||||
|
"general_strategy": vd.get("general_strategy") or "",
|
||||||
"ack_risk_keywords": bool(vd.get("ack_risk_keywords")),
|
"ack_risk_keywords": bool(vd.get("ack_risk_keywords")),
|
||||||
"ack_risk_price": bool(vd.get("ack_risk_price")),
|
"ack_risk_price": bool(vd.get("ack_risk_price")),
|
||||||
"ack_risk_concentration": bool(vd.get("ack_risk_concentration")),
|
"ack_risk_concentration": bool(vd.get("ack_risk_concentration")),
|
||||||
|
|||||||
@ -561,6 +561,14 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
maxLength: 1000
|
maxLength: 1000
|
||||||
description: 资源与预算量级备注(可选)
|
description: 资源与预算量级备注(可选)
|
||||||
|
marketing_strategy:
|
||||||
|
type: string
|
||||||
|
maxLength: 2000
|
||||||
|
description: 营销策略(传播、活动、投放、内容等,可选)
|
||||||
|
general_strategy:
|
||||||
|
type: string
|
||||||
|
maxLength: 2000
|
||||||
|
description: 总体策略(增长/品类/经营总原则,可选)
|
||||||
ack_risk_keywords:
|
ack_risk_keywords:
|
||||||
type: boolean
|
type: boolean
|
||||||
default: false
|
default: false
|
||||||
|
|||||||
@ -51,6 +51,8 @@ const decisions = reactive({
|
|||||||
audience_segment: '',
|
audience_segment: '',
|
||||||
competitor_reference: '',
|
competitor_reference: '',
|
||||||
resource_notes: '',
|
resource_notes: '',
|
||||||
|
marketing_strategy: '',
|
||||||
|
general_strategy: '',
|
||||||
ack_risk_keywords: false,
|
ack_risk_keywords: false,
|
||||||
ack_risk_price: false,
|
ack_risk_price: false,
|
||||||
ack_risk_concentration: false,
|
ack_risk_concentration: false,
|
||||||
@ -95,6 +97,8 @@ function buildPayload() {
|
|||||||
audience_segment: decisions.audience_segment,
|
audience_segment: decisions.audience_segment,
|
||||||
competitor_reference: decisions.competitor_reference,
|
competitor_reference: decisions.competitor_reference,
|
||||||
resource_notes: decisions.resource_notes,
|
resource_notes: decisions.resource_notes,
|
||||||
|
marketing_strategy: decisions.marketing_strategy,
|
||||||
|
general_strategy: decisions.general_strategy,
|
||||||
ack_risk_keywords: decisions.ack_risk_keywords,
|
ack_risk_keywords: decisions.ack_risk_keywords,
|
||||||
ack_risk_price: decisions.ack_risk_price,
|
ack_risk_price: decisions.ack_risk_price,
|
||||||
ack_risk_concentration: decisions.ack_risk_concentration,
|
ack_risk_concentration: decisions.ack_risk_concentration,
|
||||||
@ -294,6 +298,26 @@ watch(
|
|||||||
</label>
|
</label>
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
||||||
|
<fieldset class="fieldset">
|
||||||
|
<legend>五、营销策略与总体策略</legend>
|
||||||
|
<label class="fld fld-block">
|
||||||
|
<span>营销策略</span>
|
||||||
|
<textarea
|
||||||
|
v-model="decisions.marketing_strategy"
|
||||||
|
rows="3"
|
||||||
|
placeholder="传播、活动、投放、内容主线等(可选)"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
<label class="fld fld-block">
|
||||||
|
<span>总体策略</span>
|
||||||
|
<textarea
|
||||||
|
v-model="decisions.general_strategy"
|
||||||
|
rows="3"
|
||||||
|
placeholder="增长 / 品类 / 经营总原则,可与下方 4P 支柱呼应(可选)"
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</fieldset>
|
||||||
|
|
||||||
<fieldset class="fieldset">
|
<fieldset class="fieldset">
|
||||||
<legend>六、策略支柱 — 本品打算怎么做(可先填一列)</legend>
|
<legend>六、策略支柱 — 本品打算怎么做(可先填一列)</legend>
|
||||||
<label class="fld fld-block">
|
<label class="fld fld-block">
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user