diff --git a/cmd/cognitive-worker/gateway.go b/cmd/cognitive-worker/gateway.go index a17db49..5bdb8a1 100644 --- a/cmd/cognitive-worker/gateway.go +++ b/cmd/cognitive-worker/gateway.go @@ -186,7 +186,7 @@ func selectionObjectiveForMeaning(meaning cognitive.TaskMeaning, message string) } func builtinAdvisory(request cognitive.GatewayAdvisoryRequest) cognitive.GatewayAdvisoryResponse { - text := "Я — Домовой, ваш помощник. Помогаю разбираться в вопросах, искать информацию и выполнять задачи." + text := "Проверю запрос доступным маршрутом и сообщу результат." switch request.Meaning.InformationRequestKind { case interaction.InformationRequestCognitiveModelInventory: text = "Проверю подключённые модели. Ключи и пароли не показываю." @@ -201,7 +201,9 @@ func builtinAdvisory(request cognitive.GatewayAdvisoryRequest) cognitive.Gateway case "REQUEST_TO_INVESTIGATE": text = "Понял задачу. Проверю доступный путь и сообщу результат." case "QUESTION": - text = "Я — Домовой, ваш помощник. Помогаю разбираться в вопросах, искать информацию и выполнять задачи." + text = "Проверю вопрос доступным источником и отвечу по существу." + case "CONVERSATIONAL": + text = "Я — Домовой, ваш помощник." } } return cognitive.GatewayAdvisoryResponse{Version: request.Version, RequestID: request.RequestID, AdvisoryText: text, FinishReason: cognitive.FinishCompleted} diff --git a/cmd/cognitive-worker/gateway_registry_test.go b/cmd/cognitive-worker/gateway_registry_test.go index f442ede..6f8d40c 100644 --- a/cmd/cognitive-worker/gateway_registry_test.go +++ b/cmd/cognitive-worker/gateway_registry_test.go @@ -196,7 +196,7 @@ func TestBuiltInAdvisoryIdentityIsNaturalAndDoesNotMentionResponseLength(t *test RequestID: "request-identity", Meaning: cognitive.TaskMeaning{SpeechAct: "QUESTION"}, }) - if response.AdvisoryText != "Я — Домовой, ваш помощник. Помогаю разбираться в вопросах, искать информацию и выполнять задачи." { + if response.AdvisoryText != "Проверю вопрос доступным источником и отвечу по существу." { t.Fatalf("unexpected identity response: %q", response.AdvisoryText) } if strings.Contains(response.AdvisoryText, "коротко") || strings.Contains(response.AdvisoryText, "подробност") { diff --git a/modules/interaction/domain/chat.go b/modules/interaction/domain/chat.go index f595340..e9be08d 100644 --- a/modules/interaction/domain/chat.go +++ b/modules/interaction/domain/chat.go @@ -214,7 +214,7 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo questionWords := map[string]struct{}{"кто": {}, "что": {}, "как": {}, "почему": {}, "зачем": {}, "где": {}, "когда": {}, "сколько": {}, "какой": {}, "какая": {}, "какие": {}, "чей": {}} conversationWords := []string{"привет", "здравствуйте", "спасибо", "ку", "здравствуй"} investigateStems := []string{"разбер", "проверь", "проанализ", "исслед", "выясн", "уточн", "подум", "понят", "посмотри", "попроб", "повтор", "снов"} - directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "спрос", "посчитай", "рассчитай", "ответь"} + directiveStems := []string{"сдел", "запуст", "созд", "настро", "добав", "измени", "помен", "использ", "удал", "напом", "подключ", "исправ", "обрати", "спрос", "найд", "посчитай", "рассчитай", "ответь"} explanatoryRequestStems := []string{"расска", "раска", "объяс", "опиш", "покаж", "подскаж", "сообщ"} preferenceStems := []string{"предпоч", "хочу", "лучше", "не используй"} hypothesisStems := []string{"думаю", "кажется", "гипотез", "похоже"} @@ -472,13 +472,56 @@ func capabilityHintsFor(primary, secondary interaction.SpeechAct, tokens []strin // 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 + inquiry := primary == interaction.SpeechActQuestion || primary == interaction.SpeechActRequestToInvestigate || secondary == interaction.SpeechActQuestion || secondary == interaction.SpeechActRequestToInvestigate || (primary == interaction.SpeechActDirective && containsStemInTokens(tokens, "найд", "поищ", "подбер")) if !inquiry { return false } currentCue := containsStemInTokens(tokens, "сейчас", "сегодня", "текущ", "актуаль", "последн", "прямо") - observedStateCue := containsStemInTokens(tokens, "погод", "температур", "осад", "ветер", "курс", "цен", "стоим", "новост", "распис", "событ", "статус", "доступен", "работает", "запущен", "срок") - return currentCue || observedStateCue + observedStateCue := containsStemInTokens(tokens, "погод", "температур", "осад", "ветер", "курс", "цен", "стоим", "новост", "распис", "событ", "статус", "доступен", "работает", "запущен", "срок") || approximateToken(tokens, "погод", 2) + placeSearchCue := containsStemInTokens(tokens, "ресторан", "кафе", "отел", "магаз", "мест", "ближ", "адрес", "организац") + return currentCue || observedStateCue || placeSearchCue +} + +// 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 editDistance([]rune(token), []rune(target)) <= maxDistance { + return true + } + } + 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 { diff --git a/modules/interaction/domain/chat_test.go b/modules/interaction/domain/chat_test.go index 581be75..25b5655 100644 --- a/modules/interaction/domain/chat_test.go +++ b/modules/interaction/domain/chat_test.go @@ -154,6 +154,21 @@ func TestSourceDependentQuestionRequestsWebResearchWithoutEntityBranch(t *testin } } +func TestSourceDependentQuestionToleratesTyposAndPlaceSearch(t *testing.T) { + for _, message := range []string{"какая пугода в Сочи", "найди ближайший ресторан в Сочи"} { + candidate := BuildDeterministicCandidate(message, "", time.Unix(10, 0).UnixMilli()) + seen := false + for _, hint := range candidate.CapabilityHints { + if hint == "WEB_RESEARCH" { + seen = true + } + } + if !seen { + t.Fatalf("message=%q was not routed to WEB_RESEARCH: %+v", message, candidate) + } + } +} + func TestRequestToClarifyCurrentFactRequestsWebResearch(t *testing.T) { candidate := BuildDeterministicCandidate("уточни погоду в питере", "", time.Unix(10, 0).UnixMilli()) for _, hint := range candidate.CapabilityHints {