POD 趋势感知 Agent:缓存热点模式 + 三图合成 + 热点去重/风格去重 + review 兜底
- 缓存热点批量流程(有采集缓存不触发 Google) - 简报不足直接从采集缓存生成(轻量补齐) - 三图合成(模特/印花/底图)+ 底图压缩 <2MB - 热点去重→风格去重自动切换 + 不适合类目 review 兜底 - 透明背景(background=transparent)+ 提示词清洗(敏感词/背景描述) - 任务前 basemap 校验 + 模板国家校验 + 模特任务级分配
This commit is contained in:
@@ -0,0 +1,127 @@
|
||||
"""节点 4/6:合规筛选(screen)。
|
||||
|
||||
调用可插拔 LLM 后端做合规筛查 + 结构化四要素;后端调用失败时降级 MockBackend。
|
||||
按 topic 把筛查结果映射回 scored 候选(补 score/sources/country),再做最终风险过滤
|
||||
(blocked 丢弃;review 按 keep_review 决定)。
|
||||
"""
|
||||
from pathlib import Path
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from graph.loader import load_system_prompt
|
||||
from graph.llms import DEFAULT_SYSTEM_PROMPT, get_backend
|
||||
from graph.style_rules import COUNTRY_AESTHETICS
|
||||
from graph.validate import with_fallback
|
||||
|
||||
|
||||
@with_fallback("screen")
|
||||
def screen_node(state: Dict[str, Any]) -> Dict[str, Any]:
|
||||
country = state["country"]
|
||||
scored: List[Dict[str, Any]] = state.get("scored_rows") or []
|
||||
config = state["config"]
|
||||
cc = state["country_config"]
|
||||
llm_cfg = config.get("llm_screen") or {}
|
||||
provider = llm_cfg.get("provider", "mock")
|
||||
keep_review = bool(llm_cfg.get("keep_review", False))
|
||||
batch_size = int(llm_cfg.get("max_topics_per_call", 12))
|
||||
blacklist = [str(b).lower() for b in (config.get("blacklist") or [])]
|
||||
|
||||
prompts_dir = Path(state["prompts_dir"])
|
||||
system_prompt = load_system_prompt(prompts_dir, DEFAULT_SYSTEM_PROMPT)
|
||||
aesthetic_hint = cc.get("style_hint") or COUNTRY_AESTHETICS.get(country, {}).get("style_hint", "")
|
||||
|
||||
# 排除已用热点(去重生效:已用 topic 不再进入本次简报,每次跑都用新热点)
|
||||
try:
|
||||
import json as _json
|
||||
used_p = Path(state.get("cache_dir") or state.get("output_dir", "")) / "used_designs.json"
|
||||
if used_p.exists():
|
||||
ud = _json.loads(used_p.read_text(encoding="utf-8")).get("used", []) or []
|
||||
used_topics = {str(u.get("topic", "")).strip().lower() for u in ud}
|
||||
before = len(scored)
|
||||
scored = [c for c in scored if str(c.get("topic", "")).strip().lower() not in used_topics]
|
||||
if len(scored) < before:
|
||||
print(f"[screen] 排除已用热点 {before - len(scored)} 条(去重),剩余 {len(scored)} 条可选")
|
||||
except Exception: # noqa: BLE001
|
||||
pass
|
||||
|
||||
# 简报数量 = 用多少生成多少:llm_screen.max_briefs 配置优先,否则按扩展后的总任务数
|
||||
#(每个产品一个热点;数量 N=每个款-颜色条目的设计数 → 总任务=条目数×N);
|
||||
# 前台显示不依赖简报(读 collected 完整池 + used_designs 剔除已用,用完即从前台消失)。
|
||||
pcfg = config.get("product") or {}
|
||||
ls_cfg = config.get("llm_screen") or {}
|
||||
limit = int(ls_cfg.get("max_briefs") or 0)
|
||||
if not limit and pcfg.get("spu_tasks"):
|
||||
limit = len(pcfg.get("spu_tasks") or []) # 扩展后总任务数(=产品数)
|
||||
if not limit:
|
||||
limit = int(pcfg.get("spu_count") or 0)
|
||||
if limit > 0:
|
||||
ordered = sorted(scored, key=lambda c: -(float(c.get("score") or 0)))
|
||||
scored_limited = ordered[:limit]
|
||||
print(f"[screen] 简报限量 {limit} → 筛前 {len(scored_limited)} 个高分热点(共 {len(scored)} 个)")
|
||||
else:
|
||||
scored_limited = scored
|
||||
print(f"[screen] 简报全量 {len(scored_limited)} 条(未限量,全部生成简报)")
|
||||
|
||||
topics = [c["topic"] for c in scored_limited]
|
||||
|
||||
backend = get_backend(provider)
|
||||
if provider != "mock":
|
||||
backend.bind_config(llm_cfg)
|
||||
if not backend.has_key:
|
||||
print("[screen] 未检测到 LLM api_key(请配置 llm_screen.api_key 或环境变量 "
|
||||
"LLM_API_KEY/OPENAI_API_KEY),降级 Mock 兜底。")
|
||||
backend = get_backend("mock")
|
||||
try:
|
||||
screened = backend.screen(topics, country, aesthetic_hint, system_prompt, blacklist, batch_size)
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(f"[screen] {provider} 调用失败,降级 Mock: {e}")
|
||||
backend = get_backend("mock")
|
||||
screened = backend.screen(topics, country, aesthetic_hint, system_prompt, blacklist, batch_size)
|
||||
|
||||
# 映射回 scored 候选
|
||||
by_topic = {s.get("topic", "").lower(): s for s in screened}
|
||||
out: List[Dict[str, Any]] = []
|
||||
missing: List[Dict[str, Any]] = []
|
||||
for it in scored_limited:
|
||||
s = by_topic.get(it["topic"].lower())
|
||||
if s is None:
|
||||
missing.append(it)
|
||||
continue
|
||||
s = dict(s)
|
||||
s["country"] = country
|
||||
s["score"] = it.get("score", 0)
|
||||
s["sources"] = it.get("sources", "")
|
||||
out.append(s)
|
||||
|
||||
# LLM 漏判的主题用 Mock 单独补全,避免丢数据(而非整批降级)
|
||||
if missing:
|
||||
print(f"[screen] LLM 漏判 {len(missing)} 个主题,用 Mock 单独补全:"
|
||||
f"{[m['topic'] for m in missing]}")
|
||||
mock = get_backend("mock")
|
||||
m_res = mock.screen(
|
||||
[m["topic"] for m in missing], country, aesthetic_hint,
|
||||
system_prompt, blacklist, batch_size,
|
||||
)
|
||||
m_by = {r.get("topic", "").lower(): r for r in m_res}
|
||||
for it in missing:
|
||||
s = m_by.get(it["topic"].lower())
|
||||
if s is None:
|
||||
continue
|
||||
s = dict(s)
|
||||
s["country"] = country
|
||||
s["score"] = it.get("score", 0)
|
||||
s["sources"] = it.get("sources", "")
|
||||
out.append(s)
|
||||
|
||||
# 最终风险过滤:只滤 blocked(硬拦截);review(待复核)保留——
|
||||
# 由 assign_hotspots 的 allow_review 决定是否参与分配(openai 模式 review+concept 可用),
|
||||
# 避免 review 被静默丢弃导致"任务 N 个但简报不足、设计缺失"
|
||||
kept: List[Dict[str, Any]] = []
|
||||
for r in out:
|
||||
lvl = r.get("risk_level", "safe")
|
||||
if lvl == "blocked":
|
||||
continue
|
||||
kept.append(r)
|
||||
|
||||
stats = dict(state.get("stats") or {})
|
||||
stats["screen"] = {"screened": len(out), "kept": len(kept)}
|
||||
return {"screened": kept, "stats": stats}
|
||||
Reference in New Issue
Block a user