Как сейчас проходят аудио и диалог chat-agent
Сверка двух пользовательских схем, 06.10.2026. Код chat-agent проверен на
e4bfe285306fc225c3d445ade16296021bd934f3, voice - на работающей ревизии
5d1d9591d57f0b5535884425567c0440b62a91d1. В Insight в 18:49 UTC подтверждены
режим ASR streaming, один экземпляр Core, каждого ingress и Ruby runtime,
их subjects, общий broker и адрес Ollama proxy. Это проверка контрактов и
конфигурации; новые аудиопрогоны и замеры производительности здесь не выполнялись.
Схемы встроены в обзорную презентацию и техническую презентацию. Числа C3/C4 в них сохраняют прежнюю базу сравнения.
Что пришлось уточнить
| На присланной схеме | Как устроено сейчас |
|---|---|
| WebRTC/Opus как единственный фиксированный формат | WebRTC передаёт защищённый RTP; codec согласуется в SDP. Opus поддерживается, но код не закрепляет его как единственный codec. |
| WSS сразу даёт тот же PCM 16 kHz | Браузер отправляет mono PCM16LE с фактической частотой AudioContext. Resampler ingress переводит его в 16 kHz. |
| Общий PCM-блок можно принять за отдельный сервис | Это общий код src_shared/audio.py, работающий отдельно внутри каждого ingress, со своим FIFO и очередью. |
| Core управляет всем подключением браузера | SDP/ICE обслуживает WebRTC ingress; start/end PCM - WSS ingress. Core выдаёт lease через NATS и принимает ASR final. Настройки микрофона применяет frontend. |
| Voice и GPU объединены одной стрелкой | Streaming bridge делает VAD и отправляет PCM по NATS в inference account; GPU worker возвращает JSON через другой subject. Файловый HTTP API в этот путь не входит. |
| Runtime → LLM: "ответ или tool_calls" | К LLM идёт LlmRequest с mode/context/tools. Текст и tool calls возвращаются в LlmResponse. Активный runtime - Ruby + AsyncGraph. |
| Все части ответа возвращаются через runtime/Core | У LLM есть отдельный pub-sub канал chat.agent.stream к Agent. Итоговый RPC-ответ и UI-события - разные пути. |
| Всё состояние Agent находится в PostgreSQL | История и workflow state сохраняются в БД. Очередь, текущая попытка и накопленный snapshot находятся в памяти Agent. |
1. Аудио: сначала транспорт, затем единый PCM-контракт
Исходный Mermaid
flowchart LR
B["Браузер: chat-agent.gis-master.ru"]
R["webrtc_ingress"]
W["audio_ws_ingress"]
C["Core: admission и единственный ASR listener"]
N["Общий NATS: chat-agent_audio_nats:4222"]
V["voice: stt_whisper_to_nats / streaming bridge + VAD"]
G["Внешний inference: GPU ASR worker"]
A["Agent"]
B -->|"HTTPS /core/offer: JSON с SDP"| R
B <-->|"WSS /core/ice/{connection_id}: JSON ICE"| R
B -->|"DTLS-SRTP: RTP audio, codec из SDP"| R
B -->|"WSS /audio/ws: start JSON, затем binary PCM16LE"| W
R -->|"Resampler + FIFO внутри ingress: mono 16 kHz"| N
W -->|"Resampler + FIFO внутри ingress: mono 16 kHz"| N
R <-.->|"chat.agent.audio.control: JSON request/reply"| C
W <-.->|"chat.agent.audio.control: JSON request/reply"| C
N -->|"webrtc.komaroff.asr.stream.{session_id}: header + PCM"| V
V -->|"to.inference.webrtc.asr.audio.{stream_id}: header + PCM"| G
G -->|"from.inference.webrtc.asr.events.{stream_id}: JSON"| V
V -->|"webrtc.komaroff.asr.text.{session_id}: JSON result"| N
N -->|"partial / final / rejected / error"| C
C -->|"Только непустой final: nats.agent.user123 RPC"| A
C -->|"nats.events.user123: ASR и audio_status"| N
A -->|"nats.events.user123: ответ и состояние запроса"| N
N -->|"NATS protocol over WSS /ws"| BВсе нарисованные межсервисные NATS-стрелки проходят через broker, даже когда ради читаемости он не повторён между каждой парой. Переход к GPU включает account import/remap и leafnode. Нельзя заменять его HTTP-загрузкой файла или рисовать direct RPC между ingress и GPU.
Публичные адреса и кто принимает запрос
База чата: https://chat-agent.gis-master.ru. HTTPS и WSS защищены штатным
OAuth ingress; у ICE есть также token конкретного подключения, у audio WSS -
проверка Origin. NATS WebSocket использует отдельную аутентификацию NATS.
| Адрес / метод | Получатель | Что передаётся |
|---|---|---|
GET /core/rtc-capabilities |
WebRTC ingress | JSON {"trickle_ice":1}. При 404 frontend может использовать legacy offer. |
POST /core/offer |
WebRTC ingress | application/json: type:"offer", sdp, session_id, необязательный trickle:true. Ответ: type:"answer", sdp, session_id, stream_id; при Trickle также connection_id, ice_endpoint, ice_token. |
wss://chat-agent.gis-master.ru/core/ice/{connection_id} |
WebRTC ingress | TEXT JSON: auth, ready, candidate, end, error; candidate содержит RTCIceCandidateInit. Token идёт первым auth-сообщением, не в URL. |
DELETE /core/ice/{connection_id} |
WebRTC ingress | Остановка конкретного peer с проверкой header X-ICE-Token; успешный ответ HTTP 204. |
wss://chat-agent.gis-master.ru/audio/ws |
WSS ingress | TEXT JSON start за 3 с; затем binary PCM16LE mono. В конце TEXT JSON end. В обратную сторону: ready, ending, stopped, error. |
wss://chat-agent.gis-master.ru/ws |
Общий NATS, порт 9222 за ingress | Протокол NATS через WSS; payload событий - UTF-8 JSON. Это другой socket, не канал PCM. |
POST /core/message |
Core | JSON: text, session_id, необязательные request_id, turn_id, edit. Ответ - итоговый AgentInboundResponse JSON. |
GET /core/history?session_id={uuid} |
Core → Agent → PostgreSQL | JSON истории; Core не читает БД напрямую. |
GET / DELETE /core/requests/{request_id}?session_id={uuid} |
Core → Agent | Snapshot / отмена ответа. DELETE не выключает микрофон. |
Upstreams ingress в общей сети (внутренние адреса, не публичные URL):
| Маршруты | Upstream |
|---|---|
/core/offer, /core/rtc-capabilities, /core/ice/*, priority 200 |
http://chat-agent_webrtc_ingress:8000 |
/audio/ws, priority 200 |
http://chat-agent_audio_ws_ingress:8000 |
Остальные /core/* |
http://chat-agent_src_core:8000 |
/api/* |
http://chat-agent_src_api_gateway:8100 |
/, /docs/, /presentations/ |
http://chat-agent_src_front:8000 |
Chat /ws и точный voice /ws |
chat-agent_audio_nats:9222, WebSocket без внутреннего TLS; внешний ingress даёт WSS и OAuth |
Сами WebRTC audio packets идут к IP:port выбранной ICE-пары, а не в
POST /core/offer. На 06.10 в работающем ingress настроены
stun:stun.gis-master.ru:3478 и
turn:stun.gis-master.ru:3478?transport=tcp.
TURN credentials остаются на сервере. Браузерный default STUN - тот же
stun:stun.gis-master.ru:3478. Codec определяется согласованным SDP;
код не вызывает setCodecPreferences, поэтому без SDP конкретного peer
нельзя обещать "всегда Opus" или постоянный публичный media port.
WSS start:
{"type":"start","session_id":"<uuid>","format":"pcm16le","channels":1,"sample_rate":48000}
48000 - пример фактической частоты браузера, не фиксированное требование.
Допустимы 8000-96000 Hz. Один binary chunk содержит чётное число байтов,
не более 8192. ASR и ответ чата возвращаются через NATS /ws, а не через
/audio/ws.
Что именно ingress публикует в NATS
Subject: webrtc.komaroff.asr.stream.{session_id}. Формат binary payload:
[4 байта: длина JSON, uint32 big-endian]
[JSON metadata, UTF-8]
[PCM16 little-endian, mono, 16000 Hz]
Один обычный frame - 20 ms: 320 samples / 640 PCM bytes, не считая header.
Metadata: type:"frame", seq, sample_rate:16000, channels:1,
sample_width:2, session_id, stream_id и безопасный trace carrier.
Для type:"end" / "abort" PCM отсутствует. FIFO сохраняет остатки,
padding только у последнего хвоста и меньше 20 ms. Общий модуль не означает
общую очередь между двумя ingress.
Voice и граница inference
На стороне chat/voice broker:
- К GPU:
to.inference.webrtc.asr.audio.{stream_id}, тот же binary envelope с PCM16LE 16 kHz; bridge добавляетphrase_id, своюseqиspeech_samples. - От GPU:
from.inference.webrtc.asr.events.{stream_id}, UTF-8 JSON. - На inference account эти subjects называются соответственно
inference.webrtc.asr.audio.{stream_id}иinference.webrtc.asr.events.{stream_id}. Prefix меняет broker import/remap. - Желаемый leafnode в DRS:
nats://192.168.28.134:37422. В этой сверке не снималась свежая конфигурация удалённого H100; адрес leafnode подтверждён source, не отдельной сетевой пробой.
Bridge возвращает в webrtc.komaroff.asr.text.{session_id}:
{"version":1,"result_type":"final","session_id":"<uuid>","stream_id":"<uuid>","phrase_id":"<id>","revision":2,"text":"<распознанный текст>"}
result_type: partial, final, rejected, error; возможен code,
traceparent, tracestate. Core проверяет session/stream/phrase/revision,
отбрасывает дубликаты и результаты закрытого потока. Только непустой final
запускает диалог. Live-диаризации на этом пути нет. Файловые transcription /
diarization UI на https://voice.gis-master.ru/whisper-trasncription/ и других
сохранённых путях работают отдельно.
Источники аудио: WebRTC ingress, ICE signaling, WSS ingress, PCM module, Core ASR handler, voice bridge, GPU contract, worker source.
2. Диалог: RPC-итог и части ответа идут разными путями
Исходный Mermaid
flowchart LR
B["Браузер"]
C["Core"]
A["Agent: очередь и текущая попытка в памяти"]
P[("PostgreSQL: история, entities, artifacts, workflow state")]
R["Ruby + AsyncGraph: src_langgraph_rb_node"]
L["LLM adapter"]
T["API gateway: пять fixture tools"]
O["Ollama-compatible proxy"]
N["Общий NATS"]
B -->|"POST /core/message: JSON; либо ASR final в Core"| C
C -->|"nats.agent.user123: AgentInboundRequest"| A
A -->|"AgentInboundResponse: JSON reply"| C
C -->|"Итог HTTP JSON для текстового POST"| B
A <-->|"PostgreSQL wire protocol: TCP 5432"| P
A -->|"nats.workflow.run.ruby.node: WorkflowRunRequest"| R
R -->|"WorkflowRunResponse: result + next_runtime"| A
R -->|"nats.llm.user123: LlmRequest mode/input/constraints"| L
L -->|"LlmResponse: response_text / tool_calls / error"| R
R -->|"nats.tools.discover и nats.tools.{name}: JSON RPC"| T
T -->|"Каталог или ToolCallResponse: JSON reply"| R
L -->|"HTTPS POST /api/chat: JSON messages/tools/format"| O
O -->|"NDJSON stream; structured mode - JSON"| L
L -->|"chat.agent.stream: delta + request/attempt/generation/sequence"| A
A -->|"nats.events.user123: response_fragment/request_state/message/client"| N
N -->|"wss://chat-agent.gis-master.ru/ws: NATS + JSON payload"| BНа схеме broker опущен между RPC-парами; прямых HTTP-вызовов Agent → Ruby или Ruby → API gateway в этом пути нет. Ответ на RPC отправляется в одноразовый reply inbox NATS, а не обратно в subject запроса.
Внутренние адреса и request/reply payload
Общий адрес для наших NATS clients: nats://chat-agent_audio_nats:4222.
Неизменённый voice использует совместимый alias
nats://web-rtc-komaroff_audio_nats:4222 того же broker. Это не старый внешний
application host. Во всех адресах намеренно опущены credentials.
| Кто → кто | Subject / адрес | Запрос → ответ |
|---|---|---|
| Core → Agent | nats.agent.user123 |
AgentInboundRequest: text, turn_id, edit, user_id → AgentInboundResponse: ok, result, status, client_handler/client_events, errors. |
| Core → Agent, история | nats.agent.history.user123 |
HistoryGetRequest → HistoryGetResponse. |
| Agent → Ruby | nats.workflow.run.ruby.node |
WorkflowRunRequest: text, runtime(active_workflow_id/pending/context/version) → WorkflowRunResponse: status/result/next_runtime/client events/errors. |
| Ruby → LLM | nats.llm.user123 |
LlmRequest: mode, input, constraints → LlmResponse: ok, data, error. Для dialog_step data содержит response_text и tool_calls(id/name/arguments). |
| Ruby → tools | nats.tools.discover, nats.tools.{tool_name} |
{} → каталог JSON-схем; ToolCallRequest(tool_name/args) → ToolCallResponse(ok/artifact_key/data/error). |
| Ingress → Core | chat.agent.audio.control |
JSON admit/heartbeat/ending/ended/abort → JSON lease/ACK/error. Lease token и epoch - только backend. |
| Core → Agent, snapshot/cancel | chat.agent.requests |
JSON action/request_id/session_id → состояние/результат/отказ. |
| Agent → downstream, cancel | chat.agent.cancel.workflow, .llm, .tools |
JSON с идентификаторами попытки → acknowledgement. |
| Agent ↔ PostgreSQL | chat-agent_src_postgres:5432, database mcp |
PostgreSQL wire protocol, не JSON RPC. |
Typed RPC payload - UTF-8 JSON, с общим envelope
trace_id, correlation_id, request_id, session_id, ts_ms.
NATS headers несут traceparent и execution carrier; это не поля NATS subject.
У control/stream сообщений свои компактные схемы, они не обязаны повторять
весь TraceEnvelope. Дочерний request_id LLM-вызова и внешний request_id хода
не следует считать одним ID; поток дополнительно связывается attempt/generation.
Что LLM передаёт провайдеру
Адрес: https://nats-proxy-ollama.gis-master.ru/api/chat, HTTPS POST.
application/json: model:"qwen3:30b", messages, tools, think:false,
options:{num_ctx:8192,num_predict:2048,temperature:0}.
dialog_step: native tools иstream:true; HTTP-ответ разбирается как NDJSON, сmessage.content,message.tool_calls, завершением и usage.- Структурированные режимы:
format:<JSON Schema>,stream:false; один JSON. - Полный набор tool calls проверяется сервером до исполнения. Потом Ruby выполняет инструменты последовательно и возвращает результаты модели. Обычный разговор обходится одной генерацией, инструментальный - может несколькими.
- Внутренний маршрут proxy → Ollama не исследован заново и не представлен как прямое соединение к конкретному inference host.
Пять tools: search_airports_nearby, get_weather, get_flight_status,
get_current_position, build_route. Сейчас API gateway отдаёт демонстрационные
fixtures; схема не означает подключение к настоящим погодным/авиационным API.
Как ответ доходит до браузера
chat.agent.stream - внутренний pub-sub LLM → Agent, UTF-8 JSON:
request_id, session_id, attempt_id, generation_id, generation_index,
sequence, delta. Agent проверяет владельца/попытку/порядок, накапливает текст
и публикует UI-события в nats.events.user123:
response_fragment: принятый delta и идентификаторы;request_state: накопленный text, state и идентификаторы; активный snapshot раз в секунду;message: итоговый текст и trace;client: проверяемая frontend команда, например изменения карты;- Core публикует там же
asr_resultиaudio_status.
Envelope UI-события: time, service, type, kind, data, name, uid.
Это JSON payload внутри протокола NATS через WSS, не произвольный JSON WebSocket.
Frontend проверяет session/stream либо request/attempt/generation/sequence
до отображения и выполнения команды. Subject user123 всё ещё общий;
фильтрация UI не заменяет отдельные NATS ACL для пользователей.
RPC сохраняет полный результат; частичные события не являются его заменой. После reconnect frontend запрашивает snapshot через Core или историю через Agent. Общий Core NATS не даёт replay этих UI-событий.
Источники диалога: Agent service, request state, RPC contracts, Ruby runtime IO, dialog tool loop, LLM service, HTTP provider, API gateway, frontend event filter, DRS routes, names, account remap.
Граница подтверждения
| Слой | Подтверждено этой сверкой |
|---|---|
| Source | Producers/consumers, обе формы аудио, binary header, JSON contracts, directions, RPC и stream. Git diff e9122e0..e4bfe28 для Agent/LLM/API/Ruby/contracts пуст. |
| Runtime | Insight 06.10.2026: Core и ingress 0c403307, Agent/LLM/API e9122e0, Ruby/Frontend e4bfe28; voice bridge 5d1d959, ASR_MODE=streaming. Также проверены действующие router rules, priorities, upstream ports и OAuth middleware. Не ожидается одинаковый SHA у выборочно собранных сервисов. |
| Внешний inference | Worker source и account remap подтверждены; свежий inspect H100 и внутренних hops Ollama proxy не выполнялся. |
| Поведение / скорость | Эта правка меняет документацию. Результаты C3/C4 относятся к прежним прогонам, а не к повторной приёмке аудио сегодня. |