mirror of
https://github.com/primedigitaltech/market-assistant.git
synced 2026-07-21 23:41:39 +08:00
129 lines
5.0 KiB
Python
129 lines
5.0 KiB
Python
"""策略稿 → 核心信息卡 → 商详文案包(两步 LLM,JSON 输出)。"""
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
from typing import Any
|
||
|
||
from .llm_client import call_llm
|
||
|
||
_MAX_STRATEGY_CHARS = 28_000
|
||
|
||
CORE_CARD_SYSTEM = """你是电商商详与营销文案顾问。根据用户提供的「策略稿全文」与结构化决策、业务备注,输出**仅一段 UTF-8 JSON 对象**(不要 Markdown 代码围栏,不要前后说明文字)。
|
||
|
||
**硬性**:
|
||
- 事实、数字、功效、检测结论、销量、评价原文:**仅可**来自输入;**禁止**编造未出现的品牌名、数据、「用户说」引语。
|
||
- 食品/健康相关:**禁止**治疗承诺与夸大疗效;无依据写「输入未体现」或「待法务确认」。
|
||
- 句子短、可落地;兼顾**购买者决策**与商详写手可用性。
|
||
|
||
**JSON 键(须全部出现,值为字符串;无内容用空串)**:
|
||
- one_liner_value:一句话价值主张(买家能得到什么)
|
||
- buyer_job_to_be_done:购买者的任务或情境(一句)
|
||
- key_pain_or_desire:核心痛点或欲望(与策略一致)
|
||
- why_this_product:为何要选这一款(相对同类,一句)
|
||
- proof_or_trust_angle:信任或证明角度(无依据写「输入未体现」)
|
||
- differentiation_vs_alternatives:与替代方案相比的差异(一句)
|
||
- price_value_framing:价位与价值感如何表述(与策略价位可对读;无则「待业务确认」)
|
||
- compliance_taboos:表述禁区摘要(来自业务备注或策略风险)
|
||
- open_points_for_business:待业务补充(无则空串)
|
||
"""
|
||
|
||
DETAIL_PACK_SYSTEM = """你是京东商详向文案手。输入为已定稿的「核心信息卡」JSON 与关键词。请输出**仅一段 UTF-8 JSON 对象**(不要 Markdown 代码围栏)。
|
||
|
||
**硬性**:
|
||
- **仅可**依据核心信息卡展开;**禁止**新增数字、功效、认证、评价引语、竞品具体名(除非信息卡里已有)。
|
||
- 购买者视角,短句;禁止输出 JSON 键名英文给最终读者(值全部为中文商详可用文案)。
|
||
- 不要泄露「核心信息卡」「策略稿」等内部词。
|
||
|
||
**JSON 键(须全部出现)**:
|
||
- listing_titles:字符串数组,4~6 条商品短标题备选(每条约 30 字内)
|
||
- listing_subtitle:一条列表副文案(约 60 字内)
|
||
- detail_headline:商详首屏下 lead,1~2 句
|
||
- selling_bullets:字符串数组,5~8 条卖点(每条约 40 字内)
|
||
- spec_sidebar_lines:字符串数组,0~3 条参数区旁短句(可空数组)
|
||
- faq:对象数组,每项含 question、answer 字符串,3~5 组;答句不得超出信息卡承诺
|
||
"""
|
||
|
||
|
||
def _truncate_strategy(md: str) -> tuple[str, bool]:
|
||
t = (md or "").strip()
|
||
if len(t) <= _MAX_STRATEGY_CHARS:
|
||
return t, False
|
||
return t[: _MAX_STRATEGY_CHARS].rstrip() + "\n\n…(策略正文已截断,以下同)\n", True
|
||
|
||
|
||
def _parse_llm_json(raw: str) -> dict[str, Any]:
|
||
s = (raw or "").strip()
|
||
if not s:
|
||
raise ValueError("大模型返回为空")
|
||
try:
|
||
out = json.loads(s)
|
||
except json.JSONDecodeError:
|
||
i = s.find("{")
|
||
j = s.rfind("}")
|
||
if i >= 0 and j > i:
|
||
out = json.loads(s[i : j + 1])
|
||
else:
|
||
raise ValueError("大模型返回不是合法 JSON") from None
|
||
if not isinstance(out, dict):
|
||
raise ValueError("大模型 JSON 须为对象")
|
||
return out
|
||
|
||
|
||
def generate_core_info_card(
|
||
*,
|
||
keyword: str,
|
||
strategy_markdown: str,
|
||
strategy_decisions: dict[str, Any] | None,
|
||
business_notes: str,
|
||
) -> dict[str, Any]:
|
||
md, truncated = _truncate_strategy(strategy_markdown)
|
||
payload = {
|
||
"keyword": keyword,
|
||
"strategy_markdown": md,
|
||
"strategy_markdown_truncated": truncated,
|
||
"strategy_decisions": strategy_decisions or {},
|
||
"business_notes": (business_notes or "").strip(),
|
||
}
|
||
user = (
|
||
"请根据以下 JSON 输出核心信息卡(仅 JSON 对象):\n"
|
||
+ json.dumps(payload, ensure_ascii=False)
|
||
)
|
||
raw = call_llm(CORE_CARD_SYSTEM, user)
|
||
return _parse_llm_json(raw)
|
||
|
||
|
||
def generate_detail_page_pack(
|
||
*,
|
||
keyword: str,
|
||
core_info_card: dict[str, Any],
|
||
) -> dict[str, Any]:
|
||
payload = {
|
||
"keyword": keyword,
|
||
"core_info_card": core_info_card,
|
||
}
|
||
user = "请根据以下 JSON 输出商详包(仅 JSON 对象):\n" + json.dumps(
|
||
payload, ensure_ascii=False
|
||
)
|
||
raw = call_llm(DETAIL_PACK_SYSTEM, user)
|
||
return _parse_llm_json(raw)
|
||
|
||
|
||
def generate_marketing_detail_pack(
|
||
*,
|
||
keyword: str,
|
||
strategy_markdown: str,
|
||
strategy_decisions: dict[str, Any] | None = None,
|
||
business_notes: str = "",
|
||
) -> dict[str, Any]:
|
||
core = generate_core_info_card(
|
||
keyword=keyword,
|
||
strategy_markdown=strategy_markdown,
|
||
strategy_decisions=strategy_decisions,
|
||
business_notes=business_notes,
|
||
)
|
||
pack = generate_detail_page_pack(keyword=keyword, core_info_card=core)
|
||
return {
|
||
"core_info_card": core,
|
||
"detail_page_pack": pack,
|
||
}
|