Validate flight research by route meaning #384
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user