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,261 @@
|
||||
"""FailClosedProviderRouter: policy-eligibility, availability and audit."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Iterable
|
||||
from datetime import UTC, datetime
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
|
||||
from ragcore.application.audit import AuditEventService
|
||||
from ragcore.application.models.router import (
|
||||
FailClosedProviderRouter,
|
||||
NoCompatibleProviderError,
|
||||
PolicyForbidsGenerationError,
|
||||
ProviderCandidate,
|
||||
ProviderUnavailableError,
|
||||
)
|
||||
from ragcore.domain.audit import AuditEvent, AuditEventType
|
||||
from ragcore.domain.control import DataPolicy, KnowledgeSpace, ProviderKind, SpaceOperation
|
||||
|
||||
WORKSPACE_ID = UUID("00000000-0000-0000-0000-000000000001")
|
||||
|
||||
|
||||
class _InMemoryAuditRepository:
|
||||
def __init__(self) -> None:
|
||||
self.events: list[AuditEvent] = []
|
||||
|
||||
async def append(self, event: AuditEvent) -> None:
|
||||
self.events.append(event)
|
||||
|
||||
async def list_events(
|
||||
self,
|
||||
workspace_id: UUID,
|
||||
*,
|
||||
event_types: Iterable[AuditEventType] = (),
|
||||
actor_id: UUID | None = None,
|
||||
application_id: UUID | None = None,
|
||||
before: datetime | None = None,
|
||||
limit: int = 100,
|
||||
) -> tuple[AuditEvent, ...]:
|
||||
return tuple(self.events)
|
||||
|
||||
|
||||
def _space(
|
||||
*, data_policy: DataPolicy, space_id: UUID | None = None, active: bool = True
|
||||
) -> KnowledgeSpace:
|
||||
return KnowledgeSpace(
|
||||
id=space_id or uuid4(),
|
||||
workspace_id=WORKSPACE_ID,
|
||||
name="Docs",
|
||||
slug=f"docs-{uuid4().hex[:8]}",
|
||||
active=active,
|
||||
trust_required=False,
|
||||
data_policy=data_policy,
|
||||
retention_days=None,
|
||||
)
|
||||
|
||||
|
||||
def _router(*, is_available=None) -> tuple[FailClosedProviderRouter, _InMemoryAuditRepository]:
|
||||
repository = _InMemoryAuditRepository()
|
||||
audit = AuditEventService(repository, clock=lambda: datetime.now(UTC))
|
||||
kwargs = {} if is_available is None else {"is_available": is_available}
|
||||
return FailClosedProviderRouter(audit=audit, **kwargs), repository
|
||||
|
||||
|
||||
LOCAL_CANDIDATE = ProviderCandidate(kind=ProviderKind.LOCAL, identifier="ollama:llama3.1")
|
||||
CLOUD_CANDIDATE = ProviderCandidate(kind=ProviderKind.CLOUD, identifier="cloud:gpt-x")
|
||||
|
||||
|
||||
async def _always(_: ProviderCandidate) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
async def _never(_: ProviderCandidate) -> bool:
|
||||
return False
|
||||
|
||||
|
||||
async def test_local_only_never_routes_external_even_when_cloud_is_the_only_candidate() -> None:
|
||||
router, repository = _router()
|
||||
with pytest.raises(NoCompatibleProviderError):
|
||||
await router.route(
|
||||
workspace_id=WORKSPACE_ID,
|
||||
trace_id="trace-1",
|
||||
spaces=[_space(data_policy=DataPolicy.LOCAL_ONLY)],
|
||||
operation=SpaceOperation.ANSWER,
|
||||
candidates=[CLOUD_CANDIDATE],
|
||||
)
|
||||
assert len(repository.events) == 1
|
||||
assert repository.events[0].details["outcome"] == "rejected_no_compatible_provider"
|
||||
assert repository.events[0].details["allowed_kinds"] == ["local"]
|
||||
|
||||
|
||||
async def test_local_only_selects_local_even_when_cloud_is_also_offered() -> None:
|
||||
router, _ = _router()
|
||||
result = await router.route(
|
||||
workspace_id=WORKSPACE_ID,
|
||||
trace_id="trace-2",
|
||||
spaces=[_space(data_policy=DataPolicy.LOCAL_ONLY)],
|
||||
operation=SpaceOperation.ANSWER,
|
||||
candidates=[LOCAL_CANDIDATE, CLOUD_CANDIDATE],
|
||||
)
|
||||
assert result == LOCAL_CANDIDATE
|
||||
|
||||
|
||||
async def test_mixed_space_query_uses_the_strictest_policy() -> None:
|
||||
router, _ = _router()
|
||||
with pytest.raises(NoCompatibleProviderError):
|
||||
await router.route(
|
||||
workspace_id=WORKSPACE_ID,
|
||||
trace_id="trace-3",
|
||||
spaces=[
|
||||
_space(data_policy=DataPolicy.CLOUD_ALLOWED),
|
||||
_space(data_policy=DataPolicy.LOCAL_ONLY),
|
||||
],
|
||||
operation=SpaceOperation.ANSWER,
|
||||
# Cloud-first preference: even preferring cloud, the LOCAL_ONLY
|
||||
# space in the mix must still forbid it -- no cloud candidate here.
|
||||
candidates=[CLOUD_CANDIDATE],
|
||||
)
|
||||
|
||||
|
||||
async def test_mixed_space_query_still_allows_local_despite_cloud_first_preference() -> None:
|
||||
router, _ = _router()
|
||||
result = await router.route(
|
||||
workspace_id=WORKSPACE_ID,
|
||||
trace_id="trace-4",
|
||||
spaces=[
|
||||
_space(data_policy=DataPolicy.CLOUD_ALLOWED),
|
||||
_space(data_policy=DataPolicy.LOCAL_ONLY),
|
||||
],
|
||||
operation=SpaceOperation.ANSWER,
|
||||
candidates=[CLOUD_CANDIDATE, LOCAL_CANDIDATE],
|
||||
)
|
||||
assert result == LOCAL_CANDIDATE
|
||||
|
||||
|
||||
async def test_unavailable_allowed_provider_raises_rather_than_broadening() -> None:
|
||||
checked: list[ProviderKind] = []
|
||||
|
||||
async def is_available(candidate: ProviderCandidate) -> bool:
|
||||
checked.append(candidate.kind)
|
||||
return False
|
||||
|
||||
router, repository = _router(is_available=is_available)
|
||||
with pytest.raises(ProviderUnavailableError):
|
||||
await router.route(
|
||||
workspace_id=WORKSPACE_ID,
|
||||
trace_id="trace-5",
|
||||
spaces=[_space(data_policy=DataPolicy.LOCAL_ONLY)],
|
||||
operation=SpaceOperation.ANSWER,
|
||||
candidates=[LOCAL_CANDIDATE, CLOUD_CANDIDATE],
|
||||
)
|
||||
# CLOUD was excluded by policy before availability was ever consulted --
|
||||
# the router never widens the search to a kind policy already forbade.
|
||||
assert checked == [ProviderKind.LOCAL]
|
||||
assert repository.events[-1].details["outcome"] == "rejected_provider_unavailable"
|
||||
|
||||
|
||||
async def test_preferred_provider_unavailable_falls_through_to_next_allowed_candidate() -> None:
|
||||
async def is_available(candidate: ProviderCandidate) -> bool:
|
||||
return candidate.kind is ProviderKind.CLOUD
|
||||
|
||||
router, repository = _router(is_available=is_available)
|
||||
result = await router.route(
|
||||
workspace_id=WORKSPACE_ID,
|
||||
trace_id="trace-6",
|
||||
spaces=[_space(data_policy=DataPolicy.CLOUD_ALLOWED)],
|
||||
operation=SpaceOperation.ANSWER,
|
||||
candidates=[LOCAL_CANDIDATE, CLOUD_CANDIDATE],
|
||||
)
|
||||
assert result == CLOUD_CANDIDATE
|
||||
assert repository.events[-1].details["selected_kind"] == "cloud"
|
||||
|
||||
|
||||
async def test_retrieval_only_space_forbids_generation_for_any_provider() -> None:
|
||||
async def is_available(_: ProviderCandidate) -> bool:
|
||||
pytest.fail("availability must never be checked once policy forbids generation")
|
||||
|
||||
router, repository = _router(is_available=is_available)
|
||||
with pytest.raises(PolicyForbidsGenerationError):
|
||||
await router.route(
|
||||
workspace_id=WORKSPACE_ID,
|
||||
trace_id="trace-7",
|
||||
spaces=[_space(data_policy=DataPolicy.RETRIEVAL_ONLY)],
|
||||
operation=SpaceOperation.ANSWER,
|
||||
candidates=[LOCAL_CANDIDATE, CLOUD_CANDIDATE],
|
||||
)
|
||||
assert repository.events[-1].details["outcome"] == "rejected_policy_forbids_generation"
|
||||
assert repository.events[-1].details["allowed_kinds"] == []
|
||||
|
||||
|
||||
async def test_no_generation_space_forbids_generation_for_any_provider() -> None:
|
||||
router, _ = _router()
|
||||
with pytest.raises(PolicyForbidsGenerationError):
|
||||
await router.route(
|
||||
workspace_id=WORKSPACE_ID,
|
||||
trace_id="trace-8",
|
||||
spaces=[_space(data_policy=DataPolicy.NO_GENERATION)],
|
||||
operation=SpaceOperation.ANSWER,
|
||||
candidates=[LOCAL_CANDIDATE],
|
||||
)
|
||||
|
||||
|
||||
async def test_no_candidates_configured_raises_no_compatible_provider() -> None:
|
||||
router, repository = _router()
|
||||
with pytest.raises(NoCompatibleProviderError):
|
||||
await router.route(
|
||||
workspace_id=WORKSPACE_ID,
|
||||
trace_id="trace-9",
|
||||
spaces=[_space(data_policy=DataPolicy.CLOUD_ALLOWED)],
|
||||
operation=SpaceOperation.ANSWER,
|
||||
candidates=[],
|
||||
)
|
||||
assert repository.events[-1].details["outcome"] == "rejected_no_compatible_provider"
|
||||
|
||||
|
||||
async def test_successful_route_is_audited_without_prompt_or_content_fields() -> None:
|
||||
router, repository = _router()
|
||||
await router.route(
|
||||
workspace_id=WORKSPACE_ID,
|
||||
trace_id="trace-10",
|
||||
spaces=[_space(data_policy=DataPolicy.LOCAL_ONLY)],
|
||||
operation=SpaceOperation.ANSWER,
|
||||
candidates=[LOCAL_CANDIDATE],
|
||||
)
|
||||
assert len(repository.events) == 1
|
||||
event = repository.events[0]
|
||||
assert event.event_type is AuditEventType.MODEL_ROUTE
|
||||
assert event.workspace_id == WORKSPACE_ID
|
||||
assert event.resource_type == "generation_provider_route"
|
||||
assert "prompt" not in event.details
|
||||
assert "query" not in event.details
|
||||
assert event.details["outcome"] == "routed"
|
||||
assert event.details["selected_kind"] == "local"
|
||||
assert event.details["candidate"] == "ollama:llama3.1"
|
||||
|
||||
|
||||
async def test_empty_spaces_raise_value_error_without_auditing() -> None:
|
||||
router, repository = _router()
|
||||
with pytest.raises(ValueError, match="contributing space"):
|
||||
await router.route(
|
||||
workspace_id=WORKSPACE_ID,
|
||||
trace_id="trace-11",
|
||||
spaces=[],
|
||||
operation=SpaceOperation.ANSWER,
|
||||
candidates=[LOCAL_CANDIDATE],
|
||||
)
|
||||
assert repository.events == []
|
||||
|
||||
|
||||
def test_provider_candidate_rejects_invalid_identifier() -> None:
|
||||
with pytest.raises(ValueError, match="non-empty bounded string"):
|
||||
ProviderCandidate(kind=ProviderKind.LOCAL, identifier="")
|
||||
with pytest.raises(ValueError, match="non-empty bounded string"):
|
||||
ProviderCandidate(kind=ProviderKind.LOCAL, identifier="x" * 129)
|
||||
|
||||
|
||||
def test_provider_candidate_rejects_invalid_kind() -> None:
|
||||
with pytest.raises(ValueError, match="ProviderKind"):
|
||||
ProviderCandidate(kind="local", identifier="ollama:llama3.1") # type: ignore[arg-type]
|
||||
Reference in new issue
Block a user