Revert "fix: let voice early-plan turns answer from the FAQ knowledge base"
deploy / deploy (push) Successful in 31s
deploy / deploy (push) Successful in 31s
This reverts commit 8ced7a59e3.
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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}")
|
||||
|
||||
Reference in New Issue
Block a user