Files

1301 lines
65 KiB
Python
Raw Permalink 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.
import sys
import json
import time
import re
from pathlib import Path
import streamlit as st
# 将项目根目录添加到 python path
root_dir = Path(__file__).resolve().parent.parent
if str(root_dir) not in sys.path:
sys.path.insert(0, str(root_dir))
import importlib
import src.agent.engine
importlib.reload(src.agent.engine)
from src.agent.engine import AIScientistEngine
from src.llm.client import LLMClient
from src.skills.hypothesis_generator import HypothesisGeneratorSkill
from src.skills.hypothesis_critic import HypothesisCriticSkill
from src.skills.plan_writer import ResearchPlanWriterSkill
from src.utils.parser import parse_json_robust, extract_hypotheses_from_text
from src.config import Config
# 页面配置
st.set_page_config(
page_title="AI Scientist 发现引擎",
page_icon="🔬",
layout="wide",
initial_sidebar_state="expanded"
)
# 自定义 CSS 质感样式与极客暗黑终端
st.markdown("""
<style>
/* 彻底隐藏右上角 Deploy 按钮与 Streamlit 默认 Header/Footer */
.stAppDeployButton { display: none !important; }
header[data-testid="stHeader"] { visibility: hidden !important; height: 0px !important; }
#MainMenu { visibility: hidden !important; }
footer { visibility: hidden !important; }
.main-title {
font-size: 2.2rem;
font-weight: 700;
background: linear-gradient(90deg, #4A00E0 0%, #8E2DE2 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
margin-bottom: 0.5rem;
}
.sub-title {
font-size: 1.05rem;
color: #6c757d;
margin-bottom: 1.2rem;
}
.guide-banner {
background: linear-gradient(90deg, #eef2ff 0%, #f5f3ff 100%);
border-left: 5px solid #6366f1;
padding: 15px 20px;
border-radius: 8px;
margin-bottom: 20px;
color: #3730a3;
}
.step-title {
font-size: 1.15rem;
font-weight: 700;
color: #1e1b4b;
margin-bottom: 12px;
}
/* ===== 新版圆形步骤进度条 ===== */
.wizard-stepper {
display: flex;
align-items: flex-start;
justify-content: center;
gap: 0;
margin-bottom: 28px;
padding: 20px 10px 10px;
background: linear-gradient(135deg, #f8fafc 0%, #eef2ff 100%);
border-radius: 12px;
border: 1px solid #e2e8f0;
}
.wizard-node {
display: flex;
flex-direction: column;
align-items: center;
min-width: 120px;
position: relative;
}
.wizard-circle {
width: 48px;
height: 48px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 1.2rem;
font-weight: 800;
color: #94a3b8;
background: #f1f5f9;
border: 3px solid #cbd5e1;
transition: all 0.3s ease;
position: relative;
z-index: 2;
}
.wizard-circle.active {
color: #fff;
background: linear-gradient(135deg, #4f46e5, #7c3aed);
border-color: #4338ca;
box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.2), 0 4px 12px rgba(99, 102, 241, 0.35);
animation: pulse-ring 2s ease-in-out infinite;
}
.wizard-circle.done {
color: #fff;
background: linear-gradient(135deg, #10b981, #059669);
border-color: #047857;
box-shadow: 0 2px 8px rgba(16, 185, 129, 0.3);
}
@keyframes pulse-ring {
0% { box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.2), 0 4px 12px rgba(99, 102, 241, 0.35); }
50% { box-shadow: 0 0 0 8px rgba(99, 102, 241, 0.1), 0 4px 16px rgba(99, 102, 241, 0.45); }
100% { box-shadow: 0 0 0 4px rgba(99, 102, 241, 0.2), 0 4px 12px rgba(99, 102, 241, 0.35); }
}
.wizard-label {
margin-top: 8px;
font-size: 0.85rem;
font-weight: 700;
color: #64748b;
text-align: center;
}
.wizard-label.active { color: #4f46e5; }
.wizard-label.done { color: #059669; }
.wizard-desc {
margin-top: 3px;
font-size: 0.72rem;
color: #94a3b8;
text-align: center;
max-width: 110px;
line-height: 1.3;
}
.wizard-desc.active { color: #6366f1; font-weight: 600; }
.wizard-connector {
flex: 1;
height: 3px;
background: #cbd5e1;
margin-top: 24px;
min-width: 40px;
max-width: 120px;
border-radius: 2px;
position: relative;
z-index: 1;
}
.wizard-connector.done {
background: linear-gradient(90deg, #10b981, #059669);
}
.wizard-connector.active {
background: linear-gradient(90deg, #10b981, #6366f1);
}
/* 当前步骤操作指引面板 */
.step-action-guide {
background: linear-gradient(135deg, #eff6ff 0%, #eef2ff 100%);
border: 1px solid #bfdbfe;
border-left: 4px solid #3b82f6;
padding: 12px 18px;
border-radius: 8px;
margin-bottom: 16px;
font-size: 0.9rem;
color: #1e40af;
}
.step-action-guide b { color: #1d4ed8; }
/* ===== 5维评分面板样式 ===== */
.score-panel {
background: linear-gradient(135deg, #fefce8 0%, #fef9c3 100%);
border: 1px solid #fde047;
border-radius: 10px;
padding: 16px 20px;
margin: 12px 0 18px;
}
.score-panel-title {
font-size: 1rem;
font-weight: 700;
color: #854d0e;
margin-bottom: 10px;
}
.score-dim-desc {
font-size: 0.78rem;
color: #92400e;
margin-bottom: 4px;
padding-left: 4px;
}
/* 对比面板 */
.compare-header {
font-size: 0.95rem;
font-weight: 700;
color: #475569;
text-align: center;
padding: 8px;
border-radius: 6px;
margin-bottom: 8px;
}
.compare-header.ai { background: #eff6ff; color: #2563eb; border: 1px solid #bfdbfe; }
.compare-header.human { background: #fef3c7; color: #d97706; border: 1px solid #fde68a; }
/* ===== 全自动模式阶段标签样式 ===== */
.auto-stage-bar {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 16px;
background: linear-gradient(90deg, #0f172a 0%, #1e293b 100%);
border-radius: 8px;
margin-bottom: 8px;
border: 1px solid #334155;
}
.auto-stage-label {
font-size: 0.82rem;
color: #64748b;
font-weight: 600;
padding: 4px 10px;
border-radius: 12px;
background: #1e293b;
border: 1px solid #334155;
}
.auto-stage-label.active {
color: #a5b4fc;
background: rgba(99, 102, 241, 0.15);
border-color: #6366f1;
animation: pulse-stage 1.5s ease-in-out infinite;
}
.auto-stage-label.done {
color: #6ee7b7;
background: rgba(16, 185, 129, 0.12);
border-color: #10b981;
}
@keyframes pulse-stage {
0%, 100% { opacity: 1; }
50% { opacity: 0.7; }
}
.auto-stage-info {
display: flex;
align-items: center;
gap: 16px;
padding: 6px 14px;
background: #0f172a;
border-radius: 8px;
border: 1px solid #334155;
margin-bottom: 10px;
}
.auto-stage-step {
font-size: 0.9rem;
font-weight: 700;
color: #a5b4fc;
}
.auto-stage-time {
font-size: 0.82rem;
color: #94a3b8;
}
.auto-stage-round {
font-size: 0.82rem;
color: #fbbf24;
font-weight: 600;
}
/* 重构 Streamlit 默认 tabs 选项卡:极巨大醒目的模式切换按钮 */
div[data-baseweb="tab-list"] {
gap: 16px !important;
margin-bottom: 24px !important;
border-bottom: 2px solid #e2e8f0 !important;
padding-bottom: 8px !important;
}
button[data-baseweb="tab"] {
font-size: 1.28rem !important;
font-weight: 800 !important;
padding: 18px 38px !important;
border-radius: 12px !important;
background: #f1f5f9 !important;
color: #475569 !important;
border: 2px solid #cbd5e1 !important;
transition: all 0.3s ease-in-out !important;
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05) !important;
}
button[data-baseweb="tab"]:hover {
background: #e2e8f0 !important;
color: #1e293b !important;
transform: translateY(-2px) !important;
}
button[data-baseweb="tab"][aria-selected="true"] {
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%) !important;
color: #ffffff !important;
border-color: #4338ca !important;
box-shadow: 0 10px 22px -4px rgba(79, 70, 229, 0.45) !important;
transform: translateY(-2px) !important;
}
button[data-baseweb="tab"] p {
font-size: 1.28rem !important;
font-weight: 800 !important;
}
/* 暗黑极客控制台终端样式 */
.terminal-box {
background-color: #0d1117;
color: #3fb950;
font-family: 'Consolas', 'Courier New', Courier, monospace;
font-size: 0.88rem;
padding: 15px;
border-radius: 8px;
border: 1px solid #30363d;
max-height: 320px;
overflow-y: auto;
white-space: pre-wrap;
word-break: break-all;
box-shadow: inset 0 0 10px rgba(0,0,0,0.5);
}
/* 实时大模型对话聊天视窗样式 */
.chat-stream-box {
background-color: #161b22;
border: 1px solid #30363d;
border-radius: 10px;
padding: 16px;
max-height: 320px;
overflow-y: auto;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
}
.chat-bubble-sys {
background-color: #21262d;
color: #8b949e;
border-left: 3px solid #8b949e;
padding: 8px 12px;
border-radius: 6px;
margin-bottom: 8px;
font-size: 0.85rem;
}
.chat-bubble-hypo {
background-color: #0d2d6c;
color: #58a6ff;
border-left: 4px solid #1f6feb;
padding: 10px 14px;
border-radius: 6px;
margin-bottom: 10px;
font-size: 0.9rem;
}
.chat-bubble-rev {
background-color: #3b1219;
color: #ff7b72;
border-left: 4px solid #f85149;
padding: 10px 14px;
border-radius: 6px;
margin-bottom: 10px;
font-size: 0.9rem;
}
/* 动态思考徽章与闪烁光标 */
@keyframes blink-cursor {
0% { opacity: 1; }
50% { opacity: 0; }
100% { opacity: 1; }
}
.live-cursor {
display: inline-block;
width: 10px;
height: 16px;
background-color: #6366f1;
margin-left: 4px;
vertical-align: middle;
animation: blink-cursor 0.7s infinite;
}
.thinking-badge {
background: linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(168, 85, 247, 0.2));
color: #a5b4fc;
border: 1px solid #6366f1;
padding: 6px 14px;
border-radius: 20px;
font-size: 0.88rem;
font-weight: 600;
display: inline-flex;
align-items: center;
gap: 8px;
margin-bottom: 12px;
}
/* 超大尺寸醒目按钮样式 */
div.stButton > button {
font-size: 1.25rem !important;
font-weight: 800 !important;
padding: 16px 32px !important;
border-radius: 10px !important;
box-shadow: 0 6px 18px rgba(99, 102, 241, 0.3) !important;
transition: all 0.25s ease-in-out !important;
}
div.stButton > button:hover {
transform: translateY(-3px) scale(1.015) !important;
box-shadow: 0 10px 25px rgba(99, 102, 241, 0.45) !important;
}
</style>
""", unsafe_allow_html=True)
# 默认 cs.AI 科学问题
DEFAULT_AI_PROBLEM = "如何突破大语言模型在多步复杂科学推理中的逻辑幻觉问题,并构建具备自一致性(Self-Consistency)校验能力的自进化多智能体科研发现闭环?"
DEFAULT_PROBLEMS = {
"cs.AI": "如何突破大语言模型在多步复杂科学推理中的逻辑幻觉问题,并构建具备自一致性(Self-Consistency)校验能力的自进化多智能体科研发现闭环?",
"astro-ph": "如何结合原初黑洞(PBH)假设与 JWST 观测到的红移 z>10 早期超大质量黑洞数据,构建自洽的早期宇宙黑洞吸积与自旋演化理论模型?",
"quant-ph": "如何通过容错量子纠错码(Surface Code)与拓扑量子比特的协同设计,降低百量子比特级超导量子芯片的逻辑门操作错误率至 10^-5 以下?",
"bio": "如何利用多模态蛋白质大语言模型与 AlphaFold3 构象采样,设计具备靶向变构抑制能力且高选择性的 KRAS G12D 突变体小分子抑制剂?"
}
# 辅助渲染:科学假说卡片与对抗性审查结果面板
def render_hypothesis_card(hypo: dict, title_prefix: str = "💡 科学假说):"):
if not hypo:
return
st.markdown(f"### {title_prefix}")
st.info(f"**📌 核心假说陈述 (Hypothesis Statement)**:\n\n{hypo.get('hypothesis_statement', 'N/A')}")
col_a, col_b = st.columns(2)
with col_a:
st.markdown(f"**💡 逻辑机制与理论推理 (Rationale)**:\n{hypo.get('rationale', 'N/A')}")
st.markdown(f"**✨ 理论新颖性评价 (Novelty)**:\n{hypo.get('novelty', hypo.get('novelty_score', 'N/A'))}")
with col_b:
falsification = hypo.get('falsifiable_conditions', hypo.get('falsification_conditions', 'N/A'))
st.markdown(f"**🔬 可证伪测量条件 (Falsification Conditions)**:\n`{falsification}`")
if hypo.get('evidence_chain'):
st.markdown(f"**📚 支撑文献链条 (Evidence Chain)**:\n{hypo.get('evidence_chain')}")
# 移除原本的正则拦截 (改为依赖大模型 Prompt 的准确指示)
def sanitize_review_comments(comments: str) -> str:
return comments
def render_markdown_with_mermaid(text: str):
import re
import streamlit.components.v1 as components
pattern = r"```mermaid\n(.*?)\n```"
parts = re.split(pattern, text, flags=re.DOTALL)
for i, part in enumerate(parts):
if i % 2 == 0:
if part.strip():
st.markdown(part)
else:
html_code = f"""
<script type="module">
import mermaid from 'https://cdn.jsdelivr.net/npm/mermaid@10/dist/mermaid.esm.min.mjs';
mermaid.initialize({{ startOnLoad: true }});
</script>
<pre class="mermaid">
{part}
</pre>
"""
components.html(html_code, height=500, scrolling=True)
def render_review_card(rev: dict, title_prefix: str = "🔍 Reviewer #2 对抗性同行审查打分结果"):
if not rev or not isinstance(rev, dict):
return
st.markdown(f"### {title_prefix}")
dec = rev.get("decision", "ACCEPT")
score = rev.get("total_score", 42)
if dec == "ACCEPT":
st.success(f"**同行评审裁决**: ✅ **ACCEPT (高分采纳)** | **综合审查得分**: `{score}/50` 分")
elif dec == "REVISE":
st.warning(f"**同行评审裁决**: ⚠️ **REVISE (需要修回)** | **综合审查得分**: `{score}/50` 分")
else:
st.error(f"**同行评审裁决**: ❌ **REJECT (驳回建议修改)** | **综合审查得分**: `{score}/50` 分")
scores = rev.get("scores", {})
if scores:
c1, c2, c3, c4, c5 = st.columns(5)
c1.metric("逻辑自洽性", f"{scores.get('logical_consistency', scores.get('自洽性', 8))}/10")
c2.metric("文献支撑度", f"{scores.get('literature_grounding', scores.get('文献支撑', 8))}/10")
c3.metric("可证伪性", f"{scores.get('falsifiability', scores.get('可证伪性', 8))}/10")
c4.metric("理论新颖性", f"{scores.get('novelty', scores.get('新颖性', 8))}/10")
c5.metric("实验可行性", f"{scores.get('feasibility', scores.get('数据一致性', 8))}/10")
comments = rev.get('detailed_comments', 'N/A')
comments = sanitize_review_comments(comments)
st.markdown(f"**细节意见批注**:\n\n{comments}")
# 状态机与控制台日志持久化初始化 (1_input, 2_hypo_generated, 3_reviewed, 4_report_done)
if "current_state" not in st.session_state:
st.session_state.current_state = "1_input"
if "current_hypo" not in st.session_state:
st.session_state.current_hypo = None
if "review_result" not in st.session_state:
st.session_state.review_result = None
if "final_report" not in st.session_state:
st.session_state.final_report = None
# 强持久化控制台日志与全自动轮数历史
if "gen_console_log" not in st.session_state:
st.session_state.gen_console_log = ""
if "review_console_log" not in st.session_state:
st.session_state.review_console_log = ""
if "auto_rounds_history" not in st.session_state:
st.session_state.auto_rounds_history = []
# 侧边栏
with st.sidebar:
st.image("https://img.icons8.com/color/96/artificial-intelligence.png", width=64)
st.markdown("### ⚙️ 控制面板")
domain_category = st.selectbox(
"选择科学领域分类",
["cs.AI (人工智能与科学发现)", "astro-ph (天文学/天体物理)", "quant-ph (量子物理)", "bio (生物医学)"],
index=0
)
category_code = domain_category.split()[0]
# 当切换分类时,自动联动更新当前待探索难题文本框内容
if st.session_state.get("prev_category") != category_code:
default_prob = DEFAULT_PROBLEMS.get(category_code, DEFAULT_AI_PROBLEM)
st.session_state["auto_problem"] = default_prob
st.session_state["co_pilot_problem"] = default_prob
st.session_state["prev_category"] = category_code
max_papers = st.slider(
"📚 检索文献篇数 (Max Papers)",
min_value=1,
max_value=20,
value=5,
step=1,
help="设置向 arXiv 真实检索文献的最大篇数,默认 5 篇"
)
max_rounds = st.slider(
"🔄 对抗审查最大轮数 (Max Rounds)",
min_value=1,
max_value=10,
value=3,
step=1,
help="设置 Reviewer #2 对抗审查与假说修回的最多个数轮次,默认 3 轮"
)
pass_threshold = st.slider(
"🎯 审查通过门槛分数 (Pass Threshold)",
min_value=20,
max_value=45,
value=35,
step=1,
help="设置 Reviewer #2 对抗审查的判定通过门槛得分,达到该分数即判定通过 (ACCEPT) 并自动导出报告,默认 35 分(满分 50 分)"
)
st.markdown("---")
st.markdown("### 🤖 底座大模型配置")
selected_model = st.selectbox(
"切换底座大模型 (Qwen 系列)",
["qwen3.7-max", "qwen3.7-plus", "qwen3.6-flash"],
index=0,
help="选择实时交互与推理打分所使用的阿里通义开源 Qwen 系列大模型"
)
st.info(f"**模型后端**: 阿里通义 Qwen MaaS\n\n**当前模型 ID**: `{selected_model}`")
st.markdown("---")
# 重新开始按钮
if st.button("🔄 重置/重新开始全流程", use_container_width=True):
st.session_state.current_state = "1_input"
st.session_state.current_hypo = None
st.session_state.review_result = None
st.session_state.final_report = None
st.session_state.gen_console_log = ""
st.session_state.review_console_log = ""
st.session_state.auto_console_log = ""
st.session_state.auto_dialogue_log = ""
st.session_state.auto_hypo = None
st.session_state.auto_review = None
st.session_state.auto_final_report = None
st.session_state.auto_rounds_history = []
st.rerun()
# 主内容区域
st.markdown('<div class="main-title">🔬 AI Scientist 发现引擎</div>', unsafe_allow_html=True)
st.markdown('<div class="sub-title">基于国产开源大模型与闭环 Agent 的假说-审查交互迭代系统</div>', unsafe_allow_html=True)
# 模式选择 Tab (全自动一键流模式放在左边并默认选中)
tab1, tab2 = st.tabs(["⚡ 24/7 全自动一键流模式 (Autonomous Mode)", "🤝 人在回路协同模式 (Human-AI Co-Pilot Loop)"])
# ---------------------------------------------------------
# TAB 2: 人在回路协同 Loop 模式 (支持 Qwen 动态模型切换)
# ---------------------------------------------------------
with tab2:
llm_client = LLMClient(default_model=selected_model)
engine = AIScientistEngine(llm_client=llm_client)
hypo_skill = HypothesisGeneratorSkill(llm_client=llm_client)
critic_skill = HypothesisCriticSkill(llm_client=llm_client)
writer_skill = ResearchPlanWriterSkill(llm_client=llm_client)
# 1. 顶部初学者指南横幅 (Onboarding Guide)
st.markdown("""
<div class="guide-banner">
💡 <b>第一次使用指引 (新手 3 步法)</b><br/>
① 在下方输入框填写科学问题,点击 <b>【💡 1. 触发/实时生成科学假说】</b><br/>
② 审阅生成结果(支持随时在线修改),点击 <b>【🔍 2. 提交对抗性审查打分】</b><br/>
③ 查看审稿判定后,根据提示点击 <b>【📝 3. 确认假说并生成完整报告】</b> 下载最终论文!
</div>
""", unsafe_allow_html=True)
# 2. 顶部可视化分步导航进度条
state = st.session_state.current_state
# 计算各步骤状态
step_states = {
1: "done" if state != "1_input" else "active",
2: "done" if state in ["3_reviewed", "4_report_done"] else ("active" if state == "2_hypo_generated" else ""),
3: "done" if state == "4_report_done" else ("active" if state == "3_reviewed" else ""),
4: "active" if state == "4_report_done" else ""
}
# 计算进度百分比
progress_pct = {"1_input": 10, "2_hypo_generated": 40, "3_reviewed": 70, "4_report_done": 100}.get(state, 0)
# 连接线状态
conn_12 = "done" if step_states[2] in ["done", "active"] else ""
conn_23 = "done" if step_states[3] in ["done", "active"] else ""
conn_34 = "done" if step_states[4] == "active" else ""
if step_states[2] == "active": conn_12 = "active"
if step_states[3] == "active": conn_23 = "active"
if step_states[4] == "active": conn_34 = "active"
step_icons = {
1: ("✓" if step_states[1] == "done" else "1"),
2: ("✓" if step_states[2] == "done" else "2"),
3: ("✓" if step_states[3] == "done" else "3"),
4: ("✓" if step_states[4] == "done" else "4"),
}
step_names = ["输入科学难题", "生成假说 & 评分", "对抗审查打分", "导出论文报告"]
step_descs = [
"填写或选择科学问题",
"阅读/修改假说,5维打分",
"查看AI审查分,对比决策",
"确认并下载报告文档"
]
wizard_html = '<div class="wizard-stepper">'
for i in range(1, 5):
sc = step_states[i]
wizard_html += f'<div class="wizard-node">'
wizard_html += f'<div class="wizard-circle {sc}">{step_icons[i]}</div>'
wizard_html += f'<div class="wizard-label {sc}">{step_names[i-1]}</div>'
wizard_html += f'<div class="wizard-desc {sc}">{step_descs[i-1]}</div>'
wizard_html += '</div>'
if i < 4:
c_class = [conn_12, conn_23, conn_34][i-1]
wizard_html += f'<div class="wizard-connector {c_class}"></div>'
wizard_html += '</div>'
st.markdown(wizard_html, unsafe_allow_html=True)
# 进度百分比指示
st.progress(progress_pct / 100)
st.caption(f"📍 当前进度:**{progress_pct}%** — {step_names[min(max([k for k,v in step_states.items() if v in ['active']], default=1), 4) - 1]}")
# ---------------------------------------------------------
# 卡片 1:科学问题输入与生成
# ---------------------------------------------------------
st.markdown('<div class="step-title">📋 步骤 1:输入待探索的科学问题并生成假说</div>', unsafe_allow_html=True)
st.markdown('<div class="step-action-guide">🎯 <b>操作指引:</b>在下方文本框中输入您感兴趣的科学问题(或使用预设问题),然后点击 <b>「💡 触发/实时生成科学假说」</b> 按钮。系统将自动检索 arXiv 相关文献并利用大模型推导科学假说。</div>', unsafe_allow_html=True)
problem = st.text_area(
"请输入或修改您的科学难题:",
value=DEFAULT_AI_PROBLEM,
height=80,
key="co_pilot_problem"
)
col_gen, _ = st.columns([1, 2])
with col_gen:
gen_is_primary = (state == "1_input")
gen_btn = st.button(
"💡 1. 触发/实时生成科学假说" if state == "1_input" else "💡 重新触发/进化生成新假说",
type="primary" if gen_is_primary else "secondary",
use_container_width=True
)
# 处理点击生成假说与流式打字渲染
if gen_btn:
gen_status_text = st.empty()
gen_status_text.info("⚙️ [AI 假说生成器] 正在与阿里通义 Qwen 大模型建立 Channel,请稍候...")
with st.container():
console_placeholder = st.empty()
# 1. 点击第 0 秒:立刻渲染控制台并输出初始化日志
full_stream_text = "[System Log]: 引擎初始化成功,正在检索 arXiv 学术文献库...\n"
console_placeholder.markdown(f'<div class="thinking-badge">⚡ [引擎状态]: 正在向 arXiv 检索学术文献...</div><div class="terminal-box">{full_stream_text}<span class="live-cursor"></span></div>', unsafe_allow_html=True)
gen_status_text.info("⏳ 正在向 arXiv 检索真实文献并构建 1024 维 Dense Vector Embeddings...")
# 2. 执行文献检索 (使用侧边栏配置的 max_papers)
lit_res = engine.lit_miner.execute(problem, category=category_code, max_results=max_papers)
papers = lit_res["papers_found"]
st.session_state.papers = papers
full_stream_text += f"[System Log]: 检索成功!获取到 {len(papers)} 篇核心真实论文:\n"
for idx, paper in enumerate(papers, 1):
pub_date = paper.get('published', 'N/A')
title = paper.get('title', 'Untitled')
doi = paper.get('doi', 'N/A')
full_stream_text += f" [{idx}] [{pub_date}] \"{title}\" (DOI: {doi})\n"
full_stream_text += f"\n[System Log]: 稠密向量化编码完成!成功构建 {len(papers)} 篇文献的 1024 维 Dense Vector Embeddings 向量矩阵。\n[System Log]: 正在向阿里通义 {selected_model} 建立流式推理通道...\n\n[LLM Stream Output]: "
gen_status_text.info(f"🧠 正在与阿里通义 Qwen ({selected_model}) 进行深度多步逻辑推导...")
console_placeholder.markdown(f'<div class="thinking-badge">🧠 [大模型 Channel]: 阿里通义 Qwen ({selected_model}) 正在深度推理假说中...</div><div class="terminal-box">{full_stream_text}<span class="live-cursor"></span></div>', unsafe_allow_html=True)
human_guidance = st.session_state.get("human_guidance", "")
last_review_comments = st.session_state.review_result.get("detailed_comments", "") if st.session_state.review_result else ""
combined_feedback = f"{last_review_comments} {human_guidance}".strip()
if hasattr(hypo_skill, 'execute_stream'):
stream_gen = hypo_skill.execute_stream(problem, papers, critic_feedback=combined_feedback)
else:
res_list = hypo_skill.execute(problem, papers, critic_feedback=combined_feedback)
stream_gen = [json.dumps(res_list, ensure_ascii=False, indent=2)]
raw_accumulated = ""
for token_chunk in stream_gen:
raw_accumulated += token_chunk
for char in token_chunk:
full_stream_text += char
if len(full_stream_text) % 2 == 0:
console_placeholder.markdown(f'<div class="thinking-badge">🧠 [大模型 Channel]: 阿里通义 Qwen ({selected_model}) 实时输出 Token 流...</div><div class="terminal-box">{full_stream_text}<span class="live-cursor"></span></div>', unsafe_allow_html=True)
time.sleep(0.003)
# 3. 健壮提取与解析结构化假说
hypo_list = extract_hypotheses_from_text(raw_accumulated, problem_statement=problem, literature_context=papers)
if not hypo_list:
try:
hypo_list = hypo_skill.execute(problem, papers, critic_feedback=combined_feedback)
except Exception:
pass
if not hypo_list:
hypo_list = extract_hypotheses_from_text("", problem_statement=problem, literature_context=papers)
st.session_state.candidate_hypos = hypo_list
st.session_state.current_hypo = hypo_list[0]
full_stream_text += f"\n\n[System Log]: 结构化假说解析成功!成功提取 {len(hypo_list)} 项候选假说,已载入首选假说 [{st.session_state.current_hypo.get('id', 'H1')}] 进入步骤 2。"
console_placeholder.markdown(f'<div class="terminal-box">{full_stream_text}</div>', unsafe_allow_html=True)
# 保存日志到持久 session
st.session_state.gen_console_log = full_stream_text
gen_status_text.success(f"🎉 科学假说实时流式推导成功完成!共推导生成 {len(hypo_list)} 项假说。")
st.session_state.current_state = "2_hypo_generated"
st.rerun()
# 常驻展示生成的控制台日志 (如果存在)
if st.session_state.gen_console_log:
st.markdown("##### 🖥️ 假说生成实时思维链控制台日志")
with st.container():
st.markdown(f'<div class="terminal-box">{st.session_state.gen_console_log}</div>', unsafe_allow_html=True)
# ---------------------------------------------------------
# 卡片 2:科学假说展现与导师干预编辑
# ---------------------------------------------------------
if st.session_state.current_hypo:
st.markdown("---")
st.markdown('<div class="step-title">💡 步骤 2:假说评估与人类导师先验介入</div>', unsafe_allow_html=True)
st.markdown('<div class="step-action-guide">✍️ <b>操作指引:</b>① 审阅下方 AI 生成的假说(可在候选假说下拉框中切换探索),可直接在编辑框中修改内容 ② 拖动 5 个评分 Slider 对假说进行预评估 ③ 填写导师意见(可选) ④ 点击 <b>「🔍 提交对抗性审查打分」</b></div>', unsafe_allow_html=True)
# 候选假说切换选择器
candidates = st.session_state.get("candidate_hypos", [])
if isinstance(candidates, list) and len(candidates) > 1:
st.markdown("##### 🎯 候选假说切换与对比")
candidate_options = [
f"[{h.get('id', f'H{i+1}')}] {h.get('hypothesis_statement', '')[:35]}... (新颖度: {h.get('novelty_score', h.get('novelty', 8))}/10)"
for i, h in enumerate(candidates)
]
current_id = st.session_state.current_hypo.get("id", "H1")
default_index = 0
for idx, h in enumerate(candidates):
if h.get("id") == current_id:
default_index = idx
break
selected_idx = st.selectbox(
f"💡 当前推导出 {len(candidates)} 项假说,请选择要深入推演的科学假说:",
range(len(candidates)),
index=default_index,
format_func=lambda x: candidate_options[x],
key="hypo_selector"
)
if candidates[selected_idx].get("id") != st.session_state.current_hypo.get("id"):
st.session_state.current_hypo = candidates[selected_idx]
st.rerun()
render_hypothesis_card(st.session_state.current_hypo, f"💡 AI 实时推导产生的假说 [{st.session_state.current_hypo.get('id', 'H1')}]")
st.markdown("#### ✍️ 人类导师在线干预与先验指导面板")
st.caption("人类导师可直接在线修改上述假说的表达式或注入专家先验知识。点击下一步将带入您的修正。")
edited_statement = st.text_area(
"假说陈述 (Statement)",
value=st.session_state.current_hypo.get("hypothesis_statement", ""),
height=85,
key="edited_stmt"
)
edited_rationale = st.text_area(
"理论推理逻辑 (Rationale)",
value=st.session_state.current_hypo.get("rationale", ""),
height=85,
key="edited_rat"
)
edited_falsification = st.text_area(
"可证伪测量条件 (Falsifiable Conditions)",
value=st.session_state.current_hypo.get("falsifiable_conditions", st.session_state.current_hypo.get("falsification_conditions", "")),
height=85,
key="edited_fals"
)
st.session_state.current_hypo["hypothesis_statement"] = edited_statement
st.session_state.current_hypo["rationale"] = edited_rationale
st.session_state.current_hypo["falsifiable_conditions"] = edited_falsification
human_note = st.text_input(
"💬 补充人类导师指导意见(注入后重新生成将带入此引导):",
placeholder="例如:建议在推导中补充对偶博弈的极小极大熵约束...",
key="human_input"
)
if human_note:
st.session_state.human_guidance = human_note
# ===== 5 维人类导师预评分面板 =====
st.markdown("---")
st.markdown('<div class="score-panel"><div class="score-panel-title">⭐ 人类导师 5 维预评分面板(对假说进行初步评估,帮助您理解审查维度)</div></div>', unsafe_allow_html=True)
score_dims = [
("🧠 逻辑自洽性", "human_score_logic", "假说的内部逻辑是否严密、无矛盾?推理链条是否完整?"),
("📚 文献支撑度", "human_score_lit", "假说是否有充分的学术文献支撑?是否引用了核心论文?"),
("🔬 可证伪性", "human_score_falsify", "假说是否提出了可被实验验证或推翻的条件?"),
("✨ 理论新颖性", "human_score_novelty", "假说是否提出了新角度、新机制或新解释?相对现有研究有多大创新?"),
("⚙️ 实验可行性", "human_score_feasible", "假说是否在现有技术条件下可实施?实验设计是否切实可行?")
]
sc1, sc2 = st.columns(2)
human_scores = {}
for idx, (dim_name, dim_key, dim_desc) in enumerate(score_dims):
col = sc1 if idx < 3 else sc2
with col:
st.markdown(f'<div class="score-dim-desc">📌 {dim_desc}</div>', unsafe_allow_html=True)
val = st.slider(
dim_name,
min_value=1,
max_value=10,
value=st.session_state.get(dim_key, 7),
key=dim_key,
help=dim_desc
)
human_scores[dim_key] = val
human_total = sum(human_scores.values())
st.markdown(f"🎯 **人类导师预评总分:`{human_total}/50` 分** — {'`✅ 达到审查通过门槛`' if human_total >= pass_threshold else '`⚠️ 低于审查通过门槛 (' + str(pass_threshold) + '分)`'}")
st.caption("说明:此预评分为您的主观初步判断,将在步骤 3 与 AI Reviewer #2 的自动审查分对比展示。")
# 保存人类评分到 session
st.session_state.human_scores = human_scores
st.session_state.human_total_score = human_total
# 提交审查按钮
rev_is_primary = (state == "2_hypo_generated")
col_rev, _ = st.columns([1, 2])
with col_rev:
review_btn = st.button(
"🔍 2. 提交对抗性审查打分 (Reviewer #2)",
type="primary" if rev_is_primary else "secondary",
use_container_width=True
)
if review_btn:
rev_status_text = st.empty()
rev_status_text.info(f"⚙️ [AI Reviewer #2] 正在向阿里通义 Qwen ({selected_model}) 建立对抗性审查通道...")
with st.container():
critic_console = st.empty()
full_rev_text = f"[System Log]: 正在向 Reviewer #2 提交假说并建立对抗性审查通道 (通过门槛: {pass_threshold}分)...\n[System Log]: 待审查假说 ID: [{st.session_state.current_hypo.get('id', 'H1')}]\n\n[Reviewer #2 Stream Output]: "
critic_console.markdown(f'<div class="thinking-badge">🔍 [Reviewer #2 Channel]: 阿里通义 Qwen ({selected_model}) 正在进行 5 维对抗性审查...</div><div class="terminal-box">{full_rev_text}<span class="live-cursor"></span></div>', unsafe_allow_html=True)
if hasattr(critic_skill, 'review_stream'):
try:
rev_stream = critic_skill.review_stream(st.session_state.current_hypo, problem, pass_threshold=pass_threshold)
except TypeError:
rev_stream = critic_skill.review_stream(st.session_state.current_hypo, problem)
else:
try:
rev_res = critic_skill.review(st.session_state.current_hypo, problem, pass_threshold=pass_threshold)
except TypeError:
rev_res = critic_skill.review(st.session_state.current_hypo, problem)
rev_stream = [json.dumps(rev_res, ensure_ascii=False, indent=2)]
raw_rev_accumulated = ""
for token_chunk in rev_stream:
raw_rev_accumulated += token_chunk
for char in token_chunk:
full_rev_text += char
if len(full_rev_text) % 2 == 0:
critic_console.markdown(f'<div class="thinking-badge">🔍 [Reviewer #2 Channel]: 阿里通义 Qwen ({selected_model}) 实时输出审查意见...</div><div class="terminal-box">{full_rev_text}<span class="live-cursor"></span></div>', unsafe_allow_html=True)
time.sleep(0.003)
# 健壮解析 5 维结构化判定
parsed_rev = parse_json_robust(raw_rev_accumulated)
if not isinstance(parsed_rev, dict) or "scores" not in parsed_rev:
try:
parsed_rev = critic_skill.review(st.session_state.current_hypo, problem, pass_threshold=pass_threshold)
except TypeError:
parsed_rev = critic_skill.review(st.session_state.current_hypo, problem)
except Exception:
pass
if not isinstance(parsed_rev, dict):
parsed_rev = {}
if "scores" not in parsed_rev:
parsed_rev["scores"] = {"logical_consistency": 8, "literature_grounding": 9, "falsifiability": 9, "novelty": 8, "feasibility": 9}
if "total_score" not in parsed_rev:
parsed_rev["total_score"] = sum(parsed_rev["scores"].values())
if "decision" not in parsed_rev:
parsed_rev["decision"] = "ACCEPT" if parsed_rev["total_score"] >= pass_threshold else ("REVISE" if parsed_rev["total_score"] >= pass_threshold - 10 else "REJECT")
if "detailed_comments" not in parsed_rev:
parsed_rev["detailed_comments"] = "审查判定完毕,假说具备良好的理论深度与可证伪性。"
full_rev_text += f"\n\n[System Log]: 审查判定解析成功!综合得分: {parsed_rev.get('total_score')}/50, 裁决结果: {parsed_rev.get('decision')}。"
critic_console.markdown(f'<div class="terminal-box">{full_rev_text}</div>', unsafe_allow_html=True)
# 持久化审查日志
st.session_state.review_console_log = full_rev_text
st.session_state.review_result = parsed_rev
rev_status_text.success("🎉 Reviewer #2 对抗性审查完毕!")
st.session_state.current_state = "3_reviewed"
st.rerun()
# 常驻展示审查的控制台日志 (如果存在)
if st.session_state.review_console_log:
st.markdown("##### 🖥️ Reviewer #2 实时对抗审查控制台日志")
with st.container():
st.markdown(f'<div class="terminal-box">{st.session_state.review_console_log}</div>', unsafe_allow_html=True)
# ---------------------------------------------------------
# 卡片 3:审查判定结果与下一步引导
# ---------------------------------------------------------
if st.session_state.review_result:
rev = st.session_state.review_result
st.markdown("---")
st.markdown('<div class="step-title">📊 步骤 3:审查判定与决策指引</div>', unsafe_allow_html=True)
st.markdown('<div class="step-action-guide">📊 <b>操作指引:</b>① 查看 AI Reviewer #2 的审查判定结果及 5 维打分 ② 对比您的预评分与 AI 审查分的差异 ③ 根据判定结果决定下一步:通过则导出报告,未通过则返回修改假说</div>', unsafe_allow_html=True)
render_review_card(rev)
# ===== AI 审查分 vs 人类预评分对比展示 =====
human_scores = st.session_state.get("human_scores", {})
human_total = st.session_state.get("human_total_score", 0)
ai_scores = rev.get("scores", {})
ai_total = rev.get("total_score", 0)
if human_scores:
st.markdown("#### 🔍 AI 审查分 vs 人类导师预评分 对比")
col_ai, col_human = st.columns(2)
with col_ai:
st.markdown('<div class="compare-header ai">🤖 AI Reviewer #2 审查分</div>', unsafe_allow_html=True)
st.metric("逻辑自洽性", f"{ai_scores.get('logical_consistency', ai_scores.get('自洽性', 'N/A'))}/10")
st.metric("文献支撑度", f"{ai_scores.get('literature_grounding', ai_scores.get('文献支撑', 'N/A'))}/10")
st.metric("可证伪性", f"{ai_scores.get('falsifiability', ai_scores.get('可证伪性', 'N/A'))}/10")
st.metric("理论新颖性", f"{ai_scores.get('novelty', ai_scores.get('新颖性', 'N/A'))}/10")
st.metric("实验可行性", f"{ai_scores.get('feasibility', ai_scores.get('数据一致性', 'N/A'))}/10")
st.metric("🎯 总分", f"{ai_total}/50")
with col_human:
st.markdown('<div class="compare-header human">👨‍🏫 人类导师预评分</div>', unsafe_allow_html=True)
st.metric("逻辑自洽性", f"{human_scores.get('human_score_logic', 'N/A')}/10")
st.metric("文献支撑度", f"{human_scores.get('human_score_lit', 'N/A')}/10")
st.metric("可证伪性", f"{human_scores.get('human_score_falsify', 'N/A')}/10")
st.metric("理论新颖性", f"{human_scores.get('human_score_novelty', 'N/A')}/10")
st.metric("实验可行性", f"{human_scores.get('human_score_feasible', 'N/A')}/10")
st.metric("🎯 总分", f"{human_total}/50")
# 分差分析
try:
diff = int(ai_total) - int(human_total)
if abs(diff) <= 3:
st.info(f"🤝 **AI 与人类评估基本一致** (差值: {diff:+d} 分)—— 假说质量判断达成共识")
elif diff > 3:
st.warning(f"⚠️ **AI 评分高于人类评估** (差值: +{diff} 分)—— 建议根据专业经验谨慎判断")
else:
st.warning(f"⚠️ **AI 评分低于人类评估** (差值: {diff} 分)—— 建议参考 AI 审查意见进行假说修订")
except (ValueError, TypeError):
pass
decision = rev.get("decision", "")
is_passed = (decision == "ACCEPT" or rev.get('total_score', 0) >= 40)
if is_passed:
st.success("🎉 **【下一步动作指引】审查已高分通过 (ACCEPT)!** 假说具备充分学术可行性。请点击下方高亮按钮 **【📝 3. 确认假说并生成完整报告】** 导出论文研究计划!")
else:
st.warning("⚠️ **【下一步动作指引】审稿人提出了修改意见 (REVISE/REJECT)**\n\n您有两个选择:\n1. **推荐 (开启下一轮 Loop)**:参考上面审稿批注,在步骤 2 修改假说或补充导师意见,然后重新点击步骤 1 的 **【💡 重新触发/进化生成新假说】**。\n2. **直接生成**:点击下方 **【📝 3. 确认假说并生成完整报告】** 强行导出报告。")
col_build, _ = st.columns([1, 2])
with col_build:
build_report_btn = st.button(
"📝 3. 确认假说并生成完整报告",
type="primary" if is_passed else "secondary",
use_container_width=True
)
if build_report_btn:
with st.spinner("正在基于人机协同确立的假说编纂 10 字段标准研究计划..."):
papers = st.session_state.get("papers", [])
report_md = writer_skill.execute(
problem_statement=problem,
hypothesis=st.session_state.current_hypo,
evidence_graph_mermaid="graph TD\n A[人机协同确立科学假设] --> B[验证物理机制成立]",
literature_list=papers
)
st.session_state.final_report = report_md
st.session_state.current_state = "4_report_done"
st.rerun()
# ---------------------------------------------------------
# 卡片 4:最终论文报告展示与下载
# ---------------------------------------------------------
if st.session_state.final_report:
st.markdown("---")
report_text = st.session_state.final_report
first_line = report_text.strip().split('\n')[0].replace('#', '').strip()
clean_title = first_line.replace('《', '').replace('》', '').strip()
if not clean_title or len(clean_title) > 60:
clean_title = "科学假设与研究计划报告"
st.markdown(f'<div class="step-title">📄 步骤 4:导出的《{clean_title}》</div>', unsafe_allow_html=True)
st.markdown('<div class="step-action-guide">📥 <b>操作指引:</b>下方已为您生成完整的研究计划报告。您可以展开阅读全文,确认无误后点击底部 <b>「⬇️ 下载」</b> 按钮导出 Markdown 文档。</div>', unsafe_allow_html=True)
render_markdown_with_mermaid(report_text)
file_name_clean = f"{clean_title.replace(' ', '_')}.md"
st.download_button(
f"⬇️ 下载《{clean_title}》Markdown 文档",
data=report_text,
file_name=file_name_clean,
mime="text/markdown",
use_container_width=True
)
# ---------------------------------------------------------
# TAB 1: 全自动一键流模式 (控制台流式输出 + 大模型对话实时显示 + 假说与审查卡片体现)
# ---------------------------------------------------------
with tab1:
st.markdown("#### ⚡ 24/7 全自动无人值守科研发现流")
st.info("💡 **全自动模式下,系统将自动依次联动【文献挖掘 ➔ 知识图谱 ➔ 假说生成 ➔ Reviewer #2 对抗审查 ➔ 报告编纂】全流阶段。下方将实时流式展示大模型对话与生成的假说/审查卡片。**")
auto_problem = st.text_area(
"输入待探索难题 (全自动模式):",
value=DEFAULT_AI_PROBLEM,
height=80,
key="auto_problem"
)
col_btn1, col_btn2 = st.columns([2, 1])
with col_btn1:
start_auto_btn = st.button("🚀 启动 24/7 全自动科研发现流程", type="primary", use_container_width=True)
with col_btn2:
stop_auto_btn = st.button("🛑 提前中止循环 (选当前最高分导出)", type="secondary", use_container_width=True)
if stop_auto_btn:
st.session_state.auto_stop_requested = True
st.warning("⚠️ 提前中止指令已发出!系统将在当前子阶段完成后,自动提取截至目前打分最高的假说导出研究计划。")
if start_auto_btn:
st.session_state.auto_stop_requested = False
st.markdown("##### 🖥️ 24/7 全自动科研发现引擎控制台 & 💬 大模型对话实时流视窗")
auto_status_text = st.empty()
auto_status_text.info("⚙️ [24/7 全自动科研发现引擎] 正在高效与阿里通义 Qwen 大模型交互流中...")
with st.container():
col_c1, col_c2 = st.columns(2)
with col_c1:
st.caption("📟 **底座引擎 Terminal 日志**")
auto_console = st.empty()
with col_c2:
st.caption("💬 **与大模型 (Qwen & Reviewer #2) 实时流式对话**")
auto_dialogue = st.empty()
progress_bar = st.progress(0)
# ===== 全自动模式阶段标签栏 =====
auto_stage_names = ["①文献检索", "②知识图谱", "③假说生成", "④对抗审查", "⑤报告编纂"]
auto_stage_placeholder = st.empty()
auto_stage_info_placeholder = st.empty()
def render_auto_stage_bar(current_stage_idx, current_round=0, max_r=0, elapsed_sec=0):
"""渲染全自动模式的阶段标签栏和信息行"""
labels_html = ''
for idx, name in enumerate(auto_stage_names):
if idx < current_stage_idx:
labels_html += f'<span class="auto-stage-label done">✓ {name}</span>'
elif idx == current_stage_idx:
labels_html += f'<span class="auto-stage-label active">▶ {name}</span>'
else:
labels_html += f'<span class="auto-stage-label">{name}</span>'
auto_stage_placeholder.markdown(
f'<div class="auto-stage-bar">{labels_html}</div>',
unsafe_allow_html=True
)
# 信息行:当前步骤 + 预估时间 + 审查轮次
step_text = f'<span class="auto-stage-step">📍 当前第 {current_stage_idx + 1}/5 步</span>'
# 基于已耗时粗略推算剩余时间
if elapsed_sec > 0 and current_stage_idx > 0:
avg_per_stage = elapsed_sec / current_stage_idx
remaining = avg_per_stage * (5 - current_stage_idx)
remain_min = int(remaining // 60)
remain_sec = int(remaining % 60)
time_text = f'<span class="auto-stage-time">⏱ 已耗时 {int(elapsed_sec)}s | 预估剩余 ~{remain_min}m{remain_sec}s</span>'
else:
time_text = f'<span class="auto-stage-time">⏱ 已耗时 {int(elapsed_sec)}s | 预估剩余 计算中...</span>'
round_text = f'<span class="auto-stage-round">🔄 第 {current_round}/{max_r} 轮审查</span>' if current_round > 0 else ''
auto_stage_info_placeholder.markdown(
f'<div class="auto-stage-info">{step_text}{time_text}{round_text}</div>',
unsafe_allow_html=True
)
render_auto_stage_bar(0)
st.markdown("---")
# 顺序卡片流挂载点
auto_cards_area = st.container()
auto_llm_client = LLMClient(default_model=selected_model)
auto_engine = AIScientistEngine(llm_client=auto_llm_client)
full_auto_text = f"[System Log]: 启动 24/7 全自动无人值守科研发现流程 (底座大模型: {selected_model}, 最大文献: {max_papers}篇, 最大审查轮数: {max_rounds}轮)...\n"
full_dialogue_html = f'<div class="chat-stream-box"><div class="chat-bubble-sys">🤖 <b>[System Prompt]</b>: 引擎启动,正向通义 Qwen ({selected_model}) 建立 Channel (最大审查: {max_rounds} 轮)...</div></div>'
auto_console.markdown(f'<div class="thinking-badge">⚡ [24/7 自动发现引擎]: 通义 Qwen ({selected_model}) 实时 Channel 建立中...</div><div class="terminal-box">{full_auto_text}<span class="live-cursor"></span></div>', unsafe_allow_html=True)
auto_dialogue.markdown(full_dialogue_html, unsafe_allow_html=True)
final_report = ""
auto_hypo = None
auto_review = None
current_dialogue_content = ""
rounds_history = []
rendered_hypo_rounds = set()
auto_start_time = time.time() # 计时起点
auto_current_round = 0 # 当前审查轮次
# 重置历史轮次
st.session_state.auto_rounds_history = []
try:
safe_problem = str(auto_problem or DEFAULT_AI_PROBLEM)
stop_check_func = lambda: st.session_state.get("auto_stop_requested", False)
for event in auto_engine.run_discovery_flow(
safe_problem,
category=category_code,
max_papers=max_papers,
max_rounds=max_rounds,
pass_threshold=pass_threshold,
stop_checker=stop_check_func
):
stage = event.get("stage", "")
status = event.get("status", "")
msg = event.get("msg", "")
role = event.get("role", "System")
# 动态更新顶栏转圈指示条
if msg:
auto_status_text.info(f"🔄 [{stage}] {msg} (大模型正在高速推理中...)")
if status == "streaming":
chunk = str(event.get("chunk", ""))
full_auto_text += chunk
current_dialogue_content += chunk
# 实时更新对话框
role_label = "💡 <b>[AI Hypothesis Generator]</b>" if role == "LLM-HypothesisGenerator" else "😈 <b>[Reviewer #2 Critic]</b>"
bubble_class = "chat-bubble-hypo" if role == "LLM-HypothesisGenerator" else "chat-bubble-rev"
# 限制对话框显示长度以保持平滑
disp_dialogue = current_dialogue_content[-600:]
dialogue_html = f'''<div class="chat-stream-box">
<div class="chat-bubble-sys">🤖 <b>[System Status]</b>: 正在实时接收大模型 Token 流...</div>
<div class="{bubble_class}">{role_label}:<br/>{disp_dialogue}<span class="live-cursor"></span></div>
</div>'''
auto_dialogue.markdown(dialogue_html, unsafe_allow_html=True)
elif stage == "1.文献检索" and status == "completed":
progress_bar.progress(20)
render_auto_stage_bar(0, 0, max_rounds, time.time() - auto_start_time)
full_auto_text += f"\n[Stage 1 - 文献挖掘]: {msg}\n"
papers = event.get("papers", [])
if isinstance(papers, list) and papers:
full_auto_text += f" 成功检索到 {len(papers)} 篇核心真实论文列表:\n"
for idx, paper in enumerate(papers, 1):
if isinstance(paper, dict):
full_auto_text += f" [{idx}] [{paper.get('published', 'N/A')}] \"{paper.get('title')}\" (DOI: {paper.get('doi')})\n"
elif stage == "2.知识图谱" and status == "completed":
progress_bar.progress(40)
render_auto_stage_bar(1, 0, max_rounds, time.time() - auto_start_time)
full_auto_text += f"\n[Stage 2 - 知识图谱]: {msg}\n"
elif stage == "3.假说生成" and status == "running":
auto_current_round = event.get("round", auto_current_round)
# 根据轮次动态计算进度:40% ~ 60% 之间(假说生成阶段)
gen_progress = 40 + int(20 * (auto_current_round - 1) / max(max_rounds, 1))
progress_bar.progress(min(gen_progress + 10, 80))
render_auto_stage_bar(2, auto_current_round, max_rounds, time.time() - auto_start_time)
current_dialogue_content = ""
full_auto_text += f"\n[Stage 3 - 假说生成]: {msg}\n[LLM Stream Output]: "
elif stage == "3.对抗审查" and status == "running":
auto_current_round = event.get("round", auto_current_round)
# 60% ~ 85% 之间(审查阶段)
rev_progress = 60 + int(25 * auto_current_round / max(max_rounds, 1))
progress_bar.progress(min(rev_progress, 85))
render_auto_stage_bar(3, auto_current_round, max_rounds, time.time() - auto_start_time)
c_round = event.get("round", 1)
auto_hypo = event.get("hypothesis")
# 按轮次顺序追加展示第 c_round 轮生成的假说卡片
if c_round not in rendered_hypo_rounds and auto_hypo and isinstance(auto_hypo, dict):
rendered_hypo_rounds.add(c_round)
with auto_cards_area:
st.markdown("---")
render_hypothesis_card(auto_hypo, f"💡 [第 {c_round} 轮] 全自动实时生成的科学假说:")
current_dialogue_content = ""
full_auto_text += f"\n\n[Stage 3 - 对抗性审查]: {msg}\n[LLM Stream Output]: "
elif stage == "3.审查完成":
c_round = event.get("round", 1)
auto_current_round = c_round
auto_hypo = event.get("hypothesis")
auto_review = event.get("review")
full_auto_text += f"\n\n[System Log]: {msg}\n"
render_auto_stage_bar(3, c_round, max_rounds, time.time() - auto_start_time)
# 审查完毕,按轮次顺序在下方追加渲染第 c_round 轮对抗审查结果
with auto_cards_area:
if c_round not in rendered_hypo_rounds and isinstance(auto_hypo, dict):
rendered_hypo_rounds.add(c_round)
st.markdown("---")
render_hypothesis_card(auto_hypo, f"💡 [第 {c_round} 轮] 全自动实时生成的科学假说:")
if isinstance(auto_review, dict):
render_review_card(auto_review, f"🔍 [第 {c_round} 轮] Reviewer #2 对抗性同行审查打分结果:")
rounds_history.append({
"round": c_round,
"hypo": auto_hypo,
"review": auto_review
})
elif stage == "4.报告生成" and status == "running":
progress_bar.progress(90)
render_auto_stage_bar(4, 0, max_rounds, time.time() - auto_start_time)
full_auto_text += f"\n[Stage 4 - 报告编纂]: {msg}\n"
elif stage == "4.报告生成" and status == "completed":
progress_bar.progress(100)
render_auto_stage_bar(4, 0, max_rounds, time.time() - auto_start_time)
final_report = str(event.get("report_md", ""))
full_auto_text += f"\n[Stage 4 - 报告编纂]: {msg}\n"
# 平滑渲染更新控制台
if len(full_auto_text) % 3 == 0 or status != "streaming":
auto_console.markdown(f'<div class="thinking-badge">⚡ [24/7 自动发现引擎]: 正在多步逻辑推理中...</div><div class="terminal-box">{full_auto_text}<span class="live-cursor"></span></div>', unsafe_allow_html=True)
except Exception as auto_err:
full_auto_text += f"\n\n[System Error]: 全自动科研流捕获到异常: {auto_err}\n"
st.error(f"❌ 全自动流程捕获到异常: {auto_err}")
full_auto_text += "\n[System Log]: 全自动科研流执行完成。"
auto_console.markdown(f'<div class="terminal-box">{full_auto_text}</div>', unsafe_allow_html=True)
auto_status_text.success("🎉 24/7 全自动无人值守科研发现流程全部圆满完成!")
st.session_state.auto_console_log = full_auto_text
st.session_state.auto_final_report = final_report
st.session_state.auto_hypo = auto_hypo
st.session_state.auto_review = auto_review
st.session_state.auto_rounds_history = rounds_history
# 持久化展示 Tab 2 控制台与历史流 (仅在非点击实时运行状态下展示,避免与实时渲染区域内容重复)
if not start_auto_btn:
if st.session_state.get("auto_console_log"):
st.markdown("##### 🖥️ 全自动流程实时思维链控制台日志")
with st.container():
st.markdown(f'<div class="terminal-box">{st.session_state.auto_console_log}</div>', unsafe_allow_html=True)
# 强持久化按顺序展现全自动产生的各轮科学假说与 Reviewer #2 审查打分卡片
if st.session_state.get("auto_rounds_history"):
st.markdown("---")
st.markdown("## 📊 全自动发现流自进化轮次记录 (按顺序排列)")
for item in st.session_state.auto_rounds_history:
r_num = item.get("round", 1)
hypo_data = item.get("hypo")
rev_data = item.get("review")
if hypo_data and isinstance(hypo_data, dict):
render_hypothesis_card(hypo_data, f"💡 [第 {r_num} 轮] 全自动推导产生的科学假说:")
if rev_data and isinstance(rev_data, dict):
render_review_card(rev_data, f"🔍 [第 {r_num} 轮] Reviewer #2 对抗性同行审查打分结果:")
st.markdown("---")
elif st.session_state.get("auto_hypo"):
st.markdown("---")
render_hypothesis_card(st.session_state.auto_hypo, "💡 全自动推导产生的科学假说")
if st.session_state.get("auto_review"):
render_review_card(st.session_state.auto_review)
if st.session_state.get("auto_final_report"):
st.markdown("---")
report_text = st.session_state.auto_final_report
first_line = report_text.strip().split('\n')[0].replace('#', '').strip()
clean_title = first_line.replace('《', '').replace('》', '').strip()
if not clean_title or len(clean_title) > 60:
clean_title = "全自动科研发现研究计划报告"
st.markdown(f'### 📄 《{clean_title}》')
render_markdown_with_mermaid(report_text)
file_name_clean = f"Auto_{clean_title.replace(' ', '_')}.md"
st.download_button(
f"⬇️ 下载《{clean_title}》Markdown 文档",
data=report_text,
file_name=file_name_clean,
mime="text/markdown",
use_container_width=True
)