diff --git a/src/nsct/api/rest_research.py b/src/nsct/api/rest_research.py index c0c81dc..0f35a6c 100644 --- a/src/nsct/api/rest_research.py +++ b/src/nsct/api/rest_research.py @@ -686,7 +686,11 @@ async def get_sources(research_id: str) -> SourcesResponse: run = _load_run(research_id) sources = [ SourceListItem( - id=s.get("id", str(uuid.uuid4())), + # Pipeline models retain identifiers as UUID objects. The public + # REST contract deliberately exposes JSON strings, so normalize + # here instead of letting strict Pydantic validation turn a + # successful research run into a 500 response. + id=str(s.get("id", uuid.uuid4())), url=s.get("url", ""), title=s.get("title"), domain=s.get("domain", ""), @@ -712,9 +716,9 @@ async def get_claims(research_id: str) -> ClaimsResponse: run = _load_run(research_id) claims = [ ClaimListItem( - id=c.get("id", str(uuid.uuid4())), + id=str(c.get("id", uuid.uuid4())), research_run_id=run.research_id, - source_id=c.get("source_id", ""), + source_id=str(c.get("source_id", "")), claim_text=c.get("claim_text", c.get("claim", "")), evidence_span=c.get("evidence_span", ""), claim_type=c.get("claim_type", "claim"), @@ -737,7 +741,16 @@ async def get_claims(research_id: str) -> ClaimsResponse: async def get_evidence(research_id: str) -> EvidenceResponse: """Evidence-Scores für einen Research-Run abrufen.""" run = _load_run(research_id) - scores = [EvidenceScoreItem(**s) for s in run.evidence_scores] + scores = [ + EvidenceScoreItem( + **{ + **score, + "claim_id": str(score.get("claim_id", "")), + "research_run_id": str(score.get("research_run_id", run.research_id)), + } + ) + for score in run.evidence_scores + ] return EvidenceResponse( research_id=run.research_id, evidence=scores, diff --git a/tests/test_rest_research.py b/tests/test_rest_research.py index 46e2a06..bf7a8d0 100644 --- a/tests/test_rest_research.py +++ b/tests/test_rest_research.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Generator from unittest.mock import MagicMock, patch +from uuid import uuid4 import pytest from fastapi.testclient import TestClient @@ -307,6 +308,48 @@ def test_get_claims_empty(client: TestClient) -> None: assert body["claims"] == [] +def test_detail_endpoints_serialize_pipeline_uuid_identifiers(client: TestClient) -> None: + """Completed pipeline data must satisfy the string-based REST contract.""" + from nsct.api.rest_research import _ResearchRunState, _research_store + from nsct.orchestration.budget import HardBudgetConfig + + _research_store.clear() + research_id = str(uuid4()) + source_id = uuid4() + claim_id = uuid4() + _research_store[research_id] = _ResearchRunState( + research_id=research_id, + query="UUID serialization", + language="de", + depth="quick", + budget=HardBudgetConfig(), + sources=[{"id": source_id, "url": "https://example.org/article"}], + claims=[ + { + "id": claim_id, + "source_id": source_id, + "claim_text": "Eine überprüfbare Behauptung.", + "evidence_span": "Überprüfbare Passage.", + "claim_type": "claim", + } + ], + evidence_scores=[{"claim_id": claim_id, "research_run_id": uuid4()}], + report={"summary": "Zusammenfassung", "findings": [], "methodology": "Methodik"}, + ) + + sources = client.get(f"/v1/research/{research_id}/sources") + claims = client.get(f"/v1/research/{research_id}/claims") + evidence = client.get(f"/v1/research/{research_id}/evidence") + report = client.get(f"/v1/research/{research_id}/report") + + assert [response.status_code for response in (sources, claims, evidence, report)] == [200] * 4 + assert sources.json()["sources"][0]["id"] == str(source_id) + assert claims.json()["claims"][0]["id"] == str(claim_id) + assert claims.json()["claims"][0]["source_id"] == str(source_id) + assert evidence.json()["evidence"][0]["claim_id"] == str(claim_id) + assert report.json()["methodology"] == "Methodik" + + # --------------------------------------------------------------------------- # GET /v1/research/{id}/evidence — evidence # ---------------------------------------------------------------------------