Skip to content

Commit 9965336

Browse files
committed
意图增强
1 parent 14a7200 commit 9965336

6 files changed

Lines changed: 3366 additions & 306 deletions

File tree

.claude/settings.local.json

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,16 @@
1616
"WebFetch(domain:developers.home-assistant.io)",
1717
"WebFetch(domain:www.home-assistant.io)",
1818
"Bash(cp:*)",
19-
"Bash(git checkout:*)"
19+
"Bash(git checkout:*)",
20+
"Bash(for:*)",
21+
"Bash(do echo:*)",
22+
"Bash(done:*)",
23+
"Read(//c/**)",
24+
"WebFetch(domain:github.com)",
25+
"Bash(del:*)",
26+
"Bash(nul:*)",
27+
"WebFetch(domain:developers.home-assistant.dev)",
28+
"Bash(chcp:*)"
2029
],
2130
"deny": [],
2231
"ask": []

custom_components/ai_hub/conversation.py

Lines changed: 231 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -108,9 +108,28 @@ async def _async_handle_message(
108108
except Exception as e:
109109
_LOGGER.debug("Automation handling failed: %s", e)
110110

111-
# 跳过意图匹配,直接交给LLM处理
111+
# 🚀 检查是否需要本地意图处理 (全局设备控制)
112+
try:
113+
from .intents import get_global_intent_handler
114+
intent_handler = get_global_intent_handler(self.hass)
115+
116+
if intent_handler and intent_handler.should_handle(user_input.text):
117+
# 需要本地意图处理的全局控制指令
118+
_LOGGER.info(f"🚀 本地意图处理: {user_input.text}")
119+
intent_result = await intent_handler.handle(user_input.text, user_input.language)
120+
121+
if intent_result:
122+
_LOGGER.info(f"✅ 本地意图完成")
123+
return conversation.ConversationResult(
124+
response=intent_result["response"],
125+
conversation_id=user_input.conversation_id
126+
)
127+
128+
except Exception as e:
129+
_LOGGER.debug("Local intent handling failed: %s", e)
130+
131+
# 其他情况交给LLM处理
112132
# Home Assistant会通过conversation组件自动处理意图
113-
# 我们的intents.yaml作为语言扩展包被HA自动识别
114133

115134
# 步骤4: LLM处理 (如果所有意图处理都失败)
116135

@@ -157,6 +176,17 @@ async def _async_handle_message(
157176

158177
await self._async_handle_chat_log(chat_log)
159178

179+
# 🔄 新增:验证设备操作并重试 (3秒内)
180+
if hasattr(chat_log, 'tool_calls') and chat_log.tool_calls:
181+
device_operations = [
182+
call for call in chat_log.tool_calls
183+
if self._is_device_operation(call.tool_name)
184+
]
185+
186+
if device_operations:
187+
_LOGGER.info(f"🔧 验证 {len(device_operations)} 个设备操作 (3秒内完成)")
188+
await self._verify_device_operations_with_retry(device_operations)
189+
160190
# 检查是否有工具调用结果
161191
if hasattr(chat_log, 'unresponded_tool_results') and chat_log.unresponded_tool_results:
162192
# 检查是否是boolean值
@@ -177,7 +207,133 @@ async def _async_handle_message(
177207
# Return result from chat log
178208
return conversation.async_get_result_from_chat_log(user_input, chat_log)
179209

180-
210+
def _is_device_operation(self, tool_name: str) -> bool:
211+
"""判断是否是设备控制操作"""
212+
try:
213+
from .intents import is_device_operation
214+
return is_device_operation(tool_name)
215+
except Exception as e:
216+
_LOGGER.debug(f"设备操作判断失败: {e}")
217+
return False
218+
219+
async def _verify_device_operations_with_retry(self, device_operations):
220+
"""验证设备操作并重试,使用配置的时间限制"""
221+
try:
222+
from .intents import get_device_verification_config
223+
config = get_device_verification_config()
224+
except Exception as e:
225+
_LOGGER.debug(f"获取验证配置失败: {e}")
226+
# 使用默认配置
227+
config = {
228+
'total_timeout': 3,
229+
'max_retries': 3,
230+
'wait_times': [0.5, 0.8, 1.1]
231+
}
232+
233+
import time
234+
import asyncio
235+
236+
start_time = time.time()
237+
total_timeout = config.get('total_timeout', 3)
238+
max_retries = config.get('max_retries', 3)
239+
wait_times = config.get('wait_times', [0.5, 0.8, 1.1])
240+
241+
_LOGGER.info(f"🔧 开始设备操作验证,总时间限制{total_timeout}秒,最多重试{max_retries}次")
242+
243+
for attempt in range(max_retries):
244+
# 检查是否还有时间
245+
elapsed = time.time() - start_time
246+
remaining_time = total_timeout - elapsed
247+
248+
if remaining_time <= 0:
249+
_LOGGER.warning("⏰ 验证超时,停止重试")
250+
break
251+
252+
# 获取等待时间
253+
wait_time = wait_times[attempt] if attempt < len(wait_times) else wait_times[-1]
254+
# 确保不超过剩余时间
255+
wait_time = min(wait_time, remaining_time * 0.3)
256+
257+
await asyncio.sleep(wait_time)
258+
259+
# 使用GetLiveContext验证
260+
try:
261+
# 模拟调用GetLiveContext工具
262+
current_context = await self._get_live_context()
263+
264+
# 检查操作是否成功
265+
all_successful = True
266+
for operation in device_operations:
267+
if not self._is_operation_successful(operation, current_context):
268+
all_successful = False
269+
break
270+
271+
if all_successful:
272+
total_time = time.time() - start_time
273+
_LOGGER.info(f"✅ 所有设备操作验证成功 (耗时{total_time:.1f}秒)")
274+
return True
275+
else:
276+
_LOGGER.info(f"⏳ 第{attempt + 1}次验证未完全成功,继续等待 (剩余{remaining_time:.1f}秒)")
277+
278+
except Exception as e:
279+
_LOGGER.debug(f"验证过程出错: {e}")
280+
281+
total_time = time.time() - start_time
282+
_LOGGER.warning(f"⚠️ {total_timeout}秒内无法验证所有设备操作成功 (耗时{total_time:.1f}秒)")
283+
return False
284+
285+
async def _get_live_context(self):
286+
"""获取当前设备状态 (模拟GetLiveContext工具)"""
287+
# 这里应该调用实际的GetLiveContext工具
288+
# 暂时返回模拟数据
289+
return {
290+
"lights": {"living_room_main": "off", "living_room_ambient": "on"},
291+
"switches": {},
292+
"climate": {},
293+
"covers": {},
294+
"media_players": {},
295+
"locks": {},
296+
"vacuums": {}
297+
}
298+
299+
def _is_operation_successful(self, operation, context):
300+
"""检查单个操作是否成功"""
301+
tool_name = operation.tool_name
302+
arguments = operation.arguments
303+
304+
# 根据操作类型检查对应设备状态
305+
if tool_name == 'light.turn_on':
306+
# 检查灯是否打开
307+
entity_id = arguments.get('entity_id', [])
308+
if isinstance(entity_id, str):
309+
entity_id = [entity_id]
310+
311+
for eid in entity_id:
312+
# 从context中检查状态
313+
if 'living_room_main' in eid and context.get('lights', {}).get('living_room_main') != 'on':
314+
return False
315+
if 'living_room_ambient' in eid and context.get('lights', {}).get('living_room_ambient') != 'on':
316+
return False
317+
return True
318+
319+
elif tool_name == 'light.turn_off':
320+
# 检查灯是否关闭
321+
entity_id = arguments.get('entity_id', [])
322+
if isinstance(entity_id, str):
323+
entity_id = [entity_id]
324+
325+
for eid in entity_id:
326+
if 'living_room_main' in eid and context.get('lights', {}).get('living_room_main') != 'off':
327+
return False
328+
if 'living_room_ambient' in eid and context.get('lights', {}).get('living_room_ambient') != 'off':
329+
return False
330+
return True
331+
332+
# 其他设备类型的检查可以在这里添加
333+
# 暂时返回True,表示其他操作假设成功
334+
return True
335+
336+
181337
async def _handle_automation_request(
182338
self,
183339
user_input: conversation.ConversationInput
@@ -312,26 +468,85 @@ def _is_all_device_operation(self, intent_info: Dict[str, Any]) -> bool:
312468
"""判断是否是"所有设备"操作"""
313469
text = intent_info.get("text", "").lower()
314470

315-
# 检查是否包含"所有"相关的关键词
316-
all_keywords = ["所有", "全部", "一切"]
471+
# 从配置中读取"所有"相关的关键词,避免硬编码
472+
try:
473+
config = get_intents_config()
474+
all_keywords = config.get('global_keywords', []) if config else []
475+
if not all_keywords:
476+
# 如果配置中没有,使用默认值
477+
all_keywords = ["所有", "全部", "一切"]
478+
except Exception as e:
479+
_LOGGER.debug(f"读取global_keywords失败,使用默认值: {e}")
480+
all_keywords = ["所有", "全部", "一切"]
481+
317482
return any(keyword in text for keyword in all_keywords)
318483

319484
def _has_local_intent_config(self, intent_type: str, intent_info: Dict[str, Any]) -> bool:
320485
"""检查意图是否在本地配置中定义为需要特殊处理"""
321486
text = intent_info.get("text", "").lower()
322487

323-
# 检查是否包含了需要本地处理的特征
324-
# 比如所有设备控制、特殊功能等
325-
local_features = [
326-
# 所有设备相关的关键词组合
327-
"所有设备",
328-
"全部设备",
329-
"所有灯",
330-
"全部灯",
331-
332-
# 可以在配置中添加更多特征
333-
]
488+
# 从intents.yaml中读取本地特征配置,避免硬编码
489+
local_features = []
490+
try:
491+
intents_config = get_intents_config()
492+
if intents_config and 'expansion_rules' in intents_config:
493+
# 获取所有本地特征关键词
494+
for key, value in intents_config['expansion_rules'].items():
495+
if isinstance(value, str) and '|' in value:
496+
# 拆分管道符分隔的关键词
497+
local_features.extend(value.split('|'))
498+
_LOGGER.debug(f"从intents.yaml加载本地特征关键词: {len(local_features)}个")
499+
except Exception as e:
500+
_LOGGER.debug(f"读取本地配置失败,使用默认值: {e}")
501+
# 如果配置读取失败,使用默认的关键词(从配置中读取)
502+
local_features = ["所有设备", "全部设备", "所有灯", "全部灯"]
334503

335504
return any(feature in text for feature in local_features)
336505

506+
def _should_skip_ha_standard_processing(self, text: str) -> bool:
507+
"""判断是否应该跳过Home Assistant标准处理,直接进入本地意图
508+
509+
只有非常明确的本地意图才跳过HA标准处理,如:
510+
- "打开所有设备"
511+
- "关闭所有灯"
512+
"""
513+
try:
514+
from .intents import get_intents_config
515+
config = get_intents_config()
516+
if not config:
517+
return False
518+
519+
global_config = config.get('GlobalDeviceControl', {})
520+
if not global_config:
521+
return False
522+
523+
text_lower = text.lower().strip()
524+
525+
# 检查全局关键词
526+
global_keywords = global_config.get('global_keywords', [])
527+
has_global = any(keyword in text_lower for keyword in global_keywords)
528+
529+
# 检查明确的开关关键词
530+
on_keywords = global_config.get('on_keywords', [])
531+
off_keywords = global_config.get('off_keywords', [])
532+
has_action = any(keyword in text_lower for keyword in on_keywords + off_keywords)
533+
534+
# 检查设备类型关键词
535+
device_type_keywords = global_config.get('device_type_keywords', {})
536+
has_device = any(keyword in text_lower for keywords in device_type_keywords.values() for keyword in keywords)
537+
538+
# 只有同时具备:全局关键词 + 明确动作 + 设备类型,才跳过HA处理
539+
should_skip = has_global and has_action and has_device
540+
541+
if should_skip:
542+
_LOGGER.debug(f"跳过HA标准处理: 全局={has_global}, 动作={has_action}, 设备={has_device}")
543+
544+
return should_skip
545+
546+
except Exception as e:
547+
_LOGGER.debug(f"判断跳过HA处理时出错: {e}")
548+
return False
549+
550+
551+
337552

0 commit comments

Comments
 (0)