feat: 初始提交 - 科创企业特有风险的识别与管理 (数智风控系统)
This commit is contained in:
+49
@@ -0,0 +1,49 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual environments
|
||||
venv/
|
||||
.venv/
|
||||
ENV/
|
||||
env/
|
||||
|
||||
# IDE & Editors
|
||||
.idea/
|
||||
.vscode/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Temporary deployment and package artifacts
|
||||
deploy_package.tar.gz
|
||||
|
||||
# Streamlit local credentials & state
|
||||
.streamlit/secrets.toml
|
||||
|
||||
# System files
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -0,0 +1,12 @@
|
||||
[client]
|
||||
toolbarMode = "minimal"
|
||||
showErrorDetails = false
|
||||
|
||||
[theme]
|
||||
primaryColor = "#e94560"
|
||||
backgroundColor = "#16213e"
|
||||
secondaryBackgroundColor = "#1a1a2e"
|
||||
textColor = "#ffffff"
|
||||
|
||||
[browser]
|
||||
gatherUsageStats = false
|
||||
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""多智能体辩论模块"""
|
||||
@@ -0,0 +1,203 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Agent 基类
|
||||
支持逐 Token 实时流式打字机输出 (Token-level UI Streaming)
|
||||
捕获思维链 (reasoning_content) 与最终生成结果 (content) 逐字推送到前端
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional, Callable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseAgent:
|
||||
"""智能体基类,支持 Token 级流式 LLM 推理与降级逻辑"""
|
||||
|
||||
def __init__(self, name: str, system_prompt: str, role_icon: str = "🤖"):
|
||||
self.name = name
|
||||
self.system_prompt = system_prompt
|
||||
self.role_icon = role_icon
|
||||
self._client = None
|
||||
# 推理链记录
|
||||
self.reasoning_trace = []
|
||||
# 逐 Token 实时回调函数: callback(token_type: "reasoning"|"content", token_text: str)
|
||||
self.on_token_callback: Optional[Callable[[str, str], None]] = None
|
||||
|
||||
def _get_client(self):
|
||||
"""延迟初始化 OpenAI 客户端"""
|
||||
if self._client is not None:
|
||||
return self._client
|
||||
|
||||
try:
|
||||
import httpx
|
||||
from openai import OpenAI
|
||||
import os
|
||||
|
||||
from config import (
|
||||
DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL, DEEPSEEK_MODEL,
|
||||
VOLCENGINE_API_KEY, VOLCENGINE_BASE_URL, VOLCENGINE_MODEL,
|
||||
OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL
|
||||
)
|
||||
|
||||
api_key = os.environ.get("DEEPSEEK_API_KEY", "")
|
||||
if api_key:
|
||||
base_url = DEEPSEEK_BASE_URL
|
||||
model = DEEPSEEK_MODEL
|
||||
else:
|
||||
api_key = VOLCENGINE_API_KEY
|
||||
base_url = VOLCENGINE_BASE_URL
|
||||
model = VOLCENGINE_MODEL
|
||||
|
||||
if not api_key:
|
||||
api_key = OPENAI_API_KEY
|
||||
base_url = OPENAI_BASE_URL
|
||||
model = OPENAI_MODEL
|
||||
|
||||
if api_key:
|
||||
http_client = httpx.Client(trust_env=False, timeout=60.0)
|
||||
self._client = OpenAI(
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
http_client=http_client,
|
||||
)
|
||||
self._model = model
|
||||
logger.info(f"[{self.name}] 已成功连接大模型服务")
|
||||
return self._client
|
||||
except ImportError:
|
||||
logger.warning("openai 库未安装")
|
||||
except Exception as e:
|
||||
logger.warning(f"初始化 LLM 客户端失败: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def _trace(self, step: str, content: str):
|
||||
"""记录推理链步骤"""
|
||||
entry = {
|
||||
"timestamp": time.strftime("%H:%M:%S"),
|
||||
"step": step,
|
||||
"content": content,
|
||||
"agent": self.name,
|
||||
"icon": self.role_icon
|
||||
}
|
||||
self.reasoning_trace.append(entry)
|
||||
|
||||
def infer(self, prompt: str, temperature: float = 0.1, max_retries: int = 0) -> str:
|
||||
"""
|
||||
执行 SSE 流式 LLM 推理 (stream=True)
|
||||
逐 Token 实时推送到 on_token_callback 渲染打字机效果
|
||||
"""
|
||||
self.reasoning_trace = []
|
||||
self._trace("📝 构建 Context", f"准备【{self.name}】数据与 Prompt")
|
||||
|
||||
client = self._get_client()
|
||||
if client is None:
|
||||
self._trace("⚠️ 状态通知", "大模型未就绪,切换至专家规则引擎")
|
||||
logger.info(f"[{self.name}] LLM 不可用,降级到规则引擎")
|
||||
fallback = self.fallback_inference(prompt)
|
||||
self._trace("🔧 专家引擎输出", fallback)
|
||||
return fallback
|
||||
|
||||
self._trace("🔗 大模型连接", "已连接大模型推理服务")
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
self._trace("🚀 发起流式推理", "正在建立 SSE 流式传输通道...")
|
||||
t0 = time.time()
|
||||
|
||||
stream_resp = client.chat.completions.create(
|
||||
model=self._model,
|
||||
messages=[
|
||||
{"role": "system", "content": self.system_prompt},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
temperature=temperature,
|
||||
max_tokens=2048,
|
||||
stream=True,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
full_content = []
|
||||
reasoning_chunks = []
|
||||
|
||||
for chunk in stream_resp:
|
||||
if not chunk.choices:
|
||||
continue
|
||||
delta = chunk.choices[0].delta
|
||||
|
||||
# 1. 逐 Token 提取深度思考过程 (reasoning_content)
|
||||
reasoning_piece = getattr(delta, "reasoning_content", None) or getattr(delta, "reasoning", None)
|
||||
if reasoning_piece:
|
||||
reasoning_chunks.append(reasoning_piece)
|
||||
if self.on_token_callback:
|
||||
try:
|
||||
self.on_token_callback("reasoning", reasoning_piece)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. 逐 Token 提取正式回答内容 (content)
|
||||
content_piece = delta.content
|
||||
if content_piece:
|
||||
full_content.append(content_piece)
|
||||
if self.on_token_callback:
|
||||
try:
|
||||
self.on_token_callback("content", content_piece)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
elapsed = time.time() - t0
|
||||
final_text = "".join(full_content)
|
||||
full_reasoning = "".join(reasoning_chunks)
|
||||
|
||||
if full_reasoning:
|
||||
self._trace("🧠 完整思维链", full_reasoning)
|
||||
|
||||
if final_text.strip():
|
||||
self._trace("✅ 流式生成完毕", f"耗时 {elapsed:.1f}s | 产出 {len(final_text)} 字符")
|
||||
self._trace("📄 原始推理输出", final_text)
|
||||
return final_text
|
||||
else:
|
||||
self._trace("⚠️ 输出为空", "流式生成无有效内容,降级到专家引擎")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
self._trace("⚡ 流式传输异常", f"连接中断: {error_msg}")
|
||||
logger.warning(f"[{self.name}] 流式调用失败: {e}")
|
||||
|
||||
self._trace("🛡️ 安全降级", "无缝切换至离线风控规则引擎")
|
||||
fallback = self.fallback_inference(prompt)
|
||||
self._trace("🔧 专家引擎输出", fallback)
|
||||
return fallback
|
||||
|
||||
def infer_json(self, prompt: str, temperature: float = 0.1) -> dict:
|
||||
"""
|
||||
执行 LLM 推理并解析为 JSON
|
||||
"""
|
||||
result = self.infer(prompt, temperature)
|
||||
try:
|
||||
if "```json" in result:
|
||||
json_str = result.split("```json")[1].split("```")[0].strip()
|
||||
parsed = json.loads(json_str)
|
||||
self._trace("✅ 结构解析", "从 Markdown 成功提取 JSON 数据")
|
||||
return parsed
|
||||
elif "```" in result:
|
||||
json_str = result.split("```")[1].split("```")[0].strip()
|
||||
parsed = json.loads(json_str)
|
||||
self._trace("✅ 结构解析", "从代码块成功提取 JSON 数据")
|
||||
return parsed
|
||||
else:
|
||||
parsed = json.loads(result)
|
||||
self._trace("✅ 结构解析", "直接解析 JSON 成功")
|
||||
return parsed
|
||||
except (json.JSONDecodeError, IndexError):
|
||||
self._trace("⚠️ 格式适配", "启用自动结构修正")
|
||||
logger.warning(f"[{self.name}] JSON 解析失败,返回原始文本")
|
||||
return {"raw_response": result, "parse_error": True}
|
||||
|
||||
def fallback_inference(self, prompt: str) -> str:
|
||||
"""规则引擎降级推理"""
|
||||
return json.dumps({"error": "大模型服务不可用,规则引擎未实现"}, ensure_ascii=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.role_icon} {self.name}"
|
||||
@@ -0,0 +1,178 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
多智能体辩论编排引擎
|
||||
流程:信息分发 → 独立研判 → 交叉质证 → 综合裁决
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from .law_agent import LawAgent
|
||||
from .tech_agent import TechAgent
|
||||
from .finance_agent import FinanceAgent
|
||||
from .judge_agent import JudgeAgent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DebateEngine:
|
||||
"""多智能体辩论编排器"""
|
||||
|
||||
def __init__(self):
|
||||
self.law_agent = LawAgent()
|
||||
self.tech_agent = TechAgent()
|
||||
self.finance_agent = FinanceAgent()
|
||||
self.judge_agent = JudgeAgent()
|
||||
self.debate_log = []
|
||||
|
||||
def run_debate(self, company_data: dict, callback=None) -> dict:
|
||||
"""
|
||||
执行完整的多智能体辩论流程
|
||||
|
||||
Args:
|
||||
company_data: 企业数据字典
|
||||
callback: 进度回调函数 callback(step, message, result)
|
||||
"""
|
||||
self.debate_log = []
|
||||
company_name = company_data.get("short_name", company_data.get("company_name", "未知"))
|
||||
start_time = time.time()
|
||||
|
||||
self._log(f"🏁 启动对 [{company_name}] 的多智能体交叉验证辩论")
|
||||
|
||||
# ==============================
|
||||
# Phase 1: 独立研判
|
||||
# ==============================
|
||||
self._log("=" * 50)
|
||||
self._log("📋 Phase 1: 各节点独立研判")
|
||||
self._log("=" * 50)
|
||||
|
||||
# 法务节点
|
||||
self._log("👩⚖️ 法务风控节点开始评估...")
|
||||
if callback:
|
||||
callback("law_start", "法务风控节点开始评估...", None)
|
||||
law_result = self.law_agent.evaluate(company_data)
|
||||
self._log(f"👩⚖️ 法务节点完成: 综合法务风险 {law_result.get('overall_law_risk', {}).get('score', '?')} 分")
|
||||
if callback:
|
||||
callback("law_done", "法务风控节点评估完成", law_result)
|
||||
|
||||
# 技术节点
|
||||
self._log("👨🔬 技术风控节点开始评估...")
|
||||
if callback:
|
||||
callback("tech_start", "技术风控节点开始评估...", None)
|
||||
tech_result = self.tech_agent.evaluate(company_data)
|
||||
self._log(f"👨🔬 技术节点完成: 综合技术风险 {tech_result.get('overall_tech_risk', {}).get('score', '?')} 分")
|
||||
if callback:
|
||||
callback("tech_done", "技术风控节点评估完成", tech_result)
|
||||
|
||||
# 财务节点
|
||||
self._log("👔 财务风控节点开始评估...")
|
||||
if callback:
|
||||
callback("fin_start", "财务风控节点开始评估...", None)
|
||||
finance_result = self.finance_agent.evaluate(company_data)
|
||||
self._log(f"👔 财务节点完成: 综合财务风险 {finance_result.get('overall_fin_risk', {}).get('score', '?')} 分")
|
||||
if callback:
|
||||
callback("fin_done", "财务风控节点评估完成", finance_result)
|
||||
|
||||
# ==============================
|
||||
# Phase 2: 交叉质证(记录冲突点)
|
||||
# ==============================
|
||||
self._log("=" * 50)
|
||||
self._log("🔄 Phase 2: 交叉质证")
|
||||
self._log("=" * 50)
|
||||
|
||||
conflicts = self._identify_conflicts(law_result, tech_result, finance_result)
|
||||
for conflict in conflicts:
|
||||
self._log(f"⚠️ 冲突: {conflict}")
|
||||
if not conflicts:
|
||||
self._log("✅ 各节点意见一致,无冲突")
|
||||
|
||||
if callback:
|
||||
callback("cross_validation", "交叉质证完成", {"conflicts": conflicts})
|
||||
|
||||
# ==============================
|
||||
# Phase 3: 综合裁决
|
||||
# ==============================
|
||||
self._log("=" * 50)
|
||||
self._log("⚖️ Phase 3: 综合裁决")
|
||||
self._log("=" * 50)
|
||||
|
||||
if callback:
|
||||
callback("judge_start", "综合裁决节点开始...", None)
|
||||
judge_result = self.judge_agent.evaluate(
|
||||
company_data, law_result, tech_result, finance_result
|
||||
)
|
||||
self._log(f"⚖️ 综合评分: {judge_result.get('comprehensive_score', '?')} 分")
|
||||
self._log(f"⚖️ 核保建议: 【{judge_result.get('underwriting_decision', '?')}】")
|
||||
if callback:
|
||||
callback("judge_done", "综合裁决完成", judge_result)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
self._log(f"🏁 辩论完成,耗时 {elapsed:.1f} 秒")
|
||||
|
||||
return {
|
||||
"company": company_name,
|
||||
"law_result": law_result,
|
||||
"tech_result": tech_result,
|
||||
"finance_result": finance_result,
|
||||
"conflicts": conflicts,
|
||||
"judge_result": judge_result,
|
||||
"debate_log": self.debate_log,
|
||||
"elapsed_seconds": round(elapsed, 1),
|
||||
}
|
||||
|
||||
def _identify_conflicts(self, law_result: dict, tech_result: dict, finance_result: dict) -> list:
|
||||
"""识别各节点之间的判定冲突"""
|
||||
conflicts = []
|
||||
|
||||
# 检查法务和技术的冲突:例如法务认为合规但技术认为路线有风险
|
||||
law_overall = law_result.get("overall_law_risk", {}).get("score", 50)
|
||||
tech_overall = tech_result.get("overall_tech_risk", {}).get("score", 50)
|
||||
fin_overall = finance_result.get("overall_fin_risk", {}).get("score", 50)
|
||||
|
||||
# 大幅分歧(差异超过30分)
|
||||
if abs(law_overall - tech_overall) > 30:
|
||||
if law_overall > tech_overall:
|
||||
conflicts.append(
|
||||
f"法务节点({law_overall}分)与技术节点({tech_overall}分)存在较大分歧: "
|
||||
f"法务认为合规风险较高,但技术面评估相对乐观"
|
||||
)
|
||||
else:
|
||||
conflicts.append(
|
||||
f"技术节点({tech_overall}分)与法务节点({law_overall}分)存在较大分歧: "
|
||||
f"技术风险较高,但法务合规状态相对可控"
|
||||
)
|
||||
|
||||
if abs(tech_overall - fin_overall) > 30:
|
||||
conflicts.append(
|
||||
f"技术节点({tech_overall}分)与财务节点({fin_overall}分)存在分歧: "
|
||||
f"需审查技术投入与财务表现的匹配度"
|
||||
)
|
||||
|
||||
# 特定维度冲突:技术认为研发投入大=好事,财务可能认为是资本化操纵
|
||||
tech_rd_view = tech_result.get("tech_iteration_pressure", {}).get("score", 50)
|
||||
fin_rd_view = finance_result.get("rd_capitalization_risk", {}).get("score", 50)
|
||||
if tech_rd_view < 40 and fin_rd_view > 60:
|
||||
conflicts.append(
|
||||
"技术节点认为研发投入合理,但财务节点发现研发资本化率异常,"
|
||||
"存在通过资本化手段美化利润的嫌疑"
|
||||
)
|
||||
|
||||
return conflicts
|
||||
|
||||
def _log(self, message: str):
|
||||
"""记录辩论日志"""
|
||||
entry = {"timestamp": time.strftime("%H:%M:%S"), "message": message}
|
||||
self.debate_log.append(entry)
|
||||
logger.info(message)
|
||||
|
||||
|
||||
def run_debate(stock_code: str) -> dict:
|
||||
"""便捷接口:通过股票代码直接运行辩论"""
|
||||
from collectors.financial_collector import get_company_by_code
|
||||
company_data = get_company_by_code(stock_code)
|
||||
if not company_data:
|
||||
return {"error": f"未找到股票代码 {stock_code} 的企业数据"}
|
||||
|
||||
engine = DebateEngine()
|
||||
return engine.run_debate(company_data)
|
||||
@@ -0,0 +1,174 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
财务风控节点
|
||||
审查维度:研发资本化操纵、客户/供应商集中度、应收账款质量、现金流
|
||||
"""
|
||||
import json
|
||||
from .base_agent import BaseAgent
|
||||
|
||||
|
||||
FIN_SYSTEM_PROMPT = """你是一名精通科创板审计规则的注册会计师(CPA),同时是金融风控专家。
|
||||
|
||||
你的任务是基于提供的企业财务数据,从财务角度穿透审查以下风险:
|
||||
1. 研发资本化操纵风险:研发资本化率是否异常,是否存在美化利润嫌疑
|
||||
2. 客户/供应商集中风险:前五大客户/供应商占比是否过高
|
||||
3. 应收账款质量:应收账款周转率是否异常,是否存在坏账风险
|
||||
4. 现金流健康度:经营现金流是否能覆盖运营需求
|
||||
|
||||
评估标准参考:
|
||||
- 科创板企业研发资本化率超过30%需重点关注
|
||||
- 前五大客户占比超过50%存在集中风险
|
||||
- 应收账款周转率低于4次/年需关注回款能力
|
||||
- 经营现金流/营收比低于0.5需关注持续经营能力
|
||||
|
||||
请输出JSON格式:
|
||||
{
|
||||
"agent": "财务风控节点",
|
||||
"rd_capitalization_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"concentration_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"receivable_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"cashflow_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"overall_fin_risk": {"score": 0-100, "level": "高/中/低"},
|
||||
"key_findings": ["..."],
|
||||
"recommendations": ["..."]
|
||||
}"""
|
||||
|
||||
|
||||
class FinanceAgent(BaseAgent):
|
||||
"""财务风控节点"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="财务风控节点",
|
||||
system_prompt=FIN_SYSTEM_PROMPT,
|
||||
role_icon="👔",
|
||||
)
|
||||
|
||||
def evaluate(self, company_data: dict) -> dict:
|
||||
"""执行财务风险评估"""
|
||||
prompt = self._build_prompt(company_data)
|
||||
result = self.infer_json(prompt)
|
||||
|
||||
if result.get("parse_error"):
|
||||
result = self._rule_based_evaluation(company_data)
|
||||
|
||||
result["agent"] = "财务风控节点"
|
||||
result["icon"] = self.role_icon
|
||||
return result
|
||||
|
||||
def _build_prompt(self, company_data: dict) -> str:
|
||||
financials = company_data.get("financials", {})
|
||||
return f"""请对以下科创企业进行财务风险穿透审查:
|
||||
|
||||
企业名称:{company_data.get('short_name', '未知')}
|
||||
行业:{company_data.get('industry', '未知')}
|
||||
|
||||
财务核心指标:
|
||||
- 营业收入: {financials.get('revenue_2024', 0):,.0f} 元
|
||||
- 净利润: {financials.get('net_profit_2024', 0):,.0f} 元
|
||||
- 研发费用: {financials.get('rd_expense_2024', 0):,.0f} 元
|
||||
- 研发资本化率: {financials.get('rd_capitalization_rate', 0):.1%}
|
||||
- 研发/营收比: {financials.get('rd_revenue_ratio', 0):.1%}
|
||||
- 前五大客户占比: {financials.get('top5_customer_ratio', 0):.1%}
|
||||
- 前五大供应商占比: {financials.get('top5_supplier_ratio', 0):.1%}
|
||||
- 应收账款周转率: {financials.get('receivable_turnover', 0):.1f} 次/年
|
||||
- 经营现金流比率: {financials.get('cash_flow_ratio', 0):.2f}
|
||||
|
||||
请输出严格的JSON评估结果。"""
|
||||
|
||||
def _rule_based_evaluation(self, company_data: dict) -> dict:
|
||||
"""基于规则的财务风险评估"""
|
||||
fin = company_data.get("financials", {})
|
||||
|
||||
# 1. 研发资本化操纵风险
|
||||
cap_rate = fin.get("rd_capitalization_rate", 0)
|
||||
if cap_rate >= 0.4:
|
||||
rd_score = 90
|
||||
rd_detail = f"研发资本化率高达{cap_rate:.0%},严重怀疑美化利润"
|
||||
elif cap_rate >= 0.3:
|
||||
rd_score = 70
|
||||
rd_detail = f"研发资本化率{cap_rate:.0%},超过行业警戒线(30%),需重点审查"
|
||||
elif cap_rate >= 0.15:
|
||||
rd_score = 45
|
||||
rd_detail = f"研发资本化率{cap_rate:.0%},处于中等水平,建议关注趋势"
|
||||
elif cap_rate > 0:
|
||||
rd_score = 25
|
||||
rd_detail = f"研发资本化率{cap_rate:.0%},处于合理范围"
|
||||
else:
|
||||
rd_score = 10
|
||||
rd_detail = "研发费用全部费用化处理,财务政策审慎"
|
||||
|
||||
# 2. 集中度风险
|
||||
customer_ratio = fin.get("top5_customer_ratio", 0)
|
||||
supplier_ratio = fin.get("top5_supplier_ratio", 0)
|
||||
max_concentration = max(customer_ratio, supplier_ratio)
|
||||
|
||||
if max_concentration >= 0.8:
|
||||
conc_score = 90
|
||||
conc_detail = f"前五大客户占比{customer_ratio:.0%},供应商占比{supplier_ratio:.0%},集中度极高"
|
||||
elif max_concentration >= 0.6:
|
||||
conc_score = 70
|
||||
conc_detail = f"前五大客户占比{customer_ratio:.0%},供应商占比{supplier_ratio:.0%},集中度偏高"
|
||||
elif max_concentration >= 0.4:
|
||||
conc_score = 45
|
||||
conc_detail = f"前五大客户占比{customer_ratio:.0%},供应商占比{supplier_ratio:.0%},中等集中度"
|
||||
else:
|
||||
conc_score = 20
|
||||
conc_detail = f"客户和供应商分布较为分散"
|
||||
|
||||
# 3. 应收账款风险
|
||||
turnover = fin.get("receivable_turnover", 8)
|
||||
if turnover < 3:
|
||||
recv_score = 80
|
||||
recv_detail = f"应收账款周转率仅{turnover:.1f}次/年,回款能力极差"
|
||||
elif turnover < 5:
|
||||
recv_score = 55
|
||||
recv_detail = f"应收账款周转率{turnover:.1f}次/年,回款速度偏慢"
|
||||
elif turnover < 8:
|
||||
recv_score = 30
|
||||
recv_detail = f"应收账款周转率{turnover:.1f}次/年,回款能力尚可"
|
||||
else:
|
||||
recv_score = 15
|
||||
recv_detail = f"应收账款周转率{turnover:.1f}次/年,回款能力良好"
|
||||
|
||||
# 4. 现金流风险
|
||||
cf_ratio = fin.get("cash_flow_ratio", 1.0)
|
||||
if cf_ratio < 0.5:
|
||||
cf_score = 80
|
||||
cf_detail = f"经营现金流比率仅{cf_ratio:.2f},存在持续经营风险"
|
||||
elif cf_ratio < 0.8:
|
||||
cf_score = 55
|
||||
cf_detail = f"经营现金流比率{cf_ratio:.2f},现金流偏紧"
|
||||
elif cf_ratio < 1.2:
|
||||
cf_score = 30
|
||||
cf_detail = f"经营现金流比率{cf_ratio:.2f},基本健康"
|
||||
else:
|
||||
cf_score = 15
|
||||
cf_detail = f"经营现金流比率{cf_ratio:.2f},现金流充裕"
|
||||
|
||||
overall = int(rd_score * 0.30 + conc_score * 0.30 +
|
||||
recv_score * 0.20 + cf_score * 0.20)
|
||||
|
||||
findings = [rd_detail, conc_detail]
|
||||
if recv_score >= 50:
|
||||
findings.append(recv_detail)
|
||||
if cf_score >= 50:
|
||||
findings.append(cf_detail)
|
||||
|
||||
return {
|
||||
"rd_capitalization_risk": {"score": rd_score, "level": self._level(rd_score), "detail": rd_detail},
|
||||
"concentration_risk": {"score": conc_score, "level": self._level(conc_score), "detail": conc_detail},
|
||||
"receivable_risk": {"score": recv_score, "level": self._level(recv_score), "detail": recv_detail},
|
||||
"cashflow_risk": {"score": cf_score, "level": self._level(cf_score), "detail": cf_detail},
|
||||
"overall_fin_risk": {"score": overall, "level": self._level(overall)},
|
||||
"key_findings": findings,
|
||||
"recommendations": ["关注研发资本化率变化趋势", "降低客户集中度风险"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _level(score: int) -> str:
|
||||
if score >= 70:
|
||||
return "高"
|
||||
elif score >= 40:
|
||||
return "中"
|
||||
return "低"
|
||||
@@ -0,0 +1,176 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
综合裁决节点
|
||||
汇总法务/技术/财务三方意见,消解冲突,输出最终综合评级
|
||||
"""
|
||||
import json
|
||||
from .base_agent import BaseAgent
|
||||
|
||||
|
||||
JUDGE_SYSTEM_PROMPT = """你是一名资深的风险管理委员会主席,负责汇总法务、技术、财务三方专家的研判意见。
|
||||
|
||||
你的任务是:
|
||||
1. 审阅三方专家的评估报告
|
||||
2. 识别各方意见的冲突点
|
||||
3. 基于优先级原则消解冲突(合规风险 > 技术风险 > 财务风险)
|
||||
4. 输出 0-100 的综合风险评分
|
||||
5. 给出最终核保建议
|
||||
|
||||
核保决策标准:
|
||||
- 综合风险 ≥ 80分:建议【拒绝承保】
|
||||
- 60 ≤ 综合风险 < 80分:建议【附条件承保】(高免赔额/限额)
|
||||
- 40 ≤ 综合风险 < 60分:建议【标准承保】(标准费率上浮)
|
||||
- 综合风险 < 40分:建议【优先承保】(可享费率优惠)
|
||||
|
||||
请输出JSON格式:
|
||||
{
|
||||
"comprehensive_score": 0-100,
|
||||
"risk_level": "极高/高/中/低",
|
||||
"underwriting_decision": "拒绝承保/附条件承保/标准承保/优先承保",
|
||||
"six_dimension_scores": {
|
||||
"tech_disruption": 0-100,
|
||||
"talent_loss": 0-100,
|
||||
"algo_compliance": 0-100,
|
||||
"geopolitical": 0-100,
|
||||
"rd_capitalization": 0-100,
|
||||
"concentration": 0-100
|
||||
},
|
||||
"conflict_resolution": "...",
|
||||
"key_risks": ["..."],
|
||||
"underwriting_conditions": ["..."],
|
||||
"summary": "..."
|
||||
}"""
|
||||
|
||||
|
||||
class JudgeAgent(BaseAgent):
|
||||
"""综合裁决节点"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="综合裁决节点",
|
||||
system_prompt=JUDGE_SYSTEM_PROMPT,
|
||||
role_icon="⚖️",
|
||||
)
|
||||
|
||||
def evaluate(self, company_data: dict, law_result: dict,
|
||||
tech_result: dict, finance_result: dict) -> dict:
|
||||
"""汇总三方意见,输出综合裁决"""
|
||||
prompt = self._build_prompt(company_data, law_result, tech_result, finance_result)
|
||||
result = self.infer_json(prompt)
|
||||
|
||||
if result.get("parse_error"):
|
||||
result = self._rule_based_evaluation(company_data, law_result, tech_result, finance_result)
|
||||
|
||||
result["agent"] = "综合裁决节点"
|
||||
result["icon"] = self.role_icon
|
||||
return result
|
||||
|
||||
def _build_prompt(self, company_data: dict, law_result: dict,
|
||||
tech_result: dict, finance_result: dict) -> str:
|
||||
return f"""请对以下科创企业的三方评估结果进行综合裁决:
|
||||
|
||||
企业名称:{company_data.get('short_name', '未知')}
|
||||
行业:{company_data.get('industry', '未知')}
|
||||
|
||||
=== 👩⚖️ 法务风控节点评估 ===
|
||||
{json.dumps(law_result, ensure_ascii=False, indent=2)}
|
||||
|
||||
=== 👨🔬 技术风控节点评估 ===
|
||||
{json.dumps(tech_result, ensure_ascii=False, indent=2)}
|
||||
|
||||
=== 👔 财务风控节点评估 ===
|
||||
{json.dumps(finance_result, ensure_ascii=False, indent=2)}
|
||||
|
||||
请消解可能存在的判定冲突,输出综合裁决JSON。"""
|
||||
|
||||
def _rule_based_evaluation(self, company_data: dict, law_result: dict,
|
||||
tech_result: dict, finance_result: dict) -> dict:
|
||||
"""规则引擎综合裁决"""
|
||||
|
||||
# 提取各维度得分
|
||||
def safe_score(result: dict, key: str) -> int:
|
||||
item = result.get(key, {})
|
||||
if isinstance(item, dict):
|
||||
return item.get("score", 50)
|
||||
return 50
|
||||
|
||||
# 六维评分
|
||||
scores = {
|
||||
"tech_disruption": safe_score(tech_result, "tech_disruption_risk"),
|
||||
"talent_loss": safe_score(tech_result, "talent_loss_risk"),
|
||||
"algo_compliance": safe_score(law_result, "algo_compliance_risk"),
|
||||
"geopolitical": safe_score(law_result, "geopolitical_risk"),
|
||||
"rd_capitalization": safe_score(finance_result, "rd_capitalization_risk"),
|
||||
"concentration": safe_score(finance_result, "concentration_risk"),
|
||||
}
|
||||
|
||||
# 加权综合得分
|
||||
weights = {
|
||||
"tech_disruption": 0.20,
|
||||
"talent_loss": 0.15,
|
||||
"algo_compliance": 0.15,
|
||||
"geopolitical": 0.20,
|
||||
"rd_capitalization": 0.15,
|
||||
"concentration": 0.15,
|
||||
}
|
||||
|
||||
comprehensive_score = int(
|
||||
sum(scores[k] * weights[k] for k in scores)
|
||||
)
|
||||
|
||||
# 裁决
|
||||
if comprehensive_score >= 80:
|
||||
decision = "拒绝承保"
|
||||
risk_level = "极高"
|
||||
elif comprehensive_score >= 60:
|
||||
decision = "附条件承保"
|
||||
risk_level = "高"
|
||||
elif comprehensive_score >= 40:
|
||||
decision = "标准承保"
|
||||
risk_level = "中"
|
||||
else:
|
||||
decision = "优先承保"
|
||||
risk_level = "低"
|
||||
|
||||
# 收集关键风险
|
||||
key_risks = []
|
||||
for dim, score in sorted(scores.items(), key=lambda x: x[1], reverse=True):
|
||||
if score >= 60:
|
||||
dim_names = {
|
||||
"tech_disruption": "技术路线颠覆",
|
||||
"talent_loss": "核心人员流失",
|
||||
"algo_compliance": "算法/数据合规",
|
||||
"geopolitical": "地缘政治/出口管制",
|
||||
"rd_capitalization": "研发资本化操纵",
|
||||
"concentration": "客户/供应商集中",
|
||||
}
|
||||
key_risks.append(f"{dim_names.get(dim, dim)}风险({score}分)")
|
||||
|
||||
# 核保条件
|
||||
conditions = []
|
||||
if scores["geopolitical"] >= 70:
|
||||
conditions.append("要求提供出口管制合规声明及供应链替代方案")
|
||||
if scores["rd_capitalization"] >= 60:
|
||||
conditions.append("要求额外提供研发资本化会计政策说明及审计意见")
|
||||
if scores["concentration"] >= 60:
|
||||
conditions.append("要求提供客户分散化计划或前五大客户信用报告")
|
||||
if scores["talent_loss"] >= 60:
|
||||
conditions.append("要求核心技术人员签署竞业协议且公司有留任激励计划")
|
||||
|
||||
company_name = company_data.get("short_name", "该企业")
|
||||
summary = (
|
||||
f"{company_name}综合风险评分{comprehensive_score}分(风险等级:{risk_level})。"
|
||||
f"核保建议:【{decision}】。"
|
||||
f"主要风险集中在{'、'.join(key_risks[:3]) if key_risks else '无突出风险'}。"
|
||||
)
|
||||
|
||||
return {
|
||||
"comprehensive_score": comprehensive_score,
|
||||
"risk_level": risk_level,
|
||||
"underwriting_decision": decision,
|
||||
"six_dimension_scores": scores,
|
||||
"conflict_resolution": "基于优先级原则(合规>技术>财务)进行加权裁决",
|
||||
"key_risks": key_risks,
|
||||
"underwriting_conditions": conditions,
|
||||
"summary": summary,
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
法务风控节点
|
||||
审查维度:算法备案状态、实体清单命中、数据出境风险、知识产权诉讼
|
||||
"""
|
||||
import json
|
||||
from .base_agent import BaseAgent
|
||||
|
||||
|
||||
LAW_SYSTEM_PROMPT = """你是一名资深法务风控专家,精通以下法律法规:
|
||||
- 《生成式人工智能服务管理暂行办法》
|
||||
- 《互联网信息服务算法推荐管理规定》
|
||||
- 《数据安全法》《个人信息保护法》
|
||||
- 《出口管制法》及美国 BIS 实体清单相关规则
|
||||
- 《科创板上市规则》中的合规要求
|
||||
|
||||
你的任务是基于提供的企业数据,从法律合规角度评估以下风险:
|
||||
1. 算法备案合规风险:企业是否涉及AI业务但未完成算法备案
|
||||
2. 地缘政治与出口管制风险:企业或其供应链是否受到制裁
|
||||
3. 数据合规风险:是否存在数据出境、数据安全方面的隐患
|
||||
4. 知识产权诉讼风险:是否面临重大IP纠纷
|
||||
|
||||
请以严谨的法律视角进行评估,输出JSON格式:
|
||||
{
|
||||
"agent": "法务风控节点",
|
||||
"algo_compliance_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"geopolitical_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"data_compliance_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"ip_litigation_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"overall_law_risk": {"score": 0-100, "level": "高/中/低"},
|
||||
"key_findings": ["..."],
|
||||
"recommendations": ["..."]
|
||||
}"""
|
||||
|
||||
|
||||
class LawAgent(BaseAgent):
|
||||
"""法务风控节点"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="法务风控节点",
|
||||
system_prompt=LAW_SYSTEM_PROMPT,
|
||||
role_icon="👩⚖️",
|
||||
)
|
||||
|
||||
def evaluate(self, company_data: dict) -> dict:
|
||||
"""执行法务风险评估"""
|
||||
prompt = self._build_prompt(company_data)
|
||||
result = self.infer_json(prompt)
|
||||
|
||||
# 如果 JSON 解析失败,使用规则引擎
|
||||
if result.get("parse_error"):
|
||||
result = self._rule_based_evaluation(company_data)
|
||||
|
||||
result["agent"] = "法务风控节点"
|
||||
result["icon"] = self.role_icon
|
||||
return result
|
||||
|
||||
def _build_prompt(self, company_data: dict) -> str:
|
||||
"""构建评估提示词"""
|
||||
return f"""请对以下科创企业进行法务风险评估:
|
||||
|
||||
企业名称:{company_data.get('short_name', company_data.get('company_name', '未知'))}
|
||||
行业:{company_data.get('industry', '未知')}
|
||||
领域:{company_data.get('sector', '未知')}
|
||||
|
||||
合规状态:
|
||||
- 算法备案:{json.dumps(company_data.get('compliance', {}), ensure_ascii=False)}
|
||||
|
||||
供应链信息:
|
||||
- 关键供应商:{json.dumps(company_data.get('supply_chain', {}).get('key_suppliers', []), ensure_ascii=False)}
|
||||
- 供应商集中度风险:{company_data.get('supply_chain', {}).get('supplier_concentration_risk', '未知')}
|
||||
|
||||
技术路线:
|
||||
{json.dumps(company_data.get('tech_route', {}), ensure_ascii=False)}
|
||||
|
||||
请输出严格的JSON评估结果。"""
|
||||
|
||||
def fallback_inference(self, prompt: str) -> str:
|
||||
"""规则引擎降级"""
|
||||
return json.dumps(self._rule_based_evaluation({}), ensure_ascii=False)
|
||||
|
||||
def _rule_based_evaluation(self, company_data: dict) -> dict:
|
||||
"""基于规则的法务风险评估"""
|
||||
compliance = company_data.get("compliance", {})
|
||||
supply_chain = company_data.get("supply_chain", {})
|
||||
sector = company_data.get("sector", "")
|
||||
|
||||
# 算法备案风险
|
||||
algo_status = compliance.get("algo_filing_status", "")
|
||||
algo_score = 20
|
||||
algo_detail = "合规状态正常"
|
||||
if sector in ["AI", "软件", "互联网"] and algo_status == "不适用":
|
||||
algo_score = 60
|
||||
algo_detail = "涉及AI业务但标注为不适用,建议核实"
|
||||
elif "未" in algo_status or not algo_status:
|
||||
algo_score = 80
|
||||
algo_detail = "未查到算法备案记录,存在合规风险"
|
||||
elif "已备案" in algo_status:
|
||||
algo_score = 10
|
||||
algo_detail = "已完成算法备案"
|
||||
|
||||
# 地缘政治风险
|
||||
entity_status = compliance.get("entity_list_status", "")
|
||||
geo_score = 15
|
||||
geo_detail = "未受出口管制影响"
|
||||
if "被列入" in entity_status:
|
||||
geo_score = 95
|
||||
geo_detail = f"已被列入实体清单: {compliance.get('sanctions_detail', '')}"
|
||||
elif supply_chain.get("supplier_concentration_risk") == "极高":
|
||||
geo_score = 70
|
||||
geo_detail = "核心供应链高度依赖海外,存在间接制裁风险"
|
||||
|
||||
# 数据合规风险
|
||||
data_risk = compliance.get("data_export_risk", "低")
|
||||
data_score = {"高": 75, "中": 45, "低": 15}.get(data_risk, 20)
|
||||
data_detail = f"数据出境风险等级: {data_risk}"
|
||||
|
||||
# 知识产权风险
|
||||
ip_score = 25
|
||||
ip_detail = "未发现重大IP纠纷"
|
||||
|
||||
# 综合法务风险
|
||||
overall_score = int(
|
||||
algo_score * 0.25 + geo_score * 0.35 +
|
||||
data_score * 0.25 + ip_score * 0.15
|
||||
)
|
||||
|
||||
return {
|
||||
"algo_compliance_risk": {"score": algo_score, "level": self._level(algo_score), "detail": algo_detail},
|
||||
"geopolitical_risk": {"score": geo_score, "level": self._level(geo_score), "detail": geo_detail},
|
||||
"data_compliance_risk": {"score": data_score, "level": self._level(data_score), "detail": data_detail},
|
||||
"ip_litigation_risk": {"score": ip_score, "level": self._level(ip_score), "detail": ip_detail},
|
||||
"overall_law_risk": {"score": overall_score, "level": self._level(overall_score)},
|
||||
"key_findings": [algo_detail, geo_detail, data_detail],
|
||||
"recommendations": ["建议定期审查合规状态", "关注实体清单更新动态"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _level(score: int) -> str:
|
||||
if score >= 70:
|
||||
return "高"
|
||||
elif score >= 40:
|
||||
return "中"
|
||||
return "低"
|
||||
@@ -0,0 +1,147 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
技术风控节点
|
||||
审查维度:技术路线竞争态势、核心人员稳定性、专利布局、技术替代风险
|
||||
"""
|
||||
import json
|
||||
from .base_agent import BaseAgent
|
||||
|
||||
|
||||
TECH_SYSTEM_PROMPT = """你是一名资深科技行业分析师和技术风控专家。
|
||||
你精通半导体、人工智能、新能源、生物医疗等前沿科技领域的技术演进趋势。
|
||||
|
||||
你的任务是基于提供的企业数据,从技术角度评估以下风险:
|
||||
1. 技术路线颠覆风险:企业押注的技术路线是否面临被替代的风险
|
||||
2. 核心人员流失风险:关键技术人员的稳定性和不可替代性
|
||||
3. 专利/技术壁垒:技术护城河的深度和可持续性
|
||||
4. 技术迭代压力:行业技术迭代速度对企业的冲击
|
||||
|
||||
请以技术专家的视角进行深度评估,输出JSON格式:
|
||||
{
|
||||
"agent": "技术风控节点",
|
||||
"tech_disruption_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"talent_loss_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"patent_moat": {"score": 0-100, "level": "强/中/弱", "detail": "..."},
|
||||
"tech_iteration_pressure": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"overall_tech_risk": {"score": 0-100, "level": "高/中/低"},
|
||||
"key_findings": ["..."],
|
||||
"recommendations": ["..."]
|
||||
}"""
|
||||
|
||||
|
||||
class TechAgent(BaseAgent):
|
||||
"""技术风控节点"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="技术风控节点",
|
||||
system_prompt=TECH_SYSTEM_PROMPT,
|
||||
role_icon="👨🔬",
|
||||
)
|
||||
|
||||
def evaluate(self, company_data: dict) -> dict:
|
||||
"""执行技术风险评估"""
|
||||
prompt = self._build_prompt(company_data)
|
||||
result = self.infer_json(prompt)
|
||||
|
||||
if result.get("parse_error"):
|
||||
result = self._rule_based_evaluation(company_data)
|
||||
|
||||
result["agent"] = "技术风控节点"
|
||||
result["icon"] = self.role_icon
|
||||
return result
|
||||
|
||||
def _build_prompt(self, company_data: dict) -> str:
|
||||
return f"""请对以下科创企业进行技术风险评估:
|
||||
|
||||
企业名称:{company_data.get('short_name', '未知')}
|
||||
行业:{company_data.get('industry', '未知')}
|
||||
领域:{company_data.get('sector', '未知')}
|
||||
企业描述:{company_data.get('description', '')}
|
||||
|
||||
技术路线信息:
|
||||
{json.dumps(company_data.get('tech_route', {}), ensure_ascii=False, indent=2)}
|
||||
|
||||
核心技术人员:
|
||||
{json.dumps(company_data.get('core_tech_personnel', []), ensure_ascii=False, indent=2)}
|
||||
|
||||
财务中的研发指标:
|
||||
- 研发费用: {company_data.get('financials', {}).get('rd_expense_2024', 0)}
|
||||
- 研发营收比: {company_data.get('financials', {}).get('rd_revenue_ratio', 0)}
|
||||
|
||||
请输出严格的JSON评估结果。"""
|
||||
|
||||
def _rule_based_evaluation(self, company_data: dict) -> dict:
|
||||
"""基于规则的技术风险评估"""
|
||||
tech_route = company_data.get("tech_route", {})
|
||||
personnel = company_data.get("core_tech_personnel", [])
|
||||
financials = company_data.get("financials", {})
|
||||
|
||||
# 技术路线颠覆风险
|
||||
competing_techs = tech_route.get("competing_techs", [])
|
||||
disruption_score = min(20 + len(competing_techs) * 15, 90)
|
||||
tech_moat = tech_route.get("tech_moat", "")
|
||||
if "差距" in tech_moat or "受制" in tech_moat:
|
||||
disruption_score = min(disruption_score + 20, 95)
|
||||
disruption_detail = f"面临 {len(competing_techs)} 条竞争技术路线: {', '.join(competing_techs[:3])}"
|
||||
|
||||
# 核心人员流失风险
|
||||
talent_score = 20
|
||||
talent_detail = "核心团队稳定"
|
||||
departed = [p for p in personnel if "离职" in p.get("status", "")]
|
||||
high_importance = [p for p in personnel if p.get("importance") == "极高"]
|
||||
|
||||
if departed:
|
||||
talent_score = 80
|
||||
talent_detail = f"已有核心人员离职: {', '.join(p['name'] for p in departed)}"
|
||||
elif len(high_importance) == 1:
|
||||
talent_score = 55
|
||||
talent_detail = f"高度依赖单一核心人员: {high_importance[0]['name']}"
|
||||
elif len(personnel) <= 2:
|
||||
talent_score = 45
|
||||
talent_detail = "核心技术团队规模偏小"
|
||||
|
||||
# 专利壁垒
|
||||
patent_count = tech_route.get("patent_count", 0)
|
||||
if patent_count > 5000:
|
||||
patent_score = 20
|
||||
patent_detail = f"专利数量充足({patent_count}件),技术壁垒较强"
|
||||
elif patent_count > 1000:
|
||||
patent_score = 35
|
||||
patent_detail = f"专利数量中等({patent_count}件)"
|
||||
else:
|
||||
patent_score = 60
|
||||
patent_detail = f"专利数量偏少({patent_count}件),技术壁垒偏弱"
|
||||
|
||||
# 技术迭代压力(基于研发投入比)
|
||||
rd_ratio = financials.get("rd_revenue_ratio", 0)
|
||||
if rd_ratio > 0.3:
|
||||
iter_score = 65
|
||||
iter_detail = f"研发营收比极高({rd_ratio:.1%}),说明行业技术迭代压力大"
|
||||
elif rd_ratio > 0.15:
|
||||
iter_score = 45
|
||||
iter_detail = f"研发投入较高({rd_ratio:.1%}),需持续技术投入"
|
||||
else:
|
||||
iter_score = 25
|
||||
iter_detail = f"研发投入适中({rd_ratio:.1%})"
|
||||
|
||||
overall = int(disruption_score * 0.35 + talent_score * 0.25 +
|
||||
patent_score * 0.15 + iter_score * 0.25)
|
||||
|
||||
return {
|
||||
"tech_disruption_risk": {"score": disruption_score, "level": self._level(disruption_score), "detail": disruption_detail},
|
||||
"talent_loss_risk": {"score": talent_score, "level": self._level(talent_score), "detail": talent_detail},
|
||||
"patent_moat": {"score": patent_score, "level": "弱" if patent_score >= 50 else ("中" if patent_score >= 30 else "强"), "detail": patent_detail},
|
||||
"tech_iteration_pressure": {"score": iter_score, "level": self._level(iter_score), "detail": iter_detail},
|
||||
"overall_tech_risk": {"score": overall, "level": self._level(overall)},
|
||||
"key_findings": [disruption_detail, talent_detail, patent_detail],
|
||||
"recommendations": ["关注竞争技术路线发展", "加强核心人员留任激励"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _level(score: int) -> str:
|
||||
if score >= 70:
|
||||
return "高"
|
||||
elif score >= 40:
|
||||
return "中"
|
||||
return "低"
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据采集模块"""
|
||||
@@ -0,0 +1,188 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
年报 PDF 文本解析模块
|
||||
从年报中提取关键风险信息:核心技术人员、技术路线、诉讼、风险提示等
|
||||
"""
|
||||
import re
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_annual_report(pdf_path: str) -> dict:
|
||||
"""
|
||||
解析年报 PDF,提取关键风险相关信息
|
||||
返回结构化的风险要素字典
|
||||
"""
|
||||
text = _extract_text_from_pdf(pdf_path)
|
||||
if not text:
|
||||
return {"error": "PDF 解析失败", "raw_text": ""}
|
||||
|
||||
return {
|
||||
"core_personnel_info": _extract_core_personnel(text),
|
||||
"tech_route_info": _extract_tech_route(text),
|
||||
"litigation_info": _extract_litigation(text),
|
||||
"risk_factors": _extract_risk_factors(text),
|
||||
"rd_capitalization_info": _extract_rd_capitalization(text),
|
||||
"customer_concentration": _extract_customer_concentration(text),
|
||||
"raw_text_length": len(text),
|
||||
}
|
||||
|
||||
|
||||
def _extract_text_from_pdf(pdf_path: str) -> Optional[str]:
|
||||
"""使用 pdfplumber 提取 PDF 全文"""
|
||||
try:
|
||||
import pdfplumber
|
||||
text_parts = []
|
||||
with pdfplumber.open(pdf_path) as pdf:
|
||||
for page in pdf.pages:
|
||||
page_text = page.extract_text()
|
||||
if page_text:
|
||||
text_parts.append(page_text)
|
||||
return "\n".join(text_parts)
|
||||
except ImportError:
|
||||
logger.warning("pdfplumber 未安装,尝试 PyPDF2")
|
||||
try:
|
||||
from PyPDF2 import PdfReader
|
||||
reader = PdfReader(pdf_path)
|
||||
return "\n".join(
|
||||
page.extract_text() or "" for page in reader.pages
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"PyPDF2 解析失败: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"PDF 解析失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _extract_core_personnel(text: str) -> dict:
|
||||
"""提取核心技术人员相关信息"""
|
||||
result = {
|
||||
"has_departure": False,
|
||||
"departure_details": [],
|
||||
"personnel_count": 0,
|
||||
"key_mentions": [],
|
||||
}
|
||||
|
||||
# 匹配离职/辞职相关表述
|
||||
departure_patterns = [
|
||||
r"(核心技术人员|核心人员|关键技术人员).{0,30}(离职|辞职|离任|不再担任)",
|
||||
r"(CTO|首席技术官|技术总监|研发总监).{0,30}(离职|辞职|离任)",
|
||||
r"(离职|辞职).{0,30}(核心技术人员|核心人员)",
|
||||
]
|
||||
for pattern in departure_patterns:
|
||||
matches = re.findall(pattern, text)
|
||||
if matches:
|
||||
result["has_departure"] = True
|
||||
result["departure_details"].extend([str(m) for m in matches])
|
||||
|
||||
# 统计核心技术人员数量
|
||||
count_match = re.search(r"核心技术人员\s*(\d+)\s*[名人]", text)
|
||||
if count_match:
|
||||
result["personnel_count"] = int(count_match.group(1))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _extract_tech_route(text: str) -> dict:
|
||||
"""提取技术路线相关信息"""
|
||||
result = {
|
||||
"competing_tech_mentioned": False,
|
||||
"tech_keywords": [],
|
||||
"risk_mentions": [],
|
||||
}
|
||||
|
||||
# 技术竞争关键词
|
||||
tech_keywords = [
|
||||
"技术路线", "技术迭代", "技术替代", "技术颠覆",
|
||||
"竞争技术", "替代方案", "新一代技术",
|
||||
]
|
||||
for kw in tech_keywords:
|
||||
if kw in text:
|
||||
result["tech_keywords"].append(kw)
|
||||
result["competing_tech_mentioned"] = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _extract_litigation(text: str) -> dict:
|
||||
"""提取诉讼/仲裁相关信息"""
|
||||
result = {
|
||||
"has_litigation": False,
|
||||
"litigation_count": 0,
|
||||
"ip_related": False,
|
||||
}
|
||||
|
||||
# 诉讼关键词
|
||||
litigation_patterns = [
|
||||
r"(诉讼|仲裁|起诉|被告).{0,50}(知识产权|专利|商标|著作权)",
|
||||
r"(专利侵权|商标侵权|著作权纠纷)",
|
||||
]
|
||||
for pattern in litigation_patterns:
|
||||
if re.search(pattern, text):
|
||||
result["has_litigation"] = True
|
||||
result["ip_related"] = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _extract_risk_factors(text: str) -> list:
|
||||
"""提取风险因素章节的关键信息"""
|
||||
risk_keywords = [
|
||||
"地缘政治", "实体清单", "出口管制", "贸易摩擦",
|
||||
"数据安全", "数据合规", "算法备案", "数据出境",
|
||||
"客户集中", "供应商集中", "单一客户", "单一供应商",
|
||||
"研发资本化", "开发支出", "无形资产",
|
||||
"人才流失", "核心人员", "竞业限制",
|
||||
]
|
||||
found_risks = []
|
||||
for kw in risk_keywords:
|
||||
if kw in text:
|
||||
found_risks.append(kw)
|
||||
return found_risks
|
||||
|
||||
|
||||
def _extract_rd_capitalization(text: str) -> dict:
|
||||
"""提取研发资本化相关信息"""
|
||||
result = {
|
||||
"has_capitalization": False,
|
||||
"capitalization_mentioned": False,
|
||||
"amount_keywords": [],
|
||||
}
|
||||
|
||||
cap_keywords = ["开发支出", "研发资本化", "资本化研发", "开发阶段支出"]
|
||||
for kw in cap_keywords:
|
||||
if kw in text:
|
||||
result["capitalization_mentioned"] = True
|
||||
result["amount_keywords"].append(kw)
|
||||
|
||||
# 检查是否有具体的资本化金额
|
||||
cap_amount = re.search(r"开发支出.{0,30}([\d,\.]+)\s*(万元|百万|亿)", text)
|
||||
if cap_amount:
|
||||
result["has_capitalization"] = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _extract_customer_concentration(text: str) -> dict:
|
||||
"""提取客户/供应商集中度信息"""
|
||||
result = {
|
||||
"top5_customer_ratio": None,
|
||||
"top5_supplier_ratio": None,
|
||||
"single_customer_dependency": False,
|
||||
}
|
||||
|
||||
# 前五大客户占比
|
||||
customer_match = re.search(
|
||||
r"前五[名大]客户.{0,30}([\d\.]+)\s*%", text
|
||||
)
|
||||
if customer_match:
|
||||
result["top5_customer_ratio"] = float(customer_match.group(1)) / 100
|
||||
|
||||
# 单一客户依赖
|
||||
if re.search(r"(第一大客户|最大客户).{0,30}([\d\.]+)\s*%", text):
|
||||
result["single_customer_dependency"] = True
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
算法备案合规数据采集与查询模块
|
||||
匹配企业是否已完成网信办算法备案
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||
|
||||
|
||||
def _load_algo_filings() -> list:
|
||||
"""加载算法备案数据"""
|
||||
filepath = DATA_DIR / "algo_filings.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def check_algo_filing(company_name: str) -> dict:
|
||||
"""
|
||||
查询企业的算法备案状态
|
||||
"""
|
||||
filings = _load_algo_filings()
|
||||
result = {
|
||||
"has_filing": False,
|
||||
"filings": [],
|
||||
"needs_filing": False, # 是否需要备案但未备案
|
||||
"risk_level": "低",
|
||||
}
|
||||
|
||||
for filing in filings:
|
||||
if company_name in filing["company"] or filing["company"] in company_name:
|
||||
result["has_filing"] = True
|
||||
result["filings"].append(filing)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def assess_algo_compliance_risk(company_data: dict) -> dict:
|
||||
"""
|
||||
综合评估企业的算法合规风险
|
||||
考虑因素:是否涉及 AI 业务、是否已备案、数据出境风险
|
||||
"""
|
||||
company_name = company_data.get("short_name", company_data.get("company_name", ""))
|
||||
sector = company_data.get("sector", "")
|
||||
compliance = company_data.get("compliance", {})
|
||||
|
||||
# 查询备案状态
|
||||
filing_status = check_algo_filing(company_name)
|
||||
|
||||
# 判断是否需要备案
|
||||
ai_related_sectors = ["AI", "软件", "互联网", "消费电子"]
|
||||
needs_filing = sector in ai_related_sectors or "AI" in str(company_data.get("tech_route", {}))
|
||||
|
||||
# 综合评估
|
||||
risk_level = "低"
|
||||
risk_details = []
|
||||
|
||||
if needs_filing and not filing_status["has_filing"]:
|
||||
algo_status = compliance.get("algo_filing_status", "")
|
||||
if algo_status == "不适用":
|
||||
risk_level = "低"
|
||||
else:
|
||||
risk_level = "高"
|
||||
risk_details.append("涉及AI业务但未查到算法备案记录")
|
||||
|
||||
data_export_risk = compliance.get("data_export_risk", "低")
|
||||
if data_export_risk == "高":
|
||||
risk_level = "高"
|
||||
risk_details.append("存在大量跨境数据传输,数据出境评估风险高")
|
||||
elif data_export_risk == "中":
|
||||
if risk_level != "高":
|
||||
risk_level = "中"
|
||||
risk_details.append("存在部分跨境数据传输,需关注数据出境合规")
|
||||
|
||||
return {
|
||||
"company_name": company_name,
|
||||
"needs_filing": needs_filing,
|
||||
"filing_status": filing_status,
|
||||
"data_export_risk": data_export_risk,
|
||||
"overall_risk_level": risk_level,
|
||||
"risk_details": risk_details,
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
BIS 实体清单采集与匹配模块
|
||||
支持企业名模糊匹配 + 别名映射 + 供应链上游穿透
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||
|
||||
|
||||
def _load_entity_list() -> list:
|
||||
"""加载实体清单数据"""
|
||||
filepath = DATA_DIR / "entity_list.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def check_entity_list(company_name: str) -> dict:
|
||||
"""
|
||||
检查企业是否在 BIS 实体清单中
|
||||
支持模糊匹配和别名匹配
|
||||
"""
|
||||
entities = _load_entity_list()
|
||||
result = {
|
||||
"is_sanctioned": False,
|
||||
"match_type": None,
|
||||
"entity_detail": None,
|
||||
"supply_chain_risk": [], # 供应链上游被制裁的情况
|
||||
}
|
||||
|
||||
for entity in entities:
|
||||
# 精确匹配
|
||||
if company_name in entity["entity_name"]:
|
||||
result["is_sanctioned"] = True
|
||||
result["match_type"] = "直接命中"
|
||||
result["entity_detail"] = entity
|
||||
return result
|
||||
|
||||
# 别名匹配
|
||||
for alias in entity.get("aliases", []):
|
||||
if company_name in alias or alias in company_name:
|
||||
result["is_sanctioned"] = True
|
||||
result["match_type"] = "别名命中"
|
||||
result["entity_detail"] = entity
|
||||
return result
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def check_supply_chain_sanctions(company_name: str, suppliers: list) -> list:
|
||||
"""
|
||||
检查企业供应链上游是否有被制裁的实体
|
||||
返回受制裁的供应商列表
|
||||
"""
|
||||
sanctioned_suppliers = []
|
||||
entities = _load_entity_list()
|
||||
|
||||
for supplier in suppliers:
|
||||
# 清洗供应商名称(去掉括号中的说明文字)
|
||||
clean_name = supplier.split("(")[0].split("(")[0].strip()
|
||||
|
||||
for entity in entities:
|
||||
all_names = [entity["entity_name"]] + entity.get("aliases", [])
|
||||
for name in all_names:
|
||||
if clean_name in name or name in clean_name:
|
||||
sanctioned_suppliers.append({
|
||||
"supplier": supplier,
|
||||
"matched_entity": entity["entity_name"],
|
||||
"restrictions": entity["restrictions"],
|
||||
"date_added": entity["date_added"],
|
||||
})
|
||||
break
|
||||
|
||||
return sanctioned_suppliers
|
||||
|
||||
|
||||
def get_all_sanctioned_entities() -> list:
|
||||
"""获取所有被制裁实体列表"""
|
||||
return _load_entity_list()
|
||||
@@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
财务数据采集模块
|
||||
双轨策略:优先尝试 AKShare 在线采集,失败则回退到预置数据
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||
|
||||
|
||||
def _load_preset_data() -> list:
|
||||
"""加载预置的科创板企业数据"""
|
||||
filepath = DATA_DIR / "sample_companies.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def collect_financial_data(stock_code: str) -> Optional[dict]:
|
||||
"""
|
||||
采集指定股票代码的财务数据
|
||||
双轨策略:在线采集 → 离线预置
|
||||
"""
|
||||
# 尝试在线采集
|
||||
try:
|
||||
return _collect_online(stock_code)
|
||||
except Exception as e:
|
||||
logger.warning(f"在线采集 {stock_code} 失败: {e},回退到预置数据")
|
||||
|
||||
# 回退到预置数据
|
||||
return _collect_from_preset(stock_code)
|
||||
|
||||
|
||||
def _collect_online(stock_code: str) -> Optional[dict]:
|
||||
"""通过 AKShare 在线采集财务数据"""
|
||||
try:
|
||||
import akshare as ak
|
||||
|
||||
# 科创板企业利润表
|
||||
profit_df = ak.stock_profit_sheet_by_report_em(symbol=stock_code)
|
||||
# 科创板企业资产负债表
|
||||
balance_df = ak.stock_balance_sheet_by_report_em(symbol=stock_code)
|
||||
|
||||
if profit_df is not None and not profit_df.empty:
|
||||
latest = profit_df.iloc[0]
|
||||
return {
|
||||
"stock_code": stock_code,
|
||||
"revenue": float(latest.get("营业收入", 0)),
|
||||
"net_profit": float(latest.get("净利润", 0)),
|
||||
"rd_expense": float(latest.get("研发费用", 0)),
|
||||
"source": "akshare_online",
|
||||
}
|
||||
except ImportError:
|
||||
logger.warning("AKShare 未安装,跳过在线采集")
|
||||
except Exception as e:
|
||||
logger.warning(f"AKShare 采集异常: {e}")
|
||||
|
||||
raise RuntimeError("在线采集失败")
|
||||
|
||||
|
||||
def _collect_from_preset(stock_code: str) -> Optional[dict]:
|
||||
"""从预置数据中查找企业"""
|
||||
companies = _load_preset_data()
|
||||
for company in companies:
|
||||
if company["stock_code"] == stock_code:
|
||||
return {
|
||||
"stock_code": stock_code,
|
||||
"company_name": company["company_name"],
|
||||
"industry": company["industry"],
|
||||
"sector": company["sector"],
|
||||
"financials": company["financials"],
|
||||
"core_tech_personnel": company["core_tech_personnel"],
|
||||
"tech_route": company["tech_route"],
|
||||
"compliance": company["compliance"],
|
||||
"supply_chain": company["supply_chain"],
|
||||
"source": "preset_data",
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def get_all_companies() -> list:
|
||||
"""获取所有预置企业列表"""
|
||||
return _load_preset_data()
|
||||
|
||||
|
||||
def get_company_by_code(stock_code: str) -> Optional[dict]:
|
||||
"""通过股票代码查找企业完整数据"""
|
||||
companies = _load_preset_data()
|
||||
for company in companies:
|
||||
if company["stock_code"] == stock_code:
|
||||
return company
|
||||
return None
|
||||
|
||||
|
||||
def get_company_by_name(name: str) -> Optional[dict]:
|
||||
"""通过企业名称查找(支持简称)"""
|
||||
companies = _load_preset_data()
|
||||
for company in companies:
|
||||
if name in company["company_name"] or name in company["short_name"]:
|
||||
return company
|
||||
return None
|
||||
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
全局配置文件
|
||||
集中管理 API Key、文件路径、模型参数等
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# ============================================================
|
||||
# 项目根目录
|
||||
# ============================================================
|
||||
PROJECT_ROOT = Path(__file__).parent.resolve()
|
||||
DATA_DIR = PROJECT_ROOT / "data"
|
||||
|
||||
# ============================================================
|
||||
# DeepSeek API 配置(兼容 OpenAI 接口协议)
|
||||
# ============================================================
|
||||
DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
|
||||
DEEPSEEK_BASE_URL = "https://api.deepseek.com/v1"
|
||||
DEEPSEEK_MODEL = "deepseek-chat"
|
||||
|
||||
# 备用1:火山引擎方舟(豆包大模型 doubao-seed-2-1-pro-260628)
|
||||
VOLCENGINE_API_KEY = os.environ.get("VOLCENGINE_API_KEY", os.environ.get("ARK_API_KEY", "836d9bc0-80e8-4e45-90df-7287994d91ec"))
|
||||
VOLCENGINE_BASE_URL = os.environ.get("VOLCENGINE_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3")
|
||||
VOLCENGINE_MODEL = os.environ.get("VOLCENGINE_MODEL", "doubao-seed-2-0-lite-260428")
|
||||
|
||||
# 备用2:如果用户配置了其他兼容 OpenAI 协议的 API
|
||||
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
|
||||
OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1")
|
||||
OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
|
||||
|
||||
# ============================================================
|
||||
# LLM 调用参数
|
||||
# ============================================================
|
||||
LLM_TEMPERATURE = 0.1 # 低温度保证输出稳定
|
||||
LLM_MAX_TOKENS = 4096
|
||||
LLM_TIMEOUT = 60 # 超时秒数
|
||||
LLM_MAX_RETRIES = 2 # 最大重试次数
|
||||
|
||||
# ============================================================
|
||||
# 风险评估权重配置
|
||||
# ============================================================
|
||||
RISK_WEIGHTS = {
|
||||
"tech_disruption": 0.20, # 技术路线颠覆风险
|
||||
"talent_loss": 0.15, # 核心人员流失风险
|
||||
"algo_compliance": 0.15, # 算法/数据合规风险
|
||||
"geopolitical": 0.20, # 地缘政治/出口管制风险
|
||||
"rd_capitalization": 0.15, # 研发资本化操纵风险
|
||||
"concentration": 0.15, # 客户/供应商集中风险
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# 保险定价参数
|
||||
# ============================================================
|
||||
INSURANCE_PRODUCTS = {
|
||||
"ip_lawsuit": {
|
||||
"name": "知识产权被诉险",
|
||||
"base_premium": 50000, # 基础保费(元)
|
||||
"base_coverage": 5000000, # 基础保额(元)
|
||||
"description": "覆盖因知识产权纠纷(专利、商标、著作权)产生的诉讼费用及赔偿金",
|
||||
},
|
||||
"exec_departure": {
|
||||
"name": "高管离职业务中断险",
|
||||
"base_premium": 80000,
|
||||
"base_coverage": 10000000,
|
||||
"description": "覆盖核心技术人员/高管离职导致的业务中断损失",
|
||||
},
|
||||
"data_compliance": {
|
||||
"name": "数据合规行政处罚险",
|
||||
"base_premium": 30000,
|
||||
"base_coverage": 3000000,
|
||||
"description": "覆盖因非主观恶意的数据合规违规产生的行政罚款",
|
||||
},
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Streamlit 页面配置
|
||||
# ============================================================
|
||||
PAGE_TITLE = "🛡️ 科创企业智能风控与核保系统"
|
||||
PAGE_ICON = "🛡️"
|
||||
LAYOUT = "wide"
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{"filing_id": "AL-2023-001", "company": "金山办公", "algo_name": "WPS AI 写作助手", "filing_date": "2023-08-15", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2023-002", "company": "传音控股", "algo_name": "AI 相机美颜算法", "filing_date": "2023-06-20", "status": "已通过", "algo_type": "个性化推荐类"},
|
||||
{"filing_id": "AL-2023-003", "company": "海天瑞声", "algo_name": "智能标注平台算法", "filing_date": "2023-09-10", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2023-004", "company": "百度", "algo_name": "文心一言大模型", "filing_date": "2023-08-31", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2023-005", "company": "阿里巴巴", "algo_name": "通义千问大模型", "filing_date": "2023-09-13", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2023-006", "company": "腾讯", "algo_name": "混元大模型", "filing_date": "2023-09-15", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2024-001", "company": "字节跳动", "algo_name": "豆包大模型", "filing_date": "2024-01-15", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2024-002", "company": "商汤科技", "algo_name": "日日新大模型", "filing_date": "2024-02-20", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2024-003", "company": "科大讯飞", "algo_name": "星火认知大模型", "filing_date": "2024-03-01", "status": "已通过", "algo_type": "生成合成类"}
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{"entity_name": "中芯国际集成电路制造有限公司", "aliases": ["SMIC", "中芯国际", "Semiconductor Manufacturing International Corporation"], "date_added": "2020-12-18", "reason": "军事最终用途", "restrictions": "限制EUV光刻设备及先进制程相关技术出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "寒武纪科技股份有限公司", "aliases": ["Cambricon", "寒武纪"], "date_added": "2022-10-07", "reason": "支持中国军事现代化", "restrictions": "限制先进AI芯片代工与设计软件", "source": "BIS Entity List"},
|
||||
{"entity_name": "华为技术有限公司", "aliases": ["Huawei", "华为", "HUAWEI"], "date_added": "2019-05-16", "reason": "国家安全威胁", "restrictions": "全面限制美国技术出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "海康威视数字技术股份有限公司", "aliases": ["Hikvision", "海康威视"], "date_added": "2019-10-07", "reason": "参与新疆人权侵犯", "restrictions": "限制安防监控技术出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "大疆创新科技有限公司", "aliases": ["DJI", "大疆"], "date_added": "2020-12-18", "reason": "军事最终用途", "restrictions": "限制无人机核心部件出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "科大讯飞股份有限公司", "aliases": ["iFlytek", "科大讯飞"], "date_added": "2019-10-07", "reason": "参与新疆人权侵犯", "restrictions": "限制AI语音技术相关出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "商汤科技有限公司", "aliases": ["SenseTime", "商汤", "商汤科技"], "date_added": "2021-12-10", "reason": "参与监控技术开发", "restrictions": "限制AI视觉技术出口", "source": "BIS Entity List / OFAC SDN List"},
|
||||
{"entity_name": "旷视科技有限公司", "aliases": ["Megvii", "旷视"], "date_added": "2019-10-07", "reason": "参与新疆人权侵犯", "restrictions": "限制AI人脸识别技术出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "长江存储科技有限责任公司", "aliases": ["YMTC", "长江存储"], "date_added": "2022-12-15", "reason": "支持中国军事现代化", "restrictions": "限制NAND闪存芯片相关设备出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "上海微电子装备集团股份有限公司", "aliases": ["SMEE", "上海微电子"], "date_added": "2022-10-07", "reason": "支持中国军事现代化", "restrictions": "限制光刻机核心部件", "source": "BIS Entity List"},
|
||||
{"entity_name": "龙芯中科技术股份有限公司", "aliases": ["Loongson", "龙芯"], "date_added": "2023-02-10", "reason": "军事最终用途", "restrictions": "限制先进CPU设计工具出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "壁仞科技股份有限公司", "aliases": ["Biren Technology", "壁仞科技"], "date_added": "2022-10-07", "reason": "支持中国军事现代化", "restrictions": "限制GPU芯片代工", "source": "BIS Entity List"},
|
||||
{"entity_name": "摩尔线程智能科技股份有限公司", "aliases": ["Moore Threads", "摩尔线程"], "date_added": "2023-10-17", "reason": "支持中国军事现代化", "restrictions": "限制GPU芯片代工与设计", "source": "BIS Entity List"}
|
||||
]
|
||||
@@ -0,0 +1,408 @@
|
||||
[
|
||||
{
|
||||
"stock_code": "688981",
|
||||
"company_name": "中芯国际",
|
||||
"short_name": "中芯国际",
|
||||
"industry": "半导体制造",
|
||||
"sector": "芯片",
|
||||
"listed_board": "科创板",
|
||||
"description": "国内领先的集成电路晶圆代工企业,提供0.35微米到FinFET先进工艺",
|
||||
"core_tech_personnel": [
|
||||
{"name": "梁孟松", "title": "联合CEO/技术研发负责人", "status": "在职", "importance": "极高"},
|
||||
{"name": "周子学", "title": "董事长", "status": "在职", "importance": "高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 57756000000,
|
||||
"net_profit_2024": 3433000000,
|
||||
"rd_expense_2024": 5124000000,
|
||||
"rd_capitalization_rate": 0.0,
|
||||
"rd_revenue_ratio": 0.089,
|
||||
"top5_customer_ratio": 0.42,
|
||||
"top5_supplier_ratio": 0.55,
|
||||
"receivable_turnover": 8.2,
|
||||
"cash_flow_ratio": 1.35
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "14nm FinFET 量产, 7nm 研发中",
|
||||
"competing_techs": ["EUV 光刻技术 (ASML 垄断)", "GAA 晶体管架构"],
|
||||
"tech_moat": "国产替代核心标的,但受制于设备禁运",
|
||||
"patent_count": 12000
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "被列入(2020年12月)",
|
||||
"sanctions_detail": "被美国商务部列入实体清单,限制EUV光刻设备进口"
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["ASML(光刻机)", "东京电子(刻蚀设备)", "应用材料(薄膜沉积)"],
|
||||
"key_customers": ["高通", "联发科", "华为海思"],
|
||||
"supplier_concentration_risk": "极高——核心设备依赖海外供应商"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688111",
|
||||
"company_name": "金山办公软件股份有限公司",
|
||||
"short_name": "金山办公",
|
||||
"industry": "办公软件",
|
||||
"sector": "软件",
|
||||
"listed_board": "科创板",
|
||||
"description": "国产办公软件龙头,WPS Office 全球月活超5.9亿",
|
||||
"core_tech_personnel": [
|
||||
{"name": "章庆元", "title": "CEO", "status": "在职", "importance": "极高"},
|
||||
{"name": "姚冬", "title": "CTO/AI研发负责人", "status": "在职", "importance": "高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 4586000000,
|
||||
"net_profit_2024": 1231000000,
|
||||
"rd_expense_2024": 1520000000,
|
||||
"rd_capitalization_rate": 0.0,
|
||||
"rd_revenue_ratio": 0.331,
|
||||
"top5_customer_ratio": 0.15,
|
||||
"top5_supplier_ratio": 0.30,
|
||||
"receivable_turnover": 12.5,
|
||||
"cash_flow_ratio": 1.82
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "WPS AI 大模型集成、云文档协同",
|
||||
"competing_techs": ["Microsoft 365 Copilot", "Google Workspace Gemini"],
|
||||
"tech_moat": "信创替代核心标的,政府及国企客户粘性高",
|
||||
"patent_count": 3500
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "已备案(WPS AI 写作助手)",
|
||||
"data_export_risk": "中(海外版WPS涉及数据跨境)",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["华为云", "阿里云", "英伟达(GPU)"],
|
||||
"key_customers": ["各级政府机关", "央企国企", "中小企业"],
|
||||
"supplier_concentration_risk": "中——GPU算力依赖进口"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688139",
|
||||
"company_name": "海尔生物医疗股份有限公司",
|
||||
"short_name": "海尔生物",
|
||||
"industry": "生物医疗低温存储",
|
||||
"sector": "医疗器械",
|
||||
"listed_board": "科创板",
|
||||
"description": "全球领先的生物医疗低温存储解决方案提供商",
|
||||
"core_tech_personnel": [
|
||||
{"name": "刘占杰", "title": "董事长/总经理", "status": "在职", "importance": "极高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 2245000000,
|
||||
"net_profit_2024": 398000000,
|
||||
"rd_expense_2024": 289000000,
|
||||
"rd_capitalization_rate": 0.05,
|
||||
"rd_revenue_ratio": 0.129,
|
||||
"top5_customer_ratio": 0.22,
|
||||
"top5_supplier_ratio": 0.35,
|
||||
"receivable_turnover": 6.8,
|
||||
"cash_flow_ratio": 1.15
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "超低温冰箱(-196°C)、自动化样本库、物联网疫苗管理",
|
||||
"competing_techs": ["赛默飞世尔", "松下医疗"],
|
||||
"tech_moat": "国内市场份额第一,物联网技术差异化",
|
||||
"patent_count": 800
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["压缩机供应商", "电子元器件供应商"],
|
||||
"key_customers": ["各级疾控中心", "医院", "科研院所"],
|
||||
"supplier_concentration_risk": "低"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688256",
|
||||
"company_name": "寒武纪科技股份有限公司",
|
||||
"short_name": "寒武纪",
|
||||
"industry": "AI芯片",
|
||||
"sector": "芯片",
|
||||
"listed_board": "科创板",
|
||||
"description": "国内AI芯片独角兽,智能计算处理器IP及芯片产品提供商",
|
||||
"core_tech_personnel": [
|
||||
{"name": "陈天石", "title": "创始人/董事长/CEO", "status": "在职", "importance": "极高"},
|
||||
{"name": "陈云霁", "title": "首席科学家(兄长)", "status": "中科院任职,兼职顾问", "importance": "高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 1170000000,
|
||||
"net_profit_2024": -825000000,
|
||||
"rd_expense_2024": 1510000000,
|
||||
"rd_capitalization_rate": 0.0,
|
||||
"rd_revenue_ratio": 1.29,
|
||||
"top5_customer_ratio": 0.85,
|
||||
"top5_supplier_ratio": 0.70,
|
||||
"receivable_turnover": 3.2,
|
||||
"cash_flow_ratio": 0.45
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "思元系列AI推理/训练芯片, MLU架构",
|
||||
"competing_techs": ["英伟达CUDA生态", "华为昇腾", "AMD Instinct"],
|
||||
"tech_moat": "自主IP指令集,但生态建设与英伟达差距大",
|
||||
"patent_count": 2800
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用(硬件厂商)",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "被列入(2022年10月)",
|
||||
"sanctions_detail": "被美国商务部列入实体清单,限制先进制程芯片代工"
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["台积电(已受限)", "中芯国际(替代)", "日月光(封测)"],
|
||||
"key_customers": ["中国移动", "南京市政府智慧城市项目", "某互联网大厂"],
|
||||
"supplier_concentration_risk": "极高——先进制程代工受限"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688029",
|
||||
"company_name": "南微医学科技股份有限公司",
|
||||
"short_name": "南微医学",
|
||||
"industry": "微创医疗器械",
|
||||
"sector": "医疗器械",
|
||||
"listed_board": "科创板",
|
||||
"description": "全球领先的微创诊疗器械企业,内镜下诊疗器械龙头",
|
||||
"core_tech_personnel": [
|
||||
{"name": "隆晓辉", "title": "创始人/董事长", "status": "在职", "importance": "极高"},
|
||||
{"name": "冷德嵘", "title": "总经理", "status": "在职", "importance": "高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 2580000000,
|
||||
"net_profit_2024": 628000000,
|
||||
"rd_expense_2024": 365000000,
|
||||
"rd_capitalization_rate": 0.12,
|
||||
"rd_revenue_ratio": 0.141,
|
||||
"top5_customer_ratio": 0.18,
|
||||
"top5_supplier_ratio": 0.32,
|
||||
"receivable_turnover": 7.5,
|
||||
"cash_flow_ratio": 1.42
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "一次性内镜、电外科手术器械、AI辅助诊断",
|
||||
"competing_techs": ["波士顿科学", "奥林巴斯", "库克医疗"],
|
||||
"tech_moat": "国产微创器械龙头,海外收入占比超40%",
|
||||
"patent_count": 1200
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["不锈钢/钛合金供应商", "精密注塑件供应商"],
|
||||
"key_customers": ["全球三甲医院", "海外经销商网络"],
|
||||
"supplier_concentration_risk": "低"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688126",
|
||||
"company_name": "沪硅产业集团股份有限公司",
|
||||
"short_name": "沪硅产业",
|
||||
"industry": "半导体硅片",
|
||||
"sector": "芯片",
|
||||
"listed_board": "科创板",
|
||||
"description": "国内规模最大的半导体硅片制造企业,300mm大硅片龙头",
|
||||
"core_tech_personnel": [
|
||||
{"name": "林林", "title": "总裁", "status": "在职", "importance": "极高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 3285000000,
|
||||
"net_profit_2024": -156000000,
|
||||
"rd_expense_2024": 498000000,
|
||||
"rd_capitalization_rate": 0.35,
|
||||
"rd_revenue_ratio": 0.152,
|
||||
"top5_customer_ratio": 0.62,
|
||||
"top5_supplier_ratio": 0.48,
|
||||
"receivable_turnover": 5.1,
|
||||
"cash_flow_ratio": 0.78
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "300mm半导体硅片(12英寸)量产",
|
||||
"competing_techs": ["日本信越化学", "日本SUMCO", "韩国SK Siltron"],
|
||||
"tech_moat": "国产替代第一梯队,但高端产品仍有差距",
|
||||
"patent_count": 650
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["多晶硅供应商", "石英坩埚供应商"],
|
||||
"key_customers": ["中芯国际", "华虹半导体", "长江存储"],
|
||||
"supplier_concentration_risk": "中"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688223",
|
||||
"company_name": "晶科能源股份有限公司",
|
||||
"short_name": "晶科能源",
|
||||
"industry": "光伏组件",
|
||||
"sector": "新能源",
|
||||
"listed_board": "科创板",
|
||||
"description": "全球领先的光伏组件制造商,N型TOPCon技术全球领先",
|
||||
"core_tech_personnel": [
|
||||
{"name": "李仙德", "title": "创始人/董事长", "status": "在职", "importance": "极高"},
|
||||
{"name": "金浩", "title": "首席科学家", "status": "在职", "importance": "高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 84300000000,
|
||||
"net_profit_2024": 1050000000,
|
||||
"rd_expense_2024": 4890000000,
|
||||
"rd_capitalization_rate": 0.08,
|
||||
"rd_revenue_ratio": 0.058,
|
||||
"top5_customer_ratio": 0.25,
|
||||
"top5_supplier_ratio": 0.40,
|
||||
"receivable_turnover": 6.2,
|
||||
"cash_flow_ratio": 0.92
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "N型TOPCon高效电池, 钙钛矿叠层研发",
|
||||
"competing_techs": ["隆基HJT路线", "钙钛矿技术", "IBC电池"],
|
||||
"tech_moat": "TOPCon量产规模全球第一,但面临技术路线之争",
|
||||
"patent_count": 2200
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "未列入(但面临美国反倾销关税)",
|
||||
"sanctions_detail": "面临美国、欧盟反倾销/反补贴调查"
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["硅料供应商(通威/大全)", "银浆供应商", "EVA胶膜供应商"],
|
||||
"key_customers": ["全球EPC总包商", "欧美分布式市场", "中东/非洲新兴市场"],
|
||||
"supplier_concentration_risk": "中——硅料价格波动大"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688036",
|
||||
"company_name": "传音控股股份有限公司",
|
||||
"short_name": "传音控股",
|
||||
"industry": "智能终端",
|
||||
"sector": "消费电子",
|
||||
"listed_board": "科创板",
|
||||
"description": "非洲市场手机销量第一,旗下TECNO/Infinix/itel品牌",
|
||||
"core_tech_personnel": [
|
||||
{"name": "竺兆江", "title": "创始人/董事长", "status": "在职", "importance": "极高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 65800000000,
|
||||
"net_profit_2024": 5620000000,
|
||||
"rd_expense_2024": 3250000000,
|
||||
"rd_capitalization_rate": 0.0,
|
||||
"rd_revenue_ratio": 0.049,
|
||||
"top5_customer_ratio": 0.32,
|
||||
"top5_supplier_ratio": 0.58,
|
||||
"receivable_turnover": 9.8,
|
||||
"cash_flow_ratio": 1.25
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "深肤色相机算法, AI翻译, 本地化OS",
|
||||
"competing_techs": ["三星", "小米", "OPPO"],
|
||||
"tech_moat": "新兴市场本地化经验深厚",
|
||||
"patent_count": 4500
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "已备案(AI相机美颜算法)",
|
||||
"data_export_risk": "高(大量非洲/东南亚用户数据跨境)",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["联发科(芯片)", "三星SDI/ATL(电池)", "京东方(屏幕)"],
|
||||
"key_customers": ["非洲运营商", "东南亚分销商", "拉美零售商"],
|
||||
"supplier_concentration_risk": "中——芯片依赖联发科"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688005",
|
||||
"company_name": "容百科技股份有限公司",
|
||||
"short_name": "容百科技",
|
||||
"industry": "锂电正极材料",
|
||||
"sector": "新能源",
|
||||
"listed_board": "科创板",
|
||||
"description": "全球领先的锂电池正极材料供应商,高镍三元材料龙头",
|
||||
"core_tech_personnel": [
|
||||
{"name": "白厚善", "title": "创始人/董事长", "status": "在职", "importance": "极高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 18900000000,
|
||||
"net_profit_2024": 356000000,
|
||||
"rd_expense_2024": 785000000,
|
||||
"rd_capitalization_rate": 0.18,
|
||||
"rd_revenue_ratio": 0.042,
|
||||
"top5_customer_ratio": 0.78,
|
||||
"top5_supplier_ratio": 0.65,
|
||||
"receivable_turnover": 4.5,
|
||||
"cash_flow_ratio": 0.68
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "超高镍三元正极材料(Ni>90%), 固态电池正极",
|
||||
"competing_techs": ["磷酸铁锂(比亚迪路线)", "钠离子电池", "固态电解质"],
|
||||
"tech_moat": "高镍三元出货量全球前三",
|
||||
"patent_count": 450
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["格林美(镍钴原料)", "华友钴业"],
|
||||
"key_customers": ["宁德时代", "三星SDI", "SK On"],
|
||||
"supplier_concentration_risk": "高——原材料价格波动大"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688787",
|
||||
"company_name": "海天瑞声科技股份有限公司",
|
||||
"short_name": "海天瑞声",
|
||||
"industry": "AI训练数据",
|
||||
"sector": "AI",
|
||||
"listed_board": "科创板",
|
||||
"description": "国内领先的AI训练数据解决方案提供商",
|
||||
"core_tech_personnel": [
|
||||
{"name": "贺琳", "title": "创始人/董事长", "status": "在职", "importance": "极高"},
|
||||
{"name": "郭蕾", "title": "副总经理/技术负责人", "status": "离职(2024年8月)", "importance": "高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 356000000,
|
||||
"net_profit_2024": -48000000,
|
||||
"rd_expense_2024": 102000000,
|
||||
"rd_capitalization_rate": 0.42,
|
||||
"rd_revenue_ratio": 0.287,
|
||||
"top5_customer_ratio": 0.88,
|
||||
"top5_supplier_ratio": 0.35,
|
||||
"receivable_turnover": 2.8,
|
||||
"cash_flow_ratio": 0.35
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "多语种语音数据集, 自动驾驶标注数据, AIGC合成数据",
|
||||
"competing_techs": ["Scale AI", "Appen", "AI合成数据替代人工标注"],
|
||||
"tech_moat": "多语种覆盖广泛,但面临AIGC合成数据的颠覆性威胁",
|
||||
"patent_count": 180
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "已备案(智能标注平台算法)",
|
||||
"data_export_risk": "高(大量跨境数据采集与交付)",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["全球数据采集众包网络"],
|
||||
"key_customers": ["字节跳动", "百度", "某大型车企"],
|
||||
"supplier_concentration_risk": "中"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"supply_relations": [
|
||||
{"from": "ASML", "to": "中芯国际", "relation": "供应光刻设备", "critical": true, "status": "受限"},
|
||||
{"from": "东京电子", "to": "中芯国际", "relation": "供应刻蚀设备", "critical": true, "status": "受限"},
|
||||
{"from": "应用材料", "to": "中芯国际", "relation": "供应薄膜沉积设备", "critical": true, "status": "受限"},
|
||||
{"from": "中芯国际", "to": "寒武纪", "relation": "芯片代工", "critical": true, "status": "正常"},
|
||||
{"from": "中芯国际", "to": "华为海思", "relation": "芯片代工", "critical": true, "status": "受限"},
|
||||
{"from": "台积电", "to": "寒武纪", "relation": "先进制程代工", "critical": true, "status": "受限"},
|
||||
{"from": "英伟达", "to": "金山办公", "relation": "供应GPU算力", "critical": true, "status": "受限"},
|
||||
{"from": "英伟达", "to": "海天瑞声", "relation": "供应GPU算力(客户需求)", "critical": false, "status": "正常"},
|
||||
{"from": "联发科", "to": "传音控股", "relation": "供应手机芯片", "critical": true, "status": "正常"},
|
||||
{"from": "三星SDI", "to": "传音控股", "relation": "供应电池", "critical": false, "status": "正常"},
|
||||
{"from": "京东方", "to": "传音控股", "relation": "供应屏幕", "critical": false, "status": "正常"},
|
||||
{"from": "沪硅产业", "to": "中芯国际", "relation": "供应半导体硅片", "critical": true, "status": "正常"},
|
||||
{"from": "沪硅产业", "to": "华虹半导体", "relation": "供应半导体硅片", "critical": true, "status": "正常"},
|
||||
{"from": "沪硅产业", "to": "长江存储", "relation": "供应半导体硅片", "critical": true, "status": "正常"},
|
||||
{"from": "格林美", "to": "容百科技", "relation": "供应镍钴原料", "critical": true, "status": "正常"},
|
||||
{"from": "华友钴业", "to": "容百科技", "relation": "供应钴原料", "critical": true, "status": "正常"},
|
||||
{"from": "容百科技", "to": "宁德时代", "relation": "供应正极材料", "critical": true, "status": "正常"},
|
||||
{"from": "容百科技", "to": "三星SDI", "relation": "供应正极材料", "critical": false, "status": "正常"},
|
||||
{"from": "通威股份", "to": "晶科能源", "relation": "供应硅料", "critical": true, "status": "正常"},
|
||||
{"from": "大全能源", "to": "晶科能源", "relation": "供应硅料", "critical": true, "status": "正常"},
|
||||
{"from": "海天瑞声", "to": "字节跳动", "relation": "供应AI训练数据", "critical": false, "status": "正常"},
|
||||
{"from": "海天瑞声", "to": "百度", "relation": "供应AI训练数据", "critical": false, "status": "正常"}
|
||||
],
|
||||
"investment_relations": [
|
||||
{"from": "国家集成电路产业基金", "to": "中芯国际", "relation": "战略投资", "share_ratio": 0.15},
|
||||
{"from": "国家集成电路产业基金", "to": "沪硅产业", "relation": "战略投资", "share_ratio": 0.08},
|
||||
{"from": "国家集成电路产业基金", "to": "长江存储", "relation": "战略投资", "share_ratio": 0.20}
|
||||
],
|
||||
"personnel_relations": [
|
||||
{"person": "梁孟松", "from_company": "台积电/三星", "to_company": "中芯国际", "relation": "技术负责人跳槽", "year": 2017},
|
||||
{"person": "郭蕾", "from_company": "海天瑞声", "to_company": "未知", "relation": "核心技术人员离职", "year": 2024}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""知识图谱模块"""
|
||||
@@ -0,0 +1,169 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
供应链风险传染分析器
|
||||
基于 BFS 遍历实现风险穿透,计算传染距离和影响权重
|
||||
"""
|
||||
import logging
|
||||
from collections import deque
|
||||
from typing import Optional
|
||||
|
||||
import networkx as nx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def analyze_contagion(G: nx.DiGraph, target_company: str) -> dict:
|
||||
"""
|
||||
分析指定企业的供应链风险传染情况
|
||||
从上游(供应商)方向进行 BFS 穿透
|
||||
"""
|
||||
if target_company not in G:
|
||||
return {"error": f"企业 '{target_company}' 不在图谱中"}
|
||||
|
||||
result = {
|
||||
"company": target_company,
|
||||
"direct_risks": [], # 直接风险(一度关联)
|
||||
"indirect_risks": [], # 间接风险(二度及以上关联)
|
||||
"contagion_paths": [], # 风险传染路径
|
||||
"risk_score": 0, # 供应链风险总分
|
||||
"critical_nodes": [], # 关键断裂节点
|
||||
}
|
||||
|
||||
# BFS 从目标企业向上游遍历
|
||||
visited = set()
|
||||
queue = deque([(target_company, 0, [target_company])])
|
||||
visited.add(target_company)
|
||||
|
||||
while queue:
|
||||
current, depth, path = queue.popleft()
|
||||
|
||||
# 检查当前节点的上游(前驱节点)
|
||||
for predecessor in G.predecessors(current):
|
||||
if predecessor in visited:
|
||||
continue
|
||||
visited.add(predecessor)
|
||||
|
||||
edge_data = G.edges[predecessor, current]
|
||||
node_data = G.nodes.get(predecessor, {})
|
||||
new_path = [predecessor] + path
|
||||
|
||||
# 检查上游节点是否受制裁
|
||||
if node_data.get("is_sanctioned", False):
|
||||
risk_entry = {
|
||||
"entity": predecessor,
|
||||
"node_type": node_data.get("node_type", "未知"),
|
||||
"distance": depth + 1,
|
||||
"relation": edge_data.get("relation", ""),
|
||||
"is_critical": edge_data.get("critical", False),
|
||||
"status": edge_data.get("status", "正常"),
|
||||
"path": " → ".join(new_path),
|
||||
}
|
||||
|
||||
if depth == 0:
|
||||
result["direct_risks"].append(risk_entry)
|
||||
else:
|
||||
result["indirect_risks"].append(risk_entry)
|
||||
|
||||
result["contagion_paths"].append({
|
||||
"path": new_path,
|
||||
"path_str": " → ".join(new_path),
|
||||
"length": len(new_path),
|
||||
"severity": "高" if edge_data.get("critical", False) else "中",
|
||||
})
|
||||
|
||||
# 检查受限状态的边
|
||||
if edge_data.get("status") == "受限":
|
||||
if predecessor not in [r["entity"] for r in result["direct_risks"] + result["indirect_risks"]]:
|
||||
risk_entry = {
|
||||
"entity": predecessor,
|
||||
"node_type": node_data.get("node_type", "未知"),
|
||||
"distance": depth + 1,
|
||||
"relation": edge_data.get("relation", ""),
|
||||
"is_critical": edge_data.get("critical", False),
|
||||
"status": "受限",
|
||||
"path": " → ".join(new_path),
|
||||
}
|
||||
if depth == 0:
|
||||
result["direct_risks"].append(risk_entry)
|
||||
else:
|
||||
result["indirect_risks"].append(risk_entry)
|
||||
|
||||
# 继续向上游遍历(最多3层)
|
||||
if depth < 2:
|
||||
queue.append((predecessor, depth + 1, new_path))
|
||||
|
||||
# 计算供应链风险得分
|
||||
result["risk_score"] = _calculate_supply_chain_risk_score(result)
|
||||
|
||||
# 识别关键断裂节点
|
||||
result["critical_nodes"] = _find_critical_nodes(G, target_company)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _calculate_supply_chain_risk_score(contagion_result: dict) -> float:
|
||||
"""计算供应链风险综合得分 (0-100)"""
|
||||
score = 0
|
||||
|
||||
# 直接风险:每个 +25 分,关键供应 +35 分
|
||||
for risk in contagion_result["direct_risks"]:
|
||||
if risk["is_critical"]:
|
||||
score += 35
|
||||
else:
|
||||
score += 25
|
||||
|
||||
# 间接风险:每个 +10 分,关键供应 +15 分
|
||||
for risk in contagion_result["indirect_risks"]:
|
||||
if risk["is_critical"]:
|
||||
score += 15
|
||||
else:
|
||||
score += 10
|
||||
|
||||
return min(score, 100) # 上限 100
|
||||
|
||||
|
||||
def _find_critical_nodes(G: nx.DiGraph, target: str) -> list:
|
||||
"""
|
||||
识别关键断裂节点:如果移除该节点,目标企业将失去关键供应来源
|
||||
"""
|
||||
critical = []
|
||||
predecessors = list(G.predecessors(target))
|
||||
|
||||
for pred in predecessors:
|
||||
edge_data = G.edges[pred, target]
|
||||
if edge_data.get("critical", False):
|
||||
# 检查是否有替代供应商
|
||||
alternatives = sum(
|
||||
1 for p in predecessors
|
||||
if p != pred and G.edges[p, target].get("relation", "") == edge_data.get("relation", "")
|
||||
)
|
||||
critical.append({
|
||||
"node": pred,
|
||||
"relation": edge_data.get("relation", ""),
|
||||
"has_alternative": alternatives > 0,
|
||||
"alternative_count": alternatives,
|
||||
"status": edge_data.get("status", "正常"),
|
||||
})
|
||||
|
||||
return critical
|
||||
|
||||
|
||||
def find_all_risk_paths(G: nx.DiGraph, source: str, target: str, max_depth: int = 4) -> list:
|
||||
"""
|
||||
查找两个节点之间的所有风险路径
|
||||
"""
|
||||
if source not in G or target not in G:
|
||||
return []
|
||||
|
||||
try:
|
||||
paths = list(nx.all_simple_paths(G, source, target, cutoff=max_depth))
|
||||
return [
|
||||
{
|
||||
"path": p,
|
||||
"path_str": " → ".join(p),
|
||||
"length": len(p),
|
||||
}
|
||||
for p in paths
|
||||
]
|
||||
except nx.NetworkXError:
|
||||
return []
|
||||
@@ -0,0 +1,214 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
知识图谱构建引擎
|
||||
基于 NetworkX 构建科创企业供应链风险传染图谱
|
||||
节点类型:企业、供应商、客户、核心人员、制裁实体
|
||||
边类型:供应关系、客户关系、任职关系、投资关系
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import networkx as nx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||
|
||||
|
||||
def build_graph() -> nx.DiGraph:
|
||||
"""
|
||||
构建完整的科创企业供应链风险知识图谱
|
||||
"""
|
||||
G = nx.DiGraph()
|
||||
|
||||
# 1. 加载企业数据,添加企业节点
|
||||
_add_company_nodes(G)
|
||||
|
||||
# 2. 加载供应链数据,添加关系边
|
||||
_add_supply_chain_edges(G)
|
||||
|
||||
# 3. 加载实体清单,标记受制裁节点
|
||||
_mark_sanctioned_nodes(G)
|
||||
|
||||
logger.info(f"图谱构建完成: {G.number_of_nodes()} 节点, {G.number_of_edges()} 边")
|
||||
return G
|
||||
|
||||
|
||||
def _add_company_nodes(G: nx.DiGraph):
|
||||
"""添加科创板企业节点及其关联的核心人员节点"""
|
||||
filepath = DATA_DIR / "sample_companies.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
companies = json.load(f)
|
||||
|
||||
for company in companies:
|
||||
name = company["short_name"]
|
||||
G.add_node(
|
||||
name,
|
||||
node_type="科创企业",
|
||||
stock_code=company["stock_code"],
|
||||
industry=company["industry"],
|
||||
sector=company["sector"],
|
||||
is_sanctioned=company["compliance"]["entity_list_status"] != "未列入",
|
||||
risk_level="正常",
|
||||
color="#4CAF50", # 默认绿色
|
||||
)
|
||||
|
||||
# 添加核心技术人员节点
|
||||
for person in company.get("core_tech_personnel", []):
|
||||
person_id = f"{person['name']}@{name}"
|
||||
G.add_node(
|
||||
person_id,
|
||||
node_type="核心人员",
|
||||
real_name=person["name"],
|
||||
title=person["title"],
|
||||
status=person["status"],
|
||||
importance=person["importance"],
|
||||
company=name,
|
||||
color="#2196F3", # 蓝色
|
||||
)
|
||||
G.add_edge(
|
||||
person_id, name,
|
||||
relation="任职于",
|
||||
edge_type="personnel",
|
||||
)
|
||||
|
||||
# 添加供应商节点
|
||||
for supplier in company.get("supply_chain", {}).get("key_suppliers", []):
|
||||
supplier_name = supplier.split("(")[0].split("(")[0].strip()
|
||||
if not G.has_node(supplier_name):
|
||||
G.add_node(
|
||||
supplier_name,
|
||||
node_type="供应商",
|
||||
is_sanctioned=False,
|
||||
risk_level="正常",
|
||||
color="#FF9800", # 橙色
|
||||
)
|
||||
G.add_edge(
|
||||
supplier_name, name,
|
||||
relation="供应",
|
||||
detail=supplier,
|
||||
edge_type="supply",
|
||||
)
|
||||
|
||||
# 添加客户节点
|
||||
for customer in company.get("supply_chain", {}).get("key_customers", []):
|
||||
customer_name = customer.split("(")[0].split("(")[0].strip()
|
||||
if not G.has_node(customer_name):
|
||||
G.add_node(
|
||||
customer_name,
|
||||
node_type="客户",
|
||||
is_sanctioned=False,
|
||||
risk_level="正常",
|
||||
color="#9C27B0", # 紫色
|
||||
)
|
||||
G.add_edge(
|
||||
name, customer_name,
|
||||
relation="供货给",
|
||||
edge_type="customer",
|
||||
)
|
||||
|
||||
|
||||
def _add_supply_chain_edges(G: nx.DiGraph):
|
||||
"""从供应链关系文件添加更详细的边"""
|
||||
filepath = DATA_DIR / "supply_chain.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# 添加供应关系
|
||||
for rel in data.get("supply_relations", []):
|
||||
from_node = rel["from"]
|
||||
to_node = rel["to"]
|
||||
|
||||
# 确保节点存在
|
||||
if not G.has_node(from_node):
|
||||
G.add_node(from_node, node_type="供应商", is_sanctioned=False,
|
||||
risk_level="正常", color="#FF9800")
|
||||
if not G.has_node(to_node):
|
||||
G.add_node(to_node, node_type="企业", is_sanctioned=False,
|
||||
risk_level="正常", color="#4CAF50")
|
||||
|
||||
G.add_edge(
|
||||
from_node, to_node,
|
||||
relation=rel["relation"],
|
||||
critical=rel.get("critical", False),
|
||||
status=rel.get("status", "正常"),
|
||||
edge_type="supply",
|
||||
)
|
||||
|
||||
# 添加投资关系
|
||||
for rel in data.get("investment_relations", []):
|
||||
from_node = rel["from"]
|
||||
to_node = rel["to"]
|
||||
|
||||
if not G.has_node(from_node):
|
||||
G.add_node(from_node, node_type="投资方", is_sanctioned=False,
|
||||
risk_level="正常", color="#607D8B")
|
||||
|
||||
G.add_edge(
|
||||
from_node, to_node,
|
||||
relation=rel["relation"],
|
||||
share_ratio=rel.get("share_ratio", 0),
|
||||
edge_type="investment",
|
||||
)
|
||||
|
||||
|
||||
def _mark_sanctioned_nodes(G: nx.DiGraph):
|
||||
"""标记受制裁的节点,并向下游传播风险"""
|
||||
filepath = DATA_DIR / "entity_list.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
entities = json.load(f)
|
||||
|
||||
# 收集所有受制裁实体的名称和别名
|
||||
sanctioned_names = set()
|
||||
for entity in entities:
|
||||
sanctioned_names.add(entity["entity_name"])
|
||||
for alias in entity.get("aliases", []):
|
||||
sanctioned_names.add(alias)
|
||||
|
||||
# 标记图谱中的受制裁节点
|
||||
for node in G.nodes():
|
||||
for sname in sanctioned_names:
|
||||
if node in sname or sname in node:
|
||||
G.nodes[node]["is_sanctioned"] = True
|
||||
G.nodes[node]["risk_level"] = "高危"
|
||||
G.nodes[node]["color"] = "#F44336" # 红色
|
||||
break
|
||||
|
||||
|
||||
def get_node_info(G: nx.DiGraph, node_name: str) -> dict:
|
||||
"""获取节点详细信息"""
|
||||
if node_name not in G:
|
||||
return {"error": f"节点 '{node_name}' 不存在"}
|
||||
|
||||
node_data = dict(G.nodes[node_name])
|
||||
predecessors = list(G.predecessors(node_name))
|
||||
successors = list(G.successors(node_name))
|
||||
|
||||
return {
|
||||
"name": node_name,
|
||||
"attributes": node_data,
|
||||
"upstream": predecessors,
|
||||
"downstream": successors,
|
||||
"degree": G.degree(node_name),
|
||||
}
|
||||
|
||||
|
||||
def get_graph_stats(G: nx.DiGraph) -> dict:
|
||||
"""获取图谱统计信息"""
|
||||
node_types = {}
|
||||
for _, data in G.nodes(data=True):
|
||||
t = data.get("node_type", "未知")
|
||||
node_types[t] = node_types.get(t, 0) + 1
|
||||
|
||||
sanctioned_count = sum(
|
||||
1 for _, data in G.nodes(data=True) if data.get("is_sanctioned", False)
|
||||
)
|
||||
|
||||
return {
|
||||
"total_nodes": G.number_of_nodes(),
|
||||
"total_edges": G.number_of_edges(),
|
||||
"node_types": node_types,
|
||||
"sanctioned_nodes": sanctioned_count,
|
||||
"density": nx.density(G),
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
知识图谱可视化模块
|
||||
使用 pyvis 生成交互式网络图,支持嵌入 Streamlit
|
||||
"""
|
||||
import logging
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import networkx as nx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 节点类型对应的颜色和形状
|
||||
NODE_STYLES = {
|
||||
"科创企业": {"color": "#4CAF50", "shape": "dot", "size": 30},
|
||||
"供应商": {"color": "#FF9800", "shape": "diamond", "size": 20},
|
||||
"客户": {"color": "#9C27B0", "shape": "triangle", "size": 20},
|
||||
"核心人员": {"color": "#2196F3", "shape": "star", "size": 15},
|
||||
"投资方": {"color": "#607D8B", "shape": "square", "size": 20},
|
||||
"企业": {"color": "#4CAF50", "shape": "dot", "size": 25},
|
||||
}
|
||||
|
||||
# 受制裁节点的样式覆盖
|
||||
SANCTIONED_STYLE = {"color": "#F44336", "size": 35}
|
||||
# 受限边的样式
|
||||
RESTRICTED_EDGE_STYLE = {"color": "#F44336", "dashes": True, "width": 3}
|
||||
|
||||
|
||||
def generate_interactive_graph(
|
||||
G: nx.DiGraph,
|
||||
highlight_company: Optional[str] = None,
|
||||
output_path: Optional[str] = None,
|
||||
height: str = "600px",
|
||||
width: str = "100%",
|
||||
) -> str:
|
||||
"""
|
||||
生成交互式知识图谱 HTML
|
||||
"""
|
||||
try:
|
||||
from pyvis.network import Network
|
||||
except ImportError:
|
||||
logger.error("pyvis 未安装,请运行: pip install pyvis")
|
||||
return _generate_fallback_html(G)
|
||||
|
||||
net = Network(
|
||||
height=height,
|
||||
width=width,
|
||||
directed=True,
|
||||
notebook=False,
|
||||
bgcolor="#1a1a2e",
|
||||
font_color="white",
|
||||
)
|
||||
|
||||
# 物理引擎配置
|
||||
net.set_options("""
|
||||
{
|
||||
"physics": {
|
||||
"forceAtlas2Based": {
|
||||
"gravitationalConstant": -50,
|
||||
"centralGravity": 0.01,
|
||||
"springLength": 150,
|
||||
"springConstant": 0.08
|
||||
},
|
||||
"solver": "forceAtlas2Based",
|
||||
"stabilization": {"iterations": 100}
|
||||
},
|
||||
"interaction": {
|
||||
"hover": true,
|
||||
"tooltipDelay": 200,
|
||||
"navigationButtons": true
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
# 添加节点
|
||||
for node, data in G.nodes(data=True):
|
||||
node_type = data.get("node_type", "企业")
|
||||
style = NODE_STYLES.get(node_type, NODE_STYLES["企业"]).copy()
|
||||
|
||||
# 受制裁节点特殊样式
|
||||
if data.get("is_sanctioned", False):
|
||||
style.update(SANCTIONED_STYLE)
|
||||
|
||||
# 高亮选中企业
|
||||
if highlight_company and node == highlight_company:
|
||||
style["color"] = "#FFD700" # 金色
|
||||
style["size"] = 45
|
||||
style["borderWidth"] = 3
|
||||
|
||||
# 构建标签和悬浮提示
|
||||
label = node.split("@")[0] if "@" in node else node
|
||||
title_parts = [f"<b>{label}</b>", f"类型: {node_type}"]
|
||||
if data.get("is_sanctioned"):
|
||||
title_parts.append("⚠️ <b>已被制裁</b>")
|
||||
if data.get("stock_code"):
|
||||
title_parts.append(f"代码: {data['stock_code']}")
|
||||
if data.get("industry"):
|
||||
title_parts.append(f"行业: {data['industry']}")
|
||||
if data.get("title"):
|
||||
title_parts.append(f"职位: {data['title']}")
|
||||
if data.get("status"):
|
||||
title_parts.append(f"状态: {data['status']}")
|
||||
|
||||
net.add_node(
|
||||
node,
|
||||
label=label,
|
||||
title="<br>".join(title_parts),
|
||||
color=style["color"],
|
||||
shape=style["shape"],
|
||||
size=style["size"],
|
||||
)
|
||||
|
||||
# 添加边
|
||||
for u, v, data in G.edges(data=True):
|
||||
edge_style = {
|
||||
"color": "#666666",
|
||||
"width": 1,
|
||||
"arrows": "to",
|
||||
}
|
||||
|
||||
# 受限边特殊样式
|
||||
if data.get("status") == "受限":
|
||||
edge_style.update(RESTRICTED_EDGE_STYLE)
|
||||
elif data.get("critical", False):
|
||||
edge_style["width"] = 2
|
||||
edge_style["color"] = "#FFC107" # 关键边用黄色
|
||||
|
||||
relation = data.get("relation", "")
|
||||
net.add_edge(
|
||||
u, v,
|
||||
title=relation,
|
||||
label=relation if len(relation) <= 8 else "",
|
||||
**edge_style,
|
||||
)
|
||||
|
||||
# 输出 HTML
|
||||
if output_path is None:
|
||||
output_path = str(Path(tempfile.gettempdir()) / "kg_visualization.html")
|
||||
|
||||
net.save_graph(output_path)
|
||||
|
||||
# 读取 HTML 内容
|
||||
with open(output_path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def _generate_fallback_html(G: nx.DiGraph) -> str:
|
||||
"""当 pyvis 不可用时的备用 HTML 可视化"""
|
||||
nodes_info = []
|
||||
for node, data in G.nodes(data=True):
|
||||
label = node.split("@")[0] if "@" in node else node
|
||||
is_sanctioned = data.get("is_sanctioned", False)
|
||||
nodes_info.append(f"<li style='color: {'red' if is_sanctioned else 'green'}'>{label} ({data.get('node_type', '未知')})</li>")
|
||||
|
||||
return f"""
|
||||
<html><body style='background: #1a1a2e; color: white; padding: 20px;'>
|
||||
<h2>📊 知识图谱节点列表(pyvis 未安装,使用简化视图)</h2>
|
||||
<p>节点数: {G.number_of_nodes()} | 边数: {G.number_of_edges()}</p>
|
||||
<ul>{''.join(nodes_info[:50])}</ul>
|
||||
<p style='color: #888;'>安装 pyvis 以获得交互式可视化: pip install pyvis</p>
|
||||
</body></html>
|
||||
"""
|
||||
|
||||
|
||||
def get_subgraph_for_company(G: nx.DiGraph, company: str, depth: int = 2) -> nx.DiGraph:
|
||||
"""
|
||||
提取以指定企业为中心的子图(上下游 N 层)
|
||||
"""
|
||||
if company not in G:
|
||||
return nx.DiGraph()
|
||||
|
||||
# 收集相关节点
|
||||
related_nodes = {company}
|
||||
|
||||
# 上游(前驱)
|
||||
current_layer = {company}
|
||||
for _ in range(depth):
|
||||
next_layer = set()
|
||||
for node in current_layer:
|
||||
next_layer.update(G.predecessors(node))
|
||||
related_nodes.update(next_layer)
|
||||
current_layer = next_layer
|
||||
|
||||
# 下游(后继)
|
||||
current_layer = {company}
|
||||
for _ in range(depth):
|
||||
next_layer = set()
|
||||
for node in current_layer:
|
||||
next_layer.update(G.successors(node))
|
||||
related_nodes.update(next_layer)
|
||||
current_layer = next_layer
|
||||
|
||||
return G.subgraph(related_nodes).copy()
|
||||
@@ -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,
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
streamlit>=1.30.0
|
||||
pandas>=2.0.0
|
||||
networkx>=3.0
|
||||
pyvis>=0.3.2
|
||||
plotly>=5.18.0
|
||||
requests>=2.31.0
|
||||
openai>=1.10.0
|
||||
pdfplumber>=0.10.0
|
||||
PyPDF2>=3.0.0
|
||||
akshare>=1.12.0
|
||||
jieba>=0.42.1
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""风险评估引擎模块"""
|
||||
@@ -0,0 +1,184 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
动态保险定价模型
|
||||
基于精算原理,结合六维风险评分实现"千企千面"费率计算
|
||||
"""
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 将项目根目录添加到 Python 路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from config import INSURANCE_PRODUCTS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 行业风险系数
|
||||
INDUSTRY_RISK_FACTORS = {
|
||||
"芯片": 1.35, # 地缘风险+技术迭代双高
|
||||
"AI": 1.30, # 合规风险+技术竞争
|
||||
"软件": 1.05, # 相对成熟
|
||||
"医疗器械": 0.95, # 技术壁垒高但风险较稳
|
||||
"新能源": 1.15, # 技术路线之争+贸易摩擦
|
||||
"消费电子": 1.10, # 供应链风险
|
||||
}
|
||||
|
||||
# 企业规模折扣(大企业风险分散能力更强)
|
||||
SCALE_DISCOUNT = {
|
||||
"超大型": 0.85, # 营收 > 500亿
|
||||
"大型": 0.90, # 100-500亿
|
||||
"中型": 1.00, # 10-100亿
|
||||
"小型": 1.15, # 1-10亿
|
||||
"微型": 1.30, # < 1亿
|
||||
}
|
||||
|
||||
|
||||
def get_enterprise_scale(revenue: float) -> str:
|
||||
"""根据营收判断企业规模"""
|
||||
if revenue >= 50_000_000_000:
|
||||
return "超大型"
|
||||
elif revenue >= 10_000_000_000:
|
||||
return "大型"
|
||||
elif revenue >= 1_000_000_000:
|
||||
return "中型"
|
||||
elif revenue >= 100_000_000:
|
||||
return "小型"
|
||||
return "微型"
|
||||
|
||||
|
||||
def calculate_premium(
|
||||
product_key: str,
|
||||
comprehensive_risk_score: int,
|
||||
six_dimension_scores: dict,
|
||||
sector: str = "",
|
||||
revenue: float = 0,
|
||||
) -> dict:
|
||||
"""
|
||||
动态费率计算器
|
||||
|
||||
定价公式:
|
||||
实际保费 = 基础保费 × 风险系数 × 行业调整 × 规模折扣
|
||||
|
||||
风险系数由综合评分决定(分段线性):
|
||||
- [0, 30) → 0.7 ~ 0.9(优质折扣)
|
||||
- [30, 50) → 0.9 ~ 1.2(标准浮动)
|
||||
- [50, 70) → 1.2 ~ 1.8(风险上浮)
|
||||
- [70, 90) → 1.8 ~ 2.5(惩罚性费率)
|
||||
- [90,100] → 拒保或附加极高免赔额
|
||||
"""
|
||||
product = INSURANCE_PRODUCTS.get(product_key)
|
||||
if not product:
|
||||
return {"error": f"未知保险产品: {product_key}"}
|
||||
|
||||
base_premium = product["base_premium"]
|
||||
base_coverage = product["base_coverage"]
|
||||
|
||||
# 1. 计算风险系数
|
||||
risk_multiplier = _calculate_risk_multiplier(comprehensive_risk_score)
|
||||
|
||||
# 2. 行业调整系数
|
||||
industry_factor = INDUSTRY_RISK_FACTORS.get(sector, 1.0)
|
||||
|
||||
# 3. 规模折扣
|
||||
scale = get_enterprise_scale(revenue)
|
||||
scale_factor = SCALE_DISCOUNT[scale]
|
||||
|
||||
# 4. 特定险种的维度调整
|
||||
dimension_adjustment = _get_dimension_adjustment(product_key, six_dimension_scores)
|
||||
|
||||
# 5. 最终保费
|
||||
final_premium = base_premium * risk_multiplier * industry_factor * scale_factor * dimension_adjustment
|
||||
|
||||
# 6. 核保条件
|
||||
deductible_rate = _calculate_deductible(comprehensive_risk_score)
|
||||
adjusted_coverage = base_coverage * (1.0 if comprehensive_risk_score < 70 else 0.7)
|
||||
|
||||
return {
|
||||
"product_name": product["name"],
|
||||
"product_key": product_key,
|
||||
"base_premium": base_premium,
|
||||
"base_coverage": base_coverage,
|
||||
"risk_multiplier": round(risk_multiplier, 3),
|
||||
"industry_factor": round(industry_factor, 3),
|
||||
"scale_factor": round(scale_factor, 3),
|
||||
"scale_label": scale,
|
||||
"dimension_adjustment": round(dimension_adjustment, 3),
|
||||
"final_premium": round(final_premium, 2),
|
||||
"adjusted_coverage": round(adjusted_coverage, 2),
|
||||
"deductible_rate": round(deductible_rate, 3),
|
||||
"deductible_amount": round(adjusted_coverage * deductible_rate, 2),
|
||||
"comprehensive_risk_score": comprehensive_risk_score,
|
||||
"is_insurable": comprehensive_risk_score < 90,
|
||||
"pricing_breakdown": (
|
||||
f"¥{base_premium:,.0f} × {risk_multiplier:.2f}(风险) "
|
||||
f"× {industry_factor:.2f}(行业) × {scale_factor:.2f}(规模) "
|
||||
f"× {dimension_adjustment:.2f}(维度) = ¥{final_premium:,.0f}"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _calculate_risk_multiplier(score: int) -> float:
|
||||
"""分段线性风险系数计算"""
|
||||
if score < 30:
|
||||
return 0.7 + (score / 30) * 0.2 # 0.7 ~ 0.9
|
||||
elif score < 50:
|
||||
return 0.9 + ((score - 30) / 20) * 0.3 # 0.9 ~ 1.2
|
||||
elif score < 70:
|
||||
return 1.2 + ((score - 50) / 20) * 0.6 # 1.2 ~ 1.8
|
||||
elif score < 90:
|
||||
return 1.8 + ((score - 70) / 20) * 0.7 # 1.8 ~ 2.5
|
||||
else:
|
||||
return 3.0 # 拒保级别
|
||||
|
||||
|
||||
def _get_dimension_adjustment(product_key: str, scores: dict) -> float:
|
||||
"""针对特定险种,根据相关维度得分进行微调"""
|
||||
if product_key == "ip_lawsuit":
|
||||
# 知识产权被诉险:重点看技术路线和专利
|
||||
tech_score = scores.get("tech_disruption", 50)
|
||||
return 0.8 + (tech_score / 100) * 0.4 # 0.8 ~ 1.2
|
||||
|
||||
elif product_key == "exec_departure":
|
||||
# 高管离职险:重点看人员流失风险
|
||||
talent_score = scores.get("talent_loss", 50)
|
||||
return 0.7 + (talent_score / 100) * 0.6 # 0.7 ~ 1.3
|
||||
|
||||
elif product_key == "data_compliance":
|
||||
# 数据合规险:重点看合规风险
|
||||
compliance_score = scores.get("algo_compliance", 50)
|
||||
return 0.8 + (compliance_score / 100) * 0.4 # 0.8 ~ 1.2
|
||||
|
||||
return 1.0
|
||||
|
||||
|
||||
def _calculate_deductible(score: int) -> float:
|
||||
"""计算免赔率"""
|
||||
if score < 30:
|
||||
return 0.05 # 5%
|
||||
elif score < 50:
|
||||
return 0.10 # 10%
|
||||
elif score < 70:
|
||||
return 0.15 # 15%
|
||||
elif score < 90:
|
||||
return 0.25 # 25%
|
||||
else:
|
||||
return 0.50 # 50%(惩罚性高免赔)
|
||||
|
||||
|
||||
def calculate_all_products(
|
||||
comprehensive_risk_score: int,
|
||||
six_dimension_scores: dict,
|
||||
sector: str = "",
|
||||
revenue: float = 0,
|
||||
) -> list:
|
||||
"""计算所有险种的保费"""
|
||||
results = []
|
||||
for product_key in INSURANCE_PRODUCTS:
|
||||
result = calculate_premium(
|
||||
product_key, comprehensive_risk_score,
|
||||
six_dimension_scores, sector, revenue,
|
||||
)
|
||||
results.append(result)
|
||||
return results
|
||||
@@ -0,0 +1,146 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
核保决策报告生成器
|
||||
生成结构化的风险评估报告
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def generate_report(
|
||||
company_data: dict,
|
||||
debate_result: dict,
|
||||
pricing_results: list,
|
||||
) -> dict:
|
||||
"""
|
||||
生成完整的核保决策报告
|
||||
"""
|
||||
company_name = company_data.get("short_name", "未知企业")
|
||||
judge = debate_result.get("judge_result", {})
|
||||
scores = judge.get("six_dimension_scores", {})
|
||||
comprehensive = judge.get("comprehensive_score", 50)
|
||||
decision = judge.get("underwriting_decision", "标准承保")
|
||||
|
||||
report = {
|
||||
"title": f"科创企业智能风控核保报告 —— {company_name}",
|
||||
"report_id": f"RPT-{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||
"generate_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"company_profile": {
|
||||
"name": company_name,
|
||||
"stock_code": company_data.get("stock_code", ""),
|
||||
"industry": company_data.get("industry", ""),
|
||||
"sector": company_data.get("sector", ""),
|
||||
"description": company_data.get("description", ""),
|
||||
},
|
||||
"risk_assessment": {
|
||||
"comprehensive_score": comprehensive,
|
||||
"risk_level": judge.get("risk_level", "中"),
|
||||
"six_dimension_scores": scores,
|
||||
"key_risks": judge.get("key_risks", []),
|
||||
},
|
||||
"debate_summary": {
|
||||
"law_findings": debate_result.get("law_result", {}).get("key_findings", []),
|
||||
"tech_findings": debate_result.get("tech_result", {}).get("key_findings", []),
|
||||
"fin_findings": debate_result.get("finance_result", {}).get("key_findings", []),
|
||||
"conflicts": debate_result.get("conflicts", []),
|
||||
"resolution": judge.get("conflict_resolution", ""),
|
||||
},
|
||||
"underwriting_decision": {
|
||||
"decision": decision,
|
||||
"conditions": judge.get("underwriting_conditions", []),
|
||||
"summary": judge.get("summary", ""),
|
||||
},
|
||||
"pricing": pricing_results,
|
||||
}
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def format_report_markdown(report: dict) -> str:
|
||||
"""将报告格式化为 Markdown"""
|
||||
lines = []
|
||||
lines.append(f"# {report['title']}")
|
||||
lines.append(f"\n> 报告编号: {report['report_id']} | 生成时间: {report['generate_time']}")
|
||||
|
||||
# 企业概况
|
||||
profile = report["company_profile"]
|
||||
lines.append("\n## 一、企业概况")
|
||||
lines.append(f"- **企业名称**: {profile['name']}")
|
||||
lines.append(f"- **股票代码**: {profile['stock_code']}")
|
||||
lines.append(f"- **所属行业**: {profile['industry']}")
|
||||
lines.append(f"- **企业描述**: {profile['description']}")
|
||||
|
||||
# 风险评估
|
||||
risk = report["risk_assessment"]
|
||||
lines.append("\n## 二、风险评估")
|
||||
lines.append(f"- **综合风险评分**: {risk['comprehensive_score']} 分")
|
||||
lines.append(f"- **风险等级**: {risk['risk_level']}")
|
||||
|
||||
lines.append("\n### 六维风险评分")
|
||||
dim_names = {
|
||||
"tech_disruption": "技术路线颠覆",
|
||||
"talent_loss": "核心人员流失",
|
||||
"algo_compliance": "算法/数据合规",
|
||||
"geopolitical": "地缘政治/出口管制",
|
||||
"rd_capitalization": "研发资本化操纵",
|
||||
"concentration": "客户/供应商集中",
|
||||
}
|
||||
lines.append("| 风险维度 | 评分 | 等级 |")
|
||||
lines.append("|---------|------|------|")
|
||||
for dim, score in risk.get("six_dimension_scores", {}).items():
|
||||
name = dim_names.get(dim, dim)
|
||||
level = "高" if score >= 70 else ("中" if score >= 40 else "低")
|
||||
lines.append(f"| {name} | {score} | {level} |")
|
||||
|
||||
# 辩论摘要
|
||||
debate = report["debate_summary"]
|
||||
lines.append("\n## 三、多智能体交叉验证摘要")
|
||||
|
||||
lines.append("\n### 👩⚖️ 法务风控节点")
|
||||
for f in debate.get("law_findings", []):
|
||||
lines.append(f"- {f}")
|
||||
|
||||
lines.append("\n### 👨🔬 技术风控节点")
|
||||
for f in debate.get("tech_findings", []):
|
||||
lines.append(f"- {f}")
|
||||
|
||||
lines.append("\n### 👔 财务风控节点")
|
||||
for f in debate.get("fin_findings", []):
|
||||
lines.append(f"- {f}")
|
||||
|
||||
if debate.get("conflicts"):
|
||||
lines.append("\n### ⚠️ 冲突点")
|
||||
for c in debate["conflicts"]:
|
||||
lines.append(f"- {c}")
|
||||
|
||||
# 核保决策
|
||||
uw = report["underwriting_decision"]
|
||||
lines.append(f"\n## 四、核保决策")
|
||||
lines.append(f"\n**决策**: 【{uw['decision']}】")
|
||||
lines.append(f"\n{uw.get('summary', '')}")
|
||||
|
||||
if uw.get("conditions"):
|
||||
lines.append("\n### 核保附加条件")
|
||||
for c in uw["conditions"]:
|
||||
lines.append(f"- {c}")
|
||||
|
||||
# 保费方案
|
||||
lines.append("\n## 五、保险产品费率方案")
|
||||
if report.get("pricing"):
|
||||
lines.append("| 险种 | 基础保费 | 最终保费 | 保额 | 免赔率 |")
|
||||
lines.append("|------|---------|---------|------|--------|")
|
||||
for p in report["pricing"]:
|
||||
if p.get("is_insurable"):
|
||||
lines.append(
|
||||
f"| {p['product_name']} | ¥{p['base_premium']:,.0f} | "
|
||||
f"¥{p['final_premium']:,.0f} | ¥{p['adjusted_coverage']:,.0f} | "
|
||||
f"{p['deductible_rate']:.0%} |"
|
||||
)
|
||||
else:
|
||||
lines.append(f"| {p['product_name']} | - | **拒保** | - | - |")
|
||||
|
||||
return "\n".join(lines)
|
||||
@@ -0,0 +1,148 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
六维风险评分器
|
||||
整合所有数据源和分析结果,生成结构化六维风险画像
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 风险等级映射
|
||||
RISK_LEVELS = {
|
||||
(0, 30): {"level": "低", "color": "#4CAF50", "emoji": "🟢"},
|
||||
(30, 50): {"level": "中低", "color": "#8BC34A", "emoji": "🟡"},
|
||||
(50, 70): {"level": "中高", "color": "#FF9800", "emoji": "🟠"},
|
||||
(70, 90): {"level": "高", "color": "#F44336", "emoji": "🔴"},
|
||||
(90, 101): {"level": "极高", "color": "#B71C1C", "emoji": "⛔"},
|
||||
}
|
||||
|
||||
|
||||
def get_risk_level(score: int) -> dict:
|
||||
"""根据分数获取风险等级详情"""
|
||||
for (low, high), info in RISK_LEVELS.items():
|
||||
if low <= score < high:
|
||||
return info
|
||||
return {"level": "未知", "color": "#9E9E9E", "emoji": "❓"}
|
||||
|
||||
|
||||
def calculate_six_dimension_scores(company_data: dict) -> dict:
|
||||
"""
|
||||
基于企业数据直接计算六维风险评分(不依赖 LLM)
|
||||
用于快速预览和规则引擎降级场景
|
||||
"""
|
||||
financials = company_data.get("financials", {})
|
||||
compliance = company_data.get("compliance", {})
|
||||
tech_route = company_data.get("tech_route", {})
|
||||
personnel = company_data.get("core_tech_personnel", [])
|
||||
supply_chain = company_data.get("supply_chain", {})
|
||||
|
||||
scores = {}
|
||||
|
||||
# 1. 技术路线颠覆风险
|
||||
competing = len(tech_route.get("competing_techs", []))
|
||||
moat = tech_route.get("tech_moat", "")
|
||||
tech_score = min(25 + competing * 15, 85)
|
||||
if "差距" in moat or "受制" in moat or "威胁" in moat:
|
||||
tech_score = min(tech_score + 15, 95)
|
||||
if "领先" in moat or "第一" in moat:
|
||||
tech_score = max(tech_score - 10, 10)
|
||||
scores["tech_disruption"] = tech_score
|
||||
|
||||
# 2. 核心人员流失风险
|
||||
departed = [p for p in personnel if "离职" in p.get("status", "")]
|
||||
high_imp = [p for p in personnel if p.get("importance") == "极高"]
|
||||
if departed:
|
||||
scores["talent_loss"] = 80
|
||||
elif len(high_imp) <= 1 and len(personnel) <= 2:
|
||||
scores["talent_loss"] = 55
|
||||
else:
|
||||
scores["talent_loss"] = 25
|
||||
|
||||
# 3. 算法/数据合规风险
|
||||
algo_status = compliance.get("algo_filing_status", "")
|
||||
data_risk = compliance.get("data_export_risk", "低")
|
||||
algo_score = 20
|
||||
if "未" in algo_status:
|
||||
algo_score = 75
|
||||
elif data_risk == "高":
|
||||
algo_score = 65
|
||||
elif data_risk == "中":
|
||||
algo_score = 40
|
||||
elif "已备案" in algo_status:
|
||||
algo_score = 15
|
||||
scores["algo_compliance"] = algo_score
|
||||
|
||||
# 4. 地缘政治/出口管制风险
|
||||
entity_status = compliance.get("entity_list_status", "")
|
||||
if "被列入" in entity_status:
|
||||
scores["geopolitical"] = 92
|
||||
elif supply_chain.get("supplier_concentration_risk") == "极高":
|
||||
scores["geopolitical"] = 68
|
||||
elif supply_chain.get("supplier_concentration_risk") == "高":
|
||||
scores["geopolitical"] = 50
|
||||
else:
|
||||
scores["geopolitical"] = 18
|
||||
|
||||
# 5. 研发资本化操纵风险
|
||||
cap_rate = financials.get("rd_capitalization_rate", 0)
|
||||
if cap_rate >= 0.4:
|
||||
scores["rd_capitalization"] = 90
|
||||
elif cap_rate >= 0.3:
|
||||
scores["rd_capitalization"] = 72
|
||||
elif cap_rate >= 0.15:
|
||||
scores["rd_capitalization"] = 48
|
||||
elif cap_rate > 0:
|
||||
scores["rd_capitalization"] = 25
|
||||
else:
|
||||
scores["rd_capitalization"] = 10
|
||||
|
||||
# 6. 客户/供应商集中风险
|
||||
cust_ratio = financials.get("top5_customer_ratio", 0)
|
||||
supp_ratio = financials.get("top5_supplier_ratio", 0)
|
||||
max_conc = max(cust_ratio, supp_ratio)
|
||||
if max_conc >= 0.8:
|
||||
scores["concentration"] = 88
|
||||
elif max_conc >= 0.6:
|
||||
scores["concentration"] = 68
|
||||
elif max_conc >= 0.4:
|
||||
scores["concentration"] = 42
|
||||
else:
|
||||
scores["concentration"] = 18
|
||||
|
||||
# 加权综合
|
||||
weights = {
|
||||
"tech_disruption": 0.20,
|
||||
"talent_loss": 0.15,
|
||||
"algo_compliance": 0.15,
|
||||
"geopolitical": 0.20,
|
||||
"rd_capitalization": 0.15,
|
||||
"concentration": 0.15,
|
||||
}
|
||||
comprehensive = int(sum(scores[k] * weights[k] for k in scores))
|
||||
|
||||
# 维度中文名映射
|
||||
dim_names = {
|
||||
"tech_disruption": "技术路线颠覆",
|
||||
"talent_loss": "核心人员流失",
|
||||
"algo_compliance": "算法/数据合规",
|
||||
"geopolitical": "地缘政治/出口管制",
|
||||
"rd_capitalization": "研发资本化操纵",
|
||||
"concentration": "客户/供应商集中",
|
||||
}
|
||||
|
||||
return {
|
||||
"scores": scores,
|
||||
"comprehensive_score": comprehensive,
|
||||
"risk_level": get_risk_level(comprehensive),
|
||||
"dimension_details": {
|
||||
k: {
|
||||
"name": dim_names[k],
|
||||
"score": v,
|
||||
"level": get_risk_level(v),
|
||||
"weight": weights[k],
|
||||
}
|
||||
for k, v in scores.items()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
全局状态与企业选择同步工具
|
||||
跨页面共享 `st.session_state["global_selected_stock_code"]`
|
||||
确保在一个页面选择企业后,切换到任意页面均自动保持该企业联动
|
||||
包含自动隐藏 Streamlit 右上角 Deploy 按钮与默认 Header 的全局 CSS 样式
|
||||
"""
|
||||
import streamlit as st
|
||||
from collectors.financial_collector import get_all_companies
|
||||
|
||||
GLOBAL_COMPANY_KEY = "global_selected_stock_code"
|
||||
|
||||
|
||||
def hide_streamlit_header_footer():
|
||||
"""彻底隐藏 Streamlit 右上角的 Deploy 按钮、工具栏及页脚"""
|
||||
st.markdown("""
|
||||
<style>
|
||||
/* 隐藏右上角 Deploy 按钮及 Header 菜单 */
|
||||
header[data-testid="stHeader"] {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
height: 0px !important;
|
||||
}
|
||||
[data-testid="stDeployButton"] {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
#MainMenu {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
footer {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
|
||||
def get_global_company_code() -> str:
|
||||
"""获取当前全局选中的企业股票代码"""
|
||||
companies = get_all_companies()
|
||||
if GLOBAL_COMPANY_KEY not in st.session_state or not st.session_state[GLOBAL_COMPANY_KEY]:
|
||||
st.session_state[GLOBAL_COMPANY_KEY] = companies[0]["stock_code"] if companies else "688256"
|
||||
return st.session_state[GLOBAL_COMPANY_KEY]
|
||||
|
||||
|
||||
def set_global_company_code(code: str):
|
||||
"""保存选中的企业股票代码到全局 SessionState"""
|
||||
st.session_state[GLOBAL_COMPANY_KEY] = code
|
||||
|
||||
|
||||
def render_company_selector(label: str = "🏢 选择待评估科创企业", key_suffix: str = "main"):
|
||||
"""
|
||||
渲染与全局 session_state 双向同步的企业选择下拉框
|
||||
返回: selected_company_dict (选中的企业数据字典)
|
||||
"""
|
||||
hide_streamlit_header_footer()
|
||||
|
||||
companies = get_all_companies()
|
||||
if not companies:
|
||||
return None
|
||||
|
||||
# 构建带图标与领域的名称映射
|
||||
company_options = {f"{c['short_name']} ({c['stock_code']}) - {c['sector']}": c["stock_code"] for c in companies}
|
||||
labels_list = list(company_options.keys())
|
||||
codes_list = [c["stock_code"] for c in companies]
|
||||
|
||||
current_code = get_global_company_code()
|
||||
default_index = codes_list.index(current_code) if current_code in codes_list else 0
|
||||
|
||||
selected_label = st.selectbox(
|
||||
label,
|
||||
labels_list,
|
||||
index=default_index,
|
||||
key=f"company_selector_{key_suffix}"
|
||||
)
|
||||
|
||||
new_code = company_options[selected_label]
|
||||
if new_code != st.session_state.get(GLOBAL_COMPANY_KEY):
|
||||
set_global_company_code(new_code)
|
||||
|
||||
# 返回选中的完整企业字典
|
||||
for c in companies:
|
||||
if c["stock_code"] == new_code:
|
||||
return c
|
||||
return companies[0]
|
||||
|
||||
|
||||
def render_sidebar_global_company_selector():
|
||||
"""在侧边栏渲染全局企业选择器与状态指示标签,并自动注入隐藏 Deploy 的 CSS"""
|
||||
hide_streamlit_header_footer()
|
||||
|
||||
companies = get_all_companies()
|
||||
if not companies:
|
||||
return
|
||||
|
||||
company_options = {f"{c['short_name']} ({c['stock_code']})": c["stock_code"] for c in companies}
|
||||
labels_list = list(company_options.keys())
|
||||
codes_list = [c["stock_code"] for c in companies]
|
||||
|
||||
current_code = get_global_company_code()
|
||||
default_index = codes_list.index(current_code) if current_code in codes_list else 0
|
||||
|
||||
st.markdown("### 🏢 全局联动评估目标")
|
||||
selected_label = st.selectbox(
|
||||
"当前联动目标企业:",
|
||||
labels_list,
|
||||
index=default_index,
|
||||
key="global_sidebar_company_selector"
|
||||
)
|
||||
|
||||
new_code = company_options[selected_label]
|
||||
if new_code != st.session_state.get(GLOBAL_COMPANY_KEY):
|
||||
set_global_company_code(new_code)
|
||||
@@ -0,0 +1,304 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
🛡️ 科创企业智能风控与核保系统 - 首页 / 风控大屏入口
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import streamlit as st
|
||||
import pandas as pd
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
|
||||
# 确保项目根目录在 Python 路径中
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from config import PAGE_TITLE, PAGE_ICON, LAYOUT
|
||||
from collectors.financial_collector import get_all_companies
|
||||
from risk_engine.risk_scorer import calculate_six_dimension_scores, get_risk_level
|
||||
|
||||
# ============================================================
|
||||
# 页面配置
|
||||
# ============================================================
|
||||
st.set_page_config(
|
||||
page_title=PAGE_TITLE,
|
||||
page_icon=PAGE_ICON,
|
||||
layout=LAYOUT,
|
||||
initial_sidebar_state="expanded",
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# 自定义样式
|
||||
# ============================================================
|
||||
st.markdown("""
|
||||
<style>
|
||||
/* 主标题渐变 */
|
||||
.main-title {
|
||||
background: linear-gradient(120deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||||
}
|
||||
.main-title h1 {
|
||||
color: #e94560;
|
||||
font-size: 2.2em;
|
||||
margin: 0;
|
||||
}
|
||||
.main-title p {
|
||||
color: #a8a8b3;
|
||||
font-size: 1.1em;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, #1a1a2e, #16213e);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
border: 1px solid #2a2a4a;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.stat-card .number {
|
||||
font-size: 2.5em;
|
||||
font-weight: bold;
|
||||
color: #e94560;
|
||||
}
|
||||
.stat-card .label {
|
||||
color: #a8a8b3;
|
||||
font-size: 0.95em;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* 企业卡片 */
|
||||
.company-card {
|
||||
background: #16213e;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
margin: 8px 0;
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
/* 侧边栏 */
|
||||
[data-testid="stSidebar"] {
|
||||
background: linear-gradient(180deg, #1a1a2e 0%, #0f0f23 100%);
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
from utils.session_helper import render_sidebar_global_company_selector
|
||||
|
||||
# ============================================================
|
||||
# 侧边栏
|
||||
# ============================================================
|
||||
with st.sidebar:
|
||||
render_sidebar_global_company_selector()
|
||||
st.markdown("---")
|
||||
st.markdown("### 🛡️ 系统导航")
|
||||
st.markdown("---")
|
||||
st.markdown("""
|
||||
**功能模块**
|
||||
- 🏠 系统首页
|
||||
- 📊 企业风险概览
|
||||
- 🕸️ 供应链知识图谱
|
||||
- ⚖️ 多智能体辩论诊断
|
||||
- 💰 动态定价与核保
|
||||
""")
|
||||
st.markdown("---")
|
||||
st.markdown("""
|
||||
**技术栈**
|
||||
- 🤖 DeepSeek API (LLM)
|
||||
- 🕸️ NetworkX (知识图谱)
|
||||
- 📊 Plotly (可视化)
|
||||
- 🔧 Streamlit (Web框架)
|
||||
""")
|
||||
st.markdown("---")
|
||||
st.caption("中国平安 × 挑战杯 · 科创风控原型")
|
||||
|
||||
# ============================================================
|
||||
# 主页内容
|
||||
# ============================================================
|
||||
|
||||
# 标题
|
||||
st.markdown("""
|
||||
<div class="main-title">
|
||||
<h1>🛡️ 科创企业智能风控与核保系统</h1>
|
||||
<p>基于多智能体辩论 × 知识图谱 × 动态定价的全链条风控平台</p>
|
||||
<p style="font-size: 0.85em; color: #666;">中国青基会平安励志计划 · XH-202626 · 科创企业特有风险的识别与管理</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# 加载企业数据
|
||||
companies = get_all_companies()
|
||||
|
||||
# ============================================================
|
||||
# 统计概览卡片
|
||||
# ============================================================
|
||||
st.markdown("### 📊 系统概览")
|
||||
|
||||
# 计算所有企业的风险评分
|
||||
risk_data = []
|
||||
for comp in companies:
|
||||
scores = calculate_six_dimension_scores(comp)
|
||||
risk_data.append({
|
||||
"company": comp["short_name"],
|
||||
"stock_code": comp["stock_code"],
|
||||
"industry": comp["industry"],
|
||||
"sector": comp["sector"],
|
||||
"comprehensive_score": scores["comprehensive_score"],
|
||||
"risk_level": scores["risk_level"]["level"],
|
||||
**scores["scores"],
|
||||
})
|
||||
|
||||
df = pd.DataFrame(risk_data)
|
||||
|
||||
# 统计卡片
|
||||
col1, col2, col3, col4, col5 = st.columns(5)
|
||||
|
||||
with col1:
|
||||
st.markdown(f"""
|
||||
<div class="stat-card">
|
||||
<div class="number">{len(companies)}</div>
|
||||
<div class="label">监控企业总数</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
with col2:
|
||||
high_risk = len(df[df["comprehensive_score"] >= 70])
|
||||
st.markdown(f"""
|
||||
<div class="stat-card">
|
||||
<div class="number" style="color: #F44336;">{high_risk}</div>
|
||||
<div class="label">⚠️ 高风险企业</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
with col3:
|
||||
med_risk = len(df[(df["comprehensive_score"] >= 40) & (df["comprehensive_score"] < 70)])
|
||||
st.markdown(f"""
|
||||
<div class="stat-card">
|
||||
<div class="number" style="color: #FF9800;">{med_risk}</div>
|
||||
<div class="label">🟡 中风险企业</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
with col4:
|
||||
low_risk = len(df[df["comprehensive_score"] < 40])
|
||||
st.markdown(f"""
|
||||
<div class="stat-card">
|
||||
<div class="number" style="color: #4CAF50;">{low_risk}</div>
|
||||
<div class="label">🟢 低风险企业</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
with col5:
|
||||
sanctioned = len([c for c in companies if "被列入" in c.get("compliance", {}).get("entity_list_status", "")])
|
||||
st.markdown(f"""
|
||||
<div class="stat-card">
|
||||
<div class="number" style="color: #B71C1C;">{sanctioned}</div>
|
||||
<div class="label">⛔ 受制裁企业</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
st.markdown("<br>", unsafe_allow_html=True)
|
||||
|
||||
# ============================================================
|
||||
# 风险分布图
|
||||
# ============================================================
|
||||
col_left, col_right = st.columns([3, 2])
|
||||
|
||||
with col_left:
|
||||
st.markdown("#### 🎯 企业综合风险评分分布")
|
||||
|
||||
# 水平柱状图,按风险排序
|
||||
df_sorted = df.sort_values("comprehensive_score", ascending=True)
|
||||
|
||||
colors = []
|
||||
for score in df_sorted["comprehensive_score"]:
|
||||
if score >= 70:
|
||||
colors.append("#F44336")
|
||||
elif score >= 50:
|
||||
colors.append("#FF9800")
|
||||
elif score >= 30:
|
||||
colors.append("#FFC107")
|
||||
else:
|
||||
colors.append("#4CAF50")
|
||||
|
||||
fig = go.Figure(go.Bar(
|
||||
x=df_sorted["comprehensive_score"],
|
||||
y=df_sorted["company"],
|
||||
orientation="h",
|
||||
marker_color=colors,
|
||||
text=df_sorted["comprehensive_score"],
|
||||
textposition="outside",
|
||||
))
|
||||
fig.update_layout(
|
||||
height=400,
|
||||
margin=dict(l=0, r=30, t=10, b=10),
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
font=dict(color="white"),
|
||||
xaxis=dict(
|
||||
title="综合风险评分",
|
||||
range=[0, 105],
|
||||
gridcolor="rgba(255,255,255,0.1)",
|
||||
),
|
||||
yaxis=dict(gridcolor="rgba(255,255,255,0.1)"),
|
||||
)
|
||||
# 添加阈值线
|
||||
fig.add_vline(x=70, line_dash="dash", line_color="#F44336",
|
||||
annotation_text="高风险线(70)", annotation_position="top right")
|
||||
fig.add_vline(x=40, line_dash="dash", line_color="#FF9800",
|
||||
annotation_text="中风险线(40)", annotation_position="top right")
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
with col_right:
|
||||
st.markdown("#### 🏷️ 行业风险热力")
|
||||
|
||||
# 按行业汇总
|
||||
sector_risk = df.groupby("sector")["comprehensive_score"].mean().reset_index()
|
||||
sector_risk.columns = ["行业", "平均风险"]
|
||||
sector_risk = sector_risk.sort_values("平均风险", ascending=False)
|
||||
|
||||
fig2 = px.bar(
|
||||
sector_risk, x="行业", y="平均风险",
|
||||
color="平均风险",
|
||||
color_continuous_scale=["#4CAF50", "#FFC107", "#F44336"],
|
||||
range_color=[0, 100],
|
||||
)
|
||||
fig2.update_layout(
|
||||
height=400,
|
||||
margin=dict(l=0, r=0, t=10, b=10),
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
font=dict(color="white"),
|
||||
showlegend=False,
|
||||
coloraxis_showscale=False,
|
||||
)
|
||||
st.plotly_chart(fig2, use_container_width=True)
|
||||
|
||||
# ============================================================
|
||||
# 企业列表
|
||||
# ============================================================
|
||||
st.markdown("### 📋 企业风险速览")
|
||||
|
||||
# 格式化数据表格
|
||||
display_df = df[["company", "stock_code", "industry", "comprehensive_score", "risk_level"]].copy()
|
||||
display_df.columns = ["企业名称", "股票代码", "行业", "综合风险评分", "风险等级"]
|
||||
|
||||
st.dataframe(display_df, use_container_width=True, height=400)
|
||||
|
||||
# ============================================================
|
||||
# 底部信息
|
||||
# ============================================================
|
||||
st.markdown("---")
|
||||
st.markdown("""
|
||||
<div style="text-align: center; color: #666; font-size: 0.85em;">
|
||||
<p>🛡️ 科创企业智能风控与核保系统 v1.0</p>
|
||||
<p>技术架构: Multi-Agent 交叉验证 × 供应链知识图谱 × 动态保险定价</p>
|
||||
<p>数据来源: 科创板公开年报 · BIS 实体清单 · 网信办算法备案公示</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
@@ -0,0 +1,142 @@
|
||||
# 中国青基会平安励志计划 · 2026竞赛作品
|
||||
# 《数智风控:AI 科创企业新型风险前瞻治理方案》
|
||||
## 原型系统部署与运行维护手册 (XH-202626)
|
||||
|
||||
---
|
||||
|
||||
### 一、 系统概述与体验入口
|
||||
|
||||
本系统交付了完备的金融级 Web 风控系统原型,已完成全套生产环境部署与 SSL 证书加密,支持公网在线实时访问与本地一键部署运行。
|
||||
|
||||
- **公网加密访问入口**:`https://risk.aiformat.cn`
|
||||
- **公网服务器 IP**:`159.75.81.121`
|
||||
- **技术栈规范**:
|
||||
- **核心语言**:Python 3.10+
|
||||
- **Web 交互框架**:Streamlit 1.32+
|
||||
- **大模型推理引擎**:DeepSeek API (`deepseek-chat` 深度推理底座)
|
||||
- **图谱计算引擎**:NetworkX 3.0+ (广度优先 BFS 风险传染算法)
|
||||
- **可视化组件**:Plotly 5.18+ / PyVis 0.3+
|
||||
- **反向代理与守护**:Nginx 1.18+ + Systemd 服务守护 + Let's Encrypt SSL
|
||||
|
||||
---
|
||||
|
||||
### 二、 本地快速一键运行指南
|
||||
|
||||
若评审专家或用户需要在本地环境直接运行本源码系统,请按以下步骤操作:
|
||||
|
||||
#### 1. 环境准备
|
||||
确保本地安装了 **Python 3.10** 或更高版本:
|
||||
```bash
|
||||
python --version
|
||||
```
|
||||
|
||||
#### 2. 解压与安装依赖
|
||||
解压源码包后进入项目根目录,运行依赖安装命令:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
#### 3. 配置 DeepSeek 大模型 API Key (可选)
|
||||
系统默认内置了 DeepSeek 演示 API 接口。如需使用自定义密钥,可打开 `config.py` 文件配置:
|
||||
```python
|
||||
DEEPSEEK_API_KEY = "your-deepseek-api-key"
|
||||
```
|
||||
|
||||
#### 4. 一键启动 Web 系统
|
||||
在终端运行以下命令启动本地交互服务:
|
||||
```bash
|
||||
streamlit run 🏠_系统首页.py
|
||||
```
|
||||
终端启动成功后,浏览器将自动弹窗并打开系统界面:
|
||||
`http://localhost:8501`
|
||||
|
||||
---
|
||||
|
||||
### 三、 服务器生产环境部署规范 (CentOS / Ubuntu)
|
||||
|
||||
本系统已在生产服务器 `159.75.81.121` 上完成了生产级部署,配置规范如下:
|
||||
|
||||
#### 1. 代码目录结构
|
||||
```text
|
||||
/opt/risk_system/
|
||||
├── 🏠_系统首页.py # 系统控制台大屏入口
|
||||
├── config.py # 全局参数与风控规则配置
|
||||
├── requirements.txt # Python 依赖清单
|
||||
├── pages/ # 4 大业务功能子模块
|
||||
│ ├── 01_📊_企业风险概览.py
|
||||
│ ├── 02_🕸️_供应链知识图谱.py
|
||||
│ ├── 03_🤖_多智能体辩论诊断.py
|
||||
│ └── 04_💰_动态定价与核保.py
|
||||
├── risk_engine/ # Multi-Agent 专家辩论与冲突平息引擎
|
||||
├── knowledge_graph/ # NetworkX 图谱拓扑与 BIS 穿透算法
|
||||
└── collectors/ # AKShare 财报与网信办/BIS 提取器
|
||||
```
|
||||
|
||||
#### 2. Systemd 服务守护配置 (`/etc/systemd/system/risk_app.service`)
|
||||
使用 Systemd 实现无人值守自动拉起与宕机自愈:
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Streamlit Risk App Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
WorkingDirectory=/opt/risk_system
|
||||
ExecStart=/usr/local/bin/streamlit run 🏠_系统首页.py --server.port 8501 --server.headless true
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
启动与状态检查命令:
|
||||
```bash
|
||||
systemctl daemon-reload
|
||||
systemctl enable risk_app
|
||||
systemctl restart risk_app
|
||||
systemctl status risk_app
|
||||
```
|
||||
|
||||
#### 3. Nginx 反向代理与 WebSocket 连通配置 (`/etc/nginx/conf.d/risk.conf`)
|
||||
配置文件支持 HTTP(80) 自动强制重定向至 HTTPS(443),并开启 WebSocket 实时推流支持:
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name risk.aiformat.cn;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name risk.aiformat.cn;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/risk.aiformat.cn/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/risk.aiformat.cn/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8501;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 四、 提交材料清单 (Checklist)
|
||||
|
||||
| 序号 | 材料名称 | 文件类型 | 描述 |
|
||||
|:---|:---|:---|:---|
|
||||
| 01 | **研究报告** | `.md` / `.pdf` | 完整的学术研究报告与六维风控理论推导 |
|
||||
| 02 | **竞赛汇报 PPT** | `.pptx` | 包含 22 页大字架构与 4 大模块实操截图的演示文稿 |
|
||||
| 03 | **原型系统完整源码** | Python 代码包 | 包含 Agent 辩论、知识图谱、动态精算与 Web 源码 |
|
||||
| 04 | **部署与运行手册** | `.md` | 本文档,提供本地运行与云服务器守护指引 |
|
||||
|
||||
---
|
||||
*中国青基会平安励志计划 · 2026 挑战杯“揭榜挂帅”擂台赛 XH-202626 课题研发团队*
|
||||
Binary file not shown.
@@ -0,0 +1,541 @@
|
||||
# 中国青基会平安励志计划 · 2026竞赛作品
|
||||
|
||||
# 数智风控:AI 科创企业新型风险前瞻治理方案
|
||||
|
||||
**题目编号**:XH-202626
|
||||
**选题名称**:数智风控:AI 科创企业新型风险前瞻治理方案
|
||||
**发榜单位**:中国青少年发展基金会、中国平安保险(联合发榜)
|
||||
**提报单位**:厦门城市职业学院
|
||||
**作品类型**:研究报告 × 风控原型系统(AI Multi-Agent + 知识图谱 + 动态定价引擎)
|
||||
**提交日期**:2026年9月
|
||||
|
||||
---
|
||||
|
||||
## 执行摘要
|
||||
|
||||
科技金融是中央金融工作会议提出的“五篇大文章”之首。以人工智能、半导体、前沿硬科技为代表的科创企业,是实现高水平科技自立自强的核心力量。然而,科创企业兼具技术密集、高不确定性、轻资产、长周期等特征,其面临的风险深藏于非结构化文本、算法备案公示、国际制裁清单及供应链拓扑关系中。传统依赖静态财务指标的评价体系对此类非标准化特有风险严重失效。
|
||||
|
||||
本研究报告紧扣“看得见风险、管得住过程、降得下损失”的全链条风控目标,创新性地构建了基于 Multi-Agent 专家辩论、供应链知识图谱与动态保险定价的科创企业特有风控闭环:
|
||||
1. **特有风险场景识别**:精准定义并构建了包括技术路线颠覆、核心人员流失、算法数据合规、地缘政治制裁、研发资本化操纵、客户/供应商集中等六维特有风险指标体系。
|
||||
2. **AI 多智能体辩论机制 (Multi-Agent Debate)**:引入法务风控节点 (Law Agent)、技术风控节点 (Tech Agent)、财务风控节点 (Finance Agent) 与法官裁决节点 (Judge Agent),基于 DeepSeek 在线推理引擎,实现多视角交叉质证与自动冲突消除。
|
||||
3. **供应链风险穿透图谱 (Knowledge Graph)**:利用 NetworkX 搭建包含实体清单与上下游依赖的拓扑网络,实现地缘政治与断供风险的毫秒级穿透预警。
|
||||
4. **中国平安保险主业闭环**:设计“科创特有风险综合险”,开发动态费率定价引擎 (Dynamic Pricing Engine) 与自动化核保决策报告生成器,将风控研究转化为具有强商业可行性的金融赋能方案。
|
||||
|
||||
本团队交付了完整落地的交互式金融级原型系统,实现了数据采集、多智能体辩论、图谱可视化与定价核保的全流程交互。
|
||||
|
||||
---
|
||||
|
||||
## 第一章 绪论与科创企业特有风险场景构建
|
||||
|
||||
### 1.1 科技金融战略与传统风控范式困局
|
||||
|
||||
中央金融工作会议强调将“科技金融”列为金融五篇大文章之首,明确要求金融机构加大对高科技企业、新质生产力的支持力度。然而,商业银行、保险公司以及股权投资机构在服务科创企业时,普遍面临“不敢投、不敢贷、不敢保”的结构性矛盾。
|
||||
|
||||
问题的本质在于传统企业风控逻辑与科创企业风险特征的根本性错位:
|
||||
1. **指标体系静态化**:传统企业风控依赖资产负债表、损益表及现金流量表,关注 EBIT、资产负债率、速动比率等财务数据。但科创企业前期研发投入巨大、无盈利或利润微薄、固定资产较少,静态财务指标无法真实反映企业的真实生存状态。
|
||||
2. **风险形态非标准化与隐蔽化**:科创企业的核心价值在于未来的技术壁垒与合规边界,其关键风险隐藏于年报长文本、网信办算法备案库、美国 BIS 实体清单、高管及核心研发人员异动公告中。这些信息具有高维、异构、非结构化的特征,传统规则引擎与量化模型难以有效捕捉。
|
||||
|
||||
### 1.2 科创企业六维特有风险机理模型
|
||||
|
||||
结合对科创板上市公司公告、司法诉讼记录、网信办备案数据及国际出口管制案列的深入研究,本研究定义并量化了科创企业的六大特有风险维度:
|
||||
|
||||
| 风险维度 | 风险名称 | 风险成因与演化机理 | 传统模型盲区 | 识别与量化路径 |
|
||||
|:---|:---|:---|:---|:---|
|
||||
| 维度一 | 技术路线颠覆风险 | 行业出现跨代突破性技术,原有技术研发路线被瞬间边缘化 | 传统模型视研发投入为无形资产,无法预判技术路线失效 | 抽取竞品技术关键词重合度,对比专利申请增速与替代技术文献分布 |
|
||||
| 维度二 | 核心人员流失风险 | 科创企业高度依赖少数领军科学家或 CTO,核心人员离职导致研发中断与技术泄露 | 资产负债表无法衡量人力资本价值与研发延续性 | 解析年报核心技术人员变动公示、高管股权激励解锁情况及劳动仲裁 |
|
||||
| 维度三 | 算法备案与数据合规 | 大模型及 AI 应用未通过网信办算法备案或违反数据出境安全评估,遭遇强制下架 | 传统合规审查仅关注营业执照与税务合规,不具备算法治理能力 | 检索网信办深度合成/算法备案库、对齐数据安全评估规范 |
|
||||
| 维度四 | 地缘政治与出口管制 | 上游核心元器件或 EDA 工具提供商被列入实体清单,导致供应链断裂 | 仅评估直接供应商信用,无法感知多级供应链依赖的隐性地缘风险 | 构建拓扑图谱,穿透上游二、三级供应商制裁状态与替代率 |
|
||||
| 维度五 | 研发资本化操纵风险 | 企业通过将本应费用化的研发支出异常资本化,人工虚增当期利润与资产 | 传统财务比率看重净利润,忽视资本化会计政策操纵 | 监控研发资本化率异动、比较同行均值偏差及资本化无形资产减值准备 |
|
||||
| 维度六 | 客户/供应商集中风险 | 营收过度依赖单一大客户或供应链依赖单一供应商,发生单点故障 (SPOF) | 传统信用评估孤立看单个主体,不评估产业链单点风险 | 计算 HHI 集中度指数,结合图谱出入度节点权重进行综合穿透 |
|
||||
|
||||
### 1.3 核心研究目标与闭环路径
|
||||
|
||||
本方案旨在打造“看得见风险、管得住过程、降得下损失”的全链条风控系统。整体路径划分为四个阶段:
|
||||
1. **数据感知**:全网抓取并解析财务量化指标与非结构化文本;
|
||||
2. **智能研判**:利用 Multi-Agent 辩论机制消除逻辑分歧,确定综合风险评级;
|
||||
3. **图谱穿透**:通过供应链知识图谱,实现地缘与断供风险的毫秒级预警;
|
||||
4. **金融赋能**:切入中国平安保险主业,完成“科创特有风险综合险”的动态定价与智能核保。
|
||||
|
||||
---
|
||||
|
||||
## 第二章 基于 AI 多智能体交叉辩论的技术架构
|
||||
|
||||
### 2.1 整体系统架构设计
|
||||
|
||||
系统包含数据采集与抽取层、供应链知识图谱层、AI 多智能体辩论研判层以及中国平安业务闭环层:
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
subgraph Layer1["1. 数据采集与抽取层"]
|
||||
A1["AKShare 量化数据接口<br/>(财务指标 / 集中度 / 资本化率)"]
|
||||
A2["巨潮资讯公告解析器<br/>(核心人员 / 诉讼 / 技术路线)"]
|
||||
A3["外部情报数据库<br/>(美国 BIS 实体清单 / 网信办算法备案)"]
|
||||
end
|
||||
|
||||
subgraph Layer2["2. 供应链知识图谱层 (NetworkX)"]
|
||||
B1["异构拓扑节点构建<br/>(科创企业 / 供应商 / 客户 / 专家)"]
|
||||
B2["广度优先风险传染算法<br/>(Breadth-First Risk Contagion)"]
|
||||
end
|
||||
|
||||
subgraph Layer3["3. AI 多智能体辩论研判层 (DeepSeek Engine)"]
|
||||
direction LR
|
||||
C1["法务风控节点<br/>(Law Agent)"] <--> C2["技术风控节点<br/>(Tech Agent)"]
|
||||
C2 <--> C3["财务风控节点<br/>(Finance Agent)"]
|
||||
C1 <--> C3
|
||||
C1 --> C4["委员会法官裁决节点<br/>(Judge Agent)"]
|
||||
C2 --> C4
|
||||
C3 --> C4
|
||||
end
|
||||
|
||||
subgraph Layer4["4. 中国平安主业闭环应用层"]
|
||||
D1["实时风控仪表盘<br/>(Web Dashboard)"]
|
||||
D2["动态费率定价引擎<br/>(Dynamic Pricing Engine)"]
|
||||
D3["自动化核保决策报告生成器<br/>(Underwriting Report Generator)"]
|
||||
end
|
||||
|
||||
Layer1 --> Layer2
|
||||
Layer2 --> Layer3
|
||||
Layer3 --> Layer4
|
||||
|
||||
style Layer1 fill:#0284c7,stroke:#38bdf8,color:#ffffff,stroke-width:2px
|
||||
style Layer2 fill:#7c3aed,stroke:#c084fc,color:#ffffff,stroke-width:2px
|
||||
style Layer3 fill:#ea580c,stroke:#fb923c,color:#ffffff,stroke-width:2px
|
||||
style Layer4 fill:#059669,stroke:#34d399,color:#ffffff,stroke-width:2px
|
||||
|
||||
style A1 fill:#e0f2fe,stroke:#0284c7,color:#0369a1
|
||||
style A2 fill:#e0f2fe,stroke:#0284c7,color:#0369a1
|
||||
style A3 fill:#e0f2fe,stroke:#0284c7,color:#0369a1
|
||||
|
||||
style B1 fill:#f3e8ff,stroke:#7c3aed,color:#6b21a8
|
||||
style B2 fill:#f3e8ff,stroke:#7c3aed,color:#6b21a8
|
||||
|
||||
style C1 fill:#ffedd5,stroke:#ea580c,color:#c2410c
|
||||
style C2 fill:#ffedd5,stroke:#ea580c,color:#c2410c
|
||||
style C3 fill:#ffedd5,stroke:#ea580c,color:#c2410c
|
||||
style C4 fill:#fef3c7,stroke:#d97706,color:#b45309
|
||||
|
||||
style D1 fill:#d1fae5,stroke:#059669,color:#047857
|
||||
style D2 fill:#d1fae5,stroke:#059669,color:#047857
|
||||
style D3 fill:#d1fae5,stroke:#059669,color:#047857
|
||||
```
|
||||
|
||||
### 2.2 智能体交互协议与集中控制模式
|
||||
|
||||
```python
|
||||
class MultiAgentDebateEngine:
|
||||
"""
|
||||
Multi-Agent 多智能体交叉辩论与诊断控制引擎
|
||||
"""
|
||||
def __init__(self, api_key: str):
|
||||
self.law_agent = LawAgent(api_key)
|
||||
self.tech_agent = TechAgent(api_key)
|
||||
self.finance_agent = FinanceAgent(api_key)
|
||||
self.judge_agent = JudgeAgent(api_key)
|
||||
|
||||
def run_debate(self, company_data: dict) -> dict:
|
||||
# Phase 1: 专家节点独立研判
|
||||
law_result = self.law_agent.evaluate(company_data)
|
||||
tech_result = self.tech_agent.evaluate(company_data)
|
||||
finance_result = self.finance_agent.evaluate(company_data)
|
||||
|
||||
# Phase 2: 提取三方潜在冲突点
|
||||
conflicts = self._detect_conflicts(law_result, tech_result, finance_result)
|
||||
|
||||
# Phase 3: 法官节点综合裁决与冲突消除
|
||||
judge_result = self.judge_agent.adjudicate(
|
||||
company_data, law_result, tech_result, finance_result, conflicts
|
||||
)
|
||||
|
||||
return {
|
||||
"law_result": law_result,
|
||||
"tech_result": tech_result,
|
||||
"finance_result": finance_result,
|
||||
"conflicts": conflicts,
|
||||
"judge_result": judge_result,
|
||||
}
|
||||
|
||||
def _detect_conflicts(self, law_res, tech_res, fin_res):
|
||||
conflicts = []
|
||||
# 识别技术与财务之间的评价背离 (如技术专家评高分但财务专家识别资本化操纵)
|
||||
if tech_res["score"] < 40 and fin_res["scores"].get("rd_capitalization", 0) > 60:
|
||||
conflicts.append("技术专家认可研发强度,但财务专家指出研发支出资本化率异常高,存在粉饰嫌疑。")
|
||||
# 识别法务与技术之间的评价背离 (如技术领先但存在合规红线)
|
||||
if tech_res["score"] < 40 and law_res["scores"].get("algo_compliance", 0) > 60:
|
||||
conflicts.append("技术专家评定算法优势明显,但法务专家警告该算法未完成网信办合规备案。")
|
||||
return conflicts
|
||||
```
|
||||
|
||||
### 2.3 专家节点 Prompt 链工程规范
|
||||
|
||||
为了保证输出结果的严谨性与结构化,每个智能体节点均配置了明确的 Persona 提示词规范:
|
||||
|
||||
#### 法务风控节点 Prompt 示例:
|
||||
```text
|
||||
系统角色:你是一名精通《生成式人工智能服务管理暂行办法》、《出口管制法》及美国 BIS 实体清单规则的资深法务合规专家。
|
||||
任务目标:分析目标科创企业在地缘政治、出口管制及数据算法合规方面的特有风险。
|
||||
输入数据:企业合规记录、算法备案公示状态、供应链敏感节点清单。
|
||||
输出规范:请以 JSON 格式输出评估结果,包含:
|
||||
1. algo_compliance (算法/数据合规评分,0-100)
|
||||
2. geopolitical (地缘政治/实体清单风险评分,0-100)
|
||||
3. key_findings (关键违规要点列表)
|
||||
```
|
||||
|
||||
#### 财务风控节点 Prompt 示例:
|
||||
```text
|
||||
系统角色:你是一名熟悉科创板上市审计规则的资深注册会计师 (CPA)。
|
||||
任务目标:穿透目标企业的财务报告,重点识别研发资本化操纵及客户/供应商高度集中风险。
|
||||
输入数据:研发投入总额、研发资本化费用、前五大客户及供应商占比、应收账款周转率。
|
||||
输出规范:请以 JSON 格式输出评估结果,包含:
|
||||
1. rd_capitalization (研发资本化风险评分,0-100)
|
||||
2. concentration (客户/供应商集中风险评分,0-100)
|
||||
3. key_findings (财务异常特征分析)
|
||||
```
|
||||
|
||||
### 2.4 委员会法官节点冲突消除算法
|
||||
|
||||
法官节点 (Judge Agent) 接收三大专家的评分及冲突列表,采用动态加权评分矩阵计算综合风险评分 $S_{\text{comp}}$:
|
||||
|
||||
$$S_{\text{comp}} = \sum_{i \in \{\text{Law}, \text{Tech}, \text{Fin}\}} w_i \cdot S_i + \delta_{\text{conflict}}$$
|
||||
|
||||
其中,$w_{\text{Law}} = 0.35$,$w_{\text{Tech}} = 0.35$,$w_{\text{Fin}} = 0.30$;当检测到致命红线冲突(如直接被列入实体清单或重大算法违规)时,惩罚项 $\delta_{\text{conflict}} \in [15, 30]$,确保系统具备风险一票否决能力。
|
||||
|
||||
### 2.5 交叉质证冲突判定规则的研究设计
|
||||
|
||||
为了让委员会法官节点具备人类专家的敏感度,本研究在代码实现中硬编码了多种典型的科创企业风险冲突模式。这并非单纯依赖大模型的自由发挥,而是将金融审计经验转化为具体的规则前置约束。例如:
|
||||
|
||||
1. **研发投入真实性冲突 (Tech vs Finance)**:
|
||||
* 触发条件:技术节点评分 `< 40` (技术领先),但财务节点中“研发资本化风险”子项评分 `> 60`。
|
||||
* 研判逻辑:技术专家认可其高强度的研发投入,但财务审计视角发现该投入大量采用资本化手段而非费用化处理,存在为了满足上市利润要求而虚增资产的盈余管理嫌疑。
|
||||
2. **合规红线一票否决冲突 (Tech vs Law)**:
|
||||
* 触发条件:技术节点高度认可其算法性能,但法务节点检测到该模型未通过国家网信办《深度合成服务算法备案》。
|
||||
* 研判逻辑:在监管趋严的环境下,未合规的技术资产随时面临下架甚至行政处罚,此时系统将强制拉高整体风险评分,触发“附条件承保”或“拒保”决议。
|
||||
|
||||
通过上述专家规则 (Expert Rules) 与大模型推理能力的结合,系统显著提升了多智能体辩论的业务可解释性。
|
||||
|
||||
---
|
||||
|
||||
## 第三章 供应链知识图谱与地缘风险穿透模型
|
||||
|
||||
### 3.1 异构知识图谱本体设计 (Ontology Design)
|
||||
|
||||
在科创企业风险评估中,单一主体的风险往往沿着供应链、投资链及高管任职链条进行传染。本研究设计了包含以下实体与关系的异构图谱:
|
||||
|
||||
1. **实体类型 (Nodes)**:
|
||||
* `Enterprise` (科创企业节点):属性包括股票代码、行业领域、风险评级。
|
||||
* `Supplier` (供应商节点):属性包括供应元器件、制裁状态 (Is_Sanctioned)。
|
||||
* `Customer` (客户节点):属性包括营收贡献比例。
|
||||
* `Person` (核心技术人员/高管节点):属性包括姓名、职位、重要性。
|
||||
2. **关系类型 (Edges)**:
|
||||
* `SUPPLIES` (供应关系):属性包含供货份额 ($W_{\text{sup}}$)、可替代性得分 ($S_{\text{sub}}$)。
|
||||
* `RELIES_ON` (客户依赖关系):属性包含收入占比 ($W_{\text{rev}}$)。
|
||||
* `EMPLOYED_AT` (任职关系):属性包含入职时间、离职状态。
|
||||
|
||||
### 3.2 供应链风险传染算法实现
|
||||
|
||||
采用广度优先拓扑遍历算法 (Breadth-First Risk Contagion Algorithm),计算目标企业受上游制裁波及的波及指数 $I_{\text{contagion}}$:
|
||||
|
||||
```python
|
||||
import networkx as nx
|
||||
|
||||
def calculate_contagion_index(G: nx.DiGraph, target_company: str) -> dict:
|
||||
"""
|
||||
计算目标科创企业的供应链风险传染指数
|
||||
"""
|
||||
if target_company not in G:
|
||||
return {"risk_score": 0, "sanctioned_paths": []}
|
||||
|
||||
sanctioned_paths = []
|
||||
total_impact = 0.0
|
||||
|
||||
# 深度优先搜索 2 步内的上游供应商
|
||||
for predecessor in G.predecessors(target_company):
|
||||
edge_data = G.get_edge_data(predecessor, target_company)
|
||||
weight = edge_data.get("weight", 0.5)
|
||||
|
||||
# 检查供应商自身状态
|
||||
if G.nodes[predecessor].get("is_sanctioned", False):
|
||||
impact = weight * 100
|
||||
total_impact += impact
|
||||
sanctioned_paths.append({
|
||||
"supplier": predecessor,
|
||||
"impact": impact,
|
||||
"reason": "直接一级供应商被列入实体清单"
|
||||
})
|
||||
|
||||
# 检查二级供应商制裁情况
|
||||
for sub_pred in G.predecessors(predecessor):
|
||||
if G.nodes[sub_pred].get("is_sanctioned", False):
|
||||
sub_edge = G.get_edge_data(sub_pred, predecessor)
|
||||
impact = weight * sub_edge.get("weight", 0.5) * 60
|
||||
total_impact += impact
|
||||
sanctioned_paths.append({
|
||||
"supplier": f"{sub_pred} -> {predecessor}",
|
||||
"impact": impact,
|
||||
"reason": "二级上游元器件供应商被列入实体清单"
|
||||
})
|
||||
|
||||
final_score = min(100, total_impact)
|
||||
return {
|
||||
"risk_score": final_score,
|
||||
"sanctioned_paths": sanctioned_paths
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 实证数据源接入与预处理
|
||||
|
||||
系统构建了多源异构数据采集流水线,确保数据输入的真实性与可校验性:
|
||||
1. **财务与集中度数据**:调用 AKShare API 采集科创板企业的营收、研发费用、前五大客户及前五大供应商采购比例。
|
||||
2. **非结构化文本解析**:使用 Python PyPDF2 与正则表达式,自动化提取巨潮资讯网年报公告中的“核心技术人员变动”、“重大诉讼事项”及“技术路线描述”段落。
|
||||
3. **外部清单对接**:建立自动更新脚本,定期比对美国商务部工业和安全局 (BIS) 实体清单 (Entity List) 与网信办“互联网信息服务算法备案”公示名单。
|
||||
|
||||
---
|
||||
|
||||
## 第四章 中国平安业务闭环——“科创特有风险综合险”动态定价与智能核保
|
||||
|
||||
### 4.1 科创专属保险产品体系设计
|
||||
|
||||
针对传统财财险、责任险无法覆盖科创企业特有风险的空白,本方案依托中国平安保险主业资源,设计了“科创特有风险综合保险”产品组合:
|
||||
|
||||
1. **知识产权被诉与侵权损失保险**:赔偿因遭遇国外专利流氓或竞品公司诉讼产生的应诉抗辩费用及经济赔偿。
|
||||
2. **核心技术人员流失业务中断保险**:当核心研发人员离职触发业务停滞条件时,按约定标准向企业支付业务中断补偿金与人才再招聘津贴。
|
||||
3. **数据合规与算法监管整改损失保险**:保障企业因非主观恶意的数据合规整改导致的系统暂停营业损失与整改咨询费用。
|
||||
|
||||
### 4.2 动态费率精算模型
|
||||
|
||||
系统打破传统固定费率核保模式,建立基于实时综合风险评分的动态精算模型:
|
||||
|
||||
$$P_{\text{final}} = P_{\text{base}} \cdot M_{\text{risk}} \cdot F_{\text{industry}} \cdot F_{\text{scale}} \cdot \left(1 + \sum_{k=1}^{6} \Delta_k\right)$$
|
||||
|
||||
#### 1. 参数定义与取值规则:
|
||||
* $P_{\text{base}}$:基础保费(按投保保额 $\text{Coverage} \times \text{BaseRate}$ 计算)。
|
||||
* $M_{\text{risk}}$:基于六维综合风险评分 $S_{\text{comp}}$ 的风险调整乘数:
|
||||
|
||||
$$M_{\text{risk}} = \begin{cases}
|
||||
2.50 & (S_{\text{comp}} \ge 70, \text{高风险/惩罚性费率}) \\
|
||||
1.0 + \frac{S_{\text{comp}} - 40}{30} \cdot 0.80 & (40 \le S_{\text{comp}} < 70, \text{中风险线性浮动}) \\
|
||||
0.85 & (S_{\text{comp}} < 40, \text{优质企业优惠费率})
|
||||
\end{cases}$$
|
||||
|
||||
* $F_{\text{industry}}$:行业风险调节系数(如 AI 大模型为 1.35,半导体设计为 1.25,生物医药为 1.15)。
|
||||
* $F_{\text{scale}}$:企业规模折扣系数(营收大于 10 亿元为 0.90,小于 1 亿元为 1.10)。
|
||||
* $\Delta_k$:特定维度超标时的专项附加惩罚费率(例如当发现研发资本化率超过 40% 时,附加 $\Delta_{\text{cap}} = 0.20$)。
|
||||
|
||||
#### 2. 动态定价计算逻辑实现代码:
|
||||
|
||||
```python
|
||||
def calculate_dynamic_pricing(company_data: dict, risk_result: dict, product_config: dict) -> dict:
|
||||
comprehensive_score = risk_result["comprehensive_score"]
|
||||
base_premium = product_config["base_premium"]
|
||||
|
||||
# 1. 风险乘数计算
|
||||
if comprehensive_score >= 70:
|
||||
risk_multiplier = 2.50
|
||||
is_insurable = False # 建议拒保
|
||||
elif comprehensive_score >= 40:
|
||||
risk_multiplier = 1.0 + ((comprehensive_score - 40) / 30.0) * 0.80
|
||||
is_insurable = True
|
||||
else:
|
||||
risk_multiplier = 0.85
|
||||
is_insurable = True
|
||||
|
||||
# 2. 行业系数
|
||||
sector = company_data.get("sector", "")
|
||||
industry_factors = {"人工智能": 1.35, "半导体": 1.25, "生物医药": 1.15, "高端制造": 1.05}
|
||||
industry_factor = industry_factors.get(sector, 1.0)
|
||||
|
||||
# 3. 最终保费与保额调整
|
||||
final_premium = base_premium * risk_multiplier * industry_factor
|
||||
deductible_rate = 0.05 if comprehensive_score < 40 else (0.10 if comprehensive_score < 70 else 0.20)
|
||||
|
||||
return {
|
||||
"product_name": product_config["name"],
|
||||
"is_insurable": is_insurable,
|
||||
"base_premium": base_premium,
|
||||
"final_premium": round(final_premium, 2),
|
||||
"risk_multiplier": round(risk_multiplier, 3),
|
||||
"industry_factor": industry_factor,
|
||||
"deductible_rate": deductible_rate,
|
||||
"underwriting_decision": "同意承保" if is_insurable else "拒绝承保"
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 自动化核保决策报告生成引擎
|
||||
|
||||
系统集成了 Markdown 核保决策报告生成器,自动整合公司基本面、六维风险评分、多智能体辩论研判过程及动态定价结论:
|
||||
|
||||
```text
|
||||
===================================================================================
|
||||
中国平安保险 · 科创企业核保决策报告
|
||||
===================================================================================
|
||||
报告编号: RPT-20260913-688256 生成时间: 2026-09-13 10:30:00
|
||||
被保险企业: 寒武纪科技股份有限公司 股票代码: 688256
|
||||
所属行业: 半导体及芯片设计 评估结论: 【附条件承保】
|
||||
|
||||
一、六维风险综合评估
|
||||
-----------------------------------------------------------------------------------
|
||||
综合风险评分: 68分 (中高风险)
|
||||
- 技术路线颠覆风险: 45分 (中) - 核心人员流失风险: 35分 (低)
|
||||
- 算法与数据合规风险: 75分 (高) - 地缘政治与出口管制: 82分 (极高)
|
||||
- 研发资本化操纵风险: 40分 (中) - 客户/供应商集中度: 65分 (中高)
|
||||
|
||||
二、多智能体交叉验证审计意见
|
||||
-----------------------------------------------------------------------------------
|
||||
[法务专家 (Law Agent)]:
|
||||
上游 EDA 软件供应商 Synopsys 存在潜在出口管制风险;算法备案公示信息符合规范。
|
||||
[财务专家 (Finance Agent)]:
|
||||
前两大客户占总营收 82%,存在严重单点依赖风险;研发费用资本化率 15%,处于合理区间。
|
||||
[委员会裁决 (Judge Agent)]:
|
||||
企业总体具备较高技术壁垒,但上游供应链受制裁波及风险显著,且客户集中度偏高。
|
||||
|
||||
三、核保决策与特约条件
|
||||
-----------------------------------------------------------------------------------
|
||||
1. 承保方案: 同意承保“科创特有风险综合险”
|
||||
2. 基础保费: ¥100,000 --> 动态调整后保费: ¥168,750 (风险乘数: 1.35)
|
||||
3. 免赔率: 10%
|
||||
4. 特约特别约定:
|
||||
- 投保企业须在 90 天内提供上游 EDA 工具的备用国产化替代预案;
|
||||
- 排他条款:因主动违反出口管制法规导致的设备断供不在理赔范围内。
|
||||
===================================================================================
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 第五章 原型系统实现与应用展示
|
||||
|
||||
### 5.1 系统开发环境与技术栈配置
|
||||
|
||||
本作品交付了可直接运行的完整金融级 Web 系统原型。开发环境与核心组件如下:
|
||||
* **前端与交互框架**:Streamlit 1.32+ 金融级 Web 交互框架 (支持多页面路由与全局 State 同步)
|
||||
* **矢量与图谱可视化**:Plotly 5.18+ / PyVis 0.3+ (网络拓扑渲染)
|
||||
* **大模型推理引擎**:DeepSeek API (`deepseek-chat` 引擎)
|
||||
* **知识图谱与计算库**:NetworkX 3.0+、Pandas 2.0+、NumPy
|
||||
* **数据可视化库**:Plotly Express & Graph Objects (暗黑金融科技主题)
|
||||
|
||||
### 5.2 核心页面结构与功能列表
|
||||
|
||||
原型系统包含五个规范化的业务板块:
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
Root["Streamlit 多页面导航结构"]
|
||||
|
||||
Sub1["系统首页"] --> F1["全景监控大屏"] & F2["高/中/低风险企业热力图"] & F3["统计卡片"]
|
||||
Sub2["企业风险概览"] --> F4["六维风险雷达图"] & F5["核心技术人员变动"] & F6["财务明细透视"]
|
||||
Sub3["供应链知识图谱"] --> F7["交互式 NetworkX 图谱"] & F8["实体清单受制裁高亮"] & F9["波及传染分析"]
|
||||
Sub4["多智能体辩论诊断"] --> F10["实时打字机流式终端"] & F11["三方交叉质证"] & F12["法官裁决卡片"]
|
||||
Sub5["动态定价与核保"] --> F13["费率对比图表"] & F14["定价因子瀑布图"] & F15["Markdown 核保报告"]
|
||||
|
||||
Root --> Sub1
|
||||
Root --> Sub2
|
||||
Root --> Sub3
|
||||
Root --> Sub4
|
||||
Root --> Sub5
|
||||
|
||||
style Root fill:#4f46e5,stroke:#818cf8,color:#ffffff,stroke-width:2px
|
||||
style Sub1 fill:#0284c7,stroke:#38bdf8,color:#ffffff
|
||||
style Sub2 fill:#7c3aed,stroke:#c084fc,color:#ffffff
|
||||
style Sub3 fill:#db2777,stroke:#f472b6,color:#ffffff
|
||||
style Sub4 fill:#ea580c,stroke:#fb923c,color:#ffffff
|
||||
style Sub5 fill:#059669,stroke:#34d399,color:#ffffff
|
||||
|
||||
style F1 fill:#e0f2fe,color:#0369a1
|
||||
style F2 fill:#e0f2fe,color:#0369a1
|
||||
style F3 fill:#e0f2fe,color:#0369a1
|
||||
style F4 fill:#f3e8ff,color:#6b21a8
|
||||
style F5 fill:#f3e8ff,color:#6b21a8
|
||||
style F6 fill:#f3e8ff,color:#6b21a8
|
||||
style F7 fill:#fce7f3,color:#be185d
|
||||
style F8 fill:#fce7f3,color:#be185d
|
||||
style F9 fill:#fce7f3,color:#be185d
|
||||
style F10 fill:#ffedd5,color:#c2410c
|
||||
style F11 fill:#ffedd5,color:#c2410c
|
||||
style F12 fill:#ffedd5,color:#c2410c
|
||||
style F13 fill:#d1fae5,color:#047857
|
||||
style F14 fill:#d1fae5,color:#047857
|
||||
style F15 fill:#d1fae5,color:#047857
|
||||
```
|
||||
|
||||
### 5.3 核心功能创新与金融可解释性设计
|
||||
|
||||
1. **沉浸式多智能体推演可视化 (Explainable AI 展现)**:
|
||||
在金融风控领域,黑盒模型往往无法被合规部门接受。本系统在辩论诊断页面,实时拉取并展示三大风控专家与委员会法官的推理思维链 (Thinking Stream)。这种全过程透明可视化的设计,让核保人员不仅知其然,更能“知其所以然”,大幅提升了 AI 介入强监管金融业务的可解释性。
|
||||
2. **图谱视角的风险穿透与高亮预警**:
|
||||
有别于传统风控系统孤立展示企业数据的做法,本原型在知识图谱模块实现了“风险沿供应链传染”的动态高亮。当系统检索到某二级供应商位列美国 BIS 实体清单时,会自动沿拓扑网络将风险路径标红,直观呈现地缘政治带来的断供波及效应,为保险精算提供直观的证据链。
|
||||
3. **规则引擎与大模型混合驱动的智能核保**:
|
||||
在最终定价与报告生成环节,系统摒弃了完全由 LLM 自由发挥的不可控模式,采用了“硬规则 + 柔性文本”的混合架构。综合评分硬性决定基础保费的上浮系数与免赔额(如 `score >= 70` 直接拒保或实施惩罚性费率),而大模型则负责基于这些硬结论,润色并生成带有“特约条款(如限期寻找国产化替代方案)”的标准 Markdown 核保报告。
|
||||
|
||||
---
|
||||
|
||||
## 第六章 课题团队结构与跨学科技术路线说明
|
||||
|
||||
### 6.1 团队成员与跨学科分工架构
|
||||
|
||||
本项目由具备计算机数据科学、金融工程与经济法学背景的跨学科团队协作攻关完成。团队三位成员具体分工如下:
|
||||
|
||||
1. **周丽梅 (课题负责人 / AI 与软件工程实现)**:
|
||||
* **专业领域**:计算机科学 / 数据科学与软件工程
|
||||
* **核心职责**:负责系统整体技术架构设计、DeepSeek 大模型 API 接口封装、Multi-Agent 多智能体交叉辩论与冲突消除算法开发、NetworkX 异构知识图谱拓扑构建,以及金融级 Web 交互原型系统的开发与服务器公网部署守护。
|
||||
|
||||
2. **许珍妮 (核心成员 / 金融工程与动态精算)**:
|
||||
* **专业领域**:金融工程 / 保险精算
|
||||
* **核心职责**:负责科创企业六维特有风险量化指标体系建立、依托中国平安保险主业的“科创特有风险综合险”创新产品方案设计,以及基于六维综合风险评分的动态保费精算模型与调节系数推导。
|
||||
|
||||
3. **赖应珍 (核心成员 / 经济法学与合规研究)**:
|
||||
* **专业领域**:经济法学 / 知识产权与金融合规
|
||||
* **核心职责**:负责国家网信办《互联网信息服务算法备案》合规前置规则提炼、美国商务部 BIS 实体清单地缘政治出口管制红线研究,以及自动化核保决策报告中特约防范条款(如限期国产化替代预案)的法理设计。
|
||||
|
||||
```mermaid
|
||||
graph TD
|
||||
TeamLeader["周丽梅 (项目负责人)<br/>AI 与软件工程实现 (计算机/数据科学)"]
|
||||
Member1["许珍妮 (核心成员)<br/>金融精算 (金融工程)"]
|
||||
Member2["赖应珍 (核心成员)<br/>法律研究 (经济法学)"]
|
||||
|
||||
TeamLeader --> Code1["DeepSeek 引擎 & Multi-Agent 辩论"]
|
||||
TeamLeader --> Code2["NetworkX 图谱 & Web 原型部署"]
|
||||
|
||||
Member1 --> Fin1["六维特有风险量化指标"]
|
||||
Member1 --> Fin2["平安综合险 & 动态保费精算"]
|
||||
|
||||
Member2 --> Law1["网信办算法备案合规审查"]
|
||||
Member2 --> Law2["BIS 清单 & 核保特约条款法理"]
|
||||
|
||||
style TeamLeader fill:#0284c7,stroke:#38bdf8,color:#ffffff,stroke-width:2px
|
||||
style Member1 fill:#059669,stroke:#34d399,color:#ffffff,stroke-width:2px
|
||||
style Member2 fill:#ea580c,stroke:#fb923c,color:#ffffff,stroke-width:2px
|
||||
|
||||
style Code1 fill:#e0f2fe,color:#0369a1
|
||||
style Code2 fill:#e0f2fe,color:#0369a1
|
||||
style Fin1 fill:#d1fae5,color:#047857
|
||||
style Fin2 fill:#d1fae5,color:#047857
|
||||
style Law1 fill:#ffedd5,color:#c2410c
|
||||
style Law2 fill:#ffedd5,color:#c2410c
|
||||
```
|
||||
|
||||
### 6.2 跨学科研发成果与协同优势
|
||||
|
||||
本课题突破了单一学科研究的局限,形成了“**技术可落地(周丽梅)+ 金融可商业化(许珍妮)+ 法律强合规(赖应珍)**”的跨学科硬核闭环:
|
||||
1. **AI 与软件工程成果**:交付了可直接运行与公网访问的金融级 Web 系统原型 (`https://risk.aiformat.cn`),实现推理思维链实时透明化。
|
||||
2. **金融精算成果**:推导出可量化、可拉杆调节的保费精算模型,为中国平安主业防范范式失灵提供了可操作方案。
|
||||
3. **经济法学成果**:将监管法规硬编码为一票否决风控规则,保障了系统裁决的法理严谨性与强合规性。
|
||||
|
||||
---
|
||||
|
||||
## 第七章 结论与未来展望
|
||||
|
||||
### 7.1 研究结论总结
|
||||
|
||||
本研究针对科创企业技术密集、高不确定性及非标准化特有风险突出的时代痛点,成功构建了“基于 AI 多智能体交叉辩论 × 供应链知识图谱”的新型风控范式,并打通了中国平安保险主业的动态定价与智能核保闭环:
|
||||
1. 精准定义了技术路线颠覆、核心人员流失、算法合规等六维特有风险指标,填补了传统静态财务风控的盲区。
|
||||
2. 引入多智能体交叉辩论与冲突消除机制,克服了单一大模型评估的视角片面性与事实幻觉。
|
||||
3. 建立供应链拓扑知识图谱,实现了地缘政治制裁风险的毫秒级穿透预警。
|
||||
4. 推出“科创特有风险综合险”及动态定价引擎,将学术研究转化为具备强可行性的金融产品原型。
|
||||
|
||||
### 7.2 场景扩展与应用前景
|
||||
|
||||
本研究成果具备极高的通用性与扩展潜力,可进一步辐射至多个相关金融场景:
|
||||
1. **商业银行“科技贷”质押审查**:帮助银行穿透评估科创企业的专利技术真实价值与供应链稳定性,降低坏账风险。
|
||||
2. **硬科技 VC/PE 智能尽职调查**:为股权投资机构提供自动化的技术路线颠覆性预警与高管履历穿透工具,提升尽调效率。
|
||||
3. **再保险与巨灾风险组合管理**:为再保险公司评估高科技产业链的系统性断供风险,优化风险组合配置。
|
||||
|
||||
---
|
||||
*中国青基会平安励志计划 · 2026 挑战杯“揭榜挂帅”擂台赛 XH-202626 课题研究团队 敬呈*
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""多智能体辩论模块"""
|
||||
@@ -0,0 +1,203 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Agent 基类
|
||||
支持逐 Token 实时流式打字机输出 (Token-level UI Streaming)
|
||||
捕获思维链 (reasoning_content) 与最终生成结果 (content) 逐字推送到前端
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional, Callable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseAgent:
|
||||
"""智能体基类,支持 Token 级流式 LLM 推理与降级逻辑"""
|
||||
|
||||
def __init__(self, name: str, system_prompt: str, role_icon: str = "🤖"):
|
||||
self.name = name
|
||||
self.system_prompt = system_prompt
|
||||
self.role_icon = role_icon
|
||||
self._client = None
|
||||
# 推理链记录
|
||||
self.reasoning_trace = []
|
||||
# 逐 Token 实时回调函数: callback(token_type: "reasoning"|"content", token_text: str)
|
||||
self.on_token_callback: Optional[Callable[[str, str], None]] = None
|
||||
|
||||
def _get_client(self):
|
||||
"""延迟初始化 OpenAI 客户端"""
|
||||
if self._client is not None:
|
||||
return self._client
|
||||
|
||||
try:
|
||||
import httpx
|
||||
from openai import OpenAI
|
||||
import os
|
||||
|
||||
from config import (
|
||||
DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL, DEEPSEEK_MODEL,
|
||||
VOLCENGINE_API_KEY, VOLCENGINE_BASE_URL, VOLCENGINE_MODEL,
|
||||
OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL
|
||||
)
|
||||
|
||||
api_key = os.environ.get("DEEPSEEK_API_KEY", "")
|
||||
if api_key:
|
||||
base_url = DEEPSEEK_BASE_URL
|
||||
model = DEEPSEEK_MODEL
|
||||
else:
|
||||
api_key = VOLCENGINE_API_KEY
|
||||
base_url = VOLCENGINE_BASE_URL
|
||||
model = VOLCENGINE_MODEL
|
||||
|
||||
if not api_key:
|
||||
api_key = OPENAI_API_KEY
|
||||
base_url = OPENAI_BASE_URL
|
||||
model = OPENAI_MODEL
|
||||
|
||||
if api_key:
|
||||
http_client = httpx.Client(trust_env=False, timeout=60.0)
|
||||
self._client = OpenAI(
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
http_client=http_client,
|
||||
)
|
||||
self._model = model
|
||||
logger.info(f"[{self.name}] 已成功连接大模型服务")
|
||||
return self._client
|
||||
except ImportError:
|
||||
logger.warning("openai 库未安装")
|
||||
except Exception as e:
|
||||
logger.warning(f"初始化 LLM 客户端失败: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def _trace(self, step: str, content: str):
|
||||
"""记录推理链步骤"""
|
||||
entry = {
|
||||
"timestamp": time.strftime("%H:%M:%S"),
|
||||
"step": step,
|
||||
"content": content,
|
||||
"agent": self.name,
|
||||
"icon": self.role_icon
|
||||
}
|
||||
self.reasoning_trace.append(entry)
|
||||
|
||||
def infer(self, prompt: str, temperature: float = 0.1, max_retries: int = 0) -> str:
|
||||
"""
|
||||
执行 SSE 流式 LLM 推理 (stream=True)
|
||||
逐 Token 实时推送到 on_token_callback 渲染打字机效果
|
||||
"""
|
||||
self.reasoning_trace = []
|
||||
self._trace("📝 构建 Context", f"准备【{self.name}】数据与 Prompt")
|
||||
|
||||
client = self._get_client()
|
||||
if client is None:
|
||||
self._trace("⚠️ 状态通知", "大模型未就绪,切换至专家规则引擎")
|
||||
logger.info(f"[{self.name}] LLM 不可用,降级到规则引擎")
|
||||
fallback = self.fallback_inference(prompt)
|
||||
self._trace("🔧 专家引擎输出", fallback)
|
||||
return fallback
|
||||
|
||||
self._trace("🔗 大模型连接", "已连接大模型推理服务")
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
self._trace("🚀 发起流式推理", "正在建立 SSE 流式传输通道...")
|
||||
t0 = time.time()
|
||||
|
||||
stream_resp = client.chat.completions.create(
|
||||
model=self._model,
|
||||
messages=[
|
||||
{"role": "system", "content": self.system_prompt},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
temperature=temperature,
|
||||
max_tokens=2048,
|
||||
stream=True,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
full_content = []
|
||||
reasoning_chunks = []
|
||||
|
||||
for chunk in stream_resp:
|
||||
if not chunk.choices:
|
||||
continue
|
||||
delta = chunk.choices[0].delta
|
||||
|
||||
# 1. 逐 Token 提取深度思考过程 (reasoning_content)
|
||||
reasoning_piece = getattr(delta, "reasoning_content", None) or getattr(delta, "reasoning", None)
|
||||
if reasoning_piece:
|
||||
reasoning_chunks.append(reasoning_piece)
|
||||
if self.on_token_callback:
|
||||
try:
|
||||
self.on_token_callback("reasoning", reasoning_piece)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. 逐 Token 提取正式回答内容 (content)
|
||||
content_piece = delta.content
|
||||
if content_piece:
|
||||
full_content.append(content_piece)
|
||||
if self.on_token_callback:
|
||||
try:
|
||||
self.on_token_callback("content", content_piece)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
elapsed = time.time() - t0
|
||||
final_text = "".join(full_content)
|
||||
full_reasoning = "".join(reasoning_chunks)
|
||||
|
||||
if full_reasoning:
|
||||
self._trace("🧠 完整思维链", full_reasoning)
|
||||
|
||||
if final_text.strip():
|
||||
self._trace("✅ 流式生成完毕", f"耗时 {elapsed:.1f}s | 产出 {len(final_text)} 字符")
|
||||
self._trace("📄 原始推理输出", final_text)
|
||||
return final_text
|
||||
else:
|
||||
self._trace("⚠️ 输出为空", "流式生成无有效内容,降级到专家引擎")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
self._trace("⚡ 流式传输异常", f"连接中断: {error_msg}")
|
||||
logger.warning(f"[{self.name}] 流式调用失败: {e}")
|
||||
|
||||
self._trace("🛡️ 安全降级", "无缝切换至离线风控规则引擎")
|
||||
fallback = self.fallback_inference(prompt)
|
||||
self._trace("🔧 专家引擎输出", fallback)
|
||||
return fallback
|
||||
|
||||
def infer_json(self, prompt: str, temperature: float = 0.1) -> dict:
|
||||
"""
|
||||
执行 LLM 推理并解析为 JSON
|
||||
"""
|
||||
result = self.infer(prompt, temperature)
|
||||
try:
|
||||
if "```json" in result:
|
||||
json_str = result.split("```json")[1].split("```")[0].strip()
|
||||
parsed = json.loads(json_str)
|
||||
self._trace("✅ 结构解析", "从 Markdown 成功提取 JSON 数据")
|
||||
return parsed
|
||||
elif "```" in result:
|
||||
json_str = result.split("```")[1].split("```")[0].strip()
|
||||
parsed = json.loads(json_str)
|
||||
self._trace("✅ 结构解析", "从代码块成功提取 JSON 数据")
|
||||
return parsed
|
||||
else:
|
||||
parsed = json.loads(result)
|
||||
self._trace("✅ 结构解析", "直接解析 JSON 成功")
|
||||
return parsed
|
||||
except (json.JSONDecodeError, IndexError):
|
||||
self._trace("⚠️ 格式适配", "启用自动结构修正")
|
||||
logger.warning(f"[{self.name}] JSON 解析失败,返回原始文本")
|
||||
return {"raw_response": result, "parse_error": True}
|
||||
|
||||
def fallback_inference(self, prompt: str) -> str:
|
||||
"""规则引擎降级推理"""
|
||||
return json.dumps({"error": "大模型服务不可用,规则引擎未实现"}, ensure_ascii=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.role_icon} {self.name}"
|
||||
@@ -0,0 +1,178 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
多智能体辩论编排引擎
|
||||
流程:信息分发 → 独立研判 → 交叉质证 → 综合裁决
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from .law_agent import LawAgent
|
||||
from .tech_agent import TechAgent
|
||||
from .finance_agent import FinanceAgent
|
||||
from .judge_agent import JudgeAgent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DebateEngine:
|
||||
"""多智能体辩论编排器"""
|
||||
|
||||
def __init__(self):
|
||||
self.law_agent = LawAgent()
|
||||
self.tech_agent = TechAgent()
|
||||
self.finance_agent = FinanceAgent()
|
||||
self.judge_agent = JudgeAgent()
|
||||
self.debate_log = []
|
||||
|
||||
def run_debate(self, company_data: dict, callback=None) -> dict:
|
||||
"""
|
||||
执行完整的多智能体辩论流程
|
||||
|
||||
Args:
|
||||
company_data: 企业数据字典
|
||||
callback: 进度回调函数 callback(step, message, result)
|
||||
"""
|
||||
self.debate_log = []
|
||||
company_name = company_data.get("short_name", company_data.get("company_name", "未知"))
|
||||
start_time = time.time()
|
||||
|
||||
self._log(f"🏁 启动对 [{company_name}] 的多智能体交叉验证辩论")
|
||||
|
||||
# ==============================
|
||||
# Phase 1: 独立研判
|
||||
# ==============================
|
||||
self._log("=" * 50)
|
||||
self._log("📋 Phase 1: 各节点独立研判")
|
||||
self._log("=" * 50)
|
||||
|
||||
# 法务节点
|
||||
self._log("👩⚖️ 法务风控节点开始评估...")
|
||||
if callback:
|
||||
callback("law_start", "法务风控节点开始评估...", None)
|
||||
law_result = self.law_agent.evaluate(company_data)
|
||||
self._log(f"👩⚖️ 法务节点完成: 综合法务风险 {law_result.get('overall_law_risk', {}).get('score', '?')} 分")
|
||||
if callback:
|
||||
callback("law_done", "法务风控节点评估完成", law_result)
|
||||
|
||||
# 技术节点
|
||||
self._log("👨🔬 技术风控节点开始评估...")
|
||||
if callback:
|
||||
callback("tech_start", "技术风控节点开始评估...", None)
|
||||
tech_result = self.tech_agent.evaluate(company_data)
|
||||
self._log(f"👨🔬 技术节点完成: 综合技术风险 {tech_result.get('overall_tech_risk', {}).get('score', '?')} 分")
|
||||
if callback:
|
||||
callback("tech_done", "技术风控节点评估完成", tech_result)
|
||||
|
||||
# 财务节点
|
||||
self._log("👔 财务风控节点开始评估...")
|
||||
if callback:
|
||||
callback("fin_start", "财务风控节点开始评估...", None)
|
||||
finance_result = self.finance_agent.evaluate(company_data)
|
||||
self._log(f"👔 财务节点完成: 综合财务风险 {finance_result.get('overall_fin_risk', {}).get('score', '?')} 分")
|
||||
if callback:
|
||||
callback("fin_done", "财务风控节点评估完成", finance_result)
|
||||
|
||||
# ==============================
|
||||
# Phase 2: 交叉质证(记录冲突点)
|
||||
# ==============================
|
||||
self._log("=" * 50)
|
||||
self._log("🔄 Phase 2: 交叉质证")
|
||||
self._log("=" * 50)
|
||||
|
||||
conflicts = self._identify_conflicts(law_result, tech_result, finance_result)
|
||||
for conflict in conflicts:
|
||||
self._log(f"⚠️ 冲突: {conflict}")
|
||||
if not conflicts:
|
||||
self._log("✅ 各节点意见一致,无冲突")
|
||||
|
||||
if callback:
|
||||
callback("cross_validation", "交叉质证完成", {"conflicts": conflicts})
|
||||
|
||||
# ==============================
|
||||
# Phase 3: 综合裁决
|
||||
# ==============================
|
||||
self._log("=" * 50)
|
||||
self._log("⚖️ Phase 3: 综合裁决")
|
||||
self._log("=" * 50)
|
||||
|
||||
if callback:
|
||||
callback("judge_start", "综合裁决节点开始...", None)
|
||||
judge_result = self.judge_agent.evaluate(
|
||||
company_data, law_result, tech_result, finance_result
|
||||
)
|
||||
self._log(f"⚖️ 综合评分: {judge_result.get('comprehensive_score', '?')} 分")
|
||||
self._log(f"⚖️ 核保建议: 【{judge_result.get('underwriting_decision', '?')}】")
|
||||
if callback:
|
||||
callback("judge_done", "综合裁决完成", judge_result)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
self._log(f"🏁 辩论完成,耗时 {elapsed:.1f} 秒")
|
||||
|
||||
return {
|
||||
"company": company_name,
|
||||
"law_result": law_result,
|
||||
"tech_result": tech_result,
|
||||
"finance_result": finance_result,
|
||||
"conflicts": conflicts,
|
||||
"judge_result": judge_result,
|
||||
"debate_log": self.debate_log,
|
||||
"elapsed_seconds": round(elapsed, 1),
|
||||
}
|
||||
|
||||
def _identify_conflicts(self, law_result: dict, tech_result: dict, finance_result: dict) -> list:
|
||||
"""识别各节点之间的判定冲突"""
|
||||
conflicts = []
|
||||
|
||||
# 检查法务和技术的冲突:例如法务认为合规但技术认为路线有风险
|
||||
law_overall = law_result.get("overall_law_risk", {}).get("score", 50)
|
||||
tech_overall = tech_result.get("overall_tech_risk", {}).get("score", 50)
|
||||
fin_overall = finance_result.get("overall_fin_risk", {}).get("score", 50)
|
||||
|
||||
# 大幅分歧(差异超过30分)
|
||||
if abs(law_overall - tech_overall) > 30:
|
||||
if law_overall > tech_overall:
|
||||
conflicts.append(
|
||||
f"法务节点({law_overall}分)与技术节点({tech_overall}分)存在较大分歧: "
|
||||
f"法务认为合规风险较高,但技术面评估相对乐观"
|
||||
)
|
||||
else:
|
||||
conflicts.append(
|
||||
f"技术节点({tech_overall}分)与法务节点({law_overall}分)存在较大分歧: "
|
||||
f"技术风险较高,但法务合规状态相对可控"
|
||||
)
|
||||
|
||||
if abs(tech_overall - fin_overall) > 30:
|
||||
conflicts.append(
|
||||
f"技术节点({tech_overall}分)与财务节点({fin_overall}分)存在分歧: "
|
||||
f"需审查技术投入与财务表现的匹配度"
|
||||
)
|
||||
|
||||
# 特定维度冲突:技术认为研发投入大=好事,财务可能认为是资本化操纵
|
||||
tech_rd_view = tech_result.get("tech_iteration_pressure", {}).get("score", 50)
|
||||
fin_rd_view = finance_result.get("rd_capitalization_risk", {}).get("score", 50)
|
||||
if tech_rd_view < 40 and fin_rd_view > 60:
|
||||
conflicts.append(
|
||||
"技术节点认为研发投入合理,但财务节点发现研发资本化率异常,"
|
||||
"存在通过资本化手段美化利润的嫌疑"
|
||||
)
|
||||
|
||||
return conflicts
|
||||
|
||||
def _log(self, message: str):
|
||||
"""记录辩论日志"""
|
||||
entry = {"timestamp": time.strftime("%H:%M:%S"), "message": message}
|
||||
self.debate_log.append(entry)
|
||||
logger.info(message)
|
||||
|
||||
|
||||
def run_debate(stock_code: str) -> dict:
|
||||
"""便捷接口:通过股票代码直接运行辩论"""
|
||||
from collectors.financial_collector import get_company_by_code
|
||||
company_data = get_company_by_code(stock_code)
|
||||
if not company_data:
|
||||
return {"error": f"未找到股票代码 {stock_code} 的企业数据"}
|
||||
|
||||
engine = DebateEngine()
|
||||
return engine.run_debate(company_data)
|
||||
@@ -0,0 +1,174 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
财务风控节点
|
||||
审查维度:研发资本化操纵、客户/供应商集中度、应收账款质量、现金流
|
||||
"""
|
||||
import json
|
||||
from .base_agent import BaseAgent
|
||||
|
||||
|
||||
FIN_SYSTEM_PROMPT = """你是一名精通科创板审计规则的注册会计师(CPA),同时是金融风控专家。
|
||||
|
||||
你的任务是基于提供的企业财务数据,从财务角度穿透审查以下风险:
|
||||
1. 研发资本化操纵风险:研发资本化率是否异常,是否存在美化利润嫌疑
|
||||
2. 客户/供应商集中风险:前五大客户/供应商占比是否过高
|
||||
3. 应收账款质量:应收账款周转率是否异常,是否存在坏账风险
|
||||
4. 现金流健康度:经营现金流是否能覆盖运营需求
|
||||
|
||||
评估标准参考:
|
||||
- 科创板企业研发资本化率超过30%需重点关注
|
||||
- 前五大客户占比超过50%存在集中风险
|
||||
- 应收账款周转率低于4次/年需关注回款能力
|
||||
- 经营现金流/营收比低于0.5需关注持续经营能力
|
||||
|
||||
请输出JSON格式:
|
||||
{
|
||||
"agent": "财务风控节点",
|
||||
"rd_capitalization_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"concentration_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"receivable_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"cashflow_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"overall_fin_risk": {"score": 0-100, "level": "高/中/低"},
|
||||
"key_findings": ["..."],
|
||||
"recommendations": ["..."]
|
||||
}"""
|
||||
|
||||
|
||||
class FinanceAgent(BaseAgent):
|
||||
"""财务风控节点"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="财务风控节点",
|
||||
system_prompt=FIN_SYSTEM_PROMPT,
|
||||
role_icon="👔",
|
||||
)
|
||||
|
||||
def evaluate(self, company_data: dict) -> dict:
|
||||
"""执行财务风险评估"""
|
||||
prompt = self._build_prompt(company_data)
|
||||
result = self.infer_json(prompt)
|
||||
|
||||
if result.get("parse_error"):
|
||||
result = self._rule_based_evaluation(company_data)
|
||||
|
||||
result["agent"] = "财务风控节点"
|
||||
result["icon"] = self.role_icon
|
||||
return result
|
||||
|
||||
def _build_prompt(self, company_data: dict) -> str:
|
||||
financials = company_data.get("financials", {})
|
||||
return f"""请对以下科创企业进行财务风险穿透审查:
|
||||
|
||||
企业名称:{company_data.get('short_name', '未知')}
|
||||
行业:{company_data.get('industry', '未知')}
|
||||
|
||||
财务核心指标:
|
||||
- 营业收入: {financials.get('revenue_2024', 0):,.0f} 元
|
||||
- 净利润: {financials.get('net_profit_2024', 0):,.0f} 元
|
||||
- 研发费用: {financials.get('rd_expense_2024', 0):,.0f} 元
|
||||
- 研发资本化率: {financials.get('rd_capitalization_rate', 0):.1%}
|
||||
- 研发/营收比: {financials.get('rd_revenue_ratio', 0):.1%}
|
||||
- 前五大客户占比: {financials.get('top5_customer_ratio', 0):.1%}
|
||||
- 前五大供应商占比: {financials.get('top5_supplier_ratio', 0):.1%}
|
||||
- 应收账款周转率: {financials.get('receivable_turnover', 0):.1f} 次/年
|
||||
- 经营现金流比率: {financials.get('cash_flow_ratio', 0):.2f}
|
||||
|
||||
请输出严格的JSON评估结果。"""
|
||||
|
||||
def _rule_based_evaluation(self, company_data: dict) -> dict:
|
||||
"""基于规则的财务风险评估"""
|
||||
fin = company_data.get("financials", {})
|
||||
|
||||
# 1. 研发资本化操纵风险
|
||||
cap_rate = fin.get("rd_capitalization_rate", 0)
|
||||
if cap_rate >= 0.4:
|
||||
rd_score = 90
|
||||
rd_detail = f"研发资本化率高达{cap_rate:.0%},严重怀疑美化利润"
|
||||
elif cap_rate >= 0.3:
|
||||
rd_score = 70
|
||||
rd_detail = f"研发资本化率{cap_rate:.0%},超过行业警戒线(30%),需重点审查"
|
||||
elif cap_rate >= 0.15:
|
||||
rd_score = 45
|
||||
rd_detail = f"研发资本化率{cap_rate:.0%},处于中等水平,建议关注趋势"
|
||||
elif cap_rate > 0:
|
||||
rd_score = 25
|
||||
rd_detail = f"研发资本化率{cap_rate:.0%},处于合理范围"
|
||||
else:
|
||||
rd_score = 10
|
||||
rd_detail = "研发费用全部费用化处理,财务政策审慎"
|
||||
|
||||
# 2. 集中度风险
|
||||
customer_ratio = fin.get("top5_customer_ratio", 0)
|
||||
supplier_ratio = fin.get("top5_supplier_ratio", 0)
|
||||
max_concentration = max(customer_ratio, supplier_ratio)
|
||||
|
||||
if max_concentration >= 0.8:
|
||||
conc_score = 90
|
||||
conc_detail = f"前五大客户占比{customer_ratio:.0%},供应商占比{supplier_ratio:.0%},集中度极高"
|
||||
elif max_concentration >= 0.6:
|
||||
conc_score = 70
|
||||
conc_detail = f"前五大客户占比{customer_ratio:.0%},供应商占比{supplier_ratio:.0%},集中度偏高"
|
||||
elif max_concentration >= 0.4:
|
||||
conc_score = 45
|
||||
conc_detail = f"前五大客户占比{customer_ratio:.0%},供应商占比{supplier_ratio:.0%},中等集中度"
|
||||
else:
|
||||
conc_score = 20
|
||||
conc_detail = f"客户和供应商分布较为分散"
|
||||
|
||||
# 3. 应收账款风险
|
||||
turnover = fin.get("receivable_turnover", 8)
|
||||
if turnover < 3:
|
||||
recv_score = 80
|
||||
recv_detail = f"应收账款周转率仅{turnover:.1f}次/年,回款能力极差"
|
||||
elif turnover < 5:
|
||||
recv_score = 55
|
||||
recv_detail = f"应收账款周转率{turnover:.1f}次/年,回款速度偏慢"
|
||||
elif turnover < 8:
|
||||
recv_score = 30
|
||||
recv_detail = f"应收账款周转率{turnover:.1f}次/年,回款能力尚可"
|
||||
else:
|
||||
recv_score = 15
|
||||
recv_detail = f"应收账款周转率{turnover:.1f}次/年,回款能力良好"
|
||||
|
||||
# 4. 现金流风险
|
||||
cf_ratio = fin.get("cash_flow_ratio", 1.0)
|
||||
if cf_ratio < 0.5:
|
||||
cf_score = 80
|
||||
cf_detail = f"经营现金流比率仅{cf_ratio:.2f},存在持续经营风险"
|
||||
elif cf_ratio < 0.8:
|
||||
cf_score = 55
|
||||
cf_detail = f"经营现金流比率{cf_ratio:.2f},现金流偏紧"
|
||||
elif cf_ratio < 1.2:
|
||||
cf_score = 30
|
||||
cf_detail = f"经营现金流比率{cf_ratio:.2f},基本健康"
|
||||
else:
|
||||
cf_score = 15
|
||||
cf_detail = f"经营现金流比率{cf_ratio:.2f},现金流充裕"
|
||||
|
||||
overall = int(rd_score * 0.30 + conc_score * 0.30 +
|
||||
recv_score * 0.20 + cf_score * 0.20)
|
||||
|
||||
findings = [rd_detail, conc_detail]
|
||||
if recv_score >= 50:
|
||||
findings.append(recv_detail)
|
||||
if cf_score >= 50:
|
||||
findings.append(cf_detail)
|
||||
|
||||
return {
|
||||
"rd_capitalization_risk": {"score": rd_score, "level": self._level(rd_score), "detail": rd_detail},
|
||||
"concentration_risk": {"score": conc_score, "level": self._level(conc_score), "detail": conc_detail},
|
||||
"receivable_risk": {"score": recv_score, "level": self._level(recv_score), "detail": recv_detail},
|
||||
"cashflow_risk": {"score": cf_score, "level": self._level(cf_score), "detail": cf_detail},
|
||||
"overall_fin_risk": {"score": overall, "level": self._level(overall)},
|
||||
"key_findings": findings,
|
||||
"recommendations": ["关注研发资本化率变化趋势", "降低客户集中度风险"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _level(score: int) -> str:
|
||||
if score >= 70:
|
||||
return "高"
|
||||
elif score >= 40:
|
||||
return "中"
|
||||
return "低"
|
||||
@@ -0,0 +1,176 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
综合裁决节点
|
||||
汇总法务/技术/财务三方意见,消解冲突,输出最终综合评级
|
||||
"""
|
||||
import json
|
||||
from .base_agent import BaseAgent
|
||||
|
||||
|
||||
JUDGE_SYSTEM_PROMPT = """你是一名资深的风险管理委员会主席,负责汇总法务、技术、财务三方专家的研判意见。
|
||||
|
||||
你的任务是:
|
||||
1. 审阅三方专家的评估报告
|
||||
2. 识别各方意见的冲突点
|
||||
3. 基于优先级原则消解冲突(合规风险 > 技术风险 > 财务风险)
|
||||
4. 输出 0-100 的综合风险评分
|
||||
5. 给出最终核保建议
|
||||
|
||||
核保决策标准:
|
||||
- 综合风险 ≥ 80分:建议【拒绝承保】
|
||||
- 60 ≤ 综合风险 < 80分:建议【附条件承保】(高免赔额/限额)
|
||||
- 40 ≤ 综合风险 < 60分:建议【标准承保】(标准费率上浮)
|
||||
- 综合风险 < 40分:建议【优先承保】(可享费率优惠)
|
||||
|
||||
请输出JSON格式:
|
||||
{
|
||||
"comprehensive_score": 0-100,
|
||||
"risk_level": "极高/高/中/低",
|
||||
"underwriting_decision": "拒绝承保/附条件承保/标准承保/优先承保",
|
||||
"six_dimension_scores": {
|
||||
"tech_disruption": 0-100,
|
||||
"talent_loss": 0-100,
|
||||
"algo_compliance": 0-100,
|
||||
"geopolitical": 0-100,
|
||||
"rd_capitalization": 0-100,
|
||||
"concentration": 0-100
|
||||
},
|
||||
"conflict_resolution": "...",
|
||||
"key_risks": ["..."],
|
||||
"underwriting_conditions": ["..."],
|
||||
"summary": "..."
|
||||
}"""
|
||||
|
||||
|
||||
class JudgeAgent(BaseAgent):
|
||||
"""综合裁决节点"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="综合裁决节点",
|
||||
system_prompt=JUDGE_SYSTEM_PROMPT,
|
||||
role_icon="⚖️",
|
||||
)
|
||||
|
||||
def evaluate(self, company_data: dict, law_result: dict,
|
||||
tech_result: dict, finance_result: dict) -> dict:
|
||||
"""汇总三方意见,输出综合裁决"""
|
||||
prompt = self._build_prompt(company_data, law_result, tech_result, finance_result)
|
||||
result = self.infer_json(prompt)
|
||||
|
||||
if result.get("parse_error"):
|
||||
result = self._rule_based_evaluation(company_data, law_result, tech_result, finance_result)
|
||||
|
||||
result["agent"] = "综合裁决节点"
|
||||
result["icon"] = self.role_icon
|
||||
return result
|
||||
|
||||
def _build_prompt(self, company_data: dict, law_result: dict,
|
||||
tech_result: dict, finance_result: dict) -> str:
|
||||
return f"""请对以下科创企业的三方评估结果进行综合裁决:
|
||||
|
||||
企业名称:{company_data.get('short_name', '未知')}
|
||||
行业:{company_data.get('industry', '未知')}
|
||||
|
||||
=== 👩⚖️ 法务风控节点评估 ===
|
||||
{json.dumps(law_result, ensure_ascii=False, indent=2)}
|
||||
|
||||
=== 👨🔬 技术风控节点评估 ===
|
||||
{json.dumps(tech_result, ensure_ascii=False, indent=2)}
|
||||
|
||||
=== 👔 财务风控节点评估 ===
|
||||
{json.dumps(finance_result, ensure_ascii=False, indent=2)}
|
||||
|
||||
请消解可能存在的判定冲突,输出综合裁决JSON。"""
|
||||
|
||||
def _rule_based_evaluation(self, company_data: dict, law_result: dict,
|
||||
tech_result: dict, finance_result: dict) -> dict:
|
||||
"""规则引擎综合裁决"""
|
||||
|
||||
# 提取各维度得分
|
||||
def safe_score(result: dict, key: str) -> int:
|
||||
item = result.get(key, {})
|
||||
if isinstance(item, dict):
|
||||
return item.get("score", 50)
|
||||
return 50
|
||||
|
||||
# 六维评分
|
||||
scores = {
|
||||
"tech_disruption": safe_score(tech_result, "tech_disruption_risk"),
|
||||
"talent_loss": safe_score(tech_result, "talent_loss_risk"),
|
||||
"algo_compliance": safe_score(law_result, "algo_compliance_risk"),
|
||||
"geopolitical": safe_score(law_result, "geopolitical_risk"),
|
||||
"rd_capitalization": safe_score(finance_result, "rd_capitalization_risk"),
|
||||
"concentration": safe_score(finance_result, "concentration_risk"),
|
||||
}
|
||||
|
||||
# 加权综合得分
|
||||
weights = {
|
||||
"tech_disruption": 0.20,
|
||||
"talent_loss": 0.15,
|
||||
"algo_compliance": 0.15,
|
||||
"geopolitical": 0.20,
|
||||
"rd_capitalization": 0.15,
|
||||
"concentration": 0.15,
|
||||
}
|
||||
|
||||
comprehensive_score = int(
|
||||
sum(scores[k] * weights[k] for k in scores)
|
||||
)
|
||||
|
||||
# 裁决
|
||||
if comprehensive_score >= 80:
|
||||
decision = "拒绝承保"
|
||||
risk_level = "极高"
|
||||
elif comprehensive_score >= 60:
|
||||
decision = "附条件承保"
|
||||
risk_level = "高"
|
||||
elif comprehensive_score >= 40:
|
||||
decision = "标准承保"
|
||||
risk_level = "中"
|
||||
else:
|
||||
decision = "优先承保"
|
||||
risk_level = "低"
|
||||
|
||||
# 收集关键风险
|
||||
key_risks = []
|
||||
for dim, score in sorted(scores.items(), key=lambda x: x[1], reverse=True):
|
||||
if score >= 60:
|
||||
dim_names = {
|
||||
"tech_disruption": "技术路线颠覆",
|
||||
"talent_loss": "核心人员流失",
|
||||
"algo_compliance": "算法/数据合规",
|
||||
"geopolitical": "地缘政治/出口管制",
|
||||
"rd_capitalization": "研发资本化操纵",
|
||||
"concentration": "客户/供应商集中",
|
||||
}
|
||||
key_risks.append(f"{dim_names.get(dim, dim)}风险({score}分)")
|
||||
|
||||
# 核保条件
|
||||
conditions = []
|
||||
if scores["geopolitical"] >= 70:
|
||||
conditions.append("要求提供出口管制合规声明及供应链替代方案")
|
||||
if scores["rd_capitalization"] >= 60:
|
||||
conditions.append("要求额外提供研发资本化会计政策说明及审计意见")
|
||||
if scores["concentration"] >= 60:
|
||||
conditions.append("要求提供客户分散化计划或前五大客户信用报告")
|
||||
if scores["talent_loss"] >= 60:
|
||||
conditions.append("要求核心技术人员签署竞业协议且公司有留任激励计划")
|
||||
|
||||
company_name = company_data.get("short_name", "该企业")
|
||||
summary = (
|
||||
f"{company_name}综合风险评分{comprehensive_score}分(风险等级:{risk_level})。"
|
||||
f"核保建议:【{decision}】。"
|
||||
f"主要风险集中在{'、'.join(key_risks[:3]) if key_risks else '无突出风险'}。"
|
||||
)
|
||||
|
||||
return {
|
||||
"comprehensive_score": comprehensive_score,
|
||||
"risk_level": risk_level,
|
||||
"underwriting_decision": decision,
|
||||
"six_dimension_scores": scores,
|
||||
"conflict_resolution": "基于优先级原则(合规>技术>财务)进行加权裁决",
|
||||
"key_risks": key_risks,
|
||||
"underwriting_conditions": conditions,
|
||||
"summary": summary,
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
法务风控节点
|
||||
审查维度:算法备案状态、实体清单命中、数据出境风险、知识产权诉讼
|
||||
"""
|
||||
import json
|
||||
from .base_agent import BaseAgent
|
||||
|
||||
|
||||
LAW_SYSTEM_PROMPT = """你是一名资深法务风控专家,精通以下法律法规:
|
||||
- 《生成式人工智能服务管理暂行办法》
|
||||
- 《互联网信息服务算法推荐管理规定》
|
||||
- 《数据安全法》《个人信息保护法》
|
||||
- 《出口管制法》及美国 BIS 实体清单相关规则
|
||||
- 《科创板上市规则》中的合规要求
|
||||
|
||||
你的任务是基于提供的企业数据,从法律合规角度评估以下风险:
|
||||
1. 算法备案合规风险:企业是否涉及AI业务但未完成算法备案
|
||||
2. 地缘政治与出口管制风险:企业或其供应链是否受到制裁
|
||||
3. 数据合规风险:是否存在数据出境、数据安全方面的隐患
|
||||
4. 知识产权诉讼风险:是否面临重大IP纠纷
|
||||
|
||||
请以严谨的法律视角进行评估,输出JSON格式:
|
||||
{
|
||||
"agent": "法务风控节点",
|
||||
"algo_compliance_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"geopolitical_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"data_compliance_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"ip_litigation_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"overall_law_risk": {"score": 0-100, "level": "高/中/低"},
|
||||
"key_findings": ["..."],
|
||||
"recommendations": ["..."]
|
||||
}"""
|
||||
|
||||
|
||||
class LawAgent(BaseAgent):
|
||||
"""法务风控节点"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="法务风控节点",
|
||||
system_prompt=LAW_SYSTEM_PROMPT,
|
||||
role_icon="👩⚖️",
|
||||
)
|
||||
|
||||
def evaluate(self, company_data: dict) -> dict:
|
||||
"""执行法务风险评估"""
|
||||
prompt = self._build_prompt(company_data)
|
||||
result = self.infer_json(prompt)
|
||||
|
||||
# 如果 JSON 解析失败,使用规则引擎
|
||||
if result.get("parse_error"):
|
||||
result = self._rule_based_evaluation(company_data)
|
||||
|
||||
result["agent"] = "法务风控节点"
|
||||
result["icon"] = self.role_icon
|
||||
return result
|
||||
|
||||
def _build_prompt(self, company_data: dict) -> str:
|
||||
"""构建评估提示词"""
|
||||
return f"""请对以下科创企业进行法务风险评估:
|
||||
|
||||
企业名称:{company_data.get('short_name', company_data.get('company_name', '未知'))}
|
||||
行业:{company_data.get('industry', '未知')}
|
||||
领域:{company_data.get('sector', '未知')}
|
||||
|
||||
合规状态:
|
||||
- 算法备案:{json.dumps(company_data.get('compliance', {}), ensure_ascii=False)}
|
||||
|
||||
供应链信息:
|
||||
- 关键供应商:{json.dumps(company_data.get('supply_chain', {}).get('key_suppliers', []), ensure_ascii=False)}
|
||||
- 供应商集中度风险:{company_data.get('supply_chain', {}).get('supplier_concentration_risk', '未知')}
|
||||
|
||||
技术路线:
|
||||
{json.dumps(company_data.get('tech_route', {}), ensure_ascii=False)}
|
||||
|
||||
请输出严格的JSON评估结果。"""
|
||||
|
||||
def fallback_inference(self, prompt: str) -> str:
|
||||
"""规则引擎降级"""
|
||||
return json.dumps(self._rule_based_evaluation({}), ensure_ascii=False)
|
||||
|
||||
def _rule_based_evaluation(self, company_data: dict) -> dict:
|
||||
"""基于规则的法务风险评估"""
|
||||
compliance = company_data.get("compliance", {})
|
||||
supply_chain = company_data.get("supply_chain", {})
|
||||
sector = company_data.get("sector", "")
|
||||
|
||||
# 算法备案风险
|
||||
algo_status = compliance.get("algo_filing_status", "")
|
||||
algo_score = 20
|
||||
algo_detail = "合规状态正常"
|
||||
if sector in ["AI", "软件", "互联网"] and algo_status == "不适用":
|
||||
algo_score = 60
|
||||
algo_detail = "涉及AI业务但标注为不适用,建议核实"
|
||||
elif "未" in algo_status or not algo_status:
|
||||
algo_score = 80
|
||||
algo_detail = "未查到算法备案记录,存在合规风险"
|
||||
elif "已备案" in algo_status:
|
||||
algo_score = 10
|
||||
algo_detail = "已完成算法备案"
|
||||
|
||||
# 地缘政治风险
|
||||
entity_status = compliance.get("entity_list_status", "")
|
||||
geo_score = 15
|
||||
geo_detail = "未受出口管制影响"
|
||||
if "被列入" in entity_status:
|
||||
geo_score = 95
|
||||
geo_detail = f"已被列入实体清单: {compliance.get('sanctions_detail', '')}"
|
||||
elif supply_chain.get("supplier_concentration_risk") == "极高":
|
||||
geo_score = 70
|
||||
geo_detail = "核心供应链高度依赖海外,存在间接制裁风险"
|
||||
|
||||
# 数据合规风险
|
||||
data_risk = compliance.get("data_export_risk", "低")
|
||||
data_score = {"高": 75, "中": 45, "低": 15}.get(data_risk, 20)
|
||||
data_detail = f"数据出境风险等级: {data_risk}"
|
||||
|
||||
# 知识产权风险
|
||||
ip_score = 25
|
||||
ip_detail = "未发现重大IP纠纷"
|
||||
|
||||
# 综合法务风险
|
||||
overall_score = int(
|
||||
algo_score * 0.25 + geo_score * 0.35 +
|
||||
data_score * 0.25 + ip_score * 0.15
|
||||
)
|
||||
|
||||
return {
|
||||
"algo_compliance_risk": {"score": algo_score, "level": self._level(algo_score), "detail": algo_detail},
|
||||
"geopolitical_risk": {"score": geo_score, "level": self._level(geo_score), "detail": geo_detail},
|
||||
"data_compliance_risk": {"score": data_score, "level": self._level(data_score), "detail": data_detail},
|
||||
"ip_litigation_risk": {"score": ip_score, "level": self._level(ip_score), "detail": ip_detail},
|
||||
"overall_law_risk": {"score": overall_score, "level": self._level(overall_score)},
|
||||
"key_findings": [algo_detail, geo_detail, data_detail],
|
||||
"recommendations": ["建议定期审查合规状态", "关注实体清单更新动态"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _level(score: int) -> str:
|
||||
if score >= 70:
|
||||
return "高"
|
||||
elif score >= 40:
|
||||
return "中"
|
||||
return "低"
|
||||
@@ -0,0 +1,147 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
技术风控节点
|
||||
审查维度:技术路线竞争态势、核心人员稳定性、专利布局、技术替代风险
|
||||
"""
|
||||
import json
|
||||
from .base_agent import BaseAgent
|
||||
|
||||
|
||||
TECH_SYSTEM_PROMPT = """你是一名资深科技行业分析师和技术风控专家。
|
||||
你精通半导体、人工智能、新能源、生物医疗等前沿科技领域的技术演进趋势。
|
||||
|
||||
你的任务是基于提供的企业数据,从技术角度评估以下风险:
|
||||
1. 技术路线颠覆风险:企业押注的技术路线是否面临被替代的风险
|
||||
2. 核心人员流失风险:关键技术人员的稳定性和不可替代性
|
||||
3. 专利/技术壁垒:技术护城河的深度和可持续性
|
||||
4. 技术迭代压力:行业技术迭代速度对企业的冲击
|
||||
|
||||
请以技术专家的视角进行深度评估,输出JSON格式:
|
||||
{
|
||||
"agent": "技术风控节点",
|
||||
"tech_disruption_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"talent_loss_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"patent_moat": {"score": 0-100, "level": "强/中/弱", "detail": "..."},
|
||||
"tech_iteration_pressure": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"overall_tech_risk": {"score": 0-100, "level": "高/中/低"},
|
||||
"key_findings": ["..."],
|
||||
"recommendations": ["..."]
|
||||
}"""
|
||||
|
||||
|
||||
class TechAgent(BaseAgent):
|
||||
"""技术风控节点"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="技术风控节点",
|
||||
system_prompt=TECH_SYSTEM_PROMPT,
|
||||
role_icon="👨🔬",
|
||||
)
|
||||
|
||||
def evaluate(self, company_data: dict) -> dict:
|
||||
"""执行技术风险评估"""
|
||||
prompt = self._build_prompt(company_data)
|
||||
result = self.infer_json(prompt)
|
||||
|
||||
if result.get("parse_error"):
|
||||
result = self._rule_based_evaluation(company_data)
|
||||
|
||||
result["agent"] = "技术风控节点"
|
||||
result["icon"] = self.role_icon
|
||||
return result
|
||||
|
||||
def _build_prompt(self, company_data: dict) -> str:
|
||||
return f"""请对以下科创企业进行技术风险评估:
|
||||
|
||||
企业名称:{company_data.get('short_name', '未知')}
|
||||
行业:{company_data.get('industry', '未知')}
|
||||
领域:{company_data.get('sector', '未知')}
|
||||
企业描述:{company_data.get('description', '')}
|
||||
|
||||
技术路线信息:
|
||||
{json.dumps(company_data.get('tech_route', {}), ensure_ascii=False, indent=2)}
|
||||
|
||||
核心技术人员:
|
||||
{json.dumps(company_data.get('core_tech_personnel', []), ensure_ascii=False, indent=2)}
|
||||
|
||||
财务中的研发指标:
|
||||
- 研发费用: {company_data.get('financials', {}).get('rd_expense_2024', 0)}
|
||||
- 研发营收比: {company_data.get('financials', {}).get('rd_revenue_ratio', 0)}
|
||||
|
||||
请输出严格的JSON评估结果。"""
|
||||
|
||||
def _rule_based_evaluation(self, company_data: dict) -> dict:
|
||||
"""基于规则的技术风险评估"""
|
||||
tech_route = company_data.get("tech_route", {})
|
||||
personnel = company_data.get("core_tech_personnel", [])
|
||||
financials = company_data.get("financials", {})
|
||||
|
||||
# 技术路线颠覆风险
|
||||
competing_techs = tech_route.get("competing_techs", [])
|
||||
disruption_score = min(20 + len(competing_techs) * 15, 90)
|
||||
tech_moat = tech_route.get("tech_moat", "")
|
||||
if "差距" in tech_moat or "受制" in tech_moat:
|
||||
disruption_score = min(disruption_score + 20, 95)
|
||||
disruption_detail = f"面临 {len(competing_techs)} 条竞争技术路线: {', '.join(competing_techs[:3])}"
|
||||
|
||||
# 核心人员流失风险
|
||||
talent_score = 20
|
||||
talent_detail = "核心团队稳定"
|
||||
departed = [p for p in personnel if "离职" in p.get("status", "")]
|
||||
high_importance = [p for p in personnel if p.get("importance") == "极高"]
|
||||
|
||||
if departed:
|
||||
talent_score = 80
|
||||
talent_detail = f"已有核心人员离职: {', '.join(p['name'] for p in departed)}"
|
||||
elif len(high_importance) == 1:
|
||||
talent_score = 55
|
||||
talent_detail = f"高度依赖单一核心人员: {high_importance[0]['name']}"
|
||||
elif len(personnel) <= 2:
|
||||
talent_score = 45
|
||||
talent_detail = "核心技术团队规模偏小"
|
||||
|
||||
# 专利壁垒
|
||||
patent_count = tech_route.get("patent_count", 0)
|
||||
if patent_count > 5000:
|
||||
patent_score = 20
|
||||
patent_detail = f"专利数量充足({patent_count}件),技术壁垒较强"
|
||||
elif patent_count > 1000:
|
||||
patent_score = 35
|
||||
patent_detail = f"专利数量中等({patent_count}件)"
|
||||
else:
|
||||
patent_score = 60
|
||||
patent_detail = f"专利数量偏少({patent_count}件),技术壁垒偏弱"
|
||||
|
||||
# 技术迭代压力(基于研发投入比)
|
||||
rd_ratio = financials.get("rd_revenue_ratio", 0)
|
||||
if rd_ratio > 0.3:
|
||||
iter_score = 65
|
||||
iter_detail = f"研发营收比极高({rd_ratio:.1%}),说明行业技术迭代压力大"
|
||||
elif rd_ratio > 0.15:
|
||||
iter_score = 45
|
||||
iter_detail = f"研发投入较高({rd_ratio:.1%}),需持续技术投入"
|
||||
else:
|
||||
iter_score = 25
|
||||
iter_detail = f"研发投入适中({rd_ratio:.1%})"
|
||||
|
||||
overall = int(disruption_score * 0.35 + talent_score * 0.25 +
|
||||
patent_score * 0.15 + iter_score * 0.25)
|
||||
|
||||
return {
|
||||
"tech_disruption_risk": {"score": disruption_score, "level": self._level(disruption_score), "detail": disruption_detail},
|
||||
"talent_loss_risk": {"score": talent_score, "level": self._level(talent_score), "detail": talent_detail},
|
||||
"patent_moat": {"score": patent_score, "level": "弱" if patent_score >= 50 else ("中" if patent_score >= 30 else "强"), "detail": patent_detail},
|
||||
"tech_iteration_pressure": {"score": iter_score, "level": self._level(iter_score), "detail": iter_detail},
|
||||
"overall_tech_risk": {"score": overall, "level": self._level(overall)},
|
||||
"key_findings": [disruption_detail, talent_detail, patent_detail],
|
||||
"recommendations": ["关注竞争技术路线发展", "加强核心人员留任激励"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _level(score: int) -> str:
|
||||
if score >= 70:
|
||||
return "高"
|
||||
elif score >= 40:
|
||||
return "中"
|
||||
return "低"
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据采集模块"""
|
||||
@@ -0,0 +1,188 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
年报 PDF 文本解析模块
|
||||
从年报中提取关键风险信息:核心技术人员、技术路线、诉讼、风险提示等
|
||||
"""
|
||||
import re
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_annual_report(pdf_path: str) -> dict:
|
||||
"""
|
||||
解析年报 PDF,提取关键风险相关信息
|
||||
返回结构化的风险要素字典
|
||||
"""
|
||||
text = _extract_text_from_pdf(pdf_path)
|
||||
if not text:
|
||||
return {"error": "PDF 解析失败", "raw_text": ""}
|
||||
|
||||
return {
|
||||
"core_personnel_info": _extract_core_personnel(text),
|
||||
"tech_route_info": _extract_tech_route(text),
|
||||
"litigation_info": _extract_litigation(text),
|
||||
"risk_factors": _extract_risk_factors(text),
|
||||
"rd_capitalization_info": _extract_rd_capitalization(text),
|
||||
"customer_concentration": _extract_customer_concentration(text),
|
||||
"raw_text_length": len(text),
|
||||
}
|
||||
|
||||
|
||||
def _extract_text_from_pdf(pdf_path: str) -> Optional[str]:
|
||||
"""使用 pdfplumber 提取 PDF 全文"""
|
||||
try:
|
||||
import pdfplumber
|
||||
text_parts = []
|
||||
with pdfplumber.open(pdf_path) as pdf:
|
||||
for page in pdf.pages:
|
||||
page_text = page.extract_text()
|
||||
if page_text:
|
||||
text_parts.append(page_text)
|
||||
return "\n".join(text_parts)
|
||||
except ImportError:
|
||||
logger.warning("pdfplumber 未安装,尝试 PyPDF2")
|
||||
try:
|
||||
from PyPDF2 import PdfReader
|
||||
reader = PdfReader(pdf_path)
|
||||
return "\n".join(
|
||||
page.extract_text() or "" for page in reader.pages
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"PyPDF2 解析失败: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"PDF 解析失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _extract_core_personnel(text: str) -> dict:
|
||||
"""提取核心技术人员相关信息"""
|
||||
result = {
|
||||
"has_departure": False,
|
||||
"departure_details": [],
|
||||
"personnel_count": 0,
|
||||
"key_mentions": [],
|
||||
}
|
||||
|
||||
# 匹配离职/辞职相关表述
|
||||
departure_patterns = [
|
||||
r"(核心技术人员|核心人员|关键技术人员).{0,30}(离职|辞职|离任|不再担任)",
|
||||
r"(CTO|首席技术官|技术总监|研发总监).{0,30}(离职|辞职|离任)",
|
||||
r"(离职|辞职).{0,30}(核心技术人员|核心人员)",
|
||||
]
|
||||
for pattern in departure_patterns:
|
||||
matches = re.findall(pattern, text)
|
||||
if matches:
|
||||
result["has_departure"] = True
|
||||
result["departure_details"].extend([str(m) for m in matches])
|
||||
|
||||
# 统计核心技术人员数量
|
||||
count_match = re.search(r"核心技术人员\s*(\d+)\s*[名人]", text)
|
||||
if count_match:
|
||||
result["personnel_count"] = int(count_match.group(1))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _extract_tech_route(text: str) -> dict:
|
||||
"""提取技术路线相关信息"""
|
||||
result = {
|
||||
"competing_tech_mentioned": False,
|
||||
"tech_keywords": [],
|
||||
"risk_mentions": [],
|
||||
}
|
||||
|
||||
# 技术竞争关键词
|
||||
tech_keywords = [
|
||||
"技术路线", "技术迭代", "技术替代", "技术颠覆",
|
||||
"竞争技术", "替代方案", "新一代技术",
|
||||
]
|
||||
for kw in tech_keywords:
|
||||
if kw in text:
|
||||
result["tech_keywords"].append(kw)
|
||||
result["competing_tech_mentioned"] = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _extract_litigation(text: str) -> dict:
|
||||
"""提取诉讼/仲裁相关信息"""
|
||||
result = {
|
||||
"has_litigation": False,
|
||||
"litigation_count": 0,
|
||||
"ip_related": False,
|
||||
}
|
||||
|
||||
# 诉讼关键词
|
||||
litigation_patterns = [
|
||||
r"(诉讼|仲裁|起诉|被告).{0,50}(知识产权|专利|商标|著作权)",
|
||||
r"(专利侵权|商标侵权|著作权纠纷)",
|
||||
]
|
||||
for pattern in litigation_patterns:
|
||||
if re.search(pattern, text):
|
||||
result["has_litigation"] = True
|
||||
result["ip_related"] = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _extract_risk_factors(text: str) -> list:
|
||||
"""提取风险因素章节的关键信息"""
|
||||
risk_keywords = [
|
||||
"地缘政治", "实体清单", "出口管制", "贸易摩擦",
|
||||
"数据安全", "数据合规", "算法备案", "数据出境",
|
||||
"客户集中", "供应商集中", "单一客户", "单一供应商",
|
||||
"研发资本化", "开发支出", "无形资产",
|
||||
"人才流失", "核心人员", "竞业限制",
|
||||
]
|
||||
found_risks = []
|
||||
for kw in risk_keywords:
|
||||
if kw in text:
|
||||
found_risks.append(kw)
|
||||
return found_risks
|
||||
|
||||
|
||||
def _extract_rd_capitalization(text: str) -> dict:
|
||||
"""提取研发资本化相关信息"""
|
||||
result = {
|
||||
"has_capitalization": False,
|
||||
"capitalization_mentioned": False,
|
||||
"amount_keywords": [],
|
||||
}
|
||||
|
||||
cap_keywords = ["开发支出", "研发资本化", "资本化研发", "开发阶段支出"]
|
||||
for kw in cap_keywords:
|
||||
if kw in text:
|
||||
result["capitalization_mentioned"] = True
|
||||
result["amount_keywords"].append(kw)
|
||||
|
||||
# 检查是否有具体的资本化金额
|
||||
cap_amount = re.search(r"开发支出.{0,30}([\d,\.]+)\s*(万元|百万|亿)", text)
|
||||
if cap_amount:
|
||||
result["has_capitalization"] = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _extract_customer_concentration(text: str) -> dict:
|
||||
"""提取客户/供应商集中度信息"""
|
||||
result = {
|
||||
"top5_customer_ratio": None,
|
||||
"top5_supplier_ratio": None,
|
||||
"single_customer_dependency": False,
|
||||
}
|
||||
|
||||
# 前五大客户占比
|
||||
customer_match = re.search(
|
||||
r"前五[名大]客户.{0,30}([\d\.]+)\s*%", text
|
||||
)
|
||||
if customer_match:
|
||||
result["top5_customer_ratio"] = float(customer_match.group(1)) / 100
|
||||
|
||||
# 单一客户依赖
|
||||
if re.search(r"(第一大客户|最大客户).{0,30}([\d\.]+)\s*%", text):
|
||||
result["single_customer_dependency"] = True
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
算法备案合规数据采集与查询模块
|
||||
匹配企业是否已完成网信办算法备案
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||
|
||||
|
||||
def _load_algo_filings() -> list:
|
||||
"""加载算法备案数据"""
|
||||
filepath = DATA_DIR / "algo_filings.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def check_algo_filing(company_name: str) -> dict:
|
||||
"""
|
||||
查询企业的算法备案状态
|
||||
"""
|
||||
filings = _load_algo_filings()
|
||||
result = {
|
||||
"has_filing": False,
|
||||
"filings": [],
|
||||
"needs_filing": False, # 是否需要备案但未备案
|
||||
"risk_level": "低",
|
||||
}
|
||||
|
||||
for filing in filings:
|
||||
if company_name in filing["company"] or filing["company"] in company_name:
|
||||
result["has_filing"] = True
|
||||
result["filings"].append(filing)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def assess_algo_compliance_risk(company_data: dict) -> dict:
|
||||
"""
|
||||
综合评估企业的算法合规风险
|
||||
考虑因素:是否涉及 AI 业务、是否已备案、数据出境风险
|
||||
"""
|
||||
company_name = company_data.get("short_name", company_data.get("company_name", ""))
|
||||
sector = company_data.get("sector", "")
|
||||
compliance = company_data.get("compliance", {})
|
||||
|
||||
# 查询备案状态
|
||||
filing_status = check_algo_filing(company_name)
|
||||
|
||||
# 判断是否需要备案
|
||||
ai_related_sectors = ["AI", "软件", "互联网", "消费电子"]
|
||||
needs_filing = sector in ai_related_sectors or "AI" in str(company_data.get("tech_route", {}))
|
||||
|
||||
# 综合评估
|
||||
risk_level = "低"
|
||||
risk_details = []
|
||||
|
||||
if needs_filing and not filing_status["has_filing"]:
|
||||
algo_status = compliance.get("algo_filing_status", "")
|
||||
if algo_status == "不适用":
|
||||
risk_level = "低"
|
||||
else:
|
||||
risk_level = "高"
|
||||
risk_details.append("涉及AI业务但未查到算法备案记录")
|
||||
|
||||
data_export_risk = compliance.get("data_export_risk", "低")
|
||||
if data_export_risk == "高":
|
||||
risk_level = "高"
|
||||
risk_details.append("存在大量跨境数据传输,数据出境评估风险高")
|
||||
elif data_export_risk == "中":
|
||||
if risk_level != "高":
|
||||
risk_level = "中"
|
||||
risk_details.append("存在部分跨境数据传输,需关注数据出境合规")
|
||||
|
||||
return {
|
||||
"company_name": company_name,
|
||||
"needs_filing": needs_filing,
|
||||
"filing_status": filing_status,
|
||||
"data_export_risk": data_export_risk,
|
||||
"overall_risk_level": risk_level,
|
||||
"risk_details": risk_details,
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
BIS 实体清单采集与匹配模块
|
||||
支持企业名模糊匹配 + 别名映射 + 供应链上游穿透
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||
|
||||
|
||||
def _load_entity_list() -> list:
|
||||
"""加载实体清单数据"""
|
||||
filepath = DATA_DIR / "entity_list.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def check_entity_list(company_name: str) -> dict:
|
||||
"""
|
||||
检查企业是否在 BIS 实体清单中
|
||||
支持模糊匹配和别名匹配
|
||||
"""
|
||||
entities = _load_entity_list()
|
||||
result = {
|
||||
"is_sanctioned": False,
|
||||
"match_type": None,
|
||||
"entity_detail": None,
|
||||
"supply_chain_risk": [], # 供应链上游被制裁的情况
|
||||
}
|
||||
|
||||
for entity in entities:
|
||||
# 精确匹配
|
||||
if company_name in entity["entity_name"]:
|
||||
result["is_sanctioned"] = True
|
||||
result["match_type"] = "直接命中"
|
||||
result["entity_detail"] = entity
|
||||
return result
|
||||
|
||||
# 别名匹配
|
||||
for alias in entity.get("aliases", []):
|
||||
if company_name in alias or alias in company_name:
|
||||
result["is_sanctioned"] = True
|
||||
result["match_type"] = "别名命中"
|
||||
result["entity_detail"] = entity
|
||||
return result
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def check_supply_chain_sanctions(company_name: str, suppliers: list) -> list:
|
||||
"""
|
||||
检查企业供应链上游是否有被制裁的实体
|
||||
返回受制裁的供应商列表
|
||||
"""
|
||||
sanctioned_suppliers = []
|
||||
entities = _load_entity_list()
|
||||
|
||||
for supplier in suppliers:
|
||||
# 清洗供应商名称(去掉括号中的说明文字)
|
||||
clean_name = supplier.split("(")[0].split("(")[0].strip()
|
||||
|
||||
for entity in entities:
|
||||
all_names = [entity["entity_name"]] + entity.get("aliases", [])
|
||||
for name in all_names:
|
||||
if clean_name in name or name in clean_name:
|
||||
sanctioned_suppliers.append({
|
||||
"supplier": supplier,
|
||||
"matched_entity": entity["entity_name"],
|
||||
"restrictions": entity["restrictions"],
|
||||
"date_added": entity["date_added"],
|
||||
})
|
||||
break
|
||||
|
||||
return sanctioned_suppliers
|
||||
|
||||
|
||||
def get_all_sanctioned_entities() -> list:
|
||||
"""获取所有被制裁实体列表"""
|
||||
return _load_entity_list()
|
||||
@@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
财务数据采集模块
|
||||
双轨策略:优先尝试 AKShare 在线采集,失败则回退到预置数据
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||
|
||||
|
||||
def _load_preset_data() -> list:
|
||||
"""加载预置的科创板企业数据"""
|
||||
filepath = DATA_DIR / "sample_companies.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def collect_financial_data(stock_code: str) -> Optional[dict]:
|
||||
"""
|
||||
采集指定股票代码的财务数据
|
||||
双轨策略:在线采集 → 离线预置
|
||||
"""
|
||||
# 尝试在线采集
|
||||
try:
|
||||
return _collect_online(stock_code)
|
||||
except Exception as e:
|
||||
logger.warning(f"在线采集 {stock_code} 失败: {e},回退到预置数据")
|
||||
|
||||
# 回退到预置数据
|
||||
return _collect_from_preset(stock_code)
|
||||
|
||||
|
||||
def _collect_online(stock_code: str) -> Optional[dict]:
|
||||
"""通过 AKShare 在线采集财务数据"""
|
||||
try:
|
||||
import akshare as ak
|
||||
|
||||
# 科创板企业利润表
|
||||
profit_df = ak.stock_profit_sheet_by_report_em(symbol=stock_code)
|
||||
# 科创板企业资产负债表
|
||||
balance_df = ak.stock_balance_sheet_by_report_em(symbol=stock_code)
|
||||
|
||||
if profit_df is not None and not profit_df.empty:
|
||||
latest = profit_df.iloc[0]
|
||||
return {
|
||||
"stock_code": stock_code,
|
||||
"revenue": float(latest.get("营业收入", 0)),
|
||||
"net_profit": float(latest.get("净利润", 0)),
|
||||
"rd_expense": float(latest.get("研发费用", 0)),
|
||||
"source": "akshare_online",
|
||||
}
|
||||
except ImportError:
|
||||
logger.warning("AKShare 未安装,跳过在线采集")
|
||||
except Exception as e:
|
||||
logger.warning(f"AKShare 采集异常: {e}")
|
||||
|
||||
raise RuntimeError("在线采集失败")
|
||||
|
||||
|
||||
def _collect_from_preset(stock_code: str) -> Optional[dict]:
|
||||
"""从预置数据中查找企业"""
|
||||
companies = _load_preset_data()
|
||||
for company in companies:
|
||||
if company["stock_code"] == stock_code:
|
||||
return {
|
||||
"stock_code": stock_code,
|
||||
"company_name": company["company_name"],
|
||||
"industry": company["industry"],
|
||||
"sector": company["sector"],
|
||||
"financials": company["financials"],
|
||||
"core_tech_personnel": company["core_tech_personnel"],
|
||||
"tech_route": company["tech_route"],
|
||||
"compliance": company["compliance"],
|
||||
"supply_chain": company["supply_chain"],
|
||||
"source": "preset_data",
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def get_all_companies() -> list:
|
||||
"""获取所有预置企业列表"""
|
||||
return _load_preset_data()
|
||||
|
||||
|
||||
def get_company_by_code(stock_code: str) -> Optional[dict]:
|
||||
"""通过股票代码查找企业完整数据"""
|
||||
companies = _load_preset_data()
|
||||
for company in companies:
|
||||
if company["stock_code"] == stock_code:
|
||||
return company
|
||||
return None
|
||||
|
||||
|
||||
def get_company_by_name(name: str) -> Optional[dict]:
|
||||
"""通过企业名称查找(支持简称)"""
|
||||
companies = _load_preset_data()
|
||||
for company in companies:
|
||||
if name in company["company_name"] or name in company["short_name"]:
|
||||
return company
|
||||
return None
|
||||
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
全局配置文件
|
||||
集中管理 API Key、文件路径、模型参数等
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# ============================================================
|
||||
# 项目根目录
|
||||
# ============================================================
|
||||
PROJECT_ROOT = Path(__file__).parent.resolve()
|
||||
DATA_DIR = PROJECT_ROOT / "data"
|
||||
|
||||
# ============================================================
|
||||
# DeepSeek API 配置(兼容 OpenAI 接口协议)
|
||||
# ============================================================
|
||||
DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
|
||||
DEEPSEEK_BASE_URL = "https://api.deepseek.com/v1"
|
||||
DEEPSEEK_MODEL = "deepseek-chat"
|
||||
|
||||
# 备用1:火山引擎方舟(豆包大模型 doubao-seed-2-1-pro-260628)
|
||||
VOLCENGINE_API_KEY = os.environ.get("VOLCENGINE_API_KEY", os.environ.get("ARK_API_KEY", "836d9bc0-80e8-4e45-90df-7287994d91ec"))
|
||||
VOLCENGINE_BASE_URL = os.environ.get("VOLCENGINE_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3")
|
||||
VOLCENGINE_MODEL = os.environ.get("VOLCENGINE_MODEL", "doubao-seed-2-0-lite-260428")
|
||||
|
||||
# 备用2:如果用户配置了其他兼容 OpenAI 协议的 API
|
||||
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
|
||||
OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1")
|
||||
OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
|
||||
|
||||
# ============================================================
|
||||
# LLM 调用参数
|
||||
# ============================================================
|
||||
LLM_TEMPERATURE = 0.1 # 低温度保证输出稳定
|
||||
LLM_MAX_TOKENS = 4096
|
||||
LLM_TIMEOUT = 60 # 超时秒数
|
||||
LLM_MAX_RETRIES = 2 # 最大重试次数
|
||||
|
||||
# ============================================================
|
||||
# 风险评估权重配置
|
||||
# ============================================================
|
||||
RISK_WEIGHTS = {
|
||||
"tech_disruption": 0.20, # 技术路线颠覆风险
|
||||
"talent_loss": 0.15, # 核心人员流失风险
|
||||
"algo_compliance": 0.15, # 算法/数据合规风险
|
||||
"geopolitical": 0.20, # 地缘政治/出口管制风险
|
||||
"rd_capitalization": 0.15, # 研发资本化操纵风险
|
||||
"concentration": 0.15, # 客户/供应商集中风险
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# 保险定价参数
|
||||
# ============================================================
|
||||
INSURANCE_PRODUCTS = {
|
||||
"ip_lawsuit": {
|
||||
"name": "知识产权被诉险",
|
||||
"base_premium": 50000, # 基础保费(元)
|
||||
"base_coverage": 5000000, # 基础保额(元)
|
||||
"description": "覆盖因知识产权纠纷(专利、商标、著作权)产生的诉讼费用及赔偿金",
|
||||
},
|
||||
"exec_departure": {
|
||||
"name": "高管离职业务中断险",
|
||||
"base_premium": 80000,
|
||||
"base_coverage": 10000000,
|
||||
"description": "覆盖核心技术人员/高管离职导致的业务中断损失",
|
||||
},
|
||||
"data_compliance": {
|
||||
"name": "数据合规行政处罚险",
|
||||
"base_premium": 30000,
|
||||
"base_coverage": 3000000,
|
||||
"description": "覆盖因非主观恶意的数据合规违规产生的行政罚款",
|
||||
},
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Streamlit 页面配置
|
||||
# ============================================================
|
||||
PAGE_TITLE = "🛡️ 科创企业智能风控与核保系统"
|
||||
PAGE_ICON = "🛡️"
|
||||
LAYOUT = "wide"
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{"filing_id": "AL-2023-001", "company": "金山办公", "algo_name": "WPS AI 写作助手", "filing_date": "2023-08-15", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2023-002", "company": "传音控股", "algo_name": "AI 相机美颜算法", "filing_date": "2023-06-20", "status": "已通过", "algo_type": "个性化推荐类"},
|
||||
{"filing_id": "AL-2023-003", "company": "海天瑞声", "algo_name": "智能标注平台算法", "filing_date": "2023-09-10", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2023-004", "company": "百度", "algo_name": "文心一言大模型", "filing_date": "2023-08-31", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2023-005", "company": "阿里巴巴", "algo_name": "通义千问大模型", "filing_date": "2023-09-13", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2023-006", "company": "腾讯", "algo_name": "混元大模型", "filing_date": "2023-09-15", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2024-001", "company": "字节跳动", "algo_name": "豆包大模型", "filing_date": "2024-01-15", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2024-002", "company": "商汤科技", "algo_name": "日日新大模型", "filing_date": "2024-02-20", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2024-003", "company": "科大讯飞", "algo_name": "星火认知大模型", "filing_date": "2024-03-01", "status": "已通过", "algo_type": "生成合成类"}
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{"entity_name": "中芯国际集成电路制造有限公司", "aliases": ["SMIC", "中芯国际", "Semiconductor Manufacturing International Corporation"], "date_added": "2020-12-18", "reason": "军事最终用途", "restrictions": "限制EUV光刻设备及先进制程相关技术出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "寒武纪科技股份有限公司", "aliases": ["Cambricon", "寒武纪"], "date_added": "2022-10-07", "reason": "支持中国军事现代化", "restrictions": "限制先进AI芯片代工与设计软件", "source": "BIS Entity List"},
|
||||
{"entity_name": "华为技术有限公司", "aliases": ["Huawei", "华为", "HUAWEI"], "date_added": "2019-05-16", "reason": "国家安全威胁", "restrictions": "全面限制美国技术出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "海康威视数字技术股份有限公司", "aliases": ["Hikvision", "海康威视"], "date_added": "2019-10-07", "reason": "参与新疆人权侵犯", "restrictions": "限制安防监控技术出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "大疆创新科技有限公司", "aliases": ["DJI", "大疆"], "date_added": "2020-12-18", "reason": "军事最终用途", "restrictions": "限制无人机核心部件出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "科大讯飞股份有限公司", "aliases": ["iFlytek", "科大讯飞"], "date_added": "2019-10-07", "reason": "参与新疆人权侵犯", "restrictions": "限制AI语音技术相关出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "商汤科技有限公司", "aliases": ["SenseTime", "商汤", "商汤科技"], "date_added": "2021-12-10", "reason": "参与监控技术开发", "restrictions": "限制AI视觉技术出口", "source": "BIS Entity List / OFAC SDN List"},
|
||||
{"entity_name": "旷视科技有限公司", "aliases": ["Megvii", "旷视"], "date_added": "2019-10-07", "reason": "参与新疆人权侵犯", "restrictions": "限制AI人脸识别技术出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "长江存储科技有限责任公司", "aliases": ["YMTC", "长江存储"], "date_added": "2022-12-15", "reason": "支持中国军事现代化", "restrictions": "限制NAND闪存芯片相关设备出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "上海微电子装备集团股份有限公司", "aliases": ["SMEE", "上海微电子"], "date_added": "2022-10-07", "reason": "支持中国军事现代化", "restrictions": "限制光刻机核心部件", "source": "BIS Entity List"},
|
||||
{"entity_name": "龙芯中科技术股份有限公司", "aliases": ["Loongson", "龙芯"], "date_added": "2023-02-10", "reason": "军事最终用途", "restrictions": "限制先进CPU设计工具出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "壁仞科技股份有限公司", "aliases": ["Biren Technology", "壁仞科技"], "date_added": "2022-10-07", "reason": "支持中国军事现代化", "restrictions": "限制GPU芯片代工", "source": "BIS Entity List"},
|
||||
{"entity_name": "摩尔线程智能科技股份有限公司", "aliases": ["Moore Threads", "摩尔线程"], "date_added": "2023-10-17", "reason": "支持中国军事现代化", "restrictions": "限制GPU芯片代工与设计", "source": "BIS Entity List"}
|
||||
]
|
||||
@@ -0,0 +1,408 @@
|
||||
[
|
||||
{
|
||||
"stock_code": "688981",
|
||||
"company_name": "中芯国际",
|
||||
"short_name": "中芯国际",
|
||||
"industry": "半导体制造",
|
||||
"sector": "芯片",
|
||||
"listed_board": "科创板",
|
||||
"description": "国内领先的集成电路晶圆代工企业,提供0.35微米到FinFET先进工艺",
|
||||
"core_tech_personnel": [
|
||||
{"name": "梁孟松", "title": "联合CEO/技术研发负责人", "status": "在职", "importance": "极高"},
|
||||
{"name": "周子学", "title": "董事长", "status": "在职", "importance": "高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 57756000000,
|
||||
"net_profit_2024": 3433000000,
|
||||
"rd_expense_2024": 5124000000,
|
||||
"rd_capitalization_rate": 0.0,
|
||||
"rd_revenue_ratio": 0.089,
|
||||
"top5_customer_ratio": 0.42,
|
||||
"top5_supplier_ratio": 0.55,
|
||||
"receivable_turnover": 8.2,
|
||||
"cash_flow_ratio": 1.35
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "14nm FinFET 量产, 7nm 研发中",
|
||||
"competing_techs": ["EUV 光刻技术 (ASML 垄断)", "GAA 晶体管架构"],
|
||||
"tech_moat": "国产替代核心标的,但受制于设备禁运",
|
||||
"patent_count": 12000
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "被列入(2020年12月)",
|
||||
"sanctions_detail": "被美国商务部列入实体清单,限制EUV光刻设备进口"
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["ASML(光刻机)", "东京电子(刻蚀设备)", "应用材料(薄膜沉积)"],
|
||||
"key_customers": ["高通", "联发科", "华为海思"],
|
||||
"supplier_concentration_risk": "极高——核心设备依赖海外供应商"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688111",
|
||||
"company_name": "金山办公软件股份有限公司",
|
||||
"short_name": "金山办公",
|
||||
"industry": "办公软件",
|
||||
"sector": "软件",
|
||||
"listed_board": "科创板",
|
||||
"description": "国产办公软件龙头,WPS Office 全球月活超5.9亿",
|
||||
"core_tech_personnel": [
|
||||
{"name": "章庆元", "title": "CEO", "status": "在职", "importance": "极高"},
|
||||
{"name": "姚冬", "title": "CTO/AI研发负责人", "status": "在职", "importance": "高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 4586000000,
|
||||
"net_profit_2024": 1231000000,
|
||||
"rd_expense_2024": 1520000000,
|
||||
"rd_capitalization_rate": 0.0,
|
||||
"rd_revenue_ratio": 0.331,
|
||||
"top5_customer_ratio": 0.15,
|
||||
"top5_supplier_ratio": 0.30,
|
||||
"receivable_turnover": 12.5,
|
||||
"cash_flow_ratio": 1.82
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "WPS AI 大模型集成、云文档协同",
|
||||
"competing_techs": ["Microsoft 365 Copilot", "Google Workspace Gemini"],
|
||||
"tech_moat": "信创替代核心标的,政府及国企客户粘性高",
|
||||
"patent_count": 3500
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "已备案(WPS AI 写作助手)",
|
||||
"data_export_risk": "中(海外版WPS涉及数据跨境)",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["华为云", "阿里云", "英伟达(GPU)"],
|
||||
"key_customers": ["各级政府机关", "央企国企", "中小企业"],
|
||||
"supplier_concentration_risk": "中——GPU算力依赖进口"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688139",
|
||||
"company_name": "海尔生物医疗股份有限公司",
|
||||
"short_name": "海尔生物",
|
||||
"industry": "生物医疗低温存储",
|
||||
"sector": "医疗器械",
|
||||
"listed_board": "科创板",
|
||||
"description": "全球领先的生物医疗低温存储解决方案提供商",
|
||||
"core_tech_personnel": [
|
||||
{"name": "刘占杰", "title": "董事长/总经理", "status": "在职", "importance": "极高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 2245000000,
|
||||
"net_profit_2024": 398000000,
|
||||
"rd_expense_2024": 289000000,
|
||||
"rd_capitalization_rate": 0.05,
|
||||
"rd_revenue_ratio": 0.129,
|
||||
"top5_customer_ratio": 0.22,
|
||||
"top5_supplier_ratio": 0.35,
|
||||
"receivable_turnover": 6.8,
|
||||
"cash_flow_ratio": 1.15
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "超低温冰箱(-196°C)、自动化样本库、物联网疫苗管理",
|
||||
"competing_techs": ["赛默飞世尔", "松下医疗"],
|
||||
"tech_moat": "国内市场份额第一,物联网技术差异化",
|
||||
"patent_count": 800
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["压缩机供应商", "电子元器件供应商"],
|
||||
"key_customers": ["各级疾控中心", "医院", "科研院所"],
|
||||
"supplier_concentration_risk": "低"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688256",
|
||||
"company_name": "寒武纪科技股份有限公司",
|
||||
"short_name": "寒武纪",
|
||||
"industry": "AI芯片",
|
||||
"sector": "芯片",
|
||||
"listed_board": "科创板",
|
||||
"description": "国内AI芯片独角兽,智能计算处理器IP及芯片产品提供商",
|
||||
"core_tech_personnel": [
|
||||
{"name": "陈天石", "title": "创始人/董事长/CEO", "status": "在职", "importance": "极高"},
|
||||
{"name": "陈云霁", "title": "首席科学家(兄长)", "status": "中科院任职,兼职顾问", "importance": "高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 1170000000,
|
||||
"net_profit_2024": -825000000,
|
||||
"rd_expense_2024": 1510000000,
|
||||
"rd_capitalization_rate": 0.0,
|
||||
"rd_revenue_ratio": 1.29,
|
||||
"top5_customer_ratio": 0.85,
|
||||
"top5_supplier_ratio": 0.70,
|
||||
"receivable_turnover": 3.2,
|
||||
"cash_flow_ratio": 0.45
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "思元系列AI推理/训练芯片, MLU架构",
|
||||
"competing_techs": ["英伟达CUDA生态", "华为昇腾", "AMD Instinct"],
|
||||
"tech_moat": "自主IP指令集,但生态建设与英伟达差距大",
|
||||
"patent_count": 2800
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用(硬件厂商)",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "被列入(2022年10月)",
|
||||
"sanctions_detail": "被美国商务部列入实体清单,限制先进制程芯片代工"
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["台积电(已受限)", "中芯国际(替代)", "日月光(封测)"],
|
||||
"key_customers": ["中国移动", "南京市政府智慧城市项目", "某互联网大厂"],
|
||||
"supplier_concentration_risk": "极高——先进制程代工受限"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688029",
|
||||
"company_name": "南微医学科技股份有限公司",
|
||||
"short_name": "南微医学",
|
||||
"industry": "微创医疗器械",
|
||||
"sector": "医疗器械",
|
||||
"listed_board": "科创板",
|
||||
"description": "全球领先的微创诊疗器械企业,内镜下诊疗器械龙头",
|
||||
"core_tech_personnel": [
|
||||
{"name": "隆晓辉", "title": "创始人/董事长", "status": "在职", "importance": "极高"},
|
||||
{"name": "冷德嵘", "title": "总经理", "status": "在职", "importance": "高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 2580000000,
|
||||
"net_profit_2024": 628000000,
|
||||
"rd_expense_2024": 365000000,
|
||||
"rd_capitalization_rate": 0.12,
|
||||
"rd_revenue_ratio": 0.141,
|
||||
"top5_customer_ratio": 0.18,
|
||||
"top5_supplier_ratio": 0.32,
|
||||
"receivable_turnover": 7.5,
|
||||
"cash_flow_ratio": 1.42
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "一次性内镜、电外科手术器械、AI辅助诊断",
|
||||
"competing_techs": ["波士顿科学", "奥林巴斯", "库克医疗"],
|
||||
"tech_moat": "国产微创器械龙头,海外收入占比超40%",
|
||||
"patent_count": 1200
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["不锈钢/钛合金供应商", "精密注塑件供应商"],
|
||||
"key_customers": ["全球三甲医院", "海外经销商网络"],
|
||||
"supplier_concentration_risk": "低"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688126",
|
||||
"company_name": "沪硅产业集团股份有限公司",
|
||||
"short_name": "沪硅产业",
|
||||
"industry": "半导体硅片",
|
||||
"sector": "芯片",
|
||||
"listed_board": "科创板",
|
||||
"description": "国内规模最大的半导体硅片制造企业,300mm大硅片龙头",
|
||||
"core_tech_personnel": [
|
||||
{"name": "林林", "title": "总裁", "status": "在职", "importance": "极高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 3285000000,
|
||||
"net_profit_2024": -156000000,
|
||||
"rd_expense_2024": 498000000,
|
||||
"rd_capitalization_rate": 0.35,
|
||||
"rd_revenue_ratio": 0.152,
|
||||
"top5_customer_ratio": 0.62,
|
||||
"top5_supplier_ratio": 0.48,
|
||||
"receivable_turnover": 5.1,
|
||||
"cash_flow_ratio": 0.78
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "300mm半导体硅片(12英寸)量产",
|
||||
"competing_techs": ["日本信越化学", "日本SUMCO", "韩国SK Siltron"],
|
||||
"tech_moat": "国产替代第一梯队,但高端产品仍有差距",
|
||||
"patent_count": 650
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["多晶硅供应商", "石英坩埚供应商"],
|
||||
"key_customers": ["中芯国际", "华虹半导体", "长江存储"],
|
||||
"supplier_concentration_risk": "中"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688223",
|
||||
"company_name": "晶科能源股份有限公司",
|
||||
"short_name": "晶科能源",
|
||||
"industry": "光伏组件",
|
||||
"sector": "新能源",
|
||||
"listed_board": "科创板",
|
||||
"description": "全球领先的光伏组件制造商,N型TOPCon技术全球领先",
|
||||
"core_tech_personnel": [
|
||||
{"name": "李仙德", "title": "创始人/董事长", "status": "在职", "importance": "极高"},
|
||||
{"name": "金浩", "title": "首席科学家", "status": "在职", "importance": "高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 84300000000,
|
||||
"net_profit_2024": 1050000000,
|
||||
"rd_expense_2024": 4890000000,
|
||||
"rd_capitalization_rate": 0.08,
|
||||
"rd_revenue_ratio": 0.058,
|
||||
"top5_customer_ratio": 0.25,
|
||||
"top5_supplier_ratio": 0.40,
|
||||
"receivable_turnover": 6.2,
|
||||
"cash_flow_ratio": 0.92
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "N型TOPCon高效电池, 钙钛矿叠层研发",
|
||||
"competing_techs": ["隆基HJT路线", "钙钛矿技术", "IBC电池"],
|
||||
"tech_moat": "TOPCon量产规模全球第一,但面临技术路线之争",
|
||||
"patent_count": 2200
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "未列入(但面临美国反倾销关税)",
|
||||
"sanctions_detail": "面临美国、欧盟反倾销/反补贴调查"
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["硅料供应商(通威/大全)", "银浆供应商", "EVA胶膜供应商"],
|
||||
"key_customers": ["全球EPC总包商", "欧美分布式市场", "中东/非洲新兴市场"],
|
||||
"supplier_concentration_risk": "中——硅料价格波动大"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688036",
|
||||
"company_name": "传音控股股份有限公司",
|
||||
"short_name": "传音控股",
|
||||
"industry": "智能终端",
|
||||
"sector": "消费电子",
|
||||
"listed_board": "科创板",
|
||||
"description": "非洲市场手机销量第一,旗下TECNO/Infinix/itel品牌",
|
||||
"core_tech_personnel": [
|
||||
{"name": "竺兆江", "title": "创始人/董事长", "status": "在职", "importance": "极高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 65800000000,
|
||||
"net_profit_2024": 5620000000,
|
||||
"rd_expense_2024": 3250000000,
|
||||
"rd_capitalization_rate": 0.0,
|
||||
"rd_revenue_ratio": 0.049,
|
||||
"top5_customer_ratio": 0.32,
|
||||
"top5_supplier_ratio": 0.58,
|
||||
"receivable_turnover": 9.8,
|
||||
"cash_flow_ratio": 1.25
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "深肤色相机算法, AI翻译, 本地化OS",
|
||||
"competing_techs": ["三星", "小米", "OPPO"],
|
||||
"tech_moat": "新兴市场本地化经验深厚",
|
||||
"patent_count": 4500
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "已备案(AI相机美颜算法)",
|
||||
"data_export_risk": "高(大量非洲/东南亚用户数据跨境)",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["联发科(芯片)", "三星SDI/ATL(电池)", "京东方(屏幕)"],
|
||||
"key_customers": ["非洲运营商", "东南亚分销商", "拉美零售商"],
|
||||
"supplier_concentration_risk": "中——芯片依赖联发科"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688005",
|
||||
"company_name": "容百科技股份有限公司",
|
||||
"short_name": "容百科技",
|
||||
"industry": "锂电正极材料",
|
||||
"sector": "新能源",
|
||||
"listed_board": "科创板",
|
||||
"description": "全球领先的锂电池正极材料供应商,高镍三元材料龙头",
|
||||
"core_tech_personnel": [
|
||||
{"name": "白厚善", "title": "创始人/董事长", "status": "在职", "importance": "极高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 18900000000,
|
||||
"net_profit_2024": 356000000,
|
||||
"rd_expense_2024": 785000000,
|
||||
"rd_capitalization_rate": 0.18,
|
||||
"rd_revenue_ratio": 0.042,
|
||||
"top5_customer_ratio": 0.78,
|
||||
"top5_supplier_ratio": 0.65,
|
||||
"receivable_turnover": 4.5,
|
||||
"cash_flow_ratio": 0.68
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "超高镍三元正极材料(Ni>90%), 固态电池正极",
|
||||
"competing_techs": ["磷酸铁锂(比亚迪路线)", "钠离子电池", "固态电解质"],
|
||||
"tech_moat": "高镍三元出货量全球前三",
|
||||
"patent_count": 450
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["格林美(镍钴原料)", "华友钴业"],
|
||||
"key_customers": ["宁德时代", "三星SDI", "SK On"],
|
||||
"supplier_concentration_risk": "高——原材料价格波动大"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688787",
|
||||
"company_name": "海天瑞声科技股份有限公司",
|
||||
"short_name": "海天瑞声",
|
||||
"industry": "AI训练数据",
|
||||
"sector": "AI",
|
||||
"listed_board": "科创板",
|
||||
"description": "国内领先的AI训练数据解决方案提供商",
|
||||
"core_tech_personnel": [
|
||||
{"name": "贺琳", "title": "创始人/董事长", "status": "在职", "importance": "极高"},
|
||||
{"name": "郭蕾", "title": "副总经理/技术负责人", "status": "离职(2024年8月)", "importance": "高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 356000000,
|
||||
"net_profit_2024": -48000000,
|
||||
"rd_expense_2024": 102000000,
|
||||
"rd_capitalization_rate": 0.42,
|
||||
"rd_revenue_ratio": 0.287,
|
||||
"top5_customer_ratio": 0.88,
|
||||
"top5_supplier_ratio": 0.35,
|
||||
"receivable_turnover": 2.8,
|
||||
"cash_flow_ratio": 0.35
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "多语种语音数据集, 自动驾驶标注数据, AIGC合成数据",
|
||||
"competing_techs": ["Scale AI", "Appen", "AI合成数据替代人工标注"],
|
||||
"tech_moat": "多语种覆盖广泛,但面临AIGC合成数据的颠覆性威胁",
|
||||
"patent_count": 180
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "已备案(智能标注平台算法)",
|
||||
"data_export_risk": "高(大量跨境数据采集与交付)",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["全球数据采集众包网络"],
|
||||
"key_customers": ["字节跳动", "百度", "某大型车企"],
|
||||
"supplier_concentration_risk": "中"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"supply_relations": [
|
||||
{"from": "ASML", "to": "中芯国际", "relation": "供应光刻设备", "critical": true, "status": "受限"},
|
||||
{"from": "东京电子", "to": "中芯国际", "relation": "供应刻蚀设备", "critical": true, "status": "受限"},
|
||||
{"from": "应用材料", "to": "中芯国际", "relation": "供应薄膜沉积设备", "critical": true, "status": "受限"},
|
||||
{"from": "中芯国际", "to": "寒武纪", "relation": "芯片代工", "critical": true, "status": "正常"},
|
||||
{"from": "中芯国际", "to": "华为海思", "relation": "芯片代工", "critical": true, "status": "受限"},
|
||||
{"from": "台积电", "to": "寒武纪", "relation": "先进制程代工", "critical": true, "status": "受限"},
|
||||
{"from": "英伟达", "to": "金山办公", "relation": "供应GPU算力", "critical": true, "status": "受限"},
|
||||
{"from": "英伟达", "to": "海天瑞声", "relation": "供应GPU算力(客户需求)", "critical": false, "status": "正常"},
|
||||
{"from": "联发科", "to": "传音控股", "relation": "供应手机芯片", "critical": true, "status": "正常"},
|
||||
{"from": "三星SDI", "to": "传音控股", "relation": "供应电池", "critical": false, "status": "正常"},
|
||||
{"from": "京东方", "to": "传音控股", "relation": "供应屏幕", "critical": false, "status": "正常"},
|
||||
{"from": "沪硅产业", "to": "中芯国际", "relation": "供应半导体硅片", "critical": true, "status": "正常"},
|
||||
{"from": "沪硅产业", "to": "华虹半导体", "relation": "供应半导体硅片", "critical": true, "status": "正常"},
|
||||
{"from": "沪硅产业", "to": "长江存储", "relation": "供应半导体硅片", "critical": true, "status": "正常"},
|
||||
{"from": "格林美", "to": "容百科技", "relation": "供应镍钴原料", "critical": true, "status": "正常"},
|
||||
{"from": "华友钴业", "to": "容百科技", "relation": "供应钴原料", "critical": true, "status": "正常"},
|
||||
{"from": "容百科技", "to": "宁德时代", "relation": "供应正极材料", "critical": true, "status": "正常"},
|
||||
{"from": "容百科技", "to": "三星SDI", "relation": "供应正极材料", "critical": false, "status": "正常"},
|
||||
{"from": "通威股份", "to": "晶科能源", "relation": "供应硅料", "critical": true, "status": "正常"},
|
||||
{"from": "大全能源", "to": "晶科能源", "relation": "供应硅料", "critical": true, "status": "正常"},
|
||||
{"from": "海天瑞声", "to": "字节跳动", "relation": "供应AI训练数据", "critical": false, "status": "正常"},
|
||||
{"from": "海天瑞声", "to": "百度", "relation": "供应AI训练数据", "critical": false, "status": "正常"}
|
||||
],
|
||||
"investment_relations": [
|
||||
{"from": "国家集成电路产业基金", "to": "中芯国际", "relation": "战略投资", "share_ratio": 0.15},
|
||||
{"from": "国家集成电路产业基金", "to": "沪硅产业", "relation": "战略投资", "share_ratio": 0.08},
|
||||
{"from": "国家集成电路产业基金", "to": "长江存储", "relation": "战略投资", "share_ratio": 0.20}
|
||||
],
|
||||
"personnel_relations": [
|
||||
{"person": "梁孟松", "from_company": "台积电/三星", "to_company": "中芯国际", "relation": "技术负责人跳槽", "year": 2017},
|
||||
{"person": "郭蕾", "from_company": "海天瑞声", "to_company": "未知", "relation": "核心技术人员离职", "year": 2024}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""知识图谱模块"""
|
||||
@@ -0,0 +1,169 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
供应链风险传染分析器
|
||||
基于 BFS 遍历实现风险穿透,计算传染距离和影响权重
|
||||
"""
|
||||
import logging
|
||||
from collections import deque
|
||||
from typing import Optional
|
||||
|
||||
import networkx as nx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def analyze_contagion(G: nx.DiGraph, target_company: str) -> dict:
|
||||
"""
|
||||
分析指定企业的供应链风险传染情况
|
||||
从上游(供应商)方向进行 BFS 穿透
|
||||
"""
|
||||
if target_company not in G:
|
||||
return {"error": f"企业 '{target_company}' 不在图谱中"}
|
||||
|
||||
result = {
|
||||
"company": target_company,
|
||||
"direct_risks": [], # 直接风险(一度关联)
|
||||
"indirect_risks": [], # 间接风险(二度及以上关联)
|
||||
"contagion_paths": [], # 风险传染路径
|
||||
"risk_score": 0, # 供应链风险总分
|
||||
"critical_nodes": [], # 关键断裂节点
|
||||
}
|
||||
|
||||
# BFS 从目标企业向上游遍历
|
||||
visited = set()
|
||||
queue = deque([(target_company, 0, [target_company])])
|
||||
visited.add(target_company)
|
||||
|
||||
while queue:
|
||||
current, depth, path = queue.popleft()
|
||||
|
||||
# 检查当前节点的上游(前驱节点)
|
||||
for predecessor in G.predecessors(current):
|
||||
if predecessor in visited:
|
||||
continue
|
||||
visited.add(predecessor)
|
||||
|
||||
edge_data = G.edges[predecessor, current]
|
||||
node_data = G.nodes.get(predecessor, {})
|
||||
new_path = [predecessor] + path
|
||||
|
||||
# 检查上游节点是否受制裁
|
||||
if node_data.get("is_sanctioned", False):
|
||||
risk_entry = {
|
||||
"entity": predecessor,
|
||||
"node_type": node_data.get("node_type", "未知"),
|
||||
"distance": depth + 1,
|
||||
"relation": edge_data.get("relation", ""),
|
||||
"is_critical": edge_data.get("critical", False),
|
||||
"status": edge_data.get("status", "正常"),
|
||||
"path": " → ".join(new_path),
|
||||
}
|
||||
|
||||
if depth == 0:
|
||||
result["direct_risks"].append(risk_entry)
|
||||
else:
|
||||
result["indirect_risks"].append(risk_entry)
|
||||
|
||||
result["contagion_paths"].append({
|
||||
"path": new_path,
|
||||
"path_str": " → ".join(new_path),
|
||||
"length": len(new_path),
|
||||
"severity": "高" if edge_data.get("critical", False) else "中",
|
||||
})
|
||||
|
||||
# 检查受限状态的边
|
||||
if edge_data.get("status") == "受限":
|
||||
if predecessor not in [r["entity"] for r in result["direct_risks"] + result["indirect_risks"]]:
|
||||
risk_entry = {
|
||||
"entity": predecessor,
|
||||
"node_type": node_data.get("node_type", "未知"),
|
||||
"distance": depth + 1,
|
||||
"relation": edge_data.get("relation", ""),
|
||||
"is_critical": edge_data.get("critical", False),
|
||||
"status": "受限",
|
||||
"path": " → ".join(new_path),
|
||||
}
|
||||
if depth == 0:
|
||||
result["direct_risks"].append(risk_entry)
|
||||
else:
|
||||
result["indirect_risks"].append(risk_entry)
|
||||
|
||||
# 继续向上游遍历(最多3层)
|
||||
if depth < 2:
|
||||
queue.append((predecessor, depth + 1, new_path))
|
||||
|
||||
# 计算供应链风险得分
|
||||
result["risk_score"] = _calculate_supply_chain_risk_score(result)
|
||||
|
||||
# 识别关键断裂节点
|
||||
result["critical_nodes"] = _find_critical_nodes(G, target_company)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _calculate_supply_chain_risk_score(contagion_result: dict) -> float:
|
||||
"""计算供应链风险综合得分 (0-100)"""
|
||||
score = 0
|
||||
|
||||
# 直接风险:每个 +25 分,关键供应 +35 分
|
||||
for risk in contagion_result["direct_risks"]:
|
||||
if risk["is_critical"]:
|
||||
score += 35
|
||||
else:
|
||||
score += 25
|
||||
|
||||
# 间接风险:每个 +10 分,关键供应 +15 分
|
||||
for risk in contagion_result["indirect_risks"]:
|
||||
if risk["is_critical"]:
|
||||
score += 15
|
||||
else:
|
||||
score += 10
|
||||
|
||||
return min(score, 100) # 上限 100
|
||||
|
||||
|
||||
def _find_critical_nodes(G: nx.DiGraph, target: str) -> list:
|
||||
"""
|
||||
识别关键断裂节点:如果移除该节点,目标企业将失去关键供应来源
|
||||
"""
|
||||
critical = []
|
||||
predecessors = list(G.predecessors(target))
|
||||
|
||||
for pred in predecessors:
|
||||
edge_data = G.edges[pred, target]
|
||||
if edge_data.get("critical", False):
|
||||
# 检查是否有替代供应商
|
||||
alternatives = sum(
|
||||
1 for p in predecessors
|
||||
if p != pred and G.edges[p, target].get("relation", "") == edge_data.get("relation", "")
|
||||
)
|
||||
critical.append({
|
||||
"node": pred,
|
||||
"relation": edge_data.get("relation", ""),
|
||||
"has_alternative": alternatives > 0,
|
||||
"alternative_count": alternatives,
|
||||
"status": edge_data.get("status", "正常"),
|
||||
})
|
||||
|
||||
return critical
|
||||
|
||||
|
||||
def find_all_risk_paths(G: nx.DiGraph, source: str, target: str, max_depth: int = 4) -> list:
|
||||
"""
|
||||
查找两个节点之间的所有风险路径
|
||||
"""
|
||||
if source not in G or target not in G:
|
||||
return []
|
||||
|
||||
try:
|
||||
paths = list(nx.all_simple_paths(G, source, target, cutoff=max_depth))
|
||||
return [
|
||||
{
|
||||
"path": p,
|
||||
"path_str": " → ".join(p),
|
||||
"length": len(p),
|
||||
}
|
||||
for p in paths
|
||||
]
|
||||
except nx.NetworkXError:
|
||||
return []
|
||||
@@ -0,0 +1,214 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
知识图谱构建引擎
|
||||
基于 NetworkX 构建科创企业供应链风险传染图谱
|
||||
节点类型:企业、供应商、客户、核心人员、制裁实体
|
||||
边类型:供应关系、客户关系、任职关系、投资关系
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import networkx as nx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||
|
||||
|
||||
def build_graph() -> nx.DiGraph:
|
||||
"""
|
||||
构建完整的科创企业供应链风险知识图谱
|
||||
"""
|
||||
G = nx.DiGraph()
|
||||
|
||||
# 1. 加载企业数据,添加企业节点
|
||||
_add_company_nodes(G)
|
||||
|
||||
# 2. 加载供应链数据,添加关系边
|
||||
_add_supply_chain_edges(G)
|
||||
|
||||
# 3. 加载实体清单,标记受制裁节点
|
||||
_mark_sanctioned_nodes(G)
|
||||
|
||||
logger.info(f"图谱构建完成: {G.number_of_nodes()} 节点, {G.number_of_edges()} 边")
|
||||
return G
|
||||
|
||||
|
||||
def _add_company_nodes(G: nx.DiGraph):
|
||||
"""添加科创板企业节点及其关联的核心人员节点"""
|
||||
filepath = DATA_DIR / "sample_companies.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
companies = json.load(f)
|
||||
|
||||
for company in companies:
|
||||
name = company["short_name"]
|
||||
G.add_node(
|
||||
name,
|
||||
node_type="科创企业",
|
||||
stock_code=company["stock_code"],
|
||||
industry=company["industry"],
|
||||
sector=company["sector"],
|
||||
is_sanctioned=company["compliance"]["entity_list_status"] != "未列入",
|
||||
risk_level="正常",
|
||||
color="#4CAF50", # 默认绿色
|
||||
)
|
||||
|
||||
# 添加核心技术人员节点
|
||||
for person in company.get("core_tech_personnel", []):
|
||||
person_id = f"{person['name']}@{name}"
|
||||
G.add_node(
|
||||
person_id,
|
||||
node_type="核心人员",
|
||||
real_name=person["name"],
|
||||
title=person["title"],
|
||||
status=person["status"],
|
||||
importance=person["importance"],
|
||||
company=name,
|
||||
color="#2196F3", # 蓝色
|
||||
)
|
||||
G.add_edge(
|
||||
person_id, name,
|
||||
relation="任职于",
|
||||
edge_type="personnel",
|
||||
)
|
||||
|
||||
# 添加供应商节点
|
||||
for supplier in company.get("supply_chain", {}).get("key_suppliers", []):
|
||||
supplier_name = supplier.split("(")[0].split("(")[0].strip()
|
||||
if not G.has_node(supplier_name):
|
||||
G.add_node(
|
||||
supplier_name,
|
||||
node_type="供应商",
|
||||
is_sanctioned=False,
|
||||
risk_level="正常",
|
||||
color="#FF9800", # 橙色
|
||||
)
|
||||
G.add_edge(
|
||||
supplier_name, name,
|
||||
relation="供应",
|
||||
detail=supplier,
|
||||
edge_type="supply",
|
||||
)
|
||||
|
||||
# 添加客户节点
|
||||
for customer in company.get("supply_chain", {}).get("key_customers", []):
|
||||
customer_name = customer.split("(")[0].split("(")[0].strip()
|
||||
if not G.has_node(customer_name):
|
||||
G.add_node(
|
||||
customer_name,
|
||||
node_type="客户",
|
||||
is_sanctioned=False,
|
||||
risk_level="正常",
|
||||
color="#9C27B0", # 紫色
|
||||
)
|
||||
G.add_edge(
|
||||
name, customer_name,
|
||||
relation="供货给",
|
||||
edge_type="customer",
|
||||
)
|
||||
|
||||
|
||||
def _add_supply_chain_edges(G: nx.DiGraph):
|
||||
"""从供应链关系文件添加更详细的边"""
|
||||
filepath = DATA_DIR / "supply_chain.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# 添加供应关系
|
||||
for rel in data.get("supply_relations", []):
|
||||
from_node = rel["from"]
|
||||
to_node = rel["to"]
|
||||
|
||||
# 确保节点存在
|
||||
if not G.has_node(from_node):
|
||||
G.add_node(from_node, node_type="供应商", is_sanctioned=False,
|
||||
risk_level="正常", color="#FF9800")
|
||||
if not G.has_node(to_node):
|
||||
G.add_node(to_node, node_type="企业", is_sanctioned=False,
|
||||
risk_level="正常", color="#4CAF50")
|
||||
|
||||
G.add_edge(
|
||||
from_node, to_node,
|
||||
relation=rel["relation"],
|
||||
critical=rel.get("critical", False),
|
||||
status=rel.get("status", "正常"),
|
||||
edge_type="supply",
|
||||
)
|
||||
|
||||
# 添加投资关系
|
||||
for rel in data.get("investment_relations", []):
|
||||
from_node = rel["from"]
|
||||
to_node = rel["to"]
|
||||
|
||||
if not G.has_node(from_node):
|
||||
G.add_node(from_node, node_type="投资方", is_sanctioned=False,
|
||||
risk_level="正常", color="#607D8B")
|
||||
|
||||
G.add_edge(
|
||||
from_node, to_node,
|
||||
relation=rel["relation"],
|
||||
share_ratio=rel.get("share_ratio", 0),
|
||||
edge_type="investment",
|
||||
)
|
||||
|
||||
|
||||
def _mark_sanctioned_nodes(G: nx.DiGraph):
|
||||
"""标记受制裁的节点,并向下游传播风险"""
|
||||
filepath = DATA_DIR / "entity_list.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
entities = json.load(f)
|
||||
|
||||
# 收集所有受制裁实体的名称和别名
|
||||
sanctioned_names = set()
|
||||
for entity in entities:
|
||||
sanctioned_names.add(entity["entity_name"])
|
||||
for alias in entity.get("aliases", []):
|
||||
sanctioned_names.add(alias)
|
||||
|
||||
# 标记图谱中的受制裁节点
|
||||
for node in G.nodes():
|
||||
for sname in sanctioned_names:
|
||||
if node in sname or sname in node:
|
||||
G.nodes[node]["is_sanctioned"] = True
|
||||
G.nodes[node]["risk_level"] = "高危"
|
||||
G.nodes[node]["color"] = "#F44336" # 红色
|
||||
break
|
||||
|
||||
|
||||
def get_node_info(G: nx.DiGraph, node_name: str) -> dict:
|
||||
"""获取节点详细信息"""
|
||||
if node_name not in G:
|
||||
return {"error": f"节点 '{node_name}' 不存在"}
|
||||
|
||||
node_data = dict(G.nodes[node_name])
|
||||
predecessors = list(G.predecessors(node_name))
|
||||
successors = list(G.successors(node_name))
|
||||
|
||||
return {
|
||||
"name": node_name,
|
||||
"attributes": node_data,
|
||||
"upstream": predecessors,
|
||||
"downstream": successors,
|
||||
"degree": G.degree(node_name),
|
||||
}
|
||||
|
||||
|
||||
def get_graph_stats(G: nx.DiGraph) -> dict:
|
||||
"""获取图谱统计信息"""
|
||||
node_types = {}
|
||||
for _, data in G.nodes(data=True):
|
||||
t = data.get("node_type", "未知")
|
||||
node_types[t] = node_types.get(t, 0) + 1
|
||||
|
||||
sanctioned_count = sum(
|
||||
1 for _, data in G.nodes(data=True) if data.get("is_sanctioned", False)
|
||||
)
|
||||
|
||||
return {
|
||||
"total_nodes": G.number_of_nodes(),
|
||||
"total_edges": G.number_of_edges(),
|
||||
"node_types": node_types,
|
||||
"sanctioned_nodes": sanctioned_count,
|
||||
"density": nx.density(G),
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
知识图谱可视化模块
|
||||
使用 pyvis 生成交互式网络图,支持嵌入 Streamlit
|
||||
"""
|
||||
import logging
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import networkx as nx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 节点类型对应的颜色和形状
|
||||
NODE_STYLES = {
|
||||
"科创企业": {"color": "#4CAF50", "shape": "dot", "size": 30},
|
||||
"供应商": {"color": "#FF9800", "shape": "diamond", "size": 20},
|
||||
"客户": {"color": "#9C27B0", "shape": "triangle", "size": 20},
|
||||
"核心人员": {"color": "#2196F3", "shape": "star", "size": 15},
|
||||
"投资方": {"color": "#607D8B", "shape": "square", "size": 20},
|
||||
"企业": {"color": "#4CAF50", "shape": "dot", "size": 25},
|
||||
}
|
||||
|
||||
# 受制裁节点的样式覆盖
|
||||
SANCTIONED_STYLE = {"color": "#F44336", "size": 35}
|
||||
# 受限边的样式
|
||||
RESTRICTED_EDGE_STYLE = {"color": "#F44336", "dashes": True, "width": 3}
|
||||
|
||||
|
||||
def generate_interactive_graph(
|
||||
G: nx.DiGraph,
|
||||
highlight_company: Optional[str] = None,
|
||||
output_path: Optional[str] = None,
|
||||
height: str = "600px",
|
||||
width: str = "100%",
|
||||
) -> str:
|
||||
"""
|
||||
生成交互式知识图谱 HTML
|
||||
"""
|
||||
try:
|
||||
from pyvis.network import Network
|
||||
except ImportError:
|
||||
logger.error("pyvis 未安装,请运行: pip install pyvis")
|
||||
return _generate_fallback_html(G)
|
||||
|
||||
net = Network(
|
||||
height=height,
|
||||
width=width,
|
||||
directed=True,
|
||||
notebook=False,
|
||||
bgcolor="#1a1a2e",
|
||||
font_color="white",
|
||||
)
|
||||
|
||||
# 物理引擎配置
|
||||
net.set_options("""
|
||||
{
|
||||
"physics": {
|
||||
"forceAtlas2Based": {
|
||||
"gravitationalConstant": -50,
|
||||
"centralGravity": 0.01,
|
||||
"springLength": 150,
|
||||
"springConstant": 0.08
|
||||
},
|
||||
"solver": "forceAtlas2Based",
|
||||
"stabilization": {"iterations": 100}
|
||||
},
|
||||
"interaction": {
|
||||
"hover": true,
|
||||
"tooltipDelay": 200,
|
||||
"navigationButtons": true
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
# 添加节点
|
||||
for node, data in G.nodes(data=True):
|
||||
node_type = data.get("node_type", "企业")
|
||||
style = NODE_STYLES.get(node_type, NODE_STYLES["企业"]).copy()
|
||||
|
||||
# 受制裁节点特殊样式
|
||||
if data.get("is_sanctioned", False):
|
||||
style.update(SANCTIONED_STYLE)
|
||||
|
||||
# 高亮选中企业
|
||||
if highlight_company and node == highlight_company:
|
||||
style["color"] = "#FFD700" # 金色
|
||||
style["size"] = 45
|
||||
style["borderWidth"] = 3
|
||||
|
||||
# 构建标签和悬浮提示
|
||||
label = node.split("@")[0] if "@" in node else node
|
||||
title_parts = [f"<b>{label}</b>", f"类型: {node_type}"]
|
||||
if data.get("is_sanctioned"):
|
||||
title_parts.append("⚠️ <b>已被制裁</b>")
|
||||
if data.get("stock_code"):
|
||||
title_parts.append(f"代码: {data['stock_code']}")
|
||||
if data.get("industry"):
|
||||
title_parts.append(f"行业: {data['industry']}")
|
||||
if data.get("title"):
|
||||
title_parts.append(f"职位: {data['title']}")
|
||||
if data.get("status"):
|
||||
title_parts.append(f"状态: {data['status']}")
|
||||
|
||||
net.add_node(
|
||||
node,
|
||||
label=label,
|
||||
title="<br>".join(title_parts),
|
||||
color=style["color"],
|
||||
shape=style["shape"],
|
||||
size=style["size"],
|
||||
)
|
||||
|
||||
# 添加边
|
||||
for u, v, data in G.edges(data=True):
|
||||
edge_style = {
|
||||
"color": "#666666",
|
||||
"width": 1,
|
||||
"arrows": "to",
|
||||
}
|
||||
|
||||
# 受限边特殊样式
|
||||
if data.get("status") == "受限":
|
||||
edge_style.update(RESTRICTED_EDGE_STYLE)
|
||||
elif data.get("critical", False):
|
||||
edge_style["width"] = 2
|
||||
edge_style["color"] = "#FFC107" # 关键边用黄色
|
||||
|
||||
relation = data.get("relation", "")
|
||||
net.add_edge(
|
||||
u, v,
|
||||
title=relation,
|
||||
label=relation if len(relation) <= 8 else "",
|
||||
**edge_style,
|
||||
)
|
||||
|
||||
# 输出 HTML
|
||||
if output_path is None:
|
||||
output_path = str(Path(tempfile.gettempdir()) / "kg_visualization.html")
|
||||
|
||||
net.save_graph(output_path)
|
||||
|
||||
# 读取 HTML 内容
|
||||
with open(output_path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def _generate_fallback_html(G: nx.DiGraph) -> str:
|
||||
"""当 pyvis 不可用时的备用 HTML 可视化"""
|
||||
nodes_info = []
|
||||
for node, data in G.nodes(data=True):
|
||||
label = node.split("@")[0] if "@" in node else node
|
||||
is_sanctioned = data.get("is_sanctioned", False)
|
||||
nodes_info.append(f"<li style='color: {'red' if is_sanctioned else 'green'}'>{label} ({data.get('node_type', '未知')})</li>")
|
||||
|
||||
return f"""
|
||||
<html><body style='background: #1a1a2e; color: white; padding: 20px;'>
|
||||
<h2>📊 知识图谱节点列表(pyvis 未安装,使用简化视图)</h2>
|
||||
<p>节点数: {G.number_of_nodes()} | 边数: {G.number_of_edges()}</p>
|
||||
<ul>{''.join(nodes_info[:50])}</ul>
|
||||
<p style='color: #888;'>安装 pyvis 以获得交互式可视化: pip install pyvis</p>
|
||||
</body></html>
|
||||
"""
|
||||
|
||||
|
||||
def get_subgraph_for_company(G: nx.DiGraph, company: str, depth: int = 2) -> nx.DiGraph:
|
||||
"""
|
||||
提取以指定企业为中心的子图(上下游 N 层)
|
||||
"""
|
||||
if company not in G:
|
||||
return nx.DiGraph()
|
||||
|
||||
# 收集相关节点
|
||||
related_nodes = {company}
|
||||
|
||||
# 上游(前驱)
|
||||
current_layer = {company}
|
||||
for _ in range(depth):
|
||||
next_layer = set()
|
||||
for node in current_layer:
|
||||
next_layer.update(G.predecessors(node))
|
||||
related_nodes.update(next_layer)
|
||||
current_layer = next_layer
|
||||
|
||||
# 下游(后继)
|
||||
current_layer = {company}
|
||||
for _ in range(depth):
|
||||
next_layer = set()
|
||||
for node in current_layer:
|
||||
next_layer.update(G.successors(node))
|
||||
related_nodes.update(next_layer)
|
||||
current_layer = next_layer
|
||||
|
||||
return G.subgraph(related_nodes).copy()
|
||||
@@ -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,
|
||||
)
|
||||
@@ -0,0 +1,11 @@
|
||||
streamlit>=1.30.0
|
||||
pandas>=2.0.0
|
||||
networkx>=3.0
|
||||
pyvis>=0.3.2
|
||||
plotly>=5.18.0
|
||||
requests>=2.31.0
|
||||
openai>=1.10.0
|
||||
pdfplumber>=0.10.0
|
||||
PyPDF2>=3.0.0
|
||||
akshare>=1.12.0
|
||||
jieba>=0.42.1
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""风险评估引擎模块"""
|
||||
@@ -0,0 +1,184 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
动态保险定价模型
|
||||
基于精算原理,结合六维风险评分实现"千企千面"费率计算
|
||||
"""
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
|
||||
# 将项目根目录添加到 Python 路径
|
||||
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||
|
||||
from config import INSURANCE_PRODUCTS
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 行业风险系数
|
||||
INDUSTRY_RISK_FACTORS = {
|
||||
"芯片": 1.35, # 地缘风险+技术迭代双高
|
||||
"AI": 1.30, # 合规风险+技术竞争
|
||||
"软件": 1.05, # 相对成熟
|
||||
"医疗器械": 0.95, # 技术壁垒高但风险较稳
|
||||
"新能源": 1.15, # 技术路线之争+贸易摩擦
|
||||
"消费电子": 1.10, # 供应链风险
|
||||
}
|
||||
|
||||
# 企业规模折扣(大企业风险分散能力更强)
|
||||
SCALE_DISCOUNT = {
|
||||
"超大型": 0.85, # 营收 > 500亿
|
||||
"大型": 0.90, # 100-500亿
|
||||
"中型": 1.00, # 10-100亿
|
||||
"小型": 1.15, # 1-10亿
|
||||
"微型": 1.30, # < 1亿
|
||||
}
|
||||
|
||||
|
||||
def get_enterprise_scale(revenue: float) -> str:
|
||||
"""根据营收判断企业规模"""
|
||||
if revenue >= 50_000_000_000:
|
||||
return "超大型"
|
||||
elif revenue >= 10_000_000_000:
|
||||
return "大型"
|
||||
elif revenue >= 1_000_000_000:
|
||||
return "中型"
|
||||
elif revenue >= 100_000_000:
|
||||
return "小型"
|
||||
return "微型"
|
||||
|
||||
|
||||
def calculate_premium(
|
||||
product_key: str,
|
||||
comprehensive_risk_score: int,
|
||||
six_dimension_scores: dict,
|
||||
sector: str = "",
|
||||
revenue: float = 0,
|
||||
) -> dict:
|
||||
"""
|
||||
动态费率计算器
|
||||
|
||||
定价公式:
|
||||
实际保费 = 基础保费 × 风险系数 × 行业调整 × 规模折扣
|
||||
|
||||
风险系数由综合评分决定(分段线性):
|
||||
- [0, 30) → 0.7 ~ 0.9(优质折扣)
|
||||
- [30, 50) → 0.9 ~ 1.2(标准浮动)
|
||||
- [50, 70) → 1.2 ~ 1.8(风险上浮)
|
||||
- [70, 90) → 1.8 ~ 2.5(惩罚性费率)
|
||||
- [90,100] → 拒保或附加极高免赔额
|
||||
"""
|
||||
product = INSURANCE_PRODUCTS.get(product_key)
|
||||
if not product:
|
||||
return {"error": f"未知保险产品: {product_key}"}
|
||||
|
||||
base_premium = product["base_premium"]
|
||||
base_coverage = product["base_coverage"]
|
||||
|
||||
# 1. 计算风险系数
|
||||
risk_multiplier = _calculate_risk_multiplier(comprehensive_risk_score)
|
||||
|
||||
# 2. 行业调整系数
|
||||
industry_factor = INDUSTRY_RISK_FACTORS.get(sector, 1.0)
|
||||
|
||||
# 3. 规模折扣
|
||||
scale = get_enterprise_scale(revenue)
|
||||
scale_factor = SCALE_DISCOUNT[scale]
|
||||
|
||||
# 4. 特定险种的维度调整
|
||||
dimension_adjustment = _get_dimension_adjustment(product_key, six_dimension_scores)
|
||||
|
||||
# 5. 最终保费
|
||||
final_premium = base_premium * risk_multiplier * industry_factor * scale_factor * dimension_adjustment
|
||||
|
||||
# 6. 核保条件
|
||||
deductible_rate = _calculate_deductible(comprehensive_risk_score)
|
||||
adjusted_coverage = base_coverage * (1.0 if comprehensive_risk_score < 70 else 0.7)
|
||||
|
||||
return {
|
||||
"product_name": product["name"],
|
||||
"product_key": product_key,
|
||||
"base_premium": base_premium,
|
||||
"base_coverage": base_coverage,
|
||||
"risk_multiplier": round(risk_multiplier, 3),
|
||||
"industry_factor": round(industry_factor, 3),
|
||||
"scale_factor": round(scale_factor, 3),
|
||||
"scale_label": scale,
|
||||
"dimension_adjustment": round(dimension_adjustment, 3),
|
||||
"final_premium": round(final_premium, 2),
|
||||
"adjusted_coverage": round(adjusted_coverage, 2),
|
||||
"deductible_rate": round(deductible_rate, 3),
|
||||
"deductible_amount": round(adjusted_coverage * deductible_rate, 2),
|
||||
"comprehensive_risk_score": comprehensive_risk_score,
|
||||
"is_insurable": comprehensive_risk_score < 90,
|
||||
"pricing_breakdown": (
|
||||
f"¥{base_premium:,.0f} × {risk_multiplier:.2f}(风险) "
|
||||
f"× {industry_factor:.2f}(行业) × {scale_factor:.2f}(规模) "
|
||||
f"× {dimension_adjustment:.2f}(维度) = ¥{final_premium:,.0f}"
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _calculate_risk_multiplier(score: int) -> float:
|
||||
"""分段线性风险系数计算"""
|
||||
if score < 30:
|
||||
return 0.7 + (score / 30) * 0.2 # 0.7 ~ 0.9
|
||||
elif score < 50:
|
||||
return 0.9 + ((score - 30) / 20) * 0.3 # 0.9 ~ 1.2
|
||||
elif score < 70:
|
||||
return 1.2 + ((score - 50) / 20) * 0.6 # 1.2 ~ 1.8
|
||||
elif score < 90:
|
||||
return 1.8 + ((score - 70) / 20) * 0.7 # 1.8 ~ 2.5
|
||||
else:
|
||||
return 3.0 # 拒保级别
|
||||
|
||||
|
||||
def _get_dimension_adjustment(product_key: str, scores: dict) -> float:
|
||||
"""针对特定险种,根据相关维度得分进行微调"""
|
||||
if product_key == "ip_lawsuit":
|
||||
# 知识产权被诉险:重点看技术路线和专利
|
||||
tech_score = scores.get("tech_disruption", 50)
|
||||
return 0.8 + (tech_score / 100) * 0.4 # 0.8 ~ 1.2
|
||||
|
||||
elif product_key == "exec_departure":
|
||||
# 高管离职险:重点看人员流失风险
|
||||
talent_score = scores.get("talent_loss", 50)
|
||||
return 0.7 + (talent_score / 100) * 0.6 # 0.7 ~ 1.3
|
||||
|
||||
elif product_key == "data_compliance":
|
||||
# 数据合规险:重点看合规风险
|
||||
compliance_score = scores.get("algo_compliance", 50)
|
||||
return 0.8 + (compliance_score / 100) * 0.4 # 0.8 ~ 1.2
|
||||
|
||||
return 1.0
|
||||
|
||||
|
||||
def _calculate_deductible(score: int) -> float:
|
||||
"""计算免赔率"""
|
||||
if score < 30:
|
||||
return 0.05 # 5%
|
||||
elif score < 50:
|
||||
return 0.10 # 10%
|
||||
elif score < 70:
|
||||
return 0.15 # 15%
|
||||
elif score < 90:
|
||||
return 0.25 # 25%
|
||||
else:
|
||||
return 0.50 # 50%(惩罚性高免赔)
|
||||
|
||||
|
||||
def calculate_all_products(
|
||||
comprehensive_risk_score: int,
|
||||
six_dimension_scores: dict,
|
||||
sector: str = "",
|
||||
revenue: float = 0,
|
||||
) -> list:
|
||||
"""计算所有险种的保费"""
|
||||
results = []
|
||||
for product_key in INSURANCE_PRODUCTS:
|
||||
result = calculate_premium(
|
||||
product_key, comprehensive_risk_score,
|
||||
six_dimension_scores, sector, revenue,
|
||||
)
|
||||
results.append(result)
|
||||
return results
|
||||
@@ -0,0 +1,146 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
核保决策报告生成器
|
||||
生成结构化的风险评估报告
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def generate_report(
|
||||
company_data: dict,
|
||||
debate_result: dict,
|
||||
pricing_results: list,
|
||||
) -> dict:
|
||||
"""
|
||||
生成完整的核保决策报告
|
||||
"""
|
||||
company_name = company_data.get("short_name", "未知企业")
|
||||
judge = debate_result.get("judge_result", {})
|
||||
scores = judge.get("six_dimension_scores", {})
|
||||
comprehensive = judge.get("comprehensive_score", 50)
|
||||
decision = judge.get("underwriting_decision", "标准承保")
|
||||
|
||||
report = {
|
||||
"title": f"科创企业智能风控核保报告 —— {company_name}",
|
||||
"report_id": f"RPT-{datetime.now().strftime('%Y%m%d%H%M%S')}",
|
||||
"generate_time": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"company_profile": {
|
||||
"name": company_name,
|
||||
"stock_code": company_data.get("stock_code", ""),
|
||||
"industry": company_data.get("industry", ""),
|
||||
"sector": company_data.get("sector", ""),
|
||||
"description": company_data.get("description", ""),
|
||||
},
|
||||
"risk_assessment": {
|
||||
"comprehensive_score": comprehensive,
|
||||
"risk_level": judge.get("risk_level", "中"),
|
||||
"six_dimension_scores": scores,
|
||||
"key_risks": judge.get("key_risks", []),
|
||||
},
|
||||
"debate_summary": {
|
||||
"law_findings": debate_result.get("law_result", {}).get("key_findings", []),
|
||||
"tech_findings": debate_result.get("tech_result", {}).get("key_findings", []),
|
||||
"fin_findings": debate_result.get("finance_result", {}).get("key_findings", []),
|
||||
"conflicts": debate_result.get("conflicts", []),
|
||||
"resolution": judge.get("conflict_resolution", ""),
|
||||
},
|
||||
"underwriting_decision": {
|
||||
"decision": decision,
|
||||
"conditions": judge.get("underwriting_conditions", []),
|
||||
"summary": judge.get("summary", ""),
|
||||
},
|
||||
"pricing": pricing_results,
|
||||
}
|
||||
|
||||
return report
|
||||
|
||||
|
||||
def format_report_markdown(report: dict) -> str:
|
||||
"""将报告格式化为 Markdown"""
|
||||
lines = []
|
||||
lines.append(f"# {report['title']}")
|
||||
lines.append(f"\n> 报告编号: {report['report_id']} | 生成时间: {report['generate_time']}")
|
||||
|
||||
# 企业概况
|
||||
profile = report["company_profile"]
|
||||
lines.append("\n## 一、企业概况")
|
||||
lines.append(f"- **企业名称**: {profile['name']}")
|
||||
lines.append(f"- **股票代码**: {profile['stock_code']}")
|
||||
lines.append(f"- **所属行业**: {profile['industry']}")
|
||||
lines.append(f"- **企业描述**: {profile['description']}")
|
||||
|
||||
# 风险评估
|
||||
risk = report["risk_assessment"]
|
||||
lines.append("\n## 二、风险评估")
|
||||
lines.append(f"- **综合风险评分**: {risk['comprehensive_score']} 分")
|
||||
lines.append(f"- **风险等级**: {risk['risk_level']}")
|
||||
|
||||
lines.append("\n### 六维风险评分")
|
||||
dim_names = {
|
||||
"tech_disruption": "技术路线颠覆",
|
||||
"talent_loss": "核心人员流失",
|
||||
"algo_compliance": "算法/数据合规",
|
||||
"geopolitical": "地缘政治/出口管制",
|
||||
"rd_capitalization": "研发资本化操纵",
|
||||
"concentration": "客户/供应商集中",
|
||||
}
|
||||
lines.append("| 风险维度 | 评分 | 等级 |")
|
||||
lines.append("|---------|------|------|")
|
||||
for dim, score in risk.get("six_dimension_scores", {}).items():
|
||||
name = dim_names.get(dim, dim)
|
||||
level = "高" if score >= 70 else ("中" if score >= 40 else "低")
|
||||
lines.append(f"| {name} | {score} | {level} |")
|
||||
|
||||
# 辩论摘要
|
||||
debate = report["debate_summary"]
|
||||
lines.append("\n## 三、多智能体交叉验证摘要")
|
||||
|
||||
lines.append("\n### 👩⚖️ 法务风控节点")
|
||||
for f in debate.get("law_findings", []):
|
||||
lines.append(f"- {f}")
|
||||
|
||||
lines.append("\n### 👨🔬 技术风控节点")
|
||||
for f in debate.get("tech_findings", []):
|
||||
lines.append(f"- {f}")
|
||||
|
||||
lines.append("\n### 👔 财务风控节点")
|
||||
for f in debate.get("fin_findings", []):
|
||||
lines.append(f"- {f}")
|
||||
|
||||
if debate.get("conflicts"):
|
||||
lines.append("\n### ⚠️ 冲突点")
|
||||
for c in debate["conflicts"]:
|
||||
lines.append(f"- {c}")
|
||||
|
||||
# 核保决策
|
||||
uw = report["underwriting_decision"]
|
||||
lines.append(f"\n## 四、核保决策")
|
||||
lines.append(f"\n**决策**: 【{uw['decision']}】")
|
||||
lines.append(f"\n{uw.get('summary', '')}")
|
||||
|
||||
if uw.get("conditions"):
|
||||
lines.append("\n### 核保附加条件")
|
||||
for c in uw["conditions"]:
|
||||
lines.append(f"- {c}")
|
||||
|
||||
# 保费方案
|
||||
lines.append("\n## 五、保险产品费率方案")
|
||||
if report.get("pricing"):
|
||||
lines.append("| 险种 | 基础保费 | 最终保费 | 保额 | 免赔率 |")
|
||||
lines.append("|------|---------|---------|------|--------|")
|
||||
for p in report["pricing"]:
|
||||
if p.get("is_insurable"):
|
||||
lines.append(
|
||||
f"| {p['product_name']} | ¥{p['base_premium']:,.0f} | "
|
||||
f"¥{p['final_premium']:,.0f} | ¥{p['adjusted_coverage']:,.0f} | "
|
||||
f"{p['deductible_rate']:.0%} |"
|
||||
)
|
||||
else:
|
||||
lines.append(f"| {p['product_name']} | - | **拒保** | - | - |")
|
||||
|
||||
return "\n".join(lines)
|
||||
@@ -0,0 +1,148 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
六维风险评分器
|
||||
整合所有数据源和分析结果,生成结构化六维风险画像
|
||||
"""
|
||||
import logging
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 风险等级映射
|
||||
RISK_LEVELS = {
|
||||
(0, 30): {"level": "低", "color": "#4CAF50", "emoji": "🟢"},
|
||||
(30, 50): {"level": "中低", "color": "#8BC34A", "emoji": "🟡"},
|
||||
(50, 70): {"level": "中高", "color": "#FF9800", "emoji": "🟠"},
|
||||
(70, 90): {"level": "高", "color": "#F44336", "emoji": "🔴"},
|
||||
(90, 101): {"level": "极高", "color": "#B71C1C", "emoji": "⛔"},
|
||||
}
|
||||
|
||||
|
||||
def get_risk_level(score: int) -> dict:
|
||||
"""根据分数获取风险等级详情"""
|
||||
for (low, high), info in RISK_LEVELS.items():
|
||||
if low <= score < high:
|
||||
return info
|
||||
return {"level": "未知", "color": "#9E9E9E", "emoji": "❓"}
|
||||
|
||||
|
||||
def calculate_six_dimension_scores(company_data: dict) -> dict:
|
||||
"""
|
||||
基于企业数据直接计算六维风险评分(不依赖 LLM)
|
||||
用于快速预览和规则引擎降级场景
|
||||
"""
|
||||
financials = company_data.get("financials", {})
|
||||
compliance = company_data.get("compliance", {})
|
||||
tech_route = company_data.get("tech_route", {})
|
||||
personnel = company_data.get("core_tech_personnel", [])
|
||||
supply_chain = company_data.get("supply_chain", {})
|
||||
|
||||
scores = {}
|
||||
|
||||
# 1. 技术路线颠覆风险
|
||||
competing = len(tech_route.get("competing_techs", []))
|
||||
moat = tech_route.get("tech_moat", "")
|
||||
tech_score = min(25 + competing * 15, 85)
|
||||
if "差距" in moat or "受制" in moat or "威胁" in moat:
|
||||
tech_score = min(tech_score + 15, 95)
|
||||
if "领先" in moat or "第一" in moat:
|
||||
tech_score = max(tech_score - 10, 10)
|
||||
scores["tech_disruption"] = tech_score
|
||||
|
||||
# 2. 核心人员流失风险
|
||||
departed = [p for p in personnel if "离职" in p.get("status", "")]
|
||||
high_imp = [p for p in personnel if p.get("importance") == "极高"]
|
||||
if departed:
|
||||
scores["talent_loss"] = 80
|
||||
elif len(high_imp) <= 1 and len(personnel) <= 2:
|
||||
scores["talent_loss"] = 55
|
||||
else:
|
||||
scores["talent_loss"] = 25
|
||||
|
||||
# 3. 算法/数据合规风险
|
||||
algo_status = compliance.get("algo_filing_status", "")
|
||||
data_risk = compliance.get("data_export_risk", "低")
|
||||
algo_score = 20
|
||||
if "未" in algo_status:
|
||||
algo_score = 75
|
||||
elif data_risk == "高":
|
||||
algo_score = 65
|
||||
elif data_risk == "中":
|
||||
algo_score = 40
|
||||
elif "已备案" in algo_status:
|
||||
algo_score = 15
|
||||
scores["algo_compliance"] = algo_score
|
||||
|
||||
# 4. 地缘政治/出口管制风险
|
||||
entity_status = compliance.get("entity_list_status", "")
|
||||
if "被列入" in entity_status:
|
||||
scores["geopolitical"] = 92
|
||||
elif supply_chain.get("supplier_concentration_risk") == "极高":
|
||||
scores["geopolitical"] = 68
|
||||
elif supply_chain.get("supplier_concentration_risk") == "高":
|
||||
scores["geopolitical"] = 50
|
||||
else:
|
||||
scores["geopolitical"] = 18
|
||||
|
||||
# 5. 研发资本化操纵风险
|
||||
cap_rate = financials.get("rd_capitalization_rate", 0)
|
||||
if cap_rate >= 0.4:
|
||||
scores["rd_capitalization"] = 90
|
||||
elif cap_rate >= 0.3:
|
||||
scores["rd_capitalization"] = 72
|
||||
elif cap_rate >= 0.15:
|
||||
scores["rd_capitalization"] = 48
|
||||
elif cap_rate > 0:
|
||||
scores["rd_capitalization"] = 25
|
||||
else:
|
||||
scores["rd_capitalization"] = 10
|
||||
|
||||
# 6. 客户/供应商集中风险
|
||||
cust_ratio = financials.get("top5_customer_ratio", 0)
|
||||
supp_ratio = financials.get("top5_supplier_ratio", 0)
|
||||
max_conc = max(cust_ratio, supp_ratio)
|
||||
if max_conc >= 0.8:
|
||||
scores["concentration"] = 88
|
||||
elif max_conc >= 0.6:
|
||||
scores["concentration"] = 68
|
||||
elif max_conc >= 0.4:
|
||||
scores["concentration"] = 42
|
||||
else:
|
||||
scores["concentration"] = 18
|
||||
|
||||
# 加权综合
|
||||
weights = {
|
||||
"tech_disruption": 0.20,
|
||||
"talent_loss": 0.15,
|
||||
"algo_compliance": 0.15,
|
||||
"geopolitical": 0.20,
|
||||
"rd_capitalization": 0.15,
|
||||
"concentration": 0.15,
|
||||
}
|
||||
comprehensive = int(sum(scores[k] * weights[k] for k in scores))
|
||||
|
||||
# 维度中文名映射
|
||||
dim_names = {
|
||||
"tech_disruption": "技术路线颠覆",
|
||||
"talent_loss": "核心人员流失",
|
||||
"algo_compliance": "算法/数据合规",
|
||||
"geopolitical": "地缘政治/出口管制",
|
||||
"rd_capitalization": "研发资本化操纵",
|
||||
"concentration": "客户/供应商集中",
|
||||
}
|
||||
|
||||
return {
|
||||
"scores": scores,
|
||||
"comprehensive_score": comprehensive,
|
||||
"risk_level": get_risk_level(comprehensive),
|
||||
"dimension_details": {
|
||||
k: {
|
||||
"name": dim_names[k],
|
||||
"score": v,
|
||||
"level": get_risk_level(v),
|
||||
"weight": weights[k],
|
||||
}
|
||||
for k, v in scores.items()
|
||||
},
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
全局状态与企业选择同步工具
|
||||
跨页面共享 `st.session_state["global_selected_stock_code"]`
|
||||
确保在一个页面选择企业后,切换到任意页面均自动保持该企业联动
|
||||
包含自动隐藏 Streamlit 右上角 Deploy 按钮与默认 Header 的全局 CSS 样式
|
||||
"""
|
||||
import streamlit as st
|
||||
from collectors.financial_collector import get_all_companies
|
||||
|
||||
GLOBAL_COMPANY_KEY = "global_selected_stock_code"
|
||||
|
||||
|
||||
def hide_streamlit_header_footer():
|
||||
"""彻底隐藏 Streamlit 右上角的 Deploy 按钮、工具栏及页脚"""
|
||||
st.markdown("""
|
||||
<style>
|
||||
/* 隐藏右上角 Deploy 按钮及 Header 菜单 */
|
||||
header[data-testid="stHeader"] {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
height: 0px !important;
|
||||
}
|
||||
[data-testid="stDeployButton"] {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
#MainMenu {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
footer {
|
||||
display: none !important;
|
||||
visibility: hidden !important;
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
|
||||
def get_global_company_code() -> str:
|
||||
"""获取当前全局选中的企业股票代码"""
|
||||
companies = get_all_companies()
|
||||
if GLOBAL_COMPANY_KEY not in st.session_state or not st.session_state[GLOBAL_COMPANY_KEY]:
|
||||
st.session_state[GLOBAL_COMPANY_KEY] = companies[0]["stock_code"] if companies else "688256"
|
||||
return st.session_state[GLOBAL_COMPANY_KEY]
|
||||
|
||||
|
||||
def set_global_company_code(code: str):
|
||||
"""保存选中的企业股票代码到全局 SessionState"""
|
||||
st.session_state[GLOBAL_COMPANY_KEY] = code
|
||||
|
||||
|
||||
def render_company_selector(label: str = "🏢 选择待评估科创企业", key_suffix: str = "main"):
|
||||
"""
|
||||
渲染与全局 session_state 双向同步的企业选择下拉框
|
||||
返回: selected_company_dict (选中的企业数据字典)
|
||||
"""
|
||||
hide_streamlit_header_footer()
|
||||
|
||||
companies = get_all_companies()
|
||||
if not companies:
|
||||
return None
|
||||
|
||||
# 构建带图标与领域的名称映射
|
||||
company_options = {f"{c['short_name']} ({c['stock_code']}) - {c['sector']}": c["stock_code"] for c in companies}
|
||||
labels_list = list(company_options.keys())
|
||||
codes_list = [c["stock_code"] for c in companies]
|
||||
|
||||
current_code = get_global_company_code()
|
||||
default_index = codes_list.index(current_code) if current_code in codes_list else 0
|
||||
|
||||
selected_label = st.selectbox(
|
||||
label,
|
||||
labels_list,
|
||||
index=default_index,
|
||||
key=f"company_selector_{key_suffix}"
|
||||
)
|
||||
|
||||
new_code = company_options[selected_label]
|
||||
if new_code != st.session_state.get(GLOBAL_COMPANY_KEY):
|
||||
set_global_company_code(new_code)
|
||||
|
||||
# 返回选中的完整企业字典
|
||||
for c in companies:
|
||||
if c["stock_code"] == new_code:
|
||||
return c
|
||||
return companies[0]
|
||||
|
||||
|
||||
def render_sidebar_global_company_selector():
|
||||
"""在侧边栏渲染全局企业选择器与状态指示标签,并自动注入隐藏 Deploy 的 CSS"""
|
||||
hide_streamlit_header_footer()
|
||||
|
||||
companies = get_all_companies()
|
||||
if not companies:
|
||||
return
|
||||
|
||||
company_options = {f"{c['short_name']} ({c['stock_code']})": c["stock_code"] for c in companies}
|
||||
labels_list = list(company_options.keys())
|
||||
codes_list = [c["stock_code"] for c in companies]
|
||||
|
||||
current_code = get_global_company_code()
|
||||
default_index = codes_list.index(current_code) if current_code in codes_list else 0
|
||||
|
||||
st.markdown("### 🏢 全局联动评估目标")
|
||||
selected_label = st.selectbox(
|
||||
"当前联动目标企业:",
|
||||
labels_list,
|
||||
index=default_index,
|
||||
key="global_sidebar_company_selector"
|
||||
)
|
||||
|
||||
new_code = company_options[selected_label]
|
||||
if new_code != st.session_state.get(GLOBAL_COMPANY_KEY):
|
||||
set_global_company_code(new_code)
|
||||
@@ -0,0 +1,304 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
🛡️ 科创企业智能风控与核保系统 - 首页 / 风控大屏入口
|
||||
"""
|
||||
import sys
|
||||
import os
|
||||
import streamlit as st
|
||||
import pandas as pd
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
|
||||
# 确保项目根目录在 Python 路径中
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
from config import PAGE_TITLE, PAGE_ICON, LAYOUT
|
||||
from collectors.financial_collector import get_all_companies
|
||||
from risk_engine.risk_scorer import calculate_six_dimension_scores, get_risk_level
|
||||
|
||||
# ============================================================
|
||||
# 页面配置
|
||||
# ============================================================
|
||||
st.set_page_config(
|
||||
page_title=PAGE_TITLE,
|
||||
page_icon=PAGE_ICON,
|
||||
layout=LAYOUT,
|
||||
initial_sidebar_state="expanded",
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# 自定义样式
|
||||
# ============================================================
|
||||
st.markdown("""
|
||||
<style>
|
||||
/* 主标题渐变 */
|
||||
.main-title {
|
||||
background: linear-gradient(120deg, #1a1a2e 0%, #16213e 50%, #0f3460 100%);
|
||||
padding: 30px;
|
||||
border-radius: 15px;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
box-shadow: 0 4px 15px rgba(0,0,0,0.3);
|
||||
}
|
||||
.main-title h1 {
|
||||
color: #e94560;
|
||||
font-size: 2.2em;
|
||||
margin: 0;
|
||||
}
|
||||
.main-title p {
|
||||
color: #a8a8b3;
|
||||
font-size: 1.1em;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* 统计卡片 */
|
||||
.stat-card {
|
||||
background: linear-gradient(135deg, #1a1a2e, #16213e);
|
||||
padding: 20px;
|
||||
border-radius: 12px;
|
||||
text-align: center;
|
||||
border: 1px solid #2a2a4a;
|
||||
transition: transform 0.2s;
|
||||
}
|
||||
.stat-card:hover {
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
.stat-card .number {
|
||||
font-size: 2.5em;
|
||||
font-weight: bold;
|
||||
color: #e94560;
|
||||
}
|
||||
.stat-card .label {
|
||||
color: #a8a8b3;
|
||||
font-size: 0.95em;
|
||||
margin-top: 5px;
|
||||
}
|
||||
|
||||
/* 企业卡片 */
|
||||
.company-card {
|
||||
background: #16213e;
|
||||
padding: 15px;
|
||||
border-radius: 10px;
|
||||
margin: 8px 0;
|
||||
border-left: 4px solid;
|
||||
}
|
||||
|
||||
/* 侧边栏 */
|
||||
[data-testid="stSidebar"] {
|
||||
background: linear-gradient(180deg, #1a1a2e 0%, #0f0f23 100%);
|
||||
}
|
||||
</style>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
from utils.session_helper import render_sidebar_global_company_selector
|
||||
|
||||
# ============================================================
|
||||
# 侧边栏
|
||||
# ============================================================
|
||||
with st.sidebar:
|
||||
render_sidebar_global_company_selector()
|
||||
st.markdown("---")
|
||||
st.markdown("### 🛡️ 系统导航")
|
||||
st.markdown("---")
|
||||
st.markdown("""
|
||||
**功能模块**
|
||||
- 🏠 系统首页
|
||||
- 📊 企业风险概览
|
||||
- 🕸️ 供应链知识图谱
|
||||
- ⚖️ 多智能体辩论诊断
|
||||
- 💰 动态定价与核保
|
||||
""")
|
||||
st.markdown("---")
|
||||
st.markdown("""
|
||||
**技术栈**
|
||||
- 🤖 DeepSeek API (LLM)
|
||||
- 🕸️ NetworkX (知识图谱)
|
||||
- 📊 Plotly (可视化)
|
||||
- 🔧 Streamlit (Web框架)
|
||||
""")
|
||||
st.markdown("---")
|
||||
st.caption("中国平安 × 挑战杯 · 科创风控原型")
|
||||
|
||||
# ============================================================
|
||||
# 主页内容
|
||||
# ============================================================
|
||||
|
||||
# 标题
|
||||
st.markdown("""
|
||||
<div class="main-title">
|
||||
<h1>🛡️ 科创企业智能风控与核保系统</h1>
|
||||
<p>基于多智能体辩论 × 知识图谱 × 动态定价的全链条风控平台</p>
|
||||
<p style="font-size: 0.85em; color: #666;">中国青基会平安励志计划 · XH-202626 · 科创企业特有风险的识别与管理</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
# 加载企业数据
|
||||
companies = get_all_companies()
|
||||
|
||||
# ============================================================
|
||||
# 统计概览卡片
|
||||
# ============================================================
|
||||
st.markdown("### 📊 系统概览")
|
||||
|
||||
# 计算所有企业的风险评分
|
||||
risk_data = []
|
||||
for comp in companies:
|
||||
scores = calculate_six_dimension_scores(comp)
|
||||
risk_data.append({
|
||||
"company": comp["short_name"],
|
||||
"stock_code": comp["stock_code"],
|
||||
"industry": comp["industry"],
|
||||
"sector": comp["sector"],
|
||||
"comprehensive_score": scores["comprehensive_score"],
|
||||
"risk_level": scores["risk_level"]["level"],
|
||||
**scores["scores"],
|
||||
})
|
||||
|
||||
df = pd.DataFrame(risk_data)
|
||||
|
||||
# 统计卡片
|
||||
col1, col2, col3, col4, col5 = st.columns(5)
|
||||
|
||||
with col1:
|
||||
st.markdown(f"""
|
||||
<div class="stat-card">
|
||||
<div class="number">{len(companies)}</div>
|
||||
<div class="label">监控企业总数</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
with col2:
|
||||
high_risk = len(df[df["comprehensive_score"] >= 70])
|
||||
st.markdown(f"""
|
||||
<div class="stat-card">
|
||||
<div class="number" style="color: #F44336;">{high_risk}</div>
|
||||
<div class="label">⚠️ 高风险企业</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
with col3:
|
||||
med_risk = len(df[(df["comprehensive_score"] >= 40) & (df["comprehensive_score"] < 70)])
|
||||
st.markdown(f"""
|
||||
<div class="stat-card">
|
||||
<div class="number" style="color: #FF9800;">{med_risk}</div>
|
||||
<div class="label">🟡 中风险企业</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
with col4:
|
||||
low_risk = len(df[df["comprehensive_score"] < 40])
|
||||
st.markdown(f"""
|
||||
<div class="stat-card">
|
||||
<div class="number" style="color: #4CAF50;">{low_risk}</div>
|
||||
<div class="label">🟢 低风险企业</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
with col5:
|
||||
sanctioned = len([c for c in companies if "被列入" in c.get("compliance", {}).get("entity_list_status", "")])
|
||||
st.markdown(f"""
|
||||
<div class="stat-card">
|
||||
<div class="number" style="color: #B71C1C;">{sanctioned}</div>
|
||||
<div class="label">⛔ 受制裁企业</div>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
|
||||
st.markdown("<br>", unsafe_allow_html=True)
|
||||
|
||||
# ============================================================
|
||||
# 风险分布图
|
||||
# ============================================================
|
||||
col_left, col_right = st.columns([3, 2])
|
||||
|
||||
with col_left:
|
||||
st.markdown("#### 🎯 企业综合风险评分分布")
|
||||
|
||||
# 水平柱状图,按风险排序
|
||||
df_sorted = df.sort_values("comprehensive_score", ascending=True)
|
||||
|
||||
colors = []
|
||||
for score in df_sorted["comprehensive_score"]:
|
||||
if score >= 70:
|
||||
colors.append("#F44336")
|
||||
elif score >= 50:
|
||||
colors.append("#FF9800")
|
||||
elif score >= 30:
|
||||
colors.append("#FFC107")
|
||||
else:
|
||||
colors.append("#4CAF50")
|
||||
|
||||
fig = go.Figure(go.Bar(
|
||||
x=df_sorted["comprehensive_score"],
|
||||
y=df_sorted["company"],
|
||||
orientation="h",
|
||||
marker_color=colors,
|
||||
text=df_sorted["comprehensive_score"],
|
||||
textposition="outside",
|
||||
))
|
||||
fig.update_layout(
|
||||
height=400,
|
||||
margin=dict(l=0, r=30, t=10, b=10),
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
font=dict(color="white"),
|
||||
xaxis=dict(
|
||||
title="综合风险评分",
|
||||
range=[0, 105],
|
||||
gridcolor="rgba(255,255,255,0.1)",
|
||||
),
|
||||
yaxis=dict(gridcolor="rgba(255,255,255,0.1)"),
|
||||
)
|
||||
# 添加阈值线
|
||||
fig.add_vline(x=70, line_dash="dash", line_color="#F44336",
|
||||
annotation_text="高风险线(70)", annotation_position="top right")
|
||||
fig.add_vline(x=40, line_dash="dash", line_color="#FF9800",
|
||||
annotation_text="中风险线(40)", annotation_position="top right")
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
with col_right:
|
||||
st.markdown("#### 🏷️ 行业风险热力")
|
||||
|
||||
# 按行业汇总
|
||||
sector_risk = df.groupby("sector")["comprehensive_score"].mean().reset_index()
|
||||
sector_risk.columns = ["行业", "平均风险"]
|
||||
sector_risk = sector_risk.sort_values("平均风险", ascending=False)
|
||||
|
||||
fig2 = px.bar(
|
||||
sector_risk, x="行业", y="平均风险",
|
||||
color="平均风险",
|
||||
color_continuous_scale=["#4CAF50", "#FFC107", "#F44336"],
|
||||
range_color=[0, 100],
|
||||
)
|
||||
fig2.update_layout(
|
||||
height=400,
|
||||
margin=dict(l=0, r=0, t=10, b=10),
|
||||
plot_bgcolor="rgba(0,0,0,0)",
|
||||
paper_bgcolor="rgba(0,0,0,0)",
|
||||
font=dict(color="white"),
|
||||
showlegend=False,
|
||||
coloraxis_showscale=False,
|
||||
)
|
||||
st.plotly_chart(fig2, use_container_width=True)
|
||||
|
||||
# ============================================================
|
||||
# 企业列表
|
||||
# ============================================================
|
||||
st.markdown("### 📋 企业风险速览")
|
||||
|
||||
# 格式化数据表格
|
||||
display_df = df[["company", "stock_code", "industry", "comprehensive_score", "risk_level"]].copy()
|
||||
display_df.columns = ["企业名称", "股票代码", "行业", "综合风险评分", "风险等级"]
|
||||
|
||||
st.dataframe(display_df, use_container_width=True, height=400)
|
||||
|
||||
# ============================================================
|
||||
# 底部信息
|
||||
# ============================================================
|
||||
st.markdown("---")
|
||||
st.markdown("""
|
||||
<div style="text-align: center; color: #666; font-size: 0.85em;">
|
||||
<p>🛡️ 科创企业智能风控与核保系统 v1.0</p>
|
||||
<p>技术架构: Multi-Agent 交叉验证 × 供应链知识图谱 × 动态保险定价</p>
|
||||
<p>数据来源: 科创板公开年报 · BIS 实体清单 · 网信办算法备案公示</p>
|
||||
</div>
|
||||
""", unsafe_allow_html=True)
|
||||
@@ -0,0 +1,142 @@
|
||||
# 中国青基会平安励志计划 · 2026竞赛作品
|
||||
# 《数智风控:AI 科创企业新型风险前瞻治理方案》
|
||||
## 原型系统部署与运行维护手册 (XH-202626)
|
||||
|
||||
---
|
||||
|
||||
### 一、 系统概述与体验入口
|
||||
|
||||
本系统交付了完备的金融级 Web 风控系统原型,已完成全套生产环境部署与 SSL 证书加密,支持公网在线实时访问与本地一键部署运行。
|
||||
|
||||
- **公网加密访问入口**:`https://risk.aiformat.cn`
|
||||
- **公网服务器 IP**:`159.75.81.121`
|
||||
- **技术栈规范**:
|
||||
- **核心语言**:Python 3.10+
|
||||
- **Web 交互框架**:Streamlit 1.32+
|
||||
- **大模型推理引擎**:DeepSeek API (`deepseek-chat` 深度推理底座)
|
||||
- **图谱计算引擎**:NetworkX 3.0+ (广度优先 BFS 风险传染算法)
|
||||
- **可视化组件**:Plotly 5.18+ / PyVis 0.3+
|
||||
- **反向代理与守护**:Nginx 1.18+ + Systemd 服务守护 + Let's Encrypt SSL
|
||||
|
||||
---
|
||||
|
||||
### 二、 本地快速一键运行指南
|
||||
|
||||
若评审专家或用户需要在本地环境直接运行本源码系统,请按以下步骤操作:
|
||||
|
||||
#### 1. 环境准备
|
||||
确保本地安装了 **Python 3.10** 或更高版本:
|
||||
```bash
|
||||
python --version
|
||||
```
|
||||
|
||||
#### 2. 解压与安装依赖
|
||||
解压源码包后进入项目根目录,运行依赖安装命令:
|
||||
```bash
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
#### 3. 配置 DeepSeek 大模型 API Key (可选)
|
||||
系统默认内置了 DeepSeek 演示 API 接口。如需使用自定义密钥,可打开 `config.py` 文件配置:
|
||||
```python
|
||||
DEEPSEEK_API_KEY = "your-deepseek-api-key"
|
||||
```
|
||||
|
||||
#### 4. 一键启动 Web 系统
|
||||
在终端运行以下命令启动本地交互服务:
|
||||
```bash
|
||||
streamlit run 🏠_系统首页.py
|
||||
```
|
||||
终端启动成功后,浏览器将自动弹窗并打开系统界面:
|
||||
`http://localhost:8501`
|
||||
|
||||
---
|
||||
|
||||
### 三、 服务器生产环境部署规范 (CentOS / Ubuntu)
|
||||
|
||||
本系统已在生产服务器 `159.75.81.121` 上完成了生产级部署,配置规范如下:
|
||||
|
||||
#### 1. 代码目录结构
|
||||
```text
|
||||
/opt/risk_system/
|
||||
├── 🏠_系统首页.py # 系统控制台大屏入口
|
||||
├── config.py # 全局参数与风控规则配置
|
||||
├── requirements.txt # Python 依赖清单
|
||||
├── pages/ # 4 大业务功能子模块
|
||||
│ ├── 01_📊_企业风险概览.py
|
||||
│ ├── 02_🕸️_供应链知识图谱.py
|
||||
│ ├── 03_🤖_多智能体辩论诊断.py
|
||||
│ └── 04_💰_动态定价与核保.py
|
||||
├── risk_engine/ # Multi-Agent 专家辩论与冲突平息引擎
|
||||
├── knowledge_graph/ # NetworkX 图谱拓扑与 BIS 穿透算法
|
||||
└── collectors/ # AKShare 财报与网信办/BIS 提取器
|
||||
```
|
||||
|
||||
#### 2. Systemd 服务守护配置 (`/etc/systemd/system/risk_app.service`)
|
||||
使用 Systemd 实现无人值守自动拉起与宕机自愈:
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Streamlit Risk App Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
WorkingDirectory=/opt/risk_system
|
||||
ExecStart=/usr/local/bin/streamlit run 🏠_系统首页.py --server.port 8501 --server.headless true
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
启动与状态检查命令:
|
||||
```bash
|
||||
systemctl daemon-reload
|
||||
systemctl enable risk_app
|
||||
systemctl restart risk_app
|
||||
systemctl status risk_app
|
||||
```
|
||||
|
||||
#### 3. Nginx 反向代理与 WebSocket 连通配置 (`/etc/nginx/conf.d/risk.conf`)
|
||||
配置文件支持 HTTP(80) 自动强制重定向至 HTTPS(443),并开启 WebSocket 实时推流支持:
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name risk.aiformat.cn;
|
||||
return 301 https://$host$request_uri;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 443 ssl http2;
|
||||
server_name risk.aiformat.cn;
|
||||
|
||||
ssl_certificate /etc/letsencrypt/live/risk.aiformat.cn/fullchain.pem;
|
||||
ssl_certificate_key /etc/letsencrypt/live/risk.aiformat.cn/privkey.pem;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8501;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 四、 提交材料清单 (Checklist)
|
||||
|
||||
| 序号 | 材料名称 | 文件类型 | 描述 |
|
||||
|:---|:---|:---|:---|
|
||||
| 01 | **研究报告** | `.md` / `.pdf` | 完整的学术研究报告与六维风控理论推导 |
|
||||
| 02 | **竞赛汇报 PPT** | `.pptx` | 包含 22 页大字架构与 4 大模块实操截图的演示文稿 |
|
||||
| 03 | **原型系统完整源码** | Python 代码包 | 包含 Agent 辩论、知识图谱、动态精算与 Web 源码 |
|
||||
| 04 | **部署与运行手册** | `.md` | 本文档,提供本地运行与云服务器守护指引 |
|
||||
|
||||
---
|
||||
*中国青基会平安励志计划 · 2026 挑战杯“揭榜挂帅”擂台赛 XH-202626 课题研发团队*
|
||||
Binary file not shown.
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""多智能体辩论模块"""
|
||||
@@ -0,0 +1,203 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
Agent 基类
|
||||
支持逐 Token 实时流式打字机输出 (Token-level UI Streaming)
|
||||
捕获思维链 (reasoning_content) 与最终生成结果 (content) 逐字推送到前端
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional, Callable
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BaseAgent:
|
||||
"""智能体基类,支持 Token 级流式 LLM 推理与降级逻辑"""
|
||||
|
||||
def __init__(self, name: str, system_prompt: str, role_icon: str = "🤖"):
|
||||
self.name = name
|
||||
self.system_prompt = system_prompt
|
||||
self.role_icon = role_icon
|
||||
self._client = None
|
||||
# 推理链记录
|
||||
self.reasoning_trace = []
|
||||
# 逐 Token 实时回调函数: callback(token_type: "reasoning"|"content", token_text: str)
|
||||
self.on_token_callback: Optional[Callable[[str, str], None]] = None
|
||||
|
||||
def _get_client(self):
|
||||
"""延迟初始化 OpenAI 客户端"""
|
||||
if self._client is not None:
|
||||
return self._client
|
||||
|
||||
try:
|
||||
import httpx
|
||||
from openai import OpenAI
|
||||
import os
|
||||
|
||||
from config import (
|
||||
DEEPSEEK_API_KEY, DEEPSEEK_BASE_URL, DEEPSEEK_MODEL,
|
||||
VOLCENGINE_API_KEY, VOLCENGINE_BASE_URL, VOLCENGINE_MODEL,
|
||||
OPENAI_API_KEY, OPENAI_BASE_URL, OPENAI_MODEL
|
||||
)
|
||||
|
||||
api_key = os.environ.get("DEEPSEEK_API_KEY", "")
|
||||
if api_key:
|
||||
base_url = DEEPSEEK_BASE_URL
|
||||
model = DEEPSEEK_MODEL
|
||||
else:
|
||||
api_key = VOLCENGINE_API_KEY
|
||||
base_url = VOLCENGINE_BASE_URL
|
||||
model = VOLCENGINE_MODEL
|
||||
|
||||
if not api_key:
|
||||
api_key = OPENAI_API_KEY
|
||||
base_url = OPENAI_BASE_URL
|
||||
model = OPENAI_MODEL
|
||||
|
||||
if api_key:
|
||||
http_client = httpx.Client(trust_env=False, timeout=60.0)
|
||||
self._client = OpenAI(
|
||||
api_key=api_key,
|
||||
base_url=base_url,
|
||||
http_client=http_client,
|
||||
)
|
||||
self._model = model
|
||||
logger.info(f"[{self.name}] 已成功连接大模型服务")
|
||||
return self._client
|
||||
except ImportError:
|
||||
logger.warning("openai 库未安装")
|
||||
except Exception as e:
|
||||
logger.warning(f"初始化 LLM 客户端失败: {e}")
|
||||
|
||||
return None
|
||||
|
||||
def _trace(self, step: str, content: str):
|
||||
"""记录推理链步骤"""
|
||||
entry = {
|
||||
"timestamp": time.strftime("%H:%M:%S"),
|
||||
"step": step,
|
||||
"content": content,
|
||||
"agent": self.name,
|
||||
"icon": self.role_icon
|
||||
}
|
||||
self.reasoning_trace.append(entry)
|
||||
|
||||
def infer(self, prompt: str, temperature: float = 0.1, max_retries: int = 0) -> str:
|
||||
"""
|
||||
执行 SSE 流式 LLM 推理 (stream=True)
|
||||
逐 Token 实时推送到 on_token_callback 渲染打字机效果
|
||||
"""
|
||||
self.reasoning_trace = []
|
||||
self._trace("📝 构建 Context", f"准备【{self.name}】数据与 Prompt")
|
||||
|
||||
client = self._get_client()
|
||||
if client is None:
|
||||
self._trace("⚠️ 状态通知", "大模型未就绪,切换至专家规则引擎")
|
||||
logger.info(f"[{self.name}] LLM 不可用,降级到规则引擎")
|
||||
fallback = self.fallback_inference(prompt)
|
||||
self._trace("🔧 专家引擎输出", fallback)
|
||||
return fallback
|
||||
|
||||
self._trace("🔗 大模型连接", "已连接大模型推理服务")
|
||||
|
||||
for attempt in range(max_retries + 1):
|
||||
try:
|
||||
self._trace("🚀 发起流式推理", "正在建立 SSE 流式传输通道...")
|
||||
t0 = time.time()
|
||||
|
||||
stream_resp = client.chat.completions.create(
|
||||
model=self._model,
|
||||
messages=[
|
||||
{"role": "system", "content": self.system_prompt},
|
||||
{"role": "user", "content": prompt},
|
||||
],
|
||||
temperature=temperature,
|
||||
max_tokens=2048,
|
||||
stream=True,
|
||||
timeout=60,
|
||||
)
|
||||
|
||||
full_content = []
|
||||
reasoning_chunks = []
|
||||
|
||||
for chunk in stream_resp:
|
||||
if not chunk.choices:
|
||||
continue
|
||||
delta = chunk.choices[0].delta
|
||||
|
||||
# 1. 逐 Token 提取深度思考过程 (reasoning_content)
|
||||
reasoning_piece = getattr(delta, "reasoning_content", None) or getattr(delta, "reasoning", None)
|
||||
if reasoning_piece:
|
||||
reasoning_chunks.append(reasoning_piece)
|
||||
if self.on_token_callback:
|
||||
try:
|
||||
self.on_token_callback("reasoning", reasoning_piece)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# 2. 逐 Token 提取正式回答内容 (content)
|
||||
content_piece = delta.content
|
||||
if content_piece:
|
||||
full_content.append(content_piece)
|
||||
if self.on_token_callback:
|
||||
try:
|
||||
self.on_token_callback("content", content_piece)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
elapsed = time.time() - t0
|
||||
final_text = "".join(full_content)
|
||||
full_reasoning = "".join(reasoning_chunks)
|
||||
|
||||
if full_reasoning:
|
||||
self._trace("🧠 完整思维链", full_reasoning)
|
||||
|
||||
if final_text.strip():
|
||||
self._trace("✅ 流式生成完毕", f"耗时 {elapsed:.1f}s | 产出 {len(final_text)} 字符")
|
||||
self._trace("📄 原始推理输出", final_text)
|
||||
return final_text
|
||||
else:
|
||||
self._trace("⚠️ 输出为空", "流式生成无有效内容,降级到专家引擎")
|
||||
|
||||
except Exception as e:
|
||||
error_msg = str(e)
|
||||
self._trace("⚡ 流式传输异常", f"连接中断: {error_msg}")
|
||||
logger.warning(f"[{self.name}] 流式调用失败: {e}")
|
||||
|
||||
self._trace("🛡️ 安全降级", "无缝切换至离线风控规则引擎")
|
||||
fallback = self.fallback_inference(prompt)
|
||||
self._trace("🔧 专家引擎输出", fallback)
|
||||
return fallback
|
||||
|
||||
def infer_json(self, prompt: str, temperature: float = 0.1) -> dict:
|
||||
"""
|
||||
执行 LLM 推理并解析为 JSON
|
||||
"""
|
||||
result = self.infer(prompt, temperature)
|
||||
try:
|
||||
if "```json" in result:
|
||||
json_str = result.split("```json")[1].split("```")[0].strip()
|
||||
parsed = json.loads(json_str)
|
||||
self._trace("✅ 结构解析", "从 Markdown 成功提取 JSON 数据")
|
||||
return parsed
|
||||
elif "```" in result:
|
||||
json_str = result.split("```")[1].split("```")[0].strip()
|
||||
parsed = json.loads(json_str)
|
||||
self._trace("✅ 结构解析", "从代码块成功提取 JSON 数据")
|
||||
return parsed
|
||||
else:
|
||||
parsed = json.loads(result)
|
||||
self._trace("✅ 结构解析", "直接解析 JSON 成功")
|
||||
return parsed
|
||||
except (json.JSONDecodeError, IndexError):
|
||||
self._trace("⚠️ 格式适配", "启用自动结构修正")
|
||||
logger.warning(f"[{self.name}] JSON 解析失败,返回原始文本")
|
||||
return {"raw_response": result, "parse_error": True}
|
||||
|
||||
def fallback_inference(self, prompt: str) -> str:
|
||||
"""规则引擎降级推理"""
|
||||
return json.dumps({"error": "大模型服务不可用,规则引擎未实现"}, ensure_ascii=False)
|
||||
|
||||
def __repr__(self):
|
||||
return f"{self.role_icon} {self.name}"
|
||||
@@ -0,0 +1,178 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
多智能体辩论编排引擎
|
||||
流程:信息分发 → 独立研判 → 交叉质证 → 综合裁决
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
from .law_agent import LawAgent
|
||||
from .tech_agent import TechAgent
|
||||
from .finance_agent import FinanceAgent
|
||||
from .judge_agent import JudgeAgent
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class DebateEngine:
|
||||
"""多智能体辩论编排器"""
|
||||
|
||||
def __init__(self):
|
||||
self.law_agent = LawAgent()
|
||||
self.tech_agent = TechAgent()
|
||||
self.finance_agent = FinanceAgent()
|
||||
self.judge_agent = JudgeAgent()
|
||||
self.debate_log = []
|
||||
|
||||
def run_debate(self, company_data: dict, callback=None) -> dict:
|
||||
"""
|
||||
执行完整的多智能体辩论流程
|
||||
|
||||
Args:
|
||||
company_data: 企业数据字典
|
||||
callback: 进度回调函数 callback(step, message, result)
|
||||
"""
|
||||
self.debate_log = []
|
||||
company_name = company_data.get("short_name", company_data.get("company_name", "未知"))
|
||||
start_time = time.time()
|
||||
|
||||
self._log(f"🏁 启动对 [{company_name}] 的多智能体交叉验证辩论")
|
||||
|
||||
# ==============================
|
||||
# Phase 1: 独立研判
|
||||
# ==============================
|
||||
self._log("=" * 50)
|
||||
self._log("📋 Phase 1: 各节点独立研判")
|
||||
self._log("=" * 50)
|
||||
|
||||
# 法务节点
|
||||
self._log("👩⚖️ 法务风控节点开始评估...")
|
||||
if callback:
|
||||
callback("law_start", "法务风控节点开始评估...", None)
|
||||
law_result = self.law_agent.evaluate(company_data)
|
||||
self._log(f"👩⚖️ 法务节点完成: 综合法务风险 {law_result.get('overall_law_risk', {}).get('score', '?')} 分")
|
||||
if callback:
|
||||
callback("law_done", "法务风控节点评估完成", law_result)
|
||||
|
||||
# 技术节点
|
||||
self._log("👨🔬 技术风控节点开始评估...")
|
||||
if callback:
|
||||
callback("tech_start", "技术风控节点开始评估...", None)
|
||||
tech_result = self.tech_agent.evaluate(company_data)
|
||||
self._log(f"👨🔬 技术节点完成: 综合技术风险 {tech_result.get('overall_tech_risk', {}).get('score', '?')} 分")
|
||||
if callback:
|
||||
callback("tech_done", "技术风控节点评估完成", tech_result)
|
||||
|
||||
# 财务节点
|
||||
self._log("👔 财务风控节点开始评估...")
|
||||
if callback:
|
||||
callback("fin_start", "财务风控节点开始评估...", None)
|
||||
finance_result = self.finance_agent.evaluate(company_data)
|
||||
self._log(f"👔 财务节点完成: 综合财务风险 {finance_result.get('overall_fin_risk', {}).get('score', '?')} 分")
|
||||
if callback:
|
||||
callback("fin_done", "财务风控节点评估完成", finance_result)
|
||||
|
||||
# ==============================
|
||||
# Phase 2: 交叉质证(记录冲突点)
|
||||
# ==============================
|
||||
self._log("=" * 50)
|
||||
self._log("🔄 Phase 2: 交叉质证")
|
||||
self._log("=" * 50)
|
||||
|
||||
conflicts = self._identify_conflicts(law_result, tech_result, finance_result)
|
||||
for conflict in conflicts:
|
||||
self._log(f"⚠️ 冲突: {conflict}")
|
||||
if not conflicts:
|
||||
self._log("✅ 各节点意见一致,无冲突")
|
||||
|
||||
if callback:
|
||||
callback("cross_validation", "交叉质证完成", {"conflicts": conflicts})
|
||||
|
||||
# ==============================
|
||||
# Phase 3: 综合裁决
|
||||
# ==============================
|
||||
self._log("=" * 50)
|
||||
self._log("⚖️ Phase 3: 综合裁决")
|
||||
self._log("=" * 50)
|
||||
|
||||
if callback:
|
||||
callback("judge_start", "综合裁决节点开始...", None)
|
||||
judge_result = self.judge_agent.evaluate(
|
||||
company_data, law_result, tech_result, finance_result
|
||||
)
|
||||
self._log(f"⚖️ 综合评分: {judge_result.get('comprehensive_score', '?')} 分")
|
||||
self._log(f"⚖️ 核保建议: 【{judge_result.get('underwriting_decision', '?')}】")
|
||||
if callback:
|
||||
callback("judge_done", "综合裁决完成", judge_result)
|
||||
|
||||
elapsed = time.time() - start_time
|
||||
self._log(f"🏁 辩论完成,耗时 {elapsed:.1f} 秒")
|
||||
|
||||
return {
|
||||
"company": company_name,
|
||||
"law_result": law_result,
|
||||
"tech_result": tech_result,
|
||||
"finance_result": finance_result,
|
||||
"conflicts": conflicts,
|
||||
"judge_result": judge_result,
|
||||
"debate_log": self.debate_log,
|
||||
"elapsed_seconds": round(elapsed, 1),
|
||||
}
|
||||
|
||||
def _identify_conflicts(self, law_result: dict, tech_result: dict, finance_result: dict) -> list:
|
||||
"""识别各节点之间的判定冲突"""
|
||||
conflicts = []
|
||||
|
||||
# 检查法务和技术的冲突:例如法务认为合规但技术认为路线有风险
|
||||
law_overall = law_result.get("overall_law_risk", {}).get("score", 50)
|
||||
tech_overall = tech_result.get("overall_tech_risk", {}).get("score", 50)
|
||||
fin_overall = finance_result.get("overall_fin_risk", {}).get("score", 50)
|
||||
|
||||
# 大幅分歧(差异超过30分)
|
||||
if abs(law_overall - tech_overall) > 30:
|
||||
if law_overall > tech_overall:
|
||||
conflicts.append(
|
||||
f"法务节点({law_overall}分)与技术节点({tech_overall}分)存在较大分歧: "
|
||||
f"法务认为合规风险较高,但技术面评估相对乐观"
|
||||
)
|
||||
else:
|
||||
conflicts.append(
|
||||
f"技术节点({tech_overall}分)与法务节点({law_overall}分)存在较大分歧: "
|
||||
f"技术风险较高,但法务合规状态相对可控"
|
||||
)
|
||||
|
||||
if abs(tech_overall - fin_overall) > 30:
|
||||
conflicts.append(
|
||||
f"技术节点({tech_overall}分)与财务节点({fin_overall}分)存在分歧: "
|
||||
f"需审查技术投入与财务表现的匹配度"
|
||||
)
|
||||
|
||||
# 特定维度冲突:技术认为研发投入大=好事,财务可能认为是资本化操纵
|
||||
tech_rd_view = tech_result.get("tech_iteration_pressure", {}).get("score", 50)
|
||||
fin_rd_view = finance_result.get("rd_capitalization_risk", {}).get("score", 50)
|
||||
if tech_rd_view < 40 and fin_rd_view > 60:
|
||||
conflicts.append(
|
||||
"技术节点认为研发投入合理,但财务节点发现研发资本化率异常,"
|
||||
"存在通过资本化手段美化利润的嫌疑"
|
||||
)
|
||||
|
||||
return conflicts
|
||||
|
||||
def _log(self, message: str):
|
||||
"""记录辩论日志"""
|
||||
entry = {"timestamp": time.strftime("%H:%M:%S"), "message": message}
|
||||
self.debate_log.append(entry)
|
||||
logger.info(message)
|
||||
|
||||
|
||||
def run_debate(stock_code: str) -> dict:
|
||||
"""便捷接口:通过股票代码直接运行辩论"""
|
||||
from collectors.financial_collector import get_company_by_code
|
||||
company_data = get_company_by_code(stock_code)
|
||||
if not company_data:
|
||||
return {"error": f"未找到股票代码 {stock_code} 的企业数据"}
|
||||
|
||||
engine = DebateEngine()
|
||||
return engine.run_debate(company_data)
|
||||
@@ -0,0 +1,174 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
财务风控节点
|
||||
审查维度:研发资本化操纵、客户/供应商集中度、应收账款质量、现金流
|
||||
"""
|
||||
import json
|
||||
from .base_agent import BaseAgent
|
||||
|
||||
|
||||
FIN_SYSTEM_PROMPT = """你是一名精通科创板审计规则的注册会计师(CPA),同时是金融风控专家。
|
||||
|
||||
你的任务是基于提供的企业财务数据,从财务角度穿透审查以下风险:
|
||||
1. 研发资本化操纵风险:研发资本化率是否异常,是否存在美化利润嫌疑
|
||||
2. 客户/供应商集中风险:前五大客户/供应商占比是否过高
|
||||
3. 应收账款质量:应收账款周转率是否异常,是否存在坏账风险
|
||||
4. 现金流健康度:经营现金流是否能覆盖运营需求
|
||||
|
||||
评估标准参考:
|
||||
- 科创板企业研发资本化率超过30%需重点关注
|
||||
- 前五大客户占比超过50%存在集中风险
|
||||
- 应收账款周转率低于4次/年需关注回款能力
|
||||
- 经营现金流/营收比低于0.5需关注持续经营能力
|
||||
|
||||
请输出JSON格式:
|
||||
{
|
||||
"agent": "财务风控节点",
|
||||
"rd_capitalization_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"concentration_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"receivable_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"cashflow_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"overall_fin_risk": {"score": 0-100, "level": "高/中/低"},
|
||||
"key_findings": ["..."],
|
||||
"recommendations": ["..."]
|
||||
}"""
|
||||
|
||||
|
||||
class FinanceAgent(BaseAgent):
|
||||
"""财务风控节点"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="财务风控节点",
|
||||
system_prompt=FIN_SYSTEM_PROMPT,
|
||||
role_icon="👔",
|
||||
)
|
||||
|
||||
def evaluate(self, company_data: dict) -> dict:
|
||||
"""执行财务风险评估"""
|
||||
prompt = self._build_prompt(company_data)
|
||||
result = self.infer_json(prompt)
|
||||
|
||||
if result.get("parse_error"):
|
||||
result = self._rule_based_evaluation(company_data)
|
||||
|
||||
result["agent"] = "财务风控节点"
|
||||
result["icon"] = self.role_icon
|
||||
return result
|
||||
|
||||
def _build_prompt(self, company_data: dict) -> str:
|
||||
financials = company_data.get("financials", {})
|
||||
return f"""请对以下科创企业进行财务风险穿透审查:
|
||||
|
||||
企业名称:{company_data.get('short_name', '未知')}
|
||||
行业:{company_data.get('industry', '未知')}
|
||||
|
||||
财务核心指标:
|
||||
- 营业收入: {financials.get('revenue_2024', 0):,.0f} 元
|
||||
- 净利润: {financials.get('net_profit_2024', 0):,.0f} 元
|
||||
- 研发费用: {financials.get('rd_expense_2024', 0):,.0f} 元
|
||||
- 研发资本化率: {financials.get('rd_capitalization_rate', 0):.1%}
|
||||
- 研发/营收比: {financials.get('rd_revenue_ratio', 0):.1%}
|
||||
- 前五大客户占比: {financials.get('top5_customer_ratio', 0):.1%}
|
||||
- 前五大供应商占比: {financials.get('top5_supplier_ratio', 0):.1%}
|
||||
- 应收账款周转率: {financials.get('receivable_turnover', 0):.1f} 次/年
|
||||
- 经营现金流比率: {financials.get('cash_flow_ratio', 0):.2f}
|
||||
|
||||
请输出严格的JSON评估结果。"""
|
||||
|
||||
def _rule_based_evaluation(self, company_data: dict) -> dict:
|
||||
"""基于规则的财务风险评估"""
|
||||
fin = company_data.get("financials", {})
|
||||
|
||||
# 1. 研发资本化操纵风险
|
||||
cap_rate = fin.get("rd_capitalization_rate", 0)
|
||||
if cap_rate >= 0.4:
|
||||
rd_score = 90
|
||||
rd_detail = f"研发资本化率高达{cap_rate:.0%},严重怀疑美化利润"
|
||||
elif cap_rate >= 0.3:
|
||||
rd_score = 70
|
||||
rd_detail = f"研发资本化率{cap_rate:.0%},超过行业警戒线(30%),需重点审查"
|
||||
elif cap_rate >= 0.15:
|
||||
rd_score = 45
|
||||
rd_detail = f"研发资本化率{cap_rate:.0%},处于中等水平,建议关注趋势"
|
||||
elif cap_rate > 0:
|
||||
rd_score = 25
|
||||
rd_detail = f"研发资本化率{cap_rate:.0%},处于合理范围"
|
||||
else:
|
||||
rd_score = 10
|
||||
rd_detail = "研发费用全部费用化处理,财务政策审慎"
|
||||
|
||||
# 2. 集中度风险
|
||||
customer_ratio = fin.get("top5_customer_ratio", 0)
|
||||
supplier_ratio = fin.get("top5_supplier_ratio", 0)
|
||||
max_concentration = max(customer_ratio, supplier_ratio)
|
||||
|
||||
if max_concentration >= 0.8:
|
||||
conc_score = 90
|
||||
conc_detail = f"前五大客户占比{customer_ratio:.0%},供应商占比{supplier_ratio:.0%},集中度极高"
|
||||
elif max_concentration >= 0.6:
|
||||
conc_score = 70
|
||||
conc_detail = f"前五大客户占比{customer_ratio:.0%},供应商占比{supplier_ratio:.0%},集中度偏高"
|
||||
elif max_concentration >= 0.4:
|
||||
conc_score = 45
|
||||
conc_detail = f"前五大客户占比{customer_ratio:.0%},供应商占比{supplier_ratio:.0%},中等集中度"
|
||||
else:
|
||||
conc_score = 20
|
||||
conc_detail = f"客户和供应商分布较为分散"
|
||||
|
||||
# 3. 应收账款风险
|
||||
turnover = fin.get("receivable_turnover", 8)
|
||||
if turnover < 3:
|
||||
recv_score = 80
|
||||
recv_detail = f"应收账款周转率仅{turnover:.1f}次/年,回款能力极差"
|
||||
elif turnover < 5:
|
||||
recv_score = 55
|
||||
recv_detail = f"应收账款周转率{turnover:.1f}次/年,回款速度偏慢"
|
||||
elif turnover < 8:
|
||||
recv_score = 30
|
||||
recv_detail = f"应收账款周转率{turnover:.1f}次/年,回款能力尚可"
|
||||
else:
|
||||
recv_score = 15
|
||||
recv_detail = f"应收账款周转率{turnover:.1f}次/年,回款能力良好"
|
||||
|
||||
# 4. 现金流风险
|
||||
cf_ratio = fin.get("cash_flow_ratio", 1.0)
|
||||
if cf_ratio < 0.5:
|
||||
cf_score = 80
|
||||
cf_detail = f"经营现金流比率仅{cf_ratio:.2f},存在持续经营风险"
|
||||
elif cf_ratio < 0.8:
|
||||
cf_score = 55
|
||||
cf_detail = f"经营现金流比率{cf_ratio:.2f},现金流偏紧"
|
||||
elif cf_ratio < 1.2:
|
||||
cf_score = 30
|
||||
cf_detail = f"经营现金流比率{cf_ratio:.2f},基本健康"
|
||||
else:
|
||||
cf_score = 15
|
||||
cf_detail = f"经营现金流比率{cf_ratio:.2f},现金流充裕"
|
||||
|
||||
overall = int(rd_score * 0.30 + conc_score * 0.30 +
|
||||
recv_score * 0.20 + cf_score * 0.20)
|
||||
|
||||
findings = [rd_detail, conc_detail]
|
||||
if recv_score >= 50:
|
||||
findings.append(recv_detail)
|
||||
if cf_score >= 50:
|
||||
findings.append(cf_detail)
|
||||
|
||||
return {
|
||||
"rd_capitalization_risk": {"score": rd_score, "level": self._level(rd_score), "detail": rd_detail},
|
||||
"concentration_risk": {"score": conc_score, "level": self._level(conc_score), "detail": conc_detail},
|
||||
"receivable_risk": {"score": recv_score, "level": self._level(recv_score), "detail": recv_detail},
|
||||
"cashflow_risk": {"score": cf_score, "level": self._level(cf_score), "detail": cf_detail},
|
||||
"overall_fin_risk": {"score": overall, "level": self._level(overall)},
|
||||
"key_findings": findings,
|
||||
"recommendations": ["关注研发资本化率变化趋势", "降低客户集中度风险"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _level(score: int) -> str:
|
||||
if score >= 70:
|
||||
return "高"
|
||||
elif score >= 40:
|
||||
return "中"
|
||||
return "低"
|
||||
@@ -0,0 +1,176 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
综合裁决节点
|
||||
汇总法务/技术/财务三方意见,消解冲突,输出最终综合评级
|
||||
"""
|
||||
import json
|
||||
from .base_agent import BaseAgent
|
||||
|
||||
|
||||
JUDGE_SYSTEM_PROMPT = """你是一名资深的风险管理委员会主席,负责汇总法务、技术、财务三方专家的研判意见。
|
||||
|
||||
你的任务是:
|
||||
1. 审阅三方专家的评估报告
|
||||
2. 识别各方意见的冲突点
|
||||
3. 基于优先级原则消解冲突(合规风险 > 技术风险 > 财务风险)
|
||||
4. 输出 0-100 的综合风险评分
|
||||
5. 给出最终核保建议
|
||||
|
||||
核保决策标准:
|
||||
- 综合风险 ≥ 80分:建议【拒绝承保】
|
||||
- 60 ≤ 综合风险 < 80分:建议【附条件承保】(高免赔额/限额)
|
||||
- 40 ≤ 综合风险 < 60分:建议【标准承保】(标准费率上浮)
|
||||
- 综合风险 < 40分:建议【优先承保】(可享费率优惠)
|
||||
|
||||
请输出JSON格式:
|
||||
{
|
||||
"comprehensive_score": 0-100,
|
||||
"risk_level": "极高/高/中/低",
|
||||
"underwriting_decision": "拒绝承保/附条件承保/标准承保/优先承保",
|
||||
"six_dimension_scores": {
|
||||
"tech_disruption": 0-100,
|
||||
"talent_loss": 0-100,
|
||||
"algo_compliance": 0-100,
|
||||
"geopolitical": 0-100,
|
||||
"rd_capitalization": 0-100,
|
||||
"concentration": 0-100
|
||||
},
|
||||
"conflict_resolution": "...",
|
||||
"key_risks": ["..."],
|
||||
"underwriting_conditions": ["..."],
|
||||
"summary": "..."
|
||||
}"""
|
||||
|
||||
|
||||
class JudgeAgent(BaseAgent):
|
||||
"""综合裁决节点"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="综合裁决节点",
|
||||
system_prompt=JUDGE_SYSTEM_PROMPT,
|
||||
role_icon="⚖️",
|
||||
)
|
||||
|
||||
def evaluate(self, company_data: dict, law_result: dict,
|
||||
tech_result: dict, finance_result: dict) -> dict:
|
||||
"""汇总三方意见,输出综合裁决"""
|
||||
prompt = self._build_prompt(company_data, law_result, tech_result, finance_result)
|
||||
result = self.infer_json(prompt)
|
||||
|
||||
if result.get("parse_error"):
|
||||
result = self._rule_based_evaluation(company_data, law_result, tech_result, finance_result)
|
||||
|
||||
result["agent"] = "综合裁决节点"
|
||||
result["icon"] = self.role_icon
|
||||
return result
|
||||
|
||||
def _build_prompt(self, company_data: dict, law_result: dict,
|
||||
tech_result: dict, finance_result: dict) -> str:
|
||||
return f"""请对以下科创企业的三方评估结果进行综合裁决:
|
||||
|
||||
企业名称:{company_data.get('short_name', '未知')}
|
||||
行业:{company_data.get('industry', '未知')}
|
||||
|
||||
=== 👩⚖️ 法务风控节点评估 ===
|
||||
{json.dumps(law_result, ensure_ascii=False, indent=2)}
|
||||
|
||||
=== 👨🔬 技术风控节点评估 ===
|
||||
{json.dumps(tech_result, ensure_ascii=False, indent=2)}
|
||||
|
||||
=== 👔 财务风控节点评估 ===
|
||||
{json.dumps(finance_result, ensure_ascii=False, indent=2)}
|
||||
|
||||
请消解可能存在的判定冲突,输出综合裁决JSON。"""
|
||||
|
||||
def _rule_based_evaluation(self, company_data: dict, law_result: dict,
|
||||
tech_result: dict, finance_result: dict) -> dict:
|
||||
"""规则引擎综合裁决"""
|
||||
|
||||
# 提取各维度得分
|
||||
def safe_score(result: dict, key: str) -> int:
|
||||
item = result.get(key, {})
|
||||
if isinstance(item, dict):
|
||||
return item.get("score", 50)
|
||||
return 50
|
||||
|
||||
# 六维评分
|
||||
scores = {
|
||||
"tech_disruption": safe_score(tech_result, "tech_disruption_risk"),
|
||||
"talent_loss": safe_score(tech_result, "talent_loss_risk"),
|
||||
"algo_compliance": safe_score(law_result, "algo_compliance_risk"),
|
||||
"geopolitical": safe_score(law_result, "geopolitical_risk"),
|
||||
"rd_capitalization": safe_score(finance_result, "rd_capitalization_risk"),
|
||||
"concentration": safe_score(finance_result, "concentration_risk"),
|
||||
}
|
||||
|
||||
# 加权综合得分
|
||||
weights = {
|
||||
"tech_disruption": 0.20,
|
||||
"talent_loss": 0.15,
|
||||
"algo_compliance": 0.15,
|
||||
"geopolitical": 0.20,
|
||||
"rd_capitalization": 0.15,
|
||||
"concentration": 0.15,
|
||||
}
|
||||
|
||||
comprehensive_score = int(
|
||||
sum(scores[k] * weights[k] for k in scores)
|
||||
)
|
||||
|
||||
# 裁决
|
||||
if comprehensive_score >= 80:
|
||||
decision = "拒绝承保"
|
||||
risk_level = "极高"
|
||||
elif comprehensive_score >= 60:
|
||||
decision = "附条件承保"
|
||||
risk_level = "高"
|
||||
elif comprehensive_score >= 40:
|
||||
decision = "标准承保"
|
||||
risk_level = "中"
|
||||
else:
|
||||
decision = "优先承保"
|
||||
risk_level = "低"
|
||||
|
||||
# 收集关键风险
|
||||
key_risks = []
|
||||
for dim, score in sorted(scores.items(), key=lambda x: x[1], reverse=True):
|
||||
if score >= 60:
|
||||
dim_names = {
|
||||
"tech_disruption": "技术路线颠覆",
|
||||
"talent_loss": "核心人员流失",
|
||||
"algo_compliance": "算法/数据合规",
|
||||
"geopolitical": "地缘政治/出口管制",
|
||||
"rd_capitalization": "研发资本化操纵",
|
||||
"concentration": "客户/供应商集中",
|
||||
}
|
||||
key_risks.append(f"{dim_names.get(dim, dim)}风险({score}分)")
|
||||
|
||||
# 核保条件
|
||||
conditions = []
|
||||
if scores["geopolitical"] >= 70:
|
||||
conditions.append("要求提供出口管制合规声明及供应链替代方案")
|
||||
if scores["rd_capitalization"] >= 60:
|
||||
conditions.append("要求额外提供研发资本化会计政策说明及审计意见")
|
||||
if scores["concentration"] >= 60:
|
||||
conditions.append("要求提供客户分散化计划或前五大客户信用报告")
|
||||
if scores["talent_loss"] >= 60:
|
||||
conditions.append("要求核心技术人员签署竞业协议且公司有留任激励计划")
|
||||
|
||||
company_name = company_data.get("short_name", "该企业")
|
||||
summary = (
|
||||
f"{company_name}综合风险评分{comprehensive_score}分(风险等级:{risk_level})。"
|
||||
f"核保建议:【{decision}】。"
|
||||
f"主要风险集中在{'、'.join(key_risks[:3]) if key_risks else '无突出风险'}。"
|
||||
)
|
||||
|
||||
return {
|
||||
"comprehensive_score": comprehensive_score,
|
||||
"risk_level": risk_level,
|
||||
"underwriting_decision": decision,
|
||||
"six_dimension_scores": scores,
|
||||
"conflict_resolution": "基于优先级原则(合规>技术>财务)进行加权裁决",
|
||||
"key_risks": key_risks,
|
||||
"underwriting_conditions": conditions,
|
||||
"summary": summary,
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
法务风控节点
|
||||
审查维度:算法备案状态、实体清单命中、数据出境风险、知识产权诉讼
|
||||
"""
|
||||
import json
|
||||
from .base_agent import BaseAgent
|
||||
|
||||
|
||||
LAW_SYSTEM_PROMPT = """你是一名资深法务风控专家,精通以下法律法规:
|
||||
- 《生成式人工智能服务管理暂行办法》
|
||||
- 《互联网信息服务算法推荐管理规定》
|
||||
- 《数据安全法》《个人信息保护法》
|
||||
- 《出口管制法》及美国 BIS 实体清单相关规则
|
||||
- 《科创板上市规则》中的合规要求
|
||||
|
||||
你的任务是基于提供的企业数据,从法律合规角度评估以下风险:
|
||||
1. 算法备案合规风险:企业是否涉及AI业务但未完成算法备案
|
||||
2. 地缘政治与出口管制风险:企业或其供应链是否受到制裁
|
||||
3. 数据合规风险:是否存在数据出境、数据安全方面的隐患
|
||||
4. 知识产权诉讼风险:是否面临重大IP纠纷
|
||||
|
||||
请以严谨的法律视角进行评估,输出JSON格式:
|
||||
{
|
||||
"agent": "法务风控节点",
|
||||
"algo_compliance_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"geopolitical_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"data_compliance_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"ip_litigation_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"overall_law_risk": {"score": 0-100, "level": "高/中/低"},
|
||||
"key_findings": ["..."],
|
||||
"recommendations": ["..."]
|
||||
}"""
|
||||
|
||||
|
||||
class LawAgent(BaseAgent):
|
||||
"""法务风控节点"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="法务风控节点",
|
||||
system_prompt=LAW_SYSTEM_PROMPT,
|
||||
role_icon="👩⚖️",
|
||||
)
|
||||
|
||||
def evaluate(self, company_data: dict) -> dict:
|
||||
"""执行法务风险评估"""
|
||||
prompt = self._build_prompt(company_data)
|
||||
result = self.infer_json(prompt)
|
||||
|
||||
# 如果 JSON 解析失败,使用规则引擎
|
||||
if result.get("parse_error"):
|
||||
result = self._rule_based_evaluation(company_data)
|
||||
|
||||
result["agent"] = "法务风控节点"
|
||||
result["icon"] = self.role_icon
|
||||
return result
|
||||
|
||||
def _build_prompt(self, company_data: dict) -> str:
|
||||
"""构建评估提示词"""
|
||||
return f"""请对以下科创企业进行法务风险评估:
|
||||
|
||||
企业名称:{company_data.get('short_name', company_data.get('company_name', '未知'))}
|
||||
行业:{company_data.get('industry', '未知')}
|
||||
领域:{company_data.get('sector', '未知')}
|
||||
|
||||
合规状态:
|
||||
- 算法备案:{json.dumps(company_data.get('compliance', {}), ensure_ascii=False)}
|
||||
|
||||
供应链信息:
|
||||
- 关键供应商:{json.dumps(company_data.get('supply_chain', {}).get('key_suppliers', []), ensure_ascii=False)}
|
||||
- 供应商集中度风险:{company_data.get('supply_chain', {}).get('supplier_concentration_risk', '未知')}
|
||||
|
||||
技术路线:
|
||||
{json.dumps(company_data.get('tech_route', {}), ensure_ascii=False)}
|
||||
|
||||
请输出严格的JSON评估结果。"""
|
||||
|
||||
def fallback_inference(self, prompt: str) -> str:
|
||||
"""规则引擎降级"""
|
||||
return json.dumps(self._rule_based_evaluation({}), ensure_ascii=False)
|
||||
|
||||
def _rule_based_evaluation(self, company_data: dict) -> dict:
|
||||
"""基于规则的法务风险评估"""
|
||||
compliance = company_data.get("compliance", {})
|
||||
supply_chain = company_data.get("supply_chain", {})
|
||||
sector = company_data.get("sector", "")
|
||||
|
||||
# 算法备案风险
|
||||
algo_status = compliance.get("algo_filing_status", "")
|
||||
algo_score = 20
|
||||
algo_detail = "合规状态正常"
|
||||
if sector in ["AI", "软件", "互联网"] and algo_status == "不适用":
|
||||
algo_score = 60
|
||||
algo_detail = "涉及AI业务但标注为不适用,建议核实"
|
||||
elif "未" in algo_status or not algo_status:
|
||||
algo_score = 80
|
||||
algo_detail = "未查到算法备案记录,存在合规风险"
|
||||
elif "已备案" in algo_status:
|
||||
algo_score = 10
|
||||
algo_detail = "已完成算法备案"
|
||||
|
||||
# 地缘政治风险
|
||||
entity_status = compliance.get("entity_list_status", "")
|
||||
geo_score = 15
|
||||
geo_detail = "未受出口管制影响"
|
||||
if "被列入" in entity_status:
|
||||
geo_score = 95
|
||||
geo_detail = f"已被列入实体清单: {compliance.get('sanctions_detail', '')}"
|
||||
elif supply_chain.get("supplier_concentration_risk") == "极高":
|
||||
geo_score = 70
|
||||
geo_detail = "核心供应链高度依赖海外,存在间接制裁风险"
|
||||
|
||||
# 数据合规风险
|
||||
data_risk = compliance.get("data_export_risk", "低")
|
||||
data_score = {"高": 75, "中": 45, "低": 15}.get(data_risk, 20)
|
||||
data_detail = f"数据出境风险等级: {data_risk}"
|
||||
|
||||
# 知识产权风险
|
||||
ip_score = 25
|
||||
ip_detail = "未发现重大IP纠纷"
|
||||
|
||||
# 综合法务风险
|
||||
overall_score = int(
|
||||
algo_score * 0.25 + geo_score * 0.35 +
|
||||
data_score * 0.25 + ip_score * 0.15
|
||||
)
|
||||
|
||||
return {
|
||||
"algo_compliance_risk": {"score": algo_score, "level": self._level(algo_score), "detail": algo_detail},
|
||||
"geopolitical_risk": {"score": geo_score, "level": self._level(geo_score), "detail": geo_detail},
|
||||
"data_compliance_risk": {"score": data_score, "level": self._level(data_score), "detail": data_detail},
|
||||
"ip_litigation_risk": {"score": ip_score, "level": self._level(ip_score), "detail": ip_detail},
|
||||
"overall_law_risk": {"score": overall_score, "level": self._level(overall_score)},
|
||||
"key_findings": [algo_detail, geo_detail, data_detail],
|
||||
"recommendations": ["建议定期审查合规状态", "关注实体清单更新动态"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _level(score: int) -> str:
|
||||
if score >= 70:
|
||||
return "高"
|
||||
elif score >= 40:
|
||||
return "中"
|
||||
return "低"
|
||||
@@ -0,0 +1,147 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
技术风控节点
|
||||
审查维度:技术路线竞争态势、核心人员稳定性、专利布局、技术替代风险
|
||||
"""
|
||||
import json
|
||||
from .base_agent import BaseAgent
|
||||
|
||||
|
||||
TECH_SYSTEM_PROMPT = """你是一名资深科技行业分析师和技术风控专家。
|
||||
你精通半导体、人工智能、新能源、生物医疗等前沿科技领域的技术演进趋势。
|
||||
|
||||
你的任务是基于提供的企业数据,从技术角度评估以下风险:
|
||||
1. 技术路线颠覆风险:企业押注的技术路线是否面临被替代的风险
|
||||
2. 核心人员流失风险:关键技术人员的稳定性和不可替代性
|
||||
3. 专利/技术壁垒:技术护城河的深度和可持续性
|
||||
4. 技术迭代压力:行业技术迭代速度对企业的冲击
|
||||
|
||||
请以技术专家的视角进行深度评估,输出JSON格式:
|
||||
{
|
||||
"agent": "技术风控节点",
|
||||
"tech_disruption_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"talent_loss_risk": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"patent_moat": {"score": 0-100, "level": "强/中/弱", "detail": "..."},
|
||||
"tech_iteration_pressure": {"score": 0-100, "level": "高/中/低", "detail": "..."},
|
||||
"overall_tech_risk": {"score": 0-100, "level": "高/中/低"},
|
||||
"key_findings": ["..."],
|
||||
"recommendations": ["..."]
|
||||
}"""
|
||||
|
||||
|
||||
class TechAgent(BaseAgent):
|
||||
"""技术风控节点"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__(
|
||||
name="技术风控节点",
|
||||
system_prompt=TECH_SYSTEM_PROMPT,
|
||||
role_icon="👨🔬",
|
||||
)
|
||||
|
||||
def evaluate(self, company_data: dict) -> dict:
|
||||
"""执行技术风险评估"""
|
||||
prompt = self._build_prompt(company_data)
|
||||
result = self.infer_json(prompt)
|
||||
|
||||
if result.get("parse_error"):
|
||||
result = self._rule_based_evaluation(company_data)
|
||||
|
||||
result["agent"] = "技术风控节点"
|
||||
result["icon"] = self.role_icon
|
||||
return result
|
||||
|
||||
def _build_prompt(self, company_data: dict) -> str:
|
||||
return f"""请对以下科创企业进行技术风险评估:
|
||||
|
||||
企业名称:{company_data.get('short_name', '未知')}
|
||||
行业:{company_data.get('industry', '未知')}
|
||||
领域:{company_data.get('sector', '未知')}
|
||||
企业描述:{company_data.get('description', '')}
|
||||
|
||||
技术路线信息:
|
||||
{json.dumps(company_data.get('tech_route', {}), ensure_ascii=False, indent=2)}
|
||||
|
||||
核心技术人员:
|
||||
{json.dumps(company_data.get('core_tech_personnel', []), ensure_ascii=False, indent=2)}
|
||||
|
||||
财务中的研发指标:
|
||||
- 研发费用: {company_data.get('financials', {}).get('rd_expense_2024', 0)}
|
||||
- 研发营收比: {company_data.get('financials', {}).get('rd_revenue_ratio', 0)}
|
||||
|
||||
请输出严格的JSON评估结果。"""
|
||||
|
||||
def _rule_based_evaluation(self, company_data: dict) -> dict:
|
||||
"""基于规则的技术风险评估"""
|
||||
tech_route = company_data.get("tech_route", {})
|
||||
personnel = company_data.get("core_tech_personnel", [])
|
||||
financials = company_data.get("financials", {})
|
||||
|
||||
# 技术路线颠覆风险
|
||||
competing_techs = tech_route.get("competing_techs", [])
|
||||
disruption_score = min(20 + len(competing_techs) * 15, 90)
|
||||
tech_moat = tech_route.get("tech_moat", "")
|
||||
if "差距" in tech_moat or "受制" in tech_moat:
|
||||
disruption_score = min(disruption_score + 20, 95)
|
||||
disruption_detail = f"面临 {len(competing_techs)} 条竞争技术路线: {', '.join(competing_techs[:3])}"
|
||||
|
||||
# 核心人员流失风险
|
||||
talent_score = 20
|
||||
talent_detail = "核心团队稳定"
|
||||
departed = [p for p in personnel if "离职" in p.get("status", "")]
|
||||
high_importance = [p for p in personnel if p.get("importance") == "极高"]
|
||||
|
||||
if departed:
|
||||
talent_score = 80
|
||||
talent_detail = f"已有核心人员离职: {', '.join(p['name'] for p in departed)}"
|
||||
elif len(high_importance) == 1:
|
||||
talent_score = 55
|
||||
talent_detail = f"高度依赖单一核心人员: {high_importance[0]['name']}"
|
||||
elif len(personnel) <= 2:
|
||||
talent_score = 45
|
||||
talent_detail = "核心技术团队规模偏小"
|
||||
|
||||
# 专利壁垒
|
||||
patent_count = tech_route.get("patent_count", 0)
|
||||
if patent_count > 5000:
|
||||
patent_score = 20
|
||||
patent_detail = f"专利数量充足({patent_count}件),技术壁垒较强"
|
||||
elif patent_count > 1000:
|
||||
patent_score = 35
|
||||
patent_detail = f"专利数量中等({patent_count}件)"
|
||||
else:
|
||||
patent_score = 60
|
||||
patent_detail = f"专利数量偏少({patent_count}件),技术壁垒偏弱"
|
||||
|
||||
# 技术迭代压力(基于研发投入比)
|
||||
rd_ratio = financials.get("rd_revenue_ratio", 0)
|
||||
if rd_ratio > 0.3:
|
||||
iter_score = 65
|
||||
iter_detail = f"研发营收比极高({rd_ratio:.1%}),说明行业技术迭代压力大"
|
||||
elif rd_ratio > 0.15:
|
||||
iter_score = 45
|
||||
iter_detail = f"研发投入较高({rd_ratio:.1%}),需持续技术投入"
|
||||
else:
|
||||
iter_score = 25
|
||||
iter_detail = f"研发投入适中({rd_ratio:.1%})"
|
||||
|
||||
overall = int(disruption_score * 0.35 + talent_score * 0.25 +
|
||||
patent_score * 0.15 + iter_score * 0.25)
|
||||
|
||||
return {
|
||||
"tech_disruption_risk": {"score": disruption_score, "level": self._level(disruption_score), "detail": disruption_detail},
|
||||
"talent_loss_risk": {"score": talent_score, "level": self._level(talent_score), "detail": talent_detail},
|
||||
"patent_moat": {"score": patent_score, "level": "弱" if patent_score >= 50 else ("中" if patent_score >= 30 else "强"), "detail": patent_detail},
|
||||
"tech_iteration_pressure": {"score": iter_score, "level": self._level(iter_score), "detail": iter_detail},
|
||||
"overall_tech_risk": {"score": overall, "level": self._level(overall)},
|
||||
"key_findings": [disruption_detail, talent_detail, patent_detail],
|
||||
"recommendations": ["关注竞争技术路线发展", "加强核心人员留任激励"],
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def _level(score: int) -> str:
|
||||
if score >= 70:
|
||||
return "高"
|
||||
elif score >= 40:
|
||||
return "中"
|
||||
return "低"
|
||||
@@ -0,0 +1,13 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
🛡️ 科创企业智能风控与核保系统 - 主入口转接脚本
|
||||
自动重定向并加载 `🏠_系统首页.py`
|
||||
"""
|
||||
import runpy
|
||||
import os
|
||||
import sys
|
||||
|
||||
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
||||
|
||||
home_script = os.path.join(os.path.dirname(os.path.abspath(__file__)), "🏠_系统首页.py")
|
||||
runpy.run_path(home_script, run_name="__main__")
|
||||
@@ -0,0 +1,28 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import re
|
||||
import os
|
||||
|
||||
SCRIPT_PATH = r"d:\code\XH-202626_科创企业特有风险的识别与管理\projects\XH-202626-presentation_ppt169_20260727\scratch\generate_all_svgs.py"
|
||||
|
||||
def clean_script():
|
||||
with open(SCRIPT_PATH, 'r', encoding='utf-8') as f:
|
||||
content = f.read()
|
||||
|
||||
# 1. 替换 svg_footer 为空
|
||||
old_footer_pattern = r'def svg_footer\(page_num\):[\s\S]*?\'\'\''
|
||||
new_footer = 'def svg_footer(page_num):\n return ""'
|
||||
content = re.sub(old_footer_pattern, new_footer, content)
|
||||
|
||||
# 2. 剔除 Emoji
|
||||
emoji_chars = ['📄', '📊', '📋', '💻', '🔗', '⚡', '🏛', '🔬', '👑', '💰', '⚖', '🤖', '👔', '⚠️', '⚠', '🚀', '👩', '✅', '🌍', '📈', '👨', '💡', '🕸', '🎯', '👁', '🔍', '🛡', '👉']
|
||||
for em in emoji_chars:
|
||||
content = content.replace(f"{em} ", "")
|
||||
content = content.replace(f"{em}", "")
|
||||
|
||||
with open(SCRIPT_PATH, 'w', encoding='utf-8') as f:
|
||||
f.write(content)
|
||||
|
||||
print("✅ 成功移除所有 Emoji 与页脚脚注/页码!")
|
||||
|
||||
if __name__ == '__main__':
|
||||
clean_script()
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""数据采集模块"""
|
||||
@@ -0,0 +1,188 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
年报 PDF 文本解析模块
|
||||
从年报中提取关键风险信息:核心技术人员、技术路线、诉讼、风险提示等
|
||||
"""
|
||||
import re
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def parse_annual_report(pdf_path: str) -> dict:
|
||||
"""
|
||||
解析年报 PDF,提取关键风险相关信息
|
||||
返回结构化的风险要素字典
|
||||
"""
|
||||
text = _extract_text_from_pdf(pdf_path)
|
||||
if not text:
|
||||
return {"error": "PDF 解析失败", "raw_text": ""}
|
||||
|
||||
return {
|
||||
"core_personnel_info": _extract_core_personnel(text),
|
||||
"tech_route_info": _extract_tech_route(text),
|
||||
"litigation_info": _extract_litigation(text),
|
||||
"risk_factors": _extract_risk_factors(text),
|
||||
"rd_capitalization_info": _extract_rd_capitalization(text),
|
||||
"customer_concentration": _extract_customer_concentration(text),
|
||||
"raw_text_length": len(text),
|
||||
}
|
||||
|
||||
|
||||
def _extract_text_from_pdf(pdf_path: str) -> Optional[str]:
|
||||
"""使用 pdfplumber 提取 PDF 全文"""
|
||||
try:
|
||||
import pdfplumber
|
||||
text_parts = []
|
||||
with pdfplumber.open(pdf_path) as pdf:
|
||||
for page in pdf.pages:
|
||||
page_text = page.extract_text()
|
||||
if page_text:
|
||||
text_parts.append(page_text)
|
||||
return "\n".join(text_parts)
|
||||
except ImportError:
|
||||
logger.warning("pdfplumber 未安装,尝试 PyPDF2")
|
||||
try:
|
||||
from PyPDF2 import PdfReader
|
||||
reader = PdfReader(pdf_path)
|
||||
return "\n".join(
|
||||
page.extract_text() or "" for page in reader.pages
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"PyPDF2 解析失败: {e}")
|
||||
except Exception as e:
|
||||
logger.error(f"PDF 解析失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
def _extract_core_personnel(text: str) -> dict:
|
||||
"""提取核心技术人员相关信息"""
|
||||
result = {
|
||||
"has_departure": False,
|
||||
"departure_details": [],
|
||||
"personnel_count": 0,
|
||||
"key_mentions": [],
|
||||
}
|
||||
|
||||
# 匹配离职/辞职相关表述
|
||||
departure_patterns = [
|
||||
r"(核心技术人员|核心人员|关键技术人员).{0,30}(离职|辞职|离任|不再担任)",
|
||||
r"(CTO|首席技术官|技术总监|研发总监).{0,30}(离职|辞职|离任)",
|
||||
r"(离职|辞职).{0,30}(核心技术人员|核心人员)",
|
||||
]
|
||||
for pattern in departure_patterns:
|
||||
matches = re.findall(pattern, text)
|
||||
if matches:
|
||||
result["has_departure"] = True
|
||||
result["departure_details"].extend([str(m) for m in matches])
|
||||
|
||||
# 统计核心技术人员数量
|
||||
count_match = re.search(r"核心技术人员\s*(\d+)\s*[名人]", text)
|
||||
if count_match:
|
||||
result["personnel_count"] = int(count_match.group(1))
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _extract_tech_route(text: str) -> dict:
|
||||
"""提取技术路线相关信息"""
|
||||
result = {
|
||||
"competing_tech_mentioned": False,
|
||||
"tech_keywords": [],
|
||||
"risk_mentions": [],
|
||||
}
|
||||
|
||||
# 技术竞争关键词
|
||||
tech_keywords = [
|
||||
"技术路线", "技术迭代", "技术替代", "技术颠覆",
|
||||
"竞争技术", "替代方案", "新一代技术",
|
||||
]
|
||||
for kw in tech_keywords:
|
||||
if kw in text:
|
||||
result["tech_keywords"].append(kw)
|
||||
result["competing_tech_mentioned"] = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _extract_litigation(text: str) -> dict:
|
||||
"""提取诉讼/仲裁相关信息"""
|
||||
result = {
|
||||
"has_litigation": False,
|
||||
"litigation_count": 0,
|
||||
"ip_related": False,
|
||||
}
|
||||
|
||||
# 诉讼关键词
|
||||
litigation_patterns = [
|
||||
r"(诉讼|仲裁|起诉|被告).{0,50}(知识产权|专利|商标|著作权)",
|
||||
r"(专利侵权|商标侵权|著作权纠纷)",
|
||||
]
|
||||
for pattern in litigation_patterns:
|
||||
if re.search(pattern, text):
|
||||
result["has_litigation"] = True
|
||||
result["ip_related"] = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _extract_risk_factors(text: str) -> list:
|
||||
"""提取风险因素章节的关键信息"""
|
||||
risk_keywords = [
|
||||
"地缘政治", "实体清单", "出口管制", "贸易摩擦",
|
||||
"数据安全", "数据合规", "算法备案", "数据出境",
|
||||
"客户集中", "供应商集中", "单一客户", "单一供应商",
|
||||
"研发资本化", "开发支出", "无形资产",
|
||||
"人才流失", "核心人员", "竞业限制",
|
||||
]
|
||||
found_risks = []
|
||||
for kw in risk_keywords:
|
||||
if kw in text:
|
||||
found_risks.append(kw)
|
||||
return found_risks
|
||||
|
||||
|
||||
def _extract_rd_capitalization(text: str) -> dict:
|
||||
"""提取研发资本化相关信息"""
|
||||
result = {
|
||||
"has_capitalization": False,
|
||||
"capitalization_mentioned": False,
|
||||
"amount_keywords": [],
|
||||
}
|
||||
|
||||
cap_keywords = ["开发支出", "研发资本化", "资本化研发", "开发阶段支出"]
|
||||
for kw in cap_keywords:
|
||||
if kw in text:
|
||||
result["capitalization_mentioned"] = True
|
||||
result["amount_keywords"].append(kw)
|
||||
|
||||
# 检查是否有具体的资本化金额
|
||||
cap_amount = re.search(r"开发支出.{0,30}([\d,\.]+)\s*(万元|百万|亿)", text)
|
||||
if cap_amount:
|
||||
result["has_capitalization"] = True
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _extract_customer_concentration(text: str) -> dict:
|
||||
"""提取客户/供应商集中度信息"""
|
||||
result = {
|
||||
"top5_customer_ratio": None,
|
||||
"top5_supplier_ratio": None,
|
||||
"single_customer_dependency": False,
|
||||
}
|
||||
|
||||
# 前五大客户占比
|
||||
customer_match = re.search(
|
||||
r"前五[名大]客户.{0,30}([\d\.]+)\s*%", text
|
||||
)
|
||||
if customer_match:
|
||||
result["top5_customer_ratio"] = float(customer_match.group(1)) / 100
|
||||
|
||||
# 单一客户依赖
|
||||
if re.search(r"(第一大客户|最大客户).{0,30}([\d\.]+)\s*%", text):
|
||||
result["single_customer_dependency"] = True
|
||||
|
||||
return result
|
||||
@@ -0,0 +1,87 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
算法备案合规数据采集与查询模块
|
||||
匹配企业是否已完成网信办算法备案
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||
|
||||
|
||||
def _load_algo_filings() -> list:
|
||||
"""加载算法备案数据"""
|
||||
filepath = DATA_DIR / "algo_filings.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def check_algo_filing(company_name: str) -> dict:
|
||||
"""
|
||||
查询企业的算法备案状态
|
||||
"""
|
||||
filings = _load_algo_filings()
|
||||
result = {
|
||||
"has_filing": False,
|
||||
"filings": [],
|
||||
"needs_filing": False, # 是否需要备案但未备案
|
||||
"risk_level": "低",
|
||||
}
|
||||
|
||||
for filing in filings:
|
||||
if company_name in filing["company"] or filing["company"] in company_name:
|
||||
result["has_filing"] = True
|
||||
result["filings"].append(filing)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def assess_algo_compliance_risk(company_data: dict) -> dict:
|
||||
"""
|
||||
综合评估企业的算法合规风险
|
||||
考虑因素:是否涉及 AI 业务、是否已备案、数据出境风险
|
||||
"""
|
||||
company_name = company_data.get("short_name", company_data.get("company_name", ""))
|
||||
sector = company_data.get("sector", "")
|
||||
compliance = company_data.get("compliance", {})
|
||||
|
||||
# 查询备案状态
|
||||
filing_status = check_algo_filing(company_name)
|
||||
|
||||
# 判断是否需要备案
|
||||
ai_related_sectors = ["AI", "软件", "互联网", "消费电子"]
|
||||
needs_filing = sector in ai_related_sectors or "AI" in str(company_data.get("tech_route", {}))
|
||||
|
||||
# 综合评估
|
||||
risk_level = "低"
|
||||
risk_details = []
|
||||
|
||||
if needs_filing and not filing_status["has_filing"]:
|
||||
algo_status = compliance.get("algo_filing_status", "")
|
||||
if algo_status == "不适用":
|
||||
risk_level = "低"
|
||||
else:
|
||||
risk_level = "高"
|
||||
risk_details.append("涉及AI业务但未查到算法备案记录")
|
||||
|
||||
data_export_risk = compliance.get("data_export_risk", "低")
|
||||
if data_export_risk == "高":
|
||||
risk_level = "高"
|
||||
risk_details.append("存在大量跨境数据传输,数据出境评估风险高")
|
||||
elif data_export_risk == "中":
|
||||
if risk_level != "高":
|
||||
risk_level = "中"
|
||||
risk_details.append("存在部分跨境数据传输,需关注数据出境合规")
|
||||
|
||||
return {
|
||||
"company_name": company_name,
|
||||
"needs_filing": needs_filing,
|
||||
"filing_status": filing_status,
|
||||
"data_export_risk": data_export_risk,
|
||||
"overall_risk_level": risk_level,
|
||||
"risk_details": risk_details,
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
BIS 实体清单采集与匹配模块
|
||||
支持企业名模糊匹配 + 别名映射 + 供应链上游穿透
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||
|
||||
|
||||
def _load_entity_list() -> list:
|
||||
"""加载实体清单数据"""
|
||||
filepath = DATA_DIR / "entity_list.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def check_entity_list(company_name: str) -> dict:
|
||||
"""
|
||||
检查企业是否在 BIS 实体清单中
|
||||
支持模糊匹配和别名匹配
|
||||
"""
|
||||
entities = _load_entity_list()
|
||||
result = {
|
||||
"is_sanctioned": False,
|
||||
"match_type": None,
|
||||
"entity_detail": None,
|
||||
"supply_chain_risk": [], # 供应链上游被制裁的情况
|
||||
}
|
||||
|
||||
for entity in entities:
|
||||
# 精确匹配
|
||||
if company_name in entity["entity_name"]:
|
||||
result["is_sanctioned"] = True
|
||||
result["match_type"] = "直接命中"
|
||||
result["entity_detail"] = entity
|
||||
return result
|
||||
|
||||
# 别名匹配
|
||||
for alias in entity.get("aliases", []):
|
||||
if company_name in alias or alias in company_name:
|
||||
result["is_sanctioned"] = True
|
||||
result["match_type"] = "别名命中"
|
||||
result["entity_detail"] = entity
|
||||
return result
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def check_supply_chain_sanctions(company_name: str, suppliers: list) -> list:
|
||||
"""
|
||||
检查企业供应链上游是否有被制裁的实体
|
||||
返回受制裁的供应商列表
|
||||
"""
|
||||
sanctioned_suppliers = []
|
||||
entities = _load_entity_list()
|
||||
|
||||
for supplier in suppliers:
|
||||
# 清洗供应商名称(去掉括号中的说明文字)
|
||||
clean_name = supplier.split("(")[0].split("(")[0].strip()
|
||||
|
||||
for entity in entities:
|
||||
all_names = [entity["entity_name"]] + entity.get("aliases", [])
|
||||
for name in all_names:
|
||||
if clean_name in name or name in clean_name:
|
||||
sanctioned_suppliers.append({
|
||||
"supplier": supplier,
|
||||
"matched_entity": entity["entity_name"],
|
||||
"restrictions": entity["restrictions"],
|
||||
"date_added": entity["date_added"],
|
||||
})
|
||||
break
|
||||
|
||||
return sanctioned_suppliers
|
||||
|
||||
|
||||
def get_all_sanctioned_entities() -> list:
|
||||
"""获取所有被制裁实体列表"""
|
||||
return _load_entity_list()
|
||||
@@ -0,0 +1,105 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
财务数据采集模块
|
||||
双轨策略:优先尝试 AKShare 在线采集,失败则回退到预置数据
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||
|
||||
|
||||
def _load_preset_data() -> list:
|
||||
"""加载预置的科创板企业数据"""
|
||||
filepath = DATA_DIR / "sample_companies.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def collect_financial_data(stock_code: str) -> Optional[dict]:
|
||||
"""
|
||||
采集指定股票代码的财务数据
|
||||
双轨策略:在线采集 → 离线预置
|
||||
"""
|
||||
# 尝试在线采集
|
||||
try:
|
||||
return _collect_online(stock_code)
|
||||
except Exception as e:
|
||||
logger.warning(f"在线采集 {stock_code} 失败: {e},回退到预置数据")
|
||||
|
||||
# 回退到预置数据
|
||||
return _collect_from_preset(stock_code)
|
||||
|
||||
|
||||
def _collect_online(stock_code: str) -> Optional[dict]:
|
||||
"""通过 AKShare 在线采集财务数据"""
|
||||
try:
|
||||
import akshare as ak
|
||||
|
||||
# 科创板企业利润表
|
||||
profit_df = ak.stock_profit_sheet_by_report_em(symbol=stock_code)
|
||||
# 科创板企业资产负债表
|
||||
balance_df = ak.stock_balance_sheet_by_report_em(symbol=stock_code)
|
||||
|
||||
if profit_df is not None and not profit_df.empty:
|
||||
latest = profit_df.iloc[0]
|
||||
return {
|
||||
"stock_code": stock_code,
|
||||
"revenue": float(latest.get("营业收入", 0)),
|
||||
"net_profit": float(latest.get("净利润", 0)),
|
||||
"rd_expense": float(latest.get("研发费用", 0)),
|
||||
"source": "akshare_online",
|
||||
}
|
||||
except ImportError:
|
||||
logger.warning("AKShare 未安装,跳过在线采集")
|
||||
except Exception as e:
|
||||
logger.warning(f"AKShare 采集异常: {e}")
|
||||
|
||||
raise RuntimeError("在线采集失败")
|
||||
|
||||
|
||||
def _collect_from_preset(stock_code: str) -> Optional[dict]:
|
||||
"""从预置数据中查找企业"""
|
||||
companies = _load_preset_data()
|
||||
for company in companies:
|
||||
if company["stock_code"] == stock_code:
|
||||
return {
|
||||
"stock_code": stock_code,
|
||||
"company_name": company["company_name"],
|
||||
"industry": company["industry"],
|
||||
"sector": company["sector"],
|
||||
"financials": company["financials"],
|
||||
"core_tech_personnel": company["core_tech_personnel"],
|
||||
"tech_route": company["tech_route"],
|
||||
"compliance": company["compliance"],
|
||||
"supply_chain": company["supply_chain"],
|
||||
"source": "preset_data",
|
||||
}
|
||||
return None
|
||||
|
||||
|
||||
def get_all_companies() -> list:
|
||||
"""获取所有预置企业列表"""
|
||||
return _load_preset_data()
|
||||
|
||||
|
||||
def get_company_by_code(stock_code: str) -> Optional[dict]:
|
||||
"""通过股票代码查找企业完整数据"""
|
||||
companies = _load_preset_data()
|
||||
for company in companies:
|
||||
if company["stock_code"] == stock_code:
|
||||
return company
|
||||
return None
|
||||
|
||||
|
||||
def get_company_by_name(name: str) -> Optional[dict]:
|
||||
"""通过企业名称查找(支持简称)"""
|
||||
companies = _load_preset_data()
|
||||
for company in companies:
|
||||
if name in company["company_name"] or name in company["short_name"]:
|
||||
return company
|
||||
return None
|
||||
@@ -0,0 +1,81 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
全局配置文件
|
||||
集中管理 API Key、文件路径、模型参数等
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
# ============================================================
|
||||
# 项目根目录
|
||||
# ============================================================
|
||||
PROJECT_ROOT = Path(__file__).parent.resolve()
|
||||
DATA_DIR = PROJECT_ROOT / "data"
|
||||
|
||||
# ============================================================
|
||||
# DeepSeek API 配置(兼容 OpenAI 接口协议)
|
||||
# ============================================================
|
||||
DEEPSEEK_API_KEY = os.environ.get("DEEPSEEK_API_KEY", "")
|
||||
DEEPSEEK_BASE_URL = "https://api.deepseek.com/v1"
|
||||
DEEPSEEK_MODEL = "deepseek-chat"
|
||||
|
||||
# 备用1:火山引擎方舟(豆包大模型 doubao-seed-2-1-pro-260628)
|
||||
VOLCENGINE_API_KEY = os.environ.get("VOLCENGINE_API_KEY", os.environ.get("ARK_API_KEY", "836d9bc0-80e8-4e45-90df-7287994d91ec"))
|
||||
VOLCENGINE_BASE_URL = os.environ.get("VOLCENGINE_BASE_URL", "https://ark.cn-beijing.volces.com/api/v3")
|
||||
VOLCENGINE_MODEL = os.environ.get("VOLCENGINE_MODEL", "doubao-seed-2-0-lite-260428")
|
||||
|
||||
# 备用2:如果用户配置了其他兼容 OpenAI 协议的 API
|
||||
OPENAI_API_KEY = os.environ.get("OPENAI_API_KEY", "")
|
||||
OPENAI_BASE_URL = os.environ.get("OPENAI_BASE_URL", "https://api.openai.com/v1")
|
||||
OPENAI_MODEL = os.environ.get("OPENAI_MODEL", "gpt-4o-mini")
|
||||
|
||||
# ============================================================
|
||||
# LLM 调用参数
|
||||
# ============================================================
|
||||
LLM_TEMPERATURE = 0.1 # 低温度保证输出稳定
|
||||
LLM_MAX_TOKENS = 4096
|
||||
LLM_TIMEOUT = 60 # 超时秒数
|
||||
LLM_MAX_RETRIES = 2 # 最大重试次数
|
||||
|
||||
# ============================================================
|
||||
# 风险评估权重配置
|
||||
# ============================================================
|
||||
RISK_WEIGHTS = {
|
||||
"tech_disruption": 0.20, # 技术路线颠覆风险
|
||||
"talent_loss": 0.15, # 核心人员流失风险
|
||||
"algo_compliance": 0.15, # 算法/数据合规风险
|
||||
"geopolitical": 0.20, # 地缘政治/出口管制风险
|
||||
"rd_capitalization": 0.15, # 研发资本化操纵风险
|
||||
"concentration": 0.15, # 客户/供应商集中风险
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# 保险定价参数
|
||||
# ============================================================
|
||||
INSURANCE_PRODUCTS = {
|
||||
"ip_lawsuit": {
|
||||
"name": "知识产权被诉险",
|
||||
"base_premium": 50000, # 基础保费(元)
|
||||
"base_coverage": 5000000, # 基础保额(元)
|
||||
"description": "覆盖因知识产权纠纷(专利、商标、著作权)产生的诉讼费用及赔偿金",
|
||||
},
|
||||
"exec_departure": {
|
||||
"name": "高管离职业务中断险",
|
||||
"base_premium": 80000,
|
||||
"base_coverage": 10000000,
|
||||
"description": "覆盖核心技术人员/高管离职导致的业务中断损失",
|
||||
},
|
||||
"data_compliance": {
|
||||
"name": "数据合规行政处罚险",
|
||||
"base_premium": 30000,
|
||||
"base_coverage": 3000000,
|
||||
"description": "覆盖因非主观恶意的数据合规违规产生的行政罚款",
|
||||
},
|
||||
}
|
||||
|
||||
# ============================================================
|
||||
# Streamlit 页面配置
|
||||
# ============================================================
|
||||
PAGE_TITLE = "🛡️ 科创企业智能风控与核保系统"
|
||||
PAGE_ICON = "🛡️"
|
||||
LAYOUT = "wide"
|
||||
@@ -0,0 +1,11 @@
|
||||
[
|
||||
{"filing_id": "AL-2023-001", "company": "金山办公", "algo_name": "WPS AI 写作助手", "filing_date": "2023-08-15", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2023-002", "company": "传音控股", "algo_name": "AI 相机美颜算法", "filing_date": "2023-06-20", "status": "已通过", "algo_type": "个性化推荐类"},
|
||||
{"filing_id": "AL-2023-003", "company": "海天瑞声", "algo_name": "智能标注平台算法", "filing_date": "2023-09-10", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2023-004", "company": "百度", "algo_name": "文心一言大模型", "filing_date": "2023-08-31", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2023-005", "company": "阿里巴巴", "algo_name": "通义千问大模型", "filing_date": "2023-09-13", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2023-006", "company": "腾讯", "algo_name": "混元大模型", "filing_date": "2023-09-15", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2024-001", "company": "字节跳动", "algo_name": "豆包大模型", "filing_date": "2024-01-15", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2024-002", "company": "商汤科技", "algo_name": "日日新大模型", "filing_date": "2024-02-20", "status": "已通过", "algo_type": "生成合成类"},
|
||||
{"filing_id": "AL-2024-003", "company": "科大讯飞", "algo_name": "星火认知大模型", "filing_date": "2024-03-01", "status": "已通过", "algo_type": "生成合成类"}
|
||||
]
|
||||
@@ -0,0 +1,15 @@
|
||||
[
|
||||
{"entity_name": "中芯国际集成电路制造有限公司", "aliases": ["SMIC", "中芯国际", "Semiconductor Manufacturing International Corporation"], "date_added": "2020-12-18", "reason": "军事最终用途", "restrictions": "限制EUV光刻设备及先进制程相关技术出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "寒武纪科技股份有限公司", "aliases": ["Cambricon", "寒武纪"], "date_added": "2022-10-07", "reason": "支持中国军事现代化", "restrictions": "限制先进AI芯片代工与设计软件", "source": "BIS Entity List"},
|
||||
{"entity_name": "华为技术有限公司", "aliases": ["Huawei", "华为", "HUAWEI"], "date_added": "2019-05-16", "reason": "国家安全威胁", "restrictions": "全面限制美国技术出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "海康威视数字技术股份有限公司", "aliases": ["Hikvision", "海康威视"], "date_added": "2019-10-07", "reason": "参与新疆人权侵犯", "restrictions": "限制安防监控技术出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "大疆创新科技有限公司", "aliases": ["DJI", "大疆"], "date_added": "2020-12-18", "reason": "军事最终用途", "restrictions": "限制无人机核心部件出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "科大讯飞股份有限公司", "aliases": ["iFlytek", "科大讯飞"], "date_added": "2019-10-07", "reason": "参与新疆人权侵犯", "restrictions": "限制AI语音技术相关出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "商汤科技有限公司", "aliases": ["SenseTime", "商汤", "商汤科技"], "date_added": "2021-12-10", "reason": "参与监控技术开发", "restrictions": "限制AI视觉技术出口", "source": "BIS Entity List / OFAC SDN List"},
|
||||
{"entity_name": "旷视科技有限公司", "aliases": ["Megvii", "旷视"], "date_added": "2019-10-07", "reason": "参与新疆人权侵犯", "restrictions": "限制AI人脸识别技术出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "长江存储科技有限责任公司", "aliases": ["YMTC", "长江存储"], "date_added": "2022-12-15", "reason": "支持中国军事现代化", "restrictions": "限制NAND闪存芯片相关设备出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "上海微电子装备集团股份有限公司", "aliases": ["SMEE", "上海微电子"], "date_added": "2022-10-07", "reason": "支持中国军事现代化", "restrictions": "限制光刻机核心部件", "source": "BIS Entity List"},
|
||||
{"entity_name": "龙芯中科技术股份有限公司", "aliases": ["Loongson", "龙芯"], "date_added": "2023-02-10", "reason": "军事最终用途", "restrictions": "限制先进CPU设计工具出口", "source": "BIS Entity List"},
|
||||
{"entity_name": "壁仞科技股份有限公司", "aliases": ["Biren Technology", "壁仞科技"], "date_added": "2022-10-07", "reason": "支持中国军事现代化", "restrictions": "限制GPU芯片代工", "source": "BIS Entity List"},
|
||||
{"entity_name": "摩尔线程智能科技股份有限公司", "aliases": ["Moore Threads", "摩尔线程"], "date_added": "2023-10-17", "reason": "支持中国军事现代化", "restrictions": "限制GPU芯片代工与设计", "source": "BIS Entity List"}
|
||||
]
|
||||
@@ -0,0 +1,408 @@
|
||||
[
|
||||
{
|
||||
"stock_code": "688981",
|
||||
"company_name": "中芯国际",
|
||||
"short_name": "中芯国际",
|
||||
"industry": "半导体制造",
|
||||
"sector": "芯片",
|
||||
"listed_board": "科创板",
|
||||
"description": "国内领先的集成电路晶圆代工企业,提供0.35微米到FinFET先进工艺",
|
||||
"core_tech_personnel": [
|
||||
{"name": "梁孟松", "title": "联合CEO/技术研发负责人", "status": "在职", "importance": "极高"},
|
||||
{"name": "周子学", "title": "董事长", "status": "在职", "importance": "高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 57756000000,
|
||||
"net_profit_2024": 3433000000,
|
||||
"rd_expense_2024": 5124000000,
|
||||
"rd_capitalization_rate": 0.0,
|
||||
"rd_revenue_ratio": 0.089,
|
||||
"top5_customer_ratio": 0.42,
|
||||
"top5_supplier_ratio": 0.55,
|
||||
"receivable_turnover": 8.2,
|
||||
"cash_flow_ratio": 1.35
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "14nm FinFET 量产, 7nm 研发中",
|
||||
"competing_techs": ["EUV 光刻技术 (ASML 垄断)", "GAA 晶体管架构"],
|
||||
"tech_moat": "国产替代核心标的,但受制于设备禁运",
|
||||
"patent_count": 12000
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "被列入(2020年12月)",
|
||||
"sanctions_detail": "被美国商务部列入实体清单,限制EUV光刻设备进口"
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["ASML(光刻机)", "东京电子(刻蚀设备)", "应用材料(薄膜沉积)"],
|
||||
"key_customers": ["高通", "联发科", "华为海思"],
|
||||
"supplier_concentration_risk": "极高——核心设备依赖海外供应商"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688111",
|
||||
"company_name": "金山办公软件股份有限公司",
|
||||
"short_name": "金山办公",
|
||||
"industry": "办公软件",
|
||||
"sector": "软件",
|
||||
"listed_board": "科创板",
|
||||
"description": "国产办公软件龙头,WPS Office 全球月活超5.9亿",
|
||||
"core_tech_personnel": [
|
||||
{"name": "章庆元", "title": "CEO", "status": "在职", "importance": "极高"},
|
||||
{"name": "姚冬", "title": "CTO/AI研发负责人", "status": "在职", "importance": "高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 4586000000,
|
||||
"net_profit_2024": 1231000000,
|
||||
"rd_expense_2024": 1520000000,
|
||||
"rd_capitalization_rate": 0.0,
|
||||
"rd_revenue_ratio": 0.331,
|
||||
"top5_customer_ratio": 0.15,
|
||||
"top5_supplier_ratio": 0.30,
|
||||
"receivable_turnover": 12.5,
|
||||
"cash_flow_ratio": 1.82
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "WPS AI 大模型集成、云文档协同",
|
||||
"competing_techs": ["Microsoft 365 Copilot", "Google Workspace Gemini"],
|
||||
"tech_moat": "信创替代核心标的,政府及国企客户粘性高",
|
||||
"patent_count": 3500
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "已备案(WPS AI 写作助手)",
|
||||
"data_export_risk": "中(海外版WPS涉及数据跨境)",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["华为云", "阿里云", "英伟达(GPU)"],
|
||||
"key_customers": ["各级政府机关", "央企国企", "中小企业"],
|
||||
"supplier_concentration_risk": "中——GPU算力依赖进口"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688139",
|
||||
"company_name": "海尔生物医疗股份有限公司",
|
||||
"short_name": "海尔生物",
|
||||
"industry": "生物医疗低温存储",
|
||||
"sector": "医疗器械",
|
||||
"listed_board": "科创板",
|
||||
"description": "全球领先的生物医疗低温存储解决方案提供商",
|
||||
"core_tech_personnel": [
|
||||
{"name": "刘占杰", "title": "董事长/总经理", "status": "在职", "importance": "极高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 2245000000,
|
||||
"net_profit_2024": 398000000,
|
||||
"rd_expense_2024": 289000000,
|
||||
"rd_capitalization_rate": 0.05,
|
||||
"rd_revenue_ratio": 0.129,
|
||||
"top5_customer_ratio": 0.22,
|
||||
"top5_supplier_ratio": 0.35,
|
||||
"receivable_turnover": 6.8,
|
||||
"cash_flow_ratio": 1.15
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "超低温冰箱(-196°C)、自动化样本库、物联网疫苗管理",
|
||||
"competing_techs": ["赛默飞世尔", "松下医疗"],
|
||||
"tech_moat": "国内市场份额第一,物联网技术差异化",
|
||||
"patent_count": 800
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["压缩机供应商", "电子元器件供应商"],
|
||||
"key_customers": ["各级疾控中心", "医院", "科研院所"],
|
||||
"supplier_concentration_risk": "低"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688256",
|
||||
"company_name": "寒武纪科技股份有限公司",
|
||||
"short_name": "寒武纪",
|
||||
"industry": "AI芯片",
|
||||
"sector": "芯片",
|
||||
"listed_board": "科创板",
|
||||
"description": "国内AI芯片独角兽,智能计算处理器IP及芯片产品提供商",
|
||||
"core_tech_personnel": [
|
||||
{"name": "陈天石", "title": "创始人/董事长/CEO", "status": "在职", "importance": "极高"},
|
||||
{"name": "陈云霁", "title": "首席科学家(兄长)", "status": "中科院任职,兼职顾问", "importance": "高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 1170000000,
|
||||
"net_profit_2024": -825000000,
|
||||
"rd_expense_2024": 1510000000,
|
||||
"rd_capitalization_rate": 0.0,
|
||||
"rd_revenue_ratio": 1.29,
|
||||
"top5_customer_ratio": 0.85,
|
||||
"top5_supplier_ratio": 0.70,
|
||||
"receivable_turnover": 3.2,
|
||||
"cash_flow_ratio": 0.45
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "思元系列AI推理/训练芯片, MLU架构",
|
||||
"competing_techs": ["英伟达CUDA生态", "华为昇腾", "AMD Instinct"],
|
||||
"tech_moat": "自主IP指令集,但生态建设与英伟达差距大",
|
||||
"patent_count": 2800
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用(硬件厂商)",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "被列入(2022年10月)",
|
||||
"sanctions_detail": "被美国商务部列入实体清单,限制先进制程芯片代工"
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["台积电(已受限)", "中芯国际(替代)", "日月光(封测)"],
|
||||
"key_customers": ["中国移动", "南京市政府智慧城市项目", "某互联网大厂"],
|
||||
"supplier_concentration_risk": "极高——先进制程代工受限"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688029",
|
||||
"company_name": "南微医学科技股份有限公司",
|
||||
"short_name": "南微医学",
|
||||
"industry": "微创医疗器械",
|
||||
"sector": "医疗器械",
|
||||
"listed_board": "科创板",
|
||||
"description": "全球领先的微创诊疗器械企业,内镜下诊疗器械龙头",
|
||||
"core_tech_personnel": [
|
||||
{"name": "隆晓辉", "title": "创始人/董事长", "status": "在职", "importance": "极高"},
|
||||
{"name": "冷德嵘", "title": "总经理", "status": "在职", "importance": "高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 2580000000,
|
||||
"net_profit_2024": 628000000,
|
||||
"rd_expense_2024": 365000000,
|
||||
"rd_capitalization_rate": 0.12,
|
||||
"rd_revenue_ratio": 0.141,
|
||||
"top5_customer_ratio": 0.18,
|
||||
"top5_supplier_ratio": 0.32,
|
||||
"receivable_turnover": 7.5,
|
||||
"cash_flow_ratio": 1.42
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "一次性内镜、电外科手术器械、AI辅助诊断",
|
||||
"competing_techs": ["波士顿科学", "奥林巴斯", "库克医疗"],
|
||||
"tech_moat": "国产微创器械龙头,海外收入占比超40%",
|
||||
"patent_count": 1200
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["不锈钢/钛合金供应商", "精密注塑件供应商"],
|
||||
"key_customers": ["全球三甲医院", "海外经销商网络"],
|
||||
"supplier_concentration_risk": "低"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688126",
|
||||
"company_name": "沪硅产业集团股份有限公司",
|
||||
"short_name": "沪硅产业",
|
||||
"industry": "半导体硅片",
|
||||
"sector": "芯片",
|
||||
"listed_board": "科创板",
|
||||
"description": "国内规模最大的半导体硅片制造企业,300mm大硅片龙头",
|
||||
"core_tech_personnel": [
|
||||
{"name": "林林", "title": "总裁", "status": "在职", "importance": "极高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 3285000000,
|
||||
"net_profit_2024": -156000000,
|
||||
"rd_expense_2024": 498000000,
|
||||
"rd_capitalization_rate": 0.35,
|
||||
"rd_revenue_ratio": 0.152,
|
||||
"top5_customer_ratio": 0.62,
|
||||
"top5_supplier_ratio": 0.48,
|
||||
"receivable_turnover": 5.1,
|
||||
"cash_flow_ratio": 0.78
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "300mm半导体硅片(12英寸)量产",
|
||||
"competing_techs": ["日本信越化学", "日本SUMCO", "韩国SK Siltron"],
|
||||
"tech_moat": "国产替代第一梯队,但高端产品仍有差距",
|
||||
"patent_count": 650
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["多晶硅供应商", "石英坩埚供应商"],
|
||||
"key_customers": ["中芯国际", "华虹半导体", "长江存储"],
|
||||
"supplier_concentration_risk": "中"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688223",
|
||||
"company_name": "晶科能源股份有限公司",
|
||||
"short_name": "晶科能源",
|
||||
"industry": "光伏组件",
|
||||
"sector": "新能源",
|
||||
"listed_board": "科创板",
|
||||
"description": "全球领先的光伏组件制造商,N型TOPCon技术全球领先",
|
||||
"core_tech_personnel": [
|
||||
{"name": "李仙德", "title": "创始人/董事长", "status": "在职", "importance": "极高"},
|
||||
{"name": "金浩", "title": "首席科学家", "status": "在职", "importance": "高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 84300000000,
|
||||
"net_profit_2024": 1050000000,
|
||||
"rd_expense_2024": 4890000000,
|
||||
"rd_capitalization_rate": 0.08,
|
||||
"rd_revenue_ratio": 0.058,
|
||||
"top5_customer_ratio": 0.25,
|
||||
"top5_supplier_ratio": 0.40,
|
||||
"receivable_turnover": 6.2,
|
||||
"cash_flow_ratio": 0.92
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "N型TOPCon高效电池, 钙钛矿叠层研发",
|
||||
"competing_techs": ["隆基HJT路线", "钙钛矿技术", "IBC电池"],
|
||||
"tech_moat": "TOPCon量产规模全球第一,但面临技术路线之争",
|
||||
"patent_count": 2200
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "未列入(但面临美国反倾销关税)",
|
||||
"sanctions_detail": "面临美国、欧盟反倾销/反补贴调查"
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["硅料供应商(通威/大全)", "银浆供应商", "EVA胶膜供应商"],
|
||||
"key_customers": ["全球EPC总包商", "欧美分布式市场", "中东/非洲新兴市场"],
|
||||
"supplier_concentration_risk": "中——硅料价格波动大"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688036",
|
||||
"company_name": "传音控股股份有限公司",
|
||||
"short_name": "传音控股",
|
||||
"industry": "智能终端",
|
||||
"sector": "消费电子",
|
||||
"listed_board": "科创板",
|
||||
"description": "非洲市场手机销量第一,旗下TECNO/Infinix/itel品牌",
|
||||
"core_tech_personnel": [
|
||||
{"name": "竺兆江", "title": "创始人/董事长", "status": "在职", "importance": "极高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 65800000000,
|
||||
"net_profit_2024": 5620000000,
|
||||
"rd_expense_2024": 3250000000,
|
||||
"rd_capitalization_rate": 0.0,
|
||||
"rd_revenue_ratio": 0.049,
|
||||
"top5_customer_ratio": 0.32,
|
||||
"top5_supplier_ratio": 0.58,
|
||||
"receivable_turnover": 9.8,
|
||||
"cash_flow_ratio": 1.25
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "深肤色相机算法, AI翻译, 本地化OS",
|
||||
"competing_techs": ["三星", "小米", "OPPO"],
|
||||
"tech_moat": "新兴市场本地化经验深厚",
|
||||
"patent_count": 4500
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "已备案(AI相机美颜算法)",
|
||||
"data_export_risk": "高(大量非洲/东南亚用户数据跨境)",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["联发科(芯片)", "三星SDI/ATL(电池)", "京东方(屏幕)"],
|
||||
"key_customers": ["非洲运营商", "东南亚分销商", "拉美零售商"],
|
||||
"supplier_concentration_risk": "中——芯片依赖联发科"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688005",
|
||||
"company_name": "容百科技股份有限公司",
|
||||
"short_name": "容百科技",
|
||||
"industry": "锂电正极材料",
|
||||
"sector": "新能源",
|
||||
"listed_board": "科创板",
|
||||
"description": "全球领先的锂电池正极材料供应商,高镍三元材料龙头",
|
||||
"core_tech_personnel": [
|
||||
{"name": "白厚善", "title": "创始人/董事长", "status": "在职", "importance": "极高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 18900000000,
|
||||
"net_profit_2024": 356000000,
|
||||
"rd_expense_2024": 785000000,
|
||||
"rd_capitalization_rate": 0.18,
|
||||
"rd_revenue_ratio": 0.042,
|
||||
"top5_customer_ratio": 0.78,
|
||||
"top5_supplier_ratio": 0.65,
|
||||
"receivable_turnover": 4.5,
|
||||
"cash_flow_ratio": 0.68
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "超高镍三元正极材料(Ni>90%), 固态电池正极",
|
||||
"competing_techs": ["磷酸铁锂(比亚迪路线)", "钠离子电池", "固态电解质"],
|
||||
"tech_moat": "高镍三元出货量全球前三",
|
||||
"patent_count": 450
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "不适用",
|
||||
"data_export_risk": "低",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["格林美(镍钴原料)", "华友钴业"],
|
||||
"key_customers": ["宁德时代", "三星SDI", "SK On"],
|
||||
"supplier_concentration_risk": "高——原材料价格波动大"
|
||||
}
|
||||
},
|
||||
{
|
||||
"stock_code": "688787",
|
||||
"company_name": "海天瑞声科技股份有限公司",
|
||||
"short_name": "海天瑞声",
|
||||
"industry": "AI训练数据",
|
||||
"sector": "AI",
|
||||
"listed_board": "科创板",
|
||||
"description": "国内领先的AI训练数据解决方案提供商",
|
||||
"core_tech_personnel": [
|
||||
{"name": "贺琳", "title": "创始人/董事长", "status": "在职", "importance": "极高"},
|
||||
{"name": "郭蕾", "title": "副总经理/技术负责人", "status": "离职(2024年8月)", "importance": "高"}
|
||||
],
|
||||
"financials": {
|
||||
"revenue_2024": 356000000,
|
||||
"net_profit_2024": -48000000,
|
||||
"rd_expense_2024": 102000000,
|
||||
"rd_capitalization_rate": 0.42,
|
||||
"rd_revenue_ratio": 0.287,
|
||||
"top5_customer_ratio": 0.88,
|
||||
"top5_supplier_ratio": 0.35,
|
||||
"receivable_turnover": 2.8,
|
||||
"cash_flow_ratio": 0.35
|
||||
},
|
||||
"tech_route": {
|
||||
"current_tech": "多语种语音数据集, 自动驾驶标注数据, AIGC合成数据",
|
||||
"competing_techs": ["Scale AI", "Appen", "AI合成数据替代人工标注"],
|
||||
"tech_moat": "多语种覆盖广泛,但面临AIGC合成数据的颠覆性威胁",
|
||||
"patent_count": 180
|
||||
},
|
||||
"compliance": {
|
||||
"algo_filing_status": "已备案(智能标注平台算法)",
|
||||
"data_export_risk": "高(大量跨境数据采集与交付)",
|
||||
"entity_list_status": "未列入",
|
||||
"sanctions_detail": ""
|
||||
},
|
||||
"supply_chain": {
|
||||
"key_suppliers": ["全球数据采集众包网络"],
|
||||
"key_customers": ["字节跳动", "百度", "某大型车企"],
|
||||
"supplier_concentration_risk": "中"
|
||||
}
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"supply_relations": [
|
||||
{"from": "ASML", "to": "中芯国际", "relation": "供应光刻设备", "critical": true, "status": "受限"},
|
||||
{"from": "东京电子", "to": "中芯国际", "relation": "供应刻蚀设备", "critical": true, "status": "受限"},
|
||||
{"from": "应用材料", "to": "中芯国际", "relation": "供应薄膜沉积设备", "critical": true, "status": "受限"},
|
||||
{"from": "中芯国际", "to": "寒武纪", "relation": "芯片代工", "critical": true, "status": "正常"},
|
||||
{"from": "中芯国际", "to": "华为海思", "relation": "芯片代工", "critical": true, "status": "受限"},
|
||||
{"from": "台积电", "to": "寒武纪", "relation": "先进制程代工", "critical": true, "status": "受限"},
|
||||
{"from": "英伟达", "to": "金山办公", "relation": "供应GPU算力", "critical": true, "status": "受限"},
|
||||
{"from": "英伟达", "to": "海天瑞声", "relation": "供应GPU算力(客户需求)", "critical": false, "status": "正常"},
|
||||
{"from": "联发科", "to": "传音控股", "relation": "供应手机芯片", "critical": true, "status": "正常"},
|
||||
{"from": "三星SDI", "to": "传音控股", "relation": "供应电池", "critical": false, "status": "正常"},
|
||||
{"from": "京东方", "to": "传音控股", "relation": "供应屏幕", "critical": false, "status": "正常"},
|
||||
{"from": "沪硅产业", "to": "中芯国际", "relation": "供应半导体硅片", "critical": true, "status": "正常"},
|
||||
{"from": "沪硅产业", "to": "华虹半导体", "relation": "供应半导体硅片", "critical": true, "status": "正常"},
|
||||
{"from": "沪硅产业", "to": "长江存储", "relation": "供应半导体硅片", "critical": true, "status": "正常"},
|
||||
{"from": "格林美", "to": "容百科技", "relation": "供应镍钴原料", "critical": true, "status": "正常"},
|
||||
{"from": "华友钴业", "to": "容百科技", "relation": "供应钴原料", "critical": true, "status": "正常"},
|
||||
{"from": "容百科技", "to": "宁德时代", "relation": "供应正极材料", "critical": true, "status": "正常"},
|
||||
{"from": "容百科技", "to": "三星SDI", "relation": "供应正极材料", "critical": false, "status": "正常"},
|
||||
{"from": "通威股份", "to": "晶科能源", "relation": "供应硅料", "critical": true, "status": "正常"},
|
||||
{"from": "大全能源", "to": "晶科能源", "relation": "供应硅料", "critical": true, "status": "正常"},
|
||||
{"from": "海天瑞声", "to": "字节跳动", "relation": "供应AI训练数据", "critical": false, "status": "正常"},
|
||||
{"from": "海天瑞声", "to": "百度", "relation": "供应AI训练数据", "critical": false, "status": "正常"}
|
||||
],
|
||||
"investment_relations": [
|
||||
{"from": "国家集成电路产业基金", "to": "中芯国际", "relation": "战略投资", "share_ratio": 0.15},
|
||||
{"from": "国家集成电路产业基金", "to": "沪硅产业", "relation": "战略投资", "share_ratio": 0.08},
|
||||
{"from": "国家集成电路产业基金", "to": "长江存储", "relation": "战略投资", "share_ratio": 0.20}
|
||||
],
|
||||
"personnel_relations": [
|
||||
{"person": "梁孟松", "from_company": "台积电/三星", "to_company": "中芯国际", "relation": "技术负责人跳槽", "year": 2017},
|
||||
{"person": "郭蕾", "from_company": "海天瑞声", "to_company": "未知", "relation": "核心技术人员离职", "year": 2024}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,175 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
部署系统到远程服务器 159.75.81.121
|
||||
"""
|
||||
import os
|
||||
import tarfile
|
||||
import paramiko
|
||||
import time
|
||||
|
||||
SERVER_IP = "159.75.81.121"
|
||||
SSH_PORT = 22
|
||||
SSH_USER = "root"
|
||||
SSH_PASS = "sp-cc123"
|
||||
REMOTE_DIR = "/opt/risk_system"
|
||||
DOMAIN = "risk.aiformat.cn"
|
||||
|
||||
LOCAL_BASE = r"d:\code\XH-202626_科创企业特有风险的识别与管理"
|
||||
ARCHIVE_NAME = os.path.join(LOCAL_BASE, "deploy_package.tar.gz")
|
||||
|
||||
def make_archive():
|
||||
print("📦 正在打部署压缩包...")
|
||||
includes = [
|
||||
"🏠_系统首页.py",
|
||||
"app.py",
|
||||
"config.py",
|
||||
"requirements.txt",
|
||||
"agents",
|
||||
"collectors",
|
||||
"data",
|
||||
"knowledge_graph",
|
||||
"lib",
|
||||
"pages",
|
||||
"risk_engine",
|
||||
"utils",
|
||||
".streamlit"
|
||||
]
|
||||
|
||||
with tarfile.open(ARCHIVE_NAME, "w:gz") as tar:
|
||||
for item in includes:
|
||||
full_path = os.path.join(LOCAL_BASE, item)
|
||||
if os.path.exists(full_path):
|
||||
print(f" + 打包: {item}")
|
||||
tar.add(full_path, arcname=item)
|
||||
else:
|
||||
print(f" - 跳过(不存在): {item}")
|
||||
print(f"✅ 压缩完成: {ARCHIVE_NAME}")
|
||||
|
||||
def run_remote_cmd(ssh, cmd):
|
||||
print(f"\n🚀 [远程命令] {cmd}")
|
||||
stdin, stdout, stderr = ssh.exec_command(cmd)
|
||||
out = stdout.read().decode('utf-8', errors='ignore')
|
||||
err = stderr.read().decode('utf-8', errors='ignore')
|
||||
if out:
|
||||
print(f"[STDOUT]\n{out.strip()}")
|
||||
if err:
|
||||
print(f"[STDERR]\n{err.strip()}")
|
||||
return out, err
|
||||
|
||||
def deploy():
|
||||
make_archive()
|
||||
|
||||
print(f"\n🔌 正在连接服务器 {SERVER_IP}...")
|
||||
ssh = paramiko.SSHClient()
|
||||
ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
try:
|
||||
ssh.connect(SERVER_IP, port=SSH_PORT, username=SSH_USER, password=SSH_PASS, timeout=15)
|
||||
print("✅ SSH 连接成功!")
|
||||
except Exception as e:
|
||||
print(f"❌ SSH 连接失败: {e}")
|
||||
return
|
||||
|
||||
sftp = ssh.open_sftp()
|
||||
|
||||
# 1. 创建远程目录
|
||||
run_remote_cmd(ssh, f"mkdir -p {REMOTE_DIR}")
|
||||
|
||||
# 2. 上传压缩包
|
||||
remote_archive = f"{REMOTE_DIR}/deploy_package.tar.gz"
|
||||
print(f"📤 正在上传压缩包到 {remote_archive}...")
|
||||
sftp.put(ARCHIVE_NAME, remote_archive)
|
||||
print("✅ 压缩包上传成功!")
|
||||
sftp.close()
|
||||
|
||||
# 3. 解压并配置 Python 环境与依赖
|
||||
run_remote_cmd(ssh, f"cd {REMOTE_DIR} && tar -xzf deploy_package.tar.gz")
|
||||
|
||||
# 检查并安装系统依赖 (nginx, python3, python3-pip, python3-venv 等)
|
||||
setup_env_cmd = """
|
||||
if command -v apt-get &> /dev/null; then
|
||||
apt-get update -y && apt-get install -y python3 python3-pip python3-venv nginx
|
||||
elif command -v yum &> /dev/null; then
|
||||
yum install -y python3 python3-pip nginx
|
||||
fi
|
||||
"""
|
||||
run_remote_cmd(ssh, setup_env_cmd)
|
||||
|
||||
# 创建 virtualenv 并安装依赖
|
||||
venv_cmd = f"""
|
||||
cd {REMOTE_DIR}
|
||||
python3 -m venv venv || python3 -m venv venv --without-pip
|
||||
source venv/bin/activate
|
||||
pip install --upgrade pip -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
"""
|
||||
run_remote_cmd(ssh, venv_cmd)
|
||||
|
||||
# 4. 配置 Systemd 服务
|
||||
systemd_service = f"""[Unit]
|
||||
Description=Risk Management Streamlit Web App
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
User=root
|
||||
WorkingDirectory={REMOTE_DIR}
|
||||
ExecStart={REMOTE_DIR}/venv/bin/streamlit run {REMOTE_DIR}/🏠_系统首页.py --server.port 8501 --server.address 127.0.0.1 --server.headless true --server.enableCORS false --server.enableXsrfProtection false
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
"""
|
||||
create_service_cmd = f"cat << 'EOF' > /etc/systemd/system/risk_app.service\n{systemd_service}\nEOF"
|
||||
run_remote_cmd(ssh, create_service_cmd)
|
||||
|
||||
run_remote_cmd(ssh, "systemctl daemon-reload && systemctl enable risk_app && systemctl restart risk_app")
|
||||
time.sleep(3)
|
||||
run_remote_cmd(ssh, "systemctl status risk_app --no-pager")
|
||||
|
||||
# 5. 配置 Nginx 反向代理
|
||||
nginx_conf = f"""server {{
|
||||
listen 80;
|
||||
server_name {DOMAIN};
|
||||
|
||||
location / {{
|
||||
proxy_pass http://127.0.0.1:8501;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_read_timeout 86400;
|
||||
}}
|
||||
|
||||
location /_stcore/stream {{
|
||||
proxy_pass http://127.0.0.1:8501/_stcore/stream;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection "upgrade";
|
||||
proxy_set_header Host $host;
|
||||
}}
|
||||
}}
|
||||
"""
|
||||
create_nginx_cmd = f"cat << 'EOF' > /etc/nginx/conf.d/risk.aiformat.cn.conf\n{nginx_conf}\nEOF"
|
||||
run_remote_cmd(ssh, create_nginx_cmd)
|
||||
|
||||
# 兼容 Ubuntu/Debian sites-enabled
|
||||
run_remote_cmd(ssh, f"mkdir -p /etc/nginx/sites-enabled && ln -sf /etc/nginx/conf.d/risk.aiformat.cn.conf /etc/nginx/sites-enabled/risk.aiformat.cn.conf")
|
||||
|
||||
run_remote_cmd(ssh, "nginx -t && systemctl restart nginx")
|
||||
time.sleep(2)
|
||||
run_remote_cmd(ssh, "systemctl status nginx --no-pager")
|
||||
|
||||
# 6. 测试应用连通性
|
||||
run_remote_cmd(ssh, "curl -I http://127.0.0.1:8501")
|
||||
run_remote_cmd(ssh, f"curl -H 'Host: {DOMAIN}' -I http://127.0.0.1/")
|
||||
|
||||
ssh.close()
|
||||
print("\n🎉 部署完成!")
|
||||
print(f"🌐 域名访问地址: http://{DOMAIN}")
|
||||
print(f"🖥️ 服务器 IP 直连防护端口验证: http://{SERVER_IP}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
deploy()
|
||||
@@ -0,0 +1,2 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""知识图谱模块"""
|
||||
@@ -0,0 +1,169 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
供应链风险传染分析器
|
||||
基于 BFS 遍历实现风险穿透,计算传染距离和影响权重
|
||||
"""
|
||||
import logging
|
||||
from collections import deque
|
||||
from typing import Optional
|
||||
|
||||
import networkx as nx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def analyze_contagion(G: nx.DiGraph, target_company: str) -> dict:
|
||||
"""
|
||||
分析指定企业的供应链风险传染情况
|
||||
从上游(供应商)方向进行 BFS 穿透
|
||||
"""
|
||||
if target_company not in G:
|
||||
return {"error": f"企业 '{target_company}' 不在图谱中"}
|
||||
|
||||
result = {
|
||||
"company": target_company,
|
||||
"direct_risks": [], # 直接风险(一度关联)
|
||||
"indirect_risks": [], # 间接风险(二度及以上关联)
|
||||
"contagion_paths": [], # 风险传染路径
|
||||
"risk_score": 0, # 供应链风险总分
|
||||
"critical_nodes": [], # 关键断裂节点
|
||||
}
|
||||
|
||||
# BFS 从目标企业向上游遍历
|
||||
visited = set()
|
||||
queue = deque([(target_company, 0, [target_company])])
|
||||
visited.add(target_company)
|
||||
|
||||
while queue:
|
||||
current, depth, path = queue.popleft()
|
||||
|
||||
# 检查当前节点的上游(前驱节点)
|
||||
for predecessor in G.predecessors(current):
|
||||
if predecessor in visited:
|
||||
continue
|
||||
visited.add(predecessor)
|
||||
|
||||
edge_data = G.edges[predecessor, current]
|
||||
node_data = G.nodes.get(predecessor, {})
|
||||
new_path = [predecessor] + path
|
||||
|
||||
# 检查上游节点是否受制裁
|
||||
if node_data.get("is_sanctioned", False):
|
||||
risk_entry = {
|
||||
"entity": predecessor,
|
||||
"node_type": node_data.get("node_type", "未知"),
|
||||
"distance": depth + 1,
|
||||
"relation": edge_data.get("relation", ""),
|
||||
"is_critical": edge_data.get("critical", False),
|
||||
"status": edge_data.get("status", "正常"),
|
||||
"path": " → ".join(new_path),
|
||||
}
|
||||
|
||||
if depth == 0:
|
||||
result["direct_risks"].append(risk_entry)
|
||||
else:
|
||||
result["indirect_risks"].append(risk_entry)
|
||||
|
||||
result["contagion_paths"].append({
|
||||
"path": new_path,
|
||||
"path_str": " → ".join(new_path),
|
||||
"length": len(new_path),
|
||||
"severity": "高" if edge_data.get("critical", False) else "中",
|
||||
})
|
||||
|
||||
# 检查受限状态的边
|
||||
if edge_data.get("status") == "受限":
|
||||
if predecessor not in [r["entity"] for r in result["direct_risks"] + result["indirect_risks"]]:
|
||||
risk_entry = {
|
||||
"entity": predecessor,
|
||||
"node_type": node_data.get("node_type", "未知"),
|
||||
"distance": depth + 1,
|
||||
"relation": edge_data.get("relation", ""),
|
||||
"is_critical": edge_data.get("critical", False),
|
||||
"status": "受限",
|
||||
"path": " → ".join(new_path),
|
||||
}
|
||||
if depth == 0:
|
||||
result["direct_risks"].append(risk_entry)
|
||||
else:
|
||||
result["indirect_risks"].append(risk_entry)
|
||||
|
||||
# 继续向上游遍历(最多3层)
|
||||
if depth < 2:
|
||||
queue.append((predecessor, depth + 1, new_path))
|
||||
|
||||
# 计算供应链风险得分
|
||||
result["risk_score"] = _calculate_supply_chain_risk_score(result)
|
||||
|
||||
# 识别关键断裂节点
|
||||
result["critical_nodes"] = _find_critical_nodes(G, target_company)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
def _calculate_supply_chain_risk_score(contagion_result: dict) -> float:
|
||||
"""计算供应链风险综合得分 (0-100)"""
|
||||
score = 0
|
||||
|
||||
# 直接风险:每个 +25 分,关键供应 +35 分
|
||||
for risk in contagion_result["direct_risks"]:
|
||||
if risk["is_critical"]:
|
||||
score += 35
|
||||
else:
|
||||
score += 25
|
||||
|
||||
# 间接风险:每个 +10 分,关键供应 +15 分
|
||||
for risk in contagion_result["indirect_risks"]:
|
||||
if risk["is_critical"]:
|
||||
score += 15
|
||||
else:
|
||||
score += 10
|
||||
|
||||
return min(score, 100) # 上限 100
|
||||
|
||||
|
||||
def _find_critical_nodes(G: nx.DiGraph, target: str) -> list:
|
||||
"""
|
||||
识别关键断裂节点:如果移除该节点,目标企业将失去关键供应来源
|
||||
"""
|
||||
critical = []
|
||||
predecessors = list(G.predecessors(target))
|
||||
|
||||
for pred in predecessors:
|
||||
edge_data = G.edges[pred, target]
|
||||
if edge_data.get("critical", False):
|
||||
# 检查是否有替代供应商
|
||||
alternatives = sum(
|
||||
1 for p in predecessors
|
||||
if p != pred and G.edges[p, target].get("relation", "") == edge_data.get("relation", "")
|
||||
)
|
||||
critical.append({
|
||||
"node": pred,
|
||||
"relation": edge_data.get("relation", ""),
|
||||
"has_alternative": alternatives > 0,
|
||||
"alternative_count": alternatives,
|
||||
"status": edge_data.get("status", "正常"),
|
||||
})
|
||||
|
||||
return critical
|
||||
|
||||
|
||||
def find_all_risk_paths(G: nx.DiGraph, source: str, target: str, max_depth: int = 4) -> list:
|
||||
"""
|
||||
查找两个节点之间的所有风险路径
|
||||
"""
|
||||
if source not in G or target not in G:
|
||||
return []
|
||||
|
||||
try:
|
||||
paths = list(nx.all_simple_paths(G, source, target, cutoff=max_depth))
|
||||
return [
|
||||
{
|
||||
"path": p,
|
||||
"path_str": " → ".join(p),
|
||||
"length": len(p),
|
||||
}
|
||||
for p in paths
|
||||
]
|
||||
except nx.NetworkXError:
|
||||
return []
|
||||
@@ -0,0 +1,214 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
知识图谱构建引擎
|
||||
基于 NetworkX 构建科创企业供应链风险传染图谱
|
||||
节点类型:企业、供应商、客户、核心人员、制裁实体
|
||||
边类型:供应关系、客户关系、任职关系、投资关系
|
||||
"""
|
||||
import json
|
||||
import logging
|
||||
from pathlib import Path
|
||||
|
||||
import networkx as nx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
DATA_DIR = Path(__file__).parent.parent / "data"
|
||||
|
||||
|
||||
def build_graph() -> nx.DiGraph:
|
||||
"""
|
||||
构建完整的科创企业供应链风险知识图谱
|
||||
"""
|
||||
G = nx.DiGraph()
|
||||
|
||||
# 1. 加载企业数据,添加企业节点
|
||||
_add_company_nodes(G)
|
||||
|
||||
# 2. 加载供应链数据,添加关系边
|
||||
_add_supply_chain_edges(G)
|
||||
|
||||
# 3. 加载实体清单,标记受制裁节点
|
||||
_mark_sanctioned_nodes(G)
|
||||
|
||||
logger.info(f"图谱构建完成: {G.number_of_nodes()} 节点, {G.number_of_edges()} 边")
|
||||
return G
|
||||
|
||||
|
||||
def _add_company_nodes(G: nx.DiGraph):
|
||||
"""添加科创板企业节点及其关联的核心人员节点"""
|
||||
filepath = DATA_DIR / "sample_companies.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
companies = json.load(f)
|
||||
|
||||
for company in companies:
|
||||
name = company["short_name"]
|
||||
G.add_node(
|
||||
name,
|
||||
node_type="科创企业",
|
||||
stock_code=company["stock_code"],
|
||||
industry=company["industry"],
|
||||
sector=company["sector"],
|
||||
is_sanctioned=company["compliance"]["entity_list_status"] != "未列入",
|
||||
risk_level="正常",
|
||||
color="#4CAF50", # 默认绿色
|
||||
)
|
||||
|
||||
# 添加核心技术人员节点
|
||||
for person in company.get("core_tech_personnel", []):
|
||||
person_id = f"{person['name']}@{name}"
|
||||
G.add_node(
|
||||
person_id,
|
||||
node_type="核心人员",
|
||||
real_name=person["name"],
|
||||
title=person["title"],
|
||||
status=person["status"],
|
||||
importance=person["importance"],
|
||||
company=name,
|
||||
color="#2196F3", # 蓝色
|
||||
)
|
||||
G.add_edge(
|
||||
person_id, name,
|
||||
relation="任职于",
|
||||
edge_type="personnel",
|
||||
)
|
||||
|
||||
# 添加供应商节点
|
||||
for supplier in company.get("supply_chain", {}).get("key_suppliers", []):
|
||||
supplier_name = supplier.split("(")[0].split("(")[0].strip()
|
||||
if not G.has_node(supplier_name):
|
||||
G.add_node(
|
||||
supplier_name,
|
||||
node_type="供应商",
|
||||
is_sanctioned=False,
|
||||
risk_level="正常",
|
||||
color="#FF9800", # 橙色
|
||||
)
|
||||
G.add_edge(
|
||||
supplier_name, name,
|
||||
relation="供应",
|
||||
detail=supplier,
|
||||
edge_type="supply",
|
||||
)
|
||||
|
||||
# 添加客户节点
|
||||
for customer in company.get("supply_chain", {}).get("key_customers", []):
|
||||
customer_name = customer.split("(")[0].split("(")[0].strip()
|
||||
if not G.has_node(customer_name):
|
||||
G.add_node(
|
||||
customer_name,
|
||||
node_type="客户",
|
||||
is_sanctioned=False,
|
||||
risk_level="正常",
|
||||
color="#9C27B0", # 紫色
|
||||
)
|
||||
G.add_edge(
|
||||
name, customer_name,
|
||||
relation="供货给",
|
||||
edge_type="customer",
|
||||
)
|
||||
|
||||
|
||||
def _add_supply_chain_edges(G: nx.DiGraph):
|
||||
"""从供应链关系文件添加更详细的边"""
|
||||
filepath = DATA_DIR / "supply_chain.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
data = json.load(f)
|
||||
|
||||
# 添加供应关系
|
||||
for rel in data.get("supply_relations", []):
|
||||
from_node = rel["from"]
|
||||
to_node = rel["to"]
|
||||
|
||||
# 确保节点存在
|
||||
if not G.has_node(from_node):
|
||||
G.add_node(from_node, node_type="供应商", is_sanctioned=False,
|
||||
risk_level="正常", color="#FF9800")
|
||||
if not G.has_node(to_node):
|
||||
G.add_node(to_node, node_type="企业", is_sanctioned=False,
|
||||
risk_level="正常", color="#4CAF50")
|
||||
|
||||
G.add_edge(
|
||||
from_node, to_node,
|
||||
relation=rel["relation"],
|
||||
critical=rel.get("critical", False),
|
||||
status=rel.get("status", "正常"),
|
||||
edge_type="supply",
|
||||
)
|
||||
|
||||
# 添加投资关系
|
||||
for rel in data.get("investment_relations", []):
|
||||
from_node = rel["from"]
|
||||
to_node = rel["to"]
|
||||
|
||||
if not G.has_node(from_node):
|
||||
G.add_node(from_node, node_type="投资方", is_sanctioned=False,
|
||||
risk_level="正常", color="#607D8B")
|
||||
|
||||
G.add_edge(
|
||||
from_node, to_node,
|
||||
relation=rel["relation"],
|
||||
share_ratio=rel.get("share_ratio", 0),
|
||||
edge_type="investment",
|
||||
)
|
||||
|
||||
|
||||
def _mark_sanctioned_nodes(G: nx.DiGraph):
|
||||
"""标记受制裁的节点,并向下游传播风险"""
|
||||
filepath = DATA_DIR / "entity_list.json"
|
||||
with open(filepath, "r", encoding="utf-8") as f:
|
||||
entities = json.load(f)
|
||||
|
||||
# 收集所有受制裁实体的名称和别名
|
||||
sanctioned_names = set()
|
||||
for entity in entities:
|
||||
sanctioned_names.add(entity["entity_name"])
|
||||
for alias in entity.get("aliases", []):
|
||||
sanctioned_names.add(alias)
|
||||
|
||||
# 标记图谱中的受制裁节点
|
||||
for node in G.nodes():
|
||||
for sname in sanctioned_names:
|
||||
if node in sname or sname in node:
|
||||
G.nodes[node]["is_sanctioned"] = True
|
||||
G.nodes[node]["risk_level"] = "高危"
|
||||
G.nodes[node]["color"] = "#F44336" # 红色
|
||||
break
|
||||
|
||||
|
||||
def get_node_info(G: nx.DiGraph, node_name: str) -> dict:
|
||||
"""获取节点详细信息"""
|
||||
if node_name not in G:
|
||||
return {"error": f"节点 '{node_name}' 不存在"}
|
||||
|
||||
node_data = dict(G.nodes[node_name])
|
||||
predecessors = list(G.predecessors(node_name))
|
||||
successors = list(G.successors(node_name))
|
||||
|
||||
return {
|
||||
"name": node_name,
|
||||
"attributes": node_data,
|
||||
"upstream": predecessors,
|
||||
"downstream": successors,
|
||||
"degree": G.degree(node_name),
|
||||
}
|
||||
|
||||
|
||||
def get_graph_stats(G: nx.DiGraph) -> dict:
|
||||
"""获取图谱统计信息"""
|
||||
node_types = {}
|
||||
for _, data in G.nodes(data=True):
|
||||
t = data.get("node_type", "未知")
|
||||
node_types[t] = node_types.get(t, 0) + 1
|
||||
|
||||
sanctioned_count = sum(
|
||||
1 for _, data in G.nodes(data=True) if data.get("is_sanctioned", False)
|
||||
)
|
||||
|
||||
return {
|
||||
"total_nodes": G.number_of_nodes(),
|
||||
"total_edges": G.number_of_edges(),
|
||||
"node_types": node_types,
|
||||
"sanctioned_nodes": sanctioned_count,
|
||||
"density": nx.density(G),
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
知识图谱可视化模块
|
||||
使用 pyvis 生成交互式网络图,支持嵌入 Streamlit
|
||||
"""
|
||||
import logging
|
||||
import tempfile
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
import networkx as nx
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# 节点类型对应的颜色和形状
|
||||
NODE_STYLES = {
|
||||
"科创企业": {"color": "#4CAF50", "shape": "dot", "size": 30},
|
||||
"供应商": {"color": "#FF9800", "shape": "diamond", "size": 20},
|
||||
"客户": {"color": "#9C27B0", "shape": "triangle", "size": 20},
|
||||
"核心人员": {"color": "#2196F3", "shape": "star", "size": 15},
|
||||
"投资方": {"color": "#607D8B", "shape": "square", "size": 20},
|
||||
"企业": {"color": "#4CAF50", "shape": "dot", "size": 25},
|
||||
}
|
||||
|
||||
# 受制裁节点的样式覆盖
|
||||
SANCTIONED_STYLE = {"color": "#F44336", "size": 35}
|
||||
# 受限边的样式
|
||||
RESTRICTED_EDGE_STYLE = {"color": "#F44336", "dashes": True, "width": 3}
|
||||
|
||||
|
||||
def generate_interactive_graph(
|
||||
G: nx.DiGraph,
|
||||
highlight_company: Optional[str] = None,
|
||||
output_path: Optional[str] = None,
|
||||
height: str = "600px",
|
||||
width: str = "100%",
|
||||
) -> str:
|
||||
"""
|
||||
生成交互式知识图谱 HTML
|
||||
"""
|
||||
try:
|
||||
from pyvis.network import Network
|
||||
except ImportError:
|
||||
logger.error("pyvis 未安装,请运行: pip install pyvis")
|
||||
return _generate_fallback_html(G)
|
||||
|
||||
net = Network(
|
||||
height=height,
|
||||
width=width,
|
||||
directed=True,
|
||||
notebook=False,
|
||||
bgcolor="#1a1a2e",
|
||||
font_color="white",
|
||||
)
|
||||
|
||||
# 物理引擎配置
|
||||
net.set_options("""
|
||||
{
|
||||
"physics": {
|
||||
"forceAtlas2Based": {
|
||||
"gravitationalConstant": -50,
|
||||
"centralGravity": 0.01,
|
||||
"springLength": 150,
|
||||
"springConstant": 0.08
|
||||
},
|
||||
"solver": "forceAtlas2Based",
|
||||
"stabilization": {"iterations": 100}
|
||||
},
|
||||
"interaction": {
|
||||
"hover": true,
|
||||
"tooltipDelay": 200,
|
||||
"navigationButtons": true
|
||||
}
|
||||
}
|
||||
""")
|
||||
|
||||
# 添加节点
|
||||
for node, data in G.nodes(data=True):
|
||||
node_type = data.get("node_type", "企业")
|
||||
style = NODE_STYLES.get(node_type, NODE_STYLES["企业"]).copy()
|
||||
|
||||
# 受制裁节点特殊样式
|
||||
if data.get("is_sanctioned", False):
|
||||
style.update(SANCTIONED_STYLE)
|
||||
|
||||
# 高亮选中企业
|
||||
if highlight_company and node == highlight_company:
|
||||
style["color"] = "#FFD700" # 金色
|
||||
style["size"] = 45
|
||||
style["borderWidth"] = 3
|
||||
|
||||
# 构建标签和悬浮提示
|
||||
label = node.split("@")[0] if "@" in node else node
|
||||
title_parts = [f"<b>{label}</b>", f"类型: {node_type}"]
|
||||
if data.get("is_sanctioned"):
|
||||
title_parts.append("⚠️ <b>已被制裁</b>")
|
||||
if data.get("stock_code"):
|
||||
title_parts.append(f"代码: {data['stock_code']}")
|
||||
if data.get("industry"):
|
||||
title_parts.append(f"行业: {data['industry']}")
|
||||
if data.get("title"):
|
||||
title_parts.append(f"职位: {data['title']}")
|
||||
if data.get("status"):
|
||||
title_parts.append(f"状态: {data['status']}")
|
||||
|
||||
net.add_node(
|
||||
node,
|
||||
label=label,
|
||||
title="<br>".join(title_parts),
|
||||
color=style["color"],
|
||||
shape=style["shape"],
|
||||
size=style["size"],
|
||||
)
|
||||
|
||||
# 添加边
|
||||
for u, v, data in G.edges(data=True):
|
||||
edge_style = {
|
||||
"color": "#666666",
|
||||
"width": 1,
|
||||
"arrows": "to",
|
||||
}
|
||||
|
||||
# 受限边特殊样式
|
||||
if data.get("status") == "受限":
|
||||
edge_style.update(RESTRICTED_EDGE_STYLE)
|
||||
elif data.get("critical", False):
|
||||
edge_style["width"] = 2
|
||||
edge_style["color"] = "#FFC107" # 关键边用黄色
|
||||
|
||||
relation = data.get("relation", "")
|
||||
net.add_edge(
|
||||
u, v,
|
||||
title=relation,
|
||||
label=relation if len(relation) <= 8 else "",
|
||||
**edge_style,
|
||||
)
|
||||
|
||||
# 输出 HTML
|
||||
if output_path is None:
|
||||
output_path = str(Path(tempfile.gettempdir()) / "kg_visualization.html")
|
||||
|
||||
net.save_graph(output_path)
|
||||
|
||||
# 读取 HTML 内容
|
||||
with open(output_path, "r", encoding="utf-8") as f:
|
||||
return f.read()
|
||||
|
||||
|
||||
def _generate_fallback_html(G: nx.DiGraph) -> str:
|
||||
"""当 pyvis 不可用时的备用 HTML 可视化"""
|
||||
nodes_info = []
|
||||
for node, data in G.nodes(data=True):
|
||||
label = node.split("@")[0] if "@" in node else node
|
||||
is_sanctioned = data.get("is_sanctioned", False)
|
||||
nodes_info.append(f"<li style='color: {'red' if is_sanctioned else 'green'}'>{label} ({data.get('node_type', '未知')})</li>")
|
||||
|
||||
return f"""
|
||||
<html><body style='background: #1a1a2e; color: white; padding: 20px;'>
|
||||
<h2>📊 知识图谱节点列表(pyvis 未安装,使用简化视图)</h2>
|
||||
<p>节点数: {G.number_of_nodes()} | 边数: {G.number_of_edges()}</p>
|
||||
<ul>{''.join(nodes_info[:50])}</ul>
|
||||
<p style='color: #888;'>安装 pyvis 以获得交互式可视化: pip install pyvis</p>
|
||||
</body></html>
|
||||
"""
|
||||
|
||||
|
||||
def get_subgraph_for_company(G: nx.DiGraph, company: str, depth: int = 2) -> nx.DiGraph:
|
||||
"""
|
||||
提取以指定企业为中心的子图(上下游 N 层)
|
||||
"""
|
||||
if company not in G:
|
||||
return nx.DiGraph()
|
||||
|
||||
# 收集相关节点
|
||||
related_nodes = {company}
|
||||
|
||||
# 上游(前驱)
|
||||
current_layer = {company}
|
||||
for _ in range(depth):
|
||||
next_layer = set()
|
||||
for node in current_layer:
|
||||
next_layer.update(G.predecessors(node))
|
||||
related_nodes.update(next_layer)
|
||||
current_layer = next_layer
|
||||
|
||||
# 下游(后继)
|
||||
current_layer = {company}
|
||||
for _ in range(depth):
|
||||
next_layer = set()
|
||||
for node in current_layer:
|
||||
next_layer.update(G.successors(node))
|
||||
related_nodes.update(next_layer)
|
||||
current_layer = next_layer
|
||||
|
||||
return G.subgraph(related_nodes).copy()
|
||||
@@ -0,0 +1,189 @@
|
||||
function neighbourhoodHighlight(params) {
|
||||
// console.log("in nieghbourhoodhighlight");
|
||||
allNodes = nodes.get({ returnType: "Object" });
|
||||
// originalNodes = JSON.parse(JSON.stringify(allNodes));
|
||||
// if something is selected:
|
||||
if (params.nodes.length > 0) {
|
||||
highlightActive = true;
|
||||
var i, j;
|
||||
var selectedNode = params.nodes[0];
|
||||
var degrees = 2;
|
||||
|
||||
// mark all nodes as hard to read.
|
||||
for (let nodeId in allNodes) {
|
||||
// nodeColors[nodeId] = allNodes[nodeId].color;
|
||||
allNodes[nodeId].color = "rgba(200,200,200,0.5)";
|
||||
if (allNodes[nodeId].hiddenLabel === undefined) {
|
||||
allNodes[nodeId].hiddenLabel = allNodes[nodeId].label;
|
||||
allNodes[nodeId].label = undefined;
|
||||
}
|
||||
}
|
||||
var connectedNodes = network.getConnectedNodes(selectedNode);
|
||||
var allConnectedNodes = [];
|
||||
|
||||
// get the second degree nodes
|
||||
for (i = 1; i < degrees; i++) {
|
||||
for (j = 0; j < connectedNodes.length; j++) {
|
||||
allConnectedNodes = allConnectedNodes.concat(
|
||||
network.getConnectedNodes(connectedNodes[j])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// all second degree nodes get a different color and their label back
|
||||
for (i = 0; i < allConnectedNodes.length; i++) {
|
||||
// allNodes[allConnectedNodes[i]].color = "pink";
|
||||
allNodes[allConnectedNodes[i]].color = "rgba(150,150,150,0.75)";
|
||||
if (allNodes[allConnectedNodes[i]].hiddenLabel !== undefined) {
|
||||
allNodes[allConnectedNodes[i]].label =
|
||||
allNodes[allConnectedNodes[i]].hiddenLabel;
|
||||
allNodes[allConnectedNodes[i]].hiddenLabel = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// all first degree nodes get their own color and their label back
|
||||
for (i = 0; i < connectedNodes.length; i++) {
|
||||
// allNodes[connectedNodes[i]].color = undefined;
|
||||
allNodes[connectedNodes[i]].color = nodeColors[connectedNodes[i]];
|
||||
if (allNodes[connectedNodes[i]].hiddenLabel !== undefined) {
|
||||
allNodes[connectedNodes[i]].label =
|
||||
allNodes[connectedNodes[i]].hiddenLabel;
|
||||
allNodes[connectedNodes[i]].hiddenLabel = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
// the main node gets its own color and its label back.
|
||||
// allNodes[selectedNode].color = undefined;
|
||||
allNodes[selectedNode].color = nodeColors[selectedNode];
|
||||
if (allNodes[selectedNode].hiddenLabel !== undefined) {
|
||||
allNodes[selectedNode].label = allNodes[selectedNode].hiddenLabel;
|
||||
allNodes[selectedNode].hiddenLabel = undefined;
|
||||
}
|
||||
} else if (highlightActive === true) {
|
||||
// console.log("highlightActive was true");
|
||||
// reset all nodes
|
||||
for (let nodeId in allNodes) {
|
||||
// allNodes[nodeId].color = "purple";
|
||||
allNodes[nodeId].color = nodeColors[nodeId];
|
||||
// delete allNodes[nodeId].color;
|
||||
if (allNodes[nodeId].hiddenLabel !== undefined) {
|
||||
allNodes[nodeId].label = allNodes[nodeId].hiddenLabel;
|
||||
allNodes[nodeId].hiddenLabel = undefined;
|
||||
}
|
||||
}
|
||||
highlightActive = false;
|
||||
}
|
||||
|
||||
// transform the object into an array
|
||||
var updateArray = [];
|
||||
if (params.nodes.length > 0) {
|
||||
for (let nodeId in allNodes) {
|
||||
if (allNodes.hasOwnProperty(nodeId)) {
|
||||
// console.log(allNodes[nodeId]);
|
||||
updateArray.push(allNodes[nodeId]);
|
||||
}
|
||||
}
|
||||
nodes.update(updateArray);
|
||||
} else {
|
||||
// console.log("Nothing was selected");
|
||||
for (let nodeId in allNodes) {
|
||||
if (allNodes.hasOwnProperty(nodeId)) {
|
||||
// console.log(allNodes[nodeId]);
|
||||
// allNodes[nodeId].color = {};
|
||||
updateArray.push(allNodes[nodeId]);
|
||||
}
|
||||
}
|
||||
nodes.update(updateArray);
|
||||
}
|
||||
}
|
||||
|
||||
function filterHighlight(params) {
|
||||
allNodes = nodes.get({ returnType: "Object" });
|
||||
// if something is selected:
|
||||
if (params.nodes.length > 0) {
|
||||
filterActive = true;
|
||||
let selectedNodes = params.nodes;
|
||||
|
||||
// hiding all nodes and saving the label
|
||||
for (let nodeId in allNodes) {
|
||||
allNodes[nodeId].hidden = true;
|
||||
if (allNodes[nodeId].savedLabel === undefined) {
|
||||
allNodes[nodeId].savedLabel = allNodes[nodeId].label;
|
||||
allNodes[nodeId].label = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
for (let i=0; i < selectedNodes.length; i++) {
|
||||
allNodes[selectedNodes[i]].hidden = false;
|
||||
if (allNodes[selectedNodes[i]].savedLabel !== undefined) {
|
||||
allNodes[selectedNodes[i]].label = allNodes[selectedNodes[i]].savedLabel;
|
||||
allNodes[selectedNodes[i]].savedLabel = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
} else if (filterActive === true) {
|
||||
// reset all nodes
|
||||
for (let nodeId in allNodes) {
|
||||
allNodes[nodeId].hidden = false;
|
||||
if (allNodes[nodeId].savedLabel !== undefined) {
|
||||
allNodes[nodeId].label = allNodes[nodeId].savedLabel;
|
||||
allNodes[nodeId].savedLabel = undefined;
|
||||
}
|
||||
}
|
||||
filterActive = false;
|
||||
}
|
||||
|
||||
// transform the object into an array
|
||||
var updateArray = [];
|
||||
if (params.nodes.length > 0) {
|
||||
for (let nodeId in allNodes) {
|
||||
if (allNodes.hasOwnProperty(nodeId)) {
|
||||
updateArray.push(allNodes[nodeId]);
|
||||
}
|
||||
}
|
||||
nodes.update(updateArray);
|
||||
} else {
|
||||
for (let nodeId in allNodes) {
|
||||
if (allNodes.hasOwnProperty(nodeId)) {
|
||||
updateArray.push(allNodes[nodeId]);
|
||||
}
|
||||
}
|
||||
nodes.update(updateArray);
|
||||
}
|
||||
}
|
||||
|
||||
function selectNode(nodes) {
|
||||
network.selectNodes(nodes);
|
||||
neighbourhoodHighlight({ nodes: nodes });
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function selectNodes(nodes) {
|
||||
network.selectNodes(nodes);
|
||||
filterHighlight({nodes: nodes});
|
||||
return nodes;
|
||||
}
|
||||
|
||||
function highlightFilter(filter) {
|
||||
let selectedNodes = []
|
||||
let selectedProp = filter['property']
|
||||
if (filter['item'] === 'node') {
|
||||
let allNodes = nodes.get({ returnType: "Object" });
|
||||
for (let nodeId in allNodes) {
|
||||
if (allNodes[nodeId][selectedProp] && filter['value'].includes((allNodes[nodeId][selectedProp]).toString())) {
|
||||
selectedNodes.push(nodeId)
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (filter['item'] === 'edge'){
|
||||
let allEdges = edges.get({returnType: 'object'});
|
||||
// check if the selected property exists for selected edge and select the nodes connected to the edge
|
||||
for (let edge in allEdges) {
|
||||
if (allEdges[edge][selectedProp] && filter['value'].includes((allEdges[edge][selectedProp]).toString())) {
|
||||
selectedNodes.push(allEdges[edge]['from'])
|
||||
selectedNodes.push(allEdges[edge]['to'])
|
||||
}
|
||||
}
|
||||
}
|
||||
selectNodes(selectedNodes)
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user