后端(重评管线):
- 新增 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
(红线: 数据只在规范数据根), 空态提示初始化命令
195 lines
6.3 KiB
Python
195 lines
6.3 KiB
Python
"""提示词注册表 — 所有提示词的CRUD和版本管理"""
|
|
|
|
import json
|
|
import shutil
|
|
from pathlib import Path
|
|
from datetime import datetime
|
|
from typing import Optional
|
|
|
|
from .models import PromptDef, PromptVersion, PROMPT_CATEGORIES
|
|
|
|
# 数据文件路径
|
|
DATA_DIR = Path("/home/hmo/MoFin/data/prompts")
|
|
REGISTRY_PATH = DATA_DIR / "registry.json"
|
|
VERSIONS_DIR = DATA_DIR / "versions"
|
|
|
|
|
|
def ensure_dirs():
|
|
DATA_DIR.mkdir(parents=True, exist_ok=True)
|
|
VERSIONS_DIR.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
|
def load_registry() -> dict:
|
|
"""加载注册表"""
|
|
ensure_dirs()
|
|
if not REGISTRY_PATH.exists():
|
|
return {"prompts": [], "updated_at": datetime.now().isoformat()}
|
|
try:
|
|
return json.loads(REGISTRY_PATH.read_text(encoding="utf-8"))
|
|
except (json.JSONDecodeError, FileNotFoundError):
|
|
return {"prompts": [], "updated_at": datetime.now().isoformat()}
|
|
|
|
|
|
def save_registry(data: dict):
|
|
ensure_dirs()
|
|
data["updated_at"] = datetime.now().isoformat()
|
|
REGISTRY_PATH.write_text(
|
|
json.dumps(data, ensure_ascii=False, indent=2),
|
|
encoding="utf-8",
|
|
)
|
|
|
|
|
|
# ═══════════════════════════════════════════
|
|
# CRUD
|
|
# ═══════════════════════════════════════════
|
|
|
|
def list_prompts(category: str = None) -> list:
|
|
"""列出所有提示词,可选按分类过滤"""
|
|
reg = load_registry()
|
|
prompts = []
|
|
for p in reg.get("prompts", []):
|
|
if category and p.get("category") != category:
|
|
continue
|
|
prompts.append(dict(PromptDef.from_dict(p).to_dict()))
|
|
return prompts
|
|
|
|
|
|
def get_prompt(prompt_id: str) -> Optional[PromptDef]:
|
|
"""获取单个提示词"""
|
|
reg = load_registry()
|
|
for p in reg.get("prompts", []):
|
|
if p["id"] == prompt_id:
|
|
return PromptDef.from_dict(p)
|
|
return None
|
|
|
|
|
|
def add_prompt(prompt_def: PromptDef):
|
|
"""添加新提示词"""
|
|
reg = load_registry()
|
|
# 检查是否已存在
|
|
for i, p in enumerate(reg.get("prompts", [])):
|
|
if p["id"] == prompt_def.id:
|
|
reg["prompts"][i] = prompt_def.to_dict()
|
|
break
|
|
else:
|
|
reg.setdefault("prompts", []).append(prompt_def.to_dict())
|
|
save_registry(reg)
|
|
|
|
|
|
def update_prompt(prompt_id: str, updates: dict):
|
|
"""更新提示词元数据"""
|
|
reg = load_registry()
|
|
for p in reg.get("prompts", []):
|
|
if p["id"] == prompt_id:
|
|
for k, v in updates.items():
|
|
if k not in ("id", "versions", "created_at"):
|
|
p[k] = v
|
|
break
|
|
save_registry(reg)
|
|
|
|
|
|
def delete_prompt(prompt_id: str):
|
|
"""删除提示词"""
|
|
reg = load_registry()
|
|
reg["prompts"] = [p for p in reg.get("prompts", []) if p["id"] != prompt_id]
|
|
save_registry(reg)
|
|
|
|
|
|
def get_version_content(prompt_id: str, version: str) -> Optional[str]:
|
|
"""读取版本内容文件"""
|
|
content_path = VERSIONS_DIR / f"{prompt_id}-{version}.md"
|
|
if content_path.exists():
|
|
return content_path.read_text(encoding="utf-8")
|
|
# 回退:从版本记录的 content 字段读取
|
|
prompt = get_prompt(prompt_id)
|
|
if prompt:
|
|
for v in prompt.versions:
|
|
if v.version == version:
|
|
return v.content
|
|
return None
|
|
|
|
|
|
# ═══════════════════════════════════════════
|
|
# 版本管理
|
|
# ═══════════════════════════════════════════
|
|
|
|
def add_version(prompt_id: str, version: PromptVersion):
|
|
"""添加新版本到提示词"""
|
|
prompt = get_prompt(prompt_id)
|
|
if not prompt:
|
|
raise ValueError(f"提示词 '{prompt_id}' 不存在")
|
|
|
|
# 检查版本号是否已存在
|
|
for i, v in enumerate(prompt.versions):
|
|
if v.version == version.version:
|
|
prompt.versions[i] = version
|
|
break
|
|
else:
|
|
prompt.versions.append(version)
|
|
|
|
# 保存版本内容到独立文件
|
|
content_path = VERSIONS_DIR / f"{prompt_id}-{version.version}.md"
|
|
content_path.write_text(version.content, encoding="utf-8")
|
|
|
|
# 更新 content_path 只存路径,content 字段只存摘要
|
|
version.content_path = str(content_path)
|
|
version.content = version.content[:200] + "..." if len(version.content) > 200 else version.content
|
|
|
|
# 设置当前版本
|
|
prompt.current_version = version.version
|
|
prompt.updated_at = datetime.now().isoformat()
|
|
|
|
# 保存
|
|
add_prompt(prompt)
|
|
|
|
|
|
def set_active_version(prompt_id: str, version_str: str):
|
|
"""切换当前活跃版本"""
|
|
prompt = get_prompt(prompt_id)
|
|
if not prompt:
|
|
raise ValueError(f"提示词 '{prompt_id}' 不存在")
|
|
|
|
# 验证版本存在
|
|
version_exists = any(v.version == version_str for v in prompt.versions)
|
|
if not version_exists:
|
|
raise ValueError(f"版本 '{version_str}' 不存在于 '{prompt_id}'")
|
|
|
|
# 将所有版本标记为非活跃,目标版本标记为活跃
|
|
for v in prompt.versions:
|
|
if v.version == version_str:
|
|
v.status = "active"
|
|
elif v.status == "active":
|
|
v.status = "deprecated"
|
|
|
|
prompt.current_version = version_str
|
|
prompt.updated_at = datetime.now().isoformat()
|
|
add_prompt(prompt)
|
|
|
|
|
|
# ═══════════════════════════════════════════
|
|
# 分类信息
|
|
# ═══════════════════════════════════════════
|
|
|
|
def get_categories() -> dict:
|
|
"""获取所有分类"""
|
|
return dict(PROMPT_CATEGORIES)
|
|
|
|
|
|
def get_version_history(prompt_id: str) -> list:
|
|
"""获取某个提示词的所有版本历史"""
|
|
prompt = get_prompt(prompt_id)
|
|
if not prompt:
|
|
return []
|
|
history = []
|
|
for v in prompt.versions:
|
|
history.append({
|
|
"version": v.version,
|
|
"label": v.label,
|
|
"status": v.status,
|
|
"created_at": v.created_at,
|
|
"changelog": v.changelog,
|
|
"tags": v.tags,
|
|
"is_current": v.version == prompt.current_version,
|
|
})
|
|
return sorted(history, key=lambda x: x["version"])
|