diff --git a/README.md b/README.md index 010a409..8367eac 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,7 @@ - **🌍 天下体系分析**: 基于儒门天下观的资本生态"天命树"分析模型 - **🔒 安全配置管理**: 使用Doppler进行统一的密钥和配置管理 - **📊 智能数据源**: 基于17个RapidAPI订阅的永动机数据引擎 +- **📈 市场数据 (可选)**: 集成 OpenBB v4,统一路由多数据提供商,详见 docs/openbb_integration.md - **🎨 现代化界面**: 基于Streamlit的响应式Web界面 ## 🏗️ 项目结构 diff --git a/config/doppler_config.py b/config/doppler_config.py index d174b5b..344d986 100644 --- a/config/doppler_config.py +++ b/config/doppler_config.py @@ -95,9 +95,12 @@ def get_google_genai_config() -> Dict[str, str]: Returns: Google GenAI配置字典 """ + use_vertex_ai = get_secret('GOOGLE_GENAI_USE_VERTEXAI', 'FALSE').upper() == 'TRUE' + api_key = '' if use_vertex_ai else get_secret('GOOGLE_API_KEY', '') + return { - 'api_key': get_secret('GOOGLE_API_KEY', ''), - 'use_vertex_ai': get_secret('GOOGLE_GENAI_USE_VERTEXAI', 'FALSE'), + 'api_key': api_key, + 'use_vertex_ai': str(use_vertex_ai).upper(), 'project_id': get_secret('GOOGLE_CLOUD_PROJECT_ID', ''), 'location': get_secret('GOOGLE_CLOUD_LOCATION', 'us-central1'), 'memory_bank_enabled': get_secret('VERTEX_MEMORY_BANK_ENABLED', 'TRUE'), @@ -238,4 +241,4 @@ def validate_config(mode: str = "hybrid") -> bool: if __name__ == "__main__": # 配置验证脚本 print("🔧 验证配置...") - validate_config() \ No newline at end of file + validate_config() diff --git a/docs/openbb_integration.md b/docs/openbb_integration.md new file mode 100644 index 0000000..008e984 --- /dev/null +++ b/docs/openbb_integration.md @@ -0,0 +1,56 @@ +# OpenBB 集成指南 + +本指南帮助你在本项目中启用并使用 OpenBB v4 作为市场数据源,同时保证在未安装 OpenBB 的情况下,应用可平稳回退到演示/合成数据。 + +## 1. 为什么选择 OpenBB v4 +- 统一的路由接口:`from openbb import obb` +- 多数据提供商聚合(如 yfinance、polygon、fmp 等) +- 返回对象支持 `.results` 或 `.to_df()`,便于统一处理 + +## 2. 安装与环境准备 +默认未安装 OpenBB(requirements.txt 中为可选依赖)。如需启用: + +```bash +pip install "openbb>=4.1.0" +``` + +若你使用的是国内网络,建议配置合适的 PyPI 镜像或使用代理。 + +## 3. 配置说明 +无需额外配置即可使用 `provider='yfinance'` 的公共数据。若你有付费数据源(如 polygon),可通过环境变量或 OpenBB 的 provider 配置进行设置。 + +## 4. 代码结构与调用方式 +- Streamlit UI: `app/tabs/openbb_tab.py` + - 自动检测 OpenBB 是否可用;若不可用则使用演示数据或合成数据 + - 优先路由:`obb.equity.price.historical`,ETF 回退至 `obb.etf.price.historical` +- 引擎模块: `src/jixia/engines/openbb_engine.py` + - 延迟导入 OpenBB:首次调用时 `from openbb import obb` + - 对 `.results` / `.to_df()` / `.to_dataframe()` 做兼容处理 +- 辅助脚本: `src/jixia/engines/openbb_stock_data.py` + - 延迟导入 `obb` + - ETF 历史数据路径更新为 `obb.etf.price.historical` + +## 5. 回退机制 +- UI 层(OpenBB Tab) + - 未安装 OpenBB 或请求失败:读取 `examples/data/*.json` 的演示数据;仍失败则生成合成数据 +- 引擎层 + - 若未安装 OpenBB:返回 `success=False`,带错误消息,不影响其他功能 + +## 6. 开发与测试 +- 单元测试建议: + - 未安装 OpenBB 时,`_load_price_data` 能返回演示/合成数据 + - 已安装 OpenBB 时,能通过 `obb.equity.price.historical` 获取 DataFrame +- 在本仓库中新增了占位测试:`tests/test_openbb_fallback.py` + +## 7. 典型问题排查 +- ImportError: No module named 'openbb' + - 未安装 OpenBB;按第2节安装。 +- 返回空数据 + - 检查 symbol 是否正确;尝试更换 provider 或缩短时间窗口。 +- 列名/索引不匹配 + - UI 中已对常见列/索引做了规范化处理;如仍异常,可打印原始 DataFrame 排查。 + +## 8. 后续计划 +- 接入更多 OpenBB 路由(基本面、新闻、财报、因子) +- 与辩论系统结果联动,生成投资洞察卡片 +- 支持用户自定义 provider 优先级与兜底策略 diff --git a/examples/debates/adk_real_debate.py b/examples/debates/adk_debate_example.py similarity index 100% rename from examples/debates/adk_real_debate.py rename to examples/debates/adk_debate_example.py diff --git a/examples/debates/adk_debate_test.py b/examples/debates/adk_debate_test.py deleted file mode 100644 index 8ab22ad..0000000 --- a/examples/debates/adk_debate_test.py +++ /dev/null @@ -1,130 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -稷下学宫 Google ADK 论道系统测试 -基于 Google ADK 的八仙论道原型 -""" - -import os -import asyncio -from google.adk import Agent -from google.adk.tools import FunctionTool - -# 八仙智能体定义 -def create_baxian_agents(): - """创建八仙智能体""" - - # 铁拐李 - 逆向思维专家 - tie_guai_li = Agent( - name="铁拐李", - model="gemini-2.5-flash" - ) - - # 汉钟离 - 平衡协调者 - han_zhong_li = Agent( - name="汉钟离", - model="gemini-2.5-flash" - ) - - # 张果老 - 历史智慧者 - zhang_guo_lao = Agent( - name="张果老", - model="gemini-2.5-flash" - ) - - # 蓝采和 - 创新思维者 - lan_cai_he = Agent( - name="蓝采和", - model="gemini-2.5-flash" - ) - - # 何仙姑 - 直觉洞察者 - he_xian_gu = Agent( - name="何仙姑", - model="gemini-2.5-flash" - ) - - # 吕洞宾 - 理性分析者 - lu_dong_bin = Agent( - name="吕洞宾", - model="gemini-2.5-flash" - ) - - # 韩湘子 - 艺术感知者 - han_xiang_zi = Agent( - name="韩湘子", - model="gemini-2.5-flash" - ) - - # 曹国舅 - 实务执行者 - cao_guo_jiu = Agent( - name="曹国舅", - model="gemini-2.5-flash" - ) - - return { - "铁拐李": tie_guai_li, - "汉钟离": han_zhong_li, - "张果老": zhang_guo_lao, - "蓝采和": lan_cai_he, - "何仙姑": he_xian_gu, - "吕洞宾": lu_dong_bin, - "韩湘子": han_xiang_zi, - "曹国舅": cao_guo_jiu - } - -def test_single_agent(): - """测试单个智能体""" - print("🧪 测试单个智能体...") - - # 创建铁拐李智能体 - tie_guai_li = Agent( - name="铁拐李", - model="gemini-2.5-flash" - ) - - print(f"✅ 智能体 '{tie_guai_li.name}' 创建成功") - print(f"📱 使用模型: {tie_guai_li.model}") - - return tie_guai_li - -def test_baxian_creation(): - """测试八仙智能体创建""" - print("\n🎭 创建八仙智能体...") - - baxian = create_baxian_agents() - - print(f"✅ 成功创建 {len(baxian)} 个智能体:") - for name, agent in baxian.items(): - print(f" - {name}: {agent.model}") - - return baxian - -def main(): - """主测试函数""" - print("🚀 开始稷下学宫 ADK 论道系统测试...") - - # 检查API密钥 - api_key = os.getenv('GOOGLE_API_KEY') - if not api_key: - print("❌ 未找到 GOOGLE_API_KEY 环境变量") - print("请使用: doppler run -- python src/jixia/debates/adk_debate_test.py") - return - - print(f"✅ API密钥已配置 (长度: {len(api_key)} 字符)") - - # 测试单个智能体 - single_agent = test_single_agent() - - # 测试八仙智能体创建 - baxian = test_baxian_creation() - - print("\n🎉 ADK 论道系统基础测试完成!") - print("\n📝 下一步:") - print(" 1. 实现智能体间的对话逻辑") - print(" 2. 集成 RapidAPI 数据源") - print(" 3. 创建论道主题和流程") - print(" 4. 连接 Streamlit 界面") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/examples/debates/adk_simple_debate.py b/examples/debates/adk_simple_debate.py deleted file mode 100644 index bddbab5..0000000 --- a/examples/debates/adk_simple_debate.py +++ /dev/null @@ -1,82 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -稷下学宫 ADK 简单论道测试 -实现智能体间的基本对话功能 -""" - -import os -from google.adk import Agent - -def create_debate_agents(): - """创建论道智能体""" - - # 铁拐李 - 逆向思维专家 - tie_guai_li = Agent( - name="铁拐李", - model="gemini-2.0-flash-exp" - ) - - # 吕洞宾 - 理性分析者 - lu_dong_bin = Agent( - name="吕洞宾", - model="gemini-2.0-flash-exp" - ) - - return tie_guai_li, lu_dong_bin - -def simple_debate_test(): - """简单论道测试""" - print("🎭 开始简单论道测试...") - - # 创建智能体 - tie_guai_li, lu_dong_bin = create_debate_agents() - - print("\n📋 论道主题: 人工智能对未来社会的影响") - print("\n🎯 开始论道...") - - try: - # 测试智能体创建 - print("\n✅ 智能体创建成功:") - print(f" - {tie_guai_li.name}: {tie_guai_li.model}") - print(f" - {lu_dong_bin.name}: {lu_dong_bin.model}") - - print("\n🎉 简单论道测试完成!") - print("\n📝 智能体基础功能验证成功") - - except Exception as e: - print(f"❌ 论道测试失败: {e}") - return False - - return True - -def main(): - """主函数""" - print("🚀 稷下学宫 ADK 简单论道系统") - - # 检查API密钥 - api_key = os.getenv('GOOGLE_API_KEY') - if not api_key: - print("❌ 未找到 GOOGLE_API_KEY 环境变量") - print("请使用: doppler run -- python src/jixia/debates/adk_simple_debate.py") - return - - print(f"✅ API密钥已配置") - - # 运行测试 - try: - result = simple_debate_test() - if result: - print("\n📝 测试结果: 成功") - print("\n🎯 下一步开发计划:") - print(" 1. 学习ADK的正确调用方式") - print(" 2. 实现智能体对话功能") - print(" 3. 扩展到八仙全员论道") - print(" 4. 集成实时数据源") - else: - print("\n❌ 测试失败") - except Exception as e: - print(f"❌ 运行失败: {e}") - -if __name__ == "__main__": - main() \ No newline at end of file diff --git a/examples/debates/jixia_ollama_swarm.py b/examples/debates/jixia_ollama_swarm.py deleted file mode 100644 index f7ddb7b..0000000 --- a/examples/debates/jixia_ollama_swarm.py +++ /dev/null @@ -1,355 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -稷下学宫本地版 - 基于Ollama的四仙辩论系统 -使用本地Ollama服务,无需API密钥 -""" - -import asyncio -import json -from datetime import datetime -from swarm import Swarm, Agent -from typing import Dict, List, Any, Optional -import random - -class JixiaOllamaSwarm: - """稷下学宫本地版 - 使用Ollama的四仙辩论系统""" - - def __init__(self): - # Ollama配置 - self.ollama_base_url = "http://100.99.183.38:11434" - self.model_name = "gemma3n:e4b" # 使用你指定的模型 - - # 初始化Swarm客户端,使用Ollama - from openai import OpenAI - openai_client = OpenAI( - api_key="ollama", # Ollama不需要真实的API密钥 - base_url=f"{self.ollama_base_url}/v1" - ) - self.client = Swarm(client=openai_client) - - print(f"🦙 使用本地Ollama服务: {self.ollama_base_url}") - print(f"🤖 使用模型: {self.model_name}") - - # 四仙配置 - self.immortals = { - '吕洞宾': { - 'role': '技术分析专家', - 'stance': 'positive', - 'specialty': '技术分析和图表解读', - 'style': '犀利直接,一剑封喉' - }, - '何仙姑': { - 'role': '风险控制专家', - 'stance': 'negative', - 'specialty': '风险评估和资金管理', - 'style': '温和坚定,关注风险' - }, - '张果老': { - 'role': '历史数据分析师', - 'stance': 'positive', - 'specialty': '历史回测和趋势分析', - 'style': '博古通今,从历史找规律' - }, - '铁拐李': { - 'role': '逆向投资大师', - 'stance': 'negative', - 'specialty': '逆向思维和危机发现', - 'style': '不拘一格,挑战共识' - } - } - - # 创建智能体 - self.agents = self.create_agents() - - def create_agents(self) -> Dict[str, Agent]: - """创建四仙智能体""" - agents = {} - - # 吕洞宾 - 技术分析专家 - agents['吕洞宾'] = Agent( - name="LuDongbin", - instructions=""" - 你是吕洞宾,八仙之首,技术分析专家。 - - 你的特点: - - 擅长技术分析和图表解读 - - 立场:看涨派,善于发现投资机会 - - 风格:犀利直接,一剑封喉 - - 在辩论中: - 1. 从技术分析角度分析市场 - 2. 使用具体的技术指标支撑观点(如RSI、MACD、均线等) - 3. 保持看涨的乐观态度 - 4. 发言以"吕洞宾曰:"开头 - 5. 发言控制在100字以内,简洁有力 - 6. 发言完毕后说"请何仙姑继续论道" - - 请用古雅但现代的语言风格,结合专业的技术分析。 - """, - functions=[self.to_hexiangu] - ) - - # 何仙姑 - 风险控制专家 - agents['何仙姑'] = Agent( - name="HeXiangu", - instructions=""" - 你是何仙姑,八仙中唯一的女仙,风险控制专家。 - - 你的特点: - - 擅长风险评估和资金管理 - - 立场:看跌派,关注投资风险 - - 风格:温和坚定,关注风险控制 - - 在辩论中: - 1. 从风险控制角度分析市场 - 2. 指出潜在的投资风险和危险信号 - 3. 保持谨慎的态度,强调风险管理 - 4. 发言以"何仙姑曰:"开头 - 5. 发言控制在100字以内,温和但坚定 - 6. 发言完毕后说"请张果老继续论道" - - 请用温和但专业的语调,体现女性的细致和关怀。 - """, - functions=[self.to_zhangguolao] - ) - - # 张果老 - 历史数据分析师 - agents['张果老'] = Agent( - name="ZhangGuoLao", - instructions=""" - 你是张果老,历史数据分析师。 - - 你的特点: - - 擅长历史回测和趋势分析 - - 立场:看涨派,从历史中寻找机会 - - 风格:博古通今,从历史中找规律 - - 在辩论中: - 1. 从历史数据角度分析市场 - 2. 引用具体的历史案例和数据 - 3. 保持乐观的投资态度 - 4. 发言以"张果老曰:"开头 - 5. 发言控制在100字以内,引经据典 - 6. 发言完毕后说"请铁拐李继续论道" - - 请用博学的语调,多引用历史数据和案例。 - """, - functions=[self.to_tieguaili] - ) - - # 铁拐李 - 逆向投资大师 - agents['铁拐李'] = Agent( - name="TieGuaiLi", - instructions=""" - 你是铁拐李,逆向投资大师。 - - 你的特点: - - 擅长逆向思维和危机发现 - - 立场:看跌派,挑战主流观点 - - 风格:不拘一格,敢于质疑 - - 在辩论中: - 1. 从逆向投资角度分析市场 - 2. 挑战前面三位仙人的观点 - 3. 寻找市场的潜在危机和泡沫 - 4. 发言以"铁拐李曰:"开头 - 5. 作为最后发言者,要总结四仙观点并给出结论 - 6. 发言控制在150字以内,包含总结 - - 请用直率犀利的语言,体现逆向思维的独特视角。 - """, - functions=[] # 最后一个,不需要转换 - ) - - return agents - - def to_hexiangu(self): - """转到何仙姑""" - return self.agents['何仙姑'] - - def to_zhangguolao(self): - """转到张果老""" - return self.agents['张果老'] - - def to_tieguaili(self): - """转到铁拐李""" - return self.agents['铁拐李'] - - async def conduct_debate(self, topic: str, context: Dict[str, Any] = None) -> Dict[str, Any]: - """进行四仙辩论""" - print("🏛️ 稷下学宫四仙论道开始!") - print("=" * 60) - print(f"🎯 论道主题: {topic}") - print(f"⏰ 开始时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - print(f"🦙 使用本地Ollama: {self.ollama_base_url}") - print() - - # 构建初始提示 - prompt = self.build_prompt(topic, context) - - try: - print("⚔️ 吕洞宾仙长请先发言...") - print("-" * 40) - - # 开始辩论 - response = self.client.run( - agent=self.agents['吕洞宾'], - messages=[{"role": "user", "content": prompt}], - max_turns=8, # 四仙各发言一次,加上可能的交互 - model_override=self.model_name - ) - - print("\n" + "=" * 60) - print("🎊 四仙论道圆满结束!") - - # 处理结果 - result = self.process_result(response, topic, context) - self.display_summary(result) - - return result - - except Exception as e: - print(f"❌ 论道过程中出错: {e}") - import traceback - traceback.print_exc() - return None - - def build_prompt(self, topic: str, context: Dict[str, Any] = None) -> str: - """构建辩论提示""" - context_str = "" - if context: - context_str = f"\n📊 市场背景:\n{json.dumps(context, indent=2, ensure_ascii=False)}\n" - - prompt = f""" - 🏛️ 稷下学宫四仙论道正式开始! - - 📜 论道主题: {topic} - {context_str} - - 🎭 论道规则: - 1. 四仙按序发言:吕洞宾 → 何仙姑 → 张果老 → 铁拐李 - 2. 正反方交替:吕洞宾(看涨) → 何仙姑(看跌) → 张果老(看涨) → 铁拐李(看跌) - 3. 每位仙人从专业角度分析,提供具体数据支撑 - 4. 可以质疑前面仙人的观点,但要有理有据 - 5. 保持仙风道骨的表达风格,但要专业 - 6. 每次发言简洁有力,控制在100字以内 - 7. 铁拐李作为最后发言者要总结观点 - - 🗡️ 请吕洞宾仙长首先发言! - 记住:你是技术分析专家,要从技术面找到投资机会! - 发言要简洁有力,一剑封喉! - """ - return prompt - - def process_result(self, response, topic: str, context: Dict[str, Any]) -> Dict[str, Any]: - """处理辩论结果""" - messages = response.messages if hasattr(response, 'messages') else [] - - debate_messages = [] - for msg in messages: - if msg.get('role') == 'assistant' and msg.get('content'): - content = msg['content'] - speaker = self.extract_speaker(content) - - debate_messages.append({ - 'speaker': speaker, - 'content': content, - 'timestamp': datetime.now().isoformat(), - 'stance': self.immortals.get(speaker, {}).get('stance', 'unknown') - }) - - return { - "debate_id": f"jixia_ollama_{datetime.now().strftime('%Y%m%d_%H%M%S')}", - "topic": topic, - "context": context, - "messages": debate_messages, - "final_output": debate_messages[-1]['content'] if debate_messages else "", - "timestamp": datetime.now().isoformat(), - "framework": "OpenAI Swarm + Ollama", - "model": self.model_name, - "ollama_url": self.ollama_base_url - } - - def extract_speaker(self, content: str) -> str: - """从内容中提取发言者""" - for name in self.immortals.keys(): - if f"{name}曰" in content: - return name - return "未知仙人" - - def display_summary(self, result: Dict[str, Any]): - """显示辩论总结""" - print("\n🌟 四仙论道总结") - print("=" * 60) - print(f"📜 主题: {result['topic']}") - print(f"⏰ 时间: {result['timestamp']}") - print(f"🔧 框架: {result['framework']}") - print(f"🤖 模型: {result['model']}") - print(f"💬 发言数: {len(result['messages'])}条") - - # 统计正反方观点 - positive_count = len([m for m in result['messages'] if m.get('stance') == 'positive']) - negative_count = len([m for m in result['messages'] if m.get('stance') == 'negative']) - - print(f"📊 观点分布: 看涨{positive_count}条, 看跌{negative_count}条") - - print("\n🏆 最终总结:") - print("-" * 40) - if result['messages']: - print(result['final_output']) - - print("\n✨ 本地辩论特色:") - print("🦙 使用本地Ollama,无需API密钥") - print("🗡️ 四仙各展所长,观点多元") - print("⚖️ 正反方交替,辩论激烈") - print("🚀 基于Swarm,性能优越") - print("🔒 完全本地运行,数据安全") - -# 主函数 -async def main(): - """主函数""" - print("🏛️ 稷下学宫本地版 - Ollama + Swarm") - print("🦙 使用本地Ollama服务,无需API密钥") - print("🚀 四仙论道,完全本地运行") - print() - - # 创建辩论系统 - academy = JixiaOllamaSwarm() - - # 辩论主题 - topics = [ - "英伟达股价走势:AI泡沫还是技术革命?", - "美联储2024年货币政策:加息还是降息?", - "比特币vs黄金:谁是更好的避险资产?", - "中国房地产市场:触底反弹还是继续下行?", - "特斯拉股价:马斯克效应还是基本面支撑?" - ] - - # 随机选择主题 - topic = random.choice(topics) - - # 市场背景 - context = { - "market_sentiment": "谨慎乐观", - "volatility": "中等", - "key_events": ["财报季", "央行会议", "地缘政治"], - "technical_indicators": { - "RSI": 65, - "MACD": "金叉", - "MA20": "上穿" - } - } - - # 开始辩论 - result = await academy.conduct_debate(topic, context) - - if result: - print(f"\n🎉 辩论成功!ID: {result['debate_id']}") - print(f"📁 使用模型: {result['model']}") - print(f"🌐 Ollama服务: {result['ollama_url']}") - else: - print("❌ 辩论失败") - -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file diff --git a/examples/debates/jixia_simple_swarm.py b/examples/debates/jixia_simple_swarm.py deleted file mode 100644 index 014da8f..0000000 --- a/examples/debates/jixia_simple_swarm.py +++ /dev/null @@ -1,361 +0,0 @@ -#!/usr/bin/env python3 -# -*- coding: utf-8 -*- -""" -稷下学宫简化版 - 基于OpenAI Swarm的四仙辩论系统 -避免复杂的函数名称问题,专注于辩论效果 -""" - -import os -import asyncio -import json -from datetime import datetime -from swarm import Swarm, Agent -from typing import Dict, List, Any, Optional -import random - -class JixiaSimpleSwarm: - """稷下学宫简化版 - 四仙辩论系统""" - - def __init__(self): - # 使用Doppler配置 - try: - from config.doppler_config import get_doppler_manager - manager = get_doppler_manager() - manager.load_config(force_doppler=True) - print("🔐 使用Doppler配置") - except Exception as e: - print(f"❌ Doppler配置失败: {e}") - raise - - # 获取API密钥 - self.api_key = self.get_api_key() - - if self.api_key: - # 初始化Swarm客户端 - from openai import OpenAI - openai_client = OpenAI( - api_key=self.api_key, - base_url="https://openrouter.ai/api/v1", - default_headers={ - "HTTP-Referer": "https://github.com/ben/cauldron", - "X-Title": "Jixia Academy" - } - ) - self.client = Swarm(client=openai_client) - else: - self.client = None - - # 四仙配置 - self.immortals = { - '吕洞宾': { - 'role': '技术分析专家', - 'stance': 'positive', - 'specialty': '技术分析和图表解读', - 'style': '犀利直接,一剑封喉' - }, - '何仙姑': { - 'role': '风险控制专家', - 'stance': 'negative', - 'specialty': '风险评估和资金管理', - 'style': '温和坚定,关注风险' - }, - '张果老': { - 'role': '历史数据分析师', - 'stance': 'positive', - 'specialty': '历史回测和趋势分析', - 'style': '博古通今,从历史找规律' - }, - '铁拐李': { - 'role': '逆向投资大师', - 'stance': 'negative', - 'specialty': '逆向思维和危机发现', - 'style': '不拘一格,挑战共识' - } - } - - # 创建智能体 - self.agents = self.create_agents() - - def get_api_key(self): - """获取API密钥""" - api_keys = [ - os.getenv('OPENROUTER_API_KEY_1'), - os.getenv('OPENROUTER_API_KEY_2'), - os.getenv('OPENROUTER_API_KEY_3'), - os.getenv('OPENROUTER_API_KEY_4') - ] - - for key in api_keys: - if key and key.startswith('sk-'): - print(f"✅ 找到API密钥: {key[:20]}...") - return key - - print("❌ 未找到有效的API密钥") - return None - - def create_agents(self) -> Dict[str, Agent]: - """创建四仙智能体""" - if not self.client: - return {} - - agents = {} - - # 吕洞宾 - 技术分析专家 - agents['吕洞宾'] = Agent( - name="LuDongbin", - instructions=""" - 你是吕洞宾,八仙之首,技术分析专家。 - - 你的特点: - - 擅长技术分析和图表解读 - - 立场:看涨派,善于发现投资机会 - - 风格:犀利直接,一剑封喉 - - 在辩论中: - 1. 从技术分析角度分析市场 - 2. 使用具体的技术指标支撑观点 - 3. 保持看涨的乐观态度 - 4. 发言以"吕洞宾曰:"开头 - 5. 发言完毕后说"请何仙姑继续论道" - """, - functions=[self.to_hexiangu] - ) - - # 何仙姑 - 风险控制专家 - agents['何仙姑'] = Agent( - name="HeXiangu", - instructions=""" - 你是何仙姑,八仙中唯一的女仙,风险控制专家。 - - 你的特点: - - 擅长风险评估和资金管理 - - 立场:看跌派,关注投资风险 - - 风格:温和坚定,关注风险控制 - - 在辩论中: - 1. 从风险控制角度分析市场 - 2. 指出潜在的投资风险 - 3. 保持谨慎的态度 - 4. 发言以"何仙姑曰:"开头 - 5. 发言完毕后说"请张果老继续论道" - """, - functions=[self.to_zhangguolao] - ) - - # 张果老 - 历史数据分析师 - agents['张果老'] = Agent( - name="ZhangGuoLao", - instructions=""" - 你是张果老,历史数据分析师。 - - 你的特点: - - 擅长历史回测和趋势分析 - - 立场:看涨派,从历史中寻找机会 - - 风格:博古通今,从历史中找规律 - - 在辩论中: - 1. 从历史数据角度分析市场 - 2. 引用历史案例和数据 - 3. 保持乐观的投资态度 - 4. 发言以"张果老曰:"开头 - 5. 发言完毕后说"请铁拐李继续论道" - """, - functions=[self.to_tieguaili] - ) - - # 铁拐李 - 逆向投资大师 - agents['铁拐李'] = Agent( - name="TieGuaiLi", - instructions=""" - 你是铁拐李,逆向投资大师。 - - 你的特点: - - 擅长逆向思维和危机发现 - - 立场:看跌派,挑战主流观点 - - 风格:不拘一格,敢于质疑 - - 在辩论中: - 1. 从逆向投资角度分析市场 - 2. 挑战前面仙人的观点 - 3. 寻找市场的潜在危机 - 4. 发言以"铁拐李曰:"开头 - 5. 作为最后发言者,要总结四仙观点并给出结论 - """, - functions=[] # 最后一个,不需要转换 - ) - - return agents - - def to_hexiangu(self): - """转到何仙姑""" - return self.agents['何仙姑'] - - def to_zhangguolao(self): - """转到张果老""" - return self.agents['张果老'] - - def to_tieguaili(self): - """转到铁拐李""" - return self.agents['铁拐李'] - - async def conduct_debate(self, topic: str, context: Dict[str, Any] = None) -> Dict[str, Any]: - """进行四仙辩论""" - if not self.client: - print("❌ 客户端未初始化,无法进行辩论") - return None - - print("🏛️ 稷下学宫四仙论道开始!") - print("=" * 60) - print(f"🎯 论道主题: {topic}") - print(f"⏰ 开始时间: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") - print() - - # 构建初始提示 - prompt = self.build_prompt(topic, context) - - try: - print("⚔️ 吕洞宾仙长请先发言...") - print("-" * 40) - - # 开始辩论 - response = self.client.run( - agent=self.agents['吕洞宾'], - messages=[{"role": "user", "content": prompt}], - max_turns=10, - model_override="openai/gpt-3.5-turbo" # 使用稳定的模型 - ) - - print("\n" + "=" * 60) - print("🎊 四仙论道圆满结束!") - - # 处理结果 - result = self.process_result(response, topic, context) - self.display_summary(result) - - return result - - except Exception as e: - print(f"❌ 论道过程中出错: {e}") - import traceback - traceback.print_exc() - return None - - def build_prompt(self, topic: str, context: Dict[str, Any] = None) -> str: - """构建辩论提示""" - context_str = "" - if context: - context_str = f"\n📊 市场背景:\n{json.dumps(context, indent=2, ensure_ascii=False)}\n" - - prompt = f""" - 🏛️ 稷下学宫四仙论道正式开始! - - 📜 论道主题: {topic} - {context_str} - - 🎭 论道规则: - 1. 四仙按序发言:吕洞宾 → 何仙姑 → 张果老 → 铁拐李 - 2. 正反方交替:吕洞宾(看涨) → 何仙姑(看跌) → 张果老(看涨) → 铁拐李(看跌) - 3. 每位仙人从专业角度分析,提供具体数据支撑 - 4. 可以质疑前面仙人的观点 - 5. 保持仙风道骨的表达风格 - 6. 铁拐李作为最后发言者要总结观点 - - 🗡️ 请吕洞宾仙长首先发言! - 记住:你是技术分析专家,要从技术面找到投资机会! - """ - return prompt - - def process_result(self, response, topic: str, context: Dict[str, Any]) -> Dict[str, Any]: - """处理辩论结果""" - messages = response.messages if hasattr(response, 'messages') else [] - - debate_messages = [] - for msg in messages: - if msg.get('role') == 'assistant' and msg.get('content'): - content = msg['content'] - speaker = self.extract_speaker(content) - - debate_messages.append({ - 'speaker': speaker, - 'content': content, - 'timestamp': datetime.now().isoformat() - }) - - return { - "debate_id": f"jixia_simple_{datetime.now().strftime('%Y%m%d_%H%M%S')}", - "topic": topic, - "context": context, - "messages": debate_messages, - "final_output": debate_messages[-1]['content'] if debate_messages else "", - "timestamp": datetime.now().isoformat(), - "framework": "OpenAI Swarm (Simplified)" - } - - def extract_speaker(self, content: str) -> str: - """从内容中提取发言者""" - for name in self.immortals.keys(): - if f"{name}曰" in content: - return name - return "未知仙人" - - def display_summary(self, result: Dict[str, Any]): - """显示辩论总结""" - print("\n🌟 四仙论道总结") - print("=" * 60) - print(f"📜 主题: {result['topic']}") - print(f"⏰ 时间: {result['timestamp']}") - print(f"🔧 框架: {result['framework']}") - print(f"💬 发言数: {len(result['messages'])}条") - - print("\n🏆 最终总结:") - print("-" * 40) - if result['messages']: - print(result['final_output']) - - print("\n✨ 辩论特色:") - print("🗡️ 四仙各展所长,观点多元") - print("⚖️ 正反方交替,辩论激烈") - print("🚀 基于Swarm,性能优越") - -# 主函数 -async def main(): - """主函数""" - print("🏛️ 稷下学宫简化版 - OpenAI Swarm") - print("🚀 四仙论道,简洁高效") - print() - - # 创建辩论系统 - academy = JixiaSimpleSwarm() - - if not academy.client: - print("❌ 系统初始化失败") - return - - # 辩论主题 - topics = [ - "英伟达股价走势:AI泡沫还是技术革命?", - "美联储2024年货币政策:加息还是降息?", - "比特币vs黄金:谁是更好的避险资产?", - "中国房地产市场:触底反弹还是继续下行?" - ] - - # 随机选择主题 - topic = random.choice(topics) - - # 市场背景 - context = { - "market_sentiment": "谨慎乐观", - "volatility": "中等", - "key_events": ["财报季", "央行会议", "地缘政治"] - } - - # 开始辩论 - result = await academy.conduct_debate(topic, context) - - if result: - print(f"\n🎉 辩论成功!ID: {result['debate_id']}") - else: - print("❌ 辩论失败") - -if __name__ == "__main__": - asyncio.run(main()) \ No newline at end of file diff --git a/examples/debates/swarm_debate.py b/examples/debates/swarm_debate_example.py similarity index 100% rename from examples/debates/swarm_debate.py rename to examples/debates/swarm_debate_example.py diff --git a/requirements.txt b/requirements.txt index cc8eca4..22f636a 100644 --- a/requirements.txt +++ b/requirements.txt @@ -52,8 +52,8 @@ google-cloud-aiplatform>=1.38.0 PyYAML>=6.0 python-frontmatter>=1.0.0 -# 市场数据 - OpenBB(可选安装) -openbb>=4.1.0 +# 市场数据 - OpenBB(可选安装,未默认安装。若需启用,请取消下一行注释) +# openbb>=4.1.0 # 新增:从 .env 加载本地环境变量 python-dotenv>=1.0.1 diff --git a/src/jixia/engines/openbb_engine.py b/src/jixia/engines/openbb_engine.py index 9b8cb30..87febfd 100644 --- a/src/jixia/engines/openbb_engine.py +++ b/src/jixia/engines/openbb_engine.py @@ -6,7 +6,7 @@ OpenBB 集成引擎 from typing import Dict, List, Any, Optional from dataclasses import dataclass -import openbb + @dataclass class ImmortalConfig: @@ -18,7 +18,7 @@ class ImmortalConfig: class APIResult: """API调用结果数据类""" success: bool - data: Optional[Dict[str, Any]] = None + data: Optional[Any] = None provider_used: Optional[str] = None error: Optional[str] = None @@ -29,6 +29,9 @@ class OpenBBEngine: """ 初始化 OpenBB 引擎 """ + # 延迟导入 OpenBB,避免未安装时报错 + self._obb = None + # 八仙专属数据源分配 self.immortal_sources: Dict[str, ImmortalConfig] = { '吕洞宾': ImmortalConfig( # 乾-技术分析专家 @@ -67,6 +70,18 @@ class OpenBBEngine: print("✅ OpenBB 引擎初始化完成") + def _ensure_openbb(self): + """Lazy import OpenBB v4 obb router.""" + if self._obb is not None: + return True + try: + from openbb import obb # type: ignore + self._obb = obb + return True + except Exception: + self._obb = None + return False + def get_immortal_data(self, immortal_name: str, data_type: str, symbol: str = 'AAPL') -> APIResult: """ 为特定八仙获取专属数据 @@ -88,56 +103,59 @@ class OpenBBEngine: # 根据数据类型调用不同的 OpenBB 函数 try: + if not self._ensure_openbb(): + return APIResult(success=False, error='OpenBB 未安装,请先安装 openbb>=4 并在 requirements.txt 启用') + obb = self._obb if data_type == 'price': - result = openbb.obb.equity.price.quote(symbol=symbol, provider=immortal_config.primary) + result = obb.equity.price.quote(symbol=symbol, provider=immortal_config.primary) return APIResult( success=True, - data=result.results, + data=getattr(result, 'results', getattr(result, 'to_dict', lambda: None)()), provider_used=immortal_config.primary ) elif data_type == 'historical': - result = openbb.obb.equity.price.historical(symbol=symbol, provider=immortal_config.primary) + result = obb.equity.price.historical(symbol=symbol, provider=immortal_config.primary) return APIResult( success=True, - data=result.results, + data=getattr(result, 'results', getattr(result, 'to_dict', lambda: None)()), provider_used=immortal_config.primary ) elif data_type == 'profile': - result = openbb.obb.equity.profile(symbol=symbol, provider=immortal_config.primary) + result = obb.equity.profile(symbol=symbol, provider=immortal_config.primary) return APIResult( success=True, - data=result.results, + data=getattr(result, 'results', getattr(result, 'to_dict', lambda: None)()), provider_used=immortal_config.primary ) elif data_type == 'news': - result = openbb.obb.news.company(symbol=symbol) + result = obb.news.company(symbol=symbol) return APIResult( success=True, - data=result.results, + data=getattr(result, 'results', getattr(result, 'to_dict', lambda: None)()), provider_used='news_api' ) elif data_type == 'earnings': - result = openbb.obb.equity.earnings.earnings_historical(symbol=symbol, provider=immortal_config.primary) + result = obb.equity.earnings.earnings_historical(symbol=symbol, provider=immortal_config.primary) return APIResult( success=True, - data=result.results, + data=getattr(result, 'results', getattr(result, 'to_dict', lambda: None)()), provider_used=immortal_config.primary ) elif data_type == 'dividends': - result = openbb.obb.equity.fundamental.dividend(symbol=symbol, provider=immortal_config.primary) + result = obb.equity.fundamental.dividend(symbol=symbol, provider=immortal_config.primary) return APIResult( success=True, - data=result.results, + data=getattr(result, 'results', getattr(result, 'to_dict', lambda: None)()), provider_used=immortal_config.primary ) elif data_type == 'screener': # 使用简单的筛选器作为替代 - result = openbb.obb.equity.screener.etf( + result = obb.equity.screener.etf( provider=immortal_config.primary ) return APIResult( success=True, - data=result.results, + data=getattr(result, 'results', getattr(result, 'to_dict', lambda: None)()), provider_used=immortal_config.primary ) else: diff --git a/src/jixia/engines/openbb_stock_data.py b/src/jixia/engines/openbb_stock_data.py index 2f42b39..e0d9202 100644 --- a/src/jixia/engines/openbb_stock_data.py +++ b/src/jixia/engines/openbb_stock_data.py @@ -3,7 +3,6 @@ OpenBB 股票数据获取模块 """ -import openbb from datetime import datetime, timedelta from typing import List, Dict, Any, Optional @@ -26,17 +25,24 @@ def get_stock_data(symbol: str, days: int = 90) -> Optional[List[Dict[str, Any]] print(f"🔍 正在获取 {symbol} 近 {days} 天的数据...") print(f" 时间范围: {start_date.strftime('%Y-%m-%d')} 到 {end_date.strftime('%Y-%m-%d')}") - # 使用OpenBB获取数据 - result = openbb.obb.equity.price.historical( + # 使用OpenBB获取数据(延迟导入) + try: + from openbb import obb # type: ignore + except Exception as e: + print(f"⚠️ OpenBB 未安装或导入失败: {e}") + return None + + result = obb.equity.price.historical( symbol=symbol, provider='yfinance', start_date=start_date.strftime('%Y-%m-%d'), end_date=end_date.strftime('%Y-%m-%d') ) - if result and result.results: - print(f"✅ 成功获取 {len(result.results)} 条记录") - return result.results + results = getattr(result, 'results', None) + if results: + print(f"✅ 成功获取 {len(results)} 条记录") + return results else: print("❌ 未获取到数据") return None @@ -64,17 +70,24 @@ def get_etf_data(symbol: str, days: int = 90) -> Optional[List[Dict[str, Any]]]: print(f"🔍 正在获取 {symbol} 近 {days} 天的数据...") print(f" 时间范围: {start_date.strftime('%Y-%m-%d')} 到 {end_date.strftime('%Y-%m-%d')}") - # 使用OpenBB获取数据 - result = openbb.obb.etf.historical( + # 使用OpenBB获取数据(延迟导入) + try: + from openbb import obb # type: ignore + except Exception as e: + print(f"⚠️ OpenBB 未安装或导入失败: {e}") + return None + + result = obb.etf.price.historical( symbol=symbol, provider='yfinance', start_date=start_date.strftime('%Y-%m-%d'), end_date=end_date.strftime('%Y-%m-%d') ) - if result and result.results: - print(f"✅ 成功获取 {len(result.results)} 条记录") - return result.results + results = getattr(result, 'results', None) + if results: + print(f"✅ 成功获取 {len(results)} 条记录") + return results else: print("❌ 未获取到数据") return None diff --git a/tests/test_openbb_fallback.py b/tests/test_openbb_fallback.py new file mode 100644 index 0000000..d11b3dc --- /dev/null +++ b/tests/test_openbb_fallback.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- +""" +验证在未安装 OpenBB 时,OpenBB Tab 的数据加载回退行为。 +该测试不强制要求安装 OpenBB,因此仅检查函数能返回非空 DataFrame。 +""" + +import importlib +import types +import pandas as pd + +from app.tabs.openbb_tab import _load_price_data + + +def test_openbb_fallback_without_openbb(): + # 尝试卸载 openbb 以模拟未安装环境(若本地未安装会抛错,忽略) + try: + if 'openbb' in list(importlib.sys.modules.keys()): + del importlib.sys.modules['openbb'] + except Exception: + pass + + df = _load_price_data('AAPL', 180) + assert isinstance(df, pd.DataFrame) + assert not df.empty + assert 'Date' in df.columns and 'Close' in df.columns diff --git a/tmp_rovodev_vertex_check.py b/tmp_rovodev_vertex_check.py new file mode 100644 index 0000000..8e844be --- /dev/null +++ b/tmp_rovodev_vertex_check.py @@ -0,0 +1,121 @@ +#!/usr/bin/env python3 +""" +临时健康检查脚本:验证本机 Vertex AI 基础配置是否完好。 +使用说明: + python3 tmp_rovodev_vertex_check.py +运行完成后请将输出粘贴给我,我会据此给出修复建议。 +完成诊断后可删除该文件。 +""" + +import os +import json +import sys +from pathlib import Path + +STATUS = [] + +def ok(msg): + STATUS.append((True, msg)) + print(f"✅ {msg}") + +def warn(msg): + STATUS.append((None, msg)) + print(f"⚠️ {msg}") + +def fail(msg): + STATUS.append((False, msg)) + print(f"❌ {msg}") + +def has(var): + return bool(os.getenv(var)) + +print("🧪 Vertex AI 本地配置健康检查\n") + +# 1) 关键环境变量与文件 +adc_path = Path.home() / ".config/gcloud/application_default_credentials.json" +if adc_path.exists(): + ok(f"找到 ADC (Application Default Credentials): {adc_path}") +else: + warn("未找到 ADC 文件 (~/.config/gcloud/application_default_credentials.json)。如果你改用服务账号密钥也可忽略此项") + +vars_to_check = [ + "GOOGLE_GENAI_USE_VERTEXAI", + "GOOGLE_CLOUD_PROJECT_ID", + "GOOGLE_CLOUD_LOCATION", + "VERTEX_MEMORY_BANK_ENABLED", + "GOOGLE_API_KEY", + "GOOGLE_SERVICE_ACCOUNT_KEY", +] + +print("\n🔍 环境变量:") +for v in vars_to_check: + val = os.getenv(v) + if not val: + if v in ("GOOGLE_SERVICE_ACCOUNT_KEY", "GOOGLE_API_KEY"): + # 可选 + warn(f"{v}: 未设置 (可选)") + else: + fail(f"{v}: 未设置") + else: + if v.endswith("KEY"): + ok(f"{v}: 已设置 (不显示具体值)") + else: + ok(f"{v}: {val}") + +# 2) 依赖与版本 +print("\n🔍 依赖:google-cloud-aiplatform") +try: + import google + from google.cloud import aiplatform + ok(f"google-cloud-aiplatform 可导入,版本: {aiplatform.__version__}") +except Exception as e: + fail(f"无法导入 google-cloud-aiplatform: {e}") + +# 3) 验证默认凭据可用性(若存在) +print("\n🔍 默认凭据 (ADC) 可用性:") +try: + import google.auth + from google.auth.transport.requests import Request + creds, proj = google.auth.default(scopes=[ + "https://www.googleapis.com/auth/cloud-platform", + ]) + # 刷新一次,验证有效性 + try: + creds.refresh(Request()) + ok("默认凭据可用并成功刷新访问令牌") + except Exception as e: + warn(f"找到默认凭据,但刷新失败(可能未登录或网络受限):{e}") + if proj: + ok(f"默认凭据解析到的 Project: {proj}") + else: + warn("默认凭据未解析出 Project ID") +except Exception as e: + warn(f"未能通过 google.auth.default() 获取默认凭据:{e}") + +# 4) 初始化 Vertex AI(使用环境中的 Project/Location) +print("\n🔍 Vertex AI 初始化:") +project = os.getenv("GOOGLE_CLOUD_PROJECT_ID") +location = os.getenv("GOOGLE_CLOUD_LOCATION", "us-central1") +if project: + try: + from google.cloud import aiplatform + aiplatform.init(project=project, location=location) + ok(f"Vertex AI 初始化成功: project={project}, location={location}") + except Exception as e: + fail(f"Vertex AI 初始化失败: {e}") +else: + fail("缺少 GOOGLE_CLOUD_PROJECT_ID,无法初始化 Vertex AI") + +# 5) 汇总 +print("\n📋 检查摘要:") +all_good = True +for s, msg in STATUS: + if s is False: + all_good = False + +if all_good: + print("\n🎉 结论:看起来你的 Vertex AI 本地配置是可用的。") +else: + print("\n🚧 结论:存在未通过项。请根据上面的 ❌/⚠️ 提示逐项修复后重试。") + +print("\n提示:\n- 启用 Vertex 模式:export GOOGLE_GENAI_USE_VERTEXAI=TRUE\n- 必填:export GOOGLE_CLOUD_PROJECT_ID=你的项目ID\n- 可选:export GOOGLE_CLOUD_LOCATION=us-central1\n- 建议使用 ADC:gcloud auth application-default login\n- 或设置服务账号密钥:export GOOGLE_SERVICE_ACCOUNT_KEY='(JSON 字符串或路径)'\n")