From c269353213aff71ef6062f1b020791760812042b Mon Sep 17 00:00:00 2001 From: BazanJeremy Date: Mon, 28 Sep 2026 08:12:19 +0200 Subject: [PATCH] =?UTF-8?q?refactor(agents):=20sorties=20structur=C3=A9es?= =?UTF-8?q?=20pour=20le=20scoreur=20de=20s=C3=A9v=C3=A9rit=C3=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le prompt imposait le format par la prose (« Return ONLY the JSON object ») et le code retirait d'éventuelles balises Markdown à la regex avant json.loads. Le format est maintenant garanti par l'API (output_config.format, schéma JSON avec les valeurs autorisées de chaque dimension) : la consigne et la regex deviennent inutiles et sont retirées. Un arrêt autre que end_turn (max_tokens, refusal) lève désormais une erreur explicite ; score() bascule alors sur le calcul par règles, comme pour toute autre erreur de l'API. claude-sonnet-4-6 figure dans la liste des modèles compatibles avec les sorties structurées (documentation Anthropic, consultée le 28.09.2026). Co-Authored-By: Claude Opus 5.5 --- src/agents/severity_scorer.py | 31 +++++++++++++++++++++++++------ 1 file changed, 25 insertions(+), 6 deletions(-) diff --git a/src/agents/severity_scorer.py b/src/agents/severity_scorer.py index a9ba3f2..04c34d5 100644 --- a/src/agents/severity_scorer.py +++ b/src/agents/severity_scorer.py @@ -166,14 +166,31 @@ def score_rule_based( # --------------------------------------------------------------------------- _SYSTEM_PROMPT = """You are a senior QA engineer specialised in severity assessment. -Given a bug report, return a CVSS-lite severity score as valid JSON with these keys: +Given a bug report, return a CVSS-lite severity score with these fields: - functional_impact: "none" | "partial" | "full" - reproducibility: "always" | "sometimes" | "rare" - user_scope: "single" | "group" | "all" - regression_type: "new" | "known" | "reopened" -- rationale: str (one sentence justifying the scoring, max 200 chars) - -Return ONLY the JSON object. No markdown, no explanation.""" +- rationale: one sentence justifying the scoring, max 200 chars""" + +_RESPONSE_SCHEMA = { + "type": "object", + "properties": { + "functional_impact": {"type": "string", "enum": ["none", "partial", "full"]}, + "reproducibility": {"type": "string", "enum": ["always", "sometimes", "rare"]}, + "user_scope": {"type": "string", "enum": ["single", "group", "all"]}, + "regression_type": {"type": "string", "enum": ["new", "known", "reopened"]}, + "rationale": {"type": "string"}, + }, + "required": [ + "functional_impact", + "reproducibility", + "user_scope", + "regression_type", + "rationale", + ], + "additionalProperties": False, +} def score_with_claude( @@ -197,15 +214,17 @@ def score_with_claude( max_tokens=512, system=_SYSTEM_PROMPT, messages=[{"role": "user", "content": context}], + output_config={"format": {"type": "json_schema", "schema": _RESPONSE_SCHEMA}}, ) + if message.stop_reason != "end_turn": + raise RuntimeError(f"LLM stopped early (stop_reason={message.stop_reason})") raw_text = "" for block in message.content: if hasattr(block, "text"): raw_text += block.text - clean = re.sub(r"^```(?:json)?\s*|\s*```$", "", raw_text.strip(), flags=re.MULTILINE) - data = json.loads(clean) + data = json.loads(raw_text) impact: FunctionalImpact = data.get("functional_impact", "partial") repro: Reproducibility = data.get("reproducibility", "sometimes")