diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index 16e9b10..0d7772a 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -824,7 +824,7 @@ func main() { PropositionKey: fmt.Sprintf("question:%s:%d", utterance.ID, index), Query: searchQuery, }) cancelResearch() - reply, backed := renderResearchReply(part, result, researchErr) + reply, backed := renderResearchReply(searchQuery, result, researchErr) if backed { researchBacked = true } @@ -1824,13 +1824,63 @@ func researchFindingMatchesIntent(query string, finding epistemics.ResearchFindi finding.Statement, finding.Excerpt.Text, }, " ")) - return strings.Contains(evidence, "рейс") || + travelEvidence := strings.Contains(evidence, "рейс") || strings.Contains(evidence, "вылет") || strings.Contains(evidence, "авиабил") || strings.Contains(evidence, "аэропорт") || strings.Contains(evidence, "самолет") || strings.Contains(evidence, "самолёт") || strings.Contains(evidence, "расписан") + if !travelEvidence { + return false + } + routeTerms := nearestFlightRouteTerms(query) + if len(routeTerms) < 2 { + return true + } + for _, term := range routeTerms { + if !strings.Contains(evidence, term) { + return false + } + } + return true +} + +// nearestFlightRouteTerms retains only the two endpoint-shaped terms from a +// structured flight search. It has no city or provider vocabulary: endpoint +// relevance is determined from the request itself. +func nearestFlightRouteTerms(query string) []string { + fields := regexp.MustCompile(`[\p{L}]{4,}`).FindAllString(strings.ToLower(query), -1) + ignored := []string{"ближай", "авиарейс", "сегодня", "самолет", "самолёт", "вылет", "рейс", "пункт", "отправлен"} + terms := make([]string, 0, 2) + for _, field := range fields { + skip := false + for _, prefix := range ignored { + if strings.HasPrefix(field, prefix) { + skip = true + break + } + } + if skip { + continue + } + prefixLength := 4 + if len([]rune(field)) < prefixLength { + prefixLength = len([]rune(field)) + } + term := string([]rune(field)[:prefixLength]) + already := false + for _, current := range terms { + if current == term { + already = true + break + } + } + if !already { + terms = append(terms, term) + } + } + return terms } func containsFlightTimeEvidence(value string) bool { diff --git a/cmd/core-runtime/main_test.go b/cmd/core-runtime/main_test.go index 1daa31f..89f2d75 100644 --- a/cmd/core-runtime/main_test.go +++ b/cmd/core-runtime/main_test.go @@ -40,12 +40,24 @@ func TestNearestFlightReplyFiltersUnrelatedSearchSnippets(t *testing.T) { {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) + reply, backed := renderResearchReply("ближайшие авиарейсы Москва Казань сегодня", result, nil) if !backed || !strings.Contains(reply, "Рейсы Москва") || strings.Contains(reply, "Мебельная") { t.Fatalf("unrelated source leaked into flight reply: %q", reply) } } +func TestNearestFlightEvidenceRequiresBothRouteEndpointsWhenKnown(t *testing.T) { + query := "ближайшие авиарейсы Москва Казань сегодня" + matched := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание авиарейсов из Москвы в Казань."}} + if !researchFindingMatchesIntent(query, matched) { + t.Fatal("route evidence with both endpoints was rejected") + } + unrelated := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Ближайшие города к Казани"}, Excerpt: epistemics.SourceExcerpt{Text: "Авиабилеты и расстояние до Казани."}} + if researchFindingMatchesIntent(query, unrelated) { + t.Fatal("one-ended travel evidence was accepted") + } +} + func TestResponseReviewPreservesAttemptedResearchOutcome(t *testing.T) { if reviewMayReplacePlanningReply(false, true, false, "Уточните аэропорт.", "Пока не смог получить проверяемый ответ.") { t.Fatal("review replaced an honest unbacked research outcome")