Initial Daily Campaign Agent release
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
name: validate
|
||||
|
||||
on:
|
||||
push:
|
||||
pull_request:
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v7
|
||||
- uses: actions/setup-python@v7
|
||||
with:
|
||||
python-version: "3.11"
|
||||
- name: Run standard-library tests
|
||||
run: python3 -m unittest discover -s tests -v
|
||||
- name: Compile scripts
|
||||
run: python3 -m py_compile scripts/*.py tests/*.py
|
||||
@@ -0,0 +1,13 @@
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
.pytest_cache/
|
||||
.venv/
|
||||
.DS_Store
|
||||
|
||||
# User data and generated artifacts
|
||||
*.xlsx
|
||||
*.xls
|
||||
*.docx
|
||||
output/
|
||||
delivery-profile.local.md
|
||||
*.analysis.json
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Daily Campaign Triage contributors
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,126 @@
|
||||
# VK Daily Campaign Triage
|
||||
|
||||
一个面向广告优化人员的 Codex Skill:读取 `.xlsx` / `.csv` / `.tsv` 投放日报,
|
||||
先确认 KPI、成熟日期和消耗目标,再从 **Ad 层级**划分学习期池与稳定期池,产出
|
||||
当天需要人工干预的对象、证据和可执行动作。
|
||||
|
||||
**亮点:它不只报数。** 针对 VK 广告生态,它会把 7/14 天趋势翻译成可以在平台中
|
||||
落实的优化指导,例如暂停差 Ad、预算回退观察、保护成熟赢家、拆分词包/受众假设、
|
||||
创建单变量素材测试,以及按 3 / 5 / 7 天决定去留和扩量。
|
||||
|
||||
它只做分析和建议,不会擅自修改 VK 或其他广告平台的预算、出价、状态和素材。
|
||||
|
||||
## 它解决什么
|
||||
|
||||
- 将当天获取指标与滞后成熟指标分开,避免用未成熟次留误判;
|
||||
- 对学习期 Ad 按 3 / 5 / 7 天检查点排名和优胜劣汰;
|
||||
- 对稳定期 Ad 按 `S-优质 / A-稳定 / B-波动待优化 / C-劣质` 排名;
|
||||
- 结合近 7 天、前 7 天和 14 天证据识别偶发波动、持续恶化和成熟赢家;
|
||||
- 输出暂停/降量、观察、恢复测试、扩量、新素材需求和预算缺口;
|
||||
- 生成四段式 `.docx` 简报,保持 Ad 级视角,不输出 Campaign 排名。
|
||||
|
||||
## 为什么适合 VK 投放
|
||||
|
||||
普通日报往往只告诉你指标涨跌;这个 Skill 会进一步回答“今天具体动什么”:
|
||||
|
||||
| 数据现象 | VK 可落地指导 |
|
||||
|---|---|
|
||||
| 新 Ad 同时上线,样本尚未成熟 | 放入同一学习池排名,按第3/5/7天优胜劣汰 |
|
||||
| 扩量后连续成熟日恶化 | 回到最近稳定预算,保持其他变量不变,设定恢复测试退出条件 |
|
||||
| 历史强组合重组后失效 | 分离素材疲劳与词包/受众变化,优先建立干净对照测试 |
|
||||
| 原素材稳定但已投放较久 | 保护存量,不继续硬推;复制方向并只改变一个创意变量 |
|
||||
| 有消耗缺口但没有成熟赢家 | 用有上限的新素材探索池补量,不把预算强塞给差 Ad |
|
||||
| Ad 零消耗 | 提示检查 VK 审核状态、出价竞争力、受众规模和父级投放约束 |
|
||||
|
||||
每条建议都会尽量包含:**对象、证据窗口、建议动作、保持不变的变量、预算影响、
|
||||
复查条件和退出条件**。缺少预算、出价、状态或操作历史时,会明确标注限制,不会
|
||||
虚构精确的平台设置。
|
||||
|
||||
## 匿名日报模板预览
|
||||
|
||||
下图中的账户、Ad 名称、日期、数值和 KPI 阈值均为虚构内容:
|
||||
|
||||

|
||||
|
||||
## 安装
|
||||
|
||||
将仓库克隆到 Codex Skills 目录:
|
||||
|
||||
```bash
|
||||
git clone https://github.com/joliehill294-hue/daily-campaign-triage.git \
|
||||
~/.codex/skills/daily-campaign-triage
|
||||
```
|
||||
|
||||
安装后重新启动 Codex。也可以在 Codex 中使用
|
||||
`$skill-installer`,要求它从该 GitHub 仓库安装。
|
||||
|
||||
## 使用
|
||||
|
||||
在 Codex 中附上或提供报表路径,并明确调用:
|
||||
|
||||
```text
|
||||
$daily-campaign-triage 分析这份日报。KPI:CPI ≤ 5;D1次留成本 ≤ 35;
|
||||
D1次留率 ≥ 14%;日消耗目标 500。输出 Word 到 ./output。
|
||||
```
|
||||
|
||||
Skill 会先检查数据结构。若 KPI 的公式、方向、适用范围或成熟规则缺失,会先向你
|
||||
确认,绝不会从代码库或其他账户静默读取 KPI。以上调用中的阈值仅为虚构示例。
|
||||
|
||||
## 确定性工具
|
||||
|
||||
无需安装第三方 Python 包:
|
||||
|
||||
```bash
|
||||
python3 scripts/analyze_report.py report.xlsx \
|
||||
--analysis-date 2026-08-25 \
|
||||
--retention-lag-days 1 \
|
||||
--output /tmp/report.analysis.json
|
||||
|
||||
python3 scripts/build_brief.py examples/brief.example.json \
|
||||
--output /tmp/example-brief.docx
|
||||
```
|
||||
|
||||
第一个脚本只生成 Ad 级事实,不代替 Skill 做业务判断;第二个脚本将已经完成判断的
|
||||
JSON 渲染为 Word。字段映射和 JSON 合同见
|
||||
[references/script-usage.md](references/script-usage.md)。
|
||||
|
||||
## 日报输出结构
|
||||
|
||||
1. 是否达标
|
||||
2. 问题分析:学习期池排名、稳定期池排名
|
||||
3. 建议动作
|
||||
4. 近 7 天表现好的 Ads(严格筛选,允许少于 3 个)
|
||||
|
||||
仓库内的示例和模板均为匿名数据。`.gitignore` 默认排除真实 Excel、生成的 Word、
|
||||
分析 JSON 和本地交付配置。
|
||||
|
||||
## 验证
|
||||
|
||||
```bash
|
||||
python3 -m unittest discover -s tests -v
|
||||
```
|
||||
|
||||
还可以直接让 `$skill-creator` 验证当前目录中的 Skill 结构。
|
||||
|
||||
## 目录
|
||||
|
||||
```text
|
||||
SKILL.md Skill 主说明
|
||||
agents/openai.yaml Codex 展示元数据
|
||||
references/ 输入、决策、排名、输出与交付合同
|
||||
scripts/analyze_report.py CSV/TSV/XLSX 事实归一化
|
||||
scripts/build_brief.py DOCX 生成器
|
||||
examples/ 匿名 JSON 示例
|
||||
assets/ GitHub 日报 PNG 预览
|
||||
tests/ 标准库自动化测试与匿名 fixture
|
||||
```
|
||||
|
||||
## 隐私与安全
|
||||
|
||||
- 不要提交真实广告报表、账户名称、个人绝对路径或生成的客户简报;
|
||||
- 不要把固定 KPI 写进 Skill,KPI 必须由每次任务或本地配置提供;
|
||||
- 所有建议仍需人工确认,本 Skill 不执行平台变更。
|
||||
|
||||
## License
|
||||
|
||||
[MIT](LICENSE)
|
||||
@@ -0,0 +1,192 @@
|
||||
---
|
||||
name: "daily-campaign-triage"
|
||||
description: "Analyze advertising daily reports or Excel/CSV exports at Ad level, especially VK Ads ecosystem reports, using confirmed KPI, maturity dates, and 7/14-day trends to produce actionable pause, recovery, creative-test, and scale guidance. Analysis-only: do not change platform state."
|
||||
---
|
||||
|
||||
# Daily Campaign Triage
|
||||
|
||||
Turn an advertising report into a short, evidence-backed operator queue focused on
|
||||
Ads. Use Campaign and Ad Group only as source-mapping or identity context when needed;
|
||||
do not add Campaign-level rankings to the daily brief. Support raw spreadsheets,
|
||||
CSV exports, pasted tables, and narrative briefs without assuming a platform, schema,
|
||||
currency, timezone, attribution model, or KPI.
|
||||
|
||||
This is read-only decision support. Never change budgets, bids, status, targeting,
|
||||
creative, or platform state unless the user separately requests and authorizes those
|
||||
exact actions.
|
||||
|
||||
## Operating modes
|
||||
|
||||
- **Daily triage:** identify today's manual-intervention queue from the latest mature
|
||||
KPI day plus recent 7/14-day evidence.
|
||||
- **Follow-up diagnosis:** answer why a named entity changed and whether to rollback,
|
||||
hold, retest, pause/reduce, or consider scaling.
|
||||
- **VK optimization brief:** translate Ad-level evidence into VK-relevant operator
|
||||
guidance for budget rollback, learning protection, creative-variable tests, audience
|
||||
or word-pack isolation, and controlled scaling without pretending the action ran.
|
||||
|
||||
Use daily triage by default. Add follow-up diagnosis when the user names an entity,
|
||||
describes a recent intervention, or asks a causal/recovery question.
|
||||
|
||||
## Required input
|
||||
|
||||
Establish this contract before making business verdicts:
|
||||
|
||||
| Input | Required content | If absent |
|
||||
|---|---|---|
|
||||
| Report | Readable spreadsheet/CSV/table; date plus stable hierarchy or names; spend and KPI components | Ask for a readable source or explain which analyses are impossible |
|
||||
| KPI card | Metric, formula, direction, target, scope, and maturity rule for every decision KPI | Stop before pass/fail or action labels; ask once in a compact list |
|
||||
| Analysis date | Requested report date, or permission to use the latest complete delivery date; each lagged KPI's latest mature cohort | Derive only when definitions and coverage make it unambiguous; otherwise confirm |
|
||||
| Pacing card | Spend target, currency, scope, period, and whether it is a minimum, desired level, cap, or range | Required only for pacing/scale-allocation conclusions; otherwise mark pacing unavailable |
|
||||
|
||||
Useful optional input:
|
||||
|
||||
- minimum sample or persistence rule per KPI;
|
||||
- budget adjustment rule or standard test budget per new Ad;
|
||||
- timezone, currency, attribution window, status, budget, and launch date;
|
||||
- dated budget/bid/targeting/creative/structure/tracking changes;
|
||||
- platform constraints, such as whether status or budget can change at Ad level.
|
||||
|
||||
Accept natural language, or use this standard intake card when prompting is needed:
|
||||
|
||||
```text
|
||||
数据文件/表格:
|
||||
要分析的日报日期: # 留空则使用最新完整投放日
|
||||
KPI:指标|公式|方向|目标|适用范围|成熟规则
|
||||
日消耗目标:金额|币种|范围|最低/期望/上限/区间
|
||||
最低样本/持续性规则(可选):
|
||||
预算调整规则/新 Ad 标准测试预算(可选):
|
||||
近期操作(可选):日期|对象|预算/出价/词包/定向/素材/结构|改前→改后
|
||||
本次特别关注(可选):
|
||||
```
|
||||
|
||||
Do not ask the user to repeat values already supplied in the current request or
|
||||
available conversation context. Restate them in `分析口径确认` and ask only about
|
||||
missing or contradictory fields that can materially change the verdict.
|
||||
|
||||
If minimum-sample or persistence rules are not supplied, use the framework defaults
|
||||
only as provisional evidence rules and disclose them. Never manufacture missing KPI,
|
||||
budget history, or platform capability.
|
||||
|
||||
Read [references/input-contract.md](references/input-contract.md) when mapping a new
|
||||
report, resolving KPI or pacing definitions, or handling hierarchical rows and
|
||||
different maturity dates. Read
|
||||
[references/decision-framework.md](references/decision-framework.md) before making
|
||||
the triage. For every daily brief, read
|
||||
[references/learning-stable-ranking.md](references/learning-stable-ranking.md) to
|
||||
build separate learning-period and stable-period rankings. Read
|
||||
[references/output-standard.md](references/output-standard.md) before producing the
|
||||
final report. When the user has configured file delivery, also read
|
||||
[references/delivery-profile.md](references/delivery-profile.md) and deliver the
|
||||
brief through that format/path instead of placing the full brief in chat. Also read
|
||||
[references/change-response.md](references/change-response.md) when the user reports
|
||||
recent scaling, rollback, restructuring, targeting/creative changes, a sudden drop,
|
||||
a former winner that stopped working, or asks how to rescue an entity.
|
||||
When the report comes from VK or the user asks for VK-specific execution guidance,
|
||||
also read [references/vk-optimization.md](references/vk-optimization.md).
|
||||
|
||||
For local `.xlsx`, `.csv`, or `.tsv` inputs, prefer the deterministic standard-library
|
||||
analyzer in `scripts/analyze_report.py`; it produces normalized Ad-level evidence but
|
||||
does not invent business decisions. Read [references/script-usage.md](references/script-usage.md)
|
||||
before running it. After applying the decision framework, place the final judgments in
|
||||
the brief JSON contract and use `scripts/build_brief.py` to create the DOCX. The scripts
|
||||
have no third-party Python dependencies.
|
||||
|
||||
## Confirmation gates
|
||||
|
||||
Use these as decision gates, not ceremonial questions.
|
||||
|
||||
### Gate A — source and grain
|
||||
|
||||
Inspect the file first. Proceed without asking when sheet, row grain, hierarchy,
|
||||
dates, units, and totals are unambiguous. Ask for confirmation only when competing
|
||||
mappings would materially change aggregation or attribution. State the proposed
|
||||
mapping in the question. If the mapping is clear, record it in the output and continue
|
||||
in the same turn; do not wait for ceremonial approval.
|
||||
|
||||
### Gate B — KPI and maturity
|
||||
|
||||
This gate is mandatory before `达标/未达标`, pause/reduce, rollback, or scale labels.
|
||||
Confirm any missing KPI target/direction/formula/scope and the latest mature date or
|
||||
maturity rule. Never retrieve targets from unrelated code or silently reuse another
|
||||
account's values. Until confirmed, continue only with data-quality and neutral trend
|
||||
analysis labeled `无业务 KPI,仅分析趋势`.
|
||||
|
||||
### Gate C — change causality
|
||||
|
||||
Use only for follow-up diagnosis. If the recommendation depends on whether a budget,
|
||||
bid, word pack, audience, creative, or structure changed, confirm the material change
|
||||
and approximate effective date when the report cannot establish it. Do not block a
|
||||
plain performance classification that does not rely on causality.
|
||||
|
||||
### Gate D — execution boundary
|
||||
|
||||
Recommendations do not authorize platform changes. If the user later asks to execute,
|
||||
confirm the exact entities and actions immediately before mutation under the relevant
|
||||
platform workflow. This skill itself remains analysis-only.
|
||||
|
||||
## Standard workflow
|
||||
|
||||
1. **Intake:** identify mode, requested date, source coverage, and supplied KPI/pacing
|
||||
cards. List only missing inputs that can change today's verdict.
|
||||
2. **Validate:** inspect sheets, columns, row grain, identities, units, duplicates,
|
||||
blanks versus zero, parent totals, missing dates, and reconciliation risk. Preserve
|
||||
the source file.
|
||||
3. **Normalize:** aggregate from the finest trustworthy leaf rows; never double-count
|
||||
parent totals. Recompute ratio KPIs from summed numerators and denominators.
|
||||
4. **Align maturity:** set a separate analysis date and comparison windows for each
|
||||
lagged KPI. Exclude immature cohorts from negative judgments.
|
||||
5. **Build windows:** calculate analysis day, recent 7 mature days, previous 7 mature
|
||||
days, and 14-day context for each trustworthy Ad.
|
||||
6. **Split and rank pools:** separate learning-period from stable-period entities.
|
||||
Apply the 3/5/7-day checkpoints to the learning pool and the
|
||||
quality/stability/KPI tiers to the stable pool.
|
||||
7. **Diagnose Ads:** rank and diagnose Ads directly. Preserve parent names only to
|
||||
disambiguate Ads with duplicate names; keep Campaign aggregates out of the report.
|
||||
8. **Analyze changes when relevant:** compare pre-change control, transition, and
|
||||
post-change mature cohorts; separate observed facts from causal hypotheses.
|
||||
9. **Classify action:** choose one state—pause/reduce, rollback/recovery,
|
||||
hold/protect, clean-control retest, scale candidate, investigate data, or no action.
|
||||
10. **Prioritize:** rank by impact, confidence, and urgency; quantify pacing impact and
|
||||
avoid filling spend gaps with unvalidated winners.
|
||||
11. **Report:** render the fixed output contract below, including recheck and exit
|
||||
conditions. Never imply that a recommendation was executed.
|
||||
12. **Deliver:** when a delivery profile is active, create and verify the requested
|
||||
artifact, save it using the analysis date—not the generation date—and return only
|
||||
a concise completion message plus the file link in chat.
|
||||
|
||||
Keep generated evidence JSON, source exports, local delivery overrides, and output
|
||||
documents outside the reusable Skill repository. Never ship an advertiser's raw data
|
||||
or historical named-Ad diagnosis as an example.
|
||||
|
||||
## Output contract
|
||||
|
||||
Always output these sections in order:
|
||||
|
||||
1. **是否达标** — list every confirmed KPI as `KPI:值(达标/未达标)`, using its
|
||||
correct mature date; include spend target attainment and material caveats here.
|
||||
2. **问题分析** — show the Ad-level learning-period ranking and stable-period ranking
|
||||
with evidence, tier, and decision. Do not include a Campaign-level ranking.
|
||||
3. **建议动作** — list pause, budget reduction, hold/recheck, scale, new-Ad demand,
|
||||
and creative-refresh alerts; quantify released budget or spend capacity when valid.
|
||||
4. **近7天表现好的 Ads(3–5个)** — name, KPI data, spend/sample, stage, consistency,
|
||||
and why each Ad is good. Return fewer than three rather than lower the standard.
|
||||
|
||||
Use the exact table fields and concise writing rules in
|
||||
[references/output-standard.md](references/output-standard.md). Omit empty detail
|
||||
rows, but never omit a section heading; state `无` so the absence is explicit.
|
||||
|
||||
## Decision boundaries
|
||||
|
||||
- Recommend an Ad pause/reduction candidate only when that Ad has persistent mature
|
||||
underperformance, meaningful exposure, adequate confirmed sample, and no plausible
|
||||
data-lag or recent-change explanation.
|
||||
- Recommend at Ad level. Mention an Ad Group or Campaign only when the platform lever
|
||||
is unavailable at Ad level or parent context is required to identify the Ad.
|
||||
- A daily spend shortfall is not permission to scale failing traffic. Fill pacing gaps
|
||||
through verified winners or bounded exploration; if neither exists, report the gap.
|
||||
- Prefer a controlled rollback or clean retest over an immediate pause when a known
|
||||
configuration change plausibly explains deterioration and a credible prior control
|
||||
exists. Treat rollback as a recovery test, not proof of recovery.
|
||||
- Do not use an unvalidated new winner to fill a pacing gap. One exceptional cohort or
|
||||
one to two good days is not durable scale evidence.
|
||||
@@ -0,0 +1,4 @@
|
||||
interface:
|
||||
display_name: "Daily Ad Triage"
|
||||
short_description: "Turn VK ad reports into actionable daily decisions"
|
||||
default_prompt: "Use $daily-campaign-triage to analyze this VK report at Ad level, confirm KPI and maturity dates, rank learning and stable pools, recommend executable optimization actions, and export the daily brief."
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 318 KiB |
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"title": "2026-08-25 投放简报",
|
||||
"account_label": "示例账户|广告投放日报",
|
||||
"analysis_date": "2026-08-25",
|
||||
"retention_mature_date": "2026-08-24",
|
||||
"portfolio_kpis": [
|
||||
{"metric": "CPI", "actual": "4.20", "target": "≤ 5.00", "verdict": "达标", "basis": "投放数据日;汇总消耗 / 安装"},
|
||||
{"metric": "D1次留成本", "actual": "28.00", "target": "≤ 35.00", "verdict": "达标", "basis": "成熟日 cohort;汇总消耗 / 次留人数"},
|
||||
{"metric": "D1次留率", "actual": "15.00%", "target": "≥ 14.00%", "verdict": "达标", "basis": "成熟日 cohort;次留人数 / 安装"},
|
||||
{"metric": "日消耗", "actual": "420.00", "target": "500.00", "verdict": "未达标", "basis": "缺口 80.00"}
|
||||
],
|
||||
"kpi_note": "不同指标使用各自成熟日期;未成熟次留不用于负面判断。",
|
||||
"learning_pool": [
|
||||
{"rank": "1", "ad": "Creative Alpha", "stage": "第3天", "evidence": "CPI达标;次留样本尚未成熟", "decision": "继续观察至第5天"}
|
||||
],
|
||||
"stable_pool": [
|
||||
{"rank": "1", "tier": "S-优质", "ad": "Creative Cedar", "evidence": "近7日KPI超额完成且波动小", "decision": "保护存量;作为新变体母版"},
|
||||
{"rank": "2", "tier": "B-波动待优化", "ad": "Creative Birch", "evidence": "CPI达标,次留成本波动", "decision": "保持低量并复查3天"}
|
||||
],
|
||||
"actions": [
|
||||
{"action": "暂停候选", "ad": "Creative Delta", "evidence": "稳定期且连续成熟样本严重不达标", "budget_impact": "释放约30/日"},
|
||||
{"action": "新建变体", "ad": "Creative Cedar方向", "evidence": "近7日稳定且素材变量可拆分", "budget_impact": "测试预算20/日"}
|
||||
],
|
||||
"action_notes": [
|
||||
{"label": "预算影响", "text": "执行止损与新测后仍有消耗缺口;不要为填满目标而强推未验证素材。"},
|
||||
{"label": "3 / 5 / 7天", "text": "第3天优胜劣汰,第5天保留前列,第7天再决定是否进入稳定池。"}
|
||||
],
|
||||
"good_ads": [
|
||||
{"rank": "1", "ad": "Creative Cedar", "data": "消耗252;60安装;9次留;CPI 4.20", "why_good": "样本足、KPI达标且结果不集中", "next_step": "保护原Ad;新建单变量变体"}
|
||||
],
|
||||
"data_note": "这是完全匿名的格式示例,不代表任何真实账户、KPI或广告数据。"
|
||||
}
|
||||
@@ -0,0 +1,150 @@
|
||||
# Change response and recovery analysis
|
||||
|
||||
Use this reference only when a recent intervention or suspected change point matters:
|
||||
budget/bid scaling, rollback, targeting or word-pack changes, creative replacement,
|
||||
Ad Group restructuring, a sudden deterioration, a new “black horse,” or a request to
|
||||
rescue a previously successful entity.
|
||||
|
||||
## 1. Reconstruct the timeline before assigning cause
|
||||
|
||||
Create a compact event timeline from the report and intervention log. Split relevant
|
||||
history into:
|
||||
|
||||
1. **Pre-change control:** the last sufficiently stable configuration and its mature
|
||||
KPI cohorts.
|
||||
2. **Transition:** delivery dates whose outcomes are immature or mix old and new
|
||||
configuration effects.
|
||||
3. **Post-change:** only cohorts mature enough for each KPI.
|
||||
|
||||
Compare like-for-like phases at the lowest trustworthy entity. Show exact dates,
|
||||
spend, sample, KPI results, and configuration when known. Rolling 7D/14D windows remain
|
||||
useful context, but must not blur a sharp phase break.
|
||||
|
||||
Timing is not causality. Use language such as `与变更时间一致` until competing
|
||||
explanations are checked. If deterioration began before restructuring, say so: the
|
||||
restructure may have amplified or failed to fix the problem, but is not the sole
|
||||
demonstrated cause.
|
||||
|
||||
## 2. Separate expansion shock from organic deterioration
|
||||
|
||||
Call an expansion-related deterioration plausible only when a confirmed budget, bid,
|
||||
reach, allocation, or traffic-mix change precedes the mature decline. Observed spend
|
||||
alone is not budget history.
|
||||
|
||||
- If budget/delivery increased and downstream quality fell after the change, test an
|
||||
expansion or traffic-mix hypothesis.
|
||||
- If spend stayed broadly stable while acquisition remained efficient but retention,
|
||||
revenue, or another downstream KPI fell, classify it first as downstream quality
|
||||
deterioration. Investigate audience, inventory, placement, cohort concentration,
|
||||
tracking, and mix shifts; do not label it a budget shock.
|
||||
- If acquisition and downstream quality both worsen, keep creative fatigue,
|
||||
targeting saturation, competition, and structural changes in the hypothesis set.
|
||||
|
||||
Never invent a causal mechanism from aggregate metrics. State the breakdown needed to
|
||||
confirm it, such as placement, device, audience, search term, keyword/match type, geo,
|
||||
frequency, or auction diagnostics.
|
||||
|
||||
## 3. Decide rollback, containment, or pause
|
||||
|
||||
Prefer a controlled rollback/recovery test when all are true:
|
||||
|
||||
- a dated intervention plausibly precedes the mature deterioration;
|
||||
- a prior configuration is known and operationally restorable;
|
||||
- that prior phase has credible evidence, not merely one good day;
|
||||
- the rollback does not destroy a currently verified child winner or introduce a
|
||||
broader structural risk.
|
||||
|
||||
Rollback is not recovery. If the prior configuration also missed current KPI, label
|
||||
the rollback `止损/风险收敛`, not `恢复到有效状态`. Compare it against both the damaged
|
||||
phase and the current KPI contract.
|
||||
|
||||
During the recovery test, hold other controllable variables fixed. Use the operator's
|
||||
configured maturity/sample rule. If none exists, a small run of multiple mature
|
||||
cohorts may serve as a provisional checkpoint, followed by a complete recent-window
|
||||
validation; clearly label this as a default evidence rule, not a universal truth.
|
||||
|
||||
Define exits in advance:
|
||||
|
||||
- **Recover:** multiple mature cohorts return to target without a guardrail breach;
|
||||
- **Continue cautiously:** directional improvement exists but sample is insufficient;
|
||||
- **Escalate/retest:** results remain poor or mixed and a clean hypothesis is testable;
|
||||
- **Pause/reduce:** persistent mature failure continues with meaningful exposure and
|
||||
no unresolved data issue.
|
||||
|
||||
Avoid daily edits during an observation window unless a hard loss, compliance, or
|
||||
tracking guardrail is breached.
|
||||
|
||||
## 4. Validate a new or apparent “black horse”
|
||||
|
||||
Early averages can be dominated by one cohort. Separate:
|
||||
|
||||
- launch/early cohorts;
|
||||
- the standout cohort or event;
|
||||
- subsequent mature cohorts;
|
||||
- immature cohorts, which must not be treated as zeros.
|
||||
|
||||
Show how much of the retained outcomes, revenue, or other success measure comes from
|
||||
the best day/segment. One strong cohort or one to two good days supports `promising`,
|
||||
not `durable winner`. Require the configured minimum sample and multi-cohort
|
||||
consistency before scale.
|
||||
|
||||
When an early winner weakens but has not accumulated persistent failure, prefer
|
||||
hold/protect at a stable test level over immediate scale or pause. Specify the next
|
||||
mature checkpoint and the failure condition.
|
||||
|
||||
## 5. Rescue a restructured Campaign or Ad Group
|
||||
|
||||
Do not decide `replace everything` or `abandon the Campaign` from its aggregate. Build
|
||||
a comparison across:
|
||||
|
||||
- the prior clean control structure;
|
||||
- copied/reused groups;
|
||||
- new word packs, audiences, creatives, or match types;
|
||||
- current child winners worth protecting.
|
||||
|
||||
Measure whether weakness is broad or concentrated and whether the decline started
|
||||
before or after each structure change. Historical success is context, but reassess it
|
||||
against the current KPI contract; a phase that met acquisition KPI but failed
|
||||
downstream quality is not an all-KPI control.
|
||||
|
||||
A clean rescue design should:
|
||||
|
||||
1. preserve one interpretable control;
|
||||
2. isolate one material hypothesis per test group;
|
||||
3. reduce duplicate/overlapping audiences, keywords, or creatives where evidence
|
||||
supports overlap risk;
|
||||
4. protect verified child winners from being reset or drowned by the test;
|
||||
5. use mature, predeclared success and stop conditions.
|
||||
|
||||
Do not recommend swapping a whole word pack without keyword/search-term and match-type
|
||||
evidence when those are the real levers. Request deeper dimensions only when they can
|
||||
change the decision.
|
||||
|
||||
## 6. Report the change decision
|
||||
|
||||
For an explicitly requested named-Ad follow-up, the optional presentation template is:
|
||||
|
||||
```text
|
||||
<Ad名称>近7日专项判断
|
||||
近7日消耗 | 安装 / 成熟结果 | 主要KPI | 下游KPI | 达标天数
|
||||
趋势:<量级、KPI和变更前后走势>
|
||||
结论:<保持、回退、低量观察、制作新变体、暂停或扩量判断>
|
||||
```
|
||||
|
||||
Populate it only from the current source and confirmed context. This is a follow-up
|
||||
template, not a standard daily-brief section. Never preserve an example Ad name or
|
||||
example metrics as default content.
|
||||
|
||||
For each change-related recommendation include:
|
||||
|
||||
- entity path and stable ID;
|
||||
- declared or inferred change and timestamp;
|
||||
- pre-change, transition, and post-change mature evidence;
|
||||
- observed fact versus causal hypothesis;
|
||||
- action state: rollback/recovery, hold/protect, clean-control retest,
|
||||
pause/reduce, or scale candidate;
|
||||
- variables to hold fixed;
|
||||
- pacing impact, including the spend gap created by rollback/pause;
|
||||
- next mature review point and explicit recover/escalate/exit condition.
|
||||
|
||||
Never claim the intervention was executed. This skill remains analysis-only.
|
||||
@@ -0,0 +1,221 @@
|
||||
# Daily campaign triage decision framework
|
||||
|
||||
Read this reference before producing a hierarchical intervention and scale queue.
|
||||
|
||||
## 1. Construct comparable windows
|
||||
|
||||
For each KPI, let `D` be that KPI's latest mature date:
|
||||
|
||||
- analysis day: `D`;
|
||||
- recent 7D: `D-6..D` using seven mature reporting days;
|
||||
- previous 7D: the seven mature reporting days immediately before recent 7D;
|
||||
- 14D context: both complete windows together.
|
||||
|
||||
If dates are missing, do not quietly substitute seven non-contiguous rows for seven
|
||||
calendar/reporting days. Explain the coverage and downgrade confidence. Compare only
|
||||
the same metric definition, currency, timezone, attribution window, entity scope, and
|
||||
target regime.
|
||||
|
||||
For ratio KPIs, aggregate components:
|
||||
|
||||
- CTR = total clicks / total impressions
|
||||
- CVR = total conversions / configured denominator
|
||||
- CPI/CPA = total spend / total attributed outcomes
|
||||
- ROAS = total attributed revenue / total spend
|
||||
- retention = total retained eligible users / total eligible cohort
|
||||
|
||||
Use the user's confirmed formula when it differs.
|
||||
|
||||
## 2. Benchmark hierarchy
|
||||
|
||||
Use only confirmed business targets for pass/fail decisions. Apply the narrowest
|
||||
confirmed target whose scope matches the entity, for example:
|
||||
|
||||
1. Ad- or Ad Group-specific target or hard guardrail;
|
||||
2. Campaign-specific target or hard guardrail;
|
||||
3. confirmed target for the matching product/objective/geo/account;
|
||||
4. confirmed account-wide target.
|
||||
|
||||
Historical Campaign, peer, or account performance may explain direction and context,
|
||||
but is not a business target. Without a confirmed target, label findings
|
||||
`无业务 KPI,仅分析趋势` and do not place an entity in intervention or scale buckets
|
||||
solely from relative performance.
|
||||
|
||||
## 3. Diagnose the hierarchy before choosing an action
|
||||
|
||||
Use this sequence when the report contains the dimensions:
|
||||
|
||||
1. **Portfolio/account:** quantify daily spend attainment, total risk/opportunity, and
|
||||
which Campaigns concentrate it.
|
||||
2. **Campaign:** identify weak, mixed, and strong Campaign aggregates, then measure how
|
||||
performance is distributed across their Ad Groups and Ads.
|
||||
3. **Ad Group:** decide whether the issue is shared across material child Ads or
|
||||
concentrated in a few Ads.
|
||||
4. **Ad:** classify concrete losers, uncertain Ads, and durable winners.
|
||||
|
||||
Campaign averages are routing evidence. They are not sufficient for a Campaign-pause
|
||||
recommendation when material child Ads disagree. Quantify concentration with spend,
|
||||
outcome, and retained/revenue shares where possible.
|
||||
|
||||
Prefer the lowest entity whose evidence supports the proposed lever. If the platform
|
||||
cannot change budget or status at Ad level, keep the Ad diagnosis but express the
|
||||
human action at the lowest supported parent level and explain the constraint.
|
||||
|
||||
## 4. Classify evidence at each relevant entity
|
||||
|
||||
Start with the primary KPI and then apply secondary KPI, delivery, and data-quality
|
||||
modifiers. Apply these classifications independently to Campaign, Ad Group, and Ad;
|
||||
do not inherit a parent's verdict without computing the child's evidence.
|
||||
|
||||
### Underperformance
|
||||
|
||||
- **Likely one-day noise:** `D` misses target, but recent 7D still meets it or remains
|
||||
stable, same-direction misses are not a majority, and no material risk is worsening.
|
||||
- **Mixed/emerging risk:** `D` and recent 7D disagree, multiple KPIs conflict, or the
|
||||
recent window deteriorates without enough consistent evidence.
|
||||
- **Persistent underperformance:** recent 7D misses target, at least 4 of 7 mature
|
||||
days miss in the same direction, and previous 7D does not show credible recovery.
|
||||
- **Sharp deterioration:** recent 7D materially worsens versus previous 7D while
|
||||
spend is stable or rising, even when the 14D aggregate hides the turn.
|
||||
|
||||
`4 of 7` is an evidence-consistency default, not a KPI. Replace it when the user has
|
||||
configured a different persistence rule.
|
||||
|
||||
### Good performance
|
||||
|
||||
- **One-day spike:** `D` looks strong but recent 7D, sample, or concentration does not
|
||||
support it.
|
||||
- **Improving candidate:** recent 7D improves and approaches or clears target, but
|
||||
consistency, maturity, or delivery headroom is not proven.
|
||||
- **Durable winner:** recent 7D meets target, at least 4 of 7 mature days support it,
|
||||
previous 7D is not contradictory, sample is sufficient, no hard guardrail is
|
||||
breached, and results are not dominated by one event or segment.
|
||||
|
||||
## 5. Handle daily spend and pacing
|
||||
|
||||
At the configured scope calculate:
|
||||
|
||||
- attainment = actual spend / target spend;
|
||||
- shortfall = max(target spend - actual spend, 0);
|
||||
- overage = max(actual spend - target spend, 0).
|
||||
|
||||
Label whether the spend target is a minimum, desired level, cap, or range. When spend
|
||||
is below a minimum/desired target, identify how much credible capacity exists among
|
||||
durable winners. Never solve a pacing gap by scaling entities that fail efficiency,
|
||||
quality, compliance, or data guardrails. If verified winner capacity is smaller than
|
||||
the gap, report the unfilled remainder and, when appropriate, propose bounded
|
||||
exploration rather than forced scale.
|
||||
|
||||
When spend meets or exceeds target, scaling one winner may require reallocating from
|
||||
losers rather than increasing portfolio spend. State that distinction.
|
||||
|
||||
## 6. Apply decision modifiers
|
||||
|
||||
Check every candidate for:
|
||||
|
||||
- spend exposure and estimated avoidable loss/opportunity;
|
||||
- minimum sample and cohort maturity;
|
||||
- launch age and recent budget, bid, creative, targeting, status, or tracking changes;
|
||||
- pacing, budget fill, learning limits, and delivery headroom;
|
||||
- funnel conflict, such as cheap acquisition with poor retention or weak ROAS;
|
||||
- concentration in a day, creative, placement, geo, device, or large revenue event;
|
||||
- possible creative fatigue supported by frequency/CPM/CTR/CVR evidence;
|
||||
- portfolio effects, shared budgets, audience overlap, or cannibalization;
|
||||
- data integrity and attribution lag;
|
||||
- prior intervention outcomes when available.
|
||||
|
||||
Phrase unsupported causal explanations as hypotheses. A recent intervention normally
|
||||
needs time to mature; extreme ongoing exposure can still justify urgent review.
|
||||
|
||||
When a recent change, scale event, restructuring, former winner, or rescue question is
|
||||
material to the verdict, apply the phase-aware method in
|
||||
[change-response.md](change-response.md) before choosing the action.
|
||||
|
||||
For the daily brief, do not mix new and mature entities in one leaderboard. Apply the
|
||||
two-pool method in [learning-stable-ranking.md](learning-stable-ranking.md).
|
||||
|
||||
## 7. Choose the intervention state
|
||||
|
||||
Use the smallest state that matches the evidence:
|
||||
|
||||
- **Pause/reduce:** persistent mature failure with adequate exposure and no credible
|
||||
recent-change or data-lag explanation.
|
||||
- **Rollback/recovery:** deterioration follows a known intervention and a credible
|
||||
prior control can be restored without sacrificing protected winners.
|
||||
- **Hold/protect:** evidence is promising or mixed, but changing variables now would
|
||||
destroy the ability to learn; keep the entity stable until the next mature check.
|
||||
- **Clean-control retest:** structure or traffic variables are entangled; preserve a
|
||||
control and isolate one hypothesis per test cell.
|
||||
- **Scale candidate:** durable winner evidence survives maturity, concentration, and
|
||||
downstream guardrail checks.
|
||||
|
||||
Rollback, hold, and retest are explicit actions, not vague observation. State which
|
||||
variables stay fixed, the mature observation window, and the exit condition.
|
||||
|
||||
### Pause/reduction candidate
|
||||
|
||||
Require persistent mature Ad-level underperformance, meaningful spend exposure,
|
||||
adequate confirmed sample, and no unresolved attribution, tracking, or recent-change
|
||||
explanation. A single bad day or low-sample zero outcome belongs in observation.
|
||||
|
||||
When only some Ads fail, protect the good Ads and recommend intervention on the named
|
||||
losers. Move to Ad Group only when most material Ads share the problem or the lever is
|
||||
available only there.
|
||||
|
||||
### Campaign-wide pause candidate
|
||||
|
||||
Treat this as exceptional. Require one of:
|
||||
|
||||
- broad persistent failure across the Campaign's material Ad Groups and Ads;
|
||||
- a Campaign-level structural constraint such as wrong objective/targeting, compliance
|
||||
risk, broken tracking, or unsafe delivery;
|
||||
- no durable child winner worth protecting and material ongoing loss.
|
||||
|
||||
Show the child-level evidence and spend coverage. Do not recommend Campaign pause from
|
||||
the aggregate alone.
|
||||
|
||||
### Scale candidate
|
||||
|
||||
Require durable mature Ad-level KPI performance, adequate confirmed sample, at least
|
||||
the configured consistency rule, no hard downstream guardrail breach, no severe result
|
||||
concentration, and plausible delivery headroom. A good day or a good Campaign average
|
||||
does not qualify an Ad.
|
||||
|
||||
Keep the recommendation platform-neutral. If direct Ad budget scaling is unsupported,
|
||||
suggest the applicable controlled lever—parent allocation, bid/target adjustment,
|
||||
duplication/test, or creative expansion—without claiming execution. Do not invent a
|
||||
scale percentage when no growth rule is configured.
|
||||
|
||||
## 8. Prioritize the human queue
|
||||
|
||||
Use three explicit judgments instead of a fake precise score:
|
||||
|
||||
- **Impact:** avoidable loss/opportunity or share of spend affected.
|
||||
- **Confidence:** target validity, maturity, sample, consistency, and data quality.
|
||||
- **Urgency:** whether delay increases loss or blocks safe interpretation.
|
||||
|
||||
Suggested priorities:
|
||||
|
||||
- **P0:** tracking, data, or delivery fault affecting material live spend.
|
||||
- **P1:** persistent/high-confidence Ad or Ad Group underperformance, rapid
|
||||
deterioration with material exposure, or broad child-level failure supporting a
|
||||
Campaign review.
|
||||
- **P2:** mixed evidence, pending intervention, low sample, or a durable Ad winner
|
||||
that needs an operator decision.
|
||||
- **P3:** no material action; summarize only.
|
||||
|
||||
Do not recommend a generic pause because one KPI missed. Match the human review to the
|
||||
entity and evidence: verify tracking, inspect a breakdown, protect a good Ad,
|
||||
cap/reduce/pause a named loser, restore a control, refresh creative, adjust
|
||||
targeting/bid, reallocate, or run a controlled scale test. Quantify how a proposed
|
||||
pause or rollback changes the pacing gap, and do not nominate an unstable early winner
|
||||
to compensate. These remain recommendations, not executed actions.
|
||||
|
||||
## 9. Recommended row shape
|
||||
|
||||
| Priority | Entity level | Campaign > Ad Group > Ad | State | D vs target | Recent 7D | Previous 7D | Change / pre-post | Spend/sample | Evidence and risk | Human action | Recheck/exit condition |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
|
||||
Each row should make clear what changed, against which confirmed target, over which
|
||||
exact mature window, with how much spend/sample, why this entity—not merely its
|
||||
parent—owns the action, and why the next review follows.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Delivery profile
|
||||
|
||||
Use this portable default when the user has not supplied an installation-level
|
||||
delivery preference. A user-supplied directory and filename convention always wins.
|
||||
|
||||
```yaml
|
||||
output_format: docx
|
||||
output_directory: ./output
|
||||
filename_pattern: "{analysis_date}-campaign-triage.docx"
|
||||
chat_behavior: completion_and_file_link_only
|
||||
```
|
||||
|
||||
Rules:
|
||||
|
||||
- `analysis_date` is the delivery/report date analyzed in the brief. Do not substitute
|
||||
the file-generation date when they differ.
|
||||
- Create the output directory when absent. Do not overwrite a same-named report until
|
||||
the new DOCX has passed structural and visual verification; build in a temporary
|
||||
path and replace only at final delivery.
|
||||
- Preserve the four-section format from `output-standard.md` inside the document.
|
||||
- Use a compact professional business-brief design with readable Chinese fonts,
|
||||
deliberate table widths, repeated headers for multi-page tables, and explicit
|
||||
maturity dates.
|
||||
- Do not place a `核心判断` summary between the title/date block and section 1.
|
||||
- Keep the visual treatment close to a restrained business brief. Do not use
|
||||
full-width section bands, large color blocks, forced section page breaks, or repeated
|
||||
callout boxes; these create excessive pagination when text wraps.
|
||||
- Use at most three visible design colors across the document. Prefer one dark blue
|
||||
for headings, one light blue-gray for table headers or thin rules, and neutral gray
|
||||
for secondary text. Do not assign a separate color to every status or section.
|
||||
- Separate hierarchy mainly through typography and spacing: numbered section heading,
|
||||
smaller subsection heading, evidence table, and concise labeled interpretation or
|
||||
action. A thin rule beneath a major heading is acceptable; shaded paragraphs are not
|
||||
the default.
|
||||
- Keep the action-oriented expression from the accepted direct version. Use short,
|
||||
plain labeled lines such as `待排查`, `趋势`, `结论`, `预算影响`,
|
||||
`继续观察`, `去重原则`, `预算情景`, `3 / 5 / 7天`, `筛选结果`, and `次优参考`
|
||||
where applicable. Bold only the label; do not turn each line into a colored card.
|
||||
- Do not include `Campaign层级`, a Campaign ranking table, or `层级判断`. The daily
|
||||
brief is Ad-focused; parent names appear only when needed to identify an Ad.
|
||||
- Do not carry a previously discussed named-Ad专项判断 into later daily briefs. Use
|
||||
named-Ad drilldowns only for an explicit follow-up request; retain the structure as
|
||||
a template, never the old entity name or historical values.
|
||||
- Do not mechanically insert a separate `结论` paragraph before every table. Let the
|
||||
table present comparable data, then place the nearest decision immediately beside
|
||||
it. In the action section, let the action table lead and follow it with quantified
|
||||
budget impact and observation instructions.
|
||||
- Use prose for short conclusions and tables only for genuinely comparable evidence.
|
||||
Keep tables able to flow naturally across pages with repeated headers; do not force
|
||||
a new page merely to keep an entire table or section together.
|
||||
- Render every generated DOCX to page images and inspect all pages when a compatible
|
||||
renderer is available. If the renderer is unavailable, perform structural OOXML
|
||||
checks and disclose that visual QA could not run.
|
||||
- Do not paste the complete brief into chat after successful file delivery unless the
|
||||
user explicitly asks for an inline copy.
|
||||
- Never commit user reports, generated briefs, absolute home-directory paths, account
|
||||
names, or installation-specific KPI into the reusable Skill. Store local overrides
|
||||
outside the repository or in an ignored `delivery-profile.local.md` file.
|
||||
@@ -0,0 +1,139 @@
|
||||
# Input and KPI contract
|
||||
|
||||
Use this reference when a report is new, its schema is unfamiliar, or KPI maturity
|
||||
and targets have not already been confirmed.
|
||||
|
||||
## 1. Identify the reporting grain
|
||||
|
||||
Determine whether each row represents a date, account, Campaign, Ad Group, Ad,
|
||||
creative, country, placement, or a combination. Preserve the full parent-child path.
|
||||
Do not aggregate rows across accounts, currencies, timezones, attribution definitions,
|
||||
or materially different objectives without explicit normalization.
|
||||
|
||||
Prefer stable IDs at every available level over display names. If only names exist,
|
||||
disclose collision and rename risk. Detect whether blank Campaign or Ad Group cells
|
||||
mean a visually merged/continued parent; forward-fill only inside the current date and
|
||||
parent block, then reset at boundaries.
|
||||
|
||||
Identify parent totals, subtotals, and leaf rows. Aggregate from one consistent grain,
|
||||
normally the finest trustworthy Ad rows. Never sum a Campaign/Ad Group total together
|
||||
with its children. Reconcile leaf totals to exported parents when both are available.
|
||||
|
||||
## 2. Map fields by meaning, not spelling
|
||||
|
||||
Map the available fields to these roles:
|
||||
|
||||
- dimensions: date, account, Campaign ID/name, Ad Group ID/name, Ad ID/name, status,
|
||||
objective, country, platform;
|
||||
- delivery: budget, daily spend/pacing target, spend, impressions, reach, clicks,
|
||||
installs, conversions, revenue;
|
||||
- quality/outcomes: retained users, purchases, qualified leads, downstream events;
|
||||
- ratios: CPM, CPC, CPI, CPA, CTR, CVR, retention, ROAS;
|
||||
- context: currency, timezone, attribution window, cohort eligibility, data watermark,
|
||||
launch date, intervention timestamps, and an intervention/change log.
|
||||
|
||||
Column aliases vary by platform and language. Never decide that a similarly named
|
||||
field is equivalent when its denominator, attribution window, or cohort definition is
|
||||
unclear. Show the proposed mapping for material fields.
|
||||
|
||||
When numerator and denominator are present, compute the aggregate ratio from them and
|
||||
use the exported ratio only as a cross-check. If only a precomputed ratio is present,
|
||||
state that weighted aggregation may be impossible.
|
||||
|
||||
## 3. Confirm the KPI contract
|
||||
|
||||
For each decision-relevant KPI, capture:
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| Role | Primary KPI, secondary KPI, or hard guardrail |
|
||||
| Formula | Numerator, denominator, and eligible population |
|
||||
| Direction | Lower-is-better, higher-is-better, or bounded range |
|
||||
| Target | Business threshold and optional tolerance |
|
||||
| Scope | Global, account, objective, product, geo, or Campaign-specific |
|
||||
| Attribution | Click/view window and event definition |
|
||||
| Maturity lag | When a cohort is complete enough to judge |
|
||||
| Minimum sample | Spend, clicks, installs, conversions, or eligible cohort |
|
||||
|
||||
KPI targets may differ across Campaigns, Ad Groups, or Ads. Apply the narrowest
|
||||
confirmed target matching the row and display which target was used.
|
||||
|
||||
Treat spend/pacing as its own contract:
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| Scope | Portfolio/account, platform, Campaign, or Ad Group |
|
||||
| Period | Daily, weekly, flight-to-date, or another pacing window |
|
||||
| Target type | Minimum delivery, desired level, maximum cap, or range |
|
||||
| Target value | Amount and currency |
|
||||
| Allocation rule | Whether and where incremental spend may be assigned |
|
||||
|
||||
Compute target attainment and gap/overage at the configured scope. A minimum daily
|
||||
spend goal does not override efficiency or quality guardrails. Report the numerical
|
||||
shortfall, but do not automatically distribute it to weak Ads.
|
||||
|
||||
If any field needed for a verdict is missing, ask a concise question. A useful form is:
|
||||
|
||||
> 请确认主要 KPI、目标值和方向;日消耗目标的作用范围;每个滞后指标最晚成熟到哪一天;是否有最低样本门槛或分层目标?
|
||||
|
||||
Do not infer a business target from industry benchmarks, historical averages, file
|
||||
names, source code, or another user's configuration.
|
||||
|
||||
## 4. Capture intervention history
|
||||
|
||||
When the question involves scaling, sudden deterioration, rollback, a former winner,
|
||||
or a restructured Campaign, capture each known manual or automated change:
|
||||
|
||||
| Field | Meaning |
|
||||
|---|---|
|
||||
| Timestamp | Effective time in the reporting timezone |
|
||||
| Entity path | Campaign, Ad Group, or Ad and stable ID |
|
||||
| Change type | Budget, bid, targeting, word pack, match type, creative, status, structure, tracking, or platform automation |
|
||||
| Before / after | Previous and new value or configuration |
|
||||
| Reason | Operator hypothesis or system trigger, if known |
|
||||
| Expected maturity | First cohort/date on which the change can be judged |
|
||||
|
||||
Keep budget, configured caps, and observed spend distinct. A spend plateau or jump can
|
||||
suggest a delivery phase, but it does not prove that the budget changed. When the file
|
||||
contains spend but no budget history, label any inferred phase boundary as an
|
||||
inference and ask for the change log only if it could materially alter the action.
|
||||
|
||||
If there is no formal log, accept dated operator statements as declared context and
|
||||
mark their provenance. Do not silently turn an approximate recollection into an exact
|
||||
timestamp.
|
||||
|
||||
## 5. Resolve metric-specific mature dates
|
||||
|
||||
Let each KPI have its own latest mature date `D_metric`. Examples:
|
||||
|
||||
- spend, impressions, and clicks may mature on the latest complete reporting day;
|
||||
- installs or purchases may require an attribution delay;
|
||||
- D1 retention for an August 23 cohort cannot be treated as mature until its defined
|
||||
next-day observation is complete, so its latest usable cohort may be August 22;
|
||||
- D7 retention and revenue windows may lag further.
|
||||
|
||||
Report `D_metric` beside the KPI. Build recent and prior windows relative to that
|
||||
metric's mature date. Do not compare an immature value with a mature target.
|
||||
|
||||
If maturity cannot be established, present the value as provisional and exclude it
|
||||
from strong intervention claims.
|
||||
|
||||
## 6. Data-quality checks
|
||||
|
||||
Before interpretation, check:
|
||||
|
||||
- missing or duplicated entity-date rows;
|
||||
- inconsistent date formats, timezones, currencies, or units;
|
||||
- blanks confused with zero;
|
||||
- zero delivery versus absent ingestion;
|
||||
- totals mixed with detail rows;
|
||||
- hierarchy blanks incorrectly treated as missing parents or forwarded across blocks;
|
||||
- parent totals double-counted with Ad Group/Ad detail;
|
||||
- negative or impossible counts and rates;
|
||||
- spend with missing outcomes after the expected maturity lag;
|
||||
- abrupt schema, tracking, attribution, or target changes;
|
||||
- known intervention dates missing from the export or conflicting with delivery;
|
||||
- insufficient history for complete 7D/14D comparisons.
|
||||
|
||||
Do not silently discard questionable rows. Quantify affected spend and Campaigns when
|
||||
possible, and decide whether the issue blocks only one KPI or the whole report.
|
||||
@@ -0,0 +1,127 @@
|
||||
# Learning-period and stable-period ranking
|
||||
|
||||
Apply this two-pool method in every daily brief. The purpose is to prevent fresh tests
|
||||
from being judged by mature-entity standards while still pruning obvious failures
|
||||
quickly.
|
||||
|
||||
## 1. Define age and comparable peers
|
||||
|
||||
Use confirmed launch/effective dates when available. Otherwise infer launch from the
|
||||
first eligible delivery date and label it as inferred.
|
||||
|
||||
- Use calendar age since confirmed launch to detect zero-delivery problems.
|
||||
- Use delivered days and KPI-mature cohort days to judge performance.
|
||||
- A lagged KPI may have fewer mature cohorts than the entity's calendar age; never
|
||||
count immature values as failures.
|
||||
- A major reset to creative, targeting, bid, word pack, or structure may re-enter the
|
||||
affected entity into a learning cohort when it materially changes traffic.
|
||||
|
||||
Rank peers only within a comparable scope: same entity level, objective/KPI contract,
|
||||
currency, attribution regime, and sufficiently similar launch cohort. Rank sibling Ads
|
||||
launched together within their parent before comparing them portfolio-wide.
|
||||
|
||||
## 2. Rule one — learning-period pool
|
||||
|
||||
Treat a new or materially reset Ad as learning-period through the day-7 checkpoint
|
||||
unless the operator configures another window.
|
||||
|
||||
### Day 3: early selection
|
||||
|
||||
Rank simultaneous tests by this ordered evidence, without inventing a composite score:
|
||||
|
||||
1. no hard-guardrail breach;
|
||||
2. primary KPI result on mature data;
|
||||
3. number of confirmed KPIs meeting target;
|
||||
4. distance from target and direction of trend;
|
||||
5. mature sample and result concentration;
|
||||
6. delivery/budget fill and spend impact.
|
||||
|
||||
Actions:
|
||||
|
||||
- Confirmed launch with zero spend through three calendar days: label `无投放学习失败`;
|
||||
inspect review/status/bid/audience/data issues and recommend pause/rebuild if no
|
||||
correctable delivery block exists.
|
||||
- Spend exists, every mature KPI fails, and sample is adequate: rank last and recommend
|
||||
pause or reduction at day 3. If sample is inadequate, state the exact day-5 sample or
|
||||
spend stop condition rather than treating missing outcomes as proof.
|
||||
- Some KPIs meet target and delivery/budget is filling: keep stable through day 5–7;
|
||||
specify which KPI must mature and what failure would stop the test.
|
||||
- A peer clearly leads on mature KPIs: label `学习期扩量素材候选`, but do not call it a
|
||||
durable winner yet.
|
||||
|
||||
### Day 5: confirmation
|
||||
|
||||
- Eliminate persistent bottom-ranked peers when mature evidence and sample support it.
|
||||
- Continue mixed but improving or partially compliant entities to day 7 with variables
|
||||
fixed.
|
||||
- Promote consistent leaders to a bounded scale-test candidate only when downstream
|
||||
guardrails are mature enough; otherwise keep the candidate label.
|
||||
|
||||
### Day 7: graduate or stop
|
||||
|
||||
- Graduate to the stable pool when the entity has adequate mature sample and an
|
||||
interpretable run.
|
||||
- Mark as a scale candidate when multi-day KPI support, downstream guardrails, and
|
||||
headroom are credible.
|
||||
- Pause/reduce or redesign persistent failures. Do not extend `learning` indefinitely
|
||||
to avoid a decision.
|
||||
|
||||
At each checkpoint, show rank movement where earlier checkpoints are available.
|
||||
|
||||
## 3. Rule two — stable-period pool
|
||||
|
||||
Classify entities that graduated from learning or have an established run into one of
|
||||
four tiers:
|
||||
|
||||
### S-优质
|
||||
|
||||
Recent 7D exceeds the confirmed KPI targets with multi-day stability, adequate sample,
|
||||
and no severe concentration or recent deterioration. Rank the strongest durable Ads
|
||||
first.
|
||||
|
||||
### A-稳定
|
||||
|
||||
The confirmed KPIs meet target with adequate sample and acceptable multi-day
|
||||
consistency. Protect or consider bounded scaling subject to creative age and headroom.
|
||||
|
||||
### B-波动待优化
|
||||
|
||||
Results fluctuate, only some KPIs meet target, or performance hovers near thresholds,
|
||||
but there is still a specific optimization or controlled-retest path. Keep, cap,
|
||||
reduce, or test one bounded improvement hypothesis with an exit condition.
|
||||
|
||||
### C-劣质
|
||||
|
||||
The primary KPI or multiple core KPIs seriously or persistently fail on mature data
|
||||
with meaningful exposure, or performance sharply deteriorates without credible
|
||||
recovery. Treat these as risk Ads and rank by avoidable loss and confidence for
|
||||
pause/reduction review.
|
||||
|
||||
Within each tier use the same ordered evidence as the learning pool, then use spend
|
||||
impact to prioritize operator attention. Every stable-pool row must include a concrete
|
||||
`决定` column.
|
||||
|
||||
## 4. Historical winner deterioration and new-material alert
|
||||
|
||||
Flag a creative/material alert when an established Ad has a credible strong historical
|
||||
control but recent mature performance deteriorates materially and persistently. Show:
|
||||
|
||||
- total run age and historically strong window;
|
||||
- exact recent 7D versus prior 7D or pre-change evidence;
|
||||
- which funnel stage worsened;
|
||||
- whether spend, budget, targeting, placement, or audience mix changed;
|
||||
- fatigue as a hypothesis only when frequency/CPM/CTR/CVR or creative-age evidence
|
||||
supports it.
|
||||
|
||||
Recommend preparing or testing new material when the decline is not explained by data
|
||||
lag or an easily reversible traffic change. Protect any still-performing sibling Ads.
|
||||
|
||||
## 5. Ranking output invariants
|
||||
|
||||
- Show an ordinal rank and the decisive reason; do not present a fake precise score.
|
||||
- Rank Ads only; do not output Campaign or Ad Group leaderboards.
|
||||
- Show KPI pass count only alongside the primary KPI and hard guardrails; KPI counts
|
||||
alone do not determine quality.
|
||||
- State whether budget fill uses configured budget or a delivery proxy.
|
||||
- Recompute rank when mature cohorts arrive; do not retroactively treat immature zeros
|
||||
as valid evidence.
|
||||
@@ -0,0 +1,119 @@
|
||||
# Standard daily brief format
|
||||
|
||||
Follow this four-section structure exactly. Keep the brief decision-led, but include
|
||||
exact dates, mature windows, and enough evidence to audit each action.
|
||||
|
||||
## 1. 是否达标
|
||||
|
||||
Use compact lines:
|
||||
|
||||
```text
|
||||
1. 是否达标
|
||||
- KPI 1:实际值(达标/未达标;数据日期/成熟 cohort 日期;目标)
|
||||
- KPI 2:实际值(达标/未达标;数据日期/成熟 cohort 日期;目标)
|
||||
- 日消耗:实际值 / 目标值,差额 xx(达标/未达标;目标类型和范围)
|
||||
```
|
||||
|
||||
Evaluate at the confirmed portfolio/account scope unless a narrower scope was given.
|
||||
Never use the same date automatically for same-day acquisition and lagged retention or
|
||||
revenue KPIs. Add at most two short caveats here for material maturity or data issues.
|
||||
If KPI Gate B is unsatisfied, state `无法判定` and ask one consolidated question before
|
||||
producing action labels.
|
||||
|
||||
## 2. 问题分析
|
||||
|
||||
Start directly with the Ad-level learning-period and stable-period pools. Do not add a
|
||||
Campaign ranking table or `Campaign层级/层级判断` subsection. Campaign and Ad Group may
|
||||
appear only in an Ad identity when duplicate names require disambiguation. Keep each
|
||||
decision adjacent to its evidence, using a compact labeled line rather than a separate
|
||||
visual card.
|
||||
|
||||
### 学习期池排名
|
||||
|
||||
| Rank | Ad | Age / checkpoint | Spend / budget fill | Mature KPI result | Peer position | Decision |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
|
||||
Rank comparable new Ads within the same objective and launch cohort. When multiple Ads
|
||||
launch together, rank them within that test cohort. Explain day-3 pruning and
|
||||
day-5/day-7 observation or promotion decisions.
|
||||
|
||||
### 稳定期池排名
|
||||
|
||||
| Rank | Tier | Ad | Latest mature day | Recent 7D / prior 7D | KPI pass pattern | Stability / change | Diagnosis | Decision |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
|
||||
Use exactly these tiers:
|
||||
|
||||
- `S-优质`:KPI超额完成且多日稳定。
|
||||
- `A-稳定`:KPI达标,样本与持续性足够。
|
||||
- `B-波动待优化`:存在波动或部分KPI未达成,但仍有明确优化空间。
|
||||
- `C-劣质`:KPI严重或持续不达标,属于风险项。
|
||||
|
||||
Rank Ads within and across tiers by primary KPI, number of KPI passes, persistence,
|
||||
sample confidence, and spend impact. Every row must include a concrete `决定` such as
|
||||
protect, hold, reduce, optimize/retest, pause, or replace creative, together with its
|
||||
checkpoint or exit condition when observation continues.
|
||||
|
||||
## 3. 建议动作
|
||||
|
||||
Group recommendations under only the headings that have qualifying rows:
|
||||
|
||||
```text
|
||||
3. 建议动作
|
||||
- 暂停:<完整层级名称>;原因:<证据>;释放配置预算 xx。
|
||||
- 下调:<完整层级名称>;表现波动,建议预算从 xx 下调至 xx;保持至 <成熟日期/样本> 后复查;退出条件:xx。
|
||||
- 保持观察:<完整层级名称>;当前通过 xx/总 KPI;保持哪些变量不变;在第 5/7 天或指定成熟日复查。
|
||||
- 扩量:<Ad 名称>;近 xx 个成熟日表现很好;建议采用 <可用的受控扩量方式>;回退条件:xx。
|
||||
- 补量:距离日消耗目标还差 xx;现有合格扩量容量 xx;仍缺 xx,需新增 Ads <数量或“待测试预算确认”>。
|
||||
- 素材预警:<Ad 名称> 已连续跑 xx 天;历史表现好但近期严重下滑;建议验证疲劳/流量变化并准备新素材。
|
||||
```
|
||||
|
||||
Budget rules:
|
||||
|
||||
- `配置预算` comes only from an explicit budget field or operator-provided history.
|
||||
- If only spend is available, write `预计减少/释放日消耗约 xx` and label the estimate;
|
||||
never call spend a budget.
|
||||
- State an exact reduction only when a configured adjustment rule or credible prior
|
||||
control exists. Otherwise state the direction and the missing value needed.
|
||||
- After pause/reduction recommendations, recompute the expected pacing gap.
|
||||
- Estimate the number of new Ads only when a standard test budget or a defensible
|
||||
observed test-spend baseline exists; otherwise request that one value.
|
||||
|
||||
Every observation action must include a checkpoint and exit condition. A recommendation
|
||||
never implies execution.
|
||||
|
||||
## 4. 近7天表现好的 Ads(3–5个)
|
||||
|
||||
| Rank | Ad(include Campaign > Group) | Stage | Spend / sample | KPI data | Consistency | Why good | Next use |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
|
||||
Choose three to five Ads only when they genuinely qualify. Rank durable stable Ads
|
||||
before learning-period candidates. A learning Ad may appear only when clearly labeled
|
||||
`学习期候选`, with its checkpoint and concentration risk. Return one or two, or state
|
||||
`暂无合格 Ads`, rather than pad the list with weak traffic.
|
||||
|
||||
Explain why each is good using confirmed KPI performance, multi-day consistency,
|
||||
sample, trend, and delivery—not adjectives alone. Keep immature outcomes out of the
|
||||
negative evidence.
|
||||
|
||||
End with `以上均为人工决策建议,未执行任何平台变更。`
|
||||
|
||||
## Writing rules
|
||||
|
||||
- Use the user's four Chinese section names and order; do not add a separate executive
|
||||
summary or duplicate action table.
|
||||
- Do not add a title-page or pre-section `核心判断` paragraph. Begin the report body
|
||||
directly with `1. 是否达标` after the title/date block.
|
||||
- Use the exact subsection titles `学习期池排名` and `稳定期池排名`; never prefix them
|
||||
with `规则一` or `规则二`.
|
||||
- Do not add a named-Ad `近7日专项判断` section to the standard daily brief. Insights
|
||||
from prior conversation about a specific Ad belong in the reusable follow-up
|
||||
template, not as hardcoded daily-report content, unless the user explicitly asks to
|
||||
include that Ad's drilldown in the report.
|
||||
- Use exact calendar dates and exact mature-window ranges.
|
||||
- Keep each conclusion or action close to its evidence and avoid repeated colored
|
||||
callout boxes. Prefer direct labels such as `预算影响` and `继续观察` over generic
|
||||
analysis prose.
|
||||
- Separate observed facts, inference, and unverified causes.
|
||||
- Show numerator/denominator for low-sample or concentration-sensitive ratios.
|
||||
- If a subsection is empty, say `无` and why.
|
||||
@@ -0,0 +1,58 @@
|
||||
# Script usage
|
||||
|
||||
The scripts are deterministic helpers. They prepare evidence and render the final
|
||||
artifact; the Skill instructions still own KPI confirmation, interpretation, and
|
||||
action judgment.
|
||||
|
||||
## 1. Normalize a report
|
||||
|
||||
```bash
|
||||
python3 scripts/analyze_report.py report.xlsx \
|
||||
--analysis-date 2026-08-25 \
|
||||
--retention-lag-days 1 \
|
||||
--output /tmp/report.analysis.json
|
||||
```
|
||||
|
||||
Supported inputs are `.xlsx`, `.csv`, and `.tsv`. The analyzer uses the first sheet
|
||||
by default. Use `--sheet SHEET_NAME` to select another sheet. It recognizes common
|
||||
English and Chinese names for date, Campaign, Ad Group, Ad, spend, installs, and D1
|
||||
retained users. If the export differs, pass a JSON mapping:
|
||||
|
||||
```json
|
||||
{
|
||||
"date": "统计日期",
|
||||
"campaign": "系列",
|
||||
"ad_group": "广告组",
|
||||
"ad": "广告",
|
||||
"spend": "花费 USD",
|
||||
"installs": "安装",
|
||||
"retained": "D1 用户"
|
||||
}
|
||||
```
|
||||
|
||||
The evidence JSON deliberately contains no KPI verdict or action. Ratios are always
|
||||
recomputed from summed components. Retention windows end on the explicit mature date;
|
||||
acquisition windows end on the analysis date.
|
||||
|
||||
## 2. Apply the decision framework
|
||||
|
||||
Read the evidence JSON, confirm the user's KPI card and pacing card, then fill a brief
|
||||
JSON matching `examples/brief.example.json`. Do not mechanically classify Ads from a
|
||||
single ratio. Use sample, persistence, trend, maturity, learning stage, recent changes,
|
||||
and budget impact as required by the Skill.
|
||||
|
||||
## 3. Build the Word brief
|
||||
|
||||
```bash
|
||||
python3 scripts/build_brief.py /tmp/brief.json \
|
||||
--output ./output/2026-08-25-campaign-triage.docx
|
||||
```
|
||||
|
||||
The renderer enforces the stable tier names and rejects forbidden daily-brief sections
|
||||
such as Campaign ranking, `核心判断`, and stale named-Ad drilldowns. It uses only three
|
||||
visual colors and lets tables flow naturally without forced section page breaks.
|
||||
|
||||
## Output safety
|
||||
|
||||
Write temporary analysis JSON and generated briefs outside the repository. Never add
|
||||
the user's source workbook or a real advertiser name to examples, fixtures, or assets.
|
||||
@@ -0,0 +1,56 @@
|
||||
# VK optimization guidance
|
||||
|
||||
Read this reference when the source is a VK Ads export or the user wants guidance
|
||||
that can be applied inside the VK advertising ecosystem.
|
||||
|
||||
## Translate evidence into operator levers
|
||||
|
||||
Keep the primary diagnosis at Ad level. Use Campaign and Ad Group only to locate the
|
||||
Ad, detect shared constraints, or describe the platform lever the operator must use.
|
||||
|
||||
Choose the smallest justified lever:
|
||||
|
||||
- **Creative control:** pause a persistently poor Ad, protect a mature winner, or copy
|
||||
a proven creative direction into a clean single-variable test. Change one meaningful
|
||||
variable at a time: opening hook, visual sequence, message, format, or CTA.
|
||||
- **Learning protection:** do not scale an Ad from one or two good days. Rank new Ads
|
||||
within the same learning pool and apply the 3 / 5 / 7-day checkpoints using mature
|
||||
KPI and adequate sample.
|
||||
- **Budget recovery:** after a sharp post-scale decline, recommend a rollback toward
|
||||
the last credible control budget and define a mature recheck window. A rollback is
|
||||
a recovery experiment, not proof that performance has returned.
|
||||
- **Audience or word-pack isolation:** when a previously strong combination fails
|
||||
after restructuring, separate creative fatigue from audience/keyword delivery.
|
||||
Recommend a clean control using the prior proven combination when its dated history
|
||||
exists; otherwise label the cause as a hypothesis.
|
||||
- **Controlled scaling:** scale durable winners through bounded budget increases or
|
||||
new isolated copies, subject to the user's platform practice and risk limit. Never
|
||||
fill a spend gap by forcing volume into an immature or deteriorating Ad.
|
||||
- **Delivery troubleshooting:** zero spend is not automatically poor performance.
|
||||
Ask the operator to inspect VK review/status, bid competitiveness, audience size,
|
||||
schedule, budget availability, and parent-level delivery constraints when the export
|
||||
cannot explain delivery.
|
||||
|
||||
## Action wording
|
||||
|
||||
Write recommendations so an operator can act without translating vague strategy:
|
||||
|
||||
```text
|
||||
动作:回退预算并观察
|
||||
对象:Creative Alpha(Group One)
|
||||
依据:预算提升后,连续3个成熟日的CPI和次留成本同时恶化
|
||||
建议:回到最近稳定预算;保持素材、受众和出价不变
|
||||
复查:3个成熟日后;若仍未恢复则停止恢复测试
|
||||
```
|
||||
|
||||
Every action should identify the Ad, evidence window, proposed lever, controlled
|
||||
variables, recheck date or sample, and exit condition. If the export has no budget,
|
||||
bid, status, audience, or change-history fields, state that limitation instead of
|
||||
inventing the exact VK setting.
|
||||
|
||||
## Safety boundary
|
||||
|
||||
This Skill provides VK-relevant decision support only. Do not claim to have changed
|
||||
VK status, budget, bid, audience, word pack, or creative. If the user separately asks
|
||||
for execution, require an execution-capable VK workflow and confirm the exact objects
|
||||
and mutations immediately before applying them.
|
||||
@@ -0,0 +1,261 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Normalize CSV/TSV/XLSX ad reports into deterministic Ad-level evidence JSON."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import csv
|
||||
import datetime as dt
|
||||
import json
|
||||
import math
|
||||
import re
|
||||
import sys
|
||||
import zipfile
|
||||
from collections import defaultdict
|
||||
from pathlib import Path
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
NS = {"m": "http://schemas.openxmlformats.org/spreadsheetml/2006/main",
|
||||
"r": "http://schemas.openxmlformats.org/officeDocument/2006/relationships"}
|
||||
|
||||
ALIASES = {
|
||||
"date": ["date", "日期", "day", "report date"],
|
||||
"campaign": ["campaign", "campaign name", "平台 campaign name", "external_campaign_name"],
|
||||
"ad_group": ["ad group", "ad group name", "平台 ad group name", "external_ad_group_name", "adgroup"],
|
||||
"ad": ["ad", "ad name", "平台 ad name", "external_ad_name", "广告名称", "素材名称"],
|
||||
"spend": ["spend", "cost", "消耗", "花费", "广告花费"],
|
||||
"installs": ["installs", "install", "归因安装量", "安装量", "转化数"],
|
||||
"retained": ["d1 retained", "d1 retention users", "d1 留存人数", "次留人数", "次日留存人数"],
|
||||
"impressions": ["impressions", "展示量", "曝光量"],
|
||||
"clicks": ["clicks", "点击量", "归因点击量"],
|
||||
}
|
||||
|
||||
|
||||
def norm(value: object) -> str:
|
||||
return re.sub(r"[\s_\-/]+", " ", str(value or "").strip().lower())
|
||||
|
||||
|
||||
def col_index(reference: str) -> int:
|
||||
letters = re.match(r"[A-Z]+", reference or "A").group(0)
|
||||
n = 0
|
||||
for char in letters:
|
||||
n = n * 26 + ord(char) - 64
|
||||
return n - 1
|
||||
|
||||
|
||||
def xlsx_rows(path: Path, sheet_name: str | None = None):
|
||||
with zipfile.ZipFile(path) as archive:
|
||||
shared = []
|
||||
if "xl/sharedStrings.xml" in archive.namelist():
|
||||
root = ET.fromstring(archive.read("xl/sharedStrings.xml"))
|
||||
for item in root.findall("m:si", NS):
|
||||
shared.append("".join(node.text or "" for node in item.iter(f"{{{NS['m']}}}t")))
|
||||
workbook = ET.fromstring(archive.read("xl/workbook.xml"))
|
||||
rels_root = ET.fromstring(archive.read("xl/_rels/workbook.xml.rels"))
|
||||
rels = {item.attrib["Id"]: item.attrib["Target"] for item in rels_root}
|
||||
sheets = []
|
||||
for sheet in workbook.find("m:sheets", NS):
|
||||
target = rels[sheet.attrib[f"{{{NS['r']}}}id"]].lstrip("/")
|
||||
if not target.startswith("xl/"):
|
||||
target = "xl/" + target
|
||||
sheets.append((sheet.attrib["name"], target))
|
||||
if sheet_name:
|
||||
matches = [entry for entry in sheets if entry[0] == sheet_name]
|
||||
if not matches:
|
||||
raise ValueError(f"Sheet not found: {sheet_name}; available: {', '.join(x[0] for x in sheets)}")
|
||||
sheets = matches
|
||||
name, target = sheets[0]
|
||||
root = ET.fromstring(archive.read(target))
|
||||
for row in root.findall(".//m:sheetData/m:row", NS):
|
||||
values = {}
|
||||
for cell in row.findall("m:c", NS):
|
||||
idx = col_index(cell.attrib.get("r", "A1"))
|
||||
kind = cell.attrib.get("t")
|
||||
value_node = cell.find("m:v", NS)
|
||||
inline = cell.find("m:is", NS)
|
||||
raw = value_node.text if value_node is not None else ""
|
||||
if kind == "s" and raw:
|
||||
value = shared[int(raw)]
|
||||
elif kind == "inlineStr" and inline is not None:
|
||||
value = "".join(n.text or "" for n in inline.iter(f"{{{NS['m']}}}t"))
|
||||
elif kind == "b":
|
||||
value = raw == "1"
|
||||
else:
|
||||
value = raw
|
||||
values[idx] = value
|
||||
if values:
|
||||
yield name, [values.get(i, "") for i in range(max(values) + 1)]
|
||||
|
||||
|
||||
def tabular_rows(path: Path, sheet: str | None = None):
|
||||
suffix = path.suffix.lower()
|
||||
if suffix == ".xlsx":
|
||||
iterator = xlsx_rows(path, sheet)
|
||||
try:
|
||||
sheet_name, headers = next(iterator)
|
||||
except StopIteration:
|
||||
raise ValueError("Workbook contains no rows")
|
||||
for _, row in iterator:
|
||||
yield sheet_name, headers, row
|
||||
return
|
||||
delimiter = "\t" if suffix == ".tsv" else ","
|
||||
with path.open("r", encoding="utf-8-sig", newline="") as handle:
|
||||
reader = csv.reader(handle, delimiter=delimiter)
|
||||
try:
|
||||
headers = next(reader)
|
||||
except StopIteration:
|
||||
raise ValueError("File contains no rows")
|
||||
for row in reader:
|
||||
yield path.name, headers, row
|
||||
|
||||
|
||||
def resolve_columns(headers: list[str], mapping: dict[str, str] | None = None) -> dict[str, int]:
|
||||
normalized = {norm(header): i for i, header in enumerate(headers)}
|
||||
result = {}
|
||||
for field, aliases in ALIASES.items():
|
||||
requested = (mapping or {}).get(field)
|
||||
candidates = [requested] if requested else aliases
|
||||
for candidate in candidates:
|
||||
if norm(candidate) in normalized:
|
||||
result[field] = normalized[norm(candidate)]
|
||||
break
|
||||
missing = [field for field in ("date", "ad", "spend", "installs") if field not in result]
|
||||
if missing:
|
||||
raise ValueError(f"Missing required columns: {', '.join(missing)}. Headers: {headers}")
|
||||
return result
|
||||
|
||||
|
||||
def number(value: object) -> float:
|
||||
text = str(value or "").strip().replace(",", "").replace("$", "").replace("¥", "")
|
||||
if not text or text.lower() in {"nan", "none", "null", "-"}:
|
||||
return 0.0
|
||||
if text.endswith("%"):
|
||||
return float(text[:-1]) / 100
|
||||
return float(text)
|
||||
|
||||
|
||||
def date_value(value: object) -> dt.date:
|
||||
text = str(value or "").strip()
|
||||
if re.fullmatch(r"\d+(\.\d+)?", text):
|
||||
serial = int(float(text))
|
||||
if serial > 1000:
|
||||
return dt.date(1899, 12, 30) + dt.timedelta(days=serial)
|
||||
for fmt in ("%Y-%m-%d", "%Y/%m/%d", "%Y.%m.%d", "%m/%d/%Y", "%d/%m/%Y"):
|
||||
try:
|
||||
return dt.datetime.strptime(text[:10], fmt).date()
|
||||
except ValueError:
|
||||
pass
|
||||
try:
|
||||
return dt.datetime.fromisoformat(text.replace("Z", "+00:00")).date()
|
||||
except ValueError as exc:
|
||||
raise ValueError(f"Unrecognized date: {text!r}") from exc
|
||||
|
||||
|
||||
def ratio(numerator: float, denominator: float):
|
||||
return None if denominator == 0 else numerator / denominator
|
||||
|
||||
|
||||
def rounded(value):
|
||||
if value is None:
|
||||
return None
|
||||
return round(value, 6)
|
||||
|
||||
|
||||
def metrics(rows):
|
||||
spend = sum(r["spend"] for r in rows)
|
||||
installs = sum(r["installs"] for r in rows)
|
||||
retained = sum(r["retained"] for r in rows)
|
||||
return {
|
||||
"spend": rounded(spend), "installs": rounded(installs), "retained": rounded(retained),
|
||||
"cpi": rounded(ratio(spend, installs)),
|
||||
"retention_rate": rounded(ratio(retained, installs)),
|
||||
"retention_cost": rounded(ratio(spend, retained)),
|
||||
"delivery_days": len({r["date"] for r in rows if r["spend"] > 0}),
|
||||
}
|
||||
|
||||
|
||||
def in_window(rows, end: dt.date, days: int):
|
||||
start = end - dt.timedelta(days=days - 1)
|
||||
return [row for row in rows if start <= row["date"] <= end]
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("input", type=Path)
|
||||
parser.add_argument("--analysis-date", help="YYYY-MM-DD; defaults to latest source date")
|
||||
parser.add_argument("--retention-lag-days", type=int, default=1)
|
||||
parser.add_argument("--sheet")
|
||||
parser.add_argument("--mapping", type=Path, help="JSON mapping canonical fields to source headers")
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args(argv)
|
||||
if not args.input.exists():
|
||||
parser.error(f"Input not found: {args.input}")
|
||||
mapping = json.loads(args.mapping.read_text(encoding="utf-8")) if args.mapping else None
|
||||
records, source_headers, source_sheet = [], None, None
|
||||
for source_sheet, source_headers, row in tabular_rows(args.input, args.sheet):
|
||||
columns = resolve_columns(source_headers, mapping)
|
||||
def get(field):
|
||||
idx = columns.get(field)
|
||||
return row[idx] if idx is not None and idx < len(row) else ""
|
||||
if not any(str(v).strip() for v in row):
|
||||
continue
|
||||
ad = str(get("ad")).strip()
|
||||
if not ad:
|
||||
continue
|
||||
records.append({
|
||||
"date": date_value(get("date")), "campaign": str(get("campaign")).strip(),
|
||||
"ad_group": str(get("ad_group")).strip(), "ad": ad,
|
||||
"spend": number(get("spend")), "installs": number(get("installs")),
|
||||
"retained": number(get("retained")), "impressions": number(get("impressions")),
|
||||
"clicks": number(get("clicks")),
|
||||
})
|
||||
if not records:
|
||||
raise ValueError("No usable Ad-level records found")
|
||||
latest = max(row["date"] for row in records)
|
||||
analysis_date = dt.date.fromisoformat(args.analysis_date) if args.analysis_date else latest
|
||||
mature_date = analysis_date - dt.timedelta(days=args.retention_lag_days)
|
||||
by_ad = defaultdict(list)
|
||||
for row in records:
|
||||
by_ad[(row["campaign"], row["ad_group"], row["ad"])].append(row)
|
||||
ads = []
|
||||
for (campaign, ad_group, ad), rows in by_ad.items():
|
||||
all_dates = [r["date"] for r in rows]
|
||||
acquisition_7 = in_window(rows, analysis_date, 7)
|
||||
retention_7 = in_window(rows, mature_date, 7)
|
||||
previous_7 = in_window(rows, mature_date - dt.timedelta(days=7), 7)
|
||||
ads.append({
|
||||
"ad": ad, "ad_group_context": ad_group or None, "campaign_context": campaign or None,
|
||||
"first_date": min(all_dates).isoformat(), "last_date": max(all_dates).isoformat(),
|
||||
"age_days_at_analysis": (analysis_date - min(all_dates)).days + 1,
|
||||
"analysis_day": metrics([r for r in rows if r["date"] == analysis_date]),
|
||||
"recent_7_acquisition": metrics(acquisition_7),
|
||||
"recent_7_mature_retention": metrics(retention_7),
|
||||
"previous_7_mature_retention": metrics(previous_7),
|
||||
"recent_14_mature_retention": metrics(in_window(rows, mature_date, 14)),
|
||||
})
|
||||
ads.sort(key=lambda x: (-x["recent_7_acquisition"]["spend"], x["ad"]))
|
||||
result = {
|
||||
"contract_version": "1.0", "source": {"file": args.input.name, "sheet": source_sheet,
|
||||
"row_count": len(records), "columns": source_headers},
|
||||
"dates": {"analysis_date": analysis_date.isoformat(), "latest_source_date": latest.isoformat(),
|
||||
"retention_mature_date": mature_date.isoformat(), "retention_lag_days": args.retention_lag_days},
|
||||
"portfolio": {"analysis_day": metrics([r for r in records if r["date"] == analysis_date]),
|
||||
"recent_7_acquisition": metrics(in_window(records, analysis_date, 7)),
|
||||
"recent_7_mature_retention": metrics(in_window(records, mature_date, 7))},
|
||||
"ads": ads,
|
||||
"notes": ["Ratios are recomputed from summed numerators and denominators.",
|
||||
"Campaign and Ad Group are identity context only; no parent-level ranking is produced.",
|
||||
"This file contains evidence only. KPI verdicts and actions require the Skill decision gates."],
|
||||
}
|
||||
args.output.parent.mkdir(parents=True, exist_ok=True)
|
||||
args.output.write_text(json.dumps(result, ensure_ascii=False, indent=2), encoding="utf-8")
|
||||
print(f"Wrote {args.output} ({len(ads)} Ads; analysis date {analysis_date})")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
raise SystemExit(main())
|
||||
except (ValueError, zipfile.BadZipFile, json.JSONDecodeError) as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
@@ -0,0 +1,181 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Build a restrained four-section DOCX from the Skill's brief JSON contract."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import datetime as dt
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from xml.sax.saxutils import escape
|
||||
from zipfile import ZIP_DEFLATED, ZipFile
|
||||
|
||||
W = "http://schemas.openxmlformats.org/wordprocessingml/2006/main"
|
||||
FORBIDDEN = ("核心判断", "规则一", "规则二", "Campaign层级", "层级判断")
|
||||
|
||||
|
||||
def run(text, bold=False, color="1F2937", size=20):
|
||||
props = (f'<w:rFonts w:ascii="Arial" w:hAnsi="Arial" w:eastAsia="Microsoft YaHei"/>'
|
||||
f'<w:color w:val="{color}"/><w:sz w:val="{size}"/><w:szCs w:val="{size}"/>'
|
||||
+ ("<w:b/><w:bCs/>" if bold else ""))
|
||||
return f'<w:r><w:rPr>{props}</w:rPr><w:t xml:space="preserve">{escape(str(text))}</w:t></w:r>'
|
||||
|
||||
|
||||
def para(text="", bold=False, color="1F2937", size=20, align=None, before=0,
|
||||
after=90, keep_next=False, border=False):
|
||||
ppr = [f'<w:spacing w:before="{before}" w:after="{after}" w:line="276" w:lineRule="auto"/>',
|
||||
"<w:keepLines/>"]
|
||||
if align:
|
||||
ppr.append(f'<w:jc w:val="{align}"/>')
|
||||
if keep_next:
|
||||
ppr.append("<w:keepNext/>")
|
||||
if border:
|
||||
ppr.append('<w:pBdr><w:bottom w:val="single" w:sz="8" w:space="5" w:color="2E74B5"/></w:pBdr>')
|
||||
return f'<w:p><w:pPr>{"".join(ppr)}</w:pPr>{run(text, bold, color, size)}</w:p>'
|
||||
|
||||
|
||||
def rich(label, text):
|
||||
props = '<w:spacing w:before="20" w:after="110" w:line="276" w:lineRule="auto"/>'
|
||||
return f'<w:p><w:pPr>{props}</w:pPr>{run(label + ":", True, "0B2545", 20)}{run(text)}</w:p>'
|
||||
|
||||
|
||||
def heading(text, major=True):
|
||||
return para(text, True, "0B2545" if major else "2E74B5", 28 if major else 23,
|
||||
before=240 if major else 150, after=110, keep_next=True, border=major)
|
||||
|
||||
|
||||
def cell(value, width, header=False, align="left"):
|
||||
fill = '<w:shd w:val="clear" w:fill="E8EEF5"/>' if header else ""
|
||||
tcpr = (f'<w:tcW w:w="{width}" w:type="dxa"/>{fill}<w:vAlign w:val="center"/>'
|
||||
'<w:tcMar><w:top w:w="85" w:type="dxa"/><w:left w:w="100" w:type="dxa"/>'
|
||||
'<w:bottom w:w="85" w:type="dxa"/><w:right w:w="100" w:type="dxa"/></w:tcMar>')
|
||||
return f'<w:tc><w:tcPr>{tcpr}</w:tcPr>{para(value, header, "1F3A5F" if header else "1F2937", 17, align, after=0)}</w:tc>'
|
||||
|
||||
|
||||
def table(headers, rows, widths, aligns=None):
|
||||
if sum(widths) != 9360 or len(headers) != len(widths):
|
||||
raise ValueError("Every table width list must match headers and sum to 9360")
|
||||
aligns = aligns or ["left"] * len(widths)
|
||||
borders = "".join(f'<w:{edge} w:val="single" w:sz="4" w:color="CBD5E1"/>'
|
||||
for edge in ("top", "left", "bottom", "right", "insideH", "insideV"))
|
||||
props = f'<w:tblW w:w="9360" w:type="dxa"/><w:tblLayout w:type="fixed"/><w:tblBorders>{borders}</w:tblBorders>'
|
||||
grid = "".join(f'<w:gridCol w:w="{width}"/>' for width in widths)
|
||||
header = "".join(cell(value, width, True, aligns[i]) for i, (value, width) in enumerate(zip(headers, widths)))
|
||||
xml_rows = [f'<w:tr><w:trPr><w:tblHeader/></w:trPr>{header}</w:tr>']
|
||||
for row in rows:
|
||||
normalized = list(row) + [""] * (len(headers) - len(row))
|
||||
cells = "".join(cell(normalized[i], width, False, aligns[i]) for i, width in enumerate(widths))
|
||||
xml_rows.append(f'<w:tr><w:trPr><w:cantSplit/></w:trPr>{cells}</w:tr>')
|
||||
return f'<w:tbl><w:tblPr>{props}</w:tblPr><w:tblGrid>{grid}</w:tblGrid>{"".join(xml_rows)}</w:tbl>'
|
||||
|
||||
|
||||
def rows(items, fields):
|
||||
return [[str(item.get(field, "")) for field in fields] for item in items]
|
||||
|
||||
|
||||
def validate(data):
|
||||
required = ("analysis_date", "portfolio_kpis", "learning_pool", "stable_pool", "actions", "good_ads")
|
||||
missing = [key for key in required if key not in data]
|
||||
if missing:
|
||||
raise ValueError("Missing brief keys: " + ", ".join(missing))
|
||||
payload = json.dumps(data, ensure_ascii=False)
|
||||
found = [term for term in FORBIDDEN if term in payload]
|
||||
if found:
|
||||
raise ValueError("Forbidden daily-brief content: " + ", ".join(found))
|
||||
for item in data["stable_pool"]:
|
||||
tier = str(item.get("tier", ""))
|
||||
if tier and not any(tier.startswith(prefix) for prefix in ("S-优质", "A-稳定", "B-波动待优化", "C-劣质")):
|
||||
raise ValueError(f"Invalid stable tier: {tier}")
|
||||
|
||||
|
||||
def document_body(data):
|
||||
title = data.get("title") or f'{data["analysis_date"]} Campaign Triage'
|
||||
label = data.get("account_label", "广告投放日报")
|
||||
mature = data.get("retention_mature_date")
|
||||
date_line = f'投放数据日:{data["analysis_date"]}' + (f' | D1次留成熟日:{mature}' if mature else "")
|
||||
body = [para(label, True, "64748B", 18, after=55), para(title, True, "0B2545", 34, after=60),
|
||||
para(date_line, color="475569", size=19, after=160, border=True)]
|
||||
body += [heading("1. 是否达标"), table(
|
||||
["指标", "实际值", "目标", "结论", "口径"],
|
||||
rows(data["portfolio_kpis"], ["metric", "actual", "target", "verdict", "basis"]),
|
||||
[1450, 1300, 1300, 1200, 4110], ["left", "center", "center", "center", "left"])]
|
||||
if data.get("kpi_note"):
|
||||
body.append(rich("口径说明", data["kpi_note"]))
|
||||
body += [heading("2. 问题分析"), heading("学习期池排名", False)]
|
||||
learning = data["learning_pool"]
|
||||
if learning:
|
||||
body.append(table(["排名", "Ad", "阶段", "成熟表现", "决定"],
|
||||
rows(learning, ["rank", "ad", "stage", "evidence", "decision"]),
|
||||
[650, 2300, 950, 3400, 2060], ["center", "left", "center", "left", "left"]))
|
||||
else:
|
||||
body.append(para("无", color="475569"))
|
||||
body.append(heading("稳定期池排名", False))
|
||||
stable = data["stable_pool"]
|
||||
if stable:
|
||||
body.append(table(["排名", "分级", "Ad", "判断", "决定"],
|
||||
rows(stable, ["rank", "tier", "ad", "evidence", "decision"]),
|
||||
[650, 1550, 2050, 3000, 2110], ["center", "center", "left", "left", "left"]))
|
||||
else:
|
||||
body.append(para("无", color="475569"))
|
||||
body.append(heading("3. 建议动作"))
|
||||
actions = data["actions"]
|
||||
if actions:
|
||||
body.append(table(["动作", "对象", "证据", "预算影响"],
|
||||
rows(actions, ["action", "ad", "evidence", "budget_impact"]),
|
||||
[1100, 2500, 4260, 1500], ["center", "left", "left", "center"]))
|
||||
else:
|
||||
body.append(para("无", color="475569"))
|
||||
for note in data.get("action_notes", []):
|
||||
body.append(rich(note.get("label", "说明"), note.get("text", "")))
|
||||
body.append(heading("4. 近7天表现好的 Ads(3–5个)"))
|
||||
good = data["good_ads"]
|
||||
if good:
|
||||
body.append(table(["排名", "Ad", "近7日数据", "为什么好", "下一步"],
|
||||
rows(good, ["rank", "ad", "data", "why_good", "next_step"]),
|
||||
[650, 1900, 2900, 2400, 1510], ["center", "left", "left", "left", "left"]))
|
||||
else:
|
||||
body.append(para("无符合标准的成熟 Ad;不降低标准凑数。", color="475569"))
|
||||
if data.get("data_note"):
|
||||
body += [heading("数据与决策说明", False), para(data["data_note"], color="475569", size=18)]
|
||||
body.append(para("以上均为人工决策建议,未执行任何平台变更。", True, "0B2545", 19, before=60, after=0, border=True))
|
||||
return "".join(body)
|
||||
|
||||
|
||||
def write_docx(data, output: Path):
|
||||
validate(data)
|
||||
body = document_body(data)
|
||||
sect = '<w:sectPr><w:pgSz w:w="12240" w:h="15840"/><w:pgMar w:top="1080" w:right="1440" w:bottom="1080" w:left="1440" w:header="720" w:footer="720"/></w:sectPr>'
|
||||
document = f'<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:document xmlns:w="{W}"><w:body>{body}{sect}</w:body></w:document>'
|
||||
styles = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?><w:styles xmlns:w="{W}">
|
||||
<w:docDefaults><w:rPrDefault><w:rPr><w:rFonts w:ascii="Arial" w:hAnsi="Arial" w:eastAsia="Microsoft YaHei"/><w:sz w:val="20"/><w:szCs w:val="20"/><w:color w:val="1F2937"/></w:rPr></w:rPrDefault></w:docDefaults>
|
||||
<w:style w:type="paragraph" w:default="1" w:styleId="Normal"><w:name w:val="Normal"/><w:qFormat/></w:style></w:styles>'''
|
||||
content_types = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Types xmlns="http://schemas.openxmlformats.org/package/2006/content-types"><Default Extension="rels" ContentType="application/vnd.openxmlformats-package.relationships+xml"/><Default Extension="xml" ContentType="application/xml"/><Override PartName="/word/document.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.document.main+xml"/><Override PartName="/word/styles.xml" ContentType="application/vnd.openxmlformats-officedocument.wordprocessingml.styles+xml"/><Override PartName="/docProps/core.xml" ContentType="application/vnd.openxmlformats-package.core-properties+xml"/></Types>'''
|
||||
root_rels = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/officeDocument" Target="word/document.xml"/><Relationship Id="rId2" Type="http://schemas.openxmlformats.org/package/2006/relationships/metadata/core-properties" Target="docProps/core.xml"/></Relationships>'''
|
||||
doc_rels = '''<?xml version="1.0" encoding="UTF-8" standalone="yes"?><Relationships xmlns="http://schemas.openxmlformats.org/package/2006/relationships"><Relationship Id="rId1" Type="http://schemas.openxmlformats.org/officeDocument/2006/relationships/styles" Target="styles.xml"/></Relationships>'''
|
||||
stamp = dt.datetime.now(dt.timezone.utc).replace(microsecond=0).isoformat().replace("+00:00", "Z")
|
||||
core = f'''<?xml version="1.0" encoding="UTF-8" standalone="yes"?><cp:coreProperties xmlns:cp="http://schemas.openxmlformats.org/package/2006/metadata/core-properties" xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:dcterms="http://purl.org/dc/terms/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"><dc:title>{escape(data.get("title", "Daily Campaign Triage"))}</dc:title><dc:creator>daily-campaign-triage</dc:creator><dcterms:created xsi:type="dcterms:W3CDTF">{stamp}</dcterms:created><dcterms:modified xsi:type="dcterms:W3CDTF">{stamp}</dcterms:modified></cp:coreProperties>'''
|
||||
output.parent.mkdir(parents=True, exist_ok=True)
|
||||
with ZipFile(output, "w", ZIP_DEFLATED) as archive:
|
||||
for name, payload in (("[Content_Types].xml", content_types), ("_rels/.rels", root_rels),
|
||||
("word/document.xml", document), ("word/styles.xml", styles),
|
||||
("word/_rels/document.xml.rels", doc_rels), ("docProps/core.xml", core)):
|
||||
archive.writestr(name, payload)
|
||||
|
||||
|
||||
def main(argv=None):
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("input", type=Path, help="Brief JSON")
|
||||
parser.add_argument("--output", type=Path, required=True)
|
||||
args = parser.parse_args(argv)
|
||||
data = json.loads(args.input.read_text(encoding="utf-8"))
|
||||
write_docx(data, args.output)
|
||||
print(f"Wrote {args.output}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
main()
|
||||
except (ValueError, json.JSONDecodeError) as exc:
|
||||
print(f"error: {exc}", file=sys.stderr)
|
||||
raise SystemExit(2)
|
||||
@@ -0,0 +1,12 @@
|
||||
日期,平台 Campaign Name,平台 Ad Group Name,平台 Ad Name,消耗,归因安装量,D1 留存人数
|
||||
2026-08-19,Campaign Demo,Group One,Creative Alpha,30,10,1
|
||||
2026-08-20,Campaign Demo,Group One,Creative Alpha,32,10,1
|
||||
2026-08-21,Campaign Demo,Group One,Creative Alpha,34,10,2
|
||||
2026-08-22,Campaign Demo,Group One,Creative Alpha,36,10,1
|
||||
2026-08-23,Campaign Demo,Group One,Creative Alpha,38,10,1
|
||||
2026-08-24,Campaign Demo,Group One,Creative Alpha,40,10,2
|
||||
2026-08-25,Campaign Demo,Group One,Creative Alpha,42,10,0
|
||||
2026-08-22,Campaign Demo,Group Two,Creative Beta,15,3,0
|
||||
2026-08-23,Campaign Demo,Group Two,Creative Beta,20,4,0
|
||||
2026-08-24,Campaign Demo,Group Two,Creative Beta,25,5,1
|
||||
2026-08-25,Campaign Demo,Group Two,Creative Beta,30,6,0
|
||||
|
@@ -0,0 +1,49 @@
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import unittest
|
||||
import zipfile
|
||||
from pathlib import Path
|
||||
from xml.etree import ElementTree as ET
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
class ScriptTests(unittest.TestCase):
|
||||
def test_csv_analysis_separates_maturity_dates(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
output = Path(tmp) / "analysis.json"
|
||||
subprocess.run([
|
||||
sys.executable, str(ROOT / "scripts/analyze_report.py"),
|
||||
str(ROOT / "tests/fixtures/sample-report.csv"),
|
||||
"--analysis-date", "2026-08-25", "--retention-lag-days", "1",
|
||||
"--output", str(output),
|
||||
], check=True, capture_output=True, text=True)
|
||||
data = json.loads(output.read_text(encoding="utf-8"))
|
||||
self.assertEqual(data["dates"]["analysis_date"], "2026-08-25")
|
||||
self.assertEqual(data["dates"]["retention_mature_date"], "2026-08-24")
|
||||
self.assertEqual(len(data["ads"]), 2)
|
||||
self.assertNotIn("tier", data["ads"][0])
|
||||
self.assertAlmostEqual(data["portfolio"]["analysis_day"]["spend"], 72)
|
||||
|
||||
def test_docx_contract_and_forbidden_headings(self):
|
||||
with tempfile.TemporaryDirectory() as tmp:
|
||||
output = Path(tmp) / "brief.docx"
|
||||
subprocess.run([
|
||||
sys.executable, str(ROOT / "scripts/build_brief.py"),
|
||||
str(ROOT / "examples/brief.example.json"), "--output", str(output),
|
||||
], check=True, capture_output=True, text=True)
|
||||
self.assertTrue(zipfile.is_zipfile(output))
|
||||
with zipfile.ZipFile(output) as archive:
|
||||
root = ET.fromstring(archive.read("word/document.xml"))
|
||||
text = "".join(root.itertext())
|
||||
for heading in ("1. 是否达标", "2. 问题分析", "3. 建议动作", "4. 近7天表现好的 Ads"):
|
||||
self.assertIn(heading, text)
|
||||
for forbidden in ("核心判断", "规则一", "规则二", "Campaign层级", "层级判断"):
|
||||
self.assertNotIn(forbidden, text)
|
||||
self.assertIn("决定", text)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
Reference in New Issue
Block a user