diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index 4f36748..0aac6c6 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -510,6 +510,7 @@ func main() { } var effectiveTaskMessage = interactiondomain.StripAssistantVocative(utterance.Content) continuationMessage := "" + travelContinuation := false var previousTask *interaction.HumanUtterance var conversationHistory []interaction.HumanUtterance if request.ConversationID != "" { @@ -534,6 +535,7 @@ func main() { effectiveTaskMessage = continuation } else if continuation := interactiondomain.ContinueTravelOrigin(effectiveTaskMessage, conversationHistory); continuation != "" { continuationMessage = continuation + travelContinuation = true effectiveTaskMessage = continuation } if request.ConversationID != "" && interactiondomain.IsRetryRequest(utterance.Content) { @@ -564,6 +566,11 @@ func main() { semantic = continuedSemantic } } + if travelContinuation && interactiondomain.TravelSearchClarification(effectiveTaskMessage, conversationHistory) == "" { + semantic.CapabilityHints = append(semantic.CapabilityHints, string(capability.CapabilityKindWebResearch)) + semantic.NeedsCognition = false + semantic.Interpretation.NeedsCognition = false + } if previousTask != nil { previousSemantic, previousErr := interactiondomain.InterpretUtterance(*previousTask, projects, previousFocus, now) if previousErr == nil { @@ -700,6 +707,9 @@ func main() { helpPath = work.HelpPath{} } switch semantic.InformationRequestKind { + case interaction.InformationRequestMemoryInventory: + planningReply = humanMemoryInventory(conversationHistory, assistantName) + needsCognition = false case interaction.InformationRequestCapabilityInventory: kinds, err := capabilityRepository.AvailableCapabilityKinds(r.Context(), session.PrincipalID) if err == nil && len(kinds) > 0 { @@ -1649,6 +1659,25 @@ func reviewPreservesResearchEvidence(reviewed, draft string) bool { return true } +func humanMemoryInventory(history []interaction.HumanUtterance, assistantName string) string { + rows := make([]string, 0, 4) + if assistantName != "" { + rows = append(rows, "| Имя помощника | "+assistantName+" |") + } + for index := len(history) - 1; index >= 0; index-- { + message := strings.TrimSpace(history[index].Content) + lower := strings.ToLower(message) + if strings.Contains(lower, "нравится") || strings.Contains(lower, "люблю") { + rows = append(rows, "| Предпочтение пользователя | "+message+" |") + break + } + } + if len(rows) == 0 { + return "| Ячейка памяти | Значение |\n|---|---|\n| — | Сохранённых пользовательских предпочтений пока не найдено. |" + } + return "| Ячейка памяти | Значение |\n|---|---|\n" + strings.Join(rows, "\n") +} + func researchSourceLabel(finding epistemics.ResearchFinding) string { title := strings.TrimSpace(finding.Source.Title) locator := strings.TrimSpace(finding.Source.Locator) diff --git a/contracts/interaction/types.go b/contracts/interaction/types.go index 1597c64..9ac542a 100644 --- a/contracts/interaction/types.go +++ b/contracts/interaction/types.go @@ -70,6 +70,7 @@ const ( InformationRequestCognitiveModelInventory InformationRequestKind = "COGNITIVE_MODEL_INVENTORY" InformationRequestCognitiveChannelStatus InformationRequestKind = "COGNITIVE_CHANNEL_STATUS" InformationRequestCognitiveRecovery InformationRequestKind = "COGNITIVE_RECOVERY" + InformationRequestMemoryInventory InformationRequestKind = "MEMORY_INVENTORY" ) type Conversation struct { diff --git a/modules/interaction/domain/chat.go b/modules/interaction/domain/chat.go index cea0cd9..72ee42f 100644 --- a/modules/interaction/domain/chat.go +++ b/modules/interaction/domain/chat.go @@ -133,7 +133,7 @@ func PreferredAssistantName(message string) string { continue } for _, candidate := range tokens[index+1:] { - if candidate == "тебя" || candidate == "тебе" || candidate == "себя" || candidate == "меня" { + if candidate == "тебя" || candidate == "тебе" || candidate == "себя" || candidate == "меня" || candidate == "в" || candidate == "теперь" || candidate == "же" { continue } if candidate == "буду" || candidate == "будет" || candidate == "запомни" || candidate == "помни" || candidate == "ты" || candidate == "это" { @@ -144,12 +144,19 @@ func PreferredAssistantName(message string) string { } runes := []rune(candidate) runes[0] = []rune(strings.ToUpper(string(runes[0])))[0] - return string(runes) + 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 { @@ -592,6 +599,9 @@ func informationRequestKind(message string, candidate interaction.SemanticCandid if len(tokens) == 0 { return "" } + if containsStemInTokens(tokens, "памят", "помнишь", "ячейк") && (containsMeaningfulInterrogative(tokens) || containsStemInTokens(tokens, "вывед", "покаж", "напиш", "сообщ")) { + return interaction.InformationRequestMemoryInventory + } inquiry := candidate.PrimarySpeechAct == interaction.SpeechActQuestion || candidate.PrimarySpeechAct == interaction.SpeechActRequestToInvestigate || candidate.SecondarySpeechAct == interaction.SpeechActQuestion || candidate.SecondarySpeechAct == interaction.SpeechActRequestToInvestigate cognitiveSubject := containsStemInTokens(tokens, "модел", "ии", "ai", "локальн", "когнитив", "канал") inventoryCue := containsStemInTokens(tokens, "список", "доступ", "подключ", "вариант") || (containsMeaningfulInterrogative(tokens) && containsStemInTokens(tokens, "настро")) diff --git a/modules/interaction/domain/chat_test.go b/modules/interaction/domain/chat_test.go index d22ce6e..ac3ee8c 100644 --- a/modules/interaction/domain/chat_test.go +++ b/modules/interaction/domain/chat_test.go @@ -198,11 +198,23 @@ func TestNamingPreferenceIsSemanticAndProducesNaturalAcceptance(t *testing.T) { if name := PreferredAssistantName("я назову тебя дед, запомни"); name != "Дед" { t.Fatalf("expected conversational name form to be extracted, got %q", name) } + if name := PreferredAssistantName("давай переименуем тебя в фею. теперь тебя зовут фея"); name != "Фея" { + t.Fatalf("rename phrase returned %q", name) + } if !IsIdentityInquiry("ты кто?") || !IsIdentityInquiry("как тебя зовут") { t.Fatal("identity variants should be recognized") } } +func TestMemoryInventoryRequestIsSemantic(t *testing.T) { + for _, message := range []string{"выведи свою память", "напиши что ты помнишь в виде таблицы"} { + candidate := BuildDeterministicCandidate(message, "", time.Unix(10, 0).UnixMilli()) + if got := informationRequestKind(message, candidate); got != interaction.InformationRequestMemoryInventory { + t.Fatalf("%q: expected memory inventory, got %q", message, got) + } + } +} + func TestDeterministicCandidateKeepsFirstPersonCompletedWorkAsAssertion(t *testing.T) { candidate := BuildDeterministicCandidate("Я всё уже настроил.", "", time.Unix(10, 0).UnixMilli()) if candidate.PrimarySpeechAct != interaction.SpeechActAssertion { diff --git a/modules/interaction/domain/semantics_test.go b/modules/interaction/domain/semantics_test.go index f15814c..d34664f 100644 --- a/modules/interaction/domain/semantics_test.go +++ b/modules/interaction/domain/semantics_test.go @@ -103,12 +103,18 @@ func TestContinueTravelOrigin(t *testing.T) { if got := ContinueTravelOrigin("я в Казани", nil); got != "" { t.Fatalf("location must not create a travel task without context: %q", got) } + if got := ContinueTravelOrigin("Москва", history); got != "Как можно добраться до Сочи из города Москва" { + t.Fatalf("bare city should complete travel origin: %q", got) + } } func TestTravelSearchClarification(t *testing.T) { if got := TravelSearchClarification("Найди билеты", nil); got == "" { t.Fatal("underspecified travel request must ask for missing route and date") } + if got := TravelSearchClarification("Покажи ближайшие рейсы до Сочи", nil); got != "Из какого города вылетать?" { + t.Fatalf("nearest-flight request should default date to now, got %q", got) + } if got := TravelSearchClarification("Найди билеты из Москвы в Сочи на 12 сентября", nil); got != "" { t.Fatalf("complete travel request must proceed, got %q", got) } @@ -126,7 +132,7 @@ func TestTravelSearchClarification(t *testing.T) { t.Fatalf("complete general travel route must proceed, got %q", got) } contextHistory := []interaction.HumanUtterance{{Content: "Как можно добраться до Сочи?"}} - if got := TravelSearchClarification("Найди билеты, какой ближайший самолёт", contextHistory); got != "Из какого города вылетать в Сочи и на какую дату?" { + if got := TravelSearchClarification("Найди билеты, какой ближайший самолёт", contextHistory); got != "Из какого города вылетать в Сочи?" { t.Fatalf("travel destination should be inherited from context, got %q", got) } noisyContext := []interaction.HumanUtterance{ diff --git a/modules/interaction/domain/travel.go b/modules/interaction/domain/travel.go index faf12f5..29f3a27 100644 --- a/modules/interaction/domain/travel.go +++ b/modules/interaction/domain/travel.go @@ -97,15 +97,22 @@ func ContinueLocalSearch(message string, history []interaction.HumanUtterance) s // completed for routing and research. func ContinueTravelOrigin(message string, history []interaction.HumanUtterance) string { current := semanticTokens(StripAssistantVocative(message)) - if len(current) < 3 || len(current) > 6 || current[0] != "я" { + if len(current) == 0 || len(current) > 6 { return "" } location := "" - for index, token := range current { - if (token == "в" || token == "из") && index+1 < len(current) { - location = capitalizeWord(current[index+1]) - break + if current[0] == "я" { + if len(current) < 3 { + return "" } + for index, token := range current { + if (token == "в" || token == "из") && index+1 < len(current) { + location = capitalizeWord(current[index+1]) + break + } + } + } else if len(current) <= 2 && !isTravelDateWord(current[0]) && len([]rune(current[0])) >= 3 { + location = capitalizeWord(current[0]) } if location == "" { return "" @@ -124,7 +131,11 @@ func ContinueTravelOrigin(message string, history []interaction.HumanUtterance) return "" } previous := strings.TrimRight(strings.TrimSpace(StripAssistantVocative(latest.Content)), "?!.,;:") - return strings.TrimSpace(previous + " из " + location) + preposition := " из " + if current[0] != "я" { + preposition = " из города " + } + return strings.TrimSpace(previous + preposition + location) } // TravelSearchClarification keeps an incomplete travel request in dialogue @@ -149,6 +160,9 @@ func TravelSearchClarification(message string, history []interaction.HumanUttera origin := containsStemInTokens(current, "из", "откуда", "от") destination := containsStemInTokens(current, "в", "куда", "до") date := containsStemInTokens(current, "сегодня", "завтра", "послезавтр", "январ", "феврал", "март", "апрел", "мая", "июн", "июл", "август", "сентябр", "октябр", "ноябр", "декабр") + if containsStemInTokens(current, "ближ") { + date = true + } if !date { for _, token := range current { if hasDigit(token) { @@ -190,6 +204,10 @@ func TravelSearchClarification(message string, history []interaction.HumanUttera return "Уточните маршрут и дату: откуда, куда и на какой день нужны билеты?" } +func isTravelDateWord(value string) bool { + return containsStem(value, "сегодня", "завтра", "послезавтр", "январ", "феврал", "март", "апрел", "мая", "июн", "июл", "август", "сентябр", "октябр", "ноябр", "декабр") +} + func destinationLabel(current []string, history []interaction.HumanUtterance) string { for index, token := range current { if (token == "в" || token == "до") && index+1 < len(current) {