first commit
This commit is contained in:
commit
c73341b950
20
.env.example
Normal file
20
.env.example
Normal file
@ -0,0 +1,20 @@
|
||||
# Tushare API
|
||||
TUSHARE_TOKEN=your_tushare_token_here
|
||||
|
||||
# 智谱AI GLM-4 API
|
||||
ZHIPUAI_API_KEY=your_zhipuai_key_here
|
||||
|
||||
# Database (使用SQLite,无需额外配置)
|
||||
DATABASE_URL=sqlite:///./stock_agent.db
|
||||
|
||||
# API Settings
|
||||
API_HOST=0.0.0.0
|
||||
API_PORT=8000
|
||||
DEBUG=True
|
||||
|
||||
# Security
|
||||
SECRET_KEY=your_secret_key_here_change_in_production
|
||||
RATE_LIMIT=100/minute
|
||||
|
||||
# CORS
|
||||
CORS_ORIGINS=http://localhost:8000,http://127.0.0.1:8000
|
||||
62
.gitignore
vendored
Normal file
62
.gitignore
vendored
Normal file
@ -0,0 +1,62 @@
|
||||
# Python
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
*.so
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# Virtual Environment
|
||||
venv/
|
||||
ENV/
|
||||
env/
|
||||
.venv
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
# IDE
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Database
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# Logs
|
||||
*.log
|
||||
logs/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Redis
|
||||
dump.rdb
|
||||
|
||||
# Testing
|
||||
.pytest_cache/
|
||||
.coverage
|
||||
htmlcov/
|
||||
|
||||
# Claude Code
|
||||
.claude/
|
||||
348
PROJECT_SUMMARY.md
Normal file
348
PROJECT_SUMMARY.md
Normal file
@ -0,0 +1,348 @@
|
||||
# 项目完成总结
|
||||
|
||||
## 🎉 A股AI分析Agent系统 - 开发完成!
|
||||
|
||||
### 项目概述
|
||||
|
||||
成功开发了一个功能完整的A股智能分析系统,集成了AI大模型、实时数据查询、技术分析等功能。
|
||||
|
||||
---
|
||||
|
||||
## ✅ 已完成功能
|
||||
|
||||
### 1. 核心功能
|
||||
|
||||
#### 🤖 AI Agent系统
|
||||
- ✅ 增强版Agent(集成智谱AI GLM-4)
|
||||
- ✅ 规则模式(无需LLM也可运行)
|
||||
- ✅ 智能意图识别
|
||||
- ✅ 上下文管理
|
||||
- ✅ 对话历史保存
|
||||
|
||||
#### 📊 数据查询
|
||||
- ✅ 实时行情查询(Tushare)
|
||||
- ✅ 历史K线数据
|
||||
- ✅ 技术指标计算(MA、MACD、RSI、KDJ、BOLL)
|
||||
- ✅ 基本面信息查询
|
||||
- ✅ 内存缓存(无需Redis)
|
||||
|
||||
#### 🎨 数据可视化
|
||||
- ✅ 专业K线图(Lightweight Charts)
|
||||
- ✅ 成交量柱状图
|
||||
- ✅ 技术指标图表
|
||||
- ✅ 交互式图表操作
|
||||
|
||||
#### 🔌 技能插件系统
|
||||
- ✅ 插件化架构
|
||||
- ✅ 动态启用/禁用
|
||||
- ✅ 4个核心技能:
|
||||
- market_data(行情查询)
|
||||
- technical_analysis(技术分析)
|
||||
- fundamental(基本面)
|
||||
- visualization(可视化)
|
||||
|
||||
#### 🧠 智能识别
|
||||
- ✅ 200+股票名称数据库
|
||||
- ✅ 支持中文名称识别
|
||||
- ✅ 支持简称识别
|
||||
- ✅ 模糊匹配
|
||||
|
||||
### 2. 技术实现
|
||||
|
||||
#### 后端(Python)
|
||||
- ✅ FastAPI框架
|
||||
- ✅ SQLAlchemy ORM(SQLite)
|
||||
- ✅ 智谱AI GLM-4集成
|
||||
- ✅ Tushare数据接口
|
||||
- ✅ 内存缓存系统
|
||||
- ✅ 异步处理
|
||||
|
||||
#### 前端(轻量级)
|
||||
- ✅ Vue 3(CDN版本)
|
||||
- ✅ Bootstrap 5
|
||||
- ✅ Lightweight Charts
|
||||
- ✅ 响应式设计
|
||||
- ✅ 实时对话界面
|
||||
|
||||
#### 数据库
|
||||
- ✅ SQLite(轻量级)
|
||||
- ✅ 对话历史存储
|
||||
- ✅ 用户偏好管理
|
||||
|
||||
---
|
||||
|
||||
## 📁 项目结构
|
||||
|
||||
```
|
||||
Stock_Agent/
|
||||
├── backend/ # 后端(35个文件)
|
||||
│ ├── app/
|
||||
│ │ ├── agent/ # AI Agent核心
|
||||
│ │ │ ├── core.py # 原始Agent
|
||||
│ │ │ ├── enhanced_agent.py # 增强版Agent(LLM)
|
||||
│ │ │ ├── context.py # 上下文管理
|
||||
│ │ │ └── skill_manager.py # 技能管理
|
||||
│ │ ├── api/ # API路由
|
||||
│ │ │ ├── chat.py # 对话接口
|
||||
│ │ │ ├── stock.py # 股票数据
|
||||
│ │ │ └── skills.py # 技能管理
|
||||
│ │ ├── models/ # 数据模型
|
||||
│ │ │ ├── database.py # SQLAlchemy模型
|
||||
│ │ │ ├── chat.py # Pydantic模型
|
||||
│ │ │ └── stock.py # 股票模型
|
||||
│ │ ├── services/ # 服务层
|
||||
│ │ │ ├── tushare_service.py # Tushare数据
|
||||
│ │ │ ├── cache_service.py # 内存缓存
|
||||
│ │ │ ├── db_service.py # 数据库
|
||||
│ │ │ └── llm_service.py # LLM服务
|
||||
│ │ ├── skills/ # 技能插件
|
||||
│ │ │ ├── base.py # 基类
|
||||
│ │ │ ├── market_data.py # 行情查询
|
||||
│ │ │ ├── technical_analysis.py # 技术分析
|
||||
│ │ │ ├── fundamental.py # 基本面
|
||||
│ │ │ └── visualization.py # 可视化
|
||||
│ │ ├── utils/ # 工具函数
|
||||
│ │ │ ├── logger.py # 日志
|
||||
│ │ │ ├── validators.py # 验证
|
||||
│ │ │ ├── indicators.py # 技术指标
|
||||
│ │ │ └── stock_names.py # 股票名称库
|
||||
│ │ ├── config.py # 配置管理
|
||||
│ │ └── main.py # 应用入口
|
||||
│ ├── requirements.txt # 依赖
|
||||
│ ├── start.sh # 启动脚本
|
||||
│ ├── run.sh # 检查并启动
|
||||
│ └── diagnose.sh # 诊断脚本
|
||||
├── frontend/ # 前端(3个文件)
|
||||
│ ├── index.html # 主页面
|
||||
│ ├── css/style.css # 样式
|
||||
│ └── js/app.js # Vue应用
|
||||
├── docs/ # 文档(4个文件)
|
||||
│ ├── API.md
|
||||
│ ├── DEPLOYMENT.md
|
||||
│ ├── USER_GUIDE.md
|
||||
│ └── INSTALL_GUIDE.md
|
||||
├── .env.example # 配置模板
|
||||
├── .gitignore
|
||||
├── README.md
|
||||
└── install.sh # 安装脚本
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🚀 快速启动
|
||||
|
||||
### 方法1:一键启动(推荐)
|
||||
|
||||
```bash
|
||||
cd /Users/aaron/source_code/Stock_Agent/backend
|
||||
./run.sh
|
||||
```
|
||||
|
||||
### 方法2:手动启动
|
||||
|
||||
```bash
|
||||
cd /Users/aaron/source_code/Stock_Agent/backend
|
||||
source venv/bin/activate
|
||||
python -m app.main
|
||||
```
|
||||
|
||||
### 访问系统
|
||||
|
||||
- 🌐 前端界面: http://localhost:8000
|
||||
- 📚 API文档: http://localhost:8000/docs
|
||||
|
||||
---
|
||||
|
||||
## 💡 使用示例
|
||||
|
||||
### 支持的查询方式
|
||||
|
||||
```
|
||||
✅ "中国卫通的技术分析"
|
||||
✅ "贵州茅台的实时行情"
|
||||
✅ "比亚迪的K线图"
|
||||
✅ "宁德时代的基本信息"
|
||||
✅ "查询600519"
|
||||
✅ "分析000001的技术指标"
|
||||
```
|
||||
|
||||
### AI分析示例
|
||||
|
||||
```
|
||||
用户:对中国卫通进行技术分析
|
||||
|
||||
系统:
|
||||
【601698】技术指标:
|
||||
均线:MA5=15.23, MA10=15.10, MA20=14.95
|
||||
MACD:DIF=0.12, DEA=0.08, MACD=0.08
|
||||
RSI:RSI6=58.3, RSI12=55.2, RSI24=52.1
|
||||
|
||||
【AI分析】
|
||||
中国卫通(601698)当前技术面表现中性偏多...
|
||||
(智能分析总结)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🔧 配置说明
|
||||
|
||||
### 必需配置
|
||||
|
||||
在`.env`文件中配置:
|
||||
|
||||
```env
|
||||
# Tushare数据源(必需)
|
||||
TUSHARE_TOKEN=your_token_here
|
||||
|
||||
# 智谱AI(可选,不配置则使用规则模式)
|
||||
ZHIPUAI_API_KEY=your_key_here
|
||||
```
|
||||
|
||||
### 运行模式
|
||||
|
||||
1. **完整模式**(推荐)
|
||||
- 配置Tushare + 智谱AI
|
||||
- 支持所有功能 + AI分析
|
||||
|
||||
2. **规则模式**
|
||||
- 仅配置Tushare
|
||||
- 支持数据查询,无AI分析
|
||||
|
||||
3. **演示模式**
|
||||
- 不配置任何API
|
||||
- 仅展示界面和架构
|
||||
|
||||
---
|
||||
|
||||
## 📊 技术亮点
|
||||
|
||||
### 1. 双模式Agent
|
||||
- LLM模式:智能意图识别 + AI分析
|
||||
- 规则模式:快速响应 + 稳定可靠
|
||||
- 自动切换:LLM失败时回退
|
||||
|
||||
### 2. 智能股票识别
|
||||
- 200+股票名称数据库
|
||||
- 支持全称、简称、模糊匹配
|
||||
- 自动提取股票代码
|
||||
|
||||
### 3. 轻量级架构
|
||||
- 无需Redis(内存缓存)
|
||||
- 无需PostgreSQL(SQLite)
|
||||
- 无需构建工具(CDN)
|
||||
- 一键启动
|
||||
|
||||
### 4. 专业图表
|
||||
- TradingView开源图表库
|
||||
- 金融级K线渲染
|
||||
- 交互式操作
|
||||
|
||||
---
|
||||
|
||||
## 📝 文档清单
|
||||
|
||||
1. **README.md** - 项目说明和快速开始
|
||||
2. **docs/INSTALL_GUIDE.md** - 详细安装指南
|
||||
3. **docs/USER_GUIDE.md** - 用户使用手册
|
||||
4. **docs/DEPLOYMENT.md** - 部署文档
|
||||
5. **本文档** - 项目完成总结
|
||||
|
||||
---
|
||||
|
||||
## 🎯 已解决的问题
|
||||
|
||||
### 问题1:Python 3.13兼容性
|
||||
- ✅ 更新依赖版本
|
||||
- ✅ 创建安装指南
|
||||
- ✅ 提供多种解决方案
|
||||
|
||||
### 问题2:SQLAlchemy保留字冲突
|
||||
- ✅ 修改字段名(metadata → msg_metadata)
|
||||
- ✅ 更新所有引用
|
||||
|
||||
### 问题3:配置文件加载
|
||||
- ✅ 智能查找.env文件
|
||||
- ✅ 支持多目录启动
|
||||
|
||||
### 问题4:股票名称识别
|
||||
- ✅ 创建200+股票名称库
|
||||
- ✅ 支持中文名称和简称
|
||||
- ✅ 模糊匹配算法
|
||||
|
||||
### 问题5:缺少LLM分析
|
||||
- ✅ 集成智谱AI GLM-4
|
||||
- ✅ 智能意图识别
|
||||
- ✅ AI分析总结
|
||||
- ✅ 自动回退机制
|
||||
|
||||
---
|
||||
|
||||
## 🎊 项目特色
|
||||
|
||||
1. **开箱即用**
|
||||
- 一键安装脚本
|
||||
- 自动检查脚本
|
||||
- 详细错误提示
|
||||
|
||||
2. **智能分析**
|
||||
- LLM驱动的意图识别
|
||||
- 专业的技术分析
|
||||
- 自然语言总结
|
||||
|
||||
3. **易于扩展**
|
||||
- 插件化技能系统
|
||||
- 清晰的代码结构
|
||||
- 完善的文档
|
||||
|
||||
4. **生产就绪**
|
||||
- 错误处理
|
||||
- 日志系统
|
||||
- 缓存优化
|
||||
- 数据验证
|
||||
|
||||
---
|
||||
|
||||
## 📈 下一步建议
|
||||
|
||||
### 短期优化
|
||||
1. 添加更多股票名称
|
||||
2. 优化LLM提示词
|
||||
3. 添加更多技术指标
|
||||
4. 改进图表交互
|
||||
|
||||
### 中期扩展
|
||||
1. 支持港股、美股
|
||||
2. 添加实时预警
|
||||
3. 用户认证系统
|
||||
4. 自选股管理
|
||||
|
||||
### 长期规划
|
||||
1. 移动端适配
|
||||
2. 多语言支持
|
||||
3. 社区功能
|
||||
4. 量化策略
|
||||
|
||||
---
|
||||
|
||||
## 🙏 致谢
|
||||
|
||||
- **Tushare** - 金融数据接口
|
||||
- **智谱AI** - GLM-4大模型
|
||||
- **FastAPI** - 高性能Web框架
|
||||
- **LangChain** - AI应用框架
|
||||
- **Lightweight Charts** - 专业图表库
|
||||
|
||||
---
|
||||
|
||||
## 📞 支持
|
||||
|
||||
如有问题,请查看:
|
||||
1. [安装指南](docs/INSTALL_GUIDE.md)
|
||||
2. [用户手册](docs/USER_GUIDE.md)
|
||||
3. [部署文档](docs/DEPLOYMENT.md)
|
||||
|
||||
---
|
||||
|
||||
**项目状态:✅ 完成并可用**
|
||||
|
||||
**最后更新:2026-02-03**
|
||||
328
README.md
Normal file
328
README.md
Normal file
@ -0,0 +1,328 @@
|
||||
# A股AI分析Agent系统
|
||||
|
||||
基于AI Agent的股票智能分析系统,提供自然语言对话界面,支持实时行情查询、技术分析、基本面分析等功能。
|
||||
|
||||
## 功能特性
|
||||
|
||||
- **自然语言对话**:通过对话方式查询股票信息
|
||||
- **实时行情查询**:获取股票实时价格、涨跌幅等数据
|
||||
- **技术指标分析**:计算MA、MACD、RSI、KDJ、BOLL等技术指标
|
||||
- **基本面信息**:查询公司概况、行业、上市日期等
|
||||
- **数据可视化**:生成专业的K线图和技术指标图表
|
||||
- **技能插件系统**:可扩展的技能架构,支持动态启用/禁用
|
||||
- **对话历史**:保存和查看历史分析记录
|
||||
|
||||
## 技术栈
|
||||
|
||||
### 后端
|
||||
- **框架**:FastAPI
|
||||
- **AI Agent**:LangChain + 智谱AI GLM-4
|
||||
- **数据源**:Tushare
|
||||
- **缓存**:内存缓存(无需Redis)
|
||||
- **数据库**:SQLite
|
||||
- **语言**:Python 3.11+ (推荐 3.11 或 3.12)
|
||||
|
||||
### 前端
|
||||
- **框架**:Vue 3 (CDN版本)
|
||||
- **UI**:Bootstrap 5
|
||||
- **图表**:Lightweight Charts
|
||||
- **通信**:Fetch API
|
||||
|
||||
## 项目结构
|
||||
|
||||
```
|
||||
Stock_Agent/
|
||||
├── backend/ # 后端代码
|
||||
│ ├── app/
|
||||
│ │ ├── agent/ # AI Agent核心
|
||||
│ │ ├── api/ # API路由
|
||||
│ │ ├── models/ # 数据模型
|
||||
│ │ ├── services/ # 数据服务
|
||||
│ │ ├── skills/ # 技能插件
|
||||
│ │ ├── utils/ # 工具函数
|
||||
│ │ ├── config.py # 配置管理
|
||||
│ │ └── main.py # 应用入口
|
||||
│ └── requirements.txt # Python依赖
|
||||
├── frontend/ # 前端代码
|
||||
│ ├── css/ # 样式文件
|
||||
│ ├── js/ # JavaScript文件
|
||||
│ └── index.html # 主页面
|
||||
├── .env.example # 环境变量示例
|
||||
├── .gitignore
|
||||
└── README.md
|
||||
```
|
||||
|
||||
## 快速开始
|
||||
|
||||
### ⚠️ 重要提示:Python 版本
|
||||
|
||||
**推荐使用 Python 3.11 或 3.12**。如果您使用 Python 3.13,可能会遇到依赖安装问题。
|
||||
|
||||
详细的安装问题解决方案请查看:[安装指南](docs/INSTALL_GUIDE.md)
|
||||
|
||||
### 1. 环境准备
|
||||
|
||||
**系统要求**:
|
||||
- Python 3.11 或 3.12(推荐)
|
||||
- 无需 Redis(使用内存缓存)
|
||||
|
||||
**获取API密钥**:
|
||||
- [Tushare](https://tushare.pro/):注册并获取Token
|
||||
- [智谱AI](https://open.bigmodel.cn/):注册并获取API Key
|
||||
|
||||
### 2. 安装依赖
|
||||
|
||||
```bash
|
||||
# 进入后端目录
|
||||
cd backend
|
||||
|
||||
# 创建虚拟环境(使用 Python 3.11)
|
||||
python3.11 -m venv venv
|
||||
|
||||
# 激活虚拟环境
|
||||
# Windows:
|
||||
venv\Scripts\activate
|
||||
# macOS/Linux:
|
||||
source venv/bin/activate
|
||||
|
||||
# 安装依赖
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
**如果遇到安装错误**,请查看 [安装指南](docs/INSTALL_GUIDE.md) 获取详细解决方案。
|
||||
|
||||
### 3. 配置环境变量
|
||||
|
||||
复制 `.env.example` 为 `.env` 并填写配置:
|
||||
|
||||
```bash
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
编辑 `.env` 文件:
|
||||
|
||||
```env
|
||||
# Tushare API
|
||||
TUSHARE_TOKEN=your_tushare_token_here
|
||||
|
||||
# 智谱AI GLM-4 API
|
||||
ZHIPUAI_API_KEY=your_zhipuai_key_here
|
||||
|
||||
# 其他配置保持默认即可
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_DB=0
|
||||
|
||||
# 其他配置保持默认即可
|
||||
```
|
||||
|
||||
### 4. 启动Redis(可选)
|
||||
|
||||
如果要使用缓存功能,请先启动Redis:
|
||||
|
||||
```bash
|
||||
# macOS (使用Homebrew)
|
||||
brew services start redis
|
||||
|
||||
# Linux
|
||||
sudo systemctl start redis
|
||||
|
||||
# Windows
|
||||
# 下载并运行Redis for Windows
|
||||
```
|
||||
|
||||
### 5. 启动后端服务
|
||||
|
||||
```bash
|
||||
# 在backend目录下
|
||||
cd backend
|
||||
|
||||
# 启动服务
|
||||
python -m app.main
|
||||
|
||||
# 或使用uvicorn
|
||||
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000
|
||||
```
|
||||
|
||||
服务启动后,访问:
|
||||
- 前端界面:http://localhost:8000
|
||||
- API文档:http://localhost:8000/docs
|
||||
|
||||
## 使用指南
|
||||
|
||||
### 基本查询
|
||||
|
||||
1. **查询实时行情**
|
||||
```
|
||||
查询600519的实时行情
|
||||
贵州茅台的价格
|
||||
000001现在多少钱
|
||||
```
|
||||
|
||||
2. **查看K线图**
|
||||
```
|
||||
600519的K线图
|
||||
贵州茅台的走势
|
||||
```
|
||||
|
||||
3. **技术指标分析**
|
||||
```
|
||||
600519的技术指标
|
||||
分析贵州茅台的MACD
|
||||
```
|
||||
|
||||
4. **基本面信息**
|
||||
```
|
||||
600519的基本信息
|
||||
贵州茅台是什么行业
|
||||
```
|
||||
|
||||
### 技能管理
|
||||
|
||||
点击右上角"技能管理"按钮,可以:
|
||||
- 查看所有可用技能
|
||||
- 启用/禁用特定技能
|
||||
- 查看技能描述
|
||||
|
||||
## API文档
|
||||
|
||||
启动服务后,访问 http://localhost:8000/docs 查看完整的API文档。
|
||||
|
||||
### 主要接口
|
||||
|
||||
#### 1. 发送消息
|
||||
```http
|
||||
POST /api/chat/message
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"message": "查询600519的实时行情",
|
||||
"session_id": "optional_session_id"
|
||||
}
|
||||
```
|
||||
|
||||
#### 2. 获取对话历史
|
||||
```http
|
||||
GET /api/chat/history/{session_id}
|
||||
```
|
||||
|
||||
#### 3. 获取股票行情
|
||||
```http
|
||||
GET /api/stock/quote/{stock_code}
|
||||
```
|
||||
|
||||
#### 4. 获取K线数据
|
||||
```http
|
||||
GET /api/stock/kline/{stock_code}?start_date=20240101&end_date=20240201
|
||||
```
|
||||
|
||||
#### 5. 获取技能列表
|
||||
```http
|
||||
GET /api/skills/
|
||||
```
|
||||
|
||||
## 开发指南
|
||||
|
||||
### 添加新技能
|
||||
|
||||
1. 在 `backend/app/skills/` 目录下创建新的技能文件
|
||||
2. 继承 `BaseSkill` 类并实现 `execute` 方法
|
||||
3. 在 `backend/app/agent/core.py` 中注册新技能
|
||||
|
||||
示例:
|
||||
|
||||
```python
|
||||
from app.skills.base import BaseSkill, SkillParameter
|
||||
|
||||
class MyNewSkill(BaseSkill):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "my_skill"
|
||||
self.description = "我的新技能"
|
||||
self.parameters = [
|
||||
SkillParameter(
|
||||
name="param1",
|
||||
type="string",
|
||||
description="参数1",
|
||||
required=True
|
||||
)
|
||||
]
|
||||
|
||||
async def execute(self, **kwargs) -> Dict[str, Any]:
|
||||
# 实现技能逻辑
|
||||
return {"result": "success"}
|
||||
```
|
||||
|
||||
### 扩展数据源
|
||||
|
||||
在 `backend/app/services/` 目录下添加新的数据服务类,参考 `tushare_service.py` 的实现。
|
||||
|
||||
## 常见问题
|
||||
|
||||
### 1. Redis连接失败
|
||||
|
||||
如果Redis未安装或未启动,系统会自动降级,不影响核心功能,但会失去缓存能力。
|
||||
|
||||
### 2. Tushare API限制
|
||||
|
||||
免费版Tushare有调用频率限制(120次/分钟)。如果遇到限制,可以:
|
||||
- 等待一段时间后重试
|
||||
- 考虑升级到付费版
|
||||
- 使用Redis缓存减少API调用
|
||||
|
||||
### 3. 股票代码格式
|
||||
|
||||
支持的股票代码格式:
|
||||
- 6位数字:600000、000001
|
||||
- 带后缀:600000.SH、000001.SZ
|
||||
- 股票名称:贵州茅台、中国平安
|
||||
|
||||
### 4. 端口被占用
|
||||
|
||||
如果8000端口被占用,可以修改 `.env` 文件中的 `API_PORT` 配置。
|
||||
|
||||
## 性能优化
|
||||
|
||||
1. **启用Redis缓存**:显著减少API调用和响应时间
|
||||
2. **调整缓存TTL**:在 `cache_service.py` 中根据需求调整缓存时间
|
||||
3. **限制历史消息数**:在 `context.py` 中调整 `max_history` 参数
|
||||
|
||||
## 安全建议
|
||||
|
||||
1. **生产环境**:
|
||||
- 修改 `.env` 中的 `SECRET_KEY`
|
||||
- 设置 `DEBUG=False`
|
||||
- 配置严格的CORS策略
|
||||
- 使用HTTPS
|
||||
|
||||
2. **API密钥**:
|
||||
- 不要将 `.env` 文件提交到版本控制
|
||||
- 定期更换API密钥
|
||||
- 使用环境变量或密钥管理服务
|
||||
|
||||
## 贡献指南
|
||||
|
||||
欢迎贡献代码!请遵循以下步骤:
|
||||
|
||||
1. Fork本项目
|
||||
2. 创建特性分支 (`git checkout -b feature/AmazingFeature`)
|
||||
3. 提交更改 (`git commit -m 'Add some AmazingFeature'`)
|
||||
4. 推送到分支 (`git push origin feature/AmazingFeature`)
|
||||
5. 开启Pull Request
|
||||
|
||||
## 许可证
|
||||
|
||||
本项目采用 MIT 许可证。
|
||||
|
||||
## 联系方式
|
||||
|
||||
如有问题或建议,请提交Issue。
|
||||
|
||||
## 致谢
|
||||
|
||||
- [Tushare](https://tushare.pro/) - 金融数据接口
|
||||
- [智谱AI](https://open.bigmodel.cn/) - AI模型服务
|
||||
- [FastAPI](https://fastapi.tiangolo.com/) - Web框架
|
||||
- [LangChain](https://www.langchain.com/) - AI Agent框架
|
||||
- [Lightweight Charts](https://tradingview.github.io/lightweight-charts/) - 图表库
|
||||
0
backend/app/__init__.py
Normal file
0
backend/app/__init__.py
Normal file
0
backend/app/agent/__init__.py
Normal file
0
backend/app/agent/__init__.py
Normal file
93
backend/app/agent/context.py
Normal file
93
backend/app/agent/context.py
Normal file
@ -0,0 +1,93 @@
|
||||
"""
|
||||
上下文管理器
|
||||
管理对话历史和上下文
|
||||
"""
|
||||
from typing import List, Dict, Optional
|
||||
from app.services.db_service import db_service
|
||||
from app.utils.logger import logger
|
||||
|
||||
|
||||
class ContextManager:
|
||||
"""上下文管理器"""
|
||||
|
||||
def __init__(self, max_history: int = 10):
|
||||
"""
|
||||
初始化上下文管理器
|
||||
|
||||
Args:
|
||||
max_history: 最大历史消息数
|
||||
"""
|
||||
self.max_history = max_history
|
||||
|
||||
def get_context(self, session_id: str) -> List[Dict[str, str]]:
|
||||
"""
|
||||
获取对话上下文
|
||||
|
||||
Args:
|
||||
session_id: 会话ID
|
||||
|
||||
Returns:
|
||||
消息列表
|
||||
"""
|
||||
messages = db_service.get_conversation_history(session_id, limit=self.max_history)
|
||||
|
||||
context = []
|
||||
for msg in messages:
|
||||
context.append({
|
||||
"role": msg.role,
|
||||
"content": msg.content
|
||||
})
|
||||
|
||||
return context
|
||||
|
||||
def add_message(
|
||||
self,
|
||||
session_id: str,
|
||||
role: str,
|
||||
content: str,
|
||||
metadata: Optional[dict] = None
|
||||
):
|
||||
"""
|
||||
添加消息到上下文
|
||||
|
||||
Args:
|
||||
session_id: 会话ID
|
||||
role: 角色(user/assistant)
|
||||
content: 消息内容
|
||||
metadata: 元数据
|
||||
"""
|
||||
db_service.add_message(session_id, role, content, metadata)
|
||||
logger.info(f"添加消息到上下文: {session_id}, {role}")
|
||||
|
||||
def clear_context(self, session_id: str):
|
||||
"""
|
||||
清除上下文(暂不实现删除,保留历史)
|
||||
|
||||
Args:
|
||||
session_id: 会话ID
|
||||
"""
|
||||
logger.info(f"清除上下文请求: {session_id}")
|
||||
# 实际不删除,只是标记
|
||||
pass
|
||||
|
||||
def format_context_for_llm(self, session_id: str) -> str:
|
||||
"""
|
||||
格式化上下文供LLM使用
|
||||
|
||||
Args:
|
||||
session_id: 会话ID
|
||||
|
||||
Returns:
|
||||
格式化的上下文字符串
|
||||
"""
|
||||
context = self.get_context(session_id)
|
||||
|
||||
if not context:
|
||||
return ""
|
||||
|
||||
formatted = []
|
||||
for msg in context:
|
||||
role = "用户" if msg["role"] == "user" else "助手"
|
||||
formatted.append(f"{role}: {msg['content']}")
|
||||
|
||||
return "\n".join(formatted)
|
||||
378
backend/app/agent/core.py
Normal file
378
backend/app/agent/core.py
Normal file
@ -0,0 +1,378 @@
|
||||
"""
|
||||
AI Agent核心
|
||||
基于LangChain的股票分析Agent
|
||||
"""
|
||||
import re
|
||||
import json
|
||||
from typing import Dict, Any, Optional
|
||||
from app.config import get_settings
|
||||
from app.agent.context import ContextManager
|
||||
from app.agent.skill_manager import skill_manager
|
||||
from app.skills.market_data import MarketDataSkill
|
||||
from app.skills.technical_analysis import TechnicalAnalysisSkill
|
||||
from app.skills.fundamental import FundamentalSkill
|
||||
from app.skills.visualization import VisualizationSkill
|
||||
from app.utils.logger import logger
|
||||
|
||||
|
||||
class StockAnalysisAgent:
|
||||
"""股票分析Agent"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化Agent"""
|
||||
self.context_manager = ContextManager()
|
||||
self.settings = get_settings()
|
||||
|
||||
# 注册技能
|
||||
self._register_skills()
|
||||
|
||||
# 初始化LLM(简化版,使用规则匹配)
|
||||
# 在实际部署时,这里应该集成智谱AI GLM-4
|
||||
self.use_llm = bool(self.settings.zhipuai_api_key)
|
||||
|
||||
logger.info("Stock Analysis Agent初始化完成")
|
||||
|
||||
def _register_skills(self):
|
||||
"""注册所有技能"""
|
||||
skill_manager.register(MarketDataSkill())
|
||||
skill_manager.register(TechnicalAnalysisSkill())
|
||||
skill_manager.register(FundamentalSkill())
|
||||
skill_manager.register(VisualizationSkill())
|
||||
logger.info("技能注册完成")
|
||||
|
||||
async def process_message(
|
||||
self,
|
||||
message: str,
|
||||
session_id: str,
|
||||
user_id: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
处理用户消息
|
||||
|
||||
Args:
|
||||
message: 用户消息
|
||||
session_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
响应结果
|
||||
"""
|
||||
logger.info(f"处理消息: {message[:50]}...")
|
||||
|
||||
# 保存用户消息
|
||||
self.context_manager.add_message(session_id, "user", message)
|
||||
|
||||
# 意图识别和技能调用
|
||||
intent = self._recognize_intent(message)
|
||||
logger.info(f"识别意图: {intent}")
|
||||
|
||||
# 执行技能
|
||||
result = await self._execute_intent(intent, message)
|
||||
|
||||
# 生成响应
|
||||
response = self._generate_response(intent, result)
|
||||
|
||||
# 保存助手响应
|
||||
self.context_manager.add_message(
|
||||
session_id,
|
||||
"assistant",
|
||||
response["message"],
|
||||
metadata=response.get("metadata")
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def _recognize_intent(self, message: str) -> Dict[str, Any]:
|
||||
"""
|
||||
识别用户意图(简化版规则匹配)
|
||||
|
||||
Args:
|
||||
message: 用户消息
|
||||
|
||||
Returns:
|
||||
意图字典
|
||||
"""
|
||||
message_lower = message.lower()
|
||||
|
||||
# 提取股票代码
|
||||
stock_code = self._extract_stock_code(message)
|
||||
|
||||
# 行情查询
|
||||
if any(keyword in message_lower for keyword in ["行情", "价格", "涨跌", "实时", "quote"]):
|
||||
return {
|
||||
"type": "market_data",
|
||||
"skill": "market_data",
|
||||
"params": {
|
||||
"stock_code": stock_code,
|
||||
"data_type": "quote"
|
||||
}
|
||||
}
|
||||
|
||||
# K线查询
|
||||
if any(keyword in message_lower for keyword in ["k线", "kline", "走势", "图表"]):
|
||||
return {
|
||||
"type": "visualization",
|
||||
"skill": "visualization",
|
||||
"params": {
|
||||
"stock_code": stock_code,
|
||||
"chart_type": "candlestick"
|
||||
}
|
||||
}
|
||||
|
||||
# 技术分析
|
||||
if any(keyword in message_lower for keyword in ["技术", "指标", "macd", "rsi", "kdj", "均线", "ma"]):
|
||||
return {
|
||||
"type": "technical_analysis",
|
||||
"skill": "technical_analysis",
|
||||
"params": {
|
||||
"stock_code": stock_code,
|
||||
"indicators": ["ma", "macd", "rsi"]
|
||||
}
|
||||
}
|
||||
|
||||
# 基本面
|
||||
if any(keyword in message_lower for keyword in ["基本面", "公司", "行业", "信息"]):
|
||||
return {
|
||||
"type": "fundamental",
|
||||
"skill": "fundamental",
|
||||
"params": {
|
||||
"stock_code": stock_code
|
||||
}
|
||||
}
|
||||
|
||||
# 默认:行情查询
|
||||
if stock_code:
|
||||
return {
|
||||
"type": "market_data",
|
||||
"skill": "market_data",
|
||||
"params": {
|
||||
"stock_code": stock_code,
|
||||
"data_type": "quote"
|
||||
}
|
||||
}
|
||||
|
||||
# 无法识别
|
||||
return {
|
||||
"type": "unknown",
|
||||
"skill": None,
|
||||
"params": {}
|
||||
}
|
||||
|
||||
def _extract_stock_code(self, message: str) -> Optional[str]:
|
||||
"""
|
||||
从消息中提取股票代码
|
||||
|
||||
Args:
|
||||
message: 用户消息
|
||||
|
||||
Returns:
|
||||
股票代码或None
|
||||
"""
|
||||
from app.utils.stock_names import search_stock_by_name
|
||||
|
||||
# 匹配6位数字
|
||||
pattern = r'\b\d{6}\b'
|
||||
matches = re.findall(pattern, message)
|
||||
|
||||
if matches:
|
||||
return matches[0]
|
||||
|
||||
# 使用股票名称数据库搜索
|
||||
# 提取可能的股票名称(2-6个汉字)
|
||||
chinese_pattern = r'[\u4e00-\u9fa5]{2,6}'
|
||||
chinese_words = re.findall(chinese_pattern, message)
|
||||
|
||||
for word in chinese_words:
|
||||
code = search_stock_by_name(word)
|
||||
if code:
|
||||
logger.info(f"识别股票名称: {word} -> {code}")
|
||||
return code
|
||||
|
||||
return None
|
||||
|
||||
async def _execute_intent(self, intent: Dict[str, Any], message: str) -> Dict[str, Any]:
|
||||
"""
|
||||
执行意图对应的技能
|
||||
|
||||
Args:
|
||||
intent: 意图字典
|
||||
message: 原始消息
|
||||
|
||||
Returns:
|
||||
执行结果
|
||||
"""
|
||||
if intent["type"] == "unknown":
|
||||
return {
|
||||
"success": False,
|
||||
"error": "无法理解您的问题,请提供股票代码或明确的查询意图"
|
||||
}
|
||||
|
||||
skill_name = intent["skill"]
|
||||
params = intent["params"]
|
||||
|
||||
if not params.get("stock_code"):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "请提供股票代码(6位数字)"
|
||||
}
|
||||
|
||||
# 执行技能
|
||||
result = await skill_manager.execute_skill(skill_name, **params)
|
||||
|
||||
return result
|
||||
|
||||
def _generate_response(self, intent: Dict[str, Any], result: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
生成响应消息
|
||||
|
||||
Args:
|
||||
intent: 意图
|
||||
result: 执行结果
|
||||
|
||||
Returns:
|
||||
响应字典
|
||||
"""
|
||||
if not result.get("success", True):
|
||||
return {
|
||||
"message": f"抱歉,{result.get('error', '处理失败')}",
|
||||
"metadata": {
|
||||
"type": "error"
|
||||
}
|
||||
}
|
||||
|
||||
data = result.get("data", result)
|
||||
|
||||
# 根据意图类型生成不同响应
|
||||
if intent["type"] == "market_data":
|
||||
return self._format_market_data_response(data)
|
||||
elif intent["type"] == "technical_analysis":
|
||||
return self._format_technical_response(data)
|
||||
elif intent["type"] == "fundamental":
|
||||
return self._format_fundamental_response(data)
|
||||
elif intent["type"] == "visualization":
|
||||
return self._format_visualization_response(data)
|
||||
else:
|
||||
return {
|
||||
"message": "查询完成",
|
||||
"metadata": {
|
||||
"type": "data",
|
||||
"data": data
|
||||
}
|
||||
}
|
||||
|
||||
def _format_market_data_response(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""格式化行情数据响应"""
|
||||
if "error" in data:
|
||||
return {
|
||||
"message": f"查询失败:{data['error']}",
|
||||
"metadata": {"type": "error"}
|
||||
}
|
||||
|
||||
if "kline_data" in data:
|
||||
kline_data = data["kline_data"]
|
||||
message = f"已获取K线数据,共{len(kline_data)}条记录"
|
||||
return {
|
||||
"message": message,
|
||||
"metadata": {
|
||||
"type": "kline",
|
||||
"data": kline_data
|
||||
}
|
||||
}
|
||||
|
||||
# 实时行情
|
||||
message = f"""
|
||||
【{data.get('name', '股票')}】({data.get('ts_code', '')})
|
||||
交易日期:{data.get('trade_date', '')}
|
||||
最新价:{data.get('close', 0):.2f}
|
||||
涨跌额:{data.get('change', 0):.2f}
|
||||
涨跌幅:{data.get('pct_chg', 0):.2f}%
|
||||
开盘价:{data.get('open', 0):.2f}
|
||||
最高价:{data.get('high', 0):.2f}
|
||||
最低价:{data.get('low', 0):.2f}
|
||||
成交量:{data.get('vol', 0):.0f}手
|
||||
成交额:{data.get('amount', 0):.0f}千元
|
||||
""".strip()
|
||||
|
||||
return {
|
||||
"message": message,
|
||||
"metadata": {
|
||||
"type": "quote",
|
||||
"data": data
|
||||
}
|
||||
}
|
||||
|
||||
def _format_technical_response(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""格式化技术分析响应"""
|
||||
if "error" in data:
|
||||
return {
|
||||
"message": f"分析失败:{data['error']}",
|
||||
"metadata": {"type": "error"}
|
||||
}
|
||||
|
||||
indicators = data.get("indicators", {})
|
||||
message_parts = [f"【{data.get('stock_code', '')}】技术指标:\n"]
|
||||
|
||||
if "ma" in indicators:
|
||||
ma = indicators["ma"]
|
||||
message_parts.append(f"均线:MA5={ma.get('ma5')}, MA10={ma.get('ma10')}, MA20={ma.get('ma20')}")
|
||||
|
||||
if "macd" in indicators:
|
||||
macd = indicators["macd"]
|
||||
message_parts.append(f"MACD:DIF={macd.get('dif')}, DEA={macd.get('dea')}, MACD={macd.get('macd')}")
|
||||
|
||||
if "rsi" in indicators:
|
||||
rsi = indicators["rsi"]
|
||||
message_parts.append(f"RSI:RSI6={rsi.get('rsi6')}, RSI12={rsi.get('rsi12')}, RSI24={rsi.get('rsi24')}")
|
||||
|
||||
return {
|
||||
"message": "\n".join(message_parts),
|
||||
"metadata": {
|
||||
"type": "technical",
|
||||
"data": data
|
||||
}
|
||||
}
|
||||
|
||||
def _format_fundamental_response(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""格式化基本面响应"""
|
||||
if "error" in data:
|
||||
return {
|
||||
"message": f"查询失败:{data['error']}",
|
||||
"metadata": {"type": "error"}
|
||||
}
|
||||
|
||||
message = f"""
|
||||
【{data.get('name', '股票')}】基本信息
|
||||
股票代码:{data.get('ts_code', '')}
|
||||
所属地域:{data.get('area', '')}
|
||||
所属行业:{data.get('industry', '')}
|
||||
上市市场:{data.get('market', '')}
|
||||
上市日期:{data.get('list_date', '')}
|
||||
""".strip()
|
||||
|
||||
return {
|
||||
"message": message,
|
||||
"metadata": {
|
||||
"type": "fundamental",
|
||||
"data": data
|
||||
}
|
||||
}
|
||||
|
||||
def _format_visualization_response(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""格式化可视化响应"""
|
||||
if "error" in data:
|
||||
return {
|
||||
"message": f"生成图表失败:{data['error']}",
|
||||
"metadata": {"type": "error"}
|
||||
}
|
||||
|
||||
return {
|
||||
"message": f"已生成{data.get('stock_code', '')}的K线图",
|
||||
"metadata": {
|
||||
"type": "chart",
|
||||
"data": data
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
# 创建全局Agent实例
|
||||
stock_agent = StockAnalysisAgent()
|
||||
377
backend/app/agent/enhanced_agent.py
Normal file
377
backend/app/agent/enhanced_agent.py
Normal file
@ -0,0 +1,377 @@
|
||||
"""
|
||||
增强版Agent - 集成LLM智能分析
|
||||
"""
|
||||
import re
|
||||
import json
|
||||
from typing import Dict, Any, Optional
|
||||
from app.config import get_settings
|
||||
from app.agent.context import ContextManager
|
||||
from app.agent.skill_manager import skill_manager
|
||||
from app.skills.market_data import MarketDataSkill
|
||||
from app.skills.technical_analysis import TechnicalAnalysisSkill
|
||||
from app.skills.fundamental import FundamentalSkill
|
||||
from app.skills.visualization import VisualizationSkill
|
||||
from app.services.llm_service import llm_service
|
||||
from app.utils.logger import logger
|
||||
from app.utils.stock_names import search_stock_by_name, get_stock_name
|
||||
|
||||
|
||||
class EnhancedStockAgent:
|
||||
"""增强版股票分析Agent(集成LLM)"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化Agent"""
|
||||
self.context_manager = ContextManager()
|
||||
self.settings = get_settings()
|
||||
|
||||
# 注册技能
|
||||
self._register_skills()
|
||||
|
||||
# 检查LLM是否可用
|
||||
self.use_llm = bool(self.settings.zhipuai_api_key) and llm_service.client is not None
|
||||
|
||||
if self.use_llm:
|
||||
logger.info("Enhanced Agent初始化完成(LLM模式)")
|
||||
else:
|
||||
logger.info("Enhanced Agent初始化完成(规则模式)")
|
||||
|
||||
def _register_skills(self):
|
||||
"""注册所有技能"""
|
||||
skill_manager.register(MarketDataSkill())
|
||||
skill_manager.register(TechnicalAnalysisSkill())
|
||||
skill_manager.register(FundamentalSkill())
|
||||
skill_manager.register(VisualizationSkill())
|
||||
logger.info("技能注册完成")
|
||||
|
||||
async def process_message(
|
||||
self,
|
||||
message: str,
|
||||
session_id: str,
|
||||
user_id: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
处理用户消息(增强版)
|
||||
|
||||
Args:
|
||||
message: 用户消息
|
||||
session_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
响应结果
|
||||
"""
|
||||
logger.info(f"处理消息: {message[:50]}...")
|
||||
|
||||
# 保存用户消息
|
||||
self.context_manager.add_message(session_id, "user", message)
|
||||
|
||||
# 提取股票代码
|
||||
stock_code = self._extract_stock_code(message)
|
||||
|
||||
# 使用LLM或规则识别意图
|
||||
if self.use_llm:
|
||||
intent = await self._recognize_intent_with_llm(message, stock_code)
|
||||
else:
|
||||
intent = self._recognize_intent_with_rules(message, stock_code)
|
||||
|
||||
logger.info(f"识别意图: {intent}")
|
||||
|
||||
# 执行技能
|
||||
result = await self._execute_intent(intent, message)
|
||||
|
||||
# 生成响应(使用LLM增强)
|
||||
response = await self._generate_response(intent, result, stock_code)
|
||||
|
||||
# 保存助手响应
|
||||
self.context_manager.add_message(
|
||||
session_id,
|
||||
"assistant",
|
||||
response["message"],
|
||||
metadata=response.get("metadata")
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
async def _recognize_intent_with_llm(
|
||||
self,
|
||||
message: str,
|
||||
stock_code: Optional[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""使用LLM识别意图"""
|
||||
try:
|
||||
llm_result = llm_service.analyze_intent(message)
|
||||
|
||||
intent_type = llm_result.get("type", "unknown")
|
||||
confidence = llm_result.get("confidence", 0)
|
||||
|
||||
# 如果置信度太低,回退到规则模式
|
||||
if confidence < 0.5:
|
||||
logger.info("LLM置信度低,回退到规则模式")
|
||||
return self._recognize_intent_with_rules(message, stock_code)
|
||||
|
||||
# 构建意图
|
||||
intent = {
|
||||
"type": intent_type,
|
||||
"confidence": confidence,
|
||||
"skill": self._map_intent_to_skill(intent_type),
|
||||
"params": {"stock_code": stock_code} if stock_code else {}
|
||||
}
|
||||
|
||||
return intent
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"LLM意图识别失败: {e}")
|
||||
return self._recognize_intent_with_rules(message, stock_code)
|
||||
|
||||
def _recognize_intent_with_rules(
|
||||
self,
|
||||
message: str,
|
||||
stock_code: Optional[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""使用规则识别意图(原有逻辑)"""
|
||||
message_lower = message.lower()
|
||||
|
||||
# 行情查询
|
||||
if any(keyword in message_lower for keyword in ["行情", "价格", "涨跌", "实时", "quote"]):
|
||||
return {
|
||||
"type": "market_data",
|
||||
"skill": "market_data",
|
||||
"params": {
|
||||
"stock_code": stock_code,
|
||||
"data_type": "quote"
|
||||
}
|
||||
}
|
||||
|
||||
# K线查询
|
||||
if any(keyword in message_lower for keyword in ["k线", "kline", "走势", "图表"]):
|
||||
return {
|
||||
"type": "visualization",
|
||||
"skill": "visualization",
|
||||
"params": {
|
||||
"stock_code": stock_code,
|
||||
"chart_type": "candlestick"
|
||||
}
|
||||
}
|
||||
|
||||
# 技术分析
|
||||
if any(keyword in message_lower for keyword in ["技术", "指标", "macd", "rsi", "kdj", "均线", "ma"]):
|
||||
return {
|
||||
"type": "technical_analysis",
|
||||
"skill": "technical_analysis",
|
||||
"params": {
|
||||
"stock_code": stock_code,
|
||||
"indicators": ["ma", "macd", "rsi"]
|
||||
}
|
||||
}
|
||||
|
||||
# 基本面
|
||||
if any(keyword in message_lower for keyword in ["基本面", "公司", "行业", "信息"]):
|
||||
return {
|
||||
"type": "fundamental",
|
||||
"skill": "fundamental",
|
||||
"params": {
|
||||
"stock_code": stock_code
|
||||
}
|
||||
}
|
||||
|
||||
# 默认:行情查询
|
||||
if stock_code:
|
||||
return {
|
||||
"type": "market_data",
|
||||
"skill": "market_data",
|
||||
"params": {
|
||||
"stock_code": stock_code,
|
||||
"data_type": "quote"
|
||||
}
|
||||
}
|
||||
|
||||
# 无法识别
|
||||
return {
|
||||
"type": "unknown",
|
||||
"skill": None,
|
||||
"params": {}
|
||||
}
|
||||
|
||||
def _map_intent_to_skill(self, intent_type: str) -> Optional[str]:
|
||||
"""将意图类型映射到技能名称"""
|
||||
mapping = {
|
||||
"market_data": "market_data",
|
||||
"technical_analysis": "technical_analysis",
|
||||
"fundamental": "fundamental",
|
||||
"visualization": "visualization"
|
||||
}
|
||||
return mapping.get(intent_type)
|
||||
|
||||
def _extract_stock_code(self, message: str) -> Optional[str]:
|
||||
"""从消息中提取股票代码"""
|
||||
# 匹配6位数字
|
||||
pattern = r'\b\d{6}\b'
|
||||
matches = re.findall(pattern, message)
|
||||
|
||||
if matches:
|
||||
return matches[0]
|
||||
|
||||
# 使用股票名称数据库搜索
|
||||
chinese_pattern = r'[\u4e00-\u9fa5]{2,6}'
|
||||
chinese_words = re.findall(chinese_pattern, message)
|
||||
|
||||
for word in chinese_words:
|
||||
code = search_stock_by_name(word)
|
||||
if code:
|
||||
logger.info(f"识别股票名称: {word} -> {code}")
|
||||
return code
|
||||
|
||||
return None
|
||||
|
||||
async def _execute_intent(self, intent: Dict[str, Any], message: str) -> Dict[str, Any]:
|
||||
"""执行意图对应的技能"""
|
||||
if intent["type"] == "unknown":
|
||||
return {
|
||||
"success": False,
|
||||
"error": "无法理解您的问题,请提供股票代码或明确的查询意图"
|
||||
}
|
||||
|
||||
skill_name = intent["skill"]
|
||||
params = intent["params"]
|
||||
|
||||
if not params.get("stock_code"):
|
||||
return {
|
||||
"success": False,
|
||||
"error": "请提供股票代码或股票名称"
|
||||
}
|
||||
|
||||
# 执行技能
|
||||
result = await skill_manager.execute_skill(skill_name, **params)
|
||||
return result
|
||||
|
||||
async def _generate_response(
|
||||
self,
|
||||
intent: Dict[str, Any],
|
||||
result: Dict[str, Any],
|
||||
stock_code: Optional[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""生成响应消息(使用LLM增强)"""
|
||||
if not result.get("success", True):
|
||||
return {
|
||||
"message": f"抱歉,{result.get('error', '处理失败')}",
|
||||
"metadata": {"type": "error"}
|
||||
}
|
||||
|
||||
data = result.get("data", result)
|
||||
|
||||
# 基础格式化
|
||||
base_response = self._format_response_basic(intent, data)
|
||||
|
||||
# 如果启用LLM,添加智能分析
|
||||
if self.use_llm and stock_code and intent["type"] == "technical_analysis":
|
||||
try:
|
||||
stock_name = get_stock_name(stock_code) or stock_code
|
||||
llm_summary = llm_service.generate_analysis_summary(
|
||||
stock_code, stock_name, data
|
||||
)
|
||||
base_response["message"] += f"\n\n【AI分析】\n{llm_summary}"
|
||||
except Exception as e:
|
||||
logger.error(f"LLM分析生成失败: {e}")
|
||||
|
||||
return base_response
|
||||
|
||||
def _format_response_basic(self, intent: Dict[str, Any], data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""基础响应格式化(原有逻辑)"""
|
||||
if "error" in data:
|
||||
return {
|
||||
"message": f"查询失败:{data['error']}",
|
||||
"metadata": {"type": "error"}
|
||||
}
|
||||
|
||||
intent_type = intent["type"]
|
||||
|
||||
if intent_type == "market_data":
|
||||
return self._format_market_data(data)
|
||||
elif intent_type == "technical_analysis":
|
||||
return self._format_technical(data)
|
||||
elif intent_type == "fundamental":
|
||||
return self._format_fundamental(data)
|
||||
elif intent_type == "visualization":
|
||||
return self._format_visualization(data)
|
||||
else:
|
||||
return {
|
||||
"message": "查询完成",
|
||||
"metadata": {"type": "data", "data": data}
|
||||
}
|
||||
|
||||
def _format_market_data(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""格式化行情数据"""
|
||||
if "kline_data" in data:
|
||||
kline_data = data["kline_data"]
|
||||
message = f"已获取K线数据,共{len(kline_data)}条记录"
|
||||
return {
|
||||
"message": message,
|
||||
"metadata": {"type": "kline", "data": kline_data}
|
||||
}
|
||||
|
||||
message = f"""
|
||||
【{data.get('name', '股票')}】({data.get('ts_code', '')})
|
||||
交易日期:{data.get('trade_date', '')}
|
||||
最新价:{data.get('close', 0):.2f}
|
||||
涨跌额:{data.get('change', 0):.2f}
|
||||
涨跌幅:{data.get('pct_chg', 0):.2f}%
|
||||
开盘价:{data.get('open', 0):.2f}
|
||||
最高价:{data.get('high', 0):.2f}
|
||||
最低价:{data.get('low', 0):.2f}
|
||||
成交量:{data.get('vol', 0):.0f}手
|
||||
成交额:{data.get('amount', 0):.0f}千元
|
||||
""".strip()
|
||||
|
||||
return {
|
||||
"message": message,
|
||||
"metadata": {"type": "quote", "data": data}
|
||||
}
|
||||
|
||||
def _format_technical(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""格式化技术分析"""
|
||||
indicators = data.get("indicators", {})
|
||||
message_parts = [f"【{data.get('stock_code', '')}】技术指标:\n"]
|
||||
|
||||
if "ma" in indicators:
|
||||
ma = indicators["ma"]
|
||||
message_parts.append(f"均线:MA5={ma.get('ma5')}, MA10={ma.get('ma10')}, MA20={ma.get('ma20')}")
|
||||
|
||||
if "macd" in indicators:
|
||||
macd = indicators["macd"]
|
||||
message_parts.append(f"MACD:DIF={macd.get('dif')}, DEA={macd.get('dea')}, MACD={macd.get('macd')}")
|
||||
|
||||
if "rsi" in indicators:
|
||||
rsi = indicators["rsi"]
|
||||
message_parts.append(f"RSI:RSI6={rsi.get('rsi6')}, RSI12={rsi.get('rsi12')}, RSI24={rsi.get('rsi24')}")
|
||||
|
||||
return {
|
||||
"message": "\n".join(message_parts),
|
||||
"metadata": {"type": "technical", "data": data}
|
||||
}
|
||||
|
||||
def _format_fundamental(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""格式化基本面"""
|
||||
message = f"""
|
||||
【{data.get('name', '股票')}】基本信息
|
||||
股票代码:{data.get('ts_code', '')}
|
||||
所属地域:{data.get('area', '')}
|
||||
所属行业:{data.get('industry', '')}
|
||||
上市市场:{data.get('market', '')}
|
||||
上市日期:{data.get('list_date', '')}
|
||||
""".strip()
|
||||
|
||||
return {
|
||||
"message": message,
|
||||
"metadata": {"type": "fundamental", "data": data}
|
||||
}
|
||||
|
||||
def _format_visualization(self, data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""格式化可视化"""
|
||||
return {
|
||||
"message": f"已生成{data.get('stock_code', '')}的K线图",
|
||||
"metadata": {"type": "chart", "data": data}
|
||||
}
|
||||
|
||||
|
||||
# 创建全局Agent实例
|
||||
enhanced_agent = EnhancedStockAgent()
|
||||
179
backend/app/agent/skill_manager.py
Normal file
179
backend/app/agent/skill_manager.py
Normal file
@ -0,0 +1,179 @@
|
||||
"""
|
||||
技能管理器
|
||||
管理所有技能的注册、发现和调用
|
||||
"""
|
||||
from typing import Dict, Optional, List, Type
|
||||
from app.skills.base import BaseSkill
|
||||
from app.utils.logger import logger
|
||||
|
||||
|
||||
class SkillManager:
|
||||
"""技能管理器"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化技能管理器"""
|
||||
self._skills: Dict[str, BaseSkill] = {}
|
||||
logger.info("技能管理器初始化")
|
||||
|
||||
def register(self, skill: BaseSkill) -> bool:
|
||||
"""
|
||||
注册技能
|
||||
|
||||
Args:
|
||||
skill: 技能实例
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
if not skill.name:
|
||||
logger.error("技能名称不能为空")
|
||||
return False
|
||||
|
||||
if skill.name in self._skills:
|
||||
logger.warning(f"技能已存在,将被覆盖: {skill.name}")
|
||||
|
||||
self._skills[skill.name] = skill
|
||||
logger.info(f"技能注册成功: {skill.name}")
|
||||
return True
|
||||
|
||||
def unregister(self, skill_name: str) -> bool:
|
||||
"""
|
||||
注销技能
|
||||
|
||||
Args:
|
||||
skill_name: 技能名称
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
if skill_name in self._skills:
|
||||
del self._skills[skill_name]
|
||||
logger.info(f"技能注销成功: {skill_name}")
|
||||
return True
|
||||
|
||||
logger.warning(f"技能不存在: {skill_name}")
|
||||
return False
|
||||
|
||||
def get_skill(self, skill_name: str) -> Optional[BaseSkill]:
|
||||
"""
|
||||
获取技能
|
||||
|
||||
Args:
|
||||
skill_name: 技能名称
|
||||
|
||||
Returns:
|
||||
技能实例或None
|
||||
"""
|
||||
return self._skills.get(skill_name)
|
||||
|
||||
def get_all_skills(self) -> List[BaseSkill]:
|
||||
"""
|
||||
获取所有技能
|
||||
|
||||
Returns:
|
||||
技能列表
|
||||
"""
|
||||
return list(self._skills.values())
|
||||
|
||||
def get_enabled_skills(self) -> List[BaseSkill]:
|
||||
"""
|
||||
获取所有启用的技能
|
||||
|
||||
Returns:
|
||||
启用的技能列表
|
||||
"""
|
||||
return [skill for skill in self._skills.values() if skill.enabled]
|
||||
|
||||
async def execute_skill(self, skill_name: str, **kwargs) -> Dict:
|
||||
"""
|
||||
执行技能
|
||||
|
||||
Args:
|
||||
skill_name: 技能名称
|
||||
**kwargs: 技能参数
|
||||
|
||||
Returns:
|
||||
执行结果
|
||||
"""
|
||||
skill = self.get_skill(skill_name)
|
||||
|
||||
if not skill:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"技能不存在: {skill_name}"
|
||||
}
|
||||
|
||||
if not skill.enabled:
|
||||
return {
|
||||
"success": False,
|
||||
"error": f"技能已禁用: {skill_name}"
|
||||
}
|
||||
|
||||
# 验证参数
|
||||
valid, error = skill.validate_params(**kwargs)
|
||||
if not valid:
|
||||
return {
|
||||
"success": False,
|
||||
"error": error
|
||||
}
|
||||
|
||||
# 执行技能
|
||||
try:
|
||||
result = await skill.execute(**kwargs)
|
||||
return {
|
||||
"success": True,
|
||||
"data": result
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"技能执行失败 {skill_name}: {e}")
|
||||
return {
|
||||
"success": False,
|
||||
"error": str(e)
|
||||
}
|
||||
|
||||
def enable_skill(self, skill_name: str) -> bool:
|
||||
"""
|
||||
启用技能
|
||||
|
||||
Args:
|
||||
skill_name: 技能名称
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
skill = self.get_skill(skill_name)
|
||||
if skill:
|
||||
skill.enable()
|
||||
logger.info(f"技能已启用: {skill_name}")
|
||||
return True
|
||||
return False
|
||||
|
||||
def disable_skill(self, skill_name: str) -> bool:
|
||||
"""
|
||||
禁用技能
|
||||
|
||||
Args:
|
||||
skill_name: 技能名称
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
skill = self.get_skill(skill_name)
|
||||
if skill:
|
||||
skill.disable()
|
||||
logger.info(f"技能已禁用: {skill_name}")
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_skills_info(self) -> List[Dict]:
|
||||
"""
|
||||
获取所有技能信息
|
||||
|
||||
Returns:
|
||||
技能信息列表
|
||||
"""
|
||||
return [skill.get_info() for skill in self._skills.values()]
|
||||
|
||||
|
||||
# 创建全局技能管理器实例
|
||||
skill_manager = SkillManager()
|
||||
966
backend/app/agent/smart_agent.py
Normal file
966
backend/app/agent/smart_agent.py
Normal file
@ -0,0 +1,966 @@
|
||||
"""
|
||||
智能Agent - 真正使用LLM进行全面分析
|
||||
"""
|
||||
import re
|
||||
import json
|
||||
from typing import Dict, Any, Optional, List
|
||||
from app.config import get_settings
|
||||
from app.agent.context import ContextManager
|
||||
from app.agent.skill_manager import skill_manager
|
||||
from app.skills.market_data import MarketDataSkill
|
||||
from app.skills.technical_analysis import TechnicalAnalysisSkill
|
||||
from app.skills.fundamental import FundamentalSkill
|
||||
from app.skills.visualization import VisualizationSkill
|
||||
from app.skills.brave_search import BraveSearchSkill
|
||||
from app.services.llm_service import llm_service
|
||||
from app.services.tushare_service import tushare_service
|
||||
from app.utils.logger import logger
|
||||
|
||||
|
||||
class SmartStockAgent:
|
||||
"""智能股票分析Agent - 深度集成LLM"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化Agent"""
|
||||
self.context_manager = ContextManager()
|
||||
self.settings = get_settings()
|
||||
|
||||
# 注册技能
|
||||
self._register_skills()
|
||||
|
||||
# 检查LLM是否可用
|
||||
self.use_llm = bool(self.settings.zhipuai_api_key) and llm_service.client is not None
|
||||
|
||||
if self.use_llm:
|
||||
logger.info("Smart Agent初始化完成(LLM深度集成模式 + Brave搜索)")
|
||||
else:
|
||||
logger.warning("Smart Agent初始化完成(规则模式,建议配置LLM)")
|
||||
|
||||
def _register_skills(self):
|
||||
"""注册所有技能"""
|
||||
skill_manager.register(MarketDataSkill())
|
||||
skill_manager.register(TechnicalAnalysisSkill())
|
||||
skill_manager.register(FundamentalSkill())
|
||||
skill_manager.register(VisualizationSkill())
|
||||
skill_manager.register(BraveSearchSkill())
|
||||
logger.info("技能注册完成(包含Brave搜索)")
|
||||
|
||||
async def process_message(
|
||||
self,
|
||||
message: str,
|
||||
session_id: str,
|
||||
user_id: Optional[str] = None
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
处理用户消息(智能版)
|
||||
|
||||
Args:
|
||||
message: 用户消息
|
||||
session_id: 会话ID
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
响应结果
|
||||
"""
|
||||
logger.info(f"处理消息: {message[:50]}...")
|
||||
|
||||
# 保存用户消息
|
||||
self.context_manager.add_message(session_id, "user", message)
|
||||
|
||||
# 第一步:使用LLM理解问题意图
|
||||
intent_analysis = await self._analyze_question_intent(message)
|
||||
|
||||
if not intent_analysis:
|
||||
response = {
|
||||
"message": "抱歉,我无法理解您的问题。请重新描述您的需求。",
|
||||
"metadata": {"type": "error"}
|
||||
}
|
||||
self.context_manager.add_message(session_id, "assistant", response["message"])
|
||||
return response
|
||||
|
||||
# 第二步:根据意图类型处理
|
||||
question_type = intent_analysis['type']
|
||||
|
||||
if question_type == 'stock_specific':
|
||||
# 针对特定股票的问题
|
||||
response = await self._handle_stock_question(intent_analysis, message)
|
||||
elif question_type == 'macro_finance':
|
||||
# 宏观金融问题
|
||||
response = await self._handle_macro_question(intent_analysis, message)
|
||||
elif question_type == 'knowledge':
|
||||
# 金融知识问答
|
||||
response = await self._handle_knowledge_question(intent_analysis, message)
|
||||
else:
|
||||
response = {
|
||||
"message": "抱歉,我暂时无法处理这类问题。",
|
||||
"metadata": {"type": "error"}
|
||||
}
|
||||
|
||||
# 保存助手响应
|
||||
self.context_manager.add_message(
|
||||
session_id,
|
||||
"assistant",
|
||||
response["message"],
|
||||
metadata=response.get("metadata")
|
||||
)
|
||||
|
||||
return response
|
||||
|
||||
def _is_comprehensive_analysis(self, message: str) -> bool:
|
||||
"""
|
||||
判断是否需要全面分析
|
||||
|
||||
默认情况下,如果用户只是简单提到股票名称或代码,就进行全面分析
|
||||
只有明确要求特定信息时(如"技术指标"、"K线图"等),才做单一查询
|
||||
"""
|
||||
# 明确要求单一查询的关键词
|
||||
single_query_keywords = [
|
||||
"k线", "图表", "走势图", "kline",
|
||||
"技术指标", "macd", "rsi", "均线", "kdj",
|
||||
"基本面", "公司信息", "行业",
|
||||
"实时行情", "价格", "涨跌"
|
||||
]
|
||||
|
||||
# 如果明确要求单一查询,返回False
|
||||
if any(keyword in message.lower() for keyword in single_query_keywords):
|
||||
return False
|
||||
|
||||
# 默认进行全面分析
|
||||
return True
|
||||
|
||||
async def _comprehensive_analysis(
|
||||
self,
|
||||
stock_code: str,
|
||||
stock_name: Optional[str],
|
||||
message: str
|
||||
) -> Dict[str, Any]:
|
||||
"""
|
||||
全面分析:整合多个数据源 + LLM深度分析
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
stock_name: 股票名称
|
||||
message: 用户消息
|
||||
|
||||
Returns:
|
||||
综合分析结果
|
||||
"""
|
||||
logger.info(f"执行全面分析: {stock_code}")
|
||||
|
||||
display_name = stock_name or stock_code
|
||||
|
||||
# 1. 并行获取所有数据
|
||||
try:
|
||||
# 获取实时行情
|
||||
quote_result = await skill_manager.execute_skill(
|
||||
"market_data",
|
||||
stock_code=stock_code,
|
||||
data_type="quote"
|
||||
)
|
||||
|
||||
# 获取技术指标
|
||||
technical_result = await skill_manager.execute_skill(
|
||||
"technical_analysis",
|
||||
stock_code=stock_code,
|
||||
indicators=["ma", "macd", "rsi", "kdj"]
|
||||
)
|
||||
|
||||
# 获取基本面
|
||||
fundamental_result = await skill_manager.execute_skill(
|
||||
"fundamental",
|
||||
stock_code=stock_code
|
||||
)
|
||||
|
||||
# 获取最新新闻(Brave搜索)
|
||||
search_query = f"{display_name} {stock_code} 股票 最新消息"
|
||||
news_result = await skill_manager.execute_skill(
|
||||
"brave_search",
|
||||
query=search_query,
|
||||
search_type="news",
|
||||
count=5,
|
||||
freshness="pw" # 过去一周
|
||||
)
|
||||
|
||||
# 整合数据
|
||||
all_data = {
|
||||
"stock_code": stock_code,
|
||||
"stock_name": display_name,
|
||||
"quote": quote_result.get("data") if quote_result.get("success") else None,
|
||||
"technical": technical_result.get("data") if technical_result.get("success") else None,
|
||||
"fundamental": fundamental_result.get("data") if fundamental_result.get("success") else None,
|
||||
"news": news_result.get("results") if news_result and not news_result.get("error") else None
|
||||
}
|
||||
|
||||
# 2. 使用LLM进行深度分析
|
||||
if self.use_llm:
|
||||
analysis = await self._llm_comprehensive_analysis(all_data, message)
|
||||
else:
|
||||
analysis = self._rule_based_analysis(all_data)
|
||||
|
||||
return {
|
||||
"message": analysis,
|
||||
"metadata": {
|
||||
"type": "comprehensive",
|
||||
"data": all_data
|
||||
}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"全面分析失败: {e}")
|
||||
return {
|
||||
"message": f"分析{display_name}时出错:{str(e)}",
|
||||
"metadata": {"type": "error"}
|
||||
}
|
||||
|
||||
async def _llm_comprehensive_analysis(
|
||||
self,
|
||||
data: Dict[str, Any],
|
||||
user_message: str
|
||||
) -> str:
|
||||
"""使用LLM进行深度综合分析"""
|
||||
|
||||
# 获取当前时间
|
||||
from datetime import datetime
|
||||
current_time = datetime.now().strftime("%Y-%m-%d %H:%M")
|
||||
|
||||
# 获取行情数据的交易日期
|
||||
quote_date = "未知"
|
||||
if data.get('quote') and data['quote'].get('trade_date'):
|
||||
quote_date = data['quote']['trade_date']
|
||||
|
||||
# 构建新闻摘要
|
||||
news_summary = ""
|
||||
news_source_info = ""
|
||||
if data.get('news'):
|
||||
news_summary = "\n【消息面分析】\n"
|
||||
news_summary += f"数据来源:Brave Search API\n"
|
||||
news_summary += f"搜索时间:{current_time}\n"
|
||||
news_summary += f"新闻范围:过去一周内相关新闻\n\n"
|
||||
for idx, news_item in enumerate(data['news'][:5], 1):
|
||||
news_summary += f"{idx}. {news_item.get('title', '无标题')}\n"
|
||||
news_summary += f" 来源: {news_item.get('source', '未知')}\n"
|
||||
news_summary += f" 摘要: {news_item.get('description', '无描述')}\n"
|
||||
news_summary += f" 发布时间: {news_item.get('published', '未知')}\n\n"
|
||||
news_source_info = "(消息来源:Brave搜索引擎,数据可能存在延迟)"
|
||||
else:
|
||||
news_summary = "\n【消息面分析】\n暂无最新新闻数据\n"
|
||||
|
||||
# 构建详细的分析提示
|
||||
prompt = f"""你是一位专业的股票分析师。请对{data['stock_name']}({data['stock_code']})进行全面分析,用简洁专业但易懂的语言回答。
|
||||
|
||||
用户问题:{user_message}
|
||||
|
||||
【实时行情数据】
|
||||
数据来源:Tushare Pro API
|
||||
交易日期:{quote_date}
|
||||
{json.dumps(data.get('quote'), ensure_ascii=False, indent=2) if data.get('quote') else '数据获取失败'}
|
||||
|
||||
【技术指标数据】
|
||||
数据来源:Tushare Pro API(基于历史K线数据计算)
|
||||
计算截止日期:{quote_date}
|
||||
{json.dumps(data.get('technical'), ensure_ascii=False, indent=2) if data.get('technical') else '数据获取失败'}
|
||||
|
||||
【基本面数据】
|
||||
数据来源:Tushare Pro API
|
||||
{json.dumps(data.get('fundamental'), ensure_ascii=False, indent=2) if data.get('fundamental') else '数据获取失败'}
|
||||
{news_summary}
|
||||
|
||||
请按以下结构进行分析,并在每个部分明确标注数据来源和时效性:
|
||||
|
||||
## 一、基本面分析
|
||||
分段说明公司情况,每个要点独立成段:
|
||||
- 第一段:公司主营业务和行业地位
|
||||
- 第二段:所属行业发展前景
|
||||
- 第三段:如果有新闻,简要分析对公司的影响{news_source_info}
|
||||
|
||||
## 二、技术面分析(数据截止:{quote_date})
|
||||
使用清晰的分段结构,每个技术指标独立成段:
|
||||
|
||||
**价格走势**
|
||||
当前价格走势特征(上涨/下跌/震荡),结合成交量分析。
|
||||
|
||||
**均线系统**
|
||||
短期均线(MA5、MA10)与长期均线(MA20、MA60)的位置关系,判断当前趋势(多头/空头/震荡)。
|
||||
|
||||
**MACD指标**
|
||||
DIF和DEA的位置关系,MACD柱状图变化,判断动能强弱和买卖信号。
|
||||
|
||||
**RSI指标**
|
||||
当前RSI值的位置,是否超买(>70)或超卖(<30),短期走势预判。
|
||||
|
||||
**支撑与压力**
|
||||
关键支撑位和压力位的具体价格区间。
|
||||
|
||||
## 三、市场情绪分析
|
||||
分段分析市场情绪:
|
||||
- 第一段:当前市场情绪(乐观/谨慎/悲观)及原因
|
||||
- 第二段:如果有新闻,分析是利好还是利空
|
||||
- 第三段:短期可能的催化因素
|
||||
|
||||
## 四、投资建议
|
||||
清晰分段,每个时间维度独立:
|
||||
|
||||
**短期(1-2周)**
|
||||
明确的操作建议(买入/持有/观望/减仓)及理由。
|
||||
|
||||
**中期(1-3个月)**
|
||||
趋势判断和策略建议。
|
||||
|
||||
**长期(半年以上)**
|
||||
投资价值评估。
|
||||
|
||||
**风险提示**
|
||||
主要风险点和注意事项。
|
||||
|
||||
## 五、总结
|
||||
用一句话概括核心观点。
|
||||
|
||||
---
|
||||
**数据说明**
|
||||
- 行情数据来源:Tushare Pro(截止{quote_date})
|
||||
- 技术指标:基于历史K线数据计算(截止{quote_date})
|
||||
- 新闻数据:Brave搜索(搜索时间{current_time},范围:过去一周)
|
||||
|
||||
写作要求:
|
||||
1. 语言简洁专业,避免过度修饰和比喻
|
||||
2. 专业术语后用括号简单解释,例如"RSI超买(指标>70,股价可能回调)"
|
||||
3. **重要:每个分析点必须独立成段,段落之间用空行分隔**
|
||||
4. **技术面分析部分,每个指标必须使用加粗标题(**标题**)并独立成段**
|
||||
5. 分析要客观理性,基于数据而非情绪
|
||||
3. 分析要客观理性,基于数据而非情绪
|
||||
4. 结论要明确,不要模棱两可
|
||||
5. 控制在500-600字
|
||||
6. 最后必须声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。"
|
||||
"""
|
||||
|
||||
try:
|
||||
analysis = llm_service.chat(
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
temperature=0.7,
|
||||
max_tokens=2000
|
||||
)
|
||||
|
||||
if analysis:
|
||||
return f"【{data['stock_name']}({data['stock_code']}) - AI深度分析】\n\n{analysis}"
|
||||
else:
|
||||
return self._rule_based_analysis(data)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"LLM分析失败: {e}")
|
||||
return self._rule_based_analysis(data)
|
||||
|
||||
async def _llm_single_analysis(
|
||||
self,
|
||||
intent: Dict[str, Any],
|
||||
result: Dict[str, Any],
|
||||
stock_code: str,
|
||||
stock_name: Optional[str],
|
||||
user_message: str
|
||||
) -> Dict[str, Any]:
|
||||
"""使用LLM对单一查询进行分析"""
|
||||
data = result.get("data", result)
|
||||
display_name = stock_name or stock_code
|
||||
|
||||
# 根据查询类型构建不同的prompt
|
||||
if intent["type"] == "technical":
|
||||
prompt = f"""你是一位专业的股票分析师。用户询问了{display_name}({stock_code})的技术指标。
|
||||
|
||||
用户问题:{user_message}
|
||||
|
||||
【技术指标数据】
|
||||
{json.dumps(data, ensure_ascii=False, indent=2)}
|
||||
|
||||
请进行专业的技术分析:
|
||||
|
||||
## 技术指标解读
|
||||
1. 均线系统分析:
|
||||
- 短期均线(MA5、MA10)与长期均线(MA20、MA60)的位置关系
|
||||
- 判断当前趋势(多头/空头/震荡)
|
||||
|
||||
2. MACD指标分析:
|
||||
- DIF和DEA的位置关系
|
||||
- MACD柱状图的变化趋势
|
||||
- 判断动能强弱
|
||||
|
||||
3. RSI指标分析:
|
||||
- 当前RSI值的位置(超买/超卖/中性)
|
||||
- 短期可能的走势
|
||||
|
||||
4. KDJ指标分析(如有):
|
||||
- K、D、J值的位置关系
|
||||
- 金叉/死叉信号
|
||||
|
||||
## 综合判断
|
||||
- 短期走势预判(1-2周)
|
||||
- 关键支撑位和压力位
|
||||
- 操作建议(买入/持有/观望/减仓)
|
||||
|
||||
## 风险提示
|
||||
- 主要技术风险点
|
||||
|
||||
写作要求:
|
||||
1. 语言简洁专业,直接给出分析结论
|
||||
2. 基于数据进行分析,不要编造
|
||||
3. 控制在300-400字
|
||||
4. 最后声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。"
|
||||
"""
|
||||
|
||||
elif intent["type"] == "quote":
|
||||
prompt = f"""你是一位专业的股票分析师。用户询问了{display_name}({stock_code})的实时行情。
|
||||
|
||||
用户问题:{user_message}
|
||||
|
||||
【实时行情数据】
|
||||
{json.dumps(data, ensure_ascii=False, indent=2)}
|
||||
|
||||
请进行专业的行情分析:
|
||||
|
||||
## 行情解读
|
||||
1. 当日表现:
|
||||
- 涨跌幅分析
|
||||
- 成交量分析
|
||||
- 振幅分析
|
||||
|
||||
2. 价格位置:
|
||||
- 当前价格相对开盘价、最高价、最低价的位置
|
||||
- 判断多空力量对比
|
||||
|
||||
3. 短期判断:
|
||||
- 当日走势特征
|
||||
- 短期可能的走势
|
||||
- 操作建议
|
||||
|
||||
写作要求:
|
||||
1. 语言简洁专业,直接给出分析结论
|
||||
2. 基于数据进行分析,不要编造
|
||||
3. 控制在200-300字
|
||||
4. 最后声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。"
|
||||
"""
|
||||
|
||||
elif intent["type"] == "fundamental":
|
||||
prompt = f"""你是一位专业的股票分析师。用户询问了{display_name}({stock_code})的基本面信息。
|
||||
|
||||
用户问题:{user_message}
|
||||
|
||||
【基本面数据】
|
||||
{json.dumps(data, ensure_ascii=False, indent=2)}
|
||||
|
||||
请进行专业的基本面分析:
|
||||
|
||||
## 公司概况
|
||||
- 公司主营业务
|
||||
- 所属行业和地域
|
||||
- 上市时间和市场
|
||||
|
||||
## 行业分析
|
||||
- 所属行业的发展前景
|
||||
- 行业地位和竞争优势
|
||||
|
||||
## 投资价值
|
||||
- 基本面评估
|
||||
- 长期投资价值
|
||||
- 关注要点
|
||||
|
||||
写作要求:
|
||||
1. 语言简洁专业,直接给出分析结论
|
||||
2. 基于数据进行分析,不要编造
|
||||
3. 控制在200-300字
|
||||
4. 最后声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。"
|
||||
"""
|
||||
|
||||
else:
|
||||
# 其他类型,使用通用分析
|
||||
prompt = f"""你是一位专业的股票分析师。用户询问了{display_name}({stock_code})的相关信息。
|
||||
|
||||
用户问题:{user_message}
|
||||
|
||||
【数据】
|
||||
{json.dumps(data, ensure_ascii=False, indent=2)}
|
||||
|
||||
请基于提供的数据进行专业分析,给出有价值的见解和建议。
|
||||
|
||||
写作要求:
|
||||
1. 语言简洁专业,直接给出分析结论
|
||||
2. 基于数据进行分析,不要编造
|
||||
3. 控制在200-300字
|
||||
4. 最后声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。"
|
||||
"""
|
||||
|
||||
try:
|
||||
analysis = llm_service.chat(
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
temperature=0.7,
|
||||
max_tokens=1500
|
||||
)
|
||||
|
||||
if analysis:
|
||||
return {
|
||||
"message": f"【{display_name}({stock_code}) - AI分析】\n\n{analysis}",
|
||||
"metadata": {"type": intent["type"], "data": data}
|
||||
}
|
||||
else:
|
||||
# LLM失败,使用原始格式化
|
||||
return self._format_response(intent, result, stock_code, stock_name)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"LLM单一分析失败: {e}")
|
||||
return self._format_response(intent, result, stock_code, stock_name)
|
||||
|
||||
def _rule_based_analysis(self, data: Dict[str, Any]) -> str:
|
||||
"""基于规则的分析(LLM不可用时的备选方案)"""
|
||||
parts = [f"【{data['stock_name']}({data['stock_code']}) - 综合分析】\n"]
|
||||
|
||||
# 行情信息
|
||||
if data.get('quote'):
|
||||
quote = data['quote']
|
||||
parts.append("## 一、实时行情")
|
||||
parts.append(f"最新价:{quote.get('close', 0):.2f}元")
|
||||
parts.append(f"涨跌幅:{quote.get('pct_chg', 0):.2f}%")
|
||||
parts.append(f"成交量:{quote.get('vol', 0):.0f}手")
|
||||
parts.append("")
|
||||
|
||||
# 技术分析
|
||||
if data.get('technical'):
|
||||
tech = data['technical'].get('indicators', {})
|
||||
parts.append("## 二、技术指标")
|
||||
|
||||
if 'ma' in tech:
|
||||
ma = tech['ma']
|
||||
parts.append(f"均线系统:MA5={ma.get('ma5')}, MA10={ma.get('ma10')}, MA20={ma.get('ma20')}")
|
||||
|
||||
if 'macd' in tech:
|
||||
macd = tech['macd']
|
||||
parts.append(f"MACD:DIF={macd.get('dif')}, DEA={macd.get('dea')}")
|
||||
|
||||
if 'rsi' in tech:
|
||||
rsi = tech['rsi']
|
||||
rsi6 = rsi.get('rsi6', 50)
|
||||
if rsi6 > 70:
|
||||
parts.append(f"RSI:{rsi6:.1f}(超买区域,注意回调风险)")
|
||||
elif rsi6 < 30:
|
||||
parts.append(f"RSI:{rsi6:.1f}(超卖区域,可能存在反弹机会)")
|
||||
else:
|
||||
parts.append(f"RSI:{rsi6:.1f}(中性区域)")
|
||||
|
||||
parts.append("")
|
||||
|
||||
# 基本面
|
||||
if data.get('fundamental'):
|
||||
fund = data['fundamental']
|
||||
parts.append("## 三、基本信息")
|
||||
parts.append(f"所属行业:{fund.get('industry', '未知')}")
|
||||
parts.append(f"上市日期:{fund.get('list_date', '未知')}")
|
||||
parts.append("")
|
||||
|
||||
# 简单建议
|
||||
parts.append("## 四、参考建议")
|
||||
parts.append("建议结合更多信息进行综合判断。")
|
||||
parts.append("")
|
||||
parts.append("⚠️ 以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。")
|
||||
|
||||
return "\n".join(parts)
|
||||
|
||||
async def _single_query(
|
||||
self,
|
||||
stock_code: str,
|
||||
stock_name: Optional[str],
|
||||
message: str
|
||||
) -> Dict[str, Any]:
|
||||
"""单一查询处理 - 使用LLM进行分析"""
|
||||
# 识别意图
|
||||
intent = self._recognize_intent(message, stock_code)
|
||||
|
||||
# 执行技能
|
||||
result = await skill_manager.execute_skill(
|
||||
intent["skill"],
|
||||
**intent["params"]
|
||||
)
|
||||
|
||||
# 格式化响应
|
||||
if not result.get("success", True):
|
||||
return {
|
||||
"message": f"查询失败:{result.get('error', '未知错误')}",
|
||||
"metadata": {"type": "error"}
|
||||
}
|
||||
|
||||
# 所有查询都使用LLM进行分析(除了可视化)
|
||||
if intent["type"] != "visualization" and self.use_llm:
|
||||
return await self._llm_single_analysis(intent, result, stock_code, stock_name, message)
|
||||
else:
|
||||
return self._format_response(intent, result, stock_code, stock_name)
|
||||
|
||||
def _recognize_intent(self, message: str, stock_code: str) -> Dict[str, Any]:
|
||||
"""识别查询意图"""
|
||||
message_lower = message.lower()
|
||||
|
||||
# K线图
|
||||
if any(kw in message_lower for kw in ["k线", "图表", "走势图", "kline"]):
|
||||
return {
|
||||
"type": "visualization",
|
||||
"skill": "visualization",
|
||||
"params": {"stock_code": stock_code}
|
||||
}
|
||||
|
||||
# 技术分析
|
||||
if any(kw in message_lower for kw in ["技术", "指标", "macd", "rsi", "均线"]):
|
||||
return {
|
||||
"type": "technical",
|
||||
"skill": "technical_analysis",
|
||||
"params": {"stock_code": stock_code, "indicators": ["ma", "macd", "rsi"]}
|
||||
}
|
||||
|
||||
# 基本面
|
||||
if any(kw in message_lower for kw in ["基本面", "公司", "行业", "信息"]):
|
||||
return {
|
||||
"type": "fundamental",
|
||||
"skill": "fundamental",
|
||||
"params": {"stock_code": stock_code}
|
||||
}
|
||||
|
||||
# 默认:实时行情
|
||||
return {
|
||||
"type": "quote",
|
||||
"skill": "market_data",
|
||||
"params": {"stock_code": stock_code, "data_type": "quote"}
|
||||
}
|
||||
|
||||
def _format_response(
|
||||
self,
|
||||
intent: Dict[str, Any],
|
||||
result: Dict[str, Any],
|
||||
stock_code: str,
|
||||
stock_name: Optional[str]
|
||||
) -> Dict[str, Any]:
|
||||
"""格式化响应"""
|
||||
data = result.get("data", result)
|
||||
display_name = stock_name or stock_code
|
||||
|
||||
if intent["type"] == "quote":
|
||||
message = f"""【{display_name}】实时行情
|
||||
|
||||
交易日期:{data.get('trade_date', '')}
|
||||
最新价:{data.get('close', 0):.2f}元
|
||||
涨跌幅:{data.get('pct_chg', 0):+.2f}%
|
||||
涨跌额:{data.get('change', 0):+.2f}元
|
||||
开盘价:{data.get('open', 0):.2f}元
|
||||
最高价:{data.get('high', 0):.2f}元
|
||||
最低价:{data.get('low', 0):.2f}元
|
||||
成交量:{data.get('vol', 0):.0f}手
|
||||
成交额:{data.get('amount', 0):.0f}千元"""
|
||||
|
||||
return {
|
||||
"message": message,
|
||||
"metadata": {"type": "quote", "data": data}
|
||||
}
|
||||
|
||||
elif intent["type"] == "technical":
|
||||
indicators = data.get("indicators", {})
|
||||
parts = [f"【{display_name}】技术指标\n"]
|
||||
|
||||
if "ma" in indicators:
|
||||
ma = indicators["ma"]
|
||||
parts.append(f"均线:MA5={ma.get('ma5')}, MA10={ma.get('ma10')}, MA20={ma.get('ma20')}")
|
||||
|
||||
if "macd" in indicators:
|
||||
macd = indicators["macd"]
|
||||
parts.append(f"MACD:DIF={macd.get('dif')}, DEA={macd.get('dea')}, MACD={macd.get('macd')}")
|
||||
|
||||
if "rsi" in indicators:
|
||||
rsi = indicators["rsi"]
|
||||
parts.append(f"RSI:RSI6={rsi.get('rsi6')}, RSI12={rsi.get('rsi12')}")
|
||||
|
||||
return {
|
||||
"message": "\n".join(parts),
|
||||
"metadata": {"type": "technical", "data": data}
|
||||
}
|
||||
|
||||
elif intent["type"] == "visualization":
|
||||
return {
|
||||
"message": f"已生成【{display_name}】的K线图",
|
||||
"metadata": {"type": "chart", "data": data}
|
||||
}
|
||||
|
||||
elif intent["type"] == "fundamental":
|
||||
message = f"""【{display_name}】基本信息
|
||||
|
||||
股票代码:{data.get('ts_code', '')}
|
||||
所属地域:{data.get('area', '')}
|
||||
所属行业:{data.get('industry', '')}
|
||||
上市市场:{data.get('market', '')}
|
||||
上市日期:{data.get('list_date', '')}"""
|
||||
|
||||
return {
|
||||
"message": message,
|
||||
"metadata": {"type": "fundamental", "data": data}
|
||||
}
|
||||
|
||||
return {
|
||||
"message": "查询完成",
|
||||
"metadata": {"type": "data", "data": data}
|
||||
}
|
||||
|
||||
async def _analyze_question_intent(self, message: str) -> Optional[Dict[str, Any]]:
|
||||
"""
|
||||
使用LLM分析问题意图
|
||||
|
||||
Args:
|
||||
message: 用户消息
|
||||
|
||||
Returns:
|
||||
意图分析结果: {
|
||||
'type': 'stock_specific' | 'macro_finance' | 'knowledge',
|
||||
'description': '问题描述',
|
||||
'keywords': ['关键词列表'],
|
||||
'stock_names': ['股票名称'] (如果是stock_specific类型)
|
||||
}
|
||||
"""
|
||||
if not self.use_llm:
|
||||
logger.warning("LLM未配置,无法分析意图")
|
||||
return None
|
||||
|
||||
prompt = f"""分析用户的金融问题,判断问题类型和关键信息。
|
||||
|
||||
用户问题:{message}
|
||||
|
||||
请分析这个问题属于以下哪一类:
|
||||
|
||||
1. **stock_specific** - 针对特定股票的问题
|
||||
例如:"贵州茅台怎么样"、"分析一下比亚迪"、"600519的技术指标"
|
||||
|
||||
2. **macro_finance** - 宏观金融问题(不针对特定股票)
|
||||
例如:"现在A股市场怎么样"、"最近有什么投资机会"、"如何看待当前经济形势"
|
||||
|
||||
3. **knowledge** - 金融知识问答
|
||||
例如:"什么是MACD"、"如何看K线图"、"价值投资是什么"
|
||||
|
||||
请以JSON格式返回分析结果:
|
||||
{{
|
||||
"type": "问题类型",
|
||||
"description": "问题的简要描述",
|
||||
"keywords": ["关键词1", "关键词2"],
|
||||
"stock_names": ["股票名称"] (仅当type为stock_specific时)
|
||||
}}
|
||||
|
||||
只返回JSON,不要有任何其他内容。"""
|
||||
|
||||
try:
|
||||
result = llm_service.chat(
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
temperature=0.3,
|
||||
max_tokens=300
|
||||
)
|
||||
|
||||
if not result:
|
||||
logger.warning("LLM返回空结果")
|
||||
return None
|
||||
|
||||
# 清理结果,移除可能的markdown代码块标记
|
||||
result = result.strip()
|
||||
if result.startswith("```json"):
|
||||
result = result[7:]
|
||||
if result.startswith("```"):
|
||||
result = result[3:]
|
||||
if result.endswith("```"):
|
||||
result = result[:-3]
|
||||
result = result.strip()
|
||||
|
||||
# 检查是否为空
|
||||
if not result:
|
||||
logger.warning("LLM返回内容为空")
|
||||
return None
|
||||
|
||||
# 解析JSON
|
||||
intent = json.loads(result)
|
||||
logger.info(f"意图分析结果: {intent}")
|
||||
return intent
|
||||
|
||||
except json.JSONDecodeError as e:
|
||||
logger.error(f"意图分析JSON解析失败: {e}, 原始响应: {result[:200] if result else 'None'}")
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"意图分析失败: {e}")
|
||||
return None
|
||||
|
||||
async def _handle_stock_question(
|
||||
self,
|
||||
intent_analysis: Dict[str, Any],
|
||||
message: str
|
||||
) -> Dict[str, Any]:
|
||||
"""处理针对特定股票的问题"""
|
||||
stock_names = intent_analysis.get('stock_names', [])
|
||||
|
||||
if not stock_names:
|
||||
return {
|
||||
"message": "抱歉,我没有识别到您提到的股票。请提供更明确的股票代码或名称。",
|
||||
"metadata": {"type": "error"}
|
||||
}
|
||||
|
||||
# 提取第一个股票(暂时只处理单只股票)
|
||||
stock_keyword = stock_names[0]
|
||||
|
||||
# 使用Tushare搜索股票
|
||||
search_results = tushare_service.search_stock(stock_keyword)
|
||||
|
||||
if not search_results:
|
||||
return {
|
||||
"message": f"抱歉,未找到股票\"{stock_keyword}\"。请确认股票名称或代码是否正确。",
|
||||
"metadata": {"type": "error"}
|
||||
}
|
||||
|
||||
stock = search_results[0]
|
||||
stock_code = stock['symbol']
|
||||
stock_name = stock['name']
|
||||
|
||||
logger.info(f"处理股票问题: {stock_name}({stock_code})")
|
||||
|
||||
# 判断是否需要全面分析
|
||||
is_comprehensive = self._is_comprehensive_analysis(message)
|
||||
|
||||
if is_comprehensive:
|
||||
return await self._comprehensive_analysis(stock_code, stock_name, message)
|
||||
else:
|
||||
return await self._single_query(stock_code, stock_name, message)
|
||||
|
||||
async def _handle_macro_question(
|
||||
self,
|
||||
intent_analysis: Dict[str, Any],
|
||||
message: str
|
||||
) -> Dict[str, Any]:
|
||||
"""处理宏观金融问题"""
|
||||
keywords = intent_analysis.get('keywords', [])
|
||||
description = intent_analysis.get('description', '')
|
||||
|
||||
logger.info(f"处理宏观问题: {description}")
|
||||
|
||||
# 使用Brave搜索获取最新信息
|
||||
search_query = f"A股市场 {' '.join(keywords)} 最新分析"
|
||||
|
||||
try:
|
||||
news_result = await skill_manager.execute_skill(
|
||||
"brave_search",
|
||||
query=search_query,
|
||||
search_type="news",
|
||||
count=5,
|
||||
freshness="pw"
|
||||
)
|
||||
|
||||
# 构建新闻摘要
|
||||
news_summary = ""
|
||||
if news_result and not news_result.get("error"):
|
||||
results = news_result.get("results", [])
|
||||
if results:
|
||||
news_summary = "\n【最新市场动态】\n"
|
||||
for idx, news_item in enumerate(results[:5], 1):
|
||||
news_summary += f"{idx}. {news_item.get('title', '无标题')}\n"
|
||||
news_summary += f" 来源: {news_item.get('source', '未知')}\n"
|
||||
news_summary += f" 时间: {news_item.get('published', '未知')}\n\n"
|
||||
|
||||
# 使用LLM进行分析
|
||||
prompt = f"""你是一位专业的金融分析师。用户询问了宏观金融问题。
|
||||
|
||||
用户问题:{message}
|
||||
|
||||
问题分析:{description}
|
||||
关键词:{', '.join(keywords)}
|
||||
{news_summary}
|
||||
|
||||
请基于当前市场情况和最新动态,给出专业的分析和建议:
|
||||
|
||||
## 市场现状分析
|
||||
- 当前市场整体情况
|
||||
- 主要影响因素
|
||||
|
||||
## 趋势判断
|
||||
- 短期趋势
|
||||
- 中长期展望
|
||||
|
||||
## 投资建议
|
||||
- 投资策略建议
|
||||
- 风险提示
|
||||
|
||||
写作要求:
|
||||
1. 语言简洁专业,避免过度修饰
|
||||
2. 分析要客观理性,基于事实
|
||||
3. 控制在400-500字
|
||||
4. 最后声明:"以上分析仅供参考,不构成投资建议。股市有风险,投资需谨慎。"
|
||||
"""
|
||||
|
||||
analysis = llm_service.chat(
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
temperature=0.7,
|
||||
max_tokens=1500
|
||||
)
|
||||
|
||||
if analysis:
|
||||
return {
|
||||
"message": f"【宏观市场分析】\n\n{analysis}",
|
||||
"metadata": {"type": "macro_analysis"}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"宏观问题处理失败: {e}")
|
||||
|
||||
return {
|
||||
"message": "抱歉,暂时无法获取相关信息。请稍后再试。",
|
||||
"metadata": {"type": "error"}
|
||||
}
|
||||
|
||||
async def _handle_knowledge_question(
|
||||
self,
|
||||
intent_analysis: Dict[str, Any],
|
||||
message: str
|
||||
) -> Dict[str, Any]:
|
||||
"""处理金融知识问答"""
|
||||
description = intent_analysis.get('description', '')
|
||||
keywords = intent_analysis.get('keywords', [])
|
||||
|
||||
logger.info(f"处理知识问答: {description}")
|
||||
|
||||
# 直接使用LLM回答
|
||||
prompt = f"""你是一位专业的金融教育专家。用户询问了金融知识问题。
|
||||
|
||||
用户问题:{message}
|
||||
|
||||
请用通俗易懂的语言解释这个概念或回答这个问题:
|
||||
|
||||
## 核心概念
|
||||
- 清晰定义和解释
|
||||
|
||||
## 实际应用
|
||||
- 如何在投资中应用
|
||||
- 注意事项
|
||||
|
||||
## 举例说明
|
||||
- 用简单的例子帮助理解
|
||||
|
||||
写作要求:
|
||||
1. 语言通俗易懂,避免过多专业术语
|
||||
2. 如果使用专业术语,要简单解释
|
||||
3. 控制在300-400字
|
||||
4. 重点是帮助用户理解,而不是炫耀知识
|
||||
"""
|
||||
|
||||
try:
|
||||
answer = llm_service.chat(
|
||||
messages=[{"role": "user", "content": prompt}],
|
||||
temperature=0.7,
|
||||
max_tokens=1200
|
||||
)
|
||||
|
||||
if answer:
|
||||
return {
|
||||
"message": f"【金融知识解答】\n\n{answer}",
|
||||
"metadata": {"type": "knowledge"}
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"知识问答处理失败: {e}")
|
||||
|
||||
return {
|
||||
"message": "抱歉,暂时无法回答您的问题。请稍后再试。",
|
||||
"metadata": {"type": "error"}
|
||||
}
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
smart_agent = SmartStockAgent()
|
||||
0
backend/app/api/__init__.py
Normal file
0
backend/app/api/__init__.py
Normal file
67
backend/app/api/chat.py
Normal file
67
backend/app/api/chat.py
Normal file
@ -0,0 +1,67 @@
|
||||
"""
|
||||
对话API路由
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from typing import Optional
|
||||
import uuid
|
||||
from app.models.chat import ChatRequest, ChatResponse
|
||||
from app.agent.smart_agent import smart_agent # 使用智能Agent
|
||||
from app.utils.logger import logger
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.post("/message", response_model=ChatResponse)
|
||||
async def send_message(request: ChatRequest):
|
||||
"""
|
||||
发送消息给Agent
|
||||
|
||||
Args:
|
||||
request: 聊天请求
|
||||
|
||||
Returns:
|
||||
Agent响应
|
||||
"""
|
||||
try:
|
||||
# 生成或使用现有session_id
|
||||
session_id = request.session_id or str(uuid.uuid4())
|
||||
|
||||
# 处理消息(使用智能Agent)
|
||||
response = await smart_agent.process_message(
|
||||
message=request.message,
|
||||
session_id=session_id,
|
||||
user_id=request.user_id
|
||||
)
|
||||
|
||||
return ChatResponse(
|
||||
message=response["message"],
|
||||
session_id=session_id,
|
||||
metadata=response.get("metadata")
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"处理消息失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/history/{session_id}")
|
||||
async def get_history(session_id: str, limit: int = 50):
|
||||
"""
|
||||
获取对话历史
|
||||
|
||||
Args:
|
||||
session_id: 会话ID
|
||||
limit: 最大消息数
|
||||
|
||||
Returns:
|
||||
对话历史
|
||||
"""
|
||||
try:
|
||||
context = smart_agent.context_manager.get_context(session_id)
|
||||
return {
|
||||
"session_id": session_id,
|
||||
"messages": context
|
||||
}
|
||||
except Exception as e:
|
||||
logger.error(f"获取历史失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
99
backend/app/api/skills.py
Normal file
99
backend/app/api/skills.py
Normal file
@ -0,0 +1,99 @@
|
||||
"""
|
||||
技能管理API路由
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from pydantic import BaseModel
|
||||
from app.agent.skill_manager import skill_manager
|
||||
from app.utils.logger import logger
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ToggleRequest(BaseModel):
|
||||
enabled: bool
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_skills():
|
||||
"""
|
||||
获取所有技能列表
|
||||
|
||||
Returns:
|
||||
技能信息列表
|
||||
"""
|
||||
try:
|
||||
skills_info = skill_manager.get_skills_info()
|
||||
return skills_info # 直接返回数组
|
||||
except Exception as e:
|
||||
logger.error(f"获取技能列表失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{skill_name}/toggle")
|
||||
async def toggle_skill(skill_name: str, request: ToggleRequest):
|
||||
"""
|
||||
切换技能状态
|
||||
|
||||
Args:
|
||||
skill_name: 技能名称
|
||||
request: 包含enabled字段的请求体
|
||||
|
||||
Returns:
|
||||
操作结果
|
||||
"""
|
||||
try:
|
||||
if request.enabled:
|
||||
success = skill_manager.enable_skill(skill_name)
|
||||
message = f"技能 {skill_name} 已启用"
|
||||
else:
|
||||
success = skill_manager.disable_skill(skill_name)
|
||||
message = f"技能 {skill_name} 已禁用"
|
||||
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="技能不存在")
|
||||
return {"message": message, "success": True}
|
||||
except Exception as e:
|
||||
logger.error(f"切换技能失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{skill_name}/enable")
|
||||
async def enable_skill(skill_name: str):
|
||||
"""
|
||||
启用技能
|
||||
|
||||
Args:
|
||||
skill_name: 技能名称
|
||||
|
||||
Returns:
|
||||
操作结果
|
||||
"""
|
||||
try:
|
||||
success = skill_manager.enable_skill(skill_name)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="技能不存在")
|
||||
return {"message": f"技能 {skill_name} 已启用"}
|
||||
except Exception as e:
|
||||
logger.error(f"启用技能失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/{skill_name}/disable")
|
||||
async def disable_skill(skill_name: str):
|
||||
"""
|
||||
禁用技能
|
||||
|
||||
Args:
|
||||
skill_name: 技能名称
|
||||
|
||||
Returns:
|
||||
操作结果
|
||||
"""
|
||||
try:
|
||||
success = skill_manager.disable_skill(skill_name)
|
||||
if not success:
|
||||
raise HTTPException(status_code=404, detail="技能不存在")
|
||||
return {"message": f"技能 {skill_name} 已禁用"}
|
||||
except Exception as e:
|
||||
logger.error(f"禁用技能失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
80
backend/app/api/stock.py
Normal file
80
backend/app/api/stock.py
Normal file
@ -0,0 +1,80 @@
|
||||
"""
|
||||
股票数据API路由
|
||||
"""
|
||||
from fastapi import APIRouter, HTTPException, Query
|
||||
from typing import Optional
|
||||
from app.services.tushare_service import tushare_service
|
||||
from app.utils.logger import logger
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
@router.get("/quote/{stock_code}")
|
||||
async def get_quote(stock_code: str):
|
||||
"""
|
||||
获取股票实时行情
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
|
||||
Returns:
|
||||
行情数据
|
||||
"""
|
||||
try:
|
||||
quote = tushare_service.get_realtime_quote(stock_code)
|
||||
if not quote:
|
||||
raise HTTPException(status_code=404, detail="未找到股票数据")
|
||||
return quote
|
||||
except Exception as e:
|
||||
logger.error(f"获取行情失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/kline/{stock_code}")
|
||||
async def get_kline(
|
||||
stock_code: str,
|
||||
start_date: Optional[str] = Query(None, description="开始日期YYYYMMDD"),
|
||||
end_date: Optional[str] = Query(None, description="结束日期YYYYMMDD"),
|
||||
period: str = Query("D", description="周期D/W/M")
|
||||
):
|
||||
"""
|
||||
获取K线数据
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
start_date: 开始日期
|
||||
end_date: 结束日期
|
||||
period: 周期
|
||||
|
||||
Returns:
|
||||
K线数据
|
||||
"""
|
||||
try:
|
||||
kline = tushare_service.get_kline_data(stock_code, start_date, end_date, period)
|
||||
if not kline:
|
||||
raise HTTPException(status_code=404, detail="未找到K线数据")
|
||||
return {"kline_data": kline}
|
||||
except Exception as e:
|
||||
logger.error(f"获取K线失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.get("/basic/{stock_code}")
|
||||
async def get_basic(stock_code: str):
|
||||
"""
|
||||
获取股票基本信息
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
|
||||
Returns:
|
||||
基本信息
|
||||
"""
|
||||
try:
|
||||
basic = tushare_service.get_stock_basic(stock_code)
|
||||
if not basic:
|
||||
raise HTTPException(status_code=404, detail="未找到股票信息")
|
||||
return basic
|
||||
except Exception as e:
|
||||
logger.error(f"获取基本信息失败: {e}")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
69
backend/app/config.py
Normal file
69
backend/app/config.py
Normal file
@ -0,0 +1,69 @@
|
||||
"""
|
||||
配置管理模块
|
||||
从环境变量加载配置
|
||||
"""
|
||||
import os
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
from pydantic_settings import BaseSettings
|
||||
from functools import lru_cache
|
||||
|
||||
|
||||
# 查找.env文件的位置
|
||||
def find_env_file():
|
||||
"""查找.env文件,支持从backend目录或项目根目录启动"""
|
||||
current_dir = Path.cwd()
|
||||
|
||||
# 尝试当前目录
|
||||
env_path = current_dir / ".env"
|
||||
if env_path.exists():
|
||||
return str(env_path)
|
||||
|
||||
# 尝试父目录(项目根目录)
|
||||
env_path = current_dir.parent / ".env"
|
||||
if env_path.exists():
|
||||
return str(env_path)
|
||||
|
||||
# 尝试backend的父目录
|
||||
if current_dir.name == "backend":
|
||||
env_path = current_dir.parent / ".env"
|
||||
if env_path.exists():
|
||||
return str(env_path)
|
||||
|
||||
# 默认返回当前目录的.env
|
||||
return ".env"
|
||||
|
||||
|
||||
class Settings(BaseSettings):
|
||||
"""应用配置"""
|
||||
|
||||
# Tushare配置
|
||||
tushare_token: str = ""
|
||||
|
||||
# 智谱AI配置
|
||||
zhipuai_api_key: str = ""
|
||||
|
||||
# 数据库配置
|
||||
database_url: str = "sqlite:///./stock_agent.db"
|
||||
|
||||
# API配置
|
||||
api_host: str = "0.0.0.0"
|
||||
api_port: int = 8000
|
||||
debug: bool = True
|
||||
|
||||
# 安全配置
|
||||
secret_key: str = "change-this-secret-key-in-production"
|
||||
rate_limit: str = "100/minute"
|
||||
|
||||
# CORS配置
|
||||
cors_origins: str = "http://localhost:8000,http://127.0.0.1:8000"
|
||||
|
||||
class Config:
|
||||
env_file = find_env_file()
|
||||
case_sensitive = False
|
||||
|
||||
|
||||
@lru_cache()
|
||||
def get_settings() -> Settings:
|
||||
"""获取配置单例"""
|
||||
return Settings()
|
||||
70
backend/app/main.py
Normal file
70
backend/app/main.py
Normal file
@ -0,0 +1,70 @@
|
||||
"""
|
||||
FastAPI主应用
|
||||
"""
|
||||
from fastapi import FastAPI
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.staticfiles import StaticFiles
|
||||
from fastapi.responses import FileResponse
|
||||
from app.config import get_settings
|
||||
from app.utils.logger import logger
|
||||
from app.api import chat, stock, skills
|
||||
import os
|
||||
|
||||
# 创建FastAPI应用
|
||||
app = FastAPI(
|
||||
title="A股AI分析Agent系统",
|
||||
description="基于AI Agent的股票智能分析系统",
|
||||
version="1.0.0"
|
||||
)
|
||||
|
||||
# 配置CORS
|
||||
settings = get_settings()
|
||||
app.add_middleware(
|
||||
CORSMiddleware,
|
||||
allow_origins=settings.cors_origins.split(","),
|
||||
allow_credentials=True,
|
||||
allow_methods=["*"],
|
||||
allow_headers=["*"],
|
||||
)
|
||||
|
||||
# 注册路由
|
||||
app.include_router(chat.router, prefix="/api/chat", tags=["对话"])
|
||||
app.include_router(stock.router, prefix="/api/stock", tags=["股票数据"])
|
||||
app.include_router(skills.router, prefix="/api/skills", tags=["技能管理"])
|
||||
|
||||
# 挂载静态文件
|
||||
frontend_path = os.path.join(os.path.dirname(os.path.dirname(os.path.dirname(__file__))), "frontend")
|
||||
if os.path.exists(frontend_path):
|
||||
app.mount("/static", StaticFiles(directory=frontend_path), name="static")
|
||||
|
||||
@app.get("/")
|
||||
async def root():
|
||||
"""根路径,返回前端页面"""
|
||||
index_path = os.path.join(frontend_path, "index.html")
|
||||
if os.path.exists(index_path):
|
||||
return FileResponse(index_path)
|
||||
return {"message": "A股AI分析Agent系统API"}
|
||||
|
||||
@app.get("/health")
|
||||
async def health_check():
|
||||
"""健康检查"""
|
||||
return {"status": "healthy"}
|
||||
|
||||
@app.on_event("startup")
|
||||
async def startup_event():
|
||||
"""启动事件"""
|
||||
logger.info("应用启动")
|
||||
|
||||
@app.on_event("shutdown")
|
||||
async def shutdown_event():
|
||||
"""关闭事件"""
|
||||
logger.info("应用关闭")
|
||||
|
||||
if __name__ == "__main__":
|
||||
import uvicorn
|
||||
uvicorn.run(
|
||||
"app.main:app",
|
||||
host=settings.api_host,
|
||||
port=settings.api_port,
|
||||
reload=settings.debug
|
||||
)
|
||||
0
backend/app/models/__init__.py
Normal file
0
backend/app/models/__init__.py
Normal file
37
backend/app/models/chat.py
Normal file
37
backend/app/models/chat.py
Normal file
@ -0,0 +1,37 @@
|
||||
"""
|
||||
对话相关的Pydantic模型
|
||||
"""
|
||||
from datetime import datetime
|
||||
from typing import Optional, Dict, Any
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class ChatMessage(BaseModel):
|
||||
"""聊天消息"""
|
||||
role: str = Field(..., description="角色:user或assistant")
|
||||
content: str = Field(..., description="消息内容")
|
||||
metadata: Optional[Dict[str, Any]] = Field(None, description="元数据")
|
||||
|
||||
|
||||
class ChatRequest(BaseModel):
|
||||
"""聊天请求"""
|
||||
message: str = Field(..., description="用户消息", min_length=1)
|
||||
session_id: Optional[str] = Field(None, description="会话ID")
|
||||
user_id: Optional[str] = Field(None, description="用户ID")
|
||||
|
||||
|
||||
class ChatResponse(BaseModel):
|
||||
"""聊天响应"""
|
||||
message: str = Field(..., description="助手回复")
|
||||
session_id: str = Field(..., description="会话ID")
|
||||
metadata: Optional[Dict[str, Any]] = Field(None, description="元数据")
|
||||
|
||||
|
||||
class ConversationHistory(BaseModel):
|
||||
"""对话历史"""
|
||||
session_id: str
|
||||
messages: list[ChatMessage]
|
||||
created_at: datetime
|
||||
|
||||
class Config:
|
||||
from_attributes = True
|
||||
47
backend/app/models/database.py
Normal file
47
backend/app/models/database.py
Normal file
@ -0,0 +1,47 @@
|
||||
"""
|
||||
数据库模型定义
|
||||
"""
|
||||
from datetime import datetime
|
||||
from sqlalchemy import Column, Integer, String, Text, DateTime, ForeignKey, JSON
|
||||
from sqlalchemy.ext.declarative import declarative_base
|
||||
from sqlalchemy.orm import relationship
|
||||
|
||||
Base = declarative_base()
|
||||
|
||||
|
||||
class Conversation(Base):
|
||||
"""对话记录表"""
|
||||
__tablename__ = "conversations"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
session_id = Column(String(64), nullable=False, index=True)
|
||||
user_id = Column(String(64), nullable=True)
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# 关联消息
|
||||
messages = relationship("Message", back_populates="conversation", cascade="all, delete-orphan")
|
||||
|
||||
|
||||
class Message(Base):
|
||||
"""消息记录表"""
|
||||
__tablename__ = "messages"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
conversation_id = Column(Integer, ForeignKey("conversations.id"), nullable=False)
|
||||
role = Column(String(20), nullable=False) # 'user' or 'assistant'
|
||||
content = Column(Text, nullable=False)
|
||||
msg_metadata = Column(JSON, nullable=True) # 改名避免与SQLAlchemy保留字冲突
|
||||
created_at = Column(DateTime, default=datetime.utcnow)
|
||||
|
||||
# 关联对话
|
||||
conversation = relationship("Conversation", back_populates="messages")
|
||||
|
||||
|
||||
class UserPreference(Base):
|
||||
"""用户偏好表"""
|
||||
__tablename__ = "user_preferences"
|
||||
|
||||
id = Column(Integer, primary_key=True, index=True)
|
||||
user_id = Column(String(64), unique=True, nullable=False, index=True)
|
||||
preferences = Column(JSON, nullable=True)
|
||||
updated_at = Column(DateTime, default=datetime.utcnow, onupdate=datetime.utcnow)
|
||||
48
backend/app/models/stock.py
Normal file
48
backend/app/models/stock.py
Normal file
@ -0,0 +1,48 @@
|
||||
"""
|
||||
股票相关的Pydantic模型
|
||||
"""
|
||||
from datetime import date
|
||||
from typing import Optional, List
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class StockQuote(BaseModel):
|
||||
"""股票行情"""
|
||||
ts_code: str = Field(..., description="股票代码")
|
||||
name: Optional[str] = Field(None, description="股票名称")
|
||||
trade_date: Optional[str] = Field(None, description="交易日期")
|
||||
open: Optional[float] = Field(None, description="开盘价")
|
||||
high: Optional[float] = Field(None, description="最高价")
|
||||
low: Optional[float] = Field(None, description="最低价")
|
||||
close: Optional[float] = Field(None, description="收盘价")
|
||||
pre_close: Optional[float] = Field(None, description="昨收价")
|
||||
change: Optional[float] = Field(None, description="涨跌额")
|
||||
pct_chg: Optional[float] = Field(None, description="涨跌幅%")
|
||||
vol: Optional[float] = Field(None, description="成交量(手)")
|
||||
amount: Optional[float] = Field(None, description="成交额(千元)")
|
||||
|
||||
|
||||
class KLineData(BaseModel):
|
||||
"""K线数据"""
|
||||
ts_code: str = Field(..., description="股票代码")
|
||||
trade_date: str = Field(..., description="交易日期")
|
||||
open: float = Field(..., description="开盘价")
|
||||
high: float = Field(..., description="最高价")
|
||||
low: float = Field(..., description="最低价")
|
||||
close: float = Field(..., description="收盘价")
|
||||
vol: float = Field(..., description="成交量")
|
||||
amount: Optional[float] = Field(None, description="成交额")
|
||||
|
||||
|
||||
class TechnicalIndicators(BaseModel):
|
||||
"""技术指标"""
|
||||
ma5: Optional[List[float]] = Field(None, description="5日均线")
|
||||
ma10: Optional[List[float]] = Field(None, description="10日均线")
|
||||
ma20: Optional[List[float]] = Field(None, description="20日均线")
|
||||
macd_dif: Optional[List[float]] = Field(None, description="MACD DIF")
|
||||
macd_dea: Optional[List[float]] = Field(None, description="MACD DEA")
|
||||
macd: Optional[List[float]] = Field(None, description="MACD柱")
|
||||
rsi: Optional[List[float]] = Field(None, description="RSI")
|
||||
kdj_k: Optional[List[float]] = Field(None, description="KDJ K值")
|
||||
kdj_d: Optional[List[float]] = Field(None, description="KDJ D值")
|
||||
kdj_j: Optional[List[float]] = Field(None, description="KDJ J值")
|
||||
0
backend/app/services/__init__.py
Normal file
0
backend/app/services/__init__.py
Normal file
144
backend/app/services/cache_service.py
Normal file
144
backend/app/services/cache_service.py
Normal file
@ -0,0 +1,144 @@
|
||||
"""
|
||||
缓存服务
|
||||
提供数据缓存功能(使用内存缓存)
|
||||
"""
|
||||
import time
|
||||
from typing import Optional, Any, Dict
|
||||
from app.utils.logger import logger
|
||||
|
||||
|
||||
class CacheService:
|
||||
"""内存缓存服务类"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化内存缓存"""
|
||||
self._cache: Dict[str, tuple[Any, float]] = {} # key: (value, expire_time)
|
||||
logger.info("内存缓存初始化成功")
|
||||
|
||||
def get(self, key: str) -> Optional[Any]:
|
||||
"""
|
||||
获取缓存数据
|
||||
|
||||
Args:
|
||||
key: 缓存键
|
||||
|
||||
Returns:
|
||||
缓存的数据,不存在或过期返回None
|
||||
"""
|
||||
try:
|
||||
if key in self._cache:
|
||||
value, expire_time = self._cache[key]
|
||||
# 检查是否过期
|
||||
if time.time() < expire_time:
|
||||
return value
|
||||
else:
|
||||
# 删除过期数据
|
||||
del self._cache[key]
|
||||
return None
|
||||
except Exception as e:
|
||||
logger.error(f"获取缓存失败: {e}")
|
||||
return None
|
||||
|
||||
def set(self, key: str, value: Any, ttl: int = 3600) -> bool:
|
||||
"""
|
||||
设置缓存数据
|
||||
|
||||
Args:
|
||||
key: 缓存键
|
||||
value: 要缓存的数据
|
||||
ttl: 过期时间(秒)
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
try:
|
||||
expire_time = time.time() + ttl
|
||||
self._cache[key] = (value, expire_time)
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"设置缓存失败: {e}")
|
||||
return False
|
||||
|
||||
def delete(self, key: str) -> bool:
|
||||
"""
|
||||
删除缓存
|
||||
|
||||
Args:
|
||||
key: 缓存键
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
try:
|
||||
if key in self._cache:
|
||||
del self._cache[key]
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"删除缓存失败: {e}")
|
||||
return False
|
||||
|
||||
def exists(self, key: str) -> bool:
|
||||
"""
|
||||
检查缓存是否存在
|
||||
|
||||
Args:
|
||||
key: 缓存键
|
||||
|
||||
Returns:
|
||||
是否存在
|
||||
"""
|
||||
try:
|
||||
if key in self._cache:
|
||||
_, expire_time = self._cache[key]
|
||||
if time.time() < expire_time:
|
||||
return True
|
||||
else:
|
||||
del self._cache[key]
|
||||
return False
|
||||
except Exception as e:
|
||||
logger.error(f"检查缓存失败: {e}")
|
||||
return False
|
||||
|
||||
def clear_pattern(self, pattern: str) -> int:
|
||||
"""
|
||||
清除匹配模式的所有缓存
|
||||
|
||||
Args:
|
||||
pattern: 键模式(如 "stock:*")
|
||||
|
||||
Returns:
|
||||
删除的键数量
|
||||
"""
|
||||
try:
|
||||
# 简单的模式匹配(支持*通配符)
|
||||
pattern = pattern.replace('*', '')
|
||||
keys_to_delete = [k for k in self._cache.keys() if pattern in k]
|
||||
|
||||
for key in keys_to_delete:
|
||||
del self._cache[key]
|
||||
|
||||
return len(keys_to_delete)
|
||||
except Exception as e:
|
||||
logger.error(f"清除缓存失败: {e}")
|
||||
return 0
|
||||
|
||||
def clear_expired(self):
|
||||
"""清除所有过期的缓存"""
|
||||
try:
|
||||
current_time = time.time()
|
||||
expired_keys = [
|
||||
key for key, (_, expire_time) in self._cache.items()
|
||||
if current_time >= expire_time
|
||||
]
|
||||
|
||||
for key in expired_keys:
|
||||
del self._cache[key]
|
||||
|
||||
if expired_keys:
|
||||
logger.info(f"清除了{len(expired_keys)}个过期缓存")
|
||||
except Exception as e:
|
||||
logger.error(f"清除过期缓存失败: {e}")
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
cache_service = CacheService()
|
||||
210
backend/app/services/db_service.py
Normal file
210
backend/app/services/db_service.py
Normal file
@ -0,0 +1,210 @@
|
||||
"""
|
||||
数据库服务
|
||||
提供数据库操作功能
|
||||
"""
|
||||
from sqlalchemy import create_engine
|
||||
from sqlalchemy.orm import sessionmaker, Session
|
||||
from typing import Optional, List
|
||||
from datetime import datetime
|
||||
import uuid
|
||||
|
||||
from app.config import get_settings
|
||||
from app.models.database import Base, Conversation, Message, UserPreference
|
||||
from app.utils.logger import logger
|
||||
|
||||
|
||||
class DatabaseService:
|
||||
"""数据库服务类"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化数据库连接"""
|
||||
settings = get_settings()
|
||||
self.engine = create_engine(
|
||||
settings.database_url,
|
||||
connect_args={"check_same_thread": False} if "sqlite" in settings.database_url else {}
|
||||
)
|
||||
self.SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=self.engine)
|
||||
|
||||
# 创建表
|
||||
Base.metadata.create_all(bind=self.engine)
|
||||
logger.info("数据库初始化成功")
|
||||
|
||||
def get_session(self) -> Session:
|
||||
"""获取数据库会话"""
|
||||
return self.SessionLocal()
|
||||
|
||||
def create_conversation(self, session_id: Optional[str] = None, user_id: Optional[str] = None) -> Conversation:
|
||||
"""
|
||||
创建新对话
|
||||
|
||||
Args:
|
||||
session_id: 会话ID(可选,自动生成)
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
对话对象
|
||||
"""
|
||||
db = self.get_session()
|
||||
try:
|
||||
if not session_id:
|
||||
session_id = str(uuid.uuid4())
|
||||
|
||||
conversation = Conversation(
|
||||
session_id=session_id,
|
||||
user_id=user_id
|
||||
)
|
||||
db.add(conversation)
|
||||
db.commit()
|
||||
db.refresh(conversation)
|
||||
return conversation
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def get_conversation(self, session_id: str) -> Optional[Conversation]:
|
||||
"""
|
||||
获取对话
|
||||
|
||||
Args:
|
||||
session_id: 会话ID
|
||||
|
||||
Returns:
|
||||
对话对象或None
|
||||
"""
|
||||
db = self.get_session()
|
||||
try:
|
||||
return db.query(Conversation).filter(Conversation.session_id == session_id).first()
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def add_message(
|
||||
self,
|
||||
session_id: str,
|
||||
role: str,
|
||||
content: str,
|
||||
metadata: Optional[dict] = None
|
||||
) -> Message:
|
||||
"""
|
||||
添加消息
|
||||
|
||||
Args:
|
||||
session_id: 会话ID
|
||||
role: 角色(user/assistant)
|
||||
content: 消息内容
|
||||
metadata: 元数据
|
||||
|
||||
Returns:
|
||||
消息对象
|
||||
"""
|
||||
db = self.get_session()
|
||||
try:
|
||||
# 获取或创建对话
|
||||
conversation = db.query(Conversation).filter(
|
||||
Conversation.session_id == session_id
|
||||
).first()
|
||||
|
||||
if not conversation:
|
||||
conversation = Conversation(session_id=session_id)
|
||||
db.add(conversation)
|
||||
db.commit()
|
||||
db.refresh(conversation)
|
||||
|
||||
# 创建消息
|
||||
message = Message(
|
||||
conversation_id=conversation.id,
|
||||
role=role,
|
||||
content=content,
|
||||
msg_metadata=metadata
|
||||
)
|
||||
db.add(message)
|
||||
db.commit()
|
||||
db.refresh(message)
|
||||
return message
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def get_conversation_history(self, session_id: str, limit: int = 50) -> List[Message]:
|
||||
"""
|
||||
获取对话历史
|
||||
|
||||
Args:
|
||||
session_id: 会话ID
|
||||
limit: 最大消息数
|
||||
|
||||
Returns:
|
||||
消息列表
|
||||
"""
|
||||
db = self.get_session()
|
||||
try:
|
||||
conversation = db.query(Conversation).filter(
|
||||
Conversation.session_id == session_id
|
||||
).first()
|
||||
|
||||
if not conversation:
|
||||
return []
|
||||
|
||||
messages = db.query(Message).filter(
|
||||
Message.conversation_id == conversation.id
|
||||
).order_by(Message.created_at.desc()).limit(limit).all()
|
||||
|
||||
return list(reversed(messages))
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def get_user_preference(self, user_id: str) -> Optional[dict]:
|
||||
"""
|
||||
获取用户偏好
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
|
||||
Returns:
|
||||
偏好字典或None
|
||||
"""
|
||||
db = self.get_session()
|
||||
try:
|
||||
pref = db.query(UserPreference).filter(
|
||||
UserPreference.user_id == user_id
|
||||
).first()
|
||||
return pref.preferences if pref else None
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
def set_user_preference(self, user_id: str, preferences: dict) -> bool:
|
||||
"""
|
||||
设置用户偏好
|
||||
|
||||
Args:
|
||||
user_id: 用户ID
|
||||
preferences: 偏好字典
|
||||
|
||||
Returns:
|
||||
是否成功
|
||||
"""
|
||||
db = self.get_session()
|
||||
try:
|
||||
pref = db.query(UserPreference).filter(
|
||||
UserPreference.user_id == user_id
|
||||
).first()
|
||||
|
||||
if pref:
|
||||
pref.preferences = preferences
|
||||
pref.updated_at = datetime.utcnow()
|
||||
else:
|
||||
pref = UserPreference(
|
||||
user_id=user_id,
|
||||
preferences=preferences
|
||||
)
|
||||
db.add(pref)
|
||||
|
||||
db.commit()
|
||||
return True
|
||||
except Exception as e:
|
||||
logger.error(f"设置用户偏好失败: {e}")
|
||||
db.rollback()
|
||||
return False
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
db_service = DatabaseService()
|
||||
175
backend/app/services/llm_service.py
Normal file
175
backend/app/services/llm_service.py
Normal file
@ -0,0 +1,175 @@
|
||||
"""
|
||||
LLM服务 - 智谱AI GLM-4集成
|
||||
"""
|
||||
from typing import Optional, List, Dict, Any
|
||||
from app.config import get_settings
|
||||
from app.utils.logger import logger
|
||||
|
||||
try:
|
||||
from zhipuai import ZhipuAI
|
||||
ZHIPUAI_AVAILABLE = True
|
||||
except ImportError:
|
||||
ZHIPUAI_AVAILABLE = False
|
||||
logger.warning("zhipuai包未安装,LLM功能将不可用")
|
||||
|
||||
|
||||
class LLMService:
|
||||
"""LLM服务类"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化LLM服务"""
|
||||
settings = get_settings()
|
||||
|
||||
if not ZHIPUAI_AVAILABLE:
|
||||
logger.warning("智谱AI SDK未安装")
|
||||
self.client = None
|
||||
return
|
||||
|
||||
if not settings.zhipuai_api_key:
|
||||
logger.warning("智谱AI API Key未配置")
|
||||
self.client = None
|
||||
return
|
||||
|
||||
try:
|
||||
self.client = ZhipuAI(api_key=settings.zhipuai_api_key)
|
||||
logger.info("智谱AI LLM服务初始化成功")
|
||||
except Exception as e:
|
||||
logger.error(f"智谱AI初始化失败: {e}")
|
||||
self.client = None
|
||||
|
||||
def chat(
|
||||
self,
|
||||
messages: List[Dict[str, str]],
|
||||
model: str = "glm-4",
|
||||
temperature: float = 0.7,
|
||||
max_tokens: int = 2000
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
调用LLM进行对话
|
||||
|
||||
Args:
|
||||
messages: 消息列表 [{"role": "user", "content": "..."}]
|
||||
model: 模型名称
|
||||
temperature: 温度参数
|
||||
max_tokens: 最大token数
|
||||
|
||||
Returns:
|
||||
LLM响应文本
|
||||
"""
|
||||
if not self.client:
|
||||
logger.error("LLM客户端未初始化")
|
||||
return None
|
||||
|
||||
try:
|
||||
logger.info(f"调用LLM: model={model}, messages={len(messages)}条")
|
||||
response = self.client.chat.completions.create(
|
||||
model=model,
|
||||
messages=messages,
|
||||
temperature=temperature,
|
||||
max_tokens=max_tokens
|
||||
)
|
||||
|
||||
if response.choices:
|
||||
content = response.choices[0].message.content
|
||||
logger.info(f"LLM响应成功,长度: {len(content) if content else 0}")
|
||||
return content
|
||||
else:
|
||||
logger.warning("LLM响应中没有choices")
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"LLM调用失败: {type(e).__name__}: {e}")
|
||||
import traceback
|
||||
logger.error(f"详细错误: {traceback.format_exc()}")
|
||||
return None
|
||||
|
||||
def analyze_intent(self, user_message: str) -> Dict[str, Any]:
|
||||
"""
|
||||
使用LLM分析用户意图
|
||||
|
||||
Args:
|
||||
user_message: 用户消息
|
||||
|
||||
Returns:
|
||||
意图分析结果
|
||||
"""
|
||||
if not self.client:
|
||||
return {"type": "unknown", "confidence": 0}
|
||||
|
||||
prompt = f"""你是一个股票分析助手的意图识别模块。请分析用户的查询意图。
|
||||
|
||||
用户消息:{user_message}
|
||||
|
||||
请识别以下意图类型之一:
|
||||
1. market_data - 查询实时行情、价格
|
||||
2. technical_analysis - 技术分析、技术指标
|
||||
3. fundamental - 基本面信息、公司信息
|
||||
4. visualization - K线图、图表
|
||||
5. unknown - 无法识别
|
||||
|
||||
请以JSON格式返回:
|
||||
{{
|
||||
"type": "意图类型",
|
||||
"confidence": 0.0-1.0,
|
||||
"stock_name": "提取的股票名称(如果有)"
|
||||
}}
|
||||
"""
|
||||
|
||||
try:
|
||||
response = self.chat([{"role": "user", "content": prompt}], temperature=0.3)
|
||||
if response:
|
||||
import json
|
||||
# 尝试解析JSON
|
||||
result = json.loads(response)
|
||||
return result
|
||||
except Exception as e:
|
||||
logger.error(f"意图分析失败: {e}")
|
||||
|
||||
return {"type": "unknown", "confidence": 0}
|
||||
|
||||
def generate_analysis_summary(
|
||||
self,
|
||||
stock_code: str,
|
||||
stock_name: str,
|
||||
data: Dict[str, Any]
|
||||
) -> str:
|
||||
"""
|
||||
使用LLM生成分析总结
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
stock_name: 股票名称
|
||||
data: 分析数据
|
||||
|
||||
Returns:
|
||||
分析总结文本
|
||||
"""
|
||||
if not self.client:
|
||||
return "LLM服务不可用,无法生成智能分析"
|
||||
|
||||
prompt = f"""你是一个专业的股票分析师。请根据以下数据对{stock_name}({stock_code})进行分析总结。
|
||||
|
||||
数据:
|
||||
{data}
|
||||
|
||||
请提供:
|
||||
1. 当前状态评估
|
||||
2. 技术指标解读
|
||||
3. 投资建议(仅供参考)
|
||||
|
||||
注意:
|
||||
- 使用专业但易懂的语言
|
||||
- 控制在200字以内
|
||||
- 必须声明"仅供参考,不构成投资建议"
|
||||
"""
|
||||
|
||||
try:
|
||||
response = self.chat([{"role": "user", "content": prompt}], temperature=0.7)
|
||||
return response or "分析生成失败"
|
||||
except Exception as e:
|
||||
logger.error(f"分析总结生成失败: {e}")
|
||||
return "分析生成失败"
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
llm_service = LLMService()
|
||||
263
backend/app/services/tushare_service.py
Normal file
263
backend/app/services/tushare_service.py
Normal file
@ -0,0 +1,263 @@
|
||||
"""
|
||||
Tushare数据服务
|
||||
封装Tushare API调用
|
||||
"""
|
||||
import tushare as ts
|
||||
import pandas as pd
|
||||
from typing import Optional, List
|
||||
from datetime import datetime, timedelta
|
||||
from app.config import get_settings
|
||||
from app.utils.logger import logger
|
||||
from app.utils.validators import normalize_stock_code
|
||||
|
||||
|
||||
class TushareService:
|
||||
"""Tushare数据服务类"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化Tushare服务"""
|
||||
settings = get_settings()
|
||||
if not settings.tushare_token:
|
||||
logger.warning("Tushare token未配置")
|
||||
self.pro = None
|
||||
else:
|
||||
ts.set_token(settings.tushare_token)
|
||||
self.pro = ts.pro_api()
|
||||
logger.info("Tushare服务初始化成功")
|
||||
|
||||
def get_realtime_quote(self, stock_code: str) -> Optional[dict]:
|
||||
"""
|
||||
获取实时行情
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
|
||||
Returns:
|
||||
行情数据字典
|
||||
"""
|
||||
if not self.pro:
|
||||
logger.error("Tushare服务未初始化")
|
||||
return None
|
||||
|
||||
try:
|
||||
# 标准化股票代码
|
||||
ts_code = normalize_stock_code(stock_code)
|
||||
if not ts_code:
|
||||
logger.error(f"无效的股票代码: {stock_code}")
|
||||
return None
|
||||
|
||||
# 获取最新交易日数据
|
||||
df = self.pro.daily(ts_code=ts_code, start_date='', end_date='')
|
||||
|
||||
if df.empty:
|
||||
logger.warning(f"未找到股票数据: {ts_code}")
|
||||
return None
|
||||
|
||||
# 取最新一条
|
||||
latest = df.iloc[0]
|
||||
|
||||
# 获取股票名称
|
||||
stock_info = self.pro.stock_basic(ts_code=ts_code, fields='ts_code,name')
|
||||
name = stock_info.iloc[0]['name'] if not stock_info.empty else None
|
||||
|
||||
return {
|
||||
'ts_code': ts_code,
|
||||
'name': name,
|
||||
'trade_date': latest['trade_date'],
|
||||
'open': float(latest['open']),
|
||||
'high': float(latest['high']),
|
||||
'low': float(latest['low']),
|
||||
'close': float(latest['close']),
|
||||
'pre_close': float(latest['pre_close']),
|
||||
'change': float(latest['change']),
|
||||
'pct_chg': float(latest['pct_chg']),
|
||||
'vol': float(latest['vol']),
|
||||
'amount': float(latest['amount'])
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取实时行情失败: {e}")
|
||||
return None
|
||||
|
||||
def get_kline_data(
|
||||
self,
|
||||
stock_code: str,
|
||||
start_date: Optional[str] = None,
|
||||
end_date: Optional[str] = None,
|
||||
period: str = 'D'
|
||||
) -> Optional[List[dict]]:
|
||||
"""
|
||||
获取K线数据
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
start_date: 开始日期(YYYYMMDD)
|
||||
end_date: 结束日期(YYYYMMDD)
|
||||
period: 周期(D=日,W=周,M=月)
|
||||
|
||||
Returns:
|
||||
K线数据列表
|
||||
"""
|
||||
if not self.pro:
|
||||
logger.error("Tushare服务未初始化")
|
||||
return None
|
||||
|
||||
try:
|
||||
# 标准化股票代码
|
||||
ts_code = normalize_stock_code(stock_code)
|
||||
if not ts_code:
|
||||
logger.error(f"无效的股票代码: {stock_code}")
|
||||
return None
|
||||
|
||||
# 默认获取最近60个交易日
|
||||
if not start_date:
|
||||
start_date = (datetime.now() - timedelta(days=90)).strftime('%Y%m%d')
|
||||
if not end_date:
|
||||
end_date = datetime.now().strftime('%Y%m%d')
|
||||
|
||||
# 获取日线数据
|
||||
if period == 'D':
|
||||
df = self.pro.daily(
|
||||
ts_code=ts_code,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
elif period == 'W':
|
||||
df = self.pro.weekly(
|
||||
ts_code=ts_code,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
elif period == 'M':
|
||||
df = self.pro.monthly(
|
||||
ts_code=ts_code,
|
||||
start_date=start_date,
|
||||
end_date=end_date
|
||||
)
|
||||
else:
|
||||
logger.error(f"不支持的周期: {period}")
|
||||
return None
|
||||
|
||||
if df.empty:
|
||||
logger.warning(f"未找到K线数据: {ts_code}")
|
||||
return None
|
||||
|
||||
# 按日期升序排列
|
||||
df = df.sort_values('trade_date')
|
||||
|
||||
# 转换为字典列表
|
||||
kline_data = []
|
||||
for _, row in df.iterrows():
|
||||
kline_data.append({
|
||||
'ts_code': ts_code,
|
||||
'trade_date': row['trade_date'],
|
||||
'open': float(row['open']),
|
||||
'high': float(row['high']),
|
||||
'low': float(row['low']),
|
||||
'close': float(row['close']),
|
||||
'vol': float(row['vol']),
|
||||
'amount': float(row['amount']) if pd.notna(row['amount']) else None
|
||||
})
|
||||
|
||||
return kline_data
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取K线数据失败: {e}")
|
||||
return None
|
||||
|
||||
def get_stock_basic(self, stock_code: str) -> Optional[dict]:
|
||||
"""
|
||||
获取股票基本信息
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
|
||||
Returns:
|
||||
基本信息字典
|
||||
"""
|
||||
if not self.pro:
|
||||
logger.error("Tushare服务未初始化")
|
||||
return None
|
||||
|
||||
try:
|
||||
ts_code = normalize_stock_code(stock_code)
|
||||
if not ts_code:
|
||||
return None
|
||||
|
||||
df = self.pro.stock_basic(
|
||||
ts_code=ts_code,
|
||||
fields='ts_code,symbol,name,area,industry,market,list_date'
|
||||
)
|
||||
|
||||
if df.empty:
|
||||
return None
|
||||
|
||||
info = df.iloc[0]
|
||||
return {
|
||||
'ts_code': info['ts_code'],
|
||||
'symbol': info['symbol'],
|
||||
'name': info['name'],
|
||||
'area': info['area'],
|
||||
'industry': info['industry'],
|
||||
'market': info['market'],
|
||||
'list_date': info['list_date']
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"获取股票基本信息失败: {e}")
|
||||
return None
|
||||
|
||||
def search_stock(self, keyword: str) -> Optional[List[dict]]:
|
||||
"""
|
||||
搜索股票(通过名称或代码)
|
||||
|
||||
Args:
|
||||
keyword: 搜索关键词(股票名称或代码)
|
||||
|
||||
Returns:
|
||||
匹配的股票列表
|
||||
"""
|
||||
if not self.pro:
|
||||
logger.error("Tushare服务未初始化")
|
||||
return None
|
||||
|
||||
try:
|
||||
# 获取所有股票列表
|
||||
df = self.pro.stock_basic(
|
||||
fields='ts_code,symbol,name,area,industry,market,list_date'
|
||||
)
|
||||
|
||||
if df.empty:
|
||||
return None
|
||||
|
||||
# 搜索匹配的股票
|
||||
# 1. 精确匹配代码
|
||||
exact_match = df[df['symbol'] == keyword]
|
||||
if not exact_match.empty:
|
||||
return [exact_match.iloc[0].to_dict()]
|
||||
|
||||
# 2. 模糊匹配名称
|
||||
name_match = df[df['name'].str.contains(keyword, na=False)]
|
||||
if not name_match.empty:
|
||||
results = []
|
||||
for _, row in name_match.iterrows():
|
||||
results.append(row.to_dict())
|
||||
return results[:5] # 最多返回5个结果
|
||||
|
||||
# 3. 模糊匹配代码
|
||||
code_match = df[df['symbol'].str.contains(keyword, na=False)]
|
||||
if not code_match.empty:
|
||||
results = []
|
||||
for _, row in code_match.iterrows():
|
||||
results.append(row.to_dict())
|
||||
return results[:5]
|
||||
|
||||
return None
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"搜索股票失败: {e}")
|
||||
return None
|
||||
|
||||
|
||||
# 创建全局实例
|
||||
tushare_service = TushareService()
|
||||
0
backend/app/skills/__init__.py
Normal file
0
backend/app/skills/__init__.py
Normal file
78
backend/app/skills/base.py
Normal file
78
backend/app/skills/base.py
Normal file
@ -0,0 +1,78 @@
|
||||
"""
|
||||
技能基类
|
||||
所有技能插件的基类
|
||||
"""
|
||||
from abc import ABC, abstractmethod
|
||||
from typing import Dict, Any, Optional
|
||||
from pydantic import BaseModel, Field
|
||||
|
||||
|
||||
class SkillParameter(BaseModel):
|
||||
"""技能参数定义"""
|
||||
name: str = Field(..., description="参数名称")
|
||||
type: str = Field(..., description="参数类型")
|
||||
description: str = Field(..., description="参数描述")
|
||||
required: bool = Field(True, description="是否必需")
|
||||
default: Optional[Any] = Field(None, description="默认值")
|
||||
|
||||
|
||||
class BaseSkill(ABC):
|
||||
"""技能基类"""
|
||||
|
||||
def __init__(self):
|
||||
"""初始化技能"""
|
||||
self.name: str = ""
|
||||
self.description: str = ""
|
||||
self.parameters: list[SkillParameter] = []
|
||||
self.enabled: bool = True
|
||||
|
||||
@abstractmethod
|
||||
async def execute(self, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
执行技能
|
||||
|
||||
Args:
|
||||
**kwargs: 技能参数
|
||||
|
||||
Returns:
|
||||
执行结果字典
|
||||
"""
|
||||
pass
|
||||
|
||||
def validate_params(self, **kwargs) -> tuple[bool, Optional[str]]:
|
||||
"""
|
||||
验证参数
|
||||
|
||||
Args:
|
||||
**kwargs: 参数字典
|
||||
|
||||
Returns:
|
||||
(是否有效, 错误信息)
|
||||
"""
|
||||
for param in self.parameters:
|
||||
if param.required and param.name not in kwargs:
|
||||
return False, f"缺少必需参数: {param.name}"
|
||||
|
||||
return True, None
|
||||
|
||||
def get_info(self) -> Dict[str, Any]:
|
||||
"""
|
||||
获取技能信息
|
||||
|
||||
Returns:
|
||||
技能信息字典
|
||||
"""
|
||||
return {
|
||||
"name": self.name,
|
||||
"description": self.description,
|
||||
"parameters": [p.dict() for p in self.parameters],
|
||||
"enabled": self.enabled
|
||||
}
|
||||
|
||||
def enable(self):
|
||||
"""启用技能"""
|
||||
self.enabled = True
|
||||
|
||||
def disable(self):
|
||||
"""禁用技能"""
|
||||
self.enabled = False
|
||||
180
backend/app/skills/brave_search.py
Normal file
180
backend/app/skills/brave_search.py
Normal file
@ -0,0 +1,180 @@
|
||||
"""
|
||||
Brave搜索技能
|
||||
提供网页搜索、新闻搜索等能力
|
||||
"""
|
||||
import aiohttp
|
||||
from typing import Dict, Any, List, Optional
|
||||
from app.skills.base import BaseSkill, SkillParameter
|
||||
from app.utils.logger import logger
|
||||
|
||||
|
||||
class BraveSearchSkill(BaseSkill):
|
||||
"""Brave搜索技能"""
|
||||
|
||||
def __init__(self, api_key: str = "BSAcaROCUmCAI0XsQWzxooWT74LFFX_"):
|
||||
super().__init__()
|
||||
self.name = "brave_search"
|
||||
self.description = "使用Brave搜索引擎搜索网页、新闻、公司公告等实时信息"
|
||||
self.api_key = api_key
|
||||
self.base_url = "https://api.search.brave.com/res/v1"
|
||||
|
||||
self.parameters = [
|
||||
SkillParameter(
|
||||
name="query",
|
||||
type="string",
|
||||
description="搜索关键词",
|
||||
required=True
|
||||
),
|
||||
SkillParameter(
|
||||
name="search_type",
|
||||
type="string",
|
||||
description="搜索类型:web(网页)、news(新闻)",
|
||||
required=False,
|
||||
default="web"
|
||||
),
|
||||
SkillParameter(
|
||||
name="count",
|
||||
type="integer",
|
||||
description="返回结果数量(1-20)",
|
||||
required=False,
|
||||
default=5
|
||||
),
|
||||
SkillParameter(
|
||||
name="freshness",
|
||||
type="string",
|
||||
description="时效性:pd(过去一天)、pw(过去一周)、pm(过去一月)、py(过去一年)",
|
||||
required=False,
|
||||
default=None
|
||||
)
|
||||
]
|
||||
|
||||
async def execute(self, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
执行Brave搜索
|
||||
|
||||
Args:
|
||||
query: 搜索关键词
|
||||
search_type: 搜索类型(web/news)
|
||||
count: 结果数量
|
||||
freshness: 时效性过滤
|
||||
|
||||
Returns:
|
||||
搜索结果
|
||||
"""
|
||||
query = kwargs.get("query")
|
||||
search_type = kwargs.get("search_type", "web")
|
||||
count = kwargs.get("count", 5)
|
||||
freshness = kwargs.get("freshness")
|
||||
|
||||
logger.info(f"Brave搜索: {query}, 类型: {search_type}")
|
||||
|
||||
try:
|
||||
if search_type == "news":
|
||||
results = await self._search_news(query, count, freshness)
|
||||
else:
|
||||
results = await self._search_web(query, count, freshness)
|
||||
|
||||
return {
|
||||
"query": query,
|
||||
"search_type": search_type,
|
||||
"results": results,
|
||||
"count": len(results)
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Brave搜索失败: {e}")
|
||||
return {
|
||||
"error": f"搜索失败: {str(e)}"
|
||||
}
|
||||
|
||||
async def _search_web(
|
||||
self,
|
||||
query: str,
|
||||
count: int = 5,
|
||||
freshness: Optional[str] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""网页搜索"""
|
||||
url = f"{self.base_url}/web/search"
|
||||
|
||||
params = {
|
||||
"q": query,
|
||||
"count": min(count, 20),
|
||||
"text_decorations": False,
|
||||
"search_lang": "zh-hans"
|
||||
}
|
||||
|
||||
if freshness:
|
||||
params["freshness"] = freshness
|
||||
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"X-Subscription-Token": self.api_key
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, params=params, headers=headers) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
raise Exception(f"API请求失败: {response.status}, {error_text}")
|
||||
|
||||
data = await response.json()
|
||||
|
||||
# 解析结果
|
||||
results = []
|
||||
web_results = data.get("web", {}).get("results", [])
|
||||
|
||||
for item in web_results[:count]:
|
||||
results.append({
|
||||
"title": item.get("title", ""),
|
||||
"url": item.get("url", ""),
|
||||
"description": item.get("description", ""),
|
||||
"published": item.get("age", "")
|
||||
})
|
||||
|
||||
return results
|
||||
|
||||
async def _search_news(
|
||||
self,
|
||||
query: str,
|
||||
count: int = 5,
|
||||
freshness: Optional[str] = None
|
||||
) -> List[Dict[str, Any]]:
|
||||
"""新闻搜索"""
|
||||
url = f"{self.base_url}/news/search"
|
||||
|
||||
params = {
|
||||
"q": query,
|
||||
"count": min(count, 20),
|
||||
"search_lang": "zh-hans"
|
||||
}
|
||||
|
||||
if freshness:
|
||||
params["freshness"] = freshness
|
||||
|
||||
headers = {
|
||||
"Accept": "application/json",
|
||||
"X-Subscription-Token": self.api_key
|
||||
}
|
||||
|
||||
async with aiohttp.ClientSession() as session:
|
||||
async with session.get(url, params=params, headers=headers) as response:
|
||||
if response.status != 200:
|
||||
error_text = await response.text()
|
||||
raise Exception(f"API请求失败: {response.status}, {error_text}")
|
||||
|
||||
data = await response.json()
|
||||
|
||||
# 解析结果
|
||||
results = []
|
||||
news_results = data.get("results", [])
|
||||
|
||||
for item in news_results[:count]:
|
||||
results.append({
|
||||
"title": item.get("title", ""),
|
||||
"url": item.get("url", ""),
|
||||
"description": item.get("description", ""),
|
||||
"published": item.get("age", ""),
|
||||
"source": item.get("meta_url", {}).get("hostname", "")
|
||||
})
|
||||
|
||||
return results
|
||||
61
backend/app/skills/fundamental.py
Normal file
61
backend/app/skills/fundamental.py
Normal file
@ -0,0 +1,61 @@
|
||||
"""
|
||||
基本面分析技能
|
||||
提供股票基本信息查询
|
||||
"""
|
||||
from typing import Dict, Any
|
||||
from app.skills.base import BaseSkill, SkillParameter
|
||||
from app.services.tushare_service import tushare_service
|
||||
from app.services.cache_service import cache_service
|
||||
from app.utils.logger import logger
|
||||
|
||||
|
||||
class FundamentalSkill(BaseSkill):
|
||||
"""基本面分析技能"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "fundamental"
|
||||
self.description = "查询股票基本面信息(公司概况、行业、上市日期等)"
|
||||
self.parameters = [
|
||||
SkillParameter(
|
||||
name="stock_code",
|
||||
type="string",
|
||||
description="股票代码",
|
||||
required=True
|
||||
)
|
||||
]
|
||||
|
||||
async def execute(self, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
执行基本面查询
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
|
||||
Returns:
|
||||
基本面信息
|
||||
"""
|
||||
stock_code = kwargs.get("stock_code")
|
||||
|
||||
logger.info(f"查询基本面信息: {stock_code}")
|
||||
|
||||
# 尝试从缓存获取
|
||||
cache_key = f"fundamental:{stock_code}"
|
||||
cached_data = cache_service.get(cache_key)
|
||||
|
||||
if cached_data:
|
||||
logger.info(f"从缓存获取基本面信息: {stock_code}")
|
||||
return cached_data
|
||||
|
||||
# 从Tushare获取
|
||||
basic_info = tushare_service.get_stock_basic(stock_code)
|
||||
|
||||
if not basic_info:
|
||||
return {
|
||||
"error": f"未找到股票基本信息: {stock_code}"
|
||||
}
|
||||
|
||||
# 缓存1天
|
||||
cache_service.set(cache_key, basic_info, ttl=86400)
|
||||
|
||||
return basic_info
|
||||
140
backend/app/skills/market_data.py
Normal file
140
backend/app/skills/market_data.py
Normal file
@ -0,0 +1,140 @@
|
||||
"""
|
||||
行情查询技能
|
||||
提供股票实时行情和K线数据查询
|
||||
"""
|
||||
from typing import Dict, Any
|
||||
from app.skills.base import BaseSkill, SkillParameter
|
||||
from app.services.tushare_service import tushare_service
|
||||
from app.services.cache_service import cache_service
|
||||
from app.utils.logger import logger
|
||||
|
||||
|
||||
class MarketDataSkill(BaseSkill):
|
||||
"""行情查询技能"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "market_data"
|
||||
self.description = "查询股票实时行情和历史K线数据"
|
||||
self.parameters = [
|
||||
SkillParameter(
|
||||
name="stock_code",
|
||||
type="string",
|
||||
description="股票代码(如600000、000001)",
|
||||
required=True
|
||||
),
|
||||
SkillParameter(
|
||||
name="data_type",
|
||||
type="string",
|
||||
description="数据类型:quote(实时行情)或kline(K线数据)",
|
||||
required=False,
|
||||
default="quote"
|
||||
),
|
||||
SkillParameter(
|
||||
name="start_date",
|
||||
type="string",
|
||||
description="开始日期(YYYYMMDD格式,仅K线数据需要)",
|
||||
required=False
|
||||
),
|
||||
SkillParameter(
|
||||
name="end_date",
|
||||
type="string",
|
||||
description="结束日期(YYYYMMDD格式,仅K线数据需要)",
|
||||
required=False
|
||||
),
|
||||
SkillParameter(
|
||||
name="period",
|
||||
type="string",
|
||||
description="K线周期:D(日线)、W(周线)、M(月线)",
|
||||
required=False,
|
||||
default="D"
|
||||
)
|
||||
]
|
||||
|
||||
async def execute(self, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
执行行情查询
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
data_type: 数据类型(quote/kline)
|
||||
start_date: 开始日期(可选)
|
||||
end_date: 结束日期(可选)
|
||||
period: K线周期(可选)
|
||||
|
||||
Returns:
|
||||
查询结果
|
||||
"""
|
||||
stock_code = kwargs.get("stock_code")
|
||||
data_type = kwargs.get("data_type", "quote")
|
||||
|
||||
logger.info(f"查询行情数据: {stock_code}, 类型: {data_type}")
|
||||
|
||||
if data_type == "quote":
|
||||
return await self._get_quote(stock_code)
|
||||
elif data_type == "kline":
|
||||
start_date = kwargs.get("start_date")
|
||||
end_date = kwargs.get("end_date")
|
||||
period = kwargs.get("period", "D")
|
||||
return await self._get_kline(stock_code, start_date, end_date, period)
|
||||
else:
|
||||
return {
|
||||
"error": f"不支持的数据类型: {data_type}"
|
||||
}
|
||||
|
||||
async def _get_quote(self, stock_code: str) -> Dict[str, Any]:
|
||||
"""获取实时行情"""
|
||||
# 尝试从缓存获取
|
||||
cache_key = f"quote:{stock_code}"
|
||||
cached_data = cache_service.get(cache_key)
|
||||
|
||||
if cached_data:
|
||||
logger.info(f"从缓存获取行情: {stock_code}")
|
||||
return cached_data
|
||||
|
||||
# 从Tushare获取
|
||||
quote_data = tushare_service.get_realtime_quote(stock_code)
|
||||
|
||||
if not quote_data:
|
||||
return {
|
||||
"error": f"未找到股票数据: {stock_code}"
|
||||
}
|
||||
|
||||
# 缓存30秒
|
||||
cache_service.set(cache_key, quote_data, ttl=30)
|
||||
|
||||
return quote_data
|
||||
|
||||
async def _get_kline(
|
||||
self,
|
||||
stock_code: str,
|
||||
start_date: str = None,
|
||||
end_date: str = None,
|
||||
period: str = "D"
|
||||
) -> Dict[str, Any]:
|
||||
"""获取K线数据"""
|
||||
# 尝试从缓存获取
|
||||
cache_key = f"kline:{stock_code}:{start_date}:{end_date}:{period}"
|
||||
cached_data = cache_service.get(cache_key)
|
||||
|
||||
if cached_data:
|
||||
logger.info(f"从缓存获取K线: {stock_code}")
|
||||
return {"kline_data": cached_data}
|
||||
|
||||
# 从Tushare获取
|
||||
kline_data = tushare_service.get_kline_data(
|
||||
stock_code,
|
||||
start_date,
|
||||
end_date,
|
||||
period
|
||||
)
|
||||
|
||||
if not kline_data:
|
||||
return {
|
||||
"error": f"未找到K线数据: {stock_code}"
|
||||
}
|
||||
|
||||
# 缓存1小时
|
||||
cache_service.set(cache_key, kline_data, ttl=3600)
|
||||
|
||||
return {"kline_data": kline_data}
|
||||
202
backend/app/skills/technical_analysis.py
Normal file
202
backend/app/skills/technical_analysis.py
Normal file
@ -0,0 +1,202 @@
|
||||
"""
|
||||
技术分析技能
|
||||
提供技术指标计算和分析
|
||||
"""
|
||||
import pandas as pd
|
||||
from typing import Dict, Any
|
||||
from app.skills.base import BaseSkill, SkillParameter
|
||||
from app.services.tushare_service import tushare_service
|
||||
from app.utils.indicators import (
|
||||
calculate_ma, calculate_macd, calculate_rsi,
|
||||
calculate_kdj, calculate_boll
|
||||
)
|
||||
from app.utils.logger import logger
|
||||
|
||||
|
||||
class TechnicalAnalysisSkill(BaseSkill):
|
||||
"""技术分析技能"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "technical_analysis"
|
||||
self.description = "计算股票技术指标(MA、MACD、RSI、KDJ、BOLL等)"
|
||||
self.parameters = [
|
||||
SkillParameter(
|
||||
name="stock_code",
|
||||
type="string",
|
||||
description="股票代码",
|
||||
required=True
|
||||
),
|
||||
SkillParameter(
|
||||
name="indicators",
|
||||
type="array",
|
||||
description="要计算的指标列表(ma、macd、rsi、kdj、boll)",
|
||||
required=False,
|
||||
default=["ma", "macd"]
|
||||
),
|
||||
SkillParameter(
|
||||
name="period",
|
||||
type="integer",
|
||||
description="数据周期(天数)",
|
||||
required=False,
|
||||
default=60
|
||||
)
|
||||
]
|
||||
|
||||
async def execute(self, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
执行技术分析
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
indicators: 指标列表
|
||||
period: 数据周期
|
||||
|
||||
Returns:
|
||||
技术指标结果
|
||||
"""
|
||||
stock_code = kwargs.get("stock_code")
|
||||
indicators = kwargs.get("indicators", ["ma", "macd"])
|
||||
period = kwargs.get("period", 60)
|
||||
|
||||
logger.info(f"技术分析: {stock_code}, 指标: {indicators}")
|
||||
|
||||
# 获取K线数据
|
||||
kline_data = tushare_service.get_kline_data(stock_code)
|
||||
|
||||
if not kline_data:
|
||||
return {
|
||||
"error": f"未找到K线数据: {stock_code}"
|
||||
}
|
||||
|
||||
# 转换为DataFrame
|
||||
df = pd.DataFrame(kline_data)
|
||||
|
||||
# 计算指标
|
||||
result = {
|
||||
"stock_code": stock_code,
|
||||
"indicators": {}
|
||||
}
|
||||
|
||||
try:
|
||||
if "ma" in indicators:
|
||||
result["indicators"]["ma"] = self._calculate_ma(df)
|
||||
|
||||
if "macd" in indicators:
|
||||
result["indicators"]["macd"] = self._calculate_macd(df)
|
||||
|
||||
if "rsi" in indicators:
|
||||
result["indicators"]["rsi"] = self._calculate_rsi(df)
|
||||
|
||||
if "kdj" in indicators:
|
||||
result["indicators"]["kdj"] = self._calculate_kdj(df)
|
||||
|
||||
if "boll" in indicators:
|
||||
result["indicators"]["boll"] = self._calculate_boll(df)
|
||||
|
||||
return result
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"技术指标计算失败: {e}")
|
||||
return {
|
||||
"error": f"技术指标计算失败: {str(e)}"
|
||||
}
|
||||
|
||||
def _calculate_ma(self, df: pd.DataFrame) -> Dict[str, Any]:
|
||||
"""计算均线"""
|
||||
close = df['close']
|
||||
|
||||
ma5 = calculate_ma(close, 5)
|
||||
ma10 = calculate_ma(close, 10)
|
||||
ma20 = calculate_ma(close, 20)
|
||||
ma60 = calculate_ma(close, 60)
|
||||
|
||||
# 获取最新值
|
||||
latest_ma5 = ma5.iloc[-1] if not ma5.empty else None
|
||||
latest_ma10 = ma10.iloc[-1] if not ma10.empty else None
|
||||
latest_ma20 = ma20.iloc[-1] if not ma20.empty else None
|
||||
latest_ma60 = ma60.iloc[-1] if not ma60.empty else None
|
||||
|
||||
return {
|
||||
"ma5": round(latest_ma5, 2) if latest_ma5 else None,
|
||||
"ma10": round(latest_ma10, 2) if latest_ma10 else None,
|
||||
"ma20": round(latest_ma20, 2) if latest_ma20 else None,
|
||||
"ma60": round(latest_ma60, 2) if latest_ma60 else None,
|
||||
"description": "移动平均线"
|
||||
}
|
||||
|
||||
def _calculate_macd(self, df: pd.DataFrame) -> Dict[str, Any]:
|
||||
"""计算MACD"""
|
||||
close = df['close']
|
||||
|
||||
dif, dea, macd = calculate_macd(close)
|
||||
|
||||
# 获取最新值
|
||||
latest_dif = dif.iloc[-1] if not dif.empty else None
|
||||
latest_dea = dea.iloc[-1] if not dea.empty else None
|
||||
latest_macd = macd.iloc[-1] if not macd.empty else None
|
||||
|
||||
return {
|
||||
"dif": round(latest_dif, 2) if latest_dif else None,
|
||||
"dea": round(latest_dea, 2) if latest_dea else None,
|
||||
"macd": round(latest_macd, 2) if latest_macd else None,
|
||||
"description": "MACD指标"
|
||||
}
|
||||
|
||||
def _calculate_rsi(self, df: pd.DataFrame) -> Dict[str, Any]:
|
||||
"""计算RSI"""
|
||||
close = df['close']
|
||||
|
||||
rsi6 = calculate_rsi(close, 6)
|
||||
rsi12 = calculate_rsi(close, 12)
|
||||
rsi24 = calculate_rsi(close, 24)
|
||||
|
||||
# 获取最新值
|
||||
latest_rsi6 = rsi6.iloc[-1] if not rsi6.empty else None
|
||||
latest_rsi12 = rsi12.iloc[-1] if not rsi12.empty else None
|
||||
latest_rsi24 = rsi24.iloc[-1] if not rsi24.empty else None
|
||||
|
||||
return {
|
||||
"rsi6": round(latest_rsi6, 2) if latest_rsi6 else None,
|
||||
"rsi12": round(latest_rsi12, 2) if latest_rsi12 else None,
|
||||
"rsi24": round(latest_rsi24, 2) if latest_rsi24 else None,
|
||||
"description": "相对强弱指标"
|
||||
}
|
||||
|
||||
def _calculate_kdj(self, df: pd.DataFrame) -> Dict[str, Any]:
|
||||
"""计算KDJ"""
|
||||
high = df['high']
|
||||
low = df['low']
|
||||
close = df['close']
|
||||
|
||||
k, d, j = calculate_kdj(high, low, close)
|
||||
|
||||
# 获取最新值
|
||||
latest_k = k.iloc[-1] if not k.empty else None
|
||||
latest_d = d.iloc[-1] if not d.empty else None
|
||||
latest_j = j.iloc[-1] if not j.empty else None
|
||||
|
||||
return {
|
||||
"k": round(latest_k, 2) if latest_k else None,
|
||||
"d": round(latest_d, 2) if latest_d else None,
|
||||
"j": round(latest_j, 2) if latest_j else None,
|
||||
"description": "KDJ指标"
|
||||
}
|
||||
|
||||
def _calculate_boll(self, df: pd.DataFrame) -> Dict[str, Any]:
|
||||
"""计算布林带"""
|
||||
close = df['close']
|
||||
|
||||
upper, middle, lower = calculate_boll(close)
|
||||
|
||||
# 获取最新值
|
||||
latest_upper = upper.iloc[-1] if not upper.empty else None
|
||||
latest_middle = middle.iloc[-1] if not middle.empty else None
|
||||
latest_lower = lower.iloc[-1] if not lower.empty else None
|
||||
|
||||
return {
|
||||
"upper": round(latest_upper, 2) if latest_upper else None,
|
||||
"middle": round(latest_middle, 2) if latest_middle else None,
|
||||
"lower": round(latest_lower, 2) if latest_lower else None,
|
||||
"description": "布林带"
|
||||
}
|
||||
118
backend/app/skills/visualization.py
Normal file
118
backend/app/skills/visualization.py
Normal file
@ -0,0 +1,118 @@
|
||||
"""
|
||||
数据可视化技能
|
||||
生成图表配置数据
|
||||
"""
|
||||
from typing import Dict, Any, List
|
||||
from app.skills.base import BaseSkill, SkillParameter
|
||||
from app.services.tushare_service import tushare_service
|
||||
from app.utils.logger import logger
|
||||
|
||||
|
||||
class VisualizationSkill(BaseSkill):
|
||||
"""数据可视化技能"""
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.name = "visualization"
|
||||
self.description = "生成K线图和技术指标图表配置"
|
||||
self.parameters = [
|
||||
SkillParameter(
|
||||
name="stock_code",
|
||||
type="string",
|
||||
description="股票代码",
|
||||
required=True
|
||||
),
|
||||
SkillParameter(
|
||||
name="chart_type",
|
||||
type="string",
|
||||
description="图表类型:candlestick(K线图)",
|
||||
required=False,
|
||||
default="candlestick"
|
||||
),
|
||||
SkillParameter(
|
||||
name="period",
|
||||
type="integer",
|
||||
description="数据周期(天数)",
|
||||
required=False,
|
||||
default=60
|
||||
)
|
||||
]
|
||||
|
||||
async def execute(self, **kwargs) -> Dict[str, Any]:
|
||||
"""
|
||||
生成图表配置
|
||||
|
||||
Args:
|
||||
stock_code: 股票代码
|
||||
chart_type: 图表类型
|
||||
period: 数据周期
|
||||
|
||||
Returns:
|
||||
图表配置数据
|
||||
"""
|
||||
stock_code = kwargs.get("stock_code")
|
||||
chart_type = kwargs.get("chart_type", "candlestick")
|
||||
period = kwargs.get("period", 60)
|
||||
|
||||
logger.info(f"生成图表配置: {stock_code}, 类型: {chart_type}")
|
||||
|
||||
# 获取K线数据
|
||||
kline_data = tushare_service.get_kline_data(stock_code)
|
||||
|
||||
if not kline_data:
|
||||
return {
|
||||
"error": f"未找到K线数据: {stock_code}"
|
||||
}
|
||||
|
||||
# 限制数据量
|
||||
if len(kline_data) > period:
|
||||
kline_data = kline_data[-period:]
|
||||
|
||||
if chart_type == "candlestick":
|
||||
return self._generate_candlestick_config(kline_data)
|
||||
else:
|
||||
return {
|
||||
"error": f"不支持的图表类型: {chart_type}"
|
||||
}
|
||||
|
||||
def _generate_candlestick_config(self, kline_data: List[dict]) -> Dict[str, Any]:
|
||||
"""
|
||||
生成K线图配置(Lightweight Charts格式)
|
||||
|
||||
Args:
|
||||
kline_data: K线数据列表
|
||||
|
||||
Returns:
|
||||
图表配置
|
||||
"""
|
||||
# 转换为Lightweight Charts格式
|
||||
candlestick_data = []
|
||||
volume_data = []
|
||||
|
||||
for item in kline_data:
|
||||
# 转换日期格式 YYYYMMDD -> YYYY-MM-DD
|
||||
date_str = item['trade_date']
|
||||
formatted_date = f"{date_str[:4]}-{date_str[4:6]}-{date_str[6:8]}"
|
||||
|
||||
# K线数据
|
||||
candlestick_data.append({
|
||||
"time": formatted_date,
|
||||
"open": item['open'],
|
||||
"high": item['high'],
|
||||
"low": item['low'],
|
||||
"close": item['close']
|
||||
})
|
||||
|
||||
# 成交量数据
|
||||
volume_data.append({
|
||||
"time": formatted_date,
|
||||
"value": item['vol'],
|
||||
"color": "rgba(0, 150, 136, 0.8)" if item['close'] >= item['open'] else "rgba(255, 82, 82, 0.8)"
|
||||
})
|
||||
|
||||
return {
|
||||
"chart_type": "candlestick",
|
||||
"candlestick_data": candlestick_data,
|
||||
"volume_data": volume_data,
|
||||
"stock_code": kline_data[0]['ts_code'] if kline_data else None
|
||||
}
|
||||
0
backend/app/utils/__init__.py
Normal file
0
backend/app/utils/__init__.py
Normal file
158
backend/app/utils/indicators.py
Normal file
158
backend/app/utils/indicators.py
Normal file
@ -0,0 +1,158 @@
|
||||
"""
|
||||
技术指标计算模块
|
||||
提供常用技术指标的计算功能
|
||||
"""
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
from typing import Tuple
|
||||
|
||||
|
||||
def calculate_ma(data: pd.Series, period: int = 5) -> pd.Series:
|
||||
"""
|
||||
计算移动平均线(MA)
|
||||
|
||||
Args:
|
||||
data: 价格数据
|
||||
period: 周期
|
||||
|
||||
Returns:
|
||||
MA值
|
||||
"""
|
||||
return data.rolling(window=period).mean()
|
||||
|
||||
|
||||
def calculate_ema(data: pd.Series, period: int = 12) -> pd.Series:
|
||||
"""
|
||||
计算指数移动平均线(EMA)
|
||||
|
||||
Args:
|
||||
data: 价格数据
|
||||
period: 周期
|
||||
|
||||
Returns:
|
||||
EMA值
|
||||
"""
|
||||
return data.ewm(span=period, adjust=False).mean()
|
||||
|
||||
|
||||
def calculate_macd(
|
||||
data: pd.Series,
|
||||
fast_period: int = 12,
|
||||
slow_period: int = 26,
|
||||
signal_period: int = 9
|
||||
) -> Tuple[pd.Series, pd.Series, pd.Series]:
|
||||
"""
|
||||
计算MACD指标
|
||||
|
||||
Args:
|
||||
data: 价格数据
|
||||
fast_period: 快线周期
|
||||
slow_period: 慢线周期
|
||||
signal_period: 信号线周期
|
||||
|
||||
Returns:
|
||||
(DIF, DEA, MACD柱)
|
||||
"""
|
||||
ema_fast = calculate_ema(data, fast_period)
|
||||
ema_slow = calculate_ema(data, slow_period)
|
||||
|
||||
dif = ema_fast - ema_slow
|
||||
dea = dif.ewm(span=signal_period, adjust=False).mean()
|
||||
macd = (dif - dea) * 2
|
||||
|
||||
return dif, dea, macd
|
||||
|
||||
|
||||
def calculate_rsi(data: pd.Series, period: int = 14) -> pd.Series:
|
||||
"""
|
||||
计算相对强弱指标(RSI)
|
||||
|
||||
Args:
|
||||
data: 价格数据
|
||||
period: 周期
|
||||
|
||||
Returns:
|
||||
RSI值
|
||||
"""
|
||||
delta = data.diff()
|
||||
|
||||
gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
|
||||
loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
|
||||
|
||||
rs = gain / loss
|
||||
rsi = 100 - (100 / (1 + rs))
|
||||
|
||||
return rsi
|
||||
|
||||
|
||||
def calculate_kdj(
|
||||
high: pd.Series,
|
||||
low: pd.Series,
|
||||
close: pd.Series,
|
||||
period: int = 9,
|
||||
m1: int = 3,
|
||||
m2: int = 3
|
||||
) -> Tuple[pd.Series, pd.Series, pd.Series]:
|
||||
"""
|
||||
计算KDJ指标
|
||||
|
||||
Args:
|
||||
high: 最高价
|
||||
low: 最低价
|
||||
close: 收盘价
|
||||
period: 周期
|
||||
m1: K值平滑参数
|
||||
m2: D值平滑参数
|
||||
|
||||
Returns:
|
||||
(K, D, J)
|
||||
"""
|
||||
low_min = low.rolling(window=period).min()
|
||||
high_max = high.rolling(window=period).max()
|
||||
|
||||
rsv = (close - low_min) / (high_max - low_min) * 100
|
||||
|
||||
k = rsv.ewm(com=m1 - 1, adjust=False).mean()
|
||||
d = k.ewm(com=m2 - 1, adjust=False).mean()
|
||||
j = 3 * k - 2 * d
|
||||
|
||||
return k, d, j
|
||||
|
||||
|
||||
def calculate_boll(
|
||||
data: pd.Series,
|
||||
period: int = 20,
|
||||
std_dev: float = 2.0
|
||||
) -> Tuple[pd.Series, pd.Series, pd.Series]:
|
||||
"""
|
||||
计算布林带(BOLL)
|
||||
|
||||
Args:
|
||||
data: 价格数据
|
||||
period: 周期
|
||||
std_dev: 标准差倍数
|
||||
|
||||
Returns:
|
||||
(上轨, 中轨, 下轨)
|
||||
"""
|
||||
middle = data.rolling(window=period).mean()
|
||||
std = data.rolling(window=period).std()
|
||||
|
||||
upper = middle + (std * std_dev)
|
||||
lower = middle - (std * std_dev)
|
||||
|
||||
return upper, middle, lower
|
||||
|
||||
|
||||
def calculate_volume_ma(volume: pd.Series, period: int = 5) -> pd.Series:
|
||||
"""
|
||||
计算成交量移动平均
|
||||
|
||||
Args:
|
||||
volume: 成交量数据
|
||||
period: 周期
|
||||
|
||||
Returns:
|
||||
成交量MA
|
||||
"""
|
||||
return volume.rolling(window=period).mean()
|
||||
60
backend/app/utils/logger.py
Normal file
60
backend/app/utils/logger.py
Normal file
@ -0,0 +1,60 @@
|
||||
"""
|
||||
日志工具模块
|
||||
提供统一的日志配置和记录功能
|
||||
"""
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def setup_logger(
|
||||
name: str = "stock_agent",
|
||||
level: int = logging.INFO,
|
||||
log_file: Optional[str] = None
|
||||
) -> logging.Logger:
|
||||
"""
|
||||
配置并返回logger实例
|
||||
|
||||
Args:
|
||||
name: logger名称
|
||||
level: 日志级别
|
||||
log_file: 日志文件路径(可选)
|
||||
|
||||
Returns:
|
||||
配置好的logger实例
|
||||
"""
|
||||
logger = logging.getLogger(name)
|
||||
logger.setLevel(level)
|
||||
|
||||
# 避免重复添加handler
|
||||
if logger.handlers:
|
||||
return logger
|
||||
|
||||
# 日志格式
|
||||
formatter = logging.Formatter(
|
||||
'%(asctime)s - %(name)s - %(levelname)s - %(message)s',
|
||||
datefmt='%Y-%m-%d %H:%M:%S'
|
||||
)
|
||||
|
||||
# 控制台handler
|
||||
console_handler = logging.StreamHandler(sys.stdout)
|
||||
console_handler.setLevel(level)
|
||||
console_handler.setFormatter(formatter)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# 文件handler(如果指定)
|
||||
if log_file:
|
||||
log_path = Path(log_file)
|
||||
log_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
file_handler = logging.FileHandler(log_file, encoding='utf-8')
|
||||
file_handler.setLevel(level)
|
||||
file_handler.setFormatter(formatter)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
return logger
|
||||
|
||||
|
||||
# 创建默认logger
|
||||
logger = setup_logger()
|
||||
254
backend/app/utils/stock_names.py
Normal file
254
backend/app/utils/stock_names.py
Normal file
@ -0,0 +1,254 @@
|
||||
"""
|
||||
股票名称映射数据库
|
||||
包含常见A股股票的名称到代码的映射
|
||||
"""
|
||||
from typing import Optional
|
||||
|
||||
# 常见A股股票名称映射(按行业分类)
|
||||
STOCK_NAME_MAP = {
|
||||
# 白酒
|
||||
"贵州茅台": "600519",
|
||||
"茅台": "600519",
|
||||
"五粮液": "000858",
|
||||
"泸州老窖": "000568",
|
||||
"山西汾酒": "600809",
|
||||
"洋河股份": "002304",
|
||||
|
||||
# 银行
|
||||
"工商银行": "601398",
|
||||
"工行": "601398",
|
||||
"建设银行": "601939",
|
||||
"建行": "601939",
|
||||
"农业银行": "601288",
|
||||
"农行": "601288",
|
||||
"中国银行": "601988",
|
||||
"中行": "601988",
|
||||
"交通银行": "601328",
|
||||
"交行": "601328",
|
||||
"招商银行": "600036",
|
||||
"招行": "600036",
|
||||
"兴业银行": "601166",
|
||||
"浦发银行": "600000",
|
||||
"民生银行": "600016",
|
||||
"光大银行": "601818",
|
||||
"平安银行": "000001",
|
||||
"宁波银行": "002142",
|
||||
|
||||
# 保险
|
||||
"中国平安": "601318",
|
||||
"平安": "601318",
|
||||
"中国人寿": "601628",
|
||||
"中国太保": "601601",
|
||||
"新华保险": "601336",
|
||||
|
||||
# 证券
|
||||
"中信证券": "600030",
|
||||
"中信": "600030",
|
||||
"海通证券": "600837",
|
||||
"国泰君安": "601211",
|
||||
"华泰证券": "601688",
|
||||
"广发证券": "000776",
|
||||
"招商证券": "600999",
|
||||
"东方证券": "600958",
|
||||
|
||||
# 科技
|
||||
"中兴通讯": "000063",
|
||||
"中兴": "000063",
|
||||
"立讯精密": "002475",
|
||||
"京东方A": "000725",
|
||||
"京东方": "000725",
|
||||
"TCL科技": "000100",
|
||||
"海康威视": "002415",
|
||||
"大华股份": "002236",
|
||||
"科大讯飞": "002230",
|
||||
"讯飞": "002230",
|
||||
"紫光国微": "002049",
|
||||
"中芯国际": "688981",
|
||||
"韦尔股份": "603501",
|
||||
|
||||
# 新能源汽车
|
||||
"比亚迪": "002594",
|
||||
"宁德时代": "300750",
|
||||
"宁德": "300750",
|
||||
"长城汽车": "601633",
|
||||
"长城": "601633",
|
||||
"上汽集团": "600104",
|
||||
"上汽": "600104",
|
||||
"广汽集团": "601238",
|
||||
"广汽": "601238",
|
||||
"吉利汽车": "00175", # 港股
|
||||
"理想汽车": "02015", # 港股
|
||||
"小鹏汽车": "09868", # 港股
|
||||
"蔚来": "09866", # 港股
|
||||
|
||||
# 医药
|
||||
"恒瑞医药": "600276",
|
||||
"恒瑞": "600276",
|
||||
"药明康德": "603259",
|
||||
"迈瑞医疗": "300760",
|
||||
"迈瑞": "300760",
|
||||
"片仔癀": "600436",
|
||||
"云南白药": "000538",
|
||||
"白药": "000538",
|
||||
"爱尔眼科": "300015",
|
||||
"智飞生物": "300122",
|
||||
|
||||
# 消费
|
||||
"伊利股份": "600887",
|
||||
"伊利": "600887",
|
||||
"海天味业": "603288",
|
||||
"海天": "603288",
|
||||
"格力电器": "000651",
|
||||
"格力": "000651",
|
||||
"美的集团": "000333",
|
||||
"美的": "000333",
|
||||
"海尔智家": "600690",
|
||||
"海尔": "600690",
|
||||
"老板电器": "002508",
|
||||
|
||||
# 地产
|
||||
"万科A": "000002",
|
||||
"万科": "000002",
|
||||
"保利发展": "600048",
|
||||
"保利": "600048",
|
||||
"招商蛇口": "001979",
|
||||
"金地集团": "600383",
|
||||
"金地": "600383",
|
||||
|
||||
# 能源
|
||||
"中国石油": "601857",
|
||||
"中石油": "601857",
|
||||
"中国石化": "600028",
|
||||
"中石化": "600028",
|
||||
"中国神华": "601088",
|
||||
"神华": "601088",
|
||||
"陕西煤业": "601225",
|
||||
"长江电力": "600900",
|
||||
"三峡能源": "600905",
|
||||
|
||||
# 通信
|
||||
"中国移动": "600941",
|
||||
"移动": "600941",
|
||||
"中国电信": "601728",
|
||||
"电信": "601728",
|
||||
"中国联通": "600050",
|
||||
"联通": "600050",
|
||||
"中国卫通": "601698",
|
||||
"卫通": "601698",
|
||||
|
||||
# 航空航天
|
||||
"中国国航": "601111",
|
||||
"国航": "601111",
|
||||
"南方航空": "600029",
|
||||
"南航": "600029",
|
||||
"东方航空": "600115",
|
||||
"东航": "600115",
|
||||
"中国卫星": "600118",
|
||||
"航天科技": "000901",
|
||||
|
||||
# 钢铁
|
||||
"宝钢股份": "600019",
|
||||
"宝钢": "600019",
|
||||
"河钢股份": "000709",
|
||||
"河钢": "000709",
|
||||
"鞍钢股份": "000898",
|
||||
"鞍钢": "000898",
|
||||
|
||||
# 有色金属
|
||||
"紫金矿业": "601899",
|
||||
"紫金": "601899",
|
||||
"中国铝业": "601600",
|
||||
"中铝": "601600",
|
||||
"江西铜业": "600362",
|
||||
"江铜": "600362",
|
||||
"洛阳钼业": "603993",
|
||||
|
||||
# 化工
|
||||
"万华化学": "600309",
|
||||
"万华": "600309",
|
||||
"华鲁恒升": "600426",
|
||||
"恒力石化": "600346",
|
||||
"荣盛石化": "002493",
|
||||
|
||||
# 电力设备
|
||||
"隆基绿能": "601012",
|
||||
"隆基": "601012",
|
||||
"阳光电源": "300274",
|
||||
"通威股份": "600438",
|
||||
"通威": "600438",
|
||||
"特变电工": "600089",
|
||||
|
||||
# 军工
|
||||
"中航沈飞": "600760",
|
||||
"沈飞": "600760",
|
||||
"中航西飞": "000768",
|
||||
"西飞": "000768",
|
||||
"中国船舶": "600150",
|
||||
"中船": "600150",
|
||||
"航发动力": "600893",
|
||||
"航天发展": "000547",
|
||||
|
||||
# 互联网
|
||||
"腾讯控股": "00700", # 港股
|
||||
"腾讯": "00700",
|
||||
"阿里巴巴": "09988", # 港股
|
||||
"阿里": "09988",
|
||||
"美团": "03690", # 港股
|
||||
"京东": "09618", # 港股
|
||||
"拼多多": "PDD", # 美股
|
||||
"百度": "09888", # 港股
|
||||
"网易": "09999", # 港股
|
||||
"小米集团": "01810", # 港股
|
||||
"小米": "01810",
|
||||
|
||||
# 指数
|
||||
"上证指数": "000001",
|
||||
"上证": "000001",
|
||||
"沪指": "000001",
|
||||
"深证成指": "399001",
|
||||
"深成指": "399001",
|
||||
"创业板指": "399006",
|
||||
"创业板": "399006",
|
||||
"科创50": "000688",
|
||||
"沪深300": "000300",
|
||||
"中证500": "000905",
|
||||
"中证1000": "000852",
|
||||
}
|
||||
|
||||
|
||||
def search_stock_by_name(name: str) -> Optional[str]:
|
||||
"""
|
||||
根据股票名称搜索代码
|
||||
|
||||
Args:
|
||||
name: 股票名称或简称
|
||||
|
||||
Returns:
|
||||
股票代码或None
|
||||
"""
|
||||
# 精确匹配
|
||||
if name in STOCK_NAME_MAP:
|
||||
return STOCK_NAME_MAP[name]
|
||||
|
||||
# 模糊匹配(包含关系)
|
||||
for stock_name, code in STOCK_NAME_MAP.items():
|
||||
if name in stock_name or stock_name in name:
|
||||
return code
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def get_stock_name(code: str) -> Optional[str]:
|
||||
"""
|
||||
根据代码获取股票名称
|
||||
|
||||
Args:
|
||||
code: 股票代码
|
||||
|
||||
Returns:
|
||||
股票名称或None
|
||||
"""
|
||||
for name, stock_code in STOCK_NAME_MAP.items():
|
||||
if stock_code == code:
|
||||
return name
|
||||
return None
|
||||
103
backend/app/utils/validators.py
Normal file
103
backend/app/utils/validators.py
Normal file
@ -0,0 +1,103 @@
|
||||
"""
|
||||
验证工具模块
|
||||
提供各种数据验证功能
|
||||
"""
|
||||
import re
|
||||
from typing import Optional
|
||||
|
||||
|
||||
def validate_stock_code(code: str) -> bool:
|
||||
"""
|
||||
验证股票代码格式
|
||||
|
||||
A股代码格式:
|
||||
- 上海:6开头,6位数字
|
||||
- 深圳:0/3开头,6位数字
|
||||
- 创业板:3开头,6位数字
|
||||
- 科创板:688开头,6位数字
|
||||
|
||||
Args:
|
||||
code: 股票代码
|
||||
|
||||
Returns:
|
||||
是否有效
|
||||
"""
|
||||
if not code:
|
||||
return False
|
||||
|
||||
# 移除可能的后缀(如.SH, .SZ)
|
||||
code = code.split('.')[0]
|
||||
|
||||
# 检查是否为6位数字
|
||||
if not re.match(r'^\d{6}$', code):
|
||||
return False
|
||||
|
||||
# 检查首位数字
|
||||
first_digit = code[0]
|
||||
if first_digit in ['0', '3', '6']:
|
||||
return True
|
||||
|
||||
# 检查科创板
|
||||
if code.startswith('688'):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
def normalize_stock_code(code: str) -> Optional[str]:
|
||||
"""
|
||||
标准化股票代码,添加市场后缀
|
||||
|
||||
Args:
|
||||
code: 股票代码
|
||||
|
||||
Returns:
|
||||
标准化后的代码(如600000.SH)或None
|
||||
"""
|
||||
if not validate_stock_code(code):
|
||||
return None
|
||||
|
||||
# 移除已有后缀
|
||||
code = code.split('.')[0]
|
||||
|
||||
# 添加市场后缀
|
||||
if code.startswith('6'):
|
||||
return f"{code}.SH" # 上海
|
||||
elif code.startswith(('0', '3')):
|
||||
return f"{code}.SZ" # 深圳
|
||||
elif code.startswith('688'):
|
||||
return f"{code}.SH" # 科创板
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def validate_date_format(date_str: str) -> bool:
|
||||
"""
|
||||
验证日期格式(YYYYMMDD)
|
||||
|
||||
Args:
|
||||
date_str: 日期字符串
|
||||
|
||||
Returns:
|
||||
是否有效
|
||||
"""
|
||||
if not date_str:
|
||||
return False
|
||||
|
||||
# 检查格式
|
||||
if not re.match(r'^\d{8}$', date_str):
|
||||
return False
|
||||
|
||||
# 简单的日期范围检查
|
||||
year = int(date_str[:4])
|
||||
month = int(date_str[4:6])
|
||||
day = int(date_str[6:8])
|
||||
|
||||
if year < 1990 or year > 2100:
|
||||
return False
|
||||
if month < 1 or month > 12:
|
||||
return False
|
||||
if day < 1 or day > 31:
|
||||
return False
|
||||
|
||||
return True
|
||||
100
backend/diagnose.sh
Executable file
100
backend/diagnose.sh
Executable file
@ -0,0 +1,100 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 诊断脚本 - 检查系统配置
|
||||
|
||||
echo "================================"
|
||||
echo "系统诊断"
|
||||
echo "================================"
|
||||
echo ""
|
||||
|
||||
cd /Users/aaron/source_code/Stock_Agent/backend
|
||||
|
||||
# 1. 检查虚拟环境
|
||||
echo "1. 检查虚拟环境..."
|
||||
if [ -d "venv" ]; then
|
||||
echo " ✓ 虚拟环境存在"
|
||||
source venv/bin/activate
|
||||
python_version=$(python --version 2>&1)
|
||||
echo " ✓ $python_version"
|
||||
else
|
||||
echo " ❌ 虚拟环境不存在"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 2. 检查.env文件
|
||||
echo ""
|
||||
echo "2. 检查配置文件..."
|
||||
if [ -f "../.env" ]; then
|
||||
echo " ✓ .env文件存在(项目根目录)"
|
||||
elif [ -f ".env" ]; then
|
||||
echo " ✓ .env文件存在(backend目录)"
|
||||
else
|
||||
echo " ❌ .env文件不存在"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 3. 检查依赖包
|
||||
echo ""
|
||||
echo "3. 检查依赖包..."
|
||||
packages=("fastapi" "uvicorn" "tushare" "pandas" "numpy" "sqlalchemy" "pydantic")
|
||||
all_installed=true
|
||||
|
||||
for pkg in "${packages[@]}"; do
|
||||
if python -c "import $pkg" 2>/dev/null; then
|
||||
version=$(python -c "import $pkg; print($pkg.__version__)" 2>/dev/null || echo "unknown")
|
||||
echo " ✓ $pkg ($version)"
|
||||
else
|
||||
echo " ❌ $pkg 未安装"
|
||||
all_installed=false
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "$all_installed" = false ]; then
|
||||
echo ""
|
||||
echo "请运行: pip install -r requirements.txt"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 4. 测试配置加载
|
||||
echo ""
|
||||
echo "4. 测试配置加载..."
|
||||
python -c "
|
||||
from app.config import get_settings
|
||||
settings = get_settings()
|
||||
print(f' ✓ 配置加载成功')
|
||||
print(f' - Tushare Token: {'已配置 (' + settings.tushare_token[:10] + '...)' if settings.tushare_token else '❌ 未配置'}')
|
||||
print(f' - 智谱AI Key: {'已配置 (' + settings.zhipuai_api_key[:10] + '...)' if settings.zhipuai_api_key else '❌ 未配置'}')
|
||||
" 2>&1
|
||||
|
||||
# 5. 测试模块导入
|
||||
echo ""
|
||||
echo "5. 测试模块导入..."
|
||||
modules=("app.models.database" "app.services.cache_service" "app.services.tushare_service" "app.agent.core")
|
||||
|
||||
for module in "${modules[@]}"; do
|
||||
if python -c "import $module" 2>/dev/null; then
|
||||
echo " ✓ $module"
|
||||
else
|
||||
echo " ❌ $module 导入失败"
|
||||
python -c "import $module" 2>&1 | head -5
|
||||
fi
|
||||
done
|
||||
|
||||
# 6. 检查端口占用
|
||||
echo ""
|
||||
echo "6. 检查端口占用..."
|
||||
if lsof -i :8000 >/dev/null 2>&1; then
|
||||
echo " ⚠ 端口8000已被占用"
|
||||
lsof -i :8000
|
||||
else
|
||||
echo " ✓ 端口8000可用"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "诊断完成"
|
||||
echo "================================"
|
||||
echo ""
|
||||
echo "如果所有检查都通过,可以运行:"
|
||||
echo " ./start.sh"
|
||||
echo ""
|
||||
16
backend/requirements.txt
Normal file
16
backend/requirements.txt
Normal file
@ -0,0 +1,16 @@
|
||||
fastapi==0.109.0
|
||||
uvicorn[standard]==0.27.0
|
||||
langchain==0.1.0
|
||||
langchain-community==0.0.20
|
||||
zhipuai==2.0.1
|
||||
tushare==1.3.8
|
||||
sqlalchemy==2.0.25
|
||||
pydantic==2.5.3
|
||||
pydantic-settings==2.1.0
|
||||
python-dotenv==1.0.0
|
||||
slowapi==0.1.9
|
||||
websockets==12.0
|
||||
pandas>=2.2.0
|
||||
numpy>=1.26.0
|
||||
python-multipart==0.0.6
|
||||
aiohttp==3.9.1
|
||||
110
backend/run.sh
Executable file
110
backend/run.sh
Executable file
@ -0,0 +1,110 @@
|
||||
#!/bin/bash
|
||||
|
||||
# 最终启动检查和启动脚本
|
||||
|
||||
echo "================================"
|
||||
echo "A股AI分析Agent - 最终检查"
|
||||
echo "================================"
|
||||
echo ""
|
||||
|
||||
cd /Users/aaron/source_code/Stock_Agent/backend
|
||||
|
||||
# 激活虚拟环境
|
||||
if [ ! -d "venv" ]; then
|
||||
echo "❌ 虚拟环境不存在,请先运行 ../install.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
source venv/bin/activate
|
||||
|
||||
# 快速导入测试
|
||||
echo "1. 测试模块导入..."
|
||||
python3 << 'EOF'
|
||||
try:
|
||||
# 测试基础模块
|
||||
from app.config import get_settings
|
||||
print(" ✓ 配置模块")
|
||||
|
||||
from app.models.database import Base, Message
|
||||
print(" ✓ 数据库模型")
|
||||
|
||||
from app.services.cache_service import cache_service
|
||||
print(" ✓ 缓存服务")
|
||||
|
||||
from app.services.tushare_service import tushare_service
|
||||
print(" ✓ Tushare服务")
|
||||
|
||||
from app.utils.stock_names import search_stock_by_name
|
||||
print(" ✓ 股票名称库")
|
||||
|
||||
from app.services.llm_service import llm_service
|
||||
print(" ✓ LLM服务")
|
||||
|
||||
from app.agent.enhanced_agent import enhanced_agent
|
||||
print(" ✓ 增强版Agent")
|
||||
|
||||
print("\n所有模块导入成功!")
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ 导入失败: {e}")
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
exit(1)
|
||||
EOF
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo ""
|
||||
echo "模块导入失败,请检查错误信息"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查配置
|
||||
echo ""
|
||||
echo "2. 检查配置..."
|
||||
python3 << 'EOF'
|
||||
from app.config import get_settings
|
||||
settings = get_settings()
|
||||
|
||||
print(f" Tushare Token: {'✓ 已配置' if settings.tushare_token else '❌ 未配置'}")
|
||||
print(f" 智谱AI Key: {'✓ 已配置' if settings.zhipuai_api_key else '❌ 未配置'}")
|
||||
print(f" 数据库: {settings.database_url}")
|
||||
print(f" 监听: {settings.api_host}:{settings.api_port}")
|
||||
|
||||
if not settings.tushare_token:
|
||||
print("\n⚠️ 警告: Tushare Token未配置,数据查询功能将不可用")
|
||||
if not settings.zhipuai_api_key:
|
||||
print("⚠️ 警告: 智谱AI Key未配置,将使用规则模式(无AI分析)")
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
echo "3. 测试股票名称识别..."
|
||||
python3 << 'EOF'
|
||||
from app.utils.stock_names import search_stock_by_name
|
||||
|
||||
test_cases = [
|
||||
("中国卫通", "601698"),
|
||||
("贵州茅台", "600519"),
|
||||
("比亚迪", "002594"),
|
||||
("宁德时代", "300750")
|
||||
]
|
||||
|
||||
for name, expected in test_cases:
|
||||
result = search_stock_by_name(name)
|
||||
status = "✓" if result == expected else "❌"
|
||||
print(f" {status} {name} -> {result}")
|
||||
EOF
|
||||
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "检查完成!准备启动..."
|
||||
echo "================================"
|
||||
echo ""
|
||||
echo "访问地址:"
|
||||
echo " 前端: http://localhost:8000"
|
||||
echo " API: http://localhost:8000/docs"
|
||||
echo ""
|
||||
echo "按 Ctrl+C 停止服务"
|
||||
echo ""
|
||||
|
||||
# 启动应用
|
||||
python3 -m app.main
|
||||
64
backend/start.sh
Executable file
64
backend/start.sh
Executable file
@ -0,0 +1,64 @@
|
||||
#!/bin/bash
|
||||
|
||||
# A股AI分析Agent系统 - 启动脚本(改进版)
|
||||
|
||||
echo "================================"
|
||||
echo "A股AI分析Agent系统"
|
||||
echo "================================"
|
||||
echo ""
|
||||
|
||||
# 检查.env文件
|
||||
if [ ! -f "../.env" ] && [ ! -f ".env" ]; then
|
||||
echo "❌ 错误: 未找到.env配置文件"
|
||||
echo ""
|
||||
echo "请先配置环境变量:"
|
||||
echo " cd .."
|
||||
echo " cp .env.example .env"
|
||||
echo " # 编辑.env文件,填写API密钥"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查虚拟环境
|
||||
if [ ! -d "venv" ]; then
|
||||
echo "❌ 错误: 虚拟环境不存在"
|
||||
echo ""
|
||||
echo "请先运行安装脚本:"
|
||||
echo " cd .."
|
||||
echo " ./install.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 激活虚拟环境
|
||||
echo "激活虚拟环境..."
|
||||
source venv/bin/activate
|
||||
|
||||
# 检查Python版本
|
||||
python_version=$(python --version 2>&1 | awk '{print $2}')
|
||||
echo "Python版本: $python_version"
|
||||
|
||||
# 显示配置信息
|
||||
echo ""
|
||||
echo "配置信息:"
|
||||
python -c "
|
||||
from app.config import get_settings
|
||||
settings = get_settings()
|
||||
print(f' Tushare Token: {'已配置' if settings.tushare_token else '未配置'}')
|
||||
print(f' 智谱AI Key: {'已配置' if settings.zhipuai_api_key else '未配置'}')
|
||||
print(f' 数据库: {settings.database_url}')
|
||||
print(f' 监听地址: {settings.api_host}:{settings.api_port}')
|
||||
"
|
||||
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "启动服务..."
|
||||
echo "================================"
|
||||
echo ""
|
||||
echo "访问地址:"
|
||||
echo " 前端界面: http://localhost:8000"
|
||||
echo " API文档: http://localhost:8000/docs"
|
||||
echo ""
|
||||
echo "按 Ctrl+C 停止服务"
|
||||
echo ""
|
||||
|
||||
# 启动应用
|
||||
python -m app.main
|
||||
26
backend/test_import.sh
Executable file
26
backend/test_import.sh
Executable file
@ -0,0 +1,26 @@
|
||||
#!/bin/bash
|
||||
# 测试应用启动
|
||||
|
||||
cd /Users/aaron/source_code/Stock_Agent/backend
|
||||
|
||||
# 激活虚拟环境
|
||||
source venv/bin/activate
|
||||
|
||||
# 测试导入
|
||||
echo "测试数据库模型..."
|
||||
python3 -c "from app.models.database import Base, Message; print('✓ 数据库模型导入成功')"
|
||||
|
||||
echo ""
|
||||
echo "测试配置..."
|
||||
python3 -c "from app.config import get_settings; print('✓ 配置加载成功')"
|
||||
|
||||
echo ""
|
||||
echo "测试服务..."
|
||||
python3 -c "from app.services.cache_service import cache_service; print('✓ 缓存服务初始化成功')"
|
||||
|
||||
echo ""
|
||||
echo "测试Agent..."
|
||||
python3 -c "from app.agent.core import stock_agent; print('✓ Agent初始化成功')"
|
||||
|
||||
echo ""
|
||||
echo "所有测试通过!可以启动应用了。"
|
||||
0
backend/tests/__init__.py
Normal file
0
backend/tests/__init__.py
Normal file
491
docs/DEPLOYMENT.md
Normal file
491
docs/DEPLOYMENT.md
Normal file
@ -0,0 +1,491 @@
|
||||
# 部署文档
|
||||
|
||||
本文档介绍如何部署A股AI分析Agent系统到生产环境。
|
||||
|
||||
## 部署方式
|
||||
|
||||
### 方式一:本地部署
|
||||
|
||||
#### 1. 系统要求
|
||||
|
||||
- 操作系统:Linux/macOS/Windows
|
||||
- Python 3.9+
|
||||
- Redis 6.0+(可选)
|
||||
- 内存:至少2GB
|
||||
- 磁盘:至少1GB
|
||||
|
||||
#### 2. 安装步骤
|
||||
|
||||
```bash
|
||||
# 1. 克隆代码
|
||||
git clone <repository_url>
|
||||
cd Stock_Agent
|
||||
|
||||
# 2. 创建虚拟环境
|
||||
python -m venv venv
|
||||
source venv/bin/activate # Windows: venv\Scripts\activate
|
||||
|
||||
# 3. 安装依赖
|
||||
cd backend
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 4. 配置环境变量
|
||||
cp ../.env.example ../.env
|
||||
# 编辑.env文件,填写必要的配置
|
||||
|
||||
# 5. 启动Redis(可选)
|
||||
redis-server
|
||||
|
||||
# 6. 启动应用
|
||||
python -m app.main
|
||||
```
|
||||
|
||||
#### 3. 使用进程管理器
|
||||
|
||||
**使用Supervisor(推荐)**
|
||||
|
||||
创建配置文件 `/etc/supervisor/conf.d/stock_agent.conf`:
|
||||
|
||||
```ini
|
||||
[program:stock_agent]
|
||||
directory=/path/to/Stock_Agent/backend
|
||||
command=/path/to/venv/bin/python -m app.main
|
||||
user=your_user
|
||||
autostart=true
|
||||
autorestart=true
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/var/log/stock_agent.log
|
||||
```
|
||||
|
||||
启动服务:
|
||||
|
||||
```bash
|
||||
sudo supervisorctl reread
|
||||
sudo supervisorctl update
|
||||
sudo supervisorctl start stock_agent
|
||||
```
|
||||
|
||||
**使用systemd**
|
||||
|
||||
创建服务文件 `/etc/systemd/system/stock_agent.service`:
|
||||
|
||||
```ini
|
||||
[Unit]
|
||||
Description=Stock Agent Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
User=your_user
|
||||
WorkingDirectory=/path/to/Stock_Agent/backend
|
||||
Environment="PATH=/path/to/venv/bin"
|
||||
ExecStart=/path/to/venv/bin/python -m app.main
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
```
|
||||
|
||||
启动服务:
|
||||
|
||||
```bash
|
||||
sudo systemctl daemon-reload
|
||||
sudo systemctl enable stock_agent
|
||||
sudo systemctl start stock_agent
|
||||
```
|
||||
|
||||
### 方式二:Docker部署
|
||||
|
||||
#### 1. 创建Dockerfile
|
||||
|
||||
在项目根目录创建 `Dockerfile`:
|
||||
|
||||
```dockerfile
|
||||
FROM python:3.9-slim
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 安装依赖
|
||||
COPY backend/requirements.txt .
|
||||
RUN pip install --no-cache-dir -r requirements.txt
|
||||
|
||||
# 复制代码
|
||||
COPY backend/ ./backend/
|
||||
COPY frontend/ ./frontend/
|
||||
COPY .env .env
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 8000
|
||||
|
||||
# 启动应用
|
||||
CMD ["python", "-m", "backend.app.main"]
|
||||
```
|
||||
|
||||
#### 2. 创建docker-compose.yml
|
||||
|
||||
```yaml
|
||||
version: '3.8'
|
||||
|
||||
services:
|
||||
redis:
|
||||
image: redis:6-alpine
|
||||
ports:
|
||||
- "6379:6379"
|
||||
volumes:
|
||||
- redis_data:/data
|
||||
|
||||
stock_agent:
|
||||
build: .
|
||||
ports:
|
||||
- "8000:8000"
|
||||
environment:
|
||||
- REDIS_HOST=redis
|
||||
- REDIS_PORT=6379
|
||||
env_file:
|
||||
- .env
|
||||
depends_on:
|
||||
- redis
|
||||
volumes:
|
||||
- ./backend:/app/backend
|
||||
- ./frontend:/app/frontend
|
||||
- ./stock_agent.db:/app/stock_agent.db
|
||||
|
||||
volumes:
|
||||
redis_data:
|
||||
```
|
||||
|
||||
#### 3. 启动服务
|
||||
|
||||
```bash
|
||||
# 构建镜像
|
||||
docker-compose build
|
||||
|
||||
# 启动服务
|
||||
docker-compose up -d
|
||||
|
||||
# 查看日志
|
||||
docker-compose logs -f
|
||||
|
||||
# 停止服务
|
||||
docker-compose down
|
||||
```
|
||||
|
||||
### 方式三:云服务器部署
|
||||
|
||||
#### 阿里云/腾讯云部署
|
||||
|
||||
1. **购买云服务器**
|
||||
- 配置:2核4GB内存
|
||||
- 系统:Ubuntu 20.04 LTS
|
||||
|
||||
2. **安全组配置**
|
||||
- 开放8000端口(HTTP)
|
||||
- 开放22端口(SSH)
|
||||
|
||||
3. **安装环境**
|
||||
|
||||
```bash
|
||||
# 更新系统
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
|
||||
# 安装Python
|
||||
sudo apt install python3.9 python3.9-venv python3-pip -y
|
||||
|
||||
# 安装Redis
|
||||
sudo apt install redis-server -y
|
||||
sudo systemctl enable redis-server
|
||||
sudo systemctl start redis-server
|
||||
|
||||
# 安装Nginx(可选,用于反向代理)
|
||||
sudo apt install nginx -y
|
||||
```
|
||||
|
||||
4. **部署应用**
|
||||
|
||||
按照"本地部署"步骤进行。
|
||||
|
||||
5. **配置Nginx反向代理**
|
||||
|
||||
创建配置文件 `/etc/nginx/sites-available/stock_agent`:
|
||||
|
||||
```nginx
|
||||
server {
|
||||
listen 80;
|
||||
server_name your_domain.com;
|
||||
|
||||
location / {
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
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;
|
||||
}
|
||||
|
||||
location /static {
|
||||
alias /path/to/Stock_Agent/frontend;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
启用配置:
|
||||
|
||||
```bash
|
||||
sudo ln -s /etc/nginx/sites-available/stock_agent /etc/nginx/sites-enabled/
|
||||
sudo nginx -t
|
||||
sudo systemctl reload nginx
|
||||
```
|
||||
|
||||
6. **配置HTTPS(推荐)**
|
||||
|
||||
使用Let's Encrypt免费证书:
|
||||
|
||||
```bash
|
||||
sudo apt install certbot python3-certbot-nginx -y
|
||||
sudo certbot --nginx -d your_domain.com
|
||||
```
|
||||
|
||||
## 生产环境配置
|
||||
|
||||
### 1. 环境变量配置
|
||||
|
||||
生产环境的 `.env` 配置:
|
||||
|
||||
```env
|
||||
# API密钥
|
||||
TUSHARE_TOKEN=your_production_token
|
||||
ZHIPUAI_API_KEY=your_production_key
|
||||
|
||||
# Redis
|
||||
REDIS_HOST=localhost
|
||||
REDIS_PORT=6379
|
||||
REDIS_PASSWORD=your_redis_password
|
||||
|
||||
# 数据库(生产环境建议使用PostgreSQL)
|
||||
DATABASE_URL=postgresql://user:password@localhost/stock_agent
|
||||
|
||||
# API设置
|
||||
API_HOST=0.0.0.0
|
||||
API_PORT=8000
|
||||
DEBUG=False
|
||||
|
||||
# 安全
|
||||
SECRET_KEY=your_very_long_random_secret_key_here
|
||||
RATE_LIMIT=100/minute
|
||||
|
||||
# CORS(根据实际域名配置)
|
||||
CORS_ORIGINS=https://your_domain.com
|
||||
```
|
||||
|
||||
### 2. 数据库迁移到PostgreSQL
|
||||
|
||||
安装PostgreSQL:
|
||||
|
||||
```bash
|
||||
sudo apt install postgresql postgresql-contrib -y
|
||||
```
|
||||
|
||||
创建数据库:
|
||||
|
||||
```sql
|
||||
sudo -u postgres psql
|
||||
CREATE DATABASE stock_agent;
|
||||
CREATE USER stock_user WITH PASSWORD 'your_password';
|
||||
GRANT ALL PRIVILEGES ON DATABASE stock_agent TO stock_user;
|
||||
\q
|
||||
```
|
||||
|
||||
更新 `.env` 中的 `DATABASE_URL`。
|
||||
|
||||
### 3. 性能优化
|
||||
|
||||
**Redis配置优化**
|
||||
|
||||
编辑 `/etc/redis/redis.conf`:
|
||||
|
||||
```conf
|
||||
maxmemory 256mb
|
||||
maxmemory-policy allkeys-lru
|
||||
save 900 1
|
||||
save 300 10
|
||||
save 60 10000
|
||||
```
|
||||
|
||||
**应用配置优化**
|
||||
|
||||
在 `config.py` 中调整:
|
||||
|
||||
```python
|
||||
# 增加工作进程数
|
||||
workers = multiprocessing.cpu_count() * 2 + 1
|
||||
|
||||
# 调整超时时间
|
||||
timeout = 120
|
||||
```
|
||||
|
||||
### 4. 监控和日志
|
||||
|
||||
**日志配置**
|
||||
|
||||
在 `utils/logger.py` 中配置日志文件:
|
||||
|
||||
```python
|
||||
logger = setup_logger(
|
||||
name="stock_agent",
|
||||
level=logging.INFO,
|
||||
log_file="/var/log/stock_agent/app.log"
|
||||
)
|
||||
```
|
||||
|
||||
**日志轮转**
|
||||
|
||||
创建 `/etc/logrotate.d/stock_agent`:
|
||||
|
||||
```
|
||||
/var/log/stock_agent/*.log {
|
||||
daily
|
||||
rotate 7
|
||||
compress
|
||||
delaycompress
|
||||
notifempty
|
||||
create 0640 your_user your_user
|
||||
sharedscripts
|
||||
}
|
||||
```
|
||||
|
||||
**监控工具**
|
||||
|
||||
推荐使用:
|
||||
- Prometheus + Grafana(系统监控)
|
||||
- Sentry(错误追踪)
|
||||
- ELK Stack(日志分析)
|
||||
|
||||
## 备份和恢复
|
||||
|
||||
### 数据库备份
|
||||
|
||||
**SQLite备份**
|
||||
|
||||
```bash
|
||||
# 备份
|
||||
cp stock_agent.db stock_agent_backup_$(date +%Y%m%d).db
|
||||
|
||||
# 恢复
|
||||
cp stock_agent_backup_20240101.db stock_agent.db
|
||||
```
|
||||
|
||||
**PostgreSQL备份**
|
||||
|
||||
```bash
|
||||
# 备份
|
||||
pg_dump -U stock_user stock_agent > backup_$(date +%Y%m%d).sql
|
||||
|
||||
# 恢复
|
||||
psql -U stock_user stock_agent < backup_20240101.sql
|
||||
```
|
||||
|
||||
### Redis备份
|
||||
|
||||
```bash
|
||||
# 备份
|
||||
redis-cli SAVE
|
||||
cp /var/lib/redis/dump.rdb /backup/redis_$(date +%Y%m%d).rdb
|
||||
|
||||
# 恢复
|
||||
sudo systemctl stop redis
|
||||
cp /backup/redis_20240101.rdb /var/lib/redis/dump.rdb
|
||||
sudo systemctl start redis
|
||||
```
|
||||
|
||||
## 安全加固
|
||||
|
||||
### 1. 防火墙配置
|
||||
|
||||
```bash
|
||||
# 使用ufw
|
||||
sudo ufw allow 22/tcp
|
||||
sudo ufw allow 80/tcp
|
||||
sudo ufw allow 443/tcp
|
||||
sudo ufw enable
|
||||
```
|
||||
|
||||
### 2. 限制API访问
|
||||
|
||||
在Nginx中配置限流:
|
||||
|
||||
```nginx
|
||||
limit_req_zone $binary_remote_addr zone=api_limit:10m rate=10r/s;
|
||||
|
||||
location /api/ {
|
||||
limit_req zone=api_limit burst=20 nodelay;
|
||||
proxy_pass http://127.0.0.1:8000;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 定期更新
|
||||
|
||||
```bash
|
||||
# 更新系统
|
||||
sudo apt update && sudo apt upgrade -y
|
||||
|
||||
# 更新Python依赖
|
||||
pip install --upgrade -r requirements.txt
|
||||
```
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 常见问题
|
||||
|
||||
1. **服务无法启动**
|
||||
- 检查端口是否被占用:`lsof -i :8000`
|
||||
- 查看日志:`tail -f /var/log/stock_agent.log`
|
||||
|
||||
2. **Redis连接失败**
|
||||
- 检查Redis状态:`sudo systemctl status redis`
|
||||
- 测试连接:`redis-cli ping`
|
||||
|
||||
3. **数据库错误**
|
||||
- 检查数据库连接:`psql -U stock_user -d stock_agent`
|
||||
- 查看数据库日志:`sudo tail -f /var/log/postgresql/postgresql-*.log`
|
||||
|
||||
4. **API响应慢**
|
||||
- 检查Redis缓存是否正常
|
||||
- 查看系统资源:`htop`
|
||||
- 分析慢查询日志
|
||||
|
||||
## 性能测试
|
||||
|
||||
使用Apache Bench进行压力测试:
|
||||
|
||||
```bash
|
||||
# 安装ab
|
||||
sudo apt install apache2-utils -y
|
||||
|
||||
# 测试API
|
||||
ab -n 1000 -c 50 http://localhost:8000/api/stock/quote/600519
|
||||
```
|
||||
|
||||
## 更新部署
|
||||
|
||||
### 零停机更新
|
||||
|
||||
使用蓝绿部署或滚动更新:
|
||||
|
||||
```bash
|
||||
# 1. 拉取最新代码
|
||||
git pull origin main
|
||||
|
||||
# 2. 安装新依赖
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 3. 运行数据库迁移(如有)
|
||||
# python manage.py migrate
|
||||
|
||||
# 4. 重启服务
|
||||
sudo supervisorctl restart stock_agent
|
||||
# 或
|
||||
sudo systemctl restart stock_agent
|
||||
```
|
||||
|
||||
## 联系支持
|
||||
|
||||
如遇到部署问题,请提交Issue或联系技术支持。
|
||||
262
docs/INSTALL_GUIDE.md
Normal file
262
docs/INSTALL_GUIDE.md
Normal file
@ -0,0 +1,262 @@
|
||||
# 安装问题解决指南
|
||||
|
||||
## 问题:Python 3.13 兼容性问题
|
||||
|
||||
如果您在安装依赖时遇到 numpy/pandas 编译错误,这是因为 Python 3.13 是最新版本,部分科学计算库还未完全适配。
|
||||
|
||||
### 错误信息示例
|
||||
```
|
||||
fatal error: 'type_traits' file not found
|
||||
ERROR: Failed to build 'pandas' when installing build dependencies
|
||||
```
|
||||
|
||||
## 解决方案
|
||||
|
||||
### 方案1:使用 Python 3.11 或 3.12(强烈推荐)
|
||||
|
||||
这是最简单可靠的方法。
|
||||
|
||||
#### macOS (使用 Homebrew)
|
||||
|
||||
```bash
|
||||
# 1. 安装 Python 3.11
|
||||
brew install python@3.11
|
||||
|
||||
# 2. 进入项目目录
|
||||
cd /Users/aaron/source_code/Stock_Agent/backend
|
||||
|
||||
# 3. 删除旧的虚拟环境(如果存在)
|
||||
rm -rf venv
|
||||
|
||||
# 4. 使用 Python 3.11 创建新的虚拟环境
|
||||
python3.11 -m venv venv
|
||||
|
||||
# 5. 激活虚拟环境
|
||||
source venv/bin/activate
|
||||
|
||||
# 6. 升级 pip
|
||||
pip install --upgrade pip
|
||||
|
||||
# 7. 安装依赖
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
#### Linux (Ubuntu/Debian)
|
||||
|
||||
```bash
|
||||
# 1. 安装 Python 3.11
|
||||
sudo apt update
|
||||
sudo apt install python3.11 python3.11-venv python3.11-dev
|
||||
|
||||
# 2. 创建虚拟环境
|
||||
python3.11 -m venv venv
|
||||
source venv/bin/activate
|
||||
|
||||
# 3. 安装依赖
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
#### Windows
|
||||
|
||||
```powershell
|
||||
# 1. 从 python.org 下载并安装 Python 3.11
|
||||
# https://www.python.org/downloads/
|
||||
|
||||
# 2. 创建虚拟环境
|
||||
py -3.11 -m venv venv
|
||||
|
||||
# 3. 激活虚拟环境
|
||||
venv\Scripts\activate
|
||||
|
||||
# 4. 安装依赖
|
||||
pip install --upgrade pip
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 方案2:使用预编译的 wheel 包(Python 3.13)
|
||||
|
||||
如果必须使用 Python 3.13,可以尝试安装预编译的包:
|
||||
|
||||
```bash
|
||||
# 激活虚拟环境
|
||||
source venv/bin/activate # macOS/Linux
|
||||
# 或
|
||||
venv\Scripts\activate # Windows
|
||||
|
||||
# 先单独安装 numpy 和 pandas
|
||||
pip install --upgrade pip
|
||||
pip install numpy --only-binary :all:
|
||||
pip install pandas --only-binary :all:
|
||||
|
||||
# 然后安装其他依赖
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
### 方案3:使用 Conda(推荐用于数据科学项目)
|
||||
|
||||
Conda 提供预编译的包,避免编译问题:
|
||||
|
||||
```bash
|
||||
# 1. 安装 Miniconda 或 Anaconda
|
||||
# https://docs.conda.io/en/latest/miniconda.html
|
||||
|
||||
# 2. 创建环境
|
||||
conda create -n stock_agent python=3.11
|
||||
|
||||
# 3. 激活环境
|
||||
conda activate stock_agent
|
||||
|
||||
# 4. 安装依赖
|
||||
pip install -r requirements.txt
|
||||
```
|
||||
|
||||
## 验证安装
|
||||
|
||||
安装完成后,验证是否成功:
|
||||
|
||||
```bash
|
||||
# 检查 Python 版本
|
||||
python --version
|
||||
# 应该显示 Python 3.11.x 或 3.12.x
|
||||
|
||||
# 检查关键包
|
||||
python -c "import numpy; print('numpy:', numpy.__version__)"
|
||||
python -c "import pandas; print('pandas:', pandas.__version__)"
|
||||
python -c "import fastapi; print('fastapi:', fastapi.__version__)"
|
||||
python -c "import tushare; print('tushare:', tushare.__version__)"
|
||||
```
|
||||
|
||||
## 启动应用
|
||||
|
||||
安装成功后,按以下步骤启动:
|
||||
|
||||
```bash
|
||||
# 1. 确保在虚拟环境中
|
||||
source venv/bin/activate # macOS/Linux
|
||||
# 或
|
||||
venv\Scripts\activate # Windows
|
||||
|
||||
# 2. 配置环境变量
|
||||
cd /Users/aaron/source_code/Stock_Agent
|
||||
cp .env.example .env
|
||||
# 编辑 .env 文件,填写 API 密钥
|
||||
|
||||
# 3. 启动应用
|
||||
cd backend
|
||||
python -m app.main
|
||||
```
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 如何检查当前 Python 版本?
|
||||
|
||||
```bash
|
||||
python --version
|
||||
python3 --version
|
||||
python3.11 --version
|
||||
```
|
||||
|
||||
### Q2: 如何切换 Python 版本?
|
||||
|
||||
macOS/Linux:
|
||||
```bash
|
||||
# 使用特定版本创建虚拟环境
|
||||
python3.11 -m venv venv
|
||||
```
|
||||
|
||||
Windows:
|
||||
```powershell
|
||||
py -3.11 -m venv venv
|
||||
```
|
||||
|
||||
### Q3: 虚拟环境激活失败?
|
||||
|
||||
确保在正确的目录:
|
||||
```bash
|
||||
cd /Users/aaron/source_code/Stock_Agent/backend
|
||||
ls venv # 应该能看到 bin 或 Scripts 目录
|
||||
```
|
||||
|
||||
### Q4: pip 安装很慢?
|
||||
|
||||
使用国内镜像源:
|
||||
```bash
|
||||
pip install -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
|
||||
```
|
||||
|
||||
### Q5: 权限错误?
|
||||
|
||||
不要使用 sudo,确保在虚拟环境中:
|
||||
```bash
|
||||
which python # 应该显示虚拟环境路径
|
||||
```
|
||||
|
||||
## 推荐的完整安装流程
|
||||
|
||||
```bash
|
||||
# 1. 安装 Python 3.11
|
||||
brew install python@3.11 # macOS
|
||||
|
||||
# 2. 进入项目目录
|
||||
cd /Users/aaron/source_code/Stock_Agent
|
||||
|
||||
# 3. 创建虚拟环境
|
||||
python3.11 -m venv backend/venv
|
||||
|
||||
# 4. 激活虚拟环境
|
||||
source backend/venv/bin/activate
|
||||
|
||||
# 5. 升级 pip
|
||||
pip install --upgrade pip setuptools wheel
|
||||
|
||||
# 6. 安装依赖
|
||||
cd backend
|
||||
pip install -r requirements.txt
|
||||
|
||||
# 7. 配置环境变量
|
||||
cd ..
|
||||
cp .env.example .env
|
||||
nano .env # 或使用其他编辑器
|
||||
|
||||
# 8. 启动应用
|
||||
cd backend
|
||||
python -m app.main
|
||||
```
|
||||
|
||||
## 获取帮助
|
||||
|
||||
如果仍然遇到问题:
|
||||
|
||||
1. 检查 Python 版本:`python --version`
|
||||
2. 检查虚拟环境:`which python`
|
||||
3. 查看完整错误信息
|
||||
4. 提交 Issue 到项目仓库
|
||||
|
||||
## 最小依赖版本
|
||||
|
||||
如果遇到版本冲突,可以尝试最小版本:
|
||||
|
||||
```txt
|
||||
fastapi>=0.100.0
|
||||
uvicorn>=0.23.0
|
||||
langchain>=0.1.0
|
||||
tushare>=1.3.0
|
||||
sqlalchemy>=2.0.0
|
||||
pydantic>=2.0.0
|
||||
pandas>=2.0.0
|
||||
numpy>=1.24.0
|
||||
```
|
||||
|
||||
## 成功标志
|
||||
|
||||
当您看到以下输出时,说明安装成功:
|
||||
|
||||
```
|
||||
INFO: Started server process [xxxxx]
|
||||
INFO: Waiting for application startup.
|
||||
INFO: Application startup complete.
|
||||
INFO: Uvicorn running on http://0.0.0.0:8000
|
||||
```
|
||||
|
||||
然后访问 http://localhost:8000 即可使用系统!
|
||||
343
docs/USER_GUIDE.md
Normal file
343
docs/USER_GUIDE.md
Normal file
@ -0,0 +1,343 @@
|
||||
# 用户使用手册
|
||||
|
||||
欢迎使用A股AI分析Agent系统!本手册将帮助您快速上手。
|
||||
|
||||
## 目录
|
||||
|
||||
1. [系统介绍](#系统介绍)
|
||||
2. [快速开始](#快速开始)
|
||||
3. [功能使用](#功能使用)
|
||||
4. [常见问题](#常见问题)
|
||||
5. [技巧和建议](#技巧和建议)
|
||||
|
||||
## 系统介绍
|
||||
|
||||
A股AI分析Agent系统是一个智能股票分析助手,通过自然语言对话方式,帮助您:
|
||||
|
||||
- 查询股票实时行情
|
||||
- 分析技术指标
|
||||
- 查看K线图表
|
||||
- 了解公司基本信息
|
||||
|
||||
### 核心特性
|
||||
|
||||
- **自然对话**:像聊天一样查询股票信息
|
||||
- **智能理解**:自动识别股票代码和查询意图
|
||||
- **实时数据**:获取最新的市场数据
|
||||
- **专业图表**:生成专业的K线图和技术指标图
|
||||
- **历史记录**:保存对话历史,方便回顾
|
||||
|
||||
## 快速开始
|
||||
|
||||
### 1. 访问系统
|
||||
|
||||
打开浏览器,访问:http://localhost:8000
|
||||
|
||||
### 2. 界面介绍
|
||||
|
||||
系统界面分为三个主要区域:
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────┐
|
||||
│ 📈 A股AI分析Agent [技能管理] │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ │
|
||||
│ 消息显示区域 │
|
||||
│ - 显示对话历史 │
|
||||
│ - 展示图表和数据 │
|
||||
│ │
|
||||
├─────────────────────────────────────────────┤
|
||||
│ [输入框] [发送] │
|
||||
└─────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
### 3. 第一次查询
|
||||
|
||||
在输入框中输入:
|
||||
|
||||
```
|
||||
查询600519的实时行情
|
||||
```
|
||||
|
||||
按回车或点击"发送"按钮,系统会返回贵州茅台的实时行情数据。
|
||||
|
||||
## 功能使用
|
||||
|
||||
### 1. 查询实时行情
|
||||
|
||||
**支持的查询方式**:
|
||||
|
||||
```
|
||||
查询600519的实时行情
|
||||
贵州茅台的价格
|
||||
000001现在多少钱
|
||||
中国平安的行情
|
||||
```
|
||||
|
||||
**返回信息**:
|
||||
- 股票名称和代码
|
||||
- 最新价格
|
||||
- 涨跌额和涨跌幅
|
||||
- 开盘价、最高价、最低价
|
||||
- 成交量和成交额
|
||||
|
||||
**示例**:
|
||||
|
||||
```
|
||||
输入:查询600519的实时行情
|
||||
|
||||
输出:
|
||||
【贵州茅台】(600519.SH)
|
||||
交易日期:20240201
|
||||
最新价:1650.00
|
||||
涨跌额:15.50
|
||||
涨跌幅:0.95%
|
||||
开盘价:1640.00
|
||||
最高价:1655.00
|
||||
最低价:1638.00
|
||||
成交量:125000手
|
||||
成交额:206250千元
|
||||
```
|
||||
|
||||
### 2. 查看K线图
|
||||
|
||||
**支持的查询方式**:
|
||||
|
||||
```
|
||||
600519的K线图
|
||||
贵州茅台的走势
|
||||
000001的图表
|
||||
```
|
||||
|
||||
**功能特点**:
|
||||
- 显示最近60个交易日的K线
|
||||
- 包含成交量柱状图
|
||||
- 支持缩放和拖动
|
||||
- 红色表示上涨,绿色表示下跌
|
||||
|
||||
**操作技巧**:
|
||||
- 鼠标滚轮:缩放图表
|
||||
- 鼠标拖动:移动时间轴
|
||||
- 双击:重置视图
|
||||
|
||||
### 3. 技术指标分析
|
||||
|
||||
**支持的查询方式**:
|
||||
|
||||
```
|
||||
600519的技术指标
|
||||
分析贵州茅台的MACD
|
||||
000001的RSI
|
||||
```
|
||||
|
||||
**支持的指标**:
|
||||
|
||||
1. **均线(MA)**
|
||||
- MA5:5日均线
|
||||
- MA10:10日均线
|
||||
- MA20:20日均线
|
||||
- MA60:60日均线
|
||||
|
||||
2. **MACD**
|
||||
- DIF:快线
|
||||
- DEA:慢线
|
||||
- MACD:柱状图
|
||||
|
||||
3. **RSI(相对强弱指标)**
|
||||
- RSI6:6日RSI
|
||||
- RSI12:12日RSI
|
||||
- RSI24:24日RSI
|
||||
|
||||
4. **KDJ**
|
||||
- K值
|
||||
- D值
|
||||
- J值
|
||||
|
||||
5. **布林带(BOLL)**
|
||||
- 上轨
|
||||
- 中轨
|
||||
- 下轨
|
||||
|
||||
**示例**:
|
||||
|
||||
```
|
||||
输入:600519的技术指标
|
||||
|
||||
输出:
|
||||
【600519.SH】技术指标:
|
||||
均线:MA5=1645.20, MA10=1638.50, MA20=1625.30
|
||||
MACD:DIF=12.50, DEA=10.20, MACD=4.60
|
||||
RSI:RSI6=65.20, RSI12=58.30, RSI24=52.10
|
||||
```
|
||||
|
||||
### 4. 基本面信息
|
||||
|
||||
**支持的查询方式**:
|
||||
|
||||
```
|
||||
600519的基本信息
|
||||
贵州茅台是什么行业
|
||||
000001的公司信息
|
||||
```
|
||||
|
||||
**返回信息**:
|
||||
- 股票代码和名称
|
||||
- 所属地域
|
||||
- 所属行业
|
||||
- 上市市场
|
||||
- 上市日期
|
||||
|
||||
**示例**:
|
||||
|
||||
```
|
||||
输入:600519的基本信息
|
||||
|
||||
输出:
|
||||
【贵州茅台】基本信息
|
||||
股票代码:600519.SH
|
||||
所属地域:贵州
|
||||
所属行业:白酒
|
||||
上市市场:主板
|
||||
上市日期:20010827
|
||||
```
|
||||
|
||||
### 5. 技能管理
|
||||
|
||||
点击右上角"技能管理"按钮,可以:
|
||||
|
||||
- **查看所有技能**:显示系统支持的所有分析技能
|
||||
- **启用/禁用技能**:通过开关控制技能的启用状态
|
||||
- **查看技能说明**:了解每个技能的功能
|
||||
|
||||
**可用技能**:
|
||||
|
||||
1. **market_data**:行情查询技能
|
||||
2. **technical_analysis**:技术分析技能
|
||||
3. **fundamental**:基本面分析技能
|
||||
4. **visualization**:数据可视化技能
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q1: 如何输入股票代码?
|
||||
|
||||
**A**: 支持多种格式:
|
||||
|
||||
- 6位数字:`600519`、`000001`
|
||||
- 带后缀:`600519.SH`、`000001.SZ`
|
||||
- 股票名称:`贵州茅台`、`中国平安`
|
||||
|
||||
### Q2: 为什么查询失败?
|
||||
|
||||
**可能原因**:
|
||||
|
||||
1. **股票代码错误**
|
||||
- 检查代码是否正确
|
||||
- 确认是A股代码
|
||||
|
||||
2. **数据源问题**
|
||||
- Tushare API可能暂时不可用
|
||||
- 检查网络连接
|
||||
|
||||
3. **非交易时间**
|
||||
- 某些数据仅在交易时间更新
|
||||
|
||||
### Q3: 数据更新频率?
|
||||
|
||||
- **实时行情**:缓存30秒
|
||||
- **K线数据**:缓存1小时
|
||||
- **基本面信息**:缓存1天
|
||||
|
||||
### Q4: 如何清除对话历史?
|
||||
|
||||
目前系统会自动保存对话历史。如需清除,可以:
|
||||
|
||||
1. 刷新页面(会生成新的会话ID)
|
||||
2. 联系管理员清理数据库
|
||||
|
||||
### Q5: 支持哪些股票市场?
|
||||
|
||||
当前版本支持:
|
||||
- 上海证券交易所(沪市)
|
||||
- 深圳证券交易所(深市)
|
||||
- 科创板
|
||||
|
||||
未来将支持:
|
||||
- 港股
|
||||
- 美股
|
||||
|
||||
## 技巧和建议
|
||||
|
||||
### 查询技巧
|
||||
|
||||
1. **明确查询意图**
|
||||
```
|
||||
好:查询600519的实时行情
|
||||
差:600519
|
||||
```
|
||||
|
||||
2. **使用完整股票代码**
|
||||
```
|
||||
好:600519
|
||||
差:6005(不完整)
|
||||
```
|
||||
|
||||
3. **一次查询一个股票**
|
||||
```
|
||||
好:查询600519的行情
|
||||
差:查询600519和000001的行情(暂不支持)
|
||||
```
|
||||
|
||||
### 分析建议
|
||||
|
||||
1. **结合多个指标**
|
||||
- 先查看K线图,了解整体趋势
|
||||
- 再查看技术指标,确认信号
|
||||
- 最后查看基本面,评估价值
|
||||
|
||||
2. **关注关键指标**
|
||||
- 均线:判断趋势方向
|
||||
- MACD:捕捉买卖信号
|
||||
- RSI:识别超买超卖
|
||||
- 成交量:确认趋势强度
|
||||
|
||||
3. **定期跟踪**
|
||||
- 建立自选股列表
|
||||
- 定期查询关注的股票
|
||||
- 记录重要的分析结果
|
||||
|
||||
### 使用限制
|
||||
|
||||
1. **API调用限制**
|
||||
- Tushare免费版:120次/分钟
|
||||
- 建议合理安排查询频率
|
||||
|
||||
2. **数据准确性**
|
||||
- 数据来源于Tushare
|
||||
- 仅供参考,不构成投资建议
|
||||
|
||||
3. **系统性能**
|
||||
- 首次查询可能较慢(需要获取数据)
|
||||
- 后续查询会使用缓存,速度更快
|
||||
|
||||
## 投资风险提示
|
||||
|
||||
⚠️ **重要提示**:
|
||||
|
||||
1. 本系统提供的数据和分析仅供参考
|
||||
2. 不构成任何投资建议
|
||||
3. 股市有风险,投资需谨慎
|
||||
4. 请根据自身情况做出投资决策
|
||||
5. 建议咨询专业投资顾问
|
||||
|
||||
## 反馈和支持
|
||||
|
||||
如有问题或建议,请:
|
||||
|
||||
1. 查看[README.md](../README.md)
|
||||
2. 查看[部署文档](DEPLOYMENT.md)
|
||||
3. 提交Issue到项目仓库
|
||||
4. 联系技术支持
|
||||
|
||||
---
|
||||
|
||||
感谢使用A股AI分析Agent系统!祝您投资顺利!
|
||||
551
frontend/css/style.css
Normal file
551
frontend/css/style.css
Normal file
@ -0,0 +1,551 @@
|
||||
/* Tesla-inspired Cyberpunk Minimal Design */
|
||||
|
||||
:root {
|
||||
--bg-primary: #000000;
|
||||
--bg-secondary: #0a0a0a;
|
||||
--bg-tertiary: #141414;
|
||||
--text-primary: #ffffff;
|
||||
--text-secondary: #a0a0a0;
|
||||
--text-tertiary: #666666;
|
||||
--accent: #00ff41;
|
||||
--accent-dim: #00ff4120;
|
||||
--border: #1a1a1a;
|
||||
--border-bright: #333333;
|
||||
}
|
||||
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: 'Inter', -apple-system, BlinkMacSystemFont, sans-serif;
|
||||
background: var(--bg-primary);
|
||||
color: var(--text-primary);
|
||||
font-size: 15px;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Container */
|
||||
.container {
|
||||
width: 100%;
|
||||
max-width: 900px;
|
||||
height: 100%;
|
||||
max-height: 900px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/* Header */
|
||||
.header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 20px 24px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.logo {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
font-size: 15px;
|
||||
font-weight: 500;
|
||||
letter-spacing: 0.5px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.logo svg {
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.status {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
font-size: 13px;
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 1px;
|
||||
}
|
||||
|
||||
.status-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
box-shadow: 0 0 8px var(--accent);
|
||||
animation: pulse 2s ease-in-out infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% { opacity: 1; }
|
||||
50% { opacity: 0.4; }
|
||||
}
|
||||
|
||||
/* Chat Container */
|
||||
.chat-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
/* Welcome Screen */
|
||||
.welcome {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
text-align: center;
|
||||
padding: 40px;
|
||||
}
|
||||
|
||||
.welcome-icon {
|
||||
margin-bottom: 32px;
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
.welcome-icon svg {
|
||||
stroke: var(--accent);
|
||||
}
|
||||
|
||||
.welcome h1 {
|
||||
font-size: 28px;
|
||||
font-weight: 300;
|
||||
letter-spacing: 1px;
|
||||
margin-bottom: 12px;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.welcome p {
|
||||
font-size: 14px;
|
||||
color: var(--text-secondary);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
/* Messages */
|
||||
.messages {
|
||||
padding: 32px 24px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 24px;
|
||||
}
|
||||
|
||||
.message {
|
||||
display: flex;
|
||||
animation: fadeIn 0.3s ease;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.message.user {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.message.assistant {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 85%;
|
||||
}
|
||||
|
||||
.message.user .message-content {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border-bright);
|
||||
border-radius: 2px;
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.message.assistant .message-content {
|
||||
background: transparent;
|
||||
border-left: 2px solid var(--accent);
|
||||
padding: 0 0 0 20px;
|
||||
}
|
||||
|
||||
.text {
|
||||
font-size: 14px;
|
||||
line-height: 1.7;
|
||||
color: var(--text-primary);
|
||||
word-wrap: break-word;
|
||||
}
|
||||
|
||||
.message.user .text {
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Markdown Styles */
|
||||
.markdown h1,
|
||||
.markdown h2,
|
||||
.markdown h3,
|
||||
.markdown h4 {
|
||||
color: var(--text-primary);
|
||||
font-weight: 500;
|
||||
margin: 20px 0 12px 0;
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.markdown h1 {
|
||||
font-size: 20px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.markdown h2 {
|
||||
font-size: 18px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.markdown h3 {
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.markdown p {
|
||||
margin: 12px 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.markdown ul,
|
||||
.markdown ol {
|
||||
margin: 12px 0;
|
||||
padding-left: 24px;
|
||||
}
|
||||
|
||||
.markdown li {
|
||||
margin: 6px 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.markdown code {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
padding: 2px 6px;
|
||||
border-radius: 2px;
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
font-size: 13px;
|
||||
color: var(--accent);
|
||||
}
|
||||
|
||||
.markdown pre {
|
||||
background: var(--bg-tertiary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 2px;
|
||||
padding: 16px;
|
||||
overflow-x: auto;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.markdown pre code {
|
||||
background: none;
|
||||
border: none;
|
||||
padding: 0;
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
.markdown blockquote {
|
||||
border-left: 3px solid var(--accent);
|
||||
padding-left: 16px;
|
||||
margin: 12px 0;
|
||||
color: var(--text-secondary);
|
||||
font-style: italic;
|
||||
}
|
||||
|
||||
.markdown a {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
border-bottom: 1px solid transparent;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.markdown a:hover {
|
||||
border-bottom-color: var(--accent);
|
||||
}
|
||||
|
||||
.markdown strong {
|
||||
color: var(--text-primary);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.markdown table {
|
||||
width: 100%;
|
||||
border-collapse: collapse;
|
||||
margin: 12px 0;
|
||||
}
|
||||
|
||||
.markdown th,
|
||||
.markdown td {
|
||||
border: 1px solid var(--border);
|
||||
padding: 8px 12px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.markdown th {
|
||||
background: var(--bg-tertiary);
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
/* Chart */
|
||||
.chart-box {
|
||||
margin-top: 16px;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 2px;
|
||||
overflow: hidden;
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.chart {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
}
|
||||
|
||||
/* Typing Indicator */
|
||||
.typing {
|
||||
display: flex;
|
||||
gap: 6px;
|
||||
padding: 8px 0;
|
||||
}
|
||||
|
||||
.typing span {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 50%;
|
||||
background: var(--accent);
|
||||
animation: bounce 1.4s infinite ease-in-out;
|
||||
}
|
||||
|
||||
.typing span:nth-child(1) {
|
||||
animation-delay: 0s;
|
||||
}
|
||||
|
||||
.typing span:nth-child(2) {
|
||||
animation-delay: 0.2s;
|
||||
}
|
||||
|
||||
.typing span:nth-child(3) {
|
||||
animation-delay: 0.4s;
|
||||
}
|
||||
|
||||
@keyframes bounce {
|
||||
0%, 60%, 100% {
|
||||
transform: translateY(0);
|
||||
opacity: 0.4;
|
||||
}
|
||||
30% {
|
||||
transform: translateY(-8px);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
/* Input Container */
|
||||
.input-container {
|
||||
padding: 20px 24px;
|
||||
border-top: 1px solid var(--border);
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
.input-wrapper {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
background: var(--bg-secondary);
|
||||
border: 1px solid var(--border);
|
||||
border-radius: 2px;
|
||||
padding: 12px;
|
||||
transition: border-color 0.2s;
|
||||
}
|
||||
|
||||
.input-wrapper:focus-within {
|
||||
border-color: var(--accent);
|
||||
box-shadow: 0 0 0 1px var(--accent);
|
||||
}
|
||||
|
||||
/* Author Info */
|
||||
.author-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 8px;
|
||||
margin-top: 12px;
|
||||
font-size: 12px;
|
||||
color: var(--text-tertiary);
|
||||
letter-spacing: 0.5px;
|
||||
}
|
||||
|
||||
.author-label {
|
||||
color: var(--text-secondary);
|
||||
text-transform: uppercase;
|
||||
}
|
||||
|
||||
.author-divider {
|
||||
color: var(--border-bright);
|
||||
}
|
||||
|
||||
.author-contact {
|
||||
color: var(--accent);
|
||||
font-family: 'Monaco', 'Courier New', monospace;
|
||||
cursor: pointer;
|
||||
transition: all 0.2s;
|
||||
padding: 4px 8px;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.author-contact:hover {
|
||||
background: var(--accent-dim);
|
||||
box-shadow: 0 0 8px var(--accent-dim);
|
||||
}
|
||||
|
||||
.input-wrapper textarea {
|
||||
flex: 1;
|
||||
background: transparent;
|
||||
border: none;
|
||||
outline: none;
|
||||
color: var(--text-primary);
|
||||
font-size: 14px;
|
||||
font-family: inherit;
|
||||
resize: none;
|
||||
max-height: 120px;
|
||||
line-height: 1.5;
|
||||
padding: 0;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
.input-wrapper textarea::placeholder {
|
||||
color: var(--text-tertiary);
|
||||
}
|
||||
|
||||
.send-btn {
|
||||
width: 36px;
|
||||
height: 36px;
|
||||
background: transparent;
|
||||
border: 1px solid var(--border-bright);
|
||||
border-radius: 2px;
|
||||
color: var(--accent);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
transition: all 0.2s;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.send-btn:hover:not(:disabled) {
|
||||
background: var(--accent-dim);
|
||||
border-color: var(--accent);
|
||||
}
|
||||
|
||||
.send-btn:disabled {
|
||||
opacity: 0.3;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
/* Spinner */
|
||||
.spinner {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
border: 2px solid var(--border-bright);
|
||||
border-top-color: var(--accent);
|
||||
border-radius: 50%;
|
||||
animation: spin 0.8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
/* Scrollbar */
|
||||
::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-track {
|
||||
background: var(--bg-primary);
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb {
|
||||
background: var(--border-bright);
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
::-webkit-scrollbar-thumb:hover {
|
||||
background: var(--text-tertiary);
|
||||
}
|
||||
|
||||
/* Responsive */
|
||||
@media (max-width: 768px) {
|
||||
#app {
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
border-radius: 0;
|
||||
border: none;
|
||||
}
|
||||
|
||||
.header {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
|
||||
.messages {
|
||||
padding: 24px 20px;
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.input-container {
|
||||
padding: 16px 20px;
|
||||
}
|
||||
}
|
||||
|
||||
/* Selection */
|
||||
::selection {
|
||||
background: var(--accent-dim);
|
||||
color: var(--text-primary);
|
||||
}
|
||||
|
||||
/* Copy Notification Animation */
|
||||
@keyframes fadeInOut {
|
||||
0% {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(10px);
|
||||
}
|
||||
10%, 90% {
|
||||
opacity: 1;
|
||||
transform: translateX(-50%) translateY(0);
|
||||
}
|
||||
100% {
|
||||
opacity: 0;
|
||||
transform: translateX(-50%) translateY(-10px);
|
||||
}
|
||||
}
|
||||
196
frontend/css/style.css.backup
Normal file
196
frontend/css/style.css.backup
Normal file
@ -0,0 +1,196 @@
|
||||
/* 全局样式 */
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html, body {
|
||||
height: 100%;
|
||||
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
|
||||
}
|
||||
|
||||
#app {
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.container-fluid {
|
||||
flex: 1;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.row {
|
||||
height: calc(100vh - 56px);
|
||||
}
|
||||
|
||||
/* 聊天容器 */
|
||||
.chat-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
/* 消息列表 */
|
||||
.messages-container {
|
||||
flex: 1;
|
||||
overflow-y: auto;
|
||||
padding: 20px;
|
||||
background-color: #f8f9fa;
|
||||
}
|
||||
|
||||
/* 消息样式 */
|
||||
.message {
|
||||
margin-bottom: 20px;
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
.message-content {
|
||||
max-width: 80%;
|
||||
padding: 12px 16px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.user-message .message-content {
|
||||
background-color: #007bff;
|
||||
color: white;
|
||||
margin-left: auto;
|
||||
}
|
||||
|
||||
.assistant-message .message-content {
|
||||
background-color: white;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.message-header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-bottom: 8px;
|
||||
font-size: 0.9em;
|
||||
}
|
||||
|
||||
.user-message .message-header {
|
||||
color: rgba(255, 255, 255, 0.9);
|
||||
}
|
||||
|
||||
.assistant-message .message-header {
|
||||
color: #666;
|
||||
}
|
||||
|
||||
.message-time {
|
||||
font-size: 0.85em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.message-body pre {
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
font-family: inherit;
|
||||
font-size: 0.95em;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
/* 图表容器 */
|
||||
.chart-container {
|
||||
width: 100%;
|
||||
height: 400px;
|
||||
background-color: #fff;
|
||||
border-radius: 4px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
/* 输入框容器 */
|
||||
.input-container {
|
||||
padding: 20px;
|
||||
background-color: white;
|
||||
border-top: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.input-group input {
|
||||
border-radius: 20px 0 0 20px;
|
||||
padding: 12px 20px;
|
||||
}
|
||||
|
||||
.input-group button {
|
||||
border-radius: 0 20px 20px 0;
|
||||
padding: 12px 30px;
|
||||
}
|
||||
|
||||
/* 技能面板 */
|
||||
.skill-panel {
|
||||
background-color: #f8f9fa;
|
||||
border-left: 1px solid #dee2e6;
|
||||
padding: 20px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.skill-item {
|
||||
padding: 12px;
|
||||
background-color: white;
|
||||
border-radius: 8px;
|
||||
border: 1px solid #dee2e6;
|
||||
}
|
||||
|
||||
.skill-item:hover {
|
||||
box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
/* 滚动条样式 */
|
||||
.messages-container::-webkit-scrollbar,
|
||||
.skill-panel::-webkit-scrollbar {
|
||||
width: 8px;
|
||||
}
|
||||
|
||||
.messages-container::-webkit-scrollbar-track,
|
||||
.skill-panel::-webkit-scrollbar-track {
|
||||
background: #f1f1f1;
|
||||
}
|
||||
|
||||
.messages-container::-webkit-scrollbar-thumb,
|
||||
.skill-panel::-webkit-scrollbar-thumb {
|
||||
background: #888;
|
||||
border-radius: 4px;
|
||||
}
|
||||
|
||||
.messages-container::-webkit-scrollbar-thumb:hover,
|
||||
.skill-panel::-webkit-scrollbar-thumb:hover {
|
||||
background: #555;
|
||||
}
|
||||
|
||||
/* 响应式设计 */
|
||||
@media (max-width: 768px) {
|
||||
.message-content {
|
||||
max-width: 90%;
|
||||
}
|
||||
|
||||
.skill-panel {
|
||||
position: fixed;
|
||||
top: 56px;
|
||||
right: 0;
|
||||
width: 300px;
|
||||
height: calc(100vh - 56px);
|
||||
z-index: 1000;
|
||||
box-shadow: -2px 0 8px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 加载动画 */
|
||||
.spinner-border-sm {
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
border-width: 0.15em;
|
||||
}
|
||||
121
frontend/index.html
Normal file
121
frontend/index.html
Normal file
@ -0,0 +1,121 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>龙哥的 AI 金融智能体</title>
|
||||
|
||||
<!-- Fonts -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap" rel="stylesheet">
|
||||
|
||||
<!-- Marked.js for Markdown rendering -->
|
||||
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
|
||||
|
||||
<!-- Styles -->
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- Main Container -->
|
||||
<div class="container">
|
||||
<!-- Header -->
|
||||
<header class="header">
|
||||
<div class="logo">
|
||||
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
||||
<path d="M3 13h8V3H3v10zm0 8h8v-6H3v6zm10 0h8V11h-8v10zm0-18v6h8V3h-8z" fill="currentColor"/>
|
||||
</svg>
|
||||
<span>龙哥的 AI 金融智能体</span>
|
||||
</div>
|
||||
<div class="status">
|
||||
<div class="status-dot"></div>
|
||||
<span>在线</span>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Chat Area -->
|
||||
<div class="chat-container" ref="chatContainer">
|
||||
<!-- Welcome Screen -->
|
||||
<div v-if="messages.length === 0" class="welcome">
|
||||
<div class="welcome-icon">
|
||||
<svg width="80" height="80" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1">
|
||||
<path d="M21 15a2 2 0 0 1-2 2H7l-4 4V5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2z"/>
|
||||
</svg>
|
||||
</div>
|
||||
<h1>开始对话</h1>
|
||||
<p>输入股票代码或名称,获取实时分析</p>
|
||||
</div>
|
||||
|
||||
<!-- Messages -->
|
||||
<div v-else class="messages">
|
||||
<div v-for="(msg, index) in messages" :key="index"
|
||||
:class="['message', msg.role]">
|
||||
<div class="message-content">
|
||||
<div v-if="msg.role === 'user'" class="text">{{ msg.content }}</div>
|
||||
<div v-else class="text markdown" v-html="renderMarkdown(msg.content)"></div>
|
||||
|
||||
<!-- Chart Display -->
|
||||
<div v-if="msg.metadata && msg.metadata.type === 'chart'" class="chart-box">
|
||||
<div :id="'chart-' + index" class="chart"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading -->
|
||||
<div v-if="loading" class="message assistant">
|
||||
<div class="message-content">
|
||||
<div class="typing">
|
||||
<span></span>
|
||||
<span></span>
|
||||
<span></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Input Area -->
|
||||
<div class="input-container">
|
||||
<div class="input-wrapper">
|
||||
<textarea
|
||||
v-model="userInput"
|
||||
@keydown.enter.exact.prevent="sendMessage"
|
||||
placeholder="输入消息..."
|
||||
rows="1"
|
||||
:disabled="loading"
|
||||
ref="textarea"
|
||||
></textarea>
|
||||
<button
|
||||
class="send-btn"
|
||||
@click="sendMessage"
|
||||
:disabled="loading || !userInput.trim()"
|
||||
>
|
||||
<svg v-if="!loading" width="20" height="20" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||
<line x1="22" y1="2" x2="11" y2="13"/>
|
||||
<polygon points="22 2 15 22 11 13 2 9 22 2"/>
|
||||
</svg>
|
||||
<div v-else class="spinner"></div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Author Info -->
|
||||
<div class="author-info">
|
||||
<span class="author-label">点击联系作者</span>
|
||||
<span class="author-divider">|</span>
|
||||
<span class="author-contact" @click="copyWechat" title="点击复制微信号">微信:aaronlzhou</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vue 3 -->
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
|
||||
<!-- Lightweight Charts -->
|
||||
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
|
||||
|
||||
<!-- App Script -->
|
||||
<script src="/static/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
133
frontend/index.html.backup
Normal file
133
frontend/index.html.backup
Normal file
@ -0,0 +1,133 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>A股AI分析Agent系统</title>
|
||||
|
||||
<!-- Bootstrap 5 CSS -->
|
||||
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet">
|
||||
|
||||
<!-- 自定义样式 -->
|
||||
<link rel="stylesheet" href="/static/css/style.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app">
|
||||
<!-- 导航栏 -->
|
||||
<nav class="navbar navbar-dark bg-primary">
|
||||
<div class="container-fluid">
|
||||
<span class="navbar-brand mb-0 h1">📈 A股AI分析Agent</span>
|
||||
<button class="btn btn-outline-light btn-sm" @click="showSkillPanel = !showSkillPanel">
|
||||
技能管理
|
||||
</button>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div class="container-fluid h-100">
|
||||
<div class="row h-100">
|
||||
<!-- 主聊天区域 -->
|
||||
<div class="col-md-9 chat-container">
|
||||
<!-- 消息列表 -->
|
||||
<div class="messages-container" ref="messagesContainer">
|
||||
<div v-if="messages.length === 0" class="text-center text-muted mt-5">
|
||||
<h4>欢迎使用A股AI分析Agent</h4>
|
||||
<p>请输入股票代码或问题,例如:</p>
|
||||
<ul class="list-unstyled">
|
||||
<li>查询600519的实时行情</li>
|
||||
<li>贵州茅台的技术指标</li>
|
||||
<li>000001的K线图</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<div v-for="(msg, index) in messages" :key="index"
|
||||
:class="['message', msg.role === 'user' ? 'user-message' : 'assistant-message']">
|
||||
<div class="message-content">
|
||||
<div class="message-header">
|
||||
<strong>{{ msg.role === 'user' ? '您' : 'AI助手' }}</strong>
|
||||
<span class="message-time">{{ formatTime(msg.timestamp) }}</span>
|
||||
</div>
|
||||
<div class="message-body">
|
||||
<pre v-if="msg.role === 'assistant'" class="mb-0">{{ msg.content }}</pre>
|
||||
<p v-else class="mb-0">{{ msg.content }}</p>
|
||||
|
||||
<!-- 图表展示 -->
|
||||
<div v-if="msg.metadata && msg.metadata.type === 'chart'" class="mt-3">
|
||||
<div :id="'chart-' + index" class="chart-container"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="loading" class="message assistant-message">
|
||||
<div class="message-content">
|
||||
<div class="spinner-border spinner-border-sm" role="status">
|
||||
<span class="visually-hidden">加载中...</span>
|
||||
</div>
|
||||
<span class="ms-2">AI正在思考...</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 输入框 -->
|
||||
<div class="input-container">
|
||||
<div class="input-group">
|
||||
<input
|
||||
type="text"
|
||||
class="form-control"
|
||||
v-model="userInput"
|
||||
@keyup.enter="sendMessage"
|
||||
placeholder="输入股票代码或问题..."
|
||||
:disabled="loading"
|
||||
>
|
||||
<button
|
||||
class="btn btn-primary"
|
||||
@click="sendMessage"
|
||||
:disabled="loading || !userInput.trim()"
|
||||
>
|
||||
发送
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 技能面板 -->
|
||||
<div v-if="showSkillPanel" class="col-md-3 skill-panel">
|
||||
<div class="card">
|
||||
<div class="card-header">
|
||||
<h5 class="mb-0">技能列表</h5>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<div v-if="skills.length === 0" class="text-muted">
|
||||
加载中...
|
||||
</div>
|
||||
<div v-for="skill in skills" :key="skill.name" class="skill-item mb-3">
|
||||
<div class="d-flex justify-content-between align-items-center">
|
||||
<strong>{{ skill.name }}</strong>
|
||||
<div class="form-check form-switch">
|
||||
<input
|
||||
class="form-check-input"
|
||||
type="checkbox"
|
||||
:checked="skill.enabled"
|
||||
@change="toggleSkill(skill.name, $event.target.checked)"
|
||||
>
|
||||
</div>
|
||||
</div>
|
||||
<small class="text-muted">{{ skill.description }}</small>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Vue 3 -->
|
||||
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
||||
|
||||
<!-- Lightweight Charts -->
|
||||
<script src="https://unpkg.com/lightweight-charts/dist/lightweight-charts.standalone.production.js"></script>
|
||||
|
||||
<!-- 应用脚本 -->
|
||||
<script src="/static/js/app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
269
frontend/js/app.js
Normal file
269
frontend/js/app.js
Normal file
@ -0,0 +1,269 @@
|
||||
// Vue 3 Application
|
||||
const { createApp } = Vue;
|
||||
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
messages: [],
|
||||
userInput: '',
|
||||
loading: false,
|
||||
sessionId: null,
|
||||
charts: {}
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.sessionId = this.generateSessionId();
|
||||
this.autoResizeTextarea();
|
||||
},
|
||||
methods: {
|
||||
async sendMessage() {
|
||||
if (!this.userInput.trim() || this.loading) return;
|
||||
|
||||
const message = this.userInput.trim();
|
||||
this.userInput = '';
|
||||
|
||||
// Add user message
|
||||
this.messages.push({
|
||||
role: 'user',
|
||||
content: message,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
this.autoResizeTextarea();
|
||||
});
|
||||
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/chat/message', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: message,
|
||||
session_id: this.sessionId
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('请求失败');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// Add assistant message
|
||||
const assistantMessage = {
|
||||
role: 'assistant',
|
||||
content: data.message,
|
||||
timestamp: new Date(),
|
||||
metadata: data.metadata
|
||||
};
|
||||
|
||||
this.messages.push(assistantMessage);
|
||||
|
||||
// Render chart if needed
|
||||
if (data.metadata && data.metadata.type === 'chart') {
|
||||
this.$nextTick(() => {
|
||||
const index = this.messages.length - 1;
|
||||
this.renderChart(index, data.metadata.data);
|
||||
});
|
||||
}
|
||||
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error);
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
content: '抱歉,发送消息失败,请稍后重试。',
|
||||
timestamp: new Date()
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
renderMarkdown(content) {
|
||||
if (!content) return '';
|
||||
|
||||
// Configure marked options
|
||||
marked.setOptions({
|
||||
breaks: true,
|
||||
gfm: true,
|
||||
headerIds: false,
|
||||
mangle: false
|
||||
});
|
||||
|
||||
return marked.parse(content);
|
||||
},
|
||||
|
||||
renderChart(index, data) {
|
||||
const chartId = `chart-${index}`;
|
||||
const container = document.getElementById(chartId);
|
||||
|
||||
if (!container || !data.kline_data) return;
|
||||
|
||||
const chart = LightweightCharts.createChart(container, {
|
||||
width: container.clientWidth,
|
||||
height: 400,
|
||||
layout: {
|
||||
background: { color: '#000000' },
|
||||
textColor: '#a0a0a0'
|
||||
},
|
||||
grid: {
|
||||
vertLines: { color: '#1a1a1a' },
|
||||
horzLines: { color: '#1a1a1a' }
|
||||
},
|
||||
timeScale: {
|
||||
borderColor: '#333333',
|
||||
timeVisible: true
|
||||
},
|
||||
rightPriceScale: {
|
||||
borderColor: '#333333'
|
||||
}
|
||||
});
|
||||
|
||||
const candlestickSeries = chart.addCandlestickSeries({
|
||||
upColor: '#00ff41',
|
||||
downColor: '#ff0040',
|
||||
borderVisible: false,
|
||||
wickUpColor: '#00ff41',
|
||||
wickDownColor: '#ff0040'
|
||||
});
|
||||
|
||||
const klineData = data.kline_data.map(item => ({
|
||||
time: item.trade_date,
|
||||
open: item.open,
|
||||
high: item.high,
|
||||
low: item.low,
|
||||
close: item.close
|
||||
}));
|
||||
|
||||
candlestickSeries.setData(klineData);
|
||||
|
||||
if (data.volume_data) {
|
||||
const volumeSeries = chart.addHistogramSeries({
|
||||
color: '#00ff4140',
|
||||
priceFormat: {
|
||||
type: 'volume'
|
||||
},
|
||||
priceScaleId: ''
|
||||
});
|
||||
|
||||
const volumeData = data.volume_data.map(item => ({
|
||||
time: item.trade_date,
|
||||
value: item.vol,
|
||||
color: item.close >= item.open ? '#00ff4140' : '#ff004040'
|
||||
}));
|
||||
|
||||
volumeSeries.setData(volumeData);
|
||||
}
|
||||
|
||||
chart.timeScale().fitContent();
|
||||
|
||||
this.charts[chartId] = chart;
|
||||
|
||||
// Handle resize
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.charts[chartId]) {
|
||||
chart.applyOptions({ width: container.clientWidth });
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
scrollToBottom() {
|
||||
const container = this.$refs.chatContainer;
|
||||
if (container) {
|
||||
setTimeout(() => {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}, 100);
|
||||
}
|
||||
},
|
||||
|
||||
autoResizeTextarea() {
|
||||
const textarea = this.$refs.textarea;
|
||||
if (textarea) {
|
||||
textarea.style.height = 'auto';
|
||||
textarea.style.height = Math.min(textarea.scrollHeight, 120) + 'px';
|
||||
}
|
||||
},
|
||||
|
||||
generateSessionId() {
|
||||
return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
},
|
||||
|
||||
copyWechat() {
|
||||
const wechatId = 'aaronlzhou';
|
||||
|
||||
// 使用现代的 Clipboard API
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(wechatId).then(() => {
|
||||
this.showCopyNotification();
|
||||
}).catch(err => {
|
||||
console.error('复制失败:', err);
|
||||
this.fallbackCopy(wechatId);
|
||||
});
|
||||
} else {
|
||||
this.fallbackCopy(wechatId);
|
||||
}
|
||||
},
|
||||
|
||||
fallbackCopy(text) {
|
||||
// 降级方案:使用传统方法
|
||||
const textarea = document.createElement('textarea');
|
||||
textarea.value = text;
|
||||
textarea.style.position = 'fixed';
|
||||
textarea.style.opacity = '0';
|
||||
document.body.appendChild(textarea);
|
||||
textarea.select();
|
||||
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
this.showCopyNotification();
|
||||
} catch (err) {
|
||||
console.error('复制失败:', err);
|
||||
}
|
||||
|
||||
document.body.removeChild(textarea);
|
||||
},
|
||||
|
||||
showCopyNotification() {
|
||||
// 创建临时提示
|
||||
const notification = document.createElement('div');
|
||||
notification.textContent = '已复制微信号';
|
||||
notification.style.cssText = `
|
||||
position: fixed;
|
||||
bottom: 80px;
|
||||
left: 50%;
|
||||
transform: translateX(-50%);
|
||||
background: #00ff41;
|
||||
color: #000000;
|
||||
padding: 8px 16px;
|
||||
border-radius: 2px;
|
||||
font-size: 13px;
|
||||
font-weight: 500;
|
||||
z-index: 10000;
|
||||
animation: fadeInOut 2s ease;
|
||||
`;
|
||||
|
||||
document.body.appendChild(notification);
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.removeChild(notification);
|
||||
}, 2000);
|
||||
}
|
||||
},
|
||||
|
||||
watch: {
|
||||
userInput() {
|
||||
this.$nextTick(() => {
|
||||
this.autoResizeTextarea();
|
||||
});
|
||||
}
|
||||
}
|
||||
}).mount('#app');
|
||||
219
frontend/js/app.js.backup
Normal file
219
frontend/js/app.js.backup
Normal file
@ -0,0 +1,219 @@
|
||||
// Vue 3 应用
|
||||
const { createApp } = Vue;
|
||||
|
||||
createApp({
|
||||
data() {
|
||||
return {
|
||||
messages: [],
|
||||
userInput: '',
|
||||
loading: false,
|
||||
sessionId: null,
|
||||
showSkillPanel: false,
|
||||
skills: [],
|
||||
charts: {}
|
||||
};
|
||||
},
|
||||
mounted() {
|
||||
this.loadSkills();
|
||||
// 生成会话ID
|
||||
this.sessionId = this.generateSessionId();
|
||||
},
|
||||
methods: {
|
||||
async sendMessage() {
|
||||
if (!this.userInput.trim() || this.loading) return;
|
||||
|
||||
const message = this.userInput.trim();
|
||||
this.userInput = '';
|
||||
|
||||
// 添加用户消息
|
||||
this.messages.push({
|
||||
role: 'user',
|
||||
content: message,
|
||||
timestamp: new Date()
|
||||
});
|
||||
|
||||
// 滚动到底部
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
|
||||
// 发送请求
|
||||
this.loading = true;
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/chat/message', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify({
|
||||
message: message,
|
||||
session_id: this.sessionId
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('请求失败');
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
// 添加助手消息
|
||||
const assistantMessage = {
|
||||
role: 'assistant',
|
||||
content: data.message,
|
||||
timestamp: new Date(),
|
||||
metadata: data.metadata
|
||||
};
|
||||
|
||||
this.messages.push(assistantMessage);
|
||||
|
||||
// 如果有图表数据,渲染图表
|
||||
if (data.metadata && data.metadata.type === 'chart') {
|
||||
this.$nextTick(() => {
|
||||
const index = this.messages.length - 1;
|
||||
this.renderChart(index, data.metadata.data);
|
||||
});
|
||||
}
|
||||
|
||||
// 滚动到底部
|
||||
this.$nextTick(() => {
|
||||
this.scrollToBottom();
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('发送消息失败:', error);
|
||||
this.messages.push({
|
||||
role: 'assistant',
|
||||
content: '抱歉,发送消息失败,请稍后重试。',
|
||||
timestamp: new Date()
|
||||
});
|
||||
} finally {
|
||||
this.loading = false;
|
||||
}
|
||||
},
|
||||
|
||||
async loadSkills() {
|
||||
try {
|
||||
const response = await fetch('/api/skills/');
|
||||
if (!response.ok) {
|
||||
throw new Error('加载技能失败');
|
||||
}
|
||||
const data = await response.json();
|
||||
this.skills = data.skills;
|
||||
} catch (error) {
|
||||
console.error('加载技能失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
async toggleSkill(skillName, enabled) {
|
||||
try {
|
||||
const endpoint = enabled ? 'enable' : 'disable';
|
||||
const response = await fetch(`/api/skills/${skillName}/${endpoint}`, {
|
||||
method: 'POST'
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error('切换技能失败');
|
||||
}
|
||||
|
||||
// 重新加载技能列表
|
||||
await this.loadSkills();
|
||||
} catch (error) {
|
||||
console.error('切换技能失败:', error);
|
||||
// 恢复原状态
|
||||
await this.loadSkills();
|
||||
}
|
||||
},
|
||||
|
||||
renderChart(index, chartData) {
|
||||
const containerId = `chart-${index}`;
|
||||
const container = document.getElementById(containerId);
|
||||
|
||||
if (!container || !chartData) return;
|
||||
|
||||
try {
|
||||
// 创建图表
|
||||
const chart = LightweightCharts.createChart(container, {
|
||||
width: container.clientWidth,
|
||||
height: 400,
|
||||
layout: {
|
||||
background: { color: '#ffffff' },
|
||||
textColor: '#333',
|
||||
},
|
||||
grid: {
|
||||
vertLines: { color: '#f0f0f0' },
|
||||
horzLines: { color: '#f0f0f0' },
|
||||
},
|
||||
timeScale: {
|
||||
borderColor: '#cccccc',
|
||||
},
|
||||
});
|
||||
|
||||
// 添加K线图
|
||||
if (chartData.candlestick_data) {
|
||||
const candlestickSeries = chart.addCandlestickSeries({
|
||||
upColor: '#26a69a',
|
||||
downColor: '#ef5350',
|
||||
borderVisible: false,
|
||||
wickUpColor: '#26a69a',
|
||||
wickDownColor: '#ef5350',
|
||||
});
|
||||
candlestickSeries.setData(chartData.candlestick_data);
|
||||
}
|
||||
|
||||
// 添加成交量
|
||||
if (chartData.volume_data) {
|
||||
const volumeSeries = chart.addHistogramSeries({
|
||||
color: '#26a69a',
|
||||
priceFormat: {
|
||||
type: 'volume',
|
||||
},
|
||||
priceScaleId: '',
|
||||
scaleMargins: {
|
||||
top: 0.8,
|
||||
bottom: 0,
|
||||
},
|
||||
});
|
||||
volumeSeries.setData(chartData.volume_data);
|
||||
}
|
||||
|
||||
// 自适应大小
|
||||
chart.timeScale().fitContent();
|
||||
|
||||
// 保存图表实例
|
||||
this.charts[containerId] = chart;
|
||||
|
||||
// 窗口大小改变时调整图表
|
||||
window.addEventListener('resize', () => {
|
||||
if (this.charts[containerId]) {
|
||||
this.charts[containerId].applyOptions({
|
||||
width: container.clientWidth
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('渲染图表失败:', error);
|
||||
}
|
||||
},
|
||||
|
||||
scrollToBottom() {
|
||||
const container = this.$refs.messagesContainer;
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
},
|
||||
|
||||
formatTime(timestamp) {
|
||||
const date = new Date(timestamp);
|
||||
const hours = date.getHours().toString().padStart(2, '0');
|
||||
const minutes = date.getMinutes().toString().padStart(2, '0');
|
||||
return `${hours}:${minutes}`;
|
||||
},
|
||||
|
||||
generateSessionId() {
|
||||
return 'session_' + Date.now() + '_' + Math.random().toString(36).substr(2, 9);
|
||||
}
|
||||
}
|
||||
}).mount('#app');
|
||||
149
install.sh
Executable file
149
install.sh
Executable file
@ -0,0 +1,149 @@
|
||||
#!/bin/bash
|
||||
|
||||
# A股AI分析Agent系统 - 快速安装脚本
|
||||
|
||||
echo "================================"
|
||||
echo "A股AI分析Agent系统 - 安装脚本"
|
||||
echo "================================"
|
||||
echo ""
|
||||
|
||||
# 检查Python版本
|
||||
echo "检查Python版本..."
|
||||
|
||||
# 尝试找到合适的Python版本
|
||||
PYTHON_CMD=""
|
||||
|
||||
for cmd in python3.11 python3.12 python3.10 python3 python; do
|
||||
if command -v $cmd &> /dev/null; then
|
||||
version=$($cmd --version 2>&1 | awk '{print $2}')
|
||||
major=$(echo $version | cut -d. -f1)
|
||||
minor=$(echo $version | cut -d. -f2)
|
||||
|
||||
if [ "$major" = "3" ] && [ "$minor" -ge "10" ] && [ "$minor" -le "12" ]; then
|
||||
PYTHON_CMD=$cmd
|
||||
echo "✓ 找到合适的Python版本: $version ($cmd)"
|
||||
break
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ -z "$PYTHON_CMD" ]; then
|
||||
echo "❌ 错误: 未找到合适的Python版本"
|
||||
echo ""
|
||||
echo "请安装 Python 3.11 或 3.12:"
|
||||
echo " macOS: brew install python@3.11"
|
||||
echo " Ubuntu: sudo apt install python3.11"
|
||||
echo " Windows: 从 python.org 下载安装"
|
||||
echo ""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查是否在项目根目录
|
||||
if [ ! -f "README.md" ] || [ ! -d "backend" ]; then
|
||||
echo "❌ 错误: 请在项目根目录运行此脚本"
|
||||
echo "当前目录: $(pwd)"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 创建虚拟环境
|
||||
echo ""
|
||||
echo "创建虚拟环境..."
|
||||
cd backend
|
||||
|
||||
if [ -d "venv" ]; then
|
||||
echo "⚠ 虚拟环境已存在,将删除并重新创建"
|
||||
rm -rf venv
|
||||
fi
|
||||
|
||||
$PYTHON_CMD -m venv venv
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "❌ 创建虚拟环境失败"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✓ 虚拟环境创建成功"
|
||||
|
||||
# 激活虚拟环境
|
||||
echo ""
|
||||
echo "激活虚拟环境..."
|
||||
source venv/bin/activate
|
||||
|
||||
# 升级pip
|
||||
echo ""
|
||||
echo "升级pip..."
|
||||
pip install --upgrade pip setuptools wheel
|
||||
|
||||
# 安装依赖
|
||||
echo ""
|
||||
echo "安装依赖包..."
|
||||
echo "这可能需要几分钟时间..."
|
||||
echo ""
|
||||
|
||||
pip install -r requirements.txt
|
||||
|
||||
if [ $? -ne 0 ]; then
|
||||
echo ""
|
||||
echo "❌ 依赖安装失败"
|
||||
echo ""
|
||||
echo "可能的原因:"
|
||||
echo "1. Python版本不兼容(推荐使用3.11或3.12)"
|
||||
echo "2. 网络问题"
|
||||
echo "3. 缺少编译工具"
|
||||
echo ""
|
||||
echo "解决方案请查看: docs/INSTALL_GUIDE.md"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo "✓ 依赖安装成功"
|
||||
|
||||
# 检查配置文件
|
||||
echo ""
|
||||
echo "检查配置文件..."
|
||||
cd ..
|
||||
|
||||
if [ ! -f ".env" ]; then
|
||||
echo "⚠ 未找到.env文件,从模板创建..."
|
||||
cp .env.example .env
|
||||
echo "✓ 已创建.env文件"
|
||||
echo ""
|
||||
echo "⚠ 重要: 请编辑.env文件,填写以下配置:"
|
||||
echo " - TUSHARE_TOKEN (从 https://tushare.pro/ 获取)"
|
||||
echo " - ZHIPUAI_API_KEY (从 https://open.bigmodel.cn/ 获取)"
|
||||
echo ""
|
||||
else
|
||||
echo "✓ .env文件已存在"
|
||||
fi
|
||||
|
||||
# 验证安装
|
||||
echo ""
|
||||
echo "验证安装..."
|
||||
cd backend
|
||||
source venv/bin/activate
|
||||
|
||||
python -c "import fastapi; print('✓ FastAPI:', fastapi.__version__)" 2>/dev/null || echo "❌ FastAPI 安装失败"
|
||||
python -c "import pandas; print('✓ Pandas:', pandas.__version__)" 2>/dev/null || echo "❌ Pandas 安装失败"
|
||||
python -c "import numpy; print('✓ NumPy:', numpy.__version__)" 2>/dev/null || echo "❌ NumPy 安装失败"
|
||||
python -c "import tushare; print('✓ Tushare:', tushare.__version__)" 2>/dev/null || echo "❌ Tushare 安装失败"
|
||||
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "安装完成!"
|
||||
echo "================================"
|
||||
echo ""
|
||||
echo "下一步:"
|
||||
echo "1. 编辑 .env 文件,填写API密钥"
|
||||
echo "2. 启动应用:"
|
||||
echo " cd backend"
|
||||
echo " source venv/bin/activate"
|
||||
echo " python -m app.main"
|
||||
echo ""
|
||||
echo "3. 访问系统:"
|
||||
echo " 前端界面: http://localhost:8000"
|
||||
echo " API文档: http://localhost:8000/docs"
|
||||
echo ""
|
||||
echo "如有问题,请查看:"
|
||||
echo " - 安装指南: docs/INSTALL_GUIDE.md"
|
||||
echo " - 用户手册: docs/USER_GUIDE.md"
|
||||
echo ""
|
||||
78
start.sh
Executable file
78
start.sh
Executable file
@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
|
||||
# A股AI分析Agent系统 - 快速启动脚本
|
||||
|
||||
echo "================================"
|
||||
echo "A股AI分析Agent系统 - 启动脚本"
|
||||
echo "================================"
|
||||
echo ""
|
||||
|
||||
# 检查Python版本
|
||||
echo "检查Python版本..."
|
||||
python_version=$(python3 --version 2>&1 | awk '{print $2}')
|
||||
echo "Python版本: $python_version"
|
||||
|
||||
# 检查是否在虚拟环境中
|
||||
if [[ "$VIRTUAL_ENV" == "" ]]; then
|
||||
echo ""
|
||||
echo "警告: 未检测到虚拟环境"
|
||||
echo "建议创建虚拟环境:"
|
||||
echo " python3 -m venv venv"
|
||||
echo " source venv/bin/activate # macOS/Linux"
|
||||
echo " venv\\Scripts\\activate # Windows"
|
||||
echo ""
|
||||
read -p "是否继续?(y/n) " -n 1 -r
|
||||
echo
|
||||
if [[ ! $REPLY =~ ^[Yy]$ ]]; then
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
# 检查.env文件
|
||||
if [ ! -f ".env" ]; then
|
||||
echo ""
|
||||
echo "错误: 未找到.env文件"
|
||||
echo "请复制.env.example为.env并配置:"
|
||||
echo " cp .env.example .env"
|
||||
echo " 然后编辑.env文件,填写必要的配置"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# 检查依赖
|
||||
echo ""
|
||||
echo "检查依赖..."
|
||||
cd backend
|
||||
|
||||
if [ ! -d "venv" ] && [[ "$VIRTUAL_ENV" == "" ]]; then
|
||||
echo "安装依赖..."
|
||||
pip3 install -r requirements.txt
|
||||
fi
|
||||
|
||||
# 检查Redis(可选)
|
||||
echo ""
|
||||
echo "检查Redis..."
|
||||
if command -v redis-cli &> /dev/null; then
|
||||
if redis-cli ping &> /dev/null; then
|
||||
echo "✓ Redis运行正常"
|
||||
else
|
||||
echo "⚠ Redis未运行(可选,系统会自动降级)"
|
||||
echo " 启动Redis: redis-server"
|
||||
fi
|
||||
else
|
||||
echo "⚠ Redis未安装(可选,系统会自动降级)"
|
||||
fi
|
||||
|
||||
# 启动应用
|
||||
echo ""
|
||||
echo "================================"
|
||||
echo "启动应用..."
|
||||
echo "================================"
|
||||
echo ""
|
||||
echo "访问地址:"
|
||||
echo " 前端界面: http://localhost:8000"
|
||||
echo " API文档: http://localhost:8000/docs"
|
||||
echo ""
|
||||
echo "按 Ctrl+C 停止服务"
|
||||
echo ""
|
||||
|
||||
python3 -m app.main
|
||||
Loading…
Reference in New Issue
Block a user