Files
beige/backend/app/services/ai_engine/preference_aggregator.py
yangqianqian 4bed7425a8 A8多套打包+M4归档+R5多图:存量功能备份
A8 多套交付包(packaging_task.py):
- 修复交付包只打第1条混乱note的bug,按ImageCandidate.strategy分A/B/C组
- 每组生独立note_0N夹(6图+文案.txt),同seq留最新去重,老数据兼容
- task74端到端验:3套各6图,独立agent7项交叉验证全过

M4 归档(tasks.py/exports.py/前端):
- list_tasks加date_from/date_to/product_id筛选+product_name批量填(防N+1)
- 新增exports.py:产品JSON导出+标杆CSV导出(UTF-8 BOM)
- 前端HistoryFilters日期/产品筛选+产品列+打回原因红banner
- response.py加raise_param_error;独立agent验A1/A2/A9通过

R5 产品多图(product_images.py/020迁移/前端):
- product_images表+5端点(上传/列/改场景/设主图/删图)
- 生图按ROLE_SCENE_PREFERENCE选对应场景图,回落primary
- 前端ProductImageManager多图画廊

R6 账号config拆页(settings/):
- 配置页按角色拆/settings(运营+组长+admin)+/config(仅admin)
- Key只显末4位不显余额(守红线)

核销表对齐真实代码状态:D1改稿框/M7裂变/E12评图分纠偏为已完成(曾漏回写)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 17:32:49 +08:00

148 lines
5.3 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
偏好飞轮聚合preference_aggregator
扒自Clover架构方案.md §偏好飞轮怎么转 + PRD §8
三层继承L1 公司品牌基线 > L2 矩阵号人设(二期)> L3 个人手感
聚合最近 FLYWHEEL_LOOKBACK 条 events → prompt 片段注入文案生成
关键:
- 按 product_id 分开学(素颜霜偏好不串精华)
- 信号不足 FLYWHEEL_COLD_START 条时,用产品档案冷启动
- 返回结构对齐 API契约 GET /tasks/{id}/preference/context
"""
from __future__ import annotations
import logging
from collections import Counter
from typing import Any
from .constants import FLYWHEEL_LOOKBACK, FLYWHEEL_COLD_START
logger = logging.getLogger(__name__)
def aggregate_preference_context(
events: list[dict],
product: dict,
workspace_id: int,
product_id: int,
) -> dict:
"""
输入:最近 preference_events 行(已按 workspace_id+product_id 过滤)
输出:{recent_preference, reject_reasons, injected_count, prompt_fragment}
prompt_fragment 直接注入文案生成 prompt
"""
# 按 product_id 过滤(防串货)
relevant = [
e for e in events
if e.get("workspace_id") == workspace_id and e.get("product_id") == product_id
][:FLYWHEEL_LOOKBACK]
injected_count = len(relevant)
if injected_count < FLYWHEEL_COLD_START:
# 冷启动:用产品档案静态基线
return _cold_start(product, injected_count)
# ── 统计最常选角度text_select + approve 信号)
angle_counts: Counter = Counter()
reject_reasons: list[str] = []
for e in relevant:
sig_type = e.get("signal_type", "")
angle = str(e.get("angle_label") or "").strip()
weight = int(e.get("signal_weight", 1))
# text_edit(改稿)是最强真实信号,角度按权重计入(倩倩姐2026-06-16拍板)
# image_select(选图)按套别叙事角度计入,让选图偏好真正闭环回生图(R7断点2)
if sig_type in ("text_select", "approve", "text_edit", "image_select") and angle:
angle_counts[angle] += weight
elif sig_type == "reject_with_reason":
reason = str(e.get("reason") or "").strip()
if reason:
reject_reasons.append(reason)
# 取权重最高的角度
top_angles = [a for a, _ in angle_counts.most_common(3)]
# 取最近3条打回原因
recent_rejects = reject_reasons[-3:] if reject_reasons else []
# ── 拼 prompt 片段三层继承L1>L2>L3一期只跑L1+L3
prompt_fragment = _build_prompt_fragment(top_angles, recent_rejects, product)
# ── 人类可读摘要(前端"本次已注入"显示)
if top_angles:
pref_summary = f"最近偏好角度:{''.join(top_angles)}(已选{injected_count}次信号)"
else:
pref_summary = f"已注入{injected_count}条偏好信号"
return {
"recent_preference": pref_summary,
"reject_reasons": recent_rejects,
"injected_count": injected_count,
"prompt_fragment": prompt_fragment, # 注入 generate_text_variants extra_rules
}
def _cold_start(product: dict, injected_count: int) -> dict:
"""信号不足时用产品档案基线"""
angles = product.get("text_angles") or []
style = product.get("style_tone", "素人分享风")
fragment = ""
if angles:
fragment = f"优先覆盖以下文案角度:{''.join(angles[:3])}。风格调性:{style}"
return {
"recent_preference": f"冷启动(历史信号{injected_count}条,不足{FLYWHEEL_COLD_START}条),使用产品档案基线",
"reject_reasons": [],
"injected_count": injected_count,
"prompt_fragment": fragment,
}
def _build_prompt_fragment(
top_angles: list[str],
reject_reasons: list[str],
product: dict,
) -> str:
"""
组装注入文案 prompt 的片段
越积累越精准1次=全靠基线10次=知道偏好角度30次=措辞从"供参考"升为明确指令
"""
lines: list[str] = []
if top_angles:
lines.append(f"【偏好角度参考】历史选择偏好:{''.join(top_angles)},请优先采用这些角度方向。")
if reject_reasons:
formatted = "".join(f"{r}" for r in reject_reasons)
lines.append(f"【打回原因参考】以下问题请主动规避:{formatted}")
# L1 品牌基线(产品档案 custom_prompt
custom = (product.get("custom_prompt") or "").strip()
if custom:
lines.append(f"【品牌基线】{custom}")
return "\n".join(lines)
def collect_preference_event(
signal_type: str,
user_id: int,
workspace_id: int,
product_id: int,
angle_label: str = "",
reason: str = "",
weights: dict[str, int] | None = None,
) -> dict:
"""
构造 preference_event 行(由业务接口内部调用,不暴露给前端)
返回待插 DB 的字段 dict
"""
from .constants import FLYWHEEL_WEIGHTS
w_map = weights or FLYWHEEL_WEIGHTS
weight = w_map.get(signal_type, 0)
return {
"signal_type": signal_type,
"signal_weight": weight,
"user_id": user_id,
"workspace_id": workspace_id,
"product_id": product_id,
"angle_label": angle_label,
"reason": reason,
"data_ownership": "client_data", # 原始行为信号归客户PRD §3 data_ownership
}