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.
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
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>
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>
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>
AudioSocket was delivering 16kHz audio despite the telecom-kz trunk being
codec-restricted to alaw/ulaw, causing the 8k->16k ASR resample to double
an already-16kHz stream to an effective 32kHz labeled as 16000 Hz -
audible as slow, deep-pitched, unintelligible speech. Force
audioread/writeformat=slin before AudioSocket() so the channel always
delivers narrowband 8kHz, matching every rate assumption in the runtime.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Add sync_ai_operator_config_from_code() which overwrites the DB-cached
ai_operator_settings row from ai_operator_default_config() on startup
of ai_orchestrator_service and ai_voice_runtime_service. Greeting and
system prompt changes now go through git + deploy instead of manual
psql/API edits to prod. Also adds a root README pointing to existing docs.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
A separate deployment on the same host (aimaq-call-center) already binds
127.0.0.1:9019, so this project's ai-voice-runtime-service could never
start there. Remap the host side to 9024 and point Asterisk's
AudioSocket target at the new port; the container still listens on 9019
internally.
The previous marker was added to operator_persona.py's inline fallback,
which only fires when config is None. Every real call path loads a
populated AIOperatorConfig via load_effective_ai_operator_config(),
so ai_operator_default_config() in services/shared/ai_operator_config.py
is the default that's actually served.