Validate flight research by route meaning #384

Merged
m merged 1 commits from codex/household-travel-query-fix into main 2026-08-25 19:35:42 +00:00
4 changed files with 58 additions and 5 deletions
+29 -2
View File
@@ -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)
}
+12
View File
@@ -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")
+13 -1
View File
@@ -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 {
+4 -2
View File
@@ -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 {