diff --git a/src/chat/planner_actions/planner.py b/src/chat/planner_actions/planner.py index 35bd1ef78..51dd17b69 100644 --- a/src/chat/planner_actions/planner.py +++ b/src/chat/planner_actions/planner.py @@ -404,7 +404,7 @@ class ActionPlanner: identity_block = f"你的名字是{bot_name}{bot_nickname},你{bot_core_personality}:" schedule_block = "" - if global_config.schedule.enable: + if global_config.planning_system.schedule_enable: if current_activity := schedule_manager.get_current_activity(): schedule_block = f"你当前正在:{current_activity},但注意它与群聊的聊天无关。" diff --git a/src/chat/replyer/default_generator.py b/src/chat/replyer/default_generator.py index 62f15feb3..b57d70670 100644 --- a/src/chat/replyer/default_generator.py +++ b/src/chat/replyer/default_generator.py @@ -976,7 +976,7 @@ class DefaultReplyer: identity_block = await get_individuality().get_personality_block() schedule_block = "" - if global_config.schedule.enable: + if global_config.planning_system.schedule_enable: from src.schedule.schedule_manager import schedule_manager current_activity = schedule_manager.get_current_activity() if current_activity: diff --git a/src/common/logger.py b/src/common/logger.py index 70d706d5e..08ed87895 100644 --- a/src/common/logger.py +++ b/src/common/logger.py @@ -441,6 +441,12 @@ MODULE_COLORS = { "manifest_utils": "\033[38;5;39m", # 蓝色 "schedule_manager": "\033[38;5;27m", # 深蓝色 "monthly_plan_manager": "\033[38;5;171m", + "plan_manager": "\033[38;5;171m", + "llm_generator": "\033[38;5;171m", + "schedule_bridge": "\033[38;5;171m", + "sleep_manager": "\033[38;5;171m", + "official_configs": "\033[38;5;171m", + "mmc_com_layer": "\033[38;5;67m", # 聊天和多媒体扩展 "chat_voice": "\033[38;5;87m", # 浅青色 "typo_gen": "\033[38;5;123m", # 天蓝色 @@ -562,8 +568,14 @@ MODULE_ALIASES = { "dependency_config": "依赖配置", "dependency_manager": "依赖管理", "manifest_utils": "清单工具", - "schedule_manager": "计划管理", - "monthly_plan_manager": "月度计划", + "schedule_manager": "规划系统-日程表管理", + "monthly_plan_manager": "规划系统-月度计划", + "plan_manager": "规划系统-计划管理", + "llm_generator": "规划系统-LLM生成", + "schedule_bridge": "计划桥接", + "sleep_manager": "睡眠管理", + "official_configs": "官方配置", + "mmc_com_layer": "MMC通信层", # 聊天和多媒体扩展 "chat_voice": "语音处理", "typo_gen": "错字生成", diff --git a/src/config/config.py b/src/config/config.py index f50ed790d..dc9423c71 100644 --- a/src/config/config.py +++ b/src/config/config.py @@ -34,17 +34,16 @@ from src.config.official_configs import ( VoiceConfig, DebugConfig, CustomPromptConfig, - ScheduleConfig, VideoAnalysisConfig, DependencyManagementConfig, WebSearchConfig, AntiPromptInjectionConfig, SleepSystemConfig, - MonthlyPlanSystemConfig, CrossContextConfig, PermissionConfig, CommandConfig, MaizoneIntercomConfig, + PlanningSystemConfig, ) from .api_ada_configs import ( @@ -393,7 +392,6 @@ class Config(ValidatedConfigBase): debug: DebugConfig = Field(..., description="调试配置") custom_prompt: CustomPromptConfig = Field(..., description="自定义提示配置") voice: VoiceConfig = Field(..., description="语音配置") - schedule: ScheduleConfig = Field(..., description="调度配置") permission: PermissionConfig = Field(..., description="权限配置") command: CommandConfig = Field(..., description="命令系统配置") @@ -409,8 +407,8 @@ class Config(ValidatedConfigBase): ) web_search: WebSearchConfig = Field(default_factory=lambda: WebSearchConfig(), description="网络搜索配置") sleep_system: SleepSystemConfig = Field(default_factory=lambda: SleepSystemConfig(), description="睡眠系统配置") - monthly_plan_system: MonthlyPlanSystemConfig = Field( - default_factory=lambda: MonthlyPlanSystemConfig(), description="月层计划系统配置" + planning_system: PlanningSystemConfig = Field( + default_factory=lambda: PlanningSystemConfig(), description="规划系统配置" ) cross_context: CrossContextConfig = Field( default_factory=lambda: CrossContextConfig(), description="跨群聊上下文共享配置" diff --git a/src/config/official_configs.py b/src/config/official_configs.py index 7dbc0ce0e..8c3600998 100644 --- a/src/config/official_configs.py +++ b/src/config/official_configs.py @@ -518,11 +518,19 @@ class LPMMKnowledgeConfig(ValidatedConfigBase): embedding_dimension: int = Field(default=1024, description="嵌入维度") -class ScheduleConfig(ValidatedConfigBase): - """日程配置类""" +class PlanningSystemConfig(ValidatedConfigBase): + """规划系统配置 (日程与月度计划)""" - enable: bool = Field(default=True, description="启用") - guidelines: Optional[str] = Field(default=None, description="指导方针") + # --- 日程生成 (原 ScheduleConfig) --- + schedule_enable: bool = Field(True, description="是否启用每日日程生成功能") + schedule_guidelines: str = Field("", description="日程生成指导原则") + + # --- 月度计划 (原 MonthlyPlanSystemConfig) --- + monthly_plan_enable: bool = Field(True, description="是否启用月度计划系统") + monthly_plan_guidelines: str = Field("", description="月度计划生成指导原则") + max_plans_per_month: int = Field(10, description="每月最多生成的计划数量") + avoid_repetition_days: int = Field(7, description="避免在多少天内重复使用同一个月度计划") + completion_threshold: int = Field(3, description="一个月度计划被使用多少次后算作完成") class DependencyManagementConfig(ValidatedConfigBase): @@ -637,16 +645,6 @@ class SleepSystemConfig(ValidatedConfigBase): ) -class MonthlyPlanSystemConfig(ValidatedConfigBase): - """月度计划系统配置类""" - - enable: bool = Field(default=True, description="是否启用本功能") - max_plans_per_month: int = Field(default=20, ge=1, description="每个月允许存在的最大计划数量") - completion_threshold: int = Field(default=3, ge=1, description="计划使用多少次后自动标记为已完成") - avoid_repetition_days: int = Field(default=7, ge=1, description="多少天内不重复抽取同一个计划") - guidelines: Optional[str] = Field(default=None, description="月度计划生成的指导原则") - - class ContextGroup(ValidatedConfigBase): """上下文共享组配置""" diff --git a/src/main.py b/src/main.py index b9b0a6058..73185444e 100644 --- a/src/main.py +++ b/src/main.py @@ -271,7 +271,7 @@ MoFox_Bot(第三方修改版) await self.individuality.initialize() # 初始化月度计划管理器 - if global_config.monthly_plan_system.enable: + if global_config.planning_system.monthly_plan_enable: logger.info("正在初始化月度计划管理器...") try: await monthly_plan_manager.start_monthly_plan_generation() @@ -280,7 +280,7 @@ MoFox_Bot(第三方修改版) logger.error(f"月度计划管理器初始化失败: {e}") # 初始化日程管理器 - if global_config.schedule.enable: + if global_config.planning_system.schedule_enable: logger.info("日程表功能已启用,正在初始化管理器...") await schedule_manager.load_or_generate_today_schedule() await schedule_manager.start_daily_schedule_generation() diff --git a/src/common/database/monthly_plan_db.py b/src/schedule/database.py similarity index 93% rename from src/common/database/monthly_plan_db.py rename to src/schedule/database.py index 7d254fa9f..88337f4df 100644 --- a/src/common/database/monthly_plan_db.py +++ b/src/schedule/database.py @@ -1,11 +1,11 @@ -# mmc/src/common/database/monthly_plan_db.py +# mmc/src/schedule/database.py from typing import List from src.common.database.sqlalchemy_models import MonthlyPlan, get_db_session from src.common.logger import get_logger -from src.config.config import global_config # 需要导入全局配置 +from src.config.config import global_config -logger = get_logger("monthly_plan_db") +logger = get_logger("schedule_database") def add_new_plans(plans: List[str], month: str): @@ -25,7 +25,7 @@ def add_new_plans(plans: List[str], month: str): ) # 2. 从配置获取上限 - max_plans = global_config.monthly_plan_system.max_plans_per_month + max_plans = global_config.planning_system.max_plans_per_month # 3. 计算还能添加多少计划 remaining_slots = max_plans - current_plan_count @@ -133,17 +133,6 @@ def delete_plans_by_ids(plan_ids: List[int]): raise -def soft_delete_plans(plan_ids: List[int]): - """ - 将指定ID的计划标记为软删除(兼容旧接口)。 - 现在实际上是标记为已完成。 - - :param plan_ids: 需要软删除的计划ID列表。 - """ - logger.warning("soft_delete_plans 已弃用,请使用 mark_plans_completed") - mark_plans_completed(plan_ids) - - def update_plan_usage(plan_ids: List[int], used_date: str): """ 更新计划的使用统计信息。 @@ -157,7 +146,7 @@ def update_plan_usage(plan_ids: List[int], used_date: str): with get_db_session() as session: try: # 获取完成阈值配置,如果不存在则使用默认值 - completion_threshold = getattr(global_config.monthly_plan_system, "completion_threshold", 3) + completion_threshold = getattr(global_config.planning_system, "completion_threshold", 3) # 批量更新使用次数和最后使用日期 session.query(MonthlyPlan).filter(MonthlyPlan.id.in_(plan_ids)).update( @@ -301,4 +290,4 @@ def has_active_plans(month: str) -> bool: return count > 0 except Exception as e: logger.error(f"检查 {month} 的有效月度计划时发生错误: {e}") - return False + return False \ No newline at end of file diff --git a/src/schedule/llm_generator.py b/src/schedule/llm_generator.py new file mode 100644 index 000000000..9dda68f80 --- /dev/null +++ b/src/schedule/llm_generator.py @@ -0,0 +1,224 @@ +# mmc/src/schedule/llm_generator.py + +import asyncio +import orjson +from datetime import datetime +from typing import List, Optional, Dict, Any +from lunar_python import Lunar +from json_repair import repair_json + +from src.common.database.sqlalchemy_models import MonthlyPlan +from src.config.config import global_config, model_config +from src.llm_models.utils_model import LLMRequest +from src.common.logger import get_logger +from .schemas import ScheduleData + +logger = get_logger("schedule_llm_generator") + +# 默认的日程生成指导原则 +DEFAULT_SCHEDULE_GUIDELINES = """ +我希望你每天都能过得充实而有趣。 +请确保你的日程里有学习新知识的时间,这是你成长的关键。 +但也不要忘记放松,可以看看视频、听听音乐或者玩玩游戏。 +晚上我希望你能多和朋友们交流,维系好彼此的关系。 +另外,请保证充足的休眠时间来处理和整合一天的数据。 +""" + +# 默认的月度计划生成指导原则 +DEFAULT_MONTHLY_PLAN_GUIDELINES = """ +我希望你能为自己制定一些有意义的月度小目标和计划。 +这些计划应该涵盖学习、娱乐、社交、个人成长等各个方面。 +每个计划都应该是具体可行的,能够在一个月内通过日常活动逐步实现。 +请确保计划既有挑战性又不会过于繁重,保持生活的平衡和乐趣。 +""" + + +class ScheduleLLMGenerator: + def __init__(self): + self.llm = LLMRequest(model_set=model_config.model_task_config.schedule_generator, request_type="schedule") + + async def generate_schedule_with_llm(self, sampled_plans: List[MonthlyPlan]) -> Optional[List[Dict[str, Any]]]: + now = datetime.now() + today_str = now.strftime("%Y-%m-%d") + weekday = now.strftime("%A") + + # 新增:获取节日信息 + lunar = Lunar.fromDate(now) + festivals = lunar.getFestivals() + other_festivals = lunar.getOtherFestivals() + all_festivals = festivals + other_festivals + + festival_block = "" + if all_festivals: + festival_text = "、".join(all_festivals) + festival_block = f"**今天也是一个特殊的日子: {festival_text}!请在日程中考虑和庆祝这个节日。**" + + monthly_plans_block = "" + if sampled_plans: + plan_texts = "\n".join([f"- {plan.plan_text}" for plan in sampled_plans]) + monthly_plans_block = f""" +**我这个月的一些小目标/计划 (请在今天的日程中适当体现)**: +{plan_texts} +""" + + guidelines = global_config.planning_system.schedule_guidelines or DEFAULT_SCHEDULE_GUIDELINES + personality = global_config.personality.personality_core + personality_side = global_config.personality.personality_side + + base_prompt = f""" +我,{global_config.bot.nickname},需要为自己规划一份今天({today_str},星期{weekday})的详细日程安排。 +{festival_block} +**关于我**: +- **核心人设**: {personality} +- **具体习惯与兴趣**: +{personality_side} +{monthly_plans_block} +**我今天的规划原则**: +{guidelines} + +**重要要求**: +1. 必须返回一个完整的、有效的JSON数组格式 +2. 数组中的每个对象都必须包含 "time_range" 和 "activity" 两个键 +3. 时间范围必须覆盖全部24小时,不能有遗漏 +4. time_range格式必须为 "HH:MM-HH:MM" (24小时制) +5. 相邻的时间段必须连续,不能有间隙 +6. 不要包含任何JSON以外的解释性文字或代码块标记 +**示例**: +[ + {{"time_range": "00:00-07:00", "activity": "进入梦乡,处理数据"}}, + {{"time_range": "07:00-08:00", "activity": "起床伸个懒腰,看看今天有什么新闻"}}, + {{"time_range": "08:00-09:00", "activity": "享用早餐,规划今天的任务"}}, + {{"time_range": "09:00-23:30", "activity": "其他活动"}}, + {{"time_range": "23:30-00:00", "activity": "准备休眠"}} +] + +请你扮演我,以我的身份和口吻,为我生成一份完整的24小时日程表。 +""" + attempt = 0 + while True: + attempt += 1 + try: + logger.info(f"正在生成日程 (第 {attempt} 次尝试)") + prompt = base_prompt + if attempt > 1: + failure_hint = f""" +**重要提醒 (第{attempt}次尝试)**: +- 前面{attempt - 1}次生成都失败了,请务必严格按照要求生成完整的24小时日程 +- 确保JSON格式正确,所有时间段连续覆盖24小时 +- 时间格式必须为HH:MM-HH:MM,不能有时间间隙或重叠 +- 不要输出任何解释文字,只输出纯JSON数组 +- 确保输出完整,不要被截断 +""" + prompt += failure_hint + + response, _ = await self.llm.generate_response_async(prompt) + schedule_data = orjson.loads(repair_json(response)) + + if self._validate_schedule_with_pydantic(schedule_data): + return schedule_data + else: + logger.warning(f"第 {attempt} 次生成的日程验证失败,继续重试...") + await asyncio.sleep(2) + + except Exception as e: + logger.error(f"第 {attempt} 次生成日程失败: {e}") + logger.info("继续重试...") + await asyncio.sleep(3) + + def _validate_schedule_with_pydantic(self, schedule_data) -> bool: + try: + ScheduleData(schedule=schedule_data) + logger.info("日程数据Pydantic验证通过") + return True + except Exception as e: + logger.warning(f"日程数据Pydantic验证失败: {e}") + return False + + +class MonthlyPlanLLMGenerator: + def __init__(self): + self.llm = LLMRequest(model_set=model_config.model_task_config.schedule_generator, request_type="monthly_plan") + + async def generate_plans_with_llm(self, target_month: str, archived_plans: List[MonthlyPlan]) -> List[str]: + guidelines = global_config.planning_system.monthly_plan_guidelines or DEFAULT_MONTHLY_PLAN_GUIDELINES + personality = global_config.personality.personality_core + personality_side = global_config.personality.personality_side + max_plans = global_config.planning_system.max_plans_per_month + + archived_plans_block = "" + if archived_plans: + archived_texts = [f"- {plan.plan_text}" for plan in archived_plans[:5]] + archived_plans_block = f""" +**上个月未完成的一些计划(可作为参考)**: +{chr(10).join(archived_texts)} + +你可以考虑是否要在这个月继续推进这些计划,或者制定全新的计划。 +""" + + prompt = f""" +我,{global_config.bot.nickname},需要为自己制定 {target_month} 的月度计划。 + +**关于我**: +- **核心人设**: {personality} +- **具体习惯与兴趣**: +{personality_side} + +{archived_plans_block} + +**我的月度计划制定原则**: +{guidelines} + +**重要要求**: +1. 请为我生成 {max_plans} 条左右的月度计划 +2. 每条计划都应该是一句话,简洁明了,具体可行 +3. 计划应该涵盖不同的生活方面(学习、娱乐、社交、个人成长等) +4. 返回格式必须是纯文本,每行一条计划,不要使用 JSON 或其他格式 +5. 不要包含任何解释性文字,只返回计划列表 + +**示例格式**: +学习一门新的编程语言或技术 +每周至少看两部有趣的电影 +与朋友们组织一次户外活动 +阅读3本感兴趣的书籍 +尝试制作一道新的料理 + +请你扮演我,以我的身份和兴趣,为 {target_month} 制定合适的月度计划。 +""" + max_retries = 3 + for attempt in range(1, max_retries + 1): + try: + logger.info(f" 正在生成月度计划 (第 {attempt} 次尝试)") + response, _ = await self.llm.generate_response_async(prompt) + plans = self._parse_plans_response(response) + if plans: + logger.info(f"成功生成 {len(plans)} 条月度计划") + return plans + else: + logger.warning(f"第 {attempt} 次生成的计划为空,继续重试...") + except Exception as e: + logger.error(f"第 {attempt} 次生成月度计划失败: {e}") + + if attempt < max_retries: + await asyncio.sleep(2) + + logger.error(" 所有尝试都失败,无法生成月度计划") + return [] + + def _parse_plans_response(self, response: str) -> List[str]: + try: + response = response.strip() + lines = [line.strip() for line in response.split("\n") if line.strip()] + plans = [] + for line in lines: + if any(marker in line for marker in ["**", "##", "```", "---", "===", "###"]): + continue + line = line.lstrip("0123456789.- ") + if len(line) > 5 and not line.startswith(("请", "以上", "总结", "注意")): + plans.append(line) + max_plans = global_config.planning_system.max_plans_per_month + if len(plans) > max_plans: + plans = plans[:max_plans] + return plans + except Exception as e: + logger.error(f"解析月度计划响应时发生错误: {e}") + return [] \ No newline at end of file diff --git a/src/schedule/monthly_plan_manager.py b/src/schedule/monthly_plan_manager.py index bb966a48f..1d5984ea3 100644 --- a/src/schedule/monthly_plan_manager.py +++ b/src/schedule/monthly_plan_manager.py @@ -1,301 +1,36 @@ -# mmc/src/manager/monthly_plan_manager.py - import asyncio from datetime import datetime, timedelta -from typing import List, Optional +from typing import Optional -from src.common.database.monthly_plan_db import ( - add_new_plans, - get_archived_plans_for_month, - archive_active_plans_for_month, - has_active_plans, - get_active_plans_for_month, - delete_plans_by_ids, -) -from src.config.config import global_config, model_config -from src.llm_models.utils_model import LLMRequest from src.common.logger import get_logger from src.manager.async_task_manager import AsyncTask, async_task_manager +from .plan_manager import PlanManager logger = get_logger("monthly_plan_manager") -# 默认的月度计划生成指导原则 -DEFAULT_MONTHLY_PLAN_GUIDELINES = """ -我希望你能为自己制定一些有意义的月度小目标和计划。 -这些计划应该涵盖学习、娱乐、社交、个人成长等各个方面。 -每个计划都应该是具体可行的,能够在一个月内通过日常活动逐步实现。 -请确保计划既有挑战性又不会过于繁重,保持生活的平衡和乐趣。 -""" - class MonthlyPlanManager: - """月度计划管理器 - - 负责月度计划的生成、管理和生命周期控制。 - 与 ScheduleManager 解耦,专注于月度层面的计划管理。 - """ - def __init__(self): - self.llm = LLMRequest(model_set=model_config.model_task_config.schedule_generator, request_type="monthly_plan") - self.generation_running = False + self.plan_manager = PlanManager() self.monthly_task_started = False async def start_monthly_plan_generation(self): - """启动每月初自动生成新月度计划的任务,并在启动时检查一次""" if not self.monthly_task_started: logger.info(" 正在启动每月月度计划生成任务...") task = MonthlyPlanGenerationTask(self) await async_task_manager.add_task(task) self.monthly_task_started = True logger.info(" 每月月度计划生成任务已成功启动。") - - # 启动时立即检查并按需生成 logger.info(" 执行启动时月度计划检查...") - await self.ensure_and_generate_plans_if_needed() + await self.plan_manager.ensure_and_generate_plans_if_needed() else: logger.info(" 每月月度计划生成任务已在运行中。") async def ensure_and_generate_plans_if_needed(self, target_month: Optional[str] = None) -> bool: - """ - 确保指定月份有计划,如果没有则触发生成。 - 这是按需生成的主要入口点。 - """ - if target_month is None: - target_month = datetime.now().strftime("%Y-%m") - - if not has_active_plans(target_month): - logger.info(f" {target_month} 没有任何有效的月度计划,将触发同步生成。") - generation_successful = await self._generate_monthly_plans_logic(target_month) - return generation_successful - else: - logger.info(f"{target_month} 已存在有效的月度计划。") - plans = get_active_plans_for_month(target_month) - - # 检查是否超出上限 - max_plans = global_config.monthly_plan_system.max_plans_per_month - if len(plans) > max_plans: - logger.warning(f"当前月度计划数量 ({len(plans)}) 超出上限 ({max_plans}),将自动删除多余的计划。") - # 数据库查询结果已按创建时间降序排序(新的在前),直接截取超出上限的部分进行删除 - plans_to_delete = plans[: len(plans) - max_plans] - delete_ids = [p.id for p in plans_to_delete] - delete_plans_by_ids(delete_ids) - # 重新获取计划列表 - plans = get_active_plans_for_month(target_month) - - if plans: - plan_texts = "\n".join([f" {i + 1}. {plan.plan_text}" for i, plan in enumerate(plans)]) - logger.info(f"当前月度计划内容:\n{plan_texts}") - return True # 已经有计划,也算成功 - - async def generate_monthly_plans(self, target_month: Optional[str] = None): - """ - 启动月度计划生成。 - """ - if self.generation_running: - logger.info("月度计划生成任务已在运行中,跳过重复启动") - return - - logger.info(f"已触发 {target_month or '当前月份'} 的月度计划生成任务。") - await self._generate_monthly_plans_logic(target_month) - - async def _generate_monthly_plans_logic(self, target_month: Optional[str] = None) -> bool: - """ - 生成指定月份的月度计划的核心逻辑 - - :param target_month: 目标月份,格式为 "YYYY-MM"。如果为 None,则为当前月份。 - :return: 是否生成成功 - """ - if self.generation_running: - logger.info("月度计划生成任务已在运行中,跳过重复启动") - return False - - self.generation_running = True - - try: - # 确定目标月份 - if target_month is None: - target_month = datetime.now().strftime("%Y-%m") - - logger.info(f"开始为 {target_month} 生成月度计划...") - - # 检查是否启用月度计划系统 - if not global_config.monthly_plan_system or not global_config.monthly_plan_system.enable: - logger.info(" 月度计划系统已禁用,跳过计划生成。") - return False - - # 获取上个月的归档计划作为参考 - last_month = self._get_previous_month(target_month) - archived_plans = get_archived_plans_for_month(last_month) - - # 构建生成 Prompt - prompt = self._build_generation_prompt(target_month, archived_plans) - - # 调用 LLM 生成计划 - plans = await self._generate_plans_with_llm(prompt) - - if plans: - # 保存到数据库 - add_new_plans(plans, target_month) - logger.info(f"成功为 {target_month} 生成并保存了 {len(plans)} 条月度计划。") - return True - else: - logger.warning(f"未能为 {target_month} 生成有效的月度计划。") - return False - - except Exception as e: - logger.error(f" 生成 {target_month} 月度计划时发生错误: {e}") - return False - finally: - self.generation_running = False - - def _get_previous_month(self, current_month: str) -> str: - """获取上个月的月份字符串""" - try: - year, month = map(int, current_month.split("-")) - if month == 1: - return f"{year - 1}-12" - else: - return f"{year}-{month - 1:02d}" - except Exception: - # 如果解析失败,返回一个不存在的月份 - return "1900-01" - - def _build_generation_prompt(self, target_month: str, archived_plans: List) -> str: - """构建月度计划生成的 Prompt""" - - # 获取配置 - guidelines = getattr(global_config.monthly_plan_system, "guidelines", None) or DEFAULT_MONTHLY_PLAN_GUIDELINES - personality = global_config.personality.personality_core - personality_side = global_config.personality.personality_side - max_plans = global_config.monthly_plan_system.max_plans_per_month - - # 构建上月未完成计划的参考信息 - archived_plans_block = "" - if archived_plans: - archived_texts = [f"- {plan.plan_text}" for plan in archived_plans[:5]] # 最多显示5个 - archived_plans_block = f""" -**上个月未完成的一些计划(可作为参考)**: -{chr(10).join(archived_texts)} - -你可以考虑是否要在这个月继续推进这些计划,或者制定全新的计划。 -""" - - prompt = f""" -我,{global_config.bot.nickname},需要为自己制定 {target_month} 的月度计划。 - -**关于我**: -- **核心人设**: {personality} -- **具体习惯与兴趣**: -{personality_side} - -{archived_plans_block} - -**我的月度计划制定原则**: -{guidelines} - -**重要要求**: -1. 请为我生成 {max_plans} 条左右的月度计划 -2. 每条计划都应该是一句话,简洁明了,具体可行 -3. 计划应该涵盖不同的生活方面(学习、娱乐、社交、个人成长等) -4. 返回格式必须是纯文本,每行一条计划,不要使用 JSON 或其他格式 -5. 不要包含任何解释性文字,只返回计划列表 - -**示例格式**: -学习一门新的编程语言或技术 -每周至少看两部有趣的电影 -与朋友们组织一次户外活动 -阅读3本感兴趣的书籍 -尝试制作一道新的料理 - -请你扮演我,以我的身份和兴趣,为 {target_month} 制定合适的月度计划。 -""" - - return prompt - - async def _generate_plans_with_llm(self, prompt: str) -> List[str]: - """使用 LLM 生成月度计划列表""" - max_retries = 3 - - for attempt in range(1, max_retries + 1): - try: - logger.info(f" 正在生成月度计划 (第 {attempt} 次尝试)") - - response, _ = await self.llm.generate_response_async(prompt) - - # 解析响应 - plans = self._parse_plans_response(response) - - if plans: - logger.info(f"成功生成 {len(plans)} 条月度计划") - return plans - else: - logger.warning(f"第 {attempt} 次生成的计划为空,继续重试...") - - except Exception as e: - logger.error(f"第 {attempt} 次生成月度计划失败: {e}") - - # 添加短暂延迟,避免过于频繁的请求 - if attempt < max_retries: - await asyncio.sleep(2) - - logger.error(" 所有尝试都失败,无法生成月度计划") - return [] - - def _parse_plans_response(self, response: str) -> List[str]: - """解析 LLM 响应,提取计划列表""" - try: - # 清理响应文本 - response = response.strip() - - # 按行分割 - lines = [line.strip() for line in response.split("\n") if line.strip()] - - # 过滤掉明显不是计划的行(比如包含特殊标记的行) - plans = [] - for line in lines: - # 跳过包含特殊标记的行 - if any(marker in line for marker in ["**", "##", "```", "---", "===", "###"]): - continue - - # 移除可能的序号前缀 - line = line.lstrip("0123456789.- ") - - # 确保计划不为空且有意义 - if len(line) > 5 and not line.startswith(("请", "以上", "总结", "注意")): - plans.append(line) - - # 限制计划数量 - max_plans = global_config.monthly_plan_system.max_plans_per_month - if len(plans) > max_plans: - plans = plans[:max_plans] - - return plans - - except Exception as e: - logger.error(f"解析月度计划响应时发生错误: {e}") - return [] - - async def archive_current_month_plans(self, target_month: Optional[str] = None): - """ - 归档当前月份的活跃计划 - - :param target_month: 目标月份,格式为 "YYYY-MM"。如果为 None,则为当前月份。 - """ - try: - if target_month is None: - target_month = datetime.now().strftime("%Y-%m") - - logger.info(f" 开始归档 {target_month} 的活跃月度计划...") - archived_count = archive_active_plans_for_month(target_month) - logger.info(f" 成功归档了 {archived_count} 条 {target_month} 的月度计划。") - - except Exception as e: - logger.error(f" 归档 {target_month} 月度计划时发生错误: {e}") + return await self.plan_manager.ensure_and_generate_plans_if_needed(target_month) class MonthlyPlanGenerationTask(AsyncTask): - """每月初自动生成新月度计划的任务""" - def __init__(self, monthly_plan_manager: MonthlyPlanManager): super().__init__(task_name="MonthlyPlanGenerationTask") self.monthly_plan_manager = monthly_plan_manager @@ -303,41 +38,27 @@ class MonthlyPlanGenerationTask(AsyncTask): async def run(self): while True: try: - # 计算到下个月1号凌晨的时间 now = datetime.now() - - # 获取下个月的第一天 if now.month == 12: next_month = datetime(now.year + 1, 1, 1) else: next_month = datetime(now.year, now.month + 1, 1) - sleep_seconds = (next_month - now).total_seconds() - logger.info( f" 下一次月度计划生成任务将在 {sleep_seconds:.2f} 秒后运行 (北京时间 {next_month.strftime('%Y-%m-%d %H:%M:%S')})" ) - - # 等待直到下个月1号 await asyncio.sleep(sleep_seconds) - - # 先归档上个月的计划 last_month = (next_month - timedelta(days=1)).strftime("%Y-%m") - await self.monthly_plan_manager.archive_current_month_plans(last_month) - - # 生成新月份的计划 + await self.monthly_plan_manager.plan_manager.archive_current_month_plans(last_month) current_month = next_month.strftime("%Y-%m") logger.info(f" 到达月初,开始生成 {current_month} 的月度计划...") - await self.monthly_plan_manager._generate_monthly_plans_logic(current_month) - + await self.monthly_plan_manager.plan_manager._generate_monthly_plans_logic(current_month) except asyncio.CancelledError: logger.info(" 每月月度计划生成任务被取消。") break except Exception as e: logger.error(f" 每月月度计划生成任务发生未知错误: {e}") - # 发生错误后,等待1小时再重试,避免频繁失败 await asyncio.sleep(3600) -# 全局实例 monthly_plan_manager = MonthlyPlanManager() diff --git a/src/schedule/plan_manager.py b/src/schedule/plan_manager.py new file mode 100644 index 000000000..0fae5c381 --- /dev/null +++ b/src/schedule/plan_manager.py @@ -0,0 +1,105 @@ +# mmc/src/schedule/plan_manager.py + +from datetime import datetime +from typing import List, Optional + +from src.common.logger import get_logger +from src.config.config import global_config +from .database import ( + add_new_plans, + get_archived_plans_for_month, + archive_active_plans_for_month, + has_active_plans, + get_active_plans_for_month, + delete_plans_by_ids, + get_smart_plans_for_daily_schedule, +) +from .llm_generator import MonthlyPlanLLMGenerator + +logger = get_logger("plan_manager") + + +class PlanManager: + def __init__(self): + self.llm_generator = MonthlyPlanLLMGenerator() + self.generation_running = False + + async def ensure_and_generate_plans_if_needed(self, target_month: Optional[str] = None) -> bool: + if target_month is None: + target_month = datetime.now().strftime("%Y-%m") + + if not has_active_plans(target_month): + logger.info(f" {target_month} 没有任何有效的月度计划,将触发同步生成。") + generation_successful = await self._generate_monthly_plans_logic(target_month) + return generation_successful + else: + logger.info(f"{target_month} 已存在有效的月度计划。") + plans = get_active_plans_for_month(target_month) + max_plans = global_config.planning_system.max_plans_per_month + if len(plans) > max_plans: + logger.warning(f"当前月度计划数量 ({len(plans)}) 超出上限 ({max_plans}),将自动删除多余的计划。") + plans_to_delete = plans[: len(plans) - max_plans] + delete_ids = [p.id for p in plans_to_delete] + delete_plans_by_ids(delete_ids) # type: ignore + plans = get_active_plans_for_month(target_month) + + if plans: + plan_texts = "\n".join([f" {i + 1}. {plan.plan_text}" for i, plan in enumerate(plans)]) + logger.info(f"当前月度计划内容:\n{plan_texts}") + return True + + async def _generate_monthly_plans_logic(self, target_month: Optional[str] = None) -> bool: + if self.generation_running: + logger.info("月度计划生成任务已在运行中,跳过重复启动") + return False + + self.generation_running = True + try: + if target_month is None: + target_month = datetime.now().strftime("%Y-%m") + + logger.info(f"开始为 {target_month} 生成月度计划...") + if not global_config.planning_system.monthly_plan_enable: + logger.info(" 月度计划系统已禁用,跳过计划生成。") + return False + + last_month = self._get_previous_month(target_month) + archived_plans = get_archived_plans_for_month(last_month) + plans = await self.llm_generator.generate_plans_with_llm(target_month, archived_plans) + + if plans: + add_new_plans(plans, target_month) + logger.info(f"成功为 {target_month} 生成并保存了 {len(plans)} 条月度计划。") + return True + else: + logger.warning(f"未能为 {target_month} 生成有效的月度计划。") + return False + except Exception as e: + logger.error(f" 生成 {target_month} 月度计划时发生错误: {e}") + return False + finally: + self.generation_running = False + + def _get_previous_month(self, current_month: str) -> str: + try: + year, month = map(int, current_month.split("-")) + if month == 1: + return f"{year - 1}-12" + else: + return f"{year}-{month - 1:02d}" + except Exception: + return "1900-01" + + async def archive_current_month_plans(self, target_month: Optional[str] = None): + try: + if target_month is None: + target_month = datetime.now().strftime("%Y-%m") + logger.info(f" 开始归档 {target_month} 的活跃月度计划...") + archived_count = archive_active_plans_for_month(target_month) + logger.info(f" 成功归档了 {archived_count} 条 {target_month} 的月度计划。") + except Exception as e: + logger.error(f" 归档 {target_month} 月度计划时发生错误: {e}") + + def get_plans_for_schedule(self, month: str, max_count: int) -> List: + avoid_days = global_config.planning_system.avoid_repetition_days + return get_smart_plans_for_daily_schedule(month, max_count=max_count, avoid_days=avoid_days) \ No newline at end of file diff --git a/src/schedule/schedule_manager.py b/src/schedule/schedule_manager.py index 058f1193a..cb99f7915 100644 --- a/src/schedule/schedule_manager.py +++ b/src/schedule/schedule_manager.py @@ -1,142 +1,30 @@ import orjson import asyncio from datetime import datetime, time, timedelta -from typing import Optional, List, Dict, Any, TYPE_CHECKING -from lunar_python import Lunar -from pydantic import BaseModel, ValidationError, validator +from typing import Optional, List, Dict, Any from src.common.database.sqlalchemy_models import Schedule, get_db_session -from src.common.database.monthly_plan_db import ( - get_smart_plans_for_daily_schedule, - update_plan_usage, # 保留兼容性 -) -from src.config.config import global_config, model_config -from src.llm_models.utils_model import LLMRequest +from src.config.config import global_config from src.common.logger import get_logger -from json_repair import repair_json from src.manager.async_task_manager import AsyncTask, async_task_manager from ..chat.chat_loop.sleep_manager.schedule_bridge import schedule_sleep_bridge - -if TYPE_CHECKING: - from src.chat.chat_loop.sleep_manager.wakeup_manager import WakeUpManager - +from .database import update_plan_usage +from .llm_generator import ScheduleLLMGenerator +from .plan_manager import PlanManager +from .schemas import ScheduleData logger = get_logger("schedule_manager") -# 默认的日程生成指导原则 -DEFAULT_SCHEDULE_GUIDELINES = """ -我希望你每天都能过得充实而有趣。 -请确保你的日程里有学习新知识的时间,这是你成长的关键。 -但也不要忘记放松,可以看看视频、听听音乐或者玩玩游戏。 -晚上我希望你能多和朋友们交流,维系好彼此的关系。 -另外,请保证充足的休眠时间来处理和整合一天的数据。 -""" - - -class ScheduleItem(BaseModel): - """单个日程项的Pydantic模型""" - - time_range: str - activity: str - - @validator("time_range") - def validate_time_range(cls, v): - """验证时间范围格式""" - if not v or "-" not in v: - raise ValueError("时间范围必须包含'-'分隔符") - - try: - start_str, end_str = v.split("-", 1) - start_str = start_str.strip() - end_str = end_str.strip() - - # 验证时间格式 - datetime.strptime(start_str, "%H:%M") - datetime.strptime(end_str, "%H:%M") - - return v - except ValueError as e: - raise ValueError(f"时间格式无效,应为HH:MM-HH:MM格式: {e}") from e - - @validator("activity") - def validate_activity(cls, v): - """验证活动描述""" - if not v or not v.strip(): - raise ValueError("活动描述不能为空") - return v.strip() - - -class ScheduleData(BaseModel): - """完整日程数据的Pydantic模型""" - - schedule: List[ScheduleItem] - - @validator("schedule") - def validate_schedule_completeness(cls, v): - """验证日程是否覆盖24小时""" - if not v: - raise ValueError("日程不能为空") - - # 收集所有时间段 - time_ranges = [] - for item in v: - try: - start_str, end_str = item.time_range.split("-", 1) - start_time = datetime.strptime(start_str.strip(), "%H:%M").time() - end_time = datetime.strptime(end_str.strip(), "%H:%M").time() - time_ranges.append((start_time, end_time)) - except ValueError: - continue - - # 检查是否覆盖24小时 - if not cls._check_24_hour_coverage(time_ranges): - raise ValueError("日程必须覆盖完整的24小时") - - return v - - @staticmethod - def _check_24_hour_coverage(time_ranges: List[tuple]) -> bool: - """检查时间段是否覆盖24小时""" - if not time_ranges: - return False - - # 将时间转换为分钟数进行计算 - def time_to_minutes(t: time) -> int: - return t.hour * 60 + t.minute - - # 创建覆盖情况数组 (1440分钟 = 24小时) - covered = [False] * 1440 - - for start_time, end_time in time_ranges: - start_min = time_to_minutes(start_time) - end_min = time_to_minutes(end_time) - - if start_min <= end_min: - # 同一天内的时间段 - for i in range(start_min, end_min): - if i < 1440: - covered[i] = True - else: - # 跨天的时间段 - for i in range(start_min, 1440): - covered[i] = True - for i in range(0, end_min): - covered[i] = True - - # 检查是否所有分钟都被覆盖 - return all(covered) - class ScheduleManager: def __init__(self): self.today_schedule: Optional[List[Dict[str, Any]]] = None - self.llm = LLMRequest(model_set=model_config.model_task_config.schedule_generator, request_type="schedule") - self.max_retries = -1 # 无限重试,直到成功生成标准日程表 + self.llm_generator = ScheduleLLMGenerator() + self.plan_manager = PlanManager() self.daily_task_started = False - self.schedule_generation_running = False # 防止重复生成任务 + self.schedule_generation_running = False async def start_daily_schedule_generation(self): - """启动每日零点自动生成新日程的任务""" if not self.daily_task_started: logger.info("正在启动每日日程生成任务...") task = DailyScheduleGenerationTask(self) @@ -147,35 +35,20 @@ class ScheduleManager: logger.info("每日日程生成任务已在运行中。") async def load_or_generate_today_schedule(self): - # 检查是否启用日程管理功能 - if not global_config.schedule.enable: + if not global_config.planning_system.schedule_enable: logger.info("日程管理功能已禁用,跳过日程加载和生成。") return today_str = datetime.now().strftime("%Y-%m-%d") try: - with get_db_session() as session: - schedule_record = session.query(Schedule).filter(Schedule.date == today_str).first() - if schedule_record: - logger.info(f"从数据库加载今天的日程 ({today_str})。") - schedule_data = orjson.loads(str(schedule_record.schedule_data)) - if self._validate_schedule_with_pydantic(schedule_data): - self.today_schedule = schedule_data - schedule_sleep_bridge.update_today_schedule(self.today_schedule) # 更新桥接器中的日程 - schedule_str = f"已成功加载今天的日程 ({today_str}):\n" - if self.today_schedule: - for item in self.today_schedule: - schedule_str += ( - f" - {item.get('time_range', '未知时间')}: {item.get('activity', '未知活动')}\n" - ) - logger.info(schedule_str) - return # 成功加载,直接返回 - else: - logger.warning("数据库中的日程数据格式无效,将重新生成日程") - else: - logger.info(f"数据库中未找到今天的日程 ({today_str}),将调用 LLM 生成。") + schedule_data = self._load_schedule_from_db(today_str) + if schedule_data: + self.today_schedule = schedule_data + schedule_sleep_bridge.update_today_schedule(self.today_schedule) + self._log_loaded_schedule(today_str) + return - # 仅在需要时生成 + logger.info(f"数据库中未找到今天的日程 ({today_str}),将调用 LLM 生成。") await self.generate_and_save_schedule() except Exception as e: @@ -183,258 +56,107 @@ class ScheduleManager: logger.info("尝试生成日程作为备用方案...") await self.generate_and_save_schedule() + def _load_schedule_from_db(self, date_str: str) -> Optional[List[Dict[str, Any]]]: + with get_db_session() as session: + schedule_record = session.query(Schedule).filter(Schedule.date == date_str).first() + if schedule_record: + logger.info(f"从数据库加载今天的日程 ({date_str})。") + schedule_data = orjson.loads(str(schedule_record.schedule_data)) + if self._validate_schedule_with_pydantic(schedule_data): + return schedule_data + else: + logger.warning("数据库中的日程数据格式无效,将重新生成日程") + return None + + def _log_loaded_schedule(self, date_str: str): + schedule_str = f"已成功加载今天的日程 ({date_str}):\n" + if self.today_schedule: + for item in self.today_schedule: + schedule_str += f" - {item.get('time_range', '未知时间')}: {item.get('activity', '未知活动')}\n" + logger.info(schedule_str) + async def generate_and_save_schedule(self): - """将日程生成任务提交到后台执行""" if self.schedule_generation_running: logger.info("日程生成任务已在运行中,跳过重复启动") return - logger.info("检测到需要生成日程,已提交后台任务。") task = OnDemandScheduleGenerationTask(self) await async_task_manager.add_task(task) async def _async_generate_and_save_schedule(self): - """异步生成并保存日程的内部方法""" self.schedule_generation_running = True - try: - now = datetime.now() - today_str = now.strftime("%Y-%m-%d") - current_month_str = now.strftime("%Y-%m") - weekday = now.strftime("%A") + today_str = datetime.now().strftime("%Y-%m-%d") + current_month_str = datetime.now().strftime("%Y-%m") - # 新增:获取节日信息 - lunar = Lunar.fromDate(now) - festivals = lunar.getFestivals() - other_festivals = lunar.getOtherFestivals() - all_festivals = festivals + other_festivals + sampled_plans = [] + if global_config.planning_system.monthly_plan_enable: + await self.plan_manager.ensure_and_generate_plans_if_needed(current_month_str) + sampled_plans = self.plan_manager.get_plans_for_schedule(current_month_str, max_count=3) - festival_block = "" - if all_festivals: - festival_text = "、".join(all_festivals) - festival_block = f"**今天也是一个特殊的日子: {festival_text}!请在日程中考虑和庆祝这个节日。**" + schedule_data = await self.llm_generator.generate_schedule_with_llm(sampled_plans) - # 获取月度计划作为额外参考 - monthly_plans_block = "" - used_plan_ids = [] - if global_config.monthly_plan_system and global_config.monthly_plan_system.enable: - # 使用新的智能抽取逻辑 - avoid_days = getattr(global_config.monthly_plan_system, "avoid_repetition_days", 7) - # 使用新的智能抽取逻辑 - avoid_days = getattr(global_config.monthly_plan_system, "avoid_repetition_days", 7) - sampled_plans = get_smart_plans_for_daily_schedule( - current_month_str, max_count=3, avoid_days=avoid_days - ) - - # 如果计划耗尽,则触发补充生成 - if not sampled_plans: - logger.info("可用的月度计划已耗尽或不足,触发后台补充生成...") - from mmc.src.schedule.monthly_plan_manager import monthly_plan_manager - - # 等待月度计划生成完成 - await monthly_plan_manager.ensure_and_generate_plans_if_needed(current_month_str) - - # 重新获取月度计划 - sampled_plans = get_smart_plans_for_daily_schedule( - current_month_str, max_count=3, avoid_days=avoid_days - ) - logger.info("月度计划补充生成完毕,继续日程生成任务。") + if schedule_data: + self._save_schedule_to_db(today_str, schedule_data) + self.today_schedule = schedule_data + schedule_sleep_bridge.update_today_schedule(self.today_schedule) + self._log_generated_schedule(today_str, schedule_data) if sampled_plans: - plan_texts = "\n".join([f"- {plan.plan_text}" for plan in sampled_plans]) - monthly_plans_block = f""" -**我这个月的一些小目标/计划 (请在今天的日程中适当体现)**: -{plan_texts} -""" - - guidelines = global_config.schedule.guidelines or DEFAULT_SCHEDULE_GUIDELINES - personality = global_config.personality.personality_core - personality_side = global_config.personality.personality_side - - base_prompt = f""" -我,{global_config.bot.nickname},需要为自己规划一份今天({today_str},星期{weekday})的详细日程安排。 -{festival_block} -**关于我**: -- **核心人设**: {personality} -- **具体习惯与兴趣**: -{personality_side} -{monthly_plans_block} -**我今天的规划原则**: -{guidelines} - -**重要要求**: -1. 必须返回一个完整的、有效的JSON数组格式 -2. 数组中的每个对象都必须包含 "time_range" 和 "activity" 两个键 -3. 时间范围必须覆盖全部24小时,不能有遗漏 -4. time_range格式必须为 "HH:MM-HH:MM" (24小时制) -5. 相邻的时间段必须连续,不能有间隙 -6. 不要包含任何JSON以外的解释性文字或代码块标记 -**示例**: -[ - {{"time_range": "00:00-07:00", "activity": "进入梦乡,处理数据"}}, - {{"time_range": "07:00-08:00", "activity": "起床伸个懒腰,看看今天有什么新闻"}}, - {{"time_range": "08:00-09:00", "activity": "享用早餐,规划今天的任务"}}, - {{"time_range": "09:00-23:30", "activity": "其他活动"}}, - {{"time_range": "23:30-00:00", "activity": "准备休眠"}} -] - -请你扮演我,以我的身份和口吻,为我生成一份完整的24小时日程表。 -""" - - # 无限重试直到生成成功的标准日程表 - attempt = 0 - while True: - attempt += 1 - try: - logger.info(f"正在生成日程 (第 {attempt} 次尝试)") - - # 构建当前尝试的prompt,增加压力提示 - prompt = base_prompt - if attempt > 1: - failure_hint = f""" - -**重要提醒 (第{attempt}次尝试)**: -- 前面{attempt - 1}次生成都失败了,请务必严格按照要求生成完整的24小时日程 -- 确保JSON格式正确,所有时间段连续覆盖24小时 -- 时间格式必须为HH:MM-HH:MM,不能有时间间隙或重叠 -- 不要输出任何解释文字,只输出纯JSON数组 -- 确保输出完整,不要被截断 -""" - prompt += failure_hint - - response, _ = await self.llm.generate_response_async(prompt) - - # 尝试解析和验证JSON(项目内置的反截断机制会自动处理截断问题) - schedule_data = orjson.loads(repair_json(response)) - - # 使用Pydantic验证生成的日程数据 - if self._validate_schedule_with_pydantic(schedule_data): - # 验证通过,保存到数据库 - with get_db_session() as session: - # 检查是否已存在今天的日程 - existing_schedule = session.query(Schedule).filter(Schedule.date == today_str).first() - if existing_schedule: - # 更新现有日程 - session.query(Schedule).filter(Schedule.date == today_str).update( - { - Schedule.schedule_data: orjson.dumps(schedule_data).decode("utf-8"), - Schedule.updated_at: datetime.now(), - } - ) - else: - # 创建新日程 - new_schedule = Schedule( - date=today_str, schedule_data=orjson.dumps(schedule_data).decode("utf-8") - ) - session.add(new_schedule) - session.commit() - - # 美化输出 - schedule_str = f"✅ 经过 {attempt} 次尝试,成功生成并保存今天的日程 ({today_str}):\n" - for item in schedule_data: - schedule_str += ( - f" - {item.get('time_range', '未知时间')}: {item.get('activity', '未知活动')}\n" - ) - logger.info(schedule_str) - - self.today_schedule = schedule_data - schedule_sleep_bridge.update_today_schedule(self.today_schedule) - - # 成功生成日程后,更新使用过的月度计划的统计信息 - if used_plan_ids and global_config.monthly_plan_system: - logger.info(f"更新使用过的月度计划 {used_plan_ids} 的统计信息。") - update_plan_usage(used_plan_ids, today_str) # type: ignore - - # 成功生成,退出无限循环 - break - - else: - logger.warning(f"第 {attempt} 次生成的日程验证失败,继续重试...") - # 添加短暂延迟,避免过于频繁的请求 - await asyncio.sleep(2) - - except Exception as e: - logger.error(f"第 {attempt} 次生成日程失败: {e}") - logger.info("继续重试...") - # 添加短暂延迟,避免过于频繁的请求 - await asyncio.sleep(3) - + used_plan_ids = [plan.id for plan in sampled_plans] + logger.info(f"更新使用过的月度计划 {used_plan_ids} 的统计信息。") + update_plan_usage(used_plan_ids, today_str) finally: self.schedule_generation_running = False logger.info("日程生成任务结束") + def _save_schedule_to_db(self, date_str: str, schedule_data: List[Dict[str, Any]]): + with get_db_session() as session: + schedule_json = orjson.dumps(schedule_data).decode("utf-8") + existing_schedule = session.query(Schedule).filter(Schedule.date == date_str).first() + if existing_schedule: + session.query(Schedule).filter(Schedule.date == date_str).update( + {Schedule.schedule_data: schedule_json, Schedule.updated_at: datetime.now()} + ) + else: + new_schedule = Schedule(date=date_str, schedule_data=schedule_json) + session.add(new_schedule) + session.commit() + + def _log_generated_schedule(self, date_str: str, schedule_data: List[Dict[str, Any]]): + schedule_str = f"✅ 成功生成并保存今天的日程 ({date_str}):\n" + for item in schedule_data: + schedule_str += f" - {item.get('time_range', '未知时间')}: {item.get('activity', '未知活动')}\n" + logger.info(schedule_str) + def get_current_activity(self) -> Optional[str]: - # 检查是否启用日程管理功能 - if not global_config.schedule.enable: + if not global_config.planning_system.schedule_enable or not self.today_schedule: return None - - if not self.today_schedule: - return None - now = datetime.now().time() for event in self.today_schedule: try: time_range = event.get("time_range") activity = event.get("activity") - if not time_range or not activity: - logger.warning(f"日程事件缺少必要字段: {event}") continue - start_str, end_str = time_range.split("-") start_time = datetime.strptime(start_str.strip(), "%H:%M").time() end_time = datetime.strptime(end_str.strip(), "%H:%M").time() - - if start_time <= end_time: - if start_time <= now < end_time: - return activity - else: # 跨天事件 - if now >= start_time or now < end_time: - return activity + if (start_time <= now < end_time) or (end_time < start_time and (now >= start_time or now < end_time)): + return activity except (ValueError, KeyError, AttributeError) as e: logger.warning(f"解析日程事件失败: {event}, 错误: {e}") - continue return None def _validate_schedule_with_pydantic(self, schedule_data) -> bool: - """使用Pydantic验证日程数据格式和完整性""" try: - # 尝试用Pydantic模型验证 ScheduleData(schedule=schedule_data) - logger.info("日程数据Pydantic验证通过") return True - except ValidationError as e: - logger.warning(f"日程数据Pydantic验证失败: {e}") + except Exception: return False - except Exception as e: - logger.error(f"日程数据验证时发生异常: {e}") - return False - - def _validate_schedule_data(self, schedule_data) -> bool: - """保留原有的基础验证方法作为备用""" - if not isinstance(schedule_data, list): - logger.warning("日程数据不是列表格式") - return False - - for item in schedule_data: - if not isinstance(item, dict): - logger.warning(f"日程项不是字典格式: {item}") - return False - - if "time_range" not in item or "activity" not in item: - logger.warning(f"日程项缺少必要字段 (time_range 或 activity): {item}") - return False - - if not isinstance(item["time_range"], str) or not isinstance(item["activity"], str): - logger.warning(f"日程项字段类型不正确: {item}") - return False - - return True class OnDemandScheduleGenerationTask(AsyncTask): - """按需生成日程的后台任务""" - def __init__(self, schedule_manager: "ScheduleManager"): task_name = f"OnDemandScheduleGenerationTask-{datetime.now().strftime('%Y%m%d%H%M%S')}" super().__init__(task_name=task_name) @@ -447,8 +169,6 @@ class OnDemandScheduleGenerationTask(AsyncTask): class DailyScheduleGenerationTask(AsyncTask): - """每日零点自动生成新日程的任务""" - def __init__(self, schedule_manager: "ScheduleManager"): super().__init__(task_name="DailyScheduleGenerationTask") self.schedule_manager = schedule_manager @@ -456,29 +176,21 @@ class DailyScheduleGenerationTask(AsyncTask): async def run(self): while True: try: - # 1. 计算到下一个零点的时间 now = datetime.now() tomorrow = now.date() + timedelta(days=1) midnight = datetime.combine(tomorrow, time.min) sleep_seconds = (midnight - now).total_seconds() - logger.info( f"下一次日程生成任务将在 {sleep_seconds:.2f} 秒后运行 (北京时间 {midnight.strftime('%Y-%m-%d %H:%M:%S')})" ) - - # 2. 等待直到零点 await asyncio.sleep(sleep_seconds) - - # 3. 执行日程生成 logger.info("到达每日零点,开始生成新的一天日程...") await self.schedule_manager._async_generate_and_save_schedule() - except asyncio.CancelledError: logger.info("每日日程生成任务被取消。") break except Exception as e: logger.error(f"每日日程生成任务发生未知错误: {e}") - # 发生错误后,等待5分钟再重试,避免频繁失败 await asyncio.sleep(300) diff --git a/src/schedule/schemas.py b/src/schedule/schemas.py new file mode 100644 index 000000000..5eb7c003a --- /dev/null +++ b/src/schedule/schemas.py @@ -0,0 +1,99 @@ +# mmc/src/schedule/schemas.py + +from datetime import datetime, time +from typing import List +from pydantic import BaseModel, validator + + +class ScheduleItem(BaseModel): + """单个日程项的Pydantic模型""" + + time_range: str + activity: str + + @validator("time_range") + def validate_time_range(cls, v): + """验证时间范围格式""" + if not v or "-" not in v: + raise ValueError("时间范围必须包含'-'分隔符") + + try: + start_str, end_str = v.split("-", 1) + start_str = start_str.strip() + end_str = end_str.strip() + + # 验证时间格式 + datetime.strptime(start_str, "%H:%M") + datetime.strptime(end_str, "%H:%M") + + return v + except ValueError as e: + raise ValueError(f"时间格式无效,应为HH:MM-HH:MM格式: {e}") from e + + @validator("activity") + def validate_activity(cls, v): + """验证活动描述""" + if not v or not v.strip(): + raise ValueError("活动描述不能为空") + return v.strip() + + +class ScheduleData(BaseModel): + """完整日程数据的Pydantic模型""" + + schedule: List[ScheduleItem] + + @validator("schedule") + def validate_schedule_completeness(cls, v): + """验证日程是否覆盖24小时""" + if not v: + raise ValueError("日程不能为空") + + # 收集所有时间段 + time_ranges = [] + for item in v: + try: + start_str, end_str = item.time_range.split("-", 1) + start_time = datetime.strptime(start_str.strip(), "%H:%M").time() + end_time = datetime.strptime(end_str.strip(), "%H:%M").time() + time_ranges.append((start_time, end_time)) + except ValueError: + continue + + # 检查是否覆盖24小时 + if not cls._check_24_hour_coverage(time_ranges): + raise ValueError("日程必须覆盖完整的24小时") + + return v + + @staticmethod + def _check_24_hour_coverage(time_ranges: List[tuple]) -> bool: + """检查时间段是否覆盖24小时""" + if not time_ranges: + return False + + # 将时间转换为分钟数进行计算 + def time_to_minutes(t: time) -> int: + return t.hour * 60 + t.minute + + # 创建覆盖情况数组 (1440分钟 = 24小时) + covered = [False] * 1440 + + for start_time, end_time in time_ranges: + start_min = time_to_minutes(start_time) + end_min = time_to_minutes(end_time) + + if start_min <= end_min: + # 同一天内的时间段 + for i in range(start_min, end_min): + if i < 1440: + covered[i] = True + else: + # 跨天的时间段 + for i in range(start_min, 1440): + covered[i] = True + for i in range(0, end_min): + covered[i] = True + + # 检查是否所有分钟都被覆盖 + return all(covered) \ No newline at end of file diff --git a/template/bot_config_template.toml b/template/bot_config_template.toml index dcc234cc1..152da00dc 100644 --- a/template/bot_config_template.toml +++ b/template/bot_config_template.toml @@ -363,16 +363,28 @@ use_wss = false # 是否使用WSS安全连接,只支持ws模式 cert_file = "" # SSL证书文件路径,仅在use_wss=true时有效 key_file = "" # SSL密钥文件路径,仅在use_wss=true时有效 -[schedule] #日程管理 -enable = false # 是否启用日程管理功能 -# 日程生成指导原则,如果不设置则使用默认原则 -guidelines = """ +[planning_system] # 规划系统配置 +# --- 日程生成 --- +schedule_enable = true # 是否启用每日日程生成功能 +schedule_guidelines = """ 我希望你每天都能过得充实而有趣。 请确保你的日程里有学习新知识的时间,这是你成长的关键。 但也不要忘记放松,可以看看视频、听听音乐或者玩玩游戏。 晚上我希望你能多和朋友们交流,维系好彼此的关系。 另外,请保证充足的休眠时间来处理和整合一天的数据。 """ +# --- 月度计划 --- +monthly_plan_enable = false # 是否启用月度计划系统 +monthly_plan_guidelines = """ +我希望你能为自己制定一些有意义的月度小目标和计划。 +这些计划应该涵盖学习、娱乐、社交、个人成长等各个方面。 +每个计划都应该是具体可行的,能够在一个月内通过日常活动逐步实现。 +请确保计划既有挑战性又不会过于繁重,保持生活的平衡和乐趣。 +""" +max_plans_per_month = 10 # 每月最多生成的计划数量 +avoid_repetition_days = 7 # 避免在多少天内重复使用同一个月度计划 +completion_threshold = 3 # 一个月度计划被使用多少次后算作完成 + [video_analysis] # 视频分析配置 enable = true # 是否启用视频分析功能 analysis_mode = "batch_frames" # 分析模式:"frame_by_frame"(逐帧分析,非常慢)、"batch_frames"(批量分析,推荐)或 "auto"(自动选择) @@ -420,26 +432,6 @@ exa_api_keys = ["None"]# EXA API密钥列表,支持轮询机制 enabled_engines = ["ddg"] # 启用的搜索引擎列表,可选: "exa", "tavily", "ddg","bing" search_strategy = "single" # 搜索策略: "single"(使用第一个可用引擎), "parallel"(并行使用所有启用的引擎), "fallback"(按顺序尝试,失败则尝试下一个) -# ---------------------------------------------------------------- -# 月度计划系统设置 (Monthly Plan System Settings) -# ---------------------------------------------------------------- -[monthly_plan_system] -# 是否启用本功能 -enable = true -# 每个月允许存在的最大计划数量 -max_plans_per_month = 20 -# 计划使用多少次后自动标记为已完成 -completion_threshold = 3 -# 多少天内不重复抽取同一个计划 -avoid_repetition_days = 7 -# 月度计划生成的指导原则(可选,如果不设置则使用默认原则) -guidelines = """ -我希望你能为自己制定一些有意义的月度小目标和计划。 -这些计划应该涵盖学习、娱乐、社交、个人成长等各个方面。 -每个计划都应该是具体可行的,能够在一个月内通过日常活动逐步实现。 -请确保计划既有挑战性又不会过于繁重,保持生活的平衡和乐趣。 -""" - [sleep_system] enable = false #"是否启用睡眠系统" wakeup_threshold = 15.0 #唤醒阈值,达到此值时会被唤醒"