Spaces:
Runtime error
Runtime error
File size: 10,758 Bytes
ec90eae | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 | """
PregoPal - 核心循环引擎
=======================
状态机驱动的每日生命周期管理。
状态流转:
LAUNCH → FAMILY_QUIZ → SUMMARIZE → ANALYZE → BRIEF → INTERACT
→ CONSOLIDATE → (THREE_DAY) → DONE
状态位机制:
data/presets/.daily_status.json 记录每日完成状态
"""
import json
import datetime
import asyncio
from enum import Enum
from pathlib import Path
from dataclasses import dataclass, field
from plugins.base import LoopPlugin, PluginResult, PluginRegistry, LoopStage, LoopContext
PRESETS_DIR = Path("data/presets")
STATUS_FILE = PRESETS_DIR / ".daily_status.json"
# ============================================================
# 状态枚举
# ============================================================
class LoopState(Enum):
LAUNCH = "launch" # 启动检查:检查今日状态位
FAMILY_QUIZ = "family_quiz" # 家庭问卷:检查是否需要询问菜谱/体重
SUMMARIZE = "summarize" # 昨日总结:分析昨日饮食/体重/家庭记忆
ANALYZE = "analyze" # 营养分析:对比 DRIs
BRIEF = "brief" # 生成今日简报
INTERACT = "interact" # 白天交互模式(等待用户操作)
THREE_DAY = "three_day" # 每三天自动总结
CONSOLIDATE = "consolidate" # 晚间整理
DONE = "done" # 标记今日完成
# ============================================================
# 状态转移表
# ============================================================
_TRANSITIONS = {
(LoopState.LAUNCH, "need_summary"): LoopState.FAMILY_QUIZ,
(LoopState.LAUNCH, "already_done"): LoopState.INTERACT,
(LoopState.FAMILY_QUIZ, "ok"): LoopState.SUMMARIZE,
(LoopState.SUMMARIZE, "ok"): LoopState.ANALYZE,
(LoopState.ANALYZE, "ok"): LoopState.BRIEF,
(LoopState.BRIEF, "ok"): LoopState.INTERACT,
(LoopState.INTERACT, "day_ended"): LoopState.CONSOLIDATE,
(LoopState.CONSOLIDATE, "need_3day"): LoopState.THREE_DAY,
(LoopState.CONSOLIDATE, "ok"): LoopState.DONE,
(LoopState.THREE_DAY, "ok"): LoopState.DONE,
}
# ============================================================
# 状态位管理
# ============================================================
class DailyStatus:
"""管理每日状态位"""
STATUS_FILE = STATUS_FILE
@staticmethod
def load() -> dict:
"""加载状态位文件"""
if STATUS_FILE.exists():
with open(STATUS_FILE, 'r', encoding='utf-8') as f:
return json.load(f)
return {}
@staticmethod
def save(status: dict):
"""保存状态位文件"""
PRESETS_DIR.mkdir(parents=True, exist_ok=True)
with open(STATUS_FILE, 'w', encoding='utf-8') as f:
json.dump(status, f, ensure_ascii=False, indent=2)
@staticmethod
def is_today_done() -> bool:
"""检查今日是否已完成总结"""
today = datetime.date.today().isoformat()
status = DailyStatus.load()
return status.get(today, {}).get("summary_done", False)
@staticmethod
def mark_summary_done():
"""标记今日总结完成"""
today = datetime.date.today().isoformat()
status = DailyStatus.load()
status[today] = status.get(today, {})
status[today]["summary_done"] = True
status[today]["completed_at"] = datetime.datetime.now().isoformat()
DailyStatus.save(status)
@staticmethod
def mark_day_ended():
"""标记今日结束"""
today = datetime.date.today().isoformat()
status = DailyStatus.load()
status[today] = status.get(today, {})
status[today]["day_ended"] = True
status[today]["ended_at"] = datetime.datetime.now().isoformat()
DailyStatus.save(status)
@staticmethod
def get_last_summary_date() -> str | None:
"""获取最近一次总结的日期"""
status = DailyStatus.load()
done_dates = [d for d, v in status.items() if v.get("summary_done")]
return max(done_dates) if done_dates else None
@staticmethod
def days_since_last_summary() -> int:
"""距离上次总结的天数"""
last = DailyStatus.get_last_summary_date()
if last is None:
return 999
last_date = datetime.date.fromisoformat(last)
return (datetime.date.today() - last_date).days
@staticmethod
def should_three_day_summary() -> bool:
"""检查是否需要进行三天总结"""
return DailyStatus.days_since_last_summary() >= 3
# ============================================================
# 主循环引擎
# ============================================================
class PregoPalLoop:
"""核心循环引擎"""
def __init__(self):
self.plugins = PluginRegistry()
self.state = LoopState.LAUNCH
self.context = LoopContext()
self._register_default_plugins()
def _register_default_plugins(self):
"""注册默认插件"""
from plugins.family_quiz import FamilyRecipeQuizPlugin, WeightQuizPlugin
from plugins.diet_summary import DietSummaryPlugin
from plugins.weight_check import WeightCheckPlugin
from plugins.family_memory import FamilyMemoryPlugin
from plugins.dri_analysis import DRIAnalysisPlugin
from plugins.briefing_generator import BriefingGeneratorPlugin
from plugins.three_day_summary import ThreeDaySummaryPlugin
from plugins.preset_writer import PresetWriterPlugin
self.plugins.register(FamilyRecipeQuizPlugin())
self.plugins.register(WeightQuizPlugin())
self.plugins.register(DietSummaryPlugin())
self.plugins.register(WeightCheckPlugin())
self.plugins.register(FamilyMemoryPlugin())
self.plugins.register(DRIAnalysisPlugin())
self.plugins.register(BriefingGeneratorPlugin())
self.plugins.register(ThreeDaySummaryPlugin())
self.plugins.register(PresetWriterPlugin())
async def run(self) -> None:
"""主循环入口"""
while self.state is not LoopState.DONE:
handler_name = f"_state_{self.state.value}"
handler = getattr(self, handler_name)
event = await handler()
next_state = _TRANSITIONS.get((self.state, event))
if next_state is None:
raise RuntimeError(
f"No transition from {self.state} on '{event}'"
)
self.state = next_state
# ============================================================
# 状态处理器
# ============================================================
async def _state_launch(self) -> str:
"""启动检查:检查今日状态位"""
if DailyStatus.is_today_done():
return "already_done"
return "need_summary"
async def _state_family_quiz(self) -> str:
"""家庭问卷:检查是否需要询问菜谱/体重"""
for plugin in self.plugins.get_plugins(LoopStage.FAMILY_QUIZ):
result = await plugin.run(self.context)
if not result.success:
self.context.errors.append(result.message)
return "ok"
async def _state_summarize(self) -> str:
"""昨日总结:执行所有 SUMMARIZE 阶段插件"""
for plugin in self.plugins.get_plugins(LoopStage.SUMMARIZE):
result = await plugin.run(self.context)
if not result.success:
self.context.errors.append(result.message)
return "ok"
async def _state_analyze(self) -> str:
"""营养分析:执行所有 ANALYZE 阶段插件"""
for plugin in self.plugins.get_plugins(LoopStage.ANALYZE):
result = await plugin.run(self.context)
if not result.success:
self.context.errors.append(result.message)
return "ok"
async def _state_brief(self) -> str:
"""生成今日简报:执行所有 BRIEF 阶段插件"""
for plugin in self.plugins.get_plugins(LoopStage.BRIEF):
result = await plugin.run(self.context)
if not result.success:
self.context.errors.append(result.message)
return "ok"
async def _state_interact(self) -> str:
"""
白天交互模式:等待用户操作
此状态由外部事件触发(Gradio 界面交互)
"""
return "day_ended"
async def _state_three_day(self) -> str:
"""每三天总结:执行所有 THREE_DAY 阶段插件"""
for plugin in self.plugins.get_plugins(LoopStage.THREE_DAY):
result = await plugin.run(self.context)
if not result.success:
self.context.errors.append(result.message)
return "ok"
async def _state_consolidate(self) -> str:
"""晚间整理:执行所有 CONSOLIDATE 阶段插件"""
for plugin in self.plugins.get_plugins(LoopStage.CONSOLIDATE):
result = await plugin.run(self.context)
if not result.success:
self.context.errors.append(result.message)
if DailyStatus.should_three_day_summary():
return "need_3day"
return "ok"
async def _state_done(self) -> str:
"""标记今日完成"""
DailyStatus.mark_day_ended()
return "done_complete"
# ============================================================
# 外部接口
# ============================================================
def get_briefing(self) -> dict:
"""获取今日简报"""
return self.context.briefing
def get_thinking_keywords(self) -> str:
"""获取当前思考关键词(用于 UI 显示)"""
return self.context.briefing.get("thinking_keywords", "")
def get_errors(self) -> list[str]:
"""获取错误列表"""
return self.context.errors
def run_sync(self):
"""同步运行(用于非异步环境)"""
asyncio.run(self.run())
# ============================================================
# 便捷函数
# ============================================================
def run_daily_loop():
"""运行每日循环(同步入口)"""
loop = PregoPalLoop()
loop.run_sync()
return loop
def check_and_run_loop() -> PregoPalLoop:
"""
检查并运行循环(供 Gradio 启动时调用)
如果今日已总结,直接返回 loop 实例(不执行总结流程)
如果今日未总结,执行完整总结流程
"""
loop = PregoPalLoop()
if not DailyStatus.is_today_done():
loop.run_sync()
return loop
|