Remember assistant name preferences in chat #294

Merged
m merged 2 commits from codex/chat-semantic-inventory into main 2026-08-24 23:21:17 +00:00
5 changed files with 114 additions and 5 deletions
+24 -5
View File
@@ -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 := ""
+62
View File
@@ -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.
+13
View File
@@ -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 {
+5
View File
@@ -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 "Смысл сообщения пока понят недостаточно надёжно. Я сохраню его и при доступности более широкого когнитивного разбора не буду притворяться, что это уже проверенный факт."
@@ -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 {