Files
agent_jrxml/app.py
T
panda e113374682 feat: Streamlit多轮对话界面 + 集成测试
app.py:
  侧边栏:会话管理(创建/切换/删除)、快捷操作(预览/撤销/重置)、
         配置信息、JRXML下载
  主区域:多轮聊天、8种意图差异化展示(JRXML代码/咨询回答/
          错误解释/成功提示)
  URL参数:?session_id= 会话分享

tests/:
  test_validation.py: 验证服务6个单元测试(健康检查/空内容/
                      无效XML/缺少尺寸/有效JRXML/字段引用)
  test_agent.py: 5个集成验收场景(简单生成/自动修正/
                  多轮修改/上下文感知修改/最大重试处理)
2026-05-14 23:21:22 +08:00

319 lines
13 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.
"""Streamlit 多轮对话 UI,用于 JRXML 生成代理。"""
import os
import sys
import streamlit as st
from dotenv import load_dotenv
load_dotenv()
from agent.graph import build_graph, create_initial_state
from backend.session import (
create_session,
load_session,
delete_session,
list_all_sessions,
)
st.set_page_config(
page_title="JRXML 代理",
page_icon="📊",
layout="wide",
initial_sidebar_state="expanded",
)
# ---- URL 参数:session_id ----
query_params = st.query_params
url_session_id = query_params.get("session_id", "")
# ---- 会话状态初始化 ----
if "messages" not in st.session_state:
st.session_state.messages = []
if "graph" not in st.session_state:
st.session_state.graph = build_graph()
if "pending_action" not in st.session_state:
st.session_state.pending_action = None
# 确定活跃的 session_id
if "agent_state" not in st.session_state:
if url_session_id:
data = load_session(url_session_id)
if data and data.get("agent_state"):
st.session_state.agent_state = data["agent_state"]
st.session_state.agent_state["session_id"] = url_session_id
else:
st.session_state.agent_state = create_initial_state()
new_data = create_session(name="", agent_state=st.session_state.agent_state)
st.session_state.agent_state["session_id"] = new_data["session_id"]
st.session_state.agent_state["session_name"] = new_data["session_name"]
st.session_state.agent_state["created_at"] = new_data["created_at"]
else:
st.session_state.agent_state = create_initial_state()
new_data = create_session(name="", agent_state=st.session_state.agent_state)
st.session_state.agent_state["session_id"] = new_data["session_id"]
st.session_state.agent_state["session_name"] = new_data["session_name"]
st.session_state.agent_state["created_at"] = new_data["created_at"]
current_session_id = st.session_state.agent_state.get("session_id", "")
def run_agent(user_input: str) -> dict:
"""运行代理图,返回最终状态。"""
agent_state = st.session_state.agent_state
if agent_state.get("current_jrxml") and agent_state.get("status") == "pass":
agent_state["user_modification_request"] = user_input
agent_state["user_input"] = user_input
agent_state["retry_count"] = 0
final_state = None
with st.chat_message("assistant"):
status_placeholder = st.empty()
jrxml_placeholder = st.empty()
for event in st.session_state.graph.stream(agent_state):
for node_name, node_state in event.items():
final_state = node_state
if node_name == "classify_intent":
intent = node_state.get("intent", "")
intent_labels = {
"initial_generation": "🆕 识别为新建报表请求",
"modify_report": "✏️ 识别为修改报表请求",
"preview_report": "👁 识别为预览请求",
"export_pdf": "📄 识别为导出PDF请求",
"export_jrxml": "📥 识别为导出JRXML请求",
"undo_modification": "↩ 识别为撤销请求",
"consult_question": "💬 识别为咨询问题",
"reset_session": "🔄 识别为重置会话请求",
}
label = intent_labels.get(intent, f"🔍 意图: {intent}")
status_placeholder.info(label)
elif node_name == "generate":
status_placeholder.info("🔧 正在生成 JRXML...")
elif node_name == "modify_jrxml":
status_placeholder.info("🔧 正在根据您的请求修改 JRXML...")
elif node_name == "validate":
if node_state.get("status") == "pass":
status_placeholder.success("✅ 验证通过!")
else:
status_placeholder.warning("⚠ 验证失败,正在分析错误...")
elif node_name == "explain_error":
explanation = node_state.get("natural_explanation", "")
status_placeholder.warning(f"🔍 {explanation}")
elif node_name == "correct_jrxml":
status_placeholder.info(f"🛠 正在自动修正(尝试 {node_state.get('retry_count', 1)}...")
elif node_name == "handle_consult":
pass
elif node_name == "handle_undo":
status_placeholder.info("↩ 已撤销上一步修改")
elif node_name == "handle_reset":
status_placeholder.info("🔄 会话已重置")
elif node_name == "manage_context":
pass
elif node_name == "save_state_snapshot":
pass
elif node_name == "save_session":
pass
elif node_name == "finalize":
pass
if final_state:
st.session_state.agent_state = final_state
intent = final_state.get("intent", "")
if intent == "consult_question":
answer = final_state.get("consult_answer", "")
st.session_state.messages.append({
"role": "assistant",
"content": answer,
"type": "consult",
})
status_placeholder.empty()
st.markdown(answer)
elif intent in ("undo_modification", "reset_session"):
# 消息已在节点中添加,不需要额外输出
status_placeholder.empty()
jrxml_placeholder.empty()
elif intent in ("preview_report", "export_pdf", "export_jrxml"):
jrxml = final_state.get("current_jrxml", "")
if jrxml:
st.session_state.messages.append({
"role": "assistant",
"content": jrxml,
"type": "jrxml",
})
status_placeholder.success("✅ 当前报表")
jrxml_placeholder.code(jrxml, language="xml")
else:
status_placeholder.warning("⚠ 当前没有报表可以预览或导出。")
jrxml_placeholder.empty()
elif final_state.get("status") == "pass":
st.session_state.messages.append({
"role": "assistant",
"content": final_state.get("current_jrxml", ""),
"type": "jrxml",
})
st.session_state.messages.append({
"role": "assistant",
"content": "✅ JRXML 生成成功!您可以从侧边栏下载文件,或继续修改。",
"type": "success",
})
status_placeholder.success("✅ JRXML 验证通过!")
jrxml_placeholder.code(final_state.get("current_jrxml", ""), language="xml")
else:
error_msg = final_state.get("error_msg", "未知错误")
explanation = final_state.get("natural_explanation", "")
retries = final_state.get("retry_count", 0)
st.session_state.messages.append({
"role": "assistant",
"content": final_state.get("current_jrxml", ""),
"type": "jrxml",
})
st.session_state.messages.append({
"role": "assistant",
"content": f"❌ 经过 {retries} 次重试后仍无法生成有效的 JRXML。\n\n**错误:** {error_msg}\n\n**解释:** {explanation}\n\n请重新描述您的需求或简化报表结构。",
"type": "error_explanation",
})
status_placeholder.error(f"❌ 经过 {retries} 次重试后验证失败")
jrxml_placeholder.text("")
else:
st.error("未产生结果,请重试。")
return final_state
# ---- 侧边栏 ----
with st.sidebar:
st.title("📊 JRXML 代理")
st.markdown("通过自然语言生成 JasperReports 模板。")
st.divider()
# 会话管理
st.markdown("### 会话管理")
sessions = list_all_sessions()
session_options = {}
for s in sessions:
sid = s["session_id"]
name = s.get("session_name", sid)
updated = s.get("updated_at", "")[:16]
session_options[f"{name} ({updated})"] = sid
selected_label = None
for label, sid in session_options.items():
if sid == current_session_id:
selected_label = label
break
selected = st.selectbox(
"切换会话",
options=list(session_options.keys()),
index=list(session_options.keys()).index(selected_label) if selected_label else 0,
key="session_selector",
)
if selected and session_options.get(selected) != current_session_id:
new_sid = session_options[selected]
data = load_session(new_sid)
if data and data.get("agent_state"):
st.session_state.agent_state = data["agent_state"]
st.session_state.messages = []
st.rerun()
col1, col2 = st.columns(2)
with col1:
if st.button(" 新建", use_container_width=True):
new_data = create_session(name="", agent_state=create_initial_state())
st.session_state.agent_state = create_initial_state()
st.session_state.agent_state["session_id"] = new_data["session_id"]
st.session_state.agent_state["session_name"] = new_data["session_name"]
st.session_state.agent_state["created_at"] = new_data["created_at"]
st.session_state.messages = []
st.rerun()
with col2:
if st.button("🗑 删除", use_container_width=True):
if current_session_id:
delete_session(current_session_id)
st.session_state.agent_state = create_initial_state()
new_data = create_session(name="", agent_state=st.session_state.agent_state)
st.session_state.agent_state["session_id"] = new_data["session_id"]
st.session_state.agent_state["session_name"] = new_data["session_name"]
st.session_state.agent_state["created_at"] = new_data["created_at"]
st.session_state.messages = []
st.rerun()
current_name = st.session_state.agent_state.get("session_name", "")
st.caption(f"当前: {current_name} (`{current_session_id}`)")
st.divider()
st.markdown("### 快捷操作")
has_jrxml = bool(st.session_state.agent_state.get("current_jrxml", "").strip())
has_history = bool(st.session_state.agent_state.get("history_states", []))
qcol1, qcol2 = st.columns(2)
with qcol1:
if st.button("👁 预览", use_container_width=True, disabled=not has_jrxml):
with st.spinner("正在准备预览..."):
run_agent("预览报表")
st.rerun()
with qcol2:
if st.button("↩ 撤销", use_container_width=True, disabled=not has_history):
with st.spinner("正在撤销..."):
run_agent("撤销上一步修改")
st.rerun()
if st.button("🔄 重置会话", use_container_width=True):
with st.spinner("正在重置..."):
run_agent("重新来,清空当前报表")
st.rerun()
st.divider()
st.markdown("### 配置")
llm_backend = os.getenv("LLM_BACKEND", "cloud")
llm_model = os.getenv("LLM_MODEL", os.getenv("LOCAL_LLM_MODEL", "gpt-4o"))
st.caption(f"大语言模型: {llm_backend} / {llm_model}")
st.caption(f"最大重试次数: {os.getenv('MAX_RETRY', '3')}")
st.caption(f"验证服务: {os.getenv('VALIDATION_SERVICE_URL', 'http://localhost:8001/validate')}")
st.divider()
st.markdown("### 下载")
final = st.session_state.agent_state.get("final_jrxml", "")
if final:
st.download_button(
label="📥 下载 JRXML",
data=final,
file_name="report.jrxml",
mime="application/xml",
use_container_width=True,
)
# ---- 标题 ----
st.title("📝 JRXML 报表生成器")
st.caption("用自然语言描述您的报表需求,我将逐步生成可用的 JRXML 模板。")
# ---- 聊天历史 ----
for msg in st.session_state.messages:
with st.chat_message(msg["role"]):
if msg["role"] == "assistant" and msg.get("type") == "jrxml":
with st.expander("查看生成的 JRXML", expanded=False):
st.code(msg["content"], language="xml")
elif msg["role"] == "assistant" and msg.get("type") == "error_explanation":
st.warning(msg["content"])
elif msg["role"] == "assistant" and msg.get("type") == "success":
st.success(msg["content"])
elif msg["role"] == "assistant" and msg.get("type") == "consult":
st.info(msg["content"])
else:
st.markdown(msg["content"])
# ---- 聊天输入 ----
if prompt := st.chat_input("描述您的报表需求..."):
st.session_state.messages.append({"role": "user", "content": prompt})
with st.chat_message("user"):
st.markdown(prompt)
run_agent(prompt)
st.rerun()