diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index f273411..e42bb38 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -508,7 +508,7 @@ func main() { http.Error(w, "project focus unavailable", http.StatusServiceUnavailable) return } - var effectiveTaskMessage = utterance.Content + var effectiveTaskMessage = interactiondomain.StripAssistantVocative(utterance.Content) var previousTask *interaction.HumanUtterance var conversationHistory []interaction.HumanUtterance if request.ConversationID != "" { @@ -536,7 +536,7 @@ func main() { } candidate := conversationHistory[index] previousTask = &candidate - effectiveTaskMessage = candidate.Content + effectiveTaskMessage = interactiondomain.StripAssistantVocative(candidate.Content) break } } @@ -601,6 +601,13 @@ func main() { cognitiveAvailable = false } } + if travelReply := interactiondomain.TravelSearchClarification(effectiveTaskMessage, conversationHistory); travelReply != "" { + semantic.Interpretation.Clarification = interaction.ClarificationRequired + semantic.Interpretation.Applicability = interaction.SemanticCandidateClarify + semantic.Reply = travelReply + semantic.NeedsCognition = false + semantic.Interpretation.NeedsCognition = false + } if err := interactionRepository.AppendUtterance(r.Context(), conversation, utterance, intent, relations); err != nil { http.Error(w, "save chat message", http.StatusInternalServerError) return diff --git a/modules/interaction/domain/semantics_test.go b/modules/interaction/domain/semantics_test.go index baa3090..f95d4bf 100644 --- a/modules/interaction/domain/semantics_test.go +++ b/modules/interaction/domain/semantics_test.go @@ -60,6 +60,28 @@ func TestStableConversationalReplySurvivesCognitiveRewrite(t *testing.T) { } } +func TestStripAssistantVocative(t *testing.T) { + if got := StripAssistantVocative("Дед, какая погода в Сочи?"); got != "какая погода в Сочи?" { + t.Fatalf("unexpected cleaned query: %q", got) + } + if got := StripAssistantVocative("домовой: найди ресторан"); got != "найди ресторан" { + t.Fatalf("unexpected cleaned query: %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("Найди билеты из Москвы в Сочи на 12 сентября", nil); got != "" { + t.Fatalf("complete travel request must proceed, got %q", got) + } + history := []interaction.HumanUtterance{{Content: "Найди билеты"}} + if got := TravelSearchClarification("а куда летим?", history); got == "" { + t.Fatal("follow-up must remain attached to the travel goal") + } +} + func TestInterpretUtteranceDirectiveRecordsOutcomeWithoutExecution(t *testing.T) { result := interpretForTest(t, "Поменяй базу на УПО1.", activeFocus("upo", "УПО"), "") if result.Interpretation.SpeechAct != interaction.SpeechActDirective { diff --git a/modules/interaction/domain/travel.go b/modules/interaction/domain/travel.go new file mode 100644 index 0000000..019387d --- /dev/null +++ b/modules/interaction/domain/travel.go @@ -0,0 +1,72 @@ +package domain + +import ( + "strings" + + "git.cin.su/m/Assistant/contracts/interaction" +) + +// StripAssistantVocative removes a leading assistant name used as an address. +// The original utterance remains the historical source; this value is only for +// interpretation, cognitive prompts and external queries. +func StripAssistantVocative(message string) string { + trimmed := strings.TrimSpace(message) + lower := strings.ToLower(trimmed) + for _, name := range []string{"дед", "домовой"} { + if !strings.HasPrefix(lower, name) { + continue + } + rest := trimmed[len(name):] + if rest == "" || !strings.ContainsRune(" \t,;:!?—-", []rune(rest)[0]) { + continue + } + return strings.TrimSpace(strings.TrimLeft(rest, " \t,;:!?—-")) + } + return trimmed +} + +// TravelSearchClarification keeps an incomplete travel request in dialogue +// instead of reducing it to an unrelated generic web search. +func TravelSearchClarification(message string, history []interaction.HumanUtterance) string { + current := semanticTokens(StripAssistantVocative(message)) + travel := travelCue(current) + if !travel { + for index := len(history) - 1; index >= 0; index-- { + if travelCue(semanticTokens(history[index].Content)) { + travel = true + break + } + } + } + if !travel { + return "" + } + origin := containsStemInTokens(current, "из", "откуда", "от") + destination := containsStemInTokens(current, "в", "куда", "до") + date := containsStemInTokens(current, "сегодня", "завтра", "послезавтр", "январ", "феврал", "март", "апрел", "мая", "июн", "июл", "август", "сентябр", "октябр", "ноябр", "декабр") + if !date { + for _, token := range current { + if hasDigit(token) { + date = true + break + } + } + } + if origin && destination && date { + return "" + } + return "Уточните маршрут и дату: откуда, куда и на какой день нужны билеты?" +} + +func travelCue(tokens []string) bool { + return containsStemInTokens(tokens, "билет", "рейс", "поезд", "автобус", "перелет", "перелёт") +} + +func hasDigit(value string) bool { + for _, runeValue := range value { + if runeValue >= '0' && runeValue <= '9' { + return true + } + } + return false +}