233 lines
8.7 KiB
Python
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()
|