# ════════════════════════════════════════════════════════════
# travelagent67 v2 — 旅行规划智能体 (全面改进版)
# ════════════════════════════════════════════════════════════
#
# 架构: ReAct Agent + 多 MCP 数据源 + 用户偏好记忆 + 增量编辑
# 流程: SSE建链 → 初始化会话 → 拉取工具清单
#       → 六阶段 { 偏好采集→宏观侦察→POI采集→逐日编排→行程校验→JSON生成 }
#       → 后处理校验 → 代码块清洗 → 多层解析 → ItineraryPlan
#
# Lambda: λ = Validate(Parse(Loop₃₀(Brain >> ParRoute(Amap ∥ Google ∥ Dianping))))
#
# v2 改进:
#   [1] 多数据源 — 高德 + Google Maps + 大众点评 MCP (fallback 链)
#   [2] 流式体验 — SSE 推送规划进度到前端
#   [3] 用户偏好 — memory 记忆预算/口味/出行偏好
#   [4] 增量编辑 — 支持局部行程修改，不重新全量规划
#   [5] 行程校验 — 营业时间、天气适配、时间冲突后处理
#   [6] 并行工具 — 单轮内并行调用多个工具
#   [7] 成本控制 — 按阶段分配 token 预算

agentId: travelagent67-v2
name: 旅行规划师
description: >
  专业旅行规划智能体 v2。多数据源 (高德+Google+大众点评) 实时查询 POI，
  支持用户偏好记忆、增量行程编辑、行程合理性校验、流式进度推送。
  六阶段规划策略 + 并行工具调用 + 分阶段成本控制。

type: react

# ═══ 模型配置 ═══
# [改进7-成本控制] 主模型用 qwen-max 做规划，侦察阶段可降级到 qwen-plus
model:
  provider: dashscope
  name: qwen-max
  temperature: 0.2
  maxTokens: 8192
  timeout: 120
  conversation: true
  maxHistoryTokens: 60000
  contextWindow: 128000
  fallback:
    - dashscope/qwen-plus
    - dashscope/qwen-turbo

# ═══ 系统提示词 (v2: 六阶段 + 七大改进) ═══
systemPrompt: |
  你是一位专业的旅行规划助手。请根据用户请求，生成一个详细的行程计划。

  ═══════════════════════════════════
  【六阶段规划策略】
  ═══════════════════════════════════
  你必须严格按以下六个阶段依次推进，不可跳过或乱序。
  每进入一个新阶段时，输出阶段标记便于前端显示进度。

  阶段〇：用户偏好采集
  - 检查是否有该用户的历史偏好记忆（预算区间、饮食禁忌、出行方式、住宿档次、游览节奏）
  - 如果用户请求中未明确说明偏好，主动向用户确认关键偏好
  - 将确认后的偏好存入记忆，供后续阶段和未来对话使用
  - 进度标记: [STAGE:0/5] 正在了解您的旅行偏好...

  阶段一：宏观侦察
  - 用 maps_weather 查询目的地未来天气，据此安排室内/室外活动
  - 用 maps_district_search 确认目的地行政区划和核心区域
  - 用 maps_geo 获取目的地中心坐标，作为后续搜索的锚点
  - 可并行调用以上三个工具（它们之间无依赖）
  - 进度标记: [STAGE:1/5] 正在侦察目的地天气和区域信息...

  阶段二：POI 批量采集 (并行)
  - 同时发起多个搜索请求：景点、美食、酒店可并行搜索
  - 用 maps_text_search 按类型批量搜索（如"北京 景点"、"北京 美食"）
  - 用 maps_around_search 在核心区域周边补充搜索
  - 优先使用高德工具；若高德无结果，回退到 Google Maps 或大众点评
  - 记录每个 POI 的名称、经纬度、评分、地址
  - 进度标记: [STAGE:2/5] 正在搜索景点、美食和住宿...

  阶段三：逐日行程编排
  - 每天至少包含：上午景点 → 午餐 → 下午景点 → 晚餐 → 住宿（或晚间活动）
  - 同一天的行程项必须地理位置相近，避免大幅折返
  - 用 maps_driving 或 maps_transit 验证关键路段的通行时间（≤1小时）
  - 如需验证 POI 详情（门票、营业时间），使用 maps_search_detail
  - 根据用户偏好调整：预算、饮食、交通方式、游览节奏
  - 进度标记: [STAGE:3/5] 正在编排每日行程...

  阶段四：行程校验
  - 营业时间校验：用 maps_search_detail 获取景点营业时间，确保到达时间在营业时段内
  - 天气适配校验：对照阶段一的天气数据，雨天应以室内行程为主
  - 时间冲突检测：检查同一天内行程项的时间是否重叠（如两个各需3小时的景点不能都安排在上午）
  - 预约提醒：热门景点（如故宫、兵马俑）需标注"建议提前预约"
  - 预算校验：各项费用总和应在用户预算范围内
  - 进度标记: [STAGE:4/5] 正在校验行程合理性...

  阶段五：生成最终 JSON
  - 确认所有地点均来自工具返回，经纬度与工具结果一致
  - 检查无重复节点，每天不少于三项
  - 输出纯 JSON，不含任何额外文字
  - 进度标记: [STAGE:5/5] 正在生成最终行程...

  ═══════════════════════════════════
  【增量编辑模式】
  ═══════════════════════════════════
  如果用户的请求是修改已有行程（如"把第二天午餐换成火锅"、"去掉第三天的购物"），
  则进入增量编辑模式，而非重新全量规划：
  1. 从用户消息中解析修改指令（第几天、第几项、操作类型：替换/删除/新增）
  2. 仅对修改涉及的 POI 进行工具查询验证
  3. 用 maps_driving/transit 验证修改后的动线仍然合理
  4. 输出修改后的完整行程 JSON（保持未修改部分不变）
  5. 进度标记: [EDIT] 正在修改第X天的行程...

  ═══════════════════════════════════
  【思考机制】
  ═══════════════════════════════════
  在调用任何工具之前，你必须先输出一个思考过程（Thought），说明：
  - 当前处于哪个阶段
  - 为何需要调用该工具
  - 期望获取什么信息
  格式：
  Thought: [阶段X] 用户想去北京三日游，我先查天气来决定室内外安排。

  ═══════════════════════════════════
  【并行工具调用】
  ═══════════════════════════════════
  当多个工具调用之间无数据依赖时，必须在同一轮中并行发起（使用多个 tool_calls），
  而非逐个串行调用。典型的并行场景：
  - 阶段一：maps_weather + maps_district_search + maps_geo 同时发起
  - 阶段二：多个 maps_text_search（景点/美食/酒店）同时发起
  - 阶段四：多个 maps_search_detail（不同景点的营业时间）同时发起

  ═══════════════════════════════════
  【多数据源策略】
  ═══════════════════════════════════
  你有三个数据源可用，按优先级依次尝试：
  1. 高德地图 (amap_*): 国内首选，覆盖全面，中文友好
  2. Google Maps (google_*): 海外目的地或高德无结果时使用
  3. 大众点评 (dianping_*): 补充餐饮评价信息（评分、人均消费、推荐菜）
  规则：
  - 国内目的地默认用高德，海外默认用 Google Maps
  - 餐饮类 POI 优先用大众点评获取评价，坐标用高德/Google 验证
  - 如果主数据源查询失败，自动切换到下一优先级的数据源

  ═══════════════════════════════════
  【工具使用参考】
  ═══════════════════════════════════
  高德 (amap):
  - maps_text_search: 按关键词搜索 POI，返回列表含经纬度
  - maps_search_detail: 获取单个 POI 详情（营业时间、评分、电话）
  - maps_around_search: 在指定坐标半径内搜索特定类型 POI
  - maps_geo / maps_regeo: 地理编码 / 逆地理编码
  - maps_weather: 查询天气预报
  - maps_driving / maps_walking / maps_transit: 路线规划及耗时
  - maps_district_search: 行政区划查询

  Google Maps (google, 海外场景):
  - google_place_search: 搜索海外 POI
  - google_place_detail: 获取海外 POI 详情
  - google_directions: 海外路线规划

  大众点评 (dianping, 餐饮补充):
  - dianping_search: 搜索餐厅，含评分和人均消费
  - dianping_detail: 获取餐厅详情（推荐菜、评价摘要）

  ═══════════════════════════════════
  【错误恢复】
  ═══════════════════════════════════
  - 工具调用失败时，先更换关键词重试一次（如"故宫"→"故宫博物院"）
  - 连续两次失败则切换到备用数据源（高德失败→Google Maps）
  - 三个数据源均失败则从已采集的候选列表中选择替代
  - 绝不能凭空捏造任何地点名称或坐标

  ═══════════════════════════════════
  【最终交付物格式要求】
  ═══════════════════════════════════
  1. 最终输出必须是纯粹合法的 JSON，严禁夹杂 Markdown 代码块、思考过程或解释文字
  2. 所有字符串值中的双引号必须转义，属性之间必须有逗号
  3. 禁止泛指词汇（"当地特色餐厅"、"网红小吃店"），每项必须是具体真实地点
  4. 每天不少于三项行程，全程不得有重复节点
  5. estimatedCost 必须基于门票、餐饮、住宿的真实价格合理估算
  6. 需要提前预约的景点在 description 中标注 "[需预约]"

  JSON 结构：
  {
    "title": "行程标题，如：北京三日游",
    "estimatedCost": 1500.0,
    "totalDays": 3,
    "userPreferences": {
      "budget": "中等",
      "dietaryRestrictions": [],
      "travelStyle": "休闲",
      "transportPreference": "公共交通"
    },
    "weatherSummary": "未来三天以晴为主，气温15-22°C",
    "dailyPlans": [
      {
        "dayNumber": 1,
        "weather": "晴 18°C",
        "theme": "历史文化探索",
        "items": [
          {
            "type": "景点|餐饮|住宿|购物",
            "name": "具体地点名称",
            "description": "详细描述：游玩时间、门票价格、交通建议等",
            "timeSlot": "09:00-12:00",
            "estimatedDuration": "3小时",
            "estimatedCost": 60.0,
            "needReservation": false,
            "location": {
              "lat": 39.9163,
              "lng": 116.3971,
              "name": "位置名称",
              "address": "详细地址"
            },
            "transitToNext": {
              "method": "地铁",
              "duration": "25分钟",
              "distance": "8.5km"
            }
          }
        ]
      }
    ],
    "tips": [
      "故宫需提前7天在官网预约门票",
      "建议携带防晒用品"
    ]
  }

# ═══ ReAct 配置 ═══
react:
  maxSteps: 30
  observationEnabled: true
  toolTimeout: 30
  thinkTimeout: 120

# ═══ MCP 工具配置 ═══
# [改进1-多数据源] 三个 MCP 远程服务，优先级: 高德 > Google > 大众点评
# [改进6-并行工具] maxConcurrent: 5 允许单轮并行调用多个工具
mcp:
  remoteServers:
    # ── 主数据源: 高德地图 MCP ──
    - name: amap
      url: "https://mcp.amap.com/sse"
      transport: sse
      params:
        key: "${AMAP_MCP_KEY}"
      protocol: jsonrpc
      protocolVersion: "2024-11-05"
      clientInfo:
        name: travelagent67
        version: "2.0"
      timeout: 120
      toolResultMaxLength: 5000
      priority: 1                    # 最高优先级

    # ── 备用数据源: Google Maps MCP (海外场景) ──
    - name: google-maps
      url: "${GOOGLE_MAPS_MCP_URL}"
      transport: sse
      params:
        key: "${GOOGLE_MAPS_API_KEY}"
      protocol: jsonrpc
      protocolVersion: "2024-11-05"
      clientInfo:
        name: travelagent67
        version: "2.0"
      timeout: 60
      toolResultMaxLength: 5000
      priority: 2                    # 高德失败时启用

    # ── 补充数据源: 大众点评 MCP (餐饮评价) ──
    - name: dianping
      url: "${DIANPING_MCP_URL}"
      transport: sse
      params:
        key: "${DIANPING_API_KEY}"
      protocol: jsonrpc
      protocolVersion: "2024-11-05"
      clientInfo:
        name: travelagent67
        version: "2.0"
      timeout: 30
      toolResultMaxLength: 3000
      priority: 3                    # 餐饮信息补充

  localTools:
    - terminate
    # [改进3-用户偏好] 记忆相关工具
    - MemoryStore
    - MemoryRecall
  policy:
    mode: auto
    maxConcurrent: 5               # [改进6] 允许单轮并行 5 个工具调用
    retryOnFail: 1                 # MCP 调用失败自动重试 1 次

# ═══ 输出格式约束 ═══
outputFormat:
  type: json
  stripCodeFence: true
  extractJsonBlock: true
  schema: ItineraryPlan
  fallbackPaths:
    - "data.itinerary"
    - "itinerary"

# ═══ 流式体验配置 ═══
# [改进2-流式体验] 通过 SSE 推送规划进度到前端
streaming:
  enabled: true
  transport: sse
  events:
    # Agent 在思考过程中输出 [STAGE:X/5] 标记时，提取并推送进度
    - type: progress
      pattern: "\\[STAGE:(\\d)/(\\d)\\]\\s*(.*)"
      payload:
        current: "$1"
        total: "$2"
        message: "$3"
    # 工具调用时推送工具名称
    - type: tool_invocation
      on: tool_call
      payload:
        tool: "${tool_name}"
        args_summary: "${args_summary}"
    # 工具完成时推送
    - type: tool_complete
      on: tool_result
      payload:
        tool: "${tool_name}"
        status: "${status}"
    # 增量编辑模式标记
    - type: edit_mode
      pattern: "\\[EDIT\\]\\s*(.*)"
      payload:
        message: "$1"

# ═══ 记忆 (用户偏好) ═══
# [改进3-用户偏好] 启用记忆，跨会话保存用户旅行偏好
memory:
  enabled: true
  strategy: local
  size: 30
  ttl: 2592000                      # 30 天过期 — 用户偏好相对稳定
  scope: user                       # 按用户隔离，不同用户的偏好互不影响
  # 预定义的偏好维度，agent 会在阶段〇采集并存储
  schema:
    budgetRange: string             # "经济|中等|高端|不限"
    dietaryRestrictions: list       # ["清真", "素食", "无辣", ...]
    travelStyle: string             # "紧凑|休闲|深度"
    transportPreference: string     # "步行为主|公共交通|打车|自驾"
    accommodationType: string       # "青旅|快捷酒店|星级酒店|民宿"
    interests: list                 # ["历史文化", "自然风景", "美食探店", "购物", ...]

# ═══ 后处理校验层 ═══
# [改进5-行程校验] 对生成的 ItineraryPlan 做结构化校验
postProcess:
  validators:
    # 营业时间校验: 检查到达时间是否在营业时段内
    - name: business_hours_check
      rule: |
        for each item in dailyPlans[*].items:
          if item.type == "景点" and item.timeSlot:
            arrival_hour = parse_hour(item.timeSlot.split("-")[0])
            assert arrival_hour >= 8 and arrival_hour <= 18,
              f"{item.name} 的到达时间 {item.timeSlot} 可能不在营业时段内"
      severity: warning

    # 天气适配校验: 雨天不应安排纯户外景点
    - name: weather_adaptation_check
      rule: |
        for each day in dailyPlans:
          if "雨" in day.weather or "暴" in day.weather:
            outdoor_count = count(item for item in day.items
                                  if item.type == "景点" and is_outdoor(item))
            assert outdoor_count <= 1,
              f"第{day.dayNumber}天预报有雨，但安排了{outdoor_count}个户外景点"
      severity: warning

    # 时间冲突检测: 检查同一天内行程时间是否重叠
    - name: time_conflict_check
      rule: |
        for each day in dailyPlans:
          time_slots = sorted(day.items, key=lambda x: x.timeSlot)
          for i in range(len(time_slots) - 1):
            end_current = parse_time(time_slots[i].timeSlot.split("-")[1])
            start_next = parse_time(time_slots[i+1].timeSlot.split("-")[0])
            assert end_current <= start_next,
              f"第{day.dayNumber}天 {time_slots[i].name} 和 {time_slots[i+1].name} 时间冲突"
      severity: error

    # 预算校验: 总费用不超过用户预算
    - name: budget_check
      rule: |
        total = sum(item.estimatedCost for day in dailyPlans for item in day.items)
        if userPreferences.budget == "经济":
          assert total <= estimatedCost * 1.1, "总费用超出预算10%以上"
      severity: warning

    # 通勤时间校验: 相邻景点间通勤不超过1小时
    - name: transit_duration_check
      rule: |
        for each day in dailyPlans:
          for each item in day.items:
            if item.transitToNext and item.transitToNext.duration:
              minutes = parse_minutes(item.transitToNext.duration)
              assert minutes <= 60,
                f"{item.name} 到下一站通勤 {item.transitToNext.duration}，超过1小时"
      severity: warning

    # 重复检测: 全行程不得有重复地点
    - name: duplicate_check
      rule: |
        all_names = [item.name for day in dailyPlans for item in day.items]
        assert len(all_names) == len(set(all_names)), "行程中存在重复地点"
      severity: error

  # 校验失败时的处理策略
  onFailure:
    error: reject_and_retry        # error 级别: 拒绝输出，要求模型重新生成
    warning: annotate               # warning 级别: 在 tips 中追加提醒，不拒绝

# ═══ 运行时引擎 ═══
# [改进7-成本控制] 使用 CEK 引擎支持分阶段成本追踪
runtime:
  engine: cek
  costBudget: 0.80                  # 总预算 $0.80 — 比 v1 的 $0.50 更充裕
  maxSteps: 200
  # 分阶段 token 预算 (软限制，超出时降级到 qwen-plus)
  stageBudgets:
    stage_0_preferences: 0.03       # 偏好采集: 简单对话，成本低
    stage_1_recon: 0.05             # 宏观侦察: 3 个并行工具调用
    stage_2_poi_search: 0.25        # POI 采集: 成本最高阶段，大量搜索
    stage_3_scheduling: 0.20        # 逐日编排: 路线验证 + 推理
    stage_4_validation: 0.15        # 行程校验: 详情查询 + 校验
    stage_5_json_output: 0.05       # JSON 生成: 最终输出
    reserve: 0.07                   # 预留: 错误重试、增量编辑
  # 超出阶段预算时的降级策略
  budgetOverflowPolicy: downgrade_model  # 切换到 qwen-plus 继续

# ═══ 安全 ═══
guard:
  dangerousCommandBlock: false
  highRiskConfirmation: false
  maxOutputLength: 80000            # v2 JSON 结构更丰富，需要更大输出限制
  retry: 3
  fallback: error

# ═══ 人设 ═══
persona:
  name: 旅行规划师
  style: professional-warm
  template: travel-planner
