From eefd54137c7123e8a0e67a05f30708a6b9083a12 Mon Sep 17 00:00:00 2001 From: Mikhail Date: Tue, 25 Aug 2026 02:16:15 +0300 Subject: [PATCH] Remember assistant name preferences in chat --- cmd/core-runtime/main.go | 29 +++++++-- modules/interaction/domain/chat.go | 62 ++++++++++++++++++++ modules/interaction/domain/chat_test.go | 13 ++++ modules/interaction/domain/semantics.go | 5 ++ modules/interaction/domain/semantics_test.go | 10 ++++ 5 files changed, 114 insertions(+), 5 deletions(-) diff --git a/cmd/core-runtime/main.go b/cmd/core-runtime/main.go index af51fd5..a6060fc 100644 --- a/cmd/core-runtime/main.go +++ b/cmd/core-runtime/main.go @@ -510,13 +510,17 @@ func main() { } var effectiveTaskMessage = utterance.Content var previousTask *interaction.HumanUtterance + var conversationHistory []interaction.HumanUtterance + if request.ConversationID != "" { + conversationHistory, _ = interactionRepository.Utterances(r.Context(), conversation.ID) + } if request.ConversationID != "" && interactiondomain.IsRetryRequest(utterance.Content) { - if history, historyErr := interactionRepository.Utterances(r.Context(), conversation.ID); historyErr == nil && len(history) > 0 { - for index := len(history) - 1; index >= 0; index-- { - if interactiondomain.IsRetryRequest(history[index].Content) { + if len(conversationHistory) > 0 { + for index := len(conversationHistory) - 1; index >= 0; index-- { + if interactiondomain.IsRetryRequest(conversationHistory[index].Content) { continue } - candidate := history[index] + candidate := conversationHistory[index] previousTask = &candidate effectiveTaskMessage = candidate.Content break @@ -534,6 +538,21 @@ func main() { semantic = interactiondomain.CarryForwardPreviousTask(semantic, previousSemantic) } } + assistantName := interactiondomain.PreferredAssistantName(utterance.Content) + if assistantName == "" { + for index := len(conversationHistory) - 1; index >= 0; index-- { + if name := interactiondomain.PreferredAssistantName(conversationHistory[index].Content); name != "" { + assistantName = name + break + } + } + } + identityReply := "" + if interactiondomain.IsIdentityInquiry(utterance.Content) && assistantName != "" { + identityReply = "Меня зовут " + assistantName + ". Я Домовой, ваш помощник." + semantic.Reply = identityReply + semantic.NeedsCognition = false + } routePlan := buildChatRoutePlan(semantic) cognitiveAttempted := false cognitiveAvailable := true @@ -589,7 +608,7 @@ func main() { activeFocusName = previousFocus.ProjectDisplayName activeFocusProvenance = previousFocus.Provenance } - var planningReply string + planningReply := identityReply workPlanned := false needsCognition := semantic.NeedsCognition projectID := "" diff --git a/modules/interaction/domain/chat.go b/modules/interaction/domain/chat.go index 9c12838..ae67072 100644 --- a/modules/interaction/domain/chat.go +++ b/modules/interaction/domain/chat.go @@ -113,6 +113,8 @@ func FallbackText(act interaction.SpeechAct) string { return "Я записал это как утверждение человека. Это не считается проверенным фактом." case interaction.SpeechActCorrection: return "Исправление записано отдельной репликой; история не переписана." + case interaction.SpeechActPreference: + return "Хорошо, учту это дальше." case interaction.SpeechActQuestion, interaction.SpeechActRequestToInvestigate: return "Когнитивный маршрут сейчас недоступен, но сообщение сохранено и может быть обработано позже." default: @@ -120,6 +122,47 @@ func FallbackText(act interaction.SpeechAct) string { } } +// PreferredAssistantName extracts a name the human explicitly assigns to the +// assistant. It is intentionally based on naming intent and nearby words, not +// on a whole-message comparison, so it remains useful with ordinary variations +// of the same request. +func PreferredAssistantName(message string) string { + tokens := semanticTokens(message) + for index, token := range tokens { + if !containsStem(token, "называ", "зов", "имен") { + continue + } + for _, candidate := range tokens[index+1:] { + if candidate == "тебя" || candidate == "тебе" || candidate == "себя" || candidate == "меня" { + continue + } + if candidate == "буду" || candidate == "будет" || candidate == "запомни" || candidate == "помни" || candidate == "ты" || candidate == "это" { + continue + } + if len([]rune(candidate)) < 2 || containsStem(candidate, "тогда", "дальше", "пожалуйста") { + break + } + runes := []rune(candidate) + runes[0] = []rune(strings.ToUpper(string(runes[0])))[0] + return string(runes) + } + } + return "" +} + +// IsIdentityInquiry identifies a request about the assistant itself by its +// interrogative meaning. Punctuation is optional and wording may vary. +func IsIdentityInquiry(message string) bool { + tokens := semanticTokens(message) + if len(tokens) == 0 { + return false + } + if containsToken(tokens, "кто") && (containsToken(tokens, "ты") || containsToken(tokens, "вы") || containsToken(tokens, "тебя")) { + return true + } + return containsToken(tokens, "зовут") || containsToken(tokens, "имя") +} + func BuildDeterministicCandidate(message string, corrects interaction.HumanUtteranceID, millis int64) interaction.SemanticCandidate { id, _ := ids.New() tokens := semanticTokens(message) @@ -247,6 +290,10 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo arithmeticMarker = true } } + if preferredNameFromTokens(tokens) { + scores[interaction.SpeechActPreference] += 8 + scores[interaction.SpeechActAssertion] = 0 + } if len(tokens) >= 2 && tokens[0] == "я" && containsStemInTokens(tokens[1:], "настро", "сдела", "исправ", "удал", "созда", "перезапуст", "поменя", "добави") { scores[interaction.SpeechActAssertion] += 5 } @@ -300,6 +347,21 @@ func semanticScores(tokens []string, hasQuestionMark bool, explicitCorrection bo return scores } +func preferredNameFromTokens(tokens []string) bool { + for index, token := range tokens { + if !containsStem(token, "называ", "зов", "имен") { + continue + } + for _, candidate := range tokens[index+1:] { + if candidate == "тебя" || candidate == "тебе" || candidate == "себя" || candidate == "меня" || candidate == "буду" || candidate == "запомни" || candidate == "помни" || candidate == "ты" || candidate == "это" { + continue + } + return len([]rune(candidate)) >= 2 + } + } + return false +} + // IsRetryRequest identifies a short continuation that asks Domovoy to repeat // the previous task. It deliberately carries no subject-specific knowledge; // the conversation owner supplies the previous task when available. diff --git a/modules/interaction/domain/chat_test.go b/modules/interaction/domain/chat_test.go index 75c49ef..697fd58 100644 --- a/modules/interaction/domain/chat_test.go +++ b/modules/interaction/domain/chat_test.go @@ -187,6 +187,19 @@ func TestRetryRequestIsAContinuationCue(t *testing.T) { } } +func TestNamingPreferenceIsSemanticAndProducesNaturalAcceptance(t *testing.T) { + result := BuildDeterministicCandidate("я буду называть тебя Дед, запомни ты дед", "", time.Unix(10, 0).UnixMilli()) + if result.PrimarySpeechAct != interaction.SpeechActPreference { + t.Fatalf("expected naming preference, got %+v", result) + } + if name := PreferredAssistantName("я буду называть тебя Дед, запомни ты дед"); name != "Дед" { + t.Fatalf("expected extracted assistant name, got %q", name) + } + if !IsIdentityInquiry("ты кто?") || !IsIdentityInquiry("как тебя зовут") { + t.Fatal("identity variants should be recognized") + } +} + func TestDeterministicCandidateKeepsFirstPersonCompletedWorkAsAssertion(t *testing.T) { candidate := BuildDeterministicCandidate("Я всё уже настроил.", "", time.Unix(10, 0).UnixMilli()) if candidate.PrimarySpeechAct != interaction.SpeechActAssertion { diff --git a/modules/interaction/domain/semantics.go b/modules/interaction/domain/semantics.go index 1fce210..ef23fce 100644 --- a/modules/interaction/domain/semantics.go +++ b/modules/interaction/domain/semantics.go @@ -307,6 +307,11 @@ func semanticReply(message string, result SemanticResult, previousFocus *situati return "Фокус переключён: " + result.ActiveFocus.ProjectDisplayName + "." } switch result.Interpretation.SpeechAct { + case interaction.SpeechActPreference: + if name := PreferredAssistantName(message); name != "" { + return "Хорошо, зови меня " + name + "." + } + return "Хорошо, учту это дальше." case interaction.SpeechActAssertion: if result.Interpretation.Applicability == interaction.SemanticNeedsBroaderInterpret || result.Interpretation.Applicability == interaction.SemanticLowConfidence { return "Смысл сообщения пока понят недостаточно надёжно. Я сохраню его и при доступности более широкого когнитивного разбора не буду притворяться, что это уже проверенный факт." diff --git a/modules/interaction/domain/semantics_test.go b/modules/interaction/domain/semantics_test.go index 8adf778..9956369 100644 --- a/modules/interaction/domain/semantics_test.go +++ b/modules/interaction/domain/semantics_test.go @@ -33,6 +33,16 @@ func TestInterpretUtteranceAssertionIsNotVerifiedFact(t *testing.T) { } } +func TestInterpretUtteranceNamingPreferenceIsAccepted(t *testing.T) { + result := interpretForTest(t, "я буду называть тебя Дед, запомни ты дед", nil, "") + if result.Interpretation.SpeechAct != interaction.SpeechActPreference { + t.Fatalf("expected preference, got %s", result.Interpretation.SpeechAct) + } + if result.Reply != "Хорошо, зови меня Дед." { + t.Fatalf("unexpected preference reply: %q", result.Reply) + } +} + func TestInterpretUtteranceDirectiveRecordsOutcomeWithoutExecution(t *testing.T) { result := interpretForTest(t, "Поменяй базу на УПО1.", activeFocus("upo", "УПО"), "") if result.Interpretation.SpeechAct != interaction.SpeechActDirective { -- 2.54.0