"""实验 5-9:动态表单生成的意图澄清系统(★★) 核心思路 -------- 当用户请求缺少关键信息时,Agent 不是逐条追问,而是**动态生成一个自包含的 HTML 表单**(含级联显示逻辑),让用户"一次提交"补全所有澄清点;前端把表单 汇总成 JSON 交回 Agent,Agent 解析后继续任务。 本 demo 分三步验证(不依赖真实浏览器): 1) 生成澄清表单 HTML,保存为 generated_form.html; 2) 用 BeautifulSoup 结构化校验:确实含 出发城市/出发日期/旅行类型(单程,往返)/ 返程日期,且返程字段带"仅往返显示"的级联 JS 逻辑; 3) 模拟一次用户提交(构造 JSON),喂回 Agent,Agent 解析后打印订票摘要。 两种运行模式(机制完全一致,只是"谁来写表单代码"不同): * 默认(在线):让 Agent 真实调用 OpenAI 生成表单 HTML,需 OPENAI_API_KEY; * --offline :不调用 LLM,用内置机票 schema **确定性渲染**级联表单,无需 API Key。 离线渲染出的表单同样是真实可用、可在浏览器打开、含两类级联逻辑(显示/隐藏 + 动态更新可选项)的自包含 HTML。 运行: python demo.py # 在线:Agent 调 OpenAI 生成(需 API Key) python demo.py --offline # 离线:内置 schema 确定性渲染,无需 API Key python demo.py --offline --serve # 离线渲染后启动本地服务,浏览器实时体验级联/提交 python demo.py --help # 查看全部参数 环境变量: OPENAI_API_KEY (在线模式必填;未设置时自动回落到 --offline) OPENAI_BASE_URL (可选,切换到兼容 OpenAI 协议的服务) MODEL (可选,默认 gpt-5.6-luna) """ import os import re import json import argparse from bs4 import BeautifulSoup # openai 仅在线模式需要;离线模式不导入,缺包也能跑(延迟到用时再 import) # 加载 .env(若存在),方便本地运行 try: from dotenv import load_dotenv load_dotenv() except Exception: # dotenv 是可选依赖 pass SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__)) USER_REQUEST = "我想订一张去北京的机票" # --------------------------------------------------------------------------- # 配置(在线模式) # --------------------------------------------------------------------------- OPENROUTER_BASE_URL = "https://openrouter.ai/api/v1" def _map_to_openrouter_model(model: str) -> str: """把直连模型名映射为 OpenRouter 上的 id(非可映射 id 统一兜底到当前廉价旗舰)。""" if not model or "/" in model: return model or "openai/gpt-5.6-luna" m = model.lower() if m.startswith(("gpt-", "o1", "o3", "o4")): return "openai/" + model if m.startswith("claude"): if "haiku" in m: return "anthropic/claude-haiku-4.5" if "sonnet" in m: return "anthropic/claude-sonnet-4.6" return "anthropic/claude-opus-4.8" if m.startswith("gemini"): return "google/" + model return "openai/gpt-5.6-luna" def build_client_and_model(model_override=None): """构造 OpenAI 客户端与模型名,含通用 OpenRouter 兜底。""" from openai import OpenAI # 延迟导入:离线模式无需安装/配置 openai model = model_override or os.getenv("MODEL", "gpt-5.6-luna") api_key = os.getenv("OPENAI_API_KEY") base_url = os.getenv("OPENAI_BASE_URL") orkey = os.getenv("OPENROUTER_API_KEY") # 无直连 key,或默认 gpt-5.x(直连需组织实名认证)时改走 OpenRouter。 prefer_or = bool(orkey) and (model or "").lower().startswith("gpt-5") if prefer_or or (not api_key and orkey): api_key, base_url, model = orkey, OPENROUTER_BASE_URL, _map_to_openrouter_model(model) if not api_key: raise SystemExit("未找到 OPENAI_API_KEY(或 OPENROUTER_API_KEY 兜底),请先在环境变量或 .env 中设置,或改用 --offline。") client = ( OpenAI(api_key=api_key, base_url=base_url) if base_url else OpenAI(api_key=api_key) ) return client, model def _temp_for(model): """推理模型(gpt-5 / o 系列等)不接受 temperature=0。""" return (1 if any(k in (model or "").lower() for k in ("gpt-5", "o1", "o3", "o4", "thinking", "reasoner", "kimi-k3")) else 0) # --------------------------------------------------------------------------- # 步骤 1(在线):让 Agent 生成澄清表单 # --------------------------------------------------------------------------- FORM_SYSTEM_PROMPT = """你是一个"意图澄清"助手。用户会给出一个信息不完整的请求, 你的任务不是直接追问,而是**生成一个自包含的 HTML 表单**,让用户一次性补全所有 缺失信息。 严格要求(订机票场景): 1. 表单必须包含以下字段,字段的 name 属性必须使用给定的英文标识: - 出发城市:文本输入框,name="departure_city" - 出发日期:日期选择器 ,name="departure_date" - 旅行类型:单选按钮 ,两个选项 value="one_way"(单程)和 value="round_trip"(往返) - 返程日期:日期选择器,name="return_date",放在 id="return_date_field" 的 容器里 2. **级联逻辑(关键)**:返程日期字段默认隐藏,只有当旅行类型选择"往返" (round_trip) 时才通过 JavaScript 显示出来;选回"单程"时再次隐藏。 3. 提交时用 JavaScript 阻止默认提交,把所有字段汇总为一个 JSON 对象, key 使用上面的英文 name,并显示在 id="result" 的元素里 (例如
)。 4. 输出必须是**完整、自包含**的 HTML(含原始请求:__REQUEST__ —— 请一次性补全以下信息(返程日期仅在选择"往返"时出现;行李额度随舱位变化)。