ИИ-агенты для расписаний и календарей — Полный гайд | QantCore
📅

ИИ-агенты для расписаний и календарей

Научите AI-агента автоматически планировать встречи, находить оптимальные временные слоты и разрешать конфликты в расписании. Практическое руководство с интеграцией Google Calendar API, алгоритмами поиска окон и NLP-интерфейсом на естественном языке.

intermediate ⏱ 15 мин
NLP Input "Найди время для встречи" Scheduling Agent Intent Parser Slot Optimizer Calendar Cache Events + Preferences Conflict Resolution Free slot detection + ranking 🛠 Google Calendar API / CRUD Events create / update / delete / list freebusy Scheduled! Event created + notified

# 1. Архитектура агента-планировщика

Агент для работы с расписаниями — это специализированный AI-ассистент, который понимает запросы на естественном языке и преобразует их в операции с календарём. Ключевая особенность: агент должен не просто выполнять CRUD операции, но и принимать интеллектуальные решения — находить свободные слоты, учитывать предпочтения пользователя, разрешать конфликты и предлагать альтернативы. Архитектура состоит из трёх слоёв: NLP-интерфейс (понимание намерений), Scheduling Engine (алгоритмы работы со слотами), Calendar Adapter (интеграция с API календаря).

# Структура инструментов агента-планировщика
SCHEDULING_TOOLS = [
    {
        "type": "function",
        "function": {
            "name": "find_free_slots",
            "description": "Найти свободные временные слоты в указанном диапазоне дат",
            "parameters": {
                "type": "object",
                "properties": {
                    "start_date": {"type": "string", "description": "Начальная дата ISO"},
                    "end_date": {"type": "string"},
                    "duration_minutes": {"type": "integer"},
                    "preferred_hours": {"type": "array", "items": {"type": "integer"}}
                },
                "required": ["start_date", "end_date", "duration_minutes"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "create_event",
            "description": "Создать событие в календаре",
            "parameters": {
                "type": "object",
                "properties": {
                    "title": {"type": "string"},
                    "start_time": {"type": "string"},
                    "end_time": {"type": "string"},
                    "attendees": {"type": "array", "items": {"type": "string"}},
                    "description": {"type": "string"}
                },
                "required": ["title", "start_time", "end_time"]
            }
        }
    },
    {
        "type": "function",
        "function": {
            "name": "list_events",
            "description": "Получить список событий за период",
            "parameters": {
                "type": "object",
                "properties": {
                    "time_min": {"type": "string"},
                    "time_max": {"type": "string"}
                },
                "required": ["time_min"]
            }
        }
    }
]
    

# 2. Интеграция с Google Calendar API

Google Calendar — наиболее распространённый календарный сервис, и его API отлично подходит для программного управления расписаниями. Для работы потребуется создать проект в Google Cloud Console, включить Calendar API, настроить OAuth 2.0 (для персональных календарей) или Service Account (для корпоративных/доменных). Полученные credentials сохраняются в переменные окружения и используются для инициализации клиента.

# google_calendar_adapter.py — адаптер для Google Calendar API
import os
from datetime import datetime, timedelta
from google.oauth2.service_account import Credentials
from googleapiclient.discovery import build

class GoogleCalendarAdapter:
    def __init__(self, calendar_id: str = "primary"):
        credentials = Credentials.from_service_account_file(
            os.getenv("GOOGLE_CREDENTIALS_PATH"),
            scopes=["https://www.googleapis.com/auth/calendar"]
        )
        self.service = build("calendar", "v3", credentials=credentials)
        self.calendar_id = calendar_id

    def list_events(self, time_min: datetime, time_max: datetime = None):
        if not time_max:
            time_max = time_min + timedelta(days=7)
        result = self.service.events().list(
            calendarId=self.calendar_id,
            timeMin=time_min.isoformat() + "Z",
            timeMax=time_max.isoformat() + "Z",
            singleEvents=True,
            orderBy="startTime"
        ).execute()
        return result.get("items", [])

    def create_event(self, title: str, start: datetime, end: datetime,
                      attendees: list = None, description: str = ""):
        event = {
            "summary": title,
            "start": {"dateTime": start.isoformat(), "timeZone": "Europe/Moscow"},
            "end": {"dateTime": end.isoformat(), "timeZone": "Europe/Moscow"},
            "description": description,
            "attendees": [{"email": a} for a in (attendees or [])],
            "reminders": {"useDefault": True}
        }
        created = self.service.events().insert(
            calendarId=self.calendar_id, body=event, sendUpdates="all"
        ).execute()
        return created

    def get_freebusy(self, time_min: datetime, time_max: datetime):
        body = {
            "timeMin": time_min.isoformat() + "Z",
            "timeMax": time_max.isoformat() + "Z",
            "items": [{"id": self.calendar_id}]
        }
        result = self.service.freebusy().query(body=body).execute()
        return result["calendars"][self.calendar_id].get("busy", [])
    

# 3. Алгоритм поиска свободных слотов

Ключевой алгоритм агента-планировщика — поиск свободных окон в заданном диапазоне с учётом занятых интервалов. Алгоритм собирает все занятые промежутки из freebusy API, сортирует их, а затем ищет «дыры» достаточной длительности между ними. Дополнительно учитываются рабочие часы, минимальное время на подготовку и буфер между встречами.

# slot_finder.py — алгоритм поиска свободных временных окон
from datetime import datetime, timedelta
from typing import List, Tuple

def find_free_slots(
    busy_intervals: List[Tuple[datetime, datetime]],
    start: datetime, end: datetime,
    duration_min: int,
    working_hours: Tuple[int, int] = (9, 18),
    buffer_minutes: int = 15
) -> List[Tuple[datetime, datetime]]:
    # Нормализуем занятые интервалы — сортируем и объединяем пересекающиеся
    if not busy_intervals:
        busy_intervals = []
    sorted_busy = sorted(busy_intervals)
    merged = []
    for b_start, b_end in sorted_busy:
        if merged and b_start <= merged[-1][1] + timedelta(minutes=buffer_minutes):
            merged[-1] = (merged[-1][0], max(merged[-1][1], b_end))
        else:
            merged.append((b_start, b_end))

    free_slots = []
    cursor = start
    for b_start, b_end in merged:
        if cursor < b_start:
            slot_end = min(cursor + timedelta(minutes=duration_min), b_start)
            if (slot_end - cursor).total_seconds() / 60 >= duration_min:
                if _is_within_working_hours(cursor, slot_end, working_hours):
                    free_slots.append((cursor, slot_end))
        cursor = max(cursor, b_end)
    # Проверяем хвост после последнего занятого интервала
    if cursor < end:
        slot_end = min(cursor + timedelta(minutes=duration_min), end)
        if (slot_end - cursor).total_seconds() / 60 >= duration_min:
            if _is_within_working_hours(cursor, slot_end, working_hours):
                free_slots.append((cursor, slot_end))
    return free_slots

def _is_within_working_hours(slot_start, slot_end, hours):
    # Проверяем, что слот попадает в рабочие часы (с гранулярностью в день)
    sh, eh = hours
    return (sh <= slot_start.hour < eh) and (sh <= slot_end.hour <= eh)
    

# 4. NLP-интерфейс: парсинг запросов на естественном языке

Пользователь взаимодействует с агентом через естественный язык: «Найди время для встречи с командой на следующей неделе», «Перенеси стендап на час позже», «Какие у меня планы на четверг?». LLM (GPT-4, Claude) парсит запрос, извлекает намерение и параметры, а затем вызывает соответствующую функцию. Системный промпт должен чётко описывать контекст — имя пользователя, часовой пояс, рабочие часы, стандартную длительность встреч.

# scheduling_agent.py — основной цикл агента-планировщика
import json
from datetime import datetime
from openai import OpenAI

client = OpenAI()
calendar = GoogleCalendarAdapter()

SYSTEM_PROMPT = """Ты AI-ассистент по планированию. Пользователь: Алексей.
Часовой пояс: Europe/Moscow (UTC+3).
Рабочие часы: 9:00–18:00, стандартная встреча — 30 минут.
Перед созданием встречи ВСЕГДА вызывай find_free_slots.
Если подходящего слота нет — предложи ближайшие альтернативы."""

def execute_tool(tool_name: str, args: dict) -> dict:
    if tool_name == "find_free_slots":
        start = datetime.fromisoformat(args["start_date"])
        end = datetime.fromisoformat(args["end_date"])
        busy_raw = calendar.get_freebusy(start, end)
        busy = [(datetime.fromisoformat(b["start"].replace("Z","")),
                 datetime.fromisoformat(b["end"].replace("Z",""))) for b in busy_raw]
        free = find_free_slots(busy, start, end, args["duration_minutes"])
        return {"free_slots": [(s.isoformat(), e.isoformat()) for s, e in free]}
    elif tool_name == "create_event":
        event = calendar.create_event(
            title=args["title"],
            start=datetime.fromisoformat(args["start_time"]),
            end=datetime.fromisoformat(args["end_time"]),
            attendees=args.get("attendees"),
            description=args.get("description", "")
        )
        return {"event_id": event["id"], "link": event.get("htmlLink")}
    elif tool_name == "list_events":
        tmin = datetime.fromisoformat(args["time_min"])
        tmax = datetime.fromisoformat(args.get("time_max")) if args.get("time_max") else None
        events = calendar.list_events(tmin, tmax)
        return {"events": [{"summary": e.get("summary"), "start": e["start"].get("dateTime")} for e in events]}

def chat(user_message: str) -> str:
    messages = [{"role": "system", "content": SYSTEM_PROMPT},
                {"role": "user", "content": user_message}]
    for _ in range(5):  # максимум 5 tool-call раундов
        response = client.chat.completions.create(
            model="gpt-4", messages=messages,
            tools=SCHEDULING_TOOLS, tool_choice="auto"
        )
        msg = response.choices[0].message
        if msg.tool_calls:
            messages.append(msg)
            for tc in msg.tool_calls:
                result = execute_tool(tc.function.name, json.loads(tc.function.arguments))
                messages.append({"role": "tool", "tool_call_id": tc.id,
                                 "content": json.dumps(result, ensure_ascii=False)})
        else:
            return msg.content
    return "Не удалось выполнить запрос за отведённое число шагов."
    

# 5. Разрешение конфликтов и умные предложения

В реальных сценариях пользователь может запросить встречу на уже занятое время. Агент должен не просто отказать, а предложить альтернативы: ближайшие свободные слоты до и после запрошенного времени, возможность сдвинуть существующие встречи (с согласия участников), или сократить длительность. Реализуем алгоритм ранжирования альтернатив: слоты оцениваются по близости к запрошенному времени, попаданию в предпочтительные часы и отсутствию конфликтов с высокоприоритетными событиями.

# conflict_resolver.py — ранжирование альтернативных слотов
from datetime import datetime, timedelta

def rank_alternatives(
    requested_time: datetime,
    free_slots: list,
    preferred_hours: list = None
) -> list:
    """Ранжирует свободные слоты: чем ближе к запрошенному времени, тем выше."""
    scored = []
    for slot_start, slot_end in free_slots:
        score = 0
        # Близость к запрошенному времени (чем ближе — тем выше)
        distance = abs((slot_start - requested_time).total_seconds() / 3600)
        score += max(0, 100 - distance * 10)

        # Попадание в предпочтительные часы (бонус +25)
        if preferred_hours and slot_start.hour in preferred_hours:
            score += 25

        # Утренние слоты — приоритетнее вечерних
        if 9 <= slot_start.hour <= 12:
            score += 15
        elif 16 <= slot_start.hour <= 18:
            score += 5

        scored.append((score, slot_start, slot_end))

    scored.sort(key=lambda x: x[0], reverse=True)
    return [(s, e) for _, s, e in scored[:5]]  # топ-5 альтернатив
    

# 6. Мульти-календарная оркестрация и повторяющиеся события

Реальный агент-планировщик должен работать с несколькими календарями одновременно: личный, рабочий, календари коллег. Google Calendar API поддерживает запрос freebusy для нескольких calendarId одновременно, что позволяет находить слоты, свободные у всех участников. Для повторяющихся событий (recurring events) API возвращает только первое вхождение с правилом recurrence (RRULE, EXDATE) — агент должен разворачивать такие события на нужный диапазон дат.

# multi_calendar.py — поиск слотов у нескольких участников
def find_group_free_slots(
    calendar_ids: list, time_min: datetime, time_max: datetime,
    duration_min: int
):
    """Находит слоты, свободные у ВСЕХ участников."""
    body = {
        "timeMin": time_min.isoformat() + "Z",
        "timeMax": time_max.isoformat() + "Z",
        "items": [{"id": cid} for cid in calendar_ids]
    }
    result = calendar.service.freebusy().query(body=body).execute()

    # Объединяем занятости всех участников
    all_busy = []
    for cid in calendar_ids:
        busy_list = result["calendars"].get(cid, {}).get("busy", [])
        for b in busy_list:
            all_busy.append((
                datetime.fromisoformat(b["start"].replace("Z", "")),
                datetime.fromisoformat(b["end"].replace("Z", ""))
            ))

    return find_free_slots(all_busy, time_min, time_max, duration_min)

# Пример использования: найти окно для встречи команды из 3 человек
team_calendars = ["alex@company.com", "maria@company.com", "ivan@company.com"]
slots = find_group_free_slots(
    team_calendars,
    datetime(2026, 6, 22),   # понедельник
    datetime(2026, 6, 26),   # пятница
    duration_min=45
)
print(f"Найдено общих слотов: {len(slots)}")
for s, e in slots:
    print(f"  {s.strftime('%d.%m %H:%M')} - {e.strftime('%H:%M')}")
    
✅ Итог

Мы создали полноценного AI-агента для управления расписаниями: интеграция с Google Calendar API, алгоритм поиска свободных слотов, NLP-интерфейс через function calling, разрешение конфликтов и мульти-календарная оркестрация. Агент понимает запросы на естественном языке и самостоятельно находит оптимальное время для встреч. Следующие шаги: добавление интеграции с Microsoft Outlook/Exchange, поддержка видеоконференций (автоматическое создание Zoom/Google Meet), машинное обучение для предсказания оптимальной длительности встреч на основе типа события и состава участников.