"""解析模型返回为演化候选草稿(纯函数,无 IO)。 模型可能返回纯 JSON、markdown 代码块或夹杂解释文本;本模块容错提取首个 JSON 数组,并归一化为演化候选草稿。``evidence`` 序号由 application 映射为记忆 ID。 缺省字段取**安全悲观默认**:``level=L4``(必须人工确认)、``auto_apply_allowed=False``, 避免模型漏字段时误自动应用高风险演化。 """ import json import re from dataclasses import dataclass, field from typing import Any @dataclass class EvolutionCandidateDraft: type: str title: str proposal: str level: str = "L4" scope: str = "character_scope" source: str = "big_cycle" delta: dict[str, Any] = field(default_factory=dict) confidence: float = 0.5 benefit_score: float = 0.5 risk_score: float = 0.5 drift_score: float = 0.0 auto_apply_allowed: bool = False evidence: list[int] = field(default_factory=list) def parse_evolution_candidates(raw: str) -> list[EvolutionCandidateDraft]: drafts: list[EvolutionCandidateDraft] = [] for item in _extract_json_array(raw): if not isinstance(item, dict): continue type_ = str(item.get("type", "")).strip() title = str(item.get("title", "")).strip() proposal = str(item.get("proposal", "")).strip() if not type_ or not title or not proposal: continue drafts.append( EvolutionCandidateDraft( type=type_, title=title, proposal=proposal, level=_level(item.get("level")), scope=_scope(item.get("scope")), source=str(item.get("source", "big_cycle")).strip() or "big_cycle", delta=_dict(item.get("delta")), confidence=_clamp_float(item.get("confidence"), 0.5), benefit_score=_clamp_float(item.get("benefit_score"), 0.5), risk_score=_clamp_float(item.get("risk_score"), 0.5), drift_score=_clamp_float(item.get("drift_score"), 0.0), auto_apply_allowed=bool(item.get("auto_apply_allowed", False)), evidence=_int_list(item.get("evidence")), ) ) return drafts def _extract_json_array(raw: str) -> list[Any]: text = raw.strip() matched = re.search(r"```(?:json)?\s*(\[.*?\])\s*```", text, re.DOTALL) if matched: text = matched.group(1) else: start, end = text.find("["), text.rfind("]") if start != -1 and end > start: text = text[start : end + 1] try: data = json.loads(text) except json.JSONDecodeError: return [] return data if isinstance(data, list) else [] _VALID_LEVELS = {"L0", "L1", "L2", "L3", "L4"} _VALID_SCOPES = {"character_scope", "user_scope", "task_scope", "global_scope"} def _level(value: Any) -> str: text = str(value or "").strip().upper() return text if text in _VALID_LEVELS else "L4" def _scope(value: Any) -> str: text = str(value or "").strip() return text if text in _VALID_SCOPES else "character_scope" def _dict(value: Any) -> dict[str, Any]: return value if isinstance(value, dict) else {} def _clamp_float(value: Any, default: float) -> float: try: num = float(value) except (TypeError, ValueError): return default return max(0.0, min(1.0, num)) def _int_list(value: Any) -> list[int]: if not isinstance(value, list): return [] result: list[int] = [] for item in value: try: result.append(int(item)) except (TypeError, ValueError): continue return result