baba521 commited on
Commit
3a34de2
·
1 Parent(s): 07e93cd

集成机器人

Browse files
app.py CHANGED
@@ -10,6 +10,7 @@ from service.mysql_service import get_companys, insert_company, get_company_by_n
10
  from service.chat_service import get_analysis_report, get_stock_price_from_bailian, search_company, search_news, get_invest_suggest, chat_bot
11
  from service.company import check_company_exists
12
  from service.hf_upload import get_hf_files_with_links
 
13
  from service.report_mcp import query_financial_data
14
  from service.report_tools import build_financial_metrics_three_year_data, calculate_yoy_comparison, extract_financial_table, extract_last_three_with_fallback, get_yearly_data
15
  from service.three_year_table_tool import build_table_format
@@ -485,10 +486,11 @@ def create_company_buttons():
485
 
486
  # 返回按钮字典
487
  return company_buttons
488
-
489
- def update_report_section(selected_company):
490
  """根据选中的公司更新报告部分"""
491
- if selected_company == "" or selected_company is None:
 
 
492
  # 没有选中的公司,显示公司列表
493
  # html_content = get_initial_company_list_content()
494
  # 暂时返回空内容,稍后会用Gradio组件替换
@@ -496,21 +498,24 @@ def update_report_section(selected_company):
496
  return gr.update(value=html_content, visible=True)
497
  else:
498
  # 有选中的公司,显示相关报告
499
- try:
500
- # 尝试从Hugging Face获取文件列表
501
- report_data = get_hf_files_with_links("JC321/files-world")
502
- except Exception as e:
503
- # 如果获取失败,使用模拟数据并显示错误消息
504
- print(f"获取Hugging Face文件列表失败: {str(e)}")
505
- report_data = []
 
 
 
506
 
507
  html_content = '<div class="report-list-box bg-white">'
508
  html_content += '<div class="report-list-title bg-white card-title left-card-title" style="border-bottom: 1px solid #e3e3e6 !important;"><h3>Financial Reports</h3></div>'
509
  for report in report_data:
510
  html_content += f'''
511
- <div class="report-item bg-white hover:bg-blue-50 cursor-pointer" onclick="window.open('{report['link']}', '_blank')">
512
  <div class="report-item-content">
513
- <span class="text-gray-800">{report['title']}</span>
514
  <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" class="text-blue-500" viewBox="0 0 20 20" fill="currentColor">
515
  <path fill-rule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 10-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l-1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd" />
516
  </svg>
@@ -520,44 +525,47 @@ def update_report_section(selected_company):
520
 
521
  html_content += f'<div class="pdf-footer mt-3"><span class="text-xs text-gray-500">共{len(report_data)}份报告</span></div>'
522
  html_content += '</div>'
523
-
 
 
 
 
 
 
 
 
 
 
524
  try:
525
- # report_data = [
526
- # {
527
- # "title": "Alibaba Reports Q2 FY2026 Revenue of RMB 247.8 Billion, Up 15% YoY",
528
- # "url": "https://www.alibabagroup.com/en/news/article?news=p251125",
529
- # "publishedAt": "2025-11-25T08:00:00Z"
530
- # },
531
- # {
532
- # "title": "Alibaba Cloud Revenue Surges 34% Amid Strong AI Demand",
533
- # "url": "https://www.reuters.com/technology/alibaba-cloud-revenue-jumps-34-percent-ai-push-2025-11-25/",
534
- # "publishedAt": "2025-11-25T09:30:00Z"
535
- # }
536
- # ]
537
- report_data = search_news(selected_company)
538
- news_html = "<div class='news-list-box bg-white'>"
539
- news_html += '<div class="report-list-title bg-white card-title left-card-title" style="border-bottom: 1px solid #e3e3e6 !important;"><h3>News</h3></div>'
540
- from datetime import datetime
541
-
542
- for news in report_data:
543
- published_at = news['publishedAt']
544
-
545
- # 解析 ISO 8601 时间字符串(注意:strptime 不直接支持 'Z',需替换或使用 fromisoformat)
546
- dt = datetime.fromisoformat(published_at.replace("Z", "+00:00"))
547
-
548
- # 格式化为 YYYY.MM.DD
549
- formatted_date = dt.strftime("%Y.%m.%d")
550
- news_html += f'''
551
- <div class="news-item bg-white hover:bg-blue-50 cursor-pointer" onclick="window.open('{news['url']}', '_blank')">
552
- <div class="news-item-content">
553
- <span class="text-xs text-gray-500">[{formatted_date}]</span>
554
- <span class="text-gray-800">{news['title']}</span>
555
  </div>
556
- </div>
557
- '''
558
- news_html += f'<div class="pdf-footer mt-3"><span class="text-xs text-gray-500">共{len(report_data)}条新闻</span></div>'
559
- news_html += '</div>'
560
- html_content += news_html
561
  except Exception as e:
562
  print(f"Error updating report section: {str(e)}")
563
 
@@ -669,6 +677,12 @@ def create_report_section():
669
  report_display = gr.HTML(initial_content)
670
  return report_display
671
 
 
 
 
 
 
 
672
  def format_financial_metrics(data: dict, prev_data: dict = None) -> list: # pyright: ignore[reportArgumentType]
673
  """
674
  将原始财务数据转换为 financial_metrics 格式。
@@ -840,7 +854,9 @@ def create_sidebar():
840
  with gr.Group(elem_classes=["report-news-box"]) as report_section_group:
841
  # gr.Markdown("### Financial Reports", elem_classes=["card-title", "left-card-title"])
842
  report_display = create_report_section()
843
-
 
 
844
  # 处理公司选择事件
845
  def select_company_handler(company_name):
846
  """处理公司选择事件的处理器"""
@@ -848,24 +864,25 @@ def create_sidebar():
848
  g.SELECT_COMPANY = company_name if company_name else ""
849
 
850
  # 更新报告部分的内容
851
- updated_report_display = update_report_section(company_name)
852
 
 
853
  # 根据是否选择了公司来决定显示/隐藏报告部分
854
  if company_name:
855
  # 有选中的公司,显示报告部分
856
- return gr.update(visible=True), updated_report_display
857
  else:
858
  # 没有选中的公司,隐藏报告部分
859
- return gr.update(visible=False), updated_report_display
860
 
861
  company_list.change(
862
  fn=select_company_handler,
863
  inputs=[company_list],
864
- outputs=[report_section_group, report_display]
865
  )
866
 
867
  # 返回公司列表组件和报告部分组件
868
- return company_list, report_section_group, report_display
869
 
870
  def build_income_table(table_data):
871
  # 兼容两种数据结构:
@@ -1026,12 +1043,12 @@ def create_metrics_dashboard():
1026
  <div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1027
  <div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1028
  <div style="font-size: 14px; color: #555;">Prev Close</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1029
- <div style="font-size: 14px; color: #555;">Vol</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1030
  </div>
1031
  </div>
1032
  """
1033
  return html
1034
-
1035
  # 构建中间卡片
1036
  def build_financial_metrics():
1037
  metrics_html = ""
@@ -1046,11 +1063,16 @@ def create_metrics_dashboard():
1046
 
1047
  html = f"""
1048
  <div style="min-width: 300px;max-width: 450px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1049
- <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
1050
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1051
- <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1052
- </svg>
1053
- <div style="font-size: 18px; font-weight: 600;">{yearly_data} Financial Metrics</div>
 
 
 
 
 
1054
  </div>
1055
  {metrics_html}
1056
  </div>
@@ -1095,13 +1117,31 @@ def update_metrics_dashboard(company_name):
1095
  # }
1096
  company_info = {}
1097
  # 尝试获取股票价格数据,但不中断程序执行
 
1098
  try:
1099
  # 根据选择的公司获取股票代码
1100
  stock_code = get_stock_code_by_company_name(company_name)
 
 
1101
  # company_info2 = get_stock_price(stock_code)
1102
- company_info2 = get_stock_price_from_bailian(stock_code)
1103
  # print(f"股票价格数据: {company_info2}")
1104
- company_info = company_info2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1105
  # 如果成功获取数据,则用实际数据替换模拟数据
1106
  # if company_info2 and "content" in company_info2 and len(company_info2["content"]) > 0:
1107
  # import json
@@ -1144,7 +1184,7 @@ def update_metrics_dashboard(company_name):
1144
  # {"label": "Operating Expenses", "value": "$1.2B", "change": "+5.1%", "color": "green"},
1145
  # {"label": "Cash Flow", "value": "$982M", "change": "+8.7%", "color": "green"}
1146
  # ]
1147
- financial_metrics_pre = query_financial_data(company_name, "5-Year")
1148
  # financial_metrics_pre = query_financial_data(company_name, "5年趋势")
1149
  # print(f"最新财务数据: {financial_metrics_pre}")
1150
  # financial_metrics = format_financial_metrics(financial_metrics_pre)
@@ -1173,6 +1213,27 @@ def update_metrics_dashboard(company_name):
1173
  year_data = result["year_data"]
1174
  three_year_data = result["three_year_data"]
1175
  print(f"格式化后的财务数据: {financial_metrics}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1176
  except Exception as e:
1177
  print(f"Error process_financial_data: {e}")
1178
 
@@ -1209,7 +1270,7 @@ def update_metrics_dashboard(company_name):
1209
  # ]
1210
  yearly_data = year_data
1211
  table_data = build_table_format(three_year_data)
1212
- print(f"table_data: {table_data}")
1213
  # yearly_data = None
1214
  # try:
1215
  # yearly_data = get_yearly_data(financial_metrics_pre)
@@ -1244,24 +1305,6 @@ def update_metrics_dashboard(company_name):
1244
  return f'<span style="color:{color};">▼{change}</span>'
1245
 
1246
  # 构建左侧卡片
1247
- def format_volume(vol_str):
1248
- """将成交量字符串转为带 M/B 的简洁格式"""
1249
- if vol_str == "N/A" or not vol_str:
1250
- return "N/A"
1251
- try:
1252
- vol = int(float(vol_str)) # 兼容 "21453064" 或 "2.145e7"
1253
- if vol >= 1_000_000_000:
1254
- val = vol / 1_000_000_000
1255
- return f"{val:.2f}B".rstrip('0').rstrip('.')
1256
- elif vol >= 1_000_000:
1257
- val = vol / 1_000_000
1258
- return f"{val:.2f}M".rstrip('0').rstrip('.')
1259
- elif vol >= 1_000:
1260
- return f"{vol // 1_000}K"
1261
- else:
1262
- return str(vol)
1263
- except (ValueError, TypeError):
1264
- return "N/A"
1265
  def build_stock_card(company_info):
1266
  try:
1267
  if not company_info or not isinstance(company_info, dict):
@@ -1273,7 +1316,7 @@ def update_metrics_dashboard(company_name):
1273
  else:
1274
  company_name = company_info.get("company", "N/A")
1275
  symbol = company_info.get("symbol", "N/A")
1276
- price = company_info.get("price", "N/A")
1277
 
1278
  # 解析 change
1279
  change_str = company_info.get("change", "0")
@@ -1283,11 +1326,11 @@ def update_metrics_dashboard(company_name):
1283
  change = 0.0
1284
 
1285
  # 解析 change_percent
1286
- change_percent_str = company_info.get("change_percent", "0%")
1287
- try:
1288
- change_percent = float(change_percent_str.rstrip('%'))
1289
- except (ValueError, TypeError):
1290
- change_percent = 0.0
1291
 
1292
  change_color = "green" if change >= 0 else "red"
1293
  sign = "+" if change >= 0 else ""
@@ -1297,14 +1340,14 @@ def update_metrics_dashboard(company_name):
1297
  open_val = company_info.get("open", "N/A")
1298
  high_val = company_info.get("high", "N/A")
1299
  low_val = company_info.get("low", "N/A")
1300
- prev_close_val = company_info.get("previous close", "N/A")
1301
- raw_volume = company_info.get("volume", "N/A")
1302
- volume_display = format_volume(raw_volume)
1303
 
1304
  html = f"""
1305
  <div style="width: 250px; height: 300px !important; border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1306
  <div style="font-size: 16px; color: #555; font-weight: 500;">{company_name}</div>
1307
- <div style="font-size: 12px; color: #888;">{symbol}</div>
1308
  <div style="display: flex; align-items: center; gap: 10px; margin: 8px 0;">
1309
  <div style="font-size: 32px; font-weight: bold;">{price}</div>
1310
  <div style="font-size: 14px;">{change_html}</div>
@@ -1314,56 +1357,16 @@ def update_metrics_dashboard(company_name):
1314
  <div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{high_val}</div>
1315
  <div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{low_val}</div>
1316
  <div style="font-size: 14px; color: #555;">Prev Close</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{prev_close_val}</div>
1317
- <div style="font-size: 14px; color: #555;">Vol</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{volume_display}</div>
1318
  </div>
1319
  </div>
1320
  """
 
 
1321
  return html
1322
 
1323
  except Exception as e:
1324
  print(f"Error building stock card: {e}")
1325
  return '<div style="width:250px; padding:16px; color:red;">Error loading stock data</div>'
1326
- # def build_stock_card():
1327
- # try:
1328
- # # 检查是否获取到了股票数据,如果没有则显示N/A
1329
- # if company_info:
1330
- # price = company_info["price"]
1331
- # change = int(company_info["change"])
1332
- # change_percent = company_info["change_percent"]
1333
-
1334
- # # 格式化价格变动
1335
- # change_color = "green" if change > 0 else "red"
1336
- # change_html = f'<span style="color:{change_color};">+{change:.2f}({change_percent:+.2f}%)</span>' if change > 0 else \
1337
- # f'<span style="color:{change_color};">{change:.2f}({change_percent:+.2f}%)</span>'
1338
- # else:
1339
- # # 如果没有获取到数据,所有数值显示N/A
1340
- # price = "N/A"
1341
- # change = 0
1342
- # change_percent = 0
1343
- # change_html = "<span style=\"color:#888;\">N/A</span>"
1344
- # html = f"""
1345
- # <div style="width: 250px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1346
- # <div style="font-size: 16px; color: #555;font-weight: 500;">{company_info['name']}</div>
1347
- # <div style="font-size: 12px; color: #888;">{company_info['symbol'] if (company_info and 'symbol' in company_info) else 'NYSE:N/A'}</div>
1348
- # <div style="display: flex; align-items: center; gap: 10px; margin: 8px 0;">
1349
- # <div style="font-size: 32px; font-weight: bold;">{price}</div>
1350
- # <div style="font-size: 14px;">{change_html}</div>
1351
- # </div>
1352
- # <div style="margin-top: 12px; display: grid; grid-template-columns: auto 1fr; gap: 8px;">
1353
- # <div style="font-size: 14px; color: #555;">Open</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['open'] if (company_info and 'open' in company_info) else 'N/A'}</div>
1354
- # <div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['high'] if (company_info and 'high' in company_info) else 'N/A'}</div>
1355
- # <div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['low'] if (company_info and 'low' in company_info) else 'N/A'}</div>
1356
- # <div style="font-size: 14px; color: #555;">Prev Close</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['prev_close'] if (company_info and 'prev_close' in company_info) else 'N/A'}</div>
1357
- # <div style="font-size: 14px; color: #555;">Vol</div><div style="font-size: 14px; font-weight: 500;text-align: center;">{company_info['volume'] if (company_info and 'volume' in company_info) else 'N/A'}</div>
1358
- # </div>
1359
- # </div>
1360
- # """
1361
- # return html
1362
- # except Exception as e:
1363
- # print(f"Error building stock card: {e}")
1364
-
1365
-
1366
-
1367
  # 构建中间卡片
1368
  def build_financial_metrics(yearly_data):
1369
  metrics_html = ""
@@ -1378,11 +1381,16 @@ def update_metrics_dashboard(company_name):
1378
 
1379
  html = f"""
1380
  <div style="width: 450px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1381
- <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;">
1382
- <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1383
- <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1384
- </svg>
1385
- <div style="font-size: 18px; font-weight: 600;">{yearly_data} Financial Metrics</div>
 
 
 
 
 
1386
  </div>
1387
  {metrics_html}
1388
  </div>
@@ -1443,114 +1451,6 @@ def update_metrics_dashboard(company_name):
1443
 
1444
  # 返回三个HTML组件的内容
1445
  return build_stock_card(company_info), build_financial_metrics(yearly_data), build_income_table(table_data)
1446
- # gr.Column(scale=1, min_width=250)
1447
- # gr.HTML(f'''
1448
- # <div class="metric-card-item" style="{card_custom_style}width:300px;">
1449
- # <div class="" style="padding-bottom: 12px;">
1450
- # <span class="" style="font-size: 24px;">NASDAQ100</span>
1451
- # </div>
1452
- # <div class="">
1453
- # <p class="" style="font-size: 40px;font-weight: 600;color: #333333;margin-top: 20px;">$106.50</p>
1454
- # <div class="">
1455
- # <span style="color: green;font-size: 26px;">+2.4%</span>
1456
- # <span style="color: green;font-size: 26px;margin-left: 10px;">↗</span>
1457
- # </div>
1458
- # </div>
1459
- # </div>
1460
- # ''')
1461
- # gr.HTML(f'''
1462
- # <div class="metric-card-item" style="{card_custom_style}">
1463
- # <div class="" style="padding-bottom: 12px;">
1464
- # <span class="" style="font-size: 24px;">NASDAQ100</span>
1465
- # </div>
1466
- # <div class="">
1467
- # <p class="" style="font-size: 40px;font-weight: 600;color: #333333;margin-top: 20px;">$106.50</p>
1468
- # <div class="">
1469
- # <span style="color: green;font-size: 26px;">+2.4%</span>
1470
- # <span style="color: green;font-size: 26px;margin-left: 10px;">↗</span>
1471
- # </div>
1472
- # </div>
1473
- # </div>
1474
- # ''')
1475
- # gr.HTML(f'''
1476
- # <div class="metric-card-item" style="{card_custom_style}">
1477
- # <div class="" style="padding-bottom: 12px;">
1478
- # <span class="" style="font-size: 24px;">NASDAQ100</span>
1479
- # </div>
1480
- # <div class="">
1481
- # <p class="" style="font-size: 40px;font-weight: 600;color: #333333;margin-top: 20px;">$106.50</p>
1482
- # <div class="">
1483
- # <span style="color: green;font-size: 26px;">+2.4%</span>
1484
- # <span style="color: green;font-size: 26px;margin-left: 10px;">↗</span>
1485
- # </div>
1486
- # </div>
1487
- # </div>
1488
- # ''')
1489
- # 总收入
1490
- with gr.Column(scale=1, min_width=250, elem_classes=["metric-card"]):
1491
- # with gr.Row(elem_classes=["justify-between"]):
1492
- # gr.Markdown("Total Revenue", elem_classes=["font-semibold", "text-gray-700"])
1493
- # gr.Markdown("💵", elem_classes=["text-green-500"])
1494
- # with gr.Column(elem_classes=["mt-4"]):
1495
- # gr.Markdown("$2.84B", elem_classes=["metric-value"])
1496
- # with gr.Row(elem_classes=["metric-change", "positive", "mt-1"]):
1497
- # gr.Markdown("+12.4% YoY")
1498
- # gr.Markdown("↗️", elem_classes=["text-green-500"])
1499
- gr.HTML(f'''
1500
- <div class="metric-card-item" style="
1501
- # width: 250px;
1502
- height: 150px;
1503
- # border: 1px solid red;
1504
- # padding: 10px;
1505
- ">
1506
- <div class="" style="
1507
- display: flex;
1508
- justify-content: space-between;
1509
- align-items: center;
1510
- ">
1511
- <span class="" style="font-size: 18px;">Real-time Stock Price</span>
1512
- </div>
1513
- <div class="">
1514
- <p class="" style="font-size: 30px;font-weight: 600;color: #333333;margin-top: 20px;">$106.50</p> <div class="">
1515
- <span style="color: green;font-size: 18px;">+2.4%</span>
1516
- <span style="color: green;font-size: 26px;margin-left: 10px;">↗</span>
1517
- </div>
1518
- </div>
1519
- </div>
1520
- ''')
1521
- # Financial Metrics
1522
-
1523
- with gr.Column(scale=9, elem_classes=["metric-card"]):
1524
- # with gr.Row(elem_classes=["justify-between"]):
1525
- # gr.Markdown("Total Revenue", elem_classes=["font-semibold", "text-gray-700"])
1526
- # gr.Markdown("💵", elem_classes=["text-green-500"])
1527
- # with gr.Column(elem_classes=["mt-4"]):
1528
- # gr.Markdown("$2.84B", elem_classes=["metric-value"])
1529
- # with gr.Row(elem_classes=["metric-change", "positive", "mt-1"]):
1530
- # gr.Markdown("+12.4% YoY")
1531
- # gr.Markdown("↗️", elem_classes=["text-green-500"])
1532
- gr.HTML(f'''
1533
- <div class="metric-card-item" style="
1534
- # width: 250px;
1535
- height: 150px;
1536
- # border: 1px solid red;
1537
- # padding: 10px;
1538
- ">
1539
- <div class="" style="
1540
- display: flex;
1541
- justify-content: space-between;
1542
- align-items: center;
1543
- ">
1544
- <span class="" style="font-size: 18px;">Real-time Stock Price</span>
1545
- </div>
1546
- <div class="">
1547
- <p class="" style="font-size: 30px;font-weight: 600;color: #333333;margin-top: 20px;">$106.50</p> <div class="">
1548
- <span style="color: green;font-size: 18px;">+2.4%</span>
1549
- <span style="color: green;font-size: 26px;margin-left: 10px;">↗</span>
1550
- </div>
1551
- </div>
1552
- </div>
1553
- ''')
1554
 
1555
  def create_tab_content(tab_name, company_name):
1556
  """创建Tab内容组件"""
@@ -1783,7 +1683,7 @@ def main():
1783
  # 左侧边栏
1784
  with gr.Column(scale=1, min_width=350):
1785
  # 获取company_list组件的引用
1786
- company_list_component, report_section_component, report_display_component = create_sidebar()
1787
 
1788
  # 主内容区域
1789
  with gr.Column(scale=9):
@@ -1907,26 +1807,54 @@ def main():
1907
  # create_tab_content("comparison")
1908
  with gr.Column(scale=2, min_width=400):
1909
  # 聊天面板
1910
- gr.ChatInterface(
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1911
  respond,
1912
  title="Easy Financial Report",
1913
- # label="Easy Financial Report",
1914
  additional_inputs=[
1915
- # gr.Textbox(value="You are a financial analysis assistant. Provide concise investment insights from company financial reports.", label="System message"),
1916
- # gr.Slider(minimum=1, maximum=4096, value=1024, step=1, label="Max new tokens"),
1917
- # gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"),
1918
- # gr.Slider(
1919
- # minimum=0.1,
1920
- # maximum=1.0,
1921
- # value=0.95,
1922
- # step=0.05,
1923
- # label="Top-p (nucleus sampling)",
1924
- # ),
1925
- gr.State(value="") # CRITICAL: Add State to store session URL across turns
1926
  ],
 
1927
  )
1928
- # chatbot.render()
1929
- gr.LoginButton()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1930
 
1931
  # 在页面加载时自动刷新公司列表,确保显示最新的数据
1932
  # demo.load(
 
10
  from service.chat_service import get_analysis_report, get_stock_price_from_bailian, search_company, search_news, get_invest_suggest, chat_bot
11
  from service.company import check_company_exists
12
  from service.hf_upload import get_hf_files_with_links
13
+ from service.news_quote_mcp import get_company_news, get_quote
14
  from service.report_mcp import query_financial_data
15
  from service.report_tools import build_financial_metrics_three_year_data, calculate_yoy_comparison, extract_financial_table, extract_last_three_with_fallback, get_yearly_data
16
  from service.three_year_table_tool import build_table_format
 
486
 
487
  # 返回按钮字典
488
  return company_buttons
489
+ def update_report_section(selected_company, report_data, stock_code):
 
490
  """根据选中的公司更新报告部分"""
491
+ print(f"Updating report (报告部分): {selected_company}") # 添加调试信息
492
+
493
+ if selected_company == "" or selected_company is None or selected_company == "Unknown":
494
  # 没有选中的公司,显示公司列表
495
  # html_content = get_initial_company_list_content()
496
  # 暂时返回空内容,稍后会用Gradio组件替换
 
498
  return gr.update(value=html_content, visible=True)
499
  else:
500
  # 有选中的公司,显示相关报告
501
+ # try:
502
+ # # 尝试从Hugging Face获取文件列表
503
+ # report_data = get_hf_files_with_links("JC321/files-world")
504
+ # except Exception as e:
505
+ # # 如果获取失败,使用模拟数据并显示错误消息
506
+ # print(f"获取Hugging Face文件列表失败: {str(e)}")
507
+ # report_data = []
508
+ stock_code = get_stock_code_by_company_name(selected_company)
509
+ report_data = query_financial_data(stock_code, "5-Year")
510
+ # report_data = process_financial_data_with_metadata(financial_metrics_pre)
511
 
512
  html_content = '<div class="report-list-box bg-white">'
513
  html_content += '<div class="report-list-title bg-white card-title left-card-title" style="border-bottom: 1px solid #e3e3e6 !important;"><h3>Financial Reports</h3></div>'
514
  for report in report_data:
515
  html_content += f'''
516
+ <div class="report-item bg-white hover:bg-blue-50 cursor-pointer" onclick="window.open('{report['source_url']}', '_blank')">
517
  <div class="report-item-content">
518
+ <span class="text-gray-800">{report['period']}-{stock_code}-{report['source_form']}</span>
519
  <svg xmlns="http://www.w3.org/2000/svg" width="12" height="12" class="text-blue-500" viewBox="0 0 20 20" fill="currentColor">
520
  <path fill-rule="evenodd" d="M12.586 4.586a2 2 0 112.828 2.828l-3 3a2 2 0 01-2.828 0 1 1 0 10-1.414 1.414 4 4 0 005.656 0l3-3a4 4 0 00-5.656-5.656l-1.5 1.5a1 1 0 101.414 1.414l-1.5-1.5zm-5 5a2 2 0 012.828 0 1 1 0 101.414-1.414 4 4 0 00-5.656 0l-3 3a4 4 0 105.656 5.656l1.5-1.5a1 1 0 10-1.414-1.414l-1.5 1.5a2 2 0 11-2.828-2.828l3-3z" clip-rule="evenodd" />
521
  </svg>
 
525
 
526
  html_content += f'<div class="pdf-footer mt-3"><span class="text-xs text-gray-500">共{len(report_data)}份报告</span></div>'
527
  html_content += '</div>'
528
+
529
+ return gr.update(value=html_content, visible=True)
530
+ def update_news_section(selected_company):
531
+ """根据选中的公司更新报告部分"""
532
+ html_content = ""
533
+ if selected_company == "" or selected_company is None:
534
+ # 没有选中的公司,显示公司列表
535
+ # html_content = get_initial_company_list_content()
536
+ # 暂时返回空内容,稍后会用Gradio组件替换
537
+ return gr.update(value=html_content, visible=True)
538
+ else:
539
  try:
540
+ stock_code = get_stock_code_by_company_name(selected_company)
541
+ report_data = get_company_news(stock_code, None, None)
542
+ # print(f"新闻列表: {report_data['articles']}")
543
+ # report_data = search_news(selected_company)
544
+ if (report_data['articles']):
545
+ report_data = report_data['articles']
546
+ news_html = "<div class='news-list-box bg-white'>"
547
+ news_html += '<div class="report-list-title bg-white card-title left-card-title" style="border-bottom: 1px solid #e3e3e6 !important;"><h3>News</h3></div>'
548
+ from datetime import datetime
549
+
550
+ for news in report_data:
551
+ published_at = news['published']
552
+
553
+ # 解析 ISO 8601 时间字符串(注意:strptime 不直接支持 'Z',需替换或使用 fromisoformat)
554
+ dt = datetime.fromisoformat(published_at.replace("Z", "+00:00"))
555
+
556
+ # 格式化为 YYYY.MM.DD
557
+ formatted_date = dt.strftime("%Y.%m.%d")
558
+ news_html += f'''
559
+ <div class="news-item bg-white hover:bg-blue-50 cursor-pointer" onclick="window.open('{news['url']}', '_blank')">
560
+ <div class="news-item-content">
561
+ <span class="text-xs text-gray-500">[{formatted_date}]</span>
562
+ <span class="text-gray-800">{news['headline']}</span>
563
+ </div>
 
 
 
 
 
 
564
  </div>
565
+ '''
566
+ news_html += f'<div class="pdf-footer mt-3"><span class="text-xs text-gray-500">共{len(report_data)}条新闻</span></div>'
567
+ news_html += '</div>'
568
+ html_content += news_html
 
569
  except Exception as e:
570
  print(f"Error updating report section: {str(e)}")
571
 
 
677
  report_display = gr.HTML(initial_content)
678
  return report_display
679
 
680
+ def create_news_section():
681
+ """创建新闻部分组件"""
682
+ initial_content = ""
683
+ news_display = gr.HTML(initial_content)
684
+ return news_display
685
+
686
  def format_financial_metrics(data: dict, prev_data: dict = None) -> list: # pyright: ignore[reportArgumentType]
687
  """
688
  将原始财务数据转换为 financial_metrics 格式。
 
854
  with gr.Group(elem_classes=["report-news-box"]) as report_section_group:
855
  # gr.Markdown("### Financial Reports", elem_classes=["card-title", "left-card-title"])
856
  report_display = create_report_section()
857
+ news_display = create_news_section()
858
+
859
+
860
  # 处理公司选择事件
861
  def select_company_handler(company_name):
862
  """处理公司选择事件的处理器"""
 
864
  g.SELECT_COMPANY = company_name if company_name else ""
865
 
866
  # 更新报告部分的内容
867
+ updated_report_display = update_report_section(company_name, None, None)
868
 
869
+ updated_news_display = update_news_section(company_name)
870
  # 根据是否选择了公司来决定显示/隐藏报告部分
871
  if company_name:
872
  # 有选中的公司,显示报告部分
873
+ return gr.update(visible=True), updated_report_display, updated_news_display
874
  else:
875
  # 没有选中的公司,隐藏报告部分
876
+ return gr.update(visible=False), updated_report_display, updated_news_display
877
 
878
  company_list.change(
879
  fn=select_company_handler,
880
  inputs=[company_list],
881
+ outputs=[report_section_group, report_display, news_display]
882
  )
883
 
884
  # 返回公司列表组件和报告部分组件
885
+ return company_list, report_section_group, report_display, news_display
886
 
887
  def build_income_table(table_data):
888
  # 兼容两种数据结构:
 
1043
  <div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1044
  <div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1045
  <div style="font-size: 14px; color: #555;">Prev Close</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1046
+
1047
  </div>
1048
  </div>
1049
  """
1050
  return html
1051
+ # <div style="font-size: 14px; color: #555;">Vol</div><div style="font-size: 14px; font-weight: 500;text-align: center;">N/A</div>
1052
  # 构建中间卡片
1053
  def build_financial_metrics():
1054
  metrics_html = ""
 
1063
 
1064
  html = f"""
1065
  <div style="min-width: 300px;max-width: 450px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1066
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;justify-content: space-between;">
1067
+ <div style="font-size: 18px; font-weight: 600;display: flex;align-items: center;">
1068
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1069
+ <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1070
+ </svg>
1071
+ <span style="margin-left: 10px;">{yearly_data} Financial Metrics</span>
1072
+ </div>
1073
+ <div style="font-size: 16px; color: #8f8f8f;">
1074
+ YTD data
1075
+ </div>
1076
  </div>
1077
  {metrics_html}
1078
  </div>
 
1117
  # }
1118
  company_info = {}
1119
  # 尝试获取股票价格数据,但不中断程序执行
1120
+ stock_code = ""
1121
  try:
1122
  # 根据选择的公司获取股票代码
1123
  stock_code = get_stock_code_by_company_name(company_name)
1124
+ # result = get_quote(company_name.strip())
1125
+
1126
  # company_info2 = get_stock_price(stock_code)
1127
+ # company_info2 = get_stock_price_from_bailian(stock_code)
1128
  # print(f"股票价格数据: {company_info2}")
1129
+ company_info = get_quote(stock_code.strip())
1130
+ company_info['company'] = company_name
1131
+ print(f"股票价格数据====: {company_info}")
1132
+ # 查询结果:{
1133
+ # "company": "阿里巴巴",
1134
+ # "symbol": "BABA",
1135
+ # "open": "159.09",
1136
+ # "high": "161.46",
1137
+ # "low": "150.00",
1138
+ # "price": "157.60",
1139
+ # "volume": "21453064",
1140
+ # "latest trading day": "2025-11-27",
1141
+ # "previous close": "157.01",
1142
+ # "change": "+0.59",
1143
+ # "change_percent": "+0.38%"
1144
+ # }BABA
1145
  # 如果成功获取数据,则用实际数据替换模拟数据
1146
  # if company_info2 and "content" in company_info2 and len(company_info2["content"]) > 0:
1147
  # import json
 
1184
  # {"label": "Operating Expenses", "value": "$1.2B", "change": "+5.1%", "color": "green"},
1185
  # {"label": "Cash Flow", "value": "$982M", "change": "+8.7%", "color": "green"}
1186
  # ]
1187
+ financial_metrics_pre = query_financial_data(stock_code, "5-Year")
1188
  # financial_metrics_pre = query_financial_data(company_name, "5年趋势")
1189
  # print(f"最新财务数据: {financial_metrics_pre}")
1190
  # financial_metrics = format_financial_metrics(financial_metrics_pre)
 
1213
  year_data = result["year_data"]
1214
  three_year_data = result["three_year_data"]
1215
  print(f"格式化后的财务数据: {financial_metrics}")
1216
+ # 拿report数据
1217
+ # try:
1218
+ # # 从 result 中获取报告数据
1219
+ # if 'report_data' in result: # 假设 result 中包含 report_data 键
1220
+ # report_data = result['report_data']
1221
+ # else:
1222
+ # # 如果 result 中没有直接包含 report_data,则从其他键中获取
1223
+ # # 这需要根据实际的 result 数据结构来调整
1224
+ # report_data = result.get('reports', []) # 示例:假设数据在 'reports' 键下
1225
+
1226
+ # 更新报告部分的内容
1227
+ # 这里需要调用 update_report_section 函数并传入 report_data
1228
+ # 注意:update_report_section 可能需要修改以接受 report_data 参数
1229
+ # updated_report_content = update_report_section(company_name, report_data, stock_code)
1230
+
1231
+ # 然后将 updated_report_content 返回,以便在 UI 中更新
1232
+ # 这需要修改函数的返回值以包含报告内容
1233
+
1234
+ # except Exception as e:
1235
+ # print(f"Error updating report section with result data: {e}")
1236
+ # updated_report_content = "<div>Failed to load report data</div>"
1237
  except Exception as e:
1238
  print(f"Error process_financial_data: {e}")
1239
 
 
1270
  # ]
1271
  yearly_data = year_data
1272
  table_data = build_table_format(three_year_data)
1273
+ # print(f"table_data: {table_data}")
1274
  # yearly_data = None
1275
  # try:
1276
  # yearly_data = get_yearly_data(financial_metrics_pre)
 
1305
  return f'<span style="color:{color};">▼{change}</span>'
1306
 
1307
  # 构建左侧卡片
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1308
  def build_stock_card(company_info):
1309
  try:
1310
  if not company_info or not isinstance(company_info, dict):
 
1316
  else:
1317
  company_name = company_info.get("company", "N/A")
1318
  symbol = company_info.get("symbol", "N/A")
1319
+ price = company_info.get("current_price", "N/A")
1320
 
1321
  # 解析 change
1322
  change_str = company_info.get("change", "0")
 
1326
  change = 0.0
1327
 
1328
  # 解析 change_percent
1329
+ change_percent = company_info.get("percent_change", "0%")
1330
+ # try:
1331
+ # change_percent = float(change_percent_str.rstrip('%'))
1332
+ # except (ValueError, TypeError):
1333
+ # change_percent = 0.0
1334
 
1335
  change_color = "green" if change >= 0 else "red"
1336
  sign = "+" if change >= 0 else ""
 
1340
  open_val = company_info.get("open", "N/A")
1341
  high_val = company_info.get("high", "N/A")
1342
  low_val = company_info.get("low", "N/A")
1343
+ prev_close_val = company_info.get("previous_close", "N/A")
1344
+ # raw_volume = company_info.get("volume", "N/A")
1345
+ # volume_display = format_volume(raw_volume)
1346
 
1347
  html = f"""
1348
  <div style="width: 250px; height: 300px !important; border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1349
  <div style="font-size: 16px; color: #555; font-weight: 500;">{company_name}</div>
1350
+ <div style="font-size: 12px; color: #888;">NYSE:{symbol}</div>
1351
  <div style="display: flex; align-items: center; gap: 10px; margin: 8px 0;">
1352
  <div style="font-size: 32px; font-weight: bold;">{price}</div>
1353
  <div style="font-size: 14px;">{change_html}</div>
 
1357
  <div style="font-size: 14px; color: #555;">High</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{high_val}</div>
1358
  <div style="font-size: 14px; color: #555;">Low</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{low_val}</div>
1359
  <div style="font-size: 14px; color: #555;">Prev Close</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{prev_close_val}</div>
 
1360
  </div>
1361
  </div>
1362
  """
1363
+ # <div style="font-size: 14px; color: #555;">Vol</div><div style="font-size: 14px; font-weight: 500; text-align: center;">{volume_display}</div>
1364
+
1365
  return html
1366
 
1367
  except Exception as e:
1368
  print(f"Error building stock card: {e}")
1369
  return '<div style="width:250px; padding:16px; color:red;">Error loading stock data</div>'
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1370
  # 构建中间卡片
1371
  def build_financial_metrics(yearly_data):
1372
  metrics_html = ""
 
1381
 
1382
  html = f"""
1383
  <div style="width: 450px;height: 300px !important;border: 1px solid #e0e0e0; border-radius: 8px; padding: 16px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); font-family: 'Segoe UI', sans-serif;">
1384
+ <div style="display: flex; align-items: center; gap: 8px; margin-bottom: 16px;justify-content: space-between;">
1385
+ <div style="font-size: 18px; font-weight: 600;display: flex;align-items: center;">
1386
+ <svg width="16" height="16" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
1387
+ <path d="M12 2L15.09 8.26L19 9.07L16 14L16 19L12 19L8 14L8 9.07L4.91 8.26L8 2L12 2Z" fill="#0066cc"/>
1388
+ </svg>
1389
+ <span style="margin-left: 10px;">{yearly_data} Financial Metrics</span>
1390
+ </div>
1391
+ <div style="font-size: 16px; color: #8f8f8f;">
1392
+ YTD data
1393
+ </div>
1394
  </div>
1395
  {metrics_html}
1396
  </div>
 
1451
 
1452
  # 返回三个HTML组件的内容
1453
  return build_stock_card(company_info), build_financial_metrics(yearly_data), build_income_table(table_data)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1454
 
1455
  def create_tab_content(tab_name, company_name):
1456
  """创建Tab内容组件"""
 
1683
  # 左侧边栏
1684
  with gr.Column(scale=1, min_width=350):
1685
  # 获取company_list组件的引用
1686
+ company_list_component, report_section_component, report_display_component, news_display_component = create_sidebar()
1687
 
1688
  # 主内容区域
1689
  with gr.Column(scale=9):
 
1807
  # create_tab_content("comparison")
1808
  with gr.Column(scale=2, min_width=400):
1809
  # 聊天面板
1810
+ # gr.ChatInterface(
1811
+ # respond,
1812
+ # title="Easy Financial Report",
1813
+ # # label="Easy Financial Report",
1814
+ # additional_inputs=[
1815
+ # # gr.Textbox(value="You are a financial analysis assistant. Provide concise investment insights from company financial reports.", label="System message"),
1816
+ # # gr.Slider(minimum=1, maximum=4096, value=1024, step=1, label="Max new tokens"),
1817
+ # # gr.Slider(minimum=0.1, maximum=4.0, value=0.7, step=0.1, label="Temperature"),
1818
+ # # gr.Slider(
1819
+ # # minimum=0.1,
1820
+ # # maximum=1.0,
1821
+ # # value=0.95,
1822
+ # # step=0.05,
1823
+ # # label="Top-p (nucleus sampling)",
1824
+ # # ),
1825
+ # gr.State(value="") # CRITICAL: Add State to store session URL across turns
1826
+ # ],
1827
+ # )
1828
+ # chatbot.render()
1829
+ # gr.LoginButton()
1830
+ chatbot = gr.ChatInterface(
1831
  respond,
1832
  title="Easy Financial Report",
 
1833
  additional_inputs=[
1834
+ gr.State(value=""), # CRITICAL: Store session URL across turns (hidden from UI)
1835
+ gr.State(value={}) # CRITICAL: Store agent context across turns (hidden from UI)
 
 
 
 
 
 
 
 
 
1836
  ],
1837
+ additional_inputs_accordion=gr.Accordion(label="Settings", open=False, visible=False), # Hide the accordion completely
1838
  )
1839
+
1840
+
1841
+ with gr.Blocks() as demo:
1842
+ # Add custom CSS for Agent Plan styling
1843
+ gr.Markdown("""
1844
+ <style>
1845
+ .agent-plan {
1846
+ background-color: #f8f9fa;
1847
+ border-left: 4px solid #6c757d;
1848
+ padding: 10px;
1849
+ margin: 10px 0;
1850
+ border-radius: 4px;
1851
+ font-family: monospace;
1852
+ color: #495057;
1853
+ }
1854
+ </style>
1855
+ """)
1856
+
1857
+ chatbot.render()
1858
 
1859
  # 在页面加载时自动刷新公司列表,确保显示最新的数据
1860
  # demo.load(
chatbot/chat_main.py CHANGED
@@ -44,10 +44,7 @@ def start_mcp_service(hf_token=None):
44
  env = os.environ.copy()
45
 
46
  # Pass HF token to subprocess if available
47
- if hf_token and hasattr(hf_token, 'token'):
48
- env['HUGGING_FACE_HUB_TOKEN'] = hf_token.token
49
- print(f"Passing HF token to MCP subprocess (length: {len(hf_token.token)})")
50
- elif hf_token and isinstance(hf_token, str):
51
  env['HUGGING_FACE_HUB_TOKEN'] = hf_token
52
  print(f"Passing HF token to MCP subprocess (length: {len(hf_token)})")
53
  else:
@@ -177,8 +174,21 @@ def initialize_mcp_session_stdio(mcp_process):
177
  raise e
178
 
179
 
 
 
 
 
 
 
 
 
 
 
 
 
180
  # Global variable to track MCP session initialization
181
  MCP_INITIALIZED = False
 
182
 
183
  def call_mcp_tool_stdio(mcp_process, tool_name, arguments):
184
  """
@@ -281,6 +291,159 @@ def call_mcp_tool_stdio(mcp_process, tool_name, arguments):
281
  raise e
282
 
283
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
284
  def extract_url_from_user_input(user_input, hf_token):
285
  """Extract URL from user input using regex pattern matching"""
286
  import re
@@ -297,9 +460,136 @@ def extract_url_from_user_input(user_input, hf_token):
297
  return None
298
 
299
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
300
  def get_available_mcp_tools(mcp_process):
301
  """
302
- Get information about all available MCP tools
303
  """
304
  try:
305
  # First ensure session is initialized
@@ -361,12 +651,23 @@ def get_available_mcp_tools(mcp_process):
361
  error_msg += f". This typically means the request parameters are invalid. Request: {json.dumps(request)}"
362
  raise Exception(error_msg)
363
 
364
- # Return tools information
 
365
  if "result" in response and "tools" in response["result"]:
366
- return response["result"]["tools"]
367
  else:
368
  raise Exception("MCP tools list failed: no tools in response")
369
-
 
 
 
 
 
 
 
 
 
 
370
  except json.JSONDecodeError as e:
371
  raise Exception(f"Failed to parse MCP tools list response as JSON: {str(e)}")
372
  except Exception as e:
@@ -374,14 +675,26 @@ def get_available_mcp_tools(mcp_process):
374
  raise e
375
 
376
 
377
- def decide_tool_execution_plan(user_message, tools_info, history, hf_token):
378
  """
379
  Let LLM decide which tools to use based on user request
 
 
 
 
 
 
 
380
  """
 
 
 
 
 
381
  try:
382
  client = InferenceClient(
383
  model="Qwen/Qwen2.5-72B-Instruct",
384
- token=hf_token.token if hf_token else None
385
  )
386
 
387
  # Format tools information for LLM
@@ -397,13 +710,51 @@ def decide_tool_execution_plan(user_message, tools_info, history, hf_token):
397
  for i, (user_msg, assistant_msg) in enumerate(history[-3:]): # Include last 3 exchanges
398
  history_context += f"User: {user_msg}\nAssistant: {assistant_msg}\n"
399
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
400
  # Create prompt for LLM to decide tool execution plan
401
  prompt = f"""
402
  You are a financial analysis assistant that can use various tools to help users.
 
 
 
403
  Available tools:
404
  {tools_description}
405
- {history_context}User request: {user_message}
406
- Based on the user's request and conversation history, decide which tools to use and in what order.
 
 
407
  Provide your response in the following JSON format:
408
  {{
409
  "plan": [
@@ -418,6 +769,7 @@ Provide your response in the following JSON format:
418
  ],
419
  "explanation": "brief explanation of your plan"
420
  }}
 
421
  Important guidelines:
422
  1. If the user mentions a company name but no direct URL, you should first try to extract a valid URL from their message
423
  2. If no valid URL is found, and the user is asking for analysis of a specific company's financial report, use the search_and_extract_financial_report tool
@@ -488,7 +840,17 @@ Important guidelines:
488
  - The system will automatically find the most recently downloaded file
489
  - Never use placeholder names like "Microsoft_FY25_Q1_Report.pdf" or "report.pdf"
490
  - Only provide a specific filename if the user explicitly mentioned it or it was returned by a previous tool
491
- 45. Example of correct usage when filename is unknown:
 
 
 
 
 
 
 
 
 
 
492
  {{
493
  "tool": "analyze_financial_report_file",
494
  "arguments": {{
@@ -496,11 +858,12 @@ Important guidelines:
496
  }},
497
  "reason": "Analyze the previously downloaded financial report"
498
  }}
 
499
  If no tools are needed, return an empty plan array.
500
  """
501
 
502
  messages = [
503
- {"role": "system", "content": "You are a precise JSON generator that helps decide which tools to use for financial analysis. You are also helpful in guiding users to provide valid URLs for financial reports. You should ONLY generate a plan if you are certain the tools can be executed successfully. Plan all necessary steps for a complete workflow: download analyze."},
504
  {"role": "user", "content": prompt}
505
  ]
506
 
@@ -518,6 +881,9 @@ If no tools are needed, return an empty plan array.
518
  else:
519
  content = str(response)
520
 
 
 
 
521
  # Try to parse as JSON
522
  try:
523
  # Extract JSON from the response if it's wrapped in other text
@@ -525,6 +891,30 @@ If no tools are needed, return an empty plan array.
525
  json_match = re.search(r'\{.*\}', content, re.DOTALL)
526
  if json_match:
527
  json_str = json_match.group(0)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
528
  result = json.loads(json_str)
529
  # Ensure plan is empty if no valid plan can be made
530
  if not result.get("plan"):
@@ -533,18 +923,23 @@ If no tools are needed, return an empty plan array.
533
  else:
534
  # If no JSON found, return empty plan
535
  return {"plan": [], "explanation": content if content else "No valid plan could be generated"}
536
- except json.JSONDecodeError:
537
  # If JSON parsing fails, return a default plan
538
  print(f"Failed to parse LLM response as JSON: {content}")
 
539
  return {"plan": [], "explanation": "Could not generate a tool execution plan"}
540
 
541
  except Exception as e:
542
- print(f"Error in decide_tool_execution_plan: {str(e)}")
543
- # Return a safe default plan
544
- return {"plan": [], "explanation": f"Error generating plan: {str(e)}"}
 
 
 
 
545
 
546
 
547
- def execute_tool_plan(mcp_process, tool_plan, output_messages, user_message):
548
  """
549
  Execute the tool plan generated by LLM
550
  """
@@ -635,6 +1030,31 @@ def execute_tool_plan(mcp_process, tool_plan, output_messages, user_message):
635
  output_messages.append(f"🔗 Including source URL for analysis context")
636
  yield "\n".join(output_messages)
637
  break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
638
 
639
  output_messages.append(f"🤖 Agent Decision: {reason}")
640
  yield "\n".join(output_messages)
@@ -710,6 +1130,10 @@ def execute_tool_plan(mcp_process, tool_plan, output_messages, user_message):
710
  actual_data = parsed_data[key]
711
  print(f"[DEBUG] Resolved reference from tool '{prev_tool}' field '{key}': {len(actual_data)} items")
712
  break
 
 
 
 
713
  if actual_data:
714
  break
715
  except json.JSONDecodeError:
@@ -726,6 +1150,43 @@ def execute_tool_plan(mcp_process, tool_plan, output_messages, user_message):
726
  actual_data = result_data[key]
727
  print(f"[DEBUG] Resolved reference from tool '{prev_tool}' structuredContent: {len(actual_data)} items")
728
  break
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
729
 
730
  if actual_data:
731
  break
@@ -739,11 +1200,28 @@ def execute_tool_plan(mcp_process, tool_plan, output_messages, user_message):
739
 
740
  # Call the tool
741
  try:
742
- tool_result = call_mcp_tool_stdio(mcp_process, tool_name, arguments)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
743
  results.append({
744
  "tool": tool_name,
745
  "arguments": arguments,
746
- "result": tool_result
 
747
  })
748
  successful_tools += 1 # Increment successful tools counter
749
 
@@ -876,7 +1354,8 @@ def execute_tool_plan(mcp_process, tool_plan, output_messages, user_message):
876
  "search_results": links,
877
  "user_request": user_message
878
  },
879
- "result": deep_analysis_result
 
880
  })
881
  successful_tools += 1
882
 
@@ -936,24 +1415,24 @@ def execute_tool_plan(mcp_process, tool_plan, output_messages, user_message):
936
  if suggestion:
937
  output_messages.append(f"📋 {suggestion}")
938
  yield "\n".join(output_messages)
939
-
940
- # Handle cases where deep analysis found no results
941
- elif actual_result.get("type") == "no_results":
942
- no_results_message = actual_result.get("message", "")
943
- suggestion = actual_result.get("suggestion", "")
944
- output_messages.append(f"⚠️ {no_results_message}")
945
- if suggestion:
946
- output_messages.append(f"📋 {suggestion}")
947
- yield "\n".join(output_messages)
948
-
949
- # Handle cases where deep analysis encountered errors
950
- elif actual_result.get("type") == "analysis_error":
951
- error_message = actual_result.get("message", "")
952
- suggestion = actual_result.get("suggestion", "")
953
- output_messages.append(f"❌ {error_message}")
954
- if suggestion:
955
- output_messages.append(f"📋 {suggestion}")
956
- yield "\n".join(output_messages)
957
 
958
  except Exception as e:
959
  error_msg = str(e)
@@ -970,6 +1449,15 @@ def execute_tool_plan(mcp_process, tool_plan, output_messages, user_message):
970
  output_messages.append(f"⚠️ Tool '{tool_name}' failed due to network issues. This may be due to network restrictions in the execution environment. Please try again later or use a direct PDF URL.")
971
  else:
972
  output_messages.append(f"❌ Error executing tool '{tool_name}': {error_msg}")
 
 
 
 
 
 
 
 
 
973
  yield "\n".join(output_messages)
974
  # Continue with other tools rather than failing completely
975
  continue
@@ -990,20 +1478,49 @@ def execute_tool_plan(mcp_process, tool_plan, output_messages, user_message):
990
  def respond(
991
  message,
992
  history: list[tuple[str, str]],
993
- system_message,
994
- max_tokens,
995
- temperature,
996
- top_p,
997
- hf_token: gr.OAuthToken,
998
- session_url: str = "" # CRITICAL: 添加会话URL状态参数
999
  ):
1000
  """
1001
  Main response function that integrates with MCP service
1002
  """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1003
  global MCP_INITIALIZED
1004
 
1005
  print(f"\n[SESSION] Starting new turn with session_url: {session_url}")
1006
 
 
 
 
 
 
 
 
 
1007
  # CRITICAL: Track the current session's source URL across multiple turns
1008
  current_session_url = session_url # Start with previous session URL
1009
 
@@ -1015,16 +1532,19 @@ def respond(
1015
  try:
1016
  client = InferenceClient(
1017
  model="Qwen/Qwen2.5-72B-Instruct",
1018
- token=hf_token.token if hf_token else None
1019
  )
1020
 
1021
  # Quick intent check
1022
  intent_check_prompt = f"""
1023
  Analyze the user's message and determine if they need financial analysis tools or just want to have a conversation.
 
1024
  User message: {message}
 
1025
  Respond with ONLY one word:
1026
  - "TOOLS" if the user is asking for financial report analysis, searching for financial reports, or downloading financial data
1027
  - "CONVERSATION" if the user is just greeting, asking general questions, or having a casual conversation
 
1028
  Response:"""
1029
 
1030
  intent_response = client.chat.completions.create(
@@ -1053,8 +1573,11 @@ Response:"""
1053
  conversation_prompt = f"""
1054
  You are an intelligent financial analysis assistant with expertise in investment research and financial analysis.
1055
  You can engage in natural conversation and provide insights based on your knowledge and the context provided.
 
1056
  {history_context}
 
1057
  Current user message: {message}
 
1058
  Guidelines for your response:
1059
  1. If the user is just greeting you or having casual conversation, respond warmly and naturally
1060
  2. If the user is asking about a specific financial report or company analysis, explain that you can help search for and analyze financial reports
@@ -1063,6 +1586,7 @@ Guidelines for your response:
1063
  5. Always be helpful, conversational, and friendly while maintaining your expertise
1064
  6. Keep responses focused and under 500 words
1065
  7. For casual greetings or simple questions, keep your response brief and natural
 
1066
  Please provide a helpful, conversational response:
1067
  """
1068
 
@@ -1092,7 +1616,7 @@ Please provide a helpful, conversational response:
1092
  # Yield partial results for streaming output
1093
  output_messages = [conversation_result]
1094
  yield "\n".join(output_messages)
1095
- return current_session_url # Return session URL for next turn
1096
 
1097
  except Exception as e:
1098
  print(f"[DEBUG] Error in intent check: {str(e)}")
@@ -1106,7 +1630,7 @@ Please provide a helpful, conversational response:
1106
  if not success:
1107
  output_messages.append("❌ Failed to start the financial report processing service. Please check the logs.")
1108
  yield "\n".join(output_messages)
1109
- return current_session_url # Return session URL even on failure
1110
 
1111
  try:
1112
  # Get available MCP tools
@@ -1119,19 +1643,42 @@ Please provide a helpful, conversational response:
1119
  output_messages.append("🤖 Analyzing your request and deciding which tools to use...")
1120
  yield "\n".join(output_messages)
1121
 
1122
- tool_plan = decide_tool_execution_plan(message, tools_info, history, hf_token)
1123
 
1124
  # CRITICAL: Check if plan generation failed due to API error
1125
  explanation = tool_plan.get("explanation", "No explanation provided")
1126
- if explanation.startswith("Error generating plan:"):
 
 
 
 
 
 
 
1127
  # Plan generation failed - show error and stop
1128
- output_messages.append(f"❌ Unable to process your request due to API instability: {explanation}")
1129
  output_messages.append("")
1130
- output_messages.append("💡 This is likely a temporary issue with the Hugging Face API. Please try:")
1131
- output_messages.append(" • Waiting a moment and trying again")
1132
- output_messages.append(" • Refreshing the page if the issue persists")
1133
  yield "\n".join(output_messages)
1134
- return current_session_url # Stop execution to prevent hallucination
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1135
 
1136
  output_messages.append(f'<div class="agent-plan">💡 Agent Plan: {explanation}</div>')
1137
  yield "\n".join(output_messages)
@@ -1142,7 +1689,7 @@ Please provide a helpful, conversational response:
1142
  successful_tools = 0
1143
  search_returned_no_results = False # 添加标志位
1144
 
1145
- for result in execute_tool_plan(mcp_process, tool_plan, output_messages, message):
1146
  if isinstance(result, list):
1147
  tool_results = result
1148
  # Extract successful_tools count from results
@@ -1159,21 +1706,121 @@ Please provide a helpful, conversational response:
1159
  if search_returned_no_results:
1160
  output_messages.append("💡 No relevant results found from the search, I will engage in natural conversation with you based on existing knowledge.")
1161
  yield "\n".join(output_messages)
1162
- return current_session_url # Return directly without executing subsequent analysis steps
1163
 
1164
  # Check if we have any successful tool results
1165
  if successful_tools == 0:
1166
  output_messages.append("⚠️ No tools were successfully executed. Unable to provide analysis based on tool results.")
1167
  output_messages.append("💡 Please provide a valid financial report URL (PDF format) for analysis.")
1168
  yield "\n".join(output_messages)
1169
- return current_session_url
1170
  else:
1171
- # No tool plan was generated - user is likely just having a general conversation
1172
- # Engage in natural conversation without tool execution
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1173
  try:
1174
  client = InferenceClient(
1175
  model="Qwen/Qwen2.5-72B-Instruct",
1176
- token=hf_token.token if hf_token else None
1177
  )
1178
 
1179
  # Format conversation history for context
@@ -1187,8 +1834,11 @@ Please provide a helpful, conversational response:
1187
  conversation_prompt = f"""
1188
  You are an intelligent financial analysis assistant with expertise in investment research and financial analysis.
1189
  You can engage in natural conversation and provide insights based on your knowledge and the context provided.
 
1190
  {history_context}
 
1191
  Current user message: {message}
 
1192
  Guidelines for your response:
1193
  1. If the user is just greeting you or having casual conversation, respond warmly and naturally
1194
  2. If the user is asking about a specific financial report or company analysis, explain that you can help search for and analyze financial reports
@@ -1197,6 +1847,7 @@ Guidelines for your response:
1197
  5. Always be helpful, conversational, and friendly while maintaining your expertise
1198
  6. Keep responses focused and under 500 words
1199
  7. For casual greetings or simple questions, keep your response brief and natural
 
1200
  Please provide a helpful, conversational response:
1201
  """
1202
 
@@ -1240,7 +1891,7 @@ Please provide a helpful, conversational response:
1240
  yield "\n".join(output_messages)
1241
 
1242
  # Return after conversation - no need to process tool results
1243
- return current_session_url
1244
 
1245
  # Filter out the successful_tools item from tool_results
1246
  filtered_tool_results = [result for result in tool_results if not (isinstance(result, dict) and "successful_tools" in result)] if 'tool_results' in locals() else []
@@ -1307,9 +1958,12 @@ Please provide a helpful, conversational response:
1307
 
1308
  analysis_prompt = f"""
1309
  You are a professional financial analyst. Analyze the following financial report and provide comprehensive investment insights.
 
1310
  Financial Report: {filename}{source_context}
 
1311
  Report Content:
1312
  {file_content}
 
1313
  Please provide a detailed analysis covering:
1314
  1. Revenue performance and trends
1315
  2. Profitability analysis (net income, margins, etc.)
@@ -1317,6 +1971,7 @@ Please provide a detailed analysis covering:
1317
  4. Cash flow analysis
1318
  5. Key financial ratios and metrics
1319
  6. Investment recommendations and risk assessment
 
1320
  Provide specific numbers and percentages from the report. Be detailed and data-driven.
1321
  """
1322
 
@@ -1324,7 +1979,7 @@ Provide specific numbers and percentages from the report. Be detailed and data-d
1324
  try:
1325
  client = InferenceClient(
1326
  model="Qwen/Qwen2.5-72B-Instruct",
1327
- token=hf_token.token if hf_token else None
1328
  )
1329
 
1330
  messages = [
@@ -1360,13 +2015,13 @@ Provide specific numbers and percentages from the report. Be detailed and data-d
1360
  yield "\n".join(output_messages)
1361
 
1362
  # Analysis complete
1363
- return current_session_url
1364
 
1365
  except Exception as e:
1366
  print(f"[ERROR] Failed to analyze file: {str(e)}")
1367
  output_messages.append(f"\n⚠️ Analysis failed: {str(e)}")
1368
  yield "\n".join(output_messages)
1369
- return current_session_url
1370
 
1371
  # Old format - just store file path
1372
  if 'file_path' in tool_result_data:
@@ -1516,7 +2171,7 @@ Provide specific numbers and percentages from the report. Be detailed and data-d
1516
  try:
1517
  client = InferenceClient(
1518
  model="Qwen/Qwen2.5-72B-Instruct",
1519
- token=hf_token.token if hf_token else None
1520
  )
1521
 
1522
  # Format the download links for the prompt
@@ -1529,9 +2184,12 @@ Provide specific numbers and percentages from the report. Be detailed and data-d
1529
  # Create prompt for final response
1530
  final_response_prompt = f"""
1531
  You are a helpful financial analysis assistant. Based on the user's request and the tool execution results, provide a clear, concise, and intelligent final response.
 
1532
  User's original request: {message}
 
1533
  Tool execution results - Download links found:
1534
  {links_summary}
 
1535
  IMPORTANT INSTRUCTIONS:
1536
  1. Analyze the user's request carefully to understand their true intent
1537
  2. You MUST use the EXACT URLs provided in the tool results above - DO NOT modify or invent URLs
@@ -1543,6 +2201,7 @@ IMPORTANT INSTRUCTIONS:
1543
  5. Use emoji appropriately (📄 for title, 🔗 for URL, 📋 for description)
1544
  6. Keep your response helpful and aligned with the user's actual intent
1545
  7. DO NOT make assumptions - let the user's question guide your response format
 
1546
  Provide an intelligent, contextual response:
1547
  """
1548
 
@@ -1595,7 +2254,7 @@ Provide an intelligent, contextual response:
1595
  output_messages.append("✅ You can click on the links above to download the financial reports directly.")
1596
  yield "\n".join(output_messages)
1597
 
1598
- return current_session_url # End processing here - user just wanted download links
1599
  # Fallback: Present tool results summary and generate intelligent final response
1600
  elif filtered_tool_results:
1601
  # Re-check if there are any download links in the results that we might have missed
@@ -1658,7 +2317,7 @@ Provide an intelligent, contextual response:
1658
  try:
1659
  client = InferenceClient(
1660
  model="Qwen/Qwen2.5-72B-Instruct",
1661
- token=hf_token.token if hf_token else None
1662
  )
1663
 
1664
  # Format the download links for the prompt
@@ -1671,9 +2330,12 @@ Provide an intelligent, contextual response:
1671
  # Create prompt for final response
1672
  final_response_prompt = f"""
1673
  You are a helpful financial analysis assistant. Based on the user's request and the tool execution results, provide a clear, concise, and intelligent final response.
 
1674
  User's original request: {message}
 
1675
  Tool execution results - Download links found:
1676
  {links_summary}
 
1677
  IMPORTANT INSTRUCTIONS:
1678
  1. Analyze the user's request carefully to understand their true intent
1679
  2. You MUST use the EXACT URLs provided in the tool results above - DO NOT modify or invent URLs
@@ -1690,6 +2352,7 @@ IMPORTANT INSTRUCTIONS:
1690
  | Column 1 | Column 2 |
1691
  |----------|----------|
1692
  | Data 1 | Data 2 |
 
1693
  Provide an intelligent, contextual response:
1694
  """
1695
 
@@ -1746,6 +2409,99 @@ Provide an intelligent, contextual response:
1746
  # If no download links found, collect tool execution summary
1747
  # First, collect tool execution summary including actual data
1748
  tool_summary = ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1749
  for result in filtered_tool_results:
1750
  if result is not None and 'tool' in result:
1751
  tool_name = result.get('tool', 'Unknown Tool')
@@ -1773,6 +2529,39 @@ Provide an intelligent, contextual response:
1773
  tool_result_data = result['result']
1774
 
1775
  if tool_result_data:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1776
  # Show summary information
1777
  if 'message' in tool_result_data:
1778
  tool_summary += f"Message: {tool_result_data['message']}\n"
@@ -1798,29 +2587,118 @@ Provide an intelligent, contextual response:
1798
  output_messages.append("\n✅ Tool execution completed successfully!")
1799
  yield "\n".join(output_messages)
1800
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1801
  # Generate intelligent final response based on tool results
1802
  try:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1803
  client = InferenceClient(
1804
  model="Qwen/Qwen2.5-72B-Instruct",
1805
- token=hf_token.token if hf_token else None
1806
  )
1807
 
1808
  # Create prompt for final response
1809
  final_response_prompt = f"""
1810
  You are a helpful financial analysis assistant. Based on the user's request and the tool execution results, provide a clear, concise, and intelligent final response.
 
1811
  User's original request: {message}
 
1812
  Tool execution results:
1813
  {tool_summary}
 
1814
  IMPORTANT INSTRUCTIONS:
1815
  1. Carefully analyze the user's request to understand their true intent
1816
  2. The tool execution results above contain the actual data - use them!
1817
- 3. If links/URLs are provided in the results, YOU MUST include them in your response using the EXACT URLs
1818
  4. DO NOT make up or invent any information that is not in the results
1819
- 5. DO NOT create fake URLs, links, or data - only use what's in the tool results
1820
- 6. If the user requested a specific format (e.g., table), provide it using markdown
1821
- 7. Present information clearly based on what the user actually asked for
1822
- 8. If results contain links, present them properly formatted with titles and URLs
1823
- 9. Keep your response helpful and aligned with the user's actual intent
 
 
 
1824
  Provide a clear, accurate final response based on the tool execution results above:
1825
  """
1826
 
@@ -1918,7 +2796,7 @@ Provide a clear, accurate final response based on the tool execution results abo
1918
  try:
1919
  client = InferenceClient(
1920
  model="Qwen/Qwen2.5-72B-Instruct",
1921
- token=hf_token.token if hf_token else None
1922
  )
1923
 
1924
  # Format conversation history for context
@@ -1932,8 +2810,11 @@ Provide a clear, accurate final response based on the tool execution results abo
1932
  conversation_prompt = f"""
1933
  You are an intelligent financial analysis assistant with expertise in investment research and financial analysis.
1934
  You can engage in natural conversation and provide insights based on your knowledge and the context provided.
 
1935
  {history_context}
 
1936
  Current user message: {message}
 
1937
  Guidelines for your response:
1938
  1. If the user is asking about a specific financial report or company analysis, explain that you can help but need a URL (or PDF format URL)
1939
  2. If the user is asking follow-up questions about investments or financial concepts, provide informed insights based on your expertise
@@ -1963,7 +2844,9 @@ Guidelines for your response:
1963
  26. When search results are unhelpful, acknowledge this and continue with normal conversation flow
1964
  # If search returned no results flag is set, directly engage in natural conversation without executing subsequent analysis
1965
  # Return directly without executing subsequent analysis steps
 
1966
  27. For general inquiries or conversational requests that don't require financial analysis tools, engage in natural conversation without initiating financial analysis workflows
 
1967
  Please provide a helpful, conversational response:
1968
  """
1969
 
@@ -2004,7 +2887,7 @@ Please provide a helpful, conversational response:
2004
  try:
2005
  client = InferenceClient(
2006
  model="Qwen/Qwen2.5-72B-Instruct",
2007
- token=hf_token.token if hf_token else None
2008
  )
2009
 
2010
  # Format conversation history for context
@@ -2017,8 +2900,11 @@ Please provide a helpful, conversational response:
2017
  # Create intelligent conversation prompt
2018
  conversation_prompt = f"""
2019
  You are an intelligent financial analysis assistant with expertise in investment research and financial analysis.
 
2020
  {history_context}
 
2021
  Current user message: {message}
 
2022
  Guidelines for your response:
2023
  1. Respond naturally and helpfully to the user's question
2024
  2. Use your financial expertise to provide valuable insights
@@ -2026,6 +2912,7 @@ Guidelines for your response:
2026
  4. For general financial questions, provide informed answers based on your knowledge
2027
  5. Keep responses concise and focused (under 500 words)
2028
  6. Be conversational and friendly while maintaining professional expertise
 
2029
  Please provide a helpful response:
2030
  """
2031
 
@@ -2079,7 +2966,7 @@ Please provide a helpful response:
2079
  except Exception as e:
2080
  output_messages.append(f"❌ Error: {str(e)}")
2081
  yield "\n".join(output_messages)
2082
- return current_session_url # Return session URL even on error
2083
 
2084
 
2085
  def validate_url(url):
@@ -2102,6 +2989,43 @@ def validate_url(url):
2102
  print(f"URL validation error for {url}: {str(e)}")
2103
  return False
2104
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2105
  # def create_financial_chatbot():
2106
  # """
2107
  # 返回一个可嵌入的 ChatInterface 组件
 
44
  env = os.environ.copy()
45
 
46
  # Pass HF token to subprocess if available
47
+ if hf_token and isinstance(hf_token, str):
 
 
 
48
  env['HUGGING_FACE_HUB_TOKEN'] = hf_token
49
  print(f"Passing HF token to MCP subprocess (length: {len(hf_token)})")
50
  else:
 
174
  raise e
175
 
176
 
177
+ # Configuration for third-party MCP service
178
+ THIRD_PARTY_MCP_URL = "https://jc321-easyreportsmcpserver.hf.space/message"
179
+ THIRD_PARTY_MCP_TOOLS = [
180
+ "search_company",
181
+ "get_company_info",
182
+ "get_company_filings",
183
+ "get_financial_data",
184
+ "extract_financial_metrics",
185
+ "get_latest_financial_data",
186
+ "advanced_search_company"
187
+ ]
188
+
189
  # Global variable to track MCP session initialization
190
  MCP_INITIALIZED = False
191
+ THIRD_PARTY_MCP_INITIALIZED = False
192
 
193
  def call_mcp_tool_stdio(mcp_process, tool_name, arguments):
194
  """
 
291
  raise e
292
 
293
 
294
+ def call_third_party_mcp_tool(tool_name, arguments):
295
+ """
296
+ Call a third-party MCP tool via HTTP with proper error handling
297
+ Note: Third-party MCP service doesn't require authentication
298
+ """
299
+ import httpx
300
+ import asyncio
301
+ import time # Add timing
302
+
303
+ global THIRD_PARTY_MCP_INITIALIZED
304
+ output_messages = []
305
+
306
+ start_time = time.time() # Start timing
307
+ print(f"[TIMING] Starting third-party MCP call for {tool_name}")
308
+
309
+ try:
310
+ # Create the request according to MCP specification
311
+ request = {
312
+ "jsonrpc": "2.0",
313
+ "id": 1,
314
+ "method": "tools/call",
315
+ "params": {
316
+ "name": tool_name,
317
+ "arguments": arguments
318
+ }
319
+ }
320
+
321
+ output_messages.append(f"Sending third-party MCP request: {request}")
322
+ print(f"[DEBUG] Sending third-party MCP request: {request}")
323
+
324
+ request_prep_time = time.time()
325
+ print(f"[TIMING] Request preparation took: {request_prep_time - start_time:.3f}s")
326
+
327
+ # Use httpx to call the third-party MCP service
328
+ async def make_request():
329
+ http_start = time.time()
330
+ async with httpx.AsyncClient(timeout=60.0) as client:
331
+ # Import the global variable
332
+ from __main__ import THIRD_PARTY_MCP_URL
333
+
334
+ print(f"[TIMING] Target URL: {THIRD_PARTY_MCP_URL}")
335
+ print(f"[TIMING] Request payload: {request}")
336
+ print(f"[TIMING] HTTP client created, making POST request...")
337
+ post_start = time.time()
338
+
339
+ # Third-party MCP service doesn't require authentication
340
+ response = await client.post(
341
+ THIRD_PARTY_MCP_URL,
342
+ json=request,
343
+ headers={
344
+ "Content-Type": "application/json"
345
+ }
346
+ )
347
+
348
+ post_end = time.time()
349
+ print(f"[TIMING] HTTP POST took: {post_end - post_start:.3f}s")
350
+ print(f"[TIMING] Response status: {response.status_code}")
351
+ print(f"[TIMING] Response size: {len(response.content)} bytes")
352
+
353
+ response.raise_for_status()
354
+ result = response.json()
355
+
356
+ http_end = time.time()
357
+ print(f"[TIMING] Total HTTP operation took: {http_end - http_start:.3f}s")
358
+
359
+ return result
360
+
361
+ # Run async function
362
+ async_start = time.time()
363
+ print(f"[TIMING] Starting async execution...")
364
+
365
+ try:
366
+ # Try to get the current event loop
367
+ try:
368
+ loop = asyncio.get_running_loop()
369
+ print(f"[TIMING] Detected running event loop, applying nest_asyncio...")
370
+ nest_start = time.time()
371
+
372
+ # If we're already in an async context, we need to use nest_asyncio or run in thread
373
+ import nest_asyncio
374
+ nest_asyncio.apply()
375
+
376
+ nest_end = time.time()
377
+ print(f"[TIMING] nest_asyncio.apply() took: {nest_end - nest_start:.3f}s")
378
+
379
+ exec_start = time.time()
380
+ response = loop.run_until_complete(make_request())
381
+ exec_end = time.time()
382
+ print(f"[TIMING] run_until_complete() took: {exec_end - exec_start:.3f}s")
383
+
384
+ except RuntimeError:
385
+ print(f"[TIMING] No running loop, using asyncio.run()...")
386
+ # No running loop, create a new one
387
+ response = asyncio.run(make_request())
388
+ except ImportError:
389
+ print(f"[TIMING] nest_asyncio not available, creating new loop...")
390
+ # nest_asyncio not available, fall back to creating new loop
391
+ try:
392
+ loop = asyncio.get_event_loop()
393
+ except RuntimeError:
394
+ loop = asyncio.new_event_loop()
395
+ asyncio.set_event_loop(loop)
396
+ response = loop.run_until_complete(make_request())
397
+
398
+ async_end = time.time()
399
+ print(f"[TIMING] Async execution completed in: {async_end - async_start:.3f}s")
400
+
401
+ print(f"[DEBUG] Third-party MCP response: {response}")
402
+
403
+ total_time = time.time() - start_time
404
+ print(f"[TIMING] ⏱️ TOTAL third-party MCP call for '{tool_name}' took: {total_time:.3f}s")
405
+
406
+ # Check if response is None
407
+ if response is None:
408
+ error_msg = "Third-party MCP tool call failed: received None response"
409
+ print(f"[DEBUG] {error_msg}")
410
+ raise Exception(error_msg)
411
+
412
+ # Check for error response (only if error field exists and is not None)
413
+ if isinstance(response, dict) and "error" in response and response["error"] is not None:
414
+ error_content = response["error"]
415
+ error_code = error_content.get('code', 'unknown') if isinstance(error_content, dict) else 'unknown'
416
+ error_message = error_content.get('message', 'Unknown error') if isinstance(error_content, dict) else str(error_content)
417
+ error_msg = f"Third-party MCP Error {error_code}: {error_message}"
418
+ print(f"[DEBUG] {error_msg}")
419
+ raise Exception(error_msg)
420
+
421
+ # Return the result
422
+ if isinstance(response, dict) and "result" in response:
423
+ result = response["result"]
424
+ print(f"[DEBUG] Third-party MCP tool result: {result}")
425
+ return result
426
+ else:
427
+ error_msg = "Third-party MCP tool call failed: no result in response"
428
+ print(f"[DEBUG] {error_msg}")
429
+ raise Exception(error_msg)
430
+
431
+ except json.JSONDecodeError as e:
432
+ error_msg = f"Failed to parse third-party MCP response as JSON: {str(e)}"
433
+ print(f"[DEBUG] JSON decode error: {error_msg}")
434
+ raise Exception(error_msg)
435
+ except Exception as e:
436
+ # Ensure we have a meaningful error message
437
+ error_str = str(e) if str(e) else "Unknown error occurred"
438
+ output_messages.append(f"Error calling third-party MCP tool {tool_name}: {error_str}")
439
+ print(f"[DEBUG] Exception in call_third_party_mcp_tool: {error_str}")
440
+ print(f"[DEBUG] Exception type: {type(e)}")
441
+ # Print full traceback for debugging
442
+ import traceback
443
+ print(f"[DEBUG] Full traceback: {traceback.format_exc()}")
444
+ raise Exception(error_str)
445
+
446
+
447
  def extract_url_from_user_input(user_input, hf_token):
448
  """Extract URL from user input using regex pattern matching"""
449
  import re
 
460
  return None
461
 
462
 
463
+ def get_third_party_mcp_tools():
464
+ """
465
+ Get information about third-party MCP tools
466
+ """
467
+ # Define the tools with their descriptions and schemas
468
+ tools = [
469
+ {
470
+ "name": "search_company",
471
+ "description": "Search for a company by name in SEC EDGAR database. Returns company CIK, name, and ticker symbol.",
472
+ "inputSchema": {
473
+ "type": "object",
474
+ "properties": {
475
+ "company_name": {
476
+ "type": "string",
477
+ "description": "Company name to search (e.g., 'Microsoft', 'Apple', 'Tesla')"
478
+ }
479
+ },
480
+ "required": ["company_name"]
481
+ }
482
+ },
483
+ {
484
+ "name": "get_company_info",
485
+ "description": "Get detailed company information including name, tickers, SIC code, and industry description.",
486
+ "inputSchema": {
487
+ "type": "object",
488
+ "properties": {
489
+ "cik": {
490
+ "type": "string",
491
+ "description": "Company CIK code (10-digit format, e.g., '0000789019')"
492
+ }
493
+ },
494
+ "required": ["cik"]
495
+ }
496
+ },
497
+ {
498
+ "name": "get_company_filings",
499
+ "description": "Get list of company SEC filings (10-K, 10-Q, 20-F, etc.) with filing dates and document links.",
500
+ "inputSchema": {
501
+ "type": "object",
502
+ "properties": {
503
+ "cik": {
504
+ "type": "string",
505
+ "description": "Company CIK code"
506
+ },
507
+ "form_types": {
508
+ "type": "array",
509
+ "items": {
510
+ "type": "string"
511
+ },
512
+ "description": "Optional: Filter by form types (e.g., ['10-K', '10-Q'])"
513
+ }
514
+ },
515
+ "required": ["cik"]
516
+ }
517
+ },
518
+ {
519
+ "name": "get_financial_data",
520
+ "description": "Get financial data for a specific period including revenue, net income, EPS, operating expenses, and cash flow.",
521
+ "inputSchema": {
522
+ "type": "object",
523
+ "properties": {
524
+ "cik": {
525
+ "type": "string",
526
+ "description": "Company CIK code"
527
+ },
528
+ "period": {
529
+ "type": "string",
530
+ "description": "Period in format 'YYYY' for annual or 'YYYYQX' for quarterly (e.g., '2024', '2024Q3')"
531
+ }
532
+ },
533
+ "required": ["cik", "period"]
534
+ }
535
+ },
536
+ {
537
+ "name": "extract_financial_metrics",
538
+ "description": "Extract comprehensive financial metrics for multiple years including both annual and quarterly data. Returns data in chronological order (newest first).",
539
+ "inputSchema": {
540
+ "type": "object",
541
+ "properties": {
542
+ "cik": {
543
+ "type": "string",
544
+ "description": "Company CIK code"
545
+ },
546
+ "years": {
547
+ "type": "integer",
548
+ "description": "Number of recent years to extract (1-10, default: 3)",
549
+ "minimum": 1,
550
+ "maximum": 10,
551
+ "default": 3
552
+ }
553
+ },
554
+ "required": ["cik"]
555
+ }
556
+ },
557
+ {
558
+ "name": "get_latest_financial_data",
559
+ "description": "Get the most recent financial data available for a company.",
560
+ "inputSchema": {
561
+ "type": "object",
562
+ "properties": {
563
+ "cik": {
564
+ "type": "string",
565
+ "description": "Company CIK code"
566
+ }
567
+ },
568
+ "required": ["cik"]
569
+ }
570
+ },
571
+ {
572
+ "name": "advanced_search_company",
573
+ "description": "Advanced search supporting both company name and CIK code. Automatically detects input type.",
574
+ "inputSchema": {
575
+ "type": "object",
576
+ "properties": {
577
+ "company_input": {
578
+ "type": "string",
579
+ "description": "Company name, ticker, or CIK code"
580
+ }
581
+ },
582
+ "required": ["company_input"]
583
+ }
584
+ }
585
+ ]
586
+
587
+ return tools
588
+
589
+
590
  def get_available_mcp_tools(mcp_process):
591
  """
592
+ Get information about all available MCP tools including third-party tools
593
  """
594
  try:
595
  # First ensure session is initialized
 
651
  error_msg += f". This typically means the request parameters are invalid. Request: {json.dumps(request)}"
652
  raise Exception(error_msg)
653
 
654
+ # Get local MCP tools
655
+ local_tools = []
656
  if "result" in response and "tools" in response["result"]:
657
+ local_tools = response["result"]["tools"]
658
  else:
659
  raise Exception("MCP tools list failed: no tools in response")
660
+
661
+ # Get third-party MCP tools
662
+ third_party_tools = get_third_party_mcp_tools()
663
+
664
+ # Combine both tool lists
665
+ all_tools = local_tools + third_party_tools
666
+
667
+ print(f"Combined tools: {len(local_tools)} local + {len(third_party_tools)} third-party = {len(all_tools)} total")
668
+
669
+ return all_tools
670
+
671
  except json.JSONDecodeError as e:
672
  raise Exception(f"Failed to parse MCP tools list response as JSON: {str(e)}")
673
  except Exception as e:
 
675
  raise e
676
 
677
 
678
+ def decide_tool_execution_plan(user_message, tools_info, history, hf_token, agent_context=None):
679
  """
680
  Let LLM decide which tools to use based on user request
681
+
682
+ Args:
683
+ user_message: User's request
684
+ tools_info: List of available tools
685
+ history: Conversation history
686
+ hf_token: HuggingFace token
687
+ agent_context: Agent context with previously stored information (optional)
688
  """
689
+ # DEBUG: Print the actual user message
690
+ print(f"[DEBUG] User message received in decide_tool_execution_plan: '{user_message}'")
691
+ print(f"[DEBUG] User message type: {type(user_message)}")
692
+ print(f"[DEBUG] User message length: {len(user_message) if user_message else 0}")
693
+
694
  try:
695
  client = InferenceClient(
696
  model="Qwen/Qwen2.5-72B-Instruct",
697
+ token=hf_token if hf_token else None
698
  )
699
 
700
  # Format tools information for LLM
 
710
  for i, (user_msg, assistant_msg) in enumerate(history[-3:]): # Include last 3 exchanges
711
  history_context += f"User: {user_msg}\nAssistant: {assistant_msg}\n"
712
 
713
+ # Format agent context for LLM
714
+ context_info = ""
715
+ if agent_context and len(agent_context) > 0:
716
+ context_info = "\n\nAgent Context (previously gathered information that can be reused):\n"
717
+ if 'last_company_name' in agent_context:
718
+ context_info += f"- Last company: {agent_context['last_company_name']}"
719
+ if 'last_company_ticker' in agent_context:
720
+ context_info += f" ({agent_context['last_company_ticker']})"
721
+ context_info += "\n"
722
+ if 'last_company_cik' in agent_context:
723
+ context_info += f"- Company CIK: {agent_context['last_company_cik']}\n"
724
+ if 'last_period' in agent_context:
725
+ context_info += f"- Last period: {agent_context['last_period']}\n"
726
+ if 'last_financial_report_url' in agent_context:
727
+ context_info += f"- Last report URL: {agent_context['last_financial_report_url']}\n"
728
+
729
+ # Include financial data summary if available
730
+ if 'last_financial_data' in agent_context:
731
+ data = agent_context['last_financial_data']
732
+ context_info += "- Last financial data available:\n"
733
+ if 'total_revenue' in data:
734
+ context_info += f" Revenue: ${data['total_revenue']:,}\n"
735
+ if 'net_income' in data:
736
+ context_info += f" Net Income: ${data['net_income']:,}\n"
737
+ if 'earnings_per_share' in data:
738
+ context_info += f" EPS: ${data['earnings_per_share']}\n"
739
+
740
+ context_info += "\n**CRITICAL CONTEXT USAGE RULES:**\n"
741
+ context_info += "1. If the user is asking follow-up questions about the SAME company, you can skip search_company and directly use the CIK from context.\n"
742
+ context_info += "2. **If the user asks to 'analyze this report' or 'analyze the financial report' and last_financial_data is available, return an EMPTY tool plan [] - the system will use the context data directly for analysis.**\n"
743
+ context_info += "3. **DO NOT call analyze_financial_report_file for follow-up analysis requests when financial data is already in context.**\n"
744
+ context_info += "4. Only call new tools if the user is asking for DIFFERENT data (different company, different period, etc.).\n"
745
+
746
  # Create prompt for LLM to decide tool execution plan
747
  prompt = f"""
748
  You are a financial analysis assistant that can use various tools to help users.
749
+
750
+ **USER'S ACTUAL REQUEST: {user_message}**
751
+
752
  Available tools:
753
  {tools_description}
754
+ {context_info}
755
+ {history_context}
756
+
757
+ Based on the user's request above, decide which tools to use and in what order.
758
  Provide your response in the following JSON format:
759
  {{
760
  "plan": [
 
769
  ],
770
  "explanation": "brief explanation of your plan"
771
  }}
772
+
773
  Important guidelines:
774
  1. If the user mentions a company name but no direct URL, you should first try to extract a valid URL from their message
775
  2. If no valid URL is found, and the user is asking for analysis of a specific company's financial report, use the search_and_extract_financial_report tool
 
840
  - The system will automatically find the most recently downloaded file
841
  - Never use placeholder names like "Microsoft_FY25_Q1_Report.pdf" or "report.pdf"
842
  - Only provide a specific filename if the user explicitly mentioned it or it was returned by a previous tool
843
+ 45. **CRITICAL**: For financial data queries, be selective with tool usage:
844
+ - Prioritize using third-party MCP tools (SEC EDGAR data) as they provide authoritative source data
845
+ - For a simple query like "[Company] 2025 Q1 financial report", you typically need:
846
+ 1. search_company (to get the company's CIK code)
847
+ 2. get_financial_data (to get specific period financial data)
848
+ - AVOID using get_company_filings unless the user specifically asks for filing lists
849
+ - AVOID using extract_financial_metrics unless the user asks for multi-year analysis
850
+ - Use the minimum number of tools necessary to answer the user's question
851
+ - Each tool has overhead - be efficient and focused
852
+ - **CRITICAL: Always extract the company name from the USER'S ACTUAL REQUEST - never use examples or made-up company names!**
853
+ 46. Example of correct usage when filename is unknown:
854
  {{
855
  "tool": "analyze_financial_report_file",
856
  "arguments": {{
 
858
  }},
859
  "reason": "Analyze the previously downloaded financial report"
860
  }}
861
+
862
  If no tools are needed, return an empty plan array.
863
  """
864
 
865
  messages = [
866
+ {"role": "system", "content": "You are a precise JSON generator that helps decide which tools to use for financial analysis. Always plan the minimum necessary tools to answer the user's question efficiently. For financial data queries, typically use search_company + get_financial_data. Avoid extra tools unless explicitly needed. CRITICAL: Always extract company names and other parameters from the USER'S ACTUAL REQUEST - never use example data or make up information. IMPORTANT: Output ONLY valid JSON without any comments or explanations."},
867
  {"role": "user", "content": prompt}
868
  ]
869
 
 
881
  else:
882
  content = str(response)
883
 
884
+ # Debug: Log the raw LLM response
885
+ print(f"[DEBUG] Raw LLM response for tool planning: {content}")
886
+
887
  # Try to parse as JSON
888
  try:
889
  # Extract JSON from the response if it's wrapped in other text
 
891
  json_match = re.search(r'\{.*\}', content, re.DOTALL)
892
  if json_match:
893
  json_str = json_match.group(0)
894
+
895
+ # Remove JavaScript-style comments (// ...) from JSON
896
+ # This is a common issue where LLMs add explanatory comments in JSON
897
+ lines = json_str.split('\n')
898
+ cleaned_lines = []
899
+ for line in lines:
900
+ # Remove // comments but keep the rest of the line
901
+ if '//' in line:
902
+ # Find the position of //
903
+ comment_pos = line.find('//')
904
+ # Check if // is inside a string
905
+ before_comment = line[:comment_pos]
906
+ # Count quotes before comment to see if we're inside a string
907
+ quote_count = before_comment.count('"') - before_comment.count('\\"')
908
+ if quote_count % 2 == 0:
909
+ # Even number of quotes = we're outside a string, safe to remove comment
910
+ line = line[:comment_pos].rstrip()
911
+ # Also remove trailing comma if present
912
+ if line.rstrip().endswith(','):
913
+ line = line.rstrip()[:-1].rstrip()
914
+ cleaned_lines.append(line)
915
+
916
+ json_str = '\n'.join(cleaned_lines)
917
+
918
  result = json.loads(json_str)
919
  # Ensure plan is empty if no valid plan can be made
920
  if not result.get("plan"):
 
923
  else:
924
  # If no JSON found, return empty plan
925
  return {"plan": [], "explanation": content if content else "No valid plan could be generated"}
926
+ except json.JSONDecodeError as e:
927
  # If JSON parsing fails, return a default plan
928
  print(f"Failed to parse LLM response as JSON: {content}")
929
+ print(f"JSON decode error: {str(e)}")
930
  return {"plan": [], "explanation": "Could not generate a tool execution plan"}
931
 
932
  except Exception as e:
933
+ error_msg = str(e)
934
+ print(f"Error in decide_tool_execution_plan: {error_msg}")
935
+ # Check if it's a 5xx server error (retryable)
936
+ if "500" in error_msg or "502" in error_msg or "503" in error_msg or "504" in error_msg:
937
+ return {"plan": [], "explanation": "API temporarily unavailable. Please try again in a moment."}
938
+ else:
939
+ return {"plan": [], "explanation": f"Error generating plan: {error_msg}"}
940
 
941
 
942
+ def execute_tool_plan(mcp_process, tool_plan, output_messages, user_message, hf_token=None):
943
  """
944
  Execute the tool plan generated by LLM
945
  """
 
1030
  output_messages.append(f"🔗 Including source URL for analysis context")
1031
  yield "\n".join(output_messages)
1032
  break
1033
+
1034
+ # CRITICAL: Auto-fill CIK parameter for get_financial_data from search_company result
1035
+ if tool_name == "get_financial_data" and "cik" in arguments:
1036
+ # Check if there was a recent search_company call
1037
+ for prev_result in reversed(results):
1038
+ if prev_result.get("tool") == "search_company":
1039
+ tool_result = prev_result.get("result")
1040
+ if tool_result:
1041
+ # Try to extract CIK from MCP content format
1042
+ try:
1043
+ if 'content' in tool_result and isinstance(tool_result['content'], list):
1044
+ for content_item in tool_result['content']:
1045
+ if isinstance(content_item, dict) and 'text' in content_item:
1046
+ parsed_data = json.loads(content_item['text'])
1047
+ if isinstance(parsed_data, dict) and 'cik' in parsed_data:
1048
+ correct_cik = parsed_data['cik']
1049
+ if arguments['cik'] != correct_cik:
1050
+ output_messages.append(f"🔧 Correcting CIK: {arguments['cik']} → {correct_cik}")
1051
+ arguments = arguments.copy()
1052
+ arguments['cik'] = correct_cik
1053
+ yield "\n".join(output_messages)
1054
+ break
1055
+ except (json.JSONDecodeError, KeyError) as e:
1056
+ print(f"[DEBUG] Could not extract CIK from search_company result: {e}")
1057
+ break
1058
 
1059
  output_messages.append(f"🤖 Agent Decision: {reason}")
1060
  yield "\n".join(output_messages)
 
1130
  actual_data = parsed_data[key]
1131
  print(f"[DEBUG] Resolved reference from tool '{prev_tool}' field '{key}': {len(actual_data)} items")
1132
  break
1133
+ # If we're looking for a simple value like CIK, check for direct fields
1134
+ if actual_data is None and arg_name in parsed_data:
1135
+ actual_data = parsed_data[arg_name]
1136
+ print(f"[DEBUG] Resolved simple value reference from tool '{prev_tool}' field '{arg_name}': {actual_data}")
1137
  if actual_data:
1138
  break
1139
  except json.JSONDecodeError:
 
1150
  actual_data = result_data[key]
1151
  print(f"[DEBUG] Resolved reference from tool '{prev_tool}' structuredContent: {len(actual_data)} items")
1152
  break
1153
+ # If we're looking for a simple value like CIK, check for direct fields
1154
+ if actual_data is None and arg_name in result_data:
1155
+ actual_data = result_data[arg_name]
1156
+ print(f"[DEBUG] Resolved simple value reference from tool '{prev_tool}' structuredContent field '{arg_name}': {actual_data}")
1157
+
1158
+ # Handle direct result format (for simple values like CIK)
1159
+ if not actual_data and isinstance(tool_result, dict):
1160
+ # Look for the argument name directly in the result
1161
+ if arg_name in tool_result:
1162
+ actual_data = tool_result[arg_name]
1163
+ print(f"[DEBUG] Resolved simple value reference from tool '{prev_tool}' direct field '{arg_name}': {actual_data}")
1164
+ # Also check common fields that might contain the data
1165
+ elif 'result' in tool_result and isinstance(tool_result['result'], dict):
1166
+ if arg_name in tool_result['result']:
1167
+ actual_data = tool_result['result'][arg_name]
1168
+ print(f"[DEBUG] Resolved simple value reference from tool '{prev_tool}' result field '{arg_name}': {actual_data}")
1169
+ elif 'content' in tool_result and isinstance(tool_result['content'], list) and len(tool_result['content']) > 0:
1170
+ # Check if content contains a simple text result
1171
+ content_item = tool_result['content'][0]
1172
+ if isinstance(content_item, dict) and 'text' in content_item:
1173
+ try:
1174
+ # Try to parse as JSON first
1175
+ parsed_text = json.loads(content_item['text'])
1176
+ if isinstance(parsed_text, dict) and arg_name in parsed_text:
1177
+ actual_data = parsed_text[arg_name]
1178
+ print(f"[DEBUG] Resolved simple value reference from tool '{prev_tool}' content JSON field '{arg_name}': {actual_data}")
1179
+ except json.JSONDecodeError:
1180
+ # If not JSON, check if it's the actual value we're looking for
1181
+ if arg_name == 'cik' and content_item['text'].startswith('{'):
1182
+ # It might be JSON in string format
1183
+ try:
1184
+ parsed_json = json.loads(content_item['text'])
1185
+ if isinstance(parsed_json, dict) and 'cik' in parsed_json:
1186
+ actual_data = parsed_json['cik']
1187
+ print(f"[DEBUG] Resolved CIK from JSON content: {actual_data}")
1188
+ except:
1189
+ pass
1190
 
1191
  if actual_data:
1192
  break
 
1200
 
1201
  # Call the tool
1202
  try:
1203
+ # CRITICAL: Check if this is a third-party MCP tool
1204
+ is_third_party_tool = tool_name in THIRD_PARTY_MCP_TOOLS
1205
+
1206
+ if is_third_party_tool:
1207
+ # Add progress message for potentially slow operations
1208
+ if tool_name == "get_financial_data":
1209
+ output_messages.append("🔍 Fetching detailed financial data from SEC EDGAR... (this may take 30-60 seconds)")
1210
+ yield "\n".join(output_messages)
1211
+ elif tool_name in ["get_company_filings", "extract_financial_metrics"]:
1212
+ output_messages.append(f"🔍 Retrieving data from SEC database... (this may take a moment)")
1213
+ yield "\n".join(output_messages)
1214
+
1215
+ # Call third-party MCP tool via HTTP
1216
+ tool_result = call_third_party_mcp_tool(tool_name, arguments)
1217
+ else:
1218
+ # Call local MCP tool via stdio
1219
+ tool_result = call_mcp_tool_stdio(mcp_process, tool_name, arguments)
1220
  results.append({
1221
  "tool": tool_name,
1222
  "arguments": arguments,
1223
+ "result": tool_result,
1224
+ "success": True # Mark as successful
1225
  })
1226
  successful_tools += 1 # Increment successful tools counter
1227
 
 
1354
  "search_results": links,
1355
  "user_request": user_message
1356
  },
1357
+ "result": deep_analysis_result,
1358
+ "success": True # Mark as successful
1359
  })
1360
  successful_tools += 1
1361
 
 
1415
  if suggestion:
1416
  output_messages.append(f"📋 {suggestion}")
1417
  yield "\n".join(output_messages)
1418
+
1419
+ # Handle cases where deep analysis found no results
1420
+ elif actual_result.get("type") == "no_results":
1421
+ no_results_message = actual_result.get("message", "")
1422
+ suggestion = actual_result.get("suggestion", "")
1423
+ output_messages.append(f"⚠️ {no_results_message}")
1424
+ if suggestion:
1425
+ output_messages.append(f"📋 {suggestion}")
1426
+ yield "\n".join(output_messages)
1427
+
1428
+ # Handle cases where deep analysis encountered errors
1429
+ elif actual_result.get("type") == "analysis_error":
1430
+ error_message = actual_result.get("message", "")
1431
+ suggestion = actual_result.get("suggestion", "")
1432
+ output_messages.append(f"❌ {error_message}")
1433
+ if suggestion:
1434
+ output_messages.append(f"📋 {suggestion}")
1435
+ yield "\n".join(output_messages)
1436
 
1437
  except Exception as e:
1438
  error_msg = str(e)
 
1449
  output_messages.append(f"⚠️ Tool '{tool_name}' failed due to network issues. This may be due to network restrictions in the execution environment. Please try again later or use a direct PDF URL.")
1450
  else:
1451
  output_messages.append(f"❌ Error executing tool '{tool_name}': {error_msg}")
1452
+
1453
+ # Add the failed tool to results with success=False
1454
+ results.append({
1455
+ "tool": tool_name,
1456
+ "arguments": arguments,
1457
+ "result": {"error": error_msg},
1458
+ "success": False # Mark as failed
1459
+ })
1460
+
1461
  yield "\n".join(output_messages)
1462
  # Continue with other tools rather than failing completely
1463
  continue
 
1478
  def respond(
1479
  message,
1480
  history: list[tuple[str, str]],
1481
+ session_url: str = "", # CRITICAL: 会话URL状态参数
1482
+ agent_context: dict = None, # CRITICAL: Agent上下文(从上一轮传入)
 
 
 
 
1483
  ):
1484
  """
1485
  Main response function that integrates with MCP service
1486
  """
1487
+
1488
+ # Use default values for removed UI parameters
1489
+ system_message = "You are a financial analysis assistant. Provide concise investment insights from company financial reports."
1490
+ max_tokens = 1024
1491
+ temperature = 0.7
1492
+ top_p = 0.95
1493
+
1494
+ # Initialize agent_context if None
1495
+ if agent_context is None:
1496
+ agent_context = {}
1497
+ else:
1498
+ # Make a copy to avoid modifying the input dict
1499
+ agent_context = dict(agent_context)
1500
+
1501
+ # Get HF token from environment variables
1502
+ hf_token = os.environ.get("HF_TOKEN") or os.environ.get("HUGGING_FACE_HUB_TOKEN")
1503
+
1504
+ # DEBUG: Check token availability
1505
+ if hf_token:
1506
+ print(f"[AUTH] HF token found: {len(hf_token)} characters")
1507
+ else:
1508
+ print(f"[AUTH] ⚠️ WARNING: No HF token found in environment variables!")
1509
+ print(f"[AUTH] Checked: HF_TOKEN and HUGGING_FACE_HUB_TOKEN")
1510
+
1511
+
1512
  global MCP_INITIALIZED
1513
 
1514
  print(f"\n[SESSION] Starting new turn with session_url: {session_url}")
1515
 
1516
+ # CRITICAL: Initialize agent context if not provided
1517
+ if agent_context is None:
1518
+ agent_context = {}
1519
+
1520
+ # Log existing context
1521
+ if agent_context:
1522
+ print(f"[CONTEXT] Existing agent context: {list(agent_context.keys())}")
1523
+
1524
  # CRITICAL: Track the current session's source URL across multiple turns
1525
  current_session_url = session_url # Start with previous session URL
1526
 
 
1532
  try:
1533
  client = InferenceClient(
1534
  model="Qwen/Qwen2.5-72B-Instruct",
1535
+ token=hf_token if hf_token else None
1536
  )
1537
 
1538
  # Quick intent check
1539
  intent_check_prompt = f"""
1540
  Analyze the user's message and determine if they need financial analysis tools or just want to have a conversation.
1541
+
1542
  User message: {message}
1543
+
1544
  Respond with ONLY one word:
1545
  - "TOOLS" if the user is asking for financial report analysis, searching for financial reports, or downloading financial data
1546
  - "CONVERSATION" if the user is just greeting, asking general questions, or having a casual conversation
1547
+
1548
  Response:"""
1549
 
1550
  intent_response = client.chat.completions.create(
 
1573
  conversation_prompt = f"""
1574
  You are an intelligent financial analysis assistant with expertise in investment research and financial analysis.
1575
  You can engage in natural conversation and provide insights based on your knowledge and the context provided.
1576
+
1577
  {history_context}
1578
+
1579
  Current user message: {message}
1580
+
1581
  Guidelines for your response:
1582
  1. If the user is just greeting you or having casual conversation, respond warmly and naturally
1583
  2. If the user is asking about a specific financial report or company analysis, explain that you can help search for and analyze financial reports
 
1586
  5. Always be helpful, conversational, and friendly while maintaining your expertise
1587
  6. Keep responses focused and under 500 words
1588
  7. For casual greetings or simple questions, keep your response brief and natural
1589
+
1590
  Please provide a helpful, conversational response:
1591
  """
1592
 
 
1616
  # Yield partial results for streaming output
1617
  output_messages = [conversation_result]
1618
  yield "\n".join(output_messages)
1619
+ return current_session_url, agent_context # Return session URL and context for next turn
1620
 
1621
  except Exception as e:
1622
  print(f"[DEBUG] Error in intent check: {str(e)}")
 
1630
  if not success:
1631
  output_messages.append("❌ Failed to start the financial report processing service. Please check the logs.")
1632
  yield "\n".join(output_messages)
1633
+ return current_session_url, agent_context # Return session URL and context even on failure
1634
 
1635
  try:
1636
  # Get available MCP tools
 
1643
  output_messages.append("🤖 Analyzing your request and deciding which tools to use...")
1644
  yield "\n".join(output_messages)
1645
 
1646
+ tool_plan = decide_tool_execution_plan(message, tools_info, history, hf_token, agent_context)
1647
 
1648
  # CRITICAL: Check if plan generation failed due to API error
1649
  explanation = tool_plan.get("explanation", "No explanation provided")
1650
+ plan_list = tool_plan.get("plan", [])
1651
+
1652
+ # Debug logging
1653
+ print(f"[DEBUG] Tool plan explanation: {explanation}")
1654
+ print(f"[DEBUG] Tool plan list: {plan_list}")
1655
+
1656
+ # Check for API errors
1657
+ if "Error generating plan:" in explanation or "API temporarily unavailable" in explanation:
1658
  # Plan generation failed - show error and stop
1659
+ output_messages.append(f"❌ Unable to process your request: {explanation}")
1660
  output_messages.append("")
1661
+ output_messages.append("💡 Please try again in a moment. This is likely a temporary API issue.")
 
 
1662
  yield "\n".join(output_messages)
1663
+ return current_session_url, agent_context # Stop execution to prevent hallucination
1664
+
1665
+ # Check if plan is empty
1666
+ if not plan_list:
1667
+ print(f"[DEBUG] Empty tool plan received for message: {message}")
1668
+ # CRITICAL: If explanation is empty AND plan is empty, this is likely an LLM failure
1669
+ # Check if the original message looks like it was asking for tool usage
1670
+ # by looking at whether the message would have triggered tool discovery
1671
+ if explanation == "No explanation provided" or len(explanation.strip()) < 10:
1672
+ # LLM failed to provide any meaningful response - this is a technical error
1673
+ output_messages.append("❌ Oops! I encountered a technical issue while processing your request.")
1674
+ output_messages.append("")
1675
+ output_messages.append("💡 This could be due to:")
1676
+ output_messages.append(" • Temporary API service issues")
1677
+ output_messages.append(" • High system load")
1678
+ output_messages.append("")
1679
+ output_messages.append("🔄 Please try again in a moment. If the issue persists, feel free to reach out for support.")
1680
+ yield "\n".join(output_messages)
1681
+ return current_session_url, agent_context # CRITICAL: Stop here, don't enter conversation mode
1682
 
1683
  output_messages.append(f'<div class="agent-plan">💡 Agent Plan: {explanation}</div>')
1684
  yield "\n".join(output_messages)
 
1689
  successful_tools = 0
1690
  search_returned_no_results = False # 添加标志位
1691
 
1692
+ for result in execute_tool_plan(mcp_process, tool_plan, output_messages, message, hf_token):
1693
  if isinstance(result, list):
1694
  tool_results = result
1695
  # Extract successful_tools count from results
 
1706
  if search_returned_no_results:
1707
  output_messages.append("💡 No relevant results found from the search, I will engage in natural conversation with you based on existing knowledge.")
1708
  yield "\n".join(output_messages)
1709
+ return current_session_url, agent_context # Return directly without executing subsequent analysis steps
1710
 
1711
  # Check if we have any successful tool results
1712
  if successful_tools == 0:
1713
  output_messages.append("⚠️ No tools were successfully executed. Unable to provide analysis based on tool results.")
1714
  output_messages.append("💡 Please provide a valid financial report URL (PDF format) for analysis.")
1715
  yield "\n".join(output_messages)
1716
+ return current_session_url, agent_context
1717
  else:
1718
+ # No tool plan was generated - check if we should use context for analysis
1719
+ # CRITICAL: If user is asking for analysis and we have financial data in context, use it!
1720
+ if agent_context and 'last_financial_data' in agent_context:
1721
+ # User asked to analyze but we have the data in context already
1722
+ print(f"[CONTEXT] Using stored financial data for analysis request")
1723
+
1724
+ try:
1725
+ client = InferenceClient(
1726
+ model="Qwen/Qwen2.5-72B-Instruct",
1727
+ token=hf_token if hf_token else None
1728
+ )
1729
+
1730
+ # Extract financial data from context
1731
+ data = agent_context['last_financial_data']
1732
+ company_name = agent_context.get('last_company_name', 'the company')
1733
+ period = agent_context.get('last_period', 'the period')
1734
+ source_url = agent_context.get('last_financial_report_url', '')
1735
+
1736
+ # Format the financial data for analysis
1737
+ financial_summary = f"""Company: {company_name}
1738
+ Period: {period}
1739
+
1740
+ """
1741
+
1742
+ if 'total_revenue' in data:
1743
+ financial_summary += f"Total Revenue: ${data['total_revenue']:,}\n"
1744
+ if 'net_income' in data:
1745
+ financial_summary += f"Net Income: ${data['net_income']:,}\n"
1746
+ if 'earnings_per_share' in data:
1747
+ financial_summary += f"Earnings Per Share: ${data['earnings_per_share']}\n"
1748
+ if 'operating_expenses' in data:
1749
+ financial_summary += f"Operating Expenses: ${data['operating_expenses']:,}\n"
1750
+ if 'operating_cash_flow' in data:
1751
+ financial_summary += f"Operating Cash Flow: ${data['operating_cash_flow']:,}\n"
1752
+
1753
+ if source_url:
1754
+ financial_summary += f"\nSource: {source_url}\n"
1755
+
1756
+ # Create analysis prompt
1757
+ analysis_prompt = f"""
1758
+ You are a professional financial analyst. Analyze the following financial report data and provide comprehensive investment insights.
1759
+
1760
+ {financial_summary}
1761
+
1762
+ Additional data details:
1763
+ {json.dumps(data, indent=2)}
1764
+
1765
+ User's analysis request: {message}
1766
+
1767
+ Please provide a detailed analysis covering:
1768
+ 1. Revenue performance and trends
1769
+ 2. Profitability analysis (net income, margins, ROE, etc.)
1770
+ 3. Operating efficiency (expense ratios, cash flow)
1771
+ 4. Key financial metrics interpretation
1772
+ 5. Investment recommendations and risk assessment
1773
+ 6. Specific insights based on the user's request
1774
+
1775
+ Provide specific numbers and percentages from the data. Be detailed and data-driven.
1776
+ IMPORTANT: Use ONLY the actual numbers provided above - DO NOT make up or hallucinate any financial figures.
1777
+ """
1778
+
1779
+ output_messages.append("📊 Analyzing financial data from context...")
1780
+ yield "\n".join(output_messages)
1781
+
1782
+ messages = [
1783
+ {"role": "system", "content": "You are a professional financial analyst providing detailed investment insights based on financial reports. Always use actual data from the reports and provide specific numbers. Never hallucinate or make up financial figures."},
1784
+ {"role": "user", "content": analysis_prompt}
1785
+ ]
1786
+
1787
+ # Get response from LLM with streaming
1788
+ response = client.chat.completions.create(
1789
+ model="Qwen/Qwen2.5-72B-Instruct",
1790
+ messages=messages,
1791
+ max_tokens=min(max_tokens, 2048),
1792
+ temperature=0.3, # Lower temperature for more factual analysis
1793
+ top_p=top_p,
1794
+ stream=True,
1795
+ )
1796
+
1797
+ # Handle streaming response
1798
+ analysis_result = ""
1799
+ output_messages.append("") # Empty line before analysis
1800
+ for chunk in response:
1801
+ if hasattr(chunk, 'choices') and len(chunk.choices) > 0:
1802
+ if hasattr(chunk.choices[0], 'delta') and hasattr(chunk.choices[0].delta, 'content'):
1803
+ content = chunk.choices[0].delta.content
1804
+ if content:
1805
+ analysis_result += content
1806
+ output_messages[-1] = analysis_result
1807
+ yield "\n".join(output_messages)
1808
+
1809
+ # Return after analysis
1810
+ return current_session_url, agent_context
1811
+
1812
+ except Exception as e:
1813
+ error_msg = f"❌ Error during analysis: {str(e)}"
1814
+ print(f"[DEBUG] {error_msg}")
1815
+ output_messages.append(error_msg)
1816
+ yield "\n".join(output_messages)
1817
+ return current_session_url, agent_context
1818
+
1819
+ # If no context data, engage in natural conversation
1820
  try:
1821
  client = InferenceClient(
1822
  model="Qwen/Qwen2.5-72B-Instruct",
1823
+ token=hf_token if hf_token else None
1824
  )
1825
 
1826
  # Format conversation history for context
 
1834
  conversation_prompt = f"""
1835
  You are an intelligent financial analysis assistant with expertise in investment research and financial analysis.
1836
  You can engage in natural conversation and provide insights based on your knowledge and the context provided.
1837
+
1838
  {history_context}
1839
+
1840
  Current user message: {message}
1841
+
1842
  Guidelines for your response:
1843
  1. If the user is just greeting you or having casual conversation, respond warmly and naturally
1844
  2. If the user is asking about a specific financial report or company analysis, explain that you can help search for and analyze financial reports
 
1847
  5. Always be helpful, conversational, and friendly while maintaining your expertise
1848
  6. Keep responses focused and under 500 words
1849
  7. For casual greetings or simple questions, keep your response brief and natural
1850
+
1851
  Please provide a helpful, conversational response:
1852
  """
1853
 
 
1891
  yield "\n".join(output_messages)
1892
 
1893
  # Return after conversation - no need to process tool results
1894
+ return current_session_url, agent_context
1895
 
1896
  # Filter out the successful_tools item from tool_results
1897
  filtered_tool_results = [result for result in tool_results if not (isinstance(result, dict) and "successful_tools" in result)] if 'tool_results' in locals() else []
 
1958
 
1959
  analysis_prompt = f"""
1960
  You are a professional financial analyst. Analyze the following financial report and provide comprehensive investment insights.
1961
+
1962
  Financial Report: {filename}{source_context}
1963
+
1964
  Report Content:
1965
  {file_content}
1966
+
1967
  Please provide a detailed analysis covering:
1968
  1. Revenue performance and trends
1969
  2. Profitability analysis (net income, margins, etc.)
 
1971
  4. Cash flow analysis
1972
  5. Key financial ratios and metrics
1973
  6. Investment recommendations and risk assessment
1974
+
1975
  Provide specific numbers and percentages from the report. Be detailed and data-driven.
1976
  """
1977
 
 
1979
  try:
1980
  client = InferenceClient(
1981
  model="Qwen/Qwen2.5-72B-Instruct",
1982
+ token=hf_token if hf_token else None
1983
  )
1984
 
1985
  messages = [
 
2015
  yield "\n".join(output_messages)
2016
 
2017
  # Analysis complete
2018
+ return current_session_url, agent_context
2019
 
2020
  except Exception as e:
2021
  print(f"[ERROR] Failed to analyze file: {str(e)}")
2022
  output_messages.append(f"\n⚠️ Analysis failed: {str(e)}")
2023
  yield "\n".join(output_messages)
2024
+ return current_session_url, agent_context
2025
 
2026
  # Old format - just store file path
2027
  if 'file_path' in tool_result_data:
 
2171
  try:
2172
  client = InferenceClient(
2173
  model="Qwen/Qwen2.5-72B-Instruct",
2174
+ token=hf_token if hf_token else None
2175
  )
2176
 
2177
  # Format the download links for the prompt
 
2184
  # Create prompt for final response
2185
  final_response_prompt = f"""
2186
  You are a helpful financial analysis assistant. Based on the user's request and the tool execution results, provide a clear, concise, and intelligent final response.
2187
+
2188
  User's original request: {message}
2189
+
2190
  Tool execution results - Download links found:
2191
  {links_summary}
2192
+
2193
  IMPORTANT INSTRUCTIONS:
2194
  1. Analyze the user's request carefully to understand their true intent
2195
  2. You MUST use the EXACT URLs provided in the tool results above - DO NOT modify or invent URLs
 
2201
  5. Use emoji appropriately (📄 for title, 🔗 for URL, 📋 for description)
2202
  6. Keep your response helpful and aligned with the user's actual intent
2203
  7. DO NOT make assumptions - let the user's question guide your response format
2204
+
2205
  Provide an intelligent, contextual response:
2206
  """
2207
 
 
2254
  output_messages.append("✅ You can click on the links above to download the financial reports directly.")
2255
  yield "\n".join(output_messages)
2256
 
2257
+ return current_session_url, agent_context # End processing here - user just wanted download links
2258
  # Fallback: Present tool results summary and generate intelligent final response
2259
  elif filtered_tool_results:
2260
  # Re-check if there are any download links in the results that we might have missed
 
2317
  try:
2318
  client = InferenceClient(
2319
  model="Qwen/Qwen2.5-72B-Instruct",
2320
+ token=hf_token if hf_token else None
2321
  )
2322
 
2323
  # Format the download links for the prompt
 
2330
  # Create prompt for final response
2331
  final_response_prompt = f"""
2332
  You are a helpful financial analysis assistant. Based on the user's request and the tool execution results, provide a clear, concise, and intelligent final response.
2333
+
2334
  User's original request: {message}
2335
+
2336
  Tool execution results - Download links found:
2337
  {links_summary}
2338
+
2339
  IMPORTANT INSTRUCTIONS:
2340
  1. Analyze the user's request carefully to understand their true intent
2341
  2. You MUST use the EXACT URLs provided in the tool results above - DO NOT modify or invent URLs
 
2352
  | Column 1 | Column 2 |
2353
  |----------|----------|
2354
  | Data 1 | Data 2 |
2355
+
2356
  Provide an intelligent, contextual response:
2357
  """
2358
 
 
2409
  # If no download links found, collect tool execution summary
2410
  # First, collect tool execution summary including actual data
2411
  tool_summary = ""
2412
+ has_error_results = False # Flag to track if any tools returned errors
2413
+ successful_data_retrieval = False # Flag to track if we successfully retrieved data
2414
+
2415
+ for result in filtered_tool_results:
2416
+ # Check if this tool execution was marked as failed
2417
+ if isinstance(result, dict) and result.get("success") == False:
2418
+ tool_name = result.get("tool", "")
2419
+ # Check if this is a third-party MCP tool
2420
+ if tool_name in THIRD_PARTY_MCP_TOOLS:
2421
+ # Only consider it an error if it's a critical tool or if we haven't successfully retrieved data yet
2422
+ # Critical tools are those that directly retrieve financial data
2423
+ critical_tools = ["get_financial_data", "extract_financial_metrics", "get_latest_financial_data"]
2424
+ if tool_name in critical_tools or not successful_data_retrieval:
2425
+ has_error_results = True
2426
+ error_info = result.get("result", {})
2427
+ error_msg = error_info.get("error", "Unknown error") if isinstance(error_info, dict) else str(error_info)
2428
+ print(f"[DEBUG] Third-party tool {tool_name} failed: {error_msg}")
2429
+
2430
+ # Check if this tool execution was successful and retrieved financial data
2431
+ elif isinstance(result, dict) and result.get("success") == True:
2432
+ tool_name = result.get("tool", "")
2433
+ # Check if this tool retrieved financial data
2434
+ if tool_name in ["get_financial_data", "extract_financial_metrics", "get_latest_financial_data"]:
2435
+ tool_result = result.get("result", {})
2436
+ # Check if the result contains actual financial data
2437
+ if tool_result and isinstance(tool_result, dict):
2438
+ # Look for financial data indicators in various result formats
2439
+ has_financial_data = False
2440
+
2441
+ # Check direct result format
2442
+ if "period" in tool_result and ("total_revenue" in tool_result or "net_income" in tool_result):
2443
+ has_financial_data = True
2444
+
2445
+ # Check content array format
2446
+ elif "content" in tool_result and isinstance(tool_result["content"], list) and len(tool_result["content"]) > 0:
2447
+ content_item = tool_result["content"][0]
2448
+ if isinstance(content_item, dict) and "text" in content_item:
2449
+ try:
2450
+ content_json = json.loads(content_item["text"])
2451
+ if isinstance(content_json, dict) and ("period" in content_json and ("total_revenue" in content_json or "net_income" in content_json)):
2452
+ has_financial_data = True
2453
+ except json.JSONDecodeError:
2454
+ pass # Not JSON, continue normally
2455
+
2456
+ if has_financial_data:
2457
+ successful_data_retrieval = True
2458
+ has_error_results = False # Override any previous errors since we got the data
2459
+ print(f"[DEBUG] Successfully retrieved financial data from {tool_name}")
2460
+
2461
+ # Also check for errors in successful tool results (tools that ran but returned error data)
2462
+ if result is not None and 'tool' in result and 'result' in result and result['result'] is not None:
2463
+ tool_name = result.get('tool', '')
2464
+ tool_result = result.get('result', {})
2465
+
2466
+ # Check if this is a third-party MCP tool
2467
+ if tool_name in THIRD_PARTY_MCP_TOOLS:
2468
+ # Check for error in the result
2469
+ if isinstance(tool_result, dict):
2470
+ # Check direct error field
2471
+ if 'error' in tool_result and tool_result['error']:
2472
+ # Only consider it an error if we haven't successfully retrieved data yet
2473
+ if not successful_data_retrieval:
2474
+ has_error_results = True
2475
+ print(f"[DEBUG] Third-party tool {tool_name} returned error: {tool_result['error']}")
2476
+ # Check structured content for errors
2477
+ elif 'structuredContent' in tool_result and 'result' in tool_result['structuredContent']:
2478
+ structured_result = tool_result['structuredContent']['result']
2479
+ if isinstance(structured_result, dict) and 'error' in structured_result and structured_result['error']:
2480
+ # Only consider it an error if we haven't successfully retrieved data yet
2481
+ if not successful_data_retrieval:
2482
+ has_error_results = True
2483
+ print(f"[DEBUG] Third-party tool {tool_name} returned error in structuredContent: {structured_result['error']}")
2484
+ # Check content array for errors
2485
+ elif 'content' in tool_result and isinstance(tool_result['content'], list) and len(tool_result['content']) > 0:
2486
+ content_item = tool_result['content'][0]
2487
+ if isinstance(content_item, dict) and 'text' in content_item:
2488
+ try:
2489
+ content_json = json.loads(content_item['text'])
2490
+ if isinstance(content_json, dict) and 'error' in content_json and content_json['error']:
2491
+ # Only consider it an error if we haven't successfully retrieved data yet
2492
+ if not successful_data_retrieval:
2493
+ has_error_results = True
2494
+ print(f"[DEBUG] Third-party tool {tool_name} returned error in content: {content_json['error']}")
2495
+ except json.JSONDecodeError:
2496
+ pass # Not JSON, continue normally
2497
+
2498
+ # If any third-party tools returned errors, don't generate fake data
2499
+ if has_error_results:
2500
+ output_messages.append("\n❌ Some tools encountered errors. Unable to provide accurate financial data.")
2501
+ output_messages.append("💡 This may be because the requested data doesn't exist or there was an issue accessing the SEC database.")
2502
+ yield "\n".join(output_messages)
2503
+ return current_session_url, agent_context
2504
+
2505
  for result in filtered_tool_results:
2506
  if result is not None and 'tool' in result:
2507
  tool_name = result.get('tool', 'Unknown Tool')
 
2529
  tool_result_data = result['result']
2530
 
2531
  if tool_result_data:
2532
+ # CRITICAL: Check if this result contains error information
2533
+ # Many third-party tools return errors in the content as JSON strings
2534
+ if isinstance(tool_result_data, dict) and 'error' in tool_result_data and tool_result_data['error']:
2535
+ has_error_results = True
2536
+ tool_summary += f"Error: {tool_result_data['error']}\n"
2537
+ elif isinstance(tool_result_data, dict) and 'content' in tool_result_data:
2538
+ # Check if content contains error information
2539
+ content_items = tool_result_data['content']
2540
+ if isinstance(content_items, list) and len(content_items) > 0:
2541
+ first_item = content_items[0]
2542
+ if isinstance(first_item, dict) and 'text' in first_item:
2543
+ try:
2544
+ content_json = json.loads(first_item['text'])
2545
+ if isinstance(content_json, dict) and 'error' in content_json and content_json['error']:
2546
+ has_error_results = True
2547
+ tool_summary += f"Error: {content_json['error']}\n"
2548
+ except json.JSONDecodeError:
2549
+ pass # Not JSON, continue normally
2550
+
2551
+ # CRITICAL: Include source_url if available (especially for financial data)
2552
+ if 'source_url' in tool_result_data and tool_result_data['source_url']:
2553
+ tool_summary += f"Source URL: {tool_result_data['source_url']}\n"
2554
+
2555
+ # Include other financial data fields
2556
+ if 'period' in tool_result_data:
2557
+ tool_summary += f"Period: {tool_result_data['period']}\n"
2558
+ if 'total_revenue' in tool_result_data:
2559
+ tool_summary += f"Revenue: ${tool_result_data['total_revenue']:,.0f}\n"
2560
+ if 'net_income' in tool_result_data:
2561
+ tool_summary += f"Net Income: ${tool_result_data['net_income']:,.0f}\n"
2562
+ if 'earnings_per_share' in tool_result_data:
2563
+ tool_summary += f"EPS: ${tool_result_data['earnings_per_share']}\n"
2564
+
2565
  # Show summary information
2566
  if 'message' in tool_result_data:
2567
  tool_summary += f"Message: {tool_result_data['message']}\n"
 
2587
  output_messages.append("\n✅ Tool execution completed successfully!")
2588
  yield "\n".join(output_messages)
2589
 
2590
+ # CRITICAL: Extract key information from tool results to agent context for multi-turn dialogue
2591
+ for result in filtered_tool_results:
2592
+ if result is not None and 'tool' in result and 'result' in result and result['result'] is not None:
2593
+ tool_name = result.get('tool', '')
2594
+ tool_result = result.get('result', {})
2595
+
2596
+ # Extract text from MCP content format
2597
+ if 'content' in tool_result and isinstance(tool_result['content'], list) and len(tool_result['content']) > 0:
2598
+ content_item = tool_result['content'][0]
2599
+ if isinstance(content_item, dict) and 'text' in content_item:
2600
+ try:
2601
+ parsed_data = json.loads(content_item['text'])
2602
+
2603
+ # Extract company information from search_company
2604
+ if tool_name == 'search_company' and isinstance(parsed_data, dict):
2605
+ if 'cik' in parsed_data:
2606
+ agent_context['last_company_cik'] = parsed_data['cik']
2607
+ print(f"[CONTEXT] Stored CIK: {parsed_data['cik']}")
2608
+ if 'name' in parsed_data:
2609
+ agent_context['last_company_name'] = parsed_data['name']
2610
+ print(f"[CONTEXT] Stored company name: {parsed_data['name']}")
2611
+ if 'ticker' in parsed_data:
2612
+ agent_context['last_company_ticker'] = parsed_data['ticker']
2613
+ print(f"[CONTEXT] Stored ticker: {parsed_data['ticker']}")
2614
+
2615
+ # Extract financial data from get_financial_data
2616
+ elif tool_name == 'get_financial_data' and isinstance(parsed_data, dict):
2617
+ # Check if we have actual financial data or just period
2618
+ has_financial_data = any(key in parsed_data for key in ['total_revenue', 'net_income', 'earnings_per_share', 'source_url'])
2619
+
2620
+ if not has_financial_data:
2621
+ print(f"[CONTEXT] ⚠️ WARNING: get_financial_data returned incomplete data (only period)")
2622
+ print(f"[CONTEXT] This likely means the requested financial data is not available")
2623
+ # Store a flag indicating incomplete data
2624
+ agent_context['incomplete_financial_data'] = True
2625
+ agent_context['incomplete_data_reason'] = f"No financial metrics found for {parsed_data.get('period', 'requested period')}"
2626
+ else:
2627
+ # We have complete data
2628
+ agent_context['incomplete_financial_data'] = False
2629
+
2630
+ if 'period' in parsed_data:
2631
+ agent_context['last_period'] = parsed_data['period']
2632
+ print(f"[CONTEXT] Stored period: {parsed_data['period']}")
2633
+ if 'total_revenue' in parsed_data:
2634
+ agent_context['last_revenue'] = parsed_data['total_revenue']
2635
+ if 'net_income' in parsed_data:
2636
+ agent_context['last_net_income'] = parsed_data['net_income']
2637
+ if 'source_url' in parsed_data:
2638
+ agent_context['last_financial_report_url'] = parsed_data['source_url']
2639
+ print(f"[CONTEXT] Stored financial report URL: {parsed_data['source_url']}")
2640
+ # Store the complete financial data for reference
2641
+ agent_context['last_financial_data'] = parsed_data
2642
+ print(f"[CONTEXT] Stored financial data for {agent_context.get('last_company_name', 'company')}")
2643
+
2644
+ except json.JSONDecodeError:
2645
+ pass
2646
+
2647
  # Generate intelligent final response based on tool results
2648
  try:
2649
+ # DEBUG: Print the tool_summary to see what LLM receives
2650
+ print(f"[DEBUG] Tool summary being sent to LLM:")
2651
+ print(f"=" * 80)
2652
+ print(tool_summary)
2653
+ print(f"=" * 80)
2654
+
2655
+ # Check if we have incomplete financial data
2656
+ if agent_context.get('incomplete_financial_data', False):
2657
+ # Provide a helpful error message to user
2658
+ company_name = agent_context.get('last_company_name', 'the company')
2659
+ period = agent_context.get('last_period', 'the requested period')
2660
+
2661
+ output_messages.append("")
2662
+ output_messages.append(f"⚠️ Sorry, detailed financial data for {company_name} {period} is not available in the SEC EDGAR database.")
2663
+ output_messages.append("")
2664
+ output_messages.append("💡 This could be because:")
2665
+ output_messages.append(f" • {company_name}'s fiscal {period} report hasn't been filed yet")
2666
+ output_messages.append(f" • {company_name} uses a different fiscal calendar")
2667
+ output_messages.append(f" • The period name might be different (try '2024Q4' or specific fiscal year periods)")
2668
+ output_messages.append("")
2669
+ output_messages.append("🔍 You can try:")
2670
+ output_messages.append(f" • Searching for a different quarter (e.g., '2024Q4', '2024Q3')")
2671
+ output_messages.append(f" • Visiting the SEC EDGAR website directly: https://www.sec.gov/cgi-bin/browse-edgar?action=getcompany&CIK={agent_context.get('last_company_cik', '')}&type=10-Q&dateb=&owner=exclude&count=40")
2672
+ yield "\n".join(output_messages)
2673
+ return current_session_url, agent_context
2674
+
2675
  client = InferenceClient(
2676
  model="Qwen/Qwen2.5-72B-Instruct",
2677
+ token=hf_token if hf_token else None
2678
  )
2679
 
2680
  # Create prompt for final response
2681
  final_response_prompt = f"""
2682
  You are a helpful financial analysis assistant. Based on the user's request and the tool execution results, provide a clear, concise, and intelligent final response.
2683
+
2684
  User's original request: {message}
2685
+
2686
  Tool execution results:
2687
  {tool_summary}
2688
+
2689
  IMPORTANT INSTRUCTIONS:
2690
  1. Carefully analyze the user's request to understand their true intent
2691
  2. The tool execution results above contain the actual data - use them!
2692
+ 3. If a "Source URL" field is provided in the results, YOU MUST include it in your response as a clickable link
2693
  4. DO NOT make up or invent any information that is not in the results
2694
+ 5. DO NOT create fake URLs, links, or placeholder links like "Apple 2025 Q1 Financial Report" - only use EXACT URLs from the tool results
2695
+ 6. If financial data is provided with a source_url, format it as: "For more details, see the [official SEC filing](EXACT_URL_HERE)"
2696
+ 7. If the user requested a specific format (e.g., table), provide it using markdown
2697
+ 8. Present information clearly based on what the user actually asked for
2698
+ 9. If results contain links, present them properly formatted with titles and EXACT URLs from the tool results
2699
+ 10. Keep your response helpful and aligned with the user's actual intent
2700
+ 11. CRITICAL: Never create placeholder or fake links - if no URL is in the results, don't include a link
2701
+
2702
  Provide a clear, accurate final response based on the tool execution results above:
2703
  """
2704
 
 
2796
  try:
2797
  client = InferenceClient(
2798
  model="Qwen/Qwen2.5-72B-Instruct",
2799
+ token=hf_token if hf_token else None
2800
  )
2801
 
2802
  # Format conversation history for context
 
2810
  conversation_prompt = f"""
2811
  You are an intelligent financial analysis assistant with expertise in investment research and financial analysis.
2812
  You can engage in natural conversation and provide insights based on your knowledge and the context provided.
2813
+
2814
  {history_context}
2815
+
2816
  Current user message: {message}
2817
+
2818
  Guidelines for your response:
2819
  1. If the user is asking about a specific financial report or company analysis, explain that you can help but need a URL (or PDF format URL)
2820
  2. If the user is asking follow-up questions about investments or financial concepts, provide informed insights based on your expertise
 
2844
  26. When search results are unhelpful, acknowledge this and continue with normal conversation flow
2845
  # If search returned no results flag is set, directly engage in natural conversation without executing subsequent analysis
2846
  # Return directly without executing subsequent analysis steps
2847
+
2848
  27. For general inquiries or conversational requests that don't require financial analysis tools, engage in natural conversation without initiating financial analysis workflows
2849
+
2850
  Please provide a helpful, conversational response:
2851
  """
2852
 
 
2887
  try:
2888
  client = InferenceClient(
2889
  model="Qwen/Qwen2.5-72B-Instruct",
2890
+ token=hf_token if hf_token else None
2891
  )
2892
 
2893
  # Format conversation history for context
 
2900
  # Create intelligent conversation prompt
2901
  conversation_prompt = f"""
2902
  You are an intelligent financial analysis assistant with expertise in investment research and financial analysis.
2903
+
2904
  {history_context}
2905
+
2906
  Current user message: {message}
2907
+
2908
  Guidelines for your response:
2909
  1. Respond naturally and helpfully to the user's question
2910
  2. Use your financial expertise to provide valuable insights
 
2912
  4. For general financial questions, provide informed answers based on your knowledge
2913
  5. Keep responses concise and focused (under 500 words)
2914
  6. Be conversational and friendly while maintaining professional expertise
2915
+
2916
  Please provide a helpful response:
2917
  """
2918
 
 
2966
  except Exception as e:
2967
  output_messages.append(f"❌ Error: {str(e)}")
2968
  yield "\n".join(output_messages)
2969
+ return current_session_url, agent_context # Return session URL and context even on error
2970
 
2971
 
2972
  def validate_url(url):
 
2989
  print(f"URL validation error for {url}: {str(e)}")
2990
  return False
2991
 
2992
+
2993
+ # """
2994
+ # For information on how to customize the ChatInterface, peruse the gradio docs: https://www.gradio.app/docs/chatinterface
2995
+ # """
2996
+ # chatbot = gr.ChatInterface(
2997
+ # respond,
2998
+ # title="Easy Financial Report",
2999
+ # additional_inputs=[
3000
+ # gr.State(value=""), # CRITICAL: Store session URL across turns (hidden from UI)
3001
+ # gr.State(value={}) # CRITICAL: Store agent context across turns (hidden from UI)
3002
+ # ],
3003
+ # additional_inputs_accordion=gr.Accordion(label="Settings", open=False, visible=False), # Hide the accordion completely
3004
+ # )
3005
+
3006
+
3007
+ # with gr.Blocks() as demo:
3008
+ # # Add custom CSS for Agent Plan styling
3009
+ # gr.Markdown("""
3010
+ # <style>
3011
+ # .agent-plan {
3012
+ # background-color: #f8f9fa;
3013
+ # border-left: 4px solid #6c757d;
3014
+ # padding: 10px;
3015
+ # margin: 10px 0;
3016
+ # border-radius: 4px;
3017
+ # font-family: monospace;
3018
+ # color: #495057;
3019
+ # }
3020
+ # </style>
3021
+ # """)
3022
+
3023
+ # chatbot.render()
3024
+
3025
+
3026
+ # if __name__ == "__main__":
3027
+ # demo.launch(share=True)
3028
+
3029
  # def create_financial_chatbot():
3030
  # """
3031
  # 返回一个可嵌入的 ChatInterface 组件
requirements.txt CHANGED
@@ -11,4 +11,7 @@ beautifulsoup4>=4.11.0
11
  requests>=2.32.0
12
  urllib3>=2.5.0
13
  httpx>=0.23.0
14
- pymysql
 
 
 
 
11
  requests>=2.32.0
12
  urllib3>=2.5.0
13
  httpx>=0.23.0
14
+ pymysql
15
+ sse-starlette>=1.6.5
16
+ starlette>=0.27.0
17
+ mcp>=1.0.0
service/news_quote_mcp.py ADDED
@@ -0,0 +1,706 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ import requests
3
+ import gradio as gr
4
+ from typing import Optional, List, Dict, Any
5
+ from datetime import datetime, timedelta
6
+ from mcp.server.fastmcp import FastMCP
7
+ from dotenv import load_dotenv
8
+
9
+ # 加载.env文件中的环境变量
10
+ # 修复:使用更可靠的方式加载.env文件
11
+ dotenv_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), '.env')
12
+ if os.path.exists(dotenv_path):
13
+ load_dotenv(dotenv_path)
14
+ print(f"Loaded .env file from: {dotenv_path}")
15
+ else:
16
+ # 如果在当前目录找不到,尝试在项目根目录查找
17
+ root_dotenv_path = os.path.join(os.path.dirname(os.path.dirname(os.path.abspath(__file__))), '.env')
18
+ if os.path.exists(root_dotenv_path):
19
+ load_dotenv(root_dotenv_path)
20
+ print(f"Loaded .env file from root: {root_dotenv_path}")
21
+ else:
22
+ print("No .env file found, checking environment variables...")
23
+
24
+ # Initialize FastMCP server with standard MCP protocol (JSON-RPC over SSE)
25
+ mcp = FastMCP("finnhub-market-info")
26
+
27
+ # Global variable to store API key
28
+ API_KEY = None
29
+
30
+
31
+ def set_api_key(key: str):
32
+ """Set the Finnhub API key"""
33
+ global API_KEY
34
+ API_KEY = key
35
+
36
+
37
+ def get_api_key() -> Optional[str]:
38
+ """Get the Finnhub API key"""
39
+ global API_KEY
40
+ # 修复:按优先级顺序获取API密钥
41
+ api_key = (
42
+ API_KEY or # 全局变量
43
+ os.getenv("FINNHUB_API_KEY") or # 环境变量
44
+ os.environ.get('FINNHUB_API_KEY') # 备用环境变量获取方式
45
+ )
46
+
47
+ # 添加调试信息
48
+ print(f"API Key sources:")
49
+ print(f" Global API_KEY: {API_KEY}")
50
+ print(f" os.getenv('FINNHUB_API_KEY'): {os.getenv('FINNHUB_API_KEY')}")
51
+ print(f" os.environ.get('FINNHUB_API_KEY'): {os.environ.get('FINNHUB_API_KEY')}")
52
+ print(f" Final API key: {'*' * len(api_key) if api_key else 'None'}")
53
+
54
+ return api_key
55
+
56
+
57
+ def make_finnhub_request(endpoint: str, params: Dict[str, Any]) -> Dict[str, Any]:
58
+ """
59
+ Make a request to Finnhub API
60
+
61
+ Args:
62
+ endpoint: API endpoint path
63
+ params: Query parameters
64
+
65
+ Returns:
66
+ API response as dictionary
67
+ """
68
+ api_key = get_api_key()
69
+ if not api_key:
70
+ return {"error": "API key not configured. Please set your Finnhub API key."}
71
+
72
+ params["token"] = api_key
73
+ base_url = "https://finnhub.io/api/v1"
74
+ url = f"{base_url}/{endpoint}"
75
+
76
+ try:
77
+ response = requests.get(url, params=params, timeout=10)
78
+ response.raise_for_status()
79
+ return response.json()
80
+ except requests.exceptions.RequestException as e:
81
+ return {"error": f"API request failed: {str(e)}"}
82
+
83
+
84
+ @mcp.tool()
85
+ def get_quote(symbol: str) -> dict:
86
+ """
87
+ Get real-time quote data for US stocks. Use this tool when you need current stock price
88
+ information and market performance metrics for any US-listed stock.
89
+
90
+ When to use:
91
+ - User asks "What's the current price of [stock]?"
92
+ - Need real-time stock quote data
93
+ - User mentions "stock price", "current value", "how is [stock] trading?"
94
+ - Want to check latest market price and daily changes
95
+
96
+ Examples:
97
+ - "What's Apple's stock price?" → get_quote(symbol="AAPL")
98
+ - "How is Tesla trading today?" → get_quote(symbol="TSLA")
99
+ - "Show me Microsoft's current quote" → get_quote(symbol="MSFT")
100
+
101
+ Args:
102
+ symbol: Stock ticker symbol (e.g., 'AAPL', 'MSFT', 'TSLA', 'GOOGL')
103
+
104
+ Returns:
105
+ dict: Real-time quote data containing:
106
+ - symbol: Stock ticker symbol
107
+ - current_price: Current trading price (c)
108
+ - change: Price change in dollars (d)
109
+ - percent_change: Price change in percentage (dp)
110
+ - high: Today's high price (h)
111
+ - low: Today's low price (l)
112
+ - open: Opening price (o)
113
+ - previous_close: Previous trading day's closing price (pc)
114
+ - timestamp: Quote timestamp
115
+ """
116
+ result = make_finnhub_request("quote", {"symbol": symbol.upper()})
117
+
118
+ if "error" in result:
119
+ return {
120
+ "error": result["error"],
121
+ "symbol": symbol.upper()
122
+ }
123
+
124
+ # Return structured data
125
+ return {
126
+ "symbol": symbol.upper(),
127
+ "current_price": result.get('c'),
128
+ "change": result.get('d'),
129
+ "percent_change": result.get('dp'),
130
+ "high": result.get('h'),
131
+ "low": result.get('l'),
132
+ "open": result.get('o'),
133
+ "previous_close": result.get('pc'),
134
+ "timestamp": datetime.fromtimestamp(result.get('t', 0)).strftime('%Y-%m-%d %H:%M:%S') if result.get('t') else None
135
+ }
136
+
137
+
138
+ @mcp.tool()
139
+ def get_market_news(category: str = "general", min_id: int = 0) -> dict:
140
+ """
141
+ Get latest market news across different categories. Use this tool when you need current market
142
+ news, trends, and developments in general markets, forex, cryptocurrency, or mergers.
143
+
144
+ When to use:
145
+ - User asks "What's the latest market news?"
146
+ - Need current financial news and market updates
147
+ - User mentions "news", "market trends", "what's happening in the market?"
148
+ - Want to get news for specific categories (forex, crypto, M&A)
149
+
150
+ Categories explained:
151
+ - general: General market news, stocks, economy, major companies
152
+ - forex: Foreign exchange and currency market news
153
+ - crypto: Cryptocurrency and blockchain news
154
+ - merger: Mergers & acquisitions, corporate deals
155
+
156
+ Examples:
157
+ - "What's the latest market news?" → get_market_news(category="general")
158
+ - "Show me crypto news" → get_market_news(category="crypto")
159
+ - "Any forex updates?" → get_market_news(category="forex")
160
+ - "Recent merger news" → get_market_news(category="merger")
161
+
162
+ Args:
163
+ category: News category - "general", "forex", "crypto", or "merger" (default: "general")
164
+ min_id: Minimum news ID for pagination (default: 0, use 0 to get latest news)
165
+
166
+ Returns:
167
+ dict: Market news data containing:
168
+ - category: News category requested
169
+ - total_articles: Total number of articles returned
170
+ - articles: List of news articles (max 10), each with:
171
+ * id: Article ID
172
+ * headline: News headline
173
+ * summary: Brief summary of the article
174
+ * source: News source
175
+ * url: Link to full article
176
+ * published: Publication timestamp
177
+ * image: Article image URL (if available)
178
+ """
179
+ params = {"category": category}
180
+ if min_id > 0:
181
+ params["minId"] = min_id # pyright: ignore[reportArgumentType]
182
+
183
+ result = make_finnhub_request("news", params)
184
+
185
+ if isinstance(result, dict) and "error" in result:
186
+ return {
187
+ "error": result["error"],
188
+ "category": category
189
+ }
190
+
191
+ if not result or len(result) == 0:
192
+ return {
193
+ "category": category,
194
+ "total_articles": 0,
195
+ "articles": [],
196
+ "message": "No news articles found for this category"
197
+ }
198
+
199
+ # Format the news articles
200
+ articles = []
201
+ for article in result[:10]: # Limit to 10 articles # pyright: ignore[reportArgumentType]
202
+ articles.append({
203
+ "id": article.get('id'),
204
+ "headline": article.get('headline', 'No headline'),
205
+ "summary": article.get('summary', ''),
206
+ "source": article.get('source', 'Unknown'),
207
+ "url": article.get('url'),
208
+ "published": datetime.fromtimestamp(article.get('datetime', 0)).strftime('%Y-%m-%d %H:%M:%S') if article.get('datetime') else None,
209
+ "image": article.get('image')
210
+ })
211
+
212
+ return {
213
+ "category": category,
214
+ "total_articles": len(articles),
215
+ "articles": articles
216
+ }
217
+
218
+
219
+ @mcp.tool()
220
+ def get_company_news(symbol: str, from_date: Optional[str] = None, to_date: Optional[str] = None) -> dict:
221
+ """
222
+ Get latest news for a specific company by stock symbol. This endpoint provides company-specific
223
+ news, press releases, and announcements. Only available for North American companies.
224
+
225
+ When to use:
226
+ - User asks about a specific company's news (e.g., "Apple news", "Tesla updates")
227
+ - Need company-specific announcements or press releases
228
+ - User mentions "[company name] news", "recent [company] developments"
229
+ - Want to filter news by date range
230
+
231
+ Date range tips:
232
+ - Default: Last 7 days if no dates specified
233
+ - Can go back up to several years
234
+ - Use YYYY-MM-DD format (e.g., "2024-01-01")
235
+
236
+ Examples:
237
+ - "What's the latest Apple news?" → get_company_news(symbol="AAPL")
238
+ - "Tesla news from last month" → get_company_news(symbol="TSLA", from_date="2024-10-01", to_date="2024-10-31")
239
+ - "Microsoft announcements this week" → get_company_news(symbol="MSFT")
240
+ - "Show me Amazon news from January 2024" → get_company_news(symbol="AMZN", from_date="2024-01-01", to_date="2024-01-31")
241
+
242
+ Args:
243
+ symbol: Company stock ticker symbol (e.g., 'AAPL', 'MSFT', 'TSLA', 'GOOGL')
244
+ Must be a North American (US/Canada) listed company
245
+ from_date: Start date in YYYY-MM-DD format (default: 7 days ago)
246
+ to_date: End date in YYYY-MM-DD format (default: today)
247
+
248
+ Returns:
249
+ dict: Company news data containing:
250
+ - symbol: Stock ticker symbol
251
+ - from_date: Start date of news search
252
+ - to_date: End date of news search
253
+ - total_articles: Number of articles found
254
+ - articles: List of news articles (max 10), each with:
255
+ * id: Article ID
256
+ * headline: News headline
257
+ * summary: Article summary
258
+ * source: News source
259
+ * url: Link to full article
260
+ * published: Publication date and time
261
+ * image: Article image URL (if available)
262
+ * related_symbols: Other stock symbols mentioned
263
+ """
264
+ # Set default dates if not provided
265
+ if not to_date:
266
+ to_date = datetime.now().strftime('%Y-%m-%d')
267
+ if not from_date:
268
+ from_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')
269
+
270
+ params = {
271
+ "symbol": symbol.upper(),
272
+ "from": from_date,
273
+ "to": to_date
274
+ }
275
+
276
+ result = make_finnhub_request("company-news", params)
277
+
278
+ if isinstance(result, dict) and "error" in result:
279
+ return {
280
+ "error": result["error"],
281
+ "symbol": symbol.upper(),
282
+ "from_date": from_date,
283
+ "to_date": to_date
284
+ }
285
+
286
+ if not result or len(result) == 0:
287
+ # Provide helpful suggestions based on the symbol
288
+ suggestion = "Try expanding the date range or check if the symbol is correct."
289
+ if symbol.upper() in ["BABA", "BIDU", "JD", "PDD", "NIO"]:
290
+ suggestion = "Note: Chinese ADRs may have limited news coverage. Try US companies like AAPL, MSFT, TSLA, or GOOGL for better results."
291
+
292
+ return {
293
+ "symbol": symbol.upper(),
294
+ "from_date": from_date,
295
+ "to_date": to_date,
296
+ "total_articles": 0,
297
+ "articles": [],
298
+ "message": f"No news articles found for {symbol.upper()} between {from_date} and {to_date}.",
299
+ "suggestion": suggestion,
300
+ "note": "Company news is only available for North American companies. Some companies may have limited news coverage during certain periods."
301
+ }
302
+
303
+ # Format the news articles
304
+ articles = []
305
+ for article in result[:10]: # Limit to 10 articles # pyright: ignore[reportArgumentType]
306
+ articles.append({
307
+ "id": article.get('id'),
308
+ "headline": article.get('headline', 'No headline'),
309
+ "summary": article.get('summary', ''),
310
+ "source": article.get('source', 'Unknown'),
311
+ "url": article.get('url'),
312
+ "published": datetime.fromtimestamp(article.get('datetime', 0)).strftime('%Y-%m-%d %H:%M:%S') if article.get('datetime') else None,
313
+ "image": article.get('image'),
314
+ "related_symbols": article.get('related', [])
315
+ })
316
+
317
+ return {
318
+ "symbol": symbol.upper(),
319
+ "from_date": from_date,
320
+ "to_date": to_date,
321
+ "total_articles": len(articles),
322
+ "articles": articles
323
+ }
324
+
325
+
326
+ # Gradio Interface Functions
327
+ def configure_api_key(api_key: str) -> str:
328
+ """Configure the Finnhub API key"""
329
+ if not api_key or api_key.strip() == "":
330
+ return "❌ Please provide a valid API key"
331
+ set_api_key(api_key.strip())
332
+ return "✅ API Key configured successfully! You can now use the MCP tools."
333
+
334
+
335
+ def test_quote_tool(symbol: str) -> str:
336
+ """Test the Quote tool"""
337
+ if not symbol or symbol.strip() == "":
338
+ return "❌ Please provide a stock symbol"
339
+
340
+ result = get_quote(symbol.strip())
341
+
342
+ if "error" in result:
343
+ return f"❌ Error: {result['error']}"
344
+
345
+ # Format for display
346
+ output = f"""📊 Real-time Quote for {result['symbol']}
347
+
348
+ Current Price: ${result.get('current_price', 'N/A')}
349
+ Change: ${result.get('change', 'N/A')}
350
+ Percent Change: {result.get('percent_change', 'N/A')}%
351
+ High: ${result.get('high', 'N/A')}
352
+ Low: ${result.get('low', 'N/A')}
353
+ Open: ${result.get('open', 'N/A')}
354
+ Previous Close: ${result.get('previous_close', 'N/A')}
355
+ Timestamp: {result.get('timestamp', 'N/A')}
356
+ """
357
+ return output.strip()
358
+
359
+
360
+ def test_market_news_tool(category: str) -> str:
361
+ """Test the Market News tool"""
362
+ result = get_market_news(category)
363
+
364
+ if "error" in result:
365
+ return f"❌ Error: {result['error']}"
366
+
367
+ if result.get('total_articles', 0) == 0:
368
+ return result.get('message', 'No news articles found')
369
+
370
+ # Format for display
371
+ output = f"📰 Latest Market News ({result['category']})\n"
372
+ output += f"Total Articles: {result['total_articles']}\n\n"
373
+
374
+ for idx, article in enumerate(result['articles'], 1):
375
+ output += f"{idx}. {article['headline']}\n"
376
+ output += f" Source: {article['source']}\n"
377
+ if article.get('summary'):
378
+ summary = article['summary'][:200] + "..." if len(article['summary']) > 200 else article['summary']
379
+ output += f" Summary: {summary}\n"
380
+ output += f" URL: {article.get('url', 'N/A')}\n"
381
+ output += f" Published: {article.get('published', 'N/A')}\n\n"
382
+
383
+ return output.strip()
384
+
385
+
386
+ def test_company_news_tool(symbol: str, from_date: str, to_date: str) -> str:
387
+ """Test the Company News tool"""
388
+ if not symbol or symbol.strip() == "":
389
+ return "❌ Please provide a stock symbol"
390
+
391
+ # Use None if dates are empty
392
+ from_d = from_date.strip() if from_date and from_date.strip() else None
393
+ to_d = to_date.strip() if to_date and to_date.strip() else None
394
+
395
+ result = get_company_news(symbol.strip(), from_d, to_d)
396
+
397
+ if "error" in result:
398
+ return f"❌ Error: {result['error']}"
399
+
400
+ if result.get('total_articles', 0) == 0:
401
+ # Show detailed message with suggestions
402
+ output = f"⚠️ {result.get('message', 'No news articles found')}\n\n"
403
+ if 'suggestion' in result:
404
+ output += f"💡 {result['suggestion']}\n\n"
405
+ if 'note' in result:
406
+ output += f"📝 {result['note']}"
407
+ return output
408
+
409
+ # Format for display
410
+ output = f"📰 Company News for {result['symbol']}\n"
411
+ output += f"Period: {result['from_date']} to {result['to_date']}\n"
412
+ output += f"Total Articles: {result['total_articles']}\n\n"
413
+
414
+ for idx, article in enumerate(result['articles'], 1):
415
+ output += f"{idx}. {article['headline']}\n"
416
+ output += f" Source: {article['source']}\n"
417
+ if article.get('summary'):
418
+ summary = article['summary'][:200] + "..." if len(article['summary']) > 200 else article['summary']
419
+ output += f" Summary: {summary}\n"
420
+ output += f" URL: {article.get('url', 'N/A')}\n"
421
+ output += f" Published: {article.get('published', 'N/A')}\n\n"
422
+
423
+ return output.strip()
424
+
425
+
426
+ # def check_server_health() -> str:
427
+ # """Check server health status and API connectivity"""
428
+ # status_parts = []
429
+
430
+ # # Check API key configuration
431
+ # api_key = get_api_key()
432
+ # if not api_key:
433
+ # status_parts.append("❌ API Key: Not configured")
434
+ # status_parts.append("\n⚠️ Please configure your Finnhub API key in the Configuration tab or set FINNHUB_API_KEY environment variable.")
435
+ # return "\n".join(status_parts)
436
+ # else:
437
+ # status_parts.append("✅ API Key: Configured")
438
+
439
+ # # Test Finnhub API connectivity
440
+ # status_parts.append("\n🔍 Testing Finnhub API connectivity...")
441
+ # try:
442
+ # test_result = make_finnhub_request("quote", {"symbol": "AAPL"})
443
+
444
+ # if "error" in test_result:
445
+ # status_parts.append(f"❌ Finnhub API: {test_result['error']}")
446
+ # status_parts.append("\n⚠️ Possible issues:")
447
+ # status_parts.append(" • Invalid API key")
448
+ # status_parts.append(" • Network connectivity problem")
449
+ # status_parts.append(" • Finnhub API service is down")
450
+ # status_parts.append(" • Rate limit exceeded")
451
+ # else:
452
+ # status_parts.append(f"✅ Finnhub API: Online and reachable")
453
+ # status_parts.append(f" Test query result: AAPL @ ${test_result.get('c', 'N/A')}")
454
+ # except Exception as e:
455
+ # status_parts.append(f"❌ Finnhub API: Connection failed")
456
+ # status_parts.append(f" Error: {str(e)}")
457
+
458
+ # return "\n".join(status_parts)
459
+
460
+
461
+ # # Load HTML content from file
462
+ # def load_html_content():
463
+ # """Load the HTML welcome page"""
464
+ # try:
465
+ # # 优先使用精简版
466
+ # if os.path.exists('index_simple.html'):
467
+ # with open('index_simple.html', 'r', encoding='utf-8') as f:
468
+ # return f.read()
469
+ # # 后备:Gradio 优化版
470
+ # elif os.path.exists('index_gradio.html'):
471
+ # with open('index_gradio.html', 'r', encoding='utf-8') as f:
472
+ # return f.read()
473
+ # # 后备:完整版
474
+ # elif os.path.exists('index.html'):
475
+ # with open('index.html', 'r', encoding='utf-8') as f:
476
+ # return f.read()
477
+ # else:
478
+ # return "<h1>📈 Finnhub MCP Server</h1><p>欢迎使用</p>"
479
+ # except Exception as e:
480
+ # print(f"Error loading HTML: {e}")
481
+ # return "<h1>📈 Finnhub MCP Server</h1><p>欢迎使用</p>"
482
+
483
+
484
+ # # Create Gradio Interface
485
+ # with gr.Blocks(title="Finnhub Market Info MCP Server") as demo:
486
+ # # Welcome tab with HTML content
487
+ # with gr.Tab("🏠 Home"):
488
+ # gr.HTML(load_html_content())
489
+
490
+ # # Health Check tab
491
+ # with gr.Tab("🩺 Health Check"):
492
+ # gr.Markdown("### Server Health Status")
493
+ # gr.Markdown("Check if the MCP server and Finnhub API are working properly.")
494
+
495
+ # health_check_btn = gr.Button("Check Health", variant="primary", size="lg")
496
+ # health_status_output = gr.Textbox(
497
+ # label="Health Status",
498
+ # lines=15,
499
+ # interactive=False,
500
+ # placeholder="Click 'Check Health' to test the server and API connectivity..."
501
+ # )
502
+
503
+ # health_check_btn.click(
504
+ # fn=check_server_health,
505
+ # inputs=[],
506
+ # outputs=[health_status_output]
507
+ # )
508
+
509
+ # gr.Markdown("""
510
+ # ### What this checks:
511
+
512
+ # 1. **API Key Configuration** - Verifies if Finnhub API key is set
513
+ # 2. **Finnhub API Connectivity** - Tests connection to Finnhub servers
514
+ # 3. **API Response** - Validates that the API returns data correctly
515
+
516
+ # ### Troubleshooting:
517
+
518
+ # If health check fails:
519
+ # - ❌ **API Key Not Configured**: Go to API Key Config tab and enter your key
520
+ # - ❌ **Invalid API Key**: Check if your key is correct at [finnhub.io](https://finnhub.io/dashboard)
521
+ # - ❌ **Network Error**: Check your internet connection
522
+ # - ❌ **Rate Limit**: Wait a minute before trying again (free tier: 60 calls/min)
523
+ # """)
524
+
525
+ # with gr.Tab("📊 Quote Tool"):
526
+ # gr.Markdown("### Test Real-time Stock Quote")
527
+ # gr.Markdown("Get real-time quote data for US stocks. Example symbols: AAPL, MSFT, TSLA, GOOGL")
528
+ # with gr.Row():
529
+ # quote_symbol = gr.Textbox(
530
+ # label="Stock Symbol",
531
+ # placeholder="AAPL",
532
+ # value="AAPL"
533
+ # )
534
+ # quote_btn = gr.Button("Get Quote", variant="primary")
535
+ # quote_output = gr.Textbox(label="Quote Data", lines=12, interactive=False)
536
+
537
+ # quote_btn.click(
538
+ # fn=test_quote_tool,
539
+ # inputs=[quote_symbol],
540
+ # outputs=[quote_output]
541
+ # )
542
+
543
+ # with gr.Tab("📰 Market News Tool"):
544
+ # gr.Markdown("### Test Market News")
545
+ # gr.Markdown("Get latest market news by category")
546
+ # with gr.Row():
547
+ # news_category = gr.Dropdown(
548
+ # choices=["general", "forex", "crypto", "merger"],
549
+ # label="News Category",
550
+ # value="general"
551
+ # )
552
+ # market_news_btn = gr.Button("Get Market News", variant="primary")
553
+ # market_news_output = gr.Textbox(label="Market News", lines=15, interactive=False)
554
+
555
+ # market_news_btn.click(
556
+ # fn=test_market_news_tool,
557
+ # inputs=[news_category],
558
+ # outputs=[market_news_output]
559
+ # )
560
+
561
+ # with gr.Tab("🏢 Company News Tool"):
562
+ # gr.Markdown("### Test Company News")
563
+ # gr.Markdown("Get latest company news by symbol. Only available for North American companies.")
564
+ # with gr.Row():
565
+ # company_symbol = gr.Textbox(
566
+ # label="Stock Symbol",
567
+ # placeholder="AAPL",
568
+ # value="AAPL"
569
+ # )
570
+ # with gr.Row():
571
+ # company_from_date = gr.Textbox(
572
+ # label="From Date (YYYY-MM-DD)",
573
+ # placeholder="Leave empty for 7 days ago",
574
+ # value=""
575
+ # )
576
+ # company_to_date = gr.Textbox(
577
+ # label="To Date (YYYY-MM-DD)",
578
+ # placeholder="Leave empty for today",
579
+ # value=""
580
+ # )
581
+ # company_news_btn = gr.Button("Get Company News", variant="primary")
582
+ # company_news_output = gr.Textbox(label="Company News", lines=15, interactive=False)
583
+
584
+ # company_news_btn.click(
585
+ # fn=test_company_news_tool,
586
+ # inputs=[company_symbol, company_from_date, company_to_date],
587
+ # outputs=[company_news_output]
588
+ # )
589
+
590
+ # with gr.Tab("ℹ️ MCP Connection"):
591
+ # gr.Markdown("""
592
+ # ### Connect via MCP Client
593
+
594
+ # This server implements the **standard MCP protocol (JSON-RPC over SSE)**.
595
+
596
+ # #### 📥 Step 1: Clone Repository
597
+
598
+ # ```bash
599
+ # git clone https://huggingface.co/spaces/JC321/MarketandStockMCP
600
+ # cd MarketandStockMCP
601
+ # pip install -r requirements.txt
602
+ # ```
603
+
604
+ # #### 🔑 Step 2: Get API Key
605
+
606
+ # Get your free Finnhub API key at [finnhub.io/register](https://finnhub.io/register)
607
+
608
+ # #### ⚙️ Step 3: Configure MCP Client
609
+
610
+ # **For Claude Desktop:**
611
+
612
+ # Edit config file:
613
+ # - macOS: `~/Library/Application Support/Claude/claude_desktop_config.json`
614
+ # - Windows: `%APPDATA%\Claude\claude_desktop_config.json`
615
+
616
+ # Add this configuration:
617
+
618
+ # ```json
619
+ # {
620
+ # "mcpServers": {
621
+ # "finnhub": {
622
+ # "command": "python",
623
+ # "args": ["C:\\path\\to\\MarketandStockMCP\\mcp_server.py"],
624
+ # "env": {
625
+ # "FINNHUB_API_KEY": "your_finnhub_api_key_here"
626
+ # }
627
+ # }
628
+ # }
629
+ # }
630
+ # ```
631
+
632
+ # **For Cursor IDE:**
633
+
634
+ # Use the same JSON configuration in MCP settings.
635
+
636
+ # #### 🚀 Step 4: Test
637
+
638
+ # Restart your MCP client and try:
639
+ # - "What's the current price of Apple stock?"
640
+ # - "Show me the latest market news"
641
+ # - "Get Tesla company news from last week"
642
+
643
+ # #### 🛠️ Available Tools:
644
+
645
+ # - `get_quote(symbol: str)` - Real-time stock quote
646
+ # - `get_market_news(category: str, min_id: int)` - Market news by category
647
+ # - `get_company_news(symbol: str, from_date: str, to_date: str)` - Company-specific news
648
+
649
+ # #### 📖 Documentation:
650
+
651
+ # See [README.md](https://huggingface.co/spaces/JC321/MarketandStockMCP/blob/main/README.md) for detailed instructions.
652
+ # """)
653
+
654
+ # # API Key Config tab (moved to last)
655
+ # with gr.Tab("🔑 API Key Config"):
656
+ # gr.Markdown("### Configure Finnhub API Key for Web Testing")
657
+ # gr.Markdown("""
658
+ # 💡 **Note:** This is only for testing tools in the web UI above.
659
+
660
+ # For MCP client use, configure the API key in your MCP client settings (see "MCP Connection" tab).
661
+ # """)
662
+ # with gr.Row():
663
+ # api_key_input = gr.Textbox(
664
+ # label="Finnhub API Key",
665
+ # placeholder="Enter your Finnhub API key here...",
666
+ # type="password"
667
+ # )
668
+ # config_btn = gr.Button("Configure", variant="primary")
669
+ # config_output = gr.Textbox(label="Status", interactive=False)
670
+
671
+ # config_btn.click(
672
+ # fn=configure_api_key,
673
+ # inputs=[api_key_input],
674
+ # outputs=[config_output]
675
+ # )
676
+
677
+
678
+ # if __name__ == "__main__":
679
+ # import sys
680
+
681
+ # # Check for API key in environment variable
682
+ # env_api_key = os.getenv("FINNHUB_API_KEY")
683
+ # if env_api_key:
684
+ # set_api_key(env_api_key)
685
+ # print("✅ API Key loaded from environment variable")
686
+
687
+ # # Set port from environment (HF Space sets PORT=7860)
688
+ # port = int(os.getenv("PORT", "7870"))
689
+ # host = os.getenv("HOST", "0.0.0.0")
690
+
691
+ # # Check if running as MCP server or Gradio UI
692
+ # if "--mcp" in sys.argv or os.getenv("RUN_MCP_SERVER") == "true":
693
+ # # Run as MCP server (standalone mode)
694
+ # print("▶️ Starting MCP Server (standalone mode)...")
695
+ # mcp.run(transport="sse")
696
+ # else:
697
+ # # Launch Gradio interface
698
+ # print("▶️ Starting Gradio Interface...")
699
+ # print("🌐 Gradio UI available at: /")
700
+ # print("📡 MCP tools available through Gradio tabs")
701
+
702
+ # demo.launch(
703
+ # server_name=host,
704
+ # server_port=port,
705
+ # share=False
706
+ # )
service/news_quote_mcp_server.py ADDED
@@ -0,0 +1,321 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ #!/usr/bin/env python3
2
+ """
3
+ Finnhub MCP Server - Standard MCP Protocol (JSON-RPC over SSE)
4
+
5
+ This is a standalone MCP server that implements the standard Model Context Protocol.
6
+ It provides three tools for accessing Finnhub financial data:
7
+ - get_quote: Real-time stock quotes
8
+ - get_market_news: Latest market news by category
9
+ - get_company_news: Company-specific news
10
+
11
+ Usage:
12
+ python mcp_server.py
13
+
14
+ Environment Variables:
15
+ FINNHUB_API_KEY: Your Finnhub API key (required)
16
+ """
17
+
18
+ import os
19
+ import requests
20
+ from typing import Optional, Dict, Any
21
+ from datetime import datetime, timedelta
22
+ from mcp.server.fastmcp import FastMCP
23
+
24
+ # Initialize FastMCP server with standard MCP protocol
25
+ mcp = FastMCP("finnhub-market-info")
26
+
27
+
28
+ def get_api_key() -> Optional[str]:
29
+ """Get the Finnhub API key from environment"""
30
+ return os.getenv("FINNHUB_API_KEY")
31
+
32
+
33
+ def make_finnhub_request(endpoint: str, params: Dict[str, Any]) -> Dict[str, Any]:
34
+ """
35
+ Make a request to Finnhub API
36
+
37
+ Args:
38
+ endpoint: API endpoint path
39
+ params: Query parameters
40
+
41
+ Returns:
42
+ API response as dictionary
43
+ """
44
+ api_key = get_api_key()
45
+ if not api_key:
46
+ return {"error": "API key not configured. Please set FINNHUB_API_KEY environment variable."}
47
+
48
+ params["token"] = api_key
49
+ base_url = "https://finnhub.io/api/v1"
50
+ url = f"{base_url}/{endpoint}"
51
+
52
+ try:
53
+ response = requests.get(url, params=params, timeout=10)
54
+ response.raise_for_status()
55
+ return response.json()
56
+ except requests.exceptions.RequestException as e:
57
+ return {"error": f"API request failed: {str(e)}"}
58
+
59
+
60
+ @mcp.tool()
61
+ def get_quote(symbol: str) -> dict:
62
+ """
63
+ Get real-time quote data for US stocks. Use this tool when you need current stock price
64
+ information and market performance metrics for any US-listed stock.
65
+
66
+ When to use:
67
+ - User asks "What's the current price of [stock]?"
68
+ - Need real-time stock quote data
69
+ - User mentions "stock price", "current value", "how is [stock] trading?"
70
+ - Want to check latest market price and daily changes
71
+
72
+ Examples:
73
+ - "What's Apple's stock price?" → get_quote(symbol="AAPL")
74
+ - "How is Tesla trading today?" → get_quote(symbol="TSLA")
75
+ - "Show me Microsoft's current quote" → get_quote(symbol="MSFT")
76
+
77
+ Args:
78
+ symbol: Stock ticker symbol (e.g., 'AAPL', 'MSFT', 'TSLA', 'GOOGL')
79
+
80
+ Returns:
81
+ dict: Real-time quote data containing:
82
+ - symbol: Stock ticker symbol
83
+ - current_price: Current trading price (c)
84
+ - change: Price change in dollars (d)
85
+ - percent_change: Price change in percentage (dp)
86
+ - high: Today's high price (h)
87
+ - low: Today's low price (l)
88
+ - open: Opening price (o)
89
+ - previous_close: Previous trading day's closing price (pc)
90
+ - timestamp: Quote timestamp
91
+ """
92
+ result = make_finnhub_request("quote", {"symbol": symbol.upper()})
93
+
94
+ if "error" in result:
95
+ return {
96
+ "error": result["error"],
97
+ "symbol": symbol.upper()
98
+ }
99
+
100
+ # Return structured data
101
+ return {
102
+ "symbol": symbol.upper(),
103
+ "current_price": result.get('c'),
104
+ "change": result.get('d'),
105
+ "percent_change": result.get('dp'),
106
+ "high": result.get('h'),
107
+ "low": result.get('l'),
108
+ "open": result.get('o'),
109
+ "previous_close": result.get('pc'),
110
+ "timestamp": datetime.fromtimestamp(result.get('t', 0)).strftime('%Y-%m-%d %H:%M:%S') if result.get('t') else None
111
+ }
112
+
113
+
114
+ @mcp.tool()
115
+ def get_market_news(category: str = "general", min_id: int = 0) -> dict:
116
+ """
117
+ Get latest market news across different categories. Use this tool when you need current market
118
+ news, trends, and developments in general markets, forex, cryptocurrency, or mergers.
119
+
120
+ When to use:
121
+ - User asks "What's the latest market news?"
122
+ - Need current financial news and market updates
123
+ - User mentions "news", "market trends", "what's happening in the market?"
124
+ - Want to get news for specific categories (forex, crypto, M&A)
125
+
126
+ Categories explained:
127
+ - general: General market news, stocks, economy, major companies
128
+ - forex: Foreign exchange and currency market news
129
+ - crypto: Cryptocurrency and blockchain news
130
+ - merger: Mergers & acquisitions, corporate deals
131
+
132
+ Examples:
133
+ - "What's the latest market news?" → get_market_news(category="general")
134
+ - "Show me crypto news" → get_market_news(category="crypto")
135
+ - "Any forex updates?" → get_market_news(category="forex")
136
+ - "Recent merger news" → get_market_news(category="merger")
137
+
138
+ Args:
139
+ category: News category - "general", "forex", "crypto", or "merger" (default: "general")
140
+ min_id: Minimum news ID for pagination (default: 0, use 0 to get latest news)
141
+
142
+ Returns:
143
+ dict: Market news data containing:
144
+ - category: News category requested
145
+ - total_articles: Total number of articles returned
146
+ - articles: List of news articles (max 10), each with:
147
+ * id: Article ID
148
+ * headline: News headline
149
+ * summary: Brief summary of the article
150
+ * source: News source
151
+ * url: Link to full article
152
+ * published: Publication timestamp
153
+ * image: Article image URL (if available)
154
+ """
155
+ params = {"category": category}
156
+ if min_id > 0:
157
+ params["minId"] = str(min_id)
158
+
159
+ result = make_finnhub_request("news", params)
160
+
161
+ if isinstance(result, dict) and "error" in result:
162
+ return {
163
+ "error": result["error"],
164
+ "category": category
165
+ }
166
+
167
+ if not result or len(result) == 0:
168
+ return {
169
+ "category": category,
170
+ "total_articles": 0,
171
+ "articles": [],
172
+ "message": "No news articles found for this category"
173
+ }
174
+
175
+ # Format the news articles
176
+ articles = []
177
+ for article in result[:10]: # Limit to 10 articles # pyright: ignore[reportArgumentType]
178
+ articles.append({
179
+ "id": article.get('id'),
180
+ "headline": article.get('headline', 'No headline'),
181
+ "summary": article.get('summary', ''),
182
+ "source": article.get('source', 'Unknown'),
183
+ "url": article.get('url'),
184
+ "published": datetime.fromtimestamp(article.get('datetime', 0)).strftime('%Y-%m-%d %H:%M:%S') if article.get('datetime') else None,
185
+ "image": article.get('image')
186
+ })
187
+
188
+ return {
189
+ "category": category,
190
+ "total_articles": len(articles),
191
+ "articles": articles
192
+ }
193
+
194
+
195
+ @mcp.tool()
196
+ def get_company_news(symbol: str, from_date: Optional[str] = None, to_date: Optional[str] = None) -> dict:
197
+ """
198
+ Get latest news for a specific company by stock symbol. This endpoint provides company-specific
199
+ news, press releases, and announcements. Only available for North American companies.
200
+
201
+ When to use:
202
+ - User asks about a specific company's news (e.g., "Apple news", "Tesla updates")
203
+ - Need company-specific announcements or press releases
204
+ - User mentions "[company name] news", "recent [company] developments"
205
+ - Want to filter news by date range
206
+
207
+ Date range tips:
208
+ - Default: Last 7 days if no dates specified
209
+ - Can go back up to several years
210
+ - Use YYYY-MM-DD format (e.g., "2024-01-01")
211
+
212
+ Examples:
213
+ - "What's the latest Apple news?" → get_company_news(symbol="AAPL")
214
+ - "Tesla news from last month" → get_company_news(symbol="TSLA", from_date="2024-10-01", to_date="2024-10-31")
215
+ - "Microsoft announcements this week" → get_company_news(symbol="MSFT")
216
+ - "Show me Amazon news from January 2024" → get_company_news(symbol="AMZN", from_date="2024-01-01", to_date="2024-01-31")
217
+
218
+ Args:
219
+ symbol: Company stock ticker symbol (e.g., 'AAPL', 'MSFT', 'TSLA', 'GOOGL')
220
+ Must be a North American (US/Canada) listed company
221
+ from_date: Start date in YYYY-MM-DD format (default: 7 days ago)
222
+ to_date: End date in YYYY-MM-DD format (default: today)
223
+
224
+ Returns:
225
+ dict: Company news data containing:
226
+ - symbol: Company stock symbol
227
+ - from_date: Start date of news range
228
+ - to_date: End date of news range
229
+ - total_articles: Total number of articles returned
230
+ - articles: List of news articles (max 10), each with:
231
+ * headline: News headline
232
+ * summary: Brief summary
233
+ * source: News source
234
+ * url: Link to full article
235
+ * published: Publication timestamp
236
+ * related: Related stock symbols (if any)
237
+ """
238
+ # Set default date range if not provided
239
+ if not to_date:
240
+ to_date = datetime.now().strftime('%Y-%m-%d')
241
+
242
+ if not from_date:
243
+ from_date = (datetime.now() - timedelta(days=7)).strftime('%Y-%m-%d')
244
+
245
+ params = {
246
+ "symbol": symbol.upper(),
247
+ "from": from_date,
248
+ "to": to_date
249
+ }
250
+
251
+ result = make_finnhub_request("company-news", params)
252
+
253
+ if isinstance(result, dict) and "error" in result:
254
+ return {
255
+ "error": result["error"],
256
+ "symbol": symbol.upper(),
257
+ "from_date": from_date,
258
+ "to_date": to_date
259
+ }
260
+
261
+ if not result or len(result) == 0:
262
+ # Provide helpful suggestions based on the symbol
263
+ suggestion = "Try expanding the date range or check if the symbol is correct."
264
+ if symbol.upper() in ["BABA", "BIDU", "JD", "PDD", "NIO"]:
265
+ suggestion = "Note: Chinese ADRs may have limited news coverage. Try US companies like AAPL, MSFT, TSLA, or GOOGL for better results."
266
+
267
+ return {
268
+ "symbol": symbol.upper(),
269
+ "from_date": from_date,
270
+ "to_date": to_date,
271
+ "total_articles": 0,
272
+ "articles": [],
273
+ "message": f"No news found for {symbol.upper()} between {from_date} and {to_date}.",
274
+ "suggestion": suggestion,
275
+ "note": "Company news is only available for North American companies. Some companies may have limited news coverage during certain periods."
276
+ }
277
+
278
+ # Format the news articles
279
+ articles = []
280
+ for article in result[:10]: # Limit to 10 articles # pyright: ignore[reportArgumentType]
281
+ articles.append({
282
+ "headline": article.get('headline', 'No headline'),
283
+ "summary": article.get('summary', ''),
284
+ "source": article.get('source', 'Unknown'),
285
+ "url": article.get('url'),
286
+ "published": datetime.fromtimestamp(article.get('datetime', 0)).strftime('%Y-%m-%d %H:%M:%S') if article.get('datetime') else None,
287
+ "related": article.get('related', '')
288
+ })
289
+
290
+ return {
291
+ "symbol": symbol.upper(),
292
+ "from_date": from_date,
293
+ "to_date": to_date,
294
+ "total_articles": len(articles),
295
+ "articles": articles
296
+ }
297
+
298
+
299
+ if __name__ == "__main__":
300
+ # Check for API key
301
+ api_key = get_api_key()
302
+ if not api_key:
303
+ print("❌ Error: FINNHUB_API_KEY environment variable is not set")
304
+ print("Please set your Finnhub API key:")
305
+ print(" export FINNHUB_API_KEY='your_api_key_here'")
306
+ exit(1)
307
+
308
+ print("✅ API Key loaded from environment variable")
309
+ print("▶️ Starting Finnhub MCP Server (JSON-RPC over SSE)...")
310
+ print("📡 Server name: finnhub-market-info")
311
+ print("🔧 Protocol: Model Context Protocol (MCP)")
312
+ print("🌐 Transport: SSE (Server-Sent Events)")
313
+ print("")
314
+ print("Available tools:")
315
+ print(" • get_quote(symbol)")
316
+ print(" • get_market_news(category, min_id)")
317
+ print(" • get_company_news(symbol, from_date, to_date)")
318
+ print("")
319
+
320
+ # Run the MCP server with SSE transport
321
+ mcp.run(transport="sse")