224 Commits
Author SHA1 Message Date
a.arystanbek c261d08036 feat: require complete KB-grounded answers (no dropped facts) + ask did-that-answer-your-question after each answer, per ai-operator spec
deploy / deploy (push) Failing after 4s
2026-08-30 21:22:54 +00:00
a.arystanbek 78dfe6a9e1 fix: do not fire no-speech reprompt while caller is still mid-utterance with live partial ASR
deploy / deploy (push) Failing after 3s
2026-08-30 21:19:02 +00:00
a.arystanbek f978702a6c fix: retry voice turn once on Postgres deadlock instead of failing the call with a technical-error handoff
deploy / deploy (push) Successful in 34s
2026-08-30 21:14:25 +00:00
a.arystanbek f82f27c035 fix: gpt-5-nano too unreliable for aimaq voice decisions, switch to gpt-5-mini
deploy / deploy (push) Failing after 6s
2026-08-30 21:04:55 +00:00
a.arystanbek 75d105f636 fix: support GPT-5 request contract (max_completion_tokens, reasoning_effort) and switch aimaq voice decisions to gpt-5-nano
deploy / deploy (push) Failing after 0s
2026-08-30 20:37:35 +00:00
didar 9bf367abf4 fix: filter RU/KZ stopwords from KB search so filler words can't false-match
deploy / deploy (push) Successful in 30s
search_kb_rows had no relevance floor: any exact-token hit, however
generic, scored above zero and could win as the top/only result. A
caller utterance as thin as a bare "да" (confirming the language) could
exact-match that same common word inside an unrelated FAQ article's
body and get returned as "the" answer, which then got read back
almost verbatim — this is what surfaced live as the AI unprompted
launching into a voucher-activation explanation right after the
customer confirmed Russian, having said nothing else.

tokenize_kb_text now drops a curated set of RU/KZ greetings,
confirmations, pronouns, and particles. A stopword-only query naturally
falls through to the existing "no query tokens -> no results" path
instead of returning a coincidental match; genuine single-content-word
queries (e.g. "ваучер") are unaffected. Applies to every channel that
calls _kb_search (voice, Telegram, WhatsApp), not just voice.
2026-08-31 01:08:11 +05:00
arystanbek 3c5c233071 Merge pull request 'fix: stop no-answer retry racing dialplan hangup + fix event catalog validation' (#11) from fix/retry-channel-resolution-and-event-catalog into main
deploy / deploy (push) Failing after 5s
2026-08-30 20:04:04 +00:00
arys 99d169ec67 fix: stop the no-answer retry from racing the dialplan hangup + fix event catalog validation
Two bugs found via a live test with two real registered browser softphones:

1. retry_escalation_no_answer() re-resolved the client channel via
   _resolve_handoff_channel() -> a live AMI CoreShowChannels round-trip that
   can take ~10s. The new mvpcc-transfer dialplan wait window (MusicOnHold,
   also ~10s, added to give the backend time to redirect before the final
   Hangup) was consistently LOST to this exact same duration: the backend's
   AMI Redirect fired against a channel the dialplan had already hung up
   ('Channel does not exist: PJSIP/...', confirmed in escalation timeline).
   Fixed by reusing the actively-maintained AsteriskCallLinkRow.channel_name
   directly (unchanged for a PJSIP channel across Redirect between contexts
   of the same call) instead of re-discovering it, falling back to the slow
   path only if that field is empty.

2. VoiceEventIn.event_type is a pydantic Literal restricted to 6 legacy
   values (call.started/ivr.completed/...). None of the Phase 2 event
   catalog names (AgentReserved/AgentRinging/AgentNoAnswer/AgentConnected/
   TransferCompleted/TransferFailed) were ever in it, so every single
   _emit_escalation_event() call has been failing with 422 since Phase 2
   shipped (swallowed silently by the broad except there) - confirmed by
   calling app._emit_voice_event() directly against the running service.
   Extended the Literal to include all six.
2026-08-31 01:03:47 +05:00
didar 9fdaa9472f Revert "fix: let voice early-plan turns answer from the FAQ knowledge base"
deploy / deploy (push) Successful in 31s
This reverts commit 8ced7a59e3.
2026-08-31 00:54:38 +05:00
arystanbek 1dc37d2764 Merge pull request 'fix: treat AST_CAUSE no-route/unallocated as a no-answer retry outcome' (#10) from fix/no-route-retry-cause into main
deploy / deploy (push) Successful in 32s
2026-08-30 19:43:43 +00:00
arys 8382dfa9ba fix: treat AST_CAUSE no-route/unallocated as a no-answer retry outcome
process_agent_dial_outcome only recognized hangup causes 17/18/19/21/34/38.
When the reserved agent's AOR has zero registered contacts (e.g. the
softphone dropped, or nobody ever registered), Asterisk immediately
hangs up with cause 3 (no route to destination) instead of running a
Dial() long enough to produce a DialEnd/NOANSWER at all - so the retry
listener silently ignored it and the escalation was left dangling in
'ringing' status (the agent itself still got released via the
call-ended fallback path, but no retry to the next agent was ever
attempted and the escalation record never reflects the failure).

Added causes 1 (unallocated number), 3 (no route), 20 (subscriber
absent), 22 (number changed) alongside the existing set.
2026-08-31 00:43:20 +05:00
didar 8ced7a59e3 fix: let voice early-plan turns answer from the FAQ knowledge base
deploy / deploy (push) Successful in 32s
The speculative "early plan" turn (computed on partial ASR, before the
caller finishes talking) can win the race and get spoken as the actual
reply, but it unconditionally skipped KB search and answered common
questions (schedule/address/price/status/problem) with a hardcoded
clarifying question even when the FAQ already had the answer.

KB search is a cheap in-memory lexical scan over a DB-cached row set,
so it fits the early-plan latency budget unlike a real LLM call. Now
early-plan runs it and, on a match, answers from the KB snippet
(intent resolved via normalize_intent) instead of guessing a generic
clarifying question; with no match it falls back to the prior
behavior unchanged. operator_request is unaffected.
2026-08-31 00:38:06 +05:00
didar d2438b6954 feat: canonical intent taxonomy for AI operator (kb_answer -> intent_code)
deploy / deploy (push) Successful in 30s
Centralizes fixed control intents and adds a data-driven intent_code
field on kb_articles so many phrasings of the same FAQ question
resolve to one stable code (e.g. VOUCHER_ACTIVATION) instead of a
free-form, unvalidated string the LLM invented on the fly.

- services/shared/intents.py: CONTROL_INTENTS + normalize_intent()
- kb_articles.intent_code column (ORM + dev/sqlite runtime compat +
  migrations/sql/0034_* for postgres/sqlite)
- kb_service CRUD exposes intent_code
- orchestrator surfaces intent_code to the LLM and validates its
  intent output against control intents + the KB codes shown that turn
- voice.py: _voice_early_intent_bucket renamed to _voice_ack_topic_bucket
  to stop it being conflated with the canonical FAQ intent
2026-08-31 00:17:51 +05:00
arystanbek 3826f5704a Merge pull request 'fix: track escalation for legacy AI voice handoff path' (#9) from fix/handoff-escalation-tracking into main
deploy / deploy (push) Successful in 31s
2026-08-30 19:08:20 +00:00
arys 2cb358e80d fix: track escalation for legacy AI voice handoff path
request_handoff() (called by ai_voice_runtime_service for every real
call handoff, the only handoff path production calls actually use)
reserved an agent from the same routing pool as create_escalation()
but never created an EscalationRow, so the Phase 2 no-answer-retry
listener (DialEnd/Hangup) could never find it. A failed transfer
(no SIP registration, no answer, redirect error) left the agent
stuck in RESERVED forever with no retry to the next agent.

Now creates an EscalationRow (status=ringing) alongside the agent
reservation, releases the agent + marks the escalation failed if the
AMI Redirect itself errors immediately, and lets the existing
DialEnd/Hangup handler drive no-answer retry / release exactly like
the /escalations endpoint already does.

Reproduced live: call handed off to extension 2002 with no SIP
contact registered -> immediate hangup, cause=3, both pool agents
stuck in RESERVED indefinitely (had to release manually via psql).
2026-08-31 00:07:57 +05:00
didar 2f4a9795b5 fix: small fixes on filler phrases
deploy / deploy (push) Successful in 34s
2026-08-30 23:52:06 +05:00
didar 48d1fabba1 fix: update ElevenLabs Russian and Kazakh voice IDs for TTS
deploy / deploy (push) Successful in 32s
2026-08-30 23:44:05 +05:00
arystanbek 66652845d8 Merge pull request 'feat: no-answer retry, agent status machine, escalation events (ТЗ Phase 2)' (#8) from feature/escalation-no-answer-retry-and-agent-state into main
deploy / deploy (push) Successful in 32s
2026-08-30 09:19:59 +00:00
arys 1dcfaf46cf feat: no-answer retry, agent status machine, escalation events/timeline (ТЗ §13-15,22,25-27,37, AC-08,16-19)
Phase 2 of the L1->L2 routing engine (Phase 1: MR!4).

- ami_loop() now also captures native AMI DialEnd/Hangup frames (not
  only UserEvent), needed to detect that an escalated agent did not
  answer. No dialplan change required - Redirect already routes the
  client channel into an existing Dial()-based transfer context, so
  Asterisk emits these events on its own; the listener just wasn't
  reading them before.
- retry_escalation_no_answer(): on NOANSWER/BUSY/CANCEL/CHANUNAVAIL/
  CONGESTION, releases the non-answering agent, excludes it, and
  reserves+redirects to the next available agent via the routing
  engine's existing exclude_agent_ids support. Exhausted pool marks
  the escalation failed and leaves the call with the AI instead of
  dropping the client (ТЗ §32).
- Agent status now actually moves through
  RESERVED -> RINGING -> TALKING -> AFTER_CALL_WORK -> AVAILABLE
  instead of staying stuck on RESERVED for the whole call; a new
  acw_sweep_loop background thread (same pattern as the existing
  failed_retry_loop) times out AFTER_CALL_WORK back to AVAILABLE.
- escalations gains attempt_count/real_agent_id/attempted_agent_ids_json
  (migration 0033); fixes a latent bug where assigned_agent_id stored
  the SIP extension instead of the real agent_id despite routing-service
  already returning it in RoutingAgentReserveOut.
- Every transition now records an interaction timeline entry and
  publishes the ТЗ §25 event catalog (AgentReserved/AgentRinging/
  AgentNoAnswer/AgentConnected/TransferCompleted/TransferFailed)
  through the existing emit_voice_event/EventOutboxRow idempotent path.

Not in this MR (see plan): SLA config, Callback, L3 (needs real
technical agents from the business), metrics.
2026-08-30 14:19:23 +05:00
didar 010a8dcab6 fix: ensure emotive ack rotation and prebaked ack caching apply outside v2 queue eligibility
deploy / deploy (push) Successful in 30s
2026-08-30 12:49:26 +05:00
Codex 13ba8f8b56 merge: bring aimaq host-override files back into git (fix/aimaq-remove-host-override)
deploy / deploy (push) Successful in 32s
2026-08-30 07:46:21 +00:00
arystanbek 9d918af12a fix: bring aimaq host-override files (voice.py, persona env) back into git
Both services/ai_orchestrator_service/voice.py and the aimaq persona/DOMAIN
SCOPE prompt were bind-mounted straight from the host on the aimaq stack,
bypassing git and CI entirely since they were first hand-edited in prod.

voice.py: merged the host's live business logic (gas/aimaq domain keyword
list, off-domain Kazakh/Russian replies, disabled re-correction of an already
obtained name) with the timeout_seconds fix from 09bcf74 that never reached
aimaq because the bind mount blocked it.

aimaq.env.production: replaced the AI_OPERATOR_* env values (which were never
interpolated -- {agent_name}/{company_name} would have been read literally)
with the final resolved Zhanna/Kazakgaz Aimaq text including the DOMAIN SCOPE
clause, matching what was actually live on the host.
2026-08-30 07:45:35 +00:00
arystanbek ff85fa27fb Merge pull request 'feat: aimaq voice AI persona Zhanna + language-choice greeting' (#7) from feature/aimaq-zhanna-persona-language-choice into main
deploy / deploy (push) Successful in 30s
2026-08-30 07:22:16 +00:00
arys 82c89eff5a feat: give aimaq voice AI a Qazaqgaz Aimaq persona (Zhanna) and language-choice greeting
Parametrize ai_operator_default_config() via AI_OPERATOR_* env vars,
falling back to the existing hardcoded defaults so the other two stacks
(call-center, sales-call-center) that share this code are unaffected.

Set aimaq-only overrides matching the client-provided script (Скрипт
ии-оператора КГА.docx): agent renamed to Жанна, company to Казакгаз
Аймак, the voice greeting now asks the caller whether Russian or
Kazakh is more convenient before anything else, and the base system
prompt instructs the model to commit to whichever language the caller
picks for the rest of the call, ask how to address them, and confirm
there's nothing else before saying goodbye.
2026-08-30 12:22:00 +05:00
didar 36c2acb011 test: add test for emotive ack rotation not gated on v2 queue eligibility
deploy / deploy (push) Successful in 31s
2026-08-30 12:03:37 +05:00
didar fe9b3f3a80 feat: enhance voice reply logic to prevent duplicate name addressing and improve greeting handling
deploy / deploy (push) Successful in 32s
2026-08-30 11:53:56 +05:00
arystanbek 0a829d26c7 Merge pull request 'fix: TTS ack-bank stale voice cache + website/English pronunciation' (#6) from fix/voice-tts-ack-cache-and-pronunciation into main
deploy / deploy (push) Successful in 30s
2026-08-30 06:39:32 +00:00
arys fe8598e09f fix: TTS ack-bank served stale voice after voice-config changes; add website/English pronunciation rules
RuntimeConfiguredTTSProvider (the live 'dynamic' TTS provider, resolves
voice from DB-backed config) never overrode cache_fingerprint(), so it
fell back to the base class's empty string. PrebakedAckBank keys its
on-disk cache on that fingerprint, so short filler phrases like
'Секунду' kept serving audio baked with the previous ElevenLabs voice
even after a voice change, while full LLM replies (cached inside the
resolved provider itself, keyed on its own voice id) already used the
new voice — explaining why callers heard two different voices in the
same call. Fix: delegate cache_fingerprint() to the resolved provider.

Also extend the voice delivery_hint so the model transliterates website
addresses and English words/abbreviations into spoken Cyrillic instead
of leaving raw Latin text for the TTS engine to mangle (egov.kz was
coming out as 'эговкз').
2026-08-30 11:39:14 +05:00
didar 9e4c47eddd feat: enhance AudioSocketMediaRuntime to skip filler acks for closing intents and throttle repeated filler acks
deploy / deploy (push) Successful in 30s
2026-08-30 11:30:55 +05:00
arystanbek a013ac95e8 Merge pull request 'fix: voice AI spells out numbers/dates for TTS' (#5) from fix/voice-number-pronunciation into main
deploy / deploy (push) Successful in 31s
2026-08-30 06:22:05 +00:00
arys d876a67b3a fix: instruct voice AI to spell out phone numbers and dates in natural spoken Russian
TTS reads raw text digit-by-digit with no number/date normalization layer.
Short numbers like 1414 were read as a single 4-digit number (tysyacha
chetyresta chetyrnadtsat) instead of a spoken code, and dates like '25
chisla' were read in the wrong grammatical case (dvadtsat pyat chislo
instead of dvadtsat pyatogo chisla). Extend the voice delivery_hint with
explicit spell-out rules so the model itself produces already-correct
spoken-form text.
2026-08-30 11:21:42 +05:00
Hermes Agent 92095ff5d6 fix: release reserved L2 agent when AMI redirect fails during escalation
deploy / deploy (push) Successful in 32s
Found during production smoke test: if the AMI Redirect call in
create_escalation() raises (channel gone, AMI hiccup), the agent stays
RESERVED forever with no owning call — orphaned out of the pool until
someone fixes it by hand. Now releases the agent and marks the
escalation failed before re-raising as a 502.
2026-08-29 14:01:40 +05:00
Hermes Agent 8c131440fc config: route voice_lab_ai / ivr_aimaq_ai_ru queues through L2 agent pool
deploy / deploy (push) Successful in 28s
Enables the new Routing Engine for the Tele2 Kazgaz DID +77476456048
(lands on queue 7100/voice_lab_ai): AI handoff now reserves a real L2
agent from the pool instead of the static extension redirect.
2026-08-29 13:37:30 +05:00
didar 6ef2e6b81f fix: update ElevenLabs voice IDs for Russian and Kazakh languages
deploy / deploy (push) Successful in 31s
2026-08-29 13:36:27 +05:00
arystanbek 16ce9659db Merge pull request 'feat: L1->L2 agent pool and routing engine for voice escalation' (#4) from feature/l1-l2-routing-engine into main
deploy / deploy (push) Successful in 32s
2026-08-29 08:31:40 +00:00
Hermes Agent 1ad4e1ec6e feat: surface L2 agent pool and escalations on supervisor screen
- routing-service: GET /escalations (list recent escalation attempts)
- supervisor UI: new panel showing the real agent pool (status, level,
  tenant, skills, calls handled) fed by GET /agents, with a form to add
  operators to the pool
- supervisor UI: new live escalation feed (AI->L2 handoffs, status,
  assigned operator), auto-refreshed every 5s alongside live calls
2026-08-29 13:28:14 +05:00
didar a651b9c086 fix: add name correction action in _voice_downstream_name_update function
deploy / deploy (push) Successful in 30s
2026-08-29 01:53:58 +05:00
didar 78007dae38 fix: improve name matching logic in _voice_reply_with_name function
deploy / deploy (push) Successful in 31s
2026-08-29 01:32:08 +05:00
didar d2a9df36d1 feat: add cache_fingerprint method to TTSProvider and its subclasses for voice/model configuration
deploy / deploy (push) Successful in 30s
2026-08-29 01:23:23 +05:00
didar 09bcf7457c feat: add AI_VOICE_AI_TIMEOUT_SECONDS for configurable voice timeout
deploy / deploy (push) Successful in 30s
2026-08-29 00:50:08 +05:00
didarandClaude Sonnet 5 b8c922c9cf fix: revert AI_VOICE_V2_STREAMING_TTS to disabled
deploy / deploy (push) Successful in 28s
Streaming TTS caused poor voice quality in live testing on Creator
plan too — not just a quota-era fluke. Reverting to non-streaming
synthesis until the root cause is understood.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-29 00:39:46 +05:00
didar 26d718b0ae feat: enable streaming TTS for improved voice interaction
deploy / deploy (push) Successful in 30s
2026-08-29 00:29:44 +05:00
didar d9437d8b21 feat: update ElevenLabs API keys and voice IDs for improved ASR and TTS functionality
deploy / deploy (push) Successful in 32s
2026-08-28 23:45:34 +05:00
Hermes Agent 2243f305b8 feat: L1->L2 agent pool and routing engine for voice escalation
Replaces the hardcoded single-extension redirect for AI->human call
escalation with a real Agent Pool + Routing Engine:

- agents/escalations/routing_rules tables (migration 0031), asterisk_call_links
  gains tenant_id/current_level/required_skills_json/priority.
- services/routing_service/engine.py: level/tenant/skill filtered agent
  selection with atomic (CAS) reservation, no double-booking.
- routing-service: /agents CRUD + /internal/routing/reserve-agent and
  /internal/routing/release-agent.
- asterisk-bridge-service: voice_ai.request_handoff now uses the Routing
  Engine automatically for any queue_code configured in
  ASTERISK_QUEUE_LEVEL_MAP_JSON (all other queue_codes keep the existing
  static ASTERISK_TRANSFER_TARGET_MAP_JSON behavior unchanged); new
  POST /asterisk/live-calls/{call_id}/escalations entrypoint; agent is
  released back to AVAILABLE and the escalation closed when the call ends.

Targets the Tele2 Kazgaz DID +77476456048 (from-tele2-kazgaz context) as the
first queue wired to real L2 routing instead of AI-only.

Known gap (documented in docs/architecture/l1-l2-routing-engine.md):
automatic no-answer retry-to-next-agent needs a small, separately reviewed
dialplan change and is left for a follow-up MR rather than guessed at blind.

Tests: services/routing_service/engine.py covered by
tests/test_routing_engine.py (selection filtering, atomic reservation,
release); existing test_asterisk_bridge_service.py and
test_routing_service_pg_counter.py suites still pass unmodified.
2026-08-28 16:22:32 +05:00
didar b474c35608 feat: enhance acknowledgment responses for Kazakh and Russian languages
deploy / deploy (push) Successful in 33s
2026-08-28 15:12:19 +05:00
didarandClaude Sonnet 5 fadff59512 feat: reduce VAD trailing silence for faster voice bot response
deploy / deploy (push) Successful in 30s
Lower AI_VOICE_VAD_TRAILING_SILENCE_MS from 500 to 350 so the bot
starts responding sooner after the caller stops speaking.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 16:59:00 +05:00
didarandClaude Sonnet 5 7f03b26ef6 feat: switch ElevenLabs RU/KK voice to softer, younger female voice
deploy / deploy (push) Successful in 2m50s
Swap RU and KK TTS voice IDs to "Nataly Mi Soft voice" — a soft,
gentle, young female voice verified for Russian on eleven_turbo_v2_5.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 16:23:44 +05:00
didar 953268e1b8 feat: update ElevenLabs RU voice ID for improved TTS performance
deploy / deploy (push) Successful in 31s
2026-08-25 02:09:48 +05:00
didar f9793dc5f4 feat: enable emotive acknowledgments and update ElevenLabs TTS model ID for enhanced audio responses
deploy / deploy (push) Successful in 30s
2026-08-25 02:00:26 +05:00
didar 7ddc965292 fix: disable AI_VOICE_V2_STREAMING_TTS for improved stability
deploy / deploy (push) Successful in 30s
2026-08-25 01:50:31 +05:00