minimind/app_web.py

130 lines
6.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

"""Streamlit UI for inspecting sources and asking evidence-grounded TDoc questions."""
import json
import streamlit as st
from agent_core import ProposalAgent
st.set_page_config(page_title="3GPP 提案阅读助手", page_icon="📡", layout="wide")
@st.cache_resource(show_spinner="正在建立本地提案检索索引……")
def load_agent():
return ProposalAgent()
def show_sources(sources):
"""Keep source excerpts visible independently of model loading or generation errors."""
for warning in sources.get("warnings", []):
st.warning(warning)
if sources.get("documents"):
st.dataframe([
{"提案": item["tdoc"], "标题": item.get("title", ""), "原文状态": item["status"]}
for item in sources["documents"]
], hide_index=True, width="stretch")
for item in sources.get("evidence", []):
with st.expander(f"[{item['source_id']}] {item['tdoc']} · 段落 {item['paragraph_start']}–{item['paragraph_end']}"):
st.write(item["title"])
st.caption(f"{item['meeting']} | {item['company']}")
st.text(item["text"])
st.caption(item["file_path"])
st.title("📡 3GPP 提案阅读助手")
st.caption("查找本地提案原文,查看依据,再生成带来源编号的回答。提案主张不等于已通过的标准。")
with st.sidebar:
st.subheader("使用方式")
retrieval_only = st.checkbox("只查原文,不调用模型", value=True)
use_lora = st.checkbox("使用现有 LoRA 适配器", value=False, help="默认使用原模型。本轮少量实测中,现有适配器出现了引用缺失和无依据的流程描述;其整体效果尚未确认。")
max_tokens = st.slider("最长生成 token 数", 128, 1536, 640, step=128)
temperature = st.slider("采样温度", 0.0, 0.7, 0.0, step=0.1)
st.caption("设为 0 可减少采样变化;不能保证事实正确。首次生成会加载本地模型。")
st.divider()
st.subheader("示例问题")
examples = {
"XR 速率控制": "请根据 R2-2408133 原文,说明提案要解决的问题、主要主张及尚未明确的部分。",
"比较两篇 XR 提案": "请对比 R2-2408133 和 R2-2408134,分别说明目标和主要主张,再解释两篇提案是否解决同一个问题。",
"NTN 设计要求": "请根据 R2-2508703 原文,说明 NTN 对 TN/NTN 共同设计提出了哪些要求。区分提案主张与已经达成的结论。",
"按标题找资料": "XR rate control signaling",
}
for label, example in examples.items():
if st.button(label, use_container_width=True):
st.session_state["question"] = example
if st.button("清除结果", use_container_width=True):
st.session_state.pop("last_run", None)
st.session_state.pop("last_error", None)
st.session_state.pop("last_sources", None)
st.caption("支持编号检索、标题关键词排序和 DOCX 原文读取。旧版 .doc 暂不读取;缺少原文时不生成内容分析。")
question = st.text_area("问题或提案编号", key="question", height=130, placeholder="例如:对比 R2-2408133 和 R2-2408134 的目标与主张。")
extra = st.text_area("补充材料(选填,会标记为未经核实)", height=80)
if st.button("检索原文" if retrieval_only else "检索并分析", type="primary"):
st.session_state.pop("last_run", None)
st.session_state.pop("last_error", None)
st.session_state.pop("last_sources", None)
if not question.strip():
st.warning("请输入问题或提案编号。")
else:
status = st.status("正在处理……", expanded=True)
with st.container():
source_area = st.container()
output = st.empty()
completed = False
try:
agent = load_agent()
for event in agent.run_stream(
question, extra_context=extra, max_new_tokens=max_tokens,
temperature=temperature, retrieval_only=retrieval_only, use_lora=use_lora,
):
stage = event["stage"]
if stage in {"1. RETRIEVAL", "1. RETRIEVAL_DONE", "2. SYNTHESIS_START"}:
status.write(event["msg"])
if stage == "1. RETRIEVAL_DONE":
st.session_state["last_sources"] = event["retrieval"]
with source_area:
show_sources(event["retrieval"])
elif stage == "2. STREAMING":
output.markdown(event["full_text"] + " ▌")
elif stage == "4. COMPLETE":
st.session_state["last_run"] = {**event, "question": question, "use_lora": use_lora}
completed = True
elif stage == "ERROR":
st.session_state["last_error"] = event["msg"]
except Exception as exc:
st.session_state["last_error"] = f"初始化失败:{exc}"
output.empty()
status.update(label="处理完成" if completed else "处理失败", state="complete" if completed else "error", expanded=False)
if st.session_state.get("last_error"):
st.warning("生成未完成。上方原文仍可查看;不完整回答未保存为结果。")
if st.session_state.get("last_error"):
st.error(st.session_state["last_error"])
if st.session_state.get("last_sources"):
with st.expander("查看本次已找到的原文"):
show_sources(st.session_state["last_sources"])
if run := st.session_state.get("last_run"):
st.divider()
if run.get("mode") == "needs_review":
st.warning("以下为待核对草稿:来源编号或提案引用不完整。")
st.caption("对应问题:" + run.get("question", ""))
st.markdown(run["result"])
with st.expander("查看所有来源及原文"):
show_sources(run["sources"])
for warning in run.get("validation", {}).get("warnings", []):
st.warning(warning)
metrics = run.get("metrics", {})
labels = {"retrieval_seconds": "检索", "first_text_seconds": "首段文本", "total_seconds": "总耗时"}
st.caption(" | ".join(f"{labels.get(name, name)}: {value:.2f} s" for name, value in metrics.items() if value is not None))
st.caption("首段文本时间包含检索和首次模型加载,不等于纯模型首 token 延迟。")
report = run["result"] + "\n\n---\n\n来源记录:\n\n" + "\n\n".join(
f"[{item['source_id']}] {item['tdoc']} | {item['title']}\n"
f"{item['file_path']}(段落 {item['paragraph_start']}–{item['paragraph_end']})\n{item['text']}"
for item in run["sources"].get("evidence", [])
)
st.download_button("下载回答与来源 Markdown", report, "3gpp_analysis.md", "text/markdown")
st.download_button("下载完整运行记录 JSON", json.dumps(run, ensure_ascii=False, indent=2), "3gpp_run.json", "application/json")