map_nodes.py 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190
  1. from __future__ import annotations
  2. """地图研究节点:调用MapResearchAgent通过高德MCP搜索目的地周边景点、餐饮与天气。"""
  3. import json
  4. import re
  5. from typing import Any, Literal
  6. from langchain_core.messages import AIMessage
  7. from app.agents.map_agent import MapResearchAgent
  8. from app.graph.state import TravelState
  9. from app.schemas.place import MapResearchResult
  10. def _extract_json_from_messages(
  11. messages: list[Any],
  12. ) -> dict[str, Any] | None:
  13. """从 Agent 最后一条 AI 消息中提取 JSON 代码块。"""
  14. for message in reversed(messages):
  15. if not isinstance(message, AIMessage):
  16. continue
  17. content = message.content
  18. if not isinstance(content, str):
  19. continue
  20. # 优先匹配 ```json ... ``` 代码块
  21. json_match = re.search(
  22. r"```json\s*([\s\S]*?)\s*```",
  23. content,
  24. )
  25. if json_match:
  26. try:
  27. return json.loads(
  28. json_match.group(1)
  29. )
  30. except json.JSONDecodeError:
  31. continue
  32. # 尝试匹配 ``` ... ``` 无标注代码块
  33. code_match = re.search(
  34. r"```\s*(\{[\s\S]*?\})\s*```",
  35. content,
  36. )
  37. if code_match:
  38. try:
  39. return json.loads(
  40. code_match.group(1)
  41. )
  42. except json.JSONDecodeError:
  43. continue
  44. # 最后尝试在整段内容中查找 JSON 对象
  45. json_match = re.search(
  46. r"\{[\s\S]*\"city\"[\s\S]*\}",
  47. content,
  48. )
  49. if json_match:
  50. try:
  51. return json.loads(
  52. json_match.group(0)
  53. )
  54. except json.JSONDecodeError:
  55. continue
  56. return None
  57. def route_after_resources(
  58. state: TravelState,
  59. ) -> Literal[
  60. "research_destination",
  61. "end",
  62. ]:
  63. """资源查询成功后进入地图研究。"""
  64. if state.get("errors"):
  65. return "end"
  66. flight_result = state.get(
  67. "flight_search_result",
  68. {},
  69. )
  70. hotel_result = state.get(
  71. "hotel_search_result",
  72. {},
  73. )
  74. if not flight_result or not hotel_result:
  75. return "end"
  76. return "research_destination"
  77. def make_map_research_node(
  78. agent: MapResearchAgent,
  79. ):
  80. """创建地图研究节点。"""
  81. async def map_research_node(
  82. state: TravelState,
  83. ) -> dict[str, Any]:
  84. """地图研究节点:调用Agent获取高德地图POI数据,从消息中提取JSON结构化结果写入state。"""
  85. request = state["travel_request"]
  86. try:
  87. agent_result = await agent.research(
  88. request
  89. )
  90. messages = agent_result.get(
  91. "messages",
  92. [],
  93. )
  94. parsed = _extract_json_from_messages(
  95. messages
  96. )
  97. if parsed is None:
  98. # 调试:打印最后一条消息的内容
  99. last_content = ""
  100. for msg in reversed(messages):
  101. if isinstance(msg, AIMessage):
  102. last_content = str(
  103. msg.content
  104. )[:500]
  105. break
  106. return {
  107. "errors": [
  108. "地图Agent未返回有效的JSON结构化结果。"
  109. ],
  110. "map_messages": messages,
  111. "final_answer": (
  112. "地图Agent未返回有效结果。"
  113. f"最后一条消息预览:{last_content}"
  114. ),
  115. }
  116. structured_response = (
  117. MapResearchResult.model_validate(
  118. parsed
  119. )
  120. )
  121. attraction_count = len(
  122. structured_response.attractions
  123. )
  124. restaurant_count = len(
  125. structured_response.restaurants
  126. )
  127. weather_count = len(
  128. structured_response.weather
  129. )
  130. return {
  131. "map_research_result": (
  132. structured_response
  133. ),
  134. "map_messages": messages,
  135. "final_answer": (
  136. "真实地图资源查询完成:"
  137. f"获得{attraction_count}个景点候选,"
  138. f"{restaurant_count}个餐饮候选,"
  139. f"{weather_count}天天气信息。"
  140. ),
  141. }
  142. except Exception as exc:
  143. message = (
  144. "地图Agent执行失败:"
  145. f"{type(exc).__name__}: {exc}"
  146. )
  147. return {
  148. "errors": [message],
  149. "final_answer": message,
  150. }
  151. return map_research_node