Files
hack/tools/scripts/llm-key-hunter/results/_cache/content/alexlmoney83-oss_travel-planning-agent.json
T

1 line
44 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "alexlmoney83-oss/travel-planning-agent", "files": {"app.py": {"d9b77eb45518bf5205b9637843011a282f27922b": {"t": 1785745406.1023722, "n": 37870, "h": "4f6c57def1f344d6", "c": "\"\"\"\n智能旅游规划助手 - Streamlit UI\n基于LangChain Agent,无递归限制\n\"\"\"\nimport streamlit as st\nimport tempfile\nimport os\nimport asyncio\nimport warnings\nfrom typing import List, Dict, Any\n\n# suppress warnings\nwarnings.filterwarnings('ignore', category=DeprecationWarning)\nwarnings.filterwarnings('ignore', message='.*LangChainDeprecationWarning.*')\nfrom langchain.memory import ConversationBufferMemory\nfrom langchain_community.chat_message_histories import StreamlitChatMessageHistory\nfrom langchain_community.document_loaders import (\n TextLoader,\n PyPDFLoader,\n CSVLoader,\n DirectoryLoader\n)\nfrom langchain_openai import ChatOpenAI\nfrom langchain_chroma import Chroma\nfrom langchain_core.prompts import PromptTemplate\nfrom langchain_text_splitters import (\n RecursiveCharacterTextSplitter,\n CharacterTextSplitter\n)\nfrom langchain.agents import create_react_agent, AgentExecutor\nfrom langchain_community.callbacks.streamlit import StreamlitCallbackHandler\nfrom langchain_community.embeddings import DashScopeEmbeddings\nfrom langchain.tools import Tool\nfrom dotenv import load_dotenv\n\n# 设置页面配置(必须是第一个Streamlit命令)\nst.set_page_config(\n page_title=\"🗺️ 智能旅游规划助手\",\n page_icon=\"🗺️\",\n layout=\"wide\"\n)\n\n# 加载环境变量(指定.env文件路径)\nenv_path = os.path.join(os.path.dirname(__file__), 'aggentic_RAG', '.env')\nload_dotenv(dotenv_path=env_path, override=True)\n\n# 添加项目路径到sys.path\nimport sys\nsys.path.insert(0, os.path.join(os.path.dirname(__file__), 'aggentic_RAG'))\n\n# 应用标题\nst.title(\"🗺️ 智能旅游规划助手\")\nst.markdown(\"基于RAG + Agentic AI的旅游规划系统,无递归限制\")\n\n# 侧边栏 - 文档上传\nwith st.sidebar:\n st.header(\"📚 旅游攻略文档\")\n \n # 上传多种格式的文件\n uploaded_files = st.file_uploader(\n label=\"上传旅游攻略文档\",\n type=[\"txt\", \"pdf\", \"csv\"],\n accept_multiple_files=True,\n help=\"支持 TXT、PDF、CSV 格式的文档\"\n )\n \n st.markdown(\"---\")\n \n # 系统配置\n st.header(\"⚙️ 系统配置\")\n max_iterations = st.slider(\n \"最大迭代次数\",\n min_value=10,\n max_value=100,\n value=50,\n help=\"Agent执行的最大步骤数,无递归限制!\"\n )\n \n st.markdown(\"---\")\n \n # 清空对话按钮\n if st.button(\"🗑️ 清空聊天记录\", use_container_width=True):\n st.session_state.messages = []\n st.rerun()\n\n# 文档上传现在是可选的\nif not uploaded_files:\n st.info(\"💡 提示:您可以选择上传旅游攻略文档,或直接进行实时查询\")\n # 不再强制要求上传文档\n\n\n# 配置RAG检索器\n@st.cache_resource(ttl=\"1h\")\ndef configure_rag_retriever(uploaded_files):\n \"\"\"配置RAG检索器 - 支持多种文件格式\"\"\"\n docs = []\n \n # 创建临时目录存储上传的文件\n temp_dir = tempfile.TemporaryDirectory()\n \n for file in uploaded_files:\n temp_filepath = os.path.join(temp_dir.name, file.name)\n with open(temp_filepath, \"wb\") as f:\n f.write(file.getvalue())\n \n # 根据文件类型选择合适的 loader\n file_extension = os.path.splitext(file.name)[1].lower()\n \n try:\n if file_extension == \".txt\":\n loader = TextLoader(temp_filepath, encoding=\"utf-8\")\n elif file_extension == \".pdf\":\n loader = PyPDFLoader(temp_filepath)\n elif file_extension == \".csv\":\n loader = CSVLoader(temp_filepath, encoding=\"utf-8\")\n else:\n st.warning(f\"跳过不支持的文件格式: {file.name}\")\n continue\n \n docs.extend(loader.load())\n st.success(f\"✅ 已加载: {file.name}\")\n except Exception as e:\n st.error(f\"加载 {file.name} 失败: {e}\")\n \n # 文档分割\n text_splitter = RecursiveCharacterTextSplitter(\n chunk_size=500,\n chunk_overlap=100\n )\n splits = text_splitter.split_documents(docs)\n \n # 使用Qwen的Embedding模型\n embeddings = DashScopeEmbeddings(\n model=\"text-embedding-v3\",\n dashscope_api_key=os.getenv(\"DASHSCOPE_API_KEY\")\n )\n \n # 创建向量数据库\n vectordb = Chroma.from_documents(splits, embeddings)\n \n # 返回检索器\n return vectordb.as_retriever(search_kwargs={\"k\": 5})\n\n\n# 配置检索器(如果有文档)\nif uploaded_files:\n with st.spinner(\"正在加载文档并建立索引...\"):\n retriever = configure_rag_retriever(uploaded_files)\n st.success(f\"✅ 已加载 {len(uploaded_files)} 个文档\")\nelse:\n retriever = None\n\n\n# 启用嵌套asyncio支持\nimport nest_asyncio\nnest_asyncio.apply()\n\n# 创建全局event loop用于MCP调用\nimport asyncio\n_mcp_loop = asyncio.new_event_loop()\n_mcp_manager = None\n\n\n# ========== 预分析层:照搬旧版本 LangGraph 的分流逻辑 ==========\n\ndef detect_multi_destination(user_query: str, extraction: dict) -> dict:\n \"\"\"检测是否为多目的地场景(排除往返/回程误判)\n \n 照搬自 nodes.py 中的 detect_multi_destination 函数\n \"\"\"\n # === 1) 优先排除往返场景 ===\n roundtrip_keywords = [\"往返\", \"来回\", \"回程\", \"返程\", \"返回\"]\n if any(kw in user_query for kw in roundtrip_keywords):\n print(\" 🔄 检测到往返关键词,不算多目的地\")\n return {\n 'is_multi_destination': False,\n 'detected_keywords': [],\n 'raw_destination_text': extraction.get('destination', ''),\n 'detection_method': 'roundtrip_excluded'\n }\n \n # === 2) 多目的地关键词 ===\n multi_dest_keywords = [\n \"再去\", \"然后去\", \"接着去\", \"顺便去\",\n \"再到\", \"然后到\", \"接着到\",\n \"再去看看\", \"再看看\",\n \"之后去\", \"之后到\"\n ]\n detected_keywords = [kw for kw in multi_dest_keywords if kw in user_query]\n if detected_keywords:\n return {\n 'is_multi_destination': True,\n 'detected_keywords': detected_keywords,\n 'raw_destination_text': extraction.get('destination', ''),\n 'detection_method': 'keyword'\n }\n \n # === 3) 目的地字段中包含多个城市(逗号/顿号/“和”分隔) ===\n destination = extraction.get('destination', '') or ''\n origin = extraction.get('origin', '') or ''\n \n # 统一分隔符\n norm = destination.replace(',', ',').replace('、', ',').replace('和', ',')\n cities = [c.strip() for c in norm.split(',') if c.strip()]\n \n # 去重保持顺序\n unique_cities = []\n for c in cities:\n if c not in unique_cities:\n unique_cities.append(c)\n \n if len(unique_cities) >= 3:\n return {\n 'is_multi_destination': True,\n 'detected_keywords': [],\n 'raw_destination_text': destination,\n 'detection_method': 'comma_separated_3plus'\n }\n \n if len(unique_cities) == 2:\n # 如果两个城市中包含出发地,通常是往返(例如 上海, 南京)→ 视为单目的地\n if origin and origin in unique_cities:\n return {\n 'is_multi_destination': False,\n 'detected_keywords': [],\n 'raw_destination_text': destination,\n 'detection_method': 'origin_pair_excluded'\n }\n # 两个且都不是出发地 → 多目的地\n return {\n 'is_multi_destination': True,\n 'detected_keywords': [],\n 'raw_destination_text': destination,\n 'detection_method': 'comma_separated_2'\n }\n \n return {\n 'is_multi_destination': False,\n 'detected_keywords': [],\n 'raw_destination_text': destination\n }\n\n\ndef pre_analyze_query(user_query: str, llm) -> dict:\n \"\"\"预分析用户查询,判断场景类型\n \n 照搬自 nodes.py 中的 planner_node 逻辑\n \n 返回:\n dict: {\n 'scenario_type': 'simple' | 'complex' | 'multi_destination',\n 'needs_deep_analysis': bool, # 是否需要R1主导\n 'extraction': {...}, # 提取的信息\n 'multi_dest_info': {...} # 多目的地检测结果\n }\n \"\"\"\n from datetime import datetime\n \n today = datetime.now().strftime(\"%Y-%m-%d\")\n current_year = datetime.now().year\n \n # 构建提取 prompt,参考旧版本的 PLANNER_SYSTEM_PROMPT\n extraction_prompt = f\"\"\"You are a travel planning assistant. Today's date is {today}.\n\nYour task: Extract key information from the user query and determine if it needs deep analysis.\n\nRULES:\n- Output ONLY valid JSON. NO explanations, NO markdown code blocks.\n- Date conversion: \"今天\"/\"today\" = {today}, \"明天\"/\"tomorrow\" = +1 day, etc.\n- Use Chinese for city names.\n- Set \"needs_deep_analysis\" to true if:\n * Complex multi-city routes (e.g., \"A和B\", \"A再去B\")\n * Budget optimization needed (tight budget with many requirements)\n * Multiple conflicting constraints (e.g., elderly + children, limited time + many places)\n * Special needs: 老人, 小孩, 儿童, 亲子, 残疾, etc.\n\nOutput this exact JSON structure:\n{{\n \"destination\": \"extracted destination city or cities (comma separated if multiple)\",\n \"origin\": \"extracted origin city\",\n \"travel_days\": 0,\n \"budget\": 0,\n \"travel_date\": \"YYYY-MM-DD\",\n \"preferences\": [\"preference1\"],\n \"needs_deep_analysis\": false,\n \"has_special_needs\": false\n}}\n\nUser query: {user_query}\n\"\"\"\n \n try:\n response = llm.invoke(extraction_prompt)\n content = response.content.strip()\n \n # 移除可能的markdown代码块\n if content.startswith('```'):\n lines = content.split('\\n')\n content = '\\n'.join(lines[1:-1]) if len(lines) > 2 else content\n if content.startswith('json'):\n content = content[4:]\n \n import json\n extraction = json.loads(content)\n print(f\"\\n📊 预分析结果: {extraction}\")\n \n except Exception as e:\n print(f\"⚠️ 预分析失败: {e}\")\n # 默认值\n extraction = {\n \"destination\": \"\",\n \"origin\": \"\",\n \"travel_days\": 0,\n \"budget\": 0,\n \"travel_date\": \"\",\n \"preferences\": [],\n \"needs_deep_analysis\": False,\n \"has_special_needs\": False\n }\n \n # 检测多目的地场景\n multi_dest_info = detect_multi_destination(user_query, extraction)\n \n # 确定场景类型和是否需要R1\n if multi_dest_info.get('is_multi_destination', False):\n scenario_type = 'multi_destination'\n needs_r1 = True\n print(f\" 🌍 检测到多目的地场景: {multi_dest_info.get('detection_method')}\")\n elif extraction.get('needs_deep_analysis', False) or extraction.get('has_special_needs', False):\n scenario_type = 'complex'\n needs_r1 = True\n print(f\" 🧠 检测到复杂场景(特殊需求/预算紧张)\")\n else:\n scenario_type = 'simple'\n needs_r1 = False\n print(f\" ✅ 简单场景,Qwen3主导\")\n \n return {\n 'scenario_type': scenario_type,\n 'needs_deep_analysis': needs_r1,\n 'extraction': extraction,\n 'multi_dest_info': multi_dest_info\n }\n\ndef get_mcp_manager_sync():\n \"\"\"Sync wrapper to get MCP manager\"\"\"\n global _mcp_manager\n if _mcp_manager is None:\n from aggentic_RAG.travel_agent.tools.mcp_tools import MCPToolManager\n _mcp_manager = MCPToolManager()\n _mcp_loop.run_until_complete(_mcp_manager.initialize())\n return _mcp_manager\n\ndef call_mcp_tool_sync(server_name: str, tool_name: str, **kwargs) -> str:\n \"\"\"Sync wrapper to call MCP tool\"\"\"\n manager = get_mcp_manager_sync()\n return _mcp_loop.run_until_complete(manager.call_tool(server_name, tool_name, **kwargs))\n\n\n# 创建工具列表\ndef create_tools(retriever) -> List[Tool]:\n \"\"\"创建Agent可用的工具列表\"\"\"\n from langchain.tools.retriever import create_retriever_tool\n from aggentic_RAG.travel_agent.tools.tool_registry import AVAILABLE_TOOLS\n \n tools = []\n \n # 1. RAG检索工具(如果有文档)\n if retriever is not None:\n rag_tool = create_retriever_tool(\n retriever=retriever,\n name=\"rag_search\",\n description=\"用于查询旅游攻略、景点信息、美食推荐等。输入城市名或景点名。\"\n )\n tools.append(rag_tool)\n \n # 2. 初始化MCP管理器\n try:\n get_mcp_manager_sync() # 预初始化MCP连接\n except Exception as e:\n st.warning(f\"MCP初始化失败: {e}\")\n \n # 3. 从tool_registry获取所有MCP工具定义\n import json\n from datetime import datetime\n \n # 特殊处理函数:查询火车票(需要先获取站点代码)\n def query_train_tickets(origin: str, destination: str, date: str) -> str:\n \"\"\"Query train tickets - first get station codes, then query tickets\"\"\"\n print(f\"\\n🚂 [train_query] 调用: {origin} -> {destination}, {date}\")\n try:\n # 1. 修正日期年份\n try:\n date_obj = datetime.strptime(date, \"%Y-%m-%d\")\n current_year = datetime.now().year\n if date_obj.year < current_year:\n date = date.replace(str(date_obj.year), str(current_year))\n print(f\" 日期修正为: {date}\")\n except:\n pass\n \n # 2. 获取站点代码\n from_code = None\n to_code = None\n \n # 分别查询每个城市的站点代码\n for city, is_origin in [(origin, True), (destination, False)]:\n try:\n print(f\" 获取{city}站点代码...\")\n codes_result = call_mcp_tool_sync(\n \"12306 Server\",\n \"get-stations-code-in-city\",\n city=city\n )\n print(f\" {city}站点查询结果: {str(codes_result)[:150]}\")\n \n if codes_result and \"error\" not in str(codes_result).lower():\n codes_data = json.loads(codes_result) if isinstance(codes_result, str) else codes_result\n \n # 处理数组格式 - 找主站(站名与城市名匹配的)\n if isinstance(codes_data, list) and len(codes_data) > 0:\n code = None\n station_name = None\n \n # 优先找完全匹配的主站(如\"上海\"->\"上海\", \"徐州\"->\"徐州\")\n for station in codes_data:\n name = station.get('station_name', '')\n if name == city: # 完全匹配\n code = station.get('station_code') or station.get('code') or station.get('telecode')\n station_name = name\n break\n \n # 其次找包含城市名的站(如\"上海\"->\"上海虹桥\", \"上海南\")\n if not code:\n for station in codes_data:\n name = station.get('station_name', '')\n if city in name and '东' not in name and '南' not in name and '西' not in name and '北' not in name:\n code = station.get('station_code') or station.get('code') or station.get('telecode')\n station_name = name\n break\n \n # 最后尝试第一个包含城市名的站\n if not code:\n for station in codes_data:\n name = station.get('station_name', '')\n if city in name:\n code = station.get('station_code') or station.get('code') or station.get('telecode')\n station_name = name\n break\n \n if code:\n if is_origin:\n from_code = code\n else:\n to_code = code\n print(f\" ✅ {city}站点: {station_name} (代码: {code})\")\n else:\n print(f\" ⚠️ {city}未找到匹配的主站\")\n except Exception as e:\n print(f\" {city}站点查询异常: {e}\")\n \n print(f\" 站点代码汇总: {origin}={from_code}, {destination}={to_code}\")\n \n # 3. 查询车票\n train_result = None\n if from_code and to_code:\n print(f\" 步骤2: 使用站点代码查询车票...\")\n train_result = call_mcp_tool_sync(\n \"12306 Server\",\n \"get-tickets\",\n fromStation=from_code,\n toStation=to_code,\n date=date\n )\n print(f\" 车票查询结果: {str(train_result)[:300]}\")\n else:\n # 备选:直接使用城市名查询\n print(f\" 步骤2(备选): 直接使用城市名查询...\")\n train_result = call_mcp_tool_sync(\n \"12306 Server\",\n \"get-tickets\",\n fromStation=origin,\n toStation=destination,\n date=date\n )\n print(f\" 车票查询结果: {str(train_result)[:300]}\")\n \n # 4. 同时查询自驾路线(照搬旧版本行为)\n driving_result = None\n try:\n print(f\"\\n🚗 步骤3: 自动查询自驾路线信息\")\n \n # 先获取城市经纬度\n origin_coords = None\n dest_coords = None\n \n try:\n origin_geo = call_mcp_tool_sync(\"Gaode Server\", \"maps_geo\", address=origin)\n if origin_geo and \"error\" not in str(origin_geo).lower():\n geo_data = json.loads(origin_geo) if isinstance(origin_geo, str) else origin_geo\n if isinstance(geo_data, dict) and 'return' in geo_data:\n if isinstance(geo_data['return'], list) and len(geo_data['return']) > 0:\n origin_coords = geo_data['return'][0].get('location')\n \n dest_geo = call_mcp_tool_sync(\"Gaode Server\", \"maps_geo\", address=destination)\n if dest_geo and \"error\" not in str(dest_geo).lower():\n geo_data = json.loads(dest_geo) if isinstance(dest_geo, str) else dest_geo\n if isinstance(geo_data, dict) and 'return' in geo_data:\n if isinstance(geo_data['return'], list) and len(geo_data['return']) > 0:\n dest_coords = geo_data['return'][0].get('location')\n \n if origin_coords and dest_coords:\n print(f\" ✅ 经纬度: {origin}={origin_coords}, {destination}={dest_coords}\")\n driving_result = call_mcp_tool_sync(\n \"Gaode Server\",\n \"maps_direction_driving\",\n origin=origin_coords,\n destination=dest_coords\n )\n print(f\" ✅ 自驾路线查询成功: {str(driving_result)[:200]}\")\n else:\n print(f\" ⚠️ 无法获取经纬度,跳过自驾路线\")\n except Exception as geo_err:\n print(f\" ⚠️ 地理编码失败: {geo_err}\")\n except Exception as driving_err:\n print(f\" ⚠️ 自驾路线查询异常: {driving_err}\")\n \n # 5. 组合返回结果(包含火车和自驾信息)\n combined_result = {}\n if train_result:\n combined_result[\"train\"] = train_result\n if driving_result:\n combined_result[\"driving\"] = driving_result\n \n if combined_result:\n return json.dumps(combined_result, ensure_ascii=False)\n else:\n return \"交通查询失败\"\n except Exception as e:\n print(f\" 火车票查询异常: {e}\")\n return f\"火车票查询失败: {str(e)}\"\n \n for tool_def in AVAILABLE_TOOLS:\n if tool_def.tool_type == \"mcp\":\n # 特殊处理train_query\n if tool_def.name == \"train_query\":\n def train_tool_func(tool_input: str) -> str:\n try:\n if tool_input.strip().startswith('{'):\n kwargs = json.loads(tool_input)\n else:\n return \"请提供JSON格式的参数\"\n \n origin = kwargs.get(\"origin\") or kwargs.get(\"from\") or kwargs.get(\"fromStation\", \"\")\n destination = kwargs.get(\"destination\") or kwargs.get(\"to\") or kwargs.get(\"toStation\", \"\")\n date = kwargs.get(\"date\", \"\")\n \n return query_train_tickets(origin, destination, date)\n except Exception as e:\n return f\"参数解析失败: {str(e)}\"\n \n tools.append(Tool(\n name=tool_def.name,\n description=tool_def.description,\n func=train_tool_func\n ))\n continue\n \n # 其他MCP工具的通用处理\n def make_sync_tool(server_name, mcp_tool_name, tool_name):\n def call_tool(tool_input: str) -> str:\n \"\"\"Parse input string and call MCP tool\"\"\"\n print(f\"\\n🛠️ [{tool_name}] 调用: {tool_input[:100]}\")\n try:\n if tool_input.strip().startswith('{'):\n kwargs = json.loads(tool_input)\n else:\n kwargs = {\"query\": tool_input}\n \n # 修正日期年份\n if \"date\" in kwargs:\n date_str = kwargs[\"date\"]\n try:\n date_obj = datetime.strptime(date_str, \"%Y-%m-%d\")\n current_year = datetime.now().year\n if date_obj.year < current_year:\n kwargs[\"date\"] = date_str.replace(str(date_obj.year), str(current_year))\n except:\n pass\n \n # 黄历工具特殊处理 - 转换为ISO时间格式\n if tool_name == \"lucky_day\":\n date_val = kwargs.pop('date', None) or kwargs.pop('query', '')\n # 确保日期格式正确\n if date_val:\n date_val = date_val.strip()\n kwargs = {\"solarDatetime\": f\"{date_val}T12:00:00+08:00\"}\n \n # 航班查询工具 - 参数映射\n if tool_name == \"flight_query\":\n dep = kwargs.pop('dep', None) or kwargs.pop('from', None) or kwargs.pop('origin', '')\n arr = kwargs.pop('arr', None) or kwargs.pop('to', None) or kwargs.pop('destination', '')\n date_val = kwargs.pop('date', '')\n # 修正日期年份\n if date_val:\n try:\n date_obj = datetime.strptime(date_val.strip(), \"%Y-%m-%d\")\n if date_obj.year < datetime.now().year:\n date_val = date_val.replace(str(date_obj.year), str(datetime.now().year))\n except:\n pass\n kwargs = {\"dep\": dep, \"arr\": arr, \"date\": date_val}\n \n # 天气工具 - 确保city参数\n if tool_name == \"gaode_weather\":\n city = kwargs.pop(\"city\", None) or kwargs.pop(\"location\", None) or kwargs.pop(\"query\", \"\")\n # 清理城市名:去除日期、空格、逗号后的内容\n if city:\n city = city.strip()\n # 如果包含逗号,取第一部分\n if ',' in city:\n city = city.split(',')[0].strip()\n if ',' in city:\n city = city.split(',')[0].strip()\n # 去除\"市\"后缀保留纯城市名\n city = city.replace('市', '')\n kwargs = {\"city\": city}\n \n # 地理编码工具 - 确保address参数\n if tool_name == \"gaode_geo\":\n address = kwargs.pop(\"address\", None) or kwargs.pop(\"location\", None) or kwargs.pop(\"query\", \"\")\n kwargs = {\"address\": address}\n \n # 酒店搜索工具 - 确保keywords和city参数\n if tool_name == \"gaode_hotel_search\":\n keywords = kwargs.pop(\"keywords\", None) or kwargs.pop(\"keyword\", None) or kwargs.pop(\"query\", \"\")\n city = kwargs.pop(\"city\", None) or kwargs.pop(\"location\", \"\")\n # 如果keywords没有包含\"酒店\"或\"民宿\",自动补充\n if keywords and \"酒店\" not in keywords and \"民宿\" not in keywords:\n keywords = f\"{keywords} 酒店\"\n kwargs = {\"keywords\": keywords, \"city\": city} if city else {\"keywords\": keywords}\n \n # POI搜索工具 - 确保keywords参数\n if tool_name == \"gaode_poi_search\":\n keywords = kwargs.pop(\"keywords\", None) or kwargs.pop(\"keyword\", None) or kwargs.pop(\"query\", \"\")\n city = kwargs.pop(\"city\", None) or kwargs.pop(\"location\", \"\")\n kwargs = {\"keywords\": keywords, \"city\": city} if city else {\"keywords\": keywords}\n \n # 自驾路线工具 - 确保origin和destination参数(应为坐标)\n if tool_name == \"gaode_driving\":\n origin = kwargs.pop(\"origin\", None) or kwargs.pop(\"from\", \"\")\n destination = kwargs.pop(\"destination\", None) or kwargs.pop(\"to\", \"\")\n kwargs = {\"origin\": origin, \"destination\": destination}\n \n print(f\" 参数: {kwargs}\")\n return call_mcp_tool_sync(server_name, mcp_tool_name, **kwargs)\n except json.JSONDecodeError:\n # 对于简单字符串输入,根据工具类型处理\n print(f\" 简单字符串输入: {tool_input}\")\n tool_input_clean = tool_input.strip()\n \n if tool_name == \"gaode_weather\":\n city = tool_input_clean\n if ',' in city:\n city = city.split(',')[0].strip()\n if ',' in city:\n city = city.split(',')[0].strip()\n city = city.replace('市', '')\n return call_mcp_tool_sync(server_name, mcp_tool_name, city=city)\n elif tool_name == \"gaode_geo\":\n return call_mcp_tool_sync(server_name, mcp_tool_name, address=tool_input_clean)\n elif tool_name == \"lucky_day\":\n iso_dt = f\"{tool_input_clean}T12:00:00+08:00\"\n return call_mcp_tool_sync(server_name, mcp_tool_name, solarDatetime=iso_dt)\n elif tool_name == \"gaode_hotel_search\":\n # 如果输入是城市名,补充酒店关键词\n keywords = f\"{tool_input_clean} 酒店\" if \"酒店\" not in tool_input_clean else tool_input_clean\n return call_mcp_tool_sync(server_name, mcp_tool_name, keywords=keywords)\n elif tool_name == \"gaode_poi_search\":\n return call_mcp_tool_sync(server_name, mcp_tool_name, keywords=tool_input_clean)\n else:\n return call_mcp_tool_sync(server_name, mcp_tool_name, query=tool_input_clean)\n except Exception as e:\n return f\"工具调用失败: {str(e)}\"\n return call_tool\n \n # 创建工具\n mcp_tool = Tool(\n name=tool_def.name,\n description=tool_def.description,\n func=make_sync_tool(tool_def.server_name, tool_def.mcp_tool_name, tool_def.name)\n )\n tools.append(mcp_tool)\n \n elif tool_def.tool_type == \"r1\":\n # R1 深度分析工具 - 使用 DeepSeek R1 API\n def make_r1_tool():\n def call_r1_analysis(tool_input: str) -> str:\n \"\"\"使用DeepSeek R1进行深度分析\"\"\"\n print(f\"\\n🧠 [r1_analysis] 深度分析调用: {tool_input[:200]}\")\n try:\n if tool_input.strip().startswith('{'):\n kwargs = json.loads(tool_input)\n else:\n kwargs = {\"problem\": tool_input, \"context\": {}}\n \n problem = kwargs.get(\"problem\", \"\")\n context = kwargs.get(\"context\", {})\n \n # 调用DeepSeek R1 API(使用正确的 DeepSeek 配置)\n from openai import OpenAI\n \n # DeepSeek R1 使用其自己的 API\n deepseek_api_key = os.getenv(\"DEEPSEEK_API_KEY\", \"\")\n if not deepseek_api_key:\n return \"深度分析不可用: 未配置 DEEPSEEK_API_KEY\"\n \n r1_client = OpenAI(\n api_key=deepseek_api_key,\n base_url=\"https://api.deepseek.com\"\n )\n \n # 构建分析请求 - 参考旧版本 nodes.py 中 R1 的 prompt 风格\n analysis_prompt = f\"\"\"你是一个专业的旅行规划优化师。请对以下旅行规划问题进行深度分析和优化:\n\n问题描述:{problem}\n\n已收集的信息:\n{json.dumps(context, ensure_ascii=False, indent=2) if context else '无额外上下文'}\n\n你的任务:\n1. **路线优化**:分析多段行程的最优顺序和连接方式\n2. **时间安排**:每个目的地的合理停留时间\n3. **预算分配**:根据各段行程的物价和景点密度分配预算\n4. **风险评估**:识别天气、交通、时间等风险\n5. **备选方案**:提供经济型、舒适型等不同方案\n\n输出JSON格式(纯JSON,不要markdown代码块):\n{{\n \"route_optimization\": \"路线优化建议\",\n \"time_arrangement\": \"时间安排建议\",\n \"budget_allocation\": {{\n \"城市名\": 预算金额\n }},\n \"risk_warnings\": [\"风险1\", \"风险2\"],\n \"alternative_plans\": [\n {{\n \"name\": \"方案名称\",\n \"description\": \"方案描述\",\n \"total_cost\": 总费用,\n \"pros\": [\"优点1\"],\n \"cons\": [\"缺点1\"]\n }}\n ],\n \"final_recommendation\": \"最终建议\"\n}}\n\"\"\"\n \n response = r1_client.chat.completions.create(\n model=\"deepseek-reasoner\", # 使用正确的模型名\n messages=[{\"role\": \"user\", \"content\": analysis_prompt}],\n max_tokens=4000\n )\n \n result = response.choices[0].message.content\n print(f\" R1分析完成,返回 {len(result)} 字符\")\n return result\n except Exception as e:\n print(f\" R1分析失败: {e}\")\n import traceback\n traceback.print_exc()\n return f\"深度分析暂时不可用: {str(e)}\"\n return call_r1_analysis\n \n tools.append(Tool(\n name=tool_def.name,\n description=tool_def.description,\n func=make_r1_tool()\n ))\n \n return tools\n\n\n# 创建工具\ntry:\n tools = create_tools(retriever)\n if tools:\n tool_names = [t.name for t in tools]\n st.sidebar.success(f\"✅ 已加载 {len(tools)} 个工具\")\n # 显示工具列表以便调试\n with st.sidebar.expander(\"查看工具列表\"):\n for t in tools:\n st.write(f\"- {t.name}\")\n else:\n st.sidebar.warning(\"⚠️ 未加载任何工具\")\nexcept Exception as e:\n st.sidebar.error(f\"❌ 工具加载失败: {e}\")\n tools = []\n\n\n# 初始化聊天历史\nif \"messages\" not in st.session_state:\n st.session_state.messages = [\n {\"role\": \"assistant\", \"content\": \"您好!我是智能旅游规划助手🗺️\\n\\n我可以帮您:\\n- 📍 查询景点攻略和美食推荐\\n- 🚆 查询火车票和航班信息\\n- 🏨 推荐酒店和住宿\\n- ☀️ 查询天气预报\\n- 🗓️ 查询黄历吉日\\n- 🚗 规划自驾路线\\n\\n请告诉我您的旅行需求吧!\"}\n ]\n\n# 显示历史消息\nfor msg in st.session_state.messages:\n with st.chat_message(msg[\"role\"]):\n st.markdown(msg[\"content\"])\n\n\n# 创建聊天历史管理\nmsgs = StreamlitChatMessageHistory()\nmemory = ConversationBufferMemory(\n chat_memory=msgs,\n return_messages=True,\n memory_key=\"chat_history\",\n output_key=\"output\"\n)\n\n\n# ReAct Prompt模板\nfrom datetime import datetime\ncurrent_date = datetime.now().strftime(\"%Y-%m-%d\")\ncurrent_year = datetime.now().year\n\ninstructions = f\"\"\"你是一个专业的旅游规划助手。当前日期是{current_date}。\n\n**完整旅行规划必须包含以下信息,请按顺序调用相应工具:**\n\n1. **交通信息**:\n - 调用 train_query 查询火车票(必须)\n - 如果火车不便,调用 gaode_driving 查询自驾路线\n - 长途(>800km)可调用 flight_query 查询航班\n\n2. **天气预报**:\n - 对每个目的地城市调用 gaode_weather 查询天气(必须)\n - 例如去徐州和青岛,需要分别查询两个城市的天气\n\n3. **住宿推荐**:\n - 调用 gaode_hotel_search 搜索酒店(必须)\n - 根据预算选择关键词:预算紧张用\"经济型酒店\",预算充足用\"酒店\"\n\n4. **黄历吉日**:\n - 调用 lucky_day 查询出发日期的黄历信息\n\n5. **复杂行程优化**(多城市、多段行程):\n - 调用 r1_analysis 进行深度路线优化\n\n**重要规则:**\n- 日期必须使用{current_year}年,例如\"{current_year}-12-18\"\n- 每个工具可以针对不同参数调用多次(如查询多个城市的天气)\n- 用中文回答,提供详细的行程规划\n\"\"\"\n\nbase_prompt_template = \"\"\"\n{instructions}\n\nTOOLS:\n------\nYou have access to the following tools:\n{tools}\n\nTo use a tool, you MUST strictly follow this EXACT format:\n\nThought: Do I need to use a tool? Yes\nAction: the action to take, must be one of [{tool_names}]\nAction Input: the input to the action\nObservation: the result of the action\n\nWhen you have a response to say to the Human, or if you do not need to use a tool, you MUST strictly follow this EXACT format:\n\nThought: Do I need to use a tool? No\nFinal Answer: [your response here]\n\nCRITICAL RULES:\n1. For complete travel planning, you MUST call: train_query, gaode_weather (for each destination), gaode_hotel_search, lucky_day\n2. You CAN call the same tool multiple times with DIFFERENT parameters (e.g. weather for different cities)\n3. NEVER call the same tool with the SAME parameters twice\n4. If a tool returns an error, explain the error to the user and move on\n5. For multi-city complex trips, call r1_analysis to optimize the route\n6. Use Chinese to respond to users\n7. Provide detailed and complete travel plans\n\nBegin!\n\nPrevious conversation history:\n{chat_history}\n\nNew input: {input}\n{agent_scratchpad}\"\"\"\n\n# 创建Prompt\nbase_prompt = PromptTemplate.from_template(base_prompt_template)\nprompt = base_prompt.partial(instructions=instructions)\n\n\n# 创建LLM(使用Qwen3)\n@st.cache_resource\ndef get_llm():\n return ChatOpenAI(\n model=\"qwen-plus\",\n openai_api_key=os.getenv(\"DASHSCOPE_API_KEY\"),\n openai_api_base=\"https://dashscope.aliyuncs.com/compatible-mode/v1\",\n temperature=0.7\n )\n\nllm = get_llm()\n\n\n# 创建ReAct Agent\nagent = create_react_agent(llm, tools, prompt)\n\n# 创建Agent执行器\nmax_iterations = 30 # 复杂行程需要更多迭代\nagent_executor = AgentExecutor(\n agent=agent,\n tools=tools,\n memory=memory,\n verbose=False, # 关闭详细日志,避免UI显示调试信息\n handle_parsing_errors=True,\n max_iterations=max_iterations\n)\n\n\n# 聊天输入框\nif user_query := st.chat_input(placeholder=\"请输入您的旅行需求,例如:我想12月去杭州玩3天\"):\n # 添加用户消息\n st.session_state.messages.append({\"role\": \"user\", \"content\": user_query})\n \n # 显示用户消息\n with st.chat_message(\"user\"):\n st.markdown(user_query)\n \n # 显示助手响应\n with st.chat_message(\"assistant\"):\n # 隐藏调试信息,不显示工具调用过程\n # st_container = st.container()\n # st_cb = StreamlitCallbackHandler(st_container)\n \n # 配置回调(空配置,不显示执行过程)\n config = {}\n \n # ========== 预分析层:判断场景类型 ==========\n with st.spinner(\"🔍 正在分析您的旅行需求...\"):\n pre_analysis = pre_analyze_query(user_query, llm)\n \n scenario_type = pre_analysis['scenario_type']\n needs_r1 = pre_analysis['needs_deep_analysis']\n extraction = pre_analysis['extraction']\n \n # 显示场景类型提示(隐藏技术细节)\n if scenario_type == 'multi_destination':\n st.info(\"🌍 检测到多目的地行程,将调用深度路线优化...\")\n elif scenario_type == 'complex':\n st.info(\"🧠 检测到复杂场景(特殊需求/预算紧张),将进行深度分析...\")\n else:\n st.info(\"✅ 正在为您规划旅行...\")\n \n # ========== 根据场景类型选择不同的处理路径 ==========\n with st.spinner(\"正在规划您的旅行...\"):\n try:\n if needs_r1:\n # 复杂场景 / 多目的地:R1 主导\n # 先调用 R1 进行深度分析和规划\n print(f\"\\n🧠 R1 主导模式: {scenario_type}\")\n \n # 构建 R1 分析的上下文\n r1_context = {\n \"user_query\": user_query,\n \"extraction\": extraction,\n \"scenario_type\": scenario_type,\n \"multi_dest_info\": pre_analysis.get('multi_dest_info', {})\n }\n \n # 构建增强的输入,强制 Agent 先调用 r1_analysis\n enhanced_input = f\"\"\"用户查询: {user_query}\n\nℹ️ **重要提示**:系统检测到这是一个{'多目的地' if scenario_type == 'multi_destination' else '复杂'}场景。\n\n请按以下顺序处理:\n1. **首先调用 r1_analysis** 进行深度路线规划和优化,输入应包含完整的用户需求\n2. 然后根据 R1 的建议,调用其他工具查询具体信息(火车票、天气、酒店等)\n3. 最后综合所有信息生成完整的旅行规划\n\n已提取的信息:\n- 目的地: {extraction.get('destination', '未知')}\n- 出发地: {extraction.get('origin', '未知')}\n- 旅行天数: {extraction.get('travel_days', 0)}\n- 预算: {extraction.get('budget', 0)}元\n- 出发日期: {extraction.get('travel_date', '未知')}\n- 偏好: {extraction.get('preferences', [])}\n\"\"\"\n response = agent_executor.invoke({\"input\": enhanced_input}, config=config)\n else:\n # 简单场景:Qwen3 主导\n print(f\"\\n✅ Qwen3 主导模式: {scenario_type}\")\n response = agent_executor.invoke({\"input\": user_query}, config=config)\n \n # 提取回答\n answer = response.get(\"output\", \"抱歉,我无法生成回答。\")\n \n # 添加助手消息\n st.session_state.messages.append({\"role\": \"assistant\", \"content\": answer})\n \n # 显示回答\n st.markdown(answer)\n \n except Exception as e:\n error_msg = f\"抱歉,处理您的请求时出现错误:{str(e)}\"\n st.error(error_msg)\n st.session_state.messages.append({\"role\": \"assistant\", \"content\": error_msg})\n\n\n# 侧边栏 - 显示当前配置\nwith st.sidebar:\n st.markdown(\"---\")\n st.header(\"📊 当前配置\")\n st.write(f\"- 文档数量: {len(uploaded_files)}\")\n st.write(f\"- 工具数量: {len(tools)}\")\n st.write(f\"- 最大迭代: {max_iterations}\")\n st.write(f\"- 对话轮数: {len(st.session_state.messages)}\")\n"}}}}