Compare commits
7
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e54ace76b | ||
|
|
f329224d12 | ||
|
|
bb76171af4 | ||
|
|
213f06661d | ||
|
|
d350b9a565 | ||
|
|
f862c3ac13 | ||
|
|
5e18eb6082 |
@@ -1810,6 +1810,12 @@ func renderResearchReply(query string, result epistemics.ResearchResult, researc
|
||||
excerpt = string([]rune(excerpt)[:limit]) + "…"
|
||||
}
|
||||
lines = append(lines, excerpt+"\nИсточник: "+researchSourceLabel(finding))
|
||||
// A nearest-flight request needs one route-relevant schedule result.
|
||||
// Appending weaker matches makes unrelated category pages look like
|
||||
// alternatives even after the relevance check has accepted the route.
|
||||
if nearestFlightQuery(query) {
|
||||
break
|
||||
}
|
||||
if len(lines) == 2 {
|
||||
break
|
||||
}
|
||||
@@ -1857,6 +1863,9 @@ func researchFindingMatchesIntent(query string, finding epistemics.ResearchFindi
|
||||
if distanceResearchQuery(query) && !containsDistanceEvidence(finding.Excerpt.Text) {
|
||||
return false
|
||||
}
|
||||
if localPlaceSearchQuery(query) && !containsLocalPlaceEvidence(finding.Excerpt.Text) {
|
||||
return false
|
||||
}
|
||||
if !nearestFlightQuery(query) {
|
||||
return true
|
||||
}
|
||||
@@ -1886,6 +1895,22 @@ func researchFindingMatchesIntent(query string, finding epistemics.ResearchFindi
|
||||
return true
|
||||
}
|
||||
|
||||
func localPlaceSearchQuery(query string) bool {
|
||||
lower := strings.ToLower(query)
|
||||
return (strings.Contains(lower, "ресторан") || strings.Contains(lower, "кафе")) &&
|
||||
(strings.Contains(lower, "адрес") || strings.Contains(lower, "отзыв") || strings.Contains(lower, "меню"))
|
||||
}
|
||||
|
||||
func containsLocalPlaceEvidence(value string) bool {
|
||||
lower := strings.ToLower(value)
|
||||
for _, term := range []string{"ресторан", "кафе", "адрес", "меню", "отзыв", "кухн"} {
|
||||
if strings.Contains(lower, term) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func distanceResearchQuery(query string) bool {
|
||||
lower := strings.ToLower(query)
|
||||
return strings.Contains(lower, "расстоян") || strings.Contains(lower, "километр")
|
||||
@@ -2029,7 +2054,12 @@ func stableKnowledgeAnswer(answer string) bool {
|
||||
// completed entirely in the reply. It excludes specialized routes and durable
|
||||
// work, so cognition can produce content but cannot stand in for an executor.
|
||||
func shouldGenerateNonMutatingTextDirective(semantic interactiondomain.SemanticResult, routes []chatRoute, helpPath work.HelpPath, creativeRequest bool) bool {
|
||||
if semantic.Interpretation.SpeechAct != interaction.SpeechActDirective || creativeRequest || helpPath.Kind != "" || helpPath.NeedsDurableWork {
|
||||
if semantic.Interpretation.SpeechAct != interaction.SpeechActDirective || creativeRequest || helpPath.NeedsDurableWork {
|
||||
return false
|
||||
}
|
||||
// ANSWER and COGNITION are reply-only help paths. They do not execute an
|
||||
// external action, unlike direct capabilities, gaps or durable work.
|
||||
if helpPath.Kind != work.HelpPathAnswer && helpPath.Kind != work.HelpPathCognition && helpPath.Kind != "" {
|
||||
return false
|
||||
}
|
||||
return !hasChatRoute(routes, chatRouteResearch) && !hasChatRoute(routes, chatRouteMedia) && !hasChatRoute(routes, chatRouteAgent)
|
||||
|
||||
@@ -47,6 +47,17 @@ func TestNearestFlightReplyFiltersUnrelatedSearchSnippets(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNearestFlightReplyDoesNotAppendWeakerSecondMatch(t *testing.T) {
|
||||
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{
|
||||
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://schedule.example", Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание авиарейсов Москва — Казань, время вылета и прилёта."}},
|
||||
{State: epistemics.FindingSupported, Source: epistemics.Source{Locator: "https://category.example", Title: "Категория направлений"}, Excerpt: epistemics.SourceExcerpt{Text: "Список близлежащих городов и ориентировочные цены."}},
|
||||
}}
|
||||
reply, backed := renderResearchReply("ближайшие авиарейсы Москва Казань сегодня", result, nil)
|
||||
if !backed || !strings.Contains(reply, "Рейсы Москва") || strings.Contains(reply, "Категория направлений") {
|
||||
t.Fatalf("weaker second finding leaked into nearest-flight reply: %q", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNearestFlightEvidenceRequiresBothRouteEndpointsWhenKnown(t *testing.T) {
|
||||
query := "ближайшие авиарейсы Москва Казань сегодня"
|
||||
matched := epistemics.ResearchFinding{Source: epistemics.Source{Title: "Рейсы Москва — Казань"}, Excerpt: epistemics.SourceExcerpt{Text: "Расписание авиарейсов из Москвы в Казань."}}
|
||||
@@ -95,6 +106,18 @@ func TestResearchReplyRejectsGenericLandingPageForConcreteIdentifier(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestLocalPlaceReplyRejectsDictionaryFinding(t *testing.T) {
|
||||
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
|
||||
State: epistemics.FindingSupported,
|
||||
Source: epistemics.Source{Locator: "https://dictionary.example", Title: "Ближайший — словарь"},
|
||||
Excerpt: epistemics.SourceExcerpt{Text: "Ближайший — значение слова и пример употребления."},
|
||||
}}}
|
||||
reply, backed := renderResearchReply("места ресторан в Сочи адрес отзывы меню", result, nil)
|
||||
if backed || !strings.Contains(reply, "Проверяемых данных") {
|
||||
t.Fatalf("dictionary finding was presented as local place result: %q", reply)
|
||||
}
|
||||
}
|
||||
|
||||
func TestResearchReplyKeepsFindingContainingConcreteIdentifier(t *testing.T) {
|
||||
result := epistemics.ResearchResult{Findings: []epistemics.ResearchFinding{{
|
||||
State: epistemics.FindingSupported,
|
||||
@@ -151,6 +174,12 @@ func TestNonMutatingTextDirectiveCanUseCognitiveReply(t *testing.T) {
|
||||
if !shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{}, false) {
|
||||
t.Fatal("ordinary formatting directive was not eligible for a cognitive reply")
|
||||
}
|
||||
if !shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{Kind: work.HelpPathCognition}, false) {
|
||||
t.Fatal("reply-only cognition path was incorrectly rejected")
|
||||
}
|
||||
if shouldGenerateNonMutatingTextDirective(semantic, nil, work.HelpPath{Kind: work.HelpPathDirectCapability}, false) {
|
||||
t.Fatal("direct capability was incorrectly replaced by a textual reply")
|
||||
}
|
||||
if shouldGenerateNonMutatingTextDirective(semantic, []chatRoute{{Kind: chatRouteMedia}}, work.HelpPath{}, false) {
|
||||
t.Fatal("media route was incorrectly replaced by a textual reply")
|
||||
}
|
||||
|
||||
@@ -189,6 +189,13 @@ func TestTravelResearchQueryUsesKnownOriginForDistance(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestTravelResearchQueryCompilesLocalPlaceSearchWithoutSearchImperative(t *testing.T) {
|
||||
query := TravelResearchQuery("найди ближайший ресторан в Сочи", nil, time.Time{})
|
||||
if strings.Contains(query, "найди") || strings.Contains(query, "ближайший") || !strings.Contains(query, "сочи") || !strings.Contains(query, "адрес") {
|
||||
t.Fatalf("local place query was not compiled semantically: %q", query)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTravelRouteWithKnownOriginDoesNotAskForTicketDate(t *testing.T) {
|
||||
history := []interaction.HumanUtterance{{Content: "я сейчас в Москве"}}
|
||||
if got := TravelSearchClarification("как можно добраться до Сочи?", history); got != "" {
|
||||
|
||||
@@ -46,7 +46,9 @@ func normalizedTravelSearchText(message string) string {
|
||||
func TravelResearchQuery(message string, history []interaction.HumanUtterance, now time.Time) string {
|
||||
query := TravelSearchQuery(message, now)
|
||||
tokens := semanticTokens(StripAssistantVocative(message))
|
||||
if containsStemInTokens(tokens, "расстоян", "километр") {
|
||||
if localPlaceSearchRequest(tokens) {
|
||||
return localPlaceResearchQuery(tokens)
|
||||
} else if containsStemInTokens(tokens, "расстоян", "километр") {
|
||||
origin := travelContextOrigin(history)
|
||||
destination := travelContextDestinationFromTokens(tokens)
|
||||
if origin != "" && destination != "" {
|
||||
@@ -86,6 +88,23 @@ func TravelResearchQuery(message string, history []interaction.HumanUtterance, n
|
||||
return query
|
||||
}
|
||||
|
||||
// localPlaceSearchRequest recognizes a proximity search as a local-search
|
||||
// capability, keeping it separate from travel-route compilation.
|
||||
func localPlaceSearchRequest(tokens []string) bool {
|
||||
return containsStemInTokens(tokens, "ресторан", "кафе") && containsStemInTokens(tokens, "ближ", "рядом")
|
||||
}
|
||||
|
||||
func localPlaceResearchQuery(tokens []string) string {
|
||||
parts := make([]string, 0, len(tokens))
|
||||
for _, token := range tokens {
|
||||
if containsStem(token, "найд", "покаж", "ближ", "рядом") {
|
||||
continue
|
||||
}
|
||||
parts = append(parts, token)
|
||||
}
|
||||
return "места " + strings.Join(parts, " ") + " адрес отзывы меню"
|
||||
}
|
||||
|
||||
// trainScheduleResearchQuery formulates an observed schedule request from the
|
||||
// user's route and remembered origin. It contains no city, provider or answer
|
||||
// catalogue: actual departure data remains evidence from the selected source.
|
||||
|
||||
Reference in New Issue
Block a user