diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index c61a1fc..b195ad4 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -792,7 +792,9 @@ func main() { replies = append(replies, "По запросу «"+part+"»: "+clarification) continue } - searchQuery := interactiondomain.TravelResearchQuery(part, conversationHistory, now) + learnedLexicon := interactiondomain.TaskLexiconFromHistory(conversationHistory) + searchQuery := interactiondomain.SearchQueryWithLexicon(part, learnedLexicon) + searchQuery = interactiondomain.TravelResearchQuery(searchQuery, conversationHistory, now) researchCtx, cancelResearch := context.WithTimeout(r.Context(), 8*time.Second) result, researchErr := researchClient.SearchWeb(researchCtx, epistemics.WebSearchRequest{ Plan: epistemics.ResearchPlan{ID: epistemics.ResearchPlanID(fmt.Sprintf("plan:%s:%d", planID, index)), Question: searchQuery, SourceClasses: []string{"web-search"}, FreshnessPolicy: "current"}, diff --git a/modules/interaction/domain/chat_test.go b/modules/interaction/domain/chat_test.go index 6038a19..bbd5d99 100644 --- a/modules/interaction/domain/chat_test.go +++ b/modules/interaction/domain/chat_test.go @@ -204,6 +204,14 @@ 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" { + t.Fatalf("learned source was not applied to search: %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 b55e2e9..6890c3f 100644 --- a/modules/interaction/domain/task_decomposition.go +++ b/modules/interaction/domain/task_decomposition.go @@ -2,6 +2,8 @@ package domain import "strings" +import "git.cin.su/m/Assistant/contracts/interaction" + // TaskDecomposition is the small, typed intermediate representation used // before a capability is called. It separates the user's goal from the // object, source and target instead of treating the whole sentence as a @@ -38,6 +40,51 @@ func DecomposeTask(message string) TaskDecomposition { return DecomposeTaskWithLexicon(message, TaskLexicon{}) } +// TaskLexiconFromHistory rebuilds learned search-source aliases from ordinary +// human utterances. The fact remains user-owned conversational memory; core +// code never embeds mutable marketplace or provider names. +// +// A source fact has the deliberately explicit form: +// "запомни: — это , источник для поиска". +func TaskLexiconFromHistory(history []interaction.HumanUtterance) TaskLexicon { + lexicon := TaskLexicon{} + for _, utterance := range history { + message := strings.TrimSpace(utterance.Content) + lower := strings.ToLower(message) + if !strings.Contains(lower, "источник") || !strings.Contains(lower, "поиск") || !strings.Contains(lower, "это") { + continue + } + before, after, found := strings.Cut(lower, "это") + if !found { + continue + } + alias := strings.TrimSpace(strings.TrimPrefix(before, "запомни:")) + alias = strings.Trim(alias, " \t—-:;,") + value := strings.TrimSpace(after) + if index := strings.IndexAny(value, ",;."); index >= 0 { + value = strings.TrimSpace(value[:index]) + } + if len([]rune(alias)) < 2 || len([]rune(value)) < 2 { + continue + } + lexicon.SourceAliases = append(lexicon.SourceAliases, TaskLexiconEntry{Alias: alias, Value: value}) + } + return lexicon +} + +// SearchQueryWithLexicon keeps a learned source in the external query without +// turning its alias into core vocabulary. +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 strings.TrimSpace(message) +} + // 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.