Update project and configurations
This commit is contained in:
250
intelligent_cabin/app/services/social.py
Normal file
250
intelligent_cabin/app/services/social.py
Normal file
@@ -0,0 +1,250 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import re
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Any, Literal, Protocol
|
||||
from urllib import error, request
|
||||
|
||||
from app.services.session_store import SessionState
|
||||
|
||||
|
||||
SocialCategory = Literal["none", "open_social"]
|
||||
ShortSocialKind = Literal["greeting", "thanks", "goodbye", "capability"]
|
||||
|
||||
|
||||
@dataclass
|
||||
class SocialRouteResult:
|
||||
category: SocialCategory
|
||||
reason: str
|
||||
short_kind: ShortSocialKind | None = None
|
||||
|
||||
|
||||
@dataclass
|
||||
class SocialReplyResult:
|
||||
text: str
|
||||
backend: str
|
||||
model_name: str
|
||||
error_message: str | None = None
|
||||
metadata: dict[str, Any] = field(default_factory=dict)
|
||||
|
||||
|
||||
class SocialResponder(Protocol):
|
||||
def reply(self, text: str, session: SessionState) -> SocialReplyResult:
|
||||
...
|
||||
|
||||
|
||||
class SocialRouter:
|
||||
_SHORT_SOCIAL_PATTERNS: dict[ShortSocialKind, tuple[str, ...]] = {
|
||||
"greeting": ("你好", "您好", "嗨", "哈喽", "hi", "hello", "在吗", "在不在"),
|
||||
"thanks": ("谢谢", "谢啦", "多谢", "thanks", "thank you", "辛苦了"),
|
||||
"goodbye": ("再见", "拜拜", "回头见", "bye", "goodbye"),
|
||||
"capability": (
|
||||
"你是谁",
|
||||
"你叫什么",
|
||||
"你叫什么名字",
|
||||
"你叫啥",
|
||||
"怎么称呼你",
|
||||
"介绍一下你自己",
|
||||
"你能做什么",
|
||||
"你会什么",
|
||||
"你可以做什么",
|
||||
),
|
||||
}
|
||||
_OPEN_SOCIAL_PATTERNS: tuple[str, ...] = (
|
||||
"天气",
|
||||
"讲个笑话",
|
||||
"笑话",
|
||||
"无聊",
|
||||
"有点累",
|
||||
"有点困",
|
||||
"有点烦",
|
||||
"开心",
|
||||
"不开心",
|
||||
"真不错",
|
||||
"真好",
|
||||
"聊聊天",
|
||||
"你觉得",
|
||||
"你怎么看",
|
||||
"你说呢",
|
||||
)
|
||||
_OPEN_SOCIAL_REGEXES: tuple[re.Pattern[str], ...] = (
|
||||
re.compile(r"今天.*(不错|真好|挺好|真舒服)"),
|
||||
re.compile(r"(好|真)热啊"),
|
||||
re.compile(r"(好|真)冷啊"),
|
||||
re.compile(r"我今天.*(累|困|烦|开心|难过)"),
|
||||
re.compile(r".*(怎么样|如何|咋样)[??]?$"),
|
||||
)
|
||||
_CAPABILITY_REGEXES: tuple[re.Pattern[str], ...] = (
|
||||
re.compile(r"你.*(叫.*名字|叫什么|叫啥)[??]?$"),
|
||||
re.compile(r"(怎么称呼你|介绍一下你自己)[??]?$"),
|
||||
re.compile(r"你.*(能做什么|会什么|可以做什么)[??]?$"),
|
||||
)
|
||||
_TASK_KEYWORDS: tuple[str, ...] = (
|
||||
"订单",
|
||||
"物流",
|
||||
"取消",
|
||||
"转人工",
|
||||
"导航",
|
||||
"去",
|
||||
"到",
|
||||
"空调",
|
||||
"温度",
|
||||
"调到",
|
||||
"播放",
|
||||
"音乐",
|
||||
"歌曲",
|
||||
"车窗",
|
||||
"座椅",
|
||||
"后视镜",
|
||||
"灯光",
|
||||
"除雾",
|
||||
"确认",
|
||||
"不用",
|
||||
)
|
||||
|
||||
def route(self, text: str, session: SessionState) -> SocialRouteResult:
|
||||
normalized = self._normalize(text)
|
||||
if not normalized:
|
||||
return SocialRouteResult(category="none", reason="empty text")
|
||||
if self._looks_like_task(normalized):
|
||||
return SocialRouteResult(category="none", reason="contains task keywords")
|
||||
for short_kind, patterns in self._SHORT_SOCIAL_PATTERNS.items():
|
||||
if any(pattern in normalized for pattern in patterns):
|
||||
return SocialRouteResult(
|
||||
category="open_social",
|
||||
short_kind=short_kind,
|
||||
reason=f"matched social pattern routed to llm: {short_kind}",
|
||||
)
|
||||
if any(regex.search(normalized) for regex in self._CAPABILITY_REGEXES):
|
||||
return SocialRouteResult(
|
||||
category="open_social",
|
||||
short_kind="capability",
|
||||
reason="matched capability social regex routed to llm",
|
||||
)
|
||||
if any(pattern in normalized for pattern in self._OPEN_SOCIAL_PATTERNS):
|
||||
return SocialRouteResult(category="open_social", reason="matched open social phrase")
|
||||
if any(regex.search(normalized) for regex in self._OPEN_SOCIAL_REGEXES):
|
||||
return SocialRouteResult(category="open_social", reason="matched open social regex")
|
||||
if session.context_memory.get("last_dialog_mode") == "open_social" and len(normalized) <= 14:
|
||||
return SocialRouteResult(category="open_social", reason="follow-up to previous open social turn")
|
||||
return SocialRouteResult(category="none", reason="no social pattern matched")
|
||||
|
||||
def _normalize(self, text: str) -> str:
|
||||
return re.sub(r"\s+", "", text.strip().lower())
|
||||
|
||||
def _looks_like_task(self, normalized: str) -> bool:
|
||||
if any(keyword in normalized for keyword in self._TASK_KEYWORDS):
|
||||
return True
|
||||
return bool(re.match(r"^(查|帮我查|打开|关闭|设置|调|导航|播放|取消|转)(.+)", normalized))
|
||||
|
||||
|
||||
class DashScopeSocialResponder:
|
||||
def __init__(
|
||||
self,
|
||||
base_url: str,
|
||||
api_key: str,
|
||||
model_name: str,
|
||||
timeout_seconds: float = 6.0,
|
||||
) -> None:
|
||||
self._base_url = base_url.rstrip("/")
|
||||
self._api_key = api_key
|
||||
self._model_name = model_name
|
||||
self._timeout_seconds = timeout_seconds
|
||||
|
||||
def reply(self, text: str, session: SessionState) -> SocialReplyResult:
|
||||
if not self._base_url or not self._api_key or not self._model_name:
|
||||
return SocialReplyResult(
|
||||
text="可以和你聊两句,你也可以继续让我处理查询或控制。",
|
||||
backend="local-fallback",
|
||||
model_name="social-fallback",
|
||||
error_message="social responder is not configured",
|
||||
)
|
||||
|
||||
payload = {
|
||||
"model": self._model_name,
|
||||
"temperature": 0.6,
|
||||
"enable_thinking": False,
|
||||
"max_tokens": 120,
|
||||
"messages": [
|
||||
{
|
||||
"role": "system",
|
||||
"content": (
|
||||
"你是智能座舱助手,负责处理所有闲聊、问候、身份问答、能力介绍和开放聊天。"
|
||||
"请用自然、口语化、简短的中文回答,优先 1-3 句,总长度尽量不超过 50 个字。"
|
||||
"如果用户在打招呼、问你是谁、问你叫什么名字、问你会什么,请直接自然回答,不要像固定菜单。"
|
||||
"可以结合用户上下文自然接话,但不要过度展开。"
|
||||
"不要编造已经执行了任何车辆或客服动作。"
|
||||
"不要输出 JSON,不要长篇解释。"
|
||||
),
|
||||
},
|
||||
{
|
||||
"role": "user",
|
||||
"content": json.dumps(
|
||||
{
|
||||
"text": text,
|
||||
"context": {
|
||||
"last_user_text": session.last_user_text,
|
||||
"last_agent_text": session.last_agent_text,
|
||||
"current_intent": session.current_intent,
|
||||
"status": session.status,
|
||||
},
|
||||
},
|
||||
ensure_ascii=False,
|
||||
),
|
||||
},
|
||||
],
|
||||
}
|
||||
req = request.Request(
|
||||
self._endpoint(),
|
||||
data=json.dumps(payload).encode("utf-8"),
|
||||
headers={
|
||||
"Content-Type": "application/json",
|
||||
"Authorization": f"Bearer {self._api_key}",
|
||||
},
|
||||
method="POST",
|
||||
)
|
||||
try:
|
||||
with request.urlopen(req, timeout=self._timeout_seconds) as response:
|
||||
data = json.loads(response.read().decode("utf-8"))
|
||||
except (error.URLError, TimeoutError, ValueError) as exc:
|
||||
return SocialReplyResult(
|
||||
text="是啊,听起来今天状态不错。",
|
||||
backend="local-fallback",
|
||||
model_name="social-fallback",
|
||||
error_message=str(exc),
|
||||
)
|
||||
|
||||
content = self._extract_content(data)
|
||||
if not content:
|
||||
return SocialReplyResult(
|
||||
text="可以和你聊两句,你也可以继续说说看。",
|
||||
backend="local-fallback",
|
||||
model_name="social-fallback",
|
||||
error_message="empty social response",
|
||||
)
|
||||
return SocialReplyResult(
|
||||
text=content,
|
||||
backend="dashscope",
|
||||
model_name=self._model_name,
|
||||
)
|
||||
|
||||
def _endpoint(self) -> str:
|
||||
if self._base_url.endswith("/chat/completions"):
|
||||
return self._base_url
|
||||
return f"{self._base_url}/chat/completions"
|
||||
|
||||
def _extract_content(self, payload: dict[str, Any]) -> str:
|
||||
choices = payload.get("choices")
|
||||
if not isinstance(choices, list) or not choices:
|
||||
return ""
|
||||
message = choices[0].get("message", {})
|
||||
content = message.get("content", "")
|
||||
if isinstance(content, list):
|
||||
parts = [
|
||||
str(item.get("text", "")).strip()
|
||||
for item in content
|
||||
if isinstance(item, dict) and item.get("type") == "text"
|
||||
]
|
||||
return "".join(parts).strip()
|
||||
return str(content).strip()
|
||||
Reference in New Issue
Block a user