4
0

planner.py 50 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440
  1. """
  2. planner.py - AI Agent 通用 Planner 核心模块
  3. 该模块负责根据世界状态、用户意图、可用能力生成标准化的 Plan 对象。
  4. 职责边界:
  5. - 接收输入:world_snapshot, user_intent, available_tools, domain_rules, planner_mode
  6. - 输出:标准 Plan 对象
  7. - 不执行 capability
  8. - 不依赖 ROS2
  9. - 不包含场景硬编码逻辑
  10. Planner 支持三种模式:
  11. - rule:纯规则生成
  12. - llm:LLM 生成(调用 llm_client)
  13. - hybrid:优先规则,复杂情况调用 LLM
  14. 使用方式:
  15. from planner import Planner, PlannerConfig
  16. from tool_protocol import RiskLevel
  17. config = PlannerConfig(default_risk_level=RiskLevel.LOW)
  18. planner = Planner(config)
  19. plan = planner.generate_plan(
  20. world_snapshot={"temperature": 35},
  21. user_intent="降温",
  22. available_tools=["adjust_fan", "speak"],
  23. )
  24. # 使用 LLM 模式
  25. plan = planner.generate_plan(
  26. world_snapshot={"temperature": 35},
  27. user_intent="降温",
  28. available_tools=["adjust_fan", "speak"],
  29. planner_mode="llm",
  30. )
  31. """
  32. from __future__ import annotations
  33. import time
  34. import uuid
  35. from dataclasses import dataclass, field
  36. from typing import Any
  37. from tool_protocol import (
  38. Plan,
  39. PlanStep,
  40. RiskLevel,
  41. PlanStatus,
  42. StepStatus,
  43. ToolCallType,
  44. )
  45. # =============================================================================
  46. # 通用意图映射层
  47. # =============================================================================
  48. # 通用意图关键词到 action 名称的映射
  49. # 用途:当用户意图包含某个关键词时,映射到对应的 action
  50. # 可扩展:后续可通过配置文件或知识库动态加载
  51. INTENT_TO_ACTION: dict[str, str] = {
  52. # 动作类
  53. "喂": "feed",
  54. "喂食": "feed",
  55. "喂养": "feed",
  56. "打开风扇": "adjust_fan",
  57. "关闭风扇": "adjust_fan",
  58. "调整风扇": "adjust_fan",
  59. "降温": "adjust_fan",
  60. "升温": "adjust_fan",
  61. "开灯": "control_light",
  62. "关灯": "control_light",
  63. "调整灯光": "control_light",
  64. "打开": "turn_on",
  65. "关闭": "turn_off",
  66. "启动": "turn_on",
  67. "停止": "turn_off",
  68. "调节": "adjust",
  69. "移动": "move",
  70. "前往": "move",
  71. "走到": "move",
  72. # 查询类
  73. "查询": "query",
  74. "查看": "query",
  75. "检查": "inspect",
  76. "巡检": "inspect",
  77. "监测": "monitor",
  78. "监控": "monitor",
  79. # 通信类
  80. "播报": "speak",
  81. "报告": "speak",
  82. "说": "speak",
  83. "告诉": "speak",
  84. "通知": "notify",
  85. "提醒": "remind",
  86. "警告": "warn",
  87. "询问": "ask",
  88. }
  89. # action 到 ToolCallType 的默认映射
  90. ACTION_TO_TOOL_CALL_TYPE: dict[str, ToolCallType] = {
  91. "feed": ToolCallType.EXECUTE,
  92. "adjust_fan": ToolCallType.EXECUTE,
  93. "control_light": ToolCallType.EXECUTE,
  94. "turn_on": ToolCallType.EXECUTE,
  95. "turn_off": ToolCallType.EXECUTE,
  96. "adjust": ToolCallType.EXECUTE,
  97. "move": ToolCallType.EXECUTE,
  98. "query": ToolCallType.QUERY_WORLD,
  99. "inspect": ToolCallType.QUERY_WORLD,
  100. "monitor": ToolCallType.QUERY_WORLD,
  101. "speak": ToolCallType.SPEAK,
  102. "notify": ToolCallType.SPEAK,
  103. "remind": ToolCallType.SPEAK,
  104. "warn": ToolCallType.SPEAK,
  105. "ask": ToolCallType.ASK_USER,
  106. }
  107. # 高风险关键词列表(通用)
  108. HIGH_RISK_KEYWORDS: list[str] = [
  109. "紧急", "危险", "停止", "关机", "断电", "全部", "清空",
  110. "emergency", "danger", "stop", "shutdown", "all", "clear",
  111. ]
  112. # 中风险关键词列表(通用)
  113. MEDIUM_RISK_KEYWORDS: list[str] = [
  114. "调整", "改变", "修改", "设置", "配置",
  115. "adjust", "change", "modify", "set", "configure",
  116. ]
  117. # =============================================================================
  118. # Planner 配置
  119. # =============================================================================
  120. @dataclass
  121. class PlannerConfig:
  122. """Planner 配置类
  123. 用于配置 Planner 的行为参数。
  124. 属性:
  125. default_risk_level: 默认风险等级
  126. require_confirmation_on_medium_risk: 中风险是否需要确认
  127. require_confirmation_on_high_risk: 高风险是否需要确认
  128. max_plan_steps: 单个 Plan 最大步骤数
  129. default_source: 默认计划来源
  130. 示例:
  131. >>> config = PlannerConfig(
  132. ... default_risk_level=RiskLevel.LOW,
  133. ... require_confirmation_on_medium_risk=True,
  134. ... require_confirmation_on_high_risk=True,
  135. ... max_plan_steps=10,
  136. ... default_source="hybrid"
  137. ... )
  138. """
  139. default_risk_level: RiskLevel = RiskLevel.LOW
  140. require_confirmation_on_medium_risk: bool = True
  141. require_confirmation_on_high_risk: bool = True
  142. max_plan_steps: int = 10
  143. default_source: str = "hybrid"
  144. # =============================================================================
  145. # Planner 核心类
  146. # =============================================================================
  147. class Planner:
  148. """通用 Planner 类
  149. 根据世界状态、用户意图、可用能力生成标准化 Plan 对象。
  150. 属性:
  151. config: Planner 配置
  152. intent_to_action: 意图到 action 的映射字典
  153. action_to_tool_type: action 到 ToolCallType 的映射字典
  154. 示例:
  155. >>> planner = Planner()
  156. >>> plan = planner.generate_plan(
  157. ... world_snapshot={"temperature": 30},
  158. ... user_intent="降温",
  159. ... available_tools=["adjust_fan", "speak"]
  160. ... )
  161. """
  162. def __init__(
  163. self,
  164. config: PlannerConfig | None = None,
  165. intent_to_action: dict[str, str] | None = None,
  166. action_to_tool_type: dict[str, ToolCallType] | None = None,
  167. ) -> None:
  168. """初始化 Planner
  169. Args:
  170. config: Planner 配置,None 时使用默认配置
  171. intent_to_action: 自定义意图映射,None 时使用内置映射
  172. action_to_tool_type: 自定义 action 类型映射,None 时使用内置映射
  173. """
  174. self.config = config or PlannerConfig()
  175. self.intent_to_action = intent_to_action or INTENT_TO_ACTION.copy()
  176. self.action_to_tool_type = action_to_tool_type or ACTION_TO_TOOL_CALL_TYPE.copy()
  177. self._cached_normalized_snapshot: dict[str, Any] | None = None
  178. self._cached_snapshot_raw: dict[str, Any] | None = None
  179. def generate_plan(
  180. self,
  181. world_snapshot: dict[str, Any],
  182. user_intent: str,
  183. available_tools: list[str],
  184. domain_rules: dict[str, Any] | None = None,
  185. planner_mode: str = "hybrid",
  186. ) -> Plan:
  187. """生成执行计划
  188. 根据输入信息生成标准化的 Plan 对象。
  189. Args:
  190. world_snapshot: 世界状态快照
  191. user_intent: 用户意图描述
  192. available_tools: 可用能力列表
  193. domain_rules: 领域规则约束,None 表示无约束
  194. planner_mode: 规划模式,可选 "rule" / "llm" / "hybrid"
  195. Returns:
  196. 标准化的 Plan 对象
  197. Raises:
  198. ValueError: 当 planner_mode 不合法
  199. """
  200. if not user_intent or not user_intent.strip():
  201. return self._generate_empty_intent_plan(world_snapshot, available_tools)
  202. mode = planner_mode.lower()
  203. if mode == "rule":
  204. return self._generate_rule_based_plan(
  205. world_snapshot, user_intent, available_tools, domain_rules
  206. )
  207. elif mode == "llm":
  208. return self._generate_llm_plan(
  209. world_snapshot, user_intent, available_tools, domain_rules
  210. )
  211. elif mode == "hybrid":
  212. return self._generate_hybrid_plan(
  213. world_snapshot, user_intent, available_tools, domain_rules
  214. )
  215. else:
  216. raise ValueError(f"无效的 planner_mode: {planner_mode},有效值为: rule/llm/hybrid")
  217. def _generate_rule_based_plan(
  218. self,
  219. world_snapshot: dict[str, Any],
  220. user_intent: str,
  221. available_tools: list[str],
  222. domain_rules: dict[str, Any] | None,
  223. ) -> Plan:
  224. """基于规则生成 Plan
  225. 内部方法,使用规则引擎生成计划。
  226. Args:
  227. world_snapshot: 世界状态快照
  228. user_intent: 用户意图描述
  229. available_tools: 可用能力列表
  230. domain_rules: 领域规则约束
  231. Returns:
  232. Plan 对象
  233. """
  234. normalized = self._normalize_world_snapshot(world_snapshot)
  235. action = self._map_intent_to_action(user_intent)
  236. context = self._extract_recent_context(normalized)
  237. requires_confirm, confirm_msg = self._needs_confirmation(
  238. normalized, user_intent, action, domain_rules
  239. )
  240. risk_level = self._assess_risk(user_intent, action, normalized, domain_rules)
  241. plan_id = f"plan_{uuid.uuid4().hex[:8]}"
  242. if action is None:
  243. return self._generate_unknown_intent_plan(
  244. plan_id, user_intent, normalized, available_tools, risk_level, context
  245. )
  246. if not self._validate_tool_available(action, available_tools):
  247. return self._generate_tool_unavailable_plan(
  248. plan_id, user_intent, action, normalized, risk_level, context
  249. )
  250. return self._generate_executable_plan(
  251. plan_id, user_intent, action, normalized,
  252. risk_level, requires_confirm, confirm_msg, context
  253. )
  254. def _generate_llm_plan(
  255. self,
  256. world_snapshot: dict[str, Any],
  257. user_intent: str,
  258. available_tools: list[str],
  259. domain_rules: dict[str, Any] | None,
  260. ) -> Plan:
  261. """LLM 规划器实现
  262. 内部方法,调用 LLM 生成计划。
  263. Args:
  264. world_snapshot: 世界状态快照
  265. user_intent: 用户意图描述
  266. available_tools: 可用能力列表
  267. domain_rules: 领域规则约束
  268. Returns:
  269. Plan 对象
  270. """
  271. # 导入 LLM 客户端(延迟导入避免循环依赖)
  272. try:
  273. from llm_client import LLMPlannerClient
  274. llm_client = LLMPlannerClient()
  275. except ImportError:
  276. # LLM 客户端不可用,返回安全 fallback
  277. return self._generate_safe_fallback_plan(
  278. user_intent=user_intent,
  279. reason="llm_client_unavailable",
  280. )
  281. # 提取上下文(使用 normalize 后的结果)
  282. normalized = self._normalize_world_snapshot(world_snapshot)
  283. context = self._extract_recent_context(normalized)
  284. # 构造 Planner 配置
  285. planner_config = {
  286. "default_risk_level": self.config.default_risk_level.value,
  287. "require_confirmation_on_medium_risk": self.config.require_confirmation_on_medium_risk,
  288. "require_confirmation_on_high_risk": self.config.require_confirmation_on_high_risk,
  289. "max_plan_steps": self.config.max_plan_steps,
  290. }
  291. try:
  292. # 调用 LLM 生成 Plan JSON
  293. plan_json = llm_client.generate_plan_json(
  294. user_intent=user_intent,
  295. world_snapshot=normalized,
  296. available_tools=available_tools,
  297. domain_rules=domain_rules,
  298. planner_config=planner_config,
  299. )
  300. # 使用 Plan.from_dict() 转换
  301. plan = Plan.from_dict(plan_json)
  302. return plan
  303. except Exception as e:
  304. # LLM 调用失败,返回安全 fallback
  305. return self._generate_safe_fallback_plan(
  306. user_intent=user_intent,
  307. reason=f"llm_error: {e}",
  308. )
  309. def _generate_hybrid_plan(
  310. self,
  311. world_snapshot: dict[str, Any],
  312. user_intent: str,
  313. available_tools: list[str],
  314. domain_rules: dict[str, Any] | None,
  315. ) -> Plan:
  316. """混合模式生成 Plan
  317. 内部方法,优先使用规则,简单情况直接生成,复杂情况调用 LLM。
  318. Args:
  319. world_snapshot: 世界状态快照
  320. user_intent: 用户意图描述
  321. available_tools: 可用能力列表
  322. domain_rules: 领域规则约束
  323. Returns:
  324. Plan 对象
  325. """
  326. normalized = self._normalize_world_snapshot(world_snapshot)
  327. action = self._map_intent_to_action(user_intent)
  328. context = self._extract_recent_context(normalized)
  329. # 判断是否需要委托给 LLM
  330. if self._should_delegate_to_llm(
  331. normalized, user_intent, available_tools, domain_rules, action
  332. ):
  333. return self._generate_llm_plan(
  334. normalized, user_intent, available_tools, domain_rules
  335. )
  336. # 规则可处理,直接生成
  337. requires_confirm, confirm_msg = self._needs_confirmation(
  338. normalized, user_intent, action, domain_rules
  339. )
  340. risk_level = self._assess_risk(user_intent, action, normalized, domain_rules)
  341. plan_id = f"plan_{uuid.uuid4().hex[:8]}"
  342. if action is None:
  343. return self._generate_unknown_intent_plan(
  344. plan_id, user_intent, normalized, available_tools, risk_level, context
  345. )
  346. if not self._validate_tool_available(action, available_tools):
  347. return self._generate_tool_unavailable_plan(
  348. plan_id, user_intent, action, normalized, risk_level, context
  349. )
  350. return self._generate_executable_plan(
  351. plan_id, user_intent, action, normalized,
  352. risk_level, requires_confirm, confirm_msg, context
  353. )
  354. def _generate_executable_plan(
  355. self,
  356. plan_id: str,
  357. user_intent: str,
  358. action: str,
  359. world_snapshot: dict[str, Any],
  360. risk_level: RiskLevel,
  361. requires_confirm: bool,
  362. confirm_msg: str | None,
  363. context: dict[str, Any],
  364. ) -> Plan:
  365. """生成可执行的 Plan
  366. 内部方法,生成包含具体执行步骤的 Plan。
  367. 当 requires_confirm == True 时,生成 ASK_USER/SPEAK 询问步骤而非 EXECUTE 执行步骤,
  368. 以确保 Executor 不会在用户确认前误执行风险动作。
  369. Args:
  370. plan_id: 计划 ID
  371. user_intent: 用户意图
  372. action: 动作名称
  373. world_snapshot: 世界状态(应传入 normalized 后的结果)
  374. risk_level: 风险等级
  375. requires_confirm: 是否需要确认
  376. confirm_msg: 确认消息
  377. context: 提取的上下文
  378. Returns:
  379. 可执行的 Plan 对象
  380. """
  381. reasoning_parts = [
  382. f"用户意图: {user_intent}",
  383. f"匹配动作: {action}",
  384. f"风险等级: {risk_level.value}",
  385. ]
  386. if context:
  387. reasoning_parts.append(f"相关上下文: {context}")
  388. if requires_confirm and confirm_msg:
  389. reasoning_parts.append(f"需要确认: {confirm_msg}")
  390. plan = Plan(
  391. plan_id=plan_id,
  392. goal=user_intent,
  393. reasoning="; ".join(reasoning_parts),
  394. risk_level=risk_level,
  395. requires_confirmation=requires_confirm,
  396. confirmation_message=confirm_msg,
  397. status=PlanStatus.CREATED,
  398. source=self.config.default_source,
  399. )
  400. # 当需要确认时,生成询问/告知步骤而非执行步骤
  401. if requires_confirm:
  402. step = PlanStep(
  403. step_id=1,
  404. action="ask_user",
  405. tool_call_type=ToolCallType.ASK_USER,
  406. parameters={
  407. "user_intent": user_intent,
  408. "context": context,
  409. "suggested_action": action,
  410. "question": confirm_msg or f"您确定要执行 '{action}' 吗?",
  411. },
  412. description=f"询问用户确认: {action}",
  413. requires_confirmation=True,
  414. confirmation_message=confirm_msg,
  415. )
  416. else:
  417. # 不需要确认时,生成正常的执行步骤
  418. step = PlanStep(
  419. step_id=1,
  420. action=action,
  421. tool_call_type=self.action_to_tool_type.get(action, ToolCallType.EXECUTE),
  422. parameters={"user_intent": user_intent, "context": context},
  423. description=f"执行动作: {action}",
  424. )
  425. plan.add_step(step)
  426. return plan
  427. def _generate_tool_unavailable_plan(
  428. self,
  429. plan_id: str,
  430. user_intent: str,
  431. action: str,
  432. world_snapshot: dict[str, Any],
  433. risk_level: RiskLevel,
  434. context: dict[str, Any],
  435. ) -> Plan:
  436. """生成工具不可用时的 Plan
  437. 内部方法,当请求的能力不在可用列表中时调用。
  438. Args:
  439. plan_id: 计划 ID
  440. user_intent: 用户意图
  441. action: 尝试的动作名称
  442. world_snapshot: 世界状态
  443. risk_level: 风险等级
  444. context: 提取的上下文
  445. Returns:
  446. 提示能力不可用的 Plan 对象
  447. """
  448. plan = Plan(
  449. plan_id=plan_id,
  450. goal=f"处理不可用能力请求: {user_intent}",
  451. reasoning=(
  452. f"用户请求执行动作 '{action}',但该能力当前不可用。"
  453. f"用户意图: {user_intent}"
  454. ),
  455. risk_level=RiskLevel.LOW,
  456. requires_confirmation=False,
  457. status=PlanStatus.CREATED,
  458. source=self.config.default_source,
  459. )
  460. step = PlanStep(
  461. step_id=1,
  462. action="speak",
  463. tool_call_type=ToolCallType.SPEAK,
  464. parameters={
  465. "message": f"抱歉,当前系统不支持 '{action}' 操作。"
  466. f"您的请求 '{user_intent}' 无法完成。",
  467. "level": "info",
  468. },
  469. description=f"通知用户能力不可用: {action}",
  470. )
  471. plan.add_step(step)
  472. return plan
  473. def _generate_unknown_intent_plan(
  474. self,
  475. plan_id: str,
  476. user_intent: str,
  477. world_snapshot: dict[str, Any],
  478. available_tools: list[str],
  479. risk_level: RiskLevel,
  480. context: dict[str, Any],
  481. ) -> Plan:
  482. """生成意图不清晰时的 Plan
  483. 内部方法,当无法从用户意图中识别出具体动作时调用。
  484. Args:
  485. plan_id: 计划 ID
  486. user_intent: 用户意图
  487. world_snapshot: 世界状态
  488. available_tools: 可用能力列表
  489. risk_level: 风险等级
  490. context: 提取的上下文
  491. Returns:
  492. 需要进一步确认意图的 Plan 对象
  493. """
  494. plan = Plan(
  495. plan_id=plan_id,
  496. goal=f"澄清用户意图: {user_intent}",
  497. reasoning=(
  498. f"无法从用户意图 '{user_intent}' 中识别出具体动作。"
  499. f"可用能力: {available_tools}"
  500. ),
  501. risk_level=RiskLevel.LOW,
  502. requires_confirmation=False,
  503. status=PlanStatus.CREATED,
  504. source=self.config.default_source,
  505. )
  506. query_step = PlanStep(
  507. step_id=1,
  508. action="clarify_intent",
  509. tool_call_type=ToolCallType.ASK_USER,
  510. parameters={
  511. "original_intent": user_intent,
  512. "available_actions": available_tools,
  513. },
  514. description=f"询问用户确认具体意图: {user_intent}",
  515. requires_confirmation=True,
  516. confirmation_message=f"您是想要执行以下哪个操作?{', '.join(available_tools[:5])}",
  517. )
  518. plan.add_step(query_step)
  519. return plan
  520. def _generate_empty_intent_plan(
  521. self,
  522. world_snapshot: dict[str, Any],
  523. available_tools: list[str],
  524. ) -> Plan:
  525. """生成空意图的 Plan
  526. 内部方法,当用户意图为空时调用。
  527. Args:
  528. world_snapshot: 世界状态
  529. available_tools: 可用能力列表
  530. Returns:
  531. 提示输入意图的 Plan 对象
  532. """
  533. plan = Plan(
  534. plan_id=f"plan_{uuid.uuid4().hex[:8]}",
  535. goal="等待用户输入",
  536. reasoning="用户未提供有效意图",
  537. risk_level=RiskLevel.LOW,
  538. requires_confirmation=False,
  539. status=PlanStatus.CREATED,
  540. source=self.config.default_source,
  541. )
  542. step = PlanStep(
  543. step_id=1,
  544. action="speak",
  545. tool_call_type=ToolCallType.SPEAK,
  546. parameters={"message": "您好,请告诉我您想要执行的操作。"},
  547. description="等待用户输入有效意图",
  548. )
  549. plan.add_step(step)
  550. return plan
  551. def _needs_confirmation(
  552. self,
  553. world_snapshot: dict[str, Any],
  554. user_intent: str,
  555. action: str | None,
  556. domain_rules: dict[str, Any] | None,
  557. ) -> tuple[bool, str | None]:
  558. """判断是否需要用户确认
  559. 根据世界状态、用户意图、动作类型和领域规则判断。
  560. 优先使用 _normalize_world_snapshot() 转换后的结果。
  561. Args:
  562. world_snapshot: 世界状态快照
  563. user_intent: 用户意图
  564. action: 识别的动作名称
  565. domain_rules: 领域规则
  566. Returns:
  567. (是否需要确认, 确认消息)
  568. """
  569. if action is None:
  570. return True, "无法识别您的意图,请确认您想要的操作。"
  571. normalized = self._normalize_world_snapshot(world_snapshot)
  572. if self._has_world_conflict(normalized, action, domain_rules):
  573. return True, f"检测到与当前状态的潜在冲突,请确认是否继续执行 '{action}'?"
  574. risk_level = self._assess_risk(user_intent, action, normalized, domain_rules)
  575. if risk_level == RiskLevel.HIGH and self.config.require_confirmation_on_high_risk:
  576. return True, f"该操作风险较高 '{action}',是否确认执行?"
  577. if risk_level == RiskLevel.MEDIUM and self.config.require_confirmation_on_medium_risk:
  578. return True, f"执行操作 '{action}' 前,请确认。"
  579. return False, None
  580. def _has_world_conflict(
  581. self,
  582. world_snapshot: dict[str, Any],
  583. action: str,
  584. domain_rules: dict[str, Any] | None,
  585. ) -> bool:
  586. """检查世界状态是否存在冲突
  587. 内部方法,检测可能导致执行冲突的状态。
  588. 优先使用 _normalize_world_snapshot() 转换后的结果。
  589. Args:
  590. world_snapshot: 世界状态快照
  591. action: 动作名称
  592. domain_rules: 领域规则
  593. Returns:
  594. 是否存在冲突
  595. """
  596. normalized = self._normalize_world_snapshot(world_snapshot)
  597. if normalized.get("actuator_status"):
  598. actuator = normalized.get("actuator_status", {})
  599. if isinstance(actuator, dict):
  600. for key in ["unavailable", "error", "offline"]:
  601. if key in actuator and actuator[key]:
  602. return True
  603. if normalized.get("recent_actions"):
  604. recent = normalized.get("recent_actions", [])
  605. if isinstance(recent, list) and len(recent) > 0:
  606. last_action = recent[-1] if recent else None
  607. if last_action and last_action.get("action") == action:
  608. return True
  609. return False
  610. def _assess_risk(
  611. self,
  612. user_intent: str,
  613. action: str | None,
  614. world_snapshot: dict[str, Any],
  615. domain_rules: dict[str, Any] | None,
  616. ) -> RiskLevel:
  617. """评估风险等级
  618. 内部方法,根据多个因素综合评估风险。
  619. 优先使用 _normalize_world_snapshot() 转换后的结果。
  620. Args:
  621. user_intent: 用户意图
  622. action: 动作名称
  623. world_snapshot: 世界状态
  624. domain_rules: 领域规则
  625. Returns:
  626. 风险等级
  627. """
  628. normalized = self._normalize_world_snapshot(world_snapshot)
  629. if action is None:
  630. return RiskLevel.LOW
  631. for keyword in HIGH_RISK_KEYWORDS:
  632. if keyword in user_intent:
  633. return RiskLevel.HIGH
  634. for keyword in MEDIUM_RISK_KEYWORDS:
  635. if keyword in user_intent:
  636. return RiskLevel.MEDIUM
  637. if domain_rules and "high_risk_actions" in domain_rules:
  638. if action in domain_rules["high_risk_actions"]:
  639. return RiskLevel.HIGH
  640. return self.config.default_risk_level
  641. def _should_delegate_to_llm(
  642. self,
  643. world_snapshot: dict[str, Any],
  644. user_intent: str,
  645. available_tools: list[str],
  646. domain_rules: dict[str, Any] | None,
  647. action: str | None,
  648. ) -> bool:
  649. """判断是否应该委托给 LLM
  650. 内部方法,根据多个因素判断是否需要 LLM 介入。
  651. 委托条件:
  652. 1. action 无法明确映射(意图复杂)
  653. 2. 世界状态存在冲突
  654. 3. 需要结合领域规则做高层判断
  655. 4. 多步骤规划需求
  656. 5. 意图语义模糊但可理解
  657. Args:
  658. world_snapshot: 世界状态快照
  659. user_intent: 用户意图描述
  660. available_tools: 可用能力列表
  661. domain_rules: 领域规则约束
  662. action: 识别的动作(None 表示未识别)
  663. Returns:
  664. 是否应该委托给 LLM
  665. """
  666. domain_rules = domain_rules or {}
  667. # 条件1:action 无法明确映射
  668. if action is None:
  669. # 检查是否有任何关键词匹配
  670. has_partial_match = False
  671. for keyword in self.intent_to_action.keys():
  672. if keyword in user_intent:
  673. has_partial_match = True
  674. break
  675. # 有部分匹配但无法确定具体 action,需要 LLM
  676. if has_partial_match:
  677. return True
  678. # 条件2:世界状态存在冲突
  679. if self._has_world_conflict(world_snapshot, action, domain_rules):
  680. # 检查冲突是否严重到需要 LLM
  681. if self._is_complex_conflict(world_snapshot, action):
  682. return True
  683. # 条件3:涉及领域规则限制
  684. if domain_rules:
  685. high_risk_actions = domain_rules.get("high_risk_actions", [])
  686. medium_risk_actions = domain_rules.get("medium_risk_actions", [])
  687. if action in high_risk_actions or action in medium_risk_actions:
  688. # 风险动作需要更复杂判断
  689. return True
  690. # 条件4:多步骤意图(包含连接词)
  691. multi_step_keywords = ["然后", "接下来", "之后", "再", "和", "以及", "并且", "再然后"]
  692. if any(kw in user_intent for kw in multi_step_keywords):
  693. return True
  694. # 条件5:意图包含多个动作词
  695. action_count = 0
  696. for keyword in self.intent_to_action.keys():
  697. if keyword in user_intent:
  698. action_count += 1
  699. if action_count > 1:
  700. return True
  701. # 条件6:意图不完整但包含上下文暗示
  702. incomplete_indicators = ["它", "那个", "这个", "刚才", "上次", "再"]
  703. if any(ind in user_intent for ind in incomplete_indicators):
  704. if action is None:
  705. return True
  706. return False
  707. def _is_complex_conflict(
  708. self,
  709. world_snapshot: dict[str, Any],
  710. action: str | None,
  711. ) -> bool:
  712. """判断是否为复杂冲突
  713. 复杂冲突需要 LLM 介入判断。
  714. 优先使用 _normalize_world_snapshot() 转换后的结果。
  715. Args:
  716. world_snapshot: 世界状态
  717. action: 动作名称
  718. Returns:
  719. 是否为复杂冲突
  720. """
  721. normalized = self._normalize_world_snapshot(world_snapshot)
  722. # 检查 recent_actions 中是否有相同动作
  723. recent_actions = normalized.get("recent_actions", [])
  724. if isinstance(recent_actions, list) and len(recent_actions) > 0:
  725. last_action = recent_actions[-1] if recent_actions else {}
  726. if isinstance(last_action, dict) and last_action.get("action") == action:
  727. # 相同动作,检查时间间隔
  728. if "timestamp" in last_action:
  729. time_diff = time.time() - last_action.get("timestamp", 0)
  730. # 短时间内重复动作视为复杂冲突
  731. if time_diff < 600: # 10分钟内
  732. return True
  733. # 检查是否有多个警告
  734. warnings = normalized.get("warnings", [])
  735. if isinstance(warnings, list) and len(warnings) > 1:
  736. return True
  737. return False
  738. def _generate_safe_fallback_plan(
  739. self,
  740. user_intent: str,
  741. reason: str = "",
  742. ) -> Plan:
  743. """生成安全的 fallback Plan
  744. 当 LLM 调用失败时,返回一个安全的 fallback plan。
  745. 优先使用 ASK_USER 或 SPEAK,不直接执行风险动作。
  746. Args:
  747. user_intent: 用户原始意图
  748. reason: 回退原因
  749. Returns:
  750. 安全的 Plan 对象
  751. """
  752. plan = Plan(
  753. plan_id=f"plan_fallback_{uuid.uuid4().hex[:8]}",
  754. goal=f"无法处理的请求: {user_intent}",
  755. reasoning=f"LLM 处理失败或响应无效 ({reason}),返回安全 fallback",
  756. risk_level=RiskLevel.LOW,
  757. requires_confirmation=False,
  758. confirmation_message=None,
  759. status=PlanStatus.CREATED,
  760. source="llm_fallback",
  761. metadata={"fallback": True, "reason": reason},
  762. )
  763. step = PlanStep(
  764. step_id=1,
  765. action="ask_user",
  766. tool_call_type=ToolCallType.ASK_USER,
  767. parameters={
  768. "question": f"抱歉,我无法理解或处理您的请求: {user_intent}。请重新描述您的需求。",
  769. },
  770. preconditions={},
  771. fallback=None,
  772. status=StepStatus.PENDING,
  773. description="询问用户澄清意图",
  774. requires_confirmation=False,
  775. confirmation_message=None,
  776. metadata={"fallback_reason": reason},
  777. )
  778. plan.add_step(step)
  779. return plan
  780. def _validate_tool_available(self, tool_name: str, available_tools: list[str]) -> bool:
  781. """验证工具是否可用
  782. 内部方法,检查请求的动作是否在可用能力列表中。
  783. Args:
  784. tool_name: 工具/动作名称
  785. available_tools: 可用能力列表
  786. Returns:
  787. 是否可用
  788. """
  789. if not available_tools:
  790. return False
  791. return tool_name in available_tools
  792. def _normalize_world_snapshot(self, world_snapshot: dict[str, Any]) -> dict[str, Any]:
  793. """将真实 /world/snapshot 结构统一转换为 Planner 扁平上下文
  794. 真实 world_snapshot 结构可能包含嵌套的 environment、system、entities 等字段,
  795. 此方法将其转换为扁平的、易于访问的格式。
  796. 对齐真实结构:
  797. - environment: temperature, humidity, light_level, warnings, errors
  798. - system: device_status, actuator_status, mode
  799. - entities: 与 actuator/device/recent action 相关的信息
  800. Args:
  801. world_snapshot: 原始世界状态快照
  802. Returns:
  803. 扁平的 normalized dict
  804. """
  805. # 如果快照没有变化,使用缓存
  806. if self._cached_snapshot_raw is world_snapshot and self._cached_normalized_snapshot is not None:
  807. return self._cached_normalized_snapshot
  808. normalized: dict[str, Any] = {
  809. "temperature": None,
  810. "humidity": None,
  811. "light_level": None,
  812. "warnings": [],
  813. "errors": [],
  814. "device_status": {},
  815. "actuator_status": {},
  816. "mode": None,
  817. "recent_actions": [],
  818. "raw_entities": {},
  819. }
  820. # 处理 environment 字段(常见于 /world/snapshot)
  821. environment = world_snapshot.get("environment", {})
  822. if isinstance(environment, dict):
  823. normalized["temperature"] = environment.get("temperature") or environment.get("temperature_ambient")
  824. normalized["humidity"] = environment.get("humidity") or environment.get("humidity_ambient")
  825. normalized["light_level"] = environment.get("light_level") or environment.get("illuminance")
  826. normalized["warnings"] = environment.get("warnings", [])
  827. normalized["errors"] = environment.get("errors", [])
  828. # 处理 system 字段
  829. system = world_snapshot.get("system", {})
  830. if isinstance(system, dict):
  831. normalized["device_status"] = system.get("device_status", {})
  832. normalized["actuator_status"] = system.get("actuator_status", {})
  833. normalized["mode"] = system.get("mode") or system.get("operating_mode")
  834. # 直接字段(已经是扁平格式)
  835. for key in ["temperature", "humidity", "light_level", "warnings", "errors",
  836. "device_status", "actuator_status", "mode"]:
  837. if key not in environment and key in world_snapshot:
  838. if normalized.get(key) is None or normalized.get(key) == [] or normalized.get(key) == {}:
  839. normalized[key] = world_snapshot.get(key)
  840. # 处理 entities 字段(提取与 actuator/device 相关信息)
  841. entities = world_snapshot.get("entities", {})
  842. if isinstance(entities, dict):
  843. actuators = entities.get("actuator", [])
  844. devices = entities.get("device", [])
  845. if isinstance(actuators, list):
  846. for act in actuators:
  847. if isinstance(act, dict):
  848. device_id = act.get("entity_id") or act.get("device_id", "unknown")
  849. normalized["raw_entities"][f"actuator_{device_id}"] = act
  850. if isinstance(devices, list):
  851. for dev in devices:
  852. if isinstance(dev, dict):
  853. device_id = dev.get("entity_id") or dev.get("device_id", "unknown")
  854. normalized["raw_entities"][f"device_{device_id}"] = dev
  855. # 处理 recent_actions(可能在顶层或 recent_context 下)
  856. recent_context = world_snapshot.get("recent_context", {}) or world_snapshot.get("recent_actions", [])
  857. if isinstance(recent_context, dict):
  858. normalized["recent_actions"] = recent_context.get("recent_actions", [])
  859. elif isinstance(recent_context, list):
  860. normalized["recent_actions"] = recent_context
  861. # 如果顶层直接有 recent_actions
  862. if not normalized["recent_actions"] and "recent_actions" in world_snapshot:
  863. normalized["recent_actions"] = world_snapshot.get("recent_actions", [])
  864. # 缓存结果
  865. self._cached_snapshot_raw = world_snapshot
  866. self._cached_normalized_snapshot = normalized
  867. return normalized
  868. def _extract_recent_context(self, world_snapshot: dict[str, Any]) -> dict[str, Any]:
  869. """提取与当前决策相关的上下文
  870. 内部方法,从世界状态中提取关键信息。
  871. 优先使用 _normalize_world_snapshot() 转换后的结果。
  872. Args:
  873. world_snapshot: 世界状态快照
  874. Returns:
  875. 相关的上下文字典
  876. """
  877. normalized = self._normalize_world_snapshot(world_snapshot)
  878. context_keys = [
  879. "temperature",
  880. "humidity",
  881. "light_level",
  882. "device_status",
  883. "actuator_status",
  884. "recent_actions",
  885. "warnings",
  886. "errors",
  887. ]
  888. context = {}
  889. for key in context_keys:
  890. if key in normalized and normalized[key] is not None:
  891. context[key] = normalized[key]
  892. if normalized.get("raw_entities"):
  893. context["raw_entities"] = normalized["raw_entities"]
  894. return context
  895. def _map_intent_to_action(self, user_intent: str) -> str | None:
  896. """将用户意图映射到 action
  897. 内部方法,通过关键词匹配查找对应的 action。
  898. 按关键词长度从长到短排序后再匹配,避免短关键词优先匹配导致误判。
  899. Args:
  900. user_intent: 用户意图描述
  901. Returns:
  902. 匹配到的 action 名称,未匹配到返回 None
  903. """
  904. if not user_intent:
  905. return None
  906. intent_lower = user_intent.lower()
  907. # 按关键词长度从长到短排序,避免"喂"比"喂食"先匹配
  908. for keyword in sorted(self.intent_to_action.keys(), key=len, reverse=True):
  909. if keyword in intent_lower or keyword in user_intent:
  910. return self.intent_to_action[keyword]
  911. return None
  912. def register_intent_mapping(self, keyword: str, action: str) -> None:
  913. """注册新的意图映射
  914. 公共方法,运行时添加新的意图到 action 的映射。
  915. Args:
  916. keyword: 意图关键词
  917. action: 对应的 action 名称
  918. """
  919. self.intent_to_action[keyword] = action
  920. def register_tool_type(self, action: str, tool_type: ToolCallType) -> None:
  921. """注册 action 到 ToolCallType 的映射
  922. 公共方法,运行时添加新的映射。
  923. Args:
  924. action: action 名称
  925. tool_type: 对应的 ToolCallType
  926. """
  927. self.action_to_tool_type[action] = tool_type
  928. # =============================================================================
  929. # 主程序入口(测试示例)
  930. # =============================================================================
  931. if __name__ == "__main__":
  932. import json
  933. print("=" * 70)
  934. print("Planner 测试演示")
  935. print("=" * 70)
  936. # 初始化 Planner
  937. planner = Planner()
  938. # -------------------------------------------------------------------------
  939. # 构造测试数据
  940. # -------------------------------------------------------------------------
  941. world_snapshot = {
  942. "temperature": 32.5,
  943. "humidity": 70,
  944. "actuator_status": {
  945. "fan": "running",
  946. "light": "on",
  947. },
  948. "recent_actions": [
  949. {"action": "feed", "timestamp": time.time() - 300},
  950. ],
  951. "warnings": [],
  952. "errors": [],
  953. }
  954. available_tools = [
  955. "feed",
  956. "adjust_fan",
  957. "speak",
  958. "query",
  959. "move",
  960. "inspect",
  961. ]
  962. # -------------------------------------------------------------------------
  963. # 场景 1:直接可执行(工具存在 + 风险低)
  964. # -------------------------------------------------------------------------
  965. print("\n[场景 1] 直接可执行")
  966. print("-" * 40)
  967. user_intent_1 = "打开风扇降温"
  968. plan_1 = planner.generate_plan(
  969. world_snapshot=world_snapshot,
  970. user_intent=user_intent_1,
  971. available_tools=available_tools,
  972. planner_mode="rule",
  973. )
  974. print(f"用户意图: {user_intent_1}")
  975. print(f"Plan ID: {plan_1.plan_id}")
  976. print(f"目标: {plan_1.goal}")
  977. print(f"风险等级: {plan_1.risk_level.value}")
  978. print(f"需要确认: {plan_1.requires_confirmation}")
  979. print(f"步骤数: {len(plan_1.steps)}")
  980. if plan_1.steps:
  981. step = plan_1.steps[0]
  982. print(f" → Step {step.step_id}: {step.action} ({step.tool_call_type.value})")
  983. # -------------------------------------------------------------------------
  984. # 场景 2:需要确认(风险中高或世界冲突)
  985. # -------------------------------------------------------------------------
  986. print("\n[场景 2] 需要确认")
  987. print("-" * 40)
  988. world_snapshot_conflict = {
  989. "temperature": 32.5,
  990. "actuator_status": {
  991. "fan": "error",
  992. },
  993. "recent_actions": [
  994. {"action": "adjust_fan", "timestamp": time.time() - 60},
  995. ],
  996. }
  997. user_intent_2 = "再次降温"
  998. plan_2 = planner.generate_plan(
  999. world_snapshot=world_snapshot_conflict,
  1000. user_intent=user_intent_2,
  1001. available_tools=available_tools,
  1002. planner_mode="rule",
  1003. )
  1004. print(f"用户意图: {user_intent_2}")
  1005. print(f"Plan ID: {plan_2.plan_id}")
  1006. print(f"需要确认: {plan_2.requires_confirmation}")
  1007. print(f"确认消息: {plan_2.confirmation_message}")
  1008. print(f"步骤数: {len(plan_2.steps)}")
  1009. if plan_2.steps:
  1010. step = plan_2.steps[0]
  1011. print(f" → Step {step.step_id}: {step.action} ({step.tool_call_type.value})")
  1012. print(f" 需要确认: {step.requires_confirmation}")
  1013. # -------------------------------------------------------------------------
  1014. # 场景 3:工具不存在
  1015. # -------------------------------------------------------------------------
  1016. print("\n[场景 3] 工具不存在")
  1017. print("-" * 40)
  1018. user_intent_3 = "打开空调"
  1019. available_tools_3 = ["fan", "light", "speak"]
  1020. plan_3 = planner.generate_plan(
  1021. world_snapshot=world_snapshot,
  1022. user_intent=user_intent_3,
  1023. available_tools=available_tools_3,
  1024. planner_mode="rule",
  1025. )
  1026. print(f"用户意图: {user_intent_3}")
  1027. print(f"可用工具: {available_tools_3}")
  1028. print(f"Plan ID: {plan_3.plan_id}")
  1029. print(f"目标: {plan_3.goal}")
  1030. print(f"步骤数: {len(plan_3.steps)}")
  1031. if plan_3.steps:
  1032. step = plan_3.steps[0]
  1033. print(f" → Step {step.step_id}: {step.action} ({step.tool_call_type.value})")
  1034. print(f" 消息: {step.parameters.get('message', '')}")
  1035. # -------------------------------------------------------------------------
  1036. # 场景 4:LLM 模式(真实实现)
  1037. # -------------------------------------------------------------------------
  1038. print("\n[场景 4] LLM 模式")
  1039. print("-" * 40)
  1040. user_intent_4 = "打开风扇降温"
  1041. plan_4 = planner.generate_plan(
  1042. world_snapshot=world_snapshot,
  1043. user_intent=user_intent_4,
  1044. available_tools=available_tools,
  1045. planner_mode="llm",
  1046. )
  1047. print(f"用户意图: {user_intent_4}")
  1048. print(f"Plan ID: {plan_4.plan_id}")
  1049. print(f"来源: {plan_4.source}")
  1050. print(f"目标: {plan_4.goal}")
  1051. print(f"风险等级: {plan_4.risk_level.value}")
  1052. print(f"步骤数: {len(plan_4.steps)}")
  1053. if plan_4.steps:
  1054. step = plan_4.steps[0]
  1055. print(f" → Step {step.step_id}: {step.action} ({step.tool_call_type.value})")
  1056. # -------------------------------------------------------------------------
  1057. # 场景 5:LLM 模式 - 喂食询问
  1058. # -------------------------------------------------------------------------
  1059. print("\n[场景 5] LLM 模式 - 喂食询问")
  1060. print("-" * 40)
  1061. user_intent_5 = "再喂一次"
  1062. world_snapshot_recent_feed = {
  1063. "temperature": 28,
  1064. "last_feed_time": "2026-04-10T10:00:00",
  1065. "recent_actions": [
  1066. {"action": "feed", "timestamp": time.time() - 300},
  1067. ],
  1068. }
  1069. plan_5 = planner.generate_plan(
  1070. world_snapshot=world_snapshot_recent_feed,
  1071. user_intent=user_intent_5,
  1072. available_tools=["feed", "speak", "query"],
  1073. planner_mode="llm",
  1074. )
  1075. print(f"用户意图: {user_intent_5}")
  1076. print(f"Plan ID: {plan_5.plan_id}")
  1077. print(f"来源: {plan_5.source}")
  1078. print(f"目标: {plan_5.goal}")
  1079. print(f"风险等级: {plan_5.risk_level.value}")
  1080. print(f"步骤数: {len(plan_5.steps)}")
  1081. if plan_5.steps:
  1082. step = plan_5.steps[0]
  1083. print(f" → Step {step.step_id}: {step.action} ({step.tool_call_type.value})")
  1084. if step.tool_call_type == ToolCallType.ASK_USER:
  1085. print(f" 询问: {step.parameters.get('question', 'N/A')}")
  1086. # -------------------------------------------------------------------------
  1087. # 场景 6:Hybrid 模式 - 简单场景走规则
  1088. # -------------------------------------------------------------------------
  1089. print("\n[场景 6] Hybrid 模式 - 简单场景")
  1090. print("-" * 40)
  1091. user_intent_6 = "查询温度"
  1092. plan_6 = planner.generate_plan(
  1093. world_snapshot=world_snapshot,
  1094. user_intent=user_intent_6,
  1095. available_tools=available_tools,
  1096. planner_mode="hybrid",
  1097. )
  1098. print(f"用户意图: {user_intent_6}")
  1099. print(f"Plan ID: {plan_6.plan_id}")
  1100. print(f"来源: {plan_6.source}")
  1101. print(f"风险等级: {plan_6.risk_level.value}")
  1102. # -------------------------------------------------------------------------
  1103. # 场景 7:Hybrid 模式 - 复杂场景走 LLM
  1104. # -------------------------------------------------------------------------
  1105. print("\n[场景 7] Hybrid 模式 - 复杂场景")
  1106. print("-" * 40)
  1107. user_intent_7 = "先降温然后喂食" # 多步骤意图
  1108. plan_7 = planner.generate_plan(
  1109. world_snapshot=world_snapshot,
  1110. user_intent=user_intent_7,
  1111. available_tools=available_tools,
  1112. planner_mode="hybrid",
  1113. )
  1114. print(f"用户意图: {user_intent_7}")
  1115. print(f"Plan ID: {plan_7.plan_id}")
  1116. print(f"来源: {plan_7.source}")
  1117. print(f"目标: {plan_7.goal}")
  1118. print(f"风险等级: {plan_7.risk_level.value}")
  1119. print(f"步骤数: {len(plan_7.steps)}")
  1120. for step in plan_7.steps:
  1121. print(f" → Step {step.step_id}: {step.action} ({step.tool_call_type.value})")
  1122. # -------------------------------------------------------------------------
  1123. # 场景 8:意图不清晰 - 走规则未知意图
  1124. # -------------------------------------------------------------------------
  1125. print("\n[场景 8] 意图不清晰")
  1126. print("-" * 40)
  1127. user_intent_8 = "随便弄一下"
  1128. plan_8 = planner.generate_plan(
  1129. world_snapshot=world_snapshot,
  1130. user_intent=user_intent_8,
  1131. available_tools=available_tools,
  1132. planner_mode="rule",
  1133. )
  1134. print(f"用户意图: {user_intent_8}")
  1135. print(f"Plan ID: {plan_8.plan_id}")
  1136. print(f"目标: {plan_8.goal}")
  1137. print(f"步骤数: {len(plan_8.steps)}")
  1138. if plan_8.steps:
  1139. step = plan_8.steps[0]
  1140. print(f" → Step {step.step_id}: {step.action} ({step.tool_call_type.value})")
  1141. # -------------------------------------------------------------------------
  1142. # 场景 9:工具不存在
  1143. # -------------------------------------------------------------------------
  1144. print("\n[场景 9] 工具不存在")
  1145. print("-" * 40)
  1146. user_intent_9 = "打开空调"
  1147. available_tools_9 = ["fan", "light", "speak"]
  1148. plan_9 = planner.generate_plan(
  1149. world_snapshot=world_snapshot,
  1150. user_intent=user_intent_9,
  1151. available_tools=available_tools_9,
  1152. planner_mode="rule",
  1153. )
  1154. print(f"用户意图: {user_intent_9}")
  1155. print(f"可用工具: {available_tools_9}")
  1156. print(f"Plan ID: {plan_9.plan_id}")
  1157. print(f"目标: {plan_9.goal}")
  1158. print(f"步骤数: {len(plan_9.steps)}")
  1159. if plan_9.steps:
  1160. step = plan_9.steps[0]
  1161. print(f" → Step {step.step_id}: {step.action} ({step.tool_call_type.value})")
  1162. print(f" 消息: {step.parameters.get('message', '')}")
  1163. # -------------------------------------------------------------------------
  1164. # 场景 10:LLM 工具不存在场景
  1165. # -------------------------------------------------------------------------
  1166. print("\n[场景 10] LLM 模式 - 工具不存在")
  1167. print("-" * 40)
  1168. user_intent_10 = "打开空调"
  1169. plan_10 = planner.generate_plan(
  1170. world_snapshot={"temperature": 30},
  1171. user_intent=user_intent_10,
  1172. available_tools=["adjust_fan", "speak"],
  1173. planner_mode="llm",
  1174. )
  1175. print(f"用户意图: {user_intent_10}")
  1176. print(f"Plan ID: {plan_10.plan_id}")
  1177. print(f"来源: {plan_10.source}")
  1178. print(f"目标: {plan_10.goal}")
  1179. print(f"步骤数: {len(plan_10.steps)}")
  1180. if plan_10.steps:
  1181. step = plan_10.steps[0]
  1182. print(f" → Step {step.step_id}: {step.action} ({step.tool_call_type.value})")
  1183. # -------------------------------------------------------------------------
  1184. # 输出完整 JSON
  1185. # -------------------------------------------------------------------------
  1186. print("\n[完整 JSON 输出]")
  1187. print("-" * 40)
  1188. print("\n场景 1 Plan JSON (rule 模式):")
  1189. print(json.dumps(plan_1.to_dict(), indent=2, ensure_ascii=False))
  1190. print("\n场景 4 Plan JSON (llm 模式):")
  1191. print(json.dumps(plan_4.to_dict(), indent=2, ensure_ascii=False))
  1192. # -------------------------------------------------------------------------
  1193. # 测试自定义意图映射
  1194. # -------------------------------------------------------------------------
  1195. print("\n[自定义意图映射测试]")
  1196. print("-" * 40)
  1197. planner.register_intent_mapping("打扫", "clean")
  1198. planner.register_tool_type("clean", ToolCallType.EXECUTE)
  1199. user_intent_custom = "帮我打扫一下"
  1200. plan_custom = planner.generate_plan(
  1201. world_snapshot=world_snapshot,
  1202. user_intent=user_intent_custom,
  1203. available_tools=["clean", "speak"],
  1204. planner_mode="rule",
  1205. )
  1206. print(f"用户意图: {user_intent_custom}")
  1207. print(f"识别动作: {plan_custom.steps[0].action if plan_custom.steps else 'None'}")
  1208. print("\n" + "=" * 70)
  1209. print("Planner 测试演示完成")
  1210. print("=" * 70)