diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index aea310a..4f36748 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -532,6 +532,9 @@ func main() { if continuation := interactiondomain.ContinueLocalSearch(effectiveTaskMessage, conversationHistory); continuation != "" { continuationMessage = continuation effectiveTaskMessage = continuation + } else if continuation := interactiondomain.ContinueTravelOrigin(effectiveTaskMessage, conversationHistory); continuation != "" { + continuationMessage = continuation + effectiveTaskMessage = continuation } if request.ConversationID != "" && interactiondomain.IsRetryRequest(utterance.Content) { if len(conversationHistory) > 0 { diff --git a/modules/interaction/domain/semantics_test.go b/modules/interaction/domain/semantics_test.go index 6673598..f15814c 100644 --- a/modules/interaction/domain/semantics_test.go +++ b/modules/interaction/domain/semantics_test.go @@ -95,6 +95,16 @@ func TestContinueLocalSearch(t *testing.T) { } } +func TestContinueTravelOrigin(t *testing.T) { + history := []interaction.HumanUtterance{{Content: "Как можно добраться до Сочи?"}} + if got := ContinueTravelOrigin("я в Казани", history); got != "Как можно добраться до Сочи из Казани" { + t.Fatalf("unexpected travel continuation: %q", got) + } + if got := ContinueTravelOrigin("я в Казани", nil); got != "" { + t.Fatalf("location must not create a travel task without context: %q", got) + } +} + func TestTravelSearchClarification(t *testing.T) { if got := TravelSearchClarification("Найди билеты", nil); got == "" { t.Fatal("underspecified travel request must ask for missing route and date") @@ -112,6 +122,9 @@ func TestTravelSearchClarification(t *testing.T) { if got := TravelSearchClarification("Как можно добраться до Сочи?", nil); got != "Из какого города выезжать в Сочи? Подберу самолёт, поезд, автобус или машину." { t.Fatalf("general travel question should ask departure city, got %q", got) } + if got := TravelSearchClarification("Как можно добраться до Сочи из Казани", nil); got != "" { + t.Fatalf("complete general travel route must proceed, got %q", got) + } contextHistory := []interaction.HumanUtterance{{Content: "Как можно добраться до Сочи?"}} if got := TravelSearchClarification("Найди билеты, какой ближайший самолёт", contextHistory); got != "Из какого города вылетать в Сочи и на какую дату?" { t.Fatalf("travel destination should be inherited from context, got %q", got) diff --git a/modules/interaction/domain/travel.go b/modules/interaction/domain/travel.go index cf22a73..faf12f5 100644 --- a/modules/interaction/domain/travel.go +++ b/modules/interaction/domain/travel.go @@ -92,6 +92,41 @@ func ContinueLocalSearch(message string, history []interaction.HumanUtterance) s return "" } +// ContinueTravelOrigin turns a short location answer into the previous travel +// task. The original utterance remains history; only the effective task is +// 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] != "я" { + return "" + } + location := "" + for index, token := range current { + if (token == "в" || token == "из") && index+1 < len(current) { + location = capitalizeWord(current[index+1]) + break + } + } + if location == "" { + return "" + } + var latest *interaction.HumanUtterance + for index := range history { + candidate := history[index] + if !travelContextCue(semanticTokens(candidate.Content)) { + continue + } + if latest == nil || candidate.ReceivedAtUnixMillis > latest.ReceivedAtUnixMillis { + latest = &candidate + } + } + if latest == nil { + return "" + } + previous := strings.TrimRight(strings.TrimSpace(StripAssistantVocative(latest.Content)), "?!.,;:") + return strings.TrimSpace(previous + " из " + location) +} + // 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 { @@ -125,6 +160,10 @@ func TravelSearchClarification(message string, history []interaction.HumanUttera if origin && destination && date { return "" } + ticketRequest := containsStemInTokens(current, "билет", "рейс", "самолет", "самолёт", "вылет") + if !ticketRequest && origin && destination { + return "" + } if !destination { if contextDestination := travelContextDestination(history); contextDestination != "" { destination = true @@ -137,7 +176,7 @@ func TravelSearchClarification(message string, history []interaction.HumanUttera } } if destination && !origin && !date { - if !containsStemInTokens(current, "билет", "рейс", "самолет", "самолёт", "вылет") { + if !ticketRequest { return "Из какого города выезжать в " + destinationLabel(current, history) + "? Подберу самолёт, поезд, автобус или машину." } return "Из какого города вылетать и на какую дату?"