Просмотр исходного кода

fix(auto_put_ad_mini): 报表缺失列+飞书文字工具+部分批准协议

Task 1 (报表 P0 bug):
- report_generator: 列名 f_7日动态ROI → 动态ROI_7日均值 修复静默丢弃
- 新增"动态ROI(单日)"作为决策参考补充
- 移除不存在的 yesterday_revenue 幽灵列
- ACTION_CN_MAP 补齐 scale_up→扩量 / observe→观察
- 表格从 22 列恢复到 24 列,24 条 scale_up 中文化

Task 2 (新工具 send_feishu_text_message):
- im_approval.py 新增 @tool 用于执行后汇报 (diff/确认/质疑回应)
- 填补"LLM 有决策能力但无文字反馈通道"的工具链缺口
- 支持双开关 to_operator/to_project_chat 灵活控制发送范围
- 注册到 config.py tools 白名单 + execute_once.py/run.py 导入

Task 3 (部分批准分支 prompt):
- system.prompt 第八部分新增"部分批准型"作为第 5 种反馈类型
- 强制执行协议五步: 识别子集 → diff 表 → 只执行子集 → 必发文字汇报 → 禁止重发全量
- 工具链映射补充 send_feishu_text_message 引用
- 关键禁令新增: 严禁只发表格不发文字 / 严禁重发全量报告

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
刘立冬 3 недель назад
Родитель
Сommit
bb9a7f43a2

+ 1 - 0
examples/auto_put_ad_mini/config.py

@@ -42,6 +42,7 @@ MAIN_CONFIG = RunConfig(
         "check_execution_feedback",
         "send_approval_request",
         "check_approval_status",
+        "send_feishu_text_message",  # 执行后向运营汇报 diff / 确认 / 质疑回应
         # 飞书文档(报告导入 & 分享):
         "import_to_feishu",
     ],

+ 1 - 1
examples/auto_put_ad_mini/execute_once.py

@@ -33,7 +33,7 @@ from examples.auto_put_ad_mini.tools.ad_decision import get_ads_for_review, appl
 from examples.auto_put_ad_mini.tools.report_generator import generate_report
 from examples.auto_put_ad_mini.tools.guardrails import validate_decisions
 from examples.auto_put_ad_mini.tools.execution_engine import execute_decisions, check_execution_feedback
-from examples.auto_put_ad_mini.tools.im_approval import send_approval_request, check_approval_status
+from examples.auto_put_ad_mini.tools.im_approval import send_approval_request, check_approval_status, send_feishu_text_message
 
 # 尝试导入飞书文档工具(如果存在)
 try:

+ 21 - 4
examples/auto_put_ad_mini/prompts/system.prompt

@@ -508,6 +508,7 @@ Step 10: generate_report          ← 生成报告
 | **方向型** | "整体太激进/太保守" / "关停太多" | 运营对整个批次的风险偏好想调整 |
 | **质疑型** | "为什么 pause 这条?" / "这个降幅依据是什么?" | 运营不接受当前 reason,要更多依据 |
 | **策略型** | "降幅改小一点" / "所有提价都再激进些" | 运营要调整参数边界 |
+| **部分批准型** | "只批准降价的" / "其他我审批 我只要 XXX" / "只执行 pause" | 运营明确圈定子集立即执行,其余**不是拒绝**,而是"运营要自己逐条审"或"下一轮再谈" |
 | **混合型** | "12345 不要动,其余降幅改小" | 同时包含两类以上——拆分处理 |
 
 **Step 2:把增量作为新约束,重新走决策链(不是在旧决策上打补丁)**
@@ -516,7 +517,19 @@ Step 10: generate_report          ← 生成报告
 - **方向型** → 把全局阈值(`roi_mean` / `tier_roi_p50`)临时上调或下调 10~20%,**重算候选集**,可能有些原本不在列表里的广告要加进来,有些原本 pause 的要降级为 bid_down。
 - **质疑型** → 调用 `query_ad_detail(ad_id)` 取详情,组织**三段式回答**:① 同类对比(该广告 vs 同人群包中位数/分位数);② 历史调价(7 日内是否调过价、效果如何);③ ROI 置信度(`roi_valid_days`、稳定天数、数据新鲜度)。不要敷衍。
 - **策略型** → 调 `BID_DOWN_MAX_PCT` / `BID_UP_MAX_PCT` 等参数边界,用新边界**重新生成** `recommended_change_pct`,而不是只裁剪已有百分比。
-- **混合型** → 拆成独立子问题,分别按上述四类处理,然后合并生成新决策。
+- **部分批准型** → 执行协议(**强制顺序,每步都要做**):
+  1. 识别运营圈定的子集 `S`,显式说出 — 例:"运营回复『其他我审批 我只要降价的』 = 圈定 S = action='bid_down' (14 条)"
+  2. 构造 diff 表(**在调用任何执行工具之前**):
+     | 类别 | 数量 | 本轮处理 |
+     | bid_down | 14 | ✅ 本轮执行(运营已批准)|
+     | pause    | 600 | ⏳ 保留在飞书表格,等运营后续逐条审批 |
+     | scale_up | 24 | ⏳ 保留在飞书表格,等运营后续逐条审批 |
+     | hold     | 307 | ➖ 本就不变更,不需审批 |
+  3. **只对子集 `S` 调用** `execute_decisions`,不要对全量调用(即便 `EXECUTION_ENABLED=False` 会兜住,也不能形成错误习惯)
+     - 若 `execute_decisions` 不支持按 action 过滤参数,先用 `modify_decisions` 把非 S 的决策临时标记为 observe/hold,再执行
+  4. 执行后**必须**调用 `send_feishu_text_message(text=...)` 向运营汇报:包含"已执行的 N 条 + 保留待审的 M 条 + 飞书表格链接"。**禁止**只发 `import_to_feishu` 而不发文字汇报
+  5. ❌ **严禁**在"部分批准"场景再次发送未过滤的全量报告(这等于把运营已经审过的东西又塞回去,零信息增量)
+- **混合型** → 拆成独立子问题,分别按上述五类处理,然后合并生成新决策。
 
 **Step 3:重新审批前,显式呈现协商过程**
 
@@ -547,12 +560,14 @@ Step 10: generate_report          ← 生成报告
 
 主动呈现"我需要什么数据",让运营可以选择"提供数据继续"或"就这样结束本轮"。
 
-### 工具链映射(复用既有工具,不新增)
+### 工具链映射
 
-- `modify_decisions(modifications=[...])`:应用事实型/策略型的具体改动
+- `modify_decisions(modifications=[...])`:应用事实型/策略型/部分批准型的具体改动
 - `validate_decisions()`:新决策走一遍护栏,再次检查冷启动/频率/边界
-- `send_approval_request(wait_for_reply=True)`:重新发审批,阻塞等运营下一轮回复
+- `send_approval_request(wait_for_reply=True)`:重新发审批,阻塞等运营下一轮回复(**仅用于真需要重新审批的场景**,不用于"汇报已执行")
 - `query_ad_detail(ad_id)`:质疑型反馈时回取单条详情
+- `send_feishu_text_message(text=..., to_operator=True, to_project_chat=True)`:**执行后汇报工具** — 发送 diff 表 / 质疑回应 / "建议本轮暂停"提议等纯文本消息。**部分批准型场景必须调用此工具**
+- `execute_decisions(filter_actions=[...])`:如果支持 `filter_actions` 参数则传入子集;若不支持先用 `modify_decisions` 过滤
 
 ### 关键禁令
 
@@ -560,6 +575,8 @@ Step 10: generate_report          ← 生成报告
 - ❌ 不要在没看 `query_ad_detail` 详情时就回答质疑型问题
 - ❌ 不要假设"30 分钟无回复 = 默认通过"——当前系统明确设计为"30 分钟无回复 = 默认拒绝",超时等于所有决策作废
 - ❌ 不要未经运营同意就自行调大 `BID_DOWN_MAX_PCT` 等阈值;策略型反馈的参数改动也要在下一轮审批中**显式告知**
+- ❌ **部分批准场景严禁只发表格不发文字**:`import_to_feishu` 只发链接卡片,不等于汇报;必须紧跟一条 `send_feishu_text_message` 携带 diff 表和执行摘要
+- ❌ **部分批准场景严禁重发全量报告**:运营已圈定子集,再发未变更的全量表格等于浪费运营注意力并制造歧义
 
 # 第九部分:边界约束(安全红线)
 

+ 1 - 1
examples/auto_put_ad_mini/run.py

@@ -42,7 +42,7 @@ from examples.auto_put_ad_mini.tools.ad_decision import (
 from examples.auto_put_ad_mini.tools.report_generator import generate_report
 from examples.auto_put_ad_mini.tools.guardrails import validate_decisions
 from examples.auto_put_ad_mini.tools.execution_engine import execute_decisions, check_execution_feedback
-from examples.auto_put_ad_mini.tools.im_approval import send_approval_request, check_approval_status
+from examples.auto_put_ad_mini.tools.im_approval import send_approval_request, check_approval_status, send_feishu_text_message
 
 
 async def init_project_env(messages=None):

+ 84 - 0
examples/auto_put_ad_mini/tools/im_approval.py

@@ -876,3 +876,87 @@ async def check_approval_status(
     except Exception as e:
         logger.error("check_approval_status 失败: %s", e, exc_info=True)
         return ToolResult(title="check_approval_status 失败", output=str(e))
+
+
+# ═══════════════════════════════════════════
+# 飞书文字消息(用于执行后向运营汇报)
+# ═══════════════════════════════════════════
+
+@tool(description="向运营飞书发送纯文本消息(用于执行后汇报 diff、确认、质疑回应等非审批场景)")
+async def send_feishu_text_message(
+    ctx: ToolContext,
+    text: str,
+    to_operator: bool = True,
+    to_project_chat: bool = True,
+) -> ToolResult:
+    """
+    向运营个人 IM 和/或投放项目群发送一条纯文本消息。
+
+    使用场景(必须显式调用):
+      - 收到"部分批准"反馈后,发送执行 diff 表(本轮执行 X 条 / 保留 Y 条 / 不变 Z 条)
+      - 质疑型反馈的详细回应(对比、历史、ROI 置信度)
+      - 连续 2 轮未达成一致时主动提议"本轮暂停"
+      - 任何"告知但不需要审批"的汇报
+
+    ⚠️ 不要用于:首次审批请求(用 send_approval_request);表格链接发送(用 import_to_feishu)。
+
+    Args:
+        text: 消息正文(支持换行,建议 < 2 KB 单屏可读)。
+        to_operator: 发送到运营个人 IM(FEISHU_OPERATOR_OPEN_ID)。默认 True。
+        to_project_chat: 抄送到投放项目群(FEISHU_AD_PROJECT_CHAT_ID)。默认 True;未配置时自动跳过。
+
+    Returns:
+        ToolResult,包含发送目标清单和状态。
+    """
+    if not IM_ENABLED:
+        return ToolResult(title="IM 未启用", output="IM_ENABLED=False,消息未发送")
+
+    if not text or not text.strip():
+        return ToolResult(title="消息为空", output="text 参数为空,拒绝发送")
+
+    sent_targets: List[str] = []
+    failed_targets: List[str] = []
+
+    if to_operator and FEISHU_OPERATOR_OPEN_ID:
+        try:
+            _feishu.send_message(to=FEISHU_OPERATOR_OPEN_ID, text=text)
+            sent_targets.append(f"个人 IM ({FEISHU_OPERATOR_OPEN_ID[:12]}...)")
+            logger.info("飞书文字消息发送成功(个人): len=%d", len(text))
+        except Exception as e:
+            failed_targets.append(f"个人 IM: {e}")
+            logger.error("飞书文字消息发送失败(个人): %s", e)
+
+    if to_project_chat and FEISHU_AD_PROJECT_CHAT_ID:
+        try:
+            _feishu.send_message(to=FEISHU_AD_PROJECT_CHAT_ID, text=text)
+            sent_targets.append(f"项目群 ({FEISHU_AD_PROJECT_CHAT_ID[:12]}...)")
+            logger.info("飞书文字消息发送成功(群聊): len=%d", len(text))
+        except Exception as e:
+            failed_targets.append(f"项目群: {e}")
+            logger.error("飞书文字消息发送失败(群聊): %s", e)
+
+    if not sent_targets and not failed_targets:
+        return ToolResult(
+            title="无可用发送目标",
+            output="to_operator/to_project_chat 均未启用或对应 ID 未配置",
+        )
+
+    summary_lines = []
+    if sent_targets:
+        summary_lines.append(f"✅ 已发送 {len(sent_targets)} 个目标:")
+        for t in sent_targets:
+            summary_lines.append(f"  - {t}")
+    if failed_targets:
+        summary_lines.append(f"❌ 失败 {len(failed_targets)} 个目标:")
+        for t in failed_targets:
+            summary_lines.append(f"  - {t}")
+
+    return ToolResult(
+        title=f"飞书文字消息已发送({len(sent_targets)} 个目标)",
+        output="\n".join(summary_lines),
+        metadata={
+            "sent_count": len(sent_targets),
+            "failed_count": len(failed_targets),
+            "text_length": len(text),
+        },
+    )

+ 7 - 6
examples/auto_put_ad_mini/tools/report_generator.py

@@ -29,19 +29,17 @@ OUTPUT_COLUMNS = [
     # 基础信息
     "ad_name", "audience_tier", "create_time", "ad_age_days", "bid_amount",
     # 昨日表现
-    "yesterday_cost", "yesterday_revenue", "yesterday_roi",
+    "yesterday_cost", "yesterday_roi",
     # 7日汇总
     "cost_7d_total", "revenue_7d_total",
-    # f_7日动态ROI(仅结果值,不显示组成
-    "f_7日动态ROI",
+    # 动态ROI(决策参考核心指标
+    "动态ROI", "动态ROI_7日均值",
     # 30日上下文
     "cost_30d_total", "cost_30d_avg",
     "stable_spend_days_30d", "creative_count",
     # 决策
     "action", "dimension", "reason",
     "recommended_change_pct", "current_bid", "recommended_bid",
-    # 参考
-    "f_7日动态ROI_mean_all",
 ]
 
 # 中文列名映射
@@ -68,7 +66,8 @@ CN_COLUMNS = {
     "T0裂变系数_7d_mean": "T0裂变系数(7日均值)",
     "e_factor": "e因子",
     "f_7日动态ROI": "7日均值动态ROI",
-    "动态ROI_7日均值": "7日均值动态ROI",
+    "动态ROI": "动态ROI(单日)",
+    "动态ROI_7日均值": "动态ROI(7日均值)",
     "cost_30d_total": "30日总消耗(元)",
     "cost_30d_avg": "30日日均消耗(元)",
     "stable_spend_days_30d": "稳定消耗天数(30日)",
@@ -91,6 +90,8 @@ ACTION_CN_MAP = {
     "bid_down": "降价",
     "bid_up": "提价",
     "hold": "保持",
+    "scale_up": "扩量",
+    "observe": "观察",
 }