Перейти к содержанию

Realtime и история диалога

В этом разделе описано, как SIP Hook взаимодействует с Realtime‑моделью и как голосовые диалоги попадают в общую таблицу dialogue_history.

Warning

Любой реальный звонок в Realtime — это использование OpenAI API. При тестах вы будете тратить токены и время на внешние вызовы.
Если нужно просто проверить маршрутизацию/валидацию webhook, не гоняйте полный сценарий звонка без необходимости.

Работа с Realtime начинается в модуле sip_hook_app/model_setup.py. Функция build_model_settings() собирает конфигурацию для Realtime‑модели из значений, заданных в config.py: здесь определяется имя модели, голос, параметры детектора тишины (VAD), формат аудио и настройки онлайн‑транскрипции (модель, язык, подсказка для распознавания речи). Возвращаемый словарь затем используется как initial_model_settings при запуске голосового раннера. Функция build_call_accept_payload(instructions) на основе этих настроек формирует тело запроса для POST /realtime/calls/{id}/accept: указывает модель, текст инструкций и структуру блока audio (формат входного/выходного звука, VAD, скорость воспроизведения, голос и параметры транскрипции).

Параметры и возвращаемые значения model_setup

Name Type Description
build_model_settings Функция без аргументов; читает значения из config и возвращает словарь с настройками Realtime‑модели.
return (settings) dict Словарь initial_model_settings с моделью, голосом, настройками VAD и параметрами онлайн‑транскрипции.
instructions str Текст инструкций для Realtime‑агента, передаваемый в build_call_accept_payload и используемый при accept звонка.
return (accept_payload) dict Готовый payload для POST /realtime/calls/{id}/accept с полями модели, аудио и текстом инструкций.

Голосовой агент создаётся функцией build_voice_agent(metadata, mcp_server). В ней используется класс RealtimeAgent из OpenAI Agents SDK. В качестве инструкций сюда передаётся результат вызова load_instructions(metadata) (см. ниже), а список инструментов включает локальную функцию hangup_call, которая оформлена через декоратор function_tool. Если при запуске доступно подключение к MCP‑серверу, объект MCP также передаётся в конструктор агента, чтобы голосовой ассистент мог вызывать MCP‑инструменты во время диалога. Функция hangup_call извлекает call_id из контекста, вызывает client.realtime.calls.hangup для завершения звонка и дополнительно уведомляет локальный раннер о необходимости остановки.

Параметры и возвращаемые значения build_voice_agent / hangup_call

Name Type Description
metadata dict[str, Any] Метаданные звонка (номер телефона, user_id, история и служебные поля), используемые для формирования инструкций.
mcp_server MCP‑клиент | None Клиент MCP‑сервера; при None голосовой агент работает без MCP‑инструментов.
return (agent) RealtimeAgent Сконфигурированный голосовой агент, готовый к запуску в Realtime‑сессии.
hangup_call tool‑функция Realtime Функция‑инструмент без явных аргументов; берёт call_id из контекста и завершает звонок через Realtime API.

Для того чтобы голосовой ассистент начинал разговор с заранее заданного приветствия, используется функция trigger_initial_response(session, call_id, logger). Она отправляет в Realtime‑сессию специальное событие, которое заставляет модель сгенерировать приветственную реплику ещё до того, как клиент что‑то скажет.

Параметры trigger_initial_response

Name Type Description
session Realtime‑сессия Объект активной Realtime‑сессии, в который отправляется событие для первого ответа.
call_id str Идентификатор звонка; используется для логирования и формирования контекста.
logger логгер Логгер, через который записываются диагностические сообщения о запуске приветствия.

Формирование инструкций для голосового агента вынесено в модуль sip_hook_app/instructions.py. Функция _read_instruction_text() читает файл instructions.md в корне проекта SIP Hook и возвращает его содержимое. Если файл отсутствует или пустой, поднимается исключение, чтобы можно было явно увидеть проблему в конфигурации. Вспомогательная функция format_history_lines(history) принимает список элементов истории диалога и форматирует их в удобный для чтения текст (время, направление, содержимое). Основная функция load_instructions(metadata) комбинирует базовый текст из instructions.md с дополнительными блоками: при наличии метаданных добавляется информация о номере телефона и user_id, а при наличии истории — её компактное текстовое представление. Итоговая строка инструкций передаётся в build_voice_agent.

Параметры и возвращаемые значения instructions.py

| Name | Type | Description | |-------------------------|------------------|-----------------------------------------------------------------------------------------------------------------|---------| | _read_instruction_text| — | Внутренняя функция без аргументов; читает instructions.md и возвращает строку с базовыми инструкциями. | | return (text) | str | Содержимое файла instructions.md. | | history | list[dict] | Список элементов истории диалога, передаваемый в format_history_lines для формирования человекочитаемого текста. | — | | return (history_text) | str | Отформатированный текст истории (время, направление, содержимое). | | metadata | dict[str, Any] | Метаданные звонка, передаваемые в load_instructions (номер, user_id, история), на основе которых дополняются инструкции. | — | | return (instructions) | str | Итоговая строка инструкций для голосового агента, объединяющая базовый текст, данные пользователя и историю. |

Чтобы голосовая история попадала в базу данных, используется модуль sip_hook_app/history_sync.py. Функция load_history_for_metadata(metadata, user_id) запрашивает последние записи диалога для данного пользователя через fetch_dialogue_history из db.py и складывает их в поле history внутри metadata. Это происходит в момент входящего звонка и позволяет включить текстовую историю в инструкции для Realtime‑агента.

Функция handle_history_event(call_id, item, metadata) вызывается из раннера для каждого события истории (history_added, history_updated) и работает по следующему алгоритму:

  • извлекает user_id из metadata; если он отсутствует, запись в историю пропускается;
  • получает item_id из объекта истории; если его нет, событие игнорируется;
  • использует _collect_text_payload(item) для извлечения текстовой части:
  • для UserMessageItem собирает либо текст (InputText), либо транскрипт (InputAudio.transcript), определяя тип содержимого ("text" или "transcript") и направление "in";
  • для AssistantMessageItem аналогично обрабатывает AssistantText и AssistantAudio с транскриптом, проставляя направление "out";
  • если текст извлечь не удалось, событие логируется и пропускается;
  • проверяет, завершён ли элемент истории: если в нём есть статус и он не завершён (completed/done/final), функция ждёт финальную версию и не пишет промежуточные данные;
  • если для item_id уже существует записанная строка (с известным row_id и длиной текста), и новый текст длиннее, вызывает update_dialogue_entry(row_id, ...) в отдельном потоке через asyncio.to_thread, обновляя содержимое и метаданные; если текст не вырос, событие игнорируется;
  • если строки ещё нет, вызывает _record_dialogue_entry_async(...), который через insert_dialogue_entry(...) создаёт новую запись в dialogue_history с каналом "sip", направлением и типом содержимого и возвращает row_id. Эта информация кэшируется в metadata["logged_items"] для последующих обновлений.

Параметры функций работы с историей

Name Type Description
metadata dict[str, Any] Словарь метаданных звонка; в load_history_for_metadata дополняется историей, а в handle_history_event используется для кэша записей.
user_id int Идентификатор пользователя, для которого загружается начальная история диалога.
call_id str Идентификатор звонка, передаваемый в handle_history_event для логирования и поиска раннера.
item объект Realtime Элемент Realtime‑истории, из которого извлекается текстовая часть и статус.

Запуск самого Realtime‑раннера описан в sip_hook_app/runtime.py. Здесь определён вспомогательный класс UsageLoggingListener, который подписывается на события raw_server_event типа response.done и накапливает статистику использования токенов (вход, выход, суммарное количество) по каждому звонку. Структура CallRuntime хранит информацию о запущенном раннере: поток, флаг остановки и функцию закрытия сессии.

Функция run_realtime_session(call_id, stop_flag, metadata) выполняется в отдельном потоке и запускает Realtime‑раннер:

  • открывает асинхронное подключение к MCP‑серверу через connect_mcp_server();
  • строит голосового агента через build_voice_agent(metadata, mcp_server) и настраивает модель OpenAIRealtimeSIPModel;
  • создаёт RealtimeRunner и настраивает модель через build_model_settings() (с учётом того, что при наличии call_id model_name не должен передаваться в настройках);
  • запускает раннер runner.run(...) с контекстом {"call_id": call_id, "metadata": metadata} и конфигурацией модели {"call_id": call_id, "initial_model_settings": ...};
  • внутри контекста Realtime‑сессии:
  • регистрирует функцию close_session в CallRuntime, чтобы stop_runner мог корректно закрыть сессию;
  • вызывает trigger_initial_response, чтобы модель выдала приветствие до первой реплики звонящего;
  • в цикле async for event in session:
    • при stop_flag.is_set() выходит из цикла и завершает сессию;
    • при event.type == "error" логирует ошибку;
    • при event.type == "history_added" вызывает handle_history_event для одного элемента истории;
    • при event.type == "history_updated" вызывает handle_history_event для каждого элемента в event.history;
    • при event.type == "raw_server_event" логирует тип сырых событий сервера.

При сетевых ошибках (ConnectionClosedError, InvalidStatus) и любых других исключениях функция логирует их, снимает слушатель usage, сбрасывает close_session и в конце пишет, что сессия завершена. Статистика токенов по итогам call‑сессии выводится в лог через UsageLoggingListener.log_totals().

Функция start_runner(call_id, metadata) отвечает за запуск потока с run_realtime_session. Если для указанного call_id раннер уже активен, повторный запуск не происходит. Функция stop_runner(call_id) устанавливает флаг остановки и, если определена функция close_session, закрывает Realtime‑сессию. В итоге при входящем звонке (после вызова accept_call) для него создаётся отдельный поток с голосовым раннером, все значимые события истории попадают в базу данных, а использование токенов видно в логах.

Параметры функций run_realtime_session, start_runner, stop_runner

Name Type Description
call_id str Идентификатор звонка; используется для логирования, поиска раннера и замыкания Realtime‑сессии.
stop_flag threading.Event Флаг остановки, который проверяется внутри run_realtime_session для корректного завершения Realtime‑сессии.
metadata dict[str, Any] Метаданные звонка, передаваемые в раннер и используемые для истории и инструкций.
return (run_realtime_session) None Функция выполняется в отдельном потоке и завершается после остановки Realtime‑сессии.
return (start_runner) None Инициирует запуск потока для указанного call_id; явного значения не возвращает.
return (stop_runner) None Устанавливает флаг остановки и вызывает close_session, если он определён.