Files
wehub-resource-sync e4dcfc49aa
Tests / Import Check (Python 3.13) (push) Has been cancelled
Tests / Import Check (Python 3.14) (push) Has been cancelled
Tests / Python Tests (Python 3.11) (push) Has been cancelled
Tests / Python Tests (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.14) (push) Has been cancelled
Tests / Test Summary (push) Has been cancelled
Tests / Lint and Format (push) Has been cancelled
Tests / Web Node Tests (push) Has been cancelled
Tests / Import Check (Python 3.11) (push) Has been cancelled
Tests / Import Check (Python 3.12) (push) Has been cancelled
Tests / Python Tests (Python 3.13) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:00:43 +08:00

346 lines
13 KiB
Python

"""RAGService end-to-end behavior tests (with a fake pipeline)."""
from __future__ import annotations
import asyncio
import importlib
import logging
from typing import Any, Dict
import pytest
import deeptutor.services.rag.service as rag_service_module
from deeptutor.services.rag.service import RAGService
from deeptutor.services.rag.smart_retriever import SmartRetriever
class _FakePipeline:
"""Minimal pipeline stub that records calls and returns canned results."""
def __init__(self, search_result: Dict[str, Any] | None = None) -> None:
self.calls: list[dict] = []
self.search_result = search_result or {
"answer": "fake answer",
"sources": [{"id": 1}],
"provider": "lightrag", # deliberately wrong; service must overwrite
}
async def initialize(self, kb_name: str, file_paths, **kwargs) -> bool:
self.calls.append({"op": "initialize", "kb_name": kb_name, "files": list(file_paths)})
return True
async def add_documents(self, kb_name: str, file_paths, **kwargs) -> bool:
self.calls.append({"op": "add_documents", "kb_name": kb_name, "files": list(file_paths)})
return True
async def search(self, query: str, kb_name: str, **kwargs) -> Dict[str, Any]:
self.calls.append({"op": "search", "query": query, "kb_name": kb_name, "kwargs": kwargs})
return dict(self.search_result)
async def delete(self, kb_name: str) -> bool:
self.calls.append({"op": "delete", "kb_name": kb_name})
return True
@pytest.fixture
def fake_service(tmp_path) -> tuple[RAGService, _FakePipeline]:
pipeline = _FakePipeline()
service = RAGService(kb_base_dir=str(tmp_path))
# No metadata.json under tmp_path → KBs resolve to the default provider.
service._pipelines["llamaindex"] = pipeline # type: ignore[attr-defined]
return service, pipeline
def test_provider_argument_honored_for_known_provider(tmp_path) -> None:
"""An explicit known provider wins (used at create time); unknown/legacy
strings collapse to the default engine."""
assert RAGService(kb_base_dir=str(tmp_path), provider="lightrag").provider == "lightrag"
assert RAGService(kb_base_dir=str(tmp_path), provider="raganything").provider == "llamaindex"
@pytest.mark.asyncio
async def test_search_force_overwrites_provider_in_result(fake_service) -> None:
"""Even if the underlying pipeline lies about its provider, RAGService normalizes."""
service, pipeline = fake_service
pipeline.search_result = {"answer": "x", "provider": "raganything"}
result = await service.search(query="hello", kb_name="kb")
assert result["provider"] == "llamaindex"
@pytest.mark.asyncio
async def test_search_forwards_mode_kwarg_to_pipeline(fake_service) -> None:
"""Mode-aware engines must receive explicit retrieval mode overrides."""
service, pipeline = fake_service
await service.search(query="hi", kb_name="kb", mode="hybrid", top_k=5)
last = pipeline.calls[-1]
assert last["op"] == "search"
assert last["kwargs"].get("mode") == "hybrid"
assert last["kwargs"].get("top_k") == 5
def test_ragservice_routes_provider_from_kb_config_when_metadata_missing(tmp_path) -> None:
kb = tmp_path / "kb"
kb.mkdir()
(tmp_path / "kb_config.json").write_text(
'{"knowledge_bases": {"kb": {"rag_provider": "lightrag"}}}',
encoding="utf-8",
)
service = RAGService(kb_base_dir=str(tmp_path))
assert service._resolve_provider("kb") == "lightrag"
def test_ragservice_prefers_authoritative_config_over_stale_metadata(tmp_path) -> None:
kb = tmp_path / "kb"
kb.mkdir()
(kb / "metadata.json").write_text('{"rag_provider": "llamaindex"}', encoding="utf-8")
(tmp_path / "kb_config.json").write_text(
'{"knowledge_bases": {"kb": {"rag_provider": "lightrag"}}}',
encoding="utf-8",
)
service = RAGService(kb_base_dir=str(tmp_path))
assert service._resolve_provider("kb") == "lightrag"
@pytest.mark.asyncio
async def test_search_aliases_answer_and_content(fake_service) -> None:
"""Pipelines that only return ``content`` should still expose ``answer`` and vice versa."""
service, pipeline = fake_service
pipeline.search_result = {"content": "only-content", "provider": "x"}
result = await service.search(query="q", kb_name="kb")
assert result["answer"] == "only-content"
assert result["content"] == "only-content"
assert result["query"] == "q"
pipeline.search_result = {"answer": "only-answer", "provider": "x"}
result = await service.search(query="q2", kb_name="kb")
assert result["content"] == "only-answer"
assert result["answer"] == "only-answer"
@pytest.mark.asyncio
async def test_search_forwards_lightrag_native_logs_to_event_sink(
fake_service,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, pipeline = fake_service
lightrag_logger = logging.getLogger("lightrag")
original_handlers = list(lightrag_logger.handlers)
original_propagate = lightrag_logger.propagate
original_level = lightrag_logger.level
events: list[tuple[str, str, dict]] = []
async def event_sink(event_type: str, message: str, metadata: dict) -> None:
events.append((event_type, message, metadata))
async def search_with_native_log(query: str, kb_name: str, **kwargs) -> Dict[str, Any]:
lightrag_logger.info("Final context: 14 entities, 13 relations, 1 chunks")
lightrag_logger.warning("Rerank is enabled but no rerank model is configured.")
return {"answer": "ok", "provider": "lightrag"}
original_import_module = importlib.import_module
def fake_import_module(name: str, package: str | None = None):
if name == "lightrag.utils":
lightrag_logger.propagate = False
return object()
return original_import_module(name, package)
monkeypatch.setattr(pipeline, "search", search_with_native_log)
monkeypatch.setattr(rag_service_module.importlib, "import_module", fake_import_module)
try:
lightrag_logger.handlers = []
lightrag_logger.propagate = True
lightrag_logger.setLevel(logging.INFO)
await service.search(query="hello", kb_name="kb", event_sink=event_sink)
await asyncio.sleep(0)
finally:
lightrag_logger.handlers = original_handlers
lightrag_logger.propagate = original_propagate
lightrag_logger.setLevel(original_level)
raw_logs = [
(message, metadata) for event_type, message, metadata in events if event_type == "raw_log"
]
assert any(
message == "Final context: 14 entities, 13 relations, 1 chunks"
and metadata.get("logger") == "lightrag"
and metadata.get("level") == "INFO"
for message, metadata in raw_logs
)
assert any(
message == "Rerank is enabled but no rerank model is configured."
and metadata.get("logger") == "lightrag"
and metadata.get("level") == "WARNING"
for message, metadata in raw_logs
)
@pytest.mark.asyncio
async def test_search_forwards_graphrag_native_logs_to_event_sink(
fake_service,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, pipeline = fake_service
graphrag_logger = logging.getLogger("graphrag.api.query")
original_handlers = list(graphrag_logger.handlers)
original_propagate = graphrag_logger.propagate
original_level = graphrag_logger.level
events: list[tuple[str, str, dict]] = []
async def event_sink(event_type: str, message: str, metadata: dict) -> None:
events.append((event_type, message, metadata))
async def search_with_native_log(query: str, kb_name: str, **kwargs) -> Dict[str, Any]:
graphrag_logger.info("Executing local search query: %s", query)
return {"answer": "ok", "provider": "graphrag"}
monkeypatch.setattr(pipeline, "search", search_with_native_log)
try:
graphrag_logger.handlers = []
graphrag_logger.propagate = True
graphrag_logger.setLevel(logging.INFO)
await service.search(query="hello", kb_name="kb", event_sink=event_sink)
await asyncio.sleep(0)
finally:
graphrag_logger.handlers = original_handlers
graphrag_logger.propagate = original_propagate
graphrag_logger.setLevel(original_level)
assert any(
event_type == "raw_log"
and message == "Executing local search query: hello"
and metadata.get("logger") == "graphrag.api.query"
for event_type, message, metadata in events
)
@pytest.mark.asyncio
async def test_search_filters_noisy_vector_and_embedding_logs(
fake_service,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, pipeline = fake_service
lightrag_logger = logging.getLogger("lightrag")
original_lightrag_level = lightrag_logger.level
original_lightrag_propagate = lightrag_logger.propagate
events: list[tuple[str, str, dict]] = []
async def event_sink(event_type: str, message: str, metadata: dict) -> None:
events.append((event_type, message, metadata))
async def search_with_noisy_logs(query: str, kb_name: str, **kwargs) -> Dict[str, Any]:
logging.getLogger("nano-vectordb").info("Load (13, 4096) data")
logging.getLogger("nano-vectordb").info(
"Init {'embedding_dim': 4096, 'metric': 'cosine'} 13 data"
)
logging.getLogger("deeptutor.services.embedding.adapters.openai_compatible").info(
"Successfully generated 1 embeddings (model: Qwen/Qwen3-Embedding-8B, dimensions: 4096)"
)
logging.getLogger("lightrag").info(
"Raw search results: 14 entities, 13 relations, 0 vector chunks"
)
return {"answer": "ok", "provider": "lightrag"}
monkeypatch.setattr(pipeline, "search", search_with_noisy_logs)
try:
lightrag_logger.setLevel(logging.INFO)
lightrag_logger.propagate = True
await service.search(query="hello", kb_name="kb", event_sink=event_sink)
await asyncio.sleep(0)
finally:
lightrag_logger.setLevel(original_lightrag_level)
lightrag_logger.propagate = original_lightrag_propagate
raw_messages = [message for event_type, message, _metadata in events if event_type == "raw_log"]
assert "Raw search results: 14 entities, 13 relations, 0 vector chunks" in raw_messages
assert not any(message.startswith("Load ") for message in raw_messages)
assert not any(message.startswith("Init ") for message in raw_messages)
assert not any("Successfully generated 1 embeddings" in message for message in raw_messages)
@pytest.mark.asyncio
async def test_add_documents_delegates_to_pipeline(fake_service) -> None:
service, pipeline = fake_service
assert await service.add_documents(kb_name="kb", file_paths=["doc.txt"]) is True
assert pipeline.calls[-1] == {
"op": "add_documents",
"kb_name": "kb",
"files": ["doc.txt"],
}
@pytest.mark.asyncio
async def test_smart_retrieve_aggregates_passages_with_query_hints(
fake_service,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, pipeline = fake_service
pipeline.search_result = {"answer": "PASSAGE", "content": "PASSAGE", "provider": "x"}
async def _fake_aggregate(_self, _ctx, passages):
return "AGG:" + "|".join(passages)
monkeypatch.setattr(SmartRetriever, "_aggregate", _fake_aggregate, raising=True)
out = await service.smart_retrieve(
context="anything",
kb_name="kb",
query_hints=["q1", "q2"],
)
assert out["answer"].startswith("AGG:")
assert out["answer"].count("PASSAGE") == 2
assert len(out["sources"]) == 2
queries = [c["query"] for c in pipeline.calls if c["op"] == "search"]
assert queries == ["q1", "q2"]
@pytest.mark.asyncio
async def test_smart_retrieve_returns_empty_when_no_passages(
fake_service,
monkeypatch: pytest.MonkeyPatch,
) -> None:
service, pipeline = fake_service
pipeline.search_result = {"answer": "", "content": "", "provider": "x"}
out = await service.smart_retrieve(
context="anything",
kb_name="kb",
query_hints=["q"],
)
assert out == {"answer": "", "sources": []}
@pytest.mark.asyncio
async def test_delete_removes_kb_directory_when_pipeline_lacks_delete(tmp_path) -> None:
"""Fallback path: delete the KB dir directly if the pipeline does not implement delete."""
kb_dir = tmp_path / "demo"
(kb_dir / "raw").mkdir(parents=True)
(kb_dir / "raw" / "f.txt").write_text("hi")
class _NoDeletePipeline:
async def initialize(self, *a, **k):
return True
async def search(self, *a, **k):
return {}
service = RAGService(kb_base_dir=str(tmp_path))
service._pipelines["llamaindex"] = _NoDeletePipeline() # type: ignore[attr-defined]
assert await service.delete(kb_name="demo") is True
assert not kb_dir.exists()