马一丁

Fixed the Error "AttributeError: 'list' object has no attribute 'get'"

... ... @@ -39,6 +39,10 @@ from .state import ReportState
from .utils.config import settings, Settings
class StageOutputFormatError(ValueError):
"""阶段性输出结构不符合预期时抛出的受控异常。"""
class FileCountBaseline:
"""
文件数量基准管理器。
... ... @@ -177,6 +181,7 @@ class ReportAgent:
"""
_CONTENT_SPARSE_MIN_ATTEMPTS = 3
_CONTENT_SPARSE_WARNING_TEXT = "本章LLM生成的内容字数可能过低,必要时可以尝试重新运行程序。"
_STRUCTURAL_RETRY_ATTEMPTS = 2
def __init__(self, config: Optional[Settings] = None):
"""
... ... @@ -421,6 +426,11 @@ class ReportAgent:
try:
template_result = self._select_template(query, reports, forum_logs, custom_template)
template_result = self._ensure_mapping(
template_result,
"模板选择结果",
expected_keys=["template_name", "template_content"],
)
self.state.metadata.template_used = template_result.get('template_name', '')
emit('stage', {
'stage': 'template_selected',
... ... @@ -436,13 +446,17 @@ class ReportAgent:
template_text = template_result.get('template_content', '')
template_overview = self._build_template_overview(template_text, sections)
# 基于模板骨架+三引擎内容设计全局标题、目录与视觉主题
layout_design = self.document_layout_node.run(
layout_design = self._run_stage_with_retry(
"文档设计",
lambda: self.document_layout_node.run(
sections,
template_text,
normalized_reports,
forum_logs,
query,
template_overview,
),
expected_keys=["title", "hero", "toc", "tocPlan"],
)
emit('stage', {
'stage': 'layout_designed',
... ... @@ -451,13 +465,18 @@ class ReportAgent:
})
emit('progress', {'progress': 15, 'message': '文档标题/目录设计完成'})
# 使用刚生成的设计稿对全书进行篇幅规划,约束各章字数与重点
word_plan = self.word_budget_node.run(
word_plan = self._run_stage_with_retry(
"章节篇幅规划",
lambda: self.word_budget_node.run(
sections,
layout_design,
normalized_reports,
forum_logs,
query,
template_overview,
),
expected_keys=["chapters", "totalWords", "globalGuidelines"],
postprocess=self._normalize_word_plan,
)
emit('stage', {
'stage': 'word_plan_ready',
... ... @@ -871,6 +890,134 @@ class ReportAgent:
]
return any(keyword in normalized for keyword in keywords)
def _run_stage_with_retry(
self,
stage_name: str,
fn: Callable[[], Any],
expected_keys: Optional[List[str]] = None,
postprocess: Optional[Callable[[Dict[str, Any], str], Dict[str, Any]]] = None,
) -> Dict[str, Any]:
"""
运行单个LLM阶段并在结构异常时有限次重试。
该方法只针对结构类错误做本地修复/重试,避免整个Agent重启。
"""
last_error: Optional[Exception] = None
for attempt in range(1, self._STRUCTURAL_RETRY_ATTEMPTS + 1):
try:
raw_result = fn()
result = self._ensure_mapping(raw_result, stage_name, expected_keys)
if postprocess:
result = postprocess(result, stage_name)
return result
except StageOutputFormatError as exc:
last_error = exc
logger.warning(
"{stage} 输出结构异常(第 {attempt}/{total} 次),将尝试修复或重试: {error}",
stage=stage_name,
attempt=attempt,
total=self._STRUCTURAL_RETRY_ATTEMPTS,
error=exc,
)
if attempt >= self._STRUCTURAL_RETRY_ATTEMPTS:
break
raise last_error # type: ignore[misc]
def _ensure_mapping(
self,
value: Any,
context: str,
expected_keys: Optional[List[str]] = None,
) -> Dict[str, Any]:
"""
确保阶段输出为dict;若返回列表则尝试提取最佳匹配元素。
"""
if isinstance(value, dict):
return value
if isinstance(value, list):
candidates = [item for item in value if isinstance(item, dict)]
if candidates:
best = candidates[0]
if expected_keys:
candidates.sort(
key=lambda item: sum(1 for key in expected_keys if key in item),
reverse=True,
)
best = candidates[0]
logger.warning(
"{context} 返回列表,已自动提取包含最多预期键的元素继续执行",
context=context,
)
return best
raise StageOutputFormatError(f"{context} 返回列表但缺少可用的对象元素")
if value is None:
raise StageOutputFormatError(f"{context} 返回空结果")
raise StageOutputFormatError(
f"{context} 返回类型 {type(value).__name__},期望字典"
)
def _normalize_word_plan(self, word_plan: Dict[str, Any], stage_name: str) -> Dict[str, Any]:
"""
清洗篇幅规划结果,确保 chapters/globalGuidelines/totalWords 类型安全。
"""
raw_chapters = word_plan.get("chapters", [])
if isinstance(raw_chapters, dict):
chapters_iterable = raw_chapters.values()
elif isinstance(raw_chapters, list):
chapters_iterable = raw_chapters
else:
chapters_iterable = []
normalized: List[Dict[str, Any]] = []
for idx, entry in enumerate(chapters_iterable):
if isinstance(entry, dict):
normalized.append(entry)
continue
if isinstance(entry, list):
dict_candidate = next((item for item in entry if isinstance(item, dict)), None)
if dict_candidate:
logger.warning(
"{stage} 第 {idx} 个章节条目为列表,已提取首个对象用于后续流程",
stage=stage_name,
idx=idx + 1,
)
normalized.append(dict_candidate)
continue
logger.warning(
"{stage} 跳过无法解析的章节条目#{idx}(类型: {type_name})",
stage=stage_name,
idx=idx + 1,
type_name=type(entry).__name__,
)
if not normalized:
raise StageOutputFormatError(f"{stage_name} 缺少有效的章节规划,无法继续")
word_plan["chapters"] = normalized
guidelines = word_plan.get("globalGuidelines")
if not isinstance(guidelines, list):
if guidelines is None or guidelines == "":
word_plan["globalGuidelines"] = []
else:
logger.warning(
"{stage} globalGuidelines 类型异常,已转换为列表封装",
stage=stage_name,
)
word_plan["globalGuidelines"] = [guidelines]
if not isinstance(word_plan.get("totalWords"), (int, float)):
logger.warning(
"{stage} totalWords 类型异常,使用默认值 10000",
stage=stage_name,
)
word_plan["totalWords"] = 10000
return word_plan
def _finalize_sparse_chapter(self, chapter: Optional[Dict[str, Any]]) -> Dict[str, Any]:
"""
构造内容稀疏兜底章节:复制原始payload并插入温馨提示段落。
... ...
... ... @@ -584,6 +584,9 @@ def generate_report():
# 获取请求参数
data = request.get_json() or {}
if not isinstance(data, dict):
logger.warning("generate_report 接收到非对象JSON负载,已忽略原始内容")
data = {}
query = data.get('query', '智能舆情分析报告')
custom_template = data.get('custom_template', '')
... ... @@ -1285,7 +1288,13 @@ def export_pdf_from_ir():
'system_message': pango_message
}), 503
data = request.get_json()
data = request.get_json() or {}
if not isinstance(data, dict):
logger.warning("export_pdf_from_ir 请求体不是JSON对象")
return jsonify({
'success': False,
'error': '请求体必须是JSON对象'
}), 400
if not data or 'document_ir' not in data:
return jsonify({
... ...