AAYMA
RU
Войти

Bagyt · Voice Router

Веб-симулятор голосового робота контакт-центра страховой компании: понимает русскую, казахскую и смешанную речь, LLM выбирает один из 40 сценариев…

Доведённый продукт
Команда «plus» 176💬 0 комментариевс 23 сентября 2026 г.

О проекте

Команда: plus
Кейс HackAlem.ai: Коммуникации — Halyk Bank · кейс 2
Стадия: Доведённый продукт

Задача кейса: «Voice Router»: голосовой AI, маршрутизирующий звонки в контакт-центре страховой компании

Суть

Веб-симулятор голосового робота контакт-центра страховой компании: понимает русскую, казахскую и смешанную речь, LLM выбирает один из 40 сценариев по всему диалогу, отвечает голосом, переспрашивает или передаёт оператору, а супервизор видит обоснование, альтернативы и задержки по этапам.

Как устроено

Речь клиента распознаётся ElevenLabs Scribe v2 (с резервом Web Speech API и OpenAI STT), реплика с контекстом диалога через Next.js-прокси уходит в Python-роутер core-llm на OpenRouter (Gemini 2.5 Flash-Lite), который выбирает сценарий с уверенностью и альтернативами. Исполнитель диалога вызывает 31 мок-действие бэкенда, не выполняет необратимых действий без явного «да», при неуверенности уточняет или делает handoff. Ответ озвучивается через Go voice-сервис на ElevenLabs (Flash v2.5 RU, v3 KZ). Есть бесключевой режим «Мок» с прозрачным keyword-baseline и оценка через evaluate.py.

Ключевые решения

  • LLM-маршрутизация по 40 сценариям
  • keyword baseline
  • clarify → handoff
  • подтверждение необратимых действий
  • трассировка решений супервизору
  • замер латентности по этапам

Метрики

104/104 на dev-наборе (по отчёту автора роутера), ~457 мс p50

Особенности

Развёрнуто публично (bagyt.plus) без ключей для жюри; сценарий выбирает LLM по контексту всего диалога вместо intent-классификатора, есть бесключевой мок-режим и консоль супервизора с альтернативами и задержками по этапам.

Стек

Next.js 16 · React 19 · TypeScript · Tailwind 4 · Python · Go 1.26 · Docker Compose · Caddy · nginx

Модели

Gemini 2.5 Flash-Lite (OpenRouter) · ElevenLabs Scribe v2 · ElevenLabs Flash v2.5 · ElevenLabs v3 · Web Speech API · OpenAI STT


Проект создан за 5 часов на финале хакатона HackAlem.ai (23 сентября 2026, Астана). Код — BAITC-Hacks/hack-9dd7b5f3-plus.

README

README.md

Bagyt · Voice Router

*▶ Live demo: bagyt.plus

Бағыт — "direction, route" in Kazakh

A web voice robot for an insurance contact center. The client speaks Russian, Kazakh, or both in one sentence. An LLM layer — not an intent classifier — picks one of 40 scenarios from the whole dialog, the robot answers by voice, and the supervisor sees which scenario was chosen, why, what the alternatives were, and how long every stage took.

Team Plus · HackAlem AI 2026 · Track 09 «Communications» · Case Halyk Bank — Voice Router

Team Plus, all three members, in front of the HackAlem AI wall
Team Plus at HackAlem AI · Astana · 23 September 2026 · who built what · more photos

status review speech router voice frontend backend

▶ Live demo: bagyt.plus · Try it online — jury access, demo video · OpenRouter · ElevenLabs · Docs index · LLM router · Voice service · API contract · Official case

[!IMPORTANT] Status on main (2026-09-23).

  • «LLM» mode — the product. The web app sends every utterance with its dialog context through a same-origin Next.js proxy to the Python router core-llm/ on OpenRouter (google/gemini-2.5-flash-lite; its author reports 104/104 on the dev set at ~457 ms p50). The client's voice is recognized by ElevenLabs Scribe v2 and every reply is spoken by ElevenLabs (Flash v2.5 in Russian, v3 conversational in Kazakh) through the Go voice service voice/. Needs an OpenRouter key and an ElevenLabs key: NEXT_PUBLIC_API_MODE=core docker compose --profile llm --profile voice up --build.
  • «Мок» mode — keyless. The same UI, dialog executor, 31 mock backend actions, supervisor trace and quality metrics, with a transparent keyword baseline instead of the LLM and browser speech instead of ElevenLabs: docker compose up --build and nothing else.
  • In progress 🚧: the Go backend («Бэкенд» mode), streaming voice in the web app (the voice service's /ws/voice gateway), persistence and a public deployment. Every capability below is marked ✅ / 🚧 / 📋.

Кратко по-русски. Bagyt — веб-симулятор голосового робота контакт-центра страховой компании (вымышленная Saqta Insurance из стартового кита Halyk Bank). Клиент говорит по-русски, по-казахски или вперемешку; робот выбирает один из 40 сценариев с учётом всего диалога, отвечает голосом, переспрашивает, когда не уверен, передаёт разговор оператору с контекстом и не выполняет необратимых действий без явного «да». Супервизор после каждой реплики видит сценарий, уверенность, обоснование, альтернативы и задержку по этапам. Проверка без ключей: docker compose up --build → http://localhost:3000/admin, режим «Мок» (прозрачный детерминированный baseline вместо LLM). С ключом OpenRouter в core-llm/.env: NEXT_PUBLIC_API_MODE=core docker compose --profile llm up --build — режим «LLM» и продуктовый роутер core-llm/ (Gemini 2.5 Flash-Lite через OpenRouter, 104/104 на dev-наборе по отчёту автора, ~457 мс p50). Голос в режиме «LLM» — ElevenLabs: распознавание Scribe v2 (language_code=kk понимает русский, казахский и смешанную речь) и озвучка Flash v2.5 (RU) / v3 conversational (KZ); нужен ELEVENLABS_API_KEY в корневом .env и NEXT_PUBLIC_API_MODE=core docker compose --profile llm --profile voice up --build. Живая версия: https://bagyt.plus.

From the hackathon floor

Photos from the day: the entrance, the hall, the team at work, picking the case
The queue at the entrance of the EXPO hall
The morning queue at the entrance
The hackathon hall, Purple Zone C
The hall
A qairuhub cap and HackAlem stickers
Swag
Team Plus at work at the desk
Building Bagyt
A qairuhub cap
qairuhub
The hackathon platform: Track 09 Communications, the Voice Router case
Picking the case: Track 09 «Коммуникации» → Voice Router

Jury quick path

The organizers askShort answerDetails
What does it solve?Voice robots mis-route live speech — topic switches, requests between two scenarios, Russian↔Kazakh switching inside a phrase — because the scenario is picked by an encoder intent classifier. Bagyt picks it with an LLM layer over the dialog context, asks or hands off when unsure, and shows the supervisor why.§1
How do I run it?Live: bagyt.plus — just open it: nothing to install, no login, no keys (they live on the server). Locally without keys: docker compose up --build → http://localhost:3000/admin (engine «демо»). Locally with the LLM router and ElevenLabs voice: the .env files from our Google Drive folder (or your own keys) in core-llm/.env (OpenRouter) and .env (ElevenLabs), then NEXT_PUBLIC_API_MODE=core docker compose --profile llm --profile voice up --build. Without Docker: cd frontend && npm ci && npm run dev.§7
Which technologies?Next.js 16 · React 19 · TypeScript · Tailwind 4 · Python LLM core on OpenRouter (Gemini 2.5 Flash-Lite) · Go 1.26 voice service on ElevenLabs (Scribe v2 STT, Flash v2.5 / v3 conversational TTS) · browser Web Speech API as the keyless fallback§5
How do I verify it?A 10-step walkthrough in the web app (RU, KZ, mixed, multi-intent, topic return, confirmation, clarify → handoff), the same phrases in «LLM» mode, the official evaluate.py and the unit tests§8

Try it online

🌐 Live appbagyt.plus
🔑 Jury accessOpen access — no login, no password. The call screen (/call) and the supervisor console («Аналитика», /admin) are open to everyone
🎬 Demo video (3 min)Google Drive folder
🔐 Keys for a local runThe .env files (OpenRouter, ElevenLabs) are in the same Google Drive folder, shared for the jury — never in this repo. Put them in place (§7.6) and run NEXT_PUBLIC_API_MODE=core docker compose --profile llm --profile voice up --build
🧪 Test clientsSay or type a phone when the robot asks: +77010000007 Sergey Popov (claim under review) · +77010000001 Arman Tulegenov (OGPO + CASCO) · +77010000003 Yerlan Omarov (policy to renew) — all synthetic, §8.3

About API keys. None are in this repository, on purpose. bagyt.plus runs with our OpenRouter and ElevenLabs keys in private env files on the server (§7.5), so nothing is needed on your side. For a full local run, take the .env files from the Google Drive folder next to the demo video; without them, «Мок» works with no keys at all.

Test it in 60 seconds:

  1. Open bagyt.plus → «Аналитика» (the conversation on the left, the robot's reasoning on the right). No login needed.
  2. The live app keeps the OpenRouter and ElevenLabs keys on the server — you need no keys of your own. Turn the replies' sound on with the speaker icon next to the text field.
  3. Click the mic, say «Хочу продлить ОГПО и добавить сына» and click again to send — or type it. Then try «Кеше аварияға түстім, но я не виноват, виновник у вас застрахован».
  4. After each phrase, read the right column: the scenarios with confidence, why, the rejected alternatives, what the robot did and how many milliseconds each stage took.
  5. More: the 10-step walkthrough in §8 · no key? docker compose up --build gives the keyless «Мок» build (§7.1).

Contents

  1. Problem and users
  2. What is implemented
  3. How it works
  4. Architecture
  5. Technologies
  6. Requirements
  7. Install and run
  8. How to verify
  9. Results
  10. Data and integrations
  11. Design
  12. Decision log
  13. Limitations and known issues
  14. Roadmap
  15. Team
  16. Documentation map
  17. Maintaining this README

1. Problem and users

The problem (from the case). Voice robots have learned to speak and listen well, but the layer that chooses the conversation scenario is usually still an encoder intent classifier trained on fixed phrasings. It breaks exactly where live speech starts: the client changes topic mid-dialog, asks something on the border of two scenarios, or switches from Russian to Kazakh inside one sentence. Every wrong choice is a transfer to an operator or a lost client — and a pause longer than a second already sounds like a dropped call.

Who it is for.

UserWhat they needWhat Bagyt gives them
Contact-center clientExplain the problem in their own words — in Russian, Kazakh or both; change topic; get it solved without an operatorScenario chosen from the whole dialog, not from keywords; a topic stack that returns to interrupted questions; answers grounded in the client's own data; a voice reply in the client's language
Supervisor / operatorSee which scenario was chosen, why, where the robot hesitated or failed, and how fast it wasA trace after every turn — scenario, confidence, reason, alternatives, policy verdict, actions, per-stage latency — plus the official quality metrics on the dev set

Domain. The organizer's synthetic starter kit: Saqta Insurance, a fictional Kazakh non-life insurer (OGPO motor liability, CASCO, DMS health, travel, property, accident) — 40 scenarios + 3 system intents, 43 slots, 31 backend actions, a knowledge base and 11 test clients. "Today" in the data is 2026-10-01.

Our answer in one line. Replace the classifier with an LLM routing layer that reads a compact index of every scenario — its meaning, Russian and Kazakh cues, and not_this_if boundaries — together with the dialog context, and answers with scenario IDs and confidence percentages only. A deterministic policy, not the model, then decides whether to act, ask, or hand the call to a human, and assembles the explanation the supervisor sees.


2. What is implemented

Legend: ✅ works on main · 🚧 in progress, not on main yet · 📋 planned · ❌ cut · ⏳ to be filled in

Client conversation

CapabilityStatusWhere
ElevenLabs speech recognition, hands-free: click the mic and speak — it stops by itself after a pause (VAD) or on a second click, with live partial transcripts and a mic-level ring → Next.js POST /api/stt → voice service POST /api/voice/stt → Scribe v2 with language_code=kk (Russian, Kazakh and mixed speech). The recognizer in «LLM» mode; in «Мок» the browser's Web Speech recognizes, and its network errors switch to ElevenLabs automatically✅ needs ELEVENLABS_API_KEY + --profile voiceapi/stt/route.ts, voice/ — §5.2
ElevenLabs voice replies in «LLM» mode: Next.js POST /api/tts → voice service → Flash v2.5 for Russian, v3 conversational for Kazakh, the Russian voice for English greetings. Each reply keeps its MP3 for the conversation: play/pause, seekable waveform, elapsed/duration, download✅ needs ELEVENLABS_API_KEY + --profile voiceapi/tts/route.ts, audio-message.tsx
Browser voice (keyless): Web Speech API in, speechSynthesis out — «Мок» mode, and the fallback when the voice service is unavailable✅frontend/src/lib/voice.ts
Text input as the fallback channel✅conversation-panel.tsx
One RU / ҚАЗ switch in the header for the interface, speech recognition and the reply, saved in the browser; the header also shows the engine («демо» or «LLM») and has «Новый диалог»✅lib/ui-language.ts, console-shell.tsx
Language detection ru / kk / mixed; answer in the client's dominant language, keep the session language on code-switched turns✅mock/router.ts, mock/engine.ts
Client identification by phone / IIN against mock_backend.json; spoken numbers → digits in RU and KZ («плюс жеті жеті жүз бір…» → +7701…)✅mock/router.ts (wordsToDigits, extractSlots)

Routing and dialog logic

CapabilityStatusWhere
LLM router — the product: system prompt + a compact index of the catalog (scenarios.index.txt) + dialog context → ID:PERCENT pairs → decision policy. CLI, dev-set evaluation, dialog replay with context, model benchmark✅core-llm/router.py, core-llm/prompt.md, core-llm/scenarios.index.txt
«LLM» mode in the web app: browser → same-origin proxy POST /api/core-route → core-llm/server.py (POST /api/route, port 8090, private) → OpenRouter. The core's verdict (route / clarify / handoff / out_of_scope / goodbye) is authoritative; the browser executor then collects slots and runs the demo actions. The key never reaches the browser, and there is no silent fallback to the mock✅ needs OPENROUTER_API_KEYapi/core-route/route.ts, lib/core-router.ts, core-llm/server.py
The same routing inside the Go turn API (SSE)🚧integration notes in core-llm/README.md
Router output contract used by the UI: ranked scenarios[] with confidence + reason, alternatives[], language, slots, is_continuation, model, tier✅frontend/src/lib/contract.ts
Keyless mock router: token overlap with the catalog's ru/kk examples + hand-written cues that mirror not_this_if; labelled «Мок» in the UI✅mock/router.ts
Multi-intent: split the utterance; urgent scenarios first (SC11, SC15, SC38), then in spoken order✅core-llm/prompt.md, mock/router.ts, mock/engine.ts
Decision policy: ≥ 0.75 run · 0.45–0.75 clarify · < 0.45 twice → service menu (operator on request) · out-of-scope · goodbye (UI); route ≥ 60 % · extra intents ≥ 20 % (core-llm)✅mock/engine.ts (decide), core-llm/router.py (Router.decide)
Topic stack: park the interrupted scenario, come back to it when the new one is done✅mock/engine.ts
Continuation: an answer to a pending slot or a yes/no question is not re-routed✅mock/engine.ts, core-llm/prompt.md (CONTEXT rules)
Greetings without a request — SYS_GREETING (status: greeting, policy action greeting), an application system intent; the official 40 scenarios are unchanged. Replies «Здравствуйте, чем могу помочь?» / «Сәлеметсіз бе, қалай көмектесе аламын?» / “Hello, how can I help you?” (for “Hello”, “Hi”, “Good morning”) through the same TTS path; keeps the active scenario, the pending slot or confirmation and the queue, and does not count as an unclear turn. «Здравствуйте, хочу продлить полис» still routes to SC27✅core-llm/prompt.md, core-llm/router.py, mock/engine.ts
Slot extraction: phone, IIN, policy / claim number, plate, city, dates relative to 2026-10-01, doctor, country, …✅mock/router.ts
Scenario executor: identify → one question per missing slot → actions → read back → explicit «да» → execute → close✅mock/engine.ts (execute, confirmFlow)
All 31 backend actions of actions.json as mocks over mock_backend.json + knowledge_base.json✅mock/actions.ts
The 9 irreversible actions run only after a preview and an explicit confirmation✅mock/actions.ts, mock/engine.ts
Handoff to one of 6 operator queues with a context summary✅mock/engine.ts
Hybrid fast path with a measured latency gain📋—
Speculative routing on partial transcripts📋—

Supervisor console (/admin)

CapabilityStatusWhere
Conversation next to «what the robot understood»: scenario, confidence, reason, live candidates, the catalog's boundary rules✅admin/page.tsx, components/app/admin/decision.tsx
Dialog state: client, awaited slot or confirmation, known data, actions (read / preview / executed)✅components/app/admin/dialog.tsx
Per-stage latency waterfall against the 1.5 s target✅ (mock timings are simulated)components/app/admin/speed.tsx
Session metrics: turns, average confidence, clarifications, handoffs, median response time✅ (in memory)lib/api.ts (computeStats)
Routing quality card (the official evaluate.py metrics on the dev set in one click)❌ removed from the console in bc402c6 — run the metrics from the CLI (§8.4)components/app/admin/quality.tsx (unused)
Trace JSON of the last turn in the dataset README format✅components/app/admin/shared.tsx (readmeTrace)
Turn log✅components/app/admin/turn-log.tsx
History across sessions and error statistics over time (PostgreSQL)📋—
Catalog editing without developers📋—

Backend and infrastructure

CapabilityStatusWhere
Go HTTP service with GET /health✅backend/cmd/server/main.go
Go turn API behind «Бэкенд» mode: POST /api/session, POST /api/turn (SSE), trace, supervisor stats, scenarios, POST /api/eval/run🚧 contract fixeddocs/API_CONTRACT.md
LLM access: OpenRouter in core-llm ✅; OpenAI / NVIDIA / any OpenAI-compatible provider behind one Go interface 🚧✅ · 🚧core-llm/router.py
Voice service voice/ (Go): ElevenLabs Scribe v2 (streaming and batch) + per-language TTS; HTTP /api/voice/stt and /api/voice/tts used by the web app; a real-time WebSocket gateway /ws/voice with barge-in, speculative replies and cached fillers; per-call logs and p50/p95 latency stats✅ HTTP in the web app · 🚧 /ws/voice not wired to the web app yetvoice/, voice/README.md — §5.2
geko.sh Seta STT / Tokay TTS (KZ-first)📋—
PostgreSQL persistence of turns and traces📋—
One command: docker compose up --build (web app, keyless); --profile llm adds the core-llm service, --profile voice the ElevenLabs voice service✅docker-compose.yml, frontend/Dockerfile, core-llm/Dockerfile, voice/Dockerfile
Tests: the core-llm HTTP adapter and greetings; the frontend's core adapter and dialog engine; the voice service against fake ElevenLabs servers (no credits)✅core-llm/test_server.py, frontend/scripts/test-core.cjs (npm test), voice/ (go test ./...)
Deployment: bagyt.plus on a VPS — Nginx with Let's Encrypt HTTPS in front of the full Compose stack (--profile server); keys in private server env files✅deploy/, §7.5
Phone channel: Asterisk AudioSocket (a Kazakh SIP number) and Twilio Media Streams transports in the voice service, with a setup guide✅ code · ⏳ live numbervoice/transport/, voice/deploy/README.md
Emotion detection and tone adaptation❌ cut—

3. How it works

3.1 One turn, from voice to answer

  1. Hear. The client clicks the mic and speaks. In «LLM» mode the recording goes through /api/stt and the voice service to ElevenLabs Scribe v2 with language_code=kk, which handles Russian, Kazakh and mixed speech — a second click sends it. In «Мок» the browser's Web Speech API transcribes (ru-RU or kk-KZ) and stops on a pause. The moment the client stops speaking is client_t0 — latency is measured from it.
  2. Triage. Language ru / kk / mixed, normalization (spoken numbers → digits), urgency, and the split of a multi-intent utterance into parts.
  3. Route. In «LLM» mode the utterance and its dialog context — previous turns with their scenarios, the active scenario, the bot's last reply — go through the same-origin proxy /api/core-route to core-llm. The model answers with nothing but ID:PERCENT pairs (about 6 tokens, which keeps routing near 450 ms); the core applies its policy, and the explanation shown to the supervisor is assembled deterministically from the scenario names, the percentages and the verdict. In «Мок» mode the keyword baseline returns the same shape. The console shows the candidates and their confidence.
  4. Decide. Deterministic policy code — not the model — turns the ranking into an action: run, continue, clarify, handoff, out_of_scope or goodbye. Urgent scenarios go first; the rest wait on the topic stack.
  5. Act. A per-scenario state machine driven by scenarios.json: identify the client, ask for the next missing slot, call mock actions, read back and wait for an explicit «да» before anything irreversible, close, then resume the interrupted topic.
  6. Answer. One or two sentences built from the scenario's response templates and real action results — never invented facts — streamed sentence by sentence.
  7. Speak. TTS in the reply language: in «LLM» mode ElevenLabs Flash v2.5 for Russian and v3 conversational for Kazakh — the only realtime ElevenLabs model that speaks it — with a replayable, downloadable recording; browser speech in «Мок». End-to-end latency = end of the client's speech → first audio of the answer.
  8. Explain. The turn ends with a trace in the dataset README format; /admin renders it next to the conversation.

3.2 The LLM router in one call

$ python3 core-llm/router.py "Хочу продлить ОГПО и заодно добавить в него сына"
SC27:95 SC04:95   (primary SC27, intents [SC27, SC04], status route, ~450 ms)

One line out of the model: two requested scenarios (renewal SC27, add a driver SC04), both at 95 %, in spoken order. The policy turns it into status=route with two intents; an unsure answer (< 60 %) becomes clarify, and SYS_OUT_OF_SCOPE / SYS_GOODBYE / SC37 become out_of_scope / goodbye / handoff. Example from core-llm/README.md.

3.3 A real trace from the web app

The case's own example, typed into the web app in mock mode on main (produced by the same engine the browser runs):

{
  "turn": 1,
  "transcript": "Здравствуйте, я вчера оплатил, деньги списались, а заказ не подтвердился… а, и ещё, адрес доставки поменять надо",
  "language": "ru",
  "scenarios": [
    { "scenario_id": "SC30", "confidence": 0.94, "reason": "Charged but policy not issued — в реплике есть признак «деньги списал»" },
    { "scenario_id": "SC29", "confidence": 0.82, "reason": "Update contact details — похоже на примеры этого сценария из каталога" }
  ],
  "alternatives": [
    { "scenario_id": "SC33", "confidence": 0.34 },
    { "scenario_id": "SC31", "confidence": 0.17 },
    { "scenario_id": "SC12", "confidence": 0.15 }
  ],
  "reason": "Charged but policy not issued — в реплике есть признак «деньги списал»",
  "slots": { "incident_date": "2026-09-30", "contact_field": "address" },
  "actions": [],
  "latency_ms": { "stt": 43, "triage": 30, "router": 327, "policy": 16, "executor": 0, "response": 140, "tts_first_audio": 0, "total": 556 }
}

How to read it:

  • Two requests in one breath → two scenarios, in spoken order: SC30 "Charged but policy not issued" (0.94) and SC29 "Update contact details" (0.82). The policy verdict is run SC30 with SC29 parked on the stack ("stack": ["SC29"]).
  • Alternatives show what was considered and rejected: SC33 office addresses (0.34), SC31 payment methods (0.17), SC12 victim claim (0.15).
  • Slots are already extracted: «вчера» → incident_date: 2026-09-30 (relative to the dataset's "today", 2026-10-01); «адрес … поменять» → contact_field: address.
  • The answer acknowledges the second request and asks for the phone number, because SC30 requires identification: «Поняла, второй вопрос (обновление контактных данных) тоже разберём. Разберёмся. Подскажите, пожалуйста, ваш номер телефона.»
  • latency_ms: in mock mode the stage timings are simulated delays; in the browser tts_first_audio and total are replaced by real measurements (end of speech → first audio).

4. Architecture

4.1 System overview

flowchart LR
  subgraph B["Browser · Next.js 16 app (frontend/)"]
    direction TB
    CALL["Client screen /call<br/>mic + text + audio player"]
    ADMIN["Supervisor console /admin<br/>decision · dialog · speed · quality"]
    STORE["Conversation store + dialog executor<br/>lib/store.ts · lib/mock/engine.ts"]
    VOICE["Browser speech (keyless)<br/>Web Speech API · speechSynthesis"]
    MOCK["Keyword baseline (keyless)<br/>lib/mock/router.ts"]
  end

  subgraph N["Next.js server routes"]
    direction TB
    PROXY["Route /api/core-route<br/>LLM proxy"]
    STTR["Route /api/stt<br/>recording → text"]
    TTSR["Route /api/tts<br/>reply → MP3"]
  end

  CORE["core-llm/server.py · Python<br/>prompt.md + scenarios.index.txt<br/>→ ID:PERCENT pairs → policy"]
  VSVC["voice/ · Go service, profile voice<br/>/api/voice/stt · /api/voice/tts<br/>+ /ws/voice gateway · phone transports"]

  subgraph G["Go backend (backend/) · in progress"]
    API["POST /api/turn → SSE events"]
  end

  subgraph X["External providers · keys only in server-side env files"]
    direction TB
    LLM["OpenRouter<br/>google/gemini-2.5-flash-lite"]
    EL["ElevenLabs<br/>Scribe v2 · Flash v2.5 · v3 conversational"]
  end

  KIT[("data/ · official starter kit<br/>40 scenarios · 43 slots · 31 actions<br/>knowledge base · 11 test clients")]

  CALL --> STORE
  ADMIN --> STORE
  STORE <--> VOICE
  STORE -->|mode Мок| MOCK
  STORE -->|mode LLM| PROXY
  STORE -->|voice in| STTR
  STORE -->|voice out| TTSR
  STORE -.->|mode Бэкенд| API
  PROXY --> CORE
  CORE --> LLM
  STTR --> VSVC
  TTSR --> VSVC
  VSVC --> EL
  MOCK --> KIT
  STORE -->|slots, actions, replies| KIT

One conversation store and one dialog executor drive both screens. The build-time mode NEXT_PUBLIC_API_MODE (mock / core / real) picks the router: the keyword baseline in the browser, the Python LLM core behind a same-origin proxy, or the Go backend (in progress). Every mode produces the same typed event stream, so the UI does not know the difference. Voice goes through two more same-origin routes, /api/stt and /api/tts, to the Go voice service, which alone holds the ElevenLabs key; in Compose it runs with VOICE_BRAIN=echo, so routing stays in core-llm. API keys stay on the server side (core-llm/.env, the root .env) and never reach the browser. The Go backend will reuse the core's two text files and its policy, as described in core-llm/README.md.

4.2 One turn as an event stream

sequenceDiagram
  autonumber
  actor C as Client
  participant UI as Browser (/call or /admin)
  participant E as Dialog engine (browser today, Go backend later)
  participant S as STT / TTS
  participant L as LLM router (core-llm via /api/core-route)
  C->>UI: clicks the mic, speaks RU / KK / mixed, pauses
  UI->>E: turn with text (or audio_base64) and client_t0
  E-->>UI: turn.start
  E->>S: audio (real mode)
  S-->>E: transcript + language
  E-->>UI: stt.final (text, language, ms)
  E-->>UI: triage (language, urgent, parts, ms)
  E->>L: utterance + dialog context + catalog index
  L-->>E: scenario IDs with confidence percentages
  E-->>UI: router.candidates (live), then router.decision
  Note over E: policy (thresholds, urgent first, topic stack), then executor (identify, slots, mock actions, confirmation)
  E-->>UI: policy, action (read / preview / execute), state
  E-->>UI: response.delta (streamed), then response.final
  E->>S: reply text (real mode)
  E-->>UI: tts.audio (base64 audio or browser_tts)
  E-->>UI: turn.done (trace, latency_ms)
  UI->>C: voice reply + trace on /admin
EventPayloadWhat it drives in the UI
turn.startturn, session_id, t0a new row in the turn log
stt.partial / stt.finaltext, language, mslive transcript, language badge
triagelanguage, urgent, parts[], normalized, msparts of a multi-intent utterance
router.candidates (0..n)candidates[], partiallive confidence bars
router.decisionRouterDecision, msscenario, reason, alternatives
policyPolicyVerdict, msrun / continue / clarify / handoff / out_of_scope / goodbye
action (0..n)ActionCall — mode read / preview / executethe actions list
stateDialogStatedialog card: client, awaited slot, stack
response.delta / response.finaltext, language, msthe streamed reply bubble
tts.audioaudio_base64 + mime, or browser_tts: trueplayback
turn.doneTrace, latency_mstrace JSON, speed waterfall, metrics
errormessageerror banner; ends the turn

In «Мок» and «LLM» modes these events are produced in the browser; the Go backend will stream the same events over POST /api/turn → text/event-stream, one JSON event per data: line. Contract: docs/API_CONTRACT.md; types: frontend/src/lib/contract.ts.

4.3 Decision policy

flowchart TD
  IN(["Router decision:<br/>ranked scenarios + confidence"]) --> Q0{"Client says goodbye?"}
  Q0 -- yes --> GB["goodbye<br/>SYS_GOODBYE"]
  Q0 -- no --> Q1{"Yes / no to a<br/>pending confirmation?"}
  Q1 -- yes --> CF["continue<br/>execute or cancel the preview"]
  Q1 -- no --> Q2{"Out of scope?<br/>loans, life insurance, weather"}
  Q2 -- yes --> OOS["out_of_scope<br/>polite refusal + what we can do"]
  Q2 -- no --> Q3{"Answers the<br/>pending slot?"}
  Q3 -- yes --> SL["continue<br/>fill the slot, no re-routing"]
  Q3 -- no --> Q4{"Top confidence ≥ 0.75?"}
  Q4 -- yes --> RUN["run<br/>urgent first: SC11, SC15, SC38<br/>other intents → topic stack"]
  Q4 -- no --> Q5{"≥ 0.45 and not<br/>SYS_UNCLEAR?"}
  Q5 -- yes --> CLR["clarify<br/>one question, top-2 options"]
  Q5 -- no --> Q6{"Second low-confidence<br/>turn in a row?"}
  Q6 -- no --> CLR
  Q6 -- yes --> HO["service menu<br/>operator on request (SC37)"]

In «LLM» mode the core's verdict is authoritative (route_status: route / clarify / handoff / out_of_scope / goodbye; a scenario counts as requested from ROUTER_THRESHOLD=60 %, extra intents from 20 %). The thresholds in the diagram (CONFIDENCE_RUN = 0.75, CONFIDENCE_CLARIFY = 0.45 in contract.ts, from the dataset README) apply to the keyword baseline. An explicit request for a human (SC37) goes straight to handoff. The model proposes, code disposes: the router never executes anything by itself.

4.4 Scenario executor

stateDiagram-v2
  [*] --> Identify: scenario requires identification
  [*] --> Slots: no identification needed
  Identify --> Identify: phone not found, ask again once
  Identify --> Slots: find_client ok
  Slots --> Slots: ask the next missing slot, one question per turn
  Slots --> Actions: all required slots known
  Actions --> Preview: scenario has an irreversible action
  Actions --> Close: read-only scenario
  Actions --> Handoff: scenario handoff rule
  Preview --> Execute: client says yes
  Preview --> Close: client says no, nothing is done
  Execute --> Close
  Close --> Slots: resume the interrupted topic from the stack
  Close --> [*]: stack empty
  Handoff --> [*]

Slots already known from the identified client (phone, IIN, e-mail, city, the matching policy or claim) are filled automatically, so the robot does not ask for what it already knows.

4.5 Dialog state

Kept per session and sent to the UI after every turn (state event):

{
  "session_id": "s_ab12", "language": "ru",
  "client_id": "C007", "client_name": "Sergey Popov",
  "active_scenario": "SC17", "stack": ["SC27"],
  "slots": { "phone": "+77010000007" },
  "awaiting": { "kind": "slot", "slot": "claim_number" },
  "low_conf_streak": 0, "turn": 3, "ended": false
}

awaiting is null, { "kind": "slot", "slot": … } or { "kind": "confirmation", "action": … }.

4.6 Three modes, one contract

«Мок» (default)«LLM» — the product«Бэкенд»
Chosen at build timeNEXT_PUBLIC_API_MODE=mock (default)NEXT_PUBLIC_API_MODE=coreNEXT_PUBLIC_API_MODE=real
Routerkeyword / cue baseline in the browser (mock-lexical)core-llm via /api/core-route → OpenRouter (google/gemini-2.5-flash-lite)Go backend, POST /api/turn (SSE)
Policy, slots, actions, repliesbrowser dialog executor over the starter kit (frontend/src/lib/mock/)the core's verdict + the same browser executorGo backend
STTWeb Speech API (Chrome / Edge)ElevenLabs Scribe v2 via /api/stt (default) or Web SpeechElevenLabs Scribe v2 Realtime, streamed
TTSbrowser speechSynthesisElevenLabs via /api/tts: Flash v2.5 (RU) · v3 conversational (KZ); browser speech as the fallbackElevenLabs over WebSockets
KeysnoneOPENROUTER_API_KEY in core-llm/.env; ELEVENLABS_API_KEY in .env for voiceprovider keys in .env
Rundocker compose up --buildNEXT_PUBLIC_API_MODE=core docker compose --profile llm --profile voice up --build—
Status✅✅🚧

4.7 Why this is not an intent classifier — and not hardcoded

  • No encoder, no training. Nothing in this repo is a model trained to map utterances to intents. The product router is a generative LLM that reads a compact index of each scenario — its meaning, Russian and Kazakh cues, and not_this_if boundaries (e.g. "approved, but too little" → SC19 dispute, not SC17 status) — together with the dialog context, and answers with scenario IDs and confidence percentages (core-llm/).
  • The model proposes, code decides. Thresholds, clarification, handoff and confirmation are deterministic policy, so a wrong or unsure model turns into a question or a human — not into a wrong action. The supervisor's explanation is assembled from the catalog, the percentages and the thresholds, so it always matches what the policy actually did.
  • The catalog is data. Scenarios, slots, actions and reply templates are read from the starter-kit JSON; the LLM reads a hand-made index of the same catalog. A new scenario means a JSON entry and an index line — no code change and no retraining.
  • The jury's utterances are not in the repo. dev_utterances.json is read only by the evaluators (lib/eval.ts, scripts/eval-mock.ts, core-llm/router.py --eval), never by a router at runtime.
  • Honest about tuning. Both the mock's cue lists and the LLM prompt were refined against the dev set, so both dev-set scores are reported as in-sample (§9.1). The mock is labelled «Мок» everywhere and is not our answer to the case.

4.8 Repository map

.
├── frontend/                    Next.js 16 app — the product UI; runs end-to-end in mock mode
│   ├── src/app/                 routes: / (landing) · /call (client) · /admin (supervisor) · api/core-route (LLM) · api/stt + api/tts (voice)
│   ├── src/lib/contract.ts      frontend ⇄ backend event and trace contract (source of truth)
│   ├── src/lib/core-router.ts   adapter: the core-llm result → the UI's RouterDecision
│   ├── src/lib/store.ts         conversation store shared by /call and /admin
│   ├── src/lib/api.ts           mock ⇄ real switch, SSE reader, session stats
│   ├── src/lib/voice.ts         browser STT / TTS / audio recorder
│   ├── src/lib/eval.ts          port of data/evaluate.py for the /admin quality card
│   ├── src/lib/mock/            router.ts · engine.ts (policy + executor) · actions.ts (31 mock actions)
│   ├── src/components/          app/ (console) · landing/ · ui/ (coss ui) · block/ (ObsidianUI)
│   ├── src/data/                copies of ../data/*.json used by the browser
│   └── scripts/                 eval-mock.ts (dev set) · replay-dialogs.ts (sample dialogs) · test-core.cjs (npm test)
├── core-llm/                    LLM router core (Python, standard library only)
│   ├── prompt.md                system prompt: output format, multi-intent, system intents, context rules
│   ├── scenarios.index.txt      compact catalog index: meaning · ru cues · kk cues · ≠ boundaries
│   ├── router.py                Router class · CLI · --eval · --dialogs · --bench
│   ├── server.py                private HTTP adapter: GET /healthz, POST /api/route (port 8090)
│   └── test_server.py           unit tests for the adapter
├── voice/                       ElevenLabs voice service (Go): STT / TTS, /ws/voice gateway, phone, logs
│   ├── elevenlabs/              client: Scribe v2 Realtime + batch STT, TTS over HTTP and WebSockets
│   ├── speech/                  per-language model routing, on-disk phrase cache, LLM-delta chunker
│   ├── agent/ · brain/          real-time conversation engine · reply brains (backend / OpenRouter / echo)
│   ├── transport/ · deploy/     browser WebSocket, Asterisk AudioSocket, Twilio · phone setup
│   └── server/ · cmd/           HTTP routes (/api/voice/stt, /api/voice/tts, /api/voice/stats …) · binaries
├── backend/                     Go service (net/http): GET /healthz + /health, Dockerfile; turn API in progress
├── docker-compose.yml           web app, Go backend, PostgreSQL; profiles "llm" (core-llm), "voice" (ElevenLabs), "server" (both)
├── data/                        official starter kit (read-only) + our predictions_mock.json
├── docs/                        case, analysis, PRD, SPEC, API contract, tasks, pitch, progress, research, design
├── DESIGN.md                    design-system reference
├── THIRD_PARTY.md               every third-party component, model, dataset and API we use
├── .env.example                 backend variables, keyless defaults
└── AGENTS.md · CLAUDE.md        rules for AI coding agents working in this repo

5. Technologies

LayerTechnologyVersionUsed forStatus
FrontendNext.js (App Router), React, TypeScript16.3.6 · 19.2.8 · 5Landing, client screen, supervisor console; server routes /api/core-route, /api/stt, /api/tts✅
UITailwind CSS · coss ui on Base UI (@base-ui/react) · ObsidianUI blocks · lucide-react · GSAP4 · 1.8 · — · — · 3.15Design system, accessible primitives, icons, landing text stream✅
TypographyGeist, Geist Mono via next/font (latin + cyrillic)—Text; mono only for numbers and IDs✅
LLM routerPython 3 (standard library only) · OpenRouter · google/gemini-2.5-flash-lite by default (any model via ROUTER_MODEL)—Scenario choice: ID:PERCENT pairs, temperature=0, reasoning off, provider sorted by latency; a private HTTP adapter for the web app — §5.1✅
LLM, Go backendOne OpenAI-compatible client: OpenAI (gpt-4.1-mini), NVIDIA Build (meta/llama-3.3-70b-instruct), OpenRouter / Groq / Gemini / Ollama via LLM_BASE_URL; mock—The same routing inside the turn API🚧
Browser speechWeb Speech API (SpeechRecognition), speechSynthesis, MediaRecorderbuilt inKeyless STT / TTS; audio capture✅
SpeechElevenLabs Scribe v2 (scribe_v2_realtime, language_code=kk — also transcribes Russian and mixed speech) · eleven_flash_v2_5 (RU TTS) · eleven_v3_conversational (KZ TTS)—Recognition and voice replies — §5.2✅ web app («LLM») · streaming gateway in voice/
Voice serviceGo · github.com/coder/websocket1.26ElevenLabs STT / TTS behind /api/voice/*, the /ws/voice gateway, phone transports, latency logs✅
Speech, alternativegeko.sh Seta seta-kk-ru-v2 (STT) · Tokay tokay-kk-v1 (TTS)—KZ-first recognition with code-switching📋
BackendGo, standard net/http1.26.5API gateway and the turn pipeline✅ health · 🚧 API
StoragePostgreSQL—Turns, traces, statistics📋
EvaluationPython 3 — official data/evaluate.py; TypeScript scripts run with tsx—Dev-set accuracy, sample-dialog replay✅
Testsnode:test + the TypeScript compiler (npm test) · Python unittest · go test with fake ElevenLabs servers—Core adapter, dialog engine, the core's HTTP adapter, the voice service✅
InfrastructureDocker Compose (images node:22-bookworm-slim, python:3.13-slim, Go for voice/) · a VPS with Nginx + Let's Encrypt for bagyt.plus—One-command run · hosting✅ · ✅

Every third-party component, model, dataset and API with its license: THIRD_PARTY.md.

5.1 OpenRouter — the routing LLM

OpenRouter is the one gateway to the LLM, used in two places: the scenario router core-llm/ (behind «LLM» mode and the CLI) and the voice service's own brain (voice/brain/openrouter.go). One key, any model, provider routing and a cost report for every call.

FeatureHow Bagyt uses itWhere
Any model behind one APIgoogle/gemini-2.5-flash-lite by default; any OpenRouter model id through ROUTER_MODEL (router) or OPENROUTER_MODEL (voice service). 15+ models were compared on the same dev set with router.py --bench (§9.1)core-llm/router.py
Provider routingprovider: {sort: "latency", allow_fallbacks: true} — the fastest healthy provider serves each call; ROUTER_PROVIDER_SORT=latency | throughput | pricecore-llm/router.py
Model fallbacksThe voice brain sends models: [primary, …fallbacks] (OPENROUTER_FALLBACK_MODELS=openai/gpt-4o-mini,openai/gpt-4.1-nano), so a failing model does not drop the callvoice/brain/openrouter.go
Prompt cachingThe system prompt (prompt.md + scenarios.index.txt, ~5,800 tokens) is byte-identical on every call, so ~75 % of it is served from the provider's cache; warmup() primes the connection and the cache at startcore-llm/router.py
Deterministic, tiny answerstemperature=0, max_tokens=64, reasoning off (reasoning: {enabled: false}, or ROUTER_REASONING=minimal | low); the model answers with ID:PERCENT pairs, about 6 tokens, so decoding takes tens of millisecondscore-llm/router.py, core-llm/prompt.md
Cost and usage per callusage: {include: true} returns prompt, completion and cached tokens and the cost in USD; --eval and --bench print them — about $0.05 for the whole 104-utterance dev setcore-llm/router.py
Robust transportOne keep-alive HTTPS connection per thread (no TLS handshake per call), one retry on a dropped socket, an X-Title header; if a provider rejects temperature or reasoning (HTTP 400), the router drops that parameter and retriescore-llm/router.py
Streaming repliesThe voice brain streams over server-sent events: the model first writes a [[SC12|0.88]] header (scenario and confidence), then the spoken reply, which goes straight into TTS (temperature 0.3 for natural speech)voice/brain/openrouter.go
Keys stay on the serverOPENROUTER_API_KEY lives in core-llm/.env (or the voice service's environment); the browser only talks to the same-origin /api/core-routefrontend/src/app/api/core-route/route.ts

Measured: scenario choice 457 ms p50 · 617 ms p95 end to end (reported by the router's author, §9.3); first token 369 ms for gemini-2.5-flash-lite against 705 ms for gpt-4o-mini, 794 ms for claude-haiku-4.5 and 1,238 ms for gpt-4.1-mini (§9.7).

5.2 ElevenLabs — speech in and out

ElevenLabs handles both ends of the conversation. The Go voice service voice/ (Compose profile voice) holds the key and the models; the web app reaches it through two same-origin routes, /api/stt and /api/tts, so the key never reaches the browser.

In the web app («LLM» mode, --profile voice):

FeatureDetails
Speech recognitionClick the mic and speak: recording stops by itself after a pause (VAD) or on a second click, with live partial transcripts and a mic-level ring while you talk. The recording (webm / opus, up to 20 MiB) goes POST /api/stt → POST /api/voice/stt → Scribe v2 with language_code=kk; the answer is {text, language, ms, provider: "elevenlabs"} and the transcript appears in the conversation. ElevenLabs is the recognizer in «LLM» mode; in «Мок» the browser's Web Speech is, and its network errors switch to ElevenLabs by themselves
Voice repliesWith sound on (the speaker icon), every reply is spoken by ElevenLabs: POST /api/tts → POST /api/voice/tts → mp3_22050_32. Russian → eleven_flash_v2_5, Kazakh → eleven_v3_conversational, English greetings → the Russian voice with English pronunciation (lang: en). Voice: Sarah (EXAVITQu4vr4xnSDxMaL), configurable per language
Audio playerEach reply keeps its MP3 for the conversation — play / pause, a seekable waveform computed from the recording, elapsed / duration and download — with the transcript below the player. Muting turns autoplay off but still creates the recording
FallbacksA service error shows a notice and falls back to browser speech; «Мок» always uses browser speech, so it needs no key (and has no downloadable recording)

In the voice service (voice/README.md):

FeatureDetails
Streaming recognitionScribe v2 Realtime over WebSocket: 20 ms audio frames while the caller talks, partial captions every ~100–200 ms, word timestamps. SpeechEnd() maps the last word back to wall-clock time, so latency is measured from the real end of speech. Batch Scribe v2 handles whole recordings — what /api/stt uses today
Languageslanguage_code=kk for Russian, Kazakh and mixed speech: auto-detect heard Kazakh as Turkish, and ru as a secondary language bent mixed phrases towards Russian spelling. Domain keyterms bias recognition: Saqta, ОГПО, КАСКО, ДМС, полис, сақтандыру
End of turnPush-to-talk commit on the web — the final transcript 263–325 ms after release; server VAD with a 0.4 s silence window on the phone (VOICE_VAD_SILENCE_SECS)
Voice per languageFlash v2.5 (~75 ms model latency) over the stream-input WebSocket for Russian; v3 conversational over the text-to-dialogue WebSocket for Kazakh — the only realtime ElevenLabs model that speaks it, and as fast (first audio 208–221 ms)
Output formatspcm_16000 (web), pcm_8000 (Asterisk / a Kazakh SIP number), ulaw_8000 (Twilio), mp3_22050_32 (the web player) — no resampling on our side
Latency tricksThe TTS socket opens on the first partial; a chunker sends the first clause with flush, so audio starts early; a keep-alive HTTP/2 transport with Warm(); fixed phrases (greeting, the «Секунду.» filler played at 1.5 s) cached on disk — no latency and no credits after the first run; speculative replies; barge-in (tts.clear)
ChannelsBrowser WebSocket /ws/voice (push-to-talk or VAD), Asterisk AudioSocket, Twilio Media Streams, and a voice test page at /
Brainsbackend (the team's POST /api/turn), openrouter (a built-in Saqta agent: catalog, knowledge base and mock clients in the prompt), echo (keyless — what Compose runs, because the web app routes with core-llm)
ObservabilityA JSONL log per call with t_ms for every stage, optional WAV recording, /api/voice/events (live SSE), /api/voice/stats (p50 / p95 of end of speech → first audio, STT, LLM first text, TTS first audio), /api/voice/sessions
Toolinggo test ./... against fake ElevenLabs servers (no credits); go run ./cmd/voicedemo quota (characters left on the plan), voicedemo tts / stt demos that write their latencies to voice/demos/RESULTS.md; live smoke tests with VOICE_LIVE=1

Measured with real providers (§9.7): final transcript 263–325 ms after push-to-talk release; first audio 208–221 ms in Kazakh and 211–245 ms in Russian; 1.70–1.95 s from the end of speech to the reply's first audio live in phone-like VAD mode, ~1.1 s estimated for web push-to-talk. Smoke check through /api/stt on bundled synthetic recordings: RU payment → SC30 ✅, KK renewal → SC27 ✅, mixed → SC11 instead of SC12 ❌ because «кеше» (yesterday) was misrecognized (§9.4). The team key is on the ElevenLabs Creator plan (~128k characters a month).


6. Requirements

ForYou need
Recommended — everythingDocker with Compose v2. Nothing else for «Мок»; an OpenRouter API key for «LLM»; an ElevenLabs API key for ElevenLabs voice
Without DockerNode.js 22+ with npm (the version in the Dockerfile; Next.js 16 itself needs ≥ 20.9), Python 3.10+ for core-llm (standard library only) and Go 1.26+ for the voice service
VoiceA microphone; browsers allow it on localhost or HTTPS only. ElevenLabs recognition works in any modern browser; the keyless Web Speech recognizer needs Chrome or Edge on desktop. Text works everywhere
Evaluation scriptsPython 3 for data/evaluate.py; npx tsx (npx downloads tsx on first run)
Go backendGo ≥ 1.26.5 (see backend/go.mod)
API keysNone for «Мок». «LLM» needs OpenRouter, plus ElevenLabs for voice; «Бэкенд» will need provider keys (§7.6)

7. Install and run

Current platform header contains only Call/Console navigation and the RU/ҚАЗ interface language switch. Interface language is saved locally and does not change speech recognition or reply language. Runtime mode/STT selectors and the evaluation card are no longer shown in the platform. Choose NEXT_PUBLIC_API_MODE=mock|core before building (for example, NEXT_PUBLIC_API_MODE=core docker compose --profile server up --build -d). Older screenshots/instructions mentioning header mode controls or the quality card describe the earlier UI; CLI evaluation commands remain available.

Server stack in Docker

Requires Docker Compose v2.24+. All server components now have images: backend (Go API skeleton), db (PostgreSQL 17), core-llm (Python router), and voice (ElevenLabs STT/TTS). The Go service currently exposes /healthz and the compatibility alias /health; its turn API and database persistence are not implemented yet. The working dialog uses frontend core mode.

For a fresh clone, copy .env.example to .env and core-llm/.env.example to core-llm/.env. Put ELEVENLABS_API_KEY in the root .env and OPENROUTER_API_KEY in core-llm/.env. Existing local env files should be edited, not overwritten.

# All server components, without starting frontend:
docker compose --profile server up --build -d --wait db backend core-llm voice

# Full application, including frontend in LLM mode:
NEXT_PUBLIC_API_MODE=core docker compose --profile server up --build -d --wait

# Status and logs:
docker compose --profile server ps
docker compose --profile server logs --tail=100 backend core-llm voice
curl --fail http://localhost:8080/healthz

# Stop the complete stack, preserving database files:
docker compose --profile server down

Backend is bound to 127.0.0.1:8080 (BACKEND_PORT overrides it). PostgreSQL, core-llm and voice are only reachable inside the Compose network; frontend proxies speech and routing requests. Health checks cover all four server services. Backend starts after PostgreSQL is ready. Database files live in the named postgres-data volume; down preserves them. POSTGRES_USER, POSTGRES_PASSWORD, POSTGRES_DB configure a new volume; if changed, update DATABASE_URL too. Existing volumes keep their initialized users/passwords.

Without keys, use the default docker compose up --build: frontend mock mode + Go health service + PostgreSQL. The server profile (or the separate llm and voice profiles) enables integrations requiring provider keys. Docker health checks verify running processes, not provider quota or credentials.

7.1 Quick start — keyless, about two minutes

git clone https://github.com/BAITC-Hacks/hack-9dd7b5f3-plus.git
cd hack-9dd7b5f3-plus
docker compose up --build

Open http://localhost:3000/admin (supervisor console, with the conversation on the left) or http://localhost:3000/call (the client's view). It is built in «Мок» mode — no keys, no accounts, no login. Port 3000 busy? FRONTEND_PORT=3200 docker compose up --build.

Without Docker:

cd hack-9dd7b5f3-plus/frontend
npm ci
npm run dev        # starts in «Мок» when there is no frontend/.env.local

Voice in «Мок» is the browser's own (Web Speech in Chrome / Edge, speechSynthesis). For ElevenLabs voice — also the way around browsers that can't reach Google's speech service (Arc, Brave, Yandex, restricted networks) — run the voice profile from §7.2. The recognizer follows the mode: the browser's Web Speech in «Мок», ElevenLabs in «LLM».

7.2 LLM mode — the real router in the web app

The ready .env files are in our Google Drive folder next to the demo video — copy core-llm/.env and the root .env from there, or create them from the examples below.

With Docker — the router and ElevenLabs voice:

cp core-llm/.env.example core-llm/.env      # put OPENROUTER_API_KEY there
cp .env.example .env                        # put ELEVENLABS_API_KEY there
NEXT_PUBLIC_API_MODE=core docker compose --profile llm --profile voice up --build
# PowerShell: $env:NEXT_PUBLIC_API_MODE='core'; docker compose --profile llm --profile voice up --build

The web app is built in «LLM» mode: ElevenLabs recognizes your speech — click the mic, speak, click again — and, with sound on (the speaker icon), speaks the replies. Without --profile voice, «LLM» mode still routes with the LLM and falls back to browser speech. The Python and voice services are reachable only inside the Docker network, and the keys never reach the browser. The mode is fixed at build time — rebuild to switch between «Мок» and «LLM».

Without Docker — three terminals:

# terminal 1 — repository root: the LLM router
cp core-llm/.env.example core-llm/.env      # put OPENROUTER_API_KEY there
python3 core-llm/server.py                  # 127.0.0.1:8090 · GET /healthz · POST /api/route

# terminal 2 — the ElevenLabs voice service (reads ELEVENLABS_API_KEY from the repo-root .env)
cd voice
VOICE_HTTP_ADDR=127.0.0.1:8091 VOICE_BRAIN=echo VOICE_GREETING=false VOICE_FILLER_AFTER_MS=0 go run ./cmd/voice

# terminal 3 — the web app
cd frontend
cp .env.local.example .env.local            # NEXT_PUBLIC_API_MODE=core, CORE_LLM_URL=…:8090, VOICE_TTS_URL / VOICE_STT_URL=…:8091
npm ci
npm run dev

If core-llm is down or the key is wrong, «LLM» mode shows an error — there is no silent fallback to the mock. If the voice service is down, replies fall back to browser speech with a notice.

7.3 Router CLI, tests and the Go backend

# the LLM router from the command line (needs core-llm/.env)
python3 core-llm/router.py "Кеше аварияға түстім, но я не виноват, виновник у вас застрахован"
python3 core-llm/router.py --eval           # dev set → evaluate.py metrics + predictions
python3 core-llm/router.py --dialogs        # dialogs_sample.json turn by turn, with context
python3 core-llm/router.py --bench google/gemini-2.5-flash-lite,openai/gpt-4.1-mini   # compare models

# tests — no keys needed
python3 -m unittest discover -s core-llm -p 'test_*.py'
(cd frontend && npm test && npm run lint && npm run build)
(cd voice && go test ./...)                 # fake ElevenLabs servers, no credits

# ElevenLabs tools (need ELEVENLABS_API_KEY)
(cd voice && go run ./cmd/voicedemo quota)  # characters left on the plan
(cd voice && go run ./cmd/voicedemo tts && go run ./cmd/voicedemo stt)   # RU / KZ / mixed demos + latency → voice/demos/RESULTS.md

# the Go backend
(cd backend && go run ./cmd/server)         # :8080 (override with PORT); GET /health → {"status":"ok"}

More router options (dialog context flags, the Python API): core-llm/README.md.

7.4 Real mode (frontend → Go backend) 🚧

Build with NEXT_PUBLIC_API_MODE=real and NEXT_PUBLIC_API_URL=http://localhost:8080. The backend has to implement docs/API_CONTRACT.md; ⏳ until the turn API is on main, this mode cannot create a session.

7.5 Deployed version

The VPS deployment uses https://bagyt.plus (37.151.92.185). Nginx terminates Let's Encrypt HTTPS and forwards to the frontend on 127.0.0.1:3080. The frontend calls core-llm:8090 and voice:8090 over the private Compose network; provider keys never go to the browser. Go backend health is on 127.0.0.1:8085/healthz. The Go service is still a skeleton; the working dialog routes through Python core + voice.

Server files:

  • /opt/bagyt/current: active release (symlink into /opt/bagyt/releases/).
  • /opt/bagyt/shared/app.env: root runtime settings and ElevenLabs key, mode 600.
  • /opt/bagyt/shared/core.env: OpenRouter key, mode 600.
  • Each release links .env and core-llm/.env to these private files.

Production settings in app.env are NEXT_PUBLIC_API_MODE=core, FRONTEND_PORT=127.0.0.1:3080, BACKEND_PORT=8085, CORS_ORIGINS=https://bagyt.plus, a generated POSTGRES_PASSWORD and the matching DATABASE_URL. Keep the existing DB password when updating a release. Runtime keys follow the env examples; do not commit the actual env files.

cd /opt/bagyt/current
docker compose -p bagyt -f docker-compose.yml -f deploy/compose.production.yml --profile server up --build -d --wait
docker compose -p bagyt -f docker-compose.yml -f deploy/compose.production.yml --profile server ps

deploy/compose.production.yml adds frontend restart and health checks. deploy/bagyt.plus.nginx.conf is the domain configuration. Install it into /etc/nginx/sites-available/bagyt.plus and link into sites-enabled after issuing the certificate with Certbot webroot /var/www/bagyt-acme. Certbot renews automatically; the server's deploy hook reloads Nginx after renewal. All five containers restart automatically.

Verification: open /call and /admin, allow microphone access, send a Russian or Kazakh request, then check the selected scenario and audio response. No login is required.

bagyt.plus — the live version. Locally: Docker Compose (§7.1), no login required.

7.6 Environment variables

One env file per component; each ships as a keyless example. Secrets live only in your local copies (all gitignored) and in the hosting provider's variables — never in the repo.

FileCopy fromRead by
.env.env.exampleDocker Compose variable substitution (FRONTEND_PORT, NEXT_PUBLIC_API_MODE), the ElevenLabs voice service (ELEVENLABS_* — voice/ also finds this file on its own) and the Go backend
core-llm/.envcore-llm/.env.examplecore-llm/router.py and server.py; in Compose, the core-llm service's env_file
frontend/.env.localfrontend/.env.local.examplenpm run dev (Next.js reads env files from frontend/, not from the repo root); the example starts in «LLM» mode
voice/.env (optional)voice/.env.exampleThe voice service run on its own: every ElevenLabs, OpenRouter, VAD, filler, log and transport knob with its default
VariableFileDefaultRead on main today?Purpose
NEXT_PUBLIC_API_MODE.env (Compose build arg) or frontend/.env.localmock (core in .env.local.example)✅ lib/api.tsMode, fixed at build time: mock | core («LLM») | real (Go)
CORE_LLM_URLfrontend/.env.localhttp://127.0.0.1:8090 (Compose: http://core-llm:8090)✅ app/api/core-route (server only)Where the Next.js proxy finds core-llm. Never prefix it — or any key — with NEXT_PUBLIC_
FRONTEND_PORT.env3000✅ ComposeHost port of the web app
NEXT_PUBLIC_API_URLfrontend/.env.localhttp://localhost:8080✅ lib/api.tsGo backend base URL for «Бэкенд» mode
VOICE_TTS_URL / VOICE_STT_URLfrontend/.env.localhttp://127.0.0.1:8091 (Compose: http://voice:8090)✅ app/api/tts, app/api/stt (server only)Where the Next.js routes find the voice service; VOICE_STT_URL falls back to VOICE_TTS_URL
CORE_LLM_HOST / CORE_LLM_PORTcore-llm/.env127.0.0.1 / 8090 (Compose: 0.0.0.0)✅ core-llm/server.pyBind address of the router's HTTP adapter
OPENROUTER_API_KEYcore-llm/.env—✅ core-llm/router.pyLLM router via OpenRouter
ROUTER_MODELcore-llm/.envgoogle/gemini-2.5-flash-lite✅Model id on OpenRouter
ROUTER_PROVIDER_SORTcore-llm/.envlatency✅OpenRouter provider routing: latency | throughput | price
ROUTER_REASONINGcore-llm/.envoff✅off | skip | minimal | low
ROUTER_THRESHOLDcore-llm/.env60✅Confidence (%) from which a scenario counts as requested; below it → clarify
ROUTER_TIMEOUTcore-llm/.env8✅Per-request HTTP timeout, seconds
ELEVENLABS_API_KEY.env—✅ voice/ElevenLabs STT and TTS — read only by the voice service
ELEVENLABS_VOICE_ID / ELEVENLABS_VOICE_ID_KK.envEXAVITQu4vr4xnSDxMaL (Sarah) / same as RU✅ voice/The voice for Russian / Kazakh replies
ELEVENLABS_TTS_MODEL_RU / ELEVENLABS_TTS_MODEL_KK.enveleven_flash_v2_5 / eleven_v3_conversational✅ voice/TTS model per reply language
ELEVENLABS_STT_LANGUAGE.env (Compose sets kk)kk✅ voice/Scribe language — kk handles Russian, Kazakh and mixed speech
ELEVENLABS_STT_KEYTERMSvoice/.envSaqta,ОГПО,КАСКО,ДМС,полис,сақтандыру✅ voice/Domain words that bias recognition
VOICE_BRAINvoice serviceauto (Compose: echo)✅ voice/auto | backend | openrouter | echo — who writes replies when the voice service drives the call
OPENROUTER_MODEL / OPENROUTER_FALLBACK_MODELSvoice/.envgoogle/gemini-2.5-flash-lite / openai/gpt-4o-mini,openai/gpt-4.1-nano✅ voice/brainModel and fallbacks of the voice service's own OpenRouter brain
PORT.env8080✅ cmd/serverHTTP port
LLM_PROVIDER.envmock🚧mock | openai | nvidia | openai_compatible
OPENAI_API_KEY / OPENAI_MODEL.env— / gpt-4.1-mini🚧OpenAI provider for the Go backend
NVIDIA_API_KEY / NVIDIA_MODEL.env— / meta/llama-3.3-70b-instruct🚧NVIDIA Build (NIM) provider
LLM_BASE_URL / LLM_API_KEY / LLM_MODEL.env—🚧Any OpenAI-compatible API (OpenRouter, Groq, Gemini, Ollama, …)
GEKO_API_KEY.env—📋geko.sh STT / TTS for KZ / RU
SPEKO_API_KEY.env—📋 optionalVoice-provider router; not on the KZ / RU path
DATABASE_URL.envpostgres://plus:plus@db:5432/plus?sslmode=disable📋PostgreSQL connection
CORS_ORIGINS.envhttp://localhost:3000🚧Origins allowed to call the API

7.7 Troubleshooting

SymptomFix
«Распознавание речи работает в Chrome или Edge…»Use desktop Chrome or Edge — or just type: text runs the same pipeline
«Нет связи с сервисом распознавания…»The browser can't reach Google's speech service; the app switches to ElevenLabs recognition. Run the voice profile with ELEVENLABS_API_KEY (§7.2) — or type
«ElevenLabs не смог озвучить ответ…» / «…распознать речь…»Check ELEVENLABS_API_KEY, the voice ID and the quota left: cd voice && go run ./cmd/voicedemo quota
«Сервис озвучки недоступен…» / «Сервис распознавания недоступен…»Start the voice service (--profile voice, or go run ./cmd/voice) and check VOICE_TTS_URL / VOICE_STT_URL
«Нет доступа к микрофону…»Allow the microphone in the address bar; the page must be on localhost or HTTPS
«Ничего не расслышала…»Click the mic, start speaking right away, pause to send
Kazakh speech comes out as Russian wordsSwitch the header language to ҚАЗ — it sets the recognizer too. Mixed phrases work best in «LLM» mode, where ElevenLabs Scribe with kk handles both (§13)
Kazakh answers are read with a Russian voiceIn «Мок», browsers rarely ship a Kazakh voice, so it falls back to a Russian one. In «LLM» with the voice profile, ElevenLabs eleven_v3_conversational speaks Kazakh
«core-llm не отвечает…» in «LLM» modeStart the router — docker compose --profile llm up --build, or python3 core-llm/server.py — and check CORE_LLM_URL
«Ошибка core-llm…» / «LLM недоступна…»Check OPENROUTER_API_KEY and ROUTER_MODEL in core-llm/.env
Port 3000 is busyFRONTEND_PORT=3200 docker compose up --build, or npm run dev -- -p 3001
The previous dialog interferesClick «Новый диалог» — within one dialog the client stays identified and slots are remembered

8. How to verify

Prefer to watch? The demo video (3 min) is in our Google Drive folder. Prefer to click? bagyt.plus — open access, no login. Everything below also works locally.

8.1 Setup — 30 seconds

  1. Run §7.1 and open http://localhost:3000/admin in Chrome.
  2. The default build is «Мок» (the header shows «демо»). Turn sound on with the speaker icon next to the text field.
  3. Click «Новый диалог» before each scenario below. Every step works by typing or by voice (click 🎤, speak, pause); the header's language switch sets the interface, speech recognition and reply language together.

8.2 Walkthrough — 10 scenarios

The expected results below were produced by running these exact inputs through the mock engine on main.

#Say or typeYou should see (mock mode)Proves
1«Хочу узнать статус моего заявления» → then the phone: «плюс семь семьсот один ноль ноль ноль ноль ноль ноль семь» (or +77010000007)SC17 "Claim status" 0.96 → the robot asks for the phone → Sergey Popov identified (find_client, get_claim) → «По заявлению CL-500330: на рассмотрении…»Voice in and out, identification, spoken numbers, a data-grounded answer
2«Здравствуйте, я вчера оплатил, деньги списались, а заказ не подтвердился… а, и ещё, адрес доставки поменять надо» — the case's own exampleTwo scenarios: SC30 0.94 + SC29 0.82; policy run, SC29 waits on the stack; slots incident_date: 2026-09-30, contact_field: address; the robot promises to handle the second question tooMulti-intent, topic stack, slot extraction
3Switch the language to ҚАЗ, then say or type «Сәлеметсіз бе, полисімнің мерзімі қашан бітеді?» → then the phone in Kazakh words: «плюс жеті жеті жүз бір нөл нөл нөл нөл нөл нөл бір»Language KK; SC25 "Check policy validity" 0.90; the answer is in Kazakh: «Тексерейін. Телефон нөміріңізді айтып жіберіңізші.» → Arman Tulegenov → «SQ-OGPO-104501 полисі 2027-03-14 дейін жарамды.»Kazakh in and out
4«Кеше аварияға түстім, но я не виноват, виновник у вас застрахован»Language RU+KK (mixed inside one phrase); SC12 "Claim as victim under culprit's OGPO" 0.90, while SC11 "Road accident just happened" is only an alternative (0.32); the robot asks for the culprit's plateCode-switching, a scenario boundary
5«Проверьте, действует ли мой полис, и ещё мне звонили якобы из страховой и просили код из смс»SC38 "Fraud report" (urgent) is handled first although it was said second; SC25 is queued; anti-fraud adviceUrgent-first priority
6«Хочу записаться к терапевту по ДМС» → +77010000002 → «Подождите, а какие клиники есть в Астане?»SC21 → Aigerim Bekova → switch to SC23 (list_clinics: Saulet Medical, Nur Med Astana) → the robot returns on its own: «Теперь по вопросу «Запись к врачу по ДМС»…»Topic switch and return
7«Хочу продлить ОГПО» → +77010000003 → «Да»renew_policy:preview + read-back «Проверю: полис SQ-OGPO-102850, стоимость 31 200 тенге… Всё верно? Это действие нельзя будет отменить.» → only after «Да»: renew_policy:execute, send_smsNo irreversible action without an explicit «да»
8«Алло, я по поводу страховки» → «Ну там вопрос»SYS_UNCLEAR → one clarifying question with two options → a second vague turn → a menu of the services the robot can help with (no automatic transfer); «Соедините с оператором» (SC37) → transfer_to_operator with a context summaryAsks instead of guessing; hands off to a human on request
9«Можно у вас взять кредит на машину?»SYS_OUT_OF_SCOPE → a polite refusal plus what the robot can help withOut-of-scope detection
10On /admin, after any turn: «JSON последней реплики»The trace in the dataset README format — transcript, language, scenarios with confidence, alternatives, reason, slots, actions, latency_ms; «Скорость» next to it shows the per-stage waterfall against the 1.5 s lineThe trace the case asks for

The official dev-set metrics — 97.1% for the mock, in-sample (§9.1) — are one command away: §8.4.

In «LLM» mode (NEXT_PUBLIC_API_MODE=core) — the product router; needs a running core-llm with a key (§7.2) or the deployed version. Expected results as stated by the router's author; ⏳ re-check at freeze:

#Say or typeYou should see
L1«Хочу продлить ОГПО и добавить сына»Two scenarios, SC27 renewal + SC04 add a driver; the robot acknowledges the second request and asks for identification; «Консоль» shows the model, the percentages, the alternatives, the core's verdict and the timings
L2«А какие документы нужны при ДТП?»A topic switch; the console shows the new scenario
L3«Полисімнің мерзімін ұзартқым келеді»Kazakh, routed by the same core
L4python3 core-llm/router.py --evalThe dev set through the real router — 104 paid requests, metrics as in evaluate.py
CLIpython3 core-llm/router.py "Хочу продлить ОГПО и заодно добавить в него сына"SC27:95 SC04:95 (core-llm/README.md)

ElevenLabs voice — «LLM» mode with --profile voice and ELEVENLABS_API_KEY (§7.2). Expected results as stated by the voice integration's author; ⏳ re-check at freeze:

#DoYou should see
V1Turn sound on (the speaker icon), click 🎤, say «Хочу продлить ОГПО и добавить сына», click 🎤 againThe transcript appears in the conversation, the LLM routes it (SC27 + SC04), and ElevenLabs speaks the reply; an audio player with a waveform, elapsed / duration and a download button sits above the reply's text
V2Say «Полисімнің мерзімін ұзартқым келеді»A Kazakh transcript (Scribe v2 with kk), and a Kazakh reply spoken by eleven_v3_conversational
V3Mute with the speaker icon, send another phraseNo autoplay, but the recording is still created and playable

Greetings — any mode (verified in «Мок» on main):

#Say or typeYou should see
G1«Здравствуйте» · «Сәлеметсіз бе» · “Hello”SYS_GREETING: «Здравствуйте, чем могу помочь?» · «Сәлеметсіз бе, қалай көмектесе аламын?» · “Hello, how can I help you?” — the dialog state is untouched
G2«Здравствуйте, хочу продлить полис»SC27 (renewal), not a greeting — the robot asks for the phone

8.3 Test clients

Synthetic clients from data/mock_backend.json — say or type the phone when the robot asks.

PhoneClientCityWhat they have
+77010000001Arman TulegenovAlmatyOGPO SQ-OGPO-104501 and CASCO SQ-CASCO-204118 (to 2027-03-14); claim CL-500198 paid
+77010000002Aigerim BekovaAstanaDMS SQ-DMS-604220 (to 2026-12-31)
+77010000003Yerlan OmarovShymkentOGPO SQ-OGPO-102850 (to 2026-09-29); payment P-3001 charged, policy not issued
+77010000004Natalia SmirnovaAlmatyProperty SQ-PROP-404077; claim CL-500311 waiting for documents
+77010000007Sergey PopovPavlodarCASCO SQ-CASCO-204300 (to 2026-10-20); claim CL-500330 under review
+77010000010Kamila UtepovaAtyrauCASCO SQ-CASCO-204350 (to 2027-04-30)

All 11 clients: data/mock_backend.json or Appendix D of docs/CASE_ANALYSIS.md.

8.4 Reproduce the numbers from a terminal

# 1) official scorer on the committed mock predictions — needs only Python
python data/evaluate.py data/predictions_mock.json data/dev_utterances.json

# 2) regenerate the predictions from the current mock router, then score them
cd frontend
npx tsx scripts/eval-mock.ts
python ../data/evaluate.py ../data/predictions_mock.json ../data/dev_utterances.json

# 3) replay the 10 annotated sample dialogs through the mock engine (§9.2)
npx tsx scripts/replay-dialogs.ts

# 4) the LLM router on the dev set and on the dialogs (needs OPENROUTER_API_KEY in core-llm/.env)
cd ..
python3 core-llm/router.py --eval
python3 core-llm/router.py --dialogs

# 5) unit tests — no keys needed
python3 -m unittest discover -s core-llm -p 'test_*.py'
(cd frontend && npm test)

Use python3 where python is not on the path.

8.5 Case requirements → where to check them

Case requirementWhere you see itStatus
Must-have: voice interaction in the web — the jury speaks, the robot answers by voice/call or /admin: mic → spoken reply (steps 1, 3, V1–V2)✅ ElevenLabs STT + TTS in «LLM» · ✅ browser speech in «Мок»
Must-have: scenario selection on an LLM layer, not an encoder classifier«LLM» mode in the web app → /api/core-route → core-llm/: prompt + catalog index → LLM → policy (§4.7)✅ needs an OpenRouter key — ⏳ open on the deployed version
Must-have: correct selection on the jury's 10 utterancesDev set: 104/104 for the LLM router (reported, in-sample), 97.1% for the mock (§9.1)⏳ the jury's set
Must-have: trace panel after every utterance — scenario, reason, alternatives, per-stage timing/admin: decision card, «Скорость», «JSON последней реплики»✅
Must-have: Russian and Kazakh, including mixing inside a phraseSteps 3, 4 and V2; ElevenLabs Scribe with kk for RU, KZ and mixed speech, Kazakh replies by eleven_v3_conversational✅ (time markers in mixed speech can be misheard, §13)
Optional: context retention and return to an interrupted topicSteps 2 and 6; core-llm --dialogs✅
Optional: ask instead of guessing; hand off with contextStep 8✅
Optional: parameter extraction from speechSteps 1–3 (spoken phone, dates, address)✅
Optional: the 500 ms target for scenario choiceLLM router p50 457 ms, 75 % of calls ≤ 500 ms (reported, §9.3)✅ p50 · ⏳ in the web app
Optional: supervisor panel with error statistics/admin session metrics; the dev-set error list via evaluate.py (§8.4)✅ partly (per session)
Optional: streaming processingTyped event stream in the web app; streaming STT and TTS over WebSockets in the v
Звёзды
⭐ 0
Форки
0
Язык
TypeScript
Последний push
в прошлом месяце
Коммиты
62
Авторы
4
Файлы
313
Ветки
5

Авторы коммитов

heiphin7 28Tair Kaldybayev 19Alikhan 13Tair Kaldybayev 2

История коммитов

  • 2026-09-23 17:58readmeTair Kaldybayev
  • 2026-09-23 17:56docs(readme): open access, demo video and .env via the team's Google Drive folder, bagyt.plus deploymentTair Kaldybayev
  • 2026-09-23 17:56docs(deploy): record live routing and speech smoke checksheiphin7
  • 2026-09-23 17:52chore(deploy): configure bagyt VPS HTTPS and production stackheiphin7
  • 2026-09-23 17:51docs(readme): run instructions for bagyt.plus; lean navbar, one language switch, hands-free voice, service menuTair Kaldybayev
  • 2026-09-23 17:50test(voice): support audio context in recorder fixtureheiphin7
  • 2026-09-23 17:49fixheiphin7
  • 2026-09-23 17:43feat(platform): lean navbar (brand · Звонок | Аналитика · language · engine · new dialog), one language switch for UI + speech + reply, platform entrance in parallel with the last wave, hands-free voiAlikhan
Все коммиты (62)
  • 2026-09-23 17:40chore(deploy): Caddy HTTPS + prod compose override for bagyt.plusTair Kaldybayev
  • 2026-09-23 17:39docs(readme): compliance row wording for the build-time LLM modeTair Kaldybayev
  • 2026-09-23 17:39docs(readme): live demo bagyt.plus; instructions for the simplified header (build-time mode, no quality card)Tair Kaldybayev
  • 2026-09-23 17:39Merge branch 'feat/phone-call-web'Tair Kaldybayev
  • 2026-09-23 17:35docs(readme): resolve merge conflict in the repository map (voice/ + backend line)Tair Kaldybayev
  • 2026-09-23 17:34feat(web): simplify header, localize interface and remove quality panelheiphin7
  • 2026-09-23 17:33docs(readme): OpenRouter and ElevenLabs features, voice on main, key policyTair Kaldybayev
  • 2026-09-23 17:29fix(admin): display voice agent identity instead of model labelsheiphin7
  • 2026-09-23 17:27feat(docker): containerize complete server stack with health checksheiphin7
  • 2026-09-23 17:22feat(web): add replayable audio replies and English greetingsheiphin7
  • 2026-09-23 17:18feat(web): wave transition ends on black; black sheet lifts last with a slow ease-out so the dark platform opens seamlesslyAlikhan
  • 2026-09-23 17:13feat(web): living dot-matrix headline on canvas (self-animating wave, soft cursor push) and black→white→blue wave transition into the platformAlikhan
  • 2026-09-23 17:11feat(router): handle standalone greetings in Russian and Kazakhheiphin7
  • 2026-09-23 17:11readme changeTair Kaldybayev
  • 2026-09-23 17:05feat(voice): phone-style call page (/call) for demoing a call from a phoneTair Kaldybayev
  • 2026-09-23 17:03fix(web): keep the CTA wipe until the target page mounts (no flash on slow loads)Alikhan
  • 2026-09-23 17:03feat(web): live dot-matrix headline (drifting dots, cursor reveals solid glyphs), new logo (dot-plus tile + Unbounded wordmark), circle wipe transition from CTA into the simulator with reveal on arrivAlikhan
  • 2026-09-23 17:01feat(voice): connect microphone input to ElevenLabs STTheiphin7
  • 2026-09-23 17:01docs(readme): team photo at the top, hackathon gallery moved upTair Kaldybayev
  • 2026-09-23 16:57feat(web): landing harmony pass — horizon without hard clip (masked, fully inside), one section rhythm (band → heading row → content), shared card style and spacing across product, catalogue, trace anAlikhan
  • 2026-09-23 16:53feat(web): landing interactivity and tone fixes — cool white section bands, no glyph flicker in the plus dither (cursor-only), breathing horizon, scroll reveal, hoverable scenario grid with captions aAlikhan
  • 2026-09-23 16:49feat(voice): connect ElevenLabs TTS to frontend repliesheiphin7
  • 2026-09-23 16:49feat(web): landing polish after visual audit vs speko.ai — full-bleed stepped horizon, stronger dithered edge clouds, denser dot-matrix headline, pixel fields in section bands, denser plus dither, fooAlikhan
  • 2026-09-23 16:43docs(readme): fill real-provider latency and STT results from the research logTair Kaldybayev
  • 2026-09-23 16:40docs(readme): sync with main — LLM mode, Docker Compose, try-it block, photosTair Kaldybayev
  • 2026-09-23 16:40feat(web): Speko-style landing — dot-matrix headline, blurred blue horizon, dithered edge clouds, section bands with pixel clusters, scenario catalogue grid, trace code card, marble dither CTA; no AI-Alikhan
  • 2026-09-23 16:39docs(research): research log with measured STT/TTS/LLM/end-to-end numbers and chartsTair Kaldybayev
  • 2026-09-23 16:27feat(web): connect frontend dialog flow to core-llm routerheiphin7
  • 2026-09-23 16:27docs(readme): jury-ready README — status, diagrams, verified walkthrough, resultsTair Kaldybayev
  • 2026-09-23 16:27feat(voice): real-time voice gateway, conversation engine and phone callsTair Kaldybayev
  • 2026-09-23 16:24feat(web): landing redesign — Speko/ElevenLabs-style hero with PLUS dither, console product shot, four steps, fractal-glass plus, dotted-grid CTA; ObsidianUI arrow-fill buttonsAlikhan
  • 2026-09-23 16:12auto-select llm provider when a key is setheiphin7
  • 2026-09-23 16:11core llm uraaaheiphin7
  • 2026-09-23 16:11add next.js frontend with call simulator, trace panel and supervisorheiphin7
  • 2026-09-23 16:11feat(voice): ElevenLabs STT/TTS core for RU/KZ voiceTair Kaldybayev
  • 2026-09-23 16:05feat(web): server-side STT fallback (/api/stt via OpenAI) when Chrome cannot reach Google speech; provider toggle; auto-switch on network errorAlikhan
  • 2026-09-23 16:00add core-llm scenario routerheiphin7
  • 2026-09-23 15:59feat(web): calmer supervisor console, Geist type, click-to-talk mic, evaluate.py metrics on /adminAlikhan
  • 2026-09-23 15:50add demo dialog runner and llm client testheiphin7
  • 2026-09-23 15:49record implementation decisions in docsheiphin7
  • 2026-09-23 15:48add russian readmeheiphin7
  • 2026-09-23 15:47fix number word adjacency ruleheiphin7
  • 2026-09-23 15:46add tone signal and fix ambiguous number wordsheiphin7
  • 2026-09-23 15:45add catalog editing, ws smoke client and compact promptheiphin7
  • 2026-09-23 15:43docs: document verified local OpenRouter configurationheiphin7
  • 2026-09-23 15:40add voice router backend with lexical baseline, eval and audio testsheiphin7
  • 2026-09-23 15:39feat(web): mock-driven frontend — landing, call simulator, supervisor consoleAlikhan
  • 2026-09-23 15:35docs: record clean checkout verification resultsheiphin7
  • 2026-09-23 15:33feat(router): integrate official Saqta catalog and evaluatorheiphin7
  • 2026-09-23 15:19feat: build observable bilingual voice routing simulatorheiphin7
  • 2026-09-23 15:04docs(design): add architecture visual (pipeline, latency budget, frontend roles, phone channel, provider picks)Tair Kaldybayev
  • 2026-09-23 14:45docs: English living docs for Voice Router (case analysis, PRD, SPEC, tasks, pitch)Tair Kaldybayev
  • 2026-09-23 14:42docs: add Voice Router case, starter dataset and research; reorganize docsTair Kaldybayev
  • 2026-09-23 13:53initheiphin7
Структура репозитория (313 файлов)
.dockerignore
.env.example
.gitignore
AGENTS.md
CLAUDE.md
DESIGN.md
README.md
THIRD_PARTY.md
backend/.dockerignore
backend/Dockerfile
backend/cmd/server/main.go
backend/go.mod
core-llm/.dockerignore
core-llm/.env.example
core-llm/.gitignore
core-llm/Dockerfile
core-llm/README.md
core-llm/prompt.md
core-llm/router.py
core-llm/scenarios.index.txt
core-llm/server.py
core-llm/test_greeting.py
core-llm/test_server.py
data/README.kz.md
data/README.md
data/README.ru.md
data/actions.json
data/dev_utterances.json
data/dialogs_sample.json
data/evaluate.py
data/knowledge_base.json
data/mock_backend.json
data/predictions_mock.json
data/scenarios.json
data/slots.json
deploy/Caddyfile
deploy/bagyt.plus.nginx.conf
deploy/compose.production.yml
docker-compose.prod.yml
docker-compose.yml
docs/API_CONTRACT.md
docs/CASE.md
docs/CASE_ANALYSIS.md
docs/PITCH.md
docs/PRD.md
docs/PROGRESS.md
docs/README.md
docs/REQUIREMENTS.md
docs/SPEC.md
docs/TASKS.md
docs/assets/hackathon/cap.jpg
docs/assets/hackathon/case-page.jpg
docs/assets/hackathon/entrance.jpg
docs/assets/hackathon/hall.jpg
docs/assets/hackathon/swag.jpg
docs/assets/hackathon/team-at-work.jpg
docs/assets/hackathon/team.jpg
docs/design/README.md
docs/design/architecture.html
docs/design/references/speko-brand-landing.pdf
docs/design/references/speko-console-components.pdf
docs/hackathon/README_PROMPT.md
docs/hackathon/RULES.md
docs/hackathon/TEAM_PLAN.md
docs/hackathon/organizer/asu-online-course-instructions.ru.pdf
docs/hackathon/organizer/cybersecurity-instructions.ru.pdf
docs/hackathon/organizer/equipment-preparation.ru.pdf
docs/hackathon/organizer/openai-nvidia-credits-activation.ru.pdf
docs/hackathon/organizer/participant-instructions.ru.pdf
docs/hackathon/organizer/participation-credit-rules.ru.pdf
docs/hackathon/raw/RULES.ru.md
docs/research/DEEP_RESEARCH_PROMPT.md
docs/research/DEEP_RESEARCH_REPORT.md
docs/research/RESEARCH.md
docs/research/raw/case-analysis.ru.md
docs/research/raw/deep-research-output.ru.md
frontend/.dockerignore
frontend/.env.local.example
frontend/.gitignore
frontend/AGENTS.md
frontend/CLAUDE.md
frontend/Dockerfile
frontend/README.md
frontend/components.json
frontend/eslint.config.mjs
frontend/next.config.ts
frontend/package-lock.json
frontend/package.json
frontend/postcss.config.mjs
frontend/public/landing/hero-marble.svg
frontend/public/landing/marble.svg
frontend/public/landing/plus-dark.svg
frontend/public/landing/plus.svg
frontend/scripts/eval-mock.ts
frontend/scripts/replay-dialogs.ts
frontend/scripts/test-core.cjs
frontend/src/app/(console)/admin/page.tsx
frontend/src/app/(console)/call/page.tsx
frontend/src/app/(console)/layout.tsx
frontend/src/app/api/core-route/route.ts
frontend/src/app/api/stt/route.ts
frontend/src/app/api/tts/route.ts
frontend/src/app/favicon.ico
frontend/src/app/globals.css
frontend/src/app/layout.tsx
frontend/src/app/page.tsx
frontend/src/components/app/admin/decision.tsx
frontend/src/components/app/admin/dialog.tsx
frontend/src/components/app/admin/quality.tsx
frontend/src/components/app/admin/shared.tsx
frontend/src/components/app/admin/speed.tsx
frontend/src/components/app/admin/turn-log.tsx
frontend/src/components/app/audio-message.tsx
frontend/src/components/app/console-shell.tsx
frontend/src/components/app/conversation-panel.tsx
frontend/src/components/app/enter-reveal.tsx
frontend/src/components/app/page-waves.ts
frontend/src/components/block/arrow-fill-button.css
frontend/src/components/block/arrow-fill-button.jsx
frontend/src/components/block/dither-canvas.jsx
frontend/src/components/block/dotted-grid.jsx
frontend/src/components/block/fractal-glass.jsx
frontend/src/components/block/plus-dither.jsx
frontend/src/components/block/text-stream.css
frontend/src/components/block/text-stream.jsx
frontend/src/components/brand.tsx
frontend/src/components/landing/band.tsx
frontend/src/components/landing/clouds.tsx
frontend/src/components/landing/cta-button.tsx
frontend/src/components/landing/cta.tsx
frontend/src/components/landing/dot-matrix-text.tsx
frontend/src/components/landing/footer.tsx
frontend/src/components/landing/header.tsx
frontend/src/components/landing/hero.tsx
frontend/src/components/landing/product.tsx
frontend/src/components/landing/reveal.tsx
frontend/src/components/landing/scenarios.tsx
frontend/src/components/landing/section-head.tsx
frontend/src/components/landing/trace.tsx
frontend/src/components/landing/visuals.tsx
frontend/src/components/ui/accordion.tsx
frontend/src/components/ui/alert-dialog.tsx
frontend/src/components/ui/alert.tsx
frontend/src/components/ui/autocomplete.tsx
frontend/src/components/ui/avatar.tsx
frontend/src/components/ui/badge.tsx
frontend/src/components/ui/breadcrumb.tsx
frontend/src/components/ui/button.tsx
frontend/src/components/ui/calendar.tsx
frontend/src/components/ui/card.tsx
frontend/src/components/ui/checkbox-group.tsx
frontend/src/components/ui/checkbox.tsx
frontend/src/components/ui/collapsible.tsx
frontend/src/components/ui/combobox.tsx
frontend/src/components/ui/command.tsx
frontend/src/components/ui/context-menu.tsx
frontend/src/components/ui/dialog.tsx
frontend/src/components/ui/drawer.tsx
frontend/src/components/ui/empty.tsx
frontend/src/components/ui/field.tsx
frontend/src/components/ui/fieldset.tsx
frontend/src/components/ui/form.tsx
frontend/src/components/ui/frame.tsx
frontend/src/components/ui/group.tsx
frontend/src/components/ui/input-group.tsx
frontend/src/components/ui/input.tsx
frontend/src/components/ui/kbd.tsx
frontend/src/components/ui/label.tsx
frontend/src/components/ui/menu.tsx
frontend/src/components/ui/meter.tsx
frontend/src/components/ui/number-field.tsx
frontend/src/components/ui/otp-field.tsx
frontend/src/components/ui/pagination.tsx
frontend/src/components/ui/popover.tsx
frontend/src/components/ui/preview-card.tsx
frontend/src/components/ui/progress.tsx
frontend/src/components/ui/radio-group.tsx
frontend/src/components/ui/scroll-area.tsx
frontend/src/components/ui/select.tsx
frontend/src/components/ui/separator.tsx
frontend/src/components/ui/sheet.tsx
frontend/src/components/ui/sidebar.tsx
frontend/src/components/ui/skeleton.tsx
frontend/src/components/ui/slider.tsx
frontend/src/components/ui/spinner.tsx
frontend/src/components/ui/switch.tsx
frontend/src/components/ui/table.tsx
frontend/src/components/ui/tabs.tsx
frontend/src/components/ui/textarea.tsx
frontend/src/components/ui/toast.tsx
frontend/src/components/ui/toggle-group.tsx
frontend/src/components/ui/toggle.tsx
frontend/src/components/ui/toolbar.tsx
frontend/src/components/ui/tooltip.tsx
frontend/src/data/dev_utterances.json
frontend/src/data/knowledge_base.json
frontend/src/data/mock_backend.json
frontend/src/data/scenario_names.json
frontend/src/data/scenarios.json
frontend/src/data/slots.json
frontend/src/hooks/use-media-query.ts
frontend/src/lib/api.ts
frontend/src/lib/catalog.ts
frontend/src/lib/contract.ts
frontend/src/lib/core-router.ts
frontend/src/lib/effects/shared/webgl-surface.jsx
frontend/src/lib/eval.ts
frontend/src/lib/mock/actions.ts
frontend/src/lib/mock/engine.ts
frontend/src/lib/mock/router.ts
frontend/src/lib/segmented-control.ts
frontend/src/lib/store.ts
frontend/src/lib/ui-language.ts
frontend/src/lib/utils.ts
frontend/src/lib/voice-agent.ts
frontend/src/lib/voice.ts
frontend/tsconfig.json
voice/.env.example
voice/.gitignore
voice/AGENTS.md
voice/Dockerfile
voice/README.md
voice/agent/agent.go
voice/agent/agent_test.go
voice/audio/audio_test.go
voice/audio/format.go
voice/audio/mulaw.go
voice/audio/pcm.go
voice/audio/resample.go
voice/audio/vad.go
voice/audio/wav.go
voice/brain/backend.go
voice/brain/backend_test.go
voice/brain/brain.go
voice/brain/dataset.go
voice/brain/dataset_test.go
voice/brain/echo.go
voice/brain/echo_test.go
voice/brain/header.go
voice/brain/header_test.go
voice/brain/http.go
voice/brain/live_test.go
voice/brain/openrouter.go
voice/brain/openrouter_test.go
voice/brain/prompt.go
voice/cmd/voice/main.go
voice/cmd/voicedemo/e2e.go
voice/cmd/voicedemo/main.go
voice/config/config.go
voice/config/config_test.go
voice/config/dotenv.go
voice/config/resolver.go
voice/demos/01_ru_greeting.manual.stt.json
voice/demos/01_ru_greeting.tts.json
voice/demos/01_ru_greeting.vad.stt.json
voice/demos/01_ru_greeting.wav
voice/demos/02_kk_greeting.manual.stt.json
voice/demos/02_kk_greeting.tts.json
voice/demos/02_kk_greeting.vad.stt.json
voice/demos/02_kk_greeting.wav
voice/demos/03_mixed_caller.e2e.json
voice/demos/03_mixed_caller.manual.stt.json
voice/demos/03_mixed_caller.tts.json
voice/demos/03_mixed_caller.vad.stt.json
voice/demos/03_mixed_caller.wav
voice/demos/03_mixed_caller_reply.wav
voice/demos/04_ru_caller.e2e.json
voice/demos/04_ru_caller.tts.json
voice/demos/04_ru_caller.wav
voice/demos/04_ru_caller_reply.wav
voice/demos/05_kk_caller.e2e.json
voice/demos/05_kk_caller.tts.json
voice/demos/05_kk_caller.wav
voice/demos/05_kk_caller_reply.wav
voice/demos/RESULTS.md
voice/demos/e2e-logs/2026-09-23/112458_demo_e2e-03_mixed_caller.jsonl
voice/demos/e2e-logs/2026-09-23/112507_demo_e2e-04_ru_caller.jsonl
voice/demos/e2e-logs/2026-09-23/112513_demo_e2e-05_kk_caller.jsonl
voice/deploy/README.md
voice/deploy/asterisk/entrypoint.sh
voice/deploy/asterisk/extensions.conf.template
voice/deploy/asterisk/pjsip.conf.template
voice/deploy/docker-compose.yml
voice/elevenlabs/client.go
voice/elevenlabs/elevenlabs_test.go
voice/elevenlabs/live_test.go
voice/elevenlabs/stt_realtime.go
voice/elevenlabs/tts_http.go
voice/elevenlabs/tts_socket.go
voice/go.mod
voice/go.sum
voice/lang/lang.go
voice/lang/lang_test.go
voice/lang/words.go
voice/server/server.go
voice/server/server_live_test.go
voice/server/server_test.go
voice/speech/chunker.go
voice/speech/chunker_test.go
voice/speech/speech.go
voice/transport/audiosocket.go
voice/transport/registry.go
voice/transport/transport.go
voice/transport/transport_test.go
voice/transport/twilio.go
voice/transport/web.go
voice/voicelog/hub.go
voice/voicelog/stats.go
voice/voicelog/voicelog.go
voice/voicelog/voicelog_test.go
voice/web/call.html
voice/web/embed.go
voice/web/index.html
Ветки (5)

Обсуждение 0 комментариев

Войдите, чтобы комментировать

Пока нет комментариев — будьте первым.