feat: 初始提交 - 科创企业特有风险的识别与管理 (数智风控系统)
This commit is contained in:
@@ -0,0 +1,219 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
📊 企业风险概览页面
|
||||
选择企业 → 六维风险雷达图 + 关键指标卡片 + 详细财务数据
|
||||
"""
|
||||
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 plotly.express as px
|
||||
|
||||
from collectors.financial_collector import get_all_companies, get_company_by_code
|
||||
from risk_engine.risk_scorer import calculate_six_dimension_scores, get_risk_level
|
||||
|
||||
from utils.session_helper import render_company_selector, render_sidebar_global_company_selector
|
||||
|
||||
st.set_page_config(page_title="企业风险概览", page_icon="📊", layout="wide")
|
||||
|
||||
with st.sidebar:
|
||||
render_sidebar_global_company_selector()
|
||||
st.markdown("---")
|
||||
|
||||
st.markdown("# 📊 企业风险概览")
|
||||
st.markdown("选择一家科创企业,查看其六维风险画像和关键指标。")
|
||||
|
||||
# 企业选择(全局同步)
|
||||
company = render_company_selector("🏢 选择目标企业", key_suffix="overview_page")
|
||||
stock_code = company["stock_code"] if company else "688256"
|
||||
|
||||
if company:
|
||||
# 计算六维评分
|
||||
risk_result = calculate_six_dimension_scores(company)
|
||||
scores = risk_result["scores"]
|
||||
comprehensive = risk_result["comprehensive_score"]
|
||||
level_info = risk_result["risk_level"]
|
||||
|
||||
# ============================================================
|
||||
# 企业信息 + 综合评分
|
||||
# ============================================================
|
||||
col1, col2 = st.columns([2, 1])
|
||||
|
||||
with col1:
|
||||
st.markdown(f"### {company['short_name']}")
|
||||
st.markdown(f"**行业**: {company['industry']} | **领域**: {company['sector']} | **代码**: {company['stock_code']}")
|
||||
st.markdown(f"**简介**: {company['description']}")
|
||||
|
||||
# 核心人员
|
||||
st.markdown("#### 👤 核心技术人员")
|
||||
for p in company.get("core_tech_personnel", []):
|
||||
status_emoji = "✅" if "在职" in p["status"] else "⚠️"
|
||||
st.markdown(f"- {status_emoji} **{p['name']}** ({p['title']}) - 重要性: {p['importance']} - 状态: {p['status']}")
|
||||
|
||||
with col2:
|
||||
# 综合风险仪表盘
|
||||
fig = go.Figure(go.Indicator(
|
||||
mode="gauge+number",
|
||||
value=comprehensive,
|
||||
title={"text": "综合风险评分", "font": {"color": "white"}},
|
||||
number={"font": {"color": "white", "size": 48}},
|
||||
gauge={
|
||||
"axis": {"range": [0, 100], "tickcolor": "white"},
|
||||
"bar": {"color": level_info["color"]},
|
||||
"steps": [
|
||||
{"range": [0, 30], "color": "rgba(76,175,80,0.3)"},
|
||||
{"range": [30, 50], "color": "rgba(255,193,7,0.3)"},
|
||||
{"range": [50, 70], "color": "rgba(255,152,0,0.3)"},
|
||||
{"range": [70, 100], "color": "rgba(244,67,54,0.3)"},
|
||||
],
|
||||
"threshold": {
|
||||
"line": {"color": "red", "width": 4},
|
||||
"thickness": 0.75,
|
||||
"value": 70,
|
||||
},
|
||||
},
|
||||
))
|
||||
fig.update_layout(
|
||||
height=250,
|
||||
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)
|
||||
st.markdown(f"<div style='text-align:center; font-size:1.2em;'>"
|
||||
f"{level_info['emoji']} 风险等级: <b>{level_info['level']}</b></div>",
|
||||
unsafe_allow_html=True)
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
# ============================================================
|
||||
# 六维风险雷达图
|
||||
# ============================================================
|
||||
col_radar, col_detail = st.columns([1, 1])
|
||||
|
||||
with col_radar:
|
||||
st.markdown("#### 🎯 六维风险雷达图")
|
||||
|
||||
dim_names_cn = ["技术路线颠覆", "核心人员流失", "算法/数据合规",
|
||||
"地缘政治/出口管制", "研发资本化操纵", "客户/供应商集中"]
|
||||
dim_keys = ["tech_disruption", "talent_loss", "algo_compliance",
|
||||
"geopolitical", "rd_capitalization", "concentration"]
|
||||
values = [scores[k] for k in dim_keys]
|
||||
|
||||
fig = go.Figure()
|
||||
fig.add_trace(go.Scatterpolar(
|
||||
r=values + [values[0]],
|
||||
theta=dim_names_cn + [dim_names_cn[0]],
|
||||
fill="toself",
|
||||
fillcolor="rgba(233,69,96,0.3)",
|
||||
line=dict(color="#e94560", width=2),
|
||||
marker=dict(size=8, color="#e94560"),
|
||||
name=company["short_name"],
|
||||
))
|
||||
|
||||
# 添加警戒线
|
||||
fig.add_trace(go.Scatterpolar(
|
||||
r=[70] * 7,
|
||||
theta=dim_names_cn + [dim_names_cn[0]],
|
||||
line=dict(color="rgba(244,67,54,0.5)", dash="dash", width=1),
|
||||
name="高风险线(70)",
|
||||
))
|
||||
|
||||
fig.update_layout(
|
||||
polar=dict(
|
||||
radialaxis=dict(visible=True, range=[0, 100], tickfont=dict(color="white")),
|
||||
angularaxis=dict(tickfont=dict(color="white", size=11)),
|
||||
bgcolor="rgba(0,0,0,0)",
|
||||
),
|
||||
height=420,
|
||||
margin=dict(l=60, r=60, t=30, b=30),
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
font=dict(color="white"),
|
||||
showlegend=True,
|
||||
legend=dict(x=0, y=-0.15),
|
||||
)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
with col_detail:
|
||||
st.markdown("#### 📋 各维度风险详情")
|
||||
for k in dim_keys:
|
||||
detail = risk_result["dimension_details"][k]
|
||||
score = detail["score"]
|
||||
level = detail["level"]
|
||||
emoji = level["emoji"]
|
||||
color = level["color"]
|
||||
|
||||
st.markdown(
|
||||
f"<div style='background:{color}22; padding:10px; border-radius:8px; "
|
||||
f"margin:5px 0; border-left:4px solid {color};'>"
|
||||
f"<b>{emoji} {detail['name']}</b>: {score}分 ({level['level']})</div>",
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
# ============================================================
|
||||
# 关键财务指标
|
||||
# ============================================================
|
||||
st.markdown("#### 💰 关键财务指标")
|
||||
fin = company.get("financials", {})
|
||||
|
||||
col_f1, col_f2, col_f3, col_f4 = st.columns(4)
|
||||
with col_f1:
|
||||
revenue = fin.get("revenue_2024", 0)
|
||||
st.metric("营业收入", f"¥{revenue/1e8:.1f}亿")
|
||||
with col_f2:
|
||||
profit = fin.get("net_profit_2024", 0)
|
||||
st.metric("净利润", f"¥{profit/1e8:.1f}亿",
|
||||
delta="盈利" if profit > 0 else "亏损",
|
||||
delta_color="normal" if profit > 0 else "inverse")
|
||||
with col_f3:
|
||||
rd = fin.get("rd_expense_2024", 0)
|
||||
st.metric("研发费用", f"¥{rd/1e8:.1f}亿")
|
||||
with col_f4:
|
||||
cap_rate = fin.get("rd_capitalization_rate", 0)
|
||||
st.metric("研发资本化率", f"{cap_rate:.0%}",
|
||||
delta="⚠️ 偏高" if cap_rate > 0.3 else "正常",
|
||||
delta_color="inverse" if cap_rate > 0.3 else "normal")
|
||||
|
||||
col_f5, col_f6, col_f7, col_f8 = st.columns(4)
|
||||
with col_f5:
|
||||
st.metric("研发/营收比", f"{fin.get('rd_revenue_ratio', 0):.1%}")
|
||||
with col_f6:
|
||||
st.metric("前5大客户占比", f"{fin.get('top5_customer_ratio', 0):.0%}")
|
||||
with col_f7:
|
||||
st.metric("应收周转率", f"{fin.get('receivable_turnover', 0):.1f}次/年")
|
||||
with col_f8:
|
||||
st.metric("现金流比率", f"{fin.get('cash_flow_ratio', 0):.2f}")
|
||||
|
||||
# ============================================================
|
||||
# 技术路线 & 合规信息
|
||||
# ============================================================
|
||||
st.markdown("---")
|
||||
col_tech, col_comp = st.columns(2)
|
||||
|
||||
with col_tech:
|
||||
st.markdown("#### 🔬 技术路线")
|
||||
tech = company.get("tech_route", {})
|
||||
st.markdown(f"**当前技术**: {tech.get('current_tech', '未知')}")
|
||||
st.markdown(f"**技术壁垒**: {tech.get('tech_moat', '未知')}")
|
||||
st.markdown(f"**专利数量**: {tech.get('patent_count', 0)} 件")
|
||||
st.markdown("**竞争技术路线**:")
|
||||
for ct in tech.get("competing_techs", []):
|
||||
st.markdown(f" - ⚔️ {ct}")
|
||||
|
||||
with col_comp:
|
||||
st.markdown("#### 📋 合规状态")
|
||||
comp_info = company.get("compliance", {})
|
||||
st.markdown(f"**算法备案**: {comp_info.get('algo_filing_status', '未知')}")
|
||||
st.markdown(f"**数据出境风险**: {comp_info.get('data_export_risk', '未知')}")
|
||||
|
||||
entity_status = comp_info.get("entity_list_status", "未知")
|
||||
if "被列入" in entity_status:
|
||||
st.error(f"⛔ 实体清单: {entity_status}")
|
||||
if comp_info.get("sanctions_detail"):
|
||||
st.warning(f"制裁详情: {comp_info['sanctions_detail']}")
|
||||
else:
|
||||
st.success(f"✅ 实体清单: {entity_status}")
|
||||
@@ -0,0 +1,177 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
🕸️ 供应链知识图谱页面
|
||||
交互式图谱展示 + 供应链风险传染路径 + 实体清单命中标记
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
import streamlit as st
|
||||
import streamlit.components.v1 as components
|
||||
|
||||
from collectors.financial_collector import get_all_companies
|
||||
from knowledge_graph.graph_builder import build_graph, get_graph_stats
|
||||
from knowledge_graph.contagion_analyzer import analyze_contagion
|
||||
from knowledge_graph.graph_visualizer import (
|
||||
generate_interactive_graph,
|
||||
get_subgraph_for_company,
|
||||
)
|
||||
|
||||
st.set_page_config(page_title="供应链知识图谱", page_icon="🕸️", layout="wide")
|
||||
|
||||
st.markdown("# 🕸️ 供应链风险传染知识图谱")
|
||||
st.markdown("可视化科创企业供应链网络,识别风险传染路径和关键断裂节点。")
|
||||
|
||||
# 构建图谱
|
||||
@st.cache_resource
|
||||
def get_graph():
|
||||
return build_graph()
|
||||
|
||||
G = get_graph()
|
||||
stats = get_graph_stats(G)
|
||||
|
||||
# ============================================================
|
||||
# 图谱统计
|
||||
# ============================================================
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
with col1:
|
||||
st.metric("📌 总节点数", stats["total_nodes"])
|
||||
with col2:
|
||||
st.metric("🔗 总边数", stats["total_edges"])
|
||||
with col3:
|
||||
st.metric("⛔ 受制裁节点", stats["sanctioned_nodes"])
|
||||
with col4:
|
||||
st.metric("🔀 图密度", f"{stats['density']:.4f}")
|
||||
|
||||
# 节点类型分布
|
||||
with st.expander("📊 节点类型分布"):
|
||||
for ntype, count in stats["node_types"].items():
|
||||
st.markdown(f"- **{ntype}**: {count} 个")
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
# ============================================================
|
||||
# 图谱视图选择
|
||||
# ============================================================
|
||||
view_mode = st.radio(
|
||||
"🔍 视图模式",
|
||||
["全局图谱", "企业中心视图(推荐)"],
|
||||
horizontal=True,
|
||||
)
|
||||
|
||||
from utils.session_helper import render_company_selector, render_sidebar_global_company_selector
|
||||
|
||||
with st.sidebar:
|
||||
render_sidebar_global_company_selector()
|
||||
st.markdown("---")
|
||||
|
||||
if view_mode == "企业中心视图(推荐)":
|
||||
target_company = render_company_selector("🏢 选择中心企业", key_suffix="graph_page")
|
||||
selected_company = target_company["short_name"] if target_company else "寒武纪"
|
||||
|
||||
depth = st.slider("穿透深度", 1, 3, 2)
|
||||
|
||||
# 提取子图
|
||||
subgraph = get_subgraph_for_company(G, selected_company, depth=depth)
|
||||
|
||||
if subgraph.number_of_nodes() > 0:
|
||||
html_content = generate_interactive_graph(
|
||||
subgraph,
|
||||
highlight_company=selected_company,
|
||||
height="550px",
|
||||
)
|
||||
components.html(html_content, height=600, scrolling=True)
|
||||
|
||||
# ============================================================
|
||||
# 供应链风险传染分析
|
||||
# ============================================================
|
||||
st.markdown("---")
|
||||
st.markdown(f"### ⚠️ {selected_company} 供应链风险传染分析")
|
||||
|
||||
contagion = analyze_contagion(G, selected_company)
|
||||
|
||||
# 风险评分
|
||||
supply_risk_score = contagion.get("risk_score", 0)
|
||||
if supply_risk_score >= 60:
|
||||
st.error(f"🔴 供应链风险评分: **{supply_risk_score}/100** — 供应链断裂风险极高")
|
||||
elif supply_risk_score >= 30:
|
||||
st.warning(f"🟡 供应链风险评分: **{supply_risk_score}/100** — 存在一定供应链风险")
|
||||
else:
|
||||
st.success(f"🟢 供应链风险评分: **{supply_risk_score}/100** — 供应链风险可控")
|
||||
|
||||
# 直接风险
|
||||
if contagion.get("direct_risks"):
|
||||
st.markdown("#### 🔴 直接风险(一度关联)")
|
||||
for risk in contagion["direct_risks"]:
|
||||
icon = "⛔" if risk.get("status") == "受限" else "⚠️"
|
||||
st.markdown(
|
||||
f"- {icon} **{risk['entity']}** ({risk['node_type']}) — "
|
||||
f"{risk['relation']} — 状态: {risk.get('status', '未知')} "
|
||||
f"{'🔑 关键供应' if risk.get('is_critical') else ''}"
|
||||
)
|
||||
|
||||
# 间接风险
|
||||
if contagion.get("indirect_risks"):
|
||||
st.markdown("#### 🟡 间接风险(二度及以上关联)")
|
||||
for risk in contagion["indirect_risks"]:
|
||||
st.markdown(
|
||||
f"- ⚠️ **{risk['entity']}** (距离: {risk['distance']}层) — {risk['relation']}"
|
||||
)
|
||||
|
||||
# 传染路径
|
||||
if contagion.get("contagion_paths"):
|
||||
st.markdown("#### 🔗 风险传染路径")
|
||||
for path in contagion["contagion_paths"]:
|
||||
severity_icon = "🔴" if path["severity"] == "高" else "🟡"
|
||||
st.markdown(f"- {severity_icon} `{path['path_str']}` (长度: {path['length']})")
|
||||
|
||||
# 关键断裂节点
|
||||
if contagion.get("critical_nodes"):
|
||||
st.markdown("#### 🔑 关键断裂节点")
|
||||
for node in contagion["critical_nodes"]:
|
||||
alt_info = f"✅ 有{node['alternative_count']}个替代" if node["has_alternative"] else "❌ 无替代方案"
|
||||
st.markdown(
|
||||
f"- **{node['node']}** — {node['relation']} — "
|
||||
f"状态: {node['status']} — {alt_info}"
|
||||
)
|
||||
else:
|
||||
st.warning(f"未找到 {selected_company} 的相关图谱数据")
|
||||
|
||||
else:
|
||||
# 全局视图
|
||||
st.info("💡 全局图谱节点较多,加载可能需要几秒钟。推荐使用“企业中心视图”获得更好的体验。")
|
||||
html_content = generate_interactive_graph(G, height="650px")
|
||||
components.html(html_content, height=700, scrolling=True)
|
||||
|
||||
# ============================================================
|
||||
# 图例
|
||||
# ============================================================
|
||||
st.markdown("---")
|
||||
st.markdown("#### 🎨 图例说明")
|
||||
col_l1, col_l2, col_l3 = st.columns(3)
|
||||
with col_l1:
|
||||
st.markdown("""
|
||||
**节点颜色**
|
||||
- 🟢 绿色: 科创企业 (正常)
|
||||
- 🟡 金色: 选中的中心企业
|
||||
- 🟠 橙色: 供应商
|
||||
- 🟣 紫色: 客户
|
||||
- 🔵 蓝色: 核心人员
|
||||
- 🔴 红色: 受制裁实体
|
||||
""")
|
||||
with col_l2:
|
||||
st.markdown("""
|
||||
**边类型**
|
||||
- 实线: 正常关系
|
||||
- 红色虚线: 受限关系
|
||||
- 黄色粗线: 关键供应关系
|
||||
""")
|
||||
with col_l3:
|
||||
st.markdown("""
|
||||
**交互操作**
|
||||
- 鼠标悬浮: 查看节点详情
|
||||
- 拖拽: 移动节点
|
||||
- 滚轮: 缩放图谱
|
||||
- 双击: 聚焦节点
|
||||
""")
|
||||
@@ -0,0 +1,540 @@
|
||||
# -*- 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", "width": 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)
|
||||
@@ -0,0 +1,283 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
💰 动态定价与核保页面
|
||||
保险产品选择 → 费率计算器 → 核保报告生成
|
||||
"""
|
||||
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
|
||||
|
||||
from collectors.financial_collector import get_all_companies, get_company_by_code
|
||||
from risk_engine.risk_scorer import calculate_six_dimension_scores
|
||||
from risk_engine.dynamic_pricing import (
|
||||
calculate_premium, calculate_all_products,
|
||||
get_enterprise_scale, INDUSTRY_RISK_FACTORS,
|
||||
)
|
||||
from risk_engine.report_generator import generate_report, format_report_markdown
|
||||
from config import INSURANCE_PRODUCTS
|
||||
|
||||
st.set_page_config(page_title="动态定价与核保", page_icon="💰", layout="wide")
|
||||
|
||||
st.markdown("# 💰 科创特有风险综合险 · 动态定价引擎")
|
||||
st.markdown("基于六维风险评分的“千企千面”精准核保与定价。")
|
||||
|
||||
from utils.session_helper import render_company_selector, render_sidebar_global_company_selector
|
||||
|
||||
with st.sidebar:
|
||||
render_sidebar_global_company_selector()
|
||||
st.markdown("---")
|
||||
|
||||
# 企业选择(全局同步)
|
||||
company = render_company_selector("🏢 选择目标企业", key_suffix="pricing_page")
|
||||
stock_code = company["stock_code"] if company else "688256"
|
||||
|
||||
if company:
|
||||
# 计算风险评分
|
||||
risk_result = calculate_six_dimension_scores(company)
|
||||
scores = risk_result["scores"]
|
||||
comprehensive = risk_result["comprehensive_score"]
|
||||
level_info = risk_result["risk_level"]
|
||||
|
||||
revenue = company.get("financials", {}).get("revenue_2024", 0)
|
||||
sector = company.get("sector", "")
|
||||
scale = get_enterprise_scale(revenue)
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
# ============================================================
|
||||
# 风险概要
|
||||
# ============================================================
|
||||
col_info, col_score = st.columns([2, 1])
|
||||
|
||||
with col_info:
|
||||
st.markdown(f"### {company['short_name']}")
|
||||
st.markdown(f"**行业**: {company['industry']} | **领域**: {sector} | **规模**: {scale}")
|
||||
st.markdown(f"**营收**: ¥{revenue/1e8:.1f}亿 | **行业风险系数**: {INDUSTRY_RISK_FACTORS.get(sector, 1.0):.2f}")
|
||||
|
||||
with col_score:
|
||||
color = level_info["color"]
|
||||
st.markdown(
|
||||
f"<div style='background:{color}22; padding:20px; border-radius:12px; text-align:center; "
|
||||
f"border:2px solid {color};'>"
|
||||
f"<div style='font-size:2.5em; font-weight:bold; color:{color};'>{comprehensive}</div>"
|
||||
f"<div style='color:{color};'>综合风险评分 {level_info['emoji']} {level_info['level']}</div>"
|
||||
f"</div>",
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
# ============================================================
|
||||
# 保险产品费率计算
|
||||
# ============================================================
|
||||
st.markdown("### 📊 保险产品费率方案")
|
||||
|
||||
pricing_results = calculate_all_products(
|
||||
comprehensive, scores, sector, revenue
|
||||
)
|
||||
|
||||
# 三列展示三个险种
|
||||
cols = st.columns(3)
|
||||
for i, pricing in enumerate(pricing_results):
|
||||
with cols[i]:
|
||||
product = INSURANCE_PRODUCTS[pricing["product_key"]]
|
||||
is_insurable = pricing["is_insurable"]
|
||||
|
||||
if is_insurable:
|
||||
border_color = "#4CAF50" if comprehensive < 40 else ("#FF9800" if comprehensive < 70 else "#F44336")
|
||||
else:
|
||||
border_color = "#B71C1C"
|
||||
|
||||
st.markdown(
|
||||
f"<div style='background:#16213e; padding:20px; border-radius:12px; "
|
||||
f"border:2px solid {border_color}; min-height: 300px;'>"
|
||||
f"<h4 style='text-align:center; color:white;'>{product['name']}</h4>"
|
||||
f"<p style='color:#a8a8b3; font-size:0.85em; text-align:center;'>{product['description']}</p>"
|
||||
f"</div>",
|
||||
unsafe_allow_html=True,
|
||||
)
|
||||
|
||||
if is_insurable:
|
||||
st.metric("基础保费", f"¥{pricing['base_premium']:,.0f}")
|
||||
st.metric("最终保费", f"¥{pricing['final_premium']:,.0f}",
|
||||
delta=f"×{pricing['risk_multiplier']:.2f}",
|
||||
delta_color="inverse" if pricing['risk_multiplier'] > 1.2 else "normal")
|
||||
st.metric("保额", f"¥{pricing['adjusted_coverage']:,.0f}")
|
||||
st.metric("免赔率", f"{pricing['deductible_rate']:.0%}")
|
||||
|
||||
with st.expander("📐 定价明细"):
|
||||
st.markdown(f"- 风险系数: {pricing['risk_multiplier']:.3f}")
|
||||
st.markdown(f"- 行业调整: {pricing['industry_factor']:.3f}")
|
||||
st.markdown(f"- 规模折扣: {pricing['scale_factor']:.3f}")
|
||||
st.markdown(f"- 维度调整: {pricing['dimension_adjustment']:.3f}")
|
||||
st.markdown(f"- **计算公式**: {pricing['pricing_breakdown']}")
|
||||
else:
|
||||
st.error("⛔ 风险过高,建议拒保")
|
||||
|
||||
# ============================================================
|
||||
# 费率对比图
|
||||
# ============================================================
|
||||
st.markdown("---")
|
||||
st.markdown("### 📈 费率构成分析")
|
||||
|
||||
col_chart1, col_chart2 = st.columns(2)
|
||||
|
||||
with col_chart1:
|
||||
# 基础保费 vs 最终保费对比
|
||||
product_names = [p["product_name"] for p in pricing_results if p["is_insurable"]]
|
||||
base_premiums = [p["base_premium"] for p in pricing_results if p["is_insurable"]]
|
||||
final_premiums = [p["final_premium"] for p in pricing_results if p["is_insurable"]]
|
||||
|
||||
max_val = max(max(base_premiums, default=100000), max(final_premiums, default=100000))
|
||||
|
||||
fig = go.Figure(data=[
|
||||
go.Bar(
|
||||
name="基础保费",
|
||||
x=product_names,
|
||||
y=base_premiums,
|
||||
marker_color="#3B82F6",
|
||||
text=[f"¥{v:,.0f}" for v in base_premiums],
|
||||
textposition="outside",
|
||||
textfont=dict(size=11, color="#93C5FD")
|
||||
),
|
||||
go.Bar(
|
||||
name="调整后保费",
|
||||
x=product_names,
|
||||
y=final_premiums,
|
||||
marker_color="#EF4444",
|
||||
text=[f"¥{v:,.0f}" for v in final_premiums],
|
||||
textposition="outside",
|
||||
textfont=dict(size=11, color="#FCA5A5")
|
||||
),
|
||||
])
|
||||
fig.update_layout(
|
||||
title=dict(
|
||||
text="<b>📊 基础保费 vs 调整后保费对比</b>",
|
||||
font=dict(size=15, color="#F8FAFC"),
|
||||
x=0.02,
|
||||
y=0.96
|
||||
),
|
||||
barmode="group",
|
||||
height=380,
|
||||
margin=dict(l=20, r=20, t=75, b=30),
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font=dict(color="white"),
|
||||
legend=dict(
|
||||
x=0.02,
|
||||
y=0.88,
|
||||
orientation="h",
|
||||
bgcolor="rgba(0,0,0,0)",
|
||||
font=dict(color="#CBD5E1", size=12)
|
||||
),
|
||||
xaxis=dict(gridcolor="rgba(255,255,255,0.05)"),
|
||||
yaxis=dict(
|
||||
gridcolor="rgba(255,255,255,0.08)",
|
||||
range=[0, max_val * 1.3]
|
||||
),
|
||||
)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
with col_chart2:
|
||||
# 定价因子贡献瀑布图(选第一个可投保的产品)
|
||||
insurable = [p for p in pricing_results if p["is_insurable"]]
|
||||
if insurable:
|
||||
p = insurable[0]
|
||||
factors = ["基础保费", "风险系数", "行业调整", "规模折扣", "维度调整", "最终保费"]
|
||||
values = [
|
||||
p["base_premium"],
|
||||
p["base_premium"] * (p["risk_multiplier"] - 1),
|
||||
p["base_premium"] * p["risk_multiplier"] * (p["industry_factor"] - 1),
|
||||
p["base_premium"] * p["risk_multiplier"] * p["industry_factor"] * (p["scale_factor"] - 1),
|
||||
p["base_premium"] * p["risk_multiplier"] * p["industry_factor"] * p["scale_factor"] * (p["dimension_adjustment"] - 1),
|
||||
p["final_premium"],
|
||||
]
|
||||
measures = ["absolute", "relative", "relative", "relative", "relative", "total"]
|
||||
|
||||
fig2 = go.Figure(go.Waterfall(
|
||||
name=p["product_name"],
|
||||
orientation="v",
|
||||
measure=measures,
|
||||
x=factors,
|
||||
y=values,
|
||||
text=[f"¥{v:,.0f}" for v in values],
|
||||
textposition="outside",
|
||||
connector={"line": {"color": "rgba(255,255,255,0.3)"}},
|
||||
increasing={"marker": {"color": "#EF4444"}},
|
||||
decreasing={"marker": {"color": "#10B981"}},
|
||||
totals={"marker": {"color": "#3B82F6"}},
|
||||
))
|
||||
fig2.update_layout(
|
||||
title=dict(
|
||||
text=f"<b>📉 {p['product_name']} — 定价因子分解</b>",
|
||||
font=dict(size=15, color="#F8FAFC"),
|
||||
x=0.02,
|
||||
y=0.96
|
||||
),
|
||||
height=380,
|
||||
margin=dict(l=20, r=20, t=75, b=30),
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
font=dict(color="white"),
|
||||
xaxis=dict(gridcolor="rgba(255,255,255,0.05)"),
|
||||
yaxis=dict(gridcolor="rgba(255,255,255,0.08)"),
|
||||
)
|
||||
st.plotly_chart(fig2, use_container_width=True)
|
||||
|
||||
# ============================================================
|
||||
# 核保报告生成
|
||||
# ============================================================
|
||||
st.markdown("---")
|
||||
st.markdown("### 📋 核保决策报告")
|
||||
|
||||
# 检查 session_state 中是否有该企业的真实大模型辩论结果
|
||||
has_real_debate = False
|
||||
debate_res_in_session = st.session_state.get("debate_results")
|
||||
|
||||
if debate_res_in_session and debate_res_in_session.get("company", {}).get("stock_code") == stock_code:
|
||||
has_real_debate = True
|
||||
st.success("🤖 **已成功链接页面 3 的【大模型多智能体辩论】真实研判与穿透凭据!**")
|
||||
else:
|
||||
st.info("💡 **提示**:建议先至 **【⚖️ 多智能体辩论诊断】** 页面为该企业发起大模型辩论,本报告将自动整合最深度的 AI 审查凭据。")
|
||||
|
||||
if st.button("📄 生成完整核保报告", type="primary", use_container_width=True):
|
||||
if has_real_debate:
|
||||
debate_result = debate_res_in_session
|
||||
else:
|
||||
# 若尚未发起辩论,则回退到离线规则引擎评估
|
||||
from agents.law_agent import LawAgent
|
||||
from agents.tech_agent import TechAgent
|
||||
from agents.finance_agent import FinanceAgent
|
||||
from agents.judge_agent import JudgeAgent
|
||||
|
||||
law_result = LawAgent()._rule_based_evaluation(company)
|
||||
tech_result = TechAgent()._rule_based_evaluation(company)
|
||||
fin_result = FinanceAgent()._rule_based_evaluation(company)
|
||||
judge_result = JudgeAgent()._rule_based_evaluation(
|
||||
company, law_result, tech_result, fin_result
|
||||
)
|
||||
|
||||
debate_result = {
|
||||
"law_result": law_result,
|
||||
"tech_result": tech_result,
|
||||
"finance_result": fin_result,
|
||||
"judge_result": judge_result,
|
||||
"conflicts": [],
|
||||
}
|
||||
|
||||
report = generate_report(company, debate_result, pricing_results)
|
||||
markdown_report = format_report_markdown(report)
|
||||
|
||||
st.markdown(markdown_report)
|
||||
|
||||
# 下载按钮
|
||||
st.download_button(
|
||||
label="📥 下载核保报告 (Markdown 格式)",
|
||||
data=markdown_report,
|
||||
file_name=f"核保报告_{company['short_name']}_{report['report_id']}.md",
|
||||
mime="text/markdown",
|
||||
use_container_width=True,
|
||||
)
|
||||
Reference in New Issue
Block a user