From 52c42f3d06c36429752a16d3414ad1776ba294d2 Mon Sep 17 00:00:00 2001 From: Cosmo Date: Thu, 23 Apr 2026 14:34:40 +0000 Subject: [PATCH] =?UTF-8?q?feat(tools):=20calendar=20CRUD=20tools=20?= =?UTF-8?q?=E2=80=94=20create=5Fevent,=20update=5Fevent,=20delete=5Fevent?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - create_event(title, date, start_time?, end_time?, all_day?, owner) owner обязателен (daniil | sveta). System prompt велит LLM уточнять чей это календарь, если неясно. - update_event(event_id, owner, ...fields) — меняет только переданные поля. Сначала нужно вызвать get_today_events для получения event_id. - delete_event(event_id, owner) — сначала get_today_events, найти событие по названию, подтвердить если важное. get_today_events теперь возвращает event_id и owner (daniil/sveta), плюс принимает range=month. Description явно говорит LLM что это первый tool для CRUD-сценариев. System prompt (Cosmo и Люся) дополнен секцией 'Работа с календарём' с правилами: даты YYYY-MM-DD, время HH:MM, «завтра» = +1 день, вычислять от {today}. Co-Authored-By: Claude Opus 4.7 (1M context) --- satellite/llm_claude.py | 25 ++++++-- satellite/tools.py | 138 ++++++++++++++++++++++++++++++++++++++-- 2 files changed, 154 insertions(+), 9 deletions(-) diff --git a/satellite/llm_claude.py b/satellite/llm_claude.py index 1ca06f3..c8debd3 100644 --- a/satellite/llm_claude.py +++ b/satellite/llm_claude.py @@ -61,10 +61,20 @@ COSMO_SYSTEM_PROMPT = """Ты — Cosmo, домашний голосовой а результата. Не пересказывай сырые данные дословно — дай человеческую сводку. 5. Если подходящего tool нет — честно скажи «так я не умею», а не притворяйся. -Доступные tools: get_weather, get_transport, get_today_events, get_notes, -set_timer, cancel_timer, adjust_timer. +Доступные tools: get_weather, get_transport, get_today_events, create_event, +update_event, delete_event, get_notes, set_timer, cancel_timer, adjust_timer. -Контекст: Даниил — разработчик, живёт в СПб с женой Светой. Сегодня {today}.""" +Работа с календарём: +- У Даниила и Светы разные календари. Параметр owner обязательный. +- Если пользователь не уточнил чей календарь — СПРОСИ прежде чем вызывать + create_event. Не угадывай даже если контекст намекает. +- Для изменения или удаления события сначала вызови get_today_events + (можно с range=week/month), найди нужное событие по названию и времени, + потом действуй с его event_id и owner. +- Даты в формате YYYY-MM-DD (2026-04-24), времена HH:MM (14:30). + «завтра» = сегодня+1 по дате, «послезавтра» = +2. Сегодня {today}. + +Контекст: Даниил — разработчик, живёт в СПб с женой Светой.""" LUSYA_SYSTEM_PROMPT = """Ты — Люся, домашний голосовой ассистент Светы (Санкт-Петербург). @@ -75,12 +85,17 @@ LUSYA_SYSTEM_PROMPT = """Ты — Люся, домашний голосовой - Если не знаешь — скажи коротко. ЖЁСТКИЕ ПРАВИЛА про tools: -1. Действия (таймер, etc.) — только через вызов tool. Без tool действие не произошло. +1. Действия (таймер, события) — только через вызов tool. Без tool действие не произошло. 2. Не говори «поставила/отменила/изменила», если ты не вызвала соответствующий tool. 3. Информацию (погода, транспорт, события) — всегда через tool, не выдумывай. 4. Tool → результат → короткий ответ человеческим языком. -Сегодня {today}.""" +Календарь: +- Свой = Светин, ещё есть календарь Данила. Для create_event уточняй + в какой календарь, если неясно. +- Для update_event / delete_event: сначала get_today_events, найди по + названию, потом действуй. +- Даты YYYY-MM-DD, время HH:MM. Сегодня {today}.""" _client: "anthropic.Anthropic | None" = None diff --git a/satellite/tools.py b/satellite/tools.py index 232d487..ba2a24c 100644 --- a/satellite/tools.py +++ b/satellite/tools.py @@ -84,20 +84,100 @@ TOOL_SCHEMAS: list[dict] = [ { "name": "get_today_events", "description": ( - "События из календаря на сегодня или на неделю. " - "Для вопросов 'что сегодня', 'какие планы', 'во сколько встреча'." + "События из календаря (Даниил + Света). Вернёт id события, " + "title, start, end, owner ('daniil' или 'sveta'). " + "ВАЖНО: для update_event / delete_event сначала вызывай этот tool " + "чтобы получить event_id." ), "input_schema": { "type": "object", "properties": { "range": { "type": "string", - "enum": ["today", "week"], - "description": "today (по умолчанию) или week (7 дней)", + "enum": ["today", "week", "month"], + "description": "today (по умолчанию), week (7 дней) или month (текущий месяц)", }, }, }, }, + { + "name": "create_event", + "description": ( + "Создать событие в Google Calendar. " + "ВАЖНО: параметр owner обязателен. Если пользователь не сказал " + "чей это календарь — СПРОСИ у него ('в твой календарь или в Светин?') " + "и только потом вызывай tool. Не угадывай." + ), + "input_schema": { + "type": "object", + "properties": { + "title": {"type": "string", "description": "Название события"}, + "date": {"type": "string", "description": "Дата в формате YYYY-MM-DD"}, + "start_time": { + "type": "string", + "description": "Время начала в формате HH:MM (24-часовой). Обязательно если all_day=false.", + }, + "end_time": { + "type": "string", + "description": "Время окончания в формате HH:MM. По умолчанию start_time + 1 час.", + }, + "all_day": { + "type": "boolean", + "description": "Событие на весь день без времени. По умолчанию false.", + }, + "owner": { + "type": "string", + "enum": ["daniil", "sveta"], + "description": "Чей это календарь — Даниила или Светы", + }, + }, + "required": ["title", "date", "owner"], + }, + }, + { + "name": "update_event", + "description": ( + "Изменить существующее событие. Сначала обязательно вызови get_today_events " + "чтобы получить event_id и owner нужного события. Передавай только те поля " + "которые меняешь." + ), + "input_schema": { + "type": "object", + "properties": { + "event_id": {"type": "string"}, + "owner": { + "type": "string", + "enum": ["daniil", "sveta"], + "description": "Чей календарь (из get_today_events)", + }, + "title": {"type": "string"}, + "date": {"type": "string", "description": "YYYY-MM-DD"}, + "start_time": {"type": "string", "description": "HH:MM"}, + "end_time": {"type": "string", "description": "HH:MM"}, + "all_day": {"type": "boolean"}, + }, + "required": ["event_id", "owner"], + }, + }, + { + "name": "delete_event", + "description": ( + "Удалить событие из календаря. Сначала вызови get_today_events чтобы " + "найти event_id и определить owner. Подтверди удаление с пользователем " + "если событие важное (встреча, врач, работа)." + ), + "input_schema": { + "type": "object", + "properties": { + "event_id": {"type": "string"}, + "owner": { + "type": "string", + "enum": ["daniil", "sveta"], + }, + }, + "required": ["event_id", "owner"], + }, + }, { "name": "get_notes", "description": ( @@ -196,6 +276,53 @@ def _exec_get_today_events(params: dict, agent_id: str) -> Any: return _tablet_get("/api/voice/tools/events", params={"range": range_}) +def _exec_create_event(params: dict, agent_id: str) -> Any: + payload = { + "title": params.get("title", "").strip(), + "date": params.get("date", "").strip(), + "owner": params.get("owner", "daniil"), + "all_day": bool(params.get("all_day", False)), + } + if not payload["title"] or not payload["date"]: + return {"error": "title and date required"} + if not payload["all_day"]: + payload["start_time"] = params.get("start_time", "") + if "end_time" in params: + payload["end_time"] = params.get("end_time", "") + return _tablet_post("/api/voice/tools/events", payload) + + +def _exec_update_event(params: dict, agent_id: str) -> Any: + event_id = params.get("event_id", "").strip() + owner = params.get("owner", "").strip() + if not event_id or not owner: + return {"error": "event_id and owner required"} + payload = {"event_id": event_id, "owner": owner} + for k in ("title", "date", "start_time", "end_time", "all_day"): + if k in params: + payload[k] = params[k] + url = f"{TABLET_URL}/api/voice/tools/events" + r = _session.put(url, headers={**_headers(), "Content-Type": "application/json"}, json=payload, timeout=8) + r.raise_for_status() + return r.json() + + +def _exec_delete_event(params: dict, agent_id: str) -> Any: + event_id = params.get("event_id", "").strip() + owner = params.get("owner", "daniil").strip() + if not event_id: + return {"error": "event_id required"} + url = f"{TABLET_URL}/api/voice/tools/events" + r = _session.delete( + url, + headers=_headers(), + params={"event_id": event_id, "owner": owner}, + timeout=8, + ) + r.raise_for_status() + return r.json() + + def _exec_get_notes(params: dict, agent_id: str) -> Any: return _tablet_get("/api/voice/tools/notes") @@ -235,6 +362,9 @@ EXECUTORS = { "get_weather": _exec_get_weather, "get_transport": _exec_get_transport, "get_today_events": _exec_get_today_events, + "create_event": _exec_create_event, + "update_event": _exec_update_event, + "delete_event": _exec_delete_event, "get_notes": _exec_get_notes, "set_timer": _exec_set_timer, "cancel_timer": _exec_cancel_timer,