Files
MoFin/deploy/profile-scripts/llm_client.py
T
hmo de9927a627 重评核心重构:ds-v4-pro + 原策略全文 + strategy_history + 前端三改造
后端(重评管线):
- 新增 llm_client.py 共享客户端: REASSESS_MODEL=deepseek-v4-pro 单点,
  gateway预检(fail-fast), 150s超时+1次重试, 永不抛异常
- batch_reassess/per_stock_reassess: curl/urllib -> call_llm,
  prompt传入原策略全文+当前参数+最近3条变更, 输出 维持/修改判断+
  修改点理由+最终新策略, max_tokens 4096
- mofin_db: 新增 strategy_history 表 + snapshot_strategy_history(),
  write_holding_strategy 覆写前自动快照(保留20条/code)
- mofin_db: holding_strategies 补 tag 列迁移 + 写入保留
  (tag缺席=保留旧值, 显式传''=允许清除), 修复推荐标签被静默丢弃
- mo_data.read_decisions: SELECT 补 tag
- stale_detector/promote_candidates: 子进程超时 240/60 -> 480s

前端:
- 移除 报告Tab -> mofin_health 全部流程/Cron 表加 最后十次 列
  (modal列表->详情), /api/reports 支持 cron+script 多路匹配
  (jobs.json name->id 解析 + 文件名/标题子串兜底)
- 移除 决策库Tab
- 盯盘Tab 重构: 全部持仓+自选, sort_group 分组(推荐/持仓/自选),
  推荐行琥珀高亮+🔥badge+行内策略, 新增 操作策略 列查看
  最近3次完整策略(/api/strategy_history/<code>, 表缺失时降级当前行)
- 提示词Tab: registry.py 数据路径改回 /home/hmo/MoFin/data/prompts
  (红线: 数据只在规范数据根), 空态提示初始化命令
2026-07-20 23:51:24 +08:00

134 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#!/usr/bin/env python3
"""llm_client.py — 共享 LLM 客户端(重试 + gateway 预检)
所有重评脚本统一通过此模块调用 LLM gateway,避免重复的 HTTP/重试逻辑。
用法:
from llm_client import call_llm, REASSESS_MODEL, gateway_alive
if not gateway_alive():
print("Gateway 不可用,退出")
sys.exit(1)
result = call_llm(prompt)
if result["ok"]:
full_text = result["content"]
"""
import json
import time
import urllib.request
import urllib.error
# ── 常量:所有重评调用统一使用 ──
REASSESS_MODEL = "deepseek-v4-pro"
GATEWAY = "http://127.0.0.1:8643/v1/chat/completions"
GATEWAY_BASE = "http://127.0.0.1:8643/v1/models"
AUTH = "Bearer hermes123"
def gateway_alive(timeout=5):
"""快速预检 gateway 是否存活。失败立刻返回 False,不阻塞。"""
try:
req = urllib.request.Request(GATEWAY_BASE, headers={"Authorization": AUTH})
urllib.request.build_opener(urllib.request.ProxyHandler({})).open(req, timeout=timeout)
return True
except Exception:
return False
def call_llm(prompt, model=None, max_tokens=4096, timeout=150,
retries=1, backoff=20, system=None):
"""调用 LLM gateway,带重试和结构化日志。
Args:
prompt: 用户消息内容
model: 模型名(默认 REASSESS_MODEL
max_tokens: 最大输出 token 数
timeout: 单次调用超时(秒)
retries: 超时/5xx/连接错误时的重试次数
backoff: 重试间隔(秒)
system: 可选 system message
Returns:
{ok: bool, content: str, error: str|None, model: str,
elapsed: float, attempts: int}
永远不抛异常到调用方。
"""
model_name = model or REASSESS_MODEL
messages = []
if system:
messages.append({"role": "system", "content": system})
messages.append({"role": "user", "content": prompt})
payload = json.dumps({
"model": model_name,
"messages": messages,
"max_tokens": max_tokens,
}).encode()
for attempt in range(retries + 1):
t0 = time.monotonic()
try:
req = urllib.request.Request(
GATEWAY,
data=payload,
headers={
"Content-Type": "application/json",
"Authorization": AUTH,
}
)
opener = urllib.request.build_opener(urllib.request.ProxyHandler({}))
resp = opener.open(req, timeout=timeout)
elapsed = time.monotonic() - t0
body = json.loads(resp.read().decode())
if "choices" not in body:
msg = f"API响应无choices字段: {str(body)[:200]}"
print(f" [LLM] 尝试{attempt+1}/{retries+1} 失败({elapsed:.1f}s): {msg}", flush=True)
if attempt < retries:
time.sleep(backoff)
continue
content = body["choices"][0]["message"]["content"]
print(f" [LLM] 尝试{attempt+1}/{retries+1} 成功, {elapsed:.1f}s, "
f"输出{len(content)}字, model={model_name}", flush=True)
return {
"ok": True,
"content": content,
"error": None,
"model": model_name,
"elapsed": elapsed,
"attempts": attempt + 1,
}
except urllib.error.URLError as e:
elapsed = time.monotonic() - t0
err_msg = str(e)
print(f" [LLM] 尝试{attempt+1}/{retries+1} 连接失败({elapsed:.1f}s): {err_msg[:120]}", flush=True)
if attempt < retries:
print(f" [LLM] 等待{backoff}s后重试...", flush=True)
time.sleep(backoff)
else:
return {
"ok": False, "content": "", "error": f"连接失败(重试{retries}次): {err_msg[:200]}",
"model": model_name, "elapsed": elapsed, "attempts": attempt + 1,
}
except Exception as e:
elapsed = time.monotonic() - t0
err_msg = str(e)
print(f" [LLM] 尝试{attempt+1}/{retries+1} 失败({elapsed:.1f}s): {err_msg[:120]}", flush=True)
if attempt < retries:
print(f" [LLM] 等待{backoff}s后重试...", flush=True)
time.sleep(backoff)
else:
return {
"ok": False, "content": "", "error": f"调用失败(重试{retries}次): {err_msg[:200]}",
"model": model_name, "elapsed": elapsed, "attempts": attempt + 1,
}
# Unreachable
return {
"ok": False, "content": "", "error": "未预期的调用结束",
"model": model_name, "elapsed": 0, "attempts": retries + 1,
}