diff --git a/modules/interaction/domain/chat_test.go b/modules/interaction/domain/chat_test.go index 9d89591..30a41e0 100644 --- a/modules/interaction/domain/chat_test.go +++ b/modules/interaction/domain/chat_test.go @@ -214,11 +214,17 @@ func TestDecomposeUsesRememberedAliasesAcrossWordings(t *testing.T) { func TestTaskLexiconFromHistoryFeedsSearchQueryWithoutCoreAlias(t *testing.T) { history := []interaction.HumanUtterance{{Content: "запомни: WB — это Wildberries, источник для поиска"}} lexicon := TaskLexiconFromHistory(history) - if got := SearchQueryWithLexicon("найди на WB бренд marka-wear", lexicon); got != "найди на WB бренд marka-wear wildberries" { + if got := SearchQueryWithLexicon("найди на WB бренд marka-wear", lexicon); got != "на WB бренд marka-wear wildberries" { t.Fatalf("learned source was not applied to search: %q", got) } } +func TestSearchQueryWithoutLearnedSourceOmitsImperative(t *testing.T) { + if got := SearchQueryWithLexicon("найди на WB бренд marka-wear", TaskLexicon{}); got != "на WB бренд marka-wear" { + t.Fatalf("search imperative leaked into external query: %q", got) + } +} + func TestDecomposeCreativeRequestDoesNotInventResearch(t *testing.T) { lexicon := TaskLexicon{ObjectAliases: []TaskLexiconEntry{{Alias: "медвед", Value: "IMAGE"}}} got := DecomposeTaskWithLexicon("нарисуй бурого медведя", lexicon) diff --git a/modules/interaction/domain/task_decomposition.go b/modules/interaction/domain/task_decomposition.go index 6890c3f..ccdd313 100644 --- a/modules/interaction/domain/task_decomposition.go +++ b/modules/interaction/domain/task_decomposition.go @@ -72,19 +72,38 @@ func TaskLexiconFromHistory(history []interaction.HumanUtterance) TaskLexicon { return lexicon } -// SearchQueryWithLexicon keeps a learned source in the external query without -// turning its alias into core vocabulary. +// SearchQueryWithLexicon prepares a search-engine query without turning a +// learned source alias into core vocabulary. The imperative that expresses the +// user's intent is deliberately omitted: search engines must receive the +// subject of the request, not mistake a Russian command such as "найди" for a +// brand or site name. func SearchQueryWithLexicon(message string, lexicon TaskLexicon) string { decomposition := DecomposeTaskWithLexicon(message, lexicon) if decomposition.Intent == TaskIntentSearch && decomposition.Source != "" { if decomposition.Target != "" { return decomposition.Target + " " + decomposition.Source } - return strings.TrimSpace(message) + " " + decomposition.Source + return searchQueryWithoutImperative(message) + " " + decomposition.Source + } + if decomposition.Intent == TaskIntentSearch { + return searchQueryWithoutImperative(message) } return strings.TrimSpace(message) } +func searchQueryWithoutImperative(message string) string { + cleaned := strings.TrimSpace(message) + fields := strings.Fields(cleaned) + if len(fields) < 2 { + return cleaned + } + first := strings.Trim(strings.ToLower(fields[0]), ".,!?:;") + if !containsStem(first, "найд", "поищ", "подбер", "проверь") { + return cleaned + } + return strings.TrimSpace(strings.TrimPrefix(cleaned, fields[0])) +} + // DecomposeTaskWithLexicon performs a cheap semantic pass. The lexicon is // intentionally an input: a remembered alias may be revalidated or replaced // without changing the parser or shipping a new hard-coded branch.