added reminder

This commit is contained in:
2026-06-11 11:04:22 +03:00
parent 363aca293a
commit f7cc238308
22 changed files with 1265 additions and 2 deletions
+2 -1
View File
@@ -1,6 +1,6 @@
from fastapi import APIRouter
from app.api.routes import character, chat, fitness, health, homelab, media, memory, pomodoro, projects, shopping, webhooks
from app.api.routes import character, chat, fitness, health, homelab, media, memory, pomodoro, projects, reminders, shopping, webhooks
api_router = APIRouter(prefix="/api/v1")
api_router.include_router(health.router, tags=["health"])
@@ -12,5 +12,6 @@ api_router.include_router(projects.router, tags=["projects"])
api_router.include_router(memory.router, tags=["memory"])
api_router.include_router(fitness.router, tags=["fitness"])
api_router.include_router(shopping.router, prefix="/shopping", tags=["shopping"])
api_router.include_router(reminders.router, prefix="/reminders", tags=["reminders"])
api_router.include_router(webhooks.router, tags=["webhooks"])
api_router.include_router(media.router, tags=["media"])
+124
View File
@@ -0,0 +1,124 @@
from datetime import datetime, timezone
from typing import Any
from zoneinfo import ZoneInfo
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, Field
from sqlalchemy.orm import Session
from app.db.base import get_db
from app.homelab.context import resolve_timezone
from app.reminders.service import RemindersService
router = APIRouter()
class ReminderCreate(BaseModel):
title: str = Field(min_length=1, max_length=255)
due_at: str = Field(description="ISO datetime, например 2027-05-12T12:16:00")
notes: str = ""
all_day: bool = False
recurrence: str = "none"
class ReminderUpdate(BaseModel):
title: str | None = Field(default=None, min_length=1, max_length=255)
due_at: str | None = None
notes: str | None = None
all_day: bool | None = None
recurrence: str | None = None
enabled: bool | None = None
@router.get("")
def get_snapshot(db: Session = Depends(get_db)) -> dict[str, Any]:
return RemindersService(db).snapshot()
@router.get("/upcoming")
def list_upcoming(
limit: int = Query(30, ge=1, le=100),
db: Session = Depends(get_db),
) -> list[dict[str, Any]]:
return RemindersService(db).list_upcoming(limit=limit)
@router.get("/calendar")
def calendar(
year: int = Query(..., ge=2000, le=2100),
month: int = Query(..., ge=1, le=12),
db: Session = Depends(get_db),
) -> dict[str, Any]:
tz_name = resolve_timezone(db)
try:
tz = ZoneInfo(tz_name)
except Exception:
tz = ZoneInfo("Europe/Moscow")
start = datetime(year, month, 1, tzinfo=tz)
if month == 12:
end = datetime(year + 1, 1, 1, tzinfo=tz)
else:
end = datetime(year, month + 1, 1, tzinfo=tz)
service = RemindersService(db)
items = service.list_in_range(
date_from=start.astimezone(timezone.utc),
date_to=end.astimezone(timezone.utc),
)
return {
"year": year,
"month": month,
"timezone": tz_name,
"reminders": items,
}
@router.post("")
def create_reminder(payload: ReminderCreate, db: Session = Depends(get_db)) -> dict[str, Any]:
try:
return RemindersService(db).create(
title=payload.title,
due_at=payload.due_at,
notes=payload.notes,
all_day=payload.all_day,
recurrence=payload.recurrence,
)
except ValueError as exc:
raise HTTPException(status_code=400, detail=str(exc)) from exc
@router.patch("/{reminder_id}")
def update_reminder(
reminder_id: int,
payload: ReminderUpdate,
db: Session = Depends(get_db),
) -> dict[str, Any]:
try:
return RemindersService(db).update(
reminder_id,
title=payload.title,
due_at=payload.due_at,
notes=payload.notes,
all_day=payload.all_day,
recurrence=payload.recurrence,
enabled=payload.enabled,
)
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@router.delete("/{reminder_id}")
def delete_reminder(reminder_id: int, db: Session = Depends(get_db)) -> dict[str, Any]:
try:
return RemindersService(db).delete(reminder_id)
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
@router.post("/{reminder_id}/complete")
def complete_reminder(reminder_id: int, db: Session = Depends(get_db)) -> dict[str, Any]:
try:
return RemindersService(db).complete(reminder_id)
except ValueError as exc:
raise HTTPException(status_code=404, detail=str(exc)) from exc
+3
View File
@@ -25,6 +25,9 @@ TOOLS_INSTRUCTIONS = """
- Картинки: generate_image — «нарисуй себя» → draw_self=true; иначе scene_description на английском (booru-теги). Внешность из карточки персонажа. Не злоупотребляй.
- Покупки: list_shopping_lists, create_shopping_list, add_shopping_items, check_shopping_item, remove_shopping_item, delete_shopping_list.
- «Добавь в список покупок» → add_shopping_items (list_name + товары). «Что купить» → list_shopping_lists. Не выдумывай списки.
- Напоминания: list_reminders, create_reminder, update_reminder, delete_reminder, complete_reminder.
- «Напомни через 15 минут», «завтра утром», «12 мая 2027 в 12:16» → create_reminder с due_at в ISO (часовой пояс из [Текущее время]).
- Относительное время считай от «Сейчас» в контексте. «Утром» ≈ 09:00, «вечером» ≈ 19:00, если не уточнено иначе.
""".strip()
DEFAULT_CARD: dict[str, Any] = {
+28
View File
@@ -69,6 +69,14 @@ FITNESS_TOOL_NAMES = frozenset({
})
# Не засорять чат служебными ответами
REMINDER_TOOL_NAMES = frozenset({
"list_reminders",
"create_reminder",
"update_reminder",
"delete_reminder",
"complete_reminder",
})
SHOPPING_TOOL_NAMES = frozenset({
"list_shopping_lists",
"create_shopping_list",
@@ -88,6 +96,7 @@ TOOLS_SKIP_CHAT_NOTICE = frozenset({
"get_weather",
"get_morning_briefing",
"list_shopping_lists",
"list_reminders",
})
@@ -109,6 +118,8 @@ def format_tool_notice(tool_name: str, raw_result: str) -> str | None:
prefix = "💪"
elif tool_name in SHOPPING_TOOL_NAMES:
prefix = "🛒"
elif tool_name in REMINDER_TOOL_NAMES:
prefix = "📅"
else:
prefix = "📋"
return f"{prefix} {data['error']}"
@@ -231,6 +242,23 @@ def format_tool_notice(tool_name: str, raw_result: str) -> str | None:
if tool_name == "delete_shopping_list" and data.get("ok"):
return f"🛒 **Список удалён** · «{data.get('name')}»"
if tool_name == "create_reminder" and data.get("ok"):
r = data.get("reminder") or {}
rec = r.get("recurrence", "none")
rec_label = f" · повтор {rec}" if rec and rec != "none" else ""
return f"📅 **Напоминание создано** · {r.get('title')} · {r.get('due_at_local')}{rec_label}"
if tool_name == "update_reminder" and data.get("ok"):
r = data.get("reminder") or {}
return f"📅 **Напоминание обновлено** · #{r.get('id')} {r.get('title')}"
if tool_name == "delete_reminder" and data.get("ok"):
return f"📅 **Напоминание удалено** · «{data.get('title')}»"
if tool_name == "complete_reminder" and data.get("ok"):
r = data.get("reminder") or {}
return f"📅 **Готово** · {r.get('title')}"
return None
+3
View File
@@ -28,6 +28,7 @@ from app.memory.context import (
)
from app.memory.extract import extract_after_turn
from app.projects.context import format_projects_context, get_projects_snapshot
from app.reminders.context import format_reminders_context, get_reminders_snapshot
from app.shopping.context import format_shopping_context, get_shopping_snapshot
from app.db.models import ChatSession, Message
from app.llm.client import LLMClient
@@ -99,6 +100,7 @@ class ChatService:
memory_snapshot = get_memory_snapshot(self.db, session_id)
fitness_snapshot = get_fitness_snapshot(self.db)
shopping_snapshot = get_shopping_snapshot(self.db)
reminders_snapshot = get_reminders_snapshot(self.db)
projects_snapshot = get_projects_snapshot(self.db)
return (
f"{self.character.get_system_prompt()}\n\n"
@@ -106,6 +108,7 @@ class ChatService:
f"{format_memory_context(memory_snapshot)}\n\n"
f"{format_fitness_context(fitness_snapshot)}\n\n"
f"{format_shopping_context(shopping_snapshot)}\n\n"
f"{format_reminders_context(reminders_snapshot)}\n\n"
f"{format_weather_snapshot()}\n\n"
f"{format_pomodoro_context(status)}\n\n"
f"{format_projects_context(projects_snapshot)}"
+1
View File
@@ -45,6 +45,7 @@ class Settings(BaseSettings):
wger_base_url: str = "https://wger.de/api/v2"
openfoodfacts_base_url: str = "https://world.openfoodfacts.org"
fitness_reminders_enabled: bool = True
reminders_enabled: bool = True
openmeteo_base_url: str = "http://192.168.1.109:8085"
weather_lat: float = 59.9343
+19
View File
@@ -251,6 +251,25 @@ class ShoppingListItem(Base):
shopping_list: Mapped["ShoppingList"] = relationship(back_populates="items")
class Reminder(Base):
__tablename__ = "reminders"
id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True)
title: Mapped[str] = mapped_column(String(255))
notes: Mapped[str] = mapped_column(Text, default="")
due_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), index=True)
all_day: Mapped[bool] = mapped_column(Boolean, default=False)
recurrence: Mapped[str] = mapped_column(String(16), default="none")
enabled: Mapped[bool] = mapped_column(Boolean, default=True, index=True)
last_fired_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
completed_at: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True)
timezone: Mapped[str] = mapped_column(String(64), default="Europe/Moscow")
created_at: Mapped[datetime] = mapped_column(DateTime(timezone=True), server_default=func.now())
updated_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True), server_default=func.now(), onupdate=func.now()
)
class AssistantState(Base):
__tablename__ = "assistant_state"
+5
View File
@@ -10,6 +10,7 @@ from app.db.base import init_db
from app.fitness.watcher import fitness_watcher_loop
from app.homelab.watcher import homelab_watcher_loop
from app.pomodoro.watcher import pomodoro_watcher_loop
from app.reminders.watcher import reminders_watcher_loop
@asynccontextmanager
@@ -18,16 +19,20 @@ async def lifespan(_: FastAPI):
pomodoro_task = asyncio.create_task(pomodoro_watcher_loop())
fitness_task = asyncio.create_task(fitness_watcher_loop())
homelab_task = asyncio.create_task(homelab_watcher_loop())
reminders_task = asyncio.create_task(reminders_watcher_loop())
yield
pomodoro_task.cancel()
fitness_task.cancel()
homelab_task.cancel()
reminders_task.cancel()
with suppress(asyncio.CancelledError):
await pomodoro_task
with suppress(asyncio.CancelledError):
await fitness_task
with suppress(asyncio.CancelledError):
await homelab_task
with suppress(asyncio.CancelledError):
await reminders_task
def create_app() -> FastAPI:
+3
View File
@@ -0,0 +1,3 @@
from app.reminders.service import RemindersService
__all__ = ["RemindersService"]
+33
View File
@@ -0,0 +1,33 @@
from typing import Any
from sqlalchemy.orm import Session
from app.reminders.service import RemindersService
MAX_IN_CONTEXT = 10
def get_reminders_snapshot(db: Session) -> dict[str, Any]:
return RemindersService(db).snapshot()
def format_reminders_context(snapshot: dict[str, Any]) -> str:
lines = ["[Напоминания]"]
upcoming = snapshot.get("upcoming") or []
tz = snapshot.get("timezone", "Europe/Moscow")
if not upcoming:
lines.append(
"Ближайших напоминаний нет. "
"create_reminder для «напомни через 15 минут», «завтра утром», точной даты."
)
return "\n".join(lines)
lines.append(f"Часовой пояс: {tz}. Tools: list_reminders, create_reminder, update_reminder, delete_reminder, complete_reminder.")
for item in upcoming[:MAX_IN_CONTEXT]:
rec = item.get("recurrence", "none")
rec_label = f" · повтор: {rec}" if rec and rec != "none" else ""
lines.append(
f"- #{item['id']} **{item['title']}** · {item.get('due_at_local', item.get('due_at'))}{rec_label}"
)
return "\n".join(lines)
+60
View File
@@ -0,0 +1,60 @@
import logging
from datetime import datetime, timezone
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.chat.notice_inbox import post_notice_to_latest_chat
from app.db.models import Reminder
from app.reminders.notify import bump_notify_seq
from app.reminders.service import RECURRENCE_NONE, _advance_due, _format_local
logger = logging.getLogger(__name__)
def _utcnow() -> datetime:
return datetime.now(timezone.utc)
def check_due_reminders(db: Session) -> int:
now = _utcnow()
stmt = (
select(Reminder)
.where(
Reminder.enabled.is_(True),
Reminder.completed_at.is_(None),
Reminder.due_at <= now,
)
.order_by(Reminder.due_at.asc())
)
rows = list(db.scalars(stmt).all())
fired = 0
for row in rows:
if row.last_fired_at and row.last_fired_at >= row.due_at:
continue
local_when = _format_local(row.due_at, row.timezone, all_day=row.all_day)
notice = f"📅 **Напоминание** · {row.title}\n\n_{local_when}_"
if row.notes:
notice += f"\n{row.notes}"
post_notice_to_latest_chat(notice)
row.last_fired_at = now
if row.recurrence == RECURRENCE_NONE:
row.completed_at = now
row.enabled = False
else:
row.due_at = _advance_due(row.due_at, row.recurrence)
row.last_fired_at = None
row.updated_at = now
fired += 1
if fired:
db.commit()
bump_notify_seq(db)
logger.info("Reminders fired: %d", fired)
return fired
+19
View File
@@ -0,0 +1,19 @@
from sqlalchemy.orm import Session
from app.homelab.state import get_state, set_state
NOTIFY_SEQ_KEY = "reminders_notify_seq"
def get_notify_seq(db: Session) -> int:
raw = get_state(db, NOTIFY_SEQ_KEY)
try:
return int(raw or 0)
except ValueError:
return 0
def bump_notify_seq(db: Session) -> int:
seq = get_notify_seq(db) + 1
set_state(db, NOTIFY_SEQ_KEY, str(seq))
return seq
+226
View File
@@ -0,0 +1,226 @@
import calendar
from datetime import datetime, timedelta, timezone
from typing import Any
from zoneinfo import ZoneInfo
from sqlalchemy import select
from sqlalchemy.orm import Session
from app.db.models import Reminder
from app.homelab.context import resolve_timezone
from app.reminders.notify import bump_notify_seq, get_notify_seq
RECURRENCE_NONE = "none"
RECURRENCE_DAILY = "daily"
RECURRENCE_WEEKLY = "weekly"
RECURRENCE_MONTHLY = "monthly"
VALID_RECURRENCE = frozenset({RECURRENCE_NONE, RECURRENCE_DAILY, RECURRENCE_WEEKLY, RECURRENCE_MONTHLY})
def _utcnow() -> datetime:
return datetime.now(timezone.utc)
def _parse_due_at(raw: str, tz_name: str) -> datetime:
clean = raw.strip()
if not clean:
raise ValueError("due_at не может быть пустым")
try:
dt = datetime.fromisoformat(clean.replace("Z", "+00:00"))
except ValueError as exc:
raise ValueError(f"Неверный формат даты: {raw}") from exc
if dt.tzinfo is None:
try:
dt = dt.replace(tzinfo=ZoneInfo(tz_name))
except Exception:
dt = dt.replace(tzinfo=ZoneInfo("Europe/Moscow"))
return dt.astimezone(timezone.utc)
def _advance_due(due_at: datetime, recurrence: str) -> datetime:
if recurrence == RECURRENCE_DAILY:
return due_at + timedelta(days=1)
if recurrence == RECURRENCE_WEEKLY:
return due_at + timedelta(weeks=1)
if recurrence == RECURRENCE_MONTHLY:
month = due_at.month + 1
year = due_at.year
if month > 12:
month = 1
year += 1
day = min(due_at.day, calendar.monthrange(year, month)[1])
return due_at.replace(year=year, month=month, day=day)
return due_at
def _format_local(dt: datetime, tz_name: str, *, all_day: bool = False) -> str:
try:
local = dt.astimezone(ZoneInfo(tz_name))
except Exception:
local = dt.astimezone(ZoneInfo("Europe/Moscow"))
if all_day:
return local.strftime("%Y-%m-%d")
return local.strftime("%Y-%m-%d %H:%M")
class RemindersService:
def __init__(self, db: Session):
self.db = db
def _tz(self) -> str:
return resolve_timezone(self.db)
def _to_dict(self, row: Reminder) -> dict[str, Any]:
tz = row.timezone or self._tz()
return {
"id": row.id,
"title": row.title,
"notes": row.notes,
"due_at": row.due_at.isoformat(),
"due_at_local": _format_local(row.due_at, tz, all_day=row.all_day),
"all_day": row.all_day,
"recurrence": row.recurrence,
"enabled": row.enabled,
"completed_at": row.completed_at.isoformat() if row.completed_at else None,
"timezone": tz,
"created_at": row.created_at.isoformat() if row.created_at else None,
}
def snapshot(self) -> dict[str, Any]:
upcoming = self.list_upcoming(limit=12)
return {
"notify_seq": get_notify_seq(self.db),
"upcoming": upcoming,
"upcoming_count": len(upcoming),
"timezone": self._tz(),
}
def list_upcoming(self, *, limit: int = 30) -> list[dict[str, Any]]:
stmt = (
select(Reminder)
.where(
Reminder.enabled.is_(True),
Reminder.completed_at.is_(None),
)
.order_by(Reminder.due_at.asc())
.limit(limit)
)
return [self._to_dict(row) for row in self.db.scalars(stmt).all()]
def list_in_range(
self,
*,
date_from: datetime,
date_to: datetime,
) -> list[dict[str, Any]]:
stmt = (
select(Reminder)
.where(
Reminder.enabled.is_(True),
Reminder.completed_at.is_(None),
Reminder.due_at >= date_from,
Reminder.due_at < date_to,
)
.order_by(Reminder.due_at.asc())
)
return [self._to_dict(row) for row in self.db.scalars(stmt).all()]
def get(self, reminder_id: int) -> dict[str, Any] | None:
row = self.db.get(Reminder, reminder_id)
return self._to_dict(row) if row else None
def create(
self,
*,
title: str,
due_at: str,
notes: str = "",
all_day: bool = False,
recurrence: str = RECURRENCE_NONE,
) -> dict[str, Any]:
clean_title = title.strip()
if not clean_title:
raise ValueError("Название напоминания не может быть пустым")
rec = (recurrence or RECURRENCE_NONE).strip().lower()
if rec not in VALID_RECURRENCE:
raise ValueError(f"recurrence должен быть один из: {', '.join(sorted(VALID_RECURRENCE))}")
tz = self._tz()
due = _parse_due_at(due_at, tz)
row = Reminder(
title=clean_title,
notes=notes.strip(),
due_at=due,
all_day=all_day,
recurrence=rec,
timezone=tz,
)
self.db.add(row)
self.db.commit()
self.db.refresh(row)
bump_notify_seq(self.db)
return {"ok": True, "reminder": self._to_dict(row), "created": True}
def update(
self,
reminder_id: int,
*,
title: str | None = None,
due_at: str | None = None,
notes: str | None = None,
all_day: bool | None = None,
recurrence: str | None = None,
enabled: bool | None = None,
) -> dict[str, Any]:
row = self.db.get(Reminder, reminder_id)
if not row:
raise ValueError("Напоминание не найдено")
if title is not None:
clean = title.strip()
if not clean:
raise ValueError("Название не может быть пустым")
row.title = clean
if notes is not None:
row.notes = notes.strip()
if due_at is not None:
row.due_at = _parse_due_at(due_at, row.timezone or self._tz())
row.last_fired_at = None
if all_day is not None:
row.all_day = all_day
if recurrence is not None:
rec = recurrence.strip().lower()
if rec not in VALID_RECURRENCE:
raise ValueError(f"recurrence должен быть один из: {', '.join(sorted(VALID_RECURRENCE))}")
row.recurrence = rec
if enabled is not None:
row.enabled = enabled
row.updated_at = _utcnow()
self.db.commit()
self.db.refresh(row)
bump_notify_seq(self.db)
return {"ok": True, "reminder": self._to_dict(row)}
def delete(self, reminder_id: int) -> dict[str, Any]:
row = self.db.get(Reminder, reminder_id)
if not row:
raise ValueError("Напоминание не найдено")
title = row.title
self.db.delete(row)
self.db.commit()
bump_notify_seq(self.db)
return {"ok": True, "deleted_id": reminder_id, "title": title}
def complete(self, reminder_id: int) -> dict[str, Any]:
row = self.db.get(Reminder, reminder_id)
if not row:
raise ValueError("Напоминание не найдено")
now = _utcnow()
row.completed_at = now
row.enabled = False
row.updated_at = now
self.db.commit()
self.db.refresh(row)
bump_notify_seq(self.db)
return {"ok": True, "reminder": self._to_dict(row)}
+31
View File
@@ -0,0 +1,31 @@
import asyncio
import logging
from app.config import get_settings
from app.db.base import SessionLocal
from app.reminders.fire import check_due_reminders
logger = logging.getLogger(__name__)
WATCH_INTERVAL_SEC = 30
async def reminders_watcher_loop() -> None:
while True:
try:
await asyncio.sleep(WATCH_INTERVAL_SEC)
if not get_settings().reminders_enabled:
continue
await _tick()
except asyncio.CancelledError:
raise
except Exception:
logger.exception("Reminders watcher error")
async def _tick() -> None:
db = SessionLocal()
try:
check_due_reminders(db)
finally:
db.close()
+109
View File
@@ -13,6 +13,7 @@ from app.integrations.wger import WgerClient
from app.memory.service import MemoryService
from app.pomodoro.service import PomodoroService
from app.projects.service import ProjectService
from app.reminders.service import RemindersService
from app.shopping.service import ShoppingService
TOOL_DEFINITIONS: list[dict[str, Any]] = [
@@ -585,6 +586,89 @@ TOOL_DEFINITIONS: list[dict[str, Any]] = [
},
},
},
{
"type": "function",
"function": {
"name": "list_reminders",
"description": "Список активных напоминаний. «Что напомнил», «мои напоминания».",
"parameters": {
"type": "object",
"properties": {
"limit": {"type": "integer", "description": "Макс. записей, по умолчанию 20"},
},
"required": [],
},
},
},
{
"type": "function",
"function": {
"name": "create_reminder",
"description": (
"Создать напоминание. due_at — ISO datetime в часовом поясе пользователя "
"(см. [Текущее время]). Примеры: через 15 мин, завтра 09:00, 2027-05-12T12:16:00."
),
"parameters": {
"type": "object",
"properties": {
"title": {"type": "string", "description": "О чём напомнить"},
"due_at": {"type": "string", "description": "ISO datetime"},
"notes": {"type": "string"},
"all_day": {"type": "boolean"},
"recurrence": {
"type": "string",
"enum": ["none", "daily", "weekly", "monthly"],
"description": "Повтор",
},
},
"required": ["title", "due_at"],
},
},
},
{
"type": "function",
"function": {
"name": "update_reminder",
"description": "Изменить напоминание по id.",
"parameters": {
"type": "object",
"properties": {
"reminder_id": {"type": "integer"},
"title": {"type": "string"},
"due_at": {"type": "string"},
"notes": {"type": "string"},
"all_day": {"type": "boolean"},
"recurrence": {"type": "string", "enum": ["none", "daily", "weekly", "monthly"]},
"enabled": {"type": "boolean"},
},
"required": ["reminder_id"],
},
},
},
{
"type": "function",
"function": {
"name": "delete_reminder",
"description": "Удалить напоминание по id.",
"parameters": {
"type": "object",
"properties": {"reminder_id": {"type": "integer"}},
"required": ["reminder_id"],
},
},
},
{
"type": "function",
"function": {
"name": "complete_reminder",
"description": "Отметить напоминание выполненным (снять с календаря).",
"parameters": {
"type": "object",
"properties": {"reminder_id": {"type": "integer"}},
"required": ["reminder_id"],
},
},
},
{
"type": "function",
"function": {
@@ -618,6 +702,7 @@ async def execute_tool(
memory = MemoryService(db)
fitness = FitnessService(db)
shopping = ShoppingService(db)
reminders = RemindersService(db)
try:
if name == "get_pomodoro_status":
@@ -792,6 +877,30 @@ async def execute_tool(
result = shopping.remove_item(int(arguments["item_id"]))
elif name == "delete_shopping_list":
result = shopping.delete_list(int(arguments["list_id"]))
elif name == "list_reminders":
result = reminders.list_upcoming(limit=int(arguments.get("limit") or 20))
elif name == "create_reminder":
result = reminders.create(
title=arguments.get("title", ""),
due_at=arguments.get("due_at", ""),
notes=arguments.get("notes", ""),
all_day=bool(arguments.get("all_day", False)),
recurrence=arguments.get("recurrence", "none"),
)
elif name == "update_reminder":
result = reminders.update(
int(arguments["reminder_id"]),
title=arguments.get("title"),
due_at=arguments.get("due_at"),
notes=arguments.get("notes"),
all_day=arguments.get("all_day"),
recurrence=arguments.get("recurrence"),
enabled=arguments.get("enabled"),
)
elif name == "delete_reminder":
result = reminders.delete(int(arguments["reminder_id"]))
elif name == "complete_reminder":
result = reminders.complete(int(arguments["reminder_id"]))
else:
return json.dumps({"error": f"Unknown tool: {name}"}, ensure_ascii=False)