Files
hunjing/backend/app/modules/time_sense/modulation.py
T
2026-08-21 16:00:13 +08:00

176 lines
6.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.
"""调制因子派生:从时态快照 + 对话信号派生 [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,
)