"""Shared plumbing for rules that ask an LLM for a 0-10 score. llm_score 与 fluency 共用的直连 LLM 调用与评分解析——两处各自实现一遍的 重复逻辑收敛到这里(v1.3.1 Phase 3 代码质量项)。 """ import httpx from agenteval.utils.llm import extract_content_from_llm_response, parse_json_from_llm_text def parse_scored_content(content: str) -> tuple[float, str]: """Parse an LLM reply into a clamped 0-10 score plus reason.""" parsed = parse_json_from_llm_text(content) score = float(parsed["score"]) return max(0.0, min(10.0, score)), parsed.get("reason", "") async def call_scored_llm( api_url: str, api_key: str | None, model: str, system_prompt: str, user_prompt: str, timeout: float = 60.0, ) -> tuple[float | None, str]: """POST a chat request to a raw OpenAI-compatible endpoint and parse the score. Returns (score, reason); score is None with an error message on failure. """ headers = {"Content-Type": "application/json"} if api_key: headers["Authorization"] = f"Bearer {api_key}" payload = { "model": model, "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": user_prompt}, ], "temperature": 0.2, } try: async with httpx.AsyncClient(timeout=timeout) as client: resp = await client.post(api_url, headers=headers, json=payload) resp.raise_for_status() content = extract_content_from_llm_response(resp.json()) if not content: return None, "LLM 返回内容为空" return parse_scored_content(content) except Exception as exc: return None, str(exc)