Files
pod_trend_agent/graph/validate.py
T
3218485270 2a96ec0870 v88 功能增强:产品落盘持久化 + 生图网关适配 + 模板导出优化
- 产品持久化:每完成一个产品立即追加写入 products_pending.jsonl,崩溃不丢已完成产品,finish 读盘合并后统一写模板
- 503 致命错误提前终止:compose/product/seed_shot 端到端识别,提前终止搜索分析,丢弃未完成简报,保留已完成落盘产品直接合成模板
- 模特分配:material_library 合格模特图按任务序号独立随机,同 SPU 多款不再共用同一模特
- 图像网关适配:execution_mode/background 默认不再传入 yunfei 等标准网关,base_url 需带 /v1;429/5xx/空响应退避重试
- Pinterest 分析:删除 term 注入与纯文本降级,失败直接放弃;图片上传前 PIL 完整性校验;suitable_for_print=False 过滤丢弃
- 模板导出:不再产生空白 xlsx,文件名=模板原文件名_已填写;写入前按货号末 3 位升序排序
- 删除对接文档.md,更新 README,gitignore 排除测试产物
2026-08-28 10:28:35 +08:00

119 lines
4.1 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.
"""节点级兜底校验工具。
设计目标:LangGraph 流水线里每个节点都必须"失败不影响整体"。
提供两类兜底:
1. with_fallback(node_name):装饰器,节点函数抛异常时捕获,把错误写入 state['errors']
并返回最小更新(不破坏其它字段),整图继续往下走。
2. 数据校验函数:validate_rows / validate_brief,对节点产出的数据进行结构校验,
剔除非法记录并记录原因,保证下游拿到的数据"形状正确"。
"""
import functools
import threading
import traceback
from typing import Any, Dict, List
class ThreadSafeErrors:
"""线程安全的错误收集器:并发节点(compose/product 等)内 append 错误用。
避免多个 worker 线程直接写共享 list 造成竞态;主线程统一合并到 state['errors']。
"""
def __init__(self) -> None:
self._lock = threading.Lock()
self._items: List[Dict[str, Any]] = []
def append(self, item: Dict[str, Any]) -> None:
with self._lock:
self._items.append(item)
def __iter__(self):
with self._lock:
return iter(list(self._items))
def __len__(self) -> int:
with self._lock:
return len(self._items)
def with_fallback(node_name: str):
"""装饰器:捕获节点异常,转为 state['errors'] 中的一条记录,返回空更新。
节点内部仍建议自己做精细兜底(降级/默认),with_fallback 是最后一道保险:
任何未预料的异常都不会让整张图中断。
"""
def deco(fn):
@functools.wraps(fn)
def wrapper(state: Dict[str, Any]):
try:
return fn(state)
except Exception as e: # noqa: BLE001
tb = traceback.format_exc(limit=3)
err = {
"node": node_name,
"type": type(e).__name__,
"message": str(e)[:300],
"trace": tb[-400:],
}
errors = list(state.get("errors") or [])
errors.append(err)
# 只更新 errors,其它字段保持上一节点结果,下游继续
return {"errors": errors}
return wrapper
return deco
def validate_rows(rows: List[Dict[str, Any]], node: str) -> List[Dict[str, Any]]:
"""校验抓取/过滤后的行结构,剔除缺 topic 或非法记录,返回干净列表。
同时保证每个 row 至少含 country/topic/source/kind/raw_score,缺失时给默认。
"""
clean: List[Dict[str, Any]] = []
dropped = 0
for r in rows or []:
if not isinstance(r, dict):
dropped += 1
continue
topic = (r.get("topic") or "").strip()
if not topic:
dropped += 1
continue
r.setdefault("country", "")
r.setdefault("source", "unknown")
r.setdefault("kind", "unknown")
r.setdefault("raw_score", 0.0)
if r.get("raw_score") is None:
r["raw_score"] = 0.0
clean.append(r)
if dropped:
# 简单记录到返回数据的副作用里(调用方会再汇总到 stats)
pass
return clean
def validate_brief(b: Dict[str, Any]) -> Dict[str, Any]:
"""校验单条设计简报结构,补齐缺失字段,保证下游 compose 不会因缺键崩溃。"""
b = dict(b)
b.setdefault("topic", "")
b.setdefault("country", "")
b.setdefault("design_category", "Niche")
b.setdefault("risk_level", "safe")
b.setdefault("motif", b.get("topic", ""))
b.setdefault("art_style", "clean vector illustration")
b.setdefault("color_palette", "balanced modern palette")
b.setdefault("composition", "centered emblem with balanced negative space")
b.setdefault("concept", b.get("topic", ""))
b.setdefault("negative_prompt", "")
b.setdefault("image_prompt", "")
b.setdefault("wearable_prompt", "")
b.setdefault("composite_prompt", "")
b.setdefault("composite_negative", "")
return b
def safe_get(state: Dict[str, Any], key: str, default=None):
return state.get(key, default)