diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index d0d8879..f2bd2a7 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -1818,6 +1818,9 @@ func researchFindingMatchesIntent(query string, finding epistemics.ResearchFindi if !findingContainsRequestedIdentifier(query, finding) { return false } + if ticketSearchQuery(query) && !ticketFindingMatchesDestination(query, finding) { + return false + } if !nearestFlightQuery(query) { return true } @@ -1847,6 +1850,20 @@ func researchFindingMatchesIntent(query string, finding epistemics.ResearchFindi return true } +func ticketSearchQuery(query string) bool { + return interactiondomain.TicketSearchDestination(query) != "" +} + +func ticketFindingMatchesDestination(query string, finding epistemics.ResearchFinding) bool { + destination := strings.ToLower(interactiondomain.TicketSearchDestination(query)) + if destination == "" { + return true + } + // The source title or URL may describe a category; human-facing proof must + // mention the requested destination in the actual displayed excerpt. + return strings.Contains(strings.ToLower(finding.Excerpt.Text), destination) +} + // findingContainsRequestedIdentifier prevents a generic landing page from // being presented as a result for a concrete model, article or brand request. // Identifiers are detected by their syntax, not by a catalogue of providers or diff --git a/cmd/core-runtime/main_test.go b/cmd/core-runtime/main_test.go index ca43c70..7ce510a 100644 --- a/cmd/core-runtime/main_test.go +++ b/cmd/core-runtime/main_test.go @@ -58,6 +58,18 @@ func TestNearestFlightEvidenceRequiresBothRouteEndpointsWhenKnown(t *testing.T) } } +func TestTicketReplyRejectsGenericCategoryPageWithoutDestination(t *testing.T) { + query := "найди билеты до Ялты сегодня" + generic := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Купить билеты", Locator: "https://tickets.example"}, Excerpt: epistemics.SourceExcerpt{Text: "Поиск авиабилетов среди множества направлений."}} + if researchFindingMatchesIntent(query, generic) { + t.Fatal("generic ticket category was presented as destination result") + } + relevant := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Билеты"}, Excerpt: epistemics.SourceExcerpt{Text: "Билеты до Ялты: варианты на сегодня."}} + if !researchFindingMatchesIntent(query, relevant) { + t.Fatal("destination-specific ticket result was rejected") + } +} + func TestResearchReplyRejectsGenericLandingPageForConcreteIdentifier(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 d88f309..efb076f 100644 --- a/modules/interaction/domain/semantics_test.go +++ b/modules/interaction/domain/semantics_test.go @@ -85,6 +85,15 @@ func TestLocalSearchClarification(t *testing.T) { } } +func TestTicketSearchDestinationUsesRequestDestination(t *testing.T) { + if got := TicketSearchDestination("найди билеты до Ялты сегодня"); got != "Ялты" { + t.Fatalf("ticket destination = %q", got) + } + if got := TicketSearchDestination("найди билеты"); got != "" { + t.Fatalf("destination invented without one: %q", got) + } +} + func TestContinueLocalSearch(t *testing.T) { history := []interaction.HumanUtterance{{Content: "Найди ближайший ресторан"}} if got := ContinueLocalSearch("в Сочи", history); got != "Найди ближайший ресторан в Сочи" { diff --git a/modules/interaction/domain/travel.go b/modules/interaction/domain/travel.go index bd35504..53d5765 100644 --- a/modules/interaction/domain/travel.go +++ b/modules/interaction/domain/travel.go @@ -236,6 +236,16 @@ func ContinueTravelDate(message string, history []interaction.HumanUtterance) st return strings.TrimSpace(latest + " " + current) } +// TicketSearchDestination extracts the destination from a ticket request +// without assigning any city or travel-provider meaning in core code. +func TicketSearchDestination(message string) string { + tokens := semanticTokens(StripAssistantVocative(message)) + if !containsStemInTokens(tokens, "билет") { + return "" + } + return travelContextDestinationFromTokens(tokens) +} + // 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 {