916 lines
45 KiB
Python
916 lines
45 KiB
Python
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.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-bar {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
background-color: #f8fafc;
|
||
padding: 12px 20px;
|
||
border-radius: 8px;
|
||
margin-bottom: 20px;
|
||
border: 1px solid #e2e8f0;
|
||
}
|
||
.w-step { font-weight: 600; color: #94a3b8; }
|
||
.w-step.active { color: #6366f1; font-weight: 700; }
|
||
.w-step.done { color: #10b981; }
|
||
|
||
/* 重构 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. 顶部 Wizard 进度条
|
||
state = st.session_state.current_state
|
||
s1 = "w-step done" if state != "1_input" else "w-step active"
|
||
s2 = "w-step done" if state in ["3_reviewed", "4_report_done"] else ("w-step active" if state == "2_hypo_generated" else "w-step")
|
||
s3 = "w-step done" if state == "4_report_done" else ("w-step active" if state == "3_reviewed" else "w-step")
|
||
s4 = "w-step active" if state == "4_report_done" else "w-step"
|
||
|
||
st.markdown(f"""
|
||
<div class="wizard-bar">
|
||
<div class="{s1}">1. 输入科学难题</div>
|
||
<div>➔</div>
|
||
<div class="{s2}">2. 实时生成假说</div>
|
||
<div>➔</div>
|
||
<div class="{s3}">3. 对抗性审查打分</div>
|
||
<div>➔</div>
|
||
<div class="{s4}">4. 导出论文报告</div>
|
||
</div>
|
||
""", unsafe_allow_html=True)
|
||
|
||
# ---------------------------------------------------------
|
||
# 卡片 1:科学问题输入与生成
|
||
# ---------------------------------------------------------
|
||
st.markdown('<div class="step-title">📋 步骤 1:输入待探索的科学问题并生成假说</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)
|
||
|
||
full_stream_text += "\n\n[System Log]: 流式推导完成!正在解析 3 项结构化假说..."
|
||
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
|
||
|
||
try:
|
||
cleaned_res = raw_accumulated.strip()
|
||
if "```json" in raw_accumulated:
|
||
cleaned_res = raw_accumulated.split("```json")[1].split("```")[0].strip()
|
||
elif "```" in raw_accumulated:
|
||
cleaned_res = raw_accumulated.split("```")[1].split("```")[0].strip()
|
||
|
||
candidates = json.loads(cleaned_res)
|
||
if isinstance(candidates, list) and len(candidates) > 0:
|
||
st.session_state.current_hypo = candidates[0]
|
||
else:
|
||
st.session_state.current_hypo = hypo_skill.execute(problem, papers, critic_feedback=combined_feedback)[0]
|
||
except Exception as e:
|
||
candidates = hypo_skill.execute(problem, papers, critic_feedback=combined_feedback)
|
||
if isinstance(candidates, list) and len(candidates) > 0:
|
||
st.session_state.current_hypo = candidates[0]
|
||
|
||
gen_status_text.success("🎉 科学假说实时流式推导成功完成!")
|
||
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)
|
||
render_hypothesis_card(st.session_state.current_hypo, "💡 AI 实时推导产生的首选科学假说")
|
||
|
||
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
|
||
|
||
# 提交审查按钮
|
||
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:
|
||
|
||
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="terminal-box">{full_rev_text}▋</div>', unsafe_allow_html=True)
|
||
time.sleep(0.003)
|
||
|
||
full_rev_text += "\n\n[System Log]: Reviewer #2 审查完毕!正在解析 5 维结构化判定..."
|
||
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
|
||
|
||
try:
|
||
cleaned_rev = raw_rev_accumulated.strip()
|
||
if "```json" in raw_rev_accumulated:
|
||
cleaned_rev = raw_rev_accumulated.split("```json")[1].split("```")[0].strip()
|
||
elif "```" in raw_rev_accumulated:
|
||
cleaned_rev = raw_rev_accumulated.split("```")[1].split("```")[0].strip()
|
||
|
||
parsed_rev = json.loads(cleaned_rev)
|
||
st.session_state.review_result = parsed_rev
|
||
except Exception as e:
|
||
st.session_state.review_result = critic_skill.review(st.session_state.current_hypo, problem)
|
||
|
||
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)
|
||
render_review_card(rev)
|
||
|
||
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)
|
||
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)
|
||
|
||
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()
|
||
|
||
# 重置历史轮次
|
||
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(25)
|
||
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(50)
|
||
full_auto_text += f"\n[Stage 2 - 知识图谱]: {msg}\n"
|
||
elif stage == "3.假说生成" and status == "running":
|
||
progress_bar.progress(65)
|
||
current_dialogue_content = ""
|
||
full_auto_text += f"\n[Stage 3 - 假说生成]: {msg}\n[LLM Stream Output]: "
|
||
elif stage == "3.对抗审查" and status == "running":
|
||
progress_bar.progress(80)
|
||
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_hypo = event.get("hypothesis")
|
||
auto_review = event.get("review")
|
||
full_auto_text += f"\n\n[System Log]: {msg}\n"
|
||
|
||
# 审查完毕,按轮次顺序在下方追加渲染第 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)
|
||
full_auto_text += f"\n[Stage 4 - 报告编纂]: {msg}\n"
|
||
elif stage == "4.报告生成" and status == "completed":
|
||
progress_bar.progress(100)
|
||
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
|
||
)
|