From 9fdaa9472fa644e8bf8d9812b051c05f23650af6 Mon Sep 17 00:00:00 2001 From: didar Date: Mon, 31 Aug 2026 00:54:38 +0500 Subject: [PATCH] Revert "fix: let voice early-plan turns answer from the FAQ knowledge base" This reverts commit 8ced7a59e3fdc98ce48308aaf7ff23259404ca0e. --- services/ai_orchestrator_service/voice.py | 83 +++++------------------ tests/test_ai_orchestrator_service.py | 73 -------------------- 2 files changed, 17 insertions(+), 139 deletions(-) diff --git a/services/ai_orchestrator_service/voice.py b/services/ai_orchestrator_service/voice.py index 6b89914..e4c4cfe 100644 --- a/services/ai_orchestrator_service/voice.py +++ b/services/ai_orchestrator_service/voice.py @@ -19,7 +19,6 @@ from services.shared.ai_context_summary import ( ) from services.shared.core import new_id, utc_now_iso from services.shared.db import get_session -from services.shared.intents import normalize_intent from services.shared.models import VoiceAIStartIn, VoiceAIStartOut, VoiceAITurnDecisionOut, VoiceAITurnIn, VoiceStartResult from services.shared.sql_models import ( AISessionRow, @@ -1535,7 +1534,6 @@ def _voice_early_plan( transcript_text: str, context_summary: str | dict[str, Any] | None = None, request_metadata: dict[str, Any] | None = None, - kb_results: list[Any] = (), ) -> dict[str, Any]: v2_metadata = _voice_v2_metadata(transcript_text, request_metadata) payload = request_metadata if isinstance(request_metadata, dict) else {} @@ -1578,56 +1576,10 @@ def _voice_early_plan( "reply_phase": "early_plan", }, } - if early_intent == "operator_request": - return { - "language": language, - "intent": "handoff_request", - "reply_text": _voice_compact_reply_text(_voice_handoff_reply(language), language=language), - "confidence": 0.62, - "needs_handoff": True, - "handoff_reason": "Запрос требует участия живого оператора.", - "case_action": "keep_open", - "kb_refs": [], - "summary_text": "Early domain plan is prepared.", - "model": "voice_early_plan_domain", - "latency_ms": 1, - "metadata": { - **v2_metadata, - "reply_phase": "early_plan", - "early_intent": early_intent, - }, - } - if early_intent in {"schedule", "address", "price", "status", "problem"} and kb_results: - # A cheap lexical KB lookup fits the early-plan latency budget (no LLM - # round-trip), unlike the properly grounded/paraphrased "final" decision. - # Answering from the KB here beats guessing a generic clarifying question - # when the FAQ already has the answer — see the plan doc for why this - # branch exists at all (the early reply can win the race and get spoken - # before the final, LLM-grounded decision is ready). - article = kb_results[0] - snippet = _app()._article_snippet(article, limit=220) - reply_text = f"Қысқаша айтайын: {snippet}" if language == "kz" else f"Коротко подскажу: {snippet}" - topic_code = getattr(article, "intent_code", None) - return { - "language": language, - "intent": normalize_intent(topic_code or "kb_answer", known_topic_codes=[topic_code] if topic_code else []), - "reply_text": _voice_compact_reply_text(reply_text, language=language), - "confidence": 0.7, - "needs_handoff": False, - "handoff_reason": None, - "case_action": "keep_open", - "kb_refs": [article.article_id], - "summary_text": "AI ответил по базе ЧЗВ на предварительной стадии.", - "model": "voice_early_plan_kb", - "latency_ms": 1, - "metadata": { - **v2_metadata, - "reply_phase": "early_plan", - "early_intent": early_intent, - }, - } - if early_intent in {"schedule", "address", "price", "status", "problem"}: + if early_intent in {"schedule", "address", "price", "status", "problem", "operator_request"}: reply_text = _voice_summary_slot_prompt(language, context_summary) or _voice_topic_prompt(language, [transcript_text]) + if early_intent == "operator_request": + reply_text = _voice_handoff_reply(language) if not reply_text and language == "kz": if early_intent == "schedule": reply_text = "Qai filialdyn, mekenjaidyn nemese qalanyng jumys uaqyty qyzyqtyratynyn aitnyz." @@ -1653,11 +1605,15 @@ def _voice_early_plan( if reply_text: return { "language": language, - "intent": "clarification", + "intent": "handoff_request" if early_intent == "operator_request" else "clarification", "reply_text": _voice_compact_reply_text(reply_text, language=language), "confidence": 0.62, - "needs_handoff": False, - "handoff_reason": None, + "needs_handoff": early_intent == "operator_request", + "handoff_reason": ( + "Запрос требует участия Р¶РёРІРѕРіРѕ оператора." + if early_intent == "operator_request" + else None + ), "case_action": "keep_open", "kb_refs": [], "summary_text": "Early domain plan is prepared.", @@ -1880,7 +1836,6 @@ def _voice_decision( transcript_text=transcript_text, context_summary=context_summary, request_metadata=request_metadata, - kb_results=kb_results, ) if persona.is_identity_request(normalized): @@ -2582,17 +2537,13 @@ def turn_voice_session(session_id: str, payload: VoiceAITurnIn) -> VoiceAITurnDe ) ai_session.context_summary_json = dump_context_summary(user_context_summary) ai_session.context_summary_updated_at = now - # Unlike name extraction/context-summary writes above (skipped for - # early_plan_only since they're stateful and heavier), KB search is a - # cheap in-memory lexical scan over a DB-cached row set (see - # _load_kb_rows_cached) and comfortably fits the early-plan latency - # budget, so it always runs — this lets _voice_early_plan answer from - # the FAQ instead of guessing a generic clarifying question. - kb_results = app._kb_search( - session, - payload.transcript_text, - language=ai_session.language, - ) + kb_results = [] + if not early_plan_only: + kb_results = app._kb_search( + session, + payload.transcript_text, + language=ai_session.language, + ) disclosure_required = voice_session.disclosure_played_at is None decision = _voice_decision( language=ai_session.language or "ru", diff --git a/tests/test_ai_orchestrator_service.py b/tests/test_ai_orchestrator_service.py index 880e8ce..a474649 100644 --- a/tests/test_ai_orchestrator_service.py +++ b/tests/test_ai_orchestrator_service.py @@ -1589,79 +1589,6 @@ def test_voice_v2_streaming_duplex_early_plan_returns_domain_followup_without_ll assert decision["metadata"]["early_intent"] == "schedule" -def test_voice_v2_streaming_duplex_early_plan_answers_from_kb_without_llm(monkeypatch): - monkeypatch.setenv("AI_VOICE_POLICY_MODE", "v2_streaming_duplex") - - def _unexpected_llm(messages, **kwargs): - raise AssertionError(f"LLM should not be called for early plan: {messages!r}") - - monkeypatch.setattr(ai_module, "_request_structured_model_decision", _unexpected_llm) - - kb_article = SimpleNamespace( - article_id="kba_early_schedule", - title="График работы филиалов", - body="Филиалы работают с понедельника по пятницу с 9:00 до 18:00.", - intent_code="BRANCH_SCHEDULE", - ) - decision = voice_module._voice_decision( - language="ru", - customer=None, - interaction=SimpleNamespace(interaction_id="int_voice_early_schedule_kb", status="new", queue_id="que_voice", subject="unknown"), - transcript_text="Мне надо узнать график работы", - transcript_window=[], - kb_results=[kb_article], - disclosure_required=False, - request_metadata={ - "voice_v2_enabled": True, - "reply_phase": "early_plan", - "response_plan_id": "rsp_early_schedule_kb", - "early_intent": "schedule", - }, - ) - - assert decision["model"] == "voice_early_plan_kb" - assert decision["intent"] == "BRANCH_SCHEDULE" - assert decision["kb_refs"] == ["kba_early_schedule"] - assert decision["needs_handoff"] is False - assert "9:00" in decision["reply_text"] or "9" in decision["reply_text"] - assert decision["metadata"]["reply_phase"] == "early_plan" - - -def test_voice_v2_streaming_duplex_early_plan_operator_request_skips_kb(monkeypatch): - monkeypatch.setenv("AI_VOICE_POLICY_MODE", "v2_streaming_duplex") - - def _unexpected_llm(messages, **kwargs): - raise AssertionError(f"LLM should not be called for early plan: {messages!r}") - - monkeypatch.setattr(ai_module, "_request_structured_model_decision", _unexpected_llm) - - kb_article = SimpleNamespace( - article_id="kba_should_not_be_used", - title="Unrelated article", - body="Should not be referenced for an operator handoff.", - intent_code="SOMETHING_ELSE", - ) - decision = voice_module._voice_decision( - language="ru", - customer=None, - interaction=SimpleNamespace(interaction_id="int_voice_early_operator", status="new", queue_id="que_voice", subject="unknown"), - transcript_text="Соедините меня с оператором", - transcript_window=[], - kb_results=[kb_article], - disclosure_required=False, - request_metadata={ - "voice_v2_enabled": True, - "reply_phase": "early_plan", - "response_plan_id": "rsp_early_operator", - "early_intent": "operator_request", - }, - ) - - assert decision["intent"] == "handoff_request" - assert decision["needs_handoff"] is True - assert decision["kb_refs"] == [] - - def test_voice_decision_hearing_check_keeps_active_topic_without_llm(monkeypatch): def _unexpected_llm(messages, **kwargs): raise AssertionError(f"LLM should not be called for hearing check: {messages!r}")