Initial commit: import from sinmb79/Gov-chat-bot
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,164 @@
|
||||
"""
|
||||
Tier C — LLM 기반 재서술 라우팅 테스트.
|
||||
"""
|
||||
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.llm import NullLLMProvider
|
||||
from app.providers.base import SearchResult
|
||||
|
||||
|
||||
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
|
||||
|
||||
|
||||
def make_router(embedding=None, vectordb=None, llm=None) -> ResponseRouter:
|
||||
providers = {}
|
||||
if embedding:
|
||||
providers["embedding"] = embedding
|
||||
if vectordb:
|
||||
providers["vectordb"] = vectordb
|
||||
if llm:
|
||||
providers["llm"] = llm
|
||||
return ResponseRouter(
|
||||
tenant_config={
|
||||
"phone_number": "031-860-2000",
|
||||
"fallback_dept": "민원과",
|
||||
"tenant_name": "동두천시",
|
||||
},
|
||||
providers=providers,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tier_c_returns_llm_answer():
|
||||
"""LLM 활성화 + RAG 근거 있음 → Tier C 반환."""
|
||||
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
|
||||
|
||||
llm = AsyncMock()
|
||||
llm.generate = AsyncMock(return_value="여권 발급은 민원과(031-860-2000)에서 신청하시면 됩니다.")
|
||||
|
||||
router = make_router(embedding=embedding, vectordb=vectordb, llm=llm)
|
||||
result = await router.route("tenant-1", "여권 어디서 받아요", "user-1", db=db)
|
||||
|
||||
assert result.tier == "C"
|
||||
assert result.source == "llm"
|
||||
assert "여권" in result.answer
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tier_c_skipped_when_llm_is_null():
|
||||
"""NullLLMProvider → Tier C 스킵 → Tier B 또는 D."""
|
||||
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
|
||||
|
||||
llm = NullLLMProvider() # none 기본값
|
||||
|
||||
router = make_router(embedding=embedding, vectordb=vectordb, llm=llm)
|
||||
result = await router.route("tenant-1", "질문", "user-1", db=db)
|
||||
|
||||
# NullLLM → Tier C 스킵 → Tier B (RAG 매칭 있으므로)
|
||||
assert result.tier in ("B", "D")
|
||||
assert result.tier != "C"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tier_c_skipped_when_no_rag_context():
|
||||
"""RAG 근거 없음 → LLM 미호출 (할루시네이션 방지)."""
|
||||
embedding = AsyncMock()
|
||||
embedding.embed = AsyncMock(return_value=[[0.1] * 768])
|
||||
|
||||
vectordb = AsyncMock()
|
||||
vectordb.search = AsyncMock(return_value=[]) # 근거 없음
|
||||
|
||||
db = AsyncMock()
|
||||
|
||||
llm = AsyncMock()
|
||||
llm.generate = AsyncMock(return_value="LLM 답변")
|
||||
|
||||
router = make_router(embedding=embedding, vectordb=vectordb, llm=llm)
|
||||
result = await router.route("tenant-1", "질문", "user-1", db=db)
|
||||
|
||||
# 근거 없으면 LLM 미호출 → Tier D
|
||||
assert result.tier == "D"
|
||||
llm.generate.assert_not_called()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_tier_c_falls_back_to_d_when_llm_fails():
|
||||
"""LLM 실패(None 반환) → Tier D 폴백."""
|
||||
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
|
||||
|
||||
llm = AsyncMock()
|
||||
llm.generate = AsyncMock(return_value=None) # LLM 실패
|
||||
|
||||
router = make_router(embedding=embedding, vectordb=vectordb, llm=llm)
|
||||
result = await router.route("tenant-1", "질문", "user-1", db=db)
|
||||
|
||||
# LLM None → Tier C 없음 → Tier B (RAG 있으므로)
|
||||
assert result.tier in ("B", "D")
|
||||
assert result.tier != "C"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_llm_not_called_without_context_chunks():
|
||||
"""AnthropicLLMProvider: context_chunks 비어있으면 None."""
|
||||
from app.providers.llm_anthropic import AnthropicLLMProvider
|
||||
provider = AnthropicLLMProvider(api_key="test-key")
|
||||
result = await provider.generate("system", "user", context_chunks=[])
|
||||
assert result is None
|
||||
Reference in New Issue
Block a user