Apply learned source aliases to research #378

Merged
m merged 1 commits from codex/household-travel-query-fix into main 2026-08-25 18:02:30 +00:00
3 changed files with 58 additions and 1 deletions
+3 -1
View File
@@ -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"},
+8
View File
@@ -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)
@@ -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:
// "запомни: <alias> — это <source>, источник для поиска".
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.