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_idmodel_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, если он определён. |