From 95c95d95ab3afb4a134f4abeba08764078480eb5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?LLM=E4=B8=9A=E5=8A=A1=E6=95=B0=E6=8D=AE=E4=B8=93=E5=AE=B6?= <266125204+alibababye@users.noreply.github.com> Date: Tue, 23 Jun 2026 01:23:11 +0800 Subject: [PATCH] fix: parse embedded ranking JSON Handle nested ranking JSON objects when an LLM wraps the response in explanatory prose. This keeps criterion scores from silently falling back to defaults. --- backend/app/services/ranking_service.py | 2 +- backend/tests/test_ranking_variance.py | 31 ++++++++++++++++++++++++- 2 files changed, 31 insertions(+), 2 deletions(-) diff --git a/backend/app/services/ranking_service.py b/backend/app/services/ranking_service.py index 6d030fc..d1891a7 100644 --- a/backend/app/services/ranking_service.py +++ b/backend/app/services/ranking_service.py @@ -289,7 +289,7 @@ def _parse_llm_response( except json.JSONDecodeError: # Try to find JSON object in text import re - match = re.search(r'\{[^{}]*\}', response_text, re.DOTALL) + match = re.search(r'\{[\s\S]*\}', response_text) if match: try: data = json.loads(match.group()) diff --git a/backend/tests/test_ranking_variance.py b/backend/tests/test_ranking_variance.py index 0c426c0..405b2eb 100644 --- a/backend/tests/test_ranking_variance.py +++ b/backend/tests/test_ranking_variance.py @@ -10,7 +10,7 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) from app.models.idea import IdeaCandidate, DraftPlan, RiskItem, ExperimentSpec -from app.services.ranking_service import RankingService +from app.services.ranking_service import PAPER_TYPE_WEIGHTS, RankingService from datetime import datetime @@ -93,6 +93,34 @@ def _make_candidate(idx: int, session_id: str = "test_session") -> IdeaCandidate ) +def test_parse_embedded_nested_json_response(): + """LLM ranking JSON may be wrapped in explanatory prose.""" + service = RankingService() + response = """ + Here is the ranking result: + { + "novelty": {"score": 9.0, "rationale": "Fresh direction"}, + "feasibility": {"score": 8.0, "rationale": "Buildable"}, + "impact": {"score": 7.0, "rationale": "Useful"}, + "clarity": {"score": 6.0, "rationale": "Clear enough"}, + "risk": {"score": 5.0, "rationale": "Manageable"}, + "alignment": {"score": 4.0, "rationale": "Partial fit"}, + "referenceSupport": {"score": 3.0, "rationale": "Sparse refs"}, + "experimentSpecificity": {"score": 2.0, "rationale": "Needs detail"}, + "overallRationale": "Good candidate", + "confidence": 0.9 + } + """ + + result = service._parse_llm_response("cand_embedded", response, PAPER_TYPE_WEIGHTS["default"]) + + assert result.criteria["novelty"].value == 9.0 + assert result.criteria["experimentSpecificity"].value == 2.0 + assert result.overallRationale == "Good candidate" + assert result.confidence == 0.9 + print("PASS: test_parse_embedded_nested_json_response") + + def test_heuristic_scores_are_not_all_equal(): """Scores must vary across 6 distinct candidates (heuristic fallback).""" service = RankingService() @@ -197,6 +225,7 @@ def test_score_breakdown_structure(): if __name__ == "__main__": + test_parse_embedded_nested_json_response() test_overall_score_uses_all_8_criteria() test_score_breakdown_structure() test_heuristic_scores_are_not_all_equal()