"""动态策略:以 yaml 静态种子为基础,叠加动态种子(统一池 + 加权随机 + 用完全用)。 种子词机制(v49 起): 1. 全部类型放一起(统一池):静态 style + 静态 related + 月份主题 + 节日 + LLM 动态 ——合并去重(跨类型同词只保留一个,权重累加 = 多来源更受重视); 2. 节日种子词提供权重:节日权重 3.0 > 月份主题 2.0 > 静态/动态 1.0,随机抽取时加权; 3. 每次随机取:从池中按权重随机抽取(不重复),limit 内数量; 4. 用完全用:池中种子数 ≤ 需要数时全部使用(不再随机限量/截断); 5. 每个国家独立配置:configs/countries/.yaml 的 style.seeds / related.seed_keywords。 limit 由 seed_node 从 context 注入(max_style_seeds / max_related_seeds,0 或缺失=不限)。 LLM 后端生成失败时自动回退到静态+节日主题,保证不中断。 """ import random from typing import Any, Dict, List from .base import SeedStrategy def _weighted_sample(pool: List[Dict[str, Any]], k: int) -> List[Dict[str, Any]]: """按权重随机不重复取 k 个;池数量 ≤ k(或用完)时全部返回(不随机限量)。""" if k <= 0 or len(pool) <= k: return list(pool) out: List[Dict[str, Any]] = [] rest = list(pool) for _ in range(k): weights = [max(float(it["weight"]), 0.0) for it in rest] if sum(weights) <= 0: out.extend(rest) break idx = random.choices(range(len(rest)), weights=weights)[0] out.append(rest.pop(idx)) return out class DynamicStrategy(SeedStrategy): name = "dynamic" def resolve( self, country: str, cc: Dict[str, Any], context: Dict[str, Any], llm_backend: Any = None, ) -> Dict[str, Any]: base_style = list((cc.get("style", {}) or {}).get("seeds", []) or []) base_related = list((cc.get("related", {}) or {}).get("seed_keywords", []) or []) month_style = list(context.get("month_themes", []) or []) holidays = list(context.get("upcoming_holidays", []) or []) holiday_style = [f"{h.lower()} aesthetic" for h in holidays] # 节日主题同时扩充 related 源(提高节日权重 + 增加 related 扩展) holiday_related = [f"{h.lower()} tee" if not h.lower().endswith("day") else f"{h.lower()} gift" for h in holidays] # LLM 动态种子(失败回退,不影响静态/节日) dyn_style: List[str] = [] dyn_related: List[str] = [] if llm_backend is not None and hasattr(llm_backend, "generate_seeds"): try: res = llm_backend.generate_seeds(context) or {} dyn_style = list(res.get("style_seeds", []) or []) dyn_related = list(res.get("related_seeds", []) or []) except Exception as e: # noqa: BLE001 print(f"[seed] LLM 生成种子失败,仅用静态+节日主题: {e}") # 1) 统一池:全部类型合并,跨类型去重(同词权重累加 = 多来源更受重视) pool: Dict[str, Dict[str, Any]] = {} def add(items: List[str], weight: float, src: str) -> None: for it in items: it = (it or "").strip() if not it: continue key = it.lower() if key in pool: pool[key]["weight"] += weight pool[key]["sources"].append(src) else: pool[key] = {"word": it, "weight": weight, "sources": [src]} add(base_style, 1.0, "static") add(base_related, 1.0, "static") add(month_style, 2.0, "month") add(holiday_style, 3.0, "holiday") add(holiday_related, 3.0, "holiday") add(dyn_style, 1.0, "dynamic") add(dyn_related, 1.0, "dynamic") items = list(pool.values()) limit_style = int(context.get("max_style_seeds") or 0) limit_related = int(context.get("max_related_seeds") or 0) # 2) 每次随机取(加权,不重复);池不足 → 全部用 style_pick = _weighted_sample(items, limit_style) style_keys = {id(it) for it in style_pick} remaining = [it for it in items if id(it) not in style_keys] related_pick = _weighted_sample(remaining, limit_related) return { "style_seeds": [it["word"] for it in style_pick], "related_seeds": [it["word"] for it in related_pick], "dynamic": True, "pool_size": len(items), "pool": [it["word"] for it in items], "llm_style_seeds": dyn_style, "llm_related_seeds": dyn_related, "holiday_style_seeds": holiday_style, "holiday_related_seeds": holiday_related, "static_style_seeds": base_style, "static_related_seeds": base_related, }