Initial curated RAGcore source from c16667f0c9397e2bfc8dd0255b114fba23c8d05e

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:34:21 +02:00
commit 7ca726caf2
659 files changed
+98171

No files matched your search

@@ -0,0 +1,482 @@
from __future__ import annotations
import asyncio
import json
import re
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.answers.dependencies import get_answer_application, get_answer_principal
from ragcore.api.v1.answers.service import (
AnswerEventType,
AnswerQuery,
AnswerService,
)
from ragcore.api.v1.context.service import ContextExecutionRequest, ContextExecutionResult
from ragcore.api.v1.search.service import SearchQuery
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
WORKSPACE_ID = UUID("00000000-0000-0000-0000-000000000001")
SPACE_ID = UUID("00000000-0000-0000-0000-000000000002")
EVIDENCE_EXCERPT = (
"The quarterly maintenance window for the payment gateway runs every "
"second Sunday from midnight to four in the morning Central European time."
)
class FixtureContextExecutor:
def __init__(
self,
candidate: ContextCandidate | None,
*,
degraded: bool = False,
degradation_codes: tuple[str, ...] = (),
) -> None:
self.candidate = candidate
self.degraded = degraded
self.degradation_codes = degradation_codes
self.calls: list[ContextExecutionRequest] = []
async def execute(self, request: ContextExecutionRequest) -> ContextExecutionResult:
self.calls.append(request)
if self.candidate is None:
return ContextExecutionResult(
degraded=self.degraded, degradation_codes=self.degradation_codes
)
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,
)
_CITATION_ID = re.compile(r"citation_ids=([0-9a-fA-F-]{36})")
class FixtureGenerator:
"""``None`` in ``responses`` means: build a valid answer citing whichever
citation id the prompt's evidence fence actually carries -- the real id
depends on the request's own (unpredictable) retrieval_run_id, so it
cannot be precomputed by the test itself.
"""
def __init__(self, responses: list[str | None] | None = None) -> None:
self.responses: list[str | None] = list(responses if responses is not None else [None])
self.calls: list[tuple[str, ...]] = []
async def generate(self, *, prompt: str, generation_profile: str, spaces) -> str:
self.calls.append(tuple(space.slug for space in spaces))
if not self.responses:
raise AssertionError("generator was called more times than expected")
response = self.responses.pop(0)
if response is not None:
return response
match = _CITATION_ID.search(prompt)
assert match is not None, "prompt did not contain a citation id fence"
return json.dumps(
{
"status": "answerable",
"answer": (
"The quarterly maintenance window for the payment gateway runs "
"every second Sunday from midnight to four in the morning."
),
"citations": [match.group(1)],
}
)
class HangingGenerator:
def __init__(self) -> None:
self.started = asyncio.Event()
self.cancelled = False
async def generate(self, *, prompt: str, generation_profile: str, spaces) -> str:
self.started.set()
try:
await asyncio.Event().wait()
except asyncio.CancelledError:
self.cancelled = True
raise
return "unreachable"
class FixtureInputs:
def __init__(self, workspace: Workspace, principal: Principal, space_id: UUID) -> None:
self.workspace = workspace
self.principal = principal
self.space_id = space_id
async def load(self, principal: Principal):
space = self.workspace.create_knowledge_space(
id=self.space_id, name="Answers space", slug="answers-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="Answers application",
default_knowledge_space_ids=frozenset({self.space_id}),
),
},
)()
def _candidate(excerpt: str = EVIDENCE_EXCERPT) -> ContextCandidate:
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=excerpt,
source_uri="https://example.test/doc.md",
acquired_at=datetime(2026, 8, 1, 12, tzinfo=UTC),
retrieval_run_id=uuid4(),
index_alias="rag_dense_hybrid_active",
profile_version="hybrid-v1",
)
return ContextCandidate(citation, excerpt, 1, 1.0)
def _fixture(
*,
candidate: ContextCandidate | None,
generator: object,
degraded: bool = False,
degradation_codes: tuple[str, ...] = (),
) -> tuple[object, Principal, UUID, FixtureContextExecutor]:
workspace = Workspace(WORKSPACE_ID, "Answers workspace", "answers-workspace")
space_id = SPACE_ID
principal = Principal(uuid4(), workspace.id, PrincipalType.HUMAN, frozenset({Role.EDITOR}))
executor = FixtureContextExecutor(
candidate, degraded=degraded, degradation_codes=degradation_codes
)
service = AnswerService(
authorization=RetrievalAuthorizationService(),
inputs=FixtureInputs(workspace, principal, space_id),
context_executor=executor,
generator=generator,
)
api = create_app()
api.dependency_overrides[get_answer_principal] = lambda: principal
api.dependency_overrides[get_answer_application] = lambda: service
return api, principal, space_id, executor
def test_openapi_and_runtime_expose_answer_operation() -> None:
candidate = _candidate()
api, _, _, _ = _fixture(candidate=candidate, generator=FixtureGenerator())
document = yaml.safe_load(
(Path(__file__).parents[4] / "docs" / "contracts" / "openapi.yaml").read_text()
)
assert set(api.openapi()["paths"]["/v1/answers"]) == {"post"}
assert "post" in document["paths"]["/v1/answers"]
def test_grounded_answer_returns_citations_and_claims() -> None:
candidate = _candidate()
generator = FixtureGenerator()
api, _, space_id, executor = _fixture(candidate=candidate, generator=generator)
response = TestClient(api).post(
"/v1/answers",
json={"query": "when is the maintenance window?", "requested_space_ids": [str(space_id)]},
)
assert response.status_code == 200, response.json()
body = response.json()
# A single contributing document assembles as "partially_answerable"
# (ContextAssembler's own document-diversity heuristic); generation is
# still expected to proceed for any status other than not_answerable.
assert body["answerability"] == "partially_answerable"
assert "maintenance window" in body["answer"]
assert body["citations"]
assert body["claims"]
assert all(claim["citation_ids"] for claim in body["claims"])
assert executor.calls[0].scope.effective_space_ids == frozenset({space_id})
def test_reranker_failure_degrades_but_still_returns_a_grounded_answer() -> None:
"""M8-01: a reranker that 404s or times out must not turn found evidence
into a not_answerable, 0-citation response -- it degrades to the fused
ranking, and the caller can see that happened via degraded=true."""
candidate = _candidate()
generator = FixtureGenerator()
api, _, space_id, _ = _fixture(
candidate=candidate,
generator=generator,
degraded=True,
degradation_codes=("rerank_rejected",),
)
response = TestClient(api).post(
"/v1/answers",
json={"query": "when is the maintenance window?", "requested_space_ids": [str(space_id)]},
)
assert response.status_code == 200, response.json()
body = response.json()
assert body["answerability"] != "not_answerable"
assert body["citations"]
assert body["degraded"] is True
assert "rerank_rejected" in body["degradation_reasons"]
def test_search_authorization_is_reused_and_narrows_to_effective_spaces() -> None:
candidate = _candidate()
generator = FixtureGenerator()
api, _, _, executor = _fixture(candidate=candidate, generator=generator)
forbidden_space = uuid4()
response = TestClient(api).post(
"/v1/answers",
json={"query": "anything", "requested_space_ids": [str(forbidden_space)]},
)
assert response.status_code == 200
body = response.json()
assert body["answerability"] == "not_answerable"
assert body["answer"] == ""
assert executor.calls == []
assert generator.calls == []
def test_not_answerable_context_never_calls_the_generator() -> None:
generator = FixtureGenerator()
api, _, space_id, executor = _fixture(candidate=None, generator=generator)
response = TestClient(api).post(
"/v1/answers",
json={"query": "anything", "requested_space_ids": [str(space_id)]},
)
assert response.status_code == 200
body = response.json()
assert body["answerability"] == "not_answerable"
assert body["answer"] == ""
assert body["claims"] == []
assert len(executor.calls) == 1
assert generator.calls == []
def test_invalid_generated_output_is_rejected_not_silently_accepted() -> None:
candidate = _candidate()
generator = FixtureGenerator(["not json at all", "still not json"])
api, _, space_id, _ = _fixture(candidate=candidate, generator=generator)
response = TestClient(api).post(
"/v1/answers",
json={
"query": "when is the maintenance window?",
"requested_space_ids": [str(space_id)],
},
)
assert response.status_code == 503
# ClaimCitationValidator's own retry loop exhausted both attempts and
# raises its stable "exhausted" code, not a silently-degraded answer.
assert response.json()["code"] == "VALIDATION_RETRIES_EXHAUSTED"
assert len(generator.calls) == 2
def test_debug_output_requires_admin_capability() -> None:
candidate = _candidate()
api, _, space_id, _ = _fixture(candidate=candidate, generator=FixtureGenerator())
response = TestClient(api).post(
"/v1/answers",
json={
"query": "anything",
"requested_space_ids": [str(space_id)],
"include_debug": True,
},
)
assert response.status_code == 403
assert response.json()["code"] == "DEBUG_NOT_ALLOWED"
def test_missing_answer_service_is_503_problem_details() -> None:
candidate = _candidate()
api, _, _, _ = _fixture(candidate=candidate, generator=FixtureGenerator())
# create_app() always wires a real answer_application now; popping the
# dependency override alone would just fall through to that real service
# instead of exercising get_answer_application's own missing-state
# fallback, so the state attribute itself has to be genuinely absent.
api.dependency_overrides.pop(get_answer_application)
del api.state.answer_application
response = TestClient(api).post("/v1/answers", json={"query": "anything"})
assert response.status_code == 503
assert response.json()["code"] == "ANSWERS_UNAVAILABLE"
def test_missing_query_is_a_validation_error() -> None:
candidate = _candidate()
api, _, _, _ = _fixture(candidate=candidate, generator=FixtureGenerator())
response = TestClient(api).post("/v1/answers", json={})
assert response.status_code == 422
assert response.json()["code"] == "VALIDATION_ERROR"
def test_sse_stream_emits_metadata_before_done_in_order() -> None:
candidate = _candidate()
generator = FixtureGenerator()
api, _, space_id, _ = _fixture(candidate=candidate, generator=generator)
with TestClient(api).stream(
"POST",
"/v1/answers",
json={
"query": "when is the maintenance window?",
"requested_space_ids": [str(space_id)],
"stream": True,
},
) as response:
assert response.status_code == 200
assert response.headers["content-type"].startswith("text/event-stream")
raw = response.read().decode()
frames = [frame for frame in raw.strip().split("\n\n") if frame]
assert len(frames) == 2
def _parse(frame: str) -> tuple[str, dict]:
lines = frame.splitlines()
event_line = next(line for line in lines if line.startswith("event: "))
data_line = next(line for line in lines if line.startswith("data: "))
return event_line[len("event: ") :], json.loads(data_line[len("data: ") :])
first_type, first_data = _parse(frames[0])
second_type, second_data = _parse(frames[1])
assert first_type == AnswerEventType.RETRIEVAL_METADATA.value
assert first_data["answerability"] == "partially_answerable"
assert "effective_space_ids" in first_data
assert second_type == AnswerEventType.DONE.value
assert second_data["answerability"] == "partially_answerable"
assert second_data["citations"]
assert "maintenance window" in second_data["answer"]
def test_sse_stream_reports_a_terminal_error_event_on_invalid_output() -> None:
candidate = _candidate()
generator = FixtureGenerator(["not json", "still not json"])
api, _, space_id, _ = _fixture(candidate=candidate, generator=generator)
with TestClient(api).stream(
"POST",
"/v1/answers",
json={
"query": "when is the maintenance window?",
"requested_space_ids": [str(space_id)],
"stream": True,
},
) as response:
assert response.status_code == 200
raw = response.read().decode()
frames = [frame for frame in raw.strip().split("\n\n") if frame]
assert len(frames) == 2
assert "event: retrieval_metadata" in frames[0]
assert "event: error" in frames[1]
assert '"validation_retries_exhausted"' in frames[1]
async def test_disconnect_cancels_provider_work() -> None:
candidate = _candidate()
generator = HangingGenerator()
workspace = Workspace(WORKSPACE_ID, "Answers workspace", "answers-workspace")
space_id = SPACE_ID
principal = Principal(uuid4(), workspace.id, PrincipalType.HUMAN, frozenset({Role.EDITOR}))
executor = FixtureContextExecutor(candidate)
service = AnswerService(
authorization=RetrievalAuthorizationService(),
inputs=FixtureInputs(workspace, principal, space_id),
context_executor=executor,
generator=generator,
)
query = AnswerQuery(
query=SearchQuery(
query="when is the maintenance window?", requested_space_ids=(space_id,)
)
)
authorized = await service.authorize(principal, query)
stream = service.answer_stream(principal, query, authorized)
first_event = await stream.__anext__()
assert first_event.type is AnswerEventType.RETRIEVAL_METADATA
with pytest.raises(TimeoutError):
await asyncio.wait_for(stream.__anext__(), timeout=0.2)
assert generator.cancelled is True
def test_answer_response_schema_matches_runtime_openapi() -> None:
candidate = _candidate()
api, _, _, _ = _fixture(candidate=candidate, generator=FixtureGenerator())
static = yaml.safe_load(
(Path(__file__).parents[4] / "docs" / "contracts" / "openapi.yaml").read_text()
)["components"]["schemas"]
runtime = api.openapi()["components"]["schemas"]
assert static["AnswerClaim"]["required"] == runtime["AnswerClaim"]["required"]
assert static["AnswerResponse"]["required"] == runtime["AnswerResponse"]["required"]
def _resolved_required(schemas: dict, ref: dict) -> set[str]:
name = ref["$ref"].rsplit("/", 1)[-1]
return set(schemas[name].get("required", ()))
static_citation_ref = static["AnswerResponse"]["properties"]["citations"]["items"]
runtime_citation_ref = runtime["AnswerResponse"]["properties"]["citations"]["items"]
assert _resolved_required(static, static_citation_ref) == _resolved_required(
runtime, runtime_citation_ref
)