集成机器人
Browse files- app.py +192 -264
- chatbot/chat_main.py +1003 -79
- requirements.txt +4 -1
- service/news_quote_mcp.py +706 -0
- service/news_quote_mcp_server.py +321 -0
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 |
-
|
|
|
|
|
|
|
| 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 |
-
|
| 501 |
-
|
| 502 |
-
except Exception as e:
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
|
|
|
|
|
|
|
|
|
| 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['
|
| 512 |
<div class="report-item-content">
|
| 513 |
-
<span class="text-gray-800">{report['
|
| 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 |
-
|
| 526 |
-
|
| 527 |
-
#
|
| 528 |
-
#
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
| 532 |
-
|
| 533 |
-
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
| 537 |
-
|
| 538 |
-
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
|
| 542 |
-
|
| 543 |
-
|
| 544 |
-
|
| 545 |
-
|
| 546 |
-
|
| 547 |
-
|
| 548 |
-
|
| 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 |
-
|
| 557 |
-
''
|
| 558 |
-
|
| 559 |
-
|
| 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 |
-
|
| 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 |
-
<
|
| 1051 |
-
<
|
| 1052 |
-
|
| 1053 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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(
|
| 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("
|
| 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 |
-
|
| 1287 |
-
try:
|
| 1288 |
-
|
| 1289 |
-
except (ValueError, TypeError):
|
| 1290 |
-
|
| 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("
|
| 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 |
-
<
|
| 1383 |
-
<
|
| 1384 |
-
|
| 1385 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 1916 |
-
|
| 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 |
-
|
| 1929 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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 |
-
#
|
|
|
|
| 365 |
if "result" in response and "tools" in response["result"]:
|
| 366 |
-
|
| 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
|
| 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 |
-
{
|
| 406 |
-
|
|
|
|
|
|
|
| 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.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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.
|
| 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 |
-
|
| 543 |
-
|
| 544 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
| 941 |
-
|
| 942 |
-
|
| 943 |
-
|
| 944 |
-
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
|
| 950 |
-
|
| 951 |
-
|
| 952 |
-
|
| 953 |
-
|
| 954 |
-
|
| 955 |
-
|
| 956 |
-
|
| 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 |
-
|
| 994 |
-
|
| 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
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1127 |
# Plan generation failed - show error and stop
|
| 1128 |
-
output_messages.append(f"❌ Unable to process your request
|
| 1129 |
output_messages.append("")
|
| 1130 |
-
output_messages.append("💡
|
| 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 -
|
| 1172 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1173 |
try:
|
| 1174 |
client = InferenceClient(
|
| 1175 |
model="Qwen/Qwen2.5-72B-Instruct",
|
| 1176 |
-
token=hf_token
|
| 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
|
| 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
|
| 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
|
| 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
|
| 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
|
| 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
|
| 1820 |
-
6. If
|
| 1821 |
-
7.
|
| 1822 |
-
8.
|
| 1823 |
-
9.
|
|
|
|
|
|
|
|
|
|
| 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
|
| 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
|
| 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")
|