mirror of
https://ghfast.top/https://github.com/aeroxw/tick-stock-panel.git
synced 2026-09-12 16:44:15 +08:00
Merge pull request #109 from shy3130/fix/ext-concepts-cast-and-meta-normalize
fix: 扩展数据 List cast 报错 + AI 策略 META 规范化失败
This commit is contained in:
@@ -313,14 +313,24 @@ def _py_string(value: str) -> str:
|
||||
|
||||
|
||||
def _find_meta_dict(code: str) -> ast.Dict:
|
||||
# 兼容两种 LLM 常见写法:
|
||||
# META = {...} → ast.Assign
|
||||
# META: dict = {...} → ast.AnnAssign (类型注解, 合法但旧逻辑漏匹配)
|
||||
tree = ast.parse(code)
|
||||
for node in ast.walk(tree):
|
||||
value = None
|
||||
if isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name) and target.id == "META":
|
||||
if not isinstance(node.value, ast.Dict):
|
||||
raise ValueError("META 必须是字面量字典")
|
||||
return node.value
|
||||
value = node.value
|
||||
break
|
||||
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) \
|
||||
and node.target.id == "META":
|
||||
value = node.value
|
||||
if value is not None:
|
||||
if not isinstance(value, ast.Dict):
|
||||
raise ValueError("META 必须是字面量字典")
|
||||
return value
|
||||
raise ValueError("找不到 META 字典")
|
||||
|
||||
|
||||
|
||||
@@ -451,10 +451,23 @@ def parse_upload_file(file_path: Path, symbol_col: str = "symbol", data_dir: Pat
|
||||
|
||||
|
||||
def cast_df_to_schema(df: pl.DataFrame, fields: list[ExtField]) -> pl.DataFrame:
|
||||
"""按配置的字段类型转换 DataFrame 列类型。"""
|
||||
"""按配置的字段类型转换 DataFrame 列类型。
|
||||
|
||||
List → string 的处理: 上游接口常返回数组字段 (如 concepts: ["AI", "芯片"]),
|
||||
若声明为 string, 直接 cast 会抛 `cannot cast List type`。
|
||||
这里把列表元素先转字符串再以分号拼接, 与 _flatten_concept_rows 保持一致。
|
||||
"""
|
||||
schema = df.schema
|
||||
for f in fields:
|
||||
if f.name in df.columns:
|
||||
target = _POLARS_DTYPE_MAP.get(f.dtype, pl.Utf8)
|
||||
if f.name not in df.columns:
|
||||
continue
|
||||
target = _POLARS_DTYPE_MAP.get(f.dtype, pl.Utf8)
|
||||
src = schema[f.name]
|
||||
if isinstance(src, pl.List) and target == pl.Utf8:
|
||||
df = df.with_columns(
|
||||
pl.col(f.name).cast(pl.List(pl.Utf8)).list.join(";").cast(target)
|
||||
)
|
||||
else:
|
||||
df = df.with_columns(pl.col(f.name).cast(target))
|
||||
return df
|
||||
|
||||
|
||||
@@ -144,14 +144,25 @@ class AIStrategyGenerator:
|
||||
|
||||
@staticmethod
|
||||
def _extract_meta(code: str) -> dict:
|
||||
"""从代码字符串中提取 META 字典(不执行代码, 仅接受字面量)"""
|
||||
"""从代码字符串中提取 META 字典(不执行代码, 仅接受字面量)
|
||||
|
||||
兼容两种声明: META = {...} (Assign) 和 META: dict = {...} (AnnAssign)。
|
||||
与 api.strategy._find_meta_dict 保持同一套匹配逻辑。
|
||||
"""
|
||||
tree = ast.parse(code)
|
||||
for node in ast.walk(tree):
|
||||
value = None
|
||||
if isinstance(node, ast.Assign):
|
||||
for target in node.targets:
|
||||
if isinstance(target, ast.Name) and target.id == "META":
|
||||
try:
|
||||
return ast.literal_eval(node.value)
|
||||
except (ValueError, SyntaxError) as e:
|
||||
raise ValueError(f"META 必须是纯字面量字典: {e}") from e
|
||||
value = node.value
|
||||
break
|
||||
elif isinstance(node, ast.AnnAssign) and isinstance(node.target, ast.Name) \
|
||||
and node.target.id == "META":
|
||||
value = node.value
|
||||
if value is not None:
|
||||
try:
|
||||
return ast.literal_eval(value)
|
||||
except (ValueError, SyntaxError) as e:
|
||||
raise ValueError(f"META 必须是纯字面量字典: {e}") from e
|
||||
return {}
|
||||
|
||||
@@ -76,3 +76,65 @@ def filter(df: pl.DataFrame, params: dict) -> pl.Expr:
|
||||
assert '"id": "ai_inserted"' in code
|
||||
assert '"name": "中文名"' in code
|
||||
assert '"description": "描述"' in code
|
||||
|
||||
|
||||
# --- 回归: LLM 偏移写法 -------------------------------------------------
|
||||
# 模型常给 META 加类型注解 (META: dict = {...}, ast.AnnAssign 节点)。
|
||||
# 旧版匹配器只遍历 ast.Assign, 漏掉注解形式 → 报「找不到 META 字典」。
|
||||
|
||||
ANNOTATED_CODE = '''"""模型返回的策略 (带类型注解的 META — LLM 常见偏移)"""
|
||||
import polars as pl
|
||||
|
||||
META: dict = {
|
||||
"id": "annotated_wrong_id",
|
||||
"name": "Placeholder",
|
||||
"description": "model desc",
|
||||
"tags": ["AI"],
|
||||
"params": [],
|
||||
"scoring": {},
|
||||
}
|
||||
|
||||
def filter(df: pl.DataFrame, params: dict) -> pl.Expr:
|
||||
return pl.lit(True)
|
||||
'''
|
||||
|
||||
|
||||
def test_find_meta_dict_accepts_type_annotated_form():
|
||||
"""META: dict = {...} 必须能被识别 (旧版会抛「找不到 META 字典」)。"""
|
||||
from app.api.strategy import _find_meta_dict
|
||||
|
||||
node = _find_meta_dict(ANNOTATED_CODE)
|
||||
assert node is not None # 能找到就说明没抛异常
|
||||
|
||||
|
||||
def test_extract_meta_accepts_type_annotated_form():
|
||||
from app.strategy.ai_generator import AIStrategyGenerator
|
||||
|
||||
meta = AIStrategyGenerator._extract_meta(ANNOTATED_CODE)
|
||||
assert meta["id"] == "annotated_wrong_id"
|
||||
assert meta["name"] == "Placeholder"
|
||||
|
||||
|
||||
def test_normalize_strategy_meta_works_on_annotated_form():
|
||||
"""端到端: AI 生成注解形式 META 时, 规范化不再报「规范化 META 失败」。"""
|
||||
code = _normalize_strategy_meta(
|
||||
ANNOTATED_CODE,
|
||||
"ai_annotated_ok",
|
||||
name="断板反包",
|
||||
description="中文描述",
|
||||
)
|
||||
assert '"id": "ai_annotated_ok"' in code
|
||||
assert '"name": "断板反包"' in code
|
||||
assert '"description": "中文描述"' in code
|
||||
assert "annotated_wrong_id" not in code
|
||||
|
||||
|
||||
def test_normalize_build_result_succeeds_on_annotated_form():
|
||||
"""模拟前端 /build/stream 的完整结果路径 (之前报错的入口)。"""
|
||||
result = {"code": ANNOTATED_CODE, "meta": {}, "valid": True, "error": None}
|
||||
|
||||
normalized = _normalize_build_result(result, "ai_build_ok")
|
||||
|
||||
assert normalized["valid"] is True
|
||||
assert normalized["error"] is None
|
||||
assert normalized["meta"]["id"] == "ai_build_ok"
|
||||
|
||||
Reference in New Issue
Block a user