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("""
""", 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"""
{part}
"""
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('🔬 AI Scientist 发现引擎
', unsafe_allow_html=True)
st.markdown('基于国产开源大模型与闭环 Agent 的假说-审查交互迭代系统
', 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("""
💡 第一次使用指引 (新手 3 步法):
① 在下方输入框填写科学问题,点击 【💡 1. 触发/实时生成科学假说】
② 审阅生成结果(支持随时在线修改),点击 【🔍 2. 提交对抗性审查打分】
③ 查看审稿判定后,根据提示点击 【📝 3. 确认假说并生成完整报告】 下载最终论文!
""", 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 = ''
for i in range(1, 5):
sc = step_states[i]
wizard_html += f'
'
wizard_html += f'
{step_icons[i]}
'
wizard_html += f'
{step_names[i-1]}
'
wizard_html += f'
{step_descs[i-1]}
'
wizard_html += '
'
if i < 4:
c_class = [conn_12, conn_23, conn_34][i-1]
wizard_html += f'
'
wizard_html += '
'
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('📋 步骤 1:输入待探索的科学问题并生成假说
', unsafe_allow_html=True)
st.markdown('🎯 操作指引:在下方文本框中输入您感兴趣的科学问题(或使用预设问题),然后点击 「💡 触发/实时生成科学假说」 按钮。系统将自动检索 arXiv 相关文献并利用大模型推导科学假说。
', 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'⚡ [引擎状态]: 正在向 arXiv 检索学术文献...
{full_stream_text}
', 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'🧠 [大模型 Channel]: 阿里通义 Qwen ({selected_model}) 正在深度推理假说中...
{full_stream_text}
', 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'🧠 [大模型 Channel]: 阿里通义 Qwen ({selected_model}) 实时输出 Token 流...
{full_stream_text}
', 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'{full_stream_text}
', 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'{st.session_state.gen_console_log}
', unsafe_allow_html=True)
# ---------------------------------------------------------
# 卡片 2:科学假说展现与导师干预编辑
# ---------------------------------------------------------
if st.session_state.current_hypo:
st.markdown("---")
st.markdown('💡 步骤 2:假说评估与人类导师先验介入
', unsafe_allow_html=True)
st.markdown('✍️ 操作指引:① 审阅下方 AI 生成的假说(可在候选假说下拉框中切换探索),可直接在编辑框中修改内容 ② 拖动 5 个评分 Slider 对假说进行预评估 ③ 填写导师意见(可选) ④ 点击 「🔍 提交对抗性审查打分」
', 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('⭐ 人类导师 5 维预评分面板(对假说进行初步评估,帮助您理解审查维度)
', 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'📌 {dim_desc}
', 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'🔍 [Reviewer #2 Channel]: 阿里通义 Qwen ({selected_model}) 正在进行 5 维对抗性审查...
{full_rev_text}
', 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'🔍 [Reviewer #2 Channel]: 阿里通义 Qwen ({selected_model}) 实时输出审查意见...
{full_rev_text}
', 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'{full_rev_text}
', 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'{st.session_state.review_console_log}
', unsafe_allow_html=True)
# ---------------------------------------------------------
# 卡片 3:审查判定结果与下一步引导
# ---------------------------------------------------------
if st.session_state.review_result:
rev = st.session_state.review_result
st.markdown("---")
st.markdown('📊 步骤 3:审查判定与决策指引
', unsafe_allow_html=True)
st.markdown('📊 操作指引:① 查看 AI Reviewer #2 的审查判定结果及 5 维打分 ② 对比您的预评分与 AI 审查分的差异 ③ 根据判定结果决定下一步:通过则导出报告,未通过则返回修改假说
', 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('', 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('', 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'📄 步骤 4:导出的《{clean_title}》
', unsafe_allow_html=True)
st.markdown('📥 操作指引:下方已为您生成完整的研究计划报告。您可以展开阅读全文,确认无误后点击底部 「⬇️ 下载」 按钮导出 Markdown 文档。
', 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'✓ {name}'
elif idx == current_stage_idx:
labels_html += f'▶ {name}'
else:
labels_html += f'{name}'
auto_stage_placeholder.markdown(
f'{labels_html}
',
unsafe_allow_html=True
)
# 信息行:当前步骤 + 预估时间 + 审查轮次
step_text = f'📍 当前第 {current_stage_idx + 1}/5 步'
# 基于已耗时粗略推算剩余时间
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'⏱ 已耗时 {int(elapsed_sec)}s | 预估剩余 ~{remain_min}m{remain_sec}s'
else:
time_text = f'⏱ 已耗时 {int(elapsed_sec)}s | 预估剩余 计算中...'
round_text = f'🔄 第 {current_round}/{max_r} 轮审查' if current_round > 0 else ''
auto_stage_info_placeholder.markdown(
f'{step_text}{time_text}{round_text}
',
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'🤖 [System Prompt]: 引擎启动,正向通义 Qwen ({selected_model}) 建立 Channel (最大审查: {max_rounds} 轮)...
'
auto_console.markdown(f'⚡ [24/7 自动发现引擎]: 通义 Qwen ({selected_model}) 实时 Channel 建立中...
{full_auto_text}
', 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 = "💡 [AI Hypothesis Generator]" if role == "LLM-HypothesisGenerator" else "😈 [Reviewer #2 Critic]"
bubble_class = "chat-bubble-hypo" if role == "LLM-HypothesisGenerator" else "chat-bubble-rev"
# 限制对话框显示长度以保持平滑
disp_dialogue = current_dialogue_content[-600:]
dialogue_html = f'''
🤖 [System Status]: 正在实时接收大模型 Token 流...
{role_label}:
{disp_dialogue}
'''
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'⚡ [24/7 自动发现引擎]: 正在多步逻辑推理中...
{full_auto_text}
', 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'{full_auto_text}
', 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'{st.session_state.auto_console_log}
', 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
)