145 lines
4.1 KiB
Python
145 lines
4.1 KiB
Python
"""
|
|
Tier B RAG 라우팅 테스트.
|
|
"""
|
|
import pytest
|
|
from unittest.mock import AsyncMock, MagicMock
|
|
from uuid import uuid4
|
|
from datetime import datetime
|
|
|
|
from app.services.routing import ResponseRouter
|
|
from app.providers.base import SearchResult
|
|
|
|
|
|
def make_router(embedding=None, vectordb=None) -> ResponseRouter:
|
|
providers = {}
|
|
if embedding:
|
|
providers["embedding"] = embedding
|
|
if vectordb:
|
|
providers["vectordb"] = vectordb
|
|
return ResponseRouter(
|
|
tenant_config={
|
|
"phone_number": "031-860-2000",
|
|
"fallback_dept": "민원과",
|
|
"tenant_name": "동두천시",
|
|
},
|
|
providers=providers,
|
|
)
|
|
|
|
|
|
def make_doc(doc_id: str) -> MagicMock:
|
|
doc = MagicMock()
|
|
doc.id = doc_id
|
|
doc.filename = "안내문.txt"
|
|
doc.is_active = True
|
|
doc.published_at = datetime(2026, 3, 1)
|
|
return doc
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tier_b_returns_when_doc_found():
|
|
"""문서 RAG 매칭 → Tier B 반환."""
|
|
doc_id = str(uuid4())
|
|
doc = make_doc(doc_id)
|
|
|
|
embedding = AsyncMock()
|
|
embedding.embed = AsyncMock(return_value=[[0.1] * 768])
|
|
|
|
vectordb = AsyncMock()
|
|
vectordb.search = AsyncMock(return_value=[
|
|
SearchResult(text="여권은 민원과에서 발급합니다.", doc_id=f"{doc_id}_0",
|
|
score=0.75, metadata={"doc_id": doc_id})
|
|
])
|
|
|
|
db = AsyncMock()
|
|
db.execute = AsyncMock()
|
|
scalar = MagicMock()
|
|
scalar.scalar_one_or_none = MagicMock(return_value=doc)
|
|
db.execute.return_value = scalar
|
|
|
|
router = make_router(embedding=embedding, vectordb=vectordb)
|
|
result = await router.route("tenant-1", "여권 발급 방법", "user-1", db=db)
|
|
|
|
assert result.tier == "B"
|
|
assert result.source == "rag"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tier_b_skipped_when_no_doc_match():
|
|
"""문서 매칭 없으면 Tier D."""
|
|
embedding = AsyncMock()
|
|
embedding.embed = AsyncMock(return_value=[[0.1] * 768])
|
|
|
|
vectordb = AsyncMock()
|
|
vectordb.search = AsyncMock(return_value=[])
|
|
|
|
db = AsyncMock()
|
|
|
|
router = make_router(embedding=embedding, vectordb=vectordb)
|
|
result = await router.route("tenant-1", "모르는 질문", "user-1", db=db)
|
|
|
|
assert result.tier == "D"
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tier_b_result_has_doc_name():
|
|
"""Tier B 결과에 doc_name 포함."""
|
|
doc_id = str(uuid4())
|
|
doc = make_doc(doc_id)
|
|
|
|
embedding = AsyncMock()
|
|
embedding.embed = AsyncMock(return_value=[[0.1] * 768])
|
|
|
|
vectordb = AsyncMock()
|
|
vectordb.search = AsyncMock(return_value=[
|
|
SearchResult(text="내용", doc_id=f"{doc_id}_0", score=0.72,
|
|
metadata={"doc_id": doc_id})
|
|
])
|
|
|
|
db = AsyncMock()
|
|
db.execute = AsyncMock()
|
|
scalar = MagicMock()
|
|
scalar.scalar_one_or_none = MagicMock(return_value=doc)
|
|
db.execute.return_value = scalar
|
|
|
|
router = make_router(embedding=embedding, vectordb=vectordb)
|
|
result = await router.route("tenant-1", "질문", "user-1", db=db)
|
|
|
|
assert result.doc_name is not None
|
|
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_tier_a_takes_priority_over_tier_b():
|
|
"""FAQ 유사도 ≥ 0.85 → Tier A (Tier B 미실행)."""
|
|
faq_id = str(uuid4())
|
|
faq = MagicMock()
|
|
faq.id = faq_id
|
|
faq.answer = "FAQ 답변"
|
|
faq.question = "질문"
|
|
faq.is_active = True
|
|
faq.hit_count = 0
|
|
faq.updated_at = None
|
|
|
|
embedding = AsyncMock()
|
|
embedding.embed = AsyncMock(return_value=[[0.1] * 768])
|
|
|
|
# FAQ 검색: 높은 유사도
|
|
vectordb = AsyncMock()
|
|
vectordb.search = AsyncMock(return_value=[
|
|
SearchResult(text="Q\nA", doc_id=f"{faq_id}_0", score=0.92,
|
|
metadata={"faq_id": faq_id})
|
|
])
|
|
|
|
db = AsyncMock()
|
|
db.execute = AsyncMock()
|
|
scalar = MagicMock()
|
|
scalar.scalar_one_or_none = MagicMock(return_value=faq)
|
|
db.execute.return_value = scalar
|
|
db.get = AsyncMock(return_value=faq)
|
|
db.commit = AsyncMock()
|
|
|
|
router = make_router(embedding=embedding, vectordb=vectordb)
|
|
result = await router.route("tenant-1", "질문", "user-1", db=db)
|
|
|
|
# FAQ가 먼저 매칭되면 Tier A
|
|
assert result.tier == "A"
|