DM-Agent / src /formatter.py
unknown
refactor: consolidate repo structure, remove upload_to_github duplication
3291500
Raw History Blame Contribute Delete
17.7 kB
"""
固定消息模板:钉钉 Markdown 周报。
结构不变,数据填充,专家预警分级展示。
"""
from typing import Any
ALERT_ICON = {
"red": "🔴",
"yellow": "🟡",
"green": "🟢",
# 兼容旧格式
"critical": "🔴",
"warning": "🟡",
"positive": "🟢",
}
# ─── 数值格式化 ───
def _n(val: float | None, prefix: str = "", suffix: str = "", d: int = 2) -> str:
if val is None or val != val:
return "—"
if abs(val) >= 1_000_000:
return f"{prefix}{val / 1_000_000:,.{d}f}M{suffix}"
if abs(val) >= 1_000:
return f"{prefix}{val / 1_000:,.{d}f}K{suffix}"
return f"{prefix}{val:,.{d}f}{suffix}"
def _pct(val: float | None) -> str:
if val is None or val != val:
return "—"
return f"{val * 100:.2f}%" if abs(val) < 1 else f"{val:.2f}%"
def _arrow(change: float | None) -> str:
"""根据变化幅度返回方向箭头。"""
if change is None or change != change:
return ""
if change >= 10:
return "↑↑"
if change > 0:
return "↑"
if change <= -10:
return "↓↓"
if change < 0:
return "↓"
return "→"
def _deltas_zh(wow_val: float | None, yoy_val: float | None) -> str:
"""环比/同比(中文标签),适合钉钉 Markdown;大幅波动加粗。"""
parts: list[str] = []
if wow_val is not None and wow_val == wow_val:
sign = "+" if wow_val >= 0 else ""
ar = _arrow(wow_val)
s = f"环比 {ar}{sign}{wow_val:.1f}%"
if abs(wow_val) >= 10:
s = f"**{s}**"
parts.append(s)
if yoy_val is not None and yoy_val == yoy_val:
sign = "+" if yoy_val >= 0 else ""
ar = _arrow(yoy_val)
s = f"同比 {ar}{sign}{yoy_val:.1f}%"
if abs(yoy_val) >= 10:
s = f"**{s}**"
parts.append(s)
if not parts:
return ""
return " | " + " | ".join(parts)
def _append_funnel_dim_lines(L: list[str], m: dict[str, Any], cw: dict, cy: dict) -> None:
"""漏斗维度:每指标一行,钉钉端更易扫读。"""
L.append(f"- **花费** {_n(m.get('Cost'), prefix='$')}{_deltas_zh(cw.get('Cost'), cy.get('Cost'))}")
L.append(f"- **销售额** {_n(m.get('Ads.Sales'), prefix='$')}{_deltas_zh(cw.get('Ads.Sales'), cy.get('Ads.Sales'))}")
L.append(f"- **ROAS** {m.get('ROAS', 0):.2f}{_deltas_zh(cw.get('ROAS'), cy.get('ROAS'))}")
L.append(f"- **展示量** {_n(m.get('Impressions'), d=0)}{_deltas_zh(cw.get('Impressions'), cy.get('Impressions'))}")
L.append(f"- **CTR** {_pct(m.get('CTR'))}{_deltas_zh(cw.get('CTR'), cy.get('CTR'))}")
L.append(f"- **CPC** {_n(m.get('CPC'), prefix='$')}{_deltas_zh(cw.get('CPC'), cy.get('CPC'))}")
if m.get("ATC"):
L.append(f"- **加购 ATC** {_n(m.get('ATC'), d=0)}{_deltas_zh(cw.get('ATC'), cy.get('ATC'))}")
if m.get("GA4.Sessions"):
L.append(f"- **会话** {_n(m.get('GA4.Sessions'), d=0)}{_deltas_zh(cw.get('GA4.Sessions'), cy.get('GA4.Sessions'))}")
def _append_country_dim_lines(L: list[str], m: dict[str, Any], cw: dict, cy: dict, *, full: bool) -> None:
"""市场维度:full=True 与周报一致;False 仅花费+ROAS(日报/简版)。"""
L.append(f"- **花费** {_n(m.get('Cost'), prefix='$')}{_deltas_zh(cw.get('Cost'), cy.get('Cost'))}")
if full:
L.append(f"- **销售额** {_n(m.get('Ads.Sales'), prefix='$')}{_deltas_zh(cw.get('Ads.Sales'), cy.get('Ads.Sales'))}")
L.append(f"- **ROAS** {m.get('ROAS', 0):.2f}{_deltas_zh(cw.get('ROAS'), cy.get('ROAS'))}")
if full:
L.append(f"- **会话** {_n(m.get('GA4.Sessions'), d=0)}{_deltas_zh(cw.get('GA4.Sessions'), cy.get('GA4.Sessions'))}")
L.append(f"- **加购** {_n(m.get('ATC'), d=0)}{_deltas_zh(cw.get('ATC'), cy.get('ATC'))}")
L.append(f"- **转化率** {_pct(m.get('CVR'))}{_deltas_zh(cw.get('CVR'), cy.get('CVR'))}")
# ─── 固定模板 ───
def format_report(data: dict[str, Any]) -> str:
cp = data.get("current_period", {})
pp = data.get("previous_period")
year, week = cp.get("year"), cp.get("week")
L: list[str] = []
L.append(f"## DM 周报 · {year} W{week}")
cmp_bits: list[str] = []
if pp:
cmp_bits.append(f"环比:{pp['year']} 年第 {pp['week']} 周")
if data.get("yoy_period"):
yp = data["yoy_period"]
cmp_bits.append(f"同比:{yp['year']} 年第 {yp['week']} 周")
if cmp_bits:
L.append("> " + " | ".join(cmp_bits))
L.append("")
curr = data.get("current", {})
wow_t = data.get("wow_total") or {}
yoy_t = data.get("yoy_total") or {}
total = curr.get("total", {})
L.append("### 一、整体表现")
L.append("")
_block_metrics(L, total, wow_t, yoy_t if yoy_t else None)
L.append("")
by_plat = curr.get("by_platform", {})
wow_plat = data.get("wow_by_platform") or {}
yoy_plat = data.get("yoy_by_platform") or {}
L.append("### 二、平台对比")
L.append("")
for plat in ["Google", "Meta"]:
m = by_plat.get(plat)
if not m:
continue
pw = wow_plat.get(plat, {})
py = yoy_plat.get(plat, {})
L.append(f"**{plat}**")
_block_metrics(L, m, pw, py if py else None)
L.append("")
by_ct = curr.get("by_campaign_type", {})
wow_ct = data.get("wow_by_campaign") or {}
yoy_ct = data.get("yoy_by_campaign") or {}
L.append("### 三、广告漏斗")
L.append("")
for ct in ["Awareness", "Consideration", "Conversion"]:
m = by_ct.get(ct)
if not m:
continue
cw = wow_ct.get(ct, {})
cy = yoy_ct.get(ct, {})
L.append(f"**{ct}**")
_append_funnel_dim_lines(L, m, cw, cy)
L.append("")
by_country = curr.get("by_country", {})
wow_country = data.get("wow_by_country") or {}
yoy_country = data.get("yoy_by_country") or {}
L.append("### 四、市场表现")
L.append("")
for c in sorted(by_country.keys()):
m = by_country[c]
cw = wow_country.get(c, {})
cy = yoy_country.get(c, {})
L.append(f"**{c}**")
_append_country_dim_lines(L, m, cw, cy, full=True)
L.append("")
_append_expert_section(L, data.get("expert", {}))
return "\n".join(L).strip()
def _append_expert_section(L: list[str], expert: dict[str, Any]) -> None:
"""将专家分析追加到报告末尾。兼容新格式(verdict)和旧格式(summary)。"""
if not expert:
return
L.append("### 陈策点评")
L.append("")
verdict = expert.get("verdict") or expert.get("summary", "")
if verdict:
L.append(f"> 💡 {verdict}")
L.append("")
findings = expert.get("key_findings") or []
if not findings:
for key in ["efficiency", "platform", "funnel", "market"]:
findings.extend(expert.get(key, []))
if findings:
for i, f in enumerate(findings, 1):
L.append(f"**{i}.** {f}")
L.append("")
alerts = expert.get("alerts", [])
if alerts:
for a in alerts:
level = a.get("level", "yellow")
icon = ALERT_ICON.get(level, "🟡")
msg = a.get("msg") or f"{a.get('title', '')}:{a.get('detail', '')}"
L.append(f"{icon} **{msg}**" if level == "red" else f"{icon} {msg}")
L.append("")
action = expert.get("next_action", "")
if not action:
recs = expert.get("recommendations", [])
if recs:
action = recs[0]
if action:
L.append(f"> 👉 **下一步:{action}**")
L.append("")
def _flag(wow_val: float | None, yoy_val: float | None) -> str:
"""大幅异动时加前缀标记,让读者快速定位重点。"""
for v in [wow_val, yoy_val]:
if v is not None and v == v:
if v <= -20:
return "⚠️ "
if v >= 20:
return "🔥 "
return ""
def _block_metrics(
L: list[str],
m: dict[str, float],
wow: dict[str, float | None],
yoy: dict[str, float | None] | None = None,
) -> None:
"""每个核心指标独立一行,大幅异动自动标记。"""
y = yoy or {}
rows = [
("花费", _n(m.get("Cost", 0), prefix="$"), wow.get("Cost"), y.get("Cost")),
("销售额", _n(m.get("Ads.Sales", 0), prefix="$"), wow.get("Ads.Sales"), y.get("Ads.Sales")),
("ROAS", f"{m.get('ROAS', 0):.2f}", wow.get("ROAS"), y.get("ROAS")),
("展示量", _n(m.get("Impressions", 0), d=0), wow.get("Impressions"), y.get("Impressions")),
("CTR", _pct(m.get("CTR", 0)), wow.get("CTR"), y.get("CTR")),
("转化率", _pct(m.get("CVR", 0)), wow.get("CVR"), y.get("CVR")),
("CPC", _n(m.get("CPC", 0), prefix="$"), wow.get("CPC"), y.get("CPC")),
("会话", _n(m.get("GA4.Sessions", 0), d=0), wow.get("GA4.Sessions"), y.get("GA4.Sessions")),
]
if m.get("ATC", 0):
rows.append(("加购 ATC", _n(m["ATC"], d=0), wow.get("ATC"), y.get("ATC")))
for name, val, w, yr in rows:
prefix = _flag(w, yr)
L.append(f"- {prefix}**{name}** {val}{_deltas_zh(w, yr)}")
def format_title(data: dict[str, Any]) -> str:
cp = data.get("current_period", {})
return f"DM Weekly Report {cp.get('year')} W{cp.get('week')}"
# ─────────────────────────────────────────────
# 月度报告模板(与周报结构一致)
# ─────────────────────────────────────────────
def format_monthly_report(data: dict[str, Any]) -> str:
"""月度汇总报告,与周报结构一致,含环比+同比。"""
period = data.get("period_label", "")
prev = data.get("prev_label")
yoy_label = data.get("yoy_label")
error = data.get("error")
L: list[str] = []
L.append(f"## DM 月报 · {period}")
cmp_parts = []
if prev:
cmp_parts.append(f"环比:{prev}")
if yoy_label:
cmp_parts.append(f"同比:{yoy_label}")
if cmp_parts:
L.append("> " + " | ".join(cmp_parts))
L.append("")
if error:
L.append(f"> {error}")
return "\n".join(L).strip()
curr = data.get("current", {})
wow_t = data.get("wow_total") or {}
yoy_t = data.get("yoy_total") or {}
total = curr.get("total", {})
L.append("### 一、整体表现")
L.append("")
_block_metrics(L, total, wow_t, yoy_t if yoy_t else None)
L.append("")
by_plat = curr.get("by_platform", {})
wow_plat = data.get("wow_by_platform") or {}
yoy_plat = data.get("yoy_by_platform") or {}
L.append("### 二、平台对比")
L.append("")
for plat in ["Google", "Meta"]:
m = by_plat.get(plat)
if not m:
continue
pw = wow_plat.get(plat, {})
py = yoy_plat.get(plat, {})
L.append(f"**{plat}**")
_block_metrics(L, m, pw, py if py else None)
L.append("")
by_ct = curr.get("by_campaign_type", {})
wow_ct = data.get("wow_by_campaign") or {}
yoy_ct = data.get("yoy_by_campaign") or {}
L.append("### 三、广告漏斗")
L.append("")
for ct in ["Awareness", "Consideration", "Conversion"]:
m = by_ct.get(ct)
if not m:
continue
cw = wow_ct.get(ct, {})
cy = yoy_ct.get(ct, {})
L.append(f"**{ct}**")
_append_funnel_dim_lines(L, m, cw, cy)
L.append("")
by_country = curr.get("by_country", {})
wow_country = data.get("wow_by_country") or {}
yoy_country = data.get("yoy_by_country") or {}
L.append("### 四、市场表现")
L.append("")
for c in sorted(by_country.keys()):
m = by_country[c]
cw = wow_country.get(c, {})
cy = yoy_country.get(c, {})
L.append(f"**{c}**")
_append_country_dim_lines(L, m, cw, cy, full=True)
L.append("")
_append_expert_section(L, data.get("expert", {}))
return "\n".join(L).strip()
def format_monthly_title(data: dict[str, Any]) -> str:
return f"DM Monthly Report {data.get('period_label', '')}"
# ─────────────────────────────────────────────
# 日报模板(复用月报结构)
# ─────────────────────────────────────────────
def format_daily_report(data: dict[str, Any]) -> str:
"""日度报告,结构与月报一致,标签改为 DoD/YoY。"""
period = data.get("period_label", "")
prev = data.get("prev_label")
yoy_label = data.get("yoy_label")
error = data.get("error")
L: list[str] = []
L.append(f"## DM 日报 · {period}")
cmp_parts = []
if prev:
cmp_parts.append(f"环比:{prev}")
if yoy_label:
cmp_parts.append(f"同比:{yoy_label}")
if cmp_parts:
L.append("> " + " | ".join(cmp_parts))
L.append("")
if error:
L.append(f"> {error}")
return "\n".join(L).strip()
curr = data.get("current", {})
wow_t = data.get("wow_total") or {}
yoy_t = data.get("yoy_total") or {}
total = curr.get("total", {})
L.append("### 整体表现")
L.append("")
_block_metrics(L, total, wow_t, yoy_t if yoy_t else None)
L.append("")
by_plat = curr.get("by_platform", {})
wow_plat = data.get("wow_by_platform") or {}
yoy_plat = data.get("yoy_by_platform") or {}
L.append("### 平台对比")
L.append("")
for plat in ["Google", "Meta"]:
m = by_plat.get(plat)
if not m:
continue
pw = wow_plat.get(plat, {})
py = yoy_plat.get(plat, {})
L.append(f"**{plat}**")
_block_metrics(L, m, pw, py if py else None)
L.append("")
by_country = curr.get("by_country", {})
wow_country = data.get("wow_by_country") or {}
yoy_country = data.get("yoy_by_country") or {}
if by_country:
L.append("### 市场表现")
L.append("")
for c in sorted(by_country.keys()):
m = by_country[c]
cw = wow_country.get(c, {})
cy = yoy_country.get(c, {})
L.append(f"**{c}**")
_append_country_dim_lines(L, m, cw, cy, full=False)
L.append("")
_append_expert_section(L, data.get("expert", {}))
return "\n".join(L).strip()
def format_daily_title(data: dict[str, Any]) -> str:
return f"DM Daily Report {data.get('period_label', '')}"
# ─────────────────────────────────────────────
# 自定义时间段报告
# ─────────────────────────────────────────────
def format_range_report(data: dict[str, Any]) -> str:
"""自定义时间段报告,结构与月报一致。"""
period = data.get("period_label", "")
prev = data.get("prev_label")
yoy_label = data.get("yoy_label")
days = data.get("days_count", "")
error = data.get("error")
L: list[str] = []
L.append(f"## DM 自定义报告 · {period}")
sub_parts = []
if days:
sub_parts.append(f"统计 {days} 天")
if prev:
sub_parts.append(f"环比:{prev}")
if yoy_label:
sub_parts.append(f"同比:{yoy_label}")
if sub_parts:
L.append("> " + " | ".join(sub_parts))
L.append("")
if error:
L.append(f"> {error}")
return "\n".join(L).strip()
curr = data.get("current", {})
wow_t = data.get("wow_total") or {}
yoy_t = data.get("yoy_total") or {}
total = curr.get("total", {})
L.append("### 整体表现")
L.append("")
_block_metrics(L, total, wow_t, yoy_t if yoy_t else None)
L.append("")
by_plat = curr.get("by_platform", {})
wow_plat = data.get("wow_by_platform") or {}
yoy_plat = data.get("yoy_by_platform") or {}
L.append("### 平台对比")
L.append("")
for plat in ["Google", "Meta"]:
m = by_plat.get(plat)
if not m:
continue
pw = wow_plat.get(plat, {})
py = yoy_plat.get(plat, {})
L.append(f"**{plat}**")
_block_metrics(L, m, pw, py if py else None)
L.append("")
by_ct = curr.get("by_campaign_type", {})
wow_ct = data.get("wow_by_campaign") or {}
yoy_ct = data.get("yoy_by_campaign") or {}
if by_ct:
L.append("### 广告漏斗")
L.append("")
for ct in ["Awareness", "Consideration", "Conversion"]:
m = by_ct.get(ct)
if not m:
continue
cw = wow_ct.get(ct, {})
cy = yoy_ct.get(ct, {})
L.append(f"**{ct}**")
_append_funnel_dim_lines(L, m, cw, cy)
L.append("")
by_country = curr.get("by_country", {})
wow_country = data.get("wow_by_country") or {}
yoy_country = data.get("yoy_by_country") or {}
if by_country:
L.append("### 市场表现")
L.append("")
for c in sorted(by_country.keys()):
m = by_country[c]
cw = wow_country.get(c, {})
cy = yoy_country.get(c, {})
L.append(f"**{c}**")
_append_country_dim_lines(L, m, cw, cy, full=False)
L.append("")
_append_expert_section(L, data.get("expert", {}))
return "\n".join(L).strip()
def format_range_title(data: dict[str, Any]) -> str:
return f"DM Custom Report {data.get('period_label', '')}"