diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index da9e980..e5c3544 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -511,6 +511,7 @@ func main() { var effectiveTaskMessage = interactiondomain.StripAssistantVocative(utterance.Content) var previousTask *interaction.HumanUtterance var conversationHistory []interaction.HumanUtterance + var conversationOnlyHistory []interaction.HumanUtterance if request.ConversationID != "" { var historyErr error conversationHistory, historyErr = interactionRepository.Utterances(r.Context(), conversation.ID) @@ -518,6 +519,7 @@ func main() { log.Printf("interaction conversation history unavailable: %v", historyErr) } } + conversationOnlyHistory = append(conversationOnlyHistory, conversationHistory...) // Keep durable user context available even when the web client carries a // conversation ID; a new or rotated conversation must not erase learned // preferences. @@ -528,6 +530,9 @@ func main() { log.Printf("interaction principal history loaded: %d utterances", len(recentUserHistory)) } conversationHistory = append(conversationHistory, recentUserHistory...) + if continuation := interactiondomain.ContinueLocalSearch(effectiveTaskMessage, conversationOnlyHistory); continuation != "" { + effectiveTaskMessage = continuation + } if request.ConversationID != "" && interactiondomain.IsRetryRequest(utterance.Content) { if len(conversationHistory) > 0 { for index := len(conversationHistory) - 1; index >= 0; index-- { diff --git a/modules/interaction/domain/semantics_test.go b/modules/interaction/domain/semantics_test.go index 23611c6..f5c7167 100644 --- a/modules/interaction/domain/semantics_test.go +++ b/modules/interaction/domain/semantics_test.go @@ -85,6 +85,16 @@ func TestLocalSearchClarification(t *testing.T) { } } +func TestContinueLocalSearch(t *testing.T) { + history := []interaction.HumanUtterance{{Content: "Найди ближайший ресторан"}} + if got := ContinueLocalSearch("в Сочи", history); got != "Найди ближайший ресторан в Сочи" { + t.Fatalf("unexpected continuation: %q", got) + } + if got := ContinueLocalSearch("в Сочи", nil); got != "" { + t.Fatalf("location must not create a goal without conversation 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") diff --git a/modules/interaction/domain/travel.go b/modules/interaction/domain/travel.go index 2d64867..fa0096e 100644 --- a/modules/interaction/domain/travel.go +++ b/modules/interaction/domain/travel.go @@ -65,6 +65,24 @@ func LocalSearchClarification(message string) string { return "В каком городе или районе искать ресторан?" } +// ContinueLocalSearch turns a location-only follow-up into the previous local +// search request, but only when that request is present in this conversation. +func ContinueLocalSearch(message string, history []interaction.HumanUtterance) string { + current := strings.TrimSpace(StripAssistantVocative(message)) + tokens := semanticTokens(current) + if len(tokens) == 0 || len(tokens) > 3 || (tokens[0] != "в" && tokens[0] != "на" && tokens[0] != "около") { + return "" + } + for index := len(history) - 1; index >= 0; index-- { + previous := strings.TrimSpace(StripAssistantVocative(history[index].Content)) + previousTokens := semanticTokens(previous) + if containsStemInTokens(previousTokens, "ресторан", "кафе") && containsStemInTokens(previousTokens, "ближ", "рядом") { + return strings.TrimSpace(previous + " " + current) + } + } + return "" +} + // 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 {