From 4e54ace76b83f20c13959500efe4eafd38f57128 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Wed, 26 Aug 2026 10:40:15 +0300 Subject: [PATCH] Route local place searches to relevant evidence --- cmd/core-runtime/main.go | 19 ++++++++++++++++++ cmd/core-runtime/main_test.go | 12 +++++++++++ modules/interaction/domain/semantics_test.go | 7 +++++++ modules/interaction/domain/travel.go | 21 +++++++++++++++++++- 4 files changed, 58 insertions(+), 1 deletion(-) diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index 1bb7823..7fe79e5 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -1863,6 +1863,9 @@ func researchFindingMatchesIntent(query string, finding epistemics.ResearchFindi if distanceResearchQuery(query) && !containsDistanceEvidence(finding.Excerpt.Text) { return false } + if localPlaceSearchQuery(query) && !containsLocalPlaceEvidence(finding.Excerpt.Text) { + return false + } if !nearestFlightQuery(query) { return true } @@ -1892,6 +1895,22 @@ func researchFindingMatchesIntent(query string, finding epistemics.ResearchFindi return true } +func localPlaceSearchQuery(query string) bool { + lower := strings.ToLower(query) + return (strings.Contains(lower, "ресторан") || strings.Contains(lower, "кафе")) && + (strings.Contains(lower, "адрес") || strings.Contains(lower, "отзыв") || strings.Contains(lower, "меню")) +} + +func containsLocalPlaceEvidence(value string) bool { + lower := strings.ToLower(value) + for _, term := range []string{"ресторан", "кафе", "адрес", "меню", "отзыв", "кухн"} { + if strings.Contains(lower, term) { + return true + } + } + return false +} + func distanceResearchQuery(query string) bool { lower := strings.ToLower(query) return strings.Contains(lower, "расстоян") || strings.Contains(lower, "километр") diff --git a/cmd/core-runtime/main_test.go b/cmd/core-runtime/main_test.go index 0e9a954..e2acef0 100644 --- a/cmd/core-runtime/main_test.go +++ b/cmd/core-runtime/main_test.go @@ -106,6 +106,18 @@ func TestResearchReplyRejectsGenericLandingPageForConcreteIdentifier(t *testing. } } +func TestLocalPlaceReplyRejectsDictionaryFinding(t *testing.T) { + result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{ + State: epistemics.FindingSupported, + Source: epistemics.Source{Locator: "https://dictionary.example", Title: "Ближайший — словарь"}, + Excerpt: epistemics.SourceExcerpt{Text: "Ближайший — значение слова и пример употребления."}, + }}} + reply, backed := renderResearchReply("места ресторан в Сочи адрес отзывы меню", result, nil) + if backed || !strings.Contains(reply, "Проверяемых данных") { + t.Fatalf("dictionary finding was presented as local place result: %q", reply) + } +} + func TestResearchReplyKeepsFindingContainingConcreteIdentifier(t *testing.T) { result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{ State: epistemics.FindingSupported, diff --git a/modules/interaction/domain/semantics_test.go b/modules/interaction/domain/semantics_test.go index 3be1840..ba7bcfa 100644 --- a/modules/interaction/domain/semantics_test.go +++ b/modules/interaction/domain/semantics_test.go @@ -189,6 +189,13 @@ func TestTravelResearchQueryUsesKnownOriginForDistance(t *testing.T) { } } +func TestTravelResearchQueryCompilesLocalPlaceSearchWithoutSearchImperative(t *testing.T) { + query := TravelResearchQuery("найди ближайший ресторан в Сочи", nil, time.Time{}) + if strings.Contains(query, "найди") || strings.Contains(query, "ближайший") || !strings.Contains(query, "сочи") || !strings.Contains(query, "адрес") { + t.Fatalf("local place query was not compiled semantically: %q", query) + } +} + func TestTravelRouteWithKnownOriginDoesNotAskForTicketDate(t *testing.T) { history := []interaction.HumanUtterance{{Content: "я сейчас в Москве"}} if got := TravelSearchClarification("как можно добраться до Сочи?", history); got != "" { diff --git a/modules/interaction/domain/travel.go b/modules/interaction/domain/travel.go index c57e7fb..2ac4326 100644 --- a/modules/interaction/domain/travel.go +++ b/modules/interaction/domain/travel.go @@ -46,7 +46,9 @@ func normalizedTravelSearchText(message string) string { func TravelResearchQuery(message string, history []interaction.HumanUtterance, now time.Time) string { query := TravelSearchQuery(message, now) tokens := semanticTokens(StripAssistantVocative(message)) - if containsStemInTokens(tokens, "расстоян", "километр") { + if localPlaceSearchRequest(tokens) { + return localPlaceResearchQuery(tokens) + } else if containsStemInTokens(tokens, "расстоян", "километр") { origin := travelContextOrigin(history) destination := travelContextDestinationFromTokens(tokens) if origin != "" && destination != "" { @@ -86,6 +88,23 @@ func TravelResearchQuery(message string, history []interaction.HumanUtterance, n return query } +// localPlaceSearchRequest recognizes a proximity search as a local-search +// capability, keeping it separate from travel-route compilation. +func localPlaceSearchRequest(tokens []string) bool { + return containsStemInTokens(tokens, "ресторан", "кафе") && containsStemInTokens(tokens, "ближ", "рядом") +} + +func localPlaceResearchQuery(tokens []string) string { + parts := make([]string, 0, len(tokens)) + for _, token := range tokens { + if containsStem(token, "найд", "покаж", "ближ", "рядом") { + continue + } + parts = append(parts, token) + } + return "места " + strings.Join(parts, " ") + " адрес отзывы меню" +} + // trainScheduleResearchQuery formulates an observed schedule request from the // user's route and remembered origin. It contains no city, provider or answer // catalogue: actual departure data remains evidence from the selected source.