import streamlit as st import pandas as pd import folium from streamlit_folium import st_folium import os import requests import io from openai import OpenAI import json # --- 0. 配置与初始化 --- st.set_page_config( page_title="李白生平GIS与RAG整合", page_icon="🐉", layout="wide" ) # 初始化OpenAI客户端 client = OpenAI( api_key=os.getenv("DEEPSEEK_API_KEY", "sk-72997944466a4af2bcd52a068895f8cf"), base_url="https://api.deepseek.com" ) # ---------------------------------------------------- # 全局变量定义 # ---------------------------------------------------- GITHUB_XLSX_URL = "https://raw.githubusercontent.com/seblee424/5571_data/main/李白人生重要节点与代表作地理位置.xlsx" location_col = '地点(古称/今称)' summary_col = '诗作/事件摘要' # --- 1. RAG 补充函数:抓取CBDB李白人物资料 --- @st.cache_data(ttl=3600) def get_cbdb_data(name="李白"): url = f"https://cbdb.fas.harvard.edu/cbdbapi/person.php?name={name}&o=json" headers = {"User-Agent": "Mozilla/5.0 (Streamlit App)"} try: response = requests.get(url, headers=headers, timeout=10) if response.status_code == 200: return response.json() else: return None except Exception: return None # --- 2. 地理坐标匹配表 --- LOCATION_COORDS = { "碎叶城": {"lat": 42.8447, "lon": 75.1648, "match_keys": ["碎叶城"]}, "峨眉山": {"lat": 29.5807, "lon": 103.3592, "match_keys": ["峨眉山"]}, "蜀中": {"lat": 31.7828, "lon": 104.7570, "match_keys": ["蜀中", "江油"]}, "荆门/南津关": {"lat": 30.5667, "lon": 111.4500, "match_keys": ["荆门", "南津关"]}, "岳阳楼": {"lat": 29.3879, "lon": 113.1092, "match_keys": ["岳阳楼", "岳阳"]}, "安陆": {"lat": 31.3653, "lon": 113.7077, "match_keys": ["安陆"]}, "黄鹤楼": {"lat": 30.5484, "lon": 114.3168, "match_keys": ["黄鹤楼", "武汉"]}, "金陵(凤凰台)": {"lat": 32.0415, "lon": 118.7781, "match_keys": ["金陵", "凤凰台", "南京"]}, "庐山": {"lat": 29.5910, "lon": 115.9922, "match_keys": ["庐山", "九江"]}, "天姥山": {"lat": 29.5000, "lon": 120.8900, "match_keys": ["天姥山"]}, "金陵/长干里": {"lat": 32.0298, "lon": 118.7900, "match_keys": ["长干里"]}, "长安": {"lat": 34.2652, "lon": 108.9500, "match_keys": ["长安", "西安"]}, "长安/宫廷": {"lat": 34.2652, "lon": 108.9500, "match_keys": ["宫廷"]}, "长安/洛阳": {"lat": 34.6859, "lon": 112.4600, "match_keys": ["洛阳"]}, "桃花潭": {"lat": 30.4079, "lon": 118.4230, "match_keys": ["桃花潭", "泾县"]}, "敬亭山": {"lat": 30.9822, "lon": 118.7844, "match_keys": ["敬亭山", "宣城"]}, "天门山": {"lat": 31.4285, "lon": 118.3970, "match_keys": ["天门山", "芜湖"]}, "扬州/旅店": {"lat": 32.3934, "lon": 119.4290, "match_keys": ["扬州"]}, "夜郎": {"lat": 27.6888, "lon": 106.3773, "match_keys": ["夜郎", "桐梓"]}, "白帝城": {"lat": 31.0450, "lon": 109.5780, "match_keys": ["白帝城", "奉节"]}, "秋浦": {"lat": 30.6500, "lon": 117.4800, "match_keys": ["秋浦", "池州"]}, "当涂": {"lat": 31.5453, "lon": 118.4870, "match_keys": ["当涂", "马鞍山"]}, # 泛指类 "蜀道": {"lat": 31.0000, "lon": 107.0000, "match_keys": ["蜀道"]}, "月下独酌": {"lat": 34.2652, "lon": 108.9500, "match_keys": ["独酌", "月下"]}, "静夜思": {"lat": 32.3934, "lon": 119.4290, "match_keys": ["静夜思"]}, "长江沿线": {"lat": 30.5928, "lon": 114.3055, "match_keys": ["长江"]}, "战城南": {"lat": 35.0000, "lon": 100.0000, "match_keys": ["边塞", "战争"]}, "送友人": {"lat": 30.5928, "lon": 114.3055, "match_keys": ["送友人"]}, "将进酒": {"lat": 34.2652, "lon": 108.9500, "match_keys": ["将进酒", "豪饮"]}, "行路难": {"lat": 34.2652, "lon": 108.9500, "match_keys": ["行路难"]}, } # --- 3. GitHub 在线加载 XLSX --- @st.cache_data def load_and_prepare_data_github(url): try: st.info("正在从 GitHub 加载 XLSX 数据...") res = requests.get(url, timeout=10) if res.status_code != 200: st.error(f"GitHub 下载失败,状态码: {res.status_code}") return pd.DataFrame() df = pd.read_excel(io.BytesIO(res.content), sheet_name=0) st.success("✅ 成功从 GitHub 加载数据!") except Exception as e: st.error(f"❌ 加载 GitHub XLSX 失败: {e}") return pd.DataFrame() df.columns = df.columns.str.strip() if location_col not in df.columns or summary_col not in df.columns: st.error(f"XLSX 缺少关键列: {location_col}, {summary_col}") return pd.DataFrame() # 经纬度匹配 coords_list = [] df["coords_key"] = "" for idx, row in df.iterrows(): loc = str(row[location_col]).strip() match = None match_key = "未知" for key, data in LOCATION_COORDS.items(): if loc == key: match = data match_key = key break if any(k in loc for k in data["match_keys"]): match = data match_key = key break if match: coords_list.append((match["lat"], match["lon"])) df.loc[idx, "coords_key"] = match_key else: coords_list.append((34.0478, 108.4357)) df["Latitude"] = [x[0] for x in coords_list] df["Longitude"] = [x[1] for x in coords_list] return df # --- 使用 GitHub 数据加载 --- data_df = load_and_prepare_data_github(GITHUB_XLSX_URL) # --- 4. Chatbot (RAG) --- def run_chatbot(cbdb_data, prompt): cbdb_text = json.dumps(cbdb_data, ensure_ascii=False)[:5000] if cbdb_data else "无CBDB资料。" system_prompt_rag = ( "你是一个李白生平研究的聊天机器人,擅长介绍李白的生平、作品和相关地点。" "回答中如提及地点,请使用与地图匹配的地名,如安陆、桃花潭、黄鹤楼、长安。" "引用 CBDB 信息时请在结尾标注“资料源自CBDB”。" f"\n\nCBDB数据如下:{cbdb_text}" ) try: msgs = [{"role": "system", "content": system_prompt_rag}] msgs.extend(st.session_state.chat_history[-5:]) resp = client.chat.completions.create( model="deepseek-chat", messages=msgs, stream=False ) answer = resp.choices[0].message.content.strip() # 自动匹配回答的地名,对地图高亮 highlight = None for key in data_df["coords_key"].unique(): if key != "未知" and key in answer: highlight = key break st.session_state.highlight_location_key = highlight return answer except Exception as e: return f"Chatbot 发生错误: {e}" # --- 5. GIS 地图 --- def create_li_bai_map(df, highlight_key): if df.empty: return folium.Map(location=[34.0, 108.0], zoom_start=4) center_lat = df["Latitude"].mean() center_lon = df["Longitude"].mean() m = folium.Map( location=[center_lat, center_lon], zoom_start=4.5, tiles="cartodbdarkmatter" ) # 轨迹线 points = df[["Latitude", "Longitude"]].values.tolist() if len(points) > 1: folium.PolyLine(points, color="#00AEEF", weight=3, opacity=0.5).add_to(m) # 节点 for _, row in df.iterrows(): is_high = (row["coords_key"] == highlight_key) popup_html = f""" 序号: {row['序号']}
阶段: {row['阶段(大致年份)']}
地点: {row['地点(古称/今称)']}
事件/诗作: {row['诗作/事件摘要']}
核心情感: {row['核心情感/主题']}
节点类型: {row['节点类型']} """ if is_high: color, icon = "orange", "fire" elif "人生事件" in row["节点类型"]: color, icon = "blue", "user" else: color, icon = "green", "flag" folium.Marker( [row["Latitude"], row["Longitude"]], popup=folium.Popup(popup_html, max_width=300), icon=folium.Icon(color=color, icon=icon, prefix="fa", icon_color="white"), ).add_to(m) return m # --- 6. 主界面 --- cbdb_data = get_cbdb_data("李白") st.header("🐉 李白生平 GIS + RAG 交互系统") if "chat_history" not in st.session_state: st.session_state.chat_history = [] if "highlight_location_key" not in st.session_state: st.session_state.highlight_location_key = None if data_df.empty: st.error("❌ 数据加载失败,请检查 GitHub 文件") else: col1, col2 = st.columns([1, 1.5]) # 左侧 Chatbot with col1: st.subheader("💬 李白 Chatbot") if cbdb_data: st.info("✅ 已加载 CBDB 资料") else: st.warning("⚠️ CBDB 资料加载失败") for msg in st.session_state.chat_history: with st.chat_message(msg["role"]): st.markdown(msg["content"]) if prompt := st.chat_input("请输入你的问题…"): with st.chat_message("user"): st.markdown(prompt) st.session_state.chat_history.append({"role": "user", "content": prompt}) with st.chat_message("assistant"): with st.spinner("AI 正在思考…"): answer = run_chatbot(cbdb_data, prompt) st.write(answer) if st.session_state.highlight_location_key: st.success(f"地图已高亮:{st.session_state.highlight_location_key}") st.session_state.chat_history.append({"role": "assistant", "content": answer}) st.rerun() # 右侧 GIS with col2: st.subheader("🗺️ 李白一生足迹地图") st.info("提示:左侧 Chatbot 会触发地图节点高亮") current_map = create_li_bai_map(data_df, st.session_state.highlight_location_key) st_folium(current_map, width=800, height=700)