Remember assistant name preferences in chat #294
@@ -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 := ""
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user