Initial curated RAGcore source from c16667f0c9397e2bfc8dd0255b114fba23c8d05e
This commit is contained in:
commit
7ca726caf2
659 files changed
+98171
No files matched your search
@@ -0,0 +1,353 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
from uuid import uuid4
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
from qdrant_client.http import models
|
||||
|
||||
from ragcore.application.models.selection import ResolvedEmbeddingSelection
|
||||
from ragcore.application.retrieval.authorization import AuthorizedRetrievalScope
|
||||
from ragcore.config import Settings
|
||||
from ragcore.domain.authorization import Principal, PrincipalType
|
||||
from ragcore.domain.control import DataPolicy
|
||||
from ragcore.domain.models import EmbeddingProfile, EmbeddingProvider, EmbeddingSelection
|
||||
from ragcore.infrastructure.models.embedding_factory import create_embedding_adapter_factory
|
||||
from ragcore.infrastructure.models.persistence import JsonEmbeddingProfileStore
|
||||
from ragcore.web.query_lab.executor import PipelineQueryLabExecutor, QueryLabExecutionRequest
|
||||
from ragcore.web.query_lab.models import QueryLabQuery
|
||||
|
||||
WORKSPACE_ID = uuid4()
|
||||
SPACE_ID = uuid4()
|
||||
DOCUMENT_ID = uuid4()
|
||||
DOCUMENT_VERSION_ID = uuid4()
|
||||
CHUNK_ID = uuid4()
|
||||
SOURCE_ID = uuid4()
|
||||
|
||||
EMBEDDING_PROFILE = EmbeddingProfile(
|
||||
id=uuid4(),
|
||||
profile="hybrid",
|
||||
version="v1",
|
||||
provider=EmbeddingProvider.OLLAMA,
|
||||
model_identifier="nomic-embed-text:v1",
|
||||
model_digest="0" * 64,
|
||||
dimensions=2,
|
||||
context_limit=2048,
|
||||
tokenizer="cl100k",
|
||||
data_policy=DataPolicy.LOCAL_ONLY,
|
||||
)
|
||||
|
||||
_PAYLOAD = {
|
||||
"workspace_id": str(WORKSPACE_ID),
|
||||
"space_id": str(SPACE_ID),
|
||||
"document_id": str(DOCUMENT_ID),
|
||||
"document_version_id": str(DOCUMENT_VERSION_ID),
|
||||
"chunk_id": str(CHUNK_ID),
|
||||
"source_id": str(SOURCE_ID),
|
||||
"parent_chunk_id": None,
|
||||
"status": "published",
|
||||
"content_type": "text/plain",
|
||||
"language": "en",
|
||||
"trust_level": "source",
|
||||
"embedding_model_digest": EMBEDDING_PROFILE.model_digest,
|
||||
"chunk_profile_version": EMBEDDING_PROFILE.version,
|
||||
"text": "The runbook says to restart the payment service after a failover.",
|
||||
"location": {"page": 1},
|
||||
}
|
||||
|
||||
|
||||
class FakeQdrantClient:
|
||||
def __init__(
|
||||
self,
|
||||
*,
|
||||
alias_target: str = "rag_dense_hybrid_v1",
|
||||
payload: dict[str, object] = _PAYLOAD,
|
||||
) -> None:
|
||||
self.alias_target = alias_target
|
||||
self.payload = payload
|
||||
self.retrieve_calls: list[dict[str, object]] = []
|
||||
self.query_collections: list[str] = []
|
||||
|
||||
async def get_aliases(self, **kwargs: object) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
aliases=[
|
||||
SimpleNamespace(
|
||||
alias_name="rag_dense_hybrid_active", collection_name=self.alias_target
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
async def get_collection(self, collection_name: str, **kwargs: object) -> SimpleNamespace:
|
||||
return SimpleNamespace(
|
||||
config=SimpleNamespace(
|
||||
params=SimpleNamespace(
|
||||
vectors={"dense": models.VectorParams(size=2, distance=models.Distance.COSINE)},
|
||||
sparse_vectors={"sparse": models.SparseVectorParams()},
|
||||
)
|
||||
)
|
||||
)
|
||||
|
||||
async def query_points(self, **kwargs: object) -> models.QueryResponse:
|
||||
self.query_collections.append(str(kwargs.get("collection_name")))
|
||||
return models.QueryResponse(
|
||||
points=[
|
||||
models.ScoredPoint(id=str(CHUNK_ID), version=0, score=0.9, payload=self.payload)
|
||||
]
|
||||
)
|
||||
|
||||
async def retrieve(self, **kwargs: object) -> list[SimpleNamespace]:
|
||||
self.retrieve_calls.append(kwargs)
|
||||
ids = kwargs.get("ids", [])
|
||||
return [SimpleNamespace(id=raw_id, payload=self.payload) for raw_id in ids]
|
||||
|
||||
|
||||
def _embed_transport(vector: tuple[float, ...] = (0.1, 0.2)) -> httpx.MockTransport:
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(200, json={"embeddings": [list(vector)]})
|
||||
|
||||
return httpx.MockTransport(handler)
|
||||
|
||||
|
||||
def _failing_transport(status_code: int = 503) -> httpx.MockTransport:
|
||||
def handler(request: httpx.Request) -> httpx.Response:
|
||||
return httpx.Response(status_code, json={"error": "unavailable"})
|
||||
|
||||
return httpx.MockTransport(handler)
|
||||
|
||||
|
||||
def _embedding_adapter_factory(transport: httpx.MockTransport, *, settings: Settings | None = None):
|
||||
client = httpx.AsyncClient(transport=transport, follow_redirects=False)
|
||||
return create_embedding_adapter_factory(
|
||||
settings=settings or Settings(env="test", ollama_url="http://localhost:11434"),
|
||||
http_client=client,
|
||||
)
|
||||
|
||||
|
||||
def _empty_store() -> JsonEmbeddingProfileStore:
|
||||
path = Path(tempfile.mkdtemp()) / "profiles.json"
|
||||
return JsonEmbeddingProfileStore(path)
|
||||
|
||||
|
||||
async def _activated_store() -> JsonEmbeddingProfileStore:
|
||||
store = _empty_store()
|
||||
await store.add(EMBEDDING_PROFILE)
|
||||
await store.activate(EMBEDDING_PROFILE.id)
|
||||
return store
|
||||
|
||||
|
||||
async def _activated_store_for(profile: EmbeddingProfile) -> JsonEmbeddingProfileStore:
|
||||
store = _empty_store()
|
||||
await store.add(profile)
|
||||
await store.activate(profile.id)
|
||||
return store
|
||||
|
||||
|
||||
def _modelforge_profile(*, version: str = "v1") -> EmbeddingProfile:
|
||||
return EmbeddingProfile(
|
||||
id=uuid4(),
|
||||
profile="hybrid",
|
||||
version=version,
|
||||
provider=EmbeddingProvider.MODELFORGE,
|
||||
model_identifier="rag.embedding@1",
|
||||
model_digest="c" * 64,
|
||||
dimensions=2,
|
||||
context_limit=8192,
|
||||
tokenizer="capability-managed",
|
||||
data_policy=DataPolicy.LOCAL_ONLY,
|
||||
embedding_space_id=uuid4(),
|
||||
)
|
||||
|
||||
|
||||
class FixedSelectionResolver:
|
||||
def __init__(self, profile: EmbeddingProfile, *, route: str) -> None:
|
||||
self.selected = ResolvedEmbeddingSelection(
|
||||
generation=4,
|
||||
profile=profile,
|
||||
selection=EmbeddingSelection.from_profile(profile, experiment_route_key=route),
|
||||
)
|
||||
|
||||
async def resolve_active(self) -> ResolvedEmbeddingSelection:
|
||||
return self.selected
|
||||
|
||||
|
||||
def _request(query: str = "how do I restart the payment service") -> QueryLabExecutionRequest:
|
||||
return QueryLabExecutionRequest(
|
||||
principal=Principal(uuid4(), WORKSPACE_ID, PrincipalType.HUMAN, frozenset()),
|
||||
query=QueryLabQuery(query=query, max_results=5),
|
||||
scope=AuthorizedRetrievalScope(WORKSPACE_ID, frozenset({SPACE_ID})),
|
||||
retrieval_run_id=uuid4(),
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_degrades_when_no_active_embedding_profile() -> None:
|
||||
executor = PipelineQueryLabExecutor(
|
||||
qdrant_client=FakeQdrantClient(),
|
||||
embedding_profile_store=_empty_store(),
|
||||
embedding_adapter_factory=_embedding_adapter_factory(_embed_transport()),
|
||||
)
|
||||
trace = await executor.execute(_request())
|
||||
assert trace.degraded is True
|
||||
assert "no_active_embedding_profile" in trace.degradation_codes
|
||||
assert trace.dense_candidates == ()
|
||||
assert trace.revisions.embedding_profile is None
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_degrades_when_embedding_provider_unavailable() -> None:
|
||||
executor = PipelineQueryLabExecutor(
|
||||
qdrant_client=FakeQdrantClient(),
|
||||
embedding_profile_store=await _activated_store(),
|
||||
embedding_adapter_factory=_embedding_adapter_factory(_failing_transport()),
|
||||
)
|
||||
trace = await executor.execute(_request())
|
||||
assert trace.degraded is True
|
||||
assert "embedding_provider_unavailable" in trace.degradation_codes
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_full_pipeline_happy_path_produces_a_bounded_trace() -> None:
|
||||
"""M8-01: no reranking -- the rerank trace section always reports
|
||||
'skipped', never 'reranked' or 'degraded', since the step no longer runs
|
||||
(the deployed Ollama never had a working /api/rerank endpoint)."""
|
||||
|
||||
executor = PipelineQueryLabExecutor(
|
||||
qdrant_client=FakeQdrantClient(),
|
||||
embedding_profile_store=await _activated_store(),
|
||||
embedding_adapter_factory=_embedding_adapter_factory(_embed_transport()),
|
||||
)
|
||||
trace = await executor.execute(_request())
|
||||
|
||||
assert trace.degraded is False
|
||||
assert len(trace.dense_candidates) == 1
|
||||
assert len(trace.sparse_candidates) == 1
|
||||
assert len(trace.fusion.candidates) == 1
|
||||
assert trace.rerank.state == "skipped"
|
||||
assert trace.rerank.degradation_code is None
|
||||
assert len(trace.citations) == 1
|
||||
assert trace.citations[0].document_id == DOCUMENT_ID
|
||||
assert "restart the payment service" in trace.citations[0].excerpt_preview
|
||||
assert trace.answerability.status in {"partially_answerable", "not_answerable", "answerable"}
|
||||
assert trace.revisions.embedding_profile == "hybrid/v1"
|
||||
assert trace.revisions.reranker_profile is None
|
||||
assert trace.revisions.collection_alias == "rag_dense_hybrid_active"
|
||||
assert set(trace.timings_ms) >= {"embed_query", "dense_search", "sparse_search", "rerank"}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_lab_uses_durable_route_and_collection_selection(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
profile = _modelforge_profile()
|
||||
credential_file = tmp_path / "credential"
|
||||
credential_file.write_text("mf_" + "x" * 48, encoding="utf-8")
|
||||
credential_file.chmod(0o600)
|
||||
seen: list[httpx.Request] = []
|
||||
|
||||
def respond(request: httpx.Request) -> httpx.Response:
|
||||
seen.append(request)
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"capability": "rag.embedding@1",
|
||||
"dimension": profile.dimensions,
|
||||
"normalized": True,
|
||||
"embedding_space_id": str(profile.embedding_space_id),
|
||||
"data": [[1.0, 0.0]],
|
||||
"request_id": str(uuid4()),
|
||||
},
|
||||
)
|
||||
|
||||
settings = Settings(
|
||||
env="test",
|
||||
ollama_url="http://localhost:11434",
|
||||
modelforge_base_url="http://modelforge:8000",
|
||||
modelforge_credential_file=credential_file,
|
||||
modelforge_embedding_experiment_route="stale-global-route",
|
||||
)
|
||||
payload = {
|
||||
**_PAYLOAD,
|
||||
"embedding_model_digest": profile.model_digest,
|
||||
"embedding_space_id": str(profile.embedding_space_id),
|
||||
}
|
||||
qdrant = FakeQdrantClient(alias_target="rag_dense_hybrid_old", payload=payload)
|
||||
executor = PipelineQueryLabExecutor(
|
||||
qdrant_client=qdrant,
|
||||
embedding_profile_store=await _activated_store_for(profile),
|
||||
embedding_adapter_factory=_embedding_adapter_factory(
|
||||
httpx.MockTransport(respond), settings=settings
|
||||
),
|
||||
embedding_selection_resolver=FixedSelectionResolver(profile, route="rc-canary-2026-09"),
|
||||
)
|
||||
|
||||
trace = await executor.execute(_request())
|
||||
|
||||
assert trace.degraded is False
|
||||
assert trace.dense_candidates
|
||||
assert trace.revisions.embedding_profile == "hybrid/v1"
|
||||
assert trace.revisions.collection_alias == "rag_dense_hybrid_active"
|
||||
assert qdrant.query_collections == ["rag_dense_hybrid_v1", "rag_dense_hybrid_v1"]
|
||||
assert len(seen) == 1
|
||||
assert seen[0].url.path == "/api/v1/capability-experiments/rc-canary-2026-09/invoke"
|
||||
assert json.loads(seen[0].content) == {
|
||||
"input": ["how do I restart the payment service"],
|
||||
"input_type": "query",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_query_lab_uses_selected_collection_after_alias_changes(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
profile = _modelforge_profile(version="v2")
|
||||
credential_file = tmp_path / "credential"
|
||||
credential_file.write_text("mf_" + "x" * 48, encoding="utf-8")
|
||||
credential_file.chmod(0o600)
|
||||
seen: list[httpx.Request] = []
|
||||
|
||||
def respond(request: httpx.Request) -> httpx.Response:
|
||||
seen.append(request)
|
||||
return httpx.Response(
|
||||
200,
|
||||
json={
|
||||
"capability": "rag.embedding@1",
|
||||
"dimension": profile.dimensions,
|
||||
"normalized": True,
|
||||
"embedding_space_id": str(profile.embedding_space_id),
|
||||
"data": [[1.0, 0.0]],
|
||||
"request_id": str(uuid4()),
|
||||
},
|
||||
)
|
||||
|
||||
settings = Settings(
|
||||
env="test",
|
||||
ollama_url="http://localhost:11434",
|
||||
modelforge_base_url="http://modelforge:8000",
|
||||
modelforge_credential_file=credential_file,
|
||||
modelforge_embedding_experiment_route="rc-canary-2026-09",
|
||||
)
|
||||
payload = {
|
||||
**_PAYLOAD,
|
||||
"embedding_model_digest": profile.model_digest,
|
||||
"embedding_space_id": str(profile.embedding_space_id),
|
||||
"chunk_profile_version": profile.version,
|
||||
}
|
||||
qdrant = FakeQdrantClient(alias_target="rag_dense_hybrid_v1", payload=payload)
|
||||
executor = PipelineQueryLabExecutor(
|
||||
qdrant_client=qdrant,
|
||||
embedding_profile_store=await _activated_store_for(profile),
|
||||
embedding_adapter_factory=_embedding_adapter_factory(
|
||||
httpx.MockTransport(respond), settings=settings
|
||||
),
|
||||
embedding_selection_resolver=FixedSelectionResolver(profile, route="rc-canary-2026-09"),
|
||||
)
|
||||
|
||||
trace = await executor.execute(_request())
|
||||
|
||||
assert trace.degraded is False
|
||||
assert qdrant.query_collections == ["rag_dense_hybrid_v2", "rag_dense_hybrid_v2"]
|
||||
assert len(seen) == 1
|
||||
@@ -0,0 +1,208 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from ragcore.web.query_lab.models import (
|
||||
AnswerabilityTrace,
|
||||
CandidateBackendLabel,
|
||||
CandidateTrace,
|
||||
ContextTrace,
|
||||
FusionTrace,
|
||||
InclusionState,
|
||||
ParentExpansionTrace,
|
||||
QueryLabFilterRequest,
|
||||
QueryLabQuery,
|
||||
QueryLabTrace,
|
||||
RerankTrace,
|
||||
RevisionTrace,
|
||||
preview,
|
||||
)
|
||||
|
||||
|
||||
def _candidate(**overrides: object) -> CandidateTrace:
|
||||
values = {
|
||||
"rank": 1,
|
||||
"chunk_id": uuid4(),
|
||||
"document_id": uuid4(),
|
||||
"space_id": uuid4(),
|
||||
"backend": CandidateBackendLabel.DENSE,
|
||||
"score": 0.5,
|
||||
"score_label": "similarity_score",
|
||||
"content_type": "text/plain",
|
||||
"trust_level": "source",
|
||||
"language": "en",
|
||||
"text_preview": "hello",
|
||||
"text_truncated": False,
|
||||
"inclusion": InclusionState.INCLUDED,
|
||||
"reason": "selected_for_fusion",
|
||||
}
|
||||
values.update(overrides)
|
||||
return CandidateTrace(**values)
|
||||
|
||||
|
||||
def test_query_rejects_empty_and_oversized_text() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
QueryLabQuery(query=" ")
|
||||
with pytest.raises(ValueError):
|
||||
QueryLabQuery(query="x" * 8001)
|
||||
|
||||
|
||||
def test_query_rejects_duplicate_requested_spaces() -> None:
|
||||
space_id = uuid4()
|
||||
with pytest.raises(ValueError):
|
||||
QueryLabQuery(query="test", requested_space_ids=(space_id, space_id))
|
||||
|
||||
|
||||
def test_query_rejects_unsafe_profile() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
QueryLabQuery(query="test", profile="not a safe id!")
|
||||
|
||||
|
||||
def test_filter_request_bounds_cardinality() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
QueryLabFilterRequest(content_types=tuple(f"type-{i}" for i in range(51)))
|
||||
|
||||
|
||||
def test_preview_truncates_and_flags() -> None:
|
||||
text, truncated = preview("a" * 500, 400)
|
||||
assert len(text) == 400
|
||||
assert truncated is True
|
||||
text, truncated = preview("short", 400)
|
||||
assert text == "short"
|
||||
assert truncated is False
|
||||
|
||||
|
||||
def test_candidate_trace_rejects_unsafe_reason_code() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
_candidate(reason="not a safe code!")
|
||||
|
||||
|
||||
def test_candidate_trace_rejects_oversized_preview() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
_candidate(text_preview="a" * 401)
|
||||
|
||||
|
||||
def test_fusion_trace_bounds_display_candidates() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
FusionTrace(candidates=tuple(_candidate(rank=i) for i in range(1, 22)), suppressed_count=0)
|
||||
|
||||
|
||||
def test_fusion_trace_rejects_negative_suppressed_count() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
FusionTrace(candidates=(), suppressed_count=-1)
|
||||
|
||||
|
||||
def test_rerank_trace_rejects_unknown_state() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
RerankTrace(state="bogus", degradation_code=None, candidates=(), elapsed_ms=1.0)
|
||||
|
||||
|
||||
def test_answerability_trace_requires_at_least_one_reason() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
AnswerabilityTrace(
|
||||
status="not_answerable",
|
||||
reasons=(),
|
||||
evidence_count=0,
|
||||
document_diversity=0,
|
||||
source_independence=0,
|
||||
facet_coverage_ratio=0.0,
|
||||
thresholds_version="v1",
|
||||
)
|
||||
|
||||
|
||||
def test_context_trace_requires_positive_max_tokens() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
ContextTrace(token_count=0, max_context_tokens=0, block_count=0, citation_ids=())
|
||||
|
||||
|
||||
def test_parent_expansion_trace_rejects_negative_counts() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
ParentExpansionTrace(expanded_count=-1, skipped_count=0, total_expansion_tokens=0)
|
||||
|
||||
|
||||
def _full_trace(**overrides: object) -> QueryLabTrace:
|
||||
from ragcore.web.query_lab.models import EffectiveFilterTrace
|
||||
|
||||
values: dict[str, object] = {
|
||||
"retrieval_run_id": uuid4(),
|
||||
"query": "example query",
|
||||
"profile": "hybrid-v1",
|
||||
"effective_filters": EffectiveFilterTrace(
|
||||
requested_space_ids=(),
|
||||
effective_space_ids=(),
|
||||
denied_space_ids=(),
|
||||
trust_required_space_ids=(),
|
||||
requested_content_types=(),
|
||||
applied_content_types=(),
|
||||
requested_languages=(),
|
||||
applied_languages=(),
|
||||
unsupported_filter_fields=(),
|
||||
),
|
||||
"dense_candidates": (),
|
||||
"sparse_candidates": (),
|
||||
"fusion": FusionTrace(candidates=(), suppressed_count=0),
|
||||
"rerank": RerankTrace(
|
||||
state="skipped", degradation_code=None, candidates=(), elapsed_ms=0.0
|
||||
),
|
||||
"parent_expansion": ParentExpansionTrace(0, 0, 0),
|
||||
"context": ContextTrace(
|
||||
token_count=0, max_context_tokens=100, block_count=0, citation_ids=()
|
||||
),
|
||||
"answerability": AnswerabilityTrace(
|
||||
status="not_answerable",
|
||||
reasons=("insufficient_evidence",),
|
||||
evidence_count=0,
|
||||
document_diversity=0,
|
||||
source_independence=0,
|
||||
facet_coverage_ratio=0.0,
|
||||
thresholds_version="v1",
|
||||
),
|
||||
"citations": (),
|
||||
"timings_ms": {},
|
||||
"revisions": RevisionTrace(None, None, "technical/v1", None, "v1"),
|
||||
"degraded": False,
|
||||
"degradation_codes": (),
|
||||
}
|
||||
values.update(overrides)
|
||||
return QueryLabTrace(**values)
|
||||
|
||||
|
||||
def test_trace_rejects_unsafe_timing_name() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
_full_trace(timings_ms={"not a safe name": 1.0})
|
||||
|
||||
|
||||
def test_trace_rejects_negative_timing() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
_full_trace(timings_ms={"embed_query": -1.0})
|
||||
|
||||
|
||||
def test_trace_rejects_duplicate_degradation_codes() -> None:
|
||||
with pytest.raises(ValueError):
|
||||
_full_trace(degradation_codes=("backend_timeout", "backend_timeout"))
|
||||
|
||||
|
||||
def test_trace_rejects_too_many_citations() -> None:
|
||||
from ragcore.web.query_lab.models import CitationTrace
|
||||
|
||||
citations = tuple(
|
||||
CitationTrace(
|
||||
id=uuid4(),
|
||||
document_id=uuid4(),
|
||||
chunk_id=uuid4(),
|
||||
excerpt_preview="text",
|
||||
excerpt_truncated=False,
|
||||
location={},
|
||||
source_uri="ragcore://source/x",
|
||||
)
|
||||
for _ in range(21)
|
||||
)
|
||||
with pytest.raises(ValueError):
|
||||
_full_trace(citations=citations)
|
||||
|
||||
|
||||
def test_trace_accepts_a_well_formed_minimal_result() -> None:
|
||||
trace = _full_trace()
|
||||
assert trace.degraded is False
|
||||
@@ -0,0 +1,203 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from uuid import uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from ragcore.api.v1.search.service import AuthorizationInputs
|
||||
from ragcore.application.authorization import AuthorizationService
|
||||
from ragcore.application.retrieval.authorization import RetrievalAuthorizationService
|
||||
from ragcore.domain.authorization import PermissionDenied, Principal, PrincipalType
|
||||
from ragcore.domain.control import DataPolicy, KnowledgeSpace, Role, UserGrant
|
||||
from ragcore.web.query_lab.executor import QueryLabExecutionRequest
|
||||
from ragcore.web.query_lab.models import (
|
||||
AnswerabilityTrace,
|
||||
ContextTrace,
|
||||
EffectiveFilterTrace,
|
||||
FusionTrace,
|
||||
ParentExpansionTrace,
|
||||
QueryLabQuery,
|
||||
QueryLabTrace,
|
||||
RerankTrace,
|
||||
RevisionTrace,
|
||||
)
|
||||
from ragcore.web.query_lab.service import QueryLabService
|
||||
|
||||
WORKSPACE_ID = uuid4()
|
||||
SPACE_ID = uuid4()
|
||||
|
||||
|
||||
def _space() -> KnowledgeSpace:
|
||||
return KnowledgeSpace(
|
||||
id=SPACE_ID,
|
||||
workspace_id=WORKSPACE_ID,
|
||||
name="Docs",
|
||||
slug="docs",
|
||||
active=True,
|
||||
trust_required=False,
|
||||
data_policy=DataPolicy.LOCAL_ONLY,
|
||||
retention_days=None,
|
||||
)
|
||||
|
||||
|
||||
def _user_grant(user_id, role: Role = Role.VIEWER) -> UserGrant:
|
||||
return UserGrant(
|
||||
id=uuid4(),
|
||||
workspace_id=WORKSPACE_ID,
|
||||
user_id=user_id,
|
||||
knowledge_space_id=SPACE_ID,
|
||||
role=role,
|
||||
active=True,
|
||||
)
|
||||
|
||||
|
||||
class StaticInputsProvider:
|
||||
def __init__(self, spaces=(), user_grants=()) -> None:
|
||||
self._spaces = spaces
|
||||
self._user_grants = user_grants
|
||||
|
||||
async def load(self, principal: Principal) -> AuthorizationInputs:
|
||||
return AuthorizationInputs(
|
||||
spaces=self._spaces, grants=(), user_grants=self._user_grants, application=None
|
||||
)
|
||||
|
||||
|
||||
class RecordingExecutor:
|
||||
def __init__(self) -> None:
|
||||
self.requests: list[QueryLabExecutionRequest] = []
|
||||
|
||||
async def execute(self, request: QueryLabExecutionRequest) -> QueryLabTrace:
|
||||
self.requests.append(request)
|
||||
return _trace(request)
|
||||
|
||||
|
||||
def _trace(request: QueryLabExecutionRequest) -> QueryLabTrace:
|
||||
return QueryLabTrace(
|
||||
retrieval_run_id=request.retrieval_run_id,
|
||||
query=request.query.query,
|
||||
profile=request.query.profile,
|
||||
effective_filters=EffectiveFilterTrace((), (), (), (), (), (), (), (), ()),
|
||||
dense_candidates=(),
|
||||
sparse_candidates=(),
|
||||
fusion=FusionTrace(candidates=(), suppressed_count=0),
|
||||
rerank=RerankTrace(state="skipped", degradation_code=None, candidates=(), elapsed_ms=0.0),
|
||||
parent_expansion=ParentExpansionTrace(0, 0, 0),
|
||||
context=ContextTrace(token_count=0, max_context_tokens=100, block_count=0, citation_ids=()),
|
||||
answerability=AnswerabilityTrace(
|
||||
status="not_answerable",
|
||||
reasons=("insufficient_evidence",),
|
||||
evidence_count=0,
|
||||
document_diversity=0,
|
||||
source_independence=0,
|
||||
facet_coverage_ratio=0.0,
|
||||
thresholds_version="v1",
|
||||
),
|
||||
citations=(),
|
||||
timings_ms={},
|
||||
revisions=RevisionTrace(None, None, "technical/v1", None, "v1"),
|
||||
degraded=False,
|
||||
degradation_codes=(),
|
||||
)
|
||||
|
||||
|
||||
def _service(executor: RecordingExecutor, spaces=(), user_grants=()) -> QueryLabService:
|
||||
return QueryLabService(
|
||||
authorization=AuthorizationService(),
|
||||
retrieval_authorization=RetrievalAuthorizationService(),
|
||||
inputs=StaticInputsProvider(spaces, user_grants),
|
||||
executor=executor,
|
||||
)
|
||||
|
||||
|
||||
def _principal(role: Role) -> Principal:
|
||||
return Principal(uuid4(), WORKSPACE_ID, PrincipalType.HUMAN, frozenset({role}))
|
||||
|
||||
|
||||
def test_can_use_reflects_the_privilege_gate_without_side_effects() -> None:
|
||||
service = _service(RecordingExecutor())
|
||||
assert service.can_use(_principal(Role.SPACE_MANAGER)) is True
|
||||
assert service.can_use(_principal(Role.PLATFORM_ADMIN)) is True
|
||||
assert service.can_use(_principal(Role.VIEWER)) is False
|
||||
assert service.can_use(_principal(Role.EDITOR)) is False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_viewer_is_denied_query_lab_even_with_a_valid_grant() -> None:
|
||||
principal = _principal(Role.VIEWER)
|
||||
executor = RecordingExecutor()
|
||||
service = _service(executor, spaces=(_space(),), user_grants=(_user_grant(principal.id),))
|
||||
with pytest.raises(PermissionDenied):
|
||||
await service.run(principal, QueryLabQuery(query="test"))
|
||||
assert executor.requests == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_editor_is_denied_query_lab_even_with_a_valid_grant() -> None:
|
||||
principal = _principal(Role.EDITOR)
|
||||
executor = RecordingExecutor()
|
||||
service = _service(executor, spaces=(_space(),), user_grants=(_user_grant(principal.id),))
|
||||
with pytest.raises(PermissionDenied):
|
||||
await service.run(principal, QueryLabQuery(query="test"))
|
||||
assert executor.requests == []
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_space_manager_with_a_grant_can_run_and_scope_is_resolved_server_side() -> None:
|
||||
principal = _principal(Role.SPACE_MANAGER)
|
||||
executor = RecordingExecutor()
|
||||
service = _service(
|
||||
executor,
|
||||
spaces=(_space(),),
|
||||
user_grants=(_user_grant(principal.id, role=Role.SPACE_MANAGER),),
|
||||
)
|
||||
trace = await service.run(
|
||||
principal, QueryLabQuery(query="test", requested_space_ids=(SPACE_ID,))
|
||||
)
|
||||
assert trace.query == "test"
|
||||
assert executor.requests[0].scope.effective_space_ids == frozenset({SPACE_ID})
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_space_manager_without_any_grant_resolves_to_no_effective_spaces() -> None:
|
||||
principal = _principal(Role.SPACE_MANAGER)
|
||||
executor = RecordingExecutor()
|
||||
service = _service(executor, spaces=(_space(),), user_grants=())
|
||||
trace = await service.run(
|
||||
principal, QueryLabQuery(query="test", requested_space_ids=(SPACE_ID,))
|
||||
)
|
||||
assert executor.requests == []
|
||||
assert trace.effective_filters.effective_space_ids == ()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_manual_unauthorized_requested_space_is_never_included() -> None:
|
||||
principal = _principal(Role.SPACE_MANAGER)
|
||||
executor = RecordingExecutor()
|
||||
service = _service(
|
||||
executor,
|
||||
spaces=(_space(),),
|
||||
user_grants=(_user_grant(principal.id, role=Role.SPACE_MANAGER),),
|
||||
)
|
||||
unauthorized_space = uuid4()
|
||||
trace = await service.run(
|
||||
principal, QueryLabQuery(query="test", requested_space_ids=(unauthorized_space,))
|
||||
)
|
||||
# The requested space was never granted, so the resolved scope is empty
|
||||
# and the executor is never invoked -- a client cannot widen access by
|
||||
# simply asking for a different space id.
|
||||
assert executor.requests == []
|
||||
assert trace.effective_filters.effective_space_ids == ()
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_platform_admin_with_a_grant_can_run_query_lab() -> None:
|
||||
principal = _principal(Role.PLATFORM_ADMIN)
|
||||
executor = RecordingExecutor()
|
||||
service = _service(
|
||||
executor, spaces=(_space(),), user_grants=(_user_grant(principal.id, role=Role.VIEWER),)
|
||||
)
|
||||
trace = await service.run(
|
||||
principal, QueryLabQuery(query="test", requested_space_ids=(SPACE_ID,))
|
||||
)
|
||||
assert executor.requests[0].scope.effective_space_ids == frozenset({SPACE_ID})
|
||||
assert trace.retrieval_run_id == executor.requests[0].retrieval_run_id
|
||||
Reference in new issue
Block a user