Files
XH-202626/pages/3_⚖️_多智能体辩论诊断.py
T

541 lines
21 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
⚖️ 多智能体辩论诊断页面 (Premium Financial-Grade Design)
选择企业 → 真实 Token 级打字机流式推演 → 三方交叉质证 → 综合裁决与核保看板
使用 Glassmorphism 玻璃拟态 + 暗黑金融科技 CSS 调色盘
"""
import sys
import os
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
import streamlit as st
import plotly.graph_objects as go
import time
from collectors.financial_collector import get_all_companies, get_company_by_code
from agents.debate_engine import DebateEngine
st.set_page_config(page_title="多智能体辩论诊断", page_icon="⚖️", layout="wide")
# ============================================================
# 自定义 UI 视觉增强样式 (CSS 注入)
# ============================================================
st.markdown("""
<style>
/* 引入 Google 科技字体 */
@import url('https://fonts.googleapis.com/css2?family=Fira+Code:wght@400;600&family=Inter:wght@400;600;700&display=swap');
/* 顶栏 Hero 区域 */
.hero-banner {
background: linear-gradient(135deg, rgba(26, 26, 46, 0.95), rgba(22, 33, 62, 0.9), rgba(15, 52, 96, 0.95));
border: 1px solid rgba(233, 69, 96, 0.25);
border-radius: 16px;
padding: 24px 30px;
margin-bottom: 24px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.37);
backdrop-filter: blur(12px);
}
.hero-title {
color: #FFFFFF;
font-size: 2.0rem;
font-weight: 700;
margin: 0 0 8px 0;
display: flex;
align-items: center;
gap: 12px;
}
.hero-subtitle {
color: #94A3B8;
font-size: 1.0rem;
margin: 0;
}
.step-badge-container {
display: flex;
gap: 12px;
margin-top: 16px;
flex-wrap: wrap;
}
.step-badge {
background: rgba(255, 255, 255, 0.06);
border: 1px solid rgba(255, 255, 255, 0.12);
color: #CBD5E1;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.82rem;
font-weight: 500;
}
.step-badge-active {
background: rgba(233, 69, 96, 0.15);
border-color: #e94560;
color: #ff758c;
}
/* 智能体研判卡片 (Glassmorphism) */
.agent-card {
background: rgba(22, 33, 62, 0.7);
border: 1px solid rgba(255, 255, 255, 0.08);
border-radius: 14px;
padding: 20px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.25);
transition: all 0.3s ease;
height: 100%;
}
.agent-card:hover {
border-color: rgba(233, 69, 96, 0.4);
transform: translateY(-2px);
}
.agent-header {
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1px solid rgba(255, 255, 255, 0.08);
padding-bottom: 12px;
margin-bottom: 16px;
}
.agent-name {
font-size: 1.15rem;
font-weight: 700;
color: #F8FAFC;
}
/* 风险指标 Badge */
.risk-pill {
display: inline-block;
padding: 3px 8px;
border-radius: 4px;
font-size: 0.78rem;
font-weight: 600;
margin-left: 6px;
}
.pill-high { background: rgba(244, 67, 54, 0.2); color: #ff6b6b; border: 1px solid rgba(244, 67, 54, 0.4); }
.pill-med { background: rgba(255, 152, 0, 0.2); color: #ffb74d; border: 1px solid rgba(255, 152, 0, 0.4); }
.pill-low { background: rgba(76, 175, 80, 0.2); color: #81c784; border: 1px solid rgba(76, 175, 80, 0.4); }
/* 模拟黑客流式终端 (Terminal Box) */
.terminal-box {
background: #0B0E14;
border: 1px solid #1E293B;
border-radius: 10px;
padding: 14px;
font-family: 'Fira Code', monospace;
font-size: 0.84rem;
color: #38BDF8;
line-height: 1.5;
overflow-x: auto;
box-shadow: inset 0 2px 6px rgba(0, 0, 0, 0.8);
}
.terminal-thinking {
color: #94A3B8;
font-style: italic;
border-left: 2px solid #6366F1;
padding-left: 10px;
margin-bottom: 10px;
}
.terminal-output {
color: #4ADE80;
border-left: 2px solid #22C55E;
padding-left: 10px;
}
/* 裁决结果看板 */
.verdict-banner {
background: linear-gradient(135deg, #1E1B4B, #31103F);
border: 1px solid #6366F1;
border-radius: 14px;
padding: 24px;
text-align: center;
margin-top: 16px;
}
.verdict-title {
font-size: 1.8rem;
font-weight: 800;
letter-spacing: 1px;
}
</style>
""", unsafe_allow_html=True)
# ============================================================
# 页面顶部 Banner
# ============================================================
st.markdown("""
<div class="hero-banner">
<div class="hero-title">
<span>⚖️</span>
<span>多智能体交叉验证辩论诊断</span>
</div>
<div class="hero-subtitle">
基于 Multi-Agent 辩论图谱 · 法务 / 技术 / 财务专家分布式研判 · 全过程打字机流式追溯
</div>
<div class="step-badge-container">
<span class="step-badge step-badge-active">Phase 1: 独立穿透研判</span>
<span class="step-badge">Phase 2: 三方交叉质证</span>
<span class="step-badge">Phase 3: 委员会综合裁决</span>
<span class="step-badge">🛡️ 保险精算核保建议</span>
</div>
</div>
""", unsafe_allow_html=True)
from utils.session_helper import render_company_selector, render_sidebar_global_company_selector
with st.sidebar:
render_sidebar_global_company_selector()
st.markdown("---")
col_ctrl1, col_ctrl2 = st.columns([3, 1])
with col_ctrl1:
company = render_company_selector("🏢 选择待诊断科创企业", key_suffix="debate_page")
stock_code = company["stock_code"] if company else "688256"
# 初始化 session_state
if "debate_results" not in st.session_state:
st.session_state["debate_results"] = None
with col_ctrl2:
st.markdown("<div style='height: 28px;'></div>", unsafe_allow_html=True)
start_btn = st.button("🚀 启动多智能体辩论", type="primary", use_container_width=True)
# ============================================================
# 辩论触发与打字机流式渲染
# ============================================================
if start_btn:
company = get_company_by_code(stock_code)
if not company:
st.error("未找到企业数据")
st.stop()
engine = DebateEngine()
company_name = company.get("short_name", "未知")
st.markdown("---")
st.markdown("### 📡 智能体实时思考与流式推演控制台 (Live Streaming Terminal)")
progress_bar = st.progress(0, text="▶️ 正在初始化分布式智能体网络...")
# 动态渲染控制台
live_box = st.container(border=True)
def run_agent_with_typewriter(agent, agent_title, start_progress, end_progress):
progress_bar.progress(start_progress, text=f"{agent.role_icon} {agent_title} 正在穿透企业数据并与大模型通信...")
with live_box:
st.markdown(f"##### {agent.role_icon} {agent_title}")
status_placeholder = st.empty()
thinking_placeholder = st.empty()
content_placeholder = st.empty()
thinking_buf = []
content_buf = []
status_placeholder.info("🔗 建立 SSE 流式通信中...")
def on_token(token_type: str, token_text: str):
if token_type == "reasoning":
thinking_buf.append(token_text)
t_str = "".join(thinking_buf)
status_placeholder.markdown("🧠 **[大模型深度思考中...]**")
display_text = t_str[-350:] if len(t_str) > 350 else t_str
thinking_placeholder.markdown(
f"""<div class="terminal-box terminal-thinking">
<b>[THINKING]</b> {display_text}
</div>""",
unsafe_allow_html=True
)
elif token_type == "content":
content_buf.append(token_text)
c_str = "".join(content_buf)
status_placeholder.markdown("📝 **[正在实时生成审查报告...]**")
display_c = c_str[-280:] if len(c_str) > 280 else c_str
content_placeholder.markdown(
f"""<div class="terminal-box terminal-output">
<b>[REPORT OUTPUT]</b> {display_c}
</div>""",
unsafe_allow_html=True
)
agent.on_token_callback = on_token
result = agent.evaluate(company if agent != engine.judge_agent else None)
progress_bar.progress(end_progress, text=f"✅ {agent_title} 研判完毕")
status_placeholder.success(f"✅ {agent_title} 完成风险评级")
return result, list(agent.reasoning_trace)
# 1. 法务 Agent
law_result, law_trace = run_agent_with_typewriter(engine.law_agent, "法务风控节点", 10, 35)
# 2. 技术 Agent
tech_result, tech_trace = run_agent_with_typewriter(engine.tech_agent, "技术风控节点", 35, 60)
# 3. 财务 Agent
finance_result, finance_trace = run_agent_with_typewriter(engine.finance_agent, "财务风控节点", 60, 85)
# 4. 交叉质证
progress_bar.progress(88, text="🔄 正在比对三方判定结论,进行交叉质证分析...")
conflicts = engine._identify_conflicts(law_result, tech_result, finance_result)
# 5. 综合裁决 Agent
progress_bar.progress(92, text="⚖️ 综合裁决委员会进行权重复核...")
with live_box:
st.markdown("##### ⚖️ 综合裁决节点")
j_status = st.empty()
j_thinking = st.empty()
j_content = st.empty()
j_think_buf = []
j_cont_buf = []
j_status.info("🚀 综合裁决委员会正在消解分歧...")
def judge_on_token(token_type: str, token_text: str):
if token_type == "reasoning":
j_think_buf.append(token_text)
t_str = "".join(j_think_buf)
j_status.markdown("🧠 **[裁决委员会讨论中...]**")
display_t = t_str[-350:] if len(t_str) > 350 else t_str
j_thinking.markdown(
f"""<div class="terminal-box terminal-thinking">
<b>[COMMITTEE THOUGHTS]</b> {display_t}
</div>""",
unsafe_allow_html=True
)
elif token_type == "content":
j_cont_buf.append(token_text)
c_str = "".join(j_cont_buf)
j_status.markdown("📝 **[生成核保决议中...]**")
display_c = c_str[-280:] if len(c_str) > 280 else c_str
j_content.markdown(
f"""<div class="terminal-box terminal-output">
<b>[FINAL VERDICT]</b> {display_c}
</div>""",
unsafe_allow_html=True
)
engine.judge_agent.on_token_callback = judge_on_token
judge_result = engine.judge_agent.evaluate(company, law_result, tech_result, finance_result)
judge_trace = list(engine.judge_agent.reasoning_trace)
j_status.success("✅ 综合裁决完成")
progress_bar.progress(100, text="✅ 辩论诊断流程全量完成!")
# 持久化结果
st.session_state["debate_results"] = {
"company": company,
"law_result": law_result,
"tech_result": tech_result,
"finance_result": finance_result,
"law_trace": law_trace,
"tech_trace": tech_trace,
"finance_trace": finance_trace,
"judge_trace": judge_trace,
"conflicts": conflicts,
"judge_result": judge_result,
"debate_log": engine.debate_log,
}
# ============================================================
# 结果持久化渲染与面板展示
# ============================================================
def render_trace_expander(trace: list, title: str):
"""渲染研判结果对应的详细辩论链"""
with st.expander(f"🔗 查看 【{title}】 详细辩论链与凭据 (共 {len(trace)} 步)", expanded=False):
for step in trace:
ts = step.get("timestamp", "")
step_name = step.get("step", "")
content = step.get("content", "")
if "思维链" in step_name or "原始" in step_name or "Context" in step_name or len(content) > 150:
st.markdown(f"**`[{ts}]` {step_name}**")
st.code(content, language="text")
else:
st.markdown(f"- **`[{ts}]` {step_name}**: {content}")
def render_risk_card(result: dict, keys: list, trace: list, agent_title: str, overall_key: str):
"""渲染三方风控精美卡片"""
overall = result.get(overall_key, {}).get("score", 50)
level = result.get(overall_key, {}).get("level", "中")
pill_class = "pill-high" if level == "高" else ("pill-med" if level == "中" else "pill-low")
st.markdown(f"""
<div class="agent-card">
<div class="agent-header">
<span class="agent-name">{agent_title}</span>
<span><span class="risk-pill {pill_class}">{level}风险</span> <b>{overall}分</b></span>
</div>
""", unsafe_allow_html=True)
# 细分指标渲染
for key in keys:
item = result.get(key, {})
if isinstance(item, dict) and "detail" in item:
score = item.get("score", 0)
color = "#EF4444" if score >= 70 else ("#F59E0B" if score >= 40 else "#10B981")
st.markdown(
f"<div style='padding:8px 12px; border-radius:6px; margin:6px 0; "
f"border-left:4px solid {color}; background:rgba(255,255,255,0.03); font-size:0.88rem;'>"
f"<b style='color:{color};'>{score}分</b> — {item.get('detail', '')}</div>",
unsafe_allow_html=True,
)
# 关键发现
findings = result.get("key_findings", [])
if findings:
st.markdown("<p style='font-size:0.85rem; font-weight:700; color:#94A3B8; margin-top:12px; margin-bottom:4px;'>📌 关键风险发现:</p>", unsafe_allow_html=True)
for f in findings:
st.markdown(f"<span style='font-size:0.83rem; color:#E2E8F0;'>• {f}</span>", unsafe_allow_html=True)
# 建议
recs = result.get("recommendations", [])
if recs:
st.markdown("<p style='font-size:0.85rem; font-weight:700; color:#94A3B8; margin-top:10px; margin-bottom:4px;'>💡 专家处置建议:</p>", unsafe_allow_html=True)
for r in recs:
st.markdown(f"<span style='font-size:0.83rem; color:#CBD5E1;'>• {r}</span>", unsafe_allow_html=True)
st.markdown("</div>", unsafe_allow_html=True)
st.markdown("<br>", unsafe_allow_html=True)
render_trace_expander(trace, agent_title)
res = st.session_state.get("debate_results")
if res is not None:
company = res["company"]
law_result = res["law_result"]
tech_result = res["tech_result"]
finance_result = res["finance_result"]
conflicts = res["conflicts"]
judge_result = res["judge_result"]
st.markdown("---")
st.markdown(f"### 📊 多智能体交叉辩论诊断报告 — {company['short_name']} ({company['stock_code']})")
# ---- Phase 1: 三方独立研判 ----
st.markdown("#### 📋 Phase 1: 三方专家节点穿透研判全景")
col_law, col_tech, col_fin = st.columns(3)
with col_law:
render_risk_card(
law_result,
["algo_compliance_risk", "geopolitical_risk", "data_compliance_risk", "ip_litigation_risk"],
res.get("law_trace", []),
"👩‍⚖️ 法务风控节点",
"overall_law_risk",
)
with col_tech:
render_risk_card(
tech_result,
["tech_disruption_risk", "talent_loss_risk", "patent_moat", "tech_iteration_pressure"],
res.get("tech_trace", []),
"👨‍🔬 技术风控节点",
"overall_tech_risk",
)
with col_fin:
render_risk_card(
finance_result,
["rd_capitalization_risk", "concentration_risk", "receivable_risk", "cashflow_risk"],
res.get("finance_trace", []),
"👔 财务风控节点",
"overall_fin_risk",
)
# ---- Phase 2: 交叉质证分析 ----
st.markdown("---")
st.markdown("#### 🔄 Phase 2: 三方交叉质证与冲突判定")
if conflicts:
for conflict in conflicts:
st.markdown(f"""
<div style='background:rgba(239, 68, 68, 0.1); border:1px solid rgba(239, 68, 68, 0.3); border-radius:8px; padding:12px 16px; margin:6px 0; color:#FCA5A5;'>
⚠️ <b>判定冲突警示</b>{conflict}
</div>
""", unsafe_allow_html=True)
else:
st.markdown("""
<div style='background:rgba(16, 185, 129, 0.1); border:1px solid rgba(16, 185, 129, 0.3); border-radius:8px; padding:12px 16px; color:#6EE7B7;'>
✅ <b>一致性确认</b>:法务、技术、财务三方判定逻辑高度契合,无重大矛盾分歧。
</div>
""", unsafe_allow_html=True)
# ---- Phase 3: 综合裁决与保险核保 ----
st.markdown("---")
st.markdown("#### ⚖️ Phase 3: 风险委员会综合裁决与核保决策")
comp_score = judge_result.get("comprehensive_score", 50)
decision = judge_result.get("underwriting_decision", "标准承保")
risk_level = judge_result.get("risk_level", "中")
col_j1, col_j2 = st.columns([1, 1])
with col_j1:
# Plotly 仪表盘 (暗黑高精质感)
fig = go.Figure(go.Indicator(
mode="gauge+number",
value=comp_score,
title={"text": "综合风险指数 (0-100)", "font": {"color": "#F8FAFC", "size": 16}},
number={"font": {"color": "#FFFFFF", "size": 48}},
gauge={
"axis": {"range": [0, 100], "tickcolor": "#94A3B8"},
"bar": {"color": "#e94560", "thickness": 0.3},
"bgcolor": "rgba(0,0,0,0)",
"bordercolor": "rgba(255,255,255,0.1)",
"steps": [
{"range": [0, 40], "color": "rgba(16, 185, 129, 0.25)"},
{"range": [40, 60], "color": "rgba(245, 158, 11, 0.25)"},
{"range": [60, 80], "color": "rgba(249, 115, 22, 0.25)"},
{"range": [80, 100], "color": "rgba(239, 68, 68, 0.25)"},
],
},
))
fig.update_layout(
height=260,
margin=dict(l=20, r=20, t=40, b=10),
paper_bgcolor="rgba(0,0,0,0)",
font=dict(color="white"),
)
st.plotly_chart(fig, use_container_width=True)
with col_j2:
if "拒绝" in decision:
v_color = "#EF4444"
v_bg = "rgba(239, 68, 68, 0.15)"
elif "附条件" in decision:
v_color = "#F59E0B"
v_bg = "rgba(245, 158, 11, 0.15)"
elif "优先" in decision:
v_color = "#10B981"
v_bg = "rgba(16, 185, 129, 0.15)"
else:
v_color = "#3B82F6"
v_bg = "rgba(59, 130, 246, 0.15)"
st.markdown(f"""
<div class="verdict-banner" style="border-color:{v_color}; background:{v_bg};">
<div class="verdict-title" style="color:{v_color};">核保决策: 【{decision}】</div>
<p style="color:#CBD5E1; margin-top:8px;">综合风险等级: <b>{risk_level}</b> | 加权裁决得分: <b>{comp_score}分</b></p>
</div>
""", unsafe_allow_html=True)
st.markdown(f"**📝 裁决终审意见**: {judge_result.get('summary', '')}")
conditions = judge_result.get("underwriting_conditions", [])
if conditions:
st.markdown("**📌 保险核保附加条件**:")
for c in conditions:
st.markdown(f"- <span style='color:#F59E0B;'>{c}</span>", unsafe_allow_html=True)
# 挂载裁决节点的辩论链
st.markdown("<br>", unsafe_allow_html=True)
render_trace_expander(res.get("judge_trace", []), "⚖️ 综合裁决委员会")
else:
# 默认引导说明
st.markdown("""
<div style='background:rgba(255,255,255,0.02); border:1px solid rgba(255,255,255,0.08); border-radius:12px; padding:24px; margin-top:20px;'>
<h4 style='color:#F8FAFC; margin-top:0;'>💡 多智能体辩论与风险识别流程说明</h4>
<ol style='color:#94A3B8; line-height:1.8;'>
<li><b>Phase 1 - 三方穿透研判</b>: 法务风控节点审查算法与出口管制、技术风控节点审查路线与人员、财务风控节点穿透资本化与集中度。</li>
<li><b>Phase 2 - 交叉质证分析</b>: 识别法务、技术、财务意见间的潜在分歧(如:研发投入大 vs 资本化美化利润)。</li>
<li><b>Phase 3 - 委员会综合裁决</b>: 基于【合规 > 技术 > 财务】优先级规则进行加权综合评分并输出精算核保决议。</li>
</ol>
</div>
""", unsafe_allow_html=True)