Files
call-center/tests/test_ai_whatsapp_orchestrator_service.py

233 lines
8.7 KiB
Python

import importlib
from fastapi.testclient import TestClient
from sqlalchemy import select
ai_module = importlib.import_module("services.ai_orchestrator_service.app")
ai_app = ai_module.app
from services.interaction_service.app import app as interaction_app
from services.shared.core import new_id, utc_now_iso
from services.shared.db import get_session
from services.shared.sql_models import AISessionRow, KBArticleRow, KBCategoryRow, WhatsAppThreadRow
from services.whatsapp_adapter_service import app as whatsapp_module
from services.whatsapp_adapter_service.app import app as whatsapp_app
def admin_headers():
return {"X-User": "admin", "X-Role": "admin"}
def _u(value: str) -> str:
return value.encode("ascii").decode("unicode_escape")
def seed_kb_article(
title: str,
body: str,
tag: str,
*,
language: str = "ru",
article_group_id: str | None = None,
) -> dict[str, str]:
session = get_session()
try:
now = utc_now_iso()
category_id = new_id("kbc")
article_id = new_id("kba")
resolved_group_id = article_group_id or article_id
session.add(
KBCategoryRow(
category_id=category_id,
name="WhatsApp AI",
description="AI test category",
created_at=now,
)
)
session.add(
KBArticleRow(
article_id=article_id,
category_id=category_id,
article_group_id=resolved_group_id,
language=language,
title=title,
body=body,
tags_json=f'["{tag}"]',
created_at=now,
updated_at=now,
)
)
session.commit()
return {"article_id": article_id, "article_group_id": resolved_group_id}
finally:
session.close()
def patch_ai_internal_calls(monkeypatch, whatsapp_client: TestClient, interaction_client: TestClient):
def fake_whatsapp_request(method: str, path: str, *, payload: dict | None = None) -> dict:
response = whatsapp_client.request(method, path, json=payload, headers=admin_headers())
response.raise_for_status()
return response.json()
def fake_interaction_request(method: str, path: str, *, payload: dict | None = None) -> dict:
response = interaction_client.request(method, path, json=payload, headers=admin_headers())
response.raise_for_status()
return response.json()
monkeypatch.setattr(ai_module, "_whatsapp_request", fake_whatsapp_request)
monkeypatch.setattr(ai_module, "_interaction_request", fake_interaction_request)
def create_inbound_thread(whatsapp_client: TestClient, chat_id: str, text: str) -> dict:
response = whatsapp_client.post(
"/integrations/whatsapp/webhook",
json={
"chat_id": chat_id,
"text": text,
"external_message_id": f"{chat_id}-ext-1",
"whatsapp_user_id": f"user-{chat_id}",
"phone_number": "+77010000000",
"display_name": f"Lead {chat_id}",
},
)
assert response.status_code == 200
return response.json()
def fetch_ai_summary(whatsapp_client: TestClient, thread_id: str):
return whatsapp_client.get(
f"/integrations/whatsapp/threads/{thread_id}/ai-summary",
headers=admin_headers(),
)
def test_ai_whatsapp_always_reply_mode_answers_without_handoff(monkeypatch):
monkeypatch.setenv("AI_WHATSAPP_ENABLED", "1")
monkeypatch.setenv("AI_WHATSAPP_ALWAYS_REPLY", "1")
monkeypatch.setenv("AI_PROVIDER", "stub")
monkeypatch.setattr(whatsapp_module, "_ai_enqueue_request", lambda thread_id, trigger_message_id: None)
monkeypatch.setattr(whatsapp_module, "_start_whatsapp_reply_delivery", lambda message_id: None)
whatsapp_client = TestClient(whatsapp_app)
interaction_client = TestClient(interaction_app)
ai_client = TestClient(ai_app)
patch_ai_internal_calls(monkeypatch, whatsapp_client, interaction_client)
seed_kb_article("WhatsApp greeting", "Здравствуйте! Поможем через WhatsApp.", "greeting")
created = create_inbound_thread(whatsapp_client, "wa_ai_greeting", "Привет")
enqueue = ai_client.post(
f"/ai/whatsapp/threads/{created['thread_id']}/enqueue",
headers=admin_headers(),
json={"trigger_message_id": created["message_id"]},
)
assert enqueue.status_code == 200
assert enqueue.json()["status"] == "done"
thread = whatsapp_client.get(
f"/integrations/whatsapp/threads/{created['thread_id']}",
headers=admin_headers(),
)
assert thread.status_code == 200
assert thread.json()["ai_state"] == "active"
messages = whatsapp_client.get(
f"/integrations/whatsapp/threads/{created['thread_id']}/messages",
headers=admin_headers(),
)
assert messages.status_code == 200
assert messages.json()[-1]["author_type"] == "ai"
assert messages.json()[-1]["text"]
summary = fetch_ai_summary(whatsapp_client, created["thread_id"])
assert summary.status_code == 200
assert summary.json() is None
def test_ai_whatsapp_relaxed_kb_search_answers_phrase_query(monkeypatch):
monkeypatch.setenv("AI_WHATSAPP_ENABLED", "1")
monkeypatch.setenv("AI_PROVIDER", "stub")
monkeypatch.setattr(whatsapp_module, "_ai_enqueue_request", lambda thread_id, trigger_message_id: None)
monkeypatch.setattr(whatsapp_module, "_start_whatsapp_reply_delivery", lambda message_id: None)
whatsapp_client = TestClient(whatsapp_app)
interaction_client = TestClient(interaction_app)
ai_client = TestClient(ai_app)
patch_ai_internal_calls(monkeypatch, whatsapp_client, interaction_client)
seed_kb_article(
"Tariff warelaxx",
"Tariff warelaxx activates after the request is confirmed.",
"warelaxx",
)
created = create_inbound_thread(
whatsapp_client,
"wa_ai_relaxed_kb",
_u(r"\u0425\u043e\u0447\u0443 \u0443\u0437\u043d\u0430\u0442\u044c \u0442\u0430\u0440\u0438\u0444 warelaxx"),
)
enqueue = ai_client.post(
f"/ai/whatsapp/threads/{created['thread_id']}/enqueue",
headers=admin_headers(),
json={"trigger_message_id": created["message_id"]},
)
assert enqueue.status_code == 200
assert enqueue.json()["status"] == "done"
messages = whatsapp_client.get(
f"/integrations/whatsapp/threads/{created['thread_id']}/messages",
headers=admin_headers(),
)
assert messages.status_code == 200
assert messages.json()[-1]["author_type"] == "ai"
assert "warelaxx" in messages.json()[-1]["text"].lower()
def test_ai_whatsapp_handoff_creates_summary(monkeypatch):
monkeypatch.setenv("AI_WHATSAPP_ENABLED", "1")
monkeypatch.delenv("AI_WHATSAPP_ALWAYS_REPLY", raising=False)
monkeypatch.setenv("AI_PROVIDER", "stub")
monkeypatch.setattr(whatsapp_module, "_ai_enqueue_request", lambda thread_id, trigger_message_id: None)
monkeypatch.setattr(whatsapp_module, "_start_whatsapp_reply_delivery", lambda message_id: None)
whatsapp_client = TestClient(whatsapp_app)
interaction_client = TestClient(interaction_app)
ai_client = TestClient(ai_app)
patch_ai_internal_calls(monkeypatch, whatsapp_client, interaction_client)
created = create_inbound_thread(whatsapp_client, "wa_ai_handoff", "Соедините с оператором")
enqueue = ai_client.post(
f"/ai/whatsapp/threads/{created['thread_id']}/enqueue",
headers=admin_headers(),
json={"trigger_message_id": created["message_id"]},
)
assert enqueue.status_code == 200
assert enqueue.json()["status"] == "handoff_required"
thread = whatsapp_client.get(
f"/integrations/whatsapp/threads/{created['thread_id']}",
headers=admin_headers(),
)
assert thread.status_code == 200
assert thread.json()["ai_state"] == "handoff_required"
assert thread.json()["claimed_by_user"] is None
summary = fetch_ai_summary(whatsapp_client, created["thread_id"])
assert summary.status_code == 200
payload = summary.json()
assert payload["thread_id"] == created["thread_id"]
assert payload["status_tone"] == "handoff"
assert payload["customer_request_text"] == created["text"]
assert payload["handoff_reason"]
session = get_session()
try:
thread_row = session.execute(
select(WhatsAppThreadRow).where(WhatsAppThreadRow.thread_id == created["thread_id"])
).scalar_one()
ai_session = session.execute(
select(AISessionRow).where(AISessionRow.session_id == thread_row.ai_session_id)
).scalar_one()
assert thread_row.ai_state == "handoff_required"
assert ai_session.status == "handoff_required"
finally:
session.close()