diff --git a/backend/app/pipeline/orchestrator.py b/backend/app/pipeline/orchestrator.py index 1d93038..f9380b4 100644 --- a/backend/app/pipeline/orchestrator.py +++ b/backend/app/pipeline/orchestrator.py @@ -130,12 +130,19 @@ class RecommendationPipeline: ) pool = await self._grounded_pool(session_id, catalog, intent, taste) - if len(pool) < self.settings.grounding_floor: + if not pool: yield PipelineErrorEvent( - code="insufficient_grounding", - message="Not enough requested tracks could be verified safely.", + code="no_grounded_results", + message="None of the proposed tracks could be verified on Spotify.", ) return + if len(pool) < self.settings.grounding_floor: + # Fewer verified tracks than promised is still an answer; an + # empty error in its place would hide real results. + yield PipelineWarningEvent( + code="partial_results", + message="Fewer tracks than usual could be verified; showing what held up.", + ) selection = _TrackSelection(pool, self.settings.rerank_count) async for event in self._ranked_events(intent, taste.text, history, selection): diff --git a/backend/tests/test_orchestrator.py b/backend/tests/test_orchestrator.py index 5dd0189..4f2fc6f 100644 --- a/backend/tests/test_orchestrator.py +++ b/backend/tests/test_orchestrator.py @@ -243,3 +243,46 @@ def _track(track_id: str, title: str) -> Track: album_art_url=None, external_url=None, ) + + +def test_below_floor_pool_streams_partial_results_after_warning() -> None: + async def run() -> None: + found = _track("found", "Found Song") + missing = _track("missing", "Missing Song") + catalog = FakeCatalog((found,)) + recommender = FakeRecommender([_intent(found, missing)]) + pipeline = RecommendationPipeline( + recommender, + Settings( + rerank_count=2, + rerank_pool_buffer=0, + grounding_floor=2, + grounding_concurrency=2, + request_deadline_seconds=1.0, + ), + ) + + events = await _collect(pipeline, catalog, "query") + + assert [event.type for event in events] == ["metadata", "warning", "track", "done"] + warning = events[1] + assert warning.type == "warning" + assert warning.code == "partial_results" + + asyncio.run(run()) + + +def test_empty_pool_is_a_terminal_error() -> None: + async def run() -> None: + missing = _track("missing", "Missing Song") + catalog = FakeCatalog(()) + recommender = FakeRecommender([_intent(missing)]) + + events = await _run_pipeline(catalog, recommender) + + assert [event.type for event in events] == ["metadata", "error"] + error = events[-1] + assert error.type == "error" + assert error.code == "no_grounded_results" + + asyncio.run(run()) diff --git a/docs/logboek.md b/docs/logboek.md index 6d633d3..e782cdb 100644 --- a/docs/logboek.md +++ b/docs/logboek.md @@ -164,3 +164,18 @@ Wat ik deed: max_tokens en kon de gestructureerde output afkappen. - Per unresolved kandidaat wordt titel, artiest en status gelogd, zodat zichtbaar is WAT het model verzon in plaats van alleen hoeveel. + +### Eerlijke gedeeltelijke resultaten + +Wat ik deed: + +- Onder de grounding-floor geeft de pipeline nu een warning plus de nummers + die wel geverifieerd zijn, in plaats van een lege foutmelding; alleen een + volledig lege pool is nog een terminal error. + +Waarom: + +- Vijf goede, geverifieerde nummers zijn een bruikbaar antwoord; een + foutmelding die echte resultaten verbergt is dat niet. Bij "verras me + met iets nieuws" vragen valt een groot deel van de kandidaten af bij de + verificatie, dus juist daar telt dit. Een weg om dit potentieel te voorkomen/verbeteren in de toekomst is het verbeteren van de prompt, of de LLM met behulp van een derde partij API die zonder de Spotify API te overbelasten gebruikt kan worden om echte nummers te vinden.