Files
ai-device/intelligent_cabin/app/services/response_policy.py
2026-06-11 16:28:00 +08:00

300 lines
14 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
import re
from typing import Any
from app.schemas.intent import IntentDefinition
class ResponsePolicy:
_DEFAULT_INTENT_HINTS = {
"cabin_nav_to": "导航",
"cabin_nav_cancel": "结束导航",
"cabin_set_ac": "调空调",
"cabin_ac_on": "打开空调",
"cabin_ac_off": "关闭空调",
"cabin_fan_up": "调大风量",
"cabin_fan_down": "调小风量",
"cabin_defog_front_on": "打开前挡除雾",
"cabin_defog_rear_on": "打开后挡除雾",
"cabin_window_open": "打开车窗",
"cabin_window_close": "关闭车窗",
"cabin_sunroof_open": "打开天窗",
"cabin_sunroof_close": "关闭天窗",
"cabin_trunk_open": "打开后备箱",
"cabin_trunk_close": "关闭后备箱",
"cabin_lock_doors": "锁车门",
"cabin_unlock_doors": "解锁车门",
"cabin_play_music": "播放音乐",
"cabin_pause_music": "暂停音乐",
"cabin_next_track": "下一首",
"cabin_previous_track": "上一首",
"cabin_volume_up": "调大音量",
"cabin_volume_down": "调小音量",
"cabin_volume_mute": "静音",
"cabin_lights_on": "打开车灯",
"cabin_lights_off": "关闭车灯",
"cabin_reading_light_on": "打开阅读灯",
"cabin_reading_light_off": "关闭阅读灯",
"cabin_seat_heat_on": "打开座椅加热",
"cabin_seat_heat_off": "关闭座椅加热",
"cabin_seat_vent_on": "打开座椅通风",
"cabin_seat_vent_off": "关闭座椅通风",
"cabin_mirror_fold": "折叠后视镜",
"cabin_mirror_unfold": "展开后视镜",
"cabin_wiper_on": "打开雨刷",
"cabin_wiper_off": "关闭雨刷",
"cabin_screen_brightness_up": "调亮屏幕",
"cabin_screen_brightness_down": "调暗屏幕",
"cabin_answer_call": "接听电话",
"cabin_hang_up_call": "挂断电话",
"cs_query_order": "查订单",
"cs_query_logistics": "查物流",
"cs_cancel_order": "取消订单",
"cs_transfer_human": "转人工",
}
def __init__(
self,
templates: dict[str, str] | None = None,
intent_hints: dict[str, str] | None = None,
) -> None:
self._templates = templates or {}
self._intent_hints = {**self._DEFAULT_INTENT_HINTS, **(intent_hints or {})}
def ask_for_slot(self, intent: IntentDefinition, slot_name: str, default_template: str) -> str:
if slot_name == "order_id":
if intent.intent_id == "cs_cancel_order":
return self._template("ask_cancel_order_id", "请告诉我订单号。")
return self._template("ask_order_id", "请提供订单号。")
if slot_name == "destination":
return self._template("ask_destination", "请告诉我要去哪里。")
if slot_name == "temperature":
return self._template("ask_temperature", "请告诉我要设置多少度。")
if slot_name == "media_query":
return self._template("ask_media_query", "想听什么风格或者具体的歌名?")
return default_template.strip() or "请补充一个关键信息。"
def workflow_result(self, intent: IntentDefinition, plugin_result: dict[str, Any]) -> str:
if not plugin_result.get("success", True):
return self._template("workflow_failed", "这次没处理成功,请稍后再试。")
message = str(plugin_result.get("message") or "").strip()
if not message:
return self.ack(intent)
if len(message) > 42:
return message[:39].rstrip(",。;; ") + "..."
return message
def workflow_summary(self, messages: list[str]) -> str:
cleaned = [item.strip() for item in messages if item and item.strip()]
if not cleaned:
return self._template("workflow_summary_empty", "好的,已经处理完成。")
if len(cleaned) == 1:
return cleaned[0]
natural_clauses: list[str] = []
previous_subject: str | None = None
for index, item in enumerate(cleaned[:3]):
clause, subject = self._vehicle_style_clause(item, index=index, previous_subject=previous_subject)
natural_clauses.append(clause)
previous_subject = subject or previous_subject
summary = f"好,{''.join(natural_clauses)}"
if len(cleaned) > 3:
summary = summary.rstrip("") + ",其余步骤也已完成。"
if len(summary) > 70:
return summary[:67].rstrip(",。;; ") + "..."
return summary
def ask_for_confirmation(self, intent: IntentDefinition, detail: str | None = None) -> str:
if intent.intent_id == "cs_cancel_order":
if detail:
return f"即将取消订单,{detail}。请回复“确认”或“取消”。"
return "即将取消订单。请回复“确认”或“取消”。"
if detail:
return f"{detail}。请回复“确认”或“取消”。"
return "请确认是否继续执行。回复“确认”或“取消”。"
def confirm_retry(self) -> str:
return self._template("confirm_retry", "我需要一个明确确认。请回复“确认”继续,或回复“取消”终止。")
def confirm_cancelled(self) -> str:
return self._template("confirm_cancelled", "好的,已取消这一步。")
def step_skipped(self, intent: IntentDefinition, reason: str | None = None) -> str:
if intent.intent_id == "cs_cancel_order":
base = "订单取消步骤未执行。"
else:
base = "这一步已跳过。"
if reason:
return f"{base}{reason}"
return base
def ack(self, intent: IntentDefinition | None = None) -> str:
if intent is None:
return self._template("ack_default", "收到,马上处理。")
if intent.domain == "cabin":
return self._template("ack_cabin", "好的,马上处理。")
return self._template("ack_service", "收到,我来处理。")
def reject(self) -> str:
return self._template("reject", "这个我暂时做不了,但我可以帮你查询、控制或转人工。")
def short_social(self, social_kind: str) -> str:
if social_kind == "greeting":
return self._template("short_social_greeting", "你好,我在。")
if social_kind == "thanks":
return self._template("short_social_thanks", "不客气。")
if social_kind == "goodbye":
return self._template("short_social_goodbye", "好的,有需要再叫我。")
if social_kind == "capability":
return self._template(
"short_social_capability",
"我可以帮你查订单、查物流、取消订单、导航、调空调、播放音乐或转人工。",
)
return self._template("short_social_default", "我在。")
def open_social_fallback(self) -> str:
return self._template("open_social_fallback", "可以和你聊两句,你也可以继续告诉我想处理什么。")
def with_pending_hint(self, text: str, pending_hint: str | None = None) -> str:
base = text.strip() or self.open_social_fallback()
hint = (pending_hint or "").strip()
if not hint:
return base
return f"{base} {hint}"
def pending_task_hint(self, status: str, pending_slots: list[str], current_intent: str | None = None) -> str | None:
if status == "waiting_confirmation":
return self._template("pending_confirmation_hint", "当前这一步还在等你确认,回复“确认”或“取消”即可。")
if status == "waiting_slot" and pending_slots:
if pending_slots[0] == "order_id":
return self._template("pending_slot_order_id", "当前还缺订单号,你继续告诉我订单号就行。")
if pending_slots[0] == "temperature":
return self._template("pending_slot_temperature", "当前还缺温度,你继续告诉我要设置多少度就行。")
if pending_slots[0] == "destination":
return self._template("pending_slot_destination", "当前还缺目的地,你继续告诉我要去哪里就行。")
if pending_slots[0] == "media_query":
return self._template("pending_slot_media_query", "当前还缺歌名或风格,你直接说歌名、歌手或风格就行。")
return self._template("pending_slot_default", "当前还缺一个关键信息,你继续补充就行。")
if status == "running" and current_intent:
return self._template("pending_running", "当前任务还在继续,你也可以直接继续下一个指令。")
return None
def task_stopped(self) -> str:
return self._template("task_stopped", "好的,已停止当前任务。")
def clarify(self, candidate_intents: list[str]) -> str:
options = [
self._intent_hints.get(intent_id, intent_id)
for intent_id in candidate_intents
if intent_id
]
deduped: list[str] = []
for item in options:
if item not in deduped:
deduped.append(item)
if not deduped:
return "我理解得还不够确定,你是想查询、控制,还是转人工?"
if len(deduped) == 1:
return f"请确认一下,你是想{deduped[0]}吗?"
if len(deduped) == 2:
return f"请确认一下,你是想{deduped[0]}还是{deduped[1]}"
return f"请确认一下,你是想{deduped[0]}{deduped[1]},还是{deduped[2]}"
def fallback(self) -> str:
return self._template("fallback", "我还没完全听懂,你可以换个简短说法,或告诉我是查询、控制还是转人工。")
def _template(self, key: str, default: str) -> str:
value = str(self._templates.get(key, default)).strip()
return value or default
def _naturalize_workflow_message(self, text: str) -> str:
normalized = text.strip().rstrip("。;; ")
normalized = re.sub(r"^好的[,\s]*", "", normalized)
normalized = re.sub(r"^收到[,\s]*", "", normalized)
if normalized.startswith("已将"):
normalized = normalized[2:]
elif normalized.startswith("已经将"):
normalized = normalized[3:]
elif normalized.startswith("已经"):
normalized = normalized[2:]
elif normalized.startswith(""):
normalized = normalized[1:]
normalized = normalized.strip(", ")
if not normalized:
return "已经处理好了"
if normalized.endswith(""):
return normalized
return f"{normalized}"
def _vehicle_style_clause(
self,
text: str,
*,
index: int,
previous_subject: str | None = None,
) -> tuple[str, str | None]:
normalized = self._naturalize_workflow_message(text)
match = re.match(r"^(打开|关闭)(.+)了$", normalized)
if match:
action, subject = match.groups()
subject = subject.strip()
if action == "打开":
if previous_subject and previous_subject == subject:
return "也打开了", subject
if index > 0:
return f"{subject}也打开了", subject
return f"{subject}已经打开了", subject
if previous_subject and previous_subject == subject:
return "也帮你关上了", subject
if index > 0:
return f"{subject}也帮你关上了", subject
return f"{subject}已经关上了", subject
match = re.match(r"^(锁定|解锁)(.+)了$", normalized)
if match:
action, subject = match.groups()
subject = subject.strip()
action_text = "锁好了" if action == "锁定" else "解锁了"
if previous_subject and previous_subject == subject:
return f"{action_text}", subject
if index > 0:
return f"{subject}{action_text}", subject
return f"{subject}已经{action_text}", subject
match = re.match(r"^(.+)调到\s*(.+)度了$", normalized)
if match:
subject, value = match.groups()
subject = subject.strip()
value = value.strip()
if previous_subject and previous_subject == subject:
return f"也调到 {value} 度了", subject
if index > 0:
return f"{subject}也调到 {value} 度了", subject
return f"{subject}调到 {value} 度了", subject
match = re.match(r"^(调大|调小)(.+)了$", normalized)
if match:
action, subject = match.groups()
subject = subject.strip()
if previous_subject and previous_subject == subject:
return f"{action}", subject
if index > 0:
return f"{subject}{action}", subject
return f"{subject}已经{action}", subject
if normalized.startswith("正在播放 "):
target = normalized[len("正在播放 ") :].strip()
if index > 0:
return f"也开始播放 {target}", "播放"
return f"开始播放 {target}", "播放"
if normalized.startswith("订单 ") and normalized.endswith(" 已取消"):
order_text = normalized[:-4].strip()
return f"{order_text}已经取消了", "订单"
if normalized.startswith("订单 ") and "当前" in normalized:
return normalized, "订单"
return normalized, None