feat: expand colloquial reply sets and support cross-image quote intent
This commit is contained in:
@@ -310,6 +310,27 @@ class CustomerServiceAgent:
|
||||
return ""
|
||||
return cleaned
|
||||
|
||||
@staticmethod
|
||||
def _colloquialize_reply(text: str) -> str:
|
||||
"""把常见机械表达柔化为更口语的客服话术。"""
|
||||
t = (text or "").strip()
|
||||
if not t:
|
||||
return t
|
||||
repl = {
|
||||
"确认我就安排": "你点头我就开做",
|
||||
"可以的话我马上安排": "可以我就马上给你做",
|
||||
"我这边马上安排": "我马上安排",
|
||||
"立刻统一报价": "马上给你报价",
|
||||
"统一报价": "一起给你报价",
|
||||
"您": "你",
|
||||
"请您": "你",
|
||||
"可选:A": "可选:",
|
||||
"流程完成": "已经安排好了",
|
||||
}
|
||||
for k, v in repl.items():
|
||||
t = t.replace(k, v)
|
||||
return t
|
||||
|
||||
def _register_tools(self):
|
||||
"""注册所有 Tool,让 Agent 可以主动调用"""
|
||||
|
||||
@@ -1493,7 +1514,7 @@ class CustomerServiceAgent:
|
||||
has_incoming_urls=bool(incoming_urls),
|
||||
):
|
||||
quote_res = await self._quote_pending_images(state, message)
|
||||
reply_text = quote_res.get("reply", "")
|
||||
reply_text = self._colloquialize_reply(quote_res.get("reply", ""))
|
||||
need_transfer = bool(quote_res.get("need_transfer"))
|
||||
state.last_reply_at = datetime.now()
|
||||
print(f"{self.C_REPLY}[REPLY->CUSTOMER]{self.C_RESET} {reply_text}")
|
||||
@@ -1504,7 +1525,7 @@ class CustomerServiceAgent:
|
||||
transfer_msg=TRANSFER_MESSAGE if need_transfer else "",
|
||||
)
|
||||
|
||||
ack = self._build_collect_ack(len(state.pending_image_urls))
|
||||
ack = self._colloquialize_reply(self._build_collect_ack(len(state.pending_image_urls)))
|
||||
state.last_reply_at = datetime.now()
|
||||
print(f"{self.C_REPLY}[REPLY->CUSTOMER]{self.C_RESET} {ack}")
|
||||
return AgentResponse(reply=ack, should_reply=True, need_transfer=False)
|
||||
@@ -1519,7 +1540,7 @@ class CustomerServiceAgent:
|
||||
has_incoming_urls=False,
|
||||
):
|
||||
quote_res = await self._quote_pending_images(state, message)
|
||||
reply_text = quote_res.get("reply", "")
|
||||
reply_text = self._colloquialize_reply(quote_res.get("reply", ""))
|
||||
need_transfer = bool(quote_res.get("need_transfer"))
|
||||
state.last_reply_at = datetime.now()
|
||||
print(f"{self.C_REPLY}[REPLY->CUSTOMER]{self.C_RESET} {reply_text}")
|
||||
@@ -1530,7 +1551,7 @@ class CustomerServiceAgent:
|
||||
transfer_msg=TRANSFER_MESSAGE if need_transfer else "",
|
||||
)
|
||||
|
||||
remind = self._build_collect_remind(len(state.pending_image_urls))
|
||||
remind = self._colloquialize_reply(self._build_collect_remind(len(state.pending_image_urls)))
|
||||
state.last_reply_at = datetime.now()
|
||||
print(f"{self.C_REPLY}[REPLY->CUSTOMER]{self.C_RESET} {remind}")
|
||||
return AgentResponse(reply=remind, should_reply=True, need_transfer=False)
|
||||
@@ -1597,7 +1618,7 @@ class CustomerServiceAgent:
|
||||
result = await target_agent.run(user_prompt, deps=deps, message_history=history)
|
||||
# 更新历史,最多保留最近 30 条消息防止 token 超限
|
||||
self.message_histories[message.from_id] = result.all_messages()[-30:]
|
||||
reply_text = self._normalize_reply_text(result.output)
|
||||
reply_text = self._colloquialize_reply(self._normalize_reply_text(result.output))
|
||||
|
||||
# 价格谈判与信任建立固定策略(避免只回“最低了/先拍下”)
|
||||
strategy_reply = self._negotiation_strategy_reply(message.msg, state)
|
||||
@@ -1678,7 +1699,7 @@ class CustomerServiceAgent:
|
||||
# AI 失败兜底:给一个不出错的万能回复
|
||||
if not reply_text:
|
||||
return AgentResponse(
|
||||
reply="好的稍等,我看一下",
|
||||
reply="好嘞,你稍等下,我这边看一下",
|
||||
should_reply=True,
|
||||
need_transfer=False
|
||||
)
|
||||
@@ -1955,19 +1976,34 @@ class CustomerServiceAgent:
|
||||
return ""
|
||||
|
||||
if any(k in text for k in ["先发效果图", "先看效果", "不放心", "没法确认"]):
|
||||
return (
|
||||
f"小妹整理了一些案例图,亲点这个链接就能看到啦({CASE_LIBRARY_LINK})。"
|
||||
"有什么想要的效果随时可以告诉我哈,我这边都可以按您的要求来做哦~/:065 效果不好不满意,我们这边包退的哦。"
|
||||
)
|
||||
return random.choice([
|
||||
f"小妹整理了一些案例图,亲点这个链接就能看到啦({CASE_LIBRARY_LINK})。有什么想要的效果随时告诉我哈,不满意我们这边包退。",
|
||||
f"先给你看案例哈,链接在这({CASE_LIBRARY_LINK})。你想要什么效果直接说,我们按你要求做,不满意可退。",
|
||||
f"我把类似案例给你准备好了,点这个看就行({CASE_LIBRARY_LINK})。你放心说需求,效果不满意我们包退。",
|
||||
f"怕效果不稳的话你先看案例,链接在这里({CASE_LIBRARY_LINK})。你确认风格后我按你要的做,不满意可以退。",
|
||||
f"可以先看下我们做过的案例({CASE_LIBRARY_LINK})。你觉得方向OK再拍,不满意我们这边支持退。",
|
||||
])
|
||||
|
||||
if "有点贵" in text or "就是贵" in text:
|
||||
# 约定示例:两张优惠价,默认45;若已有单张价格则动态估算两张打包价
|
||||
base = state.last_price if isinstance(state.last_price, int) and state.last_price > 0 else 25
|
||||
two_pack = max(10, round(((base * 2) - 5) / 5) * 5)
|
||||
return f"理解你的顾虑,这样吧,两张一起的话给你算 {two_pack} 元?"
|
||||
return random.choice([
|
||||
f"理解你这边的预算,我给你个实在点的:两张一起按 {two_pack} 元做,行不行?",
|
||||
f"我懂你意思,这样吧,两张一起我给你算 {two_pack} 元。",
|
||||
f"那我给你压一口价,两张打包 {two_pack} 元,你看可以我就开做。",
|
||||
f"没问题,我给你优惠点,两张一起按 {two_pack} 元走。",
|
||||
f"你这边要省点的话,两张一起我给你做到 {two_pack} 元。",
|
||||
])
|
||||
|
||||
if any(k in text for k in ["优惠点", "便宜点", "少点", "打折"]):
|
||||
return "看你要做的数量,3张以上可以给你打包价~"
|
||||
return random.choice([
|
||||
"可以的,你这边数量上来我就好给价,3张以上我给你打包价。",
|
||||
"能优惠,做得多会更划算,3张以上我这边可以给你打包算。",
|
||||
"没问题,3张起我可以给你一口打包价,会比单张省一些。",
|
||||
"可以便宜点,按量走更好谈,3张以上我给你打包价。",
|
||||
"你这边如果是多张做,3张以上我能给你更划算的打包价。",
|
||||
])
|
||||
|
||||
return ""
|
||||
|
||||
@@ -2115,21 +2151,49 @@ class CustomerServiceAgent:
|
||||
elif len(state.pending_image_urls) >= 2:
|
||||
if any(k in msg for k in multi_image_finish_kw) and not any(k in msg for k in hold_kw):
|
||||
return True
|
||||
if self._is_cross_image_composite_intent(msg) and not any(k in msg for k in hold_kw):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def _is_cross_image_composite_intent(text: str) -> bool:
|
||||
"""
|
||||
识别多图跨图修改意图(A图元素放到B图)。
|
||||
例:A图的图案转到B图、这个图案放到另一张上。
|
||||
"""
|
||||
s = (text or "").strip()
|
||||
if not s:
|
||||
return False
|
||||
pair_marks = ("a图", "b图", "第一张", "第二张", "这张", "那张", "上一张", "另一张")
|
||||
op_kw = (
|
||||
"转到", "换到", "放到", "贴到", "移到", "套到", "合成", "融合", "替换到",
|
||||
"图案上去", "字放到", "元素放到", "logo放到",
|
||||
)
|
||||
return any(k in s.lower() for k in pair_marks) and any(k in s for k in op_kw)
|
||||
|
||||
def _build_collect_ack(self, count: int) -> str:
|
||||
if count <= 1:
|
||||
one_templates = [
|
||||
"这张收到啦,还有图就继续发,我一起给你看。",
|
||||
"图我看到了,后面还有就接着发,最后我一口价给你。",
|
||||
"收到这张了,你有其他图也发来,我统一帮你算。",
|
||||
"这张我先记上了,你那边还有的话接着发,我一起给你报。",
|
||||
"第1张收到,你继续发就行,发完我这边一次给你算清楚。",
|
||||
"这张没问题,我先收着。要是还有图,你直接连着发我就行。",
|
||||
"我先看到了这张,你后面还有就一起发来,我统一给你报价。",
|
||||
"这张图我已经记下了,后面有补充就继续甩过来哈。",
|
||||
]
|
||||
return random.choice(one_templates)
|
||||
templates = [
|
||||
"这几张我都收到了(现在{n}张)。还有的话继续发,我一起给你报。",
|
||||
"好嘞,先看到{n}张了。你可以继续发,或者直接说“就这些”我现在就报价。",
|
||||
"收到哈(共{n}张)。你还要补图就继续发,不补的话我现在也可以直接给价。",
|
||||
"我这边先收到了{n}张。你继续补图,或者直接说“按这些算”我就开始报。",
|
||||
"这波我已经记了{n}张,你要是还有就接着发,不补的话我立刻给总价。",
|
||||
"先看到{n}张图了,后面你看是继续发,还是直接让我现在报价都可以。",
|
||||
"好的,目前{n}张到位。你一句“就这些”,我马上给你打包价。",
|
||||
"图我都看到了({n}张)。你还发我就继续收,不发我现在就给你报。",
|
||||
]
|
||||
return random.choice(templates).format(n=count)
|
||||
|
||||
@@ -2138,13 +2202,23 @@ class CustomerServiceAgent:
|
||||
one_templates = [
|
||||
"这个要求我记住了。你还有图就继续发,不补图我就按这张给你报价。",
|
||||
"明白,这个需求我加上了。你继续发图也行,想直接报价也可以。",
|
||||
"收到,我按你这个要求做。你要是就这张,我现在就能给你报价。",
|
||||
"懂你意思,这种能做。要不先按这张来,我现在就给你报个实在价。",
|
||||
"这个需求我收到了。你要是就做这张,我现在就给你报。",
|
||||
"你这要求我记下了,后面还有图就发,没有的话我现在直接算价。",
|
||||
"行,我按你这个要求来。继续补图也行,不补我就先报这张。",
|
||||
"这个点我懂了,你还要补图就接着发,不补我立刻给你报价。",
|
||||
"要求我已经加上了。你看是继续发,还是我现在直接报这张。",
|
||||
]
|
||||
return random.choice(one_templates)
|
||||
templates = [
|
||||
"需求我记下了(当前{n}张)。你继续补图,或者直接说“就这些”我现在报价。",
|
||||
"好,这个要求也加上了(现在{n}张)。不再补图的话我立刻给你打包价。",
|
||||
"收到(共{n}张)。你还发就继续,不发的话我现在就给总价。",
|
||||
"这个需求我加进去了(现在{n}张)。你继续发也行,直接报价也行。",
|
||||
"我这边都记好了({n}张+需求)。你一句“先按这些算”,我马上报价。",
|
||||
"要求同步好了,目前{n}张。要补图继续发,不补图我现在就给你打包价。",
|
||||
"行,需求和图片我都收着了({n}张)。你直接让我报价也可以。",
|
||||
"好的,这条需求也算进去了(共{n}张)。你看要不要我现在直接报。",
|
||||
]
|
||||
return random.choice(templates).format(n=count)
|
||||
|
||||
@@ -2172,7 +2246,7 @@ class CustomerServiceAgent:
|
||||
rules = [
|
||||
(["分层", "psd", "源文件"], 30, "分层/源文件"),
|
||||
(["去背景", "抠图", "透明底", "白底"], 5, "去背景"),
|
||||
(["换背景", "换场景", "合成"], 10, "合成/换背景"),
|
||||
(["换背景", "换场景", "合成", "转到", "换到", "放到", "贴到", "移到", "套到", "图案上去", "元素放到"], 10, "跨图合成/换背景"),
|
||||
(["改字", "改文字", "替换文字", "排版"], 10, "改文字/排版"),
|
||||
(["调色", "改色", "换色", "配色"], 5, "调色"),
|
||||
(["多版本", "多个版本", "两版", "三版"], 10, "多版本"),
|
||||
@@ -2224,6 +2298,11 @@ class CustomerServiceAgent:
|
||||
"这张我看过了,先给你报下:",
|
||||
"这张可以做,价格给你报下:",
|
||||
"看了这张图,报价如下:",
|
||||
"我先按这张给你算下:",
|
||||
"这张处理没问题,我给你报个实在价:",
|
||||
"我看完这张了,价格给你说下:",
|
||||
"按这张图的难度,报价是:",
|
||||
"这张我已经评估完了,先给你个价格:",
|
||||
]
|
||||
lines = [f"{random.choice(heads)}{line.split(':', 1)[1]}"]
|
||||
if req_hit:
|
||||
@@ -2232,6 +2311,11 @@ class CustomerServiceAgent:
|
||||
f"这张做下来共{single_total}元,定了我马上开工。",
|
||||
f"合下来是{single_total}元,你点头我这边立刻安排。",
|
||||
f"总价{single_total}元,可以的话我现在就给你做。",
|
||||
f"这一张算下来{single_total}元,你说开做我就马上弄。",
|
||||
f"给你按{single_total}元做,确定的话我现在就排上。",
|
||||
f"这张我按{single_total}元给你做,没问题就直接开始。",
|
||||
f"这张最终{single_total}元,你点头我立刻开干。",
|
||||
f"这张就按{single_total}元走,你确认我就马上安排。",
|
||||
]
|
||||
lines.append(random.choice(tails))
|
||||
return "\n".join(lines)
|
||||
@@ -2240,14 +2324,31 @@ class CustomerServiceAgent:
|
||||
"我先按这几张给你报一下:",
|
||||
"这几张我都看过了,价格给你列一下:",
|
||||
"我把每张价格先给你说清楚:",
|
||||
"我先把这几张的价格拆开给你看:",
|
||||
"这几张我都评估过了,报价给你写明白:",
|
||||
"先别急,我把每张大概价给你列出来:",
|
||||
"我按这批图先报个明细给你:",
|
||||
"我先把每张费用和总价给你算出来:",
|
||||
]
|
||||
lines = [random.choice(heads)]
|
||||
lines.extend(detail_lines)
|
||||
if req_hit:
|
||||
lines.append(f"需求加价:+{extra}元({req_hit})")
|
||||
option_line = f"可选:按单张做(共{single_total}元),或打包做({bundle_price}元,会更省一点)。"
|
||||
option_line = random.choice([
|
||||
f"可选:按单张做(共{single_total}元),或打包做({bundle_price}元,会更省一点)。",
|
||||
f"可选:单张算下来一共{single_total}元;打包给你{bundle_price}元,更划算。",
|
||||
f"可选:你按单张做共{single_total}元,按打包做我给你{bundle_price}元。",
|
||||
f"可选:分开做总共{single_total}元,打包做{bundle_price}元(省一点)。",
|
||||
f"可选:按张算共{single_total}元;直接打包{bundle_price}元。",
|
||||
])
|
||||
lines.append(option_line)
|
||||
lines.append("你定一个,我这边马上开工。")
|
||||
lines.append(random.choice([
|
||||
"你定一个,我这边马上开工。",
|
||||
"你选个方案,我立刻给你安排上。",
|
||||
"你拍板就行,我这边马上开做。",
|
||||
"你看选哪个合适,我这边马上给你做。",
|
||||
"你一句话定下来,我现在就给你安排。",
|
||||
]))
|
||||
return "\n".join(lines)
|
||||
|
||||
def _prepare_batch_intake(self, state: ConversationState) -> Dict[str, Any]:
|
||||
|
||||
@@ -522,9 +522,10 @@ class QingjianAPIClient:
|
||||
low, high = max(2.0, base * 0.5), base
|
||||
|
||||
# 发图后的需求描述,优先“多等一点”收集完整需求,减少半句回复
|
||||
# 约束到 12-14s,避免等待过长。
|
||||
if is_req and not has_img:
|
||||
low = max(low, min(10.0, base * 0.8))
|
||||
high = max(high, min(20.0, base + 4.0))
|
||||
low = max(low, 12.0)
|
||||
high = min(14.0, max(high, 12.6))
|
||||
|
||||
# 短句更快,长句稍慢,避免把连续半句拆开
|
||||
text_len = len((msg or "").strip())
|
||||
@@ -1460,6 +1461,8 @@ class QingjianAPIClient:
|
||||
print(f"[{self.get_time()}] 错误: 未连接到服务器")
|
||||
return
|
||||
|
||||
reply_content = self._colloquialize_outbound_reply(reply_content)
|
||||
|
||||
# 同一客户外发限流:N 秒内最多 1 条
|
||||
try:
|
||||
from config.config import OUTBOUND_PER_CUSTOMER_COOLDOWN_SECONDS
|
||||
@@ -1499,6 +1502,50 @@ class QingjianAPIClient:
|
||||
self._log_outbound_once(original_msg, str(reply_content))
|
||||
await self.send_message(reply)
|
||||
|
||||
def _colloquialize_outbound_reply(self, text: Any) -> Any:
|
||||
"""统一外发口语化处理,避免机械话术。"""
|
||||
if not isinstance(text, str):
|
||||
return text
|
||||
raw = text.strip()
|
||||
if not raw:
|
||||
return text
|
||||
# 控制指令/转接命令不得改写
|
||||
if raw.startswith("话术|") or "[转移会话]" in raw:
|
||||
return text
|
||||
# 纯链接不改
|
||||
if re.fullmatch(r"https?://\S+", raw):
|
||||
return text
|
||||
|
||||
out = raw
|
||||
replacements = {
|
||||
"我这边": "我这边",
|
||||
"请您": "你",
|
||||
"您好": "你好",
|
||||
"稍后": "一会儿",
|
||||
"可以的话": "可以的话",
|
||||
"请稍等": "稍等哈",
|
||||
"先不乱报价": "先不急着给你乱报",
|
||||
"建议转人工评估更稳": "建议转人工看会更稳",
|
||||
"统一报价": "一起报价",
|
||||
"马上安排": "马上给你安排",
|
||||
"确认我就安排": "你点头我就开做",
|
||||
"收到,我看看哈": "收到,我先看下",
|
||||
"收到,我找找刚才那几张": "收到,我把刚才那几张一起看下",
|
||||
"这组图我这边暂时识别不稳定": "这组图我这边识别得不太稳",
|
||||
"这组图我这边暂时识别异常": "这组图我这边刚才识别有点异常",
|
||||
"你可以换一张更清晰的,我再给你准报价。": "你换张更清晰的发我,我再给你报准点。",
|
||||
"你可以换清晰图再发我。": "你换张清晰点的再发我哈。",
|
||||
"你可以稍后再发我。": "你晚点再发我也行。",
|
||||
"收到付款,我马上安排处理,有需要第一时间联系您": "收到付款啦,我马上安排处理,有进展第一时间告诉你",
|
||||
"亲,正在为您转接人工客服,请稍等~": "我这就给你转人工,稍等哈~",
|
||||
}
|
||||
for k, v in replacements.items():
|
||||
out = out.replace(k, v)
|
||||
|
||||
# 收尾语气柔化
|
||||
out = out.replace("。", "。")
|
||||
return out
|
||||
|
||||
async def send_text(self, cy_id, acc_type, content):
|
||||
"""
|
||||
主动发送文本消息
|
||||
|
||||
Reference in New Issue
Block a user