Spaces:
Running
Running
File size: 6,190 Bytes
ef5d585 5be9616 ef5d585 5be9616 ef5d585 5be9616 1bf7f2d | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 | """Shared test fixtures."""
import numpy as np
import pytest
from agent_bench.core.provider import MockProvider
from agent_bench.rag.chunker import Chunk
from agent_bench.rag.embedder import Embedder
from agent_bench.rag.retriever import Retriever
from agent_bench.rag.store import HybridStore
@pytest.fixture
def mock_provider() -> MockProvider:
"""MockProvider instance for deterministic testing."""
return MockProvider()
class MockEmbeddingModel:
"""Deterministic embedding model for tests. No model download needed.
Uses seeded random vectors, normalized to unit length.
Same input always produces the same output via content hashing.
"""
def __init__(self, dimension: int = 384) -> None:
self.dimension = dimension
self.call_count = 0
def encode(self, sentences: list[str], **kwargs: object) -> np.ndarray:
self.call_count += 1
vecs = []
for s in sentences:
seed = int.from_bytes(s.encode()[:4], "big") % (2**31)
rng = np.random.RandomState(seed)
vec = rng.randn(self.dimension).astype(np.float32)
vec = vec / np.linalg.norm(vec)
vecs.append(vec)
return np.stack(vecs)
@pytest.fixture
def mock_embedding_model() -> MockEmbeddingModel:
"""Deterministic embedding model — no model download."""
return MockEmbeddingModel()
@pytest.fixture
def mock_embedder(mock_embedding_model: MockEmbeddingModel, tmp_path: object) -> Embedder:
"""Embedder backed by mock model with temp cache dir."""
return Embedder(model=mock_embedding_model, cache_dir=str(tmp_path))
SAMPLE_CHUNKS = [
Chunk(
id="chunk_path_1",
content="Path parameters in FastAPI are defined using curly braces in the URL path.",
source="fastapi_path_params.md",
chunk_index=0,
metadata={"strategy": "recursive"},
),
Chunk(
id="chunk_path_2",
content="You can declare the type of a path parameter using Python type annotations.",
source="fastapi_path_params.md",
chunk_index=1,
metadata={"strategy": "recursive"},
),
Chunk(
id="chunk_query_1",
content="Query parameters are automatically parsed from the URL query string.",
source="fastapi_query_params.md",
chunk_index=0,
metadata={"strategy": "recursive"},
),
Chunk(
id="chunk_body_1",
content="Request body data is defined using Pydantic models in FastAPI.",
source="fastapi_request_body.md",
chunk_index=0,
metadata={"strategy": "recursive"},
),
Chunk(
id="chunk_response_1",
content="Response models control the output schema of your API endpoints.",
source="fastapi_response_model.md",
chunk_index=0,
metadata={"strategy": "recursive"},
),
]
@pytest.fixture
def sample_chunks() -> list[Chunk]:
"""5 sample chunks with known content and sources."""
return list(SAMPLE_CHUNKS)
@pytest.fixture
def test_store(mock_embedder: Embedder, sample_chunks: list[Chunk]) -> HybridStore:
"""HybridStore populated with sample chunks via mock embedder."""
store = HybridStore(dimension=384, rrf_k=60)
texts = [c.content for c in sample_chunks]
embeddings = mock_embedder.embed_batch(texts)
store.add(sample_chunks, embeddings)
return store
@pytest.fixture
def test_retriever(mock_embedder: Embedder, test_store: HybridStore) -> Retriever:
"""Retriever wired to mock embedder + test store."""
return Retriever(embedder=mock_embedder, store=test_store)
# --- Multi-corpus test app (shared across routing / meta / prompt tests) ---
class _FakeOpenAI(MockProvider):
"""Distinct MockProvider subclass so tests can tell it apart from
the default mock when asserting which orchestrator actually ran."""
@pytest.fixture
def two_corpus_two_provider_app(tmp_path, monkeypatch):
"""Two corpora (fastapi, k8s) × two providers (mock, openai-faked).
After building the app, each corpus × provider cell gets a *unique*
MockProvider instance tagged with `_tag`. create_app deliberately
shares one provider instance across corpora in production (providers
hold LLM clients and are expensive), but the test needs to distinguish
which cell ran a given request — so the fixture breaks the sharing
here and only here.
"""
from agent_bench.core import provider as provider_mod
from agent_bench.core.config import (
AppConfig,
CorpusConfig,
EmbeddingConfig,
ProviderConfig,
RAGConfig,
SecurityConfig,
)
from agent_bench.serving.app import create_app
monkeypatch.setattr(provider_mod, "OpenAIProvider", lambda _cfg: _FakeOpenAI())
monkeypatch.setenv("OPENAI_API_KEY", "test-key")
config = AppConfig(
provider=ProviderConfig(default="mock"),
rag=RAGConfig(store_path=str(tmp_path / "store_default")),
embedding=EmbeddingConfig(cache_dir=str(tmp_path / "emb_cache")),
security=SecurityConfig(),
corpora={
"fastapi": CorpusConfig(
label="FastAPI Docs",
store_path=str(tmp_path / "store_fastapi"),
data_path="data/tech_docs",
),
"k8s": CorpusConfig(
label="Kubernetes",
store_path=str(tmp_path / "store_k8s"),
data_path="data/k8s_docs",
),
},
default_corpus="fastapi",
)
app = create_app(config)
# Stamp a unique provider into each cell so call_count is per-cell.
for c_name, inner in app.state.corpus_map.items():
for p_name, orch in inner.items():
unique = MockProvider()
unique._tag = f"{c_name}:{p_name}" # type: ignore[attr-defined]
orch.provider = unique
# Keep the flat orchestrators dict and the singular orchestrator in
# sync with the per-cell instances for the default corpus.
app.state.orchestrators = dict(app.state.corpus_map[config.default_corpus])
app.state.orchestrator = app.state.orchestrators[config.provider.default]
return app
|