From ca45df3c624689c0f0181a167cada3acda0f6c63 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 25 Aug 2026 14:48:32 +0300 Subject: [PATCH] Load task aliases from semantic memory --- cmd/core-runtime/household_scenario_test.go | 8 ++- modules/interaction/domain/chat_test.go | 10 ++-- .../interaction/domain/task_decomposition.go | 54 +++++++++++++------ 3 files changed, 51 insertions(+), 21 deletions(-) diff --git a/cmd/core-runtime/household_scenario_test.go b/cmd/core-runtime/household_scenario_test.go index d8cae95..923fa17 100644 --- a/cmd/core-runtime/household_scenario_test.go +++ b/cmd/core-runtime/household_scenario_test.go @@ -38,8 +38,8 @@ func TestHouseholdCreativeRouteDoesNotUseResearch(t *testing.T) { func TestHouseholdMarketplaceBrandSearchUsesResearchRoute(t *testing.T) { candidate := interactiondomain.BuildDeterministicCandidate("найди на WB бренд marka-wear", "", time.Unix(10, 0).UnixMilli()) - decomposition := interactiondomain.DecomposeTask("найди на WB бренд marka-wear") - if decomposition.Intent != interactiondomain.TaskIntentSearch || decomposition.Object != interactiondomain.TaskObjectBrand || decomposition.Source != interactiondomain.TaskSourceWB || decomposition.Target != "marka-wear" || len(decomposition.Missing) != 0 { + decomposition := interactiondomain.DecomposeTaskWithLexicon("найди на WB бренд marka-wear", interactiondomain.TaskLexicon{ObjectAliases: []interactiondomain.TaskLexiconEntry{{Alias: "бренд", Value: "BRAND"}}, SourceAliases: []interactiondomain.TaskLexiconEntry{{Alias: "wb", Value: "WILDBERRIES"}}}) + if decomposition.Intent != interactiondomain.TaskIntentSearch || decomposition.Object != "BRAND" || decomposition.Source != "WILDBERRIES" || decomposition.Target != "marka-wear" || len(decomposition.Missing) != 0 { t.Fatalf("marketplace request was not split into executable atoms: %+v", decomposition) } routes := buildChatRoutePlan(interactiondomain.SemanticResult{NeedsCognition: candidate.NeedsCognition, CapabilityHints: candidate.CapabilityHints}) @@ -50,6 +50,10 @@ func TestHouseholdMarketplaceBrandSearchUsesResearchRoute(t *testing.T) { func TestHouseholdBearDrawingUsesCreativeLocalRoute(t *testing.T) { candidate := interactiondomain.BuildDeterministicCandidate("нарисуй бурого медведя", "", time.Unix(10, 0).UnixMilli()) + decomposition := interactiondomain.DecomposeTaskWithLexicon("нарисуй бурого медведя", interactiondomain.TaskLexicon{ObjectAliases: []interactiondomain.TaskLexiconEntry{{Alias: "медвед", Value: "IMAGE"}}}) + if decomposition.Intent != interactiondomain.TaskIntentCreate || decomposition.Object != "IMAGE" || decomposition.Target != "бурого медведя" || decomposition.NeedsResearch { + t.Fatalf("creative request was not split into local execution atoms: %+v", decomposition) + } for _, hint := range candidate.CapabilityHints { if capability.CapabilityKind(hint) == capability.CapabilityKindWebResearch { t.Fatalf("bear drawing selected web research: %+v", candidate) diff --git a/modules/interaction/domain/chat_test.go b/modules/interaction/domain/chat_test.go index 6a11770..ba8b914 100644 --- a/modules/interaction/domain/chat_test.go +++ b/modules/interaction/domain/chat_test.go @@ -171,8 +171,9 @@ func TestSourceDependentQuestionToleratesTyposAndPlaceSearch(t *testing.T) { } func TestDecomposeMarketplaceBrandRequestIntoExecutionAtoms(t *testing.T) { - got := DecomposeTask("найди на WB бренд marka-wear") - if got.Intent != TaskIntentSearch || got.Object != TaskObjectBrand || got.Source != TaskSourceWB || got.Target != "marka-wear" || !got.NeedsResearch { + lexicon := TaskLexicon{ObjectAliases: []TaskLexiconEntry{{Alias: "бренд", Value: "BRAND"}}, SourceAliases: []TaskLexiconEntry{{Alias: "wb", Value: "WILDBERRIES"}}} + got := DecomposeTaskWithLexicon("найди на WB бренд marka-wear", lexicon) + if got.Intent != TaskIntentSearch || got.Object != "BRAND" || got.Source != "WILDBERRIES" || got.Target != "marka-wear" || !got.NeedsResearch { t.Fatalf("unexpected marketplace decomposition: %+v", got) } if len(got.Missing) != 0 { @@ -181,8 +182,9 @@ func TestDecomposeMarketplaceBrandRequestIntoExecutionAtoms(t *testing.T) { } func TestDecomposeCreativeRequestDoesNotInventResearch(t *testing.T) { - got := DecomposeTask("нарисуй бурого медведя") - if got.Intent != TaskIntentCreate || got.Object != TaskObjectImage || got.Target != "бурого медведя" || got.NeedsResearch { + lexicon := TaskLexicon{ObjectAliases: []TaskLexiconEntry{{Alias: "медвед", Value: "IMAGE"}}} + got := DecomposeTaskWithLexicon("нарисуй бурого медведя", lexicon) + if got.Intent != TaskIntentCreate || got.Object != "IMAGE" || got.Target != "бурого медведя" || got.NeedsResearch { t.Fatalf("unexpected creative decomposition: %+v", got) } } diff --git a/modules/interaction/domain/task_decomposition.go b/modules/interaction/domain/task_decomposition.go index c0f7962..dce0036 100644 --- a/modules/interaction/domain/task_decomposition.go +++ b/modules/interaction/domain/task_decomposition.go @@ -16,18 +16,32 @@ type TaskDecomposition struct { NeedsResearch bool } +type TaskLexiconEntry struct { + Alias string + Value string +} + +type TaskLexicon struct { + ObjectAliases []TaskLexiconEntry + SourceAliases []TaskLexiconEntry +} + const ( TaskIntentSearch = "SEARCH" TaskIntentCreate = "CREATE" - TaskObjectBrand = "BRAND" - TaskObjectImage = "IMAGE" - TaskSourceWB = "WILDBERRIES" ) -// DecomposeTask performs a cheap semantic pass. It is deliberately a -// vocabulary-assisted parser, not a complete language grammar: the result is -// an execution plan seed for the appropriate agent, not the final answer. +// DecomposeTask performs the structural pass without assuming a mutable +// world vocabulary. Learned names, aliases and source meanings are supplied +// by memory through DecomposeTaskWithLexicon. func DecomposeTask(message string) TaskDecomposition { + return DecomposeTaskWithLexicon(message, TaskLexicon{}) +} + +// 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. +func DecomposeTaskWithLexicon(message string, lexicon TaskLexicon) TaskDecomposition { lower := strings.ToLower(strings.TrimSpace(message)) tokens := semanticTokens(message) result := TaskDecomposition{} @@ -38,20 +52,30 @@ func DecomposeTask(message string) TaskDecomposition { if containsStemInTokens(tokens, "нарис", "рису", "изобраз", "создай") { result.Intent = TaskIntentCreate } - if containsStemInTokens(tokens, "бренд") { - result.Object = TaskObjectBrand + for _, entry := range lexicon.ObjectAliases { + if containsStemInTokens(tokens, entry.Alias) { + result.Object = entry.Value + break + } } - if containsStemInTokens(tokens, "медвед", "мишк", "картин") { - result.Object = TaskObjectImage + for _, entry := range lexicon.SourceAliases { + if containsStemInTokens(tokens, entry.Alias) { + result.Source = entry.Value + break + } } - if containsToken(tokens, "wb") || containsStemInTokens(tokens, "wildberr", "маркетплейс") { - result.Source = TaskSourceWB + if result.Source != "" { result.NeedsResearch = result.Intent == TaskIntentSearch } - if result.Object == TaskObjectBrand { - result.Target = valueAfterCue(lower, "бренд") + if result.Object != "" { + for _, entry := range lexicon.ObjectAliases { + if entry.Value == result.Object { + result.Target = valueAfterCue(lower, entry.Alias) + break + } + } } - if result.Object == TaskObjectImage && result.Intent == TaskIntentCreate { + if result.Intent == TaskIntentCreate && result.Target == "" { result.Target = valueAfterAnyCue(lower, "нарисуй", "нарисовать", "изобрази", "создай") } if result.Intent == TaskIntentSearch && result.Source == "" {