Files
agent-desk-ng/internal/ai/handoff_intent.go

313 lines
12 KiB
Go
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.

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
}