from __future__ import annotations import json import time import urllib.error import urllib.request from typing import Any from screener import FACTOR_FIELDS, REGIMES class LLMCompilerError(RuntimeError): pass def test_llm_connection( api_key: str, base_url: str, model: str, timeout: int = 30, ) -> dict[str, Any]: if not api_key or not model: raise LLMCompilerError("API Key 或模型未配置。") endpoint = f"{base_url.rstrip('/')}/chat/completions" payload = json.dumps( { "model": model, "messages": [{"role": "user", "content": "只回复 OK"}], "stream": False, }, ensure_ascii=False, ).encode("utf-8") request = urllib.request.Request( endpoint, data=payload, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {api_key}", "User-Agent": "XiaobaiReviewWeb/0.5", }, method="POST", ) started = time.perf_counter() try: with urllib.request.urlopen(request, timeout=timeout) as response: result = json.loads(response.read().decode("utf-8")) reply = str(result["choices"][0]["message"]["content"]).strip() except urllib.error.HTTPError as exc: raise LLMCompilerError(_http_error_message(exc)) from exc except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, KeyError, IndexError) as exc: raise LLMCompilerError(f"模型连接测试失败:{exc}") from exc return { "ok": True, "model": model, "reply": reply[:100], "latency_ms": round((time.perf_counter() - started) * 1000), } def compile_strategy_with_llm( prompt: str, regime: str, api_key: str, base_url: str, model: str, timeout: int = 45, ) -> dict[str, Any]: if not api_key or not model: raise LLMCompilerError("尚未配置 LLM API Key 或模型。") endpoint = f"{base_url.rstrip('/')}/chat/completions" schema = { "name": "策略名称", "description": "策略说明", "regimes": [regime], "formula": { "universe": {"exclude_st": True, "listed_days_min": 120}, "filters": [{"field": "return_5d", "op": ">=", "value": 0}], "score": [{"field": "sector_strength", "weight": 0.3, "direction": "desc"}], "limit": 15, "min_score": 0.55, }, } system_prompt = ( "你是A股量化策略编译器。只输出JSON对象,不输出Markdown。" "不得生成Python、SQL、网络请求或未提供的因子。" f"当前市场阶段为{REGIMES.get(regime, regime)}。" f"可用因子为:{json.dumps(FACTOR_FIELDS, ensure_ascii=False)}。" "运算符只能使用 >, >=, <, <=, ==, !=, between, in。" "score权重均大于0且不超过1,direction只能是asc或desc。" "退潮和冰点策略必须提高门槛并允许结果为空。" f"严格遵循以下结构:{json.dumps(schema, ensure_ascii=False)}" ) payload = json.dumps( { "model": model, "messages": [ {"role": "system", "content": system_prompt}, {"role": "user", "content": prompt[:3000]}, ], "stream": False, }, ensure_ascii=False, ).encode("utf-8") request = urllib.request.Request( endpoint, data=payload, headers={ "Content-Type": "application/json", "Authorization": f"Bearer {api_key}", "User-Agent": "XiaobaiReviewWeb/0.4", }, method="POST", ) try: with urllib.request.urlopen(request, timeout=timeout) as response: result = json.loads(response.read().decode("utf-8")) content = result["choices"][0]["message"]["content"].strip() if content.startswith("```"): content = content.strip("`") if content.startswith("json"): content = content[4:].strip() compiled = json.loads(content) except urllib.error.HTTPError as exc: raise LLMCompilerError(_http_error_message(exc).replace("模型连接测试", "LLM 策略编译")) from exc except (urllib.error.URLError, TimeoutError, json.JSONDecodeError, KeyError, IndexError) as exc: raise LLMCompilerError(f"LLM 策略编译失败:{exc}") from exc compiled["compiler"] = "llm" compiled["model"] = model return compiled def _http_error_message(exc: urllib.error.HTTPError) -> str: detail = "" try: payload = json.loads(exc.read().decode("utf-8", errors="replace")) error = payload.get("error") if isinstance(error, dict): detail = str(error.get("message") or error.get("code") or "") elif error: detail = str(error) elif payload.get("message"): detail = str(payload["message"]) except (json.JSONDecodeError, OSError): detail = "" suffix = f":{detail[:300]}" if detail else "" return f"模型连接测试失败(HTTP {exc.code}){suffix}"