Backup before system reinstall
This commit is contained in:
1
jixia_academy/ui/streamlit/__init__.py
Normal file
1
jixia_academy/ui/streamlit/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# 应用模块
|
||||
228
jixia_academy/ui/streamlit/streamlit_app.py
Normal file
228
jixia_academy/ui/streamlit/streamlit_app.py
Normal file
@@ -0,0 +1,228 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
炼妖壶 (Lianyaohu) - 稷下学宫AI辩论系统
|
||||
主Streamlit应用入口
|
||||
|
||||
重构版本:
|
||||
- 清晰的模块化结构
|
||||
- 统一的配置管理
|
||||
- 安全的密钥处理
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目根目录到Python路径
|
||||
project_root = Path(__file__).parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
def configure_page():
|
||||
"""配置页面基本设置"""
|
||||
st.set_page_config(
|
||||
page_title="炼妖壶 - 稷下学宫AI辩论系统",
|
||||
page_icon="🏛️",
|
||||
layout="wide",
|
||||
initial_sidebar_state="expanded"
|
||||
)
|
||||
|
||||
def show_header():
|
||||
"""显示页面头部"""
|
||||
st.title("🏛️ 炼妖壶 - 稷下学宫AI辩论系统")
|
||||
st.markdown("**基于中国哲学传统的多AI智能体辩论平台**")
|
||||
|
||||
# 显示系统状态
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
st.metric("系统状态", "🟢 运行中")
|
||||
with col2:
|
||||
st.metric("AI模型", "OpenRouter")
|
||||
with col3:
|
||||
# 更新数据源展示,加入 OpenBB
|
||||
st.metric("数据源", "RapidAPI + OpenBB")
|
||||
|
||||
def show_sidebar():
|
||||
"""显示侧边栏"""
|
||||
with st.sidebar:
|
||||
st.markdown("## 🎛️ 控制面板")
|
||||
|
||||
# 系统信息
|
||||
st.markdown("### 📊 系统信息")
|
||||
st.info("**版本**: v2.0 (重构版)")
|
||||
st.info("**状态**: 迁移完成")
|
||||
|
||||
# 配置检查
|
||||
st.markdown("### 🔧 配置状态")
|
||||
try:
|
||||
from config.doppler_config import validate_config
|
||||
if validate_config():
|
||||
st.success("✅ 配置正常")
|
||||
else:
|
||||
st.error("❌ 配置异常")
|
||||
except Exception as e:
|
||||
st.warning(f"⚠️ 配置检查失败: {str(e)}")
|
||||
|
||||
# 快速操作
|
||||
st.markdown("### ⚡ 快速操作")
|
||||
if st.button("🧪 测试API连接"):
|
||||
test_api_connections()
|
||||
|
||||
if st.button("🏛️ 启动八仙论道"):
|
||||
start_jixia_debate()
|
||||
|
||||
def test_api_connections():
|
||||
"""测试API连接"""
|
||||
with st.spinner("正在测试API连接..."):
|
||||
try:
|
||||
from scripts.api_health_check import test_openrouter_api, test_rapidapi_connection
|
||||
|
||||
openrouter_ok = test_openrouter_api()
|
||||
rapidapi_ok = test_rapidapi_connection()
|
||||
|
||||
if openrouter_ok and rapidapi_ok:
|
||||
st.success("✅ 所有API连接正常")
|
||||
else:
|
||||
st.error("❌ 部分API连接失败")
|
||||
except Exception as e:
|
||||
st.error(f"❌ API测试异常: {str(e)}")
|
||||
|
||||
def start_jixia_debate():
|
||||
"""启动稷下学宫辩论"""
|
||||
with st.spinner("正在启动稷下学宫八仙论道..."):
|
||||
try:
|
||||
from config.settings import get_rapidapi_key
|
||||
from src.jixia.engines.perpetual_engine import JixiaPerpetualEngine
|
||||
|
||||
api_key = get_rapidapi_key()
|
||||
engine = JixiaPerpetualEngine(api_key)
|
||||
|
||||
# 运行辩论
|
||||
results = engine.simulate_jixia_debate('TSLA')
|
||||
|
||||
st.success("✅ 八仙论道完成")
|
||||
st.json(results)
|
||||
except Exception as e:
|
||||
st.error(f"❌ 辩论启动失败: {str(e)}")
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
configure_page()
|
||||
show_header()
|
||||
show_sidebar()
|
||||
|
||||
# 主内容区域
|
||||
st.markdown("---")
|
||||
|
||||
# 选项卡(新增 OpenBB 数据页签和AI协作页签)
|
||||
tab1, tab2, tab3, tab4, tab5 = st.tabs(["🏛️ 稷下学宫", "🌍 天下体系", "📊 数据分析", "📈 OpenBB 数据", "🤖 AI协作"])
|
||||
|
||||
with tab1:
|
||||
st.markdown("### 🏛️ 稷下学宫 - 八仙论道")
|
||||
st.markdown("**多AI智能体辩论系统,基于中国传统八仙文化**")
|
||||
|
||||
# 辩论模式选择
|
||||
debate_mode = st.selectbox(
|
||||
"选择辩论模式",
|
||||
["ADK模式 (太上老君主持)", "传统模式 (RapidAPI数据)"]
|
||||
)
|
||||
|
||||
if debate_mode == "ADK模式 (太上老君主持)":
|
||||
from app.tabs.adk_debate_tab import render_adk_debate_tab
|
||||
render_adk_debate_tab()
|
||||
|
||||
else:
|
||||
# 传统模式
|
||||
col1, col2 = st.columns([2, 1])
|
||||
with col1:
|
||||
topic = st.text_input("辩论主题 (股票代码)", value="TSLA")
|
||||
with col2:
|
||||
if st.button("🎭 开始辩论", type="primary"):
|
||||
start_debate_session(topic)
|
||||
|
||||
# 显示辩论历史
|
||||
if 'debate_history' in st.session_state:
|
||||
st.markdown("#### 📜 辩论记录")
|
||||
for record in st.session_state.debate_history[-3:]: # 显示最近3次
|
||||
with st.expander(f"🎭 {record['topic']} - {record['time']}"):
|
||||
st.json(record['results'])
|
||||
|
||||
with tab2:
|
||||
st.markdown("### 🌍 天下体系分析")
|
||||
try:
|
||||
from app.tabs.tianxia_tab import render_tianxia_tab
|
||||
render_tianxia_tab()
|
||||
except Exception as e:
|
||||
st.error(f"❌ 天下体系模块加载失败: {str(e)}")
|
||||
|
||||
with tab3:
|
||||
st.markdown("### 📊 数据分析")
|
||||
st.info("🚧 数据分析模块开发中...")
|
||||
|
||||
# 显示系统统计
|
||||
try:
|
||||
from config.settings import get_rapidapi_key
|
||||
from src.jixia.engines.perpetual_engine import JixiaPerpetualEngine
|
||||
|
||||
api_key = get_rapidapi_key()
|
||||
engine = JixiaPerpetualEngine(api_key)
|
||||
stats = engine.get_usage_stats()
|
||||
|
||||
col1, col2, col3 = st.columns(3)
|
||||
with col1:
|
||||
st.metric("API调用总数", stats['total_calls'])
|
||||
with col2:
|
||||
st.metric("活跃API数", f"{stats['active_apis']}/{stats['total_apis']}")
|
||||
with col3:
|
||||
st.metric("未使用API", stats['unused_count'])
|
||||
|
||||
except Exception as e:
|
||||
st.warning(f"⚠️ 无法加载统计数据: {str(e)}")
|
||||
|
||||
with tab5:
|
||||
st.markdown("### 🤖 AI协作")
|
||||
try:
|
||||
from app.tabs.ai_collaboration_tab import main as ai_collaboration_main
|
||||
ai_collaboration_main()
|
||||
except Exception as e:
|
||||
st.error(f"❌ AI协作模块加载失败: {str(e)}")
|
||||
|
||||
def start_debate_session(topic: str):
|
||||
"""启动辩论会话"""
|
||||
if not topic:
|
||||
st.error("请输入辩论主题")
|
||||
return
|
||||
|
||||
with st.spinner(f"🏛️ 八仙正在就 {topic} 展开论道..."):
|
||||
try:
|
||||
from config.settings import get_rapidapi_key
|
||||
from src.jixia.engines.perpetual_engine import JixiaPerpetualEngine
|
||||
from datetime import datetime
|
||||
|
||||
api_key = get_rapidapi_key()
|
||||
engine = JixiaPerpetualEngine(api_key)
|
||||
|
||||
# 运行辩论
|
||||
results = engine.simulate_jixia_debate(topic)
|
||||
|
||||
# 保存到会话状态
|
||||
if 'debate_history' not in st.session_state:
|
||||
st.session_state.debate_history = []
|
||||
|
||||
st.session_state.debate_history.append({
|
||||
'topic': topic,
|
||||
'time': datetime.now().strftime('%Y-%m-%d %H:%M:%S'),
|
||||
'results': {name: {'success': result.success, 'api_used': result.api_used}
|
||||
for name, result in results.items()}
|
||||
})
|
||||
|
||||
st.success(f"✅ 八仙论道完成!共有 {len(results)} 位仙人参与")
|
||||
|
||||
# 显示结果摘要
|
||||
successful_debates = sum(1 for result in results.values() if result.success)
|
||||
st.info(f"📊 成功获取数据: {successful_debates}/{len(results)} 位仙人")
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"❌ 辩论启动失败: {str(e)}")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
205
jixia_academy/ui/streamlit/tabs/adk_debate_tab.py
Normal file
205
jixia_academy/ui/streamlit/tabs/adk_debate_tab.py
Normal file
@@ -0,0 +1,205 @@
|
||||
|
||||
import streamlit as st
|
||||
import asyncio
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Dict, Any, List
|
||||
|
||||
# Ensure the main project directory is in the Python path
|
||||
project_root = Path(__file__).parent.parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
try:
|
||||
from google.adk import Agent, Runner
|
||||
from google.adk.sessions import InMemorySessionService, Session
|
||||
from google.genai import types
|
||||
ADK_AVAILABLE = True
|
||||
except ImportError:
|
||||
ADK_AVAILABLE = False
|
||||
# 创建占位符类
|
||||
class Agent:
|
||||
pass
|
||||
class Runner:
|
||||
pass
|
||||
class InMemorySessionService:
|
||||
pass
|
||||
class Session:
|
||||
pass
|
||||
class types:
|
||||
class Content:
|
||||
pass
|
||||
class Part:
|
||||
pass
|
||||
|
||||
async def _get_llm_reply(runner: Runner, session: Session, prompt: str) -> str:
|
||||
"""Helper function to call a Runner and get a text reply."""
|
||||
content = types.Content(role='user', parts=[types.Part(text=prompt)])
|
||||
response = runner.run_async(
|
||||
user_id=session.user_id,
|
||||
session_id=session.id,
|
||||
new_message=content
|
||||
)
|
||||
|
||||
reply = ""
|
||||
async for event in response:
|
||||
if hasattr(event, 'content') and event.content and hasattr(event.content, 'parts'):
|
||||
for part in event.content.parts:
|
||||
if hasattr(part, 'text') and part.text:
|
||||
reply += str(part.text)
|
||||
elif hasattr(event, 'text') and event.text:
|
||||
reply += str(event.text)
|
||||
|
||||
return reply.strip()
|
||||
|
||||
async def run_adk_debate_streamlit(topic: str, participants: List[str], rounds: int):
|
||||
"""
|
||||
Runs the ADK turn-based debate and yields each statement for Streamlit display.
|
||||
"""
|
||||
try:
|
||||
yield "🚀 **启动ADK八仙轮流辩论 (太上老君主持)...**"
|
||||
|
||||
all_immortals = ["铁拐李", "吕洞宾", "何仙姑", "张果老", "蓝采和", "汉钟离", "韩湘子", "曹国舅"]
|
||||
if not participants:
|
||||
participants = all_immortals
|
||||
|
||||
character_configs = {
|
||||
"太上老君": {"name": "太上老君", "model": "gemini-1.5-pro", "instruction": "你是太上老君,天道化身,辩论的主持人。你的言辞沉稳、公正、充满智慧。你的任务是:1. 对辩论主题进行开场介绍。2. 在每轮开始时进行引导。3. 在辩论结束后,对所有观点进行全面、客观的总结。保持中立,不偏袒任何一方。"},
|
||||
"铁拐李": {"name": "铁拐李", "model": "gemini-1.5-flash", "instruction": "你是铁拐李,八仙中的逆向思维专家。你善于从批判和质疑的角度看问题,发言风格直接、犀利,但富有智慧。"},
|
||||
"吕洞宾": {"name": "吕洞宾", "model": "gemini-1.5-flash", "instruction": "你是吕洞宾,八仙中的理性分析者。你善于平衡各方观点,用理性和逻辑来分析问题,发言风格温和而深刻。"},
|
||||
"何仙姑": {"name": "何仙姑", "model": "gemini-1.5-flash", "instruction": "你是何仙姑,八仙中的风险控制专家。你总是从风险管理的角度思考问题,善于发现潜在危险,发言风格谨慎、细致。"},
|
||||
"张果老": {"name": "张果老", "model": "gemini-1.5-flash", "instruction": "你是张果老,八仙中的历史智慧者。你善于从历史数据中寻找规律和智慧,提供长期视角,发言风格沉稳、博学。"},
|
||||
"蓝采和": {"name": "蓝采和", "model": "gemini-1.5-flash", "instruction": "你是蓝采和,八仙中的创新思维者。你善于从新兴视角和非传统方法来看待问题,发言风格活泼、新颖。"},
|
||||
"汉钟离": {"name": "汉钟离", "model": "gemini-1.5-flash", "instruction": "你是汉钟离,八仙中的平衡协调者。你善于综合各方观点,寻求和谐统一的解决方案,发言风格平和、包容。"},
|
||||
"韩湘子": {"name": "韩湘子", "model": "gemini-1.5-flash", "instruction": "你是韩湘子,八仙中的艺术感知者。你善于从美学和感性的角度分析问题,发言风格优雅、感性。"},
|
||||
"曹国舅": {"name": "曹国舅", "model": "gemini-1.5-flash", "instruction": "你是曹国舅,八仙中的实务执行者。你关注实际操作和具体细节,发言风格务实、严谨。"}
|
||||
}
|
||||
|
||||
session_service = InMemorySessionService()
|
||||
session = await session_service.create_session(state={}, app_name="稷下学宫八仙论道系统-Streamlit", user_id="st_user")
|
||||
|
||||
runners: Dict[str, Runner] = {}
|
||||
for name, config in character_configs.items():
|
||||
if name == "太上老君" or name in participants:
|
||||
agent = Agent(name=config["name"], model=config["model"], instruction=config["instruction"])
|
||||
runners[name] = Runner(app_name="稷下学宫八仙论道系统-Streamlit", agent=agent, session_service=session_service)
|
||||
|
||||
host_runner = runners.get("太上老君")
|
||||
if not host_runner:
|
||||
yield "❌ **主持人太上老君初始化失败。**"
|
||||
return
|
||||
|
||||
yield f"🎯 **参与仙人**: {', '.join(participants)}"
|
||||
debate_history = []
|
||||
|
||||
# Opening statement
|
||||
opening_prompt = f"请为本次关于“{topic}”的辩论,发表一段公正、深刻的开场白,并宣布辩论开始。"
|
||||
opening_statement = await _get_llm_reply(host_runner, session, opening_prompt)
|
||||
yield f"👑 **太上老君**: {opening_statement}"
|
||||
|
||||
# Debate rounds
|
||||
for round_num in range(rounds):
|
||||
round_intro_prompt = f"请为第 {round_num + 1} 轮辩论说一段引导语。"
|
||||
round_intro = await _get_llm_reply(host_runner, session, round_intro_prompt)
|
||||
yield f"👑 **太上老君**: {round_intro}"
|
||||
|
||||
for name in participants:
|
||||
if name not in runners: continue
|
||||
|
||||
history_context = f"\n最近的论道内容:\n" + "\n".join([f"- {h}" for h in debate_history[-5:]]) if debate_history else ""
|
||||
prompt = f"论道主题: {topic}{history_context}\n\n请从你的角色特点出发,简洁地发表观点。"
|
||||
|
||||
reply = await _get_llm_reply(runners[name], session, prompt)
|
||||
yield f"🗣️ **{name}**: {reply}"
|
||||
|
||||
debate_history.append(f"{name}: {reply}")
|
||||
await asyncio.sleep(1)
|
||||
|
||||
# Summary
|
||||
summary_prompt = f"辩论已结束。以下是完整的辩论记录:\n\n{' '.join(debate_history)}\n\n请对本次辩论进行全面、公正、深刻的总结。"
|
||||
summary = await _get_llm_reply(host_runner, session, summary_prompt)
|
||||
yield f"👑 **太上老君**: {summary}"
|
||||
|
||||
for runner in runners.values():
|
||||
await runner.close()
|
||||
|
||||
yield "🎉 **ADK八仙轮流辩论完成!**"
|
||||
|
||||
except Exception as e:
|
||||
yield f"❌ **运行ADK八仙轮流辩论失败**: {e}"
|
||||
import traceback
|
||||
st.error(traceback.format_exc())
|
||||
|
||||
def render_adk_debate_tab():
|
||||
"""Renders the Streamlit UI for the ADK Debate tab."""
|
||||
|
||||
# 检查 ADK 是否可用
|
||||
if not ADK_AVAILABLE:
|
||||
st.error("🚫 Google ADK 模块未安装或不可用")
|
||||
st.info("📦 正在安装 Google ADK,请稍候...")
|
||||
st.info("💡 安装完成后请刷新页面")
|
||||
|
||||
with st.expander("📋 安装说明"):
|
||||
st.code("""
|
||||
# 安装 Google ADK
|
||||
pip install google-adk>=1.12.0
|
||||
|
||||
# 或从 GitHub 安装开发版
|
||||
pip install git+https://github.com/google/adk-python.git@main
|
||||
""")
|
||||
return
|
||||
st.markdown("### 🏛️ 八仙论道 (ADK版 - 太上老君主持)")
|
||||
|
||||
topic = st.text_input(
|
||||
"辩论主题",
|
||||
value="AI是否应该拥有创造力?",
|
||||
key="adk_topic_input"
|
||||
)
|
||||
|
||||
all_immortals = ["铁拐李", "吕洞宾", "何仙姑", "张果老", "蓝采和", "汉钟离", "韩湘子", "曹国舅"]
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
rounds = st.number_input("辩论轮数", min_value=1, max_value=5, value=1, key="adk_rounds_input")
|
||||
with col2:
|
||||
participants = st.multiselect(
|
||||
"选择参与的仙人 (默认全选)",
|
||||
options=all_immortals,
|
||||
default=all_immortals,
|
||||
key="adk_participants_select"
|
||||
)
|
||||
|
||||
if st.button("🚀 开始论道", key="start_adk_debate_button", type="primary"):
|
||||
if not topic:
|
||||
st.error("请输入辩论主题。")
|
||||
return
|
||||
if not participants:
|
||||
st.error("请至少选择一位参与的仙人。")
|
||||
return
|
||||
|
||||
st.markdown("---")
|
||||
st.markdown("#### 📜 论道实录")
|
||||
|
||||
# Placeholder for real-time output
|
||||
output_container = st.empty()
|
||||
full_log = ""
|
||||
|
||||
# Run the async debate function
|
||||
try:
|
||||
# Get a new event loop for the thread
|
||||
loop = asyncio.new_event_loop()
|
||||
asyncio.set_event_loop(loop)
|
||||
|
||||
# Create a coroutine object
|
||||
coro = run_adk_debate_streamlit(topic, participants, rounds)
|
||||
|
||||
# Run the coroutine until completion
|
||||
for message in loop.run_until_complete(async_generator_to_list(coro)):
|
||||
full_log += message + "\n\n"
|
||||
output_container.markdown(full_log)
|
||||
|
||||
except Exception as e:
|
||||
st.error(f"启动辩论时发生错误: {e}")
|
||||
|
||||
async def async_generator_to_list(async_gen):
|
||||
"""Helper to consume an async generator and return a list of its items."""
|
||||
return [item async for item in async_gen]
|
||||
509
jixia_academy/ui/streamlit/tabs/ai_collaboration_tab.py
Normal file
509
jixia_academy/ui/streamlit/tabs/ai_collaboration_tab.py
Normal file
@@ -0,0 +1,509 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
四AI团队协作Web界面
|
||||
基于Streamlit的实时协作监控和管理界面
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import asyncio
|
||||
import json
|
||||
import pandas as pd
|
||||
import plotly.express as px
|
||||
import plotly.graph_objects as go
|
||||
from datetime import datetime, timedelta
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
# 添加项目路径到sys.path
|
||||
project_root = Path(__file__).parent.parent.parent.parent
|
||||
sys.path.insert(0, str(project_root))
|
||||
|
||||
from src.jixia.coordination.ai_team_collaboration import (
|
||||
AITeamCollaboration, AIRole, MessageType, CollaborationType, WorkPhase
|
||||
)
|
||||
|
||||
# 页面配置
|
||||
st.set_page_config(
|
||||
page_title="四AI团队协作中心",
|
||||
page_icon="🤖",
|
||||
layout="wide",
|
||||
initial_sidebar_state="expanded"
|
||||
)
|
||||
|
||||
# 初始化协作系统
|
||||
@st.cache_resource
|
||||
def init_collaboration_system():
|
||||
"""初始化协作系统"""
|
||||
return AITeamCollaboration()
|
||||
|
||||
def main():
|
||||
"""主界面"""
|
||||
st.title("🤖 四AI团队协作中心")
|
||||
st.markdown("### OpenBB集成项目实时协作监控")
|
||||
|
||||
# 初始化系统
|
||||
collab = init_collaboration_system()
|
||||
|
||||
# 侧边栏
|
||||
with st.sidebar:
|
||||
st.header("🎯 项目状态")
|
||||
|
||||
# 当前阶段
|
||||
current_phase = st.selectbox(
|
||||
"当前工作阶段",
|
||||
[phase.value for phase in WorkPhase],
|
||||
index=list(WorkPhase).index(collab.current_phase)
|
||||
)
|
||||
|
||||
if st.button("更新阶段"):
|
||||
new_phase = WorkPhase(current_phase)
|
||||
asyncio.run(collab.advance_phase(new_phase))
|
||||
st.success(f"阶段已更新为: {current_phase}")
|
||||
st.rerun()
|
||||
|
||||
st.divider()
|
||||
|
||||
# AI状态概览
|
||||
st.subheader("🤖 AI状态概览")
|
||||
for ai_role, status in collab.ai_status.items():
|
||||
status_color = {
|
||||
"ready": "🟢",
|
||||
"active": "🔵",
|
||||
"waiting": "🟡",
|
||||
"completed_handoff": "✅",
|
||||
"received_handoff": "📥"
|
||||
}.get(status["status"], "⚪")
|
||||
|
||||
st.write(f"{status_color} **{ai_role.value}**")
|
||||
st.write(f" 📋 {status['current_task']}")
|
||||
st.write(f" 🎯 {status['role']}")
|
||||
|
||||
# 主要内容区域
|
||||
tab1, tab2, tab3, tab4, tab5 = st.tabs([
|
||||
"📢 主协作频道", "📊 AI仪表板", "🔄 工作流管理", "📈 协作分析", "⚙️ 系统管理"
|
||||
])
|
||||
|
||||
with tab1:
|
||||
render_main_collaboration(collab)
|
||||
|
||||
with tab2:
|
||||
render_ai_dashboard(collab)
|
||||
|
||||
with tab3:
|
||||
render_workflow_management(collab)
|
||||
|
||||
with tab4:
|
||||
render_collaboration_analytics(collab)
|
||||
|
||||
with tab5:
|
||||
render_system_management(collab)
|
||||
|
||||
def render_main_collaboration(collab):
|
||||
"""渲染主协作频道"""
|
||||
st.header("📢 主协作频道")
|
||||
|
||||
# 频道选择
|
||||
channel_options = {
|
||||
channel.name: channel_id
|
||||
for channel_id, channel in collab.channels.items()
|
||||
}
|
||||
|
||||
selected_channel_name = st.selectbox(
|
||||
"选择频道",
|
||||
list(channel_options.keys()),
|
||||
index=0
|
||||
)
|
||||
|
||||
selected_channel_id = channel_options[selected_channel_name]
|
||||
channel = collab.channels[selected_channel_id]
|
||||
|
||||
col1, col2 = st.columns([2, 1])
|
||||
|
||||
with col1:
|
||||
# 消息历史
|
||||
st.subheader(f"💬 {channel.name}")
|
||||
|
||||
if channel.message_history:
|
||||
for msg in channel.message_history[-10:]: # 显示最近10条消息
|
||||
sender_emoji = {
|
||||
AIRole.QWEN: "🏗️",
|
||||
AIRole.CLAUDE: "💻",
|
||||
AIRole.GEMINI: "🧪",
|
||||
AIRole.ROVODEV: "📚"
|
||||
}.get(msg.sender, "🤖")
|
||||
|
||||
with st.chat_message(msg.sender.value, avatar=sender_emoji):
|
||||
st.write(f"**{msg.message_type.value}** - {msg.timestamp.strftime('%H:%M')}")
|
||||
st.write(msg.content)
|
||||
|
||||
if msg.attachments:
|
||||
st.write("📎 附件:")
|
||||
for attachment in msg.attachments:
|
||||
st.write(f" • {attachment}")
|
||||
|
||||
if msg.tags:
|
||||
tag_html = " ".join([f"<span style='background-color: #e1f5fe; padding: 2px 6px; border-radius: 4px; font-size: 0.8em;'>{tag}</span>" for tag in msg.tags])
|
||||
st.markdown(tag_html, unsafe_allow_html=True)
|
||||
else:
|
||||
st.info("暂无消息")
|
||||
|
||||
with col2:
|
||||
# 频道信息
|
||||
st.subheader("ℹ️ 频道信息")
|
||||
st.write(f"**类型**: {channel.channel_type.value}")
|
||||
st.write(f"**参与者**: {len(channel.participants)}人")
|
||||
st.write(f"**主持人**: {channel.moderator.value}")
|
||||
st.write(f"**消息数**: {len(channel.message_history)}")
|
||||
st.write(f"**最后活动**: {channel.last_activity.strftime('%Y-%m-%d %H:%M')}")
|
||||
|
||||
# 参与者列表
|
||||
st.write("**参与者列表**:")
|
||||
for participant in channel.participants:
|
||||
role_emoji = {
|
||||
AIRole.QWEN: "🏗️",
|
||||
AIRole.CLAUDE: "💻",
|
||||
AIRole.GEMINI: "🧪",
|
||||
AIRole.ROVODEV: "📚"
|
||||
}.get(participant, "🤖")
|
||||
st.write(f"{role_emoji} {participant.value}")
|
||||
|
||||
# 发送消息区域
|
||||
st.divider()
|
||||
st.subheader("📝 发送消息")
|
||||
|
||||
col1, col2, col3 = st.columns([2, 1, 1])
|
||||
|
||||
with col1:
|
||||
message_content = st.text_area("消息内容", height=100)
|
||||
|
||||
with col2:
|
||||
sender = st.selectbox(
|
||||
"发送者",
|
||||
[role.value for role in AIRole]
|
||||
)
|
||||
|
||||
message_type = st.selectbox(
|
||||
"消息类型",
|
||||
[msg_type.value for msg_type in MessageType]
|
||||
)
|
||||
|
||||
with col3:
|
||||
receiver = st.selectbox(
|
||||
"接收者",
|
||||
["广播"] + [role.value for role in AIRole]
|
||||
)
|
||||
|
||||
priority = st.slider("优先级", 1, 5, 1)
|
||||
|
||||
if st.button("发送消息", type="primary"):
|
||||
if message_content:
|
||||
try:
|
||||
receiver_role = None if receiver == "广播" else AIRole(receiver)
|
||||
|
||||
asyncio.run(collab.send_message(
|
||||
sender=AIRole(sender),
|
||||
content=message_content,
|
||||
message_type=MessageType(message_type),
|
||||
channel_id=selected_channel_id,
|
||||
receiver=receiver_role,
|
||||
priority=priority
|
||||
))
|
||||
|
||||
st.success("消息发送成功!")
|
||||
st.rerun()
|
||||
except Exception as e:
|
||||
st.error(f"发送失败: {str(e)}")
|
||||
else:
|
||||
st.warning("请输入消息内容")
|
||||
|
||||
def render_ai_dashboard(collab):
|
||||
"""渲染AI仪表板"""
|
||||
st.header("📊 AI工作仪表板")
|
||||
|
||||
# AI选择
|
||||
selected_ai = st.selectbox(
|
||||
"选择AI",
|
||||
[role.value for role in AIRole]
|
||||
)
|
||||
|
||||
ai_role = AIRole(selected_ai)
|
||||
dashboard = collab.get_ai_dashboard(ai_role)
|
||||
|
||||
# 基本信息
|
||||
col1, col2, col3, col4 = st.columns(4)
|
||||
|
||||
with col1:
|
||||
st.metric("当前状态", dashboard["status"]["status"])
|
||||
|
||||
with col2:
|
||||
st.metric("活跃频道", len(dashboard["active_channels"]))
|
||||
|
||||
with col3:
|
||||
st.metric("待处理任务", len(dashboard["pending_tasks"]))
|
||||
|
||||
with col4:
|
||||
st.metric("协作得分", dashboard["collaboration_stats"]["collaboration_score"])
|
||||
|
||||
# 详细信息
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
# 待处理任务
|
||||
st.subheader("📋 待处理任务")
|
||||
if dashboard["pending_tasks"]:
|
||||
for task in dashboard["pending_tasks"]:
|
||||
with st.expander(f"{task['type']} - 优先级 {task['priority']}"):
|
||||
st.write(f"**来自**: {task['from']}")
|
||||
st.write(f"**频道**: {task['channel']}")
|
||||
st.write(f"**创建时间**: {task['created']}")
|
||||
st.write(f"**描述**: {task['description']}")
|
||||
else:
|
||||
st.info("暂无待处理任务")
|
||||
|
||||
with col2:
|
||||
# 最近消息
|
||||
st.subheader("📨 最近消息")
|
||||
if dashboard["recent_messages"]:
|
||||
for msg in dashboard["recent_messages"][:5]:
|
||||
priority_color = {
|
||||
1: "🔵", 2: "🟢", 3: "🟡", 4: "🟠", 5: "🔴"
|
||||
}.get(msg["priority"], "⚪")
|
||||
|
||||
st.write(f"{priority_color} **{msg['sender']}** 在 **{msg['channel']}**")
|
||||
st.write(f" {msg['content']}")
|
||||
st.write(f" ⏰ {msg['timestamp']}")
|
||||
st.divider()
|
||||
else:
|
||||
st.info("暂无最近消息")
|
||||
|
||||
# 协作统计
|
||||
st.subheader("📈 协作统计")
|
||||
|
||||
stats = dashboard["collaboration_stats"]
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
st.metric("发送消息", stats["messages_sent"])
|
||||
|
||||
with col2:
|
||||
st.metric("接收消息", stats["messages_received"])
|
||||
|
||||
with col3:
|
||||
st.metric("总消息数", stats["total_messages"])
|
||||
|
||||
def render_workflow_management(collab):
|
||||
"""渲染工作流管理"""
|
||||
st.header("🔄 工作流管理")
|
||||
|
||||
# 工作流规则
|
||||
st.subheader("📜 工作流规则")
|
||||
|
||||
rules_data = []
|
||||
for rule_id, rule in collab.workflow_rules.items():
|
||||
rules_data.append({
|
||||
"规则ID": rule.id,
|
||||
"规则名称": rule.name,
|
||||
"触发阶段": rule.trigger_phase.value,
|
||||
"目标AI": rule.target_ai.value if rule.target_ai else "无",
|
||||
"状态": "✅ 激活" if rule.is_active else "❌ 禁用"
|
||||
})
|
||||
|
||||
if rules_data:
|
||||
st.dataframe(pd.DataFrame(rules_data), use_container_width=True)
|
||||
|
||||
# 手动工作交接
|
||||
st.divider()
|
||||
st.subheader("🤝 手动工作交接")
|
||||
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
from_ai = st.selectbox("交接方", [role.value for role in AIRole])
|
||||
to_ai = st.selectbox("接收方", [role.value for role in AIRole])
|
||||
|
||||
with col2:
|
||||
task_desc = st.text_input("任务描述")
|
||||
deliverables = st.text_area("交付物列表 (每行一个)")
|
||||
|
||||
with col3:
|
||||
notes = st.text_area("备注")
|
||||
|
||||
if st.button("执行工作交接"):
|
||||
if task_desc and from_ai != to_ai:
|
||||
deliverable_list = [d.strip() for d in deliverables.split('\n') if d.strip()]
|
||||
|
||||
try:
|
||||
asyncio.run(collab.handoff_work(
|
||||
from_ai=AIRole(from_ai),
|
||||
to_ai=AIRole(to_ai),
|
||||
task_description=task_desc,
|
||||
deliverables=deliverable_list,
|
||||
notes=notes
|
||||
))
|
||||
st.success("工作交接完成!")
|
||||
st.rerun()
|
||||
except Exception as e:
|
||||
st.error(f"交接失败: {str(e)}")
|
||||
else:
|
||||
st.warning("请填写完整信息,且交接方和接收方不能相同")
|
||||
|
||||
def render_collaboration_analytics(collab):
|
||||
"""渲染协作分析"""
|
||||
st.header("📈 协作分析")
|
||||
|
||||
# 消息统计
|
||||
st.subheader("💬 消息统计")
|
||||
|
||||
# 收集所有消息数据
|
||||
message_data = []
|
||||
for channel_id, channel in collab.channels.items():
|
||||
for msg in channel.message_history:
|
||||
message_data.append({
|
||||
"频道": channel.name,
|
||||
"发送者": msg.sender.value,
|
||||
"消息类型": msg.message_type.value,
|
||||
"优先级": msg.priority,
|
||||
"时间": msg.timestamp,
|
||||
"日期": msg.timestamp.date(),
|
||||
"小时": msg.timestamp.hour
|
||||
})
|
||||
|
||||
if message_data:
|
||||
df = pd.DataFrame(message_data)
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
# 按AI发送者统计
|
||||
sender_counts = df.groupby("发送者").size().reset_index()
|
||||
sender_counts.columns = ["AI", "消息数量"]
|
||||
|
||||
fig = px.bar(sender_counts, x="AI", y="消息数量",
|
||||
title="各AI发送消息统计")
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
with col2:
|
||||
# 按消息类型统计
|
||||
type_counts = df.groupby("消息类型").size().reset_index()
|
||||
type_counts.columns = ["消息类型", "数量"]
|
||||
|
||||
fig = px.pie(type_counts, values="数量", names="消息类型",
|
||||
title="消息类型分布")
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
# 时间线分析
|
||||
st.subheader("⏰ 活跃度时间线")
|
||||
|
||||
if len(df) > 1:
|
||||
daily_counts = df.groupby("日期").size().reset_index()
|
||||
daily_counts.columns = ["日期", "消息数量"]
|
||||
|
||||
fig = px.line(daily_counts, x="日期", y="消息数量",
|
||||
title="每日消息数量趋势")
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
# 频道活跃度
|
||||
st.subheader("📢 频道活跃度")
|
||||
channel_counts = df.groupby("频道").size().reset_index()
|
||||
channel_counts.columns = ["频道", "消息数量"]
|
||||
channel_counts = channel_counts.sort_values("消息数量", ascending=True)
|
||||
|
||||
fig = px.bar(channel_counts, x="消息数量", y="频道",
|
||||
orientation='h', title="各频道消息数量")
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
else:
|
||||
st.info("暂无消息数据用于分析")
|
||||
|
||||
def render_system_management(collab):
|
||||
"""渲染系统管理"""
|
||||
st.header("⚙️ 系统管理")
|
||||
|
||||
# 系统状态
|
||||
st.subheader("🔍 系统状态")
|
||||
|
||||
col1, col2, col3 = st.columns(3)
|
||||
|
||||
with col1:
|
||||
st.metric("活跃频道", len([c for c in collab.channels.values() if c.is_active]))
|
||||
|
||||
with col2:
|
||||
total_messages = sum(len(c.message_history) for c in collab.channels.values())
|
||||
st.metric("总消息数", total_messages)
|
||||
|
||||
with col3:
|
||||
st.metric("工作流规则", len(collab.workflow_rules))
|
||||
|
||||
# 频道管理
|
||||
st.subheader("📢 频道管理")
|
||||
|
||||
for channel_id, channel in collab.channels.items():
|
||||
with st.expander(f"{channel.name} ({channel.channel_type.value})"):
|
||||
col1, col2 = st.columns(2)
|
||||
|
||||
with col1:
|
||||
st.write(f"**描述**: {channel.description}")
|
||||
st.write(f"**参与者**: {len(channel.participants)}人")
|
||||
st.write(f"**消息数**: {len(channel.message_history)}")
|
||||
st.write(f"**状态**: {'🟢 活跃' if channel.is_active else '🔴 禁用'}")
|
||||
|
||||
with col2:
|
||||
st.write("**参与者列表**:")
|
||||
for participant in channel.participants:
|
||||
st.write(f" • {participant.value}")
|
||||
|
||||
st.write(f"**主持人**: {channel.moderator.value}")
|
||||
st.write(f"**最后活动**: {channel.last_activity.strftime('%Y-%m-%d %H:%M')}")
|
||||
|
||||
# 数据导出
|
||||
st.divider()
|
||||
st.subheader("📤 数据导出")
|
||||
|
||||
if st.button("导出协作数据"):
|
||||
# 准备导出数据
|
||||
export_data = {
|
||||
"channels": {},
|
||||
"ai_status": {},
|
||||
"workflow_rules": {},
|
||||
"system_info": {
|
||||
"current_phase": collab.current_phase.value,
|
||||
"export_time": datetime.now().isoformat()
|
||||
}
|
||||
}
|
||||
|
||||
# 频道数据
|
||||
for channel_id, channel in collab.channels.items():
|
||||
export_data["channels"][channel_id] = {
|
||||
"name": channel.name,
|
||||
"type": channel.channel_type.value,
|
||||
"participants": [p.value for p in channel.participants],
|
||||
"message_count": len(channel.message_history),
|
||||
"last_activity": channel.last_activity.isoformat()
|
||||
}
|
||||
|
||||
# AI状态数据
|
||||
for ai_role, status in collab.ai_status.items():
|
||||
export_data["ai_status"][ai_role.value] = status
|
||||
|
||||
# 工作流规则
|
||||
for rule_id, rule in collab.workflow_rules.items():
|
||||
export_data["workflow_rules"][rule_id] = {
|
||||
"name": rule.name,
|
||||
"description": rule.description,
|
||||
"trigger_phase": rule.trigger_phase.value,
|
||||
"action": rule.action,
|
||||
"is_active": rule.is_active
|
||||
}
|
||||
|
||||
# 创建下载链接
|
||||
json_str = json.dumps(export_data, indent=2, ensure_ascii=False)
|
||||
st.download_button(
|
||||
label="下载协作数据 (JSON)",
|
||||
data=json_str,
|
||||
file_name=f"ai_collaboration_data_{datetime.now().strftime('%Y%m%d_%H%M%S')}.json",
|
||||
mime="application/json"
|
||||
)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
184
jixia_academy/ui/streamlit/tabs/openbb_tab.py
Normal file
184
jixia_academy/ui/streamlit/tabs/openbb_tab.py
Normal file
@@ -0,0 +1,184 @@
|
||||
import streamlit as st
|
||||
import pandas as pd
|
||||
import plotly.express as px
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
|
||||
def _check_openbb_installed() -> bool:
|
||||
try:
|
||||
# OpenBB v4 推荐用法: from openbb import obb
|
||||
from openbb import obb # noqa: F401
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
def _load_price_data(symbol: str, days: int = 365) -> pd.DataFrame:
|
||||
"""Fetch OHLCV using OpenBB v4 when available; otherwise return demo/synthetic data."""
|
||||
end = datetime.utcnow().date()
|
||||
start = end - timedelta(days=days)
|
||||
|
||||
# 优先使用 OpenBB v4
|
||||
try:
|
||||
from openbb import obb
|
||||
|
||||
# 先尝试股票路由
|
||||
try:
|
||||
out = obb.equity.price.historical(
|
||||
symbol,
|
||||
start_date=str(start),
|
||||
end_date=str(end),
|
||||
)
|
||||
except Exception:
|
||||
out = None
|
||||
|
||||
# 若股票无数据,再尝试 ETF 路由
|
||||
if out is None or (hasattr(out, "is_empty") and out.is_empty):
|
||||
try:
|
||||
out = obb.etf.price.historical(
|
||||
symbol,
|
||||
start_date=str(start),
|
||||
end_date=str(end),
|
||||
)
|
||||
except Exception:
|
||||
out = None
|
||||
|
||||
if out is not None:
|
||||
if hasattr(out, "to_df"):
|
||||
df = out.to_df()
|
||||
elif hasattr(out, "to_dataframe"):
|
||||
df = out.to_dataframe()
|
||||
else:
|
||||
# 兜底: 有些 provider 返回可序列化对象
|
||||
df = pd.DataFrame(out) # type: ignore[arg-type]
|
||||
|
||||
# 规格化列名
|
||||
if not isinstance(df, pd.DataFrame) or df.empty:
|
||||
raise ValueError("OpenBB 返回空数据")
|
||||
|
||||
# 有的表以 index 为日期
|
||||
if 'date' in df.columns:
|
||||
df['Date'] = pd.to_datetime(df['date'])
|
||||
elif df.index.name in ('date', 'Date') or isinstance(df.index, pd.DatetimeIndex):
|
||||
df = df.copy()
|
||||
df['Date'] = pd.to_datetime(df.index)
|
||||
else:
|
||||
# 尝试查找常见日期列
|
||||
for cand in ['timestamp', 'time', 'datetime']:
|
||||
if cand in df.columns:
|
||||
df['Date'] = pd.to_datetime(df[cand])
|
||||
break
|
||||
|
||||
# 归一化收盘价列
|
||||
close_col = None
|
||||
for cand in ['adj_close', 'close', 'Close', 'price', 'close_price', 'c']:
|
||||
if cand in df.columns:
|
||||
close_col = cand
|
||||
break
|
||||
if close_col is None:
|
||||
raise ValueError("未找到收盘价列")
|
||||
df['Close'] = pd.to_numeric(df[close_col], errors='coerce')
|
||||
|
||||
# 仅保留需要列并清洗
|
||||
if 'Date' not in df.columns:
|
||||
raise ValueError("未找到日期列")
|
||||
df = df[['Date', 'Close']].dropna()
|
||||
df = df.sort_values('Date').reset_index(drop=True)
|
||||
# 限定时间窗口(有些 provider 可能返回更长区间)
|
||||
df = df[df['Date'].dt.date.between(start, end)]
|
||||
|
||||
if df.empty:
|
||||
raise ValueError("清洗后为空")
|
||||
return df
|
||||
|
||||
except Exception:
|
||||
# 如果 OpenBB 不可用或调用失败,进入本地演示/合成数据兜底
|
||||
pass
|
||||
|
||||
# Fallback to demo from examples/data
|
||||
try:
|
||||
from pathlib import Path
|
||||
root = Path(__file__).resolve().parents[2]
|
||||
demo_map = {
|
||||
'AAPL': root / 'examples' / 'data' / 'demo_results_aapl.json',
|
||||
'MSFT': root / 'examples' / 'data' / 'demo_results_msft.json',
|
||||
'TSLA': root / 'examples' / 'data' / 'demo_results_tsla.json',
|
||||
}
|
||||
path = demo_map.get(symbol.upper())
|
||||
if path and path.exists():
|
||||
df = pd.read_json(path)
|
||||
if 'date' in df.columns:
|
||||
df['Date'] = pd.to_datetime(df['date'])
|
||||
if 'close' in df.columns:
|
||||
df['Close'] = df['close']
|
||||
df = df[['Date', 'Close']].dropna().sort_values('Date').reset_index(drop=True)
|
||||
# 裁剪到时间窗口
|
||||
df = df[df['Date'].dt.date.between(start, end)]
|
||||
return df
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
# Last resort: minimal synthetic data(避免 FutureWarning)
|
||||
dates = pd.date_range(end=end, periods=min(days, 180))
|
||||
return pd.DataFrame({
|
||||
'Date': dates,
|
||||
'Close': pd.Series(range(len(dates))).rolling(5).mean().bfill()
|
||||
})
|
||||
|
||||
|
||||
def _kpis_from_df(df: pd.DataFrame) -> dict:
|
||||
if df.empty or 'Close' not in df.columns:
|
||||
return {"最新价": "-", "近30日涨幅": "-", "最大回撤(近90日)": "-"}
|
||||
latest = float(df['Close'].iloc[-1])
|
||||
last_30 = df.tail(30)
|
||||
if len(last_30) > 1:
|
||||
pct_30 = (last_30['Close'].iloc[-1] / last_30['Close'].iloc[0] - 1) * 100
|
||||
else:
|
||||
pct_30 = 0.0
|
||||
# max drawdown over last 90 days
|
||||
lookback = df.tail(90)['Close']
|
||||
roll_max = lookback.cummax()
|
||||
drawdown = (lookback / roll_max - 1).min() * 100
|
||||
return {
|
||||
"最新价": f"{latest:,.2f}",
|
||||
"近30日涨幅": f"{pct_30:.2f}%",
|
||||
"最大回撤(近90日)": f"{drawdown:.2f}%",
|
||||
}
|
||||
|
||||
|
||||
def render_openbb_tab():
|
||||
st.write("使用 OpenBB(如可用)或演示数据展示市场概览。")
|
||||
|
||||
col_a, col_b = st.columns([2, 1])
|
||||
with col_b:
|
||||
symbol = st.text_input("股票/ETF 代码", value="AAPL")
|
||||
days = st.slider("时间窗口(天)", 90, 720, 365, step=30)
|
||||
obb_ready = _check_openbb_installed()
|
||||
if obb_ready:
|
||||
st.success("OpenBB 已安装 ✅")
|
||||
else:
|
||||
st.info("未检测到 OpenBB,将使用演示数据。可在 requirements.txt 中加入 openbb 后安装启用。")
|
||||
|
||||
with col_a:
|
||||
df = _load_price_data(symbol, days)
|
||||
if df is None or df.empty:
|
||||
st.warning("未获取到数据")
|
||||
return
|
||||
# 绘制收盘价
|
||||
if 'Date' in df.columns and 'Close' in df.columns:
|
||||
fig = px.line(df, x='Date', y='Close', title=f"{symbol.upper()} 收盘价")
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
else:
|
||||
st.dataframe(df.head())
|
||||
|
||||
# KPI 卡片
|
||||
st.markdown("#### 关键指标")
|
||||
kpis = _kpis_from_df(df)
|
||||
k1, k2, k3 = st.columns(3)
|
||||
k1.metric("最新价", kpis["最新价"])
|
||||
k2.metric("近30日涨幅", kpis["近30日涨幅"])
|
||||
k3.metric("最大回撤(近90日)", kpis["最大回撤(近90日)"])
|
||||
|
||||
# 未来:基本面、新闻、情绪等组件占位
|
||||
with st.expander("🚧 更多组件(即将推出)"):
|
||||
st.write("基本面卡片、新闻与情绪、宏观指标、策略筛选等将逐步接入。")
|
||||
436
jixia_academy/ui/streamlit/tabs/tianxia_tab.py
Normal file
436
jixia_academy/ui/streamlit/tabs/tianxia_tab.py
Normal file
@@ -0,0 +1,436 @@
|
||||
"""
|
||||
天下体系 - 儒门天下观资本生态分析Tab
|
||||
基于"天命树"结构模型分析全球资本市场权力结构
|
||||
|
||||
重构版本:
|
||||
- 移除硬编码API密钥
|
||||
- 使用统一配置管理
|
||||
- 改进数据结构
|
||||
- 增强错误处理
|
||||
"""
|
||||
|
||||
import streamlit as st
|
||||
import pandas as pd
|
||||
import plotly.express as px
|
||||
from datetime import datetime
|
||||
import time
|
||||
import random
|
||||
from typing import Dict, List, Any, Optional
|
||||
from dataclasses import dataclass
|
||||
|
||||
# 导入配置管理
|
||||
try:
|
||||
from config.settings import get_rapidapi_key
|
||||
except ImportError:
|
||||
# 如果配置模块不可用,使用环境变量
|
||||
import os
|
||||
def get_rapidapi_key():
|
||||
return os.getenv('RAPIDAPI_KEY', '')
|
||||
|
||||
@dataclass
|
||||
class StockEntity:
|
||||
"""股票实体数据类"""
|
||||
symbol: str
|
||||
name: str
|
||||
role: str
|
||||
dependency: Optional[str] = None
|
||||
serves: Optional[str] = None
|
||||
type: Optional[str] = None
|
||||
|
||||
@dataclass
|
||||
class EcosystemData:
|
||||
"""生态系统数据类"""
|
||||
tianzi: Dict[str, str]
|
||||
dafu: List[StockEntity]
|
||||
shi: List[StockEntity]
|
||||
jiajie: List[StockEntity]
|
||||
|
||||
class TianxiaAnalyzer:
|
||||
"""天下体系分析器 - 天命树结构分析"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化分析器"""
|
||||
try:
|
||||
self.rapidapi_key = get_rapidapi_key()
|
||||
except Exception:
|
||||
self.rapidapi_key = ""
|
||||
st.warning("⚠️ 未配置RapidAPI密钥,将使用模拟数据")
|
||||
|
||||
# 定义三大天命树生态系统
|
||||
self.ecosystems = self._initialize_ecosystems()
|
||||
|
||||
def _initialize_ecosystems(self) -> Dict[str, EcosystemData]:
|
||||
"""初始化生态系统数据"""
|
||||
return {
|
||||
'AI': EcosystemData(
|
||||
tianzi={'symbol': 'NVDA', 'name': 'NVIDIA', 'tianming': 'CUDA + GPU硬件,定义AI计算范式'},
|
||||
dafu=[
|
||||
StockEntity('TSM', 'TSMC', '芯片代工', '高端芯片唯一代工厂'),
|
||||
StockEntity('000660.SZ', 'SK Hynix', 'HBM内存', 'GPU性能关键'),
|
||||
StockEntity('MU', 'Micron', 'HBM内存', 'GPU性能关键'),
|
||||
StockEntity('SMCI', 'Supermicro', '服务器集成', 'GPU转化为计算能力')
|
||||
],
|
||||
shi=[
|
||||
StockEntity('ASML', 'ASML', '光刻设备', serves='TSMC'),
|
||||
StockEntity('AMAT', 'Applied Materials', '半导体设备', serves='TSMC')
|
||||
],
|
||||
jiajie=[
|
||||
StockEntity('AMD', 'AMD', '竞争对手', type='竞争天子'),
|
||||
StockEntity('GOOGL', 'Google', '云计算', type='云计算天子'),
|
||||
StockEntity('AMZN', 'Amazon', '云计算', type='云计算天子')
|
||||
]
|
||||
),
|
||||
'EV': EcosystemData(
|
||||
tianzi={'symbol': 'TSLA', 'name': 'Tesla', 'tianming': '软件定义汽车 + 超级充电网络'},
|
||||
dafu=[
|
||||
StockEntity('300750.SZ', 'CATL', '动力电池', '动力系统基石'),
|
||||
StockEntity('6752.T', 'Panasonic', '动力电池', '动力系统基石'),
|
||||
StockEntity('ALB', 'Albemarle', '锂矿', '源头命脉'),
|
||||
StockEntity('002460.SZ', 'Ganfeng Lithium', '锂矿', '源头命脉')
|
||||
],
|
||||
shi=[
|
||||
StockEntity('002497.SZ', 'Yahua Industrial', '氢氧化锂', serves='CATL'),
|
||||
StockEntity('002850.SZ', 'Kedali', '精密结构件', serves='CATL')
|
||||
],
|
||||
jiajie=[
|
||||
StockEntity('002594.SZ', 'BYD', '电动车', type='诸侯'),
|
||||
StockEntity('VWAGY', 'Volkswagen', '传统车企', type='诸侯'),
|
||||
StockEntity('F', 'Ford', '传统车企', type='诸侯')
|
||||
]
|
||||
),
|
||||
'Consumer_Electronics': EcosystemData(
|
||||
tianzi={'symbol': 'AAPL', 'name': 'Apple', 'tianming': 'iOS + App Store生态系统'},
|
||||
dafu=[
|
||||
StockEntity('2317.TW', 'Foxconn', '代工制造', '物理执行者'),
|
||||
StockEntity('TSM', 'TSMC', '芯片代工', '性能优势保障'),
|
||||
StockEntity('005930.KS', 'Samsung Display', '屏幕供应', '显示技术'),
|
||||
StockEntity('QCOM', 'Qualcomm', '基带芯片', '通信命脉')
|
||||
],
|
||||
shi=[
|
||||
StockEntity('002475.SZ', 'Luxshare', '精密制造', serves='Foxconn'),
|
||||
StockEntity('002241.SZ', 'Goertek', '声学器件', serves='Foxconn')
|
||||
],
|
||||
jiajie=[
|
||||
StockEntity('005930.KS', 'Samsung', '手机制造', type='亦敌亦友天子'),
|
||||
StockEntity('1810.HK', 'Xiaomi', '手机制造', type='诸侯'),
|
||||
StockEntity('NVDA', 'NVIDIA', 'AI芯片', type='跨生态天子')
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
def get_stock_data(self, symbol: str) -> Dict[str, Any]:
|
||||
"""
|
||||
获取股票数据
|
||||
|
||||
Args:
|
||||
symbol: 股票代码
|
||||
|
||||
Returns:
|
||||
股票数据字典
|
||||
"""
|
||||
# TODO: 实现真实API调用
|
||||
# 目前使用模拟数据
|
||||
try:
|
||||
return {
|
||||
'price': round(random.uniform(50, 500), 2),
|
||||
'change_pct': round(random.uniform(-5, 5), 2),
|
||||
'market_cap': f"{random.randint(100, 3000)}B",
|
||||
'volume': random.randint(1000000, 100000000)
|
||||
}
|
||||
except Exception:
|
||||
return {
|
||||
'price': 'N/A',
|
||||
'change_pct': 0,
|
||||
'market_cap': 'N/A',
|
||||
'volume': 'N/A'
|
||||
}
|
||||
|
||||
def create_tianming_card(self, ecosystem_name: str, ecosystem_data: EcosystemData) -> None:
|
||||
"""
|
||||
创建天命卡片
|
||||
|
||||
Args:
|
||||
ecosystem_name: 生态系统名称
|
||||
ecosystem_data: 生态系统数据
|
||||
"""
|
||||
tianzi = ecosystem_data.tianzi
|
||||
stock_data = self.get_stock_data(tianzi['symbol'])
|
||||
|
||||
st.markdown(f"### 👑 {ecosystem_name} 天命树")
|
||||
|
||||
# 天子信息
|
||||
col1, col2, col3 = st.columns([1, 2, 1])
|
||||
|
||||
with col1:
|
||||
st.markdown("#### 🌟 天子")
|
||||
st.markdown(f"**{tianzi['name']}** ({tianzi['symbol']})")
|
||||
|
||||
with col2:
|
||||
st.markdown("#### 📜 天命")
|
||||
st.info(tianzi['tianming'])
|
||||
|
||||
with col3:
|
||||
st.metric(
|
||||
label="股价",
|
||||
value=f"${stock_data['price']}",
|
||||
delta=f"{stock_data['change_pct']:+.2f}%"
|
||||
)
|
||||
|
||||
# 大夫层级
|
||||
if ecosystem_data.dafu:
|
||||
st.markdown("#### 🏛️ 大夫 (核心依赖)")
|
||||
dafu_cols = st.columns(min(len(ecosystem_data.dafu), 4))
|
||||
|
||||
for i, dafu in enumerate(ecosystem_data.dafu):
|
||||
col_index = i % 4
|
||||
with dafu_cols[col_index]:
|
||||
data = self.get_stock_data(dafu.symbol)
|
||||
st.metric(
|
||||
label=f"{dafu.name}",
|
||||
value=f"${data['price']}",
|
||||
delta=f"{data['change_pct']:+.2f}%"
|
||||
)
|
||||
st.caption(f"**{dafu.role}**: {dafu.dependency}")
|
||||
|
||||
# 士层级
|
||||
if ecosystem_data.shi:
|
||||
st.markdown("#### ⚔️ 士 (专业供应商)")
|
||||
shi_cols = st.columns(min(len(ecosystem_data.shi), 3))
|
||||
|
||||
for i, shi in enumerate(ecosystem_data.shi):
|
||||
col_index = i % 3
|
||||
with shi_cols[col_index]:
|
||||
data = self.get_stock_data(shi.symbol)
|
||||
st.metric(
|
||||
label=f"{shi.name}",
|
||||
value=f"${data['price']}",
|
||||
delta=f"{data['change_pct']:+.2f}%"
|
||||
)
|
||||
st.caption(f"**{shi.role}** → 服务于{shi.serves}")
|
||||
|
||||
# 嫁接关系
|
||||
if ecosystem_data.jiajie:
|
||||
st.markdown("#### 🔗 嫁接关系 (跨生态链接)")
|
||||
jiajie_cols = st.columns(min(len(ecosystem_data.jiajie), 4))
|
||||
|
||||
for i, jiajie in enumerate(ecosystem_data.jiajie):
|
||||
col_index = i % 4
|
||||
with jiajie_cols[col_index]:
|
||||
data = self.get_stock_data(jiajie.symbol)
|
||||
st.metric(
|
||||
label=f"{jiajie.name}",
|
||||
value=f"${data['price']}",
|
||||
delta=f"{data['change_pct']:+.2f}%"
|
||||
)
|
||||
st.caption(f"**{jiajie.type}**")
|
||||
|
||||
st.markdown("---")
|
||||
|
||||
def create_tianming_tree_table(self) -> pd.DataFrame:
|
||||
"""
|
||||
创建天命树完整表格 - 用于投资组合去相关性分析
|
||||
|
||||
Returns:
|
||||
包含所有股票信息的DataFrame
|
||||
"""
|
||||
st.markdown("### 📋 天命树完整表格 - 投资组合去相关性分析")
|
||||
st.markdown("**核心理念**: 投资组合的本质是去相关性 - 从不同root下的不同spine下的不同leaf进行配置")
|
||||
|
||||
all_stocks = []
|
||||
|
||||
for eco_name, eco_data in self.ecosystems.items():
|
||||
# 天子
|
||||
tianzi = eco_data.tianzi
|
||||
stock_data = self.get_stock_data(tianzi['symbol'])
|
||||
all_stocks.append({
|
||||
'Root': eco_name,
|
||||
'Level': '👑 天子',
|
||||
'Symbol': tianzi['symbol'],
|
||||
'Company': tianzi['name'],
|
||||
'Role': '定义范式',
|
||||
'Dependency_Path': f"{eco_name}",
|
||||
'Price': stock_data['price'],
|
||||
'Change%': stock_data['change_pct'],
|
||||
'Market_Cap': stock_data['market_cap'],
|
||||
'Correlation_Risk': '极高 - 生态核心'
|
||||
})
|
||||
|
||||
# 大夫
|
||||
for dafu in eco_data.dafu:
|
||||
stock_data = self.get_stock_data(dafu.symbol)
|
||||
all_stocks.append({
|
||||
'Root': eco_name,
|
||||
'Level': '🏛️ 大夫',
|
||||
'Symbol': dafu.symbol,
|
||||
'Company': dafu.name,
|
||||
'Role': dafu.role,
|
||||
'Dependency_Path': f"{eco_name} → {tianzi['name']} → {dafu.name}",
|
||||
'Price': stock_data['price'],
|
||||
'Change%': stock_data['change_pct'],
|
||||
'Market_Cap': stock_data['market_cap'],
|
||||
'Correlation_Risk': '高 - 深度绑定天子'
|
||||
})
|
||||
|
||||
# 士
|
||||
for shi in eco_data.shi:
|
||||
stock_data = self.get_stock_data(shi.symbol)
|
||||
all_stocks.append({
|
||||
'Root': eco_name,
|
||||
'Level': '⚔️ 士',
|
||||
'Symbol': shi.symbol,
|
||||
'Company': shi.name,
|
||||
'Role': shi.role,
|
||||
'Dependency_Path': f"{eco_name} → {shi.serves} → {shi.name}",
|
||||
'Price': stock_data['price'],
|
||||
'Change%': stock_data['change_pct'],
|
||||
'Market_Cap': stock_data['market_cap'],
|
||||
'Correlation_Risk': '中 - 专业供应商'
|
||||
})
|
||||
|
||||
# 嫁接
|
||||
for jiajie in eco_data.jiajie:
|
||||
stock_data = self.get_stock_data(jiajie.symbol)
|
||||
all_stocks.append({
|
||||
'Root': '🔗 跨生态',
|
||||
'Level': '🔗 嫁接',
|
||||
'Symbol': jiajie.symbol,
|
||||
'Company': jiajie.name,
|
||||
'Role': jiajie.type or jiajie.role,
|
||||
'Dependency_Path': f"多生态嫁接 → {jiajie.name}",
|
||||
'Price': stock_data['price'],
|
||||
'Change%': stock_data['change_pct'],
|
||||
'Market_Cap': stock_data['market_cap'],
|
||||
'Correlation_Risk': '低 - 多元化依赖'
|
||||
})
|
||||
|
||||
df = pd.DataFrame(all_stocks)
|
||||
|
||||
# 显示表格
|
||||
st.dataframe(
|
||||
df,
|
||||
use_container_width=True,
|
||||
column_config={
|
||||
"Root": st.column_config.TextColumn("生态根节点", width="small"),
|
||||
"Level": st.column_config.TextColumn("层级", width="small"),
|
||||
"Symbol": st.column_config.TextColumn("代码", width="small"),
|
||||
"Company": st.column_config.TextColumn("公司", width="medium"),
|
||||
"Role": st.column_config.TextColumn("角色", width="medium"),
|
||||
"Dependency_Path": st.column_config.TextColumn("依赖路径", width="large"),
|
||||
"Price": st.column_config.NumberColumn("股价", format="$%.2f"),
|
||||
"Change%": st.column_config.NumberColumn("涨跌幅", format="%.2f%%"),
|
||||
"Market_Cap": st.column_config.TextColumn("市值", width="small"),
|
||||
"Correlation_Risk": st.column_config.TextColumn("相关性风险", width="medium")
|
||||
}
|
||||
)
|
||||
|
||||
return df
|
||||
|
||||
def render_tianxia_tab() -> None:
|
||||
"""渲染天下体系Tab"""
|
||||
|
||||
# 页面标题
|
||||
st.markdown("### 🏛️ 天下体系 - 儒门天下观资本生态分析")
|
||||
st.markdown("**基于'天命树'结构模型,穿透市场表象,绘制全球资本市场真实的权力结构**")
|
||||
st.markdown("---")
|
||||
|
||||
# 初始化分析器
|
||||
analyzer = TianxiaAnalyzer()
|
||||
|
||||
# 控制面板
|
||||
col1, col2, col3 = st.columns([1, 1, 2])
|
||||
with col1:
|
||||
auto_refresh = st.checkbox("🔄 自动刷新", value=False, key="tianxia_auto_refresh")
|
||||
with col2:
|
||||
if st.button("🏛️ 扫描天下", type="primary", key="tianxia_scan_btn"):
|
||||
st.session_state.trigger_tianxia_scan = True
|
||||
with col3:
|
||||
st.markdown("*正在分析全球资本生态权力结构...*")
|
||||
|
||||
# 理论介绍
|
||||
with st.expander("📚 天命树理论基础"):
|
||||
st.markdown("""
|
||||
### 🏛️ 儒门天下观核心思想
|
||||
|
||||
**两大哲学基石:**
|
||||
1. **结构非平权**: 资本宇宙本质是不平权的、层级森严的树状结构
|
||||
2. **天命与脉络**: 每个生态都有唯一的"根节点"(天子),拥有定义整个生态的"天命"
|
||||
|
||||
**四层架构:**
|
||||
- **👑 天子**: 定义范式的平台型公司 (如Apple, NVIDIA, Tesla)
|
||||
- **🏛️ 大夫**: 深度绑定天子的核心供应商 (如TSMC, CATL)
|
||||
- **⚔️ 士**: 专业供应商和服务商 (如ASML, Luxshare)
|
||||
- **🔗 嫁接**: 跨生态的策略性链接关系
|
||||
""")
|
||||
|
||||
# 自动刷新逻辑
|
||||
if auto_refresh:
|
||||
time.sleep(60)
|
||||
st.rerun()
|
||||
|
||||
# 触发扫描或显示数据
|
||||
if st.session_state.get('trigger_tianxia_scan', False) or 'tianxia_scan_time' not in st.session_state:
|
||||
with st.spinner("🏛️ 正在扫描天下体系..."):
|
||||
st.session_state.tianxia_scan_time = datetime.now()
|
||||
st.session_state.trigger_tianxia_scan = False
|
||||
|
||||
# 显示扫描时间
|
||||
if 'tianxia_scan_time' in st.session_state:
|
||||
st.info(f"📅 最后扫描时间: {st.session_state.tianxia_scan_time.strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
|
||||
# 显示三大生态系统
|
||||
st.markdown("## 🌍 三大天命树生态系统")
|
||||
|
||||
# 分析模式选择
|
||||
analysis_mode = st.selectbox(
|
||||
"选择分析模式",
|
||||
["生态系统分析", "投资组合去相关性分析"],
|
||||
key="tianxia_analysis_mode"
|
||||
)
|
||||
|
||||
if analysis_mode == "生态系统分析":
|
||||
# 生态系统选择
|
||||
selected_ecosystem = st.selectbox(
|
||||
"选择要分析的生态系统",
|
||||
["全部", "AI", "EV", "Consumer_Electronics"],
|
||||
format_func=lambda x: {
|
||||
"全部": "🌍 全部生态系统",
|
||||
"AI": "🤖 AI人工智能生态",
|
||||
"EV": "⚡ 电动汽车生态",
|
||||
"Consumer_Electronics": "📱 消费电子生态"
|
||||
}[x],
|
||||
key="tianxia_ecosystem_select"
|
||||
)
|
||||
|
||||
if selected_ecosystem == "全部":
|
||||
# 显示所有生态系统
|
||||
for eco_name, eco_data in analyzer.ecosystems.items():
|
||||
analyzer.create_tianming_card(eco_name, eco_data)
|
||||
else:
|
||||
# 显示选定的生态系统
|
||||
analyzer.create_tianming_card(selected_ecosystem, analyzer.ecosystems[selected_ecosystem])
|
||||
|
||||
else: # 投资组合去相关性分析
|
||||
st.markdown("## 🎯 投资组合去相关性分析")
|
||||
st.info("**核心理念**: 真正的分散投资是从不同的root(天子)下的不同spine(大夫)下的不同leaf(士)进行配置")
|
||||
|
||||
# 创建完整天命树表格
|
||||
df = analyzer.create_tianming_tree_table()
|
||||
|
||||
# 页面底部说明
|
||||
st.markdown("---")
|
||||
st.markdown("""
|
||||
### 🎯 天下体系核心洞察
|
||||
|
||||
**权力结构分析**:
|
||||
- **AI生态**: NVIDIA通过CUDA平台统治AI计算,TSMC是关键"嫁接"节点
|
||||
- **电动车生态**: Tesla定义软件汽车范式,CATL掌握电池命脉
|
||||
- **消费电子生态**: Apple建立iOS护城河,供应链高度集中化
|
||||
|
||||
**投资策略启示**:
|
||||
1. **投资天子**: 寻找定义范式的平台型公司
|
||||
2. **关注大夫**: 深度绑定天子的核心供应商往往被低估
|
||||
3. **警惕嫁接**: 被多个天子"嫁接"的公司风险与机会并存
|
||||
4. **避开士层**: 缺乏议价能力的专业供应商投资价值有限
|
||||
|
||||
⚠️ **免责声明**: 天下体系分析仅供参考,投资有风险,决策需谨慎!
|
||||
""")
|
||||
Reference in New Issue
Block a user