package language import ( "strings" "ai-operator/internal/dialogue/state" ) type Detector struct{} func NewDetector() *Detector { return &Detector{} } func (d *Detector) Normalize(input string) string { return Normalize(input) } func (d *Detector) DetectToolLanguage(value string) DetectionResult { return d.Detect(value, SourceToolArgs) } func (d *Detector) Detect(input string, source DetectionSource) DetectionResult { n := Normalize(input) res := DetectionResult{Language: LanguageUnknown, Intent: IntentNotLanguage, Source: source, NormalizedText: n, ReasonCode: "no_match"} if n == "" { res.ReasonCode = "empty_input" res.NeedsClarification = true return res } if len([]rune(n)) < 2 { res.ReasonCode = "too_short" return res } if containsAny(n, ambiguousPhrases) || mentionsBoth(n) { res.Intent = IntentAmbiguous res.ReasonCode = "ambiguous_ru_kk" res.Confidence = 0.4 res.NeedsClarification = true return res } if containsAny(n, falsePositivePhrases) { res.ReasonCode = "no_match" return res } if reason, ok := ruExact[n]; ok { return result(LanguageRU, IntentLanguageSelect, 0.99, n, n, source, reason, false) } if reason, ok := kkExact[n]; ok { return result(LanguageKK, IntentLanguageSelect, 0.95, n, n, source, reason, false) } if d.IsExplicitChangeRequest(n) { if p := phraseMatch(n, explicitRu); p != "" { return result(LanguageRU, IntentLanguageChange, 0.95, p, n, source, "explicit_language_request", false) } if p := phraseMatch(n, explicitKK); p != "" { return result(LanguageKK, IntentLanguageChange, 0.95, p, n, source, "explicit_language_request", false) } return DetectionResult{Language: LanguageUnknown, Intent: IntentAmbiguous, Confidence: 0.5, NormalizedText: n, Source: source, ReasonCode: "explicit_language_request", NeedsClarification: true} } if p := exactOrPhrase(n, ruPhrases); p != "" { conf := 0.95 if n == "я русский" { conf = 0.55 } return result(LanguageRU, IntentLanguageSelect, conf, p, n, source, "exact_phrase", conf < MediumConfidence) } if p := exactOrPhrase(n, kkPhrases); p != "" { return result(LanguageKK, IntentLanguageSelect, 0.95, p, n, source, "exact_phrase", false) } if p := exactOrPhrase(n, ruASR); p != "" { return result(LanguageRU, IntentLanguageSelect, 0.80, p, n, source, "weak_match", false) } if p := exactOrPhrase(n, kkASR); p != "" { return result(LanguageKK, IntentLanguageSelect, 0.80, p, n, source, "weak_match", false) } if n == "я русский" { return result(LanguageRU, IntentAmbiguous, 0.55, n, n, source, "weak_match", true) } if n == "я казах" { return result(LanguageKK, IntentAmbiguous, 0.55, n, n, source, "weak_match", true) } return res } func (d *Detector) IsExplicitChangeRequest(input string) bool { n := Normalize(input) return containsAny(n, explicitChangeMarkers) || containsAny(n, explicitRu) || containsAny(n, explicitKK) } func ShouldApplyLanguageDetection(ctx SelectionContext, res DetectionResult) LanguageDecision { if ctx.CurrentState == state.StateEnded || ctx.CurrentState == state.StateHandoff || ctx.CurrentState == state.StateClosing { return LanguageDecision{ReasonCode: "state_not_allowed", MessageKey: "language.change.denied"} } if res.NeedsClarification || res.Intent == IntentAmbiguous { return LanguageDecision{NeedsClarification: true, ReasonCode: res.ReasonCode, MessageKey: "language.ask_clarify"} } if res.Language != LanguageRU && res.Language != LanguageKK { return LanguageDecision{ReasonCode: res.ReasonCode, MessageKey: "language.not_understood"} } switch ctx.CurrentState { case state.StateLanguageSelection: if res.Confidence >= MediumConfidence && res.Intent == IntentLanguageSelect { return LanguageDecision{Apply: true, Language: res.Language, ReasonCode: res.ReasonCode, MessageKey: "language.selected." + string(res.Language)} } case state.StateRegionSelection: if res.Confidence >= HighConfidence && (res.Intent == IntentLanguageChange || res.Intent == IntentLanguageSelect) { return LanguageDecision{Apply: true, Language: res.Language, ReasonCode: res.ReasonCode, MessageKey: "language.changed." + string(res.Language)} } case state.StateReadyToHelp, state.StateQuestionAnswering: if ctx.AllowChange && res.Intent == IntentLanguageChange && res.Confidence >= HighConfidence { return LanguageDecision{Apply: true, Language: res.Language, ReasonCode: res.ReasonCode, MessageKey: "language.changed." + string(res.Language)} } } return LanguageDecision{ReasonCode: "mixed_language_no_explicit_switch", MessageKey: "language.not_understood"} } func result(lang Language, intent DetectionIntent, conf float64, phrase, norm string, src DetectionSource, reason string, clarify bool) DetectionResult { return DetectionResult{Language: lang, Intent: intent, Confidence: conf, MatchedPhrase: phrase, NormalizedText: norm, Source: src, ReasonCode: reason, NeedsClarification: clarify} } func exactOrPhrase(n string, phrases []string) string { for _, p := range phrases { p = Normalize(p) if n == p || strings.Contains(n, p) && len([]rune(p)) > 4 { return p } } return "" } func phraseMatch(n string, phrases []string) string { return exactOrPhrase(n, phrases) } func containsAny(n string, phrases []string) bool { return exactOrPhrase(n, phrases) != "" } func mentionsBoth(n string) bool { ru := exactOrPhrase(n, append(ruPhrases, ruASR...)) != "" || strings.Contains(" "+n+" ", " ru ") kk := exactOrPhrase(n, append(kkPhrases, kkASR...)) != "" || strings.Contains(" "+n+" ", " kk ") return ru && kk }