Files
Mofox-Core/src/schedule/llm_generator.py
2025-11-19 23:13:20 +08:00

232 lines
9.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.

# mmc/src/schedule/llm_generator.py
import asyncio
from datetime import datetime
from typing import Any
import orjson
from json_repair import repair_json
from lunar_python import Lunar
from src.common.database.sqlalchemy_models import MonthlyPlan
from src.common.logger import get_logger
from src.config.config import global_config, model_config
from src.llm_models.utils_model import LLMRequest
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]) -> list[dict[str, Any]] | None:
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小时日程表。
"""
max_retries = 3
for attempt in range(1, max_retries + 1):
try:
logger.info(f"正在生成日程 (第 {attempt}/{max_retries} 次尝试)")
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} 次生成的日程验证失败,继续重试...")
except Exception as e:
logger.error(f"{attempt} 次生成日程失败: {e}")
if attempt < max_retries:
logger.info("2秒后继续重试...")
await asyncio.sleep(2)
logger.error("所有尝试都失败,无法生成日程,将会在下次启动时自动重试")
return None
@staticmethod
def _validate_schedule_with_pydantic(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 []
@staticmethod
def _parse_plans_response(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 []