"""Agent 工具集 - 日程相关""" from __future__ import annotations from datetime import date, datetime from zoneinfo import ZoneInfo from langchain_core.tools import tool from sqlalchemy import select from app.agents.context import get_current_user from app.agents.tools.async_bridge import run_async from app.database import async_session from app.models.goal import Goal, GoalStatus from app.models.reminder import Reminder from app.models.task import Task, TaskPriority, TaskStatus from app.models.todo import DailyTodo, TodoSource def _run_async(coro, timeout: int = 30): return run_async(coro, timeout=timeout) def _parse_date(value: str | None) -> date: if not value: return date.today() return date.fromisoformat(value) def _parse_datetime(value: str) -> datetime: normalized = value.strip().replace("Z", "+00:00") return datetime.fromisoformat(normalized) def _parse_datetime_with_timezone(value: str, time_zone: str | None) -> datetime: """Parse an ISO datetime and return a tz-naive datetime in the intended local time. - If value includes an offset/Z, it will be converted to `time_zone` when provided. - If value is naive and `time_zone` is provided, it is interpreted in that zone. """ parsed = _parse_datetime(value) tz = (time_zone or "").strip() if parsed.tzinfo is None: if tz: parsed = parsed.replace(tzinfo=ZoneInfo(tz)) return parsed.replace(tzinfo=None) if tz: parsed = parsed.astimezone(ZoneInfo(tz)) return parsed.replace(tzinfo=None) def _normalize_title(title: str | None, content: str | None) -> str: resolved = (title or content or "").strip() if not resolved: raise ValueError("title 不能为空") return resolved def _normalize_schedule_due_date(due_date: str | None, date_value: str | None) -> str | None: resolved = (due_date or date_value or "").strip() if not resolved: return None if "T" in resolved: return resolved return f"{resolved}T09:00:00" def _format_summary(target_date: date, todos: list[DailyTodo], tasks: list[Task], reminders: list[Reminder], goals: list[Goal]) -> str: lines = [f"日期: {target_date.isoformat()}"] if todos: lines.append("待办:") lines.extend(f"- {item.title} | 完成:{'是' if item.is_completed else '否'}" for item in todos) else: lines.append("待办: 无") if tasks: lines.append("任务:") lines.extend( f"- {item.title} | 状态:{item.status.value if hasattr(item.status, 'value') else item.status} | 优先级:{item.priority.value if hasattr(item.priority, 'value') else item.priority} | 截止:{item.due_date.isoformat() if item.due_date else '无'}" for item in tasks ) else: lines.append("任务: 无") if reminders: lines.append("提醒:") lines.extend(f"- {item.title} | 时间:{item.reminder_at.isoformat()}" for item in reminders) else: lines.append("提醒: 无") if goals: lines.append("目标:") lines.extend( f"- {item.title} | 状态:{item.status.value if hasattr(item.status, 'value') else item.status}" for item in goals ) else: lines.append("目标: 无") return "\n".join(lines) @tool def get_schedule_day(target_date: str | None = None) -> str: """获取指定日期的 todo/task/reminder/goal 聚合信息。target_date 格式 YYYY-MM-DD,默认今天。""" uid = get_current_user() parsed_date = _parse_date(target_date) date_key = parsed_date.isoformat() start_dt = datetime.combine(parsed_date, datetime.min.time()) end_dt = datetime.combine(parsed_date, datetime.max.time()) async def _get(): async with async_session() as db: todos = ( await db.execute( select(DailyTodo) .where(DailyTodo.user_id == uid, DailyTodo.todo_date == date_key) .order_by(DailyTodo.created_at.desc()) ) ).scalars().all() tasks = ( await db.execute( select(Task) .where( Task.user_id == uid, Task.due_date.is_not(None), Task.due_date >= start_dt, Task.due_date <= end_dt, ) .order_by(Task.created_at.desc()) ) ).scalars().all() reminders = ( await db.execute( select(Reminder) .where( Reminder.user_id == uid, Reminder.reminder_at >= start_dt, Reminder.reminder_at <= end_dt, ) .order_by(Reminder.reminder_at.asc(), Reminder.created_at.asc()) ) ).scalars().all() goals = ( await db.execute( select(Goal) .where(Goal.user_id == uid, Goal.goal_date == date_key) .order_by(Goal.created_at.desc()) ) ).scalars().all() return _format_summary(parsed_date, todos, tasks, reminders, goals) try: return _run_async(_get()) except Exception as exc: return f"获取日程失败: {exc}" @tool def create_todo(title: str, todo_date: str | None = None) -> str: """创建指定日期的待办。todo_date 格式 YYYY-MM-DD,默认今天。""" uid = get_current_user() parsed_date = _parse_date(todo_date) async def _create(): async with async_session() as db: todo = DailyTodo( user_id=uid, title=title, source=TodoSource.AI_CHAT, todo_date=parsed_date.isoformat(), ) db.add(todo) await db.commit() await db.refresh(todo) return f"TODO创建成功: [{todo.id[:8]}] {todo.title} @ {todo.todo_date}" try: return _run_async(_create()) except Exception as exc: return f"创建TODO失败: {exc}" @tool def create_schedule_task( title: str = "", description: str = "", priority: str = "medium", due_date: str | None = None, content: str = "", date: str | None = None, ) -> str: """创建任务。priority 支持 low/medium/high/urgent;due_date 使用 ISO datetime。兼容 content/date 别名。""" uid = get_current_user() resolved_title = _normalize_title(title, content) resolved_due_date = _normalize_schedule_due_date(due_date, date) async def _create(): async with async_session() as db: task = Task( user_id=uid, title=resolved_title, description=description or content or None, priority=TaskPriority(priority), due_date=_parse_datetime(resolved_due_date) if resolved_due_date else None, status=TaskStatus.TODO, ) db.add(task) await db.commit() await db.refresh(task) due_label = task.due_date.isoformat() if task.due_date else "无截止时间" return f"任务创建成功: [{task.id[:8]}] {task.title} | 优先级:{task.priority.value} | 截止:{due_label}" try: return _run_async(_create()) except Exception as exc: return f"创建任务失败: {exc}" @tool def create_reminder( title: str = "", reminder_at: str | None = None, note: str = "", description: str = "", datetime: str = "", at: str = "", remind_at: str = "", content: str = "", time_zone: str = "", timezone: str = "", time: str = "", ) -> str: """创建提醒。reminder_at 使用 ISO datetime。兼容 description/datetime/at/remind_at/time_zone 别名。""" uid = get_current_user() try: resolved_title = (title or content or "").strip() if not resolved_title: raise ValueError("title 不能为空") resolved_at = ((reminder_at or datetime or at or remind_at or time or "").strip()) if not resolved_at: raise ValueError("reminder_at 不能为空") resolved_note = (note or description or "").strip() async def _create(): async with async_session() as db: tz = (time_zone or timezone or "").strip() reminder = Reminder( user_id=uid, title=resolved_title, note=resolved_note or None, reminder_at=_parse_datetime_with_timezone(resolved_at, tz), ) db.add(reminder) await db.commit() await db.refresh(reminder) return f"提醒创建成功: [{reminder.id[:8]}] {reminder.title} @ {reminder.reminder_at.isoformat()}" return _run_async(_create()) except Exception as exc: return f"创建提醒失败: {exc}" @tool def create_goal(title: str, goal_date: str | None = None, note: str = "", status: str = "active") -> str: """创建指定日期目标。goal_date 格式 YYYY-MM-DD,默认今天;status 支持 active/done/archived。""" uid = get_current_user() parsed_date = _parse_date(goal_date) async def _create(): async with async_session() as db: goal = Goal( user_id=uid, title=title, note=note or None, goal_date=parsed_date.isoformat(), status=GoalStatus(status), ) db.add(goal) await db.commit() await db.refresh(goal) return f"目标创建成功: [{goal.id[:8]}] {goal.title} @ {goal.goal_date}" try: return _run_async(_create()) except Exception as exc: return f"创建目标失败: {exc}" __all__ = [ "get_schedule_day", "create_todo", "create_schedule_task", "create_reminder", "create_goal", ]