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,253 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import replace
|
||||
from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from uuid import UUID, uuid4
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from ragcore.api.v1.context.dependencies import get_context_application, get_context_principal
|
||||
from ragcore.api.v1.context.service import (
|
||||
ContextExecutionRequest,
|
||||
ContextExecutionResult,
|
||||
ContextService,
|
||||
)
|
||||
from ragcore.application.retrieval.authorization import RetrievalAuthorizationService
|
||||
from ragcore.domain.authorization import Principal, PrincipalType
|
||||
from ragcore.domain.citations import Citation
|
||||
from ragcore.domain.control import Application, Role, Workspace
|
||||
from ragcore.main import create_app
|
||||
from ragcore.retrieval.context import ContextCandidate
|
||||
|
||||
|
||||
class FixtureInputs:
|
||||
def __init__(self, workspace: Workspace, space_id: UUID, principal: Principal) -> None:
|
||||
self.workspace = workspace
|
||||
self.space_id = space_id
|
||||
self.principal = principal
|
||||
|
||||
async def load(self, principal: Principal):
|
||||
space = self.workspace.create_knowledge_space(
|
||||
id=self.space_id,
|
||||
name="Context space",
|
||||
slug="context-space",
|
||||
)
|
||||
return type(
|
||||
"Inputs",
|
||||
(),
|
||||
{
|
||||
"spaces": (space,),
|
||||
"grants": (),
|
||||
"user_grants": (
|
||||
self.workspace.grant_user(
|
||||
id=uuid4(),
|
||||
user_id=principal.id,
|
||||
knowledge_space=space,
|
||||
role=Role.EDITOR,
|
||||
),
|
||||
),
|
||||
"application": Application(
|
||||
id=uuid4(),
|
||||
workspace_id=self.workspace.id,
|
||||
name="Context application",
|
||||
default_knowledge_space_ids=frozenset({self.space_id}),
|
||||
),
|
||||
},
|
||||
)()
|
||||
|
||||
|
||||
class FixtureExecutor:
|
||||
def __init__(self, candidate: ContextCandidate) -> None:
|
||||
self.candidate = candidate
|
||||
self.calls: list[ContextExecutionRequest] = []
|
||||
self.fail = False
|
||||
self.degraded = False
|
||||
self.degradation_codes: tuple[str, ...] = ()
|
||||
|
||||
async def execute(self, request: ContextExecutionRequest) -> ContextExecutionResult:
|
||||
self.calls.append(request)
|
||||
if self.fail:
|
||||
raise RuntimeError("provider secret must not leak")
|
||||
citation = self.candidate.citation
|
||||
refreshed = Citation.create(
|
||||
workspace_id=citation.workspace_id,
|
||||
space_id=citation.space_id,
|
||||
document_id=citation.document_id,
|
||||
document_version_id=citation.document_version_id,
|
||||
chunk_id=citation.chunk_id,
|
||||
parent_chunk_id=citation.parent_chunk_id,
|
||||
location=citation.location,
|
||||
excerpt=citation.excerpt,
|
||||
source_uri=citation.source_uri,
|
||||
acquired_at=citation.acquired_at,
|
||||
retrieval_run_id=request.retrieval_run_id,
|
||||
index_alias=citation.index_alias,
|
||||
profile_version=citation.profile_version,
|
||||
)
|
||||
return ContextExecutionResult(
|
||||
candidates=(replace(self.candidate, citation=refreshed),),
|
||||
degraded=self.degraded,
|
||||
degradation_codes=self.degradation_codes,
|
||||
)
|
||||
|
||||
|
||||
def _fixture() -> tuple[object, Principal, UUID, ContextCandidate, FixtureExecutor]:
|
||||
workspace = Workspace(uuid4(), "Context workspace", "context-workspace")
|
||||
space_id = uuid4()
|
||||
principal = Principal(uuid4(), workspace.id, PrincipalType.HUMAN, frozenset({Role.EDITOR}))
|
||||
citation = Citation.create(
|
||||
workspace_id=workspace.id,
|
||||
space_id=space_id,
|
||||
document_id=uuid4(),
|
||||
document_version_id=uuid4(),
|
||||
chunk_id=uuid4(),
|
||||
parent_chunk_id=None,
|
||||
location={"page_start": 1},
|
||||
excerpt="Context evidence",
|
||||
source_uri="https://example.test/context.md",
|
||||
acquired_at=datetime(2026, 8, 1, 12, tzinfo=UTC),
|
||||
retrieval_run_id=uuid4(),
|
||||
index_alias="rag_dense_hybrid_active",
|
||||
profile_version="hybrid-v1",
|
||||
)
|
||||
candidate = ContextCandidate(citation, "Context evidence", 1, 1.0)
|
||||
executor = FixtureExecutor(candidate)
|
||||
service = ContextService(
|
||||
authorization=RetrievalAuthorizationService(),
|
||||
inputs=FixtureInputs(workspace, space_id, principal),
|
||||
executor=executor,
|
||||
)
|
||||
api = create_app()
|
||||
api.dependency_overrides[get_context_principal] = lambda: principal
|
||||
api.dependency_overrides[get_context_application] = lambda: service
|
||||
return api, principal, space_id, candidate, executor
|
||||
|
||||
|
||||
def test_openapi_and_runtime_expose_context_operation() -> None:
|
||||
api, _, _, _, _ = _fixture()
|
||||
document = yaml.safe_load(
|
||||
(Path(__file__).parents[4] / "docs" / "contracts" / "openapi.yaml").read_text()
|
||||
)
|
||||
|
||||
assert set(api.openapi()["paths"]["/v1/context"]) == {"post"}
|
||||
assert "post" in document["paths"]["/v1/context"]
|
||||
|
||||
|
||||
def test_context_preserves_citations_and_budget() -> None:
|
||||
api, _, space_id, _, executor = _fixture()
|
||||
response = TestClient(api).post(
|
||||
"/v1/context",
|
||||
json={
|
||||
"query": "context",
|
||||
"requested_space_ids": [str(space_id)],
|
||||
"max_context_tokens": 256,
|
||||
},
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.json()
|
||||
body = response.json()
|
||||
assert body["blocks"][0]["citation_ids"] == [body["citations"][0]["id"]]
|
||||
assert body["citations"][0]["excerpt"] in body["blocks"][0]["text"]
|
||||
assert body["token_count"] <= 256
|
||||
assert executor.calls[0].scope.effective_space_ids == frozenset({space_id})
|
||||
|
||||
|
||||
def test_context_surfaces_degraded_retrieval() -> None:
|
||||
"""M8-01: a caller must be able to see that context was assembled from a
|
||||
degraded retrieval (e.g. reranker fallback), not just that citations
|
||||
happen to still be there."""
|
||||
|
||||
api, _, space_id, _, executor = _fixture()
|
||||
executor.degraded = True
|
||||
executor.degradation_codes = ("rerank_rejected",)
|
||||
|
||||
response = TestClient(api).post(
|
||||
"/v1/context",
|
||||
json={"query": "context", "requested_space_ids": [str(space_id)]},
|
||||
)
|
||||
|
||||
assert response.status_code == 200, response.json()
|
||||
body = response.json()
|
||||
assert body["degraded"] is True
|
||||
assert "rerank_rejected" in body["degradation_reasons"]
|
||||
assert body["citations"]
|
||||
|
||||
|
||||
def test_context_cannot_expand_to_an_unauthorized_requested_space() -> None:
|
||||
api, _, _, _, executor = _fixture()
|
||||
response = TestClient(api).post(
|
||||
"/v1/context",
|
||||
json={"query": "context", "requested_space_ids": [str(uuid4())]},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
assert response.json()["blocks"] == []
|
||||
assert executor.calls == []
|
||||
|
||||
|
||||
def test_context_validation_errors_are_problem_details() -> None:
|
||||
api, _, _, _, _ = _fixture()
|
||||
response = TestClient(api).post(
|
||||
"/v1/context",
|
||||
json={"query": " ", "max_context_tokens": 256},
|
||||
)
|
||||
|
||||
assert response.status_code == 422
|
||||
assert response.json()["code"] == "VALIDATION_ERROR"
|
||||
|
||||
|
||||
@pytest.mark.parametrize("field", ["query"])
|
||||
def test_missing_required_context_values_are_rejected(field: str) -> None:
|
||||
api, _, _, _, _ = _fixture()
|
||||
payload = {"query": "context", "max_context_tokens": 256}
|
||||
payload.pop(field)
|
||||
response = TestClient(api).post("/v1/context", json=payload)
|
||||
assert response.status_code == 422
|
||||
|
||||
def test_unexpected_executor_failure_is_redacted_and_stable() -> None:
|
||||
api, _, space_id, _, executor = _fixture()
|
||||
executor.fail = True
|
||||
|
||||
response = TestClient(api).post(
|
||||
"/v1/context",
|
||||
json={"query": "context", "requested_space_ids": [str(space_id)]},
|
||||
)
|
||||
|
||||
assert response.status_code == 503
|
||||
body = response.json()
|
||||
assert body["code"] == "CONTEXT_UNAVAILABLE"
|
||||
assert "provider secret" not in response.text
|
||||
|
||||
def test_context_response_schema_matches_runtime_openapi() -> None:
|
||||
api, _, _, _, _ = _fixture()
|
||||
static = yaml.safe_load(
|
||||
(Path(__file__).parents[4] / "docs" / "contracts" / "openapi.yaml").read_text()
|
||||
)["components"]["schemas"]
|
||||
runtime = api.openapi()["components"]["schemas"]
|
||||
|
||||
assert set(static["ContextRequest"]["properties"]) == set(
|
||||
runtime["ContextRequest"]["properties"]
|
||||
)
|
||||
assert static["ContextRequest"]["additionalProperties"] is False
|
||||
max_context_tokens = static["ContextRequest"]["properties"]["max_context_tokens"]
|
||||
assert {key: max_context_tokens[key] for key in ("type", "minimum", "maximum", "default")} == {
|
||||
"type": "integer",
|
||||
"minimum": 256,
|
||||
"maximum": 64000,
|
||||
"default": 8000,
|
||||
}
|
||||
assert static["ContextBlockResponse"]["required"] == runtime["ContextBlockResponse"]["required"]
|
||||
assert static["ContextBlockResponse"]["additionalProperties"] is False
|
||||
assert static["ContextResponse"]["required"] == runtime["ContextResponse"]["required"]
|
||||
assert static["ContextResponse"]["additionalProperties"] is False
|
||||
assert static["ContextResponse"]["properties"]["blocks"]["items"] == {
|
||||
"$ref": "#/components/schemas/ContextBlockResponse"
|
||||
}
|
||||
assert static["ContextResponse"]["properties"]["citations"]["items"] == {
|
||||
"$ref": "#/components/schemas/CitationResponse"
|
||||
}
|
||||
assert static["CitationResponse"]["required"] == runtime["CitationResponse"]["required"]
|
||||
assert static["CitationResponse"]["additionalProperties"] is False
|
||||
Reference in new issue
Block a user