feat: v3 robustness upgrade — EasyOCR, failure recovery, minimum content check
- OCR: EasyOCR (primary, ch_sim+en) with PaddleOCR fallback for Windows compatibility - Validation: _check_minimum_content() rejects empty-shell JRXML (no band/textField) - Retry: MAX_RETRY 3→5, exhaustion records pending_failure_context for next-turn auto-injection - Finalize: only saves jrxml_versions on pass, preserves last good final_jrxml on fail - Extract JRXML: improved empty markdown block handling and XML fragment fallback - UI: real-time node progress via placeholder updates, initial "analyzing" feedback - UI: use agent_state (full) instead of node_state (partial) for summary card routing - UI: unknown template_type now gives LLM meaningful image context instead of metadata - Docs: updated CLAUDE.md and CODE_GUIDE.md to reflect all v3 changes Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -228,4 +228,5 @@ def create_initial_state() -> AgentState:
|
||||
history_states=[],
|
||||
jrxml_versions=[],
|
||||
last_error_case={},
|
||||
pending_failure_context={},
|
||||
)
|
||||
|
||||
+68
-20
@@ -27,14 +27,25 @@ HISTORY_MAX_SNAPSHOTS = int(os.getenv("HISTORY_MAX_SNAPSHOTS", "10"))
|
||||
# ============================================================
|
||||
|
||||
def process_input(state: AgentState) -> Dict:
|
||||
"""记录用户输入到对话历史,重置本轮请求状态。"""
|
||||
"""记录用户输入到对话历史,重置本轮请求状态。如有上次失败上下文则自动注入。"""
|
||||
user_input = state.get("user_input", "")
|
||||
|
||||
# 维护全量对话历史
|
||||
# 维护全量对话历史(始终记录原始用户消息)
|
||||
full_history = state.get("full_conversation_history", [])
|
||||
full_history.append({"role": "user", "content": user_input, "ts": _now_iso()})
|
||||
state["full_conversation_history"] = full_history
|
||||
|
||||
# 自动注入上次失败上下文
|
||||
pending = state.get("pending_failure_context", {})
|
||||
if pending and pending.get("error_msg"):
|
||||
failure_note = (
|
||||
f"[系统提示] 上次生成失败,以下是失败详情,请基于此修正:\n"
|
||||
f"失败原因: {pending['error_msg']}\n"
|
||||
f"上次失败的输出:\n{pending.get('bad_jrxml', '(无输出)')}"
|
||||
)
|
||||
user_input = f"{failure_note}\n\n---\n用户新输入:\n{user_input}"
|
||||
state["pending_failure_context"] = {}
|
||||
|
||||
# 维护工作对话历史
|
||||
conv_history = state.get("conversation_history", [])
|
||||
conv_history.append({"role": "user", "content": user_input})
|
||||
@@ -402,6 +413,12 @@ def validate(state: AgentState) -> Dict:
|
||||
state["error_msg"] = "没有 JRXML 内容可供验证。"
|
||||
return state
|
||||
|
||||
# 过短的内容不可能是合法报表(最小骨架约 500+ 字符)
|
||||
if len(jrxml.strip()) < 200:
|
||||
state["status"] = "fail"
|
||||
state["error_msg"] = f"JRXML 内容过短({len(jrxml.strip())} 字符),可能为不完整或空内容。"
|
||||
return state
|
||||
|
||||
result = validate_jrxml(jrxml)
|
||||
state["status"] = "pass" if result.get("valid") else "fail"
|
||||
state["error_msg"] = result.get("error", "")
|
||||
@@ -481,26 +498,47 @@ def correct_jrxml(state: AgentState) -> Dict:
|
||||
def finalize(state: AgentState) -> Dict:
|
||||
"""保存最终验证通过的 JRXML 并更新对话历史 + 版本记录。"""
|
||||
jrxml = state.get("current_jrxml", "")
|
||||
state["final_jrxml"] = jrxml
|
||||
status = state.get("status", "")
|
||||
|
||||
if jrxml.strip():
|
||||
versions = state.get("jrxml_versions", [])
|
||||
if not isinstance(versions, list):
|
||||
versions = []
|
||||
intent = state.get("intent", "")
|
||||
label_map = {
|
||||
"initial_generation": "初始生成",
|
||||
"modify_report": "修改",
|
||||
"correct_jrxml": f"自动修正 (第{state.get('retry_count', 1)}次)",
|
||||
}
|
||||
versions.append({
|
||||
if status == "pass":
|
||||
state["final_jrxml"] = jrxml
|
||||
if jrxml.strip():
|
||||
versions = state.get("jrxml_versions", [])
|
||||
if not isinstance(versions, list):
|
||||
versions = []
|
||||
intent = state.get("intent", "")
|
||||
label_map = {
|
||||
"initial_generation": "初始生成",
|
||||
"modify_report": "修改",
|
||||
"correct_jrxml": f"自动修正 (第{state.get('retry_count', 1)}次)",
|
||||
}
|
||||
versions.append({
|
||||
"ts": _now_iso(),
|
||||
"jrxml": jrxml,
|
||||
"intent": intent,
|
||||
"label": label_map.get(intent, intent),
|
||||
"status": status,
|
||||
})
|
||||
state["jrxml_versions"] = versions
|
||||
else:
|
||||
# 验证未通过:不覆盖 final_jrxml,保留上一次成功的版本
|
||||
retries = state.get("retry_count", 0)
|
||||
error_msg = state.get("error_msg", "未知错误")
|
||||
# 记录失败上下文,下次用户输入时自动注入
|
||||
state["pending_failure_context"] = {
|
||||
"error_msg": error_msg,
|
||||
"bad_jrxml": state.get("current_jrxml", ""),
|
||||
"retry_count": retries,
|
||||
"ts": _now_iso(),
|
||||
"jrxml": jrxml,
|
||||
"intent": intent,
|
||||
"label": label_map.get(intent, intent),
|
||||
"status": state.get("status", ""),
|
||||
}
|
||||
state["conversation_history"].append({
|
||||
"role": "assistant",
|
||||
"content": (
|
||||
f"❌ 经过 {retries} 次重试后仍无法生成有效的 JRXML。\n"
|
||||
f"错误: {error_msg}\n"
|
||||
f"请描述您想要的修改,系统会自动加载失败上下文继续修复。"
|
||||
),
|
||||
})
|
||||
state["jrxml_versions"] = versions
|
||||
return state
|
||||
|
||||
|
||||
@@ -510,7 +548,10 @@ def _extract_jrxml(text: str) -> str:
|
||||
xml_pattern = re.compile(r"```(?:xml|jrxml)?\s*([\s\S]*?)```", re.IGNORECASE)
|
||||
m = xml_pattern.search(text)
|
||||
if m:
|
||||
return m.group(1).strip()
|
||||
content = m.group(1).strip()
|
||||
if content:
|
||||
return content
|
||||
# markdown 代码块存在但内容为空 — 回退到直接匹配
|
||||
|
||||
jasper_tag = re.search(r"(<\?xml[\s\S]*?</jasperReport>)", text, re.IGNORECASE)
|
||||
if jasper_tag:
|
||||
@@ -519,4 +560,11 @@ def _extract_jrxml(text: str) -> str:
|
||||
if text.startswith("<?xml") or text.startswith("<jasperReport"):
|
||||
return text
|
||||
|
||||
# 最终回退:如果文本中包含 XML 片段但没有被捕获到,尝试直接提取
|
||||
# 这处理 LLM 在代码块外用自然语言"包裹"JRXML 的情况
|
||||
xml_start = text.find("<?xml")
|
||||
jr_end = text.lower().rfind("</jasperreport>")
|
||||
if xml_start >= 0 and jr_end > xml_start:
|
||||
return text[xml_start:jr_end + len("</jasperreport>")].strip()
|
||||
|
||||
return text
|
||||
|
||||
@@ -37,3 +37,6 @@ class AgentState(TypedDict, total=False):
|
||||
|
||||
# 需求5:错误自增长(记录修正前的状态,供 validate 节点判断是否入知识库)
|
||||
last_error_case: dict
|
||||
|
||||
# 需求6:失败上下文传递 — 重试耗尽后暂存失败信息,下次用户输入时自动注入
|
||||
pending_failure_context: dict
|
||||
|
||||
Reference in New Issue
Block a user