baba521 commited on
Commit
f71610e
·
1 Parent(s): 3709c1b
service/company.py ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ from service.mysql_service import get_company_by_name
2
+
3
+
4
+ def check_company_exists(company_name):
5
+ result = get_company_by_name(company_name)
6
+ return len(result) > 0
service/hf_upload.py ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from huggingface_hub import HfApi, hf_hub_url
2
+
3
+ def get_hf_files_with_links(repo_id):
4
+ """
5
+ 获取Hugging Face仓库中的文件列表(文件名和完整链接),排除.gitattributes文件
6
+
7
+ Args:
8
+ repo_id (str): Hugging Face仓库ID,格式为"用户名/仓库名"
9
+
10
+ Returns:
11
+ list: 包含字典的列表,每个字典包含'title'和'link'键
12
+ 例如: [{'title': 'model.bin', 'link': 'https://huggingface.co/...'}, ...]
13
+
14
+ Raises:
15
+ ValueError: 当获取文件列表失败时抛出异常
16
+ """
17
+ try:
18
+ # 初始化Hugging Face API客户端
19
+ api = HfApi()
20
+
21
+ # 获取仓库中的所有文件
22
+ files = api.list_repo_files(repo_id=repo_id, repo_type="model")
23
+
24
+ # 过滤掉.gitattributes文件并生成完整链接
25
+ file_list = []
26
+ for file_path in files:
27
+ # 排除.gitattributes文件
28
+ if file_path != ".gitattributes":
29
+ # 生成文件的完整URL
30
+ file_url = hf_hub_url(repo_id=repo_id, filename=file_path)
31
+ file_list.append({
32
+ 'title': file_path,
33
+ 'link': file_url
34
+ })
35
+
36
+ return file_list
37
+
38
+ except Exception as e:
39
+ raise ValueError(f"获取文件列表失败: {str(e)}")
service/mcp_client.py ADDED
@@ -0,0 +1,286 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import requests
3
+ import subprocess
4
+ import os
5
+ from typing import Dict, Any, Generator, Optional
6
+ import time
7
+
8
+
9
+ class MCP_Client:
10
+ """MCP客户端,支持本地命令行和HTTP连接方式"""
11
+
12
+ def __init__(self, config: Dict[str, Any]):
13
+ """
14
+ 初始化MCP客户端
15
+
16
+ Args:
17
+ config: MCP服务器配置
18
+ """
19
+ self.config = config
20
+ self.server_name = list(config["mcpServers"].keys())[0]
21
+ self.server_config = config["mcpServers"][self.server_name]
22
+
23
+ # 根据配置类型初始化不同的连接方式
24
+ if "command" in self.server_config:
25
+ # 本地命令行方式
26
+ self.connection_type = "local"
27
+ self.process: Optional[subprocess.Popen] = None
28
+ else:
29
+ # HTTP方式
30
+ self.connection_type = "http"
31
+ self.url = self.server_config.get("url")
32
+ self.headers = self.server_config.get("headers", {})
33
+
34
+ # 初始化状态
35
+ self.initialized = False
36
+
37
+ def initialize(self) -> bool:
38
+ """
39
+ 初始化MCP会话
40
+
41
+ Returns:
42
+ bool: 初始化是否成功
43
+ """
44
+ if self.initialized:
45
+ return True
46
+
47
+ try:
48
+ if self.connection_type == "local":
49
+ # 启动本地MCP进程
50
+ command = self.server_config["command"]
51
+ args = self.server_config.get("args", [])
52
+ env = self.server_config.get("env", {})
53
+
54
+ # 合并环境变量
55
+ process_env = os.environ.copy()
56
+ process_env.update(env)
57
+
58
+ self.process = subprocess.Popen(
59
+ [command] + args,
60
+ stdin=subprocess.PIPE,
61
+ stdout=subprocess.PIPE,
62
+ stderr=subprocess.PIPE,
63
+ env=process_env,
64
+ text=True,
65
+ bufsize=1
66
+ )
67
+
68
+ # 发送初始化请求到子进程
69
+ init_request = {
70
+ "jsonrpc": "2.0",
71
+ "id": 1,
72
+ "method": "initialize",
73
+ "params": {
74
+ "protocolVersion": "2025-06-18",
75
+ "capabilities": {
76
+ "experimental": {},
77
+ "sampling": {},
78
+ "elicitations": {},
79
+ "roots": {}
80
+ },
81
+ "clientInfo": {
82
+ "name": "mcp-client",
83
+ "version": "1.0.0"
84
+ }
85
+ }
86
+ }
87
+
88
+ if self.process.stdin is not None:
89
+ self.process.stdin.write(json.dumps(init_request) + "\n")
90
+ self.process.stdin.flush()
91
+
92
+ # 读取响应
93
+ if self.process.stdout is not None:
94
+ response_line = self.process.stdout.readline()
95
+ response_data = json.loads(response_line.strip())
96
+ else:
97
+ raise Exception("无法读取进程输出")
98
+
99
+ if "result" in response_data:
100
+ # 发送初始化完成通知
101
+ initialized_notification = {
102
+ "jsonrpc": "2.0",
103
+ "method": "notifications/initialized"
104
+ }
105
+ if self.process.stdin is not None:
106
+ self.process.stdin.write(json.dumps(initialized_notification) + "\n")
107
+ self.process.stdin.flush()
108
+ self.initialized = True
109
+ return True
110
+
111
+ else:
112
+ # HTTP方式初始化
113
+ init_request = {
114
+ "jsonrpc": "2.0",
115
+ "id": 1,
116
+ "method": "initialize",
117
+ "params": {
118
+ "protocolVersion": "2025-06-18",
119
+ "capabilities": {
120
+ "experimental": {},
121
+ "sampling": {},
122
+ "elicitations": {},
123
+ "roots": {}
124
+ },
125
+ "clientInfo": {
126
+ "name": "mcp-http-client",
127
+ "version": "1.0.0"
128
+ }
129
+ }
130
+ }
131
+
132
+ init_response = requests.post(
133
+ self.url,
134
+ json=init_request,
135
+ headers={**self.headers, "Content-Type": "application/json"},
136
+ timeout=10
137
+ )
138
+
139
+ if init_response.status_code == 200:
140
+ response_data = init_response.json()
141
+ if "result" in response_data:
142
+ # 发送初始化完成通知
143
+ initialized_notification = {
144
+ "jsonrpc": "2.0",
145
+ "method": "notifications/initialized"
146
+ }
147
+ requests.post(
148
+ self.url,
149
+ json=initialized_notification,
150
+ headers={**self.headers, "Content-Type": "application/json"}
151
+ )
152
+ self.initialized = True
153
+ return True
154
+
155
+ print(f"MCP初始化失败")
156
+ return False
157
+
158
+ except Exception as e:
159
+ print(f"MCP初始化错误: {e}")
160
+ return False
161
+
162
+ def call_tool(self, tool_name: str, arguments: Dict[str, Any], timeout: int = 90) -> Dict[str, Any]:
163
+ """
164
+ 调用MCP工具
165
+
166
+ Args:
167
+ tool_name: 工具名称
168
+ arguments: 工具参数
169
+ timeout: 超时时间(秒)
170
+
171
+ Returns:
172
+ Dict[str, Any]: 工具执行结果
173
+ """
174
+ # 确保已初始化
175
+ if not self.initialized:
176
+ if not self.initialize():
177
+ raise Exception("MCP会话初始化失败")
178
+
179
+ if self.connection_type == "local":
180
+ # 本地命令行方式调用工具
181
+ tool_request = {
182
+ "jsonrpc": "2.0",
183
+ "id": 1,
184
+ "method": "tools/call",
185
+ "params": {
186
+ "name": tool_name,
187
+ "arguments": arguments
188
+ }
189
+ }
190
+
191
+ if self.process is not None and self.process.stdin is not None:
192
+ self.process.stdin.write(json.dumps(tool_request) + "\n")
193
+ self.process.stdin.flush()
194
+
195
+ # 读取响应
196
+ if self.process is not None and self.process.stdout is not None:
197
+ response_line = self.process.stdout.readline()
198
+ response_data = json.loads(response_line.strip())
199
+ else:
200
+ raise Exception("无法读取进程输出")
201
+
202
+ # 检查是否有错误
203
+ if "error" in response_data:
204
+ error = response_data["error"]
205
+ raise Exception(f"MCP错误 {error.get('code', 'unknown')}: {error.get('message', 'Unknown error')}")
206
+
207
+ # 返回结果
208
+ return response_data.get("result", {})
209
+ else:
210
+ # HTTP方式调用工具
211
+ tool_request = {
212
+ "jsonrpc": "2.0",
213
+ "id": 1,
214
+ "method": "tools/call",
215
+ "params": {
216
+ "name": tool_name,
217
+ "arguments": arguments
218
+ }
219
+ }
220
+
221
+ tool_response = requests.post(
222
+ self.url,
223
+ json=tool_request,
224
+ headers={**self.headers, "Content-Type": "application/json"},
225
+ timeout=timeout
226
+ )
227
+
228
+ if tool_response.status_code != 200:
229
+ raise Exception(f"工具调用请求失败: HTTP {tool_response.status_code}")
230
+
231
+ response_data = tool_response.json()
232
+
233
+ # 检查是否有错误
234
+ if "error" in response_data:
235
+ error = response_data["error"]
236
+ raise Exception(f"MCP错误 {error.get('code', 'unknown')}: {error.get('message', 'Unknown error')}")
237
+
238
+ # 返回结果
239
+ return response_data.get("result", {})
240
+
241
+ def close(self) -> None:
242
+ """关闭MCP客户端连接"""
243
+ if self.connection_type == "local" and self.process:
244
+ self.process.terminate()
245
+ self.process.wait()
246
+ # 对于HTTP客户端,不需要特殊清理
247
+ pass
248
+
249
+
250
+ # 使用示例
251
+ if __name__ == "__main__":
252
+ # Alpha Vantage MCP配置(本地命令行方式)
253
+ mcp_config = {
254
+ "mcpServers": {
255
+ "alpha-vantage": {
256
+ "command": "npx",
257
+ "args": ["alpha-ventage-mcp"],
258
+ "env": {
259
+ "ALPHA_VANTAGE_API_KEY": "97Q9TT7I6J9ZOLDS"
260
+ }
261
+ }
262
+ }
263
+ }
264
+
265
+ # 创建MCP客户端
266
+ client = MCP_Client(mcp_config)
267
+
268
+ try:
269
+ # 初始化MCP会话
270
+ print("正在初始化MCP会话...")
271
+ if client.initialize():
272
+ print("MCP会话初始化成功")
273
+ else:
274
+ print("MCP会话初始化失败")
275
+ exit(1)
276
+
277
+ # 示例:调用工具(请根据实际情况替换工具名称和参数)
278
+ print("MCP客户端已准备就绪,可以调用工具")
279
+
280
+ except Exception as e:
281
+ print(f"发生错误: {e}")
282
+
283
+ finally:
284
+ # 关闭连接
285
+ client.close()
286
+ print("MCP客户端连接已关闭")
service/mysql_service.py ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ from sqlalchemy import create_engine, text
4
+
5
+ # 数据库连接配置
6
+ DB_CONFIG = {
7
+ 'host': 'rm-j6c5yhe0l739e7752vo.mysql.cnhk.rds.aliyuncs.com',
8
+ 'user': 'report_user',
9
+ 'password': 'report_user_123',
10
+ 'database': 'easy_financial_report'
11
+ }
12
+
13
+ def get_database_url():
14
+ """构造数据库连接URL"""
15
+ return f"mysql+pymysql://{DB_CONFIG['user']}:{DB_CONFIG['password']}@{DB_CONFIG['host']}/{DB_CONFIG['database']}"
16
+
17
+ def execute_query_with_connection(engine, query, params=None):
18
+ """
19
+ 自动打开和关闭数据库连接执行查询
20
+
21
+ Args:
22
+ engine: SQLAlchemy引擎实例
23
+ query: SQL查询语句
24
+ params: 查询参数(可选)
25
+
26
+ Returns:
27
+ 查询结果或错误信息
28
+ """
29
+ try:
30
+ with engine.connect() as conn:
31
+ if params:
32
+ result = conn.execute(text(query), params)
33
+ else:
34
+ result = conn.execute(text(query))
35
+ return result.fetchall()
36
+ except Exception as e:
37
+ return f"查询执行失败: {str(e)}"
38
+
39
+ def execute_query(query):
40
+ """执行SQL查询并返回结果"""
41
+ if not query.strip():
42
+ return "请输入SQL查询语句"
43
+
44
+ try:
45
+ # 创建数据库连接引擎
46
+ engine = create_engine(get_database_url())
47
+
48
+ # 使用上下文管理器执行查询
49
+ with engine.connect() as conn:
50
+ df = pd.read_sql_query(text(query), conn)
51
+ return df
52
+ except Exception as e:
53
+ return f"查询执行失败: {str(e)}"
54
+ # 引擎会在with语句结束后自动清理连接
55
+
56
+ def get_table_names():
57
+ """获取数据库中的所有表名"""
58
+ try:
59
+ # 创建数据库连接引擎
60
+ engine = create_engine(get_database_url())
61
+
62
+ # 使用上下文管理器执行查询
63
+ with engine.connect() as conn:
64
+ # 查询所有表名
65
+ query = "SHOW TABLES"
66
+ df = pd.read_sql_query(text(query), conn)
67
+
68
+ # 返回表名列表
69
+ return df.iloc[:, 0].tolist() if not df.empty else []
70
+ except Exception as e:
71
+ return [f"获取表名失败: {str(e)}"]
72
+ # 引擎会在with语句结束后自动清理连接
73
+
74
+ def preview_table(table_name):
75
+ """预览表的前几行数据"""
76
+ if not table_name or "失败" in table_name:
77
+ return "请选择有效的表名"
78
+
79
+ query = f"SELECT * FROM {table_name} LIMIT 10"
80
+ return execute_query(query)
81
+
82
+ # 新增功能函数
83
+ def insert_record(title):
84
+ """向report_file_link表插入新记录"""
85
+ if not title.strip():
86
+ return "请输入标题"
87
+
88
+ try:
89
+ engine = create_engine(get_database_url())
90
+ # 插入新记录
91
+ query = "INSERT INTO report_file_link (title) VALUES (:title)"
92
+ with engine.connect() as conn:
93
+ trans = conn.begin()
94
+ try:
95
+ conn.execute(text(query), {"title": title})
96
+ trans.commit()
97
+ return f"成功插入记录: {title}"
98
+ except Exception as e:
99
+ trans.rollback()
100
+ raise e
101
+ except Exception as e:
102
+ return f"插入记录失败: {str(e)}"
103
+ # 引擎会在with语句结束后自动清理连接
104
+
105
+ def update_record(record_id, new_title):
106
+ """更新report_file_link表中的记录"""
107
+ if not record_id or not new_title.strip():
108
+ return "请输入记录ID和新标题"
109
+
110
+ try:
111
+ engine = create_engine(get_database_url())
112
+ # 更新记录
113
+ query = "UPDATE report_file_link SET title = :title WHERE id = :id"
114
+ with engine.connect() as conn:
115
+ trans = conn.begin()
116
+ try:
117
+ result = conn.execute(text(query), {"title": new_title, "id": record_id})
118
+ trans.commit()
119
+
120
+ if result.rowcount > 0:
121
+ return f"成功更新记录ID {record_id} 的标题为: {new_title}"
122
+ else:
123
+ return f"未找到ID为 {record_id} 的记录"
124
+ except Exception as e:
125
+ trans.rollback()
126
+ raise e
127
+ except Exception as e:
128
+ return f"更新记录失败: {str(e)}"
129
+ # 引擎会在with语句结束后自动清理连接
130
+
131
+ def delete_record(record_id):
132
+ """从report_file_link表中删除记录"""
133
+ if not record_id:
134
+ return "请输入记录ID"
135
+
136
+ try:
137
+ engine = create_engine(get_database_url())
138
+ # 删除记录
139
+ query = "DELETE FROM report_file_link WHERE id = :id"
140
+ with engine.connect() as conn:
141
+ trans = conn.begin()
142
+ try:
143
+ result = conn.execute(text(query), {"id": record_id})
144
+ trans.commit()
145
+
146
+ if result.rowcount > 0:
147
+ return f"成功删除ID为 {record_id} 的记录"
148
+ else:
149
+ return f"未找���ID为 {record_id} 的记录"
150
+ except Exception as e:
151
+ trans.rollback()
152
+ raise e
153
+ except Exception as e:
154
+ return f"删除记录失败: {str(e)}"
155
+ # 引擎会在with语句结束后自动清理连接
156
+
157
+ def refresh_report_file_link():
158
+ """刷新report_file_link表的数据"""
159
+ return execute_query("SELECT * FROM report_file_link")
160
+
161
+ # 新增功能函数
162
+ def insert_company(company_name, stock_code):
163
+ try:
164
+ engine = create_engine(get_database_url())
165
+ # 插入新记录
166
+ query = "INSERT INTO company (company_name, stock_code) VALUES (:company_name, :stock_code)"
167
+ with engine.connect() as conn:
168
+ trans = conn.begin()
169
+ try:
170
+ conn.execute(text(query), {"company_name": company_name, "stock_code": stock_code})
171
+ trans.commit()
172
+ return True
173
+ except Exception as e:
174
+ trans.rollback()
175
+ raise e
176
+ except Exception as e:
177
+ return False
178
+ # 引擎会在with语句结束后自动清理连接
179
+
180
+ def get_companys():
181
+ """获取company表中的所有公司"""
182
+ query = "SELECT * FROM company"
183
+ return execute_query(query)
184
+
185
+ def get_company_by_name(company_name):
186
+ """根据公司名称获取公司信息"""
187
+ query = "SELECT * FROM company WHERE company_name = :company_name"
188
+ try:
189
+ engine = create_engine(get_database_url())
190
+ with engine.connect() as conn:
191
+ df = pd.read_sql_query(text(query), conn, params={"company_name": company_name})
192
+ return df
193
+ except Exception as e:
194
+ return f"查询执行失败: {str(e)}"
service/tool_processor.py ADDED
@@ -0,0 +1,391 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import time
3
+ from .mcp_client import MCP_Client
4
+
5
+ BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1"
6
+ MODEL = "qwen3-max"
7
+ TOKEN = "sk-ef26097310ec45c184e8d84b31ea9356"
8
+
9
+ # Alpha Vantage MCP配置
10
+ MCP_CONFIG = {
11
+ "mcpServers": {
12
+ "alpha-vantage": {
13
+ "command": "npx",
14
+ "args": ["alpha-ventage-mcp"],
15
+ "env": {
16
+ "ALPHA_VANTAGE_API_KEY": "97Q9TT7I6J9ZOLDS"
17
+ }
18
+ }
19
+ }
20
+ }
21
+
22
+
23
+ def extract_stock_symbol(user_input: str) -> str:
24
+ """
25
+ 从用户输入中提取股票代码
26
+ 支持多种格式:
27
+ - "查询阿里巴巴股票" -> BABA
28
+ - "查询AAPL股票" -> AAPL
29
+ - "我想了解MSFT的股价" -> MSFT
30
+ - "BABA股价如何" -> BABA
31
+ """
32
+ # 预定义一些常见股票代码的映射
33
+ stock_mapping = {
34
+ "阿里巴巴": "BABA",
35
+ "阿里": "BABA",
36
+ "腾讯": "TCEHY",
37
+ "微软": "MSFT",
38
+ "苹果": "AAPL",
39
+ "谷歌": "GOOGL",
40
+ "亚马逊": "AMZN",
41
+ "特斯拉": "TSLA",
42
+ "英伟达": "NVDA",
43
+ "百度": "BIDU",
44
+ "京东": "JD",
45
+ "拼多多": "PDD"
46
+ }
47
+
48
+ # 检查预定义映射
49
+ for chinese_name, symbol in stock_mapping.items():
50
+ if chinese_name in user_input:
51
+ return symbol
52
+
53
+ # 尝试直接匹配常见的股票代码格式 (如 AAPL, MSFT 等)
54
+ import re
55
+ # 匹配常见的股票代码格式
56
+ patterns = [
57
+ r'\b([A-Z]{1,5})\b', # 大写字母组成的股票代码
58
+ r'股票代码[是为]?([A-Z]{1,5})',
59
+ r'([A-Z]{1,5})股票'
60
+ ]
61
+
62
+ for pattern in patterns:
63
+ match = re.search(pattern, user_input)
64
+ if match:
65
+ symbol = match.group(1)
66
+ # 验证是否为有效的股票代码格式
67
+ if 1 <= len(symbol) <= 5 and symbol.isalpha():
68
+ return symbol
69
+
70
+ # 默认返回阿里巴巴
71
+ return "BABA"
72
+
73
+
74
+ def get_stock_price(symbol: str):
75
+ """获取股票价格"""
76
+ client = MCP_Client(MCP_CONFIG)
77
+ if not client.initialize():
78
+ raise Exception("MCP初始化失败")
79
+
80
+ try:
81
+ result = client.call_tool("get_stock_price", {"symbol": symbol})
82
+ client.close()
83
+ return result
84
+ except Exception as e:
85
+ client.close()
86
+ raise e
87
+
88
+
89
+ def get_company_overview(symbol: str):
90
+ """获取公司概况"""
91
+ client = MCP_Client(MCP_CONFIG)
92
+ if not client.initialize():
93
+ raise Exception("MCP初始化失败")
94
+
95
+ try:
96
+ result = client.call_tool("get_company_overview", {"symbol": symbol})
97
+ client.close()
98
+ return result
99
+ except Exception as e:
100
+ client.close()
101
+ raise e
102
+
103
+
104
+ def get_daily_time_series(symbol: str):
105
+ """获取每日时间序列数据"""
106
+ client = MCP_Client(MCP_CONFIG)
107
+ if not client.initialize():
108
+ raise Exception("MCP初始化失败")
109
+
110
+ try:
111
+ result = client.call_tool("get_daily_time_series", {"symbol": symbol})
112
+ client.close()
113
+ return result
114
+ except Exception as e:
115
+ client.close()
116
+ raise e
117
+
118
+
119
+ def get_forex_rate(from_currency: str = "USD", to_currency: str = "CNY"):
120
+ """获取外汇汇率"""
121
+ client = MCP_Client(MCP_CONFIG)
122
+ if not client.initialize():
123
+ raise Exception("MCP初始化失败")
124
+
125
+ try:
126
+ result = client.call_tool("get_forex_rate", {
127
+ "from_currency": from_currency,
128
+ "to_currency": to_currency
129
+ })
130
+ client.close()
131
+ return result
132
+ except Exception as e:
133
+ client.close()
134
+ raise e
135
+
136
+
137
+ def get_tools_for_stock(symbol: str):
138
+ """根据股票代码生成工具列表"""
139
+ return [
140
+ {
141
+ "name": "get_stock_price",
142
+ "description": "获取股票价格",
143
+ "params": {"symbol": symbol},
144
+ },
145
+ {
146
+ "name": "get_company_overview",
147
+ "description": "获取公司概况",
148
+ "params": {"symbol": symbol},
149
+ },
150
+ {
151
+ "name": "get_daily_time_series",
152
+ "description": "获取每日时间序列数据",
153
+ "params": {"symbol": symbol},
154
+ },
155
+ {
156
+ "name": "get_forex_rate",
157
+ "description": "获取外汇汇率",
158
+ "params": {"from_currency": "USD", "to_currency": "CNY"},
159
+ }
160
+ ]
161
+
162
+
163
+ def call_llm(prompt: str):
164
+ """模拟调用大模型的函数(你可以替换为真实请求)"""
165
+ import requests
166
+
167
+ headers = {
168
+ "Authorization": f"Bearer {TOKEN}",
169
+ "Content-Type": "application/json"
170
+ }
171
+ data = {
172
+ "model": MODEL,
173
+ "messages": [{"role": "user", "content": prompt}],
174
+ "stream": True # 启用流式响应
175
+ }
176
+ response = requests.post(f"{BASE_URL}/chat/completions", headers=headers, json=data, stream=True)
177
+ for line in response.iter_lines():
178
+ if line:
179
+ yield line.decode('utf-8')
180
+
181
+
182
+ def get_stock_data_example(symbol: str):
183
+ """示例函数:获取股票数据的简化调用方式"""
184
+ try:
185
+ # 获取股票价格
186
+ price_data = get_stock_price(symbol)
187
+ print(f"股票价格数据: {price_data}")
188
+
189
+ # 获取公司概况
190
+ overview_data = get_company_overview(symbol)
191
+ print(f"公司概况数据: {overview_data}")
192
+
193
+ # 获取每日时间序列
194
+ time_series_data = get_daily_time_series(symbol)
195
+ print(f"时间序列数据: {time_series_data}")
196
+
197
+ # 获取外汇汇率
198
+ forex_data = get_forex_rate()
199
+ print(f"外汇汇率数据: {forex_data}")
200
+
201
+ return {
202
+ "price": price_data,
203
+ "overview": overview_data,
204
+ "time_series": time_series_data,
205
+ "forex": forex_data
206
+ }
207
+ except Exception as e:
208
+ print(f"获取股票数据时出错: {str(e)}")
209
+ raise e
210
+
211
+
212
+ def process_tool_analysis(user_input: str, history: list):
213
+ """
214
+ 处理工具分析的主要逻辑
215
+ - user_input: str
216
+ - history: list of {"role": ..., "content": ...}
217
+ Returns generator for streaming.
218
+ """
219
+ if not user_input.strip():
220
+ yield "", history
221
+ return
222
+
223
+ # 添加用户消息到历史
224
+ history.append({"role": "user", "content": user_input})
225
+
226
+ try:
227
+ # 从用户输入中提取股票代码
228
+ stock_symbol = extract_stock_symbol(user_input)
229
+
230
+ # 根据股票代码生成工具列表
231
+ tools_to_test = get_tools_for_stock(stock_symbol)
232
+
233
+ # 创建MCP客户端
234
+ client = MCP_Client(MCP_CONFIG)
235
+
236
+ # 初始化MCP会话
237
+ if not client.initialize():
238
+ error_msg = "❌ MCP初始化失败"
239
+ history.append({"role": "assistant", "content": error_msg})
240
+ yield "", history
241
+ return
242
+
243
+ # 收集所有工具的查询结果
244
+ all_results = []
245
+
246
+ # 依次调用每个工具
247
+ for i, tool in enumerate(tools_to_test, 1):
248
+ # 添加工具标题(使用HTML实现可折叠效果,默认折叠)
249
+ tool_content = f'''<details>
250
+ <summary class="tool-header querying" style="background-color: #f0f8ff; border: 1px solid #d0e0f0; border-radius: 5px; padding: 10px; margin: 10px 0; color: #1e40af; font-weight: bold; cursor: pointer;">
251
+ [MCP] <span class="status-tag querying">查询中</span> 🔧 工具 {i}/{len(tools_to_test)}: {tool['name']} ({tool['description']})
252
+ </summary>
253
+ <div style="border: 1px solid #d0e0f0; border-top: none; border-radius: 0 0 5px 5px; padding: 15px; background-color: #f8fafc; margin-bottom: 10px;">'''
254
+
255
+ history.append({"role": "assistant", "content": tool_content})
256
+ yield "", history
257
+
258
+ try:
259
+ result = client.call_tool(tool["name"], tool["params"])
260
+
261
+ # 解析结果
262
+ if isinstance(result, dict) and "content" in result:
263
+ content = result["content"]
264
+ if isinstance(content, list) and len(content) > 0:
265
+ text_content = content[0].get("text", "") if isinstance(content[0], dict) else str(content[0])
266
+ # 立即调用模型总结这个信息内容
267
+ summary_prompt = f"给我总结这个信息内容:{text_content}"
268
+ # 更新状态为分析中
269
+ # 找到完整的summary标签并更新状态
270
+ start_idx = history[-1]["content"].find('<summary')
271
+ end_idx = history[-1]["content"].find('</summary>') + len('</summary>')
272
+ if start_idx != -1 and end_idx != -1:
273
+ summary_content = history[-1]["content"][start_idx:end_idx]
274
+ # 更新工具标题的类名和状态文本(添加过渡效果)
275
+ updated_summary = summary_content.replace('tool-header querying',
276
+ 'tool-header analyzing').replace(
277
+ 'status-tag querying', 'status-tag analyzing').replace('查询中', '分析中')
278
+ history[-1]["content"] = history[-1]["content"].replace(summary_content, updated_summary)
279
+ yield "", history
280
+
281
+ # 流式显示模型处理结果(在可折叠面板内部)
282
+ model_summary = ""
283
+ # 移除所有冗余提示文本,直接显示分析结果区域
284
+ history[-1]["content"] += "<div id='analysis-result'>"
285
+ yield "", history
286
+
287
+ for chunk in call_llm(summary_prompt):
288
+ if not chunk or chunk == "[DONE]":
289
+ continue
290
+
291
+ if chunk.startswith("data: "):
292
+ chunk = chunk[6:]
293
+
294
+ try:
295
+ response_data = json.loads(chunk)
296
+ delta = response_data.get("choices", [{}])[0].get("delta", {})
297
+ content = delta.get("content", "")
298
+ if content:
299
+ model_summary += content
300
+ # 更新分析结果(在可折叠面板内部)
301
+ history[-1]["content"] = history[-1]["content"][0:history[-1]["content"].find(
302
+ "<div id='analysis-result'>") + 26] + model_summary
303
+ yield "", history
304
+ except json.JSONDecodeError:
305
+ continue # 忽略无效 JSON
306
+
307
+ history[-1]["content"] += "</div>"
308
+
309
+ result_msg = f"✅ {tool['name']}: {model_summary}"
310
+ all_results.append(f"{tool['description']}: {model_summary}")
311
+ # 添加最终结果到历史(在可折叠面板内部),移除所有冗余提示文本
312
+ history[-1]["content"] += f"<p><strong>分析结果:</strong></p><p>{result_msg}</p>"
313
+
314
+ # 更新状态为已完成(添加过渡效果)
315
+ # 找到完整的summary标签并更新状态
316
+ start_idx = history[-1]["content"].find('<summary')
317
+ end_idx = history[-1]["content"].find('</summary>') + len('</summary>')
318
+ if start_idx != -1 and end_idx != -1:
319
+ summary_start = history[-1]["content"][start_idx:end_idx]
320
+ updated_summary = summary_start.replace('tool-header analyzing',
321
+ 'tool-header completed').replace(
322
+ 'status-tag analyzing', 'status-tag completed').replace('分析中', '已完成')
323
+ history[-1]["content"] = history[-1]["content"].replace(summary_start, updated_summary)
324
+
325
+ # 关闭可折叠面板
326
+ history[-1]["content"] += "</div>\n</details>"
327
+
328
+ except Exception as e:
329
+ error_msg = f"❌ {tool['name']} 查询失败: {str(e)}"
330
+ history.append({"role": "assistant", "content": error_msg})
331
+ yield "", history
332
+
333
+ # 关闭可折叠面板(即使是错误情况)
334
+ history[-1]["content"] += "</div>\n</details>"
335
+ all_results.append(f"{tool['name']}: 查询失败 - {str(e)}")
336
+
337
+ # 添加短暂延迟以改善用户体验
338
+ time.sleep(0.5)
339
+
340
+ # 关闭MCP连接
341
+ client.close()
342
+
343
+ # 将所有结果汇总后传递给AI模型进行总结
344
+ final_summary_header = '''<div style="border: 2px solid #4CAF50; border-radius: 8px; padding: 20px; background-color: #f8fff8; margin: 15px 0;">
345
+ <h3 style="color: #2E7D32; text-align: center; margin-top: 0;">📈 最终总结</h3>'''
346
+ history.append({"role": "assistant", "content": final_summary_header})
347
+ yield "", history
348
+
349
+ summary_msg = "📊 正在分析所有数据并生成总结..."
350
+ history.append({"role": "assistant", "content": summary_msg})
351
+ yield "", history
352
+
353
+ # 构造给AI模型的提示
354
+ all_results_text = "\n".join(all_results)
355
+ summary_prompt = f"""用户问题: {user_input}
356
+
357
+ 收集到的金融数据:
358
+ {all_results_text}
359
+
360
+ 请根据以上数据回答用户问题,提供简洁明了的总结。"""
361
+
362
+ # 调用AI模型进行总结
363
+ bot_response = ""
364
+ for chunk in call_llm(summary_prompt):
365
+ if not chunk or chunk == "[DONE]":
366
+ continue
367
+
368
+ if chunk.startswith("data: "):
369
+ chunk = chunk[6:]
370
+
371
+ try:
372
+ response_data = json.loads(chunk)
373
+ delta = response_data.get("choices", [{}])[0].get("delta", {})
374
+ content = delta.get("content", "")
375
+ if content:
376
+ bot_response += content
377
+ # 更新累积历史记录中的最后一条消息,实现流式显示
378
+ history[-1]["content"] = "📊 正在分析所有数据并生成总结...\n" + bot_response
379
+ yield "", history
380
+ except json.JSONDecodeError:
381
+ continue # 忽略无效 JSON
382
+
383
+ # 最终完整历史
384
+ final_footer = "</div>"
385
+ history.append({"role": "assistant", "content": bot_response + final_footer})
386
+ yield "", history
387
+
388
+ except Exception as e:
389
+ error_msg = f"❌ 错误: {str(e)}"
390
+ history.append({"role": "assistant", "content": error_msg})
391
+ yield "", history