313 lines
12 KiB
Go
313 lines
12 KiB
Go
package ai
|
||
|
||
import (
|
||
"strings"
|
||
"unicode"
|
||
)
|
||
|
||
// IsCustomerHandoffRequest determines whether a user's message expresses an intent
|
||
// to speak with a human operator, agent, specialist, manager, or live person, across
|
||
// varied natural phrasing, colloquialisms, and multiple languages (Russian, English,
|
||
// Spanish, German, French, Chinese, etc.).
|
||
func IsCustomerHandoffRequest(text string) bool {
|
||
raw := strings.TrimSpace(text)
|
||
if raw == "" {
|
||
return false
|
||
}
|
||
lower := strings.ToLower(raw)
|
||
|
||
// Direct Chinese keywords
|
||
if IsHan(lower) {
|
||
for _, kw := range []string{
|
||
"人工", "转人工", "真人", "人工客服", "接人工", "转接人工", "转坐席", "人工服务", "人工专员",
|
||
} {
|
||
if strings.Contains(lower, kw) {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
|
||
// Clean punctuation for token / n-gram checks
|
||
cleaned := strings.Map(func(r rune) rune {
|
||
if unicode.IsPunct(r) || unicode.IsSymbol(r) {
|
||
return ' '
|
||
}
|
||
return r
|
||
}, lower)
|
||
fields := strings.Fields(cleaned)
|
||
if len(fields) == 0 {
|
||
return false
|
||
}
|
||
normalized := " " + strings.Join(fields, " ") + " "
|
||
|
||
// Russian handoff patterns & stems:
|
||
// Anti-bot phrases: "хватит бота", "не хочу бота", "отключи бота", "надоел бот"
|
||
if strings.Contains(normalized, " хватит бота ") ||
|
||
strings.Contains(normalized, " хватит робота ") ||
|
||
strings.Contains(normalized, " не хочу бота ") ||
|
||
strings.Contains(normalized, " не хочу общаться с ботом ") ||
|
||
strings.Contains(normalized, " надоел бот ") ||
|
||
strings.Contains(normalized, " отключи бота ") ||
|
||
strings.Contains(normalized, " без бота ") ||
|
||
strings.Contains(normalized, " убрать бота ") ||
|
||
strings.Contains(normalized, " stop bot ") ||
|
||
strings.Contains(normalized, " no bot ") ||
|
||
strings.Contains(normalized, " hate bot ") ||
|
||
strings.Contains(normalized, " not a bot ") {
|
||
return true
|
||
}
|
||
|
||
russianTargetStems := []string{
|
||
"оператор", "человек", "специалист", "сотрудник", "консультант",
|
||
"менеджер", "живо", "живой", "поддержк", "саппорт", "люди", "людей",
|
||
"админ", "администратор",
|
||
}
|
||
|
||
russianActionStems := []string{
|
||
"переведи", "переключ", "соедин", "позов", "дайт", "дай", "свяж", "связать",
|
||
"вызов", "вызовите", "хочу", "нужен", "нужна", "нужно", "можно", "позовите",
|
||
"соедините", "переведите", "переключите", "пригласи", "пригласите",
|
||
"перенаправ", "перенаправь", "перенаправьте", "передай", "передайте",
|
||
"направь", "направьте",
|
||
}
|
||
|
||
// Action + Target combinations in Russian (e.g. "позови сотрудника", "соедини со специалистом", "хочу человека")
|
||
for _, action := range russianActionStems {
|
||
if strings.Contains(normalized, " "+action) {
|
||
for _, target := range russianTargetStems {
|
||
if strings.Contains(normalized, " "+target) {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Single-word requests (e.g. user just typed "оператор", "человек", "operator", "human")
|
||
if normalized == " оператор " ||
|
||
normalized == " оператора " ||
|
||
normalized == " человек " ||
|
||
normalized == " специалист " ||
|
||
normalized == " support " ||
|
||
normalized == " operator " ||
|
||
normalized == " agent " ||
|
||
normalized == " human " {
|
||
return true
|
||
}
|
||
|
||
// Direct targets in Russian with prepositions or adjectives (e.g. "к оператору", "с человеком", "со специалистом", "живой человек")
|
||
if strings.Contains(normalized, " к оператору") ||
|
||
strings.Contains(normalized, " на оператора") ||
|
||
strings.Contains(normalized, " с оператором") ||
|
||
strings.Contains(normalized, " со специалистом") ||
|
||
strings.Contains(normalized, " к специалисту") ||
|
||
strings.Contains(normalized, " с человеком") ||
|
||
strings.Contains(normalized, " с сотрудником") ||
|
||
strings.Contains(normalized, " живой человек") ||
|
||
strings.Contains(normalized, " живого человека") ||
|
||
strings.Contains(normalized, " живым человеком") ||
|
||
strings.Contains(normalized, " поговорить с кем") ||
|
||
strings.Contains(normalized, " поговорить со ") ||
|
||
strings.Contains(normalized, " реальный человек") ||
|
||
strings.Contains(normalized, " живой есть") ||
|
||
strings.Contains(normalized, " есть живой") ||
|
||
strings.Contains(normalized, " есть кто живой") ||
|
||
strings.Contains(normalized, " можно человека") ||
|
||
strings.Contains(normalized, " дай человека") ||
|
||
strings.Contains(normalized, " дай оператора") {
|
||
return true
|
||
}
|
||
|
||
// English handoff patterns
|
||
englishTargets := []string{
|
||
"operator", "agent", "human", "representative", "person", "specialist", "someone", "support",
|
||
}
|
||
englishActions := []string{
|
||
"talk to", "speak to", "speak with", "connect", "transfer", "get me", "call", "reach", "need", "want", "let me",
|
||
}
|
||
|
||
for _, action := range englishActions {
|
||
if strings.Contains(normalized, " "+action) {
|
||
for _, target := range englishTargets {
|
||
if strings.Contains(normalized, " "+target) {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
if strings.Contains(normalized, " real person ") ||
|
||
strings.Contains(normalized, " live agent ") ||
|
||
strings.Contains(normalized, " live person ") ||
|
||
strings.Contains(normalized, " human agent ") ||
|
||
strings.Contains(normalized, " human please ") ||
|
||
strings.Contains(normalized, " customer service ") ||
|
||
strings.Contains(normalized, " human support ") ||
|
||
strings.Contains(normalized, " talk to someone ") ||
|
||
strings.Contains(normalized, " speak to someone ") ||
|
||
strings.Contains(normalized, " representative please ") ||
|
||
strings.Contains(normalized, " representative ") {
|
||
return true
|
||
}
|
||
|
||
// Spanish handoff patterns
|
||
if strings.Contains(normalized, " hablar con ") ||
|
||
strings.Contains(normalized, " pasar con ") ||
|
||
strings.Contains(normalized, " contactar con ") ||
|
||
strings.Contains(normalized, " quiero hablar con ") ||
|
||
strings.Contains(normalized, " necesito hablar con ") {
|
||
if strings.Contains(normalized, " persona") ||
|
||
strings.Contains(normalized, " humano") ||
|
||
strings.Contains(normalized, " agente") ||
|
||
strings.Contains(normalized, " operador") ||
|
||
strings.Contains(normalized, " alguien") {
|
||
return true
|
||
}
|
||
}
|
||
if strings.Contains(normalized, " atencion al cliente") ||
|
||
strings.Contains(normalized, " persona real") ||
|
||
strings.Contains(normalized, " agente humano") {
|
||
return true
|
||
}
|
||
|
||
// German handoff patterns
|
||
if strings.Contains(normalized, " mitarbeiter ") ||
|
||
strings.Contains(normalized, " echter mensch ") ||
|
||
strings.Contains(normalized, " mit jemandem sprechen ") {
|
||
return true
|
||
}
|
||
|
||
// French handoff patterns
|
||
if strings.Contains(normalized, " parler a quelqu un ") ||
|
||
strings.Contains(normalized, " conseiller ") ||
|
||
strings.Contains(normalized, " agent humain ") ||
|
||
strings.Contains(normalized, " etre mis en relation ") {
|
||
return true
|
||
}
|
||
|
||
return false
|
||
}
|
||
|
||
// IsModelClaimingHandoff checks whether an AI model's text reply claims that it is
|
||
// transferring, connecting, or forwarding the conversation to a human operator/agent.
|
||
func IsModelClaimingHandoff(text string) bool {
|
||
raw := strings.TrimSpace(text)
|
||
if raw == "" {
|
||
return false
|
||
}
|
||
lower := strings.ToLower(raw)
|
||
|
||
// Direct Russian phrases (exact matches)
|
||
russianModelClaims := []string{
|
||
"перевожу вас на оператора",
|
||
"переключаю вас на оператора",
|
||
"перевожу на оператора",
|
||
"переключаю на оператора",
|
||
"соединяю с оператором",
|
||
"соединяю со специалистом",
|
||
"передаю диалог оператору",
|
||
"передаю ваш запрос оператору",
|
||
"перевожу диалог на оператора",
|
||
"переключаю на специалиста",
|
||
"перевожу на специалиста",
|
||
"соединяю с живым",
|
||
"связываю вас с оператором",
|
||
"сейчас переведу на оператора",
|
||
"сейчас переключу на оператора",
|
||
"перенаправляю оператору",
|
||
"перенаправляю вас к оператору",
|
||
"перенаправляю вас на оператора",
|
||
"перенаправляю на оператора",
|
||
"перенаправляю вас на специалиста",
|
||
"перенаправляю на специалиста",
|
||
"перенаправляю в службу поддержки",
|
||
"перенаправляю вас",
|
||
"передаю вас оператору",
|
||
"передаю диалог",
|
||
"направляю диалог",
|
||
"направляю ваш запрос",
|
||
}
|
||
for _, claim := range russianModelClaims {
|
||
if strings.Contains(lower, claim) {
|
||
return true
|
||
}
|
||
}
|
||
|
||
// Flexible Russian stem matching (Action + Target)
|
||
// Catches arbitrary phrasing like "Похоже я не могу решить ваш вопрос, перенаправляю оператору. Ожидайте!"
|
||
ruActionStems := []string{"перенаправл", "перевод", "переключ", "соедин", "передаю", "передаем", "передам", "направляю", "направляем", "направлю", "связываю", "свяжу"}
|
||
ruTargetStems := []string{"оператор", "специалист", "сотрудник", "человек", "поддержк", "менеджер", "коллег", "живой"}
|
||
for _, act := range ruActionStems {
|
||
if strings.Contains(lower, act) {
|
||
for _, tgt := range ruTargetStems {
|
||
if strings.Contains(lower, tgt) {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// English model handoff statements
|
||
englishModelClaims := []string{
|
||
"transferring you to an operator",
|
||
"transferring you to an agent",
|
||
"transferring you to a human",
|
||
"transferring you to human",
|
||
"transferring you to our support",
|
||
"connecting you to an operator",
|
||
"connecting you to an agent",
|
||
"connecting you to a human",
|
||
"connecting you with an agent",
|
||
"connecting you with an operator",
|
||
"routing you to an agent",
|
||
"handing you over to",
|
||
}
|
||
for _, claim := range englishModelClaims {
|
||
if strings.Contains(lower, claim) {
|
||
return true
|
||
}
|
||
}
|
||
|
||
// Flexible English stem matching (Action + Target)
|
||
enActionStems := []string{"transfer", "connect", "rout", "hand over", "handing over", "forward", "pass you", "passing you"}
|
||
enTargetStems := []string{"operator", "agent", "human", "specialist", "representative", "support", "colleague"}
|
||
for _, act := range enActionStems {
|
||
if strings.Contains(lower, act) {
|
||
for _, tgt := range enTargetStems {
|
||
if strings.Contains(lower, tgt) {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
// Chinese model handoff statements
|
||
chineseModelClaims := []string{
|
||
"为您转接人工",
|
||
"正在为您转接",
|
||
"为您接入人工",
|
||
"转接人工客服",
|
||
"转接人工",
|
||
"已为您转接",
|
||
}
|
||
for _, claim := range chineseModelClaims {
|
||
if strings.Contains(lower, claim) {
|
||
return true
|
||
}
|
||
}
|
||
|
||
// Flexible Chinese stem matching
|
||
zhActionStems := []string{"转接", "转交", "接入", "转给", "转到", "交由"}
|
||
zhTargetStems := []string{"人工", "客服", "专员", "同事"}
|
||
for _, act := range zhActionStems {
|
||
if strings.Contains(lower, act) {
|
||
for _, tgt := range zhTargetStems {
|
||
if strings.Contains(lower, tgt) {
|
||
return true
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return false
|
||
}
|