package domain import ( "errors" "strings" "time" "unicode/utf8" "git.cin.su/m/Assistant/contracts/identity" "git.cin.su/m/Assistant/contracts/interaction" "git.cin.su/m/Assistant/platform/ids" ) const MaxHumanUtteranceBytes = 16 << 10 var ( ErrEmptyMessage = errors.New("empty human message") ErrMessageTooLarge = errors.New("human message too large") ErrInvalidPrincipal = errors.New("invalid interaction principal") ) type Clock func() time.Time func NewConversation(actorID identity.ActorID, principalID identity.PrincipalID, now time.Time) (interaction.Conversation, error) { if actorID == "" || principalID == "" { return interaction.Conversation{}, ErrInvalidPrincipal } id, err := ids.New() if err != nil { return interaction.Conversation{}, err } millis := now.UnixMilli() return interaction.Conversation{ ID: interaction.ConversationID(id), ActorID: actorID, PrincipalID: principalID, Channel: interaction.ChannelWebChat, State: interaction.ConversationOpen, CreatedAtUnixMillis: millis, UpdatedAtUnixMillis: millis, }, nil } func NewHumanUtterance(conversation interaction.Conversation, request interaction.ChatMessageRequest, now time.Time) (interaction.HumanUtterance, interaction.OriginalHumanIntent, []interaction.InteractionRelation, error) { message := strings.TrimSpace(request.Message) if message == "" { return interaction.HumanUtterance{}, interaction.OriginalHumanIntent{}, nil, ErrEmptyMessage } if len(message) > MaxHumanUtteranceBytes || !utf8.ValidString(message) { return interaction.HumanUtterance{}, interaction.OriginalHumanIntent{}, nil, ErrMessageTooLarge } utteranceID, err := ids.New() if err != nil { return interaction.HumanUtterance{}, interaction.OriginalHumanIntent{}, nil, err } intentID, err := ids.New() if err != nil { return interaction.HumanUtterance{}, interaction.OriginalHumanIntent{}, nil, err } millis := now.UnixMilli() candidate := BuildDeterministicCandidate(message, request.CorrectsUtteranceID, millis) speechAct := candidate.PrimarySpeechAct utterance := interaction.HumanUtterance{ ID: interaction.HumanUtteranceID(utteranceID), ConversationID: conversation.ID, ActorID: conversation.ActorID, PrincipalID: conversation.PrincipalID, Channel: interaction.ChannelWebChat, ReceivedAtUnixMillis: millis, CorrelationID: request.CorrelationID, Content: message, ContentRef: "interaction:utterance:" + string(utteranceID), SpeechAct: speechAct, InterpretationState: interaction.InterpretationInterpreted, CorrectsUtteranceID: request.CorrectsUtteranceID, } intent := interaction.OriginalHumanIntent{ ID: interaction.OriginalHumanIntentID(intentID), UtteranceID: utterance.ID, RecordedAtUnixMillis: millis, Text: message, } var relations []interaction.InteractionRelation if request.CorrectsUtteranceID != "" || speechAct == interaction.SpeechActCorrection { relationID, err := ids.New() if err != nil { return interaction.HumanUtterance{}, interaction.OriginalHumanIntent{}, nil, err } relations = append(relations, interaction.InteractionRelation{ ID: interaction.InteractionRelationID(relationID), FromUtteranceID: utterance.ID, ToUtteranceID: request.CorrectsUtteranceID, Kind: interaction.RelationCorrects, RecordedAtUnixMillis: millis, }) } return utterance, intent, relations, nil } func ClassifySpeechAct(message string, corrects interaction.HumanUtteranceID) interaction.SpeechAct { return BuildDeterministicCandidate(message, corrects, time.Now().UnixMilli()).PrimarySpeechAct } func NeedsCognition(act interaction.SpeechAct) bool { return act == interaction.SpeechActQuestion || act == interaction.SpeechActRequestToInvestigate } func FallbackText(act interaction.SpeechAct) string { switch act { case interaction.SpeechActDirective: return "Я понял действие как просьбу, но без отдельного Authority/Commit шага не выполняю мутацию." case interaction.SpeechActAssertion: return "Я записал это как утверждение человека. Это не считается проверенным фактом." case interaction.SpeechActCorrection: return "Исправление записано отдельной репликой; история не переписана." case interaction.SpeechActPreference: return "Хорошо, учту это дальше." case interaction.SpeechActQuestion, interaction.SpeechActRequestToInvestigate: return "Когнитивный маршрут сейчас недоступен, но сообщение сохранено и может быть обработано позже." default: return "Сообщение записано." } } // PreferredAssistantName extracts a name the human explicitly assigns to the // assistant. It is intentionally based on naming intent and nearby words, not // on a whole-message comparison, so it remains useful with ordinary variations // of the same request. func PreferredAssistantName(message string) string { tokens := semanticTokens(message) if !hasAssistantNamingTarget(tokens) { return "" } for index, token := range tokens { if !containsStem(token, "называ", "назов", "зов", "имен") { continue } for _, candidate := range tokens[index+1:] { if candidate == "тебя" || candidate == "тебе" || candidate == "себя" || candidate == "меня" || candidate == "в" || candidate == "теперь" || candidate == "же" { continue } if candidate == "буду" || candidate == "будет" || candidate == "запомни" || candidate == "помни" || candidate == "ты" || candidate == "это" { continue } if len([]rune(candidate)) < 2 || containsStem(candidate, "тогда", "дальше", "пожалуйста") { break } runes := []rune(candidate) runes[0] = []rune(strings.ToUpper(string(runes[0])))[0] return normalizeAssistantName(string(runes)) } } return "" } func normalizeAssistantName(value string) string { if strings.HasSuffix(value, "ю") { return strings.TrimSuffix(value, "ю") + "я" } return value } // IsIdentityInquiry identifies a request about the assistant itself by its // interrogative meaning. Punctuation is optional and wording may vary. func IsIdentityInquiry(message string) bool { tokens := semanticTokens(message) if len(tokens) == 0 { return false } if containsToken(tokens, "кто") && (containsToken(tokens, "ты") || containsToken(tokens, "вы") || containsToken(tokens, "тебя")) { return true } return containsToken(tokens, "зовут") || containsToken(tokens, "имя") } // IsExplicitMemoryInstruction recognizes a direct request to retain the // following human-provided context. It deliberately does not interpret the // content as a verified fact: the conversation owner keeps the original // utterance and its provenance. func IsExplicitMemoryInstruction(message string) bool { tokens := semanticTokens(message) return len(tokens) > 0 && containsStem(tokens[0], "запомни", "помни") } func BuildDeterministicCandidate(message string, corrects interaction.HumanUtteranceID, millis int64) interaction.SemanticCandidate { id, _ := ids.New() tokens := semanticTokens(message) hasQuestionMark := strings.Contains(message, "?") scores := semanticScores(tokens, hasQuestionMark, corrects != "") primary, primaryScore := topAct(scores) secondary, secondaryScore := secondAct(scores, primary) if (startsWithModalRequest(tokens) || startsWithExplanatoryRequest(tokens, "расска", "раска", "объяс", "опиш", "покаж", "подскаж")) && (primary == interaction.SpeechActAssertion || primary == interaction.SpeechActDirective) && (secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate) { primary, secondary = secondary, primary } if primary == interaction.SpeechActQuestion && !hasQuestionMark && !containsMeaningfulInterrogative(tokens) && !startsWithModalRequest(tokens) && !startsWithExplanatoryRequest(tokens, "расска", "раска", "объяс", "опиш", "покаж", "подскаж") { if len(tokens) > 0 && containsStem(tokens[0], "сдел", "исправ", "добав", "удал", "напом", "подключ", "обрати", "спрос", "посчитай", "рассчитай", "использ", "вывед") { primary, secondary = interaction.SpeechActDirective, interaction.SpeechActUnknown } else { primary, secondary = interaction.SpeechActAssertion, interaction.SpeechActUnknown } } candidate := interaction.SemanticCandidate{ ID: interaction.SemanticCandidateID(id), UtteranceID: "", Source: interaction.CandidateSourceDeterministic, SelectionState: interaction.CandidateObserved, PrimarySpeechAct: primary, SecondarySpeechAct: secondary, ConfidenceMillis: boundedConfidence(primaryScore, secondaryScore, scores), Applicability: applicabilityFor(primary, secondary, primaryScore, secondaryScore, tokens, corrects != ""), RequestedOutcome: requestedOutcome(message, primary), Clarification: interaction.ClarificationNotRequired, NeedsCognition: needsCognitionForCandidate(primary, secondary, tokens), SelectionReason: deterministicReason(primary, secondary, tokens), CapabilityHints: capabilityHintsFor(primary, secondary, tokens), RecordedAtUnixMillis: millis, } if candidate.PrimarySpeechAct == interaction.SpeechActUnknown { candidate.PrimarySpeechAct = interaction.SpeechActAssertion } if startsWithDirectiveWhatNibud(tokens) { candidate.NeedsCognition = false candidate.CapabilityHints = removeCapabilityHint(candidate.CapabilityHints, "COGNITIVE_REASONING") } if candidate.Applicability == interaction.SemanticCandidateClarify { candidate.Clarification = interaction.ClarificationRequired candidate.NeedsCognition = false } return candidate } func semanticTokens(message string) []string { lower := strings.ToLower(strings.TrimSpace(message)) replacer := strings.NewReplacer(".", " ", ",", " ", "!", " ", "?", " ", ":", " ", ";", " ", "(", " ", ")", " ", "\"", " ", "'", " ", "«", " ", "»", " ", "+", " + ", "-", " - ", "/", " / ") normalized := replacer.Replace(lower) return strings.Fields(normalized) } func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bool) map[interaction.SpeechAct]int { scores := map[interaction.SpeechAct]int{ interaction.SpeechActAssertion: 1, interaction.SpeechActDirective: 0, interaction.SpeechActQuestion: 0, interaction.SpeechActRequestToInvestigate: 0, interaction.SpeechActCorrection: 0, interaction.SpeechActDecision: 0, interaction.SpeechActPreference: 0, interaction.SpeechActHypothesis: 0, interaction.SpeechActConfirmation: 0, interaction.SpeechActRejection: 0, interaction.SpeechActConversational: 0, } if hasQuestionMark { scores[interaction.SpeechActQuestion]++ } if containsMeaningfulInterrogative(tokens) { scores[interaction.SpeechActQuestion] += 2 } if explicitCorrection { scores[interaction.SpeechActCorrection] += 6 } if containsStemInTokens(tokens, "опечат", "ошибк", "исправлен") && containsStemInTokens(tokens, "исправ", "поправ") { scores[interaction.SpeechActCorrection] += 6 } first := "" if len(tokens) > 0 { first = tokens[0] } if first == "нет" || first == "не" { scores[interaction.SpeechActCorrection] += 2 scores[interaction.SpeechActRejection]++ } if first == "да" { scores[interaction.SpeechActConfirmation] += 2 } questionWords := map[string]struct{}{"кто": {}, "что": {}, "как": {}, "почему": {}, "зачем": {}, "где": {}, "когда": {}, "сколько": {}, "какой": {}, "какая": {}, "какие": {}, "чей": {}} conversationWords := []string{"привет", "здравствуйте", "спасибо", "ку", "здравствуй"} confirmationWords := []string{"хорошо", "ладно", "окей", "ок", "понял", "понятно"} investigateStems := []string{"разбер", "проверь", "проанализ", "исслед", "выясн", "уточн", "подум", "понят", "посмотри", "попроб", "повтор", "снов"} directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "спрос", "найд", "посчитай", "рассчитай", "ответь", "вывед"} explanatoryRequestStems := []string{"расска", "раска", "объяс", "опиш", "покаж", "подскаж", "сообщ"} preferenceStems := []string{"предпоч", "хочу", "лучше", "не используй"} hypothesisStems := []string{"думаю", "кажется", "гипотез", "похоже"} decisionStems := []string{"решил", "решение", "выбираю"} arithmeticMarker := false for index, token := range tokens { if _, ok := questionWords[token]; ok && !(token == "что" && index+1 < len(tokens) && tokens[index+1] == "нибудь") { scores[interaction.SpeechActQuestion] += 3 } if containsStem(token, conversationWords...) { scores[interaction.SpeechActConversational] += 3 } if containsExactToken(token, confirmationWords...) { scores[interaction.SpeechActConfirmation] += 3 } if containsStem(token, investigateStems...) { scores[interaction.SpeechActRequestToInvestigate] += 3 scores[interaction.SpeechActQuestion]++ } if containsStem(token, directiveStems...) { scores[interaction.SpeechActDirective] += 3 } if containsStem(token, preferenceStems...) { scores[interaction.SpeechActPreference] += 3 } if containsStem(token, hypothesisStems...) { scores[interaction.SpeechActHypothesis] += 3 } if containsStem(token, decisionStems...) { scores[interaction.SpeechActDecision] += 3 } if token == "+" || token == "-" || token == "/" || token == "*" || token == "плюс" || token == "минус" { arithmeticMarker = true } } if preferredNameFromTokens(tokens) { scores[interaction.SpeechActPreference] += 8 scores[interaction.SpeechActAssertion] = 0 } if containsToken(tokens, "запомни") && containsStemInTokens(tokens, "нрав", "люб", "предпочит") { scores[interaction.SpeechActPreference] += 6 scores[interaction.SpeechActAssertion] = 0 } if len(tokens) >= 2 && tokens[0] == "я" && containsStemInTokens(tokens[1:], "настро", "сдела", "исправ", "удал", "созда", "перезапуст", "поменя", "добави") { scores[interaction.SpeechActAssertion] += 5 } if arithmeticMarker { scores[interaction.SpeechActQuestion] += 2 scores[interaction.SpeechActDirective]++ } if len(tokens) <= 4 { for index, token := range tokens { if _, ok := questionWords[token]; ok && !(token == "что" && index+1 < len(tokens) && tokens[index+1] == "нибудь") { scores[interaction.SpeechActQuestion]++ } } } if len(tokens) == 2 && tokens[0] == "ты" { scores[interaction.SpeechActQuestion] += 2 } if len(tokens) >= 2 && tokens[0] == "как" && containsToken(tokens, "зовут") { scores[interaction.SpeechActQuestion] += 2 } if startsWithDirectiveWhatNibud(tokens) { scores[interaction.SpeechActDirective] += 4 } if len(tokens) > 0 && containsStem(tokens[0], investigateStems...) { scores[interaction.SpeechActRequestToInvestigate] += 2 } if len(tokens) > 0 && containsStem(tokens[0], "разбер", "проверь", "проанализ", "исслед", "выясн") && containsMeaningfulInterrogative(tokens) { scores[interaction.SpeechActRequestToInvestigate] += 2 } if startsWithExplanatoryRequest(tokens, explanatoryRequestStems...) { scores[interaction.SpeechActQuestion] += 2 scores[interaction.SpeechActRequestToInvestigate] += 3 } if startsWithModalRequest(tokens) && containsStemInTokens(tokens, explanatoryRequestStems...) { scores[interaction.SpeechActQuestion] += 3 scores[interaction.SpeechActRequestToInvestigate] += 3 } if containsModelReference(tokens) { if containsStemInTokens(tokens, "обрати", "спрос", "уточни") { scores[interaction.SpeechActDirective] += 3 scores[interaction.SpeechActQuestion] += 2 } } if containsStemInTokens(tokens, "замени", "смени") { scores[interaction.SpeechActDirective] += 3 } if !hasQuestionMark && !containsInterrogative(tokens) && !startsWithModalRequest(tokens) && !startsWithExplanatoryRequest(tokens, explanatoryRequestStems...) && scores[interaction.SpeechActDirective] == 0 && scores[interaction.SpeechActRequestToInvestigate] == 0 && scores[interaction.SpeechActCorrection] == 0 { scores[interaction.SpeechActAssertion] += 2 } return scores } func preferredNameFromTokens(tokens []string) bool { if !hasAssistantNamingTarget(tokens) { return false } for index, token := range tokens { if !containsStem(token, "называ", "назов", "зов", "имен") { continue } for _, candidate := range tokens[index+1:] { if candidate == "тебя" || candidate == "тебе" || candidate == "себя" || candidate == "меня" || candidate == "буду" || candidate == "запомни" || candidate == "помни" || candidate == "ты" || candidate == "это" { continue } return len([]rune(candidate)) >= 2 } } return false } func hasAssistantNamingTarget(tokens []string) bool { return containsToken(tokens, "тебя") || containsToken(tokens, "тебе") || containsToken(tokens, "ты") } // IsRetryRequest identifies a short continuation that asks Domovoy to repeat // the previous task. It deliberately carries no subject-specific knowledge; // the conversation owner supplies the previous task when available. func IsRetryRequest(message string) bool { tokens := semanticTokens(message) if len(tokens) == 0 || len(tokens) > 8 { return false } hasRetryCue := false for _, token := range tokens { if containsStem(token, "повтор", "снов", "попроб", "ещ") { hasRetryCue = true break } } return hasRetryCue } func topAct(scores map[interaction.SpeechAct]int) (interaction.SpeechAct, int) { bestAct, bestScore := interaction.SpeechActUnknown, -1 for _, act := range []interaction.SpeechAct{ interaction.SpeechActQuestion, interaction.SpeechActRequestToInvestigate, interaction.SpeechActDirective, interaction.SpeechActCorrection, interaction.SpeechActConversational, interaction.SpeechActPreference, interaction.SpeechActHypothesis, interaction.SpeechActDecision, interaction.SpeechActConfirmation, interaction.SpeechActRejection, interaction.SpeechActAssertion, } { if score := scores[act]; score > bestScore { bestAct, bestScore = act, score } } return bestAct, bestScore } func secondAct(scores map[interaction.SpeechAct]int, primary interaction.SpeechAct) (interaction.SpeechAct, int) { second, score := interaction.SpeechActUnknown, -1 for act, value := range scores { if act == primary { continue } if value > score { second, score = act, value } } if score <= 0 { return interaction.SpeechActUnknown, 0 } return second, score } func applicabilityFor(primary, secondary interaction.SpeechAct, primaryScore, secondaryScore int, tokens []string, explicitCorrection bool) interaction.SemanticApplicability { if primary == interaction.SpeechActAssertion && !containsInterrogative(tokens) && secondaryScore <= 1 { return interaction.SemanticApplicable } if primaryScore < 2 { return interaction.SemanticNeedsBroaderInterpret } if explicitCorrection && secondaryScore >= 3 && (secondary == interaction.SpeechActDirective || secondary == interaction.SpeechActRequestToInvestigate) { return interaction.SemanticMixedIntent } if primary == interaction.SpeechActCorrection && secondaryScore >= 3 && (secondary == interaction.SpeechActDirective || secondary == interaction.SpeechActRequestToInvestigate) { return interaction.SemanticMixedIntent } if primary == interaction.SpeechActAssertion && containsMeaningfulInterrogative(tokens) { return interaction.SemanticNeedsBroaderInterpret } if primary == interaction.SpeechActDirective && likelyConsequential(tokens) { return interaction.SemanticCandidateClarify } if primary == interaction.SpeechActDirective && containsStemInTokens(tokens, "обрати", "спрос") && containsModelReference(tokens) { return interaction.SemanticLowConfidence } if primaryScore-secondaryScore <= 1 && secondaryScore >= 2 { return interaction.SemanticLowConfidence } if primary == interaction.SpeechActQuestion || primary == interaction.SpeechActRequestToInvestigate { if primaryScore < 4 { return interaction.SemanticNeedsBroaderInterpret } } return interaction.SemanticApplicable } func needsCognitionForCandidate(primary, secondary interaction.SpeechAct, tokens []string) bool { if primary == interaction.SpeechActQuestion || primary == interaction.SpeechActRequestToInvestigate { return true } if secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate { return true } if primary == interaction.SpeechActDirective && (containsInterrogative(tokens) || containsModelReference(tokens) || containsStemInTokens(tokens, "спрос", "обрати", "посчитай", "рассчитай")) { return true } // A harmless imperative can ask for a textual or structural result even // without a question mark. Treat it as a cognition-worthy request instead // of silently recording it as an assertion. Consequential instructions are // kept on their explicit confirmation/capability paths. if primary == interaction.SpeechActDirective && !likelyConsequential(tokens) { return true } return false } func deterministicReason(primary, secondary interaction.SpeechAct, tokens []string) string { reasons := []string{"deterministic candidate from token-shape, interrogative, imperative and context cues"} if primary == interaction.SpeechActQuestion && containsInterrogative(tokens) { reasons = append(reasons, "interrogative structure observed") } if secondary != interaction.SpeechActUnknown { reasons = append(reasons, "secondary competing semantic signal observed") } return strings.Join(reasons, "; ") } func capabilityHintsFor(primary, secondary interaction.SpeechAct, tokens []string) []string { seen := map[string]struct{}{} add := func(kind string) { if kind != "" { seen[kind] = struct{}{} } } if primary == interaction.SpeechActQuestion || primary == interaction.SpeechActRequestToInvestigate || secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate { add("COGNITIVE_REASONING") } if sourceDependentQuestion(primary, secondary, tokens) { add("WEB_RESEARCH") } if isCreativeRequestTokens(tokens) { // A correction that quotes a creative phrase is still a correction; // do not execute the quoted phrase as a side effect. if isImageCreationRequestTokens(tokens) && primary != interaction.SpeechActCorrection && secondary != interaction.SpeechActCorrection { add("IMAGE_GENERATION") } else { add("COGNITIVE_REASONING") } } if containsStemInTokens(tokens, "голосов", "аудио", "послуш") { add("SPEECH_TO_TEXT") } if containsStemInTokens(tokens, "фотограф", "фото", "картин", "изображ") && containsStemInTokens(tokens, "прочит", "текст", "надпис") { add("OCR") } else if containsStemInTokens(tokens, "фотограф", "фото", "картин", "изображ") { add("IMAGE_UNDERSTANDING") } if containsStemInTokens(tokens, "видео", "ролик") { add("VIDEO_UNDERSTANDING") } if containsToken(tokens, "напомни") || containsToken(tokens, "напомин") { add("SCHEDULE_REMINDER") } if containsStemInTokens(tokens, "поставь", "создай", "добавь") && containsStemInTokens(tokens, "цел", "задач", "план") { add("COGNITIVE_REASONING") } if isExplicitEmailAction(tokens) { add("SEND_EMAIL") } if containsToken(tokens, "код") || containsToken(tokens, "патч") || containsToken(tokens, "текст") || containsToken(tokens, "главной") || containsToken(tokens, "чате") || containsToken(tokens, "проект") || containsStemInTokens(tokens, "тест") { if primary == interaction.SpeechActDirective || primary == interaction.SpeechActRequestToInvestigate || secondary == interaction.SpeechActDirective { add("MODIFY_CODE") add("RUN_TESTS") } } if containsToken(tokens, "assistant") && containsStemInTokens(tokens, "исправ", "добав", "измени", "помен", "патч") { add("MODIFY_CODE") add("RUN_TESTS") } if containsStemInTokens(tokens, "исправ", "поправ") && (containsToken(tokens, "чате") || containsToken(tokens, "текст") || containsToken(tokens, "assistant")) { add("MODIFY_CODE") add("RUN_TESTS") } if containsStemInTokens(tokens, "патч", "исправ", "поправ") && containsStemInTokens(tokens, "разбер", "проверь", "почему", "тормозит", "подум") { add("MODIFY_CODE") add("RUN_TESTS") } if containsInterrogative(tokens) && containsArithmetic(tokens) { add("COGNITIVE_REASONING") } if isCreativeRequestTokens(tokens) && containsStemInTokens(tokens, "погод", "ресторан", "кафе", "билет", "рейс", "цен", "найд", "узнай", "проверь") { add("WEB_RESEARCH") } if containsToken(tokens, "свободно") || containsToken(tokens, "гб") { add("VERIFY_FACT") } modelIntent := containsStemInTokens(tokens, "настро", "список", "доступ", "подключ", "сравн", "выбер", "провер", "проверь", "перечисл") if containsStemInTokens(tokens, "модел", "ии") && modelIntent && (primary == interaction.SpeechActQuestion || primary == interaction.SpeechActDirective || primary == interaction.SpeechActRequestToInvestigate || secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActDirective || secondary == interaction.SpeechActRequestToInvestigate) { add("VERIFY_FACT") } values := make([]string, 0, len(seen)) for kind := range seen { values = append(values, kind) } return values } func isImageCreationRequestTokens(tokens []string) bool { return containsStemInTokens(tokens, "нарис", "рису", "изобраз") } func isCreativeRequestTokens(tokens []string) bool { if containsStemInTokens(tokens, "нарис", "рису", "изобраз") { return true } return containsStemInTokens(tokens, "расска", "раска", "сочин", "придум") && containsStemInTokens(tokens, "сказк", "истор", "стих", "шутк", "песн", "басн") } // sourceDependentQuestion identifies semantic classes whose answer depends on // a current or externally observed source. It intentionally uses no named // place, person or one-off question branch; when the cue is absent the // truth-first fallback remains in force. func sourceDependentQuestion(primary, secondary interaction.SpeechAct, tokens []string) bool { inquiry := primary == interaction.SpeechActQuestion || primary == interaction.SpeechActRequestToInvestigate || secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate || (primary == interaction.SpeechActDirective && containsStemInTokens(tokens, "найд", "поищ", "подбер")) if !inquiry { return false } if focusIntrospectionQuestion(tokens) { return false } currentCue := containsStemInTokens(tokens, "сейчас", "сегодня", "текущ", "актуаль", "последн", "прямо") observedStateCue := containsStemInTokens(tokens, "погод", "температур", "осад", "ветер", "курс", "цен", "стоим", "новост", "распис", "событ", "статус", "доступен", "работает", "запущен", "срок") || approximateToken(tokens, "погод", 2) placeSearchCue := containsStemInTokens(tokens, "ресторан", "кафе", "отел", "магаз", "мест", "ближ", "адрес", "организац", "бренд", "товар", "артикул", "продавец", "маркетплейс", "wildberries", "wb") travelSearchCue := travelCue(tokens) || containsStemInTokens(tokens, "отправлен", "прибыт", "расстоян", "километр", "добрат", "маршрут") timeDifferenceCue := containsStemInTokens(tokens, "разниц") && containsStemInTokens(tokens, "времен", "час") return currentCue || observedStateCue || placeSearchCue || travelSearchCue || timeDifferenceCue } // approximateToken catches ordinary keyboard slips without turning routing // into a list of complete user questions. It is only used for broad // source-dependent concepts such as weather. func approximateToken(tokens []string, target string, maxDistance int) bool { for _, token := range tokens { if nearToken(token, target, maxDistance) { return true } } return false } func nearToken(token, target string, maxDistance int) bool { if editDistance([]rune(token), []rune(target)) <= maxDistance { return true } if maxDistance < 1 { return false } left, right := []rune(token), []rune(target) if len(left) != len(right) || len(left) < 2 { return false } for index := range left { if left[index] == right[index] { continue } return index+1 < len(left) && left[index] == right[index+1] && left[index+1] == right[index] && string(left[index+2:]) == string(right[index+2:]) } return false } func editDistance(left, right []rune) int { previous := make([]int, len(right)+1) for index := range previous { previous[index] = index } for i, leftRune := range left { current := make([]int, len(right)+1) current[0] = i + 1 for j, rightRune := range right { cost := 0 if leftRune != rightRune { cost = 1 } current[j+1] = minInt(current[j]+1, previous[j+1]+1, previous[j]+cost) } previous = current } return previous[len(right)] } func minInt(values ...int) int { result := values[0] for _, value := range values[1:] { if value < result { result = value } } return result } func informationRequestKind(message string, candidate interaction.SemanticCandidate) interaction.InformationRequestKind { tokens := semanticTokens(message) if len(tokens) == 0 { return "" } if containsStemInTokens(tokens, "памят", "помнишь", "ячейк") && (containsMeaningfulInterrogative(tokens) || containsStemInTokens(tokens, "вывед", "покаж", "напиш", "сообщ")) { return interaction.InformationRequestMemoryInventory } if containsStemInTokens(tokens, "разниц") && containsStemInTokens(tokens, "времен", "час") { return interaction.InformationRequestTimeDifference } if containsStemInTokens(tokens, "почему", "зачем") && containsStemInTokens(tokens, "смог", "неполуч", "нашел", "нашёл") && containsStemInTokens(tokens, "ответ", "данн", "найт") { return interaction.InformationRequestResearchFailureExplanation } inquiry := candidate.PrimarySpeechAct == interaction.SpeechActQuestion || candidate.PrimarySpeechAct == interaction.SpeechActRequestToInvestigate || candidate.SecondarySpeechAct == interaction.SpeechActQuestion || candidate.SecondarySpeechAct == interaction.SpeechActRequestToInvestigate cognitiveSubject := containsStemInTokens(tokens, "модел", "ии", "ai", "локальн", "когнитив", "канал") if cognitiveSubject && inquiry && containsStemInTokens(tokens, "какая", "какой", "текущ", "сейчас", "отвеч") && containsStemInTokens(tokens, "модел") { return interaction.InformationRequestCognitiveCurrentModel } inventoryCue := containsStemInTokens(tokens, "список", "доступ", "подключ", "вариант") || (containsMeaningfulInterrogative(tokens) && containsStemInTokens(tokens, "настро")) inventoryRequest := inquiry || containsStemInTokens(tokens, "назов", "напиш", "покаж", "перечисл", "сообщ", "вывед", "предостав") if cognitiveSubject && inventoryRequest && inventoryCue { return interaction.InformationRequestCognitiveModelInventory } capabilitySubject := containsStemInTokens(tokens, "уме", "возможност", "способн", "доступн", "инструмент", "capabilit") || (containsMeaningfulInterrogative(tokens) && containsToken(tokens, "можешь")) if inquiry && capabilitySubject { if containsStemInTokens(tokens, "без", "внешн", "локальн") && containsStemInTokens(tokens, "модел", "ии", "ai") { return interaction.InformationRequestLocalCapabilityInventory } return interaction.InformationRequestCapabilityInventory } recoveryCue := containsStemInTokens(tokens, "восстанов", "исправ", "почин", "подключ", "настро", "недоступ", "нет") if cognitiveSubject && recoveryCue { return interaction.InformationRequestCognitiveRecovery } if cognitiveSubject && inquiry { return interaction.InformationRequestCognitiveChannelStatus } return "" } func focusIntrospectionQuestion(tokens []string) bool { if containsStemInTokens(tokens, "фокус", "focus", "контекст") && (containsStemInTokens(tokens, "сейчас", "текущ", "работ", "контекст") || containsMeaningfulInterrogative(tokens)) { return true } return containsStemInTokens(tokens, "над", "делаем", "работаем") && containsStemInTokens(tokens, "сейчас", "текущ", "мы") } func LocationContextReply(message string) string { if location := personalCurrentLocation(message); location != "" { return "Запомнил: сейчас вы в " + location + "." } return "" } // IsCreativeRequest recognizes generation requests rather than retrieval of // externally verifiable facts. The cognitive route remains responsible for // the requested style and content. func IsCreativeRequest(message string) bool { tokens := semanticTokens(message) return isCreativeRequestTokens(tokens) } // TimeDifferenceResearchQuery makes the requested measurement explicit for a // current-data capability. It preserves the user-provided entities and does // not encode any particular place or time-zone relationship. func TimeDifferenceResearchQuery(message string) string { tokens := semanticTokens(StripAssistantVocative(message)) if containsStemInTokens(tokens, "разниц") && containsStemInTokens(tokens, "времен", "час") && containsToken(tokens, "между") { return strings.TrimSpace(StripAssistantVocative(message)) + " сколько часов" } return strings.TrimSpace(message) } // CreativeDeterministicReply intentionally has no example-specific answer // branches. Creative requests must retain their full meaning and go through // cognition; canned object or color answers hide lost attributes. func CreativeDeterministicReply(message string) string { return "" } // CreativeFallbackReply intentionally has no canned creative content. The // cognitive route must construct the requested result while preserving all // user attributes; a degraded path must not pretend that a prepared answer // was generated. func CreativeFallbackReply(message string) string { return "" } func boundedConfidence(primaryScore, secondaryScore int, scores map[interaction.SpeechAct]int) int64 { total := 0 for _, score := range scores { if score > 0 { total += score } } if total == 0 { return 200 } confidence := int64(400 + (primaryScore * 100)) if secondaryScore > 0 { confidence -= int64(secondaryScore * 40) } if confidence < 200 { confidence = 200 } if confidence > 980 { confidence = 980 } return int64(confidence) } func containsStem(token string, stems ...string) bool { for _, stem := range stems { if strings.Contains(token, stem) { return true } } return false } func containsExactToken(token string, words ...string) bool { for _, word := range words { if token == word { return true } } return false } func containsStemInTokens(tokens []string, stems ...string) bool { for _, token := range tokens { if containsStem(token, stems...) { return true } } return false } func requestedOutcome(message string, act interaction.SpeechAct) string { if act == interaction.SpeechActDirective || act == interaction.SpeechActDecision || act == interaction.SpeechActRequestToInvestigate { return strings.TrimSpace(message) } return "" } func containsToken(tokens []string, expected string) bool { for _, token := range tokens { if token == expected { return true } } return false } func startsWithModalRequest(tokens []string) bool { if len(tokens) == 0 { return false } switch tokens[0] { case "можешь", "можете", "сможешь", "сможете": return true } if len(tokens) >= 3 && tokens[0] == "не" && strings.HasPrefix(tokens[1], "мог") && tokens[2] == "бы" { return true } return false } func startsWithExplanatoryRequest(tokens []string, stems ...string) bool { if len(tokens) == 0 { return false } if containsStem(tokens[0], stems...) { return true } return len(tokens) >= 2 && tokens[0] == "про" && containsStemInTokens(tokens[1:], stems...) } func containsInterrogative(tokens []string) bool { for _, token := range tokens { if token == "-" { continue } switch token { case "кто", "что", "как", "почему", "зачем", "где", "когда", "сколько", "какой", "какая", "какие", "чей": return true } } return false } func containsMeaningfulInterrogative(tokens []string) bool { for index, token := range tokens { if token == "что" && followsNibud(tokens[index+1:]) { continue } switch token { case "кто", "что", "как", "почему", "зачем", "где", "когда", "сколько", "какой", "какая", "какие", "чей": return true } } return false } func containsArithmetic(tokens []string) bool { digits := 0 for _, token := range tokens { if token == "+" || token == "-" || token == "*" || token == "/" || token == "плюс" || token == "минус" { return true } for _, r := range token { if r >= '0' && r <= '9' { digits++ break } } } return digits >= 2 } func likelyConsequential(tokens []string) bool { for _, token := range tokens { if containsStem(token, "удал", "снес", "замен", "миграц", "парол", "опубли", "перезапуст", "смени", "купи") { return true } } return false } func followsNibud(tokens []string) bool { for _, token := range tokens { if token == "-" || token == "ни" { continue } return token == "нибудь" } return false } func startsWithDirectiveWhatNibud(tokens []string) bool { if len(tokens) < 2 || !containsStem(tokens[0], "сдел", "исправ", "добав", "удал", "напом", "подключ", "обрати", "спрос", "посчитай", "рассчитай", "использ", "вывед") { return false } for index, token := range tokens { if token == "что" && followsNibud(tokens[index+1:]) { return true } } return false } func containsModelReference(tokens []string) bool { for _, token := range tokens { if token == "модель" || token == "модели" || strings.Contains(token, "gemma") || strings.Contains(token, "гемма") || strings.Contains(token, "qwen") { return true } } return false } func isExplicitEmailAction(tokens []string) bool { hasMailObject := containsToken(tokens, "письмо") || containsToken(tokens, "письма") || containsToken(tokens, "почту") || containsToken(tokens, "почта") hasMailVerb := containsStemInTokens(tokens, "отправ", "ответ", "напиш") return hasMailObject && hasMailVerb } func removeCapabilityHint(values []string, denied string) []string { result := make([]string, 0, len(values)) for _, value := range values { if value == denied { continue } result = append(result, value) } return result } // RemoveCapabilityHint lets orchestration boundaries suppress a route that // contradicts an already understood semantic class. func RemoveCapabilityHint(values []string, denied string) []string { return removeCapabilityHint(values, denied) }