From 0b619bdc08fe8f859f9dae8cdfc5692b9fe85cdd Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 25 Aug 2026 22:35:21 +0300 Subject: [PATCH] Validate flight research by route meaning --- cmd/core-runtime/main.go | 31 ++++++++++++++++++-- cmd/core-runtime/main_test.go | 12 ++++++++ modules/interaction/domain/semantics_test.go | 14 ++++++++- modules/interaction/domain/travel.go | 6 ++-- 4 files changed, 58 insertions(+), 5 deletions(-) diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index 2725c02..32d3e5e 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -1755,13 +1755,16 @@ func renderResearchReply(query string, result epistemics.ResearchResult, researc return "Сейчас не удалось получить данные из подключённого источника. Попробую снова через доступный маршрут.", false } lines := make([]string, 0, 2) - for index, finding := range result.Findings { + for _, finding := range result.Findings { if finding.State != epistemics.FindingSupported || strings.TrimSpace(finding.Excerpt.Text) == "" || strings.TrimSpace(finding.Source.Locator) == "" { continue } + if !researchFindingMatchesIntent(query, finding) { + continue + } excerpt := humanResearchExcerpt(query, finding.Excerpt.Text) limit := 180 - if index > 0 { + if len(lines) > 0 { limit = 100 } if len([]rune(excerpt)) > limit { @@ -1800,6 +1803,30 @@ func nearestFlightQuery(query string) bool { return strings.Contains(lower, "ближ") && (strings.Contains(lower, "рейс") || strings.Contains(lower, "самолет") || strings.Contains(lower, "самолёт") || strings.Contains(lower, "вылет")) } +// researchFindingMatchesIntent is a final relevance check over evidence +// already obtained by the read-only research route. It prevents unrelated +// search-engine snippets from becoming a human-facing answer when a search +// term has a common non-travel meaning. +func researchFindingMatchesIntent(query string, finding epistemics.ResearchFinding) bool { + if !nearestFlightQuery(query) { + return true + } + evidence := strings.ToLower(strings.Join([]string{ + finding.Source.Title, + finding.Source.Publisher, + finding.Source.Locator, + finding.Statement, + finding.Excerpt.Text, + }, " ")) + return strings.Contains(evidence, "рейс") || + strings.Contains(evidence, "вылет") || + strings.Contains(evidence, "авиабил") || + strings.Contains(evidence, "аэропорт") || + strings.Contains(evidence, "самолет") || + strings.Contains(evidence, "самолёт") || + strings.Contains(evidence, "расписан") +} + func containsFlightTimeEvidence(value string) bool { return regexp.MustCompile(`(?m)\b(?:[01]?\d|2[0-3]):[0-5]\d\b`).MatchString(value) } diff --git a/cmd/core-runtime/main_test.go b/cmd/core-runtime/main_test.go index bc50d4c..508efb7 100644 --- a/cmd/core-runtime/main_test.go +++ b/cmd/core-runtime/main_test.go @@ -11,6 +11,7 @@ import ( "time" "git.cin.su/m/Assistant/contracts/capability" + "git.cin.su/m/Assistant/contracts/epistemics" "git.cin.su/m/Assistant/contracts/interaction" interactiondomain "git.cin.su/m/Assistant/modules/interaction/domain" ) @@ -34,6 +35,17 @@ func TestNearestFlightEvidenceDoesNotClaimMissingDepartureTime(t *testing.T) { } } +func TestNearestFlightReplyFiltersUnrelatedSearchSnippets(t *testing.T) { + result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{ + {State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://naidy.example", Title: "Мебельная фурнитура"}, Excerpt: epistemics.SourceExcerpt{Text: "Комплектующие для мебели оптом и в розницу."}}, + {State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://rasp.example", Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание самолётов Москва — Казань, время вылета и прилёта."}}, + }} + reply, backed := renderResearchReply("найди ближайший самолет до Казани сегодня пункт отправления: Москва", result, nil) + if !backed || !strings.Contains(reply, "Рейсы Москва") || strings.Contains(reply, "Мебельная") { + t.Fatalf("unrelated source leaked into flight reply: %q", reply) + } +} + func TestBearerTokenRequiresBearerScheme(t *testing.T) { request := httptest.NewRequest("GET", "/", nil) request.Header.Set("Authorization", "Basic value") diff --git a/modules/interaction/domain/semantics_test.go b/modules/interaction/domain/semantics_test.go index f22d48a..3e1a49c 100644 --- a/modules/interaction/domain/semantics_test.go +++ b/modules/interaction/domain/semantics_test.go @@ -200,11 +200,23 @@ func TestTravelContextUsesNewestLocationRegardlessOfRepositoryOrder(t *testing.T if got := TravelSearchClarification("какой ближайший рейс до Сочи?", history); got != "" { t.Fatalf("newest remembered origin was not applied: %q", got) } - if got := TravelResearchQuery("какой ближайший рейс до Сочи?", history, time.Time{}); !strings.Contains(got, "из Москве") { + if got := TravelResearchQuery("какой ближайший рейс до Сочи?", history, time.Time{}); !strings.Contains(got, "пункт отправления: Москве") { t.Fatalf("newest origin missing from travel research query: %q", got) } } +func TestNearestPlaneUsesTravelContextAndCurrentDay(t *testing.T) { + now := time.Date(2026, time.August, 25, 18, 0, 0, 0, time.UTC) + history := []interaction.HumanUtterance{{Content: "я сейчас в Москве", ReceivedAtUnixMillis: 100}} + query := TravelResearchQuery("найди ближайший самолет до Казани", history, now) + if !strings.Contains(query, "сегодня 25.08.2026") || !strings.Contains(query, "пункт отправления: Москве") { + t.Fatalf("nearest plane query lost current context: %q", query) + } + if got := TravelSearchClarification("найди ближайший самолет до Казани", history); got != "" { + t.Fatalf("known origin and nearest-time meaning should proceed, got %q", got) + } +} + 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 index 4713edd..6cdd0d4 100644 --- a/modules/interaction/domain/travel.go +++ b/modules/interaction/domain/travel.go @@ -48,7 +48,9 @@ func TravelResearchQuery(message string, history []interaction.HumanUtterance, n } } else if travelCue(tokens) && !containsStemInTokens(tokens, "из", "откуда", "от") { if origin := travelContextOrigin(history); origin != "" { - query += " из " + origin + // Preserve the remembered location as an explicit search parameter + // instead of guessing its grammatical form in the user's language. + query += " пункт отправления: " + origin } } return query @@ -383,7 +385,7 @@ func capitalizeWord(value string) string { } func travelCue(tokens []string) bool { - return containsStemInTokens(tokens, "билет", "рейс", "поезд", "автобус", "перелет", "перелёт") + return containsStemInTokens(tokens, "билет", "рейс", "самолет", "самолёт", "вылет", "поезд", "автобус", "перелет", "перелёт") } func travelFollowUpCue(tokens []string) bool { -- 2.54.0