"""调制因子派生:从时态快照 + 对话信号派生 [0,1] 因子与关怀类型。 消费 ``TemporalSnapshot``(Plan 1)与 ``ModulationInput``(service 层收集的对话信号), 输出 ``ModulationFactors``(设计 §5.2)。全部纯函数,无副作用。 v1 用即时启发式(参数集中为模块常量,便于调参);节律曲线 / 窗口函数留 v2。 care_kind 触发规则见设计 §7.3。 """ from __future__ import annotations from pydantic import BaseModel from app.modules.time_sense.snapshot import TemporalSnapshot # ==================== v1 启发式参数(可调) ==================== # 节日喜悦度权重(设计 §5.2 festival_joy) _FESTIVAL_JOY_WEIGHTS: dict[str, float] = { "春节": 0.9, "中秋节": 0.9, "元宵节": 0.7, "端午节": 0.7, "七夕": 0.7, "重阳节": 0.7, "腊八节": 0.7, "元旦": 0.5, "劳动节": 0.5, "国庆节": 0.5, "圣诞节": 0.5, "情人节": 0.5, } _FESTIVAL_JOY_BASELINE = 0.1 # 无节日 _FESTIVAL_JOY_DEFAULT = 0.5 # 未登记节日 # 季节共振(设计 §5.2 season_resonance;v2 用季节调制函数) _SEASON_RESONANCE: dict[str, float] = {"春": 0.5, "夏": 0.6, "秋": 0.7, "冬": 0.6} # 时段共振(设计 §5.2 same_period_resonance;v2 用窗口函数) _DAY_PERIOD_RESONANCE: dict[str, float] = { "凌晨": 0.8, "深夜": 0.7, "清晨": 0.4, "夜晚": 0.4, "傍晚": 0.3, "上午": 0.2, "下午": 0.2, "中午": 0.2, } # 深夜脆弱度(设计 §5.2 late_night_fragility) _FRAGILITY_DEEP_NIGHT_BASE = 0.7 # 凌晨基线 _FRAGILITY_LATE_NIGHT_BASE = 0.5 # 深夜基线 _FRAGILITY_DAYTIME = 0.1 _FRAGILITY_PER_TURN_DEEP = 0.05 # 凌晨每轮增量 _FRAGILITY_PER_TURN_LATE = 0.03 # 深夜每轮增量 # 周末放松度 _RELAX_WEEKEND = 0.8 _RELAX_WEEKEND_WITH_FESTIVAL = 0.9 _RELAX_WORKDAY = 0.2 # 关怀 _CARE_SUGGEST_HIT = 0.7 # care_kind 命中时 _CARE_SUGGEST_MISS_FACTOR = 0.5 # 未命中时按脆弱度折半 # 时差关怀阈值(分钟,3 小时) _JETLAG_DIFF_MINUTES = 180 # 深夜持续聊天阈值(轮数) _LATE_NIGHT_TURNS = 3 class ModulationInput(BaseModel): """compute_modulation 的对话信号输入。 信号由 Plan 3 service 层收集(轮数统计、内容分类等);本模块只做纯函数判定。 关怀阈值(``jetlag_threshold_minutes`` / ``late_night_min_turns``)默认 None, 由 service 层从 ``settings`` 注入(设计 §7.1/§7.3"阈值可调");None 时回退模块常量。 """ conversation_turns_today: int = 0 is_first_message_today: bool = False content_work_related: bool = False content_lonely: bool = False user_birthday_today: bool = False # 关怀触发阈值(None → 回退模块常量;service 层从 settings 注入,设计 §7.3) jetlag_threshold_minutes: int | None = None late_night_min_turns: int | None = None class ModulationFactors(BaseModel): """辰正输出的调制因子(设计 §5.2)。""" festival_joy: float late_night_fragility: float reunion_intensity: float weekend_relax: float season_resonance: float same_period_resonance: float care_suggest: float care_kind: str | None = None def _festival_joy(snapshot: TemporalSnapshot) -> float: if not snapshot.festivals: return _FESTIVAL_JOY_BASELINE return max(_FESTIVAL_JOY_WEIGHTS.get(f.name, _FESTIVAL_JOY_DEFAULT) for f in snapshot.festivals) def _late_night_fragility(snapshot: TemporalSnapshot, turns: int) -> float: period = snapshot.day_period if period == "凌晨": return min(1.0, _FRAGILITY_DEEP_NIGHT_BASE + turns * _FRAGILITY_PER_TURN_DEEP) if period == "深夜": return min(1.0, _FRAGILITY_LATE_NIGHT_BASE + turns * _FRAGILITY_PER_TURN_LATE) return _FRAGILITY_DAYTIME def _weekend_relax(snapshot: TemporalSnapshot) -> float: if not snapshot.is_weekend: return _RELAX_WORKDAY if snapshot.festivals: return _RELAX_WEEKEND_WITH_FESTIVAL return _RELAX_WEEKEND def _care_kind(snapshot: TemporalSnapshot, signals: ModulationInput) -> str | None: """设计 §7.3 关怀触发判定(按优先级)。 阈值读 ``signals``(None 回退模块常量),由 service 层从 settings 注入(§7.3 可调)。 """ deep = snapshot.day_period in {"凌晨", "深夜"} jetlag_threshold = signals.jetlag_threshold_minutes or _JETLAG_DIFF_MINUTES late_night_turns = signals.late_night_min_turns or _LATE_NIGHT_TURNS # 1. 时差关怀:深夜/凌晨 + 家时差显著 if deep and abs(snapshot.home_tz_diff_minutes) >= jetlag_threshold: return "jetlag" # 2. 用户生日 if signals.user_birthday_today: return "birthday" # 3. 清晨异常:清晨时段 + 本地 4-6 点 + 今日首条 if signals.is_first_message_today and snapshot.day_period == "清晨" and snapshot.local_hour < 6: return "early_morning" # 4. 深夜工作话题 if deep and signals.content_work_related: return "overwork" # 5. 节日孤独 if bool(snapshot.festivals) and signals.content_lonely: return "festival_lonely" # 6. 深夜持续聊天:深夜/凌晨 + 连续 ≥ N 轮 if deep and signals.conversation_turns_today >= late_night_turns: return "late_night" return None def _care_suggest(fragility: float, care_kind_value: str | None) -> float: if care_kind_value is not None: return _CARE_SUGGEST_HIT return round(fragility * _CARE_SUGGEST_MISS_FACTOR, 3) def compute_modulation(snapshot: TemporalSnapshot, signals: ModulationInput) -> ModulationFactors: """从时态快照 + 对话信号派生调制因子。纯函数。""" fragility = _late_night_fragility(snapshot, signals.conversation_turns_today) care_kind_value = _care_kind(snapshot, signals) return ModulationFactors( festival_joy=round(_festival_joy(snapshot), 3), late_night_fragility=round(fragility, 3), reunion_intensity=round(snapshot.offline.reunion_intensity, 3), weekend_relax=_weekend_relax(snapshot), season_resonance=_SEASON_RESONANCE.get(snapshot.season, 0.5), same_period_resonance=_DAY_PERIOD_RESONANCE.get(snapshot.day_period, 0.3), care_suggest=_care_suggest(fragility, care_kind_value), care_kind=care_kind_value, )